From 717ddfdfe336847d388c098fa8d6a3115d2ebf18 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 30 Oct 2026 07:40:27 -0500 Subject: [PATCH] Keep the names of functions boxed inside records and arrays A function value converted to unknown on its own takes its name from the conversion site, but one held in a record, an array, a map or another converted value had no name there, so util.inspect printed `[Function (anonymous)]` where Node prints `[Function: toString]`. The function box now looks a nameless function up in a per-module table of (entry point, name) pairs, built from the functions the module makes closures of: each declared or lifted function's sourceName, which follows NamedEvaluation, and for an object literal's method its key (IrFunction.displayName). An adapter is looked up through the function it adapts. displayName is set only in modules that box a value that can hold a function, and the table exists only in modules that box functions. --- packages/compiler/src/backend/llvm/dyn.ts | 104 +++++++++++++++++- packages/compiler/src/backend/llvm/emitter.ts | 30 +++++ .../compiler/src/backend/llvm/expr-dynamic.ts | 8 +- .../src/frontend/lowering/function-names.ts | 99 +++++++++++++++++ .../src/frontend/lowering/lower-functions.ts | 2 + .../compiler/src/frontend/lowering/lowerer.ts | 6 + packages/compiler/src/ir/codec.generated.ts | 46 ++++---- .../src/ir/function-references.test.ts | 1 + packages/compiler/src/ir/ir.ts | 4 + .../tests/corpus/boxed-function-names.ts.json | 6 + tests/corpus/boxed-function-names.ts | 30 +++++ .../corpus-costs/boxed-function-names.ts.json | 1 + 12 files changed, 310 insertions(+), 27 deletions(-) create mode 100644 packages/compiler/src/frontend/lowering/function-names.ts create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/boxed-function-names.ts.json create mode 100644 tests/corpus/boxed-function-names.ts create mode 100644 tests/harness/corpus-costs/boxed-function-names.ts.json diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts index e9dc81c87..51b9c8543 100644 --- a/packages/compiler/src/backend/llvm/dyn.ts +++ b/packages/compiler/src/backend/llvm/dyn.ts @@ -223,6 +223,9 @@ export interface DynHost extends WalkerHost { /** The closures with a jsval body (IrFunction.jsEntry) on targets with * jsval words, which the box builders give a jsval entry. */ jsEntryClosures?(): readonly JsEntryClosure[]; + /** The entry point and JavaScript name of each named function the module + * makes closures of (LlEmitter.closureNames). */ + closureNames?(): readonly { entry: string; name: string }[]; } /** A closure with a jsval body (IrFunction.jsEntry), for the box builders. */ @@ -309,6 +312,8 @@ export class LlDyn { return this.dynFuncBoxHelper(args[0] as IrType & { kind: "func" }); case "dyn.jsEntryThunk": return this.jsEntryThunk(args[0] as string, args[1] as IrType[], args[2] as IrType); + case "dyn.unnamedFuncBoxHelper": + return this.unnamedFuncBoxHelper(args[0] as IrType & { kind: "func" }) ?? ""; case "dyn.dynFuncAdapterHelper": return this.dynFuncAdapterHelper(args[0] as IrType & { kind: "func" }); case "dyn.dynToStrHelper": @@ -3561,7 +3566,7 @@ export class LlDyn { } case "func": { const r = B.tmp(); - B.line(`${r} = call ptr @${this.dynFuncBoxHelper(t)}(ptr %v, ptr null)`); + B.line(`${r} = ${this.unnamedFuncBoxCall(t, "%v")}`); B.terminate(`ret ptr ${r}`); break; } @@ -3972,7 +3977,7 @@ export class LlDyn { // function boundary (the dynFrom func special case, sans name). const p = emitUnionPeek(B, nullable, "%v"); const r = B.tmp(); - B.line(`${r} = call ptr @${this.dynFuncBoxHelper(arm)}(ptr ${p}, ptr null)`); + B.line(`${r} = ${this.unnamedFuncBoxCall(arm, p)}`); B.terminate(`ret ptr ${r}`); } else { const p = emitUnionPeek(B, nullable, "%v"); @@ -5359,9 +5364,8 @@ export class LlDyn { private toDynExpr(B: BlockBuilder, t: IrType, expr: string): string { if (t.kind === "dyn") return this.retainDyn(B, expr); if (t.kind === "func") { - const box = this.dynFuncBoxHelper(t); const r = B.tmp(); - B.line(`${r} = call ptr @${box}(ptr ${expr}, ptr null)`); + B.line(`${r} = ${this.unnamedFuncBoxCall(t, expr)}`); return r; } @@ -5651,6 +5655,98 @@ export class LlDyn { return name; } + private closureNames: { symbol: string; count: number } | null | undefined; + + /** The module's closure-name lookup (`sc_closure_name(closure)`, over a + * table of `{ ptr entry, ptr name }` pairs), defined on first use; null + * when no named function becomes a closure. */ + private closureNameTable(): { symbol: string; count: number } | null { + if (this.closureNames !== undefined) return this.closureNames; + const entries = this.host.closureNames?.() ?? []; + if (entries.length === 0) return (this.closureNames = null); + const host = this.host; + const S = host.sizeType; + const table = "sc_closure_names"; + const symbol = "sc_closure_name"; + // The closure's entry point (an adapter's root's, ScrClosure.identity) + // compared with each entry; null when none matches. + this.defs.push( + `@${table} = internal constant [${entries.length} x { ptr, ptr }] [${entries + .map((e) => `{ ptr, ptr } { ptr @${e.entry}, ptr ${host.cstr(e.name)} }`) + .join(", ")}]`, + ``, + `define internal ptr @${symbol}(ptr %v) ${FN_ATTRS} { ; the JavaScript name of a closure's function`, + `entry:`, + ` %idp = getelementptr inbounds %ScrClosure, ptr %v, i64 0, i32 6`, + ` %id = load ptr, ptr %idp`, + ` %own = icmp eq ptr %id, null`, + ` %root = select i1 %own, ptr %v, ptr %id`, + ` %fnp = getelementptr inbounds %ScrClosure, ptr %root, i64 0, i32 1`, + ` %fn = load ptr, ptr %fnp`, + ` br label %loop`, + `loop:`, + ` %i = phi ${S} [ 0, %entry ], [ %next, %step ]`, + ` %done = icmp eq ${S} %i, ${entries.length}`, + ` br i1 %done, label %none, label %test`, + `test:`, + ` %ep = getelementptr inbounds [${entries.length} x { ptr, ptr }], ptr @${table}, i64 0, ${S} %i, i32 0`, + ` %e = load ptr, ptr %ep`, + ` %hit = icmp eq ptr %e, %fn`, + ` br i1 %hit, label %found, label %step`, + `step:`, + ` %next = add ${S} %i, 1`, + ` br label %loop`, + `found:`, + ` %np = getelementptr inbounds [${entries.length} x { ptr, ptr }], ptr @${table}, i64 0, ${S} %i, i32 1`, + ` %name = load ptr, ptr %np`, + ` ret ptr %name`, + `none:`, + ` ret ptr null`, + `}`, + ``, + ); + return (this.closureNames = { symbol, count: entries.length }); + } + + private readonly unnamedFuncBoxes = new Map(); + + /** `sc_dfu_(ptr closure) -> ptr`: the box of a function value with no + * static name (one inside a record, array or other value being + * converted), named from the module's closure-name table. The plain box + * when no named function becomes a closure. */ + unnamedFuncBoxHelper(t: IrType & { kind: "func" }): string | null { + if ((this.host.closureNames?.() ?? []).length === 0) return null; + const box = this.dynFuncBoxHelper(t); + const key = typeKey(t); + const existing = this.unnamedFuncBoxes.get(key); + if (existing) return existing; + const name = `sc_dfu_${this.unnamedFuncBoxes.size}`; + this.unnamedFuncBoxes.set(key, name); + this.host.journalAllocated?.(name, "dyn.unnamedFuncBoxHelper", [t]); + // The first box defines the table and its lookup. + const table = this.closureNameTable()!; + this.defs.push( + `define internal ptr @${name}(ptr %v) ${FN_ATTRS} { ; box ${key} into dyn with its own name`, + `entry:`, + ` %n = call ptr @${table.symbol}(ptr %v)`, + ` %r = call ptr @${box}(ptr %v, ptr %n)`, + ` ret ptr %r`, + `}`, + ``, + ); + this.host.journalBuilt?.(); + return name; + } + + /** The call that boxes the function value `value` of type `t` without a + * static name (unnamedFuncBoxHelper). */ + unnamedFuncBoxCall(t: IrType & { kind: "func" }, value: string): string { + const named = this.unnamedFuncBoxHelper(t); + return named === null + ? `call ptr @${this.dynFuncBoxHelper(t)}(ptr ${value}, ptr null)` + : `call ptr @${named}(ptr ${value})`; + } + /** The box builder dynFrom emits for one closure signature. */ dynFuncBoxHelper(t: IrType & { kind: "func" }): string { const key = typeKey(t); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 8bee15a41..52ae3bf07 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -924,6 +924,7 @@ const POLL_EVERY_ITERATION = "%worker.poll.every"; /** Runtime library calls (readsLoopMetrics). */ const LIB_CALL_KINDS: readonly IrNodeKind[] = ["libCall"]; +const CLOSURE_KINDS: readonly IrNodeKind[] = ["closure"]; /** [A-Za-z0-9_] */ function isIdentifierByte(code: number): boolean { @@ -2465,6 +2466,7 @@ export class LlEmitter { ); }, pendingTestLines: (dest) => this.pendingTestLines(dest), + closureNames: () => this.closureNames(), virtualEntry: (implFn) => this.virtualEntry(implFn), programLifetime: this.programLifetime, regionAliasTag: (word) => this.memoryAliasAttachment(`region.${word}`), @@ -2808,6 +2810,33 @@ export class LlEmitter { return this.heapCensus === null ? out : [...out, this.heapCensus.definitions().join("\n")]; } + /** The entry point and JavaScript name of each named function the + * module makes closures of: a declared function's wrapper (its interned + * closure's entry) or a lifted function's call target, with its + * displayName, else its sourceName. Function boxes without a static name + * look the closure's entry up in this table. */ + private closureNameEntries: readonly { entry: string; name: string }[] | undefined; + closureNames(): readonly { entry: string; name: string }[] { + if (this.closureNameEntries !== undefined) return this.closureNameEntries; + const entries = new Map(); + const scan = this.nodeIndex.scan(CLOSURE_KINDS); + for (const fn of this.mod.functions) { + eachNodeOfKinds(fn, scan, (node) => { + if (node.kind !== "closure") return true; + const target = this.fnByName.get(node.fnName); + const name = + target?.displayName ?? + (target?.sourceName !== "" ? target?.sourceName : undefined); + if (target === undefined || name === undefined || name === "") return true; + const entry = + target.captures === undefined ? mangleWrapper(node.fnName) : this.callTarget(node.fnName); + if (!entries.has(entry)) entries.set(entry, name); + return true; + }); + } + return (this.closureNameEntries = [...entries].map(([entry, name]) => ({ entry, name }))); + } + /** The dispose-observer gates the emitted frees and record writers test * (alloc.ts): defined after every helper is emitted, because dynCheck * builders, JSON schemas and weak collection constructors are requested @@ -8598,6 +8627,7 @@ for (const op of [ "dyn.dynFuncThunkHelper", "dyn.dynFuncBoxHelper", "dyn.jsEntryThunk", + "dyn.unnamedFuncBoxHelper", "dyn.dynFuncAdapterHelper", "dyn.dynToStrHelper", "dyn.dynIterNHelper", diff --git a/packages/compiler/src/backend/llvm/expr-dynamic.ts b/packages/compiler/src/backend/llvm/expr-dynamic.ts index d977818f1..8f34b7d5e 100644 --- a/packages/compiler/src/backend/llvm/expr-dynamic.ts +++ b/packages/compiler/src/backend/llvm/expr-dynamic.ts @@ -120,10 +120,12 @@ export function emitDynamicExpr( // A closure boxes as the checked-dynamic tree's function kind: retained closure + // the per-signature call thunk + the interned signature key. The // best-effort name rides along (null when the lowering had none). - const name = e.fnName !== undefined && e.fnName !== "" ? host.cstr(e.fnName) : "null"; - const box = host.dyn.dynFuncBoxHelper(v.type); const t = B.tmp(); - B.line(`${t} = call ptr @${box}(ptr ${v.name}, ptr ${name})`); + if (e.fnName !== undefined && e.fnName !== "") { + const name = host.cstr(e.fnName); + const box = host.dyn.dynFuncBoxHelper(v.type); + B.line(`${t} = call ptr @${box}(ptr ${v.name}, ptr ${name})`); + } else B.line(`${t} = ${host.dyn.unnamedFuncBoxCall(v.type, v.name)}`); if (e.fnLength !== undefined) { host.declare(`declare void @scr_dyn_function_set_length(ptr, i32)`); B.line(`call void @scr_dyn_function_set_length(ptr ${t}, i32 ${e.fnLength})`); diff --git a/packages/compiler/src/frontend/lowering/function-names.ts b/packages/compiler/src/frontend/lowering/function-names.ts new file mode 100644 index 000000000..bbee161cc --- /dev/null +++ b/packages/compiler/src/frontend/lowering/function-names.ts @@ -0,0 +1,99 @@ +/* The JavaScript names of object literals' methods. + * + * A function boxed into a checked value inside a record, an array or + * another converted value has no static name at the box, so the backend + * takes it from the function (LlEmitter.closureNames): its sourceName, or + * for an object literal's method, whose stack-frame name differs, its + * displayName. displayName is set only in modules that box such a + * function, so other modules lower as before. */ +import * as ts from "../ts7/adapter.js"; +import type { IrFunction, IrType } from "../../ir/ir.js"; +import { typeKey } from "../../ir/ir.js"; +import { everyStmtList } from "../../ir/traverse.js"; +import type { Lowerer } from "./lowerer.js"; + +/** Notes the name of the lifted function `fnName` when `node` is a method + * of an object literal with a static key. */ +export function noteObjectMethodName(lowerer: Lowerer, node: ts.Node, fnName: string): void { + if (!ts.isMethodDeclaration(node) || !ts.isObjectLiteralExpression(node.parent)) return; + const name = node.name; + if (ts.isIdentifier(name) || ts.isStringLiteral(name)) + lowerer.objectMethodNames.set(fnName, name.text); + else if (ts.isNumericLiteral(name)) + lowerer.objectMethodNames.set(fnName, String(Number(name.text))); +} + +/** Gives the noted methods their displayName when the module boxes a + * function without a static name, or a value that can hold one. */ +export function finalizeFunctionNames(lowerer: Lowerer, functions: readonly IrFunction[]): void { + if (lowerer.objectMethodNames.size === 0) return; + const seen = new Map(); + let boxes = false; + for (const fn of functions) { + everyStmtList(fn.body, { + stmt: () => !boxes, + expr: (e) => { + // A function boxed on its own with a static name needs none. + if ( + e.kind === "dynFrom" && + (e.value.type.kind === "func" + ? e.fnName === undefined || e.fnName === "" + : reachesFunction(lowerer, e.value.type, seen)) + ) + boxes = true; + return !boxes; + }, + }); + if (boxes) break; + } + if (!boxes) return; + for (const fn of functions) { + const name = lowerer.objectMethodNames.get(fn.name); + if (name !== undefined) fn.displayName = name; + } +} + +/** Whether a value of `t` can hold a function value. */ +function reachesFunction(lowerer: Lowerer, t: IrType, seen: Map): boolean { + const key = typeKey(t); + const known = seen.get(key); + if (known !== undefined) return known; + seen.set(key, false); + let result = false; + switch (t.kind) { + case "func": + result = true; + break; + case "record": { + const shape = lowerer.shapes.get(t.shapeId); + result = + (shape?.fields.some((field) => reachesFunction(lowerer, field.type, seen)) ?? false) || + (shape?.indexValue !== undefined && reachesFunction(lowerer, shape.indexValue, seen)); + break; + } + case "array": + case "set": + result = reachesFunction(lowerer, t.elem, seen); + break; + case "map": + result = reachesFunction(lowerer, t.key, seen) || reachesFunction(lowerer, t.value, seen); + break; + case "union": + result = (lowerer.unions.get(t.unionId)?.arms ?? []).some((arm) => + reachesFunction(lowerer, arm, seen), + ); + break; + case "promise": + result = reachesFunction(lowerer, t.inner, seen); + break; + case "object": + result = [...(lowerer.classes.get(t.className)?.fields.values() ?? [])].some((field) => + reachesFunction(lowerer, field, seen), + ); + break; + default: + result = false; + } + seen.set(key, result); + return result; +} diff --git a/packages/compiler/src/frontend/lowering/lower-functions.ts b/packages/compiler/src/frontend/lowering/lower-functions.ts index 48cdfa9cd..26c002b87 100644 --- a/packages/compiler/src/frontend/lowering/lower-functions.ts +++ b/packages/compiler/src/frontend/lowering/lower-functions.ts @@ -40,6 +40,7 @@ import { import { generatorMeta, isThisParameter, type ParamShape } from "./call-signatures.js"; import { isStrictCode } from "./strict-mode.js"; import { lowerSloppyGlobalReceiver } from "./lower-global-value.js"; +import { noteObjectMethodName } from "./function-names.js"; /** Save the call-site receiver before defaults or body effects. Escaping * arrows capture this binding instead of reading a later call's receiver. */ @@ -550,6 +551,7 @@ export function lowerLambda( if (isAsync) lifted.async = true; if (fnCtx.generator) lifted.generator = fnCtx.generator; lowerer.liftedFns.push(lifted); + noteObjectMethodName(lowerer, node, fnName); return { kind: "closure", fnName, captures: ctx.captureSources, type: funcType, loc }; } catch (e) { // JS sources defer LAMBDA poisons like function declarations diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index a29f59853..6b154d9f8 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -249,6 +249,7 @@ import { import { DeferredModuleInitializers } from "./deferred-module-initializers.js"; import { finalizeClassMethodValues } from "./class-method-values.js"; import { finalizePrivateBrands } from "./private-brands.js"; +import { finalizeFunctionNames } from "./function-names.js"; import { type ClassSymbolKey, INSPECT_CUSTOM_MEMBER, tableKeyFieldType } from "./symbol-fields.js"; import type { HttpClientFnBinding } from "./lower-server.js"; export type { WidthLift } from "./width-lift.js"; @@ -3826,6 +3827,9 @@ export class Lowerer { >(); /** Classes used as ThreadSlots keys: their hierarchies carry a slot word. */ readonly threadSlotKeys = new Set(); + /** The JavaScript names of object literals' methods, by lifted function + * (function-names.ts). */ + readonly objectMethodNames = new Map(); readonly forkTargetIdByPath = new Map(); /** Namespace ModuleBlocks this program lowers, filled by splitFiles: * "flattened" — an instantiated namespace whose body joined the file's @@ -4447,6 +4451,7 @@ export class Lowerer { checkedTable("threadSlotNames", () => this.threadSlotNames.size), checkedTable("threadSlotFamilies", () => this.threadSlotFamilies.size), checkedTable("threadSlotKeys", () => this.threadSlotKeys.size), + checkedTable("objectMethodNames", () => this.objectMethodNames.size), checkedTable("forkTargetIdByPath", () => this.forkTargetIdByPath.size), checkedTable("nsBlocks", () => this.nsBlocks.size), checkedTable("initNameOf", () => this.initNameOf.size), @@ -9816,6 +9821,7 @@ export class Lowerer { // one is exact only if the declaration is held at a single signature. checkGenericIdentityPins(this); boxBuiltinValuesChecked(this, functions); + finalizeFunctionNames(this, functions); if (this.diags.length === 0) fuseArrayPipelines(this, functions); finalizeClassMethodValues(this, functions); finalizePrivateBrands(this, functions); diff --git a/packages/compiler/src/ir/codec.generated.ts b/packages/compiler/src/ir/codec.generated.ts index b3d15dfae..911cb5065 100644 --- a/packages/compiler/src/ir/codec.generated.ts +++ b/packages/compiler/src/ir/codec.generated.ts @@ -5,7 +5,7 @@ import type { IrReader, IrWriter } from "./codec.js"; /** A digest of the coders below: an encoding written with other coders is * refused instead of misread, also after a change that keeps IR_VERSION. */ -export const IR_CODEC_LAYOUT = "c8e9ba1a465defc2"; +export const IR_CODEC_LAYOUT = "23df825e49b75d14"; /** Writes `mod` field by field (codec.ts describes the bytes). */ export function encodeIrModule(w: IrWriter, mod: IrModule): void { @@ -25,7 +25,7 @@ export function decodeIrFunction(r: IrReader): IrFunction { } const KEYS0: readonly string[] = ["irVersion","sourceFile","functions","classes","globals","records","unions","entry","workers","declBoundary","inferredFactFiles","declOrigins","lifetimeRegions","foreignElementKeys","threadSlotKeys","freezableArrays","sloppyDynWrites","untypedIntegrity","commonJsEntry","nodeIsland","ffiImports","lib","contracts","int32ArrayCandidates","wasmModules","wasmUntranslated","wasmCustomSections"]; -const KEYS1: readonly string[] = ["name","sourceName","ownsPrototype","params","returnType","locals","captures","classCaptures","body","loc","jsEntry","tier","speculativeDispatch","fastRead","plainLoops","async","generator","asyncCacheGlobal","asyncCycleCacheGlobal"]; +const KEYS1: readonly string[] = ["name","sourceName","displayName","ownsPrototype","params","returnType","locals","captures","classCaptures","body","loc","jsEntry","tier","speculativeDispatch","fastRead","plainLoops","async","generator","asyncCacheGlobal","asyncCycleCacheGlobal"]; const KEYS2: readonly string[] = ["libnode","nodeVersion","modules"]; const KEYS3: readonly string[] = ["profileName","prefix","initSymbol","sinkRegisterSymbol","collectSymbol","resultResetSymbol","threadInstances","callbackRegisterSymbol","callbacks","exports","trapOverlays","identity"]; const KEYS4: readonly string[] = ["int32Fields","int32Params","int32Returns","int32Locals","int32Elements","noThrow","acyclic","immortalGlobals","programLifetime","inlineArrays"]; @@ -404,6 +404,11 @@ function encIrFunction(w: IrWriter, v: IrFunction): void { w.byte(1); w.str(v.sourceName); } + if (v.displayName === undefined) w.byte(0); + else { + w.byte(1); + w.str(v.displayName); + } if (v.ownsPrototype === undefined) w.byte(0); else { w.byte(1); @@ -473,24 +478,25 @@ function encIrFunction(w: IrWriter, v: IrFunction): void { function decIrFunction(r: IrReader): IrFunction { const f0 = r.str(); const f1 = r.tag(2, "IrFunction.sourceName") === 0 ? undefined : r.str(); - const f2 = r.tag(2, "IrFunction.ownsPrototype") === 0 ? undefined : (r.bool() as NonNullable); - const f3 = decT15(r); - const f4 = decIrType(r); - const f5 = decT17(r); - const f6 = r.tag(2, "IrFunction.captures") === 0 ? undefined : decT15(r); - const f7 = r.tag(2, "IrFunction.classCaptures") === 0 ? undefined : decT18(r); - const f8 = decT19(r); - const f9 = decSrcLoc(r); - const f10 = r.tag(2, "IrFunction.jsEntry") === 0 ? undefined : r.str(); - const f11 = r.tag(2, "IrFunction.tier") === 0 ? undefined : (r.str() as NonNullable); - const f12 = r.tag(2, "IrFunction.speculativeDispatch") === 0 ? undefined : r.bool(); - const f13 = r.tag(2, "IrFunction.fastRead") === 0 ? undefined : (r.str() as NonNullable); - const f14 = r.tag(2, "IrFunction.plainLoops") === 0 ? undefined : (r.bool() as NonNullable); - const f15 = r.tag(2, "IrFunction.async") === 0 ? undefined : (r.bool() as NonNullable); - const f16 = r.tag(2, "IrFunction.generator") === 0 ? undefined : decT21(r); - const f17 = r.tag(2, "IrFunction.asyncCacheGlobal") === 0 ? undefined : r.str(); - const f18 = r.tag(2, "IrFunction.asyncCycleCacheGlobal") === 0 ? undefined : r.str(); - const o: IrFunction = { name: f0, ...(f1 !== undefined ? { sourceName: f1 } : {}), ...(f2 !== undefined ? { ownsPrototype: f2 } : {}), params: f3, returnType: f4, locals: f5, ...(f6 !== undefined ? { captures: f6 } : {}), ...(f7 !== undefined ? { classCaptures: f7 } : {}), body: f8, loc: f9, ...(f10 !== undefined ? { jsEntry: f10 } : {}), ...(f11 !== undefined ? { tier: f11 } : {}), ...(f12 !== undefined ? { speculativeDispatch: f12 } : {}), ...(f13 !== undefined ? { fastRead: f13 } : {}), ...(f14 !== undefined ? { plainLoops: f14 } : {}), ...(f15 !== undefined ? { async: f15 } : {}), ...(f16 !== undefined ? { generator: f16 } : {}), ...(f17 !== undefined ? { asyncCacheGlobal: f17 } : {}), ...(f18 !== undefined ? { asyncCycleCacheGlobal: f18 } : {}) }; + const f2 = r.tag(2, "IrFunction.displayName") === 0 ? undefined : r.str(); + const f3 = r.tag(2, "IrFunction.ownsPrototype") === 0 ? undefined : (r.bool() as NonNullable); + const f4 = decT15(r); + const f5 = decIrType(r); + const f6 = decT17(r); + const f7 = r.tag(2, "IrFunction.captures") === 0 ? undefined : decT15(r); + const f8 = r.tag(2, "IrFunction.classCaptures") === 0 ? undefined : decT18(r); + const f9 = decT19(r); + const f10 = decSrcLoc(r); + const f11 = r.tag(2, "IrFunction.jsEntry") === 0 ? undefined : r.str(); + const f12 = r.tag(2, "IrFunction.tier") === 0 ? undefined : (r.str() as NonNullable); + const f13 = r.tag(2, "IrFunction.speculativeDispatch") === 0 ? undefined : r.bool(); + const f14 = r.tag(2, "IrFunction.fastRead") === 0 ? undefined : (r.str() as NonNullable); + const f15 = r.tag(2, "IrFunction.plainLoops") === 0 ? undefined : (r.bool() as NonNullable); + const f16 = r.tag(2, "IrFunction.async") === 0 ? undefined : (r.bool() as NonNullable); + const f17 = r.tag(2, "IrFunction.generator") === 0 ? undefined : decT21(r); + const f18 = r.tag(2, "IrFunction.asyncCacheGlobal") === 0 ? undefined : r.str(); + const f19 = r.tag(2, "IrFunction.asyncCycleCacheGlobal") === 0 ? undefined : r.str(); + const o: IrFunction = { name: f0, ...(f1 !== undefined ? { sourceName: f1 } : {}), ...(f2 !== undefined ? { displayName: f2 } : {}), ...(f3 !== undefined ? { ownsPrototype: f3 } : {}), params: f4, returnType: f5, locals: f6, ...(f7 !== undefined ? { captures: f7 } : {}), ...(f8 !== undefined ? { classCaptures: f8 } : {}), body: f9, loc: f10, ...(f11 !== undefined ? { jsEntry: f11 } : {}), ...(f12 !== undefined ? { tier: f12 } : {}), ...(f13 !== undefined ? { speculativeDispatch: f13 } : {}), ...(f14 !== undefined ? { fastRead: f14 } : {}), ...(f15 !== undefined ? { plainLoops: f15 } : {}), ...(f16 !== undefined ? { async: f16 } : {}), ...(f17 !== undefined ? { generator: f17 } : {}), ...(f18 !== undefined ? { asyncCacheGlobal: f18 } : {}), ...(f19 !== undefined ? { asyncCycleCacheGlobal: f19 } : {}) }; return o; } diff --git a/packages/compiler/src/ir/function-references.test.ts b/packages/compiler/src/ir/function-references.test.ts index b5f2cadae..b331f5ce4 100644 --- a/packages/compiler/src/ir/function-references.test.ts +++ b/packages/compiler/src/ir/function-references.test.ts @@ -135,6 +135,7 @@ const FUNCTION_FIELDS: Record>, Role> = { name: "other", sourceName: "other", jsEntry: "reference", + displayName: "other", asyncCacheGlobal: "other", asyncCycleCacheGlobal: "other", "params[].name": "other", diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index dfa266c80..637bd3a60 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -1966,6 +1966,10 @@ export interface IrFunction { name: string; /** Source function name for native error stack frames. */ sourceName?: string; + /** The function's JavaScript `name` when it differs from sourceName: an + * object literal's method. Present only in modules that box function + * values into checked values (function-names.ts). */ + displayName?: string; /** Ordinary authored function declarations/expressions own a prototype. */ ownsPrototype?: true; params: IrParam[]; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/boxed-function-names.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/boxed-function-names.ts.json new file mode 100644 index 000000000..dc0658285 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/boxed-function-names.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/boxed-function-names.ts" + ], + "diags": [] +} diff --git a/tests/corpus/boxed-function-names.ts b/tests/corpus/boxed-function-names.ts new file mode 100644 index 000000000..0375fac7b --- /dev/null +++ b/tests/corpus/boxed-function-names.ts @@ -0,0 +1,30 @@ +// Functions held in records, arrays and maps keep their JavaScript names +// when the value is converted to unknown: util.inspect prints them. +function declared() { + return 0; +} +const named = function inner() { + return 1; +}; +const arrow = () => 2; +const wide = { + a: 1, + toString() { + return "w"; + }, + h: () => 3, + g: named, + d: declared, + "quoted"() { + return 4; + }, +}; +const box: unknown = wide; +console.log(box); +const list: unknown = [declared, named, arrow]; +console.log(list); +const byName = new Map number>([["a", arrow]]); +const mapBox: unknown = byName; +console.log(mapBox); +const plain: unknown = { f: function () { return 5; } }; +console.log(plain); diff --git a/tests/harness/corpus-costs/boxed-function-names.ts.json b/tests/harness/corpus-costs/boxed-function-names.ts.json new file mode 100644 index 000000000..151bf487b --- /dev/null +++ b/tests/harness/corpus-costs/boxed-function-names.ts.json @@ -0,0 +1 @@ +{ "plain": 1946, "san": 3000 }