mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 08:35:07 +08:00
Refactor container lowering into focused modules (#325)
* refactor(compiler): split container lowering modules * chore: clear remaining lint errors
This commit is contained in:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user