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
78 changes: 78 additions & 0 deletions .machine_readable/6a2/META.a2ml
Original file line number Diff line number Diff line change
Expand Up @@ -938,3 +938,81 @@ references = [
"META.a2ml [[adr]] ADR-011 (real modules / qualified paths)",
"META.a2ml [[adr]] ADR-012 (grammar changes are correctness assertions)",
]

[[adr]]
id = "ADR-015"
status = "accepted"
date = "2026-05-19"
title = "WASI Preview2 / WASM Component-Model migration (INT-03)"
context = """
lib/wasi_runtime.ml emits only `wasi_snapshot_preview1.fd_write` to
stdout (fd 1). There is no file, socket, environment, clock, or argv
access, so there is no server-side runtime profile (INT-06 is blocked)
and no real host I/O. INT-03 (#180) is the substrate fix. The owner was
asked (2026-05-19, AskUserQuestion one-way-door fork) to choose between
(a) expanding the preview1 import surface, (b) preview1 + an external
preview1->preview2 adapter as the run path, or (c) a full re-target to
the WebAssembly Component Model with WASI 0.2 (preview2). The owner
chose (c) — explicitly the largest, one-way, highest-blast-radius path.
"""
decision = """
Re-target the WASM output to the WebAssembly Component Model with WASI
0.2 (preview2) WIT worlds. Binding constraints:

- STAGED, non-breaking per slice; each slice is its own PR behind the
full gate. The legacy core-wasm + preview1 stdout path stays the
DEFAULT until the final slice flips it, so the migration is
reversible while in progress even though the end-state is one-way.
- The `affinescript.ownership` custom section is a MULTI-PRODUCER ABI
(shared with hyperpolymath/ephapax; the typed-wasm contract carrier,
see docs/ECOSYSTEM.adoc). It MUST survive verbatim onto the
component's embedded core module. Its format MUST NOT change here;
any change is coordinated in hyperpolymath/typed-wasm, never made
unilaterally for this migration.
- Only the `wasm`/`wasm-gc` target re-points. The 22+ source-to-source
targets (Deno-ESM, Node-CJS, Julia, …) are unaffected.
- WIT world of record: `wit/affinescript.wit` — world
`affinescript:cli/run` importing the `wasi:cli`, `wasi:clocks`,
`wasi:filesystem`, and `wasi:sockets` 0.2 interfaces.

Staged plan (ledger INT-03; each row = one gated PR):
- S1 (this PR): ADR-015 + WIT world + staged plan + tooling-prerequisite
+ roadmap truthing. No codegen change.
- S2: toolchain provisioning — `wasm-tools` + `wasm-component-ld` (and
`wac`) into guix.scm/flake.nix. ABSENT in the current toolchain ⇒ a
HARD GATE on S3+: a component cannot be built or tested without them.
- S3: componentize on-ramp — codegen still emits core wasm; a
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.
- S5: `wasi:filesystem` (open/read/write/close) — unblocks INT-06.
- S6: `wasi:sockets`; then flip the default wasm target to component
and demote the preview1 stdout path to a named legacy target.
"""
consequences = """
- End-state is one-way (the component model becomes the canonical wasm
target) but reversible-in-progress: preview1 retained through S5.
- 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
obligation with hyperpolymath/ephapax + hyperpolymath/typed-wasm is
recorded and binding.
- Source-to-source targets are untouched; no estate consumer churn from
this migration until S6, and even then only wasm-target consumers.
- This decision is settled; do not reopen without amending this ADR.
Ledger INT-03 in docs/TECH-DEBT.adoc; roadmap in docs/ECOSYSTEM.adoc.
"""
references = [
"https://github.com/hyperpolymath/affinescript/issues/180",
"https://github.com/hyperpolymath/affinescript/issues/160",
"https://github.com/hyperpolymath/affinescript/issues/225",
"wit/affinescript.wit",
"lib/wasi_runtime.ml",
"lib/codegen.ml / lib/codegen_node.ml (wasm target)",
"docs/specs/SETTLED-DECISIONS.adoc (ADR-015 section)",
"docs/ECOSYSTEM.adoc (AffineScript <-> typed-wasm contract; INT-03)",
"hyperpolymath/typed-wasm (multi-producer ownership-section ABI)",
"WASI 0.2 / WebAssembly Component Model specification",
]
7 changes: 6 additions & 1 deletion docs/ECOSYSTEM.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,12 @@ link:TECH-DEBT.adoc[TECH-DEBT.adoc].
landed in `packages/affine-js` (SAT-02 fixed; Deno/Node/browser parity,
multi-namespace import object, ownership-section accessor). S1; unblocks
INT-05/08/11. The `affinescript-dom-loader` satellite shell is downstream.
|INT-03 |WASI preview2 / host I/O beyond stdout |#180 |open, S1
|INT-03 |WASI preview2 / host I/O beyond stdout |#180 |S1, ADR-015
ACCEPTED (owner-chosen full WASM Component-Model re-target). Staged
S1..S6; legacy preview1 stdout path is the default until S6
(reversible-in-progress). S3+ HARD-GATED on S2 toolchain provisioning
(`wasm-tools`/`wasm-component-ld`, absent). WIT world of record:
`wit/affinescript.wit`
|INT-04 |Publish compiler + runtime to JSR (then npm) |#181 |open, S2
(blocked by INT-01)
|INT-05 |Loader-driven multi-module app bundling |ledger-only |planned
Expand Down
4 changes: 3 additions & 1 deletion docs/TECH-DEBT.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,9 @@ value call `Mod.fn(x)` hits a resolution error (post-#228 qualified-value
resolution unwired) — own follow-up. |S1 |`use ::{}`/`::*` DONE (PR Refs
#178); qualified-value-call resolver gap open #178
|INT-02 |Host-agnostic loader bridge |S1 |open #179 (blocks INT-05/08/11)
|INT-03 |WASI preview2 / host I/O |S1 |open #180
|INT-03 |WASI preview2 / host I/O |S1 |#180 ADR-015 accepted (full
Component-Model re-target, staged S1..S6); S3+ hard-gated on S2
toolchain (`wasm-tools`/`wasm-component-ld`)
|INT-04 |Publish to JSR/npm |S2 |open #181 (◄ INT-01)
|INT-07 |`affinescript-tea` runtime |S2 |open #182 (◄ INT-01)
|INT-08 |DOM reconciler |S2 |open #183 (◄ INT-02)
Expand Down
38 changes: 38 additions & 0 deletions docs/specs/SETTLED-DECISIONS.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -270,3 +270,41 @@ zero consumer churn (genuine ReScript-surface residue is #229).
This decision is settled; do not reopen without amending the ADR. Full
ADR in `.machine_readable/6a2/META.a2ml` (ADR-014); ledger CORE-03 in
`docs/TECH-DEBT.adoc`.

== WASI Preview2 / WASM Component-Model Migration (ADR-015)

`lib/wasi_runtime.ml` emits only `wasi_snapshot_preview1.fd_write` to
stdout. No files, sockets, environment, clock, or argv — so there is no
server-side runtime (INT-06 blocked) and no real host I/O. INT-03 (#180)
is the substrate fix. The approach was an escalated one-way-door fork
(2026-05-19 AskUserQuestion): (a) expand the preview1 surface, (b)
preview1 + an external preview1→preview2 adapter, or (c) a full
re-target to the WebAssembly Component Model with WASI 0.2. The owner
chose *(c)* — explicitly the largest, one-way, highest-blast-radius path.

Decision: re-target the `wasm`/`wasm-gc` output to the WebAssembly
Component Model with WASI 0.2 WIT worlds (`wit/affinescript.wit`, world
`affinescript:cli/run`). *Staged and non-breaking per slice*; the legacy
core-wasm + preview1 stdout path remains the default until the final
slice, so the migration is reversible while in progress even though the
end-state is one-way. The `affinescript.ownership` custom section is a
*multi-producer ABI* (shared with `hyperpolymath/ephapax`; the typed-wasm
contract carrier) and must survive verbatim onto the component's embedded
core module — its format must not change here; any change is coordinated
in `hyperpolymath/typed-wasm`, never made unilaterally for this
migration. Only the wasm target re-points; the 22+ source-to-source
targets are unaffected.

Staged plan (ledger INT-03; each row is one gated PR): *S1* (this) ADR +
WIT world + plan + tooling prerequisite + roadmap truthing, no codegen
change; *S2* toolchain provisioning (`wasm-tools`, `wasm-component-ld`,
`wac`) — ABSENT today, a hard gate on S3+; *S3* componentize on-ramp
(codegen still emits core wasm; post-step wraps it via the standard
preview1→preview2 adapter; ownership-section survival asserted; wasmtime
component-run smoke); *S4* native `wasi:clocks`/environment/argv; *S5*
`wasi:filesystem` (unblocks INT-06); *S6* `wasi:sockets`, then flip the
default wasm target to component and demote preview1 to a legacy target.

This decision is settled; do not reopen without amending the ADR. Full
ADR in `.machine_readable/6a2/META.a2ml` (ADR-015); ledger INT-03 in
`docs/TECH-DEBT.adoc`; roadmap in `docs/ECOSYSTEM.adoc`.
51 changes: 51 additions & 0 deletions wit/affinescript.wit
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// SPDX-License-Identifier: PMPL-1.0-or-later
// SPDX-FileCopyrightText: 2024-2026 hyperpolymath
//
// AffineScript WASM Component-Model target world (ADR-015, INT-03 #180).
//
// This is the WIT world of record for the WASI 0.2 (preview2) migration.
// The wasi:* package dependencies are resolved at build time once the
// toolchain prerequisite (wasm-tools / wasm-component-ld) lands (ADR-015
// slice S2). Until then this file is the *contract*, not yet wired into
// codegen — the legacy wasi_snapshot_preview1 core-wasm path remains the
// default per ADR-015 (staged, reversible-in-progress).

package affinescript:cli@0.1.0;

/// The command world an AffineScript program compiled to a WASM
/// component targets. Mirrors `wasi:cli/command` so a compiled program
/// runs under any conformant WASI 0.2 host (wasmtime, jco, …).
///
/// Slice rollout (ADR-015):
/// S3 wasi:cli/run + the preview1->preview2 adapter on-ramp
/// S4 wasi:clocks, environment, argv
/// S5 wasi:filesystem (unblocks INT-06 server-side profile)
/// S6 wasi:sockets, then this world becomes the default wasm target
world run {
// Standard input/output/error.
import wasi:cli/stdin@0.2.0;
import wasi:cli/stdout@0.2.0;
import wasi:cli/stderr@0.2.0;

// Environment and argv (ADR-015 S4).
import wasi:cli/environment@0.2.0;

// Wall-clock and monotonic clocks (ADR-015 S4).
import wasi:clocks/wall-clock@0.2.0;
import wasi:clocks/monotonic-clock@0.2.0;

// Filesystem — unblocks the server-side runtime profile, INT-06
// (ADR-015 S5).
import wasi:filesystem/types@0.2.0;
import wasi:filesystem/preopens@0.2.0;

// Sockets — networking (ADR-015 S6).
import wasi:sockets/instance-network@0.2.0;
import wasi:sockets/tcp@0.2.0;
import wasi:sockets/udp@0.2.0;

// The command entrypoint: the host invokes `run`, the guest returns
// ok | err to become the process exit status. This is what AffineScript
// `fn main()` lowers to once codegen re-targets (ADR-015 S3+).
export wasi:cli/run@0.2.0;
}
Loading