mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-29 07:04:09 +08:00
Keep every field when an object converts to a union with an empty-record arm (#1992)
This commit is contained in:
@@ -370,6 +370,36 @@ export class LlDyn {
|
||||
B.line(`call void @scr_dyn_build_add(ptr %d, ptr @${name}, ptr ${value})`);
|
||||
}
|
||||
|
||||
/** The order a dyn value tries a union's arms in: the canonical order,
|
||||
* except that among the record arms, those with more required fields go
|
||||
* first, then those with more fields. Records are width-tolerant, so an
|
||||
* arm with fewer fields (`{}`, or one whose fields are all optional,
|
||||
* matches every object) would otherwise take an object that a wider arm
|
||||
* also matches and drop the fields only the wider arm has. */
|
||||
private unionArmOrder(def: IrUnionDef): number[] {
|
||||
const width = (i: number): [number, number] | null => {
|
||||
const arm = def.arms[i]!;
|
||||
if (arm.kind !== "record") return null;
|
||||
const shape = this.host.recordsById.get(arm.shapeId);
|
||||
if (!shape || shape.tuple) return null;
|
||||
const required = shape.fields.filter(
|
||||
(field) =>
|
||||
field.type.kind !== "union" ||
|
||||
!this.host.unionsById.get(field.type.unionId)?.arms.some((x) => x.kind === "undefinedT"),
|
||||
).length;
|
||||
return [required, shape.fields.length];
|
||||
};
|
||||
const order = def.arms.map((_, i) => i);
|
||||
const records = order.filter((i) => width(i) !== null);
|
||||
const widest = [...records].sort((x, y) => {
|
||||
const [rx, fx] = width(x)!;
|
||||
const [ry, fy] = width(y)!;
|
||||
return ry - rx || fy - fx || x - y;
|
||||
});
|
||||
records.forEach((slot, k) => (order[slot] = widest[k]!));
|
||||
return order;
|
||||
}
|
||||
|
||||
/** Branch to a variant only after its literal discriminator and complete
|
||||
* structure match. Use the same path for predicates and builders. */
|
||||
private unionArmMatch(
|
||||
@@ -2368,9 +2398,10 @@ export class LlDyn {
|
||||
throw new InternalCompilerError(
|
||||
`llvm emitter bug: dynCheck of unknown union ${t.unionId}`,
|
||||
);
|
||||
// Arms in CANONICAL order, first FULL match wins. The matched
|
||||
// arm's builder can no longer fail.
|
||||
def.arms.forEach((arm, i) => {
|
||||
// Arms in unionArmOrder, first FULL match wins. The matched arm's
|
||||
// builder can no longer fail.
|
||||
for (const i of this.unionArmOrder(def)) {
|
||||
const arm = def.arms[i]!;
|
||||
const lHit = B.newLabel("dcu.h");
|
||||
const lNext = B.newLabel("dcu.n");
|
||||
this.unionArmMatch(B, def, i, lHit, lNext, preserveRefs);
|
||||
@@ -2414,7 +2445,7 @@ export class LlDyn {
|
||||
B.terminate(`ret ptr ${u}`);
|
||||
}
|
||||
B.startBlock(lNext);
|
||||
});
|
||||
}
|
||||
if (preserveRefs) {
|
||||
B.line(
|
||||
`call void @scr_dyn_check_fail(ptr %path, ptr ${host.cstr("reference with its original layout")}, ptr %d)`,
|
||||
|
||||
@@ -3898,8 +3898,10 @@ export type IrExpr =
|
||||
* not shape equality; missing or wrong-typed fields throw, EXCEPT that a
|
||||
* missing key for an undefined-armed union field — an optional field —
|
||||
* builds the interned undefined arm instead); arrays check every element;
|
||||
* unions try arms in canonical order and the first FULL match wins (no
|
||||
* match → throw; an undefined arm matches no dyn value); JSON null matches
|
||||
* unions try arms in canonical order, except that record arms with more
|
||||
* required fields and then more fields go first, and the first FULL match
|
||||
* wins (no match → throw; an undefined arm matches no dyn value); JSON
|
||||
* null matches
|
||||
* exactly the nullT arm of a union target (bare null targets cannot
|
||||
* exist). MAY THROW:
|
||||
* backends' may-throw analyses must treat it like a `throw` statement.
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7447-union-record-arm-width.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7448-union-record-arm-width.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Values read back from arrays whose elements mix object literals of
|
||||
// different widths, an empty object among them, keep every field: the
|
||||
// record arm with more fields is tried before `{}`, which matches every
|
||||
// object.
|
||||
const pairs = [
|
||||
["a", { publicKeyEncoding: 5 }],
|
||||
["b", {}],
|
||||
["c", { publicKeyEncoding: 6, extra: true }],
|
||||
];
|
||||
for (const [label, value] of pairs) console.log(label, Object.keys(value), JSON.stringify(value));
|
||||
|
||||
const options = [
|
||||
["rsa", { modulusLength: 1024 }],
|
||||
["ec", { namedCurve: "P-256", paramEncoding: "named" }],
|
||||
["none", {}],
|
||||
];
|
||||
for (const [type, value] of options) {
|
||||
const { modulusLength, namedCurve, paramEncoding } = value;
|
||||
console.log(type, modulusLength, namedCurve, paramEncoding);
|
||||
}
|
||||
|
||||
const mixed = [{ a: 1 }, {}, "s", { a: 2, b: "two" }];
|
||||
for (const value of mixed) {
|
||||
console.log(typeof value, JSON.stringify(value), typeof value === "object" ? Object.keys(value).length : -1);
|
||||
}
|
||||
|
||||
function describe(value) {
|
||||
return Object.entries(value)
|
||||
.map(([key, field]) => `${key}=${field}`)
|
||||
.join(";");
|
||||
}
|
||||
const rows = [
|
||||
[1, { x: 1, y: 2 }],
|
||||
[2, {}],
|
||||
[3, { x: 3 }],
|
||||
];
|
||||
for (const [id, value] of rows) console.log(id, describe(value), value.x, value.y);
|
||||
@@ -0,0 +1,21 @@
|
||||
// A checked cast to a union of record types keeps every field of the
|
||||
// value: arms with more required fields, then more fields, are tried
|
||||
// before an arm whose fields are all optional, which matches every object.
|
||||
type Full = { p: number; q: string };
|
||||
type Bare = { p?: number };
|
||||
type Other = { r: boolean };
|
||||
|
||||
const values = JSON.parse('[{"p":5,"q":"x"},{},{"r":true},{"p":7}]') as (Full | Bare | Other)[];
|
||||
for (const value of values) {
|
||||
const q = "q" in value ? value.q : "-";
|
||||
const r = "r" in value ? value.r : "-";
|
||||
console.log(JSON.stringify(value), "q" in value, "r" in value, q, r);
|
||||
}
|
||||
|
||||
const one = JSON.parse('{"p":1,"q":"y"}') as Bare | Full;
|
||||
console.log(JSON.stringify(one), "q" in one ? one.q : "no q");
|
||||
|
||||
type Point = { x: number; y?: number };
|
||||
type Labeled = { x: number; label: string };
|
||||
const points = JSON.parse('[{"x":1,"label":"a"},{"x":2,"y":3},{"x":4}]') as (Point | Labeled)[];
|
||||
for (const point of points) console.log(point.x, "label" in point ? point.label : "none", "y" in point ? point.y : "none");
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 5785, "san": 5722 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3204, "san": 4052 }
|
||||
Reference in New Issue
Block a user