mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-03 07:54:40 +08:00
Lower species forms through a narrowing wrapper and a union flatMap
The species lowering for a callback method rebuilt its `%arr.<method>` call by shape — the result had to be a call taking the receiver as its first argument — so two forms the method lowering does produce fell to SC1090 whenever the program declared a class that extends Array: - a filter whose callback narrows: the filtered array comes back through the checked-narrowing wrapper, `call(%arr.narrow, [call(%arr.filter, …)])`, and the species path saw a call whose first argument is not the receiver; - a flatMap whose callback returns a union with an array arm, which lowers to `%arr.flatMap.<n>` — a helper boundedArrayHofHelper rejected because arrHofInfo, which holds the pipeline-fusable helpers, has no entry for it. The call is now found by walking the expression for the `%arr.` helper call over the receiver, whatever wraps it, and boundedArrayHofHelper accepts any `%arr.` helper (forEach excepted, it makes no array), so the union flatMap earns a bounded variant. Hidden locals are still declared in the old order, so a program that compiled before gets byte-identical IR. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f09e02255d
commit
c7c214532e
@@ -647,26 +647,27 @@ export function lowerWithArraySpecies(
|
||||
// instance is created (with that length for map, 0 otherwise), then
|
||||
// the method runs to that length and its elements move into the
|
||||
// instance.
|
||||
const bounded =
|
||||
result.kind === "call" &&
|
||||
result.args[0]?.kind === "varRef" &&
|
||||
result.args[0].localId === local.id
|
||||
? boundedArrayHofHelper(lowerer, result.callee)
|
||||
: null;
|
||||
if (bounded === null || result.kind !== "call")
|
||||
const stmts: IrStmt[] = [{ kind: "varDecl", localId: local.id, init: receiver, loc }];
|
||||
let bound: IrLocal | undefined;
|
||||
const rebound = rebuildSpeciesCall(result, local.id, (inner) => {
|
||||
const args: IrExpr[] = [receiverRef];
|
||||
for (const arg of inner.args.slice(1)) {
|
||||
const held = lowerer.declareHiddenLocal("%speciesArgument", arg.type);
|
||||
stmts.push({ kind: "varDecl", localId: held.id, init: arg, loc });
|
||||
args.push({ kind: "varRef", localId: held.id, type: arg.type, loc });
|
||||
}
|
||||
const length = lowerer.declareHiddenLocal("%speciesLength", F64);
|
||||
bound = length;
|
||||
args.push({ kind: "varRef", localId: length.id, type: F64, loc });
|
||||
const helper = boundedArrayHofHelper(lowerer, inner.callee);
|
||||
return helper === null ? null : { ...inner, callee: helper, args };
|
||||
});
|
||||
if (rebound === null || bound === undefined)
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
call,
|
||||
`'${access.name.text}' in this form when the program declares a class that extends Array`,
|
||||
);
|
||||
const stmts: IrStmt[] = [{ kind: "varDecl", localId: local.id, init: receiver, loc }];
|
||||
const args: IrExpr[] = [receiverRef];
|
||||
for (const arg of result.args.slice(1)) {
|
||||
const held = lowerer.declareHiddenLocal("%speciesArgument", arg.type);
|
||||
stmts.push({ kind: "varDecl", localId: held.id, init: arg, loc });
|
||||
args.push({ kind: "varRef", localId: held.id, type: arg.type, loc });
|
||||
}
|
||||
const bound = lowerer.declareHiddenLocal("%speciesLength", F64);
|
||||
stmts.push({
|
||||
kind: "varDecl",
|
||||
localId: bound.id,
|
||||
@@ -681,7 +682,6 @@ export function lowerWithArraySpecies(
|
||||
loc,
|
||||
});
|
||||
const boundRef: IrExpr = { kind: "varRef", localId: bound.id, type: F64, loc };
|
||||
args.push(boundRef);
|
||||
const instance = lowerer.declareHiddenLocal("%speciesInstance", receiver.type);
|
||||
const length: IrExpr = mode.resultLength
|
||||
? boundRef
|
||||
@@ -706,7 +706,7 @@ export function lowerWithArraySpecies(
|
||||
fn: "array.speciesFinish",
|
||||
args: [
|
||||
{ kind: "varRef", localId: instance.id, type: receiver.type, loc },
|
||||
{ ...result, callee: bounded, args },
|
||||
rebound,
|
||||
{ kind: "boolLit", value: mode.setLength, type: BOOL, loc },
|
||||
],
|
||||
type: result.type,
|
||||
@@ -736,6 +736,31 @@ export function lowerWithArraySpecies(
|
||||
};
|
||||
}
|
||||
|
||||
/** `e` with the `%arr.<method>` helper call over `receiverLocal` rebuilt by
|
||||
* `rebuild`, or null when `e` holds no such call. The helper call is the
|
||||
* one taking the receiver as its first argument; a filter whose callback
|
||||
* narrows converts the filtered array after the helper ran, so the call
|
||||
* can sit behind a wrapper (the checked-narrowing call), which this
|
||||
* rebuild leaves in place. */
|
||||
function rebuildSpeciesCall(
|
||||
e: IrExpr,
|
||||
receiverLocal: string,
|
||||
rebuild: (inner: IrExpr & { kind: "call" }) => IrExpr | null,
|
||||
): IrExpr | null {
|
||||
if (e.kind !== "call") return null;
|
||||
const first = e.args[0];
|
||||
if (first !== undefined && first.kind === "varRef" && first.localId === receiverLocal)
|
||||
return rebuild(e);
|
||||
for (let i = 0; i < e.args.length; i++) {
|
||||
const replaced = rebuildSpeciesCall(e.args[i]!, receiverLocal, rebuild);
|
||||
if (replaced === null) continue;
|
||||
const args = e.args.slice();
|
||||
args[i] = replaced;
|
||||
return { ...e, args };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** `C.of(...items)` and `C.from(iterable)` on a class declared `extends
|
||||
* Array` that declares neither: Array.of / Array.from's elements, in an
|
||||
* instance the class constructs (`new C(items.length)` for of, `new C()`
|
||||
|
||||
@@ -3382,10 +3382,13 @@ export function lowerTuplePredicateCall(
|
||||
* of the receiver's length when it starts. ArraySpeciesCreate reads the
|
||||
* receiver's length before the species constructor runs, so a constructor
|
||||
* that appends to the receiver does not extend the loop
|
||||
* (lowerWithArraySpecies). Null when `name` is no such helper. */
|
||||
* (lowerWithArraySpecies). Every helper that reads a length is one, the
|
||||
* flatMap whose callback returns a union included, whether or not a
|
||||
* pipeline can fuse it (arrHofInfo holds the fusable ones); forEach makes
|
||||
* no array, so it stays out. Null when `name` is no such helper. */
|
||||
export function boundedArrayHofHelper(lowerer: Lowerer, name: string): string | null {
|
||||
const info = lowerer.arrHofInfo.get(name);
|
||||
if (!info || info.method === "forEach") return null;
|
||||
if (!name.startsWith("%arr.")) return null;
|
||||
if (lowerer.arrHofInfo.get(name)?.method === "forEach") return null;
|
||||
const bounded = `${name}.bounded`;
|
||||
if (lowerer.liftedFnNamed(bounded)) return bounded;
|
||||
const fn = lowerer.liftedFnNamed(name);
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8421-array-subclass-species-forms.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// One class that extends Array sends every array method call in the program
|
||||
// through the species lowering, plain-array receivers included. Two callback
|
||||
// forms the species path used to refuse: a filter whose callback narrows the
|
||||
// element union (a type predicate, or a discriminant test tsc infers one
|
||||
// from), which wraps the filtered array in a checked-narrowing conversion,
|
||||
// and a flatMap whose callback returns a union holding an array arm.
|
||||
class Stack extends Array<number> {}
|
||||
|
||||
type Item = { kind: "a"; n: number } | { kind: "b"; s: string };
|
||||
const items: Item[] = [
|
||||
{ kind: "a", n: 1 },
|
||||
{ kind: "b", s: "x" },
|
||||
{ kind: "a", n: 2 },
|
||||
];
|
||||
const isA = (item: Item): item is { kind: "a"; n: number } => item.kind === "a";
|
||||
|
||||
const tagged = items.filter(isA);
|
||||
const inferred = items.filter((item) => item.kind === "b");
|
||||
const blocked = items.filter((item) => {
|
||||
return item.kind === "b";
|
||||
});
|
||||
const flattened = items.flatMap((item) => (item.kind === "a" ? [item.n] : []));
|
||||
const mixed = items.flatMap((item) => (item.kind === "a" ? [item.n] : undefined));
|
||||
console.log(tagged.length, tagged.map((item) => item.n).join(","));
|
||||
console.log(inferred.length, inferred.map((item) => item.s).join(","), blocked.length);
|
||||
console.log(flattened.join(","), mixed.length, mixed.join(","));
|
||||
|
||||
const stack = Stack.from([1, 2, 3]);
|
||||
const even = stack.filter((n) => n % 2 === 0);
|
||||
const doubled = stack.flatMap((n) => (n > 1 ? [n, n * 10] : []));
|
||||
console.log(even instanceof Stack, even.join(","), stack.length);
|
||||
console.log(doubled instanceof Stack, doubled.join(","), stack.length);
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 1850, "san": 1250 }
|
||||
Reference in New Issue
Block a user