mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-02 07:43:42 +08:00
Retry declarations a probe flushed at the statement that commits them; keep the dyn field policy outside reached classes
A flush inside a capture buffer cannot retry (a probe that later declines must see today's answer), so its copies of the declaration's diagnostics reach the build when the statement commits. That statement commit point now reopens such a declaration: when its diagnostics are retriable, they leave the build, the declaration retries, and the statement lowers again. Probe copies of a declaration that already registered are dropped. The 'any'-typed class field refusal stays a fence at collection and is lifted by the flush retry, so only reached classes change; the coverage remainder registers the declarations reachable emit registered, and maps the degraded types alike. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8b07790325
commit
f0eb427152
@@ -153,7 +153,8 @@ export type TypeFailureFamily =
|
||||
| "component"
|
||||
| "stdlib-nominal"
|
||||
| "record-member"
|
||||
| "unsupported";
|
||||
| "unsupported"
|
||||
| "dyn-field-policy";
|
||||
|
||||
/** A type failure's structured payload: what the commit points of the
|
||||
* localized-failure design inspect (PoisonError.failure carries the same
|
||||
|
||||
@@ -2541,6 +2541,19 @@ export function collectClassShapeInner(
|
||||
if (initializerSymbol && (initializerSymbol.flags & ts.SymbolFlags.Enum) !== 0) continue;
|
||||
const type = lowerer.irTypeOf(member.name);
|
||||
if (type.kind === "void") lowerer.badType(member.name, lowerer.typeOf(member.name));
|
||||
if (
|
||||
type.kind === "dyn" &&
|
||||
!lowerer.liftDynFieldPolicy &&
|
||||
!isJsSourceFile(ts.getSourceFileOfNode(member)) &&
|
||||
!lowerer.classFieldStoresDyn(lowerer.typeOf(member.name)) &&
|
||||
(member.type !== undefined ||
|
||||
(lowerer.typeOf(member.name).flags & ts.TypeFlags.Any) === 0)
|
||||
) {
|
||||
lowerer.dynFieldPolicyFence(
|
||||
member.name,
|
||||
`${dynFieldTypeSpelling(lowerer, member.name)}-typed static fields`,
|
||||
);
|
||||
}
|
||||
staticFields.push({
|
||||
name: member.name.text,
|
||||
type,
|
||||
@@ -3044,9 +3057,22 @@ export function collectClassShapeInner(
|
||||
if (base && !fields.has(member.name.text) && lowerer.dynConvertible(type)) type = DYN;
|
||||
}
|
||||
if (type.kind === "void") lowerer.badType(member.name, lowerer.typeOf(member.name));
|
||||
// A field whose type maps to the checked dynamic (`any`, an
|
||||
// option bag, a degraded type) stores it natively, as JavaScript
|
||||
// classes and constructor-assigned fields do.
|
||||
// Bundled JS declares fields without annotations before assigning
|
||||
// them in the constructor. Use the same native checked-dynamic
|
||||
// storage as implicit constructor-assigned JS fields. In
|
||||
// TypeScript the refusal is a policy fence that a flush retry lifts
|
||||
// (Lowerer.liftDynFieldPolicy).
|
||||
if (
|
||||
type.kind === "dyn" &&
|
||||
!lowerer.liftDynFieldPolicy &&
|
||||
!isJsSourceFile(ts.getSourceFileOfNode(member)) &&
|
||||
!lowerer.classFieldStoresDyn(lowerer.typeOf(member.name))
|
||||
) {
|
||||
lowerer.dynFieldPolicyFence(
|
||||
member.name,
|
||||
`${dynFieldTypeSpelling(lowerer, member.name)}-typed class fields`,
|
||||
);
|
||||
}
|
||||
const undefinedInitializer =
|
||||
type.kind === "dyn" && !member.initializer ? locOf(member) : undefined;
|
||||
if (fields.has(member.name.text)) {
|
||||
@@ -3177,6 +3203,17 @@ export function collectClassShapeInner(
|
||||
const shape = lowerer.paramShape(p);
|
||||
const type = shape.bodyType ?? shape.type;
|
||||
if (type.kind === "void") lowerer.badType(p.name, lowerer.typeOf(p.name));
|
||||
// The class-field dyn rule verbatim (KEEP NARROW).
|
||||
if (
|
||||
type.kind === "dyn" &&
|
||||
!lowerer.liftDynFieldPolicy &&
|
||||
!lowerer.classFieldStoresDyn(lowerer.typeOf(p))
|
||||
) {
|
||||
lowerer.dynFieldPolicyFence(
|
||||
p.name,
|
||||
`${dynFieldTypeSpelling(lowerer, p)}-typed class fields`,
|
||||
);
|
||||
}
|
||||
// `override x` (and any same-named inherited member) would
|
||||
// redeclare a base slot — the declared-field rule verbatim.
|
||||
if (fields.has(name)) {
|
||||
@@ -8919,6 +8956,14 @@ export function throwingSetterFn(lowerer: Lowerer, info: ClassInfo, prop: string
|
||||
};
|
||||
}
|
||||
|
||||
/** The declared type a refused checked-dynamic class field names in its
|
||||
* diagnostic (`'any'`, `'WeakMap<object, string>'`): `unknown` and
|
||||
* `object` fields store checked-dynamic values, so the refusal is about
|
||||
* whatever else the declaration spells. */
|
||||
function dynFieldTypeSpelling(lowerer: Lowerer, node: ts.Node): string {
|
||||
return `'${lowerer.checker.typeToString(lowerer.typeOf(node))}'`;
|
||||
}
|
||||
|
||||
/** The base a constructor chain actually CALLS into: generic FAMILIES are
|
||||
* never constructed (no `%<family>.constructor` exists), so an
|
||||
* instantiation's construction-relevant base is the family's own base —
|
||||
|
||||
@@ -764,15 +764,16 @@ export function lowerStmts(lowerer: Lowerer, stmts: readonly ts.Statement[]): Ir
|
||||
// Localized failure's statement commit point (design §3.3): a
|
||||
// degradable type failure at a local, loop variable or module
|
||||
// global degrades that type and the statement lowers again.
|
||||
const slot =
|
||||
attempt < MAX_DEGRADE_RETRIES && !deferFences
|
||||
? lowerer.degradeAtCommit(e, STATEMENT_SLOTS)
|
||||
: null;
|
||||
if (slot === null) throw e;
|
||||
// A declaration this statement flushed inside a probe (where
|
||||
// no retry runs) can retry here too.
|
||||
if (attempt >= MAX_DEGRADE_RETRIES || deferFences) throw e;
|
||||
const slot = lowerer.degradeAtCommit(e, STATEMENT_SLOTS);
|
||||
if (slot === null && !lowerer.reopenFlushedAtCommit(diagsBefore)) throw e;
|
||||
// A failed attempt adds nothing to `out` but the TDZ boxes
|
||||
// forward captures declared (they stay declared), so the
|
||||
// retry appends after them.
|
||||
pendingSlots.push(slot);
|
||||
lowerer.dropAttemptDiags(diagsBefore);
|
||||
if (slot !== null) pendingSlots.push(slot);
|
||||
}
|
||||
}
|
||||
for (const slot of pendingSlots) lowerer.noteDynamicSlot(slot);
|
||||
|
||||
@@ -988,6 +988,9 @@ export interface LowererMode {
|
||||
* 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 }>;
|
||||
/** Localized failure: the declarations reachable emit registered with
|
||||
* dynamic slots (the coverage remainder registers them too). */
|
||||
retriedSymbols?: ReadonlySet<ts.Symbol>;
|
||||
/** LowerOptions.externalTypes, threaded through every lowering pass. */
|
||||
externalTypes?: ReadonlyMap<string, string>;
|
||||
/** LowerOptions.externalTypeSpecifiersByFile, shared by every pass. */
|
||||
@@ -1396,6 +1399,7 @@ export function lowerToIr(
|
||||
remainder: true,
|
||||
alreadyFlushed: resultLowerer.flushedSymbols,
|
||||
degradedTypes: resultLowerer.degradedTypes,
|
||||
retriedSymbols: resultLowerer.retriedSymbols,
|
||||
inlineRequireCycleCalls,
|
||||
targetPlatform,
|
||||
ffiImports,
|
||||
@@ -3779,6 +3783,10 @@ export class Lowerer {
|
||||
readonly degradedTypes = new Map<number, { lenient: IrType; code: string; reason: string }>();
|
||||
/** The dynamic slots the commit points created: one SC6104 each. */
|
||||
readonly dynamicSlots: DynamicSlot[] = [];
|
||||
/** Declarations retryDeferred registered. */
|
||||
readonly retriedSymbols = new Set<ts.Symbol>();
|
||||
/** LowererMode.retriedSymbols. */
|
||||
readonly priorRetriedSymbols: ReadonlySet<ts.Symbol>;
|
||||
/** Bodies run() skipped because their declaration had not registered. */
|
||||
readonly skippedBodies = new Set<ts.Node>();
|
||||
/** Declarations retryDeferred registered, in order: run() lowers the
|
||||
@@ -3790,6 +3798,9 @@ export class Lowerer {
|
||||
ts.Symbol,
|
||||
{ symbolOf: () => ts.Symbol | undefined; collect: () => void }
|
||||
>();
|
||||
/** True while a flush retry collects a declaration: the dyn class
|
||||
* field policy (dynFieldPolicyFence) is lifted for reached classes. */
|
||||
liftDynFieldPolicy = false;
|
||||
/** Collection diagnostics that a flush retry may simply run again (a
|
||||
* derived class whose base had not registered yet). */
|
||||
readonly retriableDiags = new WeakSet<ScrDiagnostic>();
|
||||
@@ -3878,6 +3889,7 @@ 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);
|
||||
this.priorRetriedSymbols = mode.retriedSymbols ?? new Set();
|
||||
this.externalTypes = mode.externalTypes ?? new Map();
|
||||
this.ffiBindingSymbols = mode.ffiBindingSymbols ?? null;
|
||||
this.ffiImports = mode.ffiImports ?? [];
|
||||
@@ -4949,7 +4961,14 @@ export class Lowerer {
|
||||
}
|
||||
|
||||
collectProgram(parts: FileParts[]): void {
|
||||
return collectProgram(this, parts);
|
||||
collectProgram(this, parts);
|
||||
// The coverage remainder registers the declarations reachable emit
|
||||
// registered with dynamic slots (retryDeferred), so it neither reports
|
||||
// them as unreached nor blocks their uses.
|
||||
for (const symbol of this.priorRetriedSymbols) {
|
||||
const diags = this.deferredDiags.get(symbol);
|
||||
if (diags !== undefined) this.retryDeferred(symbol, diags);
|
||||
}
|
||||
}
|
||||
|
||||
/** Names every file's %init and registers the run-once guard globals
|
||||
@@ -9346,6 +9365,9 @@ export class Lowerer {
|
||||
this.diagSink.push(d);
|
||||
return;
|
||||
}
|
||||
// A probe's copy of a declaration's flushed diagnostics, committed
|
||||
// after a flush retry registered the declaration: nothing to report.
|
||||
if (pending && this.retriedSymbols.has(pending.symbol)) return;
|
||||
if (pending) {
|
||||
this.commitFlush(pending.symbol, pending.source);
|
||||
const group = this.rejectedGroups.get(pending.symbol);
|
||||
@@ -9434,7 +9456,7 @@ export class Lowerer {
|
||||
const failure = e.failure;
|
||||
const type = e.failedType;
|
||||
if (failure === undefined || type === undefined) return null;
|
||||
if (!failure.degradable) return null;
|
||||
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.
|
||||
@@ -9467,6 +9489,51 @@ export class Lowerer {
|
||||
});
|
||||
}
|
||||
|
||||
/** Localized failure, statement commit point for declarations a probe
|
||||
* flushed: a statement committing the flushed diagnostics of a deferred
|
||||
* declaration (inside a capture buffer the flush could not retry) reopens
|
||||
* it here, outside every probe. When the declaration's diagnostics are
|
||||
* all retriable, they leave the build, the declaration retries
|
||||
* (retryDeferred), and on success the caller lowers the statement again.
|
||||
* Returns whether some declaration registered. */
|
||||
reopenFlushedAtCommit(diagsBefore: number): boolean {
|
||||
if (this.tierMode !== "auto" || this.diagSink !== null) return false;
|
||||
const groups = new Set<string>();
|
||||
for (let i = diagsBefore; i < this.diags.length; i++) {
|
||||
const group = this.diags[i]!.rejects;
|
||||
if (group !== undefined && !group.startsWith("statement @")) groups.add(group);
|
||||
}
|
||||
if (groups.size === 0) return false;
|
||||
let registered = false;
|
||||
for (const [symbol, group] of [...this.rejectedGroups]) {
|
||||
if (!groups.has(group) || !this.deferredCollectors.has(symbol)) continue;
|
||||
const committed = this.diags.filter((d) => d.rejects === group);
|
||||
this.diags.splice(0, this.diags.length, ...this.diags.filter((d) => d.rejects !== group));
|
||||
this.flushedSymbols.delete(symbol);
|
||||
this.rejectedGroups.delete(symbol);
|
||||
if (this.retryDeferred(symbol, committed)) {
|
||||
registered = true;
|
||||
continue;
|
||||
}
|
||||
// Not retriable: the declaration's diagnostics commit as before.
|
||||
const last = this.deferredDiags.get(symbol) ?? committed;
|
||||
this.deferredDiags.delete(symbol);
|
||||
this.flushedSymbols.add(symbol);
|
||||
this.noteRejected(symbol, last);
|
||||
for (const d of last) this.pushDiag(d);
|
||||
}
|
||||
return registered;
|
||||
}
|
||||
|
||||
/** A failed attempt's own diagnostics leave the build before its unit
|
||||
* lowers again; flushed declaration diagnostics (`rejects`) stay, since
|
||||
* the retry will not flush them a second time. */
|
||||
dropAttemptDiags(diagsBefore: number): void {
|
||||
const kept = this.diags.slice(diagsBefore).filter((d) => d.rejects !== undefined);
|
||||
this.diags.length = diagsBefore;
|
||||
this.diags.push(...kept);
|
||||
}
|
||||
|
||||
/** Records a dynamic slot whose unit lowered with it (one per site). */
|
||||
noteDynamicSlot(slot: DynamicSlot): void {
|
||||
const seen = this.dynamicSlots.some(
|
||||
@@ -9478,6 +9545,22 @@ export class Lowerer {
|
||||
if (!seen) this.dynamicSlots.push(slot);
|
||||
}
|
||||
|
||||
/** The SC1090 refusal of checked-dynamic class fields in TypeScript
|
||||
* (classFieldStoresDyn): a policy fence, not a missing representation.
|
||||
* A flush retry lifts it (liftDynFieldPolicy), so a reached class with
|
||||
* an 'any' field registers. */
|
||||
dynFieldPolicyFence(node: ts.Node, feature: string): never {
|
||||
const diag = unsupportedDiag("SC1090", locOf(node), feature);
|
||||
const failure = typeFailureOf(
|
||||
node,
|
||||
this.checker.typeToString(this.typeOf(node)),
|
||||
"dyn-field-policy",
|
||||
);
|
||||
diag.typeFailure = failure;
|
||||
this.pushDiag(diag);
|
||||
throw new PoisonError(failure, this.typeOf(node));
|
||||
}
|
||||
|
||||
badType(node: ts.Node, type: ts.Type): never {
|
||||
const widened = this.checker.getBaseTypeOfLiteralType(type);
|
||||
// Types declared by the ADOPTED @types/node (Buffer, NodeJS.Timeout,
|
||||
@@ -12454,6 +12537,12 @@ export class Lowerer {
|
||||
continue;
|
||||
}
|
||||
const failure = diag.typeFailure;
|
||||
// The dyn class field policy lifts during the retry; nothing to
|
||||
// degrade.
|
||||
if (failure?.family === "dyn-field-policy") {
|
||||
retriable = true;
|
||||
continue;
|
||||
}
|
||||
const type = failure === undefined ? undefined : this.failedTypes.get(failure);
|
||||
const id = (type as { id?: number } | undefined)?.id;
|
||||
if (
|
||||
@@ -12474,7 +12563,16 @@ export class Lowerer {
|
||||
slots.push({ loc: diag.loc, failure, code: diag.code, reason: diag.message });
|
||||
}
|
||||
this.deferredDiags.delete(symbol);
|
||||
if (this.collectDeferring(collector.symbolOf, collector.collect) === null) {
|
||||
const lifted = this.liftDynFieldPolicy;
|
||||
this.liftDynFieldPolicy = true;
|
||||
let registered: boolean;
|
||||
try {
|
||||
registered = this.collectDeferring(collector.symbolOf, collector.collect) === null;
|
||||
} finally {
|
||||
this.liftDynFieldPolicy = lifted;
|
||||
}
|
||||
if (registered) {
|
||||
this.retriedSymbols.add(symbol);
|
||||
this.deferredCollectors.delete(symbol);
|
||||
for (const [name, deferred] of this.deferredClassByName)
|
||||
if (deferred === symbol) this.deferredClassByName.delete(name);
|
||||
|
||||
@@ -34,6 +34,7 @@ const DEGRADABLE_FAMILIES: ReadonlySet<TypeFailureFamily> = new Set<TypeFailureF
|
||||
"component",
|
||||
"record-member",
|
||||
"unsupported",
|
||||
"dyn-field-policy",
|
||||
]);
|
||||
|
||||
function nameText(name: ts.Node | undefined): string | null {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
dynamic tier (tier assignment, report-only; --tier=auto)
|
||||
4 functions already operate on dynamic values (3 statements)
|
||||
Holder.constructor tests/coverage-fixtures/localized-failure.ts:28:1: dynObjLit at tests/coverage-fixtures/localized-failure.ts:29:16
|
||||
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
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user