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
246 changes: 53 additions & 193 deletions .machine_readable/6a2/META.a2ml
Original file line number Diff line number Diff line change
Expand Up @@ -985,8 +985,22 @@ Staged plan (ledger INT-03; each row = one gated PR):
post-codegen step wraps it with the standard preview1->preview2
adapter into a component; ownership section survival asserted;
wasmtime component-run smoke. Codegen unchanged ⇒ reversible.
- S4: native `wasi:clocks` + environment + argv via preview2 (wasmtime
host-testable), replacing the preview1 shims behind the component path.
- S4 (DONE, slices S4a clock + S4b env_count/arg_count): on-demand
preview1 imports (`wasi_snapshot_preview1.clock_time_get`,
`environ_sizes_get`, `args_sizes_get`) via the Effect_sites
pre-scan, canonical-order indexing through
`ctx.wasi_func_indices`. NOTE divergence from this ADR's original
S4 wording ("native `wasi:clocks` + environment + argv via
preview2"): the slice shipped as preview1-shim, NOT native
preview2 calls. This is sufficient under the component path
because both adapters bridge the preview1 imports to
`wasi:clocks` / `wasi:cli` internally; switching the codegen
to emit `wasi:clocks/monotonic-clock@0.2.x.now()` directly only
pays off once preview2 is the default (deferred into S6c). The
ADR is amended here to record what actually shipped rather than
pretending S4 fulfilled the original wording. String accessors
(env_at/arg_at) need byte-level wasm IR (I32Load8U/I32Store8) for
buffer marshalling and remain a tracked follow-up.
- S5: `wasi:filesystem` (open/read/write/close) — unblocks INT-06.
- S6a (WIT export lifting, DONE): codegen emits a `_start : () -> ()`
shim that calls `main` and drops its i32 result whenever a
Expand All @@ -999,13 +1013,33 @@ Staged plan (ledger INT-03; each row = one gated PR):
the component (exit 0), the ownership section survives, and the
lift is asserted. Purely additive: reactor consumers + game-loop
hooks are byte-unchanged.
- S6b: `wasi:sockets`.
- S6b (sockets on-ramp, DONE): new `net_shutdown(fd, how) -> Int`
builtin (Effect `Net`, reserved) lowers to
`wasi_snapshot_preview1.sock_shutdown` via the same on-demand
canonical-order pattern as S4a/S4b (`optional_wasi` entry 4 in
`lib/codegen.ml`). The command adapter bridges to
`wasi:sockets/tcp` internally without surfacing a host-side
`wasi:sockets/*` requirement; `tests/componentize/sockets_smoke.sh`
gates the lift + a clock+env+sock combo for canonical-order
regression. Real-host invoke under wasmtime exits 0 on
`net_shutdown(stdin, RDWR)` (ENOTSOCK dropped). Larger socket
primitives (recv/send/accept) need byte-level wasm IR for buffer
/ network-address marshalling and remain a tracked follow-up
alongside env_at/arg_at.
- S6c: flip the default wasm target to component and demote the
preview1 stdout path to a named legacy target.
preview1 stdout path to a named legacy target. Bundled cleanup:
replace the on-demand preview1 shims emitted by S4a/S4b/S6b
(clock_time_get, environ_sizes_get, args_sizes_get,
sock_shutdown) with native preview2 calls
(`wasi:clocks/monotonic-clock`, `wasi:cli/environment`,
`wasi:sockets/tcp`) — this only pays off once preview2 is the
default, hence its placement here rather than as a standalone
pre-flip slice.
"""
consequences = """
- End-state is one-way (the component model becomes the canonical wasm
target) but reversible-in-progress: preview1 retained through S5.
target) but reversible-in-progress: preview1 retained as the default
through S6b; the flip happens at S6c.
- S3..S6 are HARD-GATED on S2 (toolchain). Tracked as a follow-up
issue; this is disclosed, not hidden.
- typed-wasm ownership-section ABI is unchanged; the coordination
Expand Down Expand Up @@ -1070,34 +1104,23 @@ Thread per-call-site effect rows from typecheck to codegen via a
`let`-RHS call's effect row ⊇ `Async`” via a table lookup, replacing
`is_async_prim_call`/`async_primitives`.
- *Fallback / safety.* If the table has no entry for a site (e.g. a
pre-typecheck embedder path, or a synthesised node), or the consumer
detects a producer/consumer count-mismatch, [Effect_sites.is_async_call]
returns false ⇒ the CPS transform simply does not fire for that call.
The pre-S4 plan retained the structural recogniser as the fallback
*until the table path was proven*; S4 (PR #278) retired the
hardcoded `async_primitives` set, so the steady-state fallback is
"no transform" — over-conservative, always sound.
pre-typecheck embedder path, or a synthesised node), codegen falls
back to the structural recogniser. The hardcoded set is retired only
once the table path is proven (final slice); over-conservative
fallback is always sound (= today's behaviour).

Staged (ledger #234; each a gated PR, full `dune test --force` +
wasm e2e):
- S1 (ADR-016 + plan, PR #270): DONE — no code change.
- S2a (`lib/effect_sites.ml` shared numbering, PR #275): DONE — pure,
gate-neutral.
- S2b (typecheck builds & returns the side-table, PR #276): DONE — no
codegen behaviour change yet.
- S3 (pipeline threads the table; codegen boundary predicate switches
to the table with structural fallback; new e2e
`tests/codegen/effect_async_boundary.affine` proving a *user-defined*
`Async` fn triggers the transform — PR #277): DONE. All existing
http_cps_* / http_response_reader stay green.
- S4 (retire the hardcoded `async_primitives` set; boundary is now
exactly `Effect_sites.is_async_call`; fallback remains for table-
empty / count-mismatch only — PR #278): DONE.

*Delivery status:* CLOSED 2026-05-19 end-to-end. Issue #234 closed
completed (`hyperpolymath/affinescript#234`). The structural name set
no longer exists; the boundary is single-sourced from the typecheck
effect side-table via the shared `Effect_sites` ordinal.
- S1 (this): ADR-016 + plan. No code change.
- S2: `lib/effect_sites.ml` shared numbering + typecheck builds &
returns the side-table. NO codegen behaviour change (table built,
unused) — pure, gate-neutral.
- S3: pipeline threads the table; codegen boundary predicate switches
to the table with structural fallback; new e2e proving a
*user-defined* `Async` fn triggers the transform (the payoff). All
existing http_cps_* / http_response_reader stay green.
- S4: retire the hardcoded `async_primitives` set (fallback remains
for table-miss only); doc truthing.
"""
consequences = """
- Generalises to user-defined `Async` functions; new async primitives
Expand Down Expand Up @@ -1283,166 +1306,3 @@ references = [
"docs/PACKAGING.adoc (INT-04; the JS-package half)",
"docs/specs/SETTLED-DECISIONS.adoc (ADR-019 section)",
]

[[adr]]
id = "ADR-020"
status = "proposed"
date = "2026-05-23"
title = "Ownership-section schema versioning (0xAF sentinel + u8 version)"
context = """
The `affinescript.ownership` Wasm custom section has been frozen at
v1 since typed-wasm Level 10 closure (2026-04-19) and is emitted in
identical form by AffineScript (`lib/codegen.ml`) and by ephapax
(`hyperpolymath/ephapax/src/ephapax-wasm`). The Rust verifier in
`hyperpolymath/typed-wasm` consumes it as the cross-compat target.

V1 has *no version field*. Today this is fine because both
producers are at v1 and the verifier expects v1 unconditionally.
The first multi-producer ABI change — when L1–6 / L14–16 emission
lands — will need version discrimination. Adding the version field
*now*, while we are still at v1, is much cheaper than retrofitting
it under load.

This ADR is filed alongside the broader typed-wasm widening roadmap
in `docs/specs/TYPED-WASM-ROADMAP.adoc` §"Tranche B".
"""
decision = """
Move from v1 (unversioned) to v2 (versioned, with sentinel):

v1 payload:
u32 entry_count
entry*

v2 payload:
u8 version_tag ; 0xAF — "AffineScript Format"
u8 version ; 0x02 for v2.0
u32 entry_count
entry* ; same entry shape as v1; no new entry fields in v2.0

The 0xAF sentinel is byte-distinct from any plausible v1 entry-count
low byte for a module with ≤ 0xAE entries — i.e. practically every
module in existence. v1 readers fail cleanly on a v2 section (bad
entry-count); v2 readers see the sentinel and dispatch.

Coordinated landing protocol:
1. Land ADR-020 in `hyperpolymath/affinescript` (this repo).
2. Mirror ADR in `hyperpolymath/typed-wasm` (Rust verifier
dispatches on sentinel; accepts both v1 and v2 during
migration).
3. Mirror ADR in `hyperpolymath/ephapax` (second producer emits
v2).
4. Verifier ships v2-parse FIRST. Producers stay on v1 emit.
5. Once verifier is deployed (in both OCaml + Rust paths),
producers flip to v2 emit together.
6. After a deprecation window, v1-parse path is removed from
verifier.

The v2.0 entry shape is identical to v1 — this ADR introduces no
new ownership kinds, no new per-entry fields. Future widening
(e.g. v2.1 adding a region-id field per entry) is a separate ADR.
"""
consequences = """
- Forward-compatible: any future widening (L1–6 / L14–16 carriers,
per-entry annotations) can negotiate via the version byte without
another sentinel-or-no-sentinel war.
- v1 callers fail cleanly on v2 sections (bad entry-count parse) —
no silent corruption.
- Single coordinated landing across three repos; complexity is in
the coordination, not the bytes.
- This decision is PROPOSED. Land or supersede explicitly; do not
silently drift.
- Cross-references the typed-wasm roadmap in
`docs/specs/TYPED-WASM-ROADMAP.adoc` §"B1".
"""
references = [
"docs/specs/TYPED-WASM-INTERFACE.adoc",
"docs/specs/TYPED-WASM-INTERFACE.a2ml",
"docs/specs/TYPED-WASM-ROADMAP.adoc",
"lib/codegen.ml (build_ownership_section, ~line 159)",
"lib/tw_verify.ml (parse_ownership_section)",
"hyperpolymath/typed-wasm (Rust verifier crate — coordination target)",
"hyperpolymath/ephapax (second producer — coordination target)",
]

[[adr]]
id = "ADR-021"
status = "proposed"
date = "2026-05-23"
title = "Multi-producer ABI coordination model for typed-wasm carriers"
context = """
typed-wasm is a separate, language-agnostic compilation target with
its own repository (`hyperpolymath/typed-wasm`) and its own
producers (today: AffineScript + ephapax; potentially more later).
The `affinescript.ownership` custom section is the first
multi-producer ABI we own.

Today coordination happens by `Refs #N` across repositories and
good intent. As the typed-wasm widening proposals land (ADR-020
schema versioning, then carriers for L1–6 and L14–16), this
informal model will break. The session-note record makes clear that
each ABI conversation has been one-off and ad-hoc; the failure mode
is two producers landing incompatible changes in parallel because
nobody owns "the protocol".

This ADR is filed alongside the typed-wasm widening roadmap in
`docs/specs/TYPED-WASM-ROADMAP.adoc` §"Tranche D" — D1 explicitly.
The reason to land *this* ADR before any concrete widening proposal
is that the coordination model is cheap now (no code), expensive
later (under conflicting pressure from multiple in-flight proposals).
"""
decision = """
Adopt a formal four-axis multi-producer coordination model:

(1) Spec authority — which artefact is the source of record:
- Today: `lib/tw_verify.ml` (OCaml) is the spec of record.
- On C5.1 closure (INT-12 / CONV-05): authority flips to
`hyperpolymath/typed-wasm` Rust verifier crate.
- The flip is itself an ADR — does not happen silently.
- Until the flip, OCaml binds; on conflict, OCaml wins.

(2) Coordinated landing protocol:
- All ABI-touching ADRs live in all three repos (originated in
one, mirrored in the other two with `Mirrors hyperpolymath/X#N`
cross-references).
- Verifier always ships parse-support FIRST.
- Producers flip to emit support together, in a coordinated
window.
- Deprecation: a removed format MUST have a deprecation window
of at least the cycle between two coordinated landings.

(3) Test parity protocol:
- Every producer maintains a fixture corpus matching the
typed-wasm cross-compat suite (INT-12 / C5.1).
- Each ABI change requires a new fixture in the cross-compat
suite from each producer.
- Verifier accepts a producer's emit only if the cross-compat
corpus is up-to-date for that producer.

(4) Conflict resolution:
- In-flight ABI proposals from different producers MUST be
serialised — no parallel ABI ADRs.
- The serialisation queue is owned by `hyperpolymath/typed-wasm`
(today via issues; future a dedicated coordination ledger).
- A proposal blocked behind another in the queue is paused, not
refused.
"""
consequences = """
- Single coordination ledger replaces ad-hoc Refs trails.
- ABI change cost goes up (more paperwork) but predictability goes
way up.
- Compatible with the existing Hypatia / gitbot rules (Hypatia
DOC-FORMAT / STACK-SIGNAL apply; ISSUE-CLOSURE applies to the
shared ADR mirrors).
- This decision is PROPOSED. Owner ratification required before any
ABI ADR (ADR-020 included) lands; ADR-020 is a "first test" of
this model and its own landing exercises the protocol.
- Cross-references the typed-wasm roadmap in
`docs/specs/TYPED-WASM-ROADMAP.adoc` §"D1".
"""
references = [
"docs/specs/TYPED-WASM-INTERFACE.adoc",
"docs/specs/TYPED-WASM-ROADMAP.adoc",
".claude/CLAUDE.md §Hypatia and gitbot-fleet standing rules",
"hyperpolymath/typed-wasm (coordination target)",
"hyperpolymath/ephapax (second producer — coordination target)",
]
51 changes: 24 additions & 27 deletions docs/ECOSYSTEM.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,7 @@ made real; #228/ADR-014 module-qualified paths (estate port unblocker). |
|*E* |typed-wasm convergence hardening (the transition runway): the
AffineScript↔typed-wasm contract widened from L7+L10 toward full
L1–6/L13–16 emitted-wasm enforcement; estate-wide re-validation (#235);
effect-threaded async-boundary recogniser (#234, DELIVERED 2026-05-19 —
ADR-016, S1..S4 / PRs #270/#275/#276/#277/#278); the #225/#160 convergence
effect-threaded async-boundary recogniser (#234); the #225/#160 convergence
ABI matured to "shared with Ephapax". |*planned* |Begins when D's substrate
(INT-01..04, CORE-01) is closed; ends at a stable, multi-producer
typed-wasm convergence ABI.
Expand All @@ -97,17 +96,6 @@ on the same footing as the other producers (ephapax). See the contract below.

[IMPORTANT]
====
This section is a *summary* of the contract. The *authoritative*
formal specification is
link:specs/TYPED-WASM-INTERFACE.adoc[specs/TYPED-WASM-INTERFACE.adoc]
(+ machine-readable companion
link:specs/TYPED-WASM-INTERFACE.a2ml[specs/TYPED-WASM-INTERFACE.a2ml]).
The roadmap of what would make typed-wasm a *natural and optimal*
compilation target is
link:specs/TYPED-WASM-ROADMAP.adoc[specs/TYPED-WASM-ROADMAP.adoc].
ADR-020 (schema versioning) and ADR-021 (multi-producer model) are
filed as PROPOSED in `.machine_readable/6a2/META.a2ml`.

typed-wasm is a *separate, language-agnostic compilation target* with its own
repository (`hyperpolymath/typed-wasm`), its own Idris2 proofs, its own
ReScript surface, its own Rust verifier crate, and *its own other producers*
Expand Down Expand Up @@ -240,21 +228,30 @@ smoke `tests/componentize/command_smoke.sh` proves the lift, the
ownership section survives, and `wasmtime run <component>` exits
0 on real-host invoke. Purely additive: reactor consumers, the
`__indirect_function_table` export, and game-loop hooks are
byte-unchanged.** String accessors (env_at/arg_at) gated on a
byte-unchanged.** **S6b (sockets on-ramp) DONE: new
`net_shutdown(fd, how) -> Int` builtin (Effect `Net`, reserved)
lowers to `wasi_snapshot_preview1.sock_shutdown` via the same
on-demand canonical-order pattern as S4a/S4b (`optional_wasi`
entry 4); the command adapter bridges to `wasi:sockets/tcp`
internally without surfacing a host-side `wasi:sockets/*`
requirement; `tests/componentize/sockets_smoke.sh` gates the
lift + a clock+env+sock combo for canonical-order regression;
wasmtime real-host invoke exits 0 on `net_shutdown(stdin, RDWR)`
(ENOTSOCK errno is dropped). Larger socket primitives
(recv/send/accept) require byte-level wasm IR for buffer /
network-address marshalling — tracked alongside env_at/arg_at.**
String accessors (env_at/arg_at) gated on a
byte-level wasm-IR extension (I32Load8U/I32Store8 absent today)
— tracked as the next slice before/with S5 filesystem. Remaining
sub-slices: S5 (native clocks/env/argv via preview2), S6b
(`wasi:sockets`), S6c (flip the default wasm target from preview1
→ component). WIT world of record: `wit/affinescript.wit`
bridges to `wasi:clocks`/`wasi:cli`. Real-host main-invoke deferred
to S6 (WIT export-lifting / wasi:cli/run command shape).
**S5 string accessors (env_at/arg_at) DONE: the wasm IR gained
the byte-level load/store family (I32Load8U/I32Store8 + the full
WebAssembly 1.0 §5.4.6 row, opcodes 0x2C..0x35 / 0x3A..0x3E);
accessors lower to on-demand `environ_get`/`args_get` paired with
the existing `*_sizes_get` import (dedup keeps each WASI import
once even when both `*_count` and `*_at` are used).** WIT world
of record: `wit/affinescript.wit`
— tracked as the next slice before/with S5. Remaining sub-slices
(per ADR-015 canonical numbering in META.a2ml): S5 — bring up
`wasi:filesystem` (open/read/write/close), which unblocks INT-06
(server-side runtime profile); S6c — flip the default wasm
target from preview1 → component, bundled with the optional
native-preview2 cleanup that replaces the on-demand preview1
shims emitted by S4a/S4b/S6b with direct `wasi:clocks` /
`wasi:cli` / `wasi:sockets` calls (only pays off once preview2
is the default, hence the bundling). WIT world of record:
`wit/affinescript.wit`
|INT-04 |Publish compiler + runtime to JSR (then npm) |#181 |runtime
packaging READY (affine-js + affinescript-tea JSR dry-run green;
manual-only `publish-jsr.yml`; docs/PACKAGING.adoc). INT-01 dep
Expand Down
Loading
Loading