Compile conditional spreads of several properties and (#1820)

This commit is contained in:
Chris Tate
2026-10-22 18:15:22 -05:00
committed by GitHub
57 changed files with 1632 additions and 169 deletions
+12 -12
View File
@@ -3612,20 +3612,20 @@ declare module "child_process" {
export interface ChildProcess extends Disposable {
ref(): void;
on(event: "spawn", listener: () => void): void;
once(event: "spawn", listener: () => void): void;
on(event: "spawn", listener: () => void): this;
once(event: "spawn", listener: () => void): this;
/* The exit listener may also take Node's second parameter — the
* terminating signal's name, null for a normal exit. */
on(event: "exit", listener: (code: number | null, signal: string | null) => void): void;
on(event: "close", listener: (code: number | null, signal: string | null) => void): void;
on(event: "error", listener: (err: Error) => void): void;
once(event: "exit", listener: (code: number | null, signal: string | null) => void): void;
once(event: "close", listener: (code: number | null, signal: string | null) => void): void;
once(event: "error", listener: (err: Error) => void): void;
on(event: "message", listener: (message: any) => void): void;
on(event: "disconnect", listener: () => void): void;
once(event: "message", listener: (message: any) => void): void;
once(event: "disconnect", listener: () => void): void;
on(event: "exit", listener: (code: number | null, signal: string | null) => void): this;
on(event: "close", listener: (code: number | null, signal: string | null) => void): this;
on(event: "error", listener: (err: Error) => void): this;
once(event: "exit", listener: (code: number | null, signal: string | null) => void): this;
once(event: "close", listener: (code: number | null, signal: string | null) => void): this;
once(event: "error", listener: (err: Error) => void): this;
on(event: "message", listener: (message: any) => void): this;
on(event: "disconnect", listener: () => void): this;
once(event: "message", listener: (message: any) => void): this;
once(event: "disconnect", listener: () => void): this;
/* The lifecycle members, Node's exact shapes: pid is undefined exactly
* when the spawn failed; exitCode is null while running, the code
* after a normal exit, null for a signal death, and -errno once a
@@ -1,4 +1,4 @@
import { boolLit, strLit } from "../../../ir/build.js";
import { boolLit, strLit, varRef } from "../../../ir/build.js";
import * as ts from "../../ts7/adapter.js";
import { requestGeneralStreamSurface } from "../general-streams.js";
import { type Lowerer } from "../lowerer.js";
@@ -93,32 +93,47 @@ export function lowerChildMethodCall(
'"exit", "close", "error", "message", and "disconnect" are the supported child events (as literals)',
);
}
if (!ts.isExpressionStatement(call.parent)) {
lowerer.unsupported(
"SC1090",
call,
`chaining child.${name}(...) (the result is void here — register each listener as its own statement)`,
);
}
const receiver = lowerer.lowerExpr(access.expression);
// `child.on(...)` returns the child: a chained call or a stored result
// reads the receiver, which evaluates once, before the listener.
let receiver = lowerer.lowerExpr(access.expression);
const chained = ts.isExpressionStatement(call.parent)
? null
: {
local: lowerer.declareHiddenLocal("%childListenerTarget", receiver.type),
init: receiver,
};
if (chained) receiver = varRef(chained.local.id, chained.local.type, loc);
const registered = (registration: IrExpr): IrExpr =>
chained === null
? registration
: {
kind: "seqExpr",
stmts: [
{ kind: "varDecl", localId: chained.local.id, init: chained.init, loc },
{ kind: "exprStmt", expr: registration, loc },
],
result: varRef(chained.local.id, chained.local.type, loc),
type: chained.local.type,
loc,
};
const once: IrExpr = { kind: "boolLit", value: name === "once", type: BOOL, loc };
if (event === "message") {
return {
return registered({
kind: "libCall",
fn: "child.onMessage",
args: [receiver, ipcMessageListener(lowerer, call.arguments.nodes[1]!), once],
type: VOID,
loc,
};
});
}
if (event === "disconnect") {
return {
return registered({
kind: "libCall",
fn: "child.onDisconnect",
args: [receiver, ipcDisconnectListener(lowerer, call.arguments.nodes[1]!), once],
type: VOID,
loc,
};
});
}
const listenerNode = stripTypeCasts(call.arguments.nodes[1]!);
if (
@@ -163,7 +178,7 @@ export function lowerChildMethodCall(
call.arguments.nodes[1]!,
);
}
return {
return registered({
kind: "libCall",
fn: "child.onDyn",
args: [
@@ -173,7 +188,7 @@ export function lowerChildMethodCall(
],
type: VOID,
loc,
};
});
}
if (event === "spawn") {
if (cb.type.kind !== "func" || cb.type.params.length !== 0 || cb.type.ret.kind !== "void") {
@@ -182,7 +197,13 @@ export function lowerChildMethodCall(
call.arguments.nodes[1]!,
);
}
return { kind: "libCall", fn: "child.onSpawn", args: [receiver, cb], type: VOID, loc };
return registered({
kind: "libCall",
fn: "child.onSpawn",
args: [receiver, cb],
type: VOID,
loc,
});
}
if (cb.type.kind !== "func" || cb.type.params.length > (event === "error" ? 1 : 2)) {
lowerer.unsupported(
@@ -243,13 +264,13 @@ export function lowerChildMethodCall(
`${event} listeners whose signal parameter is not 'Signals | null' (got '${lowerer.fmt(sigParam!)}')`,
);
}
return {
return registered({
kind: "libCall",
fn: event === "close" ? "child.onClose" : "child.onExit",
args: [receiver, cb],
type: VOID,
loc,
};
});
}
if (param !== undefined && !(param.kind === "object" && param.className === "%Error")) {
lowerer.unsupported(
@@ -258,7 +279,13 @@ export function lowerChildMethodCall(
`error listeners whose parameter is not 'Error' (got '${lowerer.fmt(param)}')`,
);
}
return { kind: "libCall", fn: "child.onError", args: [receiver, cb], type: VOID, loc };
return registered({
kind: "libCall",
fn: "child.onError",
args: [receiver, cb],
type: VOID,
loc,
});
}
// child.kill(signal?) — Node's semantics exactly: the name resolves
// through Node's signal table (unknown names throw the ERR_UNKNOWN_SIGNAL
@@ -9,6 +9,7 @@ import { isJsSourceFile, locOf } from "../../program.js";
import {
BOOL,
DYN,
F64,
type IrExpr,
type IrFunction,
type IrStmt,
@@ -376,8 +377,14 @@ export function lowerPromiseStaticCall(
);
}
const entries = lowerer.lowerExpr(argNode);
if (isJsSourceFile(ts.getSourceFileOfNode(call)) && entries.type.kind === "dyn")
return checkedPromiseAll(lowerer, entries, loc);
if (entries.type.kind === "dyn") return checkedPromiseAll(lowerer, entries, loc);
// An 'any'-typed argument that holds a typed value (`const xs: any =
// []`): the combinator reads it as the runtime value it is.
if (
(lowerer.typeOf(argNode).flags & ts.TypeFlags.Any) !== 0 &&
lowerer.dynConvertible(entries.type)
)
return checkedPromiseAll(lowerer, lowerer.coerceToExpected(entries, DYN), loc);
// loadPlugin(p)))` — the loadPlugins shape, where the checker
// spells `any[]`): the ENGINE's own Promise.all runs over the
@@ -845,3 +852,89 @@ function lowerTypedSettledCombinator(
}
return { kind: "intrinsic", name: "promise.allSettled", args: [entries], type: resultT, loc };
}
/** `const pair = await Promise.all([p, q])` where every entry is the same
* Promise<T> and the binding's type is the checker's tuple `[T, T]`: the
* uniform entries settle through one runtime combinator into a T[] (the
* same combinator and turn as the array form), and the binding receives
* the tuple of its elements. Null for every other await. */
export function lowerAwaitedUniformPromiseAll(
lowerer: Lowerer,
expr: ts.AwaitExpression,
): IrExpr | null {
let operand: ts.Expression = expr.expression;
while (ts.isParenthesizedExpression(operand)) operand = operand.expression;
if (
!ts.isCallExpression(operand) ||
operand.questionDotToken ||
!ts.isPropertyAccessExpression(operand.expression) ||
operand.arguments.nodes.length !== 1 ||
isJsSourceFile(ts.getSourceFileOfNode(expr))
)
return null;
const argNode = operand.arguments.nodes[0]!;
if (
!ts.isArrayLiteralExpression(argNode) ||
argNode.elements.nodes.length === 0 ||
argNode.elements.nodes.some(ts.isSpreadElement)
)
return null;
let user: ts.Node = expr;
while (ts.isParenthesizedExpression(user.parent)) user = user.parent;
const decl = user.parent;
if (!ts.isVariableDeclaration(decl) || decl.initializer !== user || !ts.isIdentifier(decl.name))
return null;
if (lowerer.stdlibGlobalMember(operand.expression, "Promise") !== "all") return null;
const tuple = lowerer.mapTypeOf(lowerer.typeOf(decl.name));
if (tuple?.kind !== "record") return null;
const shape = lowerer.shapes.get(tuple.shapeId);
if (!shape?.tuple || shape.fields.length !== argNode.elements.nodes.length) return null;
const elems = argNode.elements.nodes.map((el) => lowerer.lowerExpr(el));
const first = elems[0]!.type;
if (
first.kind !== "promise" ||
first.inner.kind === "void" ||
!elems.every((e) => typeEquals(e.type, first)) ||
!shape.fields.every((f) => typeEquals(f.type, first.inner))
)
return null;
const loc = locOf(expr);
const values = arrayOf(first.inner);
const local = lowerer.declareHiddenLocal("%allValues", values);
const all: IrExpr = {
kind: "intrinsic",
name: "promise.all",
args: [{ kind: "arrayLit", elems, type: arrayOf(first), loc }],
type: { kind: "promise", inner: values },
loc,
};
return {
kind: "seqExpr",
stmts: [
{
kind: "varDecl",
localId: local.id,
init: { kind: "awaitExpr", value: all, type: values, loc },
loc,
},
],
// Shape fields are sorted by name; a tuple field's name is its position.
result: {
kind: "recordLit",
fields: shape.fields.map((f) => ({
name: f.name,
value: {
kind: "arrayGet",
arr: varRef(local.id, values, loc),
index: { kind: "numLit", value: Number(f.name), type: F64, loc },
type: f.type,
loc,
},
})),
type: tuple,
loc,
},
type: tuple,
loc,
};
}
@@ -195,7 +195,30 @@ export function lowerSearchParamsNew(
);
}
const arg = args[0];
if (arg === undefined || (ts.isIdentifier(arg) && arg.text === "undefined")) {
if (arg === undefined) {
return { kind: "libCall", fn: "sp.new", args: [], type: SEARCH_PARAMS_T, loc };
}
return lowerSearchParamsInit(lowerer, arg, loc);
}
/** The URLSearchParams built from one init expression. A conditional
* init (`new URLSearchParams(c ? { a: "1" } : {})`) evaluates its
* condition first and builds from the arm it selects, so each arm may be
* any supported init shape. */
function lowerSearchParamsInit(lowerer: Lowerer, arg: ts.Expression, loc: SrcLoc): IrExpr {
let inner = arg;
while (ts.isParenthesizedExpression(inner)) inner = inner.expression;
if (ts.isConditionalExpression(inner)) {
return {
kind: "ternary",
cond: lowerer.lowerCondition(inner.condition),
then: lowerSearchParamsInit(lowerer, inner.whenTrue, loc),
else_: lowerSearchParamsInit(lowerer, inner.whenFalse, loc),
type: SEARCH_PARAMS_T,
loc,
};
}
if (ts.isIdentifier(arg) && arg.text === "undefined") {
return { kind: "libCall", fn: "sp.new", args: [], type: SEARCH_PARAMS_T, loc };
}
// The object-literal init: `{ a: "1", b: "2" }` appends pairs in
@@ -1,7 +1,7 @@
import { planRecordUnionLift, planRecordUnionWrap } from "../union-retag.js";
import type { WidthLift } from "../width-lift.js";
import type { IrType } from "../../../ir/ir.js";
import { isUnitType, typeEquals, UNDEFINED_T } from "../../../ir/ir.js";
import { canDynCheckTo, isUnitType, typeEquals, UNDEFINED_T } from "../../../ir/ir.js";
import { type ClassInfo, findMethodOn, findGenericMethodOn } from "../lower-classes.js";
import type { Lowerer } from "../lowerer.js";
@@ -21,6 +21,22 @@ export function widthLiftPlan(lowerer: Lowerer, src: IrType, dst: IrType): Width
if (dst.kind === "dyn" && src.kind !== "dyn" && lowerer.dynConvertible(src)) {
return { how: "dynIn" };
}
// An 'unknown' (dyn) SOURCE field into a typed slot — a field TypeScript
// types `any` (a JSON.parse read), which the checker lets into any
// declared field type: the checked extraction, the same dynCheck a
// typed binding initialized from that value applies.
if (
src.kind === "dyn" &&
dst.kind !== "dyn" &&
dst.kind !== "union" &&
canDynCheckTo(
dst,
(id) => lowerer.shapes.get(id),
(id) => lowerer.unions.get(id),
)
) {
return { how: "dynOut" };
}
if (dst.kind === "union") {
if (src.kind === "union") {
if (lowerer.unionRetagMappable(src.unionId, dst.unionId)) return { how: "retag" };
@@ -108,6 +124,10 @@ export function widthLiftPlan(lowerer: Lowerer, src: IrType, dst: IrType): Width
return { how: "discriminantRoute" };
}
}
if (selected.length === 2 && src.kind === "record") {
const route = presenceRoute(lowerer, src.shapeId, selected[0]!, selected[1]!);
if (route) return route;
}
if (selected.length !== 1) return null;
return { how: "liftWrap", tag: selected[0]!.tag, arm: selected[0]!.arm };
}
@@ -202,6 +222,42 @@ export function widthLiftPlan(lowerer: Lowerer, src: IrType, dst: IrType): Width
return null;
}
/** Two record arms a source record fits, where one arm declares a field
* the other lacks and the source types that field `T | undefined`
* (`{ count, errors: errs.length ? errs : undefined }` into `{ count;
* errors: string[] } | { count }`): the field's value picks the arm at
* run time. Null for any other pair. */
function presenceRoute(
lowerer: Lowerer,
sourceId: string,
a: { tag: number; arm: IrType },
b: { tag: number; arm: IrType },
): WidthLift | null {
const source = lowerer.shapes.get(sourceId);
if (!source || source.tuple || source.indexValue || a.arm.kind !== "record") return null;
if (b.arm.kind !== "record") return null;
const shapeA = lowerer.shapes.get(a.arm.shapeId);
const shapeB = lowerer.shapes.get(b.arm.shapeId);
if (!shapeA || !shapeB) return null;
const optional = (t: IrType): boolean =>
t.kind === "union" && lowerer.armTag(t.unionId, UNDEFINED_T) >= 0;
for (const [present, absent, presentShape, absentShape] of [
[a, b, shapeA, shapeB],
[b, a, shapeB, shapeA],
] as const) {
const extra = presentShape.fields.filter(
(f) => !absentShape.fields.some((g) => g.name === f.name),
);
if (extra.length !== 1 || optional(extra[0]!.type)) continue;
if (absentShape.fields.some((g) => !presentShape.fields.some((f) => f.name === g.name)))
continue;
const field = source.fields.find((f) => f.name === extra[0]!.name);
if (!field || !optional(field.type)) continue;
return { how: "presenceRoute", field: field.name, present, absent };
}
return null;
}
/** True for the unit-only element types (`(null | undefined)[]`, the
* `readonly []` mapping): a union whose every arm is a unit. */
export function unitOnlyElem(lowerer: Lowerer, t: IrType): boolean {
@@ -6,7 +6,7 @@ import { buildArrayConversion } from "./builders.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 } from "../../../ir/ir.js";
import { BOOL, DYN, F64, STRING, UNDEFINED_T } from "../../../ir/ir.js";
import { typeKey } from "../../type-mapper.js";
import { findStaticOn, findGenericStaticOn } from "../lower-classes.js";
import { lowerRecordOvfCaptureHelper } from "../containers/indexed-objects.js";
@@ -155,6 +155,76 @@ export function applyWidthLift(
loc,
};
}
case "presenceRoute": {
if (dst.kind !== "union" || value.type.kind !== "record")
throw new InternalCompilerError("lowerer bug: presenceRoute lift shape");
const sourceType = value.type;
const fieldType = lowerer.shapes
.get(sourceType.shapeId)
?.fields.find((f) => f.name === lift.field)?.type;
if (fieldType?.kind !== "union")
throw new InternalCompilerError("lowerer bug: presenceRoute field shape");
// One helper per pair: the conversion reads the source twice.
const key = `presence:${typeKey(sourceType)}:${dst.unionId}:${lift.field}`;
let helper = lowerer.valueHelpers.get(key);
if (!helper) {
helper = `%union.presence.${lowerer.valueHelpers.size}`;
lowerer.valueHelpers.set(key, helper);
const source = (): IrExpr => varRef("v.0", sourceType, loc);
const arm = (route: { tag: number; arm: IrType }): IrExpr => {
const inner = lowerer.widthLiftPlan(sourceType, route.arm);
if (!inner)
throw new InternalCompilerError(
"lowerer bug: planned presenceRoute arm stopped lifting",
);
return {
kind: "unionWrap",
unionId: dst.unionId,
tag: route.tag,
value: lowerer.applyWidthLift(inner, source(), route.arm, loc),
type: dst,
loc,
};
};
lowerer.liftedFns.push({
name: helper,
params: [{ localId: "v.0", name: "v", type: sourceType }],
returnType: dst,
locals: [{ id: "v.0", name: "v", type: sourceType, mutable: false }],
body: [
{
kind: "return",
value: {
kind: "ternary",
cond: {
kind: "unionIsTag",
unionId: fieldType.unionId,
tag: lowerer.armTag(fieldType.unionId, UNDEFINED_T),
value: {
kind: "recordGet",
obj: source(),
shapeId: sourceType.shapeId,
field: lift.field,
type: fieldType,
loc,
},
negated: false,
type: BOOL,
loc,
},
then: arm(lift.absent),
else_: arm(lift.present),
type: dst,
loc,
},
loc,
},
],
loc,
});
}
return { kind: "call", callee: helper, args: [value], type: dst, loc };
}
case "width": {
if (dst.kind !== "record" || value.type.kind !== "record")
throw new InternalCompilerError("lowerer bug: width lift shape");
@@ -220,6 +290,8 @@ export function applyWidthLift(
throw new InternalCompilerError("lowerer bug: dynIn lift against a non-dyn slot");
return { kind: "dynFrom", value, type: DYN, loc };
}
case "dynOut":
return { kind: "dynCheck", value, type: dst, loc };
case "upcast": {
if (dst.kind !== "object" || value.type.kind !== "object")
throw new InternalCompilerError("lowerer bug: upcast lift shape");
@@ -1572,6 +1572,19 @@ export function lowerArrayFlatMapCall(
}
if (fnRet.kind === "union") {
const def = lowerer.unions.get(fnRet.unionId);
const plan = def?.arms.some((a) => a.kind === "array")
? flatMapUnionPlan(lowerer, call, argNode, fnRet)
: null;
if (plan) {
const helper = flatMapUnionHelper(lowerer, elem, fnRet, arity, plan, loc, present);
return {
kind: "call",
callee: helper,
args: [receiver, fnArg],
type: arrayOf(plan.outElem),
loc,
};
}
if (def?.arms.some((a) => a.kind === "array")) {
lowerer.unsupported(
"SC1090",
@@ -1637,6 +1650,278 @@ export function lowerArrayFlatMapCall(
};
}
/** How each arm of a flatMap callback's union result contributes to the
* output: an array arm appends its elements, an array arm the checker
* types `never[]` (an empty literal such as `s ? s : []`) appends nothing,
* and any other arm appends itself. */
interface FlatMapUnionPlan {
outElem: IrType;
arms: ("spread" | "empty" | "value")[];
}
/** The plan for a callback returning `T | U[]`: every non-array arm must
* convert into the result's element type without a check, and every
* array arm's element type must be the result's. Null when an arm does
* not. */
function flatMapUnionPlan(
lowerer: Lowerer,
call: ts.CallExpression,
argNode: ts.Expression,
fnRet: IrType & { kind: "union" },
): FlatMapUnionPlan | null {
const out = lowerer.mapTypeOf(lowerer.typeOf(call));
if (out?.kind !== "array") return null;
const outElem = out.elem;
const arms = lowerer.unions.get(fnRet.unionId)?.arms;
if (!arms) return null;
// The `never[]` constituents of the callback's checked result.
const emptyArms: IrType[] = [];
const signature = lowerer.checker.getCallSignatures(lowerer.typeOf(argNode))[0];
if (signature) {
const ret = lowerer.checker.getReturnTypeOfSignature(signature);
for (const part of ret.isUnionType() ? ts.constituentTypes(ret) : [ret]) {
if (!lowerer.checker.isArrayType(part)) continue;
const [element] = lowerer.checker.getTypeArguments(part as ts.TypeReference);
if (element === undefined || (element.flags & ts.TypeFlags.Never) === 0) continue;
const mapped = lowerer.mapTypeOf(part);
if (mapped) emptyArms.push(mapped);
}
}
const converts = (t: IrType): boolean => {
if (typeEquals(t, outElem)) return true;
if (outElem.kind !== "union") return false;
return lowerer.armTag(outElem.unionId, t) >= 0;
};
const plan: FlatMapUnionPlan["arms"] = [];
for (const arm of arms) {
if (arm.kind === "array" && emptyArms.some((e) => typeEquals(e, arm))) plan.push("empty");
// An array arm copies its elements as they are stored, a present
// undefined included, so its element type is the result's.
else if (arm.kind === "array" && typeEquals(arm.elem, outElem)) plan.push("spread");
else if (arm.kind !== "array" && converts(arm)) plan.push("value");
else return null;
}
return { outElem, arms: plan };
}
/** The flatMap loop over a union-returning callback: each result tests
* its arm and appends its elements, nothing, or itself (Node's
* IsArray decision per value). */
function flatMapUnionHelper(
lowerer: Lowerer,
elem: IrType,
fnRet: IrType & { kind: "union" },
arity: number,
plan: FlatMapUnionPlan,
loc: SrcLoc,
present: boolean,
): string {
// The plan is part of the key: one union result type can carry an empty
// `never[]` arm in one callback and a populated array arm in another.
const key = `flatMapUnion:${typeKey(elem)}:${typeKey(fnRet)}:${typeKey(plan.outElem)}:${plan.arms.join(",")}:${arity}${present ? ":present" : ""}`;
const existing = lowerer.arrHofHelpers.get(key);
if (existing) return existing;
const name = `%arr.flatMap.${lowerer.arrHofHelpers.size}`;
lowerer.arrHofHelpers.set(key, name);
const arrT = arrayOf(elem);
const outT = arrayOf(plan.outElem);
const fnT = funcOf([callbackValueType(lowerer, elem, present), F64, arrT].slice(0, arity), fnRet);
const arms = lowerer.unions.get(fnRet.unionId)!.arms;
const outRef = (): IrExpr => varRef("out.0", outT, loc);
const resultRef = (): IrExpr => varRef("r.0", fnRet, loc);
const outLength = (): IrExpr => ({
kind: "arrIntrinsic",
method: "length",
receiver: outRef(),
args: [],
type: F64,
loc,
});
const locals: IrLocal[] = [
...arrayCallbackLocals(arrT, fnT),
{ id: "out.0", name: "out", type: outT, mutable: false },
{ id: "r.0", name: "r", type: fnRet, mutable: false },
];
const armBody = (tag: number): IrStmt[] => {
const arm = arms[tag]!;
const narrowed: IrExpr = {
kind: "unionNarrow",
unionId: fnRet.unionId,
tag,
value: resultRef(),
type: arm,
loc,
};
if (plan.arms[tag] === "empty") return [];
if (plan.arms[tag] === "value") {
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,
),
plan.outElem,
loc,
),
];
}
const innerT = arm as IrType & { kind: "array" };
const listId = `list.${tag}`;
const lengthId = `m.${tag}`;
const indexId = `j.${tag}`;
const itemId = `inner.${tag}`;
locals.push(
{ id: listId, name: "list", type: innerT, mutable: false },
{ id: lengthId, name: "m", type: F64, mutable: false },
{ id: indexId, name: "j", type: F64, mutable: true },
{ id: itemId, name: "inner", type: arrayValueType(lowerer, innerT.elem), mutable: false },
);
const list = (): IrExpr => varRef(listId, innerT, loc);
const index = (): IrExpr => varRef(indexId, F64, loc);
return [
{ kind: "varDecl", localId: listId, init: narrowed, loc },
{
kind: "varDecl",
localId: lengthId,
init: {
kind: "arrIntrinsic",
method: "length",
receiver: list(),
args: [],
type: F64,
loc,
},
loc,
},
{
kind: "for",
init: { kind: "varDecl", localId: indexId, init: numLit(0, loc), loc },
cond: {
kind: "bin",
op: "<",
left: index(),
right: varRef(lengthId, F64, loc),
type: BOOL,
loc,
},
update: {
kind: "assign",
localId: indexId,
value: { kind: "bin", op: "+", left: index(), right: numLit(1, loc), type: F64, loc },
loc,
},
body: [
{
kind: "if",
cond: arrayIndexPresent(list(), index(), loc),
then: [
{
kind: "varDecl",
localId: itemId,
init: arrayValueRead(lowerer, list(), index(), innerT.elem, loc),
loc,
},
arrayValueStore(
lowerer,
outRef(),
outLength(),
varRef(itemId, arrayValueType(lowerer, innerT.elem), loc),
plan.outElem,
loc,
),
],
else_: null,
loc,
},
],
loc,
},
];
};
let dispatch: IrStmt[] = armBody(arms.length - 1);
for (let tag = arms.length - 2; tag >= 0; tag--) {
dispatch = [
{
kind: "if",
cond: {
kind: "unionIsTag",
unionId: fnRet.unionId,
tag,
value: resultRef(),
negated: false,
type: BOOL,
loc,
},
then: armBody(tag),
else_: dispatch,
loc,
},
];
}
const body: IrStmt[] = [
{
kind: "varDecl",
localId: "out.0",
init: { kind: "arrayLit", elems: [], type: outT, loc },
loc,
},
arrayLengthDeclaration(arrT, loc),
countedFor(loc, varRef("n.0", F64, loc), () => [
{
kind: "if",
cond: currentArrayIndexPresent(arrT, loc),
then: [
{
kind: "varDecl",
localId: "r.0",
init: callArrayCallback(
arrT,
fnT,
fnRet,
arity,
callbackValueRead(
lowerer,
varRef("a.0", arrT, loc),
varRef("i.0", F64, loc),
elem,
present,
loc,
),
loc,
),
loc,
},
...dispatch,
],
else_: null,
loc,
},
]),
{ kind: "return", value: outRef(), loc },
];
lowerer.liftedFns.push({
name,
params: arrayCallbackParams(arrT, fnT),
returnType: outT,
locals,
body,
loc,
});
return name;
}
/** The flatMap loop (array-returning callback), from existing IR nodes:
*
* out = []; n = a.length;
@@ -58,6 +58,25 @@ export function jsHoldsDynObject(lowerer: Lowerer, expr: ts.Expression): boolean
return false;
}
/** The literal is the source of an array destructuring: an assignment's
* right side or a declaration's initializer under an array pattern. */
function isPatternSource(expr: ts.ArrayLiteralExpression): boolean {
let node: ts.Node = expr;
while (ts.isParenthesizedExpression(node.parent)) node = node.parent;
const parent = node.parent;
if (ts.isBinaryExpression(parent))
return (
parent.right === node &&
parent.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
ts.isArrayLiteralExpression(parent.left)
);
return (
ts.isVariableDeclaration(parent) &&
parent.initializer === node &&
ts.isArrayBindingPattern(parent.name)
);
}
/** An empty TypeScript literal whose own type is `never[]`: tsc's type for
* `[]` with no contextual array type (`return []` in an async function,
* whose contextual type is `string[] | PromiseLike<string[]>`; `{ files: [] }`).
@@ -125,6 +144,22 @@ export function lowerArrayLiteral(
)
mapped = inferred;
}
// A destructuring pattern's implied type ends in a rest element
// (`[h, ...t] = [1, 2, 3]` supplies `[number, ...number[]]`), which maps
// to an array. The literal keeps its own fixed tuple layout, read by
// position like any tuple source.
if (
expected === undefined &&
ctxType &&
isPatternSource(expr) &&
lowerer.checker.isTupleType(ctxType) &&
(
(ctxType as ts.TupleTypeReference).getTarget() as ts.TupleType | undefined
)?.elementFlags?.some((flag) => (flag & ts.ElementFlags.Rest) !== 0)
) {
const own = lowerer.mapTypeOf(lowerer.typeOf(expr));
if (own?.kind === "record" && lowerer.shapes.get(own.shapeId)?.tuple) mapped = own;
}
// A JS literal whose OWN inferred type is never-tainted
// (neverTaintedJsType — the evolving `const gb = []`, the mixed
// command tuple `['pwd', []]`) carries no element information: route
@@ -933,11 +933,11 @@ export function lowerObjectLiteral(
// spread's own position (any position: they introduce one fresh
// name, collision-fenced below). Everything else stays fenced.
const cs = conditionalSpreadOf(prop.expression) ?? andSpreadOf(lowerer, prop.expression);
if (cs === "unsupported" || (cs && cs.props.length !== 1)) {
if (cs === "unsupported" && conditionalRecordSpreadOf(lowerer, prop.expression) === null) {
lowerer.unsupported(
"SC1090",
prop,
"conditional spreads beyond `...(c ? { field: v } : {})` (exactly one property against an empty arm — spell other shapes as optional fields)",
"conditional spreads beyond `...(c ? { field: v } : {})` and `...(c ? record : {})` (one arm must be empty — spell other shapes as optional fields)",
);
}
// Spread ORDER is fenced in the field-by-field desugar below, not
@@ -1721,64 +1721,71 @@ export function lowerObjectLiteral(
if (ts.isSpreadAssignment(prop)) {
const cs = conditionalSpreadOf(prop.expression) ?? andSpreadOf(lowerer, prop.expression);
if (cs && cs !== "unsupported") {
// `...(c ? { k: v } : {})` — ONE conditional field at the
// spread's own position: cond evaluates exactly once, `v` only
// when the non-empty arm is taken (ternary arms are lazy), and
// the empty arm holds the interned undefined arm — exactly the
// omitted-optional-field representation, which is also why the
// target field must be optional (tsc types the idiom that way).
const csProp = cs.props[0]!; // single-prop-checked in the fence pass
const name = csProp.name.text;
const fieldType = fieldTypes.get(name);
if (!fieldType) {
if (shape.indexValue) {
// `...(c ? { k: v } : {})` — one conditional field per carried
// property at the spread's own position: cond evaluates exactly
// once (into a hidden local when several fields test it), each
// `v` only when the non-empty arm is taken (ternary arms are
// lazy), in property order, and the empty arm holds the interned
// undefined arm — exactly the omitted-optional-field
// representation, which is also why the target fields must be
// optional (tsc types the idiom that way).
let cond: IrExpr | null = null;
for (const csProp of cs.props) {
const name = csProp.name.text;
const fieldType = fieldTypes.get(name);
if (!fieldType) {
if (shape.indexValue) {
lowerer.unsupported(
"SC1090",
prop,
"conditional spreads into index-signature keys (overflow entries model presence — write the key in an if statement instead)",
);
}
shapeMismatch(prop);
}
// An 'unknown' slot holds the checked-dynamic undefined for the
// empty arm, the absent-property read the width copy uses too.
const absent =
lowerer.absentFieldValue(fieldType, locOf(prop)) ??
(fieldType.kind === "dyn" ? dynUndefinedExpr(locOf(prop)) : null);
if (!absent) {
lowerer.unsupported(
"SC1090",
prop,
"conditional spreads into index-signature keys (overflow entries model presence — write the key in an if statement instead)",
`conditional spreads onto the required field '${name}' (the empty arm leaves it undefined — declare the field optional)`,
);
}
shapeMismatch(prop);
if (fields.some((f) => f.name === name)) {
lowerer.unsupported(
"SC1090",
prop,
`conditional spread of '${name}' over an earlier '${name}' (the desugar keeps one entry per name — restructure so each name has one contributor)`,
);
}
if (cond === null) {
cond = lowerer.lowerCondition(cs.cond);
if (cs.props.length > 1) cond = capture(cond);
}
const valueNode = ts.isPropertyAssignment(csProp) ? csProp.initializer : csProp;
let v = ts.isPropertyAssignment(csProp)
? lowerer.lowerExpr(csProp.initializer)
: lowerer.lowerShorthandValue(csProp);
v = lowerer.coerceInto(valueNode, v, fieldType);
if (!typeEquals(v.type, fieldType)) lowerer.badType(valueNode, lowerer.typeOf(valueNode));
conditionalNames.add(name);
orderSegments.push({ kind: "key", name, definite: false });
fields.push({
name,
value: {
kind: "ternary",
cond,
then: cs.whenTrue ? v : absent,
else_: cs.whenTrue ? absent : v,
type: fieldType,
loc: locOf(prop),
},
});
}
// An 'unknown' slot holds the checked-dynamic undefined for the
// empty arm, the absent-property read the width copy uses too.
const absent =
lowerer.absentFieldValue(fieldType, locOf(prop)) ??
(fieldType.kind === "dyn" ? dynUndefinedExpr(locOf(prop)) : null);
if (!absent) {
lowerer.unsupported(
"SC1090",
prop,
`conditional spreads onto the required field '${name}' (the empty arm leaves it undefined — declare the field optional)`,
);
}
if (fields.some((f) => f.name === name)) {
lowerer.unsupported(
"SC1090",
prop,
`conditional spread of '${name}' over an earlier '${name}' (the desugar keeps one entry per name — restructure so each name has one contributor)`,
);
}
const cond = lowerer.lowerCondition(cs.cond);
const valueNode = ts.isPropertyAssignment(csProp) ? csProp.initializer : csProp;
let v = ts.isPropertyAssignment(csProp)
? lowerer.lowerExpr(csProp.initializer)
: lowerer.lowerShorthandValue(csProp);
v = lowerer.coerceInto(valueNode, v, fieldType);
if (!typeEquals(v.type, fieldType)) lowerer.badType(valueNode, lowerer.typeOf(valueNode));
conditionalNames.add(name);
orderSegments.push({ kind: "key", name, definite: false });
fields.push({
name,
value: {
kind: "ternary",
cond,
then: cs.whenTrue ? v : absent,
else_: cs.whenTrue ? absent : v,
type: fieldType,
loc: locOf(prop),
},
});
continue;
}
// `{ ...base, ... }` — field-by-field copy of a known record
@@ -1790,10 +1797,13 @@ export function lowerObjectLiteral(
// overridden. Field snapshots above preserve the copy order.
let srcNode: ts.Expression = prop.expression;
while (ts.isParenthesizedExpression(srcNode)) srcNode = srcNode.expression;
const condRecord = conditionalRecordSpreadOf(lowerer, prop.expression);
const srcLowered = capture(
prop === expr.properties.nodes[0] && leadingSpreadLowered !== null
? leadingSpreadLowered
: lowerer.lowerExpr(srcNode),
condRecord !== null
? lowerConditionalRecordSpread(lowerer, condRecord, locOf(prop))
: prop === expr.properties.nodes[0] && leadingSpreadLowered !== null
? leadingSpreadLowered
: lowerer.lowerExpr(srcNode),
);
// Array callbacks can receive a record-or-undefined ABI even when
// TypeScript describes a required record. Select the copy strategy
@@ -2731,6 +2741,62 @@ function syntacticAndSpread(
return { cond, props, whenTrue: true };
}
/** `...(c ? source : {})` (either arm order; the empty arm may also be
* `undefined` or `null`) where `source` maps to a record and is not the
* literal the field desugar handles (conditionalSpreadOf). The spread
* copies the source's fields when the source arm is taken and nothing
* otherwise, which is the copy of a `Record | undefined` spread source. */
export function conditionalRecordSpreadOf(
lowerer: Lowerer,
expr: ts.Expression,
): { cond: ts.Expression; source: ts.Expression; whenTrue: boolean } | null {
let e: ts.Expression = expr;
while (ts.isParenthesizedExpression(e)) e = e.expression;
if (!ts.isConditionalExpression(e)) return null;
const unwrap = (a: ts.Expression): ts.Expression => {
let x = a;
while (ts.isParenthesizedExpression(x)) x = x.expression;
return x;
};
const isEmpty = (x: ts.Expression): boolean =>
(ts.isObjectLiteralExpression(x) && x.properties.nodes.length === 0) ||
x.kind === ts.SyntaxKind.NullKeyword ||
(ts.isIdentifier(x) &&
x.text === "undefined" &&
(lowerer.typeOf(x).flags & ts.TypeFlags.Undefined) !== 0);
const whenTrue = unwrap(e.whenTrue);
const whenFalse = unwrap(e.whenFalse);
const sourceIsTrue = isEmpty(whenFalse) && !isEmpty(whenTrue);
if (!sourceIsTrue && !(isEmpty(whenTrue) && !isEmpty(whenFalse))) return null;
const source = sourceIsTrue ? whenTrue : whenFalse;
if (lowerer.mapTypeOf(lowerer.typeOf(source))?.kind !== "record") return null;
return { cond: e.condition, source, whenTrue: sourceIsTrue };
}
/** The spread source of a conditionalRecordSpreadOf match: `c ? source :
* undefined` as a `Record | undefined` value (cond first, the source only
* when its arm is taken). */
function lowerConditionalRecordSpread(
lowerer: Lowerer,
spread: { cond: ts.Expression; source: ts.Expression; whenTrue: boolean },
loc: SrcLoc,
): IrExpr {
const cond = lowerer.lowerCondition(spread.cond);
const source = lowerer.lowerExpr(spread.source);
if (source.type.kind !== "record") lowerer.badType(spread.source, lowerer.typeOf(spread.source));
const type = lowerer.withUndefinedArm(source.type);
const present = lowerer.coerceToExpected(source, type);
const absent = lowerer.wrappedUndefined(type, loc)!;
return {
kind: "ternary",
cond,
then: spread.whenTrue ? present : absent,
else_: spread.whenTrue ? absent : present,
type,
loc,
};
}
/** A conditional spread's carrier property: `name: value` or shorthand,
* with an identifier/string-literal name. */
export type CondSpreadProp = (ts.PropertyAssignment | ts.ShorthandPropertyAssignment) & {
@@ -1,5 +1,6 @@
import { keyedHelperName } from "./stable-names.js";
import { lowerWebAssemblyValue, refuseWebAssemblyUse } from "./builtins/webassembly.js";
import { lowerAwaitedUniformPromiseAll } from "./builtins/promises.js";
import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../ir/build.js";
import { InternalCompilerError } from "../../errors.js";
/* Expression lowering: the expression dispatch (lowerExpr), literals
@@ -575,6 +576,8 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
// boundary for malformed/upstream ASTs.
lowerer.unsupported("SC1090", expr, "top-level await (await outside async functions)");
}
const uniformAll = lowerAwaitedUniformPromiseAll(lowerer, expr);
if (uniformAll) return uniformAll;
const value = lowerer.lowerExpr(expr.expression);
// A promise-or-absent union (`Promise<T> | undefined` values, calls
// of `(...) => Promise<void> | void` callbacks): the await handles
@@ -67,9 +67,11 @@ import {
F64,
type IrExpr,
type IrGlobal,
type IrLibFn,
type IrLocal,
type IrStmt,
type IrType,
SEARCH_PARAMS_T,
STRING,
type SrcLoc,
UNDEFINED_T,
@@ -106,6 +108,7 @@ import {
COMPOUND_ASSIGN_OPS,
type CompoundOp,
STR_METHODS,
NARROW_FIRST,
UNSUPPORTED_STMT,
isStdlibMember,
sideEffectFreeOptionValue,
@@ -3218,6 +3221,20 @@ export function lowerBindingPattern(
lowerModuleNamespacePattern(lowerer, pattern, nsSource, isLet, out);
return;
}
if (srcType.kind === "url") {
lowerUrlObjectPattern(lowerer, pattern, srcRef, isLet, out);
return;
}
if (
srcType.kind === "union" &&
!isJsSourceFile(ts.getSourceFileOfNode(pattern)) &&
lowerer.unions
.get(srcType.unionId)
?.arms.every((arm) => arm.kind === "record" || arm.kind === "object")
) {
lowerUnionObjectPattern(lowerer, pattern, srcRef, srcType, isLet, out);
return;
}
if (srcType.kind !== "record") {
lowerer.unsupported(
"SC1031",
@@ -3432,7 +3449,10 @@ function patternSourceValue(
!arms ||
!present?.length ||
present.length === arms.length ||
(present.length > 1 && !(arrayPattern && present.every((arm) => arm.kind === "array")))
(present.length > 1 &&
!(arrayPattern
? present.every((arm) => arm.kind === "array")
: present.every((arm) => arm.kind === "record" || arm.kind === "object")))
)
return value;
for (const [tag, arm] of arms.entries()) {
@@ -3549,6 +3569,149 @@ function lowerModuleNamespacePattern(
}
}
/** The URL getters an object pattern reads (`const { protocol, hostname }
* = new URL(text)`), each a read of the URL's current serialization. */
const URL_PATTERN_GETTERS: Record<string, IrLibFn> = {
href: "url.href",
origin: "url.origin",
protocol: "url.protocol",
username: "url.username",
password: "url.password",
host: "url.host",
hostname: "url.hostname",
port: "url.port",
pathname: "url.pathname",
search: "url.search",
hash: "url.hash",
};
/** An object pattern over a URL: each element reads its getter in pattern
* order, as `url.name` does; `searchParams` binds the URL's live view.
* Methods and rest elements stay fenced. */
function lowerUrlObjectPattern(
lowerer: Lowerer,
pattern: ts.ObjectBindingPattern,
srcRef: () => IrExpr,
isLet: boolean,
out: IrStmt[],
): void {
for (const el of pattern.elements.nodes) {
if (el.name === undefined) continue;
if (el.dotDotDotToken) {
lowerer.unsupported(
"SC1031",
el,
"rest bindings over URL sources (a URL's properties are prototype getters, which rest elements do not copy)",
);
}
const prop = el.propertyName ?? el.name;
const propName =
ts.isIdentifier(prop) ||
ts.isComputedPropertyName(prop) ||
ts.isStringLiteralLike(prop) ||
ts.isNumericLiteral(prop)
? patternKeyNameOf(lowerer, prop as ts.PropertyName)
: null;
const loc = locOf(el);
let value: IrExpr;
if (propName !== null && Object.hasOwn(URL_PATTERN_GETTERS, propName)) {
value = {
kind: "libCall",
fn: URL_PATTERN_GETTERS[propName]!,
args: [srcRef()],
type: STRING,
loc,
};
} else if (propName === "searchParams") {
value = {
kind: "libCall",
fn: "url.searchParams",
args: [srcRef()],
type: SEARCH_PARAMS_T,
loc,
};
} else {
lowerer.unsupported(
"SC1031",
el,
propName === null
? "destructuring with computed keys that do not fold to one property name"
: `destructuring '${propName}' from a URL (${Object.keys(URL_PATTERN_GETTERS).join(", ")} and searchParams are the supported URL properties)`,
);
}
if (el.initializer) value = applyBindingDefault(lowerer, el, value);
lowerer.bindPatternTarget(el.name, value, isLet, out);
}
}
/** An object pattern over a union of records and class instances
* (`function f({ kind, path }: Add | Remove)`): each element reads a data
* field every arm declares with one type, through the runtime tag, as
* `value.field` does. Rest elements and fields the arms type differently
* stay fenced. */
function lowerUnionObjectPattern(
lowerer: Lowerer,
pattern: ts.ObjectBindingPattern,
srcRef: () => IrExpr,
srcType: IrType & { kind: "union" },
isLet: boolean,
out: IrStmt[],
): void {
const arms = lowerer.unions.get(srcType.unionId)?.arms ?? [];
for (const el of pattern.elements.nodes) {
if (el.name === undefined) continue;
if (el.dotDotDotToken) {
lowerer.unsupported("SC1031", el, `rest bindings over union sources (${NARROW_FIRST})`);
}
const prop = el.propertyName ?? el.name;
const propName =
ts.isIdentifier(prop) ||
ts.isComputedPropertyName(prop) ||
ts.isStringLiteralLike(prop) ||
ts.isNumericLiteral(prop)
? patternKeyNameOf(lowerer, prop as ts.PropertyName)
: null;
if (propName === null) {
lowerer.unsupported(
"SC1031",
el,
"destructuring with computed keys that do not fold to one property name",
);
}
let common: IrType | null = null;
for (const arm of arms) {
let ft: IrType | undefined;
if (arm.kind === "record") {
ft = lowerer.shapes.get(arm.shapeId)?.fields.find((f) => f.name === propName)?.type;
} else if (arm.kind === "object") {
ft = lowerer.classes.get(arm.className)?.fields.get(propName);
}
if (!ft || ft.kind === "void" || isUnitType(ft) || (common && !typeEquals(common, ft))) {
common = null;
break;
}
common = ft;
}
if (common === null) {
lowerer.unsupported(
"SC1031",
el,
`destructuring '${propName}' from a union whose arms type it differently (every arm must be a record or class with a same-typed field '${propName}' — ${NARROW_FIRST})`,
);
}
let value: IrExpr = {
kind: "unionDisc",
unionId: srcType.unionId,
field: propName,
value: srcRef(),
type: common,
loc: locOf(el),
};
if (el.initializer) value = applyBindingDefault(lowerer, el, value);
lowerer.bindPatternTarget(el.name, value, isLet, out);
}
}
/** Whether a pattern's first element has a computed key that does not
* fold to one property name, so the key evaluates at run time. */
function startsWithRuntimeKey(lowerer: Lowerer, pattern: ts.ObjectBindingPattern): boolean {
@@ -10861,9 +10861,29 @@ export class Lowerer {
// Contextual types (such as Proxy descriptors' symbol-valued fields)
// must not force an intermediate record with incompatible field slots.
if (this.returnsLiteralIntoDyn(node, expected)) return this.lowerExprExpecting(node, expected);
if (this.returnsFreshCollection(node) && expected.kind !== "void")
return this.lowerExprExpecting(node, expected);
return this.asyncReturnValueOf(node, this.lowerExpr(node), expected);
}
/** An array literal or a `new Map(...)`, `new Set(...)` or `new
* Array(...)` of the builtins: a fresh collection, never a thenable, so
* an async return takes the declared result type as its construction
* layout the way a synchronous return does (`return []` in a function
* whose inferred result is `Promise<string[]>`). */
private returnsFreshCollection(node: ts.Expression): boolean {
let e = node;
while (ts.isParenthesizedExpression(e)) e = e.expression;
if (ts.isArrayLiteralExpression(e)) return true;
return (
ts.isNewExpression(e) &&
ts.isIdentifier(e.expression) &&
(this.isStdlibGlobal(e.expression, "Map") ||
this.isStdlibGlobal(e.expression, "Set") ||
this.isStdlibGlobal(e.expression, "Array"))
);
}
/** A fresh JS object literal returned into a checked-native (dyn) result,
* which lowers straight into that storage. */
private returnsLiteralIntoDyn(node: ts.Expression, expected: IrType): boolean {
@@ -10995,7 +11015,11 @@ export class Lowerer {
loc,
};
}
if (this.adoptsReturnedPromise() && !this.returnsLiteralIntoDyn(node, expected)) {
if (
this.adoptsReturnedPromise() &&
!this.returnsLiteralIntoDyn(node, expected) &&
!this.returnsFreshCollection(node)
) {
// The async return lowers its operand untyped first (a promise
// adopts before its payload converts); a maybe-promise union needs
// the statement form, which branches between adoption and fulfillment.
@@ -11,6 +11,16 @@ export type WidthLift =
| { how: "retag" }
| { how: "unitRetag" }
| { how: "liftWrap"; tag: number; arm: IrType }
/** A record into a union of two record arms that differ by one field:
* the source field `field` (`T | undefined`) selects `present` when it
* holds a value and `absent`, which lacks the field, when it is
* undefined. */
| {
how: "presenceRoute";
field: string;
present: { tag: number; arm: IrType };
absent: { tag: number; arm: IrType };
}
| { how: "width" }
| { how: "unionWidth" }
| { how: "arr" }
@@ -20,5 +30,6 @@ export type WidthLift =
| { how: "clsWidth" }
| { how: "narrow" }
| { how: "dynIn" }
| { how: "dynOut" }
| { how: "upcast" }
| { how: "funcAdapt" };
@@ -2361,6 +2361,27 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
const args = checker.getTypeArguments(ref);
if (args.length === 0) return arrayOf(unitOnlyUnion(unions));
if (tupleShape?.elementFlags === undefined) return null;
// A leading-required tuple ending in a rest element whose positions
// all share one type (`[string, ...string[]]`, the non-empty list)
// is an array of that type: every read, write and method behaves as
// the array's, and tsc already proves the required positions exist.
const flags = tupleShape.elementFlags;
if (
flags.length >= 2 &&
(flags[flags.length - 1]! & ts.ElementFlags.Rest) !== 0 &&
flags.slice(0, -1).every((f) => (f & ts.ElementFlags.Required) !== 0)
) {
const elems = args.map((a) => mapType(a, ctx));
const first = elems[0];
if (
first &&
first.kind !== "void" &&
isSupportedArrayElem(first) &&
elems.every((e) => e !== null && typeEquals(e, first))
)
return arrayOf(first);
return null;
}
// Optional/rest elements have no fixed native tuple layout.
if (tupleShape.elementFlags.some((f) => !(f & ts.ElementFlags.Required))) {
return null;
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7020-conditional-spread-fields.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7021-promise-all-uniform-tuple.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7022-promise-all-dynamic-arrays.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7023-union-object-patterns.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7024-async-return-fresh-collections.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7025-child-listener-chaining.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7026-url-object-patterns.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7027-search-params-conditional-init.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7028-non-empty-rest-tuples.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7029-flatmap-union-results.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7033-any-fields-into-declared-records.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7034-optional-field-union-routes.ts"
],
"diags": []
}
@@ -1,6 +0,0 @@
{
"order": [
"<repo>/tests/diagnostics/retired-engine-1123-any-conditional-spread.ts"
],
"diags": []
}
@@ -0,0 +1,137 @@
// Conditional spreads that carry several properties, and conditional
// spreads of a record value against an empty arm. The condition runs
// once, the carried values run only when their arm is taken and in
// property order, and the keys appear at the spread's position.
const log: string[] = [];
function note<T>(label: string, value: T): T {
log.push(label);
return value;
}
interface Extra {
region: string;
zone: number;
}
function build(flag: boolean, model: string | undefined, extra: Extra | undefined) {
return {
name: note("name", "tool"),
...(note("cond", flag) ? { verbose: note("verbose", true), depth: note("depth", 2) } : {}),
...(model ? { model } : {}),
...(extra ? extra : {}),
last: note("last", 0),
};
}
for (const [flag, model, extra] of [
[true, "m1", { region: "eu", zone: 3 }],
[false, undefined, undefined],
[true, undefined, { region: "us", zone: 1 }],
] as const) {
log.length = 0;
const out = build(flag, model, extra);
console.log(JSON.stringify(out), Object.keys(out).join(","), log.join(" "));
console.log("verbose" in out, out.depth ?? "no depth", out.region ?? "no region");
}
// The carrying arm can be the false arm, and `c && { ... }` carries
// several properties too.
function flags(quiet: boolean, color: boolean) {
return {
...(quiet ? {} : { level: "info", prefix: "[app]" }),
...(color && { fg: "green", bg: "black", bold: true }),
};
}
console.log(JSON.stringify(flags(false, true)), JSON.stringify(flags(true, false)));
// A record arm in either position, against `{}`, `undefined` or `null`.
interface Auth {
user: string;
token?: string;
}
const auth: Auth = { user: "u", token: "t" };
const bare: Auth = { user: "v" };
function request(useAuth: boolean, source: Auth) {
return {
url: "/x",
...(useAuth ? source : {}),
...(useAuth ? undefined : { anonymous: true }),
};
}
console.log(JSON.stringify(request(true, auth)), JSON.stringify(request(false, auth)));
console.log(JSON.stringify(request(true, bare)), Object.keys(request(true, bare)).join(","));
function fallback(primary: Auth | null, secondary: Auth) {
return { ...(primary === null ? secondary : null), id: 1 };
}
console.log(JSON.stringify(fallback(null, auth)), JSON.stringify(fallback(bare, auth)));
// A record arm read through a call runs only when its arm is taken.
let reads = 0;
function current(): Extra {
reads++;
return { region: "ap", zone: reads };
}
const a = { ...(reads > 0 ? current() : {}), tag: "a" };
const b = { ...(reads === 0 ? current() : {}), tag: "b" };
console.log(JSON.stringify(a), JSON.stringify(b), reads);
// Into a declared shape with optional fields.
interface Options {
cwd: string;
shell?: boolean;
timeout?: number;
env?: string;
}
function options(detached: boolean, env: string | undefined): Options {
return { cwd: "/", ...(detached ? { shell: false, timeout: 5 } : {}), ...(env ? { env } : {}) };
}
console.log(JSON.stringify(options(true, "prod")), JSON.stringify(options(false, undefined)));
// Inside 'any'-typed object literals, where the key is absent when the
// empty arm is taken.
function anyLiterals(): void {
function opts(aspect: string | undefined): any {
const o: any = {
mode: "M",
...(aspect ? { cfg: { aspect } } : {}),
};
return o;
}
const a = opts("1:1");
console.log(`${a.mode} ${a.cfg.aspect} ${typeof a.cfg}`);
const b = opts(undefined);
console.log(`${b.mode} ${typeof b.cfg}`);
// Several carried properties.
function multi(on: boolean): any {
return { base: 1, ...(on ? { x: "X", y: "Y" } : {}) };
}
const m1 = multi(true);
const m2 = multi(false);
console.log(`${m1.base} ${m1.x} ${m1.y}`);
console.log(`${m2.base} ${typeof m2.x} ${typeof m2.y}`);
// Reversed orientation and string-literal keys.
function rev(quiet: boolean): any {
return { kind: "r", ...(quiet ? {} : { "log-level": "loud" }) };
}
console.log(`${rev(false)["log-level"]}`);
console.log(`${typeof rev(true)["log-level"]}`);
// The spread mid-literal: properties around it evaluate exactly once and
// land in engine key order.
let sides = 0;
function side(v: number): number {
sides++;
return v;
}
function ordered(flag: boolean): any {
const o: any = { first: side(1), ...(flag ? { mid: 2 } : {}), last: side(3) };
return o;
}
const o1 = ordered(true);
const o2 = ordered(false);
console.log(`${o1.first} ${o1.mid} ${o1.last}`);
console.log(`${o2.first} ${typeof o2.mid} ${o2.last} ${sides}`);
}
anyLiterals();
@@ -0,0 +1,46 @@
// `const pair = await Promise.all([p, q])` over entries of one promise
// type: the checker types the binding as a tuple, and the values arrive
// on the same microtask turn as Node's, in entry order whatever the
// settlement order.
const order: string[] = [];
function later(label: string, turns: number): Promise<string> {
return new Promise((resolve) => {
let left = turns;
const step = (): void => {
if (left-- === 0) {
order.push(`settle ${label}`);
resolve(`${label}!`);
} else queueMicrotask(step);
};
step();
});
}
async function count(n: number): Promise<number> {
return n * 10;
}
async function main(): Promise<void> {
let ticking = true;
void (async () => {
for (let i = 0; i < 20 && ticking; i++) {
order.push(`t${i}`);
await null;
}
})();
const bodies = await Promise.all([later("a", 3), later("b", 1)]);
order.push(`bodies ${bodies[0]} ${bodies[1]} ${bodies.length}`);
const numbers = await Promise.all([count(1), count(2), count(3)]);
const [x, y, z] = numbers;
order.push(`numbers ${x + y + z} ${numbers.length}`);
const annotated: [string, string] = await Promise.all([later("c", 0), later("d", 0)]);
order.push(`annotated ${annotated.join("+")}`);
ticking = false;
console.log(order.join("\n"));
try {
const failed = await Promise.all([later("e", 2), Promise.reject<string>(new Error("no"))]);
console.log("unreachable", failed[0]);
} catch (e) {
console.log("caught", (e as Error).message);
}
}
void main();
@@ -0,0 +1,41 @@
// Promise.all over arrays built from 'any' values: the argument is a
// runtime value, so the combinator reads it with Node's iteration and
// resolves each entry (promises, thenables and plain values).
interface Check {
name: string;
ok: boolean;
}
async function check(name: string): Promise<Check> {
await null;
return { name, ok: name.length > 2 };
}
async function main(): Promise<void> {
const raw: any = JSON.parse('{"items":[{"name":"abc"},{"name":"x"}],"ids":[1,2,3]}');
const results = await Promise.all(raw.items.map((item: any) => check(item.name)));
for (const r of results) console.log(r.name, r.ok);
const doubled = await Promise.all(raw.ids.map(async (id: number) => id * 2));
console.log(doubled.join(","), doubled.length);
const mixed = await Promise.all(raw.ids.map((id: number) => (id % 2 ? id : Promise.resolve(-id))));
console.log(JSON.stringify(mixed));
const parsed: unknown = JSON.parse('["a","bb","ccc"]');
const list = Array.isArray(parsed) ? parsed : [];
const lengths = await Promise.all(list.map(async (s) => String(s).length));
console.log(lengths, lengths.reduce((a, b) => a + b, 0));
const empty: any = [];
console.log(JSON.stringify(await Promise.all(empty)));
try {
await Promise.all(raw.items.map(async (item: any) => {
if (item.name === "x") throw new Error(`bad ${item.name}`);
return item.name;
}));
} catch (e) {
console.log("rejected", (e as Error).message);
}
try {
await Promise.all(raw.missing);
} catch (e) {
console.log("not iterable", e instanceof TypeError);
}
}
void main();
@@ -0,0 +1,63 @@
// Object patterns over unions of records and classes read the fields
// every arm declares, in parameters, declarations and loops.
interface Add {
kind: "add";
path: string;
content: string;
mode?: number;
}
interface Remove {
kind: "remove";
path: string;
mode?: number;
}
type Change = Add | Remove;
function describe({ kind, path: file, mode = 0o644 }: Change): string {
return `${kind}:${file}:${mode.toString(8)}`;
}
const changes: Change[] = [
{ kind: "add", path: "a.txt", content: "x" },
{ kind: "remove", path: "b.txt", mode: 0o600 },
];
for (const change of changes) console.log(describe(change));
for (const { kind, path } of changes) console.log(kind, path.length);
function applyChange(change: Change): number {
const { path } = change;
let { kind } = change;
if (kind === "add") kind = "remove";
return path.length + kind.length;
}
console.log(applyChange(changes[0]!), applyChange(changes[1]!));
// The source runs once, and the fields read in pattern order.
let reads = 0;
function next(): Change {
reads++;
return reads % 2 ? changes[0]! : changes[1]!;
}
const { path: p1, kind: k1 } = next();
const { kind: k2 } = next();
console.log(p1, k1, k2, reads);
class Circle {
name: string;
size: number;
constructor(name: string, size: number) {
this.name = name;
this.size = size;
}
}
class Square {
name: string;
size: number;
corners = 4;
constructor(name: string, size: number) {
this.name = name;
this.size = size;
}
}
function label({ name, size }: Circle | Square): string {
return `${name}/${size}`;
}
console.log(label(new Circle("c", 2)), label(new Square("s", 3)));
@@ -0,0 +1,48 @@
// Async functions can return an empty array literal (where the result is
// inferred) or a new Map or Set: the value takes the result's element
// types, as a synchronous return does.
import { existsSync, readdirSync } from "node:fs";
async function listJson(dir: string) {
if (!existsSync(dir)) return [];
return readdirSync(dir).filter((name) => name.endsWith(".json"));
}
async function childPaths(root: string) {
if (root === "") {
return [];
}
const found = [root, `${root}/b`];
return found;
}
async function index(names: string[]): Promise<Map<string, number>> {
if (names.length === 0) return new Map();
const byName = new Map<string, number>();
for (const name of names) byName.set(name, name.length);
return byName;
}
async function unique(words: string[]): Promise<Set<string>> {
if (words.length === 0) return new Set();
return new Set<string>(words);
}
const pick = async (flag: boolean) => (flag ? [] : ["a", "b"]);
async function main(): Promise<void> {
const missing = await listJson("/nonexistent-dir-for-test");
missing.push("pushed");
console.log(missing, missing.length);
console.log(await childPaths(""), await childPaths("/root"));
const empty = await index([]);
empty.set("late", 4);
console.log(empty.size, empty.get("late"), (await index(["ab", "c"])).get("ab"));
const none = await unique([]);
none.add("z");
console.log([...none], [...(await unique(["x", "x", "y"]))]);
const first = await pick(true);
first.push("c");
console.log(first, await pick(false));
}
void main();
@@ -0,0 +1,42 @@
// child.on() and child.once() return the child, so registrations chain
// and the result can be stored or passed on. The receiver evaluates once,
// before the listener. Each child starts after the previous one closed.
import { spawn } from "node:child_process";
const events: string[] = [];
let lookups = 0;
const children = [spawn("sh", ["-c", "exit 3"], { stdio: "ignore" })];
function first() {
lookups++;
return children[0]!;
}
const returned = first()
.on("exit", (code) => events.push(`exit ${code}`))
.once("close", (code, signal) => {
events.push(`close ${code} ${signal}`);
console.log(events.join(", "), lookups);
second();
})
.on("error", (err) => events.push(`error ${err.message}`));
console.log(returned === children[0], lookups);
function second(): void {
spawn("sh", ["-c", "exit 0"], { stdio: "ignore" })
.on("spawn", () => console.log("spawned"))
.on("exit", (code) => console.log("second exit", code))
.on("close", () => third());
}
function third(): void {
const missing = spawn("/nonexistent/command-for-test", [], { stdio: "ignore" }).on(
"error",
(e) => console.log("missing", (e as NodeJS.ErrnoException).code),
);
console.log(typeof missing.pid);
const detached = spawn("sh", ["-c", "exit 0"], { detached: true, stdio: "ignore" }).once(
"error",
(e) => console.log("spawn failed", e.message),
);
detached.unref();
}
+27
View File
@@ -0,0 +1,27 @@
// Object patterns over URLs read the URL's getters in pattern order,
// with renames and defaults, in declarations and parameters.
let built = 0;
function parse(text: string): URL {
built++;
return new URL(text);
}
const { protocol, hostname, port } = parse("http://localhost:8080/path?q=1#top");
console.log(protocol, hostname, port, built);
const { host: where, pathname: path = "/", search, hash, href } = parse("https://example.com/a/b?x=1&y=2#h");
console.log(where, path, search, hash, href, built);
function origin({ protocol: scheme, host, username, password }: URL): string {
return `${scheme}//${username}:${password}@${host}`;
}
console.log(origin(new URL("ftp://user:secret@files.example.org:2121/dir")));
const { searchParams } = new URL("http://h/?a=1&b=2");
searchParams.append("c", "3");
console.log(searchParams.toString(), [...searchParams.keys()].join(","));
const { origin: o, port: p } = new URL("https://[::1]:8443/");
console.log(o, p);
for (const text of ["http://a.test:1", "http://b.test"]) {
const { hostname: name, port: listen } = new URL(text);
console.log(name, listen === "" ? "(default)" : listen);
}
@@ -0,0 +1,21 @@
// new URLSearchParams(c ? a : b) builds from the arm the condition
// selects; each arm may be a record literal, a string, pairs or another
// URLSearchParams.
let checks = 0;
function flag(value: boolean): boolean {
checks++;
return value;
}
for (const on of [true, false]) {
const params = new URLSearchParams(flag(on) ? { code: "x y", state: "s&t" } : {});
console.log(params.toString(), params.size);
const query = new URLSearchParams(on ? "a=1&b=2" : [["c", "3"]]);
console.log(query.toString());
const base = new URLSearchParams("keep=1");
const copy = new URLSearchParams(on ? base : undefined);
copy.append("more", "2");
console.log(copy.toString(), base.toString());
const nested = new URLSearchParams(on ? (checks > 1 ? { n: "2" } : { n: "1" }) : "?empty=");
console.log(nested.toString());
}
console.log(checks);
@@ -0,0 +1,38 @@
// Tuples of required positions followed by a rest element of the same
// type (`[string, ...string[]]`, the non-empty list) behave as arrays of
// that type.
type Hosts = [string, ...string[]];
const LOCAL_HOSTS: Hosts = ["127.0.0.1", "::1", "localhost"];
const single: [string, ...string[]] = ["only"];
const pair: readonly [number, number, ...number[]] = [1, 2];
function first(hosts: Hosts): string {
return hosts[0];
}
function isLocalHost(host: string): boolean {
return LOCAL_HOSTS.includes(host);
}
console.log(first(LOCAL_HOSTS), first(single), LOCAL_HOSTS.length, single.length);
console.log(isLocalHost("::1"), isLocalHost("10.0.0.1"), LOCAL_HOSTS.indexOf("localhost"));
console.log(LOCAL_HOSTS.slice(1).join(","), LOCAL_HOSTS.map((h) => h.length));
for (const host of LOCAL_HOSTS) console.log(host);
const [head, ...rest] = LOCAL_HOSTS;
console.log(head, rest, JSON.stringify(LOCAL_HOSTS));
single.push("more");
console.log(single, single[1], pair[0] + pair[1], pair.length);
function hostsFrom(text: string): Hosts {
const parts = text.split(",");
const out: Hosts = [parts[0]!];
for (const part of parts.slice(1)) out.push(part);
return out;
}
const parsed = hostsFrom("a,b,c");
console.log(parsed, parsed.length, `${parsed}`);
function join(...names: [string, ...string[]]): string {
return names.join("+");
}
console.log(join("x"), join("x", "y", "z"), join(...LOCAL_HOSTS));
const lists: Hosts[] = [LOCAL_HOSTS, single];
console.log(lists.map((l) => l[0]).join(" "));
@@ -0,0 +1,44 @@
// flatMap callbacks that return a value or an array: an array result
// appends its elements (holes skipped), any other result appends itself,
// and an empty `[]` arm appends nothing.
const words = ["a", "", "c", "", "e"];
console.log(words.flatMap((s) => (s ? s : [])));
console.log(words.flatMap((s, i) => (s ? [s, String(i)] : [])));
function expand(line: string): string | string[] {
return line.includes(",") ? line.split(",") : line;
}
console.log(["x", "y,z", "w"].flatMap(expand), ["x", "y,z"].flatMap((l) => expand(l)).length);
const numbers = [1, 2, 3, 4];
console.log(numbers.flatMap((n) => (n % 2 ? n : [n, n * 10])));
const sparse: number[] = [];
sparse[0] = 1;
sparse[2] = 3;
console.log(numbers.flatMap((n) => (n === 2 ? sparse : n)));
interface Entry {
name: string;
tags?: string[];
}
const entries: Entry[] = [{ name: "a", tags: ["t1", "t2"] }, { name: "b" }];
console.log(entries.flatMap((e) => e.tags ?? []), entries.flatMap((e) => e.tags ?? e.name));
let calls = 0;
const out = words.flatMap((s) => {
calls++;
return s.length > 0 ? s.toUpperCase() : [];
});
console.log(out, calls);
// One union result type with an empty arm in one callback and a populated
// array arm in another.
const counts = [1, 2, 3];
console.log(counts.flatMap((n) => (n % 2 ? n : [])), counts.flatMap((n) => (n % 2 ? n : [n, n])));
// Array results keep present undefined elements, and an undefined result
// appends itself.
const withGaps: (number | undefined)[] = [4, undefined, 6];
const maybe: (number | undefined)[] = [1, 2, undefined];
console.log(maybe.flatMap((n) => (n === 2 ? withGaps : n)));
const gaps: (string | undefined)[] = ["x", undefined];
console.log(["a", "b"].flatMap((s) => (s === "b" ? gaps : s)));
@@ -0,0 +1,28 @@
// Records whose fields TypeScript types `any` convert into declared
// record types: each such field is checked against the declared type,
// as a typed binding initialized from the value is.
interface Transfer {
source: string;
targetId: string;
bytes: number;
tags: string[];
}
const meta: any = JSON.parse('{"id":"x-1","size":4,"tags":["a","b"]}');
const mapped: Transfer[] = ["a", "b"].map((s, i) => ({
source: s,
targetId: meta.id,
bytes: meta.size + i,
tags: meta.tags,
}));
console.log(JSON.stringify(mapped));
function build(): Transfer {
const record = { source: "c", targetId: meta.id, bytes: meta.size, tags: [] as string[] };
return record;
}
console.log(JSON.stringify(build()));
const list: Transfer[] = [];
for (const entry of [{ source: "d", targetId: meta.id, bytes: 1, tags: meta.tags }]) list.push(entry);
console.log(list.length, list[0]!.targetId.toUpperCase(), list[0]!.tags.length);
@@ -0,0 +1,26 @@
// A record whose `T | undefined` field is the one field that tells two
// arms of a union apart converts into that union: a value picks the arm
// that declares the field, and undefined picks the arm without it.
type Summary =
| { written: number; errors: string[]; ignored: number }
| { written: number; ignored: number };
function summarize(written: number, ignored: number, errors: string[]): Summary {
return { written, errors: errors.length > 0 ? errors : undefined, ignored };
}
function errorCount(summary: Summary): number {
return "errors" in summary && summary.errors !== undefined ? summary.errors.length : 0;
}
for (const errors of [[], ["e1", "e2"]]) {
const summary = summarize(3, 1, errors);
console.log(JSON.stringify(summary), errorCount(summary), summary.written + summary.ignored);
}
type Lookup = { name: string; value: number; source: string } | { name: string; source: string };
const values = new Map<string, number>([["a", 1]]);
const lookups: Lookup[] = ["a", "b"].map((name) => ({
name,
value: values.get(name),
source: "map",
}));
console.log(JSON.stringify(lookups));
+3 -3
View File
@@ -38,12 +38,12 @@ interface E1 {
}
interface E2 {
kind: "two";
b: string;
b: number;
label: string;
}
function fromUnion(u: E1 | E2): string {
const { b } = u; // union source: narrow first
return b;
const { b } = u; // union source whose arms type the field differently: narrow first
return String(b);
}
console.log(fromUnion({ kind: "one", b: "s", count: 1 }));
@@ -1,49 +0,0 @@
// The conditional-spread idiom inside `any`-typed object literals:
// `{ a, ...(c ? { k: v } : {}) }` builds one of two engine literals behind
// the test, so the key is truly ABSENT when the empty arm is taken —
// engine-exact (`typeof o.k` answers "undefined", JSON drops it), with the
// shared properties evaluating exactly once.
function opts(aspect: string | undefined): any {
const o: any = {
mode: "M",
...(aspect ? { cfg: { aspect } } : {}),
};
return o;
}
const a = opts("1:1");
console.log(`${a.mode} ${a.cfg.aspect} ${typeof a.cfg}`);
const b = opts(undefined);
console.log(`${b.mode} ${typeof b.cfg}`);
// Multi-property carrier arms are fine in the island form.
function multi(on: boolean): any {
return { base: 1, ...(on ? { x: "X", y: "Y" } : {}) };
}
const m1 = multi(true);
const m2 = multi(false);
console.log(`${m1.base} ${m1.x} ${m1.y}`);
console.log(`${m2.base} ${typeof m2.x} ${typeof m2.y}`);
// Reversed orientation and string-literal keys.
function rev(quiet: boolean): any {
return { kind: "r", ...(quiet ? {} : { "log-level": "loud" }) };
}
console.log(`${rev(false)["log-level"]}`);
console.log(`${typeof rev(true)["log-level"]}`);
// The spread mid-literal: properties around it evaluate exactly once and
// land in engine key order.
let sides = 0;
function side(v: number): number {
sides++;
return v;
}
function ordered(flag: boolean): any {
const o: any = { first: side(1), ...(flag ? { mid: 2 } : {}), last: side(3) };
return o;
}
const o1 = ordered(true);
const o2 = ordered(false);
console.log(`${o1.first} ${o1.mid} ${o1.last}`);
console.log(`${o2.first} ${typeof o2.mid} ${o2.last} ${sides}`);
@@ -14,7 +14,7 @@ child-process.ts:27:14 - error SC1090: listeners returning a value (make the cal
| ^~~~~~~~~~~~~~~
28 | // Methods have no bound-value form — call on directly.
child-process.ts:29:7 - error SC2007: values of type '{ (event: "spawn", listener: () => void): void; (event: "exit", listener: (code: number | null, signal: string | null) => void): void; (event: "close", listener: (code: number | null, signal: string | null) => void): void; (event: "error", listener: (err: Error) => void): void; (event: "message", listener: (message:...' cannot be compiled: the type declares multiple call signatures (overloads), and a compiled function value is always one concrete signature
child-process.ts:29:7 - error SC2007: values of type '{ (event: "spawn", listener: () => void): ChildProcess; (event: "exit", listener: (code: number | null, signal: string | null) => void): ChildProcess; (event: "close", listener: (code: number | null, signal: string | null) => void): ChildProcess; (event: "error", listener: (err: Error) => void): ChildProcess; (event...' cannot be compiled: the type declares multiple call signatures (overloads), and a compiled function value is always one concrete signature
28 | // Methods have no bound-value form — call on directly.
29 | const f = c.on;
@@ -19,18 +19,18 @@ destructuring-fences.ts:31:9 - error SC1090: reading a property that has only a
| ^~~~~~
32 | console.log(onlyIn);
destructuring-fences.ts:45:9 - error SC1031: object destructuring of non-record values (the source is union-typed) are not supported yet
destructuring-fences.ts:45:11 - error SC1031: destructuring 'b' from a union whose arms type it differently (every arm must be a record or class with a same-typed field 'b' — narrow first: check a discriminant field, or compare with '!== undefined'/'!== null' for unit arms) is not supported yet
44 | function fromUnion(u: E1 | E2): string {
45 | const { b } = u; // union source: narrow first
| ^~~~~
46 | return b;
45 | const { b } = u; // union source whose arms type the field differently: narrow first
| ^
46 | return String(b);
destructuring-fences.ts:46:10 - error SC2004: uses of 'b' inherit the blocker on its declaration
destructuring-fences.ts:46:17 - error SC2004: uses of 'b' inherit the blocker on its declaration
45 | const { b } = u; // union source: narrow first
46 | return b;
| ^
45 | const { b } = u; // union source whose arms type the field differently: narrow first
46 | return String(b);
| ^
47 | }
hint: the declaration of 'b' did not compile — fix the diagnostic reported there and these sites clear with it
@@ -1,6 +0,0 @@
retired-engine-1123-any-conditional-spread.ts:21:21 - error SC1090: conditional spreads beyond `...(c ? { field: v } : {})` (exactly one property against an empty arm — spell other shapes as optional fields) is not supported yet
20 | function multi(on: boolean): any {
21 | return { base: 1, ...(on ? { x: "X", y: "Y" } : {}) };
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
22 | }
@@ -1,4 +1,4 @@
spread-fences.ts:14:21 - error SC1090: conditional spreads beyond `...(c ? { field: v } : {})` (exactly one property against an empty arm — spell other shapes as optional fields) is not supported yet
spread-fences.ts:14:21 - error SC1090: conditional spreads beyond `...(c ? { field: v } : {})` and `...(c ? record : {})` (one arm must be empty — spell other shapes as optional fields) is not supported yet
13 | const cond = true;
14 | const s1: Shape = { ...(cond ? { x: 1, y: 2 } : { x: 3, y: 4 }) };
@@ -0,0 +1 @@
{ "plain": 3640, "san": 5580 }
@@ -0,0 +1 @@
{ "plain": 2510, "san": 3630 }
@@ -0,0 +1 @@
{ "plain": 3330, "san": 4690 }
@@ -0,0 +1 @@
{ "plain": 2440, "san": 3670 }
@@ -0,0 +1 @@
{ "plain": 2570, "san": 4360 }
@@ -0,0 +1 @@
{ "plain": 2510, "san": 3950 }
@@ -0,0 +1 @@
{ "plain": 2520, "san": 4000 }
@@ -0,0 +1 @@
{ "plain": 2240, "san": 3440 }
@@ -0,0 +1 @@
{ "plain": 2520, "san": 4030 }
@@ -0,0 +1 @@
{ "plain": 2640, "san": 4710 }
@@ -0,0 +1 @@
{ "plain": 2640, "san": 3950 }
@@ -0,0 +1 @@
{ "plain": 2420, "san": 3640 }