Lifecycle hooks: @OnStart/@OnQuit, @OnDespawn, @OnAttach (like @OnSpawn)

Adds the rest of the game/entity/property lifecycle as @-hooks, each firing at
one timeline moment and reducing to ordinary code:

- @OnStart / @OnQuit (program): @OnStart aliases the Start phase; @OnQuit runs in
  the frame loop's done: block, after the loop stops and before teardown.
- @OnDespawn(Model) (entity): a destructor. Despawn doesn't statically know the
  entity's model, so hooks compile to @on_despawn_<Model>(entity) functions and
  emit_despawn dispatches on @L_kind. Symmetric with @OnSpawn.
- @OnAttach(Property): fires in emit_init_component once a property is attached
  and seeded, with the property bound by name.

Registries + parsing mirror @OnSpawn. examples/lifecycle.ludic narrates the whole
timeline (output 1 700 50 950 2). Behaviour-preserving (goldens byte-identical,
despawn users unaffected when no hooks registered); test.sh 16/16, fixpoint holds.

Deferred: @OnDetach (per-property runtime dispatch) and scene @OnEnter/@OnExit
(need scene support in the compiler).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-08-27 21:15:50 +03:00
parent 28abd3a2c3
commit 6e11212cf2
8 changed files with 3993 additions and 3258 deletions

View file

@ -295,24 +295,43 @@ property Velocity {
}
```
**`@OnSpawn` — a constructor.** `@OnSpawn(Model)` on a handler runs its body every
time a `Model` is spawned, with the model's properties bound by name — a place to
initialize an entity. It's a handler keyed to the spawn, not an observer on the
data, so the ECS model is untouched:
**Lifecycle hooks.** A game's timeline has fixed moments, and each is a handler
annotation. They fire in this order and each reduces to ordinary code, so the
data stays plain and behaviour stays in handlers:
```
boot ── @OnStart ─▶ spawn ── @OnAttach(P), @OnSpawn(M) ─▶ … ── @OnDespawn(M) ─▶ quit ── @OnQuit
```
- **`@OnStart` / `@OnQuit`** — the *program*. `@OnStart` runs once at boot (it is
the `Start` phase); `@OnQuit` runs once at shutdown, after the frame loop stops
and before the process exits — the place to `save()` or clean up.
- **`@OnSpawn(Model)` / `@OnDespawn(Model)`** — an *entity*. Both bind the model's
properties by name; `@OnSpawn` is a constructor, `@OnDespawn` a destructor.
Despawn doesn't statically know an entity's model, so despawn hooks compile to
functions dispatched on the entity's kind.
- **`@OnAttach(Property)`** — a *property*, fired each time that property is
attached to an entity (once its fields are seeded), with the property bound by
name.
```ludic
# doc-check: skip — a spawn hook
@OnSpawn(Enemy)
handler InitEnemy { Health.hp = Health.max } # start every Enemy at full HP
# doc-check: skip — lifecycle hooks
@OnStart handler Boot { seed(1) }
@OnSpawn(Enemy) handler Init { Health.hp = Health.max } # constructor
@OnDespawn(Enemy) handler Clean { drop_loot(Health.hp) } # destructor
@OnAttach(Sprite) handler Load { Sprite.id = image_load("goblin.png") }
@OnQuit handler Save { save() } # once, at shutdown
```
**`@Handles` — the handlers a program drives.** Written in front of the
`program`, `@Handles(Move)` names the handlers it uses. It parses and reads as
documentation; every declared handler still runs (registration is implicit).
See [`examples/annotations.ludic`](examples/annotations.ludic), which uses all
four. (`@OnDespawn` and change-reactions are future work — despawn doesn't
statically know an entity's model, and reactions need change-tracking.)
See [`examples/annotations.ludic`](examples/annotations.ludic) (queries, computed
fields, one hook) and [`examples/lifecycle.ludic`](examples/lifecycle.ludic) (the
whole timeline). Still to come: **`@OnDetach`** (needs the same runtime kind
dispatch as despawn, per property) and **scene** hooks (`@OnEnter`/`@OnExit`),
which wait on `scene` support landing in the compiler.
## Structs, arrays and slices