Commit graph

824 commits

Author SHA1 Message Date
Graeme Geldenhuys fb0baa229f docs: mark internal linker Phases C and D as implemented 2026-06-19 10:51:09 +01:00
Graeme Geldenhuys 3d5fdf86c8 test(linker): add internal linker e2e tests via --linker internal
Four tests exercise the full compiler-driver pipeline with
--backend native --assembler internal --linker internal:
HelloWorld, StringOps (Format/UpperCase/IntToStr), exception
handling (raise + try/except), and virtual method dispatch.

These catch regressions in TLS relocation, CRT discovery,
duplicate-symbol handling, and PIE emission.
2026-06-19 10:48:04 +01:00
Graeme Geldenhuys db309c85ea feat(linker): wire internal linker into compiler driver + fix TLS relocations
Add --linker internal|external option to the native backend driver,
routing the link step through TLinker instead of shelling out to cc.
FindCrtObjects discovers CRT startup objects (Scrt1.o, crti.o, crtn.o,
crtbeginS.o, crtendS.o) on Debian and RedHat layouts.

Fix SHF_TLS constant from $200 to the correct ELF spec value $400 in
both the reader and writer — the wrong value caused .tbss sections to
be placed twice (once in the normal NOBITS loop, once in the TLS
section), producing doubled negative TPOFF32 offsets and segfaults.

Fix FTlsAddr to use the post-alignment address from PlaceSection rather
than the pre-alignment cursor, which caused off-by-padding TPOFF values.

Change duplicate global symbol handling from error to first-wins
semantics (matching ld behaviour), needed because the main program
object and archive members may both define system runtime functions.

Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK,
3532 tests pass on both QBE-built and native-built test runners.
2026-06-19 10:43:33 +01:00
Graeme Geldenhuys 078dc81e46 feat(linker): Phase C dynamic linking — GOT/PLT, CRT objects, PIE emission
Implement Phase C of the internal ELF linker: produce position-independent
ET_DYN executables linked against libc through dynamic GOT/PLT dispatch.

New capabilities:
- SetDynamic(True) switches the linker to PIE mode
- AddCrtObject() loads system CRT startup objects (Scrt1.o, crti.o,
  crtn.o, crtbeginS.o, crtendS.o)
- CollectExternals scans undefined non-weak symbols and creates PLT entries
- CollectGotSlots creates non-PLT GOT slots for GOTPCREL references to
  locally-defined symbols (e.g. main, __dso_handle)
- BuildDynamic generates .interp, .dynstr, .dynsym, SysV .hash, .got
  (3 reserved + N PLT + M non-PLT slots), .plt (header + stubs),
  .rela.plt (JUMP_SLOT entries)
- LayoutDynamic assigns PIE virtual addresses (base 0): RO metadata →
  .text/.plt → RELRO (.init_array, .fini_array, .dynamic, .got) →
  .data/.bss, with page-aligned PT_LOAD boundaries
- EmitDynExecutable writes the full PIE byte image with ELF header,
  10 program headers (PHDR, INTERP, 4×LOAD, DYNAMIC, TLS, GNU_STACK,
  GNU_RELRO), and section data at virtual offsets

Relocation handling extended:
- R_X86_64_64 → R_X86_64_RELATIVE in .rela.dyn
- R_X86_64_32/32S for absolute symbol references
- R_X86_64_GOTPCREL/GOTPCRELX/REX_GOTPCRELX → GOT slot lookup
- R_X86_64_TPOFF32 → TLS offset calculation

Tests: TDynLinkerTests (2 structural), TDynLinkerE2ETests (1 E2E that
links a main() calling write via PLT, runs the binary, asserts exit
code 42 + correct stdout). All 3528 tests pass.
2026-06-19 10:00:08 +01:00
Graeme Geldenhuys 8f38fd1416 feat(stdlib): add CodePointToString UTF-8 encoder to strutils
Adds CodePointToString(CP: Integer): string, the inverse of CodePointAt /
CodePointFromByteIndex.  It encodes a Unicode codepoint (0..U+10FFFF) as
its 1..4 byte UTF-8 sequence, providing the codepoint-aware replacement for
the Char(n) cast used in other Pascal dialects.  Chr(n) only writes a single
raw byte, which is invalid for codepoints above 127; CodePointToString emits
the correct multi-byte form.  Out-of-range values yield an empty string.

E2E tests cover single-byte ASCII, the two-byte 0xC2 0xB1 encoding of U+00B1,
a round-trip across all four UTF-8 length classes, and out-of-range handling.
2026-06-19 09:28:53 +01:00
Graeme Geldenhuys 12613ab68e docs: mark internal linker Phases A and B as implemented 2026-06-19 07:56:13 +01:00
Graeme Geldenhuys 0d47c5f996 feat(backend): default to native backend; fix codegen bugs exposed by the switch
The native x86-64 backend is now the default (bkNative).  QBE remains
available via --backend qbe.  Several codegen bugs that were invisible
while QBE was the default are fixed in this commit:

Native backend fixes:
- EmitLeaqGlobal helper: threadvar globals now emit the correct
  fs:0 + @tpoff TLS sequence instead of bare %rip-relative leaq.
  Fixes static array write (FreeLists), string subscript assignment,
  EmitExprAddr, record memcpy, method-ptr cast, and open-array push
  paths that all used raw leaq %s(%rip).
- EmitIncDec: unified local/global paths through VarOperand so
  threadvar scalar Inc/Dec (e.g. LargeFreeCount) gets TLS addressing.
- EmitIncDec FAE.Base: Inc(P^.Field) where P is a pointer now
  evaluates the base expression instead of leaq on the stack slot.
- FinalizeEmit: data section (.data/.bss/.rodata) now emits once at
  the end of unit compilation via FinalizeEmit, not inside each
  EmitUnit call.  Fixes duplicate/missing string literals in sep-compile.
- EmitFunctionDef AExported: implementation-only helpers in monolithic
  mode stay file-local (.globl suppressed) to avoid symbol collisions
  with the RTL archive.

QBE backend fixes:
- AppendUnit typeinfo: export prefix added to data $typeinfo_TObject,
  $typeinfo_TCustomAttribute, $vtable_TObject, $vtable_TCustomAttribute
  so native-built RTL can resolve them.
- FSuppressSystemDefs: decoupled from FExportAll so CreateUnitCodeGen
  can export all symbols without suppressing system defs.

Infrastructure:
- runtime/Makefile: BLAISE_FLAGS variable for passing extra compiler
  flags (e.g. --backend qbe) from build scripts.
- fixpoint.sh: make clean before RTL rebuild to pick up backend change.
2026-06-19 04:27:41 +01:00
Graeme Geldenhuys 070a229e20 Revert "fix(native): suppress duplicate built-in system defs in unit-to-.o compilation"
This reverts commit 87c17ee589.
2026-06-19 00:26:53 +01:00
Graeme Geldenhuys 60217f9026 Revert "feat(backend): default to the native backend; QBE is now opt-in"
This reverts commit d56bdbf84f.
2026-06-19 00:26:53 +01:00
Graeme Geldenhuys d56bdbf84f feat(backend): default to the native backend; QBE is now opt-in
Flip the compiler's default --backend from qbe to native.  Native has full
feature parity with QBE (the v0.11.0 cycle's 116 fixes plus the dual-backend
e2e harness brought them level), self-hosts, and gives no external codegen
dependency, source-level debugging, and a path to cross-compilation — so it
is the right default to exercise everywhere.

QBE stays fully supported via --backend qbe and remains the cold-bootstrap
root: --emit-ir still routes to QBE regardless of the default (PickTopDriver
ignores --backend for emit modes), so scripts/rolling-bootstrap.sh,
fixpoint.sh, and CI — all of which pass --emit-ir explicitly — are unaffected
by this change.  The only behaviour change is that a plain
'blaise --source X --output Y' (and a plain 'make' in runtime/) now uses the
native backend.

Flipping the default surfaced and fixed a real native gap first (the
unit-to-.o duplicate-system-defs link collision, previous commit); the full
suite is green with the TestRunner built by BOTH a QBE-built and a
native-built compiler, and all three fixpoints pass.
2026-06-18 18:43:23 +01:00
Graeme Geldenhuys 87c17ee589 fix(native): suppress duplicate built-in system defs in unit-to-.o compilation
Compiling a unit to a standalone .o with the native backend
(blaise --source U.pas --output U.o) emitted the built-in system
definitions — typeinfo_/vtable_/_FieldCleanup_ for TObject and
TCustomAttribute — into the unit .o.  Linking that .o against a consumer
program (which emits its own copies) failed with
'multiple definition of vtable_TCustomAttribute'.

Two gaps fed this:
- The unit-as-top-level path in Blaise.pas built its codegen via
  Driver.CreateCodeGen (the program path, separate-compile = False) instead
  of Driver.CreateUnitCodeGen (separate-compile = True), so the backend's
  FSeparateCompile gate (which already suppresses the system defs) never
  engaged.  Switched the IsUnitMode branch to CreateUnitCodeGen, with a
  CreateCodeGen fallback for any backend lacking a unit codegen.
- TCodeGenNative.GenerateUnit did not propagate FSeparateCompile to the
  backend (AppendUnit already did); added the missing propagation for
  defensive correctness.

The QBE backend was unaffected (its unit path already suppresses these).
Adds TestNativeBackend_RoundTrip_NoDuplicateSystemDefs in
cp.test.e2e.sepcompile, which pins --backend native so the guard holds
regardless of the compiler's default backend.
2026-06-18 18:23:12 +01:00
Graeme Geldenhuys e9f705fdc8 fix(native): stage-stable char-literal byte RHS in string subscript write
EmitByteRhsToEax lowered a single-character string-literal RHS via
Ord(TStringLiteral(AExpr).Value[0]).  That subscript-then-Ord idiom is
miscompiled by the self-hosted native stage: a native-built compiler baked a
pointer-sized garbage immediate (movl $0xd291f9ec) instead of the byte, so
S[i] := 'c' wrote junk.  It only surfaced in CI, where the test runner is
built by the native-bootstrapped -pre binary; local verification used the
QBE-built fixpoint binary, which lowers the idiom correctly, so the bug was
invisible.

Use StrAt(Value, 0) — the OrdAt-backed byte-read helper already used
elsewhere in this backend and stage-stable — instead of Ord(Value[0]).
Matches the QBE backend, which uses StrAt and was never affected.

Verified by building the test runner with a native-built compiler (the CI
configuration): full suite passes; all three fixpoints clean.
2026-06-18 15:55:33 +01:00
Graeme Geldenhuys 07aac3322f feat(string): writable string subscript S[i] := ch with copy-on-write
S[I] := <byte> was rejected by the semantic pass ('is not a static array
or dynamic array') for any string — local, global, or var-param — on both
backends.  It is now supported as the symmetric counterpart of the S[I]
read: an in-place byte store into a 0-based UTF-8 string.

Because strings are reference-counted and literals are immortal (stored in
read-only memory; the native backend put them in .rodata, so a naive store
segfaulted, while QBE silently mutated the shared literal), the write
performs copy-on-write.  New RTL helper _StringUnique(S) returns S when it
is uniquely owned (rc=1) and otherwise allocates a fresh rc=1 copy, releases
the old reference, and returns the copy.  Both backends emit
_StringUnique -> write the result back to the slot -> storeb, so the slot
keeps exactly one owned reference and mutating one alias never disturbs
another (Delphi/FPC UniqueString semantics).

The RHS accepts a numeric ordinal, Chr(n), or a single-character literal
(the byte-shaped forms used in place of a Char type).  PChar subscript
writes keep their existing in-place path (raw pointer, no ARC header).

Dual-backend e2e tests (write, copy-on-write aliasing + literal reuse,
var-param) in cp.test.e2e.stringops.  Updates language-rationale.adoc
(writable subscript + COW) and grammar.ebnf (string as a subscript-assign
base).
2026-06-18 15:39:38 +01:00
Graeme Geldenhuys 5b5b95c18f fix(const): accept hex/octal/binary literals in array-const elements (#129)
$hex, %binary and &octal integer literals were rejected inside an
array-constant initialiser — 'array[0..1] of Int64 = ($1, $2)' failed
with 'Cannot resolve array-const element' while the scalar form '= $1'
worked.  The parser stores array elements verbatim (prefix and all), and
ResolveConstArrayElem only recognised decimal digits, so any radix-prefixed
element fell through to an identifier lookup and errored.

Fold radix-prefixed elements (with optional leading sign) through
ParseIntLiteral — the same canonical parser the rest of the compiler uses
for scalar and typed consts — so all four radixes, plus underscore digit
separators, behave identically inside an array initialiser.  Keeps FPC's
&-octal (the project prefers FPC syntax where FPC and Delphi diverge; the
lexer already tokenised all four radixes).

Documents the radix-prefix rule in language-rationale.adoc.
2026-06-18 15:39:18 +01:00
Graeme Geldenhuys fe4e5602cf feat(linker): Phase B — symbol resolution, static relocations, ET_EXEC emission
Implement the Phase B internal-linker pipeline (TLinker in
blaise.linker.elf) on top of the Phase A section merger:

- Symbol resolution: build the global symbol table from merged
  sections (STB_GLOBAL over STB_WEAK; two globals = duplicate error),
  define the five linker-synthesised symbols (_GLOBAL_OFFSET_TABLE_,
  __bss_start, _edata, _end, __TMC_END__), resolve weak-undefined
  references to 0 and raise on strong-undefined.
- Static relocations: patch R_X86_64_PC32 / R_X86_64_PLT32 as S+A-P
  (PLT32 against an internal symbol relaxes to the direct form, no
  PLT). Reject the dynamic-only forms (R_X86_64_64, GOT/TLS) and
  absolute 32-bit symbol references with a diagnostic — deferred to
  Phase C.
- Layout + emission: assign virtual addresses at a fixed base
  (0x400000) in an executable run (.text + .rodata, sharing the first
  page with the headers) and a writable run (.data + .bss) on a fresh
  page; write a non-PIE ET_EXEC with two PT_LOAD program headers, the
  relocated section payloads, and a section-header table for tooling;
  mark the file executable.

Platform/arch-variable values (Is64, OSABI, EMachine, BaseAddr,
PageSize) live in TLinkTarget rather than being hard-coded, so FreeBSD
and a future i386 ELF target are a new target record and a PE/Mach-O
container would be a sibling writer behind the same symbol/relocation
core.

The linker is not wired into the compiler driver — it is a standalone
unit exercised only by tests; all backends still link via cc. Driver
integration behind --linker internal is Phase D.

Tests: TLinkerTests (symbol resolution, PC32 cross-object displacement,
R_X86_64_64 rejection, ELF-header/entry-point structure) and
TLinkerE2ETests (hand-written write+exit syscall object linked
internally to an ET_EXEC, executed, asserted on stdout + exit code;
readelf -a reports a clean statically-linked x86-64 executable).

Two native-backend codegen constraints were worked around in this unit:
no string-element assignment through a var-string parameter (append-
style byte building + PChar-local in-place writes), and no capturing
nested functions (private methods instead).
2026-06-18 14:58:24 +01:00
Andrew Haines 7a663c561f fix(codegen): spill overflow args in native inherited calls (>6 registers)
An `inherited Method(args)` whose Self plus arguments occupy more than the
six System V integer argument registers aborted native codegen with
"System V integer arg register index 6 out of range". EmitInheritedCallSeq
only implemented the all-in-registers case: it pushed every argument and
popped each into a register, walking past the six available once Self + the
args exceeded six slots. There was no stack-spill path like the regular
method-call and constructor emitters already have.

Give EmitInheritedCallSeq the same >6-slot handling: when Self + args exceed
six register slots, build the six-register prefix plus an overflow region in
one frame, place Self in slot 0, evaluate each argument into its slot, load
registers 0..5, drop the prefix so the overflow sits at the callee's
[rsp+0..], then static-dispatch to the parent. Up to six register slots
keeps the existing push/pop path. QBE was already correct.

This is independent of the call position and of metaclass dispatch — any
inherited call with seven or more register slots (e.g. a method or
constructor with six+ scalar/aggregate args calling its parent) was
affected.

Adds an e2e regression (cp.test.e2e.classes2) exercising an inherited call
with Self + 7 integer args (8 register slots), asserting correct dispatch
and result on every backend.

(cherry picked from commit b1fc1285ff3984eedaf6a68d58e2d568f1e4c793)
2026-06-18 13:29:35 +01:00
Andrew Haines b5573e5377 fix(codegen): metaclass constructor call in statement position
Calling a constructor through a metaclass-typed variable as a statement,
with the result discarded — `C.Create(args);` where `C: class of TBase` —
compiled cleanly but segfaulted at runtime on both the QBE and native
x86-64 backends. The same call in expression position (`o := C.Create();`)
worked.

The semantic pass flags the statement node with IsConstructorCall and
IsMetaclassDispatch, but only the expression-position lowering acted on
them (allocating via _ClassCreate and dispatching the ctor through the new
instance's vtable). The statement-position emitters had no such branch, so
the call fell through to the ordinary virtual-method path: it loaded the
metaclass value (a typeinfo pointer) as if it were the receiver object,
read a bogus vtable from the typeinfo's first word, and called a garbage
slot — crashing.

Add the metaclass-constructor branch to both statement emitters
(TCodeGenQBE.EmitMethodCall and TX86_64Backend.EmitMethodCallStmt): load
the metaclass, allocate via _ClassCreate, marshal the args, dispatch the
ctor indirectly through vtable[(slot+1)*8], then release the freshly built
(refcount 1) instance so the discarded result is freed rather than leaked.
A non-metaclass constructor as a statement (TFoo.Create();) is still
rejected earlier by the semantic pass as 'not a variable', so only the
metaclass shape reaches the new branch.

Adds an e2e regression (cp.test.e2e.classes2) covering zero-arg and
multi-arg dispatch and asserting the instance is freed (destructor count).

(cherry picked from commit 3f719213a838ab886b5db74847e7bef8894c404a)
2026-06-18 13:29:30 +01:00
Graeme Geldenhuys fa106ceeee feat(sets): support integer-subrange set base (set of 0..255) and Boolean operands
Accept an anonymous integer subrange as a set base type, e.g.
'set of 0..255' or 'set of 1..10', in both the inline (var/param/field)
and 'type T = set of L..H' declaration positions. The subrange lowers to
the same bitmap machinery as 'set of Byte': the bitmap is sized to H+1
bits and member ordinals are the integer values themselves. Bounds may be
integer literals, named constants, or constant expressions; the lower
bound must be >= 0, the upper <= 255, and the range ascending.

Also fix 'set of Boolean' so a Boolean operand is accepted directly on
either side of 'in' and as the second argument of Include/Exclude, without
an intervening Ord().

Parser handles both 'set of' paths (ParseTypeName and ParseSetDef);
semantic resolution adds ResolveSubrangeSetType for on-demand and
type-decl paths. Adds dual-backend e2e tests and semantic unit tests.
Updates grammar.ebnf and language-rationale.adoc per the language-decision
rule, and removes the now-implemented item from future-improvements.adoc.
2026-06-18 13:25:22 +01:00
Graeme Geldenhuys f107b0dabc docs: narrow the ordinal-set future-improvements entry to the unimplemented part
`set of byte` and `set of Boolean` are implemented (issue #105) — named ordinal
base types lower to a bit set (byte uses the 256-bit jumbo path), with working
membership/Include/Exclude and [lo..hi] range literals on both backends. The
section is rewritten to record that, leaving only the genuinely-remaining work:

* the anonymous integer-subrange base form `set of 0..255` / `set of 1..10`,
  still rejected at parse time ("Expected enum type or '(' after 'set of'");
* a minor `set of Boolean` follow-on where `True in s` is rejected by the `in`
  type-check (Ord(b) in s works).
2026-06-18 13:00:04 +01:00
Graeme Geldenhuys bc0485285f cleanup: revert KI-3 exception-handler var renames to plain E
The KI-3 ARC triple-release bug — multiple `on E: TYPE do` handlers in one
except block colliding on a single QBE slot — is fixed, so the workaround of
giving each handler a distinct variable name can be reverted. Handler vars are
now the natural `E` again in:
  - blaise.testing.pas: the runner's 4-handler dispatch block
    (EAF/EIT/E/ETO -> E) — the exact KI-3 scenario, in the test framework's
    own outcome classifier.
  - cp.test.attributes.pas: ETO -> E.
  - cp.test.sets.pas: ESE/EEx -> E.

(There were no {$IFDEF FPC} blocks left to remove — the KI-2 .Free guards and
all other FPC conditionals were already gone from the source tree as part of
the FPC-independence migration; verified by a full-tree directive scan.)

All three fixpoints green; 3501 tests pass, including the framework's own
FAIL/IGNORED/ERROR outcome paths.
2026-06-18 12:17:06 +01:00
Graeme Geldenhuys 1ef226369d fix(semantic): accept empty open-array literal [] as an open-array argument
An empty bracket literal [] passed to a plain open-array parameter
(`array of T`) failed overload resolution with "No matching overload" on both
backends. A non-empty literal works because AnalyseArrayLiteralExpr infers the
element type and types it `array of <elem>`; an empty [] has nothing to infer
from and was left untyped (nil), so it matched neither the open-array param (nil
arg type) nor the existing set / array-of-const special cases.

Two changes in uSemantic:
* ArgMatchScore now scores an empty TArrayLiteralExpr as an exact (2) match
  against any tyOpenArray parameter — an empty open array is valid for any
  element type — placed before the nil-arg bail so overload selection accepts it.
* RetypeSetLiteralArgs pins the empty []'s ResolvedType to the chosen formal's
  open-array type, so codegen emits a zero-length open array (data=nil, high=-1)
  of the right element type.

Overload disambiguation is preserved ([] picks the open-array overload; a scalar
argument still picks the scalar overload), and two equally-matching open-array
overloads still raise a clean "ambiguous overload" error. Works for
`array of string`, `array of Integer`, and `array of const`.

This was the last open item in native-testrunner-investigation.txt; the native
TestRunner now passes the full suite unfiltered. Adds dual-backend e2e tests.
All three fixpoints green; 3501 tests pass.
2026-06-18 11:53:51 +01:00
Graeme Geldenhuys 937146a3ac fix(arrays): enum-indexed multi-dimensional arrays (#128)
A multi-dimensional array whose dimensions are enum types — array[TEnum, TEnum]
or array[TEnum, 0..1] — failed to parse with "Expected '..' but got ','". Only
the single-dimension form array[TEnum] was handled; every dimension in the
comma-separated multi-dim path was forced through the lo..hi range parser.

Both the const-array dimension parser (ReadConstArrayDim) and the type-name
parser (ParseTypeName) now recognise an enum-type dimension — a bare identifier
not followed by '..' — and encode it as the range 0 .. @TEnum, reusing the
existing single-dimension '@TEnum' convention. ResolveArrayBound resolves the
'@TEnum' high-bound marker to the enum's last ordinal (member count - 1). This
fixes const, type and var declarations in any dimension position and mix
(enum+range, range+enum, 3-D enum+range+enum).

Adds dual-backend e2e coverage: 2-D enum/enum, mixed dims in both orders,
type+var form, and a 3-D mixed case. All three fixpoints green; 3499 tests pass.
2026-06-18 11:25:15 +01:00
Graeme Geldenhuys 517ed26f5d feat(const): integer literals and ranges in const-decl set literals
A typed set constant accepted only enum-member identifiers — `const C: TByteSet =
[1, 2, 3]` and `[1..3]` both failed with "Expected set member identifier". The
const-decl set path (a separate, more limited parser than the expression set
literal) is extended to accept:

* integer-literal members (1, 2, 3),
* inclusive ranges `lo..hi` (each endpoint an integer literal, enum member, or
  named constant), expanded to all ordinals in range,
* and ordinal base types (set of Byte / Boolean), not just enums.

Parser stores each member verbatim (a range as 'lo..hi'); the semantic pass
(AnalyseSetConstDecl) resolves and expands them via a new ResolveSetMemberOrd
helper, integrating with the existing small-mask and jumbo byte-bitmap const
machinery. Enum-member const sets (the original form) still work.

Adds dual-backend e2e coverage including edge cases: mixed range+literal, a jumbo
(>64-member) set with a range, the empty set, descending-range rejection, and an
enum-set regression. All three fixpoints green; 3495 tests pass.
2026-06-18 11:08:00 +01:00
Graeme Geldenhuys 6289a5235e fix: fold Boolean/enum/named-const array-const elements + emit correct width
An array constant with Boolean (or enum, or named-constant) elements emitted the
identifiers verbatim — `(False, True, ...)` became symbol references, failing at
link time on native ("undefined reference to `False'") and in QBE ("unknown
keyword False"). And even once folded, every non-string element was emitted at a
fixed 4-byte stride that did not match the element's actual read width, so a
Boolean array read all-zero.

Two fixes:
* Semantic (AnalyseArrayConstDecls): a new ResolveConstArrayElem folds each
  bare-identifier element to its numeric value once the element type is known —
  Boolean True/False to 1/0, enum members and named integer/boolean constants to
  their ordinal/value. Numeric/float literals pass through. An unresolved
  identifier is now a clear semantic error instead of a link failure.
* Codegen (both backends): array-const element data is emitted at the element's
  real width — .byte/.word/.long/.quad on native, b/h/w/l in QBE — instead of a
  fixed .long/w. Built-in scalar names are mapped directly so the width is
  correct even when the codegen has no symbol table set (the e2e in-process path).

Adds dual-backend e2e coverage including edge cases: Byte/Word/Int64 widths,
negative integer pass-through, named-const elements, and a Boolean array indexed
by an enum. All three fixpoints green; 3490 tests pass.
2026-06-18 10:50:34 +01:00
Graeme Geldenhuys 734030932d feat(native): single-unit compilation + incremental/warm-cache builds
The native backend previously stubbed GenerateUnit and left
SupportsIncremental/SupportsWarmCache False, so --incremental fell back to QBE.
Native now drives the per-unit parallel pipeline itself:

* TCodeGenNative.GenerateUnit emits a single unit in isolation (new
  TNativeBackend.GenerateUnit: clear buffer, EmitUnit, return assembly).
* The native driver overrides SupportsIncremental/SupportsWarmCache (True),
  CreateUnitCodeGen (separate-compile mode), and LowerToObject (assemble the
  unit .s to .o via the internal assembler or `cc -c -x assembler`).
* Separate-compilation mode (FSeparateCompile): unit objects omit the
  once-per-program TObject/TCustomAttribute system defs — the program object
  provides them — and emit their own file-local string-literal blobs, so a unit
  method referencing a literal links cleanly. The system typeinfo/vtable symbols
  (typeinfo_TObject, vtable_TObject, and TCustomAttribute) are now .globl so unit
  objects can reference the program's single definition.

Works with both the external (`cc -c`) and internal assemblers, and the warm
cache (a second build reuses the per-unit .o). Adds a sepcompile e2e test
building a multi-unit class program with --backend native --incremental.

All three fixpoints green; 3485 tests pass.
2026-06-18 09:30:37 +01:00
Graeme Geldenhuys ebe7057bb7 feat(native): static-array-of-interface store, read, and dispatch
A static array of interfaces (Arr: array[0..2] of IGreet) was unusable on the
native backend in three ways, all now fixed:

* Store (Arr[I] := V): the element is a contiguous 16-byte fat pointer, but the
  generic static-subscript store wrote only the 8-byte obj and left the itab
  garbage. Now computes the element address and delegates to
  EmitInterfaceToFieldSlotsAt (obj+itab with ARC for nil/class/interface).
* Read as RHS (G := Arr[I]): EmitInterfaceAssign gained an interface-subscript
  source case that copies obj+itab with ARC.
* Method dispatch (Arr[I].M()): EmitInterfaceCall gained a subscript-receiver
  case that loads obj/itab from the element address.

A shared EmitIntfStaticElemAddr helper computes the element fat-pointer address
(base + (idx-LowBound)*16), reused by all three paths and by the interface-store
source. EmitInterfaceToFieldSlotsAt also learned an interface-subscript source
(Arr[J] := Arr[I]). Native now matches QBE exactly on the full test
(11/22/22/11/11). TestRun_StaticArrayOfInterface_FatPointer is dual-backend.

(A pre-existing scope-exit leak of interface array elements affects BOTH backends
identically — logged separately in bugs.txt, not a parity gap.)

All three fixpoints green; 3484 tests pass.
2026-06-18 09:07:52 +01:00
Graeme Geldenhuys a6378b39e2 fix(native): release the owned ref of an interface-call-result argument
Show(MakeFoo(42)), where MakeFoo returns an interface, handed the borrowing
parameter an owned (+1) fat pointer that was never released after the call —
--debug reported one leaked instance on native (QBE already released it).

Added an akIntfConsume hoist kind: EmitArgHoist drives the interface-returning
call in the pre-pass and saves the owned fat pointer (itab then obj, obj on
top); the argument reload pushes both halves as the two interface arg slots; and
EmitHoistEpilogue releases the obj after the call (mirrors akStrConsume). The
2-slot reload is wired into both EmitArgPush and EmitCall's inline argument loop.

--debug leak report is now clean on both backends; two leakcheck tests (QBE +
native) are added. All three fixpoints green; 3484 tests pass.
2026-06-18 08:55:58 +01:00
Graeme Geldenhuys 2d8497f2b3 fix(native): keep %rsp 16-aligned in the record-call argument hoist
DateAddDays(MakeDate(...), 5) segfaulted on native while the non-nested form
worked. The args arrived correct — the fault was a stack-alignment bug. The
argument hoist for a record-returning-call argument allocated a 16-byte-aligned
sret buffer (RecArgBufBytes) and then pushed an 8-byte pointer to it, a 24-byte
contribution that left %rsp 8 bytes off 16-alignment. The misalignment was
harmless until the callee chain reached an aligned SSE access — here libm's
mktime (via DateUtils _TimeJoin) executing `movdqa`, which faults on a
misaligned address.

EmitArgHoist now reserves a full 16-byte slot for the saved pointer (subq $8 +
pushq) so the hoist region stays a multiple of 16. The pad sits above the saved
pointer, so reload offsets are unchanged. This is the shared hoist, so it fixes
the alignment for every record-call-argument site, not just DateAddDays.

The DateAddDays e2e test is re-enabled on both backends; a self-contained
regression (record-call result as a record-by-value arg feeding an SSE op) is
added to the gaps suite. All three fixpoints green; 3482 tests pass.
2026-06-18 08:44:54 +01:00
Graeme Geldenhuys 28a4093244 fix(native): class-level const array element access (T.Arr[I])
A class-level const array (const Days: array[0..6] of string = (...)) read via
an instance, T.Days[I], returned an empty string on native. The semantic pass
folds the subscript into the field-access node — IsConstant=True with
ConstArraySymbol (the global data label), ConstArrayType (the static-array type)
and PropIndexExpr (the index) — but native's EmitExprToEax IsConstant branch only
handled scalar string/int consts and fell through to an empty string literal.

Added the class-const-array element path: compute
base + (idx - LowBound) * ElemSize and load the element by type (float / string
pointer / narrowed integer), plus a bare-reference case returning the label
address. Mirrors the QBE EmitExpr ConstArraySymbol path. The range- and
enum-indexed class-const-array e2e tests are converted to AssertRunsOnAll.

(Static-array-of-interface element store/read/dispatch remains a separate, larger
follow-up — tracked in bugs.txt; that test stays QBE-only meanwhile.)

All three fixpoints green; 3481 tests pass.
2026-06-18 08:12:22 +01:00
Graeme Geldenhuys 5c1293f268 fix(native): Round() rounds half-away-from-zero to match QBE/Delphi
Native lowered Round() with cvtsd2si/cvtss2si, which honour the FPU rounding
mode (round-half-to-even / banker's): Round(2.5)=2, Round(-2.5)=-2. QBE — and
Delphi/FPC — round half away from zero via C99 round(): Round(2.5)=3,
Round(-2.5)=-3. Native now calls round()/roundf() first, then truncates the
integral result, matching QBE exactly. The two Round e2e tests are converted to
AssertRunsOnAll.

All three fixpoints green; 3481 tests pass.
2026-06-18 07:57:59 +01:00
Graeme Geldenhuys 92e09e517f feat(native): TObject.InheritsFrom and default ToString builtins
The native backend raised "TMethodCallExpr has no ResolvedMethod" for the two
TObject built-in methods that the semantic pass flags rather than resolving to a
declared method:

* InheritsFrom(C): load the receiver's typeinfo (vtable[0] for a class instance),
  evaluate the class-of argument to a typeinfo pointer, and call
  _InheritsFrom(self_ti, arg_ti) → Boolean.
* ToString: virtual dispatch through vtable slot 2 (offset 16: typeinfo, Destroy,
  ToString), returning the string in %rax — the default returns the class name.

A small EmitMethodReceiverToRax helper loads the instance pointer (expression,
named local/global, or implicit Self). The 7 InheritsFrom tests and the default
ToString test in cp.test.e2e.classes2 are converted to AssertRunsOnAll, pinning
both builtins on QBE and native.

All three fixpoints green; 3481 tests pass.
2026-06-18 07:50:43 +01:00
Graeme Geldenhuys 161d8e4102 fix(native): StrToDouble returns its Double in %xmm0, not %rax
StrToDouble was lowered only in the integer EmitExprToEax path: it called
_StrToDouble (which returns the Double in %xmm0 per the System V ABI) and left
%rax untouched, so a float assignment (D := StrToDouble(S)) read %rax — a
leftover pointer — and cvtsi2sd'd it into garbage. Native printed values like
95693543141392 instead of the parsed number. Added StrToDouble to the float
emitter (EmitFloatBuiltin) so the result is consumed from %xmm0.

The 7 StrToDouble e2e tests are converted to AssertRunsOnAll, pinning the fix on
both backends. The blanket dual-backend flip of the inline CompileAndRun helper
is documented as deliberately reverted: it breaks on non-deterministic programs
(GetProcessID) and several genuine native gaps remain open (InheritsFrom/ToString
builtins, Round .5-boundary, class const-array, interface-field-assignment RHS,
EmitSretCall record-value arg) — all logged in bugs.txt for individual fixes.
Suites migrate to AssertRunsOnAll as those gaps close.

All three fixpoints green; 3481 tests pass.
2026-06-18 02:56:13 +01:00
Graeme Geldenhuys 5dd6a3d8c9 feat(native): pass an interface-returning call result as an argument
Show(MakeFoo(42)), where MakeFoo returns an interface, previously raised
"unsupported interface argument expression" on the native backend. EmitMethodArgPush
now handles a TFuncCallExpr/TMethodCallExpr argument of interface type: it lowers
the call via EmitIntfSretCall/EmitIntfSretMethodCall (which leaves the fat pointer
at (%rsp)), loads obj+itab, drops the sret buffer, and pushes the pair in arg-slot
order. Both backends now print 42. Adds a dual-backend e2e test.

Known follow-up (tracked): the owned (+1) reference of the call result is not yet
released after the call, so --debug reports a single leaked instance for this
specific borrowed-interface-call-result shape. The feature is output-correct; the
ARC release will route through EmitArgHoist (akIntfConsume) like const-string
consume args. All three fixpoints green; 3481 tests pass.
2026-06-18 02:33:14 +01:00
Graeme Geldenhuys 44a2bd93fb feat(native): 3-arg Supports out-var; run RTL/stdlib e2e on both backends
Native backend gaps surfaced by making the RTL/stdlib e2e suites dual-backend:

* 3-arg Supports(Obj, IFoo, OutVar): the native EmitExprToEax only handled the
  2-arg boolean form, so the out-var was never populated or registered — a local
  out-var read garbage and a global one produced undefined _obj/_itab symbols at
  link time. Implemented the 3-arg form (GetItab + ARC retain/release + global
  registration), mirroring the QBE EmitSupportsExpr path.

* e2e harness: CompileAndRunWithRTL now compiles and runs on BOTH backends and
  asserts parity, instead of QBE only. This brings sysutils, collections,
  streams, strutils, tstringlist, tcomponent, threading, arc and the rest of the
  RTL/stdlib suites onto the native backend for the first time. New helpers
  AssertRTLRunsOnAll/On/One and CompileAndRunWithRTLOn back this; the QBE-only
  CompileAndRunWithRTLDebug now delegates to the dual-backend *On implementation.

One pre-existing native bug remains (EmitSretCall mishandles a record-by-value
argument that is itself a record-returning call result, e.g.
DateAddDays(MakeDate(...), 5) — the v0.11.0 release crashes too). That single
case is pinned to QBE via CompileAndRunWithRTLQBEOnly with a tracked TODO; every
other RTL/stdlib e2e now runs on both backends.

All three fixpoints green; 3480 tests pass.
2026-06-18 02:23:17 +01:00
Graeme Geldenhuys f39e7e1553 fix(native): interface-sret method calls with >5 args + sret frame layout
Two related native x86-64 bugs prevented an interface-returning (sret) method
from being called with more register-eligible arguments than the ABI passes in
registers:

1. Caller side: the three sret call emitters (EmitIntfSretCall,
   EmitIntfSretMethodCall, EmitClassIntfSretMethodCall) popped every pushed arg
   slot into a System V integer register and raised once the index exceeded 5,
   instead of spilling the overflow to the stack. A shared EmitSretRegArgs helper
   now loads the register-mapped slots and relocates the overflow slots to the
   stack in ABI order, returning the byte count the caller reclaims after the
   call.

2. Callee side: BuildFrame classified FSretFunc only when allocating the Result
   slot, which runs AFTER the parameter-register accounting loop. The loop
   therefore read a stale FSretFunc (left over from the previous function) and,
   for an sret-returning class/interface method (hidden buffer in %rdi AND Self
   in %rsi — two registers), mis-placed the first stack-passed parameter as the
   last register parameter. The sret classification is hoisted above the param
   loop, and the register count now adds the sret buffer and Self independently.

Both backends now agree (IFoo.Make(1..6) returns 21). Adds three dual-backend
e2e tests. All three fixpoints green; 3480 tests pass.
2026-06-18 02:02:46 +01:00
Graeme Geldenhuys 66ff63e73d fix(semantic): resolve same external in two units (was "Undeclared function")
When two units each privately declared the same C function via
`external name '...'` in their implementation sections, the second unit failed
with "Undeclared function" at the call site.

Root cause: FProcIndex is global across all compiled units, but impl-to-forward
matching used a bare FProcIndex.IndexOf.  The second unit's external matched the
FIRST unit's already-indexed decl, so it was treated as that unit's
implementation and was never defined into its own scope.

Fixes:
- Restrict every impl-to-forward match (and the "interface function has no
  implementation" checks) to the current unit via a new IndexOfProcInUnit
  helper that filters on OwningUnit.  The overload-signature loops gain the same
  OwningUnit guard.
- When an impl pairs with its forward decl, carry the forward's OwningUnit onto
  the impl so the later "has no implementation" scan still finds it.
- Collapse the false "ambiguous overload" that the two same-named externals
  produced in the global overload group: SameExternalDecl treats two external
  decls with the same link name and arity as one function.

This also clears the obstacle that forced the float-const fix to use only
_DoubleToStr; uSemantic and blaise.assembler.x86_64 may now both declare
_StrToDouble safely.

Regression test: TestDuplicateExternalAcrossUnits_Compiles in
cp.test.e2e.sepcompile (two units declare `external name 'strlen'`, one
invocation, compile + run + assert stdout).

Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite
3477 tests, 0 failures with the QBE fixpoint binary.
2026-06-18 01:30:21 +01:00
Graeme Geldenhuys 9844b38e56 fix(const): use ABI-safe RTL helper for float-const folding (was libc snprintf)
RawDoubleToStr formatted folded float constants via libc snprintf, declared
in Pascal with a fixed Double parameter.  snprintf is genuinely variadic; the
SysV x86-64 ABI requires %al to hold the XMM-register count for variadic calls,
and neither backend emits that setup (QBE only for its '...' syntax; the native
EmitCall never sets %al).  With %al garbage, glibc could read the double from
the wrong place and emit a wrong string -- an environment-dependent
miscompilation of folded float constants that passed locally but failed on the
CI runner (TestConstExpr_FloatParens_Folds expected d_9; TestConstExpr_
NegativeFloat_Folds expected d_-3).

Replace the variadic snprintf call with the RTL's pure-Pascal _DoubleToStr
(FormatFloat(V,15) == %.15g), which is ABI-safe.  strtod is not variadic and is
kept as-is.

Add two e2e regression tests (TestRun_FloatConstFold_Parens /
_NegativeMul) that compile AND run folded-float programs and assert stdout.
The IR-only tests could not catch a host-ABI-dependent fold.

Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite
3476 tests, 0 failures with the QBE fixpoint binary.
2026-06-18 01:17:57 +01:00
Graeme Geldenhuys d72a7d9d6f chore: begin v0.12.0-dev cycle 2026-06-17 23:25:12 +01:00
Graeme Geldenhuys 87531495a2 docs: update test count to 3474 for v0.11.0 2026-06-17 23:22:43 +01:00
Graeme Geldenhuys 7a9a92aca5 release: v0.11.0
Self-hosting fixpoint verified on 371,292 lines of QBE IR (QBE, native,
and internal-assembler fixpoints all green). This cycle landed 17 language
features and 30 bug fixes: floating-point constant folding, named-constant
and enum-indexed static-array bounds, generic methods, default array
properties, set-of-ordinal types, case-label ranges, and broad codegen and
ARC hardening across both backends. 3474 tests passing.
2026-06-17 23:22:20 +01:00
Graeme Geldenhuys c79c7e015a fix(const): promote integer initialisers to float for Double/Single vars and typed consts
When a variable or typed constant declares a Double/Single type but the
initialiser expression is pure-integer (e.g. `var Y: Double = 2 * 3`),
the semantic pass now promotes the folded integer value to a float string
so the QBE backend emits a valid `d_<value>` literal instead of an empty
`d_`.
2026-06-17 22:58:29 +01:00
Graeme Geldenhuys 8c7f5c9cbc feat(arrays): enum-indexed static arrays in var/type declarations (#114)
Static-array declarations in var and type sections now accept an enum type
as the index: array[TColor] of Integer.  The array is sized 0..N-1 where
N is the enum member count.  Subscript access uses enum ordinal values —
no codegen changes were needed.

Parser: detects array[Ident] (ident followed by ']' with no '..') and
encodes it as 'array[@TEnum] of T' in the type-name string.

Semantic: a new path in FindTypeOrInstantiate resolves the '@' marker,
looks up the enum type, reads Members.Count, and creates a standard
TStaticArrayTypeDesc with bounds 0..Count-1.

This completes the enum-indexed array story — const arrays already
supported this form; var/type declarations now match.

Tests: 4 unit tests + 2 E2E tests (both backends).
Grammar and language rationale updated.
2026-06-17 19:16:12 +01:00
Graeme Geldenhuys 2dcfe8e196 feat(arrays): accept named constants and expressions as static-array bounds (#109)
Static-array bounds now accept named integer constants and compile-time
integer expressions, not only integer literals.  All of these are valid:

  const N = 10;
  type TBuf = array[0..N-1] of Byte;
  var A: array[0..N] of Integer;
  const Days: array[0..N-1] of string = (...);

Parser: ReadConstBoundText collects tokens forming a bound expression
(integers, identifiers, arithmetic operators, parentheses) into a string
embedded in the type name.

Semantic: ResolveArrayBound resolves the bound text — plain integers via
StrToInt, named constants via symbol-table lookup, expressions via
mini-parse + EvalConstIntExpr.  The canonical type name always uses
resolved integer values for cache consistency.

Both the var/type declaration path (FindTypeOrInstantiate) and the
const-array path (BuildConstArrayType / ReadConstArrayDim) are updated.

Tests: 5 unit tests + 3 E2E tests (both backends).
Grammar and language rationale updated.
2026-06-17 19:07:26 +01:00
Graeme Geldenhuys 38c77c8a93 feat(const): fold floating-point constant expressions at compile time (#108)
Constant declarations now accept arithmetic expressions involving float
literals, named float constants, and the '/' operator.  The semantic pass
detects float-containing expressions and folds them to a single Double
value at compile time, storing the result as a string — the same format
used for bare float literals.

The '/' operator always yields a float result even with integer operands,
matching Delphi/FPC semantics (e.g. const X = 10 / 4 produces 2.5).

Parser: extended ConstRhsStartsIntExpr to detect float-led and
slash-containing expressions; added tkSlash to IsConstExprOp.

Semantic: added IsFloatConstExpr (detects float leaves or '/' usage) and
EvalConstFloatExpr (folds via libc strtod/snprintf to avoid a known
self-hosting ABI issue with the Blaise StrToDouble built-in).

Tests: 8 IR unit tests + 4 E2E tests (both backends).
Grammar and language rationale updated.
2026-06-17 18:48:04 +01:00
Graeme Geldenhuys f6d488bca4 feat(sets): support ordinal-based set types — set of Byte / Boolean (#105)
Widen set base types from enum-only to also accept Byte (256-bit jumbo)
and Boolean (2-bit small).  Set literals accept integer literals and
range syntax [lo..hi], expanded at compile time via EvalConstIntExpr.
Include/Exclude and the in operator accept numeric arguments for ordinal
base types.  Both QBE and native backends handle TIntLiteral elements in
set mask computation.

15 new IR unit tests and 4 new E2E tests (both backends via
AssertRunsOnAll).  All 3444 tests pass; FIXPOINT_OK,
NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK.
2026-06-17 18:21:56 +01:00
Graeme Geldenhuys b0499aa454 fix(semantic): repair generic instance methods when type alias precedes impl (#124)
When a named type alias (e.g. TIntHolder = THolder<Integer>) appeared
in the type section before the out-of-line method implementations
(procedure THolder<T>.SetVal), InstantiateGenericRecord cloned the
template methods with Body=nil because LinkGenericClassMethodImpls had
not yet transferred the implementation bodies. Codegen skipped the
bodiless methods, producing undefined-reference link errors.

Add RepairEarlyGenericInstances which runs after LinkGenericClassMethodImpls
in all three analysis paths (AnalyseUnit, AnalyseUnitForExport,
AnalyseBlock). It iterates existing generic record and class instances,
finds methods still missing a body, clones from the now-populated
template, and analyses the repaired methods with correct type-param
scope bindings.
2026-06-17 16:51:26 +01:00
Graeme Geldenhuys 0af9f9f7bf fix(parser): allow p^.field[index] := value subscript after deref (#118)
The statement parser's deref-then-field path (p^.field) unconditionally
expected ':=' after consuming the field name, failing when the field
is an array that needs subscripting.  Now check for '[' before ':='
to handle p^.da[0] := value, matching the existing r.da[0] := value
path.
2026-06-17 15:58:02 +01:00
Graeme Geldenhuys cdd1d0cbe2 fix(codegen): return fixed-size static array by value via sret (#112)
Functions returning a static array >8 bytes now use the sret ABI on
both QBE and native x86-64 backends: the caller passes the destination
buffer as a hidden first argument, the callee writes through it, and
returns void.  Previously only the first 8 bytes survived (QBE
segfaulted, native rejected the function outright).

Key changes:
- QBE: IsRecordCall, signature, prologue, epilogue, and assignment
  dispatch all extended to handle tyStaticArray alongside tyRecord
- Native: allowed return type guard, BuildFrame FSretFunc, sret
  call-site dispatch, and subscript-assign Result indirection all
  extended for tyStaticArray
2026-06-17 15:50:22 +01:00
Graeme Geldenhuys d4a066aa93 fix(parser+semantic): typed array const with named type alias (#113)
A `const A: TArr = (10, 20, 30)` where TArr is a named alias for a
static array type failed to parse as an array constant.  The parser
now detects the value-list shape via lookahead when CD.TypeName is set,
and the semantic pass resolves the named type to its underlying
TStaticArrayTypeDesc to extract bounds and element type.
2026-06-17 15:33:32 +01:00
Graeme Geldenhuys 69e0ff6baa fix(codegen): Boolean xor emitted as ceqw instead of xor in QBE backend
The tyBoolean branch in EmitExpr intercepted boXor (and boAnd/boOr)
before the generic integer path could handle them.  The else clause
in the comparison-operator switch defaulted to ceqw (compare-equal),
which is the exact opposite of xor: True xor True produced True
instead of False.

Added boAnd/boOr/boXor cases to the tyBoolean integer-comparison
switch so they emit the correct bitwise operations.  The native
backend was already correct (no separate tyBoolean interception).

Fixes GitHub #123.

3411 tests pass.  All three fixpoints verified.
2026-06-17 15:16:59 +01:00