mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-24 06:14:01 +08:00
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.
91 lines
2.9 KiB
TypeScript
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([]));
|