Commit graph

423 commits

Author SHA1 Message Date
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
Graeme Geldenhuys 1ac1e6e536 fix(codegen): emit generic interface-instance typeinfo in multi-unit builds
A generic class implementing a generic interface (e.g. TDictionary<K,V> ->
IMap<K,V>) failed to link when instantiated inside a unit rather than the main
program:

  undefined reference to `typeinfo_IMap_string_Integer'
  (referenced by impllist_TDictionary_string_Integer)

The program codegen path emits generic interface-instance typeinfo blocks from
AProg.GenericIntfInstances (EmitInterfaceDefs), but AppendUnit emitted the
generic-class-instance impllist — which references that typeinfo — without ever
emitting the typeinfo itself for the unit's GenericIntfInstances. Single-program
use worked; the unit path left a dangling symbol.

AppendUnit now emits `data $typeinfo_<InstName> = { l 0 }` for each
AUnit.GenericIntfInstances, mirroring the program path.

Adds a CompileAndRunWithUnit e2e helper (writes a user unit to the scratch dir
so the unit, not the program, is the compilation unit) and a regression test
TE2EIMapTests.TestRun_IMap_GenericDictInUnit_LinksAndRuns that instantiates
TDictionary<string,Integer> inside a unit and runs it.

Verified: 2310 tests pass; fixpoint clean.
2026-06-02 18:58:58 +01:00
Graeme Geldenhuys 23af526bbb feat(codegen): native backend M5 — user functions, parameters, recursion
Add a stack-frame model and direct procedure/function calls to the x86_64
backend:

- Frame model: each function's params, Result, and local vars get a 4-byte
  -N(%rbp) slot, recorded in FFrame (name -> offset). VarOperand picks frame-
  slot addressing for locals and RIP-relative for program globals; variable
  reads, assignments, and the for-loop variable all route through it, so loops
  and assignments work correctly inside functions.

- EmitFunctionDef: standard prologue, reserve a 16-aligned frame, spill the
  incoming SysV argument registers (edi/esi/edx/ecx/r8d/r9d) into the param
  slots, run the body, load Result into %eax for a function, epilogue. Layout
  matches the FPC -O- reference (checked against 3.2.2 and 3.3.1) and the QBE
  backend.

- EmitCall: integer value arguments evaluated left-to-right and pushed, then
  popped into the argument registers (so a complex argument cannot clobber an
  already-set register); call; result in %eax. Wired into both expression
  position (TFuncCallExpr) and statement position (TProcCall).

Scope: integer value parameters and integer/void return. Recursion works
(verified with factorial). Indirect calls, var/out parameters, conversions,
more than six arguments, and non-Integer parameter/return types fail loudly
and are left for follow-up.

Container note: the frame map wants TDictionary<string,Integer>, but a generic
class implementing a generic interface (TDictionary -> IMap<K,V>) currently
fails to link when instantiated inside a unit (open multi-unit typeinfo bug,
see bugs.txt). Since this backend is part of the multi-unit compiler build, it
uses a TStringList for now with a TODO to switch once that bug is fixed.

Output matches the QBE backend on functions, multi-argument calls, calls in
expressions, nested calls, recursion, and for loops over a local variable.

Verified: 2309 tests pass; fixpoint clean.
2026-06-02 18:58:58 +01:00
Graeme Geldenhuys 3772213f61 feat(codegen): native backend M4 — integer variables and for loops
Add program-level integer variables and the for loop to the x86_64 backend:

- Variables: a program's top-level var block is lowered to RIP-relative .data
  globals, the same model the QBE backend uses. EmitExprToEax reads an integer
  ident as `movl name(%rip), %eax` (named constants fold to an immediate);
  TAssignment stores `movl %eax, name(%rip)`. EmitDataSection collects every
  slot and emits one .data definition each — named program variables are
  exported, hidden compiler-generated slots (.L-prefixed) stay file-local.
  Declared integer vars are registered up front so unused declarations still
  get a slot (matching QBE).

- for loop: the end expression is evaluated once into a hidden global slot
  (Pascal semantics), then cond (i <= end / i >= end for downto) - body -
  increment/decrement - loop. Supports to/downto and nesting.

Output matches the QBE backend on sum-over-for, downto, nested for, the
for-end-evaluated-once rule, and counter-driven while/repeat loops.

Native e2e tests added: vars + for, downto + nested for, counter loops, and
for-end-evaluated-once.

Verified: 2306 tests pass; fixpoint clean.
2026-06-02 18:58:58 +01:00
Graeme Geldenhuys 3c76343969 feat(codegen): native backend M3 — if/while/repeat control flow
Add control-flow lowering to the x86_64 backend:

- Comparison operators (= <> < > <= >=) in EmitExprToEax: cmpl %ecx, %eax
  followed by the matching setcc and movzbl, yielding a 0/1 in %eax.
- EmitCondBranch: evaluate a condition to %eax, testl, jne true-label, jmp
  false-label.
- NewLabel: unique .L-prefixed local labels.
- if/else, while, and repeat..until in EmitStmt, plus TCompoundStmt block
  flattening. Loop/branch shapes mirror the QBE backend (while: cond-body-cond;
  repeat: body then branch true->end / false->body).

Output matches the QBE backend on if/else, all six comparison operators,
nested if, while (false condition), and repeat (single iteration).

`for` is deferred to M4: it needs a loop variable, which requires local
variable storage (the M4 milestone).

Native e2e tests added: if/else, comparisons + nested if, while/repeat.

Verified: 2302 tests pass; fixpoint clean.
2026-06-02 18:58:58 +01:00
Graeme Geldenhuys 6afe35cb2e feat(codegen): native backend M2 — integer arithmetic and Write/WriteLn
Add expression and statement lowering to the x86_64 backend:

- EmitExprToEax: integer literals and the binary operators + - * div mod.
  Evaluates left into %eax, pushes it, evaluates right into %eax, pops left
  into %ecx, and combines (addl/subl/imull; cltd+idivl for div, with %edx for
  mod). The push/pop pairs are balanced within each expression, so %rsp stays
  16-byte aligned at every call site. Correct for arbitrary nesting with no
  register allocator.
- EmitStmt / EmitWrite: Write and WriteLn of integer arguments, lowered to
  _SysWriteInt(fd=1, value) per argument plus a trailing _SysWriteNewline for
  WriteLn — the same runtime contract the QBE backend uses.
- EmitProgram now lowers the program body statements between _SetArgs and the
  return-0 epilogue.

Output matches the QBE backend exactly on arithmetic, integer div/mod, nested
parenthesised expressions, operator precedence, negative results, and Write
without a newline.

Native e2e tests added (cp.test.e2e.native): int arithmetic + WriteLn, div/mod
+ nesting, and Write-without-newline.

Verified: 2299 tests pass; fixpoint clean.
2026-06-02 18:58:58 +01:00
Graeme Geldenhuys 4d41707c48 feat(codegen): native backend M1 — empty program compiles and runs
TX86_64Backend.EmitProgram emits the program entry function in AT&T assembly:
the $main(argc, argv) prologue, the $_SetArgs runtime call, and a return-0
epilogue — mirroring the QBE backend's $main shape. TCodeGenNative.Generate
drives the backend and returns the assembly text; the driver links it via cc
(CompileToNativeDirect) with no QBE step.

`program P; begin end.` now compiles and runs via --backend native and exits 0
with no qbe invocation (verified by strace: only cc is spawned; objdump
confirms main is the native-emitted code).

The backend accumulates assembly in a TStringBuilder (stdlib) rather than a
TStringList: emission is append-only and read once at the end, so a single
growable buffer avoids per-line heap strings and the O(N^2) cost of a final
concat — the same approach the QBE backend's TIRBuffer uses.

Adds CompileAndRunNative to the e2e base (lowers via TCodeGenNative, links,
runs) and a native e2e suite (cp.test.e2e.native) with the M1 empty-program
test. Correctness oracle is parity with the QBE path.

Verified: 2296 tests pass; fixpoint clean.
2026-06-02 18:58:58 +01:00
Graeme Geldenhuys d64656c5b5 feat(codegen): scaffold native backend (--backend native / --target)
Wire up the native code-generation backend skeleton (Phase 5 of
docs/toolchain-independence.adoc), gated behind a new --backend flag with
QBE remaining the default.

New units:
- blaise.codegen.target: TTargetDesc (OS, CPU) + PtrSize, ParseTargetName,
  TargetName, HostTarget. Targets accepted: linux-{x86_64,i386,arm64},
  freebsd-{x86_64,arm64}, windows-x86_64, macos-arm64 (only linux-x86_64 has
  a backend so far). All pointer-size logic routes through PtrSize so 32-bit
  targets flip by one value.
- uToolchain: env -> $PATH -> fallback resolution of as/cc/qbe + the RTL
  archive, ported and trimmed from a contributor LLVM branch (LLVM-specific
  slots dropped). POSIX-host path conventions; no RTL change required.
- blaise.codegen.native.backend: TNativeBackend abstract base (own unit to
  avoid a cycle with the concrete backends).
- blaise.codegen.native: TCodeGenNative (implements ICodeGen, will walk the
  AST) + CreateNativeBackend factory.
- blaise.codegen.native.x86_64: TX86_64Backend shell registered with the
  factory.

Driver (Blaise.pas): --backend qbe|native and --target <os>-<cpu> parsing
with validation; --emit-ir always uses TCodeGenQBE (fixpoint protection);
native output writes assembly text and links via CompileToNativeDirect using
the same cc line as the QBE path. Code emission is not yet implemented, so a
native compile fails loudly with a clear per-target message.

Also fix a second pre-existing codegen bug surfaced by passing TTargetDesc as
a var/out parameter: whole-record assignment to a var/out record parameter
(T := L / T := Func) had no case in EmitAssign's IsVarParam branch and fell
through to a single-word store, corrupting the record. Added a tyRecord case
that copies into the dereferenced caller address (sret for a record-returning
call, else EmitRecordCopy).

Regression tests (e2e, since both bugs were link/runtime-level):
- TestRun_Record_AssignToVarParam (records)
- TestRun_Interface_GlobalNil_LinksAndRuns (classes2)

Verified: 2295 tests pass; fixpoint clean (stage-2 IR == stage-3 IR).
2026-06-02 18:58:58 +01:00
Graeme Geldenhuys 4eb3b8a3c2 refactor(codegen): introduce ICodeGen interface for backend dispatch
Extract the codegen pass contract (Generate/GenerateUnit/SetSymbolTable/
SetDebugMode/AppendUnit/AppendProgram/GetOutput) into an ICodeGen interface
in the new uCodeGen unit. TCodeGenQBE now implements it, and the Blaise.pas
driver holds an ICodeGen and runs the codegen sequence once polymorphically
rather than via a per-backend branch. This is the seam the upcoming native
backend (TCodeGenNative) plugs into.

ICodeGen is an interface rather than an abstract base class: Blaise objects
are fully ARC-managed, so an interface carries no lifetime penalty, and the
QBE-text and (future) machine-code backends share no implementation that a
base class could host. The driver's codegen object is now ARC-released at
scope exit; the manual Free is removed.

Also fix a pre-existing codegen bug surfaced by making the driver's codegen
variable interface-typed: assigning nil to an interface-typed GLOBAL emitted
a single-slot `storew, $Name` against a symbol that has no data definition
(interface globals use the $Name_obj/$Name_itab fat-pointer pair), causing an
undefined-reference link error. EmitAssign now has a dedicated interface-nil
case clearing both slots with correct ARC release; interface locals were
already correct.

Verified: 2293 tests pass; fixpoint clean (stage-2 IR == stage-3 IR);
valgrind reports no invalid free from the ARC interface transition.
2026-06-02 18:58:58 +01:00
Graeme Geldenhuys 0055f4b480 Status update 2026-06-02 17:31:02 +01:00
Graeme Geldenhuys 7efae6d6e0 docs(benchmark): add QBE 1.3 measurement entry
Records allocator microbenchmarks and compile-step timing after
updating the vendored QBE from v1.2 to v1.3.  The compile step
(QBE translating IR to machine code) is 3.5% faster in the stage-2
binary assembled by QBE 1.3, attributed to the new GVN/GCM passes.
2026-06-02 17:17:54 +01:00
Graeme Geldenhuys c9dde987f0 vendor: update QBE from v1.2 to v1.3
QBE 1.3 (released 2026-06-01) adds Windows AMD64 ABI support
(amd64_win target), improved PIC via extern symbol constants,
GVN/GCM optimisation passes, if-conversion, and various
correctness and performance fixes.

All 2293 compiler tests pass against the new QBE binary.
2026-06-02 16:54:51 +01:00
Graeme Geldenhuys 211f9d3f46 docs: update test count to 2293 for v0.9.0 2026-06-02 10:19:18 +01:00
Graeme Geldenhuys bd57dece6b chore: begin v0.10.0-dev cycle 2026-06-02 09:58:07 +01:00
Graeme Geldenhuys 7e84fe71ac release: v0.9.0
Fixpoint achieved on the multi-file self-hosted source at 166,479 lines
of verified QBE IR. Includes FFI fixes (narrow return masking, Single
alignment, SysV aggregate ABI for record-by-value), ARC field-cleanup
destructor mangling, dynamic-array address-of, and const-initialiser
enhancements (integer typecasts and bit-op folding).
2026-06-02 09:57:17 +01:00
Graeme Geldenhuys 0ce9507775 docs: rewrite 'Function calls requiring ()' section as proper design entry
Replace the raw conversation fragment with a structured improvement entry
covering the motivation (TIdentExpr.IsNoArgFuncCall ambiguity), the
interaction with the Result-variable convention, migration impact, and
effort estimate.
2026-06-02 09:54:14 +01:00
Graeme Geldenhuys 8ea4c79f19 style(codegen): fix indentation of EmitFFIRecordTypeDecls call sites
Both calls to EmitFFIRecordTypeDecls and the following
FOutput.AppendBuffer(Body) in GenerateUnit/AppendUnit were
de-dented one level outside the enclosing try..finally body.
Align them with the surrounding code.
2026-06-02 09:49:40 +01:00
Andrew Haines 7b99e79114 feat(parser,semantic): fold bit-op chains in const initialisers
ParseConstBlock previously accepted only a single operand in the
value position — '5', '-5', 'NamedConst', or a string-concat chain.
'1 or 2', 'FG_BLUE or FG_GREEN', '$FF and 15', '1 shl 8' all failed
to parse, forcing callers to hand-fold flag combinations and bit
masks before declaring them as constants.

Add deferred bit-op support driven from the parser:

  - New TConstDecl.IntExprTokens (TStringList) carries the scalar
    operand/operator chain when at least one bit op is present.
    Operands are tagged on Objects[]: nil = integer literal, non-nil
    = ident reference.  Interleaved operator tokens carry the lower-
    case op name ('or', 'and', 'xor', 'shl', 'shr').
  - New TConstDecl.ArrayElementParts (TObjectList, parallel to
    ArrayElements) carries the same shape per element.  Elements
    that are plain literals/typecasts remain inline in ArrayElements;
    elements that include a bit op get a deferred token list, and
    semantic rewrites the matching ArrayElements entry once it has
    resolved.
  - CollectConstBitOpExpr (parser) builds the token lists.  Operands
    may be integer literals (optionally signed), TypeName(...) casts
    re-using the typecast helper, or named-constant idents.
  - FoldConstBitOpExpr (semantic) walks the token list left-to-right,
    resolves idents through the symbol table, and applies the ops.
    Wired into AnalyseConstDecls (scalar) and AnalyseArrayConstDecls
    (array elements).

Regression tests cover all-literal chains, all-named-const chains,
mixed literal+named chains, and/shl, plus an array-element case.
2026-06-02 09:47:35 +01:00
Andrew Haines 35e0a6cf7e feat(parser): accept integer-type typecast in const initialisers
ParseConstBlock previously only recognised bare literals (optionally
negated), string concatenations, and named-constant idents as const
initialiser values.  TypeName(IntLit) and TypeName(-IntLit) — common
idiom for expressing values like Cardinal(-11) = $FFFFFFF5 — failed
with "Expected ';' but got '('".

Add TryParseConstIntTypecast which recognises Byte / ShortInt /
SmallInt / Int16 / Word / UInt16 / Integer / LongInt / Cardinal /
LongWord / UInt32 / Int64 / UInt64 / QWord / PtrUInt followed by '(',
parses the optional sign and integer literal inside, applies the
target type's bit-width truncation mask, and sign-extends back when
the target is signed.  Both scalar const init and array-element
positions accept the form.

Regression tests cover all five width × sign combinations plus an
array-element case.
2026-06-02 09:47:35 +01:00
Andrew Haines 58249bcb21 fix(codegen): pass records by value through SysV aggregate ABI
Record-by-value parameters were emitted as `l <addr>` — a bare
pointer in an integer register.  For a `cdecl external` callee that
means the C side reads the low bits of the address as the struct
contents (e.g. a 4-byte RGBA record into ClearBackground would
clear the window to whatever colour fell out of the stack-frame
address).  For intra-Blaise calls it accidentally worked because
callees re-dereferenced the pointer.

QBE handles SysV §3.2.3 aggregate classification natively: declare
the record once as `type :_ffi_<Name> = align A { ... }`, then pass
the source pointer with that aggregate type at the call site.  QBE
scatters fields into INTEGER / SSE eightbytes per the ABI, the C
callee sees the struct in the right registers, and inside Pascal
callees QBE re-gathers them into a hidden buffer reachable via the
same pointer-typed parameter — so the existing memcpy-into-`%_var_X`
prologue keeps working.

Applied uniformly across param signatures and every call site
(standalone, implicit-self, indirect, method-call, inherited, ARC
release, FreeAndNil, expression-context func/method calls) — not
gated on IsExternal — so Pascal-to-Pascal calls and C-FFI calls use
the same ABI and callbacks passed to C work without translation.

A latent value-by-reference bug for value-record params disappears
as a side-effect: callees now mutate QBE's hidden gathered buffer
rather than the caller's storage, matching declared value
semantics.

Returns still use sret; aggregate-return is a separate change
because it interacts with ARC ownership transfer on managed
record fields.
2026-06-02 09:47:35 +01:00
Andrew Haines d0128ed645 fix(codegen): allow @ on dynamic-array elements
EmitAddrOfExpr's TStringSubscriptExpr block enumerated only static
and open arrays.  Taking the address of a dyn-array element via
`@A[i]` fell through to the generic L-value path, which didn't know
about TStringSubscriptExpr and raised

  Code generation error: Unsupported L-value form for var argument

Read paths and SetLength already worked; only @-of-subscript broke.
Add a tyDynArray branch that mirrors the open-array shape (no
LowBound, element size from TDynArrayTypeDesc.ElementType.RawSize) —
EmitExpr on a dyn-array var already returns the heap data pointer.

E2E regression test in cp.test.e2e.dynarray.pas writes through the
captured PRec to confirm the address points at the right element.
2026-06-02 09:47:35 +01:00
Andrew Haines f6bb0ee029 fix(codegen): narrow Double arg to Single before FFI call
CoerceArg handled w/l → s sitofp but had no d → s narrowing.  A
double-typed expression (literal or arithmetic result) passed to a
Single-typed parameter reached the assembler as `d` against an `s`
slot, which QBE rejected with `invalid type for first operand in
arg`.  On the wire — had the assembler accepted it — 8 bytes of
double would go into the float-by-value slot and the C
`float`-by-value callee would see the low mantissa half as a single,
i.e. noise (`sinf(1.5707964)` returns -0.998 instead of 1.0).

Adds the d → s arm via `truncd`.  Regression test in
cp.test.external.pas pins the `=s truncd` shape for a double literal
passed to a Single FFI parameter.
2026-06-02 09:47:35 +01:00
Andrew Haines 015388918c fix(symtab): Single fields get 4-byte alignment, not 8
TTypeDesc.AllocAlign had no tySingle case, so it fell through to the
catch-all `else: 8`.  A record of three back-to-back Single fields
then padded each 4-byte field up to an 8-byte slot, reporting SizeOf
as 24 instead of 12 — wrong on the wire for any struct-of-floats
passed to a C library (OpenGL vectors, math libraries, audio frame
structs) and wrong for GET/PUT against expected on-disk layouts.

Adds explicit `tySingle: 4` case.  Regression test in
cp.test.records.pas asserts a TVec3 of three Single fields has
TotalSize = 12.
2026-06-02 09:47:35 +01:00
Andrew Haines a1c742f851 fix(codegen): narrow double literal to float when stored to a Single
record field

Real-typed literals and double sub-expressions land in the SSA at
double width.  Storing one of these into a Single record field
emitted a double-typed store against a float-typed slot — a type
mismatch the assembler rejected outright (`stores d_1.5, ...`).

EmitFieldAssignment already coerced i32 → i64 sign-extensions for
narrow integer fields; mirror the pattern for float widths,
emitting a `truncd` when narrowing double → single and an `exts`
when widening the symmetric direction (single source into a
double field).

Adds two IR-shape regression tests covering both directions.
2026-06-02 09:47:35 +01:00