refactor(codegen): hoist symbol mangler to blaise.codegen (F5)

QBEMangle (TCodeGenQBE method) and NativeMangle (native free function)
applied the same metacharacter mapping: '<' ',' ' ' -> '_', drop '>', and
'$' '@' '^' -> '_D_' '_V_' '_P_'. Two copies of the symbol-naming rule is
a link-time-breakage risk if one side gains a case the other lacks.

Hoist a single CodegenMangle into blaise.codegen (distinct name, per the
F4 method-binding gotcha); both backends delegate. The shared version
keeps QBEMangle's clean-name fast-path (no per-character concat when no
metacharacter is present) and reads bytes via StrAt — so NativeMangle now
also uses the stage-stable StrAt instead of the Ord(S[i]) idiom the house
rules warn against.

Cross-backend dedup, no behaviour change: native .s AND QBE IR both
byte-identical on a generics + overload program (TList<Integer>,
TList<string>, Show$i/Show$S → TList_Integer_*, Show_D_i/Show_D_S). All
four fixpoints — which mangle the entire compiler's generic/overload
symbol set — and the full suite on both test runners are unchanged.
This commit is contained in:
Graeme Geldenhuys 2026-06-27 12:05:27 +01:00
parent 1e0b7fc46a
commit 1c250fb13e
3 changed files with 53 additions and 55 deletions

View file

@ -1670,25 +1670,10 @@ end;
{ Mangle a name for use as an assembly label: replace characters that are { Mangle a name for use as an assembly label: replace characters that are
invalid in label names with underscores, matching QBE backend's QBEMangle. } invalid in label names with underscores, matching QBE backend's QBEMangle. }
function NativeMangle(const AName: string): string; function NativeMangle(const AName: string): string;
var
I, C: Integer;
begin begin
Result := ''; { Delegates to the shared backend-neutral mangler in blaise.codegen
for I := 0 to Length(AName) - 1 do (formerly a per-character twin of the QBE backend's QBEMangle). }
begin Result := CodegenMangle(AName);
C := Ord(AName[I]);
case C of
60: Result := Result + '_'; { '<' }
62: ; { '>' — skip }
44: Result := Result + '_'; { ',' }
32: Result := Result + '_'; { ' ' — e.g. 'class of T' }
36: Result := Result + '_D_'; { '$' }
64: Result := Result + '_V_'; { '@' }
94: Result := Result + '_P_'; { '^' }
else
Result := Result + Chr(C);
end;
end;
end; end;
function TX86_64Backend.ClassSymName(const AClassName: string): string; function TX86_64Backend.ClassSymName(const AClassName: string): string;

View file

@ -27,7 +27,7 @@ unit blaise.codegen;
interface interface
uses uses
uAST, uSymbolTable, uDebugFacts, blaise.codegen.target; uAST, uSymbolTable, uDebugFacts, blaise.codegen.target, uStrCompat;
type type
TRecReturnClass = ( TRecReturnClass = (
@ -133,6 +133,13 @@ function RecretClassify(ARec: TRecordTypeDesc;
twins ExprOwnsRef / NativeExprOwnsRef). } twins ExprOwnsRef / NativeExprOwnsRef). }
function ArcExprOwnsRef(AExpr: TASTExpr): Boolean; function ArcExprOwnsRef(AExpr: TASTExpr): Boolean;
{ Mangle a Blaise symbol name into an assembler-legal identifier, shared by
both backends (formerly QBEMangle / NativeMangle). Replaces the generic/
overload metacharacters '<' ',' ' ' -> '_', drops '>', and maps the type-code
sigils '$' '@' '^' to '_D_' '_V_' '_P_'. A clean name (no metacharacter) is
returned unchanged via a fast pre-scan that avoids per-character concat. }
function CodegenMangle(const AName: string): string;
implementation implementation
function RecretManagedClean(ARec: TRecordTypeDesc): Boolean; function RecretManagedClean(ARec: TRecordTypeDesc): Boolean;
@ -357,4 +364,43 @@ begin
end; end;
end; end;
function CodegenMangle(const AName: string): string;
var
I, C: Integer;
Clean: Boolean;
begin
{ Fast path: the overwhelming majority of names contain none of the mangled
characters — return the input unchanged instead of rebuilding it with one
concat (and its ARC churn) per character. }
Clean := True;
for I := 0 to Length(AName) - 1 do
begin
C := StrAt(AName, I);
if (C = 60) or (C = 62) or (C = 44) or (C = 36) or (C = 64) or
(C = 94) or (C = 32) then
begin
Clean := False;
break;
end;
end;
if Clean then
Exit(AName);
Result := '';
for I := 0 to Length(AName) - 1 do
begin
C := StrAt(AName, I);
case C of
60: Result := Result + '_'; { '<' }
62: ; { '>' — skip }
44: Result := Result + '_'; { ',' }
32: Result := Result + '_'; { ' ' — e.g. 'class of T' }
36: Result := Result + '_D_'; { '$' — overload delimiter }
64: Result := Result + '_V_'; { '@' — var-param prefix }
94: Result := Result + '_P_'; { '^' — pointer prefix }
else
Result := Result + Chr(C);
end;
end;
end;
end. end.

View file

@ -13382,43 +13382,10 @@ begin
end; end;
function TCodeGenQBE.QBEMangle(const AName: string): string; function TCodeGenQBE.QBEMangle(const AName: string): string;
var
I: Integer;
C: Integer;
Clean: Boolean;
begin begin
{ Fast path: the overwhelming majority of names contain none of the { Delegates to the shared backend-neutral mangler in blaise.codegen
mangled characters — return the input unchanged instead of rebuilding (formerly a per-character twin of the native backend's NativeMangle). }
it with one concat (and its ARC churn) per character. } Result := CodegenMangle(AName);
Clean := True;
for I := 0 to Length(AName) - 1 do
begin
C := StrAt(AName, I);
if (C = 60) or (C = 62) or (C = 44) or (C = 36) or (C = 64) or
(C = 94) or (C = 32) then
begin
Clean := False;
break;
end;
end;
if Clean then
Exit(AName);
Result := '';
for I := 0 to Length(AName) - 1 do
begin
C := StrAt(AName, I);
case C of
60: Result := Result + '_'; { '<' }
62: ; { '>' — skip }
44: Result := Result + '_'; { ',' }
32: Result := Result + '_'; { ' ' — e.g. 'class of T' }
36: Result := Result + '_D_'; { '$' — overload delimiter }
64: Result := Result + '_V_'; { '@' — var-param prefix }
94: Result := Result + '_P_'; { '^' — pointer prefix }
else
Result := Result + Chr(C);
end;
end;
end; end;
function TCodeGenQBE.MethodEmitName(AMDecl: TMethodDecl; function TCodeGenQBE.MethodEmitName(AMDecl: TMethodDecl;