Skip to main content

rscad_openscad/eval/
mod.rs

1//! OpenSCAD evaluator: parsed AST → [`rscad_core::DynamicObject`] scene.
2//!
3//! The pipeline is `openscad-parser` AST → [`interp::Interp`] (scopes,
4//! expressions, user functions/modules) → geometry lowering →
5//! `DynamicObject` tree wrapped in a [`rscad_core::SceneFile`].
6//!
7//! Semantics follow OpenSCAD: hoisted last-assignment-wins variables, three
8//! namespaces, dynamically scoped `$`-variables, degree-based trig, and
9//! warn-and-skip handling for unsupported constructs.
10
11mod builtins;
12mod colors;
13mod diag;
14mod env;
15mod expr;
16mod interp;
17mod lower;
18mod modules;
19mod stmt;
20mod value;
21
22pub use diag::{ScadDiagnostic, Severity};
23pub use value::{RangeValue, Value};
24
25use std::collections::{BTreeMap, HashSet};
26use std::path::{Path, PathBuf};
27
28use openscad_parser::prelude::{Ast, Item, ModuleChild, ParseError, Span, Spannable, Stmt};
29use rscad_core::SceneFile;
30
31use diag::render_fatal;
32use env::seed_dyn_env;
33use interp::with_eval_stack;
34use lower::{root_adapter, union_of};
35use stmt::{DepMap, Exec};
36
37/// A fully evaluated OpenSCAD model: one scene tree named after the source
38/// file, plus all diagnostics produced along the way.
39#[derive(Clone, Debug, PartialEq)]
40pub struct ScadModel {
41    pub scene: SceneFile,
42    pub diagnostics: Vec<ScadDiagnostic>,
43}
44
45/// Errors that abort OpenSCAD evaluation outright.
46#[derive(Clone, Debug, PartialEq)]
47pub enum ScadError {
48    /// Lex/parse failure; pre-rendered ariadne report text.
49    Parse(String),
50    /// Fatal evaluation failure (failed `assert`, exhausted work budget,
51    /// runaway recursion). Diagnostics collected up to the failure ride along.
52    Eval {
53        message: String,
54        diagnostics: Vec<ScadDiagnostic>,
55    },
56}
57
58impl core::fmt::Display for ScadError {
59    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
60        match self {
61            ScadError::Parse(report) => write!(f, "{report}"),
62            ScadError::Eval { message, .. } => write!(f, "{message}"),
63        }
64    }
65}
66
67impl std::error::Error for ScadError {}
68
69/// Evaluate a `.scad` file (and its `include`/`use` closure) to a scene.
70pub fn evaluate_file(path: impl AsRef<Path>) -> Result<ScadModel, ScadError> {
71    let mut ast = Ast::new();
72    let root = ast
73        .parse_file(path)
74        .map_err(|e| ScadError::Parse(e.render()))?;
75    evaluate(ast, root)
76}
77
78/// Evaluate in-memory OpenSCAD source. `path` is virtual: its parent
79/// directory anchors `include`/`use` resolution and its stem names the tree.
80pub fn evaluate_source(
81    path: impl Into<PathBuf>,
82    source: impl Into<String>,
83) -> Result<ScadModel, ScadError> {
84    let path = path.into();
85    let ast =
86        Ast::parse_source(path.clone(), source.into()).map_err(|e| ScadError::Parse(e.render()))?;
87    let root = *ast
88        .file
89        .get(&path)
90        .expect("BUG: parse_source registers the path");
91    evaluate(ast, root)
92}
93
94fn evaluate(mut ast: Ast, root: usize) -> Result<ScadModel, ScadError> {
95    let (deps, pending) = collect_deps(&mut ast, root)?;
96    // Evaluation recurses per expression/call; run it on a big-stack thread.
97    with_eval_stack(move || run(&ast, deps, root, pending))
98}
99
100/// Warnings discovered before evaluation starts (unreadable include targets).
101type PendingWarning = (String, Option<Span>);
102
103/// Pre-parse the `include`/`use` closure so the AST is immutable during
104/// evaluation. Unreadable targets become warnings; files that exist but fail
105/// to parse abort with [`ScadError::Parse`].
106fn collect_deps(ast: &mut Ast, root: usize) -> Result<(DepMap, Vec<PendingWarning>), ScadError> {
107    let mut deps = DepMap::new();
108    let mut pending = Vec::new();
109    let mut visited: HashSet<usize> = HashSet::from([root]);
110    let mut queue = vec![root];
111    while let Some(fid) = queue.pop() {
112        let mut refs = Vec::new();
113        if let Some(file) = ast.get_file(fid) {
114            collect_refs(&file.stmts, &mut refs);
115        }
116        let dir: PathBuf = ast
117            .path_of(fid)
118            .and_then(Path::parent)
119            .map(Path::to_path_buf)
120            .unwrap_or_default();
121        for (raw, span) in refs {
122            if deps.contains_key(&(fid, raw.clone())) {
123                continue;
124            }
125            match ast.parse_file(dir.join(&raw)) {
126                Ok(dep) => {
127                    deps.insert((fid, raw), dep);
128                    if visited.insert(dep) {
129                        queue.push(dep);
130                    }
131                }
132                Err(err @ ParseError::Io { .. }) => pending.push((err.render(), Some(span))),
133                Err(other) => return Err(ScadError::Parse(other.render())),
134            }
135        }
136    }
137    Ok((deps, pending))
138}
139
140/// Collect `include <>`/`use <>` references from a statement tree.
141fn collect_refs(stmts: &[Stmt], out: &mut Vec<(String, Span)>) {
142    for stmt in stmts {
143        match stmt {
144            Stmt::Item(Item::Include(inc)) => {
145                out.push((inc.path().value().to_string(), inc.span()));
146            }
147            Stmt::Item(Item::Use(u)) => out.push((u.path().value().to_string(), u.span())),
148            Stmt::Item(Item::Module(m)) => collect_refs(core::slice::from_ref(m.body()), out),
149            Stmt::Item(_) | Stmt::Local(_) | Stmt::Expr(..) | Stmt::Echo(_) | Stmt::Assert(_) => {}
150            Stmt::If(i) => {
151                collect_refs(core::slice::from_ref(&i.then_branch), out);
152                if let Some((_, else_branch)) = &i.else_branch {
153                    collect_refs(core::slice::from_ref(else_branch), out);
154                }
155            }
156            Stmt::For(f) => collect_refs(core::slice::from_ref(&f.body), out),
157            Stmt::Let(l) => collect_refs(core::slice::from_ref(&l.body), out),
158            Stmt::Block(b) => collect_refs(b.stmts(), out),
159            Stmt::ModuleCall(mc) => {
160                if let ModuleChild::Stmt(s) = &mc.child {
161                    collect_refs(core::slice::from_ref(s), out);
162                }
163            }
164        }
165    }
166}
167
168fn run(
169    ast: &Ast,
170    deps: DepMap,
171    root: usize,
172    pending: Vec<PendingWarning>,
173) -> Result<ScadModel, ScadError> {
174    let mut exec = Exec::new(ast, deps);
175    for (message, span) in pending {
176        exec.interp.diags.warn(message, span);
177    }
178    let root_scope = exec.interp.scopes.root();
179    let dyn_env = seed_dyn_env();
180    let stmts = &ast.get_file(root).expect("BUG: root file parsed").stmts;
181    exec.include_active.push(root);
182    let result = exec.exec_scope(root_scope, &dyn_env, stmts);
183    let diags = core::mem::take(&mut exec.interp.diags);
184    match result {
185        Ok(children) => {
186            let tree = root_adapter(exec.show_only.take().unwrap_or_else(|| union_of(children)));
187            let name = ast
188                .path_of(root)
189                .and_then(Path::file_stem)
190                .map(|s| s.to_string_lossy().into_owned())
191                .unwrap_or_else(|| "Scene".to_string());
192            Ok(ScadModel {
193                scene: SceneFile {
194                    trees: BTreeMap::from([(name, tree)]),
195                },
196                diagnostics: diags.resolve(ast),
197            })
198        }
199        Err(fatal) => Err(ScadError::Eval {
200            message: render_fatal(ast, &fatal),
201            diagnostics: diags.resolve(ast),
202        }),
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use openscad_parser::chumsky::Parser as _;
209    use openscad_parser::prelude::*;
210
211    use super::diag::EvalResult;
212    use super::env::{DynMap, seed_dyn_env};
213    use super::interp::Interp;
214    use super::value::Value;
215
216    /// Parse a `.scad` fragment of function definitions and assignments
217    /// followed by one final expression statement, then evaluate that
218    /// expression. `foo(...);` statements count as expressions too (the
219    /// grammar parses them as module calls).
220    fn run(src: &str) -> (EvalResult<Value>, Vec<super::ScadDiagnostic>) {
221        let src = src.to_string();
222        super::interp::with_eval_stack(move || run_inner(&src))
223    }
224
225    fn run_inner(src: &str) -> (EvalResult<Value>, Vec<super::ScadDiagnostic>) {
226        let input = Tokens::to_chumsky_input(src, 0);
227        let file = File::parser()
228            .parse(input)
229            .into_result()
230            .unwrap_or_else(|e| panic!("parse failed for {src:?}: {e:?}"));
231
232        // Owned copy of the final expression; declared before `interp` so it
233        // outlives the interpreter's `'ast` borrows.
234        let mut final_expr: Option<Expr> = None;
235        let mut interp = Interp::new();
236        let root = interp.scopes.root();
237        let dyn_env: DynMap = seed_dyn_env();
238
239        let result: EvalResult<Value> = 'stmts: {
240            for stmt in &file.stmts {
241                match stmt {
242                    Stmt::Item(Item::Fn(f)) => {
243                        interp.scopes.define_fn(
244                            root,
245                            f.name().name(),
246                            super::env::UserFn {
247                                params: f.params(),
248                                body: f.body(),
249                                lexical: root,
250                            },
251                        );
252                    }
253                    Stmt::Local(local) => match interp.eval_expr(root, &dyn_env, local.value()) {
254                        Ok(v) => interp.scopes.set_var(root, local.name().name(), v),
255                        Err(fatal) => break 'stmts Err(fatal),
256                    },
257                    Stmt::Expr(e, _) => final_expr = Some(e.clone()),
258                    Stmt::ModuleCall(mc)
259                        if mc.modifier.is_none() && matches!(mc.child, ModuleChild::Empty(_)) =>
260                    {
261                        final_expr = Some(Expr::Call(mc.call.clone()));
262                    }
263                    other => panic!("unsupported test statement: {other:?}"),
264                }
265            }
266            let expr = final_expr
267                .as_ref()
268                .expect("test program must end with an expression statement");
269            break 'stmts interp.eval_expr(root, &dyn_env, expr);
270        };
271
272        let ast = Ast::parse_source("test.scad", src.to_string()).expect("re-parse");
273        let diags = interp.diags.resolve(&ast);
274        (result, diags)
275    }
276
277    fn eval(src: &str) -> Value {
278        match run(src) {
279            (Ok(v), _) => v,
280            (Err(fatal), _) => panic!("fatal evaluation error for {src:?}: {}", fatal.message),
281        }
282    }
283
284    fn num(src: &str) -> f64 {
285        match eval(src) {
286            Value::Number(n) => n,
287            other => panic!("expected number from {src:?}, got {other:?}"),
288        }
289    }
290
291    #[test]
292    fn arithmetic_and_precedence() {
293        assert_eq!(num("1 + 2 * 3;"), 7.0);
294        assert_eq!(num("(1 + 2) * 3;"), 9.0);
295        assert_eq!(num("7 % 4;"), 3.0);
296        assert_eq!(num("-2 - -3;"), 1.0);
297        assert_eq!(num("x = 5; x-1;"), 4.0);
298        assert_eq!(num("3--2;"), 5.0);
299    }
300
301    #[test]
302    fn number_literal_forms() {
303        assert_eq!(num(".5 + 1.;"), 1.5);
304        assert_eq!(num("2e3;"), 2000.0);
305    }
306
307    #[test]
308    fn comparisons_and_logic() {
309        assert_eq!(eval("1 < 2;"), Value::Bool(true));
310        assert_eq!(eval("\"a\" < \"b\";"), Value::Bool(true));
311        assert_eq!(eval("1 == 1 && 2 != 3;"), Value::Bool(true));
312        assert_eq!(eval("false || 5;"), Value::Bool(true));
313        assert_eq!(eval("undef == undef;"), Value::Bool(true));
314        // short circuit: the unknown variable on the rhs must never evaluate
315        let (v, diags) = run("false && unknown_var;");
316        assert_eq!(v.unwrap(), Value::Bool(false));
317        assert!(diags.is_empty(), "unexpected diags: {diags:?}");
318    }
319
320    #[test]
321    fn ternary_vectors_indexing() {
322        assert_eq!(num("true ? 1 : 2;"), 1.0);
323        assert_eq!(num("[1, 2, 3][1];"), 2.0);
324        assert_eq!(num("[[1, 2], [3, 4]][1][0];"), 3.0);
325        assert_eq!(num("v = [1, 2, 3]; v.z;"), 3.0);
326        assert_eq!(eval("\"abc\"[1];"), Value::String("b".into()));
327    }
328
329    #[test]
330    fn vector_math() {
331        assert_eq!(
332            eval("[1, 2, 3] + [4, 5, 6];"),
333            Value::vector(vec![
334                Value::Number(5.0),
335                Value::Number(7.0),
336                Value::Number(9.0)
337            ])
338        );
339        assert_eq!(num("[1, 2, 3] * [4, 5, 6];"), 32.0);
340        assert_eq!(
341            eval("2 * [3, 4];"),
342            Value::vector(vec![Value::Number(6.0), Value::Number(8.0)])
343        );
344        assert_eq!(
345            eval("[[1, 0], [0, 2]] * [3, 4];"),
346            Value::vector(vec![Value::Number(3.0), Value::Number(8.0)])
347        );
348    }
349
350    #[test]
351    fn out_of_bounds_warns_undef() {
352        let (v, diags) = run("[1, 2][5];");
353        assert_eq!(v.unwrap(), Value::Undef);
354        assert_eq!(diags.len(), 1);
355        assert!(diags[0].message.contains("out of bounds"));
356        assert_eq!(diags[0].location.as_deref(), Some("test.scad:1:1"));
357    }
358
359    #[test]
360    fn let_bindings() {
361        // parenthesized so they parse as let *expressions* (bare statement
362        // `let (...) ...;` is Stmt::Let, exercised in the geometry tests)
363        assert_eq!(num("(let (a = 1, b = a + 1) a + b);"), 3.0);
364        assert_eq!(num("(let (a = 1) let (a = a + 1) a);"), 2.0);
365    }
366
367    #[test]
368    fn user_functions_and_recursion() {
369        assert_eq!(num("function f(x, y = 10) = x + y; f(1);"), 11.0);
370        assert_eq!(num("function f(x, y = 10) = x + y; f(1, 2);"), 3.0);
371        assert_eq!(num("function f(x, y = 10) = x + y; f(y = 1, x = 2);"), 3.0);
372        // defaults may reference earlier parameters
373        assert_eq!(num("function f(a, b = a * 2) = a + b; f(3);"), 9.0);
374        assert_eq!(
375            num("function fact(n) = n <= 1 ? 1 : n * fact(n - 1); fact(5);"),
376            120.0
377        );
378        assert_eq!(
379            num("function fib(n) = n < 2 ? n : fib(n - 1) + fib(n - 2); fib(10);"),
380            55.0
381        );
382    }
383
384    #[test]
385    fn unbounded_recursion_is_fatal() {
386        let (result, _) = run("function boom(n) = boom(n + 1); boom(0);");
387        let err = result.unwrap_err();
388        assert!(
389            err.message.contains("recursion limit"),
390            "unexpected fatal: {}",
391            err.message
392        );
393    }
394
395    #[test]
396    fn list_comprehensions() {
397        assert_eq!(
398            eval("[for (i = [0 : 2]) i * 10];"),
399            Value::vector(vec![
400                Value::Number(0.0),
401                Value::Number(10.0),
402                Value::Number(20.0)
403            ])
404        );
405        assert_eq!(
406            eval("[for (i = [0 : 5]) if (i % 2 == 0) i];"),
407            Value::vector(vec![
408                Value::Number(0.0),
409                Value::Number(2.0),
410                Value::Number(4.0)
411            ])
412        );
413        assert_eq!(
414            eval("[for (i = [-1 : 1]) if (i < 0) -1 else if (i > 0) 1 else 0];"),
415            Value::vector(vec![
416                Value::Number(-1.0),
417                Value::Number(0.0),
418                Value::Number(1.0)
419            ])
420        );
421        assert_eq!(
422            eval("[each [1, 2], each [3], 4];"),
423            Value::vector(vec![
424                Value::Number(1.0),
425                Value::Number(2.0),
426                Value::Number(3.0),
427                Value::Number(4.0)
428            ])
429        );
430        assert_eq!(
431            eval("[for (x = [0 : 1], y = [0 : 1]) x * 2 + y];"),
432            Value::vector(vec![
433                Value::Number(0.0),
434                Value::Number(1.0),
435                Value::Number(2.0),
436                Value::Number(3.0)
437            ])
438        );
439        assert_eq!(
440            eval("[for (i = [0 : 2]) let (x = i * i) x];"),
441            Value::vector(vec![
442                Value::Number(0.0),
443                Value::Number(1.0),
444                Value::Number(4.0)
445            ])
446        );
447        // iterating a vector of points
448        assert_eq!(
449            eval("[for (p = [[1, 2], [3, 4]]) p[0]];"),
450            Value::vector(vec![Value::Number(1.0), Value::Number(3.0)])
451        );
452    }
453
454    #[test]
455    fn each_over_enormous_range_hits_budget() {
456        // The `each` count is charged before the range is materialized, so an
457        // enormous range trips the work budget instead of allocating ~10^15
458        // elements (which would hang/OOM the evaluator). This must complete
459        // fast: `charge` returns the error before `iterate` runs.
460        let (result, _) = run("[each [0 : 1 : 1e15]];");
461        let err = result.unwrap_err();
462        assert!(
463            err.message.contains("work budget exhausted"),
464            "unexpected fatal: {}",
465            err.message
466        );
467
468        // A normal, in-budget `each` still works unchanged.
469        assert_eq!(
470            eval("[each [1, 2, 3]];"),
471            Value::vector(vec![
472                Value::Number(1.0),
473                Value::Number(2.0),
474                Value::Number(3.0)
475            ])
476        );
477        assert_eq!(
478            eval("[each [0 : 2]];"),
479            Value::vector(vec![
480                Value::Number(0.0),
481                Value::Number(1.0),
482                Value::Number(2.0)
483            ])
484        );
485    }
486
487    #[test]
488    fn builtin_functions() {
489        assert!((num("sin(90);") - 1.0).abs() < 1e-12);
490        assert!((num("cos(60);") - 0.5).abs() < 1e-12);
491        assert_eq!(num("atan2(1, 1);"), 45.0);
492        assert_eq!(num("min(3, 1, 2);"), 1.0);
493        assert_eq!(num("max([3, 1, 2]);"), 3.0);
494        assert_eq!(num("len([1, 2, 3]);"), 3.0);
495        assert_eq!(num("len(\"héllo\");"), 5.0);
496        assert_eq!(num("norm([3, 4]);"), 5.0);
497        assert_eq!(num("cross([1, 0], [0, 1]);"), 1.0);
498        assert_eq!(
499            eval("cross([1, 0, 0], [0, 1, 0]);"),
500            Value::vector(vec![
501                Value::Number(0.0),
502                Value::Number(0.0),
503                Value::Number(1.0)
504            ])
505        );
506        assert_eq!(
507            eval("concat([1], 2, [3, 4]);"),
508            Value::vector(vec![
509                Value::Number(1.0),
510                Value::Number(2.0),
511                Value::Number(3.0),
512                Value::Number(4.0)
513            ])
514        );
515        assert_eq!(eval("str(\"r=\", 5, \"!\");"), Value::String("r=5!".into()));
516        assert_eq!(eval("chr(65);"), Value::String("A".into()));
517        assert_eq!(num("ord(\"A\");"), 65.0);
518        assert_eq!(num("lookup(2.5, [[0, 0], [2, 20], [3, 30]]);"), 25.0);
519        assert_eq!(num("lookup(-1, [[0, 0], [2, 20]]);"), 0.0);
520        assert_eq!(eval("is_undef(undef);"), Value::Bool(true));
521        assert_eq!(eval("is_list([1]);"), Value::Bool(true));
522        assert_eq!(eval("is_num(\"x\");"), Value::Bool(false));
523        assert_eq!(num("pow(2, 10);"), 1024.0);
524    }
525
526    #[test]
527    fn rands_is_deterministic_and_in_range() {
528        let a = eval("rands(2, 5, 8, 42);");
529        let b = eval("rands(2, 5, 8, 42);");
530        assert_eq!(a, b);
531        let nums = a.as_numbers().expect("numeric vector");
532        assert_eq!(nums.len(), 8);
533        assert!(nums.iter().all(|&x| (2.0..=5.0).contains(&x)));
534    }
535
536    #[test]
537    fn special_variables() {
538        assert_eq!(num("$fa;"), 12.0);
539        assert_eq!(num("$fn;"), 0.0);
540        assert_eq!(eval("$preview;"), Value::Bool(true));
541        // unknown special vars are silently undef
542        let (v, diags) = run("$nope;");
543        assert_eq!(v.unwrap(), Value::Undef);
544        assert!(diags.is_empty());
545        // let can bind special variables for its body
546        assert_eq!(num("(let ($fn = 32) $fn);"), 32.0);
547    }
548
549    #[test]
550    fn dollar_args_flow_into_functions() {
551        assert_eq!(num("function probe() = $fn; probe($fn = 24);"), 24.0);
552        // ...and are dynamically scoped through nested calls.
553        assert_eq!(
554            num("function inner() = $fn; function outer() = inner(); outer($fn = 17);"),
555            17.0
556        );
557    }
558
559    #[test]
560    fn echo_and_assert_expressions() {
561        let (v, diags) = run("echo(\"x=\", 42) 7;");
562        assert_eq!(v.unwrap(), Value::Number(7.0));
563        assert_eq!(diags.len(), 1);
564        assert_eq!(diags[0].message, "ECHO: \"x=\", 42");
565
566        assert_eq!(num("assert(true) 3;"), 3.0);
567
568        let (result, _) = run("assert(1 > 2, \"broken\") 3;");
569        let err = result.unwrap_err();
570        assert!(
571            err.message.contains("assertion failed: broken"),
572            "got: {}",
573            err.message
574        );
575    }
576
577    #[test]
578    fn unknown_names_warn() {
579        let (v, diags) = run("nope + 1;");
580        assert_eq!(v.unwrap(), Value::Undef);
581        assert!(diags.iter().any(|d| d.message.contains("unknown variable")));
582
583        let (v, diags) = run("nofunc(1);");
584        assert_eq!(v.unwrap(), Value::Undef);
585        assert!(diags.iter().any(|d| d.message.contains("unknown function")));
586    }
587
588    #[test]
589    fn string_escapes_decode() {
590        assert_eq!(eval("\"a\\tb\";"), Value::String("a\tb".into()));
591        assert_eq!(
592            eval("\"say \\\"hi\\\"\";"),
593            Value::String("say \"hi\"".into())
594        );
595    }
596
597    #[test]
598    fn ranges_as_values() {
599        assert_eq!(
600            eval("[0 : 2 : 6];"),
601            Value::Range(super::RangeValue {
602                start: 0.0,
603                step: 2.0,
604                end: 6.0
605            })
606        );
607    }
608}