= Symbol Name Mangling :doctype: article :toc: left :toclevels: 3 :sectnums: :icons: font :source-highlighter: rouge == What and Why Blaise emits per-unit-prefixed symbol names so that two units exporting same-named entities can't collide at link time. Without prefixing, a user library defining `function Foo` and another library doing the same would produce duplicate `$Foo` definitions in their respective `.o` files, and the linker would either error or silently bind to the wrong one. With prefixing, those become `$UnitA_Foo` and `$UnitB_Foo` — distinct global symbols, each resolvable independently. The scheme is *unit-name-as-prefix*: the QBE symbol for entity `Bar` declared in unit `Foo.Sub` becomes `Foo_Sub_Bar`. Dots in unit names collapse to underscores. Case from the source is preserved (Pascal is case-insensitive at the language level, but linker symbols are case-sensitive and the mangler treats them literally). For most user code this prefixing is invisible — every reference site goes through the same mangler at codegen time, so producer and consumer agree. == When Mangling Applies Every QBE symbol emitted for a unit-scope entity is prefixed. The full list: [cols="1,2,2",options="header"] |=== | Entity | Bare form | Prefixed form (unit `Classes`) | Free routine | `$SplitIntoList` | `$Classes_SplitIntoList` | Class method | `$TStringList_Add` | `$Classes_TStringList_Add` | Class typeinfo | `$typeinfo_TStringList` | `$typeinfo_Classes_TStringList` | Class vtable | `$vtable_TStringList` | `$vtable_Classes_TStringList` | Class name data string | `$__cn_TStringList` | `$__cn_Classes_TStringList` | Field-cleanup function | `$_FieldCleanup_TStringList` | `$_FieldCleanup_Classes_TStringList` | Published methods table | `$methods_TStringList` | `$methods_Classes_TStringList` | Class attributes table | `$attrs_TStringList` | `$attrs_Classes_TStringList` | Interface impl list | `$impllist_TStringList` | `$impllist_Classes_TStringList` | Interface tab | `$itab_TStringList_IList` | `$itab_Classes_TStringList_IList` | Generic instance | `$typeinfo_TBox_Integer` | `$typeinfo_Classes_TBox_Integer` |=== Local symbols inside a function body — basic-block labels (`@start`, `@if_then_0`), QBE temporaries (`%_t12`), variable slots (`%_var_X`) — are not prefixed. They are scoped to the QBE function they appear in. == When Mangling Does *Not* Apply A small allowlist of units stay literal. Their exports keep bare names so the runtime's hand-written hooks and codegen-emitted hardcoded references continue to resolve. The allowlist lives in `uSemantic.pas` as `IsUnmangledUnit`: [source,pascal] ---- function IsUnmangledUnit(const AUnitName: string): Boolean; begin Result := True; if AUnitName = '' then Exit; if SameText(AUnitName, 'System') then Exit; if (Length(AUnitName) >= 4) and SameText(Copy(AUnitName, 0, 4), 'rtl.') then Exit; if (Length(AUnitName) >= 7) and SameText(Copy(AUnitName, 0, 7), 'blaise_') then Exit; Result := False; end; ---- Five categories accepted as "unmangled": . **The empty unit name** — program-scope code. Program-private routines and classes stay unprefixed because they aren't shared across compilation units. . **`System`** — the implicit unit every program uses. Carries `TObject`, `TCustomAttribute`, foundational routines. . **`rtl.*` family** — `rtl.platform`, `rtl.platform.posix`, etc. The OS-abstraction layer. . **`blaise_*` family** — `blaise_arc`, `blaise_exc`, `blaise_float`, `blaise_mem`, `blaise_str`, `blaise_thread`, `blaise_weak`. The runtime support library. . **Anything else** is mangled. == Why These Specific Allowlist Rules The runtime is hand-written and has *hardcoded* references to specific symbol names that originate in these units. Concretely: `blaise.codegen.qbe.pas` emits calls and references like: * `$_StartUp` (program init, in `System` / `rtl.*`) * `$_SetArgs` * `$_SysWriteStr` * `$_StringRelease` * `$_BlaiseGetMem` (in `blaise_mem`) * `$_ClassAlloc` * `$_FieldCleanup_TObject` (System) * `$TObject_Destroy` * `$TObject_Cleanup` * `$TObject_ToString` * `$TCustomAttribute_Create` * `$_AbstractMethodError` These names appear *literally* in codegen — there is no symbol-table lookup at the call site. If the runtime emitted them with a prefix (`$blaise_mem_BlaiseGetMem`, `$System_TObject_Destroy`), every codegen call site would also need to know about the prefix, and a `grep` for the bare names in the codebase would suddenly miss what it used to catch. Two paths to fix this: [loweralpha] . **Allowlist** — exempt the small set of units the runtime lives in from mangling. Bare names stay bare. *Chosen approach.* Minimal churn; runtime hand-roll continues to work as is. . **Route everything through the symbol table** — every `$_Foo` in codegen becomes `RtlSym('_Foo')`, which consults a registry mapping logical names to mangled names. Cleaner long-term; mechanical refactor of ~113 call sites in `blaise.codegen.qbe.pas`. Documented as a follow-up in `memory/project_unit_prefix_mangling.md`. == Worked Examples === User unit Source `Classes.pas`: [source,pascal] ---- unit Classes; interface type TStringList = class procedure Add(const S: string); function Get(I: Integer): string; end; function SplitIntoList(const S: string): TStringList; implementation ... bodies ... end. ---- Emitted symbols: ---- $Classes_TStringList_Add $Classes_TStringList_Get $Classes_SplitIntoList $typeinfo_Classes_TStringList $vtable_Classes_TStringList $__cn_Classes_TStringList $_FieldCleanup_Classes_TStringList ---- A consumer `program P` using `Classes`: [source,pascal] ---- program P; uses Classes; var L: TStringList; begin L := TStringList.Create; WriteLn(L.Get(0)) end. ---- Codegen produces a call to `$Classes_TStringList_Get`, allocates via `$_ClassAlloc` (literal — `System` unit), stores `$vtable_Classes_TStringList` into the new instance's vtable slot. All names match exactly. === Program-scope routine Source: [source,pascal] ---- program Test; function Add(A, B: Integer): Integer; begin Result := A + B end; begin WriteLn(Add(1, 2)) end. ---- Emitted: `$Add` (bare). Program scope is unmangled — `Test_Add` would add noise for a symbol that never leaves the program's own `.o`. === System TObject derived A user class extending `TObject`: [source,pascal] ---- unit MyUnit; interface type TMyClass = class procedure DoIt; end; ... etc. ---- Emitted: `$MyUnit_TMyClass_DoIt`, `$typeinfo_MyUnit_TMyClass`, etc. The class inherits `TObject_Destroy` (bare) via its vtable — the vtable entries are stored as `$TObject_Destroy` because `TObject` is in `System` (unmangled). === Generic instance Source unit `Collections.pas` instantiates `TBox` somewhere: ---- $typeinfo_Collections_TBox_Integer { class typeinfo } $vtable_Collections_TBox_Integer $_FieldCleanup_Collections_TBox_Integer $Collections_TBox_Integer_Get { method on the instance } ---- The angle brackets in `TBox` collapse to underscore-separated text via `QBEMangle`, then get the unit prefix on top. == Adding a Unit to the Allowlist Rare. Don't do it unless the unit ships with hand-rolled assembly, codegen-emitted references to its symbols, or some other strong reason for bare names. To add `mynew_rtl_unit` to the allowlist: . In `uSemantic.pas`, edit `IsUnmangledUnit`: + [source,pascal] ---- if SameText(AUnitName, 'mynew_rtl_unit') then Exit; { add this line } ---- . In `blaise.codegen.qbe.pas`'s `ClassUnitPrefix`, mirror the same check. . Bump `COMPILER_ID` in `uCompilerId.pas` — the change affects which units emit prefixed symbols, so previously-built `.bif`s on disk become semantically incompatible. . Rebuild RTL: `make -C runtime clean all install`. . Run the gate. If you add a unit AND the existing allowlist no longer fits a clean pattern (e.g. the new unit is `core_rtl_x` not `blaise_x`), prefer extending the family-name check (`Copy(AUnitName, 0, 5) = 'core_'`) over a long list of explicit equality checks. == Subtleties === Empty `OwningUnit` `TSymbol.OwningUnit` is empty for builtins (no unit owns them) and for program-scope symbols (`FProg <> nil` during `Analyse(AProg)`, so the analyser skips tagging). `IsUnmangledUnit('')` returns True, so empty flows through cleanly. === Class methods and OwningUnit `TMethodDecl.OwningUnit` is set during semantic analysis from `FCurrentUnitName`. Imported `.bif`-loaded methods get their `OwningUnit` set from the `.bif`'s unit name during `uSemanticImport.RegisterClass`. So a class method's prefix matches whether the class was just compiled from source or pulled from a prebuilt `.o`. === Property accessors Properties don't have their own symbols — they expand at call sites to calls into the read/write method. `blaise.codegen.qbe`'s property-access emission inserts the prefix via `ClassUnitPrefix(FldAccess.PropOwnerType)` so `MyList.Strings[0]` compiles to a call like `$Classes_TStringList_GetStrings`. === Inherited destructor calls The `_FieldCleanup` emission walks the parent chain looking for the nearest `HasDestroyMethod = True` class and calls `_Destroy`. When that class is `TObject` (the typical case), the call site emits `$TObject_Destroy` (bare) because `TObject` is in the System allowlist. When the destructor is on a user class, the call emits `$UserUnit_TUserClass_Destroy` — prefix routed via `ClassUnitPrefix`. === Imported class-method resolution When a consumer compiles a call to `TStringList.Get`, semantic does `FindMethodDecl('TStringList', 'Get')` and gets back a `TMethodDecl` whose `ResolvedQbeName` was deserialised from the imported `.bif`. That name is the *fully prefixed* form `Classes_TStringList_Get`. Codegen emits it as-is. The prefix never has to be reconstructed consumer-side. == Currently Out of Scope * **Unit-scope free variables** — `TUnitInterface.Vars` are exported but `var` definitions in the implementation section don't yet flow through the mangler at all sites. Today these aren't a collision source in practice; revisit if it becomes one. * **Generic-instance prefix consistency** — generic instances created in one unit and referenced by name in another may get different prefixes if both units instantiate the same `TBox`. Linker dedupes by name so the cost is duplicate vtables in the binary, not incorrect behaviour. Cleaner long-term: emit generic instances into weakly-linked common symbols. == History * `5eb34b0` (2026-05-24) — `IsUnmangledUnit` + `MangleUnitPrefix` helpers landed; no call sites. * `3dd6b5a` — free-routine `ResolvedQbeName` sites apply the prefix. Added `TAddrOfExpr.ResolvedFreeRoutine` for `@FuncName` codegen. * `e25a48e` — class methods + vtable slot ImplName sites; class context promoted to `ClassUnitPrefix` helper; imported class methods registered in `FMethodIndex` so consumer-side resolution works. * `18c7c72` — class data symbols (`typeinfo_`, `vtable_`, `__cn_`, `_FieldCleanup_`, `methods_`, `attrs_`, `impllist_`, `itab_`).