Files
scriptc/tests/corpus/3171-method-census-data-writes.ts
Malte Ubl 2625ae9c9e Keep data writes out of the class method replacement census
The method-replacement census marks a method name as an observed slot
when some write might replace it, and calls of observed methods go
through a checked property lookup whose result converts back to the
declared return type. Two kinds of data writes over-approximated that:

- A computed write into a string-keyed dictionary marked every method in
  the program unless the dictionary's values were primitives. Class
  instances have no implicit index signature, so such a dictionary is a
  class view only when the class declares the index signature, and then
  every method must be assignable to its value type. Any value type no
  function can inhabit now qualifies: primitives, or non-callable object
  types that require a property functions lack, or weak types sharing no
  property with functions.
- A named write like `this.literal = token` marked `literal` for every
  class. The write can only replace a method when the written property
  can hold a function, so property writes are now judged by the written
  property's type (prefetched in one batch).

Compiling vercel-labs/tsc-ts failed after the canary promotion: one
`changedProjects[id] = {...}` write marked every method, OptionNameMap.get
and the JSON parser's literal() calls returned unknown, and their Map-typed
results could not convert back (SC1100/SC1101, 61 errors).

Also skip abstract methods when materializing observed prototype slots.
An abstract declaration emits no prototype member, so lookup continues
to the ancestors; building its value instead threw an uncaught
PoisonError out of the reachability fixed point (the tsc-ts crash).

Tests: corpus 3171 (fails with SC1100/SC1101 before this change) matches
Node in the plain and sanitized lanes; the 112 corpus programs matching
abstract|prototype|method|callback pass; class-dynamic-dispatch unit
tests pass. tsc-ts now builds and type-checks itself with output
identical to tsgo 7.0.2.
2026-10-08 14:32:33 -07:00

91 lines
2.9 KiB
TypeScript

// Writes that cannot hold a function never replace a class method, so they
// must not turn same-named method calls elsewhere into dynamic dispatch:
// a string-keyed dictionary of data records, and constructor writes to data
// fields that share a name with an unrelated class's method.
interface Entry {
name: string;
weight: number;
aliases: Map<string, number> | undefined;
}
interface Changes {
changed?: string[];
deleted?: string[];
}
type Json =
| { type: "null" }
| { type: "boolean"; value: boolean }
| { type: "object"; entries: Map<string, Json> };
class Registry {
private readonly byName = new Map<string, Entry>();
constructor(entries: readonly Entry[]) {
for (const entry of entries) this.byName.set(entry.name.toLowerCase(), entry);
}
get(name: string): Entry | undefined {
return this.byName.get(name.toLowerCase());
}
}
class Scanner {
private pos = 0;
private readonly text: string;
constructor(text: string) {
this.text = text;
}
literal(word: string, value: Json): Json | undefined {
if (!this.text.startsWith(word, this.pos)) return undefined;
this.pos += word.length;
return value;
}
}
class Token {
readonly text: string;
constructor(text: string) {
this.text = text;
}
}
class LiteralNode {
literal: Token;
value: string;
constructor(literal: Token) {
this.literal = literal;
this.value = literal.text;
}
}
function collect(ids: readonly string[]): Record<string, Changes> | undefined {
let changes: Record<string, Changes> | undefined;
for (const id of ids) {
changes ??= {};
changes[id] = { changed: [id] };
}
return changes;
}
function weightOf(entry: Entry | undefined): number {
return entry === undefined ? -1 : entry.weight;
}
function show(json: Json | undefined): string {
if (json === undefined) return "none";
if (json.type === "object") return `object ${json.entries.size}`;
return json.type === "boolean" ? `boolean ${json.value}` : "null";
}
const registry = new Registry([
{ name: "Alpha", weight: 3, aliases: new Map([["a", 1]]) },
{ name: "beta", weight: 5, aliases: undefined },
]);
console.log(weightOf(registry.get("alpha")), weightOf(registry.get("BETA")), weightOf(registry.get("gamma")));
const scanner = new Scanner("truenull");
console.log(show(scanner.literal("true", { type: "boolean", value: true })));
console.log(show(scanner.literal("true", { type: "boolean", value: true })));
console.log(show(scanner.literal("null", { type: "null" })));
console.log(show(new Scanner("{}").literal("{}", { type: "object", entries: new Map() })));
console.log(registry.get("alpha")?.aliases?.get("a"), registry.get("beta")?.aliases);
const node = new LiteralNode(new Token("42"));
console.log(node.literal.text, node.value);
const changes = collect(["p1", "p2"]);
for (const [id, change] of Object.entries(changes ?? {})) console.log(id, change.changed?.join(","), change.deleted);
console.log(collect([]));