mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-08 08:44:37 +08:00
Skip the element read for array callbacks that never use it (#2286)
This commit is contained in:
@@ -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(
|
||||
|
||||
+6
@@ -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 }
|
||||
Reference in New Issue
Block a user