mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
pi wave 6: records and unions (#2597)
This commit is contained in:
@@ -193,6 +193,26 @@ export function widthLiftPlan(lowerer: Lowerer, src: IrType, dst: IrType): Width
|
||||
if (exact.length === 1) selected = exact;
|
||||
else if (exact.length === 0 && lossless.length === 1) selected = lossless;
|
||||
}
|
||||
// An 'unknown' (dyn) source whose union destination has no dyn arm of
|
||||
// its own (`details: any` into `Details | undefined`, a tool result's
|
||||
// `structuredContent: unknown` into the JSON-value union): the checked
|
||||
// extraction, the same dynCheck coerceToExpected performs for a dyn
|
||||
// value in a union slot. A dyn source has no arm family, so every arm
|
||||
// rule above has passed it by, and every plan that exists for a dyn
|
||||
// source is the one above (its own arm) — this is exactly the case
|
||||
// that returned null (SC2002) before, so no convertible value changes
|
||||
// its lowering.
|
||||
if (
|
||||
selected.length !== 1 &&
|
||||
src.kind === "dyn" &&
|
||||
canDynCheckTo(
|
||||
dst,
|
||||
(id) => lowerer.shapes.get(id),
|
||||
(id) => lowerer.unions.get(id),
|
||||
)
|
||||
) {
|
||||
return { how: "dynOut" };
|
||||
}
|
||||
if (selected.length !== 1) return null;
|
||||
return { how: "liftWrap", tag: selected[0]!.tag, arm: selected[0]!.arm };
|
||||
}
|
||||
|
||||
@@ -632,7 +632,14 @@ function arrayHofHelper(
|
||||
* original concrete array arm — never a copied `(T | U)[]` — and an
|
||||
* arm-specific function adapter wraps the element and receiver arguments
|
||||
* into the union-wide callback ABI. That preserves sparse-array reads,
|
||||
* callback mutation, and the callback's third-argument identity. */
|
||||
* callback mutation, and the callback's third-argument identity.
|
||||
*
|
||||
* `filter` joins them: the kept elements are the checker's result element
|
||||
* (`(b): b is TextContent` over `(TextContent | ImageContent)[] |
|
||||
* (ThinkingContent | TextContent)[]` answers `TextContent[]`), so each arm
|
||||
* filters its own array and converts its survivors through the checked
|
||||
* narrowing — a predicate that kept an element it does not describe throws
|
||||
* the TypeError. An arm with no such conversion leaves the fence in place. */
|
||||
export function lowerArrayUnionHofCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
@@ -640,7 +647,7 @@ export function lowerArrayUnionHofCall(
|
||||
receiverT: IrType & { kind: "union" },
|
||||
): IrExpr | null {
|
||||
const method = access.name.text;
|
||||
if (method !== "map" && method !== "forEach") return null;
|
||||
if (method !== "map" && method !== "forEach" && method !== "filter") return null;
|
||||
if (!lowerer.isStdlibMember(access)) return null;
|
||||
const def = lowerer.unions.get(receiverT.unionId);
|
||||
if (!def || def.arms.length < 2 || !def.arms.every((arm) => arm.kind === "array")) return null;
|
||||
@@ -682,8 +689,45 @@ export function lowerArrayUnionHofCall(
|
||||
lowerer.badType(call, lowerer.typeOf(call));
|
||||
}
|
||||
if (method === "map") requireProducedArrayElement(lowerer, call, "'.map()'", fnRet);
|
||||
const outElem = method === "map" ? callbackArrayElement(lowerer, call, fnRet) : VOID;
|
||||
const resultT: IrType = method === "map" ? arrayOf(outElem) : VOID;
|
||||
if (method === "filter" && !filterPredicateOk(lowerer, fnRet)) {
|
||||
if (fnRet.kind === "void") {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
call,
|
||||
"'.filter()' with a void-returning predicate (the callback return value is erased before its truthiness can be tested)",
|
||||
);
|
||||
}
|
||||
lowerer.badType(call, lowerer.typeOf(call));
|
||||
}
|
||||
// `map` stores the callback's answer; `filter` keeps the elements its
|
||||
// predicate named. The kept element is the CHECKER's result element, which
|
||||
// for a written type predicate is narrower than every arm's own:
|
||||
// `(b): b is TextContent` over `(TextContent | ImageContent)[] |
|
||||
// (ThinkingContent | TextContent)[]` answers `TextContent[]`. Each arm
|
||||
// filters its own concrete array and converts its survivors to that
|
||||
// element through the checked narrowing — a predicate that kept an element
|
||||
// it does not describe throws instead of misreading it. No conversion the
|
||||
// arms cannot make honestly means the fence stays.
|
||||
let outElem: IrType = VOID;
|
||||
const narrowing: (string | null)[] = [];
|
||||
if (method === "map") {
|
||||
outElem = callbackArrayElement(lowerer, call, fnRet);
|
||||
} else if (method === "filter") {
|
||||
const result = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
if (!result || result.kind !== "array") return null;
|
||||
outElem = result.elem;
|
||||
const typeText = filterTypeText(lowerer, call);
|
||||
for (const array of arrays) {
|
||||
if (typeEquals(array.elem, outElem)) {
|
||||
narrowing.push(null);
|
||||
continue;
|
||||
}
|
||||
const helper = checkedFilterNarrowing(lowerer, array.elem, outElem, typeText, locOf(call));
|
||||
if (helper === null) return null;
|
||||
narrowing.push(helper);
|
||||
}
|
||||
}
|
||||
const resultT: IrType = method === "forEach" ? VOID : arrayOf(outElem);
|
||||
for (const array of arrays) {
|
||||
const armFnT = funcOf(
|
||||
[arrayValueType(lowerer, array.elem), F64, array].slice(0, arity),
|
||||
@@ -700,6 +744,7 @@ export function lowerArrayUnionHofCall(
|
||||
arity,
|
||||
fnRet,
|
||||
outElem,
|
||||
narrowing,
|
||||
locOf(call),
|
||||
);
|
||||
return {
|
||||
@@ -716,24 +761,31 @@ export function lowerArrayUnionHofCall(
|
||||
|
||||
function arrayUnionHofHelper(
|
||||
lowerer: Lowerer,
|
||||
method: "map" | "forEach",
|
||||
method: "map" | "forEach" | "filter",
|
||||
receiverT: IrType & { kind: "union" },
|
||||
arrays: (IrType & { kind: "array" })[],
|
||||
callbackT: IrType & { kind: "func" },
|
||||
arity: number,
|
||||
fnRet: IrType,
|
||||
outElem: IrType,
|
||||
narrowing: (string | null)[],
|
||||
loc: SrcLoc,
|
||||
): string {
|
||||
const resultT: IrType = method === "map" ? arrayOf(outElem) : VOID;
|
||||
const key = `union:${method}:${receiverT.unionId}:${typeKey(callbackT)}:${typeKey(resultT)}`;
|
||||
const resultT: IrType = method === "forEach" ? VOID : arrayOf(outElem);
|
||||
// A filter's per-arm conversions are part of what the helper does, so two
|
||||
// sites that share receiver, callback and result but convert differently
|
||||
// keep their own helper. `map`/`forEach` never narrow, and leaving their
|
||||
// key untouched keeps the helpers a program already emitted byte-identical.
|
||||
const narrowingKey =
|
||||
method === "filter" ? `:${narrowing.map((helper) => helper ?? "").join(",")}` : "";
|
||||
const key = `union:${method}:${receiverT.unionId}:${typeKey(callbackT)}:${typeKey(resultT)}${narrowingKey}`;
|
||||
const existing = lowerer.arrHofHelpers.get(key);
|
||||
if (existing) return existing;
|
||||
const name = keyedHelperName(lowerer, "%arr.union", key);
|
||||
lowerer.arrHofHelpers.set(key, name);
|
||||
const receiver = varRef("a.0", receiverT, loc);
|
||||
const callback = varRef("f.0", callbackT, loc);
|
||||
const branch = (array: IrType & { kind: "array" }): IrStmt[] => {
|
||||
const branch = (array: IrType & { kind: "array" }, index: number): IrStmt[] => {
|
||||
const tag = lowerer.armTag(receiverT.unionId, array);
|
||||
const concrete: IrExpr = {
|
||||
kind: "unionNarrow",
|
||||
@@ -751,16 +803,38 @@ function arrayUnionHofHelper(
|
||||
if (!typeEquals(adapted.type, armFnT)) {
|
||||
throw new InternalCompilerError("lowerer bug: union-array callback stopped adapting");
|
||||
}
|
||||
const callee = arrayHofHelper(lowerer, method, array.elem, fnRet, arity, loc, outElem);
|
||||
const invoke: IrExpr = { kind: "call", callee, args: [concrete, adapted], type: resultT, loc };
|
||||
const callee = arrayHofHelper(
|
||||
lowerer,
|
||||
method,
|
||||
array.elem,
|
||||
fnRet,
|
||||
arity,
|
||||
loc,
|
||||
method === "filter" ? array.elem : outElem,
|
||||
);
|
||||
const invoke: IrExpr = {
|
||||
kind: "call",
|
||||
callee,
|
||||
args: [concrete, adapted],
|
||||
type: method === "filter" ? arrayOf(array.elem) : resultT,
|
||||
loc,
|
||||
};
|
||||
// Survivors of this arm's own array, converted to the checker's result
|
||||
// element. The helper throws for a kept element the predicate does not
|
||||
// describe, so a lying predicate never reinterprets a value.
|
||||
const armNarrow = narrowing[index] ?? null;
|
||||
const answer: IrExpr =
|
||||
armNarrow === null
|
||||
? invoke
|
||||
: { kind: "call", callee: armNarrow, args: [invoke], type: resultT, loc };
|
||||
return resultT.kind === "void"
|
||||
? [
|
||||
{ kind: "exprStmt", expr: invoke, loc },
|
||||
{ kind: "return", value: null, loc },
|
||||
]
|
||||
: [{ kind: "return", value: invoke, loc }];
|
||||
: [{ kind: "return", value: answer, loc }];
|
||||
};
|
||||
let body = branch(arrays[arrays.length - 1]!);
|
||||
let body = branch(arrays[arrays.length - 1]!, arrays.length - 1);
|
||||
for (let i = arrays.length - 2; i >= 0; i--) {
|
||||
const array = arrays[i]!;
|
||||
body = [
|
||||
@@ -775,7 +849,7 @@ function arrayUnionHofHelper(
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: branch(array),
|
||||
then: branch(array, i),
|
||||
else_: body,
|
||||
loc,
|
||||
},
|
||||
|
||||
@@ -38,6 +38,28 @@ import { arrayValueStore, primitiveElement } from "../array-values.js";
|
||||
import { classGeneratorIterable } from "../class-iterables.js";
|
||||
import { iteratorCanStep } from "../iterator-consumption.js";
|
||||
|
||||
/** The arm an EMPTY `[]` literal builds as: `arms`' first array arm, or
|
||||
* null when the literal is not empty (an element selects the arm) or no
|
||||
* arm is an array.
|
||||
*
|
||||
* An empty literal carries no element of its own — tsc infers `never[]`,
|
||||
* whose f64-element mapping is the uninhabited element's representation,
|
||||
* not one of the destination's arms — and every empty array is the same
|
||||
* value whichever array family stores it, so several array arms raise no
|
||||
* ambiguity. This is the ONE statement of that rule: a union slot, a
|
||||
* contextual union, and the `??` default all ask it. A lone array slot
|
||||
* states the same thing without asking. */
|
||||
export function emptyArrayLiteralArm(
|
||||
node: ts.Expression,
|
||||
arms: readonly IrType[],
|
||||
): (IrType & { kind: "array" }) | null {
|
||||
let value = node;
|
||||
while (ts.isParenthesizedExpression(value)) value = value.expression;
|
||||
if (!ts.isArrayLiteralExpression(value) || value.elements.nodes.length > 0) return null;
|
||||
const arm = arms.find((candidate) => candidate.kind === "array");
|
||||
return (arm as (IrType & { kind: "array" }) | undefined) ?? null;
|
||||
}
|
||||
|
||||
/** JavaScript: whether `expr` is or holds an existing checked-dynamic
|
||||
* object — a binding stored as a dyn value whose checker type is an
|
||||
* object, or an array or object literal with such an element. Building
|
||||
@@ -206,16 +228,12 @@ export function lowerArrayLiteral(
|
||||
mapped = arrayOf(unitOnlyUnion(lowerer.unions));
|
||||
// An EMPTY literal in a union slot (`const t: string[] | undefined =
|
||||
// []`, the `[]` default argument against `string[] | undefined`) has
|
||||
// no useful own type — tsc infers `never[]`, whose f64-element
|
||||
// mapping is a representation for the uninhabited, not the slot's
|
||||
// arm. When the contextual union has exactly ONE array arm there is
|
||||
// no ambiguity: build the literal as that arm and let the slot's
|
||||
// coercion wrap it. Two array arms would need tsc's own best-fit
|
||||
// choice — none arises in practice; the fence stands.
|
||||
if (ctxUnion && expr.elements.nodes.length === 0) {
|
||||
// no useful own type: it builds as the union's first array arm (the
|
||||
// emptyArrayLiteralArm rule) and the slot's coercion wraps it.
|
||||
if (ctxUnion) {
|
||||
const def = lowerer.unions.get(ctxUnion.unionId);
|
||||
const arrayArms = def?.arms.filter((a) => a.kind === "array") ?? [];
|
||||
if (arrayArms.length === 1) mapped = arrayArms[0]!;
|
||||
const arm = emptyArrayLiteralArm(expr, def?.arms ?? []);
|
||||
if (arm) mapped = arm;
|
||||
}
|
||||
// A NON-EMPTY literal whose own type has no static home (the JS dyn
|
||||
// fallback — a null/dyn mapping, or an inference that degraded to a
|
||||
|
||||
@@ -73,6 +73,7 @@ import { nsWritableTarget } from "../lower-namespaces.js";
|
||||
import { arrayValueStore, tryLowerBytesReadNumberEquality } from "../array-values.js";
|
||||
import { cjsExportMemberWriteGlobalOf, cjsInFileExportGlobalOf } from "../cjs-exports-object.js";
|
||||
import { lowerUnionFieldWrite } from "./union-fields.js";
|
||||
import { emptyArrayLiteralArm } from "./array-literals.js";
|
||||
import { lowerAbstractEquality } from "../abstract-equality.js";
|
||||
import { lowerInExpression } from "./relational.js";
|
||||
import { lowerDynTypeofTest, lowerErrorCodeTypeofTest } from "./typeof.js";
|
||||
@@ -431,6 +432,23 @@ export function lowerNullishPair(
|
||||
const right = lowerer.lowerExprExpecting(expr.right, rest[0]!);
|
||||
return { kind: "nullish", left, right, type: rest[0]!, loc };
|
||||
}
|
||||
// An EMPTY array-literal default (`''.match(re) ?? []`) has no element of
|
||||
// its own, so it builds as the left's array arm (the emptyArrayLiteralArm
|
||||
// rule the union slot and the contextual union share) and the whole
|
||||
// expression answers the left's array type instead of the checker's
|
||||
// `never[]`-flavored second arm. A JavaScript `[]` is `any[]` and keeps
|
||||
// the dynamic path.
|
||||
if (
|
||||
rest.length === 1 &&
|
||||
rest[0]!.kind === "array" &&
|
||||
!isJsSourceFile(ts.getSourceFileOfNode(expr.right))
|
||||
) {
|
||||
const arm = emptyArrayLiteralArm(expr.right, rest);
|
||||
if (arm) {
|
||||
const right = lowerer.lowerExprExpecting(expr.right, arm);
|
||||
if (typeEquals(right.type, arm)) return { kind: "nullish", left, right, type: arm, loc };
|
||||
}
|
||||
}
|
||||
let type = lowerer.irTypeOf(expr);
|
||||
// A runtime-optional read can reach the default even when declarations
|
||||
// call the left side present. Preserve a differently typed primitive
|
||||
|
||||
@@ -1359,20 +1359,38 @@ export function lowerObjectLiteral(
|
||||
) {
|
||||
const own = lowerer.shapes.get(type.shapeId)!;
|
||||
const target = lowerer.shapes.get(expected.shapeId)!;
|
||||
if (
|
||||
!own.tuple &&
|
||||
// The field set to compare is the LITERAL's: its checker type. The
|
||||
// contextual type a few lines up can be wider than what the literal
|
||||
// writes (an unannotated method checked against a generic slot's
|
||||
// signature: `{ content, details }` under a contextual
|
||||
// `AgentToolResult<unknown>` whose optional members the literal never
|
||||
// mentions), and the destination still names the same fields as the
|
||||
// literal, so the build can construct them directly. Only when the
|
||||
// contextual build has no conversion into the destination at all: a
|
||||
// value that converts today keeps its exact construction.
|
||||
const ownType = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
||||
const ownChecked =
|
||||
ownType?.kind === "record" &&
|
||||
ownType.shapeId !== type.shapeId &&
|
||||
lowerer.widthLiftPlan(type, expected) === null
|
||||
? lowerer.shapes.get(ownType.shapeId)
|
||||
: undefined;
|
||||
const fits = (from: IrRecordShape): boolean =>
|
||||
!from.tuple &&
|
||||
!target.tuple &&
|
||||
!own.indexValue &&
|
||||
!from.indexValue &&
|
||||
!target.indexValue &&
|
||||
!shapeHasAccessorSlots(own) &&
|
||||
!shapeHasAccessorSlots(from) &&
|
||||
!shapeHasAccessorSlots(target) &&
|
||||
target.fields.every(
|
||||
(field) =>
|
||||
own.fields.some((candidate) => candidate.name === field.name) ||
|
||||
from.fields.some((candidate) => candidate.name === field.name) ||
|
||||
(field.type.kind === "union" && lowerer.armTag(field.type.unionId, UNDEFINED_T) >= 0),
|
||||
) &&
|
||||
own.fields.every((field) => target.fields.some((candidate) => candidate.name === field.name))
|
||||
) {
|
||||
from.fields.every((field) =>
|
||||
target.fields.some((candidate) => candidate.name === field.name),
|
||||
);
|
||||
if (fits(own) || (ownChecked !== undefined && fits(ownChecked))) {
|
||||
type = { kind: "record", shapeId: expected.shapeId };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +122,19 @@ function coerceSlot(lowerer: Lowerer, arg: IrExpr, want: IrType, what: string):
|
||||
);
|
||||
}
|
||||
if (arg.type.kind === "union" && want.kind !== "union" && !containsDynOrUnit(want)) {
|
||||
// A stored union at a slot whose signature names one concrete type: the
|
||||
// checker approved the call, so the SITE's type is that slot type and the
|
||||
// stored union is the declaration's wider one — an absence arm the
|
||||
// compiler itself carries (a checked-dynamic JSON stringify's
|
||||
// `string | undefined`), a stale capture, a widened inferred return.
|
||||
// That is coerceToExpected's checked ARM rule, not a refusal to lay the
|
||||
// union out: the proven arm's payload comes out, every other arm throws
|
||||
// the existing catchable TypeError (divergence 38 — sound narrowing
|
||||
// never reaches them). A union with no such arm keeps the refusal.
|
||||
if (lowerer.armTag(arg.type.unionId, want) >= 0) {
|
||||
const narrowed = lowerer.coerceToExpected(arg, want);
|
||||
if (typeEquals(narrowed.type, want)) return narrowed;
|
||||
}
|
||||
lowerer.pushDiag(unionMismatchDiag(lowerer.fmt(want), lowerer.fmt(arg.type), arg.loc));
|
||||
throw new PoisonError();
|
||||
}
|
||||
|
||||
@@ -782,6 +782,7 @@ import { lowerRecordFieldCall, lowerObjectMethodCall } from "./lower-calls.js";
|
||||
import { fenceCrossBlockNsRef, nsPathPrefix } from "./lower-namespaces.js";
|
||||
import { isSafeToRepeat } from "./expressions/evaluation-safety.js";
|
||||
import { partialAssertionFields } from "./partial-assertions.js";
|
||||
import { emptyArrayLiteralArm } from "./expressions/array-literals.js";
|
||||
import { lowerTernary } from "./lower-exprs.js";
|
||||
import {
|
||||
maybePromiseReturnStmt,
|
||||
@@ -14069,6 +14070,10 @@ export class Lowerer {
|
||||
);
|
||||
}
|
||||
}
|
||||
// An EMPTY literal carries no element to select an arm by: it
|
||||
// builds as the union's first array arm (the emptyArrayLiteralArm
|
||||
// rule the contextual union and the `?? []` default share).
|
||||
if (!arm) arm = emptyArrayLiteralArm(x, arms) ?? undefined;
|
||||
if (arm) {
|
||||
const built = this.lowerArrayLiteral(
|
||||
x,
|
||||
@@ -14229,9 +14234,49 @@ export class Lowerer {
|
||||
if (this.returnsLiteralIntoDyn(node, expected)) return this.lowerExprExpecting(node, expected);
|
||||
if (this.returnsFreshCollection(node) && expected.kind !== "void")
|
||||
return this.lowerExprExpecting(node, expected);
|
||||
// A fresh object literal into a record result: the same
|
||||
// destination-directed construction. An unannotated method inside a
|
||||
// contextually typed literal is checked against the CONTEXTUAL
|
||||
// signature's result while its own return type stays the literal's
|
||||
// inferred (narrower) record — `ToolDefinition<Schema, Details>`
|
||||
// methods returning `{ content, details }`. Building at the
|
||||
// contextual type would then coerce that wider record into the
|
||||
// declared result and refuse; the result slot is the layout.
|
||||
if (this.returnsFreshRecord(node, expected)) return this.lowerExprExpecting(node, expected);
|
||||
return this.asyncReturnValueOf(node, this.lowerExpr(node), expected);
|
||||
}
|
||||
|
||||
/** A fresh object literal returned into a RECORD result whose ordinary
|
||||
* construction cannot reach that result: not a thenable, so it is built
|
||||
* at the result type like a synchronous return. An unannotated method
|
||||
* inside a contextually typed literal is checked against the CONTEXTUAL
|
||||
* signature's result while its own return type stays the literal's
|
||||
* inferred (narrower) record — `ToolDefinition<Schema, Details>` methods
|
||||
* returning `{ content, details }`. Building at the contextual type
|
||||
* would then coerce that wider record into the declared result and
|
||||
* refuse; the result slot is the layout. A literal that declares `then`
|
||||
* is awaited instead, and a literal whose contextual build does convert
|
||||
* keeps the ordinary lowering, so no convertible value changes. */
|
||||
private returnsFreshRecord(node: ts.Expression, expected: IrType): boolean {
|
||||
if (expected.kind !== "record") return false;
|
||||
let e = node;
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
if (!ts.isObjectLiteralExpression(e)) return false;
|
||||
if (this.checker.getPropertyOfType(this.typeOf(e), "then") !== undefined) return false;
|
||||
// The record the ordinary path builds: the literal's context, with an
|
||||
// async return's `T | PromiseLike<T>` stripped to the awaited record
|
||||
// (object-literals.ts does the same before mapping).
|
||||
let context = this.checker.getContextualType(e) ?? this.typeOf(e);
|
||||
if (
|
||||
context.isUnionType() &&
|
||||
ts.constituentTypes(context).some((t) => ts.symbolNameOf(t.getSymbol()) === "PromiseLike")
|
||||
)
|
||||
context = this.checker.getAwaitedType(context) ?? context;
|
||||
const built = this.mapTypeOf(context);
|
||||
if (!built || built.kind !== "record") return false;
|
||||
return this.widthLiftPlan(built, expected) === null;
|
||||
}
|
||||
|
||||
/** 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
|
||||
@@ -14406,7 +14451,8 @@ export class Lowerer {
|
||||
if (
|
||||
this.adoptsReturnedPromise() &&
|
||||
!this.returnsLiteralIntoDyn(node, expected) &&
|
||||
!this.returnsFreshCollection(node)
|
||||
!this.returnsFreshCollection(node) &&
|
||||
!this.returnsFreshRecord(node, expected)
|
||||
) {
|
||||
// The async return lowers its operand untyped first (a promise
|
||||
// adopts before its payload converts); a maybe-promise union needs
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5798-record-field-dyn-into-union.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5799-async-return-record-literal-slot.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8530-union-array-filter.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/empty-array-union-slot.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/lib-slot-union-arm-extraction.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/nullish-empty-array-default.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// `.filter` on a union of arrays (`A[] | B[]`): the predicate runs over
|
||||
// `A | B`, and the checker's result element is the arm the predicate names.
|
||||
// The runtime value is one concrete arm's array, so the lowering must
|
||||
// dispatch on that arm (unionIsTag), filter the arm's own array, and convert
|
||||
// the survivors to the named element through the checked narrowing — never
|
||||
// hand a union receiver to a static array intrinsic.
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile, deserializeModule, validateModule } from "../src/index.js";
|
||||
|
||||
const PRELUDE = [
|
||||
"interface TextContent { type: 'text'; text: string }",
|
||||
"interface ImageContent { type: 'image'; data: string }",
|
||||
"interface ThinkingContent { type: 'thinking'; thinking: string }",
|
||||
"type MessageContent =",
|
||||
" | TextContent[]",
|
||||
" | (ImageContent | TextContent)[]",
|
||||
" | (ThinkingContent | TextContent)[];",
|
||||
];
|
||||
|
||||
type IrModule = {
|
||||
entry: string;
|
||||
functions: { name: string; body: unknown }[];
|
||||
};
|
||||
|
||||
async function compileToIr(
|
||||
body: string[],
|
||||
): Promise<{ ok: boolean; module: IrModule | null; diagnostics: string }> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-union-array-filter-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts");
|
||||
const outPath = join(dir, "main.ir.json");
|
||||
await writeFile(entry, [...PRELUDE, ...body].join("\n"));
|
||||
const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" });
|
||||
if (!result.ok)
|
||||
return {
|
||||
ok: false,
|
||||
module: null,
|
||||
diagnostics: result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"),
|
||||
};
|
||||
const module = deserializeModule(await readFile(outPath, "utf8")) as unknown as IrModule;
|
||||
expect(validateModule(module as never)).toEqual([]);
|
||||
return { ok: true, module, diagnostics: "" };
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function named(module: IrModule, part: string): string {
|
||||
return JSON.stringify(module.functions.filter((fn) => fn.name.includes(part)));
|
||||
}
|
||||
|
||||
test("filter on a union of arrays dispatches on the arm", async () => {
|
||||
const compiled = await compileToIr([
|
||||
"function texts(content: MessageContent): string {",
|
||||
" return content",
|
||||
" .filter((block): block is TextContent => block.type === 'text')",
|
||||
" .map((block) => block.text)",
|
||||
" .join(' ');",
|
||||
"}",
|
||||
"const blocks: MessageContent = [{ type: 'text', text: 'a' }];",
|
||||
"console.log(texts(blocks));",
|
||||
]);
|
||||
expect(compiled.diagnostics).toBe("");
|
||||
expect(compiled.ok).toBe(true);
|
||||
const module = compiled.module!;
|
||||
// The tag dispatch that reads the runtime arm…
|
||||
const union = named(module, "%arr.union");
|
||||
expect(union).toContain("unionIsTag");
|
||||
// …and the checked narrowing each arm's survivors pass through, so a
|
||||
// predicate that kept an element it does not describe throws.
|
||||
expect(named(module, "%arr.narrow")).toContain("%narrow.checked");
|
||||
});
|
||||
|
||||
test("filter on a union of arrays keeps the fence without a narrowing", async () => {
|
||||
// A predicate that names no narrower element answers the union-wide
|
||||
// element; converting an arm's array to that element is a widening this
|
||||
// lowering does not build, so the call stays a refusal instead of
|
||||
// compiling to a different array layout.
|
||||
const compiled = await compileToIr([
|
||||
"function counts(content: MessageContent): number {",
|
||||
" return content.filter((block): boolean => block.type !== 'image').length;",
|
||||
"}",
|
||||
"console.log(counts([]));",
|
||||
]);
|
||||
expect(compiled.ok).toBe(false);
|
||||
expect(compiled.diagnostics).toContain("SC2020");
|
||||
});
|
||||
@@ -139,3 +139,25 @@ export {};
|
||||
if (!result.ok) return;
|
||||
expect(helpers(result.ir, "%union.arr").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("an empty array literal takes the union's first array arm instead of trapping", async () => {
|
||||
const result = await lower(`
|
||||
type TextContent = { type: "text"; text: string };
|
||||
type ImageContent = { type: "image"; url: string };
|
||||
interface Message {
|
||||
content: TextContent[] | ImageContent[];
|
||||
}
|
||||
const message: Message = { content: [] };
|
||||
function lists(): string[] | number[] {
|
||||
return [];
|
||||
}
|
||||
console.log(JSON.stringify(message.content), JSON.stringify(lists()));
|
||||
export {};
|
||||
`);
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
// An empty literal carries no element of its own, so it is built as the
|
||||
// union's first array arm: no stranded-source trap and no element rebuild.
|
||||
expect(helpers(result.ir, "%union.strand")).toEqual([]);
|
||||
expect(helpers(result.ir, "%union.arr")).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
// A record field the checker types `any`/`unknown` flowing into a slot whose
|
||||
// declared type is a UNION: the checked extraction (dynCheck) is the
|
||||
// conversion, exactly as for a dyn value in a top-level union slot. Before
|
||||
// this, such a field only lifted into a NON-union destination, so a tool
|
||||
// result's `details: unknown` could not reach `Details | undefined`, a
|
||||
// `structuredContent: unknown` could not reach a JSON-value union, and a
|
||||
// spread's dynamic key could not reach an index-signature member.
|
||||
interface Details {
|
||||
limit: number;
|
||||
flag?: boolean;
|
||||
}
|
||||
|
||||
interface Wide {
|
||||
label: string;
|
||||
details: any;
|
||||
}
|
||||
interface Narrow {
|
||||
label: string;
|
||||
details: Details | undefined;
|
||||
}
|
||||
|
||||
function narrow(w: Wide): Narrow {
|
||||
return w;
|
||||
}
|
||||
|
||||
type Json = string | number | boolean | null | { [key: string]: Json } | Json[];
|
||||
interface Payload {
|
||||
text: string;
|
||||
structuredContent: any;
|
||||
}
|
||||
interface JsonPayload {
|
||||
text: string;
|
||||
structuredContent: Json | undefined;
|
||||
}
|
||||
|
||||
function widen(p: Payload): JsonPayload {
|
||||
return p;
|
||||
}
|
||||
|
||||
// A spread whose source carries the dynamic field: the same field lift.
|
||||
interface Built {
|
||||
content: string;
|
||||
sections: any;
|
||||
}
|
||||
interface Message {
|
||||
role: string;
|
||||
content: string;
|
||||
sections: { [key: string]: string | null } | undefined;
|
||||
timestamp: number;
|
||||
}
|
||||
function fromState(flag: boolean): Built {
|
||||
return { content: flag ? "on" : "off", sections: { preamble: "p" } };
|
||||
}
|
||||
function message(flag: boolean): Message {
|
||||
return { role: "system", ...fromState(flag), timestamp: 0 };
|
||||
}
|
||||
|
||||
// A callback whose result parameter narrows the dynamic field back to a
|
||||
// record type adapts into the slot's dynamic-details signature.
|
||||
interface SlotResult {
|
||||
content: { type: string; text: string }[];
|
||||
details: any;
|
||||
}
|
||||
interface ToolResult {
|
||||
content: { type: string; text: string }[];
|
||||
details: { limit: number } | undefined;
|
||||
}
|
||||
type Renderer = (result: SlotResult, expanded: boolean) => string;
|
||||
function format(result: ToolResult, expanded: boolean): string {
|
||||
return `${result.content.length}:${result.details?.limit ?? "none"}:${expanded}`;
|
||||
}
|
||||
const render: Renderer = (result, expanded) => format(result, expanded);
|
||||
|
||||
const wide: Wide = { label: "a", details: { limit: 3 } };
|
||||
const n = narrow(wide);
|
||||
console.log(n.label, n.details === undefined ? "none" : n.details.limit);
|
||||
|
||||
const p = widen({ text: "t", structuredContent: { nested: [1, true, null] } });
|
||||
console.log(p.text, JSON.stringify(p.structuredContent ?? null));
|
||||
|
||||
const m = message(true);
|
||||
console.log(m.role, m.content, m.timestamp, m.sections?.["preamble"] ?? "-");
|
||||
|
||||
console.log(render({ content: [{ type: "text", text: "abc" }], details: { limit: 7 } }, true));
|
||||
console.log(render({ content: [], details: undefined }, false));
|
||||
@@ -0,0 +1,76 @@
|
||||
// A tool definition whose `execute` is an unannotated async method inside a
|
||||
// contextually typed object literal. tsc checks the method against the
|
||||
// contextual signature's result (`AgentToolResult<Loaded>`) but INFERS its
|
||||
// own return type from the returned literal (`{ content: { type, text }[];
|
||||
// details: { loaded } }`). The fresh literal must construct at the declared
|
||||
// result slot, not at the contextual type it was checked against: the
|
||||
// contextual type's `content` is the wider content-part union array, which
|
||||
// no copy narrows back into the inferred element record.
|
||||
interface TextContent {
|
||||
type: string;
|
||||
text: string;
|
||||
textSignature?: string;
|
||||
}
|
||||
interface ImageContent {
|
||||
type: string;
|
||||
data: string;
|
||||
mimeType: string;
|
||||
}
|
||||
interface Candidate {
|
||||
name: string;
|
||||
score: number;
|
||||
}
|
||||
interface Definition<D> {
|
||||
name: string;
|
||||
search(query: string): Promise<D>;
|
||||
execute(id: string, query: string): Promise<Result<D>>;
|
||||
}
|
||||
interface Result<D> {
|
||||
content: (TextContent | ImageContent)[];
|
||||
details: D;
|
||||
structuredContent?: unknown;
|
||||
isError?: boolean;
|
||||
}
|
||||
interface Loaded {
|
||||
loaded: string[];
|
||||
}
|
||||
|
||||
function rank(query: string, names: string[]): Candidate[] {
|
||||
return names.filter((name) => name.includes(query)).map((name) => ({ name, score: name.length }));
|
||||
}
|
||||
|
||||
function makeDefinition(names: string[]): Definition<Loaded> {
|
||||
return {
|
||||
name: "search",
|
||||
async search(query: string) {
|
||||
return { loaded: rank(query, names).map((candidate) => candidate.name) };
|
||||
},
|
||||
async execute(_id: string, query: string) {
|
||||
return {
|
||||
content: [{ type: "text", text: rank(query, names).map((c) => c.name).join(",") }],
|
||||
details: { loaded: rank(query, names).map((c) => c.name) },
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// The same shape through a record of definitions, each with its own details.
|
||||
type AnyDefinition = Definition<any>;
|
||||
function makeNamed(names: string[]): Record<"search", AnyDefinition> {
|
||||
return { search: makeDefinition(names) };
|
||||
}
|
||||
|
||||
const definition = makeDefinition(["find-file", "grep-tree", "read-file"]);
|
||||
definition
|
||||
.search("file")
|
||||
.then((loaded) => {
|
||||
console.log("search:", loaded.loaded.join(","));
|
||||
return definition.execute("call-1", "file");
|
||||
})
|
||||
.then((result) => {
|
||||
console.log("execute:", result.content.length, result.content[0]!.type, result.details.loaded.join(","));
|
||||
return makeNamed(["read-file"]).search.execute("call-2", "file");
|
||||
})
|
||||
.then((second) => {
|
||||
console.log("second:", second.content.length, second.details.loaded.join(","));
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
// `.filter` on a UNION OF ARRAYS (`A[] | B[]`, TS 5.2's callable-union
|
||||
// rule): the predicate runs over `A | B`, and the checker's result element
|
||||
// is the one the predicate names — not any arm's own element. The runtime
|
||||
// value is one concrete arm's array, so the filter dispatches on that arm,
|
||||
// keeps the arm's survivors in order, and converts each survivor to the
|
||||
// named element.
|
||||
interface TextContent {
|
||||
type: "text";
|
||||
text: string;
|
||||
}
|
||||
interface ImageContent {
|
||||
type: "image";
|
||||
data: string;
|
||||
}
|
||||
interface ThinkingContent {
|
||||
type: "thinking";
|
||||
thinking: string;
|
||||
}
|
||||
interface ToolCall {
|
||||
type: "tool";
|
||||
name: string;
|
||||
}
|
||||
type MessageContent =
|
||||
| TextContent[]
|
||||
| (ImageContent | TextContent)[]
|
||||
| (ThinkingContent | ToolCall | TextContent)[];
|
||||
|
||||
function textOf(content: MessageContent): string {
|
||||
return content
|
||||
.filter((block): block is TextContent => block.type === "text")
|
||||
.map((block) => block.text)
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
const contents: MessageContent[] = [
|
||||
[{ type: "text", text: "a" }, { type: "text", text: "b" }],
|
||||
[
|
||||
{ type: "image", data: "AA" },
|
||||
{ type: "text", text: "c" },
|
||||
],
|
||||
[
|
||||
{ type: "thinking", thinking: "t" },
|
||||
{ type: "tool", name: "n" },
|
||||
{ type: "text", text: "d" },
|
||||
],
|
||||
];
|
||||
for (const content of contents) console.log("text", textOf(content));
|
||||
|
||||
// A predicate written as a function EXPRESSION rather than an arrow: the
|
||||
// same receiver, the same per-arm dispatch.
|
||||
function expressions(content: MessageContent): string {
|
||||
return content
|
||||
.filter(function (block): block is TextContent {
|
||||
return block.type === "text";
|
||||
})
|
||||
.map((block) => block.text)
|
||||
.join(" ");
|
||||
}
|
||||
console.log("expr", expressions(contents[1]!), expressions(contents[2]!));
|
||||
|
||||
// The predicate sees the union-wide element, so an arm-specific test is
|
||||
// legal; the survivors still convert per arm.
|
||||
function longTexts(content: MessageContent): string[] {
|
||||
return content
|
||||
.filter((block): block is TextContent => block.type === "text" && block.text !== "c")
|
||||
.map((block) => block.text);
|
||||
}
|
||||
console.log("long", longTexts(contents[1]!).join("+"), longTexts(contents[2]!).join("+"));
|
||||
|
||||
// Node's `filter` reads the length once and visits elements in order; the
|
||||
// arm dispatch must not change either, and the result is a fresh array.
|
||||
let seen = 0;
|
||||
function counted(content: MessageContent): number {
|
||||
const kept = content.filter((block): block is TextContent => {
|
||||
seen++;
|
||||
return block.type === "text";
|
||||
});
|
||||
return kept.length;
|
||||
}
|
||||
console.log("counted", counted(contents[2]!), "seen", seen);
|
||||
// The empty array is spelled with its arm's type: a bare `[]` literal
|
||||
// assigned to this three-arm array union is a separate, unrelated gap (the
|
||||
// literal's `number[]` element has no arm conversion), so it would obscure
|
||||
// what this program pins.
|
||||
const empty: MessageContent = [] as TextContent[];
|
||||
console.log("empty", textOf(empty), counted(empty), "seen", seen);
|
||||
@@ -0,0 +1,106 @@
|
||||
// An EMPTY array literal in a union of array slots (`content: TextContent[] |
|
||||
// ImageContent[]`, `string[] | number[]`). tsc types the literal `never[]`, an
|
||||
// element with no representation of its own: the literal used to lower as
|
||||
// never's f64-element array (`number[]`) and the slot's retag threw
|
||||
// "a 'number[]' value is not representable in the target union". An empty
|
||||
// array is the same value whichever array family stores it, so the literal is
|
||||
// built as the union's first array arm — the rule a lone array slot states —
|
||||
// and every shape below reaches Node's answer.
|
||||
type TextContent = { type: "text"; text: string };
|
||||
type ImageContent = { type: "image"; url: string };
|
||||
interface Message {
|
||||
role: string;
|
||||
content: TextContent[] | ImageContent[];
|
||||
}
|
||||
|
||||
// A nested record field: the reported shape.
|
||||
const empty: Message = { role: "user", content: [] };
|
||||
console.log(JSON.stringify(empty));
|
||||
|
||||
// A declaration, its initialization and a later reassignment.
|
||||
let wide: string[] | number[] = [];
|
||||
console.log(JSON.stringify(wide));
|
||||
wide = [1, 2];
|
||||
console.log(JSON.stringify(wide));
|
||||
wide = [];
|
||||
console.log(JSON.stringify(wide));
|
||||
|
||||
// A declared return, a ternary arm, and an async promise's payload.
|
||||
function none(): string[] | number[] {
|
||||
return [];
|
||||
}
|
||||
console.log(JSON.stringify(none()), none() !== none());
|
||||
|
||||
function either(take: boolean): string[] | number[] {
|
||||
return take ? [] : [1, 2];
|
||||
}
|
||||
console.log(JSON.stringify(either(true)), JSON.stringify(either(false)));
|
||||
|
||||
function bothWays(): string[] | number[] {
|
||||
return false ? [] : [];
|
||||
}
|
||||
console.log(JSON.stringify(bothWays()));
|
||||
|
||||
async function settled(): Promise<string[] | number[]> {
|
||||
return [];
|
||||
}
|
||||
void settled().then((value) => console.log("settled", JSON.stringify(value)));
|
||||
|
||||
// An argument slot, and a default parameter that is itself the slot.
|
||||
function report(label: string, value: string[] | number[]): void {
|
||||
console.log(label, JSON.stringify(value));
|
||||
}
|
||||
report("argument", []);
|
||||
report("argument-filled", ["x"]);
|
||||
|
||||
function withDefault(value: string[] | number[] = []): void {
|
||||
console.log("default", JSON.stringify(value));
|
||||
}
|
||||
withDefault();
|
||||
withDefault([3]);
|
||||
|
||||
// Deeper nesting, a class field, and a discriminated record's field.
|
||||
interface Nested {
|
||||
inner: { list: readonly string[] | readonly number[]; tag: string };
|
||||
}
|
||||
const nested: Nested = { inner: { list: [], tag: "t" } };
|
||||
console.log(JSON.stringify(nested));
|
||||
|
||||
class Store {
|
||||
lists: string[] | number[] = [];
|
||||
}
|
||||
const store = new Store();
|
||||
console.log("class", JSON.stringify(store.lists), store.lists !== undefined);
|
||||
|
||||
// An optional arm (`| undefined`) keeps the empty literal on its array arm.
|
||||
const maybe: string[] | number[] | undefined = [];
|
||||
console.log("maybe", maybe !== undefined, JSON.stringify(maybe));
|
||||
|
||||
// The empty literal as an element of an array, and through collections whose
|
||||
// element slot is the union.
|
||||
const rows: (string[] | number[])[] = [[], [1, 2], []];
|
||||
console.log("rows", JSON.stringify(rows));
|
||||
const grouped = new Set<string[] | number[]>();
|
||||
grouped.add([]);
|
||||
grouped.add(["s"]);
|
||||
console.log("set", JSON.stringify([...grouped]));
|
||||
const byKey = new Map<string, string[] | number[]>();
|
||||
byKey.set("a", []);
|
||||
byKey.set("b", [7]);
|
||||
console.log("map", JSON.stringify([...byKey.entries()]));
|
||||
|
||||
// A callback argument, and the assertion form of the same value.
|
||||
function apply(fn: (value: string[] | number[]) => string): string {
|
||||
return fn([]);
|
||||
}
|
||||
console.log("callback", apply((value) => JSON.stringify(value)));
|
||||
const asserted = { role: "tool", content: [] } as Message;
|
||||
console.log("asserted", JSON.stringify(asserted));
|
||||
|
||||
// Every evaluation builds a fresh empty array, as Node does.
|
||||
function fresh(): string[] | number[] {
|
||||
return [];
|
||||
}
|
||||
const first = fresh();
|
||||
const second = fresh();
|
||||
console.log("fresh", first !== second, JSON.stringify(first), JSON.stringify(second));
|
||||
@@ -0,0 +1,33 @@
|
||||
// A checked-dynamic JSON stringify — a record or array carrying an `unknown`
|
||||
// field — answers `string | undefined` in the IR (the runtime walker can
|
||||
// return undefined for a dyn root) while the checker types the call `string`.
|
||||
// Reading a string member of that result is the checked ARM extraction at the
|
||||
// stdlib slot: the proven arm's payload comes out, and the absent arm would
|
||||
// throw the catchable TypeError instead of the call being refused.
|
||||
|
||||
interface Tool {
|
||||
name: string;
|
||||
parameters: unknown;
|
||||
}
|
||||
|
||||
function sizes(tools: Tool[], extra: { note: string; value: unknown }): string {
|
||||
const text = JSON.stringify(tools);
|
||||
let chars = 0;
|
||||
chars += text.length;
|
||||
if (tools.length > 0) chars += JSON.stringify(tools[0]).length;
|
||||
chars += JSON.stringify(extra).slice(0, 24).length;
|
||||
chars += JSON.stringify({ tools }).toUpperCase().slice(0, 12).length;
|
||||
chars += JSON.stringify(tools).indexOf('"name"');
|
||||
chars += `raw:${JSON.stringify(extra)}`.length > 0 ? 1 : 0;
|
||||
return `${chars} ${text.length} ${JSON.stringify({ tools }).slice(0, 12)}`;
|
||||
}
|
||||
|
||||
console.log(sizes([{ name: "alpha", parameters: { a: 1 } }], { note: "n", value: null }));
|
||||
console.log(sizes([{ name: "beta", parameters: undefined }], { note: "empty", value: [1, 2] }));
|
||||
console.log(sizes([], { note: "none", value: { nested: { deep: true } } }));
|
||||
console.log(sizes([{ name: "gamma", parameters: "s" }], { note: "deep", value: { a: [1, { b: 2 }] } }));
|
||||
|
||||
// The same string result feeding a comparison and a concatenation.
|
||||
const one = JSON.stringify({ name: "x", parameters: 1 });
|
||||
const two = JSON.stringify({ parameters: 1, name: "x" });
|
||||
console.log(one === two, one < two, ("p" + one).length);
|
||||
@@ -0,0 +1,40 @@
|
||||
// `''.match(re) ?? []`: the empty array-literal default of a `??` adopts the
|
||||
// left's present array arm. tsc gives the literal the uninhabited `never[]`
|
||||
// element, which the compiler maps through never's f64 value representation
|
||||
// (`number[]`), so the whole expression used to be `RegExpMatchArray |
|
||||
// number[]` — a result no single array slot can receive.
|
||||
|
||||
// A union return (`"all" | readonly string[]`) whose array arm is the
|
||||
// `?? []` result.
|
||||
function reach(code: unknown): "all" | readonly string[] {
|
||||
if (typeof code !== "string") return [];
|
||||
return /\bALL\b/.test(code) ? "all" : (code.match(/\w+/g) ?? []);
|
||||
}
|
||||
console.log(JSON.stringify(reach(7)));
|
||||
console.log(JSON.stringify(reach("ALL")));
|
||||
console.log(JSON.stringify(reach("a.b c")));
|
||||
console.log(JSON.stringify(reach("")));
|
||||
|
||||
// The default's element type is the left's: string element reads and a
|
||||
// map callback over the value compile as strings, and an absent match
|
||||
// answers the empty array Node does.
|
||||
const names = "a-b c".match(/[a-z]+/g) ?? [];
|
||||
console.log(names.map((name) => name.toUpperCase()).join("+"));
|
||||
const none = "123".match(/[a-z]+/g) ?? [];
|
||||
console.log(none.length, JSON.stringify(none), names.length);
|
||||
|
||||
// A declared optional array keeps its own type through the same default,
|
||||
// and the fresh empty array is a value of its own (mutation, identity).
|
||||
function fill(source: number[] | undefined, with_: number): number[] {
|
||||
const list = source ?? [];
|
||||
list.push(with_);
|
||||
return list;
|
||||
}
|
||||
console.log(JSON.stringify(fill(undefined, 1)), JSON.stringify(fill([7, 8], 9)));
|
||||
const first = fill(undefined, 2);
|
||||
const second = fill(undefined, 3);
|
||||
console.log(first !== second, JSON.stringify(first), JSON.stringify(second));
|
||||
|
||||
// A parenthesized default and one inside a longer expression.
|
||||
const longest = ("abbbc".match(/b+/g) ?? []).length;
|
||||
console.log(longest, `${JSON.stringify("xyz".match(/q/g) ?? [])}!`);
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 1800, "san": 1900 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 1800, "san": 1900 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 1900, "san": 2200 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2700, "san": 3400 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 1517, "san": 2343 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 922, "san": 1502 }
|
||||
Reference in New Issue
Block a user