mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Split lower-exprs.ts into expression families (#1431)
This commit is contained in:
@@ -0,0 +1,767 @@
|
||||
/* Array literals. */
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer, neverTaintedJsType } from "../lowerer.js";
|
||||
import {
|
||||
BOOL,
|
||||
DYN,
|
||||
F64,
|
||||
type IrExpr,
|
||||
type IrRecordShape,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
STRING,
|
||||
type SrcLoc,
|
||||
UNDEFINED_T,
|
||||
VOID,
|
||||
arrayOf,
|
||||
isSupportedArrayElem,
|
||||
typeEquals,
|
||||
} from "../../../ir/ir.js";
|
||||
import { isJsSourceFile, locOf } from "../../program.js";
|
||||
import { underConstAssertion, unitOnlyUnion } from "../../type-mapper.js";
|
||||
import { dateJsonType, isDateJsonSource } from "../builtins/dates.js";
|
||||
import { npmStaticPackageOfPath } from "../../npm-static.js";
|
||||
import { tryLowerExpression } from "./try-lower-expression.js";
|
||||
import { maybePromiseUnionOf } from "../maybe-promise.js";
|
||||
import { lowerTernary } from "./narrowing.js";
|
||||
import { lowerCollectionSpread } from "../containers/collection-methods.js";
|
||||
import { strCharsCall } from "../containers/array-construction.js";
|
||||
import { generatorDrain } from "../iterator-adapters.js";
|
||||
import { isSearchParamsIterable } from "../builtins/url.js";
|
||||
import { varRef } from "../../../ir/build.js";
|
||||
import { arrayValueStore } from "../array-values.js";
|
||||
|
||||
/** JavaScript: whether `expr` is or holds an existing checked-dynamic
|
||||
* object — a binding stored as a dyn value whose checker type is an
|
||||
* object, or an array or object literal with such an element. Building
|
||||
* the enclosing literal with a static element type would copy it. */
|
||||
export function jsHoldsDynObject(lowerer: Lowerer, expr: ts.Expression): boolean {
|
||||
while (ts.isParenthesizedExpression(expr)) expr = expr.expression;
|
||||
if (ts.isIdentifier(expr)) {
|
||||
const stored = lowerer.peekLocal(expr)?.type ?? lowerer.globalOf(expr)?.type;
|
||||
return stored?.kind === "dyn" && (lowerer.typeOf(expr).flags & ts.TypeFlags.Object) !== 0;
|
||||
}
|
||||
if (ts.isArrayLiteralExpression(expr))
|
||||
return expr.elements.nodes.some(
|
||||
(element) => !ts.isSpreadElement(element) && jsHoldsDynObject(lowerer, element),
|
||||
);
|
||||
if (ts.isObjectLiteralExpression(expr))
|
||||
return expr.properties.nodes.some(
|
||||
(property) =>
|
||||
(ts.isPropertyAssignment(property) && jsHoldsDynObject(lowerer, property.initializer)) ||
|
||||
(ts.isShorthandPropertyAssignment(property) &&
|
||||
ts.isIdentifier(property.name) &&
|
||||
jsHoldsDynObject(lowerer, property.name)),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
/** `[a, b, c]`. The element type comes from the contextual type when tsc
|
||||
* has one (`const a: number[] = []`, arguments, nested literals) and from
|
||||
* the literal's own inferred type otherwise. A bare `[]` with no context
|
||||
* is `never[]` — unmappable, rejected with the component fence (SC2009). `expected` overrides
|
||||
* the contextual lookup where the caller knows the slot's array type and
|
||||
* tsc's API doesn't surface it (ternary arms under an array context —
|
||||
* tsc accepts the arm covariantly, but a tagged element representation
|
||||
* must be BUILT as the slot's element type). */
|
||||
export function lowerArrayLiteral(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.ArrayLiteralExpression,
|
||||
expected?: (IrType & { kind: "array" }) | (IrType & { kind: "record" }),
|
||||
): IrExpr {
|
||||
const loc = locOf(expr);
|
||||
const ctxType = lowerer.checker.getContextualType(expr);
|
||||
// `as const satisfies readonly T[]` checks against an array but keeps
|
||||
// the literal's inferred tuple type. Build that shape; an actual array
|
||||
// destination can use the ordinary tuple-to-array coercion afterward.
|
||||
const tsType = underConstAssertion(expr)
|
||||
? lowerer.typeOf(expr)
|
||||
: (ctxType ?? lowerer.typeOf(expr));
|
||||
let mapped = expected ?? lowerer.mapTypeOf(tsType);
|
||||
// Inferred JavaScript arrays can contain unrelated constructors with
|
||||
// structurally equal checker types. Their native class ABIs still differ.
|
||||
if (
|
||||
!expected &&
|
||||
isJsSourceFile(expr.getSourceFile()) &&
|
||||
mapped?.kind === "array" &&
|
||||
mapped.elem.kind === "classval"
|
||||
)
|
||||
mapped = DYN;
|
||||
// An inferred destructuring pattern supplies a contextual [any, ...]
|
||||
// tuple. Its positions do not erase the literal's inferred element
|
||||
// types; explicit destinations already arrive through expected.
|
||||
if (
|
||||
expected === undefined &&
|
||||
ctxType &&
|
||||
lowerer.checker.isTupleType(ctxType) &&
|
||||
lowerer.checker
|
||||
.getTypeArguments(ctxType as ts.TypeReference)
|
||||
.some((type) => (type.flags & ts.TypeFlags.Any) !== 0)
|
||||
) {
|
||||
const inferred = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
||||
if (
|
||||
inferred?.kind === "array" ||
|
||||
(inferred?.kind === "record" && lowerer.shapes.get(inferred.shapeId)?.tuple)
|
||||
)
|
||||
mapped = inferred;
|
||||
}
|
||||
// A JS literal whose OWN inferred type is never-tainted
|
||||
// (neverTaintedJsType — the evolving `const gb = []`, the mixed
|
||||
// command tuple `['pwd', []]`) carries no element information: route
|
||||
// it to the checked-dynamic tree fallback below rather than letting never's f64
|
||||
// representation build a static number array (a later dyn push would
|
||||
// throw "expected number at $, got string"; the tuple's union arm
|
||||
// would re-tag as number[] and fence). A REAL slot still wins:
|
||||
// `expected`, a contextual array/tuple, or a contextual union's
|
||||
// single array arm all provide element information first.
|
||||
// (The contextual lookup can answer the binding's own tainted
|
||||
// inference back — `const cmd = ['pwd', []]` — so the slot test is a
|
||||
// taint test on whichever type mapped, not a presence test.)
|
||||
const neverTaintedOwnJs =
|
||||
expected === undefined && neverTaintedJsType(lowerer, expr, lowerer.typeOf(expr));
|
||||
if (expected === undefined && neverTaintedJsType(lowerer, expr, tsType)) mapped = null;
|
||||
// A union-typed slot (`const x: string[] | number = [..]`) contextually
|
||||
// types the literal as the union; build the literal as its OWN type and
|
||||
// let the slot's coercion wrap it into the union. An unknown-typed slot
|
||||
// (`JSON.stringify([1, 2])`) likewise: build the literal's own type. An
|
||||
// UNMAPPABLE context falls back the same way — a destructuring pattern
|
||||
// without annotations contextually types its initializer `[any]`, while
|
||||
// the literal's own inferred type is the real tuple.
|
||||
if (!mapped || mapped.kind === "union" || mapped.kind === "dyn") {
|
||||
const ctxUnion = mapped?.kind === "union" ? mapped : null;
|
||||
mapped = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
||||
// The never-tainted own type carries no element information here
|
||||
// either (the JS story above) — only a contextual union's single
|
||||
// array arm below may still supply a static home.
|
||||
if (neverTaintedOwnJs) mapped = null;
|
||||
// An EMPTY literal in a union slot (`const t: string[] | undefined =
|
||||
// []`, the `[]` default argument against `string[] | undefined`) has
|
||||
// no useful own type — tsc infers `never[]`, whose f64-element
|
||||
// mapping is a representation for the uninhabited, not the slot's
|
||||
// arm. When the contextual union has exactly ONE array arm there is
|
||||
// no ambiguity: build the literal as that arm and let the slot's
|
||||
// coercion wrap it. Two array arms would need tsc's own best-fit
|
||||
// choice — none arises in practice; the fence stands.
|
||||
if (ctxUnion && expr.elements.nodes.length === 0) {
|
||||
const def = lowerer.unions.get(ctxUnion.unionId);
|
||||
const arrayArms = def?.arms.filter((a) => a.kind === "array") ?? [];
|
||||
if (arrayArms.length === 1) mapped = arrayArms[0]!;
|
||||
}
|
||||
// A NON-EMPTY literal whose own type has no static home (the JS dyn
|
||||
// fallback — a null/dyn mapping, or an inference that degraded to a
|
||||
// unit-only-element array over non-unit elements) under a union
|
||||
// with exactly ONE array-family arm — an array, or an
|
||||
// arity-matching tuple (the option-table `default: [{ value: [] }]`
|
||||
// shape): build AS that arm, exactly the empty-literal rule above;
|
||||
// the slot's coercion wraps it.
|
||||
if (ctxUnion && expr.elements.nodes.length > 0) {
|
||||
// A unit-only-element array (`(null | undefined)[]`) cannot hold
|
||||
// non-unit elements — neither as the literal's own mapping nor as
|
||||
// a competing union arm.
|
||||
const nonUnitElems = expr.elements.nodes.some(
|
||||
(el) =>
|
||||
!ts.isOmittedExpression(el) &&
|
||||
el.kind !== ts.SyntaxKind.NullKeyword &&
|
||||
!(ts.isIdentifier(el) && el.text === "undefined"),
|
||||
);
|
||||
const ownUnhelpful =
|
||||
mapped === null ||
|
||||
mapped.kind === "dyn" ||
|
||||
// The checker may echo the context union rather than infer this literal.
|
||||
mapped.kind === "union" ||
|
||||
(mapped.kind === "array" && nonUnitElems && lowerer.unitOnlyElem(mapped.elem));
|
||||
if (ownUnhelpful) {
|
||||
const def = lowerer.unions.get(ctxUnion.unionId);
|
||||
const arms = (def?.arms ?? []).filter(
|
||||
(a) =>
|
||||
(a.kind === "array" && !(nonUnitElems && lowerer.unitOnlyElem(a.elem))) ||
|
||||
(a.kind === "record" &&
|
||||
!!lowerer.shapes.get(a.shapeId)?.tuple &&
|
||||
lowerer.shapes.get(a.shapeId)!.fields.length === expr.elements.nodes.length),
|
||||
);
|
||||
if (arms.length === 1) mapped = arms[0]!;
|
||||
}
|
||||
}
|
||||
}
|
||||
// A Date's toJSON() element keeps its null in a literal with no declared
|
||||
// destination: the lib types say string.
|
||||
if (
|
||||
!expected &&
|
||||
mapped?.kind === "array" &&
|
||||
mapped.elem.kind === "string" &&
|
||||
expr.elements.nodes.some((element) => isDateJsonSource(lowerer, element))
|
||||
)
|
||||
mapped = { kind: "array", elem: dateJsonType(lowerer) };
|
||||
// An array literal inside an npm-static implicit-any instance can
|
||||
// inherit the checker's poisoned any[] context even after the
|
||||
// active binding gives every element one concrete lowering. Recover a
|
||||
// static array only when every spelled element lowers to the same
|
||||
// supported type. An explicit checked-dynamic destination still owns
|
||||
// the later slot coercion; heterogeneous literals, holes, and spreads
|
||||
// keep the checked-dynamic fallback below.
|
||||
if (
|
||||
lowerer.implicitParamTypes !== null &&
|
||||
npmStaticPackageOfPath(expr.getSourceFile().fileName) !== null &&
|
||||
(mapped === null ||
|
||||
mapped.kind === "dyn" ||
|
||||
(mapped.kind === "array" && mapped.elem.kind === "dyn")) &&
|
||||
expr.elements.nodes.length > 0 &&
|
||||
expr.elements.nodes.every(
|
||||
(element) => !ts.isSpreadElement(element) && !ts.isOmittedExpression(element),
|
||||
)
|
||||
) {
|
||||
const elements = expr.elements.nodes.map((element) => tryLowerExpression(lowerer, element));
|
||||
const first = elements[0];
|
||||
if (
|
||||
first !== undefined &&
|
||||
first !== null &&
|
||||
isSupportedArrayElem(first.type) &&
|
||||
elements.every((element) => element !== null && typeEquals(element.type, first.type))
|
||||
) {
|
||||
mapped = arrayOf(first.type);
|
||||
}
|
||||
}
|
||||
if (!expected && isJsSourceFile(expr.getSourceFile()) && mapped?.kind === "array") {
|
||||
const elem = mapped.elem;
|
||||
const arms = elem.kind === "union" ? lowerer.unions.get(elem.unionId)?.arms : [elem];
|
||||
if (arms?.length && arms.every((arm) => arm.kind === "classval")) mapped = DYN;
|
||||
// Inferred JavaScript literals mixing promises and plain values
|
||||
// (`[Promise.resolve(1), 2]`) keep the checked-dynamic array: their
|
||||
// consumers (Readable.from, Promise.all over iterators) inspect each
|
||||
// element at run time.
|
||||
if (maybePromiseUnionOf(lowerer, elem)) mapped = DYN;
|
||||
}
|
||||
if (
|
||||
!expected &&
|
||||
isJsSourceFile(expr.getSourceFile()) &&
|
||||
expr.elements.nodes.some(
|
||||
(element) =>
|
||||
ts.isSpreadElement(element) &&
|
||||
tryLowerExpression(lowerer, element.expression)?.type.kind === "dyn",
|
||||
)
|
||||
)
|
||||
mapped = DYN;
|
||||
// An element that is an existing checked-dynamic object (a file-scope
|
||||
// object literal binding, or a literal holding one) keeps its identity
|
||||
// only in a checked-dynamic array: a static element type would copy it.
|
||||
if (
|
||||
!expected &&
|
||||
isJsSourceFile(expr.getSourceFile()) &&
|
||||
mapped?.kind !== "dyn" &&
|
||||
expr.elements.nodes.some(
|
||||
(element) => !ts.isSpreadElement(element) && jsHoldsDynObject(lowerer, element),
|
||||
)
|
||||
)
|
||||
mapped = DYN;
|
||||
// An EMPTY literal under a CONST ASSERTION whose type queries panicked
|
||||
// (tsgo's readonly-[] TupleType conversion) uses a unit-element array,
|
||||
// mapType's own `[] as const` rule.
|
||||
if (
|
||||
mapped === null &&
|
||||
expr.elements.nodes.length === 0 &&
|
||||
(tsType.flags & ts.TypeFlags.Any) !== 0 &&
|
||||
underConstAssertion(expr)
|
||||
) {
|
||||
mapped = arrayOf(unitOnlyUnion(lowerer.unions));
|
||||
}
|
||||
// Heterogeneous JavaScript object arrays must retain each literal's
|
||||
// real keys. A structural union can otherwise drop descriptor fields.
|
||||
if (
|
||||
!expected &&
|
||||
isJsSourceFile(expr.getSourceFile()) &&
|
||||
mapped?.kind === "array" &&
|
||||
mapped.elem.kind === "union" &&
|
||||
expr.elements.nodes.some(ts.isObjectLiteralExpression)
|
||||
)
|
||||
mapped = DYN;
|
||||
|
||||
// A TUPLE-typed slot (`const t: [string, number] = ["a", 1]`): the
|
||||
// literal constructs the tuple's record shape — one positional field
|
||||
// per element, source order (which IS index order, so evaluation order
|
||||
// is JS-exact). tsc has already checked the arity; the recount below
|
||||
// backstops `as` smuggling. Fixed tuple spreads have known positions.
|
||||
if (mapped?.kind === "record") {
|
||||
const shape = lowerer.shapes.get(mapped.shapeId);
|
||||
if (shape?.tuple) {
|
||||
const spread = expr.elements.nodes.find(ts.isSpreadElement);
|
||||
if (spread) {
|
||||
return lowerTupleSpreadLiteral(lowerer, expr, mapped, shape);
|
||||
}
|
||||
if (expr.elements.nodes.length !== shape.fields.length) {
|
||||
// tsc padded an UNDER-LENGTH literal against an optional-element
|
||||
// tuple context (`options || []` with `options?: [string?,
|
||||
// number?]` — the instantiated type spells every position, the
|
||||
// literal spells fewer): no fixed shape holds it.
|
||||
lowerer.badType(expr, tsType);
|
||||
}
|
||||
const byName = new Map(shape.fields.map((f) => [f.name, f.type]));
|
||||
const fields = expr.elements.nodes.map((el, i) => {
|
||||
const fieldType = byName.get(String(i));
|
||||
if (!fieldType) lowerer.badType(el, lowerer.typeOf(el));
|
||||
return { name: String(i), value: lowerer.lowerExprExpecting(el, fieldType) };
|
||||
});
|
||||
return { kind: "recordLit", fields, type: mapped, loc };
|
||||
}
|
||||
}
|
||||
if (!mapped || mapped.kind !== "array") {
|
||||
// The JS declaration fallback, literal-side: an element type with no
|
||||
// static home (a string | string[] mixed command tuple, an evolving
|
||||
// []) builds as a dyn ARRAY — one dyn value whose elements each
|
||||
// convert through the usual boundary (dynFrom's JSON-safe domain);
|
||||
// an element that cannot convert fences per element. length/index
|
||||
// reads and dynamic consumers ride the keyed-dyn paths. TS literals
|
||||
// take the same build when the slot ITSELF is checked-dynamic — an
|
||||
// `unknown[]` annotation or a collapsed `(string | object)[]` maps
|
||||
// to DYN wholesale now (mapType's dyn-element array rule), so the
|
||||
// literal IS the dyn array.
|
||||
if (
|
||||
isJsSourceFile(expr.getSourceFile()) ||
|
||||
mapped?.kind === "dyn" ||
|
||||
lowerer.checkerAnyArray(expr)
|
||||
) {
|
||||
if (expr.elements.nodes.some(ts.isSpreadElement)) {
|
||||
// Native collection drains return vectors of retained dyn
|
||||
// elements. Reuse the ordinary spread builder, then expose the
|
||||
// resulting array through the established unknown[] representation.
|
||||
const vector = lowerArrayLiteral(lowerer, expr, arrayOf(DYN) as IrType & { kind: "array" });
|
||||
return lowerer.coerceInto(expr, vector, DYN);
|
||||
}
|
||||
const elems = expr.elements.nodes.map((el): IrExpr => {
|
||||
if (ts.isSpreadElement(el)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
el,
|
||||
"spread elements in a dynamic (unknown[]) array literal",
|
||||
);
|
||||
}
|
||||
const v = lowerer.coerceToExpected(lowerer.lowerExpr(el), DYN);
|
||||
if (v.type.kind !== "dyn") {
|
||||
lowerer.unsupported(
|
||||
"SC1101",
|
||||
el,
|
||||
`holding '${lowerer.fmt(v.type)}' values in a dynamic (unknown[]) array literal`,
|
||||
);
|
||||
}
|
||||
return v;
|
||||
});
|
||||
return { kind: "dynArrLit", elems, type: DYN, loc };
|
||||
}
|
||||
lowerer.badType(expr, tsType);
|
||||
}
|
||||
let type = mapped as IrType & { kind: "array" };
|
||||
// An inferred array of records takes the layout of an element literal
|
||||
// whose fields widened for missing-element reads; the other elements
|
||||
// build that layout directly. An explicit destination keeps its own.
|
||||
if (!expected && type.elem.kind === "record") {
|
||||
let variant: IrType | null = null;
|
||||
for (const candidate of expr.elements.nodes) {
|
||||
let x: ts.Expression = candidate;
|
||||
while (ts.isParenthesizedExpression(x)) x = x.expression;
|
||||
if (!ts.isObjectLiteralExpression(x)) continue;
|
||||
const probe = tryLowerExpression(lowerer, x);
|
||||
if (!probe || !lowerer.isRuntimeOptionalRecordVariant(probe.type, type.elem)) continue;
|
||||
if (variant !== null && !typeEquals(variant, probe.type)) {
|
||||
variant = null;
|
||||
break;
|
||||
}
|
||||
variant = probe.type;
|
||||
}
|
||||
if (variant !== null) type = arrayOf(variant) as IrType & { kind: "array" };
|
||||
}
|
||||
// Keep the array payload type fixed. Optional reads are stored through
|
||||
// arrayValueStore, which records UNDEFINED in the state byte while the
|
||||
// payload remains number/string/etc.; widening the array element here
|
||||
// would force boxed union storage and change aliases.
|
||||
const spreads: number[] = [];
|
||||
const holes = new Set<number>();
|
||||
const runtimeOptionalElements = new Set<number>();
|
||||
let needsStateBuild = false;
|
||||
for (let i = 0; i < expr.elements.nodes.length; i++) {
|
||||
const candidate = expr.elements.nodes[i]!;
|
||||
if (ts.isSpreadElement(candidate) || ts.isOmittedExpression(candidate)) continue;
|
||||
const probe = tryLowerExpression(lowerer, candidate);
|
||||
if (probe && lowerer.runtimeOptionalWidening(probe.type, type.elem) !== null) {
|
||||
runtimeOptionalElements.add(i);
|
||||
needsStateBuild = true;
|
||||
}
|
||||
}
|
||||
const elems: IrExpr[] = expr.elements.nodes.map((el, i): IrExpr => {
|
||||
if (ts.isSpreadElement(el)) {
|
||||
// `[...xs, b]`: xs must be an array of the literal's own element
|
||||
// type — its elements copy in at construction (a fresh array,
|
||||
// JS-exact). Iterables that aren't arrays (strings, Sets, Maps)
|
||||
// and mismatched element types stay fenced. A TERNARY source gets
|
||||
// the literal's own type as its expected type (the conditional-
|
||||
// spread idiom — tsc surfaces no contextual type through spreads,
|
||||
// and the arm literals must BUILD as this element type).
|
||||
let srcNode: ts.Expression = el.expression;
|
||||
while (ts.isParenthesizedExpression(srcNode)) srcNode = srcNode.expression;
|
||||
lowerer.fenceStaticHeadersIteration(el.expression);
|
||||
let src = ts.isConditionalExpression(srcNode)
|
||||
? lowerTernary(lowerer, srcNode, type)
|
||||
: lowerer.lowerExpr(el.expression);
|
||||
src = lowerCollectionSpread(lowerer, src, el.expression) ?? src;
|
||||
// `[...new SymbolIterator]`: a CLASS ITERABLE drains through its
|
||||
// own protocol into a fresh element array (classIteratorDrainCall
|
||||
// — an infinite iterator loops forever, exactly Node), and the
|
||||
// spread machinery copies like any array source.
|
||||
if (src.type.kind === "object") {
|
||||
const drained = lowerer.classIteratorDrainCall(src, locOf(el), type.elem);
|
||||
if (drained) src = drained;
|
||||
}
|
||||
// `[...s]` on a STRING spreads its code-point characters (the
|
||||
// string iterator's walk — astral chars whole) through the same
|
||||
// interned helper as Array.from(s); the result rides the array
|
||||
// machinery below like any string[] source.
|
||||
if (src.type.kind === "string") {
|
||||
src = strCharsCall(lowerer, src, locOf(el));
|
||||
}
|
||||
// `[...typedArray]`: represented typed arrays are dense numeric
|
||||
// iterables, so drain their elements into the fresh number[] that
|
||||
// the surrounding array literal will copy. In particular this is
|
||||
// the Uint8Array-to-Array bridge (`[...u8]`), with element values
|
||||
// read rather than backing bytes for the wider typed-array kinds.
|
||||
if (src.type.kind === "bytes") {
|
||||
src = {
|
||||
kind: "bytesIntrinsic",
|
||||
method: "toArray",
|
||||
receiver: src,
|
||||
args: [],
|
||||
type: arrayOf(F64),
|
||||
loc: locOf(el),
|
||||
};
|
||||
}
|
||||
// `[...it]` over a generator (or an iterator adapted into one):
|
||||
// drain it once, in order, into a fresh element array.
|
||||
if (src.type.kind === "generator") {
|
||||
const drained = generatorDrain(lowerer, src, type.elem, locOf(el));
|
||||
if (drained) src = drained;
|
||||
}
|
||||
// `[...sp]` and `[...sp.entries()]` over a URLSearchParams: its
|
||||
// checked-dynamic view iterates the live pair list, and the drained
|
||||
// [name, value] pairs convert to the literal's tuple elements.
|
||||
const searchParamsSource = isSearchParamsIterable(lowerer, el.expression);
|
||||
if (src.type.kind === "searchParams") src = lowerer.coerceInto(el.expression, src, DYN);
|
||||
// A native checked iterable can supply scalar elements even when
|
||||
// the checker inferred a typed array from its producer. Drain once
|
||||
// and validate the elements before the ordinary spread copy.
|
||||
if (
|
||||
src.type.kind === "dyn" &&
|
||||
(type.elem.kind === "dyn" ||
|
||||
type.elem.kind === "f64" ||
|
||||
type.elem.kind === "string" ||
|
||||
type.elem.kind === "bool" ||
|
||||
searchParamsSource)
|
||||
) {
|
||||
src = lowerer.coerceInto(el.expression, checkedIteratorPack(lowerer, src, locOf(el)), type);
|
||||
}
|
||||
// A same-family array whose ELEMENT lifts (string[] into a
|
||||
// (string | symbol)[] literal — per-element wrap/width copy):
|
||||
// the interned width helper reshapes before the spread copies.
|
||||
if (src.type.kind === "array" && !typeEquals(src.type, type)) {
|
||||
const w = lowerer.widthCoerce(src, type);
|
||||
if (w) src = w;
|
||||
}
|
||||
if (!typeEquals(src.type, type)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
el,
|
||||
`spreading '${lowerer.fmt(src.type)}' into a '${lowerer.fmt(type)}' literal (only a same-element-type array spreads)`,
|
||||
);
|
||||
}
|
||||
spreads.push(i);
|
||||
return src;
|
||||
}
|
||||
// A HOLE (`[,]` — an elision) remains absent. Ordinary reads still
|
||||
// answer undefined, while presence-sensitive methods and concat keep
|
||||
// the hole. The state-building path below advances length without
|
||||
// writing a value into this slot.
|
||||
if (ts.isOmittedExpression(el)) {
|
||||
needsStateBuild = true;
|
||||
holes.add(i);
|
||||
return { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc: locOf(el) };
|
||||
}
|
||||
// An ARRAY-LITERAL element under a UNION element slot whose own type
|
||||
// has no lift into the slot builds against the union's single array
|
||||
// arm instead (`['pwd', []]` as (string | string[])[] — tsc pushes no
|
||||
// contextual type through the unannotated chain, so the empty nested
|
||||
// literal types never[], whose f64-element representation re-tags
|
||||
// into nothing). One array arm is unambiguous — the union-slot rule's
|
||||
// stance; several keep the fence.
|
||||
if (type.elem.kind === "union" || type.elem.kind === "array") {
|
||||
let x: ts.Expression = el;
|
||||
while (ts.isParenthesizedExpression(x)) x = x.expression;
|
||||
if (ts.isArrayLiteralExpression(x)) {
|
||||
const ownT = lowerer.mapTypeOf(lowerer.checker.getContextualType(x) ?? lowerer.typeOf(x));
|
||||
if (ownT === null || lowerer.widthLiftPlan(ownT, type.elem) === null) {
|
||||
// An ARRAY-typed slot takes the literal directly; a union slot
|
||||
// routes through its single array arm and wraps.
|
||||
if (type.elem.kind === "array") {
|
||||
return lowerer.lowerArrayLiteral(x, type.elem);
|
||||
}
|
||||
const def = lowerer.unions.get(type.elem.unionId);
|
||||
const arrayArms = def?.arms.filter((a) => a.kind === "array") ?? [];
|
||||
if (arrayArms.length === 1) {
|
||||
const built = lowerer.lowerArrayLiteral(x, arrayArms[0] as IrType & { kind: "array" });
|
||||
return lowerer.coerceInto(el, built, type.elem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Elements flow into the element slot like an assignment: union
|
||||
// elements wrap plain arm values (`[1, "a"]` as (number | string)[]),
|
||||
// holes reject inside lowerExpr.
|
||||
let lowered: IrExpr;
|
||||
const directOptionalCandidate =
|
||||
runtimeOptionalElements.has(i) ||
|
||||
ts.isElementAccessExpression(el) ||
|
||||
(ts.isCallExpression(el) &&
|
||||
ts.isPropertyAccessExpression(el.expression) &&
|
||||
(el.expression.name.text === "pop" || el.expression.name.text === "shift"));
|
||||
if (directOptionalCandidate) {
|
||||
const raw = lowerer.lowerExpr(el);
|
||||
if (!typeEquals(raw.type, type.elem)) {
|
||||
needsStateBuild = true;
|
||||
return raw;
|
||||
}
|
||||
lowered = raw;
|
||||
} else {
|
||||
lowered = lowerer.lowerExprExpecting(el, type.elem);
|
||||
}
|
||||
if (!typeEquals(lowered.type, type.elem)) lowerer.badType(el, lowerer.typeOf(el));
|
||||
return lowered;
|
||||
});
|
||||
if (!needsStateBuild) {
|
||||
return { kind: "arrayLit", elems, ...(spreads.length > 0 ? { spreads } : {}), type, loc };
|
||||
}
|
||||
const out = lowerer.declareHiddenLocal("%arrayLit", type);
|
||||
const outRef = varRef(out.id, type, loc);
|
||||
// Dense capacity for the elements the statements below store.
|
||||
const capacity = Math.min(elems.length - spreads.length, 1024);
|
||||
const body: IrStmt[] = [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: out.id,
|
||||
init: {
|
||||
kind: "arrayLit",
|
||||
elems: [],
|
||||
...(capacity > 0 ? { capacity } : {}),
|
||||
type,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
];
|
||||
const spreadSet = new Set(spreads);
|
||||
const currentLength = (): IrExpr => ({
|
||||
kind: "arrIntrinsic",
|
||||
method: "length",
|
||||
receiver: outRef,
|
||||
args: [],
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
const nextLength = (): IrExpr => ({
|
||||
kind: "bin",
|
||||
op: "+",
|
||||
left: currentLength(),
|
||||
right: { kind: "numLit", value: 1, type: F64, loc },
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
for (let i = 0; i < elems.length; i++) {
|
||||
const el = elems[i]!;
|
||||
if (spreadSet.has(i)) {
|
||||
body.push({
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "arrIntrinsic",
|
||||
method: "pushSpread",
|
||||
receiver: outRef,
|
||||
args: [el],
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (holes.has(i)) {
|
||||
body.push({ kind: "arraySetLength", arr: outRef, length: nextLength(), loc });
|
||||
continue;
|
||||
}
|
||||
let value = el;
|
||||
if (!typeEquals(value.type, type.elem)) {
|
||||
const temp = lowerer.declareHiddenLocal("%arrayValue", value.type);
|
||||
body.push({ kind: "varDecl", localId: temp.id, init: value, loc });
|
||||
value = varRef(temp.id, temp.type, loc);
|
||||
}
|
||||
body.push(arrayValueStore(lowerer, outRef, currentLength(), value, type.elem, loc));
|
||||
}
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: body,
|
||||
result: outRef,
|
||||
type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** Build a fixed tuple from positional elements and fixed tuple spreads.
|
||||
* Capture each value in source order: deferring the reads until recordLit
|
||||
* would observe a later element's mutations of the spread source. */
|
||||
function lowerTupleSpreadLiteral(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.ArrayLiteralExpression,
|
||||
type: IrType & { kind: "record" },
|
||||
shape: IrRecordShape,
|
||||
): IrExpr {
|
||||
const loc = locOf(expr);
|
||||
const byName = new Map(shape.fields.map((field) => [field.name, field.type]));
|
||||
const stmts: IrStmt[] = [];
|
||||
const fields: { name: string; value: IrExpr }[] = [];
|
||||
const append = (node: ts.Expression, value: IrExpr): void => {
|
||||
const name = String(fields.length);
|
||||
const expected = byName.get(name);
|
||||
if (!expected) lowerer.badType(expr, lowerer.typeOf(expr));
|
||||
const coerced = lowerer.coerceInto(node, value, expected);
|
||||
const temp = lowerer.declareHiddenLocal("%tupleElement", expected);
|
||||
const at = locOf(node);
|
||||
stmts.push({ kind: "varDecl", localId: temp.id, init: coerced, loc: at });
|
||||
fields.push({ name, value: varRef(temp.id, expected, at) });
|
||||
};
|
||||
for (const element of expr.elements.nodes) {
|
||||
if (!ts.isSpreadElement(element)) {
|
||||
const expected = byName.get(String(fields.length));
|
||||
if (!expected) lowerer.badType(element, lowerer.typeOf(element));
|
||||
append(element, lowerer.lowerExprExpecting(element, expected));
|
||||
continue;
|
||||
}
|
||||
const source = lowerer.lowerExpr(element.expression);
|
||||
const sourceShape =
|
||||
source.type.kind === "record" ? lowerer.shapes.get(source.type.shapeId) : undefined;
|
||||
if (!sourceShape?.tuple) {
|
||||
// Empty tuples use a zero-length array representation. Preserve a
|
||||
// producing call's effects even though there are no positions to copy.
|
||||
const sourceTs = lowerer.typeOf(element.expression);
|
||||
if (
|
||||
source.type.kind === "array" &&
|
||||
lowerer.checker.isTupleType(sourceTs) &&
|
||||
lowerer.checker.getTypeArguments(sourceTs as ts.TypeReference).length === 0
|
||||
) {
|
||||
stmts.push({ kind: "exprStmt", expr: source, loc: locOf(element) });
|
||||
continue;
|
||||
}
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
element,
|
||||
"spreading a variable-length value into a fixed tuple literal",
|
||||
);
|
||||
}
|
||||
const temp = lowerer.declareHiddenLocal("%tupleSpread", source.type);
|
||||
const at = locOf(element);
|
||||
stmts.push({ kind: "varDecl", localId: temp.id, init: source, loc: at });
|
||||
const receiver = varRef(temp.id, source.type, at);
|
||||
const positions = [...sourceShape.fields].sort((a, b) => Number(a.name) - Number(b.name));
|
||||
for (const field of positions) {
|
||||
append(element.expression, {
|
||||
kind: "recordGet",
|
||||
obj: receiver,
|
||||
shapeId: sourceShape.id,
|
||||
field: field.name,
|
||||
type: field.type,
|
||||
loc: at,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (fields.length !== shape.fields.length) lowerer.badType(expr, lowerer.typeOf(expr));
|
||||
return { kind: "seqExpr", stmts, result: { kind: "recordLit", fields, type, loc }, type, loc };
|
||||
}
|
||||
|
||||
/** Drain using checked dispatch so native class and generator iterators keep their methods. */
|
||||
function checkedIteratorPack(lowerer: Lowerer, source: IrExpr, loc: SrcLoc): IrExpr {
|
||||
const name = "%iterator.pack.checked";
|
||||
if (!lowerer.liftedFns.some((fn) => fn.name === name)) {
|
||||
const ref = (id: string): IrExpr => ({ kind: "varRef", localId: id, type: DYN, loc });
|
||||
const property = (key: string): IrExpr => ({
|
||||
kind: "dynKeyGet",
|
||||
value: ref("step"),
|
||||
key: { kind: "strLit", value: key, type: STRING, loc },
|
||||
type: DYN,
|
||||
loc,
|
||||
});
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: [{ localId: "source", name: "source", type: DYN }],
|
||||
returnType: DYN,
|
||||
locals: ["source", "iterator", "step", "result"].map((id) => ({
|
||||
id,
|
||||
name: id,
|
||||
type: DYN,
|
||||
mutable: id !== "source",
|
||||
})),
|
||||
body: [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "iterator",
|
||||
init: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.iterator",
|
||||
args: [ref("source"), { kind: "strLit", value: "value", type: STRING, loc }],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "result",
|
||||
init: { kind: "dynArrLit", elems: [], type: DYN, loc },
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "while",
|
||||
cond: { kind: "boolLit", value: true, type: BOOL, loc },
|
||||
body: [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "step",
|
||||
init: {
|
||||
kind: "dynInvoke",
|
||||
recv: ref("iterator"),
|
||||
method: "next",
|
||||
calleeName: "iterator.next",
|
||||
args: [],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "if",
|
||||
cond: { kind: "dynTest", test: "truthy", value: property("done"), type: BOOL, loc },
|
||||
then: [{ kind: "return", value: ref("result"), loc }],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.packPush",
|
||||
args: [ref("result"), property("value")],
|
||||
type: VOID,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
},
|
||||
{ kind: "return", value: ref("result"), loc },
|
||||
],
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return { kind: "call", callee: name, args: [source], type: DYN, loc };
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,922 @@
|
||||
/* Identifier references in expression position: lowerExprInner's identifier branch and its helpers. */
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import * as posix from "node:path/posix";
|
||||
import { type Lowerer, PoisonError } from "../lowerer.js";
|
||||
import {
|
||||
BOOL,
|
||||
BYTES_ELEMENT_NAME,
|
||||
DYN,
|
||||
F64,
|
||||
type IrExpr,
|
||||
type IrType,
|
||||
STRING,
|
||||
type SrcLoc,
|
||||
UNDEFINED_T,
|
||||
arrayOf,
|
||||
funcOf,
|
||||
isUnitType,
|
||||
recordTextCodecClass,
|
||||
} from "../../../ir/ir.js";
|
||||
import { lowerNodeModuleIdentifier } from "../lower-node-module.js";
|
||||
import { lowerCjsExportsObjectValue } from "../cjs-exports-object.js";
|
||||
import { captureContextArguments } from "../function-context.js";
|
||||
import { isCjsExportTableLiteral, isJsSourceFile, isNodeEsmFile, locOf } from "../../program.js";
|
||||
import { builtinClassAliasOwnsNoSlot, runtimeClassValueOf } from "../builtins/class-values.js";
|
||||
import {
|
||||
cjsAccessorCall,
|
||||
primitiveCtorClosure,
|
||||
runtimeOptionalElementKey,
|
||||
runtimeOptionalUseOf,
|
||||
} from "../lower-exprs.js";
|
||||
import { runtimeOptionalReceiverRead } from "./narrowing.js";
|
||||
import { varRef } from "../../../ir/build.js";
|
||||
import {
|
||||
ambientUndefReadType,
|
||||
ambientUndefinedFnSymbolOf,
|
||||
contextualUndefReadType,
|
||||
fenceEarlyAliasUse,
|
||||
lowerNsIdentifierValue,
|
||||
nsUndefRead,
|
||||
} from "../lower-namespaces.js";
|
||||
import {
|
||||
blockedBindingUseDiag,
|
||||
unsupportedDiag,
|
||||
unsupportedPackageValueDiag,
|
||||
} from "../../../diagnostics/diagnostic.js";
|
||||
import { dirname } from "node:path";
|
||||
import { funcTypeFromParamShapes } from "../call-signatures.js";
|
||||
import { threadsImportOf } from "../../threads-import.js";
|
||||
import { experimentalImportOf } from "../../experimental-import.js";
|
||||
import { lowerWorkerMetadata } from "../builtins/workers.js";
|
||||
import { esmImportBindingKind } from "../builtins/module-bindings.js";
|
||||
import {
|
||||
builtinConstLit,
|
||||
builtinFenceHintOf,
|
||||
builtinModuleConstOf,
|
||||
builtinModuleFnOf,
|
||||
builtinModulesArrayLit,
|
||||
stdlibGlobalNameOf,
|
||||
} from "../surfaces.js";
|
||||
import { builtinConstantsObject } from "../lower-builtin-values.js";
|
||||
import { lowerTlsRootCertificates } from "../lower-server.js";
|
||||
import { builtinFunctionFenceValue } from "../builtin-function-fences.js";
|
||||
import { unsupportedModuleFeatureOf } from "../../builtin-modules.js";
|
||||
import { lowerGlobalValue } from "../lower-global-value.js";
|
||||
import { fenceEnumObjectValue } from "../lower-enums.js";
|
||||
import { mixinFnOfCallee } from "../lower-mixins.js";
|
||||
import { isNavigatorGlobal, refuseNavigator } from "../builtins/navigator.js";
|
||||
import { wasiGuestPath } from "../../../wasi-paths.js";
|
||||
|
||||
export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: SrcLoc): IrExpr {
|
||||
if (lowerer.isStdlibGlobal(expr, "console")) {
|
||||
return { kind: "libCall", fn: "console.native", args: [], type: DYN, loc };
|
||||
}
|
||||
|
||||
const moduleValue = lowerNodeModuleIdentifier(lowerer, expr);
|
||||
if (moduleValue) return moduleValue;
|
||||
if (expr.text === "exports") {
|
||||
const exportsValue = lowerCjsExportsObjectValue(lowerer, expr);
|
||||
if (exportsValue) return exportsValue;
|
||||
}
|
||||
if (lowerer.isSelfReference(expr)) {
|
||||
return { kind: "selfRef", type: lowerer.ctx.selfType!, loc };
|
||||
}
|
||||
// `arguments` in a variadic JS function (the rest-marked form): the
|
||||
// synthetic trailing dyn-array param — lambdaSignature marked the
|
||||
// type and lowerLambda declared the local.
|
||||
if (expr.text === "arguments") {
|
||||
const binding = captureContextArguments(lowerer.fnStack);
|
||||
if (binding) return { kind: "varRef", localId: binding.id, type: DYN, loc };
|
||||
}
|
||||
// A compiler-projected adapter as a VALUE: the adapter has no
|
||||
// runtime function object (its calls rewrite directly) — call it
|
||||
// through its immutable binding instead.
|
||||
{
|
||||
const sym = lowerer.resolveValueSymbol(expr);
|
||||
const projection = sym ? lowerer.staticCallables.get(sym) : undefined;
|
||||
if (
|
||||
projection?.kind === "promisified-exec-file" ||
|
||||
projection?.kind === "promisified-exec" ||
|
||||
projection?.kind === "promisified-builtin" ||
|
||||
projection?.kind === "promisified-zlib" ||
|
||||
projection?.kind === "promisified-crypto" ||
|
||||
projection?.kind === "promisified-dns"
|
||||
) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
`a promisified builtin as a value (call '${expr.text}' directly)`,
|
||||
);
|
||||
}
|
||||
if (projection?.kind === "builtin-function") {
|
||||
if (!isJsSourceFile(expr.getSourceFile())) {
|
||||
const callable = lowerer.lowerBuiltinCallableValue(
|
||||
{ module: projection.module, member: projection.member },
|
||||
loc,
|
||||
);
|
||||
if (callable) return callable;
|
||||
}
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
`a builtin function alias as an escaping value (call '${expr.text}' directly)`,
|
||||
);
|
||||
}
|
||||
// `EventEmitter`, `Readable` or `TypeError` (or a const alias of
|
||||
// one) as a value in a TypeScript file: the runtime class's class
|
||||
// object. JavaScript files keep the identity token.
|
||||
if (!isJsSourceFile(expr.getSourceFile())) {
|
||||
const runtimeClass = runtimeClassValueOf(lowerer, expr);
|
||||
if (runtimeClass) return lowerer.classValueRef(runtimeClass, expr);
|
||||
}
|
||||
// `const E = EventEmitter` owns no runtime slot: construction,
|
||||
// instanceof, typeof and .name lower at their use sites.
|
||||
if (sym && builtinClassAliasOwnsNoSlot(lowerer, sym)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
`a builtin class alias as an escaping value (use '${expr.text}' with new, instanceof, typeof or .name)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
// Union-typed bindings read through tsc's control-flow narrowing:
|
||||
// when the checker types this USE as a single arm, maybeNarrow
|
||||
// bridges the tagged representation with a unionNarrow.
|
||||
const local = lowerer.resolveLocal(expr);
|
||||
if (local) {
|
||||
// A switch can dispatch past a lexical declaration. Keep this
|
||||
// previously refused codec form out until switch-scope TDZ boxes
|
||||
// model that skipped initialization, including value aliases.
|
||||
if (
|
||||
local.type.kind === "record" &&
|
||||
recordTextCodecClass(lowerer.shapes.get(local.type.shapeId)!) !== null
|
||||
) {
|
||||
const symbol = lowerer.resolveValueSymbol(expr);
|
||||
const decl = symbol ? symbol.valueDeclaration : undefined;
|
||||
if (
|
||||
decl &&
|
||||
ts.isVariableDeclaration(decl) &&
|
||||
ts.isVariableDeclarationList(decl.parent) &&
|
||||
(decl.parent.flags & ts.NodeFlags.BlockScoped) !== 0 &&
|
||||
ts.isVariableStatement(decl.parent.parent)
|
||||
) {
|
||||
const clause = decl.parent.parent.parent;
|
||||
if (
|
||||
(ts.isCaseClause(clause) || ts.isDefaultClause(clause)) &&
|
||||
!(expr.getStart() >= clause.getStart() && expr.end <= clause.end)
|
||||
) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
"codec bindings read from another switch clause before initialization is proven",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (local.type.kind === "caught") return lowerer.caughtRead(expr, local, loc);
|
||||
const symbol = lowerer.resolveValueSymbol(expr);
|
||||
const runtimeOptionalRoot = lowerer.runtimeOptionalRootOf(local);
|
||||
const active = lowerer.runtimeOptionalLocals.has(runtimeOptionalRoot);
|
||||
const arithmetic = lowerer.runtimeOptionalArithmeticLocals.has(runtimeOptionalRoot);
|
||||
const captured =
|
||||
!!symbol &&
|
||||
lowerer.runtimeOptionalStorageLocals.has(runtimeOptionalRoot) &&
|
||||
(local.boxed === true ||
|
||||
lowerer.ctx.captureBySymbol.get(symbol) === local ||
|
||||
runtimeOptionalRoot !== local);
|
||||
if (arithmetic && !runtimeOptionalUseOf(expr)) {
|
||||
return { kind: "varRef", localId: local.id, type: local.type, loc };
|
||||
}
|
||||
if (active || captured) {
|
||||
const use = runtimeOptionalUseOf(expr);
|
||||
if (use?.complex) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
"a logical or conditional receiver containing a runtime-optional capture",
|
||||
);
|
||||
}
|
||||
if (use?.optional) return { kind: "varRef", localId: local.id, type: local.type, loc };
|
||||
if (use?.kind === "property")
|
||||
return runtimeOptionalReceiverRead(lowerer, expr, local, use.access.name.text, loc);
|
||||
if (use?.kind === "element") {
|
||||
const key = runtimeOptionalElementKey(use.access.argumentExpression);
|
||||
if (key === null) {
|
||||
return varRef(local.id, local.type, loc);
|
||||
}
|
||||
return runtimeOptionalReceiverRead(lowerer, expr, local, key, loc);
|
||||
}
|
||||
if (use?.kind === "call") {
|
||||
if (use.comma && !use.optional) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
use.access,
|
||||
"a direct call through a comma-wrapped runtime-optional capture",
|
||||
);
|
||||
}
|
||||
return runtimeOptionalReceiverRead(lowerer, expr, local, null, loc);
|
||||
}
|
||||
if (runtimeOptionalObjectWalkerArg(lowerer, expr)) {
|
||||
const narrowed = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
||||
if (local.type.kind === "union" && narrowed) {
|
||||
const helper =
|
||||
narrowed.kind === "union"
|
||||
? lowerer.narrowedRetagHelper(expr, local.type.unionId, narrowed.unionId, loc)
|
||||
: !isUnitType(narrowed)
|
||||
? lowerer.narrowedArmHelper(local.type.unionId, narrowed, loc)
|
||||
: null;
|
||||
if (helper) {
|
||||
return lowerer.checkedNarrowCall(
|
||||
helper,
|
||||
varRef(local.id, local.type, loc),
|
||||
narrowed,
|
||||
loc,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return { kind: "varRef", localId: local.id, type: local.type, loc };
|
||||
}
|
||||
const narrowed = lowerer.maybeNarrow(
|
||||
{ kind: "varRef", localId: local.id, type: local.type, loc },
|
||||
expr,
|
||||
);
|
||||
if (
|
||||
narrowed.type.kind === "union" &&
|
||||
lowerer.runtimeOptionalStorageLocals.has(runtimeOptionalRoot) &&
|
||||
lowerer.mapTypeOf(lowerer.typeOf(expr))?.kind === "record"
|
||||
) {
|
||||
// A presence guard can be followed by a predicate strengthening
|
||||
// the record's fields. Its refined shape is not a stored union
|
||||
// arm: extract the original layout before reading a member.
|
||||
const use = runtimeOptionalUseOf(expr);
|
||||
if (use && !use.complex && !use.optional) {
|
||||
if (use.kind === "property")
|
||||
return runtimeOptionalReceiverRead(lowerer, expr, local, use.access.name.text, loc);
|
||||
if (use.kind === "element") {
|
||||
const key = runtimeOptionalElementKey(use.access.argumentExpression);
|
||||
if (key !== null) return runtimeOptionalReceiverRead(lowerer, expr, local, key, loc);
|
||||
}
|
||||
}
|
||||
}
|
||||
return narrowed;
|
||||
}
|
||||
// `import x = N.y` aliases resolve transparently through globalOf/
|
||||
// fnSigOf below; their source-order guards live here (a no-op for
|
||||
// every non-import= binding — see lower-namespaces.ts).
|
||||
fenceEarlyAliasUse(lowerer, expr, expr);
|
||||
const g = lowerer.globalOf(expr);
|
||||
if (g) {
|
||||
// A global typed by a class that never REGISTERED (its collection
|
||||
// fenced): the initializing assignment never lowered, so the read
|
||||
// can only observe garbage — and the emitter would name a struct
|
||||
// that does not exist. The blocked-binding cascade names the use;
|
||||
// without this the validator's registration check ICEs on the live
|
||||
// reference (`@this {T}` inference — signature 07).
|
||||
const unregistered = lowerer.unregisteredClassOf(g.type, new Set());
|
||||
if (unregistered !== null) {
|
||||
// The class's own rejection is the root cause the cascade names.
|
||||
lowerer.flushDeferredClass(unregistered);
|
||||
lowerer.pushDiag(blockedBindingUseDiag(expr.text, loc));
|
||||
throw new PoisonError();
|
||||
}
|
||||
const ref: IrExpr = { kind: "varRef", localId: g.id, type: g.type, loc };
|
||||
if (lowerer.isRuntimeOptionalArithmeticGlobal(g) && !runtimeOptionalUseOf(expr)) {
|
||||
return ref;
|
||||
}
|
||||
const storageHasUndefined =
|
||||
g.type.kind === "union" && lowerer.armTag(g.type.unionId, UNDEFINED_T) >= 0;
|
||||
if (lowerer.isRuntimeOptionalGlobal(g) || storageHasUndefined) {
|
||||
// A checker-narrowed global receiver (`if (tags !== undefined)
|
||||
// tags[tags.length] = ...`, or `a ? Object.keys(a) : ...`) is
|
||||
// still stored as the runtime optional union. Extract the proven
|
||||
// value arm before routing the member/element operation so dynamic
|
||||
// keys and builtin record walkers do not see a union receiver.
|
||||
const narrowed = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
||||
const arithmeticUnion =
|
||||
g.type.kind === "union" &&
|
||||
lowerer.unions.get(g.type.unionId)?.arms.length === 2 &&
|
||||
lowerer.unions.get(g.type.unionId)!.arms.some((a) => a.kind === "f64") &&
|
||||
lowerer.unions.get(g.type.unionId)!.arms.some((a) => a.kind === "string");
|
||||
const symbol = lowerer.resolveValueSymbol(expr);
|
||||
if (
|
||||
!arithmeticUnion &&
|
||||
!(symbol !== null && lowerer.earlyReadVars.has(symbol)) &&
|
||||
g.type.kind === "union" &&
|
||||
narrowed &&
|
||||
narrowed.kind !== "union" &&
|
||||
!isUnitType(narrowed) &&
|
||||
!(
|
||||
narrowed.kind === "record" &&
|
||||
recordTextCodecClass(lowerer.shapes.get(narrowed.shapeId)!) !== null
|
||||
) &&
|
||||
narrowed.kind !== "f64" &&
|
||||
narrowed.kind !== "string" &&
|
||||
narrowed.kind !== "bool" &&
|
||||
lowerer.armTag(g.type.unionId, narrowed) >= 0
|
||||
) {
|
||||
const helper = lowerer.narrowedArmHelper(g.type.unionId, narrowed, loc);
|
||||
if (helper) return lowerer.checkedNarrowCall(helper, ref, narrowed, loc);
|
||||
}
|
||||
const use = runtimeOptionalUseOf(expr);
|
||||
// The enclosing logical/conditional expression consumes the union
|
||||
// before any member operation runs (`(value ?? fallback).length`).
|
||||
// Keep the tagged value intact and let that lazy expression decide
|
||||
// its branch; extracting here would evaluate or throw too early.
|
||||
if (use?.complex) return ref;
|
||||
if (use?.optional) return ref;
|
||||
if (use?.kind === "property")
|
||||
return runtimeOptionalReceiverRead(lowerer, expr, g, use.access.name.text, loc);
|
||||
if (use?.kind === "element") {
|
||||
const key = runtimeOptionalElementKey(use.access.argumentExpression);
|
||||
if (key === null)
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
use.access,
|
||||
"a computed read from a runtime-optional global (use a literal key)",
|
||||
);
|
||||
return runtimeOptionalReceiverRead(lowerer, expr, g, key, loc);
|
||||
}
|
||||
if (use?.kind === "call") return runtimeOptionalReceiverRead(lowerer, expr, g, null, loc);
|
||||
return ref;
|
||||
}
|
||||
return lowerer.maybeNarrow(ref, expr);
|
||||
}
|
||||
// `undefined` — tsc's intrinsic global (a local shadowing the name
|
||||
// resolved above): a unit literal, exactly like the `null` keyword.
|
||||
if (expr.text === "undefined" && lowerer.typeOf(expr).flags & ts.TypeFlags.Undefined) {
|
||||
return { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc };
|
||||
}
|
||||
// `__dirname` / `__filename` — the CommonJS module globals: per-
|
||||
// MODULE compile-time constants, the containing file's location
|
||||
// (Node's exact values when the same tree runs in place; WASI uses
|
||||
// the equivalent guest-visible path; locals shadowing the names
|
||||
// resolved above). In a file with ESM syntax Node never defines them
|
||||
// (ReferenceError) — fence rather than invent a value there.
|
||||
if (
|
||||
(expr.text === "__dirname" || expr.text === "__filename") &&
|
||||
lowerer.isStdlibGlobal(expr, expr.text)
|
||||
) {
|
||||
const sf = expr.getSourceFile();
|
||||
if (isNodeEsmFile(sf, lowerer.program)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
`'${expr.text}' in an ES module (Node defines the CJS module globals only in CommonJS modules)`,
|
||||
);
|
||||
}
|
||||
// The WASI runner mounts the host cwd at guest `/` and the host temp
|
||||
// directory at `/tmp`. Bake the matching GUEST spelling so resource
|
||||
// lookups relative to these CommonJS globals stay inside the exposed
|
||||
// capability instead of naming an unreachable host-absolute path.
|
||||
const fileName = moduleFileName(lowerer, sf);
|
||||
const value =
|
||||
expr.text === "__dirname"
|
||||
? lowerer.targetPlatform === "wasi"
|
||||
? posix.dirname(fileName)
|
||||
: dirname(fileName)
|
||||
: fileName;
|
||||
return { kind: "strLit", value, type: STRING, loc };
|
||||
}
|
||||
const sig = lowerer.fnSigOf(expr);
|
||||
if (sig && lowerer.isTopLevelFnSymbol(expr)) {
|
||||
// A declared function used as a value: a zero-capture closure. The
|
||||
// backend interns one per function so `f === f` holds (JS identity).
|
||||
// The value's type is the completed ABI signature; optional/default
|
||||
// slots and typed rest arrays pass the value fence first.
|
||||
lowerer.noteEdge(sig.name);
|
||||
// dynRest slots stay out of the VALUE type's param list (fn.length
|
||||
// semantics); the rest marker carries the trailing dyn-array ABI.
|
||||
const funcType: IrType = funcTypeFromParamShapes(sig.params, sig.returnType);
|
||||
lowerer.requireExactArityValue(expr, expr, sig.params, funcType);
|
||||
return { kind: "closure", fnName: sig.name, captures: [], type: funcType, loc };
|
||||
}
|
||||
{
|
||||
// A generic function as a VALUE: monomorphized by flow — the
|
||||
// reference's pinned concrete signature (contextual type or an
|
||||
// instantiation expression) names the instance; unpinned
|
||||
// references fence inside (lowerGenericFnValue).
|
||||
const gfn = lowerer.genericFnOf(expr);
|
||||
if (gfn) return lowerer.lowerGenericFnValue(expr, gfn);
|
||||
}
|
||||
// A CJS export-table ACCESSOR read (`tmpdir.path`, or a destructured
|
||||
// binding aliasing one): the getter is a real function of module
|
||||
// scope — its body reads module globals, so it lifts like a lambda
|
||||
// (interned per declaration) and every read is a call. Node runs the
|
||||
// getter per read; so does this.
|
||||
{
|
||||
const acc = cjsExportAccessorRead(lowerer, expr);
|
||||
if (acc) return acc;
|
||||
}
|
||||
// A binding destructured from a baked constants object
|
||||
// (http2.constants / crypto.constants): the literal — the
|
||||
// declaration emitted nothing (builtinConstantsDestructureDecl).
|
||||
{
|
||||
const h2c = lowerer.builtinConstantBindingOf(expr);
|
||||
if (h2c) return h2c;
|
||||
}
|
||||
// Builtin-module import bindings: constants (path.sep, os.EOL) read
|
||||
// as interned string literals; functions have no closure
|
||||
// representation (they lower to libCall at call sites only); members
|
||||
// with no lowering at all fence with the module-qualified name.
|
||||
{
|
||||
const member = threadsImportOf(lowerer.program, expr);
|
||||
// scriptc delivers published graphs by reference.
|
||||
if (member === "sharesPublishedGraphs")
|
||||
return { kind: "boolLit", value: true, type: BOOL, loc };
|
||||
if (member === "publish")
|
||||
lowerer.noLowering(
|
||||
"publish (@scriptc/threads) as a value",
|
||||
expr,
|
||||
"call publish(value) directly",
|
||||
);
|
||||
if (experimentalImportOf(lowerer.program, expr) === "LifetimeRegion")
|
||||
lowerer.noLowering(
|
||||
"LifetimeRegion (@scriptc/experimental) as a value",
|
||||
expr,
|
||||
"construct regions with new LifetimeRegion()",
|
||||
);
|
||||
if (experimentalImportOf(lowerer.program, expr) === "InternTable")
|
||||
lowerer.noLowering(
|
||||
"InternTable (@scriptc/experimental) as a value",
|
||||
expr,
|
||||
'open tables with new InternTable("name")',
|
||||
);
|
||||
if (experimentalImportOf(lowerer.program, expr) === "ThreadSlots")
|
||||
lowerer.noLowering(
|
||||
"ThreadSlots (@scriptc/experimental) as a value",
|
||||
expr,
|
||||
'open slots with new ThreadSlots("name")',
|
||||
);
|
||||
}
|
||||
{
|
||||
const bi = lowerer.builtinImportOf(expr);
|
||||
if (bi) {
|
||||
if (bi.module === "worker_threads") {
|
||||
const value = lowerWorkerMetadata(bi.member, loc);
|
||||
if (value) return value;
|
||||
}
|
||||
// A named import of defaultMaxListeners reads the ES module
|
||||
// namespace's copy. A CommonJS destructured or member binding holds
|
||||
// the value read when its declaration ran, which these bindings do
|
||||
// not store.
|
||||
if (bi.module === "events" && bi.member === "defaultMaxListeners") {
|
||||
if (esmImportBindingKind(lowerer, expr) === "named") {
|
||||
return { kind: "libCall", fn: "emitter.esmDefaultMax", args: [], type: F64, loc };
|
||||
}
|
||||
lowerer.noLowering(
|
||||
"events.defaultMaxListeners through a CommonJS destructured or member binding",
|
||||
expr,
|
||||
"read require('events').defaultMaxListeners or EventEmitter.defaultMaxListeners where the value is needed",
|
||||
);
|
||||
}
|
||||
const c = builtinModuleConstOf(lowerer, bi.module, bi.member);
|
||||
if (c !== undefined) return builtinConstLit(c, loc);
|
||||
// module.builtinModules — the baked Node v24 list, a fresh
|
||||
// string[] per read.
|
||||
if (bi.module === "module" && bi.member === "builtinModules") {
|
||||
return builtinModulesArrayLit(loc);
|
||||
}
|
||||
if (bi.module === "http" && bi.member === "METHODS") {
|
||||
return { kind: "libCall", fn: "http.methods", args: [], type: arrayOf(STRING), loc };
|
||||
}
|
||||
if ((bi.module === "http" || bi.module === "https") && bi.member === "globalAgent") {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "http.globalAgent",
|
||||
args: [{ kind: "boolLit", value: bi.module === "https", type: BOOL, loc }],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (bi.module === "http" && bi.member === "STATUS_CODES") {
|
||||
return { kind: "libCall", fn: "http.statusCodes", args: [], type: DYN, loc };
|
||||
}
|
||||
if (bi.module === "os" && bi.member === "constants") {
|
||||
return { kind: "libCall", fn: "os.constants", args: [], type: DYN, loc };
|
||||
}
|
||||
if (bi.module === "fs" && bi.member === "constants")
|
||||
return { kind: "libCall", fn: "fs.constants", args: [], type: DYN, loc };
|
||||
// `import { O_RDONLY } from "constants"`: the member of the frozen
|
||||
// module object.
|
||||
if (bi.module === "constants") {
|
||||
return {
|
||||
kind: "dynKeyGet",
|
||||
value: builtinConstantsObject(lowerer, loc),
|
||||
key: { kind: "strLit", value: bi.member, type: STRING, loc },
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// tls.rootCertificates: a runtime-valued module constant (the
|
||||
// cached bundled-CA array) — the one member read that lowers
|
||||
// to a libCall instead of a baked literal.
|
||||
{
|
||||
const roots = lowerTlsRootCertificates(lowerer, bi, loc);
|
||||
if (roots) return roots;
|
||||
}
|
||||
{
|
||||
const callable = lowerer.lowerBuiltinCallableValue(bi, loc);
|
||||
if (callable) return callable;
|
||||
}
|
||||
// JavaScript sources: an otherwise unsupported builtin VALUE is
|
||||
// the same identity-token story as stdlib globals above (the
|
||||
// harness adds worker_threads.Worker to its identity Set).
|
||||
if (isJsSourceFile(expr.getSourceFile())) {
|
||||
// A function member is a function value that fences when called.
|
||||
if (builtinModuleFnOf(lowerer, bi.module, bi.member))
|
||||
return builtinFunctionFenceValue(lowerer, bi, expr, loc);
|
||||
return {
|
||||
kind: "strLit",
|
||||
value: `[builtin ${bi.module}.${bi.member}]`,
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (builtinModuleFnOf(lowerer, bi.module, bi.member)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
`library functions as values (call '${expr.text}' directly)`,
|
||||
);
|
||||
}
|
||||
lowerer.noLowering(
|
||||
`${bi.module}.${bi.member}`,
|
||||
expr,
|
||||
builtinFenceHintOf(bi.module, bi.member),
|
||||
lowerer.resolveValueSymbol(expr),
|
||||
);
|
||||
}
|
||||
}
|
||||
// A builtin namespace import used as a bare VALUE (`const f = fs`):
|
||||
// the namespace object has no representation — members lower at
|
||||
// their access sites only. The CommonJS namespace binding
|
||||
// (`const lib = require("./lib.js")`) gets the same fence.
|
||||
const builtinNamespace = lowerer.builtinNamespaceModuleOf(expr);
|
||||
if (builtinNamespace === "constants") return builtinConstantsObject(lowerer, loc);
|
||||
if (builtinNamespace !== null) {
|
||||
const type: IrType = { kind: "moduleNs", moduleId: `builtin:${builtinNamespace}` };
|
||||
return { kind: "moduleNsRef", moduleId: type.moduleId, type, loc };
|
||||
}
|
||||
if (lowerer.cjsLocalModuleBindingOf(expr)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
`module namespace objects as values (access '${expr.text}' members directly)`,
|
||||
);
|
||||
}
|
||||
if (lowerer.stdlibGlobalFnOf(expr)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
`library functions as values (call '${expr.text}' directly)`,
|
||||
);
|
||||
}
|
||||
// A declaration-only npm binding has no compiled runtime value.
|
||||
{
|
||||
const pkg = lowerer.npmPackageOfSymbol(lowerer.resolveValueSymbol(expr) ?? undefined);
|
||||
if (pkg) {
|
||||
lowerer.pushDiag(unsupportedPackageValueDiag(pkg, loc));
|
||||
throw new PoisonError();
|
||||
}
|
||||
}
|
||||
// Bindings imported from an UNSUPPORTED builtin module: coverage
|
||||
// analyzes past import fences, so uses of `spawn` from
|
||||
// child_process reach lowering — the use site reports the same
|
||||
// "the '<module>' module" diagnostic as the import line, and the
|
||||
// report groups them into one blocker. (Builds never get here: the
|
||||
// import already failed preflight.)
|
||||
{
|
||||
const spec = lowerer.fencedBuiltinImportOf(expr);
|
||||
if (spec !== null) {
|
||||
lowerer.pushDiag(unsupportedDiag("SC1010", loc, unsupportedModuleFeatureOf(spec)));
|
||||
throw new PoisonError();
|
||||
}
|
||||
}
|
||||
// The globals `Infinity` and `NaN` (lib-declared, provenance-checked
|
||||
// like every stdlib name): non-finite numLits — `-Infinity` arrives
|
||||
// through the unary-minus literal fold. Arithmetic, comparisons, and
|
||||
// formatting are IEEE-exact in C (String(Infinity) is "Infinity" and
|
||||
// String(NaN) is "NaN" in the number formatter; `NaN !== NaN` holds
|
||||
// because f64 compares are IEEE compares).
|
||||
if (expr.text === "Infinity" || expr.text === "NaN") {
|
||||
const sym = lowerer.checker.getSymbolAtLocation(expr);
|
||||
if (lowerer.isStdlibSymbol(sym)) {
|
||||
return { kind: "numLit", value: expr.text === "NaN" ? NaN : Infinity, type: F64, loc };
|
||||
}
|
||||
}
|
||||
// The primitive constructors as VALUES (`const f = String`, an
|
||||
// option table's `type: Boolean` field, `opt.type === Number`): the
|
||||
// interned coercion closure — one synthesized module function per
|
||||
// constructor per program, so every reference is the SAME zero-
|
||||
// capture closure and `===` is JS identity (the type mapping in
|
||||
// type-mapper.ts pins the one concrete signature `(value: string) =>
|
||||
// primitive`; direct calls `String(x)` never reach here — the call
|
||||
// lowering intercepts them with the wider static coercions).
|
||||
const jsPrimitiveCtorSelector = isJsSourceFile(expr.getSourceFile());
|
||||
const primitiveName = stdlibGlobalNameOf(lowerer, expr);
|
||||
if (
|
||||
primitiveName === "String" ||
|
||||
primitiveName === "Number" ||
|
||||
primitiveName === "Boolean" ||
|
||||
primitiveName === "BigInt"
|
||||
) {
|
||||
return primitiveCtorClosure(lowerer, primitiveName, loc, jsPrimitiveCtorSelector);
|
||||
}
|
||||
if (lowerer.isStdlibGlobal(expr, "ArrayBuffer"))
|
||||
return { kind: "libCall", fn: "arrayBuffer.constructor", args: [], type: DYN, loc };
|
||||
if (isJsSourceFile(expr.getSourceFile()) && lowerer.isStdlibGlobal(expr, "Array"))
|
||||
return { kind: "libCall", fn: "dyn.arrayConstructor", args: [], type: DYN, loc };
|
||||
if (
|
||||
Object.values(BYTES_ELEMENT_NAME).includes(expr.text) &&
|
||||
lowerer.isStdlibSymbol(lowerer.checker.getSymbolAtLocation(expr))
|
||||
) {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "bytes.constructor",
|
||||
args: [{ kind: "strLit", value: expr.text, type: STRING, loc }],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (
|
||||
(expr.text === "encodeURIComponent" ||
|
||||
expr.text === "encodeURI" ||
|
||||
expr.text === "decodeURIComponent" ||
|
||||
expr.text === "decodeURI") &&
|
||||
lowerer.isStdlibGlobal(expr, expr.text)
|
||||
) {
|
||||
const name = `%builtin.${expr.text}`;
|
||||
const type = funcOf([DYN], STRING);
|
||||
if (!lowerer.liftedFns.some((fn) => fn.name === name)) {
|
||||
const value: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "dyn.toStringCoerce",
|
||||
args: [varRef("value", DYN, loc)],
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
const fn =
|
||||
expr.text === "encodeURIComponent"
|
||||
? "str.encodeUriComponent"
|
||||
: expr.text === "encodeURI"
|
||||
? "str.encodeUri"
|
||||
: expr.text === "decodeURI"
|
||||
? "str.decodeUri"
|
||||
: "str.decodeUriComponent";
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: [{ localId: "value", name: "value", type: DYN }],
|
||||
locals: [{ id: "value", name: "value", type: DYN, mutable: false }],
|
||||
returnType: STRING,
|
||||
body: [
|
||||
{
|
||||
kind: "return",
|
||||
value: { kind: "libCall", fn, args: [value], type: STRING, loc },
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return { kind: "closure", fnName: name, captures: [], type, loc };
|
||||
}
|
||||
if (lowerer.isStdlibGlobal(expr, "crypto"))
|
||||
return { kind: "libCall", fn: "crypto.native", args: [], type: DYN, loc };
|
||||
const nativeAlias = stdlibGlobalNameOf(lowerer, expr);
|
||||
if (nativeAlias === "Buffer" && expr.text !== "Buffer") {
|
||||
return { kind: "strLit", value: "[builtin Buffer]", type: STRING, loc };
|
||||
}
|
||||
if (lowerer.isStdlibGlobal(expr, "fetch")) {
|
||||
return { kind: "libCall", fn: "fetch.function", args: [], type: DYN, loc };
|
||||
}
|
||||
// The lib fence's IDENTIFIER chokepoint: the real standard library
|
||||
// resolves names the old minimal ambient world never declared
|
||||
// (Symbol, Reflect, Infinity, Date, ...) — and the adopted
|
||||
// @types/node resolves its whole global surface (Buffer, fetch,
|
||||
// setInterval, URL, ...). Reaching one that no lowering above
|
||||
// claimed is SC2020, never an ICE — EXCEPT in JavaScript sources,
|
||||
// where a stdlib global taken as a VALUE (never called through this
|
||||
// path — call sites lower earlier) becomes an opaque IDENTITY TOKEN:
|
||||
// an interned string naming the global. Identity flows (the
|
||||
// harness's knownGlobals Set, === comparisons) are exact — one
|
||||
// global, one token; what a token cannot do (be called, answer
|
||||
// typeof "function") meets per-site fences/divergences, and
|
||||
// SEMANTICS.md documents the stance.
|
||||
if (stdlibGlobalNameOf(lowerer, expr) === "globalThis") {
|
||||
return lowerGlobalValue(lowerer, expr);
|
||||
}
|
||||
{
|
||||
const sym = lowerer.checker.getSymbolAtLocation(expr);
|
||||
if (lowerer.isStdlibSymbol(sym) || expr.text === "globalThis") {
|
||||
if (isJsSourceFile(expr.getSourceFile())) {
|
||||
const canonical = stdlibGlobalNameOf(lowerer, expr) ?? expr.text;
|
||||
return { kind: "strLit", value: `[builtin ${canonical}]`, type: STRING, loc };
|
||||
}
|
||||
// The families with a WHY: each hint states what makes the
|
||||
// surface genuinely non-static (or what to use instead).
|
||||
const globalHints: Record<string, string | undefined> = {
|
||||
Proxy:
|
||||
"Proxy constructor values have no native lowering; use a direct new Proxy with checked-native plain objects",
|
||||
Reflect:
|
||||
"reflective property access has no static lowering — read and call members directly",
|
||||
Intl: 'native Intl supports default Unicode grapheme segmentation and the composed new Intl.NumberFormat("en-US").format(x); locale negotiation and other ICU-backed operations remain unsupported',
|
||||
SharedArrayBuffer: "use direct new SharedArrayBuffer(length) for fixed shared storage",
|
||||
ArrayBuffer:
|
||||
"ArrayBuffer constructor values have no native lowering; use new ArrayBuffer(length) for fixed-length shared storage",
|
||||
WeakRef:
|
||||
"deref()-after-collect exposes GC timing — genuinely dynamic; hold a strong reference instead",
|
||||
FinalizationRegistry:
|
||||
"finalization callbacks expose GC timing — genuinely dynamic; release resources explicitly instead",
|
||||
eval: "runtime code evaluation cannot be compiled ahead of time",
|
||||
};
|
||||
lowerer.noLowering(expr.text, expr, globalHints[expr.text], sym ?? undefined);
|
||||
}
|
||||
}
|
||||
// An ambient global nothing defines compiles to exactly what Node does
|
||||
// at the access: the catchable ReferenceError "<name> is not defined".
|
||||
// Stdlib/@types/node ambients never reach here (their chokepoint
|
||||
// is above); only user-file declares do.
|
||||
{
|
||||
const sym = lowerer.resolveValueSymbol(expr);
|
||||
const decl = sym ? sym.declarations[0] : undefined;
|
||||
if (
|
||||
decl !== undefined &&
|
||||
ts.isVariableDeclaration(decl) &&
|
||||
decl.initializer === undefined &&
|
||||
ts.getCombinedModifierFlags(decl) & ts.ModifierFlags.Ambient
|
||||
) {
|
||||
const declared = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
||||
if (declared && declared.kind !== "void") {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "global.undefRead",
|
||||
args: [{ kind: "strLit", value: expr.text, type: STRING, loc }],
|
||||
type: declared,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
// An ambient `declare function` nothing defines, taken as a VALUE
|
||||
// (call sites lower earlier, in lowerCall): the same story as the
|
||||
// ambient `declare const` above — Node erases the declaration and
|
||||
// the read throws the catchable ReferenceError at the access.
|
||||
{
|
||||
if (ambientUndefinedFnSymbolOf(lowerer, expr)) {
|
||||
const t = ambientUndefReadType(lowerer, expr);
|
||||
if (t) return nsUndefRead(lowerer, expr.text, expr, t);
|
||||
}
|
||||
}
|
||||
// A read of a TRAP binding — a declaration whose own initializer
|
||||
// provably threw (module init unwound there), so this reference can
|
||||
// never execute: any lowering is sound, and the trap keeps the
|
||||
// shape honest. Typed by the use site when it maps, the F64 dummy
|
||||
// otherwise (never observed — never even reached).
|
||||
{
|
||||
const sym = lowerer.resolveValueSymbol(expr);
|
||||
if (sym !== null && lowerer.trapBindings.has(sym)) {
|
||||
const t =
|
||||
ambientUndefReadType(lowerer, expr) ?? contextualUndefReadType(lowerer, expr) ?? F64;
|
||||
return nsUndefRead(lowerer, expr.text, expr, t);
|
||||
}
|
||||
}
|
||||
// A program CLASS NAME as a value: the classRef over the class's
|
||||
// immortal class object (member accesses and construction never
|
||||
// reach here — their hooks claim the property/new forms first).
|
||||
{
|
||||
const sym = lowerer.resolveValueSymbol(expr);
|
||||
const classInfo = sym ? lowerer.classBySymbol.get(sym) : undefined;
|
||||
// A GENERIC class name whose CONTEXTUAL type pins one instantiation
|
||||
// (`const B: new (v: string) => Counter<string> = Counter`): the
|
||||
// value is that instantiation's class object — the generic-fn
|
||||
// pinning rule on the static side. Unpinned references fall
|
||||
// through to classValueRef's family fence.
|
||||
if (classInfo?.generic) {
|
||||
const ctxT = lowerer.checker.getContextualType(expr);
|
||||
const mapped = ctxT ? lowerer.mapTypeOf(ctxT) : null;
|
||||
const instInfo =
|
||||
mapped?.kind === "classval" ? lowerer.classes.get(mapped.className) : undefined;
|
||||
if (instInfo && instInfo.genericInstance?.family === classInfo) {
|
||||
return lowerer.classValueRef(instInfo, expr);
|
||||
}
|
||||
}
|
||||
if (classInfo) {
|
||||
// A decorated name a replacing decorator can REBIND: the value
|
||||
// is the decoration result — the mutable classval global %init
|
||||
// assigned at the class statement (TC39's name binding), never
|
||||
// the declaration's own class object.
|
||||
const decoratedGlobal = classInfo.classDecorators?.valueGlobalId;
|
||||
if (decoratedGlobal !== undefined) {
|
||||
return {
|
||||
kind: "varRef",
|
||||
localId: decoratedGlobal,
|
||||
type: { kind: "classval", className: classInfo.def.name },
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return lowerer.classValueRef(classInfo, expr);
|
||||
}
|
||||
// The enum OBJECT as a value (member reads never reach here — the
|
||||
// access hooks fold them): iteration, storage, reverse lookups
|
||||
// through variables all land on this pointed fence.
|
||||
if (sym) fenceEnumObjectValue(lowerer, expr, sym);
|
||||
// A MIXIN function as a first-class value: no runtime function
|
||||
// exists — calls instantiate a class per call site
|
||||
// (lower-mixins.ts). Generic mixins took the generic-fn value
|
||||
// fence above; this names the non-generic spelling.
|
||||
if (mixinFnOfCallee(lowerer, expr)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
`the mixin function '${expr.text}' as a value (mixin calls compile per call site — call it directly)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
// A NAMESPACE object as a first-class value: members lower at their
|
||||
// qualified access sites; the object itself has no runtime
|
||||
// representation (ambient namespaces compile to Node's
|
||||
// ReferenceError instead — the object never exists at runtime).
|
||||
{
|
||||
const moduleNs = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
||||
if (moduleNs?.kind === "moduleNs") {
|
||||
return { kind: "moduleNsRef", moduleId: moduleNs.moduleId, type: moduleNs, loc };
|
||||
}
|
||||
const builtinNs = lowerer.builtinNamespaceModuleOf(expr);
|
||||
if (builtinNs === "constants") return builtinConstantsObject(lowerer, loc);
|
||||
if (builtinNs !== null) {
|
||||
const type: IrType = { kind: "moduleNs", moduleId: `builtin:${builtinNs}` };
|
||||
return { kind: "moduleNsRef", moduleId: type.moduleId, type, loc };
|
||||
}
|
||||
const ns = lowerNsIdentifierValue(lowerer, expr);
|
||||
if (ns) return ns;
|
||||
}
|
||||
if (isNavigatorGlobal(lowerer, expr)) {
|
||||
refuseNavigator(
|
||||
lowerer,
|
||||
expr,
|
||||
"the navigator object as a value",
|
||||
lowerer.checker.getSymbolAtLocation(expr),
|
||||
);
|
||||
}
|
||||
// Preflight guarantees no unresolved identifiers; anything else here
|
||||
// is a blocked declaration's binding (the SC2004 cascade) or a
|
||||
// binding form we don't model yet.
|
||||
lowerer.rejectUnresolved(
|
||||
expr,
|
||||
`the reference to '${expr.text}' (a binding form with no lowering)`,
|
||||
);
|
||||
}
|
||||
|
||||
function moduleFileName(lowerer: Lowerer, sf: ts.SourceFile): string {
|
||||
return lowerer.targetPlatform === "wasi"
|
||||
? (wasiGuestPath(sf.fileName) ?? sf.fileName.replace(/\\/g, "/"))
|
||||
: sf.fileName;
|
||||
}
|
||||
|
||||
/** Object.keys/Object.entries require an object receiver, but their first
|
||||
* argument is not syntactically a member receiver. Recognize that one
|
||||
* standard-library position so a runtime-optional loop element can be
|
||||
* checked against the checker-narrowed record arm before builtin lowering. */
|
||||
function runtimeOptionalObjectWalkerArg(lowerer: Lowerer, expr: ts.Expression): boolean {
|
||||
let arg = expr;
|
||||
while (
|
||||
(ts.isParenthesizedExpression(arg.parent) ||
|
||||
ts.isAssertionExpression(arg.parent) ||
|
||||
ts.isSatisfiesExpression(arg.parent) ||
|
||||
ts.isNonNullExpression(arg.parent)) &&
|
||||
arg.parent.expression === arg
|
||||
) {
|
||||
arg = arg.parent;
|
||||
}
|
||||
const call = arg.parent;
|
||||
if (
|
||||
!ts.isCallExpression(call) ||
|
||||
call.arguments.nodes[0] !== arg ||
|
||||
!ts.isPropertyAccessExpression(call.expression)
|
||||
)
|
||||
return false;
|
||||
const access = call.expression;
|
||||
return (
|
||||
(access.name.text === "keys" || access.name.text === "entries") &&
|
||||
lowerer.isStdlibMember(access)
|
||||
);
|
||||
}
|
||||
|
||||
/** A read that resolves to a CJS export-table ACCESSOR property
|
||||
* (`module.exports = { get path() {...}, set path(v) {...} }`): the
|
||||
* getter lifts as a module-level function (module globals need no
|
||||
* captures) — interned per declaration so every read site calls ONE
|
||||
* function — and the read IS the call, Node's per-read evaluation
|
||||
* exactly. Null when the identifier doesn't resolve to such a property
|
||||
* (or it has no getter: a setter-only read answers undefined in Node —
|
||||
* rare enough to keep fenced). */
|
||||
function cjsExportAccessorRead(lowerer: Lowerer, ident: ts.Identifier): IrExpr | null {
|
||||
const symbol = lowerer.resolveValueSymbol(ident);
|
||||
const getter = symbol ? symbol.declarations.find(ts.isGetAccessorDeclaration) : undefined;
|
||||
if (!getter) return null;
|
||||
if (!ts.isObjectLiteralExpression(getter.parent) || !isCjsExportTableLiteral(getter.parent)) {
|
||||
return null;
|
||||
}
|
||||
return cjsAccessorCall(lowerer, getter, locOf(ident));
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,528 @@
|
||||
/* typeof expressions and the typeof tests they lower to. */
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
import {
|
||||
BOOL,
|
||||
CAUGHT,
|
||||
DYN,
|
||||
type IrExpr,
|
||||
type IrStmt,
|
||||
STRING,
|
||||
type SrcLoc,
|
||||
UNDEFINED_T,
|
||||
} from "../../../ir/ir.js";
|
||||
import { lowerWebAssemblyTypeof } from "../builtins/webassembly.js";
|
||||
import { isNavigatorGlobal } from "../builtins/navigator.js";
|
||||
import { lowerPerfHooksTypeof } from "../builtins/performance.js";
|
||||
import { isNodeModuleValue } from "../lower-node-module.js";
|
||||
import { builtinClassRefOf } from "../builtins/class-values.js";
|
||||
import { STRING_INDEX_METHODS, builtinModuleConstOf, stdlibGlobalNameOf } from "../surfaces.js";
|
||||
import {
|
||||
constructorGlobalTypeof,
|
||||
deferredSlotOr,
|
||||
errorRootedObjectOf,
|
||||
lowerPromiseThenPresence,
|
||||
operandPairs,
|
||||
runtimeOptionalStorageOperand,
|
||||
typeofAnswer,
|
||||
} from "../lower-exprs.js";
|
||||
import { tryLowerExpression } from "./try-lower-expression.js";
|
||||
import { isJsSourceFile } from "../../program.js";
|
||||
import { isSafeToDiscard, isSafeToRepeat } from "./evaluation-safety.js";
|
||||
import { varRef } from "../../../ir/build.js";
|
||||
import { isExecExceptionType, refuseExecExceptionCode } from "../builtins/errors.js";
|
||||
import { InternalCompilerError } from "../../../errors.js";
|
||||
|
||||
export function lowerTypeofExpr(lowerer: Lowerer, expr: ts.TypeOfExpression, loc: SrcLoc): IrExpr {
|
||||
const webAssembly = lowerWebAssemblyTypeof(lowerer, expr.expression);
|
||||
if (webAssembly) return webAssembly;
|
||||
if (lowerer.isStdlibGlobal(expr.expression, "process"))
|
||||
return { kind: "strLit", value: "object", type: STRING, loc };
|
||||
if (lowerer.isStdlibGlobal(expr.expression, "fetch"))
|
||||
return { kind: "strLit", value: "function", type: STRING, loc };
|
||||
if (isNavigatorGlobal(lowerer, expr.expression))
|
||||
return { kind: "strLit", value: "object", type: STRING, loc };
|
||||
const performance = lowerPerfHooksTypeof(lowerer, expr.expression);
|
||||
if (performance) return performance;
|
||||
if (isNodeModuleValue(lowerer, expr.expression)) {
|
||||
return { kind: "strLit", value: "object", type: STRING, loc };
|
||||
}
|
||||
// `typeof EventEmitter`, `typeof zlib.Gzip`, `typeof C` over a
|
||||
// builtin class alias.
|
||||
if (builtinClassRefOf(lowerer, expr.expression) !== null) {
|
||||
return { kind: "strLit", value: "function", type: STRING, loc };
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(expr.expression)) {
|
||||
const member = expr.expression;
|
||||
const prototype = member.expression;
|
||||
if (
|
||||
STRING_INDEX_METHODS.has(member.name.text) &&
|
||||
ts.isPropertyAccessExpression(prototype) &&
|
||||
prototype.name.text === "prototype" &&
|
||||
ts.isIdentifier(prototype.expression) &&
|
||||
lowerer.isStdlibGlobal(prototype.expression, "String") &&
|
||||
lowerer.isStdlibMember(member)
|
||||
) {
|
||||
return { kind: "strLit", value: "function", type: STRING, loc };
|
||||
}
|
||||
if (
|
||||
member.name.text === "at" &&
|
||||
ts.isPropertyAccessExpression(prototype) &&
|
||||
prototype.name.text === "prototype" &&
|
||||
ts.isIdentifier(prototype.expression) &&
|
||||
lowerer.isStdlibGlobal(prototype.expression, "Array") &&
|
||||
lowerer.isStdlibMember(member)
|
||||
) {
|
||||
return { kind: "strLit", value: "function", type: STRING, loc };
|
||||
}
|
||||
const presence = lowerPromiseThenPresence(lowerer, expr.expression, {
|
||||
kind: "strLit",
|
||||
value: "function",
|
||||
type: STRING,
|
||||
loc,
|
||||
});
|
||||
if (presence !== null) return presence;
|
||||
}
|
||||
// `typeof queueMicrotask` / `typeof DOMException` on a STDLIB global
|
||||
// whose declared type is callable or constructable: folds to
|
||||
// "function" BEFORE the operand lowers — the identity-token story
|
||||
// (JS files) deliberately represents these values as strings, and
|
||||
// the TS-file fence would name a value the program never needs; an
|
||||
// identifier read has no side effects to preserve. Node's answer for
|
||||
// every function and constructor global is "function" (the harness's
|
||||
// `typeof queueMicrotask === 'function'` probes). Shadowing locals
|
||||
// have non-stdlib symbols and keep the ordinary path.
|
||||
let tested = expr.expression;
|
||||
while (ts.isParenthesizedExpression(tested)) tested = tested.expression;
|
||||
// `typeof zlib.Inflate` / `typeof zlib.createGzip`: an export of a
|
||||
// Node builtin module read through a plain binding (an import or
|
||||
// require result). The read has no side effects, and Node's answer for
|
||||
// every exported function and class is "function".
|
||||
if (
|
||||
ts.isPropertyAccessExpression(tested) &&
|
||||
ts.isIdentifier(tested.expression) &&
|
||||
lowerer.isStdlibMember(tested)
|
||||
) {
|
||||
const sym = lowerer.checker.getSymbolAtLocation(tested.name);
|
||||
const decls = sym ? sym.declarations : [];
|
||||
if (
|
||||
decls.some(isBuiltinModuleValueDecl) &&
|
||||
decls.every((decl) => builtinModuleExportParent(decl) !== undefined)
|
||||
) {
|
||||
const t = lowerer.typeOf(tested);
|
||||
if (
|
||||
lowerer.checker.getCallSignatures(t).length > 0 ||
|
||||
lowerer.checker.getConstructSignatures(t).length > 0
|
||||
) {
|
||||
return { kind: "strLit", value: "function", type: STRING, loc };
|
||||
}
|
||||
}
|
||||
}
|
||||
// `typeof globalThis.AbortSignal` / `typeof Headers`: Node 24's Web
|
||||
// platform constructor globals are functions. JavaScript sources
|
||||
// represent such a value as an identity token, and the fallback
|
||||
// declarations type some (AbortSignal) without a construct signature,
|
||||
// so neither the operand nor the type can answer; the read has no
|
||||
// effects.
|
||||
if (constructorGlobalTypeof(lowerer, tested) !== null)
|
||||
return { kind: "strLit", value: "function", type: STRING, loc };
|
||||
// `typeof globalThis.AbortSignal.any` (ky's feature probe): a static
|
||||
// member of a standard-library global, declared callable, whose value
|
||||
// has no representation. The read has no effects and Node's answer
|
||||
// for every such function is "function"; members that lower as values
|
||||
// keep their own answer.
|
||||
if (ts.isPropertyAccessExpression(tested) && !tested.questionDotToken) {
|
||||
const owner = stdlibGlobalNameOf(lowerer, tested.expression);
|
||||
if (owner !== null && lowerer.isStdlibMember(tested)) {
|
||||
const t = lowerer.typeOf(tested);
|
||||
if (
|
||||
(lowerer.checker.getCallSignatures(t).length > 0 ||
|
||||
lowerer.checker.getConstructSignatures(t).length > 0) &&
|
||||
tryLowerExpression(lowerer, tested) === null
|
||||
)
|
||||
return { kind: "strLit", value: "function", type: STRING, loc };
|
||||
}
|
||||
}
|
||||
if (ts.isIdentifier(tested)) {
|
||||
const sym = lowerer.checker.getSymbolAtLocation(tested);
|
||||
if (!sym) {
|
||||
return { kind: "strLit", value: "undefined", type: STRING, loc };
|
||||
}
|
||||
// `const { inspect } = require("util")` / `import { inspect } from
|
||||
// "node:util"`: a binding of a builtin module's exported function,
|
||||
// class or object. JavaScript sources represent such a value without
|
||||
// its own lowering as an identity token (a string), so the operand's
|
||||
// representation cannot answer. Node's answer follows the export's
|
||||
// kind, and the binding read has no effects.
|
||||
const imported = isJsSourceFile(tested.getSourceFile())
|
||||
? lowerer.builtinImportOf(tested)
|
||||
: null;
|
||||
if (imported && builtinModuleConstOf(lowerer, imported.module, imported.member) === undefined) {
|
||||
const t = lowerer.typeOf(tested);
|
||||
if (
|
||||
lowerer.checker.getCallSignatures(t).length > 0 ||
|
||||
lowerer.checker.getConstructSignatures(t).length > 0
|
||||
) {
|
||||
return { kind: "strLit", value: "function", type: STRING, loc };
|
||||
}
|
||||
if ((t.flags & ts.TypeFlags.Object) !== 0) {
|
||||
return { kind: "strLit", value: "object", type: STRING, loc };
|
||||
}
|
||||
}
|
||||
if (lowerer.isStdlibSymbol(sym)) {
|
||||
const t = lowerer.typeOf(expr.expression);
|
||||
if (
|
||||
lowerer.checker.getCallSignatures(t).length > 0 ||
|
||||
lowerer.checker.getConstructSignatures(t).length > 0
|
||||
) {
|
||||
return { kind: "strLit", value: "function", type: STRING, loc };
|
||||
}
|
||||
}
|
||||
}
|
||||
// A known static result type fixes typeof's answer, but its producer
|
||||
// still evaluates: calls, getters, and checked reads can have effects
|
||||
// or throw. Only trivial operands may disappear.
|
||||
let operand = deferredSlotOr(
|
||||
lowerer,
|
||||
runtimeOptionalStorageOperand(lowerer, expr.expression) ?? lowerer.lowerExpr(expr.expression),
|
||||
);
|
||||
|
||||
// A checker-narrowed dyn read arrives as a VALIDATED extraction
|
||||
// (maybeNarrow's dynCheck bridge). typeof needs no extraction — the
|
||||
// dyn kind table answers the question directly, and answers it even
|
||||
// where the flow type lied (the extraction would throw instead) —
|
||||
// so unwrap back to the dyn value and take the dyn.typeof path.
|
||||
if (operand.kind === "dynCheck" && operand.value.type.kind === "dyn") {
|
||||
operand = operand.value;
|
||||
}
|
||||
const FOLD: Partial<Record<string, string>> = {
|
||||
f64: "number",
|
||||
bigint: "bigint",
|
||||
string: "string",
|
||||
bool: "boolean",
|
||||
func: "function",
|
||||
array: "object",
|
||||
object: "object",
|
||||
record: "object",
|
||||
symbol: "symbol",
|
||||
map: "object",
|
||||
set: "object",
|
||||
promise: "object",
|
||||
bytes: "object",
|
||||
regex: "object",
|
||||
generator: "object",
|
||||
classval: "function",
|
||||
moduleNs: "object",
|
||||
undefinedT: "undefined",
|
||||
nullT: "object",
|
||||
};
|
||||
const folded = FOLD[operand.type.kind];
|
||||
if (folded) {
|
||||
const result: IrExpr = { kind: "strLit", value: folded, type: STRING, loc };
|
||||
return isSafeToDiscard(operand)
|
||||
? result
|
||||
: {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: operand, loc }],
|
||||
result,
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// A union operand: every arm's typeof answer is static, so the value
|
||||
// form is a ternary chain over runtime TAG tests (arms grouped by
|
||||
// answer; the last group needs no test). The operand rides several
|
||||
// tests, so only side-effect-free reads compose — and when every arm
|
||||
// agrees the whole expression folds to that one string (dropping only
|
||||
// the pure read, the trust-the-checker bet lowerUnitComparison makes).
|
||||
// An effectful producer (a getter) evaluates once into a hidden local.
|
||||
const prelude: IrStmt[] = [];
|
||||
if (operand.type.kind === "union" && !isSafeToRepeat(operand)) {
|
||||
const local = lowerer.declareHiddenLocal("%typeofOperand", operand.type);
|
||||
prelude.push({ kind: "varDecl", localId: local.id, init: operand, loc });
|
||||
operand = varRef(local.id, operand.type, loc);
|
||||
}
|
||||
if (operand.type.kind === "union" && isSafeToRepeat(operand)) {
|
||||
const def = lowerer.unions.get(operand.type.unionId);
|
||||
const answers = def?.arms.map(typeofAnswer);
|
||||
if (def && answers && answers.every((s): s is string => s !== null)) {
|
||||
const groups = new Map<string, number[]>();
|
||||
answers.forEach((s, i) => groups.set(s, [...(groups.get(s) ?? []), i]));
|
||||
const ordered = [...groups.entries()];
|
||||
let result: IrExpr = {
|
||||
kind: "strLit",
|
||||
value: ordered[ordered.length - 1]![0],
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
for (const [answer, tags] of ordered.slice(0, -1).reverse()) {
|
||||
let test: IrExpr = {
|
||||
kind: "unionIsTag",
|
||||
unionId: operand.type.unionId,
|
||||
tag: tags[0]!,
|
||||
negated: false,
|
||||
value: operand,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
for (const t of tags.slice(1)) {
|
||||
test = {
|
||||
kind: "logical",
|
||||
op: "||",
|
||||
left: test,
|
||||
right: {
|
||||
kind: "unionIsTag",
|
||||
unionId: operand.type.unionId,
|
||||
tag: t,
|
||||
negated: false,
|
||||
value: operand,
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
result = {
|
||||
kind: "ternary",
|
||||
cond: test,
|
||||
then: { kind: "strLit", value: answer, type: STRING, loc },
|
||||
else_: result,
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return prelude.length > 0
|
||||
? { kind: "seqExpr", stmts: prelude, result, type: STRING, loc }
|
||||
: result;
|
||||
}
|
||||
}
|
||||
if (operand.type.kind === "dyn") {
|
||||
// Bare typeof on a dyn value: the runtime's kind→string table
|
||||
// (null answers "object", boxed closures "function" — JS-exact
|
||||
// for every dyn kind; "bigint"/"symbol" have no producers).
|
||||
return { kind: "libCall", fn: "dyn.typeof", args: [operand], type: STRING, loc };
|
||||
}
|
||||
lowerer.unsupported("SC1090", expr, "typeof expressions on statically-typed values");
|
||||
}
|
||||
|
||||
/** `typeof e.code === "string"` / `"undefined"` (and the `!==` forms)
|
||||
* where e — through parens and as-casts — is a catch binding or a value
|
||||
* of %Error-rooted type: the runtime error's code slot is a string
|
||||
* exactly when present, so the typeof test IS the presence test (the
|
||||
* isErrnoException predicate's third conjunct,
|
||||
* `typeof (err as Record<string, unknown>).code === "string"`). The
|
||||
* as-cast changes the expression's TYPE, never the value — peeled like
|
||||
* lowerProcessStreamProperty's receiver match. Null for every other
|
||||
* shape, so the sibling typeof lowerings keep trying. */
|
||||
export function lowerErrorCodeTypeofTest(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.BinaryExpression,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
const negated = expr.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsEqualsToken;
|
||||
for (const [a, b] of operandPairs(expr)) {
|
||||
if (!ts.isTypeOfExpression(a)) continue;
|
||||
let prop: ts.Expression = a.expression;
|
||||
while (ts.isParenthesizedExpression(prop)) prop = prop.expression;
|
||||
if (!ts.isPropertyAccessExpression(prop) || prop.questionDotToken) continue;
|
||||
if (prop.name.text !== "code") continue;
|
||||
let recv: ts.Expression = prop.expression;
|
||||
while (
|
||||
ts.isParenthesizedExpression(recv) ||
|
||||
ts.isAsExpression(recv) ||
|
||||
ts.isTypeAssertion(recv)
|
||||
)
|
||||
recv = recv.expression;
|
||||
const errT = errorRootedObjectOf(lowerer, lowerer.typeOf(recv));
|
||||
if (!errT) continue;
|
||||
if (isExecExceptionType(lowerer, lowerer.typeOf(recv))) refuseExecExceptionCode(lowerer, prop);
|
||||
const caught = lowerer.caughtLocalOf(recv);
|
||||
let receiver: IrExpr;
|
||||
if (caught) {
|
||||
// The checker proved the Error narrowing (errT above), so the
|
||||
// trusted extraction is sound — caughtRead itself would fence on
|
||||
// the intersection spelling.
|
||||
receiver = {
|
||||
kind: "caughtNarrow",
|
||||
value: { kind: "varRef", localId: caught.id, type: CAUGHT, loc },
|
||||
type: errT,
|
||||
loc,
|
||||
};
|
||||
} else {
|
||||
receiver = lowerer.lowerExpr(recv);
|
||||
if (receiver.type.kind !== "object") continue;
|
||||
}
|
||||
if (!ts.isStringLiteral(b)) continue;
|
||||
if (b.text !== "string" && b.text !== "undefined") continue;
|
||||
const codeType = lowerer.envValueType();
|
||||
if (codeType.kind !== "union")
|
||||
throw new InternalCompilerError("lowerer bug: error code type is not a union");
|
||||
const undefTag = lowerer.armTag(codeType.unionId, UNDEFINED_T);
|
||||
const read: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "error.code",
|
||||
args: [receiver],
|
||||
type: codeType,
|
||||
loc,
|
||||
};
|
||||
// typeof === "string" ⇔ the code slot is present (NOT the undefined
|
||||
// arm); === "undefined" ⇔ absent. `!==` flips either.
|
||||
const isNotUndef = b.text === "string" ? !negated : negated;
|
||||
return {
|
||||
kind: "unionIsTag",
|
||||
unionId: codeType.unionId,
|
||||
tag: undefTag,
|
||||
...(isNotUndef ? { negated: true as const } : {}),
|
||||
value: read,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** `typeof v === "lit"` / `typeof v !== "lit"` where v is `unknown`-typed
|
||||
* (dyn): the runtime kind test over the dyn node's tag. Only the kinds a
|
||||
* later read can bridge narrow ("string"/"number"/"boolean") plus
|
||||
* "undefined" (no payload to read); "object"/"function" tests keep the
|
||||
* fence — an object-narrowed `unknown` read has no lowering anyway (a
|
||||
* checked cast `v as T` is the supported extraction). Null when neither
|
||||
* side is a typeof over a dyn-typed operand (not this pattern). */
|
||||
export function lowerDynTypeofTest(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.BinaryExpression,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
const negated = expr.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsEqualsToken;
|
||||
for (const [a, b] of operandPairs(expr)) {
|
||||
if (!ts.isTypeOfExpression(a)) continue;
|
||||
if (lowerer.isStdlibGlobal(a.expression, "fetch")) continue;
|
||||
// Non-literal comparisons use the ordinary typeof string value,
|
||||
// preserving both operands' evaluation order without a narrowing test.
|
||||
if (!ts.isStringLiteral(b)) continue;
|
||||
const declared = lowerer.mapTypeOf(lowerer.typeOf(a.expression));
|
||||
let value: IrExpr;
|
||||
if (declared?.kind === "dyn") {
|
||||
value = lowerer.lowerExpr(a.expression);
|
||||
if (value.type.kind !== "dyn") continue; // defensive: an already-narrowed use
|
||||
} else if (lowerer.caughtLocalOf(a.expression)) {
|
||||
continue; // the caught lowering's territory (it ran first and declined)
|
||||
} else {
|
||||
// `typeof (value as T).field === "string"` — the raw-object
|
||||
// validation idiom (routes.ts's isValidRoute): the `as` is erasure
|
||||
// in JS, so the READ is the dyn KEYED read of the un-cast value —
|
||||
// a missing key answers undefined and the guard goes false,
|
||||
// Node-exact — never the checked cast (validating T here would
|
||||
// throw on exactly the values the guard exists to reject).
|
||||
const unparen = (e: ts.Expression): ts.Expression => {
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
return e;
|
||||
};
|
||||
let keyed: IrExpr | null = null;
|
||||
const pn = unparen(a.expression);
|
||||
if (ts.isPropertyAccessExpression(pn) && !pn.questionDotToken && ts.isIdentifier(pn.name)) {
|
||||
const recv = unparen(pn.expression);
|
||||
if (ts.isAsExpression(recv) || ts.isTypeAssertion(recv)) {
|
||||
const inner = unparen(recv.expression);
|
||||
const obj = tryLowerExpression(lowerer, inner);
|
||||
if (obj?.type.kind === "dyn") {
|
||||
keyed = {
|
||||
kind: "dynKeyGet",
|
||||
key: { kind: "strLit", value: pn.name.text, type: STRING, loc },
|
||||
value: obj,
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
if (keyed) {
|
||||
value = keyed;
|
||||
} else {
|
||||
// A narrowing residue of `unknown`: a `!== undefined` (or truthy)
|
||||
// guard ahead of the typeof test spells the operand `{}`/`{} |
|
||||
// null` — no useful static mapping, but the READ is still the dyn
|
||||
// value (`if (obj.name !== undefined) { if (typeof obj.name !==
|
||||
// "string") ... }`, the validateConfig idiom). Probe the lowering
|
||||
// and claim exactly the dyn results; anything else keeps its
|
||||
// sibling lowerings and fences untouched.
|
||||
const probed = tryLowerExpression(lowerer, a.expression);
|
||||
if (probed?.type.kind !== "dyn") continue;
|
||||
value = probed;
|
||||
}
|
||||
}
|
||||
if (
|
||||
b.text === "string" ||
|
||||
b.text === "number" ||
|
||||
b.text === "boolean" ||
|
||||
b.text === "undefined" ||
|
||||
b.text === "bigint" ||
|
||||
b.text === "symbol"
|
||||
) {
|
||||
return {
|
||||
kind: "dynTest",
|
||||
test: b.text,
|
||||
...(negated ? { negated: true as const } : {}),
|
||||
value,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// `typeof v === "object"`: the dyn answers exactly (object, array,
|
||||
// bytes, and null kinds — JS's oldest wart preserved). tsc narrows
|
||||
// the branch to `object | null`; reads past the narrow take the
|
||||
// checked-cast path per site.
|
||||
if (b.text === "object") {
|
||||
return {
|
||||
kind: "dynTest",
|
||||
test: "object",
|
||||
...(negated ? { negated: true as const } : {}),
|
||||
value,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// `typeof v === "function"`: a REAL runtime test since the checked-dynamic tree's
|
||||
// function kind exists (boxed closures — the mustCall guard
|
||||
// `if (typeof fn !== 'function') throw` answers honestly).
|
||||
if (b.text === "function") {
|
||||
return {
|
||||
kind: "dynTest",
|
||||
test: "function",
|
||||
...(negated ? { negated: true as const } : {}),
|
||||
value,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// Native checked values do not yet carry symbols.
|
||||
if (b.text === "symbol") {
|
||||
return { kind: "boolLit", value: negated, type: BOOL, loc };
|
||||
}
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
b,
|
||||
`'typeof' tests against "${b.text}" on 'unknown' values (only "string"/"number"/` +
|
||||
`"boolean"/"undefined"/"object" narrow; use a checked cast — 'v as T' — to extract)`,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The `declare module "..."` body (or nested namespace body) a module
|
||||
* export's declaration sits directly in, or undefined. */
|
||||
function builtinModuleExportParent(decl: ts.Node): ts.ModuleBlock | undefined {
|
||||
const parent = ts.isVariableDeclaration(decl) ? decl.parent?.parent?.parent : decl.parent;
|
||||
return parent !== undefined && ts.isModuleBlock(parent) ? parent : undefined;
|
||||
}
|
||||
|
||||
/** A function, class or variable exported by a builtin module's body. */
|
||||
function isBuiltinModuleValueDecl(decl: ts.Node): boolean {
|
||||
return (
|
||||
builtinModuleExportParent(decl) !== undefined &&
|
||||
(ts.isFunctionDeclaration(decl) ||
|
||||
ts.isClassDeclaration(decl) ||
|
||||
ts.isVariableDeclaration(decl))
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,955 @@
|
||||
/* Increment, decrement and prefix unary operators, and compound field updates. */
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
import {
|
||||
type FieldTarget,
|
||||
bigintCompoundValue,
|
||||
compoundUnsupported,
|
||||
concatOperand,
|
||||
deferredSlotOr,
|
||||
dynCompoundValue,
|
||||
lowerOptionalNumber,
|
||||
operatorUnsupported,
|
||||
runtimeOptionalStorageOperand,
|
||||
symbolFieldTarget,
|
||||
} from "../lower-exprs.js";
|
||||
import {
|
||||
BIGINT_T,
|
||||
BOOL,
|
||||
DYN,
|
||||
F64,
|
||||
type IrExpr,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
STRING,
|
||||
type SrcLoc,
|
||||
UNDEFINED_T,
|
||||
VOID,
|
||||
} from "../../../ir/ir.js";
|
||||
import { isJsSourceFile, locOf } from "../../program.js";
|
||||
import { classToNumberConversion } from "../class-to-primitive.js";
|
||||
import { dynUndefinedExpr, nodeThrowExpr, numLit, varRef } from "../../../ir/build.js";
|
||||
import { fenceNodeModuleMutation } from "../lower-node-module.js";
|
||||
import { staticFieldWriteTarget } from "../lower-classes.js";
|
||||
import { expandoWritableTarget } from "../lower-expando.js";
|
||||
import { nsWritableTarget } from "../lower-namespaces.js";
|
||||
import { arrayNumberRead, arrayValueRead, arrayValueStore } from "../array-values.js";
|
||||
import { isSafeToRepeat } from "./evaluation-safety.js";
|
||||
import { errorPropertyWrite } from "../error-methods.js";
|
||||
import type { CompoundOp } from "../surfaces.js";
|
||||
import { tryLowerExpression } from "./try-lower-expression.js";
|
||||
import { hasRuntimeStatics } from "../class-runtime-statics.js";
|
||||
import { declareContextLocal } from "../function-context.js";
|
||||
|
||||
/** Read-modify-write updates read the member first: an unassigned
|
||||
* deferred-init receiver throws Node's member-read TypeError. */
|
||||
function checkDeferredMemberReceiver(
|
||||
lowerer: Lowerer,
|
||||
target: FieldTarget,
|
||||
access: ts.Expression,
|
||||
): void {
|
||||
if (
|
||||
target.container !== "class" ||
|
||||
!ts.isPropertyAccessExpression(access) ||
|
||||
target.obj.type.kind !== "object" ||
|
||||
lowerer.deferredFieldSlot(target.obj) === null
|
||||
)
|
||||
return;
|
||||
target.obj =
|
||||
lowerer.runtimeOptionalPropertyReceiver(
|
||||
access.expression,
|
||||
target.obj,
|
||||
target.obj.type,
|
||||
target.field,
|
||||
) ?? target.obj;
|
||||
}
|
||||
|
||||
/** Whether evaluating `expr` cannot store anything or run other code:
|
||||
* reads, arithmetic, tests and checked extractions (which at most throw). */
|
||||
function writeFree(expr: IrExpr): boolean {
|
||||
switch (expr.kind) {
|
||||
case "numLit":
|
||||
case "strLit":
|
||||
case "boolLit":
|
||||
case "unitLit":
|
||||
case "varRef":
|
||||
return true;
|
||||
case "fieldGet":
|
||||
case "recordGet":
|
||||
return writeFree(expr.obj);
|
||||
case "bin":
|
||||
case "logical":
|
||||
return writeFree(expr.left) && writeFree(expr.right);
|
||||
case "unary":
|
||||
case "toBool":
|
||||
return writeFree(expr.operand);
|
||||
case "ternary":
|
||||
return writeFree(expr.cond) && writeFree(expr.then) && writeFree(expr.else_);
|
||||
case "unionIsTag":
|
||||
case "unionNarrow":
|
||||
case "unionWrap":
|
||||
return writeFree(expr.value);
|
||||
case "libCall":
|
||||
return expr.fn === "error.nodeThrow";
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpression): IrExpr {
|
||||
const loc = locOf(expr);
|
||||
switch (expr.operator) {
|
||||
case ts.SyntaxKind.MinusToken: {
|
||||
const raw = lowerer.lowerExpr(expr.operand);
|
||||
|
||||
if (raw.type.kind === "bigint") {
|
||||
return { kind: "libCall", fn: "bigint.neg", args: [raw], type: BIGINT_T, loc };
|
||||
}
|
||||
// Match the checked numeric boundary used by JS binary arithmetic.
|
||||
const operand =
|
||||
raw.type.kind === "dyn" && isJsSourceFile(expr.getSourceFile())
|
||||
? { kind: "dynCheck" as const, value: raw, type: F64, loc }
|
||||
: lowerOptionalNumber(lowerer, raw, loc, expr.operand);
|
||||
if (operand.type.kind !== "f64") operatorUnsupported(lowerer, expr, "-", [raw.type]);
|
||||
if (operand.kind === "numLit") return { ...operand, value: -operand.value, loc };
|
||||
return { kind: "unary", op: "-", operand, type: F64, loc };
|
||||
}
|
||||
case ts.SyntaxKind.PlusToken: {
|
||||
const raw = lowerer.lowerExpr(expr.operand);
|
||||
if (raw.type.kind === "dyn") {
|
||||
return { kind: "libCall", fn: "dyn.toNumberCoerce", args: [raw], type: F64, loc };
|
||||
}
|
||||
// Unary + is ToNumber; on an already-number operand it's identity,
|
||||
// and a STRING operand runs the runtime's ECMA-exact StringToNumber
|
||||
// (num.fromString — Number(aString)'s lowering, scr_string.c).
|
||||
|
||||
if (raw.type.kind === "string") {
|
||||
return { kind: "libCall", fn: "num.fromString", args: [raw], type: F64, loc };
|
||||
}
|
||||
const classNumber = classToNumberConversion(lowerer, raw, expr.operand, "unary plus");
|
||||
if (classNumber) return classNumber;
|
||||
const operand = lowerOptionalNumber(lowerer, raw, loc, expr.operand);
|
||||
if (operand.type.kind !== "f64") operatorUnsupported(lowerer, expr, "+", [raw.type]);
|
||||
return operand;
|
||||
}
|
||||
case ts.SyntaxKind.ExclamationToken: {
|
||||
// `!x` is ToBoolean-then-negate: f64/string operands go through toBool.
|
||||
const operand = lowerer.ensureBool(
|
||||
deferredSlotOr(
|
||||
lowerer,
|
||||
runtimeOptionalStorageOperand(lowerer, expr.operand) ?? lowerer.lowerExpr(expr.operand),
|
||||
),
|
||||
expr.operand,
|
||||
);
|
||||
return { kind: "unary", op: "!", operand, type: BOOL, loc };
|
||||
}
|
||||
case ts.SyntaxKind.TildeToken: {
|
||||
// `~x`: ToInt32, complement, back to f64 (JS-exact, incl. NaN → -1).
|
||||
const raw = lowerer.lowerExpr(expr.operand);
|
||||
if (raw.type.kind === "bigint") {
|
||||
return { kind: "libCall", fn: "bigint.not", args: [raw], type: BIGINT_T, loc };
|
||||
}
|
||||
if (raw.type.kind === "dyn")
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "dyn.bitwise",
|
||||
args: [raw, dynUndefinedExpr(loc), { kind: "strLit", value: "~", type: STRING, loc }],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
// A checked-dynamic value retains enough JavaScript structure to
|
||||
// run exact ToNumber, including the object valueOf/toString
|
||||
// protocol. This is the static `any`/JS-lane answer for idioms
|
||||
// such as `while (~index)`, where inference may have widened an
|
||||
// otherwise numeric slot to the checked-dynamic tree.
|
||||
const operand = lowerOptionalNumber(lowerer, raw, loc, expr.operand);
|
||||
if (operand.type.kind !== "f64") operatorUnsupported(lowerer, expr, "~", [raw.type]);
|
||||
return { kind: "unary", op: "~", operand, type: F64, loc };
|
||||
}
|
||||
case ts.SyntaxKind.PlusPlusToken:
|
||||
case ts.SyntaxKind.MinusMinusToken:
|
||||
// `++x` / `--x` in expression position: yields the NEW value.
|
||||
return lowerIncDec(lowerer, expr, true);
|
||||
}
|
||||
lowerer.unsupported("SC1090", expr, `syntax '${ts.syntaxKindName(expr.kind)}'`);
|
||||
}
|
||||
|
||||
/** Member updates evaluate the reference once, convert its old value with
|
||||
* ToNumeric, store the new value, then yield the old or new numeric value. */
|
||||
export function lowerIncDec(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.PrefixUnaryExpression | ts.PostfixUnaryExpression,
|
||||
prefix: boolean,
|
||||
): IrExpr {
|
||||
const loc = locOf(expr);
|
||||
const op = expr.operator === ts.SyntaxKind.PlusPlusToken ? "+" : "-";
|
||||
if (!ts.isIdentifier(expr.operand)) {
|
||||
const access = expr.operand;
|
||||
if (
|
||||
(!ts.isPropertyAccessExpression(access) && !ts.isElementAccessExpression(access)) ||
|
||||
access.questionDotToken
|
||||
) {
|
||||
lowerer.unsupported("SC1045", expr, "increment/decrement of this target");
|
||||
}
|
||||
fenceNodeModuleMutation(lowerer, access, "assignment");
|
||||
if (ts.isPropertyAccessExpression(access)) {
|
||||
const target =
|
||||
staticFieldWriteTarget(lowerer, access) ??
|
||||
expandoWritableTarget(lowerer, access) ??
|
||||
nsWritableTarget(lowerer, access);
|
||||
if (target) {
|
||||
return lowerIncDecTarget(lowerer, expr, target, prefix);
|
||||
}
|
||||
}
|
||||
const stmts: IrStmt[] = [];
|
||||
const save = (value: IrExpr, name: string): IrExpr => {
|
||||
const local = lowerer.declareHiddenLocal(name, value.type);
|
||||
stmts.push({ kind: "varDecl", localId: local.id, init: value, loc });
|
||||
return varRef(local.id, value.type, loc);
|
||||
};
|
||||
const finish = (read: IrExpr, write: (value: IrExpr) => IrStmt): IrExpr => {
|
||||
if (read.type.kind === "bigint") {
|
||||
const old = save(read, "%incrementOld");
|
||||
const next = save(bigintStep(op, old, loc), "%incrementNext");
|
||||
stmts.push(write(next));
|
||||
const result = prefix ? next : old;
|
||||
return { kind: "seqExpr", stmts, result, type: result.type, loc };
|
||||
}
|
||||
const old = save(
|
||||
read.type.kind === "f64"
|
||||
? read
|
||||
: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.toNumeric",
|
||||
args: [lowerer.coerceInto(access, read, DYN)],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
"%incrementOld",
|
||||
);
|
||||
const next = save(
|
||||
old.type.kind === "f64"
|
||||
? {
|
||||
kind: "bin",
|
||||
op,
|
||||
left: old,
|
||||
right: { kind: "numLit", value: 1, type: F64, loc },
|
||||
type: F64,
|
||||
loc,
|
||||
}
|
||||
: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.increment",
|
||||
args: [old, { kind: "boolLit", value: op === "+", type: BOOL, loc }],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
"%incrementNext",
|
||||
);
|
||||
stmts.push(write(next));
|
||||
const result = prefix ? next : old;
|
||||
return { kind: "seqExpr", stmts, result, type: result.type, loc };
|
||||
};
|
||||
const raw = lowerer.lowerExpr(access.expression);
|
||||
// Keep native array/byte storage: boxing a native array can create a
|
||||
// checked view, which is not a substitute for updating its slot.
|
||||
if (
|
||||
ts.isElementAccessExpression(access) &&
|
||||
(raw.type.kind === "array" || raw.type.kind === "bytes")
|
||||
) {
|
||||
const receiver = save(raw, "%incrementArray");
|
||||
const rawIndex = lowerer.lowerExpr(access.argumentExpression);
|
||||
const index = save(
|
||||
rawIndex.type.kind === "dyn" && isJsSourceFile(expr.getSourceFile())
|
||||
? { kind: "libCall", fn: "dyn.toNumberCoerce", args: [rawIndex], type: F64, loc }
|
||||
: rawIndex,
|
||||
"%incrementIndex",
|
||||
);
|
||||
if (index.type.kind !== "f64")
|
||||
lowerer.unsupported("SC1090", access, "incrementing non-number array keys");
|
||||
const type = raw.type;
|
||||
const read: IrExpr =
|
||||
type.kind === "array"
|
||||
? type.elem.kind === "f64"
|
||||
? arrayNumberRead(receiver, index, loc)
|
||||
: arrayValueRead(lowerer, receiver, index, type.elem, loc)
|
||||
: {
|
||||
kind: "bytesIntrinsic",
|
||||
method: "get",
|
||||
receiver,
|
||||
args: [index],
|
||||
type: F64,
|
||||
loc,
|
||||
invalidNaN: true,
|
||||
};
|
||||
return finish(read, (value) =>
|
||||
type.kind === "array"
|
||||
? arrayValueStore(
|
||||
lowerer,
|
||||
receiver,
|
||||
index,
|
||||
lowerer.coerceInto(access, value, type.elem),
|
||||
type.elem,
|
||||
loc,
|
||||
)
|
||||
: {
|
||||
kind: "bytesSet",
|
||||
arr: receiver,
|
||||
index,
|
||||
value: lowerer.coerceInto(access, value, F64),
|
||||
loc,
|
||||
},
|
||||
);
|
||||
}
|
||||
// A TS member statically typed number on a checked-dynamic receiver (a
|
||||
// class-backed protocol view): the expanded form `o.v = o.v + 1`, its
|
||||
// read checked into number like any other read of the member.
|
||||
const checkedNumber =
|
||||
!isJsSourceFile(expr.getSourceFile()) &&
|
||||
raw.type.kind === "dyn" &&
|
||||
lowerer.mapTypeOf(lowerer.typeOf(access))?.kind === "f64";
|
||||
if ((isJsSourceFile(expr.getSourceFile()) || checkedNumber) && raw.type.kind === "dyn") {
|
||||
const receiver = save(raw, "%incrementReceiver");
|
||||
const key = save(
|
||||
ts.isElementAccessExpression(access)
|
||||
? lowerer.lowerExprExpecting(access.argumentExpression, DYN)
|
||||
: lowerer.coerceInto(
|
||||
access,
|
||||
{ kind: "strLit", value: access.name.text, type: STRING, loc },
|
||||
DYN,
|
||||
),
|
||||
"%incrementKey",
|
||||
);
|
||||
const read: IrExpr = { kind: "dynKeyGet", value: receiver, key, type: DYN, loc };
|
||||
return finish(checkedNumber ? lowerer.coerceInto(access, read, F64) : read, (value) => ({
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.keySetComputed",
|
||||
args: [receiver, key, lowerer.coerceInto(access, value, DYN)],
|
||||
type: VOID,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
}));
|
||||
}
|
||||
const target = ts.isPropertyAccessExpression(access)
|
||||
? lowerer.fieldTarget(access)
|
||||
: symbolFieldTarget(lowerer, access);
|
||||
if (target && lowerer.dynConvertible(target.fieldType)) {
|
||||
checkDeferredMemberReceiver(lowerer, target, access);
|
||||
target.obj = save(target.obj, "%incrementReceiver");
|
||||
return finish(lowerer.fieldGetExpr(target, loc, access), (value) =>
|
||||
lowerer.fieldSetStmt(
|
||||
target,
|
||||
lowerer.coerceInto(access, value, target.fieldType),
|
||||
loc,
|
||||
access,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (isJsSourceFile(expr.getSourceFile()) && lowerer.dynConvertible(raw.type)) {
|
||||
const receiver = save(lowerer.coerceInto(access.expression, raw, DYN), "%incrementReceiver");
|
||||
const key = save(
|
||||
ts.isElementAccessExpression(access)
|
||||
? lowerer.lowerExprExpecting(access.argumentExpression, DYN)
|
||||
: lowerer.coerceInto(
|
||||
access,
|
||||
{ kind: "strLit", value: access.name.text, type: STRING, loc },
|
||||
DYN,
|
||||
),
|
||||
"%incrementKey",
|
||||
);
|
||||
return finish({ kind: "dynKeyGet", value: receiver, key, type: DYN, loc }, (value) => ({
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.keySetComputed",
|
||||
args: [receiver, key, lowerer.coerceInto(access, value, DYN)],
|
||||
type: VOID,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
}));
|
||||
}
|
||||
lowerer.unsupported("SC1045", expr, "increment/decrement of this member target");
|
||||
}
|
||||
const target = lowerer.resolveWritable(expr.operand);
|
||||
if (!target) {
|
||||
lowerer.rejectUnresolved(
|
||||
expr.operand,
|
||||
`increment/decrement of '${expr.operand.text}' (not a writable local or module global)`,
|
||||
);
|
||||
}
|
||||
return lowerIncDecTarget(lowerer, expr, target, prefix);
|
||||
}
|
||||
|
||||
/** Numeric updates read the represented slot once and write the computed
|
||||
* value back in that same representation. In particular, an unchecked
|
||||
* indexed initializer can leave undefined in an otherwise numeric binding. */
|
||||
export function lowerIncDecTarget(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.PrefixUnaryExpression | ts.PostfixUnaryExpression,
|
||||
target: { id: string; type: IrType },
|
||||
prefix: boolean,
|
||||
): IrExpr {
|
||||
const loc = locOf(expr);
|
||||
const op = expr.operator === ts.SyntaxKind.PlusPlusToken ? "+" : "-";
|
||||
if (target.type.kind === "dyn") {
|
||||
const old = lowerer.declareHiddenLocal("%numericOld", DYN);
|
||||
const next = lowerer.declareHiddenLocal("%numericNext", DYN);
|
||||
const read = varRef(target.id, target.type, loc);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: old.id,
|
||||
init: { kind: "libCall", fn: "dyn.toNumeric", args: [read], type: DYN, loc },
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: next.id,
|
||||
init: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.increment",
|
||||
args: [
|
||||
varRef(old.id, DYN, loc),
|
||||
{ kind: "boolLit", value: op === "+", type: BOOL, loc },
|
||||
],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
{ kind: "assign", localId: target.id, value: varRef(next.id, DYN, loc), loc },
|
||||
],
|
||||
result: varRef(prefix ? next.id : old.id, DYN, loc),
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (target.type.kind === "union") {
|
||||
const read = lowerOptionalNumber(
|
||||
lowerer,
|
||||
varRef(target.id, target.type, loc),
|
||||
loc,
|
||||
expr.operand,
|
||||
);
|
||||
if (read.type.kind === "f64") {
|
||||
const old = lowerer.declareHiddenLocal("%numericOld", F64);
|
||||
const next = lowerer.declareHiddenLocal("%numericNext", F64);
|
||||
const result = varRef(prefix ? next.id : old.id, F64, loc);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{ kind: "varDecl", localId: old.id, init: read, loc },
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: next.id,
|
||||
init: {
|
||||
kind: "bin",
|
||||
op,
|
||||
left: varRef(old.id, F64, loc),
|
||||
right: numLit(1, loc),
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "assign",
|
||||
localId: target.id,
|
||||
value: lowerer.coerceInto(expr.operand, varRef(next.id, F64, loc), target.type),
|
||||
loc,
|
||||
},
|
||||
],
|
||||
result,
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (target.type.kind === "bigint") {
|
||||
const old = lowerer.declareHiddenLocal("%bigintOld", BIGINT_T);
|
||||
const next = lowerer.declareHiddenLocal("%bigintNext", BIGINT_T);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{ kind: "varDecl", localId: old.id, init: varRef(target.id, BIGINT_T, loc), loc },
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: next.id,
|
||||
init: bigintStep(op, varRef(old.id, BIGINT_T, loc), loc),
|
||||
loc,
|
||||
},
|
||||
{ kind: "assign", localId: target.id, value: varRef(next.id, BIGINT_T, loc), loc },
|
||||
],
|
||||
result: varRef(prefix ? next.id : old.id, BIGINT_T, loc),
|
||||
type: BIGINT_T,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (target.type.kind !== "f64")
|
||||
operatorUnsupported(lowerer, expr, op === "+" ? "++" : "--", [target.type]);
|
||||
return { kind: "incDec", op, prefix, localId: target.id, type: F64, loc };
|
||||
}
|
||||
|
||||
/** `++`/`--` on a bigint value: old ± 1n. */
|
||||
function bigintStep(op: "+" | "-", old: IrExpr, loc: SrcLoc): IrExpr {
|
||||
const one: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "bigint.parse",
|
||||
args: [{ kind: "strLit", value: "1", type: STRING, loc }],
|
||||
type: BIGINT_T,
|
||||
loc,
|
||||
};
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: op === "+" ? "bigint.add" : "bigint.sub",
|
||||
args: [old, one],
|
||||
type: BIGINT_T,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** The write statement for a field target (fieldSet / recordSet / setter
|
||||
* call). A write to a getter-only property never gets here in a clean
|
||||
* program (tsc's TS2540 is the fence); the rejection is the backstop. */
|
||||
export function fieldSetStmt(
|
||||
lowerer: Lowerer,
|
||||
target: FieldTarget,
|
||||
value: IrExpr,
|
||||
loc: SrcLoc,
|
||||
blame: ts.Node,
|
||||
): IrStmt {
|
||||
// A member write through an unassigned deferred-init field: the value
|
||||
// still evaluates first, then the write throws Node's TypeError.
|
||||
const deferredSlot =
|
||||
target.container === "class" &&
|
||||
!(
|
||||
(target.field === "message" || target.field === "name") &&
|
||||
(target.className === "%Error" || lowerer.isSubclassOf(target.className, "%Error"))
|
||||
)
|
||||
? lowerer.deferredFieldSlot(target.obj)
|
||||
: null;
|
||||
if (target.container === "class" && deferredSlot?.type.kind === "union") {
|
||||
const unionId = deferredSlot.type.unionId;
|
||||
const undefTag = lowerer.armTag(unionId, UNDEFINED_T);
|
||||
const presentTag = lowerer.armTag(unionId, target.obj.type);
|
||||
if (undefTag >= 0 && presentTag >= 0) {
|
||||
// A value that cannot write anything leaves the field unchanged, so
|
||||
// the field is read after it (no snapshot of the slot).
|
||||
const late = isSafeToRepeat(deferredSlot) && writeFree(value);
|
||||
const receiver = late
|
||||
? null
|
||||
: lowerer.declareHiddenLocal("%deferredWriteObject", deferredSlot.type);
|
||||
const stored = lowerer.declareHiddenLocal("%deferredWriteValue", value.type);
|
||||
const receiverRef = receiver ? varRef(receiver.id, receiver.type, loc) : deferredSlot;
|
||||
const result = varRef(stored.id, stored.type, loc);
|
||||
const write: IrStmt = {
|
||||
kind: "fieldSet",
|
||||
obj: {
|
||||
kind: "unionNarrow",
|
||||
unionId,
|
||||
tag: presentTag,
|
||||
value: receiverRef,
|
||||
type: target.obj.type,
|
||||
loc,
|
||||
},
|
||||
className: target.className,
|
||||
field: target.field,
|
||||
value: result,
|
||||
loc,
|
||||
};
|
||||
return {
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
...(receiver
|
||||
? [{ kind: "varDecl" as const, localId: receiver.id, init: deferredSlot, loc }]
|
||||
: []),
|
||||
{ kind: "varDecl", localId: stored.id, init: value, loc },
|
||||
],
|
||||
result: {
|
||||
kind: "ternary",
|
||||
cond: {
|
||||
kind: "unionIsTag",
|
||||
unionId,
|
||||
tag: undefTag,
|
||||
value: receiverRef,
|
||||
negated: false,
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: nodeThrowExpr(
|
||||
1,
|
||||
"",
|
||||
`Cannot set properties of undefined (setting '${target.field}')`,
|
||||
result.type,
|
||||
loc,
|
||||
),
|
||||
else_: { kind: "seqExpr", stmts: [write], result, type: result.type, loc },
|
||||
type: result.type,
|
||||
loc,
|
||||
},
|
||||
type: result.type,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (target.container === "dynamic")
|
||||
return {
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.keySet",
|
||||
args: [
|
||||
target.obj,
|
||||
{ kind: "strLit", value: target.field, type: STRING, loc },
|
||||
lowerer.coerceToExpected(value, DYN),
|
||||
],
|
||||
type: VOID,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
};
|
||||
if (
|
||||
target.container === "class" &&
|
||||
(target.field === "message" || target.field === "name") &&
|
||||
(target.className === "%Error" || lowerer.isSubclassOf(target.className, "%Error"))
|
||||
)
|
||||
return errorPropertyWrite(lowerer, target.obj, value, target.field);
|
||||
if (target.container === "errorStackLimit") {
|
||||
return {
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "libCall", fn: "error.stackLimitSet", args: [value], type: VOID, loc },
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (target.container === "errorCause") {
|
||||
return {
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "libCall", fn: "error.setCause", args: [target.obj, value], type: VOID, loc },
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// A record-shaped CHECKER target whose receiver VALUE lives in the checked-dynamic tree:
|
||||
// the checked-dynamic keyed write — dyn.keySet (later writes win,
|
||||
// insertion order; Node's TypeErrors on non-object receivers), the
|
||||
// value converting into the checked-dynamic tree.
|
||||
if (
|
||||
(target.container === "record" || target.container === "recordOvf") &&
|
||||
target.obj.type.kind === "dyn"
|
||||
) {
|
||||
const v = lowerer.coerceToExpected(value, DYN);
|
||||
if (v.type.kind !== "dyn") {
|
||||
lowerer.unsupported(
|
||||
"SC1101",
|
||||
blame,
|
||||
`storing '${lowerer.fmt(value.type)}' values in a checked-dynamic object (the value cannot convert into the checked-dynamic tree)`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.keySet",
|
||||
args: [target.obj, { kind: "strLit", value: target.field, type: STRING, loc }, v],
|
||||
type: VOID,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (target.container === "accessor") {
|
||||
const setF = lowerer.findMethodOn(
|
||||
lowerer.classes.get(target.className) ?? null,
|
||||
`set:${target.field}`,
|
||||
);
|
||||
if (!setF)
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
blame,
|
||||
`assignment to the getter-only property '${target.field}'`,
|
||||
);
|
||||
return {
|
||||
kind: "exprStmt",
|
||||
expr: lowerer.accessorCall(
|
||||
target.className,
|
||||
`set:${target.field}`,
|
||||
target.obj,
|
||||
[value],
|
||||
VOID,
|
||||
loc,
|
||||
),
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// Record accessor properties: the write calls the %set: closure with
|
||||
// the coerced value. A getter-only write never gets here in a clean
|
||||
// program (tsc's TS2540); the rejection is the backstop.
|
||||
if (target.container === "recordAccessor") {
|
||||
if (!target.setType) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
blame,
|
||||
`assignment to the getter-only property '${target.field}'`,
|
||||
);
|
||||
}
|
||||
const closure: IrExpr = {
|
||||
kind: "recordGet",
|
||||
obj: target.obj,
|
||||
shapeId: target.shapeId,
|
||||
field: `%set:${target.field}`,
|
||||
type: target.setType,
|
||||
loc,
|
||||
};
|
||||
return {
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "callValue", callee: closure, args: [value], type: VOID, loc },
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// Overflow dot writes: the bracket write with a literal key — a pure
|
||||
// overflow insert (recordKeySet, overflowOnly: the name declares no
|
||||
// field, so no declared collision exists to validate). The value was
|
||||
// coerced into the index-value slot type by the caller.
|
||||
if (target.container === "recordOvf") {
|
||||
return {
|
||||
kind: "recordKeySet",
|
||||
obj: target.obj,
|
||||
shapeId: target.shapeId,
|
||||
key: { kind: "strLit", value: target.field, type: STRING, loc },
|
||||
value,
|
||||
overflowOnly: true,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return target.container === "class"
|
||||
? {
|
||||
kind: "fieldSet",
|
||||
obj: target.obj,
|
||||
className: target.className,
|
||||
field: target.field,
|
||||
value,
|
||||
loc,
|
||||
}
|
||||
: {
|
||||
kind: "recordSet",
|
||||
obj: target.obj,
|
||||
shapeId: target.shapeId,
|
||||
field: target.field,
|
||||
value,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
export function lowerFieldCompoundValue(
|
||||
lowerer: Lowerer,
|
||||
access: ts.PropertyAccessExpression | ts.ElementAccessExpression,
|
||||
op: CompoundOp,
|
||||
rhsNode: ts.Expression | null,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
if (access.expression.kind === ts.SyntaxKind.SuperKeyword) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
access,
|
||||
"compound assignment through 'super' (read and write separately)",
|
||||
);
|
||||
}
|
||||
const body: IrStmt[] = [];
|
||||
const save = (value: IrExpr, name: string): IrExpr => {
|
||||
const local = lowerer.declareHiddenLocal(name, value.type);
|
||||
body.push({ kind: "varDecl", localId: local.id, init: value, loc: value.loc });
|
||||
return varRef(local.id, value.type, value.loc);
|
||||
};
|
||||
// A CHECKED-DYNAMIC receiver (`context.actual++` — test/common's call
|
||||
// accounting; dot spelling only — symbol-keyed element targets are
|
||||
// static fields): read the member (dynKeyGet), VALIDATE it as a
|
||||
// number (dynCheck — a non-number member throws the catchable
|
||||
// TypeError where JS would ToNumber-coerce; it throws instead of
|
||||
// producing NaN — SEMANTICS.md), combine, write back (dyn.keySet). The
|
||||
// receiver and member read are saved before evaluating the RHS.
|
||||
if (ts.isPropertyAccessExpression(access)) {
|
||||
let probed = tryLowerExpression(lowerer, access.expression);
|
||||
const staticClass =
|
||||
probed?.type.kind === "classval" ? lowerer.classes.get(probed.type.className) : undefined;
|
||||
if (probed && staticClass && hasRuntimeStatics(staticClass))
|
||||
probed = lowerer.coerceToExpected(probed, DYN);
|
||||
if (probed?.type.kind === "dyn") {
|
||||
const receiver = save(probed, "%compoundReceiver");
|
||||
const key: IrExpr = {
|
||||
kind: "strLit",
|
||||
value: access.name.text,
|
||||
type: STRING,
|
||||
loc: locOf(access.name),
|
||||
};
|
||||
const read = save(
|
||||
{ kind: "dynKeyGet", key, value: receiver, type: DYN, loc },
|
||||
"%compoundOld",
|
||||
);
|
||||
const cur: IrExpr = { kind: "dynCheck", value: read, type: F64, loc };
|
||||
const rhs = save(
|
||||
rhsNode ? lowerer.lowerExpr(rhsNode) : { kind: "numLit", value: 1, type: F64, loc },
|
||||
"%compoundRhs",
|
||||
);
|
||||
if (isJsSourceFile(access.getSourceFile()) && lowerer.dynConvertible(rhs.type)) {
|
||||
const numeric = ["-", "*", "/", "%", "**"].includes(op);
|
||||
const right = lowerer.coerceToExpected(rhs, DYN);
|
||||
const value = save(
|
||||
{
|
||||
kind: "libCall",
|
||||
fn: op === "+" ? "dyn.add" : numeric ? "dyn.arithmetic" : "dyn.bitwise",
|
||||
args:
|
||||
op === "+"
|
||||
? [read, right]
|
||||
: [read, right, { kind: "strLit", value: op, type: STRING, loc }],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
"%compoundResult",
|
||||
);
|
||||
body.push({
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.keySet",
|
||||
args: [receiver, key, value],
|
||||
type: VOID,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
});
|
||||
return { kind: "seqExpr", stmts: body, result: value, type: DYN, loc };
|
||||
}
|
||||
const numericRhs: IrExpr =
|
||||
rhs.type.kind === "dyn"
|
||||
? { kind: "dynCheck", value: rhs, type: F64, loc: rhs.loc }
|
||||
: lowerOptionalNumber(lowerer, rhs, loc);
|
||||
if (numericRhs.type.kind !== "f64")
|
||||
compoundUnsupported(lowerer, access, op, F64, numericRhs.type);
|
||||
const value = save(
|
||||
{ kind: "bin", op, left: cur, right: numericRhs, type: F64, loc },
|
||||
"%compoundResult",
|
||||
);
|
||||
const boxed: IrExpr = { kind: "dynFrom", value, type: DYN, loc };
|
||||
body.push({
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.keySet",
|
||||
args: [receiver, { ...key }, boxed],
|
||||
type: VOID,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
});
|
||||
return { kind: "seqExpr", stmts: body, result: value, type: value.type, loc };
|
||||
}
|
||||
}
|
||||
const targetOf = (): FieldTarget | null =>
|
||||
ts.isPropertyAccessExpression(access)
|
||||
? lowerer.fieldTarget(access)
|
||||
: symbolFieldTarget(lowerer, access);
|
||||
const target = targetOf();
|
||||
if (!target)
|
||||
lowerer.unsupported("SC1090", access, "compound assignment to unsupported field targets");
|
||||
checkDeferredMemberReceiver(lowerer, target, access);
|
||||
target.obj = save(target.obj, "%compoundReceiver");
|
||||
// Accessors observe getter, RHS side effects, then setter, all through
|
||||
// the saved receiver.
|
||||
const fieldRead = lowerer.fieldGetExpr(target, locOf(access), access);
|
||||
// `+=` on a stored string field reads it into a mutable local, so the
|
||||
// concat below can append to that local in place.
|
||||
const storedString =
|
||||
op === "+" &&
|
||||
fieldRead.type.kind === "string" &&
|
||||
target.fieldType.kind === "string" &&
|
||||
(target.container === "class" || target.container === "record");
|
||||
let read: IrExpr;
|
||||
if (storedString) {
|
||||
const old = declareContextLocal(
|
||||
lowerer.ctx,
|
||||
"%compoundOld",
|
||||
STRING,
|
||||
true,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
body.push({ kind: "varDecl", localId: old.id, init: fieldRead, loc: fieldRead.loc });
|
||||
read = varRef(old.id, STRING, fieldRead.loc);
|
||||
} else {
|
||||
read = save(fieldRead, "%compoundOld");
|
||||
}
|
||||
const rhs = save(
|
||||
rhsNode ? lowerer.lowerExpr(rhsNode) : { kind: "numLit", value: 1, type: F64, loc },
|
||||
"%compoundRhs",
|
||||
);
|
||||
const numericRhs: IrExpr =
|
||||
rhs.type.kind === "dyn" && isJsSourceFile(access.getSourceFile())
|
||||
? { kind: "dynCheck", value: rhs, type: F64, loc: rhs.loc }
|
||||
: lowerOptionalNumber(lowerer, rhs, loc);
|
||||
let value: IrExpr;
|
||||
let appended: IrExpr | null = null;
|
||||
const numericRead = lowerOptionalNumber(lowerer, read, loc, access);
|
||||
const bigintValue = rhsNode ? bigintCompoundValue(op, read, rhs, loc) : null;
|
||||
if (op === "+" && read.type.kind === "string") {
|
||||
appended = concatOperand(lowerer, rhs, rhsNode ?? access);
|
||||
value = { kind: "strConcat", left: read, right: appended, type: STRING, loc };
|
||||
} else if (numericRead.type.kind === "f64" && numericRhs.type.kind === "f64") {
|
||||
value = { kind: "bin", op, left: numericRead, right: numericRhs, type: F64, loc };
|
||||
} else if (bigintValue) {
|
||||
value = bigintValue;
|
||||
} else if (
|
||||
(target.fieldType.kind === "dyn" && isJsSourceFile(access.getSourceFile())) ||
|
||||
// An untyped right operand: the expanded `o.f = o.f op rhs`; the field
|
||||
// write below checks the result into the declared field type.
|
||||
(rhs.type.kind === "dyn" &&
|
||||
lowerer.dynConvertible(read.type) &&
|
||||
lowerer.dynConvertible(target.fieldType))
|
||||
) {
|
||||
value = dynCompoundValue(
|
||||
op,
|
||||
lowerer.coerceToExpected(read, DYN),
|
||||
lowerer.coerceToExpected(rhs, DYN),
|
||||
loc,
|
||||
);
|
||||
} else {
|
||||
compoundUnsupported(lowerer, access, op, read.type, rhs.type);
|
||||
}
|
||||
if (storedString && rhs.type.kind === "string" && read.kind === "varRef" && appended !== null) {
|
||||
// The field gives up its value before the concat (no code runs in
|
||||
// between: both operands are already strings), and the concat is the
|
||||
// self-append `old = old + rhs`, which hands the local's reference to
|
||||
// the runtime: the old string is then uniquely owned unless something
|
||||
// else holds it, and is appended to in place. Repeated `this.text += s`
|
||||
// is linear rather than a copy per append.
|
||||
const oldId = read.localId;
|
||||
const oldLoc = read.loc;
|
||||
const oldRef = (): IrExpr => varRef(oldId, STRING, oldLoc);
|
||||
const concat: IrExpr = {
|
||||
kind: "strConcat",
|
||||
left: oldRef(),
|
||||
right: appended,
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
body.push(
|
||||
lowerer.fieldSetStmt(target, { kind: "strLit", value: "", type: STRING, loc }, loc, access),
|
||||
{ kind: "assign", localId: oldId, value: concat, loc },
|
||||
lowerer.fieldSetStmt(target, oldRef(), loc, access),
|
||||
);
|
||||
return { kind: "seqExpr", stmts: body, result: oldRef(), type: STRING, loc };
|
||||
}
|
||||
const result = save(value, "%compoundResult");
|
||||
body.push(
|
||||
lowerer.fieldSetStmt(target, lowerer.coerceInto(access, result, target.fieldType), loc, access),
|
||||
);
|
||||
return { kind: "seqExpr", stmts: body, result, type: result.type, loc };
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user