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:
Chris Tate
2026-10-25 19:36:55 -05:00
parent 58a64ffb36
commit 1ea8a6c467
7 changed files with 411 additions and 104 deletions
@@ -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(
@@ -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 }