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, elementPlaceholder, insertedValues, optionalElementValue, primitiveElement, } 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 }; } /** The checker type of `node` admits undefined. */ function checkerTypeAdmitsUndefined(lowerer: Lowerer, node: ts.Expression): boolean { const type = lowerer.typeOf(node); return (type.isUnionType() ? ts.constituentTypes(type) : [type]).some( (part) => (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0, ); } /** `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 || optionalElementValue(lowerer, probe, elem)), ); 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, }; } // A `(T | undefined)[]` (primitiveElement) takes undefined values as // present undefined elements, stored after the insertion. if ( (name === "push" || name === "unshift") && primitiveElement(elem) && call.arguments.nodes.some((a) => checkerTypeAdmitsUndefined(lowerer, a)) ) { const nodes = call.arguments.nodes; const values = nodes.map((a) => lowerer.lowerExpr(a)); const inserted = insertedValues( lowerer, values, elem, (value, i) => lowerer.coerceInto(nodes[i]!, value, elem), loc, ); if (inserted.optional) { const array = lowerer.declareHiddenLocal("%insertInto", receiver.type); const arr = varRef(array.id, array.type, loc); const length = lowerer.declareHiddenLocal("%insertLength", F64); const count: IrExpr = numLit(values.length, loc); const first: IrExpr = name === "push" ? { kind: "bin", op: "-", left: varRef(length.id, F64, loc), right: count, type: F64, loc, } : numLit(0, loc); return { kind: "seqExpr", stmts: [ { kind: "varDecl", localId: array.id, init: receiver, loc }, ...inserted.stmts, { kind: "varDecl", localId: length.id, init: { kind: "arrIntrinsic", method: name, receiver: arr, args: inserted.args, type: F64, loc, }, loc, }, ...inserted.fill(arr, first), ], result: varRef(length.id, F64, loc), type: F64, loc, }; } return { kind: "arrIntrinsic", method: name, receiver, args: inserted.args, 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 | "dyn")[] = []; 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; } // A checked-dynamic value: whether JS spreads it is decided at run // time (an array spreads one level, anything else is one element). if ( (argIr === null || argIr.kind === "dyn") && (probedArg ?? tryLowerExpression(lowerer, argNode))?.type.kind === "dyn" ) { shape.push("dyn"); args.push(lowerer.lowerExpr(argNode)); 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 || optionalElementValue(lowerer, probe, elem)), ); if (statefulItems && itemNodes.length === 1 && itemProbes[0]?.type.kind === "union") { 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 (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, }; // A direct needle matches only a present value (`match` tests the state // first); a present undefined of a `(T | undefined)[]` (primitiveElement) // must not be read as T. const value = !directNeedle ? optionalValue : primitiveElement(elem) ? { kind: "ternary" as const, cond: { kind: "bin" as const, op: "===" as const, left: stateRef, right: numLit(1, loc), type: BOOL, loc, }, then: rawRead, else_: elementPlaceholder(elem, loc), type: elem, loc, } : rawRead; 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 found: IrStmt = { kind: "if", cond: directNeedle ? equal : match, then: [{ kind: "return", value: method === "includes" ? boolLit(true, loc) : indexRef, loc }], else_: null, loc, }; // A direct needle reads only value slots: an undefined or foreign // element written through a dynamic view has no element-typed payload. const visitPresent: IrStmt[] = directNeedle ? [ { kind: "if", cond: { kind: "bin", op: "===", left: stateRef, right: numLit(1, loc), type: BOOL, loc }, then: [{ kind: "varDecl", localId: "value.0", init: value, loc }, found], else_: null, loc, }, advance, ] : [{ kind: "varDecl", localId: "value.0", init: value, loc }, found, 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 }; }