Five native-backend fixes found while bringing up the natively-compiled
TestRunner:
- Bare class-name expressions (IsMetaclassRef) now emit the typeinfo
address (leaq typeinfo_<T>(%rip)), matching the QBE backend's
'copy $typeinfo_<T>'. Previously they fell through to a global
variable load of an undefined symbol, breaking RegisterTest(TFoo).
Also: tyMetaClass and tySet accepted in the function param/return
type guards, and ForceDirectories added to the TProcCall handler.
- ClassCreate: the constructor symbol now goes through
MethodEmitNameNative (unit-prefixed mangling) instead of raw
OwnerTypeName_Create, and the _ClassCreate result is kept in %rbx
across the constructor call — constructors are plain procedures and
do not return Self in %rax.
- Class-name string blobs (__cn_*) now carry the bare class name as
content while keeping the unit-prefixed symbol name. Previously the
prefixed name was embedded, so ClassName/TestClassName returned
'unit_TClass' and the runner's --suite filters never matched.
- Const string params now get the callee-side entry-retain/exit-release
pair. _StringConcat returns an rc=0 transient; QBE pins it caller
side (EnsureConstStringRef), the native backend had no protection, so
any retaining callee further down the chain freed the transient while
still in use — heap corruption that crashed the semantic pass on
'class of' expressions.
- Static-array locals are memset to zero on entry, mirroring QBE's
EmitVarAllocs. Element assignment into arrays of managed types
releases the old element; without the memset that release read stack
garbage (TProcess slots in RunFilteredTests).
- Open-array literal arguments are hoisted before the argument pushes
(HoistOALitArgs + BeginCallArgs/PushCallArg/EndCallArgs). The old
in-loop EmitOpenArrayLiteral subq'd its element block in the middle
of the push sequence, so the popq loop that loads the argument
registers popped from inside the block — Outer('gcc', ['-o', B, A])
received AExe='-o'. FMethodArgExtraStack is gone (its only writer
was the in-loop literal emission, and it was reset by nested call
sites mid-argument-evaluation).
Three new e2e tests cover metaclass values on both backends (including
the multi-unit RegisterTest pattern) and an open-array literal that is
not the first argument.
The native backend rejected MethodAddress calls where the second
argument was not a string literal. Evaluate the expression normally
and pass the result (already a data pointer) to _MethodAddress, matching
the QBE backend.
Also convert remaining old-style single-quote concatenation source
constants in e2e tests to triple-quote text blocks for readability.
scripts/fixpoint-native.sh verifies that the native backend is fully
self-hosting: stage-0 (QBE-compiled compiler) emits native assembly,
the linked stage-1 binary recompiles itself via --backend native to
produce stage-2, and stage-2 recompiles itself to produce stage-3.
A clean fixpoint means stage-2 and stage-3 assembly are identical.
Both fixpoints (QBE and native) are now required before every commit.
EmitExprToEax uses %rcx as a scratch register for binary expressions,
but the open-array, dynamic-array, static-array, and PChar element
access paths all saved the base address in %rcx before calling
EmitExprToEax for the index expression. A computed index like [I + 1]
would clobber %rcx, causing the element address computation to
dereference a garbage pointer.
Use pushq/popq to protect the base address across the EmitExprToEax
call, matching the pattern already used by the string-subscript path.
Same fix applied to the @Array[I] address-of paths.
This was the last blocker for the native fixpoint — the native-compiled
compiler can now fully recompile itself with --backend native and
produce identical assembly at stage-2 and stage-3.
The native backend's EmitWrite hardcoded fd=1 for all _SysWrite* calls,
ignoring the StdErr file argument. WriteLn(StdErr, 'msg') emitted the
integer 2 as a literal followed by the message on stdout instead of
routing to file descriptor 2.
Mirror the QBE backend's StdErr detection: if the first argument is the
StdErr identifier, set fd=2 and skip it in the argument loop.
Three related bugs in the native x86-64 backend where record data was
written to the pointer slot (leaq = address-of-slot) rather than through
the pointer (movq = load-pointer-then-deref):
1. EmitSretCall / EmitMethodSretCall: when the LHS of a record-returning
function call is a var/out parameter, pass ASretIsIndirect=True so the
memset and reg-return capture target the caller's buffer, not the
local stack slot holding the pointer.
2. EmitRecordRegReturnCapture: honour a new AIsIndirect flag — emit
movq (load pointer) instead of leaq (address-of-slot) for all
classification shapes (rcInt1..rcSSEInt).
3. EmitExprToEax for TIdentExpr with tyRecord/tyStaticArray: when the
identifier is a const/value record parameter (ParamMode = pmRecordValue
or pmStaticArrayValue), the frame slot holds a pointer to the data
copy, not inline data — emit movq instead of leaq.
Bug 1+2 caused out-param record assignments to overwrite the pointer
with the return value. Bug 3 caused const-record field assignments
(e.g. FTarget := ATarget in SetTarget) to copy a stack address into the
field instead of the record data. Both crashed the native-compiled
compiler when using --backend native.
Added E2E test: TestRun_Native_RecordSret_OutParam.
Replace the hardcoded two-backend AssertRunsOnBoth with a set-driven
design that scales to any number of backends:
- TBackends = set of TBackend; AllBackends constant
- AssertRunsOnAll: iterates AllBackends (replaces AssertRunsOnBoth)
- AssertRunsOn(ABackends: TBackends; ...): run on a specific subset
- BackendName(TBackend): string for assertion labels
When a new backend (e.g. LLVM) is added to TBackend and AllBackends,
all 232 AssertRunsOnAll call sites automatically exercise it.
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.
EmitMethodDef and EmitFuncDef called ClassifyRecordReturn four times on
the same return type (signature, declaration, prologue, epilogue).
Classify once into a local RC variable and pass it to each helper.
The per-call-site classification in EmitRecordReturnCallSite remains
unchanged — it classifies a different type (the callee's) each time.
FIXPOINT_OK, 2831 tests pass.
Implement SysV AMD64 ABI register-return classification in the native
x86-64 backend, matching the QBE backend's record-return support.
Small POD records (≤16 bytes, no managed fields) now return in registers
(rax/rdx for INTEGER, xmm0/xmm1 for SSE) instead of always using the
hidden sret pointer. Classification covers all seven SysV shapes:
rcInt1, rcInt2, rcSSE1, rcSSE2, rcIntSSE, rcSSEInt, and rcWin64Agg.
Changes:
- Move TRecReturnClass enum to uCodeGen.pas (shared by both backends)
- Add classification helpers to TNativeBackend (IsRecordManagedClean,
IsRecordAllIntegerLeaves, ClassifyRecordReturn, etc.)
- BuildFrame: reg-return records allocate Result as a local buffer
(not an sret pointer slot)
- EmitRecordReturnEpilogue: load Result buffer into return registers
- EmitRecordRegReturnCapture: store return registers into dest buffer
- EmitSretCall/EmitMethodSretCall: dispatch through reg-return path
when callee qualifies
- EmitFieldAddrToRcx: new helper for computing record/class field
addresses (needed by float field reads)
- EmitExprToXmm0: handle TFieldAccessExpr and TIntLiteral
- Fix float field assignment in TFieldAssignment (was using integer
load/store path for Single/Double fields)
- Fix Single argument passing (add cvtsd2ss when Double→Single
conversion needed)
Also adds 3 missing QBE E2E tests (nested-record, two-Single, managed-
field sret) and 9 native E2E tests covering all register-return shapes.
FIXPOINT_OK, 2831 tests pass.
Classify record returns per SysV x86-64 §3.2.3 instead of using sret
unconditionally. Small POD records (1-16 bytes, integer and/or float
leaves) now travel in registers (rax/rdx, xmm0/xmm1), matching how C
compilers return small structs. Records with managed fields (string,
class, interface, dynarray — checked recursively) always stay on sret.
Win64 delegates to QBE's -t amd64_win ABI pass via uniform aggregate
type declarations.
Based on Andrew Haines' qbe_record_byval_return branch, rebased onto
master and adapted. Added FlattenRecordToAggLetters to correctly
classify nested-record field types in QBE aggregate declarations.
39 IR tests (cp.test.recordret) + 12 E2E tests (cp.test.e2e.recordret).
2819 tests, FIXPOINT_OK.
Track string and dynamic-array buffer leaks in --debug builds.
Strings are registered in _StringAddRef (on the 0→1 transition)
and removed in _StringRelease; likewise for _DynArrayAddRef/Release.
The leak report prints "string" or "dynarray" as the type tag
instead of a class name.
Sentinel-based type tags (GLTTagString=Pointer(2), GLTTagDynArray=
Pointer(3)) distinguish buffer entries from class entries in the
hash map. The report reads the correct refcount header offset
for each type (12-byte string header, 8-byte dynarray header,
CLASS_HDR for classes).
LT_BUCKETS grows from 1024 to 4096 to accommodate the higher
entry count when strings/dynarrays are tracked. Report wording
changes from "object(s) not released" to "leak(s) not released".
2768 tests pass (2 new e2e tests), FIXPOINT_OK.
Extend _LeakTrackerRegister from 2 to 4 args (UserPtr, ClassName,
UnitName, Line) so the --debug leak report prints where each leaked
object was allocated. Both QBE and native backends now emit the
extra unit-name string literal and AST line number at every
_ClassAlloc site. Leak report output changes from:
- TBox (rc=1)
to:
- TBox (rc=1) at LeakSite:14
Runtime: bucket size grows from 16 to 32 bytes; LTInsert stores
unit-name ptr + line; _LeakTrackerReport prints the " at unit:line"
suffix.
Native backend: plumb FDebugMode from TCodeGenNative through
TNativeBackend; emit _LeakTrackerEnable in $main; emit
_LeakTrackerRegister at both constructor-call paths.
QBE backend: add FCurrentUnitName, set in Generate/GenerateUnit/
AppendUnit/AppendProgram; extend both _LeakTrackerRegister call
sites with EmitStrLit(FCurrentUnitName) + node.Line.
2766 tests pass (2 new e2e tests), FIXPOINT_OK.
When a record-returning function call is passed directly as an argument
to another call (e.g. Consume(MakeRec('hello'))), the caller allocates
an sret buffer on the stack. After the outer call returns, the managed
fields (strings, classes, dyn-arrays, interfaces) inside that buffer
must be released — otherwise they leak.
The QBE backend already handled this via EmitOwnedArgReleases; the
native backend lacked the equivalent. Add EmitSretArgReleases which
iterates args in reverse, computes each sret buffer's stack offset,
and calls EmitRecordFieldReleases to release every managed leaf.
Wire it into all call emission paths: EmitCall, EmitCallIndirect,
EmitMethodCallStmt, EmitMethodCallExpr, EmitInheritedCall,
EmitInterfaceCall, EmitInterfaceFieldCall, EmitMethodPtrCall, and
the implicit-self inline call sites.
Weak interface variables (both backends):
- Native: EmitInterfaceAssign routes [Weak] interface locals/globals
through _WeakAssign (no refcount) instead of _ClassAddRef/_ClassRelease.
Scope-exit cleanup emits _WeakClear for both interface and class weak
locals. EmitGlobalReleases checks IsWeakGlobal for program globals.
- QBE: already had full weak support (no changes needed).
Type-cast of interface variable (both backends):
- TVal(S) where S is an interface var must load from S_obj (the obj
half of the fat pointer), not bare S (which doesn't exist as a symbol).
QBE: EmitExpr type-cast path loads from VarRef_obj.
Native: EmitExprToEax type-cast path uses IntfObjOperand.
Previously crashed at link time with "undefined reference to S".
Two new e2e tests: IntfFieldFromFunc (sret into class field) and
WeakInterfaceVar (weak assignment + scope cleanup + destruction ordering).
Both backends now handle storing the result of an interface-returning
function into a class field:
H.F := MakeIntf(42); { explicit field assignment }
F := MakeIntf(42); { implicit-Self field inside a method }
QBE backend: EmitInterfaceToFieldSlots gains an IsInterfaceCall check
that allocates a 16-byte sret buffer, calls EmitRecordCallSret, loads
obj+itab from the buffer, releases the old field obj, and stores new —
no extra AddRef since the callee already retains into the sret buffer.
Native backend: both EmitInterfaceAssign (implicit-Self path) and
EmitInterfaceToFieldSlotsAt (explicit field path) gain TFuncCallExpr
branches that call EmitIntfSretCall, release the old obj from the
field, and copy obj+itab from the stack buffer into the field slots.
Previously both backends raised an error for this combination — the
sret convention for interface returns (commit c8ce6f7) only covered
local/global variable targets.
Both QBE and native backends now support functions/methods that return
an interface type. The return uses sret (struct-return) convention:
the caller allocates a 16-byte buffer (obj+itab), passes its address as
a hidden first argument, and the callee writes both fat-pointer slots
into the buffer.
QBE backend:
- Function signature adds hidden l %_par__sret, function becomes void
- Result_obj aliases sret+0, Result_itab aliases sret+8
- Epilogue is plain ret (no return value)
- Caller-side: alloc 16-byte buffer, EmitRecordCallSret, load obj+itab
- New IsInterfaceCall() helper and assignment handler for the sret path
Native backend:
- BuildFrame marks interface returns as FSretFunc (same as records)
- EmitIntfSretCall: allocates stack buffer, passes as %rdi, calls func
- EmitInterfaceAssign: new sret-Result path dereferences the Result
pointer to write obj+itab into the caller's buffer
- Assignment handler loads obj+itab from sret buffer after the call
Previously, functions returning interfaces crashed the QBE backend
(undeclared split-slot names in IR) and were unsupported in native.
Extends the ARC element-assignment pattern to static arrays: when
assigning to A[I] where the element type is string or class, emit
AddRef(new)/Release(old) before the store, matching the QBE backend
and the dynarray path added in the previous commit.
When assigning to a dynamic array element where the element type is
managed (string or class), the native backend now emits AddRef on the
new value and Release on the old value before storing, matching the
QBE backend's behaviour.
Previously A[I] := 'new' would store without releasing the old string
at A[I], leaking one reference per overwrite.
E2E test TestRun_Native_DynArrayElemArc_String verifies string element
assignment with replacement on both backends.
Both backends:
- EmitInterfaceToFieldSlots[At] now handles TNilLiteral as the RHS of an
interface-typed field assignment: release the old obj, zero both slots.
Native backend:
- The implicit-Self interface-field assignment path gains nil and
TFieldAccessExpr source handling (was limited to tyClass and TIdentExpr).
QBE backend:
- QbeTypeOf now returns 'l' for tyInterface (was falling through to the
default 'w'). This fixes Assigned(H.F) and other expressions that load
an interface-typed field — the result type 'w' conflicted with the
'loadl' instruction, causing QBE to reject the IR.
E2E test TestRun_Native_IntfFieldNilAssign covers the nil-to-field path
on both backends (sets a field, dispatches, clears, checks Assigned).
When passing an interface stored in a class/record field (e.g. H.F where
F is IFoo) as an argument to a function or method, the native backend now
uses EmitInterfaceFieldAddr to resolve the contiguous fat pointer address
and loads obj+itab from it.
Added TFieldAccessExpr handling to six call-site locations:
- EmitMethodArgPush (method/inherited calls)
- EmitInterfaceCall arg loop (interface dispatch)
- EmitInterfaceFieldCall arg loop (field-based interface dispatch)
- EmitCall <=6 args path (standalone function calls)
- EmitCall >6 args path (pre-allocate strategy)
E2E test TestRun_Native_IntfFieldAsArg verifies the pattern on both
backends: a class holding an IVal field, passed to a standalone function
that dispatches through the interface.
EmitInterfaceToFieldSlotsAt now handles two additional source expressions
when storing an interface value into a class/record field:
- TFieldAccessExpr: uses EmitInterfaceFieldAddr to resolve the contiguous
fat pointer address from a nested field access (e.g. Obj.IntfField),
then reads obj+itab from that address.
- Implicit-Self TIdentExpr: when the source identifier is an implicit
Self field (e.g. bare FIntf inside a method body), reads the fat pointer
directly from Self+offset instead of looking up split _obj/_itab slots.
Both paths read the contiguous (obj, itab) layout at the resolved address
and push them for the subsequent store logic.
Extend interface arg handling in EmitInterfaceCall and
EmitInterfaceFieldCall to support implicit-Self field receivers
(TIdentExpr.IsImplicitSelf with ImplicitFieldInfo) alongside the
existing plain ident path.
Remove the restriction on nested interface-field receivers (A.B.Method()
where B is an interface field): EmitInterfaceFieldAddr already handles
Base <> nil via EmitExprToEax, so the rejection was unnecessary.
EmitMethodArgPush now handles IsOpenArray parameters: inline array
literals allocate element storage on the stack via EmitOpenArrayLiteral
and push both the data pointer and high index; static-array and
forwarded open-array idents are handled as well.
A new FMethodArgExtraStack field tracks stack bytes allocated by
open-array literals so callers can reclaim them after the call.
All EmitMethodArgPush call sites (EmitMethodCallExpr, EmitMethodCallStmt,
EmitInheritedCall, EmitSretMethodCall, and implicit-Self paths in
EmitExprToEax/EmitExprToXmm0) reset and clean up accordingly.
This was the blocker for native self-hosting: the compiler source
uses Format() — whose array-of-const desugars to an open-array
literal — in method bodies.
The native backend stored for-loop upper bounds in global variables.
When a recursive function contained a for-loop, each nested call
overwrote the same global, corrupting the outer loop's bound. This
caused out-of-bounds list access and segfaults during self-hosting
(e.g. AnalyseStmt iterating past Stmts.Count).
Pre-allocate _for_end_N frame-local slots in BuildFrame (one per
for-loop in the function body, counted by CountForStmts), and use
frame-relative addressing in EmitForStmt. Program-main code with
no frame falls back to globals (safe since main is non-recursive).
EmitFormatCall stored the varargs array base pointer in %r11 (caller-
saved), which was clobbered by any function call inside EmitExprToEax
when evaluating format arguments. This caused a segfault when a Format
argument was a function-call expression (e.g. ClassSymName in the QBE
codegen) because the store-back wrote to a garbage address.
Switch to %rbx (callee-saved), push/pop it around the format block so
the base pointer survives arbitrarily complex argument expressions.
Add E2E test with function-call Format arguments to cover this case.
Three related fixes for the native x86-64 backend's sret (struct-return)
handling:
1. Sret forwarding: when an sret function assigns Result from an inner
sret call, the inner call's hidden first arg must be the forwarded
caller buffer pointer (movq from the Result slot), not the address of
the slot itself (leaq). Added ASretIsIndirect parameter to
EmitSretCall/EmitMethodSretCall; the Result-in-sret-function path
passes True, all other call sites pass False.
2. Arg-clobber reload: EmitSretCall saved the sret address in %r10
before arg evaluation, but complex arg expressions (e.g.
TStringList.Get) clobber caller-saved %r10. The final movq %r10, %rdi
used a stale value. Fixed by reloading the sret operand directly into
%rdi after arg evaluation, eliminating the stale-%r10 dependency.
3. Record zero-init: local record variables with managed fields (strings,
classes) were not zero-initialised at function entry. If a record was
never assigned (e.g. a loop body that never executed), the epilogue's
field-release pass called _StringRelease on stack garbage. Added
memset(0) for tyRecord locals in the function prologue, matching the
QBE backend's alloc-then-zero pattern.
Without these fixes, the native-compiled compiler segfaulted during
semantic export when importing any unit with class types.
Named constants with string type (e.g. `const NL = #10`) were compiled
as `movabsq $0, %rax` — the integer ConstValue (always 0 for strings).
The native-compiled compiler crashed in TIRBuffer.AppendLine because
`NL := IR_NL` produced a nil string, and the subsequent BufCopy of the
newline character dereferenced null.
Now the IsConstant path in EmitExprToEax checks IsString() and emits
EmitStrLitAddr(ConstString) — registering the string literal in the
.rodata section and loading its header+12 address, matching QBE.
This was the last blocker preventing the native-compiled compiler from
running the QBE codegen pipeline (the only codegen path it currently
exercises).
When a class variable is passed as a var parameter and a method is called
on it (e.g. AList.Add inside a procedure taking var AList: TStringList),
the stack slot holds the address of the caller's variable, not the object
pointer. Class method dispatch requires dereferencing twice: once to get
the caller's variable address, once to get the heap-allocated object.
The native backend was only dereferencing once, passing the address of the
global/local variable as Self. This corrupted memory when the method wrote
to fields at offsets from the wrong base address — manifesting as adjacent
globals being overwritten (e.g. TStringList.Grow corrupting DumpAST).
Fixed all seven receiver-loading sites: EmitMethodCallExpr (<=6 and >6 slot
paths), EmitMethodCallStmt (<=6 and >6 paths, plus the Free handler),
EmitMethodSretCall, and the zero-arg TFieldAccessExpr.IsMethodCall path.
Also added record-var-param cases for consistency with the QBE backend.
Three codegen bugs preventing the native-compiled compiler from working:
1. Implicit-Self record field copy (e.g. FCurrent := FLookahead inside a
method) emitted a 4-byte scalar mov instead of memcpy. Now emits
EmitRecordFieldRetains on the source, EmitRecordFieldReleases on the
dest, then memcpy for the full record size.
2. Sret Result field assignment (e.g. Result.Value := text inside a
function returning a record) stored string/class/dynarray fields
without AddRef, so the returned record held a pointer with refcount 0.
Now applies the same ARC retain/release pattern as the IsClassAccess
and IsImplicitSelf paths.
3. IsImplicitSelfMethod calls (bare method name inside another method of
the same class) did not pass Self as the first argument. Added checks
in the TProcCall, TFuncCallExpr (value + sret), and EmitExprToXmm0
paths. Also fixed sret Result := Record using movq (load the sret
pointer) instead of leaq (address of the pointer slot).
FR := FG.Make(...) inside a method body produces a TAssignment with
ImplicitSelfField set, not a TFieldAssignment. The sret handler for
record-returning calls was only in the TFieldAssignment branch and
never triggered, causing the assignment to treat a 24-byte record
return as a scalar in %rax (garbage values).
Add sret interception in the ImplicitSelfField non-managed branch
for both TMethodCallExpr and TFuncCallExpr. Also add the matching
TFieldAssignment handler for explicit obj.field := method() patterns.
Both paths compute the field address into %rbx (callee-saved), release
managed sub-fields of the old record, then call via sret convention.
Method calls that return records larger than 16 bytes must use the
sret (struct return) calling convention: the caller allocates the
destination buffer and passes its address as the hidden first argument
in %rdi, with Self shifted to %rsi and explicit args from %rdx onward.
The native backend was treating these like scalar returns, storing the
method result in %rax and trying to write a multi-byte record through a
4-byte movl — corrupting memory and crashing on the first field access.
Add EmitMethodSretCall and wire it into the TAssignStmt handler for
record-typed method call expressions. The callee side was already
correct (sret functions read %rdi as the Result buffer); only the
caller side was missing.
The <=6-args constructor path stored the newly allocated instance in
%r10 (caller-saved), then evaluated constructor arguments via
EmitMethodArgPush. When an argument required a function call (e.g.
TStringList.GetText), the callee clobbered %r10 and the constructor
received a garbage Self pointer — crashing in _ClassRelease when
trying to ARC-release uninitialised fields.
Switch to %rbx (callee-saved, with push/pop) matching the >6-args
path that already used this strategy.
The native backend stored string/dynarray/class values directly into
the local param slot (-N(%rbp)) instead of writing through the
indirection pointer that var/out parameters carry. This overwrote the
caller's variable address with a string value on the first assignment,
corrupting the stack frame. Later reads of other locals would see
garbage pointers.
Fix all four managed-type assignment paths (tyString, tyDynArray,
tyClass, tyClass+nil) to detect IsVarParam and emit the double
dereference: load the pointer from the slot, load/store the actual
value through that pointer.
This unblocks the native-compiled compiler from parsing CLI arguments
that follow --source / --unit-path (the out-param file path was
corrupting Arg's stack slot on the next loop iteration).
The native backend's binary expression handler had no special case for
string = / <> operators, so they fell through to the integer comparison
path (cmpq), comparing pointer addresses instead of string content.
Two strings with identical content but different addresses (e.g.
ParamStr(1) vs a string literal) always compared as not-equal.
Add an early-exit in EmitExprToEax for boEQ/boNE on tyString: call
_StringEquals(left, right) and xorl $1 for <>. This fixes the
native-compiled compiler's CLI flag parsing, which relies on string
comparison against '--source', '--emit-ir', etc.
EmitMethodCallExpr was always emitting a direct `callq` for method calls,
ignoring VTableSlot. Virtual calls like B.GetVal() (where B's declared
type is an abstract base) produced a direct call to the base's method
symbol, which is never emitted for abstract methods — causing link errors.
Add vtable dispatch (load vtable ptr from Self, index by VTableSlot,
indirect call) in both the <=6 and >6 arg paths of EmitMethodCallExpr,
matching what EmitMethodCallStmt already does.
This resolves the last undefined references (Streams_TInputStream_Read,
Streams_TOutputStream_Write) when native-compiling the compiler itself.
The native-compiled compiler now links and runs.
When a bare identifier inside a method resolves to a zero-arg method or
property getter on Self (TIdentExpr.IsImplicitSelfMethod), the native
backend was falling through to a global variable load instead of
emitting a function call. This produced `movq GetterName(%rip), %rax`
(reading a non-existent global) instead of `callq ClassName_GetterName`.
Add the missing EmitExprToEax branch: load Self into %rdi, call the
getter via MethodEmitNameNative.
Add e2e tests for implicit-self property getter and const array string
element access.
Emit unit-level and class-level const arrays (string and integer) as
.data sections in the native backend, mirroring the QBE backend's
EmitGlobalConstData/EmitLocalArrayConstsInBlock/EmitLocalArrayConstsInTypeDecls.
Use TIdentExpr.ConstArraySymbol (the mangled QBE data label set by the
semantic pass) for all RIP-relative base-address loads of const arrays,
matching the label emitted in the data section. Previously the native
backend used the bare Pascal name (e.g. SysVArgRegs64) while the data
label was the mangled form (__bac_2_SysVArgRegs64), causing undefined
reference link errors.
Fixed all static-array reference sites: EmitExprToEax (tyStaticArray
ident), var-param arg push (push and pre-allocate paths),
EmitMethodArgPush, open-array pass paths, and procedural-type call
paths.
The native backend emitted typeinfo, vtable, FieldCleanup, itab, and
impllist symbols with bare class names (e.g., typeinfo_TIdentExpr) but
is/as/Supports expressions and constructor calls referenced them with
unit-prefixed names (typeinfo_uAST_TIdentExpr). This caused 700+
undefined references when native-compiling the compiler.
Apply ClassSymName consistently in EmitClassSection, EmitInterfaceDefs,
constructor allocation, and interface assignment/arg-push sites. Add
IntfTypeInfoName helper (matches QBE backend) to distinguish generic
interface instances (bare mangled name) from plain interfaces (unit-
prefixed via ClassSymName).
Also fixes var-param implicit-Self field address in the pre-allocate
(>6 args) path of EmitCall.
Native-compiling the compiler now has only 8 unique undefined symbols
remaining (down from 735): SysVArgRegs const arrays, GetCurrentScope
property, and two abstract Streams methods.
When passing a class field as a var/out argument inside a method, the
address must be computed as Self + field_offset, not leaq FieldName(%rip).
The latter treats the field name as a global symbol, causing link errors.
Add IsImplicitSelf checks in all three var-param code paths:
EmitMethodArgPush, EmitCall push/pop path, and EmitCall pre-allocate path.
When a TFieldAccessExpr has both IsImplicitSelf and IsClassAccess set
(e.g. FInner.FVal where FInner is a class field of Self), the
IsClassAccess branch was checked first, emitting RecordName(%rip) as a
global symbol instead of loading through Self. This caused 100+
undefined-reference link errors when native-compiling the compiler.
Fix: check IsImplicitSelf before IsClassAccess in all three affected
sites (field read, @field address-of, and array-field subscript).
Add ClassName/ClassType built-in handlers to the native backend:
ClassName dereferences instance→vtable[0]→typeinfo[0]→name(+16),
ClassType dereferences instance→vtable[0]→typeinfo[0].
Fix method calls with >6 total argument slots (Self + user args) in
EmitMethodCallStmt, EmitMethodCallExpr, and the constructor path.
The old push/pop strategy overflowed the SysVArgRegs64 array. The new
pre-allocate strategy places overflow args at the top of the frame so
they remain on the stack after the register-loaded slots are stripped
via addq, matching the SysV ABI convention for stack-passed arguments.
The constructor path uses callee-saved %rbx to hold the instance
pointer across arg-evaluation calls.
Property writes via setter methods: TFieldAssignment with PropWriteInfo
calls the setter with Self/%rdi, optional index/%rsi, value/%rdx.
Fix codegen-time double-decrement of FExcDepth and FFinallyStack in
EmitTryFinallyStmt and EmitTryExceptStmt: both the normal and exception
code paths are emitted at codegen time, so each Dec/Delete ran twice.
Now the exception path restores the bookkeeping state before emitting
its own Dec/Delete. Also replace FFinallyStack.Clear() with Free/Create
to avoid ARC release of stale slot data in the TList backing store.
Emit method calls for TFieldAccessExpr nodes with PropRead set,
covering chained (Base), implicit-Self, and plain variable receivers.
Indexed properties pass the subscript as the second argument (%rsi).
EmitFormatCall now detects when the second argument is a
TArrayLiteralExpr (the Format('...', [a, b, c]) syntax) and iterates
its elements individually, matching the QBE backend's handling.
Previously, the array literal reached EmitExprToEax which raised
an unsupported-expression error.
Also improve the unsupported-expression error to include line/col
information for easier debugging.
Extend EmitIncDec to handle all l-value forms the QBE backend supports:
- TFieldAccessExpr (record/class fields via address computation)
- TDerefExpr (pointer dereference P^)
- Implicit-Self fields (bare FField inside methods)
- Var-param TIdentExpr (dereference the pointer slot)
Extract EmitIncDecAddrOp helper for the load-add/sub-store sequence on
an address in %rdx. Use proper 32-bit register names (eax/ecx) for
non-wide types.
Also add tyInterface to IntByteSize (was falling to the 4-byte default
despite interfaces being pointer-sized), fixing type-cast expressions
on interface values.
Add e2e tests: Inc/Dec on record field, Inc/Dec on pointer deref, and
Pointer/Integer type-cast round-trip.
Restructure EmitCall, EmitCallIndirect, EmitSretCall and
EmitMethodPtrCall to handle arbitrary argument counts using a
pre-allocate strategy: reserve SlotCount*8 bytes on the stack, evaluate
each argument into its fixed slot, then load the first 6 integer args
into SysV registers and leave overflow args on the stack in correct
forward order.
Also extend SysVXmmArgRegs from 6 to 8 entries (SysV allows 8 float
regs: xmm0-xmm7) and add mixed int/float argument routing in the
pre-allocate path.
Add e2e test TestRun_Native_TenArgs exercising a 10-integer-arg
function call on both backends.
Introduce TParamMode (pmNone, pmVar, pmRecordValue, pmStaticArrayValue)
on TIdentExpr so the semantic pass records the precise param-slot
classification once instead of merging three cases into a Boolean.
The QBE backend treats all three modes identically (ParamMode <> pmNone),
preserving existing semantics. The native x86-64 and LLVM backends can
now dispatch on the enum directly instead of re-deriving the distinction
from Sym.Kind and TypeDesc.Kind at every reader site.
Other IsVarParam fields (TFieldAccessExpr, TMethodCallExpr, TAssignment,
TMethodParam, TMethodCallStmt) are unchanged.
Based on Andrew Haines' proposal and patch (blaise_llvm d16f877).
When a child class inherits fields from its parent, the field loop in
both AnalyseTypeDecls and InstantiateGeneric copied Name and TypeDesc
but dropped IsUnretained and IsWeak flags. This caused _FieldCleanup
in child classes to emit _ClassRelease calls for inherited [Unretained]
fields, releasing references the child does not own — a use-after-free
when the shared reference (e.g. a symbol-table type cache) is still
live elsewhere.
Fix both inheritance sites to copy the flags after AddField. Add a
regression test that asserts TChild's _FieldCleanup contains exactly
one _ClassRelease (for the owned field) and not two.