Commit graph

462 commits

Author SHA1 Message Date
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
Andrew Haines 67d0843877 feat(loader): disk-path import + GENROUT block + enum-ordinal fidelity
Three related changes that together let WriteUnitInterface →
ReadUnitInterface → ImportUnitInterface end-to-end produce a
TSymbolTable equivalent to the source-build path for everything
the format covers.

1. GENROUT block: serialises generic free routines.  Generic
   class/interface templates already ride through the TYPE block
   ('generic-class' / 'generic-interface' kinds) — emitting them
   in GENROUT too would double-register.  GENROUT carries
   <name><type-param-list><method-decl>; the MethodDecl payload
   omits Body for now (AST body serialiser is future work) and
   the reader copies TypeParams back onto the rebuilt MethodDecl
   so a downstream instantiation sees a full template shape.

2. Enum-ordinal fidelity: TEnumTypeDef.AddMember stashes the
   explicit ordinal in Members.Objects[i] as a pointer-cast int —
   OrdinalAt reads it back.  The naive name-only encoding I had
   came back with ordinal 0 for every member.  Switched to
   'name=ord' pairs joined by ',' and a new LoadEnumMembers helper
   that splits + calls AddMember with the explicit value.  Bare
   names without '=' are tolerated as a sequential fallback (for
   hand-written .bif testing).

3. Five new disk-path tests verifying the full pipeline against
   ImportUnitInterface for: const, enum (ordinals!), class
   (vtable slots intact), generic-class template, generic-routine
   template.  These exercise WriteUnitInterface → ReadUnitInterface
   → ImportUnitInterface in sequence rather than the in-memory
   shortcut.

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: 90 tests, all green
2026-06-03 19:37:43 +01:00
Andrew Haines 3660e3d99a feat(loader): IO format covers generic class + interface templates
Two new TYPE kinds — 'generic-class' for TGenericTypeDef and
'generic-interface' for TGenericInterfaceDef.

Payload shape (shared shape, divergent body):
  <type-param-list> = <count>(<name lpstr><constraint lpstr>)*

  generic-class: <type-param-list> + <inner-class-payload>
    where the inner class part is identical to the regular 'class'
    payload — uSemanticExport runs PopulateClassEntry on the
    template's inner ClassDef, so the AEntry-level ParentClass /
    Implements / Attributes / Methods are already populated.
    InstanceSize stays 0 (no concrete layout until instantiation).

  generic-interface: <type-param-list> + <ParentName lpstr> +
                     <method-decl-list>
    Inner IntfDef.Methods is the source of truth, matching
    uSemanticImport.RegisterInterface which walks the same list.

Reader rebuilds:
  - TGenericTypeDef with the auto-created blank ClassDef freed
    and a freshly populated one substituted; sets AEntry.IsGeneric.
  - TGenericInterfaceDef likewise for the IntfDef; sets
    AEntry.IsGeneric.

Two new tests:
  - TBox<T> = class V: T; end → IsGeneric, ParamNames=['T'],
    inner ClassDef carries the one field.
  - IBox<T> = interface function Get: T; end → IsGeneric,
    ParamNames=['T'], inner IntfDef has the one method.

Still out of scope: inline + generic *bodies* (TBlock AST trees).
Those need a statement/expression serialiser which is a much
bigger undertaking — covered by 'no AST body serializer yet'
elsewhere in the loader notes.

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:37:43 +01:00
Andrew Haines e32fe64c21 feat(loader): IO format covers procedural types + unit metadata
Two small additions that close out the non-generic, non-body
surface of the wire format.

TYPE block: new 'proc' kind for TProceduralTypeDef.  Payload:
  <IsFunction bool><IsMethodPtr bool><ReturnTypeName lpstr>
  <param-count>(<pname><ptype><flags>)*

META block (new, sits between the unit name and TYPE):
  META\n
  <SourceFile lpstr><SourceHash lpstr><CompilerVersion lpstr>
  <UsedUnits string-list>
  END\n

SourceHash + CompilerVersion are still empty strings at construction
time (reserved fields per the original TUnitInterface header
comment); the metadata block ensures they survive round-trip so a
later patch that starts populating them lights up immediately.

Two new tests:
  - 'TCallback = procedure(N: Integer) of object' round-trips:
    IsFunction=false, IsMethodPtr=true, 1 param.
  - Hand-built interface with SourceFile + SourceHash +
    CompilerVersion + 2 UsedUnits ('SysUtils', 'Classes') survives
    write+read with all fields exact.

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: 83 tests, all green
2026-06-03 19:37:43 +01:00
Andrew Haines a7615081c7 feat(loader): IO format covers records, classes, interfaces
Extends the TYPE block with 'record', 'class', and 'interface'
kinds, completing the simple-shape coverage of the wire format.

Record payload:
  <IsPacked bool><field-list>

Class payload:
  <parent-qualref><instance-size lpstr>
  <attribute string-list><implements string-list>
  <field-list><method-list>

Interface payload:
  <ParentName lpstr><method-decl-list>

Helpers:
  * EncodeCount / DecodeCount — uniform stringified-int over lpstr,
    keeps the reader primitive set to ReadLpstrAt only.
  * EncodeBool / DecodeBool — '1'/'0' single-char lpstrs.
  * EncodeStringList / ReadStringListBlock — count + lpstrs.
  * EncodeFieldList / ReadFieldList — flattens multi-name decls
    ('X, Y: Integer') to one TFieldDecl per name on the wire,
    matching the way symbol-table AddField is called per-name.
  * EncodeMethodSig / ReadMethodSig — for class methods on the
    TTypeEntry.Methods side; carries VTableSlot, ResolvedQbeName,
    IsVirtual, IsOverride.
  * EncodeMethodDecl / ReadMethodDecl — for interface methods on
    the Def.Methods (AST) side; keeps the writer and the importer
    (uSemanticImport.RegisterInterface, which walks Def.Methods)
    symmetric.

Three new tests, all green:
  - TPoint record: 2 fields preserved (flattened from 'X, Y: Integer').
  - TFoo class with virtual Speak: parent-empty sentinel
    round-trips, InstanceSize > 0, ResolvedQbeName 'TFoo_Speak'
    and VTableSlot >= 0 recovered.
  - IGreeter interface: two methods round-trip on Def.Methods.

Out of scope (next):
  - Procedural-type kind (rare, easy add).
  - Generic types — both 'generic-class' and 'generic-interface'
    payloads need template parameter lists.
  - Inline + generic bodies (require AST block serialiser — large).
  - UsedUnits + SourceHash + CompilerVersion metadata.

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: 81 tests, all green
2026-06-03 19:37:43 +01:00
Andrew Haines 22b10e16f5 feat(loader): IO format covers vars, simple types, routines + file API
Extends the 6c-E text wire format with VAR, TYPE (enum/set/alias),
and ROUT record blocks, plus thin TStringList-based file wrappers.

VAR block:
  VAR <count>\n
  <name lpstr><typeref lpstr>
  ...
  END

TYPE block:
  TYPE <count>\n
  <kind lpstr><name lpstr><payload lpstr>
  ...
  END

  Where kind is 'enum' / 'set' / 'alias'.  Payload encoding:
    - enum: comma-joined member names
    - set:  base enum type name
    - alias: target type name
  Record / class / interface / procedural / generic kinds emit
  nothing yet (TypeEntryKind returns '' and the writer skips them).
  Those need richer payloads (fields with offsets, vtable slots,
  attribute lists) and land in follow-up commits.

ROUT block:
  ROUT <count>\n
  <name><isfn 0/1><return-typeref><paramcount>(<pname><ptype><flags>)*
  END

  Param flags pack IsVar/IsConst/IsOpenArray as a 3-bit lpstr.
  DefaultValue expressions are intentionally not serialised — that
  would require an AST writer.

File API:
  WriteUnitInterfaceToFile / ReadUnitInterfaceFromFile route the
  string-based serializer through TStringList.SaveToFile /
  LoadFromFile so platform line-ending behaviour matches the rest
  of the compiler's I/O.

Two new tests on top of the existing six:
  - real-source pipeline round-trip: parse + analyse + export +
    write + read recovers consts, vars, all three simple types,
    and the function signature.
  - file round-trip via /tmp.

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: 78 tests, all green
2026-06-03 19:37:43 +01:00
Andrew Haines 9c09217021 feat(loader): TUnitInterface text serializer/deserializer (consts)
Phase 6c-E starter — wire format for separate-compilation .bif (or
similar) artifacts.  Lays the plumbing for disk-cached unit
interfaces; this commit covers const records only, the rest is
mechanical extension.

Format (v1):
  BLAISE-IFACE 1\n
  <unit-name lpstr>\n
  CONST <count>\n
  <name lpstr><typeref lpstr><int64 lpstr><strval lpstr><flag byte>
  ...
  END\n

  Where lpstr = '<decimal-len>:' + <len raw bytes>.  Length-driven
  consumption sidesteps escaping — newlines and ':' bytes pass
  through cleanly, which the awkward-chars test exercises.

  TypeRef is rendered as 'UnitName.TypeName' inside an lpstr so the
  '$builtin' / '' sentinels survive without special-case parsing.

Text not binary, by design: easier to diff during development, no
endianness concerns, no bit-twiddling.  When the layout stabilises
we can re-encode as a compact binary form without changing the
public surface.

API: WriteUnitInterface(AIface): string and
ReadUnitInterface(AText): TUnitInterface; EIfaceFormatError on
malformed input or version mismatch.

Six tests, all green:
  - magic + version prefix on output
  - unit name round-trip
  - int-const round-trip (value, type ref)
  - string-const round-trip with embedded newlines + colons
  - empty interface round-trip
  - version mismatch raises EIfaceFormatError

Two Blaise compiler quirks worked around in this unit (memory
notes added/applicable):
  - Strings are 0-indexed everywhere; Pos returns 0-based offset,
    -1 for not found; Copy treats start as 0-based.  Cursor
    arithmetic was rewritten accordingly mid-implementation.
  - Routines that take a multi-string record as a const param and
    read its fields segfault (project_record_const_param_crash.md).
    EncodeQualRef and DecodeQualRef therefore take the unit-name
    and type-name strings separately.

Out of scope (next):
  - Vars, types (enums / sets / aliases / records / classes /
    interfaces), routines, inline bodies, generic bodies.
  - File I/O wrappers (Read/WriteFromFile).
  - SourceHash + CompilerVersion metadata.

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: 76 tests, all green
2026-06-03 19:37:43 +01:00
Andrew Haines 912217fed6 feat(loader): export TVarEntry for interface-section globals
Closes the last documented pending: ExportUnitInterface now emits
one TVarEntry per name declared in an interface-section var block
(walking AUnit.IntfBlock.Decls).  Multi-name declarations like
'X, Y: Integer' expand into one TVarEntry per name, matching how
AnalyseUnitForExport registers them as separate skVariable
symbols.

Re-activates the pending TImportRoundTripTests.TestImport_GlobalVar_
MarkedIsGlobal — Counter declared in a dep's interface is now
recoverable from the imported FTable as an IsGlobal skVariable of
type Integer.

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: 70 tests, all green (no pendings)
2026-06-03 19:37:43 +01:00
Andrew Haines 7fce033c06 test(loader): end-to-end import — analyse main against freed dep
Adds the most decisive proof of the 6c work to date: a main program
can be semantically analysed against a TSymbolTable that was
populated by ImportUnitInterface alone, after the dep unit's source
TUnit has been freed.

Two new TImportRoundTripTests:
  - dep exports a const, source freed, main program uses the const
    in an assignment statement — must analyse cleanly and the
    transferred Prog.SymbolTable must still resolve the const.
  - dep exports a class type, source freed, main program declares
    a variable of that type — must analyse and FindType must
    still hit.

These cover the semantic-side substitutive proof of 6c-D — the
source TUnit really is unnecessary downstream of ImportUnitInterface
for semantic analysis.  Codegen still walks dep ASTs (and would
crash if they were freed before Blaise.pas's codegen pass);
migrating codegen onto TUnitInterface is the remaining substitutive
work and belongs in its own commit.

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: 70 tests, only documented pendings fail
2026-06-03 19:37:43 +01:00
Andrew Haines 7f7b4981cb feat(loader): import generic free routines
Unblocks the previously-pending TestGenericRoutine_BodyExported and
adds end-to-end registration through TSymbolTable.

Parser change (uParser.ParseForwardDecl):
  Accept '<TypeParams[: constraint], …>' after the routine name in
  unit-interface forward decls.  Mirrors the subset of ParseMethodDecl's
  logic that applies to forwards (no '.MethodName' continuation here —
  those land in the implementation section).

Semantic plumbing (uSemantic):
  Generic free routines now slip through the interface-section
  forward-decl loop, the impl-side decl loop, and the impl-body
  loop: param/return resolution and global-symbol registration are
  deferred to instantiation time, matching how impl-side
  AnalyseStandaloneDecl already handles them.

  Interface verification ('has no implementation' check) skips
  generics — their template lives in FGenericFuncTemplates, not
  FProcIndex.

  AnalyseStandaloneDecl additionally mirrors the registration onto
  TSymbolTable.RegisterGenericRoutine so the lookup surface is
  shared between in-unit templates and imports.  InstantiateGenericFunc
  checks both lists.

TSymbolTable (uSymbolTable):
  New FGenericRoutines TStringList plus public RegisterGenericRoutine
  / FindGenericRoutine.  Case-insensitive (matches identifier
  resolution elsewhere).

Export side (uSemanticExport):
  TGenericBody gains a MethodDecl field — a CloneMethodDecl of the
  impl-side TMethodDecl (with body) — so the import has a fully-
  formed AST template to hand to RegisterGenericRoutine without
  synthesising one at import time.

Import side (uSemanticImport):
  New RegisterGenericRoutines step walks GenericBodies and pushes
  each non-IsType entry's MethodDecl onto FTable's routine list.

Tests:
  - Re-flips loader-testrunner's pending TestGenericRoutine_BodyExported
    to green (export-side already supported it; parser was the
    blocker).
  - New TImportRoundTripTests.TestImport_GenericRoutine_RegisteredOnTable
    asserts FindGenericRoutine('Identity') yields a TMethodDecl with
    TypeParams + Params + Body intact.

Pre-commit gate:
  - mcp compile: OK
  - TestRunner: 2249 tests, 2 pre-existing failures only — no
    regressions from the parser/semantic changes.
  - fixpoint.sh: FIXPOINT_OK at stage-3/stage-4
  - loader-testrunner: 68 tests, only documented pendings fail.
2026-06-03 19:37:43 +01:00
Andrew Haines e13e5f7a44 feat(loader): import generic class + interface templates
Walking AIface.Types: when an entry is generic (TGenericTypeDef or
TGenericInterfaceDef), register the AST template through
TSymbolTable.RegisterGeneric.  Matches uSemantic.AnalyseTypeDecls
pass-1 — instantiation later goes through FindGeneric, which the
consumer's FindTypeOrInstantiate path already uses.

Added uAST.CloneGenericInterfaceDef + wired it into CloneTypeDef
dispatch — was missing alongside the existing CloneGenericTypeDef.
Without it, exporting any generic interface used to raise
"unsupported type def" inside Export.

Two new tests:
  - TBox<T> = class V: T; end → FindGeneric('TBox') yields
    TGenericTypeDef with ParamNames = ['T'].
  - IBox<T> = interface … end → FindGeneric('IBox') yields
    TGenericInterfaceDef with ParamNames = ['T'].

Out of scope (next 6c-C continuation):
  - Generic free routines (TUnitInterface.GenericBodies has them
    but uSemantic stores them in a private FGenericFuncTemplates
    list — needs a public API on TSemanticAnalyser, or a tracker
    on TSymbolTable, to plumb through).
  - Full instantiation round-trip (would need a separate "compile
    against imports" test that actually walks codegen).

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: 67 tests, only the documented pending fails
2026-06-03 19:36:57 +01:00
Andrew Haines fe5f15190b feat(loader): import interfaces, implements list, class attributes
Three remaining 6c-B pieces, all going through the same
TUnitInterface that ExportUnitInterface already builds:

* RegisterInterface — NewInterfaceType + parent resolution +
  per-method AddMethod (using ParamVarFlags to rebuild the
  comma-separated var-flag string AddMethod expects).
* RegisterClass — walks AEntry.Implements, strips any 'Unit.'
  qualification prefix, looks up the interface descriptor, and
  calls RT.AddImplements.
* RegisterClass — walks AEntry.Attributes and calls
  AddClassAttribute, appending the 'Attribute' suffix when the
  raw name lacks it (uSemanticExport copies the raw attribute
  names verbatim; downstream wants the resolved form).  A cleaner
  fix would be to have uSemanticExport pull resolved names off
  the source TRecordTypeDesc.ClassAttributes; left as an audit
  item.

Three new tests, all green:
  - interface with two methods exported + imported, MethodCount=2
  - class(TObject, IGreeter) → RT.ImplementsCount=1, name 'IGreeter'
  - class marked [Marker] (with a locally-defined MarkerAttribute)
    → RT.ClassAttributeAt(0) = 'MarkerAttribute'

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: 65 tests, only the documented pending fails
2026-06-03 19:36:57 +01:00
Andrew Haines 593b69d2ba feat(loader): import class virtual methods + override vtable slots
TRoutineSig gains ResolvedQbeName (mangled symbol label set by
semantic), IsVirtual, and IsOverride.  uSemanticExport copies these
straight off the source TMethodDecl.

RegisterClass now walks AEntry.Methods; for each virtual it
AddVTableSlots with ImplName = '$' + ResolvedQbeName, and for each
override it OverrideVTableSlot at the parent's slot.  Static (non-
virtual, non-override) methods are no-ops on the type descriptor —
they only matter when downstream code actually calls them, which
requires importing them as global symbols (deferred along with
overloaded class methods).

Tests:
  - Virtual method registers a new slot with the expected
    '$TFoo_Speak' ImplName.
  - Override on a derived class reuses the parent's slot index,
    rewrites only the derived's ImplName, leaves parent intact.

Out of scope (next):
  - Overloaded class methods (mangled-key lookup).
  - Interface implements (data already exported as qualified names).
  - Class attributes (export emits raw names; need resolved names).
  - Properties (not currently exported).

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: 62 tests, only the documented pending fails
2026-06-03 19:36:57 +01:00
Andrew Haines 9df2981a78 feat(loader): import class types (layout only)
Extends uSemanticImport with RegisterClass — reconstructs a class's
TRecordTypeDesc with parent chain and field layout matching what
AnalyseTypeDecls produces.

Parent resolution mirrors semantic pass-2:
  - Explicit parent: look up by name in the symbol table.
  - Empty parent on a non-TObject class: implicit TObject.

Field layout is built by copying parent fields first (so storage
offsets continue past the parent's tail), then appending the
class's own fields.  RT.AddField handles the alignment + offset
computation, identical to the from-scratch semantic path.

Three new tests:
  - implicit TObject parent + inherited vtable shape
  - own field at offset 8 (after the parent's vptr)
  - explicit parent chain: TBase + TDerived, inherited field A at 8
    and own field B at 12.

Methods on classes are deferred — TRoutineSig does not yet carry
ImplName (the QBE/LLVM vtable symbol label) and AddVTableSlot
requires it.  Imported classes are usable for layout-only
consumers (typeinfo, field access); method dispatch awaits a
TRoutineSig.ImplName field plus export-side population.

Same workaround as the previous loader commit: the parent-class
resolver was originally a `const ARef: TQualTypeRef` parameter,
which segfaults on string-field read (memory:
project_record_const_param_crash.md).  Reworked to take the parent
name directly.

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: 60 tests, only documented pendings fail
2026-06-03 19:36:57 +01:00
Andrew Haines 63ad6341c3 feat(loader): import non-class record types
Extends uSemanticImport with RegisterRecord — reconstructs a
TRecordTypeDesc from the TUnitInterface.TTypeEntry's cloned
TRecordTypeDef.  Fields are resolved by name against the symbol
table and added via TRecordTypeDesc.AddField, which handles the
offset + alignment computation the same way AnalyseTypeDecls does.

One new TImportRoundTripTests assertion exercising a two-Integer
record — verifies field count, names, offsets (0 and 4), and that
the field TypeDesc points at the symbol table's Integer.

Class import is deliberately still gated with EImportError — it
needs parent-chain resolution, vtable inheritance, attribute
resolution, and method registration on the class descriptor; that
belongs in its own commit.

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: 57 tests, only documented pendings fail
2026-06-03 19:36:57 +01:00
Andrew Haines 1831a195fe feat(loader): introduce ImportUnitInterface for separate compilation
New uSemanticImport unit mirrors the interface-section side-effects of
uSemantic.AnalyseUnitForExport, but reads from a pre-built
TUnitInterface artifact instead of walking the source TUnit AST.  This
is Phase 6c-A — the small half of the substitutive consumer migration.

Covers the easy cases:
  - integer / string / float constants
  - enumeration types (and their members as skConstant)
  - set types (over enum bases)
  - simple type aliases and pointer aliases
  - free procedures and functions, with resolved param + return types

Out of scope (deferred):
  - classes, records, procedural types  → 6c-B
  - generics                              → 6c-C
  - interface-section global vars         → blocked on uSemanticExport
    not yet emitting TVarEntry records (test pending the gap close)

Compiler not yet wired to use the import path — that's the next
commit.  This one only proves the data path: parse → analyse → export
→ import-into-fresh-table reproduces the symbol shape that a
from-scratch AnalyseUnitForExport would have produced.

Eight new TImportRoundTripTests assertions; seven green, one pending
(global var — see comment in TestImport_GlobalVar_MarkedIsGlobal).

Workaround note: ResolveRef originally took a `const ARef:
TQualTypeRef` parameter, which segfaulted on the first string-field
read.  Same shape as the documented LLVM record-by-val bug, but on
QBE.  Worked around by passing the TypeName string directly.

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: 56 tests, only documented pendings fail
2026-06-03 19:36:57 +01:00
Andrew Haines a85a0e5257 feat(loader): build TUnitInterface cache during every real compile
Phase 5 of the loader work — wire the cache into Blaise.pas so it
populates on every full compile, not just on the loader-testrunner
fixtures.  Nothing queries the cache yet; this commit's value is
that it forces ExportUnitInterface to run against real codebases
(the entire RTL, stdlib, and TestRunner's e2e fixtures) on every
build.  Any bug in ExportUnitInterface against shapes we haven't
exercised yet now surfaces as a compile-time failure.

Mechanics:
  - Compile() gains a UnitIfaces TObjectList (owned).
  - After each AnalyseUnitForExport call in the dep loop, an
    ExportUnitInterface is added with the previously-built
    interfaces as ADeps.  That makes cross-unit type-ref
    resolution walk the same chain a future consumer would.
  - Cleaned up before Units in the finally block.

Pre-commit gate:
  - compiler rebuilt clean
  - TestRunner: 2249 tests, same 2 pre-existing failures —
    confirms the cache build doesn't regress any of ~50 multi-unit
    e2e fixtures.
  - fixpoint.sh: FIXPOINT_OK at stage-3/stage-4 — confirms the
    cache build doesn't perturb codegen output (the cache is
    side-effect-free; codegen still reads from source Units).
  - loader-testrunner: 47 green tests on the unit-level API.

Next: consumer migration (read InstanceSize / VTableSlot from the
cache during codegen instead of reaching back into TUnit / Symbol
Table) is the bigger Phase 6 step that actually flips the
architecture.
2026-06-03 19:36:57 +01:00
Andrew Haines 9d1fb9a713 feat(loader): wire semantic data into TUnitInterface export
Phase 4 of the TUnitInterface work — chains
TSemanticAnalyser.AnalyseUnitForExport before
uSemanticExport.ExportUnitInterface, so semantic-derived fields
flow through to the produced artifact.

Two semantic-only fields now populate:
  - TRoutineSig.VTableSlot — copied verbatim from
    TMethodDecl.VTableSlot, which uSemantic assigns during class
    method linking.  Static methods stay at -1; virtual/override
    methods get their assigned slot index.

  - TTypeEntry.InstanceSize — read from the resolved
    TRecordTypeDesc.TotalSize via the symbol table.  Zero
    pre-semantic; correct positive value after.

ExportUnitInterface now takes an optional ASymbolTable parameter.
When nil (parse-only path), the export still works — the two
fields just stay at their pre-semantic defaults.  When supplied,
PopulateClassEntry uses it to look up resolved record types and
fill InstanceSize.

TSemanticAnalyser.GetSymbolTable exposes the in-flight FTable so
callers can hand it to ExportUnitInterface after AnalyseUnitForExport
returns.  Non-owning read accessor — the analyser still owns and
frees the table.  (Tried a `property` declaration first; the current
bootstrap compiler crashes parsing properties on this class, so
expressed as a plain method.)

Test helper ParseAnalyseAndExport chains parse → semantic →
export and is used by the two newly-green tests
(TestClass_VTableSlotsAssigned, TestClass_InstanceSizeComputed).
The original ParseAndExport stays — useful for tests that need to
exercise the parse-only path or that want to compare a TASTTypeDef
identity before vs after free.

47 loader-testrunner tests now passing.  Remaining stubs are all
about features not yet supported in the underlying compiler:
  - 2 pending uAST gaps (out params, calling-convention attrs)
  - 1 pending parser gap (generic free routines in unit interface)

Pre-commit gate:
  - compiler rebuilt clean
  - TestRunner: 2249 tests, same 2 pre-existing failures
  - fixpoint.sh: FIXPOINT_OK at stage-3/stage-4
2026-06-03 19:36:31 +01:00
Andrew Haines 77f8068342 feat(loader): export class layout (parent, methods, attrs, forward decls)
Phase 3 of the TUnitInterface work — populates the class-specific
fields on TTypeEntry that consumers need to lay out instances and
resolve method calls without seeing the source class declaration.

PopulateClassEntry walks TClassTypeDef and writes:
  - ParentClass   — TQualTypeRef resolved against ADeps
  - Implements    — list of qualified 'Unit.Name' (or local 'Name')
                    interface names
  - Methods       — list of TRoutineSig per declared method, sigs
                    only (bodies stay in the source unit's .o)
  - Attributes    — class-level custom-attribute names verbatim

Forward-declared classes ('type TFoo = class;' in interface +
'type TFoo = class ... end' in implementation) merge: the exported
entry carries the full body, not the stub.  This is the documented
impl-leak called out in the TUnitInterface unit header — Blaise
allows the pattern and consumers need the full layout.

Generic classes (TGenericTypeDef wrapping TClassTypeDef) get their
class-body fields populated the same way.

Out of scope (Phase 4):
  - VTableSlot population — requires uSemantic to have assigned
    slots before Export runs.  PopulateClassEntry leaves VTableLayout
    empty until that pipeline is wired.
  - InstanceSize computation — same dependency on uSemantic.

Method-level IsPublished propagates through BuildRoutineSig now;
private/protected/public are still silently merged into the
non-published bucket by the parser.

51 loader-testrunner tests now passing: 45 green, 2 explicitly
pending Phase 4 semantic data, 2 pending uAST feature gaps (out
params, calling conventions), 1 pending parser support for generic
free routines in unit interface sections.

Pre-commit gate:
  - compiler rebuilt clean
  - TestRunner: 2249 tests, same 2 pre-existing failures
  - fixpoint.sh: FIXPOINT_OK at stage-3/stage-4
2026-06-03 19:36:31 +01:00
Andrew Haines febd34db09 feat(loader): introduce TUnitInterface for separate compilation
Foundation for letting downstream units compile against a self-contained
description of an upstream unit's interface section, without keeping the
upstream's source TUnit alive.  This is Phase 1+2 of the loader split
plan: produce the artifact, not yet consume it.  No existing pipeline
behavior changes — Blaise.pas still drives the full source TUnit chain
through semantic and codegen as before.

New units:
  uUnitInterface  — TUnitInterface + TTypeEntry/TConstEntry/TVarEntry
                    /TRoutineSig/TInlineBody/TGenericBody, plus the
                    TQualTypeRef '(unit, type)' name-pair used for every
                    cross-unit reference.  Case-insensitive lookups by
                    default; constructor takes ACaseSensitive: Boolean
                    for exact-match use cases.
  uSemanticExport — ExportUnitInterface(TUnit, ADeps: TObjectList): walks
                    the parsed AST and produces a self-contained
                    TUnitInterface.  Phase 2 scope covers types, consts,
                    free routines, inline bodies, generic types/routines,
                    used units, and cross-unit type-ref resolution.
                    Class layout (parent, methods, vtable, instance size,
                    attributes) is intentionally out of scope until
                    Phase 3 wires semantic-resolved data.

Supporting changes:
  uAST.pas  — TUnit.ImplUsedUnits new field, separates impl-section uses
              from interface-section uses (an aspirational comment that
              was previously never honoured); TMethodDecl.IsInline new
              field captures the 'inline' directive at parse time;
              CloneTypeDecl/Const/MethodDecl/MethodParam/TypeDef promoted
              from forward decls to interface so uSemanticExport can
              reuse them; CloneTypeDef extended to handle top-level
              class/generic/interface/procedural defs (previously raised
              on those, since it only saw nested defs inside method
              bodies); CloneClass/Generic/Interface/Procedural TypeDef
              added.

  uParser.pas — implementation-section uses go to ImplUsedUnits;
                'inline' directive sets TMethodDecl.IsInline in both
                ParseForwardDecl and ParseMethodDecl.

  uUnitLoader.pas — transitive load walks both UsedUnits and
                    ImplUsedUnits, since the parser now splits them.

Tests live in a new top-level module:
  loader-testrunner/ — separate from compiler/TestRunner so the loader
                      work can iterate without churn against the main
                      test set; mirrors the [Threaded] subprocess
                      fan-out pattern.  39 green tests across structural
                      carry-over, lookups, cross-unit refs, body
                      pairing, and self-containment-after-free.  Eight
                      stubs remain pending Phase 3 class export; two
                      pending uAST feature gaps (out params, calling
                      conventions); one pending parser support for
                      generic free routines in interface section.

Pre-commit gate:
  - compiler rebuilt clean
  - TestRunner: 2249 tests, 2 pre-existing failures (Const_LocalArray*,
    ConstArray_RangeIndexed_Strings — latter documented in memory)
  - fixpoint.sh: FIXPOINT_OK at stage-3/stage-4
2026-06-03 19:36:31 +01:00
Andrew Haines ba256d8ddc feat(elf): ELF reader/writer for embedded .bif sections
Adds compiler/src/main/pascal/uElfObject.pas — narrow ELF object
reader/writer scoped to the two operations the .bif-in-.o pipeline
needs:

  ProbeElf       — header-only identification (never raises)
  LoadSections   — full SHT walk, names resolved through .shstrtab
  FindSection    — linear lookup, -1 sentinel for not-found
  AppendSection  — append a SHT_PROGBITS section, write-to-temp + rename

Replaces what would otherwise be a runtime dependency on `objcopy
--add-section` / `--dump-section`.  No external tool needed; everything
self-contained in the compiler.

Scope deliberately narrow: 64-bit little-endian ET_REL only (every
target the .bif pipeline cares about today), no symbol-table edits,
no relocation fix-up, no section removal, no PE/COFF.  Existing
section contents never move; only .shstrtab and the SHT itself are
rewritten, so relocations and symbol values pointing into existing
sections stay valid.

Appended sections are marked SHF_EXCLUDE so GNU ld drops them from
final executables and shared libraries while preserving them through
`ar` archives and `ld -r` partial links.  This matches what we want
for embedded .bif blobs: ride along through static library
distribution, vanish from the user's shipped binary.

Not wired into the compiler yet — that lands in the follow-up patches
that swap the objcopy shell-outs.
2026-06-03 19:36:07 +01:00
Andrew Haines 583b77eb6f feat(semantic,codegen): unit-prefix mangling (free routines, classes, address-of)
Cross-unit symbol uniqueness via unit-name prefixing.  Routines,
methods, vtables, typeinfo, impllist, itab, _FieldCleanup_ and
__cn_ data items all get a leading <unit>_ when their owning unit
isn't in the unmangled allowlist (System / rtl.* / blaise_*).
Generic instantiations keep their existing unprefixed names —
their stitched-together instantiation names are already unique.

Plumbing:
  uSymbolTable:
    - TSymbol gains an OwningUnit: string field.
    - TSymbolTable gains a DefineOwningUnit field; when non-empty,
      Define() auto-tags any incoming symbol that doesn't already
      carry an owner.

  uAST:
    - TMethodDecl gains OwningUnit.
    - TAddrOfExpr gains ResolvedFreeRoutine: TObject — a TMethodDecl
      pointer populated by semantic when the inner expression is a
      bare identifier naming a standalone routine.  Codegen reads
      MD.ResolvedQbeName through this without re-walking the symbol
      table, and the field lives where it's used (the address-of
      node) rather than polluting TIdentExpr.

  uSemantic:
    - IsUnmangledUnit allowlist (System, rtl.*, blaise_*, empty)
      and MangleUnitPrefix helper.
    - TSemanticAnalyser.CurrentUnitPrefix — '' in program mode,
      MangleUnitPrefix(FCurrentUnitName) in unit mode (FProg=nil).
    - AnalyseUnitForExport sets FTable.DefineOwningUnit := AUnit.Name
      for the duration of the pass.
    - AnalyseAddrOfExpr stashes MD on AExpr.ResolvedFreeRoutine.

  uCodeGenQBE:
    - ClassUnitPrefix(AClassName) consults FSymTable.Lookup for the
      class's TSymbol.OwningUnit and applies the same allowlist
      semantics as uSemantic.MangleUnitPrefix.
    - ClassSymName(AClassName) = ClassUnitPrefix + AClassName — the
      identifying suffix used everywhere class-data symbols are
      constructed.

Behavior:
  Free routines and class methods: ResolvedQbeName construction sites
  in both AnalyseUnit and AnalyseUnitForExport prepend
  CurrentUnitPrefix; vtable slot values get the same prefix.
  Class data emissions ($typeinfo_, $vtable_, $impllist_, $itab_,
  $__cn_, $_FieldCleanup_) all go through ClassSymName(TD.Name).
  EmitInterfaceDefs threads ClassSymName(TD.Name) into the itab's
  method-name slot and ClassSymName(IntfDesc.Name) into the
  impllist's typeinfo slot — without this, vtable references the
  prefixed name while the methods/typeinfos themselves carry the
  bare name and link fails.
  @-of-routine references read TMethodDecl(ResolvedFreeRoutine)
  .ResolvedQbeName when available, falling back to the bare source
  name (for any TAddrOfExpr constructed post-semantic).

End-to-end verified:
  - Streams program (TFileOutputStream et al.) compiles + runs.
  - TObject program compiles + runs.
  - Cross-unit free-routine separate compile:
      blaise MyDep.pas -> mydep.o (exports MyDep_Triple)
      blaise UseMyDep.pas --skip-dep-codegen + qbe + cc
      link succeeds, program prints 21.
  - Compiler self-bootstraps clean (TThread.Start's
    @ThreadTrampoline emits as $Classes_ThreadTrampoline,
    matching the prefixed definition).
2026-06-03 19:36:07 +01:00
Andrew Haines 4bc54be1d2 feat(codegen,runtime): emit System-unit defs in unit-mode (6c-J phase 2)
Phase 2 of the two-phase 6c-J split — phase 1 (commit 3ab77ab)
reserved FSystemDefsEmitted; this commit wires it.

uCodeGenQBE:
  - AppendUnit, on a class-bearing unit, emits the System-unit
    (TObject / TCustomAttribute) FieldCleanup stubs + typeinfo +
    vtable as locals once per codegen pass.  Sets
    FSystemDefsEmitted after the second emission block so
    subsequent calls (other deps, then AppendProgram) skip.
  - EmitTypeInfoDefs / EmitVTableDefs / EmitFieldCleanupDefs each
    gate their System-unit-only portion on FSystemDefsEmitted.
    Per-class emissions continue unconditionally.  Only the flag
    *check* is added here — the flag is set exclusively by
    AppendUnit so a program-only build path (no AppendUnit calls,
    e.g. Generate) still emits System defs from
    EmitTypeInfoDefs/Vtable/Cleanup as before.

Why two phases: the existing binary at session start lacked the
field.  Adding gate-checks + AppendUnit emission in one commit
produces a binary whose stage-1 image emits duplicate System defs
when compiling its own next iteration (each AppendUnit run +
Emit*Defs all hit the symbol).  Phase 1 reserves the field so
stage-1 binaries understand it.  Phase 2 (this commit) is
compiled by a phase-1 binary whose Emit*Defs still unconditionally
emit and whose AppendUnit emits nothing — net effect: a single
set of System defs in the resulting IR.  The phase-2 binary then
self-hosts cleanly because it now has all four code paths
correctly gated.

runtime:
  - rtl.platform.posix collapses from the build-driver + sed
    pipeline to a single `blaise --source ... --output ...`
    invocation, matching the other 7 RTL units.
  - rtl.platform.posix_build_driver.pas deleted — the last shim.

Pre-commit gate:
  - mcp compile: OK
  - phase-2 binary self-hosts cleanly through another mcp compile
  - 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:34:11 +01:00
Andrew Haines 511d0bc901 chore(codegen): reserve FSystemDefsEmitted (phase 1 of 6c-J)
No-op reservation for the AppendUnit System-unit (TObject /
TCustomAttribute) emission landing in phase 2.  This commit only:

- Adds FSystemDefsEmitted: Boolean to TCodeGenQBE.
- Initializes it to False in the constructor.

Nothing reads or writes it yet.  Splitting the work this way lets
a stage-1 binary built from this commit understand the symbol
*before* the next commit starts checking it from four call sites
(AppendUnit + the three Emit*Defs).  Without this phase, a partial
intermediate state emits duplicate System-unit defs and the
resulting binary can't compile its own next iteration —
chicken-and-egg observed earlier in this session and documented in
memory:project_unit_as_toplevel.md.

Pre-commit gate:
  - mcp compile: OK
  - bootstrap binary updated cleanly
2026-06-03 19:34:11 +01:00
Andrew Haines 7db947ccb0 build(runtime): drop 7 build-driver shims, use unit-as-top-level
project_unit_as_toplevel landed last commit — blaise now accepts a
unit directly as the top-level source, with --output producing a
standalone .o (with embedded .bif).  This collapses each RTL unit's
4-step pipeline (driver shim → emit-ir → sed strip → qbe → cc -c)
into a single invocation.

Deleted (7 driver shims, mechanical wrappers):
  blaise_arc_build_driver.pas
  blaise_exc_build_driver.pas
  blaise_float_build_driver.pas
  blaise_mem_build_driver.pas
  blaise_str_build_driver.pas
  blaise_thread_build_driver.pas
  blaise_weak_build_driver.pas

Makefile rule pattern shrinks from:

  $(OBJ_DIR)/X.ssa: $(PAS_SRC_DIR)/X.pas $(PAS_SRC_DIR)/X_build_driver.pas
      $(BLAISE) --source $(PAS_SRC_DIR)/X_build_driver.pas \
                --unit-path $(PAS_SRC_DIR) --emit-ir \
      | sed '/^# Generated by Blaise/,$$d' > $@
  $(OBJ_DIR)/X.s: $(OBJ_DIR)/X.ssa
      $(QBE) -o $@ $<
  $(OBJ_DIR)/X.o: $(OBJ_DIR)/X.s
      $(CC) -c -o $@ $<

to:

  $(OBJ_DIR)/X.o: $(PAS_SRC_DIR)/X.pas
      $(BLAISE) --source $< --unit-path $(PAS_SRC_DIR) --output $@

Carve-out: rtl.platform.posix keeps the build-driver + sed pipeline.
That .o is currently the sole anchor of typeinfo_TObject /
vtable_TObject in blaise_rtl.a — they're emitted by the "compile
as program" path because TObject is reachable from the implicit
System unit there, but unit-mode codegen only emits the unit's own
classes (TRtlPlatform / TRtlPlatformPosix) and leaves TObject as
an undefined extern.  Gating System-unit emission on reachability
inside unit-mode codegen is the follow-up that would let this last
carve-out collapse too.

Each new .o also carries an embedded .bif (6c-G), so the resulting
blaise_rtl.a + objcopy --dump-section can recover the per-unit
interface — useful for future tooling.

Pre-commit gate:
  - mcp compile: OK
  - TestRunner: 2249 tests, 2 pre-existing failures only
  - fixpoint.sh: FIXPOINT_OK at stage-3/stage-4
2026-06-03 19:34:11 +01:00
Andrew Haines dfabf0c2d8 feat(parser,driver): unit-as-top-level + --skip-dep-codegen
blaise --source <unit>.pas now parses and compiles a Blaise unit
directly, producing a relocatable .o that exports the unit's
interface-section routines.

Parser:
  TParser.IsUnitTopLevel — peeks the primed first token, True
  when it's tkUnit.

Semantic:
  TSemanticAnalyser.GetSymbolTable read-accessor — codegen needs
  the analyser's symbol table in unit-mode where no TProgram
  exists to hand it off.

Driver (Blaise.pas):
  - Parse phase forks on IsUnitTopLevel: TopUnit (TUnit) or
    Prog (TProgram), with IsUnitMode tracking which arm took.
  - Semantic phase uses TopUnit.UsedUnits for dep loading and
    AnalyseUnitForExport(TopUnit) for the top-level pass.
  - Codegen forks on IsUnitMode: AppendUnit(TopUnit) only, no
    AppendProgram, so no @main lands in the IR.
  - New CompileUnitToObject runs qbe → .s → cc -c → .o, stopping
    at the object file (no link, no RTL).

  - --skip-dep-codegen omits the AppendUnit pass for dep units —
    cross-unit call sites become unresolved externs.  Caller
    must supply pre-built dep object files at link time.

Together these are the minimum primitives for separate
compilation.  The .bif-in-.o pipeline that consumes them lands
in a later feature.
2026-06-03 19:34:11 +01:00
Graeme Geldenhuys 6cb2012bd9 feat(native): M7e complete — open arrays and dynamic arrays
Open-array parameters: two-register ABI (data ptr + high index).
  - BuildFrame: IntIdx2 tracks register slots (open arrays consume 2).
  - EmitFunctionDef prologue: spills both data-ptr and high-index regs.
  - EmitCall: open-array args expand to (data_ptr, high) push pair.
  - EmitOpenArrayLiteral: allocates stack storage for [a,b,c] literals,
    fills elements, returns bytes allocated for post-call cleanup.
  - Static-array-to-open-array coercion: leaq base + compile-time high.
  - Open-array forwarding: pass through from caller's _var_N/_var_N_high.

Intrinsics for all array flavours:
  - High(A): open array → _high slot; static → compile-time HighBound;
    dynamic → _DynArrayLength - 1; string → StringLength - 1.
  - Low(A): static → LowBound; all others → 0.
  - Length(A): static → compile-time count; open → _high + 1;
    dynamic → _DynArrayLength; string → StringLength.

Dynamic arrays:
  - tyDynArray added to IntByteSize (8 bytes, pointer-sized slot).
  - Global var registration extended to include tyDynArray.
  - A[I] read: EmitExprToEax path for tyDynArray subscript.
  - A[I] := V write: TStaticSubscriptAssign handles tyDynArray.
  - SetLength(A, N): calls _DynArraySetLength(ptr, n, elemsize),
    stores new data ptr back into the variable slot.

8 new AssertRunsOnBoth e2e tests: inline literal sum, High/Low/Length,
static-to-open coercion (length, sum, nested pass-through), dynamic
array SetLength+access and Length/High. 2420 tests pass.
2026-06-03 19:00:32 +01:00
Graeme Geldenhuys f4f7dbb767 feat(native): M7d complete — exception handling parity with QBE backend
try/finally: _PushExcFrame + _blaise_setjmp dispatch; normal path pops frame
and runs finally body; exception path captures exc, pops frame, runs finally,
re-raises via _Reraise.

try/except: typed handlers matched via _IsInstance (exception in %r15), bare
catch-all body, else body for unmatched exceptions, re-raise when no handler
matches.

raise: evaluates object expression into %rdi and calls _Raise; bare raise
retrieves _CurrentException into %rdi and calls _Reraise.

Exit/Break/Continue through active try frames call EmitExcUnwind, which pops
each frame and emits the finally body inline (try/except frames have nil in
FFinallyStack so only the pop is emitted).

Exception frame allocation:
- Functions: CountTryStmts pre-allocates 512-byte, 16-byte-aligned slots
  in BuildFrame (negative %rbp offsets, _exc_frame_N names).
- Program main: FProgExcFrameCount counts try stmts; 512-byte .data globals
  (_exc_frame_N) emitted in EmitDataSection.

FFinallyStack uses TList<TCompoundStmt> for index-aligned nil-safe access
(TObjectList.Delete unconditionally calls _ClassRelease, which crashes on nil).

10 new AssertRunsOnBoth e2e tests covering: bare try/finally, nested finally,
exit-through-finally, break-through-finally, bare try/except, typed handler,
subclass match, bare-raise propagation, else body, exit-through-nested-finally.

2412 tests pass; FIXPOINT_OK.
2026-06-03 18:33:25 +01:00
Graeme Geldenhuys 2e7876cb9f feat(native): M7c complete — string operations parity with QBE backend
String literals emitted as immortal ARC blobs in .rodata (__sN labels);
leaq __sN+12(%rip) loads the data pointer convention. String assignment
uses _StringAddRef/_StringRelease ARC. WriteLn(S) routes to _SysWriteStr.
Concatenation (boAdd on tyString) calls _StringConcat. Subscript S[I]
calls _OrdAt. Built-ins: Length, Pos, Copy, UpperCase, LowerCase, Trim,
SameText, IntToStr, StrToInt, PChar (identity), string()→_StringFromPChar.
Format(Fmt,...) builds a 16-byte-per-slot args array on the stack and
calls _StringFormatN. Delete/SetLength use ARC reassignment. String
params and return values are 8-byte pointers. String locals released in
the function epilogue. 19 new AssertRunsOnBoth e2e tests.
2402 tests pass; FIXPOINT_OK.
2026-06-03 16:42:24 +01:00
Graeme Geldenhuys da57f0a00c chore: remove progress-native-backend.txt from tracking; add to .gitignore 2026-06-03 14:38:13 +01:00
Graeme Geldenhuys d6c98c1459 feat(native): M7b complete — class system, method dispatch, method pointers
Implements the full class system for the native x86_64 backend:

Data section:
- EmitClassSection emits .data vtables (typeinfo ptr + virtual method ptrs),
  typeinfo blocks (8-quad: parent, itab, class-name ptr, method table, size,
  field-cleanup fn, vtable, attrs), class-name strings (ARC header + ASCII
  null-terminated), and method tables (count + name-ptr/code pairs).
- EmitFieldCleanupFn emits _FieldCleanup_<Class> stubs in .text.
- NativeMangle strips the leading $ from vtable ImplName entries (StrAt/
  StrCopyTail) to match the QBE backend mangling convention.
- EmitClassNameString is idempotent (FClassNameEmitted dict) so MethodAddress
  and EmitClassSection can both reference the same label without duplicating it.

Code section:
- EmitClassMethods iterates AProg.Block.TypeDecls (not ProcDecls) so that
  class method bodies in CD.Methods (post LinkClassMethodImpls) are emitted.
- Constructor calls: _ClassAlloc(size, cleanup) -> wire vtable ptr -> call ctor body.
- Class variable assignment: _ClassAddRef(new), load old, _ClassRelease(old), store new.
- Class field access (read/write) through Self pointer via leaq + field offset.
- MethodAddress resolves to a _MethodAddress(instance, name_ptr) RTL call using
  the class-name string +12 (ASCII data) offset.

Method-pointer (of-object) calls:
- EmitMethodPtrCall: leaq slot->%rcx; movq 0(%rcx)->%r10 (Code);
  movq 8(%rcx)->%r11 (Data); args into %rsi/%rdx/...; movq %r11,%rdi; callq *%r10.
- Method-pointer cast assignment (TFoo(M)) copies 16 bytes via memcpy.
- tyProcedural IsMethodPtr globals now emit two .quad 0 entries (16 bytes) in
  EmitDataSection; tyClass globals registered and emitted as .quad 0 (8 bytes).

Also adds --emit-asm flag to the compiler CLI:
- Prints native assembly to stdout (analogous to --emit-ir for QBE IR).
- Implies --backend native; skips the cc linking step.
- --output is not required when --emit-asm is set.

TestRun_Native_IndirectCall_MethodPtr promoted from Ignore to AssertRunsOnBoth.
2383 tests pass.
2026-06-03 14:27:09 +01:00
Graeme Geldenhuys 23ee55e0ca docs(progress): mark M7a complete — sret record-returning functions done; M7b (class system) scoped 2026-06-03 14:02:11 +01:00
Graeme Geldenhuys bb1cfb7676 feat(codegen): native backend M7a — record-returning functions (sret)
Implement the sret calling convention for functions/procedures that
return a record type, matching the QBE backend's hidden-first-pointer
approach:

Callee side (EmitFunctionDef):
- FSretFunc flag set in BuildFrame when ResolvedReturnType.Kind = tyRecord
- Result slot becomes an 8-byte pointer slot (nil type = pointer-size)
- Prologue spills the hidden sret %rdi into the Result slot (IntIdx starts
  at 1 so normal params continue at %rsi, %rdx, ...)
- No Result initialisation (caller's buffer is already zeroed by caller)
- Epilogue emits plain ret (no return value in %rax/%xmm0)

Field writes through Result (TFieldAssignment with RecordName='Result'):
- Load the sret pointer from the Result slot into %rcx
- Write through %rcx + field offset

Caller side (EmitSretCall):
- leaq dest → %r10 before arg evaluation (survives clobbers)
- Call memset(%r10, 0, TotalSize) to zero the destination buffer
- Reload %r10 after memset (caller-saves may be clobbered)
- Evaluate normal args and push; pop into %rsi/%rdx/... (index 1+)
- movq %r10, %rdi to place sret pointer as hidden first arg
- callq function

TAssignment detection: when LHS is a record and RHS is a record-returning
TFuncCallExpr, dispatch to EmitSretCall instead of EmitExprToEax.

TestRun_Native_RecordReturnFunction promoted from Ignore to AssertRunsOnBoth.
2383 tests pass; FIXPOINT_OK.
2026-06-03 13:57:14 +01:00
Graeme Geldenhuys 07b73bf420 docs(progress): mark M6 complete — float parity in native backend 2026-06-03 13:38:02 +01:00
Graeme Geldenhuys c09664dd76 feat(codegen): native backend M6 — float (Double/Single) parity
Double and Single variables, literals, arithmetic, comparisons,
WriteLn output, value parameters, and function return now work in
the native x86_64 backend, matching QBE output on all test cases.

Implementation details:
- EmitExprToXmm0: evaluates float expressions into %xmm0.  Binary ops
  use the stack (subq/movsd) to save left operand, evaluate right into
  %xmm0, then pop left into %xmm1 and combine via addsd/subsd/mulsd/
  divsd (boSlash, not boDiv, for `/`).
- EmitLoadFloat / EmitStoreFloat: movsd/movss between memory and %xmm0.
- Float globals: .double 0.0 / .float 0.0 in .data section; registered
  in EmitProgram alongside integer globals.
- Float literals: stored as .double/.float constants in .rodata, loaded
  via RIP-relative movsd/movss.  Label prefix .LF to avoid collision
  with loop labels.
- Single coercion: when LHS is Single and literal is Double (all
  TFloatLiterals default to tyDouble), cvtsd2ss converts before store.
- Float comparisons: EmitCondBranch detects float operands and emits
  ucomisd/ucomiss + conditional jump directly (CF/ZF flags).
- EmitFunctionDef prologue: spills float params from %xmm0/%xmm1/...
  (tracked by XmmIdx counter, independent of integer IntIdx counter).
  Result slot initialised with xorpd/xorps; epilogue loads Result into
  %xmm0 for float return types.
- EmitCall: detects mixed/float param calls; pre-allocates N×8 stack
  slots, evaluates args left-to-right into slots, then loads into
  separate int/xmm register sets.  Pure-integer calls use the existing
  push/pop strategy unchanged.
- EmitWrite: checks tyDouble/tySingle before integer fallthrough;
  evaluates via EmitExprToXmm0 and dispatches to _SysWriteDouble /
  _SysWriteSingle with fd in %edi, value in %xmm0.

8 new e2e tests (all AssertRunsOnBoth): DoubleGlobal, DoubleLocal,
DoubleArithmetic, DoubleComparison, DoubleWriteLn, SingleGlobal,
DoubleFuncParam, DoubleFuncReturn.  2383 tests pass; FIXPOINT_OK.
2026-06-03 13:37:43 +01:00
Graeme Geldenhuys f4baabb9ce fix(codegen): WriteLn(Double/Single) crashed QBE with type mismatch
EmitWrite had no tyDouble/tySingle case; floats fell through to
_SysWriteInt with a d-typed temp, which QBE rejected as an invalid
argument type.

Add _SysWriteDouble and _SysWriteSingle RTL stubs in rtl.platform.pas
and rtl.platform.posix.pas (using the existing _DoubleToStr/_SingleToStr
functions from blaise_float.pas).  EmitWrite now dispatches to these
stubs so WriteLn(someDouble) and WriteLn(someSingle) compile and run
correctly without requiring DoubleToStr at the call site.

Two new e2e tests (TestRun_WriteLn_Double_Direct, TestRun_WriteLn_Single_Direct)
verify direct float output; 2375 tests pass.
2026-06-03 13:22:10 +01:00
Graeme Geldenhuys 613f326fa1 test(native): M5 complete — method-pointer call deferred to M7 with placeholder
Method-pointer calls (of object) depend on class allocation and field
access not yet in the native backend; deferred to M7 alongside full class
support.  Added TestRun_Native_IndirectCall_MethodPtr as a placeholder:
QBE path verified via CompileAndRunWithRTL, native path Ignored with a
clear TODO M7 comment.  When M7 lands, replace Ignore with AssertRunsOnBoth.

M5 is now complete for the integer-family subset.
2026-06-03 13:01:46 +01:00
Graeme Geldenhuys dd30d4d7cd test(native): add M7 placeholder for record-returning functions
QBE path is verified and asserted; native path is Ignored with a
TODO M7 comment until sret/aggregate return is implemented.  When M7
lands, replace Ignore with AssertRunsOnBoth.
2026-06-03 12:55:23 +01:00
Graeme Geldenhuys e802c0f072 feat(codegen): native backend M4 — static array element read/write
Implement TStaticSubscriptAssign (write A[I] := V) and static array
element reads (TStringSubscriptExpr with tyStaticArray inner) for
integer-family elements.  Handles zero and non-zero LowBound, local
frame slots and global RIP-relative storage.

Element address: base + (Index − LowBound) × ElementType.RawSize,
computed via imulq/addq.  TIdentExpr with tyStaticArray returns the
array's base address via leaq (not a scalar load) so subscript
expressions can use it as a base pointer.

Frame and data section:
- AddSlot uses AType.RawSize (rounded to 8) for tyStaticArray, matching
  the existing record handling.
- EmitDataSection emits .skip N / .balign 8 for global array vars.
- EmitProgram registers array-typed globals alongside integer and record
  globals.

Tests: 3 new e2e tests run on both backends via AssertRunsOnBoth —
StaticArrayGlobal, StaticArrayLocal, StaticArrayNonZeroLow.

M4 is now complete.
2026-06-03 12:34:32 +01:00
Graeme Geldenhuys d2fbb80911 feat(codegen): native backend M4 — record field read/write
Implement TFieldAssignment (write) and TFieldAccessExpr (read) for
integer-family fields of record-typed locals and globals.

Layout:
- AddSlot now allocates TotalSize bytes (rounded to the next 8-byte
  boundary) for record-typed frame slots, not a fixed 8.
- EmitDataSection emits .skip N / .balign 8 for global record vars.
- EmitProgram registers record-typed globals alongside integer globals.

Field address computation: base address + FieldInfo.Offset.  When
offset = 0 the base operand (VarOperand or name(%rip)) is used directly;
non-zero offsets leaq the base into %rcx and then use N(%rcx).

IntByteSize already returns 8 for pointer-width types (tyProcedural etc)
from the previous commit; no change needed there.

Tests: 3 new e2e tests run on both backends via AssertRunsOnBoth —
RecordGlobal (program-level record var), RecordLocal (record local inside
a function), RecordParam (record fields passed as scalar arguments).
2026-06-03 12:28:11 +01:00
Graeme Geldenhuys c419e054b5 feat(codegen): native backend M5 — bare indirect (procedural-type) calls
Support calling through a procedural-type variable (function/procedure
pointer):

  @FuncName in expression context: leaq funcname(%rip), %rax (TAddrOfExpr
  with a tyProcedural inner TIdentExpr).

  EmitCallIndirect: loads the pointer from the variable slot into %r10
  (caller-saved, not clobbered by arg evaluation in %rax), pushes args
  left-to-right, pops into SysV arg registers, then callq *%r10.  Wired
  into TProcCall.IsIndirectCall (statement) and TFuncCallExpr.IsIndirectCall
  (expression).

  IntByteSize: extended to return 8 for pointer-width types (tyProcedural,
  tyPointer, tyClass, tyString, tyPChar, tyMetaClass) — previously fell
  through to the default 4, causing proc-pointer globals to be stored/loaded
  as .long/.movl, silently truncating the 64-bit code address.

Tests: 2 new e2e tests (BareProc, BareFunc) run on both backends via
AssertRunsOnBoth.  Method pointers (of object) deferred pending
TFieldAssignment support.
2026-06-03 12:09:46 +01:00
Graeme Geldenhuys f47849a2aa feat(codegen): native backend M5 — >6 integer arguments via stack passing
SysV AMD64 ABI: args 0-5 in rdi/rsi/rdx/rcx/r8/r9; args 6-7 on the stack
at +16(%rbp)/+24(%rbp) in the callee (right-to-left push order so arg6 is
closest to the frame).

Caller (EmitCall): evaluates all args left-to-right and pushes them. For >6
args, pops the stack args (highest index first) into %r10/%r11 (caller-saved
scratch), pops reg args into their registers, then re-pushes the stack args in
ABI order (argN-1 deepest, arg6 on top at callq). Cleans up with addq after
the call. Supports up to 8 total args; more raises a clear error.

Callee (BuildFrame): params 0-5 get negative %rbp slots (spilled from regs in
the prologue as before); params 6-7 get positive %rbp offsets (+16, +24) so
VarOperand emits N(%rbp) for them — no spill needed, the caller already placed
them there. AddSlot now stores offsets as negative integers; VarOperand checks
the sign to emit -N(%rbp) for locals vs N(%rbp) for stack params.

Tests: 2 new e2e tests (SevenArgs, EightArgs) run through both QBE and native
backends via AssertRunsOnBoth.
2026-06-03 11:56:13 +01:00
Graeme Geldenhuys 682f4e0121 refactor(native): use diamond operator at all generic constructor call sites
Replace explicit type argument repetition with <> now that the feature is
available.  Five sites in TX86_64Backend: FDataGlobals, FBreakLabels,
FContinueLabels (in Create) and FFrame, FFrameTypes (in BuildFrame).
2026-06-03 11:44:27 +01:00
Graeme Geldenhuys d4f9f9e4a3 feat(lang): diamond operator — infer generic type args from LHS in assignments
TFoo<>.Create on the RHS of an assignment infers all type arguments from
the declared type of the LHS variable, eliminating the redundant repetition:

  var S: TStack<string>;
  S := TStack<>.Create;          { was: TStack<string>.Create }

  var D: TDictionary<string, Integer>;
  D := TDictionary<>.Create;     { infers both K and V }

Works for any number of type parameters.

Implementation:
- Parser: detects IDENT tkNotEquals DOT (the lexer folds '<>' into a single
  tkNotEquals token) in expression context and stores 'TFoo<>' as the sentinel
  RecordName in TFieldAccessExpr.
- Semantic: ResolveDiamond() in AnalyseAssignment replaces the '<>' sentinel
  with the full concrete type name from the resolved LHS type, before the
  normal constructor-call analysis proceeds.

Docs: grammar.ebnf and language-rationale.adoc updated.

Tests: 6 IR-level tests in cp.test.generics (parser sentinel, semantic
inference for 1 and 2 type params, IR identity with explicit form);
2 E2E tests in cp.test.e2e.misc (single-arg and two-arg, compile+run).
2026-06-03 11:29:29 +01:00
Graeme Geldenhuys 8e4608380b refactor(native): replace TStringList stacks/globals with proper generic types
Use semantically correct collection types now that the Pointer→class ARC
fix is in place:

- FBreakLabels/FContinueLabels: TStringList used as LIFO stacks (Add/Delete-
  last/Strings[Count-1]) → TStack<string> with Push/Pop/Peek.

- FDataGlobals + FGlobalTypes (parallel TStringList + TDictionary): merged
  into a single TOrderedDictionary<string, TTypeDesc>.  ContainsKey replaces
  IndexOf, Keys[I] replaces Strings[I], and TryGetValue works directly on the
  one map.  Insertion order is preserved for .data section emission.
2026-06-03 11:05:48 +01:00
Graeme Geldenhuys dcd70a7176 fix(arc): emit _ClassAddRef when assigning Pointer to class-typed variable
When a Pointer-typed expression (e.g. from TStringList.Objects[]) is
assigned to a class-typed ARC-managed local, the codegen previously fell
through to a plain scalar store — emitting no _ClassAddRef.  The variable
then received a spurious _ClassRelease at scope exit, imbalancing the
refcount.

This was the root cause of the 3rd-generic-instantiation linker error:
FindGeneric returns TObject from FGenerics.Objects[] (Pointer-typed), so
each call to InstantiateGenericInterface emitted one extra _ClassRelease
on the template object.  After two calls the template was destroyed; the
third call received a dangling pointer.

Fix: add a branch in EmitAssignment for
  ResolvedLhsType.Kind = tyClass AND Expr.ResolvedType.Kind = tyPointer
that mirrors the existing tyClass→tyClass path (retain new, release old,
store), always emitting _ClassAddRef since a raw Pointer value never
carries a pre-existing owned reference.

Also remove the debug WriteLn trace left in TGenericInterfaceDef.Destroy.

Tests added:
- cp.test.arc: IR-level check that Pointer→class assign emits _ClassAddRef
- cp.test.e2e.arc: three instantiations of the same generic class all
  compile, link, and produce correct output
2026-06-03 10:55:18 +01:00