Align the QBE backend unit names with the dotted naming convention already used by the native backend (blaise.codegen.native.*). uCodeGen.pas → blaise.codegen.pas uCodeGenQBE.pas → blaise.codegen.qbe.pas Updated all uses clauses (~70 test files, main compiler units), documentation references (README + 5 docs/*.adoc files), and comment references in runtime/stdlib.
373 lines
11 KiB
Plaintext
373 lines
11 KiB
Plaintext
= 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<Integer>` 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<Integer>` 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 `<Class>_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<Integer>`. 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_`).
|