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.
EmitPointerWrite chose the QBE store opcode from a hand-rolled
QType=='w' check that fell through to storel for everything else.
That meant:
PDouble^ := V -> storel of a d-typed temp (QBE rejects the type mismatch)
PSingle^ := V -> storel into a 4-byte slot (8-byte clobber)
PSmallInt^:= V -> storew into a 2-byte slot (4-byte clobber)
Route through the existing StoreInstrFor helper so the opcode matches
the pointee width and class (stored / stores / storeh / storeb /
storew / storel). Also add truncd/exts coercion when the value
expression's type width doesn't match the pointee (e.g. double
literal written through PSingle^).
Native backend: add tyDouble/tySingle branches to the pointer-write
handler so float values flow through EmitExprToXmm0 and store via
movsd/movss instead of the integer EmitStoreVar path.
Credit: Andrew Haines (andrewd207) reported the bug and provided the
QBE-side fix.
Regression tests:
- cp.test.pointers: IR-shape tests asserting stored/stores for
PDouble^/PSingle^ writes.
- cp.test.e2e.pointers: PDouble^ round-trip, PSingle^ adjacent-slot
clobber check.
- cp.test.e2e.native: same two cases through the native backend.
Suite: 2727 OK. FIXPOINT_OK.
Add all 14 trig/math builtins to the native x86-64 backend (Sin, Cos,
Tan, ArcTan, ArcTan2, ArcSin, ArcCos, Ln, Log2, Log10, Power, Sinh,
Cosh, Tanh) with proper Single dispatch — when the argument type is
tySingle, emit the f-suffixed libm variant (sinf, cosf, etc.) to avoid
a needless Single→Double→Single round-trip, matching the QBE backend.
Also add Single dispatch to existing builtins that were missing it:
Sqrt (sqrtss vs sqrtsd), Round/Trunc (cvtss2si vs cvtsd2si),
Floor/Ceil (floorf/ceilf + cvttss2si), IsNaN/IsInfinite (__isnanf/
__isinff), and SingleToStr (call _SingleToStr directly instead of
widening to double).
Introduces EmitFloatBuiltin() to handle trig calls in the
EmitExprToXmm0 path (float context, e.g. Round(Sin(X))), alongside
the existing EmitExprToEax handlers (integer context).
Two new e2e tests: Sin/Cos and Sqrt through the native path.
Suite: 2721 OK. FIXPOINT_OK.
Nested procedures declared inside a function body are now supported by
the native x86-64 backend. Each nested proc is mangled as Outer_Inner
and emitted before the enclosing function. Captured outer-scope
variables are passed as implicit leading pointer params (_cap_<Name>);
reads/writes inside the nested function transparently dereference
through the capture pointer. Forwarding works when the caller is
itself nested (forwards its own _cap_ slot).
Two new e2e tests: read-capture (inner reads outer var) and
write-capture (inner mutates outer var, outer sees the change).
Handle function pointer calls that return a value, e.g. Fns[0](7).
The callee expression is evaluated first and saved on the stack (safe
across arg evaluation which may invoke callq), then args are pushed,
popped into SysV registers, and the saved function pointer dispatched
via callq *%r10.
Implement `X is TFoo` (class type test via _IsInstance), `X as TFoo`
(checked downcast with _Raise_InvalidCast on failure), and
`Supports(Obj, IFoo)` (interface query via _ImplementsInterface).
Add ClassSymName helper that resolves a class/interface name to its
assembly symbol with unit prefix (e.g. Exception → SysUtils_Exception),
mirroring the QBE backend's ClassSymName. Fix existing as-cast and
except-handler typeinfo references to use ClassSymName instead of bare
NativeMangle(TypeName).
Add tyProcedural, tyDynArray, and tyInterface to the parameter and
return type guards in EmitFunctionDef. All three are 8-byte pointer
values that flow through the existing register/stack spill path.
tyProcedural params were the first blocker when trying to compile the
TestRunner with --backend native.
Boolean and/or were using bitwise andq/orq, evaluating both operands
unconditionally. Code like `(P <> nil) and P.IsX` crashed because
P.IsX was evaluated even when P was nil.
Now boolean and/or emit conditional jumps: for `and`, LHS=0 skips RHS;
for `or`, LHS<>0 skips RHS. Bitwise integer and/or (IsNumeric operands)
remain unchanged.
The native x86-64 backend now supports by-value record parameters with
correct value semantics and ARC management:
Callee side: memcpy from the incoming pointer into a local data buffer,
store the buffer address in an indirection slot (matching the semantic
pass's IsVarParam pointer-through convention), then AddRef each managed
leaf on entry and Release on exit. Const params skip both.
Caller side: record-returning function calls used as arguments (e.g.
Consume(MakeOuter())) now emit an sret call into a stack-allocated temp
buffer; the buffer address is passed to the outer call.
Adjacent fixes surfaced by the record-param tests:
- Field assignment ARC for IsVarParam records: string/class/dynarray
fields written through the var-param indirection pointer now get
AddRef(new) + Release(old), preventing double-frees and leaks.
- Field assignment ARC for local/global records: same managed-field
ARC pattern applied to the local and global record assignment paths.
- sret Result dereference: nested field access on an sret Result
(e.g. Result.Inner.N) now loads the pointer from the slot instead
of taking the slot's address, fixing a crash with nested records.
Suite: 2701 tests (136→142 native), FIXPOINT_OK, rolling bootstrap OK.
Companion to the callee-side ARC pass. When a record-returning function
result is consumed inline as a by-value record argument — DoSomething(GetRec())
— the caller allocates an sret buffer for the return, hands it to the
callee as the aggregate arg, then drops the buffer at end of statement.
With no per-managed-leaf release on that buffer the temp's string,
dynarray, class, and interface fields leaked one ref per caller
invocation, accumulating quickly in loops.
EmitOwnedArgReleases now handles tyRecord: when the argument expression
is a record-returning function or method call (the only shape that
produces a fresh sret temp), the helper walks the buffer with
EmitRecordReleaseFields and emits a release per managed leaf, recursing
through nested record fields. Variable references (DoSomething(W)) are
explicitly excluded — their storage belongs to the enclosing scope's
own ARC pass and must not be touched by the call site.
POD record temps reach the helper too, but the field walk finds nothing
to release, so the cleanup compiles down to a no-op for them.
Covered by IR-level assertions on the temp / variable / nested-record
cases, plus an e2e stress loop that runs the inline pattern 1000 times
with a two-string nested record.
A record passed by value whose fields include managed types (string,
dynarray, interface, class) was operated on by the callee without an
entry-side AddRef. When the callee reassigned such a field, the
standard release-old/addref-new lowering freed the caller's shared
heap data and left the caller pointing at memory the allocator could
reuse, producing use-after-free reads (and frequently crashes) on the
next access.
QBE materialises the aggregate so the callee already gets its own
record bytes; the bug was purely the missing per-managed-leaf retain.
Fix mirrors the existing tyString/tyClass/tyInterface ARC pass for
flat managed params: walk the record at function entry and AddRef
each managed leaf (recursing into nested records), then Release them
at exit to balance. const record params skip both ends, matching
the existing rule that the caller's retain covers the whole call.
Regression covered by IR-level assertions on prologue _StringAddRef /
_DynArrayAddRef plus an e2e test that round-trips a heap-allocated
string through a by-value record param.
A [Threaded] test suite runs as a `--suite NAME --verbose` subprocess whose
stdout is parsed for results. If that subprocess crashed mid-suite (e.g.
SIGSEGV), the runner counted however many `... OK` lines it had received and
recorded NO error — so a crash masqueraded as a green run with a silently lower
test count. This is exactly how a real codegen crash
(TCrossUnitRefTests.TestLocalTypeRef_UnqualifiedUnitName) stayed hidden behind
an `OK` summary.
A healthy suite subprocess always ends with a summary line ("OK (...)" or
"FAIL (...)"). After collecting each subprocess's output, if that line is
absent the process was killed or died early, so its parsed count is incomplete:
record a loud error naming the suite (and its exit code) so RunAll returns
non-zero and the crash is surfaced under "Errors:". ProcessExitCode maps a
signal death to 1 and a clean exit to its real code, so the missing-summary
check is the reliable signal regardless of exit code; an exit code > 1 with a
summary present is also flagged as abnormal.
This complements the earlier [Threaded]-parse fix (commit 576f60d): that one
restored failure *counts*; this one stops a *crash* from being silently dropped.
Full suite OK (2687), FIXPOINT_OK.
Passing a record (or static array) sourced from a CLASS FIELD by value crashed:
`SumIt(H.R)` where H is a class instance and R a record field emitted the
argument as `loadl <field_addr>` — dereferencing the field and passing its first
8 bytes as the `:_ffi_<Rec>` aggregate. The callee's pointer parameter then
aimed into the field's bytes instead of at the record, so the first read
returned garbage and segfaulted.
The `IsClassAccess` branch of the field-read in EmitExpr was missing the
record/static-array case that the plain record-field branch already has:
inline-aggregate fields must yield their ADDRESS (records and static arrays are
passed and assigned by address-as-value), not a loaded scalar. Add the
`tyRecord`/`tyStaticArray` -> Exit(Ptr) guard to the class-field branch, matching
the record-field branch.
This is an addressing bug independent of refcounting — it crashes a pure-integer
record too. It was the cause of the SIGSEGV in
TCrossUnitRefTests.TestLocalTypeRef_UnqualifiedUnitName (IsLocalRef(Sig.ReturnType)
passes a class-field record by const value), which a threaded test subprocess
had been crashing on silently.
Full suite OK (2687), FIXPOINT_OK.
A program that `uses` one or more units now compiles and runs under
`--backend native`, matching the QBE backend. Previously `AppendUnit`/
`AppendProgram` on the native code generator raised "multi-unit compilation not
yet implemented", so any program with a `uses` clause (including stdlib units
like StrUtils) failed — this blocked compiling most real programs and a large
part of the test suite natively.
The model is whole-program (the same one the QBE backend uses for a `--source
program` build): every dependency unit plus the program is emitted into ONE
assembly file and linked into one binary; there is no separate .o linking.
Implementation:
* TNativeBackend (base): adds abstract `EmitUnit`, and non-clearing
`AppendUnit`/`AppendProgram`/`GetOutput` so units and the program accumulate
into a single assembly buffer (mirrors the QBE `Generate` vs append split).
* TCodeGenNative: `AppendUnit`/`AppendProgram` now drive the backend instead
of raising.
* TX86_64Backend.EmitUnit: emits a unit's globals, class/record/generic method
bodies, standalone impl-block procedures, generic function instances, the
unit `initialization` as `<Unit>_init` (called from `$main` after _SetArgs),
and the unit's class + interface data sections.
* The shared section emitters (`EmitClassSection`/`EmitClassMethods`/
`EmitInterfaceDefs`) now take the type-decl list and the symbol table as
explicit parameters rather than reaching through `AProg`, so they serve both
program and unit decls. A `FSystemDefsEmitted` guard emits TObject/
TCustomAttribute system definitions exactly once across all units + program.
* Three pre-existing native gaps surfaced by compiling all of StrUtils
(whole-program) and fixed for parity: `external name '...'` symbols are
honoured verbatim (no unit mangling of C symbols); GetMem/ReallocMem/FreeMem
builtins; PChar subscript read/write.
The symbol table is passed explicitly to the section emitters because the
single-program path (`Generate` → `EmitProgram`) does not set a persistent
symbol-table field — depending on one would dereference nil there.
Tests: 5 multi-unit e2e tests in cp.test.e2e.native.pas (plain function, string
function, class, interface, globals+initialization) plus a backend-parameterised
`CompileAndRunWithUnitOn` harness in cp.test.e2e.base.pas — each runs on both
backends and asserts native==QBE stdout.
Full suite OK (2686), FIXPOINT_OK (QBE self-host path byte-identical).
EmitFunctionDef loaded the function Result into %rax (or %xmm0) at the top of
the epilogue, then ran the local/param ARC release pass — which calls
_ClassRelease / _StringRelease / _DynArrayRelease. Those calls clobber %rax, so
any value-returning function that also releases an ARC-managed parameter or
local returned garbage.
Most visibly, a method returning Integer but taking an interface (or class)
value parameter releases that parameter on exit, so `U.Use(I)` returned 3
instead of 55; a String-returning function taking a String parameter returned a
clobbered pointer.
Move the Result load to AFTER every ARC release call, immediately before
leave/ret, so nothing can clobber it. sret (record-returning) functions are
unaffected — they return via the caller's buffer and load no Result. The $main
epilogue already sets %eax=0 after its global-release pass, so it was correct.
Two e2e regression tests on both backends: TestRun_Native_RetValSurvivesArcRelease
(value return past a class-param release) and TestRun_Native_IntfArgToMethod
(interface value arg to a method, dispatched in the callee).
Full suite OK (2681), FIXPOINT_OK (native-backend-only change).
The OPDF class record stores each class's VMTAddress (the debugger reads an
object's VMT pointer at runtime and matches it to identify the dynamic type).
The QBE backend emits user-class and generic-instance vtables as LOCAL symbols
(`data $vtable_X`), while the .opdf debug section — assembled as a separate
object file — references `vtable_X`. A local symbol is not visible across
object files, so every `--debug-opdf` build of a program containing a class
failed to link with "undefined reference to vtable_X".
Add an OPDF-mode flag to the codegen (ICodeGen.SetOpdfMode, wired from the
driver's OPDFEnabled). When on, the QBE backend emits user/generic vtables as
`export data` so the .opdf object can resolve them; when off (every normal
build) the output is byte-for-byte unchanged — verified: non-OPDF IR still emits
`data $vtable_TThing`, --debug-opdf emits `export data $vtable_TThing`. The
native backend already emits its vtables `.globl`, so its SetOpdfMode is a no-op.
Scope is deliberately gated on OPDF mode to avoid duplicate-symbol errors in the
separate-compilation (per-unit) path, where the same generic vtable can appear
in multiple objects; OPDF is only used with whole-program --source compiles.
Result: `--debug-opdf` now links and runs class programs, and the OPDF reference
debugger loads them. Full suite OK (2679), FIXPOINT_OK (output unchanged for
non-OPDF builds).
Two related gaps with interfaces held in a record/class field, across both
backends.
Gap 1 — native dispatch through a non-Self interface field (H.G.Greet()):
EmitInterfaceCall assumed a named interface receiver and emitted bare
_obj/_itab operands (empty name prefix) for a field receiver, failing to link.
It now takes an optional receiver expression; when that is an interface-typed
TFieldAccessExpr the obj/itab are loaded from the field's contiguous fat
pointer via the new EmitInterfaceFieldAddr helper. The TMethodCallExpr
interface-dispatch site passes ACall.ObjExpr.
Gap 2 — reading an interface out of a field into an interface local (G := H.G),
which failed on BOTH backends:
* QBE: the interface-to-interface assignment branch required a TIdentExpr RHS,
so a TFieldAccessExpr source fell through to the scalar store path and
emitted a single-slot `storew ..., $F` against a name that has only
$F_obj/$F_itab data definitions — an undefined-symbol link error. It now
accepts a TFieldAccessExpr RHS and resolves obj/itab via
EmitInterfaceExprPair (which already handles the field-access shape).
* Native: EmitInterfaceAssign gained a TFieldAccessExpr RHS case that loads
the source field's contiguous fat pointer via EmitInterfaceFieldAddr.
e2e: TestRun_Native_IntfFieldDispatch (dispatch with an argument through a
field) and TestRun_Native_IntfFieldReadIntoLocal (read field into local, then
dispatch) — both run on native and QBE via AssertRunsOnBoth.
Full suite OK (2679), FIXPOINT_OK.
The native x86-64 backend had no Chr builtin case. Chr(N) used in a String
context — `s := Chr(66)`, `'A' + Chr(66) + 'C'`, `s := s + Chr(i)` — emitted the
integer N where a String pointer was expected, so the following _StringAddRef
dereferenced the raw integer and segfaulted.
Add a Chr case to the native builtin-function dispatch that lowers Chr(N) to
_Chr(N), which allocates and returns a one-character heap String. The result is
a String pointer and flows through the existing String ARC assignment/concat
paths unchanged — matching the QBE backend.
e2e: TestRun_Native_String_ChrConcat covers assignment and a concatenation loop,
run on both backends via AssertRunsOnBoth. Full suite OK (2677), FIXPOINT_OK,
valgrind-clean.