fix(compile): #1237 materialize inner function declarations inside class methods#1239
Conversation
…ass methods A `function` declaration inside a class method body was unreferenceable in compiled mode (`ReferenceError: Undefined variable`), while the interpreter ran it correctly. The three sync class-method emitters (EmitMethod, EmitPrivateMethodBody, EmitStaticMethodBody) emitted the body via a bare `foreach EmitStatement` and never wired the inner-function materializer that the plain-function and arrow paths run, so an inner function was collected (its method + display class emitted) but never bound to a name. Add WireInPlaceInnerFunctions(ctx) and call it before the body loop in all three method paths. It wires the #1230 in-place materializer but leaves HoistedInnerFunctions empty, so each inner function materializes at its textual position rather than being hoisted to the top of the body. Methods have no function-level display class, so a top-of-body hoist would snapshot captured method-locals as null before the body assigns them; in-place materialization captures them correctly. In-place is sufficient because the type checker rejects forward references to a method's inner functions, so no valid program needs the two-pass hoist here. Capture semantics then match how arrows inside methods already behave (snapshot at creation). Regression tests in InnerFunctionInMethodTests.cs (11 theories x both modes) cover non-capturing, method-local/param/value-type-boxed captures, self-recursion, shared captures, top-level-var capture, loop-body per-iteration, if-block, static, and private methods.
Follow-up: the CI failure was a real data race, now fixed (commit 3110b3c)The Failure: Root cause: unsynchronized check-then-act in if (!_started) { _started = true; _listener.Start(); StartAccepting(); }Under Reproduced locally on the pre-fix build: Fix: wrap the start-once transition in a lock (double-checked; publish Verification: 40/40 iterations of the three port-sharing tests pass post-fix (no hang), full This is logically independent of the #1237 inner-function fix (first commit); it's bundled here because it's what was blocking this PR's CI. Happy to split it into its own PR if preferred. |
Summary
Fixes #1237. A
functiondeclaration inside a class method body was unreferenceable in compiled mode — every reference threwReferenceError: Undefined variableat runtime, while the interpreter ran the same program correctly. This affected non-capturing, capturing, top-level-in-method, and block-nested inner functions, on instance, static, and private methods.Root cause
The three sync class-method emitters —
EmitMethodandEmitPrivateMethodBody(ILCompiler.Classes.Methods.cs) andEmitStaticMethodBody(ILCompiler.Classes.Static.cs) — emit the body via a bareforeach (var stmt in method.Body) EmitStatement(stmt)and never wired the inner-function materializer that the plain-function (ILCompiler.Functions.cs) and arrow paths run. So an innerfunctionin a method was collected (its method and display class were emitted) but never materialized into a binding, and every reference fell through toThrowUndefinedVariable.Fix
Adds
WireInPlaceInnerFunctions(ctx)(ILCompiler.InnerFunctions.cs) and calls it before the body loop in all three method paths. It wires the #1230 in-place materializer (EmitBlockScopedInnerFunctionDeclaration) onto the method context but deliberately leavesHoistedInnerFunctionsempty, so every inner function declaration — top-level-of-body or block-nested — is created at its textual position by the statement emitter'sStmt.Functionarm.Why in-place rather than the function/arrow path's top-of-body two-pass hoist: unlike a plain function or arrow, a method has no function-level display class, so hoisting a capturing inner function to the top of the body would snapshot its captured method-locals before the body assigns them (null capture, e.g.
k=wherek=Kis expected). In-place materialization snapshots after the preceding statements run, so captures are correct — matching how arrows inside methods already capture. In-place is also sufficient: the type checker rejects forward references to a method's inner functions (mutual recursion / call-before-declaration both error), so no valid program needs the two-pass pre-materialization here.3 files, +38 lines.
Tests
New
SharpTS.Tests/SharedTests/InnerFunctionInMethodTests.cs— 11 theories × both execution modes (22 cases): non-capturing, method-local const, parameter (value-type double boxing), numeric local (boxing), self-recursion, two inner functions sharing a capture, module top-level-var capture (entry-point DC), loop-body per-iteration, if-block block-const, static method, private method.Verification
--verify: passes on all reprosOut of scope (pre-existing, verified not regressions)
0in compiled mode. The identical structure crashes (NullReferenceException) at plain-function scope too — a pre-existing compiled: inner function declaration inside an arrow captures enclosing locals by snapshot (reads null/stale) — true root cause of #302 lodash failure #307/compiled: sibling closures capturing an inner function's mutable local don't share storage (inner function declarations have no scope display class) #313 arrow-scope-DC-through-inner-function-boundary threading bug, not method-specific. This fix makes the method case fail softer (wrong value, no crash).