mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-04 08:04:37 +08:00
2415 lines
81 KiB
TypeScript
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 };
|
|
}
|