Skip to content

Compile any crossings: destructuring, namespace patterns, any parameters and async callbacks - #1503

Merged
ctate merged 4 commits into
mainfrom
opt/bc-any-crossings
Oct 20, 2026
Merged

ctate merged 4 commits into
mainfrom
opt/bc-any-crossings

Conversation

@cramforce

Copy link
Copy Markdown
Contributor

TypeScript code that receives any from untyped dependencies (a command runner, a JavaScript retry helper, JSON.parse) hit a group of refusals. Each item below was a compile error on main; everything that compiled before still compiles to the same IR, LLVM and binaries.

Destructuring any

  • const { stdout, stderr } = run(cmd), const { body } = await fetchAny(), const [a, , b = 1, ...rest] = JSON.parse(text) and const { mode, path } = table[key] (with table typed any) now go through the checked-dynamic value. This covers renames, defaults, nesting, rest and holes.
  • This is what JavaScript sources already did, and what Compile the real jose root entry: string durations, chained builders and navigator #1407 already allowed for results of JavaScript-declared functions.
  • When the source spelling is a variable, a property chain or a call, a null or undefined source throws V8's own Cannot destructure property 'a' of 'f(...)' as it is null.
  • Primitive sources (<any>0), unit sources and computed keys keep their diagnostics, now reported by their own fences (see the updated retired-engine snapshots).

Chains over any strings

stdout.split("\n").map(...), .pop() and s.split(" ").length failed because split answers a native string[] that has no typed home under its any spelling. Only after every other lowering has declined, a method call or member read on a checker-any receiver whose value is a fresh call result now boxes that temporary and dispatches on its runtime kind. The box copies; a call result has no other holder that could observe the copy. 761-any-objects compiles because of this and moves back from the diagnostics fixtures to the corpus.

Module namespaces

const { add, label: name = "x" } = ns and = await import("./lib.ts") over a program module bind each export the way ns.add reads it: once, at the pattern's position. Rest elements keep a fence, because they would need the namespace object itself.

Defaulted any parameters in callable types

A defaulted parameter that the checker types any is now a dyn slot in the callable's ABI. This covers functions, arrows and function expressions in program source, in the typical form of a default under an (...args: any[]) => any wrapper, wrap((limit = 5) => ...), where the contextual any wins over the default's type. Inside the body the binding was already dyn. Other any parameters keep their existing paths: object literals holding such callbacks stay checked-dynamic objects, so checked-callback-options and cjs-open-options-npm compile exactly as before.

Async callbacks into JavaScript libraries

  • retry(async () => loose(), opts) failed when the library types the slot (...) => A | Promise<A> with A = any: the union collapses to any, and an async lambda with a bare dyn result was refused. That value now has the type Promise<unknown>.
  • In JavaScript files, Promise.resolve(value) over a dyn value that holds a promise now answers that same promise instead of wrapping it. On main, a TypeScript async callback handed to a JavaScript helper that does Promise.resolve(fn()).then(resolve) silently lost its payload. Corpus 6475 pins this.

Evidence

  • New corpus programs, each refused on main and matching Node byte for byte in both the plain and sanitized lanes:
    • 6470 any-destructuring
    • 6471 any-method-chains
    • 6472 namespace-destructuring
    • 6473 any-parameter-values
    • 6474 async-any-callbacks
    • 6475 js-promise-resolve-adoption
    • 761 any-objects (moved back from the diagnostics fixtures)
  • Zero-cost gate against origin/main 8df5e8d:
    • The full run reported 2522 programs identical. corpus/4642-acyclic-contract.ts fails on both sides, as it already does on main. Bench geomean -0.22% and self-hosted compile time -0.28%, both within tolerance.
    • That run also reported 1 difference, unknown-collections.ts: Promise.resolve<unknown>(3) in TypeScript had taken the new adoption path. The path is now limited to JavaScript files, and unknown-collections.ts plus all 99 TypeScript corpus programs that call Promise.resolve are identical again.
    • Two earlier runs caught 5197-object-entries-dynamic-objects, checked-callback-options and cjs-open-options-npm: object literals and callbacks with any parameters had changed representation. The any-parameter rule is now limited to defaulted parameters of functions, so all three are identical.
  • pnpm lint and pnpm format:check are clean. pnpm test:ts7 --baselines-only passes, with the new baselines recorded and 761's baseline moved.
  • tests/harness/diagnostics.test.ts: the retired-engine 2083 and 2104 snapshots shrink to their remaining fences. contract-acyclic.ts fails identically on main.
  • The self-hosted compiler's IR build reports no diagnostics, and tests/harness/self-hosting-native-driver.test.ts passes.

🤖 Generated with Claude Code

…ameters and async callbacks

- Destructuring an `any` value in TypeScript (a function typed `any`, an
  awaited `any`, `JSON.parse`, an `any` table read) now goes through the
  checked-dynamic value, as JavaScript sources and #1407's JS-declared
  results already did. Object sources get V8's RequireObjectCoercible
  TypeError for the spellings the assignment form reproduces.
- A method call or member read on a checker-`any` receiver whose value is
  a fresh typed call result (`out.split("\n").map(...)`, `.pop()`,
  `.length`) boxes that temporary and dispatches on the runtime kind,
  only after every other lowering declined.
- `const { a, b: c } = ns` and `= await import("./m")` over a program
  module bind each export the way `ns.a` reads it.
- A defaulted parameter of a program-source function, arrow or function
  expression that the checker types `any` (a default under an
  `(...args: any[]) => any` wrapper slot, where the contextual `any`
  wins) is a dyn ABI slot in the callable's type.
- An async lambda whose signature answered a bare dyn result (a library
  slot `A | Promise<A>` with A = any) produces Promise<unknown>.
- `761-any-objects` compiles and moves back to the corpus; the
  retired-engine 2083/2104 snapshots shrink to their remaining fences.
- JavaScript `Promise.resolve(value)` (in a JavaScript file) over a dyn value holding a promise
  answers that promise instead of wrapping it, so a TypeScript async
  callback's result is adopted by JavaScript retry helpers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
scriptc Ready Ready Preview, v0 Oct 20, 2026 5:56pm UTC

Comment thread packages/compiler/src/frontend/lowering/lower-stmts.ts Outdated
Comment thread packages/compiler/src/frontend/lowering/lower-stmts.ts Outdated
…any)` bindings dynamically

- `new Promise(run)` where `run` is a JavaScript function value whose
  parameters are untyped (the retry-helper shape `function run(resolve,
  reject) {...}; return new Promise(run)`) gets an adapter executor: it
  receives the native resolve/reject pair and calls the original with
  both boxed as checked-dynamic function values. This was a deferred
  runtime refusal inside such packages.
- A catch binding annotated `any` reads the caught value through its
  checked-dynamic face, as an unannotated `unknown` one does: member
  reads, expando writes, `delete` and `in` act on the thrown object, and
  a rethrow keeps its identity. It was refused (SC1063).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ctate
ctate merged commit 24c73ac into main Oct 20, 2026
23 of 24 checks passed
@ctate
ctate deleted the opt/bc-any-crossings branch October 20, 2026 17:58
euphemism pushed a commit to euphemism/scriptc that referenced this pull request Oct 20, 2026
The fixture's launch() helper calls run().then, which used to be the
first statement these programs fenced at (SC1090). It compiles since
vercel-labs#1503, so node24-web-stream-pipe-abort and
node24-web-stream-readable-lifecycle now stop at 'new ReadableStream'
and node24-web-stream-tee at 'ReadableStream.tee', which have no
lowering (SC2020).

This branch was successfully deployed

1 active deployment
Preview — e1816874 Deployed Oct 20, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants