fix(codegen): native HasClassAttribute — emit the call and the attribute RTTI

HasClassAttribute(AClass, AAttrClass) was unreliable on the native backend:
- The call was never emitted.  The builtin had no native lowering, so it fell
  through to a generic path that left the first metaclass's typeinfo address in
  %rax and read its low byte as the Boolean result — a layout-dependent false
  positive (a plain class reported as having an attribute).
- Even with the call, the attribute table was missing: native typeinfo
  hardcoded slot 7 (attrs) to nil, so a [Threaded]-marked class reported False.

Fixes (blaise.codegen.native.x86_64.pas):
- Add a native lowering for the HasClassAttribute builtin (gated on
  FC.IsBuiltinHasClassAttr): evaluate both metaclass args to typeinfo pointers
  in %rdi/%rsi, call _HasClassAttribute, normalise %al to 0/1.  Mirrors the
  QBE backend.
- Emit the class attribute RTTI table (count + one typeinfo pointer per
  attribute) as attrs_<Class> and reference it from typeinfo slot 7, nil when
  the class has no attributes.  Mirrors the QBE backend's attrs_<Class>.

This is what blaise.testing's runner uses to decide [Threaded] subprocess
dispatch; the false positive forked non-threaded suites and crashed them.

e2e test in cp.test.e2e.gaps (AssertRTLRunsOnAll, both backends): a plain
TTestCase reports False, a [Threaded] one reports True.  Full suite OK (3739);
all four fixpoints pass on both build paths.
This commit is contained in:
Graeme Geldenhuys 2026-06-22 18:05:32 +01:00
parent 271763034b
commit 6d46af93de
4 changed files with 64 additions and 1 deletions

View file

@ -1925,6 +1925,7 @@ var
ParentStr: string;
ImplStr: string;
MethStr: string;
AttrsStr: string;
PubCount: Integer;
Line: string;
EmitSys: Boolean;
@ -2060,6 +2061,24 @@ begin
else
MethStr := '0';
{ Class attribute RTTI table at typeinfo slot 7: a count word followed by
one typeinfo pointer per attribute. Referenced by _HasClassAttribute.
Emitted before the typeinfo so the symbol is defined when slot 7
references it; nil when the class carries no attributes. Mirrors the
QBE backend's attrs_<Class>. }
if RT.ClassAttributeCount() > 0 then
begin
Self.Emit('.balign 8');
Self.Emit('attrs_' + CSym + ':');
Self.Emit(Format(#9'.quad %d', [RT.ClassAttributeCount()]));
for J := 0 to RT.ClassAttributeCount() - 1 do
Self.Emit(#9'.quad typeinfo_' +
Self.ClassSymName(RT.ClassAttributeAt(J)));
AttrsStr := 'attrs_' + CSym;
end
else
AttrsStr := '0';
Self.Emit('.balign 8');
Self.Emit('.globl typeinfo_' + CSym);
Self.Emit('typeinfo_' + CSym + ':');
@ -2070,7 +2089,7 @@ begin
Self.Emit(Format(#9'.quad %d', [RT.TotalSize()]));
Self.Emit(#9'.quad _FieldCleanup_' + CSym);
Self.Emit(#9'.quad vtable_' + CSym);
Self.Emit(#9'.quad 0'); { attrs }
Self.Emit(#9'.quad ' + AttrsStr); { attrs }
end;
{ Typeinfo blocks for generic class instances. }
@ -5332,6 +5351,24 @@ begin
{ Result = method code pointer in %rax. }
Exit;
end;
{ HasClassAttribute(AClass, AAttrClass): Boolean query attribute RTTI.
Both args are metaclass expressions that lower to typeinfo pointers.
Pass them in %rdi/%rsi and call the runtime helper (result in %al).
Without this the native backend never emitted the call the result
was the low byte of the first metaclass's typeinfo address, a layout-
dependent false positive. }
if FC.IsBuiltinHasClassAttr and (FC.Args.Count = 2) then
begin
Self.EmitExprToEax(TASTExpr(FC.Args.Items[0])); { ti_class -> %rax }
Self.Emit(#9'pushq %rax');
Self.EmitExprToEax(TASTExpr(FC.Args.Items[1])); { ti_attr -> %rax }
Self.Emit(#9'movq %rax, %rsi');
Self.Emit(#9'popq %rdi');
Self.Emit(#9'callq _HasClassAttribute');
{ Result Boolean in %al; normalise to a clean 0/1 in %rax. }
Self.Emit(#9'movzbq %al, %rax');
Exit;
end;
{ SizeOf(expr) → integer literal = byte size of the resolved type. }
if SameText(FC.Name, 'SizeOf') and (FC.Args.Count = 1) then
begin

View file

@ -58,6 +58,12 @@ type
{ Published RTTI + MethodAddress }
procedure TestRun_PublishedRTTI_MethodAddress;
{ HasClassAttribute attribute RTTI query: a plain class reports False, a
[Threaded]-marked class reports True. Regression: the native backend
emitted no call (result = low byte of the metaclass address, a false
positive) and emitted no attribute table in typeinfo. }
procedure TestRun_HasClassAttribute_PlainAndMarked;
{ Named-type alias array const (GitHub #113) }
procedure TestRun_NamedArrayAlias_IntConst;
@ -646,6 +652,26 @@ begin
AssertRunsOnAll(Src, 'found' + Chr(10), 0);
end;
procedure TE2EGapTests.TestRun_HasClassAttribute_PlainAndMarked;
const Src = '''
program T;
uses blaise.testing;
type
TPlain = class(TTestCase) published procedure M; end;
[Threaded]
TMarked = class(TTestCase) published procedure M; end;
procedure TPlain.M; begin end;
procedure TMarked.M; begin end;
begin
WriteLn(HasClassAttribute(TPlain, ThreadedAttribute));
WriteLn(HasClassAttribute(TMarked, ThreadedAttribute))
end.
''';
begin
{ Uses blaise.testing (stdlib), so the RTL+stdlib search-path helper. }
AssertRTLRunsOnAll(Src, 'False' + Chr(10) + 'True' + Chr(10), 0);
end;
{ ---- Multi-arg WriteLn ---- }
procedure TE2EGapTests.TestRun_StaticArrayReturn_12Bytes;