mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Find kept record sources through index signatures and record fields
The capture into an index-signature record consults a record's kept source
view only when some checked conversion in the program can keep one. That
scan accepted a closed record target and looked through union arms,
promises and arrays, but stopped at an index-signature record or a record
whose fields hold closed records. A conversion into
`Record<string, { x: string }>` keeps each value's undeclared keys, so a
program whose only such conversion had that shape dropped them in the
capture. The scan now looks through record fields and index-signature
values too, memoizing records by shape as it does unions.
This commit is contained in:
@@ -40,9 +40,9 @@ export function recordSourceTest(value: IrExpr, loc: SrcLoc): IrExpr {
|
||||
return { kind: "libCall", fn: "record.sourceHas", args: [value], type: BOOL, loc };
|
||||
}
|
||||
|
||||
/** A union revisited while it is being visited answers as if its cycle arm
|
||||
* were empty (`keeps: false`), so the caller's answer is incomplete until the
|
||||
* outer union settles. `cycled` marks that incompleteness: such an answer is
|
||||
/** A union or record revisited while it is being visited answers as if its
|
||||
* cycle member were empty (`keeps: false`), so the caller's answer is
|
||||
* incomplete until the outer one settles. `cycled` marks that incompleteness: such an answer is
|
||||
* never memoized, because another entry point into the same cycle could reach
|
||||
* the missing arm. */
|
||||
interface KeepsSourceAnswer {
|
||||
@@ -50,8 +50,8 @@ interface KeepsSourceAnswer {
|
||||
cycled: boolean;
|
||||
}
|
||||
|
||||
/** Shared work list of the whole-program scan: settled union answers, and the
|
||||
* unions on the current path. */
|
||||
/** Shared work list of the whole-program scan: settled union and record
|
||||
* answers, and the unions and records on the current path, by id. */
|
||||
export interface KeepsSourceScan {
|
||||
memo: Map<string, boolean>;
|
||||
visiting: Set<string>;
|
||||
@@ -59,33 +59,47 @@ export interface KeepsSourceScan {
|
||||
|
||||
/** Whether a checked conversion into `target` can leave a record holding
|
||||
* undeclared source keys. The conversion reads one record at a time, so the
|
||||
* records nested in a union arm, a promise, or an array count too. */
|
||||
* records nested in a union arm, a promise, an array, a record field or an
|
||||
* index signature's values count too (`Record<string, { x: string }>` keeps
|
||||
* the sources of its closed values). Unions and records are memoized by id;
|
||||
* one revisited on the current path answers `cycled`. */
|
||||
function conversionKeepsSource(
|
||||
lowerer: Lowerer,
|
||||
target: IrType,
|
||||
scan: KeepsSourceScan,
|
||||
): KeepsSourceAnswer {
|
||||
if (target.kind === "record")
|
||||
return { keeps: recordMayKeepSource(lowerer, target), cycled: false };
|
||||
if (target.kind === "promise") return conversionKeepsSource(lowerer, target.inner, scan);
|
||||
if (target.kind === "array") return conversionKeepsSource(lowerer, target.elem, scan);
|
||||
if (target.kind !== "union") return { keeps: false, cycled: false };
|
||||
const known = scan.memo.get(target.unionId);
|
||||
if (target.kind === "record" && recordMayKeepSource(lowerer, target))
|
||||
return { keeps: true, cycled: false };
|
||||
let id: string;
|
||||
let members: IrType[];
|
||||
if (target.kind === "union") {
|
||||
id = target.unionId;
|
||||
members = lowerer.unions.get(target.unionId)?.arms ?? [];
|
||||
} else if (target.kind === "record") {
|
||||
id = target.shapeId;
|
||||
const shape = lowerer.shapes.get(target.shapeId);
|
||||
members = [
|
||||
...(shape?.fields.map((field) => field.type) ?? []),
|
||||
...(shape?.indexValue !== undefined ? [shape.indexValue] : []),
|
||||
];
|
||||
} else return { keeps: false, cycled: false };
|
||||
const known = scan.memo.get(id);
|
||||
if (known !== undefined) return { keeps: known, cycled: false };
|
||||
if (scan.visiting.has(target.unionId)) return { keeps: false, cycled: true };
|
||||
const union = lowerer.unions.get(target.unionId);
|
||||
if (union === undefined) return { keeps: false, cycled: false };
|
||||
scan.visiting.add(target.unionId);
|
||||
if (scan.visiting.has(id)) return { keeps: false, cycled: true };
|
||||
scan.visiting.add(id);
|
||||
let keeps = false;
|
||||
let cycled = false;
|
||||
for (const arm of union.arms) {
|
||||
const answer = conversionKeepsSource(lowerer, arm, scan);
|
||||
for (const member of members) {
|
||||
const answer = conversionKeepsSource(lowerer, member, scan);
|
||||
keeps = keeps || answer.keeps;
|
||||
cycled = cycled || answer.cycled;
|
||||
if (keeps) break;
|
||||
}
|
||||
scan.visiting.delete(target.unionId);
|
||||
if (!cycled) scan.memo.set(target.unionId, keeps);
|
||||
return { keeps, cycled };
|
||||
scan.visiting.delete(id);
|
||||
if (!cycled || keeps) scan.memo.set(id, keeps);
|
||||
return { keeps, cycled: cycled && !keeps };
|
||||
}
|
||||
|
||||
/** Whether `fn` holds a validated conversion that can note a record source:
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/record-source-keys-index-values.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// A checked conversion into an index-signature record whose values are
|
||||
// closed records keeps each value's undeclared source keys, so converting
|
||||
// such a value into an index-signature record later copies them. This
|
||||
// program has no checked conversion into a closed record itself, so the
|
||||
// capture learns that kept sources are possible from the index signature's
|
||||
// values. Node prints "extra,x".
|
||||
type H = Record<string, string | null>;
|
||||
interface T {
|
||||
transformHeaders?: (h: H) => H | Promise<H>;
|
||||
}
|
||||
const parsed: Record<string, { x: string }> = JSON.parse('{"p":{"x":"1","extra":"e"}}');
|
||||
async function viaArrow<O extends T>(o: O | undefined): Promise<H> {
|
||||
let h: H = { accept: "a" };
|
||||
if (o?.transformHeaders) h = await o.transformHeaders(h);
|
||||
return h;
|
||||
}
|
||||
viaArrow({ transformHeaders: async (h) => parsed["p"]! }).then((r) =>
|
||||
console.log(Object.keys(r).sort().join(",")),
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 1959, "san": 3000 }
|
||||
Reference in New Issue
Block a user