mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-06 08:23:19 +08:00
Compile conditional spreads of several properties and (#1820)
This commit is contained in:
+12
-12
@@ -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;
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7020-conditional-spread-fields.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7021-promise-all-uniform-tuple.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7022-promise-all-dynamic-arrays.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7023-union-object-patterns.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7024-async-return-fresh-collections.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7025-child-listener-chaining.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7026-url-object-patterns.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7027-search-params-conditional-init.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7028-non-empty-rest-tuples.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7029-flatmap-union-results.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7033-any-fields-into-declared-records.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7034-optional-field-union-routes.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
-6
@@ -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();
|
||||
}
|
||||
@@ -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));
|
||||
@@ -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 }
|
||||
Reference in New Issue
Block a user