Compiler segfaults on become inside an @On handler for a runtime-declared event (e.g. UiClicked) #91

Closed
opened 2026-09-02 09:49:02 +02:00 by orkun · 1 comment
Owner

A program that lists @On(UiClicked) handler Nav { ... become SomeScene ... } — an event listener for the engine's built-in UiClicked event (declared/emitted in runtime/native/ui_draw.ludic) whose body uses become — crashes ludicc with SIGSEGV (no diagnostic).

Minimal repro:

program P {
  import "ludic.core/components.ludic"
  ui Menu { panel id: Root w: 100 pad: 8 bg: Color.Gunmetal border: Color.SlateGray { button id: Go text: "Go" font: 0 size: 16 w: 80 } }
  @On(UiClicked) handler Nav { if id == UI_Go { become Two } }
  scene One start { on enter { ui_open(UI_Menu) }  layer L { handler D phase Render { ui_render() } } }
  scene Two { layer L2 { handler D2 phase Render { print(200); quit() } } }
}

Notes narrowing it: become inside an @On handler for a GAME-declared event works; @On(UiClicked) without become works (a listener that only sets a var is fine). It is the combination — become inside a listener for a runtime-spliced event — that segfaults. Likely the scene-register/emit_become lowering assumes an enclosing scene/handler context that an onlisten block for a spliced event doesn't provide.

Workaround: read Ui.clicked in an ordinary phase Update handler instead of @On(UiClicked). A crash should be a diagnostic, and become from any handler ought to work.

A program that lists `@On(UiClicked) handler Nav { ... become SomeScene ... }` — an event listener for the engine's built-in `UiClicked` event (declared/emitted in runtime/native/ui_draw.ludic) whose body uses `become` — crashes ludicc with SIGSEGV (no diagnostic). Minimal repro: ``` program P { import "ludic.core/components.ludic" ui Menu { panel id: Root w: 100 pad: 8 bg: Color.Gunmetal border: Color.SlateGray { button id: Go text: "Go" font: 0 size: 16 w: 80 } } @On(UiClicked) handler Nav { if id == UI_Go { become Two } } scene One start { on enter { ui_open(UI_Menu) } layer L { handler D phase Render { ui_render() } } } scene Two { layer L2 { handler D2 phase Render { print(200); quit() } } } } ``` Notes narrowing it: `become` inside an @On handler for a GAME-declared event works; `@On(UiClicked)` without `become` works (a listener that only sets a var is fine). It is the combination — `become` inside a listener for a **runtime-spliced** event — that segfaults. Likely the scene-register/`emit_become` lowering assumes an enclosing scene/handler context that an onlisten block for a spliced event doesn't provide. Workaround: read `Ui.clicked` in an ordinary `phase Update` handler instead of `@On(UiClicked)`. A crash should be a diagnostic, and `become` from any handler ought to work.
Author
Owner

Fixed on main — the repro compiles and runs (no diagnostic needed: become from any handler now works).

Two things were wrong, both in the compiler:

  • become outside a scene's own layers (an @On(Event) listener, a global handler, a plain function) has no enclosing scene at compile time. The lowering reused the last emitted handler's scene id — or dereferenced nothing and crashed, which is the SIGSEGV. The compiler now emits @L_scene_leave(), a dispatch on the live scene id that runs the leaving scene's on exit, and calls it from such sites; so a listener's become also fires the correct on exit (a global handler's become used to skip it entirely).
  • A UI_Name handle read from a listener / plain function (if id == UI_Go) indexed the widget table before @ui_build had been emitted, which came after functions and listeners — a second crash on the same repro. The table is now built on first use.

Regression: examples/library/scene_menus.ludic (in x test) does become from an @On(UiClicked) listener into a second scene and checks the exit/enter sequence. The engine also fires UiClicked { id } itself now, so the @On(UiClicked) form in your repro is the intended one — no need for the Ui.clicked polling workaround.

Commit: ad54884

Fixed on main — the repro compiles and runs (no diagnostic needed: `become` from any handler now works). Two things were wrong, both in the compiler: - `become` outside a scene's own layers (an `@On(Event)` listener, a global handler, a plain function) has no enclosing scene at compile time. The lowering reused the last emitted handler's scene id — or dereferenced nothing and crashed, which is the SIGSEGV. The compiler now emits `@L_scene_leave()`, a dispatch on the *live* scene id that runs the leaving scene's `on exit`, and calls it from such sites; so a listener's `become` also fires the correct `on exit` (a global handler's `become` used to skip it entirely). - A `UI_Name` handle read from a listener / plain function (`if id == UI_Go`) indexed the widget table before `@ui_build` had been emitted, which came after functions and listeners — a second crash on the same repro. The table is now built on first use. Regression: `examples/library/scene_menus.ludic` (in `x test`) does `become` from an `@On(UiClicked)` listener into a second scene and checks the exit/enter sequence. The engine also fires `UiClicked { id }` itself now, so the `@On(UiClicked)` form in your repro is the intended one — no need for the `Ui.clicked` polling workaround. Commit: ad54884
orkun closed this issue 2026-09-04 00:36:23 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: workshopsoft/ludic#91
No description provided.