mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-24 06:14:01 +08:00
Merge perf/typed-array-reads into perf/trace-cycle-candidates
Brings in cc7c6cb7 (inline truncation decided for the host when no triple is given), which this branch predates; without it the native bootstrap's LLVM comparison with the Node seed fails on x86-64 Linux.
This commit is contained in:
@@ -811,7 +811,14 @@ export class LlEmitter {
|
||||
this.ffiExtendNarrowIntegers =
|
||||
options.wasi === true || ffiExtendsNarrowIntegers(options.targetTriple);
|
||||
this.wasi = options.wasi === true;
|
||||
this.inlineTrunc = /^(x86_64|amd64)\b/i.test(options.targetTriple ?? "");
|
||||
// An empty triple targets the host, as for executableTls below: the
|
||||
// Node-hosted CLI passes none, the native CLI passes its toolchain's, and
|
||||
// both must emit the same module for the same target.
|
||||
this.inlineTrunc =
|
||||
!this.wasi &&
|
||||
(options.targetTriple
|
||||
? /^(x86_64|amd64)\b/i.test(options.targetTriple)
|
||||
: process.arch === "x64");
|
||||
this.executableTls =
|
||||
mod.workers === true &&
|
||||
mod.lib === undefined &&
|
||||
|
||||
@@ -3,6 +3,7 @@ import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer } from "../lowerer.js";
|
||||
import { locOf } from "../../program.js";
|
||||
import { lowerOptionalArgument, lowerStringSearchArgument } from "../optional-arguments.js";
|
||||
import { lowerToNumberArgument } from "../lower-exprs.js";
|
||||
import { F64, type IrExpr, STRING, arrayOf, isUnitType, typeEquals } from "../../../ir/ir.js";
|
||||
|
||||
/** `String.fromCharCode/fromCodePoint(...codes)` on THE String global: every argument
|
||||
@@ -97,7 +98,9 @@ export function lowerStringStaticCall(
|
||||
const packed = lowerer.lowerExprExpecting(spread.expression, arrayOf(F64));
|
||||
return { kind: "libCall", fn: `string.${member}`, args: [packed], type: STRING, loc };
|
||||
}
|
||||
const elems = call.arguments.map((a) => lowerer.lowerExprExpecting(a, F64));
|
||||
// Each code converts with ToNumber: a missing value is NaN (code unit 0
|
||||
// for fromCharCode, a RangeError for fromCodePoint), exactly Node.
|
||||
const elems = call.arguments.map((a) => lowerToNumberArgument(lowerer, a));
|
||||
const packed: IrExpr = { kind: "arrayLit", elems, type: arrayOf(F64), loc };
|
||||
return { kind: "libCall", fn: `string.${member}`, args: [packed], type: STRING, loc };
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
lowerStaticallyUndefinedArgument,
|
||||
} from "../optional-arguments.js";
|
||||
import { lowerCheckedArrayFrom } from "./array-construction.js";
|
||||
import { lowerToNumberArgument } from "../lower-exprs.js";
|
||||
|
||||
/** Uint8Array.prototype.toSorted. The receiver/comparator expressions are
|
||||
* evaluated before entering the helper; the helper snapshots with
|
||||
@@ -486,7 +487,16 @@ export function lowerBytesMethodCall(
|
||||
lowerer.noLowering(`.${name} with ${nArgs} arguments on typed arrays`, call);
|
||||
}
|
||||
const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr);
|
||||
const args = call.arguments.map((a) => lowerer.lowerExprExpecting(a, F64));
|
||||
// A missing start is 0 and a missing end is the length (+Infinity
|
||||
// clamps to it), as for omitted arguments.
|
||||
const args = call.arguments.map((a, i) =>
|
||||
lowerOptionalArgument(lowerer, a, F64, {
|
||||
kind: "numLit",
|
||||
value: i === 0 ? 0 : Infinity,
|
||||
type: F64,
|
||||
loc,
|
||||
}),
|
||||
);
|
||||
// subarray is a VIEW (TypedArray.prototype.subarray aliases), and
|
||||
// Buffer's slice() is subarray's deprecated Node alias — resolved by
|
||||
// where the member is declared, the toString discipline below. Only
|
||||
@@ -796,7 +806,8 @@ export function lowerBytesMethodCall(
|
||||
lowerer.noLowering(`.${name} with ${nArgs} arguments`, call);
|
||||
}
|
||||
const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr);
|
||||
const args = [lowerer.lowerExprExpecting(call.arguments[0]!, F64)];
|
||||
// ToIndex(undefined) is ToIndex(NaN): a missing offset reads offset 0.
|
||||
const args = [lowerToNumberArgument(lowerer, call.arguments[0]!)];
|
||||
if (nArgs === 2) args.push(lowerer.lowerExprExpecting(call.arguments[1]!, BOOL));
|
||||
return { kind: "bytesIntrinsic", method: dvGetter.method, receiver, args, type: F64, loc };
|
||||
}
|
||||
@@ -829,9 +840,11 @@ export function lowerBytesMethodCall(
|
||||
lowerer.noLowering(`.${name} with ${nArgs} arguments`, call);
|
||||
}
|
||||
const receiver = lowerer.lowerExprExpecting(access.expression, receiverIr);
|
||||
// The offset (ToIndex) and the value (ToNumber) treat a missing value
|
||||
// as NaN.
|
||||
const args = [
|
||||
lowerer.lowerExprExpecting(call.arguments[0]!, F64),
|
||||
lowerer.lowerExprExpecting(call.arguments[1]!, F64),
|
||||
lowerToNumberArgument(lowerer, call.arguments[0]!),
|
||||
lowerToNumberArgument(lowerer, call.arguments[1]!),
|
||||
];
|
||||
if (nArgs === 3) args.push(lowerer.lowerExprExpecting(call.arguments[2]!, BOOL));
|
||||
return { kind: "bytesIntrinsic", method: dvSetter.method, receiver, args, type: VOID, loc };
|
||||
|
||||
@@ -107,8 +107,10 @@ import { lowerProcStreamMethodCall } from "./builtins/process.js";
|
||||
import { lowerReflectCall } from "./builtins/reflection.js";
|
||||
import {
|
||||
lowerAbsenceProbe,
|
||||
lowerOptionalNumber,
|
||||
lowerPromiseAllTupleCall,
|
||||
lowerPromiseRejectCall,
|
||||
lowerToNumberArgument,
|
||||
stringWrapperToString,
|
||||
symbolFieldInfo,
|
||||
templateRawTextOf,
|
||||
@@ -9520,7 +9522,8 @@ export function lowerStaticNumberGlobal(
|
||||
);
|
||||
}
|
||||
if (name === "isNaN") {
|
||||
const x = lowerer.lowerExprExpecting(args[0]!, F64);
|
||||
// The global's ToNumber turns a missing value into NaN.
|
||||
const x = lowerToNumberArgument(lowerer, args[0]!);
|
||||
return { kind: "libCall", fn: "num.isNaN", args: [x], type: BOOL, loc };
|
||||
}
|
||||
const radix: IrExpr = args[1]
|
||||
@@ -9650,6 +9653,17 @@ export function lowerStaticNumberGlobal(
|
||||
if (name === "isFinite" && probed?.type.kind === "f64") {
|
||||
return { kind: "libCall", fn: "number.isFinite", args: [probed], type: BOOL, loc };
|
||||
}
|
||||
// A number the checker trusts but storage may hold missing (an
|
||||
// out-of-range read): the global's ToNumber makes undefined NaN.
|
||||
if (
|
||||
name === "isFinite" &&
|
||||
probed?.type.kind === "union" &&
|
||||
lowerer.armTag(probed.type.unionId, UNDEFINED_T) >= 0 &&
|
||||
lowerer.stripUndefinedArm(probed.type).kind === "f64"
|
||||
) {
|
||||
const x = lowerOptionalNumber(lowerer, probed, loc, args[0]);
|
||||
return { kind: "libCall", fn: "number.isFinite", args: [x], type: BOOL, loc };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -2595,6 +2595,10 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
lowerer.isRuntimeOptionalField(handled.shapeId, handled.field)
|
||||
)
|
||||
return handled;
|
||||
// A class field assigned a missing read: the checker narrows this
|
||||
// read to the assigned value's bare type, but the stored undefined
|
||||
// arm is real (`obj.n = bytes[i]; obj.n` is undefined in Node).
|
||||
if (runtimeOptionalClassField(lowerer, handled, expr)) return handled;
|
||||
return lowerer.maybeNarrow(handled, expr);
|
||||
}
|
||||
// `globalThis.<name>` that no lowering above claimed
|
||||
@@ -6487,6 +6491,33 @@ export function lowerOptionalNumber(
|
||||
: { kind: "seqExpr", stmts: prefix, result: value, type: F64, loc };
|
||||
}
|
||||
|
||||
/** A number argument of a builtin that converts it with ToNumber (Math
|
||||
* functions, String.fromCharCode, the global isNaN/isFinite, DataView
|
||||
* offsets and values). ToNumber(undefined) is NaN, so a missing value — an
|
||||
* out-of-range read, a local or field holding one, or a hole — converts to
|
||||
* NaN instead of trusting the checker's number type. */
|
||||
export function lowerToNumberArgument(lowerer: Lowerer, node: ts.Expression): IrExpr {
|
||||
let inner = node;
|
||||
while (ts.isParenthesizedExpression(inner)) inner = inner.expression;
|
||||
const loc = locOf(inner);
|
||||
const isOptionalNumber = (value: IrExpr): boolean =>
|
||||
value.type.kind === "union" &&
|
||||
lowerer.armTag(value.type.unionId, UNDEFINED_T) >= 0 &&
|
||||
lowerer.stripUndefinedArm(value.type).kind === "f64";
|
||||
const optional = ts.isIdentifier(inner)
|
||||
? runtimeOptionalStorageOperand(lowerer, inner)
|
||||
: ts.isElementAccessExpression(inner)
|
||||
? lowerAbsenceProbe(lowerer, inner)
|
||||
: null;
|
||||
if (optional && isOptionalNumber(optional))
|
||||
return lowerOptionalNumber(lowerer, optional, loc, inner);
|
||||
// A conditional builds each branch for the destination.
|
||||
if (ts.isConditionalExpression(inner)) return lowerer.lowerExprExpecting(node, F64);
|
||||
const value = lowerer.lowerExpr(node);
|
||||
if (isOptionalNumber(value)) return lowerOptionalNumber(lowerer, value, loc, inner);
|
||||
return lowerer.coerceInto(node, value, F64);
|
||||
}
|
||||
|
||||
/** `a[i]` reads. Only f64 indices into array receivers are modeled; JS
|
||||
* string-key element access (`a["length"]`) and string indexing (`s[0]`
|
||||
* typechecks against the lib's index signature; use .charAt) stay out. */
|
||||
@@ -7595,6 +7626,24 @@ export function runtimeOptionalStorageOperand(
|
||||
return optional.value;
|
||||
}
|
||||
|
||||
/** A `T | undefined` class field read that the checker narrowed to `T`
|
||||
* where the field was assigned a value that may be missing (an unchecked
|
||||
* indexed read). TypeScript's assignment narrowing is not a runtime fact
|
||||
* there, so the read keeps the stored union instead of extracting `T`. */
|
||||
function runtimeOptionalClassField(
|
||||
lowerer: Lowerer,
|
||||
value: IrExpr,
|
||||
node: ts.PropertyAccessExpression,
|
||||
): boolean {
|
||||
if (value.kind !== "fieldGet" || value.type.kind !== "union") return false;
|
||||
if (lowerer.armTag(value.type.unionId, UNDEFINED_T) < 0) return false;
|
||||
if ((lowerer.unions.get(value.type.unionId)?.arms.length ?? 0) !== 2) return false;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(node.name);
|
||||
if (!symbol || !lowerer.runtimeOptionalAssignedSymbols.has(symbol)) return false;
|
||||
const narrowed = lowerer.mapTypeOf(lowerer.typeOf(node));
|
||||
return narrowed !== null && typeEquals(narrowed, lowerer.stripUndefinedArm(value.type));
|
||||
}
|
||||
|
||||
/** A presence-test operand: an absence-aware element or field read, an
|
||||
* identifier's runtime-optional storage, or the ordinary lowering. */
|
||||
export function lowerPresenceOperand(lowerer: Lowerer, node: ts.Expression): IrExpr {
|
||||
@@ -14380,6 +14429,15 @@ function classFieldTarget(
|
||||
const lowerObjectReceiver = (): IrExpr => {
|
||||
let obj = represented;
|
||||
if (obj.type.kind === "union") {
|
||||
// A receiver that may hold a missing value (a field assigned an
|
||||
// unchecked read): Node's member-read TypeError for the unit arm.
|
||||
const present = lowerer.runtimeOptionalPropertyReceiver(
|
||||
receiverNode,
|
||||
obj,
|
||||
receiverType,
|
||||
fieldName,
|
||||
);
|
||||
if (present) return present;
|
||||
const helper = lowerer.narrowedArmHelper(obj.type.unionId, receiverType, locOf(receiverNode));
|
||||
obj = helper
|
||||
? lowerer.checkedNarrowCall(helper, obj, receiverType, locOf(receiverNode))
|
||||
|
||||
@@ -5,11 +5,7 @@ import { InternalCompilerError } from "../../errors.js";
|
||||
* surface (Math and number/string methods under --dynamic), and the npm
|
||||
* package boundary fences for node_modules-declared symbols. */
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import {
|
||||
lowerAbsenceProbe,
|
||||
lowerOptionalNumber,
|
||||
runtimeOptionalStorageOperand,
|
||||
} from "./lower-exprs.js";
|
||||
import { lowerToNumberArgument } from "./lower-exprs.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import {
|
||||
arrayOf,
|
||||
@@ -3401,23 +3397,10 @@ export function lowerIslandMethodCall(
|
||||
// Static numeric Math calls precede the island path. The scalar methods
|
||||
// use their declared arity; min/max and hypot accept their variadic forms.
|
||||
const staticMath = isMath ? own(STATIC_MATH_FNS, name) : undefined;
|
||||
// A missing element (a hole bound by a loop, a local holding a missing
|
||||
// read, or the read itself) converts to NaN, ToNumber's answer for
|
||||
// undefined, instead of trusting the checker's number type.
|
||||
const mathArg = (a: ts.Expression): IrExpr => {
|
||||
let inner = a;
|
||||
while (ts.isParenthesizedExpression(inner)) inner = inner.expression;
|
||||
const optional = ts.isIdentifier(inner)
|
||||
? runtimeOptionalStorageOperand(lowerer, inner)
|
||||
: ts.isElementAccessExpression(inner)
|
||||
? lowerAbsenceProbe(lowerer, inner)
|
||||
: null;
|
||||
return optional &&
|
||||
optional.type.kind === "union" &&
|
||||
lowerer.stripUndefinedArm(optional.type).kind === "f64"
|
||||
? lowerOptionalNumber(lowerer, optional, loc, inner)
|
||||
: lowerer.lowerExprExpecting(a, F64);
|
||||
};
|
||||
// A missing element (a hole bound by a loop, a local or field holding a
|
||||
// missing read, or the read itself) converts to NaN, ToNumber's answer
|
||||
// for undefined, instead of trusting the checker's number type.
|
||||
const mathArg = (a: ts.Expression): IrExpr => lowerToNumberArgument(lowerer, a);
|
||||
if (staticMath && name === "hypot") {
|
||||
const elems = call.arguments.map((a) =>
|
||||
ts.isSpreadElement(a) ? lowerer.lowerExprExpecting(a.expression, arrayOf(F64)) : mathArg(a),
|
||||
|
||||
@@ -4901,6 +4901,18 @@ export function lowerVarDecl(
|
||||
init.type.kind === "func"
|
||||
)
|
||||
type = init.type;
|
||||
// A TypeScript lambda whose closure returns the runtime result (a missing
|
||||
// read, a `??` default of another type) instead of the checker's bare
|
||||
// return type: the binding keeps the checker's parameters and takes the
|
||||
// closure's result.
|
||||
if (
|
||||
!decl.type &&
|
||||
type?.kind === "func" &&
|
||||
init.type.kind === "func" &&
|
||||
(ts.isArrowFunction(decl.initializer) || ts.isFunctionExpression(decl.initializer)) &&
|
||||
lowerer.runtimeOptionalFunctionReturns.has(decl.initializer)
|
||||
)
|
||||
type = { ...type, ret: init.type.ret };
|
||||
if (
|
||||
lowerer.implicitParamTypes !== null &&
|
||||
init.type.kind === "dyn" &&
|
||||
|
||||
@@ -4223,6 +4223,23 @@ export class Lowerer {
|
||||
if (!symbol) return false;
|
||||
return noteFieldSymbol(symbol, name);
|
||||
};
|
||||
/** An instance field of a TypeScript class whose declared storage is
|
||||
* exactly `T | undefined`: an assignment from a missing read stores the
|
||||
* undefined arm while the checker narrows later reads to `T`. */
|
||||
const optionalInstanceField = (symbol: ts.Symbol): boolean => {
|
||||
const decl = this.checker.valueDeclarationOf(symbol);
|
||||
if (!decl || !ts.isPropertyDeclaration(decl) || !ts.isIdentifier(decl.name)) return false;
|
||||
if (ts.getCombinedModifierFlags(decl) & ts.ModifierFlags.Static) return false;
|
||||
const sf = decl.getSourceFile();
|
||||
if (isJsSourceFile(sf) || sf.isDeclarationFile) return false;
|
||||
const declared = typeOrNull(decl.name);
|
||||
const mapped = declared ? this.mapTypeOf(declared) : null;
|
||||
if (mapped?.kind !== "union" || this.armTag(mapped.unionId, UNDEFINED_T) < 0) return false;
|
||||
const arms = this.unions.get(mapped.unionId)?.arms ?? [];
|
||||
return (
|
||||
arms.length === 2 && arms.every((arm) => arm.kind === "undefinedT" || !isUnitType(arm))
|
||||
);
|
||||
};
|
||||
const noteFieldSymbol = (symbol: ts.Symbol, name: string): boolean => {
|
||||
const set = optionalFields.get(symbol) ?? new Set<string>();
|
||||
const before = set.size;
|
||||
@@ -4353,12 +4370,29 @@ export class Lowerer {
|
||||
return found;
|
||||
};
|
||||
const jsLambdas: ts.FunctionLikeDeclaration[] = [];
|
||||
// TypeScript lambdas keep the checker's inferred return type, which
|
||||
// spells a missing read as its bare element type and `read ?? "text"`
|
||||
// as a number. Their closures return the runtime result instead.
|
||||
const tsLambdas: ts.FunctionLikeDeclaration[] = [];
|
||||
for (const sf of sourceFiles) {
|
||||
if (!isJsSourceFile(sf)) continue;
|
||||
const js = isJsSourceFile(sf);
|
||||
if (!js && sf.isDeclarationFile) continue;
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) jsLambdas.push(node);
|
||||
if (ts.isArrowFunction(node) || ts.isFunctionExpression(node))
|
||||
(js ? jsLambdas : tsLambdas).push(node);
|
||||
});
|
||||
}
|
||||
/** The primitive result union of a lambda's returns (see
|
||||
* optionalPrimitiveResultType), as declared functions record it. */
|
||||
const lambdaArithmeticReturn = (fn: ts.FunctionLikeDeclaration): IrType | null => {
|
||||
if (fn.body && !ts.isBlock(fn.body))
|
||||
return optionalPrimitiveResultType(fn.body as ts.Expression);
|
||||
for (const expression of returnsOf(fn)) {
|
||||
const result = optionalPrimitiveResultType(expression);
|
||||
if (result) return result;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
for (const [symbol, signature] of signatureBySymbol) {
|
||||
const declaration = functionDeclBySymbol.get(symbol);
|
||||
if (!declaration || !isJsSourceFile(declaration.getSourceFile())) continue;
|
||||
@@ -5049,6 +5083,13 @@ export class Lowerer {
|
||||
optionalSymbols.add(field);
|
||||
provenance?.note("static-field", node.left, node.right);
|
||||
changed = true;
|
||||
} else if (field && !optionalSymbols.has(field) && optionalInstanceField(field)) {
|
||||
// The checker narrows the field to the assigned read's bare
|
||||
// type, but the stored value can be undefined: reads keep the
|
||||
// stored union (runtimeOptionalClassField).
|
||||
optionalSymbols.add(field);
|
||||
provenance?.note("assigned-field", node.left, node.right);
|
||||
changed = true;
|
||||
} else if (ts.isIdentifier(node.left.expression)) {
|
||||
const symbol = symbolOf(node.left.expression);
|
||||
if (symbol && noteFieldSymbol(symbol, node.left.name.text)) {
|
||||
@@ -5383,6 +5424,54 @@ export class Lowerer {
|
||||
for (const sf of sourceFiles) if (scanFile(sf)) changed = true;
|
||||
for (const [symbol, decl] of functionDeclBySymbol)
|
||||
if (scanReturns(symbol, decl)) changed = true;
|
||||
for (const fn of tsLambdas) {
|
||||
// An annotated return is the author's contract, as for parameters.
|
||||
if (fn.type) continue;
|
||||
const mapped = this.mapTypeOf(this.typeOf(fn));
|
||||
if (mapped?.kind !== "func") continue;
|
||||
const previous = this.runtimeOptionalFunctionReturnType(fn, mapped.ret);
|
||||
// Async, generator and void bodies keep their own result shapes.
|
||||
if (
|
||||
previous.kind === "promise" ||
|
||||
previous.kind === "generator" ||
|
||||
previous.kind === "void"
|
||||
)
|
||||
continue;
|
||||
const arithmetic = lambdaArithmeticReturn(fn);
|
||||
const next =
|
||||
arithmetic &&
|
||||
(previous.kind === "f64" || previous.kind === "string" || previous.kind === "bool")
|
||||
? arithmetic
|
||||
: bodyReturnsOptional(fn)
|
||||
? this.runtimeOptionalType(previous)
|
||||
: previous;
|
||||
if (!typeEquals(previous, next)) {
|
||||
this.runtimeOptionalFunctionReturns.set(fn, next);
|
||||
provenance?.note("lambda-return", fn, fn.body);
|
||||
changed = true;
|
||||
}
|
||||
// Calls through the lambda's own binding (`const get = () => xs[i];
|
||||
// get()`) produce the same result as calls of a declared function.
|
||||
const binding = fn.parent;
|
||||
if (
|
||||
next !== previous &&
|
||||
ts.isVariableDeclaration(binding) &&
|
||||
binding.initializer === fn &&
|
||||
!binding.type &&
|
||||
ts.isIdentifier(binding.name)
|
||||
) {
|
||||
const symbol = symbolOf(binding.name);
|
||||
if (symbol && next.kind === "union" && this.armTag(next.unionId, UNDEFINED_T) >= 0) {
|
||||
if (!optionalReturns.has(symbol)) {
|
||||
optionalReturns.add(symbol);
|
||||
changed = true;
|
||||
}
|
||||
} else if (symbol && !arithmeticReturns.has(symbol)) {
|
||||
arithmeticReturns.set(symbol, next);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const fn of jsLambdas) {
|
||||
if (!bodyReturnsOptional(fn)) continue;
|
||||
const mapped = this.mapTypeOf(this.typeOf(fn));
|
||||
@@ -5493,11 +5582,11 @@ export class Lowerer {
|
||||
const symbol = symbolOf(node.name);
|
||||
if (!symbol) return;
|
||||
if (
|
||||
isJsSourceFile(sf) &&
|
||||
!node.type &&
|
||||
!hasJsTypeAnnotation(node) &&
|
||||
node.initializer &&
|
||||
(ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))
|
||||
(ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer)) &&
|
||||
(isJsSourceFile(sf) || this.runtimeOptionalFunctionReturns.has(node.initializer))
|
||||
) {
|
||||
const global = this.globalsBySymbol.get(symbol);
|
||||
if (global?.type.kind === "func")
|
||||
@@ -9591,14 +9680,32 @@ export class Lowerer {
|
||||
/* ── scoping and captures ─────────────────────────────────────────── */
|
||||
|
||||
declareLocal(nameNode: ts.Node, name: string, type: IrType, mutable: boolean): IrLocal {
|
||||
return declareContextLocal(
|
||||
const symbol = this.checker.getSymbolAtLocation(nameNode);
|
||||
const local = declareContextLocal(
|
||||
this.ctx,
|
||||
name,
|
||||
type,
|
||||
mutable,
|
||||
this.checker.getSymbolAtLocation(nameNode),
|
||||
symbol,
|
||||
bindingSource(nameNode),
|
||||
);
|
||||
// A binding declared `T | undefined` that receives a missing read: the
|
||||
// checker narrows it to `T` after the assignment, but the stored value
|
||||
// can be undefined. Track it like widened storage so bare reads keep
|
||||
// the union until a guard proves presence.
|
||||
if (
|
||||
symbol &&
|
||||
this.runtimeOptionalAssignedSymbols.has(symbol) &&
|
||||
type.kind === "union" &&
|
||||
this.armTag(type.unionId, UNDEFINED_T) >= 0 &&
|
||||
this.unions.get(type.unionId)?.arms.length === 2 &&
|
||||
!isUnitType(this.stripUndefinedArm(type))
|
||||
) {
|
||||
const root = this.runtimeOptionalRootOf(local);
|
||||
this.runtimeOptionalStorageLocals.add(root);
|
||||
this.runtimeOptionalLocals.add(root);
|
||||
}
|
||||
return local;
|
||||
}
|
||||
|
||||
/** A never-returning call in a value position of type `expected`: the
|
||||
|
||||
@@ -45,7 +45,8 @@ test("numeric byte pipelines retain checked fallbacks and use field widths on bo
|
||||
expect(wide).toContain("phi i64");
|
||||
expect(wide).toContain("@llvm.bswap.i64");
|
||||
const offsets = body(ll, "offsets");
|
||||
expect(offsets).toContain("@llvm.trunc.f64");
|
||||
// Truncation is the intrinsic, or inline on an x86-64 host (trunc.ts).
|
||||
expect(offsets).toMatch(/@llvm\.trunc\.f64|@llvm\.copysign\.f64/);
|
||||
expect(offsets).toContain("@scr_dataview_set");
|
||||
expect(offsets).toContain("@scr_dataview_get");
|
||||
expect(offsets).toMatch(/icmp ule i(?:32|64)/);
|
||||
|
||||
@@ -10493,6 +10493,24 @@
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/missing-read-lambda-results.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/missing-read-lambda-results.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/missing-read-narrowed-storage.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/missing-read-narrowed-storage.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/missing-read-number-conversions.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/missing-read-number-conversions.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/module-fallback-functions/main.js": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/module-fallback-functions/platform.js",
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
// A missing read (an out-of-range typed-array or array read the checker
|
||||
// types as `number`) defaulted with `??` or `||` to a string, or returned
|
||||
// as is, gives a lambda a result its inferred return type does not spell:
|
||||
// TypeScript types `read ?? "text"` and `() => xs[i]` as number. The
|
||||
// closure returns the runtime value, through bindings, callbacks taking
|
||||
// `unknown`, calls of the binding, and the enclosing function's return.
|
||||
const base: number = process.argv.length;
|
||||
const chars = new Uint16Array(4);
|
||||
chars[0] = 108;
|
||||
const nums: number[] = [7];
|
||||
const far = base + 100;
|
||||
const a = chars[far];
|
||||
const c0 = chars[0];
|
||||
|
||||
// The scanbox probe table: every arrow is typed `() => number` by
|
||||
// inference and stored in a `() => unknown` slot.
|
||||
const probes: [string, () => unknown][] = [
|
||||
["plus", () => a + 1],
|
||||
["or", () => a | 0],
|
||||
["max", () => Math.max(a, 1)],
|
||||
["fcc", () => JSON.stringify(String.fromCharCode(c0, a))],
|
||||
["and", () => c0 && 5],
|
||||
["or2", () => chars[far] || 7],
|
||||
["nullish", () => a ?? "dflt"],
|
||||
["nullish present", () => c0 ?? "dflt"],
|
||||
["tern", () => (a ? "yes" : "no")],
|
||||
["bang", () => !a],
|
||||
["concat", () => a + "s"],
|
||||
["missing", () => nums[far]],
|
||||
];
|
||||
for (const [name, f] of probes) {
|
||||
try {
|
||||
console.log(name, f());
|
||||
} catch (e) {
|
||||
console.log(name, "threw", (e as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
function take(f: () => unknown): unknown {
|
||||
return f();
|
||||
}
|
||||
console.log("take nullish", take(() => a ?? "d1"));
|
||||
console.log("take missing", take(() => nums[far]));
|
||||
|
||||
const bound = () => a ?? "d2";
|
||||
console.log("bound", bound());
|
||||
const block = () => {
|
||||
return a ?? "d3";
|
||||
};
|
||||
console.log("block", block());
|
||||
const expression = function () {
|
||||
return a || "d4";
|
||||
};
|
||||
console.log("expression", expression());
|
||||
const read = () => nums[far];
|
||||
console.log("read", read(), read() === undefined);
|
||||
|
||||
function viaBinding(i: number): number {
|
||||
const get = () => nums[i];
|
||||
return get();
|
||||
}
|
||||
console.log("via binding", viaBinding(far), viaBinding(0));
|
||||
|
||||
function declared() {
|
||||
return a ?? "d5";
|
||||
}
|
||||
console.log("declared", declared());
|
||||
|
||||
class Lexer {
|
||||
peek(i: number) {
|
||||
return chars[i] ?? "eof";
|
||||
}
|
||||
}
|
||||
console.log("method", new Lexer().peek(far), new Lexer().peek(0));
|
||||
@@ -0,0 +1,88 @@
|
||||
// A `T | undefined` slot assigned a missing read (an out-of-range
|
||||
// typed-array or array read the checker types as `T`) holds undefined,
|
||||
// even though TypeScript narrows later reads to `T`. Reads keep the stored
|
||||
// value: printing shows undefined, arithmetic gives NaN, and a member read
|
||||
// throws Node's TypeError. Class fields (through `obj.f` and `this.f`) and
|
||||
// locals both follow it; a presence guard still narrows.
|
||||
const base: number = process.argv.length;
|
||||
const chars = new Uint16Array([5, 6]);
|
||||
const nums: number[] = [7];
|
||||
const far = base + 100;
|
||||
|
||||
function twice(n: number): number {
|
||||
return n * 2;
|
||||
}
|
||||
|
||||
class Cursor {
|
||||
token: number | undefined = 0;
|
||||
peek(i: number): number {
|
||||
return chars[i];
|
||||
}
|
||||
advance(i: number): string {
|
||||
this.token = chars[i];
|
||||
return `${this.token} ${this.token === undefined} ${this.token + 1}`;
|
||||
}
|
||||
}
|
||||
|
||||
function fields(i: number, label: string): void {
|
||||
const cursor = new Cursor();
|
||||
cursor.token = chars[i];
|
||||
console.log(label, "direct", cursor.token, cursor.token === undefined);
|
||||
console.log(label, "ops", cursor.token + 1, -cursor.token, cursor.token < 3, `${cursor.token}`);
|
||||
console.log(label, "defaults", cursor.token ?? "none", cursor.token || "falsy");
|
||||
console.log(label, "call", twice(cursor.token), [cursor.token]);
|
||||
cursor.token = cursor.peek(i);
|
||||
console.log(label, "method", cursor.token, typeof cursor.token);
|
||||
cursor.token = nums[i];
|
||||
console.log(label, "array", cursor.token, String(cursor.token));
|
||||
console.log(label, "this", cursor.advance(i));
|
||||
if (cursor.token !== undefined) console.log(label, "guarded", cursor.token * 2);
|
||||
else console.log(label, "guarded absent");
|
||||
}
|
||||
fields(far, "missing");
|
||||
fields(0, "present");
|
||||
|
||||
function locals(i: number, label: string): void {
|
||||
let code: number | undefined = 0;
|
||||
code = chars[i];
|
||||
console.log(label, "local", code, code + 1, `${code}`, [code], twice(code));
|
||||
let late: number | undefined;
|
||||
late = nums[i];
|
||||
console.log(label, "late", late, late === undefined, late ?? "none");
|
||||
let branch: number | undefined = 1;
|
||||
if (base > 0) branch = chars[i];
|
||||
console.log(label, "branch", branch, typeof branch);
|
||||
if (code !== undefined) console.log(label, "local guarded", code * 3);
|
||||
const viaExpr = (code = chars[i]);
|
||||
console.log(label, "assign expr", viaExpr, code);
|
||||
}
|
||||
locals(far, "missing");
|
||||
locals(1, "present");
|
||||
|
||||
class Node2 {
|
||||
v: number;
|
||||
constructor(v: number) {
|
||||
this.v = v;
|
||||
}
|
||||
}
|
||||
const pool: Node2[] = [new Node2(4)];
|
||||
|
||||
class Holder {
|
||||
node: Node2 | undefined = undefined;
|
||||
}
|
||||
|
||||
function objects(i: number, label: string): void {
|
||||
const holder = new Holder();
|
||||
holder.node = pool[i];
|
||||
console.log(label, "object field", holder.node === undefined);
|
||||
try {
|
||||
console.log(label, "object member", holder.node.v);
|
||||
} catch (e) {
|
||||
console.log(label, "object member threw", (e as Error).name, (e as Error).message);
|
||||
}
|
||||
let current: Node2 | undefined = new Node2(1);
|
||||
current = pool[i];
|
||||
console.log(label, "object local", current === undefined);
|
||||
}
|
||||
objects(far, "missing");
|
||||
objects(0, "present");
|
||||
@@ -0,0 +1,95 @@
|
||||
// Builtins that convert a number argument with ToNumber see a missing
|
||||
// read (an out-of-range typed-array or array read the checker types as
|
||||
// `number`) as NaN, ToNumber's answer for undefined: String.fromCharCode
|
||||
// yields code unit 0, fromCodePoint throws a RangeError, Math answers NaN,
|
||||
// isNaN is true. The missing value reaches them from a module binding, a
|
||||
// local, a class field and a record field.
|
||||
const base: number = process.argv.length;
|
||||
const chars = new Uint16Array(4);
|
||||
chars[0] = 108;
|
||||
chars[1] = 66;
|
||||
const nums: number[] = [7];
|
||||
const far = base + 100;
|
||||
|
||||
const missing = chars[far];
|
||||
const present = chars[1];
|
||||
const missingNum = nums[far];
|
||||
|
||||
function show(label: string, value: unknown): void {
|
||||
console.log(label, typeof value === "string" ? JSON.stringify(value) : value);
|
||||
}
|
||||
|
||||
function attempt(label: string, run: () => unknown): void {
|
||||
try {
|
||||
show(label, run());
|
||||
} catch (e) {
|
||||
console.log(label, "threw", (e as Error).name, (e as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
// String.fromCharCode / fromCodePoint.
|
||||
show("fcc module", String.fromCharCode(chars[0], missing));
|
||||
show("fcc single", String.fromCharCode(missing));
|
||||
show("fcc present", String.fromCharCode(chars[0], present));
|
||||
show("fcc array", String.fromCharCode(missingNum, 65));
|
||||
attempt("fcp module", () => String.fromCodePoint(missing));
|
||||
show("fcp present", String.fromCodePoint(present));
|
||||
|
||||
// Math functions.
|
||||
show("abs", Math.abs(missing));
|
||||
show("floor", Math.floor(missingNum));
|
||||
show("max", Math.max(missing, 1));
|
||||
show("min3", Math.min(1, 2, missing));
|
||||
show("imul", Math.imul(missing, 3));
|
||||
show("pow", Math.pow(present, 2));
|
||||
show("hypot", Math.hypot(missing, 3));
|
||||
|
||||
// The global number predicates convert; the Number statics do not.
|
||||
show("isNaN", isNaN(missing));
|
||||
show("isNaN present", isNaN(present));
|
||||
show("isFinite", isFinite(missing));
|
||||
show("isFinite present", isFinite(present));
|
||||
show("Number.isNaN", Number.isNaN(missing));
|
||||
show("Number.isFinite", Number.isFinite(missing));
|
||||
|
||||
// DataView offsets (ToIndex) and values (ToNumber).
|
||||
const view = new DataView(new ArrayBuffer(4));
|
||||
view.setUint8(1, 9);
|
||||
show("dv get", view.getUint8(missing));
|
||||
view.setUint8(missing, 5);
|
||||
show("dv set offset", view.getUint8(0));
|
||||
view.setUint8(2, missing);
|
||||
show("dv set value", view.getUint8(2));
|
||||
|
||||
// Typed-array ranges: a missing start is 0, a missing end is the length.
|
||||
show("slice start", chars.slice(missing).length);
|
||||
show("slice end", chars.slice(1, missing).length);
|
||||
show("subarray start", chars.subarray(missing).length);
|
||||
show("subarray end", chars.subarray(2, missing).length);
|
||||
|
||||
// The same reads held in a local, a class field and a record field.
|
||||
function local(i: number, label: string): void {
|
||||
let code: number | undefined = 0;
|
||||
code = chars[i];
|
||||
show(`local fcc ${label}`, String.fromCharCode(97, code));
|
||||
show(`local max ${label}`, Math.max(code, 1));
|
||||
show(`local isNaN ${label}`, isNaN(code));
|
||||
}
|
||||
local(far, "missing");
|
||||
local(1, "present");
|
||||
|
||||
class Cursor {
|
||||
code: number | undefined = 0;
|
||||
}
|
||||
const cursor = new Cursor();
|
||||
cursor.code = chars[far];
|
||||
show("field fcc", String.fromCharCode(97, cursor.code));
|
||||
show("field abs", Math.abs(cursor.code));
|
||||
cursor.code = chars[1];
|
||||
show("field fcc present", String.fromCharCode(97, cursor.code));
|
||||
|
||||
const record: { code: number | undefined } = { code: 0 };
|
||||
record.code = chars[far];
|
||||
show("record fcc", String.fromCharCode(97, record.code));
|
||||
show("record max", Math.max(record.code, 1));
|
||||
show("record isNaN", isNaN(record.code));
|
||||
Reference in New Issue
Block a user