From c17de6e6a897012e74b4f4f27f6d0ab864d08100 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 24 May 2026 04:39:23 +0000 Subject: [PATCH] =?UTF-8?q?feat(wasi):=20#180=20ADR-015=20S6a=20=E2=80=94?= =?UTF-8?q?=20WIT=20export=20lifting=20(real-host=20main-invoke)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lift a parameter-less `fn main()` into the WASI 0.2 (preview2) command shape so `wasmtime run ` actually runs an AffineScript program end-to-end, closing the "real-host main-invoke" gap left open by S3 (reactor-only componentize) and S4a/b (preview1 import bridging). Codegen (lib/codegen.ml): - After exports_with_mem is assembled, when `main` is exported and takes no parameters, append a `_start : () -> ()` shim whose body is `Call main_idx; Drop` (drops main's i32 result — current ABI is uniformly i32-returning even for `() -> ()`). Skips silently if a user already exports `_start` or `main` has parameters. - Purely additive: every existing consumer (reactor componentize, game-loop hosts, `__indirect_function_table` for closures) sees the same exports plus an extra `_start`. Lambda-table elem indices are unaffected (start_func is appended after lambdas in all_funcs). Tooling: - tools/provision-component-toolchain.sh now fetches both the reactor and command adapters (wasmtime v44.0.1), each sha256-pinned and fail-closed. Back-compat: the S3-era `ADAPTER_URL`/`_SHA256`/`_PATH` variables remain as reactor-aliased exports. - tools/componentize.sh learns `--command` / `--reactor` modes; `--command` selects the command adapter, fails fast with a pointer to the codegen contract if the core is missing `_start`, and asserts `wasi:cli/run` is exported in the resulting component. Gate: - tests/componentize/command_smoke.sh — opt-in, SKIPs cleanly without the toolchain or wasmtime; otherwise compiles a fixture, asserts the `_start` shim, runs the command componentize path, asserts the ownership section survives + `wasi:cli/run` is lifted, and runs the component under `wasmtime run` (exit 0 is the contract). Verified end-to-end on this branch: `wasmtime run` succeeds on a real fresh-compiled component (`fn consume(x) { x }` / `fn main() { consume(42) }`), the existing S3 reactor smoke still passes, and the full OCaml test suite stays green (295 tests). Docs: - TECH-DEBT.adoc / ECOSYSTEM.adoc INT-03 rows: S6 split into S6a (WIT export lifting, DONE) + S6b (sockets) + S6c (default-flip). - META.a2ml ADR-015 decision text mirrors the split. - SETTLED-DECISIONS.adoc mirror updated in lockstep. WIT world (`wit/affinescript.wit`) unchanged — already the contract of record; the command-mode output exports a subset (`wasi:cli/run` plus the adapter's required imports). Sockets + default-target flip remain in scope for S6b/S6c per the ADR. Refs #180 --- .machine_readable/6a2/META.a2ml | 16 ++++- docs/ECOSYSTEM.adoc | 30 ++++++---- docs/TECH-DEBT.adoc | 19 ++++-- docs/specs/SETTLED-DECISIONS.adoc | 8 ++- lib/codegen.ml | 56 +++++++++++++++++- tests/componentize/command_smoke.sh | 77 ++++++++++++++++++++++++ tools/componentize.sh | 80 ++++++++++++++++++++----- tools/provision-component-toolchain.sh | 82 ++++++++++++++++++-------- 8 files changed, 304 insertions(+), 64 deletions(-) create mode 100755 tests/componentize/command_smoke.sh diff --git a/.machine_readable/6a2/META.a2ml b/.machine_readable/6a2/META.a2ml index d5d5db05..e463af67 100644 --- a/.machine_readable/6a2/META.a2ml +++ b/.machine_readable/6a2/META.a2ml @@ -988,8 +988,20 @@ Staged plan (ledger INT-03; each row = one gated PR): - 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. +- S6a (WIT export lifting, DONE): codegen emits a `_start : () -> ()` + shim that calls `main` and drops its i32 result whenever a + parameter-less `fn main()` is present (`lib/codegen.ml`); + `tools/componentize.sh --command` wraps with the fetch-pinned + preview1->preview2 *command* adapter (wasmtime v44.0.1, sha256 + 8ff2ea78...) producing a component that exports `wasi:cli/run@0.2.x` + per `wit/affinescript.wit`. End-to-end gate: + `tests/componentize/command_smoke.sh` proves `wasmtime run` invokes + 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`. +- S6c: 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 diff --git a/docs/ECOSYSTEM.adoc b/docs/ECOSYSTEM.adoc index bd70c715..0b79f023 100644 --- a/docs/ECOSYSTEM.adoc +++ b/docs/ECOSYSTEM.adoc @@ -205,25 +205,35 @@ INT-01 ↔ INT-02. S1; unblocks INT-05/08/11. The `affinescript-dom-loader` satellite shell is downstream. |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). S2 toolchain provisioned (#251). **S3 +S1..S6; legacy preview1 stdout path remains the default until S6c +(reversible-in-progress). S2 toolchain provisioned (#251). S3 DONE: `tools/componentize.sh` wraps the emitted core module into a valid WASI-0.2 component via the fetch-pinned preview1→preview2 *reactor* adapter (wasmtime v44.0.1, sha256-verified); the `affinescript.ownership` section is proven to SURVIVE the wrap; `tests/componentize/smoke.sh` gates it (SKIP-safe without the -toolchain). Codegen UNCHANGED — core preview1 stays the default -(reversible). S4a (clock) + S4b (env_count, arg_count) DONE: +toolchain). S4a (clock) + S4b (env_count, arg_count) DONE: builtins lower to on-demand `wasi_snapshot_preview1.*` imports (Effect_sites pre-scan, canonical-order indexing through `ctx.wasi_func_indices`; zero impact on units that don't use them; verified with a multi-import combo regression). Component path -bridges to `wasi:clocks`/`wasi:cli`. Real-host main-invoke deferred -to S6 (WIT export-lifting / wasi:cli/run command shape). 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.** WIT world of -record: `wit/affinescript.wit` +bridges to `wasi:clocks`/`wasi:cli`. **S6a (WIT export lifting) +DONE: `lib/codegen.ml` auto-emits a `_start : () -> ()` shim that +calls `main` and drops its i32 result whenever a parameter-less +`fn main()` is present. `tools/componentize.sh --command` wraps +with the fetch-pinned preview1→preview2 *command* adapter +(wasmtime v44.0.1, sha256 8ff2ea78…) — the resulting component +exports `wasi:cli/run@0.2.x` per `wit/affinescript.wit`. End-to-end +smoke `tests/componentize/command_smoke.sh` proves the lift, the +ownership section survives, and `wasmtime run ` 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-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` |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 diff --git a/docs/TECH-DEBT.adoc b/docs/TECH-DEBT.adoc index eb01f0c9..e7b6cf84 100644 --- a/docs/TECH-DEBT.adoc +++ b/docs/TECH-DEBT.adoc @@ -192,16 +192,25 @@ follow-up multi-ns import object, ownership accessor); 14 unit tests via pinned `deno task test` + `tests/modules/loader-bridge/` e2e on real compiler-emitted xmod wasm (closes INT-01↔INT-02). Unblocks INT-05/08/11 -|INT-03 |WASI preview2 / host I/O |S1→S4b |#180 ADR-015 (full +|INT-03 |WASI preview2 / host I/O |S1→S6a |#180 ADR-015 (full Component-Model re-target, S1..S6); S2 toolchain #251 closed; -S3 componentize done; **S4a (clock) + S4b (env_count, arg_count) +S3 componentize done; S4a (clock) + S4b (env_count, arg_count) DONE — on-demand preview1 imports via Effect_sites pre-scan, canonical-order indexing through `ctx.wasi_func_indices`; combo regression proves no collision. String accessors (env_at/arg_at) gated on byte-level wasm IR (I32Load8U/I32Store8 absent today) — -tracked next slice. Real-host main-invoke = S6 (WIT export -lifting). Next S5 -(native clocks/env/argv)** +tracked next slice. **S6a (WIT export lifting) DONE: +`lib/codegen.ml` auto-emits a `_start : () -> ()` shim whenever +a parameter-less `fn main()` is present (additive, no regression +on reactor consumers); `tools/componentize.sh --command` wraps +with the fetch-pinned command adapter (sha256 +8ff2ea78… / wasmtime v44.0.1, provisioned alongside the reactor +adapter); `tests/componentize/command_smoke.sh` gates the +contract end-to-end — `wasi:cli/run` lifted, ownership section +survives, `wasmtime run` exits 0. Real-host main-invoke now +works via `wasmtime run `.** Remaining S6 +sub-slices: S5 (native clocks/env/argv), S6b (`wasi:sockets`), +S6c (flip the default wasm target from preview1 → component) |INT-04 |Publish to JSR/npm |S2 |#181 packaging READY (dry-run green, manual workflow); compiler-binary distribution decided = **ADR-019** (#260, Releases + thin Deno/JSR shim, staged S1..S4) — S1/S2/S3 diff --git a/docs/specs/SETTLED-DECISIONS.adoc b/docs/specs/SETTLED-DECISIONS.adoc index f117fd6a..833936ca 100644 --- a/docs/specs/SETTLED-DECISIONS.adoc +++ b/docs/specs/SETTLED-DECISIONS.adoc @@ -302,8 +302,12 @@ change; *S2* toolchain provisioning (`wasm-tools`, `wasm-component-ld`, (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. +`wasi:filesystem` (unblocks INT-06); *S6a* (WIT export lifting) codegen +auto-emits a `_start : () -> ()` shim around a parameter-less `fn main()` +so `tools/componentize.sh --command` (preview1→preview2 *command* +adapter) produces a real `wasi:cli/run`-exporting component that any +WASI 0.2 host can invoke; *S6b* `wasi:sockets`; *S6c* 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 diff --git a/lib/codegen.ml b/lib/codegen.ml index 35f6959d..220af595 100644 --- a/lib/codegen.ml +++ b/lib/codegen.ml @@ -2626,6 +2626,56 @@ let generate_module ?loader (prog : program) : wasm_module result = @ table_export in + (* ADR-015 S6 — WIT export lifting. When the unit exports a parameter- + less `main`, also emit a `_start : () -> ()` shim that calls it and + drops the i32 result. With `_start` present, tools/componentize.sh + --command can wrap the core module with the WASI preview1→preview2 + *command* adapter, producing a real `wasi:cli/run`-exporting + component that any WASI 0.2 host (`wasmtime run`, jco) can invoke. + Purely additive: every existing consumer (reactor componentize, + game-loop hosts that call `main` directly) sees the same exports + plus an extra `_start`; main-with-params and units that already + export `_start` are skipped. *) + let final_types, final_funcs, final_exports = + let has_user_start = + List.exists (fun e -> e.e_name = "_start") exports_with_mem + in + let main_export = + List.find_opt (fun e -> e.e_name = "main") exports_with_mem + in + match main_export, has_user_start with + | Some { e_desc = ExportFunc main_idx; _ }, false -> + let main_local_idx = main_idx - import_offset in + if main_local_idx < 0 || main_local_idx >= List.length all_funcs then + (ctx'.types, all_funcs, exports_with_mem) + else + let main_func = List.nth all_funcs main_local_idx in + let main_type = List.nth ctx'.types main_func.f_type in + if main_type.ft_params <> [] then + (ctx'.types, all_funcs, exports_with_mem) + else + let void_ft = { ft_params = []; ft_results = [] } in + let (void_type_idx, types_after) = + intern_func_type ctx'.types void_ft + in + let drop_instrs = + List.map (fun _ -> Drop) main_type.ft_results + in + let start_func = { + f_type = void_type_idx; + f_locals = []; + f_body = Call main_idx :: drop_instrs; + } in + let funcs_after = all_funcs @ [start_func] in + let start_idx = import_offset + List.length all_funcs in + let exports_after = + exports_with_mem + @ [{ e_name = "_start"; e_desc = ExportFunc start_idx }] + in + (types_after, funcs_after, exports_after) + | _ -> (ctx'.types, all_funcs, exports_with_mem) + in + (* Stage 2: Build [affinescript.ownership] custom section from collected annotations *) let ownership_payload = build_ownership_section ctx'.ownership_annots in let custom_sections = if Bytes.length ownership_payload > 0 then @@ -2635,12 +2685,12 @@ let generate_module ?loader (prog : program) : wasm_module result = in Ok { - types = ctx'.types; - funcs = all_funcs; + types = final_types; + funcs = final_funcs; tables = tables; mems = [{ mem_type = { lim_min = 1; lim_max = None } }]; (* 1 page default *) globals = ctx'.globals; - exports = exports_with_mem; + exports = final_exports; imports = ctx'.imports; elems = elems; datas = List.rev ctx'.datas; (* Reverse to get original order *) diff --git a/tests/componentize/command_smoke.sh b/tests/componentize/command_smoke.sh new file mode 100755 index 00000000..515f7b3d --- /dev/null +++ b/tests/componentize/command_smoke.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# +# ADR-015 S6 gated smoke (WIT export lifting): compile an AffineScript +# program with `fn main()`, run it through `tools/componentize.sh +# --command`, and assert +# (a) the resulting component is a valid WASI-0.2 component, +# (b) it exports `wasi:cli/run@0.2.x`, +# (c) `wasmtime run` can invoke it (exit 0), and +# (d) the `affinescript.ownership` custom section survives the wrap. +# +# SKIPs cleanly (exit 0) when the component toolchain or wasmtime is +# not provisioned — opt-in, mirroring tests/componentize/smoke.sh. +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +cd "$ROOT" + +if ! command -v wasm-tools >/dev/null \ + || [ ! -f tools/vendor/wasi_snapshot_preview1.command.wasm ]; then + echo "SKIP: component toolchain / command adapter not provisioned (tools/provision-component-toolchain.sh)" + exit 0 +fi +if ! command -v wasmtime >/dev/null; then + echo "SKIP: wasmtime not on PATH (real-host run is the S6 contract)" + exit 0 +fi + +COMPILER="${AFFINESCRIPT:-$ROOT/_build/default/bin/main.exe}" +[ -x "$COMPILER" ] || COMPILER="dune exec affinescript --" + +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT + +# Fixture: ownership-bearing imports + a parameter-less `main` so the +# S6 codegen emits `_start` (the command adapter's lift target). The +# fn main() return value is irrelevant — the shim drops it. +cp test/e2e/fixtures/verify_ownership_clean.affine "$work/cmd.affine" +printf '\nfn main() -> Int { add(2, 3) }\n' >> "$work/cmd.affine" + +has_section() { + [ "$(wasm-tools print "$1" 2>/dev/null | grep -c 'affinescript.ownership' || true)" -gt 0 ] +} + +$COMPILER compile "$work/cmd.affine" -o "$work/cmd.wasm" + +# Codegen contract: the unit MUST export `_start` (the S6 shim) since +# `main` is parameter-less. Catches a regression in lib/codegen.ml. +if ! wasm-tools print "$work/cmd.wasm" 2>/dev/null | grep -q '(export "_start"'; then + echo "FAIL: S6 codegen did not emit the _start shim for fn main()" + exit 1 +fi + +has_section "$work/cmd.wasm" \ + || { echo "FAIL: fixture did not emit the ownership section"; exit 1; } + +tools/componentize.sh --command "$work/cmd.wasm" "$work/cmd.component.wasm" + +wasm-tools validate --features component-model "$work/cmd.component.wasm" +has_section "$work/cmd.component.wasm" \ + || { echo "FAIL: ownership section lost through command componentization"; exit 1; } + +# WIT contract: the lift must produce a `wasi:cli/run` export. +if ! wasm-tools component wit "$work/cmd.component.wasm" 2>/dev/null \ + | grep -q 'export wasi:cli/run'; then + echo "FAIL: component does not export wasi:cli/run" + exit 1 +fi + +# Real-host invoke: the whole point of S6 — `wasmtime run` actually +# runs the program end-to-end. Exit status MUST be 0 (the shim drops +# main's result; trap-free execution is the contract). +if ! wasmtime run "$work/cmd.component.wasm"; then + echo "FAIL: wasmtime run rejected the S6 component" + exit 1 +fi + +echo "ADR-015 S6 command smoke: PASSED ✓" diff --git a/tools/componentize.sh b/tools/componentize.sh index b88e502b..a1ff1e8e 100755 --- a/tools/componentize.sh +++ b/tools/componentize.sh @@ -1,28 +1,52 @@ #!/usr/bin/env bash # SPDX-License-Identifier: MPL-2.0 # -# ADR-015 S3 — the componentize on-ramp. POST-codegen, codegen -# UNCHANGED: the compiler still emits a core `wasi_snapshot_preview1` -# module (the legacy default, ADR-015 reversible-in-progress); this -# wraps that core module into a WASI-0.2 (preview2) WASM *component* -# via the official preview1->preview2 reactor adapter (reactor, not -# command: AffineScript modules export functions, not a WASI `_start`). +# ADR-015 S3 + S6 — the componentize on-ramp. POST-codegen, the OCaml +# compiler still emits a core `wasi_snapshot_preview1` module (the +# legacy default; ADR-015 reversible-in-progress through S5). This +# wrapper turns that core module into a WASI-0.2 (preview2) WASM +# *component* via the official preview1->preview2 adapter family +# (fetch-pinned + checksum-verified by provision-component-toolchain.sh): +# +# reactor (default, ADR-015 S3) — for AffineScript modules used as +# libraries (host calls plain exports like `main`). The resulting +# component instantiates but has no `wasi:cli/run`, so `wasmtime run` +# cannot invoke it; the host loads it through its own WASI bindings. +# +# command (--command, ADR-015 S6) — for AffineScript programs +# invoked as WASI commands (`wasmtime run`, jco). Requires the core +# module to export `_start : () -> ()`; the S6 codegen change in +# `lib/codegen.ml` emits this shim automatically whenever the unit +# exports a parameter-less `main`. The resulting component exports +# `wasi:cli/run@0.2.x` per `wit/affinescript.wit`. # # The `affinescript.ownership` custom section (the typed-wasm contract -# carrier, multi-producer ABI) MUST survive the wrap — asserted here. +# carrier, multi-producer ABI) MUST survive the wrap — asserted here +# in both modes. # -# Usage: tools/componentize.sh -# Adapter: tools/vendor/wasi_snapshot_preview1.reactor.wasm -# (fetch-pinned + checksum-verified by provision-component-toolchain.sh; -# override with AFFINESCRIPT_WASI_ADAPTER=/path). +# Usage: +# tools/componentize.sh [--command|--reactor] +# +# Adapters: tools/vendor/wasi_snapshot_preview1.{reactor,command}.wasm +# (fetch-pinned by provision-component-toolchain.sh; override with +# AFFINESCRIPT_WASI_ADAPTER=/path to force a specific adapter file.) set -euo pipefail -core="${1:?usage: componentize.sh }" -out="${2:?usage: componentize.sh }" -adapter="${AFFINESCRIPT_WASI_ADAPTER:-$(cd "$(dirname "$0")" && pwd)/vendor/wasi_snapshot_preview1.reactor.wasm}" +mode="reactor" +if [ "${1:-}" = "--command" ] || [ "${1:-}" = "--reactor" ]; then + mode="${1#--}" + shift +fi + +core="${1:?usage: componentize.sh [--command|--reactor] }" +out="${2:?usage: componentize.sh [--command|--reactor] }" + +adapter_dir="$(cd "$(dirname "$0")" && pwd)/vendor" +default_adapter="${adapter_dir}/wasi_snapshot_preview1.${mode}.wasm" +adapter="${AFFINESCRIPT_WASI_ADAPTER:-${default_adapter}}" [ -f "$core" ] || { echo "componentize: no core module: $core" >&2; exit 2; } -[ -f "$adapter" ] || { echo "componentize: adapter missing ($adapter) — run tools/provision-component-toolchain.sh" >&2; exit 2; } +[ -f "$adapter" ] || { echo "componentize: ${mode} adapter missing ($adapter) — run tools/provision-component-toolchain.sh" >&2; exit 2; } command -v wasm-tools >/dev/null || { echo "componentize: wasm-tools not on PATH — run tools/provision-component-toolchain.sh" >&2; exit 2; } # Count with `grep -c` (reads ALL input, so no early-close SIGPIPE that @@ -37,6 +61,18 @@ section_count() { had_ownership=0 [ "$(section_count "$core")" -gt 0 ] && had_ownership=1 +# Command mode requires `_start` in the core (the adapter lifts it into +# `wasi:cli/run`). Fail fast with a pointer to the codegen contract +# rather than letting wasm-tools complain a few lines deeper. +if [ "$mode" = "command" ]; then + if ! wasm-tools print "$core" 2>/dev/null | grep -q '(export "_start"'; then + echo "componentize --command: core module is missing the \`_start\` export." >&2 + echo " AffineScript emits \`_start\` automatically when a parameter-less" >&2 + echo " \`fn main()\` is present (ADR-015 S6 codegen, lib/codegen.ml)." >&2 + exit 2 + fi +fi + wasm-tools component new "$core" --adapt "wasi_snapshot_preview1=$adapter" -o "$out" wasm-tools validate --features component-model "$out" @@ -49,4 +85,16 @@ if [ "$had_ownership" = 1 ]; then echo "ownership section: preserved through componentization ✓" fi -echo "componentized -> $out (valid WASI-0.2 component)" +# Command-mode contract: assert the result actually exports +# `wasi:cli/run` (catches a regression where the adapter wraps without +# lifting, or the wrong adapter file is pointed at). +if [ "$mode" = "command" ]; then + if ! wasm-tools component wit "$out" 2>/dev/null | grep -q 'export wasi:cli/run'; then + echo "FATAL: --command output does not export wasi:cli/run" >&2 + rm -f "$out" + exit 1 + fi + echo "wasi:cli/run: lifted into component world ✓" +fi + +echo "componentized (${mode}) -> $out (valid WASI-0.2 component)" diff --git a/tools/provision-component-toolchain.sh b/tools/provision-component-toolchain.sh index 39edcd5b..fb3e543e 100755 --- a/tools/provision-component-toolchain.sh +++ b/tools/provision-component-toolchain.sh @@ -21,45 +21,75 @@ WASM_TOOLS_VERSION="1.249.0" WASM_COMPONENT_LD_VERSION="0.5.22" WAC_CLI_VERSION="0.10.0" -# The official `wasi_snapshot_preview1` -> preview2 *reactor* adapter -# (ADR-015 S3). AffineScript modules export functions (`main`, …), not -# a WASI command `_start`, so the REACTOR adapter is the correct one -# (the command adapter requires `_start`). Fetch-pinned (provenance + -# sha256 recorded here; verified fail-closed) rather than vendored as a -# binary blob in the source tree. Source: bytecodealliance/wasmtime -# release v44.0.1 (matches the pinned wasmtime in this estate). +# The official `wasi_snapshot_preview1` -> preview2 adapters. Two +# flavours, both fetch-pinned (provenance + sha256 recorded here, +# verified fail-closed) rather than vendored as binary blobs: +# +# * REACTOR (ADR-015 S3) — for AffineScript modules used as libraries. +# The core module exports plain functions (`main`, game-loop hooks, +# ...) and the reactor adapter wraps them without requiring a +# command entrypoint. +# * COMMAND (ADR-015 S6) — for AffineScript programs invoked under a +# WASI 0.2 host (`wasmtime run`, jco). Requires the core module to +# export `_start : () -> ()`; the S6 codegen change in +# `lib/codegen.ml` emits a `_start` shim that calls `main` and +# drops its i32 result. The resulting component exports +# `wasi:cli/run@0.2.x` per `wit/affinescript.wit`. +# +# Source: bytecodealliance/wasmtime release v44.0.1 (matches the +# pinned wasmtime in this estate). ADAPTER_WASMTIME_TAG="v44.0.1" -ADAPTER_URL="https://github.com/bytecodealliance/wasmtime/releases/download/${ADAPTER_WASMTIME_TAG}/wasi_snapshot_preview1.reactor.wasm" -ADAPTER_SHA256="e352bf5b74aec62d8e7da7e0536ede4b3d1ccdb4d9a1767031f4d6f007d22e85" ADAPTER_DIR="$(cd "$(dirname "$0")" && pwd)/vendor" -ADAPTER_PATH="${ADAPTER_DIR}/wasi_snapshot_preview1.reactor.wasm" + +ADAPTER_REACTOR_URL="https://github.com/bytecodealliance/wasmtime/releases/download/${ADAPTER_WASMTIME_TAG}/wasi_snapshot_preview1.reactor.wasm" +ADAPTER_REACTOR_SHA256="e352bf5b74aec62d8e7da7e0536ede4b3d1ccdb4d9a1767031f4d6f007d22e85" +ADAPTER_REACTOR_PATH="${ADAPTER_DIR}/wasi_snapshot_preview1.reactor.wasm" + +ADAPTER_COMMAND_URL="https://github.com/bytecodealliance/wasmtime/releases/download/${ADAPTER_WASMTIME_TAG}/wasi_snapshot_preview1.command.wasm" +ADAPTER_COMMAND_SHA256="8ff2ea78d31179f12d6fb1d2cadc0ab83356cd049f362d5a8d94a4070c7a15bd" +ADAPTER_COMMAND_PATH="${ADAPTER_DIR}/wasi_snapshot_preview1.command.wasm" + +# Back-compat: the S3-era variable names referenced the reactor by +# default. Keep them as aliases so any external script still pinning to +# them continues to resolve. +ADAPTER_URL="${ADAPTER_REACTOR_URL}" +ADAPTER_SHA256="${ADAPTER_REACTOR_SHA256}" +ADAPTER_PATH="${ADAPTER_REACTOR_PATH}" echo "Provisioning component-model toolchain (pinned, --locked)…" cargo install --locked --version "${WASM_TOOLS_VERSION}" wasm-tools cargo install --locked --version "${WASM_COMPONENT_LD_VERSION}" wasm-component-ld cargo install --locked --version "${WAC_CLI_VERSION}" wac-cli -echo "Fetch-pinning the preview1->preview2 reactor adapter…" -mkdir -p "${ADAPTER_DIR}" -if [ -f "${ADAPTER_PATH}" ] && \ - [ "$(sha256sum "${ADAPTER_PATH}" | cut -d' ' -f1)" = "${ADAPTER_SHA256}" ]; then - echo "adapter already present and verified" -else - curl -fsSL -o "${ADAPTER_PATH}.tmp" "${ADAPTER_URL}" - got="$(sha256sum "${ADAPTER_PATH}.tmp" | cut -d' ' -f1)" - if [ "${got}" != "${ADAPTER_SHA256}" ]; then - rm -f "${ADAPTER_PATH}.tmp" - echo "FATAL: adapter checksum mismatch (expected ${ADAPTER_SHA256}, got ${got})" >&2 +fetch_adapter() { + local kind="$1" url="$2" sha="$3" path="$4" + echo "Fetch-pinning the preview1->preview2 ${kind} adapter…" + if [ -f "${path}" ] && \ + [ "$(sha256sum "${path}" | cut -d' ' -f1)" = "${sha}" ]; then + echo " ${kind}: already present and verified" + return 0 + fi + curl -fsSL -o "${path}.tmp" "${url}" + local got + got="$(sha256sum "${path}.tmp" | cut -d' ' -f1)" + if [ "${got}" != "${sha}" ]; then + rm -f "${path}.tmp" + echo "FATAL: ${kind} adapter checksum mismatch (expected ${sha}, got ${got})" >&2 exit 1 fi - mv "${ADAPTER_PATH}.tmp" "${ADAPTER_PATH}" - echo "adapter verified + pinned -> ${ADAPTER_PATH}" -fi + mv "${path}.tmp" "${path}" + echo " ${kind}: verified + pinned -> ${path}" +} + +mkdir -p "${ADAPTER_DIR}" +fetch_adapter reactor "${ADAPTER_REACTOR_URL}" "${ADAPTER_REACTOR_SHA256}" "${ADAPTER_REACTOR_PATH}" +fetch_adapter command "${ADAPTER_COMMAND_URL}" "${ADAPTER_COMMAND_SHA256}" "${ADAPTER_COMMAND_PATH}" echo echo "Installed:" wasm-tools --version wasm-component-ld --version wac --version -echo "adapter: ${ADAPTER_WASMTIME_TAG} (${ADAPTER_SHA256})" -echo "OK — ADR-015 S3 (componentize on-ramp) is now unblocked." +echo "reactor adapter: ${ADAPTER_WASMTIME_TAG} (${ADAPTER_REACTOR_SHA256})" +echo "command adapter: ${ADAPTER_WASMTIME_TAG} (${ADAPTER_COMMAND_SHA256})" +echo "OK — ADR-015 S3 (componentize on-ramp) + S6 (WIT export lifting) unblocked."