Name source files relative to the entry's directory in structural positions

Structural positions started with a file's absolute path, so lowered
names (closures, recursive records and unions, and every name keyed by a
type that holds one) changed with the directory the program was compiled
in. Union arms are ordered by their types' keys, so a program compiled in
a new temporary directory could also get its union arms in another order.
union-retype.test.ts compiles each program in a fresh temporary
directory, and in about one run in ten the arms of the visitor's unions
came out in an order where no retag keeps its tags, which failed "lowered
identity retags share the box".

Positions now name a file by its path relative to the entry's directory
(default library files keep their base name), so the same program gets
the same names wherever it sits.
This commit is contained in:
Chris Tate
2026-10-22 09:39:07 -05:00
parent 8b4333b166
commit 279cdcffae
2 changed files with 13 additions and 4 deletions
@@ -181,7 +181,7 @@ export { newFnCtx, type FnCtx } from "./function-context.js";
* - Lexical scoping is resolved here: locals get function-unique ids
* ("x.0", "x.1" for shadowing); the IR is scope-flat.
*/
import { dirname, resolve } from "node:path";
import { dirname, relative, resolve } from "node:path";
import { bindingSource } from "../binding-source.js";
import { tsgoPath } from "../dts-paths.js";
import * as ts from "../ts7/adapter.js";
@@ -3192,12 +3192,14 @@ export class Lowerer {
errorPropertyDispatchRevision: string | null = null;
/** Structural source positions (structural-positions.ts) for names that
* stay the same when unrelated code moves. */
/** Default library files are named by their base name: two checkers
* install them in different directories. */
/** Files are named by their path relative to the entry's directory, so
* names do not depend on where the program sits, and default library
* files by their base name: two checkers install them in different
* directories. */
readonly positions = new StructuralPositions((sf) =>
this.program.isSourceFileDefaultLibrary(sf)
? `lib:${sf.fileName.slice(sf.fileName.lastIndexOf("/") + 1)}`
: sf.fileName,
: relative(dirname(this.entry.fileName), sf.fileName),
);
/** The outermost node whose token span is exactly `loc`, or null. */
@@ -110,3 +110,10 @@ console.log(box.value);
expect(locals).toHaveLength(2);
expect(locals[0]!.name).not.toBe(locals[1]!.name);
});
test("names do not depend on the directory the program sits in", async () => {
// Each call lowers in its own temporary directory.
const [first] = await lower(recursive);
const [second] = await lower(recursive);
expect(names(second!)).toEqual(names(first!));
});