mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-02 07:43:42 +08:00
Report which type failures localized failure would turn into dynamic slots
Stage L0 of the localized-failure design, report-only: no lowering changes. A type with no static lowering poisons the slot that holds it, and the poison rejects the declaration around it (a class, a signature, a declaration statement); each later use then fails on its own line. This records, on the committed diagnostics, where each type failure sits, whether a checked dynamic slot would keep the program's meaning, and which rejected declarations each failing statement used. coverage --explain-dynamic turns that into the would-be dynamic slot list with the cascade each slot clears, and the counts of errors that would disappear through stages L2 and L4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
68151efd37
commit
d74cc3b8bd
@@ -24,7 +24,11 @@ import {
|
||||
import { renderDiagnostics } from "../diagnostics/render.js";
|
||||
import type { LowerStats } from "../frontend/lowering/lowerer.js";
|
||||
import type { ProvenanceSources } from "../frontend/provenance-registry.js";
|
||||
import type { DemotionUnit, TierReport } from "../frontend/tier-analysis.js";
|
||||
import type {
|
||||
DemotionUnit,
|
||||
LocalizedFailureReport,
|
||||
TierReport,
|
||||
} from "../frontend/tier-analysis.js";
|
||||
import type { WasmModuleReport } from "../wasm/discover.js";
|
||||
import {
|
||||
describe as describeReason,
|
||||
@@ -668,6 +672,26 @@ function tiersEnvelope(tiers: TierReport, locator: SourceLocator): TiersEnvelope
|
||||
...at(r.loc),
|
||||
})),
|
||||
units: tiers.units.map((u) => ({ ...unit(u), requests: u.requests, sites: u.sites })),
|
||||
localizedFailure: {
|
||||
errors: tiers.localizedFailure.errors,
|
||||
typeFailures: tiers.localizedFailure.typeFailures,
|
||||
cascades: tiers.localizedFailure.cascades,
|
||||
clearedThroughL2: tiers.localizedFailure.clearedThroughL2,
|
||||
clearedThroughL4: tiers.localizedFailure.clearedThroughL4,
|
||||
remainingByCode: tiers.localizedFailure.remainingByCode,
|
||||
slots: tiers.localizedFailure.slots.map((slot) => ({
|
||||
...at(slot.loc),
|
||||
slot: slot.slot,
|
||||
name: slot.name,
|
||||
declaredType: slot.declaredType,
|
||||
family: slot.family,
|
||||
code: slot.code,
|
||||
message: slot.message,
|
||||
stage: slot.stage,
|
||||
...(slot.rejects !== null ? { rejects: slot.rejects } : {}),
|
||||
cascade: slot.cascade,
|
||||
})),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -695,6 +719,39 @@ function unitMatches(u: DemotionUnit, query: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
const SLOTS_SHOWN = 40;
|
||||
|
||||
/** The localized-failure section of --explain-dynamic (report-only): the
|
||||
* committed type failures that would become checked dynamic slots, and
|
||||
* the cascade each one causes today. */
|
||||
function renderLocalizedFailure(
|
||||
report: LocalizedFailureReport,
|
||||
out: string[],
|
||||
c: (code: string, s: string) => string,
|
||||
where: (loc: { file: string; start: number }) => string,
|
||||
): void {
|
||||
if (report.typeFailures.degradable === 0 && report.typeFailures.refused === 0) return;
|
||||
out.push("");
|
||||
out.push(
|
||||
` ${report.slots.length} would-be dynamic slot${report.slots.length === 1 ? "" : "s"} ${c(DIM, "(localized failure, report-only: type failures a checked dynamic slot would hold)")}`,
|
||||
);
|
||||
out.push(
|
||||
` ${c(DIM, `${report.errors} errors: ${report.clearedThroughL2} clear with declaration slots (L1-L2), ${report.clearedThroughL4} with member and expression slots too (L3-L4); ${report.cascades} cascade${report.cascades === 1 ? "" : "s"} attributed; ${report.typeFailures.refused} type failure${report.typeFailures.refused === 1 ? " stays" : "s stay"} refused`)}`,
|
||||
);
|
||||
for (const slot of report.slots.slice(0, SLOTS_SHOWN)) {
|
||||
const cascade = slot.cascade > 0 ? `, clears ${slot.cascade}` : "";
|
||||
out.push(
|
||||
` ${slot.stage} ${slot.slot} ${slot.name}: ${capMessage(slot.declaredType)} ${c(DIM, `${where(slot.loc)} (${slot.code} ${slot.family}${cascade})`)}`,
|
||||
);
|
||||
if (slot.rejects !== null)
|
||||
out.push(` ${c(DIM, `rejects ${slot.rejects.split(" @")[0]}`)}`);
|
||||
}
|
||||
if (report.slots.length > SLOTS_SHOWN)
|
||||
out.push(
|
||||
` ${c(DIM, `${report.slots.length - SLOTS_SHOWN} more (--print=diagnostics lists all)`)}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** `scriptc coverage --explain-dynamic[=<unit>]`: the dynamic tier today
|
||||
* and the demotion chains the report-only analysis computed. With a query,
|
||||
* only units whose name matches it (`x`, `Point.x`, `parse:x`). */
|
||||
@@ -735,6 +792,7 @@ export function renderExplainDynamic(
|
||||
(excluded > 0 ? c(DIM, `, ${excluded} excluded by policy`) : ""),
|
||||
);
|
||||
}
|
||||
if (query === undefined) renderLocalizedFailure(tiers.localizedFailure, out, c, where);
|
||||
const units = tiers.units.filter((u) => query === undefined || unitMatches(u, query));
|
||||
if (query !== undefined && units.length === 0) {
|
||||
out.push(` no demoted unit matches '${query}'`);
|
||||
|
||||
@@ -110,6 +110,64 @@ export interface ScrDiagnostic {
|
||||
* and class member body: it throws when the module initializes, which is
|
||||
* program startup for a module the entry imports. */
|
||||
startup?: true;
|
||||
/** Localized failure, report-only (stage L0): set where a type with no
|
||||
* static lowering poisoned a slot (Lowerer.badType and the dyn class
|
||||
* field policy). Never part of the rendered message. */
|
||||
typeFailure?: TypeFailure;
|
||||
/** Localized failure, report-only: the declaration this diagnostic
|
||||
* rejected, as a failure-group key. Set when a reference flushes a
|
||||
* deferred declaration's diagnostics, and on the diagnostics of a
|
||||
* declaration statement whose bindings were blocked. */
|
||||
rejects?: string;
|
||||
/** Localized failure, report-only: the failure groups (rejected
|
||||
* declarations, blocked statements) that the statement committing this
|
||||
* diagnostic used. A cascade clears when every one of them would
|
||||
* degrade instead of failing. */
|
||||
cause?: readonly string[];
|
||||
}
|
||||
|
||||
/** Where a type failure sits (localized-failure design §3.6): a
|
||||
* declaration slot, a member of a declared type, or an expression's own
|
||||
* type. */
|
||||
export type TypeFailureSlot =
|
||||
| "field"
|
||||
| "param"
|
||||
| "return"
|
||||
| "local"
|
||||
| "global"
|
||||
| "loop"
|
||||
| "member"
|
||||
| "expression";
|
||||
|
||||
/** Which badType branch (or policy fence) refused the type. */
|
||||
export type TypeFailureFamily =
|
||||
| "node-types"
|
||||
| "fetch-ambient"
|
||||
| "enum-object"
|
||||
| "stdlib-index"
|
||||
| "index-signature"
|
||||
| "package"
|
||||
| "generic-signature"
|
||||
| "overloads"
|
||||
| "intersection"
|
||||
| "component"
|
||||
| "stdlib-nominal"
|
||||
| "record-member"
|
||||
| "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
|
||||
* object). `degradable` is the §3.7 classification: a checked-dynamic slot
|
||||
* would keep the program's meaning. */
|
||||
export interface TypeFailure {
|
||||
slot: TypeFailureSlot;
|
||||
/** The slot's name (`_customTools`, `listAll`, `x`), or the expression
|
||||
* text for expression slots. */
|
||||
name: string;
|
||||
declaredType: string;
|
||||
family: TypeFailureFamily;
|
||||
degradable: boolean;
|
||||
}
|
||||
|
||||
/** SC3002–SC3004 — native-helper target, installation, and execution
|
||||
|
||||
@@ -114,6 +114,27 @@ export interface TiersEnvelope {
|
||||
message: string;
|
||||
})[];
|
||||
units: (TierUnitPosition & { requests: number; sites: number })[];
|
||||
/** Localized failure (report-only): committed type failures that would
|
||||
* become checked dynamic slots, and what they would clear. */
|
||||
localizedFailure: {
|
||||
errors: number;
|
||||
typeFailures: { degradable: number; refused: number };
|
||||
cascades: number;
|
||||
clearedThroughL2: number;
|
||||
clearedThroughL4: number;
|
||||
remainingByCode: Record<string, number>;
|
||||
slots: (SourcePosition & {
|
||||
slot: string;
|
||||
name: string;
|
||||
declaredType: string;
|
||||
family: string;
|
||||
code: string;
|
||||
message: string;
|
||||
stage: string;
|
||||
rejects?: string;
|
||||
cascade: number;
|
||||
})[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface DiagnosticsEnvelope {
|
||||
|
||||
@@ -2542,8 +2542,7 @@ export function collectClassShapeInner(
|
||||
(member.type !== undefined ||
|
||||
(lowerer.typeOf(member.name).flags & ts.TypeFlags.Any) === 0)
|
||||
) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
lowerer.dynFieldPolicyFence(
|
||||
member.name,
|
||||
`${dynFieldTypeSpelling(lowerer, member.name)}-typed static fields`,
|
||||
);
|
||||
@@ -3059,8 +3058,7 @@ export function collectClassShapeInner(
|
||||
!isJsSourceFile(ts.getSourceFileOfNode(member)) &&
|
||||
!lowerer.classFieldStoresDyn(lowerer.typeOf(member.name))
|
||||
) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
lowerer.dynFieldPolicyFence(
|
||||
member.name,
|
||||
`${dynFieldTypeSpelling(lowerer, member.name)}-typed class fields`,
|
||||
);
|
||||
@@ -3197,8 +3195,7 @@ export function collectClassShapeInner(
|
||||
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.classFieldStoresDyn(lowerer.typeOf(p))) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
lowerer.dynFieldPolicyFence(
|
||||
p.name,
|
||||
`${dynFieldTypeSpelling(lowerer, p)}-typed class fields`,
|
||||
);
|
||||
|
||||
@@ -41,6 +41,8 @@ import { isTimeValueDateDeclaration } from "./date-values.js";
|
||||
import { bindingSource } from "../binding-source.js";
|
||||
import { lowerConstructorReturn } from "./constructor-returns.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import type { ScrDiagnostic } from "../../diagnostics/diagnostic.js";
|
||||
import { statementGroupKey } from "./type-failure.js";
|
||||
import {
|
||||
cjsExportValueByName,
|
||||
cjsNamespaceDestructureOf,
|
||||
@@ -750,6 +752,8 @@ export function lowerStmts(lowerer: Lowerer, stmts: readonly ts.Statement[]): Ir
|
||||
if (!lowerer.suppressStats) lowerer.countStatement(stmt, sf?.fileName);
|
||||
const diagsBefore = lowerer.diags.length;
|
||||
const sinkMark = lowerer.diagSinkMark();
|
||||
const touched = new Set<string>();
|
||||
lowerer.failureTouches.push(touched);
|
||||
try {
|
||||
let lowered: IrStmt | IrStmt[] | null;
|
||||
let appended = false;
|
||||
@@ -813,7 +817,17 @@ export function lowerStmts(lowerer: Lowerer, stmts: readonly ts.Statement[]): Ir
|
||||
throw e;
|
||||
}
|
||||
if (!lowerer.suppressStats) lowerer.countStatementFailed(stmt, sf?.fileName);
|
||||
lowerer.noteBlockedBindings(stmt);
|
||||
// Localized failure (report-only): the statement's committed
|
||||
// diagnostics that are not themselves type failures or rejected
|
||||
// declarations are cascades of the rejected declarations it used.
|
||||
const committed =
|
||||
lowerer.diagSink === null && !deferFences ? lowerer.diags.slice(diagsBefore) : [];
|
||||
if (touched.size > 0) {
|
||||
const cause = [...touched];
|
||||
for (const d of committed)
|
||||
if (d.typeFailure === undefined && d.rejects === undefined) d.cause ??= cause;
|
||||
}
|
||||
lowerer.noteBlockedBindings(stmt, committed);
|
||||
// The runtime-fence conversion (JS sources, direct diagnostics
|
||||
// only — a capture wrapper in force means collection owns the
|
||||
// report). The statement's diagnostics move OFF the build and
|
||||
@@ -837,6 +851,8 @@ export function lowerStmts(lowerer: Lowerer, stmts: readonly ts.Statement[]): Ir
|
||||
const fence = lowerer.capturedRuntimeFence(sinkMark, stmt);
|
||||
if (fence) out.push(fence);
|
||||
}
|
||||
} finally {
|
||||
lowerer.failureTouches.pop();
|
||||
}
|
||||
}
|
||||
// Each acquisition protects the remainder of its lexical statement
|
||||
@@ -1522,7 +1538,11 @@ function noteEarlyReadVar(lowerer: Lowerer, symbol: ts.Symbol, local: IrLocal):
|
||||
* lowerVarDecl's salvage path declares the local when it can, and
|
||||
* resolution checks locals/globals before the fallthroughs consult
|
||||
* blockedBindings. */
|
||||
export function noteBlockedBindings(lowerer: Lowerer, stmt: ts.Statement): void {
|
||||
export function noteBlockedBindings(
|
||||
lowerer: Lowerer,
|
||||
stmt: ts.Statement,
|
||||
diags: readonly ScrDiagnostic[] = [],
|
||||
): void {
|
||||
let list: ts.VariableDeclarationList | null = null;
|
||||
if (ts.isVariableStatement(stmt)) list = stmt.declarationList;
|
||||
else if (
|
||||
@@ -1533,12 +1553,20 @@ export function noteBlockedBindings(lowerer: Lowerer, stmt: ts.Statement): void
|
||||
list = stmt.initializer;
|
||||
}
|
||||
if (!list) return;
|
||||
// Localized failure (report-only): the bindings form one failure group,
|
||||
// the statement, whose diagnostics are the group's.
|
||||
const group = statementGroupKey(stmt);
|
||||
let bound = false;
|
||||
for (const decl of list.declarations.nodes) {
|
||||
for (const nameNode of boundIdentifiersOf(decl.name)) {
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(nameNode);
|
||||
if (symbol) lowerer.blockedBindings.add(symbol);
|
||||
if (!symbol) continue;
|
||||
lowerer.blockedBindings.add(symbol);
|
||||
if (!lowerer.rejectedGroups.has(symbol)) lowerer.rejectedGroups.set(symbol, group);
|
||||
bound = true;
|
||||
}
|
||||
}
|
||||
if (bound) for (const d of diags) d.rejects ??= group;
|
||||
}
|
||||
|
||||
/** True when a reference/assignment fell through resolution because the
|
||||
@@ -1548,12 +1576,13 @@ export function noteBlockedBindings(lowerer: Lowerer, stmt: ts.Statement): void
|
||||
* flushed by the emit pass). */
|
||||
export function isBlockedBinding(lowerer: Lowerer, symbol: ts.Symbol | null): boolean {
|
||||
if (!symbol) return false;
|
||||
return (
|
||||
const blocked =
|
||||
lowerer.blockedBindings.has(symbol) ||
|
||||
lowerer.deferredDiags.has(symbol) ||
|
||||
lowerer.flushedSymbols.has(symbol) ||
|
||||
lowerer.alreadyFlushed.has(symbol)
|
||||
);
|
||||
lowerer.alreadyFlushed.has(symbol);
|
||||
if (blocked) lowerer.touchRejected(symbol);
|
||||
return blocked;
|
||||
}
|
||||
|
||||
/** Lowers a statement in a fresh lexical scope (if/while/for bodies). */
|
||||
|
||||
@@ -204,7 +204,12 @@ import { isBundledShimForwarder } from "../package-shims.js";
|
||||
import { erasedTypeOnlyImport } from "../type-only-imports.js";
|
||||
import { literalValues } from "../literal-values.js";
|
||||
import { discriminantOwners, literalUnionArm, type UnionLiteral } from "../union-discriminants.js";
|
||||
import type { ScrDiagnostic } from "../../diagnostics/diagnostic.js";
|
||||
import type {
|
||||
ScrDiagnostic,
|
||||
TypeFailure,
|
||||
TypeFailureFamily,
|
||||
} from "../../diagnostics/diagnostic.js";
|
||||
import { declarationGroupKey, typeFailureOf } from "./type-failure.js";
|
||||
import {
|
||||
unsupportedAnyOperationDiag,
|
||||
blockedBindingUseDiag,
|
||||
@@ -713,7 +718,16 @@ interface GenericDemandOwner {
|
||||
classDemands: ClassInfo[];
|
||||
}
|
||||
|
||||
export class PoisonError extends Error {}
|
||||
export class PoisonError extends Error {
|
||||
/** Set when a type with no static lowering poisoned a slot (badType and
|
||||
* the dyn class field policy): what failed, for the localized-failure
|
||||
* commit points. Report-only in stage L0. */
|
||||
readonly failure: TypeFailure | undefined;
|
||||
constructor(failure?: TypeFailure) {
|
||||
super();
|
||||
this.failure = failure;
|
||||
}
|
||||
}
|
||||
|
||||
/** A function-shaped value that exists only in the compiler's static
|
||||
* projection graph. Declared-function aliases point at the original
|
||||
@@ -3697,6 +3711,15 @@ export class Lowerer {
|
||||
* the fallthroughs report the inherited-blocker cascade (SC2004)
|
||||
* instead of misattributing the reference. */
|
||||
readonly blockedBindings = new Set<ts.Symbol>();
|
||||
/** Localized failure, report-only: the failure group (type-failure.ts)
|
||||
* of each declaration whose rejection reached the build — a flushed
|
||||
* deferred declaration, or a binding of a poisoned declaration
|
||||
* statement. */
|
||||
readonly rejectedGroups = new Map<ts.Symbol, string>();
|
||||
/** One frame per statement lowerStmts is lowering: the failure groups
|
||||
* its lowering touched. A statement that then poisons attributes its
|
||||
* diagnostics to them (ScrDiagnostic.cause). */
|
||||
readonly failureTouches: Set<string>[] = [];
|
||||
/** True while collectProgram runs: resolution helpers must not flush
|
||||
* deferred diagnostics (collection itself resolves symbols — extends
|
||||
* clauses — and collection order must not decide what reports). */
|
||||
@@ -9177,7 +9200,11 @@ export class Lowerer {
|
||||
this.diagSink.push(d);
|
||||
return;
|
||||
}
|
||||
if (pending) this.commitFlush(pending.symbol, pending.source);
|
||||
if (pending) {
|
||||
this.commitFlush(pending.symbol, pending.source);
|
||||
const group = this.rejectedGroups.get(pending.symbol);
|
||||
if (group !== undefined) d.rejects ??= group;
|
||||
}
|
||||
// One site, one report: some declarations map a type twice (module
|
||||
// globals pre-register before their initializers lower) — an exact
|
||||
// duplicate (code + span + message) adds noise, not information.
|
||||
@@ -9232,6 +9259,35 @@ export class Lowerer {
|
||||
return (this.typeOf(node).flags & ts.TypeFlags.Any) !== 0;
|
||||
}
|
||||
|
||||
/** badType's commit: the diagnostic carries the structured type
|
||||
* failure (report-only in stage L0), and so does the poison. */
|
||||
private typeFail(
|
||||
node: ts.Node,
|
||||
type: ts.Type,
|
||||
family: TypeFailureFamily,
|
||||
diag: ScrDiagnostic,
|
||||
): never {
|
||||
const failure = typeFailureOf(node, this.checker.typeToString(type), family);
|
||||
diag.typeFailure = failure;
|
||||
this.pushDiag(diag);
|
||||
throw new PoisonError(failure);
|
||||
}
|
||||
|
||||
/** The SC1090 refusal of checked-dynamic class fields in TypeScript
|
||||
* (classFieldStoresDyn): a policy fence, not a missing representation,
|
||||
* so its failure is recorded as degradable. */
|
||||
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);
|
||||
}
|
||||
|
||||
badType(node: ts.Node, type: ts.Type): never {
|
||||
const widened = this.checker.getBaseTypeOfLiteralType(type);
|
||||
// Types declared by the ADOPTED @types/node (Buffer, NodeJS.Timeout,
|
||||
@@ -9243,8 +9299,12 @@ export class Lowerer {
|
||||
// applies to node-typed values.
|
||||
const typeSym = widened.getAliasSymbol() ?? widened.getSymbol();
|
||||
if (this.nodeTypesOnlySymbol(typeSym)) {
|
||||
this.pushDiag(noLoweringDiag(this.checker.typeToString(type), locOf(node), undefined, true));
|
||||
throw new PoisonError();
|
||||
this.typeFail(
|
||||
node,
|
||||
type,
|
||||
"node-types",
|
||||
noLoweringDiag(this.checker.typeToString(type), locOf(node), undefined, true),
|
||||
);
|
||||
}
|
||||
// Engine-backed ambient TYPES in a STATIC build report the per-site
|
||||
// SC2012 rather than the generic supported-types recitation. Native
|
||||
@@ -9255,10 +9315,12 @@ export class Lowerer {
|
||||
(FETCH_AMBIENT_TYPES as readonly string[]).includes(ts.symbolName(typeSym)) &&
|
||||
this.isStdlibSymbol(typeSym)
|
||||
) {
|
||||
this.pushDiag(
|
||||
this.typeFail(
|
||||
node,
|
||||
type,
|
||||
"fetch-ambient",
|
||||
unsupportedNativeApiDiag(`a value of type '${ts.symbolName(typeSym)}'`, locOf(node)),
|
||||
);
|
||||
throw new PoisonError();
|
||||
}
|
||||
// ENUM OBJECTS (`typeof e` — a numeric enum's type carries the
|
||||
// reverse-map index signature, a string enum's just its members):
|
||||
@@ -9267,14 +9329,16 @@ export class Lowerer {
|
||||
// as the per-site identifier fence — never the index-signature
|
||||
// recitation.
|
||||
if (typeSym && (typeSym.flags & ts.SymbolFlags.Enum) !== 0) {
|
||||
this.pushDiag(
|
||||
this.typeFail(
|
||||
node,
|
||||
type,
|
||||
"enum-object",
|
||||
unsupportedDiag(
|
||||
"SC1090",
|
||||
locOf(node),
|
||||
`enum objects as values ('${ts.symbolName(typeSym)}' — member reads like '${ts.symbolName(typeSym)}.X' compile to constants; the object itself has no runtime representation)`,
|
||||
),
|
||||
);
|
||||
throw new PoisonError();
|
||||
}
|
||||
// Would-be records with INDEX SIGNATURES (`Record<string, T>`,
|
||||
// `{ [k: string]: T }`) get the index-signature fence (SC2006) naming
|
||||
@@ -9304,19 +9368,26 @@ export class Lowerer {
|
||||
this.isStdlibFile(ts.getSourceFileOfNode(d)),
|
||||
)
|
||||
) {
|
||||
this.pushDiag(noLoweringDiag(this.checker.typeToString(type), locOf(node)));
|
||||
throw new PoisonError();
|
||||
this.typeFail(
|
||||
node,
|
||||
type,
|
||||
"stdlib-index",
|
||||
noLoweringDiag(this.checker.typeToString(type), locOf(node)),
|
||||
);
|
||||
}
|
||||
|
||||
this.pushDiag(indexSignatureTypeDiag(this.checker.typeToString(type), locOf(node)));
|
||||
throw new PoisonError();
|
||||
this.typeFail(
|
||||
node,
|
||||
type,
|
||||
"index-signature",
|
||||
indexSignatureTypeDiag(this.checker.typeToString(type), locOf(node)),
|
||||
);
|
||||
}
|
||||
// Attribute an unsupported package type to its package.
|
||||
{
|
||||
const pkg = this.npmPackageOf(widened);
|
||||
if (pkg) {
|
||||
this.pushDiag(unsupportedPackageValueDiag(pkg, locOf(node)));
|
||||
throw new PoisonError();
|
||||
this.typeFail(node, type, "package", unsupportedPackageValueDiag(pkg, locOf(node)));
|
||||
}
|
||||
}
|
||||
// A type that keeps a GENERIC call signature (`<T>(x: T) => T` slots,
|
||||
@@ -9332,8 +9403,12 @@ export class Lowerer {
|
||||
this.checker.getCallSignatures(p).some((s) => s.getTypeParameters().length > 0),
|
||||
)
|
||||
) {
|
||||
this.pushDiag(genericSignatureTypeDiag(this.checker.typeToString(type), locOf(node)));
|
||||
throw new PoisonError();
|
||||
this.typeFail(
|
||||
node,
|
||||
type,
|
||||
"generic-signature",
|
||||
genericSignatureTypeDiag(this.checker.typeToString(type), locOf(node)),
|
||||
);
|
||||
}
|
||||
}
|
||||
// STANDARD-LIBRARY nominal provenance, decided once: interface/class
|
||||
@@ -9353,16 +9428,24 @@ export class Lowerer {
|
||||
// generic overload set keeps the monomorphization story: a value of a
|
||||
// multi-signature type has no single compiled signature to hold.
|
||||
if (!stdlibNominal && this.checker.getCallSignatures(widened).length > 1) {
|
||||
this.pushDiag(overloadedSignatureTypeDiag(this.checker.typeToString(type), locOf(node)));
|
||||
throw new PoisonError();
|
||||
this.typeFail(
|
||||
node,
|
||||
type,
|
||||
"overloads",
|
||||
overloadedSignatureTypeDiag(this.checker.typeToString(type), locOf(node)),
|
||||
);
|
||||
}
|
||||
// INTERSECTIONS that resolved to no lowering (SC2008). The ones that
|
||||
// compile never get here: member intersections intern through the
|
||||
// record path, callable hybrids map to '%call' records, and pinned
|
||||
// mixin instantiations resolve by chain structure.
|
||||
if (widened.isIntersectionType()) {
|
||||
this.pushDiag(intersectionTypeDiag(this.checker.typeToString(type), locOf(node)));
|
||||
throw new PoisonError();
|
||||
this.typeFail(
|
||||
node,
|
||||
type,
|
||||
"intersection",
|
||||
intersectionTypeDiag(this.checker.typeToString(type), locOf(node)),
|
||||
);
|
||||
}
|
||||
// SUPPORTED shapes over a component outside its slot (SC2009): the
|
||||
// Map/Set domains, array/tuple elements, union arms, function
|
||||
@@ -9374,8 +9457,12 @@ export class Lowerer {
|
||||
{
|
||||
const detail = describeComponentBlocker(widened, this.typeCtx);
|
||||
if (detail !== null) {
|
||||
this.pushDiag(componentTypeDiag(this.checker.typeToString(type), detail, locOf(node)));
|
||||
throw new PoisonError();
|
||||
this.typeFail(
|
||||
node,
|
||||
type,
|
||||
"component",
|
||||
componentTypeDiag(this.checker.typeToString(type), detail, locOf(node)),
|
||||
);
|
||||
}
|
||||
}
|
||||
// STANDARD-LIBRARY nominal types with no lowering at all: the SC2020
|
||||
@@ -9405,26 +9492,36 @@ export class Lowerer {
|
||||
TextDecoder:
|
||||
"TextDecoder instances support recognized labels, fatal/ignoreBOM option literals, and streaming UTF-8/UTF-16/single-byte decode; runtime option objects remain unsupported",
|
||||
};
|
||||
this.pushDiag(
|
||||
this.typeFail(
|
||||
node,
|
||||
type,
|
||||
"stdlib-nominal",
|
||||
noLoweringDiag(
|
||||
this.checker.typeToString(type),
|
||||
locOf(node),
|
||||
typeHints[ts.symbolNameOf(stdlibOwnSym) ?? ""],
|
||||
),
|
||||
);
|
||||
throw new PoisonError();
|
||||
}
|
||||
// USER record shapes blocked by ONE member (SC2009's record arm):
|
||||
// name the member and its type.
|
||||
{
|
||||
const detail = describeRecordMemberBlocker(widened, this.typeCtx);
|
||||
if (detail !== null) {
|
||||
this.pushDiag(componentTypeDiag(this.checker.typeToString(type), detail, locOf(node)));
|
||||
throw new PoisonError();
|
||||
this.typeFail(
|
||||
node,
|
||||
type,
|
||||
"record-member",
|
||||
componentTypeDiag(this.checker.typeToString(type), detail, locOf(node)),
|
||||
);
|
||||
}
|
||||
}
|
||||
this.pushDiag(unsupportedTypeDiag(this.checker.typeToString(type), locOf(node)));
|
||||
throw new PoisonError();
|
||||
this.typeFail(
|
||||
node,
|
||||
type,
|
||||
"unsupported",
|
||||
unsupportedTypeDiag(this.checker.typeToString(type), locOf(node)),
|
||||
);
|
||||
}
|
||||
|
||||
/** The lib fence (SC2020): a reached use of standard-library surface
|
||||
@@ -12067,12 +12164,35 @@ export class Lowerer {
|
||||
this.reportDeferred(symbol);
|
||||
}
|
||||
|
||||
/** A statement's lowering used a declaration: if that declaration was
|
||||
* rejected, the statement's own failure (if any) is its cascade. */
|
||||
touchRejected(symbol: ts.Symbol): void {
|
||||
const group = this.rejectedGroups.get(symbol);
|
||||
if (group === undefined) return;
|
||||
const frame = this.failureTouches[this.failureTouches.length - 1];
|
||||
if (frame !== undefined) frame.add(group);
|
||||
}
|
||||
|
||||
/** Records a declaration whose deferred diagnostics reached the build:
|
||||
* its failure group, on the group's diagnostics too. */
|
||||
private noteRejected(symbol: ts.Symbol, diags: readonly ScrDiagnostic[]): void {
|
||||
let group = this.rejectedGroups.get(symbol);
|
||||
if (group === undefined) {
|
||||
group = declarationGroupKey(symbol);
|
||||
this.rejectedGroups.set(symbol, group);
|
||||
}
|
||||
for (const d of diags) d.rejects ??= group;
|
||||
}
|
||||
|
||||
/** flushDeferred without the collection-pass guard: for declarations
|
||||
* whose diagnostics must report eagerly (class statements with
|
||||
* declaration-time code, extended bases — collectClassShapeDeferring). */
|
||||
reportDeferred(symbol: ts.Symbol): void {
|
||||
const diags = this.deferredDiags.get(symbol);
|
||||
if (!diags) return;
|
||||
if (!diags) {
|
||||
this.touchRejected(symbol);
|
||||
return;
|
||||
}
|
||||
if (this.alreadyFlushed.has(symbol)) {
|
||||
this.deferredDiags.delete(symbol); // the emit pass reported these
|
||||
return;
|
||||
@@ -12080,7 +12200,9 @@ export class Lowerer {
|
||||
if (this.diagSink === null) {
|
||||
this.deferredDiags.delete(symbol);
|
||||
this.flushedSymbols.add(symbol);
|
||||
this.noteRejected(symbol, diags);
|
||||
for (const d of diags) this.pushDiag(d);
|
||||
this.touchRejected(symbol);
|
||||
return;
|
||||
}
|
||||
// A capture buffer is active (a probe, or another declaration's
|
||||
@@ -12097,6 +12219,7 @@ export class Lowerer {
|
||||
* (the declaration counts as flushed for the coverage remainder). */
|
||||
private commitFlush(symbol: ts.Symbol, source: ScrDiagnostic): void {
|
||||
this.flushedSymbols.add(symbol);
|
||||
this.noteRejected(symbol, [source]);
|
||||
const list = this.deferredDiags.get(symbol);
|
||||
if (!list) return;
|
||||
const index = list.indexOf(source);
|
||||
@@ -13222,8 +13345,8 @@ export class Lowerer {
|
||||
return lowerStmts(this, stmts);
|
||||
}
|
||||
|
||||
noteBlockedBindings(stmt: ts.Statement): void {
|
||||
return noteBlockedBindings(this, stmt);
|
||||
noteBlockedBindings(stmt: ts.Statement, diags?: readonly ScrDiagnostic[]): void {
|
||||
return noteBlockedBindings(this, stmt, diags);
|
||||
}
|
||||
|
||||
isBlockedBinding(symbol: ts.Symbol | null): boolean {
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
/* Localized failure, stage L0 (report-only).
|
||||
*
|
||||
* A type with no static lowering poisons the slot that holds it (a field,
|
||||
* a parameter, a return, a local), and today the poison rejects the whole
|
||||
* declaration around the slot: a class, a signature, a declaration
|
||||
* statement. Every later use of that declaration then fails on its own
|
||||
* line. The localized-failure design turns such slots into checked
|
||||
* dynamic slots at the points where the build would otherwise commit the
|
||||
* error.
|
||||
*
|
||||
* This module holds the bookkeeping that stage measures without changing
|
||||
* any lowering: where a failing type sits (its slot), whether a dynamic
|
||||
* slot would keep the program's meaning (design §3.7), and the keys that
|
||||
* tie a cascade diagnostic to the rejected declaration it inherits from.
|
||||
* The data rides the committed diagnostics as report-only fields
|
||||
* (ScrDiagnostic.typeFailure, .rejects, .cause); tier-analysis.ts turns it
|
||||
* into the would-be dynamic slot report of `--explain-dynamic`. */
|
||||
import type {
|
||||
TypeFailure,
|
||||
TypeFailureFamily,
|
||||
TypeFailureSlot,
|
||||
} from "../../diagnostics/diagnostic.js";
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
|
||||
/** Families a checked-dynamic slot can hold (design §3.7). The others
|
||||
* name values the compiler cannot produce (opaque stdlib and @types/node
|
||||
* types, engine-backed ambient types), package value types that scope B
|
||||
* routes to Node islands, enum objects with no runtime value, and generic
|
||||
* implementations without an erased instance. */
|
||||
const DEGRADABLE_FAMILIES: ReadonlySet<TypeFailureFamily> = new Set<TypeFailureFamily>([
|
||||
"index-signature",
|
||||
"overloads",
|
||||
"intersection",
|
||||
"component",
|
||||
"record-member",
|
||||
"unsupported",
|
||||
"dyn-field-policy",
|
||||
]);
|
||||
|
||||
function nameText(name: ts.Node | undefined): string | null {
|
||||
if (name === undefined) return null;
|
||||
if (ts.isIdentifier(name) || ts.isPrivateIdentifier(name)) return name.text;
|
||||
if (ts.isStringLiteral(name) || ts.isNumericLiteral(name)) return name.text;
|
||||
return ts.getTextOfNode(name);
|
||||
}
|
||||
|
||||
function isParameterProperty(param: ts.ParameterDeclaration): boolean {
|
||||
return (
|
||||
ts.isConstructorDeclaration(param.parent) &&
|
||||
(param.modifiers?.nodes ?? []).some(
|
||||
(m) =>
|
||||
m.kind === ts.SyntaxKind.PublicKeyword ||
|
||||
m.kind === ts.SyntaxKind.PrivateKeyword ||
|
||||
m.kind === ts.SyntaxKind.ProtectedKeyword ||
|
||||
m.kind === ts.SyntaxKind.ReadonlyKeyword,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function variableSlot(decl: ts.VariableDeclaration): TypeFailureSlot {
|
||||
const list = decl.parent;
|
||||
const holder = list?.parent;
|
||||
if (
|
||||
holder !== undefined &&
|
||||
(ts.isForOfStatement(holder) || ts.isForInStatement(holder) || ts.isForStatement(holder))
|
||||
)
|
||||
return "loop";
|
||||
if (holder !== undefined && ts.isVariableStatement(holder) && ts.isSourceFile(holder.parent))
|
||||
return "global";
|
||||
return "local";
|
||||
}
|
||||
|
||||
function functionLabel(fn: ts.Node): string {
|
||||
if (ts.isConstructorDeclaration(fn)) return "constructor";
|
||||
if (ts.isFunctionLikeDeclaration(fn)) {
|
||||
const own = nameText(fn.name);
|
||||
if (own !== null) return own;
|
||||
const parent = fn.parent;
|
||||
if (
|
||||
parent !== undefined &&
|
||||
(ts.isVariableDeclaration(parent) ||
|
||||
ts.isPropertyAssignment(parent) ||
|
||||
ts.isPropertyDeclaration(parent))
|
||||
) {
|
||||
const bound = nameText(parent.name);
|
||||
if (bound !== null) return bound;
|
||||
}
|
||||
}
|
||||
return "<anonymous>";
|
||||
}
|
||||
|
||||
/** The slot a badType blame node denotes. A declaration's own node, its
|
||||
* name and its type annotation name the declaration's slot; anything
|
||||
* reached through an expression is an expression slot (the type of a
|
||||
* cast, a literal, a temporary). */
|
||||
export function typeFailureSlot(node: ts.Node): { slot: TypeFailureSlot; name: string } {
|
||||
let current: ts.Node = node;
|
||||
for (let depth = 0; depth < 64; depth++) {
|
||||
if (ts.isParameter(current)) {
|
||||
const name = nameText(current.name) ?? "<parameter>";
|
||||
return { slot: isParameterProperty(current) ? "field" : "param", name };
|
||||
}
|
||||
if (ts.isPropertyDeclaration(current))
|
||||
return { slot: "field", name: nameText(current.name) ?? "<field>" };
|
||||
if (ts.isVariableDeclaration(current))
|
||||
return { slot: variableSlot(current), name: nameText(current.name) ?? "<binding>" };
|
||||
if (ts.isBindingElement(current)) {
|
||||
let owner: ts.Node | undefined = current.parent;
|
||||
while (owner !== undefined && !ts.isVariableDeclaration(owner) && !ts.isParameter(owner))
|
||||
owner = owner.parent;
|
||||
const slot: TypeFailureSlot =
|
||||
owner === undefined
|
||||
? "local"
|
||||
: ts.isParameter(owner)
|
||||
? "param"
|
||||
: variableSlot(owner as ts.VariableDeclaration);
|
||||
return { slot, name: nameText(current.name) ?? "<binding>" };
|
||||
}
|
||||
if (ts.isPropertySignature(current) || ts.isMethodSignatureDeclaration(current))
|
||||
return { slot: "member", name: nameText(current.name) ?? "<member>" };
|
||||
if (ts.isFunctionLikeDeclaration(current))
|
||||
return { slot: "return", name: functionLabel(current) };
|
||||
if (ts.isCatchClause(current)) return { slot: "local", name: "<catch binding>" };
|
||||
const parent: ts.Node | undefined = current.parent;
|
||||
if (parent === undefined || ts.isSourceFile(parent)) break;
|
||||
// Climb from a declaration's name or type annotation, or from inside a
|
||||
// type node; any other position is an expression's own type.
|
||||
const declared =
|
||||
(ts.isParameter(parent) ||
|
||||
ts.isPropertyDeclaration(parent) ||
|
||||
ts.isVariableDeclaration(parent) ||
|
||||
ts.isBindingElement(parent) ||
|
||||
ts.isPropertySignature(parent) ||
|
||||
ts.isMethodSignatureDeclaration(parent) ||
|
||||
ts.isFunctionLikeDeclaration(parent)) &&
|
||||
((parent as { name?: ts.Node }).name === current ||
|
||||
(parent as { type?: ts.Node }).type === current);
|
||||
if (!declared && !ts.isTypeNode(current) && !ts.isTypeNode(parent)) break;
|
||||
current = parent;
|
||||
}
|
||||
const text = ts.getTextOfNode(node);
|
||||
return { slot: "expression", name: text.length > 60 ? `${text.slice(0, 57)}...` : text };
|
||||
}
|
||||
|
||||
/** Program code outside node_modules, in TypeScript: the code holes apply
|
||||
* to (design §3.3, §3.8). JavaScript keeps its dynamic fallback and its
|
||||
* runtime fences; npm packages keep island routing. */
|
||||
function programTypeScript(node: ts.Node): boolean {
|
||||
const sf = ts.getSourceFileOfNode(node);
|
||||
const file = sf.fileName;
|
||||
if (file.includes("/node_modules/")) return false;
|
||||
if (sf.isDeclarationFile) return false;
|
||||
return !/\.(?:[cm]?js|jsx)$/.test(file);
|
||||
}
|
||||
|
||||
export function typeFailureOf(
|
||||
node: ts.Node,
|
||||
declaredType: string,
|
||||
family: TypeFailureFamily,
|
||||
): TypeFailure {
|
||||
const { slot, name } = typeFailureSlot(node);
|
||||
return {
|
||||
slot,
|
||||
name,
|
||||
declaredType,
|
||||
family,
|
||||
degradable: DEGRADABLE_FAMILIES.has(family) && programTypeScript(node),
|
||||
};
|
||||
}
|
||||
|
||||
function locKey(node: ts.Node): string {
|
||||
const sf = ts.getSourceFileOfNode(node);
|
||||
return `${sf.fileName}:${ts.tokenStartOfNode(node)}`;
|
||||
}
|
||||
|
||||
/** The failure-group key of a deferred declaration (a class shape or a
|
||||
* function signature whose collection poisoned). */
|
||||
export function declarationGroupKey(symbol: ts.Symbol): string {
|
||||
const decl = symbol.declarations[0];
|
||||
const name = ts.symbolName(symbol);
|
||||
if (decl === undefined) return `declaration ${name}`;
|
||||
const kind =
|
||||
ts.isClassDeclaration(decl) || ts.isClassExpression(decl)
|
||||
? "class"
|
||||
: ts.isFunctionLikeDeclaration(decl)
|
||||
? "function"
|
||||
: "declaration";
|
||||
return `${kind} ${name} @${locKey(decl)}`;
|
||||
}
|
||||
|
||||
/** The failure-group key of a poisoned declaration statement whose
|
||||
* bindings are blocked. */
|
||||
export function statementGroupKey(stmt: ts.Statement): string {
|
||||
return `statement @${locKey(stmt)}`;
|
||||
}
|
||||
@@ -29,7 +29,12 @@
|
||||
* Attribution is syntactic and approximate: the site's innermost node, the
|
||||
* operand it acts on, and that operand's declaration. Stage S6 replaces it
|
||||
* with requests the failing lowering names exactly. */
|
||||
import type { ScrDiagnostic } from "../diagnostics/diagnostic.js";
|
||||
import type {
|
||||
ScrDiagnostic,
|
||||
TypeFailureFamily,
|
||||
TypeFailureSlot,
|
||||
} from "../diagnostics/diagnostic.js";
|
||||
import { isWarningCode } from "../diagnostics/render.js";
|
||||
import type { IrFunction, SrcLoc } from "../ir/ir.js";
|
||||
import { dynamicOperationSites, type DynamicFunction } from "../ir/tiers.js";
|
||||
import * as ts from "./ts7/adapter.js";
|
||||
@@ -85,8 +90,70 @@ export interface TierDynamicFunction extends DynamicFunction {
|
||||
statements: number;
|
||||
}
|
||||
|
||||
/** The localized-failure stage whose slot positions include a slot kind
|
||||
* (design §3.6): locals and globals degrade first, declarations next,
|
||||
* members of declared types with push-down, expression types last. */
|
||||
export type LocalizedFailureStage = "L1" | "L2" | "L3" | "L4";
|
||||
|
||||
export function slotStage(slot: TypeFailureSlot): LocalizedFailureStage {
|
||||
switch (slot) {
|
||||
case "local":
|
||||
case "global":
|
||||
case "loop":
|
||||
return "L1";
|
||||
case "field":
|
||||
case "param":
|
||||
case "return":
|
||||
return "L2";
|
||||
case "member":
|
||||
return "L3";
|
||||
case "expression":
|
||||
return "L4";
|
||||
}
|
||||
}
|
||||
|
||||
/** A committed type failure that would become a checked dynamic slot
|
||||
* (SC6104) instead of an error. */
|
||||
export interface WouldBeSlot {
|
||||
loc: SrcLoc;
|
||||
slot: TypeFailureSlot;
|
||||
name: string;
|
||||
declaredType: string;
|
||||
family: TypeFailureFamily;
|
||||
code: string;
|
||||
message: string;
|
||||
stage: LocalizedFailureStage;
|
||||
/** The declaration the failure rejects today (a failure-group key), if
|
||||
* any: a class, a signature, or a declaration statement. */
|
||||
rejects: string | null;
|
||||
/** Other committed diagnostics that inherit from this slot's group and
|
||||
* clear once every group they inherit from degrades (with push-down and
|
||||
* expression slots, the full design). */
|
||||
cascade: number;
|
||||
}
|
||||
|
||||
/** Localized failure, report-only (stage L0): what the committed errors
|
||||
* would become. Two scenarios: through stage L2 (declaration slots) and
|
||||
* through L4 (members and expressions too). */
|
||||
export interface LocalizedFailureReport {
|
||||
/** Reached errors (warnings excluded). */
|
||||
errors: number;
|
||||
/** Type failures, by whether a checked dynamic slot keeps meaning. */
|
||||
typeFailures: { degradable: number; refused: number };
|
||||
slots: WouldBeSlot[];
|
||||
/** Diagnostics attributed to a rejected declaration they used. */
|
||||
cascades: number;
|
||||
/** Errors that disappear (degraded slots plus cleared cascades). */
|
||||
clearedThroughL2: number;
|
||||
clearedThroughL4: number;
|
||||
/** The errors that remain through L4, by code. */
|
||||
remainingByCode: Record<string, number>;
|
||||
}
|
||||
|
||||
export interface TierReport {
|
||||
mode: TierMode;
|
||||
/** Localized failure (report-only). */
|
||||
localizedFailure: LocalizedFailureReport;
|
||||
/** Reached source statements that perform a dynamic operation. */
|
||||
statementsDynamic: number;
|
||||
dynamicFunctions: TierDynamicFunction[];
|
||||
@@ -576,6 +643,119 @@ function demotedUnits(requests: readonly DemotionRequest[]): DemotedUnit[] {
|
||||
);
|
||||
}
|
||||
|
||||
/* ── localized failure ───────────────────────────────────────────────── */
|
||||
|
||||
/** A diagnostic's failure-group bookkeeping (lowerer: badType, deferred
|
||||
* flushes, blocked statements) turned into the would-be report. A group
|
||||
* (a rejected declaration or blocked statement) clears in a scenario when
|
||||
* each of its diagnostics either degrades (a degradable type failure at a
|
||||
* slot the scenario covers) or is a cascade whose every cause clears. */
|
||||
export function localizedFailureReport(reached: readonly ScrDiagnostic[]): LocalizedFailureReport {
|
||||
const errors = reached.filter((d) => !isWarningCode(d.code));
|
||||
const groups = new Map<string, ScrDiagnostic[]>();
|
||||
for (const d of errors) {
|
||||
if (d.rejects === undefined) continue;
|
||||
const list = groups.get(d.rejects);
|
||||
if (list === undefined) groups.set(d.rejects, [d]);
|
||||
else list.push(d);
|
||||
}
|
||||
const stages: Record<"L2" | "L4", ReadonlySet<LocalizedFailureStage>> = {
|
||||
L2: new Set(["L1", "L2"]),
|
||||
L4: new Set(["L1", "L2", "L3", "L4"]),
|
||||
};
|
||||
const clears = (scenario: "L2" | "L4") => {
|
||||
const covered = stages[scenario];
|
||||
const memo = new Map<string, boolean>();
|
||||
const degrades = (d: ScrDiagnostic): boolean =>
|
||||
d.typeFailure !== undefined &&
|
||||
d.typeFailure.degradable &&
|
||||
covered.has(slotStage(d.typeFailure.slot));
|
||||
const causeClears = (d: ScrDiagnostic): boolean =>
|
||||
d.cause !== undefined && d.cause.length > 0 && d.cause.every((g) => groupClears(g));
|
||||
const groupClears = (group: string): boolean => {
|
||||
const known = memo.get(group);
|
||||
if (known !== undefined) return known;
|
||||
memo.set(group, false); // a cycle never clears
|
||||
const members = groups.get(group) ?? [];
|
||||
const ok = members.length > 0 && members.every((d) => degrades(d) || causeClears(d));
|
||||
memo.set(group, ok);
|
||||
return ok;
|
||||
};
|
||||
return { degrades, causeClears, groupClears };
|
||||
};
|
||||
const l2 = clears("L2");
|
||||
const l4 = clears("L4");
|
||||
const cleared = (c: ReturnType<typeof clears>, d: ScrDiagnostic) =>
|
||||
c.degrades(d) || c.causeClears(d);
|
||||
// Each slot's cascade: diagnostics whose causes (transitively) include
|
||||
// the slot's group and that clear in the full scenario.
|
||||
const reachesGroup = (d: ScrDiagnostic, group: string, seen: Set<string>): boolean => {
|
||||
for (const g of d.cause ?? []) {
|
||||
if (g === group) return true;
|
||||
if (seen.has(g)) continue;
|
||||
seen.add(g);
|
||||
for (const m of groups.get(g) ?? []) if (reachesGroup(m, group, seen)) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const slots: WouldBeSlot[] = [];
|
||||
let degradable = 0;
|
||||
let refused = 0;
|
||||
for (const d of errors) {
|
||||
const f = d.typeFailure;
|
||||
if (f === undefined) continue;
|
||||
if (!f.degradable) {
|
||||
refused++;
|
||||
continue;
|
||||
}
|
||||
degradable++;
|
||||
const group = d.rejects ?? null;
|
||||
let cascade = 0;
|
||||
if (group !== null)
|
||||
for (const e of errors)
|
||||
if (
|
||||
e !== d &&
|
||||
e.typeFailure === undefined &&
|
||||
cleared(l4, e) &&
|
||||
reachesGroup(e, group, new Set())
|
||||
)
|
||||
cascade++;
|
||||
slots.push({
|
||||
loc: d.loc,
|
||||
slot: f.slot,
|
||||
name: f.name,
|
||||
declaredType: f.declaredType,
|
||||
family: f.family,
|
||||
code: d.code,
|
||||
message: d.message,
|
||||
stage: slotStage(f.slot),
|
||||
rejects: group,
|
||||
cascade,
|
||||
});
|
||||
}
|
||||
slots.sort(
|
||||
(a, b) =>
|
||||
b.cascade - a.cascade || a.loc.file.localeCompare(b.loc.file) || a.loc.start - b.loc.start,
|
||||
);
|
||||
const remainingByCode: Record<string, number> = {};
|
||||
let clearedThroughL2 = 0;
|
||||
let clearedThroughL4 = 0;
|
||||
for (const d of errors) {
|
||||
if (cleared(l2, d)) clearedThroughL2++;
|
||||
if (cleared(l4, d)) clearedThroughL4++;
|
||||
else remainingByCode[d.code] = (remainingByCode[d.code] ?? 0) + 1;
|
||||
}
|
||||
return {
|
||||
errors: errors.length,
|
||||
typeFailures: { degradable, refused },
|
||||
slots,
|
||||
cascades: errors.filter((d) => d.cause !== undefined && d.typeFailure === undefined).length,
|
||||
clearedThroughL2,
|
||||
clearedThroughL4,
|
||||
remainingByCode,
|
||||
};
|
||||
}
|
||||
|
||||
export interface TierAnalysisInput {
|
||||
mode: TierMode;
|
||||
/** The dynamic-tier IR functions of the reached program. */
|
||||
@@ -624,6 +804,7 @@ export function analyzeTiers(program: ts.Program, input: TierAnalysisInput): Tie
|
||||
const dynamic = dynamicFunctionReports(program, input.dynamicFunctions);
|
||||
return {
|
||||
mode: input.mode,
|
||||
localizedFailure: localizedFailureReport(input.reached),
|
||||
statementsDynamic: dynamic.statements,
|
||||
dynamicFunctions: dynamic.functions,
|
||||
requests,
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
// The localized-failure report of `scriptc coverage --explain-dynamic`:
|
||||
// type failures that would become checked dynamic slots, and the uses
|
||||
// each one rejects today.
|
||||
interface Tool {
|
||||
name: string;
|
||||
execute(id: string, params: any): Promise<string>;
|
||||
}
|
||||
type Progress = (done: number, total: number) => void;
|
||||
type Callbacks = Progress | AbortSignal | Map<string, (x: number) => string> | Set<number>;
|
||||
|
||||
// A field: rejects the class, so every use of an instance fails.
|
||||
class Session {
|
||||
private tools: Tool[] = [];
|
||||
count(): number {
|
||||
return this.tools.length;
|
||||
}
|
||||
label(): string {
|
||||
return "s";
|
||||
}
|
||||
}
|
||||
|
||||
// A parameter: rejects the function, so every call fails.
|
||||
function listAll(dir: string, progress?: Callbacks): number {
|
||||
return dir.length;
|
||||
}
|
||||
|
||||
// The dyn class field policy.
|
||||
class Holder {
|
||||
state: any = {};
|
||||
get(): number {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// A local: rejects its declaration statement, so its uses fail.
|
||||
function pick(n: number): boolean {
|
||||
const callbacks: Callbacks | undefined = n > 1 ? new Set<number>() : undefined;
|
||||
return callbacks === undefined;
|
||||
}
|
||||
|
||||
// Refused: a value the compiler cannot produce stays an error.
|
||||
class Cache {
|
||||
tokens: WeakRef<object> = new WeakRef({});
|
||||
size(): number {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
const s = new Session();
|
||||
console.log(s.count(), s.label());
|
||||
console.log(listAll("abc"));
|
||||
console.log(new Holder().get());
|
||||
console.log(pick(1), new Cache().size());
|
||||
@@ -0,0 +1,49 @@
|
||||
dynamic tier (tier assignment, report-only; --tier=auto)
|
||||
0 functions already operate on dynamic values (0 statements)
|
||||
|
||||
11 demotion requests (refusals and fences the dynamic tier would replace)
|
||||
|
||||
4 would-be dynamic slots (localized failure, report-only: type failures a checked dynamic slot would hold)
|
||||
11 errors: 9 clear with declaration slots (L1-L2), 9 with member and expression slots too (L3-L4); 6 cascades attributed; 1 type failure stays refused
|
||||
L2 field tools: Tool[] tests/coverage-fixtures/localized-failure.ts:13:11 (SC2009 component, clears 2)
|
||||
rejects class Session
|
||||
L2 param progress: Callbacks | undefined tests/coverage-fixtures/localized-failure.ts:23:31 (SC2009 component, clears 1)
|
||||
rejects function listAll
|
||||
L2 field state: any tests/coverage-fixtures/localized-failure.ts:29:3 (SC1090 dyn-field-policy, clears 1)
|
||||
rejects class Holder
|
||||
L1 local callbacks: Callbacks | undefined tests/coverage-fixtures/localized-failure.ts:37:9 (SC2009 component, clears 1)
|
||||
rejects statement
|
||||
class Session tests/coverage-fixtures/localized-failure.ts:12:1 (1 request)
|
||||
<- SC1090 reflection at tests/coverage-fixtures/localized-failure.ts:49:11: constructing through a class value whose class has no lowering (the class declaration itself was rejected — see its own diagnostic) is not supported yet
|
||||
-> function <top level> turns dynamic
|
||||
class Holder tests/coverage-fixtures/localized-failure.ts:28:1 (1 request)
|
||||
<- SC1090 reflection at tests/coverage-fixtures/localized-failure.ts:52:13: constructing through a class value whose class has no lowering (the class declaration itself was rejected — see its own diagnostic) is not supported yet
|
||||
-> function <top level> turns dynamic
|
||||
class Cache tests/coverage-fixtures/localized-failure.ts:42:1 (1 request)
|
||||
<- SC1090 reflection at tests/coverage-fixtures/localized-failure.ts:53:22: constructing through a class value whose class has no lowering (the class declaration itself was rejected — see its own diagnostic) is not supported yet
|
||||
-> function <top level> turns dynamic
|
||||
field Session.tools tests/coverage-fixtures/localized-failure.ts:13:3 (1 request)
|
||||
<- SC2009 type at tests/coverage-fixtures/localized-failure.ts:13:11: values of type 'Tool[]' cannot be compiled: the array shape is supported, but its element type 'Tool' does not compile
|
||||
-> function <top level> turns dynamic
|
||||
field Holder.state tests/coverage-fixtures/localized-failure.ts:29:3 (1 request)
|
||||
<- SC1090 operation at tests/coverage-fixtures/localized-failure.ts:29:3: 'any'-typed class fields are not supported yet
|
||||
-> function <top level> turns dynamic
|
||||
field Cache.tokens tests/coverage-fixtures/localized-failure.ts:43:3 (1 request)
|
||||
<- SC2020 operation at tests/coverage-fixtures/localized-failure.ts:43:3: 'WeakRef<object>' is part of the standard library types but has no scriptc lowering yet
|
||||
-> function <top level> turns dynamic
|
||||
function <top level> tests/coverage-fixtures/localized-failure.ts:1:1 (8 sites inside)
|
||||
function listAll tests/coverage-fixtures/localized-failure.ts:23:1 (1 request, 1 site inside)
|
||||
<- SC2004 cascade at tests/coverage-fixtures/localized-failure.ts:51:13: uses of 'listAll' inherit the blocker on its declaration
|
||||
-> function <top level> turns dynamic
|
||||
function pick tests/coverage-fixtures/localized-failure.ts:36:1 (2 sites inside)
|
||||
binding progress in listAll tests/coverage-fixtures/localized-failure.ts:23:31 (1 request)
|
||||
<- SC2009 type at tests/coverage-fixtures/localized-failure.ts:23:31: values of type 'Callbacks | undefined' cannot be compiled: the union shape is supported, and unions in the 'string | object' family ride the checked-dynamic representation wholesale, but 'Map<string,…
|
||||
-> function listAll turns dynamic
|
||||
binding callbacks in pick tests/coverage-fixtures/localized-failure.ts:37:9 (2 requests)
|
||||
<- SC2009 type at tests/coverage-fixtures/localized-failure.ts:37:9: values of type 'Callbacks | undefined' cannot be compiled: the union shape is supported, and unions in the 'string | object' family ride the checked-dynamic representation wholesale, but 'Map<string,…
|
||||
-> function pick turns dynamic
|
||||
<- SC2004 cascade at tests/coverage-fixtures/localized-failure.ts:38:10: uses of 'callbacks' inherit the blocker on its declaration
|
||||
-> function pick turns dynamic
|
||||
binding s tests/coverage-fixtures/localized-failure.ts:49:7 (1 request)
|
||||
<- SC2004 cascade at tests/coverage-fixtures/localized-failure.ts:50:13: uses of 's' inherit the blocker on its declaration
|
||||
-> function <top level> turns dynamic
|
||||
@@ -428,6 +428,38 @@ test("--explain-dynamic attributes each refusal to the unit the dynamic tier wou
|
||||
);
|
||||
});
|
||||
|
||||
test("--explain-dynamic lists the type failures localized failure would turn into dynamic slots", async () => {
|
||||
const { coverage } = analyze(fixture("localized-failure.ts"), { explainDynamic: true });
|
||||
const report = coverage.tiers!.localizedFailure;
|
||||
expect(report.errors).toBe(11);
|
||||
expect(report.typeFailures).toEqual({ degradable: 4, refused: 1 });
|
||||
expect(
|
||||
report.slots.map((s) => [
|
||||
s.stage,
|
||||
s.slot,
|
||||
s.name,
|
||||
s.family,
|
||||
s.rejects?.split(" @")[0],
|
||||
s.cascade,
|
||||
]),
|
||||
).toEqual([
|
||||
["L2", "field", "tools", "component", "class Session", 2],
|
||||
["L2", "param", "progress", "component", "function listAll", 1],
|
||||
["L2", "field", "state", "dyn-field-policy", "class Holder", 1],
|
||||
["L1", "local", "callbacks", "component", "statement", 1],
|
||||
]);
|
||||
// The WeakRef field stays refused, so its class and the construction
|
||||
// that uses it stay errors.
|
||||
expect(report.clearedThroughL2).toBe(9);
|
||||
expect(report.remainingByCode).toEqual({ SC1090: 1, SC2020: 1 });
|
||||
const relative = JSON.parse(
|
||||
JSON.stringify(coverage.tiers).replaceAll(repoRoot + "/", ""),
|
||||
) as typeof coverage.tiers;
|
||||
await expect(renderExplainDynamic(relative!)).toMatchFileSnapshot(
|
||||
"__snapshots__/coverage-explain-localized-failure.txt",
|
||||
);
|
||||
});
|
||||
|
||||
test("CLI: --tier and --explain-dynamic", () => {
|
||||
const scriptcCli = join(repoRoot, "packages/cli/src/main.ts");
|
||||
const outDir = mkdtempSync(join(tmpdir(), "scriptc-tier-"));
|
||||
|
||||
Reference in New Issue
Block a user