SCAD parser: array subscript, array env, user-defined modules

This commit is contained in:
jess 2026-03-31 14:16:02 -07:00
parent afd3d6a409
commit 0f2d8a022b
3 changed files with 128 additions and 43 deletions

View File

@ -111,6 +111,7 @@ pub enum Expr {
BinaryOp { op: BinaryOp, left: Box<Expr>, right: Box<Expr> },
FnCall { name: String, args: Vec<Argument> },
Ternary { cond: Box<Expr>, then_expr: Box<Expr>, else_expr: Box<Expr> },
Index { array: Box<Expr>, index: Box<Expr> },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]

View File

@ -375,7 +375,18 @@ impl Parser {
let operand = self.parse_unary()?;
return Ok(Expr::UnaryOp { op: UnaryOp::Not, operand: Box::new(operand) });
}
self.parse_primary()
self.parse_postfix()
}
fn parse_postfix(&mut self) -> Result<Expr, ParseError> {
let mut expr = self.parse_primary()?;
while self.peek_is(&Token::LBracket) {
self.expect(&Token::LBracket)?;
let index = self.parse_expr()?;
self.expect(&Token::RBracket)?;
expr = Expr::Index { array: Box::new(expr), index: Box::new(index) };
}
Ok(expr)
}
fn parse_primary(&mut self) -> Result<Expr, ParseError> {

View File

@ -22,6 +22,7 @@ enum Value {
}
type Env = HashMap<String, Value>;
type ModuleDefs<'a> = HashMap<String, &'a ModuleDef>;
fn env_get_scalar(env: &Env, name: &str) -> Option<f64> {
match env.get(name)? {
@ -38,7 +39,9 @@ fn env_insert_scalar(env: &mut Env, name: String, val: f64) {
/// Multiple top-level statements become an implicit union.
pub fn lower_program(program: &Program) -> Result<SdfNode, LowerError> {
let mut env = Env::new();
let nodes = lower_statements(&program.statements, &mut env)?;
let mut modules = ModuleDefs::new();
collect_module_defs(&program.statements, &mut modules);
let nodes = lower_statements(&program.statements, &mut env, &modules)?;
match nodes.len() {
0 => Err(LowerError { msg: "empty scene".into() }),
@ -47,29 +50,37 @@ pub fn lower_program(program: &Program) -> Result<SdfNode, LowerError> {
}
}
fn lower_statements(stmts: &[Statement], env: &mut Env) -> Result<Vec<SdfNode>, LowerError> {
fn collect_module_defs<'a>(stmts: &'a [Statement], modules: &mut ModuleDefs<'a>) {
for stmt in stmts {
if let Statement::ModuleDef(def) = stmt {
modules.insert(def.name.clone(), def);
}
}
}
fn lower_statements(stmts: &[Statement], env: &mut Env, modules: &ModuleDefs) -> Result<Vec<SdfNode>, LowerError> {
let mut nodes = Vec::new();
for stmt in stmts {
if let Some(node) = lower_statement(stmt, env)? {
if let Some(node) = lower_statement(stmt, env, modules)? {
nodes.push(node);
}
}
Ok(nodes)
}
fn lower_statement(stmt: &Statement, env: &mut Env) -> Result<Option<SdfNode>, LowerError> {
fn lower_statement(stmt: &Statement, env: &mut Env, modules: &ModuleDefs) -> Result<Option<SdfNode>, LowerError> {
match stmt {
Statement::ModuleCall(call) => lower_module_call(call, env).map(Some),
Statement::BooleanOp(bop) => lower_boolean(bop, env).map(Some),
Statement::ModuleCall(call) => lower_module_call(call, env, modules).map(Some),
Statement::BooleanOp(bop) => lower_boolean(bop, env, modules).map(Some),
Statement::Assignment(asgn) => {
if let Some(val) = eval_expr_env(&asgn.value, env) {
env_insert_scalar(env, asgn.name.clone(), val);
if let Some(val) = eval_value(&asgn.value, env) {
env.insert(asgn.name.clone(), val);
}
Ok(None)
}
Statement::ModuleDef(_) => Ok(None),
Statement::ForLoop(fl) => lower_for_loop(fl, env).map(Some),
Statement::IfElse(ie) => lower_if_else(ie, env),
Statement::ForLoop(fl) => lower_for_loop(fl, env, modules).map(Some),
Statement::IfElse(ie) => lower_if_else(ie, env, modules),
}
}
@ -77,16 +88,15 @@ fn lower_statement(stmt: &Statement, env: &mut Env) -> Result<Option<SdfNode>, L
///
/// Each iteration gets its own copy of the environment with the loop
/// variable bound. Since iterations share no state, every unrolled
/// instance is an independent parallel branch — this is where serial
/// SCAD becomes parallel geometry.
fn lower_for_loop(fl: &ForLoop, env: &mut Env) -> Result<SdfNode, LowerError> {
/// instance is an independent parallel branch.
fn lower_for_loop(fl: &ForLoop, env: &mut Env, modules: &ModuleDefs) -> Result<SdfNode, LowerError> {
let values = expand_range(&fl.range, env)?;
let mut branches = Vec::with_capacity(values.len());
for val in values {
let mut inner_env = env.clone();
env_insert_scalar(&mut inner_env, fl.var.clone(), val);
let nodes = lower_statements(&fl.body, &mut inner_env)?;
let nodes = lower_statements(&fl.body, &mut inner_env, modules)?;
match nodes.len() {
0 => {}
1 => branches.push(nodes.into_iter().next().unwrap()),
@ -131,7 +141,6 @@ fn expand_range(range: &ForRange, env: &Env) -> Result<Vec<f64>, LowerError> {
}
}
// Safety cap
if values.len() > 10000 {
return Err(LowerError { msg: "for loop exceeds 10000 iterations".into() });
}
@ -148,20 +157,17 @@ fn expand_range(range: &ForRange, env: &Env) -> Result<Vec<f64>, LowerError> {
}
/// Lower if/else by evaluating the condition at lowering time.
/// Constant conditions → dead code elimination. Variable conditions
/// include both branches (the SDF is defined everywhere; the condition
/// selects which geometry appears).
fn lower_if_else(ie: &IfElse, env: &mut Env) -> Result<Option<SdfNode>, LowerError> {
fn lower_if_else(ie: &IfElse, env: &mut Env, modules: &ModuleDefs) -> Result<Option<SdfNode>, LowerError> {
if let Some(cond) = eval_expr_env(&ie.condition, env) {
if cond != 0.0 {
let nodes = lower_statements(&ie.then_body, env)?;
let nodes = lower_statements(&ie.then_body, env, modules)?;
match nodes.len() {
0 => Ok(None),
1 => Ok(Some(nodes.into_iter().next().unwrap())),
_ => Ok(Some(SdfNode::Union(nodes))),
}
} else if !ie.else_body.is_empty() {
let nodes = lower_statements(&ie.else_body, env)?;
let nodes = lower_statements(&ie.else_body, env, modules)?;
match nodes.len() {
0 => Ok(None),
1 => Ok(Some(nodes.into_iter().next().unwrap())),
@ -171,10 +177,8 @@ fn lower_if_else(ie: &IfElse, env: &mut Env) -> Result<Option<SdfNode>, LowerErr
Ok(None)
}
} else {
// Non-constant condition: include both branches as a union
// (conservative — the SDF field includes all possible geometry)
let mut nodes = lower_statements(&ie.then_body, env)?;
nodes.extend(lower_statements(&ie.else_body, env)?);
let mut nodes = lower_statements(&ie.then_body, env, modules)?;
nodes.extend(lower_statements(&ie.else_body, env, modules)?);
match nodes.len() {
0 => Ok(None),
1 => Ok(Some(nodes.into_iter().next().unwrap())),
@ -183,7 +187,26 @@ fn lower_if_else(ie: &IfElse, env: &mut Env) -> Result<Option<SdfNode>, LowerErr
}
}
/// Evaluate a constant expression with variable environment.
/// Evaluate an expression to a full Value (scalar or array).
fn eval_value(expr: &Expr, env: &Env) -> Option<Value> {
match expr {
Expr::Ident(name) => env.get(name).cloned(),
Expr::Vector(elems) => {
let vals: Option<Vec<f64>> = elems.iter().map(|e| eval_expr_env(e, env)).collect();
Some(Value::Array(vals?))
}
Expr::Index { array, index } => {
let idx = eval_expr_env(index, env)? as usize;
match eval_value(array, env)? {
Value::Array(arr) => arr.get(idx).map(|v| Value::Scalar(*v)),
_ => None,
}
}
_ => eval_expr_env(expr, env).map(Value::Scalar),
}
}
/// Evaluate a constant expression to a scalar f64.
fn eval_expr_env(expr: &Expr, env: &Env) -> Option<f64> {
match expr {
Expr::Number(n) => Some(*n),
@ -242,12 +265,20 @@ fn eval_expr_env(expr: &Expr, env: &Env) -> Option<f64> {
_ => None,
}
}
Expr::Index { array, index } => {
let idx = eval_expr_env(index, env)? as usize;
match eval_value(array, env)? {
Value::Array(arr) => arr.get(idx).copied(),
Value::Scalar(v) if idx == 0 => Some(v),
_ => None,
}
}
_ => None,
}
}
fn lower_module_call(call: &ModuleCall, env: &mut Env) -> Result<SdfNode, LowerError> {
let child_nodes = lower_statements(&call.children, env)?;
fn lower_module_call(call: &ModuleCall, env: &mut Env, modules: &ModuleDefs) -> Result<SdfNode, LowerError> {
let child_nodes = lower_statements(&call.children, env, modules)?;
match call.name.as_str() {
"sphere" => {
@ -311,12 +342,32 @@ fn lower_module_call(call: &ModuleCall, env: &mut Env) -> Result<SdfNode, LowerE
Ok(SdfNode::Scale { factor: [s, s, s], child: Box::new(child) })
}
}
_ => Err(LowerError { msg: format!("unknown module: {}", call.name) }),
name => {
if let Some(def) = modules.get(name) {
let mut child_env = env.clone();
for (i, param) in def.params.iter().enumerate() {
let val = call.args.get(i)
.and_then(|a| eval_value(&a.value, &child_env))
.or_else(|| param.default.as_ref().and_then(|d| eval_value(d, &child_env)));
if let Some(v) = val {
child_env.insert(param.name.clone(), v);
}
}
let nodes = lower_statements(&def.body, &mut child_env, modules)?;
match nodes.len() {
0 => Err(LowerError { msg: format!("module {name}() produced no geometry") }),
1 => Ok(nodes.into_iter().next().unwrap()),
_ => Ok(SdfNode::Union(nodes)),
}
} else {
Err(LowerError { msg: format!("unknown module: {name}") })
}
}
}
}
fn lower_boolean(bop: &BooleanOp, env: &mut Env) -> Result<SdfNode, LowerError> {
let children = lower_statements(&bop.children, env)?;
fn lower_boolean(bop: &BooleanOp, env: &mut Env, modules: &ModuleDefs) -> Result<SdfNode, LowerError> {
let children = lower_statements(&bop.children, env, modules)?;
if children.is_empty() {
return Err(LowerError { msg: "boolean operation with no children".into() });
}
@ -345,8 +396,6 @@ fn require_single_child(mut nodes: Vec<SdfNode>, op: &str) -> Result<SdfNode, Lo
}
}
// Argument extraction helpers
fn eval_const_bool(expr: &Expr) -> Option<bool> {
match expr {
Expr::Bool(b) => Some(*b),
@ -457,7 +506,6 @@ mod tests {
#[test]
fn for_loop_unrolls() {
// 4 iterations → Union of 4 translated spheres
let node = parse_and_lower(
"for (i = [0:3]) translate([i*10, 0, 0]) sphere(1);"
);
@ -466,7 +514,6 @@ mod tests {
#[test]
fn for_loop_with_step() {
// [0 : 2 : 6] → values 0, 2, 4, 6 → 4 branches
let node = parse_and_lower(
"for (i = [0:2:6]) sphere(i);"
);
@ -490,7 +537,7 @@ mod tests {
#[test]
fn if_constant_false_with_else() {
let node = parse_and_lower("if (false) sphere(1); else cube(2);");
assert!(matches!(node, SdfNode::Translate { .. })); // cube with center=false wraps in translate
assert!(matches!(node, SdfNode::Translate { .. }));
}
#[test]
@ -499,25 +546,21 @@ mod tests {
let tokens = Lexer::new(src).tokenize().unwrap();
let program = Parser::new(tokens).parse_program().unwrap();
let result = lower_program(&program);
assert!(result.is_err()); // no geometry produced → empty scene
assert!(result.is_err());
}
#[test]
fn variable_condition_includes_both_branches() {
// `x` is unknown → both branches included
let src = "x = 1; if (x) sphere(1); else cube(2);";
let node = parse_and_lower(src);
// x=1 is known, so condition evaluates truthy → only sphere
assert!(matches!(node, SdfNode::Sphere { .. }));
}
#[test]
fn nested_for_loops() {
// 3 * 3 = 9 branches
let node = parse_and_lower(
"for (i = [0:2]) for (j = [0:2]) translate([i*10, j*10, 0]) sphere(1);"
);
// Outer union of 3, each containing inner union of 3
assert_eq!(count_union_children(&node), 3);
if let SdfNode::Union(outer) = &node {
for child in outer {
@ -541,7 +584,7 @@ mod tests {
#[test]
fn for_with_variable_bounds() {
let node = parse_and_lower("n = 3; for (i = [0:n]) sphere(i);");
assert_eq!(count_union_children(&node), 4); // 0, 1, 2, 3
assert_eq!(count_union_children(&node), 4);
}
#[test]
@ -549,4 +592,34 @@ mod tests {
let node = parse_and_lower("difference() { cube(10, center=true); sphere(5); }");
assert!(matches!(node, SdfNode::Difference { .. }));
}
#[test]
fn array_subscript() {
let node = parse_and_lower("arr = [1, 5, 10]; sphere(arr[1]);");
assert!(matches!(node, SdfNode::Sphere { radius } if (radius - 5.0).abs() < 1e-10));
}
#[test]
fn array_subscript_in_for_loop() {
let node = parse_and_lower(
"vals = [2, 4, 6]; for (i = [0:2]) sphere(vals[i]);"
);
assert_eq!(count_union_children(&node), 3);
}
#[test]
fn user_defined_module() {
let node = parse_and_lower(
"module ball() { sphere(5); } ball();"
);
assert!(matches!(node, SdfNode::Sphere { radius } if (radius - 5.0).abs() < 1e-10));
}
#[test]
fn user_module_with_env() {
let node = parse_and_lower(
"r = 3; module ball() { sphere(r); } ball();"
);
assert!(matches!(node, SdfNode::Sphere { radius } if (radius - 3.0).abs() < 1e-10));
}
}