Files
cadcraft/crates/constraints/src/expr.rs
T
Brandon ThomasandClaude Opus 5.5 f9aa1b279a M4 annotation depth + M9 parametric constraints
Annotation (M4):
- TrueType text from system fonts (filled/outlined), shared placement for all text
- MTEXT formatting codes: fonts, heights, colours, under/overline, stacks, wrapping
- Full DIMSTYLE variables, per-dimension overrides, all arrowheads and ticks,
  unit formats, tolerances, limits, alternate units, colours by role
- Associative dimensions that follow geometry; DIMREASSOCIATE/DIMDISASSOCIATE
- STYLE/DIMSTYLE management commands, DIMTEDIT, DIMSPACE, TABLE create/edit/merge

Parametric (M9):
- New cadcraft-constraints crate: weighted damped Gauss-Newton solver, conflict
  and redundancy detection, expressions, AutoConstrain inference
- All GC*/DC* commands, GEOMCONSTRAINT/DIMCONSTRAINT, PARAMETERS, DELCONSTRAINT,
  constraint bar / display settings; re-solve after every edit

Also: MODEL/PAPER status bar toggle, cargo xtask alias, roadmap update
(233/491 menu items, 290 commands).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 01:27:38 -04:00

325 lines
11 KiB
Rust

//! A tiny expression language for dimensional constraints and user parameters: numbers,
//! parameter names, `+ - * /`, unary minus, parentheses and the constant `pi`.
use std::collections::BTreeMap;
/// Longest expression accepted (characters).
pub const MAX_LEN: usize = 1024;
/// Deepest nesting (parentheses, unary minus) and parameter reference chain.
pub const MAX_DEPTH: usize = 64;
#[derive(Clone, Debug, PartialEq, thiserror::Error)]
pub enum ExprError {
#[error("syntax error: {0}")]
Syntax(String),
#[error("unknown parameter `{0}`")]
Unknown(String),
#[error("parameter `{0}` refers to itself")]
Cycle(String),
#[error("division by zero")]
DivZero,
#[error("expression too long or too deeply nested")]
TooDeep,
#[error("result is not a finite number")]
NotFinite,
}
#[derive(Clone, Debug, PartialEq)]
pub enum Expr {
Num(f64),
Var(String),
Neg(Box<Expr>),
Bin(char, Box<Expr>, Box<Expr>),
}
/// True for a valid parameter name: a letter or `_`, then letters, digits or `_`.
pub fn valid_name(s: &str) -> bool {
let mut c = s.chars();
matches!(c.next(), Some(ch) if ch.is_ascii_alphabetic() || ch == '_')
&& c.all(|ch| ch.is_ascii_alphanumeric() || ch == '_')
&& s.len() <= 64
&& !s.eq_ignore_ascii_case("pi")
}
struct Parser<'a> {
s: &'a [u8],
i: usize,
depth: usize,
}
impl Parser<'_> {
fn ws(&mut self) {
while self.s.get(self.i).is_some_and(|c| c.is_ascii_whitespace()) {
self.i += 1;
}
}
fn peek(&mut self) -> Option<u8> {
self.ws();
self.s.get(self.i).copied()
}
fn enter(&mut self) -> Result<(), ExprError> {
self.depth += 1;
if self.depth > MAX_DEPTH { Err(ExprError::TooDeep) } else { Ok(()) }
}
fn sum(&mut self) -> Result<Expr, ExprError> {
self.enter()?;
let mut e = self.product()?;
while let Some(c @ (b'+' | b'-')) = self.peek() {
self.i += 1;
let r = self.product()?;
e = Expr::Bin(c as char, Box::new(e), Box::new(r));
}
self.depth -= 1;
Ok(e)
}
fn product(&mut self) -> Result<Expr, ExprError> {
let mut e = self.unary()?;
while let Some(c @ (b'*' | b'/')) = self.peek() {
self.i += 1;
let r = self.unary()?;
e = Expr::Bin(c as char, Box::new(e), Box::new(r));
}
Ok(e)
}
fn unary(&mut self) -> Result<Expr, ExprError> {
match self.peek() {
Some(b'-') => {
self.i += 1;
self.enter()?;
let e = self.unary()?;
self.depth -= 1;
Ok(Expr::Neg(Box::new(e)))
}
Some(b'+') => {
self.i += 1;
self.enter()?;
let e = self.unary();
self.depth -= 1;
e
}
_ => self.atom(),
}
}
fn atom(&mut self) -> Result<Expr, ExprError> {
match self.peek() {
Some(b'(') => {
self.i += 1;
let e = self.sum()?;
if self.peek() != Some(b')') {
return Err(ExprError::Syntax("missing `)`".into()));
}
self.i += 1;
Ok(e)
}
Some(c) if c.is_ascii_digit() || c == b'.' => {
let start = self.i;
while self.s.get(self.i).is_some_and(|c| c.is_ascii_digit() || *c == b'.') {
self.i += 1;
}
// Exponent: 1e3, 2.5E-2.
if self.s.get(self.i).is_some_and(|c| *c == b'e' || *c == b'E') {
let save = self.i;
self.i += 1;
if self.s.get(self.i).is_some_and(|c| *c == b'+' || *c == b'-') {
self.i += 1;
}
if self.s.get(self.i).is_some_and(u8::is_ascii_digit) {
while self.s.get(self.i).is_some_and(u8::is_ascii_digit) {
self.i += 1;
}
} else {
self.i = save;
}
}
let t = std::str::from_utf8(self.s.get(start..self.i).unwrap_or_default()).unwrap_or_default();
let v: f64 = t.parse().map_err(|_| ExprError::Syntax(format!("bad number `{t}`")))?;
Ok(Expr::Num(v))
}
Some(c) if c.is_ascii_alphabetic() || c == b'_' => {
let start = self.i;
while self.s.get(self.i).is_some_and(|c| c.is_ascii_alphanumeric() || *c == b'_') {
self.i += 1;
}
let t = std::str::from_utf8(self.s.get(start..self.i).unwrap_or_default()).unwrap_or_default();
if t.eq_ignore_ascii_case("pi") {
return Ok(Expr::Num(std::f64::consts::PI));
}
Ok(Expr::Var(t.to_string()))
}
Some(c) => Err(ExprError::Syntax(format!("unexpected `{}`", c as char))),
None => Err(ExprError::Syntax("unexpected end".into())),
}
}
}
/// Parse an expression.
pub fn parse(text: &str) -> Result<Expr, ExprError> {
if text.len() > MAX_LEN {
return Err(ExprError::TooDeep);
}
if text.trim().is_empty() {
return Err(ExprError::Syntax("empty expression".into()));
}
let mut p = Parser { s: text.as_bytes(), i: 0, depth: 0 };
let e = p.sum()?;
if p.peek().is_some() {
return Err(ExprError::Syntax(format!("unexpected `{}`", text.get(p.i..).unwrap_or("").trim())));
}
Ok(e)
}
/// Names referenced by an expression.
pub fn names(e: &Expr, out: &mut Vec<String>) {
match e {
Expr::Num(_) => {}
Expr::Var(n) => {
if !out.contains(n) {
out.push(n.clone());
}
}
Expr::Neg(a) => names(a, out),
Expr::Bin(_, a, b) => {
names(a, out);
names(b, out);
}
}
}
/// Evaluate with a variable lookup.
pub fn eval_with(e: &Expr, lookup: &mut dyn FnMut(&str) -> Result<f64, ExprError>) -> Result<f64, ExprError> {
let v = match e {
Expr::Num(v) => *v,
Expr::Var(n) => lookup(n)?,
Expr::Neg(a) => -eval_with(a, lookup)?,
Expr::Bin(op, a, b) => {
let (x, y) = (eval_with(a, lookup)?, eval_with(b, lookup)?);
match op {
'+' => x + y,
'-' => x - y,
'*' => x * y,
_ => {
if y == 0.0 {
return Err(ExprError::DivZero);
}
x / y
}
}
}
};
if v.is_finite() { Ok(v) } else { Err(ExprError::NotFinite) }
}
/// Evaluate an expression that uses no names.
pub fn eval_const(text: &str) -> Result<f64, ExprError> {
eval_with(&parse(text)?, &mut |n| Err(ExprError::Unknown(n.to_string())))
}
/// A set of named expressions (user parameters and dimensional constraint names), evaluated
/// lazily with cycle detection.
#[derive(Clone, Debug, Default)]
pub struct Env {
exprs: BTreeMap<String, String>,
cache: BTreeMap<String, f64>,
}
impl Env {
pub fn new() -> Self {
Env::default()
}
pub fn insert(&mut self, name: &str, expr: &str) {
self.exprs.insert(name.to_string(), expr.to_string());
self.cache.clear();
}
pub fn contains(&self, name: &str) -> bool {
self.exprs.contains_key(name)
}
/// Value of a named parameter.
pub fn value(&mut self, name: &str) -> Result<f64, ExprError> {
let mut stack = Vec::new();
self.value_inner(name, &mut stack)
}
fn value_inner(&mut self, name: &str, stack: &mut Vec<String>) -> Result<f64, ExprError> {
if let Some(v) = self.cache.get(name) {
return Ok(*v);
}
if stack.iter().any(|s| s == name) {
return Err(ExprError::Cycle(name.to_string()));
}
if stack.len() >= MAX_DEPTH {
return Err(ExprError::TooDeep);
}
let text = self.exprs.get(name).cloned().ok_or_else(|| ExprError::Unknown(name.to_string()))?;
let e = parse(&text)?;
stack.push(name.to_string());
let r = eval_with(&e, &mut |n| self.value_inner(n, stack));
stack.pop();
let v = r?;
self.cache.insert(name.to_string(), v);
Ok(v)
}
/// Evaluate a free expression against this environment.
pub fn eval(&mut self, text: &str) -> Result<f64, ExprError> {
let e = parse(text)?;
let mut stack = Vec::new();
eval_with(&e, &mut |n| self.value_inner(n, &mut stack))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn arithmetic() {
assert_eq!(eval_const("1+2*3").unwrap(), 7.0);
assert_eq!(eval_const("(1+2)*3").unwrap(), 9.0);
assert_eq!(eval_const("-2*-3").unwrap(), 6.0);
assert_eq!(eval_const("10/4").unwrap(), 2.5);
assert_eq!(eval_const("1e2+.5").unwrap(), 100.5);
assert_eq!(eval_const("8-2-1").unwrap(), 5.0);
assert!((eval_const("pi").unwrap() - std::f64::consts::PI).abs() < 1e-15);
}
#[test]
fn errors() {
assert_eq!(eval_const("1/0"), Err(ExprError::DivZero));
assert!(matches!(eval_const("1+"), Err(ExprError::Syntax(_))));
assert!(matches!(eval_const("(1"), Err(ExprError::Syntax(_))));
assert!(matches!(eval_const("1 2"), Err(ExprError::Syntax(_))));
assert!(matches!(eval_const("x"), Err(ExprError::Unknown(_))));
assert!(matches!(eval_const(""), Err(ExprError::Syntax(_))));
assert!(matches!(eval_const("1e308*1e308"), Err(ExprError::NotFinite)));
assert!(matches!(eval_const(&"(".repeat(500)), Err(ExprError::TooDeep)));
assert!(matches!(eval_const(&"-".repeat(500)), Err(ExprError::TooDeep)));
assert!(matches!(eval_const(&"1+".repeat(600)), Err(ExprError::TooDeep)));
assert!(eval_const("1.2.3").is_err());
assert!(eval_const("é").is_err());
}
#[test]
fn env_and_cycles() {
let mut e = Env::new();
e.insert("w", "10");
e.insert("h", "w/2");
e.insert("a", "b+1");
e.insert("b", "a+1");
assert_eq!(e.value("h").unwrap(), 5.0);
assert_eq!(e.eval("w*h").unwrap(), 50.0);
assert!(matches!(e.value("a"), Err(ExprError::Cycle(_))));
assert!(matches!(e.value("zz"), Err(ExprError::Unknown(_))));
let mut chain = Env::new();
for i in 0..200 {
chain.insert(&format!("p{i}"), &format!("p{}+1", i + 1));
}
assert!(chain.value("p0").is_err());
}
#[test]
fn names_valid() {
assert!(valid_name("d1") && valid_name("_w") && !valid_name("1d") && !valid_name("a-b") && !valid_name("") && !valid_name("pi"));
let mut v = Vec::new();
names(&parse("a+b*a").unwrap(), &mut v);
assert_eq!(v, vec!["a".to_string(), "b".to_string()]);
}
}