Merge enable/disable + @OnEnable/@OnDisable lifecycle hooks

This commit is contained in:
Orkun ÇAKILKAYA 2026-08-27 23:05:51 +03:00
commit 5ecd300d16
17 changed files with 4772 additions and 3911 deletions

1
.gitignore vendored
View file

@ -15,3 +15,4 @@ tools/editors/jetbrains/build/
# IntelliJ plugin SDK sandbox (tools/editors/jetbrains) # IntelliJ plugin SDK sandbox (tools/editors/jetbrains)
.intellijPlatform/ .intellijPlatform/
.idea

View file

@ -323,15 +323,46 @@ boot ── @OnStart ─▶ spawn ── @OnAttach(P), @OnSpawn(M) ─▶ …
@OnQuit handler Save { save() } # once, at shutdown @OnQuit handler Save { save() } # once, at shutdown
``` ```
**Enable / disable.** `enable` and `disable` are statements that flip something on
or off without destroying it. There are three scopes:
- **`disable P on e` / `enable P on e`** — one *property* on one entity. Disabling
clears the entity's has-flag, so queries stop matching it, but the field values
stay in storage — a later `enable` restores them untouched. `@OnDisable(P)` and
`@OnEnable(P)` are handler annotations that run at the toggle point with the
property bound by name (like a one-entity `@OnSpawn`).
- **`disable Model` / `enable Model`** — a whole *model*. Its entities drop out of
every query while disabled; the entities and their data are left alone.
- **`disable Handler` / `enable Handler`** — a *handler*. It stops being called
each phase while disabled, and resumes on `enable`.
Each toggle is one global flag flip (or one has-flag store), so nothing is copied
or freed — enable/disable is cheap and fully reversible.
```ludic
# doc-check: skip — enable/disable
@OnDisable(Shield) handler Down { play("shield_break.wav") }
@OnEnable(Shield) handler Up { play("shield_up.wav") }
disable Shield on self() # this entity loses its shield; data kept for later
enable Shield on self() # shield back, amount unchanged
disable Gravity # a whole model sits out every query
disable AiThink # a handler stops running each phase
```
See [`examples/toggle.ludic`](examples/toggle.ludic) for all three scopes in one
frame. Still to come: **`@OnDetach`** (the paired hook for a property leaving,
needing the same per-property runtime dispatch as despawn).
**`@Handles` — the handlers a program drives.** Written in front of the **`@Handles` — the handlers a program drives.** Written in front of the
`program`, `@Handles(Move)` names the handlers it uses. It parses and reads as `program`, `@Handles(Move)` names the handlers it uses. It parses and reads as
documentation; every declared handler still runs (registration is implicit). documentation; every declared handler still runs (registration is implicit).
See [`examples/annotations.ludic`](examples/annotations.ludic) (queries, computed See [`examples/annotations.ludic`](examples/annotations.ludic) (queries, computed
fields, one hook) and [`examples/lifecycle.ludic`](examples/lifecycle.ludic) (the fields, one hook) and [`examples/lifecycle.ludic`](examples/lifecycle.ludic) (the
whole timeline). Still to come: **`@OnDetach`** (needs the same runtime kind whole timeline), plus [`examples/toggle.ludic`](examples/toggle.ludic)
dispatch as despawn, per property) and **scene** hooks (`@OnEnter`/`@OnExit`), (enable/disable). Still to come: **scene** hooks (`@OnEnter`/`@OnExit`), which
which wait on `scene` support landing in the compiler. wait on `scene` support landing in the compiler.
## Structs, arrays and slices ## Structs, arrays and slices
@ -425,7 +456,8 @@ another `.ludic` file (see `examples/lib/`).
`let x = expr` · `x = expr` (`+= -= *= /=`) · `if/else` · `when cond { }` `let x = expr` · `x = expr` (`+= -= *= /=`) · `if/else` · `when cond { }`
(if-without-else) · `while cond { }` · `for i in a .. b { }` (numeric range) · (if-without-else) · `while cond { }` · `for i in a .. b { }` (numeric range) ·
`for (…) in query […] { }` · `break` · `continue` · `return` · `spawn` · `for (…) in query […] { }` · `break` · `continue` · `return` · `spawn` ·
`despawn` · `match` · `machine`. `despawn` · `enable` / `disable` (a property `on e`, a model, or a handler) ·
`match` · `machine`.
**Statements are separated by a newline or `;`** (both lex to the same separator **Statements are separated by a newline or `;`** (both lex to the same separator
token). Two statements may not sit adjacent with only spaces between them — the token). Two statements may not sit adjacent with only spaces between them — the

37
examples/toggle.ludic Normal file
View file

@ -0,0 +1,37 @@
# toggle.ludic — enable/disable at the three ECS scopes, each a plain statement.
# Disabling never destroys data: a property's values persist in storage, so a
# later `enable` restores them. Queries already skip a cleared flag, so nothing
# else in the language needs to know. Running it prints: 6 0 7 1 0
#
# disable P on e clears one entity's has-flag (@OnDisable / @OnEnable fire)
# disable Model flips the model's enabled flag (its entities drop from queries)
# disable Handler flips the handler's enabled flag (it stops running each phase)
program Toggles {
property Health { hp: int = 0, max: int = 100 }
property Shield { amount: int = 0 }
model Player { Health, Shield }
# hooks run at the toggle point with the property's data bound by name
@OnDisable(Shield) handler Down { print_int(Shield.amount + 1) } # 5 + 1
@OnEnable(Shield) handler Up { print_int(Shield.amount + 2) } # 5 + 2
handler Seed phase Start { spawn Player { Health { max: 50 }, Shield { amount: 5 } } }
handler Run phase Render {
for (e) in query [Player] { disable Shield on self() } # @OnDisable -> 6
let n = 0
for (s) in query [Shield] { n += 1 }
print_int(n) # 0 — no live Shield now
for (e) in query [Player] { enable Shield on self() } # @OnEnable -> 7, data intact
n = 0
for (s) in query [Shield] { n += 1 }
print_int(n) # 1 — amount is still 5
disable Player # whole model off
n = 0
for (p) in query [Player] { n += 1 }
print_int(n) # 0 — model disabled
quit()
}
}

View file

@ -16,6 +16,7 @@ const N_ARCH: int = 10
const N_EXTERN: int = 11 const N_EXTERN: int = 11
const N_UI: int = 12 const N_UI: int = 12
const N_ENUM: int = 27 # enum Name { A, B, ... } — named int constants const N_ENUM: int = 27 # enum Name { A, B, ... } — named int constants
const S_TOGGLE: int = 28 # enable/disable (ival: 1=enable 0=disable; s=target; a=entity or null)
# statements # statements
const S_LET: int = 10 const S_LET: int = 10
const S_ASSIGN: int = 11 const S_ASSIGN: int = 11

View file

@ -199,6 +199,26 @@ fn onattach_body(prop: ptr) -> Node {
return ptr_null() return ptr_null()
} }
# @OnEnable(Property) / @OnDisable(Property): run when a property is toggled on an
# entity, with the property bound by name.
var g_onenable: []Node
var g_ondisable: []Node
fn register_onenable(prop: ptr, body: Node) -> void { let n = node(N_BLOCK); n.s = prop; n.a = body; push(g_onenable, n) }
fn register_ondisable(prop: ptr, body: Node) -> void { let n = node(N_BLOCK); n.s = prop; n.a = body; push(g_ondisable, n) }
fn onenable_body(prop: ptr) -> Node {
let i = 0
while i < len(g_onenable) { if streq(g_onenable[i].s, prop) { return g_onenable[i].a }; i = i + 1 }
return ptr_null()
}
fn ondisable_body(prop: ptr) -> Node {
let i = 0
while i < len(g_ondisable) { if streq(g_ondisable[i].s, prop) { return g_ondisable[i].a }; i = i + 1 }
return ptr_null()
}
# is `name` a model (archetype)? — chooses model-vs-handler for a bare enable/disable
fn is_model(name: ptr) -> bool { return find_arch_id(name) > 0 }
# local variable environment # local variable environment
fn loc_reset() -> void { nloc = 0 } fn loc_reset() -> void { nloc = 0 }
fn loc_push(name: ptr, r: ptr, ty: ptr) -> void { fn loc_push(name: ptr, r: ptr, ty: ptr) -> void {

View file

@ -37,6 +37,9 @@ fn emit_ecs_storage() -> void {
emith(sconcat("@S_", sconcat(c.s, sconcat(" = internal global [", sconcat(me, sconcat(" x %Cmp_", sconcat(c.s, "] zeroinitializer\n"))))))) emith(sconcat("@S_", sconcat(c.s, sconcat(" = internal global [", sconcat(me, sconcat(" x %Cmp_", sconcat(c.s, "] zeroinitializer\n")))))))
emith(sconcat("@H_", sconcat(c.s, sconcat(" = internal global [", sconcat(me, " x i8] zeroinitializer\n"))))) emith(sconcat("@H_", sconcat(c.s, sconcat(" = internal global [", sconcat(me, " x i8] zeroinitializer\n")))))
} }
# one enabled-flag global per model and per handler (default enabled)
if c.kind == N_ARCH { emith(sconcat("@ME_", sconcat(c.s, " = internal global i32 1\n"))) }
if c.kind == N_SYS { emith(sconcat("@HE_", sconcat(c.s, " = internal global i32 1\n"))) }
i = i + 1 i = i + 1
} }
} }

View file

@ -24,7 +24,14 @@ fn emit_calls_for_phase(phase: ptr) -> void {
let i = 0 let i = 0
while i < len(prog) { while i < len(prog) {
let d = prog[i] let d = prog[i]
if d.kind == N_SYS and streq(d.ty, phase) { emit(" call void @sys_"); emit(d.s); emit("()\n") } if d.kind == N_SYS and streq(d.ty, phase) { # skip a disabled handler
let he = emit_bind(sconcat("load i32, ptr @HE_", d.s))
let hc = emit_bind(sconcat("icmp ne i32 ", sconcat(he, ", 0")))
let run = lbl("hrun"); let skip = lbl("hskip")
emit(" br i1 "); emit(hc); emit(", label %"); emit(run); emit(", label %"); emit(skip); emit("\n")
emit(run); emit(":\n call void @sys_"); emit(d.s); emit("()\n")
emit(" br label %"); emit(skip); emit("\n"); emit(skip); emit(":\n")
}
i = i + 1 i = i + 1
} }
} }

View file

@ -42,7 +42,10 @@ fn emit_query(st: Node) -> void {
if ak > 0 { if ak > 0 {
let kp = nreg(); emit(" "); emit(kp); emit(" = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 "); emit(i1); emit("\n") let kp = nreg(); emit(" "); emit(kp); emit(" = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 "); emit(i1); emit("\n")
let kv = emit_bind(sconcat("load i32, ptr ", kp)) let kv = emit_bind(sconcat("load i32, ptr ", kp))
ok = emit_bind(sconcat("icmp eq i32 ", sconcat(kv, sconcat(", ", itoa(ak))))) let kok = emit_bind(sconcat("icmp eq i32 ", sconcat(kv, sconcat(", ", itoa(ak)))))
let mev = emit_bind(sconcat("load i32, ptr @ME_", tm.s)) # model-enabled flag
let meok = emit_bind(sconcat("icmp ne i32 ", sconcat(mev, ", 0")))
ok = emit_bind(sconcat("and i1 ", sconcat(kok, sconcat(", ", meok))))
} else { } else {
let hp = nreg(); emit(" "); emit(hp); emit(" = getelementptr inbounds ["); emit(me); emit(" x i8], ptr @H_"); emit(tm.s); emit(", i32 0, i32 "); emit(i1); emit("\n") let hp = nreg(); emit(" "); emit(hp); emit(" = getelementptr inbounds ["); emit(me); emit(" x i8], ptr @H_"); emit(tm.s); emit(", i32 0, i32 "); emit(i1); emit("\n")
let hv = emit_bind(sconcat("load i8, ptr ", hp)) let hv = emit_bind(sconcat("load i8, ptr ", hp))

View file

@ -115,3 +115,30 @@ fn emit_despawn(st: Node) -> void {
} }
emit(" call void @L_free_entity(i32 "); emit(v.code); emit(")\n") emit(" call void @L_free_entity(i32 "); emit(v.code); emit(")\n")
} }
# enable/disable. `<P> on <e>` toggles a property's has-flag on an entity (its
# data persists, so re-enabling restores it, and queries already skip a cleared
# flag). A bare `<Model>` / `<Handler>` flips a global enabled flag.
fn emit_toggle(st: Node) -> void {
let val = "0"; if st.ival == 1 { val = "1" }
if not ptr_is_null(st.a) {
let ev = emit_expr(st.a)
let me = itoa(MAX_ENT)
let hp = nreg(); emit(" "); emit(hp); emit(" = getelementptr inbounds ["); emit(me); emit(" x i8], ptr @H_"); emit(st.s); emit(", i32 0, i32 "); emit(ev.code); emit("\n")
emit(" store i8 "); emit(val); emit(", ptr "); emit(hp); emit("\n")
let hb: Node = ptr_null()
if st.ival == 1 { hb = onenable_body(st.s) } else { hb = ondisable_body(st.s) }
if not ptr_is_null(hb) {
let save = nloc
let slot = nreg(); emit(" "); emit(slot); emit(" = getelementptr inbounds ["); emit(me); emit(" x %Cmp_"); emit(st.s); emit("], ptr @S_"); emit(st.s); emit(", i32 0, i32 "); emit(ev.code); emit("\n")
let vslot = emit_alloca("ptr")
emit(" store ptr "); emit(slot); emit(", ptr "); emit(vslot); emit("\n")
loc_push(st.s, vslot, st.s)
emit_block(hb)
nloc = save
}
} else {
let g = "@HE_"; if is_model(st.s) { g = "@ME_" } # model vs handler
emit(" store i32 "); emit(val); emit(", ptr "); emit(g); emit(st.s); emit("\n")
}
}

View file

@ -179,6 +179,7 @@ fn emit_stmt(st: Node) -> void {
if st.kind == S_QUERY { emit_query(st); return } if st.kind == S_QUERY { emit_query(st); return }
if st.kind == S_SPAWN { emit_spawn(st); return } if st.kind == S_SPAWN { emit_spawn(st); return }
if st.kind == S_DESPAWN { emit_despawn(st); return } if st.kind == S_DESPAWN { emit_despawn(st); return }
if st.kind == S_TOGGLE { emit_toggle(st); return }
if st.kind == S_MACHINE { emit_machine(st); return } if st.kind == S_MACHINE { emit_machine(st); return }
if st.kind == S_BECOME { emit_become(st); return } if st.kind == S_BECOME { emit_become(st); return }
if st.kind == S_EXPR { let v = emit_expr(st.a); return } if st.kind == S_EXPR { let v = emit_expr(st.a); return }

File diff suppressed because it is too large Load diff

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, "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, "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, "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, "break") { pi = pi + 1; return node(S_BREAK) }
if streq(t.text, "continue") { pi = pi + 1; return node(S_CONTINUE) } if streq(t.text, "continue") { pi = pi + 1; return node(S_CONTINUE) }
if streq(t.text, "match") { if streq(t.text, "match") {
@ -285,6 +291,8 @@ fn parse_one_decl() -> void {
let onspawn_model: ptr = ptr_null() let onspawn_model: ptr = ptr_null()
let ondespawn_model: ptr = ptr_null() let ondespawn_model: ptr = ptr_null()
let onattach_prop: 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 let hook_phase: ptr = ptr_null() # @OnStart / @OnQuit override the phase
while is_op("@") { while is_op("@") {
pi = pi + 1; let a = eat_id() # collect a leading @annotation 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, "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, "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, "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, "OnStart") { hook_phase = "Start" } # boot
else { if streq(a, "OnQuit") { hook_phase = "OnQuit" } # shutdown else { if streq(a, "OnQuit") { hook_phase = "OnQuit" } # shutdown
else { if is_op("(") { let d = 0 # any other @anno(args) — parsed and skipped 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() skipnl()
} }
if is_id("import") { pi = pi + 1 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(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(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(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(hook_phase) { h.ty = hook_phase } # @OnStart/@OnQuit
if not ptr_is_null(qspec) { # @Queries wraps the body in its S_QUERY if not ptr_is_null(qspec) { # @Queries wraps the body in its S_QUERY
qspec.a = h.a qspec.a = h.a
@ -363,6 +375,8 @@ fn parse_program() -> void {
g_onspawn = new []Node g_onspawn = new []Node
g_ondespawn = new []Node g_ondespawn = new []Node
g_onattach = new []Node g_onattach = new []Node
g_onenable = new []Node
g_ondisable = new []Node
loaded_paths = new []ptr loaded_paths = new []ptr
skipnl() skipnl()
g_game_name = "Ludic" g_game_name = "Ludic"

View file

@ -61,6 +61,11 @@ if ./selfhost/game-build.sh build/ludicc examples/lifecycle.ludic "/tmp/ludic_li
&& [ "$(/tmp/ludic_life </dev/null | tr '\n' ' ')" = "1 700 50 950 2 " ]; then && [ "$(/tmp/ludic_life </dev/null | tr '\n' ' ')" = "1 700 50 950 2 " ]; then
ok "lifecycle.ludic (@OnStart/@OnAttach/@OnSpawn/@OnDespawn/@OnQuit in order)" ok "lifecycle.ludic (@OnStart/@OnAttach/@OnSpawn/@OnDespawn/@OnQuit in order)"
else bad "lifecycle: $(tail -1 /tmp/life.out)"; fi else bad "lifecycle: $(tail -1 /tmp/life.out)"; fi
# enable/disable at property (with @OnDisable/@OnEnable), model, and query scopes.
if ./selfhost/game-build.sh build/ludicc examples/toggle.ludic "/tmp/ludic_tog" >/tmp/tog.out 2>&1 \
&& [ "$(/tmp/ludic_tog </dev/null | tr '\n' ' ')" = "6 0 7 1 0 " ]; then
ok "toggle.ludic (enable/disable + @OnDisable/@OnEnable across property/model)"
else bad "toggle: $(tail -1 /tmp/tog.out)"; fi
# --- Toolchain-agent CLI smoke tests append below this line --- # --- Toolchain-agent CLI smoke tests append below this line ---
echo "== self-hosted front-end binaries (ludicc / ludic) ==" echo "== self-hosted front-end binaries (ludicc / ludic) =="
# The two commands are one multi-call native binary built from the seed with # The two commands are one multi-call native binary built from the seed with

View file

@ -45,12 +45,12 @@ object LudicVocabulary {
"const", "var", "fn", "extern", "handler", "entry" "const", "var", "fn", "extern", "handler", "entry"
) )
val CLAUSE = setOf( val CLAUSE = setOf(
"phase", "query", "reads", "writes" "phase", "query", "reads", "writes", "on"
) )
val STMT = setOf( val STMT = setOf(
"let", "return", "if", "else", "when", "while", "for", "in", "spawn", "despawn", "let", "return", "if", "else", "when", "while", "for", "in", "spawn", "despawn",
"match", "machine", "state", "become", "enter", "where", "and", "or", "not", "enable", "disable", "match", "machine", "state", "become", "enter", "where",
"break", "continue", "new" "and", "or", "not", "break", "continue", "new"
) )
val PRIMITIVES = setOf("int", "fixed", "bool", "entity", "str", "ptr", "void") val PRIMITIVES = setOf("int", "fixed", "bool", "entity", "str", "ptr", "void")
val PHASES = setOf("Start", "Input", "FixedUpdate", "Update", "LateUpdate", "Render") val PHASES = setOf("Start", "Input", "FixedUpdate", "Update", "LateUpdate", "Render")

View file

@ -161,9 +161,9 @@
}, },
"keyword": { "keyword": {
"patterns": [ "patterns": [
{ "name": "keyword.control.ludic", "match": "\\b(if|else|when|while|for|in|match|machine|become|enter|return|spawn|despawn|where|break|continue|new)\\b" }, { "name": "keyword.control.ludic", "match": "\\b(if|else|when|while|for|in|match|machine|become|enter|return|spawn|despawn|enable|disable|where|break|continue|new)\\b" },
{ "name": "keyword.operator.logical.ludic", "match": "\\b(and|or|not)\\b" }, { "name": "keyword.operator.logical.ludic", "match": "\\b(and|or|not)\\b" },
{ "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|reads|writes)\\b" }, { "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|reads|writes|on)\\b" },
{ "name": "keyword.other.ludic", "match": "\\b(import|extern)\\b" }, { "name": "keyword.other.ludic", "match": "\\b(import|extern)\\b" },
{ "name": "storage.type.ludic", "match": "\\b(program|property|struct|model|enum|ui|const|var|let|fn|handler|entry|state)\\b" }, { "name": "storage.type.ludic", "match": "\\b(program|property|struct|model|enum|ui|const|var|let|fn|handler|entry|state)\\b" },
{ "name": "support.type.primitive.ludic", "match": "\\b(int|fixed|bool|entity|str|ptr|void)\\b" }, { "name": "support.type.primitive.ludic", "match": "\\b(int|fixed|bool|entity|str|ptr|void)\\b" },

View file

@ -161,9 +161,9 @@
}, },
"keyword": { "keyword": {
"patterns": [ "patterns": [
{ "name": "keyword.control.ludic", "match": "\\b(if|else|when|while|for|in|match|machine|become|enter|return|spawn|despawn|where|break|continue|new)\\b" }, { "name": "keyword.control.ludic", "match": "\\b(if|else|when|while|for|in|match|machine|become|enter|return|spawn|despawn|enable|disable|where|break|continue|new)\\b" },
{ "name": "keyword.operator.logical.ludic", "match": "\\b(and|or|not)\\b" }, { "name": "keyword.operator.logical.ludic", "match": "\\b(and|or|not)\\b" },
{ "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|reads|writes)\\b" }, { "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|reads|writes|on)\\b" },
{ "name": "keyword.other.ludic", "match": "\\b(import|extern)\\b" }, { "name": "keyword.other.ludic", "match": "\\b(import|extern)\\b" },
{ "name": "storage.type.ludic", "match": "\\b(program|property|struct|model|enum|ui|const|var|let|fn|handler|entry|state)\\b" }, { "name": "storage.type.ludic", "match": "\\b(program|property|struct|model|enum|ui|const|var|let|fn|handler|entry|state)\\b" },
{ "name": "support.type.primitive.ludic", "match": "\\b(int|fixed|bool|entity|str|ptr|void)\\b" }, { "name": "support.type.primitive.ludic", "match": "\\b(int|fixed|bool|entity|str|ptr|void)\\b" },

View file

@ -56,19 +56,19 @@ static const char* LUDIC_KW_DECL[] = {
"const","var","fn","extern","handler","entry", 0 "const","var","fn","extern","handler","entry", 0
}; };
static const char* LUDIC_KW_CLAUSE[] = { static const char* LUDIC_KW_CLAUSE[] = {
"phase","query","reads","writes", 0 "phase","query","reads","writes","on", 0
}; };
/* Documented design targets the self-hosted parser does not accept yet. Kept /* Documented design targets the self-hosted parser does not accept yet. Kept
* out of the highlighted vocabulary (they would read as working keywords) until * out of the highlighted vocabulary (they would read as working keywords) until
* they are implemented; check-vocabulary.py verifies the lists above are a * they are implemented; check-vocabulary.py verifies the lists above are a
* subset of what selfhost/parse*.ludic actually dispatches on. */ * subset of what selfhost/parse*.ludic actually dispatches on. */
static const char* LUDIC_KW_RESERVED[] = { static const char* LUDIC_KW_RESERVED[] = {
"scene","layer","on","start", 0 "scene","layer","start", 0
}; };
static const char* LUDIC_KW_STMT[] = { static const char* LUDIC_KW_STMT[] = {
"let","return","if","else","when","while","for","in","spawn","despawn", "let","return","if","else","when","while","for","in","spawn","despawn",
"match","machine","state","become","enter","where","and","or","not", "enable","disable","match","machine","state","become","enter","where",
"break","continue","new", 0 "and","or","not","break","continue","new", 0
}; };
static const char* LUDIC_TYPES[] = { static const char* LUDIC_TYPES[] = {
"int","fixed","bool","entity","str","ptr","void", 0 "int","fixed","bool","entity","str","ptr","void", 0