mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-31 07:23:06 +08:00
Share storage between T[] and (T | undefined)[] (#2080)
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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", ...)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8000-optional-element-arrays-share-storage.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8001-optional-element-writers.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -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 }
|
||||
Reference in New Issue
Block a user