Skip to content

Answer promises from thunked and dispatched untyped async methods - #1534

Merged
ctate merged 4 commits into
mainfrom
opt/untyped-async-promises
Oct 20, 2026
Merged

ctate merged 4 commits into
mainfrom
opt/untyped-async-promises

Conversation

@cramforce

Copy link
Copy Markdown
Contributor

Follow-up to #1497, which merged before this landed. It takes the two compiler fixes the int/bc round-5 build needed to produce the build-container binary.

Summary

  • Untyped async methods answer promises. Some untyped JavaScript async methods keep their result as dyn on the signature: the result is the promise's payload, not the promise. This happens when the result's storage passes through untyped helpers. Two call paths treated that dyn as the call's own type and failed the IR validator with "call … type dyn != return promise":
    • thunkAsyncSlot's $own adapter, created when a TypeScript override of such a method keeps the inherited slot (an async-sema acquire override). The slot's ABI is now the body's promise.
    • %dyn.class.call dispatch from untyped callers, such as zod's parseAsync and safeParseAsync called through checked-dynamic receivers. The call now answers the promise.
  • Prototype data slots skip method values that have no checked-dynamic form (an async method's promise-returning signature) instead of building an invalid slot. This removed 5 internal compiler errors in the int/bc round-5 build. No standalone repro was found: in small programs, a class prototype whose async methods are reflected is fenced before the slot builder runs.

On the build-container entry these were 75 + 2 internal compiler errors in %dyn.class.call and $own adapters, plus the 5 prototype-data errors. With async-sema 3.1.1 and zod 3.25.67 installed, the same 75 internal compiler errors reproduce, and this change clears them.

Evidence

  • New corpus program 6419-untyped-async-package, Node vs native, plain and sanitized lanes. It is a package with an untyped token pool and schema: an async acquire overridden in TypeScript, safeParseAsync/parseAsync reached from the package's own untyped callers, a limiter closure, and prototype method reads. Before this change it failed to compile with both internal compiler errors.
  • Zero-cost gate against the Adapt method overrides across ABI differences, write Error codes, and close wrapper-reachable gaps #1497 tip a2a6764 (bench, self-hosted compiler, all corpus entries): all 3,344 pre-existing programs are identical. Compile time: self −0.81%; the bench geomean is inconclusive at +1.06% (95% CI −0.77% to +2.93%), with byte-identical outputs.
  • On current main: lint, format, pnpm test:ts7 --baselines-only, frontend/IR unit tests and the shard test pass. The 64xx/639x/638x corpus passes in the plain lane, and 640x–641x also in the sanitized lane.

🤖 Generated with Claude Code

An untyped JavaScript async method whose result the compiler keeps as
`dyn` (its storage passes through untyped helpers) has a `dyn` return
on its signature: the promise's payload, not the promise. Two call paths
took that as the call's own type and failed the IR check ("call … type
dyn != return promise"):

- thunkAsyncSlot's `$own` adapter, made when a TypeScript override of
  such a method keeps the inherited slot (an async-sema `acquire`
  override). The slot's ABI is now the body's promise.
- `%dyn.class.call` dispatch from untyped callers (zod's `parseAsync`
  and `safeParseAsync` through checked-dynamic receivers), which now
  answers the promise.

Prototype data slots also skip method values that have no checked-
dynamic form (an async method's promise-returning signature) instead of
building an invalid slot.

From the int/bc round-5 build of the build-container entry.

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 9:15pm UTC

Comment thread packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts
cramforce and others added 2 commits October 20, 2026 13:22
A method whose result is either a status record or a promise of one
(a parse step that hands off to a static async merge helper in async
mode) has a union result such as `Promise<A | B> | A | B`. The dynamic
conversion accepted promise arms in a union only when every other arm
was JSON-safe, so records holding dynamic, Map or Set members were
rejected even though the composite converter boxes them. Such methods
had no checked-dynamic form: no prototype data slot, and reads through
untyped code answered undefined ("this._parse is not a function" in a
validation library's object and record schemas).

The union conversion now also accepts composite-boxable arms; the
union converter already boxes each arm through its own converter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `%dyn.class.call` dispatch answers a promise for an untyped async method whose signature keeps the promise's payload. An async generator method also has the async flag, but its signature already returns the generator object, so the call now keeps that type. Before, a call from untyped code (`schema.issues(values)` over an `async *issues` method) failed IR validation with "type promise != return generator".
- thunkAsyncSlot keeps an async generator method's slot type the same way.
- Corpus 6419 adds an async generator method that the package's own untyped helper iterates with `next()`.
@cramforce

Copy link
Copy Markdown
Contributor Author

Added 60f2823: methods whose result is a union of a promise and composite records (records holding dyn, Map or Set members, such as a parse step that answers a status record or hands off to a static async merge helper) now box into the dynamic tier. Before this commit they had no checked-dynamic form, so they got no prototype data slot and reads through untyped code answered undefined. The new corpus program is 6592-promise-armed-method-values. Zero-cost gate against eef68a7: 3432 programs identical; the only difference is the new program. I merged c9c19d8 instead of rebasing.

🤖 Generated with Claude Code

@cramforce
cramforce requested a review from ctate October 20, 2026 21:49
@ctate
ctate merged commit 691ee98 into main Oct 20, 2026
66 checks passed
@ctate
ctate deleted the opt/untyped-async-promises branch October 20, 2026 23:11

This branch was successfully deployed

1 active deployment
Preview — 48ab597d 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