Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions packages/compiler/src/frontend/lowering/lower-exprs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)!;
Expand Down