Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
df6e7fb
docs(brief): add M1.0.14 milestone brief
guysenpai Jul 4, 2026
c516af9
docs(brief): confirm specs read for M1.0.14
guysenpai Jul 4, 2026
6efa2a7
docs(brief): activate M1.0.14
guysenpai Jul 4, 2026
a3783f9
feat(etch): parse payload filter on event await targets (M1.0.14 E1)
guysenpai Jul 4, 2026
db7f5ff
feat(etch): validate entity-scoped event await targets (M1.0.14 E2)
guysenpai Jul 4, 2026
25c34bf
docs(brief): journal E2 (type-checker validation)
guysenpai Jul 4, 2026
5589078
docs(brief): amend M1.0.14 scope after E2 round-trip
guysenpai Jul 4, 2026
19d32b3
fix(etch): pin entity_event fail-loud via evalAwaitTarget (M1.0.14 E2)
guysenpai Jul 4, 2026
d283597
docs(brief): journal E2 boundary-pin fix + resolve amendment sha
guysenpai Jul 4, 2026
24dbd8b
docs(brief): re-read amended reference-part1 for E3
guysenpai Jul 4, 2026
6687e37
feat(etch): execute entity-bound async rules (M1.0.14 E3)
guysenpai Jul 4, 2026
3081d37
docs(brief): journal E3 (entity-bound async rules)
guysenpai Jul 4, 2026
222ef83
docs(etch): renumber E3 rulings to match the brief (M1.0.14)
guysenpai Jul 4, 2026
3923dcc
feat(etch): realize entity_event + payload filters (M1.0.14 E4)
guysenpai Jul 4, 2026
c9da9fe
docs(brief): journal E4 (entity_event runtime + filters)
guysenpai Jul 4, 2026
21a77be
fix(etch): stabilize runtime strings in events/filters (M1.0.14 E4)
guysenpai Jul 4, 2026
a6f4550
docs(brief): journal E4 STOP fix (runtime-string stabilization)
guysenpai Jul 4, 2026
bec269e
fix(etch): stabilize all non-AST strings in event filters (M1.0.14 E4)
guysenpai Jul 4, 2026
9ac8cf6
docs(brief): journal E4 STOP fix 2 (string_persistent stabilization)
guysenpai Jul 4, 2026
bf72122
test(etch): add M1.0.14 E5 integration tests (entity_event end-to-end)
guysenpai Jul 4, 2026
ce9a8c3
docs(brief): journal E5 (integration tests)
guysenpai Jul 4, 2026
72fb9dd
docs(claude-md): update for M1.0.14
guysenpai Jul 4, 2026
0062746
docs(etch): correct stale runAsyncRule header comment (M1.0.14)
guysenpai Jul 4, 2026
6345f14
docs(brief): close M1.0.14
guysenpai Jul 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ knowledge base — see § Quick links spec.
|---|---|
| Phase | 1 (Etch ↔ ECS) |
| Current milestone | (none — between milestones) |
| Last released tag | `v0.10.13-time-and-timers` |
| Last released tag | `v0.10.14-entity-scoped-events` |
| Active branch | `main` |
| Next planned milestone | M1.0.14entity-scoped events (`await entity_event`, the last reserved await target). M1.0 (Etch ↔ ECS interpreter) is **21 sub-milestones** (M1.0.0–M1.0.20), **NOT complete**: M1.0.0–M1.0.13 closed; M1.0.14–M1.0.20 close the remaining EBNF v0.6 execution gaps (criterion C1.6), Etch-closure tag at M1.0.20. Await-family partition: `wait_unscaled` + the timer family delivered in M1.0.13 (time subsystem); `entity_event` → M1.0.14. `every`/`after_unscaled` graduated with M1.0.13; `override` stays the last reserved top-level construct keyword, and `quantize` stays reserved in `non_s3_keywords` (its beat/bar musical clock is a later Sequencer-adjacent milestone). |
| Next planned milestone | M1.0.15test-runner + identity (execute `test "X" { }` blocks + a separate symbol namespace resolving the `test "Foo"`/`component Foo` collision → `E0101`). M1.0 (Etch ↔ ECS interpreter) is **21 sub-milestones** (M1.0.0–M1.0.20), **NOT complete**: M1.0.0–M1.0.14 closed; M1.0.15–M1.0.20 close the remaining EBNF v0.6 execution gaps (criterion C1.6), Etch-closure tag at M1.0.20. Await-family partition: `entity_event` + payload filters (both event targets) delivered in M1.0.14, alongside entity-bound `async rule` execution — `future` (`await <expression>` on a `Future<T>`: asset loading, non-inlined `async fn` returns) is now the **sole** reserved await target. `override` stays the last reserved top-level construct keyword, and `quantize` stays reserved in `non_s3_keywords` (its beat/bar musical clock is a later Sequencer-adjacent milestone). |

## Tags

Expand Down Expand Up @@ -51,6 +51,7 @@ knowledge base — see § Quick links spec.
| `v0.10.11-async-core` | 2026-07-01 | M1.0.11 — Async suspension core (`await` family + `async fn`/`async method` execution) | The per-rule `AsyncSlot` becomes a dynamic `AsyncTask` pool with a **resume frame-stack** (`run`/`loop_`/`while_`/`for_`/`try_`/`call` frames, innermost last); a statement-head `await` suspends at any depth (incl. `for`/`try` bodies) and resumes without re-running prefixes (no double `emit`); a `throw` post-resume routes to the enclosing `try_` frame. `async fn`/`async method` **execute via frame inlining** (`await f()` pushes the callee body + heap-boxed scope + `RetTarget` on the caller task; `return` resolves at the await site; a SYNC call to an `async` fn stays fail-loud — `callFn`/`callMethod` `is_async` gate). Owned `await` targets: `wait` as a **`Duration`** (fixed 60 Hz → ticks; non-literal fail-loud), `global_event`, direct-call `future`. Placement (Phase-1): `await` must be a statement's full RHS on the frame-driven spine — a sub-expression `await` or one in a synchronously-evaluated VALUE block → **`E0904 AwaitNotStatementHead`**. Coloring (§9.3): an `async` call / `await` in a non-async `fn`/`rule` → **`E0901 AsyncCallInNonAsyncContext`**. `parser.zig`/`token.zig`/`ast.zig` untouched. |
| `v0.10.12-concurrency-algebra` | 2026-07-03 | M1.0.12 — Concurrency algebra (`race`/`sync`/`branch`/`spawn { }` + cancellation + `TaskHandle` await) | Closes the concurrency chapter of EBNF v0.6 (C1.6). `race`/`sync` graduate (`kw_race`/`kw_sync`); the four statements parse (`ConcurrencyBranch` shared slab + `RaceStmt`/`SyncStmt`/`BranchStmt`/`SpawnStmt`; `spawn (`/`spawn {` token disambiguation kept local; `let h = spawn { }` IS the SpawnStmt binding form, not a let-stmt). Type-check: E0901 on the four forms outside async; **`E0905 UnconsumedAsyncEffect`** — `await` is the SOLE call-grain consumer (§9.2 **revision 2**, STOP round-trip: the constructs relocate the suspension, their bodies are ordinary async contexts); **`E0906`** `return` race-branch-only (winner-return propagation); **`E0907`** break/continue must not cross the task boundary (per-branch loop depth + label window); `TaskHandle` builtin (non-POD, field-rejected), `h.cancel()`, `await h` typed (unit Phase 1). Execution: heap-record pointer-stable monotonic task pool (husk parking — no reuse, index = Phase-1 handle identity, no generations); drive-by-origin (children interleave at the origin rule's position, creation order, mid-pass pickup); `race`/`sync` = child task per admitted branch (guards evaluated first in the live parent scope; per-branch scope SNAPSHOT copy) with parent suspended on `children_any`/`children_all` (zero admitted → same-tick passthrough; one-tick construct latency otherwise — parent precedes children in pool order, so losers are canceled before their wakes can fire); race = first-`.done`-in-declaration-order winner, losers `cancelTask`'d (NON-transitive), winner-`return` re-raised at the race site; failed tasks park `.canceled` (never a winner, never block a join, `await` on them fails loud). `branch`/`spawn` = detached tasks (`Value.task_handle` = pool index); `await h` on done = immediate resume delivering the parked result, on canceled(-while-awaited) = fail-loud. Fix-as-you-go: `return await <wake-target>` no longer drops its return at resume (M1.0.11 gap). |
| `v0.10.13-time-and-timers` | 2026-07-04 | M1.0.13 — Time subsystem and timers | First builtin engine resources (`GameTime`/`UnscaledTime`/`RealTime`), auto-registered from a single `pub const` descriptor table in `types.zig` (the `TagSet` injection-point precedent; no `.etch` prelude) and known to the type-checker by name (`get(GameTime).dt`, ambient — no `when resource` gate). `stepOnce` populates them at the tick head (fixed 1/60 timestep): two internal f64 clock accumulators in TICK units (game `+= time_scale`, 0 under `paused`; unscaled `+= 1` — E5 recorded deviation vs the brief's seconds framing, accepted: exact integer f64 sums at `scale = 1` make the byte-identical wake guarantee robust), seconds derived at publication. `await wait` re-plumbed onto the game clock (freezes under `paused`, scales with `time_scale`), `await wait_unscaled` executes against the unscaled clock (fires under pause) — both keep the M1.0.11 literal-only restriction; byte-identical wake ticks at `scale = 1` (all M1.0.11/12 tick-precise tests untouched); `async_tick` demoted to the frame counter backing `GameTime.frame`. Timer family graduated + executes: `kw_every`/`kw_after_unscaled`, `TimerStmt` AST slab, `timer_stmt` parses (unbound at statement head + `let t = after(d) { }` in `parseLetStmt`, the spawn precedent), `TimerHandle` builtin type-checks (`cancel()` only, not awaitable, non-POD field-rejected), timer arg = full `Duration` expression, timer body = synchronous context (`await`/async call inside → E0901). Runtime timer registry: heap records, monotone pointer-stable + husk (a `TimerHandle` is a bare index), scope SNAPSHOT at scheduling (`cloneLocalsInto`, handle bound in the parent scope after), fired at the head of `stepOnce` in registration order (deterministic), one-shots husk / `every` re-arms at a fixed period (no drift correction), `cancel()` idempotent with a mid-fire latch. `Value` gains `timer_handle` + `duration` (runtime Duration seconds). `quantize` stays reserved (fail-loud re-pointed at a later Sequencer-adjacent milestone). |
| `v0.10.14-entity-scoped-events` | 2026-07-05 | M1.0.14 — Entity-scoped events and entity-bound async rules | `await entity_event(e, T [{…}])` executes: designated `Entity` field resolved at compile time (`@entity_target` > single structural `Entity` field > `E0908`/`E0909`, single shared policy helper `resolveEventEntityTarget`), matched by name over the per-tick `EventStore` (predicate scan, no entity index; the matched event is NOT consumed — observers and other awaiters coexist). Payload filter (`struct_literal_body`) executes on BOTH event targets (`global_event` erratum §4.2): field-equality only, entity operand + every filter expression captured BY VALUE once at suspension (capture-once §9.4), resume delivers unit (no implicit `event` binding). Non-AST strings (`.string_run`, borrowed `.string_persistent`) deep-copied at the two escape boundaries (emit → store-owned per-tick; filter capture → husk-lifetime buffer) — fixes a pre-existing use-after-free for `@on_event` observers reading computed-string event fields. Entity-bound `async rule` executes (absorbed at the E2 round-trip): one root task per (rule, matched entity) via the SHARED sync selection walk (collect mode — identical order + `when` semantics), four rulings (§9.2): re-arm after terminal while matching; `when` gates the spawn, not the task's life; despawn does not cancel (dead-handle fail-loud on access); spawn follows selection order. Parameterless shape byte-stable; other param shapes stay fail-loud (`dt` injection later). `emit` enum-shorthand event fields fixed (fail-loud before). `future` is the sole remaining fail-loud await target. |

## Hypotheses validated by spikes

Expand Down Expand Up @@ -87,6 +88,7 @@ knowledge base — see § Quick links spec.
- **M1.0.11 scope boundary (async suspension core)** — the Phase-1 tree-walker async model (NOT the Phase-2 bytecode state machine, `etch-bytecode.md §9`) is a dynamic `AsyncTask` pool + resume frame-stack reproducing the §9 observable semantics; documented in `etch-reference-part1.md §9.12` (Claude.ai KB re-upload). **Recorded deviations (Claude.ai round-trips):** §9.12 placement broadened to `for`/`try`/`catch` bodies (E1) and to reject `await` in VALUE-position blocks (E3, `E0904`) — both real EBNF v0.6 statements, C1.6; the `programs/` integration file dropped (that corpus is the interp↔codegen differential, rejects async — Observable behavior covered by inline cross-tick tests). **Owned here:** `await` family (`wait` Duration/60 Hz, `global_event`, direct-call `future`) + `async fn`/`method` execution + coloring `E0901` + placement `E0904`. **Out (owned by later milestones, NOT debt):** `race`/`sync`/`branch`/`spawn { }` + cancellation + `await` on a `TaskHandle` → M1.0.12; `await wait_unscaled` + timers (need the scaled/unscaled time subsystem) → M1.0.13; `await entity_event` (needs entity-scoped events) → M1.0.14; entity-bound `async rule` → later; the Phase-2 bytecode async lowering. **Open decision — async effect-consumption completeness:** a BARE `async` call in an `async` context (no `await`, no wrapper) is illegal per `etch-resolver-types.md §9.2` (the `{async}` effect is consumed by `await` OR `spawn`/`branch`/`race`/`sync`) but is **not** `E0901` (the non-async-context case) and currently **fails loud at runtime** (`is_async` gate) — the compile-time check completes with the consumption constructs in **M1.0.12**.
- **M1.0.12 scope boundary (concurrency algebra)** — the four constructs (`race`/`sync`/`branch`/`spawn { }`) parse, type-check, and execute as CHILD TASKS in the interpreter's pointer-stable monotonic pool (heap records, husk parking, no slot reuse — a pool index IS the Phase-1 `TaskHandle`, no generations), driven by-origin at the creating rule's position in creation order. **Recorded deviation (STOP round-trip 2026-07-02, `etch-resolver-types.md` §9.2 revision 2):** the brief's "calls inside the four construct bodies count as consumed launch sites" is SUPERSEDED — `await` is the SOLE call-grain consumer of the `{async}` effect (`E0905` applies recursively inside the construct bodies; the constructs relocate the suspension into a child task, they do not replace the `await`). Return asymmetry (Guy's ruling 2026-07-02): `return` legal only in a `race` branch (winner-return re-raised at the race site); `sync`/`branch`/`spawn` bodies reject it (`E0906`). Documented one-tick construct latency: a race/sync parent precedes its children in pool-creation order, so it resumes the tick AFTER its wake fires — which guarantees losers are canceled before their own wakes can fire (zero-admitted constructs have no latency). Cancellation is NON-transitive (Phase 1, `etch-bytecode.md` §9.5). Failed tasks (uncaught `throw` / runtime failure) park `.canceled` — never a race winner, never block a `sync` join, `await`ing them fails loud (§9.8 amended). Fix-as-you-go: `return await <wake-target>` dropped its return at resume (M1.0.11 gap) — fixed. **Out (later):** `wait_unscaled` + timers (M1.0.13), `entity_event` (M1.0.14), entity-bound `async rule`, Phase-2 bytecode lowering (`etch-bytecode.md` §9.4), transitive cancellation (Phase 2+), task-pool slot reuse / generations (Phase-2 refcounted model), value-producing spawn bodies (no EBNF v0.6 value channel).
- **M1.0.13 scope boundary (time subsystem and timers)** — the three builtin time resources (`GameTime`/`UnscaledTime`/`RealTime`) are auto-registered from the single `pub const builtin_resources` descriptor table in `types.zig` (the `TagSet` injection-point precedent — no `.etch` prelude, no separate module) and resolve AMBIENTLY (`get(GameTime).dt` needs no `when resource` clause — the spec examples read them without one). `await wait` re-plumbed onto scaled game time and `await wait_unscaled` onto unscaled time with **byte-identical wake ticks at `time_scale = 1`** — realized by the E5 recorded deviation: the two internal clock accumulators advance in TICK units (game `+= time_scale`, 0 under `paused`; unscaled `+= 1`; deadlines `clock + round(secs * 60)`), seconds derived only at resource publication (exact integer f64 sums at `scale = 1`; a seconds accumulator would accrue rounding that can shift a wake by one tick). Both waits KEEP the M1.0.11 literal-only Duration restriction; only the timer family evaluates a full `Duration` expression. The timer registry mirrors the M1.0.12 task-pool discipline (heap records, monotone pointer-stable, husk parking, `TimerHandle` = bare index, no generations) but is a DISTINCT mechanism — a timer is not a task: `cancel()` only, not awaitable, body is a synchronous context (E0901 on `await`/async calls inside). Timer callbacks fire at the head of `stepOnce` (after the clock advance and event-store clear, before rule dispatch) in registration order; `every` re-arms at a fixed period (no drift correction Phase 1). `Value` gained `timer_handle` + `duration` (`value.zig` — outside the brief's per-file list, justified: `Value.timer_handle` is the E6 deliverable and the `task_handle` precedent lives there). **Out (later, not debt):** `quantize` (stays reserved; needs the Sequencer/Pulse beat/bar clock — later Sequencer-adjacent milestone), `time.*` stdlib sugar (later stdlib milestone; `etch-stdlib.md §20` reconciliation deferred with it), `dt` as an injected rule parameter, per-entity `TimeDilation` / selective pause / `WorldClock` (need the phase scheduler), lifting `wait`'s literal-only restriction, Phase-2 bytecode lowering.
- **M1.0.14 scope boundary (entity-scoped events + entity-bound async rules)** — `entity_event` = `global_event` + predicates (same wake path, same per-tick store, producer-before-awaiter order unchanged). Designated-field convention, filter semantics (equality-only, capture-once), and resume-unit are normative in `etch-reference-part1.md §9.4`; the four entity-bound lifecycle rulings in `§9.2`; realization in `§9.12` (Claude.ai KB re-uploads at open + at the E2 amendment). The M1.0.11/12 out-list mentions "entity-bound `async rule` → later" are **superseded** (delivered here — absorbed at the E2 round-trip: the construct is inexpressible end-to-end without an entity-scoped awaiter). String stabilization at the event/filter escape boundaries fixed a PRE-EXISTING observer-side use-after-free (fix-as-you-go, two STOP round-trips). **Out (later, NOT debt):** `future`/`Future<T>` (the last await gap); implicit `event` binding / payload delivery at resume (unit is normative); entity-keyed event index (Phase-2 typed bus); emit-side targeting (grammar v0.6 frozen); non-equality filter predicates; cancel-on-despawn (Phase 2+); `dt`/non-entity rule-parameter injection; Phase-2 bytecode async lowering.

## Non-negotiable rules

Expand Down Expand Up @@ -220,4 +222,4 @@ The `briefs/` directory is the source of truth for milestone state. The brief's

---

Last updated: 2026-07-04
Last updated: 2026-07-05
Loading