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:
Malte Ubl
2026-10-11 08:28:02 -07:00
13 changed files with 509 additions and 36 deletions
@@ -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));