Let boxable function arms ride a checked-dynamic union

A union with a checked-dynamic arm (`unknown`, fetch's AbortSignal)
maps to the checked-dynamic value when every other arm is one the
checked-dynamic tree holds faithfully. Function arms were excluded, so
`Progress | AbortSignal | undefined` was refused (SC2009).

The tree boxes a function whose parameters check in and whose result
converts out by reference (canBoxFuncIntoDyn), and `typeof x ===
"function"` narrows it back out to the typed function. Such arms now
join the collapse. The corpus program checks calls, identity (`===`
against the original), and the other arm's members against Node.

pi's SessionManager.listAll takes `onProgressOrSignal?: SessionListProgress
| AbortSignal`. The refusal rejected the whole SessionManager class.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Malte Ubl
2026-10-25 15:02:19 -07:00
co-authored by Claude Opus 5.5
parent 8a22e0f515
commit 605e312e78
4 changed files with 66 additions and 0 deletions
@@ -10,6 +10,7 @@ import {
isDynTypedRefType,
BOOL,
bytesOf,
canBoxFuncIntoDyn,
canConvertToDyn,
canDynCheckTo,
CHILD_T,
@@ -8341,6 +8342,16 @@ function dynSubsumableUnionArm(arm: IrType, ctx: TypeMapperCtx): boolean {
// `string | URL | Request`, fetch's own input type under @types/node.
case "url":
return true;
// A function the checked-dynamic tree can box (each parameter checks
// in, the result converts out) is held by reference there, and
// `typeof x === "function"` narrows it back out: `Progress | AbortSignal`
// beside fetch's handles.
case "func":
return canBoxFuncIntoDyn(
arm,
(id) => ctx.shapes.get(id),
(id) => ctx.unions.get(id),
);
case "record":
case "array":
// `T | Promise<T>` with T bound to `unknown`: the checked-dynamic value
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/dyn-union-function-arms.ts"
],
"diags": []
}
+48
View File
@@ -0,0 +1,48 @@
// A union of a function type with a value the checked-dynamic tree holds
// (AbortSignal, `unknown`) rides that representation: the function is held
// by reference, `typeof x === "function"` narrows it back out, and calls,
// identity and the other arm's members behave as in Node.
type Progress = (loaded: number, total: number) => void;
function listAll(
dirOrProgress?: string | Progress,
progressOrSignal?: Progress | AbortSignal,
signal?: AbortSignal,
): string {
const dir = typeof dirOrProgress === "string" ? dirOrProgress : "/default";
const progress =
typeof dirOrProgress === "function"
? dirOrProgress
: typeof progressOrSignal === "function"
? progressOrSignal
: undefined;
const abortSignal =
typeof dirOrProgress === "string" || typeof progressOrSignal === "function"
? signal
: (progressOrSignal ?? signal);
progress?.(1, 3);
progress?.(3, 3);
return `${dir} aborted=${abortSignal?.aborted ?? "none"}`;
}
const seen: string[] = [];
const report: Progress = (loaded, total) => seen.push(`${loaded}/${total}`);
const controller = new AbortController();
console.log(listAll());
console.log(listAll("/x", report));
console.log(listAll(report, controller.signal));
controller.abort();
console.log(listAll("/y", undefined, controller.signal));
console.log(listAll(report, controller.signal), seen.join(" "));
function same(handler: Progress | AbortSignal | undefined): boolean {
return handler === report;
}
console.log(same(report), same(controller.signal), same(undefined), same((a, b) => void (a + b)));
function describe(value: unknown | ((n: number) => string)): string {
if (typeof value === "function") return `function -> ${(value as (n: number) => string)(7)}`;
return `value ${JSON.stringify(value)}`;
}
console.log(describe((n: number) => `n=${n}`), describe({ a: 1 }), describe("s"));
@@ -0,0 +1 @@
{ "plain": 4000 }