mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Push localized failure down to the failing component of a declared type
Stage L3 of the localized-failure design. A degraded type used to become the checked dynamic as a whole slot. Its lenient mapping now keeps the compilable shape: the type maps again with every nested component whose mapping fails as the checked dynamic, so a record keeps its static members with one dynamic member, an array of records keeps its records, and a Map, Promise or function keeps its static parts. When even that shape does not map, or a unit still fails with it, the type falls back to the whole-slot checked dynamic (one more round). Lenient forms are IR types of one lowerer's shape tables, so the coverage remainder re-derives them. A pushed-down type is one SC6104, at the slot where it degraded, marked as keeping its shape. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
624d2bf873
commit
bb33ec50fe
@@ -650,6 +650,7 @@ export function coverageEnvelope(
|
||||
declaredType: slot.declaredType,
|
||||
code: slot.code,
|
||||
reason: slot.reason,
|
||||
...(slot.partial === true ? { partial: true } : {}),
|
||||
})),
|
||||
}
|
||||
: {}),
|
||||
@@ -763,7 +764,9 @@ function renderDynamicSlots(
|
||||
` dynamic slots ${slots.length} ${c(DIM, "(declared types with no static lowering, held as checked-dynamic values; SC6104)")}`,
|
||||
);
|
||||
for (const slot of slots.slice(0, DYNAMIC_SLOTS_SHOWN))
|
||||
out.push(` ${dynamicSlotLabel(slot)} ${c(DIM, where(slot.loc))}`);
|
||||
out.push(
|
||||
` ${dynamicSlotLabel(slot)}${slot.partial === true ? " (dynamic pieces)" : ""} ${c(DIM, where(slot.loc))}`,
|
||||
);
|
||||
if (slots.length > DYNAMIC_SLOTS_SHOWN)
|
||||
out.push(` ${c(DIM, `and ${slots.length - DYNAMIC_SLOTS_SHOWN} more`)}`);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,9 @@ export interface DynamicSlotEntry {
|
||||
/** The diagnostic the slot's declaration would have failed with. */
|
||||
code: string;
|
||||
reason: string;
|
||||
/** Push-down: the slot keeps its type's compilable shape, and only the
|
||||
* components that do not compile are checked-dynamic. */
|
||||
partial?: boolean;
|
||||
}
|
||||
|
||||
const SLOT_WORD: Record<TypeFailureSlot, string> = {
|
||||
@@ -38,7 +41,10 @@ export function dynamicSlotLabel(entry: DynamicSlotEntry): string {
|
||||
export function dynamicSlotDiag(entry: DynamicSlotEntry): ScrDiagnostic {
|
||||
return {
|
||||
code: DYNAMIC_SLOT_CODE,
|
||||
message: `${dynamicSlotLabel(entry)} is held as a checked-dynamic value: its type has no static lowering (${entry.reason}); reads into typed code are checked`,
|
||||
message:
|
||||
entry.partial === true
|
||||
? `${dynamicSlotLabel(entry)} keeps its compilable shape with checked-dynamic pieces where its type has no static lowering (${entry.reason}); reads of those pieces into typed code are checked`
|
||||
: `${dynamicSlotLabel(entry)} is held as a checked-dynamic value: its type has no static lowering (${entry.reason}); reads into typed code are checked`,
|
||||
loc: entry.loc,
|
||||
hint: "restate the declared type inside the compilable set to keep this slot static; scriptc coverage lists dynamic slots, and --fail-on=dynamic-slots fails on them",
|
||||
};
|
||||
|
||||
@@ -172,6 +172,7 @@ export interface DiagnosticsEnvelope {
|
||||
declaredType: string;
|
||||
code: string;
|
||||
reason: string;
|
||||
partial?: boolean;
|
||||
})[];
|
||||
/** coverage --explain-dynamic only: the report-only tier assignment. */
|
||||
tiers?: TiersEnvelope;
|
||||
|
||||
@@ -728,6 +728,9 @@ export interface DynamicSlot {
|
||||
failure: TypeFailure;
|
||||
code: string;
|
||||
reason: string;
|
||||
/** The slot keeps its type's compilable shape; only the failing
|
||||
* components are checked-dynamic (push-down, stage L3). */
|
||||
partial?: true;
|
||||
}
|
||||
|
||||
/** Localized failure: rounds of the declaration flush retry. */
|
||||
@@ -987,7 +990,7 @@ export interface LowererMode {
|
||||
/** Localized failure: the types reachable emit degraded (the coverage
|
||||
* remainder maps them alike, so the declarations reachable emit
|
||||
* registered with dynamic slots collect there too). */
|
||||
degradedTypes?: ReadonlyMap<number, { lenient: IrType; code: string; reason: string }>;
|
||||
degradedTypes?: ReadonlyMap<number, { lenient: IrType | null; code: string; reason: string }>;
|
||||
/** Localized failure: the declarations reachable emit registered with
|
||||
* dynamic slots (the coverage remainder registers them too). */
|
||||
retriedSymbols?: ReadonlySet<ts.Symbol>;
|
||||
@@ -1304,6 +1307,7 @@ export function lowerToIr(
|
||||
declaredType: slot.failure.declaredType,
|
||||
code: slot.code,
|
||||
reason: slot.reason,
|
||||
...(slot.partial ? { partial: true } : {}),
|
||||
}));
|
||||
result = {
|
||||
...result,
|
||||
@@ -3780,7 +3784,10 @@ export class Lowerer {
|
||||
* at a committed slot, with their lenient mapping. Filled only at the
|
||||
* commit points (degradeAtCommit), so a program that compiles today
|
||||
* never fills it. */
|
||||
readonly degradedTypes = new Map<number, { lenient: IrType; code: string; reason: string }>();
|
||||
readonly degradedTypes = new Map<
|
||||
number,
|
||||
{ lenient: IrType | null; code: string; reason: string }
|
||||
>();
|
||||
/** The dynamic slots the commit points created: one SC6104 each. */
|
||||
readonly dynamicSlots: DynamicSlot[] = [];
|
||||
/** Declarations retryDeferred registered. */
|
||||
@@ -3888,7 +3895,13 @@ export class Lowerer {
|
||||
) {
|
||||
// The mode's options, sorted by name; the values derived from them follow.
|
||||
this.alreadyFlushed = mode.alreadyFlushed ?? new Set();
|
||||
for (const [id, degraded] of mode.degradedTypes ?? []) this.degradedTypes.set(id, degraded);
|
||||
// Lenient forms are IR types of the other pass's shape tables: only the
|
||||
// whole-slot dynamic carries over as is.
|
||||
for (const [id, degraded] of mode.degradedTypes ?? [])
|
||||
this.degradedTypes.set(id, {
|
||||
...degraded,
|
||||
lenient: degraded.lenient?.kind === "dyn" ? DYN : null,
|
||||
});
|
||||
this.priorRetriedSymbols = mode.retriedSymbols ?? new Set();
|
||||
this.externalTypes = mode.externalTypes ?? new Map();
|
||||
this.ffiBindingSymbols = mode.ffiBindingSymbols ?? null;
|
||||
@@ -3927,7 +3940,17 @@ export class Lowerer {
|
||||
resolveBoundAliasTs: this.boundAliasTsResolver,
|
||||
degradedType: (type) => {
|
||||
const id = (type as { id?: number }).id;
|
||||
return id === undefined ? null : (this.degradedTypes.get(id)?.lenient ?? null);
|
||||
if (id === undefined) return null;
|
||||
const degraded = this.degradedTypes.get(id);
|
||||
if (degraded === undefined) return null;
|
||||
// An entry handed over from another pass (the coverage remainder)
|
||||
// re-derives its lenient form in this lowerer's shape tables; the
|
||||
// checked dynamic cuts the knot while it does.
|
||||
if (degraded.lenient === null) {
|
||||
degraded.lenient = DYN;
|
||||
degraded.lenient = this.lenientType(type);
|
||||
}
|
||||
return degraded.lenient;
|
||||
},
|
||||
isErasing: () => this.erasingDepth > 0,
|
||||
genericClassInstance: (decl, ref, mapArgument) =>
|
||||
@@ -9455,8 +9478,9 @@ export class Lowerer {
|
||||
if (!failure.degradable || failure.family === "dyn-field-policy") return null;
|
||||
if (!slots.has(failure.slot)) return null;
|
||||
const id = (type as { id?: number }).id;
|
||||
// Already degraded and still failing: no progress, commit as today.
|
||||
if (id === undefined || this.degradedTypes.has(id)) return null;
|
||||
// Already degraded to the whole-slot dynamic and still failing: no
|
||||
// progress, commit as today.
|
||||
if (id === undefined || this.degradedTypes.get(id)?.lenient?.kind === "dyn") return null;
|
||||
let committed: ScrDiagnostic | undefined;
|
||||
for (let i = this.diags.length - 1; i >= 0; i--) {
|
||||
if (this.diags[i]!.typeFailure !== failure) continue;
|
||||
@@ -9464,8 +9488,15 @@ export class Lowerer {
|
||||
this.diags.splice(i, 1);
|
||||
}
|
||||
if (committed === undefined) return null;
|
||||
this.degradedTypes.set(id, { lenient: DYN, code: committed.code, reason: committed.message });
|
||||
return { loc: committed.loc, failure, code: committed.code, reason: committed.message };
|
||||
this.degradeType(type, committed.code, committed.message);
|
||||
const partial = this.degradedTypes.get(id)?.lenient?.kind !== "dyn";
|
||||
return {
|
||||
...(partial ? { partial: true as const } : {}),
|
||||
loc: committed.loc,
|
||||
failure,
|
||||
code: committed.code,
|
||||
reason: committed.message,
|
||||
};
|
||||
}
|
||||
|
||||
/** A declaration slot whose type maps through degradedTypes is a
|
||||
@@ -9474,7 +9505,9 @@ export class Lowerer {
|
||||
noteDegradedSlot(node: ts.Node, type: ts.Type): void {
|
||||
if (this.diagSink !== null) return;
|
||||
const degraded = this.degradedTypes.get((type as { id?: number }).id ?? -1);
|
||||
if (degraded === undefined) return;
|
||||
// A pushed-down type is one slot, reported where it degraded; only
|
||||
// whole-slot dynamic declarations are reported one by one.
|
||||
if (degraded === undefined || degraded.lenient?.kind !== "dyn") return;
|
||||
const failure = typeFailureOf(node, this.checker.typeToString(type), "unsupported");
|
||||
if (failure.slot === "expression" || failure.slot === "member") return;
|
||||
this.noteDynamicSlot({
|
||||
@@ -9554,6 +9587,33 @@ export class Lowerer {
|
||||
}
|
||||
}
|
||||
|
||||
/** Localized failure's lenient mapping (stage L3, push-down): the type
|
||||
* mapped with each failing component as the checked dynamic, so a record
|
||||
* keeps its static members, an array of records its records, and a
|
||||
* function its arity. The whole slot is the checked dynamic when even
|
||||
* that shape does not map. Throwaway context: nothing memoizes. */
|
||||
lenientType(type: ts.Type): IrType {
|
||||
const { typeMemo: _strictMemo, ...strict } = this.typeCtx;
|
||||
const mapped = mapType(type, { ...strict, lenient: true, canMemoizeType: () => false });
|
||||
return mapped === null || mapped.kind === "void" ? DYN : mapped;
|
||||
}
|
||||
|
||||
/** Degrades a type for a commit point: its lenient (pushed-down) form
|
||||
* first; a type already degraded that still fails falls back to the
|
||||
* whole-slot checked dynamic. False when nothing is left to degrade. */
|
||||
private degradeType(type: ts.Type, code: string, reason: string): boolean {
|
||||
const id = (type as { id?: number }).id;
|
||||
if (id === undefined) return false;
|
||||
const known = this.degradedTypes.get(id);
|
||||
if (known !== undefined) {
|
||||
if (known.lenient?.kind === "dyn") return false;
|
||||
this.degradedTypes.set(id, { lenient: DYN, code, reason });
|
||||
return true;
|
||||
}
|
||||
this.degradedTypes.set(id, { lenient: this.lenientType(type), code, reason });
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Records a dynamic slot whose unit lowered with it (one per site). */
|
||||
noteDynamicSlot(slot: DynamicSlot): void {
|
||||
const seen = this.dynamicSlots.some(
|
||||
@@ -12549,7 +12609,7 @@ export class Lowerer {
|
||||
const slots: DynamicSlot[] = [];
|
||||
let current = diags;
|
||||
for (let round = 0; round < MAX_DECLARATION_RETRIES; round++) {
|
||||
const failing: { diag: ScrDiagnostic; failure: TypeFailure; id: number }[] = [];
|
||||
const failing: { diag: ScrDiagnostic; failure: TypeFailure; type: ts.Type }[] = [];
|
||||
let retriable = false;
|
||||
for (const diag of current) {
|
||||
if (this.retriableDiags.has(diag)) {
|
||||
@@ -12567,20 +12627,29 @@ export class Lowerer {
|
||||
const id = (type as { id?: number } | undefined)?.id;
|
||||
if (
|
||||
failure === undefined ||
|
||||
type === undefined ||
|
||||
id === undefined ||
|
||||
!failure.degradable ||
|
||||
!DECLARATION_SLOTS.has(failure.slot) ||
|
||||
this.degradedTypes.has(id)
|
||||
this.degradedTypes.get(id)?.lenient?.kind === "dyn"
|
||||
)
|
||||
return false;
|
||||
failing.push({ diag, failure, id });
|
||||
failing.push({ diag, failure, type });
|
||||
}
|
||||
// A round makes progress by degrading a type, or (once) by running
|
||||
// a collection that only waited for another declaration.
|
||||
if (failing.length === 0 && !(retriable && round === 0)) return false;
|
||||
for (const { diag, failure, id } of failing) {
|
||||
this.degradedTypes.set(id, { lenient: DYN, code: diag.code, reason: diag.message });
|
||||
slots.push({ loc: diag.loc, failure, code: diag.code, reason: diag.message });
|
||||
for (const { diag, failure, type } of failing) {
|
||||
this.degradeType(type, diag.code, diag.message);
|
||||
const partial =
|
||||
this.degradedTypes.get((type as { id?: number }).id ?? -1)?.lenient?.kind !== "dyn";
|
||||
slots.push({
|
||||
loc: diag.loc,
|
||||
failure,
|
||||
code: diag.code,
|
||||
reason: diag.message,
|
||||
...(partial ? { partial: true as const } : {}),
|
||||
});
|
||||
}
|
||||
this.deferredDiags.delete(symbol);
|
||||
const lifted = this.liftDynFieldPolicy;
|
||||
@@ -13066,8 +13135,7 @@ export class Lowerer {
|
||||
/* ── scoping and captures ─────────────────────────────────────────── */
|
||||
|
||||
declareLocal(nameNode: ts.Node, name: string, type: IrType, mutable: boolean): IrLocal {
|
||||
if (type.kind === "dyn" && this.degradedTypes.size > 0)
|
||||
this.noteDegradedSlot(nameNode, this.typeOf(nameNode));
|
||||
if (this.degradedTypes.size > 0) this.noteDegradedSlot(nameNode, this.typeOf(nameNode));
|
||||
const symbol = this.checker.getSymbolAtLocation(nameNode);
|
||||
const local = declareContextLocal(
|
||||
this.ctx,
|
||||
|
||||
@@ -1287,6 +1287,11 @@ export interface TypeMapperCtx {
|
||||
* only after the strict mapping (and its bound-alias retry) returns null,
|
||||
* at every depth, so every occurrence of a degraded type maps alike. */
|
||||
degradedType?: (type: ts.Type) => IrType | null;
|
||||
/** Localized failure's lenient mapping (Lowerer.lenientType): a nested
|
||||
* component whose mapping fails maps to the checked dynamic instead, so a
|
||||
* failing type keeps its outer shape with dynamic pieces. Only ever set
|
||||
* on a throwaway context without memoization. */
|
||||
lenient?: boolean;
|
||||
/** GENERIC program classes (monomorphization by flow): the mapping of a
|
||||
* concrete instantiation reference (`Box<number>`) — the Lowerer
|
||||
* registers/reuses the instantiation (`Box%0`) and answers its object
|
||||
@@ -1871,6 +1876,12 @@ export function mapType(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
contextResolutions++;
|
||||
}
|
||||
}
|
||||
// Push-down (stage L3): in a lenient mapping, the smallest failing
|
||||
// component is the checked dynamic.
|
||||
if (mapped === null && ctx.lenient === true && !topLevel) {
|
||||
mapped = DYN;
|
||||
contextResolutions++;
|
||||
}
|
||||
if (
|
||||
mapped !== null &&
|
||||
memoKey !== null &&
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
// Localized failure, stage L3: push-down. A declared type that does not
|
||||
// compile keeps its compilable shape; only the failing component is a
|
||||
// checked dynamic piece.
|
||||
type Hook = Map<string, (x: number) => string> | AbortSignal | (() => number) | Set<string>;
|
||||
|
||||
interface Plugin {
|
||||
name: string;
|
||||
priority: number;
|
||||
hook: Hook;
|
||||
}
|
||||
|
||||
class Registry {
|
||||
plugins: Plugin[] = [];
|
||||
byName = new Map<string, Plugin>();
|
||||
pending: Promise<Plugin> | undefined;
|
||||
add(p: Plugin): void {
|
||||
this.plugins.push(p);
|
||||
this.byName.set(p.name, p);
|
||||
}
|
||||
total(): number {
|
||||
let sum = 0;
|
||||
for (const p of this.plugins) sum += p.priority;
|
||||
return sum;
|
||||
}
|
||||
names(): string {
|
||||
return this.plugins.map((p) => p.name).join(",");
|
||||
}
|
||||
run(name: string): string {
|
||||
const p = this.byName.get(name);
|
||||
if (p === undefined) return "missing";
|
||||
const h = p.hook;
|
||||
return typeof h === "function" ? `${p.name}=${h()}` : h instanceof Set ? `${p.name}#${h.size}` : "other";
|
||||
}
|
||||
}
|
||||
|
||||
type Handler = (event: string, extra: Hook) => number;
|
||||
function dispatch(handlers: Handler[], event: string): number {
|
||||
let n = 0;
|
||||
for (const h of handlers) n += h(event, () => event.length);
|
||||
return n;
|
||||
}
|
||||
|
||||
const r = new Registry();
|
||||
const counter: Plugin = { name: "counter", priority: 2, hook: () => 41 };
|
||||
r.add(counter);
|
||||
r.add({ name: "tags", priority: 5, hook: new Set(["a", "b"]) });
|
||||
console.log(r.total(), r.names(), r.run("counter"), r.run("tags"), r.run("x"));
|
||||
counter.priority = 10;
|
||||
console.log(r.total(), r.byName.get("counter") === counter, JSON.stringify(r.plugins.map((p) => [p.name, p.priority])));
|
||||
r.pending = Promise.resolve(counter);
|
||||
r.pending.then((p) => console.log("pending", p.name, p === counter));
|
||||
console.log(dispatch([(e, x) => e.length, (e, x) => (typeof x === "function" ? x() * 2 : 0)], "hello"));
|
||||
@@ -1,9 +1,6 @@
|
||||
dynamic tier (tier assignment, report-only; --tier=auto)
|
||||
4 functions already operate on dynamic values (3 statements)
|
||||
Session.constructor tests/coverage-fixtures/localized-failure.ts:12:1: dynArrLit at tests/coverage-fixtures/localized-failure.ts:13:27
|
||||
Session.count tests/coverage-fixtures/localized-failure.ts:14:3: %dyn.class.property at tests/coverage-fixtures/localized-failure.ts:15:12
|
||||
1 function already operates on dynamic values (1 statement)
|
||||
Holder.constructor tests/coverage-fixtures/localized-failure.ts:28:1: dynObjLit at tests/coverage-fixtures/localized-failure.ts:29:16
|
||||
%dyn.class.property.0 tests/coverage-fixtures/localized-failure.ts:15:12: %dyn.class.readPropertiesOrEmpty at tests/coverage-fixtures/localized-failure.ts:15:12
|
||||
|
||||
2 demotion requests (refusals and fences the dynamic tier would replace)
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4000 }
|
||||
Reference in New Issue
Block a user