From 316124d9b6fef85c111053272067122d4f735609 Mon Sep 17 00:00:00 2001 From: Jan Klaas Kollhof Date: Wed, 6 May 2026 23:38:03 +0100 Subject: [PATCH] feat: upgrade fink to v0.69.0 and adopt JS interop host shim Switch the playground's WASM target from the Rust interop to the new JS interop landed in fink v0.69.0. Adopt upstream fink.js as the host shim instead of hand-rolling host imports. - crate: pin fink = v0.69.0; compile() now uses to_wasm_for(..., Interop::Js). - crate: handle new NodeKind::PostfixOp in token collection and AST serialization. - runner: rewrite on top of fink.js init_wasm. Capture stdout/stderr via host overrides; if the imported module exports `main`, call it with 'playground'; otherwise surface the module's last expression. - runner: split timing into four buckets (compile / init / import / run). - main: render stdout/stderr panels and the timing breakdown. - build.mjs: vendor fink.js from the cargo-resolved fink source so the shim always matches the pinned crate. - gitignore: add src/fink.js (vendored at build time); collapse .claude patterns into a single .claude rule. - Makefile: make clean removes src/fink.js. --- .gitignore | 6 +- Makefile | 2 +- build.mjs | 22 +++++ crate/Cargo.lock | 86 ++++++------------- crate/Cargo.toml | 2 +- crate/src/lib.rs | 10 ++- package-lock.json | 205 ++++++++++++++++++++++------------------------ src/main.ts | 17 +++- src/runner.ts | 174 +++++++++++++-------------------------- 9 files changed, 230 insertions(+), 294 deletions(-) diff --git a/.gitignore b/.gitignore index 8c0cdbe..cea0896 100644 --- a/.gitignore +++ b/.gitignore @@ -5,8 +5,10 @@ dist/ target/ .DS_Store +# Vendored at build time from the linked fink source (see build.mjs step 0a). +src/fink.js + # Claude local config -.claude/settings.local.json -.claude.local/ +.claude /.brain/ diff --git a/Makefile b/Makefile index f775626..7544f69 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ deps-install: node deps.mjs install clean: - rm -rf build crate/pkg + rm -rf build crate/pkg src/fink.js build: NODE_ENV=production node build.mjs diff --git a/build.mjs b/build.mjs index fee3592..c63051a 100644 --- a/build.mjs +++ b/build.mjs @@ -36,6 +36,28 @@ fs.mkdirSync(OUT, { recursive: true }) execSync('wasm-pack build --target web', { cwd: 'crate', stdio: 'inherit' }) console.log(' built crate → crate/pkg/') +// --------------------------------------------------------------------------- +// 0a. Vendor fink.js host shim from the linked fink source. +// Resolves the fink package's manifest path via `cargo metadata` and +// copies src/runtime/interop/js/fink.js next to src/main.ts so esbuild +// can bundle it. Always pulls from the version pinned by Cargo.toml, +// no risk of vendored drift. +// --------------------------------------------------------------------------- + +{ + const meta = JSON.parse(execSync('cargo metadata --format-version 1', { + cwd: 'crate', + stdio: ['ignore', 'pipe', 'inherit'], + }).toString()) + const finkPkg = meta.packages.find((p) => p.name === 'fink') + if (!finkPkg) throw new Error('cargo metadata: fink package not found') + const finkRoot = path.dirname(finkPkg.manifest_path) + const finkJsSrc = path.join(finkRoot, 'src/runtime/interop/js/fink.js') + if (!fs.existsSync(finkJsSrc)) throw new Error(`fink.js not found at ${finkJsSrc}`) + fs.copyFileSync(finkJsSrc, 'src/fink.js') + console.log(` vendored fink.js from ${finkRoot}`) +} + // --------------------------------------------------------------------------- // 1. Monaco editor worker (iife — workers don't use ES modules by default) // --------------------------------------------------------------------------- diff --git a/crate/Cargo.lock b/crate/Cargo.lock index db8c521..9989da5 100644 --- a/crate/Cargo.lock +++ b/crate/Cargo.lock @@ -10,9 +10,9 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "bumpalo" @@ -41,13 +41,13 @@ checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" [[package]] name = "fink" version = "0.0.0" -source = "git+https://github.com/fink-lang/fink.git?tag=v0.64.0#365da5fd6902756d1b8f5d61eb2f422cdc6ed035" +source = "git+https://github.com/fink-lang/fink.git?tag=v0.69.0#94ed654681fd72bd2be057b5ce7e33f7d587904c" dependencies = [ - "gimli 0.33.1", - "wasm-encoder 0.248.0", - "wasmparser 0.248.0", + "gimli 0.33.0", + "wasm-encoder", + "wasmparser", "wasmprinter", - "wast 248.0.0", + "wast", "wat", ] @@ -73,9 +73,9 @@ checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] name = "gimli" -version = "0.31.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" dependencies = [ "fallible-iterator", "indexmap", @@ -84,9 +84,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.33.1" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e16c5073773ccf057c282be832a59ee53ef5ff98db3aeff7f8314f52ffc196" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" dependencies = [ "fnv", "hashbrown 0.16.1", @@ -239,9 +239,9 @@ checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "wasm-bindgen" -version = "0.2.117" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" dependencies = [ "cfg-if", "once_cell", @@ -252,9 +252,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.117" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -262,9 +262,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.117" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" dependencies = [ "bumpalo", "proc-macro2", @@ -275,23 +275,13 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.117" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.246.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61fb705ce81adde29d2a8e99d87995e39a6e927358c91398f374474746070ef7" -dependencies = [ - "leb128fmt", - "wasmparser 0.246.2", -] - [[package]] name = "wasm-encoder" version = "0.248.0" @@ -299,18 +289,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac92cf547bc18d27ecc521015c08c353b4f18b84ab388bb6d1b6b682c620d9b6" dependencies = [ "leb128fmt", - "wasmparser 0.248.0", -] - -[[package]] -name = "wasmparser" -version = "0.246.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71cde4757396defafd25417cfb36aa3161027d06d865b0c24baaae229aac005d" -dependencies = [ - "bitflags", - "indexmap", - "semver", + "wasmparser", ] [[package]] @@ -334,21 +313,7 @@ checksum = "30b264a5410b008d4d199a92bf536eae703cbd614482fc1ec53831cf19e1c183" dependencies = [ "anyhow", "termcolor", - "wasmparser 0.248.0", -] - -[[package]] -name = "wast" -version = "246.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe3fe8e3bf88ad96d031b4181ddbd64634b17cb0d06dfc3de589ef43591a9a62" -dependencies = [ - "bumpalo", - "gimli 0.31.1", - "leb128fmt", - "memchr", - "unicode-width", - "wasm-encoder 0.246.2", + "wasmparser", ] [[package]] @@ -358,19 +323,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acc54622ed5a5cddafcdf152043f9d4aed54d4a653d686b7dfe874809fca99d7" dependencies = [ "bumpalo", + "gimli 0.32.3", "leb128fmt", "memchr", "unicode-width", - "wasm-encoder 0.248.0", + "wasm-encoder", ] [[package]] name = "wat" -version = "1.246.2" +version = "1.248.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd7fda1199b94fff395c2d19a153f05dbe7807630316fa9673367666fd2ad8c" +checksum = "d75cd9e510603909748e6ebab89f27cd04472c1d9d85a3c88a7a6fc51a1a7934" dependencies = [ - "wast 246.0.2", + "wast", ] [[package]] diff --git a/crate/Cargo.toml b/crate/Cargo.toml index 190d6b4..26a7943 100644 --- a/crate/Cargo.toml +++ b/crate/Cargo.toml @@ -8,7 +8,7 @@ crate-type = ["cdylib", "rlib"] path = "src/lib.rs" [dependencies] -fink = { git = "https://github.com/fink-lang/fink.git", tag = "v0.64.0", default-features = false, features = ["compile"] } +fink = { git = "https://github.com/fink-lang/fink.git", tag = "v0.69.0", default-features = false, features = ["compile"] } wasm-bindgen = "0.2" [profile.release] diff --git a/crate/src/lib.rs b/crate/src/lib.rs index 79d39c8..b1cd7ea 100644 --- a/crate/src/lib.rs +++ b/crate/src/lib.rs @@ -334,6 +334,10 @@ fn collect_tokens(ast: &Ast<'_>, id: AstId, tokens: &mut Vec) { collect_tokens(ast, *operand, tokens); } + NodeKind::PostfixOp { lhs, .. } => { + collect_tokens(ast, *lhs, tokens); + } + NodeKind::Group { inner, .. } => { collect_tokens(ast, *inner, tokens); } @@ -485,6 +489,7 @@ fn node_kind_label<'a>(node: &'a ast::Node<'a>) -> (&'static str, String) { StrRawTempl { .. } => ("StrRawTempl", String::new()), Ident(s) => ("Ident", s.to_string()), UnaryOp { op, .. } => ("UnaryOp", op.src.to_string()), + PostfixOp { op, .. } => ("PostfixOp", op.src.to_string()), InfixOp { op, .. } => ("InfixOp", op.src.to_string()), ChainedCmp(_) => ("ChainedCmp", String::new()), Spread { op, .. } => ("Spread", op.src.to_string()), @@ -529,6 +534,9 @@ fn serialize_children(ast: &Ast<'_>, id: AstId) -> String { UnaryOp { operand, .. } | Try(operand) => { parts.push(serialize_node(ast, *operand)); } + PostfixOp { lhs, .. } => { + parts.push(serialize_node(ast, *lhs)); + } InfixOp { lhs, rhs, .. } | Bind { lhs, rhs, .. } | BindRight { lhs, rhs, .. } @@ -607,7 +615,7 @@ fn native_sourcemap_to_json(sm: &NativeSourceMap) -> String { /// Throws a JS error with the diagnostic message on compilation failure. #[wasm_bindgen] pub fn compile(src: &str) -> Result, JsValue> { - let wasm = fink::to_wasm(src, "playground.fnk") + let wasm = fink::to_wasm_for(src, "playground.fnk", fink::passes::wasm::emit::Interop::Js) .map_err(|e| JsValue::from_str(&e))?; Ok(wasm.binary) } diff --git a/package-lock.json b/package-lock.json index 4c10922..def2706 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,9 +18,9 @@ } }, "node_modules/@actions/core": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-3.0.0.tgz", - "integrity": "sha512-zYt6cz+ivnTmiT/ksRVriMBOiuoUpDCJJlZ5KPl2/FRdvwU3f7MPh9qftvbkXJThragzUZieit2nyHUyw53Seg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-3.0.1.tgz", + "integrity": "sha512-a6d/Nwahm9fliVGRhdhofo40HjHQasUPusmc7vBfyky+7Z+P2A1J68zyFVaNcEclc/Se+eO595oAr5nwEIoIUA==", "dev": true, "license": "MIT", "dependencies": { @@ -39,9 +39,9 @@ } }, "node_modules/@actions/http-client": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-4.0.0.tgz", - "integrity": "sha512-QuwPsgVMsD6qaPD57GLZi9sqzAZCtiJT8kVBCDpLtxhL5MydQ4gS+DrejtZZPdIYyB1e95uCK9Luyds7ybHI3g==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-4.0.1.tgz", + "integrity": "sha512-+Nvd1ImaOZBSoPbsUtEhv+1z99H12xzncCkz0a3RuehINE81FZSe2QTj3uvAPTcJX/SCzUQHQ0D1GrPMbrPitg==", "dev": true, "license": "MIT", "dependencies": { @@ -50,9 +50,9 @@ } }, "node_modules/@actions/http-client/node_modules/undici": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz", - "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==", + "version": "6.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.25.0.tgz", + "integrity": "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==", "dev": true, "license": "MIT", "engines": { @@ -1043,13 +1043,13 @@ } }, "node_modules/@types/node": { - "version": "25.5.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz", - "integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==", + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.18.0" + "undici-types": "~7.19.0" } }, "node_modules/@types/normalize-package-data": { @@ -2064,9 +2064,9 @@ } }, "node_modules/fs-extra": { - "version": "11.3.4", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", - "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", "dev": true, "license": "MIT", "dependencies": { @@ -2205,9 +2205,9 @@ } }, "node_modules/hosted-git-info": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.2.tgz", - "integrity": "sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", + "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", "dev": true, "license": "ISC", "dependencies": { @@ -2455,9 +2455,9 @@ "license": "ISC" }, "node_modules/issue-parser": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/issue-parser/-/issue-parser-7.0.1.tgz", - "integrity": "sha512-3YZcUUR2Wt1WsapF+S/WiA2WmlW0cWAoPccMqne7AxEBhCdFeTPjfv/Axb8V2gyCgY3nRw+ksZ3xSUX+R47iAg==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/issue-parser/-/issue-parser-7.0.2.tgz", + "integrity": "sha512-7atWPjhGEIX3JEtMrOYd8TKzboYlq+5sNbdl9POiLYOI14G5HZiQbZP0Xj5EZdrufQVXfJlpTV0hys0CuxwxZw==", "dev": true, "license": "MIT", "dependencies": { @@ -2523,9 +2523,9 @@ "license": "MIT" }, "node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -2629,9 +2629,9 @@ "license": "MIT" }, "node_modules/lru-cache": { - "version": "11.3.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.2.tgz", - "integrity": "sha512-wgWa6FWQ3QRRJbIjbsldRJZxdxYngT/dO0I5Ynmlnin8qy7tC6xYzbcJjtN4wHLXtkbVwHzk0C+OejVw1XM+DQ==", + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", + "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -2864,9 +2864,9 @@ } }, "node_modules/npm": { - "version": "11.12.1", - "resolved": "https://registry.npmjs.org/npm/-/npm-11.12.1.tgz", - "integrity": "sha512-zcoUuF1kezGSAo0CqtvoLXX3mkRqzuqYdL6Y5tdo8g69NVV3CkjQ6ZBhBgB4d7vGkPcV6TcvLi3GRKPDFX+xTA==", + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/npm/-/npm-11.14.0.tgz", + "integrity": "sha512-6jfZzqK8EfWGnTeZQe+bgMx4IgiEZn7k1eM4GE8SE81B3iYch52dVSMO1hC2OnNbFLIwzvwUkUxsOOcUPG2XIw==", "bundleDependencies": [ "@isaacs/string-locale-compare", "@npmcli/arborist", @@ -2945,8 +2945,8 @@ ], "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/arborist": "^9.4.2", - "@npmcli/config": "^10.8.1", + "@npmcli/arborist": "^9.5.0", + "@npmcli/config": "^10.9.0", "@npmcli/fs": "^5.0.0", "@npmcli/map-workspaces": "^5.0.3", "@npmcli/metavuln-calculator": "^9.0.3", @@ -2967,24 +2967,24 @@ "hosted-git-info": "^9.0.2", "ini": "^6.0.0", "init-package-json": "^8.2.5", - "is-cidr": "^6.0.3", + "is-cidr": "^6.0.4", "json-parse-even-better-errors": "^5.0.0", "libnpmaccess": "^10.0.3", - "libnpmdiff": "^8.1.5", - "libnpmexec": "^10.2.5", - "libnpmfund": "^7.0.19", + "libnpmdiff": "^8.1.7", + "libnpmexec": "^10.2.7", + "libnpmfund": "^7.0.21", "libnpmorg": "^8.0.1", - "libnpmpack": "^9.1.5", + "libnpmpack": "^9.1.7", "libnpmpublish": "^11.1.3", "libnpmsearch": "^9.0.1", "libnpmteam": "^8.0.2", "libnpmversion": "^8.0.3", "make-fetch-happen": "^15.0.5", - "minimatch": "^10.2.4", + "minimatch": "^10.2.5", "minipass": "^7.1.3", "minipass-pipeline": "^1.2.4", "ms": "^2.1.2", - "node-gyp": "^12.2.0", + "node-gyp": "^12.3.0", "nopt": "^9.0.0", "npm-audit-report": "^7.0.0", "npm-install-checks": "^8.0.0", @@ -3003,7 +3003,7 @@ "spdx-expression-parse": "^4.0.0", "ssri": "^13.0.1", "supports-color": "^10.2.2", - "tar": "^7.5.11", + "tar": "^7.5.13", "text-table": "~0.2.0", "tiny-relative-date": "^2.0.2", "treeverse": "^3.0.0", @@ -3092,7 +3092,7 @@ } }, "node_modules/npm/node_modules/@npmcli/arborist": { - "version": "9.4.2", + "version": "9.5.0", "dev": true, "inBundle": true, "license": "ISC", @@ -3140,7 +3140,7 @@ } }, "node_modules/npm/node_modules/@npmcli/config": { - "version": "10.8.1", + "version": "10.9.0", "dev": true, "inBundle": true, "license": "ISC", @@ -3343,7 +3343,7 @@ } }, "node_modules/npm/node_modules/@sigstore/protobuf-specs": { - "version": "0.5.0", + "version": "0.5.1", "dev": true, "inBundle": true, "license": "Apache-2.0", @@ -3485,7 +3485,7 @@ } }, "node_modules/npm/node_modules/brace-expansion": { - "version": "5.0.4", + "version": "5.0.5", "dev": true, "inBundle": true, "license": "MIT", @@ -3554,7 +3554,7 @@ } }, "node_modules/npm/node_modules/cidr-regex": { - "version": "5.0.3", + "version": "5.0.5", "dev": true, "inBundle": true, "license": "BSD-2-Clause", @@ -3610,7 +3610,7 @@ } }, "node_modules/npm/node_modules/diff": { - "version": "8.0.3", + "version": "8.0.4", "dev": true, "inBundle": true, "license": "BSD-3-Clause", @@ -3777,7 +3777,7 @@ } }, "node_modules/npm/node_modules/ip-address": { - "version": "10.1.0", + "version": "10.1.1", "dev": true, "inBundle": true, "license": "MIT", @@ -3786,12 +3786,12 @@ } }, "node_modules/npm/node_modules/is-cidr": { - "version": "6.0.3", + "version": "6.0.4", "dev": true, "inBundle": true, "license": "BSD-2-Clause", "dependencies": { - "cidr-regex": "^5.0.1" + "cidr-regex": "^5.0.4" }, "engines": { "node": ">=20" @@ -3859,12 +3859,12 @@ } }, "node_modules/npm/node_modules/libnpmdiff": { - "version": "8.1.5", + "version": "8.1.7", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.4.2", + "@npmcli/arborist": "^9.5.0", "@npmcli/installed-package-contents": "^4.0.0", "binary-extensions": "^3.0.0", "diff": "^8.0.2", @@ -3878,13 +3878,13 @@ } }, "node_modules/npm/node_modules/libnpmexec": { - "version": "10.2.5", + "version": "10.2.7", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { "@gar/promise-retry": "^1.0.0", - "@npmcli/arborist": "^9.4.2", + "@npmcli/arborist": "^9.5.0", "@npmcli/package-json": "^7.0.0", "@npmcli/run-script": "^10.0.0", "ci-info": "^4.0.0", @@ -3901,12 +3901,12 @@ } }, "node_modules/npm/node_modules/libnpmfund": { - "version": "7.0.19", + "version": "7.0.21", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.4.2" + "@npmcli/arborist": "^9.5.0" }, "engines": { "node": "^20.17.0 || >=22.9.0" @@ -3926,12 +3926,12 @@ } }, "node_modules/npm/node_modules/libnpmpack": { - "version": "9.1.5", + "version": "9.1.7", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^9.4.2", + "@npmcli/arborist": "^9.5.0", "@npmcli/run-script": "^10.0.0", "npm-package-arg": "^13.0.0", "pacote": "^21.0.2" @@ -4001,7 +4001,7 @@ } }, "node_modules/npm/node_modules/lru-cache": { - "version": "11.2.7", + "version": "11.3.5", "dev": true, "inBundle": true, "license": "BlueOak-1.0.0", @@ -4033,12 +4033,12 @@ } }, "node_modules/npm/node_modules/minimatch": { - "version": "10.2.4", + "version": "10.2.5", "dev": true, "inBundle": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^5.0.5" }, "engines": { "node": "18 || 20 || >=22" @@ -4086,35 +4086,17 @@ } }, "node_modules/npm/node_modules/minipass-flush": { - "version": "1.0.5", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/npm/node_modules/minipass-flush/node_modules/minipass": { - "version": "3.3.6", + "version": "1.0.6", "dev": true, "inBundle": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "yallist": "^4.0.0" + "minipass": "^7.1.3" }, "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.17" } }, - "node_modules/npm/node_modules/minipass-flush/node_modules/yallist": { - "version": "4.0.0", - "dev": true, - "inBundle": true, - "license": "ISC" - }, "node_modules/npm/node_modules/minipass-pipeline": { "version": "1.2.4", "dev": true, @@ -4194,7 +4176,7 @@ } }, "node_modules/npm/node_modules/node-gyp": { - "version": "12.2.0", + "version": "12.3.0", "dev": true, "inBundle": true, "license": "MIT", @@ -4202,12 +4184,12 @@ "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", - "make-fetch-happen": "^15.0.0", "nopt": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", "tar": "^7.5.4", "tinyglobby": "^0.2.12", + "undici": "^6.25.0", "which": "^6.0.0" }, "bin": { @@ -4580,12 +4562,12 @@ } }, "node_modules/npm/node_modules/socks": { - "version": "2.8.7", + "version": "2.8.8", "dev": true, "inBundle": true, "license": "MIT", "dependencies": { - "ip-address": "^10.0.1", + "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" }, "engines": { @@ -4654,7 +4636,7 @@ } }, "node_modules/npm/node_modules/tar": { - "version": "7.5.11", + "version": "7.5.13", "dev": true, "inBundle": true, "license": "BlueOak-1.0.0", @@ -4682,13 +4664,13 @@ "license": "MIT" }, "node_modules/npm/node_modules/tinyglobby": { - "version": "0.2.15", + "version": "0.2.16", "dev": true, "inBundle": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -4715,7 +4697,7 @@ } }, "node_modules/npm/node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", + "version": "4.0.4", "dev": true, "inBundle": true, "license": "MIT", @@ -4749,6 +4731,15 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/npm/node_modules/undici": { + "version": "6.25.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, "node_modules/npm/node_modules/util-deprecate": { "version": "1.0.2", "dev": true, @@ -5861,14 +5852,14 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -5945,9 +5936,9 @@ } }, "node_modules/type-fest": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.5.0.tgz", - "integrity": "sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.6.0.tgz", + "integrity": "sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==", "dev": true, "license": "(MIT OR CC0-1.0)", "dependencies": { @@ -5961,9 +5952,9 @@ } }, "node_modules/typescript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", - "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -5989,9 +5980,9 @@ } }, "node_modules/undici": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.7.tgz", - "integrity": "sha512-H/nlJ/h0ggGC+uRL3ovD+G0i4bqhvsDOpbDv7At5eFLlj2b41L8QliGbnl2H7SnDiYhENphh1tQFJZf+MyfLsQ==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", + "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", "dev": true, "license": "MIT", "engines": { @@ -5999,9 +5990,9 @@ } }, "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", "dev": true, "license": "MIT" }, diff --git a/src/main.ts b/src/main.ts index cb5f2f8..0ad2322 100644 --- a/src/main.ts +++ b/src/main.ts @@ -516,6 +516,7 @@ runBtn.addEventListener('click', async () => { try { const src = editor.getValue() let wasm: Uint8Array | null + const tCompile = performance.now() try { wasm = await compile(src) } catch (err) { @@ -523,16 +524,26 @@ runBtn.addEventListener('click', async () => { outputEl.className = 'error' return } + const compileMs = performance.now() - tCompile if (!wasm) { outputEl.textContent = 'Compiler not available yet.' outputEl.className = 'error' return } const result = await run(wasm) - const time = `(${result.durationMs.toFixed(1)} ms)` - outputEl.textContent = result.ok - ? `OK — ${result.message} ${time}\n\n(stdout/stderr capture coming once interop/js.wat lands upstream)` + const parts = [ + `compile ${compileMs.toFixed(1)}ms`, + `init ${result.initMs.toFixed(1)}ms`, + `import ${result.importMs.toFixed(1)}ms`, + result.runMs !== null ? `run ${result.runMs.toFixed(1)}ms` : null, + ].filter(Boolean) + const time = `(${parts.join(' · ')})` + const head = result.ok + ? `OK — ${result.message} ${time}` : `${result.status}: ${result.message} ${time}` + const out = result.stdout.length ? `\n\nstdout:\n${result.stdout.join('')}` : '' + const err = result.stderr.length ? `\n\nstderr:\n${result.stderr.join('')}` : '' + outputEl.textContent = head + out + err outputEl.className = result.ok ? 'ok' : 'error' } catch (err) { outputEl.textContent = `Runtime error: ${err}` diff --git a/src/runner.ts b/src/runner.ts index 713af4f..a7b51ff 100644 --- a/src/runner.ts +++ b/src/runner.ts @@ -1,142 +1,78 @@ // Tier-1 ƒink WASM runner. // -// Instantiates a compiled fink module in the browser and calls its entry -// wrapper. Host imports are no-op stubs except `host_panic`, which throws — -// programs that compute pure results execute end-to-end; programs that try -// to do IO simply have their host calls swallowed for now. +// Instantiates a compiled fink module via the upstream JS host shim +// (src/fink.js) and imports the entry module. stdout/stderr writes from +// fink are captured via host overrides; the result of the module's last +// expression — or its `main` export, if present — is surfaced in the +// run summary. // -// Output is wall-clock duration + status + any trap message. Once fink -// gains a JS-friendly bytes-readback helper we'll surface stdout/stderr -// here too. +// Timing buckets (all milliseconds, all optional except init+import): +// initMs — init_wasm: WASM instantiation + host-shim wiring. +// importMs — fink.import('./playground.fnk'): module initialization. +// runMs — main(...) call (omitted if the module has no `main`). + +import { init_wasm } from './fink.js' export interface RunResult { ok: boolean - status: 'ok' | 'panic' | 'trap' | 'no-entry' + status: 'ok' | 'panic' | 'trap' message: string - durationMs: number -} - -interface HostImports { - env: { - host_panic: (...args: unknown[]) => void - host_channel_send: (...args: unknown[]) => void - host_read: (...args: unknown[]) => void - host_invoke_cont: (...args: unknown[]) => void - host_resume: (...args: unknown[]) => void - } -} - -function makeImports(): HostImports { - return { - env: { - host_panic: () => { - throw new Error('fink panic: irrefutable pattern failed') - }, - // No bytes-readback path yet — silently swallow. Once interop/js.wat - // lands upstream we can copy bytes out via a memory-readback helper. - host_channel_send: () => {}, - // No stdin source yet. The fink runtime expects host_read to settle - // a future asynchronously; a missing settle just leaves the program - // parked at that point. Programs that don't read stdin run cleanly. - host_read: () => {}, - // The cont handshake delivers main's exit code and module-init result. - // We discard both for now. - host_invoke_cont: () => {}, - // Scheduler reentry hook — the runtime calls this when the task - // queue is empty so the host can drive any pending IO / settle - // host futures and re-fill the queue. With no IO and no async - // futures, returning immediately is the correct behaviour: the - // scheduler will then exit because the queue is still empty. - host_resume: () => {}, - }, - } -} - -// Find the entry wrapper export. compile_package emits each module's -// wrapper under its canonical URL. The entry's URL is `./`. -function findEntryWrapper(instance: WebAssembly.Instance): { - name: string - func: Function -} | null { - const exports = instance.exports as Record - for (const name of Object.keys(exports)) { - if (!name.startsWith('./')) continue - const value = exports[name] - if (typeof value === 'function') { - return { name, func: value as Function } - } - } - return null -} - -/// Build an empty `$ByteArray` to pass as the wrapper's key. -/// -/// JS cannot directly construct a WasmGC array — there is no -/// `WebAssembly.Array.new()` API in any browser as of mid-2025. -/// -/// TODO(interop): only `interop/*.wat` exports are part of fink's -/// stable contract. The two `std/str.*` exports used here are internal -/// and *will* disappear under linker DCE / cleanup. Migrate to a -/// proper interop bootstrap export (e.g. an `interop/js.wat` -/// `_alloc_empty_bytes`) the moment one is available — currently -/// blocking the playground's tier-1 runner. -function makeEmptyByteArray(instance: WebAssembly.Instance): unknown { - const exports = instance.exports as Record - const strEmpty = exports['std/str.fnk:str_empty'] as Function | undefined - const bytes = exports['std/str.wat:bytes'] as Function | undefined - if (!strEmpty || !bytes) { - throw new Error('runtime missing str_empty / bytes exports — cannot bootstrap entry call') - } - return bytes(strEmpty()) -} - -function callEntry(entry: { name: string; func: Function }, key: unknown): void { - entry.func(key, 0) + initMs: number + importMs: number + runMs: number | null + stdout: string[] + stderr: string[] } export async function run(bytes: Uint8Array): Promise { - const t0 = performance.now() - let instance: WebAssembly.Instance + const stdout: string[] = [] + const stderr: string[] = [] + let initMs = 0 + let importMs = 0 + let runMs: number | null = null + try { - const result = await WebAssembly.instantiate(bytes, makeImports() as unknown as WebAssembly.Imports) - instance = result.instance - } catch (e) { - return { - ok: false, - status: 'trap', - message: `instantiate failed: ${(e as Error).message}`, - durationMs: performance.now() - t0, - } - } + const tInit = performance.now() + const fink = await init_wasm(bytes, { + stdout_write: (s: string) => stdout.push(s), + stderr_write: (s: string) => stderr.push(s), + panic: () => { throw new Error('fink panic: irrefutable pattern failed') }, + }) + initMs = performance.now() - tInit - const entry = findEntryWrapper(instance) - if (!entry) { - return { - ok: false, - status: 'no-entry', - message: 'no entry wrapper export (expected one starting with "./")', - durationMs: performance.now() - t0, + const tImport = performance.now() + const [last_val, mod] = await fink.import('./playground.fnk') + importMs = performance.now() - tImport + + let message = `last = ${formatLastVal(last_val)}` + if (mod && typeof (mod as { main?: unknown }).main === 'function') { + const main = (mod as { main: (arg: string) => Promise }).main + const tRun = performance.now() + const result = await main('playground') + runMs = performance.now() - tRun + message = `main → ${formatLastVal(result)}` } - } - try { - const key = makeEmptyByteArray(instance) - callEntry(entry, key) + return { ok: true, status: 'ok', message, initMs, importMs, runMs, stdout, stderr } } catch (e) { - const msg = (e as Error).message - const isPanic = msg.includes('fink panic') + const msg = (e as Error).message ?? String(e) + const isPanic = msg.includes('fink panic') || msg.includes('host_panic') return { ok: false, status: isPanic ? 'panic' : 'trap', message: msg, - durationMs: performance.now() - t0, + initMs, + importMs, + runMs, + stdout, + stderr, } } +} - return { - ok: true, - status: 'ok', - message: `ran ${entry.name}`, - durationMs: performance.now() - t0, - } +function formatLastVal(v: unknown): string { + if (v === undefined) return '()' + if (typeof v === 'string') return JSON.stringify(v) + if (typeof v === 'number' || typeof v === 'boolean') return String(v) + return String(v) }