Commit graph

875 commits

Author SHA1 Message Date
Graeme Geldenhuys 5e42020faf stdlib: use TList<string> instead of TStringList in Net.Http.Server
The server only used Create/Add/Get/Count/Free on its string lists — none of
the TStringList-specific API — so TList<string> is behaviour-identical and lets
the unit drop its Classes dependency.
2026-06-23 17:28:05 +01:00
Graeme Geldenhuys dd635d5140 stdlib: document the libc/kernel layering rationale in Net.Sockets
Explain why the unit binds libc (the kernel boundary, the syscall doorway), the
common three-layer pattern shared with Indy/Synapse/the JDK, how Java is itself
not libc-free, and what a pure-Pascal syscall-stub version would and would not
buy.
2026-06-23 17:21:36 +01:00
Graeme Geldenhuys 3c6fbc3184 stdlib: flag Linux-specific porting boundaries in Net.Sockets and Security.Guid
Document that the socket constants/sockaddr_in layout (Net.Sockets) and the
getrandom(2) call (Security.Guid) are Linux x86_64 specifics, with the per-OS
differences and the fact that the public helper API is the stable surface a
future port keeps.
2026-06-23 00:22:33 +01:00
Graeme Geldenhuys 2ccfd04f69 stdlib: add Security.Guid (RFC 4122 v4 GUIDs)
NewGuid returns a canonical lowercase v4 GUID; NewGuidRaw returns the 16 raw
bytes. Randomness comes from the kernel CSPRNG via getrandom(2), with the
version (4) and variant (RFC 4122) bits forced. Tests cover the canonical
format, version/variant bits in both string and raw forms, and uniqueness
(47 tests total).
2026-06-23 00:15:03 +01:00
Graeme Geldenhuys cc43c4a0c5 stdlib: add Net.Http.Server (minimal HTTP/1.1 + WebSocket)
Single-threaded HTTP/1.1 server lifted from the Luhmann web server and
generalised: THttpRequest/THttpResponse/IRequestHandler/THttpServer plus
ParseRequest/UrlDecode. WebSocket upgrades are handled via Net.WebSockets and
Broadcast(payload) pushes a text frame to every connected socket (the
reload-specific BroadcastReload became a generic Broadcast). Built on
Net.Sockets. Private split helpers are prefixed to avoid colliding with a
consumer's same-named functions. Tests cover parsing, URL decoding, a loopback
request round-trip, and a WebSocket upgrade + broadcast (43 tests total).
2026-06-23 00:07:46 +01:00
Graeme Geldenhuys e52909576a stdlib: add Net.WebSockets (RFC 6455 framing + handshake)
Transport-agnostic WebSocket helpers: WebSocketAccept (handshake key),
EncodeFrame/EncodeTextFrame (server->client, unmasked, up to 2^31-1), and
DecodeFrame (parses one frame, unmasking masked client frames). Pairs with
Net.Sockets. Tests cover the RFC 6455 accept vector, encode length forms,
and masked/unmasked round-trips (38 tests total).
2026-06-22 23:52:38 +01:00
Graeme Geldenhuys 5cbc0613b5 stdlib: add Net.Sockets (IPv4 TCP) with tests
POSIX TCP sockets over libc FFI: raw bindings plus helpers (TcpListen/
TcpConnect, AcceptConn, RecvString, SendAll, MakeNonBlocking, IgnoreSigPipe)
and IPv4/ParseIPv4 address helpers. Tests cover address parsing and a
loopback round-trip (32 tests total).
2026-06-22 20:52:05 +01:00
Graeme Geldenhuys 304504886b feat(stdlib): add Security.Crypto (SHA-1) and Encoding.Base64
Two small, independent units, split along the lines Java and .NET draw:

  Security.Crypto   SHA-1 (raw 20-byte digest + Sha1Hex), the home for hash
                    digests (cf. java.security / System.Security.Cryptography).
  Encoding.Base64   Base64 encode + decode (RFC 4648), a general text encoding,
                    not crypto (cf. java.util.Base64 / System.Convert).

Callers compose them, e.g. the WebSocket handshake is
Base64Encode(Sha1(key + GUID)).  Both TStringBuilder-backed (O(n)).

Adds Crypto.Tests (SHA-1 FIPS vectors + the RFC 6455 handshake) and
Base64.Tests (RFC 4648 vectors, decode, round-trip) to the stdlib test tree
via Test.Registry; pasbuild test -m blaise-stdlib runs 27 tests.
2026-06-22 20:33:55 +01:00
Graeme Geldenhuys 36065ecd7d feat(stdlib): add JSON library (writer, DOM, parser, reader)
A read+write JSON library for the standard library:

  Json.Writer  streaming serialiser — TStringBuilder-backed (O(n)), with a
               three-tier .NET-style API: WriteString(name,value) for object
               fields, WriteStringValue(value) for array elements, and WriteKey
               for nested-container values. Compact + pretty output. Standalone
               (no DOM dependency).
  Json.Types   in-memory document model (DOM): TJSONData + typed nodes, tree
               building, accessors; emits through Json.Writer.
  Json.Parser  RFC 8259 recursive-descent parser into the DOM, with full string
               escapes incl. \uXXXX surrogate pairs.
  Json.Reader  thin GetJSON(text) facade.

Adds the first stdlib test tree under stdlib/src/test/pascal: a self-registering
Json.Tests suite (19 tests), a Test.Registry hub unit, and a TestRunner program,
wired into 'pasbuild test -m blaise-stdlib' via the <test> block in project.xml.
run-tests.sh builds and runs the suite directly.
2026-06-22 20:07:34 +01:00
Graeme Geldenhuys 1e92604599 feat(debug): per-unit OPDF so incremental builds are fully debuggable
Incremental compilation is the default, but each dependency unit was compiled
to its own .o with NO OPDF debug info — only the top --source unit got an
.opdf section.  pdr could therefore not set breakpoints, show callstacks, or
inspect locals inside any dependency (RTL, stdlib, or a user's own units),
which makes the default pipeline effectively undebuggable.

Mirror FPC's multi-unit OPDF model (which pdr already reads): every object —
each unit AND the program — carries a complete, self-contained .opdf section
[32-byte header][records], and the linker concatenates the same-named sections.
The header's TotalRecords is written as 0 (stream-terminated): readers consume
records to section EOF and skip the embedded per-unit headers (and linker zero
padding).  pdr needs NO changes — it already handles exactly this layout.

Layers:
- uDebugOPDF.pas: EmitHeader writes TotalRecords=0 (PatchTotalRecords now a
  no-op); new CreateForUnit + unit-mode DoEmit that emits a unit's types /
  globals / constants / function scopes / line info from its IntfBlock+ImplBlock
  and the unit's symbol table, with NO program main scope and NO unit directory
  (directory is program-only; pdr ignores it).  Whole-program output is
  unchanged apart from TotalRecords=0.  MangledClassSym matches the native
  backend's class-symbol naming so vtable labels resolve in unit mode.
- Blaise.pas: TCompileWorker.Execute appends a unit-mode OPDF section to each
  unit's IR when --debug-opdf and the native codegen produced debug facts, so
  the per-unit .o embeds its own .opdf.  (QBE has no facts → no per-unit OPDF;
  native is the debug backend.)
- blaise.assembler.x86_64.pas: accept `.section .opdf, "aw", @progbits`
  (new eskOpdf); `.int` alias for `.long`; `.byte`/`.word` now parse
  comma-separated value lists (needed for the OPDF magic + BuildID bytes).
- blaise.elfwriter.pas: emit the .opdf section as SHF_ALLOC|SHF_WRITE PROGBITS
  so it lands in the loadable image where pdr finds it; the internal linker's
  section merger already concatenates .opdf by name and resolves its
  .quad <funclabel> relocations.

Verified: pdr breaks inside a dependency unit, shows the cross-unit callstack,
and inspects locals — under BOTH the internal toolchain (default) and external
gas/ld, incremental AND --no-incremental.  Full suite OK (3742) on both build
paths; all four fixpoints pass (normal builds unaffected).  Regression test
TestDebugOpdf_PerUnitSection_InDependencyObject asserts the dependency .o
carries an .opdf section.
2026-06-22 19:43:35 +01:00
Graeme Geldenhuys 18b9cb273b fix(semantic): cached-.bif import places methods at their authoritative vtable slot
A class compiled against a CACHED base unit (loaded from target/units .bif)
built a wrong vtable: RegisterClassMethod added a slot only for methods flagged
virtual/override and assembled the table in .bif append order, assuming that
order matched the source slot order.  Two things broke that:
  - the .bif lists newly-introduced virtuals before inherited overrides, not in
    source declaration order; and
  - a non-virtual constructor named Create occupies an implicit vtable slot
    (metaclass dispatch through a class-of reference) that the importer skipped
    entirely.

So a descendant compiled against a cached base dropped the Create slot and
shifted every later virtual up by one.  Cls.Create(args) then dispatched into
whatever method fell into Create's old index (e.g. RunTest), the constructor
body never ran, fields stayed nil — which aborted the stdlib JSON test suite
when its TTestCase base came from target/units.

Fix: RegisterClassMethod now places each method carrying a vtable slot at its
authoritative index (ASig.VTableSlot) via the new
TRecordTypeDesc.SetVTableSlotAt, which grows the vtable with empty-named
placeholders as needed.  Order-independent; covers virtual, override, and the
implicit-slot constructor.  Also fixes a latent SetUp-override resolution
regression the append-order import caused.

Regression test TestIncrementalRebuild_VirtualCtorVtableSlot: a warm-cache
rebuild of a class with a non-virtual Create + a virtual method, instantiated
through a class-of reference, asserts the constructor body actually runs.
Both backends: stdlib cached build OK (19 tests); compiler suite OK (3741);
all four fixpoints pass on both build paths.
2026-06-22 18:52:28 +01:00
Graeme Geldenhuys b074bc003a fix(codegen): float field read via implicit Self + float-returning method call
Two float-expression gaps, both fixed on native and QBE:

1. Implicit-Self float field READ (Result := FFloat inside a method).
   - native: EmitExprToXmm0 had no implicit-Self field case, so the field fell
     through to the plain VarOperand path and emitted `movsd FFloat(%rip)` — a
     load from a global symbol named after the field (undefined in per-unit .o
     codegen).  Now loads from Self + field offset.  (The store path was
     already correct; only the read was missing it.)
   - qbe: the implicit-Self field read in EmitExpr hardcoded `loadl` for any
     non-word field, so a Double/Single field loaded with loadl into an l temp
     and the following `stored`/`ret d` rejected it.  Now uses the field type's
     load opcode (loadd/loads) with the matching result type.

2. Qualified float-returning method call as a float expression
   (d := Obj.GetF()).  native EmitExprToXmm0 had no TMethodCallExpr case and
   raised "unsupported float expression form"; now dispatches the method (the
   SysV ABI leaves the float result in %xmm0).

e2e test in cp.test.e2e.sepcompile compiles a unit with a Double field read via
implicit Self to a .o and runs a consumer over it (the per-unit path that
exposed the native symbol bug), printing the float method-call result on both
backends.  Full suite OK (3740); all four fixpoints pass on both build paths.
2026-06-22 18:26:23 +01:00
Graeme Geldenhuys 6d46af93de fix(codegen): native HasClassAttribute — emit the call and the attribute RTTI
HasClassAttribute(AClass, AAttrClass) was unreliable on the native backend:
- The call was never emitted.  The builtin had no native lowering, so it fell
  through to a generic path that left the first metaclass's typeinfo address in
  %rax and read its low byte as the Boolean result — a layout-dependent false
  positive (a plain class reported as having an attribute).
- Even with the call, the attribute table was missing: native typeinfo
  hardcoded slot 7 (attrs) to nil, so a [Threaded]-marked class reported False.

Fixes (blaise.codegen.native.x86_64.pas):
- Add a native lowering for the HasClassAttribute builtin (gated on
  FC.IsBuiltinHasClassAttr): evaluate both metaclass args to typeinfo pointers
  in %rdi/%rsi, call _HasClassAttribute, normalise %al to 0/1.  Mirrors the
  QBE backend.
- Emit the class attribute RTTI table (count + one typeinfo pointer per
  attribute) as attrs_<Class> and reference it from typeinfo slot 7, nil when
  the class has no attributes.  Mirrors the QBE backend's attrs_<Class>.

This is what blaise.testing's runner uses to decide [Threaded] subprocess
dispatch; the false positive forked non-threaded suites and crashed them.

e2e test in cp.test.e2e.gaps (AssertRTLRunsOnAll, both backends): a plain
TTestCase reports False, a [Threaded] one reports True.  Full suite OK (3739);
all four fixpoints pass on both build paths.
2026-06-22 18:05:32 +01:00
Graeme Geldenhuys 271763034b fix(codegen): implicit-Self virtual calls dispatch through the vtable
An unqualified call to a virtual method from within a method body (implicit
Self, Foo(args) with no Self. prefix) was emitted as a static direct call to
the declaring class instead of a vtable dispatch — so an override was silently
skipped (wrong polymorphism), and an abstract base method link-failed
(undefined symbol <Class>_<Method>).  Self.Foo(args) already dispatched
correctly; Self. must not change dispatch semantics.

The implicit-Self call paths never reached the `if VTableSlot >= 0` vtable
branch the explicit-Self paths use.  Fixed in both backends:

native (blaise.codegen.native.x86_64.pas):
- New EmitSelfDispatch / EmitSelfDispatchVia: vtable dispatch on Self (in %rdi
  or an explicit register) when the method is virtual, else a static callq.
  Wired into the implicit-Self expr call, statement (TProcCall) call (<=6 and
  >6-slot paths), interface-sret call, and record-sret method call.
- New EmitFuncCallSret routes an implicit-Self method TFuncCallExpr that
  returns a record/jumbo-set through EmitMethodSretCall (which passes Self and
  vtable-dispatches) instead of the free-function EmitSretCall, covering local,
  var-param, global, implicit-Self-field and explicit-field assignment targets.
- These implicit-Self arg-marshalling paths also adopted EmitPopMethodArgsToRegs
  / EmitMethodOverflowLoad, so float args to an implicit-Self call are now
  passed in xmm registers too (same class of bug as the prior overflow fix).

qbe (blaise.codegen.qbe.pas): the implicit-Self statement and expression call
emitters now resolve the call target via SretMethodCallTarget (a loaded vtable
function pointer for a virtual method, else the static $symbol), matching the
sret path that was already correct.

A non-virtual unqualified call still emits a static direct call (no needless
vtable).  Tests in cp.test.e2e.classes2 cover override dispatch, abstract-base
link+dispatch, non-virtual stays static, and interface- and record-returning
virtual implicit-Self calls, all on both backends.  Full suite OK (3738); all
four fixpoints pass on both build paths.
2026-06-22 16:22:23 +01:00
Graeme Geldenhuys 73e1a14399 fix(codegen): native float args correct in the >6-slot overflow ABI
Float arguments to a call with more than 6 register slots were mis-classified
on both the caller and callee sides of the native x86-64 SysV ABI, because the
overflow machinery counted every argument as one integer register slot. With
floats interspersed (which take xmm registers, not integer ones) this put
integer args at the wrong overflow address and crashed the callee — affecting
free functions (EmitCall) as well as method/constructor/inherited calls.

Caller side (blaise.codegen.native.x86_64.pas):
- EmitCall (free functions): the overflow tail assumed exactly the first six
  contiguous stack slots were register-bound. It now records the actual
  integer-overflow slot offsets during the register-load pass and relocates
  them to a 16-byte-aligned region at the top of the allocation, so %rsp is
  aligned at the call and the lowest-indexed overflow arg is at 0(%rsp).
- New EmitMethodOverflowLoad + OverflowArgIsFloat replace the integer-only
  store/load in all five method-family overflow paths (method expr/stmt,
  constructor expr/stmt, inherited). Float arg slots are stored as their xmm
  bit pattern and loaded into xmm0.., integers into the SysV integer registers
  (Self = reg 0), with float-skipping overflow relocation. The former
  GuardNoFloatInOverflow stopgap is removed.

Callee side (BuildFrame + prologue spill loop): a by-value float parameter now
consumes an xmm slot (separate XmmIdx2) instead of an integer slot, so it no
longer pushes a following integer parameter onto the stack while the caller
passes it in an integer register. The prologue float spill bound is corrected
from 6 to 8 (SysV has 8 xmm argument registers).

Tests: e2e overflow-with-float cases for free function (cp.test.e2e.native),
and method/constructor/inherited (cp.test.e2e.classes2), all on both backends.
Full suite OK (3733); all four fixpoints pass on both build paths.
2026-06-22 14:44:11 +01:00
Graeme Geldenhuys 9607ca388e fix(codegen): native passes float method/constructor args via xmm registers
The native x86-64 backend marshalled float arguments correctly for free
functions (SysV: floats -> %xmm0..7) but not for instance-method, constructor,
or inherited calls: those push every arg through the integer-register model, so
a Double/Single argument was either a codegen crash (literal -> "unsupported
expression form TFloatLiteral") or a silent ABI miscompile (variable bits
landed in an integer register while the callee read %xmm).

Fix, all in blaise.codegen.native.x86_64.pas:
- EmitMethodArgPush: a by-value float scalar arg is now materialised via
  EmitExprToXmm0 (handles literals through the .LF constant path), width-
  adjusted to the param type, and pushed as its 8-byte bit pattern.
- New EmitPopMethodArgsToRegs: replaces the integer-only `popq SysVArg64(I+1)`
  loops in the <=6-slot path of EmitMethodCallExpr, EmitMethodCallStmt, the
  constructor arg loop, EmitInheritedCallSeq, and the sret method path. It
  classifies each slot and routes floats to %xmm0.. and integers to the
  integer registers, advancing independent counters per SysV (Self still
  occupies integer slot 0/1).
- The rare >6-register-slot overflow path remains integer-only; a by-value
  float there now raises a clear "not yet supported" error (GuardNoFloatInOverflow)
  instead of crashing cryptically or silently miscompiling.

Tests: 6 e2e cases in cp.test.e2e.classes2 (method/ctor x literal/variable,
mixed int+float+string interleaving, multiple floats across xmm0/xmm1) run on
both backends, plus an IR assertion in cp.test.nativeconstarg that the call
site emits movsd into %xmm0.
2026-06-22 14:16:34 +01:00
Graeme Geldenhuys ef19c01cb7 fix(codegen): QBE stores float to implicit-Self field with stored/stores
Assigning a float value to a class field via an implicit Self reference
(e.g. `V := d` in a method/constructor where V: Double) emitted a hardcoded
`storel` on a `d`/`s` SSA value, which QBE rejects ("invalid type for first
operand in storel"). The implicit-Self field-store path in EmitAssignment
fell through to a literal `storel` instead of StoreInstrFor(field type), and
lacked the d<->s width coercion the EmitFieldAssignment path already has.

Use StoreInstrFor(ISFld.TypeDesc) (-> stored/stores for tyDouble/tySingle) and
add the Single<->Double truncd/exts coercion before the store. Surfaced by the
new float-constructor e2e tests, which run on both backends.
2026-06-22 14:16:21 +01:00
Graeme Geldenhuys 74da3eadcc test(bif-coverage): extend drift guard to .bif interface types
bif-coverage verified that every AST node field round-trips through the .bif
encoder/decoder, but the .bif interface-container types (TRoutineSig,
TUnitInterface, TMethodParam, TConstEntry, TVarEntry) were hand-serialised in
WriteMeta/EncodeMethodSig/etc. with no drift guard. Every cached-rebuild bug
just fixed was a serialised field on one of those types dropped from one side
of the round-trip — invisible to the tool, surfacing only as a runtime
miscompile.

Generalise the class scanner to ScanClassFile(path, names, objs, allowList);
add ScanInterfaceTypes() over an allow-list of the container types in
uUnitInterface.pas. Split uUnitInterfaceIO.pas into encoder-side and
decoder-side text and assert each serialised interface-type field's identifier
appears in both — the same looseness as the AST mechanism. Status file gains
the interface-type entries (serialise/safe); mutator-repopulated owning
collections are { no-bif }-exempt (their element data round-trips through the
per-entry encoders).

Negative test confirmed: dropping ImplUsedUnits/HasInitialization/VTableSlot
from either side is now reported as a gap with exit 1. Clean tree exits 0.
2026-06-22 12:38:28 +01:00
Graeme Geldenhuys 2f6ee4588a test(fixpoint): add warm-cache rebuild guard
None of the existing fixpoint scripts exercise the incremental warm-cache
rebuild path: fixpoint.sh uses --emit-ir (no cache), fixpoint-native.sh diffs
.s (no cache), fixpoint-native-internal.sh compiles a tiny program. So a
regression in the cached-interface import/round-trip (dropped field, wrong
vtable slot, duplicate global, missing init) passes all of them and only
surfaces later as a corrupt binary — exactly the class of bug just fixed.

scripts/fixpoint-warmcache.sh builds the whole compiler twice into the same
--unit-cache (clean then warm), asserts the warm rebuild is non-empty and not
drastically smaller than the clean build, and compiles+runs hello-world with
both stage-2 binaries to prove the warm-cache-rebuilt compiler is correct, not
just present. Prints WARMCACHE_FIXPOINT_OK on success.
2026-06-22 12:24:41 +01:00
Graeme Geldenhuys 915077d7c8 fix(incremental): make cached-interface rebuild lossless and coherent
Incremental compilation (per-unit .o + embedded .bif, the default) produced
silently corrupt binaries when rebuilding into a populated --unit-cache: the
clean build worked, but any rebuild that loaded a unit from its cached .bif
instead of source dropped or mishandled state the in-memory path carried.
Root cause is uniform across every symptom: the .bif round-trip was not a
complete projection of the unit interface, and the cached-load path in the
loader/driver diverged from the source-load path.

.bif format bumped to version 3 (IFACE_VERSION; CompilerId +bif tag) so stale
caches are cleanly rejected and recompiled from source.

Fixes (each was a field/decision present on the source path, absent on the
cached path):

- Loader: a unit's implementation-section `uses` were never recorded in the
  .bif, so on rebuild the impl-only dependency's object was dropped from the
  link (undefined symbol). Record ImplUsedUnits in the .bif and collect those
  objects for LINK ONLY (CollectLinkOnlyObject) — not semantic import, which
  would break the leaf-first iface import on interface/impl-use cycles.

- Native codegen: a global var owned by a cached unit was re-defined by the
  recompiled top program (`multiple definition of GTarget`). The program now
  references imported-unit globals as external (FImportedUnits / IsImportedGlobal
  gate in EmitDataSection); NoteDepInitUnit records every imported unit.

- Unit initialization: <Unit>_init calls for cached deps were never emitted, so
  initialization sections silently never ran. Record HasInitialization in the
  .bif and note prebuilt-iface inits in dependency order at program codegen.

- SynthesiseMethodDecl dropped VTableSlot/IsVirtual/IsOverride from cached
  method imports, degrading virtual dispatch to direct calls into the abstract
  base (SIGABRT at run time). Propagate them.

- Generic-instance field/param types now resolve via the analyser's generic-
  aware resolver on import (ResolveImportedTypeName); generic instantiation
  during import no longer nil-derefs FProg/FCurrentUnit (pending-instance flush).

- .bif round-trip completeness: serialise block-local var decls, Exit(value)
  return values, parameter default values, free-routine ResolvedQbeName,
  generic-class template properties, and fix implements-interface name
  stripping (strip to the last dot, not the first).

Adds TestIncrementalRebuild_ImplOnlyUses_LinksDependency (e2e) and updates the
magic/version assertion. Full suite OK (3722 tests); QBE, native, and
internal-assembler fixpoints pass; clean and warm-cache rebuilds both produce
correct binaries.
2026-06-22 12:23:28 +01:00
Graeme Geldenhuys baf5079d6d fix(parser,codegen): Self.Field[Index].SubField := ... parse and codegen
The parser's Ident.Ident[Index] path unconditionally expected ']:=' after
the subscript, rejecting chains like Self.FStack[0].Count := value. Add a
chain loop after ']' that handles '.Field :=', '.Method()', and further
'[Index]' suffixes.

Additionally, the QBE backend's EmitInstancePtr did not handle
TFieldAccessExpr nodes with IsArrayAccess set — it returned the field slot
address instead of the dereferenced array element pointer. Delegate to
EmitExpr when IsArrayAccess is true so the dynarray pointer is loaded and
the element offset is computed correctly.
2026-06-22 01:38:27 +01:00
Graeme Geldenhuys 57c1699d32 fix(test): prefer fp_blaise2 over fp_blaise3 for internal-asm e2e tests
fp_blaise3 predates the movq %xmm0,%rax encoding fix, so
TestFormatFloatArg always failed when fp_blaise3 was present.
2026-06-22 00:15:07 +01:00
Graeme Geldenhuys 05892561fe perf(linker): fix O(n²) string concatenation in internal linker (80s → 3s)
LkZeros built zero-padding strings by appending one byte at a time
(Result := Result + Chr(0)), which for a ~2.7 MB executable produced
trillions of byte copies. Replace with SetLength + PChar fill.

Also fix LkLE to pre-allocate via SetLength instead of concatenating
per-byte, and add an early-exit guard to LkCopyInto.
2026-06-21 23:53:36 +01:00
Andrew Haines 9d876b46aa feat(types): resolve qualified type names UnitName.TypeName
A type written as 'UnitName.TypeName' (the unit qualifier may itself be
dotted, e.g. System.SysUtils.TFormatSettings) was a parse error: after
reading the leading identifier ParseTypeName only accepted a generic
'<...>' argument list, not a '.' continuation.

Parse the full dotted path into the type name, then resolve it by its
final component — a type is always a single identifier; the leading
components name a unit already loaded by the 'uses' clause. The strip is
centralised in TSymbolTable.FindType (via UnitQualifierTail) so every
type position benefits uniformly — var/field decls, parameters, and the
several routine return-type sites — and FindTypeOrInstantiate strips the
qualifier off a generic instance (Unit.TList<Integer>) before
instantiation. Plain dotted identifier paths only: the bracket/space/
caret/angle encodings for array, pointer, set and generic types are left
untouched.

Tested: dotted-name capture in the parser, semantic resolution of a
qualified builtin (System.Integer), and end-to-end compile+run of a
program using a cross-unit qualified type.

(cherry picked from commit 7e9c3d1f61d42769bc3341195220a25190637e15)
2026-06-21 13:35:37 +01:00
Graeme Geldenhuys cf994dcca4 fix(parser): parse nested-proc-first bodies in unit impl routines (Andrew 82e770ac)
A unit implementation-section routine whose body opens with a nested
procedure/function declaration (before any var/const/type section) was
mis-parsed: ParseUnit called ParseMethodDecl without ACanHaveNestedProcs, so a
leading 'procedure'/'function' was treated as the next top-level decl and
parsing failed at the outer 'begin' ("Expected 'end' but got 'begin'").

Pass ACanHaveNestedProcs=True for the two impl-section ParseMethodDecl calls.
The IsForward guard already present in ParseMethodDecl (our commit 33db0f6)
keeps a forward decl from claiming the following sibling as its nested body, so
only the call-site change was needed — Andrew's branch independently added the
same forward-flag mechanism, which we already have.

Note: this is a PARSER fix (the unit body now parses correctly).  Resolving a
CALL to such a nested procedure is a separate, pre-existing semantic-pass gap
that affects program routines too (Inner() reports "Cannot find declaration");
it is out of scope here.  Andrew's tests are likewise parse-level.

Tests: cp.test.units.pas parse-level tests (outer routine is one decl with a
body holding the nested proc).

Cherry-pick policy: the forward-flag half was a duplicate of our work and
skipped; the ACanHaveNestedProcs threading + tests are taken.

Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite green
on both the QBE-built and native-built runners (3717 tests).
2026-06-21 13:35:37 +01:00
Graeme Geldenhuys 54cfe37e62 fix(pasbuild): fix the Release and Debug profiles to use Blaise compiler settings 2026-06-21 13:31:58 +01:00
Graeme Geldenhuys 3cf5333b16 fix(stdlib): make TStringList.Strings the default array property 2026-06-21 13:31:16 +01:00
Graeme Geldenhuys d91d9ee65a feat(lexer): conditional compilation with predefined BLAISE (issue #131)
Implements real symbol-presence conditional compilation in the lexer.
Previously {$IFDEF} was hardcoded false (always took {$ELSE}) and
{$DEFINE}/{$UNDEF} were silently consumed, with no define table and no
command-line flag.

- A case-insensitive define table on TLexer.  {$DEFINE sym} / {$UNDEF sym}
  add and remove symbols; {$IFDEF sym} keeps its body when defined (and
  {$IFNDEF sym} when not), with an optional {$ELSE} and a closing {$ENDIF}.
  IFDEF/IFNDEF blocks nest (the existing depth-tracking skip helpers are
  reused, now driven by the real truth value).
- Predefined symbols, seeded in every lexer: BLAISE (the headline
  cross-compiler use case — {$IFDEF BLAISE} ... {$ELSE} ... {$ENDIF}) plus
  the target CPU/OS symbols CPUX86_64, CPUAMD64, LINUX, UNIX.  No version
  macro yet.
- A -d / --define <sym> command-line flag (FPC -dSYM / Delphi -D), carried
  on TFrontEndOpts and threaded to the program's lexer AND to every unit the
  TUnitLoader compiles, so {$IFDEF} resolves consistently across the program
  and its units.

Tests:
- cp.test.lexer.pas: predefined BLAISE keeps the body, undefined takes ELSE,
  IFNDEF, DEFINE-then-IFDEF, UNDEF-then-IFDEF, and AddDefine (the -d path).
- cp.test.e2e.misc.pas (dual-backend): the {$IFDEF BLAISE} cross-compiler
  pattern, and a combined DEFINE/UNDEF/IFNDEF/CPU-OS/nested program.

Docs: grammar.ebnf documents the directives and predefines; language-
rationale.adoc records the decision (symbol-presence only, no {$IF} expr
form yet; BLAISE + CPU/OS predefined, no version macro) and alternatives.

Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite
green on both the QBE-built and native-built runners (3715 tests).
2026-06-20 20:16:16 +01:00
Graeme Geldenhuys 2a6d0c5912 fix(assembler): encode movq/movd between an XMM and a GP register (SSE MOVQ)
The internal assembler routed every `movq`/`movd` to the integer mov encoder,
which reads both operands as general-purpose registers.  So `movq %xmm0, %rax`
was emitted as `mov %rax,%rax` (48 89 C0) — register 0 in both fields — silently
discarding the XMM value.

The native backend emits `movq %xmm0, %rax` to box a Double's raw bits into an
array-of-const (the Format() argument path) and to spill float bit patterns, so
ANY float passed to Format() under the internal assembler (now the default
toolchain) arrived as garbage and printed 0.0000.  A regression vs v0.11.0,
which used the external assembler.  The e2e test harness assembles native via
cc, so only the internal-assembler suite exercises this path.

Fix: `movq`/`movd` with an XMM operand is an SSE move and is routed to a new
EncodeMovqXmm:
  gp/mem -> xmm :  66 [REX.W] 0F 6E /r
  xmm -> gp/mem :  66 [REX.W] 0F 7E /r
with the XMM operand as the ModRM.reg field and REX.W set for movq (64-bit),
clear for movd (32-bit).

Tests:
- cp.test.assembler.pas TAsmEncodingTests: byte-level encoding of
  `movq %xmm0,%rax` (66 48 0F 7E C0, and asserts it is NOT 48 89 C0) and
  `movq %rax,%xmm0` (66 48 0F 6E C0).
- TInternalAsmE2ETests.TestFormatFloatArg: Format('%.4f',[1.25]) through the
  internal assembler prints 1.2500.

Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite green
on both the QBE-built and native-built runners (3707 tests).
2026-06-20 19:09:56 +01:00
Graeme Geldenhuys c5d49ba03e fix: 64-bit integer constants — no truncation, accept full bit pattern (issue #133)
Two related defects in integer constant handling:

- A large hex literal that needs more than 32 bits (e.g. $080808080808 =
  8830587504648) was silently TRUNCATED to 32 bits (printed 134744072).  An
  untyped integer constant was unconditionally typed as Integer regardless of
  its magnitude, so codegen treated it as a 32-bit word.

- A literal above High(Int64) (e.g. $8080808080808080) was REJECTED at parse
  time with "Integer literal exceeds Int64 range", even though it is a valid
  64-bit bit pattern and is accepted by FPC/Delphi.

Fixes:

- uParser: the bare-integer-literal const path now uses ParseIntOrUInt64Literal
  (which already existed for the tolerant case) instead of ParseIntLiteral, so a
  value in the UInt64 range is captured as a bit pattern and flagged via a new
  TConstDecl.IsUInt64.

- uSemantic (AnalyseConstDecls): an untyped integer constant now picks its type
  by magnitude — Integer when it fits in signed 32-bit, Int64 when larger, and
  UInt64 when the value exceeds High(Int64) (matching Delphi, which prints
  $8080808080808080 as 9259542123273814144).  A typed const (e.g. A: Int64 =
  $8080808080808080) keeps its declared type and stores the bit pattern, so it
  prints -9187201950435737472.

Tests (cp.test.e2e.misc.pas, dual-backend): large hex not truncated, typed
Int64 bit pattern, typed UInt64 bit pattern, and untyped-above-Int64 widening
to UInt64.

Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite green
on both the QBE-built and native-built runners (3704 tests).
2026-06-20 18:30:12 +01:00
Graeme Geldenhuys fe5460f79b fix: var open-array parameter element read and write (issue #130 bug5)
A `var` open-array parameter was broken three ways; a `const` open array with
the same body worked, so the defects were specific to the var/open-array path.

- Semantic (uSemantic, AnalyseStaticSubscriptAssign): writing an element of an
  open-array parameter raised "'a' is not a static array or dynamic array" —
  there was no tyOpenArray case.  Added one: a var open array allows element
  writes; a const open array is rejected with a clear "declare it 'var'"
  message.

- QBE codegen (element READ): an open-array ident value-read fell into the
  scalar var-param branch, which dereferences the slot twice.  An open-array
  param slot already holds the data pointer directly (for both const and var),
  so the second load read garbage and segfaulted.  Added a tyOpenArray case
  that loads the data pointer once.

- Native codegen (element WRITE): tyOpenArray fell through to the static-array
  store and raised "static subscript assign on non-static-array".  Open arrays
  now share the dynamic-array element-write path, with the var-param extra
  deref suppressed (the open-array slot is already the data pointer).

Tests:
- cp.test.e2e.openarray.pas (dual-backend): element read (the issue repro),
  element write mutating caller storage, and read-modify-write mixing var and
  const open arrays.
- cp.test.semantic.pas: var open-array element write is accepted; const
  open-array element write is rejected.

Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite green
on both the QBE-built and native-built runners (3700 tests).
2026-06-20 17:29:47 +01:00
Graeme Geldenhuys 56e73c8ce4 fix: class recognised as implementing an interface inherited from an ancestor (issue #130 bug3)
A class that inherits an interface from its ancestor (the ancestor declares
the interface; the descendant does not re-list it) was not recognised as
implementing it.  `g := TLoud.Create` where TLoud < TPerson(IGreeter) failed
with "Type mismatch", and after the semantic gate was opened the codegen
referenced a non-existent itab.

Three coordinated fixes:

- uSemantic (CheckTypesMatch): the class->interface compatibility check
  scanned only the class's own ImplementsList.  It now walks the whole class
  parent chain, so a descendant inherits its ancestors' interface
  implementations.

- blaise.codegen.qbe / blaise.codegen.native (EmitInterfaceDefs): itab/impllist
  generation likewise only considered a class's own ImplementsList, so no itab
  was emitted for an inherited interface.  Both backends now:
    * skip a class only when neither it nor any ancestor implements an
      interface (and, on native, point the typeinfo at the impllist on the same
      condition);
    * collect implemented interfaces by walking the class parent chain;
    * resolve each itab method ref via the vtable slot's ImplName, so an
      overridden method points at the descendant's body and an inherited
      (non-overridden) method at the ancestor's — uniformly correct across
      arbitrary inheritance depth.

Tests (cp.test.e2e.interfaces.pas, dual-backend via AssertRunsOnAll): the
exact issue repro (descendant assigned to an interface var, dispatching to its
override), and a three-level chain through an interface parameter mixing an
overridden method with an inherited non-overridden one.

Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite green
on both the QBE-built and native-built runners (3695 tests).
2026-06-20 16:56:33 +01:00
Graeme Geldenhuys 33db0f699e fix(parser): forward; routine in a program declaration section (issue #130 bug2)
A `forward;`-declared routine in a program's declaration section swallowed the
following separate implementation (and everything up to the program's `end.`)
as its own body, producing "Expected ';' but got '.'".

Root cause: in ParseMethodDecl, when ACanHaveNestedProcs is true (the
standalone-proc context used for program/unit blocks), a bare
`procedure`/`function` keyword after the signature triggers body parsing so a
nested sub-routine is absorbed into the enclosing routine.  A forward decl has
no body, so the keyword that follows it is the SEPARATE implementation — but
the body-trigger fired anyway and consumed it.

Fix: track a local IsForward flag (set when the `forward` directive is seen)
and suppress the body-parse trigger for it, exactly as IsExternal already does.
Mutually-recursive routines declared with forward now parse and run.

Tests: a parser test asserts the forward decl and its implementation are two
separate ProcDecls (forward has no body, impl has one); a dual-backend e2e
test runs mutually-recursive IsEven/IsOdd and checks the computed parity.

Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite green
on both the QBE-built and native-built runners (3693 tests).
2026-06-20 16:20:01 +01:00
Graeme Geldenhuys 8c68b09245 fix(parser): accept named integer subrange types (issue #130 bug1)
`type TByte = 0..255;` failed to parse — ParseTypeDecl had no case for an
integer-literal subrange, so the RHS fell through to the generic "expected
record/class/..." error.

Blaise does not range-check, so a named subrange is treated as an alias to
the narrowest STANDARD integer type that holds both bounds (0..255 -> Byte,
-10..10 -> SmallInt, etc.).  This keeps record/array element layout correct
(TByte is byte-sized) while the value behaves as an ordinary integer.  Two
parser helpers do the work: SubrangeAhead (lookahead: IntLit.. or -IntLit..)
and ParseIntegerSubrangeBaseType (parse lo..hi, pick the base type, reject a
descending range).  Note Blaise has no 8-bit signed alias, so a signed
subrange that would fit in ShortInt widens to SmallInt.

Only integer-literal bounds form a named type; identifier/enum-bounded
subranges (TLow..THigh, red..blue) are intentionally not handled here (they
are ambiguous as a named-type form).

Tests: parser tests (named subrange, negative bounds, descending-is-error)
and dual-backend e2e tests (named subrange runs; subrange as a record field
and array element with a negative range).  grammar.ebnf gains the
IntSubrange rule.

Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite
green on both the QBE-built and native-built runners (3691 tests).
2026-06-20 15:56:18 +01:00
Graeme Geldenhuys 3e94044550 ci(bootstrap): add branch-scoped validate-only pre-flight runs
The rolling-bootstrap workflow only triggered on master, so a risky change
could not be proven against the bootstrap chain before merging — and a bad
commit on master cannot be removed without a force-push on a public repo.

Trigger the workflow on push to ci/**, feature/** and bootstrap/** branches
(plus master and the existing workflow_dispatch).  Branch runs are
VALIDATE-ONLY:

- The carry-forward cache (stage + save steps) is written ONLY on
  refs/heads/master, so a branch never overwrites master's resume anchor and
  a broken branch's -pre binary can never leak into a later master run.
- Branch runs still restore the cache read-only, so a branch off recent
  master resumes from master's latest good -pre and replays only its own new
  commits; a diverged branch falls back to the cold-start anchor as before.
- The -pre artifact upload still runs on branches (keyed by commit SHA, so it
  never collides with master's and is pull-only).

concurrency is already keyed on github.ref, so per-branch runs don't cancel
each other or master.

Note: this only takes effect once pushed, so like the other Phase 4 CI
changes it cannot be exercised until the v0.12.0 cut.
2026-06-20 15:20:32 +01:00
Graeme Geldenhuys 909684c57f chore: remove stale info from code comment. 2026-06-20 15:09:09 +01:00
Graeme Geldenhuys 008cc12785 feat(stdlib): configurable rounding for TMoney
TMoney previously hard-wired banker's rounding (rmHalfEven) at its single
normalisation point.  The underlying TDecimal already supports all eight
TRoundingMode values plus custom IRoundingStrategy, so expose that through
TMoney: every constructor and rounding-sensitive operation gains overloads
taking an explicit TRoundingMode or an IRoundingStrategy.

  MoneyFromStr('1.005','USD')            -> 1.00  (banker's, unchanged default)
  MoneyFromStr('1.005','USD', rmHalfUp)  -> 1.01
  C := A.Add(B, rmCeiling);
  M := MoneyFromStr('9.999','USD', myCashRoundingStrategy);

Overloaded: MoneyFromStr, MoneyFromDecimal, Add, Subtract, Multiply (each
gains a TRoundingMode form and an IRoundingStrategy form).  MoneyFromInt,
MoneyZero, MultiplyInt and Negate are exact (no fraction introduced) and
need no mode.  The existing no-mode calls are unchanged and keep banker's
rounding, so this is fully backward-compatible.

Internally a single MakeMoneyS(strategy) core does the normalisation; the
mode form delegates via StandardRounding(Mode) and the default via
rmHalfEven — one rounding code path.

Tests: 5 IR/semantic tests and 8 dual-backend e2e tests covering the mode
overloads (HalfUp/Down/Ceiling), the custom-strategy overload, the
default-stays-banker's guarantee, and rounding on Add/Multiply/constructors.

Docs: language-rationale.adoc updated to record that rounding is swappable
per call (default banker's), not hard-wired.

Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite
green on both the QBE-built and native-built runners (3686 tests).
2026-06-20 13:38:40 +01:00
Graeme Geldenhuys 77aa15a8d8 fix(native): interface argument to a record-returning free function
EmitSretCall (the sret path for free-function record returns) pushed one
stack slot per LOGICAL argument, so an interface argument — a fat pointer
that occupies TWO integer-register slots (obj + itab) — had its second slot
dropped.  The arguments landed in the wrong registers and the stack was
left unbalanced, crashing.  This is the free-function sibling of the method
fix in 077f13b (EmitMethodSretCall).

Fix: in EmitSretCall's <=5-slot register path, push interface args as two
slots (obj then itab, via EmitMethodArgPush for the direct case and the
saved fat pointer for the hoisted akIntfConsume case), track bytes pushed so
the hoisted-value reloads stay correct, and pop one register per SLOT.  The
register-vs-spill guard now uses a new SretUserSlots helper that counts
interface params as two.  The >5-slot overflow path raises a precise error
for the (not-yet-supported, untested) interface-in-overflow combination
rather than silently mis-placing registers.

Surfaced by Numerics.Money's rounding overloads: MoneyFromStr(amount,
currency, IRoundingStrategy) is a free function returning a record (TMoney)
with an interface argument, which hit this path.

Regression tests in cp.test.e2e.recordret.pas cover a free function
returning an sret record with an interface arg, with and without a leading
scalar arg, via AssertRunsOnAll (QBE + native parity).

Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite
green on both the QBE-built and native-built runners (3686 tests).
2026-06-20 13:38:20 +01:00
Graeme Geldenhuys 36cad37423 feat(stdlib): add Numerics.Money — currency-aware TMoney
TMoney is a thin value-type wrapper layering an ISO-4217 currency tag onto
an exact TDecimal amount.  The numeric core (TDecimal) stays currency- and
locale-agnostic; currency policy lives entirely in this wrapper, matching
Moneta / money-gem / rusty-money.

Design:
- Currency is an upper-cased ISO-4217 string code; the set is open (unknown
  codes are accepted at the fallback minor-unit scale of 2).
- A built-in registry gives each currency its default scale (JPY 0, USD 2,
  KWD 3, fallback 2); every TMoney is normalised to its currency's scale on
  construction and after every operation, using banker's rounding.
- Cross-currency Add/Subtract/Compare raise EMoneyMismatch (no implicit
  conversion); Equals is total (False, not raise, across currencies).
- Immutable value semantics, mirroring TDecimal.

API: free-function constructors (MoneyFromStr / MoneyFromDecimal /
MoneyFromInt / MoneyZero), record methods (Amount, CurrencyCode, IsZero,
Sign, Add, Subtract, Negate, Multiply, MultiplyInt, Compare, Equals,
AmountString, ToString), and the CurrencyScale registry function.

Tests:
- cp.test.numerics.money.pas: 22 IR/semantic tests (resolution, IR shape,
  type errors) via TUnitLoader.
- cp.test.e2e.numerics.money.pas: 24 dual-backend e2e tests
  (CompileAndRunWithRTL) covering construction + per-currency normalisation,
  banker's rounding, case-folding, arithmetic, mismatch raising,
  Compare/Equals/IsZero/Sign, the registry, and a realistic invoice flow.

Docs: language-rationale.adoc gains "Currency Amounts — TMoney Wraps
TDecimal, Currency Is a String Tag" (decision + alternatives);
future-improvements.adoc marks Numerics.Money as implemented.

Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite
green on both the QBE-built and native-built runners (3671 tests).
2026-06-20 10:33:35 +01:00
Graeme Geldenhuys 62331de986 fix(qbe): record copy must use field-width loads/stores
EmitRecordCopy copied every non-managed scalar field with loadw/storew (or
loadl/storel), ignoring the field's actual width.  A sub-word field —
Boolean or Byte (1 byte), SmallInt or Word (2 bytes) — was therefore
stored with a 4-byte storew that overran into the following field.  When a
Boolean sat immediately before a managed (string / class / dynarray) field,
the wide store clobbered the low bytes of that pointer; the subsequent
_StringRelease / _DynArrayRelease then dereferenced a garbage address and
crashed.

This is exactly TDecimal's layout (FNegative/FInflated Booleans ahead of a
carrier with a managed field), so it blocked Numerics.Money on QBE:
MakeMoney rounds into a TDecimal local and stores it into TMoney.FAmount,
copying the record by value.  The native backend was always width-correct.

Fix: route scalar field copies through LoadInstrFor/StoreInstrFor (the same
width-correct helpers already used for element and variable loads), and copy
inline aggregate fields (static array / jumbo-set bitmap) with memcpy.

Regression test in cp.test.e2e.records.pas reproduces the TDecimal-shaped
layout (Boolean flags before a string field, copied through an sret Result
field) and asserts QBE/native parity via AssertRunsOnAll.

Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite
green on both the QBE-built and native-built runners (3625 tests).
2026-06-20 09:34:19 +01:00
Graeme Geldenhuys 077f13b669 fix(native): interface argument to a record-returning method
A record-returning method that takes an interface parameter crashed (sret
return) or produced garbage (register return) on the native backend.  An
interface is a fat pointer (obj + itab) occupying TWO integer-register
slots, but EmitMethodSretCall popped one register per LOGICAL argument
instead of per ABI slot.  The interface's second slot was never consumed:
the user arguments landed in the wrong registers, and in the sret case a
stack slot was left unpopped, unbalancing the frame and corrupting return
addresses (the observed 0x1 / 0xffffffffffffffff backtrace).

Fix: in both EmitMethodSretCall register-passing paths (register-return
record and sret-return record, <=4-arg form), pop CountArgSlots(Params)
registers — counting interface params as two — into the integer registers
after sret/Self, rather than ACall.Args.Count.  The <=4 register-path
guard is likewise changed to gate on slot count (UserSlots + 2 <= 6), so
an interface arg cannot silently overflow the register file.

This is the root cause of the TDecimal.Divide / RoundTo / SetScale native
crash: each takes a `const IRoundingStrategy` (interface) and returns a
record.  With the fix, the full division/rounding battery — all eight
rounding modes, banker's rounding, custom IRoundingStrategy, rmUnnecessary,
the arbitrary-precision inflated path, and div-by-zero — runs identically
on native and QBE.

Tests:
- cp.test.e2e.recordret.pas: three new regression tests for an interface
  argument to a record-returning method (sret return, register return,
  and a scalar-before-interface ordering check), via AssertRunsOnAll.
- cp.test.e2e.numerics.decimal.pas: the 14 Divide/RoundTo/SetScale/float
  e2e tests that were CompileAndRunWithRTLQBEOnly now run dual-backend
  (CompileAndRunWithRTL), giving native parity coverage for the whole
  decimal arithmetic surface.

Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite
green on both the QBE-built and native-built test runners (3624 tests).
2026-06-20 03:57:08 +01:00
Graeme Geldenhuys 06a6bc0d1e fix(native): method call on a transient record-returning result
A method call whose receiver is itself a record-returning call —
A.Plus(B).Val() — crashed on the native backend.  The receiver
expression was lowered with EmitExprToEax, leaving the record VALUE
(the register-return payload, or the first bytes of the sret buffer) in
%rax, which was then used directly as the Self POINTER.  Dereferencing
that garbage segfaulted.  QBE handled the chained form correctly, so the
dual-backend e2e harness flagged it as a native/QBE parity divergence.

Fix: when a record method's receiver is a record-returning call,
materialise the call result into a stack buffer and pass its ADDRESS as
Self.  The buffer address is carried in callee-saved %rbx so it survives
the argument push/pop sequence; both are freed after the call.  Applied
to both EmitMethodCallExpr paths (<=6 and >6 arg slots) and all four
receiver sites in EmitMethodSretCall.

For the sret path, a forwarded %rsp-relative destination (the nested
chain A.Plus(B).Plus(A), where EmitRecordCallSretAt forwards '(%rsp)')
is resolved to an absolute pointer in callee-saved %r14 BEFORE the
receiver-materialisation prologue moves %rsp, so the destination no
longer drifts.

Regression tests in cp.test.e2e.recordret.pas cover the register-return
chain, the sret-record chain, the >5-arg overflow path, the nested
double chain, and the managed-field (TDecimal-shaped) record — all via
AssertRunsOnAll (QBE + native parity).

Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full
suite green on both the QBE-built and native-built test runners
(3621 tests).
2026-06-20 03:25:31 +01:00
Graeme Geldenhuys 718ee45e20 feat(stdlib): add Numerics.Decimal — exact decimal (TDecimal)
A single exact base-10 decimal type for financial / exact-decimal use,
replacing the historical Currency/Comp/Extended proliferation with one type.

  - Construction: DecFromInt/Int64/Str, plus a safe/exact float split
    (DecFromFloat takes the shortest decimal — 0.1 stays 0.1 — while the
    dangerous binary-exact path is the explicitly-named DecFromFloatExact).
  - Value semantics + value-based equality: 2.0 = 2.00 with a consistent hash,
    so it is safe as a dictionary key (unlike Java BigDecimal).
  - Arithmetic: Add/Subtract (scale = max), Multiply (scale = sum), Negate, Abs,
    arbitrary precision via a decimal-digit magnitude (compact Int64 fast path
    inflating on overflow).
  - Division + rounding: a layered design — a TRoundingMode enum (8 modes,
    banker's default) over an IRoundingStrategy interface users can implement
    for custom rounding.  Division always carries an explicit scale + mode.
  - Formatting: ToString/ToPlainString never use scientific notation;
    StripTrailingZeros keeps integer zeros (600 stays 600, never 6E+2).
  - Conversions out: ToDouble (lossy), ToInt64 (truncating).

IR tests (32) + e2e tests (46).  Add/Subtract/Multiply and the value-semantics
run dual-backend; Divide/RoundTo and float conversion run QBE-only pending
native codegen fixes (logic verified on QBE; see bugs.txt).
2026-06-20 02:10:12 +01:00
Graeme Geldenhuys 13a8a39af0 fix(compiler): incremental unit-mode crash (nil Prog.SymbolTable)
Compiling a UNIT (top source is a `unit`, so the pipeline runs in unit-mode and
Prog stays nil) via the default incremental path segfaulted: the incremental
worker setup unconditionally read `Prog.SymbolTable` to seed each dep worker,
dereferencing nil.  In unit-mode the symbol table comes from the semantic pass,
not a program node.  This is exactly the invocation the runtime Makefile drives
(`blaise --source X.pas --output X.o` per RTL unit), so it broke `make` in
runtime/ once incremental became the default; it was a latent pre-existing bug
in the opt-in incremental path (reproduces at de0ea5d with --incremental).

Fix: seed the worker symbol table from Semantic.GetSymbolTable() when Prog is
nil, matching the non-incremental unit-mode path.

Also pin the runtime Makefile to --no-incremental.  That Makefile is itself a
hand-managed separate-compilation system (one object per unit + an explicit
archive list); the compiler's incremental mode would additionally write
per-dependency side-effect objects and skip inlining a used unit's bodies, but
those side-effect objects are not in the archive list — so a cross-unit symbol
(e.g. typeinfo_TRtlPlatform, referenced by the derived TRtlPlatformPosix in a
different unit) was left undefined at link.  Whole-program per unit keeps each
unit object self-contained.

Regression test (cp.test.e2e.sepcompile.pas): compile a unit that uses another
unit in unit-mode via the default incremental path, then build and run a
program over the emitted object.
2026-06-20 02:04:58 +01:00
Graeme Geldenhuys 47ca10d7d3 fix(native): float arguments on the managed-record sret call path
A float (Double/Single) by-value argument passed to a function or method that
returns a record with a managed field (dynamic array / string, i.e. the true
sret return path) was mis-handled by the native backend: every argument was
materialised via EmitExprToEax, the integer-only emitter.  A float LITERAL hit
"unsupported expression form TFloatLiteral", and a float VARIABLE would have
been routed through %rax into an integer argument register instead of an %xmm
register — wrong per the SysV ABI.  QBE compiled these correctly, so it was a
native/QBE parity divergence.

This blocked DecFromFloat / DecFromFloatExact (TDecimal has a dynamic-array
field) and any MoneyFromFloat-style constructor in Numerics.Money.

Fix: both EmitSretCall (free functions) and EmitMethodSretCall (methods) now
detect a float-typed by-value argument and, when present, evaluate arguments
into stack slots and load them into registers per the SysV ABI — integer args
into the integer argument registers (after %rdi=sret and, for methods,
%rsi=Self), float args into %xmm0.. with an independent counter, and %al set to
the vector-register count.  The pure-integer path is unchanged, so the common
case (and native self-compile) is byte-for-byte identical.

Adds three e2e regression tests (cp.test.e2e.recordret.pas), run on both
backends via AssertRunsOnAll: single float arg to a managed-record-returning
function, two float args, and a single float arg to a managed-record-returning
method.

Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; the new tests
pass on both the QBE and native arms.
2026-06-20 01:13:58 +01:00
Graeme Geldenhuys 738e080407 feat(compiler): make incremental compilation the default; fix dropped unit init
Incremental compilation (per-unit .o emission with embedded .bif) is now the
default for every build; --no-incremental opts out and builds a single
whole-program object.  The old --incremental opt-in flag is removed.

Fix a latent incremental-mode bug the flip exposed: in incremental /
--skip-dep-codegen mode the dependency unit BODIES are compiled into their own
objects and AppendUnit is skipped for them in the main program codegen.  But
AppendUnit was also where a unit's initialization section got registered into
the init-call list, so the program startup ($main) emitted no
`call <Unit>_init` for any dep — every dependency's initialization section
silently never ran.  This affected BOTH backends and was why a native-built
TestRunner (built incrementally by default) registered zero tests: each
cp.test.* unit's `initialization`/RegisterTest never executed.

Fix: add ICodeGen.NoteDepInitUnit(name, hasInit), implemented on the QBE
backend and the native backend (virtual on TNativeBackend, overridden by the
x86_64 backend), which records a dep unit's init symbol in dependency order
without emitting its body.  The driver calls it for each skipped dep in
incremental mode, so $main emits the calls and they resolve against the
<Unit>_init symbols exported by the per-unit objects.  Verified across the full
matrix (native/qbe x incremental/--no-incremental), including chained
multi-unit init ordering (a dep that uses another dep sees the other's init
having run first).

Guard the incremental block so it never runs in --emit-ir / --emit-asm /
--dump-ast modes (those produce stdout text and no objects); this keeps
fixpoint.sh, fixpoint-native.sh and rolling-bootstrap.sh — all of which use
--emit-ir/--emit-asm — unaffected.

Tests: replace the --incremental e2e test with one that exercises the default
incremental path (no flag), plus a new --no-incremental test asserting the
whole-program path leaves no per-unit .o behind.

Remove the eight TestCodegen_Win64_* record-return IR tests and the GenIRWin64
helper.  They pinned aspirational QBE Win64-ABI IR for a target the native
backend does not support (only linux-x86_64 is implemented) and that is being
deprecated along with QBE.  The native backend miscompiled the in-process
GTarget-global Win64 path these tests drove; rather than carry IR tests for an
unsupported, unrunnable, soon-to-be-removed target, they are dropped.  Native
Win64 will be addressed when Windows support is actually built.

Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite
3534 tests / 0 failures on both the QBE-built and native-built test runners.
2026-06-19 21:05:24 +01:00
Graeme Geldenhuys de0ea5defb chore(phase3): deprecate QBE in help text, guard internal linker, document toolchain
Phase 3 of the native-default drive — the locally-verifiable items (CI and
rolling-bootstrap changes are deferred to Phase 4, where they become correct
once v0.12.0 is the rolling-bootstrap anchor):

- Help text now marks the backends explicitly: "qbe (deprecated) | native
  (default)". QBE is retained as the e2e parity oracle and QBE-fixpoint guard;
  removal is a later release.

- fixpoint-native-internal.sh now drives the FULL internal pipeline (internal
  assembler + internal linker) against an all-external (gcc assemble + gcc link)
  reference, instead of only differing on the assembler. Both internal tools are
  now the default toolchain, so the conformance guard must exercise both. Header
  comments updated to reflect that it guards the internal linker too and why it
  stays a differential probe (the internal assembler buffers the whole object in
  memory and OOMs on the compiler's own ~631k-line .s).

- language-rationale.adoc records the decision: native backend + internal
  assembler + internal linker as the default toolchain (no external tools beyond
  the C runtime startup objects), why QBE is kept-but-deprecated, and the
  per-invocation escape hatches (--assembler external, --linker external,
  --backend qbe, --incremental).

Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite
3541 tests / 0 failures on both the QBE-built and native-built test runners.
2026-06-19 18:16:46 +01:00
Graeme Geldenhuys 91b7354c8f fix(native): SetLength(A, N) must evaluate N before loading the array pointer
The native dynamic-array SetLength lowering loaded the array's current data
pointer into %rdi (the first argument to _DynArraySetLength) BEFORE evaluating
the new-length expression. When that expression itself emitted a call — most
commonly SetLength(Result, Length(A)), where Length(A) lowers to a
_DynArrayLength call — the call clobbered %rdi, so _DynArraySetLength resized
the wrong array (the length-expression's operand) instead of the intended one.

The visible symptom: a function taking a dynamic-array parameter and returning
a dynamic array had its Result aliased to the first array parameter's buffer.
SetLength(Result, Length(A)) resized A instead of Result, every Result[I] write
was lost, and the caller got back the first argument unchanged. QBE was correct
throughout; this was a native-only parity divergence.

Fix: evaluate the length expression into %esi first, then load the array
pointer into %rdi immediately before the call — matching the safe ordering the
string and field-receiver SetLength paths already used.

Adds five e2e regression tests (cp.test.e2e.dynarray.pas) covering the result-
from-param-length case, the const-array-param subtract, global-from-global
length, local-from-local length, and length-from-a-user-function-call. All run
on both backends via AssertRunsOnAll; the native arm would have failed before
this fix. This closes a test-coverage gap: no existing test exercised a
SetLength whose length argument emitted a call.
2026-06-19 17:33:39 +01:00
Graeme Geldenhuys 30d577daae fix(assembler): handle @tpoff relocations and %fs:disp numeric TLS access
The internal assembler had two TLS-related bugs that caused undefined
symbols in .o files compiled from runtime units using threadvars:

1. sym@tpoff(%reg) operands: the parser included '@tpoff' as part of
   the symbol name instead of recognising it as a relocation modifier.
   Fixed by stopping symbol-name accumulation at '@', detecting the
   'tpoff' suffix, and setting a TpOff flag that emits R_X86_64_TPOFF32.

2. %fs:0 (thread base read): the parser fell through to symbol parsing
   and created an undefined reference to literal "0". Fixed by checking
   for a numeric displacement before attempting symbol parsing in the
   %fs: handler.

Also fix the --help text to show 'native (default)' instead of
'qbe (default)' now that the native backend is the default.
2026-06-19 16:35:11 +01:00
Graeme Geldenhuys cf274eeb89 feat(native): default to internal assembler and linker
The native backend now uses --assembler internal and --linker internal
by default when the user does not explicitly pass these flags. This
eliminates the dependency on external tools (gcc/as/ld) for native
backend compilation.

Also fixes a linker bug where writing to a currently-executing binary
on Linux would fail with EStreamError — the linker now calls
DeleteFile before creating the output file.
2026-06-19 14:32:58 +01:00