Skip to content

feat(compiler): implement first-class Signal values and Callable method references - #59

Merged
SuperIceCN merged 11 commits into
masterfrom
feat/frontend/signal
Aug 15, 2026
Merged

feat(compiler): implement first-class Signal values and Callable method references#59
SuperIceCN merged 11 commits into
masterfrom
feat/frontend/signal

Conversation

@SuperIceCN

Copy link
Copy Markdown
Collaborator

Summary

Implement GDScript signal as a first-class value, plus Signal.emit / connect / disconnect and method / utility value references as Callable, end-to-end across frontend, LIR, C backend, and runtime helpers. Godot baseline is 4.5.1. await signal and coroutine lowering remain out of scope.

What changed

  • Frontend compile gate: FrontendCompileCheckAnalyzer now only blocks remaining rejected surfaces (Dictionary instance method-refs and builtin type-meta static method-refs). Signal value reads, .emit / .connect / .disconnect, Object/self methods, non-Dictionary builtin instance methods, GDCC/engine statics, and bare utility value reads are compile-ready.
  • Scope / skeleton: ClassScope and ScopeSignalResolver publish signals as read-only GdSignalType values. FrontendClassSkeletonBuilder registers current-class signal declarations and rejects GDCC shadows of inherited engine/native signals while still allowing nearest-child GDCC shadowing.
  • CFG / lowering: add SignalLoadItem, CallableLoadItem, and StandaloneCallableLoadItem. Body lowering materializes ConstructSignalInsn, ConstructCallableInsn, and ConstructStandaloneCallableInsn from published facts only. Inherited static references resolve to the declaring owner via ClassRegistry.findStaticFunctionInHierarchy.
  • LIR: add construct_signal and construct_standalone_callable; freeze construct_callable at (VARIABLE, STRING) with live receiver + method name. Parser/serializer round-trip is covered.
  • Backend: ConstructInsnGen emits godot_new_Signal_with_Object_StringName, godot_new_Callable_with_Object_StringName, godot_Callable_create for builtin receivers, and gdcc_new_standalone_callable / godot_callable_custom_create2 for static/utility trampolines. entry.c.ftl registers current-class signals in ClassDB. Builtin Signal.emit uses generated vararg argv/argc wrappers.
  • Runtime: add gdcc_callable.h standalone-callable registry and trampolines. Object-liveness failure now publishes a typed default return value before cleanup so Signal-returning paths stay defined.
  • Docs: replace the phased plan with frontend_signal_support.md as the single fact source; retarget remaining cross-references.
  • Tests: LIR contract, compile-gate, CFG/lowering, codegen, ClassDB registration, Variant pack/unpack, inherited static owner resolution, and test-suite .gd scripts for emit/connect, interop, engine/inherited, null receiver, and dynamic Variant named-get.

Why

  • Signal reads, emit/connect, and method/utility value references previously crashed CFG or were compile-blocked, so a core GDScript surface could not be compiled.
  • Design rationale: keep Object/self and builtin instance refs on construct_callable (same operand schema, C-exit dispatch by static receiver type) and put receiver-less static/utility refs on construct_standalone_callable. Do not invent a Variant-receiver opcode, reuse CONSTRUCT_LAMBDA, or implement CALL_STATIC_METHOD codegen in this PR.
  • Lowering consumes published semantic facts only. Missing or conflicting facts fail-fast instead of re-binding.

Affected packages/files

  • gd.script.gdcc.frontend.sema.**, gd.script.gdcc.frontend.lowering.**, gd.script.gdcc.frontend.scope.ClassScope
  • gd.script.gdcc.scope.** (ClassRegistry, ScopeSignalResolver)
  • gd.script.gdcc.lir.insn.** (ConstructSignalInsn, ConstructCallableInsn, ConstructStandaloneCallableInsn)
  • gd.script.gdcc.backend.c.gen.** (ConstructInsnGen, GodotBuiltinGenerator, CGenHelper, entry.c.ftl)
  • src/main/c/codegen/include_451/gdcc/gdcc_callable.h
  • doc/module_impl/frontend/frontend_signal_support.md, doc/gdcc_low_ir.md, doc/gdcc_runtime_lib.md
  • Test-suite fixtures under src/test/resources/**/script/member/ and runtime/dynamic_member_variant_signal_read.gd

Validation

  • pwsh -ExecutionPolicy Bypass -File script/run-gradle-targeted-tests.ps1 -Tests ConstructSignalInsnContractTest,ConstructCallableInsnContractTest,ConstructStandaloneCallableInsnContractTest,FrontendCompileCheckAnalyzerTest,CConstructInsnGenTest,CCodegenSignalRegistrationTest

Result: BUILD SUCCESSFUL (170 tests: 11 signal-insn, 12 callable-insn, 12 standalone-callable-insn, 63 compile-check, 68 construct-codegen, 4 signal-registration; 0 failures, 0 skipped)

Risks / Notes

  • Explicit non-goals: await signal / coroutine state machines, lambda/capture, builtin type-meta method values (Vector2.abs), constructor values (Node.new), dict.clear as a method reference, CALL_STATIC_METHOD CInsnGen, new bind / unbind / RPC lowering, static arity/type rejection of emit against the declared signature, custom signal type annotations, and bare CONNECT_* identifiers.
  • Behavior contract changes: compile gate no longer rejects signal value reads, .emit / .connect / .disconnect, or the supported Callable value-reference surfaces. New LIR opcodes construct_signal and construct_standalone_callable are added; construct_callable no longer accepts the old 1-operand form.
  • Engine static Callables report is_valid() == true via the custom trampoline (intentional deviation from official GDScriptNativeClass Callables). Do not use JSON.parse_string.is_valid() as a gold standard.
  • Signal / Callable store a non-owning ObjectID. Null/freed Object receivers hard-fail via AssertObjectLiveInsn; self and RefCounted skip that guard. Builtin and standalone paths do not emit an Object guard.
  • ClassRegistry.checkAssignable(Signal, Variant) stays false. Cross-Variant / container / named / indexed / emit-vararg edges require explicit pack/unpack.
  • Engine tests that need Zig + GODOT_BIN skip gracefully when the environment is unavailable.

Key behaviors covered (Optional)

  • Bare and receiver signal reads materialize a new godot_Signal value; they are not stored fields.
  • sig.emit(...) is a true vararg builtin call. Declared signal parameters are ClassDB metadata only and do not reject extra or mistyped arguments at the call site.
  • Object/self method refs use Callable(ObjectID, name). Non-Dictionary builtin instance refs copy the receiver into a VariantCallable. Static/utility refs use a custom trampoline bound to the declaring owner.
  • Inherited GDCC signals may nearest-child shadow; GDCC must not redeclare inherited engine/native signals.
  • Variant named-get of a ClassDB-registered signal may still produce TYPE_SIGNAL at runtime; that is an engine get_named side effect, not frontend guessing a dynamic member as RESOLVED SIGNAL.

Diff stats (Optional)

  • 89 files changed, 7293 insertions(+), 333 deletions(-)

Breaking changes (Optional)

  • None

Related docs (Optional)

  • doc/module_impl/frontend/frontend_signal_support.md
  • doc/module_impl/frontend/frontend_signal_implementation.md
  • doc/gdcc_low_ir.md
  • doc/gdcc_runtime_lib.md
  • doc/module_impl/backend/godot_binding_implementation.md

- Add frontend_signal_support_plan.md covering scope, Godot semantic baseline, gaps, design decisions, and phased acceptance criteria
…pile mode

- Report compile blockers for receiver-qualified signal reads and Signal method calls before the RESOLVED short-circuit
- Block bare signal / method / utility-function identifier reads that would crash CFG, excluding call callees
- Extend test coverage for the new blocked surfaces
- Add CONSTRUCT_SIGNAL LIR instruction with serialization and round-trip parsing
- Route signal reads through the CFG and lower them into construct operations
- Generate godot_Signal construction in the C backend with builtin value lifecycle handling
- Lift the compile gate for signal value reads while keeping emit/connect blocked
- Extend test coverage across CFG, lowering, codegen, and the unit test suite
…ne/native shadows

- Reject GDCC signal redeclarations that shadow inherited engine/native signals during skeleton with a compile-time diagnostic, while keeping nearest-child GDCC shadowing intact
- Render signal parameter metadata through the method-arg usage surface in the C backend
- Emit `// Signals` ClassDB registration in the entry template, passing `NULL, 0` for zero-arg signals and releasing parameter metadata after registration
- Extend test coverage across skeleton conflict guards, metadata rendering, and signal registration
- Mark Phase 2 of the signal support plan as completed
- Lift the compile-time blocker on Signal.emit so it lowers through the builtin vararg call path while keeping connect/disconnect blocked
- Emit runtime argv/argc handling in generated builtin method wrappers, avoiding static initializer constraints for variable-length argument lists
- Sort generated builtin binding output deterministically across the C header and source
- Extend test coverage across frontend lowering, type and compile-check analysis, and C backend codegen
- Update signal support plan and related implementation docs
…llable

- Rework CONSTRUCT_CALLABLE to take a live receiver and method name, serialized and round-tripped through the parser
- Route RESOLVED Object/self method reads through a new CFG callable-load item and lower them into construct_callable while keeping static/utility/builtin references compile-blocked
- Generate godot_new_Callable_with_Object_StringName in the C backend with builtin value lifecycle handling
- Extend test coverage across CFG, lowering, codegen, and the unit test suite
- Update the signal support plan and implementation docs
…e values

- Load bare function identifiers through a dedicated standalone callable CFG item and lower them into a new construct callable instruction with parsed round-trip support
- Generate godot_Callable construction in the C backend with lifecycle handling
- Extend test coverage across CFG, lowering, codegen, and LIR contracts
- Update signal support plan and related docs
…osing the compile gate

- Consolidate implemented signal semantics into a dedicated fact source and mark the support plan Phase 5 complete
- Release remaining bare utility value references while keeping constructor, lambda, and await surfaces blocked
- Anchor Signal↔Variant pack/unpack boundaries and negative compile-fail contracts in targeted tests
- Extend the unit test suite with signal emit/connect, interop, engine/inherited, null-receiver, and dynamic-read scripts
…claring owner

- Look up static functions through the class hierarchy so subclass references to inherited statics bind the Callable to the declaring class
- Route frontend lowering through the new hierarchy lookup and validate the declaring owner in C codegen with fail-fast on unresolved targets
- Extend test coverage across class registry, frontend lowering, and backend codegen
- Initialize non-object return slots before cleanup when assert_object_live fails
- Cover Signal return defaults on the liveness-fail edge in codegen tests
- Stabilize deferred-connection tests by waiting an extra idle frame
…e fact source

- Merge the signal support plan and implementation notes into a unified fact source and remove the superseded plan document
- Retarget remaining doc references to the consolidated document
- Clean up stale phase markers in code and test comments
Copilot AI lite review requested due to automatic review settings August 15, 2026 06: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 06c1ac8 into master Aug 15, 2026
1 check passed
@SuperIceCN
SuperIceCN deleted the feat/frontend/signal branch August 15, 2026 06:36
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