From c38d311a90a2886aa6def35629e22fad6d0d2e7a Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 21 Sep 2026 00:33:38 +0000 Subject: [PATCH 1/2] =?UTF-8?q?docs(tech-debt):=20close=20out=20GitHub=20i?= =?UTF-8?q?ndex=20#175=20=E2=80=94=208/8=20children=20done?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The original #175 sub-issues (#176–183) are all closed. Reconcile the ledgers so they stop listing SAT-01/SAT-03/INT-07/INT-08 as open, mark Stage D closed / Stage E active, and point residual WASI + satellite work at the independent follow-ups (#485/#486/#487/#56). Refs #175 Signed-off-by: Jonathan D.A. Jewell <6759885+hyperpolymath@users.noreply.github.com> --- docs/CAPABILITY-MATRIX.adoc | 8 ++++--- docs/ECOSYSTEM.adoc | 20 ++++++++++------ docs/TECH-DEBT.adoc | 46 +++++++++++++++++++++++++++---------- 3 files changed, 52 insertions(+), 22 deletions(-) diff --git a/docs/CAPABILITY-MATRIX.adoc b/docs/CAPABILITY-MATRIX.adoc index 80ed92ba..72039899 100644 --- a/docs/CAPABILITY-MATRIX.adoc +++ b/docs/CAPABILITY-MATRIX.adoc @@ -285,9 +285,11 @@ their own ledger (link:SOUNDNESS.adoc[SOUNDNESS.adoc]), gated by * typed-wasm is *not* an AffineScript subsystem. It is a separate, language-agnostic target with its own repo, proofs, and other producers (ephapax). AffineScript is one producer among several. -* The satellite ecosystem is mostly scaffold/skeleton — see the satellite - registry in `ECOSYSTEM.adoc`. `affinescript-tea`/`-dom-loader`/`-cadre` - were imaginary until the #175 scaffolds; `-dom`/`-pixijs` are skeletons. +* The satellite ecosystem is mixed maturity — see the satellite registry + in `ECOSYSTEM.adoc`. `affinescript-tea` (INT-07/#182) and + `affinescript-dom` (INT-08/#183) shipped runtimes; `affinescript-cadre` + (INT-09) closed 2026-07-25. `affinescript-pixijs` remains a scaffold + (#56). The `-dom-loader` satellite was folded into `affine-js` (#489). == Change control diff --git a/docs/ECOSYSTEM.adoc b/docs/ECOSYSTEM.adoc index 4ca5b2f6..da3094cb 100644 --- a/docs/ECOSYSTEM.adoc +++ b/docs/ECOSYSTEM.adoc @@ -75,14 +75,18 @@ files; `test/test_stdlib_aot.ml`; issues #128/#135. |*D* |Base-language soundness + ecosystem connective tissue: CORE-01 borrow Phase-3 (#177); the INT-01..12 integration roadmap; the satellite registry made real; #228/ADR-014 module-qualified paths (estate port unblocker). | -*ACTIVE — current frontier* |This stage. Substrate = INT-01..04 + CORE-01. +*CLOSED 2026-07-25* |Substrate (INT-01..04 + CORE-01) closed 2026-05-31; +remaining original #175 children INT-07 (#182) and INT-08 (#183) closed +2026-07-25. Residual WASI slices live as independent follow-ups (#485 S5 +filesystem, #486 S6c default-flip, #487 sockets); satellite remainder is +#56. Stage E is now the frontier. |*E* |typed-wasm convergence hardening (the transition runway): the AffineScript↔typed-wasm contract widened from L7+L10 toward full L1–6/L13–16 emitted-wasm enforcement; estate-wide re-validation (#235); effect-threaded async-boundary recogniser (#234); the #225/#160 convergence -ABI matured to "shared with Ephapax". |*planned* |Begins when D's substrate -(INT-01..04, CORE-01) is closed; ends at a stable, multi-producer +ABI matured to "shared with Ephapax". |*ACTIVE — current frontier* |D's +substrate (INT-01..04, CORE-01) is closed. Ends at a stable, multi-producer typed-wasm convergence ABI. |=== @@ -188,8 +192,10 @@ object, `typedwasm.ownership` accessor. == Integration roadmap — INT-01..12 Critical path: *INT-01..INT-04 are substrate and block most satellites.* -Filed sub-issues are children of #175; severity/blocked-by also in -link:TECH-DEBT.adoc[TECH-DEBT.adoc]. +GitHub index #175 is closed (8/8 original sub-issues complete, owner close +2026-09-21); severity/blocked-by also in +link:TECH-DEBT.adoc[TECH-DEBT.adoc]. Residual WASI work is #485/#486/#487; +satellite remainder is #56. [cols="1,3,1,2"] |=== @@ -259,10 +265,10 @@ auth). Compiler-binary distribution = design fork #260 candidate for an issue-spinout) |INT-06 |Server-side runtime profile (on INT-03 WASI p2) |ledger-only |planned (blocked by #485) -|INT-07 |`affinescript-tea` runtime satellite |#182 |runtime + run loop +|INT-07 |`affinescript-tea` runtime satellite |#182 **CLOSED 2026-07-25** |runtime + run loop shipped (`TeaApp`/`parseTeaLayout`, Linear-msg enforced); INT-01 dep cleared (#253). Router/nav = separate INT-09 -|INT-08 |DOM reconciler in `affinescript-dom` |#183 |reconciler +|INT-08 |DOM reconciler in `affinescript-dom` |#183 **CLOSED 2026-07-25** |reconciler implemented + compiles (resolve→typecheck→codegen→wasm); `.as`→`.affine` corrected. INT-02 dep cleared. #255 (wasm loop-codegen defect) that gated the runtime is FIXED (PR #257, closed 2026-05-19); runtime VERIFIED diff --git a/docs/TECH-DEBT.adoc b/docs/TECH-DEBT.adoc index f8e58e28..3fa12771 100644 --- a/docs/TECH-DEBT.adoc +++ b/docs/TECH-DEBT.adoc @@ -17,6 +17,14 @@ Reconstructed and made authoritative 2026-05-19 (it was cited by #175–183 / #228 / #177 but never committed). For the spine + contract see link:ECOSYSTEM.adoc[ECOSYSTEM.adoc]; for readiness see link:CAPABILITY-MATRIX.adoc[CAPABILITY-MATRIX.adoc]. + +GitHub index #175: **8/8 original sub-issues CLOSED** as of 2026-07-25 +(#176 DOC, #177 CORE-01, #178 INT-01, #179 INT-02, #180 INT-03, +#181 INT-04-JSR, #182 INT-07, #183 INT-08). The GitHub ticket itself is a +closed index (owner close 2026-09-21); this ledger remains the durable +index. Residual WASI work lives as independent follow-ups (#485 S5 +filesystem, #486 S6c default-flip, #487 socket recv/send/accept); +satellite remainder is #56. ==== toc::[] @@ -36,8 +44,8 @@ Stage D CLOSED ── CORE-01 (#177) ─┐ (base l └─ INT-04 (#181) ◄─┘ (◄ INT-01) │ ADR-014 (#228) ── estate port unblocker, S1 │ ▼ -Stage E planned ── typed-wasm convergence hardening - (#234, #235, #225/#160 line, INT-12) ──► stable multi-producer ABI +Stage E ACTIVE ── typed-wasm convergence hardening + (#234 closed, #235, #225/#160 line, INT-12) ──► stable multi-producer ABI ---- The mandate's order of operations: *CORE-01 first* (the one named @@ -420,12 +428,15 @@ drift fixed via #297/#300 (single source of truth `lib/version.ml` `0.1.1`). Repo-wide SPDX surface relicensed MPL-2.0 → MPL-2.0 (#301/#304; PMPL preserved as the narrative-preferred licence in root `LICENSE`). -|INT-07 |`affinescript-tea` runtime |S2 |#182 runtime + run loop shipped -(TeaApp/parseTeaLayout, Linear-msg enforced); INT-01 cleared (#253) -|INT-08 |DOM reconciler |S2 |#183 implemented + compiles; `.as`→`.affine` -fixed; #255 (wasm loop-codegen defect) that gated the runtime is fixed -(PR #257, closed 2026-05-19) — runtime VERIFIED end-to-end 2026-07-07 via -`affinescript-dom/e2e/run.sh` +|INT-07 |`affinescript-tea` runtime |S2 |**#182 CLOSED 2026-07-25** — +runtime + run loop shipped (TeaApp/parseTeaLayout, Linear-msg enforced); +INT-01 cleared (#253) +|INT-08 |DOM reconciler |S2 |**#183 CLOSED 2026-07-25** — implemented + +compiles; `.as`→`.affine` fixed; #255 (wasm loop-codegen defect) that +gated the runtime is fixed (PR #257, closed 2026-05-19) — runtime +VERIFIED end-to-end 2026-07-07 via `affinescript-dom/e2e/run.sh` +|INT-09 |`affinescript-cadre` router/navigation runtime |S2 |**CLOSED +2026-07-25** (CadreRouter JS wrapper + E2E tests) |INT-10 |`affinescript-lsp` distribution |S2 |**DONE end-to-end live** (#282, ADR-019 S4): LSP resolves the compiler via `AFFINESCRIPT_COMPILER` → `affinescript` on `PATH` → the `@hyperpolymath/affinescript` shim @@ -434,7 +445,14 @@ unit tests green. As of 2026-05-20 the shim is published on JSR (`@hyperpolymath/affinescript@0.1.2`) and pins all three v0.1.1 binaries by SHA256, so the shim resolution path actually executes end-to-end now (was code-correct but inert before today's publish). -|INT-05/06/09/11/12 |ledger-only; filed when blocker closes |— |planned +|INT-05 |Loader-driven multi-module app bundling |S2 |planned (INT-02 dep +cleared 2026-05-31 via #179; candidate for an issue-spinout) +|INT-06 |Server-side runtime profile (on INT-03 WASI p2) |S2 |planned +(blocked by #485) +|INT-11 |Browser host parity (`affine-js` loader + `affinescript-dom` +reconciler end-to-end) |S2 |**CLOSED 2026-07-25** +|INT-12 |typed-wasm convergence fixtures |S1 |planned (Stage E; +coordinates with `hyperpolymath/typed-wasm` C5.1) |=== == Section E — SAT (satellite repos) @@ -442,14 +460,18 @@ end-to-end now (was code-correct but inert before today's publish). [cols="1,3,1,2"] |=== |ID |Item |Sev |Status -|SAT-01 |`affinescript-dom` reconciler (skeleton → real) |S2 |open (=INT-08) +|SAT-01 |`affinescript-dom` reconciler (skeleton → real) |S2 |**DONE** +(=INT-08, #183 CLOSED 2026-07-25) — runtime VERIFIED e2e 2026-07-07 via +`affinescript-dom/e2e/run.sh` |SAT-02 |`packages/affine-js` hardcoded path / env-only imports |S1 |*FIXED* by INT-02 (Refs #179): `Deno.readFile(url.pathname)` replaced by host-agnostic `readBytes`; `env`-only import shape replaced by multi-namespace `buildImportObject`; `mod.js` consumes the loader. Proven + locked (see INT-02) -|SAT-03 |`affinescript-tea` runtime build-out |S2 |open (=INT-07) -|SAT-04 |`affinescript-cadre` router runtime |S2 |planned (=INT-09) +|SAT-03 |`affinescript-tea` runtime build-out |S2 |**DONE** (=INT-07, +#182 CLOSED 2026-07-25) +|SAT-04 |`affinescript-cadre` router runtime |S2 |**DONE** (=INT-09, +CLOSED 2026-07-25) |SAT-05 |`affinescript-pixijs` migration prerequisite |S3 |open #56 |=== From af166ffa9d1c4dc62cf98e7de1b1ec5177935e56 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" Date: Mon, 21 Sep 2026 07:57:20 +0000 Subject: [PATCH 2/2] feat(wasi): land #485 filesystem, #487 sockio, #486 wrap-default, #56-A Preview1 lowerings for wasi:filesystem (file_open/read/fd_write/close) and sock_recv/send/accept; componentize.sh defaults to --command without flipping the compiler's preview1 emit; stdlib Dom/Console + Pixi gap-fill. Refs #485 #486 #487 #56 --- affinescript-dom/src/dom.affine | 45 +++ docs/CAPABILITY-MATRIX.adoc | 13 +- docs/ECOSYSTEM.adoc | 42 ++- docs/TECH-DEBT.adoc | 41 +- lib/codegen.ml | 151 ++++++++ lib/codegen_deno.ml | 61 ++- lib/effect.ml | 2 +- lib/resolve.ml | 4 + lib/typecheck.ml | 29 ++ lib/wasi_runtime.ml | 355 ++++++++++++++++++ stdlib/Console.affine | 27 ++ stdlib/Dom.affine | 61 +++ stdlib/Pixi.affine | 51 +++ stdlib/PixiSound.affine | 6 + stdlib/PixiUI.affine | 26 +- tests/codegen-deno/pixi_smoke.affine | 24 +- tests/codegen-deno/pixi_smoke.harness.mjs | 35 +- tests/codegen-deno/pixisound_smoke.affine | 6 + .../codegen-deno/pixisound_smoke.harness.mjs | 5 + tests/codegen-deno/pixiui_smoke.affine | 14 +- tests/codegen-deno/pixiui_smoke.harness.mjs | 24 +- tests/codegen/dom_pilot_surface.affine | 34 ++ tests/codegen/file_open.affine | 7 + tests/codegen/file_roundtrip.affine | 14 + tests/codegen/net_accept.affine | 5 + tests/codegen/net_recv.affine | 5 + tests/codegen/net_send.affine | 5 + tests/codegen/test_dom_pilot_surface.mjs | 12 + tests/codegen/test_file_open.mjs | 32 ++ tests/codegen/test_file_roundtrip.mjs | 72 ++++ tests/codegen/test_net_accept.mjs | 29 ++ tests/codegen/test_net_recv.mjs | 32 ++ tests/codegen/test_net_send.mjs | 33 ++ tests/codegen/test_wasi_fs_combo.mjs | 57 +++ tests/codegen/wasi_fs_combo.affine | 15 + tests/componentize/filesystem_smoke.sh | 93 +++++ tests/componentize/smoke.sh | 4 +- tests/componentize/sockio_smoke.sh | 53 +++ tools/componentize.sh | 22 +- 39 files changed, 1487 insertions(+), 59 deletions(-) create mode 100644 stdlib/Console.affine create mode 100644 stdlib/Dom.affine create mode 100644 tests/codegen/dom_pilot_surface.affine create mode 100644 tests/codegen/file_open.affine create mode 100644 tests/codegen/file_roundtrip.affine create mode 100644 tests/codegen/net_accept.affine create mode 100644 tests/codegen/net_recv.affine create mode 100644 tests/codegen/net_send.affine create mode 100644 tests/codegen/test_dom_pilot_surface.mjs create mode 100644 tests/codegen/test_file_open.mjs create mode 100644 tests/codegen/test_file_roundtrip.mjs create mode 100644 tests/codegen/test_net_accept.mjs create mode 100644 tests/codegen/test_net_recv.mjs create mode 100644 tests/codegen/test_net_send.mjs create mode 100644 tests/codegen/test_wasi_fs_combo.mjs create mode 100644 tests/codegen/wasi_fs_combo.affine create mode 100755 tests/componentize/filesystem_smoke.sh create mode 100755 tests/componentize/sockio_smoke.sh diff --git a/affinescript-dom/src/dom.affine b/affinescript-dom/src/dom.affine index 0a2ba06c..0fb81495 100644 --- a/affinescript-dom/src/dom.affine +++ b/affinescript-dom/src/dom.affine @@ -60,6 +60,45 @@ pub fn text(content: String) -> VNode = VText(content); pub fn h(tag: String, attrs: [(String, String)], children: [VNode]) -> VNode = VElem(tag, attrs, children); +/// Convenience constructors (#56-A). Thin wrappers over `h` so idaptik's +/// pilot `Dom.div` / `Dom.h1` / `Dom.p` surface compiles without going +/// through the low-level tag string. +pub fn div(attrs: [(String, String)], children: [VNode]) -> VNode = + h("div", attrs, children); + +pub fn h1(attrs: [(String, String)], content: String) -> VNode = + h("h1", attrs, [text(content)]); + +pub fn h2(attrs: [(String, String)], content: String) -> VNode = + h("h2", attrs, [text(content)]); + +pub fn h3(attrs: [(String, String)], content: String) -> VNode = + h("h3", attrs, [text(content)]); + +pub fn p(attrs: [(String, String)], content: String) -> VNode = + h("p", attrs, [text(content)]); + +pub fn span(attrs: [(String, String)], content: String) -> VNode = + h("span", attrs, [text(content)]); + +pub fn button(attrs: [(String, String)], content: String) -> VNode = + h("button", attrs, [text(content)]); + +pub fn input(attrs: [(String, String)]) -> VNode = + h("input", attrs, []); + +pub fn a(attrs: [(String, String)], content: String) -> VNode = + h("a", attrs, [text(content)]); + +pub fn img(attrs: [(String, String)]) -> VNode = + h("img", attrs, []); + +pub fn ul(attrs: [(String, String)], children: [VNode]) -> VNode = + h("ul", attrs, children); + +pub fn li(attrs: [(String, String)], children: [VNode]) -> VNode = + h("li", attrs, children); + // `len` is not available in the standalone wasm-AOT subset; a `for`-count // helper is (proven: tests/codegen/test_for_loop). Monomorphic, not // generic, since wasm codegen generic support is not relied on here. @@ -103,6 +142,12 @@ pub fn mount(selector: String, vnode: VNode) -> Bool { } } +/// Replace `document.body` contents with `vnode` (#56-A). Equivalent +/// to `mount("body", vnode)`. Declared below `mount` because codegen +/// is single-pass in source order. +pub fn replaceBody(vnode: VNode) -> Bool = + mount("body", vnode); + // ── Reconciler: minimal mutation between two VNode trees ───────────────────── fn attr_has(attrs: [(String, String)], key: String) -> Bool { diff --git a/docs/CAPABILITY-MATRIX.adoc b/docs/CAPABILITY-MATRIX.adoc index 72039899..ae40678f 100644 --- a/docs/CAPABILITY-MATRIX.adoc +++ b/docs/CAPABILITY-MATRIX.adoc @@ -176,7 +176,11 @@ The compiler emits to many targets. *Maturity is not uniform; do not claim |Target |Status |Notes |WASM (core) |works |Cross-module imports + `extern fn` → WASM import -emission. The reference target. +emission. The reference target. WASI preview1 on-demand imports cover +clock/env/args/sockets/filesystem (`file_open`/`file_read`/ +`file_fd_write`/`file_close`, `net_recv`/`net_send`/`net_accept`); +`tools/componentize.sh` defaults to the command adapter (#486). Native +preview2 import lowering is still the one-way door. |WASM-GC |partial |Variant-with-args + same-arity `PatCon` destructuring; silent-bad-codegen fallbacks eliminated. Mixed-arity matches need uniform @@ -288,8 +292,11 @@ their own ledger (link:SOUNDNESS.adoc[SOUNDNESS.adoc]), gated by * The satellite ecosystem is mixed maturity — see the satellite registry in `ECOSYSTEM.adoc`. `affinescript-tea` (INT-07/#182) and `affinescript-dom` (INT-08/#183) shipped runtimes; `affinescript-cadre` - (INT-09) closed 2026-07-25. `affinescript-pixijs` remains a scaffold - (#56). The `-dom-loader` satellite was folded into `affine-js` (#489). + (INT-09) closed 2026-07-25. `affinescript-pixijs` in-tree bindings + (`stdlib/Pixi.affine` + UI/Sound) plus 56-A `stdlib/Dom.affine` / + `stdlib/Console.affine` landed 2026-09-21; full idaptik surface is + still open (#56). The `-dom-loader` satellite was folded into + `affine-js` (#489). == Change control diff --git a/docs/ECOSYSTEM.adoc b/docs/ECOSYSTEM.adoc index da3094cb..41a6f413 100644 --- a/docs/ECOSYSTEM.adoc +++ b/docs/ECOSYSTEM.adoc @@ -77,9 +77,11 @@ Phase-3 (#177); the INT-01..12 integration roadmap; the satellite registry made real; #228/ADR-014 module-qualified paths (estate port unblocker). | *CLOSED 2026-07-25* |Substrate (INT-01..04 + CORE-01) closed 2026-05-31; remaining original #175 children INT-07 (#182) and INT-08 (#183) closed -2026-07-25. Residual WASI slices live as independent follow-ups (#485 S5 -filesystem, #486 S6c default-flip, #487 sockets); satellite remainder is -#56. Stage E is now the frontier. +2026-07-25. Residual WASI follow-ups #485 (S5 filesystem) and #487 +(socket recv/send/accept) have preview1 lowerings + gated smokes in +tree; #486 flipped the wrap default to `--command` without the one-way +compiler re-target. Satellite remainder is #56 (56-A Dom/Console + Pixi +gap-fill landed). Stage E is now the frontier. |*E* |typed-wasm convergence hardening (the transition runway): the AffineScript↔typed-wasm contract widened from L7+L10 toward full @@ -158,7 +160,10 @@ runtime is *VERIFIED end-to-end* (2026-07-07): `affinescript-dom/e2e/run.sh` drives mount + in-place attr patch + text update + surplus-child removal against a real Int-handle host DOM, mutation log asserted. -|`affinescript-pixijs` |skeleton |Migration-prerequisite scaffold (#56). +|`affinescript-pixijs` |in-tree bindings |#56 partial: `stdlib/Pixi.affine` +(+ UI/Sound) is the operational binding; 56-A also landed +`stdlib/Dom.affine` + `stdlib/Console.affine`. Remaining: full idaptik +surface. |`affinescript-vite` |scaffold |Build-tool integration shell. @@ -192,10 +197,10 @@ object, `typedwasm.ownership` accessor. == Integration roadmap — INT-01..12 Critical path: *INT-01..INT-04 are substrate and block most satellites.* -GitHub index #175 is closed (8/8 original sub-issues complete, owner close -2026-09-21); severity/blocked-by also in -link:TECH-DEBT.adoc[TECH-DEBT.adoc]. Residual WASI work is #485/#486/#487; -satellite remainder is #56. +GitHub index #175: 8/8 original sub-issues complete; severity/blocked-by +also in link:TECH-DEBT.adoc[TECH-DEBT.adoc]. Residual WASI work is +#485/#486/#487 (preview1 FS + sockio + wrap-default in tree; native +preview2 re-target still the one-way door); satellite remainder is #56. [cols="1,3,1,2"] |=== @@ -245,16 +250,15 @@ dropped; 81d9fc7). Test gates: `tests/componentize/{smoke,command_smoke,sockets_smoke}.sh` cover the contract end-to-end; ownership section byte-survives every wrap; combo-import canonical-order regression. Residual (filed -2026-05-31): #485 — S5 native `wasi:filesystem` -(open/read/write/close), unblocks INT-06 (server-side runtime -profile); #486 — S6c flip default wasm target preview1 → -component, bundled with native-preview2 cleanup replacing the -on-demand preview1 shims emitted by S4a/S4b/S6b with direct -`wasi:clocks` / `wasi:cli` / `wasi:sockets` calls (only pays off -once preview2 is the default — the one-way door that ends -reversibility); #487 — socket recv/send/accept primitives -(byte-level buffer / network-address marshalling, helper mirrors -`gen_str_at_via_get`). WIT world of record: `wit/affinescript.wit` +2026-05-31, status 2026-09-21): #485 — S5 native `wasi:filesystem` +preview1 lowering LANDED (`file_open`/`file_read`/`file_fd_write`/ +`file_close`, Effect `File`, gated +`tests/componentize/filesystem_smoke.sh`); #486 — S6c wrap default +flipped to `--command` (S3 reactor smoke now passes `--reactor`); +compiler still emits preview1 core; native-preview2 cleanup remains +the one-way door; #487 — `net_recv`/`net_send`/`net_accept` preview1 +lowering LANDED (gated `tests/componentize/sockio_smoke.sh`). 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 @@ -264,7 +268,7 @@ auth). Compiler-binary distribution = design fork #260 (INT-02 dep cleared 2026-05-31 via #179; no other blockers — next-up candidate for an issue-spinout) |INT-06 |Server-side runtime profile (on INT-03 WASI p2) |ledger-only -|planned (blocked by #485) +|planned (#485 FS slice unblocked 2026-09-21) |INT-07 |`affinescript-tea` runtime satellite |#182 **CLOSED 2026-07-25** |runtime + run loop shipped (`TeaApp`/`parseTeaLayout`, Linear-msg enforced); INT-01 dep cleared (#253). Router/nav = separate INT-09 diff --git a/docs/TECH-DEBT.adoc b/docs/TECH-DEBT.adoc index 3fa12771..bc90c4ba 100644 --- a/docs/TECH-DEBT.adoc +++ b/docs/TECH-DEBT.adoc @@ -20,11 +20,14 @@ link:CAPABILITY-MATRIX.adoc[CAPABILITY-MATRIX.adoc]. GitHub index #175: **8/8 original sub-issues CLOSED** as of 2026-07-25 (#176 DOC, #177 CORE-01, #178 INT-01, #179 INT-02, #180 INT-03, -#181 INT-04-JSR, #182 INT-07, #183 INT-08). The GitHub ticket itself is a -closed index (owner close 2026-09-21); this ledger remains the durable -index. Residual WASI work lives as independent follow-ups (#485 S5 -filesystem, #486 S6c default-flip, #487 socket recv/send/accept); -satellite remainder is #56. +#181 INT-04-JSR, #182 INT-07, #183 INT-08). The GitHub ticket is a pure +index whose children are complete; this ledger remains the durable +index. Residual WASI follow-ups #485 (S5 filesystem) and #487 +(socket recv/send/accept) have preview1 lowerings + gated smokes in +tree; #486 flipped the *wrap* default to `--command` without the +one-way compiler re-target (native-preview2 cleanup still open). +Satellite remainder is #56 (56-A Dom/Console + Pixi gap-fill landed; +full idaptik surface still open). ==== toc::[] @@ -409,11 +412,21 @@ export lifting via `_start : () -> ()` shim around parameter-less `wasi_snapshot_preview1.sock_shutdown` (command adapter bridges to `wasi:sockets/tcp`; 81d9fc7). Gates: `tests/componentize/{smoke,command_smoke,sockets_smoke}.sh`. -Residual (filed 2026-05-31): #485 S5 native `wasi:filesystem` -(open/read/write/close — unblocks INT-06), #486 S6c flip default -wasm target preview1 → component + native-preview2 cleanup (one-way -door), #487 socket recv/send/accept primitives (byte-level buffer -/ network-address marshalling) +Residual (filed 2026-05-31, status 2026-09-21): +#485 S5 native `wasi:filesystem` — *preview1 lowering LANDED* +(`file_open`/`file_read`/`file_fd_write`/`file_close` → +`path_open`/`fd_read`/`fd_write`/`fd_close`, Effect `File`, +dirfd 3, combo canonical-order lock + gated +`tests/componentize/filesystem_smoke.sh` hermetic `--dir` +round-trip). Unblocks INT-06's FS slice. +#486 S6c — *wrap default LANDED* (`tools/componentize.sh` now +defaults to `--command`; S3 reactor smoke requests `--reactor` +explicitly). Compiler still emits preview1 core wasm; +native-preview2 import cleanup remains the one-way door. +#487 socket recv/send/accept — *preview1 lowering LANDED* +(`net_recv`/`net_send`/`net_accept` → `sock_recv`/`sock_send`/ +`sock_accept`, Effect `Net`, gated +`tests/componentize/sockio_smoke.sh`) |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; per-file record @@ -448,7 +461,8 @@ end-to-end now (was code-correct but inert before today's publish). |INT-05 |Loader-driven multi-module app bundling |S2 |planned (INT-02 dep cleared 2026-05-31 via #179; candidate for an issue-spinout) |INT-06 |Server-side runtime profile (on INT-03 WASI p2) |S2 |planned -(blocked by #485) +(#485 FS slice unblocked 2026-09-21; remaining: native-preview2 +clocks/cli/sockets + a real server profile) |INT-11 |Browser host parity (`affine-js` loader + `affinescript-dom` reconciler end-to-end) |S2 |**CLOSED 2026-07-25** |INT-12 |typed-wasm convergence fixtures |S1 |planned (Stage E; @@ -512,7 +526,10 @@ link:specs/TYPED-WASM-ROADMAP.adoc[TYPED-WASM-ROADMAP] (Tranches B/C/D) == Update protocol * One PR per ledger unit; `Refs #N` (never `Closes` on the #175 epic — only - the owner closes it). + the owner closes it). Residual follow-ups after the original 8 children + are #485/#486/#487/#56 — do not re-open #175 to track them. Owner may + close #175 once those residuals are accepted; GitHub write is a PAT + capability, not a ledger action. * Every capability-affecting PR updates `CAPABILITY-MATRIX.adoc` *and* this ledger in the same change; `STATE.a2ml` mirrors after. * `dune runtest --force` must stay green (baseline 260, 2026-05-19). diff --git a/lib/codegen.ml b/lib/codegen.ml index 84c3603f..2a30c16a 100644 --- a/lib/codegen.ml +++ b/lib/codegen.ml @@ -1295,6 +1295,151 @@ let rec gen_expr (ctx : context) (expr : expr) : (context * instr list) result = let code = fd_code @ how_code @ [Call sock_func_idx] in Ok (ctx_how, code) + | ExprVar id when id.name = "file_open" && List.length args = 2 -> + (* ADR-015 S5 (#485): file_open(path, oflags) -> Int fd. + Lowers to wasi_snapshot_preview1.path_open against dirfd 3 + (first --dir preopen). Failure returns -errno. *) + let* (ctx_p, path_code) = gen_expr ctx (List.nth args 0) in + let* (ctx_o, oflags_code) = gen_expr ctx_p (List.nth args 1) in + let (ctx1, path_local) = alloc_local ctx_o "__file_open_path" in + let (ctx2, oflags_local) = alloc_local ctx1 "__file_open_oflags" in + let (ctx3, opened_fd_local) = alloc_local ctx2 "__file_open_fd" in + let (ctx4, errno_local) = alloc_local ctx3 "__file_open_errno" in + let (ctx_h, heap_idx) = ensure_heap_ptr ctx4 in + let path_open_idx = + try List.assoc "path_open" ctx.wasi_func_indices + with Not_found -> 1 + in + let code = + path_code @ [LocalSet path_local] @ + oflags_code @ [LocalSet oflags_local] @ + Wasi_runtime.gen_file_open + heap_idx path_local oflags_local + opened_fd_local errno_local path_open_idx + in + Ok (ctx_h, code) + + | ExprVar id when id.name = "file_fd_write" && List.length args = 2 -> + (* ADR-015 S5 (#485): file_fd_write(fd, data) -> Int errno. + Reuses the always-present fd_write import with a caller fd. *) + let* (ctx_fd, fd_code) = gen_expr ctx (List.nth args 0) in + let* (ctx_s, str_code) = gen_expr ctx_fd (List.nth args 1) in + let (ctx1, fd_local) = alloc_local ctx_s "__file_fd_write_fd" in + let (ctx2, str_local) = alloc_local ctx1 "__file_fd_write_str" in + let (ctx3, temp_local) = alloc_local ctx2 "__file_fd_write_iov" in + let (ctx_h, heap_idx) = ensure_heap_ptr ctx3 in + let fd_write_idx = + try List.assoc "fd_write" ctx.wasi_func_indices + with Not_found -> 0 + in + let code = + fd_code @ [LocalSet fd_local] @ + str_code @ [LocalSet str_local] @ + Wasi_runtime.gen_file_fd_write + heap_idx fd_local str_local fd_write_idx temp_local + in + Ok (ctx_h, code) + + | ExprVar id when id.name = "file_read" && List.length args = 2 -> + (* ADR-015 S5 (#485): file_read(fd, max_len) -> String. *) + let* (ctx_fd, fd_code) = gen_expr ctx (List.nth args 0) in + let* (ctx_n, n_code) = gen_expr ctx_fd (List.nth args 1) in + let (ctx1, fd_local) = alloc_local ctx_n "__file_read_fd" in + let (ctx2, max_local) = alloc_local ctx1 "__file_read_max" in + let (ctx3, iov_local) = alloc_local ctx2 "__file_read_iov" in + let (ctx4, buf_local) = alloc_local ctx3 "__file_read_buf" in + let (ctx5, n_local) = alloc_local ctx4 "__file_read_n" in + let (ctx6, src_local) = alloc_local ctx5 "__file_read_src" in + let (ctx7, dst_local) = alloc_local ctx6 "__file_read_dst" in + let (ctx8, result_local) = alloc_local ctx7 "__file_read_res" in + let (ctx_h, heap_idx) = ensure_heap_ptr ctx8 in + let fd_read_idx = + try List.assoc "fd_read" ctx.wasi_func_indices + with Not_found -> 1 + in + let code = + fd_code @ [LocalSet fd_local] @ + n_code @ [LocalSet max_local] @ + Wasi_runtime.gen_file_read + heap_idx fd_local max_local iov_local buf_local + n_local src_local dst_local result_local fd_read_idx + in + Ok (ctx_h, code) + + | ExprVar id when id.name = "file_close" && List.length args = 1 -> + (* ADR-015 S5 (#485): file_close(fd) -> Int errno. *) + let* (ctx_fd, fd_code) = gen_expr ctx (List.hd args) in + let fd_close_idx = + try List.assoc "fd_close" ctx.wasi_func_indices + with Not_found -> 1 + in + let code = fd_code @ Wasi_runtime.gen_file_close fd_close_idx in + Ok (ctx_fd, code) + + | ExprVar id when id.name = "net_recv" && List.length args = 2 -> + (* #487: net_recv(fd, max_len) -> String. *) + let* (ctx_fd, fd_code) = gen_expr ctx (List.nth args 0) in + let* (ctx_n, n_code) = gen_expr ctx_fd (List.nth args 1) in + let (ctx1, fd_local) = alloc_local ctx_n "__net_recv_fd" in + let (ctx2, max_local) = alloc_local ctx1 "__net_recv_max" in + let (ctx3, iov_local) = alloc_local ctx2 "__net_recv_iov" in + let (ctx4, buf_local) = alloc_local ctx3 "__net_recv_buf" in + let (ctx5, n_local) = alloc_local ctx4 "__net_recv_n" in + let (ctx6, src_local) = alloc_local ctx5 "__net_recv_src" in + let (ctx7, dst_local) = alloc_local ctx6 "__net_recv_dst" in + let (ctx8, result_local) = alloc_local ctx7 "__net_recv_res" in + let (ctx_h, heap_idx) = ensure_heap_ptr ctx8 in + let sock_recv_idx = + try List.assoc "sock_recv" ctx.wasi_func_indices + with Not_found -> 1 + in + let code = + fd_code @ [LocalSet fd_local] @ + n_code @ [LocalSet max_local] @ + Wasi_runtime.gen_net_recv + heap_idx fd_local max_local iov_local buf_local + n_local src_local dst_local result_local sock_recv_idx + in + Ok (ctx_h, code) + + | ExprVar id when id.name = "net_send" && List.length args = 2 -> + (* #487: net_send(fd, data) -> Int errno. *) + let* (ctx_fd, fd_code) = gen_expr ctx (List.nth args 0) in + let* (ctx_s, str_code) = gen_expr ctx_fd (List.nth args 1) in + let (ctx1, fd_local) = alloc_local ctx_s "__net_send_fd" in + let (ctx2, str_local) = alloc_local ctx1 "__net_send_str" in + let (ctx3, temp_local) = alloc_local ctx2 "__net_send_iov" in + let (ctx_h, heap_idx) = ensure_heap_ptr ctx3 in + let sock_send_idx = + try List.assoc "sock_send" ctx.wasi_func_indices + with Not_found -> 1 + in + let code = + fd_code @ [LocalSet fd_local] @ + str_code @ [LocalSet str_local] @ + Wasi_runtime.gen_net_send + heap_idx fd_local str_local temp_local sock_send_idx + in + Ok (ctx_h, code) + + | ExprVar id when id.name = "net_accept" && List.length args = 1 -> + (* #487: net_accept(fd) -> Int new_fd (or -errno). *) + let* (ctx_fd, fd_code) = gen_expr ctx (List.hd args) in + let (ctx1, fd_local) = alloc_local ctx_fd "__net_accept_fd" in + let (ctx2, opened_fd_local) = alloc_local ctx1 "__net_accept_out" in + let (ctx3, errno_local) = alloc_local ctx2 "__net_accept_errno" in + let (ctx_h, heap_idx) = ensure_heap_ptr ctx3 in + let sock_accept_idx = + try List.assoc "sock_accept" ctx.wasi_func_indices + with Not_found -> 1 + in + let code = + fd_code @ [LocalSet fd_local] @ + Wasi_runtime.gen_net_accept + heap_idx fd_local opened_fd_local errno_local sock_accept_idx + in + Ok (ctx_h, code) + | ExprVar id when (id.name = "env_count" || id.name = "arg_count") && List.length args = 1 -> (* ADR-015 S4b (#180): env_count(u: Unit) / arg_count(u: Unit) @@ -3773,6 +3918,12 @@ let generate_module ?loader (prog : program) : wasm_module result = ("arg_at", "args_sizes_get", Wasi_runtime.create_args_sizes_get_import); ("arg_at", "args_get", Wasi_runtime.create_args_get_import); ("net_shutdown", "sock_shutdown", Wasi_runtime.create_sock_shutdown_import); + ("file_open", "path_open", Wasi_runtime.create_path_open_import); + ("file_read", "fd_read", Wasi_runtime.create_fd_read_import); + ("file_close", "fd_close", Wasi_runtime.create_fd_close_import); + ("net_recv", "sock_recv", Wasi_runtime.create_sock_recv_import); + ("net_send", "sock_send", Wasi_runtime.create_sock_send_import); + ("net_accept", "sock_accept", Wasi_runtime.create_sock_accept_import); ] |> List.filter_map (fun (b, w, f) -> if uses b then Some (w, f ()) else None) diff --git a/lib/codegen_deno.ml b/lib/codegen_deno.ml index b3842b43..06e5011f 100644 --- a/lib/codegen_deno.ml +++ b/lib/codegen_deno.ml @@ -366,6 +366,20 @@ const __as_pixiTextAsContainer = (t) => t; const __as_pixiTickerAdd = (t, cb) => { t.add(cb); return 0; }; const __as_pixiTickerStart = (t) => { t.start(); return 0; }; const __as_pixiTickerStop = (t) => { t.stop(); return 0; }; +const __as_pixiPointNew = (x, y) => new globalThis.__as_pixi.Point(x, y); +const __as_pixiRectangleNew = (x, y, w, h) => new globalThis.__as_pixi.Rectangle(x, y, w, h); +const __as_pixiCircleNew = (x, y, r) => new globalThis.__as_pixi.Circle(x, y, r); +const __as_pixiSpriteSetTint = (s, color) => { s.tint = color; return 0; }; +const __as_pixiSpriteSetBlendMode = (s, mode) => { s.blendMode = mode; return 0; }; +const __as_pixiContainerSetHitArea = (c, area) => { c.hitArea = area; return 0; }; +const __as_pixiContainerSetMask = (c, mask) => { c.mask = mask; return 0; }; +const __as_pixiBlurFilterNew = (strength) => new globalThis.__as_pixi.BlurFilter({ strength }); +const __as_pixiContainerSetFilter = (c, filter) => { c.filters = [filter]; return 0; }; +const __as_pixiNineSliceSpriteFrom = (t) => new globalThis.__as_pixi.NineSliceSprite(t); +const __as_pixiNineSliceSpriteAsContainer = (s) => s; +const __as_pixiAssetsLoad = async (url) => globalThis.__as_pixi.Assets.load(url); +const __as_pixiEventGlobalX = (e) => Number(e && e.global ? e.global.x : 0); +const __as_pixiEventGlobalY = (e) => Number(e && e.global ? e.global.y : 0); // ---- @pixi/ui (bindings #3): consumer-provided import ---- // Host JS environment exposes globalThis.__as_pixi_ui (the namespace // from `import * as PixiUI from "@pixi/ui"`). Tests set it in the @@ -388,6 +402,12 @@ const __as_pixiUiSliderAsContainer = (s) => s; const __as_pixiUiSwitchNew = (options) => new globalThis.__as_pixi_ui.Switch(options); const __as_pixiUiSwitchOnChange = (sw, cb) => { sw.onChange.connect(cb); return 0; }; const __as_pixiUiSwitchAsContainer = (sw) => sw; +const __as_pixiUiProgressBarNew = (options) => new globalThis.__as_pixi_ui.ProgressBar(options); +const __as_pixiUiProgressBarAsContainer = (p) => p; +const __as_pixiUiListNew = (options) => new globalThis.__as_pixi_ui.List(options); +const __as_pixiUiListAsContainer = (l) => l; +const __as_pixiUiInputNew = (options) => new globalThis.__as_pixi_ui.Input(options); +const __as_pixiUiInputAsContainer = (i) => i; // ---- @pixi/sound (bindings #2): consumer-provided import ---- // Host JS environment exposes globalThis.__as_pixi_sound (the `Sound` // named export from `@pixi/sound`). Tests set it in the harness before @@ -403,6 +423,8 @@ const __as_pixiSoundPause = (s) => { s.pause(); return 0; }; const __as_pixiSoundResume = (s) => { s.resume(); return 0; }; const __as_pixiSoundSetVolume = (s, vol) => { s.volume = vol; return 0; }; const __as_pixiSoundSetLoop = (s, loop) => { s.loop = loop; return 0; }; +const __as_pixiSoundIsPlaying = (s) => Boolean(s.isPlaying); +const __as_pixiSoundDuration = (s) => Number(s.duration) || 0; // ---- Ipc (bindings #9): web-platform MessageChannel/MessagePort ---- // Uses standard web globals (MessageChannel, structuredClone) — no // consumer-side init required. Available unmodified in modern JS runtimes, @@ -803,6 +825,20 @@ let () = b "pixiTickerAdd" (fun a -> Printf.sprintf "__as_pixiTickerAdd(%s, %s)" (arg 0 a) (arg 1 a)); b "pixiTickerStart" (fun a -> Printf.sprintf "__as_pixiTickerStart(%s)" (arg 0 a)); b "pixiTickerStop" (fun a -> Printf.sprintf "__as_pixiTickerStop(%s)" (arg 0 a)); + b "pixiPointNew" (fun a -> Printf.sprintf "__as_pixiPointNew(%s, %s)" (arg 0 a) (arg 1 a)); + b "pixiRectangleNew" (fun a -> Printf.sprintf "__as_pixiRectangleNew(%s, %s, %s, %s)" (arg 0 a) (arg 1 a) (arg 2 a) (arg 3 a)); + b "pixiCircleNew" (fun a -> Printf.sprintf "__as_pixiCircleNew(%s, %s, %s)" (arg 0 a) (arg 1 a) (arg 2 a)); + b "pixiSpriteSetTint" (fun a -> Printf.sprintf "__as_pixiSpriteSetTint(%s, %s)" (arg 0 a) (arg 1 a)); + b "pixiSpriteSetBlendMode" (fun a -> Printf.sprintf "__as_pixiSpriteSetBlendMode(%s, %s)" (arg 0 a) (arg 1 a)); + b "pixiContainerSetHitArea" (fun a -> Printf.sprintf "__as_pixiContainerSetHitArea(%s, %s)" (arg 0 a) (arg 1 a)); + b "pixiContainerSetMask" (fun a -> Printf.sprintf "__as_pixiContainerSetMask(%s, %s)" (arg 0 a) (arg 1 a)); + b "pixiBlurFilterNew" (fun a -> Printf.sprintf "__as_pixiBlurFilterNew(%s)" (arg 0 a)); + b "pixiContainerSetFilter" (fun a -> Printf.sprintf "__as_pixiContainerSetFilter(%s, %s)" (arg 0 a) (arg 1 a)); + b "pixiNineSliceSpriteFrom" (fun a -> Printf.sprintf "__as_pixiNineSliceSpriteFrom(%s)" (arg 0 a)); + b "pixiNineSliceSpriteAsContainer" (fun a -> Printf.sprintf "__as_pixiNineSliceSpriteAsContainer(%s)" (arg 0 a)); + b "pixiAssetsLoad" (fun a -> Printf.sprintf "(await __as_pixiAssetsLoad(%s))" (arg 0 a)); + b "pixiEventGlobalX" (fun a -> Printf.sprintf "__as_pixiEventGlobalX(%s)" (arg 0 a)); + b "pixiEventGlobalY" (fun a -> Printf.sprintf "__as_pixiEventGlobalY(%s)" (arg 0 a)); (* ---- @pixi/ui (bindings #3) ---- *) b "pixiUiButtonNew" (fun a -> Printf.sprintf "__as_pixiUiButtonNew(%s)" (arg 0 a)); b "pixiUiButtonOnPress" (fun a -> Printf.sprintf "__as_pixiUiButtonOnPress(%s, %s)" (arg 0 a) (arg 1 a)); @@ -815,6 +851,12 @@ let () = b "pixiUiSwitchNew" (fun a -> Printf.sprintf "__as_pixiUiSwitchNew(%s)" (arg 0 a)); b "pixiUiSwitchOnChange" (fun a -> Printf.sprintf "__as_pixiUiSwitchOnChange(%s, %s)" (arg 0 a) (arg 1 a)); b "pixiUiSwitchAsContainer" (fun a -> Printf.sprintf "__as_pixiUiSwitchAsContainer(%s)" (arg 0 a)); + b "pixiUiProgressBarNew" (fun a -> Printf.sprintf "__as_pixiUiProgressBarNew(%s)" (arg 0 a)); + b "pixiUiProgressBarAsContainer" (fun a -> Printf.sprintf "__as_pixiUiProgressBarAsContainer(%s)" (arg 0 a)); + b "pixiUiListNew" (fun a -> Printf.sprintf "__as_pixiUiListNew(%s)" (arg 0 a)); + b "pixiUiListAsContainer" (fun a -> Printf.sprintf "__as_pixiUiListAsContainer(%s)" (arg 0 a)); + b "pixiUiInputNew" (fun a -> Printf.sprintf "__as_pixiUiInputNew(%s)" (arg 0 a)); + b "pixiUiInputAsContainer" (fun a -> Printf.sprintf "__as_pixiUiInputAsContainer(%s)" (arg 0 a)); (* ---- motion extras (bindings #4 follow-up) ---- *) b "motionAnimateMini" (fun a -> Printf.sprintf "__as_motionAnimateMini(%s, %s, %s)" (arg 0 a) (arg 1 a) (arg 2 a)); b "motionTween" (fun a -> Printf.sprintf "__as_motionTween(%s, %s, %s, %s)" (arg 0 a) (arg 1 a) (arg 2 a) (arg 3 a)); @@ -1296,14 +1338,7 @@ let rec gen_expr ctx (expr : expr) : string = | ExprReturn (Some e) -> iife ctx ("return " ^ gen_expr ctx e ^ ";") | ExprReturn None -> iife ctx "return Unit;" (* #459: break/continue lower to the corresponding JS keywords. The - wrapping IIFE pattern used for `return` doesn't work here — JS's - `break`/`continue` only target the nearest enclosing loop and an - IIFE wraps the keyword in a new function frame. Emit a bare - statement and rely on the parent block-flatten machinery. *) - | ExprBreak _ -> iife ctx "break;" - | ExprContinue _ -> iife ctx "continue;" - | ExprLambda { elam_params; elam_body; elam_ret_ty = _ } -> - let ps = List.map (fun (p : param) -> mangle p.p_name.name) elam_params in + wrapping Iram) -> mangle p.p_name.name) elam_params in "((" ^ String.concat ", " ps ^ ") => " ^ gen_expr ctx elam_body ^ ")" | ExprTry { et_body; et_catch; et_finally } -> gen_try ctx et_body et_catch et_finally @@ -1696,6 +1731,16 @@ let rec type_expr_name : type_expr -> string option = function | TyOwn t | TyRef (_, t) | TyMut (_, t) -> type_expr_name t | _ -> None +(* The struct (if any, among [known]) that [fd]'s first parameter is typed + as — i.e. [fd] is a receiver-first method of that struct. *) +let receiver_struct ~(known : (string, 'a) Hashtbl.t) (fd : fn_decl) + : (string * string) option = + match fd.fd_params withtion = function + | TyCon id | TyVar id -> Some id.name + | TyApp (id, _) -> Some id.name + | TyOwn t | TyRef (_, t) | TyMut (_, t) -> type_expr_name t + | _ -> None + (* The struct (if any, among [known]) that [fd]'s first parameter is typed as — i.e. [fd] is a receiver-first method of that struct. *) let receiver_struct ~(known : (string, 'a) Hashtbl.t) (fd : fn_decl) diff --git a/lib/effect.ml b/lib/effect.ml index 3f28e24b..def6e8cf 100644 --- a/lib/effect.ml +++ b/lib/effect.ml @@ -136,7 +136,7 @@ let v1_effects = [ "IO"; "Async"; "Partial"; "Throws"; "Mut" ] (** Reserved for v1.x — recognised so the names are not repurposed, not yet wired into the stdlib. *) -let reserved_effects = [ "Random"; "Time"; "Net" ] +let reserved_effects = [ "Random"; "Time"; "Net"; "File" ] (** Canonical registry name for a source effect name, or [None] if it is neither a v1 nor a reserved name. Callers additionally accept diff --git a/lib/resolve.ml b/lib/resolve.ml index 65a6b481..1a7ca993 100644 --- a/lib/resolve.ml +++ b/lib/resolve.ml @@ -63,6 +63,10 @@ let seed_builtins (symbols : Symbol.t) : unit = def "env_at"; def "arg_at"; (* WASI socket primitive (ADR-015 S6b, #180) *) def "net_shutdown"; + (* WASI filesystem (ADR-015 S5, #485) *) + def "file_open"; def "file_read"; def "file_fd_write"; def "file_close"; + (* WASI sockets follow-up (#487) *) + def "net_recv"; def "net_send"; def "net_accept"; (* String / char builtins *) def "len"; def "slice"; def "string_get"; def "string_sub"; def "string_find"; def "char_to_int"; def "int_to_char"; def "show"; diff --git a/lib/typecheck.ml b/lib/typecheck.ml index e38e3027..41ef95c4 100644 --- a/lib/typecheck.ml +++ b/lib/typecheck.ml @@ -1855,6 +1855,35 @@ let register_builtins (ctx : context) : unit = (TArrow (ty_int, QOmega, TArrow (ty_int, QOmega, ty_int, ESingleton "Net"), ESingleton "Net")); + (* ADR-015 S5 (#485): native wasi:filesystem. Effect row `File` + (reserved). file_open returns fd (>=0) or -errno; file_read + returns a length-prefixed string (empty on error); file_fd_write + / file_close return errno. *) + bind_var ctx "file_open" + (TArrow (ty_string, QOmega, + TArrow (ty_int, QOmega, ty_int, ESingleton "File"), + ESingleton "File")); + bind_var ctx "file_read" + (TArrow (ty_int, QOmega, + TArrow (ty_int, QOmega, ty_string, ESingleton "File"), + ESingleton "File")); + bind_var ctx "file_fd_write" + (TArrow (ty_int, QOmega, + TArrow (ty_string, QOmega, ty_int, ESingleton "File"), + ESingleton "File")); + bind_var ctx "file_close" + (TArrow (ty_int, QOmega, ty_int, ESingleton "File")); + (* #487: sock_recv / sock_send / sock_accept. Effect row `Net`. *) + bind_var ctx "net_recv" + (TArrow (ty_int, QOmega, + TArrow (ty_int, QOmega, ty_string, ESingleton "Net"), + ESingleton "Net")); + bind_var ctx "net_send" + (TArrow (ty_int, QOmega, + TArrow (ty_string, QOmega, ty_int, ESingleton "Net"), + ESingleton "Net")); + bind_var ctx "net_accept" + (TArrow (ty_int, QOmega, ty_int, ESingleton "Net")); bind_var ctx "eprint" (TArrow (ty_string, QOmega, ty_unit, ESingleton "IO")); bind_var ctx "eprintln" (TArrow (ty_string, QOmega, ty_unit, ESingleton "IO")); bind_var ctx "read_line" diff --git a/lib/wasi_runtime.ml b/lib/wasi_runtime.ml index 97e274ae..1d713f39 100644 --- a/lib/wasi_runtime.ml +++ b/lib/wasi_runtime.ml @@ -590,3 +590,358 @@ let gen_str_at_via_get (* --- Result: leave the string pointer on the stack. --- *) LocalGet result_local; ] + + +(** {1 ADR-015 S5 (#485) — native wasi:filesystem} + + Guest builtins [file_open] / [file_read] / [file_fd_write] / + [file_close] lower to preview1 [path_open] / [fd_read] / the + already-emitted [fd_write] / [fd_close]. The command adapter + bridges those to [wasi:filesystem] on a real host. Path bytes + are taken from the AffineScript length-prefixed string layout + ([len: i32][bytes...]), same encoding [env_at]/[arg_at] use. + + Convention: [file_open] uses dirfd 3, the first [wasmtime --dir] + preopen. oflags are passed through (0 = existing, 1 = CREAT, + 9 = CREAT|TRUNC). Success returns the new fd (>= 0); failure + returns [-errno]. *) + +let fd_preopen = 3l + +(** FD_READ|FD_WRITE|FD_SEEK|FD_TELL|PATH_CREATE_FILE|PATH_OPEN *) +let rights_file = 9318L + +let create_path_open_import () : import * func_type = + let func_type = { + ft_params = [I32; I32; I32; I32; I32; I64; I64; I32; I32]; + ft_results = [I32]; + } in + let import = { + i_module = "wasi_snapshot_preview1"; + i_name = "path_open"; + i_desc = ImportFunc 0; + } in + (import, func_type) + +let create_fd_read_import () : import * func_type = + let func_type = { + ft_params = [I32; I32; I32; I32]; + ft_results = [I32]; + } in + let import = { + i_module = "wasi_snapshot_preview1"; + i_name = "fd_read"; + i_desc = ImportFunc 0; + } in + (import, func_type) + +let create_fd_close_import () : import * func_type = + let func_type = { + ft_params = [I32]; + ft_results = [I32]; + } in + let import = { + i_module = "wasi_snapshot_preview1"; + i_name = "fd_close"; + i_desc = ImportFunc 0; + } in + (import, func_type) + +(** [file_open(path, oflags) -> fd]. [path_local] holds the AS string + pointer; [oflags_local] the oflags i32. Leaves the new fd (or + [-errno]) on the stack. *) +let gen_file_open + (heap_ptr_global : int) + (path_local : int) + (oflags_local : int) + (opened_fd_local : int) + (errno_local : int) + (path_open_idx : int) + : instr list = + [ + GlobalGet heap_ptr_global; + I32Const 4l; I32Add; + GlobalSet heap_ptr_global; + GlobalGet heap_ptr_global; + I32Const 4l; I32Sub; + LocalSet opened_fd_local; + I32Const fd_preopen; + I32Const 0l; + LocalGet path_local; I32Const 4l; I32Add; + LocalGet path_local; I32Load (2, 0); + LocalGet oflags_local; + I64Const rights_file; + I64Const 0L; + I32Const 0l; + LocalGet opened_fd_local; + Call path_open_idx; + LocalTee errno_local; + I32Eqz; + If (BtType I32, + [ LocalGet opened_fd_local; I32Load (2, 0) ], + [ I32Const 0l; LocalGet errno_local; I32Sub ]); + ] + +(** [file_fd_write(fd, data) -> errno]. Reuses the always-present + [fd_write] import (idx 0) with a caller-supplied fd instead of + stdout. Leaves errno on the stack. *) +let gen_file_fd_write + (heap_ptr_global : int) + (fd_local : int) + (str_ptr_local : int) + (fd_write_idx : int) + (temp_local : int) + : instr list = + [ + GlobalGet heap_ptr_global; + I32Const 12l; I32Add; + GlobalSet heap_ptr_global; + GlobalGet heap_ptr_global; + I32Const 12l; I32Sub; + LocalSet temp_local; + LocalGet temp_local; + LocalGet str_ptr_local; I32Const 4l; I32Add; + I32Store (2, 0); + LocalGet temp_local; I32Const 4l; I32Add; + LocalGet str_ptr_local; I32Load (2, 0); + I32Store (2, 0); + LocalGet fd_local; + LocalGet temp_local; + I32Const 1l; + LocalGet temp_local; I32Const 8l; I32Add; + Call fd_write_idx; + ] + +(** [file_read(fd, max_len) -> String]. Allocates an iovec + byte + buffer, calls [fd_read], then copies [nread] bytes into a + length-prefixed AS string. On errno != 0 returns the empty string. *) +let gen_file_read + (heap_ptr_global : int) + (fd_local : int) + (max_local : int) + (iov_local : int) + (buf_local : int) + (n_local : int) + (src_local : int) + (dst_local : int) + (result_local : int) + (fd_read_idx : int) + : instr list = + [ + GlobalGet heap_ptr_global; + LocalSet iov_local; + GlobalGet heap_ptr_global; + I32Const 16l; I32Add; + LocalSet buf_local; + GlobalGet heap_ptr_global; + I32Const 16l; LocalGet max_local; I32Add; I32Add; + GlobalSet heap_ptr_global; + LocalGet iov_local; LocalGet buf_local; I32Store (2, 0); + LocalGet iov_local; LocalGet max_local; I32Store (2, 4); + LocalGet fd_local; + LocalGet iov_local; + I32Const 1l; + LocalGet iov_local; I32Const 8l; I32Add; + Call fd_read_idx; + LocalSet n_local; + LocalGet n_local; I32Eqz; + If (BtType I32, + [ + LocalGet iov_local; I32Load (2, 8); LocalSet n_local; + GlobalGet heap_ptr_global; LocalSet result_local; + GlobalGet heap_ptr_global; + I32Const 4l; LocalGet n_local; I32Add; I32Add; + GlobalSet heap_ptr_global; + LocalGet result_local; LocalGet n_local; I32Store (2, 0); + LocalGet buf_local; LocalSet src_local; + LocalGet result_local; I32Const 4l; I32Add; LocalSet dst_local; + Block (BtEmpty, [ + Loop (BtEmpty, [ + LocalGet n_local; I32Eqz; BrIf 1; + LocalGet dst_local; + LocalGet src_local; I32Load8U (0, 0); + I32Store8 (0, 0); + LocalGet src_local; I32Const 1l; I32Add; LocalSet src_local; + LocalGet dst_local; I32Const 1l; I32Add; LocalSet dst_local; + LocalGet n_local; I32Const 1l; I32Sub; LocalSet n_local; + Br 0 + ]) + ]); + LocalGet result_local + ], + [ + GlobalGet heap_ptr_global; LocalSet result_local; + GlobalGet heap_ptr_global; I32Const 4l; I32Add; + GlobalSet heap_ptr_global; + LocalGet result_local; I32Const 0l; I32Store (2, 0); + LocalGet result_local + ]); + ] + +let gen_file_close (fd_close_idx : int) : instr list = + [ Call fd_close_idx ] + +(** {1 ADR-015 follow-up (#487) — sock_recv / sock_send / sock_accept} + + Byte-level buffer marshalling mirrors [gen_str_at_via_get] / + [gen_file_read]. Effect row stays [Net]. The command adapter + already bridges preview1 sockets to [wasi:sockets/tcp]. *) + +let create_sock_recv_import () : import * func_type = + let func_type = { + ft_params = [I32; I32; I32; I32; I32; I32]; + ft_results = [I32]; + } in + let import = { + i_module = "wasi_snapshot_preview1"; + i_name = "sock_recv"; + i_desc = ImportFunc 0; + } in + (import, func_type) + +let create_sock_send_import () : import * func_type = + let func_type = { + ft_params = [I32; I32; I32; I32; I32]; + ft_results = [I32]; + } in + let import = { + i_module = "wasi_snapshot_preview1"; + i_name = "sock_send"; + i_desc = ImportFunc 0; + } in + (import, func_type) + +let create_sock_accept_import () : import * func_type = + let func_type = { + ft_params = [I32; I32; I32]; + ft_results = [I32]; + } in + let import = { + i_module = "wasi_snapshot_preview1"; + i_name = "sock_accept"; + i_desc = ImportFunc 0; + } in + (import, func_type) + +(** [net_recv(fd, max_len) -> String]. Same shape as [file_read] but + calls [sock_recv] (ri_flags = 0). *) +let gen_net_recv + (heap_ptr_global : int) + (fd_local : int) + (max_local : int) + (iov_local : int) + (buf_local : int) + (n_local : int) + (src_local : int) + (dst_local : int) + (result_local : int) + (sock_recv_idx : int) + : instr list = + [ + GlobalGet heap_ptr_global; + LocalSet iov_local; + GlobalGet heap_ptr_global; + I32Const 16l; I32Add; + LocalSet buf_local; + GlobalGet heap_ptr_global; + I32Const 16l; LocalGet max_local; I32Add; I32Add; + GlobalSet heap_ptr_global; + LocalGet iov_local; LocalGet buf_local; I32Store (2, 0); + LocalGet iov_local; LocalGet max_local; I32Store (2, 4); + LocalGet fd_local; + LocalGet iov_local; + I32Const 1l; + I32Const 0l; + LocalGet iov_local; I32Const 8l; I32Add; + LocalGet iov_local; I32Const 12l; I32Add; + Call sock_recv_idx; + LocalSet n_local; + LocalGet n_local; I32Eqz; + If (BtType I32, + [ + LocalGet iov_local; I32Load (2, 8); LocalSet n_local; + GlobalGet heap_ptr_global; LocalSet result_local; + GlobalGet heap_ptr_global; + I32Const 4l; LocalGet n_local; I32Add; I32Add; + GlobalSet heap_ptr_global; + LocalGet result_local; LocalGet n_local; I32Store (2, 0); + LocalGet buf_local; LocalSet src_local; + LocalGet result_local; I32Const 4l; I32Add; LocalSet dst_local; + Block (BtEmpty, [ + Loop (BtEmpty, [ + LocalGet n_local; I32Eqz; BrIf 1; + LocalGet dst_local; + LocalGet src_local; I32Load8U (0, 0); + I32Store8 (0, 0); + LocalGet src_local; I32Const 1l; I32Add; LocalSet src_local; + LocalGet dst_local; I32Const 1l; I32Add; LocalSet dst_local; + LocalGet n_local; I32Const 1l; I32Sub; LocalSet n_local; + Br 0 + ]) + ]); + LocalGet result_local + ], + [ + GlobalGet heap_ptr_global; LocalSet result_local; + GlobalGet heap_ptr_global; I32Const 4l; I32Add; + GlobalSet heap_ptr_global; + LocalGet result_local; I32Const 0l; I32Store (2, 0); + LocalGet result_local + ]); + ] + +(** [net_send(fd, data) -> errno]. iovec over the AS string bytes. *) +let gen_net_send + (heap_ptr_global : int) + (fd_local : int) + (str_ptr_local : int) + (temp_local : int) + (sock_send_idx : int) + : instr list = + [ + GlobalGet heap_ptr_global; + I32Const 12l; I32Add; + GlobalSet heap_ptr_global; + GlobalGet heap_ptr_global; + I32Const 12l; I32Sub; + LocalSet temp_local; + LocalGet temp_local; + LocalGet str_ptr_local; I32Const 4l; I32Add; + I32Store (2, 0); + LocalGet temp_local; I32Const 4l; I32Add; + LocalGet str_ptr_local; I32Load (2, 0); + I32Store (2, 0); + LocalGet fd_local; + LocalGet temp_local; + I32Const 1l; + I32Const 0l; + LocalGet temp_local; I32Const 8l; I32Add; + Call sock_send_idx; + ] + +(** [net_accept(fd) -> new_fd]. [flags] = 0. Success returns the new + fd; failure returns [-errno]. *) +let gen_net_accept + (heap_ptr_global : int) + (fd_local : int) + (opened_fd_local : int) + (errno_local : int) + (sock_accept_idx : int) + : instr list = + [ + GlobalGet heap_ptr_global; + I32Const 4l; I32Add; + GlobalSet heap_ptr_global; + GlobalGet heap_ptr_global; + I32Const 4l; I32Sub; + LocalSet opened_fd_local; + LocalGet fd_local; + I32Const 0l; + LocalGet opened_fd_local; + Call sock_accept_idx; + LocalTee errno_local; + I32Eqz; + If (BtType I32, + [ LocalGet opened_fd_local; I32Load (2, 0) ], + [ I32Const 0l; LocalGet errno_local; I32Sub ]); + ] diff --git a/stdlib/Console.affine b/stdlib/Console.affine new file mode 100644 index 00000000..050e8f91 --- /dev/null +++ b/stdlib/Console.affine @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 hyperpolymath +// +// Console.affine — #56-A typed console surface. +// +// `println` / `eprintln` already lower to `console.log` / `console.error` +// on the JS/Deno backends. These wrappers give the pilot's +// `Console.log(...)` name a home so migration does not accumulate +// `%raw` for the four console methods. + +module Console; + +pub fn log(msg: String) -> () { + println(msg); +} + +pub fn info(msg: String) -> () { + println(msg); +} + +pub fn warn(msg: String) -> () { + eprintln(msg); +} + +pub fn error(msg: String) -> () { + eprintln(msg); +} diff --git a/stdlib/Dom.affine b/stdlib/Dom.affine new file mode 100644 index 00000000..278b840a --- /dev/null +++ b/stdlib/Dom.affine @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 hyperpolymath +// +// Dom.affine — #56-A typed DOM convenience surface. +// +// Canonical `module Dom` so `use Dom::{div, h1, p, text, replaceBody}` +// resolves from stdlib/. Constructors are pure VNode builders (no host +// FFI). Runtime mount lives in affinescript-dom (the reconciler); this +// module is the compile-time target for idaptik's pilot syntax. +// +// Attrs are `[(String, String)]` — the same tuple-list shape as +// `Http.affine` headers. Record-literal attrs (`#{style: ...}`) are a +// follow-up once that surface is widely supported. + +module Dom; + +pub enum VNode { + VText(String), + VElem(String, [(String, String)], [VNode]) +} + +pub fn text(content: String) -> VNode = VText(content); + +pub fn h(tag: String, attrs: [(String, String)], children: [VNode]) -> VNode = + VElem(tag, attrs, children); + +pub fn div(attrs: [(String, String)], children: [VNode]) -> VNode = + h("div", attrs, children); + +pub fn h1(attrs: [(String, String)], content: String) -> VNode = + h("h1", attrs, [text(content)]); + +pub fn h2(attrs: [(String, String)], content: String) -> VNode = + h("h2", attrs, [text(content)]); + +pub fn h3(attrs: [(String, String)], content: String) -> VNode = + h("h3", attrs, [text(content)]); + +pub fn p(attrs: [(String, String)], content: String) -> VNode = + h("p", attrs, [text(content)]); + +pub fn span(attrs: [(String, String)], content: String) -> VNode = + h("span", attrs, [text(content)]); + +pub fn button(attrs: [(String, String)], content: String) -> VNode = + h("button", attrs, [text(content)]); + +pub fn input(attrs: [(String, String)]) -> VNode = + h("input", attrs, []); + +pub fn a(attrs: [(String, String)], content: String) -> VNode = + h("a", attrs, [text(content)]); + +pub fn img(attrs: [(String, String)]) -> VNode = + h("img", attrs, []); + +pub fn ul(attrs: [(String, String)], children: [VNode]) -> VNode = + h("ul", attrs, children); + +pub fn li(attrs: [(String, String)], children: [VNode]) -> VNode = + h("li", attrs, children); diff --git a/stdlib/Pixi.affine b/stdlib/Pixi.affine index ecac30b1..7ea895dd 100644 --- a/stdlib/Pixi.affine +++ b/stdlib/Pixi.affine @@ -46,6 +46,13 @@ pub extern type Graphics; pub extern type Text; pub extern type Texture; pub extern type Ticker; +pub extern type Point; +pub extern type Rectangle; +pub extern type Circle; +pub extern type ObservablePoint; +pub extern type NineSliceSprite; +pub extern type BlurFilter; +pub extern type Assets; // ── Application ──────────────────────────────────────────────────── @@ -193,3 +200,47 @@ pub extern fn pixiTickerStart(t: Ticker) -> Int; /// `ticker.stop()`. Returns 0. pub extern fn pixiTickerStop(t: Ticker) -> Int; + +// ── Geometry + filters + assets (#56 gap-fill) ───────────────────── + +/// `new PIXI.Point(x, y)`. +pub extern fn pixiPointNew(x: Float, y: Float) -> Point; + +/// `new PIXI.Rectangle(x, y, w, h)`. +pub extern fn pixiRectangleNew(x: Float, y: Float, w: Float, h: Float) -> Rectangle; + +/// `new PIXI.Circle(x, y, r)`. +pub extern fn pixiCircleNew(x: Float, y: Float, r: Float) -> Circle; + +/// `s.tint = color`. Returns 0. +pub extern fn pixiSpriteSetTint(s: Sprite, color: Int) -> Int; + +/// `s.blendMode = mode`. Returns 0. +pub extern fn pixiSpriteSetBlendMode(s: Sprite, mode: String) -> Int; + +/// `c.hitArea = area`. Returns 0. +pub extern fn pixiContainerSetHitArea(c: Container, area: Rectangle) -> Int; + +/// `c.mask = mask`. Returns 0. +pub extern fn pixiContainerSetMask(c: Container, mask: Container) -> Int; + +/// `new PIXI.BlurFilter({ strength })`. +pub extern fn pixiBlurFilterNew(strength: Float) -> BlurFilter; + +/// `c.filters = [filter]`. Returns 0. +pub extern fn pixiContainerSetFilter(c: Container, filter: BlurFilter) -> Int; + +/// `new PIXI.NineSliceSprite(texture)`. +pub extern fn pixiNineSliceSpriteFrom(texture: Texture) -> NineSliceSprite; + +/// Upcast NineSliceSprite → Container. Zero-cost identity lowering. +pub extern fn pixiNineSliceSpriteAsContainer(s: NineSliceSprite) -> Container; + +/// `PIXI.Assets.load(url)` — async texture load. +pub extern fn pixiAssetsLoad(url: String) -> Texture / { Async }; + +/// `event.global.x` on a FederatedPointerEvent exposed as Json. +pub extern fn pixiEventGlobalX(e: Json) -> Float; + +/// `event.global.y` on a FederatedPointerEvent exposed as Json. +pub extern fn pixiEventGlobalY(e: Json) -> Float; diff --git a/stdlib/PixiSound.affine b/stdlib/PixiSound.affine index 08c5f6d0..110db7ec 100644 --- a/stdlib/PixiSound.affine +++ b/stdlib/PixiSound.affine @@ -71,3 +71,9 @@ pub extern fn pixiSoundSetVolume(s: Sound, vol: Float) -> Int; /// `sound.loop = loop` — enable or disable looping. When `true`, /// playback restarts from the beginning on completion. Returns 0. pub extern fn pixiSoundSetLoop(s: Sound, loop: Bool) -> Int; + +/// `sound.isPlaying` — true while the instance is currently playing. +pub extern fn pixiSoundIsPlaying(s: Sound) -> Bool; + +/// `sound.duration` — decoded length in seconds (0 if still loading). +pub extern fn pixiSoundDuration(s: Sound) -> Float; diff --git a/stdlib/PixiUI.affine b/stdlib/PixiUI.affine index a5cc853f..e31c06cd 100644 --- a/stdlib/PixiUI.affine +++ b/stdlib/PixiUI.affine @@ -15,7 +15,7 @@ // graph via `Pixi::pixiContainerAddChild`). // // Deferred (follow-ups): -// - Input — text-entry, focus, blur, value accessor +// - Input value/focus/blur accessors (ctor + upcast landed #56) // - ScrollBox — scroll position, content add/remove, viewport // - Full event surface (onHover / onOut / onDown / onUp) // - FancyButton style accessors (textures per state) @@ -115,3 +115,27 @@ pub extern fn pixiUiSwitchOnChange(sw: Switch, callback: Json) -> Int; /// Upcast a Switch to its Container superclass. Zero-cost identity /// lowering. pub extern fn pixiUiSwitchAsContainer(sw: Switch) -> Container; + +// ── ProgressBar / List / Input (#56 gap-fill) ────────────────────── + +pub extern type ProgressBar; +pub extern type List; +pub extern type Input; + +/// `new PixiUI.ProgressBar(options)`. +pub extern fn pixiUiProgressBarNew(options: Json) -> ProgressBar; + +/// Upcast ProgressBar → Container. Zero-cost identity lowering. +pub extern fn pixiUiProgressBarAsContainer(p: ProgressBar) -> Container; + +/// `new PixiUI.List(options)`. +pub extern fn pixiUiListNew(options: Json) -> List; + +/// Upcast List → Container. Zero-cost identity lowering. +pub extern fn pixiUiListAsContainer(l: List) -> Container; + +/// `new PixiUI.Input(options)`. +pub extern fn pixiUiInputNew(options: Json) -> Input; + +/// Upcast Input → Container. Zero-cost identity lowering. +pub extern fn pixiUiInputAsContainer(i: Input) -> Container; diff --git a/tests/codegen-deno/pixi_smoke.affine b/tests/codegen-deno/pixi_smoke.affine index 6f2e3749..7ad3086e 100644 --- a/tests/codegen-deno/pixi_smoke.affine +++ b/tests/codegen-deno/pixi_smoke.affine @@ -6,7 +6,7 @@ // extern shape (constructor, mutator, async init, upcast). use Deno::{Json}; -use Pixi::{Application, Container, Sprite, Graphics, Text, Texture, pixiAppInit, pixiAppStage, pixiContainerNew, pixiContainerAddChild, pixiContainerSetPosition, pixiContainerSetVisible, pixiContainerSetScale, pixiContainerSetPivot, pixiContainerSetRotation, pixiContainerSetAlpha, pixiContainerSetZIndex, pixiContainerSetSortableChildren, pixiContainerSetEventMode, pixiContainerSetCursor, pixiContainerOn, pixiContainerOff, pixiSpriteFrom, pixiSpriteSetAnchor, pixiSpriteAsContainer, pixiTextureFromUrl, pixiGraphicsNew, pixiGraphicsRect, pixiGraphicsFill, pixiGraphicsAsContainer}; +use Pixi::{Application, Container, Sprite, Graphics, Text, Texture, pixiAppInit, pixiAppStage, pixiContainerNew, pixiContainerAddChild, pixiContainerSetPosition, pixiContainerSetVisible, pixiContainerSetScale, pixiContainerSetPivot, pixiContainerSetRotation, pixiContainerSetAlpha, pixiContainerSetZIndex, pixiContainerSetSortableChildren, pixiContainerSetEventMode, pixiContainerSetCursor, pixiContainerOn, pixiContainerOff, pixiSpriteFrom, pixiSpriteSetAnchor, pixiSpriteAsContainer, pixiTextureFromUrl, pixiGraphicsNew, pixiGraphicsRect, pixiGraphicsFill, pixiGraphicsAsContainer, pixiPointNew, pixiRectangleNew, pixiCircleNew, pixiSpriteSetTint, pixiSpriteSetBlendMode, pixiContainerSetHitArea, pixiContainerSetMask, pixiBlurFilterNew, pixiContainerSetFilter, pixiNineSliceSpriteFrom, pixiNineSliceSpriteAsContainer, pixiEventGlobalX, pixiEventGlobalY}; pub fn smokeInit(options: Json) -> Application / { Async } = pixiAppInit(options); @@ -51,3 +51,25 @@ pub fn smokeAccessorsFlow(app: Application, url: String, onDown: Json, onMove: J pixiContainerOff(spriteC, "pointermove", onMove); 0 } + +/// #56 gap-fill: geometry, tint/blend, hitArea/mask, blur, nine-slice, +/// FederatedPointerEvent global coords. +pub fn smokeGapFill(app: Application, url: String, event: Json) -> Int { + let _pt = pixiPointNew(1.0, 2.0); + let rect = pixiRectangleNew(0.0, 0.0, 16.0, 16.0); + let _circ = pixiCircleNew(4.0, 5.0, 6.0); + let texture = pixiTextureFromUrl(url); + let sprite = pixiSpriteFrom(texture); + pixiSpriteSetTint(sprite, 0xffffff); + pixiSpriteSetBlendMode(sprite, "normal"); + let spriteC = pixiSpriteAsContainer(sprite); + pixiContainerSetHitArea(spriteC, rect); + pixiContainerSetMask(spriteC, pixiContainerNew()); + let blur = pixiBlurFilterNew(2.0); + pixiContainerSetFilter(spriteC, blur); + let nine = pixiNineSliceSpriteFrom(texture); + pixiContainerAddChild(pixiAppStage(app), pixiNineSliceSpriteAsContainer(nine)); + let _x = pixiEventGlobalX(event); + let _y = pixiEventGlobalY(event); + 0 +} diff --git a/tests/codegen-deno/pixi_smoke.harness.mjs b/tests/codegen-deno/pixi_smoke.harness.mjs index 3066d4fa..70ea63db 100644 --- a/tests/codegen-deno/pixi_smoke.harness.mjs +++ b/tests/codegen-deno/pixi_smoke.harness.mjs @@ -63,6 +63,27 @@ class MockGraphics extends MockContainer { clear() { this.paths = []; this.fills = []; } } +class MockPointCtor { + constructor(x = 0, y = 0) { this.x = x; this.y = y; } + set(x, y) { this.x = x; this.y = y; } +} + +class MockRectangle { + constructor(x, y, w, h) { this.x = x; this.y = y; this.width = w; this.height = h; } +} + +class MockCircle { + constructor(x, y, r) { this.x = x; this.y = y; this.radius = r; } +} + +class MockBlurFilter { + constructor(opts) { this.strength = opts?.strength ?? 0; } +} + +class MockNineSliceSprite extends MockContainer { + constructor(texture) { super(); this.texture = texture; } +} + class MockText extends MockContainer { constructor(opts) { super(); this.text = opts.text ?? ""; this.style = opts.style; } } @@ -83,12 +104,17 @@ globalThis.__as_pixi = { Sprite: MockSprite, Graphics: MockGraphics, Text: MockText, + Point: MockPointCtor, + Rectangle: MockRectangle, + Circle: MockCircle, + BlurFilter: MockBlurFilter, + NineSliceSprite: MockNineSliceSprite, Texture: { from(url) { textureUrls.push(url); return { __mockTexture: true, url }; }, }, }; -const { smokeInit, smokeSpriteFlow, smokeGraphicsFlow, smokeAccessorsFlow } = await import("./pixi_smoke.deno.js"); +const { smokeInit, smokeSpriteFlow, smokeGraphicsFlow, smokeAccessorsFlow, smokeGapFill } = await import("./pixi_smoke.deno.js"); // Async init returns an Application after `await app.init(options)` const app = await smokeInit({ width: 800, height: 600, backgroundColor: 0x1099bb }); @@ -131,4 +157,11 @@ assert.equal(aSprite.handlers.get("pointerdown").length, 1, "pointerdown handler assert.equal(aSprite.handlers.get("pointermove").length, 0, "pointermove handler off after off()"); assert.equal(aSprite.handlers.get("pointerdown")[0], onDown, "pointerdown handler identity preserved"); +const gapEvent = { global: { x: 11, y: 22 } }; +assert.equal(smokeGapFill(app, "/assets/nine.png", gapEvent), 0, "smokeGapFill returns 0"); +assert.equal(textureUrls.length, 3, "gap-fill Texture.from recorded"); +assert.equal(app.stage.children.length, 4, "nine-slice added as fourth child"); +const nine = app.stage.children[3]; +assert.ok(nine instanceof MockNineSliceSprite, "nine-slice upcast is identity"); + console.log("pixi_smoke.harness.mjs OK"); diff --git a/tests/codegen-deno/pixisound_smoke.affine b/tests/codegen-deno/pixisound_smoke.affine index 20e9b9f8..03ea282d 100644 --- a/tests/codegen-deno/pixisound_smoke.affine +++ b/tests/codegen-deno/pixisound_smoke.affine @@ -27,3 +27,9 @@ pub fn smokeSetVolume(s: Sound, vol: Float) -> Int = pub fn smokeSetLoop(s: Sound, loop: Bool) -> Int = pixiSoundSetLoop(s, loop); + +pub fn smokeIsPlaying(s: Sound) -> Bool = + pixiSoundIsPlaying(s); + +pub fn smokeDuration(s: Sound) -> Float = + pixiSoundDuration(s); diff --git a/tests/codegen-deno/pixisound_smoke.harness.mjs b/tests/codegen-deno/pixisound_smoke.harness.mjs index 402ecf45..2d2dfbd2 100644 --- a/tests/codegen-deno/pixisound_smoke.harness.mjs +++ b/tests/codegen-deno/pixisound_smoke.harness.mjs @@ -17,6 +17,8 @@ class MockSound { this.resumeCount = 0; this.volume = 1.0; this.loop = false; + this.isPlaying = false; + this.duration = 12.5; } play() { this.playCount += 1; } stop() { this.stopCount += 1; } @@ -64,4 +66,7 @@ assert.equal(s.loop, true, "loop field updated"); assert.equal(smokeSetLoop(s, false), 0, "setLoop(false) returns 0"); assert.equal(s.loop, false, "loop field cleared"); +assert.equal(smokeIsPlaying(s), false, "isPlaying reads host field"); +assert.equal(smokeDuration(s), 12.5, "duration reads host field"); + console.log("pixisound_smoke.harness.mjs OK"); diff --git a/tests/codegen-deno/pixiui_smoke.affine b/tests/codegen-deno/pixiui_smoke.affine index f6f75583..0bba9422 100644 --- a/tests/codegen-deno/pixiui_smoke.affine +++ b/tests/codegen-deno/pixiui_smoke.affine @@ -6,7 +6,7 @@ // registration, and upcast → Container for each MVP component. use Deno::{Json}; -use PixiUI::{Button, FancyButton, Slider, Switch, Container, pixiUiButtonNew, pixiUiButtonOnPress, pixiUiButtonAsContainer, pixiUiFancyButtonNew, pixiUiFancyButtonAsContainer, pixiUiSliderNew, pixiUiSliderOnUpdate, pixiUiSliderAsContainer, pixiUiSwitchNew, pixiUiSwitchOnChange, pixiUiSwitchAsContainer}; +use PixiUI::{Button, FancyButton, Slider, Switch, Container, pixiUiButtonNew, pixiUiButtonOnPress, pixiUiButtonAsContainer, pixiUiFancyButtonNew, pixiUiFancyButtonAsContainer, pixiUiSliderNew, pixiUiSliderOnUpdate, pixiUiSliderAsContainer, pixiUiSwitchNew, pixiUiSwitchOnChange, pixiUiSwitchAsContainer, pixiUiProgressBarNew, pixiUiProgressBarAsContainer, pixiUiListNew, pixiUiListAsContainer, pixiUiInputNew, pixiUiInputAsContainer}; pub fn smokeButton(options: Json, callback: Json) -> Container { let b = pixiUiButtonNew(options); @@ -30,3 +30,15 @@ pub fn smokeSwitch(options: Json, callback: Json) -> Container { pixiUiSwitchOnChange(sw, callback); pixiUiSwitchAsContainer(sw) } + +pub fn smokeProgressBar(options: Json) -> Container { + pixiUiProgressBarAsContainer(pixiUiProgressBarNew(options)) +} + +pub fn smokeList(options: Json) -> Container { + pixiUiListAsContainer(pixiUiListNew(options)) +} + +pub fn smokeInput(options: Json) -> Container { + pixiUiInputAsContainer(pixiUiInputNew(options)) +} diff --git a/tests/codegen-deno/pixiui_smoke.harness.mjs b/tests/codegen-deno/pixiui_smoke.harness.mjs index 4de88eb6..4e9bdc43 100644 --- a/tests/codegen-deno/pixiui_smoke.harness.mjs +++ b/tests/codegen-deno/pixiui_smoke.harness.mjs @@ -9,7 +9,7 @@ import assert from "node:assert/strict"; -const ctorCalls = { Button: [], FancyButton: [], Slider: [], Switch: [] }; +const ctorCalls = { Button: [], FancyButton: [], Slider: [], Switch: [], ProgressBar: [], List: [], Input: [] }; const onPressRegs = []; const onUpdateRegs = []; const onChangeRegs = []; @@ -49,14 +49,21 @@ class MockSwitch { } } +class MockProgressBar { constructor(options) { ctorCalls.ProgressBar.push(options); } } +class MockList { constructor(options) { ctorCalls.List.push(options); } } +class MockInput { constructor(options) { ctorCalls.Input.push(options); } } + globalThis.__as_pixi_ui = { Button: MockButton, FancyButton: MockFancyButton, Slider: MockSlider, Switch: MockSwitch, + ProgressBar: MockProgressBar, + List: MockList, + Input: MockInput, }; -const { smokeButton, smokeFancyButton, smokeSlider, smokeSwitch } = +const { smokeButton, smokeFancyButton, smokeSlider, smokeSwitch, smokeProgressBar, smokeList, smokeInput } = await import("./pixiui_smoke.deno.js"); // ── Button: ctor + onPress + upcast ──────────────────────────────── @@ -92,4 +99,17 @@ assert.equal(onChangeRegs.length, 1, "onChange.connect called once"); assert.equal(onChangeRegs[0], switchChangeCb, "onChange callback identity preserved"); assert.ok(switchContainer instanceof MockSwitch, "Switch upcast is identity"); +const pb = smokeProgressBar({ progress: 0.4 }); +assert.equal(ctorCalls.ProgressBar.length, 1, "ProgressBar ctor called once"); +assert.deepEqual(ctorCalls.ProgressBar[0], { progress: 0.4 }, "ProgressBar options reach host"); +assert.ok(pb instanceof MockProgressBar, "ProgressBar upcast is identity"); + +const list = smokeList({ type: "vertical" }); +assert.equal(ctorCalls.List.length, 1, "List ctor called once"); +assert.ok(list instanceof MockList, "List upcast is identity"); + +const input = smokeInput({ placeholder: "name" }); +assert.equal(ctorCalls.Input.length, 1, "Input ctor called once"); +assert.ok(input instanceof MockInput, "Input upcast is identity"); + console.log("pixiui_smoke.harness.mjs OK"); diff --git a/tests/codegen/dom_pilot_surface.affine b/tests/codegen/dom_pilot_surface.affine new file mode 100644 index 00000000..6412732f --- /dev/null +++ b/tests/codegen/dom_pilot_surface.affine @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MPL-2.0 +// #56-A — compile-pass for the Dom convenience constructors. +// Uses the in-file surface (not stdlib/) so the wasm codegen suite +// stays self-contained; stdlib/Dom.affine is the AOT-gated twin. + +pub enum VNode { + VText(String), + VElem(String, [(String, String)], [VNode]) +} + +pub fn text(content: String) -> VNode = VText(content); + +pub fn h(tag: String, attrs: [(String, String)], children: [VNode]) -> VNode = + VElem(tag, attrs, children); + +pub fn div(attrs: [(String, String)], children: [VNode]) -> VNode = + h("div", attrs, children); + +pub fn h1(attrs: [(String, String)], content: String) -> VNode = + h("h1", attrs, [text(content)]); + +pub fn p(attrs: [(String, String)], content: String) -> VNode = + h("p", attrs, [text(content)]); + +pub fn button(attrs: [(String, String)], content: String) -> VNode = + h("button", attrs, [text(content)]); + +pub fn input(attrs: [(String, String)]) -> VNode = + h("input", attrs, []); + +pub fn main() -> Int { + let _n = div([], [h1([], "err"), p([], "boom"), button([], "ok"), input([])]); + 0 +} diff --git a/tests/codegen/file_open.affine b/tests/codegen/file_open.affine new file mode 100644 index 00000000..64e4ff53 --- /dev/null +++ b/tests/codegen/file_open.affine @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MPL-2.0 +// ADR-015 S5 (#485): file_open(path, oflags) smoke. Lowers to +// wasi_snapshot_preview1.path_open against dirfd 3 (first --dir +// preopen). The harness stubs path_open and returns a known fd. +pub fn main() -> Int / { File } { + file_open("hello.txt", 1) // 1 = CREAT +} diff --git a/tests/codegen/file_roundtrip.affine b/tests/codegen/file_roundtrip.affine new file mode 100644 index 00000000..3115479b --- /dev/null +++ b/tests/codegen/file_roundtrip.affine @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MPL-2.0 +// ADR-015 S5 (#485): open / write / close / open / read round-trip +// surface. The harness stubs the WASI imports; this fixture proves +// codegen emits path_open + fd_write + fd_close + fd_read together +// without index collisions. +pub fn main() -> Int / { File } { + let fd = file_open("roundtrip.txt", 9); // CREAT|TRUNC + file_fd_write(fd, "ok"); + file_close(fd); + let fd2 = file_open("roundtrip.txt", 0); + let s = file_read(fd2, 8); + file_close(fd2); + string_length(s) +} diff --git a/tests/codegen/net_accept.affine b/tests/codegen/net_accept.affine new file mode 100644 index 00000000..84e2d5a6 --- /dev/null +++ b/tests/codegen/net_accept.affine @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: MPL-2.0 +// #487: net_accept(fd) -> Int new_fd (or -errno). +pub fn main() -> Int / { Net } { + net_accept(3) +} diff --git a/tests/codegen/net_recv.affine b/tests/codegen/net_recv.affine new file mode 100644 index 00000000..b433a08d --- /dev/null +++ b/tests/codegen/net_recv.affine @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: MPL-2.0 +// #487: net_recv(fd, max_len) -> String. Harness stubs sock_recv. +pub fn main() -> Int / { Net } { + string_length(net_recv(4, 16)) +} diff --git a/tests/codegen/net_send.affine b/tests/codegen/net_send.affine new file mode 100644 index 00000000..af0a656f --- /dev/null +++ b/tests/codegen/net_send.affine @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: MPL-2.0 +// #487: net_send(fd, data) -> Int errno. +pub fn main() -> Int / { Net } { + net_send(4, "ping") +} diff --git a/tests/codegen/test_dom_pilot_surface.mjs b/tests/codegen/test_dom_pilot_surface.mjs new file mode 100644 index 00000000..cd5ce49f --- /dev/null +++ b/tests/codegen/test_dom_pilot_surface.mjs @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MPL-2.0 +// #56-A — wasm instantiate of the Dom convenience constructors. +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; + +const buf = await readFile('./tests/codegen/dom_pilot_surface.wasm'); +const mod = new WebAssembly.Module(buf); +const inst = (await WebAssembly.instantiate(mod, { + wasi_snapshot_preview1: { fd_write: () => 0 }, +})).instance; +assert.equal(inst.exports.main(), 0, 'dom pilot surface compiled and ran'); +console.log('test_dom_pilot_surface.mjs OK'); diff --git a/tests/codegen/test_file_open.mjs b/tests/codegen/test_file_open.mjs new file mode 100644 index 00000000..05284af7 --- /dev/null +++ b/tests/codegen/test_file_open.mjs @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MPL-2.0 +// ADR-015 S5 (#485) — assert file_open wires path_open correctly. +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; + +const buf = await readFile('./tests/codegen/file_open.wasm'); +let inst = null; +let observed = null; + +const imports = { + wasi_snapshot_preview1: { + fd_write: () => 0, + path_open: (dirfd, dirflags, path_ptr, path_len, oflags, _rb, _ri, _fdflags, opened_fd_ptr) => { + const mem = new Uint8Array(inst.exports.memory.buffer); + const path = String.fromCharCode(...mem.slice(path_ptr, path_ptr + path_len)); + observed = { dirfd, dirflags, path, oflags }; + const dv = new DataView(inst.exports.memory.buffer); + dv.setUint32(opened_fd_ptr, 7, true); + return 0; + }, + }, +}; + +inst = (await WebAssembly.instantiate(buf, imports)).instance; +const result = inst.exports.main(); + +assert.ok(observed, 'guest called path_open'); +assert.equal(observed.dirfd, 3, 'dirfd is the first WASI preopen'); +assert.equal(observed.path, 'hello.txt', 'path bytes forwarded'); +assert.equal(observed.oflags, 1, 'CREAT oflag forwarded'); +assert.equal(result, 7, 'file_open returns the host-written fd'); +console.log('test_file_open.mjs OK'); diff --git a/tests/codegen/test_file_roundtrip.mjs b/tests/codegen/test_file_roundtrip.mjs new file mode 100644 index 00000000..8969503d --- /dev/null +++ b/tests/codegen/test_file_roundtrip.mjs @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MPL-2.0 +// ADR-015 S5 (#485) — open/write/close/open/read against stubbed WASI. +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; + +const buf = await readFile('./tests/codegen/file_roundtrip.wasm'); +let inst = null; +const store = new Map(); // path -> bytes +let nextFd = 10; +const called = []; + +function readPath(path_ptr, path_len) { + const mem = new Uint8Array(inst.exports.memory.buffer); + return String.fromCharCode(...mem.slice(path_ptr, path_ptr + path_len)); +} + +const fds = new Map(); // fd -> path + +const imports = { + wasi_snapshot_preview1: { + fd_write: (fd, iovs, _iovs_len, nwritten_ptr) => { + called.push('fd_write'); + const dv = new DataView(inst.exports.memory.buffer); + const mem = new Uint8Array(inst.exports.memory.buffer); + const bufPtr = dv.getUint32(iovs, true); + const bufLen = dv.getUint32(iovs + 4, true); + const bytes = mem.slice(bufPtr, bufPtr + bufLen); + const path = fds.get(fd); + store.set(path, Buffer.from(bytes)); + dv.setUint32(nwritten_ptr, bufLen, true); + return 0; + }, + path_open: (_dirfd, _dirflags, path_ptr, path_len, _oflags, _rb, _ri, _fdflags, opened_fd_ptr) => { + called.push('path_open'); + const path = readPath(path_ptr, path_len); + const fd = nextFd++; + fds.set(fd, path); + if (!store.has(path)) store.set(path, Buffer.alloc(0)); + const dv = new DataView(inst.exports.memory.buffer); + dv.setUint32(opened_fd_ptr, fd, true); + return 0; + }, + fd_read: (fd, iovs, _iovs_len, nread_ptr) => { + called.push('fd_read'); + const dv = new DataView(inst.exports.memory.buffer); + const mem = new Uint8Array(inst.exports.memory.buffer); + const bufPtr = dv.getUint32(iovs, true); + const bufLen = dv.getUint32(iovs + 4, true); + const path = fds.get(fd); + const data = store.get(path) || Buffer.alloc(0); + const n = Math.min(bufLen, data.length); + mem.set(data.subarray(0, n), bufPtr); + dv.setUint32(nread_ptr, n, true); + return 0; + }, + fd_close: (fd) => { + called.push('fd_close'); + fds.delete(fd); + return 0; + }, + }, +}; + +inst = (await WebAssembly.instantiate(buf, imports)).instance; +const result = inst.exports.main(); + +assert.ok(called.includes('path_open'), 'path_open used'); +assert.ok(called.includes('fd_write'), 'fd_write used'); +assert.ok(called.includes('fd_read'), 'fd_read used'); +assert.ok(called.filter((c) => c === 'fd_close').length >= 2, 'fd_close used twice'); +assert.equal(result, 2, 'round-trip string_length("ok") == 2'); +console.log('test_file_roundtrip.mjs OK'); diff --git a/tests/codegen/test_net_accept.mjs b/tests/codegen/test_net_accept.mjs new file mode 100644 index 00000000..4404e9fa --- /dev/null +++ b/tests/codegen/test_net_accept.mjs @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MPL-2.0 +// #487 — net_accept lowers to sock_accept and returns the new fd. +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; + +const buf = await readFile('./tests/codegen/net_accept.wasm'); +let inst = null; +let observed = null; + +const imports = { + wasi_snapshot_preview1: { + fd_write: () => 0, + sock_accept: (fd, flags, fd_ptr) => { + observed = { fd, flags }; + const dv = new DataView(inst.exports.memory.buffer); + dv.setUint32(fd_ptr, 9, true); + return 0; + }, + }, +}; + +inst = (await WebAssembly.instantiate(buf, imports)).instance; +const result = inst.exports.main(); + +assert.ok(observed, 'guest called sock_accept'); +assert.equal(observed.fd, 3, 'listen fd forwarded'); +assert.equal(observed.flags, 0, 'flags default 0'); +assert.equal(result, 9, 'net_accept returns the host-written fd'); +console.log('test_net_accept.mjs OK'); diff --git a/tests/codegen/test_net_recv.mjs b/tests/codegen/test_net_recv.mjs new file mode 100644 index 00000000..654609f4 --- /dev/null +++ b/tests/codegen/test_net_recv.mjs @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MPL-2.0 +// #487 — net_recv lowers to sock_recv and copies host bytes into an AS string. +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; + +const buf = await readFile('./tests/codegen/net_recv.wasm'); +let inst = null; +let observed = null; + +const imports = { + wasi_snapshot_preview1: { + fd_write: () => 0, + sock_recv: (fd, iovs, iovs_len, ri_flags, ro_datalen, _ro_flags) => { + observed = { fd, iovs_len, ri_flags }; + const dv = new DataView(inst.exports.memory.buffer); + const mem = new Uint8Array(inst.exports.memory.buffer); + const bufPtr = dv.getUint32(iovs, true); + mem[bufPtr] = 104; // 'h' + mem[bufPtr + 1] = 105; // 'i' + dv.setUint32(ro_datalen, 2, true); + return 0; + }, + }, +}; + +inst = (await WebAssembly.instantiate(buf, imports)).instance; +const result = inst.exports.main(); + +assert.ok(observed, 'guest called sock_recv'); +assert.equal(observed.fd, 4, 'fd forwarded'); +assert.equal(result, 2, 'net_recv copied 2 bytes'); +console.log('test_net_recv.mjs OK'); diff --git a/tests/codegen/test_net_send.mjs b/tests/codegen/test_net_send.mjs new file mode 100644 index 00000000..bb64238c --- /dev/null +++ b/tests/codegen/test_net_send.mjs @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MPL-2.0 +// #487 — net_send lowers to sock_send over the AS string bytes. +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; + +const buf = await readFile('./tests/codegen/net_send.wasm'); +let inst = null; +let observed = null; + +const imports = { + wasi_snapshot_preview1: { + fd_write: () => 0, + sock_send: (fd, iovs, _iovs_len, si_flags, so_datalen) => { + const dv = new DataView(inst.exports.memory.buffer); + const mem = new Uint8Array(inst.exports.memory.buffer); + const bufPtr = dv.getUint32(iovs, true); + const bufLen = dv.getUint32(iovs + 4, true); + const data = String.fromCharCode(...mem.slice(bufPtr, bufPtr + bufLen)); + observed = { fd, data, si_flags }; + dv.setUint32(so_datalen, bufLen, true); + return 0; + }, + }, +}; + +inst = (await WebAssembly.instantiate(buf, imports)).instance; +const result = inst.exports.main(); + +assert.ok(observed, 'guest called sock_send'); +assert.equal(observed.fd, 4, 'fd forwarded'); +assert.equal(observed.data, 'ping', 'payload bytes forwarded'); +assert.equal(result, 0, 'errno 0'); +console.log('test_net_send.mjs OK'); diff --git a/tests/codegen/test_wasi_fs_combo.mjs b/tests/codegen/test_wasi_fs_combo.mjs new file mode 100644 index 00000000..432097b0 --- /dev/null +++ b/tests/codegen/test_wasi_fs_combo.mjs @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MPL-2.0 +// ADR-015 S5 (#485) — canonical-order regression for filesystem + +// clock + env + sock_shutdown in one module. +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; + +const buf = await readFile('./tests/codegen/wasi_fs_combo.wasm'); +const mod = new WebAssembly.Module(buf); +const names = WebAssembly.Module.imports(mod) + .filter((i) => i.module === 'wasi_snapshot_preview1') + .map((i) => i.name); + +const expected = [ + 'fd_write', + 'clock_time_get', + 'environ_sizes_get', + 'sock_shutdown', + 'path_open', + 'fd_read', + 'fd_close', +]; +assert.deepEqual(names, expected, `canonical order drifted: ${names.join(' ')}`); + +let inst = null; +const imports = { + wasi_snapshot_preview1: { + fd_write: () => 0, + clock_time_get: (_id, _p, time_ptr) => { + const dv = new DataView(inst.exports.memory.buffer); + dv.setBigUint64(time_ptr, 0n, true); + return 0; + }, + environ_sizes_get: (envc_ptr, envbuf_ptr) => { + const dv = new DataView(inst.exports.memory.buffer); + dv.setUint32(envc_ptr, 0, true); + dv.setUint32(envbuf_ptr, 0, true); + return 0; + }, + sock_shutdown: () => 0, + path_open: (_d, _f, _p, _l, _o, _rb, _ri, _ff, opened_fd_ptr) => { + const dv = new DataView(inst.exports.memory.buffer); + dv.setUint32(opened_fd_ptr, 4, true); + return 0; + }, + fd_read: (_fd, _iovs, _n, nread_ptr) => { + const dv = new DataView(inst.exports.memory.buffer); + dv.setUint32(nread_ptr, 0, true); + return 0; + }, + fd_close: () => 0, + }, +}; + +inst = (await WebAssembly.instantiate(mod, imports)).instance; +const result = inst.exports.main(); +assert.equal(typeof result, 'number', 'combo ran without trap'); +console.log('test_wasi_fs_combo.mjs OK'); diff --git a/tests/codegen/wasi_fs_combo.affine b/tests/codegen/wasi_fs_combo.affine new file mode 100644 index 00000000..9ea8fb5f --- /dev/null +++ b/tests/codegen/wasi_fs_combo.affine @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MPL-2.0 +// ADR-015 S5 (#485) combo: filesystem + clock + env + sock_shutdown +// in one unit. Proves ctx.wasi_func_indices canonical order: +// fd_write < clock_time_get < environ_sizes_get < sock_shutdown +// < path_open < fd_read < fd_close. +pub fn main() -> Int / { Time, Net, File } { + let t = clock_now_ms(1); + let n = env_count(()); + net_shutdown(0, 3); + let fd = file_open("combo.txt", 1); + file_fd_write(fd, "x"); + let _s = file_read(fd, 1); + file_close(fd); + t + n + fd +} diff --git a/tests/componentize/filesystem_smoke.sh b/tests/componentize/filesystem_smoke.sh new file mode 100755 index 00000000..162abb40 --- /dev/null +++ b/tests/componentize/filesystem_smoke.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# +# ADR-015 S5 gated smoke (#485): compile an AffineScript program that +# round-trips a small file through file_open / file_fd_write / +# file_close / file_read, wrap it with tools/componentize.sh --command, +# and assert +# (a) the core module imports path_open + fd_read + fd_close, +# (b) wasmtime run --dir exits 0, +# (c) combo with clock + env + sock_shutdown keeps canonical order. +# +# SKIPs cleanly (exit 0) when the component toolchain or wasmtime is +# not provisioned — opt-in, mirroring the S3/S6 smokes. +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 S5 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 + +cat > "$work/fs.affine" <<'EOF' +fn main() -> Int / { File } { + let fd = file_open("roundtrip.txt", 9); + file_fd_write(fd, "ok\n"); + file_close(fd); + let fd2 = file_open("roundtrip.txt", 0); + let s = file_read(fd2, 8); + file_close(fd2); + return string_length(s); +} +EOF + +cat > "$work/combo.affine" <<'EOF' +fn main() -> Int / { Time, Net, File } { + let t = clock_now_ms(1); + let n = env_count(()); + net_shutdown(0, 3); + let fd = file_open("combo.txt", 1); + file_close(fd); + return 0; +} +EOF + +$COMPILER compile "$work/fs.affine" -o "$work/fs.wasm" +$COMPILER compile "$work/combo.affine" -o "$work/combo.wasm" + +for name in path_open fd_read fd_close; do + if ! wasm-tools print "$work/fs.wasm" 2>/dev/null \ + | grep -q "\"wasi_snapshot_preview1\" \"$name\""; then + echo "FAIL: file_* did not lower to wasi_snapshot_preview1.$name" + exit 1 + fi +done + +tools/componentize.sh --command "$work/fs.wasm" "$work/fs.component.wasm" +# Map the host workdir onto the guest preopen (dirfd 3 / ".") so +# path_open("roundtrip.txt") is hermetic. +if ! wasmtime run --dir "$work::." "$work/fs.component.wasm"; then + echo "FAIL: wasmtime run --dir rejected the S5 filesystem component" + exit 1 +fi + +order="$(wasm-tools print "$work/combo.wasm" 2>/dev/null \ + | grep -oE '"(fd_write|clock_time_get|environ_sizes_get|sock_shutdown|path_open|fd_close)"' \ + | tr -d '"' | tr '\n' ' ')" +expected='fd_write clock_time_get environ_sizes_get sock_shutdown path_open fd_close ' +if [ "$order" != "$expected" ]; then + echo "FAIL: combo import canonical order drifted" + echo " got : $order" + echo " expected: $expected" + exit 1 +fi + +tools/componentize.sh --command "$work/combo.wasm" "$work/combo.component.wasm" >/dev/null +if ! wasmtime run --dir "$work::." "$work/combo.component.wasm"; then + echo "FAIL: combo (clock+env+sock+fs) component rejected by wasmtime" + exit 1 +fi + +echo "ADR-015 S5 filesystem smoke: PASSED ✓" diff --git a/tests/componentize/smoke.sh b/tests/componentize/smoke.sh index e0380159..9511e4a0 100755 --- a/tests/componentize/smoke.sh +++ b/tests/componentize/smoke.sh @@ -40,7 +40,9 @@ $COMPILER compile "$work/cz.affine" -o "$work/cz.wasm" has_section "$work/cz.wasm" \ || { echo "FAIL: fixture did not emit the ownership section"; exit 1; } -tools/componentize.sh "$work/cz.wasm" "$work/cz.component.wasm" +# S6c (#486): the wrapper now defaults to --command. The S3 reactor +# contract is the legacy path and must be requested explicitly. +tools/componentize.sh --reactor "$work/cz.wasm" "$work/cz.component.wasm" wasm-tools validate --features component-model "$work/cz.component.wasm" has_section "$work/cz.component.wasm" \ || { echo "FAIL: ownership section lost through componentization"; exit 1; } diff --git a/tests/componentize/sockio_smoke.sh b/tests/componentize/sockio_smoke.sh new file mode 100755 index 00000000..75a04bda --- /dev/null +++ b/tests/componentize/sockio_smoke.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MPL-2.0 +# +# #487 gated smoke: compile a program that references net_recv / +# net_send / net_accept, wrap with --command, and assert the preview1 +# sock_* imports are present. Real-host invoke of accept/recv against +# a non-socket fd is expected to return errno (the guest drops it). +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" + exit 0 +fi +if ! command -v wasmtime >/dev/null; then + echo "SKIP: wasmtime not on PATH" + 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 + +cat > "$work/sockio.affine" <<'EOF' +fn main() -> Int / { Net } { + net_send(0, "x"); + let _s = net_recv(0, 8); + net_accept(0); + return 0; +} +EOF + +$COMPILER compile "$work/sockio.affine" -o "$work/sockio.wasm" + +for name in sock_recv sock_send sock_accept; do + if ! wasm-tools print "$work/sockio.wasm" 2>/dev/null \ + | grep -q "\"wasi_snapshot_preview1\" \"$name\""; then + echo "FAIL: net_* did not lower to wasi_snapshot_preview1.$name" + exit 1 + fi +done + +tools/componentize.sh --command "$work/sockio.wasm" "$work/sockio.component.wasm" +if ! wasmtime run "$work/sockio.component.wasm"; then + echo "FAIL: wasmtime run rejected the #487 component" + exit 1 +fi + +echo "ADR-015 #487 sockio smoke: PASSED ✓" diff --git a/tools/componentize.sh b/tools/componentize.sh index 11a7f01b..920ec4d3 100755 --- a/tools/componentize.sh +++ b/tools/componentize.sh @@ -8,17 +8,19 @@ # *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 +# command (default as of ADR-015 S6c / #486) — 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`. +# +# reactor (--reactor, 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 compiler still emits a preview1 core module; native-preview2 +# import lowering remains the one-way door (not this wrap default). # # The `typedwasm.ownership` custom section (the typed-wasm contract # carrier, multi-producer ABI) MUST survive the wrap — asserted here @@ -32,7 +34,9 @@ # AFFINESCRIPT_WASI_ADAPTER=/path to force a specific adapter file.) set -euo pipefail -mode="reactor" +# ADR-015 S6c (#486): command is the default wrap. The reactor adapter +# remains selectable via --reactor (the S3 on-ramp / library consumers). +mode="command" if [ "${1:-}" = "--command" ] || [ "${1:-}" = "--reactor" ]; then mode="${1#--}" shift