From 13ce4c0cf2346787b8389c812b5503a9e5b2abed Mon Sep 17 00:00:00 2001 From: wudi Date: Sun, 18 Oct 2026 04:27:54 +0000 Subject: [PATCH 01/44] ci: add test262 baseline regression gate job --- .github/workflows/ci.yml | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e89ea68..0daf2a9 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 @@ -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@v6 + + - name: Install toolchain 1.96.0 + uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.96.0 + + - 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@v4 + 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. From 031dffeaadb2e01b47a3da475fa625872bc05ba6 Mon Sep 17 00:00:00 2001 From: wudi Date: Sun, 18 Oct 2026 04:30:27 +0000 Subject: [PATCH 02/44] ci: run web workflow on push/PR and rebuild stale wasm in ensure-wasm --- .github/workflows/web.yml | 19 +++++++++++++++++-- scripts/ensure-wasm.sh | 18 +++++------------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml index b191c09..ca829f5 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: diff --git a/scripts/ensure-wasm.sh b/scripts/ensure-wasm.sh index ff3d201..eee511a 100755 --- a/scripts/ensure-wasm.sh +++ b/scripts/ensure-wasm.sh @@ -1,8 +1,7 @@ #!/usr/bin/env bash # Ensure the wasm artifact for @cdlab/jsc exists and is not stale. -# A missing artifact triggers a rebuild via build-wasm.sh. A stale artifact -# (Rust inputs newer than the artifact) only prints a warning: staleness is -# non-fatal so existing local flows keep working. +# A missing artifact, or one older than its Rust inputs, is rebuilt via +# build-wasm.sh. A fresh artifact is left alone. set -euo pipefail cd "$(dirname "$0")/.." @@ -10,6 +9,7 @@ cd "$(dirname "$0")/.." WASM_FILE="packages/jsc/wasm/jsc_wasm_bg.wasm" if [ ! -f "$WASM_FILE" ]; then + echo "ensure-wasm: rebuilding (missing: $WASM_FILE)" bash scripts/build-wasm.sh exit 0 fi @@ -21,14 +21,6 @@ if [ -z "$newer" ] && [ Cargo.lock -nt "$WASM_FILE" ]; then fi if [ -n "$newer" ]; then - { - echo - echo "=================== WARNING: STALE WASM ARTIFACT ===================" - echo "Rust sources changed after the wasm artifact was built" - echo "($newer is newer than $WASM_FILE)." - echo "Checks will run against STALE wasm. Rebuild with:" - echo " bash scripts/build-wasm.sh" - echo "====================================================================" - echo - } >&2 + echo "ensure-wasm: rebuilding (stale: $newer)" + bash scripts/build-wasm.sh fi From 1b65e9c5da1d7a0f2568c5424ec91aabb4de47ff Mon Sep 17 00:00:00 2001 From: wudi Date: Sun, 18 Oct 2026 04:33:43 +0000 Subject: [PATCH 03/44] web: back __promise/__then/__now in the JS shim and drain microtasks after main Every emitted module imports env.__promise/__then/__now, but the shim only provided print/__convert/__method, so instantiation failed with a LinkError for every program. Port the host promise engine from run/promise.rs (promise blocks in guest memory, FIFO microtask queue, reactions, re-entry through __invoke1/__async_step, exception-global handling), back __now with Date.now(), drain after main unless __exc_pending is set, and render Promise and Date values like format_value. --- packages/jsc/README.md | 5 +- packages/jsc/src/abi.ts | 37 ++++ packages/jsc/src/index.ts | 18 +- packages/jsc/src/shim.ts | 371 ++++++++++++++++++++++++++++++++- packages/jsc/src/value.ts | 92 ++++++++ packages/jsc/tests/run.test.ts | 72 +++++++ 6 files changed, 585 insertions(+), 10 deletions(-) diff --git a/packages/jsc/README.md b/packages/jsc/README.md index 0d1d2bc..1c81fc3 100644 --- a/packages/jsc/README.md +++ b/packages/jsc/README.md @@ -35,8 +35,9 @@ Rust runtime semantics: - `src/value.ts` ports the NaN-box decoder and value formatter from `crates/jsc-core/src/run/value.rs`. -- `src/shim.ts` ports the host imports (`print` / `__convert` / `__method`) and - the bump-allocator from `crates/jsc-core/src/run/host.rs` + +- `src/shim.ts` ports the host imports (`print` / `__convert` / `__method` / + `__promise` / `__then` / `__now`), the bump-allocator, and the promise engine + (microtask queue) from `crates/jsc-core/src/run/host.rs` + `run/promise.rs` + `run/harness.rs`. - `src/abi.ts` copies the runtime ABI constants from `crates/jsc-core/src/abi.rs`. diff --git a/packages/jsc/src/abi.ts b/packages/jsc/src/abi.ts index 95ab2c3..db54a4e 100644 --- a/packages/jsc/src/abi.ts +++ b/packages/jsc/src/abi.ts @@ -37,6 +37,22 @@ export const TYPE_ARRAY = 1 export const TYPE_OBJECT = 2 /** TYPE BYTE of a first-class function value. Renders as `[Function (anonymous)]`. */ export const TYPE_FUNCTION = 3 +/** TYPE BYTE of a promise block: `[type=8][pad×3][state:i32][value:i64]`. */ +export const TYPE_PROMISE = 8 +/** TYPE BYTE of a `Date` block: `[type=10][pad×3][pad:u32][ms:f64]`. */ +export const TYPE_DATE = 10 + +/** {@link TYPE_PROMISE} `state`: the promise is unsettled. */ +export const PROMISE_PENDING = 0 +/** {@link TYPE_PROMISE} `state`: the promise fulfilled with its `value`. */ +export const PROMISE_FULFILLED = 1 +/** {@link TYPE_PROMISE} `state`: the promise rejected with its `value` as the reason. */ +export const PROMISE_REJECTED = 2 + +/** Byte offset (from a generator block base) of the `done` flag (`i32`). */ +export const GEN_DONE_OFFSET = HEADER_SIZE + 8 +/** Byte offset (from a {@link TYPE_DATE} block base) of the `ms` timestamp (`f64`). */ +export const DATE_MS_OFFSET = HEADER_SIZE + 4 /** Export name of the module's linear memory. */ export const MEMORY_EXPORT_NAME = 'memory' @@ -46,6 +62,10 @@ export const HEAP_PTR_EXPORT_NAME = '__heap_ptr' export const EXC_PENDING_EXPORT_NAME = '__exc_pending' /** Export name of the in-flight exception value global (`i64`, S15.1). */ export const EXC_VALUE_EXPORT_NAME = '__exc_value' +/** Export name of the `__invoke1(callback, arg)` callback trampoline (promise reactions). */ +export const INVOKE_EXPORT_NAME = '__invoke1' +/** Export name of the `__async_step(gen, sent)` async-body step trampoline. */ +export const ASYNC_STEP_EXPORT_NAME = '__async_step' /** `print` flag: format the jsval and follow it with a single space. */ export const PRINT_FLAG_SEP = 0 @@ -98,6 +118,23 @@ export const METHOD_OP_SUBSTRING = 15 /** `__method` op: `split` (string, yields an array). */ export const METHOD_OP_SPLIT = 16 +/** `__promise` op: `Promise.resolve(a)`. */ +export const PROMISE_OP_RESOLVE = 0 +/** `__promise` op: `Promise.reject(a)`. */ +export const PROMISE_OP_REJECT = 1 +/** `__promise` op: top-level `await a`. */ +export const PROMISE_OP_AWAIT = 2 +/** `__promise` op: `a.finally(b)`. */ +export const PROMISE_OP_FINALLY = 3 +/** `__promise` op: allocate a pending promise (`new Promise`). */ +export const PROMISE_OP_NEW = 4 +/** `__promise` op: fulfill promise `a` with `b` (a `resolve` settler call). */ +export const PROMISE_OP_SETTLE_FULFILL = 5 +/** `__promise` op: reject promise `a` with `b` (a `reject` settler call). */ +export const PROMISE_OP_SETTLE_REJECT = 6 +/** `__promise` op: spawn an `async function` body from generator handle `a`. */ +export const PROMISE_OP_ASYNC_SPAWN = 7 + /** The `typeof` result strings, indexed by a boxed type-string immediate. */ export const TYPEOF_STRINGS = [ 'number', diff --git a/packages/jsc/src/index.ts b/packages/jsc/src/index.ts index cbc7cb5..ccb574a 100644 --- a/packages/jsc/src/index.ts +++ b/packages/jsc/src/index.ts @@ -87,6 +87,18 @@ export async function run(src: string): Promise { const main = instance.exports.main as () => bigint const bits = asU64(main()) + // Drain microtasks scheduled but never awaited, UNLESS `main` completed + // abruptly with a pending exception (mirrors `harness.rs`'s after-`main` + // `drain_after_main`). + const pendingGlobal = instance.exports[EXC_PENDING_EXPORT_NAME] as + | WebAssembly.Global + | undefined + const excPending = (): number => + pendingGlobal ? Number(pendingGlobal.value) : 0 + if (excPending() === 0) shim.drainAfterMain() + + // View linear memory AFTER the drain so a promise settled by a microtask + // renders its final state (the buffer may also have grown/detached). const mem = instance.exports[MEMORY_EXPORT_NAME] as | WebAssembly.Memory | undefined @@ -94,11 +106,7 @@ export async function run(src: string): Promise { // Uncaught-exception check (S15.1): a set `$exc_pending` after `main` means an // exception propagated all the way out. Render `$exc_value` and surface it. - const pendingGlobal = instance.exports[EXC_PENDING_EXPORT_NAME] as - | WebAssembly.Global - | undefined - const pending = pendingGlobal ? Number(pendingGlobal.value) : 0 - if (pending !== 0) { + if (excPending() !== 0) { const valueGlobal = instance.exports[EXC_VALUE_EXPORT_NAME] as | WebAssembly.Global | undefined diff --git a/packages/jsc/src/shim.ts b/packages/jsc/src/shim.ts index ff79996..906a0f3 100644 --- a/packages/jsc/src/shim.ts +++ b/packages/jsc/src/shim.ts @@ -1,20 +1,26 @@ /** * The JS runtime shim: the `importObject` that backs the emitted module's host - * env imports (`print` / `__convert` / `__method`) plus the bump-allocator they - * share. A port of `crates/jsc-core/src/run/harness.rs` (import wiring) and - * `run/host.rs` (handlers + allocator). ACCEPTED TEMPORARY DEBT (see README): + * env imports (`print` / `__convert` / `__method` / `__promise` / `__then` / + * `__now`) plus the bump-allocator they share. A port of + * `crates/jsc-core/src/run/harness.rs` (import wiring), `run/host.rs` (handlers + + * allocator), and `run/promise.rs` (the promise engine + microtask queue). ACCEPTED TEMPORARY DEBT (see README): * this duplicates Rust host semantics against the emitted module's memory. */ import { + ASYNC_STEP_EXPORT_NAME, CONVERT_OP_PARSE_FLOAT, CONVERT_OP_PARSE_INT, CONVERT_OP_TO_NUMBER, CONVERT_OP_TO_STRING, + EXC_PENDING_EXPORT_NAME, + EXC_VALUE_EXPORT_NAME, FALSE, + GEN_DONE_OFFSET, HEADER_SIZE, HEAP, HEAP_PTR_EXPORT_NAME, + INVOKE_EXPORT_NAME, MEMORY_EXPORT_NAME, METHOD_OP_CHAR_AT, METHOD_OP_ENDS_WITH, @@ -35,9 +41,22 @@ import { METHOD_OP_TRIM_START, PRINT_FLAG_END, PRINT_FLAG_SEP, + PROMISE_FULFILLED, + PROMISE_OP_ASYNC_SPAWN, + PROMISE_OP_AWAIT, + PROMISE_OP_FINALLY, + PROMISE_OP_NEW, + PROMISE_OP_REJECT, + PROMISE_OP_RESOLVE, + PROMISE_OP_SETTLE_FULFILL, + PROMISE_OP_SETTLE_REJECT, + PROMISE_PENDING, + PROMISE_REJECTED, QNAN, TRUE, TYPE_ARRAY, + TYPE_FUNCTION, + TYPE_PROMISE, TYPE_STRING, U32_MASK, UNDEFINED, @@ -60,8 +79,40 @@ export interface Shim { stdout: () => string /** Bind the instantiated module so the imports can read/grow its memory. */ bindInstance: (instance: WebAssembly.Instance) => void + /** Drain the microtask queue to empty (mirrors `promise.rs::drain_after_main`). */ + drainAfterMain: () => void } +/** + * A reaction registered on a still-pending promise (mirrors + * `promise.rs::Reaction`): the two handlers (`undefined` = passthrough), the + * chained result promise's block base, `finally` semantics, and — for an + * `async function` continuation — the body's generator handle. + */ +interface Reaction { + onFulfilled: bigint + onRejected: bigint + result: number + finally: boolean + asyncGen: bigint | null +} + +/** + * A queued microtask (mirrors `promise.rs::Job`): the selected callback, the + * source settlement, and the chained promise to settle from the outcome. + */ +interface Job { + callback: bigint + state: number + value: bigint + result: number + finally: boolean + asyncGen: bigint | null +} + +/** The `[type][pad×3][state:i32][value:i64]` promise block size in bytes. */ +const PROMISE_BLOCK_SIZE = HEADER_SIZE + 4 + 8 + const utf8Encode = new TextEncoder() const utf8Decoder = new TextDecoder() @@ -392,6 +443,273 @@ export function createShim(): Shim { } } + // ---- promise engine (ported from promise.rs) ---------------------------- + + /** The FIFO microtask queue. */ + const queue: Job[] = [] + /** Reactions of each still-pending promise, keyed by block base offset. */ + const reactions = new Map() + + /** An exported global by name, or `undefined` when absent. */ + function exportGlobal(name: string): WebAssembly.Global | undefined { + return instance?.exports[name] as WebAssembly.Global | undefined + } + + /** An exported `(i64, i64) -> i64` trampoline by name, or `undefined` when absent. */ + function exportTrampoline( + name: string, + ): ((a: bigint, b: bigint) => bigint) | undefined { + return instance?.exports[name] as + | ((a: bigint, b: bigint) => bigint) + | undefined + } + + /** + * Bump-allocate a fresh promise block and return its heap jsval, or `null` on + * a failed allocation (mirrors `promise.rs::alloc_promise`). + */ + function allocPromise(state: number, value: bigint): bigint | null { + const block = new Uint8Array(PROMISE_BLOCK_SIZE) + block[0] = TYPE_PROMISE + const dv = new DataView(block.buffer) + dv.setInt32(HEADER_SIZE, state, true) + dv.setBigUint64(HEADER_SIZE + 4, value, true) + const base = hostAlloc(block) + return base === null ? null : HEAP | base + } + + /** Write a promise block's settlement into guest memory (mirrors `write_settlement`). */ + function writeSettlement(base: number, state: number, value: bigint): void { + const mem = memory() + if (!mem || base + PROMISE_BLOCK_SIZE > mem.buffer.byteLength) return + const dv = new DataView(mem.buffer) + dv.setInt32(base + HEADER_SIZE, state, true) + dv.setBigUint64(base + HEADER_SIZE + 4, value, true) + } + + /** + * Run the guest trampoline `name(a, b)` re-entrantly (mirrors + * `promise.rs::invoke1` / `async_step`): returns its result bits and whether + * it threw — detected by `__exc_pending`, which (with `__exc_value`) is + * cleared so a surfaced exception does not leak into the next microtask. A + * missing export or a trap yields `[undefined, false]`. + */ + function callGuest(name: string, a: bigint, b: bigint): [bigint, boolean] { + const func = exportTrampoline(name) + const pending = exportGlobal(EXC_PENDING_EXPORT_NAME) + const excValue = exportGlobal(EXC_VALUE_EXPORT_NAME) + if (!func || !pending || !excValue) return [UNDEFINED, false] + let result: bigint + try { + result = asU64(func(a, b)) + } catch { + return [UNDEFINED, false] + } + if (Number(pending.value) !== 0) { + const value = asU64(excValue.value as bigint) + pending.value = 0 + excValue.value = 0n + return [value, true] + } + return [result, false] + } + + /** + * Settle promise `base` once; fulfilling with a promise ADOPTS it. On a real + * settlement its reactions move into the queue (mirrors `promise.rs::settle`). + */ + function settle(base: number, state: number, value: bigint): void { + if (readState(memView(), base) !== PROMISE_PENDING) return + if (state === PROMISE_FULFILLED && asPromise(memView(), value) !== null) { + registerSource(value, UNDEFINED, UNDEFINED, base, false, null) + return + } + writeSettlement(base, state, value) + const pendingReactions = reactions.get(base) ?? [] + reactions.delete(base) + for (const r of pendingReactions) { + queue.push({ + callback: state === PROMISE_FULFILLED ? r.onFulfilled : r.onRejected, + state, + value, + result: r.result, + finally: r.finally, + asyncGen: r.asyncGen, + }) + } + } + + /** + * Register a reaction on `source`: stored on a pending promise, queued now + * for a settled one, queued as fulfilled for a non-promise value (mirrors + * `promise.rs::register_source`). + */ + function registerSource( + source: bigint, + onFulfilled: bigint, + onRejected: bigint, + result: number, + isFinally: boolean, + asyncGen: bigint | null, + ): void { + const mem = memView() + const base = asPromise(mem, source) + if (base === null) { + queue.push({ + callback: onFulfilled, + state: PROMISE_FULFILLED, + value: source, + result, + finally: isFinally, + asyncGen, + }) + return + } + const state = readState(mem, base) + if (state === PROMISE_PENDING) { + const list = reactions.get(base) ?? [] + list.push({ + onFulfilled, + onRejected, + result, + finally: isFinally, + asyncGen, + }) + reactions.set(base, list) + return + } + queue.push({ + callback: state === PROMISE_FULFILLED ? onFulfilled : onRejected, + state, + value: readValue(mem, base), + result, + finally: isFinally, + asyncGen, + }) + } + + /** + * Advance an `async function` body one step and route the outcome (mirrors + * `promise.rs::drive_async`): reject on throw, fulfill on `done`, otherwise + * register a continuation on the awaited value. + */ + function driveAsync(gen: bigint, resultBase: number, sent: bigint): void { + const [value, threw] = callGuest(ASYNC_STEP_EXPORT_NAME, gen, sent) + if (threw) { + settle(resultBase, PROMISE_REJECTED, value) + return + } + if (readGenDone(memView(), gen)) { + settle(resultBase, PROMISE_FULFILLED, value) + return + } + registerSource(value, UNDEFINED, UNDEFINED, resultBase, false, gen) + } + + /** Run one microtask (mirrors `promise.rs::run_job`). */ + function runJob(job: Job): void { + if (job.asyncGen !== null) { + if (job.state === PROMISE_FULFILLED) { + driveAsync(job.asyncGen, job.result, job.value) + } else { + settle(job.result, PROMISE_REJECTED, job.value) + } + return + } + if (!asFunction(memView(), job.callback)) { + settle(job.result, job.state, job.value) + return + } + const [outcome, threw] = callGuest( + INVOKE_EXPORT_NAME, + job.callback, + job.value, + ) + if (threw) settle(job.result, PROMISE_REJECTED, outcome) + else if (job.finally) settle(job.result, job.state, job.value) + else settle(job.result, PROMISE_FULFILLED, outcome) + } + + /** Drive the microtask queue to empty, FIFO (mirrors `promise.rs::drain`). */ + function drain(): void { + for (let job = queue.shift(); job !== undefined; job = queue.shift()) { + runJob(job) + } + } + + /** + * Top-level `await p`: drain the queue, then return `p`'s fulfillment value, + * or surface a rejection through the exception globals (mirrors + * `promise.rs::await_promise`). A non-promise is returned unchanged. + */ + function awaitPromise(p: bigint): bigint { + const base = asPromise(memView(), p) + if (base === null) return p + drain() + const mem = memView() + const state = readState(mem, base) + if (state === PROMISE_FULFILLED) return readValue(mem, base) + if (state === PROMISE_REJECTED) { + const pending = exportGlobal(EXC_PENDING_EXPORT_NAME) + const excValue = exportGlobal(EXC_VALUE_EXPORT_NAME) + if (excValue) excValue.value = BigInt.asIntN(64, readValue(mem, base)) + if (pending) pending.value = 1 + } + return UNDEFINED + } + + /** `__promise(op, a, b)` (mirrors `promise.rs::dispatch`). */ + function promiseDispatch(op: number, a: bigint, b: bigint): bigint { + switch (op) { + case PROMISE_OP_RESOLVE: + return asPromise(memView(), a) !== null + ? a + : (allocPromise(PROMISE_FULFILLED, a) ?? UNDEFINED) + case PROMISE_OP_REJECT: + return allocPromise(PROMISE_REJECTED, a) ?? UNDEFINED + case PROMISE_OP_FINALLY: { + const result = allocPromise(PROMISE_PENDING, UNDEFINED) + if (result === null) return UNDEFINED + registerSource(a, b, b, baseOf(result), true, null) + return result + } + case PROMISE_OP_AWAIT: + return awaitPromise(a) + case PROMISE_OP_NEW: + return allocPromise(PROMISE_PENDING, UNDEFINED) ?? UNDEFINED + case PROMISE_OP_SETTLE_FULFILL: + case PROMISE_OP_SETTLE_REJECT: { + const base = asPromise(memView(), a) + if (base !== null) { + settle( + base, + op === PROMISE_OP_SETTLE_FULFILL + ? PROMISE_FULFILLED + : PROMISE_REJECTED, + b, + ) + } + return UNDEFINED + } + case PROMISE_OP_ASYNC_SPAWN: { + const result = allocPromise(PROMISE_PENDING, UNDEFINED) + if (result === null) return UNDEFINED + driveAsync(a, baseOf(result), UNDEFINED) + return result + } + default: + return UNDEFINED + } + } + + /** `__then(p, onF, onR)` (mirrors `promise.rs::then`). */ + function promiseThen(p: bigint, onF: bigint, onR: bigint): bigint { + const result = allocPromise(PROMISE_PENDING, UNDEFINED) + if (result === null) return UNDEFINED + registerSource(p, onF, onR, baseOf(result), false, null) + return result + } + const print = (bits: bigint, flag: number): void => { let piece: string if (flag === PRINT_FLAG_SEP || flag === PRINT_FLAG_END) { @@ -438,6 +756,12 @@ export function createShim(): Shim { print, __convert: convert, __method: method, + __promise: (op: number, a: bigint, b: bigint): bigint => + promiseDispatch(op, asU64(a), asU64(b)), + __then: (p: bigint, onF: bigint, onR: bigint): bigint => + promiseThen(asU64(p), asU64(onF), asU64(onR)), + // The host wall clock backing `Date.now()` / `new Date()` (ms since epoch). + __now: (): number => Date.now(), }, } @@ -447,6 +771,7 @@ export function createShim(): Shim { bindInstance: (bound) => { instance = bound }, + drainAfterMain: drain, } } @@ -460,6 +785,46 @@ function heapPtr(bits: bigint): number | null { return null } +/** The base offset (low 32 bits) of a heap jsval. */ +function baseOf(bits: bigint): number { + return Number(bits & U32_MASK) +} + +/** The block base of a heap jsval whose TYPE BYTE is {@link TYPE_PROMISE}, else `null`. */ +function asPromise(mem: Uint8Array, bits: bigint): number | null { + const base = heapPtr(bits) + return base !== null && mem[base] === TYPE_PROMISE ? base : null +} + +/** Whether `bits` is a heap function value (TYPE BYTE {@link TYPE_FUNCTION}). */ +function asFunction(mem: Uint8Array, bits: bigint): boolean { + const base = heapPtr(bits) + return base !== null && mem[base] === TYPE_FUNCTION +} + +/** A promise block's `i32` state; a truncated block reads as pending. */ +function readState(mem: Uint8Array, base: number): number { + const at = base + HEADER_SIZE + if (at + 4 > mem.length) return PROMISE_PENDING + return new DataView(mem.buffer, mem.byteOffset).getInt32(at, true) +} + +/** A promise block's settled value bits; a truncated block reads as `undefined`. */ +function readValue(mem: Uint8Array, base: number): bigint { + const at = base + HEADER_SIZE + 4 + if (at + 8 > mem.length) return UNDEFINED + return new DataView(mem.buffer, mem.byteOffset).getBigUint64(at, true) +} + +/** An async-body generator handle's `done` flag (mirrors `promise.rs::read_gen_done`). */ +function readGenDone(mem: Uint8Array, gen: bigint): boolean { + const base = heapPtr(gen) + if (base === null) return false + const at = base + GEN_DONE_OFFSET + if (at + 4 > mem.length) return false + return new DataView(mem.buffer, mem.byteOffset).getInt32(at, true) !== 0 +} + /** The `u32` `len` header of an array/string block at `ptr + HEADER_SIZE`. */ function blockLen(mem: Uint8Array, ptr: number): number { const at = ptr + HEADER_SIZE diff --git a/packages/jsc/src/value.ts b/packages/jsc/src/value.ts index 234f96b..0bb8e9e 100644 --- a/packages/jsc/src/value.ts +++ b/packages/jsc/src/value.ts @@ -6,11 +6,16 @@ */ import { + DATE_MS_OFFSET, HEADER_SIZE, + PROMISE_FULFILLED, + PROMISE_REJECTED, QNAN, TYPE_ARRAY, + TYPE_DATE, TYPE_FUNCTION, TYPE_OBJECT, + TYPE_PROMISE, TYPE_STRING, TYPEOF_STRINGS, U32_MASK, @@ -28,6 +33,8 @@ export type Value = | { kind: 'array'; items: Value[] } | { kind: 'object'; entries: [string, Value][] } | { kind: 'function' } + | { kind: 'promise'; state: number; value: Value } + | { kind: 'date'; ms: number } // A single scratch buffer for f64 <-> u64 bit reinterpretation, reused so we do // not allocate a DataView per decode. @@ -116,11 +123,39 @@ function decodeHeap(bits: bigint, mem: Uint8Array): Value { return decodeObject(ptr, mem) case TYPE_FUNCTION: return { kind: 'function' } + case TYPE_PROMISE: + return decodePromise(ptr, mem) + case TYPE_DATE: + return decodeDate(ptr, mem) default: return { kind: 'undefined' } } } +/** + * Resolve a heap promise block at `ptr` (mirrors `value.rs::decode_promise`): + * the `i32` state at `ptr + HEADER_SIZE` and the settled `i64` value after it. A + * truncated block reads as pending with an `undefined` value. + */ +function decodePromise(ptr: number, mem: Uint8Array): Value { + const state = readU32(mem, ptr + HEADER_SIZE) + const bits = readU64(mem, ptr + HEADER_SIZE + 4) + return { + kind: 'promise', + state: state === undefined ? 0 : state | 0, + value: bits === undefined ? { kind: 'undefined' } : decode(bits, mem), + } +} + +/** + * Resolve a heap `Date` block at `ptr` by reading its `f64` ms payload (mirrors + * `value.rs::decode_date`); a truncated block reads as `NaN` (`Invalid Date`). + */ +function decodeDate(ptr: number, mem: Uint8Array): Value { + const bits = readU64(mem, ptr + DATE_MS_OFFSET) + return { kind: 'date', ms: bits === undefined ? Number.NaN : bitsToF64(bits) } +} + /** Resolve a heap string block at `ptr`: `[len:u32][utf8]` at `ptr + HEADER_SIZE`. */ function decodeString(ptr: number, mem: Uint8Array): Value { const len = readU32(mem, ptr + HEADER_SIZE) @@ -199,9 +234,66 @@ export function formatValue(value: Value): string { : `{ ${value.entries.map(([k, v]) => `${k}: ${formatValue(v)}`).join(', ')} }` case 'function': return '[Function (anonymous)]' + case 'promise': + if (value.state === PROMISE_FULFILLED) + return `Promise { ${formatValue(value.value)} }` + if (value.state === PROMISE_REJECTED) + return `Promise { ${formatValue(value.value)} }` + return 'Promise { }' + case 'date': + return formatDateIso(value.ms) } } +/** + * Render a millisecond epoch timestamp as `YYYY-MM-DDTHH:MM:SS.mmmZ` (mirrors + * `value.rs::format_date_iso`, Howard Hinnant's `civil_from_days`); a + * non-finite timestamp renders as `Invalid Date`. Ported rather than delegated + * to `Date.prototype.toISOString`, which uses extended `±YYYYYY` years and + * throws out of range. + */ +function formatDateIso(ms: number): string { + if (!Number.isFinite(ms)) return 'Invalid Date' + const totalMs = Math.floor(ms) + const days = Math.floor(totalMs / 86_400_000) + let tod = totalMs - days * 86_400_000 + const millis = tod % 1000 + tod = Math.trunc(tod / 1000) + const secs = tod % 60 + tod = Math.trunc(tod / 60) + const mins = tod % 60 + const hours = Math.trunc(tod / 60) + + const z = days + 719_468 + const era = Math.trunc((z >= 0 ? z : z - 146_096) / 146_097) + const doe = z - era * 146_097 + const yoe = Math.trunc( + (doe - + Math.trunc(doe / 1460) + + Math.trunc(doe / 36_524) - + Math.trunc(doe / 146_096)) / + 365, + ) + const doy = doe - (365 * yoe + Math.trunc(yoe / 4) - Math.trunc(yoe / 100)) + const mp = Math.trunc((5 * doy + 2) / 153) + const day = doy - Math.trunc((153 * mp + 2) / 5) + 1 + const month = mp < 10 ? mp + 3 : mp - 9 + const year = yoe + era * 400 + (month <= 2 ? 1 : 0) + + return `${padSigned(year, 4)}-${pad2(month)}-${pad2(day)}T${pad2(hours)}:${pad2(mins)}:${pad2(secs)}.${String(millis).padStart(3, '0')}Z` +} + +/** Zero-pad to two digits (Rust `{:02}` for a non-negative integer). */ +function pad2(n: number): string { + return String(n).padStart(2, '0') +} + +/** Zero-pad to `width` chars counting the sign (Rust `{:0width}` for a signed integer). */ +function padSigned(n: number, width: number): string { + const sign = n < 0 ? '-' : '' + return sign + String(Math.abs(n)).padStart(width - sign.length, '0') +} + /** * Coerce a decoded {@link Value} to a JS number (the `Number(x)` subset from * `host.rs::value_to_number`): number is identity, `true`/`false` are `1`/`0`, diff --git a/packages/jsc/tests/run.test.ts b/packages/jsc/tests/run.test.ts index 3b256b6..438612f 100644 --- a/packages/jsc/tests/run.test.ts +++ b/packages/jsc/tests/run.test.ts @@ -1,6 +1,7 @@ import { expect, it } from 'vitest' import { compile, run } from '../src/index.js' +import { createShim } from '../src/shim.js' // Expected `result` strings are the EXACT output of `cargo run -p jsc -- -e ` // (jsc-core's `format_value`); the shim must reproduce them byte for byte. @@ -54,3 +55,74 @@ it('dispatches the M2 string methods through the shim', async () => { expect((await run("'hello'.substring(3,1)")).result).toBe('el') expect((await run("'a,b,c'.split(',').length")).result).toBe('3') }) + +it('links every emitted module against the shim (no LinkError)', async () => { + expect((await run('1+1')).result).toBe('2') +}) + +it('provides every env import an emitted module declares', async () => { + const module = await WebAssembly.compile( + (await compile('1')) as Uint8Array, + ) + const provided = Object.keys(createShim().imports.env ?? {}) + for (const imp of WebAssembly.Module.imports(module)) { + expect(imp.module).toBe('env') + expect(provided).toContain(imp.name) + } +}) + +it('runs a Promise chain through the microtask queue', async () => { + const chain = await run( + 'Promise.resolve(1).then(x => x + 1).then(x => console.log(x))', + ) + expect(chain.stdout).toBe('2\n') + expect(chain.result).toBe('Promise { undefined }') + const caught = await run( + 'Promise.reject("bad").catch(e => console.log("caught", e))', + ) + expect(caught.stdout).toBe('caught bad\n') + expect((await run('Promise.reject(3).catch(e=>e)')).result).toBe( + 'Promise { 3 }', + ) + const fin = await run('Promise.resolve(1).finally(()=>console.log("fin"))') + expect(fin.stdout).toBe('fin\n') + expect(fin.result).toBe('Promise { 1 }') +}) + +it('renders promises by settlement', async () => { + expect((await run('Promise.resolve(42)')).result).toBe('Promise { 42 }') + expect( + (await run('const p = Promise.resolve(1).then(x=>x+1); p')).result, + ).toBe('Promise { 2 }') +}) + +it('runs microtasks after synchronous code', async () => { + const { stdout, result } = await run( + 'Promise.resolve(1).then(()=>console.log("micro")); console.log("sync")', + ) + expect(stdout).toBe('sync\nmicro\n') + expect(result).toBe('undefined') +}) + +it('runs async/await with top-level await', async () => { + const { stdout, result } = await run( + 'async function f(){ const v = await Promise.resolve(5); console.log("in", v); return v*2 } console.log("sync"); const r = await f(); console.log("r", r); r', + ) + expect(stdout).toBe('sync\nin 5\nr 10\n') + expect(result).toBe('10') +}) + +it('backs Date.now() with the host clock', async () => { + expect((await run('typeof Date.now()')).result).toBe('number') + const before = Date.now() + const now = Number((await run('Date.now()')).result) + expect(now).toBeGreaterThanOrEqual(before) + expect(now).toBeLessThanOrEqual(Date.now()) + expect((await run('new Date(0)')).result).toBe('1970-01-01T00:00:00.000Z') +}) + +it('rejects run() on an unhandled top-level await rejection', async () => { + await expect(run('await Promise.reject("boom")')).rejects.toThrow( + /^Uncaught boom$/, + ) +}) From 9cdea000adaeaa22f47d248a4f75cc7ad3ca0652 Mon Sep 17 00:00:00 2001 From: wudi Date: Sun, 18 Oct 2026 04:39:42 +0000 Subject: [PATCH 04/44] test262: add runner core library crate Frontmatter parsing, eligibility filtering, program assembly with the assert adapter rewrite, outcome classification, baseline diffing, test discovery and report aggregation, each with unit tests. --- Cargo.lock | 4 + Cargo.toml | 2 +- crates/jsc-test262/Cargo.toml | 17 +++ crates/jsc-test262/src/assemble.rs | 110 ++++++++++++++++ crates/jsc-test262/src/baseline.rs | 63 +++++++++ crates/jsc-test262/src/filter.rs | 174 +++++++++++++++++++++++++ crates/jsc-test262/src/frontmatter.rs | 149 +++++++++++++++++++++ crates/jsc-test262/src/lib.rs | 12 ++ crates/jsc-test262/src/main.rs | 8 ++ crates/jsc-test262/src/outcome.rs | 178 ++++++++++++++++++++++++++ crates/jsc-test262/src/report.rs | 167 ++++++++++++++++++++++++ crates/jsc-test262/src/walk.rs | 88 +++++++++++++ 12 files changed, 971 insertions(+), 1 deletion(-) create mode 100644 crates/jsc-test262/Cargo.toml create mode 100644 crates/jsc-test262/src/assemble.rs create mode 100644 crates/jsc-test262/src/baseline.rs create mode 100644 crates/jsc-test262/src/filter.rs create mode 100644 crates/jsc-test262/src/frontmatter.rs create mode 100644 crates/jsc-test262/src/lib.rs create mode 100644 crates/jsc-test262/src/main.rs create mode 100644 crates/jsc-test262/src/outcome.rs create mode 100644 crates/jsc-test262/src/report.rs create mode 100644 crates/jsc-test262/src/walk.rs diff --git a/Cargo.lock b/Cargo.lock index b3151d0..bf6ea86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -848,6 +848,10 @@ dependencies = [ "wasmtime", ] +[[package]] +name = "jsc-test262" +version = "0.1.0" + [[package]] name = "jsc-wasm" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 3aa2b87..af7bae4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [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] diff --git a/crates/jsc-test262/Cargo.toml b/crates/jsc-test262/Cargo.toml new file mode 100644 index 0000000..051c4b3 --- /dev/null +++ b/crates/jsc-test262/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "jsc-test262" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +publish = false + +[lib] +path = "src/lib.rs" + +[[bin]] +name = "jsc-test262" +path = "src/main.rs" + +[lints] +workspace = true diff --git a/crates/jsc-test262/src/assemble.rs b/crates/jsc-test262/src/assemble.rs new file mode 100644 index 0000000..77111ff --- /dev/null +++ b/crates/jsc-test262/src/assemble.rs @@ -0,0 +1,110 @@ +//! Builds the program text handed to jsc. +//! +//! [`Mode::Strict`] programs are `"use strict";`, then the harness `sta.js`, +//! `assert.js` and the listed includes, then the test source. [`Mode::Raw`] +//! programs are the test source byte-for-byte. +//! +//! # Adapter rewrite +//! +//! jsc functions cannot carry properties, so the harness shims define +//! `assert_sameValue`, `assert_throws`, ... as plain functions. The single +//! rewrite applied to strict test sources turns every `assert.(` into +//! `assert_(`, where `` is `[A-Za-z_$][A-Za-z0-9_$]*` and +//! `assert` is not preceded by an identifier character or `.`. The scan is +//! purely lexical (strings and comments are rewritten too); includes are +//! never rewritten. + +use crate::filter::Mode; + +/// Assembles the program for `source` run in `mode`. +pub fn program(mode: Mode, source: &str, sta: &str, assert: &str, includes: &[&str]) -> String { + match mode { + Mode::Raw => source.to_owned(), + Mode::Strict => { + let mut out = String::from("\"use strict\";\n"); + for part in [sta, assert].iter().chain(includes) { + out.push_str(part); + out.push('\n'); + } + out.push_str(&rewrite(source)); + out + } + } +} + +fn is_ident(b: u8) -> bool { + b.is_ascii_alphanumeric() || b == b'_' || b == b'$' +} + +/// Applies the adapter rewrite described in the module docs. +pub(crate) fn rewrite(source: &str) -> String { + let bytes = source.as_bytes(); + let mut out = String::with_capacity(source.len()); + let mut last = 0; + let mut from = 0; + while let Some(off) = source[from..].find("assert.") { + let at = from + off; + from = at + 1; + if at > 0 && (is_ident(bytes[at - 1]) || bytes[at - 1] == b'.') { + continue; + } + let dot = at + 6; + let name_start = dot + 1; + let mut end = name_start; + while end < bytes.len() && is_ident(bytes[end]) { + end += 1; + } + if end > name_start && !bytes[name_start].is_ascii_digit() && bytes.get(end) == Some(&b'(') + { + out.push_str(&source[last..dot]); + out.push('_'); + last = dot + 1; + } + } + out.push_str(&source[last..]); + out +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn strict_prefix_and_order() { + let p = program( + Mode::Strict, + "assert.sameValue(1, 1);", + "STA", + "ASSERT", + &["INC1", "assert.x("], + ); + assert_eq!( + p, + "\"use strict\";\nSTA\nASSERT\nINC1\nassert.x(\nassert_sameValue(1, 1);" + ); + } + + #[test] + fn raw_untouched() { + let src = "assert.sameValue(1, 1); 'use strict'"; + assert_eq!(program(Mode::Raw, src, "STA", "ASSERT", &["I"]), src); + } + + #[test] + fn rewrite_cases() { + assert_eq!(rewrite("assert(x);"), "assert(x);"); + assert_eq!( + rewrite("assert.sameValue(a, b);"), + "assert_sameValue(a, b);" + ); + assert_eq!(rewrite("x;assert.throws(E, f)"), "x;assert_throws(E, f)"); + assert_eq!(rewrite("assert.$_9("), "assert_$_9("); + assert_eq!(rewrite("myassert.x(1)"), "myassert.x(1)"); + assert_eq!(rewrite("foo.assert.x(1)"), "foo.assert.x(1)"); + assert_eq!( + rewrite("assert.x;assert.(;assert.1("), + "assert.x;assert.(;assert.1(" + ); + assert_eq!(rewrite("é assert.ok("), "é assert_ok("); + } +} diff --git a/crates/jsc-test262/src/baseline.rs b/crates/jsc-test262/src/baseline.rs new file mode 100644 index 0000000..f961ae8 --- /dev/null +++ b/crates/jsc-test262/src/baseline.rs @@ -0,0 +1,63 @@ +//! Baseline of passing tests: one path (relative to `test/`) per line. + +use std::collections::BTreeSet; + +/// Difference between a baseline and the current passing set. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct Diff { + /// Baseline tests (within the filter) that no longer pass. + pub regressed: Vec, + /// Passing tests absent from the baseline. + pub newly_passing: Vec, +} + +/// Parses baseline text; blank lines are ignored. +pub fn parse(text: &str) -> BTreeSet { + text.lines() + .map(str::trim) + .filter(|l| !l.is_empty()) + .map(str::to_owned) + .collect() +} + +/// Renders a baseline: sorted, one path per `\n`-terminated line. +pub fn render(set: &BTreeSet) -> String { + set.iter().flat_map(|p| [p.as_str(), "\n"]).collect() +} + +/// Compares `passing` against `baseline`. Only baseline entries starting with +/// one of the `filter` prefixes (all, if empty) can regress. +pub fn diff(baseline: &BTreeSet, passing: &BTreeSet, filter: &[String]) -> Diff { + let in_scope = + |p: &String| filter.is_empty() || filter.iter().any(|f| p.starts_with(f.as_str())); + Diff { + regressed: baseline + .iter() + .filter(|p| in_scope(p) && !passing.contains(*p)) + .cloned() + .collect(), + newly_passing: passing.difference(baseline).cloned().collect(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn round_trip() { + let set = parse("b.js\n\n a.js \r\n"); + assert_eq!(render(&set), "a.js\nb.js\n"); + assert_eq!(render(&BTreeSet::new()), ""); + } + + #[test] + fn diff_scoped() { + let base = parse("x/a.js\nx/b.js\ny/c.js\n"); + let pass = parse("x/a.js\nx/new.js\n"); + let all = diff(&base, &pass, &[]); + assert_eq!(all.regressed, ["x/b.js", "y/c.js"]); + assert_eq!(all.newly_passing, ["x/new.js"]); + assert_eq!(diff(&base, &pass, &["x/".to_owned()]).regressed, ["x/b.js"]); + } +} diff --git a/crates/jsc-test262/src/filter.rs b/crates/jsc-test262/src/filter.rs new file mode 100644 index 0000000..73a05f8 --- /dev/null +++ b/crates/jsc-test262/src/filter.rs @@ -0,0 +1,174 @@ +//! Test eligibility: decides whether a test runs (and how) or is skipped. + +use std::collections::BTreeSet; + +use crate::frontmatter::Meta; + +/// Features jsc does not support. Entries ending in `*` match by prefix. +#[derive(Debug, Clone, Default)] +pub struct Denylist { + exact: BTreeSet, + prefixes: Vec, +} + +impl Denylist { + /// Parses one feature per line; `#` comments and blank lines are ignored. + pub fn parse(text: &str) -> Self { + let mut deny = Self::default(); + for line in text.lines() { + let entry = line.split('#').next().unwrap_or("").trim(); + if let Some(prefix) = entry.strip_suffix('*') { + deny.prefixes.push(prefix.to_owned()); + } else if !entry.is_empty() { + deny.exact.insert(entry.to_owned()); + } + } + deny + } + + /// Whether `feature` is denied. + pub fn matches(&self, feature: &str) -> bool { + self.exact.contains(feature) + || self + .prefixes + .iter() + .any(|p| feature.starts_with(p.as_str())) + } +} + +/// How an eligible test is run. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Mode { + /// Strict-mode directive plus harness, adapter rewrite applied. + Strict, + /// Source run unmodified (`raw` flag). + Raw, +} + +/// Why a test was not run. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum SkipReason { + /// Unsupported flag (`module`, `async`, `CanBlockIs*`). + Flag(String), + /// Sloppy-mode-only test. + NoStrict, + /// Denied feature. + Feature(String), + /// Harness include without a jsc shim. + Include(String), +} + +impl SkipReason { + /// Stable short label for reports, e.g. `feature:Symbol`. + pub fn label(&self) -> String { + match self { + Self::Flag(f) => format!("flag:{f}"), + Self::NoStrict => "noStrict".to_owned(), + Self::Feature(f) => format!("feature:{f}"), + Self::Include(i) => format!("include:{i}"), + } + } +} + +/// Classifies a test by its metadata. `shimmed` holds the harness file names +/// available to jsc (`assert.js` / `sta.js` are implicit and never listed). +pub fn classify( + meta: &Meta, + deny: &Denylist, + shimmed: &BTreeSet, +) -> Result { + let has = |flag: &str| meta.flags.iter().any(|f| f == flag); + for flag in ["module", "async", "CanBlockIsFalse", "CanBlockIsTrue"] { + if has(flag) { + return Err(SkipReason::Flag(flag.to_owned())); + } + } + if meta + .negative + .as_ref() + .is_some_and(|n| n.phase == "resolution") + { + return Err(SkipReason::Flag("module".to_owned())); + } + let raw = has("raw"); + if !raw && has("noStrict") { + return Err(SkipReason::NoStrict); + } + if let Some(f) = meta.features.iter().find(|f| deny.matches(f)) { + return Err(SkipReason::Feature(f.clone())); + } + if raw { + return Ok(Mode::Raw); + } + if let Some(i) = meta.includes.iter().find(|i| !shimmed.contains(i.as_str())) { + return Err(SkipReason::Include(i.clone())); + } + Ok(Mode::Strict) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::frontmatter::Negative; + + fn meta(flags: &[&str], features: &[&str], includes: &[&str]) -> Meta { + let v = |s: &[&str]| s.iter().map(|x| (*x).to_owned()).collect(); + Meta { + flags: v(flags), + features: v(features), + includes: v(includes), + negative: None, + } + } + + fn run(m: &Meta) -> Result { + let deny = Denylist::parse("# unsupported\nSymbol*\n\nProxy # exact\n"); + let shimmed = BTreeSet::from(["compareArray.js".to_owned()]); + classify(m, &deny, &shimmed) + } + + #[test] + fn flag_rules() { + for f in ["module", "async", "CanBlockIsFalse", "CanBlockIsTrue"] { + assert_eq!(run(&meta(&[f], &[], &[])), Err(SkipReason::Flag(f.into()))); + } + let mut m = meta(&[], &[], &[]); + m.negative = Some(Negative { + phase: "resolution".into(), + kind: "SyntaxError".into(), + }); + assert_eq!(run(&m).map_err(|r| r.label()), Err("flag:module".into())); + assert_eq!( + run(&meta(&["noStrict"], &[], &[])), + Err(SkipReason::NoStrict) + ); + } + + #[test] + fn modes() { + assert_eq!(run(&meta(&["onlyStrict"], &[], &[])), Ok(Mode::Strict)); + assert_eq!(run(&meta(&[], &[], &["compareArray.js"])), Ok(Mode::Strict)); + assert_eq!(run(&meta(&["raw", "noStrict"], &[], &[])), Ok(Mode::Raw)); + assert_eq!( + run(&meta(&["raw"], &["Symbol"], &[])), + Err(SkipReason::Feature("Symbol".into())) + ); + } + + #[test] + fn denylist_and_includes() { + assert_eq!( + run(&meta(&[], &["let", "Symbol.iterator"], &[])).map_err(|r| r.label()), + Err("feature:Symbol.iterator".into()) + ); + assert_eq!( + run(&meta(&[], &["Proxy"], &[])), + Err(SkipReason::Feature("Proxy".into())) + ); + assert_eq!(run(&meta(&[], &["Proxy.revocable"], &[])), Ok(Mode::Strict)); + assert_eq!( + run(&meta(&[], &[], &["compareArray.js", "propertyHelper.js"])).map_err(|r| r.label()), + Err("include:propertyHelper.js".into()) + ); + } +} diff --git a/crates/jsc-test262/src/frontmatter.rs b/crates/jsc-test262/src/frontmatter.rs new file mode 100644 index 0000000..b492844 --- /dev/null +++ b/crates/jsc-test262/src/frontmatter.rs @@ -0,0 +1,149 @@ +//! Hand-written parser for the subset of YAML used by test262 frontmatter +//! (`/*--- ... ---*/`). Only `features`, `flags`, `includes` and `negative` +//! are extracted; every other key (including `|` / `>` multi-line scalars) is +//! skipped. + +/// Expected failure declared by a test's `negative:` key. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct Negative { + /// `parse`, `early`, `resolution` or `runtime`. + pub phase: String, + /// Expected error constructor name (YAML key `type`). + pub kind: String, +} + +/// Metadata relevant to running a test. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct Meta { + /// `features:` entries. + pub features: Vec, + /// `flags:` entries. + pub flags: Vec, + /// `includes:` harness file names. + pub includes: Vec, + /// `negative:` expectation, if any. + pub negative: Option, +} + +/// Parses the frontmatter of a test source; returns `Meta::default()` when +/// there is none. +pub fn parse(source: &str) -> Meta { + let mut meta = Meta::default(); + let Some(start) = source.find("/*---") else { + return meta; + }; + let rest = &source[start + 5..]; + let Some(end) = rest.find("---*/") else { + return meta; + }; + let mut key = String::new(); + for line in rest[..end].lines() { + let line = line.trim_end_matches('\r'); + let text = strip_comment(line.trim()); + if text.is_empty() { + continue; + } + let top_level = !line.starts_with([' ', '\t']); + if top_level && !text.starts_with('-') { + let (k, value) = text.split_once(':').unwrap_or((text, "")); + k.trim().clone_into(&mut key); + let value = value.trim(); + if key == "negative" { + meta.negative = Some(Negative::default()); + } else if let (Some(list), Some(inner)) = ( + list_mut(&mut meta, &key), + value.strip_prefix('[').and_then(|v| v.strip_suffix(']')), + ) { + list.extend( + inner + .split(',') + .map(str::trim) + .filter(|s| !s.is_empty()) + .map(str::to_owned), + ); + } + } else if let Some(item) = text.strip_prefix('-') { + if let Some(list) = list_mut(&mut meta, &key) { + list.push(item.trim().to_owned()); + } + } else if let (Some(neg), Some((k, v))) = (meta.negative.as_mut(), text.split_once(':')) + && key == "negative" + { + match k.trim() { + "phase" => v.trim().clone_into(&mut neg.phase), + "type" => v.trim().clone_into(&mut neg.kind), + _ => {} + } + } + } + meta +} + +fn list_mut<'a>(meta: &'a mut Meta, key: &str) -> Option<&'a mut Vec> { + match key { + "features" => Some(&mut meta.features), + "flags" => Some(&mut meta.flags), + "includes" => Some(&mut meta.includes), + _ => None, + } +} + +/// Drops a `#` comment that starts the text or follows whitespace. +fn strip_comment(text: &str) -> &str { + let mut prev_space = true; + for (i, c) in text.char_indices() { + if c == '#' && prev_space { + return text[..i].trim_end(); + } + prev_space = c.is_whitespace(); + } + text +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn inline_lists() { + let m = parse("/*---\nfeatures: [Symbol, Proxy] # note\nflags: [onlyStrict]\n---*/\n"); + assert_eq!(m.features, ["Symbol", "Proxy"]); + assert_eq!(m.flags, ["onlyStrict"]); + } + + #[test] + fn block_list_and_crlf() { + let m = parse( + "/*---\r\nincludes:\r\n - compareArray.js\r\n - propertyHelper.js # c\r\nflags: [raw]\r\n---*/", + ); + assert_eq!(m.includes, ["compareArray.js", "propertyHelper.js"]); + assert_eq!(m.flags, ["raw"]); + } + + #[test] + fn negative_map() { + let m = parse("/*---\nnegative:\n phase: parse\n type: SyntaxError\nflags: [x]\n---*/"); + assert_eq!( + m.negative, + Some(Negative { + phase: "parse".into(), + kind: "SyntaxError".into() + }) + ); + assert_eq!(m.flags, ["x"]); + } + + #[test] + fn multi_line_scalars_skipped() { + let src = "/*---\ndescription: |\n - not an item\n phase: no\ninfo: >\n features: [Nope]\nfeatures: [Yes]\n---*/"; + let m = parse(src); + assert_eq!(m.features, ["Yes"]); + assert!(m.includes.is_empty() && m.negative.is_none()); + } + + #[test] + fn missing_frontmatter() { + assert_eq!(parse("assert(true);"), Meta::default()); + assert_eq!(parse("/*--- features: [a]"), Meta::default()); + } +} diff --git a/crates/jsc-test262/src/lib.rs b/crates/jsc-test262/src/lib.rs new file mode 100644 index 0000000..b145581 --- /dev/null +++ b/crates/jsc-test262/src/lib.rs @@ -0,0 +1,12 @@ +//! Core logic of the jsc test262 conformance runner: frontmatter parsing, +//! eligibility filtering, program assembly, outcome classification, baseline +//! diffing, test discovery and report aggregation. Process execution lives in +//! the binary. + +pub mod assemble; +pub mod baseline; +pub mod filter; +pub mod frontmatter; +pub mod outcome; +pub mod report; +pub mod walk; diff --git a/crates/jsc-test262/src/main.rs b/crates/jsc-test262/src/main.rs new file mode 100644 index 0000000..d0807d8 --- /dev/null +++ b/crates/jsc-test262/src/main.rs @@ -0,0 +1,8 @@ +//! `jsc-test262` command-line entry point. + +use std::process::ExitCode; + +fn main() -> ExitCode { + eprintln!("usage: jsc-test262 [FILTER...]"); + ExitCode::from(2) +} diff --git a/crates/jsc-test262/src/outcome.rs b/crates/jsc-test262/src/outcome.rs new file mode 100644 index 0000000..0760c85 --- /dev/null +++ b/crates/jsc-test262/src/outcome.rs @@ -0,0 +1,178 @@ +//! Maps a finished jsc process to a test outcome. +//! +//! Both exit-code conventions are accepted: compile errors exit 2 (or exit +//! nonzero with stderr starting `error:`), uncaught exceptions exit 1 with +//! `Uncaught` on stderr. + +use crate::frontmatter::Negative; + +/// How the jsc process ended. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Status { + /// Exited with this code. + Exited(i32), + /// Killed by a signal. + Signaled, + /// Killed by the runner's timeout. + TimedOut, +} + +/// Final classification of a run test. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum Outcome { + /// Behaved as expected. + Pass, + /// Behaved differently from expected. + Fail, + /// jsc rejected a program that should compile. + CompileError, + /// Exceeded the time limit. + Timeout, +} + +impl Outcome { + /// Stable short label for reports. + pub fn label(self) -> &'static str { + match self { + Self::Pass => "pass", + Self::Fail => "fail", + Self::CompileError => "compile-error", + Self::Timeout => "timeout", + } + } +} + +enum Verdict { + Ok, + CompileError, + Uncaught, + Timeout, + Other, +} + +fn verdict(status: Status, stderr: &str) -> Verdict { + match status { + Status::TimedOut => Verdict::Timeout, + Status::Exited(0) => Verdict::Ok, + Status::Exited(code) if code == 2 || stderr.trim_start().starts_with("error:") => { + Verdict::CompileError + } + Status::Exited(1) if stderr.contains("Uncaught") => Verdict::Uncaught, + Status::Signaled | Status::Exited(_) => Verdict::Other, + } +} + +/// Name of the uncaught error: the identifier right after the first +/// `Uncaught ` in stderr. +fn uncaught_name(stderr: &str) -> Option<&str> { + let rest = &stderr[stderr.find("Uncaught ")? + "Uncaught ".len()..]; + let end = rest + .find(|c: char| c == ':' || c.is_whitespace()) + .unwrap_or(rest.len()); + Some(&rest[..end]) +} + +/// Classifies a process result against the test's `negative` expectation. +pub fn classify(status: Status, stderr: &str, negative: Option<&Negative>) -> Outcome { + let v = verdict(status, stderr); + if let Verdict::Timeout = v { + return Outcome::Timeout; + } + match negative { + None => match v { + Verdict::Ok => Outcome::Pass, + Verdict::CompileError => Outcome::CompileError, + _ => Outcome::Fail, + }, + Some(n) if n.phase == "runtime" => match v { + Verdict::Uncaught if uncaught_name(stderr) == Some(n.kind.as_str()) => Outcome::Pass, + Verdict::CompileError => Outcome::CompileError, + _ => Outcome::Fail, + }, + Some(_) => match v { + Verdict::CompileError => Outcome::Pass, + _ => Outcome::Fail, + }, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn neg(phase: &str, kind: &str) -> Negative { + Negative { + phase: phase.into(), + kind: kind.into(), + } + } + + const UNCAUGHT: &str = "Error: Uncaught TypeError: x is not a function\n"; + + #[test] + fn positive() { + assert_eq!(classify(Status::Exited(0), "", None), Outcome::Pass); + assert_eq!( + classify(Status::Exited(1), "error: bad\n", None), + Outcome::CompileError + ); + assert_eq!( + classify(Status::Exited(2), "whatever", None), + Outcome::CompileError + ); + assert_eq!(classify(Status::Exited(1), UNCAUGHT, None), Outcome::Fail); + assert_eq!(classify(Status::Exited(101), "", None), Outcome::Fail); + assert_eq!(classify(Status::Signaled, "", None), Outcome::Fail); + assert_eq!(classify(Status::TimedOut, "", None), Outcome::Timeout); + assert_eq!(Outcome::CompileError.label(), "compile-error"); + } + + #[test] + fn negative_parse() { + for phase in ["parse", "early"] { + let n = neg(phase, "SyntaxError"); + assert_eq!( + classify(Status::Exited(1), "error: x", Some(&n)), + Outcome::Pass + ); + assert_eq!( + classify(Status::Exited(2), "error: x", Some(&n)), + Outcome::Pass + ); + assert_eq!(classify(Status::Exited(0), "", Some(&n)), Outcome::Fail); + assert_eq!( + classify(Status::Exited(1), UNCAUGHT, Some(&n)), + Outcome::Fail + ); + assert_eq!(classify(Status::TimedOut, "", Some(&n)), Outcome::Timeout); + } + } + + #[test] + fn negative_runtime() { + let n = neg("runtime", "TypeError"); + assert_eq!( + classify(Status::Exited(1), UNCAUGHT, Some(&n)), + Outcome::Pass + ); + assert_eq!( + classify(Status::Exited(1), "Uncaught TypeError", Some(&n)), + Outcome::Pass + ); + let mismatch = neg("runtime", "RangeError"); + assert_eq!( + classify(Status::Exited(1), UNCAUGHT, Some(&mismatch)), + Outcome::Fail + ); + assert_eq!( + classify(Status::Exited(1), "error: x", Some(&n)), + Outcome::CompileError + ); + assert_eq!( + classify(Status::Exited(2), "", Some(&n)), + Outcome::CompileError + ); + assert_eq!(classify(Status::Exited(0), "", Some(&n)), Outcome::Fail); + assert_eq!(classify(Status::TimedOut, "", Some(&n)), Outcome::Timeout); + } +} diff --git a/crates/jsc-test262/src/report.rs b/crates/jsc-test262/src/report.rs new file mode 100644 index 0000000..d74016b --- /dev/null +++ b/crates/jsc-test262/src/report.rs @@ -0,0 +1,167 @@ +//! Aggregation of per-test results into a per-directory summary. + +use std::collections::BTreeMap; +use std::fmt::Write as _; + +use crate::filter::SkipReason; +use crate::outcome::Outcome; + +/// Counts for one directory (first two path segments). +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct Row { + /// Directory key, e.g. `built-ins/Array`. + pub dir: String, + /// Passing tests. + pub pass: usize, + /// Tests that were run (not skipped). + pub eligible: usize, + /// All collected tests. + pub total: usize, +} + +/// Aggregated results. +#[derive(Debug, Clone, Default, PartialEq)] +pub struct Report { + /// Per-directory rows, sorted by directory. + pub rows: Vec, + /// Totals over all directories (`dir` is `OVERALL`). + pub overall: Row, + /// Count per outcome label. + pub outcomes: BTreeMap<&'static str, usize>, + /// Count per skip-reason label. + pub skips: BTreeMap, +} + +impl Report { + /// Builds a report from `(path, result)` records. + pub fn new(records: &[(String, Result)]) -> Self { + let mut dirs: BTreeMap = BTreeMap::new(); + let mut report = Self { + overall: Row { + dir: "OVERALL".to_owned(), + ..Row::default() + }, + ..Self::default() + }; + for (path, result) in records { + let segs: Vec<&str> = path.split('/').collect(); + let dir = segs[..segs.len().saturating_sub(1).min(2)].join("/"); + let row = dirs.entry(dir.clone()).or_insert_with(|| Row { + dir, + ..Row::default() + }); + for r in [&mut *row, &mut report.overall] { + r.total += 1; + r.eligible += usize::from(result.is_ok()); + r.pass += usize::from(*result == Ok(Outcome::Pass)); + } + match result { + Ok(o) => *report.outcomes.entry(o.label()).or_default() += 1, + Err(s) => *report.skips.entry(s.label()).or_default() += 1, + } + } + report.rows = dirs.into_values().collect(); + report + } +} + +#[expect(clippy::cast_precision_loss, reason = "test counts are far below 2^52")] +fn pct(n: usize, d: usize) -> f64 { + if d == 0 { + 0.0 + } else { + n as f64 * 100.0 / d as f64 + } +} + +/// Renders the report as a plain-text table followed by an `OVERALL` line. +pub fn render_table(report: &Report) -> String { + let width = report + .rows + .iter() + .map(|r| r.dir.len()) + .max() + .unwrap_or(0) + .max(9); + let mut out = format!( + "{:7} {:>8} {:>7} {:>7}\n", + "directory", "pass", "eligible", "total", "pass%" + ); + for r in &report.rows { + let _ = writeln!( + out, + "{:7} {:>8} {:>7} {:>6.1}%", + r.dir, + r.pass, + r.eligible, + r.total, + pct(r.pass, r.eligible) + ); + } + let o = &report.overall; + let _ = writeln!( + out, + "OVERALL: {} pass / {} eligible ({:.1}%) / {} total ({:.1}%)", + o.pass, + o.eligible, + pct(o.pass, o.eligible), + o.total, + pct(o.pass, o.total) + ); + out +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn counts_and_table() { + let records = vec![ + ("built-ins/Array/a.js".to_owned(), Ok(Outcome::Pass)), + ("built-ins/Array/sub/b.js".to_owned(), Ok(Outcome::Fail)), + ("built-ins/Array/c.js".to_owned(), Err(SkipReason::NoStrict)), + ("language/expressions/x/d.js".to_owned(), Ok(Outcome::Pass)), + ( + "language/e.js".to_owned(), + Err(SkipReason::Feature("Symbol".into())), + ), + ]; + let report = Report::new(&records); + let rows: Vec<_> = report + .rows + .iter() + .map(|r| (r.dir.as_str(), r.pass, r.eligible, r.total)) + .collect(); + assert_eq!( + rows, + [ + ("built-ins/Array", 1, 2, 3), + ("language", 0, 0, 1), + ("language/expressions", 1, 1, 1) + ] + ); + assert_eq!( + ( + report.overall.pass, + report.overall.eligible, + report.overall.total + ), + (2, 3, 5) + ); + assert_eq!(report.outcomes, BTreeMap::from([("fail", 1), ("pass", 2)])); + assert_eq!( + report.skips, + BTreeMap::from([("feature:Symbol".to_owned(), 1), ("noStrict".to_owned(), 1)]) + ); + let table = render_table(&report); + assert!( + table.contains("built-ins/Array 1 2 3 50.0%\n"), + "{table}" + ); + assert!( + table.ends_with("OVERALL: 2 pass / 3 eligible (66.7%) / 5 total (40.0%)\n"), + "{table}" + ); + } +} diff --git a/crates/jsc-test262/src/walk.rs b/crates/jsc-test262/src/walk.rs new file mode 100644 index 0000000..d556256 --- /dev/null +++ b/crates/jsc-test262/src/walk.rs @@ -0,0 +1,88 @@ +//! Discovery of test files. + +use std::fs; +use std::io; +use std::path::Path; + +const EXCLUDED_TOP: [&str; 3] = ["intl402", "annexB", "staging"]; + +/// Collects test files under `test_root` (the test262 `test/` directory) as +/// sorted `/`-separated paths relative to it. Skips `intl402/`, `staging/`, +/// any `annexB` segment, `_FIXTURE` files and non-`.js` files; a non-empty +/// `filter` keeps only paths starting with one of its prefixes. +pub fn collect(test_root: &Path, filter: &[String]) -> io::Result> { + let mut out = Vec::new(); + visit(test_root, "", &mut out)?; + out.retain(|p| filter.is_empty() || filter.iter().any(|f| p.starts_with(f.as_str()))); + out.sort(); + Ok(out) +} + +fn visit(dir: &Path, rel: &str, out: &mut Vec) -> io::Result<()> { + for entry in fs::read_dir(dir)? { + let entry = entry?; + let Ok(name) = entry.file_name().into_string() else { + continue; + }; + let path = if rel.is_empty() { + name.clone() + } else { + format!("{rel}/{name}") + }; + if entry.file_type()?.is_dir() { + if name == "annexB" || (rel.is_empty() && EXCLUDED_TOP.contains(&name.as_str())) { + continue; + } + visit(&entry.path(), &path, out)?; + } else if Path::new(&name).extension().is_some_and(|e| e == "js") + && !name.contains("_FIXTURE") + { + out.push(path); + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn excludes_and_filters() -> io::Result<()> { + let root = std::env::temp_dir().join(format!("jsc-test262-walk-{}", std::process::id())); + let files = [ + "built-ins/Array/a.js", + "built-ins/Array/b.txt", + "built-ins/Array/c_FIXTURE.js", + "built-ins/RegExp/annexB/d.js", + "language/expressions/e.js", + "intl402/f.js", + "annexB/g.js", + "staging/h.js", + "language/staging/i.js", + ]; + for f in files { + let p = root.join(f); + if let Some(parent) = p.parent() { + fs::create_dir_all(parent)?; + } + fs::write(p, "")?; + } + let all = collect(&root, &[]); + let filtered = collect(&root, &["language/".to_owned()]); + fs::remove_dir_all(&root)?; + assert_eq!( + all?, + [ + "built-ins/Array/a.js", + "language/expressions/e.js", + "language/staging/i.js" + ] + ); + assert_eq!( + filtered?, + ["language/expressions/e.js", "language/staging/i.js"] + ); + Ok(()) + } +} From 3fd94946d79e114ce609f7b5cbd62912b5158cd7 Mon Sep 17 00:00:00 2001 From: wudi Date: Sun, 18 Oct 2026 04:49:19 +0000 Subject: [PATCH 05/44] test262: pinned fetch script, jsc harness shims and feature denylist --- .gitignore | 3 + scripts/test262-fetch.sh | 22 ++++ tests/test262/harness/assert.js | 162 +++++++++++++++++++++++++ tests/test262/harness/compareArray.js | 38 ++++++ tests/test262/harness/sta.js | 18 +++ tests/test262/unsupported-features.txt | 78 ++++++++++++ 6 files changed, 321 insertions(+) create mode 100755 scripts/test262-fetch.sh create mode 100644 tests/test262/harness/assert.js create mode 100644 tests/test262/harness/compareArray.js create mode 100644 tests/test262/harness/sta.js create mode 100644 tests/test262/unsupported-features.txt 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/scripts/test262-fetch.sh b/scripts/test262-fetch.sh new file mode 100755 index 0000000..4081259 --- /dev/null +++ b/scripts/test262-fetch.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Fetch tc39/test262 at a pinned commit into /.tmp/test262 (idempotent). +set -euo pipefail + +TEST262_SHA=2e0a56762801e275a9fdf96dc49d90ba0cddcf63 +TEST262_URL=https://github.com/tc39/test262.git + +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +dest="$root/.tmp/test262" + +if [ -d "$dest/.git" ] && [ "$(git -C "$dest" rev-parse HEAD 2>/dev/null || true)" = "$TEST262_SHA" ]; then + echo "test262 already at $TEST262_SHA in $dest" + exit 0 +fi + +rm -rf "$dest" +mkdir -p "$dest" +git -C "$dest" init -q +git -C "$dest" remote add origin "$TEST262_URL" +git -C "$dest" fetch -q --depth 1 origin "$TEST262_SHA" +git -C "$dest" -c advice.detachedHead=false checkout -q --detach FETCH_HEAD +git -C "$dest" rev-parse HEAD diff --git a/tests/test262/harness/assert.js b/tests/test262/harness/assert.js new file mode 100644 index 0000000..d16ac7e --- /dev/null +++ b/tests/test262/harness/assert.js @@ -0,0 +1,162 @@ +// jsc-compatible replacement for upstream test262 harness/assert.js. +// jsc functions cannot carry properties yet, so `assert.` helpers are +// top-level functions named `assert_`; the runner rewrites every +// `assert.(` call in the test source accordingly. +// jsc also rejects calling a top-level function that references a top-level +// binding (here: the Test262Error class) from another top-level function, so +// no helper that throws Test262Error calls another such helper; shared logic +// lives in helpers that only touch their arguments. + +function isNegativeZero(value) { + return value === 0 && 1 / value === -Infinity; +} + +function isPrimitive(value) { + return !value || (typeof value !== "object" && typeof value !== "function"); +} + +function assert__toString(value) { + if (value === null) { + return "null"; + } + if (typeof value === "string") { + return '"' + value + '"'; + } + if (isNegativeZero(value)) { + return "-0"; + } + return String(value); +} + +function assert(mustBeTrue, message) { + if (mustBeTrue === true) { + return; + } + if (message === undefined) { + message = "Expected true but got " + assert__toString(mustBeTrue); + } + throw new Test262Error(message); +} + +function assert__isSameValue(a, b) { + if (a === b) { + // Handle +/-0 vs. -/+0 + return a !== 0 || 1 / a === 1 / b; + } + // Handle NaN vs. NaN + return a !== a && b !== b; +} + +function assert_sameValue(actual, expected, message) { + if (assert__isSameValue(actual, expected)) { + return; + } + if (message === undefined) { + message = ""; + } else { + message += " "; + } + message += "Expected SameValue(«" + assert__toString(actual) + "», «" + assert__toString(expected) + "») to be true"; + throw new Test262Error(message); +} + +function assert_notSameValue(actual, unexpected, message) { + if (!assert__isSameValue(actual, unexpected)) { + return; + } + if (message === undefined) { + message = ""; + } else { + message += " "; + } + message += "Expected SameValue(«" + assert__toString(actual) + "», «" + assert__toString(unexpected) + "») to be false"; + throw new Test262Error(message); +} + +function assert__throwsMessage(message) { + return message === undefined ? "" : message + " "; +} + +// Runs func and returns `{ failure, thrown }`: `failure` is "" when func threw +// an object, otherwise the upstream failure text for `name`. +function assert__runThrows(func, name) { + if (typeof func !== "function") { + return { failure: "assert.throws requires two arguments: the error constructor and a function to run", thrown: undefined }; + } + let threw = false; + let thrown; + try { + func(); + } catch (e) { + threw = true; + thrown = e; + } + if (!threw) { + return { failure: "Expected a " + name + " to be thrown but no exception was thrown at all", thrown: undefined }; + } + if (typeof thrown !== "object" || thrown === null) { + return { failure: "Thrown value was not an object!", thrown: thrown }; + } + return { failure: "", thrown: thrown }; +} + +// Same check as upstream (`thrown.constructor` identity). jsc does not yet +// expose `.constructor`, and builtin error constructors are not first-class +// values, so on jsc this only works once those land; until then use the +// `assert_throws_` variants below. +function assert_throws(expectedErrorConstructor, func, message) { + let r = assert__runThrows(func, expectedErrorConstructor.name); + if (r.failure !== "") { + throw new Test262Error(assert__throwsMessage(message) + r.failure); + } + if (r.thrown.constructor !== expectedErrorConstructor) { + throw new Test262Error(assert__throwsMessage(message) + "Expected a " + expectedErrorConstructor.name + " but got a " + r.thrown.constructor.name); + } +} + +// Variants of `assert.throws(, func, message)` for the constructors jsc +// can check today: a runner may rewrite `assert.throws(,` to +// `assert_throws_(`. jsc's `instanceof` only accepts a literal user +// class or Error/TypeError/RangeError on its right-hand side, so each variant +// names its constructor directly. Builtin variants also compare `.name` so a +// TypeError/RangeError does not satisfy an expected plain Error, keeping +// upstream's exact-constructor semantics. +function assert_throws_Test262Error(func, message) { + let r = assert__runThrows(func, "Test262Error"); + if (r.failure !== "") { + throw new Test262Error(assert__throwsMessage(message) + r.failure); + } + if (!(r.thrown instanceof Test262Error)) { + throw new Test262Error(assert__throwsMessage(message) + "Expected a Test262Error but got a " + r.thrown.name); + } +} + +function assert_throws_Error(func, message) { + let r = assert__runThrows(func, "Error"); + if (r.failure !== "") { + throw new Test262Error(assert__throwsMessage(message) + r.failure); + } + if (!(r.thrown instanceof Error) || r.thrown.name !== "Error") { + throw new Test262Error(assert__throwsMessage(message) + "Expected a Error but got a " + r.thrown.name); + } +} + +function assert_throws_TypeError(func, message) { + let r = assert__runThrows(func, "TypeError"); + if (r.failure !== "") { + throw new Test262Error(assert__throwsMessage(message) + r.failure); + } + if (!(r.thrown instanceof TypeError) || r.thrown.name !== "TypeError") { + throw new Test262Error(assert__throwsMessage(message) + "Expected a TypeError but got a " + r.thrown.name); + } +} + +function assert_throws_RangeError(func, message) { + let r = assert__runThrows(func, "RangeError"); + if (r.failure !== "") { + throw new Test262Error(assert__throwsMessage(message) + r.failure); + } + if (!(r.thrown instanceof RangeError) || r.thrown.name !== "RangeError") { + throw new Test262Error(assert__throwsMessage(message) + "Expected a RangeError but got a " + r.thrown.name); + } +} diff --git a/tests/test262/harness/compareArray.js b/tests/test262/harness/compareArray.js new file mode 100644 index 0000000..5f4ce84 --- /dev/null +++ b/tests/test262/harness/compareArray.js @@ -0,0 +1,38 @@ +// jsc-compatible replacement for upstream test262 harness/compareArray.js +// (upstream now defines these in assert.js). jsc functions cannot carry +// properties yet, so `assert.compareArray` is `assert_compareArray` and +// `compareArray.format` is the top-level `compareArray_format`. Failures throw +// Test262Error directly instead of calling `assert` (see assert.js). + +function compareArray(a, b) { + if (b.length !== a.length) { + return false; + } + for (let i = 0; i < a.length; i++) { + if (!assert__isSameValue(b[i], a[i])) { + return false; + } + } + return true; +} + +function compareArray_format(arrayLike) { + let parts = []; + for (let i = 0; i < arrayLike.length; i++) { + parts.push(String(arrayLike[i])); + } + return "[" + parts.join(", ") + "]"; +} + +function assert_compareArray(actual, expected, message) { + message = message === undefined ? "" : message; + if (isPrimitive(actual)) { + throw new Test262Error("Actual argument [" + actual + "] shouldn't be primitive. " + String(message)); + } else if (isPrimitive(expected)) { + throw new Test262Error("Expected argument [" + expected + "] shouldn't be primitive. " + String(message)); + } + if (compareArray(actual, expected)) { + return; + } + throw new Test262Error("Actual " + compareArray_format(actual) + " and expected " + compareArray_format(expected) + " should have the same contents. " + String(message)); +} diff --git a/tests/test262/harness/sta.js b/tests/test262/harness/sta.js new file mode 100644 index 0000000..322083a --- /dev/null +++ b/tests/test262/harness/sta.js @@ -0,0 +1,18 @@ +// jsc-compatible replacement for upstream test262 harness/sta.js. +// Upstream defines Test262Error as a plain-function constructor with +// properties on the function (Test262Error.thrower); jsc supports neither +// yet, so Test262Error is a class and `thrower` is omitted. + +class Test262Error { + constructor(message) { + this.message = message || ""; + } + + toString() { + return "Test262Error: " + this.message; + } +} + +function $DONOTEVALUATE() { + throw "Test262: This statement should not be evaluated."; +} diff --git a/tests/test262/unsupported-features.txt b/tests/test262/unsupported-features.txt new file mode 100644 index 0000000..308492f --- /dev/null +++ b/tests/test262/unsupported-features.txt @@ -0,0 +1,78 @@ +# test262 features jsc certainly does not implement; tests tagged with any of +# them are skipped. One feature name per line (see test262 features.txt); +# a trailing `*` is a prefix match. Only list a feature when it is entirely +# missing (probed against jsc); partially working features stay enabled so +# their failures are counted. Remove entries as support lands. + +# Symbols (no `Symbol` global; well-known symbols are unavailable) +Symbol* +symbols-as-weakmap-keys + +# Proxy / Reflect (no globals) +Proxy +proxy-missing-checks +Reflect* + +# BigInt (literals rejected at compile time) +BigInt + +# Regular expressions (literals and the RegExp constructor are rejected) +regexp-* +RegExp.escape +legacy-regexp + +# Binary data: typed arrays, buffers, DataView, Atomics (no constructors) +TypedArray* +Int8Array +Int16Array +Int32Array +Uint8Array +Uint8ClampedArray +Uint16Array +Uint32Array +Float16Array +Float32Array +Float64Array +uint8array-base64 +stable-typedarray-sort +ArrayBuffer +arraybuffer-transfer +resizable-arraybuffer +immutable-arraybuffer +align-detached-buffer-semantics-with-web-reality +DataView* +SharedArrayBuffer +Atomics* + +# Weak references and GC-observable behaviour (no constructors / no host hook) +WeakMap +WeakSet +WeakRef +FinalizationRegistry +host-gc-required + +# Temporal and ECMA-402 (no globals) +Temporal +canonical-tz +Intl* +intl-normative-optional + +# Realms and host hooks jsc has no notion of +ShadowRealm +cross-realm +IsHTMLDDA + +# Not implemented by design or not yet attempted +tail-call-optimization +decorators +explicit-resource-management + +# Module loading extensions (jsc compiles a single script; no import()) +dynamic-import +import-attributes +json-modules +source-phase-imports* +import-defer +export-defer +import-text +import-bytes From 71f76c88761b215ef04a59563627127b6e46590b Mon Sep 17 00:00:00 2001 From: wudi Date: Sun, 18 Oct 2026 04:50:06 +0000 Subject: [PATCH 06/44] cli: node-like run/-p/build modes with exit codes 2/1 - jsc FILE / -e SRC run and print only console output - -p/--print SRC prints the completion value (node -p) - jsc build FILE -o OUT writes the module (-o - for stdout) - compile errors exit 2, uncaught/runtime failures exit 1 - remove --emit; convert integration suites to the new interface --- crates/jsc/src/main.rs | 170 ++++++++++++++------- crates/jsc/tests/arrays.rs | 124 ++++++++-------- crates/jsc/tests/builtins.rs | 174 ++++++++++------------ crates/jsc/tests/classes.rs | 174 +++++++++++----------- crates/jsc/tests/cli_modes.rs | 239 ++++++++++++++++++++++-------- crates/jsc/tests/closures.rs | 18 +-- crates/jsc/tests/collections.rs | 58 ++++---- crates/jsc/tests/control_flow.rs | 60 ++++---- crates/jsc/tests/destructuring.rs | 24 +-- crates/jsc/tests/errors.rs | 68 ++++----- crates/jsc/tests/functions.rs | 78 +++++----- crates/jsc/tests/numbers.rs | 76 +++++----- crates/jsc/tests/objects.rs | 219 +++++++++++++-------------- crates/jsc/tests/operators.rs | 210 +++++++++++++------------- crates/jsc/tests/regression.rs | 205 ++++++++++++------------- crates/jsc/tests/strings.rs | 62 ++++---- 16 files changed, 1058 insertions(+), 901 deletions(-) diff --git a/crates/jsc/src/main.rs b/crates/jsc/src/main.rs index 73d2e95..0b7ba05 100644 --- a/crates/jsc/src/main.rs +++ b/crates/jsc/src/main.rs @@ -1,84 +1,142 @@ #![forbid(unsafe_code)] //! `jsc` command-line entry point. //! -//! A thin [`clap`] shell over [`jsc_core`]. It reads a JavaScript source string -//! either inline (`-e`) or from a file, then either runs it (`--emit run`, -//! the default) and prints the result value, or emits the raw WebAssembly -//! module bytes to stdout (`--emit wasm`). +//! A thin [`clap`] shell over [`jsc_core`] with node-like modes: +//! +//! - `jsc FILE` / `jsc -e SRC` — compile and run; only `console` output is +//! printed. +//! - `jsc -p SRC` — compile and run, then print the completion value. +//! - `jsc build FILE -o OUT.wasm` — emit the WebAssembly module (`-o -` writes +//! to stdout). +//! +//! Exit codes: `0` success, `1` uncaught exception / runtime or I/O failure, +//! `2` compile error. use std::io::Write as _; +use std::process::ExitCode; -use anyhow::anyhow; -use clap::{Parser, ValueEnum}; +use clap::{Parser, Subcommand}; -/// Compile and run a numeric JavaScript expression. +/// Compile JavaScript/TypeScript to WebAssembly and run it, node-style. #[derive(Parser)] #[command(name = "jsc", version, about)] +#[command(args_conflicts_with_subcommands = true, subcommand_negates_reqs = true)] #[command(group( - clap::ArgGroup::new("source").required(true).args(["eval", "file"]) + clap::ArgGroup::new("source").required(true).args(["eval", "print", "file"]) ))] struct Cli { - /// Evaluate an inline expression string. - #[arg(short, long, value_name = "EXPR", allow_hyphen_values = true)] + #[command(subcommand)] + command: Option, + + /// Run an inline source string, printing only console output. + #[arg(short, long, value_name = "SRC", allow_hyphen_values = true)] eval: Option, - /// Read JavaScript source from a file. + /// Run an inline source string and print its completion value. + #[arg(short, long, value_name = "SRC", allow_hyphen_values = true)] + print: Option, + + /// Run a JavaScript source file, printing only console output. #[arg(value_name = "FILE")] file: Option, +} + +#[derive(Subcommand)] +enum Command { + /// Compile FILE to a WebAssembly module. + Build { + /// JavaScript source file to compile. + #[arg(value_name = "FILE")] + file: String, - /// What to produce: run the program or emit the WebAssembly module. - #[arg(long, value_enum, default_value_t = Emit::Run)] - emit: Emit, + /// Output path for the module bytes; `-` writes to stdout. + #[arg(short, long, value_name = "OUT", allow_hyphen_values = true)] + output: String, + }, } -/// Output mode selected by `--emit`. -#[derive(Clone, Copy, ValueEnum)] -enum Emit { - /// Execute the program and print the numeric result. - Run, - /// Write the raw WebAssembly module bytes to stdout. - Wasm, +/// A failed CLI run, carrying the process exit code it maps to. +enum Failure { + /// Parse or lower failed (exit 2). + Compile(anyhow::Error), + /// Uncaught exception, runtime trap, or I/O failure (exit 1). + Runtime(anyhow::Error), } -fn main() -> anyhow::Result<()> { +fn main() -> ExitCode { let cli = Cli::parse(); - - // `label` names the source in diagnostics: `` for inline `-e` input, - // the real path for a file. - let (src, label) = match (cli.eval, cli.file) { - (Some(expr), None) => (expr, "".to_owned()), - (None, Some(path)) => (std::fs::read_to_string(&path)?, path), - // The `source` ArgGroup guarantees exactly one is present. - _ => return Err(anyhow!("exactly one of --eval or FILE must be provided")), + let result = match cli.command { + Some(Command::Build { file, output }) => build(&file, &output), + None => run(cli.eval, cli.print, cli.file), }; - - match cli.emit { - Emit::Run => match jsc_core::run::compile_and_run(&src) { - Ok(value) => println!("{}", jsc_core::run::format_value(&value)), - Err(err) => { - // A spanned CompileError renders a source-pointing caret block; - // span-less or other errors fall through to anyhow's default - // message-and-nonzero-exit behavior (unchanged). - let rendered = err - .downcast_ref::() - .and_then(|ce| ce.render(&src, &label)); - if let Some(block) = rendered { - eprintln!("{block}"); - std::process::exit(1); - } - return Err(err); - } - }, - Emit::Wasm => { - let program = jsc_core::frontend::parse(&src)?; - let ir = jsc_core::codegen::lower(&program)?; - let wasm = jsc_core::emit::emit(&ir); - let stdout = std::io::stdout(); - let mut handle = stdout.lock(); - handle.write_all(&wasm)?; - handle.flush()?; + match result { + Ok(()) => ExitCode::SUCCESS, + Err(Failure::Compile(err)) => { + eprintln!("{err:#}"); + ExitCode::from(2) + } + Err(Failure::Runtime(err)) => { + eprintln!("{err:#}"); + ExitCode::from(1) } } +} +/// Run the `-e` / `-p` / FILE form. The `source` `ArgGroup` guarantees exactly one +/// source is present. +fn run(eval: Option, print: Option, file: Option) -> Result<(), Failure> { + // `label` names the source in diagnostics: `` for inline input, the + // real path for a file. + let (src, label, print_value) = match (eval, print, file) { + (Some(src), None, None) => (src, "".to_owned(), false), + (None, Some(src), None) => (src, "".to_owned(), true), + (None, None, Some(path)) => (read_source(&path)?, path, false), + _ => { + return Err(Failure::Runtime(anyhow::anyhow!( + "exactly one of --eval, --print or FILE must be provided" + ))); + } + }; + let wasm = compile(&src, &label)?; + // `console.log` streams to the real stdout while the module runs. + let (bits, memory) = jsc_core::run::run(&wasm).map_err(Failure::Runtime)?; + if print_value { + let value = jsc_core::run::decode(bits, &memory); + println!("{}", jsc_core::run::format_value(&value)); + } Ok(()) } + +/// `jsc build FILE -o OUT`: write the module bytes to OUT (`-` = stdout). +fn build(path: &str, output: &str) -> Result<(), Failure> { + let src = read_source(path)?; + let wasm = compile(&src, path)?; + let written = if output == "-" { + let mut handle = std::io::stdout().lock(); + handle.write_all(&wasm).and_then(|()| handle.flush()) + } else { + std::fs::write(output, &wasm) + }; + written.map_err(|err| Failure::Runtime(anyhow::anyhow!("cannot write {output}: {err}"))) +} + +fn read_source(path: &str) -> Result { + std::fs::read_to_string(path) + .map_err(|err| Failure::Runtime(anyhow::anyhow!("cannot read {path}: {err}"))) +} + +/// Parse, lower, and emit `src`. A spanned `CompileError` is replaced by its +/// rendered caret block (labelled `label`); a span-less one keeps its message. +fn compile(src: &str, label: &str) -> Result, Failure> { + let lowered = + jsc_core::frontend::parse(src).and_then(|program| jsc_core::codegen::lower(&program)); + match lowered { + Ok(ir) => Ok(jsc_core::emit::emit(&ir)), + Err(err) => { + let rendered = err + .downcast_ref::() + .and_then(|ce| ce.render(src, label)); + Err(Failure::Compile(rendered.map_or(err, anyhow::Error::msg))) + } + } +} diff --git a/crates/jsc/tests/arrays.rs b/crates/jsc/tests/arrays.rs index e9fbae1..c067e5e 100644 --- a/crates/jsc/tests/arrays.rs +++ b/crates/jsc/tests/arrays.rs @@ -3,32 +3,32 @@ mod common; use common::jsc; -/// The S11 array milestones run inline with `-e` through the built binary. +/// The S11 array milestones run inline with `-p` through the built binary. /// /// Covers the array wave end to end: element read + sum, `.length`, `typeof`, /// a `while`-driven sum over `.length`, and the empty array. #[test] fn array_milestones_via_eval() { - assert_eq!(jsc(&["-e", "let a=[1,2,3]; a[0]+a[2]"]), "4"); - assert_eq!(jsc(&["-e", "[1,2,3].length"]), "3"); - assert_eq!(jsc(&["-e", "typeof []"]), "object"); + assert_eq!(jsc(&["-p", "let a=[1,2,3]; a[0]+a[2]"]), "4"); + assert_eq!(jsc(&["-p", "[1,2,3].length"]), "3"); + assert_eq!(jsc(&["-p", "typeof []"]), "object"); assert_eq!( jsc(&[ - "-e", + "-p", "let a=[1,2,3,4]; let i=0; let s=0; while(i x*2)[2]"]), "6"); - assert_eq!(jsc(&["-e", "[1,2,3,4].filter(x => x > 2).length"]), "2"); - assert_eq!(jsc(&["-e", "[1,2,3,4].reduce((a,b) => a+b, 0)"]), "10"); + assert_eq!(jsc(&["-p", "[1,2,3].map(x => x*2)[2]"]), "6"); + assert_eq!(jsc(&["-p", "[1,2,3,4].filter(x => x > 2).length"]), "2"); + assert_eq!(jsc(&["-p", "[1,2,3,4].reduce((a,b) => a+b, 0)"]), "10"); assert_eq!( - jsc(&["-e", "let s=0; [1,2,3,4].forEach(x => { s = s + x }); s"]), + jsc(&["-p", "let s=0; [1,2,3,4].forEach(x => { s = s + x }); s"]), "10", ); - assert_eq!(jsc(&["-e", "[1,2,3].map(x => x+1).length"]), "3"); + assert_eq!(jsc(&["-p", "[1,2,3].map(x => x+1).length"]), "3"); // The transformed array prints Node-console style. - assert_eq!(jsc(&["-e", "[1,2,3].map(x => x*2)"]), "[ 2, 4, 6 ]"); + assert_eq!(jsc(&["-p", "[1,2,3].map(x => x*2)"]), "[ 2, 4, 6 ]"); // Chaining: map result feeds filter. assert_eq!( - jsc(&["-e", "[1,2,3,4].map(x => x*2).filter(x => x > 4).length"]), + jsc(&["-p", "[1,2,3,4].map(x => x*2).filter(x => x > 4).length"]), "2", ); // Bound-variable receiver, closure-capturing callback. - assert_eq!(jsc(&["-e", "let k=10; [1,2,3].map(x => x + k)[0]"]), "11"); + assert_eq!(jsc(&["-p", "let k=10; [1,2,3].map(x => x + k)[0]"]), "11"); // forEach evaluates to undefined. - assert_eq!(jsc(&["-e", "[1,2,3].forEach(x => x)"]), "undefined"); + assert_eq!(jsc(&["-p", "[1,2,3].forEach(x => x)"]), "undefined"); } /// S14.6 — non-callback array/string methods end-to-end through the CLI: @@ -82,22 +82,22 @@ fn s14_array_higher_order_via_eval() { /// overloaded `indexOf`/`slice` dispatched on the receiver's runtime type. #[test] fn s14_array_and_string_methods_via_eval() { - assert_eq!(jsc(&["-e", "[10,20,30].indexOf(20)"]), "1"); - assert_eq!(jsc(&["-e", "[10,20,30].indexOf(99)"]), "-1"); - assert_eq!(jsc(&["-e", "[1,2,3].includes(2)"]), "true"); - assert_eq!(jsc(&["-e", "[1,2,3].includes(9)"]), "false"); - assert_eq!(jsc(&["-e", "[1,2,3].join(\"-\")"]), "1-2-3"); - assert_eq!(jsc(&["-e", "[1,2,3].join()"]), "1,2,3"); - assert_eq!(jsc(&["-e", "[1,2,3,4].slice(1,3)"]), "[ 2, 3 ]"); - assert_eq!(jsc(&["-e", "[1,2,3,4].slice(1,3).length"]), "2"); - assert_eq!(jsc(&["-e", "\"abc\".toUpperCase()"]), "ABC"); - assert_eq!(jsc(&["-e", "\"ABC\".toLowerCase()"]), "abc"); - assert_eq!(jsc(&["-e", "\"HELLO\".slice(1,3)"]), "EL"); - assert_eq!(jsc(&["-e", "\"hello\".indexOf(\"ll\")"]), "2"); - assert_eq!(jsc(&["-e", "\"hello\".indexOf(\"z\")"]), "-1"); - assert_eq!(jsc(&["-e", "\"abc\".charAt(1)"]), "b"); + assert_eq!(jsc(&["-p", "[10,20,30].indexOf(20)"]), "1"); + assert_eq!(jsc(&["-p", "[10,20,30].indexOf(99)"]), "-1"); + assert_eq!(jsc(&["-p", "[1,2,3].includes(2)"]), "true"); + assert_eq!(jsc(&["-p", "[1,2,3].includes(9)"]), "false"); + assert_eq!(jsc(&["-p", "[1,2,3].join(\"-\")"]), "1-2-3"); + assert_eq!(jsc(&["-p", "[1,2,3].join()"]), "1,2,3"); + assert_eq!(jsc(&["-p", "[1,2,3,4].slice(1,3)"]), "[ 2, 3 ]"); + assert_eq!(jsc(&["-p", "[1,2,3,4].slice(1,3).length"]), "2"); + assert_eq!(jsc(&["-p", "\"abc\".toUpperCase()"]), "ABC"); + assert_eq!(jsc(&["-p", "\"ABC\".toLowerCase()"]), "abc"); + assert_eq!(jsc(&["-p", "\"HELLO\".slice(1,3)"]), "EL"); + assert_eq!(jsc(&["-p", "\"hello\".indexOf(\"ll\")"]), "2"); + assert_eq!(jsc(&["-p", "\"hello\".indexOf(\"z\")"]), "-1"); + assert_eq!(jsc(&["-p", "\"abc\".charAt(1)"]), "b"); // Chaining across array + string methods. - assert_eq!(jsc(&["-e", "[1,2,3,4].slice(1,3).join(\"-\")"]), "2-3"); + assert_eq!(jsc(&["-p", "[1,2,3,4].slice(1,3).join(\"-\")"]), "2-3"); } /// P1.5 — spread `...` in array literals through the CLI: a spread source's @@ -105,20 +105,20 @@ fn s14_array_and_string_methods_via_eval() { /// result length and indexing are only known after the build. #[test] fn p1_5_array_spread_via_eval() { - assert_eq!(jsc(&["-e", "let a=[1,2]; [...a,3].length"]), "3"); - assert_eq!(jsc(&["-e", "let a=[1,2],b=[3]; [...a,...b][2]"]), "3"); - assert_eq!(jsc(&["-e", "[0,...[1,2],9][3]"]), "9"); + assert_eq!(jsc(&["-p", "let a=[1,2]; [...a,3].length"]), "3"); + assert_eq!(jsc(&["-p", "let a=[1,2],b=[3]; [...a,...b][2]"]), "3"); + assert_eq!(jsc(&["-p", "[0,...[1,2],9][3]"]), "9"); // A leading/trailing plain element around a spread, and a bare spread. - assert_eq!(jsc(&["-e", "[0,...[1,2],9].length"]), "4"); - assert_eq!(jsc(&["-e", "let a=[5,6,7]; [...a].length"]), "3"); - assert_eq!(jsc(&["-e", "let a=[1,2],b=[3,4]; [...a,...b].length"]), "4"); + assert_eq!(jsc(&["-p", "[0,...[1,2],9].length"]), "4"); + assert_eq!(jsc(&["-p", "let a=[5,6,7]; [...a].length"]), "3"); + assert_eq!(jsc(&["-p", "let a=[1,2],b=[3,4]; [...a,...b].length"]), "4"); // Spread feeds an ordinary array operation. - assert_eq!(jsc(&["-e", "[0,...[1,2],9][0]"]), "0"); - assert_eq!(jsc(&["-e", "let a=[1,2]; [...a,3,...a].length"]), "5"); + assert_eq!(jsc(&["-p", "[0,...[1,2],9][0]"]), "0"); + assert_eq!(jsc(&["-p", "let a=[1,2]; [...a,3,...a].length"]), "5"); // Math.max over a spread argument (variadic built-in). - assert_eq!(jsc(&["-e", "Math.max(...[1,9,3])"]), "9"); - assert_eq!(jsc(&["-e", "Math.min(...[4,2,8])"]), "2"); - assert_eq!(jsc(&["-e", "Math.max(1,...[9,3],7)"]), "9"); + assert_eq!(jsc(&["-p", "Math.max(...[1,9,3])"]), "9"); + assert_eq!(jsc(&["-p", "Math.min(...[4,2,8])"]), "2"); + assert_eq!(jsc(&["-p", "Math.max(1,...[9,3],7)"]), "9"); } /// Spreading a STRING into an array literal explodes it into its characters @@ -127,15 +127,15 @@ fn p1_5_array_spread_via_eval() { /// for-of / indexing over a string. #[test] fn string_spread_explodes_chars() { - assert_eq!(jsc(&["-e", "[...\"abc\"].length"]), "3"); - assert_eq!(jsc(&["-e", "[...\"abc\"][0]"]), "a"); - assert_eq!(jsc(&["-e", "[...\"abc\"][2]"]), "c"); - assert_eq!(jsc(&["-e", "[...\"\"].length"]), "0"); + assert_eq!(jsc(&["-p", "[...\"abc\"].length"]), "3"); + assert_eq!(jsc(&["-p", "[...\"abc\"][0]"]), "a"); + assert_eq!(jsc(&["-p", "[...\"abc\"][2]"]), "c"); + assert_eq!(jsc(&["-p", "[...\"\"].length"]), "0"); // Interleaved with plain elements and an array spread. - assert_eq!(jsc(&["-e", "let a=[1,2]; [0,...a,...\"xy\"].length"]), "5"); - assert_eq!(jsc(&["-e", "[0,...\"xy\",9][2]"]), "y"); + assert_eq!(jsc(&["-p", "let a=[1,2]; [0,...a,...\"xy\"].length"]), "5"); + assert_eq!(jsc(&["-p", "[0,...\"xy\",9][2]"]), "y"); // A spread string feeds an ordinary array operation. - assert_eq!(jsc(&["-e", "[...\"hi\"].join(\"-\")"]), "h-i"); + assert_eq!(jsc(&["-p", "[...\"hi\"].join(\"-\")"]), "h-i"); } /// P2.2b regression: an element WRITE with a computed RHS, and a computed-index @@ -145,7 +145,7 @@ fn string_spread_explodes_chars() { #[test] fn p2_2b_index_computed_rhs_and_index_no_clobber() { // Computed RHS on an element store. - assert_eq!(jsc(&["-e", "let a=[0,0]; a[1]=3+4; a[1]"]), "7"); + assert_eq!(jsc(&["-p", "let a=[0,0]; a[1]=3+4; a[1]"]), "7"); // Computed index on a READ. - assert_eq!(jsc(&["-e", "let a=[10,20]; let i=0; a[i+1]"]), "20"); + assert_eq!(jsc(&["-p", "let a=[10,20]; let i=0; a[i+1]"]), "20"); } diff --git a/crates/jsc/tests/builtins.rs b/crates/jsc/tests/builtins.rs index 144ce5c..4eac12b 100644 --- a/crates/jsc/tests/builtins.rs +++ b/crates/jsc/tests/builtins.rs @@ -5,36 +5,26 @@ use common::{jsc, jsc_raw_stdout}; /// S14.1 — `console.log` writes to the process's real stdout DURING execution. /// -/// The printed lines appear before the final-expression value line, single-space -/// separated with one trailing newline per call, and each argument is formatted -/// exactly as the CLI formats a final value (numbers bare, strings unquoted, -/// Node-style arrays). The final `console.log(..)` here evaluates to `undefined`, -/// which the CLI then prints as the completion value AFTER the logged lines. +/// The printed lines are single-space separated with one trailing newline per +/// call, and each argument is formatted exactly as `-p` formats a completion +/// value (numbers bare, strings unquoted, Node-style arrays). `-e` prints only +/// the logged lines; `-p` prints the completion value AFTER them. #[test] fn s14_console_log_streams_to_stdout() { // Single number argument. - assert_eq!( - jsc_raw_stdout(&["-e", "console.log(40 + 2)"]), - "42\nundefined\n" - ); + assert_eq!(jsc_raw_stdout(&["-e", "console.log(40 + 2)"]), "42\n"); // Unquoted string. - assert_eq!( - jsc_raw_stdout(&["-e", r#"console.log("hi")"#]), - "hi\nundefined\n" - ); + assert_eq!(jsc_raw_stdout(&["-e", r#"console.log("hi")"#]), "hi\n"); // Multiple arguments, space-separated, one trailing newline. - assert_eq!( - jsc_raw_stdout(&["-e", "console.log(1, 2, 3)"]), - "1 2 3\nundefined\n" - ); + assert_eq!(jsc_raw_stdout(&["-e", "console.log(1, 2, 3)"]), "1 2 3\n"); // Node-style array formatting. assert_eq!( jsc_raw_stdout(&["-e", "console.log([1, 2, 3])"]), - "[ 1, 2, 3 ]\nundefined\n" + "[ 1, 2, 3 ]\n" ); - // Logged lines precede the independent final-expression value (99). + // Under `-p`, logged lines precede the independent completion value (99). assert_eq!( - jsc_raw_stdout(&["-e", "console.log(1); console.log(2); 99"]), + jsc_raw_stdout(&["-p", "console.log(1); console.log(2); 99"]), "1\n2\n99\n" ); } @@ -43,18 +33,18 @@ fn s14_console_log_streams_to_stdout() { /// CLI, formatting their numeric result exactly like any other final value. #[test] fn s14_math_methods_via_eval() { - assert_eq!(jsc(&["-e", "Math.floor(3.7)"]), "3"); - assert_eq!(jsc(&["-e", "Math.ceil(3.2)"]), "4"); - assert_eq!(jsc(&["-e", "Math.round(2.5)"]), "3"); - assert_eq!(jsc(&["-e", "Math.trunc(-3.7)"]), "-3"); - assert_eq!(jsc(&["-e", "Math.abs(-42)"]), "42"); - assert_eq!(jsc(&["-e", "Math.sqrt(1764)"]), "42"); - assert_eq!(jsc(&["-e", "Math.min(5, 2, 9)"]), "2"); - assert_eq!(jsc(&["-e", "Math.max(1, 42)"]), "42"); - assert_eq!(jsc(&["-e", "Math.pow(2, 10)"]), "1024"); - assert_eq!(jsc(&["-e", "Math.PI"]), "3.141592653589793"); - assert_eq!(jsc(&["-e", "Math.E"]), "2.718281828459045"); - assert_eq!(jsc(&["-e", "typeof Math.PI"]), "number"); + assert_eq!(jsc(&["-p", "Math.floor(3.7)"]), "3"); + assert_eq!(jsc(&["-p", "Math.ceil(3.2)"]), "4"); + assert_eq!(jsc(&["-p", "Math.round(2.5)"]), "3"); + assert_eq!(jsc(&["-p", "Math.trunc(-3.7)"]), "-3"); + assert_eq!(jsc(&["-p", "Math.abs(-42)"]), "42"); + assert_eq!(jsc(&["-p", "Math.sqrt(1764)"]), "42"); + assert_eq!(jsc(&["-p", "Math.min(5, 2, 9)"]), "2"); + assert_eq!(jsc(&["-p", "Math.max(1, 42)"]), "42"); + assert_eq!(jsc(&["-p", "Math.pow(2, 10)"]), "1024"); + assert_eq!(jsc(&["-p", "Math.PI"]), "3.141592653589793"); + assert_eq!(jsc(&["-p", "Math.E"]), "2.718281828459045"); + assert_eq!(jsc(&["-p", "typeof Math.PI"]), "number"); } /// S14.3 — `String`/`Number`/`Boolean`/`parseInt`/`parseFloat` conversions compute @@ -63,27 +53,27 @@ fn s14_math_methods_via_eval() { #[test] fn s14_conversions_via_eval() { // String(x): number form matches `format_number`, and the result is a string. - assert_eq!(jsc(&["-e", "String(42)"]), "42"); - assert_eq!(jsc(&["-e", "String(3.5)"]), "3.5"); - assert_eq!(jsc(&["-e", "typeof String(42)"]), "string"); - assert_eq!(jsc(&["-e", r#"String(42) + "px""#]), "42px"); + assert_eq!(jsc(&["-p", "String(42)"]), "42"); + assert_eq!(jsc(&["-p", "String(3.5)"]), "3.5"); + assert_eq!(jsc(&["-p", "typeof String(42)"]), "string"); + assert_eq!(jsc(&["-p", r#"String(42) + "px""#]), "42px"); // Number(x): string parse, boolean/null/undefined coercion. - assert_eq!(jsc(&["-e", r#"Number("42")"#]), "42"); - assert_eq!(jsc(&["-e", r#"Number(" 3.5 ")"#]), "3.5"); - assert_eq!(jsc(&["-e", r#"Number("")"#]), "0"); - assert_eq!(jsc(&["-e", r#"Number("x")"#]), "NaN"); + assert_eq!(jsc(&["-p", r#"Number("42")"#]), "42"); + assert_eq!(jsc(&["-p", r#"Number(" 3.5 ")"#]), "3.5"); + assert_eq!(jsc(&["-p", r#"Number("")"#]), "0"); + assert_eq!(jsc(&["-p", r#"Number("x")"#]), "NaN"); // Boolean(x): JS truthiness. - assert_eq!(jsc(&["-e", "Boolean(0)"]), "false"); - assert_eq!(jsc(&["-e", r#"Boolean("")"#]), "false"); - assert_eq!(jsc(&["-e", r#"Boolean("x")"#]), "true"); - assert_eq!(jsc(&["-e", "Boolean(1)"]), "true"); + assert_eq!(jsc(&["-p", "Boolean(0)"]), "false"); + assert_eq!(jsc(&["-p", r#"Boolean("")"#]), "false"); + assert_eq!(jsc(&["-p", r#"Boolean("x")"#]), "true"); + assert_eq!(jsc(&["-p", "Boolean(1)"]), "true"); // parseInt / parseFloat: leading numeric prefix. - assert_eq!(jsc(&["-e", r#"parseInt("42px")"#]), "42"); - assert_eq!(jsc(&["-e", r#"parseInt("abc")"#]), "NaN"); - assert_eq!(jsc(&["-e", r#"parseFloat("3.14xyz")"#]), "3.14"); + assert_eq!(jsc(&["-p", r#"parseInt("42px")"#]), "42"); + assert_eq!(jsc(&["-p", r#"parseInt("abc")"#]), "NaN"); + assert_eq!(jsc(&["-p", r#"parseFloat("3.14xyz")"#]), "3.14"); // Shadowable by a user binding of the same name. assert_eq!( - jsc(&["-e", "let String = function (x) { return 99 }; String(1)"]), + jsc(&["-p", "let String = function (x) { return 99 }; String(1)"]), "99", ); } @@ -93,26 +83,26 @@ fn s14_conversions_via_eval() { /// timestamps and `Number`-coerced arguments are handled. #[test] fn date_from_timestamp_renders_iso() { - assert_eq!(jsc(&["-e", "new Date(0)"]), "1970-01-01T00:00:00.000Z"); + assert_eq!(jsc(&["-p", "new Date(0)"]), "1970-01-01T00:00:00.000Z"); assert_eq!( - jsc(&["-e", "new Date(1710000000000)"]), + jsc(&["-p", "new Date(1710000000000)"]), "2024-03-09T16:00:00.000Z", ); // Pre-epoch (negative) timestamps split correctly. assert_eq!( - jsc(&["-e", "new Date(-86400000)"]), + jsc(&["-p", "new Date(-86400000)"]), "1969-12-31T00:00:00.000Z", ); // `.getTime()` reads the payload back; the argument is `Number`-coerced. - assert_eq!(jsc(&["-e", "new Date(1500).getTime()"]), "1500"); - assert_eq!(jsc(&["-e", "new Date(true).getTime()"]), "1"); + assert_eq!(jsc(&["-p", "new Date(1500).getTime()"]), "1500"); + assert_eq!(jsc(&["-p", "new Date(true).getTime()"]), "1"); // Nested rendering (inside an array) uses the same ISO form. assert_eq!( - jsc(&["-e", "[new Date(1000)]"]), + jsc(&["-p", "[new Date(1000)]"]), "[ 1970-01-01T00:00:01.000Z ]", ); // `getTime()` on a non-Date receiver yields NaN (no method-receiver TypeError). - assert_eq!(jsc(&["-e", "({}).getTime()"]), "NaN"); + assert_eq!(jsc(&["-p", "({}).getTime()"]), "NaN"); } /// P-series — `Date.now()` and `new Date()` read the host wall clock, and both a @@ -120,20 +110,20 @@ fn date_from_timestamp_renders_iso() { /// non-deterministic, so these assert structural properties, not exact values. #[test] fn date_now_reads_host_clock() { - assert_eq!(jsc(&["-e", "typeof Date.now()"]), "number"); + assert_eq!(jsc(&["-p", "typeof Date.now()"]), "number"); // A clock well past 2023-11 (1_700_000_000_000 ms) confirms a real timestamp. - assert_eq!(jsc(&["-e", "Date.now() > 1700000000000"]), "true"); - assert_eq!(jsc(&["-e", "typeof new Date()"]), "object"); - assert_eq!(jsc(&["-e", "new Date() instanceof Date"]), "true"); + assert_eq!(jsc(&["-p", "Date.now() > 1700000000000"]), "true"); + assert_eq!(jsc(&["-p", "typeof new Date()"]), "object"); + assert_eq!(jsc(&["-p", "new Date() instanceof Date"]), "true"); // `new Date()` and `Date.now()` share the same clock, in order. assert_eq!( - jsc(&["-e", "let a = Date.now(); new Date().getTime() >= a"]), + jsc(&["-p", "let a = Date.now(); new Date().getTime() >= a"]), "true", ); // `instanceof Date` is exact: only a Date value matches. - assert_eq!(jsc(&["-e", "new Date(0) instanceof Date"]), "true"); - assert_eq!(jsc(&["-e", "({}) instanceof Date"]), "false"); - assert_eq!(jsc(&["-e", "5 instanceof Date"]), "false"); + assert_eq!(jsc(&["-p", "new Date(0) instanceof Date"]), "true"); + assert_eq!(jsc(&["-p", "({}) instanceof Date"]), "false"); + assert_eq!(jsc(&["-p", "5 instanceof Date"]), "false"); } /// P-series — the guest-side `Date` field methods computed from the stored ms: @@ -142,51 +132,51 @@ fn date_now_reads_host_clock() { #[test] fn date_field_methods() { // valueOf() == getTime() == the stored ms. - assert_eq!(jsc(&["-e", "new Date(1000).valueOf()"]), "1000"); + assert_eq!(jsc(&["-p", "new Date(1000).valueOf()"]), "1000"); // Epoch: 1970-01-01T00:00:00.000Z, a Thursday (getDay 4). - assert_eq!(jsc(&["-e", "new Date(0).getFullYear()"]), "1970"); - assert_eq!(jsc(&["-e", "new Date(0).getMonth()"]), "0"); - assert_eq!(jsc(&["-e", "new Date(0).getDate()"]), "1"); - assert_eq!(jsc(&["-e", "new Date(0).getDay()"]), "4"); - assert_eq!(jsc(&["-e", "new Date(0).getHours()"]), "0"); - assert_eq!(jsc(&["-e", "new Date(0).getMinutes()"]), "0"); - assert_eq!(jsc(&["-e", "new Date(0).getSeconds()"]), "0"); - assert_eq!( - jsc(&["-e", "new Date(0).toISOString()"]), + assert_eq!(jsc(&["-p", "new Date(0).getFullYear()"]), "1970"); + assert_eq!(jsc(&["-p", "new Date(0).getMonth()"]), "0"); + assert_eq!(jsc(&["-p", "new Date(0).getDate()"]), "1"); + assert_eq!(jsc(&["-p", "new Date(0).getDay()"]), "4"); + assert_eq!(jsc(&["-p", "new Date(0).getHours()"]), "0"); + assert_eq!(jsc(&["-p", "new Date(0).getMinutes()"]), "0"); + assert_eq!(jsc(&["-p", "new Date(0).getSeconds()"]), "0"); + assert_eq!( + jsc(&["-p", "new Date(0).toISOString()"]), "1970-01-01T00:00:00.000Z", ); // 2021-01-01T00:00:00Z (a Friday, getDay 5). assert_eq!( - jsc(&["-e", "new Date(1609459200000).getFullYear()"]), + jsc(&["-p", "new Date(1609459200000).getFullYear()"]), "2021" ); - assert_eq!(jsc(&["-e", "new Date(1609459200000).getMonth()"]), "0"); - assert_eq!(jsc(&["-e", "new Date(1609459200000).getDate()"]), "1"); - assert_eq!(jsc(&["-e", "new Date(1609459200000).getDay()"]), "5"); + assert_eq!(jsc(&["-p", "new Date(1609459200000).getMonth()"]), "0"); + assert_eq!(jsc(&["-p", "new Date(1609459200000).getDate()"]), "1"); + assert_eq!(jsc(&["-p", "new Date(1609459200000).getDay()"]), "5"); // A timestamp exercising every field: 2024-03-09T19:25:45.678Z. assert_eq!( - jsc(&["-e", "new Date(1710012345678).toISOString()"]), + jsc(&["-p", "new Date(1710012345678).toISOString()"]), "2024-03-09T19:25:45.678Z", ); - assert_eq!(jsc(&["-e", "new Date(1710012345678).getHours()"]), "19"); - assert_eq!(jsc(&["-e", "new Date(1710012345678).getMinutes()"]), "25"); - assert_eq!(jsc(&["-e", "new Date(1710012345678).getSeconds()"]), "45"); + assert_eq!(jsc(&["-p", "new Date(1710012345678).getHours()"]), "19"); + assert_eq!(jsc(&["-p", "new Date(1710012345678).getMinutes()"]), "25"); + assert_eq!(jsc(&["-p", "new Date(1710012345678).getSeconds()"]), "45"); // Pre-epoch (negative) timestamps split correctly (1969-12-31, a Wednesday). assert_eq!( - jsc(&["-e", "new Date(-86400000).toISOString()"]), + jsc(&["-p", "new Date(-86400000).toISOString()"]), "1969-12-31T00:00:00.000Z", ); - assert_eq!(jsc(&["-e", "new Date(-86400000).getDay()"]), "3"); - assert_eq!(jsc(&["-e", "new Date(-1).getFullYear()"]), "1969"); + assert_eq!(jsc(&["-p", "new Date(-86400000).getDay()"]), "3"); + assert_eq!(jsc(&["-p", "new Date(-1).getFullYear()"]), "1969"); // toISOString() is an ordinary heap string (concatenable, has .length). - assert_eq!(jsc(&["-e", "new Date(0).toISOString().length"]), "24"); + assert_eq!(jsc(&["-p", "new Date(0).toISOString().length"]), "24"); assert_eq!( - jsc(&["-e", r#""[" + new Date(0).toISOString() + "]""#]), + jsc(&["-p", r#""[" + new Date(0).toISOString() + "]""#]), "[1970-01-01T00:00:00.000Z]", ); // A non-finite timestamp: numeric fields are NaN, toISOString is "Invalid Date". - assert_eq!(jsc(&["-e", "new Date(NaN).getFullYear()"]), "NaN"); - assert_eq!(jsc(&["-e", "new Date(NaN).toISOString()"]), "Invalid Date"); + assert_eq!(jsc(&["-p", "new Date(NaN).getFullYear()"]), "NaN"); + assert_eq!(jsc(&["-p", "new Date(NaN).toISOString()"]), "Invalid Date"); } /// P-series — the `Date` field-method names still work as ordinary user object @@ -196,21 +186,21 @@ fn date_field_methods() { fn date_method_names_fall_back_to_user_methods() { assert_eq!( jsc(&[ - "-e", + "-p", "let o = { getFullYear: function () { return 42 } }; o.getFullYear()" ]), "42", ); assert_eq!( jsc(&[ - "-e", + "-p", "let o = { valueOf: function () { return 7 } }; o.valueOf()" ]), "7", ); assert_eq!( jsc(&[ - "-e", + "-p", "let o = { toISOString: function () { return \"x\" } }; o.toISOString()", ]), "x", @@ -223,7 +213,7 @@ fn date_method_names_fall_back_to_user_methods() { fn date_shadowing_and_arity() { // A user `let Date = ..` shadows the builtin constructor. assert_eq!( - jsc(&["-e", "let Date = function () { return 7 }; Date()"]), + jsc(&["-p", "let Date = function () { return 7 }; Date()"]), "7", ); } diff --git a/crates/jsc/tests/classes.rs b/crates/jsc/tests/classes.rs index 13339ac..18317b9 100644 --- a/crates/jsc/tests/classes.rs +++ b/crates/jsc/tests/classes.rs @@ -3,7 +3,7 @@ mod common; use common::jsc; -/// The P2.2 milestones run inline with `-e` through the built binary. +/// The P2.2 milestones run inline with `-p` through the built binary. /// /// A single class (no inheritance) with a constructor: `new A(args)` allocates a /// fresh instance, binds `this` to it, runs the constructor for its side effects, @@ -14,7 +14,7 @@ fn class_milestones_via_eval() { // Constructor stores a parameter onto `this`; the field reads back. assert_eq!( jsc(&[ - "-e", + "-p", "class A{constructor(x){this.v=x}} let a=new A(42); a.v" ]), "42" @@ -22,18 +22,18 @@ fn class_milestones_via_eval() { // Two fields set from constant initializers, summed after construction. assert_eq!( jsc(&[ - "-e", + "-p", "class P{constructor(){this.x=40; this.y=2}} let p=new P(); p.x+p.y" ]), "42" ); // A fresh instance is an object. assert_eq!( - jsc(&["-e", "class A{constructor(x){this.v=x}} typeof (new A(1))"]), + jsc(&["-p", "class A{constructor(x){this.v=x}} typeof (new A(1))"]), "object" ); // An empty class (no constructor => default no-op ctor) still yields an object. - assert_eq!(jsc(&["-e", "class E{} let e=new E(); typeof e"]), "object"); + assert_eq!(jsc(&["-p", "class E{} let e=new E(); typeof e"]), "object"); } /// A multi-parameter constructor stores each argument onto its own field; the @@ -42,7 +42,7 @@ fn class_milestones_via_eval() { fn class_multi_field_constructor_via_eval() { assert_eq!( jsc(&[ - "-e", + "-p", "class Point{constructor(a,b){this.x=a; this.y=b}} let p=new Point(3,4); p.x+p.y" ]), "7" @@ -50,7 +50,7 @@ fn class_multi_field_constructor_via_eval() { // A field left unset by the constructor reads back `undefined`. assert_eq!( jsc(&[ - "-e", + "-p", "class A{constructor(){this.a=1}} let o=new A(); typeof o.b" ]), "undefined" @@ -65,7 +65,7 @@ fn class_instance_is_a_plain_object_via_eval() { // The instance's fields are writable after construction (`a.v = 99`). assert_eq!( jsc(&[ - "-e", + "-p", "class A{constructor(){this.v=1}} let a=new A(); a.v=99; a.v" ]), "99" @@ -73,7 +73,7 @@ fn class_instance_is_a_plain_object_via_eval() { // An instance held by an object, reached through a property hop. assert_eq!( jsc(&[ - "-e", + "-p", "class A{constructor(x){this.v=x}} let o={a:new A(7)}; o.a.v" ]), "7" @@ -81,7 +81,7 @@ fn class_instance_is_a_plain_object_via_eval() { // A conditional field assignment in the constructor still slots the field. assert_eq!( jsc(&[ - "-e", + "-p", "class A{constructor(c){if(c){this.v=5}else{this.v=0}}} let a=new A(1); a.v" ]), "5" @@ -89,7 +89,7 @@ fn class_instance_is_a_plain_object_via_eval() { // One instance passed as another constructor's argument, read through two hops. assert_eq!( jsc(&[ - "-e", + "-p", "class B{constructor(n){this.n=n}} class A{constructor(b){this.b=b}} \ let a=new A(new B(3)); a.b.n" ]), @@ -105,7 +105,7 @@ fn class_instance_is_a_plain_object_via_eval() { fn class_in_function_scope_via_eval() { assert_eq!( jsc(&[ - "-e", + "-p", "function mk(x){ class A{constructor(v){this.v=v}} return new A(x); } mk(9).v" ]), "9" @@ -113,7 +113,7 @@ fn class_in_function_scope_via_eval() { // The constructor captures an outer binding and stores it onto `this`. assert_eq!( jsc(&[ - "-e", + "-p", "let base=40; class A{constructor(){this.v=base}} let a=new A(); a.v" ]), "40" @@ -121,7 +121,7 @@ fn class_in_function_scope_via_eval() { // `new` reached through an arrow that captures the class binding. assert_eq!( jsc(&[ - "-e", + "-p", "class A{constructor(x){this.v=x}} let f=()=>new A(8); f().v" ]), "8" @@ -137,7 +137,7 @@ fn class_name_resolvable_in_method_bodies() { // A method constructs a fresh instance of its own class and returns it. assert_eq!( jsc(&[ - "-e", + "-p", "class C{constructor(v){this.v=v} mk(){return new C(this.v+1)}} new C(41).mk().v" ]), "42" @@ -145,7 +145,7 @@ fn class_name_resolvable_in_method_bodies() { // A method that recurses through `new` of its own class (5! = 120). assert_eq!( jsc(&[ - "-e", + "-p", "class F{constructor(n){this.n=n} fact(){ if(this.n<=1){return 1} \ return this.n * new F(this.n-1).fact() }} new F(5).fact()" ]), @@ -155,7 +155,7 @@ fn class_name_resolvable_in_method_bodies() { // walked back by field hops: 3 + 2 + 1 == 6. assert_eq!( jsc(&[ - "-e", + "-p", "class N{constructor(v){this.v=v; this.next=undefined} \ cons(v){let n=new N(v); n.next=this; return n}} \ let a=new N(1); let b=a.cons(2); let c=b.cons(3); c.v+c.next.v+c.next.next.v" @@ -165,7 +165,7 @@ fn class_name_resolvable_in_method_bodies() { // The builtin `new TypeError(..)` path is unchanged inside a method body. assert_eq!( jsc(&[ - "-e", + "-p", "class C{constructor(){} boom(){ return (new TypeError('x')).message }} new C().boom()" ]), "x" @@ -180,7 +180,7 @@ fn class_method_milestones_via_eval() { // A method reads an own field set by the constructor. assert_eq!( jsc(&[ - "-e", + "-p", "class C{constructor(){this.n=41} inc(){return this.n+1}} new C().inc()" ]), "42" @@ -189,7 +189,7 @@ fn class_method_milestones_via_eval() { // `this` still bound to the same instance. assert_eq!( jsc(&[ - "-e", + "-p", "class C{constructor(){this.a=6} two(){return this.a} big(){return this.two()*7}} \ new C().big()" ]), @@ -198,7 +198,7 @@ fn class_method_milestones_via_eval() { // Multiple instances share the one method but observe their own state. assert_eq!( jsc(&[ - "-e", + "-p", "class C{constructor(x){this.n=x} get2(){return this.n}} \ let a=new C(5); let b=new C(9); a.get2()+b.get2()" ]), @@ -206,7 +206,7 @@ fn class_method_milestones_via_eval() { ); // A method value read off an instance is a function. assert_eq!( - jsc(&["-e", "class C{m(){return 1}} typeof (new C().m)"]), + jsc(&["-p", "class C{m(){return 1}} typeof (new C().m)"]), "function" ); } @@ -219,7 +219,7 @@ fn class_method_args_and_precedence_via_eval() { // Method arguments flow through the uniform ABI like any call. assert_eq!( jsc(&[ - "-e", + "-p", "class C{constructor(){this.base=10} add(x){return this.base+x}} new C().add(5)" ]), "15" @@ -228,7 +228,7 @@ fn class_method_args_and_precedence_via_eval() { // method returns the function; reading an own field returns the field value. assert_eq!( jsc(&[ - "-e", + "-p", "class C{constructor(){this.v=3} m(){return this.v*2}} \ let c=new C(); c.v + c.m()" ]), @@ -244,7 +244,7 @@ fn class_method_args_and_precedence_via_eval() { fn p2_2b_ctor_field_computed_init_via_eval() { assert_eq!( jsc(&[ - "-e", + "-p", "class P{constructor(a,b){this.x=a+b}} let p=new P(3,4); p.x" ]), "7" @@ -259,12 +259,12 @@ fn p2_2b_ctor_field_computed_init_via_eval() { fn class_field_milestones_via_eval() { // A single field with a constant initializer, no constructor: a default // constructor is synthesized from the field initializers. - assert_eq!(jsc(&["-e", "class A{ v = 42 } new A().v"]), "42"); + assert_eq!(jsc(&["-p", "class A{ v = 42 } new A().v"]), "42"); // The field initializer runs BEFORE the constructor body: the constructor // observes the already-initialized `base` and bumps it. assert_eq!( jsc(&[ - "-e", + "-p", "class A{ base = 40; constructor(){ this.base = this.base + 2 } } new A().base" ]), "42" @@ -273,7 +273,7 @@ fn class_field_milestones_via_eval() { // prototype. assert_eq!( jsc(&[ - "-e", + "-p", "class A{ n = 41; inc(){ return this.n + 1 } } new A().inc()" ]), "42" @@ -282,7 +282,7 @@ fn class_field_milestones_via_eval() { // independent instances. assert_eq!( jsc(&[ - "-e", + "-p", "class P{ x = 40; y = 2; constructor(k){ this.x = this.x + k } } \ new P(0).x + new P(0).y" ]), @@ -298,21 +298,21 @@ fn class_field_milestones_via_eval() { fn class_field_ordering_and_undefined_via_eval() { // A later field reads an earlier one through `this` (declaration order). assert_eq!( - jsc(&["-e", "class A{ a = 40; b = this.a + 2 } new A().b"]), + jsc(&["-p", "class A{ a = 40; b = this.a + 2 } new A().b"]), "42" ); // An uninitialized field initializes to `undefined`. assert_eq!( - jsc(&["-e", "class A{ x; } typeof (new A().x)"]), + jsc(&["-p", "class A{ x; } typeof (new A().x)"]), "undefined" ); // A field-only class is still a plain object. - assert_eq!(jsc(&["-e", "class A{ v = 1 } typeof (new A())"]), "object"); + assert_eq!(jsc(&["-p", "class A{ v = 1 } typeof (new A())"]), "object"); // The union shape: a field the constructor never assigns coexists with a field // only the constructor assigns. assert_eq!( jsc(&[ - "-e", + "-p", "class C{ a = 5; constructor(){ this.b = 7 } } let c=new C(); c.a + c.b" ]), "12" @@ -326,56 +326,56 @@ fn class_field_ordering_and_undefined_via_eval() { fn p2_full_regression_acceptance_via_eval() { // P2.1 — object-literal methods bind `this`. assert_eq!( - jsc(&["-e", "let o={n:41, inc(){return this.n+1}}; o.inc()"]), + jsc(&["-p", "let o={n:41, inc(){return this.n+1}}; o.inc()"]), "42" ); assert_eq!( jsc(&[ - "-e", + "-p", "let o={a:2,b:3, sum(){return this.a+this.b}, scaled(){return this.sum()*8}}; o.scaled()" ]), "40" ); - assert_eq!(jsc(&["-e", "let n=7; let o={n}; o.n"]), "7"); + assert_eq!(jsc(&["-p", "let n=7; let o={n}; o.n"]), "7"); // P2.2 — class, constructor, new. assert_eq!( jsc(&[ - "-e", + "-p", "class A{constructor(x){this.v=x}} let a=new A(42); a.v" ]), "42" ); assert_eq!( jsc(&[ - "-e", + "-p", "class P{constructor(){this.x=40; this.y=2}} let p=new P(); p.x+p.y" ]), "42" ); assert_eq!( - jsc(&["-e", "class A{constructor(x){this.v=x}} typeof (new A(1))"]), + jsc(&["-p", "class A{constructor(x){this.v=x}} typeof (new A(1))"]), "object" ); // P2.3 — prototype-shared methods, including a method calling another method. assert_eq!( jsc(&[ - "-e", + "-p", "class C{constructor(){this.n=41} inc(){return this.n+1}} new C().inc()" ]), "42" ); assert_eq!( jsc(&[ - "-e", + "-p", "class C{constructor(){this.a=6} two(){return this.a} big(){return this.two()*7}} new C().big()" ]), "42" ); // P2.4 — class fields, including a field init reading an earlier field. - assert_eq!(jsc(&["-e", "class A{ v = 42 } new A().v"]), "42"); + assert_eq!(jsc(&["-p", "class A{ v = 42 } new A().v"]), "42"); assert_eq!( jsc(&[ - "-e", + "-p", "class A{ base = 40; constructor(){ this.base = this.base + 2 } } new A().base" ]), "42" @@ -394,7 +394,7 @@ fn class_method_news_own_class_and_captures_enclosing_binding_via_eval() { // path, and 40 + 2 == 42. assert_eq!( jsc(&[ - "-e", + "-p", "let base=40; class C{ constructor(v){this.v=v} mk(){ return new C(base + this.v) } } \ new C(2).mk().v" ]), @@ -404,7 +404,7 @@ fn class_method_news_own_class_and_captures_enclosing_binding_via_eval() { // two calls bump the shared cell to 2, observed after the calls return. assert_eq!( jsc(&[ - "-e", + "-p", "let count=0; class C{ constructor(){} bump(){ count = count + 1; return new C() } } \ let c=new C(); c.bump(); c.bump(); count" ]), @@ -414,7 +414,7 @@ fn class_method_news_own_class_and_captures_enclosing_binding_via_eval() { // resolves the class name, `total` reads the captured const (10 + 32 == 42). assert_eq!( jsc(&[ - "-e", + "-p", "const bonus=10; class C{ constructor(n){this.n=n} mk(){ return new C(this.n) } \ total(){ return bonus + this.mk().n } } new C(32).total()" ]), @@ -431,7 +431,7 @@ fn class_extends_milestones_via_eval() { // super(n) forwards to the parent constructor, which sets the inherited field. assert_eq!( jsc(&[ - "-e", + "-p", "class A{constructor(n){this.n=n}} class B extends A{constructor(n){super(n)}} \ new B(\"x\").n", ]), @@ -441,7 +441,7 @@ fn class_extends_milestones_via_eval() { // ctor): the argument reaches the parent field and inherited methods resolve. assert_eq!( jsc(&[ - "-e", + "-p", "class A{constructor(n){this.n=n} dbl(){return this.n*2}} class B extends A{} \ new B(5).dbl()", ]), @@ -450,7 +450,7 @@ fn class_extends_milestones_via_eval() { // A method override shadows the inherited one; `super.m()` still reaches it. assert_eq!( jsc(&[ - "-e", + "-p", "class A{greet(){return \"A\"}} class B extends A{greet(){return super.greet()+\"B\"}} \ new B().greet()", ]), @@ -459,7 +459,7 @@ fn class_extends_milestones_via_eval() { // A three-level `super` chain accumulates through each ancestor. assert_eq!( jsc(&[ - "-e", + "-p", "class A{m(){return 1}} class B extends A{m(){return super.m()+10}} \ class C extends B{m(){return super.m()+100}} new C().m()", ]), @@ -468,7 +468,7 @@ fn class_extends_milestones_via_eval() { // Own and inherited fields coexist on the instance shape. assert_eq!( jsc(&[ - "-e", + "-p", "class A{constructor(a){this.a=a}} \ class B extends A{constructor(a,b){super(a); this.b=b}} \ let o=new B(1,2); o.a+o.b", @@ -478,7 +478,7 @@ fn class_extends_milestones_via_eval() { // An inherited method reads a field set by the parent constructor. assert_eq!( jsc(&[ - "-e", + "-p", "class Animal{constructor(nm){this.name=nm} speak(){return this.name+\" noise\"}} \ class Dog extends Animal{speak(){return this.name+\" barks\"}} \ new Dog(\"Rex\").speak()", @@ -493,29 +493,29 @@ fn class_extends_milestones_via_eval() { fn class_extends_instanceof_via_eval() { let prelude = "class A{} class B extends A{} class C extends B{} class Z{} "; assert_eq!( - jsc(&["-e", &format!("{prelude} new C() instanceof A")]), + jsc(&["-p", &format!("{prelude} new C() instanceof A")]), "true" ); assert_eq!( - jsc(&["-e", &format!("{prelude} new C() instanceof B")]), + jsc(&["-p", &format!("{prelude} new C() instanceof B")]), "true" ); assert_eq!( - jsc(&["-e", &format!("{prelude} new C() instanceof C")]), + jsc(&["-p", &format!("{prelude} new C() instanceof C")]), "true" ); assert_eq!( - jsc(&["-e", &format!("{prelude} new A() instanceof C")]), + jsc(&["-p", &format!("{prelude} new A() instanceof C")]), "false" ); assert_eq!( - jsc(&["-e", &format!("{prelude} new C() instanceof Z")]), + jsc(&["-p", &format!("{prelude} new C() instanceof Z")]), "false" ); // A non-instance left operand never matches (no trap). - assert_eq!(jsc(&["-e", &format!("{prelude} 5 instanceof A")]), "false"); + assert_eq!(jsc(&["-p", &format!("{prelude} 5 instanceof A")]), "false"); assert_eq!( - jsc(&["-e", &format!("{prelude} typeof new C() === \"object\"")]), + jsc(&["-p", &format!("{prelude} typeof new C() === \"object\"")]), "true" ); } @@ -528,24 +528,24 @@ fn class_extends_instanceof_via_eval() { fn class_static_members_via_eval() { // A static method called off the class name. assert_eq!( - jsc(&["-e", "class C{ static make(){ return 42 } } C.make()"]), + jsc(&["-p", "class C{ static make(){ return 42 } } C.make()"]), "42" ); // A static method with parameters. assert_eq!( - jsc(&["-e", "class M{ static add(a,b){ return a+b } } M.add(3,4)"]), + jsc(&["-p", "class M{ static add(a,b){ return a+b } } M.add(3,4)"]), "7" ); // A static field read off the class name. - assert_eq!(jsc(&["-e", "class C{ static count = 7 } C.count"]), "7"); + assert_eq!(jsc(&["-p", "class C{ static count = 7 } C.count"]), "7"); // A static field with an expression initializer, evaluated once at definition. - assert_eq!(jsc(&["-e", "class C{ static v = 3*10 + 2 } C.v"]), "32"); + assert_eq!(jsc(&["-p", "class C{ static v = 3*10 + 2 } C.v"]), "32"); // An uninitialized static field reads back `undefined`. - assert_eq!(jsc(&["-e", "class C{ static f; } typeof C.f"]), "undefined"); + assert_eq!(jsc(&["-p", "class C{ static f; } typeof C.f"]), "undefined"); // A static method reads a static field through `this` (bound to the class). assert_eq!( jsc(&[ - "-e", + "-p", "class C{ static n = 5; static getN(){ return this.n } } C.getN()" ]), "5" @@ -553,7 +553,7 @@ fn class_static_members_via_eval() { // A static method calls a sibling static through the class name. assert_eq!( jsc(&[ - "-e", + "-p", "class C{ static a(){ return 10 } static b(){ return C.a()+5 } } C.b()" ]), "15" @@ -562,7 +562,7 @@ fn class_static_members_via_eval() { // on the same class, read from their respective receivers. assert_eq!( jsc(&[ - "-e", + "-p", "class C{ constructor(){this.x=1} static y=2; inst(){return this.x} static st(){return this.y} } \ let c=new C(); c.inst()+C.st()" ]), @@ -578,30 +578,30 @@ fn class_static_members_via_eval() { fn class_static_field_computed_init_via_eval() { // References an earlier static through the class name. assert_eq!( - jsc(&["-e", "class C{ static a = 1; static b = C.a + 1 } C.b"]), + jsc(&["-p", "class C{ static a = 1; static b = C.a + 1 } C.b"]), "2" ); assert_eq!( - jsc(&["-e", "class C{ static a = 2; static b = C.a * 3 } C.b"]), + jsc(&["-p", "class C{ static a = 2; static b = C.a * 3 } C.b"]), "6" ); // Chained: each line sees the previous one already assigned. assert_eq!( jsc(&[ - "-e", + "-p", "class C{ static a=1; static b=C.a+1; static c=C.b+1 } C.c" ]), "3" ); // `this` inside a static initializer is the class. assert_eq!( - jsc(&["-e", "class C{ static a = 5; static b = this.a + 10 } C.b"]), + jsc(&["-p", "class C{ static a = 5; static b = this.a + 10 } C.b"]), "15" ); // A literal initializer alongside a computed one: both resolve, in order. assert_eq!( jsc(&[ - "-e", + "-p", "class C{ static a=1; static b=\"x\"; static c=C.a+2 } C.a + C.c" ]), "4" @@ -609,7 +609,7 @@ fn class_static_field_computed_init_via_eval() { // A computed static is still NOT visible on an instance. assert_eq!( jsc(&[ - "-e", + "-p", "class C{ static a=1; static b=C.a+1 } typeof (new C()).b" ]), "undefined" @@ -626,7 +626,7 @@ fn class_instance_accessor_via_eval() { // A getter computes from fields. assert_eq!( jsc(&[ - "-e", + "-p", "class C{ constructor(w,h){this.w=w; this.h=h} get area(){return this.w*this.h} } \ new C(3,4).area" ]), @@ -635,7 +635,7 @@ fn class_instance_accessor_via_eval() { // A getter+setter pair: the setter transforms and stores; the getter reads back. assert_eq!( jsc(&[ - "-e", + "-p", "class C{ constructor(){this._v=0} get v(){return this._v} set v(x){this._v=x*2} } \ let c=new C(); c.v=5; c.v" ]), @@ -644,7 +644,7 @@ fn class_instance_accessor_via_eval() { // Assigning a get-only accessor is silently ignored (non-strict semantics). assert_eq!( jsc(&[ - "-e", + "-p", "class C{ get x(){return 7} } let c=new C(); c.x=99; c.x" ]), "7" @@ -652,7 +652,7 @@ fn class_instance_accessor_via_eval() { // Reading a set-only accessor yields `undefined`. assert_eq!( jsc(&[ - "-e", + "-p", "class C{ constructor(){this.k=1} set y(v){this.k=v} } typeof (new C()).y" ]), "undefined" @@ -660,7 +660,7 @@ fn class_instance_accessor_via_eval() { // A setter's side effect is observable through a plain field. assert_eq!( jsc(&[ - "-e", + "-p", "class C{ constructor(){this.k=0} set y(v){this.k=v+1} } let c=new C(); c.y=41; c.k" ]), "42" @@ -668,7 +668,7 @@ fn class_instance_accessor_via_eval() { // A computed member read `c[key]` also fires the getter. assert_eq!( jsc(&[ - "-e", + "-p", "class C{ get z(){return 42} } let c=new C(); let k=\"z\"; c[k]" ]), "42" @@ -676,7 +676,7 @@ fn class_instance_accessor_via_eval() { // Two accessors with distinct names coexist on one prototype. assert_eq!( jsc(&[ - "-e", + "-p", "class C{ constructor(){this._a=1;this._b=2} get a(){return this._a} get b(){return this._b} } \ let c=new C(); c.a+\",\"+c.b" ]), @@ -686,7 +686,7 @@ fn class_instance_accessor_via_eval() { // the setter. assert_eq!( jsc(&[ - "-e", + "-p", "class C{ constructor(){this._n=10} get n(){return this._n} set n(v){this._n=v} } \ let c=new C(); c.n += 5; c.n" ]), @@ -695,7 +695,7 @@ fn class_instance_accessor_via_eval() { // A getter body may call an instance method. assert_eq!( jsc(&[ - "-e", + "-p", "class C{ constructor(){this.r=2} area(){return this.r*this.r} get a2(){return this.area()+1} } \ new C().a2" ]), @@ -705,7 +705,7 @@ fn class_instance_accessor_via_eval() { // getter, then calls its result. assert_eq!( jsc(&[ - "-e", + "-p", "class C{ get f(){ return function(){ return 99 } } } new C().f()" ]), "99" @@ -719,7 +719,7 @@ fn class_inherited_accessor_via_eval() { // A subclass instance reads a getter defined on the parent's prototype. assert_eq!( jsc(&[ - "-e", + "-p", "class A{ get name(){return \"A\"+this.n} } class B extends A{ constructor(){super(); this.n=5} } \ new B().name" ]), @@ -728,7 +728,7 @@ fn class_inherited_accessor_via_eval() { // A getter+setter pair inherited from the base drives a subclass field. assert_eq!( jsc(&[ - "-e", + "-p", "class A{ get v(){return this._v} set v(x){this._v=x+1} } \ class B extends A{ constructor(){super(); this._v=0} } \ let b=new B(); b.v=10; b.v" @@ -745,7 +745,7 @@ fn class_throwing_accessor_via_eval() { // routed into a binding read as the trailing expression). assert_eq!( jsc(&[ - "-e", + "-p", "class C{ get bad(){ throw new Error(\"boom\") } } let c=new C(); \ let m=\"none\"; try { c.bad } catch(e){ m=e.message } m" ]), @@ -754,7 +754,7 @@ fn class_throwing_accessor_via_eval() { // A throwing setter is caught by an enclosing `try`. assert_eq!( jsc(&[ - "-e", + "-p", "class C{ set bad(v){ throw new Error(\"set-boom\") } } let c=new C(); \ let m=\"none\"; try { c.bad=1 } catch(e){ m=e.message } m" ]), diff --git a/crates/jsc/tests/cli_modes.rs b/crates/jsc/tests/cli_modes.rs index b8e3b67..0a15969 100644 --- a/crates/jsc/tests/cli_modes.rs +++ b/crates/jsc/tests/cli_modes.rs @@ -1,93 +1,208 @@ -//! Integration tests: CLI invocation modes: file input and `--emit wasm`. +//! Integration tests: CLI invocation modes — `jsc FILE`, `-e`, `-p`, +//! `jsc build`, and the exit-code contract (0 success, 1 uncaught/runtime, +//! 2 compile error). #![allow(clippy::expect_used)] -use std::path::Path; -use std::process::Command; +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; mod common; use common::{jsc, jsc_raw_stdout}; -/// `jsc ` reads a multi-statement program from disk and runs it. -/// -/// The recursive `fib(10)` covers the fourth milestone (55) and exercises the -/// file-input path end to end. +/// Spawn the built `jsc` binary with `args`, returning the raw process output. +fn jsc_output(args: &[&str]) -> Output { + Command::new(env!("CARGO_BIN_EXE_jsc")) + .args(args) + .output() + .expect("failed to spawn jsc binary") +} + +/// Write `src` to `name` under the cargo test temp dir and return its path. +fn temp_source(name: &str, src: &str) -> PathBuf { + let path = Path::new(env!("CARGO_TARGET_TMPDIR")).join(name); + std::fs::write(&path, src).expect("write temp source"); + path +} + +/// `jsc FILE` runs the program and prints only its console output — the +/// completion value (`fib(10)` = 55) is not printed. #[test] -fn milestone_via_file() { - let path = Path::new(env!("CARGO_TARGET_TMPDIR")).join("fib.js"); - std::fs::write( - &path, - "function fib(n){ if(n<2) return n; return fib(n-1)+fib(n-2) } fib(10)", - ) - .expect("write temp source"); +fn file_run_prints_console_output_only() { + let path = temp_source( + "fib.js", + "function fib(n){ if(n<2) return n; return fib(n-1)+fib(n-2) } \ + console.log(fib(10)); fib(10)", + ); + assert_eq!( + jsc_raw_stdout(&[path.to_str().expect("utf-8 path")]), + "55\n" + ); +} - let out = jsc(&[path.to_str().expect("utf-8 path")]); - assert_eq!(out, "55"); +/// `jsc FILE` with no console output prints nothing at all. +#[test] +fn file_run_without_console_prints_nothing() { + let path = temp_source( + "counter.js", + "function counter(){ let c=0; return function(){ c=c+1; return c } } \ + let n=counter(); n();n();n()", + ); + assert_eq!(jsc_raw_stdout(&[path.to_str().expect("utf-8 path")]), ""); } -/// `--emit wasm` writes a non-empty WebAssembly module to stdout. +/// S14.7 — a `console.log`-driven program runs from a source *file*; only the +/// printed lines reach stdout (no trailing completion value). #[test] -fn emit_wasm_writes_module_bytes() { - let output = Command::new(env!("CARGO_BIN_EXE_jsc")) - .args(["-e", "40 + 2", "--emit", "wasm"]) - .output() - .expect("failed to spawn jsc binary"); - assert!(output.status.success(), "emit wasm should succeed"); - // Every wasm module starts with the `\0asm` magic and a version word. +fn s14_console_log_acceptance_via_file() { + let path = temp_source( + "s14_log.js", + "let a=[1,2]; a.push(3); console.log(a.length); console.log(a); \ + console.log([1,2,3].map(x=>x*2))", + ); + assert_eq!( + jsc_raw_stdout(&[path.to_str().expect("utf-8 path")]), + "3\n[ 1, 2, 3 ]\n[ 2, 4, 6 ]\n" + ); +} + +/// `-e` runs the source and prints only console output, never the value. +#[test] +fn eval_prints_console_output_only() { + assert_eq!(jsc_raw_stdout(&["-e", "console.log(1); 2"]), "1\n"); + assert_eq!(jsc_raw_stdout(&["-e", "40 + 2"]), ""); +} + +/// `-p` prints the completion value after any console output (node -p). +#[test] +fn print_prints_completion_value() { + assert_eq!(jsc(&["-p", "40 + 2"]), "42"); + assert_eq!(jsc(&["--print", "1 + 1"]), "2"); + assert_eq!(jsc_raw_stdout(&["-p", "console.log(1); 2"]), "1\n2\n"); +} + +/// `-p` accepts a value that starts with `-`; without `allow_hyphen_values` clap +/// treats `-1 * 2` as an unknown flag and the parse fails before evaluation. +#[test] +fn print_accepts_leading_hyphen_expression() { + assert_eq!(jsc(&["-p", "-1 * 2"]), "-2"); +} + +/// `-p '-0'` parses and runs without a clap error, and (N3) renders as `0`: +/// JS `String(-0)` is `"0"`. +#[test] +fn print_accepts_leading_hyphen_negative_zero() { + assert_eq!(jsc(&["-p", "-0"]), "0"); +} + +/// `-e` also accepts a leading-hyphen source. +#[test] +fn eval_accepts_leading_hyphen_source() { + assert_eq!(jsc_raw_stdout(&["-e", "-1; console.log(3)"]), "3\n"); +} + +/// `-e`, `-p`, and FILE are mutually exclusive. +#[test] +fn source_forms_are_mutually_exclusive() { + let output = jsc_output(&["-e", "1", "-p", "2"]); + assert!(!output.status.success(), "-e with -p must be rejected"); +} + +/// `jsc build FILE -o OUT.wasm` writes the WebAssembly module to OUT.wasm. +#[test] +fn build_writes_module_to_file() { + let src = temp_source("build_src.js", "40 + 2"); + let out = Path::new(env!("CARGO_TARGET_TMPDIR")).join("build_out.wasm"); + let _ = std::fs::remove_file(&out); + let output = jsc_output(&[ + "build", + src.to_str().expect("utf-8 path"), + "-o", + out.to_str().expect("utf-8 path"), + ]); + assert_eq!(output.status.code(), Some(0), "build should succeed"); + assert!(output.stdout.is_empty(), "build -o FILE prints nothing"); + let bytes = std::fs::read(&out).expect("read built module"); + assert!(bytes.starts_with(b"\0asm"), "expected wasm magic header"); +} + +/// `jsc build FILE -o -` writes the module bytes to stdout. +#[test] +fn build_writes_module_to_stdout() { + let src = temp_source("build_stdout.js", "40 + 2"); + let output = jsc_output(&["build", src.to_str().expect("utf-8 path"), "-o", "-"]); + assert_eq!(output.status.code(), Some(0), "build should succeed"); assert!( output.stdout.starts_with(b"\0asm"), "expected wasm magic header" ); } -/// `-e` accepts a value that starts with `-`; without `allow_hyphen_values` clap -/// treats `-1 * 2` as an unknown flag and the parse fails before evaluation. +/// `jsc build` requires `-o`. #[test] -fn eval_accepts_leading_hyphen_expression() { - assert_eq!(jsc(&["-e", "-1 * 2"]), "-2"); +fn build_requires_output() { + let src = temp_source("build_no_o.js", "1"); + let output = jsc_output(&["build", src.to_str().expect("utf-8 path")]); + assert!(!output.status.success(), "build without -o must fail"); } -/// `-e '-0'` parses and runs without a clap error, and (N3) renders as `0`: -/// JS `String(-0)` is `"0"`. The stored f64 sign is preserved, so this pins both -/// that the hyphen-leading value is accepted and the normalized rendering. +/// A successful run exits 0. #[test] -fn eval_accepts_leading_hyphen_negative_zero() { - assert_eq!(jsc(&["-e", "-0"]), "0"); +fn success_exits_zero() { + assert_eq!(jsc_output(&["-e", "1 + 1"]).status.code(), Some(0)); + assert_eq!(jsc_output(&["-p", "1 + 1"]).status.code(), Some(0)); } -/// A non-hyphen `-e` invocation still works unchanged after enabling -/// `allow_hyphen_values`. +/// A compile error exits 2 and renders the caret diagnostic on stderr. #[test] -fn eval_non_hyphen_still_works() { - assert_eq!(jsc(&["-e", "40 + 2"]), "42"); +fn compile_error_exits_two() { + for flag in ["-e", "-p"] { + let output = jsc_output(&[flag, "let x = ;"]); + assert_eq!(output.status.code(), Some(2), "{flag}: compile error"); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("--> :1:"), "got: {stderr}"); + assert!(stderr.contains('^'), "got: {stderr}"); + } } -/// S13.7 — a closure acceptance milestone also runs from a source *file*, so the -/// file-input path is exercised against the S13 feature set, not only `-e`. +/// A compile error in a FILE exits 2 and labels the diagnostic with the path. #[test] -fn s13_closure_acceptance_via_file() { - let path = Path::new(env!("CARGO_TARGET_TMPDIR")).join("counter.js"); - std::fs::write( - &path, - "function counter(){ let c=0; return function(){ c=c+1; return c } } \ - let n=counter(); n();n();n()", - ) - .expect("write temp source"); - assert_eq!(jsc(&[path.to_str().expect("utf-8 path")]), "3"); +fn file_compile_error_exits_two() { + let path = temp_source("bad.js", "let x = ;"); + let path = path.to_str().expect("utf-8 path"); + let output = jsc_output(&[path]); + assert_eq!(output.status.code(), Some(2)); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains(&format!("--> {path}:1:")), "got: {stderr}"); } -/// S14.7 — a `console.log`-driven program runs from a source *file*, exercising the -/// file-input path against S14's print channel (not only `-e`). The program prints -/// several lines as it goes and ends on `undefined`. +/// `jsc build` on a source with a compile error exits 2. #[test] -fn s14_console_log_acceptance_via_file() { - let path = Path::new(env!("CARGO_TARGET_TMPDIR")).join("s14_log.js"); - std::fs::write( - &path, - "let a=[1,2]; a.push(3); console.log(a.length); console.log(a); \ - console.log([1,2,3].map(x=>x*2))", - ) - .expect("write temp source"); - assert_eq!( - jsc_raw_stdout(&[path.to_str().expect("utf-8 path")]), - "3\n[ 1, 2, 3 ]\n[ 2, 4, 6 ]\nundefined\n" +fn build_compile_error_exits_two() { + let src = temp_source("build_bad.js", "let x = ;"); + let output = jsc_output(&["build", src.to_str().expect("utf-8 path"), "-o", "-"]); + assert_eq!(output.status.code(), Some(2)); + assert!( + output.stdout.is_empty(), + "no module bytes on a compile error" ); + assert!(String::from_utf8_lossy(&output.stderr).contains('^')); +} + +/// An uncaught exception exits 1 with `Uncaught ...` on stderr. +#[test] +fn uncaught_exception_exits_one() { + for flag in ["-e", "-p"] { + let output = jsc_output(&[flag, "throw 1"]); + assert_eq!(output.status.code(), Some(1), "{flag}: uncaught throw"); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.starts_with("Uncaught 1"), "got: {stderr}"); + assert!(output.stdout.is_empty(), "no value printed on a throw"); + } +} + +/// An unreadable FILE fails with a message on stderr. +#[test] +fn missing_file_fails() { + let output = jsc_output(&["/nonexistent/jsc-missing.js"]); + assert!(!output.status.success()); + assert!(!output.stderr.is_empty(), "expected an I/O error message"); } diff --git a/crates/jsc/tests/closures.rs b/crates/jsc/tests/closures.rs index caf6b44..ab0cd71 100644 --- a/crates/jsc/tests/closures.rs +++ b/crates/jsc/tests/closures.rs @@ -11,14 +11,14 @@ use common::jsc; fn closures_via_eval() { assert_eq!( jsc(&[ - "-e", + "-p", "function adder(x){ return y => x + y } let a5 = adder(5); a5(37)" ]), "42" ); assert_eq!( jsc(&[ - "-e", + "-p", "function counter(){ let c=0; return function(){ c=c+1; return c } } \ let n=counter(); n(); n(); n()" ]), @@ -26,7 +26,7 @@ fn closures_via_eval() { ); assert_eq!( jsc(&[ - "-e", + "-p", "function adder(x){ return y => x + y } let a=adder(5); let b=adder(100); a(1)+b(1)" ]), "107" @@ -41,17 +41,17 @@ fn closures_via_eval() { fn function_declaration_captures_enclosing_binding_via_eval() { // Read a `let` and a `const`. assert_eq!( - jsc(&["-e", "let n=41; function f(){ return n+1 } f()"]), + jsc(&["-p", "let n=41; function f(){ return n+1 } f()"]), "42" ); assert_eq!( - jsc(&["-e", "const k=42; function f(){ return k } f()"]), + jsc(&["-p", "const k=42; function f(){ return k } f()"]), "42" ); // Read AND write a shared cell across calls. assert_eq!( jsc(&[ - "-e", + "-p", "let s=0; function add(x){ s = s + x } add(40); add(2); s" ]), "42" @@ -59,7 +59,7 @@ fn function_declaration_captures_enclosing_binding_via_eval() { // A nested `function` inside a top-level `function` still captures locally. assert_eq!( jsc(&[ - "-e", + "-p", "function outer(){ let c=0; function inc(){ c=c+1; return c } inc(); return inc() } \ outer()" ]), @@ -68,7 +68,7 @@ fn function_declaration_captures_enclosing_binding_via_eval() { // A non-capturing declaration keeps working (direct call, hoisted). assert_eq!( jsc(&[ - "-e", + "-p", "function g(){ return 7 } function f(){ return g() } f()" ]), "7" @@ -76,7 +76,7 @@ fn function_declaration_captures_enclosing_binding_via_eval() { // A class method reads an enclosing top-level binding. assert_eq!( jsc(&[ - "-e", + "-p", "let base=40; class C { add(x){ return base + x } } new C().add(2)" ]), "42" diff --git a/crates/jsc/tests/collections.rs b/crates/jsc/tests/collections.rs index d98937e..3878e40 100644 --- a/crates/jsc/tests/collections.rs +++ b/crates/jsc/tests/collections.rs @@ -6,7 +6,7 @@ use std::process::Command; use common::jsc; -/// Run `jsc -e ` expecting a non-zero exit, returning its stderr so a +/// Run `jsc -e ` expecting compile-error exit 2, returning its stderr so a /// rejection message can be asserted. fn jsc_reject(src: &str) -> String { let output = Command::new(env!("CARGO_BIN_EXE_jsc")) @@ -14,13 +14,13 @@ fn jsc_reject(src: &str) -> String { .output() .expect("failed to spawn jsc binary"); assert!( - !output.status.success(), - "jsc -e {src:?} unexpectedly succeeded" + output.status.code() == Some(2), + "jsc -e {src:?} should exit 2 (compile error)" ); String::from_utf8_lossy(&output.stderr).into_owned() } -/// The P3.5 Map/Set milestones run inline with `-e` through the built binary. +/// The P3.5 Map/Set milestones run inline with `-p` through the built binary. /// /// Covers the collection wave end to end: `Map` set/get sum, `has`, `size`, `Set` /// add dedup + `size`, `Set` `has`, and `typeof (new Map())`. @@ -28,34 +28,34 @@ fn jsc_reject(src: &str) -> String { fn map_set_milestones_via_eval() { assert_eq!( jsc(&[ - "-e", + "-p", "let m=new Map(); m.set(\"a\",40); m.set(\"b\",2); m.get(\"a\")+m.get(\"b\")" ]), "42" ); assert_eq!( - jsc(&["-e", "let m=new Map(); m.set(\"a\",40); m.has(\"a\")"]), + jsc(&["-p", "let m=new Map(); m.set(\"a\",40); m.has(\"a\")"]), "true" ); assert_eq!( jsc(&[ - "-e", + "-p", "let m=new Map(); m.set(\"a\",40); m.set(\"b\",2); m.size" ]), "2" ); assert_eq!( jsc(&[ - "-e", + "-p", "let s=new Set(); s.add(1); s.add(1); s.add(2); s.size" ]), "2" ); assert_eq!( - jsc(&["-e", "let s=new Set(); s.add(1); s.add(2); s.has(2)"]), + jsc(&["-p", "let s=new Set(); s.add(1); s.add(2); s.has(2)"]), "true" ); - assert_eq!(jsc(&["-e", "typeof (new Map())"]), "object"); + assert_eq!(jsc(&["-p", "typeof (new Map())"]), "object"); } /// A `Map`'s `set` updates an existing key in place (no duplicate, `size` @@ -65,29 +65,29 @@ fn map_set_milestones_via_eval() { fn map_update_and_miss_via_eval() { assert_eq!( jsc(&[ - "-e", + "-p", "let m=new Map(); m.set(\"a\",1); m.set(\"a\",9); m.get(\"a\")" ]), "9" ); assert_eq!( jsc(&[ - "-e", + "-p", "let m=new Map(); m.set(\"a\",1); m.set(\"a\",9); m.size" ]), "1" ); assert_eq!( - jsc(&["-e", "let m=new Map(); m.set(\"a\",1); m.get(\"x\")"]), + jsc(&["-p", "let m=new Map(); m.set(\"a\",1); m.get(\"x\")"]), "undefined" ); assert_eq!( - jsc(&["-e", "let m=new Map(); m.set(\"a\",1); m.has(\"x\")"]), + jsc(&["-p", "let m=new Map(); m.set(\"a\",1); m.has(\"x\")"]), "false" ); assert_eq!( jsc(&[ - "-e", + "-p", "let m=new Map(); m.set(1,\"x\"); m.set(2,\"y\"); m.get(1)+m.get(2)" ]), "xy" @@ -100,23 +100,23 @@ fn map_update_and_miss_via_eval() { #[test] fn map_delete_via_eval() { assert_eq!( - jsc(&["-e", "let m=new Map(); m.set(\"a\",1); m.delete(\"a\")"]), + jsc(&["-p", "let m=new Map(); m.set(\"a\",1); m.delete(\"a\")"]), "true" ); assert_eq!( - jsc(&["-e", "let m=new Map(); m.set(\"a\",1); m.delete(\"x\")"]), + jsc(&["-p", "let m=new Map(); m.set(\"a\",1); m.delete(\"x\")"]), "false" ); assert_eq!( jsc(&[ - "-e", + "-p", "let m=new Map(); m.set(\"a\",1); m.delete(\"a\"); m.size" ]), "0" ); assert_eq!( jsc(&[ - "-e", + "-p", "let m=new Map(); m.set(\"a\",1); m.set(\"b\",2); m.set(\"c\",3); m.delete(\"b\"); \ m.get(\"a\")+m.get(\"c\")+\"/\"+m.size" ]), @@ -124,7 +124,7 @@ fn map_delete_via_eval() { ); assert_eq!( jsc(&[ - "-e", + "-p", "let m=new Map(); m.set(\"a\",1); m.delete(\"a\"); m.set(\"a\",5); m.get(\"a\")" ]), "5" @@ -137,21 +137,21 @@ fn map_delete_via_eval() { fn set_ops_via_eval() { assert_eq!( jsc(&[ - "-e", + "-p", "let s=new Set(); s.add(1); s.add(2); s.delete(1); s.has(1)" ]), "false" ); assert_eq!( jsc(&[ - "-e", + "-p", "let s=new Set(); s.add(1); s.add(2); s.delete(1); s.size" ]), "1" ); assert_eq!( jsc(&[ - "-e", + "-p", "let s=new Set(); s.add(\"x\"); s.add(\"x\"); s.add(\"y\"); s.size" ]), "2" @@ -162,14 +162,14 @@ fn set_ops_via_eval() { /// render with their entry count. #[test] fn map_set_render_and_chain_via_eval() { - assert_eq!(jsc(&["-e", "new Map()"]), "Map(0)"); - assert_eq!(jsc(&["-e", "new Set()"]), "Set(0)"); + assert_eq!(jsc(&["-p", "new Map()"]), "Map(0)"); + assert_eq!(jsc(&["-p", "new Set()"]), "Set(0)"); assert_eq!( - jsc(&["-e", "let s=new Set(); s.add(1); s.add(2); s"]), + jsc(&["-p", "let s=new Set(); s.add(1); s.add(2); s"]), "Set(2)" ); assert_eq!( - jsc(&["-e", "let m=new Map(); m.set(\"a\",1).set(\"b\",2); m.size"]), + jsc(&["-p", "let m=new Map(); m.set(\"a\",1).set(\"b\",2); m.size"]), "2" ); } @@ -180,12 +180,12 @@ fn map_set_render_and_chain_via_eval() { fn method_name_fallback_to_user_method() { assert_eq!( jsc(&[ - "-e", + "-p", "class C{constructor(){this.base=10} add(x){return this.base+x}} new C().add(5)" ]), "15" ); - assert_eq!(jsc(&["-e", "let o={get(){return 7}}; o.get()"]), "7"); + assert_eq!(jsc(&["-p", "let o={get(){return 7}}; o.get()"]), "7"); } /// Out-of-scope forms are rejected with a span: constructor arguments beyond the diff --git a/crates/jsc/tests/control_flow.rs b/crates/jsc/tests/control_flow.rs index 73741de..d60351e 100644 --- a/crates/jsc/tests/control_flow.rs +++ b/crates/jsc/tests/control_flow.rs @@ -8,17 +8,17 @@ use common::jsc; fn c_style_for_via_eval() { // Brace body, `for`-`let` binding. assert_eq!( - jsc(&["-e", "let s=0; for (let i=0;i<5;i=i+1) { s = s + i } s"]), + jsc(&["-p", "let s=0; for (let i=0;i<5;i=i+1) { s = s + i } s"]), "10" ); // Single-statement (no-brace) body. assert_eq!( - jsc(&["-e", "let s=0; for (let i=0;i<5;i=i+1) s = s + i; s"]), + jsc(&["-p", "let s=0; for (let i=0;i<5;i=i+1) s = s + i; s"]), "10" ); // Empty init and empty update sections. assert_eq!( - jsc(&["-e", "let i=0; let s=0; for(;i<3;){ s=s+1; i=i+1 } s"]), + jsc(&["-p", "let i=0; let s=0; for(;i<3;){ s=s+1; i=i+1 } s"]), "3" ); } @@ -29,7 +29,7 @@ fn switch_case_via_eval() { // Matching case with `break`. assert_eq!( jsc(&[ - "-e", + "-p", "let x=2; let r=0; switch(x){ case 1: r=10; break; case 2: r=42; break; default: r=99 } r" ]), "42" @@ -37,7 +37,7 @@ fn switch_case_via_eval() { // `default` runs when no case matches. assert_eq!( jsc(&[ - "-e", + "-p", "let x=9; let r=0; switch(x){ case 1: r=10; break; default: r=99 } r" ]), "99" @@ -45,7 +45,7 @@ fn switch_case_via_eval() { // Empty case falls through to the next case body. assert_eq!( jsc(&[ - "-e", + "-p", "let x=1; let r=0; switch(x){ case 1: case 2: r=42; break; default: r=0 } r" ]), "42" @@ -53,7 +53,7 @@ fn switch_case_via_eval() { // A case without `break` falls through into the following case body. assert_eq!( jsc(&[ - "-e", + "-p", "let x=1; let r=0; switch(x){ case 1: r=1; case 2: r=r+40; break; default: } r" ]), "41" @@ -61,7 +61,7 @@ fn switch_case_via_eval() { // `default` in the middle falls through into the following case (source order). assert_eq!( jsc(&[ - "-e", + "-p", "let x=5; let r=0; switch(x){ case 1: r=1; break; default: r=r+3; case 2: r=r+4; break } r" ]), "7" @@ -69,7 +69,7 @@ fn switch_case_via_eval() { // No match and no `default`: the switch is a no-op. assert_eq!( jsc(&[ - "-e", + "-p", "let x=7; let r=0; switch(x){ case 1: r=1; break; case 2: r=2; break } r" ]), "0" @@ -77,7 +77,7 @@ fn switch_case_via_eval() { // `break` nested inside an `if` still exits the switch. assert_eq!( jsc(&[ - "-e", + "-p", "let x=1; let r=0; switch(x){ case 1: if(x>0){ r=42; break } r=5; break; default: r=9 } r" ]), "42" @@ -85,7 +85,7 @@ fn switch_case_via_eval() { // Strict-equality matching on a string discriminant. assert_eq!( jsc(&[ - "-e", + "-p", "let x=\"b\"; let r=0; switch(x){ case \"a\": r=1; break; case \"b\": r=2; break } r" ]), "2" @@ -97,18 +97,18 @@ fn switch_case_via_eval() { fn for_of_via_eval() { // Milestone 1: iterate array elements, accumulating their sum. assert_eq!( - jsc(&["-e", "let s=0; for (const v of [10,20,30]) { s = s + v } s"]), + jsc(&["-p", "let s=0; for (const v of [10,20,30]) { s = s + v } s"]), "60" ); // Milestone 2: iterate a string's characters, rebuilding it. assert_eq!( - jsc(&["-e", r#"let r=""; for (const c of "abc") { r = r + c } r"#]), + jsc(&["-p", r#"let r=""; for (const c of "abc") { r = r + c } r"#]), "abc" ); // Milestone 3: `break` exits the loop early. assert_eq!( jsc(&[ - "-e", + "-p", "let s=0; for (const v of [1,2,3,4]) { if (v===3) break; s=s+v } s" ]), "3" @@ -116,20 +116,20 @@ fn for_of_via_eval() { // Milestone 4: `continue` skips an iteration but still advances the index. assert_eq!( jsc(&[ - "-e", + "-p", "let s=0; for (const v of [1,2,3,4]) { if (v===2) continue; s=s+v } s" ]), "8" ); // Single-statement (no-brace) body, and a `let` binding. assert_eq!( - jsc(&["-e", "let s=0; for (let v of [1,2,3]) s = s + v; s"]), + jsc(&["-p", "let s=0; for (let v of [1,2,3]) s = s + v; s"]), "6" ); // The loop binding is scoped to the loop and does not leak past it. assert_eq!( jsc(&[ - "-e", + "-p", "let last=0; for (const v of [7,8,9]) { last = v } last" ]), "9" @@ -141,17 +141,17 @@ fn for_of_via_eval() { #[test] fn block_scoping_via_eval() { // Inner `let x` shadows the outer; the outer binding survives the block. - assert_eq!(jsc(&["-e", "let x=1; { let x=2; } x"]), "1"); + assert_eq!(jsc(&["-p", "let x=1; { let x=2; } x"]), "1"); // The inner block sees its own binding while it is in scope. - assert_eq!(jsc(&["-e", "let x=1; let r=0; { let x=2; r=x } r"]), "2"); + assert_eq!(jsc(&["-p", "let x=1; let r=0; { let x=2; r=x } r"]), "2"); // Sibling blocks reuse a name for distinct bindings. assert_eq!( - jsc(&["-e", "let r=0; { let x=1; r=r+x } { let x=2; r=r+x } r"]), + jsc(&["-p", "let r=0; { let x=1; r=r+x } { let x=2; r=r+x } r"]), "3" ); // A `for`-let binding is scoped to the loop and does not leak past it. assert_eq!( - jsc(&["-e", "let s=0; for (let i=0;i<3;i=i+1) { s = s + i } s"]), + jsc(&["-p", "let s=0; for (let i=0;i<3;i=i+1) { s = s + i } s"]), "3" ); } @@ -162,16 +162,16 @@ fn block_scoping_via_eval() { fn do_while_via_eval() { // Body runs while the condition holds, accumulating across iterations. assert_eq!( - jsc(&["-e", "let i=0,s=0; do { s=s+i; i=i+1 } while (i<3); s"]), + jsc(&["-p", "let i=0,s=0; do { s=s+i; i=i+1 } while (i<3); s"]), "3" ); // The body runs at least once even when the condition is immediately false. - assert_eq!(jsc(&["-e", "let n=0; do { n=n+1 } while (false); n"]), "1"); + assert_eq!(jsc(&["-p", "let n=0; do { n=n+1 } while (false); n"]), "1"); // `continue` in a `do`-`while` jumps to the condition test (re-evaluated), // still terminating the loop. assert_eq!( jsc(&[ - "-e", + "-p", "let i=0,s=0; do { i=i+1; if (i===2) continue; s=s+i } while (i<4); s" ]), "8" @@ -179,7 +179,7 @@ fn do_while_via_eval() { // `break` exits the `do`-`while` immediately. assert_eq!( jsc(&[ - "-e", + "-p", "let n=0; do { n=n+1; if (n===2) break } while (true); n" ]), "2" @@ -194,7 +194,7 @@ fn labeled_break_continue_via_eval() { // `break LABEL` from an inner loop exits the labeled outer loop entirely. assert_eq!( jsc(&[ - "-e", + "-p", "let n=0; outer: for(let i=0;i<3;i=i+1){ for(let j=0;j<3;j=j+1){ if(j===1) break outer; n=n+1 } } n" ]), "1" @@ -202,7 +202,7 @@ fn labeled_break_continue_via_eval() { // `continue LABEL` skips to the labeled outer loop's next iteration. assert_eq!( jsc(&[ - "-e", + "-p", "let n=0; outer: for(let i=0;i<3;i=i+1){ for(let j=0;j<3;j=j+1){ if(j===1) continue outer; n=n+1 } } n" ]), "3" @@ -211,7 +211,7 @@ fn labeled_break_continue_via_eval() { // plain `break`. assert_eq!( jsc(&[ - "-e", + "-p", "let s=0; L: for(let i=0;i<10;i=i+1){ if(i===5) break L; s=s+1 } s" ]), "5" @@ -219,7 +219,7 @@ fn labeled_break_continue_via_eval() { // A label may name a `do`-`while`, and `break LABEL` exits it. assert_eq!( jsc(&[ - "-e", + "-p", "let n=0; L: do { n=n+1; if(n===3) break L } while(true); n" ]), "3" @@ -227,7 +227,7 @@ fn labeled_break_continue_via_eval() { // `break LABEL` still runs an intervening `finally` on the way out (S15.3). assert_eq!( jsc(&[ - "-e", + "-p", "let n=0,f=0; outer: for(let i=0;i<3;i=i+1){ for(let j=0;j<3;j=j+1){ try { if(j===1) break outer; n=n+1 } finally { f=f+1 } } } n*10+f" ]), "12" diff --git a/crates/jsc/tests/destructuring.rs b/crates/jsc/tests/destructuring.rs index 6b0b1fc..0354fe6 100644 --- a/crates/jsc/tests/destructuring.rs +++ b/crates/jsc/tests/destructuring.rs @@ -10,17 +10,17 @@ use common::jsc; #[test] fn array_destructuring_declarations_via_eval() { // Plain two-element bind. - assert_eq!(jsc(&["-e", "let [a,b] = [1,2]; a+b"]), "3"); + assert_eq!(jsc(&["-p", "let [a,b] = [1,2]; a+b"]), "3"); // A hole skips its index. - assert_eq!(jsc(&["-e", "let [,b] = [1,2]; b"]), "2"); + assert_eq!(jsc(&["-p", "let [,b] = [1,2]; b"]), "2"); // A default fills a missing (undefined) slot. - assert_eq!(jsc(&["-e", "let [a=9] = []; a"]), "9"); + assert_eq!(jsc(&["-p", "let [a=9] = []; a"]), "9"); // A rest collects the remaining elements into a new array. - assert_eq!(jsc(&["-e", "let [x, ...r] = [1,2,3]; r.length"]), "2"); + assert_eq!(jsc(&["-p", "let [x, ...r] = [1,2,3]; r.length"]), "2"); // `const` bindings destructure the same way. - assert_eq!(jsc(&["-e", "const [a,b]=[3,4]; a+b"]), "7"); + assert_eq!(jsc(&["-p", "const [a,b]=[3,4]; a+b"]), "7"); // A nested pattern recurses into a sub-array. - assert_eq!(jsc(&["-e", "let [[a]] = [[5]]; a"]), "5"); + assert_eq!(jsc(&["-p", "let [[a]] = [[5]]; a"]), "5"); } /// P1.2 milestones: object destructuring in declarations desugars to keyed reads @@ -29,21 +29,21 @@ fn array_destructuring_declarations_via_eval() { #[test] fn object_destructuring_declarations_via_eval() { // Shorthand two-key bind. - assert_eq!(jsc(&["-e", "let {x,y} = {x:40,y:2}; x+y"]), "42"); + assert_eq!(jsc(&["-p", "let {x,y} = {x:40,y:2}; x+y"]), "42"); // A renamed bind (`x: a`) binds the value under the new name. - assert_eq!(jsc(&["-e", "let {x:a} = {x:5}; a"]), "5"); + assert_eq!(jsc(&["-p", "let {x:a} = {x:5}; a"]), "5"); // A default fills a missing (undefined) key. - assert_eq!(jsc(&["-e", "let {z=7} = {}; z"]), "7"); + assert_eq!(jsc(&["-p", "let {z=7} = {}; z"]), "7"); // A rest collects every remaining own key into a new object. assert_eq!( - jsc(&["-e", "let {a, ...rest} = {a:1,b:2,c:3}; rest.b"]), + jsc(&["-p", "let {a, ...rest} = {a:1,b:2,c:3}; rest.b"]), "2" ); // The rest object excludes the destructured keys. assert_eq!( - jsc(&["-e", "let {a, ...rest} = {a:1,b:2,c:3}; rest.a"]), + jsc(&["-p", "let {a, ...rest} = {a:1,b:2,c:3}; rest.a"]), "undefined" ); // A nested object pattern recurses into a sub-object. - assert_eq!(jsc(&["-e", "let {p:{q}} = {p:{q:9}}; q"]), "9"); + assert_eq!(jsc(&["-p", "let {p:{q}} = {p:{q:9}}; q"]), "9"); } diff --git a/crates/jsc/tests/errors.rs b/crates/jsc/tests/errors.rs index f75333a..7f6e668 100644 --- a/crates/jsc/tests/errors.rs +++ b/crates/jsc/tests/errors.rs @@ -6,7 +6,7 @@ mod common; use common::jsc; /// S12.6 — reassigning a `const` binding is a compile error: the CLI exits -/// non-zero and reports the constant on stderr. +/// 2 and reports the constant on stderr. #[test] fn const_reassignment_fails_at_compile_time() { let output = Command::new(env!("CARGO_BIN_EXE_jsc")) @@ -14,8 +14,8 @@ fn const_reassignment_fails_at_compile_time() { .output() .expect("failed to spawn jsc binary"); assert!( - !output.status.success(), - "reassigning a const must make jsc exit non-zero" + output.status.code() == Some(2), + "reassigning a const must make jsc exit 2" ); let stderr = String::from_utf8_lossy(&output.stderr); assert!( @@ -25,7 +25,7 @@ fn const_reassignment_fails_at_compile_time() { } /// S13.7 — reassigning a `const` remains a compile error end to end (the CLI -/// exits non-zero and names the constant), confirming S13's member-compound and +/// exits 2 and names the constant), confirming S13's member-compound and /// closure work did not weaken the S12 immutability check. #[test] fn s13_const_reassignment_still_errors() { @@ -34,8 +34,8 @@ fn s13_const_reassignment_still_errors() { .output() .expect("failed to spawn jsc binary"); assert!( - !output.status.success(), - "reassigning a const must make jsc exit non-zero" + output.status.code() == Some(2), + "reassigning a const must make jsc exit 2" ); let stderr = String::from_utf8_lossy(&output.stderr); assert!( @@ -46,14 +46,14 @@ fn s13_const_reassignment_still_errors() { /// The diagnostic renderer points a caret at the offending source: a spanned /// `CompileError` (const-reassignment) prints a rustc/swc-style block on stderr -/// with `:LINE:COL` and a `^` caret, and the CLI exits non-zero. +/// with `:LINE:COL` and a `^` caret, and the CLI exits 2. #[test] fn const_reassignment_renders_caret_diagnostic() { let output = Command::new(env!("CARGO_BIN_EXE_jsc")) .args(["-e", "const c = 1; c = 2"]) .output() .expect("failed to spawn jsc binary"); - assert!(!output.status.success(), "must exit non-zero"); + assert!(output.status.code() == Some(2), "must exit 2"); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains("error: cannot assign to constant binding: c"), @@ -81,7 +81,7 @@ fn unsupported_statement_renders_caret_diagnostic() { .args(["-e", "debugger"]) .output() .expect("failed to spawn jsc binary"); - assert!(!output.status.success(), "must exit non-zero"); + assert!(output.status.code() == Some(2), "must exit 2"); let stderr = String::from_utf8_lossy(&output.stderr); assert!(stderr.contains("--> :1:"), "got: {stderr}"); assert!(stderr.contains('^'), "got: {stderr}"); @@ -96,7 +96,7 @@ fn undeclared_identifier_renders_caret_at_column_one() { .args(["-e", "NaN2 + 1"]) .output() .expect("failed to spawn jsc binary"); - assert!(!output.status.success(), "must exit non-zero"); + assert!(output.status.code() == Some(2), "must exit 2"); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains(":1:1"), @@ -118,7 +118,7 @@ fn unsupported_construct_renders_caret_diagnostic() { .args(["-e", "let f = async () => 1;"]) .output() .expect("failed to spawn jsc binary"); - assert!(!output.status.success(), "must exit non-zero"); + assert!(output.status.code() == Some(2), "must exit 2"); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains(":1:"), @@ -142,7 +142,7 @@ fn for_of_renders_clean_message_without_ast_debug() { .args(["-e", "for (x of [1,2]) x"]) .output() .expect("failed to spawn jsc binary"); - assert!(!output.status.success(), "must exit non-zero"); + assert!(output.status.code() == Some(2), "must exit 2"); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains("for-of requires a `const`/`let` loop variable"), @@ -177,7 +177,7 @@ fn parse_error_renders_caret_diagnostic() { .args(["-e", "let x = ("]) .output() .expect("failed to spawn jsc binary"); - assert!(!output.status.success(), "must exit non-zero"); + assert!(output.status.code() == Some(2), "must exit 2"); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains(":1:"), @@ -215,8 +215,8 @@ fn compile_errors_render_locator_and_caret() { .output() .expect("failed to spawn jsc binary"); assert!( - !output.status.success(), - "{what} (`{src}`) must make jsc exit non-zero" + output.status.code() == Some(2), + "{what} (`{src}`) must make jsc exit 2" ); let stderr = String::from_utf8_lossy(&output.stderr); assert!( @@ -231,35 +231,35 @@ fn compile_errors_render_locator_and_caret() { } /// S15 — the exception system (`throw`/`try`/`catch`/`finally`/`Error`) driven -/// through the real CLI on the `-e` path, one milestone per assertion. +/// through the real CLI on the `-p` path, one milestone per assertion. #[test] fn s15_exceptions_via_eval() { // try/catch binds the thrown number and, separately, the thrown string. assert_eq!( - jsc(&["-e", "let r = 0; try { throw 42 } catch (e) { r = e } r"]), + jsc(&["-p", "let r = 0; try { throw 42 } catch (e) { r = e } r"]), "42" ); assert_eq!( jsc(&[ - "-e", + "-p", r#"let r = 0; try { throw "boom" } catch (e) { r = e } r"# ]), "boom" ); // No-throw path: the catch is skipped and the try's own value flows out. assert_eq!( - jsc(&["-e", "let r = 42; try { r = r } catch (e) { r = -1 } r"]), + jsc(&["-p", "let r = 42; try { r = r } catch (e) { r = -1 } r"]), "42" ); // finally on the normal path runs (x ends 42) ... assert_eq!( - jsc(&["-e", "let x = 40; try { x = 40 } finally { x = x + 2 } x"]), + jsc(&["-p", "let x = 40; try { x = 40 } finally { x = x + 2 } x"]), "42" ); // ... and finally still runs after a caught throw (x ends 42). assert_eq!( jsc(&[ - "-e", + "-p", "let x = 0; try { throw 1 } catch (e) { x = 40 } finally { x = x + 2 } x" ]), "42" @@ -267,7 +267,7 @@ fn s15_exceptions_via_eval() { // Propagation across functions: f throws, g calls f, an outer try catches. assert_eq!( jsc(&[ - "-e", + "-p", "function f(){ throw 42 } function g(){ f() } \ let r = 0; try { g() } catch (e) { r = e } r" ]), @@ -276,17 +276,17 @@ fn s15_exceptions_via_eval() { // new Error(msg): `.message` reads the argument back; a thrown Error is caught. assert_eq!( jsc(&[ - "-e", + "-p", r#"let r = 0; try { throw new Error("bad") } catch (e) { r = e.message } r"# ]), "bad" ); // An Error reuses the OBJECT heap representation, so `typeof` is "object". - assert_eq!(jsc(&["-e", r#"typeof (new Error("x"))"#]), "object"); + assert_eq!(jsc(&["-p", r#"typeof (new Error("x"))"#]), "object"); // Nested try + rethrow: inner catch transforms the value, outer catch binds 42. assert_eq!( jsc(&[ - "-e", + "-p", "let r = 0; try { try { throw 1 } catch (e) { throw e + 41 } } \ catch (e) { r = e } r" ]), @@ -295,17 +295,17 @@ fn s15_exceptions_via_eval() { } /// S15 — an UNCAUGHT `throw` (no enclosing handler) must fail the process: the -/// CLI exits non-zero and reports the unwound value as `Uncaught ` on +/// CLI exits 1 and reports the unwound value as `Uncaught ` on /// stderr, never printing a normal result to stdout. #[test] -fn s15_uncaught_throw_exits_nonzero() { +fn s15_uncaught_throw_exits_one() { let output = Command::new(env!("CARGO_BIN_EXE_jsc")) .args(["-e", "throw 42"]) .output() .expect("failed to spawn jsc binary"); assert!( - !output.status.success(), - "an uncaught throw must make jsc exit non-zero" + output.status.code() == Some(1), + "an uncaught throw must make jsc exit 1" ); let stderr = String::from_utf8_lossy(&output.stderr); assert!( @@ -319,7 +319,7 @@ fn s15_uncaught_throw_exits_nonzero() { } /// P2.1 — out-of-scope object method forms (getter/setter, computed method name, -/// async/generator methods) are rejected at compile time with a non-zero exit and +/// async/generator methods) are rejected at compile time with exit code 2 and /// an explanatory message. #[test] fn p2_unsupported_object_methods_error() { @@ -336,8 +336,8 @@ fn p2_unsupported_object_methods_error() { .output() .expect("failed to spawn jsc binary"); assert!( - !output.status.success(), - "`{src}` must make jsc exit non-zero" + output.status.code() == Some(2), + "`{src}` must make jsc exit 2" ); let stderr = String::from_utf8_lossy(&output.stderr); assert!( @@ -373,8 +373,8 @@ fn p2_unsupported_class_forms_error() { .output() .expect("failed to spawn jsc binary"); assert!( - !output.status.success(), - "`{src}` must make jsc exit non-zero" + output.status.code() == Some(2), + "`{src}` must make jsc exit 2" ); let stderr = String::from_utf8_lossy(&output.stderr); assert!( diff --git a/crates/jsc/tests/functions.rs b/crates/jsc/tests/functions.rs index b7a49b3..9b85270 100644 --- a/crates/jsc/tests/functions.rs +++ b/crates/jsc/tests/functions.rs @@ -9,18 +9,18 @@ use common::jsc; #[test] fn function_values_via_eval() { assert_eq!( - jsc(&["-e", "function add(a,b){return a+b} let f = add; f(40,2)"]), + jsc(&["-p", "function add(a,b){return a+b} let f = add; f(40,2)"]), "42" ); assert_eq!( jsc(&[ - "-e", + "-p", "function apply(f,x){return f(x)} function dbl(n){return n*2} apply(dbl,21)" ]), "42" ); assert_eq!( - jsc(&["-e", "function id(x){return x} let g=id; typeof g"]), + jsc(&["-p", "function id(x){return x} let g=id; typeof g"]), "function" ); } @@ -30,17 +30,17 @@ fn function_values_via_eval() { /// an arrow passed straight as a callback, and `typeof` over an arrow. #[test] fn arrow_functions_via_eval() { - assert_eq!(jsc(&["-e", "let add = (a,b) => a+b; add(40,2)"]), "42"); - assert_eq!(jsc(&["-e", "let sq = x => x*x; sq(7)"]), "49"); + assert_eq!(jsc(&["-p", "let add = (a,b) => a+b; add(40,2)"]), "42"); + assert_eq!(jsc(&["-p", "let sq = x => x*x; sq(7)"]), "49"); assert_eq!( - jsc(&["-e", "let f = (n) => { let t = n*2; return t+1 }; f(20)"]), + jsc(&["-p", "let f = (n) => { let t = n*2; return t+1 }; f(20)"]), "41" ); assert_eq!( - jsc(&["-e", "function apply(f,x){return f(x)} apply(x=>x*3, 14)"]), + jsc(&["-p", "function apply(f,x){return f(x)} apply(x=>x*3, 14)"]), "42" ); - assert_eq!(jsc(&["-e", "typeof (x=>x)"]), "function"); + assert_eq!(jsc(&["-p", "typeof (x=>x)"]), "function"); } /// S13.7 — the S13 acceptance milestones as one consolidated end-to-end gate @@ -53,23 +53,23 @@ fn arrow_functions_via_eval() { fn s13_acceptance_milestones() { // First-class function values: bind-and-call, pass-as-argument. assert_eq!( - jsc(&["-e", "function add(a,b){return a+b} let f=add; f(40,2)"]), + jsc(&["-p", "function add(a,b){return a+b} let f=add; f(40,2)"]), "42" ); assert_eq!( jsc(&[ - "-e", + "-p", "function apply(f,x){return f(x)} function dbl(n){return n*2} apply(dbl,21)" ]), "42" ); // Arrow functions: multi-param and single-param expression bodies. - assert_eq!(jsc(&["-e", "let add=(a,b)=>a+b; add(40,2)"]), "42"); - assert_eq!(jsc(&["-e", "let sq=x=>x*x; sq(7)"]), "49"); + assert_eq!(jsc(&["-p", "let add=(a,b)=>a+b; add(40,2)"]), "42"); + assert_eq!(jsc(&["-p", "let sq=x=>x*x; sq(7)"]), "49"); // Closures: returned arrow capturing a parameter by reference. assert_eq!( jsc(&[ - "-e", + "-p", "function adder(x){ return y=>x+y } let a5=adder(5); a5(37)" ]), "42" @@ -77,15 +77,15 @@ fn s13_acceptance_milestones() { // Closures: stateful counter mutating one shared cell across three calls. assert_eq!( jsc(&[ - "-e", + "-p", "function counter(){ let c=0; return function(){ c=c+1; return c } } \ let n=counter(); n();n();n()" ]), "3" ); // Member-target compound assignment: array element and object property. - assert_eq!(jsc(&["-e", "let a=[10,20]; a[0]+=5; a[0]"]), "15"); - assert_eq!(jsc(&["-e", "let o={x:1}; o.x+=41; o.x"]), "42"); + assert_eq!(jsc(&["-p", "let a=[10,20]; a[0]+=5; a[0]"]), "15"); + assert_eq!(jsc(&["-p", "let o={x:1}; o.x+=41; o.x"]), "42"); } /// P1.3 — default parameters: a missing or explicitly-`undefined` argument @@ -94,16 +94,16 @@ fn s13_acceptance_milestones() { #[test] fn default_parameters() { // Literal default used when the argument is omitted, kept when supplied. - assert_eq!(jsc(&["-e", "function f(a, b=2){ return a+b } f(40)"]), "42"); + assert_eq!(jsc(&["-p", "function f(a, b=2){ return a+b } f(40)"]), "42"); assert_eq!( - jsc(&["-e", "function f(a, b=2){ return a+b } f(40, 100)"]), + jsc(&["-p", "function f(a, b=2){ return a+b } f(40, 100)"]), "140" ); // A default referencing an earlier parameter. - assert_eq!(jsc(&["-e", "function g(a, b=a+1){ return b } g(41)"]), "42"); + assert_eq!(jsc(&["-p", "function g(a, b=a+1){ return b } g(41)"]), "42"); // Explicit `undefined` triggers the default, like an omitted argument. assert_eq!( - jsc(&["-e", "function f(a, b=2){ return a+b } f(40, undefined)"]), + jsc(&["-p", "function f(a, b=2){ return a+b } f(40, undefined)"]), "42" ); } @@ -115,40 +115,40 @@ fn default_parameters() { fn parameter_destructuring() { // Array-pattern and object-pattern parameters (the core milestones). assert_eq!( - jsc(&["-e", "function f([a,b]){ return a+b } f([40,2])"]), + jsc(&["-p", "function f([a,b]){ return a+b } f([40,2])"]), "42" ); assert_eq!( - jsc(&["-e", "function g({x,y}){ return x*y } g({x:6,y:7})"]), + jsc(&["-p", "function g({x,y}){ return x*y } g({x:6,y:7})"]), "42" ); // A nested default inside an array-pattern parameter fills a missing element. assert_eq!( - jsc(&["-e", "function d([a,b=5]){ return a+b } d([40])"]), + jsc(&["-p", "function d([a,b=5]){ return a+b } d([40])"]), "45" ); // Object-pattern rename plus a shorthand default. assert_eq!( - jsc(&["-e", "function g({x:m,y=10}){ return m+y } g({x:32})"]), + jsc(&["-p", "function g({x:m,y=10}){ return m+y } g({x:32})"]), "42" ); // A nested object pattern binds a deep property. assert_eq!( - jsc(&["-e", "function h({p:{q}}){ return q } h({p:{q:42}})"]), + jsc(&["-p", "function h({p:{q}}){ return q } h({p:{q:42}})"]), "42" ); // A whole-pattern (parameter-level) default applies when the argument is // omitted, and is ignored when supplied. assert_eq!( - jsc(&["-e", "function f([a,b]=[40,2]){ return a+b } f()"]), + jsc(&["-p", "function f([a,b]=[40,2]){ return a+b } f()"]), "42" ); assert_eq!( - jsc(&["-e", "function f([a,b]=[1,2]){ return a+b } f([40,2])"]), + jsc(&["-p", "function f([a,b]=[1,2]){ return a+b } f([40,2])"]), "42" ); // A destructuring parameter on an arrow. - assert_eq!(jsc(&["-e", "let f=([a,b])=>a+b; f([40,2])"]), "42"); + assert_eq!(jsc(&["-p", "let f=([a,b])=>a+b; f([40,2])"]), "42"); } /// P1.4 — a trailing rest parameter gathers every argument from its position @@ -159,17 +159,17 @@ fn parameter_destructuring() { fn rest_parameters() { // The rest binding collects all arguments; its length is the actual count. assert_eq!( - jsc(&["-e", "function f(...xs){ return xs.length } f(1,2,3)"]), + jsc(&["-p", "function f(...xs){ return xs.length } f(1,2,3)"]), "3" ); assert_eq!( - jsc(&["-e", "function f(...xs){ return xs.length } f()"]), + jsc(&["-p", "function f(...xs){ return xs.length } f()"]), "0" ); // A leading fixed parameter, then the rest of the arguments. assert_eq!( jsc(&[ - "-e", + "-p", "function h(a, ...rest){ return a + rest.length } h(10,1,1)" ]), "12" @@ -177,18 +177,18 @@ fn rest_parameters() { // The rest binding is a normal array: higher-order methods and indexing work. assert_eq!( jsc(&[ - "-e", + "-p", "function m(...xs){ return xs.map(x=>x*2)[1] } m(1,2,3)" ]), "4" ); // A rest arrow, and a rest function called indirectly through a variable. assert_eq!( - jsc(&["-e", "let f = (...xs) => xs.length; f(1,2,3,4,5)"]), + jsc(&["-p", "let f = (...xs) => xs.length; f(1,2,3,4,5)"]), "5" ); assert_eq!( - jsc(&["-e", "function f(...xs){return xs.length} let g=f; g(1,2)"]), + jsc(&["-p", "function f(...xs){return xs.length} let g=f; g(1,2)"]), "2" ); } @@ -199,12 +199,12 @@ fn rest_parameters() { #[test] fn p1_5_call_spread_via_eval() { assert_eq!( - jsc(&["-e", "function add(a,b,c){return a+b+c} add(...[10,20,12])"]), + jsc(&["-p", "function add(a,b,c){return a+b+c} add(...[10,20,12])"]), "42" ); // Plain and spread arguments mixed at one call site. assert_eq!( - jsc(&["-e", "function add(a,b,c){return a+b+c} add(10,...[20,12])"]), + jsc(&["-p", "function add(a,b,c){return a+b+c} add(10,...[20,12])"]), "42" ); // A rest-parameter callee collects the spread via the dynamic argc. The @@ -212,18 +212,18 @@ fn p1_5_call_spread_via_eval() { // `sum(1,2,3,4)` call) must cover the spread's runtime length. assert_eq!( jsc(&[ - "-e", + "-p", "function sum(...xs){let t=0;for(const x of xs)t=t+x;return t} \ sum(1,2,3,4); sum(...[1,2,3,4])" ]), "10" ); // Indirect spread call through a bound function value. - assert_eq!(jsc(&["-e", "let f=(a,b)=>a*b; f(...[6,7])"]), "42"); + assert_eq!(jsc(&["-p", "let f=(a,b)=>a*b; f(...[6,7])"]), "42"); // A spread whose source is a bound array variable. assert_eq!( jsc(&[ - "-e", + "-p", "function add(a,b,c){return a+b+c} let xs=[10,20,12]; add(...xs)" ]), "42" diff --git a/crates/jsc/tests/numbers.rs b/crates/jsc/tests/numbers.rs index fad16d4..dfa6929 100644 --- a/crates/jsc/tests/numbers.rs +++ b/crates/jsc/tests/numbers.rs @@ -3,20 +3,20 @@ mod common; use common::jsc; -/// The S8 milestone programs, run inline with `-e`, print their numbers. +/// The S8 milestone programs, run inline with `-p`, print their numbers. /// /// One program per language wave: a bare expression, a `while` loop with a /// comparison, and a function call — all multi-statement except the first. #[test] fn milestones_via_eval() { - assert_eq!(jsc(&["-e", "40 + 2"]), "42"); + assert_eq!(jsc(&["-p", "40 + 2"]), "42"); assert_eq!( - jsc(&["-e", "function add(a, b) { return a + b } add(40, 2)"]), + jsc(&["-p", "function add(a, b) { return a + b } add(40, 2)"]), "42" ); assert_eq!( jsc(&[ - "-e", + "-p", "let i=0; let s=0; while(i<10){ s = s + i; i = i + 1 } s" ]), "45", @@ -29,18 +29,18 @@ fn milestones_via_eval() { /// guards that the number path survived the carrier change unchanged. #[test] fn number_regression_survives_jsval() { - assert_eq!(jsc(&["-e", "40+2"]), "42"); - assert_eq!(jsc(&["-e", "let x=40;x+2"]), "42"); + assert_eq!(jsc(&["-p", "40+2"]), "42"); + assert_eq!(jsc(&["-p", "let x=40;x+2"]), "42"); assert_eq!( jsc(&[ - "-e", + "-p", "let i=0; let s=0; while(i<10){ s = s + i; i = i + 1 } s" ]), "45", ); assert_eq!( jsc(&[ - "-e", + "-p", "function fib(n){ if(n<2) return n; return fib(n-1)+fib(n-2) } fib(10)" ]), "55", @@ -51,27 +51,27 @@ fn number_regression_survives_jsval() { /// Rust's `inf`/`-inf`, while finite renders stay byte-for-byte unchanged. #[test] fn non_finite_renders_js_style() { - assert_eq!(jsc(&["-e", "1/0"]), "Infinity"); - assert_eq!(jsc(&["-e", "-1/0"]), "-Infinity"); - assert_eq!(jsc(&["-e", "0/0"]), "NaN"); + assert_eq!(jsc(&["-p", "1/0"]), "Infinity"); + assert_eq!(jsc(&["-p", "-1/0"]), "-Infinity"); + assert_eq!(jsc(&["-p", "0/0"]), "NaN"); // Finite regressions: integers and shortest fractionals are untouched. - assert_eq!(jsc(&["-e", "42"]), "42"); - assert_eq!(jsc(&["-e", "4.5"]), "4.5"); - assert_eq!(jsc(&["-e", "0.5"]), "0.5"); + assert_eq!(jsc(&["-p", "42"]), "42"); + assert_eq!(jsc(&["-p", "4.5"]), "4.5"); + assert_eq!(jsc(&["-p", "0.5"]), "0.5"); } /// Negative zero renders as `"0"` (JS `String(-0)`), while the stored f64 sign /// is preserved so arithmetic still distinguishes `-0` from `+0`. #[test] fn negative_zero_renders_zero() { - assert_eq!(jsc(&["-e", "-0"]), "0"); - assert_eq!(jsc(&["-e", "0 * -1"]), "0"); + assert_eq!(jsc(&["-p", "-0"]), "0"); + assert_eq!(jsc(&["-p", "0 * -1"]), "0"); // Sign survives in math: 1 / -0 is -Infinity, not Infinity. - assert_eq!(jsc(&["-e", "1 / (0 * -1)"]), "-Infinity"); + assert_eq!(jsc(&["-p", "1 / (0 * -1)"]), "-Infinity"); // Regression: plain and nonzero renders are unchanged. - assert_eq!(jsc(&["-e", "0"]), "0"); - assert_eq!(jsc(&["-e", "-5"]), "-5"); - assert_eq!(jsc(&["-e", "5"]), "5"); + assert_eq!(jsc(&["-p", "0"]), "0"); + assert_eq!(jsc(&["-p", "-5"]), "-5"); + assert_eq!(jsc(&["-p", "5"]), "5"); } /// Large/small magnitudes render in exponential form per the ECMAScript @@ -81,32 +81,32 @@ fn negative_zero_renders_zero() { #[test] fn exponential_tostring_thresholds() { // Exponential above/below the fixed-form window. - assert_eq!(jsc(&["-e", "1e21"]), "1e+21"); - assert_eq!(jsc(&["-e", "1e-7"]), "1e-7"); - assert_eq!(jsc(&["-e", "1e21 + 0"]), "1e+21"); - assert_eq!(jsc(&["-e", "1.5e22"]), "1.5e+22"); - assert_eq!(jsc(&["-e", "-1e21"]), "-1e+21"); + assert_eq!(jsc(&["-p", "1e21"]), "1e+21"); + assert_eq!(jsc(&["-p", "1e-7"]), "1e-7"); + assert_eq!(jsc(&["-p", "1e21 + 0"]), "1e+21"); + assert_eq!(jsc(&["-p", "1.5e22"]), "1.5e+22"); + assert_eq!(jsc(&["-p", "-1e21"]), "-1e+21"); // The boundary just inside the window is still fixed. - assert_eq!(jsc(&["-e", "1e-6"]), "0.000001"); + assert_eq!(jsc(&["-p", "1e-6"]), "0.000001"); // Fixed-form regressions across the ordinary range. - assert_eq!(jsc(&["-e", "123"]), "123"); - assert_eq!(jsc(&["-e", "100000"]), "100000"); - assert_eq!(jsc(&["-e", "0.5"]), "0.5"); - assert_eq!(jsc(&["-e", "9/2"]), "4.5"); + assert_eq!(jsc(&["-p", "123"]), "123"); + assert_eq!(jsc(&["-p", "100000"]), "100000"); + assert_eq!(jsc(&["-p", "0.5"]), "0.5"); + assert_eq!(jsc(&["-p", "9/2"]), "4.5"); } /// `NaN` and `Infinity` resolve as read-only global number bindings, sharing /// the same value path as their computed counterparts. #[test] fn nan_infinity_global_bindings() { - assert_eq!(jsc(&["-e", "typeof NaN"]), "number"); - assert_eq!(jsc(&["-e", "typeof Infinity"]), "number"); - assert_eq!(jsc(&["-e", "Infinity > 1e300"]), "true"); + assert_eq!(jsc(&["-p", "typeof NaN"]), "number"); + assert_eq!(jsc(&["-p", "typeof Infinity"]), "number"); + assert_eq!(jsc(&["-p", "Infinity > 1e300"]), "true"); // NaN is never equal to itself, exactly like a computed `0/0`. - assert_eq!(jsc(&["-e", "NaN === NaN"]), "false"); - assert_eq!(jsc(&["-e", "-Infinity < 0"]), "true"); + assert_eq!(jsc(&["-p", "NaN === NaN"]), "false"); + assert_eq!(jsc(&["-p", "-Infinity < 0"]), "true"); // Rendering of the bindings themselves is unchanged. - assert_eq!(jsc(&["-e", "Infinity"]), "Infinity"); - assert_eq!(jsc(&["-e", "-Infinity"]), "-Infinity"); - assert_eq!(jsc(&["-e", "NaN"]), "NaN"); + assert_eq!(jsc(&["-p", "Infinity"]), "Infinity"); + assert_eq!(jsc(&["-p", "-Infinity"]), "-Infinity"); + assert_eq!(jsc(&["-p", "NaN"]), "NaN"); } diff --git a/crates/jsc/tests/objects.rs b/crates/jsc/tests/objects.rs index 47abd28..ebfb918 100644 --- a/crates/jsc/tests/objects.rs +++ b/crates/jsc/tests/objects.rs @@ -3,17 +3,17 @@ mod common; use common::jsc; -/// The S11.3 object milestones run inline with `-e` through the built binary. +/// The S11.3 object milestones run inline with `-p` through the built binary. /// /// Covers the object wave end to end: `.a` read + sum, computed `["a"]` read, /// a string-literal key, and `typeof {}`. #[test] fn object_milestones_via_eval() { - assert_eq!(jsc(&["-e", "let o={a:1,b:2}; o.a+o.b"]), "3"); - assert_eq!(jsc(&["-e", "let o={a:1}; o[\"a\"]"]), "1"); - assert_eq!(jsc(&["-e", "let o={x:10}; o.x"]), "10"); - assert_eq!(jsc(&["-e", "typeof {}"]), "object"); - assert_eq!(jsc(&["-e", "let o={\"k\":5}; o.k"]), "5"); + assert_eq!(jsc(&["-p", "let o={a:1,b:2}; o.a+o.b"]), "3"); + assert_eq!(jsc(&["-p", "let o={a:1}; o[\"a\"]"]), "1"); + assert_eq!(jsc(&["-p", "let o={x:10}; o.x"]), "10"); + assert_eq!(jsc(&["-p", "typeof {}"]), "object"); + assert_eq!(jsc(&["-p", "let o={\"k\":5}; o.k"]), "5"); } /// S11.4: `o.a = v` / `o["a"] = v` store into an EXISTING key's value slot; the @@ -21,10 +21,10 @@ fn object_milestones_via_eval() { /// documented no-op that must not corrupt existing entries. #[test] fn object_property_assignment_via_eval() { - assert_eq!(jsc(&["-e", "let o={x:10}; o.x=42; o.x"]), "42"); - assert_eq!(jsc(&["-e", "let o={a:1}; o[\"a\"]=7; o.a"]), "7"); - assert_eq!(jsc(&["-e", "let o={a:0}; let x=(o.a=9); x"]), "9"); - assert_eq!(jsc(&["-e", "let o={a:1}; o.b=5; o.a"]), "1"); + assert_eq!(jsc(&["-p", "let o={x:10}; o.x=42; o.x"]), "42"); + assert_eq!(jsc(&["-p", "let o={a:1}; o[\"a\"]=7; o.a"]), "7"); + assert_eq!(jsc(&["-p", "let o={a:0}; let x=(o.a=9); x"]), "9"); + assert_eq!(jsc(&["-p", "let o={a:1}; o.b=5; o.a"]), "1"); } /// S11.5: nested arrays and objects compose — arrays and objects store full @@ -33,15 +33,15 @@ fn object_property_assignment_via_eval() { #[test] fn nested_values_compose_via_eval() { // Object holding an array (the S11 milestone), read through both hops. - assert_eq!(jsc(&["-e", "let o={v:[7,8]}; o.v[1]"]), "8"); + assert_eq!(jsc(&["-p", "let o={v:[7,8]}; o.v[1]"]), "8"); // Array of objects — index then property, on each element. - assert_eq!(jsc(&["-e", "let a=[{n:1},{n:2}]; a[0].n + a[1].n"]), "3"); + assert_eq!(jsc(&["-p", "let a=[{n:1},{n:2}]; a[0].n + a[1].n"]), "3"); // Object of arrays, nested index past the first slot. - assert_eq!(jsc(&["-e", "let o={xs:[10,20,30]}; o.xs[2]"]), "30"); + assert_eq!(jsc(&["-p", "let o={xs:[10,20,30]}; o.xs[2]"]), "30"); // Deep object nesting — three property hops. - assert_eq!(jsc(&["-e", "let o={a:{b:{c:5}}}; o.a.b.c"]), "5"); + assert_eq!(jsc(&["-p", "let o={a:{b:{c:5}}}; o.a.b.c"]), "5"); // Array of arrays — two index hops. - assert_eq!(jsc(&["-e", "let a=[[1,2],[3,4]]; a[1][0]"]), "3"); + assert_eq!(jsc(&["-p", "let a=[[1,2],[3,4]]; a[1][0]"]), "3"); } /// S11.5: mutation reaches through a nesting hop and the write sticks. @@ -51,9 +51,9 @@ fn nested_values_compose_via_eval() { #[test] fn nested_mutation_via_eval() { // Write into an array held by an object. - assert_eq!(jsc(&["-e", "let o={v:[1,2]}; o.v[0]=9; o.v[0]"]), "9"); + assert_eq!(jsc(&["-p", "let o={v:[1,2]}; o.v[0]=9; o.v[0]"]), "9"); // Write into an object held by an array. - assert_eq!(jsc(&["-e", "let a=[{n:1}]; a[0].n=7; a[0].n"]), "7"); + assert_eq!(jsc(&["-p", "let a=[{n:1}]; a[0].n=7; a[0].n"]), "7"); } /// S11.5: a `while` loop sums a numeric array nested inside an object. @@ -64,7 +64,7 @@ fn nested_mutation_via_eval() { fn nested_array_while_sum_via_eval() { assert_eq!( jsc(&[ - "-e", + "-p", "let o={xs:[1,2,3,4]}; let i=0; let s=0; while(i