mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
b200bde558
commit
3c5b9eadd2
@@ -129,7 +129,13 @@ export function omittableTrailingParam(lowerer: Lowerer, type: IrType): boolean
|
||||
* parameters are ignored. A source with MORE parameters than the slot is
|
||||
* clean when every extra trailing one is omittable — the adapter passes
|
||||
* that absent value explicitly (funcCoerceAdapter's omittedParams), the
|
||||
* same way a call through the slot would. */
|
||||
* same way a call through the slot would. A parameter the slot coercion
|
||||
* converts through a STRUCTURAL width counts (cleanParamPair), which
|
||||
* recurses through a nested function type: a callback parameter of the
|
||||
* value taking a narrower record where the slot's takes its `unknown` view
|
||||
* converts the argument the same way a top-level call does. The result side
|
||||
* likewise accepts what funcCoerceAdapter converts (cleanResult), including
|
||||
* a promise whose payload widens. */
|
||||
export function cleanFuncAdaptable(
|
||||
lowerer: Lowerer,
|
||||
src: IrType & { kind: "func" },
|
||||
@@ -137,15 +143,35 @@ export function cleanFuncAdaptable(
|
||||
): boolean {
|
||||
if (restPackingAdapterFits(lowerer, src, dst)) return true;
|
||||
if (src.rest === true || dst.rest === true) return false;
|
||||
for (let i = dst.params.length; i < src.params.length; i++) {
|
||||
if (!omittableTrailingParam(lowerer, src.params[i]!)) return false;
|
||||
// Recursive function types (`type Handler = (h: Handler) => void`) reach
|
||||
// the same pair again one parameter down: assume it converts, the same
|
||||
// coinductive answer recordWidthPlan gives its own back-edges.
|
||||
const key = `funcClean:${typeKey(src)}:${typeKey(dst)}`;
|
||||
if (lowerer.coercions.planning.has(key)) return true;
|
||||
lowerer.coercions.planning.add(key);
|
||||
try {
|
||||
for (let i = dst.params.length; i < src.params.length; i++) {
|
||||
if (!omittableTrailingParam(lowerer, src.params[i]!)) return false;
|
||||
}
|
||||
for (let i = 0; i < src.params.length && i < dst.params.length; i++) {
|
||||
if (!cleanParamPair(lowerer, dst.params[i]!, src.params[i]!)) return false;
|
||||
}
|
||||
return cleanResult(lowerer, src.ret, dst.ret);
|
||||
} finally {
|
||||
lowerer.coercions.planning.delete(key);
|
||||
}
|
||||
for (let i = 0; i < src.params.length && i < dst.params.length; i++) {
|
||||
if (!lowerer.coercibleValue(dst.params[i]!, src.params[i]!)) return false;
|
||||
}
|
||||
if (dst.ret.kind === "void") return true;
|
||||
if (lowerer.coercibleValue(src.ret, dst.ret)) return true;
|
||||
return src.ret.kind === "void" && dst.ret.kind === "dyn";
|
||||
}
|
||||
|
||||
/** Whether the slot's coercion converts the value's RESULT without a
|
||||
* trap-only path: the dispositions funcCoerceAdapter's result side accepts
|
||||
* (a void slot drops the value; coercibleValue; a structural width, which
|
||||
* includes the promise payload widening and a nested function adaptation;
|
||||
* the void-into-unknown drop). */
|
||||
function cleanResult(lowerer: Lowerer, actual: IrType, expected: IrType): boolean {
|
||||
if (expected.kind === "void") return true;
|
||||
if (lowerer.coercibleValue(actual, expected)) return true;
|
||||
if (actual.kind === "void" && expected.kind === "dyn") return true;
|
||||
return lowerer.widthLiftPlan(actual, expected) !== null;
|
||||
}
|
||||
|
||||
/** Whether the slot's argument type `actual` converts into the function's
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/record-func-field-width.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// 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);
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3050, "san": 4250 }
|
||||
Reference in New Issue
Block a user