Skip to content

Add first-class function literals: function(params) expr (v3.6)#73

Merged
particlesector merged 2 commits into
mainfrom
claude/openscad-v3.5-roadmap-3r5uec
Jul 16, 2026
Merged

Add first-class function literals: function(params) expr (v3.6)#73
particlesector merged 2 commits into
mainfrom
claude/openscad-v3.5-roadmap-3r5uec

Conversation

@particlesector

Copy link
Copy Markdown
Owner

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 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.
  • Lexical scoping: a closure sees the variables visible where function(...) ... was written, not the scope it's called from.
  • Direct self-recursion: fact = function(n) n<=1 ? 1 : n*fact(n-1); recurses by name without needing a named function def — implemented narrowly (only when the literal is directly on the assignment's right-hand side) so a plain copy like g = f; can't leak g into f's own captured scope.
  • is_function() now recognizes actual function values instead of always returning false.
  • echo()/assert() formatting for a function value prints function(params).

Implementation

  • Value gains a Function tag holding a closure via a new ClosureEnv struct (declared right after Value to avoid a self-referential-type problem, since Value only ever touches it through shared_ptr indirection).
  • New FunctionLit AST node (src/lang/Expr.h) and parser support for function(params) expr as a primary expression (src/lang/Parser.cpp), without disturbing the existing statement-level function name(...) = expr; grammar.
  • FunctionCall dispatch now checks whether the callee name is a variable holding a closure before falling back to named function defs / builtins (src/lang/Interpreter.cpp).
  • A new Interpreter::assignVar helper 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 plain setVar to avoid mutating a closure that arrived as an argument.

Test plan

  • New Catch2 unit tests in tests/test_parser.cpp (literal parsing, params/defaults, passing a literal as a call argument, named-def parsing unaffected) and tests/test_interpreter.cpp (basic call, is_function, defaults, lexical capture, higher-order args, self-recursion, copy-aliasing safety).
  • Verified end-to-end against the real source files with a standalone harness (the language subsystem has no external deps, so it compiles/runs without the Vulkan/vcpkg toolchain): closures, higher-order args, lexical capture, recursion, aliasing safety, defaults, list storage, and is_function all pass.
  • CI green on the branch.

🤖 Generated with Claude Code


Generated by Claude Code

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 particlesector left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/lang/Interpreter.cpp
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.
@particlesector
particlesector merged commit 2d4cb03 into main Jul 16, 2026
4 checks passed
@particlesector
particlesector deleted the claude/openscad-v3.5-roadmap-3r5uec branch July 16, 2026 06:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants