Skip to content

Keep a static initialiser that throws to its own module - #55

Open
Tutez64 wants to merge 2 commits into
MeguminBOT:mainfrom
Tutez64:bugfix/static-initialiser-throw-unwinds
Open

Tutez64 wants to merge 2 commits into
MeguminBOT:mainfrom
Tutez64:bugfix/static-initialiser-throw-unwinds

Conversation

@Tutez64

@Tutez64 Tutez64 commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

A static initialiser that throws no longer reaches beyond its own field. It could crash the next static function of its class, and, compiled, take modules with nothing wrong with them out of the compile. One commit for each.

1. Unwind a function that throws through a static initialiser.

class L2 {
        static var a:Int = (function():Int { throw 'x'; })();
        public static function b():Int return 4;
}
// L2.b(): before a segfault on hxcpp while the class initialises (null on eval), after 4

A script function unwinds its frame on a throw only inside a script try (inTry). What a static initialiser throws is caught by ScriptedClass.init instead, so the frame of the function that threw stayed on the stack. Building b then took the stack.length > 1 branch of buildFunction, where capturedLocals is null for a class's statics. initField now runs the initialiser as if inside a try, as the :safe bridges already do around the methods they call, and restores the declared names when it throws.

Any other host catch around a script function call outside :safe leaves the frame the same way, Module.start around the module-level program for one. Only the static initialiser is fixed here.

2. Leave a module whose static initialiser threw interpreted.

Compiled, the module ran the initialiser again as it booted, and the throw was the loader refusing the whole batch. batch then split it until the module was alone, and the halves could no longer name each other's classes, so modules with nothing wrong with them were left interpreted as compiled elsewhere. Measured with Lib, six modules using it, and:

class Bad {
        static var v:Int = throw "start";
        public static function go():Int return 3;
}
Before After
Bad refused by the loader skipped: a static initialiser threw when the module started (v: start)
users of Lib compiled 3 of 6, the others uses Lib, which is compiled elsewhere 6 of 6
Bad.go() 3, interpreted 3, interpreted

ScriptedClass keeps the first static initialiser that threw in staticFailure (field: message, reset when the class initialises again). batch hands that to the compiler as the new Unit.skip, and Backend.compile records the module as skipped before writing it. dropDanglingUsers then takes its users with it, as for any skip. onExpressionError is still called as before.

Tests

  • A ClassProbe case for the first. Without the fix it reads GAP ... => null (want 4) on eval and ends the hxcpp suite.
  • StaticInitTest.throwing for the second, over the world above. Without the fix, three checks fail: the module is refused rather than skipped, and 3 of 6 users of the library compiled.
  • test/run.sh eval cpp: 422 / 422 / 455 passed (upstream: 421 / 421 / 451). Conformance columns (eval-interp, hxcpp-interp, hxcpp-cppia, hxcpp-cppia-jit) and the frontier are unchanged.
  • Flixel sandbox (flixel 6.2.0, flixel-addons 4.0.1, flixel-ui 2.6.5), its four projects probed with the JIT on and off: same modules compiled, same byte counts, same two left interpreted (Playground, Shapes) as upstream. HashLink not run.

Tutez64 and others added 2 commits October 7, 2026 01:17
A script function unwinds its frame on a throw only inside a script try.
What a static initialiser throws is caught by ScriptedClass.init instead, so
the frame of a function that threw stayed on the stack, and building the next
static function of the class crashed. The initialiser now runs as if inside a
try.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Compiled, the module ran the initialiser again as it booted, and the throw
was the loader refusing the whole batch. The batch was then split until the
module was alone, and the halves could no longer name each other's classes,
so modules with nothing wrong with them were left interpreted as compiled
elsewhere. The class now remembers the first initialiser that threw, and the
compiler skips its module before writing it, with that as the reason.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Tutez64 Tutez64 changed the title Unwind a function that throws through a static initialiser Keep a static initialiser that throws to its own module Oct 7, 2026
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.

1 participant