Commit graph

859 commits

Author SHA1 Message Date
Graeme Geldenhuys 1e3f3ccc5d fix(capture): nested-proc capture of outer var-param records and arrays
A nested procedure that captured a `var` parameter (record or static
array) of its enclosing proc emitted the parameter as a global symbol
reference instead of reaching it through the closure pointer, so the
program either failed to link or read/wrote the wrong storage.

Three layers:

* Semantic (uSemantic): CollectCaptures now takes the enclosing
  TMethodDecl and seeds the outer-name set from its PARAMETERS as well
  as its locals, so a captured var-param is detected.  Capture detection
  also descends through TFieldAssignment, TStaticSubscriptAssign,
  TFieldAccessExpr and TStringSubscriptExpr, so `R.F := ...`, `A[i] := ...`
  and their reads contribute the receiver/array name.  The repeated
  "add if outer and not already captured" idiom is hoisted to
  MaybeCaptureName.

* QBE backend: VarRef routes a captured name through `%_cap_<Name>`.
  The aggregate-ident read distinguishes a captured plain local (the
  `_cap_` slot IS the aggregate address) from a captured var/out param
  (the slot holds the caller's pointer, needing one extra load).

* Native backend: base resolution is unified through EmitVarBaseToReg,
  which checks IsCaptured first.  The static-array element paths and the
  aggregate-ident path now treat a captured var-param array like the
  record field paths: AWantAddress=False for a var-param capture (one
  deref to the data pointer), True for a plain-local capture.

E2E regression tests (run on both backends): capture of a var record
param, a plain local record, and a var static-array param.
2026-06-27 20:56:11 +01:00
Graeme Geldenhuys b5726091be feat(units): support classes declared in a unit implementation section
A class (or record-with-methods) declared in a unit's `implementation`
section is now fully supported: its methods are accepted, its bodies are
type-checked, and its type metadata (typeinfo, vtable, _FieldCleanup) is
emitted on both backends so the unit links and runs.

Three layered problems, fixed together:

1. Semantic ordering + body analysis (uSemantic.AnalyseUnitForExport):
   impl-section TYPE decls are registered before LinkClassMethodImpls (so an
   impl-section class has its methods in FMethodGroups), and ImplBlock method
   bodies are now analysed alongside IntfBlock ones (otherwise field/param
   references carried no ResolvedType and codegen aborted).

2. Codegen emission: native EmitUnit and QBE AppendUnit walked only
   IntfBlock.TypeDecls.  They now emit ImplBlock classes too — method bodies,
   class section (typeinfo/vtable/_FieldCleanup) and interface defs — under the
   owning unit's mangling prefix.  Each backend establishes the emitted unit as
   the symbol-table viewing context so the impl-private class resolves during
   emission and the reference/definition symbol names agree.

3. Cross-unit leak (the blocker): impl-section symbols were registered in the
   shared global scope and leaked into unrelated units via TSymbolTable.Lookup's
   flat-table fallback — a class in unit A's implementation section resolved
   inside an unrelated unit B that never `uses` A (and self-built the compiler
   into a SIGSEGV).  Impl-section symbols are now tagged IsImplPrivate and
   Lookup suppresses them whenever the viewing unit is not the owner.  Interface
   symbols and transitive interface-uses visibility are unchanged.

This also closes the corresponding latent leak: an implementation-section
declaration of any unit no longer resolves in units that do not use it.

Tests: cp.test.e2e.sepcompile gains TestNativeImplSectionClass_Compiles,
TestQBEImplSectionClass_Compiles, and TestImplSectionClass_DoesNotLeakCrossUnit.
Rationale recorded in docs/language-rationale.adoc.

All four fixpoints (QBE, native, internal-asm, warm-cache) green; full suite
3859 tests on both the QBE-built and native-built test runner.
2026-06-27 17:40:08 +01:00
Graeme Geldenhuys e7d236ddac fix(codegen-native): ARC the managed fields of a var-to-var record copy
A record-to-record assignment whose type has managed fields (string,
class, interface, dyn-array, or a nested record thereof) lowered to a bare
memcpy in the catch-all record-copy branch — no _StringAddRef on the
source and no _StringRelease on the destination's prior contents.  The two
records then shared one buffer at refcount 1, so mutating or churning
either side dropped the shared buffer to 0 and freed it under the other
(use-after-free, then `_StringRelease double-free (refcount < -1)` abort
once the heap slot was reused).

  var a, b: TPerson;          { TPerson = record Name: string; ... }
  a.Name := 'Alice';
  b := a;                     { b.Name shared a.Name's buffer, rc still 1 }
  a.Name := 'Bob';            { freed the buffer b still points at }
  WriteLn(b.Name)             { use-after-free }

Fix: in the record/static-array copy branch, when the record type is not
managed-clean, release the destination's managed fields and — unless the
source already transferred ownership (+1, guarded by NativeExprOwnsRef, as
the string/dyn-array assignment paths do) — retain the source's managed
fields, then memcpy.  Mirrors the existing record-field-store and
dyn-array-element-store copy paths.  QBE was already correct; native-only.

Test: cp.test.e2e.records TestRun_Record_VarToVarCopy_StringField_ARC —
copies a record with a string field, churns the source through 200 heap
reallocations, and asserts the copy still reads its own 'Alice-42' rather
than freed/reused memory.  Aborts before the fix, passes after.
2026-06-27 14:32:59 +01:00
Graeme Geldenhuys 1c250fb13e refactor(codegen): hoist symbol mangler to blaise.codegen (F5)
QBEMangle (TCodeGenQBE method) and NativeMangle (native free function)
applied the same metacharacter mapping: '<' ',' ' ' -> '_', drop '>', and
'$' '@' '^' -> '_D_' '_V_' '_P_'. Two copies of the symbol-naming rule is
a link-time-breakage risk if one side gains a case the other lacks.

Hoist a single CodegenMangle into blaise.codegen (distinct name, per the
F4 method-binding gotcha); both backends delegate. The shared version
keeps QBEMangle's clean-name fast-path (no per-character concat when no
metacharacter is present) and reads bytes via StrAt — so NativeMangle now
also uses the stage-stable StrAt instead of the Ord(S[i]) idiom the house
rules warn against.

Cross-backend dedup, no behaviour change: native .s AND QBE IR both
byte-identical on a generics + overload program (TList<Integer>,
TList<string>, Show$i/Show$S → TList_Integer_*, Show_D_i/Show_D_S). All
four fixpoints — which mangle the entire compiler's generic/overload
symbol set — and the full suite on both test runners are unchanged.
2026-06-27 12:05:27 +01:00
Graeme Geldenhuys 1e0b7fc46a refactor(codegen): hoist ARC ownership predicate to blaise.codegen (F2)
ExprOwnsRef (QBE backend) and NativeExprOwnsRef (native backend) were
byte-identical twins: a pure walk over the AST node + its resolved type
deciding whether an r-value leaves an ARC-managed value at refcount +1
that the consuming site must not AddRef again. Two drift-prone copies of
an ARC decision is exactly the failure mode that leaks/UAFs when one side
is updated and the other is not.

Hoist the single source of truth into blaise.codegen as ArcExprOwnsRef
(distinct name so an unqualified call inside a method cannot bind to a
same-named method — the F4 gotcha). Both backends keep their existing
free function as a one-line delegator, so every call site is unchanged.

Cross-backend dedup, no behaviour change: native .s AND QBE IR both
byte-identical on an ARC-heavy program (function-returned class, string
property read via getter, method call returning a class, dynamic-array
return). All four fixpoints and the full suite (QBE-built and native-built
test runners) unchanged.
2026-06-27 11:57:28 +01:00
Graeme Geldenhuys aa52ab6bef refactor(codegen-native): extract BuildArgSlotClasses (item 10)
The per-argument SysV slot classification — open-array and by-value
interface occupy two integer slots, a by-value Double/Single occupies one
xmm slot, everything else one integer slot — was duplicated verbatim in
EmitPopMethodArgsToRegs and EmitMethodOverflowLoad (the <=6-slot pop path
and the >6-slot overflow-load path). Hoist the classification loop into
BuildArgSlotClasses(AParams, AArgs, AList); each caller still prepends its
own Self slot (overflow-load adds slot 0, the pop path handles Self via
AIntBase) and consumes the list as before.

Pure dedup: byte-identical native .s verified on a method-call program
exercising float args, integer overflow (>6 slots), open-array, and
interface params. All four fixpoints and the full suite (QBE-built and
native-built test runners) unchanged.
2026-06-27 11:51:08 +01:00
Graeme Geldenhuys 5232ea7bc0 refactor(codegen-native): extract EmitVarAddr local/global address-of helper
The four-line "load the address of a named local-or-global into a
register" selector

    if Self.IsLocal(Name) then
      Self.Emit('leaq <VarOperand>, <reg>')
    else
      Self.EmitLeaqGlobal(Name, <reg>)

recurred verbatim at 20 sites across the field-access ladders, the
record-method receiver ladders (the Phase-2 item-12 target), the static-
array base loads, and the @-address-of paths. Collapse them into a single
EmitVarAddr(AName, ADstReg) helper.

Pure dedup, no behaviour change: EmitVarAddr emits exactly the same two
instruction forms the inlined selector did (and EmitLeaqGlobal already
routes threadvars through TLS, per the preceding fix). Verified byte-
identical native .s across threadvar and non-threadvar record/array/
method-call programs; all four fixpoints and the full suite (both QBE-
built and native-built test runners) unchanged.
2026-06-27 11:06:42 +01:00
Graeme Geldenhuys a86fb84eea fix(codegen-native): address record/aggregate threadvars via TLS
A threadvar of record (or any aggregate whose base address is taken via
leaq) was addressed with a bare `leaq Name(%rip)` for both direct field
access and method-call receivers, with no %fs:0/@tpoff. The symbol was
correctly placed in .tbss, but a %rip-relative reference to a TLS symbol
does not resolve to the per-thread slot, so all threads shared one static
slot and clobbered each other. Scalar threadvars were already correct
(VarOperand emits %fs:Name@tpoff); QBE was unaffected.

Route every global record/array base load through the TLS-aware
EmitLeaqGlobal helper instead of bare `leaq Name(%rip)`:
  - 16 field-access base ladders (FAE/FA/AFA.RecordName)
  - 4 record-method receiver sites (the >6-slot and <=6-slot ladders)
  - the offset-0 scalar-field-read fast path, which read the field
    directly off Name(%rip); now branches on IsThreadVarGlobal and falls
    through to EmitLeaqGlobal + indirect load for threadvars.

EmitLeaqGlobal emits the identical bare leaq for non-threadvar globals,
so codegen for ordinary globals is byte-identical (verified by .s diff)
and all four fixpoints are unchanged.

Tests: cp.test.e2e.threading gains
TestRun_ThreadVar_RecordField_PerThreadIsolation and
TestRun_ThreadVar_RecordMethod_PerThreadIsolation — 3 threads each stamp
a distinct id into a record threadvar (via field access / via method
call), spin re-reading, and assert no cross-thread clobber. Both fail
before the fix (native prints CORRUPT) and pass after.
2026-06-27 11:00:26 +01:00
Graeme Geldenhuys 180fbb62b4 refactor(codegen-native): extract EmitArgsToSlots (>6-slot call marshalling)
Phase 2 item 11 of the de-duplication plan — the largest single dedup.  The
">6-slot store-into-stack-slots" loop (write each logical argument to its
(I+1)*8(%rsp) slot, dispatching per kind: reload a hoisted akRecCall buffer
from its hoist depth / store a var-param address / store a width-adjusted xmm
bit pattern for a by-value float / store %rax for a plain scalar) was copy-
pasted verbatim at 6 call sites — the regular, metaclass-ctor, implicit-Self,
and proc-field >6-slot method paths.  Collapse into EmitArgsToSlots(AArgs,
AParams, AAllocSz, AHoistTotal, AHoistDepths, AHoistKinds); 7 call sites now
share it.

Risk gates preserved as the helper contract: all 6 sites used the same Self-at-
slot-0 base offset (Dest = (I+1)*8) and the same push-phase-free akRecCall
reload formula (AAllocSz + AHoistTotal - depth), so they parameterize cleanly —
no phase/index divergence to thread.

Behaviour-reconciling, but verified behaviour-preserving: native .s for a
>6-arg program (Self+8 int args, and a 7-slot mixed int/double/var-param call)
is byte-identical to pre-refactor HEAD; the native self-host fixpoint (which
compiles the whole compiler, exercising every >6-slot path including metaclass
and proc-field) is unchanged.  All four fixpoints pass; full suite OK (3853
tests) on both QBE-built and native-built test runners.
2026-06-27 10:23:41 +01:00
Graeme Geldenhuys 8e52f7769f refactor(codegen-native): extract EmitSretBufferSlideDown
Phase 1 item 7 of the de-duplication plan.  The post-call sret return-buffer
slide-down (`if HTotal > 0 then` reclaim the arg-hoist region and re-push the
fat pointer at (%rsp)) was byte-identical at the tail of all three
interface-sret return paths: EmitIntfSretCall, EmitIntfSretMethodCall, and
EmitClassIntfSretMethodCall.  Collapse into EmitSretBufferSlideDown(AHoistTotal).

Proven byte-identical: native .s for an interface-returning program (direct and
chained `MakeGreeter('Hi ').Greet('there')`, exercising the sret-return-with-
hoisted-arg slide) is identical to pre-refactor HEAD.  All four self-host
fixpoints pass; full suite OK (3853 tests) on both QBE-built and native-built
test runners.
2026-06-27 09:13:57 +01:00
Graeme Geldenhuys fdcce09254 refactor(codegen-native): extract EmitStaticElemScale for static-array indexing
Phase 1 item 5 of the de-duplication plan.  The read-path static-array
subscript scaling — `if SAT.LowBound <> 0 then subq $lowbound; imulq $elemsize`
applied to an index already in %rax — was spelled out at 7 sites, each casting
a different expression to TStaticArrayTypeDesc.  Collapse the body into
EmitStaticElemScale(ASAT) and pass the resolved type.

Only the read sites are touched: the static-array STORE paths compute the
element size from a precomputed DAElemType local rather than
ASAT.ElementType.RawSize(), so they are deliberately left as-is (the imulq
operand differs).

Proven byte-identical: native .s for a static-array program (non-zero low
bounds, integer + double + record-nested arrays, read and write) is identical
to pre-refactor HEAD.  All four self-host fixpoints pass; full suite OK (3853
tests) on both QBE-built and native-built test runners.
2026-06-27 09:09:07 +01:00
Graeme Geldenhuys 8a32c3ab57 refactor(codegen-native): extract EmitStmtList + try-frame EH helpers
Phase 1 (byte-identical) de-duplication of the native x86-64 backend, items
1-3 of Andrew's plan:

- EmitStmtList(AList): the compound-body inline idiom
  `for I := 0 to L.Count-1 do EmitStmt(TASTStmt(L.Items[I]))` was copy-pasted
  at 14 sites (try/finally/except bodies, loop bodies, program block, unit
  init).  Now one helper; nil-safe.  The element cast stays internal — the AST
  stores statements in TObjectList (raw-pointer slots), so the cast is
  inherent, just no longer repeated 14×.

- EmitTryFramePrologue(AFinallyBody, ALblExc, ALblTry): the setjmp try-frame
  entry (slot alloc, _PushExcFrame, FExcDepth bump, FFinallyStack push,
  _blaise_setjmp, testl/jnz/jmp) was byte-identical between EmitTryFinallyStmt
  and EmitTryExceptStmt, varying only in the FFinallyStack payload (FinallyBody
  vs nil) and the jnz target — now both parameters.

- EmitPopExcFrame: the frame-pop bookkeeping triplet (_PopExcFrame +
  Dec(FExcDepth) + FFinallyStack.Delete) appeared at 5 sites; the Dec and
  Delete must always travel with the pop, so they are emitted together.  The
  EmitExcUnwind loop deliberately keeps its bare _PopExcFrame (it must not
  touch the codegen-time depth bookkeeping).

Proven byte-identical: native .s for an EH-heavy program (try/finally,
try/except with handlers + else, nested try, repeat/continue, compound) is
identical to pre-refactor HEAD.  All four self-host fixpoints pass; full suite
OK (3853 tests) on both QBE-built and native-built test runners.
2026-06-27 08:49:14 +01:00
Graeme Geldenhuys c8faf1ed24 refactor(codegen): hoist record-return ABI classifier into shared blaise.codegen
The SysV/Win64 record-return classifier — ClassifyRecordReturn plus its five
leaf predicates (IsRecordManagedClean, IsRecordAllIntegerLeaves,
IsRecordAllFloatLeaves, IsRecordAllIntOrFloatLeaves, EightbyteIsSSE) — existed
as byte-identical twins in TCodeGenQBE and TNativeBackend (~145 lines each).
This is a drift-prone ABI decision table that both backends must keep in
lockstep; a divergence would silently misclassify a record return on one
backend only.

Move the logic to free functions in blaise.codegen (Recret* — renamed to avoid
colliding with the backend methods, since Blaise resolves an unqualified call
inside a method to the method, not the unit function).  Only the top-level
classifier consults the target OS (the Win64 aggregate rule), so it takes a
TTargetDesc parameter; the leaf predicates are target-independent pure type
walks.  Each backend keeps its method as a one-line delegator, so every
existing Self.X call site is unchanged.

Validated behaviour-preserving: native .s AND QBE IR are byte-identical
pre/post for a record-return program exercising every return class
(rcInt1/rcInt2/rcSSE1/rcSSE2/rcIntSSE/rcSSEInt/rcSret/rcWin64-clean).  All four
self-host fixpoints (QBE, native, internal-assembler, warm-cache) pass; full
suite OK (3853 tests) on both QBE-built and native-built test runners.

Implements item F4 of Andrew's native-codegen de-duplication plan — the
highest-value, lowest-risk cross-backend hoist.
2026-06-27 02:29:19 +01:00
Graeme Geldenhuys 6b99e4db79 fix(codegen-native): pin chained field value before releasing transient base
A deep field chain feeding a method/property call — MakeIt().A.B.Method() —
crashed the native backend.  The chained field read released the transient
function result immediately after the first field hop, but _ClassRelease at
refcount 0 runs _FieldCleanup, which frees the intermediate objects (A, then B)
that the rest of the chain still dereferences.  The terminal call then read
freed memory: a use-after-free that segfaulted (or, when the freed slot was not
yet reused, returned the stale value — non-deterministic).

This was the root cause of the TProcTypesOfObjectTests crash that killed the
TestRunner process and blocked ~975 alphabetically-later tests in the full
sequential suite (all of which pass individually).  QBE never released the
transient there, so it leaked instead of crashing — the bug was native-only and
only reproduced under a native-built test runner.

Fix: when the loaded field value is a class/interface reference borrowed from
the transient base, AddRef-pin it before the release so the cleanup's matching
release nets out and the object survives the chain.  This intentionally leaks +1
on the result, matching the QBE deep-chain transient leak already documented in
bugs.txt; correctness over a crash until the chain gains a deferred-release
mechanism.

Adds TE2EArcTests.TestRun_TransientDeepChainMethodCall_NoUseAfterFree and a
RunUnderValgrindNative base helper (the QBE-only RunUnderValgrind cannot see a
native UAF; the stdout value check alone is unreliable because the read is
non-deterministic, so native valgrind is the real guard).
2026-06-27 02:13:28 +01:00
Graeme Geldenhuys e08521a1f9 fix(codegen): release transient receiver after field-access load (both backends)
A function/getter result used as a field-access receiver was never
released, leaking +1 refcount per access on both QBE and native.
MakeCreature().HitPoints left the TCreature alive; TList<T>[I].Field
leaked via the Get getter.

Both backends now emit _ClassRelease for the transient base after the
field value is loaded. ExprOwnsRef/NativeExprOwnsRef extended to
recognise indexed-property subscripts (TStringSubscriptExpr wrapping a
ReadMethod-backed TFieldAccessExpr) as owning +1.

Dual-backend E2E regression test added
(TestDebug_ReceiverFieldAccess_NoLeak).

All 4 fixpoints verified (FIXPOINT_OK, NATIVE_FIXPOINT_OK,
NATIVE_INTERNAL_OK, WARMCACHE_FIXPOINT_OK).
2026-06-27 01:21:30 +01:00
Graeme Geldenhuys 7abf5b6255 fix(codegen-native): elide class-assignment AddRef when RHS owns +1
A function or method returning a class leaves its Result at refcount +1: the
callee's `Result := x` AddRef'd and the epilogue did not release Result. The
caller's assignment therefore consumes that transferred reference and must NOT
AddRef again. The native backend's plain class-variable assignment emitted
_ClassAddRef unconditionally, so `C := MakeIt()` (any object-returning call)
leaked exactly one reference per call.

Guard the AddRef with NativeExprOwnsRef, mirroring the tyString/tyDynArray
branches directly above it and the QBE backend's ExprOwnsRef elision (the QBE
path has always been leak-free here). Eliding is safe even for a borrowed return
(`Result := G`): the callee's Result assignment still AddRef'd, so the
transient genuinely owns +1 — verified no use-after-free when the source object
outlives the assignment.

Found via a TList<TCreature> + `L[I].HitPoints := ...` program reporting leaks
under --debug; reduced to a non-generic `C := MakeIt(); C := nil` minimal case.

NOTE: this fixes the assignment-site half. Object-returning call results used as
a field/method *receiver* or as a subscript getter result (e.g. L[I].Field) are
a separate, deeper gap — the native backend lacks the QBE backend's
statement-level deferred-release (PendingRelease/Flush) — addressed next.

cp.test.e2e.leakcheck.TestDebug_FuncReturnAssign_NoLeak pins both backends.
Full suite 3850 green; native fixpoint reproduces (and shrank the compiler's own
emitted asm by ~1400 lines of redundant retains).
2026-06-27 00:21:12 +01:00
Graeme Geldenhuys 948cd4ed1b feat(rtl): FreeBSD freestanding _start + minimal syscall leaf (Step 3)
Add the FreeBSD entry stub for a static, libc-free ET_EXEC:

- runtime.start.static.freebsd.pas — the FreeBSD sibling of
  runtime.start.static.linux. _start captures %rsp, aligns the stack, and calls
  the Pascal _BlaiseStartC, which parses argc/argv/envp off the kernel's initial
  stack (same layout as Linux), captures environ, calls main(argc, argv), and
  exits via the FreeBSD exit syscall. Deliberately minimal: no TLS/auxv-walk
  (a trivial program does no threadvar access) — that lands with the threads
  work in Step 4, mirroring how the Linux static start gained TLS.

- runtime.syscall.freebsd.pas — the minimum kernel leaf _start needs: _exit
  (SYS_exit = 1), write (SYS_write = 4, aliased from _sys_write), and the
  environ global. The full file/process/thread leaf grows here in Step 4. ABI
  notes record the FreeBSD differences from Linux: different syscall numbers and
  the carry-flag error convention (only relevant for the error-translating
  wrappers that arrive in Step 4).

Both units are standalone — nothing in the default Linux build graph uses them,
so the Linux RTL and self-hosting are unaffected; the FreeBSD RTL composition
links them at Step 5.

TLinkerE2ETests.TestLink_FreeBSDStart_StaticExecShape links the _start fixture
with the FreeBSD target and asserts the Strategy-B shape: e_type = ET_EXEC,
entry == _start, no PT_INTERP. (Uses LinkToBytes, not ReadFile — the latter
truncates at the ELF header's first NUL byte.)

Step 3 of docs/freebsd-x86_64-backend-design.adoc. Full suite 3850 green;
FIXPOINT_OK.
2026-06-26 23:58:08 +01:00
Graeme Geldenhuys a7ded4c228 feat(rtl): FreeBSD TPlatformLayout adapter (struct stat + OS constants)
Add rtl.platform.layout.freebsd with TPlatformLayoutFreeBSDX86_64, the
FreeBSD sibling of the Linux layout adapter. It supplies the FreeBSD 14.x
amd64 struct stat field offsets (st_mode@24, st_mtim.tv_sec@64, st_size@112,
sizeof=224 — all differing from Linux) and the O_CREAT/O_TRUNC/O_APPEND flag
bits (0x200/0x400/0x008, also differing); the remaining constants (S_*,
SEEK_*, CLOCK_REALTIME, WNOHANG) share Linux's values. Layout pinned to
FreeBSD 14.x and valid for 13.x (ino_t/dev_t widening landed in 12, stable
since).

The unit is a standalone sibling: nothing in the default (Linux) build graph
uses it, so the Linux RTL and self-hosting are unaffected. The FreeBSD RTL
archive composes it in place of rtl.platform.layout.linux (Step 5).

cp.test.platformlayout.freebsd adds 11 tests that instantiate the adapter
directly and assert each constant plus the three struct-stat accessors, the
latter by planting sentinels at the FreeBSD offsets in a 224-byte buffer and
reading them back — catching an offset typo on the Linux CI host without
needing FreeBSD emulation.

Step 2 of docs/freebsd-x86_64-backend-design.adoc. Full suite 3849 tests
green; FIXPOINT_OK.
2026-06-26 23:12:44 +01:00
Graeme Geldenhuys 75628f3e20 feat(rtl): complete --static self-host support (chmod, strtod-free, TLS align)
Closes the gaps that blocked a --static (libc-free) build of the compiler itself:

- runtime.syscall.linux: add the chmod(2) syscall stub (SYS_chmod=90) defining
  the bare 'chmod' the internal linker's MakeFileExecutable imports.
- uSemantic: route RawStrToDouble through the RTL's pure-Pascal _StrToDouble
  (runtime.float) instead of libc's strtod, dropping the last libc dependency in
  the compiler's float-literal constant folding.  Mirrors the existing
  _DoubleToStr routing and is correct for both static and dynamic builds.
- blaise.linker.elf: align FTlsSize to FTlsAlign in both the static and dynamic
  TLS layout so TPOFF32 (sym - FTlsAddr - FTlsSize) matches the thread pointer
  the freestanding _start places at AlignUp(memsz, align).  Previously these
  diverged when the raw .tdata+.tbss sum was not a multiple of the TLS alignment,
  shifting every threadvar read by the padding (latent: today's 104-byte/8-align
  TLS happens to coincide).

With these, `blaise --static` can link the compiler itself; the resulting
binary is a fully static, libc-free ET_EXEC.
2026-06-26 19:15:25 +01:00
Graeme Geldenhuys a20a82c417 fix(asm): brace-comment strip must skip quoted strings
The asm brace-comment strip (added with the threads leaf to remove {...}
comments the lexer leaves inside asm blocks) ran over the WHOLE line, including
inside .ascii string literals.  The native backend emits string-const data as
.ascii directives, and the compiler's own codegen contains template strings with
literal braces (e.g. codegen.qbe.pas's 'data $__s%d = { w -1, w %d, w %d, b
"%s", b 0 }').  When the compiler was assembled by the internal assembler, the
strip ate the '{ ... }' span out of those .ascii literals, corrupting the
template strings.

The damage only showed when the compiler ran --emit-ir / GenerateIR: the
corrupted template produced garbage data lines (data $__s0 = <garbage>), so the
~1240 IR-assertion tests failed (TestHelloWorld_HasStrLitData etc.).  Compiled
user programs were unaffected (their own Format/strings come from freshly built
RTL), and the QBE-built compiler was unaffected (gcc assembles its .ascii), which
is why local QBE fixpoints stayed green while the CI native-internal TestRunner
went red.

Fix: skip over quoted strings in the brace strip, exactly as the adjacent
'#'-comment strip already does, so a brace inside a .ascii literal is left
intact.

Root-caused from the CI failure on ci/linux-direct-syscalls; the corruption is
visible as -clean but -garbled string data from a
native-internal-assembled compiler.
2026-06-26 18:30:53 +01:00
Graeme Geldenhuys e218b6978c fix(rtl,asm): address pre-landing review findings on the static leaf
Pre-landing review fixes, all in the direct-syscall / threads-leaf series:

- runtime.thread.static.linux + runtime.start.static.linux: reclaim the
  per-thread TLS block when clone(2) fails after BuildThreadTLS (previously only
  the stack mapping was unmapped, leaking one TLS mapping per failed
  pthread_create).  Add a symmetric FreeThreadTLS that recovers the mmap base
  from the thread pointer using the same alignment math BuildThreadTLS used.
- runtime.libc2.linux: system() now forwards `environ` to execve instead of
  nil, so the spawned /bin/sh sees PATH/HOME/etc. (was running with an empty
  environment under --static).
- runtime.libc2.linux: mkstemp loops getrandom until all 6 bytes are filled
  rather than ignoring a short read that would weaken the random suffix.
- runtime.libc.linux: drop execvp's dead HasSlash branch (both arms called the
  identical execve); keep the single call + the no-PATH-search note.
- runtime.thread.static.linux: correct the stale _clone_thread comment that
  claimed args pass through callee-saved regs (they are seeded on the child
  stack and popped).
- blaise.assembler.x86_64: in the asm brace-comment strip, splice without
  trimming mid-scan (a left-trim shifted the string under the scan index) and
  trim once at the end, so a second brace comment on the same line is still
  found.
- blaise.codegen.native.driver: replace a non-ASCII ellipsis in an added
  comment with '...' (ASCII-only source rule).

Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; self-hosted
compiler builds clean (0 spurious lock prefixes); static threads binary runs
counter=80000 PASS (deterministic across runs); no unit/codegen/assembler/linker
test regressions.
2026-06-26 18:02:51 +01:00
Graeme Geldenhuys b03f84c4c3 feat(rtl): clone/futex threads leaf for --static (Linux)
Replaces the single-threaded no-op pthread_mutex stubs with a real, libc-free
threading leaf built directly on clone(2) and futex(2), so a static binary can
spawn worker threads — the last RTL piece needed for a libc-free static program
to do real multithreaded work.

runtime.thread.static.linux:
- pthread_create via raw clone(2): each thread gets its own mmap'd stack and a
  fresh per-thread TLS block (CLONE_SETTLS) so threadvar access (exception
  frames, the allocator) is per-thread.  A small asm trampoline seeds the start
  routine + arg onto the child stack, calls it, then exit(2) (thread-only).
- pthread_join via the CLONE_CHILD_CLEARTID futex word: the kernel clears it on
  thread exit and futex-wakes; join futex-waits on it, then munmaps the stack.
- 3-state futex mutex (Drepper): the caller's pthread_mutex_t buffer's first
  Integer is the futex word (0=unlocked, 1=locked, 2=contended); fast-path CAS,
  slow-path FUTEX_WAIT/FUTEX_WAKE.  sysconf is left to runtime.libc.linux.

runtime.syscall.linux: add the futex(2) syscall (SYS_futex, 6-arg, %rcx->%r10).

runtime.start.static.linux: capture the PT_TLS template at startup and expose
BuildThreadTLS so pthread_create can build each thread's TLS block the same way
_start builds the main thread's (variant II: TLS data then TCB self-pointer).

blaise.assembler.x86_64: add the xchg/cmpxchg encoders (xchgl/xchgq/cmpxchgl/
cmpxchgq, reg<->reg and reg<->mem) the futex mutex needs; strip brace comments
inside asm blocks in ParseLine (whole-line and trailing); and initialise
TParsedLine.HasLock to False so the lock prefix never carries between lines.

Verified: thrtest (8 worker threads x 10000 increments under the futex mutex)
prints counter=80000 PASS; QBE self-host FIXPOINT_OK.

The --static link path is currently blocked by a pre-existing self-host codegen
bug (spurious lock prefixes in large binaries built by a stage-2 compiler),
which is independent of this change and tracked separately.
2026-06-26 15:10:15 +01:00
Graeme Geldenhuys 3e4619a507 feat(rtl,linker): static TLS + mutex stubs — a libc-free static binary now RUNS
Completes the runnable-static-binary milestone: `blaise --static` now produces a
fully static, libc-free ELF (statically linked, no NEEDED libc.so.6, no
PT_INTERP) that EXECUTES correctly — `hello` prints, arithmetic/WriteLn work.

Static TLS (the last blocker — threadvar access via %fs faulted because %fs was
unset and static mode emitted no PT_TLS):
- linker LayoutSections now lays out .tdata/.tbss as a TLS block and sets
  FTlsAddr/FTlsSize/FTlsFileSize/FTlsAlign (previously only LayoutDynamic did,
  so static-mode TPOFF relocs resolved against 0).
- EmitExecutable emits a PT_TLS program header when the program has TLS, and
  LayoutSections reserves a 3rd phdr slot so the first section does not overlap
  and overwrite it.
- runtime.start.static.linux: rewritten with a Pascal core (_BlaiseStartC) that
  parses argc/argv/envp + the auxv, reads PT_TLS via AT_PHDR, mmaps + inits the
  TLS block, sets the thread pointer with arch_prctl(ARCH_SET_FS) (variant II:
  TP past the block, TCB self-pointer at %fs:0), then calls main.

runtime.thread.static.linux: no-op single-threaded pthread_mutex_* stubs
(correct for a single-threaded process).  pthread_create is intentionally
unprovided, so a thread-using static program (e.g. `uses classes`) fails to
link — the honest signal that the threads leaf (clone/futex) is the one
remaining piece.

Also fixed: runtime.start.static.linux had its `uses` in the implementation
section (Blaise only honours interface-section uses) which made syscall symbols
invisible; and em-dashes in comments tripped the lexer (known UTF-8 issue).

Full suite green (3829); dynamic native/QBE (incl. TLS + classes) unaffected.
2026-06-26 15:10:15 +01:00
Graeme Geldenhuys 7b6e275053 feat(rtl): single-threaded mutex stubs — static hello now links fully static
runtime.thread.static.linux: no-op pthread_mutex_init/lock/unlock/destroy
(correct for a single-threaded process; real clone/futex threads are deferred).
pthread_create is intentionally absent so a thread-spawning static program fails
to link rather than silently misbehaving.

With this the LAST libc leaf is resolved: a --static hello now links to a fully
static ELF (statically linked, no NEEDED libc.so.6, no PT_INTERP).  It does not
yet RUN — it faults on the first threadvar access because %fs (the thread
pointer) is unset and the linker emits no PT_TLS for static mode.  Static TLS
setup (PT_TLS + arch_prctl(ARCH_SET_FS) in _start + correct TPOFF) is the next
piece.

Full suite green (3829); dynamic native/QBE unaffected.
2026-06-26 15:10:15 +01:00
Graeme Geldenhuys 5c3e6c1527 feat(rtl): Tier 2 freestanding libc leaves for --static (Linux)
The logic-bearing (non-syscall) leaves.  A --static link now resolves
abort/__cxa_atexit/exit/gmtime_r/localtime_r/timegm/mkstemp/system; the only
remaining gap is the pthread_mutex_* stubs (Tier 3).

New runtime.libc2.linux:
- atexit registry: __cxa_atexit records (func,arg) LIFO; the bare `exit`
  (main's epilogue) runs them then exit_group.  `exit`/`_exit` moved out of
  runtime.syscall.linux (which keeps only the raw _exit that runs no handlers).
- abort: kill(getpid, SIGABRT) then exit_group(134).
- gmtime_r/localtime_r/timegm: freestanding civil-calendar math (UTC;
  days_from_civil / civil_from_days).  No timezone DB yet — localtime == UTC.
- mkstemp: getrandom-named O_CREAT|O_EXCL|O_RDWR 0600 file, retry on EEXIST.
- system: fork + execve /bin/sh -c + wait4.

Full suite green (3829); dynamic native/QBE unaffected.
2026-06-26 15:10:15 +01:00
Graeme Geldenhuys a9db1b0b0a feat(rtl): Tier 1 syscall-backed libc leaves for --static (Linux)
Adds the syscall-backed half of the libc replacement.  A --static link now
resolves mremap/getcwd/fork/execvp/waitpid/time/getenv/sysconf; the remaining
gap is the freestanding-logic leaves (abort/__cxa_atexit/gmtime_r/localtime_r/
timegm/mkstemp/system, Tier 2) and the mutex stubs (Tier 3).

- runtime.syscall.linux: + raw mremap/fork/execve/wait4/sched_getaffinity/
  getrandom/arch_prctl/kill/sys_time/sys_getcwd (4th syscall arg %rcx->%r10 for
  the >=4-arg ones).
- runtime.libc.linux (new): the libc-shaped wrappers that need more than a raw
  syscall — getcwd (return buf/nil), getenv (walk environ), time, waitpid
  (wait4 + NULL rusage), execvp (execve; direct path, PATH search deferred to
  when a bare-name exec appears), sysconf(_SC_NPROCESSORS_ONLN) via
  sched_getaffinity + popcount.  Owns the `environ` global.
- runtime.start.static.linux: capture environ = &argv[argc+1] from the initial
  process stack into the global before calling main.
- EnsureRTLObjects: link runtime.libc.linux in --static builds.

Full suite green (3829); dynamic native/QBE unaffected.
2026-06-26 15:10:15 +01:00
Graeme Geldenhuys be0e519084 feat(rtl): direct-syscall kernel-leaf foundation + --static link mode (Linux, WIP)
First increment of the libc-free direct-syscall RTL (Linux as the proving
ground for FreeBSD; docs/linux-syscall-migration.adoc to follow).  The default
build is unchanged — libc-backed dynamic ELF; everything here is behind the new
--static flag.

New kernel-leaf units (selected by EnsureRTLObjects when --static):
- runtime.syscall.linux — raw `syscall` stubs DEFINING the bare POSIX names the
  RTL imports (open/read/write/lseek/close/fstat/stat/mkdir/rmdir/unlink/rename/
  chdir/getpid/dup2/pipe/mmap/munmap/nanosleep/clock_gettime/exit/_exit).
  `write`/`exit` are Pascal-reserved, so their Pascal names are _sys_* and the
  asm body emits a bare label alias.  4th syscall arg moved %rcx->%r10.
- runtime.cstub — freestanding memcpy/memset/memcmp/strlen (no Char type; PChar
  byte indexing).
- runtime.start.static.linux — freestanding _start (argc/argv off %rsp, call
  main, exit_group) replacing the libc __libc_start_main entry.

Wiring: --static -> TBackendOpts.Static; EnsureRTLObjects swaps runtime.start
for runtime.start.static.linux and adds the syscall+cstub leaves; the native
internal linker calls SetDynamic(False) for a non-PIE ET_EXEC.

Internal linker static-mode fixes (these are correct in any ET_EXEC, not just
PIE — they were wrongly gated to dynamic-only):
- R_X86_64_64 against a defined symbol resolves to the symbol's absolute address
  (no dynamic reloc); only PIE/dynamic mode also emits the .rela.dyn RELATIVE.
- R_X86_64_TPOFF32 (Local-Exec TLS) resolves at link time — it IS the static TLS
  model. Updated TestReloc_Quad64_* to assert the now-correct resolution.

A --static link currently reaches symbol resolution and reports the remaining
libc leaves to implement (mremap/getcwd/fork/execvp/waitpid/time/getenv/sysconf/
gmtime_r/localtime_r/timegm/__cxa_atexit/abort/mkstemp/system/pthread_mutex_*) —
the next increments.  Full suite green (3829); dynamic native/QBE unaffected.
2026-06-26 15:10:15 +01:00
Graeme Geldenhuys 93ac17bb62 fix(codegen-qbe): pad FFI aggregate types to match record field offsets
A record passed BY VALUE to a function (e.g. `const AParsed: TParsedLine`)
lowers to a QBE `:_ffi_<Name>` aggregate type whose field list was built by
FlattenRecordToAggLetters inlining each field's type letter back-to-back, with
nested records expanded letter-by-letter. That dropped the per-field and nested-
record tail padding, so QBE laid out the struct SMALLER than Blaise's record and
with shifted field offsets.

For TParsedLine this put the trailing HasLock byte at QBE offset 244 (struct size
248) while the function body reads it at the real Pascal offset 252 (size 256).
A by-value read at +252 then lands 4 bytes PAST the passed struct, in caller
stack garbage. When that garbage was non-zero, the assembler's
`if AParsed.HasLock then CBEmit($F0)` fired on EVERY instruction, prepending a
spurious lock prefix — `lock ret` is illegal, so the produced binary SIGILL'd.

This silently corrupted large native binaries built by a self-hosted compiler
(the assembler unit is where it bit hardest), and it was the real cause of the
"--static binaries are poisoned" and "stage-2 TestRunner SIGILLs" symptoms.
It hid from all four fixpoints: the QBE fixpoint compares IR text (the IR was
always correct — `loadub +252` is right; QBE miscompiled the by-value param
layout), and the native fixpoints pipe through the EXTERNAL assembler.

Fix: FlattenRecordToAggLetters now walks fields by their real TFieldInfo.Offset,
inserting explicit `b` byte padding before each field and after the last one up
to TotalSize, so the QBE aggregate's field offsets and size match the Pascal
record exactly. Nested records / static arrays / sets are emitted as their whole
byte span (the by-value ABI only needs correct size and field positions).

Verified: QBE-built compiler now produces 0 spurious lock prefixes; static
threads binary runs (counter=80000 PASS); compiler/target/blaise self-compiles
clean; FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK.
2026-06-26 15:03:08 +01:00
Graeme Geldenhuys af59423f11 fix(codegen): allocate sret buffer for discarded record-returning method calls
Calling a record-returning method as a statement (result discarded) omitted
the hidden sret pointer required by the SysV x86-64 ABI.  Self was passed
in %rdi where the sret buffer should be, so the callee wrote the returned
record through the object pointer, corrupting its fields.

Semantic pass: set ResolvedReturnTypeDesc on TMethodCallStmt at all three
class-method resolution sites so both backends can detect the record return.

QBE backend: allocate a temporary buffer via alloc8, zero it, route through
EmitRecordReturnCallSite, then release any managed fields (strings, classes,
interfaces, dynamic arrays).  Both the regular and ObjExpr receiver paths.

Native backend: reserve an aligned stack buffer, shift Self from %rdi to
%rsi, place the sret pointer in %rdi, and clean up managed fields after the
call.  Handles the <=6-slot path.
2026-06-26 14:13:10 +01:00
Graeme Geldenhuys 09c2d1a0b9 feat(lexer): #nnnn / #$hhhh Unicode-codepoint string literals
A `#`-prefixed numeric literal now denotes a Unicode codepoint and contributes
its UTF-8 encoding to the surrounding string constant — the coherent semantics
for Blaise's Char-less, UTF-8-native string type:

  #65      -> 'A'              (1 byte  $41)
  #$20AC   -> the euro sign    (3 bytes E2 82 AC)
  #$1F600  -> the grinning face(4 bytes F0 9F 98 80)

The literal is always a string, and adjacent `#` and '...' literals merge into
one compile-time constant (#72#73'!' -> 'HI!'), matching Pascal's literal-
merging tradition.  Decimal (#nnnn) and hexadecimal (#$hhhh) are both accepted;
the underlying tokeniser already captured #$-hex runs, so the change is confined
to UnescapeString + a new CodepointToUtf8.

Codepoints outside 0..U+10FFFF and the surrogate range U+D800..U+DFFF (not valid
Unicode scalar values) are a compile error, so emitted string constants are
always valid UTF-8.

Tests cover decimal ASCII, hex BMP (3-byte), astral (4-byte), and #+string
merging.  All four fixpoints green; full suite 3835.
2026-06-26 10:40:40 +01:00
Graeme Geldenhuys a6cdba7fbe fix(driver): auto-add the RTL source dir to the loader search path
After the RTL-unification move, stdlib units (classes, contnrs) explicitly
`uses runtime.arc` and friends.  The compiler driver already source-builds the
RTL from compiler/src/main/pascal at link time, but the FRONT-END unit loader
was never told where that source lives — so compiling any program that uses a
stdlib unit (or any RTL unit transitively) failed with
"Unit 'runtime.arc' not found in search paths" unless the user manually passed
--unit-path compiler/src/main/pascal.

Add the RTL source directory to the loader's search paths automatically,
resolved the same way EnsureRTLObjects resolves it for linking: --rtl-src, then
$BLAISE_RTL_SRC, then the binary/CWD-relative RTLSourceDir default.  (stdlib
itself stays an explicit --unit-path, as before — it is an opt-in library, not
implicit like the RTL.)

Regression introduced by the RTL-unification land; reported by a user.
Verified: a `uses classes` program now compiles + runs with only the stdlib
unit-path, from any CWD.  All four fixpoints green; full suite 3831.
2026-06-26 09:12:38 +01:00
Andrew Haines 18b215835c fix(codegen-native): spill implicit-Self call args beyond six registers to the stack
An implicit-Self method call — a bare `Method(...)` inside another method that
resolves to a method of the current class — loaded Self plus every argument
into the System V integer argument registers unconditionally. When Self and the
arguments together needed more than the six available integer registers,
codegen aborted with "System V integer arg register index 6 out of range".

The explicit-receiver path (obj.Method() / Self.Method()) already split on slot
count and spilled the overflow to the stack; only the implicit-Self expression
path was missing it. Add the same split: keep the push/pop-into-registers fast
path for <= 6 slots, and for more, evaluate the arguments into a stack slot
block, load the first six slots into registers, and spill the rest per the ABI
(EmitImplicitSelfCallOverflow, mirroring EmitMethodCallExpr's overflow branch).

Tests: an assembly-level test asserts the slot-block spill shape and dispatch,
and an end-to-end test runs a program with an 8-argument non-virtual and a
7-argument virtual implicit-Self call and verifies every argument — including
the spilled ones — arrives intact.
2026-06-26 09:05:50 +01:00
Graeme Geldenhuys 7a95fd0472 rtl-unification: remove dead archive-resolution code; last test off the archive
Partial Stage-3 teardown — the safe removals that do not affect the bootstrap
chain (the runtime Makefile/ar stay until a release ships a source-build-capable
binary, since the v0.12.0-rooted rolling-bootstrap replay needs them for its
pre-source-build commits):

- Remove FindRTLArchive + TToolchain.RTLPath from uToolchain — dead since the
  link paths source-build the RTL; the field was set but never read.
- TInternalLinkerE2ETests gated on blaise_rtl.a existing, so it silently skipped
  once the archive was gone; gate on the RTL source instead.  It now runs (5
  tests) — the last automated test still tied to the archive.
- Refresh the runtime mem test/bench build-instruction comments to the
  source-built, archive-free invocation (blaise --output, no qbe+gcc+.a).

Full suite green (3829 tests, 1 ignored) — TInternalLinkerE2ETests no longer
skipped.
2026-06-25 23:52:39 +01:00
Graeme Geldenhuys 773d773008 rtl-unification: move E2E test helpers off blaise_rtl.a (source-built RTL)
The E2E test helpers linked Blaise programs against the shipped blaise_rtl.a.
With the archive on its way out (RTL-unification Stage 3), migrate them to the
source-built RTL:

- cp.test.e2e.base gains LinkWithRTL: assembles the program object, builds the
  RTL from source via scripts/build-rtl-objects.sh (--exclude-defined-by drops
  the RTL units the whole-program assembly already inlined), and links the loose
  objects.  Its five cc-link sites and the ToolchainAvailable gate now use it /
  the RTL source instead of the archive.
- New cp.test.rtllink unit factors the same link helper for the standalone
  helpers (publishedrtti, proctypes_ofobject, attributes) that lower programs
  themselves rather than via the base class.
- cp.test.cli and cp.test.assembler drive an external blaise binary that
  source-builds its own RTL, so their gates now require the binary + RTL source,
  not the archive.
- cp.test.linker's TestArchive_ParsesRTL kept its archive-parser coverage
  (including the GNU long-name member) by building a throwaway .a from the
  source-built objects with ar — a test-time-only ar use, not a product one.

build-rtl-objects.sh resolves its RTL source relative to its own location (so it
works from any CWD — the fixpoint scripts run from the project root, the test
runner from compiler/), caches a per-object .syms list to keep repeated
--exclude-defined-by calls cheap, and skips rebuilding fresh objects.

Full suite green (3829 tests) on both QBE- and native-built runners; the
previously-skipped TInternalAsmE2ETests / TCLIContractTests and the archive
parser test now run.
2026-06-25 23:08:30 +01:00
Graeme Geldenhuys 406a8c303c rtl-unification: dedup RTL objects by symbol, build race-free; move fixpoint scripts off the archive
Builds on the source-built-RTL link path.  Two correctness fixes plus the
fixpoint-script migration that takes the self-host pipeline off blaise_rtl.a.

1. Symbol-based dedup.  A program that uses an RTL unit (e.g. `uses classes`
   pulls runtime.arc) compiles that unit into its own object cache, so
   EnsureRTLObjects must not supply a second copy or the link double-defines it.
   The dedup now compares the DEFINED symbols of the caller's objects against
   each RTL object (via the ELF reader) and skips one only when fully redundant
   — matching the old archive's member selection.  An earlier filename-only
   check wrongly suppressed the real bare-symbol provider when a same-named but
   differently-mangled stale object was present.

2. Race-free build.  EnsureRTLObjects now builds into a per-process private
   directory (rtl/build-<pid>/) and atomically renames each finished object
   into the shared cache.  Parallel builders (e.g. e2e suites warming a cold
   cache) no longer corrupt each other: a reader never sees a half-written
   object, and two builders just publish identical results.  The private dir is
   cleaned up afterwards.  getpid/rmdir are declared directly rather than via
   GRtlPlatform to avoid a unit-init-order crash in the driver.

3. Fixpoint scripts off the archive.  New scripts/build-rtl-objects.sh builds
   the RTL from source (the script-level equivalent of EnsureRTLObjects), with
   --exclude-defined-by so a whole-program --emit-ir/--emit-asm dump (which
   inlines the RTL units the compiler uses) is not handed a duplicate copy.
   fixpoint.sh and fixpoint-native.sh link via this helper instead of
   blaise_rtl.a; fixpoint-native-internal.sh and fixpoint-warmcache.sh no longer
   copy the archive beside the compiler (the driver source-builds the RTL).

All four fixpoints green (FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK,
WARMCACHE_FIXPOINT_OK); full suite green (3829 tests) under QBE- and
native-built runners; four cold-cache parallel suite runs pass.

The runtime Makefile / ar / make install are still present; removing them is the
final Stage-3 step.
2026-06-25 23:08:30 +01:00
Graeme Geldenhuys 7d8a4c7ad9 fix(runtime): --debug leak tracker crashed with "undefined symbol: atexit" on the native backend
The leak tracker registered its end-of-run report via glibc's bare `atexit`.
That symbol lives only in the static libc_nonshared.a; it is NOT a dynamic
export of libc.so.6.  The QBE/cc path linked fine because gcc auto-pulls
libc_nonshared.a, but the native backend's linkers (internal ELF linker and
the native external-assembler cc invocation) do not, so a --debug build aborted
at load time with `symbol lookup error: undefined symbol: atexit`.

Register via __cxa_atexit instead — a genuine dynamic libc export that resolves
on every link path.  Its third argument is the DSO handle; nil registers the
handler against the main program.  The handler takes one (ignored) argument.

Pre-existing bug (reproduces on v0.12.0); affects only --debug builds compiled
with --backend native.  Verified on all three link paths (QBE/cc, native
internal linker, native external assembler) plus the full suite and all four
fixpoints.
2026-06-25 23:08:30 +01:00
Graeme Geldenhuys 6fa4c62e80 rtl-unification: source-build the RTL on every link path; fix two separate-compilation bugs
Stage 3 of the RTL unification: the cc/QBE link line now source-builds the
RTL the same way the native internal linker already did, via a single shared
TBackendDriver.EnsureRTLObjects.  Both paths compile the implicit RTL units
(runtime.* + rtl.platform.*) into a per-compiler object cache and link those
.o files directly, instead of linking the pre-built blaise_rtl.a.  Verified
by linking a program with the archive moved aside on all three paths
(qbe+cc, native internal linker, native external assembler).

Moving from an archive to explicit .o files on the link line exposed two
pre-existing separate-compilation bugs that the archive's member selection
had masked (an archive member is only pulled to satisfy an undefined symbol,
so duplicate definitions across members never collide):

1. A unit compiled standalone via --source never had SourceFile set (only
   loader-loaded dependency units did), so its exported interface carried an
   empty SourceHash.  A later compile depending on that unit could not
   validate the cached .o and silently recompiled it from source, re-inlining
   the dependency body.  Set TopUnit.SourceFile from the --source path.

2. In unit-as-top-level mode the compiler never told the backend which
   dependencies were imported (only the program path called NoteDepInitUnit),
   so the backend re-defined imported units' globals (e.g. GPlatformLayout,
   owned by rtl.platform) instead of referencing them externally.  Mirror the
   program path: note prebuilt-iface deps and SkipDepCodegen source deps so
   their globals stay external.

EnsureRTLObjects builds the RTL leaf-first through a --unit-cache (so each
unit references its deps' globals externally and intermediate deps such as
rtl.platform are built once) using the native backend + internal assembler
(RTL units carry inline asm).  An AIncludeStartup flag controls runtime.start:
the native internal linker includes its bare _start (Blaise owns the entry),
the cc link line omits it (libc's startup provides _start and calls main).

The transitional blaise_rtl.a is still produced by the runtime Makefile and
still consumed by the fixpoint scripts and the driver-bypassing e2e test
helpers; removing those is the remaining Stage 3 work.

All four fixpoints pass (FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK,
WARMCACHE_FIXPOINT_OK); full suite green (3829 tests) under both the QBE-built
and native-built test runner.
2026-06-25 23:08:30 +01:00
Graeme Geldenhuys d85ff170cf feat(codegen): native linker builds the RTL from source, drops blaise_rtl.a
RTL-unification Stage 2 (docs/rtl-unification-plan.adoc): the native internal
linker no longer links a pre-built blaise_rtl.a.  Instead it compiles the
implicit RTL units (runtime.* + rtl.platform.*) from source into an object cache
beside the compiler (compiler/target/rtl/*.o) and links those, recompiling a
unit only when its cached .o is missing or older than the source.

- EnsureRTLObjects (native driver) drives the per-unit compile via self-invoke
  (blaise --no-incremental --assembler internal --source U.pas), the proven
  per-unit path; LinkViaInternalLinker adds the cached .o via AddOwnedObject
  instead of AddArchive.
- uToolchain.RTLSourceDir resolves the RTL source robustly: binary-relative
  (compiler/target -> ../src/main/pascal), then CWD-relative (handles a binary
  copied to /tmp but invoked from the project root, as the fixpoints do).
- New --rtl-src DIR flag (TBackendOpts.RTLSrcDir) + $BLAISE_RTL_SRC for a
  relocated/release binary; the driver fails loudly when the RTL source is
  unreachable rather than emitting a broken binary.

Performance: warm cache adds ~0ms (RTL skipped); a cold full RTL recompile is
~0.4s.  Scope: this changes the NATIVE internal-linker path only — the QBE
backend and cc-link paths still use blaise_rtl.a, so the archive remains a build
artifact (the fixpoint scripts and QBE-based tests still build it).

TestLink_MissingRTL_FailsLoudly rewritten for the new contract (--rtl-src at a
nonexistent dir must fail naming the RTL source).  All four fixpoints OK; full
suite 3829 green; cold-bootstrap from the existing release works.
2026-06-25 23:08:30 +01:00
Graeme Geldenhuys ac9e2fa0b1 refactor(rtl): relocate RTL units into the compiler tree (dotted-flat names) 2026-06-25 16:05:44 +01:00
Graeme Geldenhuys bdca25c99e feat(semantic): IsUnmangledUnit recognises the runtime. prefix 2026-06-25 16:05:44 +01:00
Graeme Geldenhuys 52fd1d5b3b feat(asm): SSE2 + AVX2/VEX encoders 2026-06-25 14:53:30 +01:00
Graeme Geldenhuys 7781242696 feat(asm): strip @PLT from symbol names; probe stale fallback compiler in e2e tests 2026-06-25 14:53:30 +01:00
Graeme Geldenhuys c673fc21f1 feat(asm): unsuffixed mnemonics, mem-indirect jmp, endbr64/hlt/syscall 2026-06-25 14:53:30 +01:00
Graeme Geldenhuys 85a46aa1ec feat(asm): lock prefix + negl/xaddl encoders 2026-06-25 14:53:30 +01:00
Graeme Geldenhuys 8467eed451 feat(codegen): internal linker honours --target's TLinkTarget
LinkViaInternalLinker always built the linker with the default (Linux)
TLinkTarget, so --target freebsd-x86_64 still emitted a SYSV/Linux ELF. Resolve
the target's toolkit and build TLinker with its MakeLinkTarget, so the emitted
ELF carries the right EI_OSABI / machine / load base for the requested target.

--target freebsd-x86_64 now stamps EI_OSABI = 9 (FreeBSD); the default Linux
target is unchanged (EI_OSABI = 0, still compiles+runs). The FreeBSD binary is
not yet runnable (Strategy-B syscall _start stub lands later) — this is the
OSABI/link-target wiring (Step 1 of the FreeBSD design).

Adds TLinkerE2ETests.TestLink_FreeBSDTarget_StampsOSABI locking in that the
FreeBSD link target produces a static ET_EXEC with EI_OSABI=9.

All four fixpoints OK; full suite 3813 green.
2026-06-25 14:53:30 +01:00
Graeme Geldenhuys e723e80063 feat(codegen): target toolkit + registry; register freebsd-x86_64
Introduce TTargetToolkit (Abstract Factory) and TTargetRegistry so per-target
compiler-side objects (native backend + ELF link target) are resolved by name
rather than hard-coded case statements. CreateNativeBackend now delegates to
the registry; adding a target is a new toolkit + one registration with no edits
to existing dispatch.

Registers linux-x86_64 (unchanged behaviour) and freebsd-x86_64 (link target
with EI_OSABI=9; reuses TX86_64Backend since FreeBSD shares the System V AMD64
ABI). FreeBSD codegen/runtime lands in later steps.

The registry uses TOrderedDictionary<string, TTargetToolkit> (name -> toolkit)
per the Generics.Collections policy. It is built publish-after-populate: the
--incremental compile worker pool is threads, so a half-built global would let
a worker wrongly conclude a target has no backend.

Step 0a of docs/native-target-architecture.adoc. Behaviour-preserving:
FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK, WARMCACHE_FIXPOINT_OK;
full suite 3761 tests green (QBE- and native-built runners).
2026-06-25 14:53:30 +01:00
Graeme Geldenhuys ed262aa238 fix(driver): write per-unit .o/.bif to CWD, not root, for a bare --output
In default incremental mode each used unit is compiled to its own .o/.bif
by a worker thread.  The worker's output directory was derived as
IncludeTrailingPathDelimiter(ExtractFilePath(OutputFile)).  When --output is
a bare filename with no directory part (e.g. `--output mcd`), ExtractFilePath
returns '' and IncludeTrailingPathDelimiter('') yields '/', so the worker
tried to write `/<unit>.o.bif.tmp` at the filesystem root and aborted with
`Worker exception: Cannot open file for writing: /<unit>.o.bif.tmp`.

Resolve the artefact directory once before the worker loop, treating an empty
directory (bare or omitted --output, no --unit-cache) as the current
directory instead of root.  Priority is unchanged: --unit-cache, then the
--output directory, then CWD.

Add a CLI regression test that compiles a program using a separate unit with
a bare --output name and asserts no root-path worker failure and a clean
exit.
2026-06-25 14:39:44 +01:00
Graeme Geldenhuys d7460cb069 feat(parser): require parentheses on inherited calls; fix metaclass diagnostic
The mandatory-parentheses rule ("every function, procedure, method, and
constructor call requires parentheses, even with no arguments") made no
exception for 'inherited'.  A bare 'inherited Create' is a method call and
should be rejected like any other parenless call, but the parser silently
accepted it.

Enforce the rule in both 'inherited' parse paths (statement and expression
position): a bare 'inherited Method' now raises the same "requires () for a
call" diagnostic as any other parenless call.

Also fix a misleading semantic error for the metaclass-variable case.  A bare
'C.Create' on a 'class of T' variable parses as a field access, which reached
the "'C' is not a record or class" guard — confusing, since C is a valid
metaclass variable.  AnalyseFieldAccess now detects a constructor/method name
on a metaclass receiver and emits the "requires () for a call" diagnostic
instead.  (The parenthesised 'C.Create()' already worked: it parses as a
TMethodCallExpr, which has the metaclass-dispatch branch.)

Migrate every bare 'inherited Method;' call site in stdlib, the bif-coverage
tool, and embedded e2e test programs to the parenthesised form.  The
parenthesised form compiles on the prior compiler too, so the migration and the
enforcement land together without breaking the rolling-bootstrap chain.

Docs: update language-rationale.adoc (inherited section + mandatory-parens
section) and grammar.ebnf (expression-position inherited rule).

Tests: add TestSemantic_MetaclassVar_BareCreate_RequiresParens and
TestParse_Inherited_Bare{Stmt,Expr}_RequiresParens.
2026-06-25 13:36:27 +01:00
Andrew Haines 4d5fb3ac96 fix(units): tolerate an implementation-section circular dependency
The unit loader treated any back-edge into a unit still on the load stack
as a circular dependency, rejecting a legal Object Pascal arrangement: a
unit whose implementation uses a unit whose interface (transitively) uses
it back.  That is not a real cycle — interfaces are compiled before bodies,
so the back-referenced interface is available by the time the body needs it.

Split cycle detection into two stacks.  FLoading still guards re-entry /
infinite recursion across every edge.  FIfaceChain tracks only the units
reached along an unbroken chain of interface-section uses; a back-edge is a
true circular dependency exactly when it closes that chain.  Implementation-
section uses are traversed with a fresh interface chain, so a back-edge
reached through one is tolerated (the unit is already loading higher up the
stack) instead of raising.  An interface↔interface cycle still errors.

Test: a two-unit fixture where BackA's interface uses BackB and BackB's
implementation uses BackA now loads without ECircularDependency, while the
existing interface↔interface DetectsCycle test still raises.
2026-06-25 01:41:59 +01:00
Andrew Haines ff2d73f70d fix(codegen): convert integer to float on class-field stores
A store into a Double or Single class field never converted an integer
right-hand side to floating point, on either backend:

  * QBE never emitted swtof/sltof — the field-store paths only handled
    integer widening (w->l) and float-width adjustment (s<->d).  An Int64
    or Integer RHS was deposited into the float slot as raw integer bits;
    on the explicit-field path QBE rejected the `stored` of an integer SSA
    value, and on the implicit-Self path a hardcoded `storel` silently
    wrote garbage.

  * The native x86-64 backend converted an integer RHS to a DOUBLE in
    %xmm0 (cvtsi2sdq) but then stored it into a Single field with a bare
    movss — writing only the low 32 bits of the double, i.e. garbage.  A
    Single field needs the value narrowed (cvtsd2ss) first; the Double
    field happened to work.

Both backends now promote/convert to the field's width.  QBE: swtof/sltof
for an integer RHS plus exts/truncd for a float-width mismatch, in
EmitFieldAssignment and the implicit-Self field branch of EmitAssignment.
Native: an EmitXmm0WidthAdjust after EmitExprToXmm0 in both the
TFieldAssignment and implicit-Self float field-store paths, narrowing a
Double/integer RHS to the Single field width before the movss.

Tests: cp.test.codegen IR assertions for the Self.Field, bare-field and
Single targets; cp.test.e2e.classes2 end-to-end coverage of Int64->Double
and Integer->Single through both Self.Field and bare implicit-Self forms,
verified on QBE and native.
2026-06-25 01:39:38 +01:00
Andrew Haines bfbcfb0489 fix(stdlib): release managed elements in generics.collections TList
TList<T> stored its elements in a raw GetMem buffer and freed only the
buffer (Destroy did FreeMem with no element cleanup; Clear just reset
the count; Delete's shift left a duplicate ref on the vacated tail
slot). For a managed element type — a class (ARC) or a string — that
leaked every contained value: destroying or clearing the list never
released its items, so their destructors never ran.

Release each managed element by storing a zero-initialised T through
its slot before dropping it: the compiler's ARC discipline on the
managed store releases the previous value, so freeing the list cascades
to its items. For a non-managed T (e.g. Integer) the same store is a
harmless zero write, so TList<Integer> is unaffected.

Applied to Destroy (all elements), Clear (all elements), and Delete
(the vacated tail slot). The sibling containers (TStack, TQueue, TSet,
and the dictionaries) share the same raw-buffer pattern and leak
likewise; left as a follow-up.

Test: an e2e regression on both backends asserts a class element's
destructor fires on Clear and on Free (the cascade), and the existing
TList<Integer> coverage confirms the non-managed path is unchanged.
2026-06-25 01:37:08 +01:00
Andrew Haines 4eeff7c2c0 feat(stdlib): add SameFileName to SysUtils
SameFileName compares two file names for equality. This is a partial,
Unix-only stub: an exact case-sensitive comparison, which is correct on
case-sensitive file systems. Case-insensitive platforms would need the
names folded before comparing, but the stdlib has no platform-variance
facility yet — system.pas hardcodes DirectorySeparator, PathSeparator
and LineEnding to their Unix values — so there is nothing to key the
case-folding on. When a FileNameCaseSensitive constant lands alongside
the other platform constants, the case-insensitive branch can be added.

e2e test asserts identical names match and differing names do not.
2026-06-25 01:34:42 +01:00
Andrew Haines 4addcd1076 feat(types): support forward class and interface declarations
A bare `TFoo = class;` / `IFoo = interface;` (the keyword immediately
followed by `;`, with no ancestor clause and no body) is a forward
declaration: it registers a placeholder type that a later full
declaration of the same name completes within the same declaration
scope. This is the standard Object Pascal idiom for mutually
referential types, e.g. a class declared between the forward and its
full declaration may name the forward-declared type.

Previously the front end registered the bare `class;` as a complete
type and then rejected the full declaration as a duplicate type name,
so the idiom could not be used at all.

The parser flags the stub via TClassTypeDef.IsForward /
TInterfaceTypeDef.IsForward. AnalyseTypeDecls registers the placeholder
in pass 1, lets the matching full declaration complete it (reusing the
placeholder descriptor) instead of erroring, skips the stub in pass 2,
and after resolution drops the now-redundant stub from the AST so later
phases see a single declaration. A forward never completed in the scope
is reported as "Forward type not resolved"; a type forward-declared
twice is "Duplicate forward type declaration".

Tests: IR/semantic coverage for completion, mutual reference,
unresolved, and double-forward errors, plus a single-vtable codegen
check (the stub must not double-emit); an e2e test compiles and runs a
mutually-referential class pair with a managed field.
2026-06-25 01:32:22 +01:00
Andrew Haines 3c092babc6 feat(parser): resolve unit-qualified symbols UnitName.Symbol
A reference written `Unit.Symbol` (a procedure/function call, variable, or
constant qualified by a used unit) previously failed to resolve: the parser
built it as a record/field or method-call chain rooted at the unit name, and
the analyser then rejected the unit name as an undeclared variable.

Collapse the qualifier in the parser instead.  Each `uses` clause feeds a
name list (plus the implicit System), consulted purely as syntax — no symbol
or type lookup — to recognise a leading run that spells a used unit followed
by a trailing `.Symbol`.  On a match the unit prefix is dropped, leaving the
bare symbol, which the existing uses-chain resolution and codegen already
handle.  Producing canonical TProcCall / TFuncCallExpr / TIdentExpr nodes
keeps the change entirely in the front end.

The unit name may be dotted to any depth (System.SysUtils.Foo, A.B.C.D.Sym):
an on-demand lookahead buffer scans the whole dotted chain, exposed through
scalar peek accessors (kind/value/line/col) so no transient TToken records —
which carry a managed string — are produced on the matcher's hot path.  The
required trailing `.Symbol` is what distinguishes a qualifier from a
same-prefixed record/field chain, so `My.Pkg := 4` (record `My`, field `Pkg`,
with `My.Pkg` also a used unit) is left untouched.

Known limitation: in the rare case where a dotted unit's first component is
also an in-scope variable (`var My` + `uses My.Pkg`, then `My.Pkg.Foo`), FPC
resolves the variable; the parser has no scope and resolves the unit.  Single-
identifier units cannot collide this way (a variable sharing a unit's name is
a duplicate-identifier error).

Tests: cp.test.parser asserts the canonical-node collapse at one, two, and
three component depths, plus the two negative cases (non-unit receiver stays a
method call; no-trailing-symbol stays a field write).  cp.test.e2e.useschain
compiles and runs qualified System calls and qualified cross-unit calls/vars
through single and dotted unit names.
2026-06-25 01:29:42 +01:00
Andrew Haines dc3b7a1c2e feat(semantic+codegen): implicit-Self write through a default array property
Field[I] := V inside a method, where Field is a class field of the current
class carrying a writable default array property, reported "Undeclared variable
'Field'": AnalyseStaticSubscriptAssign's implicit-Self branch only handled
array-typed fields, so a class field with a default property fell through.

Extend that branch: when the Self field is a class with a writable default
property, set IsImplicitSelf + ImplicitFieldInfo + PropWriteInfo and lower to
the default property's setter (mirroring the variable-receiver path). Both
codegen backends gain an implicit-Self receiver in the static-subscript
property-write path: load Self, reach the field slot, dereference to the
field's object, then call the setter.

(The implicit-Self read Field[I] already worked.) e2e test writes and reads a
default property through a Self field from inside methods, verified on QBE and
native.
2026-06-25 01:27:05 +01:00
Andrew Haines 1bd52ec39a feat(semantic+codegen): write through a default array property on a member result
Reading a default array property through a member result already worked
(Recv.Member[idx]); the write form Recv.Member[idx] := V did not. A class field
reported "Field is not an array — cannot assign to a subscript", and a property
reported "Property is read-only", because the write paths only handled array
fields and writable properties directly, never a default property on the
member's result.

Add TryLowerDefaultPropertyWrite: when the assignment target is a class member
(field or property) whose type carries a writable default array property and a
trailing index is present, lower Recv.Member[idx] := V to
(Recv.Member).Default[idx] := V — read the member into an inner object
expression and re-target the assignment at the default property's setter.
Wired into the field path (TryAnalyseFieldElemWrite, so it covers every
receiver form) and the read-only-property path. Class members only; a record
getter result is a by-value temp, so a write through it would be discarded.

The setter receiver is now an arbitrary expression, so both codegen backends
gain an ObjExpr branch in the property-write path (QBE EmitFieldAssignment and
the x86-64 native field-assignment): evaluate the receiver expression to get
the object pointer instead of loading a named var or Self.

e2e tests cover writing through a class field and through a (read-only)
property, verified on QBE and native.
2026-06-25 01:24:29 +01:00
Andrew Haines 9024ffc766 fix(semantic): read a default array property through a property result
Recv.Prop[idx] where Prop is a (non-indexed) property whose type carries a
default array property previously dropped the index: the parser folds the
trailing [idx] into the field-access PropIndexExpr, and the property-read
branches in AnalyseFieldAccess returned the property's type without consuming
it (Recv.Prop[idx] resolved to the property's class type, e.g. TStringList).
A field with a default property already worked via FindIndexedProperty; a
property read did not.

Add TryLowerDefaultPropertyIndex: when a resolved read property is non-indexed
but has a trailing index and its result type has a default property, rewrite
Recv.Prop[idx] to (Recv.Prop).Default[idx] — moving the property read into an
inner field-access and re-pointing the node at the default property, then
re-analysing (which lands on the existing chained indexed-property path). Wired
into the field-backed and method-backed property branches (chained receiver,
implicit Self, and variable receiver).

Reads only; writing through a property-result default property is a separate
gap (the write path reports the property read-only).
2026-06-25 01:22:13 +01:00
Graeme Geldenhuys bcb2f68222 test(codegen-native): lock in offset-0 sret-Result field read
Adds an e2e regression (both backends) that read-modifies Result.A (offset 0)
of a managed sret-returned record mid-body.  The native offset-0 field-read
shortcut used to load the Result slot directly — but for an sret function that
slot holds a POINTER to the caller's buffer, so the pointer was read as the
field value (garbage).

The fix already landed (the inline-asm commit routes the offset-0 integer
field-read leaf through EmitLocalRecordBase so the sret-Result deref happens);
this test, from Andrew Haines's parallel fix (236e9525), locks it in.  His code
change is omitted as a duplicate of the already-landed fix.

Co-authored-by: Andrew Haines <andrewd207@aol.com>
2026-06-25 01:20:45 +01:00
Andrew Haines 801feb0792 fix(codegen): thread sret pointer for inherited record-by-value returns
An override that returns a record by value and sources it from `inherited`
corrupted the heap when the record had a managed (string) field:

    function TDeriv.Next: TTok;   { TTok = record Kind; Value: string; ... }
    begin
      Result := inherited Next();
    end;

The inherited-call emitters carried their own argument-marshalling, copied
from the normal call path but missing the hidden sret pointer that a
record-returning callee requires. So the call passed Self into the callee's
sret slot — the base then wrote its managed field straight into the Self
object and _StringRelease ran on garbage, segfaulting.

Route inherited record/static-array returns through the same sret machinery
every other record-returning call uses, dispatched statically to the parent:

  - QBE: a shared InheritedArgLine helper marshals Self + args once; the
    statement, expression, and assignment (IsRecordCall) paths emit through
    EmitRecordReturnCallSite with the destination as the hidden first arg.
  - native: EmitInheritedRecordSret reuses EmitMethodSretCall via a transient
    implicit-Self node, force-static, so the sret pointer / register-return
    capture is threaded identically to a normal call.

The assignment form writes straight into the destination slot (no temporary,
no double AddRef); the implicit-Result statement form writes into Result.

Tests: cp.test.recordret asserts the QBE IR threads the sret pointer (Result
slot) + Self with a static parent call; cp.test.e2e.recordret covers the
assignment and statement forms with a managed-field record on both backends.
2026-06-25 01:17:22 +01:00
Andrew Haines 6eea55bc59 feat(semantic): allow unqualified calls to a procedural-typed field
Inside a method, an unqualified FFn(args) -- no 'Self.' prefix -- where
FFn is a procedural-typed field of the current class failed with
"Undeclared function/procedure 'FFn'".  Only the explicit Self.FFn(args)
form resolved.

Both call analysers now fall through to an implicit-Self field lookup
after the method lookup fails: a procedural-typed field of the current
class is dispatched through its stored pointer, validating argument count
and types (and the l-value requirement for var/out arguments) against the
field signature, exactly like the explicit-receiver path.

Codegen:
  * QBE: a shared EmitImplicitSelfProcFieldCall loads Self, indexes the
    field, and dispatches; var/out arguments are passed by reference and
    'of object' fields pass the captured Data as the implicit first arg.
    Used by both the statement and expression forms.
  * Native x86-64: both forms reuse EmitProcFieldCall with Self as the
    receiver.

The two new AST flags (TProcCall.IsProcFieldCall,
TFuncCallExpr.IsProcFieldCall) are recorded in the bif-coverage status
inventory as semantic-only (safe, not serialised), matching the existing
IsIndirectCall flags.

Covered by an IR test (loads Self, indirect dispatch) and end-to-end
tests on both backends for the expression and statement forms.
2026-06-25 01:14:42 +01:00
Andrew Haines aa33103659 fix(codegen): store @Obj.Method into a class field on the native backend
Assigning a method pointer (procedure/function of object) captured with
@Obj.Method into a class field -- B.FEvt := @S.Handle -- raised
"@Obj.Method must be used in assignment context" on the native x86-64
backend.  The simple-variable destination was already handled, but a
field destination fell through to evaluating @Obj.Method as a general
expression, which has no standalone lowering.

The field-assignment path now special-cases a method-pointer field whose
right-hand side is @Obj.Method: it computes the field's containing-object
base per receiver shape (as the interface-field store already does), then
stores the code pointer at offset 0 and the captured object pointer at
offset 8 of the field's 16-byte slot.

This also makes the of-object arm of procedural-field call dispatch
reachable on the native backend (a field must be populated before it can
be called).  Covered end-to-end on both backends: capture @Obj.Method
into a field, then dispatch through it.
2026-06-25 01:12:24 +01:00
Andrew Haines 5a4bc7f7d1 fix(codegen): support calling a procedural-typed field through a receiver
Calling a function-pointer class field via its receiver (Self.FFn(args))
was broken in three independent ways:

  * Semantic analysis gave the call expression a nil result type, so
    using it as a value (Result := Self.FFn(S)) failed with "Expression
    has no value type in assignment".  The three procedural-field call
    branches now carry the field signature's return type.

  * The QBE backend passed var/out arguments by value instead of by
    reference, so a callee writing through an out parameter corrupted
    memory and crashed; the expression form also hardcoded the call's
    result width to l rather than the signature's return type.

  * The native x86-64 backend had no support for procedural-field calls
    at all -- both the statement and expression forms raised "has no
    ResolvedMethod".  EmitProcFieldCall lowers the dispatch by pushing
    the arguments first, then loading the (call-free) receiver and the
    field's code pointer, mirroring the interface-dispatch shape.

Covered by an IR-level test (indirect dispatch, not a direct call) and
end-to-end tests that run on both backends: value return, statement
call, out parameter, and multiple value arguments.
2026-06-25 01:10:10 +01:00
Graeme Geldenhuys 0396ed7b1c feat(lang): inline assembler blocks (asm … end routine bodies)
A routine body may now be written as inline assembly:

    function GetSelf: Pointer; assembler; nostackframe;
    asm
        movq %rdi, %rax
        ret
    end;

The block is opaque GNU/AT&T assembly: the lexer captures the whole asm … end
as one tkAsmBlock token (verbatim text, never tokenised as Pascal), the parser
wraps it in a TAsmStmt, the semantic pass treats it as a black box, and the
native backend emits it verbatim into the assembly stream where the existing
internal/external assembler parses it.  `nostackframe` suppresses the compiler
prologue/epilogue so the asm body owns the whole frame.  asm routines mix
freely with ordinary Pascal routines in a standard .pas unit (no .inc needed).

This is the FPC model (rtl/linux/x86_64/si_c.inc) and the path to retiring the
hand-written runtime/src/main/asm/*.s files (assembled by `cc -c` today) — once
each body moves into an asm routine the RTL builds with no external assembler.

Design follows ports-and-adapters: x86-64 knowledge stays at the backend/
assembler edge, the portable core never interprets the block.  The QBE backend
rejects asm bodies (it emits no assembly text); native is the inline-asm target
and the default.  TAsmStmt round-trips through the .bif unit cache.

Pipeline: lexer (tkAsmBlock + ReadAsmBody raw capture), uAST (TAsmStmt,
TMethodDecl.NoStackFrame), parser (nostackframe directive + asm-body path),
semantic (opaque no-op), native codegen (verbatim emit + nostackframe null-frame
guard), QBE rejection, uUnitInterfaceIO encode/decode, bif-coverage entry.
`asm` becomes a reserved word (one local var named Asm renamed in a test).

Fixes a native sret-Result field-read codegen bug the feature exposed: reading a
field of an sret function's Result at offset 0 (e.g. `Result.Kind` in a record-
returning function) read the Result frame slot DIRECTLY instead of dereferencing
the caller-buffer pointer it holds, so `Result.Field = const` was always false.
The offset-0 fast path in the integer field-read leaf now routes through
EmitLocalRecordBase like the offset>0 path, so the sret indirection happens in
both.  QBE was already correct; this was a native-only divergence.

Tests: lexer raw-capture (3), native verbatim-body/no-prologue IR test (1),
internal-assembler e2e returns-value + adds-two-args (2).  All four fixpoints
and both QBE- and native-built test runners pass (3764 tests).

Design note: docs/inline-asm-design.adoc; grammar + rationale updated.
2026-06-25 01:01:41 +01:00
Graeme Geldenhuys 626ee4c9f8 fix(linker): ship own _start; drop system CRT startup objects (#142)
The native internal linker linked every program against system CRT startup
objects — Scrt1.o for _start plus crtbeginS.o/crtendS.o/crti.o/crtn.o —
located by scanning a hard-coded list of multiarch and versioned gcc
directories (/usr/lib/gcc/<triple>/<ver>/).  That layout varies by distro, so
the scan failed with "internal linker: CRT startup objects not found" on
distributions whose triple/gcc version was not listed (issue #142, Mageia 9).

Asking gcc (cc -print-file-name) would locate them portably but reintroduces a
link-time gcc dependency, defeating the internal linker's purpose
(toolchain independence).  Instead, follow FPC's approach: the runtime now
supplies its OWN _start (runtime/src/main/asm/blaise_start_x86_64.s), which
marshals argc/argv and tail-calls __libc_start_main(main, ...) — the modern
glibc convention (init/fini = NULL; glibc runs the init array itself).

Blaise consumes nothing else from the CRT set: main is a plain C-style entry,
unit init runs explicitly from main, and the emitted code references no
crtbegin/crtend symbol.  So the internal linker now needs no gcc-provided
object and no versioned gcc directory; FindCrtObjects and its directory lists
are removed.

The new _start ships inside blaise_rtl.a; AddArchive includes all members
unconditionally, so the entry symbol is always present.

Tests: two TInternalAsmE2ETests cases drive the REAL compiler binary through
the internal assembler + internal linker (the only harness that exercises the
runtime _start; the e2e suite links via cc) and assert the two things _start
owns — exit-code propagation (Halt(7)) and argc/argv forwarding (ParamCount/
ParamStr). The native fixpoints already link the compiler itself via this path.

Design note: docs/self-contained-start-design.adoc.
2026-06-24 23:39:11 +01:00
Graeme Geldenhuys 47d18eb749 test(codegen): lock in Int64(<large-literal>) value-cast lowering
The QBE backend once mis-lowered a value-cast of a >32-bit literal
(cMax = Int64(922337203685477580)), emitting a 'w'-typed operand that QBE
rejected with "invalid type for first operand ... in arg".  The width-by-
magnitude literal lowering now emits these as 'l', so the cast is correct on
both backends.

Adds an IR unit test asserting the 'l' (not 'w') lowering and an e2e test
(AssertRunsOnAll, both backends) exercising the const through folding,
arithmetic, and argument positions.
2026-06-24 17:21:48 +01:00
Graeme Geldenhuys 175a5cc692 fix(codegen): self-assigned record method no longer aliases Self (M := M.Method())
Assigning a record-returning method's result back into the same variable
that is the receiver (M := M.Method(...)) corrupted the computation: the
sret destination buffer aliased Self, so the callee wrote Result (== Self)
while still reading Self, clobbering it mid-flight.  Affected both backends.

Both backends now detect the case where the call's receiver is exactly the
destination variable and route the sret call through a fresh zeroed temporary,
moving the constructed result into the destination (release old managed fields,
raw ownership-transfer memcpy) only after the call returns.

Adds an IR unit test (RoutesThroughTemp) and two e2e tests (managed-string
field and scalar fields) covering the self-assign and a non-aliasing
regression guard.
2026-06-24 17:15:46 +01:00
Graeme Geldenhuys 38cf13fd92 fix(units): decode dotted unit-qualified type refs at the last dot
Warm --unit-cache rebuild: a unit recompiled from source could not resolve a
method inherited from a class in a DIFFERENT, still-cached ancestor unit when
that ancestor unit had a DOTTED name (e.g. blaise.testing):

    note: ...money.o iface stale vs source on path; recompiling from source
    Semantic error: Undeclared procedure 'Ignore' in cp.test.e2e.numerics.money

Root cause: DecodeQualRef in uUnitInterfaceIO split a serialised 'Unit.Type'
reference at the FIRST '.'.  A type name never contains a dot, but a unit
qualifier can — so 'blaise.testing.TTestCase' decoded to unit='blaise',
type='testing.TTestCase'.  ReadClassPayload then stored the mangled
'testing.TTestCase' as the imported class's ParentName, and the bare Lookup in
ResolveParentClassByName could not find it.  The intermediate cached class's
Parent pointer was left nil, so the leaf's inherited-method walk dead-ended one
level early and reported 'Undeclared procedure'.

Fixes:
* DecodeQualRef now splits at the LAST '.', so the type name is always the bare
  trailing component and the unit qualifier keeps its full dotted name.  This
  corrects every qual-ref decode site (consts, vars, fields, return types,
  parents), not only class parents.
* ResolveParentClassByName resolves the parent via TSymbolTable.FindType
  (qualifier-aware, uses-chain-honouring) instead of a bare Lookup, as defence
  in depth for any qualified parent name.

Regression: TSepCompileTests.TestIncrementalRebuild_QualifiedGrandparentMethod
drives a real warm --unit-cache rebuild over a 3-level chain rooted in a dotted
unit; it fails against the pre-fix compiler and passes after.  Verified to
reproduce the original 'Undeclared' failure when either fix is reverted.

All four fixpoints (QBE, native, internal-assembler, warm-cache) and the full
suite (3751 tests, QBE-built and native-built runners) pass.
2026-06-24 16:28:30 +01:00
Graeme Geldenhuys b0f92f0d3c fix(semantic): tolerate cross-unit duplicate external bindings + private impl procs in flat-merge
Compiling a multi-unit program whose uses-clause pulls in every runtime
unit (the auto-generated bootstrap_program used by 'pasbuild compile -m
blaise-runtime') failed in the semantic pass. Several runtime units each
privately bind the same C/RTL symbol (e.g. 'external name '_BlaiseGetMem'',
'external name 'abort'') or define a private implementation-section helper
of the same name (DiagAbort in both blaise_arc and blaise_exc). In the
flat-merge these all share one global scope and overload group, producing
spurious 'Duplicate identifier' and 'Ambiguous overload' errors.

Three coordinated changes:

* SameLinkSymbol/EffectiveLinkName: collapse candidates that denote the
  same underlying link symbol when at least one side is an external
  binding — covering both two-units-bind-same-C-symbol and a binding that
  targets a real function exported by an unmangled RTL unit (blaise_*/rtl.*,
  whose exports keep their bare Pascal name). SameExternalDecl and the
  overload-collapse / zero-arity paths now route through it.

* BenignDuplicateExternal: when an interface-section Define collides in the
  global scope, tolerate it if the colliding symbol is the same link symbol.

* IsImplOnly flag on TMethodDecl: marks implementation-section-only
  routines as private to their unit; ResolveStandaloneOverload excludes
  cross-unit private candidates so each unit's call binds its own helper.

Adds three regression tests in cp.test.multifile.pas. All four fixpoints
(QBE, native, internal-assembler, warm-cache) and the full suite (3750
tests, QBE-built and native-built runners) pass.
2026-06-24 15:59:30 +01:00
Graeme Geldenhuys 01b5b97659 fix(codegen): itab ref for non-virtual inherited interface method (#130 bug3)
When a class inherits its interface implementation from an ancestor and the
implementing method is NON-virtual, the descendant's interface table named
$<descendant>_<method> — a symbol that does not exist — causing a link error
('undefined reference to TDerived_Hello'). A non-virtual method has no vtable
slot, so the itab resolver could not find it via the vtable and fell back to
naming the method after the descendant class instead of the declaring ancestor.

Fix in both backends: when the vtable lookup fails, walk the AST class chain
(self, then ParentName ancestors) to the nearest class that DECLARES the method
and name $<declaringclass>_<method>. QBE gains ItabImplClassName; native
extends ItabMethodRefNative with the same chain walk (it now takes the type-decl
list to resolve ancestors by name).

The existing inherited-interface tests only used virtual methods, which DO get
vtable slots, so this path was uncovered. New regression
TestRun_InheritedInterface_NonVirtualMethod compiles+links+runs on both backends.

Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, full suite green (3747) under both the
QBE-built and native-built test runners.
2026-06-24 12:40:46 +01:00
Graeme Geldenhuys e72b644a94 fix(stdlib,semantic): TObjectList default property + chained default-subscript crash (#138)
TObjectList.Items lacked the `default` directive, so OL[i] failed with
'String subscript requires a string expression' even though TStringList.Strings
already had it. Add `default;` to TObjectList.Items.

Separately, TStringList(OL[0])[1] — a typecast whose operand is itself a
default-property subscript, indexed again by a default property — crashed the
compiler. AnalyseStringSubscriptExpr rewrites the node in place (swaps StrExpr
for a synthesised field-access, clears IndexExpr) on the default/indexed-property
paths; the outer subscript re-analyses its typecast base, which re-analyses the
already-rewritten inner subscript, recursing without bound. Guard against
re-analysis by returning the cached ResolvedType when already set, and set
ResolvedType on the plain-string exit path so the guard is complete.

Regression test TestRun_DefaultProp_StringList_And_ObjectList compiles+runs the
issue's exact example on both backends with parity.
2026-06-24 12:32:11 +01:00
Graeme Geldenhuys 65e7b87a7c release: v0.12.0
Native x86-64 is the default backend with a complete internal assembler and
internal linker — compiling and linking with zero external tools (QBE is now
opt-in via --backend qbe, deprecated).  Incremental, per-unit compilation is
the default, and per-unit OPDF makes incrementally-built multi-unit programs
fully debuggable in pdr.  Ships a real opt-in stdlib: JSON, SHA-1/Base64, RFC
4122 GUIDs, TCP sockets, WebSockets, a minimal HTTP/1.1 server, plus the
blaise.testing framework.  .bif format advanced to v2.

Includes two incremental-toolchain fixes folded in before release: the internal
linker now errors instead of silently emitting a runtime-less binary when
blaise_rtl.a is unreachable, and warm --unit-cache rebuilds propagate staleness
to dependents of an edited unit (previously raised EImportError / could crash).

Fixpoint verified at 410477 lines of QBE IR — FIXPOINT_OK, NATIVE_FIXPOINT_OK,
NATIVE_INTERNAL_OK, and WARMCACHE_FIXPOINT_OK all green.  3745 compiler tests
(1 ignored) + 57 stdlib tests passing.
2026-06-24 07:44:29 +01:00
Graeme Geldenhuys 834b51baba chore: begin v0.13.0-dev cycle
Version 0.12.0 -> 0.13.0-SNAPSHOT (Blaise.pas, project.xml) and COMPILER_ID
-> blaise-0.13.0-SNAPSHOT+bif2 (the .bif format is unchanged, so the +bif2
tag stays; the version prefix tracks the dev cycle).
2026-06-24 07:44:29 +01:00
Graeme Geldenhuys 3ff19c0a00 fix(incremental): propagate cache staleness to dependents of edited units
When a unit's source content changed, it was correctly recompiled from a warm
--unit-cache, but a cached unit that USES it kept its cached interface and was
imported in the cached-iface phase — which runs BEFORE source-recompiled units
are analysed.  Importing the cached dependent then failed to resolve a type
defined only by the not-yet-analysed dependency, raising EImportError (empty
message), e.g. editing blaise.codegen.driver and rebuilding the whole compiler
warm broke on blaise.frontend.opts (TBackendKind unresolved).  The same
corrupted warm cache also produced an intermittent SIGSEGV in the e2e runner.

uUnitLoader now tracks units taken via the source path (FSourceLoadedNames).
In the cached path, after recursing into a unit's interface-use dependencies,
if any dependency was source-loaded the cached iface is discarded and the unit
is recompiled from source too, so it is analysed after its dependency.
Post-order DFS makes this transitive.

fixpoint-warmcache.sh now edits a dependency unit's content between its two
builds (previously two clean fills that never exercised staleness propagation);
it FAILS on the pre-fix compiler and PASSES on the fixed one.
2026-06-24 07:40:30 +01:00
Graeme Geldenhuys 48569f36d1 fix(linker,bootstrap): error on missing RTL; carry RTL with correct name
The internal linker silently emitted a runtime-less binary when blaise_rtl.a
could not be found beside the compiler (RTLPath empty): every RTL symbol became
an undefined dynamic import, so the binary linked but died at runtime with
`undefined symbol: _SetArgs`.

scripts/rolling-bootstrap.sh hit exactly this: it carried each step's binary
forward as _carry_blaise with its RTL as _carry_rtl.a, but FindRTLArchive looks
for `blaise_rtl.a` beside the binary (CompilerBinDir) — a name it never used —
so the next step linked the compiler with no runtime archive and SMOKE_FAILED.

- Carry the binary + RTL into a dedicated _carry/ dir as blaise + blaise_rtl.a,
  so FindRTLArchive resolves it. Rolling bootstrap reaches HEAD again.
- Make an empty RTLPath a hard error on both link paths (internal and external
  cc) instead of producing a broken binary.
- Regression test TInternalLinkerE2ETests.TestLink_MissingRTL_FailsLoudly:
  runs a symlinked compiler from an RTL-free dir and asserts a non-zero exit, a
  message naming blaise_rtl.a, and no output binary.

The four fixpoint scripts missed this because fixpoint-native.sh links the .s
via gcc, never exercising the internal-linker + FindRTL path on a carried
binary layout.
2026-06-24 07:40:30 +01:00
Graeme Geldenhuys 4547a9e174 fix(bif): set IFACE_VERSION to 2 for the v0.12.0 release (was 1 in public v0.11.x)
The .bif format was bumped 1->2->3 internally across this development cycle, but
the last PUBLIC commit (d56bdbf, release v0.11.x) shipped IFACE_VERSION = 1 with
COMPILER_ID +bif1.  None of the intermediate v2/v3 layouts ever reached a public
reader, so the cumulative format change since v1 is a SINGLE public step: collapse
it to v2.

- IFACE_VERSION 3 -> 2; comment now lists all the META/record additions made this
  cycle under v2 (ImplUsedUnits + HasInitialization, block local var decls,
  Exit value, parameter defaults, free-routine ResolvedQbeName, generic-class
  template properties, TRoutineSig vtable facts).
- COMPILER_ID stays blaise-0.12.0-SNAPSHOT+bif2 (now matches IFACE_VERSION).
- Magic-string test updated to expect 'BLAISE-IFACE 2'.

v1 .bif written by v0.11.x are rejected and recompiled from source, as intended.
2026-06-23 23:07:34 +01:00
Graeme Geldenhuys bee059143c codegen: sanitise spaces in mangled symbol names
A generic instantiated over a metaclass type argument (e.g.
TList<class of TFoo>) carries a space in its type name ('class of TFoo'),
which flowed unescaped into emitted assembler labels like
_FieldCleanup_TListEnumerator_class of TFoo — rejected by the assembler.
Map space to '_' in both the native x86_64 and QBE name manglers, alongside
the existing < > , $ @ ^ handling, so metaclass-typed generic instantiations
emit valid labels.
2026-06-23 19:45:44 +01:00
Graeme Geldenhuys c904bf321d Canonicalise type-alias generic arguments to their underlying type
Instantiating a generic with a type alias as the argument (e.g. TList<TAlias>)
produced a different instantiation identity than using the alias's underlying
type, because one path substituted the alias name literally while another
resolved it. Inside the generic body this made TList<T>.GetEnumerator's
declared result type (TListEnumerator<TAlias>) differ from the constructed
value (TListEnumerator<Underlying>), failing an internal assignment — so
TList<AnyAlias> was unusable.

Resolve each generic argument through its alias chain to the canonical
underlying-type name before forming the instantiation key (mirroring the
canonicalisation already done for array-of / pointer / set-of / class-of
element types), only when the resolved name genuinely differs (preserving
existing string/String mangled names). Adds a regression test.

3744 compiler tests (20 pre-existing toolchain failures unchanged), stdlib OK.
2026-06-23 19:36:04 +01:00
Graeme Geldenhuys 676cee0cae Modernise stdlib testing + tools: TList<String> over TStringList, drop .Free
Showcase generics + ARC across the test framework and tools. Replace
TStringList with TList<String> wherever only the basic list API was used
(Create/Add/Get/Count/Clear/Delete/IndexOf), rewriting .Strings[i] to .Get(i),
and remove manual .Free calls since Blaise reference-counts objects.

Lists genuinely needing TStringList-only API stay as TStringList: file I/O
(LoadFromFile/SaveToFile/Text), object association (AddObject/Objects[] in the
test registry and bif-coverage AST), and dup control (Duplicates). Destructors
that only freed fields are deleted.

stdlib 47 tests, compiler 3743 tests (20 pre-existing toolchain failures
unchanged), varcheck 18 regression tests all pass.
2026-06-23 19:21:09 +01:00
Graeme Geldenhuys 93d7bda739 Resolve generics in method and standalone-function return types too
Follow-up to the interface free-function fix: the remaining return-type
resolution sites — two method paths and the standalone/program-body function
path — still used FTable.FindType, so a generic return like TList<String> on
a program-level function or certain method paths failed 'Unknown return type'.
Switch them to FindTypeOrInstantiate for consistency; additive, so non-generic
returns are unaffected. 3743 compiler tests pass (20 pre-existing
toolchain-dependent failures unchanged).
2026-06-23 19:20:28 +01:00
Graeme Geldenhuys c81d65bb78 Fix generic return type on interface free functions
A unit interface-section free function returning a generic instantiation
(e.g. function MakeList: TList<string>) failed with 'Unknown return type'
because the free-function return-type resolution used FTable.FindType, which
only matches already-registered types and never instantiates a generic on
demand. Class-method return types already resolved correctly via
FindTypeOrInstantiate; this aligns the free-function paths (interface and
implementation, in both AnalyseUnit and AnalyseUnitForExport) with that.
The change is additive — FindTypeOrInstantiate tries FindType first — so
non-generic returns are unaffected. Adds an e2e regression test.
2026-06-23 18:15:52 +01:00
Graeme Geldenhuys 1e92604599 feat(debug): per-unit OPDF so incremental builds are fully debuggable
Incremental compilation is the default, but each dependency unit was compiled
to its own .o with NO OPDF debug info — only the top --source unit got an
.opdf section.  pdr could therefore not set breakpoints, show callstacks, or
inspect locals inside any dependency (RTL, stdlib, or a user's own units),
which makes the default pipeline effectively undebuggable.

Mirror FPC's multi-unit OPDF model (which pdr already reads): every object —
each unit AND the program — carries a complete, self-contained .opdf section
[32-byte header][records], and the linker concatenates the same-named sections.
The header's TotalRecords is written as 0 (stream-terminated): readers consume
records to section EOF and skip the embedded per-unit headers (and linker zero
padding).  pdr needs NO changes — it already handles exactly this layout.

Layers:
- uDebugOPDF.pas: EmitHeader writes TotalRecords=0 (PatchTotalRecords now a
  no-op); new CreateForUnit + unit-mode DoEmit that emits a unit's types /
  globals / constants / function scopes / line info from its IntfBlock+ImplBlock
  and the unit's symbol table, with NO program main scope and NO unit directory
  (directory is program-only; pdr ignores it).  Whole-program output is
  unchanged apart from TotalRecords=0.  MangledClassSym matches the native
  backend's class-symbol naming so vtable labels resolve in unit mode.
- Blaise.pas: TCompileWorker.Execute appends a unit-mode OPDF section to each
  unit's IR when --debug-opdf and the native codegen produced debug facts, so
  the per-unit .o embeds its own .opdf.  (QBE has no facts → no per-unit OPDF;
  native is the debug backend.)
- blaise.assembler.x86_64.pas: accept `.section .opdf, "aw", @progbits`
  (new eskOpdf); `.int` alias for `.long`; `.byte`/`.word` now parse
  comma-separated value lists (needed for the OPDF magic + BuildID bytes).
- blaise.elfwriter.pas: emit the .opdf section as SHF_ALLOC|SHF_WRITE PROGBITS
  so it lands in the loadable image where pdr finds it; the internal linker's
  section merger already concatenates .opdf by name and resolves its
  .quad <funclabel> relocations.

Verified: pdr breaks inside a dependency unit, shows the cross-unit callstack,
and inspects locals — under BOTH the internal toolchain (default) and external
gas/ld, incremental AND --no-incremental.  Full suite OK (3742) on both build
paths; all four fixpoints pass (normal builds unaffected).  Regression test
TestDebugOpdf_PerUnitSection_InDependencyObject asserts the dependency .o
carries an .opdf section.
2026-06-22 19:43:35 +01:00
Graeme Geldenhuys 18b9cb273b fix(semantic): cached-.bif import places methods at their authoritative vtable slot
A class compiled against a CACHED base unit (loaded from target/units .bif)
built a wrong vtable: RegisterClassMethod added a slot only for methods flagged
virtual/override and assembled the table in .bif append order, assuming that
order matched the source slot order.  Two things broke that:
  - the .bif lists newly-introduced virtuals before inherited overrides, not in
    source declaration order; and
  - a non-virtual constructor named Create occupies an implicit vtable slot
    (metaclass dispatch through a class-of reference) that the importer skipped
    entirely.

So a descendant compiled against a cached base dropped the Create slot and
shifted every later virtual up by one.  Cls.Create(args) then dispatched into
whatever method fell into Create's old index (e.g. RunTest), the constructor
body never ran, fields stayed nil — which aborted the stdlib JSON test suite
when its TTestCase base came from target/units.

Fix: RegisterClassMethod now places each method carrying a vtable slot at its
authoritative index (ASig.VTableSlot) via the new
TRecordTypeDesc.SetVTableSlotAt, which grows the vtable with empty-named
placeholders as needed.  Order-independent; covers virtual, override, and the
implicit-slot constructor.  Also fixes a latent SetUp-override resolution
regression the append-order import caused.

Regression test TestIncrementalRebuild_VirtualCtorVtableSlot: a warm-cache
rebuild of a class with a non-virtual Create + a virtual method, instantiated
through a class-of reference, asserts the constructor body actually runs.
Both backends: stdlib cached build OK (19 tests); compiler suite OK (3741);
all four fixpoints pass on both build paths.
2026-06-22 18:52:28 +01:00
Graeme Geldenhuys b074bc003a fix(codegen): float field read via implicit Self + float-returning method call
Two float-expression gaps, both fixed on native and QBE:

1. Implicit-Self float field READ (Result := FFloat inside a method).
   - native: EmitExprToXmm0 had no implicit-Self field case, so the field fell
     through to the plain VarOperand path and emitted `movsd FFloat(%rip)` — a
     load from a global symbol named after the field (undefined in per-unit .o
     codegen).  Now loads from Self + field offset.  (The store path was
     already correct; only the read was missing it.)
   - qbe: the implicit-Self field read in EmitExpr hardcoded `loadl` for any
     non-word field, so a Double/Single field loaded with loadl into an l temp
     and the following `stored`/`ret d` rejected it.  Now uses the field type's
     load opcode (loadd/loads) with the matching result type.

2. Qualified float-returning method call as a float expression
   (d := Obj.GetF()).  native EmitExprToXmm0 had no TMethodCallExpr case and
   raised "unsupported float expression form"; now dispatches the method (the
   SysV ABI leaves the float result in %xmm0).

e2e test in cp.test.e2e.sepcompile compiles a unit with a Double field read via
implicit Self to a .o and runs a consumer over it (the per-unit path that
exposed the native symbol bug), printing the float method-call result on both
backends.  Full suite OK (3740); all four fixpoints pass on both build paths.
2026-06-22 18:26:23 +01:00
Graeme Geldenhuys 6d46af93de fix(codegen): native HasClassAttribute — emit the call and the attribute RTTI
HasClassAttribute(AClass, AAttrClass) was unreliable on the native backend:
- The call was never emitted.  The builtin had no native lowering, so it fell
  through to a generic path that left the first metaclass's typeinfo address in
  %rax and read its low byte as the Boolean result — a layout-dependent false
  positive (a plain class reported as having an attribute).
- Even with the call, the attribute table was missing: native typeinfo
  hardcoded slot 7 (attrs) to nil, so a [Threaded]-marked class reported False.

Fixes (blaise.codegen.native.x86_64.pas):
- Add a native lowering for the HasClassAttribute builtin (gated on
  FC.IsBuiltinHasClassAttr): evaluate both metaclass args to typeinfo pointers
  in %rdi/%rsi, call _HasClassAttribute, normalise %al to 0/1.  Mirrors the
  QBE backend.
- Emit the class attribute RTTI table (count + one typeinfo pointer per
  attribute) as attrs_<Class> and reference it from typeinfo slot 7, nil when
  the class has no attributes.  Mirrors the QBE backend's attrs_<Class>.

This is what blaise.testing's runner uses to decide [Threaded] subprocess
dispatch; the false positive forked non-threaded suites and crashed them.

e2e test in cp.test.e2e.gaps (AssertRTLRunsOnAll, both backends): a plain
TTestCase reports False, a [Threaded] one reports True.  Full suite OK (3739);
all four fixpoints pass on both build paths.
2026-06-22 18:05:32 +01:00
Graeme Geldenhuys 271763034b fix(codegen): implicit-Self virtual calls dispatch through the vtable
An unqualified call to a virtual method from within a method body (implicit
Self, Foo(args) with no Self. prefix) was emitted as a static direct call to
the declaring class instead of a vtable dispatch — so an override was silently
skipped (wrong polymorphism), and an abstract base method link-failed
(undefined symbol <Class>_<Method>).  Self.Foo(args) already dispatched
correctly; Self. must not change dispatch semantics.

The implicit-Self call paths never reached the `if VTableSlot >= 0` vtable
branch the explicit-Self paths use.  Fixed in both backends:

native (blaise.codegen.native.x86_64.pas):
- New EmitSelfDispatch / EmitSelfDispatchVia: vtable dispatch on Self (in %rdi
  or an explicit register) when the method is virtual, else a static callq.
  Wired into the implicit-Self expr call, statement (TProcCall) call (<=6 and
  >6-slot paths), interface-sret call, and record-sret method call.
- New EmitFuncCallSret routes an implicit-Self method TFuncCallExpr that
  returns a record/jumbo-set through EmitMethodSretCall (which passes Self and
  vtable-dispatches) instead of the free-function EmitSretCall, covering local,
  var-param, global, implicit-Self-field and explicit-field assignment targets.
- These implicit-Self arg-marshalling paths also adopted EmitPopMethodArgsToRegs
  / EmitMethodOverflowLoad, so float args to an implicit-Self call are now
  passed in xmm registers too (same class of bug as the prior overflow fix).

qbe (blaise.codegen.qbe.pas): the implicit-Self statement and expression call
emitters now resolve the call target via SretMethodCallTarget (a loaded vtable
function pointer for a virtual method, else the static $symbol), matching the
sret path that was already correct.

A non-virtual unqualified call still emits a static direct call (no needless
vtable).  Tests in cp.test.e2e.classes2 cover override dispatch, abstract-base
link+dispatch, non-virtual stays static, and interface- and record-returning
virtual implicit-Self calls, all on both backends.  Full suite OK (3738); all
four fixpoints pass on both build paths.
2026-06-22 16:22:23 +01:00
Graeme Geldenhuys 73e1a14399 fix(codegen): native float args correct in the >6-slot overflow ABI
Float arguments to a call with more than 6 register slots were mis-classified
on both the caller and callee sides of the native x86-64 SysV ABI, because the
overflow machinery counted every argument as one integer register slot. With
floats interspersed (which take xmm registers, not integer ones) this put
integer args at the wrong overflow address and crashed the callee — affecting
free functions (EmitCall) as well as method/constructor/inherited calls.

Caller side (blaise.codegen.native.x86_64.pas):
- EmitCall (free functions): the overflow tail assumed exactly the first six
  contiguous stack slots were register-bound. It now records the actual
  integer-overflow slot offsets during the register-load pass and relocates
  them to a 16-byte-aligned region at the top of the allocation, so %rsp is
  aligned at the call and the lowest-indexed overflow arg is at 0(%rsp).
- New EmitMethodOverflowLoad + OverflowArgIsFloat replace the integer-only
  store/load in all five method-family overflow paths (method expr/stmt,
  constructor expr/stmt, inherited). Float arg slots are stored as their xmm
  bit pattern and loaded into xmm0.., integers into the SysV integer registers
  (Self = reg 0), with float-skipping overflow relocation. The former
  GuardNoFloatInOverflow stopgap is removed.

Callee side (BuildFrame + prologue spill loop): a by-value float parameter now
consumes an xmm slot (separate XmmIdx2) instead of an integer slot, so it no
longer pushes a following integer parameter onto the stack while the caller
passes it in an integer register. The prologue float spill bound is corrected
from 6 to 8 (SysV has 8 xmm argument registers).

Tests: e2e overflow-with-float cases for free function (cp.test.e2e.native),
and method/constructor/inherited (cp.test.e2e.classes2), all on both backends.
Full suite OK (3733); all four fixpoints pass on both build paths.
2026-06-22 14:44:11 +01:00
Graeme Geldenhuys 9607ca388e fix(codegen): native passes float method/constructor args via xmm registers
The native x86-64 backend marshalled float arguments correctly for free
functions (SysV: floats -> %xmm0..7) but not for instance-method, constructor,
or inherited calls: those push every arg through the integer-register model, so
a Double/Single argument was either a codegen crash (literal -> "unsupported
expression form TFloatLiteral") or a silent ABI miscompile (variable bits
landed in an integer register while the callee read %xmm).

Fix, all in blaise.codegen.native.x86_64.pas:
- EmitMethodArgPush: a by-value float scalar arg is now materialised via
  EmitExprToXmm0 (handles literals through the .LF constant path), width-
  adjusted to the param type, and pushed as its 8-byte bit pattern.
- New EmitPopMethodArgsToRegs: replaces the integer-only `popq SysVArg64(I+1)`
  loops in the <=6-slot path of EmitMethodCallExpr, EmitMethodCallStmt, the
  constructor arg loop, EmitInheritedCallSeq, and the sret method path. It
  classifies each slot and routes floats to %xmm0.. and integers to the
  integer registers, advancing independent counters per SysV (Self still
  occupies integer slot 0/1).
- The rare >6-register-slot overflow path remains integer-only; a by-value
  float there now raises a clear "not yet supported" error (GuardNoFloatInOverflow)
  instead of crashing cryptically or silently miscompiling.

Tests: 6 e2e cases in cp.test.e2e.classes2 (method/ctor x literal/variable,
mixed int+float+string interleaving, multiple floats across xmm0/xmm1) run on
both backends, plus an IR assertion in cp.test.nativeconstarg that the call
site emits movsd into %xmm0.
2026-06-22 14:16:34 +01:00
Graeme Geldenhuys ef19c01cb7 fix(codegen): QBE stores float to implicit-Self field with stored/stores
Assigning a float value to a class field via an implicit Self reference
(e.g. `V := d` in a method/constructor where V: Double) emitted a hardcoded
`storel` on a `d`/`s` SSA value, which QBE rejects ("invalid type for first
operand in storel"). The implicit-Self field-store path in EmitAssignment
fell through to a literal `storel` instead of StoreInstrFor(field type), and
lacked the d<->s width coercion the EmitFieldAssignment path already has.

Use StoreInstrFor(ISFld.TypeDesc) (-> stored/stores for tyDouble/tySingle) and
add the Single<->Double truncd/exts coercion before the store. Surfaced by the
new float-constructor e2e tests, which run on both backends.
2026-06-22 14:16:21 +01:00
Graeme Geldenhuys 74da3eadcc test(bif-coverage): extend drift guard to .bif interface types
bif-coverage verified that every AST node field round-trips through the .bif
encoder/decoder, but the .bif interface-container types (TRoutineSig,
TUnitInterface, TMethodParam, TConstEntry, TVarEntry) were hand-serialised in
WriteMeta/EncodeMethodSig/etc. with no drift guard. Every cached-rebuild bug
just fixed was a serialised field on one of those types dropped from one side
of the round-trip — invisible to the tool, surfacing only as a runtime
miscompile.

Generalise the class scanner to ScanClassFile(path, names, objs, allowList);
add ScanInterfaceTypes() over an allow-list of the container types in
uUnitInterface.pas. Split uUnitInterfaceIO.pas into encoder-side and
decoder-side text and assert each serialised interface-type field's identifier
appears in both — the same looseness as the AST mechanism. Status file gains
the interface-type entries (serialise/safe); mutator-repopulated owning
collections are { no-bif }-exempt (their element data round-trips through the
per-entry encoders).

Negative test confirmed: dropping ImplUsedUnits/HasInitialization/VTableSlot
from either side is now reported as a gap with exit 1. Clean tree exits 0.
2026-06-22 12:38:28 +01:00
Graeme Geldenhuys 915077d7c8 fix(incremental): make cached-interface rebuild lossless and coherent
Incremental compilation (per-unit .o + embedded .bif, the default) produced
silently corrupt binaries when rebuilding into a populated --unit-cache: the
clean build worked, but any rebuild that loaded a unit from its cached .bif
instead of source dropped or mishandled state the in-memory path carried.
Root cause is uniform across every symptom: the .bif round-trip was not a
complete projection of the unit interface, and the cached-load path in the
loader/driver diverged from the source-load path.

.bif format bumped to version 3 (IFACE_VERSION; CompilerId +bif tag) so stale
caches are cleanly rejected and recompiled from source.

Fixes (each was a field/decision present on the source path, absent on the
cached path):

- Loader: a unit's implementation-section `uses` were never recorded in the
  .bif, so on rebuild the impl-only dependency's object was dropped from the
  link (undefined symbol). Record ImplUsedUnits in the .bif and collect those
  objects for LINK ONLY (CollectLinkOnlyObject) — not semantic import, which
  would break the leaf-first iface import on interface/impl-use cycles.

- Native codegen: a global var owned by a cached unit was re-defined by the
  recompiled top program (`multiple definition of GTarget`). The program now
  references imported-unit globals as external (FImportedUnits / IsImportedGlobal
  gate in EmitDataSection); NoteDepInitUnit records every imported unit.

- Unit initialization: <Unit>_init calls for cached deps were never emitted, so
  initialization sections silently never ran. Record HasInitialization in the
  .bif and note prebuilt-iface inits in dependency order at program codegen.

- SynthesiseMethodDecl dropped VTableSlot/IsVirtual/IsOverride from cached
  method imports, degrading virtual dispatch to direct calls into the abstract
  base (SIGABRT at run time). Propagate them.

- Generic-instance field/param types now resolve via the analyser's generic-
  aware resolver on import (ResolveImportedTypeName); generic instantiation
  during import no longer nil-derefs FProg/FCurrentUnit (pending-instance flush).

- .bif round-trip completeness: serialise block-local var decls, Exit(value)
  return values, parameter default values, free-routine ResolvedQbeName,
  generic-class template properties, and fix implements-interface name
  stripping (strip to the last dot, not the first).

Adds TestIncrementalRebuild_ImplOnlyUses_LinksDependency (e2e) and updates the
magic/version assertion. Full suite OK (3722 tests); QBE, native, and
internal-assembler fixpoints pass; clean and warm-cache rebuilds both produce
correct binaries.
2026-06-22 12:23:28 +01:00
Graeme Geldenhuys baf5079d6d fix(parser,codegen): Self.Field[Index].SubField := ... parse and codegen
The parser's Ident.Ident[Index] path unconditionally expected ']:=' after
the subscript, rejecting chains like Self.FStack[0].Count := value. Add a
chain loop after ']' that handles '.Field :=', '.Method()', and further
'[Index]' suffixes.

Additionally, the QBE backend's EmitInstancePtr did not handle
TFieldAccessExpr nodes with IsArrayAccess set — it returned the field slot
address instead of the dereferenced array element pointer. Delegate to
EmitExpr when IsArrayAccess is true so the dynarray pointer is loaded and
the element offset is computed correctly.
2026-06-22 01:38:27 +01:00
Graeme Geldenhuys 57c1699d32 fix(test): prefer fp_blaise2 over fp_blaise3 for internal-asm e2e tests
fp_blaise3 predates the movq %xmm0,%rax encoding fix, so
TestFormatFloatArg always failed when fp_blaise3 was present.
2026-06-22 00:15:07 +01:00
Graeme Geldenhuys 05892561fe perf(linker): fix O(n²) string concatenation in internal linker (80s → 3s)
LkZeros built zero-padding strings by appending one byte at a time
(Result := Result + Chr(0)), which for a ~2.7 MB executable produced
trillions of byte copies. Replace with SetLength + PChar fill.

Also fix LkLE to pre-allocate via SetLength instead of concatenating
per-byte, and add an early-exit guard to LkCopyInto.
2026-06-21 23:53:36 +01:00
Andrew Haines 9d876b46aa feat(types): resolve qualified type names UnitName.TypeName
A type written as 'UnitName.TypeName' (the unit qualifier may itself be
dotted, e.g. System.SysUtils.TFormatSettings) was a parse error: after
reading the leading identifier ParseTypeName only accepted a generic
'<...>' argument list, not a '.' continuation.

Parse the full dotted path into the type name, then resolve it by its
final component — a type is always a single identifier; the leading
components name a unit already loaded by the 'uses' clause. The strip is
centralised in TSymbolTable.FindType (via UnitQualifierTail) so every
type position benefits uniformly — var/field decls, parameters, and the
several routine return-type sites — and FindTypeOrInstantiate strips the
qualifier off a generic instance (Unit.TList<Integer>) before
instantiation. Plain dotted identifier paths only: the bracket/space/
caret/angle encodings for array, pointer, set and generic types are left
untouched.

Tested: dotted-name capture in the parser, semantic resolution of a
qualified builtin (System.Integer), and end-to-end compile+run of a
program using a cross-unit qualified type.

(cherry picked from commit 7e9c3d1f61d42769bc3341195220a25190637e15)
2026-06-21 13:35:37 +01:00
Graeme Geldenhuys cf994dcca4 fix(parser): parse nested-proc-first bodies in unit impl routines (Andrew 82e770ac)
A unit implementation-section routine whose body opens with a nested
procedure/function declaration (before any var/const/type section) was
mis-parsed: ParseUnit called ParseMethodDecl without ACanHaveNestedProcs, so a
leading 'procedure'/'function' was treated as the next top-level decl and
parsing failed at the outer 'begin' ("Expected 'end' but got 'begin'").

Pass ACanHaveNestedProcs=True for the two impl-section ParseMethodDecl calls.
The IsForward guard already present in ParseMethodDecl (our commit 33db0f6)
keeps a forward decl from claiming the following sibling as its nested body, so
only the call-site change was needed — Andrew's branch independently added the
same forward-flag mechanism, which we already have.

Note: this is a PARSER fix (the unit body now parses correctly).  Resolving a
CALL to such a nested procedure is a separate, pre-existing semantic-pass gap
that affects program routines too (Inner() reports "Cannot find declaration");
it is out of scope here.  Andrew's tests are likewise parse-level.

Tests: cp.test.units.pas parse-level tests (outer routine is one decl with a
body holding the nested proc).

Cherry-pick policy: the forward-flag half was a duplicate of our work and
skipped; the ACanHaveNestedProcs threading + tests are taken.

Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite green
on both the QBE-built and native-built runners (3717 tests).
2026-06-21 13:35:37 +01:00
Graeme Geldenhuys d91d9ee65a feat(lexer): conditional compilation with predefined BLAISE (issue #131)
Implements real symbol-presence conditional compilation in the lexer.
Previously {$IFDEF} was hardcoded false (always took {$ELSE}) and
{$DEFINE}/{$UNDEF} were silently consumed, with no define table and no
command-line flag.

- A case-insensitive define table on TLexer.  {$DEFINE sym} / {$UNDEF sym}
  add and remove symbols; {$IFDEF sym} keeps its body when defined (and
  {$IFNDEF sym} when not), with an optional {$ELSE} and a closing {$ENDIF}.
  IFDEF/IFNDEF blocks nest (the existing depth-tracking skip helpers are
  reused, now driven by the real truth value).
- Predefined symbols, seeded in every lexer: BLAISE (the headline
  cross-compiler use case — {$IFDEF BLAISE} ... {$ELSE} ... {$ENDIF}) plus
  the target CPU/OS symbols CPUX86_64, CPUAMD64, LINUX, UNIX.  No version
  macro yet.
- A -d / --define <sym> command-line flag (FPC -dSYM / Delphi -D), carried
  on TFrontEndOpts and threaded to the program's lexer AND to every unit the
  TUnitLoader compiles, so {$IFDEF} resolves consistently across the program
  and its units.

Tests:
- cp.test.lexer.pas: predefined BLAISE keeps the body, undefined takes ELSE,
  IFNDEF, DEFINE-then-IFDEF, UNDEF-then-IFDEF, and AddDefine (the -d path).
- cp.test.e2e.misc.pas (dual-backend): the {$IFDEF BLAISE} cross-compiler
  pattern, and a combined DEFINE/UNDEF/IFNDEF/CPU-OS/nested program.

Docs: grammar.ebnf documents the directives and predefines; language-
rationale.adoc records the decision (symbol-presence only, no {$IF} expr
form yet; BLAISE + CPU/OS predefined, no version macro) and alternatives.

Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite
green on both the QBE-built and native-built runners (3715 tests).
2026-06-20 20:16:16 +01:00
Graeme Geldenhuys 2a6d0c5912 fix(assembler): encode movq/movd between an XMM and a GP register (SSE MOVQ)
The internal assembler routed every `movq`/`movd` to the integer mov encoder,
which reads both operands as general-purpose registers.  So `movq %xmm0, %rax`
was emitted as `mov %rax,%rax` (48 89 C0) — register 0 in both fields — silently
discarding the XMM value.

The native backend emits `movq %xmm0, %rax` to box a Double's raw bits into an
array-of-const (the Format() argument path) and to spill float bit patterns, so
ANY float passed to Format() under the internal assembler (now the default
toolchain) arrived as garbage and printed 0.0000.  A regression vs v0.11.0,
which used the external assembler.  The e2e test harness assembles native via
cc, so only the internal-assembler suite exercises this path.

Fix: `movq`/`movd` with an XMM operand is an SSE move and is routed to a new
EncodeMovqXmm:
  gp/mem -> xmm :  66 [REX.W] 0F 6E /r
  xmm -> gp/mem :  66 [REX.W] 0F 7E /r
with the XMM operand as the ModRM.reg field and REX.W set for movq (64-bit),
clear for movd (32-bit).

Tests:
- cp.test.assembler.pas TAsmEncodingTests: byte-level encoding of
  `movq %xmm0,%rax` (66 48 0F 7E C0, and asserts it is NOT 48 89 C0) and
  `movq %rax,%xmm0` (66 48 0F 6E C0).
- TInternalAsmE2ETests.TestFormatFloatArg: Format('%.4f',[1.25]) through the
  internal assembler prints 1.2500.

Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite green
on both the QBE-built and native-built runners (3707 tests).
2026-06-20 19:09:56 +01:00
Graeme Geldenhuys c5d49ba03e fix: 64-bit integer constants — no truncation, accept full bit pattern (issue #133)
Two related defects in integer constant handling:

- A large hex literal that needs more than 32 bits (e.g. $080808080808 =
  8830587504648) was silently TRUNCATED to 32 bits (printed 134744072).  An
  untyped integer constant was unconditionally typed as Integer regardless of
  its magnitude, so codegen treated it as a 32-bit word.

- A literal above High(Int64) (e.g. $8080808080808080) was REJECTED at parse
  time with "Integer literal exceeds Int64 range", even though it is a valid
  64-bit bit pattern and is accepted by FPC/Delphi.

Fixes:

- uParser: the bare-integer-literal const path now uses ParseIntOrUInt64Literal
  (which already existed for the tolerant case) instead of ParseIntLiteral, so a
  value in the UInt64 range is captured as a bit pattern and flagged via a new
  TConstDecl.IsUInt64.

- uSemantic (AnalyseConstDecls): an untyped integer constant now picks its type
  by magnitude — Integer when it fits in signed 32-bit, Int64 when larger, and
  UInt64 when the value exceeds High(Int64) (matching Delphi, which prints
  $8080808080808080 as 9259542123273814144).  A typed const (e.g. A: Int64 =
  $8080808080808080) keeps its declared type and stores the bit pattern, so it
  prints -9187201950435737472.

Tests (cp.test.e2e.misc.pas, dual-backend): large hex not truncated, typed
Int64 bit pattern, typed UInt64 bit pattern, and untyped-above-Int64 widening
to UInt64.

Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite green
on both the QBE-built and native-built runners (3704 tests).
2026-06-20 18:30:12 +01:00
Graeme Geldenhuys fe5460f79b fix: var open-array parameter element read and write (issue #130 bug5)
A `var` open-array parameter was broken three ways; a `const` open array with
the same body worked, so the defects were specific to the var/open-array path.

- Semantic (uSemantic, AnalyseStaticSubscriptAssign): writing an element of an
  open-array parameter raised "'a' is not a static array or dynamic array" —
  there was no tyOpenArray case.  Added one: a var open array allows element
  writes; a const open array is rejected with a clear "declare it 'var'"
  message.

- QBE codegen (element READ): an open-array ident value-read fell into the
  scalar var-param branch, which dereferences the slot twice.  An open-array
  param slot already holds the data pointer directly (for both const and var),
  so the second load read garbage and segfaulted.  Added a tyOpenArray case
  that loads the data pointer once.

- Native codegen (element WRITE): tyOpenArray fell through to the static-array
  store and raised "static subscript assign on non-static-array".  Open arrays
  now share the dynamic-array element-write path, with the var-param extra
  deref suppressed (the open-array slot is already the data pointer).

Tests:
- cp.test.e2e.openarray.pas (dual-backend): element read (the issue repro),
  element write mutating caller storage, and read-modify-write mixing var and
  const open arrays.
- cp.test.semantic.pas: var open-array element write is accepted; const
  open-array element write is rejected.

Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite green
on both the QBE-built and native-built runners (3700 tests).
2026-06-20 17:29:47 +01:00
Graeme Geldenhuys 56e73c8ce4 fix: class recognised as implementing an interface inherited from an ancestor (issue #130 bug3)
A class that inherits an interface from its ancestor (the ancestor declares
the interface; the descendant does not re-list it) was not recognised as
implementing it.  `g := TLoud.Create` where TLoud < TPerson(IGreeter) failed
with "Type mismatch", and after the semantic gate was opened the codegen
referenced a non-existent itab.

Three coordinated fixes:

- uSemantic (CheckTypesMatch): the class->interface compatibility check
  scanned only the class's own ImplementsList.  It now walks the whole class
  parent chain, so a descendant inherits its ancestors' interface
  implementations.

- blaise.codegen.qbe / blaise.codegen.native (EmitInterfaceDefs): itab/impllist
  generation likewise only considered a class's own ImplementsList, so no itab
  was emitted for an inherited interface.  Both backends now:
    * skip a class only when neither it nor any ancestor implements an
      interface (and, on native, point the typeinfo at the impllist on the same
      condition);
    * collect implemented interfaces by walking the class parent chain;
    * resolve each itab method ref via the vtable slot's ImplName, so an
      overridden method points at the descendant's body and an inherited
      (non-overridden) method at the ancestor's — uniformly correct across
      arbitrary inheritance depth.

Tests (cp.test.e2e.interfaces.pas, dual-backend via AssertRunsOnAll): the
exact issue repro (descendant assigned to an interface var, dispatching to its
override), and a three-level chain through an interface parameter mixing an
overridden method with an inherited non-overridden one.

Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite green
on both the QBE-built and native-built runners (3695 tests).
2026-06-20 16:56:33 +01:00