diff --git a/src/csg/CsgEvaluator.cpp b/src/csg/CsgEvaluator.cpp index 92c9cec..41e2ebd 100644 --- a/src/csg/CsgEvaluator.cpp +++ b/src/csg/CsgEvaluator.cpp @@ -49,6 +49,8 @@ CsgNodePtr CsgEvaluator::evalNode(const AstNode& node, const glm::mat4& xform) { return evalTransform(n, xform); else if constexpr (std::is_same_v) return evalIf(n, xform); + else if constexpr (std::is_same_v) + return evalFor(n, xform); return nullptr; }, node); } @@ -201,4 +203,51 @@ CsgNodePtr CsgEvaluator::evalIf(const IfNode& node, const glm::mat4& xform) { return makeBoolean(std::move(u)); } +// --------------------------------------------------------------------------- +// for — iterate range or list, union all child geometry +// --------------------------------------------------------------------------- +CsgNodePtr CsgEvaluator::evalFor(const ForNode& node, const glm::mat4& xform) { + // Build the sequence of iteration values + std::vector values; + static constexpr int kMaxIter = 10000; + + if (node.range.isRange) { + double start = m_interp->evalNumber(*node.range.start); + double end = m_interp->evalNumber(*node.range.end); + double step = node.range.step + ? m_interp->evalNumber(*node.range.step) + : 1.0; + if (step == 0.0) return nullptr; + if (step > 0.0) + for (double v = start; v <= end + 1e-10 && (int)values.size() < kMaxIter; v += step) + values.push_back(v); + else + for (double v = start; v >= end - 1e-10 && (int)values.size() < kMaxIter; v += step) + values.push_back(v); + } else { + for (const auto& e : node.range.list) + values.push_back(m_interp->evalNumber(*e)); + } + + // Save the loop variable's current binding, iterate, then restore + const Value saved = m_interp->getVar(node.var); + std::vector all; + for (double v : values) { + m_interp->setVar(node.var, Value::fromNumber(v)); + for (const auto& child : node.children) { + if (auto c = evalNode(*child, xform)) + all.push_back(std::move(c)); + } + } + m_interp->setVar(node.var, saved); + + if (all.empty()) return nullptr; + if (all.size() == 1) return all[0]; + + CsgBoolean u; + u.op = CsgBoolean::Op::Union; + u.children = std::move(all); + return makeBoolean(std::move(u)); +} + } // namespace chisel::csg diff --git a/src/csg/CsgEvaluator.h b/src/csg/CsgEvaluator.h index 443b3d5..c0afc94 100644 --- a/src/csg/CsgEvaluator.h +++ b/src/csg/CsgEvaluator.h @@ -29,6 +29,7 @@ class CsgEvaluator { CsgNodePtr evalBoolean(const chisel::lang::BooleanNode& b, const glm::mat4& xform); CsgNodePtr evalTransform(const chisel::lang::TransformNode& t, const glm::mat4& xform); CsgNodePtr evalIf(const chisel::lang::IfNode& n, const glm::mat4& xform); + CsgNodePtr evalFor(const chisel::lang::ForNode& n, const glm::mat4& xform); glm::mat4 makeMatrix(const chisel::lang::TransformNode& t) const; }; diff --git a/src/lang/AST.h b/src/lang/AST.h index ff5a151..b232f99 100644 --- a/src/lang/AST.h +++ b/src/lang/AST.h @@ -14,13 +14,14 @@ struct PrimitiveNode; struct BooleanNode; struct TransformNode; struct IfNode; +struct ForNode; // --------------------------------------------------------------------------- // AstNode — the top-level variant // All nodes are heap-allocated via unique_ptr so the tree is // easy to move/own and the variant stays small. // --------------------------------------------------------------------------- -using AstNode = std::variant; +using AstNode = std::variant; using AstNodePtr = std::unique_ptr; // --------------------------------------------------------------------------- @@ -90,6 +91,30 @@ inline AstNodePtr makeIf(IfNode n) { return std::make_unique(std::move(n)); } +// --------------------------------------------------------------------------- +// ForNode — for (var = [start:step:end]) { ... } +// for (var = [start:end]) { ... } (step defaults to 1) +// for (var = [v0, v1, v2, ...]) { ... } (explicit list) +// --------------------------------------------------------------------------- +struct ForRange { + bool isRange = true; // true: start/step/end; false: list + ExprPtr start; // range form — required + ExprPtr step; // range form — nullptr means step of 1 + ExprPtr end; // range form — required + std::vector list; // list form +}; + +struct ForNode { + std::string var; + ForRange range; + std::vector children; + SourceLoc loc; +}; + +inline AstNodePtr makeFor(ForNode n) { + return std::make_unique(std::move(n)); +} + // --------------------------------------------------------------------------- // AssignStmt — a variable assignment at file or block scope: x = expr; // --------------------------------------------------------------------------- diff --git a/src/lang/Interpreter.cpp b/src/lang/Interpreter.cpp index b4e3894..b8a7e59 100644 --- a/src/lang/Interpreter.cpp +++ b/src/lang/Interpreter.cpp @@ -136,6 +136,18 @@ std::array Interpreter::evalVec3(const ExprNode& expr) const { return result; } +// --------------------------------------------------------------------------- +// getVar / setVar — used by CsgEvaluator to bind for-loop variables +// --------------------------------------------------------------------------- +Value Interpreter::getVar(const std::string& name) const { + auto it = m_env.find(name); + return it != m_env.end() ? it->second : Value::undef(); +} + +void Interpreter::setVar(const std::string& name, Value val) { + m_env[name] = std::move(val); +} + // --------------------------------------------------------------------------- // Built-in functions (V2a subset — math functions added in V2c) // --------------------------------------------------------------------------- diff --git a/src/lang/Interpreter.h b/src/lang/Interpreter.h index cc3228f..6c13387 100644 --- a/src/lang/Interpreter.h +++ b/src/lang/Interpreter.h @@ -31,6 +31,10 @@ class Interpreter { // Missing elements default to 0.0. std::array evalVec3(const ExprNode& expr) const; + // For-loop variable binding — used by CsgEvaluator::evalFor. + Value getVar(const std::string& name) const; + void setVar(const std::string& name, Value val); + private: std::unordered_map m_env; diff --git a/src/lang/Lexer.cpp b/src/lang/Lexer.cpp index 539f21a..691899f 100644 --- a/src/lang/Lexer.cpp +++ b/src/lang/Lexer.cpp @@ -25,6 +25,7 @@ static const std::unordered_map kKeywords = { {"mirror", TokenKind::Mirror}, {"if", TokenKind::If}, {"else", TokenKind::Else}, + {"for", TokenKind::For}, }; // --------------------------------------------------------------------------- @@ -94,6 +95,7 @@ std::vector Lexer::tokenize() { case ']': tokens.push_back(makeToken(TokenKind::RBracket, startOffset)); break; case ',': tokens.push_back(makeToken(TokenKind::Comma, startOffset)); break; case ';': tokens.push_back(makeToken(TokenKind::Semicolon, startOffset)); break; + case ':': tokens.push_back(makeToken(TokenKind::Colon, startOffset)); break; case '+': tokens.push_back(makeToken(TokenKind::Plus, startOffset)); break; case '-': tokens.push_back(makeToken(TokenKind::Minus, startOffset)); break; case '*': tokens.push_back(makeToken(TokenKind::Star, startOffset)); break; diff --git a/src/lang/Parser.cpp b/src/lang/Parser.cpp index 7b15069..4cbd5ca 100644 --- a/src/lang/Parser.cpp +++ b/src/lang/Parser.cpp @@ -137,6 +137,9 @@ AstNodePtr Parser::parseNode() { case TokenKind::If: return parseIf(); + case TokenKind::For: + return parseFor(); + default: return nullptr; } @@ -234,6 +237,57 @@ AstNodePtr Parser::parseIf() { return makeIf(std::move(node)); } +// --------------------------------------------------------------------------- +// for — for (var = [start:step:end]) or for (var = [v0, v1, ...]) +// --------------------------------------------------------------------------- +AstNodePtr Parser::parseFor() { + const Token& kw = advance(); // consume 'for' + ForNode node; + node.loc = kw.loc; + + expect(TokenKind::LParen, "expected '(' after 'for'"); + node.var = expect(TokenKind::Ident, "expected loop variable").text; + expect(TokenKind::Equals, "expected '=' after loop variable"); + expect(TokenKind::LBracket, "expected '[' for range/list"); + + // Parse first expression — determines range vs list form + auto first = parseExpr(); + + if (check(TokenKind::Colon)) { + // Range form: [start : end] or [start : step : end] + advance(); // consume ':' + auto second = parseExpr(); + if (check(TokenKind::Colon)) { + advance(); // consume ':' + auto third = parseExpr(); + // [first:second:third] = [start:step:end] + node.range.isRange = true; + node.range.start = std::move(first); + node.range.step = std::move(second); + node.range.end = std::move(third); + } else { + // [first:second] = [start:end], implicit step of 1 + node.range.isRange = true; + node.range.start = std::move(first); + node.range.end = std::move(second); + } + } else { + // List form: [first, ...] + node.range.isRange = false; + node.range.list.push_back(std::move(first)); + while (match(TokenKind::Comma)) { + if (check(TokenKind::RBracket)) break; + node.range.list.push_back(parseExpr()); + } + } + + expect(TokenKind::RBracket, "expected ']' after range/list"); + expect(TokenKind::RParen, "expected ')' after for header"); + + node.children = parseBody(); + return makeFor(std::move(node)); +} + // --------------------------------------------------------------------------- // parseVecExpr — parse a [x, y, z] literal into a VectorLit ExprPtr // --------------------------------------------------------------------------- diff --git a/src/lang/Parser.h b/src/lang/Parser.h index f188538..cac4814 100644 --- a/src/lang/Parser.h +++ b/src/lang/Parser.h @@ -37,6 +37,7 @@ class Parser { AstNodePtr parseBoolean(TokenKind k); AstNodePtr parseTransform(TokenKind k); AstNodePtr parseIf(); + AstNodePtr parseFor(); // ---- expressions (Pratt parser) -------------------------------------- ExprPtr parseExpr(int minPrec = 0); diff --git a/src/lang/Token.h b/src/lang/Token.h index 1e7b93d..47a1043 100644 --- a/src/lang/Token.h +++ b/src/lang/Token.h @@ -49,6 +49,10 @@ enum class TokenKind : uint8_t { // Control flow If, // if Else, // else + For, // for + + // Range separator + Colon, // : // Arithmetic operators Plus, // + diff --git a/tests/for_test.scad b/tests/for_test.scad new file mode 100644 index 0000000..2309a2d --- /dev/null +++ b/tests/for_test.scad @@ -0,0 +1,42 @@ +// for loop test — V2 Tier 2c +// Each scene is a translate() block; comment out all but one to isolate. +$fn = 32; + +// ─── Scene 1 (0, 0, 0): simple range — row of spheres ──────────────────────── +// for (i = [0:4]) — five spheres spaced 12 apart +translate([0, 0, 0]) + for (i = [0:4]) + translate([i*12, 0, 0]) sphere(r=4); + +// ─── Scene 2 (0, -25, 0): range with step — every other position ───────────── +// for (i = [0:2:8]) — five spheres at i=0,2,4,6,8 +translate([0, -25, 0]) + for (i = [0:2:8]) + translate([i*6, 0, 0]) sphere(r=3); + +// ─── Scene 3 (0, -50, 0): list of values ───────────────────────────────────── +// Cylinders of varying radii +translate([0, -50, 0]) + for (r = [2, 4, 6, 4, 2]) + translate([r*5, 0, 0]) cylinder(h=r*2, r=r, center=true); + +// ─── Scene 4 (0, -80, 0): loop variable in expression ──────────────────────── +// Staircase — cube height grows with i +translate([0, -80, 0]) + for (i = [0:5]) + translate([i*8, 0, (i+1)*2]) cube([6, 6, (i+1)*4], center=true); + +// ─── Scene 5 (0, -115, 0): for inside difference (cutting pattern) ─────────── +// Cube with a row of cylindrical holes +translate([0, -115, 0]) + difference() { + cube([60, 12, 12], center=true); + for (i = [0:4]) + translate([-24 + i*12, 0, 0]) cylinder(h=20, r=3, center=true); + } + +// ─── Scene 6 (0, -145, 0): nested for — grid of spheres ───────────────────── +translate([0, -145, 0]) + for (x = [0:3]) + for (y = [0:3]) + translate([x*10, y*10, 0]) sphere(r=3); diff --git a/tests/test_csg_evaluator.cpp b/tests/test_csg_evaluator.cpp index caf145e..ef04657 100644 --- a/tests/test_csg_evaluator.cpp +++ b/tests/test_csg_evaluator.cpp @@ -300,3 +300,60 @@ TEST_CASE("CsgEval:if inherits outer transform", "[csg]") { REQUIRE(s.roots.size() == 1); REQUIRE(asLeaf(s.roots[0]).transform[3][0] == Approx(7.0f)); } + +// --------------------------------------------------------------------------- +// for loops +// --------------------------------------------------------------------------- +TEST_CASE("CsgEval:for range produces one child per step", "[csg]") { + // [0:2] → i=0,1,2 — three spheres wrapped in a union + auto s = evaluate("for (i = [0:2]) sphere(r=1);"); + REQUIRE(s.roots.size() == 1); + const auto& b = asBool(s.roots[0]); + REQUIRE(b.op == CsgBoolean::Op::Union); + REQUIRE(b.children.size() == 3); +} + +TEST_CASE("CsgEval:for range with step", "[csg]") { + // [0:2:6] → i=0,2,4,6 — four children + auto s = evaluate("for (i = [0:2:6]) sphere(r=1);"); + const auto& b = asBool(s.roots[0]); + REQUIRE(b.children.size() == 4); +} + +TEST_CASE("CsgEval:for list produces one child per value", "[csg]") { + auto s = evaluate("for (v = [1, 5, 9]) cube([v, v, v]);"); + const auto& b = asBool(s.roots[0]); + REQUIRE(b.op == CsgBoolean::Op::Union); + REQUIRE(b.children.size() == 3); +} + +TEST_CASE("CsgEval:for single iteration returns leaf directly", "[csg]") { + // [3:3] → i=3 only — no wrapping union needed + auto s = evaluate("for (i = [3:3]) sphere(r=2);"); + REQUIRE(s.roots.size() == 1); + REQUIRE(asLeaf(s.roots[0]).kind == CsgLeaf::Kind::Sphere); +} + +TEST_CASE("CsgEval:for uses loop variable in child", "[csg]") { + // translate([i,0,0]) — each sphere should sit at x = i + auto s = evaluate("for (i = [0:2]) translate([i*5, 0, 0]) sphere(r=1);"); + const auto& b = asBool(s.roots[0]); + REQUIRE(b.children.size() == 3); + REQUIRE(asLeaf(b.children[0]).transform[3][0] == Approx(0.0f)); + REQUIRE(asLeaf(b.children[1]).transform[3][0] == Approx(5.0f)); + REQUIRE(asLeaf(b.children[2]).transform[3][0] == Approx(10.0f)); +} + +TEST_CASE("CsgEval:for restores outer variable after loop", "[csg]") { + // 'i' is set before the loop; the for loop should restore it afterward + auto s = evaluate("i = 99; for (i = [0:1]) sphere(r=1); cube([i,i,i]);"); + // cube gets i=99 (restored), for produces 2 spheres + REQUIRE(s.roots.size() == 2); + REQUIRE(asLeaf(s.roots[1]).params.at("x") == Approx(99.0)); +} + +TEST_CASE("CsgEval:for empty range yields no geometry", "[csg]") { + // [5:3] with positive step — no iterations + auto s = evaluate("for (i = [5:3]) sphere(r=1);"); + REQUIRE(s.roots.empty()); +} diff --git a/tests/test_lexer.cpp b/tests/test_lexer.cpp index ee49350..43296be 100644 --- a/tests/test_lexer.cpp +++ b/tests/test_lexer.cpp @@ -55,6 +55,21 @@ TEST_CASE("Lexer:if and else keywords", "[lexer]") { REQUIRE(kinds("else") == std::vector{TokenKind::Else}); } +TEST_CASE("Lexer:for keyword and colon", "[lexer]") { + REQUIRE(kinds("for") == std::vector{TokenKind::For}); + REQUIRE(kinds(":") == std::vector{TokenKind::Colon}); +} + +TEST_CASE("Lexer:for range tokens", "[lexer]") { + // for (i = [0:5]) → for ( i = [ 0 : 5 ] ) + auto t = kinds("for (i = [0:5])"); + REQUIRE(t[0] == TokenKind::For); + REQUIRE(t[4] == TokenKind::LBracket); + REQUIRE(t[5] == TokenKind::Number); + REQUIRE(t[6] == TokenKind::Colon); + REQUIRE(t[7] == TokenKind::Number); +} + // --------------------------------------------------------------------------- // Transform keywords // --------------------------------------------------------------------------- diff --git a/tests/test_parser.cpp b/tests/test_parser.cpp index 4ca9a23..6e46be7 100644 --- a/tests/test_parser.cpp +++ b/tests/test_parser.cpp @@ -319,6 +319,43 @@ TEST_CASE("Parser:if condition is expression", "[parser]") { REQUIRE(std::holds_alternative(*r.roots[0])); } +// --------------------------------------------------------------------------- +// for loops +// --------------------------------------------------------------------------- +static const ForNode& asFor(const AstNodePtr& n) { + return std::get(*n); +} + +TEST_CASE("Parser:for range [start:end]", "[parser]") { + auto r = parse("for (i = [0:4]) sphere(r=1);"); + REQUIRE(r.roots.size() == 1); + auto& f = asFor(r.roots[0]); + REQUIRE(f.var == "i"); + REQUIRE(f.range.isRange == true); + REQUIRE(f.range.step == nullptr); // implicit step + REQUIRE(f.children.size() == 1); +} + +TEST_CASE("Parser:for range [start:step:end]", "[parser]") { + auto r = parse("for (i = [0:2:8]) sphere(r=1);"); + auto& f = asFor(r.roots[0]); + REQUIRE(f.range.isRange == true); + REQUIRE(f.range.step != nullptr); +} + +TEST_CASE("Parser:for list", "[parser]") { + auto r = parse("for (v = [1, 3, 7]) sphere(r=1);"); + auto& f = asFor(r.roots[0]); + REQUIRE(f.range.isRange == false); + REQUIRE(f.range.list.size() == 3); +} + +TEST_CASE("Parser:for with brace body", "[parser]") { + auto r = parse("for (i = [0:2]) { cube([5,5,5]); sphere(r=2); }"); + auto& f = asFor(r.roots[0]); + REQUIRE(f.children.size() == 2); +} + // --------------------------------------------------------------------------- // Error recovery // ---------------------------------------------------------------------------