feat(compiler): implement first-class Signal values and Callable method references - #59
Merged
Conversation
- 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
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
Implement GDScript
signalas a first-class value, plusSignal.emit/connect/disconnectand method / utility value references asCallable, end-to-end across frontend, LIR, C backend, and runtime helpers. Godot baseline is 4.5.1.await signaland coroutine lowering remain out of scope.What changed
FrontendCompileCheckAnalyzernow 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.ClassScopeandScopeSignalResolverpublish signals as read-onlyGdSignalTypevalues.FrontendClassSkeletonBuilderregisters current-class signal declarations and rejects GDCC shadows of inherited engine/native signals while still allowing nearest-child GDCC shadowing.SignalLoadItem,CallableLoadItem, andStandaloneCallableLoadItem. Body lowering materializesConstructSignalInsn,ConstructCallableInsn, andConstructStandaloneCallableInsnfrom published facts only. Inherited static references resolve to the declaring owner viaClassRegistry.findStaticFunctionInHierarchy.construct_signalandconstruct_standalone_callable; freezeconstruct_callableat(VARIABLE, STRING)with live receiver + method name. Parser/serializer round-trip is covered.ConstructInsnGenemitsgodot_new_Signal_with_Object_StringName,godot_new_Callable_with_Object_StringName,godot_Callable_createfor builtin receivers, andgdcc_new_standalone_callable/godot_callable_custom_create2for static/utility trampolines.entry.c.ftlregisters current-class signals in ClassDB. BuiltinSignal.emituses generated varargargv/argcwrappers.gdcc_callable.hstandalone-callable registry and trampolines. Object-liveness failure now publishes a typed default return value before cleanup so Signal-returning paths stay defined.frontend_signal_support.mdas the single fact source; retarget remaining cross-references..gdscripts for emit/connect, interop, engine/inherited, null receiver, and dynamic Variant named-get.Why
construct_callable(same operand schema, C-exit dispatch by static receiver type) and put receiver-less static/utility refs onconstruct_standalone_callable. Do not invent a Variant-receiver opcode, reuseCONSTRUCT_LAMBDA, or implementCALL_STATIC_METHODcodegen in this PR.Affected packages/files
gd.script.gdcc.frontend.sema.**,gd.script.gdcc.frontend.lowering.**,gd.script.gdcc.frontend.scope.ClassScopegd.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.hdoc/module_impl/frontend/frontend_signal_support.md,doc/gdcc_low_ir.md,doc/gdcc_runtime_lib.mdsrc/test/resources/**/script/member/andruntime/dynamic_member_variant_signal_read.gdValidation
pwsh -ExecutionPolicy Bypass -File script/run-gradle-targeted-tests.ps1 -Tests ConstructSignalInsnContractTest,ConstructCallableInsnContractTest,ConstructStandaloneCallableInsnContractTest,FrontendCompileCheckAnalyzerTest,CConstructInsnGenTest,CCodegenSignalRegistrationTestResult:
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
await signal/ coroutine state machines, lambda/capture, builtin type-meta method values (Vector2.abs), constructor values (Node.new),dict.clearas a method reference,CALL_STATIC_METHODCInsnGen, newbind/unbind/ RPC lowering, static arity/type rejection ofemitagainst the declared signature, customsignaltype annotations, and bareCONNECT_*identifiers..emit/.connect/.disconnect, or the supported Callable value-reference surfaces. New LIR opcodesconstruct_signalandconstruct_standalone_callableare added;construct_callableno longer accepts the old 1-operand form.is_valid() == truevia the custom trampoline (intentional deviation from officialGDScriptNativeClassCallables). Do not useJSON.parse_string.is_valid()as a gold standard.Signal/Callablestore a non-owning ObjectID. Null/freed Object receivers hard-fail viaAssertObjectLiveInsn;selfand 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.GODOT_BINskip gracefully when the environment is unavailable.Key behaviors covered (Optional)
godot_Signalvalue; 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.Callable(ObjectID, name). Non-Dictionary builtin instance refs copy the receiver into aVariantCallable. Static/utility refs use a custom trampoline bound to the declaring owner.TYPE_SIGNALat runtime; that is an engineget_namedside effect, not frontend guessing a dynamic member asRESOLVED SIGNAL.Diff stats (Optional)
Breaking changes (Optional)
Related docs (Optional)
doc/module_impl/frontend/frontend_signal_support.mddoc/module_impl/frontend/frontend_signal_implementation.mddoc/gdcc_low_ir.mddoc/gdcc_runtime_lib.mddoc/module_impl/backend/godot_binding_implementation.md