Add first-class function literals: function(params) expr (v3.6)#73
Merged
Conversation
Closes the largest gap explicitly deferred by the v3.5 completeness audit. `f = function(x) x*2;` now evaluates to a real closure Value (AST pointer + captured defining scope) that can be assigned to a variable, stored in a list, passed as an argument to (or returned from) another function, and invoked later via ordinary `f(...)` call syntax wherever `f` names a variable holding one — matching OpenSCAD 2019.05+ semantics, including lexical scoping and direct self- recursion for `name = function(...) ...name(...)...;`-style bindings. is_function() now recognizes actual function values instead of always returning false.
particlesector
commented
Jul 15, 2026
particlesector
left a comment
Owner
Author
There was a problem hiding this comment.
Solid implementation overall — closures, lexical capture, and the self-recursion aliasing-safety guard are all handled correctly and well-tested. One real bug found (inline): the self-recursion seeding creates a shared_ptr reference cycle that leaks the closure's environment.
Generated by Claude Code
Review feedback on #73: seeding recursion by storing a Value (holding closure) back into closure->vars made ClosureEnv reference itself through a shared_ptr cycle, which shared_ptr can never collect — every self-recursive function literal leaked its captured environment for the process lifetime. Replace the stored self-reference with a plain selfName string on ClosureEnv; callClosure now resolves it into a fresh, call-local binding instead of a persistent cycle. Also fixes a dangling-reference bug this surfaced: callClosure took its callee by const reference (typically an element inside m_env) but reassigns m_env wholesale as its first step, so any post-reassignment access to that reference was already a use-after-free — now takes the callee by value. Adds a weak_ptr-based regression test that fails against the old code and passes against the fix.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes the largest gap explicitly deferred by the v3.5 completeness audit (
docs/roadmap.md): first-class function literals, OpenSCAD 2019.05+.f = function(x) x*2;now evaluates to a real closureValue(AST pointer + captured defining scope) that can be assigned to a variable, stored in a list, passed as an argument to (or returned from) another function, and invoked later via ordinaryf(...)call syntax whereverfnames a variable holding one.function(...) ...was written, not the scope it's called from.fact = function(n) n<=1 ? 1 : n*fact(n-1);recurses by name without needing a namedfunctiondef — implemented narrowly (only when the literal is directly on the assignment's right-hand side) so a plain copy likeg = f;can't leakgintof's own captured scope.is_function()now recognizes actual function values instead of always returningfalse.echo()/assert()formatting for a function value printsfunction(params).Implementation
Valuegains aFunctiontag holding a closure via a newClosureEnvstruct (declared right afterValueto avoid a self-referential-type problem, sinceValueonly ever touches it throughshared_ptrindirection).FunctionLitAST node (src/lang/Expr.h) and parser support forfunction(params) expras a primary expression (src/lang/Parser.cpp), without disturbing the existing statement-levelfunction name(...) = expr;grammar.FunctionCalldispatch now checks whether the callee name is a variable holding a closure before falling back to namedfunctiondefs / builtins (src/lang/Interpreter.cpp).Interpreter::assignVarhelper centralizes the "real assignment" call sites (global assignments, local block assignments,let(...)bindings) so only those get the self-recursion seeding — parameter/loop-variable binding still uses plainsetVarto avoid mutating a closure that arrived as an argument.Test plan
tests/test_parser.cpp(literal parsing, params/defaults, passing a literal as a call argument, named-def parsing unaffected) andtests/test_interpreter.cpp(basic call,is_function, defaults, lexical capture, higher-order args, self-recursion, copy-aliasing safety).is_functionall pass.🤖 Generated with Claude Code
Generated by Claude Code