Refactor container lowering into focused modules (#325)

* refactor(compiler): split container lowering modules

* chore: clear remaining lint errors
This commit is contained in:
Chris Tate
2026-09-14 21:33:20 -05:00
committed by GitHub
parent 5a48403470
commit 2bd2e6f54c
13 changed files with 1601 additions and 1637 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ import { existsSync, readFileSync, rmSync, statSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { parseArgs } from "node:util";
import { analyze, buildTargetPlatform, compile, compileExternalC, compileLibrary, isExactExternalTypeSpecifier, renderDiagnostics, renderCoverage, resolveProvenanceSources, setProvenanceSources, sourceTargetPlatform, warmNativeCaches, type NativeCacheWarmProfile } from "@scriptc/compiler";
import { analyze, compile, compileExternalC, compileLibrary, isExactExternalTypeSpecifier, renderDiagnostics, renderCoverage, resolveProvenanceSources, setProvenanceSources, sourceTargetPlatform, warmNativeCaches, type NativeCacheWarmProfile } from "@scriptc/compiler";
import { LEGACY_C_EXECUTABLE_WARNING, shouldWarnLegacyCExecutable } from "./legacy-c-warning.js";
import { resolveOutputOptions } from "./output-options.js";
import { selectOutputPaths } from "./paths.js";
@@ -11,7 +11,6 @@
import {
compileC,
compileLibArchive,
configuredTargetPlatform,
type CcOptions,
type LibArchiveOptions,
} from "./native-toolchain.js";
@@ -102,7 +102,8 @@ test("requires a portable target backend in addition to the host helper backend"
});
test("accepts a WASI-capable helper whose default host target differs", () => {
const helper = WASM32_WASI_TARGET.hostHelpers?.["darwin-arm64"]!;
const helper = WASM32_WASI_TARGET.hostHelpers?.["darwin-arm64"];
if (!helper) throw new Error("missing darwin-arm64 WASI helper fixture");
expect(validateNativeCodegenVersion({
ok: true,
protocol_version: "1",
@@ -3,6 +3,26 @@ import { varRef } from "../../ir/build.js";
import type { Lowerer } from "./lowerer.js";
import { dynUndefinedExpr } from "./lowerer.js";
/** Test whether one array index contains a value rather than a hole. */
export function arrayIndexPresent(arr: IrExpr, index: IrExpr, loc: SrcLoc): IrExpr {
return {
kind: "arrayHas",
arr,
index,
type: BOOL,
loc,
};
}
/** Test the conventional `a.0[i.0]` slot used by synthesized array loops. */
export function currentArrayIndexPresent(arrType: IrType, loc: SrcLoc): IrExpr {
return arrayIndexPresent(
varRef("a.0", arrType, loc),
varRef("i.0", F64, loc),
loc,
);
}
/** The value yielded by a JavaScript array read, independent of its slot's storage type. */
export function arrayValueType(lowerer: Lowerer, elem: IrType): IrType {
if (elem.kind === "jsval" || elem.kind === "dyn") return elem;
@@ -19,10 +39,12 @@ export function arrayValueRead(lowerer: Lowerer, arr: IrExpr, index: IrExpr, ele
return {
kind: "ternary",
cond: {
kind: "bin", op: "===",
kind: "bin",
op: "===",
left: { kind: "arrayState", arr, index, type: F64, loc },
right: { kind: "numLit", value: 1, type: F64, loc },
type: BOOL, loc,
type: BOOL,
loc,
},
then: typeEquals(elem, type) ? read : lowerer.coerceToExpected(read, type),
else_: missing,
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,354 @@
import * as ts from "../../ts7/adapter.js";
import { BOOL, F64, IrExpr, IrFunction, IrStmt, IrType, STRING, SrcLoc, arrayOf, isUnitType, typeEquals } from "../../../ir/ir.js";
import { numLit, varRef } from "../../../ir/build.js";
import { locOf } from "../../program.js";
import type { Lowerer } from "../lowerer.js";
import { own } from "../lowerer.js";
import { isRequireMainFilename } from "../expressions/optional-chains.js";
import { STR_METHODS } from "../surfaces.js";
import { lowerOptionalArgument } from "../optional-arguments.js";
function lowerSplitLimitArg(lowerer: Lowerer, node: ts.Expression | undefined, loc: SrcLoc): IrExpr {
const defaultValue: IrExpr = { kind: "numLit", value: 4294967295, type: F64, loc };
return node ? lowerOptionalArgument(lowerer, node, F64, defaultValue) : defaultValue;
}
/** Extract the checker-proven receiver arm from a runtime-optional property
* read. The receiver is stabilized once before its tag is tested, and a
* missing value throws the same property-read TypeError Node throws before
* evaluating method arguments. */
function lowerMethodReceiver(
lowerer: Lowerer,
node: ts.Expression,
expected: IrType,
member: string,
): IrExpr {
const lowered = lowerer.lowerExpr(node);
const optional = lowerer.runtimeOptionalPropertyReceiver(node, lowered, expected, member);
if (optional) return optional;
if (typeEquals(lowered.type, expected)) return lowered;
return lowerer.coerceInto(node, lowered, expected);
}
/** Regex method calls, both directions: `re.test(s)` on a regex receiver,
* and `s.replace(re, tpl)` / `s.replaceAll(re, tpl)` / `s.split(re)` on a
* string receiver whose FIRST ARGUMENT is a regex (the string-pattern
* overloads keep their island lowering — the argument's mapped type is
* what routes here, before lowerIslandMethodCall can claim the name).
* Null when neither shape matches. */
export function lowerRegexMethodCall(lowerer: Lowerer, call: ts.CallExpression,
access: ts.PropertyAccessExpression,
dynReceiver?: () => IrExpr,): IrExpr | null {
// chainBlocked, not a raw token test: an optional-chain re-dispatch
// (`rawName?.match(re)` — the receiver reads back chain-narrowed)
// rides the same lowering as the plain spelling.
if (lowerer.chainBlocked(access, call)) return null;
// A validated dyn receiver (lowerStringMethodCall's story) is a STRING
// by construction — the symbol gate doesn't apply to `any` receivers.
if (dynReceiver === undefined && !lowerer.isStdlibMember(access)) return null;
const name = access.name.text;
const receiverKind = dynReceiver ? "string" : lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind;
const lowerReceiver = (): IrExpr => {
if (dynReceiver) return dynReceiver();
if (receiverKind === "string") return lowerMethodReceiver(lowerer, access.expression, STRING, access.name.text);
if (receiverKind === "regex") return lowerMethodReceiver(lowerer, access.expression, { kind: "regex" }, access.name.text);
return lowerer.lowerExpr(access.expression);
};
const loc = locOf(call);
if (receiverKind === "regex" && name === "test") {
// The statefulness fence, at compile time where the flags are
// visible: a literal receiver (possibly parenthesized). Values that
// flow through variables hit the same fence at runtime.
let recv: ts.Expression = access.expression;
while (ts.isParenthesizedExpression(recv)) recv = recv.expression;
if (ts.isRegularExpressionLiteral(recv)) {
const flags = recv.text.slice(recv.text.lastIndexOf("/") + 1);
if (flags.includes("g") || flags.includes("y")) {
lowerer.unsupported("SC1121", call);
}
}
const receiver = lowerReceiver();
const args = call.arguments.map((a) => lowerer.lowerExpr(a));
return { kind: "regexIntrinsic", method: "test", receiver, args, type: BOOL, loc };
}
// `re.exec(s)` for non-g/y regexes: spec-identical to `s.match(re)`
// (Symbol.match delegates to exec when lastIndex is out of play), so
// it lowers to the SAME match intrinsic with the operands swapped —
// the honest `string[] | null` slice, nonparticipating captures ""
// (match's documented rule). The g/y statefulness fence applies at
// compile time on literal receivers, exactly test()'s stance; values
// reaching the runtime with those flags abort there.
if (receiverKind === "regex" && name === "exec") {
if (call.arguments.length !== 1) return null; // exec takes exactly the subject
let recv: ts.Expression = access.expression;
while (ts.isParenthesizedExpression(recv)) recv = recv.expression;
if (ts.isRegularExpressionLiteral(recv)) {
const flags = recv.text.slice(recv.text.lastIndexOf("/") + 1);
if (flags.includes("g") || flags.includes("y")) {
lowerer.unsupported("SC1121", call);
}
}
const re = lowerReceiver();
const subject = lowerer.lowerExprExpecting(call.arguments[0]!, STRING);
const resultT: IrType = { kind: "union", unionId: lowerer.unions.intern([arrayOf(STRING), { kind: "nullT" }]) };
return { kind: "regexIntrinsic", method: "match", receiver: subject, args: [re], type: resultT, loc };
}
// `s.match(re)` for non-g/y regexes: Node's exec-shaped result reduced
// to the honest slice — the `string[] | null` union holding
// [whole match, ...captures] or the null arm. The g-flag match returns
// EVERY match (a different shape) and /y is stateful — both fence at
// compile time on literal arguments (values reaching the runtime with
// those flags abort, the test() stance). `.index`/`.input` reads on
// the result fence per member (array-typed value); `.groups` reads
// desugar at their access sites when the regex is statically known
// (lowerMatchGroupsRead).
// `s.match(re)` also claims a NULLABLE string receiver (string + unit
// arms — `process.versions.openssl.match(...)`, the Dict<string>
// member the node suite's crypto helper reads): the checked
// extraction narrows to the string arm and a unit value throws the
// catchable TypeError at the read, where Node's own member read
// throws — tsc only admits the spelling in JS sources.
const nullableStringRecv =
receiverKind === "union" &&
name === "match" &&
(() => {
const t = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
if (t?.kind !== "union") return false;
const arms = lowerer.unions.get(t.unionId)?.arms ?? [];
return arms.some((a) => a.kind === "string") && arms.every((a) => a.kind === "string" || isUnitType(a));
})();
if ((receiverKind === "string" || nullableStringRecv) && name === "match") {
const arg0 = call.arguments[0];
if (!arg0 || lowerer.mapTypeOf(lowerer.typeOf(arg0))?.kind !== "regex") return null; // string-pattern match: the SC2020 fence
if (call.arguments.length !== 1) return null;
let reNode: ts.Expression = arg0;
while (ts.isParenthesizedExpression(reNode)) reNode = reNode.expression;
if (ts.isRegularExpressionLiteral(reNode)) {
const flags = reNode.text.slice(reNode.text.lastIndexOf("/") + 1);
if (flags.includes("g") || flags.includes("y")) {
lowerer.unsupported(
"SC1120",
call,
"'.match()' with the 'g' or 'y' flag (an every-match array is a different shape — use replaceAll/split, or test() per position)",
);
}
}
const receiver = nullableStringRecv
? lowerMethodReceiver(lowerer, access.expression, STRING, access.name.text)
: lowerReceiver();
const re = lowerer.lowerExpr(arg0);
// RegExpMatchArray | null maps to the string[] | null union by
// itself; intern it directly when the checker's spelling doesn't —
// or when it maps to something WIDER (an optional-chain call node
// types `s?.match(re)` with the chain's `| undefined`; the intrinsic
// itself answers string[] | null, and the chain wrapper widens).
const exactT: IrType = { kind: "union", unionId: lowerer.unions.intern([arrayOf(STRING), { kind: "nullT" }]) };
const mapped = lowerer.mapTypeOf(lowerer.typeOf(call));
const resultT: IrType = mapped && typeEquals(mapped, exactT) ? mapped : exactT;
return { kind: "regexIntrinsic", method: "match", receiver, args: [re], type: resultT, loc };
}
// `s.matchAll(re)` — the every-match iterator drained EAGERLY into a
// string[][] (one honest match slice per row — match's rule). Lazy vs
// eager is unobservable here: strings are immutable, and the spec
// clones the regex at the call, so nothing can perturb the drain. The
// eager array IS what the two lowered consumers see anyway (the
// immediate [...spread] and the for-of walk); a stored iterator's
// .next() fences as an array member like any other. Non-global
// regexes throw Node's exact TypeError at runtime, catchably
// (replaceAll's stance).
if (receiverKind === "string" && name === "matchAll") {
const arg0 = call.arguments[0];
if (!arg0 || lowerer.mapTypeOf(lowerer.typeOf(arg0))?.kind !== "regex") return null; // string-pattern form: the SC2020 fence
if (call.arguments.length !== 1) return null;
const receiver = lowerReceiver();
const re = lowerer.lowerExpr(arg0);
return {
kind: "regexIntrinsic",
method: "matchAll",
receiver,
args: [re],
type: arrayOf(arrayOf(STRING)),
loc,
};
}
// `s.search(re)` — the first match's UTF-16 index, or -1. No g/y fence
// (unlike test/match): Symbol.search neither reads nor writes lastIndex
// — a fresh exec from position 0, so /g is irrelevant and /y anchors at
// 0, exactly Node. Never throws.
if (receiverKind === "string" && name === "search") {
const arg0 = call.arguments[0];
if (!arg0 || lowerer.mapTypeOf(lowerer.typeOf(arg0))?.kind !== "regex") return null; // string-pattern form: the SC2020 fence
if (call.arguments.length !== 1) return null;
const receiver = lowerReceiver();
const re = lowerer.lowerExpr(arg0);
return { kind: "regexIntrinsic", method: "search", receiver, args: [re], type: F64, loc };
}
if (
receiverKind === "string" &&
(name === "replace" || name === "replaceAll" || name === "split")
) {
const arg0 = call.arguments[0];
if (!arg0 || lowerer.mapTypeOf(lowerer.typeOf(arg0))?.kind !== "regex") return null;
const receiver = lowerReceiver();
// Split's omitted or undefined limit is 2^32-1. Complete it here so
// both IR backends and the runtime have one required (regex, limit)
// shape; an optional-number value selects the default at runtime.
const args = name === "split"
? [lowerer.lowerExpr(arg0), lowerSplitLimitArg(lowerer, call.arguments[1], loc)]
: call.arguments.map((a) => lowerer.lowerExpr(a));
if (name !== "split" && args[1]?.type.kind !== "string") {
lowerer.unsupported(
"SC1120",
call.arguments[1] ?? call,
"function replacement values (replacements must be string templates)",
);
}
return {
kind: "regexIntrinsic",
method: name,
receiver,
args,
type: name === "split" ? arrayOf(STRING) : STRING,
loc,
};
}
return null;
}
/** `s.slice(1, 4)` and friends → strIntrinsic. Null when this isn't an
* ambient string method call (caller keeps its generic rejection). Missing
* optional arguments are omitted from `args` — the backend fills the
* documented defaults; the IR never encodes them (Infinity isn't
* JSON-safe). tsc has already checked arity and argument types against
* ambient/scriptc.d.ts. */
export function lowerStringMethodCall(lowerer: Lowerer, call: ts.CallExpression,
access: ts.PropertyAccessExpression,
dynReceiver?: () => IrExpr,): IrExpr | null {
if (lowerer.chainBlocked(access, call)) return null;
if (dynReceiver === undefined && access.name.text === "localeCompare") return lowerLocaleCompareCall(lowerer, call, access);
const entry = own(STR_METHODS, access.name.text);
if (!entry) return null;
// A validated dyn receiver (`pkg.name.replace(...)` on a JSON.parse
// value) arrives pre-extracted through `dynReceiver`; its checker type
// is `any`, so the type/symbol gates don't apply — the dyn value's
// methods can only BE the string intrinsics.
if (dynReceiver === undefined) {
const receiverIr = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
// `require.main?.filename.startsWith(...)`: the checker types the
// receiver `string | undefined` (the chain's short-circuit arm), but
// the entry-module fold lowers it to a compile-time STRING — the
// suite harness's skip() shape. Everything else keeps the strict
// string gate.
if (receiverIr?.kind !== "string" && !isRequireMainFilename(lowerer, access.expression)) return null;
if (!lowerer.isStdlibMember(access)) return null;
}
// The lib declares optional parameters beyond the lowered forms
// (includes/startsWith/endsWith take a position); fence the unlowered
// arities instead of passing arguments the runtime doesn't take.
if (call.arguments.length < entry.minArgs || call.arguments.length > entry.maxArgs) {
lowerer.noLowering(
`.${access.name.text} with ${call.arguments.length} argument${call.arguments.length === 1 ? "" : "s"} on strings`,
call,
);
}
const receiver = dynReceiver
? dynReceiver()
: lowerMethodReceiver(lowerer, access.expression, STRING, access.name.text);
const args = entry.method === "split"
? [lowerer.lowerExpr(call.arguments[0]!), lowerSplitLimitArg(lowerer, call.arguments[1], locOf(call))]
: call.arguments.map((a) => lowerer.lowerExpr(a));
// split's separator must BE a string here (a regex argument was
// claimed by lowerRegexMethodCall before this path) — the lib's
// `string | RegExp` union has no lowering as a VALUE.
if (entry.method === "split" && args[0]!.type.kind !== "string") {
lowerer.unsupported(
"SC1090",
call.arguments[0]!,
`'.split()' on a '${lowerer.fmt(args[0]!.type)}' separator (pass a string, or a regex literal)`,
);
}
// padStart/padEnd with the fill omitted: Node pads with " " — the
// same call with the default made explicit.
if ((entry.method === "padStart" || entry.method === "padEnd") && args.length === 1) {
args.push({ kind: "strLit", value: " ", type: STRING, loc: locOf(call) });
}
return {
kind: "strIntrinsic",
method: entry.method,
receiver,
args,
type: entry.result,
loc: locOf(call),
};
}
/** `a.localeCompare(b)` — the one-argument form only (locales/options
* select ICU collations that do not exist here). Lowers to an interned
* synthetic function returning -1/0/1 by CODE-UNIT order — the same
* ordering as the string relational operators — NOT Node's ICU default
* collation: a documented divergence (SEMANTICS.md; e.g. Node says
* "a" < "B" under ICU while code units say "B" < "a"). For same-case
* ASCII the orders agree. Null when the receiver isn't a stdlib string
* (caller keeps its generic rejection). */
function lowerLocaleCompareCall(lowerer: Lowerer, call: ts.CallExpression,
access: ts.PropertyAccessExpression,): IrExpr | null {
const receiverIr = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
if (receiverIr?.kind !== "string") return null;
if (!lowerer.isStdlibMember(access)) return null;
const loc = locOf(call);
if (call.arguments.length !== 1) {
lowerer.noLowering(
`.localeCompare with ${call.arguments.length} arguments`,
call,
"the locales/options parameters select ICU collations that have no lowering — " +
"pass exactly the comparison string",
);
}
const receiver = lowerMethodReceiver(lowerer, access.expression, STRING, access.name.text);
const arg = lowerer.lowerExpr(call.arguments[0]!);
if (arg.type.kind !== "string") lowerer.badType(call.arguments[0]!, lowerer.typeOf(call.arguments[0]!));
const key = "localeCompare";
let helper = lowerer.arrHofHelpers.get(key);
if (!helper) {
helper = `%str.localeCompare`;
lowerer.arrHofHelpers.set(key, helper);
lowerer.liftedFns.push(buildLocaleCompareFn(helper, loc));
}
return { kind: "call", callee: helper, args: [receiver, arg], type: F64, loc };
}
/** `return a < b ? -1 : a > b ? 1 : 0` over the strCmp primitive (the
* relational operators' exact machinery — one interned helper, no new IR
* or runtime surface). */
function buildLocaleCompareFn(name: string, loc: SrcLoc): IrFunction {
const cmp = (op: "<" | ">"): IrExpr => ({ kind: "strCmp", op, left: varRef("a.0", STRING, loc), right: varRef("b.0", STRING, loc), type: BOOL, loc });
const body: IrStmt[] = [
{
kind: "return",
value: {
kind: "ternary",
cond: cmp("<"),
then: numLit(-1, loc),
else_: { kind: "ternary", cond: cmp(">"), then: numLit(1, loc), else_: numLit(0, loc), type: F64, loc },
type: F64,
loc,
},
loc,
},
];
return {
name,
params: [
{ localId: "a.0", name: "a", type: STRING },
{ localId: "b.0", name: "b", type: STRING },
],
returnType: F64,
locals: [
{ id: "a.0", name: "a", type: STRING, mutable: true },
{ id: "b.0", name: "b", type: STRING, mutable: true },
],
body,
loc,
};
}
@@ -31,6 +31,7 @@ import {
import { lowerAbsenceProbe } from "./lower-exprs.js";
import { conditionalSpreadOf, lowerDynObjectLiteral } from "./expressions/object-literals.js";
import { isSafeToDiscard } from "./expressions/evaluation-safety.js";
import { defaultAfterUndefined, lowerStaticallyUndefinedArgument } from "./optional-arguments.js";
import { HTTP2_CONSTANTS } from "./http2-constants.js";
import { CRYPTO_CIPHERS, CRYPTO_CONSTANTS, CRYPTO_CURVES, CRYPTO_HASHES } from "./crypto-tables.js";
import { generatorMeta, timerStyleCallback } from "./lower-calls.js";
@@ -188,45 +189,6 @@ function lowerOptionalNumberPredicate(
return { kind: "call", callee: helper, args: [value], type: BOOL, loc };
}
/** Lower an optional builtin argument whose checker type is statically
* undefined/void. The returned expression exists only to preserve effects;
* callers replace its value with the API default. */
function lowerStaticallyUndefinedBuiltinArg(lowerer: Lowerer, node: ts.Expression): IrExpr | null {
const peel = (value: ts.Expression): ts.Expression => {
let expr = value;
while (
ts.isParenthesizedExpression(expr) ||
ts.isAsExpression(expr) ||
ts.isTypeAssertion(expr) ||
ts.isSatisfiesExpression(expr)
) {
expr = expr.expression;
}
return expr;
};
let expr = node;
while (ts.isParenthesizedExpression(expr)) expr = expr.expression;
if ((lowerer.typeOf(expr).flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) === 0) return null;
expr = peel(expr);
let sawVoid = false;
while (ts.isVoidExpression(expr)) {
sawVoid = true;
expr = peel(expr.expression);
}
return lowerer.lowerExpr(sawVoid ? expr : node);
}
function defaultAfterUndefined(value: IrExpr, dflt: IrExpr): IrExpr {
if (isSafeToDiscard(value)) return dflt;
return {
kind: "seqExpr",
stmts: [{ kind: "exprStmt", expr: value, loc: value.loc }],
result: dflt,
type: dflt.type,
loc: value.loc,
};
}
/** Complete an optional builtin argument. Statically undefined spellings
* preserve their effects and select the default; a runtime unit-armed union
* uses nullish selection when every non-unit arm is the expected type. */
@@ -237,7 +199,7 @@ function lowerBuiltinOptionalDefault(
dflt: IrExpr,
nullIsDefault = false,
): IrExpr {
const undefinedArg = lowerStaticallyUndefinedBuiltinArg(lowerer, node);
const undefinedArg = lowerStaticallyUndefinedArgument(lowerer, node);
if (undefinedArg) return defaultAfterUndefined(undefinedArg, dflt);
if (nullIsDefault && (lowerer.typeOf(node).flags & ts.TypeFlags.Null) !== 0) {
return defaultAfterUndefined(lowerer.lowerExpr(node), dflt);
@@ -4792,7 +4754,7 @@ function lowerOptionalStringSearchParams(lowerer: Lowerer, init: IrExpr, loc: Sr
const num = (node: ts.Expression | undefined, dflt: number): { value: IrExpr; defaulted: IrExpr } => {
const defaultValue = { kind: "numLit", value: dflt, type: F64, loc } satisfies IrExpr;
if (!node) return { value: defaultValue, defaulted: boolLit(true, loc) };
const undefinedArg = lowerStaticallyUndefinedBuiltinArg(lowerer, node);
const undefinedArg = lowerStaticallyUndefinedArgument(lowerer, node);
if (undefinedArg) {
return { value: defaultAfterUndefined(undefinedArg, defaultValue), defaulted: boolLit(true, loc) };
}
@@ -6457,10 +6419,10 @@ function lowerOptionalStringSearchParams(lowerer: Lowerer, init: IrExpr, loc: Sr
const secondNode = args[1];
const thirdNode = args[2];
const secondUndefined = secondNode
? lowerStaticallyUndefinedBuiltinArg(lowerer, secondNode)
? lowerStaticallyUndefinedArgument(lowerer, secondNode)
: null;
const thirdUndefined = thirdNode
? lowerStaticallyUndefinedBuiltinArg(lowerer, thirdNode)
? lowerStaticallyUndefinedArgument(lowerer, thirdNode)
: null;
const secondT = secondNode && !secondUndefined ? lowerer.mapTypeOf(lowerer.typeOf(secondNode)) : undefined;
const callbackNode = args.length === 3 && !thirdUndefined
@@ -16,7 +16,9 @@ import { NARROW_FIRST, builtinFenceHintOf, builtinModuleFnOf } from "./surfaces.
import { ffiBindingDiag, ffiSignatureDiag, libCallbackDiag, requiresDynamicDiag } from "../../diagnostics/diagnostic.js";
import type { ScrDiagnostic } from "../../diagnostics/diagnostic.js";
import { mixinFnShapeOf } from "./lower-mixins.js";
import { bufEncoding, dynStringReceiver, lowerArrayFromCall, lowerDynArrayFilterCall, lowerDynArrayFlatMapCall, lowerGroupByStaticCall, lowerIteratorHelperCall, lowerObjectAssignIndexShape, lowerObjectFromEntriesCall, lowerObjectIterOverIndexShape, lowerRegexMethodCall, lowerStringMethodCall, lowerTupleReadMethodCall } from "./lower-containers.js";
import { dynStringReceiver, lowerArrayFromCall, lowerDynArrayFilterCall, lowerDynArrayFlatMapCall, lowerGroupByStaticCall, lowerIteratorHelperCall, lowerObjectAssignIndexShape, lowerObjectFromEntriesCall, lowerObjectIterOverIndexShape, lowerTupleReadMethodCall } from "./lower-containers.js";
import { bufEncoding } from "./containers/bytes.js";
import { lowerRegexMethodCall, lowerStringMethodCall } from "./containers/string-and-regexp.js";
import { lowerChildStreamMethodCall, lowerCreateRequireCall, lowerDirentMethodCall, lowerFileHandleMethodCall, lowerImportMetaResolveCall, lowerPerfHooksCall, lowerProcStreamMethodCall, lowerReflectApplyCall, lowerRequireResolveCall, lowerWatcherMethodCall } from "./lower-builtins.js";
import { lowerAbsenceProbe, lowerPromiseAllTupleCall, lowerPromiseRejectCall, templateRawTextOf } from "./lower-exprs.js";
import { isSafeToDiscard } from "./expressions/evaluation-safety.js";
@@ -11,7 +11,8 @@ import { MAX_GENERIC_INSTANCES, generatorMeta, genericCallInstance, implicitAnyP
import { isGenericCallableMemberType, typeKey } from "../type-mapper.js";
import { cjsClassExprWholeExportOf, isCjsJsFile, isJsSourceFile, isModuleExportsAccess, isNodeTypesPath, locOf } from "../program.js";
import { PoisonError, dynFallbackType, dynUndefinedExpr, newFnCtx, own } from "./lowerer.js";
import { bufEncoding, lowerMapSeedArrayNew } from "./lower-containers.js";
import { lowerMapSeedArrayNew } from "./lower-containers.js";
import { bufEncoding } from "./containers/bytes.js";
import { isSafeToRepeat } from "./expressions/evaluation-safety.js";
import { lowerSearchParamsNew } from "./lower-builtins.js";
import { requiresDynamicPackageDiag, unsupportedDiag } from "../../diagnostics/diagnostic.js";
File diff suppressed because it is too large Load Diff
@@ -34,7 +34,7 @@ import type { ClassInfo } from "./lower-classes.js";
import { isJsSourceFile, locOf } from "../program.js";
import { newFnCtx, own } from "./lowerer.js";
import { appendImplicitUndefinedReturn } from "./lower-calls.js";
import { bufEncoding, knownBufEncoding } from "./lower-containers.js";
import { bufEncoding, knownBufEncoding } from "./containers/bytes.js";
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
import { BOOL, DYN, F64, IrExpr, IrFunction, IrLibFn, IrStmt, IrType, RUNTIME_STREAM_CLASSES, STRING, SrcLoc, VOID, arrayOf, bytesOf, canBoxFuncIntoDyn, funcOf, typeEquals, typeKey } from "../../ir/ir.js";
import { boolLit, numLit, strLit } from "../../ir/build.js";
@@ -95,7 +95,9 @@ import { FileParts, splitFiles, collectProgram, collectNpmImports, collectJsonIm
import { ClassInfo, ClassIteratorInfo, GenericClassInfo, registerBuiltinErrorClasses, registerBuiltinEmitterClass, registerBuiltinStreamClasses, builtinErrorInfoOf, builtinEmitterInfoOf, builtinStreamInfoOf, analyzeClassDecoration, classIteratorDrainCall, classIteratorNextCall, classIteratorOf, classIteratorOpenCall, classIteratorRestDrainCall, classMemberNameOf, classValueRef, collectClassShape, exactClassOfReceiver, collectClassShapeInner, ctorAbiEquals, findMethodOn, findStaticOn, findGenericMethodOn, findGenericStaticOn, genericClassInstanceType, isSubclassOf, inHierarchy, overrideBelow, staticShadowBelow, upcastTo, lowerClassMembers, lowerClassCtor, lowerClassExpression, lowerClassExpressionInfo, lowerClassMethodMember, lowerClassValueProperty, lowerStaticMethod, throwingSetterFn, fieldInitStmts, lowerStaticFieldInits, lowerStaticFieldRead, lowerDerivedCtorBody, superCallStmt, lowerSuperMethodCall, superThisRef, lowerSuperAccessorRead, lowerSuperAccessorWrite, inheritsBuiltinErrorCtor, inheritsBuiltinEmitterCtor, errorMessageArg, lowerNew, accessorCall } from "./lower-classes.js";
import { MixinFnShape, mixinCallClassInfoOf, mixinIntersectionInstanceType } from "./lower-mixins.js";
import { ParamShape, FnSig, GenericFnInfo, GenericInstance, bindingNeverReassigned, bodyReadsArguments, implicitMonoFile, isThisParameter, paramShape, paramShapes, checkDefaultParamBodyType, completeArgs, wrappedUndefined, undefinedArgFor, requireExactArityValue, bodyReturnType, declaredReturnType, collectSignature, collectSignatureInner, collectGenericSignature, genericFnOf, lowerGenericCall, lowerGenericFnValue, inferTypeParamBindings, lowerGenericInstance, lowerCall, lowerFfiCall, lowerTimersMemberCall, lowerPromiseMethodCall, lowerFilterNarrowCall, isTopLevelFnSymbol, lowerNestedFunctionDecl, lambdaSignature, lowerLambda, lowerFunction, validateFfiImports } from "./lower-calls.js";
import { lowerArrayMethodCall, lowerBufferStaticCall, lowerBytesMethodCall, lowerBytesNew, lowerMapMethodCall, lowerMapForEachCall, buildMapForEachFn, lowerRecordOvfCaptureHelper, lowerEnvToPairsHelper, lowerSetMethodCall, lowerSetForEachCall, buildSetForEachFn, lowerRegexMethodCall, lowerStringMethodCall } from "./lower-containers.js";
import { lowerArrayMethodCall, lowerMapMethodCall, lowerMapForEachCall, buildMapForEachFn, lowerRecordOvfCaptureHelper, lowerEnvToPairsHelper, lowerSetMethodCall, lowerSetForEachCall, buildSetForEachFn } from "./lower-containers.js";
import { lowerBufferStaticCall, lowerBytesMethodCall, lowerBytesNew } from "./containers/bytes.js";
import { lowerRegexMethodCall, lowerStringMethodCall } from "./containers/string-and-regexp.js";
import { lowerStreamModuleCall } from "./lower-stream.js";
import { lowerEmitOverrideSpec, type EmitSpecCtx, type EmitSpecRequest } from "./lower-event-emitter.js";
import { builtinImportOf, createRequireBindingDecl, createRequireNamespaceDecl, createRequireSpecOf, stripTypeCasts, lowerBuiltinModuleCall, lowerTimersPromisesSetInterval, lowerFsToUnixTimestampCall, lowerFsLadderCall, lowerChildArgsArg, lowerSpawnSyncCall, lowerSpawnCall, lowerExecSyncCall, recordToEnvPairs, lowerJsonMethodCall, fencedBuiltinImportOf, lowerCryptoComposedCall, lowerUrlMethodCall, lowerSearchParamsMethodCall, lowerStatsMethodCall, lowerChildMethodCall, lowerAtomicsCall, lowerBuiltinExtraProperty, promisifiedExecFileDecl, lowerExecFileAsyncCall, execFileAsyncHelper, lowerStringDecoderMethodCall, strdecHelper, lowerReadlineMethodCall, lowerDcChannelMethodCall, lowerDcChannelProperty, lowerAlsMethodCall, lowerDcTracingChannelMethodCall, lowerDcTracingChannelProperty, lowerJsonProperty, lowerErrorCodeProperty, lowerProcessProperty, isProcessEnv, envValueType, lowerProcessEnvGet, lowerProcessMethodCall, lowerProcessOptionalMethodCall, lowerTimeoutMethodCall, envSnapshotHelper, isConsoleLog, consoleCallMember, lowerNumberStaticCall, lowerNumberStaticProperty, lowerDateCall, lowerTextCodecCall, lowerCryptoModuleCall, lowerFsConstantsProperty, lowerBuiltinConstantsProperty, builtinConstantBindingOf, builtinConstantsDestructureDecl, lowerProcessStreamProperty, lowerStringStaticCall, lowerStringLastIndexOfCall, lowerPromiseStaticCall, textCodecBindingClassOf } from "./lower-builtins.js";
@@ -0,0 +1,66 @@
import * as ts from "../ts7/adapter.js";
import type { IrExpr, IrType } from "../../ir/ir.js";
import { typeEquals } from "../../ir/ir.js";
import type { Lowerer } from "./lowerer.js";
import { isSafeToDiscard } from "./expressions/evaluation-safety.js";
/** Lower an expression whose checker type is statically `undefined`/`void`. */
export function lowerStaticallyUndefinedArgument(lowerer: Lowerer, node: ts.Expression): IrExpr | null {
const peelErasableWrappers = (value: ts.Expression): ts.Expression => {
let expr = value;
while (
ts.isParenthesizedExpression(expr) ||
ts.isAsExpression(expr) ||
ts.isTypeAssertion(expr) ||
ts.isSatisfiesExpression(expr)
) {
expr = expr.expression;
}
return expr;
};
let expr = node;
while (ts.isParenthesizedExpression(expr)) expr = expr.expression;
if ((lowerer.typeOf(expr).flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) === 0) return null;
expr = peelErasableWrappers(expr);
let sawVoid = false;
while (ts.isVoidExpression(expr)) {
sawVoid = true;
expr = peelErasableWrappers(expr.expression);
}
return sawVoid ? lowerer.lowerExpr(expr) : lowerer.lowerExpr(node);
}
/** Preserve an explicitly undefined argument's effects, then use its default. */
export function defaultAfterUndefined(value: IrExpr, defaultValue: IrExpr): IrExpr {
if (isSafeToDiscard(value)) return defaultValue;
return {
kind: "seqExpr",
stmts: [{ kind: "exprStmt", expr: value, loc: value.loc }],
result: defaultValue,
type: defaultValue.type,
loc: value.loc,
};
}
/** Lower an optional argument, applying its default only to the undefined arm. */
export function lowerOptionalArgument(
lowerer: Lowerer,
node: ts.Expression,
expected: IrType,
defaultValue: IrExpr,
): IrExpr {
const undefinedArg = lowerStaticallyUndefinedArgument(lowerer, node);
if (undefinedArg) return defaultAfterUndefined(undefinedArg, defaultValue);
const value = lowerer.lowerExpr(node);
if (value.type.kind === "union") {
const def = lowerer.unions.get(value.type.unionId);
if (
def?.arms.length === 2 &&
def.arms.some((arm) => arm.kind === "undefinedT") &&
def.arms.some((arm) => typeEquals(arm, expected))
) {
return { kind: "nullish", left: value, right: defaultValue, type: expected, loc: value.loc };
}
}
return lowerer.coerceInto(node, value, expected);
}