Commit graph

595 commits

Author SHA1 Message Date
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 959dcaf54b chore: gitignore docs/opdf-emission-design.adoc (local FYI note) 2026-06-09 17:33:44 +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 Haines 565fbea9bc fix(tools): bif-coverage resolves paths by walking up from CWD
Previously the binary used `../../compiler/src/main/pascal/...` and
`bif-coverage.status` directly, locking it to invocation from
tools/bif-coverage/.  Now it walks up from CWD looking for a directory
that contains both `compiler/src/main/pascal/uAST.pas` and
`project.xml`, then resolves every other path under that root.  Lets
the verifier be invoked from the project root, from any subdir, or
from a test runner's CWD (compiler/) without setup.
2026-06-08 00:27:24 +01:00
Andrew Haines 56c3fe1d0b feat(tools): add bif-coverage verifier for AST/encoder/decoder drift
Static-analysis tool that cross-checks uAST.pas against
uUnitInterfaceIO.pas and the root project.xml. For every TASTStmt /
TASTExpr subclass it confirms the class has a dispatch case in
EncodeStmt/EncodeExpr and ReadStmt/ReadExpr, then walks the public
fields and ensures each is either referenced from both encoder and
decoder (`serialise`) or explicitly excluded (`safe`).

Truth is checked-in: bif-coverage.status is a flat file with one
`<TClass>.<Field>  <serialise|safe>` line per field. The default
invocation diffs the live sources against the status file and reports:

  [version]   COMPILER_ID does not match root project <version>
  [encoder]   missing (Class.Field, uAST.pas line)
  [decoder]   missing (Class.Field, uAST.pas line)
  [new]       field exists in AST but is not in the status file
  [stale]     status names a field or class the AST no longer has
  [broken]    serialise field missing from encoder or decoder
  [drift]     safe field has crept into encoder/decoder (with the
              offending uUnitInterfaceIO.pas line)

`bif-coverage --reset` regenerates the status file from current state,
inferring `serialise` when the encoder references the field and `safe`
otherwise. Use after deliberate AST or .bif format changes to
re-baseline.
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 fec6fa9ddd docs: update language rationale and grammar for codepoint iteration
Document the for-in string iteration dual-mode semantics: Byte loop
variable iterates raw UTF-8 bytes, Integer iterates codepoints via
_Utf8DecodeAt.  Remove "deferred to future Runes(S) iterator" notes
since CodePointAt and for-in Integer are now implemented.
2026-06-07 23:58:06 +01:00
Graeme Geldenhuys c26e198180 perf(rtl): SIMD-accelerated UTF-8 codepoint counting (SSE2 + AVX2)
CodePointLength now delegates to _Utf8CountCodePoints in hand-written
x86_64 assembly.  Processes 32 bytes/iteration with AVX2 (runtime-
detected via CPUID) and 16 bytes/iteration with SSE2, falling back to
scalar for tail bytes.  The algorithm counts non-continuation bytes
(bytes where (b & 0xC0) != 0x80) using signed pcmpgtb against -65.
2026-06-07 23:56:12 +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 8c615040b7 feat(stdlib): add UTF-8 codepoint functions to StrUtils
Seven new functions for Unicode-aware string operations:
- CodePointSize: byte width of a UTF-8 sequence from its lead byte
- CodePointLength: count codepoints in a string (O(n) scan)
- CodePointCopy: extract substring by codepoint index and count
- CodePointAt: codepoint value at a codepoint position
- CodePointPos: find substring, return codepoint index
- CodePointByteIndex: convert codepoint index to byte index
- CodePointFromByteIndex: decode codepoint at a byte position (O(1))

All functions use PChar arithmetic for zero-allocation performance.
2026-06-07 23:33:17 +01:00
Graeme Geldenhuys d06cd40a0c feat(rtl): add _Utf8DecodeAt helper for UTF-8 codepoint decoding
Decodes one UTF-8 codepoint at a given byte index in a string. Returns
both the codepoint value and byte count packed into a single Int64:
(ByteCount shl 32) or CodePoint. This will be used by the for-in
codepoint iteration codegen and the StrUtils codepoint functions.
2026-06-07 23:31:01 +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 64060a542d fix(rtl): resolve overload ambiguity in test_blaise_mem
PtrUInt maps to UInt64 which has no matching AssertEquals overload,
causing an ambiguous-overload error. Use Pointer cast instead since
the test is comparing pointer identity — the Pointer overload is the
correct match.
2026-06-07 12:26:34 +01:00
Graeme Geldenhuys fcd654b58c fix(rtl): add mandatory () to bare calls in punit test framework
The punit test framework had bare zero-arg function calls
(GetTestError, CheckInactive) that are now caught by the semantic
diagnostic. Added () to all call sites.
2026-06-07 12:16:12 +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 c822075164 docs: update test count to 2627 for v0.10.0 2026-06-07 02:09:35 +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 0a3a42f982 fix(kanban): add missing () on SystemOffset call in kanban.data.pas 2026-06-06 21:30:07 +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