Commit graph

507 commits

Author SHA1 Message Date
Graeme Geldenhuys 216e8704f5 feat(generics): support generic records (monomorphization)
Extend the generics system to support record types alongside classes and
interfaces.  Generic records use the same <T> syntax and monomorphization
strategy: each instantiation (e.g. TMyVal<Integer>) creates a concrete
TRecordTypeDesc with substituted field types and re-analysed method bodies.

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

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

Platform constant values are now derived from GTarget (the compilation
target) via TargetLineEnding/TargetDirectorySeparator/TargetPathSeparator
in blaise.codegen.target, so cross-compilation produces the correct
values without conditional compilation.
2026-06-05 17:01:04 +01:00
Graeme Geldenhuys fe63b2559d fix(arc): release owned RHS when storing to [Unretained] class field
Assigning a function return (owned +1 ref) to an [Unretained] field
leaked the temporary — the field borrows but nobody consumed the +1.
Both EmitAssignment (implicit-Self path) and EmitFieldAssignment now
call _ClassRelease on the value when ExprOwnsRef indicates ownership.
2026-06-05 15:17:47 +01:00
Graeme Geldenhuys 8f6cf04481 fix(codegen): address-of operator on array field elements (@R.Arr[I])
The parser absorbs [I] in R.Arr[I] into TFieldAccessExpr.PropIndexExpr,
which the semantic pass and codegen only handled for indexed properties
and string char access.  Array-typed fields (dynamic, static, open) were
left unresolved, producing a type mismatch ("^array of T" instead of
"^ElementType").

Add IsArrayAccess flag to TFieldAccessExpr, resolve it in all three
PropIndexExpr sites in uSemantic (Base-chained, RecordName-leaf, and
trailing field path), and emit the element-address computation in
uCodeGenQBE for both EmitExpr and EmitAddrOfExpr.

Native backend has TODO markers for the new flag.
2026-06-05 15:02:15 +01:00
Graeme Geldenhuys cd136b63d0 feat(native): interface parameters in method/constructor/inherited calls
Interface parameters are fat pointers (obj + itab = 2 × 8 bytes) that
consume two consecutive integer register slots in the x86-64 SysV ABI.
Previously the native backend rejected any function with an interface-typed
parameter with "unsupported parameter type".

Changes in blaise.codegen.native.x86_64.pas:

BuildFrame: interface params now get a 16-byte slot (same as interface
locals) and advance IntIdx twice, matching the two-register convention.

EmitFunctionDef prologue: spill both incoming registers into the 16-byte
slot — obj register into slot base (VarOperand), itab register into
base+8 (IntfItabOperand).

Type validation gate: add tyInterface to the accepted parameter kinds.

EmitMethodArgPush: new interface branch pushes obj first then itab (pop
loop runs high-to-low so itab lands in the higher register).  Handles
TIdentExpr (load from _obj/_itab slots), TAsExpr (runtime _GetItab),
implicit-Self field (load from Self+FieldOffset), and class expressions
(static itab symbol).

CountArgSlots: new helper that counts register slots rather than logical
argument positions; interface and open-array params each count as 2.

EmitMethodCallExpr: add IsConstructorCall path (TMethodCallExpr) with
full arg loop.  Pop loop now uses CountArgSlots.

EmitMethodCallStmt / EmitInheritedCall: pop loops use CountArgSlots;
inherited call removes the "not yet supported" error.

EmitInterfaceCall / EmitInterfaceFieldCall: arg loops push two values for
interface-typed arguments; slot-count pop replaces arg-count pop.

EmitCall (standalone function): slot-count detection and push loop both
handle interface params as 2-slot args.

Changes in uCodeGenQBE.pas:

All arg loops that call InterfaceArgFragment or EmitInterfaceExprPair
now intercept the case where the actual argument is a class expression
(ResolvedType.Kind = tyClass) and emit the static itab symbol directly,
instead of routing through EmitInterfaceExprPair which only handles
TIdentExpr and TAsExpr.  Affected sites: EmitMethodCall, EmitInheritedCall,
EmitProcCall (two arg loops).

Tests: five new AssertRunsOnBoth cases in cp.test.e2e.native.pas covering
interface arg to a standalone procedure, class method, constructor,
inherited call, and a class expression passed directly as an interface
parameter.  TestRun_Native_InterfaceField_ShadowsGlobal upgraded from
QBE-only to AssertRunsOnBoth now that interface params work on native.
2026-06-05 13:30:46 +01:00
Graeme Geldenhuys 4a2c54639d fix(semantic): class fields shadow same-named program globals in method bodies
Inside a method, an unqualified name should resolve as:
  local/param > implicit Self.field > unit/program global

The semantic analyser (AnalyseAssignment, AnalyseProcCall, and the
TIdentExpr read path) only fell back to the class-field lookup when
the symbol table returned nil.  When a same-named program-level global
existed, Lookup() returned it — bypassing the field altogether.

Fix: in all three resolution sites, also try the class-field path when
the symbol found is a global (IsGlobal=true), mirroring Pascal's
field-shadowing rules.

Also fix the implicit-Self interface-field branch of AnalyseProcCall: it
was using ResolveMethodOverload for interface-typed fields, which doesn't
know about interface method descriptors.  Now uses HasMethod + interface
dispatch (nil ResolvedMethod), matching the non-implicit path.

QBE codegen fixes:
- EmitAssignment/ImplicitSelfField: add tyInterface case — stores both
  fat-pointer slots (obj+8, itab+8) with ARC, via new
  EmitInterfaceToFieldSlots helper.
- EmitProcCall interface dispatch: when IsImplicitSelf, load obj/itab
  from Self+FieldOffset rather than from %_var_ObjectName slots.
- EmitExpr constructor call arg loop: detect tyInterface params and emit
  the fat-pointer pair (obj + static itab) instead of a single word.
- EmitInterfaceExprPair: handle implicit-Self TIdentExpr (loads from
  Self+FieldOffset) and class expressions (runtime _GetItab lookup).

Native x86_64 backend fixes:
- EmitInterfaceAssign: handle ImplicitSelfField — compute Self+offset
  into %r15 (callee-saved) so the fat-pointer stores survive intervening
  _ClassAddRef / _ClassRelease calls.
- New EmitInterfaceFieldCall: dispatches interface methods through a class
  field (Self+FieldOffset) rather than a variable slot.
- EmitMethodCallStmt interface dispatch: route through EmitInterfaceFieldCall
  when IsImplicitSelf.

Regression tests: cp.test.interfaces TestSemantic_InterfaceField_ShadowsGlobal_OK
(unit), cp.test.e2e.classes2 TestRun_InterfaceField_ShadowsGlobal_Dispatches (QBE
e2e), cp.test.e2e.native TestRun_Native_InterfaceField_ShadowsGlobal (QBE path only
— native backend interface-arg passing is a separate open item, documented in
bugs.txt).

Closes issue #64.
2026-06-05 12:46:52 +01:00
Graeme Geldenhuys 5dbb3a2584 test(arc): guard Pointer-rvalue-to-class-local lifetime
Bring in Andrew Haines's regression test (from his bf8d94a) that assigns a
Pointer-returning rvalue (TObjectList.Items[0]) into a TObject local and asserts
the list-held object survives the borrowing procedure (Destroy counter 0 after
Borrow, 1 after L.Free).

Only the test is brought in, not the accompanying code change: bf8d94a switched
three assignment ARC dispatch sites from RHS-type to LHS-type to fix a net -1
imbalance, but that bug does not reproduce on our master — our EmitAssignment
already retains when a Pointer rvalue (including an explicit TThing(P) cast) is
stored into a class-typed local, so all the repro variants are balanced. The
test passes as-is and now guards that behaviour from regressing.

Full suite 0 failures. (Test-only change — no compiler source touched, so
fixpoint is unaffected.)
2026-06-05 08:14:05 +01:00
Graeme Geldenhuys a5f01c1cd1 perf(arc): elide const-param retain/release, retain transient args
Reapply the const-parameter ARC elision that was reverted in 03b59e8,
now paired with the caller-side transient retain that makes it sound.

The four entry/exit ARC loops skip IsConstParam again (a const parameter's
object is kept alive by the caller for the whole call, so the callee needs no
_StringAddRef/_StringRelease or _ClassAddRef/_ClassRelease pair). The earlier
revert was because that premise fails for a TEMPORARY bound to a const param
(e.g. `Use(A + ' ' + B)`): the concat result is +0, its only reference is the
argument slot, and with the callee retain elided it was freed mid-call.

The fix (Andrew Haines, cherry-picked from the llvm branch — commits 00fcf41 +
e94e70544) adds the missing reference at the CALL SITE: EnsureConstStringRef
emits _StringAddRef before the call and ReleaseConstStringArgs emits
_StringRelease after, for each value-mode argument to a const-string parameter.
The pair is a no-op on immortal literals and nets to zero on owned strings, so
the overhead falls only on the transients that actually need it.

Tests: restored the elision IR tests (string + interface const params),
Andrew's caller-retains-transient IR tests and valgrind e2e
(TestRun_ConstStringParam_TransientRetained_Valgrind), alongside the existing
TestRun_ConstStringTemp_StaysAlive_Valgrind. docs/future-improvements.adoc marks
the optimisation shipped. Full suite 0 failures; FIXPOINT_OK; the original
metaclass-ref crash (`C := TFoo`) compiles cleanly.
2026-06-05 08:08:19 +01:00
Graeme Geldenhuys 988bb24d35 feat(native): M8 — var/out arguments to method calls
The native method-call paths (EmitMethodCallStmt, EmitMethodCallExpr,
EmitInheritedCall) pushed every argument by value, so a `var`/`out` parameter
to a method received a copy and the caller's variable was not mutated — silently
wrong (e.g. `F.Bump(N)` left N unchanged). The standalone EmitCall path already
passed var args by address; the method paths did not.

Add EmitMethodArgPush: for a var/out param it pushes the argument's address
(leaq for a local/global, or the stored pointer when the arg is itself a var
param), otherwise the value. Use it in all three method-call paths. This also
lets EmitInheritedCall accept var args (the earlier "not yet supported" guard is
now only for interface args, which the native call ABI still cannot pass).

Two AssertRunsOnBoth e2e tests (a single var param mutated; a two-var-param
swap) match the QBE oracle. Native suite 95 tests; full suite 0 failures;
FIXPOINT_OK.
2026-06-05 07:49:41 +01:00
Graeme Geldenhuys d53226ed7b feat(native): M8 — inherited calls (TInheritedCallStmt)
The native backend rejected `inherited Method[(args)]` with "unsupported
statement TInheritedCallStmt".  Add EmitInheritedCall: a direct static call to
the parent method (bypassing the vtable, as inherited must), with Self taken
from the current method's Self slot.  Value arguments are pushed/popped into the
SysV registers as for a regular method call; a value-returning parent stores its
result into the Result slot so `inherited F;` as a statement seeds Result.

var/out and interface arguments to an inherited call raise a clear
"not yet supported" — the native call ABI does not yet pass arguments by address
or as interface fat pointers, so failing loudly beats passing the wrong thing.

Two AssertRunsOnBoth e2e tests (Inherited_Proc; Inherited_FuncSetsResult, which
exercises the Result-seeding path) match the QBE oracle.  Native suite 93 tests;
full suite 0 failures; FIXPOINT_OK.
2026-06-05 07:45:05 +01:00
Graeme Geldenhuys 03b59e8ff0 Revert "perf(arc): elide retain/release for const string and class params"
This reverts commit 5a5b5d4. The optimisation elided the callee-side
ARC retain/release for const string/class/interface value params on the
premise that the caller keeps the argument alive for the whole call. That
premise fails for a TEMPORARY bound to a const param (e.g. `Use(A + ' ' + B)`):
the concatenation result's only reference is the argument slot, so without the
callee-side retain its refcount hits zero at the call boundary and it is freed
before the callee reads it — a use-after-free.

This bit the RTL hardest: `_StringCopy` / `StrHead` take `const string` params
and are called with built-at-runtime temporaries, so the emitted RTL was
miscompiled. Under self-hosting the defect is self-reproducing and only
manifests at the SECOND generation (the compiler that emits the broken RTL is
itself fine), which is why a one-step fixpoint did not expose it and the
compiler's own sources did not reliably trigger it. The symptom was a
deterministic crash compiling any program that uses a metaclass reference
(`C := TFoo`) or HasClassAttribute — which is why TestRunner (via
blaise.testing.runner.text) could not be built, blocking the whole suite.

The original change's valgrind e2e test passed only because it bound a string
LITERAL (immortal) to the const param, not a temporary.

Removed the now-invalid IR/e2e tests that asserted the elided behaviour
(string const params, and the interface-const variant from 088d12f which
relied on this commit's IsConstParam guard). Added
TestRun_ConstStringTemp_StaysAlive_Valgrind, which passes a concatenation
result as a const string param and reads it in the callee — the exact case the
optimisation broke. docs/future-improvements.adoc records how to re-attempt the
elision safely (condition on the argument, not the parameter; retain temporaries
either caller- or callee-side).

Verified: stage-2 build clean, TestRunner builds, full suite 0 failures,
FIXPOINT_OK.
2026-06-04 23:21:25 +01:00
Graeme Geldenhuys 4df81d49d0 fix(codegen): pass var/out params by reference in inherited calls
EmitInheritedCall's argument loop had no var-param branch: it always
evaluated the argument and passed its loaded value, so `inherited Foo(X)`
where the parent method declares `var X` passed a `w` value where the callee
expects an `l` address.  The parent then dereferenced a small integer as a
pointer and the program segfaulted.

Pass EmitLValueAddr(arg) as an `l` for var/out params, matching every other
method-dispatch arg loop.

e2e test: a TDer.Bump override that calls `inherited Bump(X)` on a `var X`
param must mutate the caller's variable (5 -> 16). FIXPOINT_OK.
2026-06-04 17:41:15 +01:00
Graeme Geldenhuys 790fa4c3f5 fix(codegen): pass a method interface param as a fat pointer
A *method* (OwnerTypeName<>'') taking a by-value interface parameter was
emitted as a single `w` slot instead of the two-slot fat pointer (obj +
itab) the standalone-routine path already uses.  EmitMethodDef's signature
and EmitParamAllocs both fell into the generic `else`, so the callee's
`storel %_par_X` was invalid QBE ("invalid type for first operand") and the
method failed to compile; dispatch inside it would also have read an
undefined %_var_X_obj.

Split the interface param into `l %_par_X_obj, l %_par_X_itab` in the method
signature and alloc both `%_var_X_obj`/`%_var_X_itab` local slots, mirroring
the standalone-routine path.  The call sites must pass both halves too: add
an InterfaceArgFragment helper (wraps EmitInterfaceExprPair) and use it in
the five method-call arg loops (sret method-call expr, non-sret method-call
expr, the value-returning and statement method dispatch, and inherited
calls) so an interface argument is forwarded as the obj+itab pair.

Latent, not a regression: the compiler's own source declares no method with
a by-value interface param, so the fixpoint stayed clean; it bit user code
only.  Two IR unit tests (method signature splits the param; the call site
passes both slots) and one e2e test (a class method taking an interface and
dispatching through it returns the right value).  Full interface + ARC
suites green; FIXPOINT_OK.
2026-06-04 17:39:17 +01:00
Graeme Geldenhuys 8d648ef3d9 feat(native): M7f complete — interface dispatch through itab
Interface vars become fat pointers: locals a contiguous 16-byte slot
(obj@+0, itab@+8); globals two .data labels Name_obj/Name_itab, matching
the QBE backend's $Name_obj/$Name_itab. AddSlot reserves 16 bytes for
tyInterface; IntfObjOperand/IntfItabOperand return the two halves for
locals (rbp-relative) or globals (RIP labels).

EmitInterfaceCall lowers a dispatch: push args, load obj->%r10 and
itab->%rax, index the itab by MethodIndex*8 (a flat method-pointer array,
no leading typeinfo slot unlike a vtable), load the code ptr->%r11, pop
args into %rsi.. (shifted for Self in %rdi), then callq *%r11. Wired into
EmitExprToEax (TFieldAccessExpr.IsInterfaceCall = zero-arg),
EmitMethodCallExpr and EmitMethodCallStmt (ResolvedClassType = tyInterface,
with args).

EmitInterfaceAssign handles four RHS forms, strong refs only: := nil
(release obj, zero both slots); class->interface (static itab_Class_Intf,
addref/release obj); interface->interface copy (load src obj+itab,
addref/release, store both); T as IFoo (_GetItab(obj, typeinfo_Intf); nil
result -> _Raise_InvalidCast; else addref/release/store). ARC operands are
kept on the stack across the AddRef/Release calls, so no callee-saved
register is relied upon.

EmitInterfaceDefs emits typeinfo_<Intf> (.quad 0 identity token), flat
itab_<Class>_<Intf> method-pointer arrays (abstract slots ->
_AbstractMethodError), and NULL-terminated impllist_<Class> (typeinfo,itab)
pairs — mirrors the QBE EmitInterfaceDefs. Run after EmitClassSection so the
method symbols and class-name strings it references exist. EmitDataSection
emits Name_obj/Name_itab for interface globals; the function prologue
zero-inits string and interface locals (both fat-pointer halves) so the
first assignment's release-old step sees nil; the epilogue releases
interface locals via _ClassRelease on the obj slot.

6 new AssertRunsOnBoth e2e tests (ZeroArg, Arg, Proc, IntfToIntfCopy,
AsCast, NilClear) — all pass on QBE and native; valgrind clean on the
as-cast program. FIXPOINT_OK.

Weak interface refs (_WeakAssign/_WeakClear) deferred. Pointer size is the
literal 8 here, matching the rest of this x86_64-only unit; i386/arm64 will
be separate TNativeBackend subclasses.
2026-06-04 17:21:12 +01:00
Graeme Geldenhuys 088d12f647 fix(arc): retain/release by-value interface params in the callee
By-value interface params were never retained on entry or released on exit:
the QBE entry/exit ARC loops only handled tyString and tyClass. A callee
could therefore drop the caller's sole reference mid-call and then use the
param as a freed object. Add a tyInterface branch to all four loops,
AddRef/Release on the object slot of the fat pointer (the itab is static).

const and var interface params correctly emit no retain/release — the
const skip is the IsConstParam guard, and var params never reached the
class/interface branches.

Two IR unit tests (value param retained; const/var not) and one e2e
Valgrind test that drops the caller's reference mid-call and reuses the
param, proving the retain prevents a use-after-free and the release
balances it.

The native x86_64 backend still does not retain value params at all; its
TODO is updated to cover interfaces alongside string/class.
2026-06-04 09:43:07 +01:00
Graeme Geldenhuys 5a5b5d405f perf(arc): elide retain/release for const string and class params
A const parameter guarantees the caller keeps the argument alive for the
whole call, so the callee-side _StringAddRef/_StringRelease and
_ClassAddRef/_ClassRelease pair is redundant. Skip it in all four ARC
entry/exit loops (method + standalone routine) by adding IsConstParam to
the existing IsVarParam/IsOpenArray guard.

The native x86_64 backend does not yet retain/release value params at all,
so there is nothing to elide there today; a TODO marks that it must respect
IsConstParam when that retain/release is added.

Covered by two IR unit tests and one e2e Valgrind test confirming the
elision is balanced (no leak, no use-after-free).
2026-06-04 09:37:56 +01:00
Graeme Geldenhuys 16c4f5b690 fix(fixpoint): drive runtime build with stage-1 binary, not compiler/target/blaise
fixpoint.sh's runtime build (step 1) relied on the runtime Makefile's default
BLAISE=../compiler/target/blaise. After scripts/rolling-bootstrap.sh — which
installs only to releases/ and leaves the live compiler/target/ empty — that
path does not exist, so `cd runtime && make` failed with
"compiler/target/blaise: No such file or directory" (RUNTIME_FAIL) even though
the release binary was perfectly usable.

Pass BLAISE=<abs stage-1 path> into both the runtime `make` and `make install`
so the build never depends on a pre-existing compiler/target/blaise. Resolve
the RTL archive into $RTL_ARCHIVE once (installed compiler/target copy, falling
back to runtime/target/), and use it at both link sites.

Verified by running the patched script from a fresh clone with no
compiler/target/ present (the exact post-rolling-bootstrap state): reaches
FIXPOINT_OK where it previously aborted at step 1.
2026-06-04 01:29:28 +01:00
Graeme Geldenhuys 41a16ad6cc feat(params): preserve calling-convention directives on routine declarations
cdecl/stdcall/register/pascal/safecall directives were recognised by the
parser but silently discarded. Record them on TMethodDecl.CallingConv (both
the standalone-routine and class-method directive loops), copy through
CloneMethodDecl, propagate into TRoutineSig.CallingConv in BuildRoutineSig,
and persist in the free-routine .bif format (appended per routine record,
symmetric writer/reader) so the convention survives separate compilation.

Codegen is unchanged: every routine still emits the System V AMD64 convention
(which is the C ABI on Linux x86_64, so cdecl and the default already agree).
The directive is metadata only — the prerequisite for a future Windows/x86
target where stdcall and cdecl differ, and for faithful FFI/debugger tooling.

Replace the pending-placeholder TestCallingConv_Cdecl_Preserved with a real
assertion that 'procedure Beep; cdecl;' yields CallingConv='cdecl'.

Grammar and rationale updated: MethodDirective notes the retained conventions
and a new rationale subsection records the metadata-only decision.

The compiler test suite is now fully green (2518 tests, 0 failures).
2026-06-04 00:56:24 +01:00
Graeme Geldenhuys 7dbf6c232c feat(params): track 'out' parameter mode distinctly via IsOutParam
'out' was parsed as a synonym for 'var' — by-reference, but indistinguishable
from 'var' afterwards. Add TMethodParam.IsOutParam, set alongside IsVarParam
when the 'out' keyword is present, and carry it through CloneMethodParam and
the .bif param-flags pack (new bit 3) so the loader's TRoutineSig and any
future tooling can recover the declared mode.

Codegen is unchanged: 'out' still lowers identically to 'var' (a pointer
parameter). The flag is metadata only — the prerequisite for a future
read-before-write lint and/or zero-on-entry semantics.

Replace the pending-placeholder TestParam_ModeOut_Preserved with a real
assertion that 'out' yields IsOutParam=True, IsVarParam=True, IsConstParam=False.

Grammar and rationale updated to match: ParamGroup gains the OUT alternative
and the out-parameter section documents the preserved-metadata decision.
2026-06-04 00:50:15 +01:00
Graeme Geldenhuys c479aa5895 fix(codegen): leak-tracker typeinfo reference must use the class unit prefix
In --debug mode EmitExpr registers each newly allocated instance with the leak
tracker by loading the class-name pointer from $typeinfo_<Class>+16. Both
register sites built that symbol with a bare QBEMangle(Name), omitting the
prefix that ClassSymName applies. The typeinfo data itself is defined as
$typeinfo_<prefix><Class> (e.g. $typeinfo_P_TBox for a program-scope class),
so the reference resolved to an undefined $typeinfo_TBox and every --debug
build failed to link.

Route both leak-tracker references through ClassSymName(QBEMangle(Name)), the
same form the adjacent _ClassAlloc and vtable-store lines already use.

Fixes all six TE2ELeakCheckTests, which compile and run real --debug binaries
and so exercise the link step the IR-only harness cannot see.
2026-06-04 00:44:08 +01:00
Graeme Geldenhuys deb2e043fd fix(codegen): for-in class enumerator calls must use the method's unit prefix
EmitForInStmt built the GetEnumerator/MoveNext/GetCurrent call names with a
bare QBEMangle(OwnerTypeName + '_' + Name), omitting any unit prefix. For an
enumerable class defined in a unit (e.g. TStringList in Classes) the method
def is emitted as classes_TStringList_GetEnumerator while the for-in call site
emitted TStringList_GetEnumerator, yielding undefined-reference link errors.

Route all three calls through MethodEmitName, which prefers the method's
ResolvedQbeName and otherwise falls back to ClassUnitPrefix-qualified names —
the same resolution used by ordinary method calls.

Covered by the existing e2e test TE2ETStringListTests.TestRun_ForIn, which
iterates a real Classes.TStringList; the IR-only harness compiles a single
program and cannot model a unit-defined enumerable, so the e2e layer is the
correct guard here.
2026-06-04 00:38:40 +01:00
Graeme Geldenhuys c00b71e0d3 fix(generic): program-scope generic methods must not get unit prefix in ResolvedQbeName
InstantiateGeneric mangled each generic-instance method's ResolvedQbeName
with MangleUnitPrefix(Sym.OwningUnit). For program-scope generics OwningUnit
is the program name (e.g. 'P'), yielding a spurious 'P_' prefix. The method
def and itab then used '$P_TBox_Integer_SetValue' while the property
setter/getter call path used ClassUnitPrefix (which returns '' for program
scope), producing 'undefined reference' link errors.

Use CurrentUnitPrefix instead: '' for programs, the mangled unit prefix for
units — consistent with the property-call path on both sides.

Also pin the generic instance's DestroyResolvedQbeName to the same prefix as
the method def, so the emitted $_FieldCleanup_<T> calls the destructor symbol
that actually exists. Previously it was left empty and the cleanup fell back
to ClassUnitPrefix(), which disagreed for program-scope generics and broke
the TList<T> e2e link tests.

Update the IR-level test assertions across the generic suites to expect the
unprefixed symbol names, and cp.test.unitinterface.pas to expect the unit
prefix for unit-scope classes.
2026-06-04 00:29:27 +01:00
Graeme Geldenhuys 3addd9630f chore(runtime): switch RTL build to unit-as-top-level; drop build-driver shims
Now that the compiler supports `blaise --source Unit.pas --output Unit.o`
directly, the build-driver pattern (a thin program wrapper + IR sed strip)
is no longer needed.  Replace every multi-step rule with a single invocation
and delete the eight *_build_driver.pas files.

This commit must follow the merge of blaise_clean_split in git history so
that the rolling-bootstrap script can build the merge commit using the
previous binary (which still uses the old Makefile), then build this commit
using the freshly-merged binary (which understands unit-as-top-level).
2026-06-03 23:49:29 +01:00
Graeme Geldenhuys 7142f80cfa feat(loader): incremental separate compilation via .bif-in-.o
Merges Andrew Haines' blaise_clean_split branch (50 commits):

  - Unit-as-top-level compilation (blaise --source Unit.pas -> Unit.o)
  - .bif (Blaise Interface File) embedded as an ELF section in .o — no
    sidecar file that can drift out of sync
  - Full uses-chain rewrite: layered lookup (current-unit → uses-chain →
    builtins) with per-unit symbol cache and visibility seam
  - Unit-prefix mangling for free routines, classes, and address-of
  - --emit-iface, --incremental, --unit-cache CLI flags
  - Generic class method bodies serialised/deserialised through .bif
  - Generic instance mangling fix (ResolvedQbeName + OwningUnit on clones)
  - EmitMethodNameRef scoped by class to prevent published-method link collisions
  - 92 loader tests integrated into main TestRunner (2346 tests total)
  - E2E separate-compilation round-trip tests (free routines + generics)

Rebased onto master; merge conflicts in Blaise.pas resolved to preserve
the native backend (--backend native, --target, --emit-asm, --debug).
2026-06-03 23:49:17 +01:00
Andrew Haines ebdd8eccfe refactor(loader): use TStringBuilder in .bif writers
The Write* helpers in uUnitInterfaceIO.pas all build their output as
TStringList instances and serialise via SL.Text.  That's quadratic
under the covers (every Add reallocates the joined view) and ties the
exact line ending to whatever TStringList chooses.  Switch the
accumulator to stdlib TStringBuilder.AppendLine, which appends #10
explicitly and avoids the join cost on every Add.

No on-disk format change — the wire output is byte-identical because
both paths emit #10-separated records.  Self-bootstrap and TestRunner
(2346 tests) pass.
2026-06-03 19:40:44 +01:00
Andrew Haines d7bab10428 test(e2e): separate-compilation round-trip through the blaise CLI
Spawn the actual blaise binary end-to-end:

  1. blaise --source MyDep.pas --output MyDep.o
       -> ELF .o with .blaise.iface embedded
  2. MyDep.pas removed from disk
  3. blaise --source use_mydep.pas --output use_mydep --unit-path <dir>
       -> auto-discovers MyDep.o, reads the iface, links
  4. ./use_mydep prints the expected line

Documents the headline feature in the main test gate.  Anything that
needs the source TUnit at codegen time (currently: generic and inline
bodies) is still covered by cp.test.unitinterface; this test focuses
on the free-routine round-trip which already works.
2026-06-03 19:40:44 +01:00
Andrew Haines 661cd4d6bd test(loader): fold loader-testrunner into main TestRunner
The TUnitInterface and import/export tests lived in a parallel
loader-testrunner/ project so they could iterate without churning
TestRunner.pas during development.  Now that the loader interfaces
have stabilised, move them into the main test gate alongside the
rest of cp.test.*.

Drops the standalone loader-testrunner/ project entirely and adds
cp.test.unitinterface to TestRunner.pas's uses clause.  Also fixes
a stale reference in the test file (TUnitInterface.CompilerVersion
was renamed to CompilerId by the validation-header commit before the
file moved into the gate).

TestRunner reports OK (2344 tests) — 2252 from the existing suite
plus 92 newly-gated loader tests.
2026-06-03 19:40:44 +01:00
Andrew Haines 581e88c6ed refactor(loader): compute imported ResolvedQbeName via MangleUnitPrefix
Drop the per-routine ResolvedQbeName field from the .bif ROUT block
now that the uses-chain rewrite tracks OwningUnit on every
TMethodDecl.  SynthesiseMethodDecl takes the iface's unit name as a
new parameter and computes ResolvedQbeName := MangleUnitPrefix(...) +
ASig.Name directly, so import and codegen agree on the same global
symbol the exporting unit defines without round-tripping the name
through the on-disk format.

Expose MangleUnitPrefix in uSemantic's interface section so
uSemanticImport can call it without touching internals.

Net effect on .bif: one fewer Lpstr per free routine, no behaviour
change.  TestRunner 2252/0; separate-compile end-to-end check
(MyDep.pas -> built/MyDep.o, then build a program against built/ with
source hidden) prints the expected output.
2026-06-03 19:40:44 +01:00
Andrew Haines aa67211e89 test(e2e): implicit System unit reachable without explicit uses (6c-M step 10)
Regression test for task #44 step 10.  Three programs with no
`uses` clause must still resolve WriteLn / IntToStr / Length —
proving the implicit `System` prepend in BuildUsesChain produces
a reachable chain entry, not just a name in a list.

  - SrcWriteLnInt: WriteLn(42) → '42\n'
  - SrcIntToStr:   IntToStr(123) → '123\n'
  - SrcLength:     Length('hello') → '5\n'

Each shells out to the freshly built blaise compiler via the
test framework's CompileAndRun.  TestRunner: 2245 → 2248 (+3),
zero failures.  Fixpoint OK, loader-testrunner 92/92.
2026-06-03 19:40:44 +01:00
Andrew Haines 8d2728c7f5 feat(semantic): per-unit symbol cache for uses-chain retrieval (6c-M step 9)
Add FUnitSymbols: TStringList on TSemanticAnalyser keyed by
'UnitName' + #1 + 'SymbolName', Objects[I] = TSymbol (non-owning;
lifetime stays with FTable's global scope today).  Populated by
RegisterUnitIface walking FTable's global scope after each import
or AnalyseUnitForExport completes and absorbing every symbol whose
OwningUnit matches the unit being registered.

LookupViaUsesChain now queries the per-unit cache directly instead
of doing a flat FTable.Lookup and filtering by OwningUnit, with the
old path retained as a fallback for the in-flight unit whose
absorption hasn't run yet.

This is the data substrate that lets two units export same-named
symbols once the flat-merge duplicate-check is later relaxed — the
chain walker no longer collapses on the global table.  The flat
merge itself stays in place as a fallback / for the current-unit-
being-analysed; full removal is a follow-up once import-time
chain context is wired so cross-unit type refs can resolve through
the chain rather than via flat-merged FTable.Lookup.

Adds TScope.SymbolCount/SymbolAt accessors and TSymbolTable.GlobalScope
so the analyser can iterate scope 0 without threading itself
through uSemanticImport helpers.

TestRunner 2249/0, fixpoint OK, loader-testrunner 92/92.
2026-06-03 19:40:44 +01:00
Andrew Haines eb759c345e feat(semantic): track OwningUnit on TMethodDecl + funnel FProcIndex.AddObject (6c-M step 8)
Add OwningUnit: string to TMethodDecl in uAST.  Auto-populate via a
new TSemanticAnalyser.RegisterProcDecl wrapper that sets the field
from FCurrentUnitName when not already set, then forwards to
FProcIndex.AddObject.

Migrate the six source-side AddObject call sites (interface section,
impl-section overload extension, AnalyseUnit's two equivalents,
generic instantiation, AnalyseStandaloneDecl) through the wrapper.
The seventh site is RegisterImportedRoutine — kept direct because
uSemanticImport already pre-sets MDecl.OwningUnit to the iface name
before calling, and FCurrentUnitName is not reliable during import.

Sets a tag for cross-unit prefix mangling (#43) and for the per-unit
FProcIndex partitioning that lands as part of step 9.  No observable
behavior change today since no test exercises cross-unit name
duplicates.  TestRunner 2249/0, fixpoint OK, loader-testrunner 92/92.
2026-06-03 19:40:44 +01:00
Andrew Haines 55d4527dfc feat(semantic): layered Lookup ordering — current-unit before uses-chain (6c-M step 7a)
TSymbolTable.Lookup now walks resolution layers in canonical Object
Pascal order:

  1. Pushed scopes (locals, params, nested-routine outer locals)
  2. Class context — still handled by caller-side fallback
     (FCurrentClass.FindField) because TFieldInfo isn't a TSymbol;
     promotion lands with the step-9 refactor of caller flow
  3. Current compilation unit's own symbols
  4. Uses chain, right-to-left
  5. System — prepended to the chain at index 0, falls out of layer 4
  6. True global — builtins and (today) any flat-merged residue

Layer 3 fires when a scope-0 hit's OwningUnit matches the unit/program
currently being analysed.  Layer 6 returns whatever scope 0 produced
as the bottom-of-chain fallback so builtins (OwningUnit='') remain
reachable.

Also tag program globals with the program's name in Analyse() so
layer 3 also activates during program compilation, not just unit-
for-export analysis.

No observable change today since flat-merge errors prevent
cross-unit name collisions; once step 9 removes the flat merge,
this ordering becomes load-bearing.  TestRunner 2249/0, fixpoint OK,
loader-testrunner 92/92.
2026-06-03 19:40:28 +01:00
Andrew Haines 113a5a18b2 feat(semantic): rewire TSymbolTable.Lookup to consult uses-chain (6c-M step 7)
TSymbolTable.Lookup now walks scopes in three layers:

  1. Pushed scopes (1..N): locals, params, for-loop vars — these
     don't carry OwningUnit and aren't subject to uses-chain
     filtering.
  2. Uses-chain provider: TSymbolTable.UsesChainProvider, set by
     the analyser to itself, walks FCurrentUsesChain right-to-left
     and returns the first visible hit.
  3. Global scope (index 0): language builtins (Length, IntToStr,
     …) registered by RegisterBuiltins plus today's flat-merged
     unit imports (the redundancy goes away in step 9).

The chain provider is wired through a new abstract base class,
TUsesChainProvider, that TSemanticAnalyser inherits and overrides.
A method-pointer-of-object hook would have been more Delphi-idiomatic
but @MethodName produces a plain function pointer in Blaise; the
abstract base sidesteps that.

A FBypassUsesChain flag breaks the recursion when the chain walker
itself calls Lookup to retrieve the canonical TSymbol from the flat
FTable.

FindType also now routes through Lookup() so type-name resolution
participates in the chain.

No breakage in the gate as predicted: today's flat FTable holds
exactly one TSymbol per name (the analyser already errors on
duplicates), so the chain and the global agree on every hit.  The
visible-vs-leaking distinction only starts mattering once step 9
removes the flat merge.  TestRunner 2249/0, fixpoint OK,
loader-testrunner 92/92.
2026-06-03 19:40:28 +01:00
Andrew Haines cbcfcc7415 feat(semantic): member-lookup visibility seam (6c-M step 6)
Add the structural seam for class-member access control:

  - IsVisibleFromUnit overload taking a member's owning-unit
    string directly (avoids synthesising a TSymbol at sites
    where we have a TMethodDecl/TFieldInfo).

  - AssertMemberVisible(OwningUnit, ClassContext, MemberName,
    Line, Col): hard-error wrapper that raises ESemanticError
    on a False filter result.  This embodies the
    project_per_unit_visibility.md rule: an invisible *qualified*
    member is a hard error, not a fall-through to a same-named
    free symbol elsewhere.

Wire one canonical call site at FindMethodDecl using the class
type's OwningUnit as the member's effective owner.  Until
TMethodDecl gains a per-member OwningUnit, members of class TFoo
are gated at the same boundary as TFoo itself — which is the
right semantics for the unit-scope private/protected work that
will follow.

Filter still returns True so this is a no-op today; the seam is
the deliverable.  TestRunner 2249/0, fixpoint OK,
loader-testrunner 92/92.
2026-06-03 19:40:28 +01:00
Andrew Haines 58a4352f73 feat(semantic): LookupViaUsesChain + TUnitInterface.HasSymbol (6c-M step 5)
Add the uses-chain lookup helper for unqualified identifiers:

  - TUnitInterface.HasSymbol(AName): Boolean
      Probe across types/consts/vars/routines/generic bodies and
      walks enum defs for member names (each enum member is its
      own top-level identifier, matching what uSemanticImport
      publishes).

  - TSemanticAnalyser.LookupViaUsesChain(AName): TSymbol
      Walks FCurrentUsesChain right-to-left ("last in uses wins").
      For each unit whose iface exports AName, retrieves the
      canonical TSymbol from FTable and applies IsVisibleFromUnit.
      Returns the first visible hit, or nil.

Also: BuildUsesChain now prepends the implicit `System` unit (every
unit's effective `uses` is "Uses System(hidden), Classes;"), except
when the unit being analysed IS System — a unit cannot use itself.
User-written `uses System` (case-insensitive) deduplicates so the
right-to-left walk doesn't shadow the implicit entry.

No call sites consult LookupViaUsesChain yet — the behavioural
switch happens in step 7 when TSymbolTable.Lookup is rewired.  No
breakage expected even then, since today's compiler errors on name
duplicates so no working test code holds a cross-unit conflict.

TestRunner 2249/0, fixpoint OK, loader-testrunner 92/92.
2026-06-03 19:40:08 +01:00
Andrew Haines 5d6649e85a feat(semantic): IsVisibleFromUnit hook (6c-M step 4)
Add the 3-arg visibility chokepoint:

  function TSemanticAnalyser.IsVisibleFromUnit(
            ASym: TSymbol;
            const AFromUnit: string;
            AFromClass: TRecordTypeDesc): Boolean;

Stub returns True unconditionally — private/protected modifiers
don't exist on Blaise class members yet.  When they land, this
seam plugs in without changing call sites: private gates on
(AFromUnit = ASym.OwningUnit); protected additionally accepts
AFromClass walking up ParentClass to ASym's declaring class.

The 3-arg signature matters more than the body: AFromUnit needs
no special context to populate (it's FCurrentUnitName), and
AFromClass uses TRecordTypeDesc since Blaise represents classes
as record descriptors.

No call sites yet — wired in later steps.  TestRunner 2249/0,
fixpoint OK, loader-testrunner 92/92.
2026-06-03 19:40:08 +01:00
Andrew Haines d1472c69b5 feat(semantic): FUnitIfaces registry + RegisterUnitIface (6c-M step 3)
Add per-analyser TUnitInterface registry, keyed by unit name
(case-insensitive).  Populated in Blaise.pas's existing iface paths:

  - prebuilt .bif ifaces (auto-discovery from .o sections): registered
    immediately after ImportUnitInterface
  - source-compiled deps: registered after ExportUnitInterface returns
    the freshly-built iface

The analyser does NOT own the TUnitInterface objects — caller retains
lifetime (Loader.PrebuiltIfaces for .bif ones, UnitIfaces for the
source-built ones).  Last-write-wins on duplicate names, matching
the eventual uses-chain semantics.

Plumbing only — no lookup path consults the registry yet.  Adds
uUnitInterface to uSemantic's uses (no circular dep — uUnitInterface
imports uAST only).  TestRunner 2249/0, fixpoint OK,
loader-testrunner 92/92.
2026-06-03 19:40:08 +01:00
Andrew Haines 3f7f758bcf feat(semantic): FCurrentUsesChain + BuildUsesChain (6c-M step 2)
Add per-compilation FCurrentUsesChain (TStringList, owned) to
TSemanticAnalyser, populated from Prog.UsedUnits / AUnit.UsedUnits
at the entry of Analyse() and AnalyseUnitForExport().

In Blaise, language builtins live in the global symbol table from
TSymbolTable.RegisterBuiltins (no separately-named "System" unit),
so the chain holds only the user's `uses` entries in source order;
they'll be walked right-to-left in a later step and a non-hit falls
back to the global builtins.

Pure plumbing — field populated but not yet consulted.  TestRunner
2249/0, fixpoint OK, loader-testrunner 92/92.
2026-06-03 19:40:08 +01:00
Andrew Haines fd84fe7c63 feat(semantic): auto-tag OwningUnit on global Define via FDefineOwningUnit (6c-M step 1b)
Add TSymbolTable.DefineOwningUnit property; Define()/DefineGlobal()
auto-populate ASymbol.OwningUnit from it when (a) the symbol has no
explicit value and (b) we're at scope depth 1 (global only — locals
must stay OwningUnit='').

Wire AnalyseUnitForExport to set the property at entry and clear on
exit, so source-compiled deps tag their interface symbols the same
way uSemanticImport already tags .bif-loaded ones.  Also wire
ImportUnitInterface to set+restore the property as belt-and-braces
in case a future helper grows a Define site we missed.

Net behavior change: zero.  TestRunner 2249/0, fixpoint OK,
loader-testrunner 92/92.
2026-06-03 19:40:08 +01:00
Andrew Haines f4840dd923 feat(semantic): TSymbol.OwningUnit field + populate during import (6c-M step 1)
Add an OwningUnit string to TSymbol, populated by uSemanticImport when
registering an imported unit interface (consts, vars, routines, enum
members, sets, aliases, classes, records, interfaces).  Default empty
for builtins, locals, and program-level symbols.

This is plumbing only — the field is written but not yet read.
Consumed by per-unit visibility (uses-chain lookup) and by unit-prefix
mangling.  Net behavior change: zero (TestRunner 2249/0, fixpoint OK,
loader-testrunner 92/92).
2026-06-03 19:40:08 +01:00
Andrew Haines a1fdd9a0cb docs: name mangling reference 2026-06-03 19:40:08 +01:00
Andrew Haines 1723494326 docs: .bif file format reference 2026-06-03 19:40:08 +01:00
Andrew Haines d611314cc9 docs: extending the AST and .bif format — checklist + worked example
Captures the rule of thumb that every AST shape change must touch
three places (uAST.pas / uSemantic.pas / uUnitInterfaceIO.pas), with
codegen as a fourth when the node has runtime semantics.

Worked example walks through adding a hypothetical 'when' statement
end-to-end: parser, semantic, QBE codegen, .bif encoder/decoder, and
test surface.  Plus a shorter checklist for plain field additions,
common-pitfalls section, and the rationale behind the positional
(rather than tagged) .bif format.
2026-06-03 19:40:08 +01:00
Andrew Haines cb9e130931 feat(iface): .bif validation header — compiler id + source hash (6c-N)
Add validation to the .bif loader so stale or cross-compiler-version
ifaces are rejected instead of silently producing wrong-shape symbol
tables.

.bif format v2 (bumped from v1; v1 .bifs now rejected):
  - Magic 'BLAISE-IFACE' and validation header on the first line.
  - META block now carries:
      SourceFile     (was already there)
      SourceHash     (NEW: FNV-1a 64 hex of source content)
      CompilerId     (renamed from CompilerVersion; populated now)
      SourceModTime  (NEW: reserved Int64, currently always 0 —
                      no RTL FileAge yet; populated when a
                      TRtlPlatform.FileAge method lands)

New unit uCompilerId.pas owns COMPILER_ID — the build-time identity
string baked into every .bif written by this compiler.  Convention:
'blaise-<semver>+<short-suffix>'; bump suffix on codegen-affecting
changes.  Initial value 'blaise-0.9.0-dev+6c-N'.

Load-time validation in TUnitLoader.ValidateIface, called after
LoadIfaceFromObject succeeds:

  - Source .pas present on path:
      hash-compare current source against AIface.SourceHash.
      Match → accept iface (fast path: skip reparse).
      Mismatch → discard, fall through to source-compile path.
      Empty SourceHash on iface → treat as mismatch (forces
      rebuild for any iface written before this format).

  - Source .pas absent:
      CompilerId exact match → accept.
      CompilerId differs or empty → reject with a clear error
      message ('compiled by X, this compiler is Y, source
      unavailable to rebuild').

This is the safety net for the binary-only-deps case (RTL shipped
without source, third-party precompiled units): the only signal
the iface is safe is that the exact compiler wrote it.  Any change
that bumps COMPILER_ID forces re-emit of every iface — desired
behavior whenever codegen or .bif schema changes.

Implementation notes:
  - FNV-1a 64-bit content hash exposed as ContentHashFnv1a64 from
    uUnitInterfaceIO; assembled from 32-bit halves because Blaise's
    literal parser caps at Int64.
  - Hash populated by WriteUnitInterfaceToFile (where we have the
    source path on disk).  Best-effort: empty hash on IO failure.

TestRunner 2248/0, fixpoint OK (stage-3/stage-4), loader-testrunner
92/92.
2026-06-03 19:40:08 +01:00
Andrew Haines 118317c3bc feat(loader): --incremental — implicit per-unit compile during program build
Cold-start completion of the separate-compilation flow.  Without
this, a fresh source tree could only reach auto-discovery after a
user manually compiled each unit to a .o first.  With it, the
program build does that as a side effect.

--incremental flag:
  After deps are loaded + semantic-analysed (the existing flow
  unchanged), but before the main codegen, walk Units and for
  each one:
    1. Fresh TCodeGenQBE, shared symbol table.
    2. AppendUnit(U); GetOutput → per-unit IR.
    3. Write IR to <unitcache>/<unit>.o.ssa.tmp.
    4. WriteUnitInterfaceToFile to <unit>.o.bif.tmp.
    5. CompileUnitToObject runs qbe + cc -c + objcopy embed.
    6. Delete the two temps; add <unit>.o to PrebuiltObjPaths.
  Then SkipDepCodegen := True so the main IR no longer carries
  dep bodies — they're linked from the per-unit .o instead.

--unit-cache <dir> picks the per-unit .o directory.  Default:
ExtractFilePath(OutputFile), so the .o lands alongside the main
output.

Cold-start dogfood (clean /tmp/incdemo):

  $ blaise --source use_mydep.pas --output use_mydep \
           --unit-path . --incremental
  $ ls
  mydep.o  MyDep.pas  use_mydep  use_mydep.pas
  $ ./use_mydep
  21

  $ rm MyDep.pas
  $ blaise --source use_mydep.pas --output use_mydep \
           --unit-path .          # no --incremental needed
  $ ./use_mydep                   # auto-discovered mydep.o
  21

Note: --incremental implies the codegen-side dep skip, so the
main IR shape changes from the default (now smaller).  Existing
non-incremental builds are unaffected; the default path inlines
dep bodies as before.

Pre-commit gate:
  - mcp compile: OK
  - TestRunner: 2249 tests, 2 pre-existing failures only
  - fixpoint.sh: FIXPOINT_OK at stage-3/stage-4
  - loader-testrunner: 92 tests, all green
2026-06-03 19:40:08 +01:00
Andrew Haines e263b9b063 feat(loader): embed .bif inside .o + auto-discovery
The .o and its .bif now travel together: every unit-mode compile
embeds the TUnitInterface bytes into a non-loaded section of the
output .o, and the loader picks them up automatically when it
encounters a unit whose source is unavailable but whose .o sits
on the search path.

This eliminates the "mismatched .bif/.o pair on disk" failure
mode — the iface is *inside* the .o, you can't desync them by
copying one without the other.

New unit uIfaceObject:
  - TObjectFormat enum (ofELF today, ofPECOFF placeholder).
  - EmbedBifInObject / ExtractBifFromObject / LoadEmbeddedBifString
    shell out to objcopy --add-section / --dump-section with
    section names per format ('.blaise.iface' on ELF, '.bliface'
    on PE/COFF when we cross that bridge).  Section is flagged
    'noload,readonly' so ld drops it at link time and it never
    reaches the executable.

Blaise.pas (unit-mode compile):
  - Always exports the iface to a sibling .bif.tmp regardless of
    --emit-iface, so CompileUnitToObject can embed it.
  - CompileUnitToObject gains an ABifFile parameter and calls
    EmbedBifInObject after cc -c finishes.

uUnitLoader (auto-discovery):
  - New LocateObject(AName) — scans search paths for <name>.o
    (lowercase first, exact-case fallback).
  - New LoadIfaceFromObject(APath) — extracts the iface section
    and runs ReadUnitInterface on the bytes.  Malformed iface
    surfaces a warning and falls back to the .pas.
  - LoadTransitive prefers a .o with embedded iface over the
    .pas source.  When found, the dep is added to PrebuiltIfaces
    (with paths in PrebuiltObjectPaths) and the .pas is *not*
    parsed.  Recursion walks the iface's UsedUnits.
  - Two new public properties: PrebuiltIfaces, PrebuiltObjectPaths.

Blaise.pas (consumer side):
  - After LoadAll, walks Loader.PrebuiltIfaces and calls
    ImportUnitInterface(iface, table, Semantic) — the new
    ASemantic arg is the next change.
  - PrebuiltObjPaths captured off the loader before Loader.Free
    so the post-finally link-step dispatch can pass them to
    CompileToNative.
  - CompileToNative gains an optional AExtraObjects: TStringList
    that gets appended to the cc command line.

uSemanticImport:
  - ImportUnitInterface signature grows an optional ASemantic
    parameter.  When non-nil, RegisterRoutines synthesises a
    TMethodDecl per free routine (carries Name +
    ResolvedReturnType + cloned Params with ResolvedType +
    ResolvedQbeName) and registers it via
    ASemantic.RegisterImportedRoutine — needed because
    AnalyseFuncCall looks up callees in FProcIndex rather than
    going through the symbol table.  The synthesised decls are
    owned by ATable.OwnImportedDecl.

uSemantic:
  - New public RegisterImportedRoutine(AName, ADecl): pushes
    into the previously-private FProcIndex.

uSymbolTable:
  - New FImportedDecls: TObjectList owning synthesised method
    decls + OwnImportedDecl(ADecl) helper.

End-to-end auto-discovery verified:
  $ blaise --source MyDep.pas --output mydep.o   # embeds iface
  $ rm /tmp/MyDep.pas                            # hide source
  $ blaise --source UseMyDep.pas --unit-path /tmp \
           --output /tmp/use_mydep
  $ /tmp/use_mydep
  21

Pre-commit gate:
  - mcp compile: OK
  - TestRunner: 2249 tests, 2 pre-existing failures only
  - fixpoint.sh: FIXPOINT_OK at stage-3/stage-4
  - loader-testrunner: 92 tests, all green
2026-06-03 19:39:24 +01:00
Andrew Haines a12b89e82c feat(loader): --emit-iface CLI flag — write .bif per dep unit
First on-disk artifact: Blaise.pas, when invoked with
--emit-iface <DIR>, writes each dep unit's TUnitInterface to
<DIR>/<unitname>.bif (lowercased) right after ExportUnitInterface
builds it.

Dogfood: compiling a program that 'uses SysUtils' against the
real stdlib produces a 539-byte sysutils.bif carrying the
Exception class hierarchy, PathDelim const, and BoolToStr +
ExpandFileName signatures.  Spot-checked the file by eye — same
format the TIfaceIOTests round-trip in-memory.

ParseArgs gains an EmitIfaceDir out-param (default '' = off).
Usage line added.  No change to existing semantics: the flag is
purely additive and the compile pipeline still runs codegen +
link as before.

This is the *write* half of separate compilation.  The read half —
loading a .bif from disk in place of parsing + analysing the
source — is task #31's substitutive piece and remains future
work; codegen still walks the source TUnit list, so freeing the
deps in the live driver would crash.

Pre-commit gate:
  - mcp compile: OK
  - TestRunner: 2249 tests, 2 pre-existing failures only
  - fixpoint.sh: FIXPOINT_OK at stage-3/stage-4
  - loader-testrunner: 85 tests, all green
2026-06-03 19:38:34 +01:00
Andrew Haines feb5151fca feat(loader): INLINE block — inline body IO round-trip
Final wire-format piece.  TUnitInterface.InlineBodies is a list of
(RoutineName, Block) emitted for every interface-section routine
marked 'inline'.  Writer emits an INLINE block per entry; reader
reconstitutes TInlineBody and pushes onto AIface.InlineBodies via
AddInlineBody (which also indexes by routine name for the
existing FindInlineBody lookup).

Block payload uses the AST body serialiser from the previous
commit — same EncodeBlock / ReadBlock pair the GENROUT block
uses.

One new test: 'function Square(N: Integer): Integer; inline;'
survives parse → analyse → export → write → read with the body
attached and findable via Round.FindInlineBody('Square').

The importer (uSemanticImport.ImportUnitInterface) does NOT yet
consume InlineBodies — the wire side stores the data but
ImportUnitInterface stops short of attaching it to the imported
routine's Sym.Decl.  Wiring that up is the next consumer-side
piece; the disk format now carries everything ImportUnitInterface
would ever need.

Pre-commit gate:
  - mcp compile: OK
  - TestRunner: 2249 tests, 2 pre-existing failures only
  - fixpoint.sh: FIXPOINT_OK at stage-3/stage-4
  - loader-testrunner: 92 tests, all green
2026-06-03 19:37:43 +01:00
Andrew Haines 256462d53d feat(loader): AST body serialiser — statements + expressions
Inlines TBlock / TASTStmt / TASTExpr trees in the wire format,
unblocking GENROUT bodies through the disk path.  Every node
parsed by CloneStmt / CloneExpr in uAST is covered:

Expressions (17): int, float, str, nillit, id, bin (with op
ordinal), not, call, mcall, field, deref, addr, ssub, alit,
isop, asop, supp.

Statements (18): asn, comp, if, while, rep, for, forin, tryfin,
tryex (with handler list), raise, exit, brk, cont, case (with
branch list), fasn, ssasn, pw, pcall, mcs, inh.

Blocks (TBlock): a 'block' kind tag followed by the flattened
Stmts list.  Local var / const / type / proc decls inside a
block are intentionally not serialised yet — generic routine
bodies in practice don't carry nested type or proc decls; add
later if a real workload needs them.

nil children encoded as a single 'nil' lpstr and the readers
return nil straight back.  TBinaryExpr.Op is encoded as the
TBinaryOp ordinal — keeps the format stable across re-ordering
the enum case constants (anything new added needs a version
bump anyway).

GENROUT block now also writes G.MethodDecl.Body and the reader
restores MD.Body with OwnBody=True so the importer downstream
can clone it for instantiation.

Test:
  - 'function Identity<T>(V: T): T; begin Result := V; end;'
    survives the disk path with the assignment body intact —
    walks the (possibly TCompoundStmt-wrapped) block, recovers
    the TAssignment with LHS 'Result' and RHS TIdentExpr 'V'.

Out of scope (next):
  - Inline bodies (TUnitInterface.InlineBodies).  The wire side
    of this is mechanical — same EncodeBlock — but the importer
    doesn't consume them through ImportUnitInterface yet either.
  - Local var/const/type decls inside a TBlock.

Pre-commit gate:
  - mcp compile: OK
  - TestRunner: 2249 tests, 2 pre-existing failures only
  - fixpoint.sh: FIXPOINT_OK at stage-3/stage-4
  - loader-testrunner: 91 tests, all green
2026-06-03 19:37:43 +01:00