Files
scriptc/tests/corpus/record-func-field-width.ts
T
Malte UblandClaude Opus 5.5 3c5b9eadd2 Lift a record field whose function value narrows its parameters
A record width conversion copies a function-valued field when the field's
signature adapts (cleanFuncAdaptable). That gate asked coercibleValue of each
parameter pair, which excludes the checked-dynamic extraction and the
structural width the parameter conversion actually performs, so a definition
whose `execute`/`renderCall` take a concrete parameter record could not
complete a slot declaring `unknown` members — the SC2002 `field 'execute'` /
`field 'renderCall'` shape that dominates the census's tool-definition sites.

The gate now measures what funcCoerceAdapter converts: cleanParamPair for
parameters (so a callback parameter recurses through the same rule one
function type down) and cleanResult for the result (a structural width, which
carries the promise payload widening, and the void-into-unknown drop). A
recursion guard keyed in coercions.planning keeps structurally recursive
function aliases from looping, the coinductive answer recordWidthPlan already
gives its own back-edges.

A parameter that narrows a dynamic member to a record is the checked-dynamic
extraction, the same conversion a cast performs: an argument of the wrong
shape throws from the adapter rather than reading undefined, as with any
checked conversion in the language.

The gate widens admission only: a pair the old gate refused was an SC2002
error, so programs that already compiled keep byte-identical IR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 05:38:14 -07:00

110 lines
3.7 KiB
TypeScript

// A record field holding a function adapts when the slot's function type is a
// structural width of the field's: a parameter record whose `unknown` members
// gain shapes (the checked-dynamic extraction), a callback parameter whose
// record widens the same way one level down, and a promise payload that widens.
//
// `wrapToolDefinition` states the definition's state and details parameters as
// `any`/default `unknown`, so passing a concrete definition crosses the
// `execute` and `renderCall` fields of the record.
interface Truncation {
content: string;
truncated: boolean;
}
interface ToolResult<D> {
content: { type: string; text: string }[];
details: D;
isError: boolean | undefined;
}
type UpdateCallback<D> = (partial: ToolResult<D>) => void;
interface ToolDefinition<S, D> {
name: string;
execute(id: string, onUpdate: UpdateCallback<D> | undefined): Promise<ToolResult<D>>;
renderCall(context: { state: S; label: string }): string;
}
interface BashState {
startedAt: number;
executionStarted: boolean;
}
interface BashDetails {
truncation: Truncation | undefined;
fullOutputPath: string | undefined;
}
function wrapToolDefinition<D = unknown>(
definition: ToolDefinition<any, D>,
): ToolDefinition<any, D> {
return {
name: definition.name,
execute: definition.execute,
renderCall: definition.renderCall,
};
}
function bashDefinition(name: string): ToolDefinition<BashState, BashDetails> {
return {
name,
execute: (id, onUpdate) => {
const result: ToolResult<BashDetails> = {
content: [{ type: "text", text: `ran ${id}` }],
details: { truncation: { content: "tail", truncated: true }, fullOutputPath: undefined },
isError: undefined,
};
onUpdate?.(result);
return Promise.resolve(result);
},
renderCall: (context) => `${context.label} after ${context.state.startedAt}ms`,
};
}
// The `execute` field: the slot's update callback takes `details: unknown`
// where the value's takes the concrete record, and the result promise payload
// widens the same way.
function widenDetails(): ToolDefinition<any, unknown> {
return wrapToolDefinition<unknown>(bashDefinition("bash"));
}
// The `renderCall` field: the slot's context record has `state: unknown`
// where the value's has the concrete state record.
function widenState(definition: ToolDefinition<BashState, BashDetails>): ToolDefinition<any, BashDetails> {
return wrapToolDefinition<BashDetails>(definition);
}
// The same crossing when the argument is an object literal carrying optional
// members the slot's shape also declares.
interface WithHints {
annotations: { readOnlyHint: boolean | undefined } | undefined;
defaultActive: boolean | undefined;
}
function widenLiteral(): ToolDefinition<any, BashDetails> {
const hints: WithHints = { annotations: { readOnlyHint: true }, defaultActive: undefined };
const definition: ToolDefinition<BashState, BashDetails> & WithHints = {
...bashDefinition("literal"),
...hints,
};
console.log("hints:", definition.annotations?.readOnlyHint, definition.defaultActive);
return wrapToolDefinition<BashDetails>(definition);
}
const widened = widenDetails();
widened
.execute("ls", (partial) => {
console.log("update:", partial.content[0]?.text, partial.isError);
})
.then((result) => {
console.log("result:", result.details, result.isError);
});
const rendered = widenState(bashDefinition("grep"));
console.log("render:", rendered.renderCall({ state: { startedAt: 12, executionStarted: true }, label: "grep" }));
const literal = widenLiteral();
console.log("literal render:", literal.renderCall({ state: { startedAt: 3, executionStarted: false }, label: "ls" }));
console.log("name:", literal.name);