mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-08 08:44:37 +08:00
Narrow .filter predicate results to structural types, not just arms
A written type predicate usually names FEWER members than the
receiver's element has — `(c): c is { text: string } => ...` over a
block that also carries a `textSignature` — and may name a structural
type that is no arm of the element union at all. Both shapes refused,
so `commitments.filter(pred)` over a union of records with differing
members did not compile.
Record targets now convert when their members are a SUBSET of the
source's: each target member is read and converted, the copy keeps the
source's keys in the source's order, and a member the source lacks
still has no slot to read and stays refused.
When no union arm matches the predicate's type, `structuralUnionConversion`
converts tag by tag: each arm the target can be read from returns its
converted value and an arm with no such read throws the same checked
TypeError a lying predicate throws, so the checked extraction still
traps rather than silently passing a foreign value through.
Corpus 8524 pins the subset-record and structural-union conversions;
`narrow-guard-traps.test.ts` pins the TypeError a predicate that
returns true for a foreign value raises.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e198a9eec2
commit
94036b088b
@@ -161,7 +161,13 @@ function conversionBody(
|
||||
target.indexValue
|
||||
)
|
||||
return null;
|
||||
if (source.fields.length !== target.fields.length) return null;
|
||||
// A written predicate usually names FEWER members than the receiver's
|
||||
// element has (`(c): c is { text: string } => ...` over a block with a
|
||||
// `textSignature` and a `type`): the target's members are a subset of
|
||||
// the source's, each read and converted, and the copy keeps the
|
||||
// source's keys in the source's order. A member the source lacks has
|
||||
// no slot to read, so it stays refused.
|
||||
if (target.fields.length > source.fields.length) return null;
|
||||
const fields: { name: string; value: IrExpr }[] = [];
|
||||
for (const field of target.fields) {
|
||||
const own = source.fields.find((f) => f.name === field.name);
|
||||
@@ -214,7 +220,13 @@ function keepAbsence(
|
||||
}
|
||||
|
||||
/** A union value to one of its arms, or to a union of some of its arms:
|
||||
* each arm the target holds returns, any other arm throws. */
|
||||
* each arm the target holds returns, any other arm throws.
|
||||
*
|
||||
* A written predicate may name a structural type that is no arm at all
|
||||
* (`(c): c is { text: string; type: string } => ...` over the block union
|
||||
* above, whose text arm also carries `textSignature`): when no arm matches
|
||||
* exactly, each arm the target can be read from converts by its own shape,
|
||||
* tag by tag, and an arm with no such read throws. */
|
||||
function unionConversion(
|
||||
lowerer: Lowerer,
|
||||
input: IrExpr,
|
||||
@@ -250,6 +262,8 @@ function unionConversion(
|
||||
});
|
||||
}
|
||||
// Every arm the target names must be one of the union's own arms.
|
||||
if (kept === 0 && to.kind !== "union")
|
||||
return structuralUnionConversion(lowerer, input, unionId, to, typeText, loc);
|
||||
if (kept === 0 || kept !== targetArms.length) return null;
|
||||
body.push(
|
||||
typeError(
|
||||
@@ -259,3 +273,42 @@ function unionConversion(
|
||||
);
|
||||
return body;
|
||||
}
|
||||
|
||||
/** A union value to a structural type that is no arm: each arm the target
|
||||
* can be read from returns its converted value, any other arm throws. */
|
||||
function structuralUnionConversion(
|
||||
lowerer: Lowerer,
|
||||
input: IrExpr,
|
||||
unionId: string,
|
||||
to: IrType,
|
||||
typeText: string,
|
||||
loc: SrcLoc,
|
||||
): IrStmt[] | null {
|
||||
const source = lowerer.unions.get(unionId);
|
||||
if (!source) return null;
|
||||
const body: IrStmt[] = [];
|
||||
let kept = 0;
|
||||
for (let tag = 0; tag < source.arms.length; tag++) {
|
||||
const arm = source.arms[tag]!;
|
||||
if (isUnitType(arm)) continue;
|
||||
const payload: IrExpr = { kind: "unionNarrow", unionId, tag, value: input, type: arm, loc };
|
||||
const value = converted(lowerer, payload, arm, to, loc);
|
||||
if (value === null) continue;
|
||||
kept++;
|
||||
body.push({
|
||||
kind: "if",
|
||||
cond: { kind: "unionIsTag", unionId, tag, negated: false, value: input, type: BOOL, loc },
|
||||
then: [{ kind: "return", value, loc }],
|
||||
else_: null,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
if (kept === 0) return null;
|
||||
body.push(
|
||||
typeError(
|
||||
`a kept element is not a '${typeText}' (a type predicate returned true for a value of another type)`,
|
||||
loc,
|
||||
),
|
||||
);
|
||||
return body;
|
||||
}
|
||||
|
||||
@@ -70,3 +70,29 @@ for (const v of inputs) console.log(width(v));
|
||||
expect(result.stderr.toString()).toBe(UNION_TRAP);
|
||||
},
|
||||
);
|
||||
|
||||
test.skipIf(process.platform === "win32")(
|
||||
"a lying filter predicate throws the checked-extraction TypeError",
|
||||
async () => {
|
||||
const result = await run(`
|
||||
interface Cat {
|
||||
kind: "cat";
|
||||
meows: number;
|
||||
}
|
||||
interface Dog {
|
||||
kind: "dog";
|
||||
}
|
||||
type Pet = Cat | Dog;
|
||||
const pets: Pet[] = [{ kind: "cat", meows: 2 }, { kind: "dog" }];
|
||||
const alwaysCat = (p: Pet): p is Cat => p !== undefined;
|
||||
try {
|
||||
console.log("kept", pets.filter(alwaysCat).length);
|
||||
} catch (e) {
|
||||
console.log("caught", (e as Error).name);
|
||||
}
|
||||
`);
|
||||
expect(result.error).toBeUndefined();
|
||||
// Node keeps the Dog and reports 2; the checked projection throws on it.
|
||||
expect(result.stdout.toString()).toBe("caught TypeError\n");
|
||||
},
|
||||
);
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8524-filter-predicate-subset-record.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// A written type predicate usually names FEWER members than the receiver's
|
||||
// element (`(b): b is { type: "text"; text: string }` over a block that
|
||||
// also carries `textSignature`): the predicate's record is no arm of the
|
||||
// element union, so each kept element converts to it by its own arm, tag
|
||||
// by tag. An arm the predicate's record cannot be read from throws the
|
||||
// checked TypeError, because a predicate that returned true for it lied.
|
||||
type Block =
|
||||
| { type: "image"; data: string; mimeType: string }
|
||||
| { type: "text"; text: string; textSignature?: string };
|
||||
|
||||
const blocks: Block[] = [
|
||||
{ type: "text", text: "first" },
|
||||
{ type: "image", data: "AAAA", mimeType: "image/png" },
|
||||
{ type: "text", text: "second", textSignature: "sig" },
|
||||
];
|
||||
|
||||
function texts(list: Block[]): { type: "text"; text: string }[] {
|
||||
return list.filter((b): b is { type: "text"; text: string } => b.type === "text");
|
||||
}
|
||||
console.log("texts", texts(blocks).map((t) => `${t.type}:${t.text}`).join(","));
|
||||
|
||||
// The same narrowing through a guard function passed as a value.
|
||||
function isText(b: Block): b is { type: "text"; text: string } {
|
||||
return b.type === "text";
|
||||
}
|
||||
console.log("guarded", blocks.filter(isText).length);
|
||||
|
||||
// Every arm converts when the predicate names members all of them carry:
|
||||
// each kept element is read field by field into the predicate's record.
|
||||
interface A {
|
||||
tag: "a";
|
||||
n: number;
|
||||
}
|
||||
interface B {
|
||||
tag: "b";
|
||||
n: number;
|
||||
}
|
||||
function numbers(list: (A | B)[]): { tag: "a" | "b"; n: number }[] {
|
||||
return list.filter((v): v is { tag: "a" | "b"; n: number } => v.n > 0);
|
||||
}
|
||||
console.log(
|
||||
"numbers",
|
||||
numbers([
|
||||
{ tag: "a", n: 1 },
|
||||
{ tag: "b", n: 0 },
|
||||
{ tag: "a", n: 3 },
|
||||
])
|
||||
.map((v) => `${v.tag}${v.n}`)
|
||||
.join(","),
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 1300, "san": 2800 }
|
||||
Reference in New Issue
Block a user