Skip to content

feat(compiler): implement GDScript lambda expressions from semantics to C codegen - #60

Merged
SuperIceCN merged 9 commits into
masterfrom
feat/frontend/lambda
Aug 18, 2026
Merged

feat(compiler): implement GDScript lambda expressions from semantics to C codegen#60
SuperIceCN merged 9 commits into
masterfrom
feat/frontend/lambda

Conversation

@SuperIceCN

Copy link
Copy Markdown
Collaborator

Summary

Implement GDScript func(...) lambda expressions end-to-end across frontend, LIR, C backend, and runtime helpers. Recorded lambdas in function/constructor bodies now plan captures, synthesize hidden _lambda_<k> shells, lower to construct_lambda, and materialize custom Callables. Property-initializer, parameter-default, match, and await lambda surfaces remain fail-closed.

What changed

  • Frontend inventory / capture planning: FrontendVariableAnalyzer.bindLambdaInventory binds lambda parameters, locals, and CAPTURE slots. FrontendLambdaCapturePlanner derives ordered captures (outer PARAMETER / LOCAL / CAPTURE, plus leading self when needed) without capturing class/global/utility names.
  • Frontend suite / typing: FrontendSuiteResolver publishes the first complete FrontendLambdaPlan after filling declaration-site capture types and the declared return type. Recorded lambdas type as RESOLVED(GdCallableType); FrontendTypeCheckAnalyzer type-checks lambda bodies as independent callable islands.
  • Compile gate: FrontendCompileCheckAnalyzer releases recorded lambdas (published plan + published body) onto the compile surface and recursively scans body facts. Unrecorded lambdas stay fail-closed.
  • Lowering: FrontendLoweringFunctionPreparationPass synthesizes hidden static _lambda_<k> shells (is_lambda + is_hidden + is_static). CFG emits LambdaConstructItem; body lowering materializes ConstructLambdaInsn from the frozen plan only.
  • LIR: add construct_lambda "<name>" $capture.... LirFunctionDef preserves capture insertion order; DomLirSerializer writes real <capture name type> entries.
  • Backend / runtime: ConstructInsnGen.emitConstructLambda copies captures into a heap ${Class}_Capture_${func} block and calls gdcc_new_lambda_callable. CCodegen copies _capture->name into matching locals before __prepare__ and excludes capture locals from default construction. entry.h.ftl generates call / is_valid / free / get_argument_count wrappers.
  • Docs: add frontend_lambda_implementation.md as the single fact source and retarget LIR, runtime, compile-gate, lowering, signal, and test-suite docs.
  • Tests: focused frontend/backend coverage plus test-suite fixtures under script/lambda/ and validation/lambda/, including signal_connect_lambda.gd.

Why

  • Lambda expressions were previously compile-blocked or unsupported, so a core GDScript surface could not be compiled or connected to Signal / Callable consumers.
  • Design rationale: freeze one FrontendLambdaPlan per recorded lambda and consume it downstream instead of re-deriving captures. Use a single construct_lambda opcode with self as a leading capture, rather than adding OPCODE_CREATE_SELF_LAMBDA or reusing construct_callable / construct_standalone_callable.
  • Capture types come from the outer binding's declaration-site type (including := stabilization), not later assertions. Captures are construction-time copies, matching Godot user-visible semantics while using a userdata ABI.

Affected packages/files

  • gd.script.gdcc.frontend.sema.** (FrontendLambdaPlan, FrontendLambdaCapturePlanner, FrontendSuiteResolver, FrontendVariableAnalyzer, FrontendCompileCheckAnalyzer, FrontendTypeCheckAnalyzer)
  • gd.script.gdcc.frontend.lowering.** (FrontendLoweringFunctionPreparationPass, LambdaConstructItem, FrontendLambdaConstructInsnLoweringProcessor)
  • gd.script.gdcc.lir.** (ConstructLambdaInsn, LirFunctionDef, DomLirSerializer)
  • gd.script.gdcc.backend.c.gen.** (ConstructInsnGen, CCodegen, CGenHelper)
  • src/main/c/codegen/include_451/gdcc/gdcc_callable.h, src/main/c/codegen/template_451/entry.h.ftl, func.ftl
  • doc/module_impl/frontend/frontend_lambda_implementation.md, doc/gdcc_low_ir.md, doc/gdcc_runtime_lib.md
  • Test-suite fixtures under src/test/resources/**/script/lambda/ and validation/lambda/

Validation

  • pwsh -ExecutionPolicy Bypass -File script/run-gradle-targeted-tests.ps1 -Tests FrontendLambdaCapturePlannerTest,FrontendLambdaSuiteResolutionTest,FrontendLambdaExpressionTypeTest,FrontendLambdaInventoryTest,FrontendLambdaPlanSideTableTest,FrontendLambdaLoweringTest,ConstructLambdaInsnGenTest,FuncHeaderCaptureTemplateTest,FrontendCompileCheckAnalyzerTest,FrontendTypeCheckAnalyzerTest

Result: BUILD SUCCESSFUL (174 tests: 12 capture-planner, 11 suite-resolution, 2 expression-type, 2 inventory, 3 plan side-table, 14 lowering, 10 construct-codegen, 4 capture-template, 67 compile-check, 49 type-check; 0 failures, 0 skipped)

Risks / Notes

  • Explicit non-goals: lambdas in property initializers, parameter defaults, or class-level expressions; lambda-owned parameter defaults; match / block-local const / await inside lambda bodies; CAPTURE as a direct-slot alias root; Callable.bind / unbind; ClassDB registration of hidden lambda functions; compiler-only types on lambda params/returns/captures.
  • Behavior contract changes: recorded lambdas are now compile-ready. New LIR opcode construct_lambda is added. _lambda_ is a reserved compiler-owned prefix (sema.class_skeleton + skip).
  • Deliberate Godot divergence: no GDScriptLambdaSelfCallable; object_id is the cached fat-pointer instance_id only when capturesSelf, otherwise 0. Captures travel in a userdata struct plus a trailing _capture pointer, not as prefixed Godot call arguments. Hash/equal stay Godot's default (call func + userdata identity).
  • Missing or conflicting published plans fail-fast in lowering (InvalidInsnException); source errors stay on DiagnosticManager + skip.
  • Engine tests (ConstructLambdaInsnGenEngineTest) are environment-aware and skip gracefully when Zig / GODOT_BIN is unavailable.

Key behaviors covered (Optional)

  • Nested lambdas transfer outer captures through intermediate layers unless a same-name param/local shadows them.
  • self is a leading capture of the enclosing class object type. Signal.connect(func(): self.foo()) uses ObjectDB-checked is_valid_func so the connection invalidates after instance free.
  • User-visible arity is the source parameter count; captures are excluded from get_argument_count.
  • Capture locals are excluded from __prepare__ default construction, then copied from _capture->name in the prologue.
  • Silent local stabilization does not refine var cb := func(): ... slots to Callable.

Diff stats (Optional)

  • 107 files changed, 7593 insertions(+), 341 deletions(-)

Breaking changes (Optional)

  • None

Related docs (Optional)

  • doc/module_impl/frontend/frontend_lambda_implementation.md
  • doc/module_impl/frontend/frontend_rules.md
  • doc/gdcc_low_ir.md
  • doc/gdcc_runtime_lib.md
  • doc/module_impl/frontend/frontend_signal_support.md

- Track ordered lambda captures with self and nested-scope handling
- Publish validated lambda plans through frontend analysis data
- Serialize capture metadata into LIR and generate compatible C function headers
- Add coverage for capture planning, side-table stability, serialization, and templates
- Document lambda implementation and diagnostic requirements
- Bind lambda parameters, locals, and captures with placeholder types
- Plan nested capture propagation and instance self captures
- Preserve fail-closed handling for unsupported match and const subtrees
- Add diagnostics, tests, and implementation documentation
- Resolve lambda-local values, captures, and nested suite ownership
- Add transactional resolution patches with fail-closed diagnostics
- Improve visible-value and lexical environment handling
- Expand semantic analysis and lambda resolution test coverage
- Update frontend implementation documentation
- Publish Callable types for recorded lambda expressions
- Type-check nested lambda bodies with inherited callable context
- Preserve Variant inference for silent lambda initializers
- Add compile-time blockers until lambda lowering is implemented
- Expand semantic analysis tests and implementation documentation
- Materialize hidden static lambda functions with typed parameters, returns, and captures
- Lower nested lambda bodies through the executable CFG and instruction pipeline
- Propagate declared lambda return types through semantic analysis and lowering
- Reserve the synthetic `_lambda_` namespace and fail fast on missing plans or collisions
- Expand frontend tests and implementation documentation
- Lower recorded lambda expressions into construct operations
- Preserve local, self, and nested capture bindings
- Validate synthesized lambda metadata during instruction lowering
- Thread result value identities through CFG construction
- Expand frontend tests and implementation documentation
…port

- Generate lambda Callable callbacks with capture storage and lifecycle handling
- Preserve captured values and self object identity across lambda invocation
- Add backend, frontend, and integration coverage for lambda construction
- Update lambda implementation and runtime documentation
- Finalize lambda semantic analysis, capture propagation, and lowering across nested scopes
- Add compile-time diagnostics for invalid and unsupported lambda constructs
- Align Callable construction and generated capture handling with backend runtime requirements
- Expand unit and integration coverage for lambdas and signal connections
- Consolidate frontend lambda documentation and implementation tracking
Copilot AI lite review requested due to automatic review settings August 18, 2026 16:35

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@SuperIceCN
SuperIceCN merged commit 188dea5 into master Aug 18, 2026
1 check passed
@SuperIceCN
SuperIceCN deleted the feat/frontend/lambda branch August 18, 2026 16:45
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.

3 participants