diff --git a/crates/cord-parse/src/ast.rs b/crates/cord-parse/src/ast.rs index ac90993..62cd9b6 100644 --- a/crates/cord-parse/src/ast.rs +++ b/crates/cord-parse/src/ast.rs @@ -111,6 +111,7 @@ pub enum Expr { BinaryOp { op: BinaryOp, left: Box, right: Box }, FnCall { name: String, args: Vec }, Ternary { cond: Box, then_expr: Box, else_expr: Box }, + Index { array: Box, index: Box }, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/crates/cord-parse/src/parser.rs b/crates/cord-parse/src/parser.rs index 35de8e1..ccee01f 100644 --- a/crates/cord-parse/src/parser.rs +++ b/crates/cord-parse/src/parser.rs @@ -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 { + 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 { diff --git a/crates/cord-sdf/src/lower.rs b/crates/cord-sdf/src/lower.rs index 13009cc..fff9ba6 100644 --- a/crates/cord-sdf/src/lower.rs +++ b/crates/cord-sdf/src/lower.rs @@ -22,6 +22,7 @@ enum Value { } type Env = HashMap; +type ModuleDefs<'a> = HashMap; fn env_get_scalar(env: &Env, name: &str) -> Option { 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 { 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 { } } -fn lower_statements(stmts: &[Statement], env: &mut Env) -> Result, 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, 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, LowerError> { +fn lower_statement(stmt: &Statement, env: &mut Env, modules: &ModuleDefs) -> Result, 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, 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 { +/// instance is an independent parallel branch. +fn lower_for_loop(fl: &ForLoop, env: &mut Env, modules: &ModuleDefs) -> Result { 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, 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, 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, LowerError> { +fn lower_if_else(ie: &IfElse, env: &mut Env, modules: &ModuleDefs) -> Result, 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, 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, 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 { + match expr { + Expr::Ident(name) => env.get(name).cloned(), + Expr::Vector(elems) => { + let vals: Option> = 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 { match expr { Expr::Number(n) => Some(*n), @@ -242,12 +265,20 @@ fn eval_expr_env(expr: &Expr, env: &Env) -> Option { _ => 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 { - let child_nodes = lower_statements(&call.children, env)?; +fn lower_module_call(call: &ModuleCall, env: &mut Env, modules: &ModuleDefs) -> Result { + 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 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 { - let children = lower_statements(&bop.children, env)?; +fn lower_boolean(bop: &BooleanOp, env: &mut Env, modules: &ModuleDefs) -> Result { + 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, op: &str) -> Result Option { 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)); + } }