From 61f1329b323cefdb7849ea17c7a2292849c2f6e7 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 23:26:29 -0500 Subject: [PATCH 1/2] Name lowered helpers and closures independently of lowering order Lowering named helpers by the size of their registry at first use (%union.retag.N, %arr.map.N, ...), lifted closures and optional-chain locals by compiler-wide counters (%fnN, chain.N), generic instances by their instance count, and records and unions by creation order (rN, uN). Class expressions, mixin instances, function-local generics, non-exported namespace segments and some module globals carried the byte offset of their declaration. A name therefore depended on which unit lowered first and on the positions of unrelated code. Helpers, instances, records and unions are now named by a hash of the key they are interned by (keyedHelperName), and recursive records and unions by their printed type and declaration. Closures and optional-chain ids are named by their lowering unit, their structural position and a count per key within the unit. Offset-based names use the structural position: the chain of enclosing named declarations and the node's ordinal among same-kind nodes inside the nearest one, so an edit outside that declaration leaves the name unchanged. Two different keys with the same name throw an InternalCompilerError. The module lists its records and unions by id. The generated program is unchanged apart from these names and that order, and test:lowering-order now fails on any name difference. --- package.json | 2 +- packages/compiler/src/contracts/check.ts | 3 +- .../lowering/array-indexed-mutation.ts | 3 +- .../src/frontend/lowering/array-values.ts | 5 +- .../frontend/lowering/builtins/arguments.ts | 3 +- .../lowering/builtins/child-process-async.ts | 3 +- .../lowering/builtins/child-process-sync.ts | 3 +- .../lowering/builtins/disposable-stack.ts | 3 +- .../frontend/lowering/builtins/filesystem.ts | 3 +- .../src/frontend/lowering/builtins/json.ts | 3 +- .../lowering/builtins/operating-system.ts | 7 +- .../frontend/lowering/builtins/performance.ts | 5 +- .../src/frontend/lowering/builtins/process.ts | 5 +- .../frontend/lowering/builtins/promises.ts | 5 +- .../lowering/builtins/promisify-callback.ts | 3 +- .../frontend/lowering/builtins/text-codecs.ts | 3 +- .../src/frontend/lowering/builtins/timers.ts | 2 +- .../src/frontend/lowering/builtins/url.ts | 7 +- .../src/frontend/lowering/builtins/v8.ts | 3 +- .../src/frontend/lowering/call-arguments.ts | 3 +- .../src/frontend/lowering/call-signatures.ts | 7 +- .../src/frontend/lowering/class-assertions.ts | 5 +- .../src/frontend/lowering/class-unions.ts | 3 +- .../lowering/coercions/function-adapters.ts | 11 +- .../frontend/lowering/coercions/structural.ts | 15 +-- .../src/frontend/lowering/coercions/unions.ts | 15 +-- .../frontend/lowering/collection-ingestion.ts | 3 +- .../lowering/containers/array-callbacks.ts | 25 +++-- .../lowering/containers/array-concat.ts | 3 +- .../lowering/containers/array-construction.ts | 7 +- .../lowering/containers/array-methods.ts | 11 +- .../lowering/containers/array-pipelines.ts | 3 +- .../src/frontend/lowering/containers/bytes.ts | 5 +- .../lowering/containers/callback-arguments.ts | 3 +- .../containers/collection-grouping.ts | 5 +- .../lowering/containers/collection-methods.ts | 15 +-- .../lowering/containers/indexed-objects.ts | 15 +-- .../lowering/containers/iterator-helpers.ts | 7 +- .../lowering/containers/string-and-regexp.ts | 11 +- .../lowering/containers/string-replacement.ts | 3 +- .../frontend/lowering/expressions/binary.ts | 3 +- .../lowering/expressions/object-literals.ts | 7 +- .../lowering/expressions/optional-chains.ts | 10 +- .../lowering/expressions/property-access.ts | 3 +- .../lowering/expressions/relational.ts | 5 +- .../frontend/lowering/generic-functions.ts | 17 ++- .../src/frontend/lowering/intern-tables.ts | 7 +- .../frontend/lowering/iterator-adapters.ts | 17 +-- .../frontend/lowering/js-dynamic-objects.ts | 5 +- .../src/frontend/lowering/json-require.ts | 3 +- .../src/frontend/lowering/lifetime-regions.ts | 3 +- .../src/frontend/lowering/lower-assert.ts | 11 +- .../src/frontend/lowering/lower-calls.ts | 49 +++++---- .../src/frontend/lowering/lower-classes.ts | 9 +- .../src/frontend/lowering/lower-dgram.ts | 5 +- .../frontend/lowering/lower-event-emitter.ts | 13 ++- .../src/frontend/lowering/lower-exprs.ts | 9 +- .../src/frontend/lowering/lower-functions.ts | 2 +- .../src/frontend/lowering/lower-inspect.ts | 7 +- .../src/frontend/lowering/lower-mixins.ts | 3 +- .../src/frontend/lowering/lower-mock.ts | 3 +- .../frontend/lowering/lower-modules.test.ts | 4 +- .../src/frontend/lowering/lower-modules.ts | 15 ++- .../src/frontend/lowering/lower-namespaces.ts | 11 +- .../src/frontend/lowering/lower-server.ts | 13 ++- .../src/frontend/lowering/lower-stdlib.ts | 2 +- .../src/frontend/lowering/lower-stream.ts | 15 +-- .../src/frontend/lowering/lower-timers.ts | 7 +- .../src/frontend/lowering/lowerer.test.ts | 30 +++-- .../compiler/src/frontend/lowering/lowerer.ts | 100 ++++++++++++++--- .../src/frontend/lowering/native-ffi.ts | 2 +- .../lowering/object-enumeration-receiver.ts | 3 +- .../src/frontend/lowering/pairs-snapshot.ts | 3 +- .../lowering/promise-executor-adapter.ts | 3 +- .../lowering/require-initialization.ts | 3 +- .../src/frontend/lowering/stable-names.ts | 48 ++++++++ .../src/frontend/lowering/strict-equality.ts | 3 +- .../frontend/lowering/structural-positions.ts | 103 ++++++++++++++++++ packages/compiler/src/frontend/ts7/ast.ts | 90 ++++++++++++++- .../compiler/src/frontend/type-mapper.test.ts | 2 +- packages/compiler/src/frontend/type-mapper.ts | 46 ++++++-- .../test/array-pipeline-fusion.test.ts | 2 +- .../test/format-float-arguments.test.ts | 2 +- .../test/in-runtime-key-emission.test.ts | 2 +- .../compiler/test/type-mapper-unions.test.ts | 3 +- 85 files changed, 693 insertions(+), 256 deletions(-) create mode 100644 packages/compiler/src/frontend/lowering/stable-names.ts create mode 100644 packages/compiler/src/frontend/lowering/structural-positions.ts diff --git a/package.json b/package.json index 5cc30d9f8..d1a8e34ae 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "test:test262": "tsx tests/test262/run.ts", "test:test262:fetch": "node tests/test262/fetch.mjs", "test:cache-identity": "node tests/harness/cache-identity.mjs", - "test:lowering-order": "node scripts/lowering-order.mjs --fail-on content", + "test:lowering-order": "node scripts/lowering-order.mjs --fail-on names", "test:sandbox": "node scripts/sandbox-test.mjs", "test:sandbox:image": "node scripts/sandbox-image.mjs", "test:watch": "vitest", diff --git a/packages/compiler/src/contracts/check.ts b/packages/compiler/src/contracts/check.ts index 5934624bf..c80468961 100644 --- a/packages/compiler/src/contracts/check.ts +++ b/packages/compiler/src/contracts/check.ts @@ -174,7 +174,8 @@ class ContractChecker { return `${owner}.${method[2]}`; } if (fn?.sourceName !== undefined && fn.sourceName !== "") return fn.sourceName; - if (fn !== undefined && /^%fn\d+$/.test(fnName)) return `the function at ${this.where(fn.loc)}`; + if (fn !== undefined && /^%fn\.[0-9a-f]{12}$/.test(fnName)) + return `the function at ${this.where(fn.loc)}`; return fnName.replace(/^%/, ""); } diff --git a/packages/compiler/src/frontend/lowering/array-indexed-mutation.ts b/packages/compiler/src/frontend/lowering/array-indexed-mutation.ts index 6d539212e..0ded460da 100644 --- a/packages/compiler/src/frontend/lowering/array-indexed-mutation.ts +++ b/packages/compiler/src/frontend/lowering/array-indexed-mutation.ts @@ -9,6 +9,7 @@ import { type SrcLoc, typeEquals, } from "../../ir/ir.js"; +import { keyedHelperName } from "./stable-names.js"; import { numLit, varRef } from "../../ir/build.js"; import { typeKey } from "../type-mapper.js"; import type { Lowerer } from "./lowerer.js"; @@ -125,7 +126,7 @@ export function lowerArrayFill( writeNull; let name = lowerer.arrHofHelpers.get(key); if (!name) { - name = "%arr.fill." + lowerer.arrHofHelpers.size; + name = keyedHelperName(lowerer, "%arr.fill", key); lowerer.arrHofHelpers.set(key, name); const a = varRef("a.0", arrType, loc); const n = varRef("n.0", F64, loc); diff --git a/packages/compiler/src/frontend/lowering/array-values.ts b/packages/compiler/src/frontend/lowering/array-values.ts index 0ca8503df..314cdfd24 100644 --- a/packages/compiler/src/frontend/lowering/array-values.ts +++ b/packages/compiler/src/frontend/lowering/array-values.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { InternalCompilerError } from "../../errors.js"; import { dynUndefinedExpr, helperLoc, varRef } from "../../ir/build.js"; import { @@ -252,7 +253,7 @@ export function lowerSafeIndexRead( let name = lowerer.arrHofHelpers.get(key); if (!name) { const at = helperLoc(loc); - name = `%arr.idxOr.${lowerer.arrHofHelpers.size}`; + name = keyedHelperName(lowerer, "%arr.idxOr", key); lowerer.arrHofHelpers.set(key, name); const arrT = arr.type; lowerer.liftedFns.push({ @@ -298,7 +299,7 @@ export function lowerSafeBytesRead( let name = lowerer.arrHofHelpers.get(key); if (!name) { const at = helperLoc(loc); - name = `%bytes.idxOr.${lowerer.arrHofHelpers.size}`; + name = keyedHelperName(lowerer, "%bytes.idxOr", key); lowerer.arrHofHelpers.set(key, name); const b = varRef("b.0", bytesT, at); const i = varRef("i.0", F64, at); diff --git a/packages/compiler/src/frontend/lowering/builtins/arguments.ts b/packages/compiler/src/frontend/lowering/builtins/arguments.ts index 7f862f603..d2fc430dd 100644 --- a/packages/compiler/src/frontend/lowering/builtins/arguments.ts +++ b/packages/compiler/src/frontend/lowering/builtins/arguments.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { BYTES_U8, BOOL, @@ -72,7 +73,7 @@ export function lowerOptionalNumberPredicate( const key = `number.optionalPredicate:${fn}:${typeKey(widened)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%number.optionalPredicate.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%number.optionalPredicate", key); lowerer.valueHelpers.set(key, helper); const input = varRef("value.0", widened, loc); lowerer.liftedFns.push({ diff --git a/packages/compiler/src/frontend/lowering/builtins/child-process-async.ts b/packages/compiler/src/frontend/lowering/builtins/child-process-async.ts index 5590fbe9f..d0991dce5 100644 --- a/packages/compiler/src/frontend/lowering/builtins/child-process-async.ts +++ b/packages/compiler/src/frontend/lowering/builtins/child-process-async.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { recordToEnvPairs } from "./environment.js"; import { lowerChildArgsArg } from "./child-process.js"; import { boolLit, numLit, varRef } from "../../../ir/build.js"; @@ -218,7 +219,7 @@ function execFileAsyncHelper( const key = shell ? "execAsync" : "execFileAsync"; const existing = lowerer.valueHelpers.get(key); if (existing) return { name: existing, shapeId, envShapeId }; - const name = `%${key}.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, `%${key}`, key); lowerer.valueHelpers.set(key, name); const recT: IrType = { kind: "record", shapeId }; const envRecT: IrType = { kind: "record", shapeId: envShapeId }; diff --git a/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts b/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts index 24adcc1ba..b9858e768 100644 --- a/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts +++ b/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { recordToEnvPairs } from "./environment.js"; import { lowerChildArgsArg } from "./child-process.js"; import { boolLit, numLit, strLit, varRef } from "../../../ir/build.js"; @@ -541,7 +542,7 @@ function execStdioModeHelper( const key = `cp.stdiomode:${shapeId}:${fd}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%cp.stdioMode.${fd}.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, `%cp.stdioMode.${fd}`, key); lowerer.arrHofHelpers.set(key, name); const optsT: IrType = { kind: "record", shapeId }; diff --git a/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts b/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts index 1b9a9c03a..375c3f1e7 100644 --- a/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts +++ b/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { locOf } from "../../program.js"; @@ -84,7 +85,7 @@ export function lowerDisposableStackUseCall( const key = `disposableStack.use:${className}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%disposableStack.use.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%disposableStack.use", key); lowerer.valueHelpers.set(key, helper); const context = newFnCtx(false, null, null, VOID); lowerer.fnStack.push(context); diff --git a/packages/compiler/src/frontend/lowering/builtins/filesystem.ts b/packages/compiler/src/frontend/lowering/builtins/filesystem.ts index 0e1c062c5..6b988c491 100644 --- a/packages/compiler/src/frontend/lowering/builtins/filesystem.ts +++ b/packages/compiler/src/frontend/lowering/builtins/filesystem.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { dynUndefinedExpr, boolLit, numLit, varRef } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer, ladderFenceExpr } from "../lowerer.js"; @@ -442,7 +443,7 @@ export function lowerStringOrBytesWrite( const key = `fs.${append ? "append" : "write"}:${dataType.unionId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%fs.writeData.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%fs.writeData", key); lowerer.valueHelpers.set(key, helper); const body: IrStmt[] = []; for (let tag = 0; tag < arms.length; tag++) { diff --git a/packages/compiler/src/frontend/lowering/builtins/json.ts b/packages/compiler/src/frontend/lowering/builtins/json.ts index 7eb8aa9b2..64f829865 100644 --- a/packages/compiler/src/frontend/lowering/builtins/json.ts +++ b/packages/compiler/src/frontend/lowering/builtins/json.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { isDateJsonSource } from "./dates.js"; import { dynUndefinedExpr, varRef } from "../../../ir/build.js"; import { InternalCompilerError } from "../../../errors.js"; @@ -253,7 +254,7 @@ function lowerOptionalStringifyRoot( const key = `json.optionalString:${value.type.unionId}:${JSON.stringify(indent)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%json.optionalString.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%json.optionalString", key); lowerer.valueHelpers.set(key, helper); const input = varRef("value.0", value.type, loc); const serialized: IrExpr = { diff --git a/packages/compiler/src/frontend/lowering/builtins/operating-system.ts b/packages/compiler/src/frontend/lowering/builtins/operating-system.ts index c10465039..ee189a1f3 100644 --- a/packages/compiler/src/frontend/lowering/builtins/operating-system.ts +++ b/packages/compiler/src/frontend/lowering/builtins/operating-system.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { numLit, strLit, varRef } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; @@ -219,7 +220,7 @@ export function lowerOsCpusCall(lowerer: Lowerer, call: ts.CallExpression, loc: const key = `os.cpus:${typeKey(result)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%os.cpus.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%os.cpus", key); lowerer.valueHelpers.set(key, helper); const index = varRef("i.0", F64, loc); const field = (k: number): IrExpr => ({ @@ -346,7 +347,7 @@ export function lowerPathParseCall( const key = `${fn}:${typeKey(result)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%path.parse.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%path.parse", key); lowerer.valueHelpers.set(key, helper); const parts = varRef("parts.0", arrayOf(STRING), loc); lowerer.liftedFns.push({ @@ -448,7 +449,7 @@ export function lowerPathFormatCall( const key = `${fn}:${typeKey(recordT)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%path.format.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%path.format", key); lowerer.valueHelpers.set(key, helper); const object = varRef("object.0", recordT, loc); const read = (name: string): IrExpr => { diff --git a/packages/compiler/src/frontend/lowering/builtins/performance.ts b/packages/compiler/src/frontend/lowering/builtins/performance.ts index 72304e6a9..bae525c17 100644 --- a/packages/compiler/src/frontend/lowering/builtins/performance.ts +++ b/packages/compiler/src/frontend/lowering/builtins/performance.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { locOf } from "../../program.js"; @@ -330,7 +331,7 @@ function lowerPerfTimelineCall( const key = `perf.${many ? "entries" : timeline.fn}:${typeKey(result)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%perf.${many ? "entries" : member}.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, `%perf.${many ? "entries" : member}`, key); lowerer.valueHelpers.set(key, helper); const argsRef = varRef("a.0", DYN, loc); const whichRef = varRef("w.0", F64, loc); @@ -461,7 +462,7 @@ export function perfEntryTag( const key = `perf.tag:${entryT.shapeId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%perf.tag.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%perf.tag", key); lowerer.valueHelpers.set(key, helper); const e = varRef("e.0", entryT, loc); lowerer.liftedFns.push({ diff --git a/packages/compiler/src/frontend/lowering/builtins/process.ts b/packages/compiler/src/frontend/lowering/builtins/process.ts index 25653f541..842f42b57 100644 --- a/packages/compiler/src/frontend/lowering/builtins/process.ts +++ b/packages/compiler/src/frontend/lowering/builtins/process.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { stdinKeypressListener } from "./readline.js"; import { envSnapshotHelper, lowerProcessLoadEnvFile } from "./environment.js"; import { dynUndefinedExpr, boolLit, numLit, strLit, varRef } from "../../../ir/build.js"; @@ -1561,7 +1562,7 @@ export function lowerProcessMethodCall( const key = `${prefix}usage.diff:${prevT.shapeId}:${t.shapeId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%${prefix}usage.diff.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, `%${prefix}usage.diff`, key); lowerer.valueHelpers.set(key, helper); const pRef: IrExpr = { kind: "varRef", localId: "p.0", type: prevT, loc }; const fieldOf = (name: string): IrExpr => ({ @@ -1636,7 +1637,7 @@ export function lowerProcessMethodCall( const key = `process.memoryUsage:${t.shapeId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%process.memoryUsage.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%process.memoryUsage", key); lowerer.valueHelpers.set(key, helper); lowerer.liftedFns.push({ name: helper, diff --git a/packages/compiler/src/frontend/lowering/builtins/promises.ts b/packages/compiler/src/frontend/lowering/builtins/promises.ts index 668ef01f8..75e527aa5 100644 --- a/packages/compiler/src/frontend/lowering/builtins/promises.ts +++ b/packages/compiler/src/frontend/lowering/builtins/promises.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { checkedPromiseAll, thenReader } from "../checked-promise-all.js"; import { InternalCompilerError } from "../../../errors.js"; import * as ts from "../../ts7/adapter.js"; @@ -55,7 +56,7 @@ function lowerTuplePromiseAll( const key = `${elems.map((e) => typeKey(e.type)).join(",")}->${typeKey(resultT.inner)}`; let name = lowerer.arrHofHelpers.get(`promise.allTuple:${key}`); if (name === undefined) { - name = `%promise.allTuple.${lowerer.arrHofHelpers.size}`; + name = keyedHelperName(lowerer, "%promise.allTuple", `promise.allTuple:${key}`); lowerer.arrHofHelpers.set(`promise.allTuple:${key}`, name); const ctx = newFnCtx(false, null, null, resultT.inner); ctx.isAsync = true; @@ -557,7 +558,7 @@ export function lowerPromiseStaticCall( const key = `promise.try:${typeKey(f.type)}:${args.map((a) => typeKey(a.type)).join(",")}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%promise.try.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%promise.try", key); lowerer.arrHofHelpers.set(key, helper); const fRef: IrExpr = { kind: "varRef", localId: "f.0", type: f.type, loc }; const argLocals = args.map((a, i) => ({ id: `a${i}.0`, name: `a${i}`, type: a.type })); diff --git a/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts b/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts index 822a4fecc..b4dbbf513 100644 --- a/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts +++ b/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { varRef } from "../../../ir/build.js"; @@ -170,7 +171,7 @@ function promisifyMaker(lowerer: Lowerer, shape: CallbackShape, loc: SrcLoc): st const key = `promisify:${typeKey(shape.fnT)}=>${typeKey(shape.resultT)}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const base = `%promisify.${lowerer.arrHofHelpers.size}`; + const base = keyedHelperName(lowerer, "%promisify", key); lowerer.arrHofHelpers.set(key, base); const { fnT, resultT, args, callbackT, callbackParamT, inner, error } = shape; const promiseT = resultT.ret; diff --git a/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts b/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts index 7229d5b84..8d47c8e3a 100644 --- a/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts +++ b/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { isBuiltinClassInstance } from "./receiver-types.js"; import { strLit, varRef, dynUndefinedExpr } from "../../../ir/build.js"; import { staticTextDecoderEncoding } from "../text-decoder-encoding.js"; @@ -129,7 +130,7 @@ function stringDecoderHelper( const key = `strdec.${op}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%strdec.${op}.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, `%strdec.${op}`, key); lowerer.valueHelpers.set(key, name); const recT: IrType = { kind: "record", shapeId }; diff --git a/packages/compiler/src/frontend/lowering/builtins/timers.ts b/packages/compiler/src/frontend/lowering/builtins/timers.ts index 2d531663f..d7cbd63b5 100644 --- a/packages/compiler/src/frontend/lowering/builtins/timers.ts +++ b/packages/compiler/src/frontend/lowering/builtins/timers.ts @@ -85,7 +85,7 @@ export function lowerTimersPromisesSetInterval( const genT = timersPromisesIntervalType(lowerer, expr); if (genT === null) lowerer.badType(expr, lowerer.typeOf(expr)); const yielded = lowerer.coerceInto(valueNode, value, genT.yieldT); - const fnName = `%fn${lowerer.lambdaCounter++}_tpInterval`; + const fnName = lowerer.closureName(loc, "_tpInterval"); const msParam: IrLocal = { id: "%tp.ms", name: "delay", type: F64, mutable: false }; const valueParam: IrLocal = { id: "%tp.value", name: "value", type: genT.yieldT, mutable: false }; const promiseVoid: IrType = { kind: "promise", inner: VOID }; diff --git a/packages/compiler/src/frontend/lowering/builtins/url.ts b/packages/compiler/src/frontend/lowering/builtins/url.ts index dca8a7cc0..2a77fa85c 100644 --- a/packages/compiler/src/frontend/lowering/builtins/url.ts +++ b/packages/compiler/src/frontend/lowering/builtins/url.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { nodeThrowExpr, countedFor, strLit, varRef, dynUndefinedExpr } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer, own } from "../lowerer.js"; @@ -292,7 +293,7 @@ function searchParamsFromTuples( const key = `sp.fromTuples:${row.shapeId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%sp.fromTuples.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%sp.fromTuples", key); lowerer.valueHelpers.set(key, helper); const pairsT = init.type; const sp = (): IrExpr => varRef("sp.0", SEARCH_PARAMS_T, loc); @@ -372,7 +373,7 @@ function lowerOptionalStringSearchParams( const key = `sp.optionalString:${init.type.unionId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%sp.optionalString.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%sp.optionalString", key); lowerer.valueHelpers.set(key, helper); const value = varRef("init.0", init.type, loc); lowerer.liftedFns.push({ @@ -678,7 +679,7 @@ function lowerSpForEachCall( const key = `${arity}:${typeKey(fnRet)}`; let helper = lowerer.spHofHelpers.get(key); if (!helper) { - helper = `%sp.forEach.${lowerer.spHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%sp.forEach", key); lowerer.spHofHelpers.set(key, helper); lowerer.liftedFns.push(buildSpForEachFn(helper, arity, fnRet, loc)); } diff --git a/packages/compiler/src/frontend/lowering/builtins/v8.ts b/packages/compiler/src/frontend/lowering/builtins/v8.ts index 3582ce8ae..989a61a3f 100644 --- a/packages/compiler/src/frontend/lowering/builtins/v8.ts +++ b/packages/compiler/src/frontend/lowering/builtins/v8.ts @@ -5,6 +5,7 @@ * spirit of process.memoryUsage(): approximate values, not V8 heap * figures (a compiled binary embeds no V8 engine). Every other member * fences at its use with the by-design reason (builtinFenceHintOf). */ +import { keyedHelperName } from "../stable-names.js"; import { numLit } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; @@ -55,7 +56,7 @@ export function lowerV8HeapStatisticsCall( const key = `v8.getHeapStatistics:${t.shapeId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%v8.getHeapStatistics.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%v8.getHeapStatistics", key); lowerer.valueHelpers.set(key, helper); const sample = (index: number): IrExpr => ({ kind: "libCall", diff --git a/packages/compiler/src/frontend/lowering/call-arguments.ts b/packages/compiler/src/frontend/lowering/call-arguments.ts index 49f7c8ec0..060ffe9d2 100644 --- a/packages/compiler/src/frontend/lowering/call-arguments.ts +++ b/packages/compiler/src/frontend/lowering/call-arguments.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { forwardedTupleType, pureForwardedValues, stripTypeWrappers } from "./typed-arguments.js"; import { dynUndefinedExpr } from "../../ir/build.js"; import { InternalCompilerError } from "../../errors.js"; @@ -753,7 +754,7 @@ function holeReadingLift(lowerer: Lowerer, from: IrType, to: IrType, loc: SrcLoc const key = `arr.holes:${typeKey(elem)}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%arr.holes.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%arr.holes", key); lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push( buildArrayConversion( diff --git a/packages/compiler/src/frontend/lowering/call-signatures.ts b/packages/compiler/src/frontend/lowering/call-signatures.ts index 34343ad27..dacb4d324 100644 --- a/packages/compiler/src/frontend/lowering/call-signatures.ts +++ b/packages/compiler/src/frontend/lowering/call-signatures.ts @@ -849,7 +849,10 @@ export function collectSignatureInner(lowerer: Lowerer, decl: ts.FunctionDeclara lowerer.genericFnsBySymbol.set(symbol, { decl, baseName: nameText, - qualifiedName: lowerer.qualify(ts.getSourceFileOfNode(decl), nsPathPrefix(decl) + nameText), + qualifiedName: lowerer.qualify( + ts.getSourceFileOfNode(decl), + nsPathPrefix(lowerer, decl) + nameText, + ), typeParams: [], instances: new Map(), implicitParams: implicit, @@ -897,7 +900,7 @@ export function collectSignatureInner(lowerer: Lowerer, decl: ts.FunctionDeclara // "%default" spelling ('%' cannot appear in a user identifier). name: lowerer.qualify( ts.getSourceFileOfNode(decl), - nsPathPrefix(decl) + (decl.name?.text ?? "%default"), + nsPathPrefix(lowerer, decl) + (decl.name?.text ?? "%default"), ), params, returnType, diff --git a/packages/compiler/src/frontend/lowering/class-assertions.ts b/packages/compiler/src/frontend/lowering/class-assertions.ts index 900c32426..52654fab8 100644 --- a/packages/compiler/src/frontend/lowering/class-assertions.ts +++ b/packages/compiler/src/frontend/lowering/class-assertions.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { nodeThrowExpr, varRef } from "../../ir/build.js"; import { BOOL, @@ -122,7 +123,7 @@ export function checkedClassAssertion( const key = `classassert:${typeKey(source.type)}:${typeKey(target)}`; let name = lowerer.coercions.retags.get(key); if (!name) { - name = `%class.assert.${lowerer.coercions.retags.size}`; + name = keyedHelperName(lowerer, "%class.assert", key); lowerer.coercions.retags.set(key, name); const input = varRef("value.0", source.type, loc); const body: IrStmt[] = []; @@ -228,7 +229,7 @@ export function checkedClassUnionAssertion( const key = `classunionassert:${typeKey(value.type)}:${typeKey(target)}`; let name = lowerer.coercions.retags.get(key); if (!name) { - name = `%class.assert.${lowerer.coercions.retags.size}`; + name = keyedHelperName(lowerer, "%class.assert", key); lowerer.coercions.retags.set(key, name); const input = varRef("value.0", value.type, loc); const locals = [{ id: "value.0", name: "value", type: value.type, mutable: false }]; diff --git a/packages/compiler/src/frontend/lowering/class-unions.ts b/packages/compiler/src/frontend/lowering/class-unions.ts index f52026ba4..0021b2cf8 100644 --- a/packages/compiler/src/frontend/lowering/class-unions.ts +++ b/packages/compiler/src/frontend/lowering/class-unions.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import * as ts from "../ts7/adapter.js"; import { BOOL, typeKey, type IrExpr, type IrStmt, type IrType } from "../../ir/ir.js"; import { nodeThrowExpr, varRef } from "../../ir/build.js"; @@ -132,7 +133,7 @@ export function narrowClassUnion( const key = `classUnion:${source.className}:${target.unionId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%class.union.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%class.union", key); lowerer.valueHelpers.set(key, helper); const input = varRef("value.0", source, loc); // Rank by ancestry before sorting: returning zero for unrelated pairs diff --git a/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts b/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts index ffdac19f0..fe3c14605 100644 --- a/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts +++ b/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { dynUndefinedExpr } from "../../../ir/build.js"; import { buildFunctionAdapter } from "./builders.js"; import { InternalCompilerError } from "../../../errors.js"; @@ -33,7 +34,7 @@ export function funcReturnWidthAdapter( const key = `fn:${typeKey(fromT.ret.elem)}:${typeKey(toT.ret.elem)}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%fn.width.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%fn.width", key); lowerer.valueHelpers.set(key, name); lowerer.freshClosureAdapters.add(name); // The returned closure's body: call the captured original, width-map. @@ -163,7 +164,7 @@ function restPackingAdapter( const key = `fnrestadapt:${typeKey(fromT)}:${typeKey(toT)}`; const existing = lowerer.coercions.retags.get(key); if (existing) return existing; - const name = `%fn.adapt.${lowerer.coercions.retags.size}`; + const name = keyedHelperName(lowerer, "%fn.adapt", key); lowerer.coercions.retags.set(key, name); lowerer.freshClosureAdapters.add(name); const fixed = fromT.params.length - 1; @@ -220,7 +221,7 @@ function funcIntoDynRestAdapter( const key = `fnadapt:${typeKey(fromT)}:${typeKey(toT)}`; const existing = lowerer.coercions.retags.get(key); if (existing) return existing; - const name = `%fn.adapt.${lowerer.coercions.retags.size}`; + const name = keyedHelperName(lowerer, "%fn.adapt", key); lowerer.coercions.retags.set(key, name); lowerer.freshClosureAdapters.add(name); const params: IrParam[] = [{ localId: "rest.0", name: "args", type: DYN }]; @@ -376,7 +377,7 @@ export function funcCoerceAdapter( const key = `fnadapt:${typeKey(fromT)}:${typeKey(toT)}`; const existing = lowerer.coercions.retags.get(key); if (existing) return existing; - const name = `%fn.adapt.${lowerer.coercions.retags.size}`; + const name = keyedHelperName(lowerer, "%fn.adapt", key); lowerer.coercions.retags.set(key, name); lowerer.freshClosureAdapters.add(name); const params: IrParam[] = toT.params.map((t, i) => ({ @@ -604,7 +605,7 @@ export function spawnResFnAdapter( const key = `fnspawn:${typeKey(fromT)}:${typeKey(toT)}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%fnval.spawnres.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%fnval.spawnres", key); lowerer.valueHelpers.set(key, name); lowerer.freshClosureAdapters.add(name); const params: IrParam[] = toT.params.map((p, i) => ({ diff --git a/packages/compiler/src/frontend/lowering/coercions/structural.ts b/packages/compiler/src/frontend/lowering/coercions/structural.ts index 496a29607..5b5e283b3 100644 --- a/packages/compiler/src/frontend/lowering/coercions/structural.ts +++ b/packages/compiler/src/frontend/lowering/coercions/structural.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { constructedValue } from "../constructor-returns.js"; import { dynUndefinedExpr, varRef } from "../../../ir/build.js"; import type { FieldLift } from "./structural-plans.js"; @@ -253,7 +254,7 @@ export function unionRecordWidthHelper( const key = `unionWidth:${fromId}:${target.shapeId}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%union.record.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%union.record", key); lowerer.valueHelpers.set(key, name); const type: IrType = { kind: "union", unionId: fromId }; const value = varRef("value.0", type, loc); @@ -312,7 +313,7 @@ export function recordWidthHelper( const key = `rec:${fromId}:${toId}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%rec.width.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%rec.width", key); // Interned BEFORE the body builds: a recursive nested-width field // (self-referential shapes) resolves to this helper itself. lowerer.valueHelpers.set(key, name); @@ -341,7 +342,7 @@ export function tupleArrayWidthHelper( const key = `tuparr:${fromId}:${typeKey(toT.elem)}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%tup.arr.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%tup.arr", key); lowerer.valueHelpers.set(key, name); const fromT: IrType = { kind: "record", shapeId: fromId }; const t: IrExpr = { kind: "varRef", localId: "t.0", type: fromT, loc }; @@ -388,7 +389,7 @@ export function emptyArrayLiftHelper( const key = `emptyarr:${typeKey(fromT.elem)}:${typeKey(toT.elem)}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%arr.empty.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%arr.empty", key); lowerer.valueHelpers.set(key, name); const a: IrExpr = { kind: "varRef", localId: "a.0", type: fromT, loc }; lowerer.liftedFns.push({ @@ -452,7 +453,7 @@ export function arrayWidthHelper( const key = `arr:${typeKey(fromElem)}:${typeKey(toElem)}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%arr.width.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%arr.width", key); lowerer.valueHelpers.set(key, name); const arrT: IrType = { kind: "array", elem: fromElem }; const outT: IrType = { kind: "array", elem: toElem }; @@ -492,7 +493,7 @@ export function objRecordWidthHelper( const key = `obj:${className}:${toId}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%obj.width.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%obj.width", key); lowerer.valueHelpers.set(key, name); const fromT: IrType = { kind: "object", className }; lowerer.liftedFns.push(buildRecordProjection(lowerer, name, fromT, to, plan, loc)); @@ -517,7 +518,7 @@ export function recordClassWidthHelper( const key = `cls:${fromId}:${className}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%cls.width.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%cls.width", key); lowerer.valueHelpers.set(key, name); lowerer.noteEdge(`%${className}.constructor`); const fromT: IrType = { kind: "record", shapeId: fromId }; diff --git a/packages/compiler/src/frontend/lowering/coercions/unions.ts b/packages/compiler/src/frontend/lowering/coercions/unions.ts index 06c0af9c6..434e687a5 100644 --- a/packages/compiler/src/frontend/lowering/coercions/unions.ts +++ b/packages/compiler/src/frontend/lowering/coercions/unions.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { buildUnionNarrow, unionNarrowFailureMessage } from "../union-narrow.js"; import { isSafeToRepeat } from "../expressions/evaluation-safety.js"; import { nodeThrowExpr } from "../../../ir/build.js"; @@ -147,7 +148,7 @@ export function strandedUnitTrap( const key = `strandunit:${typeKey(expected)}:${expr.type.kind}`; let name = lowerer.coercions.retags.get(key); if (!name) { - name = `%unit.strand.${lowerer.coercions.retags.size}`; + name = keyedHelperName(lowerer, "%unit.strand", key); lowerer.coercions.retags.set(key, name); lowerer.liftedFns.push({ name, @@ -227,7 +228,7 @@ export function strandedCoercionTrap( const key = `strand:${expected.unionId}:${typeKey(src)}`; let name = lowerer.coercions.retags.get(key); if (!name) { - name = `%union.strand.${lowerer.coercions.retags.size}`; + name = keyedHelperName(lowerer, "%union.strand", key); lowerer.coercions.retags.set(key, name); const toT: IrType = { kind: "union", unionId: expected.unionId }; lowerer.liftedFns.push({ @@ -275,7 +276,7 @@ export function recordUnionWrapHelper( const key = `recordWrap:${source.shapeId}:${toId}`; const existing = lowerer.coercions.retags.get(key); if (existing) return existing; - const name = `%record.wrap.${lowerer.coercions.retags.size}`; + const name = keyedHelperName(lowerer, "%record.wrap", key); lowerer.coercions.retags.set(key, name); lowerer.liftedFns.push( buildRecordUnionWrap(name, source, to, plan, loc, (lift, value, dst) => @@ -307,7 +308,7 @@ export function recordUnionRouteHelper( (from, dst) => lowerer.widthLiftPlan(from, dst), ); if (!plan) return null; - const name = `%record.route.${lowerer.coercions.retags.size}`; + const name = keyedHelperName(lowerer, "%record.route", key); lowerer.coercions.retags.set(key, name); lowerer.liftedFns.push( buildRecordUnionWrap(name, source, to, plan, loc, (lift, value, dst) => @@ -352,7 +353,7 @@ export function unionRetagHelper( }); const key = `${fromId}:${toId}:${stranded.join(".")}`; const existing = lowerer.coercions.retags.get(key); - const name = existing ?? `%union.retag.${lowerer.coercions.retags.size}`; + const name = existing ?? keyedHelperName(lowerer, "%union.retag", key); if (!existing) { lowerer.coercions.retags.set(key, name); lowerer.liftedFns.push( @@ -410,7 +411,7 @@ export function narrowedArmHelper( lowerer.coercions.checkedNarrows.add(existing); return existing; } - const name = `%union.narrow.${lowerer.coercions.narrows.size}`; + const name = keyedHelperName(lowerer, "%union.narrow", key); lowerer.coercions.narrows.set(key, name); lowerer.coercions.checkedNarrows.add(name); const fn = buildUnionNarrow(name, from, target, loc, (type) => lowerer.fmt(type)); @@ -587,7 +588,7 @@ export function deferredReadHelper( const key = `deferred:${fromId}:${tag}`; const existing = lowerer.coercions.narrows.get(key); if (existing) return existing; - const name = `%deferred.read.${lowerer.coercions.narrows.size}`; + const name = keyedHelperName(lowerer, "%deferred.read", key); lowerer.coercions.narrows.set(key, name); const dflt: IrExpr = target.kind === "bool" diff --git a/packages/compiler/src/frontend/lowering/collection-ingestion.ts b/packages/compiler/src/frontend/lowering/collection-ingestion.ts index 5d14d5492..8fe2a6bbb 100644 --- a/packages/compiler/src/frontend/lowering/collection-ingestion.ts +++ b/packages/compiler/src/frontend/lowering/collection-ingestion.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import * as ts from "../ts7/adapter.js"; import { InternalCompilerError } from "../../errors.js"; import { typeKey } from "../type-mapper.js"; @@ -197,7 +198,7 @@ export function ingestCollection( const key = `ingest:${input.collection ? typeKey(input.collection) + ":" + input.selection : "cursor:" + typeKey(input.element)}:${typeKey(destination)}:${mapper ? typeKey(mapper.type) : "copy"}`; let name = lowerer.arrHofHelpers.get(key); if (!name) { - name = `%collection.ingest.${lowerer.arrHofHelpers.size}`; + name = keyedHelperName(lowerer, "%collection.ingest", key); lowerer.arrHofHelpers.set(key, name); const context = newFnCtx(false, null, null, destination); lowerer.fnStack.push(context); diff --git a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts index eb99592bb..7ca69d9ba 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { boolLit, countedFor, numLit, varRef } from "../../../ir/build.js"; import { transformStmtList } from "../../../ir/traverse.js"; import type { ScrDiagnostic } from "../../../diagnostics/diagnostic.js"; @@ -506,7 +507,7 @@ function arrayHofHelper( const key = `${method}:${typeKey(elem)}:${typeKey(fnRet)}:${arity}:${typeKey(outElem)}${site}${fastT ? ":split" : ""}${present ? ":present" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, `%arr.${method}`, key); lowerer.arrHofHelpers.set(key, name); lowerer.arrHofInfo.set(name, { method, elem, fnRet, arity, outElem, present, split: !!fastT }); const fn = buildArrayHofFn( @@ -619,7 +620,7 @@ function arrayUnionHofHelper( const key = `union:${method}:${receiverT.unionId}:${typeKey(callbackT)}:${typeKey(resultT)}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%arr.union.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, "%arr.union", key); lowerer.arrHofHelpers.set(key, name); const receiver = varRef("a.0", receiverT, loc); const callback = varRef("f.0", callbackT, loc); @@ -1204,7 +1205,7 @@ function findHelper( const key = `${method}:${typeKey(elem)}:${typeKey(resultT)}:${typeKey(fnRet)}:${arity}${site}${present ? ":present" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, `%arr.${method}`, key); lowerer.arrHofHelpers.set(key, name); const arrT = arrayOf(elem); const valueT = callbackValueType(lowerer, elem, present); @@ -1304,7 +1305,7 @@ function findIndexHelper( const key = `${method}:${typeKey(elem)}:${typeKey(fnRet)}:${arity}${site}${fastT ? ":split" : ""}${present ? ":present" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, `%arr.${method}`, key); lowerer.arrHofHelpers.set(key, name); if (!last) lowerer.arrHofInfo.set(name, { @@ -1388,7 +1389,7 @@ function someEveryHelper( const key = `${method}:${typeKey(elem)}:${typeKey(fnRet)}:${arity}${site}${fastT ? ":split" : ""}${present ? ":present" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, `%arr.${method}`, key); lowerer.arrHofHelpers.set(key, name); lowerer.arrHofInfo.set(name, { method, @@ -1587,7 +1588,7 @@ function flatMapHelper( const key = `flatMap:${typeKey(elem)}:${typeKey(fnRet)}:${arity}${site}${fastT ? ":split" : ""}${present ? ":present" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%arr.flatMap.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, "%arr.flatMap", key); lowerer.arrHofHelpers.set(key, name); lowerer.arrHofInfo.set(name, { method: "flatMapArray", @@ -1829,7 +1830,7 @@ function reduceHelper( const key = `${method}:${typeKey(elem)}:${typeKey(accT)}:${typeKey(fnRet)}:${arity}:${hasInit ? "init" : "seed"}${site}${fastT ? ":split" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, `%arr.${method}`, key); lowerer.arrHofHelpers.set(key, name); if (method === "reduce") lowerer.arrHofInfo.set(name, { @@ -2121,7 +2122,7 @@ export function lowerArraySortCall( const key = `${method}:${typeKey(elem)}:${arity}${native ? ":native" : ""}${keyed ? `:keyed${order}${reads ? `:field${reads.length}` : ""}` : ""}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%arr.${method}.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%arr.${method}`, key); lowerer.arrHofHelpers.set(key, helper); const undefinedTag = elem.kind === "union" ? lowerer.armTag(elem.unionId, UNDEFINED_T) : -1; lowerer.liftedFns.push( @@ -2167,7 +2168,7 @@ function sortKeyReader( const cacheKey = `sortKey:${typeKey(elem)}:${sortKeysId(reads.map((read) => ({ order: 1, read })))}`; let reader = lowerer.arrHofHelpers.get(cacheKey); if (!reader) { - reader = `%arr.sortKey.${lowerer.arrHofHelpers.size}`; + reader = keyedHelperName(lowerer, "%arr.sortKey", cacheKey); lowerer.arrHofHelpers.set(cacheKey, reader); lowerer.liftedFns.push(buildSortKeyFn(reader, elem, reads, loc)); } @@ -2308,7 +2309,7 @@ function dynFlatMapHelper(lowerer: Lowerer, elemT: IrType, arity: number, loc: S const key = `dynflatmap:${typeKey(elemT)}:${arity}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%dyn.flatmap.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, "%dyn.flatmap", key); lowerer.arrHofHelpers.set(key, name); lowerer.liftedFns.push(buildDynFlatMapFn(name, elemT, arity, loc)); return name; @@ -2481,7 +2482,7 @@ function dynFilterHelper(lowerer: Lowerer, elemT: IrType, arity: number, loc: Sr const key = `dynfilter:${typeKey(elemT)}:${arity}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%dyn.filter.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, "%dyn.filter", key); lowerer.arrHofHelpers.set(key, name); lowerer.liftedFns.push(buildDynFilterFn(name, elemT, arity, loc)); return name; @@ -2676,7 +2677,7 @@ export function lowerTuplePredicateCall( const key = `tuple.${method}:${typeKey(receiver.type)}:${typeKey(fnArg.type)}:${typeKey(resultT)}:${arity}${callbackSite(fnArg)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%tuple.${method}.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%tuple.${method}`, key); lowerer.arrHofHelpers.set(key, helper); const v = varRef("v.0", elem, loc); const out = varRef("out.0", resultT, loc); diff --git a/packages/compiler/src/frontend/lowering/containers/array-concat.ts b/packages/compiler/src/frontend/lowering/containers/array-concat.ts index 4f6e4ca43..b4e285ee9 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-concat.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-concat.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { identityPreservingWidening } from "../coercions/identity.js"; import { countedFor, numLit, varRef } from "../../../ir/build.js"; import { @@ -28,7 +29,7 @@ export function arrayConcatHelper( const key = `concat:${typeKey(elem)}:${shape.map((type) => (type ? `a:${typeKey(type)}` : "e")).join(";")}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%arr.concat.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, "%arr.concat", key); const arrT = arrayOf(elem); const params = [ { localId: "a.0", name: "a", type: arrT }, diff --git a/packages/compiler/src/frontend/lowering/containers/array-construction.ts b/packages/compiler/src/frontend/lowering/containers/array-construction.ts index 40e4d6436..1f10640c0 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-construction.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-construction.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { generatorDrain } from "../iterator-adapters.js"; import { identityPreservingWidening } from "../coercions/identity.js"; import { liftedFreshSpread } from "../coercions/fresh-spread.js"; @@ -256,7 +257,7 @@ function appendWidenedArrayValues( const key = `insertValues:${typeKey(sourceType.elem)}:${typeKey(elem)}`; let name = lowerer.arrHofHelpers.get(key); if (!name) { - name = `%arr.insertValues.${lowerer.arrHofHelpers.size}`; + name = keyedHelperName(lowerer, "%arr.insertValues", key); lowerer.arrHofHelpers.set(key, name); const outType = arrayOf(elem); const valueType = arrayValueType(lowerer, source.type.elem); @@ -1035,7 +1036,7 @@ export function lowerArrayFromCall( const key = `fromArray:${typeKey(arrT.elem)}:${typeKey(outElem)}:${typeKey(fnRet)}:${arity}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%arr.fromArray.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%arr.fromArray", key); lowerer.arrHofHelpers.set(key, helper); lowerer.liftedFns.push( buildArrayFromArrayFn(lowerer, helper, arrT.elem, outElem, fnRet, arity, loc), @@ -1122,7 +1123,7 @@ export function lowerArrayFromCall( const key = `fromLen:${typeKey(fnRet)}:${arity}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%arr.fromLen.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%arr.fromLen", key); lowerer.arrHofHelpers.set(key, helper); lowerer.liftedFns.push(buildArrayFromLenFn(helper, fnRet, arity, loc)); } diff --git a/packages/compiler/src/frontend/lowering/containers/array-methods.ts b/packages/compiler/src/frontend/lowering/containers/array-methods.ts index e9a130a2d..44182fbda 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-methods.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { liftedFreshSpread } from "../coercions/fresh-spread.js"; import { lowerArrayIteratorValue } from "../iterator-adapters.js"; import { identityPreservingWidening } from "../coercions/identity.js"; @@ -1616,7 +1617,7 @@ function arraySearchHelper( const key = `${method}:${typeKey(elem)}:${typeKey(valueT)}:${typeKey(needleT)}:${directNeedle}:${typeKey(fromT)}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, `%arr.${method}`, key); lowerer.arrHofHelpers.set(key, name); const arrT = arrayOf(elem); const resultT = method === "includes" ? BOOL : F64; @@ -1972,7 +1973,7 @@ export function lowerTupleReadMethodCall( let helper = lowerer.arrHofHelpers.get(key); const resultType = method === "includes" ? BOOL : F64; if (!helper) { - helper = `%tuple.${method}.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%tuple.${method}`, key); lowerer.arrHofHelpers.set(key, helper); const params = [ { localId: "a.0", name: "a", type: receiver.type }, @@ -2034,7 +2035,7 @@ export function lowerTupleReadMethodCall( const key = `tuple.slice:${typeKey(receiver.type)}:${args.length}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%tuple.slice.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%tuple.slice", key); lowerer.arrHofHelpers.set(key, helper); const params = [receiver, ...args].map((arg, i) => ({ localId: i === 0 ? "a.0" : `arg.${i}`, @@ -2125,7 +2126,7 @@ export function lowerTupleReadMethodCall( const key = `tuple.${method}:${typeKey(receiver.type)}:${typeKey(fnArg.type)}:${typeKey(outElem)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%tuple.${method}.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%tuple.${method}`, key); lowerer.arrHofHelpers.set(key, helper); const out = varRef("out.0", outType, loc); const result = varRef("r.0", ret, loc); @@ -2253,7 +2254,7 @@ function lowerArrayAtCall( const key = `at:${typeKey(elem)}:${typeKey(resultT)}:${typeKey(index.type)}`; let name = lowerer.arrHofHelpers.get(key); if (!name) { - name = `%arr.at.${lowerer.arrHofHelpers.size}`; + name = keyedHelperName(lowerer, "%arr.at", key); lowerer.arrHofHelpers.set(key, name); const arrT = arrayOf(elem); diff --git a/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts b/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts index cf45a7575..180f40435 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { boolLit, countedFor, numLit, varRef } from "../../../ir/build.js"; import { BOOL, @@ -851,7 +852,7 @@ class PipelineFusion { if (!built) return null; let name = this.helpers.get(built.key); if (!name) { - name = `%arr.fused.${this.helpers.size}`; + name = keyedHelperName(this.lowerer, "%arr.fused", built.key); this.helpers.set(built.key, name); this.added.push({ ...built.fn, name }); } diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts index bf9821d31..4b20d7bf5 100644 --- a/packages/compiler/src/frontend/lowering/containers/bytes.ts +++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { boolLit, dynUndefinedExpr, numLit, varRef } from "../../../ir/build.js"; import { BUF_NUM_METHODS } from "./buffer-numeric-methods.js"; import * as ts from "../../ts7/adapter.js"; @@ -77,7 +78,7 @@ function lowerBytesSortCall( const cacheKey = `bytes.${method}:${bytesT.elem}:${key}`; let helper = lowerer.arrHofHelpers.get(cacheKey); if (!helper) { - helper = `%bytes.${method}.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%bytes.${method}`, cacheKey); lowerer.arrHofHelpers.set(cacheKey, helper); lowerer.liftedFns.push( buildBytesSortFn(helper, bytesT, arity, hasComparator, copyFirst, order, loc), @@ -270,7 +271,7 @@ function bytesCallbackHelper( const key = `bytes.${method}:${bytesT.elem}:${typeKey(resultT)}:${typeKey(fnRet)}:${arity}:${hasInit ? "init" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%bytes.${method}.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, `%bytes.${method}`, key); lowerer.arrHofHelpers.set(key, name); const reduce = method === "reduce" || method === "reduceRight"; const backward = method === "reduceRight" || method === "findLast" || method === "findLastIndex"; diff --git a/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts b/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts index 18754f088..f39c8e86c 100644 --- a/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts +++ b/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import type { Lowerer } from "../lowerer.js"; import { isJsSourceFile, locOf } from "../../program.js"; @@ -285,7 +286,7 @@ export function truthyFilterCallback( const key = `filterBoolean:${typeKey(valueT)}`; let name = lowerer.arrHofHelpers.get(key); if (!name) { - name = `%arr.filterBoolean.${lowerer.arrHofHelpers.size}`; + name = keyedHelperName(lowerer, "%arr.filterBoolean", key); lowerer.arrHofHelpers.set(key, name); const value = varRef("v.0", valueT, loc); lowerer.liftedFns.push({ diff --git a/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts b/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts index 0bf126830..dae68d518 100644 --- a/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts +++ b/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { countedFor, numLit, varRef } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import type { Lowerer } from "../lowerer.js"; @@ -104,7 +105,7 @@ export function lowerGroupByStaticCall( const key = `map.groupBy:${typeKey(items.type)}:${typeKey(f.type)}`; let helper = lowerer.mapHofHelpers.get(key); if (!helper) { - helper = `%map.groupBy.${lowerer.mapHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%map.groupBy", key); lowerer.mapHofHelpers.set(key, helper); lowerer.liftedFns.push(buildGroupByFn(lowerer, helper, items.type, f.type, resultT, loc)); } @@ -141,7 +142,7 @@ export function lowerGroupByStaticCall( const key = `object.groupBy:${typeKey(items.type)}:${typeKey(f.type)}`; let helper = lowerer.mapHofHelpers.get(key); if (!helper) { - helper = `%object.groupBy.${lowerer.mapHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%object.groupBy", key); lowerer.mapHofHelpers.set(key, helper); lowerer.liftedFns.push(buildGroupByFn(lowerer, helper, items.type, f.type, resultT, loc)); } diff --git a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts index 370974c85..d3169ec5c 100644 --- a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { nodeThrowExpr, boolLit, countedFor, numLit, strLit, varRef } from "../../../ir/build.js"; import { InternalCompilerError } from "../../../errors.js"; import * as ts from "../../ts7/adapter.js"; @@ -232,7 +233,7 @@ function lowerMapIterDrainCall( const key = `${method}:${typeKey(mapT.key)}:${typeKey(mapT.value)}`; let helper = lowerer.mapHofHelpers.get(key); if (!helper) { - helper = `%map.${method}.${lowerer.mapHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%map.${method}`, key); lowerer.mapHofHelpers.set(key, helper); lowerer.liftedFns.push(buildMapIterDrainFn(helper, mapT, method, elemT, tupleT, loc)); } @@ -348,7 +349,7 @@ export function mapEntriesArray(lowerer: Lowerer, receiver: IrExpr): IrExpr { const key = `entries:${typeKey(mapT.key)}:${typeKey(mapT.value)}`; let helper = lowerer.mapHofHelpers.get(key); if (!helper) { - helper = `%map.entries.${lowerer.mapHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%map.entries", key); lowerer.mapHofHelpers.set(key, helper); lowerer.liftedFns.push( buildMapIterDrainFn(helper, mapT, "entries", tupleT, tupleT, receiver.loc), @@ -519,7 +520,7 @@ export function lowerMapForEachCall( const key = `${typeKey(mapT.key)}:${typeKey(mapT.value)}:${arity}:${typeKey(fnRet)}`; let helper = lowerer.mapHofHelpers.get(key); if (!helper) { - helper = `%map.forEach.${lowerer.mapHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%map.forEach", key); lowerer.mapHofHelpers.set(key, helper); lowerer.liftedFns.push(buildCollectionForEachFn(helper, mapT, arity, fnRet, loc)); } @@ -693,7 +694,7 @@ export function lowerSetForEachCall( const key = `${typeKey(setT.elem)}:${arity}:${typeKey(fnRet)}`; let helper = lowerer.setHofHelpers.get(key); if (!helper) { - helper = `%set.forEach.${lowerer.setHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%set.forEach", key); lowerer.setHofHelpers.set(key, helper); lowerer.liftedFns.push(buildCollectionForEachFn(helper, setT, arity, fnRet, loc)); } @@ -745,7 +746,7 @@ function lowerSetCombineCall( const key = `${name}:${typeKey(setT.elem)}:${typeKey(otherT)}`; let helper = lowerer.setHofHelpers.get(key); if (!helper) { - helper = `%set.${name}.${lowerer.setHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%set.${name}`, key); lowerer.setHofHelpers.set(key, helper); lowerer.liftedFns.push(buildSetCombineFn(helper, name, setT, otherT, loc)); } @@ -1357,7 +1358,7 @@ export function mapFromSeedValue( const key = `checked-seed:${typeKey(mapT)}`; let name = lowerer.mapHofHelpers.get(key); if (!name) { - name = `%map.checkedSeed.${lowerer.mapHofHelpers.size}`; + name = keyedHelperName(lowerer, "%map.checkedSeed", key); lowerer.mapHofHelpers.set(key, name); const source = varRef("source.0", DYN, loc); const entries = varRef("entries.0", DYN, loc); @@ -1457,7 +1458,7 @@ export function mapFromSeedValue( const key = `seed:${typeKey(mapT.key)}:${typeKey(mapT.value)}:${elem.shapeId}`; let helper = lowerer.mapHofHelpers.get(key); if (!helper) { - helper = `%map.seed.${lowerer.mapHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%map.seed", key); lowerer.mapHofHelpers.set(key, helper); lowerer.liftedFns.push(buildMapSeedFn(mapT, helper, seed.type, elem, kT, vT, loc)); } diff --git a/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts b/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts index efc173ffc..b3faf649e 100644 --- a/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts +++ b/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { nullProtoRecordShape, recordNullProtoTest } from "../null-prototype.js"; import { countedFor, numLit, varRef } from "../../../ir/build.js"; import { objectEnumerationReceiver } from "../object-enumeration-receiver.js"; @@ -184,7 +185,7 @@ export function objectIterOverIndexShape( const key = `obj.${member}:ovf:${argIr.shapeId}:${typeKey(resultT)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%obj.${member}.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%obj.${member}`, key); const outRef = varRef("out.0", resultT, loc); const rRef = varRef("r.0", argIr, loc); @@ -608,7 +609,7 @@ function lowerTypedObjectFromEntriesCall( const key = `obj.fromEntries:${argIr.elem.shapeId}:${resultT.shapeId}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%obj.fromEntries.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%obj.fromEntries", key); const tupleT = argIr.elem as IrType & { kind: "record" }; const tRef = varRef("t.0", tupleT, loc); @@ -712,7 +713,7 @@ function lowerFromEntriesStringRows( const key = `obj.fromEntries:strrows:${resultT.shapeId}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%obj.fromEntries.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%obj.fromEntries", key); const tRef = varRef("t.0", rowT, loc); const rowLen: IrExpr = { @@ -931,7 +932,7 @@ export function lowerRecordOvfCaptureHelper( const key = `ovf:${fromId}:${toId}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%rec.capture.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%rec.capture", key); lowerer.valueHelpers.set(key, name); const fromT: IrType = { kind: "record", shapeId: fromId }; const toT: IrType = { kind: "record", shapeId: toId }; @@ -1204,7 +1205,7 @@ export function lowerObjectAssignIndexShape( const key = `assign:${targetIr.shapeId}:${plan.fromId}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%obj.assign.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%obj.assign", key); lowerer.valueHelpers.set(key, name); const toT: IrType = { kind: "record", shapeId: targetIr.shapeId }; const fromT: IrType = { kind: "record", shapeId: plan.fromId }; @@ -1474,7 +1475,7 @@ export function lowerIndexMergeHelper( .join(","); const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%rec.merge.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%rec.merge", key); lowerer.valueHelpers.set(key, name); const toT: IrType = { kind: "record", shapeId: toId }; const ksT = arrayOf(STRING); @@ -1805,7 +1806,7 @@ export function lowerEnvToPairsHelper( const key = `env.pairs:${shapeId}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%env.pairs.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%env.pairs", key); lowerer.valueHelpers.set(key, name); const recT: IrType = { kind: "record", shapeId }; const arrT = arrayOf(STRING); diff --git a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts index c23e6931c..48b4caabd 100644 --- a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts +++ b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { boolLit, numLit, varRef } from "../../../ir/build.js"; import { InternalCompilerError } from "../../../errors.js"; import * as ts from "../../ts7/adapter.js"; @@ -966,7 +967,7 @@ function lowerFusedChain( const key = `iter:${srcKey}:${stageKeys.join(",")}:${terminal}:${termArgs.map((a) => typeKey(a.type)).join(",")}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%iter.${terminal}.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%iter.${terminal}`, key); lowerer.arrHofHelpers.set(key, helper); lowerer.liftedFns.push( buildIterChainFn( @@ -1038,7 +1039,7 @@ function lowerHelperObject( const key = `iterHelper:${stage.kind}:${srcKey}:${typeKey(srcElem)}:${typeKey(arg.type)}:${typeKey(outT)}`; let name = lowerer.arrHofHelpers.get(key); if (!name) { - name = `%iter.${stage.kind}.${lowerer.arrHofHelpers.size}`; + name = keyedHelperName(lowerer, `%iter.${stage.kind}`, key); lowerer.arrHofHelpers.set(key, name); lowerer.liftedFns.push( buildHelperGenFn(lowerer, name, source, srcElem, stage, arg.type, outT, resultType, loc), @@ -1058,7 +1059,7 @@ function internBudgetChecker(lowerer: Lowerer, loc: SrcLoc, srcT: GenType | null const key = srcT === null ? "iter:budget" : `iter:budget:close:${typeKey(srcT)}`; let name = lowerer.arrHofHelpers.get(key); if (name !== undefined) return name; - name = srcT === null ? "%iter.budget" : `%iter.budget.close.${lowerer.arrHofHelpers.size}`; + name = srcT === null ? "%iter.budget" : keyedHelperName(lowerer, "%iter.budget.close", key); lowerer.arrHofHelpers.set(key, name); const v = (): IrExpr => ({ kind: "varRef", localId: "v.0", type: F64, loc }); diff --git a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts index 9567ee0f6..fe4ddbc6c 100644 --- a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts +++ b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { matchAllRowsReadByElements } from "./match-row-uses.js"; import { boolLit, @@ -159,7 +160,7 @@ export function lowerStringSplitCall( const key = `str.split:${receiver.type.kind === "union" ? typeKey(receiver.type) : receiver.type.kind}:${typeKey(separator.type)}:${typeKey(limit.type)}:${absent}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%str.split.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%str.split", key); const values = [receiver, separator, limit]; const params = values.map((value, index) => ({ localId: `arg.${index}`, @@ -395,7 +396,7 @@ function stringIndexHelperCall( const key = `str.index:${method}:${typeKey(receiver.type)}:${typeKey(index.type)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%str.index.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%str.index", key); lowerer.valueHelpers.set(key, helper); const rawReceiver = varRef("arg.0", receiver.type, loc); const rawIndex = varRef("arg.1", index.type, loc); @@ -673,7 +674,7 @@ export function lowerStringPaddingCall( const key = `str.pad:${method}:${values.map((value) => typeKey(value.type)).join(":")}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%str.pad.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%str.pad", key); const params = values.map((value, index) => ({ localId: `arg.${index}`, name: `arg${index}`, @@ -1309,7 +1310,7 @@ export function lowerStringMethodCall( const key = `str.positions:${entry.method}:${typeKey(needle.type)}:${typeKey(position.type)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%str.positions.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%str.positions", key); const params = [receiver, needle, position].map((arg, index) => ({ localId: `arg.${index}`, name: `arg${index}`, @@ -1411,7 +1412,7 @@ export function lowerStringMethodCall( const key = `str.positions:${entry.method}:${args.map((arg) => typeKey(arg.type)).join(":")}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%str.positions.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%str.positions", key); const params = [receiver, ...args].map((arg, index) => ({ localId: `arg.${index}`, name: `arg${index}`, diff --git a/packages/compiler/src/frontend/lowering/containers/string-replacement.ts b/packages/compiler/src/frontend/lowering/containers/string-replacement.ts index 6856bc50b..780c77d0f 100644 --- a/packages/compiler/src/frontend/lowering/containers/string-replacement.ts +++ b/packages/compiler/src/frontend/lowering/containers/string-replacement.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import { BOOL, @@ -162,7 +163,7 @@ function lowerStringReplacementValues( const key = `str.${method}:${typeKey(replacement.type)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%str.${method}.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, `%str.${method}`, key); lowerer.valueHelpers.set(key, helper); const subject = varRef("s.0", STRING, loc); const needle = varRef("search.0", STRING, loc); diff --git a/packages/compiler/src/frontend/lowering/expressions/binary.ts b/packages/compiler/src/frontend/lowering/expressions/binary.ts index fafbbf695..7e449f12b 100644 --- a/packages/compiler/src/frontend/lowering/expressions/binary.ts +++ b/packages/compiler/src/frontend/lowering/expressions/binary.ts @@ -1,4 +1,5 @@ /* Binary operators: arithmetic, comparisons and equality, logical and nullish pairs, and string conversion for concatenation. */ +import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import { cleanFuncAdaptable } from "../coercions/function-adapters.js"; import { type Lowerer, jsFuncNameOf } from "../lowerer.js"; @@ -801,7 +802,7 @@ function nullProtoToString(lowerer: Lowerer, e: IrExpr, t: IrType & { kind: "rec const key = `nullProtoToString:${t.shapeId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%rec.tostr.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%rec.tostr", key); lowerer.valueHelpers.set(key, helper); const r = varRef("r.0", t, loc); lowerer.liftedFns.push({ diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index 565db0cd8..93bf43e95 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { dateJsonType, isDateJsonSource } from "../builtins/dates.js"; import { dynUndefinedExpr, numLit, strLit, varRef } from "../../../ir/build.js"; import { pathModuleValue } from "../lower-builtin-values.js"; @@ -406,7 +407,7 @@ export function lowerDynObjectLiteral( if (!(err instanceof PoisonError) || !pureMember) throw err; const fence = lowerer.deferToRuntimeFence(propDiagsBefore, prop, { kind: "closure", - name: () => `%fn${lowerer.lambdaCounter++}_dynfence`, + name: () => lowerer.closureName(loc, "_dynfence"), returnType: VOID, type: { kind: "func", params: [], ret: VOID }, }); @@ -461,7 +462,7 @@ export function lowerDynObjectLiteral( if (!(err instanceof PoisonError)) throw err; const fence = lowerer.deferToRuntimeFence(convDiagsBefore, prop, { kind: "closure", - name: () => `%fn${lowerer.lambdaCounter++}_dynfence`, + name: () => lowerer.closureName(loc, "_dynfence"), returnType: VOID, type: { kind: "func", params: [], ret: VOID }, }); @@ -2956,7 +2957,7 @@ function lowerDeclaredSpreadMerge( const key = `declmerge-source:${type.shapeId}:${source.shapeId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%rec.declmerge.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%rec.declmerge", key); const sourceType = source.value.type; const destination = varRef("out.0", type, loc); const receiver = varRef("source.0", sourceType, loc); diff --git a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts index fbcecdc32..4b8c5692a 100644 --- a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts +++ b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts @@ -240,7 +240,7 @@ export function lowerOptionalChain( const receiver = lowerer.runtimeOptionalSourceValue(recvNode, loweredReceiver) ?? loweredReceiver; if (receiver.type.kind === "dyn") { if (dotNode === expr && ts.isCallExpression(expr)) { - const id = `chain.${lowerer.chainCounter++}`; + const id = lowerer.closureName(loc, "", "chain"); const callee: IrExpr = { kind: "chainRecv", id, type: DYN, loc }; const stmts: IrStmt[] = []; let guarded = receiver; @@ -298,7 +298,7 @@ export function lowerOptionalChain( ); } if (dotNode === expr && ts.isElementAccessExpression(expr)) { - const id = `chain.${lowerer.chainCounter++}`; + const id = lowerer.closureName(loc, "", "chain"); const reference: IrExpr = { kind: "chainRecv", id, type: DYN, loc }; lowerer.chainRecvByNode.set(recvNode, reference); lowerer.chainHandled.add(dotNode); @@ -331,7 +331,7 @@ export function lowerOptionalChain( dotNode === expr.expression ) { const m = dotNode.name.text; - const id = `chain.${lowerer.chainCounter++}`; + const id = lowerer.closureName(loc, "", "chain"); const recvRef: IrExpr = { kind: "chainRecv", id, type: DYN, loc: locOf(recvNode) }; lowerer.chainRecvByNode.set(recvNode, recvRef); lowerer.chainHandled.add(dotNode); @@ -358,7 +358,7 @@ export function lowerOptionalChain( } return { kind: "optChain", id, receiver, body, type: DYN, loc }; } - const id = `chain.${lowerer.chainCounter++}`; + const id = lowerer.closureName(loc, "", "chain"); const reference: IrExpr = { kind: "chainRecv", id, type: DYN, loc }; lowerer.chainRecvByNode.set(recvNode, reference); lowerer.chainHandled.add(dotNode); @@ -420,7 +420,7 @@ export function lowerOptionalChain( rest.length === 1 ? rest[0]! : { kind: "union" as const, unionId: lowerer.unions.transform(def, rest) }; - const id = `chain.${lowerer.chainCounter++}`; + const id = lowerer.closureName(loc, "", "chain"); // A single present arm binds its payload. Multiple present arms bind // the original tagged value, then retag inside the guarded body. The // nullish arms never enter that body, and each record keeps its identity. diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index f0b980da0..6c8dfa3bf 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -1,4 +1,5 @@ /* Property reads (`a.b`): lowerExprInner's property-access branch, intrinsic and union properties, field reads and targets, and match-group reads. */ +import { keyedHelperName } from "../stable-names.js"; import { staticallySelected } from "../static-typeof.js"; import * as ts from "../../ts7/adapter.js"; import { lowerHistogramProperty } from "../builtins/performance.js"; @@ -2783,7 +2784,7 @@ export function lowerGroupsProjection( const key = `regexgroups:${shapeId}:${groups.map((g) => `${g.name}=${g.index}`).join(",")}:${recvKey}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%regex.groups.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%regex.groups", key); lowerer.valueHelpers.set(key, helper); const body: IrStmt[] = []; const locals: IrLocal[] = [{ id: "m.0", name: "m", type: recv.type, mutable: true }]; diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts index 088b45cff..836391835 100644 --- a/packages/compiler/src/frontend/lowering/expressions/relational.ts +++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts @@ -1,4 +1,5 @@ /* instanceof and in, including private-name and runtime-key in. */ +import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import { builtinClassInstanceOfAny, builtinClassKind } from "../builtins/builtin-class-dyn.js"; import { @@ -647,7 +648,7 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc const key = `instanceof.union:${unionId}:${target.def.name}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%instanceof.union.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%instanceof.union", key); lowerer.valueHelpers.set(key, helper); const value: IrExpr = { kind: "varRef", localId: "value.0", type: left.type, loc }; const body: IrStmt[] = []; @@ -1847,7 +1848,7 @@ function lowerRuntimeKeyIn( const hkey = `haskey:${recvT.shapeId}`; let helper = lowerer.valueHelpers.get(hkey); if (!helper) { - helper = `%rec.haskey.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%rec.haskey", hkey); lowerer.valueHelpers.set(hkey, helper); const recT: IrType = { kind: "record", shapeId: recvT.shapeId }; diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index abcd5a584..daf082db7 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -1,4 +1,6 @@ import { staticNameBindings } from "./static-function-names.js"; +import { positionTag } from "./structural-positions.js"; +import { keyedHelperName } from "./stable-names.js"; import * as ts from "../ts7/adapter.js"; import { bodyReadsArguments } from "../arguments-usage.js"; import type { Lowerer } from "./lowerer.js"; @@ -229,7 +231,10 @@ export function collectGenericSignature(lowerer: Lowerer, decl: ts.FunctionDecla lowerer.genericFnsBySymbol.set(symbol, { decl, baseName: nameText, - qualifiedName: lowerer.qualify(ts.getSourceFileOfNode(decl), nsPathPrefix(decl) + nameText), + qualifiedName: lowerer.qualify( + ts.getSourceFileOfNode(decl), + nsPathPrefix(lowerer, decl) + nameText, + ), typeParams, instances: new Map(), }); @@ -651,7 +656,7 @@ function internGenericInstance( const typeArgsText = `<${rendered.length > 80 ? rendered.slice(0, 77) + "..." : rendered}>`; inst = { path, - name: `${info.qualifiedName}%${info.instances.size}`, + name: keyedHelperName(lowerer, `${info.qualifiedName}%`, key, ""), ordinal: info.instances.size, params, returnType, @@ -2129,7 +2134,7 @@ function internImplicitInstance( const declared = dynamicBroadPromise ? null : implicitDeclaredReturn(lowerer, info); inst = { path, - name: `${info.qualifiedName}%${info.instances.size}`, + name: keyedHelperName(lowerer, `${info.qualifiedName}%`, key, ""), ordinal: info.instances.size, params: shapes, // The promise callers rely on before the body settles it: the @@ -2317,7 +2322,7 @@ export function implicitLocalFnInfoOf( baseName: name, qualifiedName: lowerer.qualify( ts.getSourceFileOfNode(decl), - nsPathPrefix(stmt ?? decl, decl) + `${name}%l${ts.tokenStartOfNode(decl)}`, + nsPathPrefix(lowerer, stmt ?? decl, decl) + `${name}%l${positionTag(lowerer, decl)}`, ), typeParams: [], instances: new Map(), @@ -2413,7 +2418,7 @@ export function objLitGenericFnInfoOf( baseName: name, qualifiedName: lowerer.qualify( ts.getSourceFileOfNode(fnNode), - `%ol${ts.tokenStartOfNode(fnNode)}.${name}`, + `%ol${positionTag(lowerer, fnNode)}.${name}`, ), typeParams, instances: new Map(), @@ -2559,7 +2564,7 @@ export function bindingGenericFnInfoOf( baseName: name, qualifiedName: lowerer.qualify( ts.getSourceFileOfNode(decl), - nsPathPrefix(stmt ?? decl, decl) + + nsPathPrefix(lowerer, stmt ?? decl, decl) + (topLevel ? name : `${name}%l${ts.tokenStartOfNode(decl)}`), ), typeParams, diff --git a/packages/compiler/src/frontend/lowering/intern-tables.ts b/packages/compiler/src/frontend/lowering/intern-tables.ts index fc69237db..e3f2b0889 100644 --- a/packages/compiler/src/frontend/lowering/intern-tables.ts +++ b/packages/compiler/src/frontend/lowering/intern-tables.ts @@ -23,6 +23,7 @@ * The entry handles that itable.find and itable.insert answer stay inside * these lowerings: f64 locals that only reach toBool and itable.value. * Any other use of the class or its members as values is refused. */ +import { keyedHelperName } from "./stable-names.js"; import { dynUndefinedExpr } from "../../ir/build.js"; import { BOOL, F64, STRING, funcOf, typeKey } from "../../ir/ir.js"; import type { IrExpr, IrFunction, IrStmt, IrType, SrcLoc } from "../../ir/ir.js"; @@ -447,7 +448,11 @@ function helperName(lowerer: Lowerer, member: InternTableMember, signature: stri } function registerHelper(lowerer: Lowerer, member: InternTableMember, signature: string): string { - const name = `%${INTERN_TABLE_CLASS}.${member}.${lowerer.internTableHelpers.size}`; + const name = keyedHelperName( + lowerer, + `%${INTERN_TABLE_CLASS}.${member}`, + `${member}|${signature}`, + ); lowerer.internTableHelpers.set(`${member}|${signature}`, name); return name; } diff --git a/packages/compiler/src/frontend/lowering/iterator-adapters.ts b/packages/compiler/src/frontend/lowering/iterator-adapters.ts index 898c46f20..e50652182 100644 --- a/packages/compiler/src/frontend/lowering/iterator-adapters.ts +++ b/packages/compiler/src/frontend/lowering/iterator-adapters.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { dynUndefinedExpr, varRef } from "../../ir/build.js"; import { BOOL, @@ -86,7 +87,7 @@ function dynIteratorAdapter( const key = `iterAdapt:${typeKey(expected)}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = `%iter.adapt.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%iter.adapt", key); lowerer.valueHelpers.set(key, name); const locals: IrLocal[] = []; const local = (id: string, type: IrType, mutable = false): IrExpr => { @@ -313,7 +314,7 @@ export function lowerCollectionIteratorValue( loc, }; if (!typeEquals(yielded.type, type.yieldT)) return null; - name = `%coll.iter.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%coll.iter", key); lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, @@ -411,7 +412,7 @@ export function lowerBytesIteratorValue( const key = `bytesIter:${method}:${typeKey(bytesT)}:${typeKey(type)}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = `%bytes.iter.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%bytes.iter", key); lowerer.valueHelpers.set(key, name); const list = varRef("list.0", bytesT, loc); const index = varRef("index.0", F64, loc); @@ -517,7 +518,7 @@ export function lowerStringIteratorValue( const key = `strIter:${typeKey(type)}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = `%str.iter.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%str.iter", key); lowerer.valueHelpers.set(key, name); const text = varRef("text.0", STRING, loc); const index = varRef("index.0", F64, loc); @@ -677,7 +678,7 @@ function generatorRetypeAdapter( type: resumeType(expected.nextT), loc, }; - name = `%gen.retype.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%gen.retype", key); lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, @@ -794,7 +795,7 @@ export function generatorDrain( const outRef = varRef("out.0", arrT, loc); const value = lowerer.coerceToExpected(genValue(genRef, loc), elem); if (!typeEquals(value.type, elem)) return null; - name = `%gen.drain.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%gen.drain", key); lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, @@ -856,7 +857,7 @@ export function generatorDrain( if (!raw) return null; const value = lowerer.coerceToExpected(raw, elem); if (!typeEquals(value.type, elem)) return null; - name = `%gen.drain.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%gen.drain", key); lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, @@ -978,7 +979,7 @@ export function lowerArrayIteratorValue( }; } if (!yielded || !typeEquals(yielded.type, type.yieldT)) return null; - name = `%arr.iter.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%arr.iter", key); lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, diff --git a/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts b/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts index b9bbf6216..e27dd4183 100644 --- a/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts +++ b/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts @@ -21,6 +21,7 @@ * files only, and an empty answer leaves every mapping unchanged, so * TypeScript programs and JavaScript that never uses these patterns * compile exactly as before. */ +import { keyedHelperName } from "./stable-names.js"; import * as ts from "../ts7/adapter.js"; import { isJsSourceFile, locOf } from "../program.js"; import { BOOL, DYN, type IrExpr, type IrStmt } from "../../ir/ir.js"; @@ -215,10 +216,10 @@ export function lowerValueReturningConstructorNew( "spread arguments when constructing a class whose constructor returns a value", ); } - const key = `ctorValue:${ts.getSourceFileOfNode(decl).fileName}:${ts.tokenStartOfNode(decl)}`; + const key = `ctorValue:${lowerer.positions.of(decl)}`; let name = lowerer.valueHelpers.get(key); if (name === undefined) { - name = `%ctorValue.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%ctorValue", key); lowerer.valueHelpers.set(key, name); const savedClass = lowerer.currentClass; lowerer.currentClass = null; diff --git a/packages/compiler/src/frontend/lowering/json-require.ts b/packages/compiler/src/frontend/lowering/json-require.ts index 927745253..d6c2da571 100644 --- a/packages/compiler/src/frontend/lowering/json-require.ts +++ b/packages/compiler/src/frontend/lowering/json-require.ts @@ -10,6 +10,7 @@ * JSON module is ordinary storage holding that object — destructuring * copies its properties at the require, exactly like Node. ES-module JSON * imports keep their own record (Node's ESM loader caches separately). */ +import { keyedHelperName } from "./stable-names.js"; import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; import { PoisonError } from "./lowerer.js"; @@ -84,7 +85,7 @@ export function collectJsonRequires(lowerer: Lowerer, parts: FileParts[]): void ); throw new PoisonError(); } - const id = `%g.jsonRequire.${lowerer.jsonRequireModules.size}`; + const id = keyedHelperName(lowerer, "%g.jsonRequire", jsonSf.fileName); const global: IrGlobal = { id, name: "%json", type: mapped, mutable: true }; const loadedId = `${id}%loaded`; lowerer.globalsList.push(global, { diff --git a/packages/compiler/src/frontend/lowering/lifetime-regions.ts b/packages/compiler/src/frontend/lowering/lifetime-regions.ts index b212ae473..b684c0395 100644 --- a/packages/compiler/src/frontend/lowering/lifetime-regions.ts +++ b/packages/compiler/src/frontend/lowering/lifetime-regions.ts @@ -14,6 +14,7 @@ * Any other use of the class or its methods as values is refused. The * module is marked lifetimeRegions, which moves every ProgramLifetime * class into counted regions. */ +import { keyedHelperName } from "./stable-names.js"; import { F64, VOID, funcOf, typeKey } from "../../ir/ir.js"; import type { IrExpr, IrFunction, IrStmt, IrType, SrcLoc } from "../../ir/ir.js"; import { experimentalImportOf, lifetimeRegionMemberOf } from "../experimental-import.js"; @@ -152,7 +153,7 @@ function runHelper(lowerer: Lowerer, fnType: IrType & { kind: "func" }, loc: Src const key = typeKey(fnType); const known = lowerer.regionRunHelpers.get(key); if (known !== undefined) return known; - const name = `%${LIFETIME_REGION_CLASS}.run.${lowerer.regionRunHelpers.size}`; + const name = keyedHelperName(lowerer, `%${LIFETIME_REGION_CLASS}.run`, key); lowerer.regionRunHelpers.set(key, name); const regionT = lifetimeRegionType(lowerer); const ret = fnType.ret; diff --git a/packages/compiler/src/frontend/lowering/lower-assert.ts b/packages/compiler/src/frontend/lowering/lower-assert.ts index 460f4d53a..bc51a9382 100644 --- a/packages/compiler/src/frontend/lowering/lower-assert.ts +++ b/packages/compiler/src/frontend/lowering/lower-assert.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { InternalCompilerError } from "../../errors.js"; /* The node:assert lowering (a spoke module like lower-dgram.ts): the * callable module form (`assert(x)` through a default import or a CJS @@ -667,7 +668,7 @@ function partialEqHelper(lowerer: Lowerer, ta: IrType, tb: IrType, loc: SrcLoc): const key = `assert.pdeq:${typeKey(ta)}|${typeKey(tb)}`; const existing = lowerer.assertHelpers.get(key); if (existing) return existing; - const name = `%assert.pdeq.${lowerer.assertHelpers.size}`; + const name = keyedHelperName(lowerer, "%assert.pdeq", key); lowerer.assertHelpers.set(key, name); const a = (): IrExpr => varRef("a.0", ta, loc); const b = (): IrExpr => varRef("b.0", tb, loc); @@ -1280,7 +1281,7 @@ function deepCheckCall( const key = `assert.dcheck:${variant}:${negated}:${typeKey(a.type)}:${typeKey(b.type)}`; let name = lowerer.assertHelpers.get(key); if (!name) { - name = `%assert.dcheck.${lowerer.assertHelpers.size}`; + name = keyedHelperName(lowerer, "%assert.dcheck", key); lowerer.assertHelpers.set(key, name); const pa = varRef("a.0", a.type, loc); const pb = varRef("b.0", b.type, loc); @@ -1844,7 +1845,7 @@ function assertThrowsHelper( const key = `assert.${mode}:${recvIsPromise ? "p" : "f"}:${typeKey(recvType)}:${sig}`; const existing = lowerer.assertHelpers.get(key); if (existing) return existing; - const name = `%assert.${mode}.${lowerer.assertHelpers.size}`; + const name = keyedHelperName(lowerer, `%assert.${mode}`, key); lowerer.assertHelpers.set(key, name); const caughtRef = (): IrExpr => varRef("e.0", CAUGHT, loc); @@ -2293,7 +2294,7 @@ function ifErrorHelper(lowerer: Lowerer, t: IrType & { kind: "union" }, loc: Src const key = `assert.iferr:${typeKey(t)}`; const existing = lowerer.assertHelpers.get(key); if (existing) return existing; - const name = `%assert.iferr.${lowerer.assertHelpers.size}`; + const name = keyedHelperName(lowerer, "%assert.iferr", key); lowerer.assertHelpers.set(key, name); const def = lowerer.unions.get(t.unionId); if (!def) throw new InternalCompilerError(`assert.ifError over unknown union ${t.unionId}`); @@ -2464,7 +2465,7 @@ function deepEqHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc, loose = false): const key = `assert.${prefix}:${typeKey(t)}`; const existing = lowerer.assertHelpers.get(key); if (existing) return existing; - const name = `%assert.${prefix}.${lowerer.assertHelpers.size}`; + const name = keyedHelperName(lowerer, `%assert.${prefix}`, key); lowerer.assertHelpers.set(key, name); const a = (): IrExpr => varRef("a.0", t, loc); diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 7102f37ab..63b8781f2 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { isForwardedTupleSpread, lowerForwardingApply } from "./typed-arguments.js"; import { guardNullProto, nullProtoRecordShape } from "./null-prototype.js"; import { lowerDynStreamMemberCall } from "./dyn-streams.js"; @@ -2094,7 +2095,7 @@ function lowerOptionalNumberDefault( const key = `number.optionalDefault:${widened.unionId}:${dflt}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%number.optionalDefault.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%number.optionalDefault", key); lowerer.valueHelpers.set(key, helper); const input = varRef("value.0", widened, loc); lowerer.liftedFns.push({ @@ -2203,7 +2204,7 @@ function primitiveConversionHandler( const key = `then.${name}:${typeKey(inner)}`; let fnName = lowerer.arrHofHelpers.get(key); if (fnName === undefined) { - fnName = `%then.${name}.${lowerer.arrHofHelpers.size}`; + fnName = keyedHelperName(lowerer, `%then.${name}`, key); lowerer.arrHofHelpers.set(key, fnName); const value = varRef("v.0", inner, loc); let body: IrExpr; @@ -2372,7 +2373,7 @@ function lowerBigIntConstructorValue(lowerer: Lowerer, arg: IrExpr, loc: SrcLoc) const key = `bigint.scalar:${unionId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%bigint.scalar.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%bigint.scalar", key); lowerer.valueHelpers.set(key, helper); const value = varRef("value.0", arg.type, loc); const body: IrStmt[] = []; @@ -3234,7 +3235,7 @@ function lowerScalarUnionNumber( const key = `number.scalar:${arg.type.unionId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%number.scalar.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%number.scalar", key); lowerer.valueHelpers.set(key, helper); const value = varRef("value.0", arg.type, loc); const nan: IrExpr = { @@ -3279,7 +3280,7 @@ function lowerOptionalStringNumber( const key = `${label}.optionalString:${arg.type.unionId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%${label}.optionalString.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, `%${label}.optionalString`, key); lowerer.valueHelpers.set(key, helper); const value = varRef("value.0", arg.type, loc); lowerer.liftedFns.push({ @@ -4706,7 +4707,7 @@ function lowerDefaultToStringCall( const key = `objToStr:${typeKey(recv.type)}${missing ? `:${missing}` : ""}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%obj.tostr.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%obj.tostr", key); lowerer.valueHelpers.set(key, helper); const o = varRef("o.0", recv.type, loc); lowerer.liftedFns.push({ @@ -5393,7 +5394,7 @@ function loweredTemplateStrings( const site = lowerer.globalsList.length; const globalId = `%g.templateStrings.${site}`; const readyId = `%g.templateStringsReady.${site}`; - getter = `%templateStrings.get.${lowerer.valueHelpers.size}`; + getter = keyedHelperName(lowerer, "%templateStrings.get", key); lowerer.valueHelpers.set(key, getter); lowerer.globalsList.push( { id: globalId, name: "%templateStrings", type, mutable: true }, @@ -5574,7 +5575,7 @@ export function lowerPromiseMethodCall( // IS the spec's identity/thrower/no-op substitution for all three // members. An argument expression, when present, is undefined/null // by construction — nothing to evaluate. - const fnName = `%fn${lowerer.lambdaCounter++}_${member}pass`; + const fnName = lowerer.closureName(loc, `_${member}pass`); const funcType: IrType & { kind: "func" } = { kind: "func", params: [promT], ret: promT }; const fnCtx = newFnCtx(true, null, funcType, inner); fnCtx.isAsync = true; @@ -5671,7 +5672,7 @@ export function lowerPromiseMethodCall( // Evaluate both callback expressions before the wrapper runs, in source order. const fulfilled = fulfilledAbsent ? null : boxHandler(fulfilledNode, true); const rejected = rejectedAbsent ? null : boxHandler(rejectedNode, false); - const fnName = `%fn${lowerer.lambdaCounter++}_then2`; + const fnName = lowerer.closureName(loc, "_then2"); const paramTypes: IrType[] = [ promT, ...(fulfilled ? [fulfilled.value.type] : []), @@ -5920,7 +5921,7 @@ export function lowerPromiseMethodCall( ); }; const resultT: IrType & { kind: "promise" } = { kind: "promise", inner: DYN }; - const fnName = `%fn${lowerer.lambdaCounter++}_then`; + const fnName = lowerer.closureName(loc, "_then"); const funcType: IrType & { kind: "func" } = { kind: "func", params: [promT, DYN], @@ -6015,7 +6016,7 @@ export function lowerPromiseMethodCall( ); } const R = resultT.inner; - const fnName = `%fn${lowerer.lambdaCounter++}_then`; + const fnName = lowerer.closureName(loc, "_then"); const funcType: IrType & { kind: "func" } = { kind: "func", params: [promT, cb.type], @@ -6118,7 +6119,7 @@ export function lowerPromiseMethodCall( "finally callbacks with parameters or a return value (use () => { ... })", ); } - const fnName = `%fn${lowerer.lambdaCounter++}_finally`; + const fnName = lowerer.closureName(loc, "_finally"); const funcType: IrType & { kind: "func" } = { kind: "func", params: [promT, cb.type], @@ -6221,7 +6222,7 @@ export function lowerPromiseMethodCall( } if (cb.type.kind === "dyn") { const resultT: IrType & { kind: "promise" } = { kind: "promise", inner: DYN }; - const fnName = `%fn${lowerer.lambdaCounter++}_catchdyn`; + const fnName = lowerer.closureName(loc, "_catchdyn"); const funcType: IrType & { kind: "func" } = { kind: "func", params: [promT, DYN], @@ -6351,7 +6352,7 @@ export function lowerPromiseMethodCall( ); } const R = resultT.inner; - const fnName = `%fn${lowerer.lambdaCounter++}_catch`; + const fnName = lowerer.closureName(loc, "_catch"); const funcType: IrType & { kind: "func" } = { kind: "func", params: [promT], ret: resultT }; const fnCtx = newFnCtx(true, null, funcType, R); fnCtx.isAsync = true; @@ -6588,7 +6589,7 @@ function filterNarrowHelper( const key = `filterNarrow:${test}:${typeKey(elem)}:${typeKey(outElem)}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%arr.filterNarrow.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, "%arr.filterNarrow", key); lowerer.arrHofHelpers.set(key, name); const arrT = arrayOf(elem); @@ -6834,7 +6835,7 @@ export function recordKeysArrayCall( const key = `obj.keys:${argIr.shapeId}:${typeKey(resultT)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%obj.keys.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%obj.keys", key); const ref: IrExpr = { kind: "varRef", localId: "r.0", type: argIr, loc }; const outRef: IrExpr = { kind: "varRef", localId: "out.0", type: resultT, loc }; lowerer.arrHofHelpers.set(key, helper); @@ -6904,7 +6905,7 @@ function recordHasOwnHelper(lowerer: Lowerer, shapeId: string, loc: SrcLoc): str const key = `obj.hasOwn:${shapeId}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const helper = `%obj.hasOwn.${lowerer.arrHofHelpers.size}`; + const helper = keyedHelperName(lowerer, "%obj.hasOwn", key); lowerer.arrHofHelpers.set(key, helper); const shape = lowerer.shapes.get(shapeId)!; const recT: IrType = { kind: "record", shapeId }; @@ -6989,7 +6990,7 @@ function classAssignHelper( const key = `obj.assignClass:${className}:${srcShapeId}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const helper = `%obj.assignClass.${lowerer.arrHofHelpers.size}`; + const helper = keyedHelperName(lowerer, "%obj.assignClass", key); lowerer.arrHofHelpers.set(key, helper); const sShape = lowerer.shapes.get(srcShapeId)!; const oT: IrType = { kind: "object", className }; @@ -7057,7 +7058,7 @@ function recordAssignHelper( const key = `obj.assign:${targetShapeId}:${srcShapeId}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const helper = `%obj.assign.${lowerer.arrHofHelpers.size}`; + const helper = keyedHelperName(lowerer, "%obj.assign", key); lowerer.arrHofHelpers.set(key, helper); const sShape = lowerer.shapes.get(srcShapeId)!; const tShape = lowerer.shapes.get(targetShapeId)!; @@ -8514,7 +8515,7 @@ function lowerObjectStaticCall( const key = `obj.${member}:${argIr.shapeId}:${typeKey(resultT)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%obj.${member}.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%obj.${member}`, key); const recT = argIr; const ref: IrExpr = { kind: "varRef", localId: "r.0", type: recT, loc }; const outRef: IrExpr = { kind: "varRef", localId: "out.0", type: resultT, loc }; @@ -9150,7 +9151,7 @@ function lowerInlineArrowBind( const length = firstDefault < 0 ? arrow.parameters.nodes.length : firstDefault; const key = `arrow.bind:${typeKey(type)}:${length}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? `%arrow.bind.${lowerer.arrHofHelpers.size}`; + const name = existing ?? keyedHelperName(lowerer, "%arrow.bind", key); if (!existing) { lowerer.arrHofHelpers.set(key, name); lowerer.freshClosureAdapters.add(name); @@ -10079,7 +10080,7 @@ function lowerUnionObjectDynFieldCall( const receiverRef = varRef(receiverLocal.id, receiverT, loc); let helper = lowerer.unionCallHelpers.get(key); if (!helper) { - helper = `%union.call.${lowerer.unionCallHelpers.size}`; + helper = keyedHelperName(lowerer, "%union.call", key); lowerer.unionCallHelpers.set(key, helper); const receiverRef = varRef("this.0", receiverT, loc); const branch = (plan: (typeof plans)[number]): IrStmt[] => { @@ -10172,7 +10173,7 @@ function unionObjectMethodHelper( const key = `${receiverT.unionId}:${method}:${shapes.map((shape) => `${shape.mode}:${typeKey(shape.type)}`).join(",")}:${typeKey(resultT)}`; const existing = lowerer.unionCallHelpers.get(key); if (existing) return existing; - const name = `%union.call.${lowerer.unionCallHelpers.size}`; + const name = keyedHelperName(lowerer, "%union.call", key); lowerer.unionCallHelpers.set(key, name); // A probe that catches a refusal while the body lowers must not leave // the name registered for a helper that was never emitted. @@ -10366,7 +10367,7 @@ export function lowerStaticNumberGlobal( const key = `parseInt.optional:${unionT.unionId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%parseInt.optional.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%parseInt.optional", key); lowerer.valueHelpers.set(key, helper); const value: IrExpr = { kind: "varRef", localId: "value.0", type: unionT, loc }; const radixRef: IrExpr = { kind: "varRef", localId: "radix.0", type: F64, loc }; diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 790efe988..fcea03790 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { lowerValueReturningConstructorNew } from "./js-dynamic-objects.js"; import { adaptPromiseExecutor } from "./promise-executor-adapter.js"; import { dynUndefinedExpr, nodeThrowExpr, varRef } from "../../ir/build.js"; @@ -4663,7 +4664,7 @@ export function genericClassInstanceType( ); } const ordinal = gci.instances.size; - const name = `${gci.family.def.name}%${ordinal}`; + const name = keyedHelperName(lowerer, `${gci.family.def.name}%`, key, ""); const entry: { name: string; info: ClassInfo | null; poisoned?: boolean } = { name, info: null }; gci.instances.set(key, entry); const bindings = new Map(); @@ -7910,7 +7911,7 @@ export function classIteratorDrainCall( const key = `${cit.className}:${typeKey(outElem)}`; let name = lowerer.iterDrainHelpers.get(key); if (!name) { - name = `%iter.drain.${lowerer.iterDrainHelpers.size}`; + name = keyedHelperName(lowerer, "%iter.drain", key); lowerer.iterDrainHelpers.set(key, name); const recvT: IrType = { kind: "object", className: cit.className }; const recvRef: IrExpr = { kind: "varRef", localId: "r.0", type: recvT, loc }; @@ -8024,7 +8025,7 @@ export function classIteratorRestDrainCall( const key = `it:${cit.iterT.className}`; let name = lowerer.iterDrainHelpers.get(key); if (!name) { - name = `%iter.drain.${lowerer.iterDrainHelpers.size}`; + name = keyedHelperName(lowerer, "%iter.drain", key); lowerer.iterDrainHelpers.set(key, name); const itRef: IrExpr = { kind: "varRef", localId: "it.0", type: cit.iterT, loc }; const outRef: IrExpr = { kind: "varRef", localId: "out.0", type: outT, loc }; @@ -10532,7 +10533,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const key = `x509.record:${isBytes ? "bytes" : "str"}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%x509.record.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%x509.record", key); lowerer.valueHelpers.set(key, helper); const dataT = data.type; const dRef: IrExpr = { kind: "varRef", localId: "d.0", type: dataT, loc }; diff --git a/packages/compiler/src/frontend/lowering/lower-dgram.ts b/packages/compiler/src/frontend/lowering/lower-dgram.ts index 5c99ebc82..aa90fc7c7 100644 --- a/packages/compiler/src/frontend/lowering/lower-dgram.ts +++ b/packages/compiler/src/frontend/lowering/lower-dgram.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { dynUndefinedExpr, boolLit } from "../../ir/build.js"; /* The dgram/dns-surface lowering (node:dgram + node:dns — a spoke module * like lower-server.ts): module-function calls (dgram createSocket, dns @@ -357,7 +358,7 @@ function checkedDynPromise( const key = `dns.promise:${typeKey(declared)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%dns.promise.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%dns.promise", key); lowerer.valueHelpers.set(key, helper); const promiseT: IrType = { kind: "promise", inner: DYN }; lowerer.liftedFns.push({ @@ -891,7 +892,7 @@ export function lowerPromisifiedDnsCall( const key = `dnsPromise:${JSON.stringify(value)}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = `%dnsPromise.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%dnsPromise", key); lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, diff --git a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts index f472672b2..f105af786 100644 --- a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts +++ b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts @@ -42,6 +42,7 @@ * emit-overrides block at the bottom of this file (per-event * monomorphization of the override body, dispatch on the dynamic class, * super.emit as the prototype chain). */ +import { keyedHelperName } from "./stable-names.js"; import { checkKeypressListener } from "./builtins/readline.js"; import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; @@ -631,7 +632,7 @@ function lowerFlexListenerCall( const key = `emitter.${registering ? "onFlex" : "offDyn"}:${typeKey(recvT)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%emitter.${registering ? "onFlex" : "offFlex"}.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%emitter.${registering ? "onFlex" : "offFlex"}`, key); lowerer.arrHofHelpers.set(key, helper); const params: IrParam[] = [ { localId: "r.0", name: "r", type: recvT }, @@ -850,7 +851,7 @@ function lowerDynListenerCall( const existing = lowerer.arrHofHelpers.get(key); let helper = existing; if (!helper) { - helper = `%emitter.${registering ? "onDyn" : "offDyn"}.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%emitter.${registering ? "onDyn" : "offDyn"}`, key); lowerer.arrHofHelpers.set(key, helper); const check: IrStmt = { kind: "exprStmt", @@ -1000,7 +1001,7 @@ export function lowerDynamicEmitterEmit( const key = `emitter.dynEmit:${typeKey(recvT)}:${k}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%emitter.dynEmit.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%emitter.dynEmit", key); lowerer.arrHofHelpers.set(key, helper); const params: IrParam[] = [ { localId: "r.0", name: "r", type: recvT }, @@ -1092,7 +1093,7 @@ export function lowerDynamicEmitterOn( const key = `emitter.dynOn:${typeKey(recvT)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%emitter.dynOn.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%emitter.dynOn", key); lowerer.arrHofHelpers.set(key, helper); const params: IrParam[] = [ { localId: "r.0", name: "r", type: recvT }, @@ -2359,7 +2360,7 @@ function emitDispatchExpr( const key = `emitter.emitDispatch:${typeKey(recvT)}:${name}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%emitter.emitDispatch.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%emitter.emitDispatch", key); lowerer.arrHofHelpers.set(key, helper); const params: IrParam[] = [ { localId: "r.0", name: "r", type: recvT }, @@ -2424,7 +2425,7 @@ function eventsOnceBoxer( const key = `eventsOnce:${typeKey(fnType)}`; let name = lowerer.valueHelpers.get(key); if (name === undefined) { - name = `%events.onceArgs.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%events.onceArgs", key); lowerer.valueHelpers.set(key, name); const params: IrParam[] = tuple.map((type, i) => ({ localId: `%once.a${i}`, diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index e7a1b79a8..ba96bea8c 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { lowerWebAssemblyValue, refuseWebAssemblyUse } from "./builtins/webassembly.js"; import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../ir/build.js"; import { InternalCompilerError } from "../../errors.js"; @@ -2274,7 +2275,11 @@ export function cjsAccessorSnapshotOf( if (!lexerVisibleAccessors(ts.getSourceFileOfNode(getter)).includes(getter)) return null; const call = cjsAccessorCall(lowerer, getter, locOf(getter)); if (call === null) return null; - const snapshot = { id: `%g.cjsSnapshot.${lowerer.cjsAccessorSnapshots.size}`, type: call.type }; + const at = locOf(getter); + const snapshot = { + id: keyedHelperName(lowerer, "%g.cjsSnapshot", `${at.file}:${at.start}:${at.end}`), + type: call.type, + }; lowerer.globalsList.push({ id: snapshot.id, name: "%cjsSnapshot", @@ -2299,7 +2304,7 @@ export function cjsAccessorSnapshotStores( if (getters.length === 0) return []; let readyId = lowerer.cjsSnapshotReady.get(dep); if (readyId === undefined) { - readyId = `%g.cjsSnapshotReady.${lowerer.cjsSnapshotReady.size}`; + readyId = keyedHelperName(lowerer, "%g.cjsSnapshotReady", dep.fileName); lowerer.globalsList.push({ id: readyId, name: "%cjsSnapshotReady", type: BOOL, mutable: true }); lowerer.cjsSnapshotReady.set(dep, readyId); } diff --git a/packages/compiler/src/frontend/lowering/lower-functions.ts b/packages/compiler/src/frontend/lowering/lower-functions.ts index cd8dc31d1..653c0d837 100644 --- a/packages/compiler/src/frontend/lowering/lower-functions.ts +++ b/packages/compiler/src/frontend/lowering/lower-functions.ts @@ -270,7 +270,7 @@ export function lowerLambda( const nameIdent = !ts.isArrowFunction(node) && node.name && ts.isIdentifier(node.name) ? node.name : null; const baseName = nameIdent ? nameIdent.text : ""; - const fnName = `%fn${lowerer.lambdaCounter++}${baseName ? `_${baseName}` : ""}`; + const fnName = lowerer.closureName(locOf(node), baseName ? `_${baseName}` : ""); // Named function expressions/declarations can self-reference by name; an // object-literal method's name is a PROPERTY, not a binding — no self. const selfSymbol = diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index 4be5d0b7e..1979b43aa 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { dynUndefinedExpr, boolLit, @@ -268,7 +269,7 @@ function errorInspectHelper( const key = `insp.userError:${typeKey(t)}`; const existing = lowerer.inspectHelpers.get(key); if (existing) return existing; - const name = `%util.insp.${lowerer.inspectHelpers.size}`; + const name = keyedHelperName(lowerer, "%util.insp", key); lowerer.inspectHelpers.set(key, name); const v = (): IrExpr => varRef("v.0", t, loc); const r = (): IrExpr => varRef("r.0", F64, loc); @@ -1145,7 +1146,7 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string { const key = `insp:${typeKey(t)}`; const existing = lowerer.inspectHelpers.get(key); if (existing) return existing; - const name = `%util.insp.${lowerer.inspectHelpers.size}`; + const name = keyedHelperName(lowerer, "%util.insp", key); lowerer.inspectHelpers.set(key, name); const v = (): IrExpr => varRef("v.0", t, loc); @@ -2074,7 +2075,7 @@ function formatUnionHelper( const existing = lowerer.inspectHelpers.get(key); if (existing) return existing; const loc = helperLoc(site); - const name = `%util.fmtv.${lowerer.inspectHelpers.size}`; + const name = keyedHelperName(lowerer, "%util.fmtv", key); lowerer.inspectHelpers.set(key, name); const def = lowerer.unions.get(t.unionId); if (!def) throw new InternalCompilerError(`format value over unknown union ${t.unionId}`); diff --git a/packages/compiler/src/frontend/lowering/lower-mixins.ts b/packages/compiler/src/frontend/lowering/lower-mixins.ts index 766059dfb..9de027563 100644 --- a/packages/compiler/src/frontend/lowering/lower-mixins.ts +++ b/packages/compiler/src/frontend/lowering/lower-mixins.ts @@ -34,6 +34,7 @@ * statics-bearing mixin classes outside a plain top-level statement * (their declaration-time code must run exactly where the call * evaluates). */ +import { positionTag } from "./structural-positions.js"; import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; import { PoisonError } from "./lowerer.js"; @@ -452,7 +453,7 @@ function instantiateMixinCall( const base = mixinBaseClassOf(lowerer, call.arguments.nodes[0]!); const instName = lowerer.qualify( ts.getSourceFileOfNode(call), - `%mx${ts.tokenStartOfNode(call)}.${shape.classNode.name?.text ?? ""}`, + `%mx${positionTag(lowerer, call)}.${shape.classNode.name?.text ?? ""}`, ); const ordinal = lowerer.mixinOrdinals.get(shape.classNode) ?? 0; lowerer.mixinOrdinals.set(shape.classNode, ordinal + 1); diff --git a/packages/compiler/src/frontend/lowering/lower-mock.ts b/packages/compiler/src/frontend/lowering/lower-mock.ts index fcc68c787..8c50dee46 100644 --- a/packages/compiler/src/frontend/lowering/lower-mock.ts +++ b/packages/compiler/src/frontend/lowering/lower-mock.ts @@ -17,6 +17,7 @@ * writes the original back on restore(). Each mock registers a closure * that calls its restore() with its tracker (scr_test.c): the module's * `mock`, or the test's t.mock, which is reset when the test ends. */ +import { keyedHelperName } from "./stable-names.js"; import * as ts from "../ts7/adapter.js"; import { BOOL, @@ -136,7 +137,7 @@ function liftOnce( const key = `mock.${role}:${owner}`; const existing = lowerer.assertHelpers.get(key); if (existing) return existing; - const name = `%mock.${role}.${lowerer.assertHelpers.size}`; + const name = keyedHelperName(lowerer, `%mock.${role}`, key); lowerer.assertHelpers.set(key, name); build(name); return name; diff --git a/packages/compiler/src/frontend/lowering/lower-modules.test.ts b/packages/compiler/src/frontend/lowering/lower-modules.test.ts index 84cc96e7d..f1021e4da 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.test.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.test.ts @@ -34,7 +34,7 @@ test("retains dependencies from function storage, captures, generator channels a const completed = record("completed"); const body = record("body"); const global = record("global", record("nested")); - const wanted = lowerer.shapes.shapes.map((shape) => shape.id); + const wanted = lowerer.shapes.shapes.map((shape) => shape.id).sort(); record("unused"); const unionId = lowerer.unions.intern([body, UNDEFINED_T]); const unusedUnion = lowerer.unions.intern([F64, UNDEFINED_T]); @@ -162,7 +162,7 @@ test("closes nested structural types and index-signature values", () => { sent = record("sent"), indexed = record("indexed"); const containerId = lowerer.shapes.intern([], false, indexed); - const wanted = lowerer.shapes.shapes.map((shape) => shape.id); + const wanted = lowerer.shapes.shapes.map((shape) => shape.id).sort(); record("unused"); const nested: IrType = { kind: "func", diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index d1f276d44..0b99e88d3 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -1,4 +1,5 @@ import { THREADS_MODULE } from "../threads-import.js"; +import { positionTag } from "./structural-positions.js"; import { EXPERIMENTAL_MODULE } from "../experimental-import.js"; import { dynUndefinedExpr } from "../../ir/build.js"; import { everyStmtList, everyTypeChild } from "../../ir/traverse.js"; @@ -1074,8 +1075,14 @@ export function moduleArtifacts( else delete rest.abstractMethods; return rest; }), - records: lowerer.shapes.shapes.filter((r) => shapeIds.has(r.id)), - unions: lowerer.unions.unions.filter((u) => unionIds.has(u.id)), + // Ids come from what each shape and union is (keyedHelperName), so + // ordering by id gives every lowering order the same module. + records: lowerer.shapes.shapes + .filter((r) => shapeIds.has(r.id)) + .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)), + unions: lowerer.unions.unions + .filter((u) => unionIds.has(u.id)) + .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)), }; } @@ -1677,7 +1684,7 @@ export function collectGlobals( } if (type.kind === "void") lowerer.badType(stmt.name, lowerer.typeOf(stmt.name)); const g: IrGlobal = { - id: `%g.${tag}${nsPathPrefix(stmt)}%alias.${stmt.name.text}%${ts.tokenStartOfNode(stmt)}`, + id: `%g.${tag}${nsPathPrefix(lowerer, stmt)}%alias.${stmt.name.text}%${positionTag(lowerer, stmt)}`, name: stmt.name.text, type, mutable: false, @@ -1754,7 +1761,7 @@ export function collectGlobals( // (`%g.e.A%B.x`), so `namespace A { export const x }` and a // top-level `const x` never collide; non-exported members add the // declaring block's position (block-local bindings). - const nsPrefix = nsPathPrefix(stmt, list.declarations.nodes[0]); + const nsPrefix = nsPathPrefix(lowerer, stmt, list.declarations.nodes[0]); for (const decl of list.declarations.nodes) { // --provenance-sources: an elided pure-annotated dead const in a // fetched source module registers no global (the statement diff --git a/packages/compiler/src/frontend/lowering/lower-namespaces.ts b/packages/compiler/src/frontend/lowering/lower-namespaces.ts index dca835d77..19123dae3 100644 --- a/packages/compiler/src/frontend/lowering/lower-namespaces.ts +++ b/packages/compiler/src/frontend/lowering/lower-namespaces.ts @@ -36,6 +36,7 @@ * of the compiler already takes (a body running before the declaring * statement is not modeled — same as top-level const reads through early * calls). */ +import { positionTag, type PositionTagOwner } from "./structural-positions.js"; import * as ts from "../ts7/adapter.js"; import { packageShimFor } from "../package-shims.js"; import type { Lowerer } from "./lowerer.js"; @@ -164,7 +165,11 @@ function registerTypeOnlyBlocks(lowerer: Lowerer, decl: ts.ModuleDeclaration): v * distinct (resolution is by symbol; names only need uniqueness and * determinism). `modifierDecl` carries the declaration whose modifiers * decide exportedness (the first declarator of a variable statement). */ -export function nsPathPrefix(node: ts.Node, modifierDecl?: ts.Node): string { +export function nsPathPrefix( + owner: PositionTagOwner, + node: ts.Node, + modifierDecl?: ts.Node, +): string { const parent = node.parent; if (parent === undefined || !ts.isModuleBlock(parent)) return ""; const flagsHost = modifierDecl ?? node; @@ -179,10 +184,10 @@ export function nsPathPrefix(node: ts.Node, modifierDecl?: ts.Node): string { const nested = p.parent !== undefined && ts.isModuleBlock(p.parent); const nonExported = nested && (ts.getCombinedModifierFlags(p) & ts.ModifierFlags.Export) === 0; - segs.unshift(p.name.text + (nonExported ? `%${ts.tokenStartOfNode(p)}` : "")); + segs.unshift(p.name.text + (nonExported ? `%${positionTag(owner, p)}` : "")); } } - return `${segs.join("%")}${exported ? "" : `%${ts.tokenStartOfNode(parent)}`}.`; + return `${segs.join("%")}${exported ? "" : `%${positionTag(owner, parent)}`}.`; } /** The registered namespace-block kind a symbol's declaration sits in diff --git a/packages/compiler/src/frontend/lowering/lower-server.ts b/packages/compiler/src/frontend/lowering/lower-server.ts index 058d7874a..94d49d8b3 100644 --- a/packages/compiler/src/frontend/lowering/lower-server.ts +++ b/packages/compiler/src/frontend/lowering/lower-server.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { dynUndefinedExpr, nodeThrowExpr, @@ -183,7 +184,7 @@ export function voidizedCallback(lowerer: Lowerer, cb: IrExpr, loc: SrcLoc): IrE const toT = funcOf([...fromT.params], VOID); const key = `server.voidcb:${typeKey(fromT)}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? `%server.voidcb.${lowerer.arrHofHelpers.size}`; + const name = existing ?? keyedHelperName(lowerer, "%server.voidcb", key); if (!existing) { lowerer.arrHofHelpers.set(key, name); const impl = `${name}.impl`; @@ -279,7 +280,7 @@ function receiverReturningCall( ): IrExpr { const key = `server.recvret:${fn}:${opts?.prefix?.fn ?? ""}:${(opts?.mainArgIndices ?? []).join(".")}:${callArgs.map((a) => typeKey(a.type)).join(",")}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? `%server.recvret.${lowerer.arrHofHelpers.size}`; + const name = existing ?? keyedHelperName(lowerer, "%server.recvret", key); if (!existing) { lowerer.arrHofHelpers.set(key, name); const params = callArgs.map((a, i) => ({ localId: `p${i}.0`, name: `p${i}`, type: a.type })); @@ -590,7 +591,7 @@ function h2HeadersCallbackAdapter( ); const key = `h2.hdrcb:${handleKind}:${withStatus}:${typeKey(fromT)}:${shapeId ?? ""}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? `%h2.hdrcb.${lowerer.arrHofHelpers.size}`; + const name = existing ?? keyedHelperName(lowerer, "%h2.hdrcb", key); if (!existing) { lowerer.arrHofHelpers.set(key, name); const impl = `${name}.impl`; @@ -3583,7 +3584,7 @@ export function lowerCompatReqStreamOptionalCall( type: unionT, loc, }; - const id = `chain.${lowerer.chainCounter++}`; + const id = lowerer.closureName(loc, "", "chain"); const body = lowerH2StreamMethodCall(lowerer, call, call.expression, { kind: "chainRecv", id, @@ -4424,7 +4425,7 @@ function lowerHttpCreateServerForms( const hasMax = opts.maxHeaderSize !== null; const key = `server.opts:${opts.joinDup ? 1 : 0}:${opts.allowMissingHost ? 1 : 0}:${hasBuffer ? 1 : 0}:${hasMax ? 1 : 0}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? `%server.opts.${lowerer.arrHofHelpers.size}`; + const name = existing ?? keyedHelperName(lowerer, "%server.opts", key); if (!existing) { lowerer.arrHofHelpers.set(key, name); const params = [ @@ -8009,7 +8010,7 @@ function lowerHttpResMethodCall( const all = [...callArgs, cb]; const key = `server.endcb:${fn}:${all.map((a) => typeKey(a.type)).join(",")}`; const existing = lowerer.arrHofHelpers.get(key); - const helper = existing ?? `%server.endcb.${lowerer.arrHofHelpers.size}`; + const helper = existing ?? keyedHelperName(lowerer, "%server.endcb", key); if (!existing) { lowerer.arrHofHelpers.set(key, helper); const params = all.map((a, i) => ({ localId: `p${i}.0`, name: `p${i}`, type: a.type })); diff --git a/packages/compiler/src/frontend/lowering/lower-stdlib.ts b/packages/compiler/src/frontend/lowering/lower-stdlib.ts index 2ad3657c7..0beb24a0e 100644 --- a/packages/compiler/src/frontend/lowering/lower-stdlib.ts +++ b/packages/compiler/src/frontend/lowering/lower-stdlib.ts @@ -2278,7 +2278,7 @@ function lowerStaticNamespaceImport( ): IrExpr { const loc = locOf(call); const resultT: IrType & { kind: "promise" } = { kind: "promise", inner: ns }; - const fnName = `%fn${lowerer.lambdaCounter++}_staticImport`; + const fnName = lowerer.closureName(loc, "_staticImport"); const fnType: IrType & { kind: "func" } = { kind: "func", params: [], ret: resultT }; const fnCtx = newFnCtx(true, null, fnType, ns); fnCtx.isAsync = true; diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index b9add95a6..a75ec43b7 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { nodeThrowExpr, boolLit, numLit, strLit } from "../../ir/build.js"; import { hashStreamOf, isHashValueType } from "./builtins/hash-stream.js"; import { InternalCompilerError } from "../../errors.js"; @@ -311,7 +312,7 @@ function lowerStreamCallback( ); } } - const fnName = `%fn${lowerer.lambdaCounter++}_${which}cb`; + const fnName = lowerer.closureName(loc, `_${which}cb`); const fnCtx = newFnCtx(true, null, funcType, funcType.ret); lowerer.fnStack.push(fnCtx); try { @@ -1144,7 +1145,7 @@ function streamMethodWrapper( paramTypes.push(got.type); } const ret = found.sig.ret; - const fnName = `%fn${lowerer.lambdaCounter++}_${which}vt`; + const fnName = lowerer.closureName(loc, `_${which}vt`); const funcType = funcOf([thisType, ...paramTypes], ret); lowerer.fnStack.push(newFnCtx(true, null, funcType, ret)); try { @@ -1690,7 +1691,7 @@ function fromGenHelpers( const key = `streamFromGen:${role}:${typeKey(gen)}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = `%stream.fromGen.${role}.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, `%stream.fromGen.${role}`, key); lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, @@ -1779,7 +1780,7 @@ function classIteratorObject( const key = `streamFromIter:${cit.iterT.className}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = `%stream.fromIter.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%stream.fromIter", key); lowerer.valueHelpers.set(key, name); const nextName = `${name}.next`; const nextT = funcOf([], DYN); @@ -1940,7 +1941,7 @@ function classAsyncIteratorObject( const key = `streamFromAsyncIter:${cit.className}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = `%stream.fromAsyncIter.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%stream.fromAsyncIter", key); lowerer.valueHelpers.set(key, name); const it: IrLocal = { id: "it.0", name: "it", type: cit.iterT, mutable: false, boxed: true }; const src: IrLocal = { @@ -2894,7 +2895,7 @@ function pipelineBoxHelper( const key = `pipelineBox:${typeKey(promiseT)}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = `%stream.pipelineBox.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%stream.pipelineBox", key); lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, @@ -2921,7 +2922,7 @@ function pipelineAbortHelper(lowerer: Lowerer, ac: IrExpr, loc: SrcLoc): IrExpr let name = lowerer.valueHelpers.get(key); const abortT = funcOf([], VOID); if (!name) { - name = `%stream.pipelineAbort.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%stream.pipelineAbort", key); lowerer.valueHelpers.set(key, name); const acLocal: IrLocal = { id: "ac.0", name: "ac", type: DYN, mutable: false, boxed: true }; const acRef = varRef(acLocal.id, DYN, loc); diff --git a/packages/compiler/src/frontend/lowering/lower-timers.ts b/packages/compiler/src/frontend/lowering/lower-timers.ts index 7d4bba34b..03c9c5c1f 100644 --- a/packages/compiler/src/frontend/lowering/lower-timers.ts +++ b/packages/compiler/src/frontend/lowering/lower-timers.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { dynUndefinedExpr, numLit, varRef } from "../../ir/build.js"; import * as ts from "../ts7/adapter.js"; import { isJsSourceFile } from "../program.js"; @@ -55,7 +56,7 @@ export function adaptZeroArgTimerCallback( const toT: IrType = { kind: "func", params: [], ret: VOID }; const key = `timer.dropret:${typeKey(fromT)}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? `%timer.dropret.${lowerer.arrHofHelpers.size}`; + const name = existing ?? keyedHelperName(lowerer, "%timer.dropret", key); if (!existing) { lowerer.arrHofHelpers.set(key, name); lowerer.liftedFns.push( @@ -117,7 +118,7 @@ export function adaptZeroArgTimerCallback( const toT: IrType = { kind: "func", params: [], ret: VOID }; const key = `timer.droparg:${typeKey(fromT)}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? `%timer.droparg.${lowerer.arrHofHelpers.size}`; + const name = existing ?? keyedHelperName(lowerer, "%timer.droparg", key); if (!existing) { lowerer.arrHofHelpers.set(key, name); lowerer.liftedFns.push( @@ -203,7 +204,7 @@ function makeTimerArgsThunk( const toT: IrType = { kind: "func", params: [], ret: VOID }; const key = `timer.argsthunk:${n}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? `%timer.argsthunk.${lowerer.arrHofHelpers.size}`; + const name = existing ?? keyedHelperName(lowerer, "%timer.argsthunk", key); if (!existing) { lowerer.arrHofHelpers.set(key, name); const impl = `${name}.impl`; diff --git a/packages/compiler/src/frontend/lowering/lowerer.test.ts b/packages/compiler/src/frontend/lowering/lowerer.test.ts index bacdc7941..750c27694 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.test.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.test.ts @@ -10,6 +10,7 @@ import { } from "../../ir/ir.js"; import type { Node, Program, SourceFile, Symbol, Type } from "../ts7/adapter.js"; import { Lowerer } from "./lowerer.js"; +import { UnionRegistry } from "../type-mapper.js"; import { classMethodValue } from "./class-method-values.js"; const loc = { file: "island.ts", start: 0, end: 1 }; @@ -32,7 +33,7 @@ test("reuses complete copy routes without repeating discriminator planning", () expect(plan).not.toHaveBeenCalled(); expect(lowerer.liftedFns.filter((fn) => fn.name === helper)).toHaveLength(1); const recursive = {} as Type; - lowerer.shapes.recursiveRef(recursive); + lowerer.shapes.recursiveRef(recursive, "Recursive"); lowerer.shapes.finalizeRecursive(recursive, [{ name: "kind", type: STRING }]); expect(lowerer.unionRetagHelper(from, to, loc)).toBe(helper); expect(plan).toHaveBeenCalled(); @@ -54,7 +55,7 @@ test("replans after a recursive union gains its final arms", () => { const lowerer = context(); const recursive = {} as Type; const from = lowerer.unions.intern([UNDEFINED_T, NULL_T]); - const to = lowerer.unions.recursiveRef(recursive); + const to = lowerer.unions.recursiveRef(recursive, "Recursive"); const trapped = lowerer.unionRetagHelper(from, to, loc); expect(trapped).not.toBeNull(); lowerer.unions.finalizeRecursive(recursive, [UNDEFINED_T]); @@ -75,7 +76,7 @@ test("rechecks width conversions after recursive record definitions settle", () const lowerer = context(); const recursive = {} as Type; const source = record(lowerer.shapes.intern([{ name: "value", type: F64 }])); - const target = record(lowerer.shapes.recursiveRef(recursive)); + const target = record(lowerer.shapes.recursiveRef(recursive, "Recursive")); const from = lowerer.unions.intern([source, UNDEFINED_T]); const to = lowerer.unions.intern([target, NULL_T]); expect(lowerer.unionRetagHelper(from, to, loc)).not.toBeNull(); @@ -129,7 +130,7 @@ test("literal ownership is reused across values and rechecked when recursive rec expect(inspect).not.toHaveBeenCalled(); const recursive = {} as Type; - const pendingShape = record(lowerer.shapes.recursiveRef(recursive)); + const pendingShape = record(lowerer.shapes.recursiveRef(recursive, "Recursive")); const pending = lowerer.unions.intern([pendingShape], { field: "kind", cases: [{ tag: 0, values: ["a"] }], @@ -143,7 +144,7 @@ test("literal ownership is reused across values and rechecked when recursive rec test("literal ownership sees a recursive union's finalized discriminator", () => { const lowerer = context(); const recursive = {} as Type; - const union = lowerer.unions.recursiveRef(recursive); + const union = lowerer.unions.recursiveRef(recursive, "Recursive"); const shape = record(lowerer.shapes.intern([{ name: "kind", type: STRING }])); expect(lowerer.literalUnionArm(union, ["ready"])).toBeNull(); lowerer.unions.finalizeRecursive(recursive, [shape], { @@ -182,7 +183,10 @@ test("removing undefined reuses the discriminator transformation without hiding ); const recursive = {} as Type; - const pending: IrType = { kind: "union", unionId: lowerer.unions.recursiveRef(recursive) }; + const pending: IrType = { + kind: "union", + unionId: lowerer.unions.recursiveRef(recursive, "Recursive"), + }; expect(lowerer.stripUndefinedArm(pending)).toEqual(pending); lowerer.unions.finalizeRecursive(recursive, [STRING, UNDEFINED_T]); expect(lowerer.stripUndefinedArm(pending)).toEqual(STRING); @@ -215,7 +219,10 @@ test("adding undefined reuses semantic union transformations and sees recursive expect(lowerer.unions.get(added.unionId)?.discriminant).toEqual(discriminant); const recursive = {} as Type; - const pending: IrType = { kind: "union", unionId: lowerer.unions.recursiveRef(recursive) }; + const pending: IrType = { + kind: "union", + unionId: lowerer.unions.recursiveRef(recursive, "Recursive"), + }; const before = lowerer.withUndefinedArmOf(pending); lowerer.unions.finalizeRecursive(recursive, [a, b], discriminant); expect(lowerer.withUndefinedArmOf(pending)).toEqual(added); @@ -227,13 +234,18 @@ test("adding undefined reuses semantic union transformations and sees recursive test("optional union widening keeps refused and missing contracts current", () => { const lowerer = context(); const recursive = {} as Type; - const pending: IrType = { kind: "union", unionId: lowerer.unions.recursiveRef(recursive) }; + const pending: IrType = { + kind: "union", + unionId: lowerer.unions.recursiveRef(recursive, "Recursive"), + }; expect(lowerer.withUndefinedArmOf(pending)).not.toBeNull(); lowerer.unions.finalizeRecursive(recursive, [{ kind: "date" }, STRING]); expect(lowerer.withUndefinedArmOf(pending)).toBeNull(); expect(lowerer.withUndefinedArmOf(pending)).toBeNull(); - const missing: IrType = { kind: "union", unionId: `u${lowerer.unions.unions.length}` }; + // Union ids come from their arms, so a fresh registry names the union + // this one has not interned yet. + const missing: IrType = { kind: "union", unionId: new UnionRegistry().intern([F64, STRING]) }; expect(lowerer.withUndefinedArmOf(missing)).toBeNull(); expect(lowerer.unions.intern([F64, STRING])).toBe(missing.unionId); expect(lowerer.withUndefinedArmOf(missing)?.kind).toBe("union"); diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index ee8ba7ecb..687a2fb1a 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -1,4 +1,6 @@ import { relieveHeapPressure } from "../../heap-pressure.js"; +import { keyedHelperName } from "./stable-names.js"; +import { positionTag, StructuralPositions } from "./structural-positions.js"; import { onlyForwarded, stripTypeWrappers } from "./typed-arguments.js"; import { needsGeneralNodeStreams, @@ -1004,6 +1006,11 @@ function lowerShuffle(): ((wave: string[]) => void) | null { * the bodies reachable emit did NOT mark (plus deferred collection * diagnostics nothing flushed), reported separately: whole-program analysis without * letting unreached code fail builds. */ +/** The closure root of an emit-override specialization body. */ +function emitSpecRoot(spec: EmitSpecRequest): string { + return `%emit.${spec.info.def.name}.${spec.event}.${spec.ordinal}`; +} + export function lowerToIr( program: ts.Program, entry: ts.SourceFile, @@ -2801,6 +2808,8 @@ export class Lowerer { /** Generated value helpers, keyed by operation and type. Callers own * their key prefixes; this includes more than structural conversions. */ readonly valueHelpers = new Map(); + /** Each name keyedHelperName made, with a second hash of its key. */ + readonly keyedNameKeys = new Map(); /** structuralNarrowing's assignable classes, keyed by static and * narrowed class name. */ readonly structuralClassPeers = new Map(); @@ -2937,7 +2946,6 @@ export class Lowerer { ctx: FnCtx; } >(); - chainCounter = 0; /** Keyed by program-wide qualified class name (what IR object types carry). */ readonly classes = new Map(); readonly classBySymbol = new Map(); @@ -3167,7 +3175,47 @@ export class Lowerer { * dispatch revision needs no weak-key identity or process-global state. */ errorMethodDispatchRevision: string | null = null; errorPropertyDispatchRevision: string | null = null; - lambdaCounter = 0; + /** Structural source positions (structural-positions.ts) for names that + * stay the same when unrelated code moves. */ + readonly positions = new StructuralPositions(); + + /** The outermost node whose token span is exactly `loc`, or null. */ + nodeAtLoc(loc: SrcLoc): ts.Node | null { + const sf = this.program.getSourceFile(loc.file); + return sf === undefined ? null : (ts.nodeAtSpan(sf, loc.start, loc.end) ?? null); + } + + /** The lowering unit whose name scopes the closures it lifts. */ + private closureRoot = "%pre"; + private readonly closureNameCounts = new Map(); + + /** Runs `fn` with the closures it lifts named under `root`. */ + withClosureRoot(root: string, fn: () => T): T { + const previous = this.closureRoot; + this.closureRoot = root; + try { + return fn(); + } finally { + this.closureRoot = previous; + } + } + + /** The name of a closure (or wrapper) lifted for the source at `loc`: + * `.` of the current lowering unit, the location and + * `suffix`, then `suffix`. A unit that lifts several functions for the same + * location and suffix numbers them in the order it lifts them, so a + * closure's name depends only on its unit, not on which units lowered + * before it. */ + closureName(loc: SrcLoc, suffix = "", prefix = "%fn"): string { + const node = this.nodeAtLoc(loc); + const where = node === null ? `${loc.file}:${loc.start}:${loc.end}` : this.positions.of(node); + const key = `${prefix}|${this.closureRoot}|${where}|${suffix}`; + const first = keyedHelperName(this, prefix, key); + const seen = this.closureNameCounts.get(first) ?? 0; + this.closureNameCounts.set(first, seen + 1); + return (seen === 0 ? first : keyedHelperName(this, prefix, `${key}|${seen}`)) + suffix; + } + readonly preparedClassFactories = new Map(); readonly preparingClassFactories = new Set(); @@ -3808,11 +3856,11 @@ export class Lowerer { })() ? this.qualify( ts.getSourceFileOfNode(decl), - `%cx${ts.tokenStartOfNode(decl)}.${decl.name?.text ?? ""}`, + `%cx${positionTag(this, decl)}.${decl.name?.text ?? ""}`, ) : this.qualify( ts.getSourceFileOfNode(decl), - nsPathPrefix(decl) + (decl.name ? decl.name.text : "%anon"), + nsPathPrefix(this, decl) + (decl.name ? decl.name.text : "%anon"), ); for (let index = this.localClassInstantiations.length - 1; index >= 0; index--) { const entry = this.localClassInstantiations[index]!; @@ -7371,7 +7419,10 @@ export class Lowerer { } for (const decl of fp.classDecls) { const info = this.classes.get(this.classNamer(decl)); - if (info) functions.push(...this.lowerClassMembers(info)); + if (info) + functions.push( + ...this.withClosureRoot(info.def.name, () => this.lowerClassMembers(info)), + ); else if (this.countsSkips()) this.countSkippedBody(decl); } } @@ -7384,7 +7435,10 @@ export class Lowerer { // are reachable by definition, already counted by reachable emit. if (!this.remainder) { for (const fp of parts) { - functions.push(this.lowerFileInit(fp.sf, fp.topStmts, this.initNameOf.get(fp.sf)!)); + const initName = this.initNameOf.get(fp.sf)!; + functions.push( + this.withClosureRoot(initName, () => this.lowerFileInit(fp.sf, fp.topStmts, initName)), + ); } functions.push(this.buildMain()); } @@ -7426,7 +7480,9 @@ export class Lowerer { // default): the diagnostic is recorded — the instance skips // like a signature-blocked function (lowerFunction's rule). try { - functions.push(this.lowerGenericInstance(info, inst)); + functions.push( + this.withClosureRoot(inst.name, () => this.lowerGenericInstance(info, inst)), + ); } catch (e) { if (!(e instanceof PoisonError)) throw e; } @@ -7436,7 +7492,10 @@ export class Lowerer { // instances of its own; the joint fixpoint covers both). while (es < this.emitSpecQueue.length) { try { - const fn = lowerEmitOverrideSpec(this, this.emitSpecQueue[es++]!); + const spec = this.emitSpecQueue[es++]!; + const fn = this.withClosureRoot(emitSpecRoot(spec), () => + lowerEmitOverrideSpec(this, spec), + ); if (fn) functions.push(fn); } catch (e) { if (!(e instanceof PoisonError)) throw e; @@ -7990,7 +8049,9 @@ export class Lowerer { this.genericClassDemandPriority.set(info, ref); instanceFunctions.push( ...this.withGenericDemandOwner(owner, () => - this.shapes.withDeclaredOrderPriority(ref, () => this.lowerClassMembers(info)), + this.shapes.withDeclaredOrderPriority(ref, () => + this.withClosureRoot(info.def.name, () => this.lowerClassMembers(info)), + ), ), ); relieveHeapPressure(); @@ -8017,7 +8078,7 @@ export class Lowerer { instanceFunctions.push( this.withGenericDemandOwner(owner, () => this.shapes.withDeclaredOrderPriority(ref, () => - this.lowerGenericInstance(info, inst), + this.withClosureRoot(inst.name, () => this.lowerGenericInstance(info, inst)), ), ), ); @@ -8050,9 +8111,12 @@ export class Lowerer { ); while (specLowered < waveEnd) { try { - const fn = this.shapes.withDeclaredOrderPriority([4, instanceMetadataOrder++], () => - lowerEmitOverrideSpec(this, this.emitSpecQueue[specLowered++]!), - ); + const fn = this.shapes.withDeclaredOrderPriority([4, instanceMetadataOrder++], () => { + const spec = this.emitSpecQueue[specLowered++]!; + return this.withClosureRoot(emitSpecRoot(spec), () => + lowerEmitOverrideSpec(this, spec), + ); + }); if (fn) instanceFunctions.push(fn); } catch (e) { if (!(e instanceof PoisonError)) throw e; @@ -8080,7 +8144,9 @@ export class Lowerer { initFunctions.push( this.withGenericDemandOwner(owner, () => this.shapes.withDeclaredOrderPriority(priority, () => - this.lowerFileInit(fp.sf, fp.topStmts, this.initNameOf.get(fp.sf)!), + this.withClosureRoot(this.initNameOf.get(fp.sf)!, () => + this.lowerFileInit(fp.sf, fp.topStmts, this.initNameOf.get(fp.sf)!), + ), ), ), ); @@ -8106,7 +8172,9 @@ export class Lowerer { const priority = metadataPriority.get(name) ?? [3, expressionMetadataOrder++]; const owner = demandOwner(priority); const fn = this.withGenericDemandOwner(owner, () => - this.shapes.withDeclaredOrderPriority(priority, unit.lower), + this.shapes.withDeclaredOrderPriority(priority, () => + this.withClosureRoot(name, unit.lower), + ), ); if (fn) loweredUnits.set(name, fn); relieveHeapPressure(); @@ -13489,7 +13557,7 @@ export class Lowerer { const key = `${fn.valueFn ?? fn.fn}:${typeKey(funcType)}:${adapterShape}`; let fnName = this.builtinCallableValueFns.get(key); if (!fnName) { - fnName = `%builtin.value.${fn.valueFn ?? fn.fn}.${this.builtinCallableValueFns.size}`; + fnName = keyedHelperName(this, `%builtin.value.${fn.valueFn ?? fn.fn}`, key); this.builtinCallableValueFns.set(key, fnName); this.builtinCallableValueNames.set(fnName, display.slice(display.lastIndexOf(".") + 1)); const params = funcType.params.map((type, index) => ({ diff --git a/packages/compiler/src/frontend/lowering/native-ffi.ts b/packages/compiler/src/frontend/lowering/native-ffi.ts index 0f932ba93..62f2be9f1 100644 --- a/packages/compiler/src/frontend/lowering/native-ffi.ts +++ b/packages/compiler/src/frontend/lowering/native-ffi.ts @@ -562,7 +562,7 @@ export function lowerFfiMemoryModule(lowerer: Lowerer, loc: SrcLoc): IrExpr { const entries = lowerer.ffiImports .filter((entry) => entry.library !== undefined) .map((entry) => { - const name = `%ffi.module.${lowerer.lambdaCounter++}`; + const name = lowerer.closureName(loc, "", "%ffi.module"); const sourceTypes = ffiSourceParamTypes(entry.params); const locals: IrLocal[] = sourceTypes.map((_, i) => ({ id: `%arg${i}`, diff --git a/packages/compiler/src/frontend/lowering/object-enumeration-receiver.ts b/packages/compiler/src/frontend/lowering/object-enumeration-receiver.ts index 855da3bda..388ac3904 100644 --- a/packages/compiler/src/frontend/lowering/object-enumeration-receiver.ts +++ b/packages/compiler/src/frontend/lowering/object-enumeration-receiver.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { BOOL, STRING, @@ -27,7 +28,7 @@ export function objectEnumerationReceiver( const key = `obj.receiver:${source.unionId}:${expected.shapeId}`; let name = lowerer.arrHofHelpers.get(key); if (name === undefined) { - name = `%obj.receiver.${lowerer.arrHofHelpers.size}`; + name = keyedHelperName(lowerer, "%obj.receiver", key); lowerer.arrHofHelpers.set(key, name); const value = varRef("value.0", source, loc); lowerer.liftedFns.push({ diff --git a/packages/compiler/src/frontend/lowering/pairs-snapshot.ts b/packages/compiler/src/frontend/lowering/pairs-snapshot.ts index 3b33fb0cd..acf7b3bf9 100644 --- a/packages/compiler/src/frontend/lowering/pairs-snapshot.ts +++ b/packages/compiler/src/frontend/lowering/pairs-snapshot.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { arrayOf, BOOL, @@ -49,7 +50,7 @@ export function pairsSnapshotHelper( const key = `${options.keyPrefix}.snapshot:${shapeId}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%${options.keyPrefix}.snapshot.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, `%${options.keyPrefix}.snapshot`, key); lowerer.valueHelpers.set(key, name); const recordType: IrType = { kind: "record", shapeId }; const pairsType = arrayOf(STRING); diff --git a/packages/compiler/src/frontend/lowering/promise-executor-adapter.ts b/packages/compiler/src/frontend/lowering/promise-executor-adapter.ts index 09d228b05..84aa8dac0 100644 --- a/packages/compiler/src/frontend/lowering/promise-executor-adapter.ts +++ b/packages/compiler/src/frontend/lowering/promise-executor-adapter.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { varRef } from "../../ir/build.js"; import { type IrExpr, @@ -40,7 +41,7 @@ export function adaptPromiseExecutor( const key = `promise.executor:${typeKey(execT)}:${typeKey(inner)}`; let name = lowerer.arrHofHelpers.get(key); if (name === undefined) { - name = `%promise.executor.${lowerer.arrHofHelpers.size}`; + name = keyedHelperName(lowerer, "%promise.executor", key); lowerer.arrHofHelpers.set(key, name); const impl = `${name}.impl`; const captured: IrLocal = { id: "f.0", name: "f", type: execT, mutable: false, boxed: true }; diff --git a/packages/compiler/src/frontend/lowering/require-initialization.ts b/packages/compiler/src/frontend/lowering/require-initialization.ts index d979423b0..c8fc9c730 100644 --- a/packages/compiler/src/frontend/lowering/require-initialization.ts +++ b/packages/compiler/src/frontend/lowering/require-initialization.ts @@ -9,6 +9,7 @@ * and every reference that can run earlier checks it first — the same * module.tdzCheck the import-cycle bindings use. A reference whose lowering * leaves no expression to check keeps the SC1013 fence. */ +import { positionTag } from "./structural-positions.js"; import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; import { @@ -119,7 +120,7 @@ export function markRequireEarlyBindings(lowerer: Lowerer): void { const tag = rawTag === "" ? "e." : rawTag.replace(/^%/, ""); const bySymbol = new Map(); for (const { decl } of declared) { - const flagId = `%g.${tag}%required.${ts.tokenStartOfNode(decl)}`; + const flagId = `%g.${tag}%required.${positionTag(lowerer, decl)}`; lowerer.globalsList.push({ id: flagId, name: "%initialized", type: BOOL, mutable: true }); const flag: RequireInitFlag = { flagId, names: new Map() }; for (const id of boundNames(decl.name)) { diff --git a/packages/compiler/src/frontend/lowering/stable-names.ts b/packages/compiler/src/frontend/lowering/stable-names.ts new file mode 100644 index 000000000..8f7d0967e --- /dev/null +++ b/packages/compiler/src/frontend/lowering/stable-names.ts @@ -0,0 +1,48 @@ +import { InternalCompilerError } from "../../errors.js"; + +/** The owner of the names keyedHelperName hands out. */ +export interface KeyedNameOwner { + /** Each keyed name with a second hash of the key it was made from, which + * tells a repeated key from a different key with the same name. */ + readonly keyedNameKeys: Map; +} + +/** 48 bits of two FNV-1a hashes of `key`, as 12 hex digits. */ +function keyHash(key: string): string { + let a = 0x811c9dc5; + let b = 0x01000193 ^ 0x5bd1e995; + for (let i = 0; i < key.length; i++) { + const c = key.charCodeAt(i); + a = Math.imul(a ^ c, 0x01000193); + b = Math.imul(b ^ c, 0x01000193) ^ (b >>> 13); + } + const high = (b >>> 0).toString(16).padStart(8, "0").slice(4); + return high + (a >>> 0).toString(16).padStart(8, "0"); +} + +/** A 32-bit hash of `key` independent of keyHash's bits. */ +function checkHash(key: string): number { + let h = 0x9e3779b9; + for (let i = 0; i < key.length; i++) + h = Math.imul(h ^ key.charCodeAt(i), 0x85ebca6b) ^ (h >>> 15); + return h >>> 0; +} + +/** The name of a helper interned by `key`: ``. It depends only on the key, not on how many helpers were interned + * before, so a helper has the same name whichever unit first needs it. Two + * keys that hash to the same name throw an InternalCompilerError. */ +export function keyedHelperName( + owner: KeyedNameOwner, + prefix: string, + key: string, + separator = ".", +): string { + const name = `${prefix}${separator}${keyHash(key)}`; + const check = checkHash(key); + const previous = owner.keyedNameKeys.get(name); + if (previous === undefined) owner.keyedNameKeys.set(name, check); + else if (previous !== check) + throw new InternalCompilerError(`helper name ${name} is shared by two helper keys`); + return name; +} diff --git a/packages/compiler/src/frontend/lowering/strict-equality.ts b/packages/compiler/src/frontend/lowering/strict-equality.ts index 7b9b6902b..8a87c8111 100644 --- a/packages/compiler/src/frontend/lowering/strict-equality.ts +++ b/packages/compiler/src/frontend/lowering/strict-equality.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { boolLit, varRef } from "../../ir/build.js"; import { BOOL, @@ -135,7 +136,7 @@ export function lowerUnionEquality( loc, ); if (!result) return null; - name = `%value.eq.${lowerer.equalityHelpers.size}`; + name = keyedHelperName(lowerer, "%value.eq", key); lowerer.equalityHelpers.set(key, name); lowerer.liftedFns.push({ name, diff --git a/packages/compiler/src/frontend/lowering/structural-positions.ts b/packages/compiler/src/frontend/lowering/structural-positions.ts new file mode 100644 index 000000000..cdc290b4b --- /dev/null +++ b/packages/compiler/src/frontend/lowering/structural-positions.ts @@ -0,0 +1,103 @@ +import * as ts from "../ts7/adapter.js"; +import { keyedHelperName, type KeyedNameOwner } from "./stable-names.js"; + +/** The declaration kinds whose names anchor the positions inside them. */ +const ANCHOR_KINDS: ReadonlySet = new Set([ + ts.SyntaxKind.FunctionDeclaration, + ts.SyntaxKind.ClassDeclaration, + ts.SyntaxKind.ClassExpression, + ts.SyntaxKind.VariableDeclaration, + ts.SyntaxKind.MethodDeclaration, + ts.SyntaxKind.GetAccessor, + ts.SyntaxKind.SetAccessor, + ts.SyntaxKind.PropertyDeclaration, + ts.SyntaxKind.PropertyAssignment, + ts.SyntaxKind.ModuleDeclaration, + ts.SyntaxKind.EnumDeclaration, + ts.SyntaxKind.Constructor, +]); + +/** The name of `node` when it is a named declaration that anchors the + * positions of the nodes inside it, or null. */ +function anchorName(node: ts.Node): string | null { + return ts.declarationNameAt(node, node.index, ANCHOR_KINDS); +} + +/** The nearest enclosing anchor of `node` (not `node` itself), or its + * source file. */ +function anchorOf(node: ts.Node): ts.Node { + for (let parent = node.parent; parent !== undefined; parent = parent.parent) { + if (ts.isSourceFile(parent) || anchorName(parent) !== null) return parent; + } + return ts.getSourceFileOfNode(node); +} + +/** Source positions that stay the same when code outside the enclosing + * declaration changes. A node's structural position is its file, the chain + * of enclosing named declarations (each with its ordinal among the + * declarations of the same kind and name in its own anchor), and its + * ordinal among the nodes of its kind whose nearest anchor is the same + * declaration. Editing another declaration, or code nested in another + * named declaration of the same anchor, leaves the position unchanged. */ +export class StructuralPositions { + /** Per anchor and key: the indices of the nodes with that key whose + * nearest anchor it is, in preorder (anchoredNodes). */ + private readonly nodesByKey = new Map>(); + private readonly paths = new Map(); + + /** The ordinal of `node` among the nodes of `anchor` with its key. */ + private ordinalIn(anchor: ts.Node, node: ts.Node, key: string): number { + let byKey = this.nodesByKey.get(anchor); + if (byKey === undefined) this.nodesByKey.set(anchor, (byKey = new Map())); + let indices = byKey.get(key); + if (indices === undefined) { + indices = ts.anchoredNodes(anchor, key, ANCHOR_KINDS); + byKey.set(key, indices); + } + // Indices increase in preorder. + let low = 0; + let high = indices.length - 1; + while (low <= high) { + const middle = (low + high) >> 1; + const at = indices[middle]!; + if (at === node.index) return middle; + if (at < node.index) low = middle + 1; + else high = middle - 1; + } + return -1; + } + + /** The anchor chain of `anchor` from the file down. */ + private pathOf(anchor: ts.Node): string { + const cached = this.paths.get(anchor); + if (cached !== undefined) return cached; + let path: string; + if (ts.isSourceFile(anchor)) path = anchor.fileName; + else { + const parent = anchorOf(anchor); + const name = anchorName(anchor); + path = `${this.pathOf(parent)}/${name}#${this.ordinalIn(parent, anchor, `${anchor.kind}:${name}`)}`; + } + this.paths.set(anchor, path); + return path; + } + + /** The structural position of `node`. */ + of(node: ts.Node): string { + const anchor = anchorOf(node); + const name = anchorName(node); + const key = name === null ? `${node.kind}` : `${node.kind}:${name}`; + return `${this.pathOf(anchor)}|${key}#${this.ordinalIn(anchor, node, key)}`; + } +} + +/** The owner of structural positions and the names made from them. */ +export interface PositionTagOwner extends KeyedNameOwner { + readonly positions: StructuralPositions; +} + +/** A short tag for `node`'s structural position, for names that must stay + * unique per source node (class expressions, namespace blocks). */ +export function positionTag(owner: PositionTagOwner, node: ts.Node): string { + return keyedHelperName(owner, "", owner.positions.of(node), ""); +} diff --git a/packages/compiler/src/frontend/ts7/ast.ts b/packages/compiler/src/frontend/ts7/ast.ts index 369213d89..0bddd1a80 100644 --- a/packages/compiler/src/frontend/ts7/ast.ts +++ b/packages/compiler/src/frontend/ts7/ast.ts @@ -47,8 +47,9 @@ import { } from "./ast-guards.generated.js"; import type { Ts7DiagnosticData as Diagnostic } from "./session-schema.generated.js"; import { ModifierFlags, SyntaxKind } from "./enums.js"; -import { AstKind } from "./ast-schema.generated.js"; +import { AstKind, KIND_NODE_LIST } from "./ast-schema.generated.js"; import { PREORDER_LIST } from "./ast-wire.js"; +import { tokenStartOfNode } from "./node-functions.js"; /* ---- everything 7 kept under the same name ---- */ @@ -196,6 +197,93 @@ function walkLinkedPreorder( } } +/** The semantic children of a wire node: list items stand in for their + * list, and attached JSDoc trees are left out (the walkPreorder view). */ +function semanticChildIndices(root: Node, index: number, out: number[]): void { + const wire = root.file.wire; + for (let child = wire.firstChild(index); child !== 0; child = wire.next(child)) { + const kind = wire.kind(child); + if (kind === KIND_NODE_LIST) { + for (let item = wire.firstChild(child); item !== 0; item = wire.next(item)) out.push(item); + } else if (kind !== AstKind.JSDoc) out.push(child); + } +} + +/** The outermost node of `sourceFile` whose token span is exactly + * [start, end), or undefined. Only nodes ending at `end` are + * materialized. */ +export function nodeAtSpan(sourceFile: SourceFile, start: number, end: number): Node | undefined { + const wire = sourceFile.file.wire; + const children: number[] = []; + let index = sourceFile.index; + for (;;) { + children.length = 0; + semanticChildIndices(sourceFile, index, children); + let next = 0; + for (const child of children) { + if (wire.pos(child) > start || wire.end(child) < end) continue; + if (wire.end(child) === end) { + const node = sourceFile.file.node(child); + if (tokenStartOfNode(node) === start) return node; + } + next = child; + break; + } + if (next === 0) return undefined; + index = next; + } +} + +/** The name of the declaration at wire `index` in `root`'s file when its + * kind is in `kinds`: its identifier, private identifier or string literal + * name, "constructor" for a constructor, or null. Reads the wire without + * materializing nodes. */ +export function declarationNameAt( + root: Node, + index: number, + kinds: ReadonlySet, +): string | null { + const wire = root.file.wire; + const kind = wire.kind(index); + if (!kinds.has(kind)) return null; + if (kind === SyntaxKind.Constructor) return "constructor"; + const name = wire.namedChild(index, "name"); + if (name === 0) return null; + const nameKind = wire.kind(name); + if ( + nameKind !== SyntaxKind.Identifier && + nameKind !== SyntaxKind.PrivateIdentifier && + nameKind !== SyntaxKind.StringLiteral + ) + return null; + return wire.text(name) ?? null; +} + +/** The indices, in preorder, of the nodes below `root` that have `key` and + * are not inside a nested named declaration (declarationNameAt with + * `kinds`). A named declaration is keyed `:` and its subtree is + * not visited; other nodes are keyed by their kind. No node is + * materialized. */ +export function anchoredNodes(root: Node, key: string, kinds: ReadonlySet): number[] { + const wire = root.file.wire; + const out: number[] = []; + const children: number[] = []; + const stack: number[] = []; + semanticChildIndices(root, root.index, children); + for (let i = children.length - 1; i >= 0; i--) stack.push(children[i]!); + while (stack.length > 0) { + const index = stack.pop()!; + const kind = wire.kind(index); + const name = declarationNameAt(root, index, kinds); + if ((name === null ? String(kind) : `${kind}:${name}`) === key) out.push(index); + if (name !== null) continue; + children.length = 0; + semanticChildIndices(root, index, children); + for (let i = children.length - 1; i >= 0; i--) stack.push(children[i]!); + } + return out; +} + export function getLineAndCharacterOfPosition( sourceFile: SourceFile, position: number, diff --git a/packages/compiler/src/frontend/type-mapper.test.ts b/packages/compiler/src/frontend/type-mapper.test.ts index cf78554b4..a99eb44cc 100644 --- a/packages/compiler/src/frontend/type-mapper.test.ts +++ b/packages/compiler/src/frontend/type-mapper.test.ts @@ -41,7 +41,7 @@ describe("union arm lookup", () => { test("sees finalized recursive arms after a missing lookup", () => { const unions = new UnionRegistry(); const recursive = {} as Type; - const union = unions.recursiveRef(recursive); + const union = unions.recursiveRef(recursive, "Recursive"); expect(unions.armTag(union, STRING)).toBe(-1); unions.finalizeRecursive(recursive, [ F64, diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index a233f0186..ce73e8bc2 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -1,4 +1,5 @@ import { InternalCompilerError } from "../errors.js"; +import { keyedHelperName } from "./lowering/stable-names.js"; import { mockCallRecordType, mockContextType } from "./mock-shapes.js"; import * as ts from "./ts7/adapter.js"; import { bodyReadsArguments } from "./arguments-usage.js"; @@ -252,6 +253,8 @@ function comparePriority(left: DeclaredOrderPriority, right: DeclaredOrderPriori export class ShapeRegistry { /** Existing layouts change only when a recursive placeholder closes. */ revision = 0; + /** Each shape id with a second hash of its key (keyedHelperName). */ + readonly keyedNameKeys = new Map(); private readonly byKey = new Map(); private readonly byId = new Map(); /** Historical emit-order rank of the declaration-order metadata each @@ -270,7 +273,7 @@ export class ShapeRegistry { * writer after all priority candidates have closed. */ private readonly derivedDeclaredOrderFinalizers: (() => void)[] = []; private currentDeclaredOrderPriority: DeclaredOrderPriority = [0, 0]; - /** All interned shapes in first-seen (`r0`, `r1`, ...) order. */ + /** All interned shapes in first-seen order. */ readonly shapes: IrRecordShape[] = []; /** ts.Types currently being mapped — a BACK-REFERENCE to one of these is * the recursive knot (`interface TreeNode { children: TreeNode[] }`): @@ -391,10 +394,12 @@ export class ShapeRegistry { /** The shape id a back-reference to an in-progress type resolves to: * reuses the type's persistent recursive id or mints a PLACEHOLDER * entry (empty fields) the outer frame finalizes. */ - recursiveRef(t: ts.Type): string { + recursiveRef(t: ts.Type, description: string): string { let id = this.recIds.get(t); if (id === undefined) { - id = `r${this.shapes.length}`; + id = keyedHelperName(this, "r", `recursive:${description}`, ""); + if (this.byId.has(id)) + throw new InternalCompilerError(`two recursive types share the description ${description}`); const shape: IrRecordShape = { id, fields: [] }; this.byId.set(id, shape); this.shapes.push(shape); @@ -463,7 +468,7 @@ export class ShapeRegistry { const key = this.keyOf(fields, tuple, indexValue); let id = this.byKey.get(key); if (id === undefined) { - id = `r${this.shapes.length}`; + id = keyedHelperName(this, "r", key, ""); const shape: IrRecordShape = { id, fields, @@ -541,6 +546,17 @@ export class ShapeRegistry { } } +/** A recursive checker type's printed form and first declaration, which + * name its placeholder whichever frame first reaches it. */ +function recursiveTypeName(t: ts.Type, ctx: Pick): string { + const symbol = t.getAliasSymbol() ?? t.getSymbol(); + const declaration = symbol?.declarations[0]; + const where = declaration + ? `${ts.getSourceFileOfNode(declaration).fileName}:${declaration.pos}` + : ""; + return `${ctx.checker.typeToString(t)}@${where}`; +} + /** The frontend's union interner — mirrors ShapeRegistry. A union's * canonical identity includes its typeKey-sorted arms and literal * discriminator contract. An arm's index in that list IS its runtime tag. @@ -548,10 +564,12 @@ export class ShapeRegistry { export class UnionRegistry { /** Existing arm/discriminator contracts change when a placeholder closes. */ revision = 0; + /** Each union id with a second hash of its key (keyedHelperName). */ + readonly keyedNameKeys = new Map(); private readonly byKey = new Map(); private readonly byId = new Map(); private readonly armTags = new Map>(); - /** All interned unions in first-seen (`u0`, `u1`, ...) order. */ + /** All interned unions in first-seen order. */ readonly unions: IrUnionDef[] = []; /** ts.Types currently being mapped — a back-reference to one is the * recursive knot passing through a union (`type Tree = Leaf | Branch` @@ -568,10 +586,12 @@ export class UnionRegistry { /** The union id a back-reference to an in-progress union resolves to: * reuses the type's persistent recursive id or mints a PLACEHOLDER * entry (empty arms) the outer frame finalizes. */ - recursiveRef(t: ts.Type): string { + recursiveRef(t: ts.Type, description: string): string { let id = this.recIds.get(t); if (id === undefined) { - id = `u${this.unions.length}`; + id = keyedHelperName(this, "u", `recursive:${description}`, ""); + if (this.byId.has(id)) + throw new InternalCompilerError(`two recursive types share the description ${description}`); const def: IrUnionDef = { id, arms: [] }; this.byId.set(id, def); this.unions.push(def); @@ -621,7 +641,7 @@ export class UnionRegistry { const key = JSON.stringify([arms.map(typeKey), discriminant]); let id = this.byKey.get(key); if (id === undefined) { - id = `u${this.unions.length}`; + id = keyedHelperName(this, "u", key, ""); const def: IrUnionDef = { id, arms, ...(discriminant ? { discriminant } : {}) }; this.byKey.set(key, id); this.byId.set(id, def); @@ -4651,7 +4671,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const knownRecursive = unions.recursiveUnionFor(widened); if (knownRecursive !== undefined) return { kind: "union", unionId: knownRecursive }; if (unions.inProgress.has(widened)) { - return { kind: "union", unionId: unions.recursiveRef(widened) }; + return { + kind: "union", + unionId: unions.recursiveRef(widened, recursiveTypeName(widened, ctx)), + }; } unions.inProgress.add(widened); const sensitivityAtEntry = contextResolutions; @@ -6995,7 +7018,10 @@ function mapRecordType(widened: ts.Type, ctx: TypeMapperCtx): IrType | null { // frame completes (finalizeRecursive below). Backends represent records // as heap references, so the self-reference is an ordinary pointer. if (shapes.inProgress.has(widened)) { - return { kind: "record", shapeId: shapes.recursiveRef(widened) }; + return { + kind: "record", + shapeId: shapes.recursiveRef(widened, recursiveTypeName(widened, ctx)), + }; } shapes.inProgress.add(widened); const sensitivityAtEntry = contextResolutions; diff --git a/packages/compiler/test/array-pipeline-fusion.test.ts b/packages/compiler/test/array-pipeline-fusion.test.ts index f5d743265..55cd0025e 100644 --- a/packages/compiler/test/array-pipeline-fusion.test.ts +++ b/packages/compiler/test/array-pipeline-fusion.test.ts @@ -26,7 +26,7 @@ function helperCalls(fn: IrFunction): string[] { everyStmtList(fn.body, { expr: (e) => { if (e.kind === "call" && e.callee.startsWith("%arr.")) - calls.push(e.callee.replace(/\.\d+$/, "")); + calls.push(e.callee.replace(/\.[0-9a-f]+$/, "")); return true; }, stmt: () => true, diff --git a/packages/compiler/test/format-float-arguments.test.ts b/packages/compiler/test/format-float-arguments.test.ts index e59e023ca..6ce9c4bc0 100644 --- a/packages/compiler/test/format-float-arguments.test.ts +++ b/packages/compiler/test/format-float-arguments.test.ts @@ -27,7 +27,7 @@ test("util.format %f lowers a boolean argument once", async () => { const helpers = mod.functions .map((f) => f.name) .filter((name) => name.startsWith("%arr.")) - .map((name) => name.replace(/\.\d+$/, "")) + .map((name) => name.replace(/\.[0-9a-f]+$/, "")) .sort(); expect(helpers).toEqual(["%arr.every", "%arr.some"]); } finally { diff --git a/packages/compiler/test/in-runtime-key-emission.test.ts b/packages/compiler/test/in-runtime-key-emission.test.ts index aebbb2ea4..589685a7a 100644 --- a/packages/compiler/test/in-runtime-key-emission.test.ts +++ b/packages/compiler/test/in-runtime-key-emission.test.ts @@ -55,7 +55,7 @@ const d: Record = {}; d["a"] = 1; console.log(has(d, "a"), has(d, "b")); `); - const helper = body(llvm, /sc_b?f__x25_rec_haskey_\d+/); + const helper = body(llvm, /sc_b?f__x25_rec_haskey_[0-9a-f]+/); expect(helper).toContain("@scr_map_has_str("); // Not a scan over a fresh key snapshot per probe. expect(helper).not.toContain("@scr_map_keys_js_order("); diff --git a/packages/compiler/test/type-mapper-unions.test.ts b/packages/compiler/test/type-mapper-unions.test.ts index c714df4d7..684960002 100644 --- a/packages/compiler/test/type-mapper-unions.test.ts +++ b/packages/compiler/test/type-mapper-unions.test.ts @@ -139,7 +139,8 @@ describe("checker and synthesized optional union identity", () => { nullable.arms.filter((arm) => arm.kind !== "nullT" && arm.kind !== "undefinedT"), ), ).toBe(required.unionId); - expect(nullable.discriminant!.cases.map((entry) => entry.values)).toEqual([ + // Cases follow the arms, which are ordered by their type keys. + expect(nullable.discriminant!.cases.map((entry) => entry.values).sort()).toEqual([ ["a", "alias"], ["b"], ]); From 93e13f49a0d0e44896769ec157bd24b153b73492 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 01:06:31 -0500 Subject: [PATCH 2/2] Keep structural names apart by declaration kind and name recursive types by position Structural positions named an enclosing declaration by its name and its ordinal among the declarations of the same kind and name, without the kind. A getter and the setter of the same property, or a class and the namespace merged with it, therefore had the same path, and local classes declared in both shared one class. The path now includes the kind, and interfaces and type aliases anchor the positions inside them. Recursive record and union placeholders were named by their printed type and the byte offset of their declaration. A comment or blank line above the type renamed them, and two instantiations that print alike (type arguments that are type parameters or classes of one name from different declarations) threw an InternalCompilerError. Placeholders are now named by the printed type and the structural position of the declaration, and the same for up to three levels of type arguments. A description that two types still share gets a numbered suffix for the later one. --- .../src/frontend/lowering/lowerer.test.ts | 16 +-- .../compiler/src/frontend/lowering/lowerer.ts | 1 + .../frontend/lowering/structural-positions.ts | 16 ++- .../compiler/src/frontend/type-mapper.test.ts | 2 +- packages/compiler/src/frontend/type-mapper.ts | 78 +++++++++--- packages/compiler/test/stable-names.test.ts | 112 ++++++++++++++++++ 6 files changed, 192 insertions(+), 33 deletions(-) create mode 100644 packages/compiler/test/stable-names.test.ts diff --git a/packages/compiler/src/frontend/lowering/lowerer.test.ts b/packages/compiler/src/frontend/lowering/lowerer.test.ts index 750c27694..7fbb9661f 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.test.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.test.ts @@ -33,7 +33,7 @@ test("reuses complete copy routes without repeating discriminator planning", () expect(plan).not.toHaveBeenCalled(); expect(lowerer.liftedFns.filter((fn) => fn.name === helper)).toHaveLength(1); const recursive = {} as Type; - lowerer.shapes.recursiveRef(recursive, "Recursive"); + lowerer.shapes.recursiveRef(recursive, () => "Recursive"); lowerer.shapes.finalizeRecursive(recursive, [{ name: "kind", type: STRING }]); expect(lowerer.unionRetagHelper(from, to, loc)).toBe(helper); expect(plan).toHaveBeenCalled(); @@ -55,7 +55,7 @@ test("replans after a recursive union gains its final arms", () => { const lowerer = context(); const recursive = {} as Type; const from = lowerer.unions.intern([UNDEFINED_T, NULL_T]); - const to = lowerer.unions.recursiveRef(recursive, "Recursive"); + const to = lowerer.unions.recursiveRef(recursive, () => "Recursive"); const trapped = lowerer.unionRetagHelper(from, to, loc); expect(trapped).not.toBeNull(); lowerer.unions.finalizeRecursive(recursive, [UNDEFINED_T]); @@ -76,7 +76,7 @@ test("rechecks width conversions after recursive record definitions settle", () const lowerer = context(); const recursive = {} as Type; const source = record(lowerer.shapes.intern([{ name: "value", type: F64 }])); - const target = record(lowerer.shapes.recursiveRef(recursive, "Recursive")); + const target = record(lowerer.shapes.recursiveRef(recursive, () => "Recursive")); const from = lowerer.unions.intern([source, UNDEFINED_T]); const to = lowerer.unions.intern([target, NULL_T]); expect(lowerer.unionRetagHelper(from, to, loc)).not.toBeNull(); @@ -130,7 +130,7 @@ test("literal ownership is reused across values and rechecked when recursive rec expect(inspect).not.toHaveBeenCalled(); const recursive = {} as Type; - const pendingShape = record(lowerer.shapes.recursiveRef(recursive, "Recursive")); + const pendingShape = record(lowerer.shapes.recursiveRef(recursive, () => "Recursive")); const pending = lowerer.unions.intern([pendingShape], { field: "kind", cases: [{ tag: 0, values: ["a"] }], @@ -144,7 +144,7 @@ test("literal ownership is reused across values and rechecked when recursive rec test("literal ownership sees a recursive union's finalized discriminator", () => { const lowerer = context(); const recursive = {} as Type; - const union = lowerer.unions.recursiveRef(recursive, "Recursive"); + const union = lowerer.unions.recursiveRef(recursive, () => "Recursive"); const shape = record(lowerer.shapes.intern([{ name: "kind", type: STRING }])); expect(lowerer.literalUnionArm(union, ["ready"])).toBeNull(); lowerer.unions.finalizeRecursive(recursive, [shape], { @@ -185,7 +185,7 @@ test("removing undefined reuses the discriminator transformation without hiding const recursive = {} as Type; const pending: IrType = { kind: "union", - unionId: lowerer.unions.recursiveRef(recursive, "Recursive"), + unionId: lowerer.unions.recursiveRef(recursive, () => "Recursive"), }; expect(lowerer.stripUndefinedArm(pending)).toEqual(pending); lowerer.unions.finalizeRecursive(recursive, [STRING, UNDEFINED_T]); @@ -221,7 +221,7 @@ test("adding undefined reuses semantic union transformations and sees recursive const recursive = {} as Type; const pending: IrType = { kind: "union", - unionId: lowerer.unions.recursiveRef(recursive, "Recursive"), + unionId: lowerer.unions.recursiveRef(recursive, () => "Recursive"), }; const before = lowerer.withUndefinedArmOf(pending); lowerer.unions.finalizeRecursive(recursive, [a, b], discriminant); @@ -236,7 +236,7 @@ test("optional union widening keeps refused and missing contracts current", () = const recursive = {} as Type; const pending: IrType = { kind: "union", - unionId: lowerer.unions.recursiveRef(recursive, "Recursive"), + unionId: lowerer.unions.recursiveRef(recursive, () => "Recursive"), }; expect(lowerer.withUndefinedArmOf(pending)).not.toBeNull(); lowerer.unions.finalizeRecursive(recursive, [{ kind: "date" }, STRING]); diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 687a2fb1a..3f8a0a66e 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -3568,6 +3568,7 @@ export class Lowerer { this.checker = program.getTypeChecker(); this.typeCtx = { checker: this.checker, + positionOf: (node) => this.positions.of(node), shapes: this.shapes, unions: this.unions, classNamer: this.classNamer, diff --git a/packages/compiler/src/frontend/lowering/structural-positions.ts b/packages/compiler/src/frontend/lowering/structural-positions.ts index cdc290b4b..ea8535d9e 100644 --- a/packages/compiler/src/frontend/lowering/structural-positions.ts +++ b/packages/compiler/src/frontend/lowering/structural-positions.ts @@ -15,6 +15,8 @@ const ANCHOR_KINDS: ReadonlySet = new Set([ ts.SyntaxKind.ModuleDeclaration, ts.SyntaxKind.EnumDeclaration, ts.SyntaxKind.Constructor, + ts.SyntaxKind.InterfaceDeclaration, + ts.SyntaxKind.TypeAliasDeclaration, ]); /** The name of `node` when it is a named declaration that anchors the @@ -34,10 +36,10 @@ function anchorOf(node: ts.Node): ts.Node { /** Source positions that stay the same when code outside the enclosing * declaration changes. A node's structural position is its file, the chain - * of enclosing named declarations (each with its ordinal among the - * declarations of the same kind and name in its own anchor), and its - * ordinal among the nodes of its kind whose nearest anchor is the same - * declaration. Editing another declaration, or code nested in another + * of enclosing named declarations (each with its kind, its name and its + * ordinal among the declarations of the same kind and name in its own + * anchor), and its ordinal among the nodes of its kind whose nearest anchor + * is the same declaration. Editing another declaration, or code nested in another * named declaration of the same anchor, leaves the position unchanged. */ export class StructuralPositions { /** Per anchor and key: the indices of the nodes with that key whose @@ -75,8 +77,10 @@ export class StructuralPositions { if (ts.isSourceFile(anchor)) path = anchor.fileName; else { const parent = anchorOf(anchor); - const name = anchorName(anchor); - path = `${this.pathOf(parent)}/${name}#${this.ordinalIn(parent, anchor, `${anchor.kind}:${name}`)}`; + // The kind keeps declarations of one name apart: a getter and its + // setter, or a class and the namespace merged with it. + const key = `${anchor.kind}:${anchorName(anchor)}`; + path = `${this.pathOf(parent)}/${key}#${this.ordinalIn(parent, anchor, key)}`; } this.paths.set(anchor, path); return path; diff --git a/packages/compiler/src/frontend/type-mapper.test.ts b/packages/compiler/src/frontend/type-mapper.test.ts index a99eb44cc..163ba7b9e 100644 --- a/packages/compiler/src/frontend/type-mapper.test.ts +++ b/packages/compiler/src/frontend/type-mapper.test.ts @@ -41,7 +41,7 @@ describe("union arm lookup", () => { test("sees finalized recursive arms after a missing lookup", () => { const unions = new UnionRegistry(); const recursive = {} as Type; - const union = unions.recursiveRef(recursive, "Recursive"); + const union = unions.recursiveRef(recursive, () => "Recursive"); expect(unions.armTag(union, STRING)).toBe(-1); unions.finalizeRecursive(recursive, [ F64, diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index ce73e8bc2..79091cadc 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -1,5 +1,5 @@ import { InternalCompilerError } from "../errors.js"; -import { keyedHelperName } from "./lowering/stable-names.js"; +import { keyedHelperName, type KeyedNameOwner } from "./lowering/stable-names.js"; import { mockCallRecordType, mockContextType } from "./mock-shapes.js"; import * as ts from "./ts7/adapter.js"; import { bodyReadsArguments } from "./arguments-usage.js"; @@ -393,13 +393,12 @@ export class ShapeRegistry { /** The shape id a back-reference to an in-progress type resolves to: * reuses the type's persistent recursive id or mints a PLACEHOLDER - * entry (empty fields) the outer frame finalizes. */ - recursiveRef(t: ts.Type, description: string): string { + * entry (empty fields) the outer frame finalizes. `describe` names a new + * placeholder (recursiveTypeName). */ + recursiveRef(t: ts.Type, describe: () => string): string { let id = this.recIds.get(t); if (id === undefined) { - id = keyedHelperName(this, "r", `recursive:${description}`, ""); - if (this.byId.has(id)) - throw new InternalCompilerError(`two recursive types share the description ${description}`); + id = recursivePlaceholderName(this, "r", describe(), (name) => this.byId.has(name)); const shape: IrRecordShape = { id, fields: [] }; this.byId.set(id, shape); this.shapes.push(shape); @@ -546,15 +545,55 @@ export class ShapeRegistry { } } -/** A recursive checker type's printed form and first declaration, which - * name its placeholder whichever frame first reaches it. */ -function recursiveTypeName(t: ts.Type, ctx: Pick): string { +/** The id of a recursive placeholder described by `description`. Two + * different types can still share a description (recursiveTypeName stops + * after a few levels of type arguments); the later one takes the first free + * name with a numbered suffix. */ +function recursivePlaceholderName( + owner: KeyedNameOwner, + prefix: string, + description: string, + taken: (name: string) => boolean, +): string { + let name = keyedHelperName(owner, prefix, `recursive:${description}`, ""); + for (let n = 1; taken(name); n++) + name = keyedHelperName(owner, prefix, `recursive:${description}#${n}`, ""); + return name; +} + +/** How many levels of type arguments recursiveTypeName describes. */ +const RECURSIVE_NAME_DEPTH = 3; + +/** A recursive checker type's description, which names its placeholder + * whichever frame first reaches it: its printed form, the structural + * position of its first declaration, and the same for its type arguments + * (two instantiations print the same when their type arguments are type + * parameters or classes of the same name from different declarations). */ +function recursiveTypeName( + t: ts.Type, + ctx: Pick, + depth = 0, +): string { const symbol = t.getAliasSymbol() ?? t.getSymbol(); const declaration = symbol?.declarations[0]; const where = declaration - ? `${ts.getSourceFileOfNode(declaration).fileName}:${declaration.pos}` + ? (ctx.positionOf?.(declaration) ?? + `${ts.getSourceFileOfNode(declaration).fileName}:${declaration.pos}`) : ""; - return `${ctx.checker.typeToString(t)}@${where}`; + let description = `${ctx.checker.typeToString(t)}@${where}`; + if (depth < RECURSIVE_NAME_DEPTH) { + const aliasArguments = + t.getAliasSymbol() !== undefined ? (t.getAliasTypeArguments() ?? []) : []; + const typeArguments = + aliasArguments.length > 0 + ? aliasArguments + : t.isTypeReference() + ? ctx.checker.getTypeArguments(t) + : []; + if (typeArguments.length > 0) + description += `<${typeArguments.map((arg) => recursiveTypeName(arg, ctx, depth + 1)).join(",")}>`; + } + return description; } /** The frontend's union interner — mirrors ShapeRegistry. A union's @@ -585,13 +624,12 @@ export class UnionRegistry { /** The union id a back-reference to an in-progress union resolves to: * reuses the type's persistent recursive id or mints a PLACEHOLDER - * entry (empty arms) the outer frame finalizes. */ - recursiveRef(t: ts.Type, description: string): string { + * entry (empty arms) the outer frame finalizes. `describe` names a new + * placeholder (recursiveTypeName). */ + recursiveRef(t: ts.Type, describe: () => string): string { let id = this.recIds.get(t); if (id === undefined) { - id = keyedHelperName(this, "u", `recursive:${description}`, ""); - if (this.byId.has(id)) - throw new InternalCompilerError(`two recursive types share the description ${description}`); + id = recursivePlaceholderName(this, "u", describe(), (name) => this.byId.has(name)); const def: IrUnionDef = { id, arms: [] }; this.byId.set(id, def); this.unions.push(def); @@ -1135,6 +1173,10 @@ export interface TypeMapperCtx { * empty object type rule in the object mapping). */ emptyLiteralArm?: boolean; checker: ts.TypeChecker; + /** The structural position of a declaration (structural-positions.ts), + * which names recursive placeholders independently of where unrelated + * code sits. The Lowerer supplies it. */ + positionOf?: (node: ts.Node) => string; /** JavaScript declarations whose instance or object-literal types only * the checked-dynamic representation holds (js-dynamic-objects.ts). * Absent when the program has none: every mapping is unchanged. */ @@ -4673,7 +4715,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { if (unions.inProgress.has(widened)) { return { kind: "union", - unionId: unions.recursiveRef(widened, recursiveTypeName(widened, ctx)), + unionId: unions.recursiveRef(widened, () => recursiveTypeName(widened, ctx)), }; } unions.inProgress.add(widened); @@ -7020,7 +7062,7 @@ function mapRecordType(widened: ts.Type, ctx: TypeMapperCtx): IrType | null { if (shapes.inProgress.has(widened)) { return { kind: "record", - shapeId: shapes.recursiveRef(widened, recursiveTypeName(widened, ctx)), + shapeId: shapes.recursiveRef(widened, () => recursiveTypeName(widened, ctx)), }; } shapes.inProgress.add(widened); diff --git a/packages/compiler/test/stable-names.test.ts b/packages/compiler/test/stable-names.test.ts new file mode 100644 index 000000000..720472556 --- /dev/null +++ b/packages/compiler/test/stable-names.test.ts @@ -0,0 +1,112 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule, type IrModule } from "../src/index.js"; + +/** Lowers each source in turn as the same file, so names that include the + * file's path compare equal. */ +async function lower(...sources: string[]): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-stable-names-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ir.json"); + const modules: IrModule[] = []; + for (const source of sources) { + await writeFile(entry, source); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + modules.push(deserializeModule(await readFile(outPath, "utf8"))); + } + return modules; + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +function names(mod: IrModule): string[] { + return [ + ...mod.records.map((record) => record.id), + ...mod.unions.map((union) => union.id), + ...mod.functions.map((fn) => fn.name), + ...mod.classes.map((cls) => cls.name), + ].sort(); +} + +const recursive = ` +type Tree = { value: number; children: Tree[]; parent: Tree | null }; +function total(tree: Tree): number { + let sum = tree.value; + for (const child of tree.children) sum += total(child); + return sum; +} +const root: Tree = { value: 1, children: [], parent: null }; +root.children.push({ value: 2, children: [], parent: root }); +console.log(total(root)); +`; + +test("recursive record and union ids do not depend on the code above the type", async () => { + const [before, after] = await lower( + recursive, + `// A comment above the type.\n\ntype Unrelated = { name: string };\n\n${recursive}`, + ); + expect(before.records.some((record) => record.fields.some((f) => f.name === "children"))).toBe( + true, + ); + expect(names(after)).toEqual(names(before)); +}); + +test("recursive instantiations that print alike get distinct placeholders", async () => { + const [mod] = await lower(` +namespace A { + export class Item { + a = 1; + } +} +namespace B { + export class Item { + b = "b"; + } +} +type Chain = { item: T; next: Chain | null }; +const first: Chain = { item: new A.Item(), next: null }; +const second: Chain = { item: new B.Item(), next: null }; +first.next = { item: new A.Item(), next: null }; +second.next = { item: new B.Item(), next: null }; +console.log(first.next.item.a, second.next.item.b); +`); + const chains = mod.records.filter((record) => record.fields.some((f) => f.name === "next")); + expect(new Set(chains.map((record) => record.id)).size).toBe(chains.length); + expect(chains.length).toBeGreaterThanOrEqual(2); +}); + +test("a getter and a setter of one name keep their local classes apart", async () => { + const [mod] = await lower(` +class Box { + private v = 0; + get value(): number { + class Local { + n(): number { + return 1; + } + } + return new Local().n() + this.v; + } + set value(x: number) { + class Local { + n(): number { + return 2; + } + } + this.v = x * new Local().n(); + } +} +const box = new Box(); +box.value = 5; +console.log(box.value); +`); + const locals = mod.classes.filter((cls) => cls.name.includes("Local")); + expect(locals).toHaveLength(2); + expect(locals[0]!.name).not.toBe(locals[1]!.name); +});