Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 6 additions & 4 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.10-structural-mutation` |
| Last released tag | `v0.10.11-async-core` |
| Active branch | `main` |
| Next planned milestone | M1.0.11`async` complete (`race`/`sync`/`branch` + the remaining `await` targets). M1.0 (Etch ↔ ECS interpreter) is **21 sub-milestones** (M1.0.0–M1.0.20), **NOT complete**: M1.0.0–M1.0.10 closed; M1.0.11–M1.0.20 close the remaining EBNF v0.6 execution gaps (criterion C1.6), with the Etch-closure tag at M1.0.20. `override` stays the last reserved `non_s3_keywords` member (waits for a Tier-1 overridable module). |
| Next planned milestone | M1.0.12concurrency algebra (`race`/`sync`/`branch`/`spawn { }` + cancellation + `await` on a `TaskHandle`). M1.0 (Etch ↔ ECS interpreter) is **21 sub-milestones** (M1.0.0–M1.0.20), **NOT complete**: M1.0.0–M1.0.11 closed; M1.0.12–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` + timers → M1.0.13 (time subsystem); `entity_event` → M1.0.14 (entity-scoped events). `override` stays the last reserved `non_s3_keywords` member. |

## Tags

Expand Down Expand Up @@ -48,6 +48,7 @@ knowledge base — see § Quick links spec.
| `v0.10.8-const-private-test` | 2026-06-29 | M1.0.8 — `const` top-level + `private` + `test` graduation | The last three `non_s3_keywords` graduate parser-up (`override` stays reserved). Lexer: `kw_const`/`kw_private`/`kw_test` added to `s3_keywords`, removed from the reserve list (identifier→keyword logic unchanged). AST: `ConstDecl`/`TestDecl` side-slabs + `Visibility {public, private}` field on the `Item` node (`itemVisibility`/`setItemVisibility`). Parser: `parseConstDecl` (`const ( IDENT \| TYPE_IDENT ) : type = const_expr`, top-level only — `parseStmt` untouched, so `const` in a block is a parse error per part1 §4.5); `parseTestDecl` (`test STRING block`, reuses `parseBlockExpr`, no execution); `private` prefix in `parseOneTopLevel` (after annotations, before dispatch; rejects `private import/const/type`; sets the item `.private`). Lockstep set extended {dispatch, `recoverToTopLevel` stop-set, the single error-message enumeration} — `private` adds no stop-set member. Resolver: `SymbolKind` += `const_`/`test_`; `pass1Collect` registers both, `checkConstValue` reuses the field-default surface (`E1101 NotConstEvaluable` + `E0200 TypeMismatch`); tests registered but not exported. `buildExports` exports `const_decl` and reads `Item.visibility` per decl → **activates the dormant `E0107 ImportPrivateItem`** check. **Cleared M1.0.7 debts**: cross-file `const` resolves; selectively importing a `private` item emits `E0107`. |
| `v0.10.9-extension-hooks` | 2026-06-30 | M1.0.9 — Execute extension hooks (`on_attach`/`on_detach`) | Founds the runtime text-execution surface M1.0.6 deferred. Decision: **text re-parse, not bytecode** (the VM is Phase 2, `etch-bytecode.md §17`). `parser.parseStmtBlock` parses a cooked hook statement-run (`"; "`-separated, no braces — the shape `descriptor.renderStmtRunAlloc` emits) into a transient `AstArena` via a new `parseStmtFragment` loop (reuses `parseStmt`, skips one optional `.semicolon` between statements). `interp.execHookText` rebinds `self.ast` to the hook arena (the field is a reassignable `*const AstArena`; the executor resolves identifiers via `self.ast.strings`, so the rebind is required), binds the implicit `entity`, runs via the existing `execStmtRun`, and routes deferred structural changes through the world's shared observer-deferred buffer (mirror of `runObserverBody`). `bindToWorld` registers the real `on_attach`/`on_detach` trampolines (`ctx = *Interpreter`); the loader's `dispatchOnAttach` now reaches execution. `world.zig` gains the `on_detach` seam (`registerOnDetach`/`dispatchOnDetach` + `ExtensionDetachFn`/`detach_hook`, mirror of `on_attach`) + a per-entity active-extension side-table (`entity_extensions`: `addEntityExtension`/`removeEntityExtension`/`hasEntityExtension`/`entityExtensions`, freed in `deinit`). `dispatchMethodOnValue` entity arm gains `activate_extension`/`deactivate_extension`/`has_extension`/`active_extensions`; the `Bridge` holds an optional `ExtensionResolver` (`setExtensionResolver`); `loader.runtimeActivate`/`runtimeDeactivate` are the pub runtime entries (deactivate fires `on_detach` first, then `removeComponentDynamic`). Headline: a cooked scene activating `CombatModule` runs `on_attach` at load (`Health.max` 100→150). Conflict policy stays **reject** (`ExtensionComponentConflict`), not last-wins. **B1+B2 merge-blocker round-trip (same day):** the Etch `activate_extension`/`deactivate_extension` now ENQUEUE a deferred op (interp-side `pending_extensions`, drained at the tick boundary) instead of mutating immediately mid-`iterateArchetype` (the immediate `runtimeActivate`/`runtimeDeactivate` stay for load + direct paths); and the type-checker (`dispatchMethodOnType` entity arm) recognizes all four methods so they pass `weld check`. |
| `v0.10.10-structural-mutation` | 2026-06-30 | M1.0.10 — Structural mutation in bodies (`spawn`/`despawn`/`add(T)`/`remove(T)`) | The four structural ops are **executable from `rule`/observer/hook bodies** as DEFERRED changes, and the S4 "no structural mutation" interpreter boundary is **lifted**. Parser: `spawn` graduates `non_s3_keywords`→`kw_spawn` + new `ExprKind.spawn_struct` (`spawn (` structural vs `spawn {` = fail-loud M1.0.11 async seam); `despawn`/`add`/`remove` need no parser change (postfix methods on an `Entity` receiver). Type-checker: the four ops recognized on an `Entity` receiver; component-literal fields validated via `checkComponentInstance` reuse (`E0306` unknown field / `E0307` field-type) — no body handle (`E0304`, statement-position only, v0.6) / prefab-name spawn refused (`E0305`, gated on the prefab runtime). Interp: each op ENQUEUES a Tier-0 `CommandBuffer` command onto `world.observer_registry.deferred` (eager payload resolution), drained at the tick boundary by `flushStructural` via `applyWithObservers` (observers fire per op — `on_spawned`+`on_add` / `on_remove`+`on_despawned` / `on_replaced`), never mid-`iterateArchetype`. No sentinel/planned-pool (reserved post-v0.6); `command_buffer.zig`/`entity.zig` untouched (FROZEN). |
| `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. |

## Hypotheses validated by spikes

Expand Down Expand Up @@ -80,7 +81,8 @@ knowledge base — see § Quick links spec.
- **M1.0.7 scope boundary (cross-file import)**: `import` graduated parser-up (the only `non_s3_keywords` member to leave; `const`/`private`/`test`/`override` stay reserved for M1.0.8). **Validated approach**: a **per-module** byte-keyed exports index (`{name bytes → {kind, arena_index, item_id}}`) extends the M0.9 byte-keyed `ProjectContext` pattern (StringIds are per-arena) — NOT a flat global index, so two modules exporting the same name never collide; the imported-component cross-arena resolution (decl fetched from its defining arena, field names compared by bytes) **unblocks the E1793 false positive** for `.prefab.etch`. **Deferred-but-pre-wired**: module-alias qualified `m.Type` resolution (D-F — the `imported_alias` binding is recorded at E5, so the descending `Path` walk is purely additive later); `E0107 ImportPrivateItem` (D-G — wired through the exports `visibility` flag, dormant until `private` graduates M1.0.8). **Not debt — moot**: the cross-arena field-TYPE check (E1795) resolves builtin types; this is COMPLETE for components because `validateFieldsInDecl(.component_like)` admits only builtin-POD field types (named struct/enum/string rejected on components) — the named-type branch is unreachable for a valid component (forward-compat headroom only). The cross-file `const`-import acceptance test is deferred to M1.0.8 (`const` is not parseable until it graduates) — cross-file resolution is covered by the imported-component type test + the prefab unblock.
- **M1.0.8 scope boundary (`const`/`private`/`test` graduation)**: the last three `non_s3_keywords` graduate parser-up; `override` stays reserved (waits for a Tier-1 overridable module). **Top-level `const` only** — `parseStmt` is deliberately NOT extended, so a block-level `const` is a parse error; the tri-document drift (`const_stmt` under `etch-grammar.md §4.1` statements vs §4.5 "top-level only" vs `local_const` in `etch-resolver-types.md §2.1`) is a PREEXISTING cross-doc inconsistency deferred to a KB-audit conversation (NOT resolved here). **`private` is direct export-visibility + `E0107` only** — visibility inheritance (`etch-resolver-types.md §10.2`) and `W0902 PrivateTypeInPublicImpl` stay additive/deferred; `private` is parsed as a prefix on a `declaration_body` (rejects `private import/const/type`) and adds no `recoverToTopLevel` stop-set member. **`test` is parse + validate + symbol registration only** — no execution surface exists (same blocker family as M1.0.9); tests register a `test_` symbol but are never exported. **Residual**: a string-named `test "X"` registers under the byte sequence `X` via `registerSymbol`, so it shares the name namespace with identifier-named symbols (a `test "Foo"` collides with `component Foo` → `E0101`); acceptable for M1.0.8, revisit when the M1.0.9 test-runner formalizes test identity. **Cross-file tests** live in `tests/etch/import_resolve_test.zig` (the `validateProject` harness), not inline in `types.zig` (which cannot reach `validateProject` — a tier-up dependency).
- **M1.0.9 scope boundary (execute extension hooks)** — **decided: TEXT re-parse, not bytecode** (the VM is Phase 2, `etch-bytecode.md §17`; `etch-visual-scripting.md §4`). The Tier-0 seam (`register`/`dispatch` `On{Attach,Detach}`) is backend-agnostic — a Phase-2 migration swaps the bridge callback + the cook side + a format bump. `execHookText` reuses the SAME `execStmtRun` that runs all Phase-1 gameplay (not hook-specific scaffolding). **Surface findings vs the brief's premise** (the brief's NOTE pre-authorizes adapting to the actual surface): (1) the interpreter has **no `entity.add(T)`/`spawn`/`despawn`** structural mutation in bodies (S4 boundary, `interp.zig` header) and the only deferred-structural producer (tag mutation) is **not in the cookable hook subset** `{let,emit,expr,assign,return}` — so a cooked Etch hook **cannot** itself issue a deferred structural change; the "drained before `on_spawned`" test is realized with a Tier-0 stand-in attach callback that enqueues into the same observer-deferred channel (Recorded deviation). (2) **[B1 round-trip 2026-06-30]** the Etch `activate_extension`/`deactivate_extension` ENQUEUE a deferred command (interp-side `pending_extensions`, mirror of `pending_tags`, drained at the tick boundary by `flushPendingExtensions`) instead of an immediate `add`/`removeComponentDynamic` — fixing the archetype-migration-mid-`iterateArchetype` corruption. The immediate `runtimeActivate`/`runtimeDeactivate` (and the new bytes-taking `pub activateExtension`/`deactivateExtension`) stay for the load + direct-programmatic paths (outside iteration). (3) The activate/deactivate/has/active tests live in `tests/scene/extensions_test.zig`, not inline in `interp.zig`, because they need the cook pipeline (`scene_cook` → circular import otherwise) — the M1.0.8 tier-dependency precedent. (4) **[B2 round-trip 2026-06-30]** the four entity methods are recognized by the **type-checker** (`dispatchMethodOnType` entity arm, `types.zig`: `activate`/`deactivate_extension → unit`/unknown, `has_extension → bool`, `active_extensions → [string]`, arg-validated) — a real rule body calling them passes `weld check`; tests no longer skip the checker. **Deferred (not M1.0.9)**: the §30.5 compile-time additive-conflict warning (out of brief scope); a `test`-runner (the `ast.zig` `TestDecl` comment "execution surface is M1.0.9" is a pre-existing M1.0.8 imprecision — M1.0.9 delivers HOOK execution, not `test` execution).
- **M1.0.10 scope boundary (structural mutation in bodies)** — the four ops (`spawn`/`despawn`/`add(T)`/`remove(T)`) are DEFERRED structural changes routed through the Tier-0 `CommandBuffer` (`world.observer_registry.deferred`), drained at the tick boundary via `applyWithObservers` (NOT a parallel `pending_*` queue). **In-body `spawn` is statement-only, NO body handle** (v0.6, `etch-grammar.md §4.5`): binding/using a spawn result is refused (`E0304`); the sentinel/planned-pool that would back a body handle is reserved post-v0.6 (`etch-bytecode.md §11.2`) — NOT built. **Prefab-name `spawn("X")`** parses + is recognized but refused (`E0305`), gating on the prefab runtime (post-Etch). **Recorded deviation (Claude.ai round-trip, VERDICT E2 — STOP):** E2 was completed to statically validate component-literal fields (`E0306`/`E0307`) by reusing the scene/prefab `checkComponentInstance` path — "type the expression" implies field validation for `weld check` / C1.6. **Design note (E3):** the 4 ops route via a `structuralDeferred` helper (prefers `observer_deferred` when bound, else the world's shared buffer) rather than binding `observer_deferred` in `execBody` — equivalent routing, leaves the tag/extension `pending_*` paths untouched. **Out (later milestones):** async `spawn { }` task (M1.0.11); the §30.5 additive-conflict cook warning (later cook milestone).
- **M1.0.10 scope boundary (structural mutation in bodies)** — the four ops (`spawn`/`despawn`/`add(T)`/`remove(T)`) are DEFERRED structural changes routed through the Tier-0 `CommandBuffer` (`world.observer_registry.deferred`), drained at the tick boundary via `applyWithObservers` (NOT a parallel `pending_*` queue). **In-body `spawn` is statement-only, NO body handle** (v0.6, `etch-grammar.md §4.5`): binding/using a spawn result is refused (`E0304`); the sentinel/planned-pool that would back a body handle is reserved post-v0.6 (`etch-bytecode.md §11.2`) — NOT built. **Prefab-name `spawn("X")`** parses + is recognized but refused (`E0305`), gating on the prefab runtime (post-Etch). **Recorded deviation (Claude.ai round-trip, VERDICT E2 — STOP):** E2 was completed to statically validate component-literal fields (`E0306`/`E0307`) by reusing the scene/prefab `checkComponentInstance` path — "type the expression" implies field validation for `weld check` / C1.6. **Design note (E3):** the 4 ops route via a `structuralDeferred` helper (prefers `observer_deferred` when bound, else the world's shared buffer) rather than binding `observer_deferred` in `execBody` — equivalent routing, leaves the tag/extension `pending_*` paths untouched. **Out (later milestones):** async `spawn { }` task (M1.0.12 — M1.0.11 built the async suspension core but the `spawn { }` task form ships with the concurrency algebra); the §30.5 additive-conflict cook warning (later cook milestone).
- **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**.

## Non-negotiable rules

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

---

Last updated: 2026-06-30
Last updated: 2026-07-01
Loading