Skip to content

Commit c17de6e

Browse files
committed
feat(wasi): #180 ADR-015 S6a — WIT export lifting (real-host main-invoke)
Lift a parameter-less `fn main()` into the WASI 0.2 (preview2) command shape so `wasmtime run <component>` 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
1 parent 8137236 commit c17de6e

8 files changed

Lines changed: 304 additions & 64 deletions

File tree

.machine_readable/6a2/META.a2ml

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -988,8 +988,20 @@ Staged plan (ledger INT-03; each row = one gated PR):
988988
- S4: native `wasi:clocks` + environment + argv via preview2 (wasmtime
989989
host-testable), replacing the preview1 shims behind the component path.
990990
- 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.
991+
- S6a (WIT export lifting, DONE): codegen emits a `_start : () -> ()`
992+
shim that calls `main` and drops its i32 result whenever a
993+
parameter-less `fn main()` is present (`lib/codegen.ml`);
994+
`tools/componentize.sh --command` wraps with the fetch-pinned
995+
preview1->preview2 *command* adapter (wasmtime v44.0.1, sha256
996+
8ff2ea78...) producing a component that exports `wasi:cli/run@0.2.x`
997+
per `wit/affinescript.wit`. End-to-end gate:
998+
`tests/componentize/command_smoke.sh` proves `wasmtime run` invokes
999+
the component (exit 0), the ownership section survives, and the
1000+
lift is asserted. Purely additive: reactor consumers + game-loop
1001+
hooks are byte-unchanged.
1002+
- S6b: `wasi:sockets`.
1003+
- S6c: flip the default wasm target to component and demote the
1004+
preview1 stdout path to a named legacy target.
9931005
"""
9941006
consequences = """
9951007
- End-state is one-way (the component model becomes the canonical wasm

docs/ECOSYSTEM.adoc

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -205,25 +205,35 @@ INT-01 ↔ INT-02. S1; unblocks INT-05/08/11. The `affinescript-dom-loader`
205205
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
208-
S1..S6; legacy preview1 stdout path is the default until S6
209-
(reversible-in-progress). S2 toolchain provisioned (#251). **S3
208+
S1..S6; legacy preview1 stdout path remains the default until S6c
209+
(reversible-in-progress). S2 toolchain provisioned (#251). S3
210210
DONE: `tools/componentize.sh` wraps the emitted core module into a
211211
valid WASI-0.2 component via the fetch-pinned preview1→preview2
212212
*reactor* adapter (wasmtime v44.0.1, sha256-verified); the
213213
`affinescript.ownership` section is proven to SURVIVE the wrap;
214214
`tests/componentize/smoke.sh` gates it (SKIP-safe without the
215-
toolchain). Codegen UNCHANGED — core preview1 stays the default
216-
(reversible). S4a (clock) + S4b (env_count, arg_count) DONE:
215+
toolchain). S4a (clock) + S4b (env_count, arg_count) DONE:
217216
builtins lower to on-demand `wasi_snapshot_preview1.*` imports
218217
(Effect_sites pre-scan, canonical-order indexing through
219218
`ctx.wasi_func_indices`; zero impact on units that don't use them;
220219
verified with a multi-import combo regression). Component path
221-
bridges to `wasi:clocks`/`wasi:cli`. Real-host main-invoke deferred
222-
to S6 (WIT export-lifting / wasi:cli/run command shape). String
223-
accessors (env_at/arg_at) gated on a byte-level wasm-IR extension
224-
(I32Load8U/I32Store8 absent today) — tracked as the next slice
225-
before/with S5 filesystem.** WIT world of
226-
record: `wit/affinescript.wit`
220+
bridges to `wasi:clocks`/`wasi:cli`. **S6a (WIT export lifting)
221+
DONE: `lib/codegen.ml` auto-emits a `_start : () -> ()` shim that
222+
calls `main` and drops its i32 result whenever a parameter-less
223+
`fn main()` is present. `tools/componentize.sh --command` wraps
224+
with the fetch-pinned preview1→preview2 *command* adapter
225+
(wasmtime v44.0.1, sha256 8ff2ea78…) — the resulting component
226+
exports `wasi:cli/run@0.2.x` per `wit/affinescript.wit`. End-to-end
227+
smoke `tests/componentize/command_smoke.sh` proves the lift, the
228+
ownership section survives, and `wasmtime run <component>` exits
229+
0 on real-host invoke. Purely additive: reactor consumers, the
230+
`__indirect_function_table` export, and game-loop hooks are
231+
byte-unchanged.** String accessors (env_at/arg_at) gated on a
232+
byte-level wasm-IR extension (I32Load8U/I32Store8 absent today)
233+
— tracked as the next slice before/with S5 filesystem. Remaining
234+
sub-slices: S5 (native clocks/env/argv via preview2), S6b
235+
(`wasi:sockets`), S6c (flip the default wasm target from preview1
236+
→ component). WIT world of record: `wit/affinescript.wit`
227237
|INT-04 |Publish compiler + runtime to JSR (then npm) |#181 |runtime
228238
packaging READY (affine-js + affinescript-tea JSR dry-run green;
229239
manual-only `publish-jsr.yml`; docs/PACKAGING.adoc). INT-01 dep

docs/TECH-DEBT.adoc

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -192,16 +192,25 @@ follow-up
192192
multi-ns import object, ownership accessor); 14 unit tests via pinned
193193
`deno task test` + `tests/modules/loader-bridge/` e2e on real
194194
compiler-emitted xmod wasm (closes INT-01↔INT-02). Unblocks INT-05/08/11
195-
|INT-03 |WASI preview2 / host I/O |S1→S4b |#180 ADR-015 (full
195+
|INT-03 |WASI preview2 / host I/O |S1→S6a |#180 ADR-015 (full
196196
Component-Model re-target, S1..S6); S2 toolchain #251 closed;
197-
S3 componentize done; **S4a (clock) + S4b (env_count, arg_count)
197+
S3 componentize done; S4a (clock) + S4b (env_count, arg_count)
198198
DONE — on-demand preview1 imports via Effect_sites pre-scan,
199199
canonical-order indexing through `ctx.wasi_func_indices`; combo
200200
regression proves no collision. String accessors (env_at/arg_at)
201201
gated on byte-level wasm IR (I32Load8U/I32Store8 absent today) —
202-
tracked next slice. Real-host main-invoke = S6 (WIT export
203-
lifting). Next S5
204-
(native clocks/env/argv)**
202+
tracked next slice. **S6a (WIT export lifting) DONE:
203+
`lib/codegen.ml` auto-emits a `_start : () -> ()` shim whenever
204+
a parameter-less `fn main()` is present (additive, no regression
205+
on reactor consumers); `tools/componentize.sh --command` wraps
206+
with the fetch-pinned command adapter (sha256
207+
8ff2ea78… / wasmtime v44.0.1, provisioned alongside the reactor
208+
adapter); `tests/componentize/command_smoke.sh` gates the
209+
contract end-to-end — `wasi:cli/run` lifted, ownership section
210+
survives, `wasmtime run` exits 0. Real-host main-invoke now
211+
works via `wasmtime run <component>`.** Remaining S6
212+
sub-slices: S5 (native clocks/env/argv), S6b (`wasi:sockets`),
213+
S6c (flip the default wasm target from preview1 → component)
205214
|INT-04 |Publish to JSR/npm |S2 |#181 packaging READY (dry-run green,
206215
manual workflow); compiler-binary distribution decided = **ADR-019**
207216
(#260, Releases + thin Deno/JSR shim, staged S1..S4) — S1/S2/S3

docs/specs/SETTLED-DECISIONS.adoc

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -302,8 +302,12 @@ change; *S2* toolchain provisioning (`wasm-tools`, `wasm-component-ld`,
302302
(codegen still emits core wasm; post-step wraps it via the standard
303303
preview1→preview2 adapter; ownership-section survival asserted; wasmtime
304304
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.
305+
`wasi:filesystem` (unblocks INT-06); *S6a* (WIT export lifting) codegen
306+
auto-emits a `_start : () -> ()` shim around a parameter-less `fn main()`
307+
so `tools/componentize.sh --command` (preview1→preview2 *command*
308+
adapter) produces a real `wasi:cli/run`-exporting component that any
309+
WASI 0.2 host can invoke; *S6b* `wasi:sockets`; *S6c* flip the default
310+
wasm target to component and demote preview1 to a legacy target.
307311

308312
This decision is settled; do not reopen without amending the ADR. Full
309313
ADR in `.machine_readable/6a2/META.a2ml` (ADR-015); ledger INT-03 in

lib/codegen.ml

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2626,6 +2626,56 @@ let generate_module ?loader (prog : program) : wasm_module result =
26262626
@ table_export
26272627
in
26282628

2629+
(* ADR-015 S6 — WIT export lifting. When the unit exports a parameter-
2630+
less `main`, also emit a `_start : () -> ()` shim that calls it and
2631+
drops the i32 result. With `_start` present, tools/componentize.sh
2632+
--command can wrap the core module with the WASI preview1→preview2
2633+
*command* adapter, producing a real `wasi:cli/run`-exporting
2634+
component that any WASI 0.2 host (`wasmtime run`, jco) can invoke.
2635+
Purely additive: every existing consumer (reactor componentize,
2636+
game-loop hosts that call `main` directly) sees the same exports
2637+
plus an extra `_start`; main-with-params and units that already
2638+
export `_start` are skipped. *)
2639+
let final_types, final_funcs, final_exports =
2640+
let has_user_start =
2641+
List.exists (fun e -> e.e_name = "_start") exports_with_mem
2642+
in
2643+
let main_export =
2644+
List.find_opt (fun e -> e.e_name = "main") exports_with_mem
2645+
in
2646+
match main_export, has_user_start with
2647+
| Some { e_desc = ExportFunc main_idx; _ }, false ->
2648+
let main_local_idx = main_idx - import_offset in
2649+
if main_local_idx < 0 || main_local_idx >= List.length all_funcs then
2650+
(ctx'.types, all_funcs, exports_with_mem)
2651+
else
2652+
let main_func = List.nth all_funcs main_local_idx in
2653+
let main_type = List.nth ctx'.types main_func.f_type in
2654+
if main_type.ft_params <> [] then
2655+
(ctx'.types, all_funcs, exports_with_mem)
2656+
else
2657+
let void_ft = { ft_params = []; ft_results = [] } in
2658+
let (void_type_idx, types_after) =
2659+
intern_func_type ctx'.types void_ft
2660+
in
2661+
let drop_instrs =
2662+
List.map (fun _ -> Drop) main_type.ft_results
2663+
in
2664+
let start_func = {
2665+
f_type = void_type_idx;
2666+
f_locals = [];
2667+
f_body = Call main_idx :: drop_instrs;
2668+
} in
2669+
let funcs_after = all_funcs @ [start_func] in
2670+
let start_idx = import_offset + List.length all_funcs in
2671+
let exports_after =
2672+
exports_with_mem
2673+
@ [{ e_name = "_start"; e_desc = ExportFunc start_idx }]
2674+
in
2675+
(types_after, funcs_after, exports_after)
2676+
| _ -> (ctx'.types, all_funcs, exports_with_mem)
2677+
in
2678+
26292679
(* Stage 2: Build [affinescript.ownership] custom section from collected annotations *)
26302680
let ownership_payload = build_ownership_section ctx'.ownership_annots in
26312681
let custom_sections = if Bytes.length ownership_payload > 0 then
@@ -2635,12 +2685,12 @@ let generate_module ?loader (prog : program) : wasm_module result =
26352685
in
26362686

26372687
Ok {
2638-
types = ctx'.types;
2639-
funcs = all_funcs;
2688+
types = final_types;
2689+
funcs = final_funcs;
26402690
tables = tables;
26412691
mems = [{ mem_type = { lim_min = 1; lim_max = None } }]; (* 1 page default *)
26422692
globals = ctx'.globals;
2643-
exports = exports_with_mem;
2693+
exports = final_exports;
26442694
imports = ctx'.imports;
26452695
elems = elems;
26462696
datas = List.rev ctx'.datas; (* Reverse to get original order *)
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#!/usr/bin/env bash
2+
# SPDX-License-Identifier: MPL-2.0
3+
#
4+
# ADR-015 S6 gated smoke (WIT export lifting): compile an AffineScript
5+
# program with `fn main()`, run it through `tools/componentize.sh
6+
# --command`, and assert
7+
# (a) the resulting component is a valid WASI-0.2 component,
8+
# (b) it exports `wasi:cli/run@0.2.x`,
9+
# (c) `wasmtime run` can invoke it (exit 0), and
10+
# (d) the `affinescript.ownership` custom section survives the wrap.
11+
#
12+
# SKIPs cleanly (exit 0) when the component toolchain or wasmtime is
13+
# not provisioned — opt-in, mirroring tests/componentize/smoke.sh.
14+
set -euo pipefail
15+
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
16+
cd "$ROOT"
17+
18+
if ! command -v wasm-tools >/dev/null \
19+
|| [ ! -f tools/vendor/wasi_snapshot_preview1.command.wasm ]; then
20+
echo "SKIP: component toolchain / command adapter not provisioned (tools/provision-component-toolchain.sh)"
21+
exit 0
22+
fi
23+
if ! command -v wasmtime >/dev/null; then
24+
echo "SKIP: wasmtime not on PATH (real-host run is the S6 contract)"
25+
exit 0
26+
fi
27+
28+
COMPILER="${AFFINESCRIPT:-$ROOT/_build/default/bin/main.exe}"
29+
[ -x "$COMPILER" ] || COMPILER="dune exec affinescript --"
30+
31+
work="$(mktemp -d)"
32+
trap 'rm -rf "$work"' EXIT
33+
34+
# Fixture: ownership-bearing imports + a parameter-less `main` so the
35+
# S6 codegen emits `_start` (the command adapter's lift target). The
36+
# fn main() return value is irrelevant — the shim drops it.
37+
cp test/e2e/fixtures/verify_ownership_clean.affine "$work/cmd.affine"
38+
printf '\nfn main() -> Int { add(2, 3) }\n' >> "$work/cmd.affine"
39+
40+
has_section() {
41+
[ "$(wasm-tools print "$1" 2>/dev/null | grep -c 'affinescript.ownership' || true)" -gt 0 ]
42+
}
43+
44+
$COMPILER compile "$work/cmd.affine" -o "$work/cmd.wasm"
45+
46+
# Codegen contract: the unit MUST export `_start` (the S6 shim) since
47+
# `main` is parameter-less. Catches a regression in lib/codegen.ml.
48+
if ! wasm-tools print "$work/cmd.wasm" 2>/dev/null | grep -q '(export "_start"'; then
49+
echo "FAIL: S6 codegen did not emit the _start shim for fn main()"
50+
exit 1
51+
fi
52+
53+
has_section "$work/cmd.wasm" \
54+
|| { echo "FAIL: fixture did not emit the ownership section"; exit 1; }
55+
56+
tools/componentize.sh --command "$work/cmd.wasm" "$work/cmd.component.wasm"
57+
58+
wasm-tools validate --features component-model "$work/cmd.component.wasm"
59+
has_section "$work/cmd.component.wasm" \
60+
|| { echo "FAIL: ownership section lost through command componentization"; exit 1; }
61+
62+
# WIT contract: the lift must produce a `wasi:cli/run` export.
63+
if ! wasm-tools component wit "$work/cmd.component.wasm" 2>/dev/null \
64+
| grep -q 'export wasi:cli/run'; then
65+
echo "FAIL: component does not export wasi:cli/run"
66+
exit 1
67+
fi
68+
69+
# Real-host invoke: the whole point of S6 — `wasmtime run` actually
70+
# runs the program end-to-end. Exit status MUST be 0 (the shim drops
71+
# main's result; trap-free execution is the contract).
72+
if ! wasmtime run "$work/cmd.component.wasm"; then
73+
echo "FAIL: wasmtime run rejected the S6 component"
74+
exit 1
75+
fi
76+
77+
echo "ADR-015 S6 command smoke: PASSED ✓"

tools/componentize.sh

Lines changed: 64 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,52 @@
11
#!/usr/bin/env bash
22
# SPDX-License-Identifier: MPL-2.0
33
#
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`).
4+
# ADR-015 S3 + S6 — the componentize on-ramp. POST-codegen, the OCaml
5+
# compiler still emits a core `wasi_snapshot_preview1` module (the
6+
# legacy default; ADR-015 reversible-in-progress through S5). This
7+
# wrapper turns that core module into a WASI-0.2 (preview2) WASM
8+
# *component* via the official preview1->preview2 adapter family
9+
# (fetch-pinned + checksum-verified by provision-component-toolchain.sh):
10+
#
11+
# reactor (default, ADR-015 S3) — for AffineScript modules used as
12+
# libraries (host calls plain exports like `main`). The resulting
13+
# component instantiates but has no `wasi:cli/run`, so `wasmtime run`
14+
# cannot invoke it; the host loads it through its own WASI bindings.
15+
#
16+
# command (--command, ADR-015 S6) — for AffineScript programs
17+
# invoked as WASI commands (`wasmtime run`, jco). Requires the core
18+
# module to export `_start : () -> ()`; the S6 codegen change in
19+
# `lib/codegen.ml` emits this shim automatically whenever the unit
20+
# exports a parameter-less `main`. The resulting component exports
21+
# `wasi:cli/run@0.2.x` per `wit/affinescript.wit`.
1022
#
1123
# The `affinescript.ownership` custom section (the typed-wasm contract
12-
# carrier, multi-producer ABI) MUST survive the wrap — asserted here.
24+
# carrier, multi-producer ABI) MUST survive the wrap — asserted here
25+
# in both modes.
1326
#
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).
27+
# Usage:
28+
# tools/componentize.sh [--command|--reactor] <core.wasm> <out.component.wasm>
29+
#
30+
# Adapters: tools/vendor/wasi_snapshot_preview1.{reactor,command}.wasm
31+
# (fetch-pinned by provision-component-toolchain.sh; override with
32+
# AFFINESCRIPT_WASI_ADAPTER=/path to force a specific adapter file.)
1833
set -euo pipefail
1934

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}"
35+
mode="reactor"
36+
if [ "${1:-}" = "--command" ] || [ "${1:-}" = "--reactor" ]; then
37+
mode="${1#--}"
38+
shift
39+
fi
40+
41+
core="${1:?usage: componentize.sh [--command|--reactor] <core.wasm> <out.component.wasm>}"
42+
out="${2:?usage: componentize.sh [--command|--reactor] <core.wasm> <out.component.wasm>}"
43+
44+
adapter_dir="$(cd "$(dirname "$0")" && pwd)/vendor"
45+
default_adapter="${adapter_dir}/wasi_snapshot_preview1.${mode}.wasm"
46+
adapter="${AFFINESCRIPT_WASI_ADAPTER:-${default_adapter}}"
2347

2448
[ -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; }
49+
[ -f "$adapter" ] || { echo "componentize: ${mode} adapter missing ($adapter) — run tools/provision-component-toolchain.sh" >&2; exit 2; }
2650
command -v wasm-tools >/dev/null || { echo "componentize: wasm-tools not on PATH — run tools/provision-component-toolchain.sh" >&2; exit 2; }
2751

2852
# Count with `grep -c` (reads ALL input, so no early-close SIGPIPE that
@@ -37,6 +61,18 @@ section_count() {
3761
had_ownership=0
3862
[ "$(section_count "$core")" -gt 0 ] && had_ownership=1
3963

64+
# Command mode requires `_start` in the core (the adapter lifts it into
65+
# `wasi:cli/run`). Fail fast with a pointer to the codegen contract
66+
# rather than letting wasm-tools complain a few lines deeper.
67+
if [ "$mode" = "command" ]; then
68+
if ! wasm-tools print "$core" 2>/dev/null | grep -q '(export "_start"'; then
69+
echo "componentize --command: core module is missing the \`_start\` export." >&2
70+
echo " AffineScript emits \`_start\` automatically when a parameter-less" >&2
71+
echo " \`fn main()\` is present (ADR-015 S6 codegen, lib/codegen.ml)." >&2
72+
exit 2
73+
fi
74+
fi
75+
4076
wasm-tools component new "$core" --adapt "wasi_snapshot_preview1=$adapter" -o "$out"
4177
wasm-tools validate --features component-model "$out"
4278

@@ -49,4 +85,16 @@ if [ "$had_ownership" = 1 ]; then
4985
echo "ownership section: preserved through componentization ✓"
5086
fi
5187

52-
echo "componentized -> $out (valid WASI-0.2 component)"
88+
# Command-mode contract: assert the result actually exports
89+
# `wasi:cli/run` (catches a regression where the adapter wraps without
90+
# lifting, or the wrong adapter file is pointed at).
91+
if [ "$mode" = "command" ]; then
92+
if ! wasm-tools component wit "$out" 2>/dev/null | grep -q 'export wasi:cli/run'; then
93+
echo "FATAL: --command output does not export wasi:cli/run" >&2
94+
rm -f "$out"
95+
exit 1
96+
fi
97+
echo "wasi:cli/run: lifted into component world ✓"
98+
fi
99+
100+
echo "componentized (${mode}) -> $out (valid WASI-0.2 component)"

0 commit comments

Comments
 (0)