@cplieger/deadset-ts 2.3.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -16
- package/dist/src/alias-chain.js +96 -0
- package/dist/src/analysis.js +50 -16
- package/dist/src/components.js +289 -0
- package/dist/src/discover.js +98 -24
- package/dist/src/graph.js +120 -0
- package/dist/src/index.js +0 -2
- package/dist/src/matrix.js +138 -0
- package/dist/src/print.js +3 -6
- package/dist/src/references.js +162 -79
- package/dist/src/resolve.js +82 -20
- package/dist/src/roots.js +14 -37
- package/dist/src/run.js +11 -7
- package/dist/src/schema.js +1 -1
- package/dist/src/session.js +16 -5
- package/dist/src/sweep.js +152 -0
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/alias-chain.ts +135 -0
- package/src/analysis.ts +85 -24
- package/src/components.ts +372 -0
- package/src/config.ts +23 -2
- package/src/discover.ts +144 -29
- package/src/graph.ts +187 -0
- package/src/index.ts +0 -20
- package/src/matrix.ts +212 -0
- package/src/print.ts +5 -6
- package/src/references.ts +235 -93
- package/src/resolve.ts +117 -24
- package/src/roots.ts +15 -46
- package/src/run.ts +11 -7
- package/src/schema.ts +5 -2
- package/src/session.ts +18 -5
- package/src/sweep.ts +224 -0
- package/src/version.ts +1 -1
package/src/roots.ts
CHANGED
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
* The root set of one project: every declaration the analysis keeps live without a
|
|
3
3
|
* reference, and why.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* something outside the program reaches is live under
|
|
9
|
-
* candidate under
|
|
5
|
+
* Every root seeds REACHABILITY, and no root counts as a reference. A root of a kind
|
|
6
|
+
* that names a caller is live under reference counting as well, which the sweep
|
|
7
|
+
* decides; a root that only supposes a consumer is not, so an exported declaration
|
|
8
|
+
* something outside the program reaches is live under reachability and still a
|
|
9
|
+
* candidate under reference counting.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import type { SourceFile } from "@typescript/native/unstable/ast";
|
|
13
|
-
import
|
|
13
|
+
import { aliasChains } from "./alias-chain.ts";
|
|
14
14
|
import { REPOSITORY_DOCUMENT } from "./config.ts";
|
|
15
15
|
import { entryPoints, type EntryRule } from "./entry-points.ts";
|
|
16
16
|
import type { Finding } from "./finding.ts";
|
|
@@ -63,7 +63,8 @@ export interface Roots {
|
|
|
63
63
|
* reason it is live.
|
|
64
64
|
*
|
|
65
65
|
* The member says which relation the set feeds: every declaration here is live
|
|
66
|
-
* under reachability
|
|
66
|
+
* under reachability. A root of a kind that names a caller is also live under
|
|
67
|
+
* reference counting, which the sweep decides.
|
|
67
68
|
*/
|
|
68
69
|
readonly liveUnderReachability: readonly Root[];
|
|
69
70
|
/**
|
|
@@ -230,12 +231,10 @@ export function roots<Brand>(
|
|
|
230
231
|
enter(point.file, point.rule, point.source, point.exports);
|
|
231
232
|
}
|
|
232
233
|
|
|
233
|
-
const exportsOf = exportTables(
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
files.byName,
|
|
238
|
-
);
|
|
234
|
+
const exportsOf = exportTables(project, held, [
|
|
235
|
+
...entered.filter((entry) => entry.exports).map((entry) => entry.file),
|
|
236
|
+
...published,
|
|
237
|
+
]);
|
|
239
238
|
for (const entry of entered) {
|
|
240
239
|
const id = fileIds.get(entry.file.fileName);
|
|
241
240
|
if (id === undefined) {
|
|
@@ -282,7 +281,6 @@ function exportTables<Brand>(
|
|
|
282
281
|
project: ProjectView<Brand>,
|
|
283
282
|
held: Inventory,
|
|
284
283
|
wanted: readonly SourceFile[],
|
|
285
|
-
ownFiles: ReadonlyMap<string, SourceFile>,
|
|
286
284
|
): ReadonlyMap<string, readonly string[]> {
|
|
287
285
|
const unique = [...new Map(wanted.map((file) => [file.fileName, file])).values()];
|
|
288
286
|
const tables = new Map<string, readonly string[]>();
|
|
@@ -290,36 +288,7 @@ function exportTables<Brand>(
|
|
|
290
288
|
return tables;
|
|
291
289
|
}
|
|
292
290
|
const modules = project.symbolsAt(unique.map((file) => project.handle(file)));
|
|
293
|
-
const chains =
|
|
294
|
-
|
|
295
|
-
const chainOf = (symbol: TSSymbol): readonly string[] => {
|
|
296
|
-
const cached = chains.get(symbol.id);
|
|
297
|
-
if (cached !== undefined) {
|
|
298
|
-
return cached;
|
|
299
|
-
}
|
|
300
|
-
const ids: string[] = [];
|
|
301
|
-
const walked = new Set<number>();
|
|
302
|
-
let at: TSSymbol | undefined = symbol;
|
|
303
|
-
while (at !== undefined && !walked.has(at.id)) {
|
|
304
|
-
walked.add(at.id);
|
|
305
|
-
for (const handle of at.declarations) {
|
|
306
|
-
if (!ownFiles.has(handle.path)) {
|
|
307
|
-
continue;
|
|
308
|
-
}
|
|
309
|
-
const node = project.declarationAt(handle)?.node;
|
|
310
|
-
const id =
|
|
311
|
-
node === undefined
|
|
312
|
-
? undefined
|
|
313
|
-
: held.declarations.get(nodeKey(node.getSourceFile(), node));
|
|
314
|
-
if (id !== undefined && !ids.includes(id)) {
|
|
315
|
-
ids.push(id);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
at = project.aliasStepOf(at);
|
|
319
|
-
}
|
|
320
|
-
chains.set(symbol.id, ids);
|
|
321
|
-
return ids;
|
|
322
|
-
};
|
|
291
|
+
const chains = aliasChains(project, held);
|
|
323
292
|
|
|
324
293
|
unique.forEach((file, index) => {
|
|
325
294
|
const module = modules[index];
|
|
@@ -329,8 +298,8 @@ function exportTables<Brand>(
|
|
|
329
298
|
}
|
|
330
299
|
const ids = new Set<string>();
|
|
331
300
|
for (const exported of project.checker.getExportsOfModule(module)) {
|
|
332
|
-
for (const
|
|
333
|
-
ids.add(id);
|
|
301
|
+
for (const target of chains.chainOf(exported)) {
|
|
302
|
+
ids.add(target.id);
|
|
334
303
|
}
|
|
335
304
|
}
|
|
336
305
|
tables.set(file.fileName, [...ids]);
|
package/src/run.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
import type { Host } from "./host.ts";
|
|
10
10
|
import { joinPath, resolvePath } from "./paths.ts";
|
|
11
11
|
import { printConfig } from "./print.ts";
|
|
12
|
-
import { resolve } from "./resolve.ts";
|
|
12
|
+
import { resolve, resolveMatrix } from "./resolve.ts";
|
|
13
13
|
import { readScope, ScopeError, scopeForDir, type Scope } from "./scope.ts";
|
|
14
14
|
import { diagnosticsOf, openEngine, runSession, type Engine } from "./session.ts";
|
|
15
15
|
import { CONTRACT_VERSION } from "./version.ts";
|
|
@@ -245,8 +245,10 @@ function printConfigVerb(args: readonly string[], out: Writer, err: Writer, host
|
|
|
245
245
|
}
|
|
246
246
|
|
|
247
247
|
/**
|
|
248
|
-
* Writes the
|
|
249
|
-
*
|
|
248
|
+
* Writes the name of every project one run analyzes, one per line, having read each
|
|
249
|
+
* project's diagnostics first: the identifier the build matrix declares for it, or,
|
|
250
|
+
* where discovery derived the project, its configuration file's path below the
|
|
251
|
+
* target root.
|
|
250
252
|
*/
|
|
251
253
|
function printProjectsVerb(
|
|
252
254
|
args: readonly string[],
|
|
@@ -258,14 +260,16 @@ function printProjectsVerb(
|
|
|
258
260
|
let engine: Engine | undefined;
|
|
259
261
|
try {
|
|
260
262
|
const options = readOptions(args);
|
|
263
|
+
const matrix = resolveMatrix(inputsOf(host, options));
|
|
261
264
|
const scope = scopeOf(host, options);
|
|
262
265
|
engine = openClient(false);
|
|
263
|
-
const discovered = discoverProjects(engine, host, scope);
|
|
266
|
+
const discovered = discoverProjects(engine, host, scope, matrix);
|
|
267
|
+
const ids = new Map(discovered.projects.map((project) => [project.configFile, project.id]));
|
|
264
268
|
const failures: string[] = [];
|
|
265
269
|
const { projects } = runSession(engine, discovered.configFiles, (project) => {
|
|
266
270
|
const errors = diagnosticErrors(diagnosticsOf(project));
|
|
267
271
|
failures.push(...errors.map(renderDiagnostic));
|
|
268
|
-
return project.configFile;
|
|
272
|
+
return ids.get(project.configFile) ?? project.configFile;
|
|
269
273
|
});
|
|
270
274
|
if (failures.length > 0) {
|
|
271
275
|
for (const line of failures) {
|
|
@@ -274,8 +278,8 @@ function printProjectsVerb(
|
|
|
274
278
|
err.write(`deadset-ts: ${String(failures.length)} error(s); no answer was produced\n`);
|
|
275
279
|
return EXIT_FAILURE;
|
|
276
280
|
}
|
|
277
|
-
for (const
|
|
278
|
-
out.write(`${
|
|
281
|
+
for (const id of projects) {
|
|
282
|
+
out.write(`${id}\n`);
|
|
279
283
|
}
|
|
280
284
|
return EXIT_CLEAN;
|
|
281
285
|
} catch (error: unknown) {
|
package/src/schema.ts
CHANGED
|
@@ -8,7 +8,10 @@ import { ConfigError } from "./config.ts";
|
|
|
8
8
|
* holds no value of its own.
|
|
9
9
|
* - `map` is an object whose member names the key list leaves open, its values
|
|
10
10
|
* scalars: the severity and provenance objects.
|
|
11
|
-
* - `list` is an array whose entries are objects with declared members.
|
|
11
|
+
* - `list` is an array whose entries are objects with declared members. Where
|
|
12
|
+
* the key list admits an entry in more than one shape, the members are every
|
|
13
|
+
* shape's together, and which of them one entry may hold is the decoder's
|
|
14
|
+
* refusal, not the walk's.
|
|
12
15
|
* - `object` is one setting written as an object: its member names are declared
|
|
13
16
|
* and a source supplies the object whole.
|
|
14
17
|
*/
|
|
@@ -44,7 +47,7 @@ export const SCHEMA_ROOT: KeyNode = {
|
|
|
44
47
|
consumer_tests: leaf,
|
|
45
48
|
configurations: {
|
|
46
49
|
kind: "list",
|
|
47
|
-
members: { id: leaf, os: leaf, arch: leaf, tags: leaf },
|
|
50
|
+
members: { id: leaf, os: leaf, arch: leaf, tags: leaf, project: leaf },
|
|
48
51
|
},
|
|
49
52
|
matrix: { kind: "section", members: { complete: leaf } },
|
|
50
53
|
template_dirs: leaf,
|
package/src/session.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
} from "@typescript/native/unstable/sync";
|
|
13
13
|
import { isIdentifier } from "@typescript/native/unstable/ast";
|
|
14
14
|
import type { Node, SourceFile } from "@typescript/native/unstable/ast";
|
|
15
|
+
import { DiscoveryError } from "./discover.ts";
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* The compiler the analysis reads through. It is a client over a protocol to a
|
|
@@ -218,13 +219,16 @@ export interface SessionResult<Result> {
|
|
|
218
219
|
|
|
219
220
|
/**
|
|
220
221
|
* Runs `visit` over every project the compiler configurations name, inside one
|
|
221
|
-
* snapshot of one client.
|
|
222
|
+
* snapshot of one client, in the order they are named, and answers in that order.
|
|
222
223
|
*
|
|
223
224
|
* The snapshot is opened once with every configuration, so each becomes a project
|
|
224
225
|
* with its own program and checker; it is never updated, because the analysis
|
|
225
|
-
* reads and the tree does not change under it.
|
|
226
|
-
*
|
|
227
|
-
*
|
|
226
|
+
* reads and the tree does not change under it. The order the snapshot lists its
|
|
227
|
+
* projects in is the compiler's own, so each named configuration is looked up in it
|
|
228
|
+
* instead, and a configuration the snapshot opened no project for ends the run: the
|
|
229
|
+
* run would otherwise answer for fewer projects than it was asked to analyze. The
|
|
230
|
+
* snapshot is disposed once when the last project has been visited, and the client
|
|
231
|
+
* is released after it, whether the visit completed or threw.
|
|
228
232
|
*/
|
|
229
233
|
export function runSession<Result>(
|
|
230
234
|
engine: Engine,
|
|
@@ -234,7 +238,16 @@ export function runSession<Result>(
|
|
|
234
238
|
try {
|
|
235
239
|
const snapshot = engine.updateSnapshot(configFiles);
|
|
236
240
|
try {
|
|
237
|
-
const
|
|
241
|
+
const opened = new Map(
|
|
242
|
+
snapshot.getProjects().map((project) => [project.configFileName, project]),
|
|
243
|
+
);
|
|
244
|
+
const projects = configFiles.map((configFile) => {
|
|
245
|
+
const project = opened.get(configFile);
|
|
246
|
+
if (project === undefined) {
|
|
247
|
+
throw new DiscoveryError(`${configFile} opened no project`, []);
|
|
248
|
+
}
|
|
249
|
+
return visit(viewOf(project));
|
|
250
|
+
});
|
|
238
251
|
return { projects, timing: engine.getTimingInfo() };
|
|
239
252
|
} finally {
|
|
240
253
|
snapshot.dispose();
|
package/src/sweep.ts
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which declarations of one graph are dead, and under which liveness relation.
|
|
3
|
+
* Reference counting holds a declaration live when any declaration references it,
|
|
4
|
+
* whatever that one's own liveness, or when it is a root of a kind that names a caller;
|
|
5
|
+
* reachability holds it live when a path of references reaches it from a root. So a
|
|
6
|
+
* declaration only unreachable ones reference is live under the first, dead under the second.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { namesACaller, OUTSIDE, type Graph } from "./graph.ts";
|
|
10
|
+
|
|
11
|
+
/** The liveness relation that decided a declaration, as a finding spells it. */
|
|
12
|
+
export type Relation = "reference-counting" | "reachability";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The run's reference mode, decided once by the composition and read by every stage
|
|
16
|
+
* that counts references.
|
|
17
|
+
*/
|
|
18
|
+
export interface Mode {
|
|
19
|
+
/**
|
|
20
|
+
* Counts no reference a test file made, under either relation. Every reference a root
|
|
21
|
+
* in a test file makes is such a reference, so nothing only a test reaches is live.
|
|
22
|
+
*/
|
|
23
|
+
readonly production: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** What one sweep runs over besides the graph. */
|
|
27
|
+
export interface SweepInput {
|
|
28
|
+
/**
|
|
29
|
+
* The declarations a matched suppression names. A mark holds its declaration live
|
|
30
|
+
* under both relations and seeds reachability through every reference it makes, so
|
|
31
|
+
* nothing only a marked declaration references is dead.
|
|
32
|
+
*/
|
|
33
|
+
readonly marked: readonly string[];
|
|
34
|
+
readonly mode: Mode;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** One dead declaration, the relation that found it, and the counts a kind reads. */
|
|
38
|
+
export interface Candidate {
|
|
39
|
+
readonly id: string;
|
|
40
|
+
/** The references a production file made to the declaration, whatever the mode counts. */
|
|
41
|
+
readonly productionRefs: number;
|
|
42
|
+
/** The references a test file made to it. */
|
|
43
|
+
readonly testRefs: number;
|
|
44
|
+
/**
|
|
45
|
+
* Reference counting where the mode counts no reference to the declaration, and
|
|
46
|
+
* reachability otherwise, so a declaration dead under both carries the stronger claim.
|
|
47
|
+
*/
|
|
48
|
+
readonly relation: Relation;
|
|
49
|
+
/**
|
|
50
|
+
* Whether the test-of-dead-code rule admitted the declaration: a test declaration whose
|
|
51
|
+
* set of referenced production declarations is non-empty and wholly dead.
|
|
52
|
+
*/
|
|
53
|
+
readonly testOfDeadCode: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** One graph's answer: what each relation holds live, and what is dead. */
|
|
57
|
+
export interface Liveness {
|
|
58
|
+
/**
|
|
59
|
+
* Per declaration, the relations that hold it live, reference counting first. A
|
|
60
|
+
* declaration neither holds live has no entry.
|
|
61
|
+
*/
|
|
62
|
+
readonly liveUnder: ReadonlyMap<string, readonly Relation[]>;
|
|
63
|
+
/** The dead declarations, in the order the inventory holds them. */
|
|
64
|
+
readonly candidates: readonly Candidate[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Answers which declarations of one graph are dead under the input's mode.
|
|
69
|
+
*
|
|
70
|
+
* The order is the order the answers depend on: the callers and the marks are live
|
|
71
|
+
* before either relation runs, the candidate set is every judged declaration at least
|
|
72
|
+
* one relation does not hold live, and the tests of dead code join it.
|
|
73
|
+
*/
|
|
74
|
+
export function sweep(graph: Graph, input: SweepInput): Liveness {
|
|
75
|
+
const marked = graph.symbols.map(() => false);
|
|
76
|
+
for (const id of input.marked) {
|
|
77
|
+
const at = graph.at(id);
|
|
78
|
+
if (at !== OUTSIDE) {
|
|
79
|
+
marked[at] = true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Every root and every mark seeds reachability. A root of a kind that names a caller,
|
|
83
|
+
// and a mark, are live under reference counting as well; a root that only supposes a
|
|
84
|
+
// caller is not.
|
|
85
|
+
const called = graph.symbols.map(() => false);
|
|
86
|
+
for (const root of graph.rooted) {
|
|
87
|
+
if (namesACaller(root.kind)) {
|
|
88
|
+
called[root.at] = true;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const counted = (at: number): number => {
|
|
93
|
+
const made = graph.made[at];
|
|
94
|
+
if (made === undefined) {
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
return input.mode.production ? made.production : made.production + made.test;
|
|
98
|
+
};
|
|
99
|
+
const reached = reachable(graph, marked, input.mode);
|
|
100
|
+
const liveUnder = graph.symbols.map((_symbol, at): readonly Relation[] => {
|
|
101
|
+
const relations: Relation[] = [];
|
|
102
|
+
if (marked[at] === true || called[at] === true || counted(at) > 0) {
|
|
103
|
+
relations.push("reference-counting");
|
|
104
|
+
}
|
|
105
|
+
if (reached[at] === true) {
|
|
106
|
+
relations.push("reachability");
|
|
107
|
+
}
|
|
108
|
+
return relations;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const dead = graph.symbols.map(
|
|
112
|
+
(_symbol, at) => graph.subject[at] === true && (liveUnder[at]?.length ?? 0) < 2,
|
|
113
|
+
);
|
|
114
|
+
const testOfDeadCode = admitTestsOfDeadCode(graph, dead, marked);
|
|
115
|
+
|
|
116
|
+
const candidates: Candidate[] = [];
|
|
117
|
+
const live = new Map<string, readonly Relation[]>();
|
|
118
|
+
graph.symbols.forEach((symbol, at) => {
|
|
119
|
+
const relations = liveUnder[at] ?? [];
|
|
120
|
+
if (relations.length > 0) {
|
|
121
|
+
live.set(symbol.id, relations);
|
|
122
|
+
}
|
|
123
|
+
if (dead[at] !== true) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
candidates.push({
|
|
127
|
+
id: symbol.id,
|
|
128
|
+
productionRefs: graph.made[at]?.production ?? 0,
|
|
129
|
+
testRefs: graph.made[at]?.test ?? 0,
|
|
130
|
+
relation: counted(at) === 0 ? "reference-counting" : "reachability",
|
|
131
|
+
testOfDeadCode: testOfDeadCode[at] === true,
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
return { liveUnder: live, candidates };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Per declaration, whether a seed reaches it. The two seeds reach through different
|
|
140
|
+
* reference sets: a marked declaration is live by a mechanism the analysis cannot see, so
|
|
141
|
+
* no mode withholds a reference it makes, while a root reaches through the references the
|
|
142
|
+
* mode counts. The marks run first, so what they reach is expanded under the wider rule.
|
|
143
|
+
*/
|
|
144
|
+
function reachable(graph: Graph, marked: readonly boolean[], mode: Mode): readonly boolean[] {
|
|
145
|
+
const reached = graph.symbols.map(() => false);
|
|
146
|
+
const expanded = graph.symbols.map(() => false);
|
|
147
|
+
const queue: number[] = [];
|
|
148
|
+
|
|
149
|
+
// Reaching a file reaches its top level: the references written outside every
|
|
150
|
+
// declaration it holds. A reference that reads a file reads the module's namespace,
|
|
151
|
+
// which reaches what the module exports as well. An evaluation runs the top level
|
|
152
|
+
// and reads no export, and a root does not expand either: a file a runtime runs
|
|
153
|
+
// without reading its exports is a root by itself.
|
|
154
|
+
const enter = (at: number, expands: boolean): void => {
|
|
155
|
+
if (!reached[at]) {
|
|
156
|
+
reached[at] = true;
|
|
157
|
+
queue.push(at);
|
|
158
|
+
}
|
|
159
|
+
if (!expands || expanded[at] === true) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
expanded[at] = true;
|
|
163
|
+
for (const exported of graph.exportsOf[at] ?? []) {
|
|
164
|
+
enter(exported, false);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
const walk = (held: boolean): void => {
|
|
168
|
+
for (let at = queue.pop(); at !== undefined; at = queue.pop()) {
|
|
169
|
+
for (const edge of graph.out[at] ?? []) {
|
|
170
|
+
if (!held && mode.production && edge.test) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
enter(edge.to, !edge.evaluation);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
marked.forEach((mark, at) => {
|
|
179
|
+
if (mark) {
|
|
180
|
+
enter(at, false);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
walk(true);
|
|
184
|
+
for (const root of graph.rooted) {
|
|
185
|
+
enter(root.at, false);
|
|
186
|
+
}
|
|
187
|
+
walk(false);
|
|
188
|
+
return reached;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Admits every test declaration whose set of referenced production declarations is
|
|
193
|
+
* non-empty and wholly dead, and returns which it admitted. The set is read from every
|
|
194
|
+
* reference the test makes, not the ones the mode counts: a production sweep counts none,
|
|
195
|
+
* which is what makes the targets dead. A test referencing one live target is never admitted.
|
|
196
|
+
*/
|
|
197
|
+
function admitTestsOfDeadCode(
|
|
198
|
+
graph: Graph,
|
|
199
|
+
dead: boolean[],
|
|
200
|
+
marked: readonly boolean[],
|
|
201
|
+
): readonly boolean[] {
|
|
202
|
+
const admitted = graph.symbols.map(() => false);
|
|
203
|
+
graph.symbols.forEach((_symbol, at) => {
|
|
204
|
+
if (graph.test[at] !== true || graph.subject[at] !== true || marked[at] === true) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
let targets = 0;
|
|
208
|
+
let live = 0;
|
|
209
|
+
for (const edge of graph.out[at] ?? []) {
|
|
210
|
+
if (graph.test[edge.to] === true || graph.subject[edge.to] !== true) {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
targets += 1;
|
|
214
|
+
if (dead[edge.to] !== true) {
|
|
215
|
+
live += 1;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (targets > 0 && live === 0) {
|
|
219
|
+
dead[at] = true;
|
|
220
|
+
admitted[at] = true;
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
return admitted;
|
|
224
|
+
}
|
package/src/version.ts
CHANGED