From 4e89d8af8b07164d964768f3a26e91c7b00c9a60 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sun, 1 Nov 2026 07:58:39 -0800 Subject: [PATCH 1/5] chore(quest): claim quest/m3/ts-abort-differential.md Quest-Claim: 9c7c5e02b69f7e78 From ead1fdf26efe88d984a6fe5038b8b0f5d8a2ca50 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sun, 1 Nov 2026 08:24:04 -0800 Subject: [PATCH 2/5] test(ts): compare wasm32 aborts in the differential A trap without a panic hook's message is an abort natively, and the runtime's `RwLock` re-lock throws `rt.Abort`, so the lang differential compares the two by kind and keeps comparing panics by message. `locks::relock` now re-locks a mutex and a read-write lock through each guard, mapped or not. Co-Authored-By: Claude Opus 5.5 --- ts/README.md | 3 +- ts/bench/main.ts | 4 +- ts/bench/time.ts | 2 +- ts/fixtures/lang/caller.ts | 9 ++- ts/fixtures/lang/expected/lib.ts | 22 ++++++ ts/fixtures/lang/expected/locks.ts | 123 ++++++++++++++++++++++++----- ts/fixtures/lang/src/locks.rs | 80 ++++++++++++++++--- ts/fixtures/wasm.rs | 16 ++-- ts/runtime/rt.ts | 5 ++ ts/runtime/rt/task.ts | 6 +- ts/tests/differential.ts | 19 +++-- ts/tests/wasm.ts | 19 +++-- 12 files changed, 249 insertions(+), 59 deletions(-) diff --git a/ts/README.md b/ts/README.md index 82559a8e1..e5c8841de 100644 --- a/ts/README.md +++ b/ts/README.md @@ -156,7 +156,8 @@ a debug build checks it, so translated code panics where native code would: it n wraps silently. A panic throws `rt.Panic`, with the message native Rust panics with, and aborts as wasm32's does: it runs no cleanup, so what the translation held is undefined after one, as a wasm trap leaves it, and a caller that catches `rt.Panic` must not use it -again. +again. An abort without a panic, as std's for an `RwLock` locked again while held, +throws `rt.Abort`, a `Panic` with std's abort message. Each integer type is its own type to `tsc`, named as Rust's: `rt.u8` is a `number` with a type-only brand, so a `u16` where a `u8` goes fails the type check, while a plain diff --git a/ts/bench/main.ts b/ts/bench/main.ts index fd72a243f..c2ccfdb59 100644 --- a/ts/bench/main.ts +++ b/ts/bench/main.ts @@ -186,7 +186,7 @@ function measureSize({ route, files }: { route: string; files: Uint8Array { const taskWasm = new Wasm(await WebAssembly.compile(taskWasmBytes)); const handoff = (input: Uint8Array) => { const output = taskWasm.call(input); - if (output === "panic") throw new Error(`native Rust panics on ${hex(input)}`); + if (!(output instanceof Uint8Array)) throw new Error(`native Rust traps on ${hex(input)}`); return output; }; const timings = [ diff --git a/ts/bench/time.ts b/ts/bench/time.ts index f4eae5394..59b9cca25 100644 --- a/ts/bench/time.ts +++ b/ts/bench/time.ts @@ -111,7 +111,7 @@ export async function measure( const { progress, sampleMs } = options; const want = workloads.map((workload) => { const output = wasm.call(workload.input); - if (output === "panic") throw new Error(`${workload.name} ${workload.op} panics in native Rust`); + if (!(output instanceof Uint8Array)) throw new Error(`${workload.name} ${workload.op} traps in native Rust`); return hex(output); }); // Each subject's timings, one per body, which every round's worker lists alike. diff --git a/ts/fixtures/lang/caller.ts b/ts/fixtures/lang/caller.ts index 73c9b4d74..a5cf3b100 100644 --- a/ts/fixtures/lang/caller.ts +++ b/ts/fixtures/lang/caller.ts @@ -421,6 +421,13 @@ const mappedLocks = [[0x04, 0x01, 0x02, 0x86, 0x05, 0x0a, 0x03, 0x02, 0x80, 0x01 Uint8Array.of(op("locks::mapped"), 0, ...bytes), ); +/** + * `locks::relock` with each pick: a mutex locked again while a guard holds it, which + * panics, and a read-write lock read or written again while a guard, mapped or not, holds + * it, which aborts but for a read under a read. + */ +const relocks = Array.from({ length: 16 }, (_, pick) => Uint8Array.of(op("locks::relock"), 0, pick)); + /** `locks::rwlocks` scripts: `get_mut` and `into_inner` among writes and reads. */ const rwlocks = [[6, 7, 0, 6, 2, 14, 15, 1]].map((bytes) => Uint8Array.of(op("locks::rwlocks"), 0, ...bytes)); @@ -784,4 +791,4 @@ const gets = [0, 0x10].flatMap((text) => { ); }); -export const cases: Uint8Array[] = [...loops, ...durations, ...ints(), ...utf8, ...floats, ...formats, ...parsed, ...singlesCases, ...singlesFormats, ...singlesParsed, ...dyns, ...cells, ...shared, ...locks, ...guards, ...maps, ...mappedLocks, ...rwlocks, ...onces, ...bounds, ...unsafeCells, ...edits, ...subslices, ...consumers, ...closures, ...elements, ...ranges, ...reducers, ...sliceEdits, ...drains, ...iters, ...hinted, ...peekedOthers, ...hints, ...reserves, ...setOps, ...zeros, ...writes, ...floatsMulAdds, ...singlesMulAdds, ...unlikeEqs, ...ownNes, ...rangeFroms, ...compares, ...unlikeCompares, ...rangeMethods, ...wraps(), ...refOrders, ...saturates(), ...methods(), ...separated, ...asciiCases, ...gets]; +export const cases: Uint8Array[] = [...loops, ...durations, ...ints(), ...utf8, ...floats, ...formats, ...parsed, ...singlesCases, ...singlesFormats, ...singlesParsed, ...dyns, ...cells, ...shared, ...locks, ...guards, ...maps, ...mappedLocks, ...relocks, ...rwlocks, ...onces, ...bounds, ...unsafeCells, ...edits, ...subslices, ...consumers, ...closures, ...elements, ...ranges, ...reducers, ...sliceEdits, ...drains, ...iters, ...hinted, ...peekedOthers, ...hints, ...reserves, ...setOps, ...zeros, ...writes, ...floatsMulAdds, ...singlesMulAdds, ...unlikeEqs, ...ownNes, ...rangeFroms, ...compares, ...unlikeCompares, ...rangeMethods, ...wraps(), ...refOrders, ...saturates(), ...methods(), ...separated, ...asciiCases, ...gets]; diff --git a/ts/fixtures/lang/expected/lib.ts b/ts/fixtures/lang/expected/lib.ts index 70fe8186f..5fb9ce06a 100644 --- a/ts/fixtures/lang/expected/lib.ts +++ b/ts/fixtures/lang/expected/lib.ts @@ -898,6 +898,14 @@ export function drop$Result_results_Logged_results_Logged(_1: rt.Result, rt.RefCell>): void { + if (_1.ok) { + rt.refRelease(_1.value); + } else { + rt.refRelease(_1.error); + } +} + export function drop$Result_sync_MappedRwLockWriteGuard_u64_sync_MappedRwLockWriteGuard_u64(_1: rt.Result, rt.RefCell>): void { if (_1.ok) { rt.refMutRelease(_1.value); @@ -922,6 +930,14 @@ export function drop$Result_sync_MutexGuard_u32_sync_TryLockError_sync_MutexGuar } } +export function drop$Result_sync_MutexGuard_u8_sync_TryLockError_sync_MutexGuard_u8(_1: rt.Result, rt.TryLockError>>): void { + if (_1.ok) { + rt.mutexUnlock(_1.value); + } else { + drop$sync_TryLockError_sync_MutexGuard_u8(_1.error); + } +} + export function drop$Result_sync_RwLockReadGuard_locks_Tally_sync_TryLockError_sync_RwLockReadGuard_locks_Tally(_1: rt.Result, rt.TryLockError>>): void { if (_1.ok) { rt.refRelease(_1.value); @@ -1018,6 +1034,12 @@ export function drop$sync_TryLockError_sync_MutexGuard_u32(_1: rt.TryLockError>): void { + if (_1.tag === "Poisoned") { + rt.mutexUnlock((_1 as { tag: "Poisoned"; _0: rt.Mutex })._0); + } +} + export function drop$sync_TryLockError_sync_RwLockReadGuard_locks_Tally(_1: rt.TryLockError>): void { if (_1.tag === "Poisoned") { rt.refRelease((_1 as { tag: "Poisoned"; _0: rt.RefCell })._0); diff --git a/ts/fixtures/lang/expected/locks.ts b/ts/fixtures/lang/expected/locks.ts index b80893d2f..f5b8b95d2 100644 --- a/ts/fixtures/lang/expected/locks.ts +++ b/ts/fixtures/lang/expected/locks.ts @@ -1,9 +1,9 @@ // Generated by rust2-ts. Do not edit. import * as rt from "./rt.ts"; -import { drop$Result_Result_sync_MappedRwLockWriteGuard_u64_sync_MappedRwLockWriteGuard_u64_sync_RwLockWriteGuard_locks_Tally, drop$Result_sync_MutexGuard_locks_Tally_sync_TryLockError_sync_MutexGuard_locks_Tally, drop$Result_sync_MutexGuard_u32_sync_TryLockError_sync_MutexGuard_u32, drop$Result_sync_RwLockReadGuard_locks_Tally_sync_TryLockError_sync_RwLockReadGuard_locks_Tally, drop$Result_sync_RwLockWriteGuard_locks_Tally_sync_TryLockError_sync_RwLockWriteGuard_locks_Tally } from "./lib.ts"; +import { drop$Result_Result_sync_MappedRwLockWriteGuard_u64_sync_MappedRwLockWriteGuard_u64_sync_RwLockWriteGuard_locks_Tally, drop$Result_sync_MappedRwLockReadGuard_u8_sync_RwLockReadGuard_locks_Tally, drop$Result_sync_MutexGuard_locks_Tally_sync_TryLockError_sync_MutexGuard_locks_Tally, drop$Result_sync_MutexGuard_u32_sync_TryLockError_sync_MutexGuard_u32, drop$Result_sync_MutexGuard_u8_sync_TryLockError_sync_MutexGuard_u8, drop$Result_sync_RwLockReadGuard_locks_Tally_sync_TryLockError_sync_RwLockReadGuard_locks_Tally, drop$Result_sync_RwLockWriteGuard_locks_Tally_sync_TryLockError_sync_RwLockWriteGuard_locks_Tally } from "./lib.ts"; import { thenSome$0sf2w1i, thenSome$0d5am30 } from "./core/bool.ts"; -import { isSomeAnd$09tpccf, unwrapOr$0r2bmpr as option_unwrapOr$0r2bmpr } from "./core/option.ts"; +import { unwrapOr$0r2bmpr as option_unwrapOr$0r2bmpr } from "./core/option.ts"; import { map$1g6jln5 as result_map$1g6jln5 } from "./core/result.ts"; import * as locks_Lock from "./locks.Lock.ts"; import * as locks_Tally from "./locks.Tally.ts"; @@ -96,22 +96,110 @@ export function locks(data: rt.ByteSlice, out: rt.ByteVec): void { shared[Symbol.dispose](); } -/** Locks a mutex while a guard of it is held, if the first byte is odd, which panics. */ +/** + * Locks a lock again while a guard of it is held, as the first byte picks, then once more + * after the guard drops: a mutex, locked or tried, which panics as wasm32's does, or a + * read-write lock read or written through a guard, mapped or not, then read if the low + * bit is clear and otherwise written, which aborts as wasm32's does unless both read. + */ export function relock(data: rt.ByteSlice, out: rt.ByteVec): void { - const mutex: rt.Mutex = new rt.Mutex(data.length & 0xff); - const guard: rt.Mutex = rt.mutexLock(mutex); - rt.byteVecPush(out, guard.value); - if (isSomeAnd$09tpccf(rt.byteSliceFirst(data), (b: rt.u8): boolean => { - return rt.u8Rem(b, 2) === 1; - })) { - const _19: rt.Mutex = rt.mutexLock(mutex); - rt.byteVecPush(out, _19.value); - rt.mutexUnlock(_19); + const _4: rt.u8 | undefined = rt.byteSliceFirst(data); + if (_4 !== undefined) { + const pick: rt.u8 = _4; + const mutex: rt.Mutex = new rt.Mutex(pick); + const lock: rt.RefCell = new rt.RefCell({ n: pick as rt.u64, seen: rt.byteVecFrom(Uint8Array.of(pick)) }); + const again: (a0: rt.ByteVec) => undefined = (out2: rt.ByteVec): undefined => { + if ((pick & 1) === 0) { + const _93: rt.RefCell = rt.rwLockRead(lock); + rt.byteVecPush(out2, rt.u64Low(_93.value.n) & 0xff); + rt.refRelease(_93); + } else { + const _162: rt.RefCell = rt.rwLockWrite(lock); + rt.byteVecPush(out2, _162.value.seen.length & 0xff); + rt.refMutRelease(_162); + } + }; + const _15: rt.u8 = rt.u8Rem(pick >> 1, 8); + if (_15 === 0) { + const guard: rt.Mutex = rt.mutexLock(mutex); + rt.byteVecPush(out, guard.value); + const _31: rt.Mutex = rt.mutexLock(mutex); + rt.byteVecPush(out, _31.value); + rt.mutexUnlock(_31); + rt.mutexUnlock(guard); + } else if (_15 === 1) { + const _37: rt.Result, rt.TryLockError>> = rt.mutexTryLock(mutex); + if (_37.ok) { + const guard2: rt.Mutex = _37.value; + rt.byteVecPush(out, guard2.value); + const _48: rt.Mutex = rt.mutexLock(mutex); + rt.byteVecPush(out, _48.value); + rt.mutexUnlock(_48); + rt.mutexUnlock(guard2); + } else { + drop$Result_sync_MutexGuard_u8_sync_TryLockError_sync_MutexGuard_u8(_37); + rt.panicFmt(["internal error: entered unreachable code: nothing holds the mutex"]); + } + } else if (_15 === 2) { + const read: rt.RefCell = rt.rwLockRead(lock); + rt.byteVecPush(out, rt.u64Low(read.value.n) & 0xff); + again(out); + rt.refRelease(read); + } else if (_15 === 3) { + const write: rt.RefCell = rt.rwLockWrite(lock); + rt.byteVecPush(out, rt.u64Low(write.value.n) & 0xff); + again(out); + rt.refMutRelease(write); + } else if (_15 === 4) { + const _74: rt.RefCell = rt.rwLockRead(lock); + const seen: rt.RefCell = rt.refMap(_74, ((t4: locks_Tally.Tally): rt.ByteVec => { + return t4.seen; + })(_74.value)); + rt.byteVecPush(out, seen.value.length & 0xff); + again(out); + rt.refRelease(seen); + } else if (_15 === 5) { + const _86: rt.RefCell = rt.rwLockWrite(lock); + const n: rt.RefCell = rt.refMutMap(_86, ((t3: locks_Tally.Tally): rt.Place => { + return rt.place(() => t3.n, (value2) => t3.n = value2); + })(_86.value)); + rt.byteVecPush(out, rt.u64Low(n.value) & 0xff); + again(out); + rt.refMutRelease(n); + } else if (_15 === 6) { + const _101: rt.RefCell = rt.rwLockRead(lock); + const _100: rt.Result, rt.RefCell> = rt.refFilterMap(_101, ((t2: locks_Tally.Tally): rt.u8 | undefined => { + return rt.byteSliceFirst(rt.byteVecSlice(t2.seen)); + })(_101.value)); + if (_100.ok) { + const first: rt.RefCell = _100.value; + const first2: rt.RefCell = rt.refMap(first, ((b: rt.u8): rt.u8 => { + return b; + })(first.value)); + rt.byteVecPush(out, first2.value); + again(out); + rt.refRelease(first2); + } else { + drop$Result_sync_MappedRwLockReadGuard_u8_sync_RwLockReadGuard_locks_Tally(_100); + rt.panicFmt(["internal error: entered unreachable code: seen holds the pick"]); + } + } else { + const _114: rt.RefCell = rt.rwLockWrite(lock); + const n2: rt.RefCell = rt.refMutMap(_114, ((t: locks_Tally.Tally): rt.Place => { + return rt.place(() => t.n, (value) => t.n = value); + })(_114.value)); + const n3: rt.RefCell = rt.refMutMap(n2, ((n4: rt.Place): rt.Place => { + return n4; + })(rt.valuePlace(n2))); + rt.byteVecPush(out, rt.u64Low(n3.value) & 0xff); + again(out); + rt.refMutRelease(n3); + } + const _130: rt.Mutex = rt.mutexLock(mutex); + rt.byteVecPush(out, _130.value); + rt.mutexUnlock(_130); + again(out); } - rt.mutexUnlock(guard); - const _27: rt.Mutex = rt.mutexLock(mutex); - rt.byteVecPush(out, _27.value); - rt.mutexUnlock(_27); } /** @@ -213,8 +301,7 @@ export function rwlocks(data: rt.ByteSlice, out: rt.ByteVec): void { /** * Each byte an operation on a read-write lock through guards mapped and filtered to its * parts, its value the operand; each puts what it read through them, what the lock holds - * after they drop, and whether it is still locked. Locking it again while one is held - * aborts on wasm32, with no panic message to compare. + * after they drop, and whether it is still locked. */ export function mapped(data: rt.ByteSlice, out: rt.ByteVec): void { const lock: rt.RefCell = new rt.RefCell(locks_Tally.default$Default()); diff --git a/ts/fixtures/lang/src/locks.rs b/ts/fixtures/lang/src/locks.rs index 7d7014575..289adb9c7 100644 --- a/ts/fixtures/lang/src/locks.rs +++ b/ts/fixtures/lang/src/locks.rs @@ -2,8 +2,9 @@ //! `Default`, read and written through a guard, recovered from a `PoisonError` that never //! comes, read and written by `get_mut` without locking, taken apart, tried while held and free, and locked again //! while held, which panics as wasm32's does; and read-write locks, read and written, -//! tried while read and while written, written through `get_mut`, taken apart, and read -//! and written through guards mapped and filtered to parts of their values. +//! tried while read and while written, written through `get_mut`, taken apart, read +//! and written through guards mapped and filtered to parts of their values, and locked +//! again while a guard holds them, which aborts as wasm32's does. use std::fmt; use std::sync::{ @@ -95,16 +96,74 @@ pub fn locks(data: &[u8], out: &mut Vec) { out.push(slot.into_inner().unwrap().unwrap_or(0xfd)); } -/// Locks a mutex while a guard of it is held, if the first byte is odd, which panics. +/// Locks a lock again while a guard of it is held, as the first byte picks, then once more +/// after the guard drops: a mutex, locked or tried, which panics as wasm32's does, or a +/// read-write lock read or written through a guard, mapped or not, then read if the low +/// bit is clear and otherwise written, which aborts as wasm32's does unless both read. pub fn relock(data: &[u8], out: &mut Vec) { - let mutex = Mutex::new(data.len() as u8); - let guard = mutex.lock().unwrap(); - out.push(*guard); - if data.first().is_some_and(|b| b % 2 == 1) { - out.push(*mutex.lock().unwrap()); + let Some(&pick) = data.first() else { + return; + }; + let mutex = Mutex::new(pick); + let lock = RwLock::new(Tally { + n: u64::from(pick), + seen: vec![pick], + }); + let again = |out: &mut Vec| match pick & 1 { + 0 => out.push(lock.read().unwrap().n as u8), + _ => out.push(lock.write().unwrap().seen.len() as u8), + }; + match (pick >> 1) % 8 { + 0 => { + let guard = mutex.lock().unwrap(); + out.push(*guard); + out.push(*mutex.lock().unwrap()); + } + 1 => { + let Ok(guard) = mutex.try_lock() else { + unreachable!("nothing holds the mutex"); + }; + out.push(*guard); + out.push(*mutex.lock().unwrap()); + } + 2 => { + let read = lock.read().unwrap(); + out.push(read.n as u8); + again(out); + } + 3 => { + let write = lock.write().unwrap(); + out.push(write.n as u8); + again(out); + } + 4 => { + let seen = RwLockReadGuard::map(lock.read().unwrap(), |t| &t.seen); + out.push(seen.len() as u8); + again(out); + } + 5 => { + let n = RwLockWriteGuard::map(lock.write().unwrap(), |t| &mut t.n); + out.push(*n as u8); + again(out); + } + // A mapped guard's own maps, which hold the lock as the guard they came from did. + 6 => { + let Ok(first) = RwLockReadGuard::filter_map(lock.read().unwrap(), |t| t.seen.first()) else { + unreachable!("seen holds the pick"); + }; + let first = MappedRwLockReadGuard::map(first, |b| b); + out.push(*first); + again(out); + } + _ => { + let n = RwLockWriteGuard::map(lock.write().unwrap(), |t| &mut t.n); + let n = MappedRwLockWriteGuard::map(n, |n| n); + out.push(*n as u8); + again(out); + } } - drop(guard); out.push(*mutex.lock().unwrap()); + again(out); } /// Each byte an operation on read-write locks, its value the operand; each puts what it @@ -174,8 +233,7 @@ pub fn rwlocks(data: &[u8], out: &mut Vec) { /// Each byte an operation on a read-write lock through guards mapped and filtered to its /// parts, its value the operand; each puts what it read through them, what the lock holds -/// after they drop, and whether it is still locked. Locking it again while one is held -/// aborts on wasm32, with no panic message to compare. +/// after they drop, and whether it is still locked. pub fn mapped(data: &[u8], out: &mut Vec) { let lock = RwLock::new(Tally::default()); let locked = |out: &mut Vec| { diff --git a/ts/fixtures/wasm.rs b/ts/fixtures/wasm.rs index a88d8f721..f3b0f9ff9 100644 --- a/ts/fixtures/wasm.rs +++ b/ts/fixtures/wasm.rs @@ -2,13 +2,14 @@ //! differential test, or the benchmark's workloads for its WASM route. //! //! The host writes a case into `input`, calls `run`, and reads `output`. A panic traps, -//! after its message is saved for `message`, and the host starts a fresh instance. +//! after its message is saved for `message`, and an abort traps without one, as std's +//! `rtabort!` runs no panic hook; either way the host starts a fresh instance. use std::sync::{Mutex, Once}; static INPUT: Mutex> = Mutex::new(Vec::new()); static OUTPUT: Mutex> = Mutex::new(Vec::new()); -static MESSAGE: Mutex = Mutex::new(String::new()); +static MESSAGE: Mutex> = Mutex::new(None); static HOOK: Once = Once::new(); /// Size the input buffer to `len` bytes and return where to write them. @@ -17,7 +18,7 @@ pub extern "C" fn input(len: usize) -> *mut u8 { // The default hook's message, without its location, which translation can't match. HOOK.call_once(|| { std::panic::set_hook(Box::new(|info| { - *MESSAGE.lock().unwrap() = info.payload_as_str().unwrap_or("Box").to_string(); + *MESSAGE.lock().unwrap() = Some(info.payload_as_str().unwrap_or("Box").to_string()); })) }); let mut input = INPUT.lock().unwrap(); @@ -41,14 +42,15 @@ pub extern "C" fn output() -> *const u8 { OUTPUT.lock().unwrap().as_ptr() } -/// Where the last panic's message is, as UTF-8; its length is `message_len`. +/// Where the panic's message is, as UTF-8, or null if no panic ran the hook, as after an +/// abort; its length is `message_len`. #[unsafe(no_mangle)] pub extern "C" fn message() -> *const u8 { - MESSAGE.lock().unwrap().as_ptr() + MESSAGE.lock().unwrap().as_ref().map_or(std::ptr::null(), |message| message.as_ptr()) } -/// The length of the last panic's message. +/// The length of the panic's message. #[unsafe(no_mangle)] pub extern "C" fn message_len() -> usize { - MESSAGE.lock().unwrap().len() + MESSAGE.lock().unwrap().as_ref().map_or(0, String::len) } diff --git a/ts/runtime/rt.ts b/ts/runtime/rt.ts index b93329416..e6de27555 100644 --- a/ts/runtime/rt.ts +++ b/ts/runtime/rt.ts @@ -30,6 +30,11 @@ export class Panic extends Error { override name = "Panic"; } +/** An abort that runs no panic hook, as std's `rtabort!`: a `Panic`, so catching one catches both. */ +export class Abort extends Panic { + override name = "Abort"; +} + /** Panic with `message`. */ export function panic(message: string): never { throw new Panic(message); diff --git a/ts/runtime/rt/task.ts b/ts/runtime/rt/task.ts index 0359945d1..27f39409a 100644 --- a/ts/runtime/rt/task.ts +++ b/ts/runtime/rt/task.ts @@ -4,7 +4,7 @@ // A value whose drop JS can observe has `[Symbol.dispose]`, which runs its `Drop` impl // and drops its fields. `None` is `undefined`, which drops as nothing. -import { type Debug, debugTupleField, debugTupleFinish, Err, type FmtResult, type Formatter, fmtDebugTuple, fmtPad, type None, OK, Ok, Panic, panic, type Place, place, ptrWrite, type Result, vecAssign } from "../rt.ts"; +import { Abort, type Debug, debugTupleField, debugTupleFinish, Err, type FmtResult, type Formatter, fmtDebugTuple, fmtPad, type None, OK, Ok, Panic, panic, type Place, place, ptrWrite, type Result, vecAssign } from "../rt.ts"; // Drops @@ -367,14 +367,14 @@ export function refCellReplace(cell: RefCell, value: T): T { /** `RwLock::read`: a shared guard, which aborts while a write is held, as wasm32's does. */ export function rwLockRead(lock: RefCell): RefCell { - if (lock.borrows < 0) panic("rwlock locked for writing"); + if (lock.borrows < 0) throw new Abort("rwlock locked for writing"); lock.borrows++; return lock; } /** `RwLock::write`: an exclusive guard, which aborts while any other is held, as wasm32's does. */ export function rwLockWrite(lock: RefCell): RefCell { - if (lock.borrows !== 0) panic("rwlock locked for reading"); + if (lock.borrows !== 0) throw new Abort("rwlock locked for reading"); lock.borrows = -1; return lock; } diff --git a/ts/tests/differential.ts b/ts/tests/differential.ts index 2b87c55df..82eca6e9d 100644 --- a/ts/tests/differential.ts +++ b/ts/tests/differential.ts @@ -9,15 +9,15 @@ // Rust leaves the result to the platform, as libm's are. // The wasm build exports `input`, `run`, and `output` around the crate's entry, which // `wasm.ts` calls. A panic traps natively and throws `Panic` in -// TypeScript, and the two agree only if their messages do. +// TypeScript, and the two agree only if their messages do. An abort traps natively +// without a panic message, as wasm32 has nowhere to print std's, and throws `Abort` in +// TypeScript, so the two agree that it aborted but not on why. import { readFile } from "node:fs/promises"; import { dirname, join } from "node:path"; import { pathToFileURL } from "node:url"; -import { Wasm } from "./wasm.ts"; - -type Outcome = Uint8Array | { panic: string }; +import { type Outcome, Wasm } from "./wasm.ts"; const [wasmPath, harnessPath, countArg] = process.argv.slice(2); if (!wasmPath || !harnessPath) throw new Error("usage: differential.ts [cases]"); @@ -33,6 +33,7 @@ function translated(input: Uint8Array): Outcome { const out = harness.run(rt.byteSliceFrom(input)); return out.data.slice(0, out.length); } catch (err) { + if (err instanceof Error && err.name === "Abort") return "abort"; if (err instanceof Error && err.name === "Panic") return { panic: err.message }; throw err; } @@ -66,6 +67,7 @@ function interesting(): number { } function show(outcome: Outcome): string { + if (outcome === "abort") return outcome; return outcome instanceof Uint8Array ? Buffer.from(outcome).toString("hex") : `panic ${JSON.stringify(outcome.panic)}`; } @@ -108,9 +110,9 @@ for (let i = 0; i < count; i++) { let failures = 0; let panics = 0; +let aborts = 0; for (const input of cases) { - const called = wasm.call(input); - const want: Outcome = called === "panic" ? { panic: wasm.panicMessage } : called; + const want = wasm.call(input); let got: Outcome; try { got = translated(input); @@ -119,7 +121,8 @@ for (const input of cases) { failures++; continue; } - if (called === "panic") panics++; + if (want === "abort") aborts++; + else if (!(want instanceof Uint8Array)) panics++; const close = want instanceof Uint8Array && got instanceof Uint8Array && harness.within?.(input, want, got) === true; if (show(want) !== show(got) && !close) { failures++; @@ -129,5 +132,5 @@ for (const input of cases) { } } -console.log(`${cases.length} cases, ${panics} panics, ${failures} differences`); +console.log(`${cases.length} cases, ${panics} panics, ${aborts} aborts, ${failures} differences`); if (failures > 0) process.exit(1); diff --git a/ts/tests/wasm.ts b/ts/tests/wasm.ts index 9897acc9b..9bd366754 100644 --- a/ts/tests/wasm.ts +++ b/ts/tests/wasm.ts @@ -11,11 +11,15 @@ interface Exports { message_len(): number; } +/** + * How a call ended: its output, a panic with the message the module saved before it + * trapped, or an abort, which trapped without running the panic hook. + */ +export type Outcome = Uint8Array | { panic: string } | "abort"; + export class Wasm { #module: WebAssembly.Module; #exports: Exports; - /** The message of the last panic, which the module saved before it trapped. */ - panicMessage = ""; constructor(module: WebAssembly.Module) { this.#module = module; @@ -38,18 +42,19 @@ export class Wasm { return new Uint8Array(this.#exports.memory.buffer, this.#exports.output(), len).slice(); } - /** All three, or "panic" if the entry trapped, with its message in `panicMessage`. */ - call(input: Uint8Array): Uint8Array | "panic" { + /** All three, or how the entry trapped. */ + call(input: Uint8Array): Outcome { try { this.input(input); return this.output(this.run()); } catch (err) { if (!(err instanceof WebAssembly.RuntimeError)) throw err; - const message = new Uint8Array(this.#exports.memory.buffer, this.#exports.message(), this.#exports.message_len()); - this.panicMessage = new TextDecoder().decode(message); + const ptr = this.#exports.message(); + const panic = () => new TextDecoder().decode(new Uint8Array(this.#exports.memory.buffer, ptr, this.#exports.message_len())); + const outcome: Outcome = ptr === 0 ? "abort" : { panic: panic() }; // A trap leaves the instance mid-call: start a fresh one. this.#exports = new WebAssembly.Instance(this.#module, {}).exports as unknown as Exports; - return "panic"; + return outcome; } } } From 38c7371fe5e79bea52ce5960c4ff109a90db698a Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sun, 1 Nov 2026 08:24:12 -0800 Subject: [PATCH 3/5] chore(quest): finish ts-abort-differential Co-Authored-By: Claude Opus 5.5 --- quest/m3/README.md | 1 - quest/m3/ts-abort-differential.md | 14 -------------- 2 files changed, 15 deletions(-) delete mode 100644 quest/m3/ts-abort-differential.md diff --git a/quest/m3/README.md b/quest/m3/README.md index 2f5429677..b5a034d16 100644 --- a/quest/m3/README.md +++ b/quest/m3/README.md @@ -33,5 +33,4 @@ generic std gap is planned here even when moq never calls it. - [Rc and Arc of a slice](/quest/m3/ts-shared-slices.md) - `Rc<[T]>` and `Arc<[T]>` from a `Vec`, a slice, or an iterator translate - [The rest of a boxed slice's impls](/quest/m3/ts-boxed-slice-rest.md) - `clone_from`, `AsRef`, `AsMut`, `TryFrom` into a boxed array, and `Box` into bytes translate for boxed slices - [The rest of a guard's methods](/quest/m3/ts-guard-rest.md) - `Debug` of `RefCell` and `RwLock` guards, `MutexGuard::map`, and the guards' `try_map` translate -- [Aborts in the differential](/quest/m3/ts-abort-differential.md) - the lang differential compares wasm32's aborts, so an `RwLock` locked again while held is tested - [A reversed zip's per-pair cost](/quest/m3/ts-zip-rev-overhead.md) - a reversed by-index zip costs per pair what a forward one does, or the gap is explained diff --git a/quest/m3/ts-abort-differential.md b/quest/m3/ts-abort-differential.md deleted file mode 100644 index 6a8c7c0a2..000000000 --- a/quest/m3/ts-abort-differential.md +++ /dev/null @@ -1,14 +0,0 @@ -# [S] Aborts in the differential - -## Goal - -The lang differential compares a wasm32 abort with the translation's, so the runtime's -abort of an `RwLock` locked again while held, `rwlock locked for reading` or `for writing`, -is tested as `RefCell`'s panics are. Today wasm32's `rtabort!` traps with no panic -message, which no translation's `rt.Panic` matches, so the fixtures avoid it. - -## Plan - -Tell an abort from a panic on both sides: natively by the trap without a panic hook's -message, and in the runtime by a distinct error or message prefix such as std's -`fatal runtime error: ..., aborting`. Keep the comparison exact for panics. From 5c4cf2172096eae8add3b754775634f2d970a7b3 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sun, 1 Nov 2026 08:27:46 -0800 Subject: [PATCH 4/5] style(ts): format the wasm harness Co-Authored-By: Claude Opus 5.5 --- ts/fixtures/wasm.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ts/fixtures/wasm.rs b/ts/fixtures/wasm.rs index f3b0f9ff9..33b631087 100644 --- a/ts/fixtures/wasm.rs +++ b/ts/fixtures/wasm.rs @@ -46,7 +46,8 @@ pub extern "C" fn output() -> *const u8 { /// abort; its length is `message_len`. #[unsafe(no_mangle)] pub extern "C" fn message() -> *const u8 { - MESSAGE.lock().unwrap().as_ref().map_or(std::ptr::null(), |message| message.as_ptr()) + let message = MESSAGE.lock().unwrap(); + message.as_ref().map_or(std::ptr::null(), |message| message.as_ptr()) } /// The length of the panic's message. From 70d22d6541d427107c8d127569562bb1b8d01fa3 Mon Sep 17 00:00:00 2001 From: Luke Curley <432854+kixelated@users.noreply.github.com> Date: Sun, 1 Nov 2026 16:28:37 -0800 Subject: [PATCH 5/5] test(ts): regenerate the lang snapshots after the merge Co-Authored-By: Claude Opus 5.5 --- ts/fixtures/lang/expected/lib.ts | 22 +++++ ts/fixtures/lang/expected/locks.ts | 126 ++++++++++++++++++++++++----- 2 files changed, 128 insertions(+), 20 deletions(-) diff --git a/ts/fixtures/lang/expected/lib.ts b/ts/fixtures/lang/expected/lib.ts index 988119ea2..1e2b769bf 100644 --- a/ts/fixtures/lang/expected/lib.ts +++ b/ts/fixtures/lang/expected/lib.ts @@ -958,6 +958,14 @@ export function drop$Result_sync_MappedMutexGuard_u64_sync_MappedMutexGuard_u64( } } +export function drop$Result_sync_MappedRwLockReadGuard_u8_sync_RwLockReadGuard_locks_Tally(_1: rt.Result, rt.RefCell>): void { + if (_1.ok) { + rt.refRelease(_1.value); + } else { + rt.refRelease(_1.error); + } +} + export function drop$Result_sync_MappedRwLockWriteGuard_u64_sync_MappedRwLockWriteGuard_u64(_1: rt.Result, rt.RefCell>): void { if (_1.ok) { rt.refMutRelease(_1.value); @@ -990,6 +998,14 @@ export function drop$Result_sync_MutexGuard_u32_sync_TryLockError_sync_MutexGuar } } +export function drop$Result_sync_MutexGuard_u8_sync_TryLockError_sync_MutexGuard_u8(_1: rt.Result, rt.TryLockError>>): void { + if (_1.ok) { + rt.mutexUnlock(_1.value); + } else { + drop$sync_TryLockError_sync_MutexGuard_u8(_1.error); + } +} + export function drop$Result_sync_RwLockReadGuard_array4_u8_sync_TryLockError_sync_RwLockReadGuard_array4_u8(_1: rt.Result, rt.TryLockError>>): void { if (_1.ok) { rt.refRelease(_1.value); @@ -1124,6 +1140,12 @@ export function drop$sync_TryLockError_sync_MutexGuard_u32(_1: rt.TryLockError>): void { + if (_1.tag === "Poisoned") { + rt.mutexUnlock((_1 as { tag: "Poisoned"; _0: rt.Mutex })._0); + } +} + export function drop$sync_TryLockError_sync_RwLockReadGuard_array4_u8(_1: rt.TryLockError>): void { if (_1.tag === "Poisoned") { rt.refRelease((_1 as { tag: "Poisoned"; _0: rt.RefCell })._0); diff --git a/ts/fixtures/lang/expected/locks.ts b/ts/fixtures/lang/expected/locks.ts index d893a4317..d57d6e49a 100644 --- a/ts/fixtures/lang/expected/locks.ts +++ b/ts/fixtures/lang/expected/locks.ts @@ -1,9 +1,9 @@ // Generated by rust2-ts. Do not edit. import * as rt from "./rt.ts"; -import { drop$Result_Result_sync_MappedMutexGuard_u64_sync_MappedMutexGuard_u64_sync_MutexGuard_locks_Tally, drop$Result_Result_sync_MappedRwLockWriteGuard_u64_sync_MappedRwLockWriteGuard_u64_sync_RwLockWriteGuard_locks_Tally, drop$Result_sync_MutexGuard_array4_u8_sync_TryLockError_sync_MutexGuard_array4_u8, drop$Result_sync_MutexGuard_locks_Tally_sync_TryLockError_sync_MutexGuard_locks_Tally, drop$Result_sync_MutexGuard_u32_sync_TryLockError_sync_MutexGuard_u32, drop$Result_sync_RwLockReadGuard_array4_u8_sync_TryLockError_sync_RwLockReadGuard_array4_u8, drop$Result_sync_RwLockReadGuard_locks_Tally_sync_TryLockError_sync_RwLockReadGuard_locks_Tally, drop$Result_sync_RwLockReadGuard_u32_sync_TryLockError_sync_RwLockReadGuard_u32, drop$Result_sync_RwLockWriteGuard_array4_u8_sync_TryLockError_sync_RwLockWriteGuard_array4_u8, drop$Result_sync_RwLockWriteGuard_locks_Tally_sync_TryLockError_sync_RwLockWriteGuard_locks_Tally, drop$Result_sync_RwLockWriteGuard_u32_sync_TryLockError_sync_RwLockWriteGuard_u32 } from "./lib.ts"; +import { drop$Result_Result_sync_MappedMutexGuard_u64_sync_MappedMutexGuard_u64_sync_MutexGuard_locks_Tally, drop$Result_Result_sync_MappedRwLockWriteGuard_u64_sync_MappedRwLockWriteGuard_u64_sync_RwLockWriteGuard_locks_Tally, drop$Result_sync_MappedRwLockReadGuard_u8_sync_RwLockReadGuard_locks_Tally, drop$Result_sync_MutexGuard_array4_u8_sync_TryLockError_sync_MutexGuard_array4_u8, drop$Result_sync_MutexGuard_locks_Tally_sync_TryLockError_sync_MutexGuard_locks_Tally, drop$Result_sync_MutexGuard_u32_sync_TryLockError_sync_MutexGuard_u32, drop$Result_sync_MutexGuard_u8_sync_TryLockError_sync_MutexGuard_u8, drop$Result_sync_RwLockReadGuard_array4_u8_sync_TryLockError_sync_RwLockReadGuard_array4_u8, drop$Result_sync_RwLockReadGuard_locks_Tally_sync_TryLockError_sync_RwLockReadGuard_locks_Tally, drop$Result_sync_RwLockReadGuard_u32_sync_TryLockError_sync_RwLockReadGuard_u32, drop$Result_sync_RwLockWriteGuard_array4_u8_sync_TryLockError_sync_RwLockWriteGuard_array4_u8, drop$Result_sync_RwLockWriteGuard_locks_Tally_sync_TryLockError_sync_RwLockWriteGuard_locks_Tally, drop$Result_sync_RwLockWriteGuard_u32_sync_TryLockError_sync_RwLockWriteGuard_u32 } from "./lib.ts"; import { thenSome$0sf2w1i, thenSome$0d5am30 } from "./core/bool.ts"; -import { isSomeAnd$09tpccf, unwrapOr$1iemzn8 as option_unwrapOr$1iemzn8, unwrapOr$0r2bmpr as option_unwrapOr$0r2bmpr } from "./core/option.ts"; +import { unwrapOr$1iemzn8 as option_unwrapOr$1iemzn8, unwrapOr$0r2bmpr as option_unwrapOr$0r2bmpr } from "./core/option.ts"; import { map$02txiwz as result_map$02txiwz, map$1g6jln5 as result_map$1g6jln5, mapOr$0666x4n as result_mapOr$0666x4n, mapOr$1qkf3ab as result_mapOr$1qkf3ab } from "./core/result.ts"; import * as locks_Lock from "./locks.Lock.ts"; import * as locks_Tally from "./locks.Tally.ts"; @@ -96,22 +96,110 @@ export function locks(data: rt.ByteSlice, out: rt.ByteVec): void { shared[Symbol.dispose](); } -/** Locks a mutex while a guard of it is held, if the first byte is odd, which panics. */ +/** + * Locks a lock again while a guard of it is held, as the first byte picks, then once more + * after the guard drops: a mutex, locked or tried, which panics as wasm32's does, or a + * read-write lock read or written through a guard, mapped or not, then read if the low + * bit is clear and otherwise written, which aborts as wasm32's does unless both read. + */ export function relock(data: rt.ByteSlice, out: rt.ByteVec): void { - const mutex: rt.Mutex = new rt.Mutex(data.length & 0xff); - const guard: rt.Mutex = rt.mutexLock(mutex); - rt.byteVecPush(out, guard.value); - if (isSomeAnd$09tpccf(rt.byteSliceFirst(data), (b: rt.u8): boolean => { - return rt.u8Rem(b, 2) === 1; - })) { - const _19: rt.Mutex = rt.mutexLock(mutex); - rt.byteVecPush(out, _19.value); - rt.mutexUnlock(_19); + const _4: rt.u8 | undefined = rt.byteSliceFirst(data); + if (_4 !== undefined) { + const pick: rt.u8 = _4; + const mutex: rt.Mutex = new rt.Mutex(pick); + const lock: rt.RefCell = new rt.RefCell({ n: pick as rt.u64, seen: rt.byteVecFrom(Uint8Array.of(pick)) }); + const again: (a0: rt.ByteVec) => undefined = (out2: rt.ByteVec): undefined => { + if ((pick & 1) === 0) { + const _93: rt.RefCell = rt.rwLockRead(lock); + rt.byteVecPush(out2, rt.u64Low(_93.value.n) & 0xff); + rt.refRelease(_93); + } else { + const _162: rt.RefCell = rt.rwLockWrite(lock); + rt.byteVecPush(out2, _162.value.seen.length & 0xff); + rt.refMutRelease(_162); + } + }; + const _15: rt.u8 = rt.u8Rem(pick >> 1, 8); + if (_15 === 0) { + const guard: rt.Mutex = rt.mutexLock(mutex); + rt.byteVecPush(out, guard.value); + const _31: rt.Mutex = rt.mutexLock(mutex); + rt.byteVecPush(out, _31.value); + rt.mutexUnlock(_31); + rt.mutexUnlock(guard); + } else if (_15 === 1) { + const _37: rt.Result, rt.TryLockError>> = rt.mutexTryLock(mutex); + if (_37.ok) { + const guard2: rt.Mutex = _37.value; + rt.byteVecPush(out, guard2.value); + const _48: rt.Mutex = rt.mutexLock(mutex); + rt.byteVecPush(out, _48.value); + rt.mutexUnlock(_48); + rt.mutexUnlock(guard2); + } else { + drop$Result_sync_MutexGuard_u8_sync_TryLockError_sync_MutexGuard_u8(_37); + rt.panicFmt(["internal error: entered unreachable code: nothing holds the mutex"]); + } + } else if (_15 === 2) { + const read: rt.RefCell = rt.rwLockRead(lock); + rt.byteVecPush(out, rt.u64Low(read.value.n) & 0xff); + again(out); + rt.refRelease(read); + } else if (_15 === 3) { + const write: rt.RefCell = rt.rwLockWrite(lock); + rt.byteVecPush(out, rt.u64Low(write.value.n) & 0xff); + again(out); + rt.refMutRelease(write); + } else if (_15 === 4) { + const _74: rt.RefCell = rt.rwLockRead(lock); + const seen: rt.RefCell = rt.refMap(_74, ((t4: locks_Tally.Tally): rt.ByteVec => { + return t4.seen; + })(_74.value)); + rt.byteVecPush(out, seen.value.length & 0xff); + again(out); + rt.refRelease(seen); + } else if (_15 === 5) { + const _86: rt.RefCell = rt.rwLockWrite(lock); + const n: rt.RefCell = rt.refMutMap(_86, ((t3: locks_Tally.Tally): rt.Place => { + return rt.place(() => t3.n, (value2) => t3.n = value2); + })(_86.value)); + rt.byteVecPush(out, rt.u64Low(n.value) & 0xff); + again(out); + rt.refMutRelease(n); + } else if (_15 === 6) { + const _101: rt.RefCell = rt.rwLockRead(lock); + const _100: rt.Result, rt.RefCell> = rt.refFilterMap(_101, ((t2: locks_Tally.Tally): rt.u8 | undefined => { + return rt.byteSliceFirst(rt.byteVecSlice(t2.seen)); + })(_101.value)); + if (_100.ok) { + const first: rt.RefCell = _100.value; + const first2: rt.RefCell = rt.refMap(first, ((b: rt.u8): rt.u8 => { + return b; + })(first.value)); + rt.byteVecPush(out, first2.value); + again(out); + rt.refRelease(first2); + } else { + drop$Result_sync_MappedRwLockReadGuard_u8_sync_RwLockReadGuard_locks_Tally(_100); + rt.panicFmt(["internal error: entered unreachable code: seen holds the pick"]); + } + } else { + const _114: rt.RefCell = rt.rwLockWrite(lock); + const n2: rt.RefCell = rt.refMutMap(_114, ((t: locks_Tally.Tally): rt.Place => { + return rt.place(() => t.n, (value) => t.n = value); + })(_114.value)); + const n3: rt.RefCell = rt.refMutMap(n2, ((n4: rt.Place): rt.Place => { + return n4; + })(rt.valuePlace(n2))); + rt.byteVecPush(out, rt.u64Low(n3.value) & 0xff); + again(out); + rt.refMutRelease(n3); + } + const _130: rt.Mutex = rt.mutexLock(mutex); + rt.byteVecPush(out, _130.value); + rt.mutexUnlock(_130); + again(out); } - rt.mutexUnlock(guard); - const _27: rt.Mutex = rt.mutexLock(mutex); - rt.byteVecPush(out, _27.value); - rt.mutexUnlock(_27); } /** @@ -213,8 +301,7 @@ export function rwlocks(data: rt.ByteSlice, out: rt.ByteVec): void { /** * Each byte an operation on a read-write lock through guards mapped and filtered to its * parts, its value the operand; each puts what it read through them, what the lock holds - * after they drop, and whether it is still locked. Locking it again while one is held - * aborts on wasm32, with no panic message to compare. + * after they drop, and whether it is still locked. */ export function mapped(data: rt.ByteSlice, out: rt.ByteVec): void { const lock: rt.RefCell = new rt.RefCell(locks_Tally.default$Default()); @@ -364,8 +451,7 @@ export function mapped(data: rt.ByteSlice, out: rt.ByteVec): void { /** * Each byte an operation on mutexes through guards mapped and filtered to parts of their * values, its value the operand; each puts what it read through them, what the mutex - * holds after they drop, and whether it is still locked. Locking it again while one is - * held aborts on wasm32, with no panic message to compare. + * holds after they drop, and whether it is still locked. */ export function mutexMapped(data: rt.ByteSlice, out: rt.ByteVec): void { const mutex: rt.Mutex = new rt.Mutex(locks_Tally.default$Default());