Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,42 @@
- [ ] Extrusion: `linear_extrude`, `rotate_extrude`
- [ ] `hull()` and `minkowski()`

## v2.5 — OpenSCAD Language Completeness

### Tier A — High Impact (used constantly)
- [ ] List indexing `v[i]`
- [ ] Ternary operator `condition ? a : b`
- [ ] User-defined functions `function f(x) = expr;`
- [ ] `let` expression `let (x=10) child`
- [ ] `undef` literal
- [ ] `concat()` built-in

### Tier B — Math & String Completeness
- [ ] Inverse trig: `asin()`, `acos()`, `atan()`, `atan2()`
- [ ] Vector math: `norm()`, `cross()`, `sign()`
- [ ] `rands()`, `lookup()`
- [ ] String literals + `str()`, `chr()`, `ord()`
- [ ] `len()` on strings

### Tier C — Module System Completeness
- [ ] `children()` / `$children`
- [ ] `echo()`
- [ ] `assert()`
- [ ] Recursive functions (enabled by user-defined functions)

### Tier D — Geometry Operations
- [ ] `multmatrix()`
- [ ] `color()`
- [ ] `offset()`
- [ ] `projection()`
- [ ] `render()`

### Tier E — File I/O (complex)
- [ ] `include <>` / `use <>`
- [ ] `import()`
- [ ] `surface()`
- [ ] `text()` (requires font rendering — significant work)

## v3 — Tooling & Visual Quality

- [ ] VS Code LSP extension (syntax highlighting, error squiggles, completions)
Expand Down
75 changes: 58 additions & 17 deletions src/csg/CsgEvaluator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ static constexpr double kDeg2Rad = 3.14159265358979323846 / 180.0;
CsgScene CsgEvaluator::evaluate(const ParseResult& result) {
Interpreter defaultInterp;
defaultInterp.loadAssignments(result);
defaultInterp.loadFunctions(result);
return evaluate(result, defaultInterp);
}

Expand Down Expand Up @@ -61,6 +62,8 @@ CsgNodePtr CsgEvaluator::evalNode(const AstNode& node, const glm::mat4& xform) {
return evalModuleCall(n, xform);
else if constexpr (std::is_same_v<T, ExtrusionNode>)
return evalExtrusion(n, xform);
else if constexpr (std::is_same_v<T, LetNode>)
return evalLet(n, xform);
return nullptr;
}, node);
}
Expand Down Expand Up @@ -226,7 +229,18 @@ CsgNodePtr CsgEvaluator::evalTransform(const TransformNode& t, const glm::mat4&
// Rotation order: Z first, then Y, then X (OpenSCAD convention).
// ---------------------------------------------------------------------------
glm::mat4 CsgEvaluator::makeMatrix(const TransformNode& t) const {
auto vec = m_interp->evalVec3(*t.vec);
Value rotVal = m_interp->evaluate(*t.vec); // evaluate once, share result
auto vec = [&]() -> std::array<double, 3> {
if (rotVal.isVector()) {
std::array<double, 3> r = {0.0, 0.0, 0.0};
for (std::size_t i = 0; i < 3 && i < rotVal.asVec().size(); ++i)
if (rotVal.asVec()[i].isNumber()) r[i] = rotVal.asVec()[i].asNumber();
return r;
}
if (rotVal.isNumber())
return {0.0, 0.0, rotVal.asNumber()}; // scalar → Z axis
return {0.0, 0.0, 0.0};
}();
const double vx = vec[0], vy = vec[1], vz = vec[2];

glm::mat4 m{1.0f};
Expand All @@ -240,6 +254,7 @@ glm::mat4 CsgEvaluator::makeMatrix(const TransformNode& t) const {
break;

case TransformNode::Kind::Rotate: {
// scalar rotate(angle) → Z-axis; vector → XYZ Euler
float rx = static_cast<float>(vx * kDeg2Rad);
float ry = static_cast<float>(vy * kDeg2Rad);
float rz = static_cast<float>(vz * kDeg2Rad);
Expand Down Expand Up @@ -307,7 +322,7 @@ CsgNodePtr CsgEvaluator::evalIf(const IfNode& node, const glm::mat4& xform) {
// ---------------------------------------------------------------------------
CsgNodePtr CsgEvaluator::evalFor(const ForNode& node, const glm::mat4& xform) {
// Build the sequence of iteration values
std::vector<double> values;
std::vector<Value> values;
static constexpr int kMaxIter = 10000;

if (node.range.isRange) {
Expand All @@ -319,20 +334,27 @@ CsgNodePtr CsgEvaluator::evalFor(const ForNode& node, const glm::mat4& xform) {
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);
values.push_back(Value::fromNumber(v));
else
for (double v = start; v >= end - 1e-10 && (int)values.size() < kMaxIter; v += step)
values.push_back(v);
values.push_back(Value::fromNumber(v));
} else {
for (const auto& e : node.range.list)
values.push_back(m_interp->evalNumber(*e));
// List form — evaluate each element; if one evaluates to a Vector,
// expand it so that `for (pt = pts)` iterates over pts' elements.
for (const auto& e : node.range.list) {
Value v = m_interp->evaluate(*e);
if (v.isVector())
for (const auto& elem : v.asVec()) values.push_back(elem);
else
values.push_back(std::move(v));
}
}

// Save the loop variable's current binding, iterate, then restore
const Value saved = m_interp->getVar(node.var);
std::vector<CsgNodePtr> all;
for (double v : values) {
m_interp->setVar(node.var, Value::fromNumber(v));
for (const Value& v : values) {
m_interp->setVar(node.var, v);
for (const auto& child : node.children) {
if (auto c = evalNode(*child, xform))
all.push_back(std::move(c));
Expand Down Expand Up @@ -365,30 +387,25 @@ CsgNodePtr CsgEvaluator::evalModuleCall(const ModuleCallNode& call, const glm::m
std::size_t posIdx = 0;
for (const auto& arg : call.args) {
if (arg.name.empty()) {
// Positional: bind to parameter at posIdx
if (posIdx < def.params.size())
m_interp->setVar(def.params[posIdx].name,
Value::fromNumber(m_interp->evalNumber(*arg.value)));
m_interp->evaluate(*arg.value));
++posIdx;
} else {
// Named: bind to the matching parameter
m_interp->setVar(arg.name,
Value::fromNumber(m_interp->evalNumber(*arg.value)));
m_interp->setVar(arg.name, m_interp->evaluate(*arg.value));
}
}

// Fill in defaults for parameters that were not supplied
// Fill in defaults for parameters not supplied
for (std::size_t i = 0; i < def.params.size(); ++i) {
const auto& param = def.params[i];
// Skip params already bound by positional or named args
bool alreadyBound = (i < posIdx);
if (!alreadyBound) {
for (const auto& arg : call.args)
if (arg.name == param.name) { alreadyBound = true; break; }
}
if (!alreadyBound && param.defaultVal)
m_interp->setVar(param.name,
Value::fromNumber(m_interp->evalNumber(*param.defaultVal)));
m_interp->setVar(param.name, m_interp->evaluate(*param.defaultVal));
}

// Evaluate the module body and collect geometry
Expand Down Expand Up @@ -449,4 +466,28 @@ CsgNodePtr CsgEvaluator::evalExtrusion(const ExtrusionNode& e, const glm::mat4&
return makeExtrusion(std::move(ext));
}

// ---------------------------------------------------------------------------
// let — bind variables in scope, evaluate children, restore
// ---------------------------------------------------------------------------
CsgNodePtr CsgEvaluator::evalLet(const LetNode& node, const glm::mat4& xform) {
auto savedEnv = m_interp->snapshotEnv();
for (const auto& [name, valExpr] : node.bindings)
m_interp->setVar(name, m_interp->evaluate(*valExpr));

std::vector<CsgNodePtr> all;
for (const auto& child : node.children) {
if (auto c = evalNode(*child, xform))
all.push_back(std::move(c));
}
m_interp->restoreEnv(std::move(savedEnv));

if (all.empty()) return nullptr;
if (all.size() == 1) return std::move(all[0]);

CsgBoolean u;
u.op = CsgBoolean::Op::Union;
u.children = std::move(all);
return makeBoolean(std::move(u));
}

} // namespace chisel::csg
1 change: 1 addition & 0 deletions src/csg/CsgEvaluator.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class CsgEvaluator {
CsgNodePtr evalFor(const chisel::lang::ForNode& n, const glm::mat4& xform);
CsgNodePtr evalModuleCall(const chisel::lang::ModuleCallNode& n, const glm::mat4& xform);
CsgNodePtr evalExtrusion(const chisel::lang::ExtrusionNode& e, const glm::mat4& xform);
CsgNodePtr evalLet(const chisel::lang::LetNode& n, const glm::mat4& xform);

glm::mat4 makeMatrix(const chisel::lang::TransformNode& t) const;
};
Expand Down
50 changes: 41 additions & 9 deletions src/lang/AST.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,12 @@ struct IfNode;
struct ForNode;
struct ModuleCallNode;
struct ExtrusionNode;
struct LetNode;

// ---------------------------------------------------------------------------
// 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<PrimitiveNode, BooleanNode, TransformNode, IfNode, ForNode, ModuleCallNode, ExtrusionNode>;
using AstNode = std::variant<PrimitiveNode, BooleanNode, TransformNode, IfNode, ForNode, ModuleCallNode, ExtrusionNode, LetNode>;
using AstNodePtr = std::unique_ptr<AstNode>;

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -184,16 +183,49 @@ inline AstNodePtr makeExtrusion(ExtrusionNode n) {
return std::make_unique<AstNode>(std::move(n));
}

// ---------------------------------------------------------------------------
// FunctionParam — one formal parameter in a function definition
// ---------------------------------------------------------------------------
struct FunctionParam {
std::string name;
ExprPtr defaultVal; // nullptr means no default (required)
};

// ---------------------------------------------------------------------------
// FunctionDef — user-defined function: function name(params) = expr;
// ---------------------------------------------------------------------------
struct FunctionDef {
std::string name;
std::vector<FunctionParam> params;
ExprPtr body; // expression — not a geometry block
SourceLoc loc;
};

// ---------------------------------------------------------------------------
// LetNode — statement-level let: let(x = expr) { children }
// Creates a scoped variable binding for child geometry.
// ---------------------------------------------------------------------------
struct LetNode {
std::vector<std::pair<std::string, ExprPtr>> bindings;
std::vector<AstNodePtr> children;
SourceLoc loc;
};

inline AstNodePtr makeLetNode(LetNode n) {
return std::make_unique<AstNode>(std::move(n));
}

// ---------------------------------------------------------------------------
// ParseResult — the output of a successful parse
// ---------------------------------------------------------------------------
struct ParseResult {
std::vector<AstNodePtr> roots; // geometry-producing top-level nodes
std::vector<AssignStmt> assignments; // variable assignments (x = expr;)
std::vector<ModuleDef> moduleDefs; // user-defined module definitions
double globalFn = 0.0; // $fn if set at file scope (0 = unset)
double globalFs = 2.0; // $fs default
double globalFa = 12.0; // $fa default
std::vector<AstNodePtr> roots; // geometry-producing top-level nodes
std::vector<AssignStmt> assignments; // variable assignments (x = expr;)
std::vector<ModuleDef> moduleDefs; // user-defined module definitions
std::vector<FunctionDef> functionDefs; // user-defined function definitions
double globalFn = 0.0; // $fn if set at file scope (0 = unset)
double globalFs = 2.0; // $fs default
double globalFa = 12.0; // $fa default
};

} // namespace chisel::lang
61 changes: 49 additions & 12 deletions src/lang/Expr.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,30 @@
#include "Token.h"
#include <memory>
#include <string>
#include <utility>
#include <variant>
#include <vector>

namespace chisel::lang {

// ---------------------------------------------------------------------------
// Forward declarations — allows ExprPtr to be used inside node structs
// before ExprNode is fully defined (same pattern as AST.h).
// Forward declarations
// ---------------------------------------------------------------------------
struct NumberLit;
struct BoolLit;
struct UndefLit;
struct VectorLit;
struct VarRef;
struct BinaryExpr;
struct UnaryExpr;
struct TernaryExpr;
struct IndexExpr;
struct LetExpr;
struct FunctionCall;

using ExprNode = std::variant<NumberLit, BoolLit, VectorLit, VarRef,
BinaryExpr, UnaryExpr, FunctionCall>;
using ExprNode = std::variant<NumberLit, BoolLit, UndefLit, VectorLit, VarRef,
BinaryExpr, UnaryExpr, TernaryExpr, IndexExpr,
LetExpr, FunctionCall>;
using ExprPtr = std::unique_ptr<ExprNode>;

template<typename T>
Expand All @@ -41,8 +46,12 @@ struct BoolLit {
SourceLoc loc;
};

struct UndefLit {
SourceLoc loc;
};

struct VectorLit {
std::vector<ExprPtr> elements; // each element is an expression
std::vector<ExprPtr> elements;
SourceLoc loc;
};

Expand All @@ -56,10 +65,10 @@ struct VarRef {
// ---------------------------------------------------------------------------
struct BinaryExpr {
enum class Op {
Add, Sub, Mul, Div, Mod, // arithmetic
Eq, Ne, // equality
Lt, Le, Gt, Ge, // comparison
And, Or // logical
Add, Sub, Mul, Div, Mod,
Eq, Ne,
Lt, Le, Gt, Ge,
And, Or
};
Op op;
ExprPtr left;
Expand All @@ -74,10 +83,38 @@ struct UnaryExpr {
SourceLoc loc;
};

// condition ? then : else_
struct TernaryExpr {
ExprPtr condition;
ExprPtr then;
ExprPtr else_;
SourceLoc loc;
};

// target[index]
struct IndexExpr {
ExprPtr target;
ExprPtr index;
SourceLoc loc;
};

// let(x = expr, ...) body_expr
struct LetExpr {
std::vector<std::pair<std::string, ExprPtr>> bindings;
ExprPtr body;
SourceLoc loc;
};

// One argument in a function call — named or positional
struct FunctionArg {
std::string name; // empty = positional
ExprPtr value;
};

struct FunctionCall {
std::string name;
std::vector<ExprPtr> args; // positional arguments
SourceLoc loc;
std::string name;
std::vector<FunctionArg> args;
SourceLoc loc;
};

} // namespace chisel::lang
Loading
Loading