1mod 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#[derive(Clone, Debug, PartialEq)]
40pub struct ScadModel {
41 pub scene: SceneFile,
42 pub diagnostics: Vec<ScadDiagnostic>,
43}
44
45#[derive(Clone, Debug, PartialEq)]
47pub enum ScadError {
48 Parse(String),
50 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
69pub 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
78pub 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 with_eval_stack(move || run(&ast, deps, root, pending))
98}
99
100type PendingWarning = (String, Option<Span>);
102
103fn 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
140fn 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 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 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 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 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 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 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 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 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 let (v, diags) = run("$nope;");
543 assert_eq!(v.unwrap(), Value::Undef);
544 assert!(diags.is_empty());
545 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 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}