Merge enable/disable + @OnEnable/@OnDisable lifecycle hooks
This commit is contained in:
commit
5ecd300d16
17 changed files with 4772 additions and 3911 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
40
LANGUAGE.md
40
LANGUAGE.md
|
|
@ -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
37
examples/toggle.ludic
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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"
|
||||||
|
|
|
||||||
5
test.sh
5
test.sh
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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" },
|
||||||
|
|
|
||||||
|
|
@ -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" },
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue