diff --git a/docs/roadmap.md b/docs/roadmap.md index 06310d4..6fc43d8 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -87,12 +87,6 @@ cover. Fixed: **Explicitly deferred, not done in this pass** (tracked here so they aren't lost, not because they don't matter): -- First-class function literals (`f = function(x) x*2;`, OpenSCAD 2019.05+) - as values assignable to variables/passable as arguments — the language - has named `function foo(x) = expr;` defs but no closure value type; adding - one touches the `Value` variant, the parser grammar, and call dispatch - broadly enough that it deserves its own pass rather than riding along with - smaller fixes. - `$vpr`/`$vpt`/`$vpd` viewport special variables — would need the render layer's camera state plumbed into the interpreter, a cross-subsystem wire that doesn't exist today; rarely load-bearing in real `.scad` files. @@ -102,6 +96,19 @@ lost, not because they don't matter): parser-level deviation and any fix is a grammar-level change, not a point fix — out of scope here. +## v3.6 — First-class function literals ✓ + +- [x] Function literals (`f = function(x) x*2;`, OpenSCAD 2019.05+) as + values: assignable to variables, storable in lists, passable as + arguments to (and returnable from) other functions, and callable later + via ordinary `f(...)` syntax wherever `f` names a variable holding one. + Adds a `Value::Tag::Function` closure (AST pointer + captured defining + scope) alongside the existing named `function foo(x) = expr;` form. + Direct self-binding (`fact = function(n) n<=1 ? 1 : n*fact(n-1);`) + recurses by name without needing a named `function` def. +- [x] `is_function()` now recognizes actual function values (was a + stubbed-`false` placeholder pending this work). + ## v4 — Tooling & Visual Quality - [ ] VS Code LSP extension (syntax highlighting, error squiggles, completions) diff --git a/src/csg/CsgEvaluator.cpp b/src/csg/CsgEvaluator.cpp index 965674d..319ee29 100644 --- a/src/csg/CsgEvaluator.cpp +++ b/src/csg/CsgEvaluator.cpp @@ -154,7 +154,7 @@ CsgNodePtr CsgEvaluator::evalNode(const AstNode& node, const glm::mat4& xform, // scope in place (the enclosing evalXxx call is responsible for // snapshot/restore around its whole child list, so this doesn't // leak past the block it's written in). Produces no geometry. - m_interp->setVar(n.name, m_interp->evaluate(*n.value)); + m_interp->assignVar(n.name, *n.value); return nullptr; } return nullptr; @@ -634,6 +634,18 @@ std::string CsgEvaluator::formatValue(const Value& v) { formatValue(Value::fromNumber(v.rangeStep)) + ":" + formatValue(Value::fromNumber(v.rangeEnd)) + "]"; } + if (v.isFunction()) { + std::string s = "function("; + if (v.closure && v.closure->def) { + const auto& params = v.closure->def->params; + for (std::size_t i = 0; i < params.size(); ++i) { + if (i > 0) s += ", "; + s += params[i].name; + } + } + s += ")"; + return s; + } return "undef"; } @@ -1461,7 +1473,7 @@ CsgNodePtr CsgEvaluator::evalLet(const LetNode& node, const glm::mat4& xform, const ColorAttr& color) { auto savedEnv = m_interp->snapshotEnv(); for (const auto& [name, valExpr] : node.bindings) - m_interp->setVar(name, m_interp->evaluate(*valExpr)); + m_interp->assignVar(name, *valExpr); std::vector all; for (const auto& child : node.children) { diff --git a/src/lang/Expr.h b/src/lang/Expr.h index dd35eef..c3570bd 100644 --- a/src/lang/Expr.h +++ b/src/lang/Expr.h @@ -25,10 +25,11 @@ struct LetExpr; struct FunctionCall; struct RangeLit; struct ListCompExpr; +struct FunctionLit; using ExprNode = std::variant; + LetExpr, FunctionCall, RangeLit, ListCompExpr, FunctionLit>; using ExprPtr = std::unique_ptr; template @@ -179,4 +180,25 @@ struct ListCompExpr { SourceLoc loc; }; +// --------------------------------------------------------------------------- +// Function literal — `function(params) expr`, OpenSCAD 2019.05+. +// +// Unlike FunctionDef (AST.h) — a named `function foo(x) = expr;` declaration +// dispatched by name through FunctionCall — a FunctionLit is itself a +// first-class expression: it evaluates to a closure Value (see Value::Tag:: +// Function) that can be assigned to a variable, stored in a list, passed as +// an argument, and returned from another function, then invoked later via +// ordinary call syntax (`f(...)`) wherever `f` names a variable holding one. +// --------------------------------------------------------------------------- +struct FunctionLitParam { + std::string name; + ExprPtr defaultVal; // nullptr = no default (required) +}; + +struct FunctionLit { + std::vector params; + ExprPtr body; + SourceLoc loc; +}; + } // namespace chisel::lang diff --git a/src/lang/Interpreter.cpp b/src/lang/Interpreter.cpp index e2c91a2..d249915 100644 --- a/src/lang/Interpreter.cpp +++ b/src/lang/Interpreter.cpp @@ -27,6 +27,8 @@ static bool valEqRec(const Value& a, const Value& b) { } if (a.isRange()) return a.rangeStart == b.rangeStart && a.rangeStep == b.rangeStep && a.rangeEnd == b.rangeEnd; + if (a.isFunction()) + return a.closure == b.closure; // identity: same closure instance return a.isUndef(); // both Undef, since tags already matched } @@ -35,7 +37,7 @@ static bool valEqRec(const Value& a, const Value& b) { // --------------------------------------------------------------------------- void Interpreter::loadAssignments(const ParseResult& result) { for (const auto& stmt : result.assignments) - m_env[stmt.name] = evaluate(*stmt.value); + assignVar(stmt.name, *stmt.value); } void Interpreter::loadFunctions(const ParseResult& result) { @@ -217,7 +219,7 @@ Value Interpreter::evaluate(const ExprNode& expr) { else if constexpr (std::is_same_v) { auto savedEnv = snapshotEnv(); for (const auto& [name, valExpr] : node.bindings) - setVar(name, evaluate(*valExpr)); + assignVar(name, *valExpr); Value result = evaluate(*node.body); restoreEnv(std::move(savedEnv)); return result; @@ -258,6 +260,14 @@ Value Interpreter::evaluate(const ExprNode& expr) { return Value::fromVec(std::move(out)); } + // ---- Function literal: function(params) expr ---- + else if constexpr (std::is_same_v) { + auto env = std::make_shared(); + env->def = &node; + env->vars = m_env; // capture the defining scope by value + return Value::fromClosure(std::move(env)); + } + // ---- Function call ---- else if constexpr (std::is_same_v) { // Collect positional and named argument values @@ -271,6 +281,15 @@ Value Interpreter::evaluate(const ExprNode& expr) { namedArgs.push_back({arg.name, std::move(v)}); } + // A variable bound to a function-literal value takes priority + // over a same-named `function` definition or builtin — mirrors + // OpenSCAD: once `f = function(x) x*2;` exists, `f(3)` calls + // that closure even if a builtin or `function f(...)` also + // exists under that name. + auto varIt = m_env.find(node.name); + if (varIt != m_env.end() && varIt->second.isFunction()) + return callClosure(varIt->second, posArgs, namedArgs); + // Try user-defined function first auto fit = m_funcDefs.find(node.name); if (fit != m_funcDefs.end()) { @@ -415,6 +434,54 @@ void Interpreter::setVar(const std::string& name, Value val) { m_env[name] = std::move(val); } +void Interpreter::assignVar(const std::string& name, const ExprNode& valueExpr) { + Value v = evaluate(valueExpr); + if (v.isFunction() && v.closure && std::holds_alternative(valueExpr)) + v.closure->selfName = name; // let `name(...)` recurse from within its own body (see callClosure) + m_env[name] = std::move(v); +} + +// --------------------------------------------------------------------------- +// callClosure — invoke a Value::Tag::Function closure +// --------------------------------------------------------------------------- +Value Interpreter::callClosure(Value fnVal, + const std::vector& posArgs, + const std::vector>& namedArgs) { + if (!fnVal.closure || !fnVal.closure->def || m_callDepth >= kMaxCallDepth) + return Value::undef(); + const FunctionLit& def = *fnVal.closure->def; + + auto savedEnv = snapshotEnv(); + m_env = fnVal.closure->vars; // lexical scope: where the literal was written, not where it's called + // Rebind the closure's own name (if any) fresh for this call only — never + // stored back into fnVal.closure->vars itself, which would recreate the + // shared_ptr cycle assignVar's comment warns about. + if (!fnVal.closure->selfName.empty()) + m_env[fnVal.closure->selfName] = fnVal; + + std::size_t posIdx = 0; + for (const auto& param : def.params) { + bool bound = false; + for (const auto& [n, v] : namedArgs) { + if (n == param.name) { setVar(param.name, v); bound = true; break; } + } + if (!bound) { + if (posIdx < posArgs.size()) + setVar(param.name, posArgs[posIdx++]); + else if (param.defaultVal) + setVar(param.name, evaluate(*param.defaultVal)); + else + setVar(param.name, Value::undef()); + } + } + + ++m_callDepth; + Value result = evaluate(*def.body); + --m_callDepth; + restoreEnv(std::move(savedEnv)); + return result; +} + // --------------------------------------------------------------------------- // Built-in functions // --------------------------------------------------------------------------- @@ -633,10 +700,7 @@ Value Interpreter::callBuiltin(const std::string& name, if (name == "is_num") return args.size() >= 1 ? Value::fromBool(args[0].isNumber()) : Value::undef(); if (name == "is_string") return args.size() >= 1 ? Value::fromBool(args[0].isString()) : Value::undef(); if (name == "is_list") return args.size() >= 1 ? Value::fromBool(args[0].isVector()) : Value::undef(); - // No first-class function-literal value exists yet (see roadmap v3.5), - // so nothing can ever actually be one — always false rather than absent, - // which is still correct given today's set of possible Value tags. - if (name == "is_function") return args.size() >= 1 ? Value::fromBool(false) : Value::undef(); + if (name == "is_function") return args.size() >= 1 ? Value::fromBool(args[0].isFunction()) : Value::undef(); // ---- version() / version_num() ---- // ChiselCAD isn't OpenSCAD, so there's no real "version" to report; this diff --git a/src/lang/Interpreter.h b/src/lang/Interpreter.h index d5eced7..8a21fbb 100644 --- a/src/lang/Interpreter.h +++ b/src/lang/Interpreter.h @@ -34,6 +34,26 @@ class Interpreter { Value getVar(const std::string& name) const; void setVar(const std::string& name, Value val); + // Binds `name` in the current scope to the evaluated result of valueExpr + // — used everywhere a real assignment happens (global `x = expr;`, local + // block assignments, `let(x = expr, ...)` bindings), as opposed to + // setVar's other callers (module/function parameter binding, for-loop + // variables) which must NOT get the special case below. When valueExpr + // is directly a function literal (`f = function(n) ... f(n-1) ...;`), + // this also records the closure's own name (ClosureEnv::selfName) so + // callClosure can bind it for the literal's body to call itself by that + // name — mirroring OpenSCAD's function-literal recursion. Restricting + // this to a literal directly on the right-hand side (checked via the AST + // node type, not the runtime Value) keeps it safe: the closure was just + // constructed by this same evaluate() call, so nothing else can be + // aliasing it yet. A plain copy (`g = f;`) does not re-trigger this — + // renaming a closure that's already shared with other bindings would + // rebind `name` in their calls too. Note this stores only a name string, + // not a Value referencing the closure itself: the latter would be a + // shared_ptr cycle back into its own vars map, which + // shared_ptr can never collect. + void assignVar(const std::string& name, const ExprNode& valueExpr); + // Expands a range's [start:step:end] bounds into the concrete sequence // of values it denotes — shared by for-loops (both the `for (v = // [a:b:c])` literal form and `for (v = expr)` when expr is a Range @@ -98,6 +118,20 @@ class Interpreter { Value callBuiltin(const std::string& name, const std::vector& args) const; + // Invokes a closure Value (see Value::Tag::Function): binds fnVal's + // params against posArgs/namedArgs the same way a named FunctionDef call + // does, but starting from the closure's *captured* environment rather + // than the caller's — a function literal sees the scope where + // `function(...) ...` was written, not the scope it's called from. + // Takes fnVal by value, not by reference: the caller's reference is + // typically an element inside m_env (e.g. a FunctionCall looks the + // callee up via m_env.find), and this function reassigns m_env wholesale + // as its first step (to switch to the closure's own captured scope) — + // a reference into the old map would dangle the moment that happens. + Value callClosure(Value fnVal, + const std::vector& posArgs, + const std::vector>& namedArgs); + // Appends v's per-iteration values (see iterationValues()) onto out — // used by `each` in both a plain list literal and a list-comprehension // body. diff --git a/src/lang/Parser.cpp b/src/lang/Parser.cpp index 73f81fb..4bd7969 100644 --- a/src/lang/Parser.cpp +++ b/src/lang/Parser.cpp @@ -784,6 +784,14 @@ ExprPtr Parser::parsePrimary() { if (check(TokenKind::Let)) { return parseLetExpr(); } + // Function literal: function(params) expr — a first-class function value. + // Only reachable here (inside expression parsing); a bare `function` at + // statement level is instead a named `function foo(...) = expr;` + // definition, handled by parseFunctionDef before expression parsing ever + // starts. + if (check(TokenKind::Function)) { + return parseFunctionLit(); + } // Parenthesised expression if (match(TokenKind::LParen)) { auto expr = parseExpr(); @@ -927,6 +935,32 @@ void Parser::parseFunctionDef(ParseResult& result) { result.functionDefs.push_back(std::move(def)); } +// --------------------------------------------------------------------------- +// function literal — function(params) expr, usable as a general expression +// (assigned to a variable, passed as an argument, returned from another +// function). Same parameter-list grammar as parseFunctionDef, minus the name +// and the `=`. +// --------------------------------------------------------------------------- +ExprPtr Parser::parseFunctionLit() { + const Token& kw = advance(); // consume 'function' + FunctionLit lit; + lit.loc = kw.loc; + + expect(TokenKind::LParen, "expected '(' after 'function'"); + while (!check(TokenKind::RParen) && !atEnd()) { + FunctionLitParam param; + param.name = expect(TokenKind::Ident, "expected parameter name").text; + if (match(TokenKind::Equals)) + param.defaultVal = parseExpr(); + lit.params.push_back(std::move(param)); + if (!match(TokenKind::Comma)) break; + } + expect(TokenKind::RParen, "expected ')' after parameter list"); + lit.body = parseExpr(); + + return makeExpr(std::move(lit)); +} + // --------------------------------------------------------------------------- // let statement — let(x = expr, ...) { children } // --------------------------------------------------------------------------- diff --git a/src/lang/Parser.h b/src/lang/Parser.h index 881e238..ebd2838 100644 --- a/src/lang/Parser.h +++ b/src/lang/Parser.h @@ -63,6 +63,7 @@ class Parser { ExprPtr parsePostfix(); // handles postfix [idx] after primary ExprPtr parsePrimary(); ExprPtr parseLetExpr(); + ExprPtr parseFunctionLit(); // function(params) expr — a function-literal value VectorElem parseVectorElem(); // one list element: expr, or `each expr` ExprPtr parseListComp(SourceLoc loc); // [for (var = source) body] ListCompBodyPtr parseListCompBody(); // body clause: expr / each expr / if (..) body [else body] diff --git a/src/lang/Value.h b/src/lang/Value.h index 4c04c14..dddeb8c 100644 --- a/src/lang/Value.h +++ b/src/lang/Value.h @@ -1,17 +1,23 @@ #pragma once +#include #include +#include #include namespace chisel::lang { +struct FunctionLit; // Expr.h — only referenced by pointer here +struct ClosureEnv; // defined below, after Value (holds Value members itself) + // --------------------------------------------------------------------------- // Value — the runtime type produced by evaluating an expression. // -// OpenSCAD types: number (double), bool, string, vector (list), range, undef. -// Strings are included for completeness but not yet used in geometry. +// OpenSCAD types: number (double), bool, string, vector (list), range, +// function (closure), undef. Strings are included for completeness but not +// yet used in geometry. // --------------------------------------------------------------------------- struct Value { - enum class Tag { Number, Bool, Vector, String, Range, Undef }; + enum class Tag { Number, Bool, Vector, String, Range, Function, Undef }; Tag tag = Tag::Undef; @@ -26,6 +32,13 @@ struct Value { double rangeStart = 0.0; double rangeStep = 1.0; double rangeEnd = 0.0; + // Function only — the closure: which FunctionLit AST node to invoke, and + // a snapshot of the environment visible where `function(...) ...` was + // written (so the closure still sees those variables later, from a + // different call site). A shared_ptr so copying a Value (e.g. storing it + // in a list, passing it as an argument) is cheap and every copy shares + // the same captured scope. + std::shared_ptr closure; // ---- Factories -------------------------------------------------------- static Value fromNumber(double v) { Value r; r.tag = Tag::Number; r.number = v; return r; } @@ -37,15 +50,19 @@ struct Value { r.rangeStart = start; r.rangeStep = step; r.rangeEnd = end; return r; } + static Value fromClosure(std::shared_ptr env) { + Value r; r.tag = Tag::Function; r.closure = std::move(env); return r; + } static Value undef() { return {}; } // ---- Type queries ----------------------------------------------------- - bool isNumber() const { return tag == Tag::Number; } - bool isBool() const { return tag == Tag::Bool; } - bool isVector() const { return tag == Tag::Vector; } - bool isString() const { return tag == Tag::String; } - bool isRange() const { return tag == Tag::Range; } - bool isUndef() const { return tag == Tag::Undef; } + bool isNumber() const { return tag == Tag::Number; } + bool isBool() const { return tag == Tag::Bool; } + bool isVector() const { return tag == Tag::Vector; } + bool isString() const { return tag == Tag::String; } + bool isRange() const { return tag == Tag::Range; } + bool isFunction() const { return tag == Tag::Function; } + bool isUndef() const { return tag == Tag::Undef; } // ---- Accessors -------------------------------------------------------- double asNumber() const { return number; } @@ -57,14 +74,31 @@ struct Value { // 0, empty vector, and undef are falsy; everything else truthy. explicit operator bool() const { switch (tag) { - case Tag::Number: return number != 0.0; - case Tag::Bool: return boolean; - case Tag::Vector: return !vec.empty(); - case Tag::String: return !str.empty(); - case Tag::Range: return true; - default: return false; + case Tag::Number: return number != 0.0; + case Tag::Bool: return boolean; + case Tag::Vector: return !vec.empty(); + case Tag::String: return !str.empty(); + case Tag::Range: return true; + case Tag::Function: return true; + default: return false; } } }; +// Defined after Value so its `vars` map can hold Value members directly; +// Value itself only ever touches ClosureEnv through shared_ptr indirection +// (legal with an incomplete type), so this ordering doesn't create a cycle. +struct ClosureEnv { + const FunctionLit* def = nullptr; // params + body + std::unordered_map vars; // captured defining scope + // Set (by Interpreter::assignVar) when this closure was bound directly + // to a name — `name = function(...) ...;` — so its body can recurse by + // that name. Deliberately just a string, not a Value stored in `vars`: + // a Value naming this same closure would hold a shared_ptr + // back to this very object, an uncollectable reference cycle that leaks + // it for the process lifetime. Interpreter::callClosure resolves this + // into a fresh, call-local binding instead. + std::string selfName; +}; + } // namespace chisel::lang diff --git a/tests/test_interpreter.cpp b/tests/test_interpreter.cpp index a54cc77..96ccd8f 100644 --- a/tests/test_interpreter.cpp +++ b/tests/test_interpreter.cpp @@ -3,6 +3,7 @@ #include "lang/Interpreter.h" #include "lang/Lexer.h" #include "lang/Parser.h" +#include using namespace chisel::lang; using Catch::Approx; @@ -797,11 +798,102 @@ TEST_CASE("Interp:is_bool/is_num/is_string/is_list each match only their own typ REQUIRE(bool(evalVal("is_list(\"abc\")")) == false); } -TEST_CASE("Interp:is_function is always false (no function-literal values exist yet)", "[interp][v35]") { +TEST_CASE("Interp:is_function matches only function-literal values", "[interp][v35]") { REQUIRE(bool(evalVal("is_function(42)")) == false); REQUIRE(bool(evalVal("is_function(\"f\")")) == false); } +// --------------------------------------------------------------------------- +// v3.6 — first-class function literals: `f = function(x) expr;` +// --------------------------------------------------------------------------- +TEST_CASE("Interp:function literal assigned to a variable is callable by name", "[interp][v36]") { + auto ctx = loadEnvWithFuncs("f = function(x) x * 2;"); + REQUIRE(ctx.interp.getVar("f").isFunction()); + REQUIRE(bool(ctx.interp.getVar("f")) == true); // functions are always truthy + ExprNode call = makeCall("f", {21.0}); + REQUIRE(ctx.interp.evalNumber(call) == Approx(42.0)); +} + +TEST_CASE("Interp:is_function recognizes a function-literal value", "[interp][v36]") { + auto ctx = loadEnvWithFuncs("f = function(x) x;"); + REQUIRE(ctx.interp.getVar("f").isFunction()); + FunctionCall fc; + fc.name = "is_function"; + FunctionArg arg; + arg.value = makeExpr(VarRef{"f", {}}); + fc.args.push_back(std::move(arg)); + ExprNode isFnCall = std::move(fc); + REQUIRE(bool(ctx.interp.evaluate(isFnCall)) == true); +} + +TEST_CASE("Interp:function literal with a default parameter", "[interp][v36]") { + auto ctx = loadEnvWithFuncs("f = function(x, y=10) x + y;"); + ExprNode call1 = makeCall("f", {5.0}); + REQUIRE(ctx.interp.evalNumber(call1) == Approx(15.0)); + ExprNode call2 = makeCall("f", {5.0, 1.0}); + REQUIRE(ctx.interp.evalNumber(call2) == Approx(6.0)); +} + +TEST_CASE("Interp:function literal closes over its defining scope", "[interp][v36]") { + auto ctx = loadEnvWithFuncs("k = 100; f = function(x) x + k;"); + ExprNode call = makeCall("f", {5.0}); + REQUIRE(ctx.interp.evalNumber(call) == Approx(105.0)); +} + +TEST_CASE("Interp:function literal passed as a higher-order argument", "[interp][v36]") { + auto ctx = loadEnvWithFuncs("function apply(f, x) = f(x); g = function(x) x + 1;"); + FunctionCall fc; + fc.name = "apply"; + FunctionArg fArg; fArg.value = makeExpr(VarRef{"g", {}}); + FunctionArg xArg; xArg.value = makeExpr(NumberLit{10.0, {}}); + fc.args.push_back(std::move(fArg)); + fc.args.push_back(std::move(xArg)); + ExprNode call = std::move(fc); + REQUIRE(ctx.interp.evalNumber(call) == Approx(11.0)); +} + +TEST_CASE("Interp:function literal bound directly to a name can recurse by that name", "[interp][v36]") { + // `name = function(...) ... name(...) ...;` seeds the closure's own + // captured environment with `name -> itself`, so this doesn't need a + // named `function` def to recurse. + auto ctx = loadEnvWithFuncs("fact = function(n) n <= 1 ? 1 : n * fact(n - 1);"); + ExprNode call = makeCall("fact", {6.0}); + REQUIRE(ctx.interp.evalNumber(call) == Approx(720.0)); +} + +TEST_CASE("Interp:copying a function-literal variable does not leak into the original's scope", "[interp][v36]") { + // `h = f;` is a plain copy, not a new literal — it must not retroactively + // mutate f's captured environment (e.g. by injecting "h" as a self-ref). + auto ctx = loadEnvWithFuncs("f = function(x) x + 1; h = f;"); + ExprNode callF = makeCall("f", {1.0}); + ExprNode callH = makeCall("h", {2.0}); + REQUIRE(ctx.interp.evalNumber(callF) == Approx(2.0)); + REQUIRE(ctx.interp.evalNumber(callH) == Approx(3.0)); +} + +TEST_CASE("Interp:self-recursive function literal does not leak a shared_ptr reference cycle", + "[interp][v36][bugfix]") { + // Regression test for a real bug: seeding recursion by storing a Value + // that itself holds `closure` back into `closure->vars` would make + // ClosureEnv reference itself through a shared_ptr cycle, which + // shared_ptr can never collect — every self-recursive function literal + // would leak its captured environment for the process lifetime. + std::weak_ptr weakClosure; + { + auto ctx = loadEnvWithFuncs("fact = function(n) n <= 1 ? 1 : n * fact(n - 1);"); + Value f = ctx.interp.getVar("fact"); + REQUIRE(f.isFunction()); + weakClosure = f.closure; + REQUIRE_FALSE(weakClosure.expired()); + + ExprNode call = makeCall("fact", {6.0}); + REQUIRE(ctx.interp.evalNumber(call) == Approx(720.0)); + } + // ctx (and its Interpreter's env) and the local `f` are both gone now — + // if the ClosureEnv were kept alive by a cycle, this would still be false. + REQUIRE(weakClosure.expired()); +} + // --------------------------------------------------------------------------- // v3.5 — search() // --------------------------------------------------------------------------- diff --git a/tests/test_parser.cpp b/tests/test_parser.cpp index ec0946b..e769e23 100644 --- a/tests/test_parser.cpp +++ b/tests/test_parser.cpp @@ -498,6 +498,45 @@ TEST_CASE("Parser:range literal as a function argument", "[parser]") { REQUIRE(std::holds_alternative(*call.args[0].value)); } +// --------------------------------------------------------------------------- +// Function literals: function(params) expr +// --------------------------------------------------------------------------- +TEST_CASE("Parser:function literal assigned to a variable", "[parser][v36]") { + auto r = parse("f = function(x) x * 2;"); + REQUIRE(r.assignments.size() == 1); + const auto& lit = std::get(*r.assignments[0].value); + REQUIRE(lit.params.size() == 1); + REQUIRE(lit.params[0].name == "x"); + REQUIRE(lit.params[0].defaultVal == nullptr); + REQUIRE(std::holds_alternative(*lit.body)); +} + +TEST_CASE("Parser:function literal with multiple params and a default value", "[parser][v36]") { + auto r = parse("f = function(x, y=10) x + y;"); + const auto& lit = std::get(*r.assignments[0].value); + REQUIRE(lit.params.size() == 2); + REQUIRE(lit.params[0].name == "x"); + REQUIRE(lit.params[0].defaultVal == nullptr); + REQUIRE(lit.params[1].name == "y"); + REQUIRE(lit.params[1].defaultVal != nullptr); +} + +TEST_CASE("Parser:function literal passed directly as a call argument", "[parser][v36]") { + auto r = parse("y = apply(function(x) x + 1, 10);"); + REQUIRE(r.assignments.size() == 1); + const auto& call = std::get(*r.assignments[0].value); + REQUIRE(call.args.size() == 2); + REQUIRE(std::holds_alternative(*call.args[0].value)); +} + +TEST_CASE("Parser:named function definition is unaffected by function-literal parsing", "[parser][v36]") { + // Statement-level `function name(...) = expr;` must still work exactly + // as before now that `function` also starts an expression-level literal. + auto r = parse("function double(x) = x * 2;"); + REQUIRE(r.functionDefs.size() == 1); + REQUIRE(r.functionDefs[0].name == "double"); +} + // --------------------------------------------------------------------------- // List comprehensions and `each` // ---------------------------------------------------------------------------