From 279cdcffae822a41934914bd1f2f71ff4966659f Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 09:39:07 -0500 Subject: [PATCH] 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. --- packages/compiler/src/frontend/lowering/lowerer.ts | 10 ++++++---- packages/compiler/test/stable-names.test.ts | 7 +++++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 7a2bfd9e7..747e396af 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -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. */ diff --git a/packages/compiler/test/stable-names.test.ts b/packages/compiler/test/stable-names.test.ts index 720472556..c82db039d 100644 --- a/packages/compiler/test/stable-names.test.ts +++ b/packages/compiler/test/stable-names.test.ts @@ -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!)); +});