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 }