From 76e5a1c48f41ba031d6f1ac6b9c6c68f66b3b434 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Mon, 19 Oct 2026 14:54:50 -0700 Subject: [PATCH] Accept optional checked-dynamic parameters in generic method instances paramShapes, which builds the parameters of an instantiated signature (generic call instances and the erased instance that a method value or a reflected prototype slot uses), refused an optional parameter unless its type had an undefined union arm. An interface view implemented by a class (`handle?: Handle | null`) and an erased type parameter map to the checked-dynamic type, which holds undefined directly; paramShape, the declared-signature path, already accepts it. Direct calls of such a method failed with "optional parameters of type 'unknown'", and reflecting its class prototype from the dynamic-dispatch fixpoint raised the same refusal outside any statement and crashed the compiler with an uncaught PoisonError. paramShapes now treats the parameter as omittable, so the instance lowers. Co-Authored-By: Claude Opus 5.5 --- .../src/frontend/lowering/call-signatures.ts | 10 ++++- ...ype-reflection-optional-view-param.ts.json | 6 +++ ...rototype-reflection-optional-view-param.ts | 42 +++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6340-prototype-reflection-optional-view-param.ts.json create mode 100644 tests/corpus/6340-prototype-reflection-optional-view-param.ts diff --git a/packages/compiler/src/frontend/lowering/call-signatures.ts b/packages/compiler/src/frontend/lowering/call-signatures.ts index 9d9c9668c..557b1a462 100644 --- a/packages/compiler/src/frontend/lowering/call-signatures.ts +++ b/packages/compiler/src/frontend/lowering/call-signatures.ts @@ -408,7 +408,15 @@ export function paramShapes( return { type: mapped, mode: "rest" }; } if (declParam.initializer) return defaultParameterShape(lowerer, declParam, mapped); - if (declParam.questionToken && !lowerer.bareUndefinedArmedUnion(mapped)) { + // A checked-value slot holds undefined directly, so an omitted + // argument needs no union arm (paramShape's declared-signature rule; + // an erased type parameter and a class-implemented interface view + // both map here). + if ( + declParam.questionToken && + !lowerer.bareUndefinedArmedUnion(mapped) && + mapped.kind !== "dyn" + ) { lowerer.unsupported( "SC1090", declParam, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6340-prototype-reflection-optional-view-param.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6340-prototype-reflection-optional-view-param.ts.json new file mode 100644 index 000000000..89aef0f85 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6340-prototype-reflection-optional-view-param.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6340-prototype-reflection-optional-view-param.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6340-prototype-reflection-optional-view-param.ts b/tests/corpus/6340-prototype-reflection-optional-view-param.ts new file mode 100644 index 000000000..58e21164d --- /dev/null +++ b/tests/corpus/6340-prototype-reflection-optional-view-param.ts @@ -0,0 +1,42 @@ +// A generic method whose optional parameter is an interface view (or an +// erased type parameter) reflects through the prototype of an instance +// that crossed into checked-dynamic code, and calls directly with the +// argument omitted. +interface Handle { + close(): string; +} +class Plain implements Handle { + close(): string { + return "closed"; + } +} +class Registry { + private current: Handle | null = null; + use(handle: Handle | null): void { + this.current = handle; + } + wrap(fn: F, handle?: Handle | null): F { + if (handle) this.current = handle; + return fn; + } + tag(value: T, label?: T): string { + return `${String(value)}:${label === undefined ? "none" : String(label)}`; + } + size(): number { + return this.current === null ? 0 : 1; + } +} + +const registry = new Registry(); +const inc = registry.wrap((x: number) => x + 1); +console.log(inc(1), registry.size()); +const dec = registry.wrap((x: number) => x - 1, new Plain()); +console.log(dec(1), registry.size()); +console.log(registry.tag(1), registry.tag("a", "b")); + +const boxed: unknown = registry; +const proto = Object.getPrototypeOf(boxed); +console.log(Object.getOwnPropertyNames(proto).join(",")); +console.log(typeof proto.wrap, typeof proto.tag, proto.size.call(registry)); +const loose: any = registry; +console.log(loose.size(), proto.tag.call(registry, 2), proto.tag.call(registry, 2, 3));