Commit graph

732 commits

Author SHA1 Message Date
Graeme Geldenhuys fcf4d36fad fix(generics): substitute type param in generic-function body locals
A generic free function with a local variable of the type parameter failed:
    function Echo<T>(X: T): T; var tmp: T; begin tmp := X; Result := tmp end;
    -> "Semantic error: Unknown type 'T'"
Signature substitution (params + return) was applied during instantiation, but
the cloned body's local var declarations still referenced the bare type
parameter, so AnalyseStandaloneDecl saw `var tmp: T` with T unresolved.

InstantiateGenericFunc now substitutes the type parameter in the cloned body's
TVarDecl.TypeName entries (via SubstTypeParam, which already handles T, ^T, and
nested SomeName<T>), so a local of type T (or ^T, or List<T>) resolves to the
concrete type. Generic class/record method bodies already substituted their
locals; this brings free functions in line.

Found by the e2e generics hardening sweep. New file cp.test.e2e.generics.pas
(10 tests, all run on BOTH backends): generic funcs incl. typed locals,
paramless+typed-local-return, two typed locals; generic classes/records;
method typed locals; two type params; distinct instantiations; nesting.

All three fixpoints + full suite (3234 tests) pass.
2026-06-16 10:32:47 +01:00
Graeme Geldenhuys e2b715b53b fix(parser): support nested generic type arguments
TList<TList<Integer>> and TBox<TPair<Integer, string>> failed to parse:
each type argument was read as a single bare identifier, with no recursion
into a nested <...>, so a nested '<' produced "Expected '>' but got '<'"
(type position) or "Expected '.' or '(' after generic type arguments"
(constructor/expression position).

Both type-argument parse sites now recurse through ParseTypeName, so a type
argument may itself be a generic specialisation, to arbitrary depth
(TList<TList<TList<Integer>>> works). The expression-position heuristic also
accepts tkLessThan as the lookahead-2 token (the first arg being itself
generic). The comparison-operator disambiguation (a < b, (a < b) and (b < c))
is unaffected.

Found by the e2e generics hardening sweep. Regression:
TE2EMiscTests.TestRun_NestedGenericTypeArgs (TBox<TBox<Integer>>, both
backends). docs/grammar.ebnf TypeArgList rule updated to recurse via
GenericName.

All three fixpoints + full suite (3224 tests) pass.
2026-06-16 10:13:45 +01:00
Graeme Geldenhuys 236736a3be fix(parser): accept parenthesised lvalue as assignment target
A statement beginning with '(' was rejected ("Expected statement"), so a
parenthesised cast could not be an assignment TARGET:
    (a as TB).FX := 42;   -> Parse error
Reading the same expression worked, and the hard-cast target TB(a).FX := 42
worked, so only this statement-parser entry point was missing.

ParseStmt now handles a leading '(' by parsing the parenthesised expression
and requiring a '.Field := Expr' suffix, building a TFieldAssignment whose
receiver is that expression (ObjExpr) — the same AST + semantic + codegen path
already used for element-field writes (a[i].F := v). No AST/semantic/codegen
change was needed; the gap was purely the parser.

Found by the e2e test-hardening sweep of the inheritance cluster. Regression:
TE2EMiscTests.TestRun_ParenCastAsAssignmentTarget, run on BOTH backends.
docs/grammar.ebnf FieldAssignment rule updated.

All three fixpoints + full suite (3223 tests) pass.
2026-06-16 10:06:40 +01:00
Graeme Geldenhuys 74f71023da feat(oop): support 'inherited Method()' in expression position
`inherited` previously parsed only as a statement, so calling an inherited
FUNCTION and using its result failed to parse:
    Result := inherited Value() + 100;   -> "Parse error: Expected expression"
This blocked the normal OOP pattern of an overriding function extending its
parent's result.

Adds the expression form alongside the existing statement form:
* uAST: new TInheritedCallExpr (sibling of TInheritedCallStmt) + CloneExpr case.
* uParser: tkInherited handled in ParseFactor (primary expression).
* uSemantic: AnalyseInheritedCallExpr resolves to the parent method (must be a
  non-void function) and sets ResolvedType to the return type.
* Codegen: static (non-virtual) call to the parent slot, result returned as a
  value. QBE EmitInheritedCallExpr; native EmitInheritedCallSeq (shared by the
  statement and expression forms) leaves the result in %rax/%xmm0.
* uUnitInterfaceIO: 'inhc' encode/decode so inherited-expr in an inline method
  body round-trips through the .bif unit-interface cache; bif-coverage.status
  marks the two fields serialise (bif-coverage OK).
* docs/grammar.ebnf + docs/language-rationale.adoc updated (same commit).

Found by the e2e test-hardening sweep of the inheritance feature cluster.
Regression: TE2EMiscTests.TestRun_InheritedFunctionCall_InExpression covers a
value-returning inherited call and an inherited call with an argument, run on
BOTH backends via AssertRunsOnAll.

All three fixpoints (FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK) and
the full suite (3222 tests) pass.
2026-06-16 10:01:26 +01:00
Graeme Geldenhuys 5f340f0956 perf(asm): amortized byte buffers in the internal assembler's ELF writer
Assembling the compiler's own object with --assembler internal needed ~53 GB
of peak memory and OOM-killed. Root cause: blaise.elfwriter built byte data by
string concatenation (X := X + ...), which reallocates and copies the whole
buffer on every append — O(n^2). The dominant cost was Finish() building the
.strtab and .symtab one element at a time across 32914 symbols; the section
bodies (~2 MB .text) and the final image assembly had the same shape.

Introduces TByteBuf, a capacity-doubling array-of-Byte buffer (amortized O(1)
append, bulk memcpy for byte<->string and buf<->buf), and routes the section
storage, the strtab/symtab/rela tables, and the final ELF image assembly
through it. TElfWriterSection.Data (string) becomes Bytes+Count (array of
Byte) — also the natural shape for the future internal linker, which needs
indexed byte access into section bodies.

Result: the internal assembler builds the whole compiler in ~5 s using
~740 MB peak RSS (was OOM at 54 GB) — a 73x memory reduction. Note: this is
allocation churn, not a leak; ARC was freeing the old buffers correctly (the
--debug leak tracker shows no new leaks), but the O(n) reallocation volume
blew the RSS high-water mark.

The internal assembler now self-hosts on both backends (the QBE-compiled path
relies on the @(class-field array)[idx] address fix in the previous commit).

Both fixpoints, the internal-assembler conformance guard, and the full suite
(3221 tests) pass.
2026-06-16 02:06:45 +01:00
Graeme Geldenhuys 32756f6f25 fix(qbe): load instance pointer for @(class-field array)[idx] address
TCodeGenQBE.EmitAddrOfExpr's @Rec.Arr[I] branch (TFieldAccessExpr with
IsArrayAccess) computed the receiver base for Base / IsVarParam / plain-local
records but was MISSING the IsClassAccess and IsImplicitSelf cases. For a
class instance variable it fell through to VarRef(RecordName) = $Obj, i.e. the
ADDRESS of the variable's slot, instead of loadl-ing $Obj to get the instance
pointer. So @Obj.Arr[I] emitted "add $Obj, off" and produced a garbage element
address that crashed when the pointer was actually used (e.g. passed to an
external memcpy).

Adds the IsClassAccess case (load the slot to get the instance pointer; load
twice for a var-param class) and the IsImplicitSelf case, mirroring the
canonical read-path receiver ladder. This is the QBE analogue of the native
EmitLocalRecordBase fix.

Regression: TE2EMiscTests.TestRun_AddrOfClassFieldDynArrayElem_LoadsInstance
writes through @B.Data[idx] via memcpy and reads it back; AssertRunsOnAll runs
it on BOTH backends.

Both fixpoints + the internal-assembler conformance guard + the full suite
(3221 tests) pass.
2026-06-16 02:06:25 +01:00
Graeme Geldenhuys 0b9b66f848 feat(cli): per-backend option contract (Chain of Responsibility)
Adds the driver option contract from docs/backend-options-design.adoc
(Steps 2-5), so a backend owns parsing, describing, and validating its own
private flags and the common parser stays backend-agnostic.

Four virtual methods on TBackendDriver, with inert base defaults:
* AcceptOption  — the parser offers an unrecognised flag to the active
  driver (Chain of Responsibility); typed accept/reject result.
* DescribeOptions — the driver contributes its --help lines (via the new
  shared FormatFlagLine helper, so column layout is owned in one place).
* ValidateOptions — post-parse, full-context validity rules (Template
  Method seam).
* ClaimsEmitIR — retires PickTopDriver's hard-coded bkQBE for --emit-ir.

Parser integration is one-pass with a deferred drain: unknown flags are
collected during the loop and offered to the driver only after --backend
resolves it, so argument order never matters and the value-skip logic is
not duplicated across passes.

First real adopter on master: the native driver takes ownership of
--assembler internal|external. The inline --assembler arm is removed from
ParseArgs; the flag now flows through the drain into Native.AcceptOption.

Eng-review addenda folded in:
* ValidateOptions runs UNCONDITIONALLY, above the stdout-mode toolchain
  skip (so flag-combination rules fire even with --emit-ir).
* A backend-private flag passed to the wrong backend is now a clear hard
  error (intentional behaviour change: --backend qbe --assembler internal
  was previously silently accepted-and-ignored).
* Pending entries carry {flag, lookahead} with consumed-index bookkeeping
  so a flag's value token is not re-reported as unknown.

Tests: cp.test.driver.pas (TBackendDriverContractTests, 11 unit tests on
the real registered singletons) and cp.test.cli.pas (CLI E2E: internal
accepted, bogus rejected, wrong-backend rejected, bad value still rejected
under --emit-ir, --assembler listed in --help).

Both fixpoints, the internal-assembler conformance guard
(NATIVE_INTERNAL_OK), and the full suite (3220 tests) pass.
2026-06-16 00:57:24 +01:00
Graeme Geldenhuys 0631bb0cdf test(native): add internal-assembler conformance guard
Neither fixpoint exercises the in-process internal assembler (--assembler
internal): fixpoint.sh assembles via qbe+gcc, fixpoint-native.sh assembles
the native .s with gcc.  A miscompilation that only corrupts the internal
assembler's object output therefore passes both fixpoints cleanly — which is
exactly how the sret-Result field-read bug (fixed in the previous commit)
escaped the fixpoint gate.

scripts/fixpoint-native-internal.sh closes that gap.  It compiles a small
representative program (record-returning function with sret-Result field
reads, plus immutable string literals) with BOTH --assembler internal and
--assembler external, then asserts the two binaries behave identically
(stdout + exit code).  Behavioural equivalence is the sound invariant: the
two assemblers may emit different-but-valid encodings, so a byte-level
section compare would false-positive.

A true self-hosting internal-assembler fixpoint (compile the compiler with
itself via --assembler internal) is deferred: the internal assembler buffers
the whole object in memory and needs ~53 GB for the compiler's own 631k-line
.s, OOM-killing.  That scalability limit is tracked in bugs.txt; until the
assembler streams its output, this differential conformance check plus
TInternalAsmE2ETests are the internal-assembler guards.

Verified: prints NATIVE_INTERNAL_OK on a fixed compiler, exits non-zero with
EXIT_MISMATCH on a compiler carrying the sret-Result bug.
2026-06-16 00:50:18 +01:00
Graeme Geldenhuys eee7f54284 fix(native): load sret Result pointer with movq, not leaq, when reading its fields
The native backend's field-access receiver ladder, when the record base is
a NAMED LOCAL record, emitted 'leaq <slot>' to get the record's address.
That is correct for an ordinary stack value record, but WRONG for the
Result of an sret (record-returning) function: there the frame slot holds
the caller's buffer POINTER, not the record itself, so the address must be
loaded with 'movq'.  Reading Result.<field> on the RHS (e.g.
'Result.Imm := Result.Disp' in the internal assembler's ParseOperand) thus
dereferenced the wrong location and produced garbage.

The receiver ladder was duplicated across 12 local-leaf sites, each missing
the sret-Result case (the implicit-Result analogue of the implicit-Self
field-access symmetry rule).  This centralises the local-record base
computation into one EmitLocalRecordBase(AName, AReg) helper that makes the
leaq-vs-movq decision once, and routes all 12 leaf sites through it.

The bug was latent: the wrong read happened to land on a harmless stack slot
under the prior register/stack allocation, so existing code worked by
accident.  Any change that shifts global layout (e.g. growing a vtable by
one slot) re-allocated registers and exposed it as corrupted output from the
--assembler internal path.  Neither fixpoint exercises the internal
assembler, so it surfaced only via TInternalAsmE2ETests once a layout change
armed it.

Both fixpoints (FIXPOINT_OK, NATIVE_FIXPOINT_OK) and the full suite
(3204 tests, incl. TInternalAsmE2ETests) pass.
2026-06-16 00:14:26 +01:00
Graeme Geldenhuys 88ebf75476 refactor(cli): collapse ParseArgs out-params into opts objects
ParseArgs changes from fifteen 'out' parameters to two caller-constructed,
populated objects:

* TFrontEndOpts (new unit blaise.frontend.opts) carries front-end-only
  state no backend driver reads: SourceFile, OutputFile, SearchPaths,
  EmitIfaceDir, Incremental, UnitCacheDir, DumpAST, SkipDepCodegen, plus
  the output-mode policy flags EmitIR/EmitAsm and the Backend selection
  input. It lives in its own unit (not blaise.codegen.driver, the
  backend-facing abstraction) so front-end state stays out of the
  backend opts bag.

* TBackendOpts (existing) carries only the cross-cutting knobs a driver
  reads: Target, OPDFEnabled, DebugMode, UseInternalAsm (and OPDFAsmFile,
  bound later during OPDF emit). EmitAsm is removed from TBackendOpts —
  no driver method read it; it is a front-end selection flag.

The main body seeds its working locals from the two objects right after
the call, so the large downstream body reads them unchanged. The parser
now populates one pair of objects instead of fifteen by-reference returns.

Behaviour-preserving. Step 1 of docs/backend-options-design.adoc.

Both fixpoints (FIXPOINT_OK, NATIVE_FIXPOINT_OK) and the full suite
(3204 tests) pass.
2026-06-15 20:46:56 +01:00
Graeme Geldenhuys e642c7c8d7 refactor(cli): remove FPC-style CLI emulation
PasBuild v1.9.0 ships the native TBlaiseBackend (driving Blaise via
--source/--output and probing the version via --help), which was the
agreed trigger to retire the FPC-style argument path.

Deletes ParseFPCArgs, IsFPCStyleInvocation, HandleFPCInfoQuery, the
main-body 'if IsFPCStyleInvocation()' branch, and the now-dead
uStrCompat import (~120 lines, all confined to Blaise.pas). This
leaves a single argument parser (ParseArgs) for the backend-options
refactor to build on, and eliminates the hand-maintained option-default
block in the FPC branch that was a recurring source of
uninitialised-option bugs.

Adds cp.test.cli.pas (TCLIContractTests) with a CLI smoke test:
--help works, a normal --source/--output compile works, and an -iV
probe no longer returns FPC's 3.2.2 (now a rejected unknown flag).

Step 0 of docs/backend-options-design.adoc.

Both fixpoints (FIXPOINT_OK, NATIVE_FIXPOINT_OK) and the full suite
(3204 tests) pass.
2026-06-15 20:25:05 +01:00
Graeme Geldenhuys 9362114ee1 feat(sets): range syntax in set literals — [lo..hi] (#105)
A set literal element may now be a constant range:

    e := [Red..Blue];        { {Red, Green, Blue} }
    e := [m0, m2..m4, m7];   { ranges mix with single members }

The parser builds a transient TSetRangeExpr for each lo..hi element; the
semantic pass (AnalyseSetLiteralExpr) expands it into the individual member
idents before any other consumer runs, so overload resolution, the bitmask
folder, the jumbo-set path, and both code generators see an ordinary member
list and need no range-specific logic.

Both bounds must be compile-time constants of the set's base type. A
reversed constant range [Blue..Red] is a compile-time error rather than a
silently empty set — matching Blaise's preference for rejecting
confusing-but-legal constructs over FPC's empty-set-with-warning behaviour.
Variable bounds [lo..hi] are rejected ("set range bound must be a
constant"); runtime-variable ranges are deferred.

TSetRangeExpr is wired into the .bif serialiser and the AST cloner so a
generic template body containing an unexpanded range round-trips through
separate compilation (verified by hand: a generic returning [m1..m3]
compiled to .o, then instantiated from the .bif with source hidden).

Set base types remain enumerations; the issue's set-of-byte example needs
ordinal-base set types, tracked separately in docs/future-improvements.adoc.
Range expansion is base-type-agnostic, so ranges will work for those
automatically once they land.

Docs: grammar.ebnf SetElement rule; language-rationale.adoc decision +
alternatives. bif-coverage.status regenerated for the new node.

FIXPOINT_OK + NATIVE_FIXPOINT_OK; full suite OK (3201 tests, 11 new).
2026-06-15 17:14:05 +01:00
Graeme Geldenhuys 4e90165b73 feat(semantic): reject variables that shadow a type name (#102)
A variable declaration sharing its identifier with any visible type name
compiled without complaint — e.g. an Iface interface and an iface variable
in the same program (Pascal is case-insensitive, so these are one
identifier). The variable silently shadows the type, which is confusing
and almost always a mistake.

Blaise already rejected var-vs-var, var-vs-const and type-vs-type clashes;
var-vs-type slipped through because type decls are registered in the
block's enclosing scope (so they outlive the block scope) while var decls
are registered one scope deeper, hiding the type from FTable.Define's
duplicate check. AnalyseVarDecls now looks up the name in the type
namespace (FTable.FindType, which honours uses-chain visibility) and
rejects any match.

The rule is stricter than FPC mode objfpc, which permits shadowing a
built-in or outer-scope type (var Integer: Int64 compiles in FPC). Blaise
rejects the whole class — same-block, outer-scope, imported, or built-in —
to eliminate the confusion rather than carry FPC's footgun. Recorded in
docs/language-rationale.adoc with the alternatives considered.

FIXPOINT_OK + NATIVE_FIXPOINT_OK; full suite OK (3190 tests, 5 new).
2026-06-15 16:38:19 +01:00
Graeme Geldenhuys bf63d7419f fix(codegen): three bugs compiling units with generics + float code (#107)
Compiling the issue's waveCalcs unit surfaced three independent codegen
bugs, two of which the reporter saw directly and a third uncovered while
verifying the fix on the native backend.

1. QBE: a generic FUNCTION declared in a unit but never instantiated was
   code-generated as a template — its T-typed locals have no resolved
   type, so codegen raised "Variable 'MaxValue' has no resolved type —
   semantic pass required". EmitStandaloneDefs skipped templates, but the
   unit-emission path (GenerateUnit/AppendUnit -> EmitFuncDef) did not.
   Guard centrally in EmitFuncDef: skip decls with TypeParams <> nil; only
   concrete instances (from GenericFuncInstances) are emitted.

2. native: a float comparison nested inside a short-circuit and/or — e.g.
   (P > 1.0) or (P < -1.0) — reaches EmitExprToEax rather than
   EmitCondBranch. The integer comparison path there emits its operands via
   EmitExprToEax, which has no TFloatLiteral handler, so it raised
   "unsupported expression form TFloatLiteral". Add a float-comparison
   branch to EmitExprToEax that materialises the 0/1 result via ucomisd +
   setcc, mirroring EmitCondBranch.

3. native: a local float const (const TwoPi = 6.28; inside a function) was
   lowered as a load from a symbol named after the const, which is never
   emitted — link error "undefined reference to TwoPi". The const has no
   storage; inline its value from ConstString via .rodata in
   EmitExprToXmm0, as a TFloatLiteral already does.

Regression tests:
- TestUninstantiatedGenericFunc_InUnit_Compiles (sepcompile e2e)
- TestRun_Native_FloatCompareInOrAnd (runs on both backends)
- TestRun_Native_LocalFloatConst (runs on both backends)

FIXPOINT_OK + NATIVE_FIXPOINT_OK; full suite OK (3185 tests).
2026-06-15 16:22:29 +01:00
Graeme Geldenhuys 267e8db702 fix(rtl): WriteDecimal mishandles Low(Int64), printing only '-'
WriteDecimal negated N before extracting digits. For Low(Int64) =
-9223372036854775808 the negation overflows (no positive counterpart in
two's complement) and leaves the value negative, so the 'while AbsN > 0'
loop ran zero times and only the sign was emitted.

Extract digits while keeping the value negative — the negative range
reaches Low(Int64) — and negate each remainder. Also fixes
Low(Integer), which routes through the same function.

Adds e2e regression TestRun_Int64_MinValue.
2026-06-15 16:05:37 +01:00
Graeme Geldenhuys fbb5a675aa feat(format): float Format() support on the native x86-64 backend
Mirror the QBE backend's tag-2 float handling in EmitFormatCall: a float
argument stores tag 2, evaluates to %xmm0 (widening a Single to Double), and
bit-copies the 64-bit pattern into the value slot via movq — the native
equivalent of QBE's 'cast'.

E2E tests (cp.test.e2e.stringops) exercise %f default/precision/width (both
justifications), %e, %g, and a float interleaved with int+string args, all via
AssertRunsOnAll so they run on both the QBE and native backends.
2026-06-15 00:58:43 +01:00
Graeme Geldenhuys 7c26598bb8 feat(format): float support in Format() — %f/%e/%g with width/precision (QBE)
Format() previously handled only %d, %s and %%; any float directive rendered
verbatim, and on the QBE backend a float argument failed to compile because the
double value was stored with 'storel <d-temp>', which QBE rejects.

RTL (blaise_float.pas): add _FormatFloatSpec(V, Spec, Prec) — a precision-aware
renderer for %f/%F, %e/%E and %g/%G built on the existing Grisu1 digit
generator, with round-half-up at the precision boundary (including all-nines
carry) and printf-style exponent formatting.

RTL (blaise_str.pas): rewrite _StringFormatN to parse the full
%[-][width][.prec]<conv> specifier syntax, render each argument (int/string/
float) into a growable buffer, and apply field-width space padding. A new
arg tag (2) carries the IEEE-754 binary64 bit pattern for float values.

QBE codegen: float Format arguments now emit tag 2 and reinterpret the double
bits to an integer via 'cast' before storel, fixing the compile error.

Tests: IR unit tests assert tag-2 emission and the bit cast. E2E coverage and
the native backend follow in the next commit.
2026-06-15 00:58:37 +01:00
Graeme Geldenhuys d3dbebbe85 test(sets): extract set-of e2e tests to cp.test.e2e.sets, run on both backends
The general 'set of' operation e2e tests (Include/Exclude, in, union/intersect,
valued constant, literal argument, equality, for-in, and the 33..64-member
Set64 boundary cases) lived in cp.test.e2e.misc and ran on QBE only via
CompileAndRun. They test named-set operator semantics, distinct from the
inline-set syntax covered by cp.test.e2e.inlineset, so they move to a new
purpose-named unit cp.test.e2e.sets (TE2ESetOpsTests) paralleling the IR-level
cp.test.sets — and are promoted to AssertRunsOnAll so each now runs on both the
QBE and native backends, closing a native-coverage gap.

Pure relocation + both-backend promotion: full suite unchanged at 3172 tests
(11 set tests moved misc -> sets). README test count updated 2768 -> 3172.
2026-06-15 00:34:39 +01:00
Graeme Geldenhuys fa853e0eb2 feat(sets): jumbo sets — set of enum up to 256 members
Extend set of <enum> from a 64-member cap to 256, Java-EnumSet style. Sets
of 64 members or fewer keep the existing single-register bitmask (QBE w/l);
sets of 65..256 members ('jumbo') become an inline byte-array bitmap of
ceil(N/8) bytes, treated as a value aggregate (passed by reference, returned
via sret, memset/memcpy), with operations performed by new RTL helpers.

Representation (uSymbolTable): TSetTypeDesc.IsJumbo (BitCount > 64) and
RawByteSize; RawSize/ByteSize/AllocAlign sized accordingly. TSymbol gains
ConstSetBytes/ConstSetQbe for jumbo constants (can't fit an Int64 mask).

RTL: new runtime/src/main/pascal/blaise_set.pas — _SetIn/_SetInclude/
_SetExclude/_SetUnion/_SetInter/_SetDiff/_SetEqual/_SetCopy over byte-array
bitmaps (overlap-safe). Wired into runtime/Makefile.

Semantic: the four >64 caps become >256. AnalyseSetConstDecl folds a jumbo
const to a byte array. The anonymous set for 'X in [a,b,c]' is sized to the
largest listed ordinal (when constant), not the full enum — keeping the
common low-ordinal membership test (incl. the compiler's own TokenKind
tests) on the fast register path. This also fixes a latent miscompile: the
old fixed-l representation silently dropped any listed ordinal >= 64.

Codegen (both backends): jumbo branches for literal, in, +/-/*, =/<>,
Include/Exclude, for-in, assignment, params (pmJumboSetValue ABI), and sret
returns. The register 'in' gained a range guard for literal-sized sets.
Native reserves two 32-byte scratch slots per frame (and .bss in main) for
set-op/literal result buffers. QBE adds IsAggregateAddrType so jumbo sets
ride the record/static-array address paths and are never promoted.

OPDF: no format change — recSet SizeInBytes (1 byte) already covers <=32.

Verified: FIXPOINT_OK and NATIVE_FIXPOINT_OK; full suite (3172 tests) green
built by the stage-2 binary, the QBE fixpoint binary, and the native
fixpoint binary; bif-coverage OK. Tests: cp.test.jumboset (12 IR) and
cp.test.e2e.jumboset (6 e2e, both backends via AssertRunsOnAll); the
>256-member rejection test in cp.test.sets updated. Docs: grammar.ebnf and
language-rationale set-type sections updated for the 256 cap and literal
sizing.

Closes the design discussion behind #81.
2026-06-14 23:23:19 +01:00
Graeme Geldenhuys 2708a6209e feat(const): compile-time integer constant expressions (#96)
A const declaration may now take a compile-time integer expression on the
right-hand side, not just a single literal:

  const A = 2 * 3;            // 6
        B = 2 + 3 * 4;        // 14 (precedence)
        C = (2 + 3) * 4;      // 20 (parentheses)
        D = 100 div 7;        // 14
        E = Base * 2 + 1;     // references a prior const

Previously only single literals and a flat, single-precedence bit-op chain
(a or b or c) were accepted; 2 * 3 failed with "Expected ';' but got '*'"
and (2 * 3) with "Expected numeric or string constant".

Parser: when the const RHS starts an integer expression (a leading '(', an
integer literal or leading-minus literal followed by a binary operator, or an
ident followed by a numeric operator), parse it with the existing
full-precedence ParseExpr into a normal expression AST stored on
TConstDecl.IntValueExpr. A bare literal keeps the fast IntVal path; '+' on an
ident is left to the string-concat path for backward compatibility.

Semantic: EvalConstIntExpr recursively folds the AST to an Int64, resolving
named-constant references against the symbol table. Folding in the semantic
pass (after all consts are registered) is what makes forward references work
regardless of declaration order. Wired into both the const-block and the
initialised-variable (var G = expr) fold sites. Operator precedence and
grouping come from the AST shape, superseding the precedence-unaware bit-op
token chain.

Scope: folds to integer. Float constant arithmetic and using a const as a
static-array bound remain separate, pre-existing limitations (noted in the
rationale).

Tests: eight IR/fold cases in cp.test.constants (multiply, parens, precedence,
div/mod, named-ref, unary minus, mixed arith+bitwise); one e2e case on both
backends in cp.test.e2e.misc. Docs: grammar.ebnf gains the ConstIntExpr rules;
language-rationale gains a Constant Expressions section.
2026-06-13 19:14:14 +01:00
Graeme Geldenhuys 9a511a22e2 feat(const): multi-dimensional array constants
Range-indexed array constants now support multiple dimensions in both the
comma form (array[0..1, 0..2] of Integer) and the equivalent nested form
(array[0..1] of array[0..1] of Integer), with nested initialiser groups
((1,2,3),(4,5,6)). Previously the const-declaration parser had its own
single-dimension array-type and value-list parsers, so any multi-dim const
failed at the type (Expected ']' but got ',') or the nested value group.

Parser: ParseConstArrayType walks one-or-more 'array[...] of' headers,
reading comma-separated ranges per header and recursing on a nested 'array';
each dimension's bounds go to CD.ArrayDimLows/ArrayDimHighs. The value
parser is now recursive (ParseConstArrayGroup/ParseConstArrayScalar),
flattening nested groups into ArrayElements in row-major order. Dim-0 bounds
mirror onto the legacy ArrayLowBound/ArrayHighBound so the single-dim path
is unchanged.

Semantic: BuildConstArrayType builds the nested static-array type
innermost-first and validates the flat element count equals the product of
dimension extents. Shared by the program/unit and class-const sites.

Codegen: the QBE emitter already lays a flat row-major blob, so it needs no
change for integer/string elements. The native emitter now drills through
nested static-array types to the innermost scalar to pick the element
directive.

Tests: parse/semantic/IR in cp.test.constants; e2e (comma, nested, 3-D) on
both backends via AssertRunsOnAll in cp.test.e2e.staticarray. Docs:
grammar.ebnf gains ConstArrayType/ConstArrayValue rules; language-rationale
updated (the stale 'enum index only' constraint corrected to document
range and multi-dimensional const arrays).
2026-06-13 18:48:28 +01:00
Graeme Geldenhuys 5fa44b33a5 fix(codegen): float-typed array elements on both backends
Storing a Double or Single into a dynamic- or static-array element was
miscompiled. The QBE backend's element-store selected storel/storew for
any non-integer element, so a double element got 'storel %d-temp' and
QBE rejected it (invalid type for first operand in storel). The native
backend evaluated the RHS through the integer path (EmitExprToEax),
which raised 'unsupported expression form TFloatLiteral', and even past
that would have stored from a GPR rather than %xmm0; the float element
READ had the matching gap (unsupported float expression form
TStringSubscriptExpr).

QBE: add tyDouble->stored / tySingle->stores to both the dyn-array and
static-array element-store cases, and coerce the value temp to the
element width via CoerceArg (a 1.5 literal is a 'd' temp; a Single slot
needs truncd first).

Native: in the element store, evaluate float RHS via EmitExprToXmm0,
width-adjust to the element type, spill through %rax (x86 has no push for
xmm), then reload and store with movsd/movss. Add float element-read
cases to EmitExprToXmm0 mirroring the integer subscript address maths.

Tests: three e2e cases in cp.test.e2e.staticarray (dyn double, dyn
single, static double) run on both backends via AssertRunsOnAll.
2026-06-13 18:07:42 +01:00
Graeme Geldenhuys 12ebcbe09f feat(parser): clear diagnostics for packed/bitpacked array
FPC's bitpacked array (bit-level packing) and packed array have no
Blaise equivalent. Previously bitpacked fell through to tkIdent and
produced a confusing "Expected ';'" error, while packed array gave a
generic "may only precede 'record'" message.

Catch both in the type-declaration RHS and emit an actionable error
that points users at the supported idiom (set of for packed boolean
flags). packed record is unaffected.

Adds five tests to cp.test.packedrecord: parse-error coverage for both
keywords plus message-content assertions that the error names the
keyword and suggests set of.
2026-06-13 17:56:22 +01:00
Graeme Geldenhuys f830a9f1ac feat(params): array of const (heterogeneous variadic parameters)
A parameter declared 'array of const' accepts a single call-site bracket list
of mixed-type values, boxed into an array of the intrinsic record TVarRec and
passed via the existing open-array ABI:

  procedure Log(args: array of const);
  ...
  Log([42, 'hi', 3.5, True]);

This is the one loosely-typed-passing mechanism Blaise adopts (see the
rationale section); untyped params, varargs, and Variant remain omitted.

- RTL/builtins: TVarRec is registered as a compiler-intrinsic record
  { VType: Byte; VValue: Pointer } (16-byte layout, mirroring TMethod), with
  vt* discriminant constants - all available with no uses clause, matching
  Delphi's auto-available System.TVarRec.  Blaise has no record variant parts,
  so the callee reads each element by reinterpret-casting the single VValue
  slot (Integer(v.VValue), string(PChar(v.VValue)), PDouble(v.VValue)^, ...).
- Parser: 'array of const' parses as an open array whose element is TVarRec.
- Semantic: a heterogeneous bracket literal is typed 'array of TVarRec' rather
  than rejected; overload resolution binds it (and homogeneous / empty
  literals) to an array-of-const formal; retyping runs for proc, func, and
  method calls.
- Codegen (both backends): EmitConstArrayLiteral builds one 16-byte TVarRec
  per element, tagging by inferred type.  Borrow semantics (FPC) - strings and
  objects are stored without AddRef.  Doubles are heap-boxed via _BlaiseGetMem
  (vtExtended holds a PDouble) since a double does not fit the pointer slot.
- Native: also fixes a pre-existing gap - reading a float through a pointer
  deref (PDouble^) in EmitExprToXmm0 - needed for vtExtended read-back.

Tests: cp.test.arrayofconst (parser/semantic/IR) and cp.test.e2e.arrayofconst
(compile + run on both backends, including value read-back, empty/homogeneous
lists, and string-variable borrow).  Grammar and language rationale documented.
2026-06-13 17:37:59 +01:00
Graeme Geldenhuys 28bd5cdb38 docs(rationale): position on loosely-typed parameter passing
Record Blaise's stance on the four conflated "pass a loosely-typed value"
features: untyped const/var parameters, array of const, varargs, and Variant.

Blaise adopts only array of const - the one mechanism overloads and generics
cannot replace (arbitrary-length, mixed-type argument lists, as used by
Format-style APIs); it is a compiler-bounded, per-element-tagged call construct
rather than a free-floating dynamic value.

Untyped parameters are omitted on footgun grounds: they implicitly take an
address and erase the type, whereas a typed pointer (PByte / ^T) does the same
job explicitly and keeps type information for indexing/field access. varargs is
omitted as C-interop-only and untagged; Variant is omitted in favour of
overloads, generics, and explicit tagged unions that keep checking at compile
time.
2026-06-13 15:29:20 +01:00
Graeme Geldenhuys 54d7d2a573 feat(types): inline set types in any type position
A set type may now be written inline anywhere a type is expected (var,
parameter, record field, ...), not only in a named type declaration.  The
element type may be a named enumeration or an anonymous enumeration written
in place:

  var
    Days:  set of TWeekday;       // named enum element
    Flags: set of (fA, fB, fC);   // anonymous enum element

Implementation resolves an inline set entirely from its canonical type-name
string, with no parser-side state:
- Parser: ParseTypeName gains a 'set of' branch.  A named element yields
  'set of <Name>'; an anonymous element is encoded verbatim as
  'set of (a,b,c)' (ParseAnonEnumName), self-contained in the type string.
- Semantic: FindTypeOrInstantiate recognises the 'set of ' prefix and builds
  the TSetTypeDesc on demand (as it already does for 'array of'/'^'/'class
  of').  For the anonymous form, SynthAnonEnum synthesises an enum type from
  the encoded member list - registering each member as an enum constant - and
  reuses an identical inline enum if its members are already defined.  Set
  types compare structurally, so the synthetic enum name need only be unique.

Encoding the enum in the type string rather than carrying parser state makes
the feature work uniformly in every type position with no declaration-ordering
or ownership concerns.

Tests: cp.test.inlineset (parser/semantic) and cp.test.e2e.inlineset (compile
+ run on both backends).  Grammar and language rationale documented.
2026-06-13 13:45:13 +01:00
Graeme Geldenhuys ba51a53365 feat(vars): initialised global variables (var G: T = value)
A global variable declaration may now carry an initialiser whose value is
folded at compile time and emitted into the data section, so the variable
holds its initial value before the program body runs (matches FPC/Delphi):

  var
    G: Integer = 42;
    S: string = 'hello';
    A: array[0..2] of Integer = (10, 20, 30);

Implementation reuses the typed-constant pipeline end to end:
- Parser: the const value scanner is factored into ParseConstValue, shared by
  const declarations and the new var-initialiser path.  TVarDecl carries an
  owned InitConst: TConstDecl (TConstDecl moved ahead of TVarDecl so no
  forward declaration is needed).
- Semantic: AnalyseVarInitializer folds the value and type-checks it against
  the declared type; array initialisers derive element type and bounds from
  the resolved static-array type and mint a data label.
- Codegen: EmitGlobalVarInit (QBE) and EmitGlobalInitData (native) emit the
  folded value as a typed data slot - a single field for scalars/strings (a
  string global points at an immortal static header __sN + 12) and an inline
  element list for arrays.  Covers program- and unit-level globals on both
  backends.

Restrictions, each rejected with a clear diagnostic: global scope only (no
local initialisers), a single name per initialised declaration, and arrays
only for aggregates (record and inline-set initialisers are deferred - no
record-constant machinery yet, and a set initialiser would clash with the
const symbol the set folder defines).

Tests: cp.test.varinit (IR + parser/semantic) and cp.test.e2e.varinit
(compile+run on both backends).  Grammar and language rationale documented.
2026-06-13 13:29:43 +01:00
Graeme Geldenhuys 42f48c9ece fix(codegen): Inc/Dec on a captured outer-scope variable
A nested procedure that did Inc(Counter)/Dec(Counter) on a captured outer
local failed to compile: the captured value lives behind the '_cap_<Name>'
pointer slot (which holds the var's address), but Inc/Dec fell through to the
plain local/global path and referenced a non-existent '%_var_Counter' /
'Counter' symbol. QBE rejected it ("invalid type for first operand in
loadsw"); native linking failed ("undefined reference to Counter").

Assignment to a captured var already used the _cap_ indirection; Inc/Dec did
not. Add a captured-var branch to both backends that loads/modifies/stores
through the _cap_ pointer (QBE: loadw/storew via %_cap_Name; native: move the
_cap_ pointer into %rdx and use the existing addr-based inc/dec path). Covers
the w and l element types and the optional step argument.

Adds e2e TestRun_IncDec_CapturedVar. Full suite 3088, both fixpoints OK.

Discovered while migrating the OPDF debugger's nested-scope test
(test_12_teaser_demo) to the Blaise suite.
2026-06-13 12:29:05 +01:00
Graeme Geldenhuys 7567356e84 fix(opdf): emit Boolean and real constants correctly
Constant debug records mishandled two kinds, surfaced while debugging const
values in pdr:

- Boolean (const Enabled = True): True/False are plain identifiers with no
  dedicated token, so the const parser fell into the generic ident-as-string
  path and emitted an empty STRING constant. Recognise True/False as a Boolean
  ordinal (value 1/0) and tag it with TypeName 'Boolean' so the debug emitter
  points the constant at the Boolean primitive's TypeID — pdr then renders
  'True'/'False' rather than a raw 1/0.

- Real (const PiApprox = 3.14159): IsFloat constants fell into the ordinal
  branch and emitted IntVal (always 0). Emit them as ckReal (kind 2, already
  supported by the debugger) with the IEEE-754 Double via GAS .double.

EmitConstants now branches IsFloat -> ckReal, IsString -> ckString, else
ckOrd (with a typed TypeID when TypeName is set). Adds TestOPDF_Constant_-
BooleanTyped and TestOPDF_Constant_Real. Full suite green (3085), both
fixpoints OK.
2026-06-13 12:11:14 +01:00
Graeme Geldenhuys ccd109ce77 feat(arrays): support multi-dimensional static arrays
Add multi-dimensional static array syntax, both the comma form
(array[0..1, 0..2] of Integer; A[i, j]) and the equivalent nested/chained
form (array[0..1] of array[0..2] of Integer; A[i][j]).  The comma forms are
syntactic sugar: the parser desugars array[a, b] of T into the nested
array[a] of array[b] of T, and A[i, j] into chained subscripts A[i][j], so
the two notations are fully interchangeable in every position.

Layers:
- uParser: comma loops in ParseTypeName and subscript reads; the statement
  LHS now lowers A[i, j] := v and A[i][j] := v to a TStaticSubscriptAssign
  carrying a new BaseExpr (the inner-array address expression).  The previous
  "chained base not yet supported" rejection is removed.
- uAST: TStaticSubscriptAssign.BaseExpr (owned); wired into CloneStmt and the
  .bif encoder/decoder (uUnitInterfaceIO); bif-coverage status updated.
- uSemantic: BaseExpr branch resolves the inner static-array type and checks
  the index and value element type.
- Codegen (both backends): a nested static-array element now evaluates to its
  inline address (mirroring record/interface elements) so a further subscript
  indexes into it; the static-subscript store reads its base from BaseExpr
  when set.  Nested arrays are a flat row-major contiguous block.
- OPDF: no new record needed - each dimension emits one recArray whose element
  points at the next inner recArray; pdr follows the chain and renders the
  value as a true multi-dimensional structure.

Tests: IR unit tests (cp.test.staticarray), e2e tests on both backends via
AssertRunsOnAll (cp.test.e2e.staticarray), and a nested-recArray OPDF test
(cp.test.opdf).  Grammar and language rationale documented.
2026-06-13 11:56:38 +01:00
Graeme Geldenhuys 7b8116310f feat(opdf): emit open-array debug info (ArrayKind=2 + companion slot)
Open-array parameters were emitted to the OPDF companion file as dynamic
arrays (IsDynamic=1), so the debugger read their length from the heap
header at data-4. An open array has no header — it is a (data ptr, high
index) pair on the stack — so this read returned garbage (Length=32765).

Model open arrays as a distinct kind:

- TDbgVar gains IsOpenArray + HighRbpOffset; DbgMarkParams records the
  '_high' companion slot offset from the frame map.
- The recArray byte previously named IsDynamic now carries ArrayKind
  (0=static, 1=dynamic, 2=open-array); open arrays emit ArrayKind=2.
- recLocalVar for an open array uses LocationExpr=5 and appends the
  companion '_high' slot offset as a trailing SmallInt.
- Open arrays get a distinct canonical name ('open array of T') so they
  no longer alias a same-element dynamic array's TypeID.
- Parameter and facts-path variable type records are now emitted; the
  anonymous open-array type previously had no recArray written at all.

The debugger side (length read from the companion slot) lives in the
opdebugger repo. FPC's dbgopdf.pas has no open-array concept; this is a
Blaise extension to the OPDF format.
2026-06-13 01:06:16 +01:00
Graeme Geldenhuys 6ba1bc9b78 fix(semantic): reject void/procedure-call results as WriteLn/Write arguments
The previous fix caught procedural-typed variables passed directly to
WriteLn, but missed the case of calling a procedure variable and passing
the (non-existent) result: WriteLn(proce(2,3)).  A procedure call
resolves to tyVoid, not nil, so the nil check didn't fire.  The codegen
emitted an empty QBE operand ('w ') causing QBE to reject the IR.

Extend the Write/WriteLn arg check to also reject tyVoid, covering both:
  - WriteLn(proce)      — procedural variable (tyProcedural)
  - WriteLn(proce(a,b)) — call to a procedure variable (tyVoid)

Adds a semantic unit test for the procedure-call-result case.
2026-06-12 18:12:48 +01:00
Graeme Geldenhuys b3024facc0 fix(semantic): reject procedural/record/class types as WriteLn/Write arguments
WriteLn/Write had no type-check on their argument list — a procedural
variable (or any other unprintable type: record, class, interface, array)
was silently accepted by the semantic pass and handed to the codegen,
which emitted it as an integer (the raw code pointer).  The native backend
printed the address; QBE rejected the IR with "invalid argument".

Add a post-analyse check in the Write/WriteLn branch: after resolving each
argument's type, reject tyProcedural, tyRecord, tyClass, tyInterface,
tyMetaClass, tyStaticArray, tyDynArray, tyOpenArray, and tyPointer with
a clear error message matching FPC's behaviour.

Adds one semantic unit test covering the procedural-variable case.
2026-06-12 17:46:56 +01:00
Graeme Geldenhuys 1b0afaf3d3 fix(test): derive blaise binary path from TestRunner location, not cwd
BlaisePath() was using GetCurrentDir() as the project root fallback,
then appending 'compiler/target/blaise'.  This worked when the TestRunner
was invoked from the project root, but broke when PasBuild cd'd into
compiler/target/ before running ./TestRunner — the path doubled up to
.../compiler/target/compiler/target/blaise.

Fix by deriving the path from ExtractFilePath(ParamStr(0)), which always
points to the directory containing the TestRunner binary regardless of
the caller's working directory.  The BLAISE_PROJECT_ROOT env var override
is preserved unchanged.
2026-06-12 16:59:15 +01:00
Graeme Geldenhuys 75834d393d fix(semantic): out-of-line methods now link for generic records
LinkGenericClassMethodImpls only handled TGenericTypeDef (class), but
TGenericRecordDef is registered in the same generic registry. When an
out-of-line method implementation was written for a generic record
(function MyRec<T>.Foo), the lookup succeeded but the cast to
TGenericTypeDef followed by .ClassDef access was wrong for records,
producing a spurious "Generic type not found" error.

Fix: check the runtime type and dispatch to .ClassDef.Methods for
classes or .RecordDef.Methods for records. Also broadened the
"not declared in generic class" error message to "generic type".

Tests: two new cases in TGenericRecordTests cover the semantic link
and codegen emission for out-of-line generic record methods.
2026-06-12 16:45:33 +01:00
Graeme Geldenhuys 512cbfef98 test(zero-init): use S=[] directly in SetLocal now that semantic crash is fixed
The SrcZeroInit_SetLocal source previously used `fA in S` to probe
set zero-initialisation, working around the S=[] semantic-pass crash
(now fixed).  Update it to use S=[] directly — both more idiomatic
and exercises the fixed equality path in the same test run.
2026-06-12 16:29:57 +01:00
Graeme Geldenhuys 75db9584dc fix(semantic): set equality S=[] and S=[lit] no longer crash
AnalyseBinaryExpr crashed with a nil-deref when one operand was a set
type (tySet) and the other was an empty array literal [] — because
AnalyseArrayLiteralExpr returns nil for an empty literal (no element
type to infer), and the set-dispatch guard read .Kind off that nil
pointer.

Fix: before the set-dispatch guard, coerce any array literal on either
side (empty or non-empty) to the set type of the opposite operand,
mirroring the existing pattern for `x in [A, B, C]`.  This also
enables `S = [fA, fB]` without requiring an explicit cast.

The native-backend set-variable crash documented in bugs.txt was
already fixed in HEAD (the release binary produced a codegen error, but
the current compiler handles it correctly).

Tests added:
- TSetTests.TestSemantic_Set_EqualityEmptyLiteral_OK — S=[] must not crash
- TSetTests.TestSemantic_Set_EqualityLiteral_OK — S=[fA,fB] accepted
- TSetTests.TestCodegen_Set_EqualityEmptyLiteralEmitsCeqw — IR check
- TSetTests.TestCodegen_Set_EqualityLiteralEmitsCeqw — IR check
- TE2EMiscTests.TestRun_Set_EqualityWithLiteral — compile+run assertion
- TE2ENativeTests.TestRun_ZeroInit_SetLocal now runs on both backends
2026-06-12 16:21:25 +01:00
Graeme Geldenhuys a2d263e0e8 feat(lang): guarantee zero-initialisation of all variables
Blaise now guarantees zero-initialisation of every variable as a language
semantic — local variables, globals, record fields, static-array elements,
threadvars, and Result.  The QBE backend already satisfied this; the native
x86-64 backend was the only gap (scalar locals were uninitialised on stack).

Native backend changes (blaise.codegen.native.x86_64.pas):
- Replaced the ARC-only zero-init loop in EmitFunctionDef with an
  exhaustive case over all TTypeKind values, covering every scalar
  type (integer family, float, boolean, pointer, enum, set, procedural)
  in addition to the already-handled managed types.  An else-raise clause
  ensures any future new type kind is caught at compile time rather than
  silently skipped.
- Fixed AddSlot to allocate 16 bytes for method-pointer locals (Code +
  Data slots), matching the 16-byte allocation the QBE backend already
  used.  Previously only 8 bytes were reserved, which would corrupt an
  adjacent frame slot if a local method-pointer was written.

10 new E2E tests run on both backends via AssertRunsOnAll, using a
Dirty() helper that pre-fills the stack with 0xDEADBEEF to prove zero-init
comes from the prologue and not from lucky stack layout:
  TestRun_ZeroInit_ScalarIntegers, FloatLocals, BooleanAndChar,
  PointerLocals, EnumLocal, SetLocal (QBE-only — native crashes on sets,
  pre-existing bug), RecordWithMixedFields, StaticArray, ThreadVar,
  GlobalVars.

Documented in docs/language-rationale.adoc (decision, alternatives
rejected, implementation notes, future noinit/definite-assignment roadmap).
2026-06-12 16:09:02 +01:00
Graeme Geldenhuys d182b4ab7e fix(debug): attribute generic-instance allocation sites to the defining unit
The --debug leak tracker reports allocation sites as '<unit>:<line>'.
For objects allocated inside generic method bodies the two halves of
that pair came from different sources: the line number is the cloned
template AST's Line field (which refers to the template's source file),
but the unit name was FCurrentUnitName — the unit or program being
emitted when the generic instance's method bodies were generated, i.e.
the INSTANTIATING unit.  A TListEnumerator<Integer> created inside
TList<T>.GetEnumerator (generics.collections.pas:316) was therefore
reported as 'P:316' for a 33-line program P.

Fix: record the declaring unit on TGenericTypeDef when the template is
registered (both the direct semantic path and the .bif import path),
copy it onto TGenericInstance at instantiation, and have both backends
temporarily switch FCurrentUnitName to GI.DefUnitName around the
emission of generic-instance method bodies, restoring it afterwards.
The report now reads 'Generics.Collections:316'.

The new DefUnitName fields are populated by the semantic pass at
runtime and are intentionally not serialised; bif-coverage passes
unchanged.  Generic record and generic function instances still carry
the old mismatch and are tracked separately.

New e2e tests (QBE + native): TestDebug_GenericAllocSite_ReportsDefiningUnit
and TestDebug_GenericAllocSite_ReportsDefiningUnit_Native.
2026-06-12 15:16:45 +01:00
Graeme Geldenhuys 1dfc693fe5 fix(codegen): for-in over enumerators — promoted loop var and leaked enumerator
Two bugs in the enumerator-protocol for-in lowering, found via a user
program iterating a TList<Integer>:

- QBE: the mem2reg promotion scan did not treat the for-in loop
  variable as a store target, so an otherwise-unaddressed scalar loop
  var was promoted to an SSA temp while the lowering wrote the element
  with storew %t, %_var_<name> — invalid IR ('invalid type for second
  operand ... in storew').  The loop variable now always keeps a stack
  slot.

- Both backends: GetEnumerator's result (an owned +1 constructor
  return) was re-retained when stored into the synthetic enumerator
  slot, while the function epilogue releases the slot exactly once —
  leaking one enumerator per loop (rc=1 in every --debug leak report).
  The owned reference is now transferred into the slot.

e2e leak-check tests run the same list-summing loop under --debug on
both backends and assert clean output with no leak report.
2026-06-12 15:05:43 +01:00
Graeme Geldenhuys eabf20ae7f refactor(driver): driver-aware worker; registry-driven flags and pre-flight
TCompileWorker now carries Driver + Opts and builds its per-unit
codegen through Driver.CreateUnitCodeGen, names its temp IR file with
the driver's extension, and lowers through the same driver.  A driver
that claims SupportsIncremental but returns a nil unit codegen fails
with a clear error instead of crashing.  The incremental dispatcher
prefers the top-program backend's driver and falls back to QBE when
the backend has no per-unit emission yet — the only remaining backend
comparison outside PickTopDriver.

--backend validation and the --help backend line are now driven by the
driver registry (ParseBackendName / RegisteredBackendNames), so adding
a backend no longer touches the flag parser.  The toolchain pre-flight
runs once through Driver.CheckToolchain before the front-end; stdout-
only modes (--emit-ir / --emit-asm / --dump-ast) skip it since they
need no external tools.

Blaise.pas no longer references TCodeGenQBE/TCodeGenNative anywhere;
unused locals from the pre-driver pipeline are removed.

Architecture follows Andrew Haines' unify_backend_interface proposal.
2026-06-12 14:05:38 +01:00
Graeme Geldenhuys 543f495f48 refactor(driver): lowering and linking through the backend drivers
Move the IR-to-object and IR-to-binary pipelines behind TBackendDriver
(architecture per Andrew Haines' unify_backend_interface proposal):

* LowerToObject — QBE driver runs qbe -> .s -> cc -c -> .o; the base
  class default fails loudly for drivers without per-unit emission.
* LinkProgram — QBE driver lowers via qbe then links; the native driver
  links its assembly directly, or assembles in-process
  (AssembleToObject) and links the .o when --assembler internal is
  selected.  The --assembler policy now lives inside the native driver
  instead of Blaise.pas.
* LinkViaToolchain — shared protected link line (input, OPDF sidecar,
  prebuilt dep objects, RTL archive, -lm, -lpthread) resolved through
  uToolchain, so BLAISE_QBE/BLAISE_LINKER/BLAISE_RTL overrides now apply
  uniformly; the unit-object path previously hard-coded 'qbe'/'cc'.
* RunProcess moves to blaise.codegen.driver (used from worker threads).

Blaise.pas drops CompileToNative, CompileToNativeDirect, LinkObjectFile,
FindRTL, and the backend branch in the output dispatch; the single
shared path writes the IR with the driver's file extension and calls
Driver.LinkProgram.  Behaviour deltas: the native link now also receives
auto-discovered prebuilt dep objects (previously dropped on the native
path), and link failures report 'link error (exit N)' uniformly.
2026-06-12 14:01:48 +01:00
Graeme Geldenhuys 5603767298 refactor(driver): introduce TBackendDriver; route codegen construction
Add blaise.codegen.driver with the abstract TBackendDriver base class
(virtual Kind/Name/IRFileExt/SupportsIncremental/SupportsWarmCache/
CheckToolchain/CreateCodeGen/CreateUnitCodeGen), the TBackendOpts flag
bag, a fixed-array registry keyed by backend kind, and PickTopDriver —
the single backend-selection policy decision (--emit-ir forces QBE,
--emit-asm implies native, otherwise --backend).

blaise.codegen.qbe.driver and blaise.codegen.native.driver register
class singletons at unit initialization; they are ARC-managed globals
released by the program-exit release pass.

Blaise.pas now builds one TBackendOpts up front, resolves the driver
once via PickTopDriver, and constructs the code generator through
Driver.CreateCodeGen — the backend if/else around TCodeGenQBE /
TCodeGenNative construction is gone.

The architecture follows Andrew Haines' unify_backend_interface
proposal; full credit to Andrew for the driver/registry design.  This
tree uses an abstract class with virtual methods instead of an
interface so shared behaviour can live in the base class.
2026-06-12 13:55:01 +01:00
Graeme Geldenhuys 06517bf1d3 fix(qbe): dispatch record/interface-returning virtual methods via vtable
EmitRecordCallSret always emitted a direct call to the declaring class's
method symbol, even for virtual methods.  Overrides never ran on the
record/interface sret return path, and calling a virtual-abstract method
(which has no emitted body) produced an undefined-symbol link error.

Add SretMethodCallTarget: when VTableSlot >= 0 load the vptr from the
instance and the function pointer from the vtable slot (slot 0 is
typeinfo, so method N lives at (N+1)*8), otherwise keep the static
symbol.  Applied to all three receiver shapes in EmitRecordCallSret
(implicit-Self call, class-receiver method call, zero-arg field-access
call).

E2E coverage: TestRun_Native_IntfFromClassMethod now exercises an
override returning an interface; new TestRun_Native_RecReturnVirtualOverride
covers the record-returning case on both backends.
2026-06-12 13:53:25 +01:00
Graeme Geldenhuys 1ba78cf90b fix(native): support class-receiver method calls returning interfaces
IntfVar := ClassObj.Method() failed with 'unsupported interface-field
assignment RHS' on the native backend (the QBE backend already handled
it).  EmitInterfaceAssign only covered itab-dispatch receivers and plain
function calls in its sret branches.

Add EmitClassIntfSretMethodCall — sret buffer as hidden first arg
(%rdi), receiver in %rsi, static or vtable dispatch from the receiver's
class — and wire it into all three EmitInterfaceAssign regions
(implicit-Self field, sret Result/var-param, local/global LHS).  Calls
with more than four user argument slots fail loudly until needed.

Covered by TE2ENativeTests.TestRun_Native_IntfFromClassMethod, which
exercises every LHS shape plus virtual dispatch on both backends.
2026-06-12 13:44:19 +01:00
Graeme Geldenhuys f0f888f2c5 fix(codegen): interface-dispatch ABI — record args, var/out params, call receivers, discarded returns
Four itab-dispatch ABI gaps reported from Andrew Haines'
unify_backend_interface branch, fixed on both backends:

- Record by const/value through interface dispatch (QBE) passed the
  record as 'w <addr>' — truncating to the low 8 bytes and shifting
  every later argument.  Dispatch sites now use the same :_ffi_<Name>
  aggregate ABI as direct calls.  (Native already passed the address.)
- var/out parameters through interface dispatch loaded the VALUE
  instead of passing the slot address.  Root causes: a 1-based loop
  over the 0-based var-flag string in MethodParamIsVar (parameter 0's
  flag was never seen), the QBE statement path ignoring var flags, and
  the native push loops never emitting addresses.  Strings and
  dynarrays covered.
- TFuncCallExpr receivers (GetDriver(x).Info()) previously raised
  fail-loud.  The receiver call is sret-evaluated into a temporary fat
  pair; the owned +1 obj is released right after the consuming call
  (QBE: pending-release list flushed by every call emitter; native:
  pair kept above the hoist region, released preserving result regs).
- Discarded interface-returning itab calls in statement position
  clobbered memory through the register the callee expects to hold the
  sret buffer (QBE: missing buffer; native: receiver passed where the
  buffer belongs, shifting Self).  Statement calls now get a throwaway
  sret buffer and release the returned obj.  Semantic records the itab
  return type on TMethodCallStmt (bif-coverage clean).

e2e tests run on both backends (cp.test.e2e.imap); IR tests pin the
aggregate ABI, slot-address passing, and the discard-sret+release
sequence (cp.test.interfaces).
2026-06-12 13:27:00 +01:00
Graeme Geldenhuys c20ddd549d fix(codegen): interface nil-compare and interface-method sret returns
Two interface-dispatch bugs surfaced by Andrew Haines'
unify_backend_interface branch, fixed on both backends:

1. Interface value reads through a bare identifier (nil compares,
   most visibly 'if Result = nil' inside an interface-returning
   function) loaded a non-existent single slot.  Interface idents now
   load the obj half of the fat pointer: locals via the _obj slot,
   globals via the Name_obj label, Result via the sret buffer
   pointer.  QBE additionally treats tyInterface as a pointer kind in
   comparisons (ceql/cnel).  The native backend previously compared
   the sret buffer ADDRESS against nil — always false, silently.

2. V := Intf.Method(...) where Method returns an interface emitted a
   single-slot store against the split-slot local (invalid QBE IR /
   wrong native code).  Itab-dispatched calls returning interfaces now
   route through the sret convention like plain function calls: QBE
   grows EmitIntfSretDispatch; native grows EmitIntfSretMethodCall,
   wired into every interface-assignment LHS shape.  Ownership follows
   the existing convention (callee AddRefs into the sret buffer, the
   caller takes the owned +1 pair).

Also fixes a latent bug found while testing: assigning an
interface-typed Result to a global stored the sret buffer pointer as
the obj half (native PushIntfIdentPair now dereferences correctly).

Still open, fail-loud (recorded in bugs.txt): TFuncCallExpr receivers
of interface calls, and discarded interface-returning itab calls in
statement position on the QBE path.

IR tests in cp.test.interfaces; e2e tests in cp.test.e2e.imap run the
registry-pattern acceptance program on both backends.
2026-06-12 12:50:19 +01:00
Andrew Haines 81c5804ae4 fix(codegen): static-array element read/write of interface uses fat pointer
Ported from Andrew Haines' unify_backend_interface branch (97b4d6a9).

Both halves of the static-array element path missed the interface
fat-pointer layout: Arr[I] := IFaceVal fell through to the generic
element store, which picked storew against the 16-byte slot and left
itab uninitialised; F := Arr[I] emitted a single-slot load and a store
against the bare global name (undefined reference at link time).  The
element slot is now treated as the contiguous obj+itab pair on both
paths, with class->element stores resolving the itab by name and
nil stores releasing the prior object.

Two additions to the original patch:
- The class->element store transfers ownership when the RHS already
  owns +1 (ExprOwnsRef guard, mirroring EmitAssignment's class->iface
  branch) instead of an unconditional _ClassAddRef that leaked one
  reference per constructor-RHS store.
- The new TStringSubscriptExpr case in EmitInterfaceExprPair is
  restricted to STATIC array bases: dynamic-array subscripts return a
  loaded value, not an address, and keep hitting the fail-loud error
  until they get their own handling.

Tests: the two IR tests from the original patch (read assertion updated
to the contiguous global fat-pointer layout that landed after Andrew's
base), plus an e2e test (TestRun_StaticArrayOfInterface_FatPointer)
covering store, dispatch through Arr[I], element-to-var copy,
element-to-element copy, and nil store at runtime.
2026-06-12 12:13:54 +01:00
Graeme Geldenhuys 1babf0c9a5 feat(linker): section merger (completes Phase A)
TSectionMerger in the new blaise.linker.elf unit concatenates
like-named allocatable sections across input objects, padding each
contribution to its declared alignment, and records a placement
(merged section + offset) per input section — the basis for symbol
and relocation rebasing in Phase B.

SHT_NOBITS contributions advance the merged size without emitting
bytes; mixing NOBITS and PROGBITS under one name is an error.
Bookkeeping sections (symtab, strtab, rela, .note.GNU-stack,
.comment) are skipped — the linker rebuilds those itself — while
non-alloc .opdf.* debug sections are kept for the OPDF pass-through.

Tests cover text concatenation with placement offsets, alignment
padding between contributions, .bss size accumulation, and the
bookkeeping-section skip list.
2026-06-12 10:13:16 +01:00
Graeme Geldenhuys 5415815f8d feat(linker): ELF object and ar-archive reader (Phase A)
First step of the internal-linker plan
(docs/internal-linker-design.adoc): blaise.elfreader parses ELF64
little-endian ET_REL x86-64 objects — section headers with contents,
the symbol table, and RELA relocation entries — and !<arch> static
archives with GNU long-name table support (blaise_rtl.a carries
member names beyond the 15-character ar limit).

Parsed entities are heap objects rather than records to sidestep the
known dynamic-array-of-record element-assignment hazard.  The archive
API fills a caller-owned TList (generic function return types are not
supported yet).

TElfReaderTests covers: section bytes and flags from the internal
assembler's output, global function symbols, .quad relocations with
addends, NOBITS sections, bad-magic rejection for both formats, a
synthetic archive exercising the GNU long-name table and member
padding, and a sweep over every member of the real blaise_rtl.a.
2026-06-12 10:10:21 +01:00