diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e89ea68..4354b4e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,7 +5,8 @@ # # This gate only checks the Rust workspace, so it runs only when Rust-relevant # files change (paths allowlist below). Changes to the JS/TS monorepo -# (package.json, apps/**, packages/**, scripts/**) and docs do NOT trigger it. +# (package.json, apps/**, packages/**, scripts/**) and docs do NOT trigger it, +# except scripts/test262-fetch.sh, which pins the test262 checkout. name: CI @@ -21,6 +22,8 @@ on: - "rustfmt.toml" - "deny.toml" - "_typos.toml" + - "tests/test262/**" + - "scripts/test262-fetch.sh" - ".github/workflows/ci.yml" pull_request: paths: *rust-paths @@ -34,13 +37,13 @@ jobs: name: rust (fmt + clippy + test + deny + shear + typos) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - # Toolchain is pinned to 1.96.0 (matches rust-toolchain.toml / MSRV). - - name: Install toolchain 1.96.0 + # Toolchain is pinned to 1.99.1 (matches rust-toolchain.toml / MSRV). + - name: Install toolchain 1.99.1 uses: dtolnay/rust-toolchain@master with: - toolchain: 1.96.0 + toolchain: 1.99.1 components: rustfmt, clippy - name: Cache cargo build @@ -76,5 +79,37 @@ jobs: - name: Supply-chain (cargo-deny) run: cargo deny check + test262: + name: test262 (baseline regression gate) + runs-on: ubuntu-latest + timeout-minutes: 90 + steps: + - uses: actions/checkout@v7 + + - name: Install toolchain 1.99.1 + uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.99.1 + + - name: Cache cargo build + uses: Swatinem/rust-cache@v2 + + # The pinned test262 SHA lives in the fetch script, so key on its hash. + - name: Cache test262 checkout + uses: actions/cache@v6 + with: + path: .tmp/test262 + key: test262-${{ hashFiles('scripts/test262-fetch.sh') }} + + - name: Fetch test262 + run: bash scripts/test262-fetch.sh + + - name: Build jsc (release) + run: cargo build --release -p jsc + + # Fails if any test passing in tests/test262/baseline.txt regressed. + - name: Check against baseline + run: cargo run --release -p jsc-test262 -- --check + # NOTE: portable musl / multi-platform release binaries are built by # release.yml on `v*` tags (and manual dispatch), not on every push. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1e0c0a7..d07edd1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,12 +26,12 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - - name: Install toolchain 1.96.0 (+ musl target) + - name: Install toolchain 1.99.1 (+ musl target) uses: dtolnay/rust-toolchain@master with: - toolchain: 1.96.0 + toolchain: 1.99.1 targets: x86_64-unknown-linux-musl - name: Install musl tools diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml index b191c09..fe9ee93 100644 --- a/.github/workflows/web.yml +++ b/.github/workflows/web.yml @@ -1,9 +1,24 @@ name: Web # Frontend/JS-side quality gate for the monorepo (packages/jsc + apps/jsc-web). -# MANUAL only — run it from the Actions tab (workflow_dispatch) when you want to -# check the JS/TS side. The push/PR Rust gate lives in ci.yml. +# Runs on push to main and on pull requests when JS/TS or wasm inputs change +# (paths allowlist below), and manually via workflow_dispatch. The Rust +# workspace gate lives in ci.yml. on: + push: + branches: [main] + paths: &web-paths + - "crates/**" + - "packages/**" + - "apps/**" + - "scripts/**" + - "Cargo.toml" + - "Cargo.lock" + - "package.json" + - "bun.lock" + - ".github/workflows/web.yml" + pull_request: + paths: *web-paths workflow_dispatch: env: @@ -14,14 +29,14 @@ jobs: name: biome + typecheck + build + test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 # The @cdlab/jsc package loads a wasm-bindgen artifact that is gitignored, # so build it first (needs Rust + wasm-pack). - - name: Install toolchain 1.96.0 (+ wasm target) + - name: Install toolchain 1.99.1 (+ wasm target) uses: dtolnay/rust-toolchain@master with: - toolchain: 1.96.0 + toolchain: 1.99.1 targets: wasm32-unknown-unknown - uses: Swatinem/rust-cache@v2 diff --git a/.gitignore b/.gitignore index 57d3802..d26e9da 100644 --- a/.gitignore +++ b/.gitignore @@ -202,3 +202,6 @@ apps/*/dist/ # bun / vite caches .vite/ *.tsbuildinfo + +# Local scratch: test262 checkout (scripts/test262-fetch.sh) and other tool output +.tmp/ diff --git a/Cargo.lock b/Cargo.lock index b3151d0..3606591 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -81,45 +81,45 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.103" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "ar_archive_writer" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4087686b4b0a3427190bae57a1d9a478dbb2d40c5dc1bd6e2b6d797913bdd348" +checksum = "73cd58deff2140a0a8eae87e417bd01db68a33e148aa93d1e8cd837e55e312b6" dependencies = [ - "object 0.37.3", + "object 0.39.1", ] [[package]] name = "arbitrary" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +checksum = "3bc62ac97cc33321f50863d514c3bc38a453947a8f9e781137e47c7401020aed" [[package]] name = "ast_node" -version = "6.0.0" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "015d969fabb96fa6cb6d3cc3a7dfcf7cfa277a909e8b1dbfe5c8dd8f957d5cb5" +checksum = "edf54a7a1bf98e127c22e9e2b9d19619092c74283fdc14e4d67da69780f90db6" dependencies = [ "quote", "swc_macros_common", - "syn", + "syn 2.0.119", ] [[package]] name = "async-trait" -version = "0.1.89" +version = "0.1.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.9", ] [[package]] @@ -128,6 +128,12 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "better_scoped_tls" version = "1.0.1" @@ -139,9 +145,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.13.1" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" [[package]] name = "block-buffer" @@ -169,28 +175,19 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "bytes-str" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "577d2bf5650f8554d5a372af5ac93535110a0fc75b3e702bb853369febf227c2" +checksum = "4dde6d05e75a31ec9610eb6446a6f0a10dd30ff5100d720fee4c7c7a9008b5ba" dependencies = [ "bytes", "serde", ] -[[package]] -name = "castaway" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" -dependencies = [ - "rustversion", -] - [[package]] name = "cc" -version = "1.2.67" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38" +checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838" dependencies = [ "find-msvc-tools", "shlex", @@ -198,15 +195,15 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" [[package]] name = "clap" -version = "4.6.2" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd059f9da4f5c36b3787f65d38ccaab1cc315f07b01f89abc8359ee6a8205011" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" dependencies = [ "clap_builder", "clap_derive", @@ -214,9 +211,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.2" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" dependencies = [ "anstream", "anstyle", @@ -226,21 +223,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.1" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 3.0.9", ] [[package]] name = "clap_lex" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" [[package]] name = "cobs" @@ -257,24 +254,11 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" -[[package]] -name = "compact_str" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" -dependencies = [ - "castaway", - "cfg-if", - "itoa", - "ryu", - "static_assertions", -] - [[package]] name = "cpp_demangle" -version = "0.4.5" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2bb79cb74d735044c972aae58ed0aaa9a837e85b01106a54c39e42e97f62253" +checksum = "0667304c32ea56cb4cd6d2d7c0cfe9a2f8041229db8c033af7f8d69492429def" dependencies = [ "cfg-if", ] @@ -290,27 +274,27 @@ dependencies = [ [[package]] name = "cranelift-assembler-x64" -version = "0.133.1" +version = "0.136.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e06aeba2c965fc446d13c56a6ccb2631b78445d7544543dd9a25289977630914" +checksum = "f6d504c8c7ba3f3accaf8e98b7096beb98e1a271404357744c7e9e730c6865ff" dependencies = [ "cranelift-assembler-x64-meta", ] [[package]] name = "cranelift-assembler-x64-meta" -version = "0.133.1" +version = "0.136.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee2d2dde4ec1352715595b5cfa6fe2e5b8ebb9da3457b3ee8db0aa2808c069aa" +checksum = "c065757905596bc06e82969ebd7b92d691b7fd710a7e9e8181ae0c30dab588d0" dependencies = [ "cranelift-srcgen", ] [[package]] name = "cranelift-bforest" -version = "0.133.1" +version = "0.136.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03b4982ef9fa54ec9eee841e891e7ddc5434be1250e88de31572e000c888f30b" +checksum = "5ca7162cd4d4fc5f9f2f88d1459d939882203d93a8c5a19ef2c405b2e82ef13f" dependencies = [ "cranelift-entity", "wasmtime-internal-core", @@ -318,9 +302,9 @@ dependencies = [ [[package]] name = "cranelift-bitset" -version = "0.133.1" +version = "0.136.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "529143118c4eeb58c39ecb02319557d512be6c61348486422974ab8e3906b8a8" +checksum = "0ec2ba2d4f3d959ec4023a10266c3dcfb28381d1938d507a9a5e1abb78bbcc88" dependencies = [ "serde", "serde_derive", @@ -329,9 +313,9 @@ dependencies = [ [[package]] name = "cranelift-codegen" -version = "0.133.1" +version = "0.136.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7780677247ad3577e3a6a3ebf43f39b325a11d6393db72b2c9968a910d4d13d" +checksum = "51a64cb821d5fed56bdd6053acd36fa928cd82b70bf6b19b8b14b8b27d047b9a" dependencies = [ "bumpalo", "cranelift-assembler-x64", @@ -360,9 +344,9 @@ dependencies = [ [[package]] name = "cranelift-codegen-meta" -version = "0.133.1" +version = "0.136.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac9645250416cbf92454fe61160e17e026e0ce405906a54500b114f923ddffc9" +checksum = "142be1e267a7497ba3c2b5a829eaf5387b92b9bcdba6e0a6020f1ffccae7f2b8" dependencies = [ "cranelift-assembler-x64-meta", "cranelift-codegen-shared", @@ -373,24 +357,24 @@ dependencies = [ [[package]] name = "cranelift-codegen-shared" -version = "0.133.1" +version = "0.136.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20ee8d222ff0fd3681791979afbf88586ac9f49010d3db96b3cbe4c96759aee3" +checksum = "0e41f75410da6cce8601330137d73718f679db81cb790a33de3871b5c90a0de3" [[package]] name = "cranelift-control" -version = "0.133.1" +version = "0.136.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "591abe6f5312bd2c4220f1b3bead56c2ad00257c52668015ba013b85dcf2a17a" +checksum = "0c2616c6d01c14921ac9289ef65eb7702d9c760dfdc4537a8ee5bf38ea2832c6" dependencies = [ "arbitrary", ] [[package]] name = "cranelift-entity" -version = "0.133.1" +version = "0.136.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5300c49cf940526fe771517b3b3eabd5d0ff164ee61698579cf403fe8d3af3c" +checksum = "a03e8b44c4877cf2fb21e5788d514af6fd291a066ed9ae37adf114a25fecb4aa" dependencies = [ "cranelift-bitset", "serde", @@ -400,9 +384,9 @@ dependencies = [ [[package]] name = "cranelift-frontend" -version = "0.133.1" +version = "0.136.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da4adbf760207fdbbe130f1191cce01cdef66831a9f648b1f39ff2800d126d45" +checksum = "366ff7cb014bd08752210eba55a650969f8db9b52670d69d2ea0b23e1e9e3edf" dependencies = [ "cranelift-codegen", "hashbrown 0.17.1", @@ -413,15 +397,15 @@ dependencies = [ [[package]] name = "cranelift-isle" -version = "0.133.1" +version = "0.136.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8315b21ff018226a42a60a4702c2dd75f6447cac26e9bca622e14c22088c2ff5" +checksum = "9ea6526ebf9064fff8ce3b1e26afad7027f9e6bb0e45d2b9200e4cbab8343345" [[package]] name = "cranelift-native" -version = "0.133.1" +version = "0.136.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d506ef23a60715bde451b06620b14402166ded3b648454fccbf04f3e46a4aa70" +checksum = "b44d323f3e5108a8a5bc61f3acd227285ece066e19afe9aece211d66b34a64ea" dependencies = [ "cranelift-codegen", "libc", @@ -430,15 +414,15 @@ dependencies = [ [[package]] name = "cranelift-srcgen" -version = "0.133.1" +version = "0.136.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48ed47e602652e3410f9387fc0db70fefadcee4d78a78881421aabcab4e26b89" +checksum = "fc173168f291ff3b95b5d0769d6a05b1eaef5e6d74c2600affab8396b4e7b420" [[package]] name = "crc32fast" -version = "1.5.0" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" dependencies = [ "cfg-if", ] @@ -465,20 +449,26 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.9", ] +[[package]] +name = "dragonbox_ecma" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd8e701084c37e7ef62d3f9e453b618130cbc0ef3573847785952a3ac3f746bf" + [[package]] name = "either" -version = "1.16.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" [[package]] name = "embedded-io" @@ -494,9 +484,9 @@ checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" [[package]] name = "equivalent" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" [[package]] name = "errno" @@ -510,9 +500,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.9" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" [[package]] name = "fnv" @@ -542,14 +532,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ff35a391aef949120a0340d690269b3d9f63460a6106e99bd07b961f345ea9" dependencies = [ "swc_macros_common", - "syn", + "syn 2.0.119", ] [[package]] name = "futures" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" dependencies = [ "futures-channel", "futures-core", @@ -561,9 +551,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" dependencies = [ "futures-core", "futures-sink", @@ -571,33 +561,33 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-channel", "futures-core", @@ -664,11 +654,17 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + [[package]] name = "hstr" -version = "4.0.0" +version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76e2be4baa20c69a32de3b3b8ee67d7a7376591cc61f81ecc08fa43a1e36b897" +checksum = "2b904c58532ecdbf77f46013daec3ecfa9aec83ec17cf4c8053be80a9c1d8d15" dependencies = [ "hashbrown 0.14.5", "new_debug_unreachable", @@ -680,9 +676,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" dependencies = [ "displaydoc", "potential_utf", @@ -694,9 +690,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" dependencies = [ "displaydoc", "litemap", @@ -707,9 +703,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -721,16 +717,17 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" [[package]] name = "icu_properties" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" dependencies = [ + "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", @@ -741,15 +738,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" [[package]] name = "icu_provider" -version = "2.2.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" dependencies = [ "displaydoc", "icu_locale_core", @@ -783,9 +780,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.14.0" +version = "2.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" dependencies = [ "equivalent", "hashbrown 0.17.1", @@ -795,14 +792,14 @@ dependencies = [ [[package]] name = "is-macro" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d57a3e447e24c22647738e4607f1df1e0ec6f72e16182c4cd199f647cdfb0e4" +checksum = "8267aa6001e25494f3015f9663bbd88a18240c74483afa5f0934a1b3e4c388e9" dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -843,11 +840,17 @@ dependencies = [ "swc_common", "swc_ecma_ast", "swc_ecma_parser", - "wasm-encoder 0.253.0", - "wasmparser 0.253.0", + "swc_ecma_transforms_base", + "swc_ecma_transforms_typescript", + "wasm-encoder 0.262.0", + "wasmparser 0.262.0", "wasmtime", ] +[[package]] +name = "jsc-test262" +version = "0.1.0" + [[package]] name = "jsc-wasm" version = "0.1.0" @@ -865,9 +868,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.186" +version = "0.2.191" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" [[package]] name = "libm" @@ -883,15 +886,15 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" [[package]] name = "log" -version = "0.4.33" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "mach2" @@ -907,9 +910,9 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "memfd" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" +checksum = "57804b2c9b69967f1536a56f86297e367a33b19e98852ed624b84551cdbc0d90" dependencies = [ "rustix", ] @@ -933,9 +936,9 @@ dependencies = [ [[package]] name = "num-integer" -version = "0.1.46" +version = "0.1.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" dependencies = [ "num-traits", ] @@ -950,12 +953,13 @@ dependencies = [ ] [[package]] -name = "object" -version = "0.37.3" +name = "num_cpus" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" dependencies = [ - "memchr", + "hermit-abi", + "libc", ] [[package]] @@ -963,6 +967,15 @@ name = "object" version = "0.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" +dependencies = [ + "memchr", +] + +[[package]] +name = "object" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd229a0361b9d0d4396176e02d65897f487eebeab7caa6d443855ee152ca0b9c" dependencies = [ "crc32fast", "hashbrown 0.17.1", @@ -982,6 +995,15 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "par-core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e96cbd21255b7fb29a5d51ef38a779b517a91abd59e2756c039583f43ef4c90f" +dependencies = [ + "once_cell", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1018,7 +1040,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1027,7 +1049,7 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ - "siphasher 1.0.3", + "siphasher 1.0.4", ] [[package]] @@ -1050,27 +1072,27 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" dependencies = [ "zerovec", ] [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] [[package]] name = "psm" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645dbe486e346d9b5de3ef16ede18c26e6c70ad97418f4874b8b1889d6e761ea" +checksum = "4dcd034599e63b970727f70d79e02d62390a4a84f7c6b827c27c46d5ac3fa622" dependencies = [ "ar_archive_writer", "cc", @@ -1078,9 +1100,9 @@ dependencies = [ [[package]] name = "pulley-interpreter" -version = "46.0.1" +version = "49.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b92604caae1a1899b6a5b54967289dd538177c626004c91accf9d0ec7e4a12" +checksum = "def59f28ccaf2cb4f8dfaac83197fd7d56a4470a70e4f2589454c731370101c5" dependencies = [ "cranelift-bitset", "log", @@ -1090,29 +1112,29 @@ dependencies = [ [[package]] name = "pulley-macros" -version = "46.0.1" +version = "49.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a7ac85c0bb3fb351f10d531230aaa5e366b46d7c4e5328e5f02801d6dac1165" +checksum = "3a1834d3b3ef994f266a0c9ed29a8214f2c2faaa9b1c44babfa57566bb05af83" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "quote" -version = "1.0.46" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] [[package]] name = "rand" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" dependencies = [ "rand_core", ] @@ -1125,9 +1147,9 @@ checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" [[package]] name = "regalloc2" -version = "0.15.1" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de2c52737737f8609e94f975dee22854a2d5c125772d4b1cf292120f4d45c186" +checksum = "757712e8e61590d6d4f5d563483755538b5aa13467837a3b41cd9832509a7f85" dependencies = [ "allocator-api2", "bumpalo", @@ -1152,9 +1174,9 @@ checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustix" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" dependencies = [ "bitflags", "errno", @@ -1169,12 +1191,6 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" -[[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - [[package]] name = "scoped-tls" version = "1.0.1" @@ -1199,9 +1215,9 @@ checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc" [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -1209,22 +1225,46 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.9", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", ] [[package]] @@ -1252,9 +1292,9 @@ checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "siphasher" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" [[package]] name = "slab" @@ -1264,9 +1304,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.2" +version = "1.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" dependencies = [ "serde", ] @@ -1279,9 +1319,9 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "stacker" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "640c8cdd92b6b12f5bcb1803ca3bbf5ab96e5e6b6b96b9ab77dabe9e880b3190" +checksum = "707f49d46706bacf8a2b00d51dace3f9de527c13eec3778f570c411f89e69967" dependencies = [ "cc", "cfg-if", @@ -1290,12 +1330,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - [[package]] name = "string_enum" version = "1.0.2" @@ -1304,7 +1338,7 @@ checksum = "ae36a4951ca7bd1cfd991c241584a9824a70f6aff1e7d4f693fb3f2465e4030e" dependencies = [ "quote", "swc_macros_common", - "syn", + "syn 2.0.119", ] [[package]] @@ -1326,9 +1360,9 @@ dependencies = [ [[package]] name = "swc_common" -version = "24.0.0" +version = "26.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5d7b9e59d7284543c4e6a2a9e264fee6443cdf33fa9101c113958f078df12e2" +checksum = "142d2a06cafce623859ca799f5d1935bb8a886f18b66a2585ddeadff210121d0" dependencies = [ "anyhow", "ast_node", @@ -1349,11 +1383,37 @@ dependencies = [ "url", ] +[[package]] +name = "swc_config" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf6924ef781f1f8af3dfdac9674e94f0ab0c20af6b88b2e116af57b1e755b591" +dependencies = [ + "anyhow", + "bytes-str", + "indexmap", + "serde", + "serde_json", + "swc_config_macro", +] + +[[package]] +name = "swc_config_macro" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b416e8ce6de17dc5ea496e10c7012b35bbc0e3fef38d2e065eed936490db0b3" +dependencies = [ + "proc-macro2", + "quote", + "swc_macros_common", + "syn 2.0.119", +] + [[package]] name = "swc_ecma_ast" -version = "26.0.0" +version = "29.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0986e96d361539754e086df7c406865b725eebf8bb255daaa1d5b459c9ca3f4a" +checksum = "2839d2cf4e611b944438c304ec59396221b3db4580acd172fcd70522bb450ee9" dependencies = [ "bitflags", "is-macro", @@ -1368,14 +1428,25 @@ dependencies = [ "unicode-id-start", ] +[[package]] +name = "swc_ecma_hooks" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3abc7b19ffc01d358fe6dfce904cc0a9c30c4db6deed11011b67c320f71682c" +dependencies = [ + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_visit", +] + [[package]] name = "swc_ecma_parser" -version = "42.0.0" +version = "46.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a091f013503c9f17a48871bc6396ceb15e19a8253235d2f3d3c70e8f73df937" +checksum = "2fc77f241a90e3d5f82187bf1b6aa51ac054095b77c29b9e87518f98f0858218" dependencies = [ "bitflags", - "compact_str", "either", "num-bigint", "phf", @@ -1389,6 +1460,105 @@ dependencies = [ "tracing", ] +[[package]] +name = "swc_ecma_transforms_base" +version = "50.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30f910d367033f90ca145da726c3964f7fac664d68e7de0ff87c9803daa93513" +dependencies = [ + "better_scoped_tls", + "indexmap", + "once_cell", + "par-core", + "phf", + "rustc-hash", + "serde", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_parser", + "swc_ecma_utils", + "swc_ecma_visit", + "tracing", +] + +[[package]] +name = "swc_ecma_transforms_react" +version = "56.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b8afd049010c50a4da9a46622a80ad89961a399f91d987ff30e5d36bad6beb4" +dependencies = [ + "base64", + "bytes-str", + "indexmap", + "once_cell", + "rustc-hash", + "serde", + "sha1", + "string_enum", + "swc_atoms", + "swc_common", + "swc_config", + "swc_ecma_ast", + "swc_ecma_hooks", + "swc_ecma_parser", + "swc_ecma_transforms_base", + "swc_ecma_utils", + "swc_ecma_visit", +] + +[[package]] +name = "swc_ecma_transforms_typescript" +version = "56.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56dc3ffb560b68db6fdde19f37fb16723fc8debb675f70f0b518abefee87da78" +dependencies = [ + "bytes-str", + "rustc-hash", + "serde", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_transforms_base", + "swc_ecma_transforms_react", + "swc_ecma_utils", + "swc_ecma_visit", +] + +[[package]] +name = "swc_ecma_utils" +version = "35.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecd5288b59983c77a838f49498a8f88aa7ff9c1c5a059885dceb244293a6701b" +dependencies = [ + "dragonbox_ecma", + "indexmap", + "num_cpus", + "once_cell", + "par-core", + "rustc-hash", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_visit", + "tracing", +] + +[[package]] +name = "swc_ecma_visit" +version = "29.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a830a070fea84a3a8a8c556463f0551e3bfc6cf2f8bdeecd62fa753e22a72289" +dependencies = [ + "new_debug_unreachable", + "num-bigint", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_visit", + "tracing", +] + [[package]] name = "swc_eq_ignore_macros" version = "1.0.1" @@ -1397,7 +1567,7 @@ checksum = "c16ce73424a6316e95e09065ba6a207eba7765496fed113702278b7711d4b632" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1408,17 +1578,16 @@ checksum = "aae1efbaa74943dc5ad2a2fb16cbd78b77d7e4d63188f3c5b4df2b4dcd2faaae" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "swc_visit" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62fb71484b486c185e34d2172f0eabe7f4722742aad700f426a494bb2de232a2" +checksum = "e7df72a1be54e379864cebf04f26abb4d7ee637977f62f8ec8387233f7a27e88" dependencies = [ "either", - "new_debug_unreachable", ] [[package]] @@ -1432,15 +1601,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "synstructure" -version = "0.13.2" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.9", ] [[package]] @@ -1460,29 +1640,29 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.9", ] [[package]] name = "tinystr" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" dependencies = [ "displaydoc", "zerovec", @@ -1507,7 +1687,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1521,9 +1701,9 @@ dependencies = [ [[package]] name = "triomphe" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b40688ea6389c8171614b25491f71d4a27946e0c7ce2da1c6de27e25abf1a0ae" +checksum = "0d15f4f5898bcfc5d8a8e45ed30b2f108d710c1a2e5489dbf80e078f707181bc" dependencies = [ "serde", "stable_deref_trait", @@ -1537,15 +1717,15 @@ checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "unicode-id-start" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81b79ad29b5e19de4260020f8919b443b2ef0277d242ce532ec7b7a2cc8b6007" +checksum = "0825d2e09dcae814a4273c1e1435979d414ede25b2ee235b1545da6682483765" [[package]] name = "unicode-ident" -version = "1.0.24" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" [[package]] name = "unicode-width" @@ -1585,9 +1765,9 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wasm-bindgen" -version = "0.2.126" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" dependencies = [ "cfg-if", "once_cell", @@ -1598,9 +1778,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.126" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1608,51 +1788,51 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.126" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 3.0.9", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.126" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" dependencies = [ "unicode-ident", ] [[package]] name = "wasm-encoder" -version = "0.251.0" +version = "0.258.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a879a421bd17c528b74721b2abf4c62e8f1d1889c2ba8c3c50d02deaf2ce395" +checksum = "f64765e43c000fae008a48db8b288e175e237fc9ce20a26586836878c1bee89f" dependencies = [ "leb128fmt", - "wasmparser 0.251.0", + "wasmparser 0.258.3", ] [[package]] name = "wasm-encoder" -version = "0.253.0" +version = "0.262.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59972d6cd272259de647b7c1f1912e45e289c75ffd4be04e10695507cd7e1b59" +checksum = "4495a6f85dd94c411c749303efcbc5defade0b0cbdbc0541495241ab917a9b5a" dependencies = [ "leb128fmt", - "wasmparser 0.253.0", + "wasmparser 0.262.0", ] [[package]] name = "wasmparser" -version = "0.251.0" +version = "0.258.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "437970b35b1a85cfde9c74b2398352d8d653f3bd8e3a3db0c063ea8f5b4b36ff" +checksum = "fe9269dd817e0e7021bf534ccd9761bc2d64778b6708e41228b8439d75eaa0b1" dependencies = [ "bitflags", "hashbrown 0.17.1", @@ -1663,9 +1843,9 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.253.0" +version = "0.262.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19db11f87d2486580e1e8b6f494c54df7e0566b87d0b599db843c24019667339" +checksum = "974d643b125b272607e2fff8012ea14a8328acd7621e4bafa7afb32500052b1b" dependencies = [ "bitflags", "hashbrown 0.17.1", @@ -1676,33 +1856,32 @@ dependencies = [ [[package]] name = "wasmprinter" -version = "0.251.0" +version = "0.258.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8798c1a699bd25648b6708eefe94d97c6f9891febb94b42cca1f7a4b086ea64e" +checksum = "14a457ba8670a3db3d357c1aeac3ed4fe378a76054a221789f0df0ea507e292b" dependencies = [ "anyhow", "termcolor", - "wasmparser 0.251.0", + "wasmparser 0.258.3", ] [[package]] name = "wasmtime" -version = "46.0.1" +version = "49.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4213d2f019a5e44aa8a61d8826dd33a505bff79f749b14a8bafd67321cb9351" +checksum = "4fbdd39f5c66f63ba6b5b31391b324b7ade7237d29c866cdb561330118ef436e" dependencies = [ "addr2line", "async-trait", "bitflags", "bumpalo", "cc", - "cfg-if", "futures", "libc", "log", "mach2", "memfd", - "object 0.39.1", + "object 0.40.0", "once_cell", "postcard", "pulley-interpreter", @@ -1711,7 +1890,7 @@ dependencies = [ "serde_derive", "smallvec", "target-lexicon", - "wasmparser 0.251.0", + "wasmparser 0.258.3", "wasmtime-environ", "wasmtime-internal-core", "wasmtime-internal-cranelift", @@ -1725,9 +1904,9 @@ dependencies = [ [[package]] name = "wasmtime-environ" -version = "46.0.1" +version = "49.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d45863de41977ec6453e859cf843d456fa3fcb45a659b66d16e794f90ec4f5b7" +checksum = "e915c27826de4f7b4411b61fd6db32ca448657a5136b8c8e8874a15e90cecbf4" dependencies = [ "anyhow", "cpp_demangle", @@ -1738,7 +1917,7 @@ dependencies = [ "hashbrown 0.17.1", "indexmap", "log", - "object 0.39.1", + "object 0.40.0", "postcard", "rustc-demangle", "semver", @@ -1747,8 +1926,8 @@ dependencies = [ "sha2", "smallvec", "target-lexicon", - "wasm-encoder 0.251.0", - "wasmparser 0.251.0", + "wasm-encoder 0.258.3", + "wasmparser 0.258.3", "wasmprinter", "wasmtime-internal-component-util", "wasmtime-internal-core", @@ -1756,15 +1935,15 @@ dependencies = [ [[package]] name = "wasmtime-internal-component-util" -version = "46.0.1" +version = "49.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819ad5abd5822a22dbf4014475cdfd1fe790707761cd732d74aaa3ba4d5ba489" +checksum = "e754a3ff5690c34eb4713bd7c65f60dd607ead4e39042cbf52ac8011de14455b" [[package]] name = "wasmtime-internal-core" -version = "46.0.1" +version = "49.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fc28372e36eaf8cf70faa83b5779137f7e99c8d18569a125d1580e735cc9e4d" +checksum = "ba00c5db77adb0749ee059068e5a417bfac3d8969f5a3479d9bd1ca8c1ba248d" dependencies = [ "anyhow", "hashbrown 0.17.1", @@ -1774,11 +1953,10 @@ dependencies = [ [[package]] name = "wasmtime-internal-cranelift" -version = "46.0.1" +version = "49.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a433efc6e35112a5457e1dc8bc4d8d39820ac7722267e89bc04e5df641f32124" +checksum = "97ceeb4325712a253f463e50a69b5a547fbbecd52e45612360c543eaa25a03ff" dependencies = [ - "cfg-if", "cranelift-codegen", "cranelift-control", "cranelift-entity", @@ -1787,12 +1965,12 @@ dependencies = [ "gimli", "itertools", "log", - "object 0.39.1", + "object 0.40.0", "pulley-interpreter", "smallvec", "target-lexicon", "thiserror", - "wasmparser 0.251.0", + "wasmparser 0.258.3", "wasmtime-environ", "wasmtime-internal-core", "wasmtime-internal-unwinder", @@ -1801,12 +1979,11 @@ dependencies = [ [[package]] name = "wasmtime-internal-fiber" -version = "46.0.1" +version = "49.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a1d3a39d0d210f6b8574ee96a4315e0a14c67f3a1fc3cd5372cb10d2fb4422" +checksum = "3eb3f21bc32b5382ea95f83ce9a5f2f62b68f1b25a37d0802a41a65236eae5ae" dependencies = [ "cc", - "cfg-if", "libc", "rustix", "wasmtime-environ", @@ -1816,9 +1993,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-debug" -version = "46.0.1" +version = "49.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f667288cb4dfa68a4639ffac4d5628535dda64ebdc2b990526efb12b30ba803" +checksum = "e786f9925126e0d2e466c5308e572e8b6226f4047596228a3d63d99528a78949" dependencies = [ "cc", "wasmtime-internal-versioned-export-macros", @@ -1826,11 +2003,10 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-icache-coherence" -version = "46.0.1" +version = "49.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eba651d44ab0faad4c58106b3adb45068189fb65ef50f0c404b6d9e3bf81a357" +checksum = "92cb1dd5da78f5b21d65e91063bdd8c2eb343c1f077a1dcbc29003d4a794abb0" dependencies = [ - "cfg-if", "libc", "wasmtime-internal-core", "windows-sys", @@ -1838,26 +2014,25 @@ dependencies = [ [[package]] name = "wasmtime-internal-unwinder" -version = "46.0.1" +version = "49.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ecc52563b0558af2a7487eb710de07cc4532564b55528876129238e83118cb1" +checksum = "d0243d244f6dd07ccd114e6bb099e8dd1197b47de5c054576adc9483f80f65d8" dependencies = [ - "cfg-if", "cranelift-codegen", "log", - "object 0.39.1", + "object 0.40.0", "wasmtime-environ", ] [[package]] name = "wasmtime-internal-versioned-export-macros" -version = "46.0.1" +version = "49.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e747f4a074699ba1b4e4d841fb263f9b7df5bd1555181c4752bf5990d21ba676" +checksum = "20fa5616cd875ec78f5b92020fa47ced7c8bb923079185c8475751098b8c4a69" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1886,9 +2061,9 @@ dependencies = [ [[package]] name = "writeable" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" [[package]] name = "yoke" @@ -1903,34 +2078,34 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.2" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.9", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.54" +version = "0.8.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" +checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.54" +version = "0.8.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" +checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -1944,21 +2119,21 @@ dependencies = [ [[package]] name = "zerofrom-derive" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.9", "synstructure", ] [[package]] name = "zerotrie" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" dependencies = [ "displaydoc", "yoke", @@ -1967,9 +2142,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.6" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" dependencies = [ "yoke", "zerofrom", @@ -1978,11 +2153,17 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.3" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.9", ] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml index 3aa2b87..3dbb9e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ [workspace] resolver = "3" -members = ["crates/jsc-core", "crates/jsc", "crates/jsc-wasm"] +members = ["crates/jsc-core", "crates/jsc", "crates/jsc-wasm", "crates/jsc-test262"] exclude = ["tmp"] [workspace.package] version = "0.1.0" edition = "2024" -rust-version = "1.96" +rust-version = "1.99" license = "Apache-2.0" publish = false diff --git a/DESIGN.md b/DESIGN.md index 02f7ab7..3cd674c 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -1,2456 +1,1116 @@ # jsc — Design -> A Rust-native **ahead-of-time (AOT) JavaScript compiler**. jsc parses JavaScript -> with swc, lowers it to an in-house IR, emits WebAssembly, and executes the module -> on wasmtime — no external toolchain at runtime. +> An ahead-of-time **JavaScript → WebAssembly** compiler written in Rust, with +> conformance tracked by test262. jsc parses source with swc, lowers it to an +> in-house IR, emits a WebAssembly module with `wasm-encoder`, and runs that +> module on wasmtime (natively) or in a JS shim (in the browser). -This is the authoritative architecture spec. It covers the pipeline, the value and -memory model, the ABI, and the language as it grew from the MVP onward through the core language -feature waves (all implemented). Each wave has its own reference section in §8. +This is the authoritative technical spec. It describes the compiler as it is +today: the pipeline, the value and memory model, the module ABI, the runtime and +host imports, the supported language surface, diagnostics, tooling, and the +known limitations. ## Contents -- [1. Goal & MVP milestone](#1-goal--mvp-milestone) -- [2. Architecture: end-to-end pipeline](#2-architecture-end-to-end-pipeline) -- [3. Design decisions](#3-design-decisions) -- [4. Module & crate layout](#4-module--crate-layout) -- [5. Constraints (hard locks)](#5-constraints-hard-locks) -- [6. CI gates](#6-ci-gates) -- [7. Building a static release](#7-building-a-static-release) -- [8. Language reference](#8-language-reference) -- [9. Diagnostics (compile-time)](#9-diagnostics-compile-time) -- [10. Roadmap](#10-roadmap) +- [1. Overview](#1-overview) +- [2. Pipeline](#2-pipeline) +- [3. Crate & package layout](#3-crate--package-layout) +- [4. Value model & module ABI](#4-value-model--module-abi) +- [5. Heap & allocator](#5-heap--allocator) +- [6. Runtime & host imports](#6-runtime--host-imports) +- [7. Language surface](#7-language-surface) +- [8. Diagnostics](#8-diagnostics) +- [9. CLI](#9-cli) +- [10. Conformance (test262)](#10-conformance-test262) +- [11. CI gates](#11-ci-gates) +- [12. Building a static release](#12-building-a-static-release) +- [13. Current limitations](#13-current-limitations) +- [14. Roadmap](#14-roadmap) --- -## 1. Goal & MVP milestone - -A Rust-native AOT compiler for JavaScript. The first milestone compiles a -numeric-expression subset end to end and runs it, proving every pipeline stage is -wired. - -**MVP acceptance — the one command that must work:** - -``` -jsc -e "40 + 2" -42 -``` - -`jsc -e ""` parses the expression, compiles it to a wasm module exporting a -single function, runs that function on wasmtime, and prints the returned number. - -MVP scope is exactly: number literals, the binary operators `+ - * /`, and -parenthesized grouping. Everything else (variables, control flow, functions, the -full jsval type model) is roadmap (§10). - -**Success criteria (verifiable):** - -- `jsc -e "40 + 2"` prints `42`. -- `jsc -e "6 * (1 + 6)"` prints `42` (precedence + parens). -- `jsc -e "9 / 2"` prints `4.5` (f64 division, not integer). -- All CI gates (§6) pass on the workspace. +## 1. Overview + +jsc compiles a JavaScript program **ahead of time** into a WebAssembly module. +There is no interpreter and no JIT: every program becomes a real, validatable +wasm module whose exported `main` function runs the top-level code. + +- **Output.** A wasm module (`jsc build FILE -o OUT.wasm`), or an immediate run of + that module on an embedded wasmtime (`jsc FILE`, `jsc -e`, `jsc -p`). +- **Embeddable core.** `jsc-core` is a library: `frontend::parse` → + `codegen::lower` → `emit::emit` produce module bytes; the optional `run` stage + (feature `native-run`) executes them on wasmtime. +- **In the browser.** `crates/jsc-wasm` compiles `jsc-core` (without the `run` + stage) to wasm via `wasm-bindgen`; the `@cdlab/jsc` package (`packages/jsc`) + wraps it with a TypeScript host shim, and `apps/jsc-web` is a browser + playground on top of that package. +- **Input language.** JavaScript, or TypeScript reduced to JavaScript by type + stripping (no type checking). `frontend::parse(src, dialect)` takes a + `Dialect`: the CLI selects `Dialect::Ts` for `.ts` / `.mts` / `.cts` files, + rejects `.tsx`, and uses `Dialect::Js` for every other file and for `-e` / + `-p`; `jsc-wasm` and the test262 harness always use `Dialect::Js`. +- **Semantics target.** Full JavaScript semantics is the direction; the compiled + subset follows strict-mode rules (§7, §13). Conformance is measured by the + test262 runner (§10). + +Three structural decisions shape the implementation: + +- **The IR is the seam.** A Rust `enum IrNode` / `IrStmt` (`crates/jsc-core/src/ir.rs`) + sits between the swc AST and the wasm backend. swc AST shapes never reach + `emit`; wasm concerns never reach `codegen`. Language features are added by + extending the IR and the two walks over it. +- **Emit wasm directly.** The backend builds the module straight from the IR with + `wasm-encoder` — no C, text, or LLVM intermediate. +- **One value word.** Every JS value is a single NaN-boxed `i64`; all heap data + lives in one bump-allocated linear memory (§4, §5). --- -## 2. Architecture: end-to-end pipeline +## 2. Pipeline ``` -JS source ──▶ swc AST ──▶ in-house IR ──▶ wasm bytes ──▶ result - (-e str) (frontend) (codegen) (emit) (run) - swc_ecma_ enum IrNode wasm-encoder wasmtime - parser +JS source ──▶ swc AST ──▶ IR ──────────▶ wasm bytes ──▶ result + frontend codegen emit run (wasmtime) + swc_ecma_ IrProgram / wasm-encoder or the JS shim + parser IrNode/IrStmt (@cdlab/jsc) ``` -Five stages, one per pipeline hop: - -1. **frontend** — hand the source string to `swc_ecma_parser`, get back an - `swc_ecma_ast` `Expr`/`Module`. swc owns lexing and parsing (precedence, - associativity, parentheses), so jsc never writes a parser. -2. **codegen** — walk the swc AST and build the in-house IR (`IrNode`). This is the - AST → IR lowering. -3. **emit** — walk the IR and produce wasm bytes with `wasm-encoder`. -4. **run** — load the wasm bytes into wasmtime, instantiate, call the exported - entry function, read its return value. -5. **CLI** — a thin `clap` shell that reads `-e `, drives stages 1–4, and - prints the result. The `-e`/`--eval` arg sets `allow_hyphen_values = true` so a - leading-minus expression (e.g. `jsc -e "-0"`, `jsc -e "-5 * 2"`) is taken as the - value rather than parsed as an unknown flag. - -The **IR is the stable seam** between frontend and backend: swc AST shapes never -reach `emit`, and wasm concerns never reach `codegen`. This is the single most -important boundary for the later feature waves (§10) — new language features are -added by extending `IrNode` and the two walks over it, not by touching swc or -wasmtime glue. +1. **frontend** (`frontend.rs`) — hands the source to `swc_ecma_parser` with + `Syntax::Es(EsSyntax::default())` (`Dialect::Js`) or + `Syntax::Typescript(TsSyntax::default())` (`Dialect::Ts`) and returns a + `swc_ecma_ast::Program`. A fatal parse error, or else the first recoverable + error swc collected (e.g. a strict-mode early error), becomes a spanned + `CompileError` (§8). `Dialect::Ts` programs then run through swc's + `resolver` and TypeScript `strip` pass, so codegen only sees plain + ECMAScript. `strip` lowers `enum` and `namespace` to `var`-based code that + codegen rejects (`undeclared variable`, §13). +2. **codegen** (`codegen/`) — walks the AST and builds an `IrProgram`: the `main` + entry (top-level statements), the lifted user functions, and per-program + facts (uniform arity `K`, module-cell count). This stage owns scoping, + capture analysis, desugaring, and every compile-time rejection. +3. **emit** (`emit/`) — walks the IR and produces the module: types, imports, + function bodies, the runtime helper functions, the funcref table, memory, + globals, exports, data segments for interned strings. +4. **run** (`run/`, feature `native-run`) — instantiates the module on wasmtime + with a `Linker` that supplies the six host imports (§6), calls `main`, + drains the microtask queue, checks for an uncaught exception, and decodes the + result jsval against a snapshot of linear memory. + +`run::compile_and_run` and `run::compile_and_run_capturing` chain all four +stages; the CLI and `jsc-wasm` call the stages individually. --- -## 3. Design decisions - -Three choices define jsc's shape: - -- **The IR is the stable seam.** A Rust `enum IrNode` sits between the swc AST and the - wasm backend — idiomatic, exhaustively matchable, compiler-checked. swc AST shapes - never reach `emit`; wasm concerns never reach `codegen`. New language features are - added by extending `IrNode` and the two walks over it, not by touching the parser or - the wasmtime glue. -- **Emit wasm directly.** The backend produces a wasm module straight from the IR with - `wasm-encoder` — no C or text intermediate. The result is a real, validatable module - you can inspect with `--emit wasm`. -- **Embed the runtime.** `wasmtime` runs the module in-process, so there is no external - toolchain at runtime — parse, compile, and execute all happen inside one binary. - -Values are a single NaN-boxed `i64`, and all heap data lives in one bump-allocated -linear-memory arena (§8). This keeps the value model uniform and the -backend free of per-type carrier plumbing. - ---- +## 3. Crate & package layout -## 4. Module & crate layout - -A Cargo workspace: a core library crate (`jsc-core`) plus a thin binary crate (`jsc`). - -As the language subset grew (MVP → the exceptions wave), the three pipeline stages `codegen`, -`emit`, and `run` outgrew single files and were split — swc-style — into -**directory modules** with one submodule per concern and a `tests.rs` submodule -holding that stage's unit tests. The single-file stages (`frontend`, `ir`) and -`lib` keep their code in the `.rs` file but extract their unit tests into a -sibling `tests` submodule. Integration tests are reorganized by theme under each -crate's `tests/`, sharing a `tests/common/` helper module. +A Cargo workspace (`resolver = "3"`) with four members, plus a Bun workspace for +the JS side. ``` jsc/ -├── Cargo.toml # [workspace]; shared lint table, MSRV, edition, [profile.dist] +├── Cargo.toml # workspace, shared lints, [profile.release], [profile.dist] +├── rust-toolchain.toml # channel 1.99.1 (+ rustfmt, clippy) ├── crates/ -│ ├── jsc-core/ # library: the compiler pipeline +│ ├── jsc-core/ # library: the compiler pipeline │ │ ├── src/ -│ │ │ ├── lib.rs # pipeline entry; tests.rs sibling -│ │ │ ├── frontend.rs # swc parse wrapper; tests.rs sibling -│ │ │ ├── ir.rs # IrProgram / IrNode types; tests.rs sibling -│ │ │ ├── codegen/ # AST -> IR, split by concern -│ │ │ │ ├── mod.rs # driver + shared lowering state -│ │ │ │ ├── expr.rs # expression lowering -│ │ │ │ ├── stmt.rs # statement lowering -│ │ │ │ ├── op.rs # operator mapping -│ │ │ │ ├── function.rs # function / closure lifting -│ │ │ │ ├── capture.rs # captured-binding analysis -│ │ │ │ ├── arity.rs # call-arity handling -│ │ │ │ ├── scope.rs # lexical scope / bindings -│ │ │ │ └── tests.rs # codegen unit tests -│ │ │ ├── emit/ # IR -> wasm (wasm-encoder), split by concern -│ │ │ │ ├── mod.rs # driver + module builder -│ │ │ │ ├── sections.rs # wasm section assembly -│ │ │ │ ├── expr.rs # expression opcode emission -│ │ │ │ ├── stmt.rs # statement / control-flow emission -│ │ │ │ ├── numeric.rs # f64 / numeric ops -│ │ │ │ ├── value.rs # jsval encode helpers -│ │ │ │ ├── array.rs # array heap ops -│ │ │ │ ├── object.rs # object heap ops -│ │ │ │ ├── function.rs # function bodies / call frames -│ │ │ │ ├── runtime.rs # runtime helpers / host imports -│ │ │ │ └── tests.rs # emit unit tests -│ │ │ └── run/ # wasmtime load + call, split by concern -│ │ │ ├── mod.rs # run entry -│ │ │ ├── harness.rs # wasmtime engine/instance harness -│ │ │ ├── host.rs # __convert / __method host imports -│ │ │ ├── value.rs # jsval decode + JS-style render -│ │ │ └── tests.rs # run unit tests -│ │ └── tests/ # integration tests by theme -│ │ ├── common/mod.rs # shared compile-and-run helpers -│ │ ├── arrays.rs console.rs conversions.rs errors.rs -│ │ ├── closures.rs functions.rs math.rs strings.rs -│ │ ├── regression.rs # cross-stage regression -│ │ └── unsupported.rs # rejected-syntax guards -│ └── jsc/ # binary: clap shell, `jsc -e` -│ ├── src/main.rs -│ └── tests/ # CLI integration tests by theme -│ ├── common/mod.rs # shared CLI-invocation helpers -│ ├── cli_modes.rs arrays.rs builtins.rs closures.rs -│ ├── control_flow.rs errors.rs functions.rs numbers.rs -│ ├── objects.rs operators.rs strings.rs -│ └── regression.rs -├── DESIGN.md # this document -└── docs/ # plan + task tracking +│ │ │ ├── lib.rs # stage modules; `run` behind feature `native-run` +│ │ │ ├── abi.rs # single source of the runtime ABI constants +│ │ │ ├── error.rs # CompileError (message + optional span) + caret renderer +│ │ │ ├── frontend.rs # swc parse wrapper +│ │ │ ├── ir.rs # IrProgram / IrFunction / IrNode / IrStmt +│ │ │ ├── codegen/ # AST -> IR +│ │ │ │ ├── mod.rs # driver, shared lowering state, module cells +│ │ │ │ ├── expr.rs stmt.rs # expression / statement lowering +│ │ │ │ ├── op.rs # operator + Math mapping +│ │ │ │ ├── function.rs # function / arrow / method lifting +│ │ │ │ ├── capture.rs # capture analysis, object-shape facts +│ │ │ │ ├── arity.rs # uniform-ABI K scan +│ │ │ │ ├── scope.rs # scope stack, binding resolution, TDZ guard +│ │ │ │ └── generator.rs # generator / async state-machine CFG +│ │ │ ├── emit/ # IR -> wasm +│ │ │ │ ├── mod.rs # module builder, globals, Helpers index table +│ │ │ │ ├── sections.rs # type / import / function index layout +│ │ │ │ ├── expr.rs stmt.rs # node / statement emission +│ │ │ │ ├── numeric.rs # arithmetic, comparisons, Math, conversions +│ │ │ │ ├── value.rs # jsval helpers (__box_f64, __to_number, type tests) +│ │ │ │ ├── array.rs object.rs function.rs generator.rs +│ │ │ │ ├── json_parse.rs # in-wasm JSON.parse +│ │ │ │ └── runtime.rs # __alloc, trampolines, Map/Set, equality kernels +│ │ │ └── run/ # wasmtime host +│ │ │ ├── harness.rs # Linker, host imports, main call, OOM + uncaught checks +│ │ │ ├── host.rs # __method dispatch, host heap allocation +│ │ │ ├── promise.rs # promise engine + microtask queue + async driver +│ │ │ └── value.rs # jsval decode + JS-style rendering +│ │ └── tests/ # integration tests by theme (arrays, equality, memory, +│ │ # nan_canonical, promises, to_number, unsupported, ...) +│ ├── jsc/ # binary: clap CLI (src/main.rs) + CLI integration tests +│ ├── jsc-test262/ # test262 conformance runner (§10) +│ └── jsc-wasm/ # wasm-bindgen boundary: `compile(src) -> Uint8Array` +├── packages/jsc/ # @cdlab/jsc: compile() + run() in JS, host shim +│ └── src/ index.ts shim.ts abi.ts value.ts +├── apps/jsc-web/ # browser playground (Vite + React + CodeMirror) +├── scripts/ # build-wasm.sh, ensure-wasm.sh, test262-fetch.sh +└── tests/test262/ # harness shims, denylist, baseline, history ``` -**IR (MVP subset).** For the MVP the IR represents a folded arithmetic expression -tree over f64: +**`jsc-core`.** The library. `#![forbid(unsafe_code)]`. The default feature +`native-run` pulls in `wasmtime` (`cranelift`, `runtime`, `std`); building with +`default-features = false` leaves frontend + codegen + emit only. Dependencies: +`swc_ecma_parser 46`, `swc_ecma_ast 29`, `swc_common 26`, +`swc_ecma_transforms_base 50`, `swc_ecma_transforms_typescript 56`, +`wasm-encoder 0.262`, `anyhow`, optional `wasmtime 49`. + +**`jsc`.** A thin `clap` shell (§9). `#![forbid(unsafe_code)]`. + +**`jsc-wasm`.** `crate-type = ["cdylib", "rlib"]`, depends on `jsc-core` with +`default-features = false`. Exposes one `#[wasm_bindgen]` function, +`compile(src) -> Result, JsError>`; a spanned error becomes the caret +block labelled ``. Its lint table mirrors the workspace's but relaxes +`unsafe_code` to `deny` because the `wasm-bindgen` glue expands to generated +`unsafe` shims. `wasm-pack` builds it with `wasm-opt -Oz`. + +**`@cdlab/jsc`** (`packages/jsc`). `compile(src)` returns module bytes from the +wasm-bindgen artifact; `run(src)` compiles, instantiates the module against a +TypeScript host shim (`shim.ts`, which implements the same six `env` imports and +promise engine as `run/`), calls `main`, and returns `{ result, stdout }`. +`abi.ts` and `value.ts` are ports of `abi.rs` and `run/value.rs`. The wasm +artifact is gitignored; `scripts/ensure-wasm.sh` rebuilds it (via +`scripts/build-wasm.sh` → `wasm-pack build crates/jsc-wasm --target web`) when +missing or stale. + +**`apps/jsc-web`.** A browser playground that imports `compile`/`run` from +`@cdlab/jsc`. + +### Workspace constraints + +- **Toolchain.** Rust edition 2024, MSRV 1.99, pinned to `1.99.1` by + `rust-toolchain.toml`. +- **Safety.** `unsafe_code = "forbid"` in `[workspace.lints.rust]` (relaxed to + `deny` only in `jsc-wasm`). +- **Lints.** `warnings = deny` is inherited from `[workspace.lints]`, so + `cargo clippy` needs no `-D`. `clippy::pedantic` warns; `unwrap_used`, + `expect_used`, `panic`, and `dbg_macro` are denied — runtime paths never panic. -```rust -enum IrNode { - Num(f64), // number literal - Binary { op: BinOp, lhs: Box, rhs: Box }, -} -enum BinOp { Add, Sub, Mul, Div } -``` +--- -- **All numbers are f64.** JavaScript's number type is IEEE-754 double, so `9 / 2` - is `4.5`. This maps one-to-one to wasm's `f64` value type, making `emit` trivial. -- **Parenthesization is not an IR node.** swc resolves grouping and precedence - during parsing, so the AST tree shape already encodes it. -- **Constant folding is optional** for the MVP — emitting `f64.add` over two - `f64.const` operands produces the same result at run time. +## 4. Value model & module ABI -**Wasm emission (MVP).** One function `main` with signature `() -> f64`; its body is -a post-order walk of the IR emitting `f64.const` / `f64.add` / `f64.sub` / -`f64.mul` / `f64.div`, then `end`. Exported as `"main"`. No memory, no imports, no -globals — sections used: Type, Function, Export, Code. `run` calls `main`, receives -an `f64`, and the CLI formats it JS-style (integral doubles print without `.0`). +### 4.1 The NaN-boxed `jsval` ---- +Every JS value is one 64-bit word carried as a wasm **`i64`**. `QNAN = +0x7FF8_0000_0000_0000`. A word whose bits do not contain the full `QNAN` mask is an +ordinary IEEE-754 `f64` (a number). With the mask set, the 3-bit tag in bits +`[50:48]` discriminates: -## 5. Constraints (hard locks) +| tag | meaning | encoding | +|---|---|---| +| `0b000` | the number NaN | exactly `0x7FF8_0000_0000_0000` (canonical) | +| `0b001` | boolean | `false = 0x7FF9_0000_0000_0000`, `true = 0x7FF9_0000_0000_0001` | +| `0b010` | `null` | `0x7FFA_0000_0000_0000` | +| `0b011` | `undefined` | `0x7FFB_0000_0000_0000` | +| `0b100` | unused | — | +| `0b101` | heap pointer | `0x7FFD_0000_0000_0000 \| ptr` (32-bit byte offset in the low bits) | +| `0b110` | unused | — | +| `0b111` | unused | — | + +There is exactly **one** heap tag. The concrete heap kind (string, array, +object, function, …) is the **type byte** at offset 0 of the heap block (§5), +not the NaN-box tag. + +**Why `i64`, not `f64`.** The wasm spec allows NaN payloads to be canonicalized +by `f64` arithmetic and at the host boundary, which would destroy tags. `i64` +moves are bit-preserving. Numeric operations reinterpret operands to `f64` +(`f64.reinterpret_i64`), compute with `f64.*`, and rebox the result. + +**NaN canonicalization.** The number NaN is *always* exactly `QNAN`. Every site +that turns an `f64` back into a jsval — arithmetic, `Math.*`, a `Date` payload, +the host clock — calls the `__box_f64(x: f64) -> i64` helper +(`x == x ? reinterpret(x) : QNAN`), because wasm may produce a sign-bit or +payload-carrying NaN (`0/0` on x86, or a NaN operand such as `undefined` +propagating its tag bits). Integer-to-float conversions cannot produce NaN and +reinterpret directly. Consequently NaN tests compare bits directly, and a +computed NaN can never alias a boolean, `null`, `undefined`, or pointer. + +**`typeof`.** `typeof` yields an interned heap string: the type index +(`number`, `boolean`, `undefined`, `object`, `string`, `function` — +`abi::TYPEOF_STRINGS`) selects the data-segment offset of the matching word, so +`typeof x === "number"`, concatenation, and `.length` all go through ordinary +string machinery. Heap kinds other than string and function report `"object"`. + +**Truthiness.** Falsy is exactly `false`, `null`, `undefined`, `±0`, `NaN`, and +the empty string; every other value — including `[]`, `{}`, and every function — +is truthy. + +### 4.2 Module layout + +**Types.** `main` has type `() -> i64`. Every user function (declarations, +expressions, arrows, methods, constructors, generator state machines) shares +**one** uniform type: -Hard acceptance gates, not preferences: +```text +(a0: i64, …, a_{K-1}: i64, env: i64, argc: i64) -> i64 +``` -- **Toolchain.** MSRV **1.96**, Rust **edition 2024** (pinned via - `rust-toolchain.toml`). -- **Safety.** `#![forbid(unsafe_code)]` in every crate. -- **Lints.** Deny warnings via the workspace-inherited `[workspace.lints]` table - (`warnings = deny`; `-D` is not needed at the clippy call site). No panics in - runtime paths (`unwrap_used` / `expect_used` / `panic` denied). -- **swc from crates.io.** The frontend tracks the latest published majors - (`crates/jsc-core/Cargo.toml`): `swc_ecma_parser = "42"`, - `swc_ecma_ast = "26"`, `swc_common = "24"`. The workspace is self-contained - and builds on any machine — no local checkout required. -- **Pure-Rust dependencies.** `wasm-encoder` (emit), `wasmtime` (run), `clap` (CLI). -- **Workspace layout** as described in §4. +`K` is the program-wide maximum positional arity, computed by `codegen/arity.rs` +from every function's parameter count and every call site's argument count +(including callback-invoking built-ins, promise executors, and setters, which +are called with one argument by assignment). `env` is the closure environment +(`undefined` for a non-capturing callee); `argc` is the actual argument count as +a number jsval. A single signature lets `call_indirect` serve every function. + +**Function index space.** + +| index | function | +|---|---| +| `0`–`5` | host imports `print`, `__convert`, `__method`, `__promise`, `__then`, `__now` | +| `6` | `main` (exported as `"main"`) | +| `7 … 6+n` | the `n` user functions | +| then | runtime helpers, in a fixed order: `__alloc`, `__concat`, `__str_eq`, `__str_cmp`, `__str_index`, `__obj_get`, `__obj_set`, `__array_push`, `__obj_rest`, `__array_concat`, `__obj_keys`, `__obj_values`, `__obj_entries`, `__obj_has`, `__obj_delete`, `__json_escape`, `__json_str`, `__same_value_zero`, `__map_find`, `__map_set`, `__map_get`, `__map_has`, `__map_delete`, `__loose_eq`, `__str_to_array`, the `JSON.parse` family (`__json_ws`, `__json_throw`, `__json_string`, `__json_number`, `__json_value`, `__json_array`, `__json_object`, `__array_to_object`, `__json_parse`), `__gen_to_array`, `__nullish_throw`, `__box_f64`, `__to_number` | +| last | `__invoke1`, `__async_step`, and the two `new Promise` settler functions | + +**Table.** When the program has functions, a funcref table of exactly `n + 2` +entries (min = max) is filled by an active element segment with the user +functions and the two settler functions; a function value's `func_idx` is a slot +in this table. + +**Memory.** One linear memory, `min = 1` page, `max = 8192` pages (512 MiB, +`abi::MEMORY_MAX_PAGES`), exported as `"memory"`. + +**Globals.** + +| index | name | type | exported as | role | +|---|---|---|---|---| +| 0 | `$__heap_ptr` | `i32` | `__heap_ptr` | bump-allocator cursor (§5) | +| 1 | `$exc_pending` | `i32` | `__exc_pending` | `1` while an exception propagates | +| 2 | `$exc_value` | `i64` | `__exc_value` | the in-flight thrown jsval | +| 3 | `$cf_kind` | `i32` | — | pending `return`/`break`/`continue` routed through `finally` | +| 4 | `$cf_value` | `i64` | — | routed return value / labeled-loop target | +| 5 | `$__this` | `i64` | — | `this` carrier, initialized to `undefined` | +| 6–8 | `$__json_data`, `$__json_pos`, `$__json_len` | `i32` | — | `JSON.parse` cursor | +| 9+ | module cells | `i64` | — | one cell pointer per module binding (§7.1) | + +**Exports.** `main`, `memory`, `__heap_ptr`, `__exc_pending`, `__exc_value`, +`__invoke1(fn: i64, arg: i64) -> i64`, `__async_step(gen: i64, sent: i64) -> i64`. + +### 4.3 Calling convention + +- **Direct calls.** A call to a statically known, non-capturing top-level + function emits `call` with its arguments, then `undefined` padding up to `K` + slots, then `env = undefined`, then `argc`. +- **Indirect calls.** A call through a function *value* evaluates the callee + once, stashes the block base, pushes the arguments and padding, then reads the + callee's own `env` (`+HEADER_SIZE+4`) and `func_idx` (`+HEADER_SIZE`) and issues + `call_indirect`. `env` is last so the callee data is read only after every + argument (which may itself contain calls) has been evaluated. +- **Arity.** Calls are not arity-checked: missing arguments read as `undefined`, + extra arguments are ignored, and `argc` carries the real count so a trailing + rest parameter collects exactly `[arg_p, …, arg_{argc-1}]`. A spread call + (`f(...xs)`) fills the `K` positional slots from the flattened array; elements + beyond `K` are dropped (§13). +- **`this`.** `this` rides the `$__this` global rather than a parameter. A method + call `obj.m(args)` evaluates `obj` and the arguments, then sets + `$__this = obj` immediately before `call_indirect`; a plain call resets it to + `undefined`. A function that reads `this` snapshots `$__this` into a local as + the first statement of its prologue, so nested calls cannot clobber it. +- **Exceptions.** After every call into user code the caller checks + `$exc_pending` and branches to its nearest handler or returns early (§6.3). --- -## 6. CI gates +## 5. Heap & allocator -The full gate runs in [`.github/workflows/ci.yml`](.github/workflows/ci.yml) -on push and pull_request, pinned to toolchain **1.96.0**: +### 5.1 Block layouts -- `cargo fmt --all --check` -- `cargo clippy --all-targets --workspace` (deny-warnings inherited from lints) -- `cargo nextest run --workspace` -- `cargo test --doc --workspace` (doctests) -- `cargo deny check` (supply chain) -- `cargo shear` (unused-dependency check) -- `typos` +Every heap block starts with a 4-byte header: the **type byte** at offset 0 and +three padding bytes; the payload starts at `HEADER_SIZE = 4`. Multi-byte +fields are little-endian. -Because swc is consumed from crates.io (§5), every gate — including the ones that -compile the workspace (clippy / nextest / doctests / shear) — runs unconditionally -on GitHub-hosted runners. A `release (musl, profile dist)` job also builds the -portable static binary (§7). +| type byte | kind | layout | `typeof` | +|---|---|---|---| +| `0` `TYPE_STRING` | string | `[0][pad×3][len:u32][utf8 bytes × len]` | `string` | +| `1` `TYPE_ARRAY` | array | `[1][pad×3][len:u32][elem:i64 × len]` | `object` | +| `2` `TYPE_OBJECT` | object | `[2][pad×3][n:u32][(key:i64, val:i64) × n]` | `object` | +| `3` `TYPE_FUNCTION` | function value | `[3][pad×3][func_idx:u32][env:i64]` | `function` | +| `4` `TYPE_ENV` | closure environment (internal) | `[4][pad×3][count:u32][cell_ptr:i64 × count]` | — | +| `5` `TYPE_CELL` | boxed binding (internal) | `[5][pad×3][value:i64]` | — | +| `6` `TYPE_MAP` | `Map` handle | `[6][pad×3][size:u32][entries:i64]` | `object` | +| `7` `TYPE_SET` | `Set` handle | `[7][pad×3][size:u32][entries:i64]` | `object` | +| `8` `TYPE_PROMISE` | promise | `[8][pad×3][state:i32][value:i64]` | `object` | +| `9` `TYPE_GENERATOR` | generator handle | `[9][pad×3][sm_idx:u32][state:i32][done:i32][env:i64]` | `object` | +| `10` `TYPE_DATE` | `Date` | `[10][pad×3][pad×4][ms:f64]` | `object` | +| `11` `TYPE_ACCESSOR` | accessor pair (internal) | `[11][pad×3][pad×4][getter:i64][setter:i64]` | — | + +Notes per kind: + +- **Strings** store UTF-8 bytes; `len` and `.length` are byte counts (§13). + String literals are interned into active data segments starting at offset `8` + (offset `0` is a null guard), so a literal costs no run-time allocation and + identical literals (including object keys) share one blob. +- **Arrays** hold full jsvals, so they nest. `arr[i]` for `0 <= i < len` is an + `i64.load` at `ptr + HEADER_SIZE + 4 + 8*i`; an out-of-range read yields + `undefined`. +- **Objects** are a count-prefixed entry table; keys are heap-string jsvals + compared by byte content (`__str_eq`). Insertion order is table order. + Reserved keys beginning with `@@` — `@@prototype`, `@@constructor`, + `@@errorkind` — are hidden from enumeration (`for-in`, `Object.keys/values/ + entries`, `JSON.stringify`) by a first-byte `@` check, yet `__obj_get` reads + them. +- **Map / Set** handles point (`entries`) at a growable array holding + `(key, value)` pairs flattened into even/odd slots (a Set's value slot is + `undefined`). Growth reallocates the backing array and rewrites `entries` in + place, so the handle jsval never moves. Lookup is a linear scan with + `__same_value_zero`. +- **Promise** blocks mirror settlement (`state` ∈ pending `0` / fulfilled `1` / + rejected `2`, plus the settled `value`) so a memory snapshot can render them; + reactions live in the host engine (§6.4). +- **Generator** handles carry the state-machine table index, the resume state, + the `done` flag, and the frame (a `TYPE_ENV` of cells holding every generator + local). +- **Accessor** blocks are stored under a property name on a class prototype and + are always consumed by the member get/set path; they are never observed as + values. + +### 5.2 `__alloc` and memory growth + +`__alloc(size: i32) -> i32` is a bump allocator over the `$__heap_ptr` global, +which starts at the first free offset after all static data: + +1. `old = __heap_ptr`; `end = u64(old) + u64(size)` (computed in `i64`, so it + cannot wrap). +2. Fast path: if `end <= memory.size * 64 KiB`, set `__heap_ptr = end` and return + `old`. +3. Otherwise grow: try `memory.grow(max(need, memory.size))` (at least doubling); + if that fails, retry with exactly `need` pages. +4. If both fail (the 8192-page / 512 MiB maximum is reached), store the sticky + sentinel `abi::HEAP_PTR_OOM` (`-1`, i.e. `0xFFFF_FFFF`) in `__heap_ptr` and + execute `unreachable`. + +Host-side allocations (strings and arrays built by `__convert`, `__method`, the +promise engine) bump the same exported `__heap_ptr` global and grow the same +memory, so host-built values are visible to the guest without copying. Every +host import result goes through `oom_guard`, which turns a sentinel heap pointer +into a trap. + +**Out of memory.** After `main` (and after the post-`main` microtask drain) the +harness checks the sentinel; if set, the run fails with +`Uncaught RangeError: out of memory` — no wasm backtrace — regardless of the trap +it caused. Because the sentinel makes every later allocation fail, an +out-of-memory condition inside `try` is not catchable; the program terminates. + +**No free, no GC.** Nothing is ever freed. Reallocation (`push` growth, Map/Set +growth) copies into a fresh block and abandons the old one. Long-running +allocation-heavy programs consume memory monotonically up to the 512 MiB cap. --- -## 7. Building a static release - -For a portable, self-contained binary, build against the musl target with the -optimized `[profile.dist]` profile (defined in the root `Cargo.toml`): +## 6. Runtime & host imports -``` -# One-time: add the musl target (not installed by default). -rustup target add x86_64-unknown-linux-musl - -# Build the static, stripped, LTO-optimized jsc binary. -cargo build --profile dist --target x86_64-unknown-linux-musl -p jsc - -# Result: -target/x86_64-unknown-linux-musl/dist/jsc -``` - -`x86_64-unknown-linux-musl` links statically against musl libc, producing a binary -with no dynamic-loader or glibc-version dependency — it runs on any x86-64 Linux. -The system may also need `musl-tools` (`musl-gcc`) for some transitive C shims. +### 6.1 Host imports -**`[profile.dist]`** inherits `release`, then tightens for distribution: +Every module imports exactly six functions from the `"env"` namespace +(`abi::IMPORT_MODULE`), declared in this order (function indices `0`–`5`): -| Setting | Value | Rationale | -|---|---|---| -| `inherits` | `"release"` | start from the release profile | -| `opt-level` | `3` | jsc embeds wasmtime — favor compiler throughput over shaving KB; `"z"` is the size-first alternative | -| `lto` | `"fat"` | whole-program link-time optimization | -| `codegen-units` | `1` | maximize cross-unit optimization (slower build, smaller/faster binary) | -| `strip` | `true` | drop symbols/debug info | -| `panic` | `"abort"` | no unwinding tables; runtime paths never unwind (panics are lint-denied) | +| # | import | signature | backs | +|---|---|---|---| +| 0 | `print` | `(bits: i64, flag: i32) -> ()` | `console.log` | +| 1 | `__convert` | `(bits: i64, op: i32) -> i64` | `String`, `Number`/ToNumber, `parseInt`, `parseFloat` | +| 2 | `__method` | `(recv: i64, op: i32, arg0: i64, arg1: i64) -> i64` | non-callback array/string methods | +| 3 | `__promise` | `(op: i32, a: i64, b: i64) -> i64` | promise primitives, top-level `await`, `async` spawn | +| 4 | `__then` | `(p: i64, on_f: i64, on_r: i64) -> i64` | `.then` / `.catch` | +| 5 | `__now` | `() -> f64` | `Date.now()`, `new Date()` | + +The native host is `run/harness.rs` (wasmtime `Linker::func_wrap`); the browser +host is `packages/jsc/src/shim.ts`. Both decode jsvals with the same rules +(`run/value.rs` / `value.ts`). + +- **`print` flags.** `0` = format the value and append a space (every argument + but the last), `1` = format and append a newline (the last argument), + `2` = newline only (zero-argument `console.log()`). Values render with the same + `format_value` used for the CLI's `-p` output. +- **`__convert` ops.** `0` ToString (a string argument is returned unchanged; + otherwise the formatted text is allocated as a heap string), `1` ToNumber, + `2` parseInt (base 10), `3` parseFloat. +- **`__method` ops.** `0` `indexOf`, `1` `includes`, `2` `join`, `3` `slice`, + `4` `toUpperCase`, `5` `toLowerCase`, `6` `charAt`, `7` `trim`, `8` `trimStart`, + `9` `trimEnd`, `10` `repeat`, `11` `startsWith`, `12` `endsWith`, + `13` `padStart`, `14` `padEnd`, `15` `substring`, `16` `split`. The host + dispatches on the receiver's type byte for the overloaded array/string names. +- **`__promise` ops.** `0` resolve, `1` reject, `2` await, `3` finally, + `4` new pending promise, `5` settle-fulfill, `6` settle-reject, + `7` async spawn. +- **`__now`.** `SystemTime::now()` since the Unix epoch in milliseconds (`0` if + the clock is before the epoch). It is the only source of non-determinism. + +### 6.2 Guest runtime helpers + +Everything else runs in wasm, emitted per module by `emit/`: allocation +(`__alloc`), string concatenation/equality/comparison/indexing, object get/set +with prototype fallback, array push/concat/spread, object rest, reflection +(`Object.keys/values/entries`, `in`, `delete`), `JSON.stringify` and +`JSON.parse`, Map/Set operations, the equality kernels (`__same_value_zero`, +`__loose_eq`), ToNumber (`__to_number`), NaN canonicalization (`__box_f64`), the +nullish-receiver guard (`__nullish_throw`), generator draining +(`__gen_to_array`), and the host re-entry trampolines (`__invoke1`, +`__async_step`). + +`__to_number(v)` implements ToPrimitive-then-ToNumber for non-number operands: +an array's primitive is its `join()` string (via `__method`), which is then +parsed; every other value goes to `__convert` op `1` (strings by the +StringToNumber rules, `true`/`false` → `1`/`0`, `null` → `0`, `undefined` and +other heap kinds → `NaN`). Numeric call sites skip the helper when the operand +already is a number. + +### 6.3 Exceptions (manual unwinding) + +jsc does not use the wasm exception-handling proposal. Two exported globals +carry the in-flight exception: `$exc_pending` and `$exc_value`. + +- `throw e` stores `e` in `$exc_value`, sets `$exc_pending = 1`, then branches to + the innermost enclosing `try` handler label in the same function, or returns a + dummy value from the function. +- After every call into user code (direct, `call_indirect`, callback-invoking + helpers), the caller checks `$exc_pending` and propagates the same way. Pure + built-ins that cannot throw need no check. +- `try { B } catch (e) { H }` wraps `B` in a block whose label is pushed on the + codegen handler stack; after the block, if `$exc_pending` is set it is cleared + and `$exc_value` is bound to `e` in a fresh scope for `H`. +- `finally` runs on every exit edge — normal fall-through, throw, and any + `return`/`break`/`continue` leaving the protected region. An abrupt exit is + recorded in `$cf_kind`/`$cf_value` before the finalizer runs and resumed after + it; a finalizer that itself completes abruptly overrides the pending action + (`try { return 1 } finally { return 2 }` returns `2`). A pending exception is + re-propagated after a normally completing `finally`. +- At the boundary the harness reads `__exc_pending`; if set, it renders + `__exc_value` with `format_value` and fails with `Uncaught `. + +### 6.4 Promises, microtasks, and the async driver + +Promise *settlement* lives in the guest heap (`TYPE_PROMISE`); *scheduling* — +each promise's reaction list and the FIFO microtask queue — lives in the host +(`run/promise.rs`, mirrored by the shim), keyed by the promise block's offset. + +- `Promise.resolve` / `Promise.reject` / `p.finally` / top-level `await` lower to + `__promise`; `p.then(onF, onR)` and `p.catch(onR)` lower to `__then`, which + returns the chained promise. Missing handlers pass the settlement through. + Resolving with a promise adopts it. A non-promise receiver or awaited operand + is treated as already fulfilled (`await 5` → `5`). +- **Re-entry.** Reactions are guest closures; the engine runs them by calling the + exported `__invoke1(fn, arg)` trampoline, which issues the uniform-ABI + `call_indirect`. A callback that throws sets `$exc_pending`; the engine reads + and clears it and rejects the chained promise. +- **`new Promise(executor)`** allocates a pending promise `P` (op `4`), then + passes the executor two function values whose table slots are the + hand-emitted settler functions and whose `env` slot holds `P` directly. + Calling them settles `P` (ops `5`/`6`, settle-once). A throwing executor + rejects `P`. +- **Draining.** There are no timers or I/O, so the microtask queue is the only + asynchronous source. Top-level `await` drains the queue until its operand + settles (a rejection surfaces as an uncaught throw). After `main` returns + normally the harness drains whatever remains, so unawaited `.then` chains run. + If `main` ends with a pending exception, the queue is not drained. +- **`async` functions** (§7.8) are spawned with op `7`: the host allocates the + result promise, drives the body's state machine one step at a time through the + exported `__async_step(gen, sent)` trampoline, fulfills on completion, rejects + on a throw, and on each `await` registers a continuation on the awaited value + that resumes the generator from the microtask queue. +- Unhandled rejections are not reported: a program whose only output is a + rejected promise exits `0`. + +### 6.5 Value rendering + +`run/value.rs` decodes a jsval against a memory snapshot into a `Value` +(number, boolean, null, undefined, string, array, object, function, map, set, +promise, date, …) and `format_value` renders it Node-console style: numbers by +the ECMAScript Number::toString rules (shortest round-trip digits, exponential +form at `>= 1e21` and `< 1e-6`, `-0` prints `0`), strings unquoted at top level, +`[ 1, 2, 3 ]`, `{ a: 1, b: 2 }`, `[Function (anonymous)]`, `Map(N)` / `Set(N)`, +`Promise { 42 }` / `Promise { }` / `Promise { r }`, dates as +ISO 8601 UTC (`Invalid Date` for a non-finite payload). An object carrying +`@@errorkind` renders as its single-line stack, `Name: message`. -`opt-level = 3` (not `"z"`) is the deliberate choice: because jsc embeds the -wasmtime runtime, compiler execution speed matters more than the last few kilobytes, -and fat LTO + a single codegen unit + `strip` already keep the artifact compact. +--- -A commented, `if: false` `release-musl` job in the CI workflow documents this build -as an artifact-producing step; enable it once the swc path deps (§6) are provisioned -on the runner. +## 7. Language surface + +The compiled language is the strict-mode subset described below. Anything not +listed is rejected at compile time with a spanned diagnostic (§8) or noted in +§13. + +### 7.1 Bindings & scoping + +- **Declarations.** `let`, `const`, `var`, function declarations, class + declarations. Assigning to an undeclared name, or reading one, is a compile + error (`undeclared variable: x`). +- **Block scoping.** Name resolution walks a compile-time scope stack: every + `{}` block, loop body, `for` head, and `catch` clause pushes a frame. Each + `let`/`const` gets a fresh wasm local; inner declarations shadow outer ones. +- **`const`.** Rebinding a `const` (by `=`, compound assignment, `++`/`--`) is a + compile-time error; mutating the referenced object/array is allowed. +- **Closures.** Capture analysis marks every binding referenced from an inner + function. Captured bindings are promoted to heap **cells** (`TYPE_CELL`); + uncaptured bindings stay plain wasm locals. A capturing function value + carries a `TYPE_ENV` of cell pointers, so capture is **by reference** — every + closure and the defining scope share one cell. A captured `for-of`/`for-in` + loop binding gets a fresh cell per iteration; a C-style `for (let …)` binding + is one cell shared by all iterations (§13). +- **Function declarations.** A top-level function declaration is hoisted and + called directly by index with `env = undefined`. A declaration nested inside + another function is lifted like an arrow and bound to its function value at + its source position (not hoisted, and not visible inside its own body). +- **Module cells.** A top-level binding that is referenced from inside a + top-level function is stored in a **module cell**: a cell whose pointer lives + in a dedicated `i64` global (index `9 + slot`), allocated in `main`'s prologue + before any user code runs. Top-level functions reach it through the global, so + they stay hoisted and directly callable from anywhere, including from other + top-level functions. The cell holds a TDZ hole (an `undefined`-tagged box + with a nonzero payload) until the declaration executes; reading or writing it + earlier throws a branded `ReferenceError` + (`Cannot access 'n' before initialization`). +- **Class names** are bound before the class body is lowered, so constructors and + methods can `new` their own class. + +### 7.2 Operators & coercion + +- **Arithmetic** `+ - * / % **` (and the compound forms `+= -= *= /= %= **=`), + unary `-` / `+`, prefix/postfix `++` / `--`. + - `- * / % **`, unary `-`/`+`, and `++`/`--` apply **ToNumber** to each operand + (`"3" * "4"` → `12`, `null * 2` → `0`, `-"3"` → `-3`, `+[]` → `0`; a string + binding incremented by `++` becomes a number). + - Binary `+` concatenates when either operand is a string (the other is + rendered with the console formatter) and otherwise adds the two operands as + numbers *without* ToNumber on non-number operands (§13). + - `**` and `Math.pow` share one inline implementation: repeated multiply for + the integer part of the exponent plus bit-by-bit square roots for the + fraction; negative exponents reciprocate. +- **Relational** `< > <= >=`: two strings compare lexicographically by bytes; + otherwise both operands are converted with ToNumber and compared as `f64`. +- **Strict equality** `===` / `!==`: type then value — numbers numerically + (`NaN !== NaN`, `+0 === -0`), strings by byte content, everything else by + identity bits. +- **Loose equality** `==` / `!=` (`__loose_eq`): booleans are first normalized + to numbers; `null` and `undefined` equal each other and nothing else; two + numbers compare as `f64`; two strings by content; number vs. string converts + the string with ToNumber; anything else compares identity. Object-to-primitive + conversion is not modelled (`[] == 0` is `false`). +- **SameValueZero** (`__same_value_zero`) backs Map/Set keys: strings by content, + numbers by `f64` equality or both NaN (`+0` equals `-0`), everything else by + identity. `Array.prototype.includes` also treats NaN as equal to NaN. +- **Logical** `!`, `&&`, `||`, `??` (each evaluates its left operand once and + yields an operand value, not a coerced boolean) and `&&=`, `||=`, `??=` (short + circuit: the store is skipped when not needed). +- **Conditional** `c ? a : b`, optional chaining `a?.b`, `a?.[k]`, `a?.()` + (a nullish base short-circuits the rest of the chain to `undefined`). +- **`typeof`**, **`instanceof`** (§7.5), **`in`** (own-key probe), **`delete`** + (removes an own key by compacting the entry table). +- **Member access on `null`/`undefined`** — reading or writing a property of a + nullish receiver throws a branded `TypeError` + (`Cannot read properties of null (reading 'x')`), catchable like any throw. + +### 7.3 Control flow + +`if`/`else`, `while`, `do … while`, C-style `for`, `for-of` (arrays, strings, +generators), `for-in` (own enumerable keys of objects; index strings of arrays +and strings), `switch` (strict `===` matching, fall-through, `default`), +`break` / `continue` (with labels on loops), `return`, `throw`, +`try`/`catch`/`finally` (with or without a catch binding). + +A `for` loop with a `let` head is scoped to the loop; `continue` still runs the +update clause. Labels may only be applied to loops. + +The CLI's `-p` mode prints the value of the **last expression statement** +evaluated by `main`; statement completion values of `try`, `switch`, and loops +are not tracked. + +### 7.4 Functions + +- **Forms.** Function declarations, function expressions, arrows (expression- or + block-bodied), object-literal methods and shorthand methods, class + constructors/methods/accessors. All are lifted to top-level wasm functions + with the uniform ABI (§4.2); a function value is a `TYPE_FUNCTION` block. +- **Parameters.** Default values (evaluated per call in a prologue, may reference + earlier parameters), trailing rest parameters (collected into a real array + using `argc`), and array/object destructuring patterns (composable with + defaults). +- **Spread.** In calls and array literals; sources may be arrays, strings + (exploded per byte), or generators (drained via `__gen_to_array`). +- **`this`.** Bound by method calls (`obj.m()`), by `new`, and in static class + methods called as `C.m()`. A plain call sees `this === undefined`. Top-level + `this` is rejected. + +### 7.5 Objects & arrays + +- **Object literals** with identifier and string keys, shorthand properties, + methods, and spread-free construction. Property reads (`o.k`, `o["k"]`, + `o[expr]`) resolve own keys first, then walk `@@prototype` links. A missing key + reads `undefined`. +- **Fixed shape.** An object's key set is decided when it is allocated (an object + literal's keys, or a class's field scan). Assignment updates existing keys; + assigning a key the object does not have throws a `TypeError` (§13). `delete` + removes keys. +- **Destructuring.** Array patterns (holes, defaults, rest, nesting) and object + patterns (rename, defaults, nesting, rest via `__obj_rest`) in declarations + and parameters. +- **Arrays.** Literals, indexing, in-range element assignment, `.length`, and + the methods in §7.9. `push` grows the array by reallocation and writes the new + pointer back to the receiver's binding. +- **Computed dispatch.** `x[i]` and `x[i] = v` dispatch at run time on the + receiver's type byte: string → one-character string, array → element, + object/Map/etc. → keyed lookup with the key converted to a string. + +### 7.6 Classes + +- **Declaration.** `class C { … }` binds `C` to a heap object holding hidden + `@@constructor` (an ordinary uniform-ABI function value; a class without a + constructor gets a synthesized one) and `@@prototype` (one shared prototype + object per class). +- **Instances.** `new C(args)` allocates an instance pre-slotted with the + `@@prototype` link and every field the class body and its constructor chain + assign (a compile-time field-shape scan), sets `this`, runs the constructor, + and yields the instance. Field initializers (`f = init`, or `f;` for + `undefined`) run in source order before the constructor body. +- **Methods** live once on the shared prototype and are found through the + prototype fallback in `__obj_get`. +- **Accessors.** Instance `get x()` / `set x(v)` lower to one `TYPE_ACCESSOR` + block on the prototype. A read resolving to it calls the getter with + `this` = receiver; a write resolves the key through the prototype chain and + calls the setter. A set-only property reads `undefined`; writing a get-only + property is ignored. The accessor paths are emitted only when the program + defines an accessor. +- **Static members.** `static m(){}` and `static f = init` live on the class + object. A literal initializer is placed inline; any other initializer runs + after the class binding exists, in source order, with `this` = the class. +- **Inheritance.** `class B extends A` (a named, earlier-declared superclass) + links `B`'s prototype to `A`'s, so inherited and overridden methods resolve + through the chain. `super(args)` calls `A`'s constructor with the current + `this`; `super.m(args)` calls the parent prototype's method. A subclass + without a constructor forwards its arguments to the parent constructor. +- **`instanceof`.** The right-hand side must be an identifier: + - a user class → walk the receiver's `@@prototype` chain comparing against the + class prototype by identity (multi-level); + - unshadowed `Error` / `TypeError` / `RangeError` → check the `@@errorkind` + brand (`Error` matches any brand); + - unshadowed `Array` / `Function` / `Date` → the receiver's type byte; + `Object` → any heap pointer that is not a string. + Non-object operands yield `false` without trapping. Any other right-hand side + is a compile error. + +### 7.7 Errors + +- `new Error(msg)`, `new TypeError(msg)`, `new RangeError(msg)` build ordinary + objects with `name`, `message`, `stack` (`"Name: message"`, or `"Name"` with no + argument — single line, no frames), and the hidden `@@errorkind` brand + (`0` Error, `1` TypeError, `2` RangeError). The message expression is + evaluated once. +- `throw` accepts any value. +- **Engine-raised errors** are real branded objects, catchable with + `try`/`catch` and recognised by `instanceof`: + - `TypeError: called value is not a function` — a method call whose callee is + not a function (an unknown method on a receiver whose shape is not known at + compile time, a non-callable `super.m`, a non-generator `.next()` receiver); + - `TypeError: Cannot read properties of null|undefined (reading 'k')` — + member access on a nullish receiver; + - `ReferenceError: Cannot access 'x' before initialization` — a module cell + read in its TDZ (branded with the base `Error` kind, `.name` + `ReferenceError`); + - a `JSON.parse` syntax error — an `Error`-branded object with `.name` + `SyntaxError`. +- Out of memory is reported as `Uncaught RangeError: out of memory` and is not + catchable (§5.2). + +### 7.8 Iteration, generators & async + +**Generators.** A top-level `function*` declaration compiles to two +uniform-ABI functions: a **constructor** that allocates a `TYPE_GENERATOR` +handle and its frame without running the body, and a **state machine** +`SM(sent, gen, …)` that resumes at `gen.state`. Every generator local lives in a +frame cell, so locals survive suspension. The body is lowered to a control-flow +graph of basic blocks (`codegen/generator.rs`); `if`/`while`/`for`, `break`, +`continue`, and `return` crossing a `yield` become block jumps dispatched by a +`loop { br_table(state) }`, while regions without a suspend keep ordinary wasm +control flow. + +- `yield e;`, `x = yield e`, `return yield e`; `it.next(v)` sends `v` and + returns a fresh `{ value, done }` object. `.next` is claimed for every + receiver: a non-generator receiver throws + `TypeError: called value is not a function`. +- `yield* g` delegates to another generator. +- `for-of` over a generator, spread of a generator (`[...g()]`, `f(...g())`): + a run-time generator test selects the iterator path; arrays and strings keep + the indexed path. + +**`async`/`await`.** A top-level `async function` declaration is a generator whose +`await e` is a `yield e`; the declared name binds to a wrapper that builds the +generator and hands it to the host async driver (§6.4), returning the result +promise. All generator control-flow support applies to `await`. A rejection of +an awaited promise rejects the function's result promise. Top-level `await` is +supported in `main`. + +**Promises.** `new Promise(executor)`, `Promise.resolve`, `Promise.reject`, +`.then`, `.catch`, `.finally` (§6.4). The `.then`/`.catch`/`.finally` names are +claimed for every receiver. + +### 7.9 Built-ins + +Built-ins are recognized syntactically at the call or member site when the name +is not shadowed by a user binding; they are not first-class values. + +| area | supported | +|---|---| +| output | `console.log(...)` | +| `Math` | `floor`, `ceil`, `round` (half-up), `trunc`, `abs`, `sqrt`, `min`, `max` (variadic, spread-aware), `pow`; constants `PI`, `E` | +| conversions | `String(x)`, `Number(x)`, `Boolean(x)`, `parseInt(s)` (base 10), `parseFloat(s)` | +| globals | `NaN`, `Infinity`, `undefined` | +| array methods | `push(v)`, `pop()`, `map`, `filter`, `forEach` (callbacks receive `(element, index)`), `reduce(cb, init)` (init required; callback receives `(acc, element, index)`), `indexOf`, `includes`, `join`, `slice` | +| string methods | `toUpperCase`, `toLowerCase`, `indexOf`, `includes`, `slice`, `substring`, `charAt`, `trim`, `trimStart`, `trimEnd`, `repeat`, `startsWith`, `endsWith`, `padStart`, `padEnd`, `split`; `.length`, indexing | +| `Object` | `Object.keys`, `Object.values`, `Object.entries` | +| `JSON` | `JSON.stringify(v)` (in-wasm; `undefined`/functions become `null` in arrays and are omitted in objects), `JSON.parse(s)` (in-wasm recursive descent; argument coerced with ToString; objects built with runtime keys) | +| `Map` / `Set` | `new Map()`, `new Set()`; `set`/`get`/`has`/`delete`/`.size`; `add`/`has`/`delete`/`.size` | +| `Promise` | see §7.8 | +| `Date` | `new Date()`, `new Date(ms)` (argument via ToNumber), `Date.now()`, `getTime`, `valueOf`, `getFullYear`, `getMonth`, `getDate`, `getDay`, `getHours`, `getMinutes`, `getSeconds`, `toISOString` — all UTC, computed in wasm | +| errors | `Error`, `TypeError`, `RangeError` constructors | + +Map/Set and Date method names dispatch on the receiver's type byte at run time +and fall back to an ordinary user method call for other receivers, so the names +stay usable on user objects. Built-in method arity is checked at compile time. + +**Method resolution.** For `recv.name(args)` the lowering tries, in order: + +1. a recognized built-in for the receiver (above); +2. a user method call (`MethodCallUser`: member read with prototype fallback, + then `call_indirect` with `this = recv`); +3. compile-time rejection when absence is provable — the receiver is a + primitive/array literal, the program has no functions, or the receiver is an + object literal / never-reassigned binding initialized with an all-static-key + object literal whose key set lacks `name` + (`method '' is not supported`); +4. otherwise a run-time check that the resolved callee is a function before the + `call_indirect`, throwing `TypeError: called value is not a function`. + +No input produces an invalid wasm module. --- -## 8. Language reference - -The language grew in numbered feature waves, each landed and documented as a -unit. The subsections below are the authoritative per-wave reference; all are -implemented and on `main`. - -### Core language subset - -The MVP compiled a single folded arithmetic expression. This wave grew that -into a small but real **numeric-program** language: multi-statement top-level -programs, local variables, control flow, and user-defined recursive functions — -all still in the single `f64` domain. The CLI now runs whole programs from -either `-e ""` or a file argument (`jsc `). - -**Language features now supported:** - -- **f64 numerics.** Number literals and the binary operators `+ - * /`, plus - parenthesized grouping, exactly as in the MVP. -- **`let` / `const` + assignment.** Declarations bind an identifier to a wasm - local; a later `x = expr` reassigns it. Reads and writes resolve to a local - index during lowering. -- **`if` / `else`, `while`, and comparisons.** The comparison operators - `< > <= >= === !==` produce a boolean encoded as `1.0`/`0.0`; `if`/`else` and - `while` branch and loop on truthiness (nonzero is true). -- **Functions / recursion.** `function f(a, b) { .. return .. }` declarations and - `f(x, y)` calls, including self-recursion (e.g. `fib`) and forward calls to a - function declared later in the program. - -**The numeric-domain decision (f64, no NaN-boxing).** Every value in this subset -is an `f64`. Booleans are `1.0`/`0.0`; a bare `return;` yields `NaN` standing in -for `undefined`. This is deliberate: it keeps the IR in one type, maps one-to-one -onto wasm's `f64`, and defers the NaN-boxed `jsval` type model (which unifies -number / boolean / null / undefined / pointer and unlocks strings and objects) -to a later wave (§10). Nothing in this subset needs memory, GC, or a type tag. - -**How programs map to wasm.** The IR (`crate::ir`) now carries a program, not a -lone expression tree: - -- **`IrProgram`** = the exported `main` entry function (top-level `stmts` plus its - `locals` count) and a vector of user-defined `IrFunction`s. -- **Locals.** Each `let`/`const` and each parameter is a wasm `f64` local; the - lowering counts them so `emit` can declare `(local f64 …)` up front. Reads emit - `local.get`; an assignment emits `local.tee` (store and yield the value). -- **Structured control.** `IrStmt::If` lowers to a wasm structured `if`/`else` - block and `IrStmt::While` to a `block`+`loop` pair with a conditional - `br_if`/`br` — no raw jumps, matching wasm's structured control flow. -- **Functions.** Each `IrFunction` becomes its own wasm function with signature - `(f64 … ) -> f64`; `main` is function index `0` and exported as `"main"`, and - user functions take indices `1..=n`, so a `Call` is a direct `call` to a known - index. Arguments are evaluated left-to-right onto the stack; the callee's `f64` - result is left in place. `run` still just calls the exported `main` and reads - back one `f64`. - -Sections used by `emit` grow accordingly (Type, Function, Export, Code); there is -still no memory, no imports, and no globals. - -### The NaN-boxed value model (`jsval`) - -The base subset kept every value a raw `f64`. This wave replaces that single -numeric domain with a **NaN-boxed `jsval`**: `number`, `boolean`, `null`, and -`undefined` become distinct first-class values that coexist in one 64-bit word, -flow through the whole pipeline, and are decoded back to their real JS type in -`run.rs`. This unlocks `typeof`, real truthiness, and strict equality over mixed -types — none of which an f64-only value could express. - -**Bit layout.** A `jsval` is a 64-bit value. `QNAN = 0x7FF8_0000_0000_0000` -(exponent all ones + the quiet bit). A value is a **NUMBER** iff -`(bits & QNAN) != QNAN`, or it is the canonical number-NaN (`0x7FF8_0000_0000_0000`, -tag `0b000`); numbers are stored as raw `f64` bits (`f64::to_bits(x)`). A boxed -non-number sets `QNAN` plus a 3-bit tag in payload bits `[50:48]`: - -| tag (bits 50:48) | type | encoding | notes | -|---|---|---|---| -| `0b000` | (reserved) number NaN | `0x7FF8_0000_0000_0000` | canonical NaN; still a NUMBER | -| `0b001` | boolean | `FALSE = 0x7FF9_0000_0000_0000`, `TRUE = 0x7FF9_0000_0000_0001` | value in bit 0 | -| `0b010` | null | `0x7FFA_0000_0000_0000` | | -| `0b011` | undefined | `0x7FFB_0000_0000_0000` | | -| `0b100` | typeof-result string immediate | `0x7FFC_0000_0000_0000 + index` | payload low bits = index into `TYPEOF_STRINGS` | - -`TYPEOF_STRINGS = ["number", "boolean", "undefined", "object", "string", -"function"]`; the value model only produces indices `0..3`. - -**The i64-carrier decision.** The wasm value type carrying a `jsval` is **`i64`**, -not `f64`. A NaN-boxed value lives in the NaN space of a double, and the wasm spec -permits an implementation to **canonicalize a NaN payload** both on `f64` -arithmetic and at the wasmtime return boundary — either of which would silently -rewrite the tag/payload and destroy the type. Carrying the value as `i64` is -bit-preserving (integer ops and the `i64` return path never canonicalize), so the -`jsval` bits survive intact. Numbers are still real doubles: arithmetic -reinterprets the `i64` operands to `f64` (`f64.reinterpret_i64`), computes with -`f64.*`, and reinterprets the result back to `i64` (`i64.reinterpret_f64`). The -exported entry signature therefore changes from `() -> f64` to `() -> i64` — the -one intentional contract change of the campaign. - -**The `run.rs` Value seam.** `run.rs` decodes the returned `i64` bits into a typed -`Value` and formats it JS-style: - -```rust -pub enum Value { Number(f64), Bool(bool), Null, Undefined, TypeStr(&'static str) } -pub fn decode(bits: u64) -> Value; // per the layout above -pub fn format_value(v: &Value) -> String; // Number -> format_number; Bool -> "true"/"false"; - // Null -> "null"; Undefined -> "undefined"; TypeStr -> the word -pub fn compile_and_run(src: &str) -> anyhow::Result; // CLI prints format_value(&value) -``` +## 8. Diagnostics -`format_number` is reused for the `Number` arm (integral doubles print without a -trailing `.0`). It follows the ECMAScript `Number::toString` fixed-vs-exponential -thresholds: a value renders in exponential form (lowercase `e`, explicit sign, -no leading zeros in the exponent — e.g. `1e+21`, `1.5e+22`, `1e-7`) when its -decimal exponent puts it `>= 1e21` or in `0 < |x| < 1e-6`, and in fixed form -otherwise (`1e-6` => `0.000001`, `123` => `123`, `4.5` => `4.5`). The shortest -round-tripping digit string is recovered from Rust's `{:e}` shortest mantissa -(digits + `n = exp + 1`). The non-finite and zero cases bypass the digit path: -`NaN` => `"NaN"`, `±Infinity` => `"Infinity"`/`"-Infinity"`, and both `+0`/`-0` => -`"0"` (JS `String(-0)` is `"0"`; the stored f64 sign is left intact so arithmetic -still sees `-0`, e.g. `1 / -0` => `-Infinity`). The bare `NaN` / `Infinity` -identifiers are read-only global number literals that lower to the canonical `f64` -constants. - -**Semantics.** *Truthiness* — falsy is exactly `false`, `null`, `undefined`, -`0`/`-0`, and `NaN`; everything else is truthy. `if` / `while` conditions (and the -ternary condition, below) test truthiness by this rule instead of the base subset `!= 0.0`. -*Strict equality* (`===` / `!==`) is type-then-value: different types => `false`; -same type => compare by value (numbers numerically, with `NaN === NaN` **false** -and `+0 === -0` **true**; booleans bit-for-bit; `null`/`undefined` reflexively -equal). The result is a boxed boolean. *Conditional (ternary) `cond ? a : b`* — -`cond` is tested for truthiness and only the selected branch is evaluated, -yielding that branch's jsval. - -**Documented simplifications.** Strings, objects, and arrays as real values are out -of scope (the string type onward); they need linear memory + an arena + GC. `typeof` returns a -**boxed type-string immediate** (tag `0b100`, an index into `TYPEOF_STRINGS`) — -*not* a heap string, and not a general string type. All four in-scope types -(`number`, `boolean`, `null`, `undefined`) are **immediate** — The value model introduces **no -linear memory** anywhere in the pipeline. Loose equality (`==`) and JS coercion -(`ToNumber`, `+` string concat) are likewise deferred to the string type onward, as is the -logical-not operator `!` (truthiness is reached only through `if` / `while` and -the ternary condition in the base numeric subset). - -### Strings: the first heap type - -The base numeric subset kept every `jsval` an **immediate** — number, boolean, -`null`, `undefined`, and the `typeof` type-word all fit in the 64-bit word. The -This adds the **first heap-allocated type: the string**. A string does not fit -in 64 bits, so this wave introduces **linear memory** into the emitted module, a new -NaN-box **pointer** variant (tag `0b101`) whose payload is a 32-bit byte offset -into that memory, compile-time **data segments** for string literals, and a -runtime **bump allocator** for strings built at run time. The `jsval` carrier -(`i64`) and the pipeline shape are unchanged; the change is that a `jsval` can now -be a **pointer** into a heap, and the host reads that pointer back into text. - -**Linear memory.** The module gains one Memory, `min = 1 page` (64 KiB), **no -max**, exported as `"memory"` so the host (`run.rs`) can read string bytes back -*after* `main` returns. The module never emits `memory.grow`; programs are assumed -to stay within one page, and **heap overflow beyond one page is undefined** in -this learning subset — there are **no bounds checks**. Offset `0` is reserved as a -null guard; static literal data begins at offset `8`. - -**String representation.** A string lives in memory as a length-prefixed UTF-8 -blob: a little-endian `u32` `len` at the pointer, followed by exactly `len` UTF-8 -bytes. +**Compile-time errors** — parse failures from swc and every codegen rejection — +are `jsc_core::error::CompileError { span: Option, msg }`. The CLI renders +a spanned error as a rustc-style caret block: ```text -ptr ptr+4 -┌──────────┬──────────────────────────┐ -│ len:u32 │ utf8 bytes (len of them) │ -└──────────┴──────────────────────────┘ -``` - -`len` and `.length` use the **byte** length (ASCII-first: for ASCII input this -equals JS `.length`). JS **UTF-16** `.length` semantics and non-ASCII correctness -are **deferred** — a documented simplification. - -**The pointer jsval variant.** This adds one new NaN-box tag, `0b101` (the next -free tag after the value model's typeof-string `0b100`), marking a heap pointer. -`POINTER_BITS = 0x7FFD_0000_0000_0000` (QNAN set plus tag `0b101`); a string jsval -is `QNAN | (0b101 << 48) | (ptr & 0xFFFF_FFFF)` — the 32-bit byte offset in the -low bits. The tag table extends the value model's: - -| tag (bits 50:48) | type | encoding | notes | -|---|---|---|---| -| `0b000` | (reserved) number NaN | `0x7FF8_0000_0000_0000` | canonical NaN; still a NUMBER | -| `0b001` | boolean | `FALSE = 0x7FF9_0000_0000_0000`, `TRUE = 0x7FF9_0000_0000_0001` | value in bit 0 | -| `0b010` | null | `0x7FFA_0000_0000_0000` | | -| `0b011` | undefined | `0x7FFB_0000_0000_0000` | | -| `0b100` | typeof-result string immediate | `0x7FFC_0000_0000_0000 + index` | payload low bits = index into `TYPEOF_STRINGS` | -| `0b101` | **heap string pointer** (NEW) | `0x7FFD_0000_0000_0000 + (ptr & 0xFFFF_FFFF)` | payload low 32 bits = byte offset into linear memory | - -`typeof` a pointer → index **4** (`"string"`) in `TYPEOF_STRINGS` (the value model already -reserved it). A string is the only heap type so far; objects and arrays (which -would reuse the pointer mechanism with different payload layouts) are the object & array types onward. - -**Static literal data + bump allocator.** String **literals** are emitted as -active wasm **data segments** at compile-time-assigned offsets (each -`[len][bytes]`, packed from base offset `8`); a literal lowers to a pointer jsval -with no run-time allocation. Strings built at run time (concat, indexing) come -from a **bump allocator**: a mutable wasm global `__heap_ptr: i32` initialized to -`HEAP_BASE` (the first free offset after all static literal data) and a helper -`__alloc(size: i32) -> i32` that returns the current `__heap_ptr` and bumps it — -**no free, no GC**. To keep user-function wasm indices stable (`main` is index -`0`, user functions `1..=n`), the five heap helpers — `__alloc`, `__concat`, -`__str_eq`, `__str_cmp`, `__str_index` — are appended (in that order) as the -**last** wasm functions, after all user functions; `__concat` and `__str_index` -call `__alloc`. - -**The `run.rs` boundary change.** the value model's `run` returned only the raw `i64` bits; -The string type must also hand the host the linear-memory bytes, since a pointer jsval is only -meaningful against the memory it indexes. `run` reads the exported `"memory"` and -returns the bytes alongside the result bits; `decode` gains the memory as a -parameter and a pointer arm that reads `[len][bytes]` into a new `Value::Str`; -`format_value` prints a `Str` as its raw text **without** surrounding quotes -(console style). Every the value model immediate arm is unchanged. - -```rust -pub enum Value { Number(f64), Bool(bool), Null, Undefined, TypeStr(&'static str), Str(String) } -pub fn decode(bits: u64, memory: &[u8]) -> Value; // pointer arm reads [len][bytes] -pub fn format_value(v: &Value) -> String; // Str -> raw text, no quotes +error: cannot assign to constant binding: c + --> :1:14 + | + 1 | const c = 1; c = 2 + | ^ ``` -**Operations.** *`.length`* loads the `u32` at `ptr` and reboxes it as a number. -*Concatenation & `+` coercion* makes `+` a runtime type check: if **either** -operand is a string, the **other** is coerced to its string form (`ToString`: -`"x" + 8` → `"x8"`, `8 + "x"` → `"8x"`, `"x" + true` → `"xtrue"`, `"x" + null` → -`"xnull"`) and the two are concatenated (`__alloc(len1 + len2 + 4)`, copy, write -header → new pointer); when neither is a string it is the numeric add — so -`40 + 2` still yields `42`. The full numeric `ToPrimitive`/`ToNumber` path is -**not** implemented: `+` with a non-string operand like `null` or a boolean is -**not** coerced to a number (`1 + null` does not fold to `1`) — a documented -boundary. *Indexing `s[i]`* allocates a fresh one-char string; an out-of-bounds -index (`i >= length` or negative) reads `undefined`. *Strict `===` / `!==`* compare pointer -operands by byte content (`__str_eq`); *relational `< > <= >=`* compare -lexicographically by bytes (`__str_cmp`); the immediate cases keep the value model paths. -*String truthiness* — `""` (len `0`) is falsy, any non-empty string is truthy. - -**Documented simplifications.** One page, no `memory.grow`, no bounds checks -(overflow is undefined); byte-length `.length` (UTF-16 / non-ASCII deferred); a -bump allocator with no free and no GC. Objects and arrays, template literals, -string methods beyond `.length` / indexing, loose equality (`==`), and the -logical-not operator `!` are all deferred to **the object & array types onward**. The `main` export signature -stays `() -> i64`; the string type *adds* the exported `"memory"` — the one intentional -contract addition of the campaign. - -### Objects & arrays - -This wave added the first heap type, the **string**, backed by linear -memory, a bump allocator (`__alloc`), and a NaN-box **pointer** jsval (tag -`0b101`). This adds the **second and third heap types: the array and the object**, -reusing that machinery unchanged — the same memory, the same allocator, the same -boxed-32-bit-offset pointer form. Each new type gets its own NaN-box **pointer -tag** (`0b110` array, `0b111` object) and its own in-memory layout. The `jsval` -carrier (`i64`) and the pipeline shape are unchanged; the change is that a pointer -jsval can now refer to an array or an object as well as a string, and computed -access (`x[i]`) becomes a **runtime tag dispatch**. - -**Reused the string type machinery.** No new memory, allocator, or `run.rs` plumbing beyond -the two new decode arms: one Memory (`min = 1 page`, no max, exported `"memory"`, -no grow, no bounds checks, offset `0` null guard); the bump allocator -(`__heap_ptr` global + `__alloc` — no free, no GC); and the pointer jsval form -`QNAN | (tag << 48) | (ptr & 0xFFFF_FFFF)`. Arrays and objects are built at run -time with the **same** `__alloc`. - -**The two new pointer tags.** This adds `0b110` (array) and `0b111` (object), the -two remaining free tags after the string type's string `0b101`. With -`ARRAY_BITS = 0x7FFE_0000_0000_0000` and `OBJECT_BITS = 0x7FFF_0000_0000_0000`, -an array jsval is `ARRAY_BITS | (ptr & 0xFFFF_FFFF)` and an object jsval is -`OBJECT_BITS | (ptr & 0xFFFF_FFFF)` — the byte offset in the low 32 bits, exactly -as for the string pointer. The tag table extends the string type's: - -| tag (bits 50:48) | type | encoding | notes | -|---|---|---|---| -| `0b000` | (reserved) number NaN | `0x7FF8_0000_0000_0000` | canonical NaN; still a NUMBER | -| `0b001` | boolean | `FALSE = 0x7FF9_0000_0000_0000`, `TRUE = 0x7FF9_0000_0000_0001` | value in bit 0 | -| `0b010` | null | `0x7FFA_0000_0000_0000` | | -| `0b011` | undefined | `0x7FFB_0000_0000_0000` | | -| `0b100` | typeof-result string immediate | `0x7FFC_0000_0000_0000 + index` | payload low bits = index into `TYPEOF_STRINGS` | -| `0b101` | heap string pointer | `0x7FFD_0000_0000_0000 + (ptr & 0xFFFF_FFFF)` | payload low 32 bits = byte offset | -| `0b110` | **heap array pointer** (NEW) | `0x7FFE_0000_0000_0000 + (ptr & 0xFFFF_FFFF)` | payload low 32 bits = byte offset into linear memory | -| `0b111` | **heap object pointer** (NEW) | `0x7FFF_0000_0000_0000 + (ptr & 0xFFFF_FFFF)` | payload low 32 bits = byte offset into linear memory | - -`typeof` an array pointer and `typeof` an object pointer both → index **3** -(`"object"`) in `TYPEOF_STRINGS` (matching JS, where `typeof []` is `"object"`). -**All eight tag values `000..111` are now assigned — the 3-bit tag space is -exhausted.** A fourth heap type (the operator & scoping wave onward) cannot get a fresh tag; it must switch to a -**header-byte discriminator** inside the allocated blob (one reused tag for "heap -object", first byte selects the concrete kind). This is a documented boundary. - -**Array layout.** An array is a length-prefixed run of `jsval` slots: +The label is `` for `-e`/`-p` input and the file path otherwise +(`jsc-wasm` uses ``). Columns count characters; a multi-column span +underlines its full width. A span-less error prints its bare message. Because +`CompileError` implements `std::error::Error`, it travels through `anyhow` and +is recovered with `downcast_ref`. -```text -ptr ptr+4 ptr+12 ptr+4+8*(len-1) -┌──────────┬───────────┬───────────┬─── … ───┬───────────────┐ -│ len:u32 │ elem_0:i64│ elem_1:i64│ │ elem_{len-1} │ -│ (LE) │ │ │ │ :i64 │ -└──────────┴───────────┴───────────┴─── … ───┴───────────────┘ -``` +Representative messages: `undeclared variable: x`, `call to undeclared +function: f`, `cannot assign to constant binding: c`, `method 'sort' is not +supported`, `regular expression literals are not supported`, `BigInt literals +are not supported`, `with statements are not supported`, `class expressions are +not supported`, `unsupported object key: …`, `a label may only be applied to a +loop`, `a \`yield\` inside try/catch/finally is not supported`. -`len` is a little-endian `u32` at the pointer; element `k` is a full 8-byte -`jsval` at `ptr + 4 + 8*k` (unaligned `i64` load/store — wasm permits this with -`align=0`). Elements are full jsvals, so arrays nest. `.length` reads the `u32` at -`ptr` — the **same header read** the string type's `StrLen` uses, so `.length` is uniform for -strings and arrays. `arr[i]` read = `i64.load` at `ptr + 4 + 8*i` for an in-range -`i`; an out-of-bounds index (`i >= len` or negative) reads `undefined` rather than -loading past the block. `arr[i] = v` = `i64.store` at `ptr + 4 + 8*i` -(**existing index only** — growth is deferred). The -literal `[a,b,c]` lowers to `__alloc(4 + 8*n)`, write `len = n`, write each -element. `typeof []` → `"object"`; an array is **always truthy** (even `[]`). +**Run-time errors** carry no source span: -**Object layout.** An object is a count-prefixed run of `(key, value)` entries, -each 16 bytes: +- An uncaught JS exception fails the run with `Uncaught ` + (e.g. `Uncaught TypeError: Cannot read properties of null (reading 'x')`, + `Uncaught 1`). +- Heap exhaustion fails with `Uncaught RangeError: out of memory` (§5.2). +- Remaining machine faults surface as wasmtime traps. -```text -ptr ptr+4 ptr+4+16 ptr+4+16*(n-1) -┌──────┬─────────────┬───────────┬─────────────┬───────────┬─ … ─┬─────────────┬───────────┐ -│ n:u32│ key_ptr_0 │ val_0 │ key_ptr_1 │ val_1 │ │ key_ptr_{n-1}│ val_{n-1} │ -│ (LE) │ :i64 │ :i64 │ :i64 │ :i64 │ │ :i64 │ :i64 │ -└──────┴─────────────┴───────────┴─────────────┴───────────┴─ … ─┴─────────────┴───────────┘ -``` +--- -`n` is a little-endian `u32` at the pointer; entry `k` lives at `ptr + 4 + 16*k`, -its key jsval at `ptr + 4 + 16*k` and its value jsval at `ptr + 4 + 16*k + 8`. -Keys are the string type heap-string pointers; literal keys reuse the compile-time static -string data (the existing `collect_strings` interning), so key `a` shares its -`[len][bytes]` blob with any `"a"` literal. The literal `{a:1,b:2}` lowers to -`__alloc(4 + 16*n)`, write `n`, write each `(key_ptr, val)`. Property read -`obj.a` / `obj["a"]` linear-scans the `n` entries, comparing each stored key by -**byte content** via the string type `__str_eq` helper, and returns the matching value -jsval (or `undefined` if no key matches). Property assignment `obj.a = v` / -`obj["a"] = v` does the same scan and stores `v` into the matching entry's value -slot (**existing keys only** — adding a new key is a documented no-op / -undefined). `typeof {}` → `"object"`; an object is **always truthy** (even `{}`). - -**Shared `x[i]` dispatch.** Computed access `x[i]` and computed assignment -`x[i] = v` share syntax with the string type string indexing, so the emitter cannot pick the -operation statically — it **runtime-dispatches on the pointer tag** of `x`: -string (`0b101`) → the string type `__str_index`; array (`0b110`) → element `i64.load` / -`i64.store`; object (`0b111`) → the object key scan (keyed on the string jsval). -The emitter branches with an `if`/`else` chain (not `select`), so only the -matching path is reached — the same trap-safety discipline the string type uses for its -pointer guard. New heap helpers (the object get/set scan loop) are appended -**after** the string type helper `__str_index`, keeping `main` = index `0`, user -functions `1..=n`, and the string type helper order (`__alloc`, `__concat`, `__str_eq`, -`__str_cmp`, `__str_index`) unchanged. - -**The `run.rs` boundary change.** `decode(bits, &memory)` already threads the -exported memory. This adds an **array arm** (tag `0b110`) that reads `len` then -recursively decodes each element jsval, and an **object arm** (tag `0b111`) that -reads `n` then decodes each key (string pointer → `String`) and recursively -decodes each value jsval. `Value` gains `Array(Vec)` and -`Object(Vec<(String, Value)>)`; `format_value` prints them Node-console style -(`[ 1, 2, 3 ]`, `{ a: 1, b: 2 }`). The milestones only assert numeric / `typeof` -results, so exact print formatting is not milestone-tested. - -```rust -pub enum Value { - Number(f64), Bool(bool), Null, Undefined, TypeStr(&'static str), - Str(String), Array(Vec), Object(Vec<(String, Value)>), -} -pub fn decode(bits: u64, memory: &[u8]) -> Value; // array + object arms recurse -pub fn format_value(v: &Value) -> String; // [ .. ] / { .. } console style -``` +## 9. CLI -**Documented simplifications.** Element assignment and property assignment target -**existing** slots only — growing an array, adding a new object key, `push`, and -`delete` are deferred; assigning through a missing index/key is a no-op / -undefined. The tag space is exhausted (a fourth heap type needs a header-byte -discriminator). Array/object methods (`map` / `keys` / …), prototypes, -`for-of` / `for-in`, spread, computed / shorthand object keys, `JSON`, -`Array.isArray`, loose equality (`==`), and the logical-not operator `!` are all -deferred to **the operator & scoping wave onward**. There is still no free and no GC. The exported `"memory"` -from the string type is reused, not re-added. - -### Statements, operators, `const` immutability & block scoping - -This wave rounded out the heap types (array, object) and **exhausted the -3-bit NaN-box tag space**. The operator & scoping wave turns the numeric-expression subset into a **small -imperative language** without touching the value representation: it adds the -**short-circuit logical operators** (`!`, `&&`, `||`, `??`), **compound assignment** -(`+=`, `-=`, `*=`, `/=`, `%=`) and **increment/decrement** (`++`/`--`, prefix + -postfix), the **C-style `for` loop**, **`switch`/`case`/`default`/`break`** with JS -fall-through, **lexical block scoping** (a fresh scope per `{}` block; `for`-`let` -block-scoped), and **`const` immutability** as a **compile-time error**. Every one -is a **frontend + codegen + emit lowering**: no new tag, no new heap layout, and — -unlike the string type/the object & array types — **no `run.rs` change** (no new `Value` variant, no new decode -arm). The `i64` jsval carrier and the whole early-waves pipeline is unchanged. - -**Reused machinery (unchanged).** The NaN-boxed `i64` jsval (immediates + string -`0b101` / array `0b110` / object `0b111` pointers); the JS-truthiness lowering -(`emit_cond`) behind `if`/`while`/`?:`; the flat per-function wasm local frame; the -`block`/`loop`/`if`/`br`/`br_if` encoders; strict `===` (`BinOp::Eq`); and the -function-index order (`main`=0, user funcs `1..=n`, then the heap helpers -`__alloc`, `__concat`, `__str_eq`, `__str_cmp`, `__str_index`, `__obj_get`, -`__obj_set`). **This adds no helper function**, so every index is stable. - -**Short-circuit logical lowering.** `!e` is eager: evaluate `e`, apply the -truthiness test, produce the **boolean** inverse (the operator deferred since the value model). -`&&`, `||`, `??` **evaluate the left operand exactly once** into a temp local `t`, -then branch — and the result is the **operand jsval**, not a coerced boolean: +`crates/jsc/src/main.rs`, node-like: -```text - ; local.tee t ;; a into temp, value also available for the test - ;; truthy(t) for && / || | nullish(t) for ?? -if (result i64) - ;; b (the right) is evaluated in exactly one arm -else - -end -``` +| command | behaviour | +|---|---| +| `jsc FILE` | compile and run the file; only `console.log` output is printed | +| `jsc -e SRC` / `--eval SRC` | compile and run inline source; only `console.log` output is printed | +| `jsc -p SRC` / `--print SRC` | compile and run inline source, then print the completion value (last expression statement) | +| `jsc build FILE -o OUT` | write the wasm module to `OUT`; `-o -` writes to stdout | -`a && b` yields `b` iff `truthy(t)` else `t` (`0 && x` → `0`, `1 && "s"` → `"s"`); -`a || b` yields `t` iff `truthy(t)` else `b` (`"" || "x"` → `"x"`); `a ?? b` yields -`b` iff `t` is **`null`/`undefined`** (a **nullish** test — `t === null || -t === undefined`, *not* truthiness, so `0 ?? 5` → `0`) else `t`. The right operand -`b` sits in one branch arm, so it runs only when the operator does not -short-circuit; each short-circuit operator uses one temp local. The IR gains -`Not(Box)` and `Logical { op: {And,Or,Nullish}, lhs, rhs }`. - -**Compound assignment + inc/dec.** `target op= e` desugars to `target = target op e` -with the **target read once**; `op ∈ {Add,Sub,Mul,Div,Mod}`, where **`BinOp::Mod`** -(`%`, `f64` remainder) is new. The **shipped** target set is a plain **local** -identifier: `local.get`/`op`/`local.tee`, reusing the `+` string/number overload so -`s += t` concatenates. **Compound assignment to a member target — `a[i] op= e` / -`o.k op= e` — is a documented deferral** (`Err("unsupported compound assignment -target")`; plain `=` member stores are unaffected): the read-once-into-temps -member-target lowering is left to the closures wave onward. `++x`/`--x` (prefix) yield the **new** -value; `x++`/`x--` (postfix) read `old`, write `old ± 1`, and yield **`old`** — also -local-identifier targets only. `op=` / `++` / `--` on a **`const`** target is a -compile-time error. - -**C-style `for`.** `for (init; test; update) body` **desugars** to a `while`: +Exactly one of `-e`, `-p`, or `FILE` is required (a clap argument group); +`-e`/`-p`/`-o` accept values beginning with `-` (`jsc -p "-0"`). `console.log` +streams to stdout while the module runs. -```text -{ ;; for-scope: a for-let binding lives here only - init ; ;; a let/const Decl, an Expr, or empty - while (test) { ;; missing test => literal `true` - body ;; body is its own block scope - update ; ;; an Expr or empty; runs after body, before next test - } -} -``` +**Exit codes.** -The `for`-init `let`/`const` is scoped to the loop (invisible after; no collision -with an outer name). Because the operator & scoping wave has **no `continue`** (deferred), the plain -`while` desugaring is exact — nothing can skip the `update`. This reuses the -existing `IrStmt::While` + `Decl`/`Expr`; no dedicated `For` IR node is required. - -**`switch` — fall-through + `break`.** `switch (disc) { case v: … default: … }` -evaluates the discriminant **once** into a temp `td`, compares each `case` label to -`td` with **strict `===`**, and on the first match runs that case's statements -**and every subsequent case's statements in source order** (JS fall-through) until -a **`break`** or the switch end; `default` runs when no case matches. It lowers to -a wasm `block` (the break target) wrapping a compare-and-branch entry dispatch -followed by the straight-line, concatenated case bodies; each `break` is a `br` out -of that block. The IR gains `Switch { disc, cases: Vec<(Option, -Vec)> }` (a `None` label = `default`) and a `Break` statement. `break` in -The operator & scoping wave is **switch-only** (loop `break`/`continue` are deferred). - -**Lexical block scoping.** Block scope is a **compile-time name-resolution** model, -not a runtime frame. A flat per-function name table is replaced by a **scope -stack**: entering any `{}` block (function body, `if`/`else`, `while`/`for` body, -`for`-init, bare block) pushes a frame; leaving pops. A `let`/`const` declares its -name in the **innermost** frame and allocates a **fresh wasm local** (locals are -never reused across sibling blocks — the flat wasm frame just grows; a documented -simplification, no liveness reuse). A reference resolves by walking the stack -**innermost-first**, so an inner `let x` **shadows** an outer `x`; sibling blocks -at the same depth may reuse a name for **distinct** bindings. `let x=1; { let x=2; } -x` → `1`. A `for`-let binding lives in an enclosing for-scope popped at loop end, so -it does not escape the loop. - -**`const` immutability.** Each binding records `mutable` in its scope frame -(`let` → true, `const` → false). Any assignment to a name — `x = e`, `x op= e`, -`++x`/`x++`/`--x`/`x--` — resolves the name and checks the flag; assigning to a -`const` binding is a **compile-time error** (the frontend/codegen returns -`Err("assignment to constant variable")` and **no wasm is emitted**). `const` -without an initializer is a compile error. `const` object/array **contents** stay -mutable — `const o = {}; o.k = v` is allowed; only **rebinding** `o` errors — -matching JS. - -**The `run.rs` boundary (unchanged).** the operator & scoping wave introduces **no new `Value` variant and -no new decode arm**. Every the operator & scoping wave program still returns one of the existing decoded -values (number, boolean, `null`, `undefined`, typeof-word, string, array, object); -`decode` and `format_value` are untouched. Unlike the string type and the object & array types, the operator & scoping wave does **not** -move the `run.rs` boundary — it is entirely a frontend + codegen + emit change. - -**Documented simplifications / deferrals (the closures wave onward).** **compound assignment to a -member target** (`a[i] op= e` / `o.k op= e` — only local-identifier compound and -`++`/`--` shipped in the operator & scoping wave), `for-of` / `for-in`, labeled -statements and labeled `break`/`continue`, **`continue`** (excluded so the `for` → -`while` desugaring stays a plain loop — a `continue` must still run the `update`, -needing a labeled block/loop restructure), `do…while`, **TDZ** runtime error -semantics (the operator & scoping wave block scoping is compile-time name resolution only — no -use-before-declare trap), **closures capturing block scope** (no closures exist -yet), loose equality `==` / `!=` (`switch` uses strict `===`), the **comma -operator**, and **`**` exponentiation** (this wave adds `%` but not `**`) are all deferred -to the closures wave onward. - -### First-class functions & closures - -This wave turned the subset into a small imperative language without -touching the value representation. The closures wave makes **functions first-class values** and -adds **closures** — and to do so it must first **reclaim NaN-box tag space**, which -The object & array types exhausted. The three per-type heap tags (string `0b101`, array `0b110`, object -`0b111`) collapse into a **single heap-pointer tag** plus a **type-byte header** in -each heap block; that frees the tag budget for a new heap type — the **function -value** — and sets the pattern for the exceptions wave error objects. On that base the closures wave lands the -The operator & scoping wave-deferred **member-compound assignment** (`a[i] op= e`, `o.k op= e`), **function -values** (a `function`/arrow used as a value, passed as an argument, called through -a funcref **Table** with `call_indirect`), **arrow functions**, and **closures** (a -returned function that captures its enclosing bindings **by reference** through a -heap **environment of boxed cells**). The `i64` jsval carrier and the frontend → -codegen → emit → run pipeline shape are unchanged; the heap **encoding** moves from -"tag per type" to "one heap tag + header byte", and two module pieces are added (a -funcref Table + element segment, and the box/closure helpers). - -**Reused machinery (unchanged carrier).** The NaN-boxed `i64` jsval with immediates -(number, boolean, `null`, `undefined`, typeof-word) — **unchanged**. One Memory -(`min = 1 page`, no max, exported `"memory"`, no grow, no bounds checks, offset `0` -null guard); the bump allocator (`__heap_ptr` + `__alloc` — no free, no GC); the -truthiness lowering (`emit_cond`); the operator & scoping wave scope stack + `const` check; strict `===` -(`BinOp::Eq`); and the function-index order (`main`=0, user funcs `1..=n`, then heap -helpers). Only the heap **pointer encoding** changes, and new module pieces are -appended. - -**(a) The header-byte heap scheme.** Today a heap value's concrete type is carried -in the **NaN-box tag** (`0b101`/`0b110`/`0b111`) and the block begins directly with -the per-type payload; all eight 3-bit tags are assigned, so **the tag space is -exhausted**. This replaces the three per-type heap tags with a **single -heap-pointer tag** — `0b101` remains the sole "this jsval is a heap pointer" tag -(`HEAP_BITS = 0x7FFD_0000_0000_0000`; `0b110`/`0b111` retire to free/reserved) — -and moves the concrete kind into a **type byte at offset `0` of the heap block**. -Every block gains a header: byte `0` = type byte, bytes `1..4` = reserved padding, -so the existing per-type payload begins at **`HEADER_SIZE = 4`** (unchanged in -shape, just shifted): +| code | meaning | +|---|---| +| `0` | success | +| `1` | uncaught exception, out of memory, runtime trap, or I/O failure (unreadable input, unwritable output) | +| `2` | compile error (parse or lowering); also clap usage errors | -```text -ptr ptr+1 ptr+4 (= HEADER_SIZE) -┌────────┬────────┬──────────────────────────────────────────────┐ -│ type:u8│ pad ×3 │ len:u32 / n:u32 + payload (as the string type/the object & array types) │ -└────────┴────────┴──────────────────────────────────────────────┘ -``` +Errors are printed to stderr. -| type byte | kind | payload (from offset `HEADER_SIZE`) | user-visible | -|---|---|---|---| -| `0` | string | `len:u32` + `len` UTF-8 bytes | yes | -| `1` | array | `len:u32` + `len` × `jsval:i64` | yes | -| `2` | object | `n:u32` + `n` × `(key_ptr:i64, val:i64)` | yes | -| `3` | **function** (NEW) | `func_idx:u32` + `env:i64` | yes (`typeof` → `"function"`) | -| `4` | **closure env** (NEW, internal) | `count:u32` + `count` × `cell_ptr:i64` | no | -| `5` | **boxed cell** (NEW, internal) | `value:i64` | no | -| `6` | (reserved) **error object** — The exceptions wave | — | — | - -Consequences are wide but mechanical: every allocation writes the type byte then -the payload (optionally via `__alloc_typed(type, payload_size)`); every -`len`/`n`/element/entry/char load-store shifts by `HEADER_SIZE` (string `len` at -`ptr + HEADER_SIZE`, array element `k` at `ptr + HEADER_SIZE + 4 + 8*k`, object -entry `k` at `ptr + HEADER_SIZE + 4 + 16*k`); `typeof`, the object & array types `x[i]` runtime -dispatch, and the `run.rs` decode dispatch on the **type byte** instead of the -NaN-box tag. **No user-visible behavior changes** — the refactor is validated -entirely by the base-subset-through-operators regression staying result-for-result green; it is the -campaign's highest-risk stage and ships first. - -**(b) Function-value representation.** A function value is a **heap block, type byte -`3`**, holding a wasm function-table index and an environment pointer: +--- -```text -ptr ptr+HEADER_SIZE ptr+HEADER_SIZE+4 -┌────────┬────────┬───────────────┬───────────────┐ -│ type=3 │ pad ×3 │ func_idx:u32 │ env:i64 │ -└────────┴────────┴───────────────┴───────────────┘ +## 10. Conformance (test262) + +`crates/jsc-test262` runs the official tc39/test262 suite against the release +`jsc` binary. + +- **Checkout.** `scripts/test262-fetch.sh` clones test262 at a pinned commit + (`2e0a56762801e275a9fdf96dc49d90ba0cddcf63`) into `.tmp/test262`; it is + idempotent. +- **Eligibility** (`filter.rs`). A test is skipped when it has a `module`, + `async`, `CanBlockIsFalse`, or `CanBlockIsTrue` flag (or a `resolution`-phase + negative), is `noStrict`-only, uses a feature listed in + `tests/test262/unsupported-features.txt` (exact names or `prefix*`), or needs a + harness include jsc does not shim. `raw` tests run byte-for-byte; all others run + in strict mode. +- **Assembly** (`assemble.rs`). Strict programs are `"use strict";` + the jsc + harness shims from `tests/test262/harness/` (`sta.js`, `assert.js`, + `compareArray.js`) + the test source. Because jsc functions cannot carry + properties, `assert.throws(Name,` and `assert.(` calls in the test + source are rewritten to plain `assert_throws_Name(` / `assert_(` + functions. +- **Outcome** (`outcome.rs`). Each test runs as `jsc FILE` with a timeout + (default 10 s); exit codes map to pass / fail / compile-error / timeout, with + negative tests matched on parse versus runtime phase and the uncaught error + name. +- **Baseline.** `tests/test262/baseline.txt` lists every passing test path. + `--check` fails if any baseline test no longer passes; `--update-baseline` + merges the current passes into it. +- **History.** `--history` appends `commit pass eligible total` for a full run to + `tests/test262/history.tsv`. The current metric is its last row: + + | commit | pass | eligible | total | + |---|---|---|---| + | `5a97d3c` | 5195 | 22992 | 47665 | + + i.e. **5195 / 22992 eligible tests pass (≈ 22.6 %)**, out of 47665 tests in the + pinned checkout; `baseline.txt` holds the same 5195 paths. + +**Running it** (from the repository root): + +```bash +bash scripts/test262-fetch.sh +cargo build --release -p jsc +cargo run --release -p jsc-test262 -- --check # regression gate +cargo run --release -p jsc-test262 -- built-ins/Array # PATH_PREFIX filter +cargo run --release -p jsc-test262 -- --update-baseline --history ``` -`func_idx` indexes the module's new funcref **Table** (populated by an active -**element segment** listing every callable function); `env` points at the closure -**environment** (below) or is the **`null`** immediate for a non-capturing function. -A `function`/arrow literal lowers to `__alloc` of this block. `typeof` a function -value → `"function"`; a function value is **always truthy**. A `function foo(){}` -declaration is **both** directly callable by name (unchanged) **and** bound to a -function value in scope, so `let f = foo; f(…)` works. +Options: `--test262 DIR` (default `.tmp/test262`), `--jsc PATH` (default +`target/release/jsc`), `--jobs N` (default: available parallelism), +`--timeout SECS`, `--check`, `--update-baseline`, `--history` (full runs only), +and positional `PATH_PREFIX` filters. Per-test results are written to +`.tmp/test262-results.tsv`. The runner exits `0` on success, `1` when `--check` +finds a regression, and `2` on a setup error. -Calls to a function **value** cannot resolve the callee statically, so they lower to -**`call_indirect`** against the Table. Since `call_indirect` needs one function type -but source functions vary in arity, the closures wave adopts a **uniform ABI**: +--- -```text -(a0: i64, a1: i64, …, a_{K-1}: i64, env: i64) -> i64 ;; K = max arity in the program -``` +## 11. CI gates -Positional args are a fixed `K` slots (`K` = the **maximum arity across the -program's functions**, computed at emit time — a single per-program signature, not -a hard-coded constant; `K` ≥ every function's arity by construction) followed by a -**trailing `env`** slot (the closure environment, or `undefined` for a non-capturing -function). **Missing args are `undefined`; extra args are ignored** — matching JS and -letting one signature serve every function. **`env` reaches the callee as this -trailing argument, pushed by the *caller*:** a direct name-call pushes -`env = undefined` (a top-level declaration never captures); an indirect -`call_indirect` reads the callee's **own** `env` out of its function block -(`+HEADER_SIZE+4`) and pushes that. `env` is **last**, not first, so the callee data -(env + table index) is needed only *after* the arguments are on the stack — the -callee is evaluated once, its block base stashed in a scratch local, and env then -index read back-to-back, so a nested indirect call in an argument can never clobber -it. Inside a closure body `env` is read from wasm local `K` (the last parameter). One -body and one ABI serve every source function. Arrow functions (`(a,b)=>a+b`, -`x=>x*x`, expression- or block-bodied) and function expressions/nested function -declarations all lift to top-level functions and use the **same** representation and -ABI — they differ from a `function` value only in syntax (no `this`, no `arguments`; -both deferred to the built-ins wave). - -**(c) Closure environment.** A function created inside another may **reference** the -enclosing bindings; when it **escapes** (is returned/stored) those bindings must -outlive the enclosing call. The closures wave captures **by reference** through a heap -**environment of boxed cells**. A compile-time **capture analysis** over the operator & scoping wave -scope stack marks a binding referenced from an **inner** function as **captured**; -a captured binding is promoted to a **boxed cell** — heap block type `5`, -`[type=5][pad][value:i64]` (`__cell_get`/`__cell_set`) — while non-captured bindings -stay flat wasm locals (zero overhead for the common case). When a capturing function -literal is evaluated, the enclosing function builds a **closure env** — heap block -type `4`, `[type=4][pad][count:u32][cell_ptr_0]…[cell_ptr_{count-1}]` — holding the -**cell pointers** for the captured bindings, and stores it in the function value's -`env` slot. Inside the closure body a captured name resolves to `env[slot] → cell → -value`. Because the env holds **cell pointers** and both the enclosing function and -the closure read/write the **same** cells, capture is **by reference**: the returned -`counter` and any other holder share the one `c` cell, so `n(); n(); n()` yields -`3`. Cells and envs are bump-allocated and never freed (no GC — a closure-heavy -program can exhaust the one page; undefined, as elsewhere). - -**Member-compound assignment (the operator & scoping wave deferral).** `a[i] op= e` / `o.k op= e` (and -member `++`/`--`) land with the **object + index/key read once**: eval `a`,`i` (or -`o`) into temps, load the slot (array `i64.load` at `ta + HEADER_SIZE + 4 + 8*ti`; -object via `__obj_get`), combine with `e` via `op ∈ {Add,Sub,Mul,Div,Mod}`, store -back (array `i64.store`; object `__obj_set`); prefix yields the new value, postfix -yields `old`. Existing index/key only; `const` **rebinding** still errors while -member mutation of a `const`-bound object/array is allowed. - -**The `run.rs` boundary (moves once).** `decode(bits, memory)` recognizes the single -heap tag, reads the **type byte** at `ptr`, and dispatches string (`0`) / array (`1`) -/ object (`2`) — each shifted by `HEADER_SIZE` — plus a new **function** arm (`3`); -env/cell type bytes (`4`/`5`) are internal and never appear as a top-level value. -`Value` gains a unit `Function` variant printed Node-console style; the shipped -decoder does not recover a function's source name, so it always prints -`[Function (anonymous)]`. Every immediate arm is unchanged. - -```rust -pub enum Value { - Number(f64), Bool(bool), Null, Undefined, TypeStr(&'static str), - Str(String), Array(Vec), Object(Vec<(String, Value)>), - Function, // NEW — printed as [Function ...] -} -pub fn decode(bits: u64, memory: &[u8]) -> Value; // heap arm reads the type byte -pub fn format_value(v: &Value) -> String; // Function -> "[Function ...]" -``` +All workflows use Rust toolchain `1.99.1`. -**Documented simplifications / deferrals (the built-ins wave onward).** `this` / method calls -(`o.m()`, `this` binding), **default parameters**, **rest / spread**, **generators** -(`function*`, `yield`), **async / await**, `Function.prototype.{bind,apply,call}`, -the **array higher-order methods** `map` / `filter` / `reduce` (expressible once -functions are first-class, but the method surface is the built-ins wave onward), **loose equality** -`==` / `!=`, **`**` exponentiation**, and **`for-of` / `for-in`** are all deferred -to the built-ins wave onward. The closures wave gives functions identity, arity (missing → `undefined`, extra → -ignored), and lexical capture — but no receiver binding, no argument adapters, and -no iteration protocols. There is still no GC. - -**Boundary hardening — how each deferred construct fails.** Integration -hardening pinned the out-of-scope boundary so a deferred construct never silently -produces a wrong value. Most fail at **compile time** with a clear message: -`this`, method-shorthand object properties (`{ m(){} }`), default parameters -(`a = 1`), rest parameters / spread (`...`), `for-of` / `for-in`, `==` / `!=`, and -`**` were already rejected by the lowering; **`async` and generator (`function*`) -functions** were *not* — the lowering ignored the flags and compiled the body as an -ordinary function (an `async` body must return a `Promise`, a `function*` a -generator), so this adds an explicit reject at every function-building site -(declaration, function expression, arrow). `Function.prototype.{bind,apply,call}` -fail at **run time** (a `.bind`/`.call` member read off a function value traps -rather than mis-dispatching). **Remaining known gap:** the array higher-order -methods `map` / `filter` / `reduce` / `forEach` are *not* cleanly rejected — a -named-property read on an array/string (`a.map`, `a.foo`) reuses the object & array types `x[i]` -runtime dispatch, which coerces the string key and returns garbage instead of the -JS `undefined`, so `a.map(f)` mis-evaluates. A clean fix belongs to the object & array types array -runtime (there is no static type to reject `a.map` at compile time without also -rejecting valid object reads `o.map`), so it is deferred with the rest of the -the built-ins wave onward method surface rather than patched here. - -### Built-in functions & the runtime - -This wave made **functions first-class values** without touching the value -representation. The built-ins wave gives the subset a **standard-library surface and real program -output**: `console.log` as a wasmtime **host import** (so a program's output no longer -rides on its final-expression value), the `Math` numeric functions + constants, the -global type **conversions** (`String`/`Number`/`Boolean`/`parseInt`/`parseFloat`), -**growable arrays** (a `realloc` on the bump allocator + `push`/`pop`), the **array -higher-order methods** `map`/`filter`/`reduce`/`forEach` (invoking a jsval function -callback through the closures wave `call_indirect` **uniform ABI** — this **fixes** the -The closures wave-documented gap where `a.map` mis-evaluated through the `x[i]` dispatch), and the -remaining everyday **array/string methods**. The `i64` jsval carrier, the closures wave -header-byte heap encoding, and the frontend → codegen → emit → run pipeline shape are -all **unchanged**; it adds **one host import** (`console.log`), a family of runtime -`__`-helpers, and a **`realloc`** on the allocator, and teaches the lowering to -**recognize built-in calls** at the call/member sites. - -**Reused machinery (unchanged carrier + heap encoding).** The NaN-boxed `i64` jsval -and the closures wave single-heap-tag + type-byte header (`HEADER_SIZE = 4`; type bytes `0` -string, `1` array, `2` object, `3` function, `4` env, `5` cell) — **unchanged**. The built-ins wave -adds **no** new NaN-box tag and **no** new heap type byte: growable arrays reuse type -byte `1`, callbacks are ordinary type-`3` function values, and `console.log` renders -the existing decoded `Value` set. The funcref Table + `call_indirect` uniform ABI -(`(a0..a_{K-1}, env) -> i64`, `env` **last**), the bump allocator, the truthiness -lowering (`emit_cond`), the coercion arms (`+`/`ToNumber`/`ToString`), and the operator & scoping wave/the closures wave -scope stack are all reused; the only structural change under the hood is the allocator -gaining a **`realloc`** (still bump-only, no free). - -**(a) `console.log` host import.** Through the closures wave a program's observable result is its -**final expression**, decoded once at the `run.rs` boundary. This adds `console.log` as -a wasmtime **host import** — the first time the guest calls back into the host — so -**output** is decoupled from the returned value. The module imports `"console"`.`"log"`; -`console.log(a, b, …)` writes its `argc` jsvals into a memory region and calls -`$console_log(argc:i32, args_ptr:i32)`; the wasmtime `Linker` satisfies it with a host -closure over the guest `Memory` that reads `argc` `i64` jsvals at `args_ptr`, -**`decode`s + `format_value`s each with the same functions the boundary uses**, joins -with a single space, and writes `line + "\n"` to stdout: +### `ci.yml` — Rust gate -```text -console.log(40+2) => prints 42 -console.log("hi") => prints hi (string UNQUOTED) -console.log(1,2,3) => prints 1 2 3 (space-separated) -console.log(true,null)=> prints true null -``` +**Triggers.** `push` to `main` and `pull_request`, both restricted to these +paths: `crates/**`, `Cargo.toml`, `Cargo.lock`, `rust-toolchain.toml`, +`clippy.toml`, `rustfmt.toml`, `deny.toml`, `_typos.toml`, `tests/test262/**`, +`scripts/test262-fetch.sh`, `.github/workflows/ci.yml`. Changes elsewhere +(JS/TS packages, apps, other scripts, docs) do not run it. -Numbers render shortest-round-trip, strings **unquoted**, `true`/`false`/`null`/ -`undefined` literally, arrays/objects in `format_value` shape, functions -`[Function (anonymous)]`. `console.log` evaluates to `undefined`. A program may print -many times and end on any (or no) value; the CLI keeps printing the decoded final -value for expression programs, but the built-ins wave tests that assert **stdout** drive it through -`console.log`. - -**(b) `Math`, conversions.** `Math` is a **built-in namespace recognized at the -member/call site**, not a heap object (and **not** a passable value). `Math.floor`/ -`ceil`/`round`/`trunc`/`abs`/`sqrt` lower to the wasm `f64` ops (`round` = `floor(x + -0.5)` for JS round-half-**up**, unlike wasm `f64.nearest`'s round-half-to-even); -`Math.min`/`max` fold `f64.min`/`max` over the `ToNumber`-coerced args seeded from -the identity infinity (so `Math.min()` => `Infinity`, `Math.max()` => `-Infinity`, -and any `NaN` argument propagates); `Math.pow` → -a runtime `__math_pow` helper (no wasm `f64.pow` opcode); `Math.PI`/`Math.E` emit -`f64.const` at the member-read site. The global **conversions** are recognized by -callee name over the existing coercion arms: `String(x)` = `ToString` → a **string -jsval** (so `typeof String(42)` is `"string"`), `Number(x)` = `ToNumber` -(`Number("42")` => `42`, `Number(true)` => `1`, non-numeric → `NaN`), `Boolean(x)` = -The value model truthiness, `parseInt(s)` = a leading **base-10** integer off the string-coerced -arg (`parseInt("42px")` => `42`), `parseFloat(s)` = a leading decimal. All three of -`String`/`Number`/`Boolean` route through the **same** helpers as `+`/`typeof`/ -truthiness so conversions and coercion agree. - -**(c) Growable arrays.** the object & array types arrays are fixed-length. The built-ins wave extends the allocator with -`__realloc(ptr, old_size, new_size)` — bump a **fresh** block, `memory.copy` the old -contents, return the new pointer; the old block is **abandoned** (still no free, no -GC). `Array.prototype.push(v)` grows the block to `HEADER_SIZE + 4 + 8*(len+1)` (exact -one-slot growth), writes `v` at element `len`, stores the new `len`, and — because the -realloc **changes the array pointer** — writes the possibly-new pointer back into the -array's **binding** (local or boxed cell), returning the new length. `pop()` reads the -last element and lowers `len` (no realloc). So `let a=[1,2]; a.push(3); a.length` => `3` -and `a[2]` => `3` through the same binding. - -**(d) Higher-order array methods (fixing the closures wave gap).** `map`/`filter`/`reduce`/ -`forEach` are the first **runtime** clients of the closures wave's first-class functions: each -receives a **function-value jsval** callback and invokes it through the closures wave -`call_indirect` uniform ABI. This is the stage that **fixes** the closures wave-documented gap -where a named-property read on an array (`a.map`) fell through the object & array types `x[i]` runtime -dispatch and mis-evaluated (the boundary-hardening rules). A helper calls the callback `cb` -**exactly like the closures wave's indirect call** — read `func_idx` (`cb+HEADER_SIZE`) and `env` -(`cb+HEADER_SIZE+4`), push the element then `env` (**env last**), pad to `K` with -`undefined`, `call_indirect`. Callbacks receive `(element)` only (the `(index, array)` -extra params are the exceptions wave onward). `map` builds a new same-length array of `cb(x)`; `filter` a new -(growable) array of the elements where `cb(x)` is truthy; `reduce(cb, init)` folds -`acc = cb(acc, x)` from the **required** `init`; `forEach` calls `cb(x)` for side -effects and returns `undefined` (`let s=0; [1,2,3,4].forEach(x=>{s=s+x}); s` => `10`, -the callback mutating a captured the closures wave cell). Recognition is **call-position only**, so a -valid object property `let o={map:1}; o.map` still reads `1`. - -**(e) Array & string methods.** The everyday non-callback methods, recognized as -built-in method calls on an array/string receiver and lowered to runtime helpers over -the closures wave heap layout. Array: `indexOf(v)` (strict `===` scan, `-1` if absent), -`includes(v)`, `join(sep)` (`ToString` each element + the string-coerced `sep` -between), `slice(a,b)` (a new array, negative-index/clamp). String (**ASCII**, -byte-indexed): `toUpperCase`/`toLowerCase` (map the `a–z`/`A–Z` bytes), `indexOf(sub)` -(byte offset or `-1`), `slice(a,b)` (a new string, negative-index/clamp), `charAt(i)` -(a one-char string or empty). `[1,2,3].join("-")` => `"1-2-3"`; `"HELLO".slice(1,3)` -=> `"EL"`. - -**The `run.rs` boundary (one addition).** This adds **no** `Value` variant and **no** -decode arm — every the built-ins wave value is an existing the closures wave kind. The boundary changes in exactly -one way: the wasmtime `Linker` gains the `console.log` host import, and its formatter -**reuses `format_value`** so a printed line and a decoded final value render -identically. `decode` / `format_value` are otherwise unchanged from the closures wave. - -```rust -// run.rs — unchanged decode/format; the Linker gains one host import. -pub fn decode(bits: u64, memory: &[u8]) -> Value; // the closures wave, unchanged -pub fn format_value(v: &Value) -> String; // the closures wave, unchanged — reused by console.log -// linker.func_wrap("console", "log", |caller, argc: i32, ptr: i32| { … render each jsval … }) -``` +**Job `rust`** (fmt + clippy + test + deny + shear + typos): -**Documented simplifications / deferrals (the exceptions wave onward).** `this` / method calls on **user** -objects (`o.m()`, `this` binding), `Object.keys`/`values`/`entries`, **JSON**, -**`Date`**, **`RegExp`**, **`Map`**/**`Set`**, **`Promise`** / async / await, -**generators**, loose equality `==`/`!=`, **`**`** exponentiation, **`for-of`**/ -**`for-in`**, **spread**, **default / rest params**, the `Number`/`String` **prototype** -surface beyond §(e) (`toFixed`, `padStart`, `repeat`, `split`, …), `Math.floor` **as a -value**, the callback `(index, array)` extra params, `reduce` without an init, the -`parseInt` radix, and **locale / full-Unicode** correctness (the built-ins wave string case + search -are **ASCII**, byte-indexed, no normalization) are all deferred to the exceptions wave onward. The built-ins wave gives the -subset a built-in library surface and a print channel — but no user-object method -dispatch, no reflection/serialization, and no new host/heap types. There is still no -GC (the `realloc` abandons the old block; a push-heavy loop can exhaust the one page). - -**Delivered — reconciled with the shipped code.** The sketch above holds; -the concrete runtime diverges in three named ways. **(1) Three host imports, not one.** -The module declares **three** host functions in the **`"env"`** namespace (function -indices 0/1/2): `env.print(bits: i64, flag: i32)` (backs `console.log`, per-arg flag for -space vs. newline), `env.__convert(bits: i64, op: i32) -> i64` (backs `String`/`Number`/ -`parseInt`/`parseFloat`), and `env.__method(recv: i64, op: i32, arg0: i64, arg1: i64) -> -i64` (backs the non-callback array/string methods, dispatched host-side on the receiver's -runtime type). All three reuse the boundary's `decode`/`format_value`. **(2) Host-side -heap allocation via the exported `__heap_ptr` global.** The bump allocator's free offset -is a wasm global exported as `"__heap_ptr"`; `env.__convert`/`env.__method` build strings -and arrays directly into guest linear memory by reading and bumping that exported global, -so a host-built heap value is visible to the guest with no copy-back. **(3) Arity-scan -bump for higher-order callbacks.** The uniform `call_indirect` arity `K` is widened at any -`.map`/`.filter`/`.forEach` call to `max(K, 2)` and any `.reduce` call to `max(K, 3)`, -because those helpers push extra positional args (`(element, index)` / `(acc, element, -index)`) beyond the callback's own params; without the bump a `call_indirect` type -mismatch would trap. The **delivered limitations** carried into the exceptions wave onward: `Math.pow` -truncates fractional exponents (no `f64.pow`), `parseInt` is base-10 only (radix/`0x` -ignored), string case/search/slice/`charAt` are ASCII byte-indexed, `push`/`pop` update -only a plain-local/cell binding (an aliased copy is not), and `reduce` requires an init -value. - -### Exception handling: `throw` / `try` / `catch` / `finally` - -This wave gave the subset a **standard-library surface**. The exceptions wave — the **final** -campaign — gives it **error handling**: `throw`, `try` / `catch` / `finally`, and -`Error` / `TypeError` / `RangeError` objects. The mechanism is a **manual-unwinding** -model — **no wasm exception-handling proposal** — so the emitted module stays portable -on plain `wasm-encoder` + wasmtime. Two mutable module globals **`$exc_pending`** (i32, -0/1) and **`$exc_value`** (i64 jsval) carry the in-flight exception; codegen keeps a -**handler-label stack**; a **call-site check-and-propagate** runs after every -user-function / callback call; and `run.rs` surfaces an uncaught top-level exception as -a distinct, non-panic `Uncaught ` error. The `i64` jsval carrier, the closures wave -header-byte heap encoding, and the frontend → codegen → emit → run pipeline shape are -all **unchanged**; it adds **two module globals**, three IR nodes (`Throw`, `Try`, -`NewError`), the handler-label plumbing, an `Error` heap object, and the uncaught -surfacing. - -**Reused machinery (unchanged carrier + heap encoding).** The NaN-boxed `i64` jsval -and the closures wave single-heap-tag + type-byte header (`HEADER_SIZE = 4`; type bytes `0` -string, `1` array, `2` object, `3` function, `4` env, `5` cell, `6` reserved error) — -**unchanged**. This adds **no** new NaN-box tag: the `Error` object is an **ordinary -heap object** (the existing object byte `2`) carrying `name`/`message` — the reserved -error byte `6` was left **unused** (an `Error` needs no distinct layout), and the -unwinding state lives in **two module globals**, not in the value encoding. The funcref Table + `call_indirect` uniform ABI (`env` last), the bump -allocator (+ the built-ins wave `__heap_ptr` global / `__realloc`), the truthiness lowering, the -coercion arms, and the operator & scoping wave/the closures wave scope stack are all reused; the only structural additions -are the two globals and the codegen handler plumbing (inert when nothing throws). - -**(a) Unwinding infrastructure.** Two **mutable, exported** module globals: -`$exc_pending` (i32, init `0`; `1` = an exception is in flight) and `$exc_value` (i64 -jsval, init `0`; the thrown value), exported under the names `__exc_pending` / -`__exc_value` for `run.rs` to read back. `throw expr` evaluates `expr`, `global.set -$exc_value`, `global.set $exc_pending = 1`, then transfers control to the nearest -handler — `br` to the innermost `try`'s catch-dispatch block label if inside a `try` in -the **same function**, else **early-return** a dummy `i64` from the current function. -After **every** call into user code (direct user calls and `call_indirect`) codegen -emits a **check-and-propagate**: - -```wat -;; … call $user_fn (or call_indirect …) -global.get $exc_pending -if - ;; br to the innermost handler label, or early-return if the handler stack is empty -end -``` - -Built-in / host-import calls known not to throw (`console.log`, `Math.*`, the -conversion/method helpers) need **no** check — only user code raises a jsval exception. -Codegen keeps a **handler-label stack** of catch-dispatch labels: entering a `try` -pushes its label, leaving pops it; `throw` and the call-site checks branch to the -**top**, or early-return when empty. - -**(b) `try` / `catch`.** `try { BODY } catch (e) { HANDLER }` wraps `BODY` in a wasm -`block` whose end is the **catch-dispatch** (its label pushed on the handler stack for -`BODY`'s duration). After the block: `global.get $exc_pending; if { clear pending; bind -$exc_value to the catch param `e` in a fresh scope; run HANDLER }`. On the no-throw path -`$exc_pending` is `0`, the dispatch is skipped, and `BODY`'s effects stand; on the -exceptional path the `catch` clears the flag and handles the value. +- `cargo fmt --all --check` +- `typos` +- `cargo clippy --all-targets --workspace` (deny-warnings from the workspace lints) +- `cargo nextest run --workspace` +- `cargo test --doc --workspace` +- `cargo shear` +- `cargo deny check` -```text -try { throw 42 } catch (e) { e } => 42 -try { throw "boom" } catch (e) { e } => boom -let r=0; try { r = 42 } catch(e){ r = -1 } r => 42 (no-throw path) -``` +**Job `test262`** (baseline regression gate, 90-minute timeout): fetch test262 +(cached on the hash of `scripts/test262-fetch.sh`), `cargo build --release -p +jsc`, then `cargo run --release -p jsc-test262 -- --check`. -**(c) `finally`.** `try{}finally{}` and `try{}catch{}finally{}` — the `finally` body is -emitted so it runs on **every** exit from the protected region: the normal fall-through, -every exceptional (throw) branch, and a `return` / `break` / `continue` that leaves the -`try` body. After `finally`, **if `$exc_pending` is still set**, re-propagate (branch to -the outer handler / return) — a normally-completing `finally` does **not** swallow a -pending exception. +### `web.yml` — JS/TS gate -```text -let x=0; try { x=40 } finally { x = x + 2 } x => 42 (finally normal) -let x=0; try { throw 1 } catch(e){ x=40 } finally { x=x+2 } x => 42 (finally on throw) -``` +**Triggers.** `push` to `main` and `pull_request` on `crates/**`, `packages/**`, +`apps/**`, `scripts/**`, `Cargo.toml`, `Cargo.lock`, `package.json`, `bun.lock`, +`.github/workflows/web.yml`; plus manual `workflow_dispatch`. -**`finally` on every exit edge.** The `finally` landing block converges **all** ways -control leaves the protected region — the normal fall-through, the *exceptional* `br` -(throw) edges, and a `return` / `break` / `continue` that exits the `try` body. Each -abrupt exit is recorded as a **deferred action** before the finalizer runs; when -`finally` completes normally the pending action resumes (return the stashed value, -branch to the enclosing loop/switch target, or re-propagate the exception). A `finally` -that **itself** completes abruptly — a `return`, `throw`, `break`, or `continue` inside -the `finally` — **overrides** the pending action (standard JS completion semantics): -`try { return 1 } finally { return 2 }` returns `2`; `try { throw 1 } finally { return 2 }` -swallows the throw and returns `2`; `try { return 5 } finally { throw 9 }` discards the -return and propagates `9`. `finally` is therefore correct on every normal, throw, -`return`, `break`, and `continue` path. - -**(d) `Error` objects.** `new Error(msg)` / `new TypeError(msg)` / `new RangeError(msg)` -are recognized at the `new`-call site (like the built-ins wave built-ins, only when unshadowed) and -lower to the **same IR as the object literal** `{ name: "", message: }` — -an ordinary object (byte `2`) carrying `.name` (the constructor name) and `.message` -(the string-coerced arg; empty for no arg). Member reads `e.name` / `e.message` reuse the -existing member-read path, and `typeof (new Error("x"))` returns `"object"`. `throw` -accepts **any** jsval — `Error` is just the idiomatic payload. +**Job `web`** (biome + typecheck + build + test): install Rust with the +`wasm32-unknown-unknown` target, `wasm-pack`, and Bun; `bun install +--frozen-lockfile`; build `@cdlab/jsc` (wasm artifact + TS dist); `bun run lint` +(biome); typecheck `@cdlab/jsc` and `@cdlab/jsc-web`; build `@cdlab/jsc-web`; +test `@cdlab/jsc` (vitest). -```text -try { throw new Error("bad") } catch (e) { e.message } => bad -typeof (new Error("x")) => object -``` +### `release.yml` — release binary -**(e) Propagation, nesting & rethrow.** A `throw` in a deep callee early-returns; each -caller's call-site check re-branches, unwinding frame by frame to the nearest enclosing -`try`. Nested `try` pushes the inner label above the outer; an inner-`catch` throw -continues to the outer. `throw e` inside a `catch` is an ordinary throw from within a -handler. +**Triggers.** `push` of a `v*` tag, or `workflow_dispatch` with a `version` +input (the workflow then creates and pushes that tag). -```text -function f(){ throw 42 } function g(){ f() } try { g() } catch(e){ e } => 42 (propagation) -try { try { throw 1 } catch(e){ throw e + 41 } } catch(e){ e } => 42 (nested / rethrow) -``` +**Job `release`.** Installs the `x86_64-unknown-linux-musl` target and +`musl-tools`, runs `cargo build --profile dist --target +x86_64-unknown-linux-musl --locked -p jsc`, packages the binary as +`jsc-linux-x64.tar.gz`, and publishes it with `gh release create +--generate-notes`. -**The `run.rs` boundary (uncaught surfacing).** This adds **no** `Value` variant and -**no** decode arm — a thrown value is an ordinary jsval. The boundary changes in -exactly one way: **after** invoking `main`, `run.rs` checks the exported `$exc_pending` -global; if set, it renders the exported `$exc_value` with `format_value` and returns a -distinct, **non-panic** `Uncaught ` `Err`. Otherwise the run proceeds exactly as -Every wave through the built-ins (decode the final value / the `console.log` output stands). - -```rust -// run.rs — after invoking main, before decoding the result: -// let pending: i32 = exc_pending_global.get(&mut store); -// if pending != 0 { -// let bits: i64 = exc_value_global.get(&mut store); -// return Err(anyhow!("Uncaught {}", format_value(&decode(bits as u64, mem)))); -// } -pub fn decode(bits: u64, memory: &[u8]) -> Value; // the closures wave/the built-ins wave, unchanged -pub fn format_value(v: &Value) -> String; // the closures wave/the built-ins wave, unchanged — reused for the Uncaught render -``` +--- -**Documented final boundary (the exceptions wave is the last campaign).** The existing **runtime traps** -(division-by-zero, `push` on a non-array, …) **stay wasm traps** — they are **not** -converted into catchable JS errors; this is the explicit boundary between the JS-level -exception model and the machine-level trap surface. (An out-of-bounds array/string index -read is **not** in this set: it returns `undefined` rather than trapping, so -`try { a[999] } catch(e){}` neither throws nor traps — `a[999]` is simply `undefined`.) -Also out of scope, as the documented final -boundary (of which **`error.stack`**, **`.name`**, and **`instanceof` object -identity** later landed in the [Error-identity section](#error-object-identity-instanceof--name--stack)): -**custom `Error` subclasses via `class`**, -**`Error.captureStackTrace`**, **Promise / async rejection**, and (carried from the built-ins wave) -loose equality `==`/`!=`, `**`, `for-of`/`for-in`, `this` / methods on user objects, -spread, default/rest params, generators, and `Date`/`RegExp`/`Map`/`Set`/JSON. Because -The final wrap-up writes the definitive core-language -summary enumerating what the subset supports and this final boundary. - -### Syntax sugar - -A wave of **syntactic sugar** layered on top of the core subset. Every feature here is -**pure front-end lowering**: each surface form is rewritten into IR that the core subset already -emit, so no new ABI constant, no new runtime import, and no new wasm-emit path is -introduced — the additions live entirely in `frontend`/`codegen` and reuse existing -`IrNode`/`IrStmt` shapes. swc's `EsSyntax` already parses all of these forms; the work is -in the AST → IR lowering. Valid-program behaviour is unchanged for any program that did -not use these forms. - -**Template literals → `+` string concatenation.** A `` `a${x}b` `` (`Expr::Tpl`) lowers to -a left-folded chain of `IrNode::Binary { op: BinOp::Add }` alternating cooked-quasi string -literals and interpolated expressions: `Str(q0) + x + Str(q1) + …`. This reuses `Add`'s -existing **string-coercion** path (the same one that makes `"x" + 8` yield `"x8"`), so an -interpolated number/boolean is stringified exactly as `+` already does — no dedicated -"template" IR node. An empty leading/trailing quasi lowers to `Str("")`. Tagged templates -are **not** in scope. - -**Exponentiation `**` / `**=` → `MathOp::Pow`.** The binary `a ** b` (`BinExpr` with -`BinaryOp::Exp`) lowers to `IrNode::Math { op: MathOp::Pow, .. }` — the same inline-pow IR -`Math.pow` already uses, so no new emit code. `**` is **right-associative** (`2 ** 3 ** 2` -= `2 ** (3 ** 2)` = 512), which swc's parse tree already encodes. The compound `x **= e` -desugars, like the other compound assignments, to `x = x ** e` — an assignment whose RHS is -the `MathOp::Pow` node — so it flows through the existing lvalue-assignment path (bindings, -array elements, and object properties alike). - -**`for-of` over arrays/strings → counted index `while`.** `for (const v of ITER)` -(`Stmt::ForOf`) lowers to an `IrStmt::While`: `ITER` is evaluated **once** into a temp -local and its `.length` is cached **once** into a second temp, then an index counter walks -`0..length`, binding `v` to `ITER[i]` at the top of each iteration. The loop variable is -**block-scoped** per iteration (a captured `let` binding gets a fresh cell each pass), and -the index counter lives in an outer scope so `continue` re-runs the increment via the -`While.update` slot — matching the C-style-`for` desugar. The iterable is restricted to -**arrays and strings**; a statically non-iterable literal (e.g. a number) is a **spanned -codegen rejection** (`for-of iterable must be an array or string`), and `for-await-of`, -destructuring loop bindings, and `for-in` remain rejected. - -**Optional chaining `?.` (`a?.b` / `a?.[i]` / `a?.()`) → nullish short-circuit to -`undefined`.** An `Expr::OptChain` is **flattened** (`collect_chain_links`) into its root -base plus an ordered list of member/index/call links, where each link records whether it -was written with `?.` (optional) or `.` (a plain continuation link such as the `.c` in -`a?.b.c`, which swc nests in the same tree). Each **optional** link evaluates its base once -into a fresh temp, tests it for nullishness (`temp === null || temp === undefined`, built -from an `IrNode::Logical`), and routes the short-circuit through the **`else` arm of a -`Ternary`**: a nullish base yields `undefined` and the entire remaining chain is skipped; -otherwise the link (member access, computed index, or call) is applied to the temp and -evaluation continues. Non-optional links are applied unconditionally. - -**Logical assignment `??=` / `||=` / `&&=` → conditional assign via `LogicalOp`.** These -compound assignments map (`map_logical_assign_op`) to the existing -`LogicalOp::{Nullish, Or, And}` rather than to an eager `BinOp`, preserving JS -**short-circuit** semantics: `x ??= e` assigns `e` only when `x` is `null`/`undefined`, -`x ||= e` only when `x` is falsy, and `x &&= e` only when `x` is truthy — so `e` (and the -store) is skipped otherwise. They reuse the same lvalue targets as the arithmetic -compounds. - -**`do-while` + labeled `break`/`continue` → loop lowering + label→target control flow.** -`do { body } while (cond)` (`Stmt::DoWhile`) lowers to an `IrStmt::While` whose `body` is -prefixed so the body runs **once before** the condition is first tested. Labels extend the -loop IR with a `label: Option` id (and `IrStmt::Break(Option)` / -`IrStmt::Continue(Option)`): a `name:` label allocates a **fresh label id** -(`fresh_label`), binds `name → id` while its immediately-nested loop body is lowered, and -stamps that id onto the loop's `While.label`. A `break name` / `continue name` resolves -`name` to its id and carries `Some(id)`, targeting **that** loop even from inside a nested -loop; an unlabeled `break`/`continue` carries `None` and targets the nearest enclosing -loop/switch as before. A label on a non-loop statement is a **spanned rejection**. Intervening -`finally` clauses still run on the way to a labeled target, exactly as for the -unlabeled forms. - -### Modern syntax: destructuring, spread, default/rest params & optional chaining - -A second wave layered on top of the sugar wave, adding **destructuring**, **parameter defaults**, -and **rest/spread**. Unlike the sugar wave, the modern-syntax wave is **not** pure front-end lowering: rest/spread need -real heap arrays and the uniform calling convention had to grow a dynamic argument -count, so the modern-syntax wave touches `abi.rs`, the runtime helpers, and the emit paths as well as -`codegen`. - -**Array destructuring in `let`/`const`/`var` declarations → hidden temp + indexed -reads.** `const [a, b] = e` evaluates `e` **once** into a hidden temp local, then binds -each target to an indexed read (`temp[0]`, `temp[1]`, …) via the shared -`lower_binding_pattern` helper. It supports **holes** (`[, b]` skips index 0 without -binding), **defaults** (`[a = 9]` lowers to `temp[0] === undefined ? 9 : temp[0]`), -a **rest element** (`[a, ...r]` binds `r` to the tail slice of the temp), and arbitrary -**nesting** (`[[a], {b}]`), where each nested pattern recurses on the corresponding -indexed read as its own sub-temp. - -**Object destructuring in declarations → keyed reads + rest copy.** `const {x} = e` -reads each named property off the once-evaluated temp. It supports **rename** -(`{x: a}` binds `a` to `temp.x`), **defaults** (`{z = 7}` guards an `undefined` -property with the same `=== undefined ? default : value` ternary), **nesting** -(`{p: {q}}`), and a **rest property** (`{a, ...rest}`), where `rest` is built by the -`__obj_rest` runtime helper — a key-excluding shallow copy that clones the source -object while omitting the already-bound keys. Both array and object patterns share the -one `lower_binding_pattern` entry point. - -**Default parameters `f(a, b = 2)` → per-call prologue guard.** Each defaulted parameter -emits an `if (param === undefined) param = default;` guard at the **top of the function -body**, run **per call**, so an omitted (or explicitly `undefined`) argument takes the -default. A default expression may **reference earlier parameters** (`f(a, b = a + 1)`) -because the guards run in declaration order after all slots are bound. `FnSig` gained a -**required-argument count** so callers and the prologue know how many leading params are -mandatory. - -**Rest parameters `f(...args)` → trailing args collected into a real array, dynamic -argc.** A trailing rest parameter binds to a **heap array** holding every argument beyond -the fixed ones. Supporting this required widening the **uniform fixed-arity ABI**: the -canonical wasm function signature became `(a0 … a_{K-1}, env, argc) -> i64` — the fixed -positional slots, the closure `env` slot, and a new **`argc`** (actual argument count) -threaded through **direct calls, indirect/closure calls, and higher-order calls** alike. -`abi.rs` remains the **single source** for these slot/arity constants; `argc` lets the -rest-collection code know where the real arguments stop. - -**Spread in call args and array literals `f(...a)`, `[...a, ...b]` → `__array_concat`.** -A spread source is flattened by the `__array_concat` runtime helper, which builds a fresh -**heap array** from its inputs. In an **array literal**, the concatenated array is the -value. In a **call**, the flattened elements are spread into the K fixed positional slots -with the true length passed as `argc`. Spread-aware `Math.max`/`Math.min` **fold** over -the concatenated array rather than taking fixed positional operands. - -**Parameter destructuring `f([a, b])`, `g({x, y})` → `lower_binding_pattern` on a temp -slot.** A destructuring parameter is received into an ordinary temp parameter slot, then -`lower_binding_pattern` (the same helper the declaration forms use) unpacks it in the -prologue. It **composes** with defaults (`f([a] = [])`) and with rest, since all three go -through the shared binding path. - -**Caveat (no GC):** rest parameters and spread/array-literal concatenation allocate their -arrays (and `__obj_rest` its object copy) on the **bump heap**, which is never reclaimed — -fine for the short programs jsc targets, but every such allocation is permanent for the -run. **ABI limit:** a spread whose **runtime** length exceeds the fixed positional arity -`K` **drops the surplus arguments** — an inherent consequence of the fixed-arity ABI -(a call site reserves exactly `K` slots), and outside the modern-syntax wave milestones. - -### OOP: `this`, object methods & `class` - -**`this` + method calls on object literals.** Object literals gained **method** -and **shorthand-method** properties (`{ m(){..} }` ≡ `{ m: function(){..} }`, a -first-class function value that carries `this`) and **shorthand** properties (`{ n }` ≡ -`{ n: n }`). A call whose callee is a member expression, `obj.m(args)` — once the builtin -array/string/`Math`/`console` method names have been ruled out — is a **user method call** -that binds `this = obj` for the callee. It lowers to a dedicated `IrNode::MethodCallUser` -(kept distinct from `IrNode::CallIndirect`, which resets `this`, and from the builtin -`IrNode::MethodCall`). - -**This-binding model — a global carrier + per-function local snapshot.** The uniform -closure ABI is **unchanged** (`(a0 … a_{K-1}, env, argc) -> i64`); `this` rides a dedicated -mutable wasm **global** `$__this` (a jsval `i64`, index `THIS_GLOBAL_INDEX` in `abi.rs`, -initialized to `undefined`) rather than an extra parameter: - -- A **method call** `obj.m(args)` evaluates `obj` once into a temp, evaluates the - arguments, reads the callee function value at `obj.m` (the ordinary `__obj_get` member - path), sets `$__this = obj` **immediately before** the `call_indirect` (after every - sub-expression is evaluated, so a nested method call in an argument cannot leak its - receiver), then performs the normal indirect call. -- A **plain call** (`f(args)`, an indirect closure call, a spread call, a higher-order - callback) resets `$__this = undefined` right before the call, so a callee invoked - without a receiver sees `this === undefined`. -- **Every function/arrow prologue** that reads `this` snapshots `$__this` into a - function-local slot as its **first** statement, and a `this` expression reads that local. - This makes the global-clobber safe: a nested `this.other()` re-sets the global just - before its own call, but the enclosing frame already captured its own `this` into a local - at entry, so it is unaffected after the nested call returns. The snapshot is emitted - **only when the body actually reads `this`** — a `this`-free function needs none, and its - IR is unchanged — a pure optimization with no observable effect on the mandated model. - -**Out of scope (rejected with spans).** Getters/setters (`get x(){}` / `set x(v){}`), -computed method names (`[k](){}`), and async/generator methods are rejected. A **top-level -`this`** (outside any function, with no receiver) is rejected. **Arrows do not yet get -lexical `this`:** an arrow snapshots `$__this` like any function rather than capturing its -enclosing `this` — a documented limitation for a future refinement (an arrow used as a -callback therefore sees `this === undefined`, since a plain call resets the carrier). - -**`class` declaration + `constructor` + `new`.** A class name binds a **heap -object** holding two reserved, non-JS-writable **sigil keys** (interned string constants -in `abi.rs`): `@@constructor` = the constructor lowered as an **ordinary uniform-ABI -function value** (`(a0 … a_{K-1}, env, argc) -> i64`, the same closure ABI as every other -function — a class with no `constructor` gets a synthesized empty one), and `@@prototype` -= **one shared prototype object per class**. `new A(args)` (`IrNode::New`) -**allocates a fresh instance object** that is **pre-slotted** with the `@@prototype` link -plus one slot per own field, sets `$__this = instance`, runs the constructor with the -supplied arguments, **discards the constructor's return value**, and yields the instance -— so `typeof (new A()) === "object"`. The object model has **no dynamic key growth**, so -every own property must exist at allocation time: the instance shape is computed by a -**compile-time field-shape scan** of the class body, and the constructor may only -assign into slots that scan already reserved. - -**Member/index-write correctness fix.** This fixed a pre-existing -**scratch-clobber** bug in member/index assignment. `emit_index_set_node` / -`emit_index_node` stashed the receiver / key / value in the **low** scratch slots and then -re-emitted child sub-expressions over **those same slots**, so a **computed RHS** -(`o.v = 3 + 4`, `this.x = a + b`) overwrote the stash mid-emit and produced a wrong value -(or a trap in a constructor). The fix **relocates the set/get stash to dedicated HIGH -scratch slots** (disjoint read vs. write banks) and extends the fixed scratch bank -accordingly, so evaluating the RHS can no longer touch the stashed receiver/key. -**Documented limitation:** **same-kind nesting** — `a[b[i]]` (index inside index) or a -chained store-in-store `a[i] = (b[j] = k)` — reuses the same fixed high slots and remains -**out of scope**; correct nesting would need a **depth-indexed scratch allocator**. - -**class methods (shared via the prototype).** Each **non-constructor** method is -lowered **once** and stored on the class's **single shared `@@prototype` object** — never -copied per instance. A **member read** resolves **own properties first**, then falls back -to the instance's `@@prototype`. The fallback lives inside the `__obj_get` runtime helper, -which **recurses on the `@@prototype` slot** (own wins; the recursion terminates because a -prototype object itself carries no `@@prototype`). `MethodCallUser`'s callee read **reuses -the same member path**, so `obj.m` (read) and `obj.m()` (call) share the identical -own-then-prototype fallback. The fallback is emitted **only when a class exists** in the -program, so class-free programs keep the exact prior plain-object read behavior. - -**class fields (`field = init`).** An instance **field declaration** is desugared -to a synthetic `this. = ` statement and **prepended, in source order**, to -the constructor body *before* lowering — so field initializers run **before** the -constructor body and a later field init may read an earlier field through `this`. The -**pre-slotted instance shape** is the **union** of `{field declarations}` and -`{constructor `this. = ` assignments}`, **deduplicated by first occurrence**. A class -that has fields but **no explicit constructor** synthesizes a default constructor -consisting of just those field inits. An **uninitialized field** `f;` desugars to -`this.f = undefined`. - -**Out of scope for the class wave (all rejected WITH SPANS; future work):** `extends` / `super` / -inheritance, **static** members (methods and fields), getters/setters, **private `#`** -members, **computed** method/field names, and class **expressions**. The arrow -limitation also carries forward: **arrows do not yet get lexical `this`.** - -**`extends` / `super` and `static` members.** Inheritance shipped later (see the classes suite). **Static** members follow the same object-model discipline: -`static m(){}` and `static f = init` are lowered onto the class **VALUE** object (alongside -the reserved `@@constructor` / `@@prototype` entries), so `C.m()` / `C.f` read them directly -off the class. A static method is an ordinary lifted function whose `this` binds to the class -on a `C.m()` call. A static field's initializer that is a **bare literal** (`static x = 1`) is -evaluated inline in the class value; **any other** initializer — one that references the class, -an **earlier** static, or `this` — is **pre-slotted `undefined`** and assigned **after** the -class binding exists (`C. = `), in **source declaration order**, so a later static -observes the earlier ones and `this` inside the initializer resolves to the class. An -uninitialized `static f;` is `undefined`. Still out of scope: **static inheritance** (a derived -class's value has no constructor-side prototype link, so it does not see a parent's statics), -static **blocks**, and static **getter/setter/async/generator/computed** members (spanned -rejections, static or not). - -**Instance accessors (`get x(){}` / `set x(v){}`).** An **instance getter/setter** is a -property whose read/write runs a function. Each accessor property lowers to ONE -**`TYPE_ACCESSOR`** heap block (`[type=11][pad×4][getter:i64][setter:i64]`, `IrNode::MakeAccessor`) -holding the getter and setter as ordinary **lifted function values** (`undefined` for an absent -half); a `get x` and `set x` sharing the name `x` collapse into the same box. The box is stored -under the property name on the class's **shared `@@prototype`**, right alongside the plain method -values — so it is resolved by the **same own→prototype `__obj_get` fallback** and is **inherited** -by a subclass through the prototype-chain link. A **member/computed READ** (`obj.x` / `obj[k]`) -whose resolved value is a `TYPE_ACCESSOR` box invokes the **getter** with `this` = the receiver -(the stashed `scratch + 17`); a **WRITE** (`obj.x = v`) first resolves the key **through the -prototype chain** and, on an accessor hit, invokes the **setter** with `this` = the receiver and -the assigned value, discarding its result. A half-declared accessor degrades to JS semantics: -reading a **set-only** property yields `undefined`; assigning a **get-only** property is a silent -**no-op** (non-strict). Both halves read `this`/`super` through the this-carrier exactly like a -method, so a getter/setter body freely calls other methods and reads fields. A getter/setter that -**throws** propagates like any call — the get/set invocation sites track their enclosing `if` -frames, so a `try`/`catch` around the read/write observes the throw at the right handler -depth. The getter/setter's own arity is folded into the uniform ABI **`K`** by the class-body -arity scan (a setter's lone parameter has no arg-bearing call site, so it must widen `K` there). - -The invocation paths are **gated** on a compile-time sentinel (`@@accessor`, interned iff the -program builds any `MakeAccessor`), so an accessor-free program keeps **byte-identical** -member-read/write emission — the same idiom as the `@@prototype` gate on the prototype fallback. -Still out of scope: **static** accessors, **object-literal** accessors, and **computed** -accessor names (all spanned rejections). - -### Collection utilities: iteration, reflection & serialization - -A wave of **object/collection utilities** built on the fixed-shape object model and -the growable-array model. Every the collections wave primitive is an **in-wasm** implementation — no new host -import is added — so the guest stays self-contained and the built-ins wave host-shim debt is not -widened. The `__convert` import is reused for number formatting where needed; no other -import is touched. Objects remain **fixed-shape** (compile-time field scan; no dynamic key -growth), which is the single constraint that shapes every decision below — most visibly the -`JSON.parse` deferral. - -**`for-in` over object/array/string keys → counted index loop + `ObjectKey` -primitive.** `for (const k in obj)` (`Stmt::ForIn`) lowers, like `for-of`, to a counted -index `while`: the operand is evaluated **once** into a temp, its **key count** is cached -**once** (`IrNode::StrLen` reused as the count query), and an index counter walks `0..count`, -binding the loop variable to `IrNode::ObjectKey { iter, index }` — a reusable -key-**enumeration** primitive — as the **first** body statement (a captured `let` gets a -fresh cell per iteration; the increment lives in the `While.update` slot so `continue` still -advances it). For an **object** operand `ObjectKey` yields the own key **heap-string** in -insertion order; for an **array/string** operand it yields the decimal **index string** -(`"0"`, `"1"`, …). Hidden reserved keys are **skipped**: any key whose first payload byte is -`@` (the `@@prototype` / `@@constructor` sigils of the class wave) is not enumerated. Only a **single -`const`/`let`/`var` identifier head** is supported; a **destructuring/assignment head**, -`for await`, or `using` head is a **spanned rejection**, as is a statically-known -**non-enumerable** operand (a number/boolean/null literal → `for-in operand must be an -object, array, or string`). The head opens a lexical scope, so the binding is not visible -after the loop. - -**`Object.keys` / `Object.values` / `Object.entries` → in-wasm enumerators over the -key table.** Each lowers to a dedicated runtime helper (`__obj_keys` / `__obj_values` / -`__obj_entries`) that linear-scans the object's `(key_ptr, val)` entry table `0..n` and -appends each non-reserved entry to a **fresh growable array**: `keys` collects the key -heap-strings, `values` the stored jsvals, and `entries` a `[key, value]` two-element array -per entry. Reserved `@`-prefixed keys are skipped by the same first-byte guard -(`emit_key_not_reserved`) that `for-in` uses, so the class wave prototype/constructor sigils never -leak into reflection output. The returned arrays are ordinary heap arrays (grow via `push`, -render as `[…]`). - -**`in` + `delete` → own-key probe + compaction (no tombstone).** The `in` operator -(`"k" in obj`) lowers to the `__obj_has(obj_ptr, key_ptr)` runtime helper: an **own-key -only** membership probe (**no prototype walk** — a prototype-inherited method is *not* an own -key), returning `1`/`0`. `delete obj.k` lowers to `__obj_delete`, which **compacts the entry -out** rather than tombstoning it: on a key hit it `memory.copy`-shifts the trailing entries -down over the removed slot and **decrements the entry count `n`**. Because the deleted slot -physically disappears, every consumer **auto-skips** it with no per-consumer tombstone check — -`__obj_get` misses (`undefined`), `__obj_has` returns `0`, and every index-based enumerator -(`for-in`, `Object.keys/values/entries`) sees the shortened table. Compaction over -tombstoning trades a one-time `memory.copy` for keeping the hot read/enumerate paths -branch-free. - -**`JSON.stringify` → fully in-wasm recursive serializer.** `JSON.stringify(v)` lowers -to `IrNode::JsonStringify`, backed by the recursive `__json_str(v) -> str` helper (with -`__json_escape` for string quoting). It serializes by value type: `null`/`true`/`false`/ -numbers via the reused **`__convert`** import (no new host import for number formatting), -strings via `__json_escape` (quote + escape), arrays as `[…]` recursing on each element, and -objects as `{"k":…}` recursing on each own value. It matches JS's **hole rules**: an -`undefined`/function **element** in an array serializes as `"null"`, whereas an -`undefined`/function **value** in an object is **omitted** entirely (key and value dropped). -The optional `replacer`/`space` second and third arguments are **unsupported and rejected**. - -**`JSON.parse` — originally deferred, now delivered (see the loose-equality section).** It was first deferred -`JSON.parse`, reasoning that object keys are **interned static-string pointers** fixed at -compile time and `__obj_set` **cannot add a key**. That reasoning was **too strong** and has -since been **reversed** (see the loose-equality section): the fixed-shape constraint only blocks *mutating* an -existing object's key set, not *allocating a fresh* object with a runtime-sized entry table, -and every consumer matches keys by **byte content**. `JSON.parse` is now a fully in-wasm -recursive-descent parser (`emit/json_parse.rs`), no new host import. The paragraph is kept for -history; the current design is described in the loose-equality section. - -**`Map` / `Set` → new heap types over a growable flattened entry array.** `Map` and -`Set` get **two new heap types**, `TYPE_MAP = 6` and `TYPE_SET = 7` (`abi.rs`), whose handle -block is `[type][pad×3][size: u32][entries…]` backed by a **growable flattened `(key, value)` -array** (a Set stores each element as its own key with an ignored value slot). `new Map()` / -`new Set()` (`IrNode::MapNew`) allocate a fresh empty handle. Lookup is a **linear scan** via -strict `===` (the `__js_eq` kernel factored out of `emit_strict_eq`, packaged so `__map_find` -can call it): `__map_find` returns the address of the first key-matching slot or `-1`. The -methods — `set` / `get` / `has` / `delete` and `.size` for `Map`, `add` / `has` / `delete` -and `.size` for `Set` — **dispatch at runtime on the receiver's TYPE byte**, so a same-named -method on a plain object or class instance still routes to the **user method call** -(`MethodCallUser`) fallback rather than the collection op. `set` on an existing key **updates -in place** (size unchanged); `add` de-dupes by `===`. `typeof (new Map()) === "object"`, and -`run.rs` decodes only the `size` word (the backing entries are not walked) to render -`Map(N)` / `Set(N)`. - -**Out of scope for the collections wave (rejected with spans / omitted):** **computed property keys** -(`{[k]: v}`), **`WeakMap` / `WeakSet`**, **`Object.assign` / `Object.freeze`**, the -**Map/Set iteration protocols** (`forEach` / `entries` / `keys` / `values` beyond the listed -methods), and **Symbol keys**. `Map`/`Set` **constructor arguments** (`new Map([[k, v]])`) -are also rejected — collections start empty and are populated via `set`/`add`. **`JSON.parse`** -was originally deferred here pending dynamic key-growth, but has since **shipped** in the loose-equality section (an -in-wasm parser that builds objects with byte-compared keys — no fixed-shape scan needed). - -**Caveat (no GC):** the reflection helpers (`Object.keys/values/entries`), `JSON.stringify`'s -string builder, and every `Map`/`Set` grow allocate on the **bump heap**, which is never -reclaimed (§10) — fine for the short programs jsc targets, but a grow-heavy `Map` loop -accumulates abandoned backing arrays. - -### Unified scoping: functions & methods reference enclosing bindings & class names - -Before this campaign, the lexical-capture rules and the class-name binding were -**scoped to arrow/closure bodies**: an arrow could close over an enclosing `let`/`const` and a -top-level `class C` could be `new C()`'d only at its declaration scope. A **`function` -declaration** or a **class method** body, by contrast, could neither read an enclosing -`let`/`const` (the closures wave analyzed only arrow/function-*expression* bodies as nested frames) nor -resolve the class name from inside a constructor/method (`new C()` inside a method failed with -`unsupported constructor: C`). This campaign closes both gaps so **all** function forms — -arrows, function expressions, function declarations, and methods — share **one** scoping rule: -a body may reference any enclosing binding, whether an outer `let`/`const` or the class's own -name, matching JS lexical semantics. Two milestones, both purely codegen (no IR, ABI, or -run.rs change — the closures wave boxed-cell / closure-env machinery is *reused*, not extended): - -**Gap 1 — the class name resolves inside its own constructor and method bodies (`new C()` -everywhere).** The class name is **bound and its instance shape recorded before** the -constructor and method bodies are lowered, so a `new C()` referencing the class from inside any -of them resolves to the binding instead of failing. Capture analysis binds the name **first** -and analyzes each method body as a **nested frame** (mirroring the constructor), so a body that -references the name marks it **captured across the function boundary**. A captured class name -routes through the **same boxed cell** an arrow uses (per the closure-capture rules): the cell is allocated **empty -first**, the ctor/method closures capture that live cell pointer, and it is **filled with the -fully-built class value afterwards** — so a `new C()` running later reads back the completed -class. The builtin `new TypeError(..)`/`new Error(..)` path inside a method body is unchanged -(it never routes through the user-class binding). Verified end to end: a method that `new C()`s -a fresh instance, a method that recurses through `new` of its own class (`fact()`), and a -factory method building a linked-node chain. - -**Gap 2 — `function` declarations and methods capture enclosing `let`/`const` (read + write + -nested).** the closures wave's capture analysis is extended to **top-level function declarations**: each -declaration body is analyzed **nested under the main frame**, so a reference reaching an -enclosing `let`/`const` marks **both** the binding captured **and** the declaration as -*capturing*. Pass 1 then **partitions** the hoisted declarations: a **non-capturing** one keeps -the **direct-call fast path unchanged** (hoisted, called by name with `env = undefined` — zero -overhead, the common case), while a **capturing** one is **demoted to a closure** (a boxed cell -in `main`, lowered at its source position via the existing `lower_nested_fn_decl` path, and -called indirectly with a trailing `env`). Capture is **by reference** through the shared cell, -so a declaration that **writes** an enclosing `let` (`function add(x){ s = s + x }`) mutates the -one cell every holder sees, and a **nested** `function` inside a `function` captures locally the -same way. **Class methods already route through the lifted-function mechanism**, so they capture -enclosing bindings identically — a method reading an outer `let base` needs no extra machinery. -The combined case (a method that **both** `new C()`s its own class *and* closes over an enclosing -`let`) is covered by a dedicated L3 integration test. - -**No-capture fast path preserved.** The whole point of the partition is that the **overwhelming -common case pays nothing**: a `function` declaration that references no enclosing binding stays -a **hoisted, directly-called** top-level function with `env = undefined` — identical wasm to -before this campaign. Only a declaration that *actually* reaches an outer `let`/`const` is -demoted; the full regression stays result-for-result green because non-capturing programs -compile unchanged. - -**Bounded rejections the runtime guard kept (each a precise **span** diagnostic, not a silent miscompile).** -Demotion narrows a capturing declaration's reachability, so three forms are rejected at compile -time rather than mis-lowered: - -1. **Sibling-call of a demoted capturing function.** A demoted closure is bound only within - `main`, so another **top-level function** cannot reach it: a call to / reference of it from a - sibling top-level function is rejected — *"`f` closes over enclosing vars, cannot be - called/referenced from another top-level function"* — instead of emitting an unresolved call. -2. **Use-before-declaration of a captured binding.** A demoted declaration **loses hoisting** - (it is lowered at its source position, not raised to the top), so calling it **before** its - captured binding is declared (`f(); let n=41; function f(){ return n }`) is an error — an - intended narrowing versus JS's hoisting, surfaced with a span rather than reading an - uninitialized cell. -3. **Out-of-scope class forms** (as of this campaign). Class-name capture (Gap 1) did **not** - extend to `extends`/`super`, `static` members/fields/blocks, or getter/setter accessors — at - the time these were the pre-existing spanned the class wave rejections. `extends`/`super`, - `static` members, and **instance** getter/setter **accessors** have since shipped; - what remains rejected is **static** accessors, static **blocks**, **object-literal** accessors, - and **computed** accessor names. - -The net effect: **one** lexical-scoping rule across every function form, the zero-overhead -direct-call path intact for non-capturing declarations, and every construct the campaign does -not yet support failing loudly with a span. - -### String-method surface & the unknown-method guard - -The built-ins wave (e) method surface shipped the everyday non-callback array/string methods but -left the receiver-method lowering **open at the bottom**: a method name it did not recognize -(`"x".zzz()`, `[1,2].flat()`, `let o={}; o.foo()`) fell through to an unresolved -`call_indirect` and surfaced as the wasm-validator failure `failed to compile: wasm...` — a -leaky, locationless miscompile rather than a language diagnostic. This campaign closes that -boundary and, on top of the now-guarded lowering, fills out the string-method set that real -programs (the motivating FizzBuzz below) reach for. Two milestones, both purely codegen + -host-side runtime (no `run.rs` boundary change — every value stays an existing the closures wave/the built-ins wave kind): - -**An unknown method is a clean compile-time diagnostic, never invalid wasm.** The -receiver-method lowering is **guarded**: a call whose method name is not a recognized built-in -for its receiver kind (nor a supported user method call, per the object-method rules) is rejected with the spanned -diagnostic *"method '``' is not supported"* (§9) instead of emitting an unresolved -`call_indirect`. The invariant is total — **no input may produce `failed to compile: wasm...`**; -every out-of-scope method fails loudly at its source span. The guard sits **below** the existing -built-in and user-method dispatch, so it only fires for genuinely unrecognized names and changes -no valid program's behavior. - -**The static-shape unknown-method extension.** The diagnostic -closed the *invalid-wasm* case (a primitive/array-literal receiver, or a program with no funcref -table). It left a *wrong-value* gap: a program that DOES define functions could call an unknown -method on an object that itself carries methods — `let o = { f() { return 1 } }; o.bar()` — and -the generic path read `o.bar` → `undefined`, then used its bits as a funcref-table index that -happened to hit a real function, returning a **wrong value** (valid wasm, so the diagnostic gate passed). -The static check closes this with a **static-shape** guard. A pre-pass ([`analyze_captures`], reusing its total -traversal) records, for every `let`/`const` binding initialized directly with an -all-static-key **object literal**, that binding's own-key set — keyed by [`binding_id`] and kept -only when the binding's name is **never reassigned** anywhere (a name-based over-approximation, so -the recorded shape is provably fixed; the object model has **no dynamic key growth** (the object model), so a -key absent from the literal is absent forever). At a user-method call whose receiver is a direct -object literal or such a binding, a statically-known method name that is not among the shape's keys -is rejected with the same *"method '``' is not supported"* span. The guard is **sound by -construction** — it never fires on a receiver whose shape is not statically fixed (a reassigned -binding, a `new`-constructed instance, a computed/dynamic key, a call result), so runtime dispatch -is unchanged for every valid program. - -**The dynamic-shape unknown-method runtime guard.** -The static check's soundness left exactly those unprovable receivers — a `new`-instance, a reassigned binding, a -computed key, a call result — to **runtime dispatch**, where a genuinely absent method still read -`undefined` and dispatched its bits as a funcref-table index: a **wrong value**, or, when the index -was out of range or its callee's type mismatched, a **cryptic wasm trap** with no source context. -The runtime guard closes this last gap with a **runtime** guard emitted in [`emit_method_call_user`]: immediately -after the member read resolves the callee jsval, and **before** the `call_indirect`, codegen checks -the value is a heap **function** (the [`emit_is_function`] tag test — a pointer whose block TYPE -byte is `TYPE_FUNCTION`). When it is not, the call raises a clean exception — the fixed message -*"TypeError: called value is not a function"* stashed into `$exc_value`, `$exc_pending` set — and -unwinds through the ordinary the exceptions wave machinery, so it is **catchable** by an enclosing `try`/`catch` -and otherwise surfaces as `Uncaught TypeError: called value is not a function` (a non-zero exit), -never a trap. The message blob is interned by [`collect_node`] at every method-call site (the -`MethodCallUser` path and the unknown-collection-method `MapMethod` fallback). A valid method call — -the callee IS a function — takes the same path as before at the cost of one tag test, so no valid -program's behaviour changes. Together the invalid-wasm fix, the static-shape check, and the runtime guard -(dynamic-shape wrong-value/trap) make **every** unknown-method call fail loudly: a compile-time span -where the absence is provable, a runtime `TypeError` where it is not. - -**The string-method set completed.** The built-ins wave (e) non-callback method family gained the -remaining everyday string methods, recognized on a string receiver and lowered to the **same -`env.__method` host import** (dispatched host-side on the receiver's TYPE byte, per the runtime method-dispatch rules) — each -producing a **new** heap string (or, for `split`, a new array of new strings), never mutating the -receiver: - -- `trim()` / `trimStart()` / `trimEnd()` — strip ASCII whitespace from both ends / the start / - the end. -- `repeat(n)` — the receiver concatenated `n` times (`n = 0` => `""`). -- `startsWith(sub)` / `endsWith(sub)` — a boolean prefix / suffix test (the argument is - string-coerced). -- `padStart(len, pad)` / `padEnd(len, pad)` — pad to BYTE length `len` with `pad` (default a - space), truncated to fit; a no-op once already `len` bytes. -- `substring(a, b)` — the `[a, b)` substring by BYTE index (JS clamp semantics). -- `split(sep)` — a new array of substrings; an empty `sep` splits into one element per byte. -- `includes(sub)` — substring membership on a string receiver (the array form already shipped in - the built-ins wave (e)). - -Milestones: `" hi ".trim()` => `"hi"`; `"ab".repeat(3)` => `"ababab"`; -`"hello".startsWith("he")` => `true`; `" Hello ".trim().toUpperCase()` => `"HELLO"`. Method -**arity mismatches** on the new names are compile errors, matching the existing built-in arity -style. The whitelist sits **above** the diagnostic guard, so these names now lower to their host helper -while every still-unknown name (`"x".zzz()`, `[1,2].flat()`) keeps failing with the clean -diagnostic. - -**FizzBuzz-with-trim capstone.** The wave's motivating program — a classic `for`-loop FizzBuzz -that builds each line, pads it, and `.trim()`s the padding off before `console.log` — now -compiles and runs, printing the canonical `1 / 2 / Fizz / 4 / Buzz / … / FizzBuzz` lines. It -composes the loop, `%`, `===`, string concatenation, `String(i)`, `.trim()`, and the print -channel end to end, and is pinned by an integration smoke test. - -**Method-resolution rules — the consolidated decision procedure.** The three fixes above each closed one failure mode of the -same lowering boundary; this is the single ordered rule set that call-expression lowering now -follows for a member call `recv.name(args)` (a computed `recv[expr](args)` is treated as a -dynamic key throughout). The steps are tried **top to bottom**; the first that matches wins, so a -guard only fires once every dispatch above it has declined: - -1. **Recognized built-in method.** If `name` is a built-in for the receiver's *static kind* — a - string method (`trim`, `toUpperCase`, `startsWith`, … including the added string methods), an array - method (`push`, `map`, `filter`, `includes`, …), or a supported collection/number/`Math` member — the call lowers to its intrinsic or the shared `env.__method` host import (dispatched - host-side on the receiver's TYPE byte, per the runtime method-dispatch rules). **Arity is checked at compile time**; a wrong - argument count is a spanned diagnostic, matching the built-in style. - -2. **User-defined method.** Otherwise, if the call is a user method — an object-literal - method, a class method, or a `this.m()` inside a method body — it lowers to the user-method path - (`MethodCallUser` → funcref-table `call_indirect`, env-last ABI). - -3. **Provable-absence rejections at compile time.** A user-method call whose receiver's shape is - *statically knowable* and provably lacks `name` never reaches emit: - - **Invalid-wasm fix** — the receiver is a statically-primitive / array-literal value, or the whole program - defines no functions (no funcref table). The generic path could only emit an unresolved - `call_indirect`, so the call is rejected up front. - - **Static-shape guard** — the receiver is a direct object literal, or a `let`/`const` binding whose initializer - is an all-static-key object literal and whose name is **never reassigned** (recorded by the - [`analyze_captures`] pre-pass, keyed by [`binding_id`]); the object model has no dynamic key - growth, so a key absent from the literal is absent forever. A statically-known `name` - outside that fixed key set is rejected. - - Both raise the identical **compile-time span diagnostic** *"method '``' is not supported"* - (§9), pointing at the offending call. - -4. **Unprovable-absence guard at runtime.** When the receiver's shape is **not** statically fixed — - a `new`-constructed class instance (`new C().bar()`), a function-parameter receiver, an array - element, a reassigned binding, a computed/dynamic key, a dynamic `this.m()`, or a call result — - absence cannot be proven at compile time, so dispatch proceeds but is guarded. [`emit_method_call_user`] - resolves the callee jsval from the member read and, **before** the `call_indirect`, tests it is a - heap **function** ([`emit_is_function`] — a pointer whose block TYPE byte is `TYPE_FUNCTION`). If it - is not, the call raises the fixed **runtime** exception *"TypeError: called value is not a - function"* through the ordinary the exceptions wave unwinding path (**catchable** by an enclosing `try`/`catch`, - otherwise `Uncaught TypeError: called value is not a function` with a non-zero exit) — never a - wrong value and never a cryptic wasm trap. - -**Residual bound (closed by the Error-identity work — see [the Error-object identity section](#error-object-identity-instanceof--name--stack)).** -The runtime guard's thrown value was originally a **plain heap string** stashed straight into -`$exc_value`, because jsc had **no `Error`/`TypeError` object identity** at the member-call boundary: -a `catch (e)` bound `e` to that string, so `e instanceof TypeError` was not modelled. This later work -upgraded the guard to throw a **real branded `TypeError` object** (`emit_method_call_user` now emits -[`internal_type_error`] into `$exc_value`), so `catch (e) { e instanceof TypeError }` holds and -`e.name`/`e.message`/`e.stack` read back; the uncaught render is unchanged -(`Uncaught TypeError: called value is not a function`). The message is still **generic** — the fixed -`"called value is not a function"`, with **no receiver-expression text** (it does not name the -offending method or receiver) — an acceptable residual for the totality guarantee. - -**Invariants.** (a) *Totality* — no input can reach the wasm validator as `failed to compile: -wasm...`; every unknown method fails loudly, at its source span where absence is provable and as a -catchable runtime `TypeError` where it is not. (b) *Zero cost to valid programs* — a recognized -built-in or a genuine user method takes exactly the path it took before; the only added work is the -single tag test in step 4, on the dynamic-receiver path alone. (c) *Soundness* — steps 3–4 never -fire on a call that a correct JS engine would dispatch, because the static guard's shapes are provably -fixed and the runtime guard only rejects a callee that is *not a function* at runtime. - -### Error-object identity: `instanceof` / `.name` / `.stack` - -The exceptions wave gave `new Error(msg)` / `new TypeError(msg)` / `new RangeError(msg)` a heap -representation — a plain `{ name, message }` object (type byte `TYPE_OBJECT`) — -but **no identity**: the three kinds were structurally indistinguishable, there -was no `.stack`, and `instanceof` was unimplemented (it fell through -`map_binary_op` to *"unsupported operator"*). This later work closes that gap -while keeping errors ordinary byte-`2` objects — no new heap TYPE byte. - -**(a) The identity marker `@@errorkind`.** `lower_new` now brands each error -object with a **hidden reserved property** `@@errorkind` (`abi::ERROR_KIND_KEY`) -whose value is a number jsval encoding the kind — `Error` = `0`, `TypeError` = -`1`, `RangeError` = `2` (`abi::ERROR_KIND_{ERROR,TYPE,RANGE}`). Like -`@@prototype`/`@@constructor`, the `@@` prefix makes it **invisible** to -`for-in` / `Object.keys` / `JSON.stringify` (the `emit_key_not_reserved` guard -skips any key whose first byte is `@`), yet `__obj_get` reads it back. The value -is never `undefined`, so "the object is an error" is testable as "the loaded -marker `!== undefined`", and the exact kind by strict-equality against the -constants. A plain object that merely *looks* like an error -(`{ name: "Error", message: "x" }`) lacks the marker, so it is **not** -`instanceof Error` — matching JS. - -**(b) `.stack`.** The error object gains a regular `stack` string property: -`": "`, or just `""` when the constructor took no argument -(jsc keeps **no runtime call frames**, so there is no ` at …` section — a -documented single-line approximation). The message sub-expression is **teed into -a fresh temp** so it is evaluated exactly once: `new Error(f())` runs `f` a -single time even though both `message` and `stack` reference it. - -**(c) `instanceof` (user classes + builtin errors).** `x instanceof RHS` lowers -to `IrNode::InstanceOf { value, kind: InstanceOfKind }` (recognized beside the -`in` operator in `lower_expr`). The RHS must be a **bare identifier**, dispatched -by name at lowering: - -- a **user class** (matched first, so `class Error {}` *shadows* the builtin) → - `kind = Class(C["@@prototype"])`; the test compares the receiver's own - `@@prototype` slot to the class's shared prototype by **pointer identity** — a - single hop, exact under the current no-`extends` model, so `new C() instanceof - C` holds and `new C() instanceof D` does not; -- an **unshadowed** `Error` / `TypeError` / `RangeError` → the hidden-brand check - (`Error` matches any present `@@errorkind`; `TypeError`/`RangeError` match their - exact kind), so a `TypeError` is `instanceof TypeError` **and** `instanceof - Error`, but a base `Error` is neither `instanceof TypeError` nor `RangeError`; -- anything else (a non-identifier, `Object`/`Array`, or a `let`-shadowed name) → - a **clean span diagnostic**. jsc models no general prototype-chain `instanceof`. - -Emission (`emit_instanceof_node`) guards on `emit_is_object` **first** — only a -heap object (type byte `TYPE_OBJECT`) can be an instance — so a non-heap or -non-object operand (`5`, `null`, an array, a `Map`) yields `false` **without -trapping** and without a false positive. (This deliberately uses an -object-type-byte guard rather than a `typeof (recv) === "object"` prefix: on the -current runtime a `typeof` result is a boxed *immediate*, and `===` only -byte-compares two heap *strings*, so `typeof x === "object"` is `false` for every -`x` — it cannot gate the test.) Both prototypes (user-class case) and the -`@@errorkind` marker (error case) are read via `__obj_get`, whose own-key scan -finds the receiver's own slot before any prototype walk. The needed keys are -interned by `collect_node` even when the program constructs no error/instance in -that position. - -**(d) Error-aware rendering.** `format_value` special-cases a decoded object -carrying `@@errorkind`: it renders as its single-line stack `": "` -(or `""` with no message), the way a JS console prints an error's first -line, instead of the generic `{ name: …, message: … }`. This changes an uncaught -error to `Uncaught Error: boom` and `console.log(new Error("x"))` to `Error: x`. - -**(e) The internal runtime-guard `TypeError` becomes a real error.** The dynamic-receiver -*"called value is not a function"* guard ([the string-method / unknown-method-guard section](#string-method-surface--the-unknown-method-guard)) -now throws a **branded `TypeError` object** (`internal_type_error`) instead of a -bare string, so `catch (e) { e instanceof TypeError }` holds and -`e.name`/`e.message`/`e.stack` read back. Its uncaught render is unchanged -(`Uncaught TypeError: called value is not a function`). - -```js -new Error("boom").stack => Error: boom -new Error().stack => Error -new TypeError("t") instanceof Error => true -new Error("e") instanceof TypeError => false -5 instanceof Error => false // no trap -new Map() instanceof Error => false // Map is not a byte-2 object -({ name: "Error", message: "x" }) instanceof Error => false // no marker -class C {} new C() instanceof C => true // prototype hop -class C {} new C() instanceof Error => false -class Error {} new Error() instanceof Error => true // user class shadows builtin -Object.keys(new Error("x")).join(",") => name,message,stack -try { throw new TypeError("nope") } catch (e) { e.name } => TypeError -``` +## 12. Building a static release -**Known limitations (documented, acceptable for a learning compiler).** `.stack` -is single-line (no frames). `instanceof` covers user classes (a `@@prototype` -hop — generalized to a full chain walk for `extends` in [the typeof / extends section](#typeof-string-equality--class-extends-super-multi-hop-instanceof)), -the three builtin error constructors, and the runtime-type-byte builtins -`Array`/`Object`/`Function`/`Date` (added by the instanceof-builtins wave). -`.name`/`.message`/`.stack` remain **enumerable** -(real Error -properties are non-enumerable), so `Object.keys(err)` returns -`["name","message","stack"]` and `JSON.stringify(err)` includes them — a -consequence of the fixed-shape object model having no per-key enumerable flag. -`.stack` uses the `: ` prefix whenever a constructor argument was given, so -a runtime-empty message still renders `": "` (only the syntactic no-arg -form drops the colon). - -### `typeof` string equality + class `extends` (`super`, multi-hop `instanceof`) - -Two follow-ons this work exposed: the `typeof` guard idiom did not work, and -`instanceof` (plus the whole class model) was single-level. - -**(a) `typeof` now yields a real interned heap string (the immediate is retired).** -Previously a `typeof` result was a NaN-boxed **immediate** (tag `0b100`, low bits = -index into `TYPEOF_STRINGS`), a *second* string representation distinct from heap -strings (tag `0b101`). The strict-equality kernel only byte-compares two heap -strings and otherwise falls to a raw-bits identity compare, so `typeof x === "…"` -was **always `false`** — and every other string context (`+`, `.length`, …) would -have needed the same special-casing. Rather than teach each consumer about the -immediate, jsc **retires the immediate at the producer**: `emit_typeof_node` keeps -its `0..5` type-index computation but maps that index to the **interned -data-segment offset** of the matching word (a branchless nested-`select` fold) and -ORs `HEAP_BITS`, so a `typeof` result **is** an ordinary heap string. Interning -gives the same no-allocation property the immediate had. Consequences: - -- `===`/`!==`, `+` concatenation, `.length`, indexing, and `console.log` all flow - through the **existing string machinery with zero `typeof` special cases** — the - entire leaky second representation (and its bug class) is gone. -- `collect_node` interns the six words whenever a program uses `typeof`. -- `Value::TypeStr` and the `0b100` decode arm are **deleted** — a `typeof` result - decodes as a normal `Value::Str`; `format_value` / `str_to_number` lose their - `TypeStr` arms; the truthiness path loses its dead non-heap `typeof` case (a - `typeof` word is a non-empty heap string, hence truthy via the string path). -- NaN-box tag `0b100` is **retired** in `abi.rs` (like `0b110`/`0b111`). - `abi::TYPEOF_STRINGS` remains the single source of the words shared by the - emitter and the runtime. - -**(b) `class B extends A`.** A derived class is parsed from a **named** superclass -declared earlier (other superclass expressions and an unknown name are clean span -diagnostics). The runtime model reuses the existing single-shared-prototype shape: - -- **Inherited methods (free, via the prototype chain).** `B`'s shared prototype - object gains a reserved `@@prototype` link to `A`'s prototype. `__obj_get`'s - existing **prototype-fallback recursion** then walks the whole chain: `b.m` misses - `B`'s own instance slots, follows the instance's `@@prototype` to `B`'s prototype, - misses there, follows *its* `@@prototype` to `A`'s prototype, and resolves — so - inherited and overridden methods just work with no new emit path. -- **Inherited fields.** `ClassInfo.fields` for a derived class is the parent's - **flattened** field list followed by its own, so `new B()` pre-slots every field - the whole constructor chain assigns (the object model has no dynamic key growth). -- **`super(args)`** lowers to `IrNode::ThisCall { callee: A["@@constructor"], args, - this_local }` — a call that keeps the **current** `this` (the enclosing method's - snapshot) rather than a fresh receiver, so the parent constructor's `this.f = …` - mutates the same instance. **`super.m(args)`** is the same node with - `callee = A["@@prototype"].m` (resolved up the parent's own chain). A subclass - with **no constructor** reuses the parent's constructor value directly (an implicit - forwarding `constructor(...a){ super(...a) }`); a subclass that declares fields but - no constructor is rejected (the alias would skip its field init). `super` outside a - derived class body, and a `super.m` that resolves to a non-function, are handled - (a compile diagnostic and a branded runtime `TypeError` respectively). The - superclass binding is force-captured into a cell so the lifted body can read it. -- **Multi-hop `instanceof`.** `InstanceOfKind::Class` now **walks** the receiver's - `@@prototype` chain (a small `block`/`loop` over two dedicated scratch slots), - matching the class's prototype at any depth: `new C() instanceof A` holds for - `C extends B extends A`. The `emit_is_object` guard and the one-hop base case are - unchanged, so non-object operands still yield `false` without trapping. - -```js -typeof 1 === "number" => true -typeof x === "string" ? "s" : "o" // (x a string) => s -"boolean" === typeof true => true -class A{constructor(n){this.n=n} dbl(){return this.n*2}} -class B extends A{} new B(5).dbl() => 10 // inherited ctor + method -class A{greet(){return "A"}} -class B extends A{greet(){return super.greet()+"B"}} -new B().greet() => AB // override + super.m() -class A{} class B extends A{} class C extends B{} -new C() instanceof A => true // multi-hop -``` +```bash +# One-time: add the musl target. +rustup target add x86_64-unknown-linux-musl -**Known limitations (documented).** The default forwarding constructor covers a -subclass with **no own fields**; a field-declaring subclass must write an explicit -constructor that calls `super()`. `super`/`instanceof` are lexical to the class -being lowered (no dynamic `[[HomeObject]]`). Field-initializer ordering relative -to `super()` is not modelled (jsc pre-allocates the instance, so `this` is always -available). (The builtin `instanceof Array`/`Object`/`Function`/`Date` are added -by the instanceof-builtins wave — see [the instanceof-builtins / Date section](#instanceof-built-ins--date).) - -### Promises, microtasks & top-level `await` - -The async wave opens with a **host-driven promise engine**. A promise's -*settlement* lives in the guest heap as a new block type -`TYPE_PROMISE` (`[type=8][pad×3][state:i32][value:i64]`, `state` ∈ -pending/fulfilled/rejected) so a bare memory snapshot still renders it -(`Promise { 42 }`, `Promise { }`, `Promise { reason }`); the -*scheduling* — each pending promise's reaction list and the FIFO **microtask -queue** — lives host-side in the `wasmtime` store data (`run::promise`), keyed by -the promise block's base offset. This mirrors the built-ins wave philosophy of reimplementing -complex operations host-side (`__convert`/`__method`), where dynamic data -structures are trivial and panic-free in Rust rather than hand-emitted wasm. - -**Surface.** `Promise.resolve(v)` / `Promise.reject(v)` (recognized syntactically -like `Object`/`JSON`, only when unshadowed); `new Promise(executor)` (see below); -`p.then(onF, onR)` / `p.catch(onR)` (one chained result promise, passthrough for a -missing handler, rejection skips a fulfillment-only `.then`); `p.finally(fn)` (run -on both settlements, original settlement passed through unless `fn` throws); and -top-level **`await p`**. -`.then`/`.catch`/`.finally` are claimed for any receiver — a user object method of -those names is a documented collision — and the engine treats a non-promise -receiver as an already-fulfilled value (so `await 5` → `5`). - -**Two new host imports + a trampoline.** `__promise(op, a, b)` backs -resolve/reject/await/finally; `__then(p, onF, onR)` backs then/catch -(`IMPORT_FUNC_COUNT` 3 → 5). Reaction callbacks are guest closures, so the engine -runs them by **re-entering the guest** through an exported `__invoke1(fn, arg)` -trampoline (a hand-emitted `call_indirect` under the uniform env-last ABI). A -callback that throws sets `$exc_pending` (the exceptions wave mechanism); `__invoke1` returns -normally and the engine reads + clears the flag to reject the chained promise. - -**`new Promise(executor)` — native settler functions.** The constructor allocates -a pending promise `P`, then hands the executor `resolve`/`reject` as first-class -FUNCTION values pointing at two hand-emitted **settler** functions (appended to the -funcref table after the user functions) with `P` stored directly in their **env -slot** (not a closure environment). Calling `resolve(v)` therefore reaches -`__settle_resolve`, which reads `P` from its env and drives the host settle logic -(`__promise(SETTLE_FULFILL, P, v)`) — no bound-native-function value kind is -needed, just a repurposed env slot. The executor runs synchronously; if it throws, -the emitted constructor catches `$exc_pending` and rejects `P` (a throwing executor -rejects rather than propagating). `resolve`/`reject` may be stored and called later -from a microtask, which the `await`/after-`main` drain naturally reaches. (A -consequence: capture analysis must descend into an awaited expression so a closure -nested under `await new Promise(...)` still has its captures boxed.) - -**No suspension needed.** There is no state-machine/CPS transform: `async function` -bodies stay rejected, so an `await` only ever appears at the **top level** (in -`main`), where "block until settled" is just "drive the microtask queue until the -awaited promise settles" — a synchronous host loop. Because the runtime has no -timers or I/O, the microtask is the *only* async primitive, so draining the queue -fully models the event loop: top-level `await` drains until its target settles -(surfacing a rejection through `$exc_pending`/`$exc_value` as an uncaught throw), -and the harness drains any remaining queue **after `main`** so unawaited `.then` -chains still run. Resolving a promise with another promise **adopts** it (chains -via a passthrough reaction). - -**Known limitations (documented, deferred).** -`Promise.all`/`race`/`allSettled`, `async function` bodies (`await` inside a -function requires real suspension), thenable assimilation of non-promise objects, -and unhandled-rejection reporting are all out of scope here. - -### Generators (state-machine transform) - -A `function*` cannot use porffor's native fiber stack — jsc emits ONE stackless -wasm module — so `yield` is compiled by a **source→state-machine** transform. -Each generator declaration lowers to TWO ordinary uniform-ABI functions: - -- a **constructor** (the name `g` binds to) that allocates a `TYPE_GENERATOR` - handle `[type=9][sm_idx][state][done][env]` plus its **frame** — a `TYPE_ENV` - block of boxed cells, one per generator local — and returns the handle WITHOUT - running the body; and -- a **state machine** `SM(sent, gen, …, env)` whose body is a `br_table` dispatch - on `gen.state` (read from the handle) into one **segment** per suspend point. - -The frame reuses the closure-cell machinery: the body is lowered in an -`env_backed` frame so every local resolves to `EnvLoad + cell` (a `CellGet`/ -`CellSet`), which is exactly what lets a local — and `++`/`+=`/`push` on it — -survive a `yield` (a `call_indirect` return). `gen.next(v)` (`IrNode::GenResume`) -drives the SM through `call_indirect`, passing the frame as the trailing `env` -argument, and builds the `{ value, done }` result as an ordinary object; a -non-generator receiver throws a branded `TypeError`. - -**Straight-line bodies.** `yield` as a whole statement (`yield e;`), an -assignment RHS (`x = yield e`), or a returned value (`return yield e`); each -`yield` ends a segment (store next state, return the value); `return` / fall-off -sets `done = 1`. - -**Control flow crossing a yield (CFG flattening).** The lowered body is built -into a **control-flow graph** of basic blocks: a straight-line run is one block, -and `if`/`while`/`for` (which desugar to `If`/`While`) crossing a `yield` are -flattened into blocks linked by `Jump` / `Branch` / `Yield` terminators, with -`break`/`continue`/`return` becoming jumps. The state machine is a -`loop { br_table (gen.state) }`: a `Yield`/`Return` block returns; a -`Jump`/`Branch` block stores the next block index and re-dispatches. A -self-contained inner control-flow region with no suspend and no escaping jump -keeps its ordinary wasm lowering (emitted verbatim inside a block). - -**Consuming a generator.** `yield* inner` (delegation) desugars to a driving -`while` over `inner.next()`. `for (const x of gen)` drives the iterator protocol: -the iterable is evaluated once and a runtime `IsGenerator` test picks between -driving it via `.next()` (`{ value, done }`) and the ordinary array/string index -walk — the array path is unchanged. `[...gen]` / `f(...gen)` SPREAD drains a -generator into an array through the `__gen_to_array` guest helper (a -`call_indirect` drive loop collecting each yielded value; the generator's `return` -value is excluded), selected by the same runtime `IsGenerator` test so array -spread is untouched. A `for-of`/`for-in` whose OWN body `yield`s inside a generator -is rejected (its hidden index/length temporaries do not persist across a suspend; -use an indexed `for` over `.length`). - -**Still rejected (deferred).** A `yield` inside `try`/`catch`/`finally` (handler -state across a suspend), inside a `switch`, or in a loop/`if` CONDITION; a `yield` -nested in a larger expression; a generator EXPRESSION / arrow / method; a nested -closure inside a generator body; and non-simple parameters. `.return()`/`.throw()` -are also deferred. - -### `async`/`await` (a generator driven by a promise) - -An `async function` is compiled as a **generator whose `await` is a `yield`**, -driven by a host-side promise engine — reusing the generator state-machine transform and -the microtask queue wholesale, so almost no new machinery is added. A top-level -`async function f(params) { … }` lowers to exactly the generator pair — a state machine -(the body, with `await e` lowered to `yield e`) plus a frame — but the name `f` -binds to a **wrapper** whose whole body is `return AsyncSpawn(MakeGenerator{…})`: -it builds the generator handle without running the body and hands it to the host, -which returns the async function's result **promise**. (`await e` becomes a -generator `yield e` under an `async_body` flag set while lowering the body; the CFG -segmentation then treats it identically to a `yield`, so all of the control-flow -flattening — `await` inside `if`/`while`/`for`, `break`/`continue`/`return` across -a suspend — works for free.) - -**The host async driver.** `AsyncSpawn` lowers to the `__promise` import with a -new `PROMISE_OP_ASYNC_SPAWN` op; the operand is the fresh generator handle. The -host (`run::promise`) allocates the result promise `P`, then **drives** the -generator: `__async_step(gen, sent)` — a new exported trampoline, the single-step -sibling of the generator `__gen_to_array` drain — `call_indirect`s the state machine with -`(sent, gen)` and returns the yielded/returned value, leaving the handle's `done` -flag and any thrown exception (`__exc_pending`) for the host to read. Each step: - -- **threw** → reject `P` with the thrown value (a synchronous `throw`, or an - awaited rejection surfaced below); -- **done** → fulfill `P` with the return value (a returned promise is **adopted**, - matching spec); -- **suspended** (a `yield`/`await`) → the yielded value is the awaited operand; - register an **async continuation** on it (via `Promise.resolve` semantics — - `register_source` with `async_gen = Some(gen)`), so when it settles the engine - resumes the generator with the settled value (`drive_async` again) on fulfillment - or rejects `P` on rejection. - -The continuation rides the SAME microtask queue and reaction lists as `.then` — a -`Job`/`Reaction` simply carries an optional `async_gen` handle, and `run_job` -routes it to `drive_async` instead of a guest callback. So the first step runs -synchronously inside the `f()` call (re-entering the guest exactly as a `.then` -reaction does), and each `await` yields to the queue, which top-level `await` and -the after-`main` drain turn as usual. A synchronous (no-`await`) body therefore -returns an already-fulfilled promise; a body that awaits returns a pending one. - -**Rejection of an awaited promise propagates straight out** (rejecting `P`) rather -than throwing back into the body: there is no `gen.throw`, and a `try`/`catch` -around an `await` is already rejected by the CFG (a `yield` inside `try`), so the -observable behaviour — an uncaught `await` rejection becomes the function's -rejection — is exactly right for the supported surface. - -**Still rejected (deferred).** An async function EXPRESSION / arrow / method, an -`async function*` (async generator), and — inherited from the generator transform -— an `await` inside `try`/`switch`/a loop-or-`if` CONDITION, an `await` nested in a -larger expression (so `x += await e` is out; use `let t = await e; x += t`), and -non-simple parameters. - -### Loose equality (`==` / `!=`) + `[...str]` string spread - -**Loose equality `==`/`!=`.** `BinOp::LooseEq`/`LooseNe` (mapped from swc's `EqEq`/ -`NotEq`) lower to the `__loose_eq(a, b) -> i32` runtime kernel, boxed to a boolean -jsval (negated for `!=`) with a `select` — the same shape as strict `===`. The -kernel implements JS **Abstract Equality** for jsc's value types: each boolean -operand is first **normalized to a number** (`true`→`1`, `false`→`0`); then if -either operand is `null`/`undefined` the result is `true` iff **both** are (nullish -values equal each other and nothing else); two numbers compare as `f64` (so -`NaN == NaN` is false); two strings by byte content (`__str_eq`); a number and a -string coerce the **string** to a number via the existing `__convert` `ToNumber` -host import and compare as `f64` (`1 == "1"`, `"" == 0` → true); anything else -compares by **raw identity bits** (two distinct objects differ, the same object is -equal). **Documented v1 cut:** object↔primitive `ToPrimitive` coercion is NOT -modeled, so `[] == 0` and `[1] == "1"` are `false` (not `true` as in spec) — a rare -footgun; the common `x == null`, `1 == "1"`, `true == 1` cases are exact. Strict -`===` and every other internal equality (optional-chaining nullish, destructuring -defaults, `switch`/`case`, Map/Set keys) stay strict and are untouched. - -**`[...str]` string spread.** Spreading a **string** into an array literal now -explodes it into its characters. A pre-existing bug had `[...str]` fall through to -`__array_concat`, which assumed an array source and reinterpreted the string's -UTF-8 bytes as boxed jsvals (garbage floats). The `SpreadElem::Spread` emitter now -tests the source type at runtime: a STRING goes through the new -`__str_to_array(s) -> arr` helper (allocate a `TYPE_ARRAY` of `len` slots, fill -element `k` with `__str_index(s, k)` — a one-byte heap string), a GENERATOR through -`__gen_to_array`, and any other source (an array) straight through, all -funnelling into `__array_concat`. The character walk is **byte-indexed** (matching -for-of / indexing over a string), so multi-byte UTF-8 splits per byte — the same -limitation the rest of the string surface carries. `[..."abc"]` → `["a","b","c"]`. - -**`JSON.parse` — un-deferring the earlier backlog item.** It had rejected `JSON.parse` -as needing dynamic object key-growth the fixed-shape object model lacks. That -reasoning was **too strong**: the fixed-shape constraint only blocks *mutating* an -existing object's key set (`__obj_set` writes existing slots only) — it never -blocked *allocating a fresh* object with a **runtime-determined entry table**. And -every object consumer (`__obj_get`, `__obj_has`, `for-in`, `Object.keys`) matches -keys by **byte content** (`__str_eq`), so a parsed object whose keys are runtime -heap strings resolves correctly against a literal access like `parsed.name` (which -interns `"name"` and compares by content). So `JSON.parse` is now a fully **in-wasm -recursive-descent parser** (`crates/jsc-core/src/emit/json_parse.rs`), consistent -with the in-wasm `JSON.stringify` — **no new host import**, so the browser -shim debt does not widen. - -- **Cursor.** Three module globals set once at `__json_parse` entry — - `$__json_data` (the input's UTF-8 payload offset), `$__json_pos`, `$__json_len` — - drive the recursion. A parse runs synchronously to completion, so nothing - re-enters and disturbs them. -- **Grammar.** `__json_value` skips whitespace then dispatches on the current byte: - `{`→`__json_object`, `[`→`__json_array`, `"`→`__json_string`, `t`/`f`/`n`→a - verified `true`/`false`/`null` literal, else `__json_number`. Numbers scan the - token substring and reuse the existing `__convert` `ToNumber` import (as - `JSON.stringify` reuses `ToString`). Strings decode into a fresh heap string, - handling `\" \\ \/ \b \f \n \r \t`. -- **Runtime-key objects.** `__json_object` accumulates each `(key, value)` as two - elements of a growable array via `__array_push`, then `__array_to_object` - materializes a fresh `TYPE_OBJECT` block sized to the member count, copying the - pairs into its entry table. This is the key primitive the earlier deferral said was - missing — it just never required the *mutable* key-growth the deferral assumed. -- **The argument** is coerced to a string via the `__convert` `ToString` import - (matching spec `ToString`), so `JSON.parse(42)` parses `"42"`. -- **Errors.** Malformed input calls `__json_throw`, which stashes a **branded - `Error`** (`.name` `SyntaxError` — jsc has no `SyntaxError` kind, so it is branded - base `Error`, and `e instanceof Error` holds) in `$exc_value` and unwinds via the - the exceptions wave mechanism; each helper checks `$exc_pending` after a recursive call and - propagates, and the emit site issues the final `emit_propagate_if_pending` so a - `try`/`catch` around `JSON.parse` catches it. Covers an unexpected/absent token, - a mismatched literal, an unterminated string/array/object, and **trailing - content** after the value. -- **v1 cuts (documented).** String `\u` escapes are unsupported (they throw); the - **reviver** argument is rejected at codegen; each parsed string over-allocates its - worst-case decoded length on the bump heap (never reclaimed, §10); a JSON key - literally beginning with `@` would be hidden from enumeration by the reserved-key - guard (accessible only by explicit access). - -### `instanceof` built-ins + `Date` - -**Runtime-type-byte `instanceof`.** `instanceof` now recognizes four more -right-hand sides beyond user classes and the branded error constructors: -`Array`, `Function`, `Date`, and `Object`. Like the error names, they are matched -syntactically from an **unshadowed** bare identifier (a user `let Array = ..` -shadows the builtin and falls through to the clean compile error). Instead of a -`@@prototype` hop, they test the receiver's runtime **TYPE BYTE** -(`InstanceOfKind::Array`/`Function`/`Date`/`Object`, emitted by -`emit_type_byte_instanceof`): `Array`/`Function`/`Date` match their own byte, and -`Object` matches **any heap pointer whose byte is not `TYPE_STRING`** — so arrays, -plain objects, functions, maps, sets, promises, generators, errors, and dates are -all `instanceof Object`, while a primitive string/number/boolean/`null`/ -`undefined` is not. A non-pointer operand is never an instance (the type-byte read -is gated behind an `is_pointer` check, so it never dereferences a bogus offset). - -```js -[] instanceof Array => true -[] instanceof Object => true -(function(){}) instanceof Function => true -new Error("x") instanceof Object => true -"s" instanceof Object => false // a primitive string is not an object -5 instanceof Object => false -``` +# Build the static, stripped, LTO-optimized binary. +cargo build --profile dist --target x86_64-unknown-linux-musl -p jsc -**`Date` — a heap block over an f64 millisecond timestamp.** A new block type -`TYPE_DATE` (`[type=10][pad×3][pad×4][ms:f64]`) carries the one f64 payload: -milliseconds since the Unix epoch, the same value `Date.now()` / `d.getTime()` -yield. `typeof aDate` is `"object"` (an unrecognized type byte maps to the base -"object" arm). Because a number jsval's raw i64 *is* its f64 bit pattern, the -payload moves between the block and a number jsval by a bare -`reinterpret` — no boxing. - -- **Construction.** `new Date(ms)` stores `Number(ms)` (the argument is coerced - through the existing `__convert` `ToNumber` import, so `new Date(true)` is `1` - and `new Date("2020")` is `2020` — jsc parses **no** date strings, a documented - cut). `new Date()` seeds the payload from the host wall clock. The - calendar-field constructor (`new Date(y, m, d, …)`) and a spread argument are - rejected at codegen. -- **The host clock — one new import.** `env.__now() -> f64` (import index 5, the - first host import added since the promise engine's `__promise`/`__then`) returns the current - time as ms since the epoch; the wasmtime harness backs it with - `SystemTime::now()`. It backs both `new Date()` and the static `Date.now()` - (which returns a plain number). This is jsc's **only** source of runtime - non-determinism, so its tests assert structural properties (`typeof`, - `instanceof`, monotonicity) rather than exact values. -- **`d.getTime()`.** Claimed for any receiver (like the promise/generator instance - methods); a `TYPE_DATE` receiver's f64 payload is read back as a number, and any - other receiver yields `NaN` (jsc models no method-receiver `TypeError` here — a - documented cut). -- **The field methods — guest-side, no host round-trip.** `valueOf()`, - `getFullYear()`, `getMonth()` (0-based), `getDate()`, `getDay()` (0=Sunday), - `getHours()`, `getMinutes()`, `getSeconds()`, and `toISOString()` all compute - **in wasm** from the stored f64 ms — the clock is the only host piece. They - dispatch on the receiver TYPE BYTE exactly like the Map/Set methods - (`IrNode::DateMethod`, `DateOp`): a `TYPE_DATE` block computes the field, and any - other receiver **falls back to an ordinary user method call** `recv.(args)`, - so the names stay usable as plain user object methods. `valueOf() == getTime() ==` - the stored ms. The calendar fields run the same civil-from-days algorithm as the - renderer (floor-divide `ms` by `86_400_000` for the day count, `mod` for the - time-of-day) over six dedicated i64 scratch slots; every field is **UTC**. A - non-finite timestamp yields `NaN` (numeric) or `"Invalid Date"` (`toISOString`, - an interned literal). `toISOString()` builds a fresh fixed-width 24-byte heap - string (`YYYY-MM-DDTHH:MM:SS.mmmZ`) by writing each zero-padded digit at its known - offset — an ordinary string that concatenates and has `.length`. Documented cut: - the 4-digit year field assumes a year in `0..=9999`. -- **Rendering.** `console.log(new Date(0))` prints the ISO 8601 UTC string - `1970-01-01T00:00:00.000Z`; the run stage decodes the payload to `Value::Date` - and derives the civil date with Howard Hinnant's `civil_from_days` algorithm - (correct for pre-epoch/negative timestamps via `div_euclid`/`rem_euclid`). A - non-finite payload renders `Invalid Date`. - -```js -new Date(0) => 1970-01-01T00:00:00.000Z -new Date(1710000000000) => 2024-03-09T16:00:00.000Z -new Date(-86400000) => 1969-12-31T00:00:00.000Z -new Date(1500).getTime() => 1500 -new Date(0) instanceof Date => true -typeof Date.now() => number -new Date(1609459200000).getFullYear() => 2021 -new Date(0).getDay() => 4 // 1970-01-01 was a Thursday -new Date(0).toISOString() => 1970-01-01T00:00:00.000Z +# Result: +target/x86_64-unknown-linux-musl/dist/jsc ``` -**Browser-shim debt (documented).** `Date` adds the `env.__now` import to every -emitted module. The TypeScript browser shim (`packages/jsc`) has not tracked the -host imports since the promise engine (it still lacks `__promise`/`__then`), so it does not yet -supply `__now` either; wiring the promise/date host surface into the shim is -tracked separately from the Rust compiler campaigns. +`x86_64-unknown-linux-musl` links statically against musl libc, so the binary has +no dynamic-loader or glibc-version dependency. The host may need `musl-tools` +(`musl-gcc`) for transitive C shims; `release.yml` installs it. -## 9. Diagnostics (compile-time) +`[profile.dist]` (root `Cargo.toml`): -jsc reports **compile-time** errors — both **parse rejections** (swc refuses the -source) and **codegen rejections** (an unsupported construct, an undeclared variable, -a `const` reassignment) — in a **rustc/swc-style** block: an `error: ` line, a -`-->