Files
scriptc/packages/compiler/src/frontend/lowering/containers/array-methods.ts
T

2415 lines
81 KiB
TypeScript

import { keyedHelperName } from "../stable-names.js";
import { liftedFreshSpread } from "../coercions/fresh-spread.js";
import { lowerArrayIteratorValue } from "../iterator-adapters.js";
import { identityPreservingWidening } from "../coercions/identity.js";
import { arrayConcatHelper } from "./array-concat.js";
import { boolLit, countedFor, numLit, strLit, varRef } from "../../../ir/build.js";
import { InternalCompilerError } from "../../../errors.js";
import * as ts from "../../ts7/adapter.js";
import type { Lowerer } from "../lowerer.js";
import {
BOOL,
DYN,
F64,
type IrExpr,
type IrStmt,
type IrType,
STRING,
type SrcLoc,
UNDEFINED_T,
arrayOf,
isRefCounted,
isUnitType,
typeEquals,
} from "../../../ir/ir.js";
import { ARRAY_METHODS } from "../surfaces.js";
import { tryLowerExpression } from "../expressions/try-lower-expression.js";
import { isSafeToRepeat } from "../expressions/evaluation-safety.js";
import { isJsSourceFile, locOf } from "../../program.js";
import { lowerDynDispatchMethodCall } from "../lower-calls.js";
import {
arrayIndexPresent,
arrayValueRead,
arrayValueStore,
arrayValueType,
} from "../array-values.js";
import { typeKey } from "../../type-mapper.js";
import {
defaultAfterUndefined,
lowerPositionArgument,
lowerStaticallyUndefinedArgument,
positionNumber,
} from "../optional-arguments.js";
import { lowerArrayCopyWithin, lowerArrayFill } from "../array-indexed-mutation.js";
import {
lowerArrayUnionHofCall,
lowerArraySortCall,
lowerArrayHofCall,
lowerArrayFindLikeCall,
lowerArrayFlatMapCall,
lowerArrayReduceCall,
lowerTuplePredicateCall,
} from "./array-callbacks.js";
import {
lowerArraySpreadItems,
lowerArrayValueItems,
widenArraySpread,
} from "./array-construction.js";
import { arrayLengthDeclaration } from "./array-iteration.js";
import {
lowerArrayCallback,
callbackArrayElement,
requireProducedArrayElement,
} from "./callback-arguments.js";
/** `a.push(v, ...)` where some argument is a runtime-optional read of the
* element type (an unchecked array read or a hole-observing iteration
* binding). Every argument evaluates before the first append; a present
* value takes the ordinary push (and its inline dense fast path), while a
* missing one appends a present-undefined slot at the current length,
* exactly what Node stores. The result is the final length. Pure
* receivers and arguments are repeated instead of copied into temporaries:
* appends run no user code, so repeated reads observe the same values. */
function lowerOptionalArrayPush(
lowerer: Lowerer,
receiver: IrExpr,
values: IrExpr[],
elem: IrType,
loc: SrcLoc,
): IrExpr {
const stmts: IrStmt[] = [];
const pure = isSafeToRepeat(receiver) && values.every(isSafeToRepeat);
const stable = (value: IrExpr, name: string): IrExpr => {
if (pure) return value;
const temp = lowerer.declareHiddenLocal(name, value.type);
stmts.push({ kind: "varDecl", localId: temp.id, init: value, loc });
return varRef(temp.id, value.type, loc);
};
const target = stable(receiver, "%pushTarget");
const args = values.map((value) => stable(value, "%pushValue"));
const length = (): IrExpr => ({
kind: "arrIntrinsic",
method: "length",
receiver: target,
args: [],
type: F64,
loc,
});
const push = (value: IrExpr): IrStmt => ({
kind: "exprStmt",
expr: { kind: "arrIntrinsic", method: "push", receiver: target, args: [value], type: F64, loc },
loc,
});
for (const value of args) {
if (value.type.kind === "union" && lowerer.runtimeOptionalWidening(value.type, elem) !== null) {
const unionId = value.type.unionId;
stmts.push({
kind: "if",
cond: {
kind: "unionIsTag",
unionId,
tag: lowerer.armTag(unionId, UNDEFINED_T),
negated: false,
value,
type: BOOL,
loc,
},
then: [{ kind: "arraySetUndefined", arr: target, index: length(), loc }],
else_: [push(lowerer.coerceToExpected(value, elem))],
loc,
});
} else {
stmts.push(push(lowerer.coerceToExpected(value, elem)));
}
}
return { kind: "seqExpr", stmts, result: length(), type: F64, loc };
}
/** `a.splice(start, count, v)` where v may hold undefined at run time.
* Receiver, start, count, and v evaluate once, in order; a present v is
* inserted as a plain element literal (which a statement-position splice
* then inserts without any temporary array), a missing one through the
* present-undefined item array. Both arms return the removed elements. */
function lowerOptionalSpliceItem(
lowerer: Lowerer,
receiver: IrExpr,
positions: IrExpr[],
value: IrExpr,
elem: IrType,
arrType: IrType & { kind: "array" },
loc: SrcLoc,
): IrExpr {
const stmts: IrStmt[] = [];
const repeatable = (e: IrExpr): boolean => e.kind === "numLit" || isSafeToRepeat(e);
const pure = repeatable(receiver) && positions.every(repeatable) && repeatable(value);
const stable = (e: IrExpr, name: string): IrExpr => {
if (pure) return e;
const temp = lowerer.declareHiddenLocal(name, e.type);
stmts.push({ kind: "varDecl", localId: temp.id, init: e, loc });
return varRef(temp.id, e.type, loc);
};
const target = stable(receiver, "%spliceTarget");
const bounds = positions.map((position) => stable(position, "%splicePosition"));
const item = stable(value, "%spliceValue");
if (item.type.kind !== "union")
throw new InternalCompilerError("runtime-optional splice item is not a union");
const splice = (items: IrExpr): IrExpr => ({
kind: "arrIntrinsic",
method: "spliceInsert",
receiver: target,
args: [...bounds, items],
type: arrType,
loc,
});
return {
kind: "seqExpr",
stmts,
result: {
kind: "ternary",
cond: {
kind: "unionIsTag",
unionId: item.type.unionId,
tag: lowerer.armTag(item.type.unionId, UNDEFINED_T),
negated: false,
value: item,
type: BOOL,
loc,
},
then: splice(lowerArrayValueItems(lowerer, [item], elem, arrType, loc)),
else_: splice({
kind: "arrayLit",
elems: [lowerer.coerceToExpected(item, elem)],
type: arrType,
loc,
}),
type: arrType,
loc,
},
type: arrType,
loc,
};
}
function primitivePositionType(lowerer: Lowerer, type: IrType): boolean {
if (type.kind === "union") {
return (
lowerer.unions.get(type.unionId)?.arms.every((arm) => primitivePositionType(lowerer, arm)) ??
false
);
}
return (
type.kind === "f64" ||
type.kind === "string" ||
type.kind === "bool" ||
type.kind === "nullT" ||
type.kind === "undefinedT"
);
}
function lowerArrayPosition(
lowerer: Lowerer,
node: ts.Expression | undefined,
defaultValue: IrExpr,
subject: string,
): IrExpr {
if (!node) return defaultValue;
if (ts.isSpreadElement(node)) lowerer.noLowering(`${subject} with a spread argument`, node);
const value = lowerPositionArgument(lowerer, node, defaultValue);
if (!primitivePositionType(lowerer, value.type)) {
lowerer.noLowering(`${subject} of '${lowerer.fmt(value.type)}' values`, node);
}
const loc = locOf(node);
const local = lowerer.declareHiddenLocal("%arrayPosition", value.type);
const ref = varRef(local.id, value.type, loc);
return {
kind: "seqExpr",
stmts: [{ kind: "varDecl", localId: local.id, init: value, loc }],
result: positionNumber(lowerer, ref, defaultValue, node, subject),
type: F64,
loc,
};
}
function lowerArrayJoinSeparator(
lowerer: Lowerer,
node: ts.Expression | undefined,
loc: SrcLoc,
): IrExpr {
const comma = strLit(",", loc);
if (!node) return comma;
const undefinedValue = lowerStaticallyUndefinedArgument(lowerer, node);
if (undefinedValue) return defaultAfterUndefined(undefinedValue, comma);
const value = lowerer.lowerExpr(node);
if (value.type.kind === "nullT") return defaultAfterUndefined(value, strLit("null", loc));
if (value.type.kind === "union") {
const undefinedTag = lowerer.armTag(value.type.unionId, UNDEFINED_T);
if (undefinedTag >= 0) {
const local = lowerer.declareHiddenLocal("%joinSeparator", value.type);
const ref = varRef(local.id, value.type, loc);
return {
kind: "seqExpr",
stmts: [{ kind: "varDecl", localId: local.id, init: value, loc }],
result: {
kind: "ternary",
cond: {
kind: "unionIsTag",
unionId: value.type.unionId,
tag: undefinedTag,
value: ref,
negated: false,
type: BOOL,
loc,
},
then: comma,
else_: lowerer.ensureString(ref, node),
type: STRING,
loc,
},
type: STRING,
loc,
};
}
}
if (value.type.kind === "dyn") {
lowerer.noLowering(".join separator with a runtime-dependent undefined value", node);
}
return lowerer.ensureString(value, node);
}
/** Ambient array method calls. `push`/`unshift`/`pop`/`reverse`/
* `indexOf`/`includes`/`join`
* lower to arrIntrinsic; the HOF family — `map`/`filter`/`forEach`/
* `find`/`some`/`every`/`flatMap`/`reduce`/`reduceRight` — desugars to a
* direct call of a synthetic loop function (see lowerArrayHofCall and
* friends). Null when this isn't an ambient array method call. tsc has
* already checked arity and argument types against ambient/scriptc.d.ts. */
/** Methods with native lowerings for arrays of checked-dynamic elements. */
const DYN_ELEMENT_METHODS = new Set([
"copyWithin",
"every",
"fill",
"filter",
"findIndex",
"findLastIndex",
"flatMap",
"forEach",
"join",
"map",
"pop",
"push",
"reduce",
"reduceRight",
"reverse",
"shift",
"slice",
"some",
"sort",
"splice",
"toReversed",
"toSorted",
"toSpliced",
"unshift",
"with",
]);
export function lowerArrayMethodCall(
lowerer: Lowerer,
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
): IrExpr | null {
if (lowerer.chainBlocked(access, call)) return null;
const name = access.name.text;
const iteratorMethod =
(name === "values" || name === "keys" || name === "entries") &&
call.arguments.nodes.length === 0;
if (
!ARRAY_METHODS.has(name) &&
name !== "sort" &&
name !== "shift" &&
name !== "splice" &&
!iteratorMethod
)
return null;
let receiverIr = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
const checkerReceiver = lowerer.checker.getTypeAtLocation(access.expression);
const implicitArrayReceiver =
receiverIr?.kind === "array" &&
(checkerReceiver.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown) ||
lowerer.checkerAnyArrayType(checkerReceiver));
// A checker-`any[]` receiver (the readonly-array Array.isArray quirk)
// whose VALUE lowers to a real static array (maybeNarrow's isArray
// bridge): ride the ordinary tables on the lowered element type — the
// dyn-receiver string precedent (re-lowering is pure IR construction).
// A checker-untyped receiver has no stdlib-declared member symbol, so
// the isStdlibMember gate is skipped for probed receivers (nothing
// user-declared can shadow a method on a value the checker calls any).
let probedUntyped = false;
if (implicitArrayReceiver) probedUntyped = true;
if (
(receiverIr === null ||
receiverIr.kind === "dyn" ||
(receiverIr.kind === "array" && receiverIr.elem.kind === "dyn")) &&
(lowerer.checkerAnyArray(access.expression) ||
// A checker-`any` CHAIN whose value lowers to a real static array
// (`context.stack.split('\n').slice(2)` — the dyn-receiver string
// machinery answered string[]): same rule, any-typed spelling.
((lowerer.typeOf(access.expression).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !==
0 &&
isJsSourceFile(ts.getSourceFileOfNode(access))))
) {
const probe = lowerer.lowerExpr(access.expression);
if (probe.type.kind === "array") {
receiverIr = probe.type;
probedUntyped = true;
} else if (
probe.type.kind === "dyn" &&
!isJsSourceFile(ts.getSourceFileOfNode(access)) &&
lowerer.isStdlibMember(access)
) {
// An `unknown[]` (or `any[]`) array stored checked-dynamic — a
// generic body instantiated at `unknown`, a spread of unknown
// elements: the method runs on the runtime array (dynInvoke, JS
// exact), and a typed result is checked like any member read.
const dispatched = lowerDynDispatchMethodCall(lowerer, call, access, probe, true);
if (!dispatched) return null;
const result = lowerer.mapTypeOf(lowerer.typeOf(call));
return result && result.kind !== "void" && result.kind !== "dyn"
? lowerer.coerceInto(call, dispatched, result)
: dispatched;
}
}
if (receiverIr?.kind === "union") {
const unionHof = lowerArrayUnionHofCall(lowerer, call, access, receiverIr);
if (unionHof) return unionHof;
}
if (receiverIr?.kind !== "array") return null;
if (!probedUntyped && !lowerer.isStdlibMember(access)) return null;
let elem = receiverIr.elem;
const loc = locOf(call);
if (iteratorMethod) {
const type = lowerer.mapTypeOf(lowerer.typeOf(call));
const receiver = lowerer.lowerExpr(access.expression);
// Only a static array resolves to a generator here; other lowered
// values continue to their dedicated handling below.
if (receiver.type.kind === "array") {
const value = type ? lowerArrayIteratorValue(lowerer, receiver, name, type, loc) : null;
if (value) return value;
lowerer.unsupported("SC1090", call, `'${name}()' iterators over this array element type`);
}
}
{
const receiver = lowerer.lowerExpr(access.expression);
if (receiver.type.kind === "union") {
const arms = lowerer.unions.get(receiver.type.unionId)?.arms;
const present = arms?.find((arm): boolean => arm.kind === "array");
if (
present?.kind === "array" &&
arms?.every((arm) => typeEquals(arm, present) || isUnitType(arm))
) {
// A predicate can narrow the element type without changing the
// original array layout. Extract that stored array before choosing
// a helper, then adapt callback arguments at their actual ABI.
const array =
lowerer.runtimeOptionalPropertyReceiver(access.expression, receiver, present, name) ??
lowerer.coerceInto(access.expression, receiver, present);
return lowerer.withExpressionOverride(access.expression, array, () =>
lowerArrayMethodCall(lowerer, call, access),
);
}
}
if (receiver.type.kind === "record" && lowerer.shapes.get(receiver.type.shapeId)?.tuple) {
return lowerTupleReadMethodCall(lowerer, call, access, receiver);
}
// An array-mapped CHECKER type whose VALUE lives in the checked-dynamic tree
// (`Object.keys(dynObj).sort()` — the key-walk answers a dyn array):
// the record family's array sibling. Methods with a runtime
// receiver-kind dispatch ride dynInvoke — the real method runs on
// the dyn array, JS-exact; the rest keep an honest fence instead of
// handing a static array intrinsic a dyn receiver (the validator
// ICE). Consumers validate the dyn result where a static type is
// required (dynCheck — the member-read discipline).
if (receiver.type.kind === "dyn") {
const dispatched = lowerDynDispatchMethodCall(lowerer, call, access, receiver, true);
if (dispatched) return dispatched;
lowerer.noLowering(
`.${name} on an array value held in a checked-dynamic binding`,
call,
"assign it to an array-typed binding first (the validated extraction), then call the method",
);
}
// Flow predicates and evolving-any analysis can refine the checker
// element type without changing the array's storage. Helpers and
// callback array arguments must retain the original layout and
// identity; element conversions happen at the callback boundary.
if (receiver.type.kind === "array" && !typeEquals(receiverIr, receiver.type)) {
receiverIr = receiver.type;
elem = receiver.type.elem;
}
// Arrays of checked-dynamic elements (arrays of class-backed protocol
// values) lower natively for the methods below. Other methods run on the
// array's live checked-dynamic view, which shares its storage.
if (elem.kind === "dyn" && !DYN_ELEMENT_METHODS.has(name)) {
const view = lowerer.coerceInto(access.expression, receiver, DYN);
const dispatched = lowerDynDispatchMethodCall(lowerer, call, access, view, true);
if (!dispatched) return null;
const result = lowerer.mapTypeOf(lowerer.typeOf(call));
return result && result.kind !== "void" && result.kind !== "dyn"
? lowerer.coerceInto(call, dispatched, result)
: dispatched;
}
}
if (name === "sort" || name === "toSorted") {
return lowerArraySortCall(lowerer, call, access, elem, receiverIr);
}
if (name === "toReversed") {
if (call.arguments.nodes.length !== 0) {
lowerer.noLowering(`.toReversed with ${call.arguments.nodes.length} arguments`, call);
}
return {
kind: "arrIntrinsic",
method: "toReversed",
receiver: lowerer.lowerExpr(access.expression),
args: [],
type: receiverIr,
loc,
};
}
if (name === "reverse") {
if (call.arguments.nodes.length !== 0) {
lowerer.noLowering(`.reverse with ${call.arguments.nodes.length} arguments`, call);
}
return {
kind: "arrIntrinsic",
method: "reverse",
receiver: lowerer.lowerExpr(access.expression),
args: [],
type: receiverIr,
loc,
};
}
if (name === "fill") {
if (call.arguments.nodes.length > 3 || call.arguments.nodes.some(ts.isSpreadElement)) {
lowerer.noLowering(`.fill with ${call.arguments.nodes.length} arguments`, call);
}
const value = call.arguments.nodes[0] ? lowerer.lowerExpr(call.arguments.nodes[0]) : null;
if (
value &&
value.type.kind === "dyn" &&
isRefCounted(elem) &&
elem.kind !== "string" &&
!typeEquals(value.type, elem)
) {
lowerer.noLowering(
".fill of a checked-dynamic reference into a static array",
call.arguments.nodes[0]!,
);
}
const writeUndefined =
value === null || value.type.kind === "undefinedT" || value.type.kind === "void";
return lowerArrayFill(
lowerer,
lowerer.lowerExpr(access.expression),
value,
writeUndefined,
lowerArrayPosition(lowerer, call.arguments.nodes[1], numLit(0, loc), "array fill start"),
lowerArrayPosition(lowerer, call.arguments.nodes[2], numLit(Infinity, loc), "array fill end"),
receiverIr,
loc,
);
}
if (name === "copyWithin") {
if (call.arguments.nodes.length > 3 || call.arguments.nodes.some(ts.isSpreadElement)) {
lowerer.noLowering(`.copyWithin with ${call.arguments.nodes.length} arguments`, call);
}
return lowerArrayCopyWithin(
lowerer.lowerExpr(access.expression),
lowerArrayPosition(
lowerer,
call.arguments.nodes[0],
numLit(0, loc),
"array copyWithin target",
),
lowerArrayPosition(
lowerer,
call.arguments.nodes[1],
numLit(0, loc),
"array copyWithin start",
),
lowerArrayPosition(
lowerer,
call.arguments.nodes[2],
numLit(Infinity, loc),
"array copyWithin end",
),
receiverIr,
loc,
);
}
if (name === "with") {
if (call.arguments.nodes.length !== 2 || call.arguments.nodes.some(ts.isSpreadElement)) {
lowerer.noLowering(`.with with ${call.arguments.nodes.length} arguments`, call);
}
const receiver = lowerer.lowerExpr(access.expression);
const index = lowerPositionArgument(lowerer, call.arguments.nodes[0]!, numLit(0, loc));
if (!primitivePositionType(lowerer, index.type)) {
lowerer.noLowering(
`.with index of '${lowerer.fmt(index.type)}' values`,
call.arguments.nodes[0]!,
);
}
const value = lowerer.lowerExpr(call.arguments.nodes[1]!);
const receiverLocal = lowerer.declareHiddenLocal("%withRecv", receiverIr);
const indexLocal = lowerer.declareHiddenLocal("%withIndex", index.type);
const valueLocal = lowerer.declareHiddenLocal("%withValue", value.type);
const receiverRef = varRef(receiverLocal.id, receiverIr, loc);
const indexRef = varRef(indexLocal.id, index.type, loc);
const valueRef = varRef(valueLocal.id, value.type, loc);
const numericIndex = positionNumber(
lowerer,
indexRef,
numLit(0, loc),
call.arguments.nodes[0]!,
"array with index",
);
const stmts: IrStmt[] = [
{ kind: "varDecl", localId: receiverLocal.id, init: receiver, loc },
{ kind: "varDecl", localId: indexLocal.id, init: index, loc },
{ kind: "varDecl", localId: valueLocal.id, init: value, loc },
];
if (value.type.kind === "union" && lowerer.runtimeOptionalWidening(value.type, elem) !== null) {
// `.with()` validates its index and returns a fresh copy. Stabilize
// all three operands, then select the scalar replacement or the
// present-undefined copy without evaluating a read twice.
const unionId = value.type.unionId;
const undefTag = lowerer.armTag(unionId, UNDEFINED_T);
const condition: IrExpr = {
kind: "unionIsTag",
unionId,
tag: undefTag,
negated: false,
value: valueRef,
type: BOOL,
loc,
};
const missing: IrExpr = {
kind: "arrIntrinsic",
method: "withUndefined",
receiver: receiverRef,
args: [numericIndex],
type: receiverIr,
loc,
};
const present: IrExpr = {
kind: "arrIntrinsic",
method: "with",
receiver: receiverRef,
args: [numericIndex, lowerer.coerceToExpected(valueRef, elem)],
type: receiverIr,
loc,
};
return {
kind: "seqExpr",
stmts,
result: {
kind: "ternary",
cond: condition,
then: missing,
else_: present,
type: receiverIr,
loc,
},
type: receiverIr,
loc,
};
}
const coercedValue = lowerer.coerceInto(call.arguments.nodes[1]!, valueRef, elem);
return {
kind: "seqExpr",
stmts,
result: {
kind: "arrIntrinsic",
method: "with",
receiver: receiverRef,
args: [numericIndex, coercedValue],
type: receiverIr,
loc,
},
type: receiverIr,
loc,
};
}
if (name === "toSpliced") {
const receiver = lowerer.lowerExpr(access.expression);
const start = lowerArrayPosition(
lowerer,
call.arguments.nodes[0],
numLit(0, loc),
"array toSpliced start",
);
const deleteCount = lowerArrayPosition(
lowerer,
call.arguments.nodes[1],
numLit(call.arguments.nodes.length === 1 ? Infinity : 0, loc),
"array toSpliced deleteCount",
);
const itemNodes = call.arguments.nodes.slice(2);
const hasSpread = itemNodes.some(ts.isSpreadElement);
const itemProbes = hasSpread ? [] : itemNodes.map((arg) => tryLowerExpression(lowerer, arg));
const statefulItems = itemProbes.some(
(probe) => probe !== null && lowerer.runtimeOptionalWidening(probe.type, elem) !== null,
);
const items: IrExpr = hasSpread
? lowerArraySpreadItems(lowerer, itemNodes, elem, receiverIr, loc)
: statefulItems
? lowerArrayValueItems(
lowerer,
itemNodes.map((arg) => lowerer.lowerExpr(arg)),
elem,
receiverIr,
loc,
)
: {
kind: "arrayLit",
elems: itemNodes.map((arg) => lowerer.coerceInto(arg, lowerer.lowerExpr(arg), elem)),
type: receiverIr,
loc,
};
return {
kind: "arrIntrinsic",
method: "toSpliced",
receiver,
args: [start, deleteCount, items],
type: receiverIr,
loc,
};
}
if (name === "flat") return lowerArrayFlatCall(lowerer, call, access, receiverIr);
// The lib declares wider call forms than the lowered surface — the
// predicate/mapping HOFs take a thisArg. Each unlowered form is fenced
// per site (SC2020) instead of being truncated to the supported
// arguments. push/unshift lower every declared form (variadic, 0 args
// included — Node returns the unchanged length); reduce/reduceRight
// lower both declared forms (with and without an initial value).
const arity = {
push: [0, Number.MAX_SAFE_INTEGER],
unshift: [0, Number.MAX_SAFE_INTEGER],
pop: [0, 0],
indexOf: [1, 2],
lastIndexOf: [1, 2],
includes: [0, 2],
join: [0, 1],
concat: [0, Number.MAX_SAFE_INTEGER],
slice: [0, 2],
shift: [0, 0],
splice: [0, Number.MAX_SAFE_INTEGER],
at: [0, 1],
map: [1, 1],
filter: [1, 1],
forEach: [1, 1],
find: [1, 1],
findIndex: [1, 1],
some: [1, 1],
findLast: [1, 1],
findLastIndex: [1, 1],
every: [1, 1],
flatMap: [1, 1],
reduce: [1, 2],
reduceRight: [1, 2],
}[
name as
| "push"
| "unshift"
| "pop"
| "concat"
| "indexOf"
| "lastIndexOf"
| "includes"
| "join"
| "slice"
| "shift"
| "splice"
| "at"
| "map"
| "filter"
| "forEach"
| "find"
| "findIndex"
| "findLast"
| "findLastIndex"
| "some"
| "every"
| "flatMap"
| "reduce"
| "reduceRight"
];
if (call.arguments.nodes.length < arity[0]! || call.arguments.nodes.length > arity[1]!) {
const hint =
name === "map" ||
name === "filter" ||
name === "forEach" ||
name === "find" ||
name === "some" ||
name === "every" ||
name === "flatMap"
? "the thisArg parameter has no lowering — use an arrow function"
: undefined;
lowerer.noLowering(
`.${name} with ${call.arguments.nodes.length} argument${call.arguments.nodes.length === 1 ? "" : "s"}`,
call,
hint,
);
}
if (name === "push" || name === "unshift") {
let receiver = lowerer.lowerExpr(access.expression);
if (receiver.type.kind === "union" && lowerer.armTag(receiver.type.unionId, UNDEFINED_T) >= 0) {
const present = lowerer.stripUndefinedArm(receiver.type);
const helper =
present.kind === "array"
? lowerer.narrowedArmHelper(receiver.type.unionId, present, loc)
: null;
receiver = helper
? { kind: "call", callee: helper, args: [receiver], type: present, loc }
: lowerer.maybeNarrow(receiver, access.expression);
}
// `a.push(...src)` / `a.unshift(...src)`: copy src's elements in
// order (count snapshotted first, so the self-spread forms duplicate
// like JS) and return the new length.
const spreadArg =
call.arguments.nodes.length === 1 && ts.isSpreadElement(call.arguments.nodes[0]!)
? call.arguments.nodes[0]!
: null;
if ((name === "push" || name === "unshift") && spreadArg && ts.isSpreadElement(spreadArg)) {
let src = lowerer.lowerExpr(spreadArg.expression);
// `a.push(...someSet)` / `a.unshift(...someSet)`: a compatible Set drains first
// (setIntrinsic toArray — insertion order), then appends.
if (src.type.kind === "set" && identityPreservingWidening(lowerer, src.type.elem, elem)) {
src = {
kind: "setIntrinsic",
method: "toArray",
receiver: src,
args: [],
type: arrayOf(src.type.elem),
loc,
};
}
if (
src.type.kind === "array" &&
!typeEquals(src.type, receiverIr) &&
identityPreservingWidening(lowerer, src.type.elem, elem)
) {
const items = widenArraySpread(lowerer, src, elem, loc);
return {
kind: "arrIntrinsic",
method: name === "push" ? "pushSpread" : "unshiftSpread",
receiver,
args: [items],
type: F64,
loc,
};
}
if (!typeEquals(src.type, receiverIr)) {
// Fresh elements (an array literal) convert into the element layout.
src = liftedFreshSpread(lowerer, spreadArg.expression, src, receiverIr, loc) ?? src;
}
if (!typeEquals(src.type, receiverIr)) {
lowerer.unsupported(
"SC1090",
spreadArg,
`${name === "push" ? "pushing" : "unshifting"} a spread of '${lowerer.fmt(src.type)}' onto a '${lowerer.fmt(receiverIr)}' array (only a same-element-type array spreads)`,
);
}
return {
kind: "arrIntrinsic",
method: name === "push" ? "pushSpread" : "unshiftSpread",
receiver,
args: [src],
type: F64,
loc,
};
}
if (call.arguments.nodes.some(ts.isSpreadElement)) {
// Finish argument evaluation before mutating the receiver. Each
// spread snapshots its elements immediately, so a later argument
// can mutate that source without changing already-collected values.
const items = lowerArraySpreadItems(lowerer, call.arguments.nodes, elem, receiverIr, loc);
return {
kind: "arrIntrinsic",
method: name === "push" ? "pushSpread" : "unshiftSpread",
receiver,
args: [items],
type: F64,
loc,
};
}
const valueProbes = call.arguments.nodes.map((arg) => tryLowerExpression(lowerer, arg));
if (
valueProbes.some(
(probe) => probe !== null && lowerer.runtimeOptionalWidening(probe.type, elem) !== null,
)
) {
if (name === "push") {
return lowerOptionalArrayPush(
lowerer,
receiver,
call.arguments.nodes.map((arg) => lowerer.lowerExpr(arg)),
elem,
loc,
);
}
const items = lowerArrayValueItems(
lowerer,
call.arguments.nodes.map((arg) => lowerer.lowerExpr(arg)),
elem,
receiverIr,
loc,
);
return {
kind: "arrIntrinsic",
method: "unshiftSpread",
receiver,
args: [items],
type: F64,
loc,
};
}
// Inserted values flow into the element slot like an assignment would:
// union-element arrays wrap plain arm values (coerceInto is inert
// when the types already agree).
const args = call.arguments.nodes.map((a) =>
name === "push" || name === "unshift"
? lowerer.lowerExprExpecting(a, elem)
: lowerer.lowerExpr(a),
);
return {
kind: "arrIntrinsic",
method: name,
receiver,
args,
type: F64,
loc,
};
}
if (name === "pop") {
// An unused result needs no value at all: drop the last slot.
if (resultDiscarded(call)) return lowerDiscardedPop(lowerer, access.expression, loc);
// Widen a union payload through the normal array-read machinery
// before removing it. Reinterpreting the stored union's tag as the
// result union would confuse arms when undefined changes their order.
if (elem.kind === "union" && lowerer.armTag(elem.unionId, UNDEFINED_T) < 0) {
return lowerUnionArrayRemoval(lowerer, access.expression, elem, false, loc);
}
const receiver = lowerer.lowerExpr(access.expression);
return {
kind: "arrIntrinsic",
method: "pop",
receiver,
args: [],
type: arrayValueType(lowerer, elem),
loc,
};
}
if (name === "indexOf" || name === "lastIndexOf" || name === "includes") {
return lowerArraySearchCall(lowerer, call, access, name, elem);
}
if (name === "concat") {
// `a.concat(x, ys, ...)` — a fresh array of a's elements followed by
// each argument in order: JS spreads array arguments one level
// (IsArray, never deeper) and appends plain values. Each argument is
// either an ELEMENT (pushed) or a SAME-ELEMENT ARRAY (spread) —
// decided by its static type, which matches IsArray exactly here
// because the two kinds map differently. The one ambiguous corner —
// an array-of-arrays receiver given a bare inner array, where TS
// types it as an element but JS would SPREAD it — is fenced.
const receiver = lowerer.lowerExpr(access.expression);
const shape: (IrType | null)[] = [];
const args: IrExpr[] = [];
for (const argNode of call.arguments.nodes) {
if (ts.isSpreadElement(argNode)) {
lowerer.unsupported(
"SC1090",
argNode,
"spread arguments to concat (pass the array itself — concat already spreads array arguments)",
);
}
const argIr = lowerer.mapTypeOf(lowerer.typeOf(argNode));
const probedArg = probedUntyped ? tryLowerExpression(lowerer, argNode) : null;
const argArrayType =
probedArg?.type.kind === "array" ? probedArg.type : argIr?.kind === "array" ? argIr : null;
if (argArrayType !== null) {
if (elem.kind === "array" && typeEquals(argArrayType, elem)) {
// number[][].concat(inner: number[]) — TS says element, JS's
// IsArray says spread; no honest static answer exists.
lowerer.unsupported(
"SC1090",
argNode,
"concat of a bare inner array onto an array-of-arrays (JS would SPREAD it one level — wrap it: a.concat([inner]))",
);
}
// Declaration-backed JS methods can return checked values even
// when their public signature names a concrete array. Validate
// that boundary before spreading the represented elements.
let arg = lowerer.lowerExpr(argNode);
if (arg.type.kind === "dyn") arg = lowerer.coerceInto(argNode, arg, argArrayType);
if (arg.type.kind !== "array") lowerer.badType(argNode, lowerer.typeOf(argNode));
shape.push(arg.type.elem);
args.push(arg);
continue;
}
// An element value — union elements wrap exactly like a push.
shape.push(null);
args.push(lowerer.lowerExprExpecting(argNode, elem));
}
const helper = arrayConcatHelper(lowerer, elem, shape, call, loc);
return { kind: "call", callee: helper, args: [receiver, ...args], type: receiverIr, loc };
}
if (name === "slice") {
const receiver = lowerer.lowerExpr(access.expression);
const args = [
lowerArrayPosition(lowerer, call.arguments.nodes[0], numLit(0, loc), "array slice start"),
lowerArrayPosition(
lowerer,
call.arguments.nodes[1],
numLit(Infinity, loc),
"array slice end",
),
];
return { kind: "arrIntrinsic", method: "slice", receiver, args, type: receiverIr, loc };
}
if (name === "splice") {
const receiver = lowerer.lowerExpr(access.expression);
const args: IrExpr[] = [
lowerArrayPosition(lowerer, call.arguments.nodes[0], numLit(0, loc), "array splice start"),
lowerArrayPosition(
lowerer,
call.arguments.nodes[1],
numLit(call.arguments.nodes.length === 1 ? Infinity : 0, loc),
"array splice deleteCount",
),
];
if (call.arguments.nodes.length <= 2) {
return { kind: "arrIntrinsic", method: "splice", receiver, args, type: receiverIr, loc };
}
const itemNodes = call.arguments.nodes.slice(2);
const hasSpread = itemNodes.some(ts.isSpreadElement);
const itemProbes = hasSpread ? [] : itemNodes.map((arg) => tryLowerExpression(lowerer, arg));
const statefulItems = itemProbes.some(
(probe) => probe !== null && lowerer.runtimeOptionalWidening(probe.type, elem) !== null,
);
if (statefulItems && itemNodes.length === 1) {
return lowerOptionalSpliceItem(
lowerer,
receiver,
args,
lowerer.lowerExpr(itemNodes[0]!),
elem,
receiverIr,
loc,
);
}
const items: IrExpr = hasSpread
? lowerArraySpreadItems(lowerer, itemNodes, elem, receiverIr, loc)
: statefulItems
? lowerArrayValueItems(
lowerer,
itemNodes.map((arg) => lowerer.lowerExpr(arg)),
elem,
receiverIr,
loc,
)
: {
kind: "arrayLit",
elems: itemNodes.map((arg) => lowerer.coerceInto(arg, lowerer.lowerExpr(arg), elem)),
type: receiverIr,
loc,
};
return {
kind: "arrIntrinsic",
method: "spliceInsert",
receiver,
args: [...args, items],
type: receiverIr,
loc,
};
}
if (name === "shift") {
// Like pop, a union without undefined needs an explicit tag widening.
if (elem.kind === "union" && lowerer.armTag(elem.unionId, UNDEFINED_T) < 0) {
return lowerUnionArrayRemoval(lowerer, access.expression, elem, true, loc);
}
const receiver = lowerer.lowerExpr(access.expression);
return {
kind: "arrIntrinsic",
method: "shift",
receiver,
args: [],
type: arrayValueType(lowerer, elem),
loc,
};
}
if (name === "join") {
if (elem.kind === "dyn") {
const receiver = lowerer.coerceToExpected(lowerer.lowerExpr(access.expression), DYN);
const args = call.arguments.nodes.length
? [lowerer.lowerExprExpecting(call.arguments.nodes[0]!, DYN)]
: [];
return lowerer.coerceToExpected(
{
kind: "dynInvoke",
recv: receiver,
method: "join",
calleeName: ts.getTextOfNode(access),
args,
type: DYN,
loc,
},
STRING,
);
}
// The ambient declares join on every Array<T> (a per-element-type
// interface split isn't expressible there), so string-convertible
// elements are enforced here: nested arrays would need JS's recursive
// Array#toString. UNIONS of the convertible kinds join too —
// undefined/null arms print EMPTY per Array.prototype.join (the
// `.filter(Boolean)` idiom keeps its checker type `(string |
// undefined)[]`, and JS joins the units without an error) — via a per-union
// interned walker in the backend.
const joinableUnion =
elem.kind === "union" &&
(lowerer.unions
.get(elem.unionId)
?.arms.every(
(a) =>
a.kind === "f64" ||
a.kind === "string" ||
a.kind === "bool" ||
a.kind === "date" ||
isUnitType(a),
) ??
false);
if (
elem.kind !== "f64" &&
elem.kind !== "string" &&
elem.kind !== "bool" &&
elem.kind !== "date" &&
!joinableUnion
) {
lowerer.unsupported(
"SC1090",
call,
"'.join()' on arrays of this element type (number, string, boolean and Date arrays join — unions of those with undefined/null arms too, the units printing empty like JS)",
);
}
const receiver = lowerer.lowerExpr(access.expression);
const sep = lowerArrayJoinSeparator(lowerer, call.arguments.nodes[0], loc);
return { kind: "arrIntrinsic", method: "join", receiver, args: [sep], type: STRING, loc };
}
if (name === "map" || name === "filter" || name === "forEach") {
return lowerArrayHofCall(lowerer, call, access, name, elem);
}
if (
name === "find" ||
name === "findIndex" ||
name === "findLast" ||
name === "findLastIndex" ||
name === "some" ||
name === "every"
) {
return lowerArrayFindLikeCall(lowerer, call, access, name, elem, probedUntyped);
}
if (name === "at") return lowerArrayAtCall(lowerer, call, access, elem);
if (name === "flatMap") return lowerArrayFlatMapCall(lowerer, call, access, elem);
// reduce / reduceRight
return lowerArrayReduceCall(lowerer, call, access, name as "reduce" | "reduceRight", elem);
}
/** The call's value is never read: an expression statement, a `void`
* operand, the left of a comma, or a for-loop initializer or update. */
function resultDiscarded(call: ts.Expression): boolean {
let child: ts.Node = call;
let parent = call.parent;
while (parent && ts.isParenthesizedExpression(parent)) {
child = parent;
parent = parent.parent;
}
if (!parent) return false;
if (ts.isExpressionStatement(parent) || ts.isVoidExpression(parent)) return true;
if (ts.isBinaryExpression(parent) && parent.operatorToken.kind === ts.SyntaxKind.CommaToken)
return parent.left === child || resultDiscarded(parent);
return (
ts.isForStatement(parent) && (parent.initializer === child || parent.incrementor === child)
);
}
/** `xs.pop()` whose result is unused: truncate a non-empty array by one
* slot. Truncation releases the removed element and leaves the length of
* an empty array at zero, as pop does (pop's own `length = 0` store, a
* no-op unless the array is frozen). */
function lowerDiscardedPop(lowerer: Lowerer, source: ts.Expression, loc: SrcLoc): IrExpr {
const receiver = lowerer.lowerExpr(source);
const arr = lowerer.declareHiddenLocal("%popArray", receiver.type);
const arrRef = varRef(arr.id, arr.type, loc);
const length = lowerer.declareHiddenLocal("%popLength", F64);
const lengthRef = varRef(length.id, F64, loc);
return {
kind: "seqExpr",
stmts: [
{ kind: "varDecl", localId: arr.id, init: receiver, loc },
{
kind: "varDecl",
localId: length.id,
init: {
kind: "arrIntrinsic",
method: "length",
receiver: arrRef,
args: [],
type: F64,
loc,
},
loc,
},
{
kind: "if",
cond: { kind: "bin", op: ">", left: lengthRef, right: numLit(0, loc), type: BOOL, loc },
then: [
{
kind: "arraySetLength",
arr: arrRef,
length: {
kind: "bin",
op: "-",
left: lengthRef,
right: numLit(1, loc),
type: F64,
loc,
},
loc,
pop: true,
},
],
else_: [{ kind: "arraySetLength", arr: arrRef, length: numLit(0, loc), loc, pop: true }],
loc,
},
],
// The value is never read; any placeholder scalar keeps it unboxed.
result: numLit(0, loc),
type: F64,
loc,
};
}
/** Read before removal so it cannot destroy the result's owned payload.
* The source is evaluated once and empty/sparse arrays yield undefined using
* the same slot-state checks as an indexed read. */
function lowerUnionArrayRemoval(
lowerer: Lowerer,
source: ts.Expression,
elem: IrType,
first: boolean,
loc: SrcLoc,
): IrExpr {
const receiver = lowerer.lowerExpr(source);
const arr = lowerer.declareHiddenLocal("%removeArray", receiver.type);
const arrRef = varRef(arr.id, arr.type, loc);
const index = lowerer.declareHiddenLocal("%removeIndex", F64);
const indexRef = varRef(index.id, F64, loc);
const value = arrayValueRead(lowerer, arrRef, indexRef, elem, loc);
const result = lowerer.declareHiddenLocal("%removedValue", value.type);
return {
kind: "seqExpr",
stmts: [
{ kind: "varDecl", localId: arr.id, init: receiver, loc },
{
kind: "varDecl",
localId: index.id,
init: first
? numLit(0, loc)
: {
kind: "bin",
op: "-",
left: {
kind: "arrIntrinsic",
method: "length",
receiver: arrRef,
args: [],
type: F64,
loc,
},
right: numLit(1, loc),
type: F64,
loc,
},
loc,
},
{ kind: "varDecl", localId: result.id, init: value, loc },
first
? {
kind: "exprStmt",
expr: {
kind: "arrIntrinsic",
method: "splice",
receiver: arrRef,
args: [indexRef, numLit(1, loc)],
type: arr.type,
loc,
},
loc,
}
: {
// Removing the last slot is a truncation: no removed-element
// array, and an empty array keeps length zero.
kind: "if",
cond: {
kind: "bin",
op: ">=",
left: indexRef,
right: numLit(0, loc),
type: BOOL,
loc,
},
then: [{ kind: "arraySetLength", arr: arrRef, length: indexRef, loc, pop: true }],
// Pop's `length = 0` store on an empty array (a no-op unless the
// array is frozen).
else_: [
{ kind: "arraySetLength", arr: arrRef, length: numLit(0, loc), loc, pop: true },
],
loc,
},
],
result: varRef(result.id, result.type, loc),
type: result.type,
loc,
};
}
function literalFlatDepth(value: IrExpr): number | null {
if (value.kind === "numLit") return value.value;
if (value.kind === "strLit") return Number(value.value);
if (value.kind === "boolLit") return value.value ? 1 : 0;
if (value.kind === "unitLit") return value.unit === "undefined" ? 1 : 0;
if (value.kind === "unary" && value.op === "-") {
const operand = literalFlatDepth(value.operand);
return operand === null ? null : -operand;
}
return null;
}
function lowerArrayFlatCall(
lowerer: Lowerer,
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
receiverType: IrType & { kind: "array" },
): IrExpr {
const loc = locOf(call);
if (call.arguments.nodes.length > 1 || call.arguments.nodes.some(ts.isSpreadElement)) {
lowerer.noLowering(`.flat with ${call.arguments.nodes.length} arguments`, call);
}
const elemCouldNest = (elem: IrType): boolean =>
elem.kind === "array" ||
elem.kind === "dyn" ||
(elem.kind === "union" &&
(lowerer.unions
.get(elem.unionId)
?.arms.some((arm) => arm.kind === "array" || arm.kind === "dyn") ??
true));
if (!elemCouldNest(receiverType.elem) && call.arguments.nodes[0]) {
const receiver = lowerer.lowerExpr(access.expression);
const depth = lowerArrayPosition(
lowerer,
call.arguments.nodes[0],
numLit(1, loc),
"array flat depth",
);
const local = lowerer.declareHiddenLocal("%flatReceiver", receiverType);
const ref = varRef(local.id, receiverType, loc);
return {
kind: "seqExpr",
stmts: [
{ kind: "varDecl", localId: local.id, init: receiver, loc },
{ kind: "exprStmt", expr: depth, loc },
],
result: {
kind: "arrIntrinsic",
method: "flatCopy",
receiver: ref,
args: [{ kind: "arrayLit", elems: [], type: receiverType, loc }],
type: receiverType,
loc,
},
type: receiverType,
loc,
};
}
const depth = call.arguments.nodes[0]
? literalFlatDepth(lowerer.lowerExpr(call.arguments.nodes[0]))
: 1;
if (depth === null) lowerer.noLowering(".flat with a nonconstant depth", call);
let remaining = Number.isNaN(depth) || depth <= 0 ? 0 : Math.trunc(depth);
let result: IrExpr = lowerer.lowerExpr(access.expression);
let currentType = receiverType;
let flattened = false;
while (remaining > 0) {
const elem = currentType.elem;
if (elem.kind !== "array" && elemCouldNest(elem)) {
lowerer.noLowering(".flat over elements with runtime-dependent array shape", call);
}
if (elem.kind !== "array") break;
result = {
kind: "arrIntrinsic",
method: "flatOne",
receiver: result,
args: [{ kind: "arrayLit", elems: [], type: elem, loc }],
type: elem,
loc,
};
currentType = elem;
remaining--;
flattened = true;
}
if (!flattened) {
result = {
kind: "arrIntrinsic",
method: "flatCopy",
receiver: result,
args: [{ kind: "arrayLit", elems: [], type: currentType, loc }],
type: currentType,
loc,
};
}
return result;
}
/** `indexOf` and `includes` normally use the runtime's typed search helpers,
* but an indexed array read is a `T | undefined` value. Keep that union
* through a small lowered loop so the needle's missing state is observable:
* indexOf and lastIndexOf skip holes, while includes treats holes as
* undefined. The same loop also gives union-element arrays their value
* equality rather than comparing the compiler's union boxes by pointer. */
function lowerArraySearchCall(
lowerer: Lowerer,
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
method: "indexOf" | "lastIndexOf" | "includes",
elem: IrType,
): IrExpr {
const loc = locOf(call);
const receiver = lowerer.lowerExpr(access.expression);
if (call.arguments.nodes.some(ts.isSpreadElement))
lowerer.noLowering(`.${method} with spread arguments`, call);
const needle: IrExpr = call.arguments.nodes[0]
? lowerer.lowerExpr(call.arguments.nodes[0])
: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc };
const fromIndex: IrExpr = call.arguments.nodes[1]
? lowerPositionArgument(lowerer, call.arguments.nodes[1], numLit(0, loc))
: method === "lastIndexOf"
? { kind: "bin", op: "/", left: numLit(1, loc), right: numLit(0, loc), type: F64, loc }
: numLit(0, loc);
return lowerArraySearchValues(lowerer, call, method, elem, receiver, needle, fromIndex);
}
function lowerArraySearchValues(
lowerer: Lowerer,
call: ts.CallExpression,
method: "indexOf" | "lastIndexOf" | "includes",
elem: IrType,
receiver: IrExpr,
needle: IrExpr,
fromIndex: IrExpr,
): IrExpr {
const loc = locOf(call);
const argNode = call.arguments.nodes[0] ?? call;
// A derived CLASS VALUE against a base-classval element widens (the same
// pointer — identity search is exact); the coercion path owns the ABI gate.
if (
needle.type.kind === "classval" &&
elem.kind === "classval" &&
!typeEquals(needle.type, elem)
) {
needle = lowerer.coerceInto(argNode, needle, elem);
}
// Preserve the existing fast path for an ordinary, non-union needle. Its
// runtime implementation already has exact primitive/reference equality.
if (
method !== "lastIndexOf" &&
call.arguments.nodes.length === 1 &&
elem.kind !== "union" &&
elem.kind !== "bigint" &&
typeEquals(needle.type, elem)
) {
return {
kind: "arrIntrinsic",
method,
receiver,
args: [needle],
type: method === "includes" ? BOOL : F64,
loc,
};
}
let valueT = arrayValueType(lowerer, elem);
// Checked-value elements have no separate undefined union arm. Keep
// their runtime path; typed arrays take the state-aware helper below.
if (valueT.kind !== "union") {
if (method === "lastIndexOf")
lowerer.noLowering(".lastIndexOf on an array of checked values", call);
if (call.arguments.nodes.length > 1)
lowerer.noLowering(`.${method} with fromIndex on '${lowerer.fmt(elem)}' elements`, call);
if (!typeEquals(needle.type, elem)) lowerer.badType(argNode, lowerer.typeOf(argNode));
return {
kind: "arrIntrinsic",
method,
receiver,
args: [needle],
type: method === "includes" ? BOOL : F64,
loc,
};
}
// Search uses strict equality, never conversion to the array's element
// type. Retain primitive needles of a different kind in the comparison
// union so a nonmatching kind returns a miss instead of throwing.
const needleArms =
needle.type.kind === "union" ? lowerer.unions.get(needle.type.unionId)!.arms : [needle.type];
const scalarNeedle = needleArms.every(
(arm) =>
arm.kind === "f64" ||
arm.kind === "string" ||
arm.kind === "bool" ||
arm.kind === "bigint" ||
arm.kind === "symbol" ||
isUnitType(arm),
);
const valueDef = lowerer.unions.get(valueT.unionId)!;
const valueArms = valueDef.arms;
if (scalarNeedle && !valueArms.some((arm) => arm.kind === "func" || arm.kind === "set")) {
const arms = [...valueArms];
for (const arm of needleArms)
if (!arms.some((existing) => typeEquals(existing, arm))) arms.push(arm);
if (arms.length !== valueArms.length)
valueT = { kind: "union", unionId: lowerer.unions.transform(valueDef, arms) };
}
const directNeedle =
method === "lastIndexOf" && elem.kind !== "union" && typeEquals(needle.type, elem);
const searchNeedle = directNeedle ? needle : lowerer.coerceInto(argNode, needle, valueT);
// Checked dynamic extraction can copy a composite. Its new identity cannot
// be compared to an array element as though it were the original value.
if (
method === "lastIndexOf" &&
searchNeedle.kind === "dynCheck" &&
(elem.kind === "record" || elem.kind === "array")
) {
lowerer.noLowering(".lastIndexOf with a checked dynamic reference needle", call);
}
if (!directNeedle && !typeEquals(searchNeedle.type, valueT))
lowerer.badType(argNode, lowerer.typeOf(argNode));
const helper = arraySearchHelper(
lowerer,
method,
elem,
valueT,
searchNeedle.type,
directNeedle,
fromIndex.type,
call.arguments.nodes[1] ?? call,
loc,
);
return {
kind: "call",
callee: helper,
args: [receiver, searchNeedle, fromIndex],
type: method === "includes" ? BOOL : F64,
loc,
};
}
/** SameValueZero over a static value union. `unionEq` supplies strict
* equality for every arm; the extra f64-arm test is the only difference for
* includes, where NaN equals NaN while +0 and -0 remain equal. */
function arraySearchEquality(
lowerer: Lowerer,
left: IrExpr,
right: IrExpr,
sameValueZero: boolean,
loc: SrcLoc,
): IrExpr {
if (
left.type.kind !== "union" ||
right.type.kind !== "union" ||
left.type.unionId !== right.type.unionId
) {
if (left.type.kind === "string" && right.type.kind === "string") {
return { kind: "strEq", negated: false, left, right, type: BOOL, loc };
}
if (left.type.kind === "bigint" && right.type.kind === "bigint") {
return {
kind: "bin",
op: "===",
left: { kind: "libCall", fn: "bigint.cmp", args: [left, right], type: F64, loc },
right: numLit(0, loc),
type: BOOL,
loc,
};
}
return { kind: "bin", op: "===", left, right, type: BOOL, loc };
}
const unionId = left.type.unionId;
const strict: IrExpr = {
kind: "unionEq",
unionId,
negated: false,
sameValue: false,
left,
right,
type: BOOL,
loc,
};
if (!sameValueZero) return strict;
const numberTag = lowerer.unions.get(unionId)?.arms.findIndex((arm) => arm.kind === "f64") ?? -1;
if (numberTag < 0) return strict;
const leftIsNumber: IrExpr = {
kind: "unionIsTag",
unionId,
tag: numberTag,
negated: false,
value: left,
type: BOOL,
loc,
};
const rightIsNumber: IrExpr = {
kind: "unionIsTag",
unionId,
tag: numberTag,
negated: false,
value: right,
type: BOOL,
loc,
};
const leftNumber: IrExpr = {
kind: "unionNarrow",
unionId,
tag: numberTag,
value: left,
type: F64,
loc,
};
const rightNumber: IrExpr = {
kind: "unionNarrow",
unionId,
tag: numberTag,
value: right,
type: F64,
loc,
};
const bothNaN: IrExpr = {
kind: "logical",
op: "&&",
left: {
kind: "logical",
op: "&&",
left: leftIsNumber,
right: rightIsNumber,
type: BOOL,
loc,
},
right: {
kind: "logical",
op: "&&",
left: { kind: "libCall", fn: "num.isNaN", args: [leftNumber], type: BOOL, loc },
right: { kind: "libCall", fn: "num.isNaN", args: [rightNumber], type: BOOL, loc },
type: BOOL,
loc,
},
type: BOOL,
loc,
};
return { kind: "logical", op: "||", left: strict, right: bothNaN, type: BOOL, loc };
}
function arraySearchHelper(
lowerer: Lowerer,
method: "indexOf" | "lastIndexOf" | "includes",
elem: IrType,
valueT: IrType & { kind: "union" },
needleT: IrType,
directNeedle: boolean,
fromT: IrType,
fromNode: ts.Expression,
loc: SrcLoc,
): string {
const key = `${method}:${typeKey(elem)}:${typeKey(valueT)}:${typeKey(needleT)}:${directNeedle}:${typeKey(fromT)}`;
const existing = lowerer.arrHofHelpers.get(key);
if (existing) return existing;
const name = keyedHelperName(lowerer, `%arr.${method}`, key);
lowerer.arrHofHelpers.set(key, name);
const arrT = arrayOf(elem);
const resultT = method === "includes" ? BOOL : F64;
const arrRef = varRef("a.0", arrT, loc);
const indexRef = varRef("i.0", F64, loc);
const needleRef = varRef("needle.0", needleT, loc);
const stateRef = varRef("state.0", F64, loc);
const nextRef = varRef("next.0", F64, loc);
const fromRef = varRef("from.0", fromT, loc);
const numberRef = varRef("number.0", F64, loc);
const integerRef = varRef("integer.0", F64, loc);
// Snapshot length before coercing fromIndex, then apply ToIntegerOrInfinity.
const number = positionNumber(
lowerer,
fromRef,
numLit(0, loc),
fromNode,
"array search position",
);
const integer: IrExpr = {
kind: "ternary",
cond: { kind: "libCall", fn: "num.isNaN", args: [numberRef], type: BOOL, loc },
then: numLit(0, loc),
else_: { kind: "libCall", fn: "math.trunc", args: [numberRef], type: F64, loc },
type: F64,
loc,
};
const forwardStart: IrExpr = {
kind: "ternary",
cond: { kind: "bin", op: "<", left: integerRef, right: numLit(0, loc), type: BOOL, loc },
then: {
kind: "libCall",
fn: "math.max",
args: [
{ kind: "bin", op: "+", left: varRef("n.0", F64, loc), right: integerRef, type: F64, loc },
numLit(0, loc),
],
type: F64,
loc,
},
// Adding positive zero canonicalizes a truncated negative zero.
else_: { kind: "bin", op: "+", left: integerRef, right: numLit(0, loc), type: F64, loc },
type: F64,
loc,
};
const start: IrExpr =
method === "lastIndexOf"
? {
kind: "ternary",
cond: { kind: "bin", op: "<", left: integerRef, right: numLit(0, loc), type: BOOL, loc },
then: {
kind: "bin",
op: "+",
left: varRef("n.0", F64, loc),
right: integerRef,
type: F64,
loc,
},
else_: {
kind: "libCall",
fn: "math.min",
args: [
{ kind: "bin", op: "+", left: integerRef, right: numLit(0, loc), type: F64, loc },
{
kind: "bin",
op: "-",
left: varRef("n.0", F64, loc),
right: numLit(1, loc),
type: F64,
loc,
},
],
type: F64,
loc,
},
type: F64,
loc,
}
: forwardStart;
const rawRead: IrExpr = { kind: "arrayGet", arr: arrRef, index: indexRef, type: elem, loc };
const missing = lowerer.wrappedUndefined(valueT, loc);
if (!missing) throw new InternalCompilerError("array search helper needs an undefined arm");
const optionalValue: IrExpr = {
kind: "ternary",
cond: { kind: "bin", op: "===", left: stateRef, right: numLit(1, loc), type: BOOL, loc },
then: typeEquals(elem, valueT) ? rawRead : lowerer.coerceToExpected(rawRead, valueT),
else_: missing,
type: valueT,
loc,
};
const value = directNeedle ? rawRead : optionalValue;
const readT = directNeedle ? elem : valueT;
const equal = arraySearchEquality(
lowerer,
varRef("value.0", readT, loc),
needleRef,
method === "includes",
loc,
);
const match =
method !== "includes"
? {
kind: "logical" as const,
op: "&&" as const,
left: {
kind: "bin" as const,
op: directNeedle ? ("===" as const) : ("!==" as const),
left: stateRef,
right: numLit(directNeedle ? 1 : 0, loc),
type: BOOL,
loc,
},
right: equal,
type: BOOL,
loc,
}
: equal;
const undefinedTag = lowerer.armTag(valueT.unionId, UNDEFINED_T);
if (undefinedTag < 0)
throw new InternalCompilerError("array search helper needs an undefined tag");
const needleIsUndefined: IrExpr = directNeedle
? boolLit(false, loc)
: {
kind: "unionIsTag",
unionId: valueT.unionId,
tag: undefinedTag,
negated: false,
value: needleRef,
type: BOOL,
loc,
};
const advance: IrStmt = {
kind: "assign",
localId: "i.0",
value: {
kind: "bin",
op: method === "lastIndexOf" ? "-" : "+",
left: indexRef,
right: numLit(1, loc),
type: F64,
loc,
},
loc,
};
const visitPresent: IrStmt[] = [
{ kind: "varDecl", localId: "value.0", init: value, loc },
{
kind: "if",
cond: match,
then: [{ kind: "return", value: method === "includes" ? boolLit(true, loc) : indexRef, loc }],
else_: null,
loc,
},
advance,
];
const skipHole: IrStmt[] =
method === "lastIndexOf"
? [advance]
: [
{
kind: "varDecl",
localId: "next.0",
init: {
kind: "arrIntrinsic",
method: "nextPresent",
receiver: arrRef,
args: [indexRef],
type: F64,
loc,
},
loc,
},
...(method === "includes"
? [
{
kind: "if" as const,
cond: needleIsUndefined,
then: [{ kind: "return" as const, value: boolLit(true, loc), loc }],
else_: null,
loc,
},
]
: []),
{ kind: "assign", localId: "i.0", value: nextRef, loc },
];
const loopBody: IrStmt[] = [
{
kind: "varDecl",
localId: "state.0",
init: { kind: "arrayState", arr: arrRef, index: indexRef, type: F64, loc },
loc,
},
{
kind: "if",
cond: { kind: "bin", op: "===", left: stateRef, right: numLit(0, loc), type: BOOL, loc },
then: skipHole,
else_: visitPresent,
loc,
},
];
lowerer.liftedFns.push({
name,
params: [
{ localId: "a.0", name: "a", type: arrT },
{ localId: "needle.0", name: "needle", type: needleT },
{ localId: "from.0", name: "from", type: fromT },
],
returnType: resultT,
locals: [
{ id: "a.0", name: "a", type: arrT, mutable: true },
{ id: "needle.0", name: "needle", type: needleT, mutable: true },
{ id: "from.0", name: "from", type: fromT, mutable: false },
{ id: "number.0", name: "number", type: F64, mutable: false },
{ id: "integer.0", name: "integer", type: F64, mutable: false },
{ id: "n.0", name: "n", type: F64, mutable: false },
{ id: "i.0", name: "i", type: F64, mutable: true },
{ id: "state.0", name: "state", type: F64, mutable: false },
{ id: "next.0", name: "next", type: F64, mutable: false },
{ id: "value.0", name: "value", type: readT, mutable: false },
],
body: [
arrayLengthDeclaration(arrT, loc),
{ kind: "varDecl", localId: "number.0", init: number, loc },
{ kind: "varDecl", localId: "integer.0", init: integer, loc },
{ kind: "varDecl", localId: "i.0", init: start, loc },
{
kind: "while",
cond:
method === "lastIndexOf"
? { kind: "bin", op: ">=", left: indexRef, right: numLit(0, loc), type: BOOL, loc }
: {
kind: "bin",
op: "<",
left: indexRef,
right: varRef("n.0", F64, loc),
type: BOOL,
loc,
},
body: loopBody,
loc,
},
{ kind: "return", value: method === "includes" ? boolLit(false, loc) : numLit(-1, loc), loc },
],
loc,
});
return name;
}
/** Tuple methods keep the original receiver alive through argument evaluation.
* Callback methods read each position only when it is visited, so writes to
* later positions through a captured alias are visible. A tuple cannot be
* passed as the callback's array parameter without changing its identity;
* that parameter retains an explicit representation boundary. */
export function lowerTupleReadMethodCall(
lowerer: Lowerer,
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
tupleReceiver?: IrExpr,
): IrExpr | null {
if (lowerer.chainBlocked(access, call)) return null;
const method = access.name.text;
const search = method === "includes" || method === "indexOf" || method === "lastIndexOf";
const predicate =
method === "filter" ||
method === "find" ||
method === "findIndex" ||
method === "some" ||
method === "every";
if (
method !== "slice" &&
method !== "map" &&
method !== "flatMap" &&
method !== "join" &&
!search &&
!predicate
)
return null;
if (!lowerer.isStdlibMember(access)) return null;
let receiverIr = tupleReceiver?.type ?? lowerer.mapTypeOf(lowerer.typeOf(access.expression));
let receiver: IrExpr | null = tupleReceiver ?? null;
const checkerArray = lowerer.checkerArrayValue(access.expression);
if (checkerArray?.type.kind === "record") {
receiverIr = checkerArray.type;
receiver = checkerArray;
}
if (receiverIr?.kind !== "record") return null;
const shape = lowerer.shapes.get(receiverIr.shapeId);
if (!shape?.tuple) return null;
receiver ??= lowerer.lowerExpr(access.expression);
if (method === "join") {
// Object.entries can retain its dynamic array representation behind a
// checker tuple. Native tuples box as live references, so separator
// evaluation can still mutate their elements before join reads them.
// An unchecked outer array read must validate its optional payload
// before passing the tuple to the runtime.
if (receiver.type.kind === "union") {
receiver = lowerer.coerceInto(access.expression, receiver, receiverIr);
}
const converted = lowerer.coerceInto(access.expression, receiver, DYN);
const boxed: IrExpr =
converted.kind === "dynFrom" && converted.value.type.kind === "record"
? { ...converted, liveRef: true }
: converted;
const joined = lowerDynDispatchMethodCall(lowerer, call, access, boxed, true);
return joined ? lowerer.coerceInto(call, joined, STRING) : null;
}
if (receiver.type.kind !== "record") return null;
const fields = [...shape.fields].sort((a, b) => Number(a.name) - Number(b.name));
const arms: IrType[] = [];
for (const field of fields) {
const types =
field.type.kind === "union" ? lowerer.unions.get(field.type.unionId)!.arms : [field.type];
for (const type of types) if (!arms.some((arm) => typeEquals(arm, type))) arms.push(type);
}
if (arms.length === 0) return null;
const elem: IrType =
arms.length === 1 ? arms[0]! : { kind: "union", unionId: lowerer.unions.intern(arms) };
const loc = locOf(call);
const tuple = varRef("a.0", receiver.type, loc);
const read = (field: (typeof fields)[number]): IrExpr =>
lowerer.coerceInto(
access.expression,
{
kind: "recordGet",
obj: tuple,
shapeId: receiverIr.shapeId,
field: field.name,
type: field.type,
loc,
},
elem,
);
if (search) {
if (call.arguments.nodes.length > 2 || call.arguments.nodes.some(ts.isSpreadElement)) {
return lowerer.noLowering(`tuple .${method} with spread or extra arguments`, call);
}
const arg = call.arguments.nodes[0];
const undefinedArg = arg ? lowerStaticallyUndefinedArgument(lowerer, arg) : null;
let needle = arg ? (undefinedArg ?? lowerer.lowerExpr(arg)) : null;
if (!needle || isUnitType(needle.type) || needle.type.kind === "void") {
const optional = arrayValueType(lowerer, elem);
const wrapped =
needle?.type.kind === "nullT"
? lowerer.coerceInto(arg ?? call, needle, optional)
: lowerer.wrappedUndefined(optional, loc);
if (!wrapped) return lowerer.noLowering(`tuple .${method} missing search value`, call);
needle = needle ? defaultAfterUndefined(needle, wrapped) : wrapped;
}
const from = call.arguments.nodes[1]
? lowerPositionArgument(lowerer, call.arguments.nodes[1], numLit(0, loc))
: numLit(method === "lastIndexOf" ? Infinity : 0, loc);
const key = `tuple.${method}:${typeKey(receiver.type)}:${typeKey(needle.type)}:${typeKey(from.type)}:${call.arguments.nodes.length}`;
let helper = lowerer.arrHofHelpers.get(key);
const resultType = method === "includes" ? BOOL : F64;
if (!helper) {
helper = keyedHelperName(lowerer, `%tuple.${method}`, key);
lowerer.arrHofHelpers.set(key, helper);
const params = [
{ localId: "a.0", name: "a", type: receiver.type },
{ localId: "needle.0", name: "needle", type: needle.type },
{ localId: "from.0", name: "from", type: from.type },
];
const result = lowerArraySearchValues(
lowerer,
call,
method,
elem,
{ kind: "arrayLit", elems: fields.map(read), type: arrayOf(elem), loc },
varRef("needle.0", needle.type, loc),
varRef("position.0", F64, loc),
);
lowerer.liftedFns.push({
name: helper,
params,
returnType: resultType,
locals: [
...params.map((param) => ({
id: param.localId,
name: param.name,
type: param.type,
mutable: false,
})),
{ id: "position.0", name: "position", type: F64, mutable: false },
],
body: [
{
kind: "varDecl",
localId: "position.0",
init: positionNumber(
lowerer,
varRef("from.0", from.type, loc),
numLit(0, loc),
call.arguments.nodes[1] ?? call,
"tuple search position",
),
loc,
},
{ kind: "return", value: result, loc },
],
loc,
});
}
return { kind: "call", callee: helper, args: [receiver, needle, from], type: resultType, loc };
}
if (method === "slice") {
if (call.arguments.nodes.length > 2 || call.arguments.nodes.some(ts.isSpreadElement)) {
lowerer.noLowering(`.slice with ${call.arguments.nodes.length} arguments`, call);
}
const args = call.arguments.nodes.map((arg) => lowerer.lowerExpr(arg));
args.forEach((arg, i) => {
if (arg.type.kind !== "f64")
lowerer.badType(call.arguments.nodes[i]!, lowerer.typeOf(call.arguments.nodes[i]!));
});
const resultType = arrayOf(elem);
const key = `tuple.slice:${typeKey(receiver.type)}:${args.length}`;
let helper = lowerer.arrHofHelpers.get(key);
if (!helper) {
helper = keyedHelperName(lowerer, "%tuple.slice", key);
lowerer.arrHofHelpers.set(key, helper);
const params = [receiver, ...args].map((arg, i) => ({
localId: i === 0 ? "a.0" : `arg.${i}`,
name: `arg${i}`,
type: arg.type,
}));
lowerer.liftedFns.push({
name: helper,
params,
returnType: resultType,
locals: params.map((param) => ({
id: param.localId,
name: param.name,
type: param.type,
mutable: false,
})),
body: [
{
kind: "return",
value: {
kind: "arrIntrinsic",
method: "slice",
receiver: { kind: "arrayLit", elems: fields.map(read), type: resultType, loc },
args: args.map((arg, i) => varRef(`arg.${i + 1}`, arg.type, loc)),
type: resultType,
loc,
},
loc,
},
],
loc,
});
}
return { kind: "call", callee: helper, args: [receiver, ...args], type: resultType, loc };
}
if (call.arguments.nodes.length !== 1 || call.arguments.nodes.some(ts.isSpreadElement)) {
lowerer.noLowering(
`.${method} with ${call.arguments.nodes.length} arguments`,
call,
"the thisArg parameter has no lowering — use an arrow function",
);
}
const callback = call.arguments.nodes[0]!;
// Check before the array HOF adapter can create a tuple-to-array copy.
const signatures = lowerer.checker.getCallSignatures(lowerer.typeOf(callback));
if (signatures.some((signature) => signature.getParameters().length > 2)) {
lowerer.noLowering(
`tuple .${method} callback array parameter`,
callback,
"capture the original tuple explicitly to preserve its identity",
);
}
if (predicate) {
return lowerTuplePredicateCall(lowerer, call, method, receiver, elem, fields.map(read));
}
const { fnArg, arity } = lowerArrayCallback(lowerer, callback, [elem], receiver.type);
const ret = fnArg.type.ret;
if (ret.kind === "void" || ret.kind === "func") lowerer.badType(call, lowerer.typeOf(call));
if (
method === "flatMap" &&
(ret.kind === "dyn" || (ret.kind === "record" && lowerer.shapes.get(ret.shapeId)?.tuple))
) {
return {
kind: "dynInvoke",
recv: { kind: "dynFrom", value: receiver, type: DYN, loc },
method: "flatMap",
calleeName: ts.getTextOfNode(access),
args: [{ kind: "dynFrom", value: fnArg, type: DYN, loc }],
type: DYN,
loc,
};
}
if (
method === "flatMap" &&
ret.kind === "union" &&
lowerer.unions.get(ret.unionId)!.arms.some((arm) => arm.kind === "array")
) {
lowerer.noLowering(
"tuple .flatMap callback mixing array and scalar results",
callback,
"return an array from every path",
);
}
const flatten = method === "flatMap" && ret.kind === "array";
const outElem = flatten ? ret.elem : callbackArrayElement(lowerer, call, ret);
requireProducedArrayElement(lowerer, call, `'.${method}()'`, outElem);
const outType = arrayOf(outElem);
const key = `tuple.${method}:${typeKey(receiver.type)}:${typeKey(fnArg.type)}:${typeKey(outElem)}`;
let helper = lowerer.arrHofHelpers.get(key);
if (!helper) {
helper = keyedHelperName(lowerer, `%tuple.${method}`, key);
lowerer.arrHofHelpers.set(key, helper);
const out = varRef("out.0", outType, loc);
const result = varRef("r.0", ret, loc);
const outputLength: IrExpr = {
kind: "arrIntrinsic",
method: "length",
receiver: out,
args: [],
type: F64,
loc,
};
const body: IrStmt[] = [
{
kind: "varDecl",
localId: "out.0",
init: { kind: "arrayLit", elems: [], type: outType, loc },
loc,
},
];
for (const field of fields) {
const invocation: IrExpr = {
kind: "callValue",
callee: varRef("f.0", fnArg.type, loc),
args: [read(field), numLit(Number(field.name), loc)].slice(0, arity),
type: ret,
loc,
};
body.push(
field === fields[0]
? { kind: "varDecl", localId: "r.0", init: invocation, loc }
: { kind: "assign", localId: "r.0", value: invocation, loc },
);
if (flatten) {
const index = varRef("i.0", F64, loc);
body.push(
countedFor(
loc,
{ kind: "arrIntrinsic", method: "length", receiver: result, args: [], type: F64, loc },
() => [
{
kind: "if",
cond: arrayIndexPresent(result, index, loc),
then: [
{
kind: "varDecl",
localId: "inner.0",
init: arrayValueRead(lowerer, result, index, outElem, loc),
loc,
},
arrayValueStore(
lowerer,
out,
outputLength,
varRef("inner.0", arrayValueType(lowerer, outElem), loc),
outElem,
loc,
),
],
else_: null,
loc,
},
],
),
);
} else {
body.push(arrayValueStore(lowerer, out, outputLength, result, outElem, loc));
}
}
body.push({ kind: "return", value: out, loc });
lowerer.liftedFns.push({
name: helper,
params: [
{ localId: "a.0", name: "a", type: receiver.type },
{ localId: "f.0", name: "f", type: fnArg.type },
],
returnType: outType,
locals: [
{ id: "a.0", name: "a", type: receiver.type, mutable: false },
{ id: "f.0", name: "f", type: fnArg.type, mutable: false },
{ id: "out.0", name: "out", type: outType, mutable: false },
{ id: "r.0", name: "r", type: ret, mutable: true },
...(flatten
? [
{ id: "i.0", name: "i", type: F64, mutable: true },
{
id: "inner.0",
name: "inner",
type: arrayValueType(lowerer, outElem),
mutable: false,
},
]
: []),
],
body,
loc,
});
}
return { kind: "call", callee: helper, args: [receiver, fnArg], type: outType, loc };
}
/** `a.at(i)` — the es2022 relative-index read. Desugars to an interned
* helper over existing IR nodes, ToIntegerOrInfinity-exact: the index
* truncates toward zero (floor for non-negatives, mirrored floor for
* negatives, NaN → 0), negatives wrap by the length once, and anything
* still outside [0, n) answers the undefined arm — never a bounds throw.
* The result is the checker's own `T | undefined` union, the find
* machinery's wrap rules (an undefined-armed element type passes
* through). */
function lowerArrayAtCall(
lowerer: Lowerer,
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
elem: IrType,
): IrExpr {
const loc = locOf(call);
if (call.arguments.nodes.some(ts.isSpreadElement))
lowerer.noLowering(".at with spread arguments", call);
const receiver = lowerer.lowerExpr(access.expression);
const indexNode = call.arguments.nodes[0];
const index = lowerPositionArgument(lowerer, indexNode, numLit(0, loc));
const resultT = lowerer.irTypeOf(call);
if (resultT.kind !== "union") lowerer.badType(call, lowerer.typeOf(call)); // defensive: T | undefined always maps to a union
const undefTag = lowerer.armTag(resultT.unionId, UNDEFINED_T);
if (undefTag < 0) lowerer.badType(call, lowerer.typeOf(call));
const key = `at:${typeKey(elem)}:${typeKey(resultT)}:${typeKey(index.type)}`;
let name = lowerer.arrHofHelpers.get(key);
if (!name) {
name = keyedHelperName(lowerer, "%arr.at", key);
lowerer.arrHofHelpers.set(key, name);
const arrT = arrayOf(elem);
const t = varRef("t.0", F64, loc);
const i = varRef("i.0", index.type, loc);
const number = varRef("number.0", F64, loc);
const n = varRef("n.0", F64, loc);
const found = lowerer.coerceToExpected(
arrayValueRead(lowerer, varRef("a.0", arrT, loc), t, elem, loc),
resultT,
);
const miss: IrExpr = {
kind: "unionWrap",
unionId: resultT.unionId,
tag: undefTag,
value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc },
type: resultT,
loc,
};
const floorOf = (e: IrExpr): IrExpr => ({
kind: "libCall",
fn: "math.floor",
args: [e],
type: F64,
loc,
});
const lt = (l: IrExpr, r: IrExpr): IrExpr => ({
kind: "bin",
op: "<",
left: l,
right: r,
type: BOOL,
loc,
});
const body: IrStmt[] = [
arrayLengthDeclaration(arrT, loc),
{
kind: "varDecl",
localId: "number.0",
init: positionNumber(lowerer, i, numLit(0, loc), indexNode ?? call, "array at position"),
loc,
},
// ToIntegerOrInfinity: floor is trunc for i >= 0; negatives mirror
// (trunc(i) = -floor(-i)); NaN becomes 0. ±Infinity floors to
// itself and falls out of range below, exactly the spec.
{ kind: "varDecl", localId: "t.0", init: floorOf(number), loc },
{
kind: "if",
cond: lt(number, numLit(0, loc)),
then: [
{
kind: "assign",
localId: "t.0",
value: {
kind: "bin",
op: "-",
left: numLit(0, loc),
right: floorOf({
kind: "bin",
op: "-",
left: numLit(0, loc),
right: number,
type: F64,
loc,
}),
type: F64,
loc,
},
loc,
},
],
else_: null,
loc,
},
{
kind: "if",
cond: { kind: "libCall", fn: "num.isNaN", args: [number], type: BOOL, loc },
then: [{ kind: "assign", localId: "t.0", value: numLit(0, loc), loc }],
else_: null,
loc,
},
{
kind: "if",
cond: lt(t, numLit(0, loc)),
then: [
{
kind: "assign",
localId: "t.0",
value: { kind: "bin", op: "+", left: t, right: n, type: F64, loc },
loc,
},
],
else_: null,
loc,
},
{
kind: "if",
cond: lt(t, numLit(0, loc)),
then: [{ kind: "return", value: miss, loc }],
else_: null,
loc,
},
{
kind: "if",
cond: { kind: "bin", op: ">=", left: t, right: n, type: BOOL, loc },
then: [{ kind: "return", value: miss, loc }],
else_: null,
loc,
},
{ kind: "return", value: found, loc },
];
lowerer.liftedFns.push({
name,
params: [
{ localId: "a.0", name: "a", type: arrT },
{ localId: "i.0", name: "i", type: index.type },
],
returnType: resultT,
locals: [
{ id: "a.0", name: "a", type: arrT, mutable: true },
{ id: "i.0", name: "i", type: index.type, mutable: false },
{ id: "n.0", name: "n", type: F64, mutable: false },
{ id: "number.0", name: "number", type: F64, mutable: false },
{ id: "t.0", name: "t", type: F64, mutable: true },
],
body,
loc,
});
}
return { kind: "call", callee: name, args: [receiver, index], type: resultT, loc };
}