Files
scriptc/tests/corpus/optional-param-function-values.ts
T
Malte UblandClaude Opus 5.5 7245ec9a04 Admit function values whose completed parameters the slot coercion adapts
A method declaring `ctx?: Ctx` completes to `(id, Ctx | undefined) => string`;
a slot that requires the narrower `(id, ToolCtx) => string` was refused with
SC1090 at the value's birth, because the gate only compared the two
signatures for equality. The slot coercion (funcCoerceAdapter) already
converts every parameter through coercibleValue, the record width conversion
(which also selects a width-liftable arm of a union destination — the
`ToolCtx` into `Ctx | undefined` shape), the partial checked-dynamic
extraction, and the union arm preservation. `funcSlotAdaptable` now asks the
same question through `cleanParamPair`, so the gate admits exactly the pairs
the adapter converts without a trap-only path, and both call sites (the
object-literal method slot and the mapped value target) use it.

The gate also learns the two slot shapes it could not name: an object
literal contextualized by an INDEX SIGNATURE (`Record<string, Handler>`)
takes its member slot from the index signature's value type, and a literal
with no contextual type takes the member's own completed type — the type
every later read of the member spells.

Nested function DECLARATIONS are no longer judged at birth: lowering a
declaration builds the body, not a value, and each value use re-checks the
completed signature against that use's own target. An overloaded nested
declaration with an optional parameter, returned as a value (`return write`
into a one-parameter `Writer`), is now accepted instead of refused.

Corpus: tests/corpus/optional-param-function-values.ts (a required slot over
an optional parameter, an index-signature member with an extra trailing
optional parameter, an annotated value with a defaulted parameter, the nested
overloaded declaration returned as a value, direct-call controls, and a
union-typed slot). Node-identical stdout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 17:58:02 -07:00

108 lines
3.9 KiB
TypeScript

// Functions whose own parameters are optional, defaulted or extra take part
// as VALUES in slots that spell the completed signature (WP-A, the
// `tool-definition-wrapper` shape): a method's narrower required parameter
// against the declaring interface's optional one, a handler with more
// (trailing optional) parameters than the slot's type names, and a nested
// function declaration returned in an object literal. Node semantics: the
// slot passes fewer arguments than the value declares, so the missing ones
// are `undefined` and defaults apply.
interface Ctx {
cwd: string;
}
interface ToolCtx extends Ctx {
tools: string[];
}
interface RequiredSlot {
run(id: string, ctx: ToolCtx): string;
}
// A: a method declaring its parameter OPTIONAL fills a slot that requires
// the (narrower) parameter: the slot always passes it, so the `undefined`
// arm is unreachable through that slot.
const a: RequiredSlot = {
run(id: string, ctx?: Ctx): string {
return `${id}@${ctx === undefined ? "-" : ctx.cwd}`;
},
};
console.log(a.run("A", { cwd: "/a", tools: [] }));
// The same method through its own object: the omitted call is real here.
const aLiteral = {
run(id: string, ctx?: Ctx): string {
return `${id}@${ctx === undefined ? "-" : ctx.cwd}`;
},
};
console.log(aLiteral.run("A"), aLiteral.run("A", { cwd: "/a" }));
// B: a handler with an EXTRA trailing optional parameter in a two-parameter
// slot: the slot never passes it, so it reads undefined.
type Handler = (id: string, payload: number) => string;
const handlers: Record<string, Handler> = {
plain(id, payload, extra?: string) {
return `${id}:${payload}:${extra === undefined ? "-" : extra}`;
},
};
console.log(handlers["plain"]("one", 1));
const annotated: Handler = (id, payload, extra?: string) =>
`${id}:${payload}:${extra === undefined ? "-" : extra}`;
console.log(annotated("two", 2));
// A defaulted trailing parameter: the slot's omission applies the default.
const withDefault: Handler = (id, payload, extra = "dflt") => `${id}:${payload}:${extra}`;
console.log(withDefault("three", 3));
// C: a nested function DECLARATION (overloaded, with an optional parameter)
// returned as a value: lowering the declaration builds its body; the value
// use at the return re-checks the completed signature against the slot.
interface Writer {
(text: string): string;
}
function makeWriter(prefix: string): Writer {
function write(text: string): string;
function write(text: string, times?: number): string;
function write(text: string, times?: number): string {
const n = times === undefined ? 1 : times;
let out = "";
for (let i = 0; i < n; i++) out += `${prefix}${text}`;
return out;
}
return write;
}
const writer = makeWriter("> ");
console.log(writer("x"));
// Control: the same signatures through direct calls.
function greet(name?: string): string {
return name === undefined ? "hello" : `hello ${name}`;
}
console.log(greet(), greet("ada"));
function repeat(text: string, times = 3, sep = "|"): string {
let out = "";
for (let i = 0; i < times; i++) out += (i === 0 ? "" : sep) + text;
return out;
}
console.log(repeat("z"), repeat("z", 2), repeat("z", 2, "+"));
const pick: (x: number | undefined) => number = (x?: number) => (x === undefined ? -1 : x);
console.log(pick(undefined), pick(4));
// A method whose optional parameter is READ through the value it was given
// (the `arguments.length`-free shape the tool registry uses).
interface UnsafeSlot {
call(payload: string): string;
}
const unsafe: UnsafeSlot = {
call(payload: string, options?: { tag: string }): string {
return `${payload}/${options === undefined ? "-" : options.tag}`;
},
};
console.log(unsafe.call("q"));
// A value stored in a union-typed slot (the omitted call stays observable).
type Runner = RequiredSlot | undefined;
const maybeRunner: Runner = a;
console.log(maybeRunner === undefined ? "none" : maybeRunner.run("U", { cwd: "/u", tools: [] }));