The previous fix caught procedural-typed variables passed directly to
WriteLn, but missed the case of calling a procedure variable and passing
the (non-existent) result: WriteLn(proce(2,3)). A procedure call
resolves to tyVoid, not nil, so the nil check didn't fire. The codegen
emitted an empty QBE operand ('w ') causing QBE to reject the IR.
Extend the Write/WriteLn arg check to also reject tyVoid, covering both:
- WriteLn(proce) — procedural variable (tyProcedural)
- WriteLn(proce(a,b)) — call to a procedure variable (tyVoid)
Adds a semantic unit test for the procedure-call-result case.
WriteLn/Write had no type-check on their argument list — a procedural
variable (or any other unprintable type: record, class, interface, array)
was silently accepted by the semantic pass and handed to the codegen,
which emitted it as an integer (the raw code pointer). The native backend
printed the address; QBE rejected the IR with "invalid argument".
Add a post-analyse check in the Write/WriteLn branch: after resolving each
argument's type, reject tyProcedural, tyRecord, tyClass, tyInterface,
tyMetaClass, tyStaticArray, tyDynArray, tyOpenArray, and tyPointer with
a clear error message matching FPC's behaviour.
Adds one semantic unit test covering the procedural-variable case.
BlaisePath() was using GetCurrentDir() as the project root fallback,
then appending 'compiler/target/blaise'. This worked when the TestRunner
was invoked from the project root, but broke when PasBuild cd'd into
compiler/target/ before running ./TestRunner — the path doubled up to
.../compiler/target/compiler/target/blaise.
Fix by deriving the path from ExtractFilePath(ParamStr(0)), which always
points to the directory containing the TestRunner binary regardless of
the caller's working directory. The BLAISE_PROJECT_ROOT env var override
is preserved unchanged.
LinkGenericClassMethodImpls only handled TGenericTypeDef (class), but
TGenericRecordDef is registered in the same generic registry. When an
out-of-line method implementation was written for a generic record
(function MyRec<T>.Foo), the lookup succeeded but the cast to
TGenericTypeDef followed by .ClassDef access was wrong for records,
producing a spurious "Generic type not found" error.
Fix: check the runtime type and dispatch to .ClassDef.Methods for
classes or .RecordDef.Methods for records. Also broadened the
"not declared in generic class" error message to "generic type".
Tests: two new cases in TGenericRecordTests cover the semantic link
and codegen emission for out-of-line generic record methods.
The SrcZeroInit_SetLocal source previously used `fA in S` to probe
set zero-initialisation, working around the S=[] semantic-pass crash
(now fixed). Update it to use S=[] directly — both more idiomatic
and exercises the fixed equality path in the same test run.
AnalyseBinaryExpr crashed with a nil-deref when one operand was a set
type (tySet) and the other was an empty array literal [] — because
AnalyseArrayLiteralExpr returns nil for an empty literal (no element
type to infer), and the set-dispatch guard read .Kind off that nil
pointer.
Fix: before the set-dispatch guard, coerce any array literal on either
side (empty or non-empty) to the set type of the opposite operand,
mirroring the existing pattern for `x in [A, B, C]`. This also
enables `S = [fA, fB]` without requiring an explicit cast.
The native-backend set-variable crash documented in bugs.txt was
already fixed in HEAD (the release binary produced a codegen error, but
the current compiler handles it correctly).
Tests added:
- TSetTests.TestSemantic_Set_EqualityEmptyLiteral_OK — S=[] must not crash
- TSetTests.TestSemantic_Set_EqualityLiteral_OK — S=[fA,fB] accepted
- TSetTests.TestCodegen_Set_EqualityEmptyLiteralEmitsCeqw — IR check
- TSetTests.TestCodegen_Set_EqualityLiteralEmitsCeqw — IR check
- TE2EMiscTests.TestRun_Set_EqualityWithLiteral — compile+run assertion
- TE2ENativeTests.TestRun_ZeroInit_SetLocal now runs on both backends
Blaise now guarantees zero-initialisation of every variable as a language
semantic — local variables, globals, record fields, static-array elements,
threadvars, and Result. The QBE backend already satisfied this; the native
x86-64 backend was the only gap (scalar locals were uninitialised on stack).
Native backend changes (blaise.codegen.native.x86_64.pas):
- Replaced the ARC-only zero-init loop in EmitFunctionDef with an
exhaustive case over all TTypeKind values, covering every scalar
type (integer family, float, boolean, pointer, enum, set, procedural)
in addition to the already-handled managed types. An else-raise clause
ensures any future new type kind is caught at compile time rather than
silently skipped.
- Fixed AddSlot to allocate 16 bytes for method-pointer locals (Code +
Data slots), matching the 16-byte allocation the QBE backend already
used. Previously only 8 bytes were reserved, which would corrupt an
adjacent frame slot if a local method-pointer was written.
10 new E2E tests run on both backends via AssertRunsOnAll, using a
Dirty() helper that pre-fills the stack with 0xDEADBEEF to prove zero-init
comes from the prologue and not from lucky stack layout:
TestRun_ZeroInit_ScalarIntegers, FloatLocals, BooleanAndChar,
PointerLocals, EnumLocal, SetLocal (QBE-only — native crashes on sets,
pre-existing bug), RecordWithMixedFields, StaticArray, ThreadVar,
GlobalVars.
Documented in docs/language-rationale.adoc (decision, alternatives
rejected, implementation notes, future noinit/definite-assignment roadmap).
The --debug leak tracker reports allocation sites as '<unit>:<line>'.
For objects allocated inside generic method bodies the two halves of
that pair came from different sources: the line number is the cloned
template AST's Line field (which refers to the template's source file),
but the unit name was FCurrentUnitName — the unit or program being
emitted when the generic instance's method bodies were generated, i.e.
the INSTANTIATING unit. A TListEnumerator<Integer> created inside
TList<T>.GetEnumerator (generics.collections.pas:316) was therefore
reported as 'P:316' for a 33-line program P.
Fix: record the declaring unit on TGenericTypeDef when the template is
registered (both the direct semantic path and the .bif import path),
copy it onto TGenericInstance at instantiation, and have both backends
temporarily switch FCurrentUnitName to GI.DefUnitName around the
emission of generic-instance method bodies, restoring it afterwards.
The report now reads 'Generics.Collections:316'.
The new DefUnitName fields are populated by the semantic pass at
runtime and are intentionally not serialised; bif-coverage passes
unchanged. Generic record and generic function instances still carry
the old mismatch and are tracked separately.
New e2e tests (QBE + native): TestDebug_GenericAllocSite_ReportsDefiningUnit
and TestDebug_GenericAllocSite_ReportsDefiningUnit_Native.
Two bugs in the enumerator-protocol for-in lowering, found via a user
program iterating a TList<Integer>:
- QBE: the mem2reg promotion scan did not treat the for-in loop
variable as a store target, so an otherwise-unaddressed scalar loop
var was promoted to an SSA temp while the lowering wrote the element
with storew %t, %_var_<name> — invalid IR ('invalid type for second
operand ... in storew'). The loop variable now always keeps a stack
slot.
- Both backends: GetEnumerator's result (an owned +1 constructor
return) was re-retained when stored into the synthetic enumerator
slot, while the function epilogue releases the slot exactly once —
leaking one enumerator per loop (rc=1 in every --debug leak report).
The owned reference is now transferred into the slot.
e2e leak-check tests run the same list-summing loop under --debug on
both backends and assert clean output with no leak report.
TCompileWorker now carries Driver + Opts and builds its per-unit
codegen through Driver.CreateUnitCodeGen, names its temp IR file with
the driver's extension, and lowers through the same driver. A driver
that claims SupportsIncremental but returns a nil unit codegen fails
with a clear error instead of crashing. The incremental dispatcher
prefers the top-program backend's driver and falls back to QBE when
the backend has no per-unit emission yet — the only remaining backend
comparison outside PickTopDriver.
--backend validation and the --help backend line are now driven by the
driver registry (ParseBackendName / RegisteredBackendNames), so adding
a backend no longer touches the flag parser. The toolchain pre-flight
runs once through Driver.CheckToolchain before the front-end; stdout-
only modes (--emit-ir / --emit-asm / --dump-ast) skip it since they
need no external tools.
Blaise.pas no longer references TCodeGenQBE/TCodeGenNative anywhere;
unused locals from the pre-driver pipeline are removed.
Architecture follows Andrew Haines' unify_backend_interface proposal.
Move the IR-to-object and IR-to-binary pipelines behind TBackendDriver
(architecture per Andrew Haines' unify_backend_interface proposal):
* LowerToObject — QBE driver runs qbe -> .s -> cc -c -> .o; the base
class default fails loudly for drivers without per-unit emission.
* LinkProgram — QBE driver lowers via qbe then links; the native driver
links its assembly directly, or assembles in-process
(AssembleToObject) and links the .o when --assembler internal is
selected. The --assembler policy now lives inside the native driver
instead of Blaise.pas.
* LinkViaToolchain — shared protected link line (input, OPDF sidecar,
prebuilt dep objects, RTL archive, -lm, -lpthread) resolved through
uToolchain, so BLAISE_QBE/BLAISE_LINKER/BLAISE_RTL overrides now apply
uniformly; the unit-object path previously hard-coded 'qbe'/'cc'.
* RunProcess moves to blaise.codegen.driver (used from worker threads).
Blaise.pas drops CompileToNative, CompileToNativeDirect, LinkObjectFile,
FindRTL, and the backend branch in the output dispatch; the single
shared path writes the IR with the driver's file extension and calls
Driver.LinkProgram. Behaviour deltas: the native link now also receives
auto-discovered prebuilt dep objects (previously dropped on the native
path), and link failures report 'link error (exit N)' uniformly.
Add blaise.codegen.driver with the abstract TBackendDriver base class
(virtual Kind/Name/IRFileExt/SupportsIncremental/SupportsWarmCache/
CheckToolchain/CreateCodeGen/CreateUnitCodeGen), the TBackendOpts flag
bag, a fixed-array registry keyed by backend kind, and PickTopDriver —
the single backend-selection policy decision (--emit-ir forces QBE,
--emit-asm implies native, otherwise --backend).
blaise.codegen.qbe.driver and blaise.codegen.native.driver register
class singletons at unit initialization; they are ARC-managed globals
released by the program-exit release pass.
Blaise.pas now builds one TBackendOpts up front, resolves the driver
once via PickTopDriver, and constructs the code generator through
Driver.CreateCodeGen — the backend if/else around TCodeGenQBE /
TCodeGenNative construction is gone.
The architecture follows Andrew Haines' unify_backend_interface
proposal; full credit to Andrew for the driver/registry design. This
tree uses an abstract class with virtual methods instead of an
interface so shared behaviour can live in the base class.
EmitRecordCallSret always emitted a direct call to the declaring class's
method symbol, even for virtual methods. Overrides never ran on the
record/interface sret return path, and calling a virtual-abstract method
(which has no emitted body) produced an undefined-symbol link error.
Add SretMethodCallTarget: when VTableSlot >= 0 load the vptr from the
instance and the function pointer from the vtable slot (slot 0 is
typeinfo, so method N lives at (N+1)*8), otherwise keep the static
symbol. Applied to all three receiver shapes in EmitRecordCallSret
(implicit-Self call, class-receiver method call, zero-arg field-access
call).
E2E coverage: TestRun_Native_IntfFromClassMethod now exercises an
override returning an interface; new TestRun_Native_RecReturnVirtualOverride
covers the record-returning case on both backends.
IntfVar := ClassObj.Method() failed with 'unsupported interface-field
assignment RHS' on the native backend (the QBE backend already handled
it). EmitInterfaceAssign only covered itab-dispatch receivers and plain
function calls in its sret branches.
Add EmitClassIntfSretMethodCall — sret buffer as hidden first arg
(%rdi), receiver in %rsi, static or vtable dispatch from the receiver's
class — and wire it into all three EmitInterfaceAssign regions
(implicit-Self field, sret Result/var-param, local/global LHS). Calls
with more than four user argument slots fail loudly until needed.
Covered by TE2ENativeTests.TestRun_Native_IntfFromClassMethod, which
exercises every LHS shape plus virtual dispatch on both backends.
Four itab-dispatch ABI gaps reported from Andrew Haines'
unify_backend_interface branch, fixed on both backends:
- Record by const/value through interface dispatch (QBE) passed the
record as 'w <addr>' — truncating to the low 8 bytes and shifting
every later argument. Dispatch sites now use the same :_ffi_<Name>
aggregate ABI as direct calls. (Native already passed the address.)
- var/out parameters through interface dispatch loaded the VALUE
instead of passing the slot address. Root causes: a 1-based loop
over the 0-based var-flag string in MethodParamIsVar (parameter 0's
flag was never seen), the QBE statement path ignoring var flags, and
the native push loops never emitting addresses. Strings and
dynarrays covered.
- TFuncCallExpr receivers (GetDriver(x).Info()) previously raised
fail-loud. The receiver call is sret-evaluated into a temporary fat
pair; the owned +1 obj is released right after the consuming call
(QBE: pending-release list flushed by every call emitter; native:
pair kept above the hoist region, released preserving result regs).
- Discarded interface-returning itab calls in statement position
clobbered memory through the register the callee expects to hold the
sret buffer (QBE: missing buffer; native: receiver passed where the
buffer belongs, shifting Self). Statement calls now get a throwaway
sret buffer and release the returned obj. Semantic records the itab
return type on TMethodCallStmt (bif-coverage clean).
e2e tests run on both backends (cp.test.e2e.imap); IR tests pin the
aggregate ABI, slot-address passing, and the discard-sret+release
sequence (cp.test.interfaces).
Two interface-dispatch bugs surfaced by Andrew Haines'
unify_backend_interface branch, fixed on both backends:
1. Interface value reads through a bare identifier (nil compares,
most visibly 'if Result = nil' inside an interface-returning
function) loaded a non-existent single slot. Interface idents now
load the obj half of the fat pointer: locals via the _obj slot,
globals via the Name_obj label, Result via the sret buffer
pointer. QBE additionally treats tyInterface as a pointer kind in
comparisons (ceql/cnel). The native backend previously compared
the sret buffer ADDRESS against nil — always false, silently.
2. V := Intf.Method(...) where Method returns an interface emitted a
single-slot store against the split-slot local (invalid QBE IR /
wrong native code). Itab-dispatched calls returning interfaces now
route through the sret convention like plain function calls: QBE
grows EmitIntfSretDispatch; native grows EmitIntfSretMethodCall,
wired into every interface-assignment LHS shape. Ownership follows
the existing convention (callee AddRefs into the sret buffer, the
caller takes the owned +1 pair).
Also fixes a latent bug found while testing: assigning an
interface-typed Result to a global stored the sret buffer pointer as
the obj half (native PushIntfIdentPair now dereferences correctly).
Still open, fail-loud (recorded in bugs.txt): TFuncCallExpr receivers
of interface calls, and discarded interface-returning itab calls in
statement position on the QBE path.
IR tests in cp.test.interfaces; e2e tests in cp.test.e2e.imap run the
registry-pattern acceptance program on both backends.
Ported from Andrew Haines' unify_backend_interface branch (97b4d6a9).
Both halves of the static-array element path missed the interface
fat-pointer layout: Arr[I] := IFaceVal fell through to the generic
element store, which picked storew against the 16-byte slot and left
itab uninitialised; F := Arr[I] emitted a single-slot load and a store
against the bare global name (undefined reference at link time). The
element slot is now treated as the contiguous obj+itab pair on both
paths, with class->element stores resolving the itab by name and
nil stores releasing the prior object.
Two additions to the original patch:
- The class->element store transfers ownership when the RHS already
owns +1 (ExprOwnsRef guard, mirroring EmitAssignment's class->iface
branch) instead of an unconditional _ClassAddRef that leaked one
reference per constructor-RHS store.
- The new TStringSubscriptExpr case in EmitInterfaceExprPair is
restricted to STATIC array bases: dynamic-array subscripts return a
loaded value, not an address, and keep hitting the fail-loud error
until they get their own handling.
Tests: the two IR tests from the original patch (read assertion updated
to the contiguous global fat-pointer layout that landed after Andrew's
base), plus an e2e test (TestRun_StaticArrayOfInterface_FatPointer)
covering store, dispatch through Arr[I], element-to-var copy,
element-to-element copy, and nil store at runtime.
TSectionMerger in the new blaise.linker.elf unit concatenates
like-named allocatable sections across input objects, padding each
contribution to its declared alignment, and records a placement
(merged section + offset) per input section — the basis for symbol
and relocation rebasing in Phase B.
SHT_NOBITS contributions advance the merged size without emitting
bytes; mixing NOBITS and PROGBITS under one name is an error.
Bookkeeping sections (symtab, strtab, rela, .note.GNU-stack,
.comment) are skipped — the linker rebuilds those itself — while
non-alloc .opdf.* debug sections are kept for the OPDF pass-through.
Tests cover text concatenation with placement offsets, alignment
padding between contributions, .bss size accumulation, and the
bookkeeping-section skip list.
First step of the internal-linker plan
(docs/internal-linker-design.adoc): blaise.elfreader parses ELF64
little-endian ET_REL x86-64 objects — section headers with contents,
the symbol table, and RELA relocation entries — and !<arch> static
archives with GNU long-name table support (blaise_rtl.a carries
member names beyond the 15-character ar limit).
Parsed entities are heap objects rather than records to sidestep the
known dynamic-array-of-record element-assignment hazard. The archive
API fills a caller-owned TList (generic function return types are not
supported yet).
TElfReaderTests covers: section bytes and flags from the internal
assembler's output, global function symbols, .quad relocations with
addends, NOBITS sections, bad-magic rejection for both formats, a
synthetic archive exercising the GNU long-name table and member
padding, and a sweep over every member of the real blaise_rtl.a.
Rec.Field[N] / Obj.Data[N] where the field is string-typed
(IsCharAccess) read the wrong byte:
- QBE backend: leftover 1-based lowering subtracted 1 from the index —
every read was off by one. The receiver base also ignored the leaf
shape (always loaded the slot as a pointer), which mis-addressed
inline record receivers. The branch now uses the same receiver
ladder as array-field subscripts and indexes 0-based.
- Native backend: had no IsCharAccess handling at all — the expression
fell through to the plain field-read path and returned the string
POINTER bytes instead of the subscripted character. Added the
missing branch (receiver ladder, load data pointer, movzbq at the
index).
Pinned by TestRun_Native_StringFieldCharRead, which runs on both
backends and covers record, class, expression-index, and
implicit-Self receivers. Found while parsing ELF symbol tables with
Ord(Sec.Data[Off + 4]) in the new linker reader.
- TAsmEncodingTests: byte-level regression tests against AssembleToBytes
(bare (%reg) operands, .quad symbol relocations, TLS prefix ordering,
PC32 addend with trailing immediates, branch relocations,
.note.GNU-stack presence, REX.X for extended index registers, imm16
width, unknown-directive/duplicate-label errors, line-numbered
diagnostics).
- TElfWriterTests: ELF header fields, section append/align/offset
tracking, symbol definition and lookup, BSS reservation.
- TInternalAsmE2ETests: compile->link->run through the compiler CLI
with --backend native --assembler internal, including class virtual
dispatch (vtable .quad relocations) and float arithmetic (SSE
spills). A compile failure now Fails the test rather than being
masked as a missing toolchain.
- docs/internal-linker-design.adoc: design for the next
toolchain-independence phase — an internal ELF linker built into the
blaise binary (PIE output, eager binding, .rela.dyn/R_X86_64_RELATIVE,
TLS, CRT discovery, FreeBSD/macOS outlook, OPDF pass-through).
Route A of the toolchain-independence plan: a built-in assembler that
parses the restricted AT&T subset the native backend emits and encodes
it into ELF relocatable objects, replacing the shell-out to GNU as.
New units:
- blaise.elfwriter: ET_REL ELF64 writer (.text/.data/.rodata/.bss/
.tbss, .symtab/.strtab, .rela.* with RELA addends). Always emits an
empty .note.GNU-stack section so linked binaries get a non-executable
stack, matching GNU as output.
- blaise.assembler.x86_64: two-pass assembler covering the backend's
full mnemonic set (ALU, MOV family incl. extensions, SSE scalar
ops, branches, calls, TLS) and directives.
Correctness points carried by the encoder:
- bare (%reg), disp(%base,%index,scale) and RIP-relative operands;
- .quad/.long <symbol>[+disp] emit R_X86_64_64/_32 relocations
(vtables, typeinfo, class-name records);
- the FS segment prefix is prepended ahead of REX for TLS operands;
- PC32 addends account for trailing immediate bytes (addend -8 for
movq $imm, sym(%rip));
- branches to external/other-section labels emit PLT32 relocations;
- REX.X is set for r8-r15 index registers on every mem-operand path;
- 16-bit moves emit imm16, not imm32.
Fail-loudly policy: unknown directives, duplicate labels, and
unsupported operand forms raise EAssembler with the source line number
and raw line text; Blaise.pas reports them as proper diagnostics.
The textual .s remains the canonical artefact; --assembler external
(the default) still shells out to GNU as.
Two wrong-code bugs in the x86-64 native backend, found while running
the internal assembler inside the native-compiled TestRunner:
1. var/out arguments passed to sret (record-returning) function calls
and indirect calls loaded the variable's VALUE (movslq) instead of
its ADDRESS (leaq), handing the callee a garbage pointer. The same
inline emission logic was duplicated across seven call paths, four
of which silently pushed a stale %rax for any non-identifier var
argument (e.g. Result.Field in an sret function). All paths now
share a single EmitVarArgAddrToRax helper which also handles
field-access arguments (including fields of the sret Result) and
fails loudly on unsupported forms instead of emitting garbage.
2. Assigning a record value into a record-typed field of the sret
Result (Result.Op1 := T) fell through to the scalar store path,
writing the source record's ADDRESS as an 8-byte field value. A
record-to-record-field assignment now performs the full ARC-aware
copy: retain source managed fields, release destination fields,
then memcpy of the record size.
Both bugs are pinned by new e2e tests that run on the QBE and native
backends (TestRun_Native_SretCall_VarParamArg,
TestRun_Native_SretResult_NestedRecordFieldAssign).
ParseFPCArgs only assigns SourceFile, OutputFile, SearchPaths and
OPDFEnabled; the FPC-style branch then defaulted just EmitIR/EmitAsm,
leaving DumpAST, DebugMode, Backend, Target, SkipDepCodegen,
EmitIfaceDir, Incremental and UnitCacheDir uninitialised — several of
which are read unconditionally later (UnitCacheDir, Backend, DumpAST).
Locals are not zero-initialised; the path only behaved because main's
frame lands on freshly mapped zero pages.
Diagnosed by Andrew Haines: a flow analysis flagged DumpAST as
potentially used before assignment, and the FPC-style branch is the
incoming path that proves the warning a true positive.
The program/unit's own name and the names of directly used units can
no longer be redeclared by top-level declarations, matching FPC and
Delphi (Duplicate identifier / E2004). Previously the module name was
stored on the AST node and never entered the symbol table, so
'program P; var P: Integer;' compiled by accident of omission.
The semantic pass plants skModule marker symbols in the scopes that
receive top-level declarations (global + program-block scope for
programs, unit scope for units); the ordinary duplicate check on
TSymbolTable.Define then rejects redeclarations. Lookup treats a
marker hit as unresolvable, so the reserved name is not a value and
inner scopes can still shadow it, as in FPC. The const paths get an
explicit marker check because their Define-failure branch otherwise
tolerates the clash silently (cross-unit const shadowing).
One deliberate divergence: FPC accepts a procedure named after the
program (an accident of its overload machinery); Blaise rejects all
declaration forms uniformly.
Grammar is unchanged — this is a name-resolution rule, not syntax —
so grammar.ebnf is untouched; the decision is recorded in
language-rationale.adoc. 44 test programs named 'program P' that also
declared an identifier P are renamed to 'program Prg'.
Closes#84
The array-field-subscript read path (Rec.Arr[I]) in EmitExpr dispatches
on the field's type kind — dynarray, static array, open array — with no
final else. A field kind outside those three would fall through with
Result unassigned (QBE: garbage temp name) or a stale %rax (native:
silent wrong value).
Unreachable today: the semantic pass only sets IsArrayAccess for those
three kinds. But the invariant lives in a different unit, so add an
explicit raise in both backends to turn any future mismatch into a
compile-time internal error instead of silently wrong code.
Diagnosis credit: Andrew Haines (issue #89) correctly identified that
the subscript-assign path loads through a var/out parameter's slot only
once — the slot holds the ADDRESS of the caller's variable, so element
writes landed in the wrong memory and corrupted the heap. His patch
covered the QBE dyn-array and class cases; the class half had already
landed independently (248dccf). This implements the remaining cases
across BOTH backends in the current tree.
Subscript writes through var/out params (TStaticSubscriptAssign gains
IsVarParam, set by the semantic pass; bif-coverage status updated):
- dynamic arrays: one extra dereference to reach the data pointer
(writes were lost and stray stores corrupted the heap; SetLength and
element reads already worked),
- static arrays: load the array address from the slot instead of
offsetting the slot itself (QBE wrote into the parameter slot region;
the native ident READ also produced garbage — pmVar now treated like
the other by-ref param modes),
- PChar: extra dereference before the byte store (writes were lost).
Interface var/out parameters (previously did not even compile: QBE
emitted loads from a non-existent %_var_G_obj; native mis-spilled the
single incoming pointer as a two-register fat pointer):
- interface variables now occupy ONE contiguous 16-byte fat-pointer
block (obj at +0, itab at +8). QBE locals: a single alloc8 16 with
the _itab name derived at +8; QBE globals: a single 16-byte data item
$Name_obj with the itab half addressed as $Name_obj + 8 (the separate
$Name_itab item could not be guaranteed adjacent). Native locals and
globals were already contiguous.
- new IntfObjAddr/IntfItabAddr helpers route every QBE obj/itab access
(assignment variants incl. weak, dispatch, expr-pair reads, as-out
binding); var/out params dereference the slot first.
- native: var-param-aware receiver load in EmitInterfaceCall, var-param
LHS in EmitInterfaceAssign (shares the sret-Result pointer path),
interface globals usable as var args (leaq Name_obj), and the call
slot counter treats a var interface arg as ONE pointer slot (it was
counted as two, desynchronising the argument register pops).
IR assertions updated for the new global fat-pointer layout. New e2e
tests: TestRun_VarParamDynArray_WriteAndGrow,
TestRun_VarParamStaticArray_PChar,
TestRun_VarParamInterface_DispatchAndReassign.
Closes#89.
Every recLineInfo record carried the program's main source file, so
'break unit.pas:NN' failed with 'No code found' for any line inside a
unit, and callstack frames attributed unit code to the program file
(found trying to break inside kanban.ui.pas in the kanban tool).
TDbgFunc gains a SourceFile field; the native backend stamps it from
TUnit.SourceFile while emitting each unit (empty for the main program,
which keeps the emitter's own fallback). The facts-mode line emission
writes each function's own file into its records. The recLineInfo
format already carries a per-record filename and pdr matches by
basename, so no format or debugger change is needed.
Verified: breakpoints by unit-file:line resolve and fire, parameters
print at the stop, and callstacks name the correct file per frame.
compiler/project.xml now passes --backend native --debug-opdf for the
blaise-compiler module, so local builds of the compiler and TestRunner
exercise the native x86-64 backend end-to-end and are debuggable with
pdr. The shipped default backend remains QBE.
Compiling the compiler itself natively (never done before) exposed two
pre-existing native codegen bugs, both fixed:
- Local dynamic-array variables were not zero-initialised. SetLength
reads the old data pointer and the epilogue releases it, so stack
garbage in the slot corrupted the heap — the unit→.o iface-embed
path crashed in the allocator. Dyn-array locals now start nil like
string/class/record locals. New e2e test
TestRun_Native_LocalDynArray_DirtyStack pins this with a
dirty-stack helper.
- P[I] := Chr(N) stored the low byte of the _Chr-allocated STRING
POINTER instead of N: the native backend lacked the QBE backend's
EmitByteRhs short-circuit. Added EmitByteRhsToEax (Chr folds to its
argument, single-char literals to their ordinal) and applied it at
every byte-sized store site (PChar subscript, dyn/static array
byte elements, P^ byte writes, byte array-field elements). This
corrupted every ELF header patch in uElfObject, producing .o files
with garbage e_shoff/e_shnum.
With both fixes the full separate-compilation round trip works under
the all-native toolchain and the complete suite passes (2959 tests)
with a natively-built compiler and TestRunner.
uDebugOPDF: interface-typed globals are two labels (Name_obj/_itab);
recGlobalVar now references Name_obj so --debug-opdf links for
programs with interface globals (e.g. the compiler's own CG).
pdr now resolves the ASLR slide correctly (load base from the
binary's offset-0 mapping), so the -no-pie guard in both native link
paths is no longer needed. Debug binaries are position-independent
again, matching the platform default.
Verified under live ASLR: breakpoints, var-param drilldown, captured
vars, dynamic arrays, TList<T> inspection, callstack and stepping all
work against PIE binaries.
Remove the 'PIE (ASLR) support in PDR' section from
future-improvements.adoc — implemented.
OPDF/native debugging:
- uDebugFacts: TDbgVar.Indirect — the frame slot holds the value's
ADDRESS rather than the value (var/out params, captured outer locals).
- uDebugOPDF: emit LocationExpr=3 'RBP-relative indirect' for such
slots; mangle generic instance names in class/property records
(vtable_TList_Integer — raw 'TList<Integer>' broke the assembler);
set OPDF_FLAG_DYNARRAY_LEN32 in the header so debuggers read the
[refcount:Int32][length:Int32] dynamic-array header correctly.
- native: DbgMarkParams marks var/out params and _cap_ capture slots
indirect; captured vars are typed from the enclosing declaration
(OuterVarType via FDbgOuterDecl).
Verified with pdr: var-param record AND class field drilldown inside
the callee, captured-var inspection in nested procedures, dynamic
array printing and subscripting, TList<T> instance inspection.
Codegen bugs uncovered by the debug probes, fixed in both backends:
- var-param class field read/write used a single deref: the slot holds
the caller variable's address, so code treated that address as the
instance pointer — reads returned garbage and writes corrupted the
caller's frame. All field-access leaf sites now load twice when
IsClassAccess and IsVarParam (QBE: EmitInstancePtr/EmitLValueAddr/
EmitFieldAssignment/EmitExpr leaves; native: the parallel
RecordName-based receiver loads).
- QBE nested-proc signatures emitted a double comma when a proc has
captured vars AND regular params (l %_cap_X, , w %_par_K).
- native: assignments to captured string/dynarray/class/record/float
vars wrote to a global of the same name instead of through the
capture pointer; captured float reads did the same.
- native: 'var D: Double' params were classified as xmm args on both
sides of a call — the pointer arrived in an xmm register and the
callee stored through garbage. Var-param floats now travel in
integer registers like every other by-ref param.
New e2e tests: TestRun_VarParamClass_FieldReadWrite,
TestRun_NestedProc_CaptureAndParams, TestRun_VarParamFloat_CapturedFloat.
Three gaps between the OPDF data and a realistic multi-unit program:
- Type records for unit-defined classes referenced bare symbols
(vtable_TStringList, TStringList_SetText) while the emitted symbols
carry the owning-unit prefix — any program using TStringList failed
to LINK under --debug-opdf. New MangledClassSym mirrors the
codegen's ClassSymName allowlist (System / rtl.* / blaise_* /
program-scope stay bare) for VMTAddress and property accessor
addresses.
- Self was recorded untyped (its slot is allocated as a raw pointer),
so 'print Self.FField' failed. DbgMarkParams now types Self as the
owning class — pdr dereferences class-typed variables, so instance
field drilldown works. The unit-less driver path now passes the
program's symbol table to the backend (the analyser, which the
table's uses-chain provider points at, is still alive there; the
in-test fallback variant dangled and was reverted).
- Value record parameters present their '_data' shadow slot (the
callee's inline copy) as the parameter instead of the raw ABI
pointer slot — record fields are inline at known offsets, which is
exactly what field access needs.
Verified with pdr against a program using classes+sysutils units:
print P.X / P.Y (record param fields), Self.FCount / Self.FName
(instance fields, string rendering), Msg (local string) all correct.
Suite: 2955 OK; FIXPOINT_OK; NATIVE_FIXPOINT_OK.
The OPDF emitter was a standalone AST walk that GUESSED runtime facts:
local offsets ignored parameter slots and native slot sizing, every line
record pointed at the function entry, and HighPC was a next-function
approximation. The native backend owns all of those exactly — so it now
produces them.
New uDebugFacts unit: under --debug-opdf the native backend records, per
emitted function, the assembly symbol (LowPC), an end label after the
final ret (exact HighPC), every frame slot with its real %rbp offset
(parameters flagged with var/const, internal bookkeeping slots
filtered), and a .Ldbg_N local label per statement with its source
line/column. TOPDFEmitter gains a facts mode that emits scopes, params
(plus a locatable recLocalVar per parameter), locals and per-STATEMENT
recLineInfo from those facts; without facts (QBE backend — QBE assigns
frames and addresses itself) it keeps the approximate AST walk.
The driver appends the OPDF section to the SAME assembly file for the
native backend: local labels resolve within one object, so nothing needs
exporting. Debug links pass -no-pie (OPDF stores absolute addresses;
the pdr loader does not yet apply a PIE slide).
Verified end to end with the pdr reference debugger against a native
binary: 'break file.pas:line' hits mid-function, 'step' advances one
source line at a time, 'print <param>/<local>' reads correct values via
the real RBP offsets, and 'callstack' shows named frames with lines
(TThing_Bump at dbg.pas:11 <- main at dbg.pas:25). None of these worked
before. Normal builds are byte-identical (labels only under OPDF mode;
both fixpoints clean).
Tests: facts-mode emitter assertions (exact HighPC, real offsets,
statement-label line records) + native backend facts collection
(.Ldbg labels, param/local offsets, label-free normal builds).
Suite: 2954 OK; FIXPOINT_OK; NATIVE_FIXPOINT_OK.
Three OPDF emitter gaps from the debugger-feedback backlog:
- Class and record METHODS (destructors included) never got
recFunctionScope records: after semantic analysis the implementation
bodies live on the type declarations' method lists, while
EmitFunctionScopes only walked Block.ProcDecls (whose impl entries are
body-less matching stubs). The walk now gathers both sources; method
records carry the qualified label (TThing_Bump) as the record name —
the symbol a debugger resolves for break-by-function-name — and as
LowPC. Plain functions are unchanged (label = name).
- Under --debug-opdf every function symbol is now exported (ExportPrefix
and EmitFuncDef honour FOpdfMode): the .opdf companion is assembled as
a separate object whose scope records reference function labels, so
local symbols failed to link the moment a method got a scope record.
Same rationale as the existing VTableDataPrefix export.
- Generic-instance vtables bypassed VTableDataPrefix and stayed local
under --debug-opdf.
Verified end to end: a program with a virtual-method class compiles,
links and runs with --debug-opdf (previously the documented blocker).
Still open (bugs.txt): per-statement line records need codegen-emitted
statement labels — the line table remains function-start granularity.
Tests: TestOPDF_MethodScope_Emitted + TestOPDF_DestructorScope_Emitted.
Suite: 2950 OK; FIXPOINT_OK; NATIVE_FIXPOINT_OK.
obj.Field := MakeClass() leaked one reference per store: class-returning
functions and methods uniformly transfer +1 (the callee retains into
Result and never releases it — verified for constructor results, locals,
globals, fields and chained calls on both backends), but the class-field
store sites retained unconditionally on top of that transfer.
The ExprOwnsRef guard is now applied to every class-field store:
QBE EmitFieldAssignment, native TFieldAssignment (ObjExpr, sret-Result,
RecordName branches) and the native implicit-Self assignment — matching
the long-guarded string/dyn-array paths and QBE's implicit-Self path.
The historical blocker (a stage-2 compiler built with this guard
corrupted its own heap) was a TAIL of pre-existing over-releases that
the tighter refcounts exposed. Found and fixed with a temporary
negative-refcount abort in _ClassRelease:
- Typed exception handlers injected one synthetic local PER 'on E:'
clause: two same-named handlers double-allocated the slot and emitted
one epilogue release each, while only the matched body bound (+1) —
the exception's refcount went negative on scope exit. The synthetic
local is now deduplicated by name (uSemantic).
- Handler re-binding leaked the previous binding: the shared slot is now
released before each bind (both backends).
Verified shapes: ExprOwnsRef=True can only be user functions/methods/
getters/indirect calls — inlining excludes class returns and no external
returns a class, so every owning report is a genuine +1.
Compiler self-compile leak count drops 14 -> 6 objects. Tests:
TestDebug_ClassFieldFromCall_NoLeak + TestDebug_MultiHandlerVar_
NoOverRelease (leak-tracker asserted, both backends). Suite: 2948 OK on
working and fixpoint binaries (stage-2); FIXPOINT_OK; NATIVE_FIXPOINT_OK;
full native TestRunner OK.
The FTypeXxx caches (FTypeInteger, FTypeString, ...) held an extra
retained reference on top of FAllTypes' owning reference — NewType
returns +1 and the strong field store retained again, so every builtin
TTypeDesc ran one refcount high for the whole compilation.
With the [Unretained] store lowering in place (the owned +1 from the
NewType call is released at the assignment site), the caches are now
plain borrowed views into the type pool, which matches the ownership
story: FAllTypes owns the single reference, everything else borrows.
A previous attempt at this marking (pre-dating the Unretained
temp-release lowering) regressed leak counts; with the prerequisite in
place the leak report is unchanged (14 residual objects, none of them
type-pool related) and all gates pass on a stage-2 rebuild.
Suite: 2946 OK (stage-2 binary); FIXPOINT_OK; NATIVE_FIXPOINT_OK;
full natively-compiled TestRunner OK (2946).
Resolves the TStringList.Objects / TList<T> retention question from the
bug backlog as an explicit design decision (language-rationale,
'Collection Ownership'):
- TList<T>/TStack<T>/TQueue<T> managed elements ARE retained on store —
this already works (generic stores lower to ARC-aware pointer writes);
TE2ETListTests.TestRun_TList_ClassElements_RetainedAcrossScope now
pins it on both backends (object survives its creating scope).
- TStringList.Objects[] stays NON-OWNING by design: the API type is
Pointer and the integer-cast idiom (TObject(PtrUInt(N)), used in 27
places in the compiler itself) makes blind retention impossible.
Convention documented at the declaration and in the rationale.
- Known limitation recorded: generic Clear/Destroy do not yet release
remaining managed elements (needs a Default(T)-style zero-store);
tracked in the leak backlog.
Suite: 2946 OK; FIXPOINT_OK; NATIVE_FIXPOINT_OK.
Two native miscompiles, found chasing the generic-template in-memory
bif round-trip crash (TIfaceIOTests.TestRoundTrip_Generic*_TemplateParams):
1. .Free() on an l-value receiver expression (Def.ClassDef.Free()) only
released — the slot was not nilled (the QBE lowering stores 0). The
stale pointer aliased the next same-size allocation, so the following
ARC field store double-released the NEW object; its owned lists died
while still referenced (ReadGenericClassPayload's Free+Create+assign
dance left ClassDef.Fields freed → nil deref in the test). Field and
ident receivers now release AND nil via EmitLValueSlotAddr; other
expression receivers keep the release-only behaviour.
2. A record-returning call assigned into an array element
(t[i] := FLexer.Next()) evaluated the RHS via EmitExprToEax, which
cannot pass the hidden sret pointer — the callee wrote its Result
record through garbage (TLexerTests crashed; the suite had never run
natively because the round-trip crash aborted earlier). Both
subscript-assign branches now sret record-returning calls directly
into the element after releasing the old element's managed fields.
The FULL natively-compiled TestRunner passes for the first time:
2944 tests, 0 failures (previously aborted at TIfaceIOTests).
Tests: TNativeConstArgTests.TestFree_FieldReceiver_NilsSlot (asm-level)
+ TE2ERecordsTests.TestRun_RecordCallResult_IntoArrayElement (both
backends). Suite: 2945 OK; FIXPOINT_OK; NATIVE_FIXPOINT_OK.
H.S.Note(); failed semantic analysis with "Class 'IShape' has no method
'Note'" — AnalyseMethodCall's receiver-expression path accepted
interface-typed receivers in its kind check but then resolved the method
against TRecordTypeDesc unconditionally. Interface receivers now get
their own branch (mirror of the implicit-Self interface path): validate
against the interface's method list and mark the call for itab dispatch.
The QBE backend already lowered the shape; the native EmitMethodCallStmt
site additionally failed to pass ObjExpr to EmitInterfaceCall, emitting
bogus _obj/_itab operands from the empty ObjectName — it now forwards
the receiver expression like EmitMethodCallExpr does.
Test: TE2EClasses2Tests.TestRun_InterfaceFieldCall_StatementPosition
(both backends). Suite: 2943 OK; FIXPOINT_OK; NATIVE_FIXPOINT_OK.
Open-array arguments were lowered correctly only at standalone
procedure/function call sites. Method calls (statement and expression
paths, static and virtual) and constructor calls emitted just the data
pointer — the callee read its (data, high) pair shifted, so High(A) and
the elements were garbage (F.Sum([10, 20, 30]) printed junk; the
statement form printed a wrong count).
New OpenArrayArgFragment helper emits the 'l data, l high' pair for
inline array literals, static-array coercions, and open-array parameter
forwarding; the four method/constructor argument loops now use it.
The native backend already handled all these shapes.
Test: TE2EOpenArrayTests.TestRun_OpenArray_MethodAndCtorParams —
constructor, function and procedure (statement) open-array params on a
class, both backends. Suite: 2942 OK; FIXPOINT_OK; NATIVE_FIXPOINT_OK.
A function-of-object variable called in EXPRESSION position went through
EmitCallIndirect, which loads only the Code pointer and passes the first
user argument in %rdi — Data (Self) from offset +8 of the TMethod block
was never loaded and the argument registers were not shifted. The
STATEMENT path already dispatched on IsMethodPtr to EmitMethodPtrCall;
the expression path now does the same.
Repro: any 'function(N: Integer): Integer of object' variable call in an
expression — QBE was correct, native segfaulted.
Test: TE2EMiscTests.TestRun_FunctionOfObject_IndirectCall (both
backends). Suite: 2941 OK; FIXPOINT_OK; NATIVE_FIXPOINT_OK.
Interfaces may declare properties (FPC/Delphi parity):
IValued = interface
function GetValue(): Integer;
procedure SetValue(AValue: Integer);
property Value: Integer read GetValue write SetValue;
end;
Accessors must be methods of the interface or an inherited parent
(interfaces have no fields), validated at registration. I.Value reads
lower to the existing zero-arg getter itab dispatch; I.Value := X
lowers to the setter dispatch with X as the single argument — pure
compile-time sugar, no itab slots, no layout change. Child interfaces
see inherited properties via the parent chain. Wired end to end:
parser, TInterfaceTypeDef.Properties (AST + clone), TInterfaceTypeDesc
property registry, semantic read/write resolution, both backends, .bif
serialisation and import registration. v1 limits (recorded in
language-rationale): plain interface-typed receivers; no indexed/
default array properties.
Two pre-existing linking bugs surfaced by the dogfood program and are
fixed alongside:
- Property accessor names written in a different case than the method
declaration (read getValue for GetValue) produced unresolved symbols —
accessor names are now normalised to the declared casing at
registration (classes and interfaces, compile and import paths).
- A program-level class implementing an interface failed to link
whenever the program had a uses clause: program-scope methods carry
bare symbol names (uSemantic.CurrentUnitPrefix) but itabs and
property-setter call sites prefixed them with the program name via
Sym.OwningUnit. ClassUnitPrefix (QBE) / ClassSymName (native) now
skip the prefix for program-owned classes (new FProgramName field).
Tests: 7 in cp.test.interfaces (parse, registration, accessor
validation, read-only enforcement, inheritance, IR dispatch), bif
round-trip in cp.test.unitinterface, 2 e2e suites on both backends in
cp.test.e2e.classes2 (interface read/write incl. compound assignment
and inherited dispatch; case-mismatch + uses regression). Suite: 2940
OK on working and fixpoint binaries; FIXPOINT_OK; NATIVE_FIXPOINT_OK.
Sin(12) and Tanh(I) now compile: the float compiler builtins (Sqrt,
Ceil/Floor/Round/Trunc, Ln/Log2/Log10, Power, the trig family, IsNaN/
IsInfinite) accept integer-family arguments and widen them to Double —
matching FPC/Delphi and Blaise's own implicit int→float assignment
rule. The trig builtins return Double for integer arguments (still
Single→Single / Double→Double for float arguments). Power previously
type-checked nothing and emitted invalid IR for integer (and Single)
arguments; it now validates and coerces both to double.
Double(I) / Single(I) typecasts were lowered as bit copies: QBE emitted
an integer temp into 'stored' (rejected by qbe), native stored an
integer register — so the Tanh(Single(I)) workaround failed too. Float
casts now emit real conversions on both backends (swtof/sltof/ultof/
uwtof + exts/truncd on QBE; cvtsi2sd/ss + cvtss2sd/cvtsd2ss natively),
including float→float width changes.
Fixing this exposed two pre-existing native float-width bugs: a Single
RHS assigned to a Double variable was stored without cvtss2sd, and
mixed-width binary operands (s * 1000 — integer literals are emitted as
.double constants) ran the Single binary path at the wrong width.
Added EmitXmm0WidthAdjust and applied it at assignment and to both
binary operands.
Decision recorded in docs/language-rationale.adoc (Implicit
Integer→Float Widening). Tests: 8 IR/semantic tests in cp.test.math
(the two RejectInteger tests inverted to acceptance) + 2 e2e tests on
both backends in cp.test.e2e.math. Suite: 2930 OK on working and
fixpoint binaries; FIXPOINT_OK; NATIVE_FIXPOINT_OK.
Array-typed FIELDS were second-class citizens for element access; this
lands the full variation family on both backends:
- Semantic: r.A[i] := v dropped the subscript from the LHS type — the
parser stores it in TFieldAssignment.PropIndexExpr (the indexed-
property slot) and semantic ignored it for real fields, demanding the
whole array type on the RHS. A subscript on a real dyn/static-array
field is now an ELEMENT write (new semantic-set IsElemWrite flag);
both backends emit the element store with the standard ARC and
record-copy rules.
- Parser: c.N.A[i] := v failed with "Expected 'end' but got '['" —
the chained L-value walker now accepts a terminating Field[idx] :=,
and the subscript-chain path accepts arr[i].A[j] := v (subscript
directly over another subscript stays a clear parse error).
- Bare implicit-Self: A[i] := v inside a method raised "Undeclared
variable 'A'" — TStaticSubscriptAssign now resolves array-typed
fields of Self (IsImplicitSelf + ImplicitFieldInfo).
- SetLength(r.A, n): QBE refused ("first argument must be a
variable"); native silently emitted NO code for field receivers and
mis-stored through var-param receivers. QBE routes through
EmitLValueAddr; native gains EmitLValueSlotAddr covering field,
var-param and implicit-Self receivers for dyn-array and string
SetLength.
- Read side: c.A[i] through a class variable computed the element base
as if c were an inline record (missing object-pointer load) and
segfaulted; implicit-Self bases had the same gap; native missed
chained reads (c.N.A[i]) entirely. All base shapes are handled in
the IsArrayAccess read paths of both backends now.
bif-coverage.status regenerated for the new semantic-set AST fields
(safe). Tests: 4 IR tests (cp.test.dynarray) + 6 e2e tests on both
backends (cp.test.e2e.records) covering record/class/implicit-Self
receivers, nested chains, static-array fields and string-element ARC.
Suite: 2922 OK on working and fixpoint binaries; FIXPOINT_OK (single
round); NATIVE_FIXPOINT_OK.
Dynamic arrays of records were broken in three interlocking ways:
- Parser: a[i].Field := v (and a[i].Method, chained a[i].F.G := v) on
the statement LHS raised "Expected ':=' but got '.'" — the subscript
statement branch only accepted ':=' directly after ']'. It now
builds a subscript-rooted postfix chain ending in a TFieldAssignment
(via ObjExpr) or TMethodCallStmt (via ObjExpr). grammar.ebnf gains
SubscriptFieldAssign / SubscriptMethodCall rules.
- Both backends: a[i] := r stored the ADDRESS of r into the element
instead of copying the record, and element reads loaded the first
8 bytes of the element as if it were a pointer. The two bugs masked
each other (elements aliased r — the TElfSection workaround comment
in uElfObject.pas documents the symptom). Record-element subscript
reads now yield the element address (dyn/open/static arrays) and
writes do an ARC-aware fieldwise copy: EmitRecordCopy on QBE,
retain-src/release-dest/memcpy on native. Native static arrays of
records had the same read/write bug and are fixed too.
- QBE backend: Exit inside the SECOND (or later) try block of a
function skipped _PopExcFrame, leaving a stale g_exc_top that
corrupted later raises/pops. EmitTryFinallyStmt/EmitTryExceptStmt
emitted normal and exception paths sequentially but decremented the
codegen-time FExcDepth on both (net -1 per try statement). Ported
the native backend's rebalancing (restore depth before emitting the
exception path). This is the likely root cause of the historical
'avoid bare Exit inside try' convention.
Tests: 3 IR tests (cp.test.dynarray), 4 e2e tests on both backends
(cp.test.e2e.records), IR pop-count + e2e regression for the exc-frame
bug (cp.test.exceptions, cp.test.e2e.exceptions). Suite: 2912 OK on
working and fixpoint binaries; FIXPOINT_OK; NATIVE_FIXPOINT_OK.
Records how the landed implementation differs from the plan: a single
unified hoist region (EmitArgHoist) for open-array literals,
record-call sret buffers and string pin slots, and shape-based
protection at unknown-signature sites instead of a TInterfaceTypeDesc
const-flag extension.
Port the QBE backend's shape-aware const-string argument convention to
the native x86-64 backend: const params of any kind no longer get the
callee entry-retain/exit-release pair; the caller protects string
arguments by shape (borrowed / consume / pin — see
blaise.codegen.arcshapes.TConstArgMode). Both backends now share one
convention, which also makes native calls into the QBE-compiled RTL
consistent for const params.
The pin slots live in a unified pre-call hoist region (EmitArgHoist)
shared with open-array literal blocks and — new — the sret buffers of
record-returning call arguments. Every call path emits the same
region + post-call epilogue (string releases, record-temp field
releases, reclaim) with the call's return registers preserved:
direct calls, method calls (both strategies), constructor calls,
interface dispatch (var flags from TInterfaceTypeDesc mark positions
to skip; shape protection is applied to every string-typed value arg
since const-ness is invisible there), proc-pointer and method-pointer
calls (TProcParamInfo const flags), inherited calls, and the three
sret call paths (dest pointer saved below the region so %rsp-relative
dest operands cannot drift).
The address-taken walkers move to the shared unit
blaise.codegen.arcshapes (QBE IR output is byte-identical); the native
backend rebuilds its borrowed-argument blocklist per function from
them, mirroring the QBE backend's FConstArgUnsafe.
Bugs found by the new tests and the natively-compiled TestRunner,
fixed here (native backend, all pre-existing):
- record-returning calls as arguments interleaved their sret buffer
between pushed argument slots (garbage args); the hoist fixes this
and EmitSretArgReleases is gone
- callee prologue copied record/static-array value params via memcpy
before spilling later argument registers (memcpy clobbered them);
spills now run in two phases
- >6-slot method calls forwarded var/out params by slot address
instead of the held pointer (no ParamMode check)
- TStringLiteral ignored IsCharCoerce: S[I] = ',' compared the
literal's data pointer against the byte
- EmitNarrowToType truncated pointer-shaped values (class casts) to
32 bits
- IntByteSize defaulted nil/unmapped kinds to 4 bytes (QBE uses 'l');
forward-class fields loaded half a pointer
- EmitCallIndirect/EmitMethodPtrCall loaded the target into %r10/%r11
before argument evaluation (caller-saved clobber window)
- EmitCall's push/pop loop used a stale ParamType from the counting
loop
Tests: cp.test.nativeconstarg (12 asm-level shape tests incl. the
callee-pair flip) and cp.test.e2e.constarg (21 runtime parity tests on
both backends). Full suite 2903 OK on the working and fixpoint
binaries; FIXPOINT_OK; NATIVE_FIXPOINT_OK; natively-compiled
TestRunner green except two pre-existing generic-template round-trip
failures (bugs.txt).
With the default-argument statement bug fixed, the borrowed-local
elision blocked in 91f44ec is sound and enabled: a const-string argument
that is a plain local or by-value/const parameter variable emits no
caller pin at all — the frame's own reference outlives the call.
Exclusions guard the aliasing edges: address-taken locals (explicit @,
passed to var/out params), locals captured by nested procedures (both
via a per-function blocklist rebuilt in EmitVarAllocs), and calls whose
signature has a var/out string param (F(L, L) lets the callee release
L's buffer through the alias). Nine IR contract tests cover every
shape, including the new exclusion guards.
Tail wins on the remaining profile:
- QBEMangle: fast path returns the input unchanged when it contains no
mangled characters (1.3M calls were rebuilding clean names one concat
per character).
- TStringList.FindSorted: compares the probe slot in place through the
CompareStr/CompareText builtins instead of copying it into a local
and dispatching through Compare — several ARC ops per binary-search
step on the hottest lookup path.
- _StringFormatN: the sizing pass uses a pure DecimalWidth count
instead of rendering every %d argument twice.
Compiler self-compile: 0.78 s -> 0.60 s wall, 8.60G -> 6.45G
instructions. Verified through two self-hosted generations, gen-2
native emission, both fixpoints and the full suite (2870 tests).