From b74224f31c4f165a951ecaa12e2c01ca7a654bfd Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Mon, 19 Oct 2026 23:56:38 -0700 Subject: [PATCH] Throw checked-dynamic primitives as primitives so catch typeof tests see them A value thrown from `unknown` storage (`throw values[i]`) that holds a number, string or boolean now throws as that primitive, like a statically typed `throw 5`. Before, it threw as a checked-dynamic reference, so `typeof e === "number"` (and "string"/"boolean") on the catch binding answered false and the narrowed reads were unreachable. A primitive has no identity to keep, and reading the binding as `unknown` converts it back. Co-Authored-By: Claude Opus 5.5 --- .../6480-catch-typeof-untyped-throws.ts.json | 6 +++ packages/runtime/src/scr_json.c | 22 ++++++++++ .../6480-catch-typeof-untyped-throws.ts | 40 +++++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6480-catch-typeof-untyped-throws.ts.json create mode 100644 tests/corpus/6480-catch-typeof-untyped-throws.ts diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6480-catch-typeof-untyped-throws.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6480-catch-typeof-untyped-throws.ts.json new file mode 100644 index 000000000..38b99f9fc --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6480-catch-typeof-untyped-throws.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6480-catch-typeof-untyped-throws.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 27168dad1..33793c26d 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -6359,6 +6359,28 @@ void scr_dyn_throw(ScrDyn *d) { scr_dyn_release(d); return; } + /* A checked-dynamic primitive throws as the primitive itself, as a + * statically typed `throw 5` does: catch bindings test and extract the + * same snapshot kinds (`typeof e === "number"`), and a primitive has no + * identity to keep. */ + if (d->kind == SCR_DYN_NUM) { + double value = d->v.num; + scr_dyn_release(d); + scr_throw_f64(value); + return; + } + if (d->kind == SCR_DYN_BOOL) { + bool value = d->v.b; + scr_dyn_release(d); + scr_throw_bool(value); + return; + } + if (d->kind == SCR_DYN_STR) { + ScrStr *value = scr_str_retain(d->v.str); + scr_dyn_release(d); + scr_throw_str(value); + return; + } scr_throw_ref_classified(d, scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v, scr_dyn_is_object(d)); } diff --git a/tests/corpus/6480-catch-typeof-untyped-throws.ts b/tests/corpus/6480-catch-typeof-untyped-throws.ts new file mode 100644 index 000000000..98122f851 --- /dev/null +++ b/tests/corpus/6480-catch-typeof-untyped-throws.ts @@ -0,0 +1,40 @@ +// Values thrown from `unknown` storage read back through typeof tests on +// the catch binding, like the same values thrown with static types. +const thrown: unknown[] = [5, 0, -1.5, "text", "", true, false, null, undefined, { kind: "box" }]; +function describe(value: unknown): string { + try { + throw value; + } catch (e) { + const same = e === value; + if (typeof e === "number") return `${same} number ${e + 1}`; + if (typeof e === "string") return `${same} string ${e.length}`; + if (typeof e === "boolean") return `${same} boolean ${!e}`; + if (typeof e === "object") return `${same} object ${e === null ? "null" : "value"}`; + return `${same} ${typeof e}`; + } +} +for (const value of thrown) console.log(describe(value)); +function rethrow(value: unknown): void { + try { + throw value; + } catch (e) { + if (typeof e !== "string") throw e; + console.log("kept string", e); + } +} +for (const value of ["s", 7, false]) { + try { + rethrow(value); + } catch (e) { + console.log("rethrown", typeof e, String(e)); + } +} +let count = 0; +for (const value of thrown) { + try { + throw value; + } catch (e) { + if (typeof e === "number" && e > 0) count += e; + } +} +console.log(count);