Skip the element read for array callbacks that never use it (#2286)

This commit is contained in:
Chris Tate
2026-10-26 00:29:20 -05:00
committed by GitHub
7 changed files with 671 additions and 106 deletions
@@ -3,8 +3,10 @@ import { InternalCompilerError } from "../../errors.js";
import { dynUndefinedExpr, helperLoc, varRef } from "../../ir/build.js";
import {
BOOL,
DYN,
F64,
type IrExpr,
type IrLocal,
type IrStmt,
type IrType,
isIntegerBytesElem,
@@ -446,6 +448,167 @@ 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;
}
/** The locals of an element snapshot (arraySlotSnapshot): whether the slot
* held an element outside the element type, the value read otherwise, and
* that element's dynamic value otherwise. */
export interface SlotSnapshot {
isForeign: string;
value: string;
foreign: string;
}
/** The locals a snapshot of an `elem` element uses. */
export function slotSnapshotLocals(lowerer: Lowerer, elem: IrType, ids: SlotSnapshot): IrLocal[] {
return [
{ id: ids.isForeign, name: "foreign", type: BOOL, mutable: false },
{ id: ids.value, name: "value", type: arrayValueType(lowerer, elem), mutable: false },
{ id: ids.foreign, name: "element", type: DYN, mutable: false },
];
}
/** Read the element at `arr[index]` as it is stored, before a callback runs,
* as JavaScript reads it before calling a callback that ignores it: a value
* or a present undefined into `ids.value`, or an element outside the element
* type (stored through a dynamic view) as its dynamic value into
* `ids.foreign`, which `ids.isForeign` tells apart. */
export function arraySlotSnapshot(
lowerer: Lowerer,
arr: IrExpr,
index: IrExpr,
elem: IrType,
ids: SlotSnapshot,
loc: SrcLoc,
): IrStmt[] {
const valueT = arrayValueType(lowerer, elem);
const isForeign: IrExpr = { kind: "varRef", localId: ids.isForeign, type: BOOL, loc };
const missing =
valueT.kind === "dyn" ? dynUndefinedExpr(loc) : lowerer.wrappedUndefined(valueT, loc)!;
return [
{
kind: "varDecl",
localId: ids.isForeign,
init: {
kind: "bin",
op: "===",
left: { kind: "arrayState", arr, index, type: F64, loc },
right: { kind: "numLit", value: 3, type: F64, loc },
type: BOOL,
loc,
},
loc,
},
{
kind: "varDecl",
localId: ids.value,
init: {
kind: "ternary",
cond: isForeign,
then: missing,
else_: arrayValueRead(lowerer, arr, index, elem, loc),
type: valueT,
loc,
},
loc,
},
{
kind: "varDecl",
localId: ids.foreign,
init: {
kind: "ternary",
cond: isForeign,
then: { kind: "libCall", fn: "array.foreign", args: [arr, index], type: DYN, loc },
else_: dynUndefinedExpr(loc),
type: DYN,
loc,
},
loc,
},
];
}
/** Append a snapshot (arraySlotSnapshot) of an element to `out`, an array of
* the same element type, as it was stored: an element outside the element
* type stays one, appended as the view's push appends it. `out` is a
* binding. */
export function arraySnapshotAppend(
lowerer: Lowerer,
out: IrExpr,
elem: IrType,
ids: SlotSnapshot,
loc: SrcLoc,
): IrStmt {
const end: IrExpr = {
kind: "arrIntrinsic",
method: "length",
receiver: out,
args: [],
type: F64,
loc,
};
const value: IrExpr = {
kind: "varRef",
localId: ids.value,
type: arrayValueType(lowerer, elem),
loc,
};
return {
kind: "if",
cond: { kind: "varRef", localId: ids.isForeign, 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: "varRef", localId: ids.foreign, type: DYN, loc }],
type: DYN,
loc,
},
loc,
},
],
else_: [arrayValueStore(lowerer, out, end, value, elem, loc)],
loc,
};
}
/** A snapshot (arraySlotSnapshot) as a value of `type`: the value read, or
* the element outside the element type through the checked conversion,
* which such an element fails as a typed read does. `typed` converts the
* value read. */
export function arraySnapshotValue(
lowerer: Lowerer,
elem: IrType,
ids: SlotSnapshot,
type: IrType,
typed: (value: IrExpr) => IrExpr,
loc: SrcLoc,
): IrExpr {
return {
kind: "ternary",
cond: { kind: "varRef", localId: ids.isForeign, type: BOOL, loc },
then: lowerer.coerceToExpected({ kind: "varRef", localId: ids.foreign, type: DYN, loc }, type),
else_: typed({ kind: "varRef", localId: ids.value, type: arrayValueType(lowerer, elem), loc }),
type,
loc,
};
}
/** Store a stabilized value without changing the array's element ABI. */
export function arrayValueStore(
lowerer: Lowerer,
@@ -38,6 +38,9 @@ import {
} from "../lower-array-sort.js";
import {
arrayIndexPresent,
arraySlotSnapshot,
arraySnapshotAppend,
arraySnapshotValue,
arrayValueRead,
arrayValueStore,
arrayValueType,
@@ -45,6 +48,9 @@ import {
elementMayBeForeign,
foreignElementThrow,
primitiveElement,
slotSnapshotLocals,
unreadArgument,
type SlotSnapshot,
} from "../array-values.js";
import { typeKey } from "../../type-mapper.js";
import {
@@ -101,9 +107,12 @@ 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;
// Only an array that can hold elements outside its element type needs to
// leave an element its callback ignores unread.
const unread = callback.unread && !present && elementMayBeForeign(lowerer, elem);
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 +142,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 +158,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,12 +552,21 @@ 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);
}
/** The snapshot locals of the filter and find helpers; `value` is their
* element local `v.0`. */
const FILTER_SNAPSHOT: SlotSnapshot = { isForeign: "vf.0", value: "v.0", foreign: "vd.0" };
/** Interned synthetic loop function for one (method, elem, fnRet, arity)
* combo. Named `%arr.<method>.<n>` ('%' keeps it out of the user
* namespace); rides `liftedFns` into the module like a lifted lambda (it
@@ -562,13 +582,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 +610,7 @@ function arrayHofHelper(
outElem,
fastT,
present,
unread,
);
lowerer.liftedFns.push(fastT ? withFastCallback(fn, fastT, elem) : fn);
return name;
@@ -794,6 +825,7 @@ function buildArrayHofFn(
outElem: IrType,
fastT: CallbackType | null = null,
present = false,
unread = false,
): IrFunction {
const arrT = arrayOf(elem);
const valueT = callbackValueType(lowerer, elem, present);
@@ -810,6 +842,19 @@ function buildArrayHofFn(
present,
loc,
);
// What the callback takes: the element, or a placeholder for one it
// ignores (callbackValueRead's `unread`).
const argElem = unread
? callbackValueRead(
lowerer,
varRef("a.0", arrT, loc),
varRef("i.0", F64, loc),
elem,
present,
loc,
true,
)
: getElem;
const hasElem: IrExpr = {
kind: "arrayHas",
arr: varRef("a.0", arrT, loc),
@@ -858,7 +903,7 @@ function buildArrayHofFn(
kind: "if",
cond: hasElem,
then: [
{ kind: "varDecl", localId: "mapped.0", init: callF(getElem), loc },
{ kind: "varDecl", localId: "mapped.0", init: callF(argElem), loc },
arrayValueStore(
lowerer,
varRef("out.0", outT, loc),
@@ -994,6 +1039,19 @@ function buildArrayHofFn(
);
returnType = outT;
const retained = filterRetainedValue(lowerer, varRef("v.0", valueT, loc), elem, outElem, loc);
// A predicate that ignores the element of an array that can hold
// elements outside its element type keeps a snapshot of the slot, read
// before the predicate runs, as stored (arraySlotSnapshot).
const copied =
(arity === 0 || unread) &&
!present &&
typeEquals(outElem, elem) &&
elementMayBeForeign(lowerer, elem);
// `v.0` is declared above; the snapshot adds its other two locals.
if (copied)
locals.push(
...slotSnapshotLocals(lowerer, elem, FILTER_SNAPSHOT).filter((l) => l.id !== "v.0"),
);
body = [
{
kind: "varDecl",
@@ -1006,34 +1064,62 @@ 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(
// A kept element is appended as it was stored, so one the element
// type cannot hold stays in the result.
then: copied
? [
...arraySlotSnapshot(
lowerer,
varRef("out.0", outT, loc),
{
kind: "arrIntrinsic",
method: "length",
receiver: varRef("out.0", outT, loc),
args: [],
type: F64,
loc,
},
retained,
outElem,
varRef("a.0", arrT, loc),
varRef("i.0", F64, loc),
elem,
FILTER_SNAPSHOT,
loc,
),
{
kind: "if",
cond: filterCond(callF(argElem), fnRet, loc),
then: [
arraySnapshotAppend(
lowerer,
varRef("out.0", outT, loc),
elem,
FILTER_SNAPSHOT,
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,
},
@@ -1048,7 +1134,7 @@ function buildArrayHofFn(
{
kind: "if",
cond: hasElem,
then: [{ kind: "exprStmt", expr: callF(getElem), loc }],
then: [{ kind: "exprStmt", expr: callF(argElem), loc }],
else_: null,
loc,
},
@@ -1088,13 +1174,17 @@ export function lowerArrayFindLikeCall(
presentElements(lowerer, call, elem) &&
((method !== "find" && method !== "findLast") ||
(!bindUntyped && lowerer.coercibleValue(elem, lowerer.irTypeOf(call))));
const { fnArg, arity } = lowerArrayCallback(
const lowered = lowerArrayCallback(
lowerer,
argNode,
[callbackValueType(lowerer, elem, present)],
arrT,
bindUntyped,
);
const { fnArg, arity } = lowered;
// Only an array that can hold elements outside its element type needs to
// leave an element its callback ignores unread.
const unread = lowered.unread && !present && elementMayBeForeign(lowerer, elem);
// JS takes the ToBoolean of the predicate's result — allowed wherever
// that ToBoolean has a static answer (bool passes through; f64/string
// by value; a truthy-answerable union by its arm — the
@@ -1109,18 +1199,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 +1225,7 @@ export function lowerArrayFindLikeCall(
callbackSite(fast ?? fnArg),
fastT,
present,
unread,
)
: someEveryHelper(
lowerer,
@@ -1145,6 +1237,7 @@ export function lowerArrayFindLikeCall(
callbackSite(fast ?? fnArg),
fastT,
present,
unread,
);
return {
kind: "call",
@@ -1182,6 +1275,7 @@ export function lowerArrayFindLikeCall(
retag,
callbackSite(fnArg),
present,
unread,
);
return { kind: "call", callee: helper, args: [receiver, fnArg], type: resultT, loc };
}
@@ -1275,9 +1369,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 +1391,62 @@ 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 ignores the element of an array that can hold
// elements outside its element type keeps a snapshot of the slot, read
// before the predicate runs as JavaScript reads it (arraySlotSnapshot):
// the predicate is called without reading it as typed, and a found element
// outside the element type takes the checked conversion into the result.
const ignored = (arity === 0 || unread) && !present && elementMayBeForeign(lowerer, elem);
const placeholder = unread ? unreadArgument(lowerer, valueT, loc) : null;
const visitBody: IrStmt[] = ignored
? [
...arraySlotSnapshot(
lowerer,
varRef("a.0", arrT, loc),
varRef("i.0", F64, loc),
elem,
FILTER_SNAPSHOT,
loc,
),
{
kind: "if",
cond: predicateToBoolean(
callArrayCallback(arrT, fnT, fnRet, arity, placeholder ?? v, loc),
),
then: [
{
kind: "return",
value: arraySnapshotValue(lowerer, elem, FILTER_SNAPSHOT, resultT, () => found, loc),
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)
@@ -1334,6 +1463,9 @@ function findHelper(
locals: [
...arrayCallbackLocals(arrT, fnT),
{ id: "v.0", name: "v", type: valueT, mutable: false },
...(ignored
? slotSnapshotLocals(lowerer, elem, FILTER_SNAPSHOT).filter((l) => l.id !== "v.0")
: []),
],
body,
loc,
@@ -1375,9 +1507,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 +1524,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 +1545,7 @@ function findIndexHelper(
elem,
present,
loc,
unread,
),
loc,
),
@@ -1460,8 +1595,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 +1610,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 +1627,7 @@ function someEveryHelper(
elem,
present,
loc,
unread,
),
loc,
),
@@ -1553,12 +1691,16 @@ 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 lowered = lowerArrayCallback(
lowerer,
argNode,
[callbackValueType(lowerer, elem, present)],
arrT,
);
const { fnArg, arity } = lowered;
// Only an array that can hold elements outside its element type needs to
// leave an element its callback ignores unread.
const unread = lowered.unread && !present && elementMayBeForeign(lowerer, elem);
const fnRet = fnArg.type.ret;
if (
fnRet.kind === "dyn" ||
@@ -1603,12 +1745,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 +1763,7 @@ export function lowerArrayFlatMapCall(
callbackSite(fast ?? fnArg),
fast?.type ?? null,
present,
unread,
);
return {
kind: "call",
@@ -1629,12 +1773,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 +1789,7 @@ export function lowerArrayFlatMapCall(
callbackSite(fast ?? fnArg),
fast?.type ?? null,
present,
unread,
);
return {
kind: "call",
@@ -1943,8 +2089,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 +2104,7 @@ function flatMapHelper(
outElem: fnRet.elem,
present,
split: !!fastT,
unread,
});
const arrT = arrayOf(elem);
const inner = fnRet.elem;
@@ -2053,6 +2201,7 @@ function flatMapHelper(
elem,
present,
loc,
unread,
),
loc,
),
@@ -2125,7 +2274,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 lowered = lowerArrayCallback(
lowerer,
argNode,
[accT, arrayValueType(lowerer, elem)],
@@ -2133,17 +2282,23 @@ export function lowerArrayReduceCall(
false,
accT,
);
const fast = lowerFastCallback(
lowerer,
argNode,
[accT, arrayValueType(lowerer, elem)],
1,
elem,
arrT,
{ fnArg, arity },
false,
accT,
);
const { fnArg, arity } = lowered;
// Only an array that can hold elements outside its element type needs to
// leave an element its callback ignores unread.
const unread = lowered.unread && elementMayBeForeign(lowerer, elem);
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 +2310,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 +2341,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 +2358,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 +2368,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 +2481,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,
),
@@ -6,6 +6,7 @@ import { AmbientReceiverReaders } from "../../../ir/receiver-readers.js";
import { boolLit, countedFor, numLit, varRef } from "../../../ir/build.js";
import {
BOOL,
DYN,
F64,
VOID,
type IrBindingSource,
@@ -32,9 +33,14 @@ import {
import {
arrayIndexPresent,
arrayValueRead,
arraySlotSnapshot,
arraySnapshotAppend,
arraySnapshotValue,
arrayValueStore,
arrayValueType,
elementMayBeForeign,
unreadArgument,
type SlotSnapshot,
} from "../array-values.js";
import type { Lowerer } from "../lowerer.js";
@@ -62,6 +68,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" };
@@ -610,7 +619,13 @@ function chainCallbacks(chain: Chain): Stage[] | null {
/** The values a fused loop hands from one stage to the next: a local of a
* known type, or (before the first read) the source slot itself. */
type Flow = { kind: "value"; id: string; type: IrType } | { kind: "slot" };
/** What reaches a stage: a value, the source slot not yet read, or a
* snapshot of the slot read before an earlier callback that ignored it
* (arraySlotSnapshot). */
type Flow =
| { kind: "value"; id: string; type: IrType }
| { kind: "slot" }
| { kind: "snapshot"; ids: SlotSnapshot };
interface Plan {
chain: Chain;
@@ -1595,13 +1610,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,11 +1653,62 @@ 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 [];
};
/** Bring `flow` to the callback of `stage`, whose element parameter is
* at `at`. A slot is read for a callback that reads it. For one that
* ignores it, the slot of an array that can hold elements outside its
* element type is read as stored before the callback runs (a snapshot,
* as JavaScript reads the element first), so such an element passes the
* callback as it is; other arrays read the slot as usual. A snapshot
* reaching a callback that reads it becomes its value, and an element
* outside the element type takes the ordinary read's checked
* conversion there. */
const reach = (
stage: Stage,
at: number,
flow: Flow,
rest: (flow: Flow) => IrStmt[],
): IrStmt[] => {
const reads = stage.info.arity > at && !unreadSlot(stage, at);
if (flow.kind === "slot") {
if (reads || !elementMayBeForeign(this.lowerer, arrT.elem))
return read(stage, (value) => rest(value));
const ids: SlotSnapshot = {
isForeign: this.local("sf", BOOL),
value: this.local("sv", arrayValueType(this.lowerer, arrT.elem)),
foreign: this.local("sd", DYN),
};
return [
...arraySlotSnapshot(this.lowerer, a, i, arrT.elem, ids, loc),
...rest({ kind: "snapshot", ids }),
];
}
if (flow.kind === "snapshot" && reads) {
const valueT = arrayValueType(this.lowerer, arrT.elem);
const u = this.local("u", valueT);
return [
{
kind: "varDecl",
localId: u,
init: arraySnapshotValue(this.lowerer, arrT.elem, flow.ids, valueT, (v) => v, loc),
loc,
},
...rest({ kind: "value", id: u, type: valueT }),
];
}
return rest(flow);
};
/** Turn the source slot into a value for the callback of `stage`. */
const read = (stage: Stage, rest: (flow: Flow & { kind: "value" }) => IrStmt[]): IrStmt[] => {
if (stage.info.present) {
@@ -1690,11 +1764,12 @@ class FusedLoop {
};
/** The statements that run one value through stages k.. and the terminal. */
const visit = (k: number, flow: Flow): IrStmt[] => {
const visit = (k: number, flow: Flow, reached = false): IrStmt[] => {
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));
if (flow.kind !== "value" && !reached)
return reach(stage, 0, flow, (next) => visit(k, next, true));
const called = deliver(stage, k, [], flow);
if (!called) return fail();
const { pre, value: result } = called;
@@ -1791,11 +1866,23 @@ class FusedLoop {
}
};
/** Append a snapshot of the source slot to the collected result: a
* filter whose callback ignored the element keeps it as it was stored
* (arraySnapshotAppend). */
const copySlot = (flow: Flow & { kind: "snapshot" }): IrStmt[] => {
const outT = resultT as IrType & { kind: "array" };
if (holes || !typeEquals(outT.elem, arrT.elem)) return fail();
return [
arraySnapshotAppend(this.lowerer, varRef(out!, resultT, loc), arrT.elem, flow.ids, loc),
];
};
/** The terminal's action for one value. */
const finish = (flow: Flow): IrStmt[] => {
const finish = (flow: Flow, reached = false): IrStmt[] => {
if (terminal.kind === "length") return [increment(count!, loc)];
if (terminal.kind === "collect") {
if (flow.kind !== "value") return fail();
if (flow.kind === "snapshot") return copySlot(flow);
if (flow.kind === "slot")
return read(chain.stages[chain.stages.length - 1]!, (value) => finish(value));
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 +1903,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 !== "value" && !reached)
return reach(stage, lead, flow, (next) => finish(next, true));
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,118 @@
// 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));
// A callback that ignores its element still receives the element as it was
// before the callback ran: filter keeps it, and find returns it, even when
// the callback overwrites or removes the slot.
const changing: number[] = [1, 2, 3];
const changingView: unknown[] = changing;
changingView.push("x");
let at = 0;
console.log(
changing.filter(() => {
changingView[at] = at === 1 ? "w" : 100 + at;
at++;
return true;
}),
changing,
);
const found: number[] = [1, 2];
(found as unknown[]).push("y");
let step = 0;
console.log(
found.find(() => {
found[step] = 50;
step++;
return step === 2;
}),
found,
);
const shrinking: number[] = [4, 5];
(shrinking as unknown[]).push("z", 6);
console.log(
shrinking.findLast(() => {
shrinking.length = 1;
return true;
}),
shrinking.length,
);
const cut: number[] = [7, 8, 9];
(cut as unknown[]).push("q");
console.log(
cut.filter((_v, i) => {
if (i === 1) cut.length = 1;
return true;
}),
cut.length,
);
// Arrays proven to hold no undefined keep their elements in filter results
// when the predicate ignores them.
const dense = [1, 2, 3, 4];
let calls = 0;
console.log(
dense.filter((_v, i) => {
calls++;
return i % 2 === 0;
}),
calls,
dense.filter((_v, i) => i > 1),
);
@@ -0,0 +1 @@
{ "plain": 5089, "san": 8118 }