brep_kernel/feature_pipeline/
expression.rs1use std::collections::HashMap;
29
30#[derive(Debug, Clone)]
34enum EvalValue {
35 Scalar(f64),
36 Object(HashMap<String, f64>),
37}
38
39#[derive(Debug, Clone, Default)]
43pub struct Env {
44 vars: HashMap<String, EvalValue>,
45 poison: Option<String>,
49}
50
51impl Env {
52 pub fn build(expressions: &str, configurator_json: &serde_json::Value) -> Result<Env, String> {
58 let mut env = Env::default();
59 env.vars
60 .insert("resolution".to_string(), EvalValue::Scalar(32.0));
61 env.vars.insert(
62 "configurator".to_string(),
63 EvalValue::Object(configurator_values(configurator_json)),
64 );
65 env.run_statements(expressions)?;
66 Ok(env)
67 }
68
69 pub fn poisoned(msg: String) -> Env {
73 Env {
74 poison: Some(msg),
75 ..Env::default()
76 }
77 }
78
79 pub fn eval(&self, source: &str) -> Result<f64, String> {
81 if let Some(poison) = &self.poison {
82 return Err(format!("expression environment failed to build: {poison}"));
83 }
84 let tokens = tokenize(source)?;
85 let mut parser = Parser::new(&tokens, self);
86 let value = parser.parse_expr()?;
87 parser.expect_eof()?;
88 Ok(value)
89 }
90
91 pub fn get(&self, name: &str) -> Option<f64> {
93 match self.vars.get(name) {
94 Some(EvalValue::Scalar(value)) => Some(*value),
95 _ => None,
96 }
97 }
98
99 fn run_statements(&mut self, source: &str) -> Result<(), String> {
100 let tokens = tokenize(source)?;
101 let mut index = 0;
102 while index < tokens.len() && tokens[index] != Token::Eof {
103 let name = match &tokens[index] {
105 Token::Ident(name) => name.clone(),
106 other => {
107 return Err(format!(
108 "expected an identifier at the start of a statement, found {other:?}"
109 ))
110 }
111 };
112 index += 1;
113 if tokens.get(index) != Some(&Token::Assign) {
114 return Err(format!("expected '=' after `{name}` in expression statement"));
115 }
116 index += 1;
117 let start = index;
119 while index < tokens.len()
120 && tokens[index] != Token::Semi
121 && tokens[index] != Token::Eof
122 {
123 index += 1;
124 }
125 let rhs = &tokens[start..index];
126 let mut parser = Parser::new(rhs, self);
127 let value = parser.parse_expr()?;
128 parser.expect_eof()?;
129 self.vars.insert(name, EvalValue::Scalar(value));
130 if tokens.get(index) == Some(&Token::Semi) {
132 index += 1;
133 }
134 }
135 Ok(())
136 }
137}
138
139fn configurator_values(configurator: &serde_json::Value) -> HashMap<String, f64> {
144 let source = configurator
145 .get("values")
146 .filter(|value| value.is_object())
147 .unwrap_or(configurator);
148 let mut map = HashMap::new();
149 if let Some(object) = source.as_object() {
150 for (key, value) in object {
151 if let Some(number) = value_as_f64(value) {
152 map.insert(key.clone(), number);
153 }
154 }
155 }
156 map
157}
158
159fn value_as_f64(value: &serde_json::Value) -> Option<f64> {
160 match value {
161 serde_json::Value::Number(number) => number.as_f64(),
162 serde_json::Value::String(text) => text.trim().parse::<f64>().ok(),
163 serde_json::Value::Bool(flag) => Some(if *flag { 1.0 } else { 0.0 }),
164 _ => None,
165 }
166}
167
168#[derive(Debug, Clone, PartialEq)]
173enum Token {
174 Number(f64),
175 Ident(String),
176 Plus,
177 Minus,
178 Star,
179 Slash,
180 Percent,
181 LParen,
182 RParen,
183 Dot,
184 Comma,
185 Assign,
186 Semi,
187 Eof,
188}
189
190fn tokenize(source: &str) -> Result<Vec<Token>, String> {
191 let bytes = source.as_bytes();
192 let mut tokens = Vec::new();
193 let mut i = 0;
194 while i < bytes.len() {
195 let c = bytes[i] as char;
196 if c.is_whitespace() {
197 i += 1;
198 continue;
199 }
200 if c == '/' && i + 1 < bytes.len() && bytes[i + 1] as char == '/' {
202 while i < bytes.len() && bytes[i] as char != '\n' {
203 i += 1;
204 }
205 continue;
206 }
207 match c {
208 '+' => {
209 tokens.push(Token::Plus);
210 i += 1;
211 }
212 '-' => {
213 tokens.push(Token::Minus);
214 i += 1;
215 }
216 '*' => {
217 tokens.push(Token::Star);
218 i += 1;
219 }
220 '/' => {
221 tokens.push(Token::Slash);
222 i += 1;
223 }
224 '%' => {
225 tokens.push(Token::Percent);
226 i += 1;
227 }
228 '(' => {
229 tokens.push(Token::LParen);
230 i += 1;
231 }
232 ')' => {
233 tokens.push(Token::RParen);
234 i += 1;
235 }
236 '.' if !next_is_digit(bytes, i + 1) => {
237 tokens.push(Token::Dot);
238 i += 1;
239 }
240 ',' => {
241 tokens.push(Token::Comma);
242 i += 1;
243 }
244 '=' => {
245 tokens.push(Token::Assign);
246 i += 1;
247 }
248 ';' => {
249 tokens.push(Token::Semi);
250 i += 1;
251 }
252 _ if c.is_ascii_digit() || c == '.' => {
253 let (number, next) = scan_number(bytes, i)?;
254 tokens.push(Token::Number(number));
255 i = next;
256 }
257 _ if c.is_ascii_alphabetic() || c == '_' || c == '$' => {
258 let start = i;
259 while i < bytes.len() {
260 let ch = bytes[i] as char;
261 if ch.is_ascii_alphanumeric() || ch == '_' || ch == '$' {
262 i += 1;
263 } else {
264 break;
265 }
266 }
267 tokens.push(Token::Ident(source[start..i].to_string()));
268 }
269 _ => return Err(format!("unexpected character `{c}` in expression")),
270 }
271 }
272 tokens.push(Token::Eof);
273 Ok(tokens)
274}
275
276fn next_is_digit(bytes: &[u8], i: usize) -> bool {
277 i < bytes.len() && (bytes[i] as char).is_ascii_digit()
278}
279
280fn scan_number(bytes: &[u8], start: usize) -> Result<(f64, usize), String> {
282 let mut i = start;
283 while next_is_digit(bytes, i) {
284 i += 1;
285 }
286 if i < bytes.len() && bytes[i] as char == '.' {
287 i += 1;
288 while next_is_digit(bytes, i) {
289 i += 1;
290 }
291 }
292 if i < bytes.len() && (bytes[i] as char == 'e' || bytes[i] as char == 'E') {
293 let mut j = i + 1;
294 if j < bytes.len() && (bytes[j] as char == '+' || bytes[j] as char == '-') {
295 j += 1;
296 }
297 if next_is_digit(bytes, j) {
298 i = j;
299 while next_is_digit(bytes, i) {
300 i += 1;
301 }
302 }
303 }
304 let text = std::str::from_utf8(&bytes[start..i]).map_err(|error| error.to_string())?;
305 let value = text
306 .parse::<f64>()
307 .map_err(|_| format!("invalid number literal `{text}`"))?;
308 Ok((value, i))
309}
310
311struct Parser<'a> {
316 tokens: &'a [Token],
317 pos: usize,
318 env: &'a Env,
319}
320
321impl<'a> Parser<'a> {
322 fn new(tokens: &'a [Token], env: &'a Env) -> Self {
323 Self {
324 tokens,
325 pos: 0,
326 env,
327 }
328 }
329
330 fn peek(&self) -> &Token {
331 self.tokens.get(self.pos).unwrap_or(&Token::Eof)
332 }
333
334 fn advance(&mut self) -> Token {
335 let token = self.tokens.get(self.pos).cloned().unwrap_or(Token::Eof);
336 self.pos += 1;
337 token
338 }
339
340 fn expect_eof(&self) -> Result<(), String> {
341 match self.peek() {
342 Token::Eof => Ok(()),
343 other => Err(format!("unexpected trailing token {other:?} in expression")),
344 }
345 }
346
347 fn parse_expr(&mut self) -> Result<f64, String> {
348 let mut value = self.parse_term()?;
349 loop {
350 match self.peek() {
351 Token::Plus => {
352 self.advance();
353 value += self.parse_term()?;
354 }
355 Token::Minus => {
356 self.advance();
357 value -= self.parse_term()?;
358 }
359 _ => break,
360 }
361 }
362 Ok(value)
363 }
364
365 fn parse_term(&mut self) -> Result<f64, String> {
366 let mut value = self.parse_unary()?;
367 loop {
368 match self.peek() {
369 Token::Star => {
370 self.advance();
371 value *= self.parse_unary()?;
372 }
373 Token::Slash => {
374 self.advance();
375 value /= self.parse_unary()?;
376 }
377 Token::Percent => {
378 self.advance();
379 value %= self.parse_unary()?;
380 }
381 _ => break,
382 }
383 }
384 Ok(value)
385 }
386
387 fn parse_unary(&mut self) -> Result<f64, String> {
388 if self.peek() == &Token::Minus {
389 self.advance();
390 return Ok(-self.parse_unary()?);
391 }
392 if self.peek() == &Token::Plus {
393 self.advance();
394 return self.parse_unary();
395 }
396 self.parse_primary()
397 }
398
399 fn parse_primary(&mut self) -> Result<f64, String> {
400 match self.advance() {
401 Token::Number(value) => Ok(value),
402 Token::LParen => {
403 let value = self.parse_expr()?;
404 if self.advance() != Token::RParen {
405 return Err("expected ')'".to_string());
406 }
407 Ok(value)
408 }
409 Token::Ident(first) => {
410 let mut parts = vec![first];
411 while self.peek() == &Token::Dot {
412 self.advance();
413 match self.advance() {
414 Token::Ident(name) => parts.push(name),
415 other => {
416 return Err(format!("expected identifier after '.', found {other:?}"))
417 }
418 }
419 }
420 if self.peek() == &Token::LParen {
421 self.advance();
422 let args = self.parse_args()?;
423 self.eval_call(&parts.join("."), &args)
424 } else {
425 self.eval_reference(&parts)
426 }
427 }
428 other => Err(format!("unexpected token {other:?} in expression")),
429 }
430 }
431
432 fn parse_args(&mut self) -> Result<Vec<f64>, String> {
433 let mut args = Vec::new();
434 if self.peek() == &Token::RParen {
435 self.advance();
436 return Ok(args);
437 }
438 loop {
439 args.push(self.parse_expr()?);
440 match self.advance() {
441 Token::Comma => continue,
442 Token::RParen => break,
443 other => return Err(format!("expected ',' or ')' in argument list, found {other:?}")),
444 }
445 }
446 Ok(args)
447 }
448
449 fn eval_reference(&self, parts: &[String]) -> Result<f64, String> {
450 match parts.len() {
451 1 => match self.env.vars.get(&parts[0]) {
452 Some(EvalValue::Scalar(value)) => Ok(*value),
453 Some(EvalValue::Object(_)) => {
454 Err(format!("`{}` is an object, not a number", parts[0]))
455 }
456 None => Err(format!("unknown identifier `{}`", parts[0])),
457 },
458 2 => {
459 if parts[0] == "Math" {
460 return match parts[1].as_str() {
461 "PI" => Ok(std::f64::consts::PI),
462 "E" => Ok(std::f64::consts::E),
463 other => Err(format!("unsupported Math constant `Math.{other}`")),
464 };
465 }
466 match self.env.vars.get(&parts[0]) {
467 Some(EvalValue::Object(map)) => map
468 .get(&parts[1])
469 .copied()
470 .ok_or_else(|| format!("`{}.{}` is not a number", parts[0], parts[1])),
471 Some(EvalValue::Scalar(_)) => {
472 Err(format!("`{}` is a number, not an object", parts[0]))
473 }
474 None => Err(format!("unknown identifier `{}`", parts[0])),
475 }
476 }
477 _ => Err(format!("unsupported member access `{}`", parts.join("."))),
478 }
479 }
480
481 fn eval_call(&self, name: &str, args: &[f64]) -> Result<f64, String> {
482 let expect = |arity: usize| -> Result<(), String> {
483 if args.len() == arity {
484 Ok(())
485 } else {
486 Err(format!(
487 "`{name}` expects {arity} argument(s), got {}",
488 args.len()
489 ))
490 }
491 };
492 match name {
493 "Math.sin" => {
494 expect(1)?;
495 Ok(args[0].sin())
496 }
497 "Math.cos" => {
498 expect(1)?;
499 Ok(args[0].cos())
500 }
501 "Math.tan" => {
502 expect(1)?;
503 Ok(args[0].tan())
504 }
505 "Math.sqrt" => {
506 expect(1)?;
507 Ok(args[0].sqrt())
508 }
509 "Math.abs" => {
510 expect(1)?;
511 Ok(args[0].abs())
512 }
513 "Math.floor" => {
514 expect(1)?;
515 Ok(args[0].floor())
516 }
517 "Math.ceil" => {
518 expect(1)?;
519 Ok(args[0].ceil())
520 }
521 "Math.round" => {
522 expect(1)?;
523 Ok((args[0] + 0.5).floor())
526 }
527 "Math.pow" => {
528 expect(2)?;
529 Ok(args[0].powf(args[1]))
530 }
531 "Math.min" => {
532 if args.is_empty() {
533 return Ok(f64::INFINITY);
534 }
535 Ok(args.iter().copied().fold(f64::INFINITY, f64::min))
536 }
537 "Math.max" => {
538 if args.is_empty() {
539 return Ok(f64::NEG_INFINITY);
540 }
541 Ok(args.iter().copied().fold(f64::NEG_INFINITY, f64::max))
542 }
543 other => Err(format!("unsupported function `{other}`")),
544 }
545 }
546}
547
548#[cfg(test)]
549mod tests {
550 use super::*;
551
552 fn env_from(source: &str) -> Env {
553 Env::build(source, &serde_json::Value::Null).unwrap()
554 }
555
556 #[test]
557 fn statements_build_env() {
558 let env = env_from("x = 10 + 6; y = x * 2;");
559 assert_eq!(env.get("x"), Some(16.0));
560 assert_eq!(env.get("y"), Some(32.0));
561 }
562
563 #[test]
564 fn configurator_member_access() {
565 let configurator = serde_json::json!({ "values": { "width": 5 } });
566 let env = Env::build("", &configurator).unwrap();
567 assert_eq!(env.eval("configurator.width * 2").unwrap(), 10.0);
568 }
569
570 #[test]
571 fn configurator_bare_values_map() {
572 let configurator = serde_json::json!({ "width": 5 });
574 let env = Env::build("", &configurator).unwrap();
575 assert_eq!(env.eval("configurator.width * 2").unwrap(), 10.0);
576 }
577
578 #[test]
579 fn math_functions_and_constants() {
580 let env = env_from("");
581 assert_eq!(env.eval("Math.sqrt(16) + 1").unwrap(), 5.0);
582 assert!((env.eval("Math.PI").unwrap() - std::f64::consts::PI).abs() < 1e-12);
583 assert_eq!(env.eval("Math.pow(2, 10)").unwrap(), 1024.0);
584 assert_eq!(env.eval("Math.max(1, 7, 3)").unwrap(), 7.0);
585 assert_eq!(env.eval("Math.min(1, 7, 3)").unwrap(), 1.0);
586 assert_eq!(env.eval("Math.round(2.5)").unwrap(), 3.0);
587 assert_eq!(env.eval("Math.floor(2.9)").unwrap(), 2.0);
588 }
589
590 #[test]
591 fn precedence_parens_unary_modulo() {
592 let env = env_from("");
593 assert_eq!(env.eval("2 + 3 * 4").unwrap(), 14.0);
594 assert_eq!(env.eval("(2 + 3) * 4").unwrap(), 20.0);
595 assert_eq!(env.eval("-2 * -3").unwrap(), 6.0);
596 assert_eq!(env.eval("10 % 3").unwrap(), 1.0);
597 assert_eq!(env.eval("1e3 + 1").unwrap(), 1001.0);
598 }
599
600 #[test]
601 fn default_resolution_and_override() {
602 assert_eq!(env_from("").eval("resolution").unwrap(), 32.0);
603 assert_eq!(env_from("resolution = 64;").get("resolution"), Some(64.0));
604 }
605
606 #[test]
607 fn line_comments_skipped() {
608 let env = env_from("x = 5; // ignored\n y = x + 1;");
609 assert_eq!(env.get("y"), Some(6.0));
610 }
611
612 #[test]
613 fn unsupported_construct_errors() {
614 let env = env_from("");
615 assert!(env.eval("foo(1)").is_err());
616 assert!(env.eval("unknownVar").is_err());
617 assert!(env.eval("1 +").is_err());
618 assert!(env.eval("Math.tanh(1)").is_err());
619 }
620
621 #[test]
622 fn poisoned_env_reports_on_eval() {
623 let env = Env::poisoned("bad expressions".to_string());
624 assert!(env.eval("1 + 1").is_err());
625 }
626}