Select the discriminated member for asserted object literals

An object literal asserted to a discriminated union keeps its own record
layout, and that layout can equal another member's layout: in
`{ kind: "return", value, loc } as Stmt`, a non-null `value` gives the
fields of a `throw` member. The value was wrapped as that member, so a
switch on `kind` read it with the `return` member's layout. The compiled
program aborted with an invalid union tag or read past the record; the
native compiler crashed this way while lowering three.js.

The literal's discriminant now selects the member through type
assertions too, as it already did through parentheses and `satisfies`.
This commit is contained in:
Chris Tate
2026-10-18 10:22:24 -05:00
parent 2e2a29426d
commit 4d417da7b8
4 changed files with 60 additions and 1 deletions
@@ -8581,9 +8581,18 @@ export class Lowerer {
// initial contextual layout was chosen. Its known discriminator still
// selects the destination arm; validate that payload before wrapping.
// Work with the already lowered value so property effects run once.
// A type assertion keeps the literal's own record layout, which can
// equal another arm's layout (`{ kind: "return", value, loc } as Stmt`
// when a `throw` arm has the same fields), so assertions are looked
// through as well.
if (expr.type.kind === "record" && expected.kind === "union") {
let literal = node;
while (ts.isParenthesizedExpression(literal) || ts.isSatisfiesExpression(literal))
while (
ts.isParenthesizedExpression(literal) ||
ts.isSatisfiesExpression(literal) ||
ts.isAsExpression(literal) ||
ts.isTypeAssertion(literal)
)
literal = literal.expression;
const def = this.unions.get(expected.unionId);
if (def?.discriminant && ts.isObjectLiteralExpression(literal)) {
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6021-asserted-literal-union-arm.ts"
],
"diags": []
}
@@ -0,0 +1,42 @@
// Object literals asserted to a discriminated union whose own fields match
// another member's fields: the discriminant selects the member.
interface Loc {
start: number;
}
type Expr = { kind: "num"; value: number; loc: Loc } | { kind: "str"; text: string; loc: Loc };
type Stmt =
| { kind: "return"; value: Expr | null; awaited?: true; loc: Loc }
| { kind: "throw"; value: Expr; loc: Loc }
| { kind: "if"; cond: Expr; then: Stmt[]; loc: Loc };
function show(stmt: Stmt): string {
switch (stmt.kind) {
case "return":
return `return${stmt.awaited ? " awaited" : ""} ${stmt.value === null ? "-" : stmt.value.kind}`;
case "throw":
return `throw ${stmt.value.kind}`;
case "if":
return `if ${stmt.cond.kind} { ${stmt.then.map(show).join("; ")} }`;
}
}
function guarded(value: Expr, loc: Loc): Stmt {
return { kind: "if", cond: value, then: [{ kind: "return", value, loc } as Stmt], loc };
}
function branches(reflect: boolean, value: Expr, loc: Loc): Stmt[] {
return [
...(reflect
? [{ kind: "return", value, loc } as Stmt]
: [{ kind: "throw", value, loc } as Stmt, { kind: "return", value: null, loc } as Stmt]),
];
}
const loc: Loc = { start: 0 };
const num: Expr = { kind: "num", value: 1, loc };
const str: Expr = { kind: "str", text: "x", loc };
console.log(show(guarded(num, loc)));
console.log(branches(true, str, loc).map(show).join(" | "));
console.log(branches(false, num, loc).map(show).join(" | "));
const listed: Stmt[] = [{ kind: "return", value: str, loc } as Stmt, { kind: "throw", value: num, loc }];
console.log(listed.map(show).join(" | "), listed.map((stmt) => stmt.kind).join(","));
+2
View File
@@ -1927,6 +1927,7 @@
"601-closures-loops.ts": 1788,
"602-closures-identity-recursion.ts": 2372,
"6020-class-extends-function-constructor.js": 4300,
"6021-asserted-literal-union-arm.ts": 3400,
"603-closures-rc-stress.ts": 1832,
"604-closures-for-of.ts": 2639,
"605-closures-forward-capture-tdz.ts": 3277,
@@ -4635,6 +4636,7 @@
"601-closures-loops.ts": 2297,
"602-closures-identity-recursion.ts": 2427,
"6020-class-extends-function-constructor.js": 7500,
"6021-asserted-literal-union-arm.ts": 7200,
"603-closures-rc-stress.ts": 2589,
"604-closures-for-of.ts": 3514,
"605-closures-forward-capture-tdz.ts": 4638,