Add enable/disable across ECS scopes + @OnEnable/@OnDisable hooks

Three enable/disable statement scopes, each a reversible flag flip:
  - `disable P on e` / `enable P on e` — one property on one entity. Clears
    the has-flag so queries stop matching; field data persists in storage, so
    enable restores it untouched. @OnDisable(P)/@OnEnable(P) handler hooks run
    at the toggle point with the property bound by name.
  - `disable Model` / `enable Model` — @ME_<Model> global flag; the model's
    entities drop out of every query while disabled.
  - `disable Handler` / `enable Handler` — @HE_<Handler> global flag; the
    handler stops being called each phase while disabled.

Nothing is copied or freed — each toggle is one global store or one has-flag
store. Reduces entirely to existing ECS machinery (has-flags, kind filter,
per-phase call guards), so the data-oriented model is untouched.

New AST node S_TOGGLE; emit_toggle lowers it. Query {Model} filter now ANDs
@ME_; phase calls now guard on @HE_. Parser gains enable/disable statements
and @OnEnable/@OnDisable annotations.

Vocabulary: `on` promoted from RESERVED to CLAUSE (parser now dispatches on
it); enable/disable added as STMT keywords — synced across ludic_syntax.h,
the TextMate grammar, and LudicTokens.kt (check-vocabulary.py clean).

examples/toggle.ludic demonstrates all three scopes (prints 6 0 7 1 0);
test.sh smoke asserts it. Reseeded; C-free fixpoint holds; goldens identical.
Also: stop tracking tools/.idea/ (gitignored).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-08-27 23:05:47 +03:00
parent bfc868c546
commit 0bded528df
17 changed files with 4772 additions and 3911 deletions

View file

@ -200,6 +200,12 @@ fn stmt() -> Node {
if streq(t.text, "become") { pi = pi + 1; let n = node(S_BECOME); n.s = eat_id(); return n }
if streq(t.text, "enter") { pi = pi + 1; let n = node(S_BECOME); n.s = eat_id(); n.ival = 1; return n }
if streq(t.text, "despawn") { pi = pi + 1; let n = node(S_DESPAWN); n.a = expr(); return n }
if streq(t.text, "enable") or streq(t.text, "disable") {
let en = 0; if streq(t.text, "enable") { en = 1 }
pi = pi + 1; let n = node(S_TOGGLE); n.ival = en; n.s = eat_id() # `enable P on e` / `disable Model` / `disable Handler`
if is_id("on") { pi = pi + 1; n.a = expr() } # property on an entity
return n
}
if streq(t.text, "break") { pi = pi + 1; return node(S_BREAK) }
if streq(t.text, "continue") { pi = pi + 1; return node(S_CONTINUE) }
if streq(t.text, "match") {
@ -285,6 +291,8 @@ fn parse_one_decl() -> void {
let onspawn_model: ptr = ptr_null()
let ondespawn_model: ptr = ptr_null()
let onattach_prop: ptr = ptr_null()
let onenable_prop: ptr = ptr_null()
let ondisable_prop: ptr = ptr_null()
let hook_phase: ptr = ptr_null() # @OnStart / @OnQuit override the phase
while is_op("@") {
pi = pi + 1; let a = eat_id() # collect a leading @annotation
@ -293,10 +301,12 @@ fn parse_one_decl() -> void {
else { if streq(a, "OnSpawn") { eat_op("("); onspawn_model = eat_id(); eat_op(")") }
else { if streq(a, "OnDespawn") { eat_op("("); ondespawn_model = eat_id(); eat_op(")") }
else { if streq(a, "OnAttach") { eat_op("("); onattach_prop = eat_id(); eat_op(")") }
else { if streq(a, "OnEnable") { eat_op("("); onenable_prop = eat_id(); eat_op(")") }
else { if streq(a, "OnDisable") { eat_op("("); ondisable_prop = eat_id(); eat_op(")") }
else { if streq(a, "OnStart") { hook_phase = "Start" } # boot
else { if streq(a, "OnQuit") { hook_phase = "OnQuit" } # shutdown
else { if is_op("(") { let d = 0 # any other @anno(args) — parsed and skipped
while true { if is_op("(") { d = d + 1 }; if is_op(")") { d = d - 1 }; pi = pi + 1; if d == 0 { break } } } } } } } } } }
while true { if is_op("(") { d = d + 1 }; if is_op(")") { d = d - 1 }; pi = pi + 1; if d == 0 { break } } } } } } } } } } } }
skipnl()
}
if is_id("import") { pi = pi + 1
@ -315,6 +325,8 @@ fn parse_one_decl() -> void {
if not ptr_is_null(onspawn_model) { register_onspawn(onspawn_model, h.a); return } # spawn hook
if not ptr_is_null(ondespawn_model) { register_ondespawn(ondespawn_model, h.a); return } # despawn hook
if not ptr_is_null(onattach_prop) { register_onattach(onattach_prop, h.a); return } # attach hook
if not ptr_is_null(onenable_prop) { register_onenable(onenable_prop, h.a); return } # enable hook
if not ptr_is_null(ondisable_prop) { register_ondisable(ondisable_prop, h.a); return } # disable hook
if not ptr_is_null(hook_phase) { h.ty = hook_phase } # @OnStart/@OnQuit
if not ptr_is_null(qspec) { # @Queries wraps the body in its S_QUERY
qspec.a = h.a
@ -363,6 +375,8 @@ fn parse_program() -> void {
g_onspawn = new []Node
g_ondespawn = new []Node
g_onattach = new []Node
g_onenable = new []Node
g_ondisable = new []Node
loaded_paths = new []ptr
skipnl()
g_game_name = "Ludic"