mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-02 07:43:42 +08:00
Build spreads of union sources one arm at a time
`{ ...u, k: v }` where `u` is a union of records (`{ ...msg, content }` in
a message-mapping switch, `{ ...command, id } as Cmd`, `{ ...base, entries }
as A | B`) is typed by TypeScript as one spread per arm of `u`, but the
frontend mapped the literal to the union and refused with SC2003 ("a spread
merge over union-typed sources"). The only union-source spreads that lowered
were shared-field overrides and the single-record-arm present test.
lowerDistributedUnionSpread builds the literal the way the checker types it.
The source evaluates once into a hidden local; a tag dispatch builds the
running arm's literal with that arm as the spread source, at the destination
arm the discriminant's last writer picks, else the source arm itself, else
the one arm whose keys are exactly the keys the literal writes. Every key a
contributor may write must be a field of the destination, so a genuine merge
(`{ ...http, ...patch }` where the patch can carry a stdio-only key) keeps
the refusal instead of silently dropping keys. A stored source the checker
narrowed converts through the checked re-tag first, so an excluded arm throws
the catchable TypeError.
Cost: the function runs only where lowerObjectLiteral would have refused;
the new `exact` parameter defaults to false and the new
`spreadArmOverrides` map is empty outside the dispatch, so every existing
literal lowers to the same IR.
`fix/records-literals` changes the same field-desugar capture (it adds a
`checkedSpreadSource` block around it). This commit deliberately keeps the
per-arm source as the expression `(lowerer.spreadArmOverrides.get(srcNode)
?? lowerer.lowerExpr(srcNode))` inside that capture, so the merge keeps
records-literals' `let srcLowered` plus its checkedSource block and applies
the override inside the same expression, as the design specifies.
Pi sites: execution.ts:431, agent-session.ts:3795, runner.ts:1048,
model-resolver.ts:216, sdk.ts:324, session-manager.ts:538, rpc-client.ts:577.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
2a1f7ac212
commit
12a3f8a225
@@ -43,6 +43,7 @@ import {
|
||||
symbolFieldDisplayName,
|
||||
} from "../symbol-fields.js";
|
||||
import { planUnionRetag } from "../union-retag.js";
|
||||
import { literalValues } from "../../literal-values.js";
|
||||
import { jsHoldsDynObject, runtimeOptionalStorageOperand } from "../lower-exprs.js";
|
||||
import { builtinGlobalValue } from "./property-access.js";
|
||||
import { holdsEvolvingArray } from "../lower-stmts.js";
|
||||
@@ -850,6 +851,10 @@ export function lowerObjectLiteral(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.ObjectLiteralExpression,
|
||||
expected?: IrType & { kind: "record" },
|
||||
/** Build exactly at `expected` (one arm of a distributed union spread,
|
||||
* lowerDistributedUnionSpread): the checker types the literal as the
|
||||
* whole union, so its contextual type cannot choose the layout. */
|
||||
exact = false,
|
||||
): IrExpr {
|
||||
// A spread of an untyped (`any`) source into a typed slot: TypeScript
|
||||
// types such a literal `any`, and the copy holds whatever own properties
|
||||
@@ -968,7 +973,7 @@ export function lowerObjectLiteral(
|
||||
) {
|
||||
return lowerDynObjectLiteral(lowerer, expr);
|
||||
}
|
||||
const unionClone = lowerUnionRecordClone(lowerer, expr);
|
||||
const unionClone = exact ? null : lowerUnionRecordClone(lowerer, expr);
|
||||
if (unionClone !== null) return unionClone;
|
||||
// The RUNTIME-KEYED literal (JS): a computed key that doesn't fold to a
|
||||
// compile-time string means the literal's shape is not a compile-time
|
||||
@@ -1106,7 +1111,7 @@ export function lowerObjectLiteral(
|
||||
) {
|
||||
tsType = lowerer.checker.getAwaitedType(tsType) ?? tsType;
|
||||
}
|
||||
let mapped = lowerer.mapTypeOf(tsType);
|
||||
let mapped = exact && expected ? expected : lowerer.mapTypeOf(tsType);
|
||||
// A checked-dynamic source has runtime keys even when later explicit
|
||||
// fields make the literal's inferred type look like a fixed record.
|
||||
// Keep a checked-dynamic destination in that representation so copying
|
||||
@@ -1314,6 +1319,13 @@ export function lowerObjectLiteral(
|
||||
lowerer.unsupported("SC1090", deferredConditionalSpread, CONDITIONAL_SPREAD_FENCE);
|
||||
}
|
||||
if (!mapped || mapped.kind !== "record") {
|
||||
// Distribute against the contextual union when there is one (its arms
|
||||
// carry the discriminants), else against the literal's own union.
|
||||
const ctx = lowerer.mapTypeOf(tsType);
|
||||
const distributed = exact
|
||||
? null
|
||||
: lowerDistributedUnionSpread(lowerer, expr, ctx?.kind === "union" ? ctx : mapped);
|
||||
if (distributed) return distributed;
|
||||
// A spread merge over UNION-typed sources (`{ ...base, ...override }`
|
||||
// with `base`, `override` of arm-union types) has one field set per
|
||||
// combination of source arms, so its own type is a union of records.
|
||||
@@ -1915,7 +1927,7 @@ export function lowerObjectLiteral(
|
||||
? lowerConditionalRecordSpread(lowerer, condRecord, locOf(prop))
|
||||
: prop === expr.properties.nodes[0] && leadingSpreadLowered !== null
|
||||
? leadingSpreadLowered
|
||||
: lowerer.lowerExpr(srcNode),
|
||||
: (lowerer.spreadArmOverrides.get(srcNode) ?? lowerer.lowerExpr(srcNode)),
|
||||
);
|
||||
// Array callbacks can receive a record-or-undefined ABI even when
|
||||
// TypeScript describes a required record. Select the copy strategy
|
||||
@@ -2663,6 +2675,217 @@ function isEmptyArrayLiteral(node: ts.Node): boolean {
|
||||
return ts.isArrayLiteralExpression(inner) && inner.elements.nodes.length === 0;
|
||||
}
|
||||
|
||||
/** `{ ...u, k: v }` where the spread source `u` is a union of records (and
|
||||
* unit arms): TypeScript types the literal as the union of one spread per
|
||||
* source arm, so it is built that way. The source evaluates once into a
|
||||
* hidden local; a tag dispatch builds the running arm's literal with that
|
||||
* arm as the source, at the destination arm chosen for it:
|
||||
* 1. the arms the discriminant's last writer selects (an explicit
|
||||
* property, or the source arm when it carries the field);
|
||||
* 2. among those, the source arm itself (`{ ...command, id }`);
|
||||
* 3. else the one arm whose keys are exactly the keys the literal writes.
|
||||
* Every key a contributor may write must be a field of the destination:
|
||||
* the record path's width copy would drop the others, and Node keeps them.
|
||||
* So a genuine merge (`{ ...http, ...patch }` where `patch` may carry a
|
||||
* stdio-only key) stays refused. A stored source the checker narrowed
|
||||
* converts through the checked re-tag first, so an excluded arm (a hidden
|
||||
* absence arm included) throws the catchable TypeError. Null when the
|
||||
* literal is not this shape, before anything is lowered except the
|
||||
* source; the caller keeps its refusal. */
|
||||
function lowerDistributedUnionSpread(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.ObjectLiteralExpression,
|
||||
mapped: IrType | null,
|
||||
): IrExpr | null {
|
||||
if (isJsSourceFile(ts.getSourceFileOfNode(expr))) return null;
|
||||
if (mapped?.kind !== "union") return null;
|
||||
const target = lowerer.unions.get(mapped.unionId);
|
||||
if (!target) return null;
|
||||
const props = expr.properties.nodes;
|
||||
// Exactly one distributed source: a plain spread whose checker type maps
|
||||
// to a union of records and unit arms. Several would be a product.
|
||||
let spreadIndex = -1;
|
||||
let srcNode: ts.Expression | null = null;
|
||||
for (let i = 0; i < props.length; i++) {
|
||||
const prop = props[i]!;
|
||||
if (!ts.isSpreadAssignment(prop)) continue;
|
||||
if (
|
||||
conditionalSpreadOf(prop.expression) !== null ||
|
||||
andSpreadOf(lowerer, prop.expression) !== null ||
|
||||
conditionalRecordSpreadOf(lowerer, prop.expression) !== null
|
||||
)
|
||||
continue;
|
||||
let node: ts.Expression = prop.expression;
|
||||
while (ts.isParenthesizedExpression(node)) node = node.expression;
|
||||
if (lowerer.mapTypeOf(lowerer.typeOf(node))?.kind !== "union") continue;
|
||||
if (srcNode !== null) return null;
|
||||
spreadIndex = i;
|
||||
srcNode = node;
|
||||
}
|
||||
if (srcNode === null) return null;
|
||||
const site = srcNode;
|
||||
// The source evaluates before the dispatch, so everything written before
|
||||
// it must be free of effects.
|
||||
for (const prop of props.slice(0, spreadIndex)) {
|
||||
if (ts.isShorthandPropertyAssignment(prop)) continue;
|
||||
if (ts.isPropertyAssignment(prop) && pureKeyExpr(lowerer, prop.initializer)) continue;
|
||||
return null;
|
||||
}
|
||||
const siteType = lowerer.mapTypeOf(lowerer.typeOf(site));
|
||||
if (siteType?.kind !== "union") return null;
|
||||
const siteDef = lowerer.unions.get(siteType.unionId);
|
||||
if (!siteDef || !siteDef.arms.every((arm) => arm.kind === "record" || isUnitType(arm)))
|
||||
return null;
|
||||
const discField = target.discriminant?.field;
|
||||
// The discriminant values the literal's LAST writer of that field gives
|
||||
// this source arm; null when unknown.
|
||||
const discriminantOf = (tag: number, arm: IrType): ReturnType<typeof literalValues> => {
|
||||
if (discField === undefined) return null;
|
||||
for (let i = props.length - 1; i >= 0; i--) {
|
||||
const prop = props[i]!;
|
||||
if (i === spreadIndex) {
|
||||
if (arm.kind !== "record") continue;
|
||||
if (!lowerer.shapes.get(arm.shapeId)?.fields.some((f) => f.name === discField)) continue;
|
||||
const own = siteDef.discriminant;
|
||||
return own?.field === discField
|
||||
? (own.cases.find((c) => c.tag === tag)?.values ?? null)
|
||||
: null;
|
||||
}
|
||||
if (ts.isSpreadAssignment(prop)) return null;
|
||||
if (
|
||||
(ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) &&
|
||||
propNameText(lowerer, prop.name) === discField
|
||||
)
|
||||
return ts.isPropertyAssignment(prop)
|
||||
? literalValues(lowerer.typeOf(prop.initializer))
|
||||
: literalValues(lowerer.typeOf(prop.name));
|
||||
}
|
||||
return null;
|
||||
};
|
||||
// Every key some contributor may write for this source arm; null when a
|
||||
// contributor has no fixed key set (an index signature, a non-record).
|
||||
const writtenKeys = (arm: IrType): Set<string> | null => {
|
||||
const names = new Set<string>();
|
||||
for (let i = 0; i < props.length; i++) {
|
||||
const prop = props[i]!;
|
||||
if (ts.isSpreadAssignment(prop)) {
|
||||
let node: ts.Expression = prop.expression;
|
||||
while (ts.isParenthesizedExpression(node)) node = node.expression;
|
||||
const t = i === spreadIndex ? arm : lowerer.mapTypeOf(lowerer.typeOf(node));
|
||||
if (t !== null && isUnitType(t)) continue;
|
||||
if (t?.kind !== "record") return null;
|
||||
const shape = lowerer.shapes.get(t.shapeId);
|
||||
if (!shape || shape.indexValue || shape.tuple) return null;
|
||||
for (const f of shape.fields) names.add(f.name);
|
||||
} else if (ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) {
|
||||
names.add(propNameText(lowerer, prop.name));
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return names;
|
||||
};
|
||||
const destinationOf = (tag: number, arm: IrType): (IrType & { kind: "record" }) | null => {
|
||||
const keys = writtenKeys(arm);
|
||||
if (keys === null) return null;
|
||||
let candidates = target.arms
|
||||
.map((a, t) => ({ a, t }))
|
||||
.filter((c): c is { a: IrType & { kind: "record" }; t: number } => c.a.kind === "record")
|
||||
.filter((c) => {
|
||||
const fields = lowerer.shapes.get(c.a.shapeId)!.fields;
|
||||
return [...keys].every((k) => fields.some((f) => f.name === k));
|
||||
});
|
||||
const values = discriminantOf(tag, arm);
|
||||
if (values && values.length > 0 && target.discriminant) {
|
||||
const cases = target.discriminant.cases;
|
||||
candidates = candidates.filter((c) =>
|
||||
(cases.find((k) => k.tag === c.t)?.values ?? []).some((v) => values.includes(v)),
|
||||
);
|
||||
}
|
||||
if (candidates.length === 1) return candidates[0]!.a;
|
||||
if (arm.kind === "record") {
|
||||
const same = candidates.find((c) => c.a.shapeId === arm.shapeId);
|
||||
if (same) return same.a;
|
||||
}
|
||||
const exactKeys = candidates.filter(
|
||||
(c) => lowerer.shapes.get(c.a.shapeId)!.fields.length === keys.size,
|
||||
);
|
||||
return exactKeys.length === 1 ? exactKeys[0]!.a : null;
|
||||
};
|
||||
// Plan every arm before lowering anything.
|
||||
const plan: { tag: number; arm: IrType; destination: IrType & { kind: "record" } }[] = [];
|
||||
for (let tag = 0; tag < siteDef.arms.length; tag++) {
|
||||
const arm = siteDef.arms[tag]!;
|
||||
const destination = destinationOf(tag, arm);
|
||||
if (destination === null) return null;
|
||||
plan.push({ tag, arm, destination });
|
||||
}
|
||||
const loc = locOf(expr);
|
||||
let source = lowerer.lowerExpr(site);
|
||||
if (source.type.kind === "union" && !typeEquals(source.type, siteType)) {
|
||||
const helper = lowerer.narrowedRetagHelper(
|
||||
site,
|
||||
source.type.unionId,
|
||||
siteType.unionId,
|
||||
source.loc,
|
||||
);
|
||||
if (helper)
|
||||
source = { kind: "call", callee: helper, args: [source], type: siteType, loc: source.loc };
|
||||
}
|
||||
if (!typeEquals(source.type, siteType)) return null;
|
||||
const saved = lowerer.declareHiddenLocal("%spreadArms", siteType);
|
||||
const ref = varRef(saved.id, siteType, loc);
|
||||
let result: IrExpr | null = null;
|
||||
for (let i = plan.length - 1; i >= 0; i--) {
|
||||
const { tag, arm, destination } = plan[i]!;
|
||||
const armValue: IrExpr =
|
||||
arm.kind === "record"
|
||||
? { kind: "unionNarrow", value: ref, unionId: siteType.unionId, tag, type: arm, loc }
|
||||
: {
|
||||
kind: "recordLit",
|
||||
fields: [],
|
||||
type: { kind: "record", shapeId: lowerer.shapes.intern([]) },
|
||||
loc,
|
||||
};
|
||||
lowerer.spreadArmOverrides.set(site, armValue);
|
||||
let built: IrExpr;
|
||||
try {
|
||||
built = lowerObjectLiteral(lowerer, expr, destination, true);
|
||||
} finally {
|
||||
lowerer.spreadArmOverrides.delete(site);
|
||||
}
|
||||
const wrapped = lowerer.coerceToExpected(built, mapped);
|
||||
if (!typeEquals(wrapped.type, mapped)) return null;
|
||||
result =
|
||||
result === null
|
||||
? wrapped
|
||||
: {
|
||||
kind: "ternary",
|
||||
cond: {
|
||||
kind: "unionIsTag",
|
||||
value: ref,
|
||||
unionId: siteType.unionId,
|
||||
tag,
|
||||
negated: false,
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: wrapped,
|
||||
else_: result,
|
||||
type: mapped,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (result === null) return null;
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "varDecl", localId: saved.id, init: source, loc }],
|
||||
result,
|
||||
type: mapped,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** A union of plain records can preserve its existing variant while
|
||||
* replacing shared fields. Dispatch before constructing the copy: only
|
||||
* the selected arm evaluates overrides, and recordClone snapshots fields
|
||||
|
||||
@@ -3548,6 +3548,9 @@ export class Lowerer {
|
||||
readonly neverValued = new Set<IrExpr>();
|
||||
readonly chainNarrowedType = new Map<ts.Node, ts.Type>();
|
||||
readonly chainHandled = new Set<ts.Node>();
|
||||
/** One arm's narrowed value for a spread source while a distributed
|
||||
* union spread lowers that arm (lowerDistributedUnionSpread). */
|
||||
readonly spreadArmOverrides = new Map<ts.Node, IrExpr>();
|
||||
/** for-of-over-matchAll bindings whose `.index` reads the companion-index
|
||||
* array: binding SYMBOL → the hidden number[] of match start indices plus
|
||||
* the hidden cursor holding THIS iteration's position (registered while
|
||||
@@ -4617,6 +4620,7 @@ export class Lowerer {
|
||||
"neverValued",
|
||||
"chainNarrowedType",
|
||||
"chainHandled",
|
||||
"spreadArmOverrides",
|
||||
"splitIdentifierUses",
|
||||
"typeMemo",
|
||||
"fnStack",
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/union-clone-override-width.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/union-spread-distribution.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/union-clone-override-width.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+1
-2
@@ -3,8 +3,7 @@ type WiderPayload = { tag: "number"; value: number; extra: boolean } | { tag: "t
|
||||
type Container = { kind: "left"; payload: Payload; left: number } | { kind: "right"; payload: Payload; right: number };
|
||||
|
||||
function copy(source: Container, payload: WiderPayload): boolean {
|
||||
// Retagging the override into source.payload would discard extra and
|
||||
// copy its record. The inferred spread result keeps the wider payload.
|
||||
// An override wider than every source arm's field: the distributed spread builds each arm at the inferred result's arm, which keeps the wider payload.
|
||||
const result = { ...source, payload };
|
||||
return result.payload.extra;
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
// Object spreads whose source is a union of records: TypeScript types
|
||||
// `{ ...u, k: v }` as one spread per arm of `u`, and the literal is built
|
||||
// that way. The running arm decides which destination arm holds the result:
|
||||
// the discriminant's last writer, else the source arm itself, else the one
|
||||
// arm with exactly the written keys. JSON.stringify pins key order too.
|
||||
|
||||
// 1. One union source plus an override, inside an array callback, after a
|
||||
// discriminant test narrowed the stored union.
|
||||
type Text = { type: "text"; text: string };
|
||||
type Image = { type: "image"; data: string };
|
||||
type UserMsg = { role: "user"; content: string | (Text | Image)[]; ts: number };
|
||||
type AsstMsg = { role: "assistant"; content: Text[]; model: string };
|
||||
type ToolMsg = { role: "tool"; content: (Text | Image)[]; toolId: string; isError?: boolean };
|
||||
type Msg = UserMsg | AsstMsg | ToolMsg;
|
||||
|
||||
function stripImages(msgs: Msg[]): Msg[] {
|
||||
return msgs.map((msg) => {
|
||||
if (msg.role === "user" || msg.role === "tool") {
|
||||
const content = msg.content;
|
||||
if (Array.isArray(content)) {
|
||||
const filtered = content.map((c) =>
|
||||
c.type === "image" ? { type: "text" as const, text: "[image]" } : c,
|
||||
);
|
||||
return { ...msg, content: filtered };
|
||||
}
|
||||
}
|
||||
return msg;
|
||||
});
|
||||
}
|
||||
|
||||
// 2. A union source plus a field the arms declare optional, cast back.
|
||||
type Cmd =
|
||||
| { id?: string; type: "prompt"; message: string }
|
||||
| { id?: string; type: "abort" }
|
||||
| { id?: string; type: "set_model"; provider: string; modelId: string };
|
||||
let next = 0;
|
||||
function withId(command: Cmd): Cmd {
|
||||
const id = `req_${++next}`;
|
||||
return { ...command, id } as Cmd;
|
||||
}
|
||||
|
||||
// 3. An optional group `{} | { during }` spread into a discriminated result.
|
||||
type Result =
|
||||
| { ok: true; value: number; during?: { seq: number } }
|
||||
| { ok: false; error: string; during?: { seq: number } };
|
||||
function finish(error: string | undefined, seq: number | undefined): Result {
|
||||
const ended = seq === undefined ? {} : { during: { seq } };
|
||||
return error ? { ok: false, error, ...ended } : { ok: true, value: 1, ...ended };
|
||||
}
|
||||
|
||||
// 4. A discriminant test leaves several arms; overrides set and clear keys.
|
||||
type Ref =
|
||||
| { status: "resolved"; name: string; level?: string; warning?: string }
|
||||
| { status: "ambiguous"; names: string[]; level?: string; warning?: string }
|
||||
| { status: "not-found" };
|
||||
function relevel(inner: Ref): Ref {
|
||||
if (inner.status === "not-found") return inner;
|
||||
return { ...inner, level: undefined, warning: "w" };
|
||||
}
|
||||
|
||||
// 5. The source supplies the second half of an event whose discriminant
|
||||
// value is shared by two arms; the written keys pick the arm.
|
||||
type Ev =
|
||||
| { type: "start" }
|
||||
| { type: "retry"; source: "branch" }
|
||||
| { type: "retry"; source: "compact"; reason: "manual" | "auto" }
|
||||
| { type: "end"; count: number };
|
||||
const events: string[] = [];
|
||||
function emit(event: Ev): void {
|
||||
events.push(JSON.stringify(event));
|
||||
}
|
||||
function retrying(source: { source: "branch" } | { source: "compact"; reason: "manual" | "auto" }) {
|
||||
emit({ type: "retry", ...source });
|
||||
}
|
||||
|
||||
// 6. A cast-built event from a union base.
|
||||
type BaseA = { type: "settle"; id: string };
|
||||
type BaseB = { type: "turn"; id: string; turn: number };
|
||||
type SettleEv = BaseA & { entries: string[]; more: boolean };
|
||||
type TurnEv = BaseB & { entries: string[]; more: boolean };
|
||||
function boundary(base: BaseA | BaseB, entries: string[]): SettleEv | TurnEv {
|
||||
return { ...base, entries, more: entries.length > 1 } as SettleEv | TurnEv;
|
||||
}
|
||||
|
||||
// 7. A message update whose override's type differs per arm.
|
||||
type Part = { type: "text"; text: string };
|
||||
type UMsg = { role: "user"; content: string | Part[] };
|
||||
type AMsg = { role: "assistant"; content: Part[]; model: string };
|
||||
type Note = { role: "note"; body: string };
|
||||
type AnyMsg = UMsg | AMsg | Note;
|
||||
function replace(messages: AnyMsg[], text: string): AnyMsg[] {
|
||||
return messages.map((message) => {
|
||||
if (message.role !== "user" && message.role !== "assistant") return message;
|
||||
const content = message.role === "assistant" ? [{ type: "text" as const, text }] : text;
|
||||
return { ...message, content } as AnyMsg;
|
||||
});
|
||||
}
|
||||
|
||||
const msgs: Msg[] = [
|
||||
{ role: "user", content: [{ type: "image", data: "x" }, { type: "text", text: "hi" }], ts: 1 },
|
||||
{ role: "assistant", content: [{ type: "text", text: "ok" }], model: "m" },
|
||||
{ role: "tool", content: [{ type: "image", data: "y" }], toolId: "t1", isError: false },
|
||||
{ role: "user", content: "plain", ts: 2 },
|
||||
];
|
||||
for (const m of stripImages(msgs)) console.log(JSON.stringify(m));
|
||||
console.log(JSON.stringify(withId({ type: "prompt", message: "go" })));
|
||||
console.log(JSON.stringify(withId({ type: "abort", id: "old" })));
|
||||
console.log(JSON.stringify(withId({ type: "set_model", provider: "p", modelId: "q" })));
|
||||
console.log(JSON.stringify(finish(undefined, undefined)));
|
||||
console.log(JSON.stringify(finish("bad", 3)));
|
||||
console.log(JSON.stringify(finish(undefined, 4)));
|
||||
const resolved = relevel({ status: "resolved", name: "a", level: "hi" });
|
||||
console.log(JSON.stringify(resolved), "level" in resolved, "warning" in resolved);
|
||||
console.log(JSON.stringify(relevel({ status: "ambiguous", names: ["a", "b"] })));
|
||||
console.log(JSON.stringify(relevel({ status: "not-found" })));
|
||||
retrying({ source: "branch" });
|
||||
retrying({ source: "compact", reason: "auto" });
|
||||
console.log(events.join(" "));
|
||||
console.log(JSON.stringify(boundary({ type: "settle", id: "s" }, ["a"])));
|
||||
console.log(JSON.stringify(boundary({ type: "turn", id: "t", turn: 3 }, ["a", "b"])));
|
||||
for (const m of replace(
|
||||
[
|
||||
{ role: "user", content: "old" },
|
||||
{ role: "assistant", content: [], model: "m" },
|
||||
{ role: "note", body: "n" },
|
||||
],
|
||||
"new",
|
||||
))
|
||||
console.log(JSON.stringify(m));
|
||||
// The spread copies a snapshot: a later write to the source is not seen.
|
||||
const base: Cmd = { type: "prompt", message: "before" };
|
||||
const copied = withId(base);
|
||||
if (base.type === "prompt") base.message = "after";
|
||||
console.log(JSON.stringify(copied), JSON.stringify(base));
|
||||
@@ -1,8 +0,0 @@
|
||||
union-clone-override-width.ts:8:18 - error SC2003: a spread merge over union-typed sources builds a value that can carry fields from several arms at once, so no single arm of '{ kind: "left"; left: number; payload: WiderPayload; } | { kind: "right"; right: number; payload: WiderPayload; }' can represent it (candidate arms: '{ kind: "left"; left: number; payload: WiderPayload; }', '{ kind: "right"; right: number; payload: WiderPayload; }')
|
||||
|
||||
7 | // copy its record. The inferred spread result keeps the wider payload.
|
||||
8 | const result = { ...source, payload };
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~
|
||||
9 | return result.payload.extra;
|
||||
|
||||
hint: merge ONE arm per path (narrow every spread source to that same arm first — a discriminant switch or an assertion — so the spread builds a single record), or declare ONE open record type whose fields are all optional instead of a union of all-optional arms
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4537, "san": 6468 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 6958, "san": 12595 }
|
||||
Reference in New Issue
Block a user