Skip to content

Commit bbe6ebc

Browse files
feat(componentize): #180 ADR-015 S3 — preview2 component on-ramp (Refs #180) (#288)
ADR-015 slice S3. Post-codegen, CODEGEN UNCHANGED (the compiler still emits a core wasi_snapshot_preview1 module — the legacy default; ADR-015 reversible-in-progress). - `tools/provision-component-toolchain.sh` (extends #251): also fetch-pins the official preview1->preview2 **reactor** adapter (wasmtime v44.0.1, sha256 e352bf5b…; verified fail-closed). Reactor, not command: AffineScript modules export functions (`main`, …), not a WASI `_start`, so the command adapter rejects them — proven empirically. Adapter is fetched to `tools/vendor/` (gitignored), not vendored as a blob. - `tools/componentize.sh CORE OUT`: `wasm-tools component new --adapt` → `wasm-tools validate --features component-model` → asserts the `affinescript.ownership` section (the typed-wasm multi-producer carrier) SURVIVES the wrap (it does — verified). Fail-closed. - `tests/componentize/smoke.sh`: compiles the L10-clean ownership fixture, componentizes, asserts valid WASI-0.2 component + section survival + wasmtime loads it. SKIPs cleanly when the toolchain is unprovisioned, so it never breaks the base gate (opt-in / the toolchain-bearing lane). Finding (load-bearing): `set -o pipefail` + `grep -q` on `wasm-tools print` is a SIGPIPE footgun (grep closes the pipe early → wasm-tools dies 141 → pipefail mis-reports the check as failed). Use `grep -c | … || true` (reads all input). The ownership section genuinely survives — the earlier "did NOT survive" was this footgun. Gate: `dune test --force` 295/295 (no compiler/source change — S3 is post-codegen tooling only; zero regression). S3 smoke PASSED. Refs #180 — ADR-015 S3 done; S4 (native wasi:clocks/env/argv) next; S5 filesystem (unblocks INT-06); S6 flip default. Not Closes.
1 parent 6c0e1b6 commit bbe6ebc

6 files changed

Lines changed: 158 additions & 8 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,6 @@ htmlcov/
9696
# Local-only build workaround (see file header); never committed.
9797
/dune-workspace
9898
packages/affinescript-cli/deno.lock
99+
100+
# ADR-015 S3: fetch-pinned WASI adapter (provisioned, not committed)
101+
tools/vendor/

docs/ECOSYSTEM.adoc

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -206,10 +206,15 @@ satellite shell is downstream.
206206
|INT-03 |WASI preview2 / host I/O beyond stdout |#180 |S1, ADR-015
207207
ACCEPTED (owner-chosen full WASM Component-Model re-target). Staged
208208
S1..S6; legacy preview1 stdout path is the default until S6
209-
(reversible-in-progress). **S2 toolchain provisioned (#251 closed):
210-
`tools/provision-component-toolchain.sh` — pinned wasm-tools 1.249.0 /
211-
wasm-component-ld 0.5.22 / wac-cli 0.10.0. S3 (componentize on-ramp)
212-
now UNBLOCKED.** WIT world of record: `wit/affinescript.wit`
209+
(reversible-in-progress). S2 toolchain provisioned (#251). **S3
210+
DONE: `tools/componentize.sh` wraps the emitted core module into a
211+
valid WASI-0.2 component via the fetch-pinned preview1→preview2
212+
*reactor* adapter (wasmtime v44.0.1, sha256-verified); the
213+
`affinescript.ownership` section is proven to SURVIVE the wrap;
214+
`tests/componentize/smoke.sh` gates it (SKIP-safe without the
215+
toolchain). Codegen UNCHANGED — core preview1 stays the default
216+
(reversible).** Next: S4 native clocks/env/argv. WIT world of
217+
record: `wit/affinescript.wit`
213218
|INT-04 |Publish compiler + runtime to JSR (then npm) |#181 |runtime
214219
packaging READY (affine-js + affinescript-tea JSR dry-run green;
215220
manual-only `publish-jsr.yml`; docs/PACKAGING.adoc). INT-01 dep

docs/TECH-DEBT.adoc

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -185,10 +185,12 @@ follow-up
185185
multi-ns import object, ownership accessor); 14 unit tests via pinned
186186
`deno task test` + `tests/modules/loader-bridge/` e2e on real
187187
compiler-emitted xmod wasm (closes INT-01↔INT-02). Unblocks INT-05/08/11
188-
|INT-03 |WASI preview2 / host I/O |S1→S2 |#180 ADR-015 (full
189-
Component-Model re-target, S1..S6); **S2 toolchain provisioned —
190-
#251 closed (`tools/provision-component-toolchain.sh`, pinned
191-
wasm-tools/wasm-component-ld/wac); S3 componentize on-ramp unblocked**
188+
|INT-03 |WASI preview2 / host I/O |S1→S3 |#180 ADR-015 (full
189+
Component-Model re-target, S1..S6); S2 toolchain #251 closed;
190+
**S3 DONE — `tools/componentize.sh` (pinned reactor adapter) →
191+
valid WASI-0.2 component, ownership section survives,
192+
`tests/componentize/smoke.sh` gates it; codegen unchanged. Next S4
193+
(native clocks/env/argv)**
192194
|INT-04 |Publish to JSR/npm |S2 |#181 packaging READY (dry-run green,
193195
manual workflow); JSR publish authorised + dispatched (owner go
194196
2026-05-19); compiler-binary distribution decided = **ADR-019**

tests/componentize/smoke.sh

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#!/usr/bin/env bash
2+
# SPDX-License-Identifier: PMPL-1.0-or-later
3+
#
4+
# ADR-015 S3 gated smoke: compile a core module that carries the
5+
# `affinescript.ownership` section, run it through the componentize
6+
# on-ramp, and assert the result is a valid WASI-0.2 component with the
7+
# ownership section intact (and that wasmtime can load it).
8+
#
9+
# SKIPs cleanly (exit 0) when the component toolchain is not provisioned
10+
# — it is opt-in (run tools/provision-component-toolchain.sh first /
11+
# the toolchain-bearing CI lane), so it never breaks the base gate.
12+
set -euo pipefail
13+
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
14+
cd "$ROOT"
15+
16+
if ! command -v wasm-tools >/dev/null \
17+
|| [ ! -f tools/vendor/wasi_snapshot_preview1.reactor.wasm ]; then
18+
echo "SKIP: component toolchain not provisioned (tools/provision-component-toolchain.sh)"
19+
exit 0
20+
fi
21+
22+
COMPILER="${AFFINESCRIPT:-$ROOT/_build/default/bin/main.exe}"
23+
[ -x "$COMPILER" ] || COMPILER="dune exec affinescript --"
24+
25+
work="$(mktemp -d)"
26+
trap 'rm -rf "$work"' EXIT
27+
28+
# Known-good ownership fixture (emits the affinescript.ownership
29+
# section) + a main so the module is non-trivial.
30+
cp test/e2e/fixtures/verify_ownership_clean.affine "$work/cz.affine"
31+
printf '\nfn main() -> Int { add(2, 3) }\n' >> "$work/cz.affine"
32+
33+
# `grep -c | … || true`: read all input (no early-close SIGPIPE that
34+
# `set -o pipefail` would mis-report — the `grep -q` footgun).
35+
has_section() {
36+
[ "$(wasm-tools print "$1" 2>/dev/null | grep -c 'affinescript.ownership' || true)" -gt 0 ]
37+
}
38+
39+
$COMPILER compile "$work/cz.affine" -o "$work/cz.wasm"
40+
has_section "$work/cz.wasm" \
41+
|| { echo "FAIL: fixture did not emit the ownership section"; exit 1; }
42+
43+
tools/componentize.sh "$work/cz.wasm" "$work/cz.component.wasm"
44+
wasm-tools validate --features component-model "$work/cz.component.wasm"
45+
has_section "$work/cz.component.wasm" \
46+
|| { echo "FAIL: ownership section lost through componentization"; exit 1; }
47+
48+
if command -v wasmtime >/dev/null; then
49+
# Reactor component: no command entrypoint, so a successful
50+
# instantiate (no trap) is the smoke. `--invoke` a missing export
51+
# would error; instead just validate it loads as a component.
52+
wasmtime compile "$work/cz.component.wasm" -o "$work/cz.cwasm" >/dev/null 2>&1 \
53+
&& echo "wasmtime: component compiles/loads ✓" \
54+
|| { echo "FAIL: wasmtime could not compile the component"; exit 1; }
55+
fi
56+
57+
echo "ADR-015 S3 componentize smoke: PASSED ✓"

tools/componentize.sh

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#!/usr/bin/env bash
2+
# SPDX-License-Identifier: PMPL-1.0-or-later
3+
#
4+
# ADR-015 S3 — the componentize on-ramp. POST-codegen, codegen
5+
# UNCHANGED: the compiler still emits a core `wasi_snapshot_preview1`
6+
# module (the legacy default, ADR-015 reversible-in-progress); this
7+
# wraps that core module into a WASI-0.2 (preview2) WASM *component*
8+
# via the official preview1->preview2 reactor adapter (reactor, not
9+
# command: AffineScript modules export functions, not a WASI `_start`).
10+
#
11+
# The `affinescript.ownership` custom section (the typed-wasm contract
12+
# carrier, multi-producer ABI) MUST survive the wrap — asserted here.
13+
#
14+
# Usage: tools/componentize.sh <core.wasm> <out.component.wasm>
15+
# Adapter: tools/vendor/wasi_snapshot_preview1.reactor.wasm
16+
# (fetch-pinned + checksum-verified by provision-component-toolchain.sh;
17+
# override with AFFINESCRIPT_WASI_ADAPTER=/path).
18+
set -euo pipefail
19+
20+
core="${1:?usage: componentize.sh <core.wasm> <out.component.wasm>}"
21+
out="${2:?usage: componentize.sh <core.wasm> <out.component.wasm>}"
22+
adapter="${AFFINESCRIPT_WASI_ADAPTER:-$(cd "$(dirname "$0")" && pwd)/vendor/wasi_snapshot_preview1.reactor.wasm}"
23+
24+
[ -f "$core" ] || { echo "componentize: no core module: $core" >&2; exit 2; }
25+
[ -f "$adapter" ] || { echo "componentize: adapter missing ($adapter) — run tools/provision-component-toolchain.sh" >&2; exit 2; }
26+
command -v wasm-tools >/dev/null || { echo "componentize: wasm-tools not on PATH — run tools/provision-component-toolchain.sh" >&2; exit 2; }
27+
28+
# Count with `grep -c` (reads ALL input, so no early-close SIGPIPE that
29+
# `set -o pipefail` would mis-report as a failure — the `grep -q`
30+
# footgun). `|| true` neutralises grep's no-match exit 1.
31+
section_count() {
32+
wasm-tools print "$1" 2>/dev/null | grep -c 'affinescript.ownership' || true
33+
}
34+
35+
# Does the core carry the typed-wasm ownership section? (only assert
36+
# survival if it was there to begin with.)
37+
had_ownership=0
38+
[ "$(section_count "$core")" -gt 0 ] && had_ownership=1
39+
40+
wasm-tools component new "$core" --adapt "wasi_snapshot_preview1=$adapter" -o "$out"
41+
wasm-tools validate --features component-model "$out"
42+
43+
if [ "$had_ownership" = 1 ]; then
44+
if [ "$(section_count "$out")" -eq 0 ]; then
45+
echo "FATAL: affinescript.ownership section did NOT survive componentization" >&2
46+
rm -f "$out"
47+
exit 1
48+
fi
49+
echo "ownership section: preserved through componentization ✓"
50+
fi
51+
52+
echo "componentized -> $out (valid WASI-0.2 component)"

tools/provision-component-toolchain.sh

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,45 @@ WASM_TOOLS_VERSION="1.249.0"
2121
WASM_COMPONENT_LD_VERSION="0.5.22"
2222
WAC_CLI_VERSION="0.10.0"
2323

24+
# The official `wasi_snapshot_preview1` -> preview2 *reactor* adapter
25+
# (ADR-015 S3). AffineScript modules export functions (`main`, …), not
26+
# a WASI command `_start`, so the REACTOR adapter is the correct one
27+
# (the command adapter requires `_start`). Fetch-pinned (provenance +
28+
# sha256 recorded here; verified fail-closed) rather than vendored as a
29+
# binary blob in the source tree. Source: bytecodealliance/wasmtime
30+
# release v44.0.1 (matches the pinned wasmtime in this estate).
31+
ADAPTER_WASMTIME_TAG="v44.0.1"
32+
ADAPTER_URL="https://github.com/bytecodealliance/wasmtime/releases/download/${ADAPTER_WASMTIME_TAG}/wasi_snapshot_preview1.reactor.wasm"
33+
ADAPTER_SHA256="e352bf5b74aec62d8e7da7e0536ede4b3d1ccdb4d9a1767031f4d6f007d22e85"
34+
ADAPTER_DIR="$(cd "$(dirname "$0")" && pwd)/vendor"
35+
ADAPTER_PATH="${ADAPTER_DIR}/wasi_snapshot_preview1.reactor.wasm"
36+
2437
echo "Provisioning component-model toolchain (pinned, --locked)…"
2538
cargo install --locked --version "${WASM_TOOLS_VERSION}" wasm-tools
2639
cargo install --locked --version "${WASM_COMPONENT_LD_VERSION}" wasm-component-ld
2740
cargo install --locked --version "${WAC_CLI_VERSION}" wac-cli
2841

42+
echo "Fetch-pinning the preview1->preview2 reactor adapter…"
43+
mkdir -p "${ADAPTER_DIR}"
44+
if [ -f "${ADAPTER_PATH}" ] && \
45+
[ "$(sha256sum "${ADAPTER_PATH}" | cut -d' ' -f1)" = "${ADAPTER_SHA256}" ]; then
46+
echo "adapter already present and verified"
47+
else
48+
curl -fsSL -o "${ADAPTER_PATH}.tmp" "${ADAPTER_URL}"
49+
got="$(sha256sum "${ADAPTER_PATH}.tmp" | cut -d' ' -f1)"
50+
if [ "${got}" != "${ADAPTER_SHA256}" ]; then
51+
rm -f "${ADAPTER_PATH}.tmp"
52+
echo "FATAL: adapter checksum mismatch (expected ${ADAPTER_SHA256}, got ${got})" >&2
53+
exit 1
54+
fi
55+
mv "${ADAPTER_PATH}.tmp" "${ADAPTER_PATH}"
56+
echo "adapter verified + pinned -> ${ADAPTER_PATH}"
57+
fi
58+
2959
echo
3060
echo "Installed:"
3161
wasm-tools --version
3262
wasm-component-ld --version
3363
wac --version
64+
echo "adapter: ${ADAPTER_WASMTIME_TAG} (${ADAPTER_SHA256})"
3465
echo "OK — ADR-015 S3 (componentize on-ramp) is now unblocked."

0 commit comments

Comments
 (0)