Commit graph

446 commits

Author SHA1 Message Date
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
Graeme Geldenhuys f5f99ab78a feat(codegen): native backend M5 — break/continue/exit + test upgrades
Add break, continue, and exit statement support to the native x86_64
backend.  FBreakLabels/FContinueLabels stacks are pushed before each
loop body and popped after; break jumps to the loop-end label, continue
to the increment (for) or condition (while/repeat) label.  Exit jumps
to FExitLabel at the function epilogue; Exit(Value) uses the semantic
pass's synthesised ResultAssign.

Upgrade six existing QBE-only e2e tests to AssertRunsOnBoth: ForBreak,
ExitFromFunction, ConstInt, ConstNeg, DefaultParam, TypeCastIntByte.
Four new native e2e tests: ForBreak, WhileContinue, ExitFromFunction,
ExitValueShorthand.

2351 tests pass; FIXPOINT_OK.
2026-06-03 09:56:52 +01:00
Graeme Geldenhuys dc7680fe93 feat(codegen): native backend M5 — var/out parameter support
Var/out params pass the caller's address via leaq (locals/globals);
the param slot holds a pointer (always movq spill); reads dereference
through the pointer, writes store through it.  Pass-through (var param
forwarded to another var param) loads the pointer from the slot.

Uses AST IsVarParam flags from the semantic analyser — simpler and
consistent with the QBE backend.  Wider-int var params (Int64, Byte)
dereference at the declared type's width.

Existing QBE-only e2e tests (VarParamSwap, ConstParam) upgraded to
AssertRunsOnBoth.  Four new native e2e tests: VarParamSwap,
VarParamPassThrough, VarParamWiderInt, OutParam.

2347 tests pass; FIXPOINT_OK.
2026-06-03 09:23:27 +01:00
Graeme Geldenhuys 2d509cf764 tooling: rolling-bootstrap script for development-checkout builds
Add scripts/rolling-bootstrap.sh to rebuild the self-hosting chain from
the last release binary up to the checked-out revision. A released binary
can only compile source up to its own feature set, so between releases it
can no longer cold-bootstrap master directly — the moment a feature is
taught to the parser in one commit and used in the runtime/compiler in a
later commit, the gap opens. The script replays the chain commit-by-commit
(each step built by the previous step's binary), carrying the binary
forward, and installs the result as releases/v<version>-pre/.

It runs in a throwaway git worktree, so the live tree (including
uncommitted changes) is untouched; it passes QBE= and BLAISE= into the
runtime Makefile and uses the main repo's built QBE; and it smoke-tests
each step. A failed step names the exact commit (BOOTSTRAP_BROKEN),
making it usable as a CI check that the two-step discipline holds.

Document the prerequisite (place the last release binary under
releases/vX.Y.Z/, which is gitignored) and usage in scripts/BOOTSTRAP.adoc,
with a pointer from README.adoc.
2026-06-03 08:05:16 +01:00
Graeme Geldenhuys 6711a6cfdd fix: green the test suite — array-const link collisions and TComponent ARC
Three fixes that were causing six e2e failures:

Local array consts collided at link time. Function/block/unit-level
`const X: array[...] = (...)` were emitted as `export data $X` with no
mangling. The RTL's own `Days` const (rtl.platform.posix.pas) then
clashed with any user program declaring a `Days` const
(`multiple definition of 'Days'`). Local array consts now use a mangled,
file-local label (`__bac_N_Name`): the mangled name is set in semantic
(AnalyseArrayConstDecls -> ResolvedQbeName / ConstArrayQbe), carried to
the bare-ident read path via TIdentExpr.ConstArraySymbol, and emitted as
non-exported `data` so two separately-compiled objects cannot clash.
Class/record consts keep their exported, type-qualified label.

TComponent fought ARC and crashed. The destructor force-Freed children
it held only as raw pointers, while a caller's variable still referenced
them — a use-after-free at scope exit, surfacing as infinite
Destroy->RemoveComponent->ClassRelease recursion. Ownership is now honest
with ARC: FOwner is [Unretained]; AddComponent takes a strong ref via
_ClassAddRef; Destroy releases the owner's hold rather than Freeing, so a
child the caller still holds survives and an unheld child is destroyed;
RemoveComponent only unlinks the slot (the dying child's ref already
reached zero).

Deflaked TestRun_Thread_Terminate_Flag. The worker could finish via its
`I > 1000` break before the main thread's Terminate landed, racing the
printed flag between 0 and 1. The loop now exits only via the terminate
flag (with a high safety cap), so FTerminated is deterministically True.

Unit tests in cp.test.constants.pas updated to assert the mangled,
non-exported label.
2026-06-03 01:49:00 +01:00
Graeme Geldenhuys 576f60d807 fix(stdlib): TStringList.Text/LoadFromFile preserve verbatim lines
SplitIntoList — the sole backend for TStringList.SetText and LoadFromFile —
trimmed leading and trailing spaces from each split segment. That is correct
for CSV-style splitting but wrong for line-splitting: a TStringList must store
each line verbatim, including indentation.

Removed the trimming so each segment is added as Copy(S, Start, Len) untouched.
SplitIntoList has exactly one caller (SetText), so no trimming-dependent code
is affected.

This also fixes a latent test-infrastructure bug: the threaded test runner
parses each subprocess suite's stdout via TStringList.Text and identifies
failure-detail lines by their two-space indentation. With indentation stripped,
those lines never matched, AddFailure was never called, and the merged result
reported zero failures — so the plain summary printed OK even when [Threaded]
suites failed. With lines preserved, the summary now correctly reports failures.

Regression test: TestRun_TextSet_PreservesLeadingWhitespace asserts on raw
stdout so the spaces inside [] are checked without round-tripping through Text.

Self-hosting fixpoint verified clean.
2026-06-03 00:40:00 +01:00
Graeme Geldenhuys 99b7761bf4 refactor: use Exit(X) shorthand for Result-then-Exit early returns
Now that Exit(X) is supported (0f63626), replace the historical
'Result := X; Exit;' workaround with the Exit(X) function-result shorthand
across compiler, runtime, and stdlib (305 sites).

Only same-indentation pairs are merged: a 'Result := X;' immediately followed
by an 'Exit;' at the same indentation level. Sites where the trailing 'Exit;'
is less-indented than the assignment are left untouched — there the Exit
belongs to an enclosing for/else/if block, not to the assignment, so merging
would change control flow (e.g. the xor/and numeric-result branch in
uSemantic falling through to the Boolean-operand check). 14 such sites are
deliberately not converted.

Three converted sites sit inside a real try/finally (uSemantic overload/
generic resolution); each try body already contained bare Exit; statements
and its finally body is a plain local-list Free, so control flow is unchanged.
A new e2e test (TestRun_ExitValueThroughFinally) confirms Exit(X) from inside
a try/finally runs the finally body and returns the value.

Self-hosting fixpoint verified clean (stage-2 IR == stage-3 IR).
2026-06-03 00:33:50 +01:00
Graeme Geldenhuys 0f63626397 feat(lang): Exit(Value) function-result shorthand
Inside a function, Exit(X) now assigns X to Result and returns, matching
Delphi/FPC:

  function Classify(n: Integer): Integer;
  begin
    if n < 0 then Exit(-1);
    if n = 0 then Exit(0);
    Result := 1
  end;

Pipeline:
- AST: TExitStmt gains Value (the parsed X) and ResultAssign (a
  synthesised 'Result := X' built by semantic). CloneStmt copies them.
- Parser: the tkExit branch parses an optional (Expr) after Exit.
- Semantic: Exit(X) is valid only inside a function (Result in scope);
  it is rewritten into a 'Result := X' TAssignment that is analysed like
  any assignment, so it inherits return-type compatibility checking and
  the widening / ARC handling for string and class returns. Exit(X) in a
  procedure, or with a type-incompatible X, is a clear error.
- Codegen: emits the synthesised assignment (via EmitAssignment) before
  the normal exit jump; CollectAddressTakenStmt walks it too. Bare Exit
  is unchanged.

Tests: 5 IR/semantic cases in cp.test.flowjumps (parse attaches value;
function OK; procedure + type-mismatch errors; codegen stores Result
then jumps) and an e2e in cp.test.e2e.controlflow covering int and
string (ARC) returns plus fall-through. Grammar (ExitStmt) and
language-rationale updated. Full suite 2341 tests pass; fixpoint clean.
2026-06-03 00:06:52 +01:00
Graeme Geldenhuys 703e44dfa1 feat(semantic): accept set literals as 'set of' arguments
A bracket literal can now be passed directly where a `set of <enum>`
parameter is expected, e.g. Configure([optA, optC]) or Report([]) — no
named intermediate variable needed.

Previously such a call failed overload resolution: a bare [a, b] is
analysed (lacking set context) as an open-array of the enum, which does
not match the set parameter. Now:

- ArgMatchScore matches a TArrayLiteralExpr argument against a set
  parameter — non-empty when the members share the param's base enum,
  and the empty [] against any set — checked before the nil-arg bail so
  [] (which has no resolved type) is handled.
- AnalyseArrayLiteralExpr defers an empty [] to a nil type instead of
  erroring outright; it is only meaningful with a set target.
- After overload resolution, RetypeSetLiteralArgs re-points each matched
  set-literal argument's ResolvedType at the parameter's set type, so
  codegen emits the bitmask (EmitArrayLiteralExpr dispatches on tySet).
- CheckTypesMatch gained a nil-actual guard (a clean "no value type"
  error instead of a segfault on a stray []), and the assignment path
  rejects an empty [] assigned to a non-set LHS.

Fixes a second, latent bug exposed by passing sets by value: a `set of`
value parameter of <=32 members (QBE type w) was spilled in the prologue
with storel, which QBE rejects. Added a tySet case to both param-spill
sites (storew for w sets, storel for l sets).

Tests: 6 IR/semantic cases in cp.test.sets (arg OK / empty / wrong-enum
/ empty-non-set-assign fail, bitmask fold, w-width param spill) and an
e2e in cp.test.e2e.misc. language-rationale updated. Full suite 2335
tests pass; fixpoint clean.
2026-06-02 20:04:17 +01:00
Graeme Geldenhuys 62165dac3d feat(const): set-valued constants (const X = [a, b])
Allow a const declaration to take a set literal on its RHS, e.g.

  const
    Primary       = [cRed, cBlue];        // type inferred: set of TColor
    Both: TDirSet = [dNorth, dEast];      // type annotated
    None: TDirSet = [];                   // empty set

Parser: a tkLBracket branch in ParseConstBlock parses the member
identifier list (or empty []) onto new TConstDecl.IsSet / SetElements.

Semantic: set consts are resolved in the second constant pass
(AnalyseArrayConstDecls), after AnalyseTypeDecls has registered the enum
members. AnalyseSetConstDecl resolves each member to its enum ordinal,
ORs (1 shl ord) into the bitmask, and registers the const with a tySet
type — the declared set type when annotated (members checked against its
base enum), otherwise the inferred 'set of <Enum>' (found-or-created and
defined globally). Members must share one enum; a non-enum member or an
empty unannotated set is a clear error.

CheckTypesMatch now treats two tySet types over the same base enum as the
same type, so an inferred 'set of TDir' const assigns to a TDirSet
variable — set values are structural, not nominal.

Codegen needs no change: a set const is an integer bitmask, emitted by
the existing constant-ident path.

Tests: 9 IR/semantic cases in cp.test.sets (parse, inferred/annotated/
empty OK, mixed-enum / non-enum / empty-unannotated failures, bitmask
fold), plus an e2e in cp.test.e2e.misc. Grammar (ConstRhs) and
language-rationale updated. Full suite 2328 tests pass; fixpoint clean.
2026-06-02 19:48:27 +01:00
Graeme Geldenhuys 3bf5fad1ae test(e2e): run native suite through both QBE and native backends
Add a both-backend e2e harness so a single test exercises both code
generators against one expected value — the native backend's correctness
model is parity with QBE on the same source, so this is the natural way
to test it.

- TBackend = (beQBE, beNative); CompileAndRunOn(backend, ...) is the
  shared compile+run core. CompileAndRun and CompileAndRunNative are now
  thin wrappers over it (one path instead of three near-duplicates).
- AssertRunsOnBoth(Src, ExpectedOut, ExpectedCode) compiles+runs on both
  backends and asserts each matches, tagging failures [qbe] / [native]
  so a one-backend regression is unambiguous.
- The native suite's 20 tests now call AssertRunsOnBoth, so each runs on
  QBE and native (was native-only).

A per-method [Backends(...)] attribute would read better but needs
method-level attribute RTTI (a layout-changing compiler feature Blaise
lacks); AssertRunsOnBoth gives the same per-method granularity today with
no compiler change. Two compiler gaps found and logged in bugs.txt: a
set-valued const, and passing a set literal to a 'set of <enum>'
parameter — both worked around by not using a set-typed API.

Full suite 2318 tests pass; fixpoint clean (test-only change, IR
unaffected).
2026-06-02 19:34:57 +01:00
Graeme Geldenhuys 577b3fc160 chore: remove FPC conditional-compilation directives
Blaise cannot be compiled with FPC, so every {$IFDEF FPC} block was dead
code. Remove them across the compiler:

- uPasTokeniser: collapse the {$IFDEF FPC}/{$ELSE} PosOrd/PosSubstr shims
  to the Blaise (0-based) branch only.
- uSemantic, uParser: drop {$IFDEF FPC}...Free{$ENDIF} manual frees (Blaise
  is ARC; the frees were FPC-only and never compiled under Blaise).
- uConfig: drop the stray {$mode objfpc}{$H+} directive — no other unit
  carries one.

No behavioural change: the removed blocks emitted nothing under Blaise.
Full suite 2318 tests pass; fixpoint IR byte-identical (FIXPOINT_OK).
2026-06-02 18:58:58 +01:00
Graeme Geldenhuys 6b5c2b2ce4 refactor(codegen): native backend slot maps use TTypeDesc, drop encoding
With the generic-template lifetime bug fixed, the native x86_64 backend
can again hold a second TDictionary instantiation in its own unit, so
the per-slot type maps revert from the small-integer encoding workaround
back to a plain TDictionary<string,TTypeDesc>.

Removes EncodeIntType/DecodedSize/DecodedUnsigned and the *Code helper
variants; EmitLoadVar/EmitStoreVar/EmitSpillArg/AddGlobal and the frame/
global slot maps now carry TTypeDesc directly, and width/signedness is
read straight off the type via IntByteSize/IsUnsignedInt at each use.
Behaviour is unchanged — same instructions emitted; this is a clarity
cleanup now that the encoding is no longer needed.

Full suite 2318 tests pass; fixpoint clean (verified with a stage-1 that
carries the generics fix; the local v0.10.0-pre reference binary was
refreshed to that fixpoint-verified binary so the default bootstrap
stays self-consistent).
2026-06-02 18:58:58 +01:00
Graeme Geldenhuys d1d89125bf fix(semantic): retain generic templates so repeat instantiation works
A second distinct instantiation of a generic whose template inherits
from a generic interface (e.g. TDictionary<K,V> = class(IMap<K,V>)) was
silently mis-wired: it got no TObject parent and no vtable, so its
$vtable_... symbol was referenced from its typeinfo but never emitted,
and linking failed with an undefined-reference error. Plain generic
classes and single instantiations were unaffected.

Root cause: TSymbolTable.FGenerics (base name -> template AST node) was
non-owning. The IMap template AST node was freed during the first
instantiation and its heap slot reused by an unrelated object; the next
FindGeneric('IMap') returned that dangling slot, the `is
TGenericInterfaceDef` test failed, InstantiateGenericInterface bailed,
the cloned class's parent name was left as 'IMap<...>' instead of moving
to ImplementsNames, and the parent/vtable wiring no-op'd.

Fix: retain a strong reference to every registered template in a new
owning list (FGenericTemplates) so templates outlive any single
instantiation. RegisterGeneric adds the template there in addition to
the FGenerics name index.

Regression test: cp.test.e2e.collections2 compiles a unit holding two
distinct TDictionary instantiations and asserts both link and run. Full
suite 2318 tests pass; fixpoint clean.
2026-06-02 18:58:58 +01:00
Graeme Geldenhuys 563ce0b274 fix(codegen): print unsigned 32-bit values as unsigned in WriteLn
WriteLn of a Cardinal/UInt32 (or Word) value above 2^31 printed a
negative signed wrap because both backends routed it through the signed
32-bit _SysWriteInt runtime routine (e.g. Cardinal 3000000000 printed as
-1294967296).

Route unsigned 32-bit arguments (tyUInt32, tyWord) through the existing
_SysWriteUInt64 routine instead, zero-extending the value to 64 bits
first. Byte/Boolean/Enum stay on the signed 32-bit writer: their value
range is always non-negative there, matching prior behaviour. Int64 and
UInt64 paths are unchanged.

Both the QBE backend (extuw before the call) and the native x86_64
backend (the value is already zero-extended in %rax) get the fix, keeping
them in lockstep so the native backend's QBE-parity oracle still holds.

Adds an e2e test in each suite (cp.test.e2e.misc and cp.test.e2e.native)
asserting Cardinal 3000000000 prints as 3000000000. Full suite 2317
tests pass; fixpoint clean.
2026-06-02 18:58:58 +01:00
Graeme Geldenhuys 7a077ab72c feat(codegen): native backend M5 — wider integer family + conversions
Extend the x86_64 native backend beyond 32-bit Integer to the full
integer family (Byte, Word, SmallInt, Int64, UInt32, UInt64, Boolean,
Enum) as program globals, function locals, value parameters and return
values, plus explicit type-cast conversions.

Integer values now flow 64-bit-extended in %rax: loads sign/zero-extend
by width and signedness (movsbq/movzbq/movswq/movzwq/movslq/movl/movq),
stores re-narrow through the matching register sub-view (%al/%ax/%eax/
%rax), and arithmetic/comparison run in 64-bit (addq/subq/imulq, and
idivq+cqto for signed div/mod, divq with a zeroed %rdx for unsigned).
Explicit casts Byte(X)/Word(X)/Int64(X) lower to a truncate-then-extend.

WriteLn now dispatches _SysWriteInt / _SysWriteInt64 / _SysWriteUInt64
by argument width and signedness, matching the QBE backend oracle.
Frame slots are 8-byte-aligned and sized by type; program globals emit
.byte/.word/.long/.quad to match.

Each slot's width+signedness is encoded as a small Integer so the unit
keeps a single TDictionary<string,Integer> instantiation; a second
distinct instantiation of the same generic in one unit currently drops
a vtable definition (logged in bugs.txt, along with the unsupported
Exit(Value) shorthand and broken nested-procedure emission found while
implementing this).

Five new e2e tests cover wider-int globals, Int64 arithmetic, wider-int
params/return, type-cast conversions and signedness/wraparound. Full
suite 2315 tests pass; fixpoint clean (--emit-ir is unaffected, it
always uses the QBE backend).
2026-06-02 18:58:58 +01:00
Graeme Geldenhuys d1efe5888f refactor(codegen): use TDictionary for the native backend frame map
Replace the TStringList-with-Objects[]-as-offset frame map in the x86_64
backend with TDictionary<string, Integer> from generics.collections. The frame
is a name->offset map looked up on every ident read and assignment, so a
dictionary is the correct container for the access pattern, and it removes the
TObject(PtrUInt(...)) cast hack.

This was blocked until the preceding fix: TDictionary implements IMap<K,V>, and
a generic class implementing a generic interface inside a unit previously failed
to link. With that fixed, the compiler can use TDictionary internally.

Verified: compiler bootstraps with TDictionary used in its own unit; 2310 tests
pass; fixpoint clean.
2026-06-02 18:58:58 +01:00