Share storage between T[] and (T | undefined)[] (#2080)

This commit is contained in:
Chris Tate
2026-10-25 05:40:56 -05:00
committed by GitHub
30 changed files with 1401 additions and 112 deletions
+54 -3
View File
@@ -66,6 +66,25 @@ import { recordMayKeepSource, recordSourceFieldOrder } from "./record-sources.js
import type { WalkerHost } from "./walkers.js";
import type { LlStreamTypedRefAdapter } from "./expr-context.js";
/** typeKey with `?` after every `(T | undefined)[]` (optionalElements),
* at any depth of arrays, Maps, Sets and promises: checked conversions to
* `number[][]` and `(number | undefined)[][]` need separate helpers, though
* the two types share a typeKey. */
function checkedConversionKey(t: IrType): string {
switch (t.kind) {
case "array":
return `array<${checkedConversionKey(t.elem)}>${t.optionalElements ? "?" : ""}`;
case "map":
return `map<${checkedConversionKey(t.key)},${checkedConversionKey(t.value)}>`;
case "set":
return `set<${checkedConversionKey(t.elem)}>`;
case "promise":
return `promise<${checkedConversionKey(t.inner)}>`;
default:
return typeKey(t);
}
}
/** Whether to-dyn of a record shape keeps the record itself: an empty shape
* and an event-listener record (a `handleEvent` function field) box their
* identity. Every other shape, tuples included, copies its fields into a
@@ -759,6 +778,13 @@ export class LlDyn {
/* ── dyn node plumbing ─────────────────────────────────────────────── */
/** Loads a dyn node's kind tag (after the size_t rc word). */
/** i1: the checked-dynamic value `d` is undefined. */
isUndefined(B: BlockBuilder, d: string): string {
const is = B.tmp();
B.line(`${is} = icmp eq i32 ${this.kindOf(B, d)}, ${DYN_KIND.UNDEF}`);
return is;
}
private kindOf(B: BlockBuilder, d: string): string {
const p = B.tmp();
const k = B.tmp();
@@ -1145,7 +1171,7 @@ export class LlDyn {
/** `sc_dm_<n>(ptr d) -> i1` — does this dyn fit T? Never throws. */
dynMatchHelper(t: IrType, preserveRefs = false): string {
const key = typeKey(t);
const key = checkedConversionKey(t);
const matcherKey = preserveRefs ? `identity:${key}` : key;
const existing = this.dynMatchers.get(matcherKey);
if (existing) return existing;
@@ -1460,7 +1486,7 @@ export class LlDyn {
B.startBlock(l0);
const n = this.lenOf(B, "%d");
const items = this.itemsOf(B, "%d");
this.i64Loop(B, "dm.el", n, (i, brNext) => {
this.i64Loop(B, "dm.el", n, (i, brNext, next) => {
this.host.declare(`declare zeroext i1 @scr_dyn_arr_has_index(ptr, ${this.S})`);
const present = B.tmp();
B.line(`${present} = call zeroext i1 @scr_dyn_arr_has_index(ptr %d, ${this.S} ${i})`);
@@ -1471,6 +1497,15 @@ export class LlDyn {
brNext();
B.startBlock(valueLabel);
const e = this.itemAt(B, items, i);
if (t.optionalElements) {
// A `(T | undefined)[]` stored as T elements takes undefined
// elements as well.
const undef = B.tmp();
const checkLabel = B.newLabel("dm.check");
B.line(`${undef} = icmp eq i32 ${this.kindOf(B, e)}, ${DYN_KIND.UNDEF}`);
B.condBr(undef, next, checkLabel);
B.startBlock(checkLabel);
}
const ok = B.tmp();
B.line(`${ok} = call zeroext i1 @${m}(ptr ${e})`);
const lOk = B.newLabel("dm.eo");
@@ -1946,7 +1981,8 @@ export class LlDyn {
* BUILD the typed value (+1), or throw the catchable path-annotated
* TypeError and return a dummy with the pending flag set. */
dynCheckHelper(t: IrType, preserveRefs = false): string {
const key = typeKey(t);
// A `(T | undefined)[]` stored as T elements also takes undefined ones.
const key = checkedConversionKey(t);
const builderKey = preserveRefs ? `identity:${key}` : key;
const existing = this.dynBuilders.get(builderKey);
if (existing) return existing;
@@ -2753,6 +2789,21 @@ export class LlDyn {
B.line(`${ip} = getelementptr inbounds %ScrDynPath, ptr ${pathSlot}, i64 0, i32 2`);
B.line(`store ${host.sizeType} ${i}, ptr ${ip}`);
const e = this.itemAt(B, items, i);
if (t.optionalElements) {
// An undefined element is a present undefined element.
const undef = B.tmp();
const storeUndefined = B.newLabel("dca.undef");
const checkLabel = B.newLabel("dca.check");
B.line(`${undef} = icmp eq i32 ${this.kindOf(B, e)}, ${DYN_KIND.UNDEF}`);
B.condBr(undef, storeUndefined, checkLabel);
B.startBlock(storeUndefined);
host.declare(`declare void @scr_arr_set_undefined(ptr, double)`);
const at = B.tmp();
B.line(`${at} = ${sizeToDouble(this.S)} ${this.S} ${i} to double`);
B.line(`call void @scr_arr_set_undefined(ptr ${a}, double ${at})`);
B.br(doneLabel);
B.startBlock(checkLabel);
}
const v = B.tmp();
B.line(`${v} = call ${this.valTy(elem)} @${c}(ptr ${e}, ptr ${pathSlot})`);
this.pendingBail(
@@ -792,13 +792,16 @@ export function streamTypedRefMaterializeAdapter(
t: IrType,
): LlStreamTypedRefAdapter {
const key = typeKey(t);
const existing = host.liveDynRefAdapters.get(key);
// A `(T | undefined)[]` stored as T elements shares T[]'s capsule type,
// but its adapters also store undefined elements.
const adapterKey = t.kind === "array" && t.optionalElements ? `${key}?` : key;
const existing = host.liveDynRefAdapters.get(adapterKey);
if (existing) return existing;
// The converter depends only on the static type. Intern the complete
// graph across roots and union arms so recursive types emit once.
const snapshot = `sc_ldr_${host.liveDynRefAdapters.size}_materialize`;
const adapter: LlStreamTypedRefAdapter = { snapshot, commit: "null" };
host.liveDynRefAdapters.set(key, adapter);
host.liveDynRefAdapters.set(adapterKey, adapter);
host.journal?.allocated(
snapshot.replace(/_materialize$/, ""),
"streamTypedRefMaterializeAdapter",
@@ -1312,6 +1315,18 @@ function liveArrayOps(
G.terminate(`ret ptr ${boxed}`);
const S = new BlockBuilder();
if (t.optionalElements) {
// A `(T | undefined)[]` stores undefined as a present undefined.
const undef = host.dyn.isUndefined(S, "%v");
const storeUndefined = S.newLabel("live.array.undefined");
const other = S.newLabel("live.array.value");
S.condBr(undef, storeUndefined, other);
S.startBlock(storeUndefined);
host.declare(`declare void @scr_arr_set_undefined(ptr, double)`);
S.line(`call void @scr_arr_set_undefined(ptr %p, double %i)`);
S.terminate(`ret i1 true`);
S.startBlock(other);
}
const fits = S.tmp();
S.line(`${fits} = call zeroext i1 @${host.dyn.dynMatchHelper(elem)}(ptr %v)`);
const store = S.newLabel("live.array.store");
@@ -269,6 +269,140 @@ function readsMutableLocal(lowerer: Lowerer, e: IrExpr): boolean {
});
}
/** A number, string or boolean element type. Arrays of it hold a present
* undefined in the element's state, so `(T | undefined)[]` stores T
* elements (the type mapper) and a T[] seen as one stays the same array. */
export function primitiveElement(elem: IrType): boolean {
return elem.kind === "f64" || elem.kind === "string" || elem.kind === "bool";
}
/** `value` is the element type with undefined, stored into an array of a
* primitive element type: a present undefined or the element. */
export function optionalElementValue(lowerer: Lowerer, value: IrExpr, elem: IrType): boolean {
if (!primitiveElement(elem)) return false;
if (value.type.kind === "undefinedT") return true;
return (
value.type.kind === "union" &&
lowerer.armTag(value.type.unionId, UNDEFINED_T) >= 0 &&
typeEquals(lowerer.stripUndefinedArm(value.type), elem)
);
}
/** A value of a primitive element type that holds a slot until the value
* stored there replaces it (insertedValues), or stands for a slot that is
* not read. */
export function elementPlaceholder(elem: IrType, loc: SrcLoc): IrExpr {
if (elem.kind === "f64") return { kind: "numLit", value: 0, type: F64, loc };
if (elem.kind === "bool") return { kind: "boolLit", value: false, type: BOOL, loc };
return { kind: "strLit", value: "", type: STRING, loc };
}
/** Values inserted into an array (push, unshift, splice, a literal), in
* source order. A value that can be undefined (optionalElementValue) is
* kept in a temporary: the insertion takes a placeholder for it, and
* `fill` then stores the value at the placeholder's index. */
export function insertedValues(
lowerer: Lowerer,
values: readonly IrExpr[],
elem: IrType,
coerce: (value: IrExpr, index: number) => IrExpr,
loc: SrcLoc,
): {
stmts: IrStmt[];
args: IrExpr[];
optional: boolean;
fill: (arr: IrExpr, first: IrExpr) => IrStmt[];
} {
if (!values.some((value) => optionalElementValue(lowerer, value, elem)))
return { stmts: [], args: values.map(coerce), optional: false, fill: () => [] };
const stmts: IrStmt[] = [];
const args: IrExpr[] = [];
const held: { offset: number; value: IrExpr }[] = [];
values.forEach((value, offset) => {
// Every value is evaluated in order before the insertion.
if (value.type.kind === "undefinedT") {
if (value.kind !== "unitLit") stmts.push({ kind: "exprStmt", expr: value, loc });
args.push(elementPlaceholder(elem, loc));
held.push({ offset, value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc } });
return;
}
const temp = lowerer.declareHiddenLocal("%inserted", value.type);
stmts.push({ kind: "varDecl", localId: temp.id, init: value, loc });
const stable = varRef(temp.id, temp.type, loc);
if (optionalElementValue(lowerer, value, elem)) {
args.push(elementPlaceholder(elem, loc));
held.push({ offset, value: stable });
} else {
args.push(coerce(stable, offset));
}
});
const fill = (arr: IrExpr, first: IrExpr): IrStmt[] =>
held.map(({ offset, value }) =>
arrayValueStore(
lowerer,
arr,
offset === 0
? first
: {
kind: "bin",
op: "+",
left: first,
right: { kind: "numLit", value: offset, type: F64, loc },
type: F64,
loc,
},
value,
elem,
loc,
),
);
return { stmts, args, optional: true, fill };
}
/** Statements appending `value` to `arr` inside a lifted helper. A value
* that can be undefined in a primitive array (optionalElementValue) is held
* in the helper's local `slot` (which the caller declares with the value's
* type) and stored as a present undefined or the element; any other value
* converts to the element type. */
export function appendElement(
lowerer: Lowerer,
arr: IrExpr,
value: IrExpr,
elem: IrType,
slot: string,
loc: SrcLoc,
): IrStmt[] {
if (optionalElementValue(lowerer, value, elem) && value.type.kind !== "undefinedT") {
const held = varRef(slot, value.type, loc);
const length: IrExpr = {
kind: "arrIntrinsic",
method: "length",
receiver: arr,
args: [],
type: F64,
loc,
};
return [
{ kind: "varDecl", localId: slot, init: value, loc },
arrayValueStore(lowerer, arr, length, held, elem, loc),
];
}
return [
{
kind: "exprStmt",
expr: {
kind: "arrIntrinsic",
method: "push",
receiver: arr,
args: [lowerer.coerceToExpected(value, elem)],
type: F64,
loc,
},
loc,
},
];
}
/** Store a stabilized value without changing the array's element ABI. */
export function arrayValueStore(
lowerer: Lowerer,
@@ -279,6 +413,12 @@ export function arrayValueStore(
loc: SrcLoc,
): IrStmt {
if (typeEquals(value.type, elem)) return { kind: "arraySet", arr, index, value, loc };
// An undefined value of a `(T | undefined)[]` (primitiveElement).
if (value.type.kind === "undefinedT" && primitiveElement(elem)) {
const write: IrStmt = { kind: "arraySetUndefined", arr, index, loc };
if (value.kind === "unitLit") return write;
return { kind: "block", body: [{ kind: "exprStmt", expr: value, loc }, write], loc };
}
if (value.type.kind === "union" && lowerer.armTag(value.type.unionId, UNDEFINED_T) >= 0) {
// The tag test and the VALUE arm extraction both consume the value. Keep
// an arbitrary expression (especially an array read or call) single
@@ -3,7 +3,8 @@ import { checkedPromiseAll, thenReader } from "../checked-promise-all.js";
import { InternalCompilerError } from "../../../errors.js";
import * as ts from "../../ts7/adapter.js";
import type { Lowerer } from "../lowerer.js";
import { varRef } from "../../../ir/build.js";
import { countedFor, varRef } from "../../../ir/build.js";
import { arrayValueStore, optionalElementValue, primitiveElement } from "../array-values.js";
import { maybePromiseResolve, maybePromiseUnionOf } from "../maybe-promise.js";
import { isJsSourceFile, locOf } from "../../program.js";
import {
@@ -14,6 +15,7 @@ import {
type IrFunction,
type IrStmt,
type IrType,
type SrcLoc,
UNDEFINED_T,
VOID,
arrayOf,
@@ -435,6 +437,28 @@ export function lowerPromiseStaticCall(
inner.kind === "void"
? { kind: "promise", inner: VOID }
: lowerer.mapTypeOf(lowerer.typeOf(call));
// Promise<T | undefined>[] combines into a `(T | undefined)[]`, which
// stores T elements (primitiveElement): collect the values, then copy
// them in with their present undefined values.
if (
resultT?.kind === "promise" &&
resultT.inner.kind === "array" &&
primitiveElement(resultT.inner.elem) &&
optionalElementValue(
lowerer,
{ kind: "unitLit", unit: "undefined", type: inner, loc },
resultT.inner.elem,
)
) {
const joined = optionalPromiseAll(
lowerer,
entries,
inner,
resultT as IrType & { kind: "promise" },
loc,
);
if (joined) return joined;
}
if (
resultT?.kind !== "promise" ||
(inner.kind !== "void" &&
@@ -954,3 +978,139 @@ export function lowerAwaitedUniformPromiseAll(
loc,
};
}
/** `Promise.all(ps)` over `Promise<T | undefined>[]` for a number, string or
* boolean T, whose result is a `(T | undefined)[]` (stored as T elements).
* The combinator joins the entries (promise.allTuple), and its builder
* reads each fulfilled entry into the result with its present undefined,
* inside the combinator's settlement, so the result settles on Node's
* turn. Entries of another type keep the general combinator. */
function optionalPromiseAll(
lowerer: Lowerer,
entries: IrExpr,
inner: IrType,
resultT: IrType & { kind: "promise" },
loc: SrcLoc,
): IrExpr | null {
const entriesT = entries.type;
if (entriesT.kind !== "array" || entriesT.elem.kind !== "promise") return null;
if (!typeEquals(entriesT.elem.inner, inner)) return null;
const outT = resultT.inner as IrType & { kind: "array" };
const voidPromise: IrType & { kind: "promise" } = { kind: "promise", inner: VOID };
const joinedT = arrayOf(voidPromise);
const key = `promise.allOptional:${typeKey(entriesT)}:${typeKey(outT)}`;
let helper = lowerer.arrHofHelpers.get(key);
if (!helper) {
helper = keyedHelperName(lowerer, "%promise.allOptional", key);
lowerer.arrHofHelpers.set(key, helper);
const builder = keyedHelperName(lowerer, "%promise.allOptionalValues", key);
const length = (arr: IrExpr): IrExpr => ({
kind: "arrIntrinsic",
method: "length",
receiver: arr,
args: [],
type: F64,
loc,
});
const joined = varRef("joined.0", joinedT, loc);
const out = varRef("out.0", outT, loc);
lowerer.liftedFns.push({
name: builder,
params: [{ localId: "joined.0", name: "joined", type: joinedT }],
returnType: outT,
locals: [
{ id: "joined.0", name: "joined", type: joinedT, mutable: false },
{ id: "out.0", name: "out", type: outT, mutable: false },
{ id: "i.0", name: "i", type: F64, mutable: true },
{ id: "v.0", name: "v", type: inner, mutable: false },
],
body: [
{
kind: "varDecl",
localId: "out.0",
init: { kind: "arrayLit", elems: [], type: outT, loc },
loc,
},
countedFor(loc, length(joined), (index) => [
{
kind: "varDecl",
localId: "v.0",
init: {
kind: "libCall",
fn: "promise.fulfilledValue",
args: [{ kind: "arrayGet", arr: joined, index, type: voidPromise, loc }],
type: inner,
loc,
},
loc,
},
arrayValueStore(lowerer, out, index, varRef("v.0", inner, loc), outT.elem, loc),
]),
{ kind: "return", value: out, loc },
],
loc,
});
const ps = varRef("ps.0", entriesT, loc);
lowerer.liftedFns.push({
name: helper,
params: [{ localId: "ps.0", name: "ps", type: entriesT }],
returnType: resultT,
locals: [
{ id: "ps.0", name: "ps", type: entriesT, mutable: false },
{ id: "joined.0", name: "joined", type: joinedT, mutable: false },
{ id: "i.0", name: "i", type: F64, mutable: true },
],
body: [
{
kind: "varDecl",
localId: "joined.0",
init: { kind: "arrayLit", elems: [], type: joinedT, loc },
loc,
},
countedFor(loc, length(ps), (index) => [
{
kind: "exprStmt",
expr: {
kind: "arrIntrinsic",
method: "push",
receiver: joined,
args: [
{
kind: "promiseVoidWiden",
value: { kind: "arrayGet", arr: ps, index, type: entriesT.elem, loc },
type: voidPromise,
loc,
},
],
type: F64,
loc,
},
loc,
},
]),
{
kind: "return",
value: {
kind: "libCall",
fn: "promise.allTuple",
args: [
joined,
{
kind: "closure",
fnName: builder,
captures: [],
type: funcOf([joinedT], outT),
loc,
},
],
type: resultT,
loc,
},
loc,
},
],
loc,
});
}
return { kind: "call", callee: helper, args: [entries], type: resultT, loc };
}
@@ -28,6 +28,7 @@ import { checkedIterableSpread } from "./checked-iterable-spread.js";
import { type ParamShape } from "./call-signatures.js";
import { arrayValueRead, arrayValueType } from "./array-values.js";
import { buildArrayConversion } from "./coercions/builders.js";
import { lowerArraySpreadItems, lowerArrayValueItems } from "./containers/array-construction.js";
import { restTupleElementTypes, typeKey } from "../type-mapper.js";
function fixedTupleSpreadInfo(
@@ -403,6 +404,51 @@ export function completeArgs(
}
}
if (restType.kind !== "array") lowerer.unsupported("SC1090", blame, "this call form");
// A `(T | undefined)[]` rest (stored as T elements) packs undefined
// arguments as present undefined elements.
const restNodes = sources.slice(restAt);
// Plain arguments and spreads, also together with spread tuple fields
// (pre-lowered): each item, in argument order.
if (
restType.optionalElements === true &&
(!restNodes.some(isIr) || restNodes.some((a) => !isIr(a) && ts.isSpreadElement(a)))
) {
const items: (ts.Expression | IrExpr)[] = [];
for (const a of restNodes) {
if (isIr(a)) items.push(a.ir);
else items.push(a);
}
out.push(lowerArraySpreadItems(lowerer, items, restType.elem, restType, loc));
return out;
}
// Tuple-spread values (pre-lowered) and plain arguments pack the same
// way. Each value evaluates once, in argument order.
if (
restType.optionalElements === true &&
!restNodes.some((a) => !isIr(a) && ts.isSpreadElement(a))
) {
const stmts: IrStmt[] = [];
const values = restNodes.map((a): IrExpr => {
// A tuple field is read where its spread appears: a later
// argument may write the tuple.
let value: IrExpr;
if (isIr(a)) {
value = a.ir;
if (value.kind === "seqExpr") {
stmts.push(...value.stmts);
value = value.result;
}
} else value = lowerer.lowerExpr(a as ts.Expression);
const saved = lowerer.declareHiddenLocal("%callArg", value.type);
stmts.push({ kind: "varDecl", localId: saved.id, init: value, loc: value.loc });
return { kind: "varRef", localId: saved.id, type: value.type, loc: value.loc };
});
const items = lowerArrayValueItems(lowerer, values, restType.elem, restType, loc);
out.push(
stmts.length === 0 ? items : { kind: "seqExpr", stmts, result: items, type: restType, loc },
);
return out;
}
// The rest pack is a fresh array per call; surplus SPREADS copy
// their elements in (JS-exact — `f(a, ...xs, b, ...ys)` packs in
// order, sources untouched).
@@ -1,12 +1,13 @@
import { keyedHelperName } from "../stable-names.js";
import { constructedValue } from "../constructor-returns.js";
import { dynUndefinedExpr, varRef } from "../../../ir/build.js";
import { countedFor, dynUndefinedExpr, varRef } from "../../../ir/build.js";
import type { FieldLift } from "./structural-plans.js";
import { buildArrayConversion } from "./builders.js";
import { arrayValueRead, arrayValueStore, primitiveElement } from "../array-values.js";
import type { WidthLift } from "../width-lift.js";
import { InternalCompilerError } from "../../../errors.js";
import type { IrExpr, IrFunction, IrRecordShape, IrStmt, IrType, SrcLoc } from "../../../ir/ir.js";
import { BOOL, DYN, F64, STRING, UNDEFINED_T } from "../../../ir/ir.js";
import { BOOL, DYN, F64, STRING, UNDEFINED_T, typeEquals } from "../../../ir/ir.js";
import { typeKey } from "../../type-mapper.js";
import { findStaticOn, findGenericStaticOn } from "../lower-classes.js";
import { lowerRecordOvfCaptureHelper } from "../containers/indexed-objects.js";
@@ -54,6 +55,9 @@ export function widthCoerce(lowerer: Lowerer, expr: IrExpr, expected: IrType): I
return lowerer.classStaticsProjection(expr.type.className, expected.shapeId, expr.loc);
}
if (expected.kind === "array" && expr.type.kind === "array") {
const optional = optionalArrayHelper(lowerer, expr.type, expected, expr.loc);
if (optional)
return { kind: "call", callee: optional, args: [expr], type: expected, loc: expr.loc };
const helper = lowerer.arrayWidthHelper(expr.type, expected, expr.loc);
if (helper)
return { kind: "call", callee: helper, args: [expr], type: expected, loc: expr.loc };
@@ -518,6 +522,69 @@ export function emptyArrayLiftHelper(
return name;
}
/** Copy an array of `T | undefined` unions (a collection's values, an
* iterator drain) into a `(T | undefined)[]` of a number, string or
* boolean T, which stores T elements: undefined values become present
* undefined elements. Null for other element types. */
function optionalArrayHelper(
lowerer: Lowerer,
fromT: IrType & { kind: "array" },
toT: IrType & { kind: "array" },
loc: SrcLoc,
): string | null {
const from = fromT.elem;
const to = toT.elem;
if (
!primitiveElement(to) ||
from.kind !== "union" ||
lowerer.armTag(from.unionId, UNDEFINED_T) < 0 ||
!typeEquals(lowerer.stripUndefinedArm(from), to)
)
return null;
const key = `arrOptional:${typeKey(from)}:${typeKey(to)}`;
const existing = lowerer.valueHelpers.get(key);
if (existing) return existing;
const name = keyedHelperName(lowerer, "%arr.width", key);
lowerer.valueHelpers.set(key, name);
const input = varRef("a.0", fromT, loc);
const out = varRef("out.0", toT, loc);
lowerer.liftedFns.push({
name,
params: [{ localId: "a.0", name: "a", type: fromT }],
returnType: toT,
locals: [
{ id: "a.0", name: "a", type: fromT, mutable: true },
{ id: "out.0", name: "out", type: toT, mutable: false },
{ id: "i.0", name: "i", type: F64, mutable: true },
{ id: "v.0", name: "v", type: from, mutable: false },
],
body: [
{
kind: "varDecl",
localId: "out.0",
init: { kind: "arrayLit", elems: [], type: toT, loc },
loc,
},
countedFor(
loc,
{ kind: "arrIntrinsic", method: "length", receiver: input, args: [], type: F64, loc },
(index) => [
{
kind: "varDecl",
localId: "v.0",
init: arrayValueRead(lowerer, input, index, from, loc),
loc,
},
arrayValueStore(lowerer, out, index, varRef("v.0", from, loc), to, loc),
],
),
{ kind: "return", value: out, loc },
],
loc,
});
return name;
}
/** Copy an array through its validated element conversion. Intern before
* building the body so recursive element conversions can reuse this helper. */
export function arrayWidthHelper(
@@ -542,14 +609,37 @@ export function arrayWidthHelper(
name,
arrT,
{ kind: "arrayLit", elems: [], type: outT, loc },
(element, _index, result) => ({
kind: "arrIntrinsic",
method: "push",
receiver: result,
args: [lowerer.applyWidthLift(elemLift, element, toElem, loc)],
type: F64,
loc,
}),
(element, index, result) => {
let value = lowerer.applyWidthLift(elemLift, element, toElem, loc);
// A number, string or boolean array can hold present undefined
// elements (`(T | undefined)[]` stores T): they, and holes, copy
// as the target's undefined.
const missing = primitiveElement(fromElem) ? lowerer.wrappedUndefined(toElem, loc) : null;
if (missing !== null)
value = {
kind: "ternary",
cond: {
kind: "bin",
op: "===",
left: { kind: "arrayState", arr: varRef("a.0", arrT, loc), index, type: F64, loc },
right: { kind: "numLit", value: 1, type: F64, loc },
type: BOOL,
loc,
},
then: value,
else_: missing,
type: toElem,
loc,
};
return {
kind: "arrIntrinsic",
method: "push",
receiver: result,
args: [value],
type: F64,
loc,
};
},
loc,
),
);
@@ -42,6 +42,7 @@ import {
arrayValueStore,
arrayValueType,
currentArrayIndexPresent,
primitiveElement,
} from "../array-values.js";
import { typeKey } from "../../type-mapper.js";
import {
@@ -1690,6 +1691,8 @@ function flatMapUnionPlan(
}
const converts = (t: IrType): boolean => {
if (typeEquals(t, outElem)) return true;
// A `(T | undefined)[]` result stores T elements (primitiveElement).
if (t.kind === "undefinedT" && primitiveElement(outElem)) return true;
if (outElem.kind !== "union") return false;
return lowerer.armTag(outElem.unionId, t) >= 0;
};
@@ -1755,24 +1758,21 @@ function flatMapUnionHelper(
};
if (plan.arms[tag] === "empty") return [];
if (plan.arms[tag] === "value") {
// A unit arm's value is the unit itself, which has no narrowed
// representation.
const value: IrExpr = isUnitType(arm)
? { kind: "unitLit", unit: arm.kind === "nullT" ? "null" : "undefined", type: arm, loc }
: narrowed;
return [
arrayValueStore(
lowerer,
outRef(),
outLength(),
lowerer.coerceToExpected(
// A unit arm's value is the unit itself, which has no
// narrowed representation.
isUnitType(arm)
? {
kind: "unitLit",
unit: arm.kind === "nullT" ? "null" : "undefined",
type: arm,
loc,
}
: narrowed,
plan.outElem,
),
// An undefined value is a present undefined element of a
// primitive array (arrayValueStore).
arm.kind === "undefinedT" && primitiveElement(plan.outElem)
? value
: lowerer.coerceToExpected(value, plan.outElem),
plan.outElem,
loc,
),
@@ -2348,6 +2348,43 @@ function reduceHelper(
return name;
}
/** The comparator of a sort over a `(T | undefined)[]`, which stores T
* elements. The comparator only receives present elements (undefined
* elements sort last without a call), so the unannotated parameters of an
* inline comparator take the element type, and the runtime calls it
* without an adapter. */
function lowerOptionalElementComparator(
lowerer: Lowerer,
argNode: ts.Expression,
comparator: ts.Expression,
elem: IrType,
arrT: IrType & { kind: "array" },
): IrExpr {
if (
arrT.optionalElements !== true ||
!(ts.isArrowFunction(comparator) || ts.isFunctionExpression(comparator)) ||
isJsSourceFile(ts.getSourceFileOfNode(comparator))
)
return lowerer.lowerExpr(argNode);
const rebound: { symbol: ts.Symbol; previous: IrType | undefined }[] = [];
for (const param of comparator.parameters.nodes.slice(0, 2)) {
if (param.type || param.initializer || param.dotDotDotToken || !ts.isIdentifier(param.name))
continue;
const symbol = lowerer.checker.getSymbolAtLocation(param.name);
if (!symbol) continue;
rebound.push({ symbol, previous: lowerer.runtimeOptionalBindingTypes.get(symbol) });
lowerer.runtimeOptionalBindingTypes.set(symbol, elem);
}
try {
return lowerer.lowerExpr(argNode);
} finally {
for (const { symbol, previous } of rebound) {
if (previous === undefined) lowerer.runtimeOptionalBindingTypes.delete(symbol);
else lowerer.runtimeOptionalBindingTypes.set(symbol, previous);
}
}
}
/** `a.sort(cmp)` / `a.toSorted(cmp)`, desugared to an interned synthetic
* function like the other array HOFs. sort mutates and returns the receiver;
* toSorted takes its shallow snapshot INSIDE the helper, after the receiver
@@ -2436,7 +2473,8 @@ export function lowerArraySortCall(
loc,
};
}
let fnArg = undefinedArg ?? lowerer.lowerExpr(argNode);
let fnArg =
undefinedArg ?? lowerOptionalElementComparator(lowerer, argNode, unwrappedArg, elem, arrT);
// Reusable JS comparators often accept checked values. Adapt the array's
// element ABI just as other array callbacks do before sorting.
if (
@@ -4,6 +4,7 @@ import { generatorDrain } from "../iterator-adapters.js";
import { identityPreservingWidening } from "../coercions/identity.js";
import { liftedFreshSpread } from "../coercions/fresh-spread.js";
import { checkedIterableSpread } from "../checked-iterable-spread.js";
import { AstNode } from "../../ts7/ast-node.js";
import { InternalCompilerError } from "../../../errors.js";
import {
dynUndefinedExpr,
@@ -27,7 +28,9 @@ import {
type IrType,
STRING,
type SrcLoc,
UNDEFINED_T,
arrayOf,
isUnitType,
canConvertToDyn,
funcOf,
isRefCounted,
@@ -35,7 +38,13 @@ import {
typeEquals,
} from "../../../ir/ir.js";
import { isJsSourceFile, locOf } from "../../program.js";
import { arrayValueRead, arrayValueStore, arrayValueType } from "../array-values.js";
import {
arrayValueRead,
arrayValueStore,
arrayValueType,
optionalElementValue,
primitiveElement,
} from "../array-values.js";
import { typeKey } from "../../type-mapper.js";
import { newFnCtx } from "../lowerer.js";
import { iteratorCanStep, iteratorValue } from "../iterator-consumption.js";
@@ -51,6 +60,34 @@ import {
} from "./callback-arguments.js";
import { classGeneratorIterable } from "../class-iterables.js";
/** A value whose type admits only null and undefined, stored into an array
* that keeps undefined elements: the checker admits it there only when its
* type is `undefined` (a tuple element typed `undefined` maps to such a
* union), so it is evaluated and stored as undefined. */
function undefinedForNullish(
lowerer: Lowerer,
value: IrExpr,
arrType: IrType & { kind: "array" },
): IrExpr {
if (arrType.optionalElements !== true || value.type.kind !== "union") return value;
const arms = lowerer.unions.get(value.type.unionId)?.arms ?? [];
if (arms.length === 0 || !arms.every(isUnitType)) return value;
const loc = value.loc;
const optional: IrType = {
kind: "union",
unionId: lowerer.unions.intern([arrType.elem, UNDEFINED_T]),
};
const missing = lowerer.wrappedUndefined(optional, loc);
if (!missing) return value;
return {
kind: "seqExpr",
stmts: [{ kind: "exprStmt", expr: value, loc }],
result: missing,
type: optional,
loc,
};
}
/** Build the argument array for a mutating/copying operation while keeping
* the receiver's scalar payload ABI. A missing read is represented as the
* present UNDEFINED state in this temporary, so push/unshift and toSpliced
@@ -86,9 +123,11 @@ export function lowerArrayValueItems(
loc,
},
];
for (const value of values) {
for (const item of values) {
const value = undefinedForNullish(lowerer, item, arrType);
const stored =
lowerer.runtimeOptionalWidening(value.type, elem) !== null
lowerer.runtimeOptionalWidening(value.type, elem) !== null ||
optionalElementValue(lowerer, value, elem)
? value
: lowerer.coerceToExpected(value, elem);
body.push(arrayValueStore(lowerer, outRef, length(), stored, elem, loc));
@@ -96,16 +135,21 @@ export function lowerArrayValueItems(
return { kind: "seqExpr", stmts: body, result: outRef, type: arrType, loc };
}
/** The items of an array built from argument nodes (a spread copies its
* elements in) and values already lowered (a spread tuple's fields), in
* order. Each value is stored when its turn comes. */
export function lowerArraySpreadItems(
lowerer: Lowerer,
nodes: readonly ts.Expression[],
nodes: readonly (ts.Expression | IrExpr)[],
elem: IrType,
arrType: IrType & { kind: "array" },
loc: SrcLoc,
): IrExpr {
const out = lowerer.declareHiddenLocal("%arrayItems", arrType);
const outRef = varRef(out.id, arrType, loc);
const plainItems = nodes.filter((node) => !ts.isSpreadElement(node)).length;
const plainItems = nodes.filter(
(node) => !(node instanceof AstNode) || !ts.isSpreadElement(node),
).length;
const length = (): IrExpr => ({
kind: "arrIntrinsic",
method: "length",
@@ -129,7 +173,23 @@ export function lowerArraySpreadItems(
},
];
for (const node of nodes) {
if (ts.isSpreadElement(node)) {
if (!(node instanceof AstNode)) {
// A spread tuple's first field declares the tuple's storage, which
// the later fields read: its statements join this sequence, whose
// frame lasts until every item is stored.
let item = node;
if (item.kind === "seqExpr") {
body.push(...item.stmts);
item = item.result;
}
const value = undefinedForNullish(lowerer, item, arrType);
const stored =
lowerer.runtimeOptionalWidening(value.type, elem) !== null ||
optionalElementValue(lowerer, value, elem)
? value
: lowerer.coerceToExpected(value, elem);
body.push(arrayValueStore(lowerer, outRef, length(), stored, elem, loc));
} else if (ts.isSpreadElement(node)) {
let source = lowerer.lowerExpr(node.expression);
if (
source.type.kind === "set" &&
@@ -212,7 +272,8 @@ export function lowerArraySpreadItems(
} else {
const value = lowerer.lowerExpr(node);
const stored =
lowerer.runtimeOptionalWidening(value.type, elem) !== null
lowerer.runtimeOptionalWidening(value.type, elem) !== null ||
optionalElementValue(lowerer, value, elem)
? value
: lowerer.coerceInto(node, value, elem);
body.push(arrayValueStore(lowerer, outRef, length(), stored, elem, loc));
@@ -518,6 +579,20 @@ export function lowerArrayOfCall(
if (call.arguments.nodes.some(ts.isSpreadElement)) {
return lowerArraySpreadItems(lowerer, call.arguments.nodes, result.elem, result, locOf(call));
}
// A `(T | undefined)[]` (primitiveElement) stores undefined arguments as
// present undefined elements.
if (result.optionalElements === true) {
const nodes = call.arguments.nodes;
const values = nodes.map((arg) => lowerer.lowerExpr(arg));
if (values.some((value) => optionalElementValue(lowerer, value, result.elem)))
return lowerArrayValueItems(lowerer, values, result.elem, result, locOf(call));
return {
kind: "arrayLit",
elems: values.map((value, i) => lowerer.coerceInto(nodes[i]!, value, result.elem)),
type: result,
loc: locOf(call),
};
}
return {
kind: "arrayLit",
elems: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, result.elem)),
@@ -959,6 +1034,31 @@ export function lowerArrayFromCall(
const arrT = lowerer.mapTypeOf(lowerer.typeOf(call));
if (arrT?.kind !== "array") lowerer.badType(call, lowerer.typeOf(call));
const elem = arrT.elem;
// A `(T | undefined)[]` (primitiveElement): n present undefined
// elements.
const elemTs = lowerer.checker.isArrayType(lowerer.typeOf(call))
? lowerer.checker.getTypeArguments(lowerer.typeOf(call) as ts.TypeReference)[0]
: undefined;
if (
primitiveElement(elem) &&
elemTs !== undefined &&
(elemTs.isUnionType() ? ts.constituentTypes(elemTs) : [elemTs]).some(
(part) => (part.flags & ts.TypeFlags.Undefined) !== 0,
)
) {
return {
kind: "arrIntrinsic",
method: "fillUndefined",
receiver: { kind: "arrayNewLen", length: n, type: arrT, loc },
// To the end: fill clamps an infinite end to the length.
args: [
numLit(0, loc),
{ kind: "bin", op: "/", left: numLit(1, loc), right: numLit(0, loc), type: F64, loc },
],
type: arrT,
loc,
};
}
const absent =
elem.kind === "union"
? lowerer.wrappedUndefined(elem, loc) !== null ||
@@ -1085,7 +1185,10 @@ export function lowerArrayFromCall(
// A generator (or an iterator adapted into one) drains in order.
if (src.type.kind === "generator" && args.length === 1) {
const arrT = lowerer.mapTypeOf(lowerer.typeOf(call));
const drained = arrT?.kind === "array" ? generatorDrain(lowerer, src, arrT.elem, loc) : null;
const drained =
arrT?.kind === "array"
? generatorDrain(lowerer, src, arrT.elem, loc, arrT.optionalElements === true)
: null;
if (drained) return drained;
}
lowerer.noLowering(
@@ -1159,6 +1262,11 @@ function buildArrayFromArrayFn(
type: fnRet,
loc,
};
// A result of another type than the element (`T | undefined` into a
// `(T | undefined)[]`, which stores T elements) is held in a local, so
// the store tests and converts one value.
const held = !typeEquals(fnRet, outElem);
const value: IrExpr = held ? varRef("v.0", fnRet, loc) : mapped;
return {
name,
params: [
@@ -1171,6 +1279,7 @@ function buildArrayFromArrayFn(
{ id: "f.0", name: "f", type: fnT, mutable: true },
{ id: "out.0", name: "out", type: outT, mutable: false },
{ id: "i.0", name: "i", type: F64, mutable: true },
...(held ? [{ id: "v.0", name: "v", type: fnRet, mutable: false }] : []),
],
body: [
{
@@ -1191,7 +1300,8 @@ function buildArrayFromArrayFn(
loc,
},
body: [
arrayValueStore(lowerer, out, i, mapped, outElem, loc),
...(held ? [{ kind: "varDecl", localId: "v.0", init: mapped, loc } as const] : []),
arrayValueStore(lowerer, out, i, value, outElem, loc),
{
kind: "assign",
localId: "i.0",
@@ -32,6 +32,10 @@ import {
arrayValueRead,
arrayValueStore,
arrayValueType,
elementPlaceholder,
insertedValues,
optionalElementValue,
primitiveElement,
} from "../array-values.js";
import { typeKey } from "../../type-mapper.js";
import {
@@ -125,6 +129,14 @@ function lowerOptionalArrayPush(
return { kind: "seqExpr", stmts, result: length(), type: F64, loc };
}
/** The checker type of `node` admits undefined. */
function checkerTypeAdmitsUndefined(lowerer: Lowerer, node: ts.Expression): boolean {
const type = lowerer.typeOf(node);
return (type.isUnionType() ? ts.constituentTypes(type) : [type]).some(
(part) => (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0,
);
}
/** `a.splice(start, count, v)` where v may hold undefined at run time.
* Receiver, start, count, and v evaluate once, in order; a present v is
* inserted as a plain element literal (which a statement-position splice
@@ -658,7 +670,10 @@ export function lowerArrayMethodCall(
const hasSpread = itemNodes.some(ts.isSpreadElement);
const itemProbes = hasSpread ? [] : itemNodes.map((arg) => tryLowerExpression(lowerer, arg));
const statefulItems = itemProbes.some(
(probe) => probe !== null && lowerer.runtimeOptionalWidening(probe.type, elem) !== null,
(probe) =>
probe !== null &&
(lowerer.runtimeOptionalWidening(probe.type, elem) !== null ||
optionalElementValue(lowerer, probe, elem)),
);
const items: IrExpr = hasSpread
? lowerArraySpreadItems(lowerer, itemNodes, elem, receiverIr, loc)
@@ -875,6 +890,65 @@ export function lowerArrayMethodCall(
loc,
};
}
// A `(T | undefined)[]` (primitiveElement) takes undefined values as
// present undefined elements, stored after the insertion.
if (
(name === "push" || name === "unshift") &&
primitiveElement(elem) &&
call.arguments.nodes.some((a) => checkerTypeAdmitsUndefined(lowerer, a))
) {
const nodes = call.arguments.nodes;
const values = nodes.map((a) => lowerer.lowerExpr(a));
const inserted = insertedValues(
lowerer,
values,
elem,
(value, i) => lowerer.coerceInto(nodes[i]!, value, elem),
loc,
);
if (inserted.optional) {
const array = lowerer.declareHiddenLocal("%insertInto", receiver.type);
const arr = varRef(array.id, array.type, loc);
const length = lowerer.declareHiddenLocal("%insertLength", F64);
const count: IrExpr = numLit(values.length, loc);
const first: IrExpr =
name === "push"
? {
kind: "bin",
op: "-",
left: varRef(length.id, F64, loc),
right: count,
type: F64,
loc,
}
: numLit(0, loc);
return {
kind: "seqExpr",
stmts: [
{ kind: "varDecl", localId: array.id, init: receiver, loc },
...inserted.stmts,
{
kind: "varDecl",
localId: length.id,
init: {
kind: "arrIntrinsic",
method: name,
receiver: arr,
args: inserted.args,
type: F64,
loc,
},
loc,
},
...inserted.fill(arr, first),
],
result: varRef(length.id, F64, loc),
type: F64,
loc,
};
}
return { kind: "arrIntrinsic", method: name, receiver, args: inserted.args, type: F64, loc };
}
// Inserted values flow into the element slot like an assignment would:
// union-element arrays wrap plain arm values (coerceInto is inert
// when the types already agree).
@@ -996,9 +1070,12 @@ export function lowerArrayMethodCall(
const hasSpread = itemNodes.some(ts.isSpreadElement);
const itemProbes = hasSpread ? [] : itemNodes.map((arg) => tryLowerExpression(lowerer, arg));
const statefulItems = itemProbes.some(
(probe) => probe !== null && lowerer.runtimeOptionalWidening(probe.type, elem) !== null,
(probe) =>
probe !== null &&
(lowerer.runtimeOptionalWidening(probe.type, elem) !== null ||
optionalElementValue(lowerer, probe, elem)),
);
if (statefulItems && itemNodes.length === 1) {
if (statefulItems && itemNodes.length === 1 && itemProbes[0]?.type.kind === "union") {
return lowerOptionalSpliceItem(
lowerer,
receiver,
@@ -1734,7 +1811,28 @@ function arraySearchHelper(
type: valueT,
loc,
};
const value = directNeedle ? rawRead : optionalValue;
// A direct needle matches only a present value (`match` tests the state
// first); a present undefined of a `(T | undefined)[]` (primitiveElement)
// must not be read as T.
const value = !directNeedle
? optionalValue
: primitiveElement(elem)
? {
kind: "ternary" as const,
cond: {
kind: "bin" as const,
op: "===" as const,
left: stateRef,
right: numLit(1, loc),
type: BOOL,
loc,
},
then: rawRead,
else_: elementPlaceholder(elem, loc),
type: elem,
loc,
}
: rawRead;
const readT = directNeedle ? elem : valueT;
const equal = arraySearchEquality(
lowerer,
@@ -2,6 +2,7 @@ import { keyedHelperName } from "../stable-names.js";
import * as ts from "../../ts7/adapter.js";
import type { Lowerer } from "../lowerer.js";
import { isJsSourceFile, locOf } from "../../program.js";
import { primitiveElement } from "../array-values.js";
import { varRef } from "../../../ir/build.js";
import { typeKey } from "../../type-mapper.js";
import {
@@ -144,12 +145,22 @@ export function lowerArrayCallback(
const untyped = (checkerType.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0;
const raw = untyped ? null : lowerer.mapTypeOf(checkerType);
const mapped = raw ? lowerer.runtimeOptionalBindingType(param.name, raw) : null;
// A helper that passes present number, string or boolean elements
// (a `(T | undefined)[]` stores T elements) binds a `T | undefined`
// parameter to T, so the callback takes them without an adapter.
const presentElement =
mapped !== null &&
primitiveElement(expected) &&
mapped.kind === "union" &&
lowerer.armTag(mapped.unionId, UNDEFINED_T) >= 0 &&
typeEquals(lowerer.stripUndefinedArm(mapped), expected);
if (
untyped
? !bindUntyped
: mapped === null ||
(lowerer.coercibleValue(expected, mapped) &&
!runtimeOptionalPairVariant(lowerer, expected, mapped))
!runtimeOptionalPairVariant(lowerer, expected, mapped) &&
!presentElement)
)
return;
const symbol = lowerer.checker.getSymbolAtLocation(param.name);
@@ -1,4 +1,5 @@
import { keyedHelperName } from "../stable-names.js";
import { primitiveElement } from "../array-values.js";
import { nodeThrowExpr, boolLit, countedFor, numLit, strLit, varRef } from "../../../ir/build.js";
import { InternalCompilerError } from "../../../errors.js";
import * as ts from "../../ts7/adapter.js";
@@ -1137,6 +1138,17 @@ function setFromSeedValue(
seed = strCharsCall(lowerer, source, loc);
} else if (source.type.kind === "array" && typeEquals(source.type.elem, setT.elem)) {
seed = source;
} else if (
source.type.kind === "array" &&
primitiveElement(source.type.elem) &&
setT.elem.kind === "union" &&
lowerer.armTag(setT.elem.unionId, UNDEFINED_T) >= 0 &&
typeEquals(lowerer.stripUndefinedArm(setT.elem), source.type.elem)
) {
// A `(T | undefined)[]` stores T elements: its present undefined
// elements copy as the Set's undefined.
seed = lowerer.coerceToExpected(source, arrayOf(setT.elem));
if (!typeEquals(seed.type, arrayOf(setT.elem))) return null;
} else if (source.type.kind === "record") {
const shape = lowerer.shapes.get(source.type.shapeId);
if (!shape?.tuple || !shape.fields.every((field) => typeEquals(field.type, setT.elem)))
@@ -1,6 +1,7 @@
import { keyedHelperName } from "../stable-names.js";
import { nullProtoRecordShape, recordNullProtoTest } from "../null-prototype.js";
import { countedFor, numLit, varRef } from "../../../ir/build.js";
import { arrayValueStore, optionalElementValue, primitiveElement } from "../array-values.js";
import { objectEnumerationReceiver } from "../object-enumeration-receiver.js";
import { InternalCompilerError } from "../../../errors.js";
import * as ts from "../../ts7/adapter.js";
@@ -156,7 +157,14 @@ export function objectIterOverIndexShape(
// also holds numbers): each overflow value checks into the element type,
// so a value of a dropped arm throws instead of mistyping.
let ivNarrows = false;
if (valueT) {
// `T | undefined` values into a `(T | undefined)[]`, which stores T
// elements (primitiveElement): each value is stored with its present
// undefined.
const optionalValues =
member === "values" &&
valueT !== null &&
optionalElementValue(lowerer, { kind: "unitLit", unit: "undefined", type: iv, loc }, valueT);
if (valueT && !optionalValues) {
const ivSurfaces =
typeEquals(iv, valueT) ||
(valueT.kind === "union" && lowerer.armTag(valueT.unionId, iv) >= 0) ||
@@ -350,7 +358,25 @@ export function objectIterOverIndexShape(
loc,
},
then: [fieldStmt],
else_: undefinedElement ? [push(pushedOf(undefinedElement))] : null,
else_: undefinedElement
? [push(pushedOf(undefinedElement))]
: member === "values" && valueT && primitiveElement(valueT)
? [
{
kind: "arraySetUndefined",
arr: outRef,
index: {
kind: "arrIntrinsic",
method: "length",
receiver: outRef,
args: [],
type: F64,
loc,
},
loc,
},
]
: null,
loc,
};
}
@@ -374,35 +400,45 @@ export function objectIterOverIndexShape(
const kRef = varRef("k.0", STRING, loc);
const readValue: IrExpr | null = !valueT
? null
: ivNarrows
: optionalValues
? {
kind: "dynCheck",
value: {
kind: "dynFrom",
value: {
kind: "recordKeyGet",
obj: rRef,
shapeId: argIr.shapeId,
key: kRef,
overflowOnly: true,
type: iv,
loc,
},
type: DYN,
loc,
},
type: valueT,
loc,
}
: {
kind: "recordKeyGet",
obj: rRef,
shapeId: argIr.shapeId,
key: kRef,
overflowOnly: true,
type: valueT,
type: iv,
loc,
};
}
: ivNarrows
? {
kind: "dynCheck",
value: {
kind: "dynFrom",
value: {
kind: "recordKeyGet",
obj: rRef,
shapeId: argIr.shapeId,
key: kRef,
overflowOnly: true,
type: iv,
loc,
},
type: DYN,
loc,
},
type: valueT,
loc,
}
: {
kind: "recordKeyGet",
obj: rRef,
shapeId: argIr.shapeId,
key: kRef,
overflowOnly: true,
type: valueT,
loc,
};
const loopPushed: IrExpr =
member === "keys"
? kRef
@@ -440,7 +476,26 @@ export function objectIterOverIndexShape(
},
loc,
},
push(loopPushed),
...(optionalValues
? [
{ kind: "varDecl" as const, localId: "v.0", init: readValue!, loc },
arrayValueStore(
lowerer,
outRef,
{
kind: "arrIntrinsic",
method: "length",
receiver: outRef,
args: [],
type: F64,
loc,
},
varRef("v.0", iv, loc),
valueT!,
loc,
),
]
: [push(loopPushed)]),
],
),
{ kind: "return", value: outRef, loc },
@@ -457,6 +512,7 @@ export function objectIterOverIndexShape(
{ id: "ks.0", name: "ks", type: ksT, mutable: false },
{ id: "i.0", name: "i", type: F64, mutable: true },
{ id: "k.0", name: "k", type: STRING, mutable: false },
...(optionalValues ? [{ id: "v.0", name: "v", type: iv, mutable: false }] : []),
],
body,
loc,
@@ -31,7 +31,7 @@ import { strCharsCall } from "../containers/array-construction.js";
import { generatorDrain } from "../iterator-adapters.js";
import { isSearchParamsIterable } from "../builtins/url.js";
import { dynUndefinedExpr, varRef } from "../../../ir/build.js";
import { arrayValueStore } from "../array-values.js";
import { arrayValueStore, primitiveElement } from "../array-values.js";
import { classGeneratorIterable } from "../class-iterables.js";
import { iteratorCanStep } from "../iterator-consumption.js";
@@ -103,6 +103,13 @@ function emptyNeverLiteral(lowerer: Lowerer, expr: ts.ArrayLiteralExpression): b
* tsc's API doesn't surface it (ternary arms under an array context —
* tsc accepts the arm covariantly, but a tagged element representation
* must be BUILT as the slot's element type). */
/** The checker type of `node` admits undefined. */
function mayHoldUndefined(lowerer: Lowerer, node: ts.Expression): boolean {
const type = lowerer.typeOf(node);
const parts = type.isUnionType() ? ts.constituentTypes(type) : [type];
return parts.some((part) => (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0);
}
export function lowerArrayLiteral(
lowerer: Lowerer,
expr: ts.ArrayLiteralExpression,
@@ -524,7 +531,13 @@ export function lowerArrayLiteral(
// `[...it]` over a generator (or an iterator adapted into one):
// drain it once, in order, into a fresh element array.
if (src.type.kind === "generator") {
const drained = generatorDrain(lowerer, src, type.elem, locOf(el));
const drained = generatorDrain(
lowerer,
src,
type.elem,
locOf(el),
type.optionalElements === true,
);
if (drained) src = drained;
}
// `[...sp]` and `[...sp.entries()]` over a URLSearchParams: its
@@ -615,6 +628,9 @@ export function lowerArrayLiteral(
let lowered: IrExpr;
const directOptionalCandidate =
runtimeOptionalElements.has(i) ||
// An element that can be undefined in a `(T | undefined)[]`
// (primitiveElement) is stored as a present undefined.
(primitiveElement(type.elem) && mayHoldUndefined(lowerer, el)) ||
ts.isElementAccessExpression(el) ||
(ts.isCallExpression(el) &&
ts.isPropertyAccessExpression(el.expression) &&
@@ -692,7 +708,8 @@ export function lowerArrayLiteral(
continue;
}
let value = el;
if (!typeEquals(value.type, type.elem)) {
// An undefined element is stored directly (arrayValueStore).
if (!typeEquals(value.type, type.elem) && value.type.kind !== "undefinedT") {
const temp = lowerer.declareHiddenLocal("%arrayValue", value.type);
body.push({ kind: "varDecl", localId: temp.id, init: value, loc });
value = varRef(temp.id, temp.type, loc);
@@ -61,6 +61,8 @@ import {
arrayValueType,
lowerSafeBytesRead,
lowerSafeIndexRead,
primitiveElement,
unionArrayValueRead,
} from "../array-values.js";
import { lowerOptionalStringIndex, lowerUnionStringIndex } from "../string-index.js";
import { InternalCompilerError } from "../../../errors.js";
@@ -594,6 +596,8 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre
}
}
if (key.type.kind === "f64") {
const optional = optionalArmIndexRead(lowerer, value, key, locOf(expr));
if (optional) return lowerer.maybeNarrow(optional, expr);
const safe = lowerOptionalArrayIndexRead(lowerer, expr, value, key);
if (safe) return safe;
}
@@ -884,9 +888,14 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre
// must be the element type (library ABI checks and the catchable
// RangeError contract for an out-of-bounds assert). Where undefined is
// representable, the assertion keeps JavaScript's unchecked read.
// A declared `(T | undefined)[]` element (stored as T) keeps its
// optional read: the assertion then narrows it with a catchable
// TypeError, as for a union element.
const declaredOptional = arr.type.optionalElements === true;
const provenPresent =
ts.isNonNullExpression(expr.parent) &&
expr.parent.expression === expr &&
!declaredOptional &&
!assertedElementKeepsUndefined(lowerer, expr.parent);
if (provenPresent) {
return lowerer.maybeNarrow(
@@ -2437,3 +2446,46 @@ export function lowerEnvironmentKey(lowerer: Lowerer, node: ts.Expression): IrEx
return { kind: "libCall", fn: "dyn.toStringCoerce", args: [key], type: STRING, loc: key.loc };
return lowerRecordPropertyKey(lowerer, key, node);
}
/** A number-keyed read of a union of arrays with a `(T | undefined)[]` arm
* (stored as T elements, primitiveElement) whose element facts admit
* missing values: each arm reads its element by its state
* (unionArrayValueRead), where the keyed read would trap on a present
* undefined. The receiver and key evaluate once. Null for other unions. */
function optionalArmIndexRead(
lowerer: Lowerer,
value: IrExpr,
key: IrExpr,
loc: SrcLoc,
): IrExpr | null {
if (value.type.kind !== "union") return null;
const arms = lowerer.unions.get(value.type.unionId)?.arms ?? [];
if (
!arms.some(
(arm) =>
arm.kind === "array" &&
primitiveElement(arm.elem) &&
arrayValueType(lowerer, arm.elem).kind === "union",
)
)
return null;
if (unionArrayValueRead(lowerer, value, key, loc) === null) return null;
const stmts: IrStmt[] = [];
const stable = (e: IrExpr, name: string, keep: boolean): IrExpr => {
if (keep) return e;
const local = lowerer.declareHiddenLocal(name, e.type);
stmts.push({ kind: "varDecl", localId: local.id, init: e, loc });
return varRef(local.id, e.type, loc);
};
// A receiver local stays a direct read only when the key cannot assign
// it (`a[(a = other, 0)]` reads the array `a` held before the key).
const plainKey = key.kind === "varRef" || key.kind === "numLit";
const receiver = stable(
value,
"%indexReceiver",
value.kind === "numLit" || (value.kind === "varRef" && plainKey),
);
const index = stable(key, "%indexKey", plainKey);
const read = unionArrayValueRead(lowerer, receiver, index, loc)!;
return stmts.length === 0 ? read : { kind: "seqExpr", stmts, result: read, type: read.type, loc };
}
@@ -1,3 +1,4 @@
import { appendElement, optionalElementValue, primitiveElement } from "./array-values.js";
import { keyedHelperName } from "./stable-names.js";
import { dynUndefinedExpr, varRef } from "../../ir/build.js";
import {
@@ -964,16 +965,27 @@ function generatorRetypeAdapter(
return { kind: "call", callee: name, args: [expr], type: expected, loc };
}
/** A checked-dynamic yielded value for a `(T | undefined)[]` of a
* number, string or boolean T: converted to `T | undefined`, which the
* drain stores as a T element or a present undefined. */
function optionalYield(lowerer: Lowerer, value: IrExpr, elem: IrType, optional: boolean): IrExpr {
if (!optional || value.type.kind !== "dyn" || !primitiveElement(elem)) return value;
return lowerer.coerceToExpected(value, lowerer.withUndefinedArm(elem));
}
/** Drain a synchronous generator into a fresh `elem[]` — the eager
* consumers of an iterator (`[...it]`, `Array.from(it)`). Each step resumes
* the generator once and appends its value; completion stops the drain
* without appending the return value, as in JS. Null when the yielded
* values cannot be extracted into the element type. */
* values cannot be extracted into the element type. `optional`: the
* result is a `(T | undefined)[]`, which stores T elements, so a
* checked-dynamic value may also be undefined. */
export function generatorDrain(
lowerer: Lowerer,
source: IrExpr,
elem: IrType,
loc: SrcLoc,
optional = false,
): IrExpr | null {
const gen = source.type;
if (gen.kind !== "generator" || gen.async || gen.yieldT.kind === "void") return null;
@@ -982,14 +994,16 @@ export function generatorDrain(
const valueT = shape?.fields.find((field) => field.name === "value")?.type;
if (!resultType || !valueT) return null;
const arrT: IrType = { kind: "array", elem };
const key = `genDrain:${typeKey(gen)}:${typeKey(elem)}`;
const key = `genDrain:${typeKey(gen)}:${typeKey(elem)}${optional ? "?" : ""}`;
let name = lowerer.valueHelpers.get(key);
if (!name && stepDrivable(gen)) {
// The record-free drive: test done, then take the yielded value.
const genRef = varRef("gen.0", gen, loc) as IrExpr & { type: typeof gen };
const outRef = varRef("out.0", arrT, loc);
const value = lowerer.coerceToExpected(genValue(genRef, loc), elem);
if (!typeEquals(value.type, elem)) return null;
const yielded = optionalYield(lowerer, genValue(genRef, loc), elem, optional);
const present = optionalElementValue(lowerer, yielded, elem);
const value = present ? yielded : lowerer.coerceToExpected(yielded, elem);
if (!present && !typeEquals(value.type, elem)) return null;
name = keyedHelperName(lowerer, "%gen.drain", key);
lowerer.valueHelpers.set(key, name);
lowerer.liftedFns.push({
@@ -999,6 +1013,7 @@ export function generatorDrain(
locals: [
{ id: "gen.0", name: "gen", type: gen, mutable: false },
{ id: "out.0", name: "out", type: arrT, mutable: false },
...(present ? [{ id: "v.0", name: "v", type: value.type, mutable: false }] : []),
],
body: [
{
@@ -1010,20 +1025,7 @@ export function generatorDrain(
{
kind: "while",
cond: { kind: "unary", op: "!", operand: genStep(genRef, loc), type: BOOL, loc },
body: [
{
kind: "exprStmt",
expr: {
kind: "arrIntrinsic",
method: "push",
receiver: outRef,
args: [value],
type: F64,
loc,
},
loc,
},
],
body: appendElement(lowerer, outRef, value, elem, "v.0", loc),
loc,
},
{ kind: "return", value: outRef, loc },
@@ -1050,8 +1052,10 @@ export function generatorDrain(
loc,
);
if (!raw) return null;
const value = lowerer.coerceToExpected(raw, elem);
if (!typeEquals(value.type, elem)) return null;
const yielded = optionalYield(lowerer, raw, elem, optional);
const present = optionalElementValue(lowerer, yielded, elem);
const value = present ? yielded : lowerer.coerceToExpected(yielded, elem);
if (!present && !typeEquals(value.type, elem)) return null;
name = keyedHelperName(lowerer, "%gen.drain", key);
lowerer.valueHelpers.set(key, name);
lowerer.liftedFns.push({
@@ -1062,6 +1066,7 @@ export function generatorDrain(
{ id: "gen.0", name: "gen", type: gen, mutable: false },
{ id: "out.0", name: "out", type: arrT, mutable: false },
{ id: "step.0", name: "step", type: resultType, mutable: false },
...(present ? [{ id: "v.0", name: "v", type: value.type, mutable: false }] : []),
],
body: [
{
@@ -1101,18 +1106,7 @@ export function generatorDrain(
else_: null,
loc,
},
{
kind: "exprStmt",
expr: {
kind: "arrIntrinsic",
method: "push",
receiver: outRef,
args: [value],
type: F64,
loc,
},
loc,
},
...appendElement(lowerer, outRef, value, elem, "v.0", loc),
],
loc,
},
@@ -6585,6 +6585,17 @@ export class Lowerer {
mayBeUndefined: (node) => {
if (explicitlyNonNull(node)) return assertedReadMayBeUndefined(node);
if (mayBeOptional(node)) return true;
// A value the checker types as possibly undefined: `(T | undefined)[]`
// stores T elements (the type mapper), so its undefined values are
// facts of T's arrays.
const checked = typeOrNull(node);
if (
checked !== null &&
(checked.isUnionType() ? ts.constituentTypes(checked) : [checked]).some(
(part) => (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0,
)
)
return true;
// `undefined as unknown as T` keeps the undefined value.
for (
let inner: ts.Expression = node;
@@ -6603,7 +6614,49 @@ export class Lowerer {
const symbol = symbolOf(e);
return symbol !== null && iterationSymbols.has(symbol) && !provenPresent(e, symbol);
},
callbackReturnsUndefined: (node) => callbackReturnsOptional(node),
iterationMayYieldUndefined: (node) => {
const type = typeOrNull(node);
if (type === null || this.checker.isArrayType(type)) return false;
// A tuple yields its elements, in order.
if (this.checker.isTupleType(type))
return panicSafe(
() =>
this.checker
.getTypeArguments(type as ts.TypeReference)
.some((element) =>
(element.isUnionType() ? ts.constituentTypes(element) : [element]).some(
(part) => (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0,
),
),
true,
);
// The first type argument of Set<T>, Map and Set iterators,
// generators and iterables is the value they yield.
const element =
(type.flags & ts.TypeFlags.Object) !== 0
? panicSafe(() => this.checker.getTypeArguments(type as ts.TypeReference)[0], undefined)
: undefined;
if (element === undefined) return false;
return (element.isUnionType() ? ts.constituentTypes(element) : [element]).some(
(part) => (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0,
);
},
callbackReturnsUndefined: (node) => {
if (callbackReturnsOptional(node)) return true;
// A callback the checker types as possibly returning undefined
// (`(m) => map.get(m)`): its results are present undefined
// elements of a `(T | undefined)[]`, which stores T elements.
const type = typeOrNull(node);
return (
type !== null &&
this.checker.getCallSignatures(type).some((signature) => {
const result = this.checker.getReturnTypeOfSignature(signature);
return (result.isUnionType() ? ts.constituentTypes(result) : [result]).some(
(part) => (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0,
);
})
);
},
isGlobal: (node, name) => this.isStdlibGlobal(node, name),
isString: (node) => {
const type = typeOrNull(node);
@@ -6630,6 +6683,14 @@ export class Lowerer {
const rest = sig.params.find((shape) => shape.mode === "rest");
return rest?.type.kind === "array" ? [typeKey(rest.type.elem)] : [];
},
restStart: (call) => {
const callee = peel(call.expression);
if (!ts.isIdentifier(callee) && !ts.isPropertyAccessExpression(callee)) return null;
const symbol = callableSymbolOf(callee);
const sig = symbol ? signatureBySymbol.get(symbol) : undefined;
const at = sig ? sig.params.findIndex((shape) => shape.mode === "rest") : -1;
return at >= 0 ? at : null;
},
markIterationBinding: (node, known) => {
const symbol = symbolOf(node);
if (!symbol) return false;
@@ -6664,6 +6725,7 @@ export class Lowerer {
const cached = scanSites.get(sf);
if (cached !== undefined) return cached;
const nodes: ts.Node[] = [];
const values: ts.Node[] = [];
ts.walkPreorder(sf, (node) => {
if (
ts.isVariableDeclaration(node) ||
@@ -6679,7 +6741,21 @@ export class Lowerer {
ts.isPostfixUnaryExpression(node)
)
nodes.push(node);
// The values these sites store, whose checker types mayBeUndefined
// reads: batched here, since dead bodies are scanned too.
if (ts.isArrayLiteralExpression(node))
for (const element of node.elements.nodes)
values.push(ts.isSpreadElement(element) ? element.expression : element);
else if (ts.isCallExpression(node) || ts.isNewExpression(node))
for (const arg of node.arguments?.nodes ?? [])
values.push(ts.isSpreadElement(arg) ? arg.expression : arg);
else if (
ts.isBinaryExpression(node) &&
node.operatorToken.kind === ts.SyntaxKind.EqualsToken
)
values.push(node.right);
});
this.checker.prefetchCollectionTypes(values);
scanSites.set(sf, nodes);
return nodes;
};
@@ -16,6 +16,9 @@ export interface ElementStateHost {
elementAdmitsUndefined(key: string): boolean;
/** The value stored by `node` can be `undefined` at runtime. */
mayBeUndefined(node: ts.Expression): boolean;
/** Iterating `node`, an iterable other than an array (a Set, a Map's
* values, a generator), can yield `undefined`. */
iterationMayYieldUndefined?(node: ts.Expression): boolean;
/** A callback argument can return `undefined` at runtime. */
callbackReturnsUndefined(node: ts.Expression): boolean;
/** The value may be a plain array-like object, whose missing indices
@@ -28,6 +31,9 @@ export interface ElementStateHost {
/** Element keys of the rest parameter that the spread arguments of
* `call` fill: `[]` when the callee has none, null when it is unknown. */
restKeys(call: ts.CallExpression | ts.NewExpression): readonly string[] | null;
/** The argument index where the callee's rest parameter starts, or null
* when the callee has none or is unknown. */
restStart?(call: ts.CallExpression | ts.NewExpression): number | null;
/** Record that iterating `node`'s binding can observe `undefined`;
* `known` when the iterated array's own facts (not only those shared by
* its element ABI) say so. Returns true when the binding's record grew. */
@@ -353,7 +359,11 @@ export class ArrayElementStates {
/** One stored value, or the elements a spread copies. */
private valueMayBeUndefined(value: ts.Expression): boolean {
if (ts.isSpreadElement(value)) return this.mayYieldUndefined(value.expression);
if (ts.isSpreadElement(value))
return (
this.mayYieldUndefined(value.expression) ||
(this.host.iterationMayYieldUndefined?.(value.expression) ?? false)
);
return this.host.mayBeUndefined(value);
}
@@ -511,14 +521,26 @@ export class ArrayElementStates {
private scanCall(node: ts.CallExpression | ts.NewExpression): boolean {
let changed = false;
const args = node.arguments?.nodes ?? [];
// Spreading a sparse array into a rest parameter fills the holes.
// Spreading a sparse array, or a tuple with undefined elements, into a
// rest parameter fills the holes.
if (args.some((arg) => ts.isSpreadElement(arg))) {
for (const arg of args) {
if (!ts.isSpreadElement(arg) || !this.mayYieldUndefined(arg.expression)) continue;
if (!ts.isSpreadElement(arg) || !this.valueMayBeUndefined(arg)) continue;
const rest = this.host.restKeys(node) ?? this.host.arrayKeys(arg.expression);
if (this.addUndefined(rest)) changed = true;
}
}
// An argument in a rest position that may be undefined is a present
// undefined element of the rest array.
const restStart = this.host.restStart?.(node) ?? null;
if (restStart !== null)
for (const arg of args.slice(restStart))
if (
!ts.isSpreadElement(arg) &&
this.host.mayBeUndefined(arg) &&
this.addUndefined(this.host.restKeys(node))
)
changed = true;
if (this.record(node)) changed = true;
const callee = peel(node.expression);
if (!ts.isPropertyAccessExpression(callee)) return changed;
@@ -16,6 +16,8 @@ export function sanitizeUnregisteredClassTypes(
case "object":
return hasClass(type.className) ? type : F64;
case "array":
type.elem = rewrite(type.elem);
break;
case "set":
type.elem = rewrite(type.elem);
break;
+18 -1
View File
@@ -2349,6 +2349,23 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
elem = unitOnlyUnion(unions);
if (!elem) return null;
// `(T | undefined)[]` of a number, string or boolean T stores T
// elements: an array slot holds a present undefined by its own state,
// so a T[] seen as `(T | undefined)[]` stays the same array.
let optionalElements = false;
if (elem.kind === "union") {
const arms = unions.get(elem.unionId)?.arms;
const present =
arms?.length === 2 ? arms.find((arm) => arm.kind !== "undefinedT") : undefined;
if (
present !== undefined &&
arms!.some((arm) => arm.kind === "undefinedT") &&
(present.kind === "f64" || present.kind === "string" || present.kind === "bool")
) {
elem = present;
optionalElements = true;
}
}
// A dyn ELEMENT makes the WHOLE array the checked-dynamic value:
// `unknown[]`, `object[]`, and the collapsed `(string | object)[]`
// (the plugins-slot shape) — the checked-dynamic tree has real arrays, so length/
@@ -2370,7 +2387,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
// drain of the auxiliary-server registries): handles are ordinary
// refcounted REF elements (their `_v` adapters, no trace — both drop
// their closures at their terminal event).
return arrayOf(elem);
return optionalElements ? { kind: "array", elem, optionalElements: true } : arrayOf(elem);
}
// Tuples: `[string, string]` maps to an interned RECORD shape flagged
// `tuple`, with one field per position named by its index ("0", "1", ...)
+10 -1
View File
@@ -87,7 +87,16 @@ export type IrType =
| { kind: "date" }
| { kind: "string" } // heap, refcounted, UTF-8
| { kind: "bool" }
| { kind: "array"; elem: IrType } // heap, refcounted, monomorphic elements
| {
kind: "array";
elem: IrType;
/** A `(T | undefined)[]` of a number, string or boolean T, stored as
* T elements (which hold a present undefined by their state). Not
* part of the type's identity (typeEquals, typeKey): it only lets a
* checked conversion from a dynamic value accept undefined
* elements. */
optionalElements?: true;
} // heap, refcounted, monomorphic elements
/** ES `Map<K, V>` — heap, refcounted, insertion-ordered hash map with ONE
* runtime representation (ScrMap) and type-directed key/value handling,
* exactly the array pattern (never per-instantiation structs). Keys use
+8 -2
View File
@@ -2072,12 +2072,18 @@ function validateFunction(
}
case "arrayNewLen": {
// Mapper-less Array.from({length: n}): absent-slot fill exists
// only for refcounted element kinds (frontend fences scalars).
// only for refcounted element kinds. Number and boolean slots are
// filled with present undefined values right after
// (`(T | undefined)[]`); the frontend fences other scalar uses.
if (e.type.kind !== "array") {
err(`arrayNewLen must be array-typed, got ${e.type.kind}`, e.loc);
break;
}
if (!isRefCounted(e.type.elem)) {
if (
!isRefCounted(e.type.elem) &&
e.type.elem.kind !== "f64" &&
e.type.elem.kind !== "bool"
) {
err(
`arrayNewLen with non-refcounted ${e.type.elem.kind} elements (no absent value)`,
e.loc,
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8000-optional-element-arrays-share-storage.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8001-optional-element-writers.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8004-optional-element-conversions.ts"
],
"diags": []
}
@@ -0,0 +1,61 @@
// A number[] or string[] seen as `(T | undefined)[]` is the same array, as
// in Node: undefined written through either name is visible through both,
// whether the wider view is a cast, a binding or a parameter.
const c: number[] = [1.5, 2];
(c as (number | undefined)[])[1] = undefined;
console.log(c);
const e: number[] = [1.5, 2];
const f = e as (number | undefined)[];
f[1] = undefined;
console.log(e, f, e === f);
function clearFirst(values: (number | undefined)[]): void {
values[0] = undefined;
values.push(undefined, 4);
}
const a: number[] = [1, 2, 3];
clearFirst(a);
console.log(a, a.length, a.indexOf(4), a.includes(undefined as unknown as number));
const names: string[] = ["x"];
const wider: (string | undefined)[] = names;
wider.push(undefined);
wider.unshift(undefined);
console.log(names, wider === names, names.length, JSON.stringify(names));
// The wider arrays themselves: literals, pushes, splices and copies keep
// their undefined elements.
const lit: (number | undefined)[] = [1, undefined, 3];
const maybe = (i: number): number | undefined => (i % 2 ? i : undefined);
const built: (number | undefined)[] = [];
for (let i = 0; i < 4; i++) built.push(maybe(i));
built.splice(1, 0, undefined);
console.log(lit, built, lit.indexOf(undefined), built.filter((x): boolean => x === undefined).length);
console.log(built.map((x) => x ?? -1), built.find((x) => x !== undefined), [...built].reverse());
let sum = 0;
for (const x of built) sum += x ?? 0;
console.log(sum, built.join("-"), JSON.stringify(built));
const flags: (boolean | undefined)[] = [true, undefined, false];
const strs: (string | undefined)[] = ["a", undefined];
console.log(flags, flags.filter(Boolean).length, strs.map((s) => s?.toUpperCase()));
// Values from other containers keep their undefined.
const m = new Map<string, number | undefined>([["a", undefined], ["b", 2]]);
const fromMap = [...m.values()];
const fromSet = Array.from(new Set<string | undefined>(["s", undefined]));
const sized = Array.from<number | undefined>({ length: 2 });
console.log(fromMap, fromSet, sized, sized.length);
const dict: { [key: string]: number | undefined } = { a: 1, b: undefined };
console.log(Object.values(dict));
// An asserted element that is undefined throws a catchable TypeError.
function lengthAt(items: (string | undefined)[], i: number): number {
try {
return items[i]!.length;
} catch {
return -1;
}
}
console.log(lengthAt(strs, 0), lengthAt(strs, 1));
@@ -0,0 +1,103 @@
// Every way a value reaches a `(T | undefined)[]` of numbers, strings or
// booleans keeps its undefined: callbacks, generators, Array.of, rest
// packs, destructuring, class fields and copies into Sets and Maps.
const slots = new Map<string, number>([["a", 1]]);
const looked = ["x", "a", "y"].map((key) => slots.get(key));
console.log(looked[0] === undefined, looked, looked.length);
const fromMapped = Array.from(["a", "b"], (key) => slots.get(key));
console.log(fromMapped[1] === undefined, fromMapped);
const objects: { n?: number }[] = [{ n: 1 }, {}];
const fields = objects.map((o) => o.n);
function pick(o: { n?: number }): number | undefined {
return o.n;
}
console.log(fields[1] === undefined, objects.map(pick)[1] === undefined, fields);
function wrap<T>(value: T): T[] {
return [value];
}
console.log(wrap<number | undefined>(undefined)[0] === undefined);
function* maybe(): Generator<number | undefined> {
yield undefined;
yield 2;
}
const drained = [...maybe()];
console.log(drained[0] === undefined, drained, Array.from(maybe()));
const yielding = function* (): Generator<string | undefined> {
yield "s";
yield undefined;
};
console.log([...yielding()]);
console.log(Array.of<number | undefined>(undefined, 1), Array.of<boolean | undefined>(true));
function sum(...values: (number | undefined)[]): number {
return values.reduce<number>((total, v) => total + (v ?? 0), 0);
}
const spread: (number | undefined)[] = [undefined, 4];
console.log(sum(1, undefined, 2), sum(...spread), sum(...spread, undefined, 1));
const [first, second = 7, third] = [3, undefined, 1] as (number | undefined)[];
console.log(first, second, third);
const strings: (string | undefined)[] = ["a", undefined, "a"];
const seen = new Set(strings);
console.log(seen.size, seen.has(undefined), [...seen]);
for (const [i, s] of strings.entries()) console.log(i, s);
for (const key in strings) console.log("key", key);
class Holder {
values: (number | undefined)[] = [];
add(value: number | undefined): void {
this.values.push(value);
}
first(): number | undefined {
return this.values[0];
}
}
const holder = new Holder();
holder.add(undefined);
holder.add(2);
console.log(holder.first(), holder.values, holder.values.length);
const xs: (number | undefined)[] = [3, undefined, 1];
xs.forEach((x, i) => console.log(i, x, typeof x));
console.log(xs.findIndex((x) => x === undefined), xs.findLast((x) => x !== undefined));
console.log(xs.some((x) => x === undefined), xs.every((x) => x !== undefined));
console.log(xs.reduce<number>((acc, x) => acc + (x ?? 10), 0), xs.join("/"), xs.at(1), xs.at(-1));
console.log(xs.toSorted((a, b) => (a ?? 0) - (b ?? 0)), [...xs].sort());
xs[1] ??= 5;
console.log(xs);
const flags: (boolean | undefined)[] = [undefined, true];
console.log(flags.map((b) => !b), flags.indexOf(undefined), flags.includes(true), String(flags));
const nested: (number | undefined)[][] = [[undefined], [1]];
console.log(nested.flat(), nested[0]![0]);
const byKey = new Map<string, (number | undefined)[]>([["k", [undefined, 1]]]);
console.log(byKey.get("k")![0], byKey);
// Promise.all over `Promise<T | undefined>[]` settles on Node's turn, with
// its undefined values.
const lookup = async (key: string): Promise<number | undefined> => {
if (key === "x") throw new Error("missing " + key);
return slots.get(key);
};
const turns: string[] = [];
const pending: Promise<number | undefined>[] = [lookup("a"), lookup("b")];
void Promise.all(pending).then((vs) => turns.push("all " + vs.join(",") + " " + (vs[1] === undefined)));
const failing: Promise<number | undefined>[] = [lookup("a"), lookup("x")];
Promise.all(failing).then(
() => turns.push("unexpected"),
(error: unknown) => turns.push("rejected " + (error as Error).message),
);
void Promise.all([] as Promise<number | undefined>[]).then((vs) => turns.push("empty " + vs.length));
void Promise.resolve()
.then(() => turns.push("t1"))
.then(() => turns.push("t2"))
.then(() => turns.push("t3"))
.then(() => console.log(turns.join(" ")));
// Index reads through a union of arrays, and searches with a number
// needle, see the present undefined elements.
const pairs: ((null | undefined)[] | (number | undefined)[])[] = [[null, undefined], [undefined, 0]];
for (const pair of pairs) console.log(pair[0], pair[1], pair[2]);
const sparse: (number | undefined)[] = [1, , undefined, 1];
console.log(sparse.lastIndexOf(undefined), sparse.lastIndexOf(1, 2), sparse.indexOf(1, 1));
@@ -0,0 +1,82 @@
// `(T | undefined)[]` values built by checked conversions, rest parameters
// and union index reads: nested arrays converted beside their non-optional
// form, a union arm that is an optional-element array, undefined arguments
// and tuple spreads packed into a rest parameter, and an index expression
// that reassigns the receiver.
const plainRows = JSON.parse("[[1, 2], [3]]") as number[][];
console.log(plainRows);
const optionalSource: unknown = (JSON.parse("[[1, null], [null, 4]]") as unknown[][]).map((row) =>
row.map((v) => (v === null ? undefined : v)),
);
const optionalRows = optionalSource as (number | undefined)[][];
console.log(optionalRows, optionalRows[0]![1], optionalRows[1]!.length);
const armSource: unknown = [1, undefined, 3];
const arm = armSource as (number | undefined)[] | string;
console.log(typeof arm === "string" ? arm : arm.length, arm);
const stringArm = ["a", undefined] as unknown as (string | undefined)[] | number;
console.log(stringArm);
const textArm = "text" as unknown as (string | undefined)[] | string;
console.log(textArm);
function flags(...values: (boolean | undefined)[]): string {
let text = "";
for (const value of values) text += value === undefined ? "u" : value ? "t" : "f";
return `${values.length}:${text}:${String(values[1])}`;
}
console.log(flags(true, undefined));
const flagPair: [boolean, boolean | undefined] = [false, undefined];
console.log(flags(...flagPair), flags(true, ...flagPair), flags(...flagPair, true));
function label(first: string, ...rest: (string | undefined)[]): string {
return `${first}/${rest.length}/${rest.map((v) => v ?? "-").join("")}`;
}
const words: [string, string | undefined, string | undefined] = ["a", undefined, "c"];
console.log(label(...words), label("b", undefined));
// Spread tuple fields are read before later arguments run.
const changing: [boolean, boolean | undefined] = [true, undefined];
function flip(pair: [boolean, boolean | undefined], second: boolean | undefined): boolean {
pair[0] = !pair[0];
pair[1] = second;
return pair[0];
}
console.log(flags(...changing, flip(changing, true)));
console.log(flags(...changing, flip(changing, undefined), ...changing));
const swapped: [string, string | undefined] = ["x", "y"];
function swap(pair: [string, string | undefined]): string {
pair[0] = "w";
pair[1] = undefined;
return "v";
}
console.log(label("z", ...swapped, swap(swapped), ...swapped));
// Tuple spreads together with array and Set spreads keep undefined
// elements, and every item is read in argument order.
const mixedPair: [boolean, boolean | undefined] = [true, undefined];
const mixedMore: (boolean | undefined)[] = [false, undefined];
console.log(flags(...mixedPair, ...mixedMore), flags(...mixedMore, ...mixedPair), flags(true, ...mixedPair, ...mixedMore, undefined));
const mixedChanging: [boolean, boolean | undefined] = [true, undefined];
console.log(flags(...mixedChanging, ...mixedMore, flip(mixedChanging, true), ...mixedChanging));
const mixedWords: [string, string | undefined] = ["a", undefined];
console.log(label("z", ...["b", undefined, "c"], ...mixedWords), label("y", ...mixedWords, ...new Set(["s", undefined])));
// A tuple returned by a call keeps its string fields alive until every
// field is packed beside the other spreads.
function makeTuple(n: number): [string, string | undefined, string] {
return ["p" + n.toString(16).repeat(4), n % 2 ? undefined : "q" + n, "r".repeat((n % 7) + 3) + n];
}
for (let i = 0; i < 3; i++) {
console.log(label("z", ...["m", undefined], ...makeTuple(i), ...mixedWords), label("y", ...makeTuple(i + 10), ...new Set(["s" + i])));
}
function pick(start: (number | undefined)[] | string[], other: (number | undefined)[] | string[]): string {
let a = start;
const read = a[((a = other), 0)];
return `${String(read)} ${String(a[0])}`;
}
console.log(pick([1, undefined, 3], ["x", "y"]), pick([undefined, 2], ["z"]));
function counted(start: (number | undefined)[] | string[]): string {
let b = start;
let i = 1;
const read = b[((b = ["q"]), i--)];
return `${String(read)} ${b.length} ${i}`;
}
console.log(counted([4, undefined]), counted(["s", "t"]));
@@ -0,0 +1 @@
{ "plain": 6206, "san": 8487 }
@@ -0,0 +1 @@
{ "plain": 3690, "san": 4000 }
@@ -0,0 +1 @@
{ "plain": 4882, "san": 6379 }