From deb1a378a4150fe842e734cf5485b1c153e0ea8f Mon Sep 17 00:00:00 2001 From: Max B Date: Sun, 23 Aug 2026 21:50:01 +0200 Subject: [PATCH] fix(dynamic): lower TS-source object literals with a JSVAL contextual type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In --dynamic mode, object literals inside callbacks receiving JSVAL values (e.g. `import("foo").then((m) => ({ key: m.value }))`) failed with SC2001: the contextual type was JSVAL, but the literal's field values were JSVAL IR expressions (island property reads), causing a type mismatch at the field assignment check. Add `lowerJsvalObjectLiteralFromTsSource`: a helper that builds an island JSVAL object literal from a TS-source object literal expression. It mirrors lower-island's `lowerIslandObjectLiteral` strategy (alternating jsMarshal key/value pairs fed to jsOp/objLit, with jsOp/objSpread for spreads) but operates on TypeScript AST nodes rather than JavaScript source. Two call sites in `lowerObjectLiteral`: 1. Before the own-type fallback: when `mapped?.kind === "jsval"` and the source file is not a JS source file. 2. After the own-type fallback: if `mapped?.kind` is still "jsval" after trying the literal's own type. Both call sites are specific to TS-source files — JS-source literals with a JSVAL context are handled by the island gate in `lowerExpr`. --- .../src/frontend/lowering/lower-exprs.ts | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index b0f1f0973..17b92d562 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -4437,6 +4437,89 @@ function literalUnionArmOf( return recordArms.find((a) => a.shapeId === only) ?? null; } +/** Extract a literal property key as a string (identifier, string literal, + * numeric literal, or const-folded computed key). Returns null for + * runtime-computed / symbol keys. */ +function jsvalObjectLiteralKey(L: Lowerer, prop: ts.ObjectLiteralElementLike): string | null { + if ( + !ts.isPropertyAssignment(prop) && + !ts.isShorthandPropertyAssignment(prop) && + !ts.isMethodDeclaration(prop) + ) return null; + const name = prop.name; + if (ts.isIdentifier(name) || ts.isStringLiteral(name)) return name.text; + if (ts.isNumericLiteral(name)) return String(Number(name.text)); + if (ts.isComputedPropertyName(name)) return foldedStringKeyOf(L, name.expression); + return null; +} + +/** Lower a TS-source object literal whose contextual type is JSVAL. Builds a + * JSVAL island object literal using the same key/value strategy as + * lower-island's lowerIslandObjectLiteral. */ +function lowerJsvalObjectLiteralFromTsSource(L: Lowerer, expr: ts.ObjectLiteralExpression): IrExpr { + const loc = locOf(expr); + let args: IrExpr[] = []; + let acc: IrExpr | null = null; + const flushFields = (): void => { + if (args.length === 0) return; + const chunk: IrExpr = { kind: "jsOp", op: "objLit", args, type: JSVAL, loc }; + acc = acc === null + ? chunk + : { kind: "jsOp", op: "objSpread", args: [acc, chunk], type: JSVAL, loc }; + args = []; + }; + for (const prop of expr.properties) { + if (ts.isSpreadAssignment(prop)) { + flushFields(); + const spread = ts.isObjectLiteralExpression(prop.expression) + ? lowerJsvalObjectLiteralFromTsSource(L, prop.expression) + : L.jsvalIn(L.lowerExpr(prop.expression), prop.expression); + acc ??= { kind: "jsOp", op: "objLit", args: [], type: JSVAL, loc }; + acc = { kind: "jsOp", op: "objSpread", args: [acc, spread], type: JSVAL, loc: locOf(prop) }; + continue; + } + if ( + !ts.isPropertyAssignment(prop) && + !ts.isShorthandPropertyAssignment(prop) && + !ts.isMethodDeclaration(prop) + ) { + L.unsupported("SC1090", prop, "this property form in a JSVAL-typed object literal (use a spelled key with a value or method)"); + } + const propertyName = jsvalObjectLiteralKey(L, prop); + if (propertyName === null) { + L.unsupported("SC1090", prop, "this property key in a JSVAL-typed object literal (use a spelled or pure const-folded key)"); + } + const nameLoc = locOf(prop.name); + args.push({ + kind: "jsMarshal", + value: { kind: "strLit", value: propertyName, type: STRING, loc: nameLoc }, + type: JSVAL, loc: nameLoc, + }); + const valueNode: ts.Expression | ts.MethodDeclaration = + ts.isPropertyAssignment(prop) + ? prop.initializer + : ts.isShorthandPropertyAssignment(prop) + ? prop.name as ts.Identifier + : prop; + if (ts.isObjectLiteralExpression(valueNode)) { + args.push(lowerJsvalObjectLiteralFromTsSource(L, valueNode)); + } else if ( + ts.isArrayLiteralExpression(valueNode) && + !valueNode.elements.some(ts.isSpreadElement) + ) { + const elems = valueNode.elements.map((el) => L.jsvalIn(L.lowerExpr(el), el)); + args.push({ kind: "jsOp", op: "arrLit", args: elems, type: JSVAL, loc: locOf(valueNode) }); + } else { + const value = ts.isMethodDeclaration(valueNode) + ? (L.rejectThisInObjectMethod(valueNode.body ?? valueNode), L.lowerLambda(valueNode)) + : L.lowerExpr(valueNode); + args.push(L.jsvalIn(value, valueNode)); + } + } + flushFields(); + return acc ?? { kind: "jsOp", op: "objLit", args: [], type: JSVAL, loc }; +} + export function lowerObjectLiteral(L: Lowerer, expr: ts.ObjectLiteralExpression): IrExpr { const loc = locOf(expr); // The RUNTIME-KEYED literal (JS): a computed key that doesn't fold to a @@ -4607,6 +4690,14 @@ export function lowerObjectLiteral(L: Lowerer, expr: ts.ObjectLiteralExpression) // the slot's width coercion constructs through the trivial // parameter-property constructor (recordToClassPlan), or fences with // the record-shape story. + // A TS-source object literal with a JSVAL contextual type — build as an + // island JSVAL object literal so values are jsvalIn-wrapped rather than + // failing with SC2001 (the contextual type can't be statically shaped). + // Only for TS-source files: JS-source object literals with a JSVAL context + // are already handled by the island gate in lowerExpr above. + if (mapped?.kind === "jsval" && !isJsSourceFile(expr.getSourceFile())) { + return lowerJsvalObjectLiteralFromTsSource(L, expr); + } // A jsval-mapped context reaches here only when lowerExpr's island // gate DECLINED it (a project-declared typedef that absorbed to the // island through checker-`any` field residue): the literal builds at @@ -4703,6 +4794,12 @@ export function lowerObjectLiteral(L: Lowerer, expr: ts.ObjectLiteralExpression) if ((!mapped || mapped.kind === "dyn") && isJsSourceFile(expr.getSourceFile())) { return lowerDynObjectLiteral(L, expr); } + // A TS-source object literal whose remaining contextual type is JSVAL + // (reached after the own-type fallback ran): build as an island object + // so values are jsvalIn-wrapped rather than failing with SC2001. + if (mapped?.kind === "jsval") { + return lowerJsvalObjectLiteralFromTsSource(L, expr); + } if (!mapped || mapped.kind !== "record") L.badType(expr, tsType); let type = mapped; let shape = L.shapes.get(type.shapeId)!;