Skip to content

Phase 1 / Etch / Execute extension hooks#37

Open
guysenpai wants to merge 16 commits into
mainfrom
phase-1/etch/extension-hooks
Open

Phase 1 / Etch / Execute extension hooks#37
guysenpai wants to merge 16 commits into
mainfrom
phase-1/etch/extension-hooks

Conversation

@guysenpai

@guysenpai guysenpai commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Milestone

Brief: briefs/M1.0.9-extension-hooks.md — Status CLOSED.

M1.0.9 founds the runtime text-execution surface M1.0.6 deferred: a cooked extension on_attach/on_detach hook is re-parsed and walked against the live world. Decision frozen at scoping: TEXT re-parse, not bytecode (the VM is Phase 2). The last Etch milestone.

Deliverables (Scope E1–E5)

  • E1parser.parseStmtBlock: parse a cooked hook statement-run ("; "-joined, no braces) into a transient AstArena (reuses parseStmt via parseStmtFragment, skipping one optional .semicolon).
  • E2interp.execHookText: transient-arena self.ast rebind + implicit entity bind + execStmtRun + observer-deferred routing; bindToWorld registers the real on_attach/on_detach trampolines so the loader's dispatchOnAttach reaches execution.
  • E3world.zig on_detach seam (mirror) + loader runtime activate/deactivate + dispatchMethodOnValue activate_extension/deactivate_extension.
  • E4 — per-entity active-extension side-table + has_extension/active_extensions Etch methods.
  • E5CLAUDE.md §3.4.

✅ B1 + B2 round-trip (the two merge blockers — fixed)

Recorded as round-tripped FROZEN deltas in the brief (docs(brief): record B1+B2 round-trip).

  • B1 — immediate structural mutation mid-iteration → deferred. iterateArchetype walks arch.chunks live; the Etch activate_extension/deactivate_extension previously did an immediate add/removeComponentDynamic → archetype migration mid-walk → corruption. Now dispatchMethodOnValue ENQUEUES a deferred op (pending_extensions, mirror of pending_tags, extension bytes resolved at the call), drained at the tick boundary by flushPendingExtensions (after iteration; snapshot → no recursive drain) via the loader's bytes-taking activateExtension / new deactivateExtension (both pub), which fire the Tier-0 on_attach/on_detach seam. The immediate runtimeActivate/runtimeDeactivate stay for the load + direct-programmatic paths (outside iteration). Realization note (Recorded deviations): the deferred queue is interp-side, not the Tier-0 CommandBuffer, because the ECS command-buffer apply path cannot reach scene/loader (the ecs → scene layering forbids it); the interpreter is the correct tier to own + flush it. +1 test: a multi-entity rule activates every matched entity with no corruption, effects applied after the flush.
  • B2 — type-checker now recognizes the four methods. dispatchMethodOnType's entity arm (types.zig) rejected any non-inherent/trait method on an Entity, so a real rule body calling the methods failed weld check. The entity arm now recognizes activate_extension(string) -> unit, deactivate_extension(string) -> unit, has_extension(string) -> bool, active_extensions() -> [string] (arg-validated) before the "no method" error. This supersedes the first-close "interpreter-level only" position — tests no longer skip the checker. +1 test: a checked program calling all four passes clean; a wrong-typed arg is still rejected.

⚠️ One item still for review

The two B-flags are resolved. The only remaining point is surface-adaptation #1 (Recorded deviations): the "on_attach structural command drained before on_spawned" acceptance test uses a Tier-0 stand-in attach callback, NOT a cooked Etch hook issuing entity.add(...)/spawn — because the interpreter has no entity.add/spawn in bodies (S4 boundary) and tag mutation is not cookable, so a cooked hook cannot itself issue a deferred structural change today. The drain mechanism is built + correct (and now also exercised by B1's deferred extension ops). Happy to round-trip if the literal form is wanted (it needs interp structural-mutation support — a separate milestone). Four further surface-adaptations (tests location, trampolines location, { source: entity }, §30.5 warning out of scope) are documented in the brief.

Closing notes

  • What worked: Recon-first caught, before any code: the ;-separator (cooked text is "; "-joined; the parser only consumes ; inside fill-arrays → parseStmtBlock needs its own separator-skipping loop), the brief's correct call that self.ast is rebindable (the rebind is required — the executor resolves identifiers via self.ast.strings), and the two-EntityId duality. execHookText mirrors runObserverBody; B1's pending_extensions mirrors pending_tags — both inherit proven discipline.
  • What deviated: the round-trip closed the two first-close flags (B1 deferral, B2 type-checking). Remaining: five non-round-tripped surface-adaptations, only Phase -1 / Bootstrap / Repo and CI #1 touching a frozen acceptance test.
  • Final measurements: N/A — correctness milestone. +14 tests (parser 2, interp 3, world 2, scene 6 incl. B1, types 1 B2); full suite green debug + ReleaseSafe (no leaks), test-extensions 14/14, zero build warnings, fmt/lint clean.
  • Residual: a cooked hook can't issue a deferred structural change until the interp gains entity.add/spawn (drain wiring future-proofs it); §30.5 compile-time warning deferred; two out-of-scope "M1.0.9" doc-comment refs left untouched (ast.zig:647, prefab_integration_test.zig:6).

Validation (Étape 4, re-run after B1+B2)

  • All Scope deliverables present (E1–E5 + B1 + B2)
  • No drift into Out of scope (no bytecode; no last-wins; no @exclusive_with; no multi-entity; no weld extension reapply; no re-type-check of the fragment)
  • Acceptance tests green in debug and ReleaseSafe (incl. the B1 multi-entity + B2 type-check tests)
  • Benchmarks: N/A (correctness milestone)
  • Observable behavior demonstrable: cooked scene + CombatModuleHealth.max 100→150 at load; multi-entity activate round-trip
  • zig build (zero warnings), zig build test, zig fmt --check, zig build lint green; commit-msg green on every commit; pre-push (build + test + test-release + test-tsan-wayland) green
  • CLAUDE.md updated (§3.4 + B1/B2 reconciliation) and committed on the branch
  • §3.6.1 language audit passed (no French in diff + brief)
  • §3.6.1 drift audit passed (stale "execution is M1.0.9" comments refreshed; B1/B2 terms have no orphans; 2 out-of-scope residual refs recorded)
  • Closing notes filled + reconciled; Status: CLOSED, Closed: 2026-06-30
  • Final commit docs: reconcile CLAUDE.md for B1+B2 (M1.0.9)

Merge + tag (v0.10.9-extension-hooks) left to you.

🤖 Generated with Claude Code

guysenpai and others added 16 commits June 30, 2026 00:13
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Parse a bare statement-run fragment (the canonical text a cooked extension
hook body carries: statements joined by "; ", no enclosing braces) into a
fresh AstArena, exposing the body statement range (body_start/body_len) in the
same encoding rule/fn bodies use. Reuses the existing parseStmt via a new
parseStmtFragment loop that skips one optional .semicolon between statements
(the parser otherwise only consumes ; inside a fill-array literal). New entry
point parseStmtBlock + StmtBlockResult, not a new statement grammar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
world.zig gains the on_detach dispatch seam (ExtensionDetachFn + detach_hook +
registerOnDetach/dispatchOnDetach), a mirror of the M1.0.6 on_attach pair —
fired by the runtime deactivate path before removing an extension's components.
Plus a per-entity active-extension side-table (entity_extensions:
EntityId -> owned name slices) with addEntityExtension/removeEntityExtension/
hasEntityExtension/entityExtensions, populated by the shared activate path and
freed in deinit. Backs the interpreter's has_extension/active_extensions.
Inline tests cover the detach seam (register/dispatch/no-op) and the side-table
(add/has/remove/order + leak-freedom).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The M1.0.6 on_attach seam now runs the cooked Etch text. interp.zig gains
execHookText: parse the hook statement-run (parseStmtBlock) into a transient
AstArena, rebind self.ast to it, bind the implicit entity, run via execStmtRun,
and route deferred structural changes through the world's shared observer-
deferred buffer (mirrors runObserverBody). bindToWorld registers the real
on_attach/on_detach trampolines (ctx = *Interpreter); dispatchMethodOnValue's
entity arm gains activate_extension/deactivate_extension/has_extension/
active_extensions. The Bridge holds an optional ExtensionResolver
(setExtensionResolver) for name-only runtime activation.

loader.zig: shared activateExtension now records the active extension
(addEntityExtension) before firing on_attach; runtimeActivate/runtimeDeactivate
are the runtime entries (deactivate fires on_detach first, then removes the
extension's components); the load sequence drains hook-issued deferred commands
after the activation pass, before on_spawned. ecs_bridge.zig: ext_resolver
field. Decision frozen at scoping: TEXT re-parse, not bytecode.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Headline: a cooked scene activating CombatModule runs on_attach at load
(Health.max 100->150). Plus activate/deactivate/has_extension/active_extensions
via Etch methods (single-entity rules keep the immediate structural mutation
safe), and a deferred-command drain-before-on_spawned test via a Tier-0 stand-in
attach callback (the interpreter has no entity.add/spawn in bodies, so a cooked
hook cannot itself issue a deferred structural change). These live here, not in
interp.zig, because they need the cook pipeline (circular import otherwise) —
the M1.0.8 tier-dependency precedent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Now that the on_attach/on_detach seam executes the cooked Etch text via the
bridge's registered callback, the doc comments on ExtensionAttachFn,
dispatchOnAttach, applyExtensions, and the M1.0.6 seam test no longer frame
execution as a future M1.0.9 deferral. Part of the §3.6.1 closing audit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Current-state table (last released tag v0.10.9, next milestone M1.1.0), a new
Tags row for v0.10.9-extension-hooks, the M1.0.6 text-vs-bytecode decision closed
(text re-parse), a new M1.0.9 scope-boundary open-decision entry (surface
findings + recorded deviations), and the Last updated date.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reopened ACTIVE for the merge-blocker round-trip. FROZEN delta: E3 gains the
B1 deferred-command requirement (Etch activate/deactivate enqueue, applied at
the flush boundary after iteration — runtimeActivate/runtimeDeactivate stay for
load + direct paths); E4 gains the B2 type-checker recognition of the four
entity methods; two acceptance tests added; Recorded deviations B1 + B2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
iterateArchetype walks arch.chunks LIVE, so an Etch activate_extension/
deactivate_extension doing an immediate add/removeComponentDynamic migrated the
entity's archetype mid-iteration and corrupted the walk. dispatchMethodOnValue
now ENQUEUES a deferred command (PendingExtension, mirror of pending_tags;
extension bytes resolved at the call), drained at the tick boundary by
flushPendingExtensions (after iteration, snapshot to avoid recursive drain),
which calls the loader's bytes-taking activateExtension / new deactivateExtension
(both pub now) — components added/removed + the Tier-0 seam fired. The immediate
runtimeActivate/runtimeDeactivate stay for the load + direct-programmatic paths.
+1 test: a multi-entity rule activates each matched entity with no corruption.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
synthMethodCall's entity arm (dispatchMethodOnType, types.zig) emitted
type_mismatch "no method on an Entity" for any non-inherent/trait method, so a
real rule body calling activate_extension/deactivate_extension/has_extension/
active_extensions failed weld check — the API was unusable from type-checked
Etch. The entity arm now recognizes the four builtin methods before the trait
lookup (falling through for anything else): activate/deactivate_extension(string)
-> unit (unknown, statement-use), has_extension(string) -> bool,
active_extensions() -> [string], with arg-count/type validation
(checkExtensionNameArg). +1 test: a checked program calling all four passes
clean; a wrong-typed arg is still rejected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Journal B1+B2 implementation; reconcile Closing notes (the two first-close flags
are now RESOLVED — B1 defers the Etch activate/deactivate, B2 type-checks the
four methods); +14 tests; Status CLOSED.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The M1.0.9 scope-boundary items (2) immediate-mutation and (4) interpreter-level-
only are now superseded by the round-trip: B1 defers the Etch activate/deactivate,
B2 type-checks the four methods. Tags row notes B1+B2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant