mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-05 08:13:06 +08:00
Skip the element read for array callbacks that never use it
A callback that declares no element parameter, or binds it to a name that appears nowhere else in the function (`filter(() => true)`, `map((_, i) => i)`, `reduce((s, v) => s + 1, 0)`), no longer gets the element read and converted for each visit. The helpers and the fused loops pass a constant of the parameter's type in its place (NaN, "", false, or the undefined arm of a union that has one); other parameter types keep the read. An array holding an element its type cannot hold, written through an unknown[] view, threw a TypeError at that element for every callback method. With such callbacks it now behaves as in Node: - map, flatMap, forEach, some, every, findIndex, findLastIndex, reduce and reduceRight visit it without reading it; - filter appends each kept element as it is stored, so the written value stays in the result as the view's push would store it; - find and findLast read the element only once the predicate holds. Callbacks that read the element keep the TypeError for a value their parameter type cannot hold.
This commit is contained in:
@@ -3,6 +3,7 @@ import { InternalCompilerError } from "../../errors.js";
|
||||
import { dynUndefinedExpr, helperLoc, varRef } from "../../ir/build.js";
|
||||
import {
|
||||
BOOL,
|
||||
DYN,
|
||||
F64,
|
||||
type IrExpr,
|
||||
type IrStmt,
|
||||
@@ -446,6 +447,94 @@ export function appendElement(
|
||||
];
|
||||
}
|
||||
|
||||
/** The argument a helper passes in place of an element its callback never
|
||||
* reads (lowerArrayCallback's `unread`): a constant of the parameter's
|
||||
* type, so the slot stays unread and an element the type cannot hold
|
||||
* (stored through a dynamic view) passes. Null for a type without one. */
|
||||
export function unreadArgument(lowerer: Lowerer, type: IrType, loc: SrcLoc): IrExpr | null {
|
||||
if (type.kind === "f64") return { kind: "numLit", value: NaN, type: F64, loc };
|
||||
if (type.kind === "bool") return { kind: "boolLit", value: false, type: BOOL, loc };
|
||||
if (type.kind === "string") return { kind: "strLit", value: "", type: STRING, loc };
|
||||
if (type.kind === "union" && lowerer.armTag(type.unionId, UNDEFINED_T) >= 0)
|
||||
return lowerer.wrappedUndefined(type, loc) ?? null;
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Append the element at `arr[index]` to `out`, an array of the same
|
||||
* element type, as it is stored: a value, a present undefined, or an
|
||||
* element outside the element type (stored through a dynamic view), which
|
||||
* `out` keeps as one, as the view's push stores it. `out` is a binding,
|
||||
* and `value` names a local of the read's type (arrayValueType) that holds
|
||||
* a stored value on its way. */
|
||||
export function arraySlotAppend(
|
||||
lowerer: Lowerer,
|
||||
out: IrExpr,
|
||||
arr: IrExpr,
|
||||
index: IrExpr,
|
||||
elem: IrType,
|
||||
value: string,
|
||||
loc: SrcLoc,
|
||||
): IrStmt {
|
||||
const valueT = arrayValueType(lowerer, elem);
|
||||
const end: IrExpr = {
|
||||
kind: "arrIntrinsic",
|
||||
method: "length",
|
||||
receiver: out,
|
||||
args: [],
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
const plain: IrStmt = {
|
||||
kind: "block",
|
||||
body: [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: value,
|
||||
init: arrayValueRead(lowerer, arr, index, elem, loc),
|
||||
loc,
|
||||
},
|
||||
arrayValueStore(
|
||||
lowerer,
|
||||
out,
|
||||
end,
|
||||
{ kind: "varRef", localId: value, type: valueT, loc },
|
||||
elem,
|
||||
loc,
|
||||
),
|
||||
],
|
||||
loc,
|
||||
};
|
||||
if (!elementMayBeForeign(lowerer, elem)) return plain;
|
||||
return {
|
||||
kind: "if",
|
||||
cond: {
|
||||
kind: "bin",
|
||||
op: "===",
|
||||
left: { kind: "arrayState", arr, index, type: F64, loc },
|
||||
right: { kind: "numLit", value: 3, type: F64, loc },
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: [
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "dynInvoke",
|
||||
recv: { kind: "dynFrom", value: out, liveRef: true, type: DYN, loc },
|
||||
method: "push",
|
||||
calleeName: "push",
|
||||
args: [{ kind: "libCall", fn: "array.foreign", args: [arr, index], type: DYN, loc }],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
else_: [plain],
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** Store a stabilized value without changing the array's element ABI. */
|
||||
export function arrayValueStore(
|
||||
lowerer: Lowerer,
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
} from "../lower-array-sort.js";
|
||||
import {
|
||||
arrayIndexPresent,
|
||||
arraySlotAppend,
|
||||
arrayValueRead,
|
||||
arrayValueStore,
|
||||
arrayValueType,
|
||||
@@ -45,6 +46,7 @@ import {
|
||||
elementMayBeForeign,
|
||||
foreignElementThrow,
|
||||
primitiveElement,
|
||||
unreadArgument,
|
||||
} from "../array-values.js";
|
||||
import { typeKey } from "../../type-mapper.js";
|
||||
import {
|
||||
@@ -101,9 +103,9 @@ export function lowerArrayHofCall(
|
||||
}
|
||||
}
|
||||
const callback = booleanFilter
|
||||
? { fnArg: truthyFilterCallback(lowerer, valueT, loc), arity: 1 }
|
||||
? { fnArg: truthyFilterCallback(lowerer, valueT, loc), arity: 1, unread: false }
|
||||
: lowerArrayCallback(lowerer, argNode, [valueT], arrayOf(elem));
|
||||
const { fnArg, arity } = callback;
|
||||
const { fnArg, arity, unread } = callback;
|
||||
const fnRet = fnArg.type.ret;
|
||||
if (method === "map" && (fnRet.kind === "void" || fnRet.kind === "func")) {
|
||||
// The result array U[] is unrepresentable here (no void elements;
|
||||
@@ -133,8 +135,9 @@ export function lowerArrayHofCall(
|
||||
: null;
|
||||
const outElem = filtered !== null ? filtered.elem : callbackArrayElement(lowerer, call, fnRet);
|
||||
// A filter that re-tags its elements keeps the single union-typed visit.
|
||||
// A callback that never reads its element has no use for a typed one.
|
||||
const fast =
|
||||
present || booleanFilter || (method === "filter" && !typeEquals(outElem, elem))
|
||||
present || booleanFilter || unread || (method === "filter" && !typeEquals(outElem, elem))
|
||||
? null
|
||||
: lowerFastCallback(lowerer, argNode, [valueT], 0, elem, arrayOf(elem), callback);
|
||||
const helper = arrayHofHelper(
|
||||
@@ -148,6 +151,7 @@ export function lowerArrayHofCall(
|
||||
callbackSite(fast ?? fnArg),
|
||||
fast?.type ?? null,
|
||||
present,
|
||||
unread,
|
||||
);
|
||||
const resultType: IrType = method === "map" || method === "filter" ? arrayOf(outElem) : VOID;
|
||||
const result: IrExpr = {
|
||||
@@ -541,7 +545,12 @@ function callbackValueRead(
|
||||
elem: IrType,
|
||||
present: boolean,
|
||||
loc: SrcLoc,
|
||||
unread = false,
|
||||
): IrExpr {
|
||||
if (unread) {
|
||||
const placeholder = unreadArgument(lowerer, callbackValueType(lowerer, elem, present), loc);
|
||||
if (placeholder) return placeholder;
|
||||
}
|
||||
return present
|
||||
? { kind: "arrayGet", arr, index, type: elem, loc }
|
||||
: arrayValueRead(lowerer, arr, index, elem, loc);
|
||||
@@ -562,13 +571,23 @@ function arrayHofHelper(
|
||||
site = "",
|
||||
fastT: CallbackType | null = null,
|
||||
present = false,
|
||||
unread = false,
|
||||
): string {
|
||||
const key = `${method}:${typeKey(elem)}:${typeKey(fnRet)}:${arity}:${typeKey(outElem)}${site}${fastT ? ":split" : ""}${present ? ":present" : ""}`;
|
||||
const key = `${method}:${typeKey(elem)}:${typeKey(fnRet)}:${arity}:${typeKey(outElem)}${site}${fastT ? ":split" : ""}${present ? ":present" : ""}${unread ? ":unread" : ""}`;
|
||||
const existing = lowerer.arrHofHelpers.get(key);
|
||||
if (existing) return existing;
|
||||
const name = keyedHelperName(lowerer, `%arr.${method}`, key);
|
||||
lowerer.arrHofHelpers.set(key, name);
|
||||
lowerer.arrHofInfo.set(name, { method, elem, fnRet, arity, outElem, present, split: !!fastT });
|
||||
lowerer.arrHofInfo.set(name, {
|
||||
method,
|
||||
elem,
|
||||
fnRet,
|
||||
arity,
|
||||
outElem,
|
||||
present,
|
||||
split: !!fastT,
|
||||
unread,
|
||||
});
|
||||
const fn = buildArrayHofFn(
|
||||
lowerer,
|
||||
name,
|
||||
@@ -580,6 +599,7 @@ function arrayHofHelper(
|
||||
outElem,
|
||||
fastT,
|
||||
present,
|
||||
unread,
|
||||
);
|
||||
lowerer.liftedFns.push(fastT ? withFastCallback(fn, fastT, elem) : fn);
|
||||
return name;
|
||||
@@ -794,6 +814,7 @@ function buildArrayHofFn(
|
||||
outElem: IrType,
|
||||
fastT: CallbackType | null = null,
|
||||
present = false,
|
||||
unread = false,
|
||||
): IrFunction {
|
||||
const arrT = arrayOf(elem);
|
||||
const valueT = callbackValueType(lowerer, elem, present);
|
||||
@@ -809,6 +830,7 @@ function buildArrayHofFn(
|
||||
elem,
|
||||
present,
|
||||
loc,
|
||||
unread,
|
||||
);
|
||||
const hasElem: IrExpr = {
|
||||
kind: "arrayHas",
|
||||
@@ -994,6 +1016,7 @@ function buildArrayHofFn(
|
||||
);
|
||||
returnType = outT;
|
||||
const retained = filterRetainedValue(lowerer, varRef("v.0", valueT, loc), elem, outElem, loc);
|
||||
const copied = (arity === 0 || unread) && !present && typeEquals(outElem, elem);
|
||||
body = [
|
||||
{
|
||||
kind: "varDecl",
|
||||
@@ -1006,34 +1029,57 @@ function buildArrayHofFn(
|
||||
{
|
||||
kind: "if",
|
||||
cond: hasElem,
|
||||
then: [
|
||||
{ kind: "varDecl", localId: "v.0", init: getElem, loc },
|
||||
{
|
||||
kind: "if",
|
||||
// ToBoolean over the predicate's answer — inert when it already
|
||||
// returned bool, the per-union helper when it returned a union.
|
||||
cond: filterCond(callF(varRef("v.0", valueT, loc)), fnRet, loc),
|
||||
then: [
|
||||
arrayValueStore(
|
||||
lowerer,
|
||||
varRef("out.0", outT, loc),
|
||||
{
|
||||
kind: "arrIntrinsic",
|
||||
method: "length",
|
||||
receiver: varRef("out.0", outT, loc),
|
||||
args: [],
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
retained,
|
||||
outElem,
|
||||
// A predicate that never reads the element leaves the slot unread:
|
||||
// a kept element is appended as it is stored (arraySlotAppend), so
|
||||
// one the element type cannot hold stays in the result.
|
||||
then: copied
|
||||
? [
|
||||
{
|
||||
kind: "if",
|
||||
cond: filterCond(callF(getElem), fnRet, loc),
|
||||
then: [
|
||||
arraySlotAppend(
|
||||
lowerer,
|
||||
varRef("out.0", outT, loc),
|
||||
varRef("a.0", arrT, loc),
|
||||
varRef("i.0", F64, loc),
|
||||
elem,
|
||||
"v.0",
|
||||
loc,
|
||||
),
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
),
|
||||
},
|
||||
]
|
||||
: [
|
||||
{ kind: "varDecl", localId: "v.0", init: getElem, loc },
|
||||
{
|
||||
kind: "if",
|
||||
// ToBoolean over the predicate's answer — inert when it already
|
||||
// returned bool, the per-union helper when it returned a union.
|
||||
cond: filterCond(callF(varRef("v.0", valueT, loc)), fnRet, loc),
|
||||
then: [
|
||||
arrayValueStore(
|
||||
lowerer,
|
||||
varRef("out.0", outT, loc),
|
||||
{
|
||||
kind: "arrIntrinsic",
|
||||
method: "length",
|
||||
receiver: varRef("out.0", outT, loc),
|
||||
args: [],
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
retained,
|
||||
outElem,
|
||||
loc,
|
||||
),
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
@@ -1088,7 +1134,7 @@ export function lowerArrayFindLikeCall(
|
||||
presentElements(lowerer, call, elem) &&
|
||||
((method !== "find" && method !== "findLast") ||
|
||||
(!bindUntyped && lowerer.coercibleValue(elem, lowerer.irTypeOf(call))));
|
||||
const { fnArg, arity } = lowerArrayCallback(
|
||||
const { fnArg, arity, unread } = lowerArrayCallback(
|
||||
lowerer,
|
||||
argNode,
|
||||
[callbackValueType(lowerer, elem, present)],
|
||||
@@ -1109,18 +1155,19 @@ export function lowerArrayFindLikeCall(
|
||||
method === "findIndex" ||
|
||||
method === "findLastIndex"
|
||||
) {
|
||||
const fast = present
|
||||
? null
|
||||
: lowerFastCallback(
|
||||
lowerer,
|
||||
argNode,
|
||||
[arrayValueType(lowerer, elem)],
|
||||
0,
|
||||
elem,
|
||||
arrT,
|
||||
{ fnArg, arity },
|
||||
bindUntyped,
|
||||
);
|
||||
const fast =
|
||||
present || unread
|
||||
? null
|
||||
: lowerFastCallback(
|
||||
lowerer,
|
||||
argNode,
|
||||
[arrayValueType(lowerer, elem)],
|
||||
0,
|
||||
elem,
|
||||
arrT,
|
||||
{ fnArg, arity },
|
||||
bindUntyped,
|
||||
);
|
||||
const fastT = fast?.type ?? null;
|
||||
const helper =
|
||||
method === "findIndex" || method === "findLastIndex"
|
||||
@@ -1134,6 +1181,7 @@ export function lowerArrayFindLikeCall(
|
||||
callbackSite(fast ?? fnArg),
|
||||
fastT,
|
||||
present,
|
||||
unread,
|
||||
)
|
||||
: someEveryHelper(
|
||||
lowerer,
|
||||
@@ -1145,6 +1193,7 @@ export function lowerArrayFindLikeCall(
|
||||
callbackSite(fast ?? fnArg),
|
||||
fastT,
|
||||
present,
|
||||
unread,
|
||||
);
|
||||
return {
|
||||
kind: "call",
|
||||
@@ -1182,6 +1231,7 @@ export function lowerArrayFindLikeCall(
|
||||
retag,
|
||||
callbackSite(fnArg),
|
||||
present,
|
||||
unread,
|
||||
);
|
||||
return { kind: "call", callee: helper, args: [receiver, fnArg], type: resultT, loc };
|
||||
}
|
||||
@@ -1275,9 +1325,10 @@ function findHelper(
|
||||
retag: string | null,
|
||||
site = "",
|
||||
present = false,
|
||||
unread = false,
|
||||
): string {
|
||||
const method = last ? "findLast" : "find";
|
||||
const key = `${method}:${typeKey(elem)}:${typeKey(resultT)}:${typeKey(fnRet)}:${arity}${site}${present ? ":present" : ""}`;
|
||||
const key = `${method}:${typeKey(elem)}:${typeKey(resultT)}:${typeKey(fnRet)}:${arity}${site}${present ? ":present" : ""}${unread ? ":unread" : ""}`;
|
||||
const existing = lowerer.arrHofHelpers.get(key);
|
||||
if (existing) return existing;
|
||||
const name = keyedHelperName(lowerer, `%arr.${method}`, key);
|
||||
@@ -1296,28 +1347,45 @@ function findHelper(
|
||||
type: resultT,
|
||||
loc,
|
||||
};
|
||||
const visitBody: IrStmt[] = [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "v.0",
|
||||
init: callbackValueRead(
|
||||
lowerer,
|
||||
varRef("a.0", arrT, loc),
|
||||
varRef("i.0", F64, loc),
|
||||
elem,
|
||||
present,
|
||||
loc,
|
||||
),
|
||||
const read: IrStmt = {
|
||||
kind: "varDecl",
|
||||
localId: "v.0",
|
||||
init: callbackValueRead(
|
||||
lowerer,
|
||||
varRef("a.0", arrT, loc),
|
||||
varRef("i.0", F64, loc),
|
||||
elem,
|
||||
present,
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "if",
|
||||
cond: predicateToBoolean(callArrayCallback(arrT, fnT, fnRet, arity, v, loc)),
|
||||
then: [{ kind: "return", value: found, loc }],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
];
|
||||
),
|
||||
loc,
|
||||
};
|
||||
// A predicate that takes no element, or never reads it, reads it only
|
||||
// once it holds: the element of a visit it rejects stays unread.
|
||||
const placeholder = unread ? unreadArgument(lowerer, valueT, loc) : null;
|
||||
const visitBody: IrStmt[] =
|
||||
arity === 0 || placeholder
|
||||
? [
|
||||
{
|
||||
kind: "if",
|
||||
cond: predicateToBoolean(
|
||||
callArrayCallback(arrT, fnT, fnRet, arity, placeholder ?? v, loc),
|
||||
),
|
||||
then: [read, { kind: "return", value: found, loc }],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
]
|
||||
: [
|
||||
read,
|
||||
{
|
||||
kind: "if",
|
||||
cond: predicateToBoolean(callArrayCallback(arrT, fnT, fnRet, arity, v, loc)),
|
||||
then: [{ kind: "return", value: found, loc }],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
];
|
||||
const visit: IrStmt[] = visitBody;
|
||||
const loop = last
|
||||
? reverseArrayLoop(loc, visit)
|
||||
@@ -1375,9 +1443,10 @@ function findIndexHelper(
|
||||
site = "",
|
||||
fastT: CallbackType | null = null,
|
||||
present = false,
|
||||
unread = false,
|
||||
): string {
|
||||
const method = last ? "findLastIndex" : "findIndex";
|
||||
const key = `${method}:${typeKey(elem)}:${typeKey(fnRet)}:${arity}${site}${fastT ? ":split" : ""}${present ? ":present" : ""}`;
|
||||
const key = `${method}:${typeKey(elem)}:${typeKey(fnRet)}:${arity}${site}${fastT ? ":split" : ""}${present ? ":present" : ""}${unread ? ":unread" : ""}`;
|
||||
const existing = lowerer.arrHofHelpers.get(key);
|
||||
if (existing) return existing;
|
||||
const name = keyedHelperName(lowerer, `%arr.${method}`, key);
|
||||
@@ -1391,6 +1460,7 @@ function findIndexHelper(
|
||||
outElem: elem,
|
||||
present,
|
||||
split: !!fastT,
|
||||
unread,
|
||||
});
|
||||
const arrT = arrayOf(elem);
|
||||
const fnT = funcOf([callbackValueType(lowerer, elem, present), F64, arrT].slice(0, arity), fnRet);
|
||||
@@ -1411,6 +1481,7 @@ function findIndexHelper(
|
||||
elem,
|
||||
present,
|
||||
loc,
|
||||
unread,
|
||||
),
|
||||
loc,
|
||||
),
|
||||
@@ -1460,8 +1531,9 @@ function someEveryHelper(
|
||||
site = "",
|
||||
fastT: CallbackType | null = null,
|
||||
present = false,
|
||||
unread = false,
|
||||
): string {
|
||||
const key = `${method}:${typeKey(elem)}:${typeKey(fnRet)}:${arity}${site}${fastT ? ":split" : ""}${present ? ":present" : ""}`;
|
||||
const key = `${method}:${typeKey(elem)}:${typeKey(fnRet)}:${arity}${site}${fastT ? ":split" : ""}${present ? ":present" : ""}${unread ? ":unread" : ""}`;
|
||||
const existing = lowerer.arrHofHelpers.get(key);
|
||||
if (existing) return existing;
|
||||
const name = keyedHelperName(lowerer, `%arr.${method}`, key);
|
||||
@@ -1474,6 +1546,7 @@ function someEveryHelper(
|
||||
outElem: elem,
|
||||
present,
|
||||
split: !!fastT,
|
||||
unread,
|
||||
});
|
||||
const arrT = arrayOf(elem);
|
||||
const fnT = funcOf([callbackValueType(lowerer, elem, present), F64, arrT].slice(0, arity), fnRet);
|
||||
@@ -1490,6 +1563,7 @@ function someEveryHelper(
|
||||
elem,
|
||||
present,
|
||||
loc,
|
||||
unread,
|
||||
),
|
||||
loc,
|
||||
),
|
||||
@@ -1553,7 +1627,7 @@ export function lowerArrayFlatMapCall(
|
||||
const argNode = call.arguments.nodes[0];
|
||||
if (!argNode) lowerer.unsupported("SC1090", call, "this call form"); // tsc-guarded
|
||||
const present = presentElements(lowerer, call, elem);
|
||||
const { fnArg, arity } = lowerArrayCallback(
|
||||
const { fnArg, arity, unread } = lowerArrayCallback(
|
||||
lowerer,
|
||||
argNode,
|
||||
[callbackValueType(lowerer, elem, present)],
|
||||
@@ -1603,12 +1677,13 @@ export function lowerArrayFlatMapCall(
|
||||
if (fnRet.kind === "void" || fnRet.kind === "func") lowerer.badType(call, lowerer.typeOf(call));
|
||||
requireProducedArrayElement(lowerer, call, "'.flatMap()'", fnRet);
|
||||
const outElem = callbackArrayElement(lowerer, call, fnRet);
|
||||
const fast = present
|
||||
? null
|
||||
: lowerFastCallback(lowerer, argNode, [arrayValueType(lowerer, elem)], 0, elem, arrT, {
|
||||
fnArg,
|
||||
arity,
|
||||
});
|
||||
const fast =
|
||||
present || unread
|
||||
? null
|
||||
: lowerFastCallback(lowerer, argNode, [arrayValueType(lowerer, elem)], 0, elem, arrT, {
|
||||
fnArg,
|
||||
arity,
|
||||
});
|
||||
const helper = arrayHofHelper(
|
||||
lowerer,
|
||||
"flatMap",
|
||||
@@ -1620,6 +1695,7 @@ export function lowerArrayFlatMapCall(
|
||||
callbackSite(fast ?? fnArg),
|
||||
fast?.type ?? null,
|
||||
present,
|
||||
unread,
|
||||
);
|
||||
return {
|
||||
kind: "call",
|
||||
@@ -1629,12 +1705,13 @@ export function lowerArrayFlatMapCall(
|
||||
loc,
|
||||
};
|
||||
}
|
||||
const fast = present
|
||||
? null
|
||||
: lowerFastCallback(lowerer, argNode, [arrayValueType(lowerer, elem)], 0, elem, arrT, {
|
||||
fnArg,
|
||||
arity,
|
||||
});
|
||||
const fast =
|
||||
present || unread
|
||||
? null
|
||||
: lowerFastCallback(lowerer, argNode, [arrayValueType(lowerer, elem)], 0, elem, arrT, {
|
||||
fnArg,
|
||||
arity,
|
||||
});
|
||||
const helper = flatMapHelper(
|
||||
lowerer,
|
||||
elem,
|
||||
@@ -1644,6 +1721,7 @@ export function lowerArrayFlatMapCall(
|
||||
callbackSite(fast ?? fnArg),
|
||||
fast?.type ?? null,
|
||||
present,
|
||||
unread,
|
||||
);
|
||||
return {
|
||||
kind: "call",
|
||||
@@ -1943,8 +2021,9 @@ function flatMapHelper(
|
||||
site = "",
|
||||
fastT: CallbackType | null = null,
|
||||
present = false,
|
||||
unread = false,
|
||||
): string {
|
||||
const key = `flatMap:${typeKey(elem)}:${typeKey(fnRet)}:${arity}${site}${fastT ? ":split" : ""}${present ? ":present" : ""}`;
|
||||
const key = `flatMap:${typeKey(elem)}:${typeKey(fnRet)}:${arity}${site}${fastT ? ":split" : ""}${present ? ":present" : ""}${unread ? ":unread" : ""}`;
|
||||
const existing = lowerer.arrHofHelpers.get(key);
|
||||
if (existing) return existing;
|
||||
const name = keyedHelperName(lowerer, "%arr.flatMap", key);
|
||||
@@ -1957,6 +2036,7 @@ function flatMapHelper(
|
||||
outElem: fnRet.elem,
|
||||
present,
|
||||
split: !!fastT,
|
||||
unread,
|
||||
});
|
||||
const arrT = arrayOf(elem);
|
||||
const inner = fnRet.elem;
|
||||
@@ -2053,6 +2133,7 @@ function flatMapHelper(
|
||||
elem,
|
||||
present,
|
||||
loc,
|
||||
unread,
|
||||
),
|
||||
loc,
|
||||
),
|
||||
@@ -2125,7 +2206,7 @@ export function lowerArrayReduceCall(
|
||||
lowerer.runtimeOptionalReduceTypes.get(call) ??
|
||||
(hasInit ? lowerer.irTypeOf(call) : arrayValueType(lowerer, elem));
|
||||
if (accT.kind === "void" || accT.kind === "func") lowerer.badType(call, lowerer.typeOf(call));
|
||||
const { fnArg, arity } = lowerArrayCallback(
|
||||
const { fnArg, arity, unread } = lowerArrayCallback(
|
||||
lowerer,
|
||||
argNode,
|
||||
[accT, arrayValueType(lowerer, elem)],
|
||||
@@ -2133,17 +2214,19 @@ export function lowerArrayReduceCall(
|
||||
false,
|
||||
accT,
|
||||
);
|
||||
const fast = lowerFastCallback(
|
||||
lowerer,
|
||||
argNode,
|
||||
[accT, arrayValueType(lowerer, elem)],
|
||||
1,
|
||||
elem,
|
||||
arrT,
|
||||
{ fnArg, arity },
|
||||
false,
|
||||
accT,
|
||||
);
|
||||
const fast = unread
|
||||
? null
|
||||
: lowerFastCallback(
|
||||
lowerer,
|
||||
argNode,
|
||||
[accT, arrayValueType(lowerer, elem)],
|
||||
1,
|
||||
elem,
|
||||
arrT,
|
||||
{ fnArg, arity },
|
||||
false,
|
||||
accT,
|
||||
);
|
||||
const helper = reduceHelper(
|
||||
lowerer,
|
||||
method,
|
||||
@@ -2155,6 +2238,7 @@ export function lowerArrayReduceCall(
|
||||
loc,
|
||||
callbackSite(fast ?? fnArg),
|
||||
fast?.type ?? null,
|
||||
unread,
|
||||
);
|
||||
const args: IrExpr[] = [receiver, fnArg];
|
||||
if (hasInit) args.push(lowerer.lowerExprExpecting(call.arguments.nodes[1]!, accT));
|
||||
@@ -2185,8 +2269,9 @@ function reduceHelper(
|
||||
loc: SrcLoc,
|
||||
site = "",
|
||||
fastT: CallbackType | null = null,
|
||||
unread = false,
|
||||
): string {
|
||||
const key = `${method}:${typeKey(elem)}:${typeKey(accT)}:${typeKey(fnRet)}:${arity}:${hasInit ? "init" : "seed"}${site}${fastT ? ":split" : ""}`;
|
||||
const key = `${method}:${typeKey(elem)}:${typeKey(accT)}:${typeKey(fnRet)}:${arity}:${hasInit ? "init" : "seed"}${site}${fastT ? ":split" : ""}${unread ? ":unread" : ""}`;
|
||||
const existing = lowerer.arrHofHelpers.get(key);
|
||||
if (existing) return existing;
|
||||
const name = keyedHelperName(lowerer, `%arr.${method}`, key);
|
||||
@@ -2201,6 +2286,7 @@ function reduceHelper(
|
||||
present: false,
|
||||
split: !!fastT,
|
||||
hasInit,
|
||||
unread,
|
||||
});
|
||||
const arrT = arrayOf(elem);
|
||||
const fnT = funcOf([accT, arrayValueType(lowerer, elem), F64, arrT].slice(0, arity), fnRet);
|
||||
@@ -2210,6 +2296,9 @@ function reduceHelper(
|
||||
const right = method === "reduceRight";
|
||||
const at = (index: IrExpr): IrExpr =>
|
||||
arrayValueRead(lowerer, varRef("a.0", arrT, loc), index, elem, loc);
|
||||
// The visited element, or a placeholder for a callback that never reads it.
|
||||
const placeholder = unread ? unreadArgument(lowerer, arrayValueType(lowerer, elem), loc) : null;
|
||||
const visited = (): IrExpr => placeholder ?? at(i);
|
||||
const locals: IrLocal[] = [
|
||||
{ id: "a.0", name: "a", type: arrT, mutable: true },
|
||||
{ id: "f.0", name: "f", type: fnT, mutable: true },
|
||||
@@ -2320,7 +2409,7 @@ function reduceHelper(
|
||||
{
|
||||
kind: "callValue",
|
||||
callee: varRef("f.0", fnT, loc),
|
||||
args: [varRef("acc.0", accT, loc), at(i), i, varRef("a.0", arrT, loc)].slice(
|
||||
args: [varRef("acc.0", accT, loc), visited(), i, varRef("a.0", arrT, loc)].slice(
|
||||
0,
|
||||
arity,
|
||||
),
|
||||
|
||||
@@ -32,9 +32,11 @@ import {
|
||||
import {
|
||||
arrayIndexPresent,
|
||||
arrayValueRead,
|
||||
arraySlotAppend,
|
||||
arrayValueStore,
|
||||
arrayValueType,
|
||||
elementMayBeForeign,
|
||||
unreadArgument,
|
||||
} from "../array-values.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
|
||||
@@ -62,6 +64,9 @@ export interface ArrayHofInfo {
|
||||
present: boolean;
|
||||
split: boolean;
|
||||
hasInit?: boolean;
|
||||
/** The callback never reads the element it declares
|
||||
* (lowerArrayCallback's `unread`). */
|
||||
unread?: boolean;
|
||||
}
|
||||
|
||||
type FuncType = IrType & { kind: "func" };
|
||||
@@ -1595,13 +1600,21 @@ class FusedLoop {
|
||||
stage: Stage,
|
||||
k: number,
|
||||
lead: IrExpr[],
|
||||
flow: Flow & { kind: "value" },
|
||||
flow: Flow,
|
||||
): { pre: IrStmt[]; value: IrExpr } | null => {
|
||||
const arity = stage.info.arity;
|
||||
const value = varRef(flow.id, flow.type, loc);
|
||||
const at = lead.length;
|
||||
const pick = (): { arg: IrExpr & { type: FuncType }; value: IrExpr | null } | null => {
|
||||
if (arity <= at) return { arg: stage.slow, value: null };
|
||||
// A callback that reads the value reaches it read (`read`); one
|
||||
// that never reads it takes a placeholder for the unread slot.
|
||||
if (flow.kind !== "value") {
|
||||
const placeholder = stage.info.unread
|
||||
? unreadArgument(this.lowerer, stage.slow.type.params[at]!, loc)
|
||||
: null;
|
||||
return placeholder ? { arg: stage.slow, value: placeholder } : null;
|
||||
}
|
||||
const value = varRef(flow.id, flow.type, loc);
|
||||
const fast = stage.fast;
|
||||
if (fast && typeEquals(fast.type.params[at]!, flow.type)) return { arg: fast, value };
|
||||
const paramT = stage.slow.type.params[at]!;
|
||||
@@ -1630,6 +1643,12 @@ class FusedLoop {
|
||||
return { pre: [...before, ...called.pre], value: called.value };
|
||||
};
|
||||
|
||||
/** The stage's callback never reads the element at parameter `at`, and
|
||||
* a placeholder of its type stands for it (unreadArgument). */
|
||||
const unreadSlot = (stage: Stage, at: number): boolean =>
|
||||
stage.info.unread === true &&
|
||||
unreadArgument(this.lowerer, stage.slow.type.params[at]!, loc) !== null;
|
||||
|
||||
const fail = (): IrStmt[] => {
|
||||
this.failed = true;
|
||||
return [];
|
||||
@@ -1694,7 +1713,12 @@ class FusedLoop {
|
||||
if (k === chain.stages.length) return finish(flow);
|
||||
const stage = chain.stages[k]!;
|
||||
const info = stage.info;
|
||||
if (flow.kind === "slot") return read(stage, (value) => visit(k, value));
|
||||
// A callback that declares no element parameter, or never reads the
|
||||
// one it declares, leaves the slot unread, so an element outside the
|
||||
// element type passes it as it is (a kept one is copied by the next
|
||||
// stage or the collecting terminal).
|
||||
if (flow.kind === "slot" && info.arity > 0 && !unreadSlot(stage, 0))
|
||||
return read(stage, (value) => visit(k, value));
|
||||
const called = deliver(stage, k, [], flow);
|
||||
if (!called) return fail();
|
||||
const { pre, value: result } = called;
|
||||
@@ -1791,11 +1815,20 @@ class FusedLoop {
|
||||
}
|
||||
};
|
||||
|
||||
/** Append the source slot to the collected result unread: a filter
|
||||
* whose callback never read the element keeps it as it is stored
|
||||
* (arraySlotAppend). */
|
||||
const copySlot = (): IrStmt[] => {
|
||||
const outT = resultT as IrType & { kind: "array" };
|
||||
if (holes || !typeEquals(outT.elem, arrT.elem)) return fail();
|
||||
const v = this.local("v", arrayValueType(this.lowerer, arrT.elem));
|
||||
return [arraySlotAppend(this.lowerer, varRef(out!, resultT, loc), a, i, arrT.elem, v, loc)];
|
||||
};
|
||||
/** The terminal's action for one value. */
|
||||
const finish = (flow: Flow): IrStmt[] => {
|
||||
if (terminal.kind === "length") return [increment(count!, loc)];
|
||||
if (terminal.kind === "collect") {
|
||||
if (flow.kind !== "value") return fail();
|
||||
if (flow.kind !== "value") return copySlot();
|
||||
const last = chain.stages[chain.stages.length - 1]!;
|
||||
const outT = resultT as IrType & { kind: "array" };
|
||||
if (!typeEquals(outT.elem, last.info.outElem)) return fail();
|
||||
@@ -1816,7 +1849,9 @@ class FusedLoop {
|
||||
}
|
||||
const stage = terminal.stage;
|
||||
const method = stage.info.method;
|
||||
if (flow.kind === "slot") return read(stage, (value) => finish(value));
|
||||
const lead = method === "reduce" ? 1 : 0;
|
||||
if (flow.kind === "slot" && stage.info.arity > lead && !unreadSlot(stage, lead))
|
||||
return read(stage, (value) => finish(value));
|
||||
if (method === "reduce") {
|
||||
const accT = stage.info.outElem;
|
||||
const called = deliver(stage, chain.stages.length, [varRef(acc!, accT, loc)], flow);
|
||||
|
||||
@@ -53,7 +53,7 @@ export function lowerArrayCallback(
|
||||
arrT: IrType,
|
||||
bindUntyped = false,
|
||||
expectedReturn?: IrType,
|
||||
): { fnArg: IrExpr & { type: IrType & { kind: "func" } }; arity: number } {
|
||||
): { fnArg: IrExpr & { type: IrType & { kind: "func" } }; arity: number; unread: boolean } {
|
||||
// `xs.map(((x) => x))`: a parenthesized callback is the function itself.
|
||||
while (ts.isParenthesizedExpression(argNode)) argNode = argNode.expression;
|
||||
// The ambient `Boolean` as the callback (`flags.every(Boolean)`,
|
||||
@@ -72,7 +72,11 @@ export function lowerArrayCallback(
|
||||
if (valueT === undefined || valueT.kind === "void" || isUnitType(valueT))
|
||||
lowerer.badType(argNode, lowerer.typeOf(argNode));
|
||||
if (valueT.kind === "union") lowerer.requireTruthyUnion(valueT.unionId, argNode);
|
||||
return { fnArg: truthyFilterCallback(lowerer, valueT, locOf(argNode)), arity: 1 };
|
||||
return {
|
||||
fnArg: truthyFilterCallback(lowerer, valueT, locOf(argNode)),
|
||||
arity: 1,
|
||||
unread: false,
|
||||
};
|
||||
}
|
||||
// The ambient `String` (`values.map(String)`): String(value) converts its
|
||||
// first argument as a direct call does, whatever the element type; the
|
||||
@@ -86,7 +90,7 @@ export function lowerArrayCallback(
|
||||
const valueT = lead[0];
|
||||
if (valueT === undefined || valueT.kind === "void")
|
||||
lowerer.badType(argNode, lowerer.typeOf(argNode));
|
||||
return { fnArg: stringConversionCallback(lowerer, valueT, argNode), arity: 1 };
|
||||
return { fnArg: stringConversionCallback(lowerer, valueT, argNode), arity: 1, unread: false };
|
||||
}
|
||||
const full = [...lead, F64, arrT];
|
||||
// A DYN-receiver HOF's callback (`parsed.flatMap((value) => ...)`):
|
||||
@@ -276,9 +280,32 @@ export function lowerArrayCallback(
|
||||
return {
|
||||
fnArg: fnArg as IrExpr & { type: IrType & { kind: "func" } },
|
||||
arity: fnArg.type.params.length,
|
||||
unread: !readsParameter(argNode, lead.length - 1),
|
||||
};
|
||||
}
|
||||
|
||||
/** Whether a callback can observe its parameter at `index`: false for a
|
||||
* function or arrow expression that declares no such parameter, or binds
|
||||
* it to a plain name that appears nowhere else in it (any identifier of
|
||||
* that name counts, including shorthand properties and shadowing
|
||||
* bindings). Any other callback may. */
|
||||
function readsParameter(argNode: ts.Expression, index: number): boolean {
|
||||
if (!ts.isArrowFunction(argNode) && !ts.isFunctionExpression(argNode)) return true;
|
||||
const param = argNode.parameters.nodes[index];
|
||||
if (!param) return false;
|
||||
if (!ts.isIdentifier(param.name) || param.initializer || param.dotDotDotToken) return true;
|
||||
const name = param.name.text;
|
||||
const mentions = (node: ts.Node): boolean =>
|
||||
(node !== param.name &&
|
||||
ts.isIdentifier(node) &&
|
||||
(node.text === name || node.text === "arguments")) ||
|
||||
(ts.forEachChild(node, (child) => (mentions(child) ? true : undefined)) ?? false);
|
||||
return (
|
||||
argNode.parameters.nodes.some((other) => other !== param && mentions(other)) ||
|
||||
mentions(argNode.body)
|
||||
);
|
||||
}
|
||||
|
||||
/** Preserve the checker-selected payload ABI when a callback's runtime
|
||||
* return gained only undefined. The array state byte carries that value. */
|
||||
export function callbackArrayElement(
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8014-foreign-element-callbacks.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// Callbacks over a number[] or string[] that holds elements written
|
||||
// through an unknown[] view, when the callback never reads the element:
|
||||
// it declares no element parameter, or never references the one it
|
||||
// declares. The methods visit every element as in Node, and filter keeps
|
||||
// a written element as it is.
|
||||
|
||||
const numbers: number[] = [1, 2];
|
||||
const numbersView: unknown[] = numbers;
|
||||
numbersView.push("x", undefined, { k: 1 });
|
||||
numbers.push(5);
|
||||
|
||||
// Inline callbacks (fused loops).
|
||||
console.log(numbers.filter(() => true));
|
||||
console.log(numbers.filter((_, i) => i % 2 === 0));
|
||||
console.log(numbers.map(() => 0));
|
||||
console.log(numbers.map((_, i) => i * 10));
|
||||
console.log(numbers.some(() => false), numbers.every((_v) => true));
|
||||
console.log(numbers.findIndex((_, i) => i === 2), numbers.findLastIndex((_, i) => i < 3));
|
||||
console.log(numbers.find(() => false), numbers.findLast((_, i) => i === 0));
|
||||
console.log(numbers.reduce((s) => s + 1, 0), numbers.reduce((s, v) => s + 1, 100));
|
||||
console.log(numbers.reduceRight((s, _v, i) => s + i, 0));
|
||||
let visits = 0;
|
||||
numbers.forEach((_, i) => {
|
||||
visits += i;
|
||||
});
|
||||
console.log(visits);
|
||||
console.log(numbers.filter((_, i) => i > 1).map((_, i) => i));
|
||||
console.log(numbers.flatMap(() => [7]), numbers.flatMap((_, i) => [i, -i]));
|
||||
console.log(numbers.filter(() => true).length, typeof (numbers.filter(() => true) as unknown[])[2]);
|
||||
|
||||
// Callback values (shared helpers).
|
||||
const always = (): boolean => true;
|
||||
const never = (): boolean => false;
|
||||
const count = (s: number): number => s + 1;
|
||||
console.log(numbers.filter(always));
|
||||
console.log(numbers.some(never), numbers.every(always), numbers.find(never));
|
||||
console.log(numbers.findIndex(never), numbers.reduce(count, 0));
|
||||
console.log(numbers.map(always));
|
||||
|
||||
// A kept written element is the same value in the result.
|
||||
const kept = numbers.filter(always) as unknown[];
|
||||
console.log(kept[2] === "x", kept[3] === undefined, 3 in kept, kept[4], kept.length);
|
||||
|
||||
// string[] the same way.
|
||||
const words: string[] = ["a", "b"];
|
||||
const wordsView: unknown[] = words;
|
||||
wordsView.push(7, null);
|
||||
console.log(words.filter(() => true), words.map((_, i) => `#${i}`));
|
||||
console.log(words.filter((_w, i) => i !== 1), words.reduce((s) => s + 1, 0));
|
||||
|
||||
// Holes stay holes and are skipped as in Node.
|
||||
const sparse: number[] = [1];
|
||||
const sparseView: unknown[] = sparse;
|
||||
sparseView[3] = "z";
|
||||
console.log(sparse.filter(() => true), sparse.map((_, i) => i), sparse.findIndex((_, i) => i === 1));
|
||||
|
||||
// A parameter used only through a shorthand property is still read.
|
||||
const plain = [1, 2, 3];
|
||||
console.log(plain.map((v) => ({ v })), plain.reduce((acc, v) => ({ sum: acc.sum + v, v }), { sum: 0, v: 0 }));
|
||||
console.log(plain.filter((v) => [{ v }].length > 0 && v > 1));
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 5089, "san": 8118 }
|
||||
Reference in New Issue
Block a user