Skip to content

Commit a41b296

Browse files
committed
docs(adr): ADR-015 WASI Preview2 / WASM Component-Model migration (Refs #180)
INT-03 (#180): lib/wasi_runtime.ml is wasi_snapshot_preview1 fd_write to stdout only — no files/sockets/env/clock, so no server-side runtime (INT-06 blocked). The approach was an escalated one-way-door fork (AskUserQuestion 2026-05-19); the owner chose the FULL WebAssembly Component-Model re-target (WASI 0.2) over preview1-surface expansion and the preview1+adapter option. This is slice S1 — the decision record + contract, no codegen change (the rigorous discipline for a HIGH one-way door: ADR before irreversible implementation): - ADR-015 added to docs/specs/SETTLED-DECISIONS.adoc + the .machine_readable/6a2/META.a2ml [[adr]] block (format-matched to ADR-012/014). Staged S1..S6; legacy preview1 path stays DEFAULT until S6 ⇒ reversible-in-progress. The affinescript.ownership custom section is a multi-producer ABI (ephapax + typed-wasm) and must survive verbatim — format unchanged, coordinated upstream only. - wit/affinescript.wit — the WIT world of record (`affinescript:cli` world `run`), wasi:cli/clocks/filesystem/sockets 0.2 imports + the wasi:cli/run export, with the slice rollout annotated. - docs/ECOSYSTEM.adoc + docs/TECH-DEBT.adoc INT-03 rows truthed. - S2 toolchain prerequisite (wasm-tools/wasm-component-ld/wac — ABSENT; hard gate on S3+) filed as #251 and disclosed in the ADR. Gate: dune test --force 271/271 (no code change; zero regression). Refs #180 (#251 = S2 toolchain gate). Not Closes — INT-03 is the staged campaign; owner closes per ISSUE-CLOSURE.
1 parent b9db675 commit a41b296

5 files changed

Lines changed: 176 additions & 2 deletions

File tree

.machine_readable/6a2/META.a2ml

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -938,3 +938,81 @@ references = [
938938
"META.a2ml [[adr]] ADR-011 (real modules / qualified paths)",
939939
"META.a2ml [[adr]] ADR-012 (grammar changes are correctness assertions)",
940940
]
941+
942+
[[adr]]
943+
id = "ADR-015"
944+
status = "accepted"
945+
date = "2026-05-19"
946+
title = "WASI Preview2 / WASM Component-Model migration (INT-03)"
947+
context = """
948+
lib/wasi_runtime.ml emits only `wasi_snapshot_preview1.fd_write` to
949+
stdout (fd 1). There is no file, socket, environment, clock, or argv
950+
access, so there is no server-side runtime profile (INT-06 is blocked)
951+
and no real host I/O. INT-03 (#180) is the substrate fix. The owner was
952+
asked (2026-05-19, AskUserQuestion one-way-door fork) to choose between
953+
(a) expanding the preview1 import surface, (b) preview1 + an external
954+
preview1->preview2 adapter as the run path, or (c) a full re-target to
955+
the WebAssembly Component Model with WASI 0.2 (preview2). The owner
956+
chose (c) — explicitly the largest, one-way, highest-blast-radius path.
957+
"""
958+
decision = """
959+
Re-target the WASM output to the WebAssembly Component Model with WASI
960+
0.2 (preview2) WIT worlds. Binding constraints:
961+
962+
- STAGED, non-breaking per slice; each slice is its own PR behind the
963+
full gate. The legacy core-wasm + preview1 stdout path stays the
964+
DEFAULT until the final slice flips it, so the migration is
965+
reversible while in progress even though the end-state is one-way.
966+
- The `affinescript.ownership` custom section is a MULTI-PRODUCER ABI
967+
(shared with hyperpolymath/ephapax; the typed-wasm contract carrier,
968+
see docs/ECOSYSTEM.adoc). It MUST survive verbatim onto the
969+
component's embedded core module. Its format MUST NOT change here;
970+
any change is coordinated in hyperpolymath/typed-wasm, never made
971+
unilaterally for this migration.
972+
- Only the `wasm`/`wasm-gc` target re-points. The 22+ source-to-source
973+
targets (Deno-ESM, Node-CJS, Julia, …) are unaffected.
974+
- WIT world of record: `wit/affinescript.wit` — world
975+
`affinescript:cli/run` importing the `wasi:cli`, `wasi:clocks`,
976+
`wasi:filesystem`, and `wasi:sockets` 0.2 interfaces.
977+
978+
Staged plan (ledger INT-03; each row = one gated PR):
979+
- S1 (this PR): ADR-015 + WIT world + staged plan + tooling-prerequisite
980+
+ roadmap truthing. No codegen change.
981+
- S2: toolchain provisioning — `wasm-tools` + `wasm-component-ld` (and
982+
`wac`) into guix.scm/flake.nix. ABSENT in the current toolchain ⇒ a
983+
HARD GATE on S3+: a component cannot be built or tested without them.
984+
- S3: componentize on-ramp — codegen still emits core wasm; a
985+
post-codegen step wraps it with the standard preview1->preview2
986+
adapter into a component; ownership section survival asserted;
987+
wasmtime component-run smoke. Codegen unchanged ⇒ reversible.
988+
- S4: native `wasi:clocks` + environment + argv via preview2 (wasmtime
989+
host-testable), replacing the preview1 shims behind the component path.
990+
- S5: `wasi:filesystem` (open/read/write/close) — unblocks INT-06.
991+
- S6: `wasi:sockets`; then flip the default wasm target to component
992+
and demote the preview1 stdout path to a named legacy target.
993+
"""
994+
consequences = """
995+
- End-state is one-way (the component model becomes the canonical wasm
996+
target) but reversible-in-progress: preview1 retained through S5.
997+
- S3..S6 are HARD-GATED on S2 (toolchain). Tracked as a follow-up
998+
issue; this is disclosed, not hidden.
999+
- typed-wasm ownership-section ABI is unchanged; the coordination
1000+
obligation with hyperpolymath/ephapax + hyperpolymath/typed-wasm is
1001+
recorded and binding.
1002+
- Source-to-source targets are untouched; no estate consumer churn from
1003+
this migration until S6, and even then only wasm-target consumers.
1004+
- This decision is settled; do not reopen without amending this ADR.
1005+
Ledger INT-03 in docs/TECH-DEBT.adoc; roadmap in docs/ECOSYSTEM.adoc.
1006+
"""
1007+
references = [
1008+
"https://github.com/hyperpolymath/affinescript/issues/180",
1009+
"https://github.com/hyperpolymath/affinescript/issues/160",
1010+
"https://github.com/hyperpolymath/affinescript/issues/225",
1011+
"wit/affinescript.wit",
1012+
"lib/wasi_runtime.ml",
1013+
"lib/codegen.ml / lib/codegen_node.ml (wasm target)",
1014+
"docs/specs/SETTLED-DECISIONS.adoc (ADR-015 section)",
1015+
"docs/ECOSYSTEM.adoc (AffineScript <-> typed-wasm contract; INT-03)",
1016+
"hyperpolymath/typed-wasm (multi-producer ownership-section ABI)",
1017+
"WASI 0.2 / WebAssembly Component Model specification",
1018+
]

docs/ECOSYSTEM.adoc

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,12 @@ link:TECH-DEBT.adoc[TECH-DEBT.adoc].
184184
landed in `packages/affine-js` (SAT-02 fixed; Deno/Node/browser parity,
185185
multi-namespace import object, ownership-section accessor). S1; unblocks
186186
INT-05/08/11. The `affinescript-dom-loader` satellite shell is downstream.
187-
|INT-03 |WASI preview2 / host I/O beyond stdout |#180 |open, S1
187+
|INT-03 |WASI preview2 / host I/O beyond stdout |#180 |S1, ADR-015
188+
ACCEPTED (owner-chosen full WASM Component-Model re-target). Staged
189+
S1..S6; legacy preview1 stdout path is the default until S6
190+
(reversible-in-progress). S3+ HARD-GATED on S2 toolchain provisioning
191+
(`wasm-tools`/`wasm-component-ld`, absent). WIT world of record:
192+
`wit/affinescript.wit`
188193
|INT-04 |Publish compiler + runtime to JSR (then npm) |#181 |open, S2
189194
(blocked by INT-01)
190195
|INT-05 |Loader-driven multi-module app bundling |ledger-only |planned

docs/TECH-DEBT.adoc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,9 @@ value call `Mod.fn(x)` hits a resolution error (post-#228 qualified-value
140140
resolution unwired) — own follow-up. |S1 |`use ::{}`/`::*` DONE (PR Refs
141141
#178); qualified-value-call resolver gap open #178
142142
|INT-02 |Host-agnostic loader bridge |S1 |open #179 (blocks INT-05/08/11)
143-
|INT-03 |WASI preview2 / host I/O |S1 |open #180
143+
|INT-03 |WASI preview2 / host I/O |S1 |#180 ADR-015 accepted (full
144+
Component-Model re-target, staged S1..S6); S3+ hard-gated on S2
145+
toolchain (`wasm-tools`/`wasm-component-ld`)
144146
|INT-04 |Publish to JSR/npm |S2 |open #181 (◄ INT-01)
145147
|INT-07 |`affinescript-tea` runtime |S2 |open #182 (◄ INT-01)
146148
|INT-08 |DOM reconciler |S2 |open #183 (◄ INT-02)

docs/specs/SETTLED-DECISIONS.adoc

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,3 +270,41 @@ zero consumer churn (genuine ReScript-surface residue is #229).
270270
This decision is settled; do not reopen without amending the ADR. Full
271271
ADR in `.machine_readable/6a2/META.a2ml` (ADR-014); ledger CORE-03 in
272272
`docs/TECH-DEBT.adoc`.
273+
274+
== WASI Preview2 / WASM Component-Model Migration (ADR-015)
275+
276+
`lib/wasi_runtime.ml` emits only `wasi_snapshot_preview1.fd_write` to
277+
stdout. No files, sockets, environment, clock, or argv — so there is no
278+
server-side runtime (INT-06 blocked) and no real host I/O. INT-03 (#180)
279+
is the substrate fix. The approach was an escalated one-way-door fork
280+
(2026-05-19 AskUserQuestion): (a) expand the preview1 surface, (b)
281+
preview1 + an external preview1→preview2 adapter, or (c) a full
282+
re-target to the WebAssembly Component Model with WASI 0.2. The owner
283+
chose *(c)* — explicitly the largest, one-way, highest-blast-radius path.
284+
285+
Decision: re-target the `wasm`/`wasm-gc` output to the WebAssembly
286+
Component Model with WASI 0.2 WIT worlds (`wit/affinescript.wit`, world
287+
`affinescript:cli/run`). *Staged and non-breaking per slice*; the legacy
288+
core-wasm + preview1 stdout path remains the default until the final
289+
slice, so the migration is reversible while in progress even though the
290+
end-state is one-way. The `affinescript.ownership` custom section is a
291+
*multi-producer ABI* (shared with `hyperpolymath/ephapax`; the typed-wasm
292+
contract carrier) and must survive verbatim onto the component's embedded
293+
core module — its format must not change here; any change is coordinated
294+
in `hyperpolymath/typed-wasm`, never made unilaterally for this
295+
migration. Only the wasm target re-points; the 22+ source-to-source
296+
targets are unaffected.
297+
298+
Staged plan (ledger INT-03; each row is one gated PR): *S1* (this) ADR +
299+
WIT world + plan + tooling prerequisite + roadmap truthing, no codegen
300+
change; *S2* toolchain provisioning (`wasm-tools`, `wasm-component-ld`,
301+
`wac`) — ABSENT today, a hard gate on S3+; *S3* componentize on-ramp
302+
(codegen still emits core wasm; post-step wraps it via the standard
303+
preview1→preview2 adapter; ownership-section survival asserted; wasmtime
304+
component-run smoke); *S4* native `wasi:clocks`/environment/argv; *S5*
305+
`wasi:filesystem` (unblocks INT-06); *S6* `wasi:sockets`, then flip the
306+
default wasm target to component and demote preview1 to a legacy target.
307+
308+
This decision is settled; do not reopen without amending the ADR. Full
309+
ADR in `.machine_readable/6a2/META.a2ml` (ADR-015); ledger INT-03 in
310+
`docs/TECH-DEBT.adoc`; roadmap in `docs/ECOSYSTEM.adoc`.

wit/affinescript.wit

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// SPDX-FileCopyrightText: 2024-2026 hyperpolymath
3+
//
4+
// AffineScript WASM Component-Model target world (ADR-015, INT-03 #180).
5+
//
6+
// This is the WIT world of record for the WASI 0.2 (preview2) migration.
7+
// The wasi:* package dependencies are resolved at build time once the
8+
// toolchain prerequisite (wasm-tools / wasm-component-ld) lands (ADR-015
9+
// slice S2). Until then this file is the *contract*, not yet wired into
10+
// codegen — the legacy wasi_snapshot_preview1 core-wasm path remains the
11+
// default per ADR-015 (staged, reversible-in-progress).
12+
13+
package affinescript:cli@0.1.0;
14+
15+
/// The command world an AffineScript program compiled to a WASM
16+
/// component targets. Mirrors `wasi:cli/command` so a compiled program
17+
/// runs under any conformant WASI 0.2 host (wasmtime, jco, …).
18+
///
19+
/// Slice rollout (ADR-015):
20+
/// S3 wasi:cli/run + the preview1->preview2 adapter on-ramp
21+
/// S4 wasi:clocks, environment, argv
22+
/// S5 wasi:filesystem (unblocks INT-06 server-side profile)
23+
/// S6 wasi:sockets, then this world becomes the default wasm target
24+
world run {
25+
// Standard input/output/error.
26+
import wasi:cli/stdin@0.2.0;
27+
import wasi:cli/stdout@0.2.0;
28+
import wasi:cli/stderr@0.2.0;
29+
30+
// Environment and argv (ADR-015 S4).
31+
import wasi:cli/environment@0.2.0;
32+
33+
// Wall-clock and monotonic clocks (ADR-015 S4).
34+
import wasi:clocks/wall-clock@0.2.0;
35+
import wasi:clocks/monotonic-clock@0.2.0;
36+
37+
// Filesystem — unblocks the server-side runtime profile, INT-06
38+
// (ADR-015 S5).
39+
import wasi:filesystem/types@0.2.0;
40+
import wasi:filesystem/preopens@0.2.0;
41+
42+
// Sockets — networking (ADR-015 S6).
43+
import wasi:sockets/instance-network@0.2.0;
44+
import wasi:sockets/tcp@0.2.0;
45+
import wasi:sockets/udp@0.2.0;
46+
47+
// The command entrypoint: the host invokes `run`, the guest returns
48+
// ok | err to become the process exit status. This is what AffineScript
49+
// `fn main()` lowers to once codegen re-targets (ADR-015 S3+).
50+
export wasi:cli/run@0.2.0;
51+
}

0 commit comments

Comments
 (0)