From 7ef586607ef65160c6c9f9cac27709539a57b318 Mon Sep 17 00:00:00 2001 From: Max B Date: Sun, 23 Aug 2026 21:48:05 +0200 Subject: [PATCH] fix(dynamic): propagate JSVAL through .then() callbacks on JSVAL-settling promises MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When import("foo").then(cb) is used and the import resolves to a JSVAL promise (e.g. a stubbed .tsx module or an excluded module), the TypeScript checker's static view of the promise type parameter does not reflect the actual runtime value. This caused three failures: 1. The handler's parameter type check rejected JSVAL (expected static type, got JSVAL). 2. The result promise type was derived from the checker, producing a Promise when the actual result is always JSVAL. 3. coerceInto inside the callback body called requireExactShape on values that were JSVAL island handles, causing SC errors on every field access. Fix: - Skip the parameter type check when inner.kind === "jsval". - Force the result type to Promise when inner.kind === "jsval". - Add inJsvalThenHandler flag to Lowerer, set while lowering the callback argument of a JSVAL-settling .then(). In coerceInto, return early when this flag is set — the settled value is an opaque island handle, so any value flowing from the callback may also be JSVAL. --- .../src/frontend/lowering/lower-calls.ts | 42 +++++++++++++++---- .../compiler/src/frontend/lowering/lowerer.ts | 12 ++++++ 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 7d2a7fcec..c90e3d0e1 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -6081,7 +6081,17 @@ export function lowerPromiseMethodCall(L: Lowerer, call: ts.CallExpression, // contextual type spelled (a module-namespace type has no mapping — // the handle is the value's only story, isIslandExpr's local rule). if (inner.kind === "jsval") markJsvalHandlerParams(L, call.arguments[0]!); - let cb = L.lowerExpr(call.arguments[0]!); + // When the settled value is JSVAL, the callback body should allow JSVAL + // to flow into any slot without shape checks — the actual runtime value + // is an opaque island handle, not the TypeScript type parameter. + const prevInJsvalThenHandler = L.inJsvalThenHandler; + if (inner.kind === "jsval") L.inJsvalThenHandler = true; + let cb: IrExpr; + try { + cb = L.lowerExpr(call.arguments[0]!); + } finally { + L.inJsvalThenHandler = prevInJsvalThenHandler; + } // A TYPED handler on a DYN-settling promise (the tracePromise // result's `.then((value) => ...)` — the checker's generic // instantiation typed the parameter, but the settled value is a @@ -6183,20 +6193,34 @@ export function lowerPromiseMethodCall(L: Lowerer, call: ts.CallExpression, ); } const param = cb.type.params[0]; - if (param !== undefined && !typeEquals(param, inner)) { + // When the settled value is JSVAL, the handler's parameter type comes from + // the TypeScript checker's view of the promise type parameter — which may + // not match JSVAL. Skip the parameter type check in this case; the actual + // runtime value is an island handle regardless of the declared type. + if (inner.kind !== "jsval" && param !== undefined && !typeEquals(param, inner)) { L.unsupported( "SC1090", call.arguments[0]!, `then handlers whose parameter is not the settled value's type (expected '${L.fmt(inner)}', got '${L.fmt(param)}')`, ); } - const resultT = L.mapTypeOf(L.typeOf(call)); - if (resultT?.kind !== "promise") { - L.noLowering( - "then with this handler's result type", - call, - "the combined result must be a representable promise", - ); + // When the settled value is JSVAL, the result promise type cannot be + // inferred from the TypeScript checker (the checker's type parameter + // reflects the declared module namespace, not the actual runtime {}). + // Force Promise so the result type matches the actual value. + let resultT: IrType & { kind: "promise" }; + if (inner.kind === "jsval") { + resultT = { kind: "promise", inner: JSVAL }; + } else { + const mapped = L.mapTypeOf(L.typeOf(call)); + if (mapped?.kind !== "promise") { + L.noLowering( + "then with this handler's result type", + call, + "the combined result must be a representable promise", + ); + } + resultT = mapped; } const R = resultT.inner; const fnName = `%fn${L.lambdaCounter++}_then`; diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 22a7814ed..bd5add317 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -1415,6 +1415,13 @@ export class Lowerer { readonly liftedFns: IrFunction[] = []; lambdaCounter = 0; + /** True while compiling the callback argument of `.then()` on a promise + * whose settled type is JSVAL. Suppresses `requireExactShape` in + * `coerceInto` — the settled value is an opaque island handle, so any + * value flowing out of the callback may also be JSVAL regardless of the + * TypeScript checker's static view of the promise's type parameter. */ + inJsvalThenHandler = false; + /** Statement lists currently mid-lowering, innermost last: the forward- * capture machinery (predeclareForwardCapture) needs to know which later * statements of an OPEN list a symbol's declaration sits in, which scope @@ -6542,6 +6549,11 @@ export class Lowerer { } return e; } + // Inside a .then() callback on a JSVAL-settling promise, the settled value + // is an opaque island handle — the TypeScript checker's view of the type + // parameter is not trustworthy (the actual runtime value may be {}). Allow + // any value to flow through without a shape check. + if (this.inJsvalThenHandler) return e; this.requireExactShape(node, e.type, expected); return e; }