Commit graph

859 commits

Author SHA1 Message Date
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 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
Graeme Geldenhuys a82269f48c feat(stdlib): add Items default property to TDictionary and TOrderedDictionary
Add GetItem/SetItem methods and an Items[Key: K] default property to
both TDictionary<K,V> and TOrderedDictionary<K,V>.  This enables the
idiomatic d[key] syntax for reads and writes:

  D['one'] := 1;
  WriteLn(D['one']);

GetItem halts on missing key (consistent with index-out-of-bounds in
TList).  SetItem delegates to Add (add-or-update semantics).

The compiler already had full default property infrastructure (parser,
AST, semantic, both codegens) — only the stdlib declarations were
missing.  This is the first non-Integer-typed default property in the
codebase, exercising the generic index type path end-to-end.

Tests: 2 IR codegen tests (GetItem/SetItem emission) + 3 e2e tests
on both backends (integer keys, string keys, update-via-bracket).
3407 tests pass.  All three fixpoints verified.
2026-06-17 15:05:02 +01:00
Graeme Geldenhuys 2b745eaeb2 fix(codegen): native sar, unsigned compare, mixed-float comparison + 32 e2e tests
Three native backend bugs found and fixed by the IR-emit audit:

1. `sar` operator raised ENativeCodeGenError — added boSar -> sarq.
2. UInt64 comparison used signed setl/setg/setle/setge — max UInt64
   compared as negative. Fixed: unsigned types and pointer-like types
   now emit setb/seta/setbe/setae.
3. Mixed Single/Double float comparison in EmitCondBranch and
   EmitExprToEax determined IsS (single-precision) by checking only
   the left operand. A Single compared to a Double literal loaded
   double-width data but compared with ucomiss. Fixed: require BOTH
   operands to be Single for IsS, and add EmitXmm0WidthAdjust calls.

New e2e test files (all both-backend via AssertRunsOnAll):
- cp.test.e2e.external (10): FFI name binding, narrow-int return
  masking, Single param/return, record-by-value, name alias
- cp.test.e2e.genericintfs (7): generic interfaces, two type params,
  IEqualityComparer/IComparer, string type arg, polymorphic dispatch
- cp.test.e2e.gaps (15): packed records, sar/shr, UInt64 operations,
  [Unretained] back-references, generic records, published RTTI
  MethodAddress, multi-arg WriteLn

3402 tests pass. All three fixpoints verified.
2026-06-17 14:13:24 +01:00
Graeme Geldenhuys 4e0e0a90a9 refactor(testing): use Cls.Create() instead of ClassCreate() builtin
Dogfood the new metaclass constructor dispatch syntax in the test
runner — the primary real-world consumer of metaclass construction.
2026-06-17 13:32:53 +01:00
Graeme Geldenhuys 215082afc3 feat(codegen): implicit-virtual constructor dispatch via metaclass
Constructors are now auto-slotted into the vtable by the semantic pass.
When a constructor is called through a metaclass-typed variable
(C.Create(args)), the compiler emits _ClassCreate for allocation
followed by a vtable-indirect call to the most-derived constructor
body. Direct calls (TFoo.Create) remain fully static.

Both backends (QBE and native x86-64) emit correct dispatch for:
- MetaclassVar.Create(args) syntax
- ClassCreate(MetaclassVar, args) builtin
- Zero-arg and multi-arg constructor signatures

This is a layout-changing commit (adds constructor vtable slots).
Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK,
3370 tests pass.
2026-06-17 10:07:47 +01:00
Graeme Geldenhuys 1753b5d2a8 fix(codegen): native scalar field of a record-call result as a call argument
The native x86-64 backend corrupted other arguments when a SCALAR FIELD of a
record-returning call result was read inline as a call argument — e.g.

    Check('vals', 1, MakeR().A);     // 'vals' lost on native; QBE was fine

Reading MakeR().A went through EmitExprToEax, which materialises the call's
sret buffer (a stack subq) and never frees it (the frame epilogue
reclaims it for a standalone expression).  Inside the argument push/pop
sequence that stray allocation shifted %rsp, so an already-pushed argument
(typically the const-string) was reloaded from the wrong slot and lost —
crashing later in _StringAddRef when the callee retained the garbage pointer.

Fix: in the chained-field-read path, when the base is a record-returning call
and the field is scalar, allocate the sret buffer, read the field out of it,
then FREE the buffer so the expression nets zero stack change and the argument
protocol stays balanced.  Record/static-array fields are excluded — they
return an address INTO the buffer, whose lifetime must outlive the read.

Regression test (both backends) in cp.test.e2e.recordret; the inline
HostTarget().OS assertion in cp.test.toolchain (previously worked around) now
exercises the fixed path directly.

FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite OK (3363).
2026-06-17 08:34:16 +01:00
Graeme Geldenhuys c266a8ee74 feat(cli): reject --emit-ir/--emit-asm with an incompatible explicit backend
--emit-ir prints QBE IR and --emit-asm prints native assembly; PickTopDriver
routes each output mode to the backend that produces it, ignoring --backend.
That silent override is acceptable for the default backend, but when the user
EXPLICITLY passes a --backend that cannot produce the requested output it was
silently honoured anyway — e.g. '--backend native --emit-ir' quietly emitted
QBE IR, ignoring the requested native backend.

Track whether --backend was given explicitly (TFrontEndOpts.BackendExplicit)
and, after backend selection, reject the incompatible combinations:

  --backend native --emit-ir   -> error (use --emit-asm)
  --backend qbe    --emit-asm  -> error (use --emit-ir)

The default-backend cases are unchanged: --emit-ir with no --backend still
resolves to QBE, and --backend qbe --emit-ir / --backend native --emit-asm
remain valid.  This matches the driver-owns-its-flags principle of the option
refactor — a backend-specific output mode belongs to its backend.

Tests in cp.test.cli cover both rejections and both still-valid paths.
FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite OK (3362).
2026-06-17 08:27:54 +01:00
Graeme Geldenhuys 88db7f3b23 ci: run CLI-contract & internal-assembler tests; surface skips
The TCLIContractTests and TInternalAsmE2ETests suites resolve their
compiler binary from BLAISE_QBE_COMPILER, falling back to the
/tmp/fp_blaise{3,2} fixpoint binaries.  Those only exist after a local
fixpoint.sh run, so in CI both suites silently Ignore'd ~24 tests while
the run still reported green (25 ignored in CI vs 1 locally).

Two changes:
  - bootstrap.yml: set BLAISE_QBE_COMPILER to the bootstrapped -pre
    binary so both suites actually run in CI.
  - CompilerAvailable() in each suite now prints a one-time StdErr note
    when it skips, so a missing toolchain is loud instead of a silent
    green with hidden ignores.
2026-06-17 08:13:50 +01:00
Andrew Haines a0e0fd567b feat(toolchain): host-aware tool resolution with spec + prefix env
Generalise external-tool lookup so a cross/host build finds the right
binaries:

- target: HostTarget detects the host OS from ParamStr(0) (a '.exe' suffix
  means Windows, incl. under wine) instead of assuming linux; add
  ExecutableExtension(target).
- uToolchain: TToolSpec (ordered candidate basenames, a cross-triple
  prefix, and a HostTool flag) + ResolveSpec resolve a tool by env
  override, then $BLAISE_TOOLCHAIN_PREFIX + name, then the cross prefix on
  $PATH, then bare name — each probe appending the host exe extension.
  HostTool distinguishes host tools (run anywhere, target via flags) from
  target tools (must be the target's own binary; no host fallback when
  cross). TrySingleName now uses the real host extension.
- driver: TBackendDriver.DescribeTools(target) declares a backend's tools
  (base = the shared linker: cc/clang or the mingw cross-linker for a
  Windows target); ToolPath resolves one by name; LinkViaToolchain links
  through it. Self.DescribeTools for virtual dispatch.

Native sret codegen fixes (folded in so this commit bootstraps): the
toolchain code is the first in the compiler's own source to build a record
via a function whose body does SetLength on a dynamic-array Result field,
and to pass a record-returning method result directly as a const-record
argument.  Both miscompiled on the native x86-64 backend (QBE was correct),
so a native-compiled compiler could not bootstrap at this commit:

- EmitLValueSlotAddr: an l-value field slot on a local record used a raw
  leaq, but the sret-function Result slot holds a POINTER — route through
  EmitLocalRecordBase so the pointer is loaded before adding the field
  offset (fixes SetLength(Result.DynField, N)).
- EmitArgHoist: a record-returning method-call argument was materialised
  via EmitExprToEax, which has no sret path for method calls — allocate the
  buffer and drive EmitMethodSretCall directly, mirroring the function path.

Tests:
- cp.test.toolchain: ResolveSpec resolution order, host-tool vs target-tool
  cross-suppression, fallback naming, and the host/extension helpers.
- cp.test.e2e.recordret: regressions for both native sret fixes (both
  backends).
- cp.test.codegen: native backend also emits the _BlaiseInit startup call
  (the existing assertion only covered QBE).

A third, pre-existing native sret bug (sret-call result + const-string arg
in the same call) surfaced while writing the toolchain tests; it is
independent of this work and logged in bugs.txt.

Backend-neutral feature; native fixes are x86-64-only.
2026-06-17 00:44:35 +01:00
Andrew Haines 4e61cb1e8f fix(rtl): run one-time RTL setup via a _BlaiseInit startup hook
A unit's initialization section only runs when that unit is compiled
into the program's own translation unit; units linked from a prebuilt
archive never get their _init called.  blaise_weak relies on its
initialization section to set up the weak-reference table mutex, so for
archive builds that setup never happened.  A zero-filled mutex object is
a valid unlocked mutex on some platforms, which kept this latent — but
that is not guaranteed everywhere.

Add a _BlaiseInit RTL entry point that performs the one-time setup
(currently the weak-table mutex, guarded to stay idempotent) and have
each backend's $main emit a call to it right after _SetArgs, before any
user code runs.  The mutex is now initialised explicitly instead of by
relying on a zero-filled object happening to be valid.

Regression: cp.test.codegen TestMain_CallsBlaiseInit asserts the call.
2026-06-17 00:10:53 +01:00
Graeme Geldenhuys 2f54707a63 feat(strutils): clean literal replace API — Replace (first) + ReplaceAll
Replace the FPC/Delphi replace surface (ReplaceStr, ReplaceText, and the
proposed StringReplace + TReplaceFlags set) with two self-explanatory
functions:

  Replace(S, Old, New)    — replaces the FIRST occurrence
  ReplaceAll(S, Old, New) — replaces EVERY occurrence

Both are case-sensitive and literal (non-pattern).  This sheds the legacy
redundancy: ReplaceStr/ReplaceText were two names for one operation split by
a 'String vs Text' distinction that is not obvious, and Delphi's StringReplace
encodes the same two booleans (all-vs-first, sensitive-vs-insensitive) as an
awkward flag-set.  Modern languages (Go, Python, Java, Rust) use a plain
first/all split with no case flag.

Case-insensitive replace is expressed by lower-casing the inputs with the
built-in LowerCase/UpperCase before calling Replace/ReplaceAll; a
case-*preserving* insensitive replace and pattern/regex matching are deferred
and recorded in docs/future-improvements.adoc.

The internal worker keeps the existing TStringBuilder-based loop; it gains a
FirstOnly flag and drops the now-unused case-fold path.

No callers of the removed functions existed anywhere in the tree.  IR/semantic
and e2e tests updated to the new names; both fixpoints green (FIXPOINT_OK,
NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK); full suite OK (3346 tests).
2026-06-16 23:49:30 +01:00
Graeme Geldenhuys 712263abd4 feat(rtl): integer div/mod by zero raises catchable EDivByZero
Integer div and mod by zero previously trapped in hardware (SIGFPE),
uncatchable even inside try/except — a single bad divisor terminated the
process.  Both backends now emit a divisor-zero guard before each integer
idiv that raises a catchable EDivByZero exception via the normal exception
machinery, matching standard Pascal/Delphi semantics.

- SysUtils: add EDivByZero = class(Exception) and the _RaiseDivByZero helper
  that raises EDivByZero('Division by zero').
- QBE + native x86-64: emit a divisor==0 check before integer div/rem
  (32- and 64-bit); on zero, call SysUtils__RaiseDivByZero (which longjmps).
- The guard is emitted only when SysUtils is in scope (EDivByZero resolves
  through the symbol table); without SysUtils, division traps as before.
  This mirrors Delphi's placement of EDivByZero in System.SysUtils and keeps
  the always-linked runtime free of stdlib class machinery.
- Tests: in-process e2e proves the guard does not disturb normal division on
  both backends; shell-out cli tests (real compiler + linked stdlib) prove
  catchable div/mod-by-zero on both backends.
- Rationale recorded in language-rationale.adoc.

All three fixpoints green (FIXPOINT_OK, NATIVE_FIXPOINT_OK,
NATIVE_INTERNAL_OK); full suite OK (3345 tests).
2026-06-16 23:04:56 +01:00
Graeme Geldenhuys 5a15d4d15e fix(codegen): string relational operators (< > <= >=)
Relational comparison of strings was unhandled in both backends — only =
and <> were special-cased. The operands fell through to the integer
comparison path:

- QBE emitted a comparison that aborted QBE itself
  (amd64/isel.c: selcmp: Assertion `k != Kw' failed).
- Native silently compared the string POINTERS, giving wrong results
  (e.g. 'b' > 'a' returned False).

Both backends now route <, >, <=, >= through the existing _StringCompare
RTL helper (strcmp-like: <0 / 0 / >0) and compare its result against 0
with the matching signed condition.

Verified the full truth table ('abc' < 'abd', 'b' > 'a', <=/>= equal
cases, empty-string ordering) on both backends. Adds
TE2EStringOpsTests.TestRun_StringRelational_Order.
2026-06-16 20:04:55 +01:00
Graeme Geldenhuys 6c088024ec fix(native): spill implicit-Self method calls with >6 register slots
An implicit-Self method call in statement position (an unqualified
`Sink(...)` inside a method body) used a register-only fast path that
pushed every argument and popped it into SysVArgRegs64[I+1] — with no
check that the arguments fit the 6 System V integer registers. Self
takes %rdi, leaving 5 registers for arguments, so a call with 6+ value
args (Self + 6 = 7 slots) indexed SysVArgRegs64[6], one past the
[0..5] array. The out-of-bounds read returned an adjacent data-section
value (a garbage string pointer) that was later concatenated, crashing
_StringConcat — which is exactly how the native self-compile segfaulted.

(This was the "latent native self-compile crash" logged in bugs.txt. It
surfaced when a property-accessor helper shifted codegen enough to route
a 6+-slot implicit-Self call through this path; the workaround there
sidestepped it, but the bug was in the call-emission path itself.)

The implicit-Self statement-call path now guards the register fast path
with `CountArgSlots + 1 <= 6` and, when the slots overflow, spills the
surplus to the stack — mirroring the correct handling already present in
EmitMethodCallStmt for qualified method calls.

Also adds SysVArg64(), a bounds-checked accessor for the System V
integer-arg registers; every dynamic SysVArgRegs64[expr] read now goes
through it, so any future over-allocation raises a clear codegen error
("System V integer arg register index N out of range") instead of
silently reading past the array and miscompiling.

Verified: the pre-release segfaults native-compiling the minimal repro
(implicit-Self method, 6 args); the fix compiles and runs it correctly
(21) on both backends, and the full native self-compile succeeds. Adds
TE2ENativeTests.TestRun_Native_ImplicitSelfMethod_{Six,Seven}Args.
2026-06-16 19:39:03 +01:00
Graeme Geldenhuys d15501af7b fix: correct 0-based string iteration in FNV-1a hash loops
Both FNV-1a hash helpers carried over FPC's 1-based string indexing,
which is wrong in Blaise's 0-based world: each loop skipped the first
byte (index 0) and read one byte past the end (index Length).

  - uDebugOPDF.FNV1a32          (32-bit; OPDF type IDs)
  - uUnitInterfaceIO.ContentHashFnv1a64 (64-bit; .bif source hash)

Both now use 'for B in S' byte iteration, which is bounds-safe and is
also the natural iteration for FNV-1a (defined over the byte stream).
Output validated against a reference FNV-1a32 implementation.

No layout/ABI impact: OPDF type IDs are opaque internal tokens, and the
.bif SourceHash is a staleness cache key. Existing .bif caches will
mismatch once and trigger a one-time recompile, which uUnitLoader
already handles.
2026-06-16 18:50:56 +01:00
Graeme Geldenhuys 07a1236896 feat(sets): subset/superset operators; reject enum-member shadowing
Two related set/enum fixes from shaking the sets cluster.

1. Set subset (<=) and superset (>=) operators, e.g. `if s <= t`:
   - Semantic: <= and >= on two sets yield Boolean (alongside =, <>).
   - Codegen both backends: non-jumbo sets test (s and not t)=0 for <=
     (operands swapped for >=); jumbo sets call a new RTL _SetSubset
     (added to blaise_set.pas) which returns whether every bit of A is in B.
   Completes the set-operator suite (+, -, *, in, =, <>, <=, >= now all work).

2. Reject a variable that shadows a visible enum member (e.g. `var c` when
   an enum member `C` is visible — Pascal is case-insensitive). Such a
   shadow silently retargeted the member in a set literal `[A, C, D]` to the
   variable: QBE errored with "Set literal element 'c' is not a constant",
   and — worse — the native backend produced a WRONG bitmask with no error
   (the member's bit was dropped). This surfaced as a corrupt for-in over a
   set when the loop variable collided with a member. Now rejected at the
   declaration with a clear message, mirroring the existing type-name-shadow
   rule (FPC/Delphi allow the shadow; Blaise does not).

Verified subset/superset truth tables and for-in over a set (no shadow) on
both backends, and that the shadow is now a compile error. Adds
TE2ESetOpsTests.TestRun_Set_SubsetSuperset and
TSemanticTests.TestVarDecl_ShadowsEnumMember_RaisesError. Rationale's
"Variable Names May Not Shadow Type Names" section extended to enum members.
2026-06-16 17:00:09 +01:00
Graeme Geldenhuys f4c612ef77 feat: case-label ranges and Succ/Pred ordinal builtins
Two standard-Pascal ordinal features that were missing, found while
shaking the sets/enums/case cluster:

1. case-label ranges — `2..4:` as a case label:
       case i of
         0, 1: ...;
         2..4: ...;      // previously: Parse error "Expected ':' but got '..'"
       else ...;
       end;
   The parser now accepts `lo..hi` per case label (reusing TSetRangeExpr to
   represent the range, as set literals already do). Semantic checks both
   bounds against the selector type; codegen matches a range with a
   (sel >= lo) and (sel <= hi) test on both backends. Singles and ranges may
   be mixed in one branch (`5, 7..9:`), and enum ranges work (`Red..Yellow`).
   Ranges are rejected for a string selector.

2. Succ(x)/Pred(x) — the next/previous value of an ordinal (enum or integer
   family); previously "Undeclared function 'Succ'". The result keeps the
   argument's type, so Succ(enumValue) is still that enum. Lowered to +1 / -1
   on the ordinal value on both backends.

Verified case ranges (int + enum, mixed with singles, with else) and
Succ/Pred (integer + enum, nested, type-preserved) on both backends. Adds
five dual-backend e2e tests in cp.test.e2e.controlflow.pas.
2026-06-16 16:42:16 +01:00
Graeme Geldenhuys 9b45a6bbff feat(generics): out-of-line impl form for generic methods + arg checking
Completes generic methods: the out-of-line implementation form now parses
and links, alongside the inline-body form added previously.

    type TUtil = class
      function Pick<T>(cond: Boolean; a, b: T): T;   // declaration
    end;
    function TUtil.Pick<T>(cond: Boolean; a, b: T): T;  // out-of-line body
    begin if cond then Result := a else Result := b end;

- Parser: a method-level <T> appearing AFTER the qualified name
  (Owner.Method<T>) is now parsed into TypeParams. (The <T> before the dot
  remains the owner's type params, as for TList<T>.Add.)
- Semantic: LinkClassMethodImpls detects a generic-method impl
  (TypeParams <> nil) and transfers its body onto the in-class
  generic-method template, instead of trying to resolve its T-typed params.

Also fixes a real hole found via the out-of-line probe: generic-method
call arguments were not type-checked against the parameters (the path
bypasses ResolveMethodOverload), so e.g. passing a string for a Boolean
parameter compiled silently. The call site now validates argument count
and types against the monomorphised signature.

Verified out-of-line impl (string + Integer instantiations) on both
backends, and that a mismatched argument is now rejected. Adds
TE2EGenericsTests.TestRun_GenericMethod_OutOfLineImpl. Grammar and
language-rationale updated to drop the inline-only caveat.
2026-06-16 16:30:07 +01:00
Graeme Geldenhuys 3490a03aa7 feat(generics): generic methods (method-level type parameters)
A class or record method may now declare its own type parameters,
independent of the enclosing type, and is monomorphised per call site:

    type TUtil = class
      function Pick<T>(cond: Boolean; a, b: T): T;
        begin if cond then Result := a else Result := b end;
    end;
    ...
    u.Pick<Integer>(True, 7, 9);     // -> 7
    u.Pick<string>(False, 'a', 'b'); // -> 'b'

Each distinct set of explicit type arguments produces one concrete body
named <Owner>_<Method>_<Args> (e.g. TUtil_Pick_Integer); the implicit
Self is preserved, so a generic method can read the receiver's fields and
call its other methods. This is distinct from (and composes with) methods
of a generic CLASS.

Implementation mirrors the existing generic free-function machinery:
- Parser: a method-call folds an explicit <...> type-arg list into the
  method name (Pick<Integer>), using the same two-token '<' lookahead as
  generic free-function calls.
- Semantic: generic-method templates (TypeParams <> nil) are registered
  by Owner.Method and skipped from signature/vtable/body analysis;
  InstantiateGenericMethod clones the template, substitutes the type
  params, keeps OwnerTypeName + Self, analyses via AnalyseMethodDecl, and
  records a TGenericMethodInstance (deduplicated per owner+args). The
  call site resolves obj.M<T>(...) to the instance.
- Codegen (both backends): GenericMethodInstances are emitted as ordinary
  methods (Self param + mangled ResolvedQbeName).

Verified pick/echo, two distinct instantiations (Integer + string), use
of a Self field, and two type parameters on both backends. Adds IR tests
(TGenericFuncTests.TestCodegen_GenericMethod_{Body,Call}Emitted) and
dual-backend e2e tests (TE2EGenericsTests.TestRun_GenericMethod_*).
Grammar and language-rationale updated.

Limitation (logged in bugs.txt): only the inline-body declaration form is
supported; the out-of-line  function TOwner.Method<T>(...)  form is not
yet parsed.
2026-06-16 16:13:12 +01:00
Graeme Geldenhuys 42ca9d7535 feat(stdlib): TList<T> default array property — List[i] subscript
TList<T> now exposes a `default` array property, so elements can be read
and written with the familiar subscript syntax instead of .Get(i):

    L[0] := 100;            // L.SetItem(0, 100)
    WriteLn(L[0] + L[1]);   // L.Get(i)

Adds TList<T>.SetItem (a pointer-target store, so the compiler's ARC
discipline releases the old element and retains the new for managed T)
and the property Items[AIndex]: T read Get write SetItem; default.

Also fixes the generic-instance clone path in the semantic analyser: it
copied every property-decl field except IsDefault, so a default property
declared on a generic template (TList<T>) lost its default flag on
instantiation and List[i] did not resolve. The clone now carries
IsDefault.

Verified List[i] read, write, and polymorphic element dispatch
(List[i].Area through a base-typed element) on both backends. Adds
TE2ETListTests.TestRun_TList_DefaultProperty_{ReadWrite,Polymorphic}.

Note: this makes the stdlib depend on the `default` directive (added in
the previous commit), so the pre-release bootstrap binary is refreshed to
a default-aware stage-2 in a follow-up.
2026-06-16 14:57:50 +01:00
Graeme Geldenhuys 798df8eb15 feat(properties): default array property — Obj[I] sugar
Adds the `default` directive on an indexed property, enabling subscript
sugar on the object itself:

    property Items[I: Integer]: T read Get write Put; default;
    ...
    V[0] := 10;        // lowers to V.Put(0, 10)
    WriteLn(V[0]);     // lowers to V.Get(0)

This is the mechanism behind the familiar List[i] syntax and was a real
foundational gap (the directive did not even parse — "Expected ':'").

Implementation:
- Parser: accept the trailing `default;` directive after a property
  declaration; set TPropertyDecl.IsDefault.
- AST / symbol table: IsDefault on TPropertyDecl and TPropertyInfo;
  TRecordTypeDesc.FindDefaultProperty walks the inheritance chain.
- Semantic: Obj[I] read (AnalyseStringSubscriptExpr) synthesises the
  default property's field access and reuses the indexed-property read
  path; Obj[I] := V write (AnalyseStaticSubscriptAssign) records the
  setter on the TStaticSubscriptAssign node.
- Codegen (both backends): the write path emits the setter call via the
  existing PropAccessorTarget / EmitPropAccessorCallNative helpers, so it
  honours virtual/override on the accessor; the read path delegates the
  TStringSubscriptExpr to its folded property-read field access.
- Cross-unit: IsDefault is serialised in the .bif interface so a default
  property declared in one unit keeps its subscript sugar elsewhere.

Verified read, write, string-element, inherited, and cross-unit cases on
both backends. Adds IR tests (TPropertyTests.TestCodegen_DefaultProperty_
{Read,Write}) and dual-backend e2e tests (TE2EPropertyTests.TestRun_
DefaultProperty_{ReadWrite,StringElement,Inherited}). Grammar and
language-rationale updated; the two new TStaticSubscriptAssign fields are
marked safe in bif-coverage.status.
2026-06-16 14:48:38 +01:00
Graeme Geldenhuys 0f707d20ac fix(semantic): generic class implementing a non-generic interface
A generic class that implements a non-generic interface could not be
assigned to an interface variable:

    IVal = interface function Get: Integer; end;
    TBox<T> = class(IVal) ... function Get: Integer; ... end;
    iv: IVal := TBox<Integer>.Create(77);   // expected IVal, got TBox<Integer>

The first heritage entry of class(X) is parsed into ParentName whether X
is a class or an interface. The generic-instance clone path only moved
ParentName to ImplementsNames (so AddImplements would be called) when X
was a GENERIC interface — its name contained '<'. A plain interface name
like IVal failed that test, stayed in ParentName, and the instance's
implements list ended up empty, so class -> interface compatibility
rejected the assignment.

The move now fires for any ParentName that resolves to tyInterface,
generic or not, mirroring the non-generic class path. Verified a generic
class used through its interface, including a method with arguments, on
both backends. Adds TE2EGenericsTests.TestRun_GenericClassImplements
Interface and _MethodArgs.
2026-06-16 14:29:05 +01:00
Graeme Geldenhuys 6fc43d246c fix(codegen): emit itabs for inherited (base) interfaces
A class implementing a derived interface could not be narrowed directly
to a base interface:

    IDog = interface(IAnimal) ... end;
    TDog = class(IDog) ... end;
    a: IAnimal := TDog.Create;   // link error: undefined itab_TDog_IAnimal

The class only emitted the itab for the interface it directly named
(itab_TDog_IDog), so assigning the instance to a base-interface variable
or parameter referenced an itab that was never generated. (The type
check itself was fixed in 37690de; this is the codegen half.)

EmitInterfaceDefs on both backends now expands each implemented interface
to its full parent chain, deduplicates, and emits an itab plus an
impllist entry for every ancestor. An ancestor's interface methods are a
prefix of the descendant's itab layout, so the same implementation
pointers apply — the base itab is just the leading slots.

Verified class-instance -> base-interface variable and parameter on both
backends. Adds TE2EInterfaceTests.TestRun_ClassImplDerived_To{BaseVar,
BaseParam}.
2026-06-16 14:22:23 +01:00
Graeme Geldenhuys 37690dea45 fix(semantic): honour interface inheritance in type compatibility
A value of a derived interface was not accepted where a base interface
was expected:

    IAnimal = interface function Name: string; end;
    IDog = interface(IAnimal) function Bark: string; end;
    d: IDog; a: IAnimal;
    a := d;          // rejected: "expected IAnimal but got IDog"
    Describe(d);     // (a: IAnimal) -> "No matching overload"

CheckTypesMatch treated the two interfaces as unrelated. It now walks the
TInterfaceTypeDesc.Parent chain (new helper InterfaceInheritsFrom):

- interface -> base interface: assignable when the expected interface is
  on the actual interface's parent chain (IDog is-a IAnimal).
- class -> interface: a class that implements a DESCENDANT of the expected
  interface also satisfies it.

ArgMatchScore picks this up automatically through its CheckTypesMatch
fall-through, so parameter passing and overload resolution work too.

Verified derived->base assignment, derived-arg->base-param, and a
three-level interface chain (IC -> IA) on both backends. Adds
TE2EInterfaceTests.TestRun_{DerivedInterface_ToBaseVar,
DerivedInterface_ToBaseParam,ThreeLevelInterfaceChain}.

Note: assigning a class INSTANCE that implements a derived interface
directly to a base-interface variable still needs an itab for the base
that is not yet emitted (logged in bugs.txt); the interface->interface
copy path used by these tests does not need it.
2026-06-16 14:16:38 +01:00
Graeme Geldenhuys 659071f136 fix: inherit a non-generic class from a generic-class instance
A non-generic class could not inherit from a generic-class instance:

    TBox<T> = class ... end;
    TIntBox = class(TBox<Integer>) ... end;   // rejected, then bad IR

Two bugs:

1. Semantic: AnalyseTypeDecls assumed any generic name in the first
   heritage slot was an interface — it cast FindTypeOrInstantiate's
   result to TInterfaceTypeDesc and moved it to the implements list, so a
   generic CLASS parent was rejected as "Unknown interface 'TBox<Integer>'
   in implements list". It now classifies the instantiated descriptor: a
   tyInterface result is an implements entry, anything else stays as the
   parent class for normal class-parent resolution.

2. QBE codegen: the inheriting class's typeinfo referenced its parent as
   $typeinfo_TBox<Integer> via ClassSymName (which does not mangle <>),
   but the generic instance's typeinfo is defined under the QBEMangle'd
   symbol $typeinfo_TBox_Integer — producing invalid QBE IR ("invalid
   character <"). The parent reference now uses QBEMangle for a generic
   instance, matching its definition. The native backend already mangled
   correctly and only needed the semantic fix.

Verified inherited method + field access and a virtual method declared on
the generic base and overridden in the derived class, on both backends.
Adds TE2EGenericsTests.TestRun_InheritFromGenericInstance_{MethodAndField,
VirtualOverride} and a note in docs/language-rationale.adoc.
2026-06-16 14:08:59 +01:00
Graeme Geldenhuys d91be47103 fix(codegen): class const accessed via the type name, both backends
Accessing a class const through the class type (TThing.MaxCount, not an
instance) was broken differently on each backend:

- Native: EmitExprToEax had no IsConstant case for TFieldAccessExpr, so
  the access raised "unsupported expression form TFieldAccessExpr".
- QBE: the string class-const path emitted
  `$_StringRetain(l $%tN)` — a non-existent RTL symbol, with the literal
  temp wrongly double-prefixed by `$` (invalid IR). This path had never
  worked; it was the only reference to the phantom _StringRetain.

Native now emits the inlined constant (movabsq for ordinals,
EmitStrLitAddr for strings), mirroring the IDENT-const path. QBE returns
the string literal's data-pointer temp directly, exactly like a plain
string literal (the consumer manages ARC). Accessing the same const via
an instance (t.MaxCount) already worked on both.

Adds TE2EClasses2Tests.TestRun_ClassConst_{IntViaType,StringViaType,
ViaInstanceAndType} (dual-backend); the string test also concatenates the
const to exercise ARC.
2026-06-16 13:16:51 +01:00
Graeme Geldenhuys 536610a7c5 fix(semantic): merge method overload sets across inheritance
A derived class that declared an `overload` method with the same name as
an overload inherited from its base SHADOWED the inherited variants
instead of merging with them:

    TBase = class
      function F(x: Integer): string; overload;
    end;
    TDerived = class(TBase)
      function F(s: string): string; overload;
    end;
    d.F('a');  // ok
    d.F(5);    // was rejected: "expected string but got Integer"

Two causes, both fixed:

1. ResolveMethodOverload stopped walking the inheritance chain as soon
   as the first class declaring the name yielded any arity match, so the
   base overloads were never considered. It now unions the overload
   groups up the parent chain, stopping only when a level declares the
   name WITHOUT `overload` — a non-overload method hides inherited ones
   (the conventional redeclaration-hides rule), an `overload` method
   extends the set (Delphi semantics).

2. The variable-receiver method-call path used FindMethodDecl (first
   match in the chain) and never invoked overload resolution at all. It
   now analyses the arguments and calls ResolveMethodOverload, falling
   back to FindMethodDecl for the no-overload case. (Arguments must be
   analysed before resolution so candidates can be scored against their
   resolved types.)

Adds TE2EInheritTests.TestRun_OverloadMergeAcrossInheritance (dual
backend) and an "Overload sets merge across inheritance" subsection to
docs/language-rationale.adoc.
2026-06-16 13:10:12 +01:00
Graeme Geldenhuys 519aefdc5c fix(codegen): virtual-dispatch property accessors through the vtable
A property whose getter or setter is declared `virtual` did not dispatch
through the vtable: reading or writing the property through a base-typed
variable holding a derived instance called the BASE accessor, not the
override. A direct b.GetVal() call dispatched correctly, but b.Val (the
property over the same virtual getter) did not.

    TBase = class
      function GetVal: Integer; virtual; begin Result := 1; end;
      property Val: Integer read GetVal;
    end;
    TDerived = class(TBase)
      function GetVal: Integer; override; begin Result := 99; end;
    end;
    b: TBase := TDerived.Create;
    WriteLn(b.Val);   // was 1, now 99

The property read/write lowering always emitted a static call to the
accessor's declaring class. Now the semantic pass records the accessor's
vtable slot on the AST node (PropAccessorVSlot, -1 when the accessor is
not virtual), and codegen dispatches through the vtable when the slot is
>= 0, exactly as a direct method call does.

QBE: PropAccessorTarget computes the call target — emitting the vptr+slot
loads and returning the function-pointer temp for a virtual accessor, or
the static mangled symbol otherwise — and each call site emits its own
`call <target>(...)`. (A single emit-and-return helper was tried first
but tripped a latent native-backend miscompile on the self-compile; see
bugs.txt. The target-string shape avoids it.) Native:
EmitPropAccessorCallNative dispatches through the vtable or statically.

Covers getter and setter, on both backends. Adds
TE2EInheritTests.TestRun_VirtualProperty{Getter,Setter}_Dispatches
(dual-backend). The two new AST fields are marked `safe` in
bif-coverage.status (semantic-set, not serialised).
2026-06-16 12:59:46 +01:00
Graeme Geldenhuys 0d612485b8 fix(semantic): resolve inherited property accessors to declaring class (#111)
A child class that inherits a method-backed property failed to link:

    Tbase = class
      property classValue: Integer read getValue write setValue;
    end;
    Tchild = class(Tbase) ... end;
    child.classValue := 12;   // -> undefined reference to Tchild_setValue
    writeln(child.classValue); // -> undefined reference to Tchild_getValue

Property accessors are mangled as <owner>_<method>. The read/write
lowering set PropOwnerType to the static variable's class (Tchild), but
the getter/setter symbols are only emitted for the class that declares
them (Tbase). The call referenced Tchild_getValue / Tchild_setValue,
which never exist, so linking failed on both backends.

Adds PropAccessorOwner, which walks the inheritance chain from the
variable's class upward and returns the class that actually declares the
accessor (falling back to the static type when none is found, leaving
the non-inherited case unchanged). All property read and write lowering
sites — direct, qualified-base, implicit-Self, and indexed-property-on-
field-type — now resolve PropOwnerType through it.

Regression test TestRun_InheritedProperty_AccessorsResolveToParent in
cp.test.e2e.classes2.pas compiles + runs the issue's program on both
backends and asserts stdout '12'.

Fix approach contributed via the issue thread by andrewd207; extended to
cover the indexed-property-on-field-type accessor sites as well.
2026-06-16 12:17:13 +01:00
Graeme Geldenhuys 9c87347a9e test(inherit): add dual-backend e2e tests for class inheritance
cp.test.inherit.pas asserted only on generated QBE IR substrings and
never fed the IR to a backend, so virtual dispatch, inherited calls,
multi-level field layout, and destructor chaining were never proven to
run correctly — exactly the kind of gap where plausible-looking IR can
still produce wrong runtime behaviour.

Adds cp.test.e2e.inherit.pas with nine tests that compile + run on BOTH
the QBE and native x86-64 backends and assert on stdout:

- three-level virtual override reached through a base-typed variable
- inherited call inside an override
- constructor chain via inherited Create
- polymorphic dispatch through a base-typed array
- is / as operators with downcast
- virtual dispatch from inside a base constructor (template method)
- four-level field inheritance (offset accumulation)
- double dispatch (override -> inherited -> virtual)
- virtual destructor chain via inherited Destroy

All nine pass on both backends. No compiler source changed.
2026-06-16 12:06:46 +01:00
Graeme Geldenhuys 031ea9a8c0 fix(codegen): chained element write on dynamic-array-of-dynamic-array
A chained subscript WRITE on a dynamic array of dynamic arrays —
m[i][j] := v — was rejected at semantic time with "Multi-dimensional
subscript base must be a static array". Only a static-array base was
accepted in the chained-assign path; reading via an intermediate row
variable (row := m[i]; row[j] := v) worked, but the direct write did
not, making jagged 2-D+ dynamic arrays awkward to fill.

Semantic: AnalyseStaticSubscriptAssign now accepts a tyDynArray base in
the BaseExpr (chained) path and derives the element type from
TDynArrayTypeDesc.

Codegen (both backends): when the chained-write base resolves to a
dynamic array, evaluate BaseExpr (m[i]) to the inner array's data
pointer and index into it — heap indirection per level — instead of the
inline-offset static-array path. QBE adds a BaseExpr branch in the
dynarray block of EmitStaticSubscriptAssign. Native stashes the base
pointer below the spilled value on the stack and reloads it at the
base-resolve step, with matching cleanup on every Exit path (float,
record, string/class, and scalar).

Verified 2-D int, 2-D string (with overwrite), and 3-D int nesting on
both backends. Adds three dual-backend e2e tests in
cp.test.e2e.dynarray.pas and a "Dynamic-array nesting (jagged arrays)"
subsection to docs/language-rationale.adoc.
2026-06-16 12:03:11 +01:00
Graeme Geldenhuys 267c825237 fix(codegen): accept dynamic array as open-array argument
A dynamic-array variable passed where an open-array parameter was
expected was rejected with "No matching overload", and even once the
semantic check accepted it the codegen emitted the wrong (data, high)
pair: it loaded a `<name>_high` slot that only exists for static-array
open-array temporaries.

Semantic pass: CheckTypesMatch and ArgMatchScore now treat tyDynArray
as assignment-compatible with a tyOpenArray parameter when the element
types match.

Codegen (both backends): when a tyDynArray argument is coerced to an
open-array parameter, pass the data pointer plus (runtime length - 1)
as the high index, where the length comes from _DynArrayLength at run
time rather than a compile-time static bound. QBE: OpenArrayArgFragment
gains a dynarray case (using distinct temps for the w-length and the
l-high computation to keep the IR well-typed). Native: the three
open-array argument emission paths (EmitMethodArgPush plus the two
call-site push/slot handlers) each gain a tyDynArray case.

Adds five dual-backend e2e tests in cp.test.e2e.openarray.pas covering
sum, High/Length, empty array, array-of-string, and pass-to-nested.
2026-06-16 11:56:36 +01:00
Graeme Geldenhuys 51efaefa9d fix(dynarray): allow SetLength on an array element (ragged 2-D arrays)
SetLength on an array element was rejected:
    var m: array of array of Integer;
    SetLength(m, 2); SetLength(m[0], 3);   -> "First argument of 'SetLength'
                                              must be an assignable variable"
This blocked ragged 2-D dynamic arrays (resizing each inner row).  The
SetLength first-argument check accepted only a variable or field, not an
array-element l-value.

Semantic: the check now routes through the shared IsVarArgLValue helper, which
also accepts an array-element subscript (the type check already requires the
element to be a string or dynamic array).  Codegen: QBE's EmitDynArraySetLength
already used EmitLValueAddr (extended for subscripts in the var-arg fix);
native's EmitLValueSlotAddr now handles a TStringSubscriptExpr element slot
(its address is what @m[i] computes).

Found by the e2e dynamic-array hardening sweep.  New file
cp.test.e2e.dynarray.pas (8 tests, all run on BOTH backends): SetLength fill,
Length/High, grow-preserves, array-of-string, reference semantics, return by
value, ragged 2-D via SetLength(m[i], n), fill-inner-via-row.

(Two further dynarray gaps found and logged for follow-up: m[i][j] := v write
on dyn-of-dyn, and open-array param + dynarray arg compatibility.)

All three fixpoints + full suite (3283 tests) pass.
2026-06-16 11:43:25 +01:00
Graeme Geldenhuys 2ec86bf7f6 test(records): expand record e2e coverage (both backends)
Adds four record e2e tests from the hardening sweep, all run on BOTH backends:
record method mutating Self, record-return-field used inline (Make(V).X),
record-with-static-array-field deep copy (y := x then y.A[i] must not alias x),
and a nested-record method call.

No compiler change — every record case probed (value-copy semantics, by-value
params, return-by-value, nested copy, string-field ARC, static-array-field
deep copy, methods) was already correct on both backends; this locks in the
proof.

Test-only; full suite 3275 tests green.
2026-06-16 11:36:59 +01:00
Graeme Geldenhuys 09c20cd379 test(pointers): expand pointer e2e coverage (both backends)
Adds five pointer e2e tests run on BOTH backends (AssertRunsOnAll), from the
hardening sweep's pointer probing: pointer-to-record-field write, pointer
parameter, pointer equality, pointer-to-array-element, and a GetMem-built
linked list traversal.

No compiler change — pointer codegen was already correct on both backends for
every case probed; this just locks in the proof.  (Separately, New/Dispose are
not Blaise builtins — a language-design question logged in bugs.txt, not a bug.)

Test-only; full suite 3271 tests green.  Compiler unchanged since 64160a0, so
the three fixpoints from that commit still hold.
2026-06-16 11:33:15 +01:00
Graeme Geldenhuys 64160a0745 fix(varparam): accept an array element as a var/out argument
Passing an array element to a var/out parameter was rejected:
    procedure Swap(var A, B: Integer); ...
    Swap(a[0], a[1]);   -> "var argument 1 of 'Swap' must be a variable"
An array element is a legitimate addressable l-value (Swap(a[i], a[j]) is
idiomatic Pascal); only a plain variable, field access, or pointer deref were
accepted.

Semantic: the three general var-param l-value checks now route through a shared
IsVarArgLValue helper that also accepts a TStringSubscriptExpr over an array
base (static / dynamic / open array).  String and PChar subscripts (a char by
value) are still rejected, as are literals and expressions.  The stricter
Delete/SetLength first-argument checks are unchanged.

Codegen: EmitLValueAddr (QBE) and EmitVarArgAddrToRax (native) now handle a
TStringSubscriptExpr actual by reusing the existing element-address logic (via
a transient TAddrOfExpr), so the element's address is passed by reference.

Found by the e2e var/out parameter hardening sweep.  New file
cp.test.e2e.varparams.pas (11 tests, all run on BOTH backends): var int/swap/
string, out params, var record, nested var call, var class field, and array
element (static/dynamic/string) as a var actual incl. Swap(a[0], a[1]).

All three fixpoints + full suite (3266 tests) pass.
2026-06-16 11:26:24 +01:00
Graeme Geldenhuys 0a9577e6d9 fix(qbe): return address (not loaded value) for an implicit-Self aggregate field
Reading a static-array field of the current object via implicit Self inside a
method (FD[i], no explicit Self.) segfaulted on QBE:
    type TC = class FD: array[0..4] of Integer;
      function At(i: Integer): Integer; begin Result := FD[i] end; end;
The implicit-Self bare-field path in EmitExpr returned the field's storage
ADDRESS only for tyRecord; every other field kind was loaded as a value.  A
static array lives inline in the object, so loading `FD` dereferenced the
array's first 8 bytes as a pointer; the subsequent FD[i] indexing then ran off
that garbage pointer.  Native handled it; only QBE was broken.

Now the address is returned for any aggregate-address type (record, static
array, jumbo set) via IsAggregateAddrType, so `FD` yields &FD and FD[i]
indexes the inline array correctly.  This also fixes indexed properties backed
by a static-array field (the common Items[I] pattern), which is how the bug
first surfaced.

Found by the e2e properties hardening sweep.  New file
cp.test.e2e.properties.pas (11 tests, all run on BOTH backends): property
read/write, getter/setter, read-only, string, default value, inherited,
indexed; static-array field read/write/sum in a method; static-array-of-record
field.

All three fixpoints + full suite (3255 tests) pass.
2026-06-16 11:12:45 +01:00
Graeme Geldenhuys 8c8913a27b fix(qbe): pass a class instance directly to an interface parameter
Passing a class value to an interface-typed parameter (implicit
class->interface narrowing at the call site) produced a QBE link error,
referencing a nonexistent $Name_obj symbol:
    function Describe(N: INamed): string; ...
    Describe(t)        // t: TThing implementing INamed
    -> undefined reference to `t_obj'
The native backend handled it; only QBE was broken — the first
backend-divergence bug of the e2e hardening sweep.

EmitInterfaceExprPair's TIdentExpr branch assumed the source was
interface-typed (split obj/itab slots, addressed as Name_obj/Name_itab).
For a class-typed source there is just a single class pointer, so it emitted
a reference to the non-existent Name_obj. Now a class-typed expression
narrowed to an interface is handled directly: the value is the obj pointer,
paired with the static $itab_<Class>_<Interface> (mirroring the existing
class->interface assignment path). The target interface type is threaded
through InterfaceArgFragment/EmitInterfaceExprPair from the param type at the
call sites that lacked an inline class check.

Covers class var (global + local), constructor result, and procedure
interface params. Found by the e2e interfaces hardening sweep. New file
cp.test.e2e.interfaces.pas (8 tests, all run on BOTH backends).

All three fixpoints + full suite (3244 tests) pass.
2026-06-16 10:56:04 +01:00
Graeme Geldenhuys 875d511b51 fix(generics): allow a local var named after a type parameter
In a generic method/function, a local variable whose name matched the type
parameter (case-insensitively) was wrongly rejected:
    function R: T; var t: T; begin t := V; Result := t end;
    -> "Duplicate identifier 't' — a type with this name is already visible"
During instantiation each type parameter is registered as a skType alias
(T = Integer) so the body resolves, and the shadow-a-type-name check (#102)
then saw the local `t` as shadowing the type `T`.  But a type PARAMETER is a
placeholder, not a user-declared type the programmer is shadowing, so a local
sharing its name is legitimate.

Tracks the type-parameter names currently in scope (FActiveTypeParams, pushed
around instantiated class/record body analysis) and exempts them from the
shadow check.  A genuine var-vs-type clash (var TFoo vs type TFoo) is still
rejected — verified.

Found by the e2e generics hardening sweep. Regression:
TE2EGenericsTests.TestRun_GenericClass_LocalNamedLikeTypeParam and
TestRun_GenericRecord_LocalNamedLikeTypeParam, both backends. The #102
shadow-type tests (TSemanticTests) still pass.

All three fixpoints + full suite (3236 tests) pass.
2026-06-16 10:39:04 +01:00
Graeme Geldenhuys fcf4d36fad fix(generics): substitute type param in generic-function body locals
A generic free function with a local variable of the type parameter failed:
    function Echo<T>(X: T): T; var tmp: T; begin tmp := X; Result := tmp end;
    -> "Semantic error: Unknown type 'T'"
Signature substitution (params + return) was applied during instantiation, but
the cloned body's local var declarations still referenced the bare type
parameter, so AnalyseStandaloneDecl saw `var tmp: T` with T unresolved.

InstantiateGenericFunc now substitutes the type parameter in the cloned body's
TVarDecl.TypeName entries (via SubstTypeParam, which already handles T, ^T, and
nested SomeName<T>), so a local of type T (or ^T, or List<T>) resolves to the
concrete type. Generic class/record method bodies already substituted their
locals; this brings free functions in line.

Found by the e2e generics hardening sweep. New file cp.test.e2e.generics.pas
(10 tests, all run on BOTH backends): generic funcs incl. typed locals,
paramless+typed-local-return, two typed locals; generic classes/records;
method typed locals; two type params; distinct instantiations; nesting.

All three fixpoints + full suite (3234 tests) pass.
2026-06-16 10:32:47 +01:00
Graeme Geldenhuys e2b715b53b fix(parser): support nested generic type arguments
TList<TList<Integer>> and TBox<TPair<Integer, string>> failed to parse:
each type argument was read as a single bare identifier, with no recursion
into a nested <...>, so a nested '<' produced "Expected '>' but got '<'"
(type position) or "Expected '.' or '(' after generic type arguments"
(constructor/expression position).

Both type-argument parse sites now recurse through ParseTypeName, so a type
argument may itself be a generic specialisation, to arbitrary depth
(TList<TList<TList<Integer>>> works). The expression-position heuristic also
accepts tkLessThan as the lookahead-2 token (the first arg being itself
generic). The comparison-operator disambiguation (a < b, (a < b) and (b < c))
is unaffected.

Found by the e2e generics hardening sweep. Regression:
TE2EMiscTests.TestRun_NestedGenericTypeArgs (TBox<TBox<Integer>>, both
backends). docs/grammar.ebnf TypeArgList rule updated to recurse via
GenericName.

All three fixpoints + full suite (3224 tests) pass.
2026-06-16 10:13:45 +01:00
Graeme Geldenhuys 236736a3be fix(parser): accept parenthesised lvalue as assignment target
A statement beginning with '(' was rejected ("Expected statement"), so a
parenthesised cast could not be an assignment TARGET:
    (a as TB).FX := 42;   -> Parse error
Reading the same expression worked, and the hard-cast target TB(a).FX := 42
worked, so only this statement-parser entry point was missing.

ParseStmt now handles a leading '(' by parsing the parenthesised expression
and requiring a '.Field := Expr' suffix, building a TFieldAssignment whose
receiver is that expression (ObjExpr) — the same AST + semantic + codegen path
already used for element-field writes (a[i].F := v). No AST/semantic/codegen
change was needed; the gap was purely the parser.

Found by the e2e test-hardening sweep of the inheritance cluster. Regression:
TE2EMiscTests.TestRun_ParenCastAsAssignmentTarget, run on BOTH backends.
docs/grammar.ebnf FieldAssignment rule updated.

All three fixpoints + full suite (3223 tests) pass.
2026-06-16 10:06:40 +01:00
Graeme Geldenhuys 74f71023da feat(oop): support 'inherited Method()' in expression position
`inherited` previously parsed only as a statement, so calling an inherited
FUNCTION and using its result failed to parse:
    Result := inherited Value() + 100;   -> "Parse error: Expected expression"
This blocked the normal OOP pattern of an overriding function extending its
parent's result.

Adds the expression form alongside the existing statement form:
* uAST: new TInheritedCallExpr (sibling of TInheritedCallStmt) + CloneExpr case.
* uParser: tkInherited handled in ParseFactor (primary expression).
* uSemantic: AnalyseInheritedCallExpr resolves to the parent method (must be a
  non-void function) and sets ResolvedType to the return type.
* Codegen: static (non-virtual) call to the parent slot, result returned as a
  value. QBE EmitInheritedCallExpr; native EmitInheritedCallSeq (shared by the
  statement and expression forms) leaves the result in %rax/%xmm0.
* uUnitInterfaceIO: 'inhc' encode/decode so inherited-expr in an inline method
  body round-trips through the .bif unit-interface cache; bif-coverage.status
  marks the two fields serialise (bif-coverage OK).
* docs/grammar.ebnf + docs/language-rationale.adoc updated (same commit).

Found by the e2e test-hardening sweep of the inheritance feature cluster.
Regression: TE2EMiscTests.TestRun_InheritedFunctionCall_InExpression covers a
value-returning inherited call and an inherited call with an argument, run on
BOTH backends via AssertRunsOnAll.

All three fixpoints (FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK) and
the full suite (3222 tests) pass.
2026-06-16 10:01:26 +01:00
Graeme Geldenhuys 5f340f0956 perf(asm): amortized byte buffers in the internal assembler's ELF writer
Assembling the compiler's own object with --assembler internal needed ~53 GB
of peak memory and OOM-killed. Root cause: blaise.elfwriter built byte data by
string concatenation (X := X + ...), which reallocates and copies the whole
buffer on every append — O(n^2). The dominant cost was Finish() building the
.strtab and .symtab one element at a time across 32914 symbols; the section
bodies (~2 MB .text) and the final image assembly had the same shape.

Introduces TByteBuf, a capacity-doubling array-of-Byte buffer (amortized O(1)
append, bulk memcpy for byte<->string and buf<->buf), and routes the section
storage, the strtab/symtab/rela tables, and the final ELF image assembly
through it. TElfWriterSection.Data (string) becomes Bytes+Count (array of
Byte) — also the natural shape for the future internal linker, which needs
indexed byte access into section bodies.

Result: the internal assembler builds the whole compiler in ~5 s using
~740 MB peak RSS (was OOM at 54 GB) — a 73x memory reduction. Note: this is
allocation churn, not a leak; ARC was freeing the old buffers correctly (the
--debug leak tracker shows no new leaks), but the O(n) reallocation volume
blew the RSS high-water mark.

The internal assembler now self-hosts on both backends (the QBE-compiled path
relies on the @(class-field array)[idx] address fix in the previous commit).

Both fixpoints, the internal-assembler conformance guard, and the full suite
(3221 tests) pass.
2026-06-16 02:06:45 +01:00
Graeme Geldenhuys 32756f6f25 fix(qbe): load instance pointer for @(class-field array)[idx] address
TCodeGenQBE.EmitAddrOfExpr's @Rec.Arr[I] branch (TFieldAccessExpr with
IsArrayAccess) computed the receiver base for Base / IsVarParam / plain-local
records but was MISSING the IsClassAccess and IsImplicitSelf cases. For a
class instance variable it fell through to VarRef(RecordName) = $Obj, i.e. the
ADDRESS of the variable's slot, instead of loadl-ing $Obj to get the instance
pointer. So @Obj.Arr[I] emitted "add $Obj, off" and produced a garbage element
address that crashed when the pointer was actually used (e.g. passed to an
external memcpy).

Adds the IsClassAccess case (load the slot to get the instance pointer; load
twice for a var-param class) and the IsImplicitSelf case, mirroring the
canonical read-path receiver ladder. This is the QBE analogue of the native
EmitLocalRecordBase fix.

Regression: TE2EMiscTests.TestRun_AddrOfClassFieldDynArrayElem_LoadsInstance
writes through @B.Data[idx] via memcpy and reads it back; AssertRunsOnAll runs
it on BOTH backends.

Both fixpoints + the internal-assembler conformance guard + the full suite
(3221 tests) pass.
2026-06-16 02:06:25 +01:00
Graeme Geldenhuys 0b9b66f848 feat(cli): per-backend option contract (Chain of Responsibility)
Adds the driver option contract from docs/backend-options-design.adoc
(Steps 2-5), so a backend owns parsing, describing, and validating its own
private flags and the common parser stays backend-agnostic.

Four virtual methods on TBackendDriver, with inert base defaults:
* AcceptOption  — the parser offers an unrecognised flag to the active
  driver (Chain of Responsibility); typed accept/reject result.
* DescribeOptions — the driver contributes its --help lines (via the new
  shared FormatFlagLine helper, so column layout is owned in one place).
* ValidateOptions — post-parse, full-context validity rules (Template
  Method seam).
* ClaimsEmitIR — retires PickTopDriver's hard-coded bkQBE for --emit-ir.

Parser integration is one-pass with a deferred drain: unknown flags are
collected during the loop and offered to the driver only after --backend
resolves it, so argument order never matters and the value-skip logic is
not duplicated across passes.

First real adopter on master: the native driver takes ownership of
--assembler internal|external. The inline --assembler arm is removed from
ParseArgs; the flag now flows through the drain into Native.AcceptOption.

Eng-review addenda folded in:
* ValidateOptions runs UNCONDITIONALLY, above the stdout-mode toolchain
  skip (so flag-combination rules fire even with --emit-ir).
* A backend-private flag passed to the wrong backend is now a clear hard
  error (intentional behaviour change: --backend qbe --assembler internal
  was previously silently accepted-and-ignored).
* Pending entries carry {flag, lookahead} with consumed-index bookkeeping
  so a flag's value token is not re-reported as unknown.

Tests: cp.test.driver.pas (TBackendDriverContractTests, 11 unit tests on
the real registered singletons) and cp.test.cli.pas (CLI E2E: internal
accepted, bogus rejected, wrong-backend rejected, bad value still rejected
under --emit-ir, --assembler listed in --help).

Both fixpoints, the internal-assembler conformance guard
(NATIVE_INTERNAL_OK), and the full suite (3220 tests) pass.
2026-06-16 00:57:24 +01:00
Graeme Geldenhuys eee7f54284 fix(native): load sret Result pointer with movq, not leaq, when reading its fields
The native backend's field-access receiver ladder, when the record base is
a NAMED LOCAL record, emitted 'leaq <slot>' to get the record's address.
That is correct for an ordinary stack value record, but WRONG for the
Result of an sret (record-returning) function: there the frame slot holds
the caller's buffer POINTER, not the record itself, so the address must be
loaded with 'movq'.  Reading Result.<field> on the RHS (e.g.
'Result.Imm := Result.Disp' in the internal assembler's ParseOperand) thus
dereferenced the wrong location and produced garbage.

The receiver ladder was duplicated across 12 local-leaf sites, each missing
the sret-Result case (the implicit-Result analogue of the implicit-Self
field-access symmetry rule).  This centralises the local-record base
computation into one EmitLocalRecordBase(AName, AReg) helper that makes the
leaq-vs-movq decision once, and routes all 12 leaf sites through it.

The bug was latent: the wrong read happened to land on a harmless stack slot
under the prior register/stack allocation, so existing code worked by
accident.  Any change that shifts global layout (e.g. growing a vtable by
one slot) re-allocated registers and exposed it as corrupted output from the
--assembler internal path.  Neither fixpoint exercises the internal
assembler, so it surfaced only via TInternalAsmE2ETests once a layout change
armed it.

Both fixpoints (FIXPOINT_OK, NATIVE_FIXPOINT_OK) and the full suite
(3204 tests, incl. TInternalAsmE2ETests) pass.
2026-06-16 00:14:26 +01:00
Graeme Geldenhuys 88ebf75476 refactor(cli): collapse ParseArgs out-params into opts objects
ParseArgs changes from fifteen 'out' parameters to two caller-constructed,
populated objects:

* TFrontEndOpts (new unit blaise.frontend.opts) carries front-end-only
  state no backend driver reads: SourceFile, OutputFile, SearchPaths,
  EmitIfaceDir, Incremental, UnitCacheDir, DumpAST, SkipDepCodegen, plus
  the output-mode policy flags EmitIR/EmitAsm and the Backend selection
  input. It lives in its own unit (not blaise.codegen.driver, the
  backend-facing abstraction) so front-end state stays out of the
  backend opts bag.

* TBackendOpts (existing) carries only the cross-cutting knobs a driver
  reads: Target, OPDFEnabled, DebugMode, UseInternalAsm (and OPDFAsmFile,
  bound later during OPDF emit). EmitAsm is removed from TBackendOpts —
  no driver method read it; it is a front-end selection flag.

The main body seeds its working locals from the two objects right after
the call, so the large downstream body reads them unchanged. The parser
now populates one pair of objects instead of fifteen by-reference returns.

Behaviour-preserving. Step 1 of docs/backend-options-design.adoc.

Both fixpoints (FIXPOINT_OK, NATIVE_FIXPOINT_OK) and the full suite
(3204 tests) pass.
2026-06-15 20:46:56 +01:00
Graeme Geldenhuys e642c7c8d7 refactor(cli): remove FPC-style CLI emulation
PasBuild v1.9.0 ships the native TBlaiseBackend (driving Blaise via
--source/--output and probing the version via --help), which was the
agreed trigger to retire the FPC-style argument path.

Deletes ParseFPCArgs, IsFPCStyleInvocation, HandleFPCInfoQuery, the
main-body 'if IsFPCStyleInvocation()' branch, and the now-dead
uStrCompat import (~120 lines, all confined to Blaise.pas). This
leaves a single argument parser (ParseArgs) for the backend-options
refactor to build on, and eliminates the hand-maintained option-default
block in the FPC branch that was a recurring source of
uninitialised-option bugs.

Adds cp.test.cli.pas (TCLIContractTests) with a CLI smoke test:
--help works, a normal --source/--output compile works, and an -iV
probe no longer returns FPC's 3.2.2 (now a rejected unknown flag).

Step 0 of docs/backend-options-design.adoc.

Both fixpoints (FIXPOINT_OK, NATIVE_FIXPOINT_OK) and the full suite
(3204 tests) pass.
2026-06-15 20:25:05 +01:00
Graeme Geldenhuys 9362114ee1 feat(sets): range syntax in set literals — [lo..hi] (#105)
A set literal element may now be a constant range:

    e := [Red..Blue];        { {Red, Green, Blue} }
    e := [m0, m2..m4, m7];   { ranges mix with single members }

The parser builds a transient TSetRangeExpr for each lo..hi element; the
semantic pass (AnalyseSetLiteralExpr) expands it into the individual member
idents before any other consumer runs, so overload resolution, the bitmask
folder, the jumbo-set path, and both code generators see an ordinary member
list and need no range-specific logic.

Both bounds must be compile-time constants of the set's base type. A
reversed constant range [Blue..Red] is a compile-time error rather than a
silently empty set — matching Blaise's preference for rejecting
confusing-but-legal constructs over FPC's empty-set-with-warning behaviour.
Variable bounds [lo..hi] are rejected ("set range bound must be a
constant"); runtime-variable ranges are deferred.

TSetRangeExpr is wired into the .bif serialiser and the AST cloner so a
generic template body containing an unexpanded range round-trips through
separate compilation (verified by hand: a generic returning [m1..m3]
compiled to .o, then instantiated from the .bif with source hidden).

Set base types remain enumerations; the issue's set-of-byte example needs
ordinal-base set types, tracked separately in docs/future-improvements.adoc.
Range expansion is base-type-agnostic, so ranges will work for those
automatically once they land.

Docs: grammar.ebnf SetElement rule; language-rationale.adoc decision +
alternatives. bif-coverage.status regenerated for the new node.

FIXPOINT_OK + NATIVE_FIXPOINT_OK; full suite OK (3201 tests, 11 new).
2026-06-15 17:14:05 +01:00
Graeme Geldenhuys 4e90165b73 feat(semantic): reject variables that shadow a type name (#102)
A variable declaration sharing its identifier with any visible type name
compiled without complaint — e.g. an Iface interface and an iface variable
in the same program (Pascal is case-insensitive, so these are one
identifier). The variable silently shadows the type, which is confusing
and almost always a mistake.

Blaise already rejected var-vs-var, var-vs-const and type-vs-type clashes;
var-vs-type slipped through because type decls are registered in the
block's enclosing scope (so they outlive the block scope) while var decls
are registered one scope deeper, hiding the type from FTable.Define's
duplicate check. AnalyseVarDecls now looks up the name in the type
namespace (FTable.FindType, which honours uses-chain visibility) and
rejects any match.

The rule is stricter than FPC mode objfpc, which permits shadowing a
built-in or outer-scope type (var Integer: Int64 compiles in FPC). Blaise
rejects the whole class — same-block, outer-scope, imported, or built-in —
to eliminate the confusion rather than carry FPC's footgun. Recorded in
docs/language-rationale.adoc with the alternatives considered.

FIXPOINT_OK + NATIVE_FIXPOINT_OK; full suite OK (3190 tests, 5 new).
2026-06-15 16:38:19 +01:00
Graeme Geldenhuys bf63d7419f fix(codegen): three bugs compiling units with generics + float code (#107)
Compiling the issue's waveCalcs unit surfaced three independent codegen
bugs, two of which the reporter saw directly and a third uncovered while
verifying the fix on the native backend.

1. QBE: a generic FUNCTION declared in a unit but never instantiated was
   code-generated as a template — its T-typed locals have no resolved
   type, so codegen raised "Variable 'MaxValue' has no resolved type —
   semantic pass required". EmitStandaloneDefs skipped templates, but the
   unit-emission path (GenerateUnit/AppendUnit -> EmitFuncDef) did not.
   Guard centrally in EmitFuncDef: skip decls with TypeParams <> nil; only
   concrete instances (from GenericFuncInstances) are emitted.

2. native: a float comparison nested inside a short-circuit and/or — e.g.
   (P > 1.0) or (P < -1.0) — reaches EmitExprToEax rather than
   EmitCondBranch. The integer comparison path there emits its operands via
   EmitExprToEax, which has no TFloatLiteral handler, so it raised
   "unsupported expression form TFloatLiteral". Add a float-comparison
   branch to EmitExprToEax that materialises the 0/1 result via ucomisd +
   setcc, mirroring EmitCondBranch.

3. native: a local float const (const TwoPi = 6.28; inside a function) was
   lowered as a load from a symbol named after the const, which is never
   emitted — link error "undefined reference to TwoPi". The const has no
   storage; inline its value from ConstString via .rodata in
   EmitExprToXmm0, as a TFloatLiteral already does.

Regression tests:
- TestUninstantiatedGenericFunc_InUnit_Compiles (sepcompile e2e)
- TestRun_Native_FloatCompareInOrAnd (runs on both backends)
- TestRun_Native_LocalFloatConst (runs on both backends)

FIXPOINT_OK + NATIVE_FIXPOINT_OK; full suite OK (3185 tests).
2026-06-15 16:22:29 +01:00
Graeme Geldenhuys 267e8db702 fix(rtl): WriteDecimal mishandles Low(Int64), printing only '-'
WriteDecimal negated N before extracting digits. For Low(Int64) =
-9223372036854775808 the negation overflows (no positive counterpart in
two's complement) and leaves the value negative, so the 'while AbsN > 0'
loop ran zero times and only the sign was emitted.

Extract digits while keeping the value negative — the negative range
reaches Low(Int64) — and negate each remainder. Also fixes
Low(Integer), which routes through the same function.

Adds e2e regression TestRun_Int64_MinValue.
2026-06-15 16:05:37 +01:00
Graeme Geldenhuys fbb5a675aa feat(format): float Format() support on the native x86-64 backend
Mirror the QBE backend's tag-2 float handling in EmitFormatCall: a float
argument stores tag 2, evaluates to %xmm0 (widening a Single to Double), and
bit-copies the 64-bit pattern into the value slot via movq — the native
equivalent of QBE's 'cast'.

E2E tests (cp.test.e2e.stringops) exercise %f default/precision/width (both
justifications), %e, %g, and a float interleaved with int+string args, all via
AssertRunsOnAll so they run on both the QBE and native backends.
2026-06-15 00:58:43 +01:00
Graeme Geldenhuys 7c26598bb8 feat(format): float support in Format() — %f/%e/%g with width/precision (QBE)
Format() previously handled only %d, %s and %%; any float directive rendered
verbatim, and on the QBE backend a float argument failed to compile because the
double value was stored with 'storel <d-temp>', which QBE rejects.

RTL (blaise_float.pas): add _FormatFloatSpec(V, Spec, Prec) — a precision-aware
renderer for %f/%F, %e/%E and %g/%G built on the existing Grisu1 digit
generator, with round-half-up at the precision boundary (including all-nines
carry) and printf-style exponent formatting.

RTL (blaise_str.pas): rewrite _StringFormatN to parse the full
%[-][width][.prec]<conv> specifier syntax, render each argument (int/string/
float) into a growable buffer, and apply field-width space padding. A new
arg tag (2) carries the IEEE-754 binary64 bit pattern for float values.

QBE codegen: float Format arguments now emit tag 2 and reinterpret the double
bits to an integer via 'cast' before storel, fixing the compile error.

Tests: IR unit tests assert tag-2 emission and the bit cast. E2E coverage and
the native backend follow in the next commit.
2026-06-15 00:58:37 +01:00
Graeme Geldenhuys d3dbebbe85 test(sets): extract set-of e2e tests to cp.test.e2e.sets, run on both backends
The general 'set of' operation e2e tests (Include/Exclude, in, union/intersect,
valued constant, literal argument, equality, for-in, and the 33..64-member
Set64 boundary cases) lived in cp.test.e2e.misc and ran on QBE only via
CompileAndRun. They test named-set operator semantics, distinct from the
inline-set syntax covered by cp.test.e2e.inlineset, so they move to a new
purpose-named unit cp.test.e2e.sets (TE2ESetOpsTests) paralleling the IR-level
cp.test.sets — and are promoted to AssertRunsOnAll so each now runs on both the
QBE and native backends, closing a native-coverage gap.

Pure relocation + both-backend promotion: full suite unchanged at 3172 tests
(11 set tests moved misc -> sets). README test count updated 2768 -> 3172.
2026-06-15 00:34:39 +01:00
Graeme Geldenhuys fa853e0eb2 feat(sets): jumbo sets — set of enum up to 256 members
Extend set of <enum> from a 64-member cap to 256, Java-EnumSet style. Sets
of 64 members or fewer keep the existing single-register bitmask (QBE w/l);
sets of 65..256 members ('jumbo') become an inline byte-array bitmap of
ceil(N/8) bytes, treated as a value aggregate (passed by reference, returned
via sret, memset/memcpy), with operations performed by new RTL helpers.

Representation (uSymbolTable): TSetTypeDesc.IsJumbo (BitCount > 64) and
RawByteSize; RawSize/ByteSize/AllocAlign sized accordingly. TSymbol gains
ConstSetBytes/ConstSetQbe for jumbo constants (can't fit an Int64 mask).

RTL: new runtime/src/main/pascal/blaise_set.pas — _SetIn/_SetInclude/
_SetExclude/_SetUnion/_SetInter/_SetDiff/_SetEqual/_SetCopy over byte-array
bitmaps (overlap-safe). Wired into runtime/Makefile.

Semantic: the four >64 caps become >256. AnalyseSetConstDecl folds a jumbo
const to a byte array. The anonymous set for 'X in [a,b,c]' is sized to the
largest listed ordinal (when constant), not the full enum — keeping the
common low-ordinal membership test (incl. the compiler's own TokenKind
tests) on the fast register path. This also fixes a latent miscompile: the
old fixed-l representation silently dropped any listed ordinal >= 64.

Codegen (both backends): jumbo branches for literal, in, +/-/*, =/<>,
Include/Exclude, for-in, assignment, params (pmJumboSetValue ABI), and sret
returns. The register 'in' gained a range guard for literal-sized sets.
Native reserves two 32-byte scratch slots per frame (and .bss in main) for
set-op/literal result buffers. QBE adds IsAggregateAddrType so jumbo sets
ride the record/static-array address paths and are never promoted.

OPDF: no format change — recSet SizeInBytes (1 byte) already covers <=32.

Verified: FIXPOINT_OK and NATIVE_FIXPOINT_OK; full suite (3172 tests) green
built by the stage-2 binary, the QBE fixpoint binary, and the native
fixpoint binary; bif-coverage OK. Tests: cp.test.jumboset (12 IR) and
cp.test.e2e.jumboset (6 e2e, both backends via AssertRunsOnAll); the
>256-member rejection test in cp.test.sets updated. Docs: grammar.ebnf and
language-rationale set-type sections updated for the 256 cap and literal
sizing.

Closes the design discussion behind #81.
2026-06-14 23:23:19 +01:00
Graeme Geldenhuys 2708a6209e feat(const): compile-time integer constant expressions (#96)
A const declaration may now take a compile-time integer expression on the
right-hand side, not just a single literal:

  const A = 2 * 3;            // 6
        B = 2 + 3 * 4;        // 14 (precedence)
        C = (2 + 3) * 4;      // 20 (parentheses)
        D = 100 div 7;        // 14
        E = Base * 2 + 1;     // references a prior const

Previously only single literals and a flat, single-precedence bit-op chain
(a or b or c) were accepted; 2 * 3 failed with "Expected ';' but got '*'"
and (2 * 3) with "Expected numeric or string constant".

Parser: when the const RHS starts an integer expression (a leading '(', an
integer literal or leading-minus literal followed by a binary operator, or an
ident followed by a numeric operator), parse it with the existing
full-precedence ParseExpr into a normal expression AST stored on
TConstDecl.IntValueExpr. A bare literal keeps the fast IntVal path; '+' on an
ident is left to the string-concat path for backward compatibility.

Semantic: EvalConstIntExpr recursively folds the AST to an Int64, resolving
named-constant references against the symbol table. Folding in the semantic
pass (after all consts are registered) is what makes forward references work
regardless of declaration order. Wired into both the const-block and the
initialised-variable (var G = expr) fold sites. Operator precedence and
grouping come from the AST shape, superseding the precedence-unaware bit-op
token chain.

Scope: folds to integer. Float constant arithmetic and using a const as a
static-array bound remain separate, pre-existing limitations (noted in the
rationale).

Tests: eight IR/fold cases in cp.test.constants (multiply, parens, precedence,
div/mod, named-ref, unary minus, mixed arith+bitwise); one e2e case on both
backends in cp.test.e2e.misc. Docs: grammar.ebnf gains the ConstIntExpr rules;
language-rationale gains a Constant Expressions section.
2026-06-13 19:14:14 +01:00
Graeme Geldenhuys 9a511a22e2 feat(const): multi-dimensional array constants
Range-indexed array constants now support multiple dimensions in both the
comma form (array[0..1, 0..2] of Integer) and the equivalent nested form
(array[0..1] of array[0..1] of Integer), with nested initialiser groups
((1,2,3),(4,5,6)). Previously the const-declaration parser had its own
single-dimension array-type and value-list parsers, so any multi-dim const
failed at the type (Expected ']' but got ',') or the nested value group.

Parser: ParseConstArrayType walks one-or-more 'array[...] of' headers,
reading comma-separated ranges per header and recursing on a nested 'array';
each dimension's bounds go to CD.ArrayDimLows/ArrayDimHighs. The value
parser is now recursive (ParseConstArrayGroup/ParseConstArrayScalar),
flattening nested groups into ArrayElements in row-major order. Dim-0 bounds
mirror onto the legacy ArrayLowBound/ArrayHighBound so the single-dim path
is unchanged.

Semantic: BuildConstArrayType builds the nested static-array type
innermost-first and validates the flat element count equals the product of
dimension extents. Shared by the program/unit and class-const sites.

Codegen: the QBE emitter already lays a flat row-major blob, so it needs no
change for integer/string elements. The native emitter now drills through
nested static-array types to the innermost scalar to pick the element
directive.

Tests: parse/semantic/IR in cp.test.constants; e2e (comma, nested, 3-D) on
both backends via AssertRunsOnAll in cp.test.e2e.staticarray. Docs:
grammar.ebnf gains ConstArrayType/ConstArrayValue rules; language-rationale
updated (the stale 'enum index only' constraint corrected to document
range and multi-dimensional const arrays).
2026-06-13 18:48:28 +01:00
Graeme Geldenhuys 5fa44b33a5 fix(codegen): float-typed array elements on both backends
Storing a Double or Single into a dynamic- or static-array element was
miscompiled. The QBE backend's element-store selected storel/storew for
any non-integer element, so a double element got 'storel %d-temp' and
QBE rejected it (invalid type for first operand in storel). The native
backend evaluated the RHS through the integer path (EmitExprToEax),
which raised 'unsupported expression form TFloatLiteral', and even past
that would have stored from a GPR rather than %xmm0; the float element
READ had the matching gap (unsupported float expression form
TStringSubscriptExpr).

QBE: add tyDouble->stored / tySingle->stores to both the dyn-array and
static-array element-store cases, and coerce the value temp to the
element width via CoerceArg (a 1.5 literal is a 'd' temp; a Single slot
needs truncd first).

Native: in the element store, evaluate float RHS via EmitExprToXmm0,
width-adjust to the element type, spill through %rax (x86 has no push for
xmm), then reload and store with movsd/movss. Add float element-read
cases to EmitExprToXmm0 mirroring the integer subscript address maths.

Tests: three e2e cases in cp.test.e2e.staticarray (dyn double, dyn
single, static double) run on both backends via AssertRunsOnAll.
2026-06-13 18:07:42 +01:00
Graeme Geldenhuys 12ebcbe09f feat(parser): clear diagnostics for packed/bitpacked array
FPC's bitpacked array (bit-level packing) and packed array have no
Blaise equivalent. Previously bitpacked fell through to tkIdent and
produced a confusing "Expected ';'" error, while packed array gave a
generic "may only precede 'record'" message.

Catch both in the type-declaration RHS and emit an actionable error
that points users at the supported idiom (set of for packed boolean
flags). packed record is unaffected.

Adds five tests to cp.test.packedrecord: parse-error coverage for both
keywords plus message-content assertions that the error names the
keyword and suggests set of.
2026-06-13 17:56:22 +01:00
Graeme Geldenhuys f830a9f1ac feat(params): array of const (heterogeneous variadic parameters)
A parameter declared 'array of const' accepts a single call-site bracket list
of mixed-type values, boxed into an array of the intrinsic record TVarRec and
passed via the existing open-array ABI:

  procedure Log(args: array of const);
  ...
  Log([42, 'hi', 3.5, True]);

This is the one loosely-typed-passing mechanism Blaise adopts (see the
rationale section); untyped params, varargs, and Variant remain omitted.

- RTL/builtins: TVarRec is registered as a compiler-intrinsic record
  { VType: Byte; VValue: Pointer } (16-byte layout, mirroring TMethod), with
  vt* discriminant constants - all available with no uses clause, matching
  Delphi's auto-available System.TVarRec.  Blaise has no record variant parts,
  so the callee reads each element by reinterpret-casting the single VValue
  slot (Integer(v.VValue), string(PChar(v.VValue)), PDouble(v.VValue)^, ...).
- Parser: 'array of const' parses as an open array whose element is TVarRec.
- Semantic: a heterogeneous bracket literal is typed 'array of TVarRec' rather
  than rejected; overload resolution binds it (and homogeneous / empty
  literals) to an array-of-const formal; retyping runs for proc, func, and
  method calls.
- Codegen (both backends): EmitConstArrayLiteral builds one 16-byte TVarRec
  per element, tagging by inferred type.  Borrow semantics (FPC) - strings and
  objects are stored without AddRef.  Doubles are heap-boxed via _BlaiseGetMem
  (vtExtended holds a PDouble) since a double does not fit the pointer slot.
- Native: also fixes a pre-existing gap - reading a float through a pointer
  deref (PDouble^) in EmitExprToXmm0 - needed for vtExtended read-back.

Tests: cp.test.arrayofconst (parser/semantic/IR) and cp.test.e2e.arrayofconst
(compile + run on both backends, including value read-back, empty/homogeneous
lists, and string-variable borrow).  Grammar and language rationale documented.
2026-06-13 17:37:59 +01:00
Graeme Geldenhuys 54d7d2a573 feat(types): inline set types in any type position
A set type may now be written inline anywhere a type is expected (var,
parameter, record field, ...), not only in a named type declaration.  The
element type may be a named enumeration or an anonymous enumeration written
in place:

  var
    Days:  set of TWeekday;       // named enum element
    Flags: set of (fA, fB, fC);   // anonymous enum element

Implementation resolves an inline set entirely from its canonical type-name
string, with no parser-side state:
- Parser: ParseTypeName gains a 'set of' branch.  A named element yields
  'set of <Name>'; an anonymous element is encoded verbatim as
  'set of (a,b,c)' (ParseAnonEnumName), self-contained in the type string.
- Semantic: FindTypeOrInstantiate recognises the 'set of ' prefix and builds
  the TSetTypeDesc on demand (as it already does for 'array of'/'^'/'class
  of').  For the anonymous form, SynthAnonEnum synthesises an enum type from
  the encoded member list - registering each member as an enum constant - and
  reuses an identical inline enum if its members are already defined.  Set
  types compare structurally, so the synthetic enum name need only be unique.

Encoding the enum in the type string rather than carrying parser state makes
the feature work uniformly in every type position with no declaration-ordering
or ownership concerns.

Tests: cp.test.inlineset (parser/semantic) and cp.test.e2e.inlineset (compile
+ run on both backends).  Grammar and language rationale documented.
2026-06-13 13:45:13 +01:00
Graeme Geldenhuys ba51a53365 feat(vars): initialised global variables (var G: T = value)
A global variable declaration may now carry an initialiser whose value is
folded at compile time and emitted into the data section, so the variable
holds its initial value before the program body runs (matches FPC/Delphi):

  var
    G: Integer = 42;
    S: string = 'hello';
    A: array[0..2] of Integer = (10, 20, 30);

Implementation reuses the typed-constant pipeline end to end:
- Parser: the const value scanner is factored into ParseConstValue, shared by
  const declarations and the new var-initialiser path.  TVarDecl carries an
  owned InitConst: TConstDecl (TConstDecl moved ahead of TVarDecl so no
  forward declaration is needed).
- Semantic: AnalyseVarInitializer folds the value and type-checks it against
  the declared type; array initialisers derive element type and bounds from
  the resolved static-array type and mint a data label.
- Codegen: EmitGlobalVarInit (QBE) and EmitGlobalInitData (native) emit the
  folded value as a typed data slot - a single field for scalars/strings (a
  string global points at an immortal static header __sN + 12) and an inline
  element list for arrays.  Covers program- and unit-level globals on both
  backends.

Restrictions, each rejected with a clear diagnostic: global scope only (no
local initialisers), a single name per initialised declaration, and arrays
only for aggregates (record and inline-set initialisers are deferred - no
record-constant machinery yet, and a set initialiser would clash with the
const symbol the set folder defines).

Tests: cp.test.varinit (IR + parser/semantic) and cp.test.e2e.varinit
(compile+run on both backends).  Grammar and language rationale documented.
2026-06-13 13:29:43 +01:00
Graeme Geldenhuys 42f48c9ece fix(codegen): Inc/Dec on a captured outer-scope variable
A nested procedure that did Inc(Counter)/Dec(Counter) on a captured outer
local failed to compile: the captured value lives behind the '_cap_<Name>'
pointer slot (which holds the var's address), but Inc/Dec fell through to the
plain local/global path and referenced a non-existent '%_var_Counter' /
'Counter' symbol. QBE rejected it ("invalid type for first operand in
loadsw"); native linking failed ("undefined reference to Counter").

Assignment to a captured var already used the _cap_ indirection; Inc/Dec did
not. Add a captured-var branch to both backends that loads/modifies/stores
through the _cap_ pointer (QBE: loadw/storew via %_cap_Name; native: move the
_cap_ pointer into %rdx and use the existing addr-based inc/dec path). Covers
the w and l element types and the optional step argument.

Adds e2e TestRun_IncDec_CapturedVar. Full suite 3088, both fixpoints OK.

Discovered while migrating the OPDF debugger's nested-scope test
(test_12_teaser_demo) to the Blaise suite.
2026-06-13 12:29:05 +01:00
Graeme Geldenhuys 7567356e84 fix(opdf): emit Boolean and real constants correctly
Constant debug records mishandled two kinds, surfaced while debugging const
values in pdr:

- Boolean (const Enabled = True): True/False are plain identifiers with no
  dedicated token, so the const parser fell into the generic ident-as-string
  path and emitted an empty STRING constant. Recognise True/False as a Boolean
  ordinal (value 1/0) and tag it with TypeName 'Boolean' so the debug emitter
  points the constant at the Boolean primitive's TypeID — pdr then renders
  'True'/'False' rather than a raw 1/0.

- Real (const PiApprox = 3.14159): IsFloat constants fell into the ordinal
  branch and emitted IntVal (always 0). Emit them as ckReal (kind 2, already
  supported by the debugger) with the IEEE-754 Double via GAS .double.

EmitConstants now branches IsFloat -> ckReal, IsString -> ckString, else
ckOrd (with a typed TypeID when TypeName is set). Adds TestOPDF_Constant_-
BooleanTyped and TestOPDF_Constant_Real. Full suite green (3085), both
fixpoints OK.
2026-06-13 12:11:14 +01:00
Graeme Geldenhuys ccd109ce77 feat(arrays): support multi-dimensional static arrays
Add multi-dimensional static array syntax, both the comma form
(array[0..1, 0..2] of Integer; A[i, j]) and the equivalent nested/chained
form (array[0..1] of array[0..2] of Integer; A[i][j]).  The comma forms are
syntactic sugar: the parser desugars array[a, b] of T into the nested
array[a] of array[b] of T, and A[i, j] into chained subscripts A[i][j], so
the two notations are fully interchangeable in every position.

Layers:
- uParser: comma loops in ParseTypeName and subscript reads; the statement
  LHS now lowers A[i, j] := v and A[i][j] := v to a TStaticSubscriptAssign
  carrying a new BaseExpr (the inner-array address expression).  The previous
  "chained base not yet supported" rejection is removed.
- uAST: TStaticSubscriptAssign.BaseExpr (owned); wired into CloneStmt and the
  .bif encoder/decoder (uUnitInterfaceIO); bif-coverage status updated.
- uSemantic: BaseExpr branch resolves the inner static-array type and checks
  the index and value element type.
- Codegen (both backends): a nested static-array element now evaluates to its
  inline address (mirroring record/interface elements) so a further subscript
  indexes into it; the static-subscript store reads its base from BaseExpr
  when set.  Nested arrays are a flat row-major contiguous block.
- OPDF: no new record needed - each dimension emits one recArray whose element
  points at the next inner recArray; pdr follows the chain and renders the
  value as a true multi-dimensional structure.

Tests: IR unit tests (cp.test.staticarray), e2e tests on both backends via
AssertRunsOnAll (cp.test.e2e.staticarray), and a nested-recArray OPDF test
(cp.test.opdf).  Grammar and language rationale documented.
2026-06-13 11:56:38 +01:00
Graeme Geldenhuys 7b8116310f feat(opdf): emit open-array debug info (ArrayKind=2 + companion slot)
Open-array parameters were emitted to the OPDF companion file as dynamic
arrays (IsDynamic=1), so the debugger read their length from the heap
header at data-4. An open array has no header — it is a (data ptr, high
index) pair on the stack — so this read returned garbage (Length=32765).

Model open arrays as a distinct kind:

- TDbgVar gains IsOpenArray + HighRbpOffset; DbgMarkParams records the
  '_high' companion slot offset from the frame map.
- The recArray byte previously named IsDynamic now carries ArrayKind
  (0=static, 1=dynamic, 2=open-array); open arrays emit ArrayKind=2.
- recLocalVar for an open array uses LocationExpr=5 and appends the
  companion '_high' slot offset as a trailing SmallInt.
- Open arrays get a distinct canonical name ('open array of T') so they
  no longer alias a same-element dynamic array's TypeID.
- Parameter and facts-path variable type records are now emitted; the
  anonymous open-array type previously had no recArray written at all.

The debugger side (length read from the companion slot) lives in the
opdebugger repo. FPC's dbgopdf.pas has no open-array concept; this is a
Blaise extension to the OPDF format.
2026-06-13 01:06:16 +01:00
Graeme Geldenhuys 6ba1bc9b78 fix(semantic): reject void/procedure-call results as WriteLn/Write arguments
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.
2026-06-12 18:12:48 +01:00
Graeme Geldenhuys b3024facc0 fix(semantic): reject procedural/record/class types as WriteLn/Write arguments
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.
2026-06-12 17:46:56 +01:00
Graeme Geldenhuys 1b0afaf3d3 fix(test): derive blaise binary path from TestRunner location, not cwd
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.
2026-06-12 16:59:15 +01:00
Graeme Geldenhuys 75834d393d fix(semantic): out-of-line methods now link for generic records
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.
2026-06-12 16:45:33 +01:00
Graeme Geldenhuys 512cbfef98 test(zero-init): use S=[] directly in SetLocal now that semantic crash is fixed
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.
2026-06-12 16:29:57 +01:00
Graeme Geldenhuys 75db9584dc fix(semantic): set equality S=[] and S=[lit] no longer crash
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
2026-06-12 16:21:25 +01:00
Graeme Geldenhuys a2d263e0e8 feat(lang): guarantee zero-initialisation of all variables
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).
2026-06-12 16:09:02 +01:00
Graeme Geldenhuys d182b4ab7e fix(debug): attribute generic-instance allocation sites to the defining unit
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.
2026-06-12 15:16:45 +01:00
Graeme Geldenhuys 1dfc693fe5 fix(codegen): for-in over enumerators — promoted loop var and leaked enumerator
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.
2026-06-12 15:05:43 +01:00
Graeme Geldenhuys eabf20ae7f refactor(driver): driver-aware worker; registry-driven flags and pre-flight
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.
2026-06-12 14:05:38 +01:00
Graeme Geldenhuys 543f495f48 refactor(driver): lowering and linking through the backend drivers
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.
2026-06-12 14:01:48 +01:00
Graeme Geldenhuys 5603767298 refactor(driver): introduce TBackendDriver; route codegen construction
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.
2026-06-12 13:55:01 +01:00
Graeme Geldenhuys 06517bf1d3 fix(qbe): dispatch record/interface-returning virtual methods via vtable
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.
2026-06-12 13:53:25 +01:00
Graeme Geldenhuys 1ba78cf90b fix(native): support class-receiver method calls returning interfaces
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.
2026-06-12 13:44:19 +01:00
Graeme Geldenhuys f0f888f2c5 fix(codegen): interface-dispatch ABI — record args, var/out params, call receivers, discarded returns
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).
2026-06-12 13:27:00 +01:00
Graeme Geldenhuys c20ddd549d fix(codegen): interface nil-compare and interface-method sret returns
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.
2026-06-12 12:50:19 +01:00
Andrew Haines 81c5804ae4 fix(codegen): static-array element read/write of interface uses fat pointer
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.
2026-06-12 12:13:54 +01:00
Graeme Geldenhuys 1babf0c9a5 feat(linker): section merger (completes Phase A)
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.
2026-06-12 10:13:16 +01:00
Graeme Geldenhuys 5415815f8d feat(linker): ELF object and ar-archive reader (Phase A)
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.
2026-06-12 10:10:21 +01:00
Graeme Geldenhuys 62a9b02870 fix(codegen): string-field subscripts are 0-based on both backends
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.
2026-06-12 10:10:09 +01:00
Graeme Geldenhuys ff5c481ec0 test: add assembler/ELF writer tests and internal linker design doc
- 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).
2026-06-12 09:55:44 +01:00
Graeme Geldenhuys 83d40490fc feat: add self-assembler for native x86-64 backend (--assembler internal)
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.
2026-06-12 09:55:34 +01:00
Graeme Geldenhuys da2bb5a9a9 fix(native): var/out argument addresses and record-typed field assignment
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).
2026-06-12 09:55:20 +01:00
Graeme Geldenhuys eeadc177a1 fix: default all option variables in the FPC-style invocation branch
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.
2026-06-12 00:55:16 +01:00
Graeme Geldenhuys 1a5a881d6c feat(semantic): reserve module names as identifiers
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
2026-06-11 23:51:11 +01:00
Graeme Geldenhuys b776ceb5dd fix(codegen): fail loudly on IsArrayAccess fall-through in both backends
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.
2026-06-11 23:06:54 +01:00
Graeme Geldenhuys 35f8ff5970 Revert defaulting the compiler to Native backend.
My local setting accidently got comitted in ac0acc7bdd.
2026-06-11 22:53:26 +01:00
Graeme Geldenhuys 840b053812 fix: var parameters of dynamic-array, static-array, PChar and interface types
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.
2026-06-11 18:50:50 +01:00
Graeme Geldenhuys 4f2246cc57 fix(debug): per-unit source files in OPDF line records
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.
2026-06-11 17:56:23 +01:00
Graeme Geldenhuys ac0acc7bdd feat(native): build blaise-compiler with the native backend by default
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).
2026-06-11 17:32:25 +01:00
Graeme Geldenhuys ca21b884bc feat(debug): link --debug-opdf binaries as PIE
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.
2026-06-11 15:43:02 +01:00
Graeme Geldenhuys 248dccf6ca feat(debug): indirect variable locations + codegen fixes they uncovered
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.
2026-06-11 15:07:06 +01:00
Graeme Geldenhuys 525037a4db feat(opdf): field drilldown — unit-class symbols, Self typing, record-param shadows
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.
2026-06-11 14:12:55 +01:00
Graeme Geldenhuys d2d5a8de3d feat(opdf): native-first debugging — exact frame offsets, statement-level lines
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.
2026-06-11 13:38:20 +01:00
Graeme Geldenhuys bc2bbdef03 fix(opdf): method/destructor scope records; export symbols the companion references
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.
2026-06-11 12:35:45 +01:00
Graeme Geldenhuys 9d35fbef1c fix(arc): class fields consume +1 call results — ends the per-store leak
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.
2026-06-11 12:23:50 +01:00
Graeme Geldenhuys d9da320d6d fix(arc): mark TSymbolTable builtin-type caches [Unretained]
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).
2026-06-11 12:04:33 +01:00
Graeme Geldenhuys b9e0ea7a9e docs+test: collection ownership conventions — generic retention pinned
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.
2026-06-11 11:55:34 +01:00
Graeme Geldenhuys d6b706ffd3 fix(native): nil l-value Free slots; sret record calls into array elements
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.
2026-06-11 11:50:24 +01:00
Graeme Geldenhuys c62ce5b8b1 fix: statement-position method call through a non-Self interface field
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.
2026-06-11 11:35:18 +01:00
Graeme Geldenhuys dbf2e3c879 fix(qbe): open-array args on method and constructor calls dropped the high
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.
2026-06-11 11:30:42 +01:00
Graeme Geldenhuys 409e1cada7 fix(native): function-of-object indirect call must load Self from TMethod
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.
2026-06-11 11:24:32 +01:00
Graeme Geldenhuys f8d75e586d feat: interface properties
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.
2026-06-11 11:04:47 +01:00
Graeme Geldenhuys 1b366f4c97 feat: implicit integer→float widening for math builtins and float typecasts
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.
2026-06-11 10:17:00 +01:00
Graeme Geldenhuys 5f0fbb7643 fix: element access through array-typed fields — r.A[i], c.N.A[i], SetLength(r.A)
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.
2026-06-11 09:19:35 +01:00
Graeme Geldenhuys 7b4db4d11d fix: record elements in arrays — subscript-LHS chains, deep copy, element reads
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.
2026-06-11 08:38:08 +01:00
Graeme Geldenhuys 434058ceda feat(native): caller-side const-string ARC + unified call-arg hoist region
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).
2026-06-11 04:04:39 +01:00
Graeme Geldenhuys 17c14b37aa perf(qbe): borrowed-local const-string args + lookup/format tail wins
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).
2026-06-10 23:53:01 +01:00
Graeme Geldenhuys cfbf2b305b fix(semantic): materialise default args in method-call statements
AnalyseMethodCall (TMethodCallStmt) never called AppendDefaultArgs, so a
method-call statement that omitted defaulted parameters was emitted with
fewer arguments than the callee's parameter list — the callee then read
whatever the missing slots happened to contain.  For parameters past the
six SysV integer registers that meant caller-frame stack junk: the
native backend's own EmitInterfaceCall (defaulted 7th param AObjExpr)
read garbage instead of nil, and only survived because the old register
allocation upstream happened to leave benign values there.  Expression
calls, proc-call statements and implicit-Self calls already appended
defaults; the three resolution sites in AnalyseMethodCall now do too
(AppendDefaultArgs is idempotent).

Pinned by cp.test.defaultargs.pas: explicit-receiver statement,
seventh-slot default, implicit-Self statement, and proc statement.
2026-06-10 23:52:45 +01:00
Graeme Geldenhuys 91f44ec823 perf(qbe): shape-aware const-string argument ARC
Const-string args were pinned (AddRef before the call, Release after)
regardless of shape.  ConstArgMode now classifies them: string literals
and named consts are immortal and emit no ARC ops at all; +1 owned
temps (function/method/getter returns) emit a single post-call Release
that consumes the transferred reference — previously these leaked one
buffer per call (the pin pair netted to zero and EmitOwnedArgReleases
deliberately skipped const-string params); every other shape keeps the
pin.  The hot TStringList lookup signatures (Compare, KeyEquals,
HashOf, Find, FindSorted, IndexOf) become const.

Borrowed elision for plain local variables — the larger win — is
deliberately NOT enabled: it is sound at the ARC level, but removing
the balanced pin pairs changes caller register allocation and exposes a
latent use-before-def of a callee-saved register in the native
backend's EmitInterfaceCall (a second-generation compiler then reads
the caller's leftover %r13 as an AST pointer and crashes).  The full
repro and bisect trail are recorded in the bug log; the camPin branch
carries a pointer to it.

Verified through two self-hosted generations plus both fixpoints;
contract pinned by 7 IR tests in cp.test.constarg.pas.
2026-06-10 23:18:24 +01:00
Graeme Geldenhuys 3799f1516a perf(stdlib): hash lookup for TDictionary, TOrderedDictionary and TSet
The generic keyed containers used documented linear key scans.  They now
build the same lazy open-addressing index as TStringList: threshold 16,
kept at <= 50% load, maintained on Add, invalidated by Remove/Clear/
Destroy (storage stays insertion-ordered, so TOrderedDictionary indexed
access is unchanged).

Hashing an arbitrary key type works through monomorphisation: the
generic bodies call GCHashOf(Key), which resolves per instantiation
against new overloads (string hashes its bytes FNV-1a, matching the
content semantics of '=' on strings; Integer/Int64/Pointer use a
multiplicative mix).  Instantiating a keyed container with a type that
has no GCHashOf overload is a compile-time error; the threshold const
lives in the interface because generic bodies are analysed at their
instantiation site.

Neutral on the compiler self-compile (its dictionaries are small); this
is stdlib quality for user programs.  Contract pinned by 7 new
in-process tests (cp.test.maphash.pas) written against the linear
implementation first.
2026-06-10 19:41:38 +01:00
Graeme Geldenhuys 475d579968 perf(semantic): overload groups replace per-call index scans
Every standalone call resolved overloads by scanning the whole
FProcIndex with SameText (twice: IndexOf + ResolveStandaloneOverload),
and every method call scanned FMethodIndex once per inheritance hop
(2.35% of the compile after the TStringList hash landed).  FProcGroups/
FMethodGroups now map each key to a TObjectList holding every decl
registered under it, so resolution, the registration-time duplicate
check and impl-signature matching fetch all candidates with one hashed
IndexOf.  FProcIndex.Objects[] rewrites go through
ReplaceProcIndexObject so the groups see iface->impl swaps.

The groups are additionally held in an owning TObjectList keep-alive:
TStringList.Objects stores raw, non-retained pointers, so a group whose
only strong reference was a local would be released at the creating
routine's exit and its memory recycled (manifested as a crash resolving
Exception.Create during self-compile).  TObjectList itself retains on
Add and releases on Put/Clear/Destroy.  The non-retaining behaviour of
TStringList.Objects and generic TList<T> class elements is recorded in
the bug log.

Compiler self-compile: 0.91 s -> 0.80 s, 10.5G -> 9.2G instructions.
2026-06-10 19:26:45 +01:00
Graeme Geldenhuys 4c74506e0f perf(stdlib): hash index for unsorted TStringList.Find/IndexOf
Profiling the compiler self-compile (--emit-ir, callgrind) showed 86% of
all instructions inside linear scans of five unsorted TStringLists
(FUnitSymbols 68%, FMethodIndex 12%, FStrLits 4%, FUnitIfaces 2%);
TStringList.Find averaged 145 Compare calls per lookup, and each
iteration paid six ARC ops through Compare's by-value string params.

Unsorted lists with >= 16 entries now build a lazy open-addressing hash
index on first Find: FNV-1a over the bytes, case-folded when the list is
case-insensitive, kept at <= 50% load so an empty slot terminates every
probe.  Duplicate keys keep the first-added index, preserving the
IndexOf contract that overload registration relies on.  Appends update
the table in place; Delete/Insert/Put/Clear/CustomSort and the
Sorted/CaseSensitive setters invalidate it for lazy rebuild.  Sorted
lists keep using binary search; small lists keep the linear scan.

Compiler self-compile: 4.75 s -> 0.91 s wall (5.2x), 71.6G -> 10.5G
instructions (6.8x).  Behaviour contract pinned by 12 new in-process
tests in cp.test.stringlistfind.pas, written against the old linear
implementation first.
2026-06-10 18:54:37 +01:00
Graeme Geldenhuys 4da1233f12 fix(native): metaclass refs, ClassCreate, const-string ARC, locals init, OA literals
Five native-backend fixes found while bringing up the natively-compiled
TestRunner:

- Bare class-name expressions (IsMetaclassRef) now emit the typeinfo
  address (leaq typeinfo_<T>(%rip)), matching the QBE backend's
  'copy $typeinfo_<T>'.  Previously they fell through to a global
  variable load of an undefined symbol, breaking RegisterTest(TFoo).
  Also: tyMetaClass and tySet accepted in the function param/return
  type guards, and ForceDirectories added to the TProcCall handler.

- ClassCreate: the constructor symbol now goes through
  MethodEmitNameNative (unit-prefixed mangling) instead of raw
  OwnerTypeName_Create, and the _ClassCreate result is kept in %rbx
  across the constructor call — constructors are plain procedures and
  do not return Self in %rax.

- Class-name string blobs (__cn_*) now carry the bare class name as
  content while keeping the unit-prefixed symbol name.  Previously the
  prefixed name was embedded, so ClassName/TestClassName returned
  'unit_TClass' and the runner's --suite filters never matched.

- Const string params now get the callee-side entry-retain/exit-release
  pair.  _StringConcat returns an rc=0 transient; QBE pins it caller
  side (EnsureConstStringRef), the native backend had no protection, so
  any retaining callee further down the chain freed the transient while
  still in use — heap corruption that crashed the semantic pass on
  'class of' expressions.

- Static-array locals are memset to zero on entry, mirroring QBE's
  EmitVarAllocs.  Element assignment into arrays of managed types
  releases the old element; without the memset that release read stack
  garbage (TProcess slots in RunFilteredTests).

- Open-array literal arguments are hoisted before the argument pushes
  (HoistOALitArgs + BeginCallArgs/PushCallArg/EndCallArgs).  The old
  in-loop EmitOpenArrayLiteral subq'd its element block in the middle
  of the push sequence, so the popq loop that loads the argument
  registers popped from inside the block — Outer('gcc', ['-o', B, A])
  received AExe='-o'.  FMethodArgExtraStack is gone (its only writer
  was the in-loop literal emission, and it was reset by nested call
  sites mid-argument-evaluation).

Three new e2e tests cover metaclass values on both backends (including
the multi-unit RegisterTest pattern) and an open-array literal that is
not the first argument.
2026-06-10 18:01:57 +01:00
Graeme Geldenhuys 4e220def35 fix(native): support MethodAddress with variable string argument
The native backend rejected MethodAddress calls where the second
argument was not a string literal.  Evaluate the expression normally
and pass the result (already a data pointer) to _MethodAddress, matching
the QBE backend.

Also convert remaining old-style single-quote concatenation source
constants in e2e tests to triple-quote text blocks for readability.
2026-06-10 15:43:41 +01:00
Graeme Geldenhuys 5f2731eb08 fix(native): save array base on stack across EmitExprToEax calls
EmitExprToEax uses %rcx as a scratch register for binary expressions,
but the open-array, dynamic-array, static-array, and PChar element
access paths all saved the base address in %rcx before calling
EmitExprToEax for the index expression.  A computed index like [I + 1]
would clobber %rcx, causing the element address computation to
dereference a garbage pointer.

Use pushq/popq to protect the base address across the EmitExprToEax
call, matching the pattern already used by the string-subscript path.
Same fix applied to the @Array[I] address-of paths.

This was the last blocker for the native fixpoint — the native-compiled
compiler can now fully recompile itself with --backend native and
produce identical assembly at stage-2 and stage-3.
2026-06-10 13:56:06 +01:00
Graeme Geldenhuys a6cb6b9b8d fix(native): route WriteLn(StdErr, ...) to fd 2 instead of stdout
The native backend's EmitWrite hardcoded fd=1 for all _SysWrite* calls,
ignoring the StdErr file argument. WriteLn(StdErr, 'msg') emitted the
integer 2 as a literal followed by the message on stdout instead of
routing to file descriptor 2.

Mirror the QBE backend's StdErr detection: if the first argument is the
StdErr identifier, set fd=2 and skip it in the argument loop.
2026-06-10 13:15:40 +01:00
Graeme Geldenhuys f50f206cfa fix(native): record return/assign through out params and const record params
Three related bugs in the native x86-64 backend where record data was
written to the pointer slot (leaq = address-of-slot) rather than through
the pointer (movq = load-pointer-then-deref):

1. EmitSretCall / EmitMethodSretCall: when the LHS of a record-returning
   function call is a var/out parameter, pass ASretIsIndirect=True so the
   memset and reg-return capture target the caller's buffer, not the
   local stack slot holding the pointer.

2. EmitRecordRegReturnCapture: honour a new AIsIndirect flag — emit
   movq (load pointer) instead of leaq (address-of-slot) for all
   classification shapes (rcInt1..rcSSEInt).

3. EmitExprToEax for TIdentExpr with tyRecord/tyStaticArray: when the
   identifier is a const/value record parameter (ParamMode = pmRecordValue
   or pmStaticArrayValue), the frame slot holds a pointer to the data
   copy, not inline data — emit movq instead of leaq.

Bug 1+2 caused out-param record assignments to overwrite the pointer
with the return value.  Bug 3 caused const-record field assignments
(e.g. FTarget := ATarget in SetTarget) to copy a stack address into the
field instead of the record data.  Both crashed the native-compiled
compiler when using --backend native.

Added E2E test: TestRun_Native_RecordSret_OutParam.
2026-06-10 13:06:10 +01:00
Graeme Geldenhuys 7aca461a16 refactor(test): generalise multi-backend E2E infrastructure for 2+ backends
Replace the hardcoded two-backend AssertRunsOnBoth with a set-driven
design that scales to any number of backends:

  - TBackends = set of TBackend; AllBackends constant
  - AssertRunsOnAll: iterates AllBackends (replaces AssertRunsOnBoth)
  - AssertRunsOn(ABackends: TBackends; ...): run on a specific subset
  - BackendName(TBackend): string for assertion labels

When a new backend (e.g. LLVM) is added to TBackend and AllBackends,
all 232 AssertRunsOnAll call sites automatically exercise it.
2026-06-10 12:31:52 +01:00
Graeme Geldenhuys bb2fe2da75 refactor: rename uCodeGen/uCodeGenQBE to blaise.codegen/blaise.codegen.qbe
Align the QBE backend unit names with the dotted naming convention
already used by the native backend (blaise.codegen.native.*).

  uCodeGen.pas    → blaise.codegen.pas
  uCodeGenQBE.pas → blaise.codegen.qbe.pas

Updated all uses clauses (~70 test files, main compiler units),
documentation references (README + 5 docs/*.adoc files), and
comment references in runtime/stdlib.
2026-06-10 12:12:14 +01:00
Graeme Geldenhuys 8119634b0a refactor(qbe): classify record return once per function definition
EmitMethodDef and EmitFuncDef called ClassifyRecordReturn four times on
the same return type (signature, declaration, prologue, epilogue).
Classify once into a local RC variable and pass it to each helper.

The per-call-site classification in EmitRecordReturnCallSite remains
unchanged — it classifies a different type (the callee's) each time.

FIXPOINT_OK, 2831 tests pass.
2026-06-10 12:02:33 +01:00
Graeme Geldenhuys a6aa7b524f feat(native): ABI-correct record-by-value returns for x86-64
Implement SysV AMD64 ABI register-return classification in the native
x86-64 backend, matching the QBE backend's record-return support.

Small POD records (≤16 bytes, no managed fields) now return in registers
(rax/rdx for INTEGER, xmm0/xmm1 for SSE) instead of always using the
hidden sret pointer.  Classification covers all seven SysV shapes:
rcInt1, rcInt2, rcSSE1, rcSSE2, rcIntSSE, rcSSEInt, and rcWin64Agg.

Changes:
- Move TRecReturnClass enum to uCodeGen.pas (shared by both backends)
- Add classification helpers to TNativeBackend (IsRecordManagedClean,
  IsRecordAllIntegerLeaves, ClassifyRecordReturn, etc.)
- BuildFrame: reg-return records allocate Result as a local buffer
  (not an sret pointer slot)
- EmitRecordReturnEpilogue: load Result buffer into return registers
- EmitRecordRegReturnCapture: store return registers into dest buffer
- EmitSretCall/EmitMethodSretCall: dispatch through reg-return path
  when callee qualifies
- EmitFieldAddrToRcx: new helper for computing record/class field
  addresses (needed by float field reads)
- EmitExprToXmm0: handle TFieldAccessExpr and TIntLiteral
- Fix float field assignment in TFieldAssignment (was using integer
  load/store path for Single/Double fields)
- Fix Single argument passing (add cvtsd2ss when Double→Single
  conversion needed)

Also adds 3 missing QBE E2E tests (nested-record, two-Single, managed-
field sret) and 9 native E2E tests covering all register-return shapes.

FIXPOINT_OK, 2831 tests pass.
2026-06-10 11:49:08 +01:00
Graeme Geldenhuys 593c59432c feat(qbe): ABI-correct record-by-value returns for C-FFI
Classify record returns per SysV x86-64 §3.2.3 instead of using sret
unconditionally.  Small POD records (1-16 bytes, integer and/or float
leaves) now travel in registers (rax/rdx, xmm0/xmm1), matching how C
compilers return small structs.  Records with managed fields (string,
class, interface, dynarray — checked recursively) always stay on sret.
Win64 delegates to QBE's -t amd64_win ABI pass via uniform aggregate
type declarations.

Based on Andrew Haines' qbe_record_byval_return branch, rebased onto
master and adapted.  Added FlattenRecordToAggLetters to correctly
classify nested-record field types in QBE aggregate declarations.

39 IR tests (cp.test.recordret) + 12 E2E tests (cp.test.e2e.recordret).
2819 tests, FIXPOINT_OK.
2026-06-10 11:27:08 +01:00
Graeme Geldenhuys 3404ac13ea chore: adopt -SNAPSHOT version suffix, update README
Align the compiler's internal version strings with PasBuild's
convention: 0.11.0-dev → 0.11.0-SNAPSHOT (Blaise.pas, uCompilerId.pas).
Update rolling-bootstrap.sh to strip -SNAPSHOT instead of -dev.

README updates:
- Document the native x86-64 backend alongside QBE
- Update Phase 7 from "LLVM" to native backend parity
- Fix runtime layout (main/asm/, not main/c/)
- Add --backend native usage example
- Update test count to 2768
2026-06-10 10:39:14 +01:00
Graeme Geldenhuys a9cb4961ea feat(debug): extend leak tracking to strings and dynamic arrays
Track string and dynamic-array buffer leaks in --debug builds.
Strings are registered in _StringAddRef (on the 0→1 transition)
and removed in _StringRelease; likewise for _DynArrayAddRef/Release.
The leak report prints "string" or "dynarray" as the type tag
instead of a class name.

Sentinel-based type tags (GLTTagString=Pointer(2), GLTTagDynArray=
Pointer(3)) distinguish buffer entries from class entries in the
hash map.  The report reads the correct refcount header offset
for each type (12-byte string header, 8-byte dynarray header,
CLASS_HDR for classes).

LT_BUCKETS grows from 1024 to 4096 to accommodate the higher
entry count when strings/dynarrays are tracked.  Report wording
changes from "object(s) not released" to "leak(s) not released".

2768 tests pass (2 new e2e tests), FIXPOINT_OK.
2026-06-10 10:23:05 +01:00
Graeme Geldenhuys c1c9a0851e feat(debug): add allocation-site info (unit:line) to leak tracker
Extend _LeakTrackerRegister from 2 to 4 args (UserPtr, ClassName,
UnitName, Line) so the --debug leak report prints where each leaked
object was allocated.  Both QBE and native backends now emit the
extra unit-name string literal and AST line number at every
_ClassAlloc site.  Leak report output changes from:
  - TBox (rc=1)
to:
  - TBox (rc=1) at LeakSite:14

Runtime: bucket size grows from 16 to 32 bytes; LTInsert stores
unit-name ptr + line; _LeakTrackerReport prints the " at unit:line"
suffix.

Native backend: plumb FDebugMode from TCodeGenNative through
TNativeBackend; emit _LeakTrackerEnable in $main; emit
_LeakTrackerRegister at both constructor-call paths.

QBE backend: add FCurrentUnitName, set in Generate/GenerateUnit/
AppendUnit/AppendProgram; extend both _LeakTrackerRegister call
sites with EmitStrLit(FCurrentUnitName) + node.Line.

2766 tests pass (2 new e2e tests), FIXPOINT_OK.
2026-06-10 10:11:52 +01:00
Graeme Geldenhuys 924964089c fix(native): release managed fields of sret record arg temporaries
When a record-returning function call is passed directly as an argument
to another call (e.g. Consume(MakeRec('hello'))), the caller allocates
an sret buffer on the stack.  After the outer call returns, the managed
fields (strings, classes, dyn-arrays, interfaces) inside that buffer
must be released — otherwise they leak.

The QBE backend already handled this via EmitOwnedArgReleases; the
native backend lacked the equivalent.  Add EmitSretArgReleases which
iterates args in reverse, computes each sret buffer's stack offset,
and calls EmitRecordFieldReleases to release every managed leaf.
Wire it into all call emission paths: EmitCall, EmitCallIndirect,
EmitMethodCallStmt, EmitMethodCallExpr, EmitInheritedCall,
EmitInterfaceCall, EmitInterfaceFieldCall, EmitMethodPtrCall, and
the implicit-self inline call sites.
2026-06-10 09:43:29 +01:00
Graeme Geldenhuys bfaedec1a5 feat(codegen): weak interface vars + interface type-cast fix
Weak interface variables (both backends):
- Native: EmitInterfaceAssign routes [Weak] interface locals/globals
  through _WeakAssign (no refcount) instead of _ClassAddRef/_ClassRelease.
  Scope-exit cleanup emits _WeakClear for both interface and class weak
  locals.  EmitGlobalReleases checks IsWeakGlobal for program globals.
- QBE: already had full weak support (no changes needed).

Type-cast of interface variable (both backends):
- TVal(S) where S is an interface var must load from S_obj (the obj
  half of the fat pointer), not bare S (which doesn't exist as a symbol).
  QBE: EmitExpr type-cast path loads from VarRef_obj.
  Native: EmitExprToEax type-cast path uses IntfObjOperand.
  Previously crashed at link time with "undefined reference to S".

Two new e2e tests: IntfFieldFromFunc (sret into class field) and
WeakInterfaceVar (weak assignment + scope cleanup + destruction ordering).
2026-06-10 09:23:47 +01:00
Graeme Geldenhuys b76e05b7e4 feat(codegen): interface-field := func-returning-interface (sret)
Both backends now handle storing the result of an interface-returning
function into a class field:

  H.F := MakeIntf(42);      { explicit field assignment }
  F := MakeIntf(42);        { implicit-Self field inside a method }

QBE backend: EmitInterfaceToFieldSlots gains an IsInterfaceCall check
that allocates a 16-byte sret buffer, calls EmitRecordCallSret, loads
obj+itab from the buffer, releases the old field obj, and stores new —
no extra AddRef since the callee already retains into the sret buffer.

Native backend: both EmitInterfaceAssign (implicit-Self path) and
EmitInterfaceToFieldSlotsAt (explicit field path) gain TFuncCallExpr
branches that call EmitIntfSretCall, release the old obj from the
field, and copy obj+itab from the stack buffer into the field slots.

Previously both backends raised an error for this combination — the
sret convention for interface returns (commit c8ce6f7) only covered
local/global variable targets.
2026-06-10 09:09:09 +01:00
Graeme Geldenhuys c8ce6f7bbe feat(codegen): function-returning-interface via sret convention
Both QBE and native backends now support functions/methods that return
an interface type.  The return uses sret (struct-return) convention:
the caller allocates a 16-byte buffer (obj+itab), passes its address as
a hidden first argument, and the callee writes both fat-pointer slots
into the buffer.

QBE backend:
- Function signature adds hidden l %_par__sret, function becomes void
- Result_obj aliases sret+0, Result_itab aliases sret+8
- Epilogue is plain ret (no return value)
- Caller-side: alloc 16-byte buffer, EmitRecordCallSret, load obj+itab
- New IsInterfaceCall() helper and assignment handler for the sret path

Native backend:
- BuildFrame marks interface returns as FSretFunc (same as records)
- EmitIntfSretCall: allocates stack buffer, passes as %rdi, calls func
- EmitInterfaceAssign: new sret-Result path dereferences the Result
  pointer to write obj+itab into the caller's buffer
- Assignment handler loads obj+itab from sret buffer after the call

Previously, functions returning interfaces crashed the QBE backend
(undeclared split-slot names in IR) and were unsupported in native.
2026-06-10 08:54:59 +01:00
Graeme Geldenhuys 4eaec15e67 fix(native): ARC for managed-type static array element assignment
Extends the ARC element-assignment pattern to static arrays: when
assigning to A[I] where the element type is string or class, emit
AddRef(new)/Release(old) before the store, matching the QBE backend
and the dynarray path added in the previous commit.
2026-06-10 08:30:27 +01:00
Graeme Geldenhuys 0317a3b454 fix(native): ARC for managed-type dynamic array element assignment
When assigning to a dynamic array element where the element type is
managed (string or class), the native backend now emits AddRef on the
new value and Release on the old value before storing, matching the
QBE backend's behaviour.

Previously A[I] := 'new' would store without releasing the old string
at A[I], leaking one reference per overwrite.

E2E test TestRun_Native_DynArrayElemArc_String verifies string element
assignment with replacement on both backends.
2026-06-10 08:29:12 +01:00
Graeme Geldenhuys 02b03dbbd0 fix(codegen): nil-to-interface-field assignment and QBE tyInterface type code
Both backends:
- EmitInterfaceToFieldSlots[At] now handles TNilLiteral as the RHS of an
  interface-typed field assignment: release the old obj, zero both slots.

Native backend:
- The implicit-Self interface-field assignment path gains nil and
  TFieldAccessExpr source handling (was limited to tyClass and TIdentExpr).

QBE backend:
- QbeTypeOf now returns 'l' for tyInterface (was falling through to the
  default 'w').  This fixes Assigned(H.F) and other expressions that load
  an interface-typed field — the result type 'w' conflicted with the
  'loadl' instruction, causing QBE to reject the IR.

E2E test TestRun_Native_IntfFieldNilAssign covers the nil-to-field path
on both backends (sets a field, dispatches, clears, checks Assigned).
2026-06-10 08:26:35 +01:00
Graeme Geldenhuys ad40a64e3b fix(native): handle TFieldAccessExpr as interface argument in all call paths
When passing an interface stored in a class/record field (e.g. H.F where
F is IFoo) as an argument to a function or method, the native backend now
uses EmitInterfaceFieldAddr to resolve the contiguous fat pointer address
and loads obj+itab from it.

Added TFieldAccessExpr handling to six call-site locations:
- EmitMethodArgPush (method/inherited calls)
- EmitInterfaceCall arg loop (interface dispatch)
- EmitInterfaceFieldCall arg loop (field-based interface dispatch)
- EmitCall <=6 args path (standalone function calls)
- EmitCall >6 args path (pre-allocate strategy)

E2E test TestRun_Native_IntfFieldAsArg verifies the pattern on both
backends: a class holding an IVal field, passed to a standalone function
that dispatches through the interface.
2026-06-10 08:19:39 +01:00
Graeme Geldenhuys c12177e090 fix(native): interface-to-field store from field-access and implicit-Self sources
EmitInterfaceToFieldSlotsAt now handles two additional source expressions
when storing an interface value into a class/record field:

- TFieldAccessExpr: uses EmitInterfaceFieldAddr to resolve the contiguous
  fat pointer address from a nested field access (e.g. Obj.IntfField),
  then reads obj+itab from that address.

- Implicit-Self TIdentExpr: when the source identifier is an implicit
  Self field (e.g. bare FIntf inside a method body), reads the fat pointer
  directly from Self+offset instead of looking up split _obj/_itab slots.

Both paths read the contiguous (obj, itab) layout at the resolved address
and push them for the subsequent store logic.
2026-06-10 08:14:42 +01:00
Graeme Geldenhuys d19f2afbab fix(native): handle implicit-Self and nested interface dispatch receivers
Extend interface arg handling in EmitInterfaceCall and
EmitInterfaceFieldCall to support implicit-Self field receivers
(TIdentExpr.IsImplicitSelf with ImplicitFieldInfo) alongside the
existing plain ident path.

Remove the restriction on nested interface-field receivers (A.B.Method()
where B is an interface field): EmitInterfaceFieldAddr already handles
Base <> nil via EmitExprToEax, so the rejection was unnecessary.
2026-06-10 08:10:10 +01:00
Graeme Geldenhuys 756667e2a6 feat(native): support open-array literal args in method/inherited calls
EmitMethodArgPush now handles IsOpenArray parameters: inline array
literals allocate element storage on the stack via EmitOpenArrayLiteral
and push both the data pointer and high index; static-array and
forwarded open-array idents are handled as well.

A new FMethodArgExtraStack field tracks stack bytes allocated by
open-array literals so callers can reclaim them after the call.
All EmitMethodArgPush call sites (EmitMethodCallExpr, EmitMethodCallStmt,
EmitInheritedCall, EmitSretMethodCall, and implicit-Self paths in
EmitExprToEax/EmitExprToXmm0) reset and clean up accordingly.

This was the blocker for native self-hosting: the compiler source
uses Format() — whose array-of-const desugars to an open-array
literal — in method bodies.
2026-06-10 08:05:04 +01:00
Graeme Geldenhuys 058ec7e3c4 fix(native): use frame-local slots for for-loop end bounds
The native backend stored for-loop upper bounds in global variables.
When a recursive function contained a for-loop, each nested call
overwrote the same global, corrupting the outer loop's bound. This
caused out-of-bounds list access and segfaults during self-hosting
(e.g. AnalyseStmt iterating past Stmts.Count).

Pre-allocate _for_end_N frame-local slots in BuildFrame (one per
for-loop in the function body, counted by CountForStmts), and use
frame-relative addressing in EmitForStmt. Program-main code with
no frame falls back to globals (safe since main is non-recursive).
2026-06-10 07:48:44 +01:00
Graeme Geldenhuys 7f7418eb25 fix(native): use callee-saved %rbx for Format args array base
EmitFormatCall stored the varargs array base pointer in %r11 (caller-
saved), which was clobbered by any function call inside EmitExprToEax
when evaluating format arguments. This caused a segfault when a Format
argument was a function-call expression (e.g. ClassSymName in the QBE
codegen) because the store-back wrote to a garbage address.

Switch to %rbx (callee-saved), push/pop it around the format block so
the base pointer survives arbitrarily complex argument expressions.

Add E2E test with function-call Format arguments to cover this case.
2026-06-10 07:19:46 +01:00
Graeme Geldenhuys 956ca3ce66 fix(native): sret forwarding, arg-clobber reload, and record zero-init
Three related fixes for the native x86-64 backend's sret (struct-return)
handling:

1. Sret forwarding: when an sret function assigns Result from an inner
   sret call, the inner call's hidden first arg must be the forwarded
   caller buffer pointer (movq from the Result slot), not the address of
   the slot itself (leaq). Added ASretIsIndirect parameter to
   EmitSretCall/EmitMethodSretCall; the Result-in-sret-function path
   passes True, all other call sites pass False.

2. Arg-clobber reload: EmitSretCall saved the sret address in %r10
   before arg evaluation, but complex arg expressions (e.g.
   TStringList.Get) clobber caller-saved %r10. The final movq %r10, %rdi
   used a stale value. Fixed by reloading the sret operand directly into
   %rdi after arg evaluation, eliminating the stale-%r10 dependency.

3. Record zero-init: local record variables with managed fields (strings,
   classes) were not zero-initialised at function entry. If a record was
   never assigned (e.g. a loop body that never executed), the epilogue's
   field-release pass called _StringRelease on stack garbage. Added
   memset(0) for tyRecord locals in the function prologue, matching the
   QBE backend's alloc-then-zero pattern.

Without these fixes, the native-compiled compiler segfaulted during
semantic export when importing any unit with class types.
2026-06-10 07:12:28 +01:00
Graeme Geldenhuys 4d47f49536 fix(native): emit string-typed named constants as literal addresses
Named constants with string type (e.g. `const NL = #10`) were compiled
as `movabsq $0, %rax` — the integer ConstValue (always 0 for strings).
The native-compiled compiler crashed in TIRBuffer.AppendLine because
`NL := IR_NL` produced a nil string, and the subsequent BufCopy of the
newline character dereferenced null.

Now the IsConstant path in EmitExprToEax checks IsString() and emits
EmitStrLitAddr(ConstString) — registering the string literal in the
.rodata section and loading its header+12 address, matching QBE.

This was the last blocker preventing the native-compiled compiler from
running the QBE codegen pipeline (the only codegen path it currently
exercises).
2026-06-10 06:39:45 +01:00
Graeme Geldenhuys d267bbc7fc fix(native): double-deref var-param class receivers in method calls
When a class variable is passed as a var parameter and a method is called
on it (e.g. AList.Add inside a procedure taking var AList: TStringList),
the stack slot holds the address of the caller's variable, not the object
pointer.  Class method dispatch requires dereferencing twice: once to get
the caller's variable address, once to get the heap-allocated object.

The native backend was only dereferencing once, passing the address of the
global/local variable as Self.  This corrupted memory when the method wrote
to fields at offsets from the wrong base address — manifesting as adjacent
globals being overwritten (e.g. TStringList.Grow corrupting DumpAST).

Fixed all seven receiver-loading sites: EmitMethodCallExpr (<=6 and >6 slot
paths), EmitMethodCallStmt (<=6 and >6 paths, plus the Free handler),
EmitMethodSretCall, and the zero-arg TFieldAccessExpr.IsMethodCall path.
Also added record-var-param cases for consistency with the QBE backend.
2026-06-10 06:31:02 +01:00
Graeme Geldenhuys b0aa8b2701 fix(native): implicit-Self record copy, sret field ARC, and implicit-Self method calls
Three codegen bugs preventing the native-compiled compiler from working:

1. Implicit-Self record field copy (e.g. FCurrent := FLookahead inside a
   method) emitted a 4-byte scalar mov instead of memcpy.  Now emits
   EmitRecordFieldRetains on the source, EmitRecordFieldReleases on the
   dest, then memcpy for the full record size.

2. Sret Result field assignment (e.g. Result.Value := text inside a
   function returning a record) stored string/class/dynarray fields
   without AddRef, so the returned record held a pointer with refcount 0.
   Now applies the same ARC retain/release pattern as the IsClassAccess
   and IsImplicitSelf paths.

3. IsImplicitSelfMethod calls (bare method name inside another method of
   the same class) did not pass Self as the first argument. Added checks
   in the TProcCall, TFuncCallExpr (value + sret), and EmitExprToXmm0
   paths.  Also fixed sret Result := Record using movq (load the sret
   pointer) instead of leaq (address of the pointer slot).
2026-06-10 06:13:08 +01:00
Graeme Geldenhuys de954cafe4 fix(native): implicit-Self record field assignment uses sret for method calls
FR := FG.Make(...) inside a method body produces a TAssignment with
ImplicitSelfField set, not a TFieldAssignment. The sret handler for
record-returning calls was only in the TFieldAssignment branch and
never triggered, causing the assignment to treat a 24-byte record
return as a scalar in %rax (garbage values).

Add sret interception in the ImplicitSelfField non-managed branch
for both TMethodCallExpr and TFuncCallExpr. Also add the matching
TFieldAssignment handler for explicit obj.field := method() patterns.
Both paths compute the field address into %rbx (callee-saved), release
managed sub-fields of the old record, then call via sret convention.
2026-06-10 05:29:26 +01:00
Graeme Geldenhuys 83025ec137 feat(native): method calls returning records use sret convention
Method calls that return records larger than 16 bytes must use the
sret (struct return) calling convention: the caller allocates the
destination buffer and passes its address as the hidden first argument
in %rdi, with Self shifted to %rsi and explicit args from %rdx onward.

The native backend was treating these like scalar returns, storing the
method result in %rax and trying to write a multi-byte record through a
4-byte movl — corrupting memory and crashing on the first field access.

Add EmitMethodSretCall and wire it into the TAssignStmt handler for
record-typed method call expressions.  The callee side was already
correct (sret functions read %rdi as the Result buffer); only the
caller side was missing.
2026-06-10 05:18:00 +01:00
Graeme Geldenhuys 5a63ab077d fix(native): save Self in callee-saved %rbx across constructor arg eval
The <=6-args constructor path stored the newly allocated instance in
%r10 (caller-saved), then evaluated constructor arguments via
EmitMethodArgPush.  When an argument required a function call (e.g.
TStringList.GetText), the callee clobbered %r10 and the constructor
received a garbage Self pointer — crashing in _ClassRelease when
trying to ARC-release uninitialised fields.

Switch to %rbx (callee-saved, with push/pop) matching the >6-args
path that already used this strategy.
2026-06-10 05:07:03 +01:00
Graeme Geldenhuys 9edcf53a88 fix(native): var/out managed-type params use indirection for assignment
The native backend stored string/dynarray/class values directly into
the local param slot (-N(%rbp)) instead of writing through the
indirection pointer that var/out parameters carry.  This overwrote the
caller's variable address with a string value on the first assignment,
corrupting the stack frame.  Later reads of other locals would see
garbage pointers.

Fix all four managed-type assignment paths (tyString, tyDynArray,
tyClass, tyClass+nil) to detect IsVarParam and emit the double
dereference: load the pointer from the slot, load/store the actual
value through that pointer.

This unblocks the native-compiled compiler from parsing CLI arguments
that follow --source / --unit-path (the out-param file path was
corrupting Arg's stack slot on the next loop iteration).
2026-06-10 05:02:13 +01:00
Graeme Geldenhuys ef11eed238 fix(native): string equality uses _StringEquals instead of pointer cmp
The native backend's binary expression handler had no special case for
string = / <> operators, so they fell through to the integer comparison
path (cmpq), comparing pointer addresses instead of string content.
Two strings with identical content but different addresses (e.g.
ParamStr(1) vs a string literal) always compared as not-equal.

Add an early-exit in EmitExprToEax for boEQ/boNE on tyString: call
_StringEquals(left, right) and xorl $1 for <>.  This fixes the
native-compiled compiler's CLI flag parsing, which relies on string
comparison against '--source', '--emit-ir', etc.
2026-06-10 04:52:16 +01:00
Graeme Geldenhuys 75ab341a2d fix(native): vtable dispatch for virtual method calls in expression context
EmitMethodCallExpr was always emitting a direct `callq` for method calls,
ignoring VTableSlot.  Virtual calls like B.GetVal() (where B's declared
type is an abstract base) produced a direct call to the base's method
symbol, which is never emitted for abstract methods — causing link errors.

Add vtable dispatch (load vtable ptr from Self, index by VTableSlot,
indirect call) in both the <=6 and >6 arg paths of EmitMethodCallExpr,
matching what EmitMethodCallStmt already does.

This resolves the last undefined references (Streams_TInputStream_Read,
Streams_TOutputStream_Write) when native-compiling the compiler itself.
The native-compiled compiler now links and runs.
2026-06-10 04:46:41 +01:00
Graeme Geldenhuys 367a8c0a79 fix(native): emit call for implicit-self property getter / zero-arg method
When a bare identifier inside a method resolves to a zero-arg method or
property getter on Self (TIdentExpr.IsImplicitSelfMethod), the native
backend was falling through to a global variable load instead of
emitting a function call.  This produced `movq GetterName(%rip), %rax`
(reading a non-existent global) instead of `callq ClassName_GetterName`.

Add the missing EmitExprToEax branch: load Self into %rdi, call the
getter via MethodEmitNameNative.

Add e2e tests for implicit-self property getter and const array string
element access.
2026-06-10 04:41:24 +01:00
Graeme Geldenhuys fe321ed42c feat(native): emit const array data + use ConstArraySymbol for references
Emit unit-level and class-level const arrays (string and integer) as
.data sections in the native backend, mirroring the QBE backend's
EmitGlobalConstData/EmitLocalArrayConstsInBlock/EmitLocalArrayConstsInTypeDecls.

Use TIdentExpr.ConstArraySymbol (the mangled QBE data label set by the
semantic pass) for all RIP-relative base-address loads of const arrays,
matching the label emitted in the data section. Previously the native
backend used the bare Pascal name (e.g. SysVArgRegs64) while the data
label was the mangled form (__bac_2_SysVArgRegs64), causing undefined
reference link errors.

Fixed all static-array reference sites: EmitExprToEax (tyStaticArray
ident), var-param arg push (push and pre-allocate paths),
EmitMethodArgPush, open-array pass paths, and procedural-type call
paths.
2026-06-10 04:35:33 +01:00
Graeme Geldenhuys 33ba90ca67 fix(native): unit-prefix class/interface data symbols via ClassSymName
The native backend emitted typeinfo, vtable, FieldCleanup, itab, and
impllist symbols with bare class names (e.g., typeinfo_TIdentExpr) but
is/as/Supports expressions and constructor calls referenced them with
unit-prefixed names (typeinfo_uAST_TIdentExpr).  This caused 700+
undefined references when native-compiling the compiler.

Apply ClassSymName consistently in EmitClassSection, EmitInterfaceDefs,
constructor allocation, and interface assignment/arg-push sites.  Add
IntfTypeInfoName helper (matches QBE backend) to distinguish generic
interface instances (bare mangled name) from plain interfaces (unit-
prefixed via ClassSymName).

Also fixes var-param implicit-Self field address in the pre-allocate
(>6 args) path of EmitCall.

Native-compiling the compiler now has only 8 unique undefined symbols
remaining (down from 735): SysVArgRegs const arrays, GetCurrentScope
property, and two abstract Streams methods.
2026-06-10 04:19:57 +01:00
Graeme Geldenhuys c4b23f5b20 fix(native): var-param implicit-Self field address via Self + offset
When passing a class field as a var/out argument inside a method, the
address must be computed as Self + field_offset, not leaq FieldName(%rip).
The latter treats the field name as a global symbol, causing link errors.

Add IsImplicitSelf checks in all three var-param code paths:
EmitMethodArgPush, EmitCall push/pop path, and EmitCall pre-allocate path.
2026-06-10 04:03:42 +01:00
Graeme Geldenhuys 44f3508b5d fix(native): implicit-Self class-field access priority over IsClassAccess
When a TFieldAccessExpr has both IsImplicitSelf and IsClassAccess set
(e.g. FInner.FVal where FInner is a class field of Self), the
IsClassAccess branch was checked first, emitting RecordName(%rip) as a
global symbol instead of loading through Self. This caused 100+
undefined-reference link errors when native-compiling the compiler.

Fix: check IsImplicitSelf before IsClassAccess in all three affected
sites (field read, @field address-of, and array-field subscript).
2026-06-10 03:58:37 +01:00
Graeme Geldenhuys 55d634bf58 feat(native): ClassName/ClassType + method call >6 args
Add ClassName/ClassType built-in handlers to the native backend:
ClassName dereferences instance→vtable[0]→typeinfo[0]→name(+16),
ClassType dereferences instance→vtable[0]→typeinfo[0].

Fix method calls with >6 total argument slots (Self + user args) in
EmitMethodCallStmt, EmitMethodCallExpr, and the constructor path.
The old push/pop strategy overflowed the SysVArgRegs64 array. The new
pre-allocate strategy places overflow args at the top of the frame so
they remain on the stack after the register-loaded slots are stripped
via addq, matching the SysV ABI convention for stack-passed arguments.
The constructor path uses callee-saved %rbx to hold the instance
pointer across arg-evaluation calls.
2026-06-10 03:47:25 +01:00
Graeme Geldenhuys 4c8efe8947 feat(native): property writes via setter + fix exception bookkeeping
Property writes via setter methods: TFieldAssignment with PropWriteInfo
calls the setter with Self/%rdi, optional index/%rsi, value/%rdx.

Fix codegen-time double-decrement of FExcDepth and FFinallyStack in
EmitTryFinallyStmt and EmitTryExceptStmt: both the normal and exception
code paths are emitted at codegen time, so each Dec/Delete ran twice.
Now the exception path restores the bookkeeping state before emitting
its own Dec/Delete. Also replace FFinallyStack.Clear() with Free/Create
to avoid ARC release of stale slot data in the TList backing store.
2026-06-10 03:25:18 +01:00
Graeme Geldenhuys 383d10be4d feat(native): property reads via getter methods
Emit method calls for TFieldAccessExpr nodes with PropRead set,
covering chained (Base), implicit-Self, and plain variable receivers.
Indexed properties pass the subscript as the second argument (%rsi).
2026-06-10 03:14:47 +01:00
Graeme Geldenhuys 60879d71c5 fix(native): Format() with array-literal args + better error diagnostics
EmitFormatCall now detects when the second argument is a
TArrayLiteralExpr (the Format('...', [a, b, c]) syntax) and iterates
its elements individually, matching the QBE backend's handling.
Previously, the array literal reached EmitExprToEax which raised
an unsupported-expression error.

Also improve the unsupported-expression error to include line/col
information for easier debugging.
2026-06-10 03:02:10 +01:00
Graeme Geldenhuys 37434ec4a0 feat(native): Inc/Dec on field access, deref and var params + type-cast fix
Extend EmitIncDec to handle all l-value forms the QBE backend supports:
- TFieldAccessExpr (record/class fields via address computation)
- TDerefExpr (pointer dereference P^)
- Implicit-Self fields (bare FField inside methods)
- Var-param TIdentExpr (dereference the pointer slot)

Extract EmitIncDecAddrOp helper for the load-add/sub-store sequence on
an address in %rdx.  Use proper 32-bit register names (eax/ecx) for
non-wide types.

Also add tyInterface to IntByteSize (was falling to the 4-byte default
despite interfaces being pointer-sized), fixing type-cast expressions
on interface values.

Add e2e tests: Inc/Dec on record field, Inc/Dec on pointer deref, and
Pointer/Integer type-cast round-trip.
2026-06-10 02:54:54 +01:00
Graeme Geldenhuys afb99da767 feat(native): support >6 argument slots in all call variants
Restructure EmitCall, EmitCallIndirect, EmitSretCall and
EmitMethodPtrCall to handle arbitrary argument counts using a
pre-allocate strategy: reserve SlotCount*8 bytes on the stack, evaluate
each argument into its fixed slot, then load the first 6 integer args
into SysV registers and leave overflow args on the stack in correct
forward order.

Also extend SysVXmmArgRegs from 6 to 8 entries (SysV allows 8 float
regs: xmm0-xmm7) and add mixed int/float argument routing in the
pre-allocate path.

Add e2e test TestRun_Native_TenArgs exercising a 10-integer-arg
function call on both backends.
2026-06-10 02:37:45 +01:00
Graeme Geldenhuys 40755b987a refactor(ast): replace TIdentExpr.IsVarParam with TParamMode enum
Introduce TParamMode (pmNone, pmVar, pmRecordValue, pmStaticArrayValue)
on TIdentExpr so the semantic pass records the precise param-slot
classification once instead of merging three cases into a Boolean.

The QBE backend treats all three modes identically (ParamMode <> pmNone),
preserving existing semantics.  The native x86-64 and LLVM backends can
now dispatch on the enum directly instead of re-deriving the distinction
from Sym.Kind and TypeDesc.Kind at every reader site.

Other IsVarParam fields (TFieldAccessExpr, TMethodCallExpr, TAssignment,
TMethodParam, TMethodCallStmt) are unchanged.

Based on Andrew Haines' proposal and patch (blaise_llvm d16f877).
2026-06-10 02:06:43 +01:00
Graeme Geldenhuys d00323e0b2 fix(arc): propagate IsUnretained/IsWeak flags during field inheritance
When a child class inherits fields from its parent, the field loop in
both AnalyseTypeDecls and InstantiateGeneric copied Name and TypeDesc
but dropped IsUnretained and IsWeak flags.  This caused _FieldCleanup
in child classes to emit _ClassRelease calls for inherited [Unretained]
fields, releasing references the child does not own — a use-after-free
when the shared reference (e.g. a symbol-table type cache) is still
live elsewhere.

Fix both inheritance sites to copy the flags after AddField.  Add a
regression test that asserts TChild's _FieldCleanup contains exactly
one _ClassRelease (for the owned field) and not two.
2026-06-10 01:44:15 +01:00
Graeme Geldenhuys b7e82d6ffb fix(codegen): typed-pointer writes pick store opcode from pointee type
EmitPointerWrite chose the QBE store opcode from a hand-rolled
QType=='w' check that fell through to storel for everything else.
That meant:

  PDouble^  := V  -> storel of a d-typed temp  (QBE rejects the type mismatch)
  PSingle^  := V  -> storel into a 4-byte slot (8-byte clobber)
  PSmallInt^:= V  -> storew into a 2-byte slot (4-byte clobber)

Route through the existing StoreInstrFor helper so the opcode matches
the pointee width and class (stored / stores / storeh / storeb /
storew / storel).  Also add truncd/exts coercion when the value
expression's type width doesn't match the pointee (e.g. double
literal written through PSingle^).

Native backend: add tyDouble/tySingle branches to the pointer-write
handler so float values flow through EmitExprToXmm0 and store via
movsd/movss instead of the integer EmitStoreVar path.

Credit: Andrew Haines (andrewd207) reported the bug and provided the
QBE-side fix.

Regression tests:
- cp.test.pointers: IR-shape tests asserting stored/stores for
  PDouble^/PSingle^ writes.
- cp.test.e2e.pointers: PDouble^ round-trip, PSingle^ adjacent-slot
  clobber check.
- cp.test.e2e.native: same two cases through the native backend.
Suite: 2727 OK. FIXPOINT_OK.
2026-06-10 01:31:50 +01:00
Graeme Geldenhuys 1e3d3e442d feat(native): trig/math builtins with Single-precision dispatch
Add all 14 trig/math builtins to the native x86-64 backend (Sin, Cos,
Tan, ArcTan, ArcTan2, ArcSin, ArcCos, Ln, Log2, Log10, Power, Sinh,
Cosh, Tanh) with proper Single dispatch — when the argument type is
tySingle, emit the f-suffixed libm variant (sinf, cosf, etc.) to avoid
a needless Single→Double→Single round-trip, matching the QBE backend.

Also add Single dispatch to existing builtins that were missing it:
Sqrt (sqrtss vs sqrtsd), Round/Trunc (cvtss2si vs cvtsd2si),
Floor/Ceil (floorf/ceilf + cvttss2si), IsNaN/IsInfinite (__isnanf/
__isinff), and SingleToStr (call _SingleToStr directly instead of
widening to double).

Introduces EmitFloatBuiltin() to handle trig calls in the
EmitExprToXmm0 path (float context, e.g. Round(Sin(X))), alongside
the existing EmitExprToEax handlers (integer context).

Two new e2e tests: Sin/Cos and Sqrt through the native path.
Suite: 2721 OK. FIXPOINT_OK.
2026-06-10 01:19:47 +01:00
Graeme Geldenhuys b09984afd9 feat(native): ClassCreate builtin for runtime metaclass construction 2026-06-10 01:06:20 +01:00
Graeme Geldenhuys dd0cf0c69f feat(native): file/process/path/math builtins for M9 self-hosting
Add 40+ expression and statement builtins to the native x86-64 backend
covering all RTL functions used by the compiler source:

Expression builtins: OrdAt, FileExists, DirectoryExists, ReadFile,
  ExtractFilePath/FileName/FileDir/FileExt, ChangeFileExt,
  ForceDirectories, ExcludeTrailingPathDelimiter,
  IncludeTrailingPathDelimiter, RenameFile, SetCurrentDir,
  GetCurrentDir, ParamStr, ParamCount, GetEnvVar/GetEnvironmentVariable,
  GetProcessID, GetTempDir, GetTempFileName, Exec,
  CurrentExceptionMessage, ProcessCreate/Running/ReadOutput/ExitCode,
  FileAge, Floor, Ceil, IsNaN, IsInfinite.

Statement builtins: DeleteFile, RemoveDir, WriteFile, AppendFile,
  ProcessSetExe/AddArg/Execute/WaitOnExit/Free.
2026-06-10 01:03:58 +01:00
Graeme Geldenhuys 8cb24857e7 feat(native): nested (local) procedures with captured variables
Nested procedures declared inside a function body are now supported by
the native x86-64 backend.  Each nested proc is mangled as Outer_Inner
and emitted before the enclosing function.  Captured outer-scope
variables are passed as implicit leading pointer params (_cap_<Name>);
reads/writes inside the nested function transparently dereference
through the capture pointer.  Forwarding works when the caller is
itself nested (forwards its own _cap_ slot).

Two new e2e tests: read-capture (inner reads outer var) and
write-capture (inner mutates outer var, outer sees the change).
2026-06-10 00:59:38 +01:00
Graeme Geldenhuys 08e21bce92 feat(native): builtins + 32-bit comparison fix
Add expression builtins to the native x86-64 backend: Ord, Assigned,
Int64ToStr, UInt64ToStr, Abs, DoubleToStr, SingleToStr, StrToDouble,
Round, Trunc, Sqrt, CompareStr, CompareText, PosEx, UpCase.

Add statement builtins: Halt, ZeroMem, Sleep.

Fix 32-bit signed comparison: use cmpl for narrow integer types instead
of cmpq — prevents misinterpreting negative int32 return values (e.g.
from _StringCompare) as large positives due to zero-extension.

8 new e2e tests covering Ord, Assigned, Abs, Halt, Round/Trunc,
CompareStr, UpCase, and Int64ToStr on the native path.
2026-06-10 00:50:07 +01:00
Graeme Geldenhuys 6ff7d2dd14 feat(native): TIndirectFuncCallExpr (callback calls as expressions)
Handle function pointer calls that return a value, e.g. Fns[0](7).
The callee expression is evaluated first and saved on the stack (safe
across arg evaluation which may invoke callq), then args are pushed,
popped into SysV registers, and the saved function pointer dispatched
via callq *%r10.
2026-06-10 00:29:15 +01:00
Graeme Geldenhuys 43d9d702f0 feat(native): TIsExpr, TAsExpr, TSupportsExpr + ClassSymName
Implement `X is TFoo` (class type test via _IsInstance), `X as TFoo`
(checked downcast with _Raise_InvalidCast on failure), and
`Supports(Obj, IFoo)` (interface query via _ImplementsInterface).

Add ClassSymName helper that resolves a class/interface name to its
assembly symbol with unit prefix (e.g. Exception → SysUtils_Exception),
mirroring the QBE backend's ClassSymName. Fix existing as-cast and
except-handler typeinfo references to use ClassSymName instead of bare
NativeMangle(TypeName).
2026-06-10 00:26:14 +01:00
Graeme Geldenhuys 54b19e6fee feat(native): procedural/dynarray/interface param and return types
Add tyProcedural, tyDynArray, and tyInterface to the parameter and
return type guards in EmitFunctionDef.  All three are 8-byte pointer
values that flow through the existing register/stack spill path.

tyProcedural params were the first blocker when trying to compile the
TestRunner with --backend native.
2026-06-10 00:18:47 +01:00
Graeme Geldenhuys c0d603f39f fix(native): short-circuit boolean and/or evaluation
Boolean and/or were using bitwise andq/orq, evaluating both operands
unconditionally.  Code like `(P <> nil) and P.IsX` crashed because
P.IsX was evaluated even when P was nil.

Now boolean and/or emit conditional jumps: for `and`, LHS=0 skips RHS;
for `or`, LHS<>0 skips RHS.  Bitwise integer and/or (IsNumeric operands)
remain unchanged.
2026-06-10 00:14:04 +01:00
Graeme Geldenhuys 18e5826ee6 feat(native): by-value record params with full ARC
The native x86-64 backend now supports by-value record parameters with
correct value semantics and ARC management:

Callee side: memcpy from the incoming pointer into a local data buffer,
store the buffer address in an indirection slot (matching the semantic
pass's IsVarParam pointer-through convention), then AddRef each managed
leaf on entry and Release on exit.  Const params skip both.

Caller side: record-returning function calls used as arguments (e.g.
Consume(MakeOuter())) now emit an sret call into a stack-allocated temp
buffer; the buffer address is passed to the outer call.

Adjacent fixes surfaced by the record-param tests:
- Field assignment ARC for IsVarParam records: string/class/dynarray
  fields written through the var-param indirection pointer now get
  AddRef(new) + Release(old), preventing double-frees and leaks.
- Field assignment ARC for local/global records: same managed-field
  ARC pattern applied to the local and global record assignment paths.
- sret Result dereference: nested field access on an sret Result
  (e.g. Result.Inner.N) now loads the pointer from the slot instead
  of taking the slot's address, fixing a crash with nested records.

Suite: 2701 tests (136→142 native), FIXPOINT_OK, rolling bootstrap OK.
2026-06-09 21:24:51 +01:00
Andrew Haines 4313c6e53a fix(arc): release managed fields of record arg temporaries after the call
Companion to the callee-side ARC pass.  When a record-returning function
result is consumed inline as a by-value record argument — DoSomething(GetRec())
— the caller allocates an sret buffer for the return, hands it to the
callee as the aggregate arg, then drops the buffer at end of statement.
With no per-managed-leaf release on that buffer the temp's string,
dynarray, class, and interface fields leaked one ref per caller
invocation, accumulating quickly in loops.

EmitOwnedArgReleases now handles tyRecord: when the argument expression
is a record-returning function or method call (the only shape that
produces a fresh sret temp), the helper walks the buffer with
EmitRecordReleaseFields and emits a release per managed leaf, recursing
through nested record fields.  Variable references (DoSomething(W)) are
explicitly excluded — their storage belongs to the enclosing scope's
own ARC pass and must not be touched by the call site.

POD record temps reach the helper too, but the field walk finds nothing
to release, so the cleanup compiles down to a no-op for them.

Covered by IR-level assertions on the temp / variable / nested-record
cases, plus an e2e stress loop that runs the inline pattern 1000 times
with a two-string nested record.
2026-06-09 19:30:29 +01:00
Andrew Haines f5368c0699 fix(arc): retain managed fields of by-value record params
A record passed by value whose fields include managed types (string,
dynarray, interface, class) was operated on by the callee without an
entry-side AddRef.  When the callee reassigned such a field, the
standard release-old/addref-new lowering freed the caller's shared
heap data and left the caller pointing at memory the allocator could
reuse, producing use-after-free reads (and frequently crashes) on the
next access.

QBE materialises the aggregate so the callee already gets its own
record bytes; the bug was purely the missing per-managed-leaf retain.
Fix mirrors the existing tyString/tyClass/tyInterface ARC pass for
flat managed params: walk the record at function entry and AddRef
each managed leaf (recursing into nested records), then Release them
at exit to balance.  const record params skip both ends, matching
the existing rule that the caller's retain covers the whole call.

Regression covered by IR-level assertions on prologue _StringAddRef /
_DynArrayAddRef plus an e2e test that round-trips a heap-allocated
string through a by-value record param.
2026-06-09 19:30:25 +01:00
Graeme Geldenhuys 1ae1b3f547 test(runner): fail loudly when a suite subprocess crashes or truncates
A [Threaded] test suite runs as a `--suite NAME --verbose` subprocess whose
stdout is parsed for results.  If that subprocess crashed mid-suite (e.g.
SIGSEGV), the runner counted however many `... OK` lines it had received and
recorded NO error — so a crash masqueraded as a green run with a silently lower
test count.  This is exactly how a real codegen crash
(TCrossUnitRefTests.TestLocalTypeRef_UnqualifiedUnitName) stayed hidden behind
an `OK` summary.

A healthy suite subprocess always ends with a summary line ("OK (...)" or
"FAIL (...)").  After collecting each subprocess's output, if that line is
absent the process was killed or died early, so its parsed count is incomplete:
record a loud error naming the suite (and its exit code) so RunAll returns
non-zero and the crash is surfaced under "Errors:".  ProcessExitCode maps a
signal death to 1 and a clean exit to its real code, so the missing-summary
check is the reliable signal regardless of exit code; an exit code > 1 with a
summary present is also flagged as abnormal.

This complements the earlier [Threaded]-parse fix (commit 576f60d): that one
restored failure *counts*; this one stops a *crash* from being silently dropped.

Full suite OK (2687), FIXPOINT_OK.
2026-06-09 19:06:07 +01:00
Graeme Geldenhuys e6f234b9de fix(qbe): pass class-field record/array by address, not a loaded scalar
Passing a record (or static array) sourced from a CLASS FIELD by value crashed:
`SumIt(H.R)` where H is a class instance and R a record field emitted the
argument as `loadl <field_addr>` — dereferencing the field and passing its first
8 bytes as the `:_ffi_<Rec>` aggregate.  The callee's pointer parameter then
aimed into the field's bytes instead of at the record, so the first read
returned garbage and segfaulted.

The `IsClassAccess` branch of the field-read in EmitExpr was missing the
record/static-array case that the plain record-field branch already has:
inline-aggregate fields must yield their ADDRESS (records and static arrays are
passed and assigned by address-as-value), not a loaded scalar.  Add the
`tyRecord`/`tyStaticArray` -> Exit(Ptr) guard to the class-field branch, matching
the record-field branch.

This is an addressing bug independent of refcounting — it crashes a pure-integer
record too.  It was the cause of the SIGSEGV in
TCrossUnitRefTests.TestLocalTypeRef_UnqualifiedUnitName (IsLocalRef(Sig.ReturnType)
passes a class-field record by const value), which a threaded test subprocess
had been crashing on silently.

Full suite OK (2687), FIXPOINT_OK.
2026-06-09 19:05:56 +01:00
Graeme Geldenhuys 4f5588d344 feat(native): multi-unit program support (whole-program model)
A program that `uses` one or more units now compiles and runs under
`--backend native`, matching the QBE backend.  Previously `AppendUnit`/
`AppendProgram` on the native code generator raised "multi-unit compilation not
yet implemented", so any program with a `uses` clause (including stdlib units
like StrUtils) failed — this blocked compiling most real programs and a large
part of the test suite natively.

The model is whole-program (the same one the QBE backend uses for a `--source
program` build): every dependency unit plus the program is emitted into ONE
assembly file and linked into one binary; there is no separate .o linking.

Implementation:

  * TNativeBackend (base): adds abstract `EmitUnit`, and non-clearing
    `AppendUnit`/`AppendProgram`/`GetOutput` so units and the program accumulate
    into a single assembly buffer (mirrors the QBE `Generate` vs append split).
  * TCodeGenNative: `AppendUnit`/`AppendProgram` now drive the backend instead
    of raising.
  * TX86_64Backend.EmitUnit: emits a unit's globals, class/record/generic method
    bodies, standalone impl-block procedures, generic function instances, the
    unit `initialization` as `<Unit>_init` (called from `$main` after _SetArgs),
    and the unit's class + interface data sections.
  * The shared section emitters (`EmitClassSection`/`EmitClassMethods`/
    `EmitInterfaceDefs`) now take the type-decl list and the symbol table as
    explicit parameters rather than reaching through `AProg`, so they serve both
    program and unit decls.  A `FSystemDefsEmitted` guard emits TObject/
    TCustomAttribute system definitions exactly once across all units + program.
  * Three pre-existing native gaps surfaced by compiling all of StrUtils
    (whole-program) and fixed for parity: `external name '...'` symbols are
    honoured verbatim (no unit mangling of C symbols); GetMem/ReallocMem/FreeMem
    builtins; PChar subscript read/write.

The symbol table is passed explicitly to the section emitters because the
single-program path (`Generate` → `EmitProgram`) does not set a persistent
symbol-table field — depending on one would dereference nil there.

Tests: 5 multi-unit e2e tests in cp.test.e2e.native.pas (plain function, string
function, class, interface, globals+initialization) plus a backend-parameterised
`CompileAndRunWithUnitOn` harness in cp.test.e2e.base.pas — each runs on both
backends and asserts native==QBE stdout.

Full suite OK (2686), FIXPOINT_OK (QBE self-host path byte-identical).
2026-06-09 18:38:30 +01:00
Graeme Geldenhuys facd18bdca fix(native): load Result after epilogue ARC releases, not before
EmitFunctionDef loaded the function Result into %rax (or %xmm0) at the top of
the epilogue, then ran the local/param ARC release pass — which calls
_ClassRelease / _StringRelease / _DynArrayRelease.  Those calls clobber %rax, so
any value-returning function that also releases an ARC-managed parameter or
local returned garbage.

Most visibly, a method returning Integer but taking an interface (or class)
value parameter releases that parameter on exit, so `U.Use(I)` returned 3
instead of 55; a String-returning function taking a String parameter returned a
clobbered pointer.

Move the Result load to AFTER every ARC release call, immediately before
leave/ret, so nothing can clobber it.  sret (record-returning) functions are
unaffected — they return via the caller's buffer and load no Result.  The $main
epilogue already sets %eax=0 after its global-release pass, so it was correct.

Two e2e regression tests on both backends: TestRun_Native_RetValSurvivesArcRelease
(value return past a class-param release) and TestRun_Native_IntfArgToMethod
(interface value arg to a method, dispatched in the callee).

Full suite OK (2681), FIXPOINT_OK (native-backend-only change).
2026-06-09 17:25:39 +01:00
Graeme Geldenhuys 971897a411 fix(opdf): export class vtables under --debug-opdf so .opdf links
The OPDF class record stores each class's VMTAddress (the debugger reads an
object's VMT pointer at runtime and matches it to identify the dynamic type).
The QBE backend emits user-class and generic-instance vtables as LOCAL symbols
(`data $vtable_X`), while the .opdf debug section — assembled as a separate
object file — references `vtable_X`.  A local symbol is not visible across
object files, so every `--debug-opdf` build of a program containing a class
failed to link with "undefined reference to vtable_X".

Add an OPDF-mode flag to the codegen (ICodeGen.SetOpdfMode, wired from the
driver's OPDFEnabled).  When on, the QBE backend emits user/generic vtables as
`export data` so the .opdf object can resolve them; when off (every normal
build) the output is byte-for-byte unchanged — verified: non-OPDF IR still emits
`data $vtable_TThing`, --debug-opdf emits `export data $vtable_TThing`.  The
native backend already emits its vtables `.globl`, so its SetOpdfMode is a no-op.

Scope is deliberately gated on OPDF mode to avoid duplicate-symbol errors in the
separate-compilation (per-unit) path, where the same generic vtable can appear
in multiple objects; OPDF is only used with whole-program --source compiles.

Result: `--debug-opdf` now links and runs class programs, and the OPDF reference
debugger loads them.  Full suite OK (2679), FIXPOINT_OK (output unchanged for
non-OPDF builds).
2026-06-09 16:53:23 +01:00
Graeme Geldenhuys d3777ffcfa fix(codegen): interface stored in a field — dispatch and read-into-local
Two related gaps with interfaces held in a record/class field, across both
backends.

Gap 1 — native dispatch through a non-Self interface field (H.G.Greet()):
EmitInterfaceCall assumed a named interface receiver and emitted bare
_obj/_itab operands (empty name prefix) for a field receiver, failing to link.
It now takes an optional receiver expression; when that is an interface-typed
TFieldAccessExpr the obj/itab are loaded from the field's contiguous fat
pointer via the new EmitInterfaceFieldAddr helper.  The TMethodCallExpr
interface-dispatch site passes ACall.ObjExpr.

Gap 2 — reading an interface out of a field into an interface local (G := H.G),
which failed on BOTH backends:
  * QBE: the interface-to-interface assignment branch required a TIdentExpr RHS,
    so a TFieldAccessExpr source fell through to the scalar store path and
    emitted a single-slot `storew ..., $F` against a name that has only
    $F_obj/$F_itab data definitions — an undefined-symbol link error.  It now
    accepts a TFieldAccessExpr RHS and resolves obj/itab via
    EmitInterfaceExprPair (which already handles the field-access shape).
  * Native: EmitInterfaceAssign gained a TFieldAccessExpr RHS case that loads
    the source field's contiguous fat pointer via EmitInterfaceFieldAddr.

e2e: TestRun_Native_IntfFieldDispatch (dispatch with an argument through a
field) and TestRun_Native_IntfFieldReadIntoLocal (read field into local, then
dispatch) — both run on native and QBE via AssertRunsOnBoth.

Full suite OK (2679), FIXPOINT_OK.
2026-06-08 19:41:41 +01:00
Graeme Geldenhuys b3ea6138cb fix(native): lower Chr(N) to _Chr so it works as a String
The native x86-64 backend had no Chr builtin case.  Chr(N) used in a String
context — `s := Chr(66)`, `'A' + Chr(66) + 'C'`, `s := s + Chr(i)` — emitted the
integer N where a String pointer was expected, so the following _StringAddRef
dereferenced the raw integer and segfaulted.

Add a Chr case to the native builtin-function dispatch that lowers Chr(N) to
_Chr(N), which allocates and returns a one-character heap String.  The result is
a String pointer and flows through the existing String ARC assignment/concat
paths unchanged — matching the QBE backend.

e2e: TestRun_Native_String_ChrConcat covers assignment and a concatenation loop,
run on both backends via AssertRunsOnBoth.  Full suite OK (2677), FIXPOINT_OK,
valgrind-clean.
2026-06-08 19:33:16 +01:00
Graeme Geldenhuys 4d1e53c5b7 fix(native): bring x86-64 backend ARC to parity with the QBE backend
The native x86-64 backend lacked the three recent QBE ARC fixes (f74e5cc,
96514ee, 935bd52) and carried two adjacent ARC gaps of its own.  Managed
record/class fields, dyn-array variables, interface fields, and global
objects leaked or were never released, diverging from the QBE backend's
behaviour.

Ported the three QBE fixes:

  * NativeExprOwnsRef broadened from tyClass-only to also cover tyString and
    tyDynArray, so a String/dyn-array call result assigned into a variable or
    field consumes its transferred +1 instead of being retained again
    (mirrors 96514ee).  Class retains stay unconditional — some method calls
    return a borrowed class reference that NativeExprOwnsRef reports as owning,
    so eliding the retain would release a reference the slot never acquired.

  * EmitRecordFieldReleases (new): a field-kind walker that releases string,
    class, dyn-array and interface-obj fields, clears weak fields, skips
    unretained ones, and recurses into nested record fields.  Used by
    _FieldCleanup_<T> and by record-local scope-exit cleanup, so a class with
    a record field whose own fields are managed no longer leaks them
    (mirrors f74e5cc).  Adds dyn-array variable assignment and dyn-array
    local scope-exit release.

  * EmitInterfaceToFieldSlotsAt (new): stores an interface into a record/class
    field's contiguous 16-byte fat pointer (obj at +0, itab at +8) with ARC on
    the obj slot — class source references the static itab_<Class>_<Interface>
    symbol, interface-ident source copies both slots (mirrors 935bd52).  A
    unified interface-field block computes the receiver base for each shape and
    dispatches to it.

Fixed two adjacent native-only ARC gaps:

  * Program-exit global release (EmitGlobalReleases): the native $main epilogue
    released no globals, so a global object's _FieldCleanup (and destructor)
    never ran.  The QBE backend releases each managed global at @main_exit;
    the native epilogue now does the same for string/class/interface/dyn-array
    and record-field globals.

  * Chained field stores through a receiver expression (H.R.Obj := X) emitted a
    plain store with no ARC; managed fields now retain the new value and release
    the old, matching direct field stores.

The implicit-Self managed-field store (FName := x inside a method) was a plain
store with no ARC and was also intercepted by the type-keyed variable branches,
which treated the field name as a variable.  It is now hoisted to the top of
the assignment handler and ARC-managed; interface implicit-Self fields continue
to route through EmitInterfaceAssign.

The ARC decision logic (NativeExprOwnsRef, the field-kind walks) is
target-independent; only the leaf emission is CPU-specific.  A header comment in
blaise.codegen.native.backend.pas records that these walkers should lift into
the base class as template methods when the i386/arm64 backends land.

Tests: five cross-backend e2e tests in cp.test.e2e.native.pas
(TestRun_Native_Arc{DynArrayField, InterfaceField, NestedRecordField,
StringReturnToField, ImplicitSelfStringField}) — AssertRunsOnBoth compiles and
runs each program on both the native and QBE backends and asserts identical
stdout.  They observe ARC through destructor side effects and read-back values.

Full suite OK (2676 tests), valgrind-clean on a combined stress program,
FIXPOINT_OK (the native backend is not on the self-hosting path).
2026-06-08 19:21:59 +01:00
Graeme Geldenhuys 53ea92da00 fix(semantic): break analyser<->symbol-table cycle that leaked the AST
Compiling even a tiny program leaked ~339 heap objects (TSymbol ×129,
TObjectList ×156, TTypeDesc ×14, plus the TProgram, TSymbolTable and
TSemanticAnalyser singletons), despite the driver correctly calling
Prog.Free() and Semantic.Free().

Root cause was a strong reference cycle: TSemanticAnalyser owns its
TSymbolTable (FTable), and the table held a plain strong reference back to
the analyser via FUsesChainProvider (set in Analyse so Lookup can consult
per-unit visibility).  Under refcount ARC the two keep each other alive:
Semantic.Free() drops the analyser to rc=1 (the table still references it),
the analyser destructor never runs, FTable.Free() is never called, and the
entire reachable graph — every scope, symbol, builtin type descriptor, and
the whole AST — leaks as one unit.

Marking FUsesChainProvider [Unretained] breaks the cycle.  The provider is
the analyser that owns the table and always outlives it (its destructor is
what frees the table), so a non-owning reference cannot dangle.

Impact: --debug leak count on a tiny compile drops from 339 to 14 (~96%
reclaimed).  The remaining 14 are a separate, smaller global-scope fragment.
Full suite OK (2671 tests), self-compile clean, FIXPOINT_OK.

This adjusts TSymbolTable's field layout (one ARC slot becomes non-managed),
so it is a layout-affecting change validated via stage-2 fixpoint.
2026-06-08 18:05:07 +01:00
Graeme Geldenhuys 935bd5237d fix(qbe): support interface-typed fields inside records and classes
An interface stored in a record/class field could not be used: assigning a
class into such a field dropped the itab and skipped ARC, calling a method
through the field emitted an undefined %_var__obj temp that QBE rejected, and
the field was sized 8 bytes so a following field overlapped the itab slot.

Root cause: interfaces have two in-memory representations.  A named interface
local/global uses two SEPARATELY-allocated slots (%_var_X_obj, %_var_X_itab);
an interface stored in a record/class field uses a CONTIGUOUS 16-byte fat
pointer (obj@+0, itab@+8).  The codegen only knew how to read/write the
split-slot form, so any field-access receiver fell through to the split-slot
path and emitted undefined temps, and the field was never sized for 16 bytes.

This commit:

  * uSymbolTable.pas — adds tyInterface: Result := 16 to RawSize and ByteSize
    (fat pointer).  Record/class field offsets and TotalSize flow from
    ByteSize, so a following field now sits past the itab.  AllocAlign stays 8
    (two 8-byte slots).  Interface LOCALS are unaffected — their split slots
    are allocated by hardcoded codegen, not from these sizes.  LAYOUT-CHANGING:
    requires a stage-2 rebuild to validate.
  * EmitInterfaceExprPair — adds a TFieldAccessExpr case that loads obj/itab
    from the field's contiguous memory (addr / addr+8), so an interface read
    from a record field can be passed as an argument or dispatched on.
  * Both interface method-dispatch sites (EmitMethodCall and the
    TMethodCallExpr emitter) route an expression receiver through
    EmitInterfaceExprPair instead of the %_var_<ObjectName>_obj/_itab naming
    that only a named local has.
  * EmitFieldAssignment — adds a tyInterface case that stores the fat pointer
    (obj@Ptr, itab@Ptr+8) with ARC via EmitInterfaceToFieldSlots, skipping the
    single-value EmitExpr that would drop the itab.
  * EmitInterfaceToFieldSlots — takes the destination interface type and, for a
    class source, references the static $itab_<Class>_<Interface> symbol (as
    the interface direct-assignment path does) instead of a bogus _GetItab call
    keyed only by the class typeinfo, which returned the wrong itab.

Verified: interface-in-record assign + method dispatch + record copy all run
correctly; plain interface locals unchanged; full suite OK (2671 tests),
self-compile clean, --debug leak count unchanged at 339, FIXPOINT_OK.

Tests: e2e TestRun_Record_InterfaceField_AssignCallAndCopy plus IR tests
asserting no undefined %_var__obj temp, use of the static itab, and the
16-byte field layout (Tag at offset 16).
2026-06-08 17:50:32 +01:00
Graeme Geldenhuys 96514eeca4 fix(qbe): stop double-retaining ARC return values on assignment
A function or method returning a String or dynamic array transfers a +1
reference to the caller (the callee AddRef'd on `Result := x` and did not
release Result at scope exit).  The string and dyn-array assignment branches
nonetheless emitted an unconditional _StringAddRef / _DynArrayAddRef on the
call result, so every `r := MakeString()` / `r := MakeArray()` leaked one
buffer per call.  Argument passing leaked the same way: a string/dyn-array
call-result passed by value was never released after the call.

Root cause: ExprOwnsRef — the helper that detects an already-owned +1
transient so the assignment site can skip the retain — was gated to tyClass
only.  For strings and dyn-arrays it always returned False, so the retain
was never elided.

This commit:

  * Broadens ExprOwnsRef to tyString and tyDynArray.  The body logic
    (function/method/property-getter calls own +1; variable/field reads and
    casts are borrowed) is type-agnostic and already correct for these kinds.
  * Guards the AddRef with `if not ExprOwnsRef` in the string and dyn-array
    branches of variable assignment, var-param assignment, implicit-Self
    field assignment, and field assignment — mirroring the class branches.
  * Rewrites EmitOwnedArgReleases to dispatch the transient release on the
    argument's ARC kind (_StringRelease / _DynArrayRelease / _ClassRelease —
    the headers sit at different offsets, so a wrong-kind release corrupts
    the heap) and to skip const-string params, which are already balanced by
    EnsureConstStringRef + ReleaseConstStringArgs.  Threads the param list
    through all call sites.

The class field-assignment retain is deliberately left unconditional: some
method calls return a borrowed class reference without an AddRef yet
ExprOwnsRef reports them as owning, so guarding that retain would release a
reference the field never acquired (a use-after-free the self-hosting
compiler hits in its own codegen).  That remaining transient leak is the
safe direction and is tracked among the known-leak backlog.

Verified: string-return leak 14MB->1.3MB, dyn-array-return leak 207MB->1.3MB,
string-arg leak bounded, all at 100k iterations.  Full suite OK (2668 tests),
self-compile clean, FIXPOINT_OK.

Adds four IR regression tests in cp.test.arc.pas asserting the call-result
assignment elides the retain while a borrowed variable assignment still
retains, for both String and dyn-array.
2026-06-08 17:25:23 +01:00
Andrew Haines f74e5ccf43 fix(qbe): refcount dyn-array, interface, and nested-record fields in records
Records with value semantics may contain fields with reference semantics
(String, dynamic array, interface, nested managed record).  The QBE
backend's field walkers only covered the String and Class cases, so:

  * A record whose field is a dynamic array leaked the buffer on every
    copy and return-by-value — _DynArrayAddRef/_DynArrayRelease were not
    invoked from EmitRecordCopy or EmitRecordReleaseFields, and there
    was no scope-exit release for dyn-array locals.
  * A class with a record field whose own fields were managed (String,
    Class, dyn-array, interface, or a deeper record) leaked those
    sub-fields when the class was released — _FieldCleanup_<T> skipped
    record-typed fields entirely.
  * Interface fields inside records were never copied or released.

This commit:

  * Adds _DynArrayAddRef / _DynArrayRelease to blaise_arc.pas alongside
    the existing _StringAddRef/_StringRelease and _ClassAddRef/_ClassRelease.
    They walk the existing [refcount:4][length:4] dyn-array buffer header
    (layout defined by _DynArraySetLength in blaise_str.pas), are nil-safe
    and immortal-safe (rc = -1), and use _AtomicAddInt32 / _AtomicSubInt32
    (lock xadd) so the refcount stays sound under threading — matching the
    other ARC primitives.
  * Extends EmitRecordCopy, EmitRecordReleaseFields, and
    EmitFieldCleanupFn to handle tyDynArray, tyInterface, and nested
    tyRecord fields (the latter by recursing through the shared
    EmitRecordReleaseFields walker).
  * Routes tyDynArray field assignments through the existing IsArc
    path so a field write retains the new buffer and releases the old.
  * Adds tyDynArray to EmitArcCleanup + EmitExcPathArcCleanup so
    dyn-array locals balance their first-assignment retain at scope
    exit (and on exception paths).
  * Adds tyDynArray to EmitAssignment for variable-to-variable
    assignment so b := a between dyn-array vars is refcount-symmetric.

Adds two regression tests in cp.test.e2e.records.pas:

  * TestRun_Record_DynArrayField_ReturnByValue_NoLeak — 5000-iter
    return-by-value of a record with a dyn-array field; asserts the
    buffer contents survive (proves AddRef/Release pairing) and the
    program runs to completion.
  * TestRun_Class_RecordField_NestedClass_FullCleanup — class -> record
    -> class -> record -> class chain with each Create/Destroy bumping
    a global AliveCount; after 100 iterations AliveCount must be 0,
    proving _FieldCleanup recurses through nested record fields.

TestRunner: OK (2663 tests).  Self-bootstrap from a master-built stage-1
through stage-2/3/4 reaches a clean stage-3 == stage-4 IR fixpoint.
2026-06-07 20:28:16 -04:00
Graeme Geldenhuys 328493baf2 fix(bif): resolve bif-coverage gaps for current master
- Add TIndirectFuncCallExpr encoder/decoder to uUnitInterfaceIO.pas
  (was missing entirely, causing --incremental to silently drop
  indirect call nodes)
- Bump COMPILER_ID to blaise-0.11.0-dev+bif1
- Fix version check to compare base version only (strip -SNAPSHOT
  and -dev suffixes before comparing)
- Change test to Ignore() when binary missing (module is not
  activeByDefault)
- Remove redundant <version> from tool project.xml (inherits root)
- Regenerate bif-coverage.status for current AST (68 classes,
  38 encoder/decoder cases)
2026-06-08 00:34:12 +01:00
Andrew Haines 00ca54afc4 fix(tools): add () to bare zero-arg calls in bif-coverage
Master's mandatory-parens enforcement (51ebf23) now flags every bare
zero-arg function reference. Sweep BifCoverage.pas and its TestRunner
wrapper, and swap sLineBreak for LineEnding.
2026-06-08 00:27:24 +01:00
Andrew Haines 02169411b0 test(compiler): wire bif-coverage into TestRunner
Shell out to tools/bif-coverage/target/bif-coverage and assert exit 0.
Hard-fails (not skips) when the binary is missing - a working
verifier is a prerequisite for landing AST changes, and silently
skipping would let drift accumulate.  No chdir or env setup needed
since bif-coverage now resolves its own paths from CWD.
2026-06-08 00:27:24 +01:00
Andrew Hathaway 7fe0e90563 refactor(driver): promote UseNative Boolean to TBackend enum
Replace UseNative: Boolean with Backend: TBackend (bkQBE, bkNative).
The enum makes the driver's intent explicit — picking one of N codegen
pipelines — and adding a third backend (LLVM) becomes a one-line type
change plus exhaustiveness checks at the two dispatch sites.
2026-06-08 00:15:07 +01:00
Graeme Geldenhuys a9c55eddcc feat(lang): for-in over string with Integer var iterates codepoints
When the loop variable is Byte, for-in iterates raw UTF-8 bytes (existing
behaviour). When the loop variable is Integer, for-in now iterates Unicode
codepoints by calling _Utf8DecodeAt and advancing by the decoded byte
count. Any other loop variable type is a semantic error.

Both QBE and native x86-64 backends implement the new codegen path using
a synthetic __adv_N variable to preserve the byte advance across the loop
body. Includes E2E tests for 2-byte and 3-byte UTF-8 sequences, unit
tests for the semantic restriction, and 8 new E2E tests for the StrUtils
codepoint functions.
2026-06-07 23:46:07 +01:00
Graeme Geldenhuys dd2e5ddb47 fix(codegen): emit \x hex escapes in string literals for GNU as
Both QBE and native backends were emitting \C3 format (bare backslash +
hex digits) for non-ASCII bytes in string constants. GNU as only
recognises \xNN as hex escapes; bare \NN is treated as an unknown
escape and the characters are output literally. This caused WriteLn to
print hex representations instead of raw UTF-8 bytes (e.g. 'Pâques'
displayed as 'PC3A2ques'). Fix: emit \xC3 instead of \C3.
2026-06-07 23:26:47 +01:00
Graeme Geldenhuys f0bf9b33f8 fix(native): add SizeOf handler to native x86-64 backend (fixes #86)
The native backend was missing a handler for SizeOf() in EmitExprToEax,
causing it to fall through to the generic function call path and produce
garbage values. Emit movq $N, %rax where N is the resolved type's
ByteSize(), matching the QBE backend's compile-time constant folding.
2026-06-07 14:53:32 +01:00
Graeme Geldenhuys f6ea10194f feat(lang): support enum sets with up to 64 members (fixes #81)
Sets with 33–64 enum members now use 8-byte (QBE 'l' / x86-64 64-bit)
storage instead of silently truncating to 32 bits. Both QBE and native
x86-64 backends emit correct instructions for all set operations:
literals, in, Include/Exclude, union/difference/intersection, equality,
and for-in iteration. Enumerations with more than 64 members in a
set-of declaration are rejected with a clear semantic error.

Also fixes tkThreadvar missing from CheckUnitNamePart, which broke
parsing of unit names containing 'threadvar' in self-hosted builds.
2026-06-07 13:52:27 +01:00
Graeme Geldenhuys 58f6a050db feat(punit): add AssertEquals overload for UInt64, register UInt64ToStr built-in
PtrUInt (= UInt64) had no matching AssertEquals overload, causing
ambiguous-overload errors in test_blaise_mem. Added the UInt64 overload
to punit and registered UInt64ToStr in the symbol table (it was already
handled in semantic + codegen but missing from the built-in registry).

Reverts the Pointer-cast workaround from the previous commit — the test
now uses PtrUInt directly as intended.
2026-06-07 12:31:04 +01:00
Graeme Geldenhuys 00596b2ca6 fix(lang): add mandatory () to all bare zero-arg calls, add semantic diagnostic
The mandatory-() migration (commit 57c0660) missed ~1000 bare zero-arg
function and method calls across the compiler, stdlib, and test suite.
Without (), these were silently treated as variable reads, producing
broken QBE IR (%_var_ temporaries instead of call instructions).

Added semantic error diagnostics in uSemantic.pas to catch bare references
to functions/procedures that require () for a call. This prevents silent
miscompilation and gives users a clear error message.

Fixed all bare calls across 69 files: compiler pipeline (uParser,
uSemantic, uCodeGenQBE, uLexer, uPasTokeniser, etc.), stdlib (classes),
and the full test suite (inline test programs and test framework code).

FIXPOINT_OK at 262,220 lines. 2627 tests pass. Rolling bootstrap from
v0.10.0 verified.
2026-06-07 05:46:48 +01:00
Graeme Geldenhuys dafeb84147 chore: begin v0.11.0-dev cycle 2026-06-07 02:09:50 +01:00
Graeme Geldenhuys b7cdcd29c4 release: v0.10.0
Self-hosting fixpoint verified on 262,202 lines of QBE IR.
Mandatory () on all zero-argument calls, parallel incremental
compilation, thread-safe ARC, and 2627 tests passing.
2026-06-07 02:09:09 +01:00
Graeme Geldenhuys 57c0660a37 refactor(lang): remove IsNoArgFuncCall dead code, fix remaining bare calls
With mandatory () on all zero-argument calls (51ebf23), the
IsNoArgFuncCall/NoArgFuncDecl fields on TIdentExpr and the
synthesised TFuncCallExpr codegen path are dead code. Remove them.

This exposed ~300 bare function calls missed by the original
migration script across compiler, runtime, stdlib, tests, and
embedded Blaise source strings. Fix all of them.

Add IsProcFieldCall support to TMethodCallExpr so that
H.Handler() works for procedure-type fields — the parser creates
TMethodCallExpr for Obj.Name(), and the semantic pass now detects
when 'Name' is a procedural-typed field rather than a method,
routing through the indirect-call codegen path.

Update fixpoint.sh to fall back to an existing blaise_rtl.a when
the release binary is too old to build the runtime.

2627 tests pass, FIXPOINT_OK.
2026-06-06 20:28:13 +01:00
Graeme Geldenhuys 51ebf2350a feat(lang): require mandatory () on all zero-argument calls
Make parentheses mandatory on every function, procedure, method, and
constructor call — even those with zero arguments.  A bare identifier
or field access is now unambiguously a variable/field/property read;
appending () makes it a call.

Mechanically migrated all 144 source files (compiler, runtime, stdlib,
tests, kanban tool).  Fixed several latent bugs exposed by the AST node
transition from TFieldAccessExpr.IsMethodCall to TMethodCallExpr:

- IsBuiltinToString applied to record methods (added tyClass guard)
- IsVarParam not set for value record/static-array parameters in
  AnalyseMethodCallExpr (extended to check skParameter + aggregate type)
- Native backend used movq (pointer load) for record receivers instead
  of leaq (address-of) in EmitMethodCallExpr
- ResolveDiamond now handles TMethodCallExpr for diamond-operator
  constructor calls

Updated grammar.ebnf (MethodCall, ProcCall, Factor rules) and
language-rationale.adoc with the design decision.  Marked the
future-improvements.adoc entry as implemented.

All 2627 tests pass.  Fixpoint verified (FIXPOINT_OK).
2026-06-06 19:14:47 +01:00
Graeme Geldenhuys 1a2b04cef3 test(e2e): standalone pointer-to-record field deref test
Adds TestRun_Record_PointerDerefFieldAccess covering PP^.X / PP^.Y
at program scope (no class/implicit-self). Runs on both QBE and native
backends via AssertRunsOnBoth.
2026-06-06 17:08:38 +01:00
Graeme Geldenhuys 8c0d0774fd fix(tests): include stdout in e2e exit-code mismatch messages
When an e2e test fails with unexpected exit code, the failure message
now includes the program's actual stdout. Previously it only showed
"exit code Expected: 0 Actual: 1" with no indication of what the
program printed (often the compilation error or runtime output).
2026-06-06 17:04:25 +01:00
Graeme Geldenhuys f19a3e4c7d feat(compiler): add --dump-ast flag for post-semantic AST inspection
Prints the resolved AST tree to stdout after semantic analysis, then
exits. Shows node types, resolved types with sizes, field offsets, and
semantic flags (IsImplicitSelf, IsClassAccess, IsVarParam, VarParam,
ImplicitSelfField, etc.) that are critical for diagnosing codegen bugs
where the wrong IR or assembly is emitted due to an incorrect flag or
missing field offset.
2026-06-06 17:02:34 +01:00
Graeme Geldenhuys a7d5240086 fix(codegen): @RecordField inside methods and pointer-to-record deref
Address-of an implicit-self record field (@Pt inside a method where Pt
is a class field) emitted a reference to a non-existent local %_var_Pt.
QBE codegen now delegates TIdentExpr addr-of to EmitVarArgAddr which
already computes Self + field offset. Also added IsImplicitSelf handling
to EmitLValueAddr for TFieldAccessExpr.

Native backend: added @ImplicitSelfField path before the generic
@Variable path, computing Self + offset like the QBE backend.

Native backend: fixed pointer-to-record dereference (PP^.X) crash.
EmitExprToEax for TDerefExpr was loading the pointed-to value even for
record types, causing a double-dereference when followed by field
access. Records are address-represented, so P^ on a ^TRecord just
returns the pointer value (the record address).
2026-06-06 16:48:18 +01:00
Graeme Geldenhuys 230ba1ee87 fix(compiler): support record method calls in statement position
The semantic pass only allowed tyClass/tyInterface receivers for
statement-position method calls (TMethodCallStmt), rejecting record
variables with "'X' is not a class or interface variable". Expression-
position record method calls already worked. Extended AnalyseMethodCall
to accept tyRecord receivers.

QBE codegen: record Self is a pointer-to-stack (not a heap pointer), so
the receiver address is passed directly without a pointer dereference.
Added record-method branches to EmitMethodCall for local, var-param,
and implicit-self cases, with a tyRecord guard to skip the class-pointer
load.

Native x86-64 codegen: added parallel handling for record methods across
five sites — EmitMethodCallStmt Self-loading, TIdentExpr.IsImplicitSelf
(bare field reads inside methods), TAssignment.ImplicitSelfField (bare
field writes), and three TFieldAccessExpr.IsImplicitSelf sites that were
missing ImplicitBaseInfo.Offset and IsClassAccess deref.

Also fixed a nil-deref crash in EmitMethodCall when IsProcFieldCall is
set (ResolvedMethod is nil) by guarding MDecl.IsRecordMethod checks.
2026-06-06 16:40:13 +01:00
Graeme Geldenhuys f9e6bcdfcf feat(native): implement for-in and case statements in x86-64 backend
Add EmitForInStmt with all five iteration strategies: static array,
dynamic array, string byte-iteration, set bit-scan, and class
enumerator protocol (GetEnumerator/MoveNext/Current with vtable
dispatch).  Add EmitCaseStmt with linear comparison chain supporting
both integer/enum and string selectors via _StringEquals.

Also add set literal expression support (TArrayLiteralExpr with tySet)
to EmitExprToEax, computing the bitmask at compile time.

Extend CountTryStmts to recurse into TForInStmt and TCaseStmt bodies
for correct exception frame pre-allocation.

Upgrade 10 existing QBE-only e2e tests to AssertRunsOnBoth, covering
all for-in paths (string, static array, dynamic array, set, class
enumerator) and case statements (integer branch, else branch, enum
selector).

2622 tests pass; FIXPOINT_OK (stage-3/stage-4).
2026-06-06 15:54:13 +01:00
Graeme Geldenhuys 0aa69e3bfb fix(stdlib): TThread.Create(False) now joins on Free via ARC
TThread.Create(False) appeared to silently skip execution (GitHub #73).
Root cause: Start took an extra _ClassAddRef so the trampoline could
release it on exit. This kept the refcount at 1 after Free, preventing
Destroy (and its WaitFor/pthread_join) from running — the main program
exited before the thread finished.

Fix: remove the trampoline's ARC reference entirely. The caller's
reference is the only one; releasing it triggers Destroy → WaitFor →
pthread_join, which blocks until the thread has fully exited. This is
safe because pthread_join guarantees the trampoline has returned before
Destroy frees the object.

Also remove FreeOnTerminate — ARC makes it redundant. In Delphi/FPC it
exists because manual Free is the only cleanup path; in Blaise, scope
exit or reassignment automatically joins and frees the thread. The
migration analyser can flag this for ported code.

Document the TThread ARC lifetime model in language-rationale.adoc and
add class/method documentation comments to classes.pas.
2026-06-06 14:56:18 +01:00
Graeme Geldenhuys 66c000405b fix(compiler): resolve all EImportError crashes in warm incremental compile
The unit-cache warm path (--incremental --unit-cache) crashed on import
because the BIF interface reader could not handle several type patterns
and registration gaps:

- Add two-pass type registration (pre-register class/record/interface
  names in pass 1, fill details in pass 2) so forward references within
  the same unit resolve correctly
- Add ResolveInlineTypeName helper for field types stored as raw strings
  (^Pointer, array[L..H] of T, array of T, class of T)
- Add RegisterProcType for TProceduralTypeDef import (e.g. procedure-of-
  object types like TFieldCleanupProc)
- Fix 0-based string indexing in RegisterAlias (StrAt instead of [1])
- Register imported class methods in FMethodIndex so call-site resolution
  works during warm compile
- Serialize/deserialize TPropertyDecl through BIF (clone, export, import)
  so property access resolves on cached units
- Add unit-cache directory to search paths so .o files are found
- Include exception class name in compiler error output
2026-06-06 14:25:01 +01:00
Graeme Geldenhuys 43c5a9d637 feat(compiler): parallel incremental compilation via worker threads
Refactor the --incremental compilation path to use worker threads.
Each dependency unit now gets its own TCompileWorker thread that
independently handles codegen (fresh TCodeGenQBE, read-only symbol
table), IR file writing, and external tool invocation (qbe + cc).
All workers run concurrently and are joined before the main codegen
phase.

Also extract CompileUnitToObjectSafe which returns an error string
instead of calling Halt, making it safe for use in worker threads.
2026-06-06 12:57:10 +01:00
Graeme Geldenhuys e093be0b70 feat(rtl): atomic ARC refcount operations for thread safety
Replace plain read-modify-write refcount operations with atomic
lock xadd instructions via a new blaise_atomic_x86_64.s assembly
stub.  _AtomicAddInt32 and _AtomicSubInt32 return the previous
value, so exactly one thread sees the transition to zero and
triggers destruction -- eliminating the TOCTOU race in
_ClassRelease and _StringRelease.

Affected operations:
  - _ClassAddRef: atomic increment
  - _ClassRelease: atomic decrement, destroy on old_rc == 1
  - _StringAddRef: atomic increment (immortal check unchanged)
  - _StringRelease: atomic decrement, free on old_rc == 1
2026-06-06 12:44:53 +01:00
Graeme Geldenhuys 51a00a5038 feat(rtl): per-thread memory allocator via threadvar
Convert the allocator's global state (FreeLists, ArenaHead,
LargeFreeHead, LargeFreeCount) from var to threadvar, giving each
thread its own independent arenas and freelists with zero contention
and no locks required.

Also fix the native backend .tbss emission for aggregate types
(tyRecord, tyStaticArray) — the threadvar section was using
IntByteSize which returns 4 for unknown types, instead of RawSize
which returns the actual byte size of the type.
2026-06-06 12:35:46 +01:00
Graeme Geldenhuys 53817d1b64 fix(native): emit .data section unconditionally before globals loop
The threadvar refactor moved .data emission to be conditional inside the
globals loop, which meant when no regular globals existed but exception
frame slots did, the frames were emitted without a .data directive and
landed in .note.GNU-stack — causing linker errors for any program with
try/finally blocks but no global variables.

Move .data emission before the loop, unconditionally when HasData is true.
2026-06-06 12:27:35 +01:00
Graeme Geldenhuys 66d7a3a10f fix(config): silence stderr warning for unknown config keys
The warning leaked into test runner output, making it look like a real problem.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-06 12:18:49 +01:00
Graeme Geldenhuys 43f5e49d9e feat(kanban): detect and merge external file changes in TUI
Add file-change monitoring to the kanban TUI so that tasks added via CLI
(or any external process) while the TUI is running are automatically
merged into the live view instead of being silently overwritten on save.

The implementation has three layers:

1. New FileAge built-in (compiler + runtime): returns the file's mtime as
   Int64 via stat(), or -1 if the file does not exist.  Follows the same
   pattern as FileExists — symbol table registration, semantic analysis,
   codegen, and POSIX ABI stub.

2. Merge-aware TBoard (kanban.data): tracks last-known mtime, deleted IDs
   (so re-reads don't resurrect user-deleted tasks), and provides
   HasExternalChanges/MergeFromDisk.  Save() now auto-merges before
   writing.  ID conflicts (same ID, different task) are resolved by
   reassigning the in-memory task to a fresh ID.

3. Idle polling in TKanbanUI.Run: every ~2 seconds (20 idle ReadKey
   cycles at VTIME=1), checks HasExternalChanges and merges new tasks
   with a status bar notification.
2026-06-06 12:08:57 +01:00
Graeme Geldenhuys 3bf220c7f4 feat(lang): add threadvar support for thread-local storage
Add the `threadvar` keyword for declaring thread-local storage variables.
Each thread gets its own zero-initialised copy. Only allowed at program
or unit scope.

Lexer/Parser: new tkThreadVar token; ParseVarBlock accepts threadvar.
AST: IsThreadVar field on TVarDecl, TAssignment, TIdentExpr.
Symbol table: IsThreadVar on TSymbol.
Semantic: propagates IsThreadVar through analysis; rejects local scope.
QBE backend: emits `export thread data` declarations and `thread $Name`
references for correct TLS access (local-exec model via %fs:@tpoff).
Native backend: emits .tbss section and %fs:Name@tpoff addressing.
Unit interface: serialises IsThreadVar through .bif files.

Tests: 8 unit tests (parser, semantic, IR emission) + 2 E2E tests
(compile and run programs using threadvars).
2026-06-06 12:03:36 +01:00
Graeme Geldenhuys 577febbad5 feat(native): implement Inc/Dec built-ins and class-local ARC in native backend
Add inline Inc(x)/Inc(x,n)/Dec(x)/Dec(x,n) lowering for both 32-bit
and 64-bit operands — previously these emitted an empty callq that
caused assembler errors.

Zero-initialise class-typed local variables in function prologues and
release them in epilogues, matching the QBE backend's ARC behaviour.
Without this, the first assignment to a class local tried to
_ClassRelease uninitialised stack garbage.

Handle .Free on expression-based receivers (e.g. TThing(P).Free)
where the receiver is a temporary with no named slot to nil-out.
2026-06-06 10:50:50 +01:00
Graeme Geldenhuys e122aa48f8 feat(native): implement .Free intrinsic in native backend
The QBE backend lowers O.Free to _ClassRelease + nil-out-slot, but
the native backend had no handling for it — Free has no ResolvedMethod
(it is a compiler intrinsic, not a real symbol table entry) so
EmitMethodCallStmt raised an error.

Added the same lowering: load slot value, call _ClassRelease, store
zero.  Handles both local/global variables and implicit-Self field
access (Self.FChild.Free).
2026-06-06 10:38:33 +01:00
Graeme Geldenhuys 0a5bc44150 fix(arc): release old value when assigning nil to a class variable
Assigning nil to a class-typed variable (O := nil) did not call
_ClassRelease on the old value in either backend.  The nil literal
has ResolvedType.Kind = tyNil which bypassed the tyClass ARC branch
in EmitAssignment, falling through to the generic scalar store that
performs no reference counting.

QBE backend: added a class-nil branch in EmitAssignment that loads the
old value, calls _ClassRelease, and stores zero.  Handles strong, weak,
and promoted-local cases.

Native backend: added the same class-nil assignment branch, plus two
further fixes:
- EmitExprToEax now handles TNilLiteral (emits xorq %rax, %rax)
- EmitFieldCleanupFn now emits the destructor call and ARC field
  releases instead of being an empty stub, bringing it to parity with
  the QBE backend's FieldCleanup emission.

Updated leak-check tests whose leak scenarios relied on the broken
nil-assignment behaviour — they now use _ClassAddRef to create genuine
unbalanced reference leaks.

Updated test count in README (2293 -> 2606).
2026-06-06 10:33:43 +01:00
Graeme Geldenhuys 4447a67211 fix(incremental): export symbols from per-unit .o files and use correct symbol table
Two bugs prevented --incremental from producing linkable per-unit .o files:

1. Per-unit codegen used Semantic.GetSymbolTable which returns nil after
   Analyse() (the table is transferred to Prog.SymbolTable). This caused
   all class metadata (typeinfo, vtables, field cleanup) to be silently
   skipped. Fixed by using Prog.SymbolTable.

2. All functions and data emitted by AppendUnit lacked the QBE `export`
   keyword, making symbols file-local in each .o — the linker could not
   resolve cross-unit references. Added FExportAll flag and ExportPrefix
   helper to TCodeGenQBE; the incremental path sets SetExportAll(True).

Additionally, system defs (TObject/TCustomAttribute typeinfo, vtable,
FieldCleanup stubs) are now skipped in per-unit .o mode (they would
cause multiple-definition errors) and always exported in the program
path so all per-unit .o files can reference them.
2026-06-06 09:31:05 +01:00
Graeme Geldenhuys cee6cef934 test(e2e): add nested record field assign + method call tests
Covers the codegen fix for TFieldAccessExpr on record-typed fields,
which previously emitted a scalar load instead of returning the field
address. Tests both QBE (D.ToString via string return) and native
(D.Sum via integer return) backends, verifying chained field access
(DT.Date), record-to-record assignment, and method calls on nested
record fields.
2026-06-06 00:28:04 +01:00
Graeme Geldenhuys e11e5dad5e feat: extend not operator to integer types (bitwise complement)
The `not` operator previously only accepted Boolean operands. It now
accepts all integer types (Byte, SmallInt, Word, Integer, Int64, UInt64)
and performs bitwise complement, enabling bitmask patterns like
`Flags := Flags and (not MASK)`.

Semantic pass promotes narrow types to Integer, preserves Int64/UInt64.
QBE backend emits `xor -1` (w or l suffix by width). Native backend
emits `notl`/`notq`. Also adds missing bitwise binary ops (and, or,
xor, shl, shr) and nested record field read/write support to the
native backend.
2026-06-06 00:21:59 +01:00
Graeme Geldenhuys 5b15406a18 feat(native): implement TAddrOfExpr, TDerefExpr and TPointerWriteStmt
Full address-of expression support in the x86-64 native backend:
- @Variable (local and global)
- @VarParam (load pointer value from param slot)
- @StaticArray[I], @OpenArray[I], @DynArray[I] (element address)
- @Rec.Arr[I] (field array element address, all access patterns)
- @FuncName (code address with mangled name via ResolvedFreeRoutine)
- @Obj.Method (method-pointer construction at assignment level)
- @Rec.Field (general field address)

Also adds TDerefExpr (P^ read) and TPointerWriteStmt (P^ := val)
with ARC handling for string and class element types.

Includes five e2e tests covering @Variable, @StaticArr[I],
@DynArr[I], @Rec.Arr[I], and @Obj.Method patterns.
2026-06-05 23:48:54 +01:00
Graeme Geldenhuys 04a0b275dd feat(native): ARC retain/release for string/class/interface value params
Retain string, class, and interface value params on function entry and
release them on exit, matching the QBE backend's ARC semantics. Const,
var, out, and open-array params are skipped. Includes e2e tests for
string and class value param passing.
2026-06-05 20:17:10 +01:00
Graeme Geldenhuys f6315b8ac3 feat(native): ARC retain/release for class and string field assignments
Add proper ARC handling when storing to class or string fields via
class-access or implicit-self paths. [Unretained] fields store-only
with optional RHS release, [Weak] fields call _WeakAssign, normal
class fields do addref(new)/release(old)/store, and string fields
do _StringAddRef/_StringRelease. Adds NativeExprOwnsRef helper.

Includes e2e tests for class field store+read and string field
store+read.
2026-06-05 20:14:03 +01:00
Graeme Geldenhuys d96eef10bb feat(native): array field subscript read (R.Arr[I]) and var-param validation
Implement IsArrayAccess handling in the native x86-64 backend for
record field reads. Supports static arrays, dynamic arrays, and open
arrays with correct element-address computation (base + field offset +
index * element size). Also fix var-param type validation to skip the
unsupported-type check for var/out params (always pointer-sized).
2026-06-05 20:04:55 +01:00
Graeme Geldenhuys 52731f4642 feat(native): full generics support in x86-64 backend
Implement monomorphised generic emission for all four generic kinds:
generic classes (typeinfo, vtable, field cleanup, name strings, method
bodies), generic records (method bodies), generic interfaces (typeinfo),
generic standalone functions, and class-to-interface itab/impllist.

Also fixes three latent native backend bugs exposed by generic code
paths: NativeMangle was not applied in FuncSymbolFromDecl/FuncSymbolOf
(raw angle brackets in assembly symbols), TFieldAccessExpr.IsMethodCall
was unhandled (zero-arg method calls like B.GetVal), and IsVarParam
field access was not dereferenced (record Self passed by pointer).

Four new e2e tests verify generic records, classes, standalone functions,
and class-implementing-interface all produce correct output on both QBE
and native backends.
2026-06-05 19:26:50 +01:00
Graeme Geldenhuys deb09f5b56 fix(codegen): mangle generic class name in itab symbol for interface assignment
QBEMangle was not applied to the class name portion of itab symbols when
emitting class-to-interface assignments for generic classes. This caused
QBE to reject symbols containing raw angle brackets (e.g.
$itab_TBox<Integer>_IValue). Now emits $itab_TBox_Integer_IValue.
2026-06-05 19:26:39 +01:00
Graeme Geldenhuys 216e8704f5 feat(generics): support generic records (monomorphization)
Extend the generics system to support record types alongside classes and
interfaces.  Generic records use the same <T> syntax and monomorphization
strategy: each instantiation (e.g. TMyVal<Integer>) creates a concrete
TRecordTypeDesc with substituted field types and re-analysed method bodies.

Unlike generic classes, generic records do not emit typeinfo, vtable, or
field-cleanup data — they are value types with no class metadata.

Parser, semantic, QBE codegen, and native codegen all updated.  15 new
unit tests (parser + semantic + codegen) and 4 E2E tests.  Fixpoint OK.
2026-06-05 18:57:21 +01:00
Graeme Geldenhuys d69fcacaca feat(rtl): WriteLn prints True/False for Boolean values
Add _SysWriteBool to the runtime platform layer so WriteLn(Boolean)
prints 'True' or 'False' instead of '1' or '0', matching Delphi/FPC
behaviour. Both QBE and native x86_64 backends emit the new call for
tyBoolean arguments. Updated ~65 assertions across 13 test files.
2026-06-05 18:08:40 +01:00