Dead Code
in reviewThis lesson is complete and being validated before release - details may still change.
in reviewThis lesson is complete and being validated before release - details may still change.
dead-code.ts
import type { ClassDecl, Expr, MemberRef, Program, Stmt } from "./program";import { ProgramQuery } from "./program-query";import type { Tracer } from "../trace";import type { SmellDetector } from "./detector";export type DeadKind = "method" | "field" | "statement";export type DeadReason = "unreached" | "unread" | "after-exit" | "dead-store";export interface DeadItem { readonly kind: DeadKind; readonly className: string; /** The method or field for a member item; the method holding the statement for a statement item. */ readonly member: string; /** The statement's id for a statement item; null for a member. */ readonly nodeId: string | null; readonly reason: DeadReason;}export interface DeadCodeReport { readonly entries: readonly MemberRef[]; /** Every method control can reach, as "Class.method", entries first and then in the order the search found them. */ readonly reachable: readonly string[]; /** Unreached methods, then unread fields, then after-exit statements, then dead stores; declaration order within each. */ readonly items: readonly DeadItem[]; readonly smelly: boolean;}type CallExpr = Extract<Expr, { kind: "call" }>;type FieldExpr = Extract<Expr, { kind: "field" }>;/** A live assignment to a field, kept until the search knows whether the field is ever read. */interface Store { readonly className: string; readonly member: string; readonly nodeId: string; readonly field: string;}/** * Dead code, found by reachability analysis: the first half of the * dead-code elimination a compiler runs, applied to a whole program * from the methods the outside world calls. The search is a breadth- * first walk over the call graph from `program.entryPoints`: dequeue a * method, follow every call in it that resolves to a program method * (`this.m()` on the class or an ancestor, `x.m()` on the declared type * of `x`), enqueue what is new. Only live statements are walked: once a * block has returned or thrown, the statements after that point never * run, so a call there revives nothing and the count stays monotone as * the student removes items. Along the way the walk notes every field * a live statement reads, which is every `field` expression that is * not the target of an assignment. * * What is left over is dead, in four kinds and in the order a reader * should delete them: methods the search never reached, fields nothing * reads, statements after a block's exit, and stores into those unread * fields. Removing a caller may strand its callees and removing a store * may free its field, so the list cascades: each removal is one step, * and the next report shows what it exposed. * * Time: O(n) over n statements and expression nodes, each method walked * once. Space: O(methods + fields). */export class DeadCode implements SmellDetector<Program, DeadCodeReport> { constructor(private readonly tracer?: Tracer) {} analyze(program: Program): DeadCodeReport { const query = new ProgramQuery(program); const entries = program.entryPoints ?? []; // ▸ an entry point: alive because the outside calls it entries.forEach((entry, index) => this.tracer?.emit("entry", { className: entry.className, member: entry.member, index, count: entries.length })); const reachable: string[] = []; const queue: MemberRef[] = []; for (const entry of entries) { if (reachable.includes(DeadCode.key(entry))) continue; reachable.push(DeadCode.key(entry)); queue.push(entry); } const read = new Set<string>(); const afterExit = new Map<string, DeadItem[]>(); const stores = new Map<string, Store[]>(); while (queue.length > 0) { const ref = queue.shift()!; const cls = query.class(ref.className); const method = query.method(ref); const locals = ProgramQuery.locals(method); const from = DeadCode.key(ref); const dead: DeadItem[] = []; const stored: Store[] = []; DeadCode.partition( method.body, (stmt) => { DeadCode.scan(stmt, (call) => { const callee = DeadCode.callee(query, cls, locals, call); if (!callee) return; const to = DeadCode.key(callee); const reached = reachable.includes(to); // ▸ a live call, and the program method it lands on this.tracer?.emit("call", { from, to, nodeId: stmt.id, reached }); if (reached) return; reachable.push(to); queue.push(callee); // ▸ a method reached for the first time: alive, and queued this.tracer?.emit("reach", { className: callee.className, member: callee.member, via: from, count: reachable.length }); }, (field, written) => { const target = DeadCode.fieldRef(query, cls, locals, field); if (!target) return; if (written) stored.push({ className: ref.className, member: ref.member, nodeId: stmt.id, field: DeadCode.key(target) }); else read.add(DeadCode.key(target)); }); }, (stmt) => dead.push({ kind: "statement", className: ref.className, member: ref.member, nodeId: stmt.id, reason: "after-exit" }), ); afterExit.set(from, dead); stores.set(from, stored); } const items: DeadItem[] = []; const classes = query.classes(); let methods = 0; for (const cls of classes) { for (const method of cls.methods) { methods++; if (reachable.includes(`${cls.name}.${method.name}`)) continue; // ▸ a method no live call ever landed on this.tracer?.emit("unreachable", { className: cls.name, member: method.name }); items.push({ kind: "method", className: cls.name, member: method.name, nodeId: null, reason: "unreached" }); } } for (const cls of classes) { for (const field of cls.fields) { if (!read.has(`${cls.name}.${field.name}`)) items.push({ kind: "field", className: cls.name, member: field.name, nodeId: null, reason: "unread" }); } } for (const cls of classes) for (const method of cls.methods) items.push(...(afterExit.get(`${cls.name}.${method.name}`) ?? [])); for (const cls of classes) { for (const method of cls.methods) { for (const store of stores.get(`${cls.name}.${method.name}`) ?? []) { if (!read.has(store.field)) items.push({ kind: "statement", className: store.className, member: store.member, nodeId: store.nodeId, reason: "dead-store" }); } } } // ▸ one dead item, with the reason it is dead items.forEach((item, index) => this.tracer?.emit("dead", { kind: item.kind, className: item.className, member: item.member, nodeId: item.nodeId ?? "", reason: item.reason, count: index + 1 })); const smelly = items.length > 0; // ▸ what was reached, what is dead, and the verdict this.tracer?.emit("verdict", { entries: entries.length, methods, reached: reachable.length, dead: items.length, smelly }); return { entries, reachable, items, smelly }; } /** "Class.member": how the report names a reached method and the search keys a field. */ static key(ref: MemberRef): string { return `${ref.className}.${ref.member}`; } /** * Walks a body the way control flow does: each block's statements up * to and including its first return or throw are live and their own * blocks are walked in turn; the statements after that point are * handed to `onDead` and never looked into. */ static partition(body: readonly Stmt[], onLive: (stmt: Stmt) => void, onDead: (stmt: Stmt) => void): void { let exited = false; for (const stmt of body) { if (exited) { onDead(stmt); continue; } onLive(stmt); for (const block of ProgramQuery.children(stmt)) DeadCode.partition(block, onLive, onDead); if (stmt.kind === "return" || stmt.kind === "throw") exited = true; } } /** Every call and every field access in the expressions a statement evaluates itself; an assignment's own target is the one access reported as written. */ static scan(stmt: Stmt, onCall: (call: CallExpr) => void, onField: (field: FieldExpr, written: boolean) => void): void { ProgramQuery.exprs(stmt).forEach((expr, index) => { const target = stmt.kind === "assign" && index === 0 ? expr : null; ProgramQuery.visitExpr(expr, (e) => { if (e.kind === "call") onCall(e); if (e.kind === "field") onField(e, e === target); }); }); } /** The program method a call lands on, named by the class that declares it; null for a call the model cannot follow. */ static callee(query: ProgramQuery, cls: ClassDecl, locals: ReadonlyMap<string, string>, call: CallExpr): MemberRef | null { const owner = query.typeOf(call.target, cls, locals); return owner === null ? null : DeadCode.declaring(query, ProgramQuery.baseType(owner), (c) => c.methods.some((m) => m.name === call.method), call.method); } /** The program field an access names, by its declaring class; null when the target's type is outside the program. */ static fieldRef(query: ProgramQuery, cls: ClassDecl, locals: ReadonlyMap<string, string>, field: FieldExpr): MemberRef | null { const owner = query.typeOf(field.target, cls, locals); return owner === null ? null : DeadCode.declaring(query, ProgramQuery.baseType(owner), (c) => c.fields.some((f) => f.name === field.name), field.name); } /** Every method of the program that calls the member or touches the field, as "Class.method", in declaration order; live or not. */ static users(program: Program, target: MemberRef, kind: "method" | "field"): readonly string[] { const query = new ProgramQuery(program); const out: string[] = []; for (const cls of query.classes()) { for (const method of cls.methods) { if (cls.name === target.className && method.name === target.member) continue; const locals = ProgramQuery.locals(method); let hit = false; ProgramQuery.walk(method.body, (stmt) => DeadCode.scan( stmt, (call) => { if (kind === "method" && DeadCode.same(DeadCode.callee(query, cls, locals, call), target)) hit = true; }, (field) => { if (kind === "field" && DeadCode.same(DeadCode.fieldRef(query, cls, locals, field), target)) hit = true; }, ), ); if (hit) out.push(`${cls.name}.${method.name}`); } } return out; } private static same(a: MemberRef | null, b: MemberRef): boolean { return a !== null && a.className === b.className && a.member === b.member; } private static declaring(query: ProgramQuery, className: string, has: (cls: ClassDecl) => boolean, member: string): MemberRef | null { for (let cls = query.classOrNull(className); cls; cls = cls.extends ? query.classOrNull(cls.extends) : null) { if (has(cls)) return { className: cls.name, member }; } return null; }}4 dead items; 46 printed lines. Still something to remove.