mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
`JSON.parse(text) as T`, `const x: T = anyValue` and `await res.json() as T` threw `TypeError: expected X at $.path, got Y` when the value did not fit `T`. Node never checks an assertion and continues with the value it has, so the build-container listener rejected an order Node accepts and a JWT with a string `exp` became a 500. A failed cast now keeps its value when it initializes a `const`/`let` binding declared in a function or block and only the code after the cast reads the binding. That code (the statements after the cast in each enclosing statement list, up to the declaration) lowers twice: the static copy, unchanged, runs while the value fits; a dynamic copy, in which the binding and everything derived from it is typed `any`, runs after a failure, so missing members read undefined, wrong-typed members keep their values, extra members stay visible in source order, and objects keep their identity. The cast sets the flag that selects the copy: the fused JSON.parse helper's slow path and the plain check call scr_dyn_check_recover, which takes back the TypeError and parks the value for checkedCast.fallbackTaken/fallbackTake. A failed value that flows into a typed slot outside the region is checked there, as any `any` value is: it continues when it fits and throws at that use when it does not. A statement of the dynamic copy with no `any` lowering becomes a runtime fence that throws only when a failed value reaches it. Module-level bindings, regions with hoisted declarations or super() calls, union targets and targets holding class instances, and --tier=static-only (and library mode) keep the throwing cast. `a?.b.c` on an `any` value now short-circuits only on `a`, so `.c` throws when `a.b` is undefined, as in Node. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>