Commit graph

67 commits

Author SHA1 Message Date
mzhoot 94ae8c354c Рабочие синонимы 2026-07-04 21:44:54 +03:00
Graeme Geldenhuys 7a95fd0472 rtl-unification: remove dead archive-resolution code; last test off the archive
Partial Stage-3 teardown — the safe removals that do not affect the bootstrap
chain (the runtime Makefile/ar stay until a release ships a source-build-capable
binary, since the v0.12.0-rooted rolling-bootstrap replay needs them for its
pre-source-build commits):

- Remove FindRTLArchive + TToolchain.RTLPath from uToolchain — dead since the
  link paths source-build the RTL; the field was set but never read.
- TInternalLinkerE2ETests gated on blaise_rtl.a existing, so it silently skipped
  once the archive was gone; gate on the RTL source instead.  It now runs (5
  tests) — the last automated test still tied to the archive.
- Refresh the runtime mem test/bench build-instruction comments to the
  source-built, archive-free invocation (blaise --output, no qbe+gcc+.a).

Full suite green (3829 tests, 1 ignored) — TInternalLinkerE2ETests no longer
skipped.
2026-06-25 23:52:39 +01:00
Graeme Geldenhuys ac9e2fa0b1 refactor(rtl): relocate RTL units into the compiler tree (dotted-flat names) 2026-06-25 16:05:44 +01:00
Graeme Geldenhuys 743ca4f5e6 fix(rtl): blaise_atomic uses Pointer params, not a public PInteger alias
blaise_atomic exported a PInteger = ^Integer type that collides with the same
alias in blaise_arc when the RTL units are compiled together into one program
(the pasbuild library / whole-program path), giving 'Duplicate type name
PInteger'.  The per-unit Makefile build never saw it.  Type the params as
Pointer (the asm body reads through %rdi regardless); the external-name callers
in blaise_arc are unaffected (link is by symbol).  Prerequisite for compiling
the RTL units together (RTL-unification).
2026-06-25 23:08:30 +01:00
Graeme Geldenhuys 0c4a22f5e2 feat(rtl): migrate blaise_utf8 .s to inline asm — RTL now pure Pascal 2026-06-25 14:53:30 +01:00
Graeme Geldenhuys d840f40e89 feat(rtl): migrate blaise_start .s to inline asm 2026-06-25 14:53:30 +01:00
Graeme Geldenhuys 7fbf4d8192 feat(rtl): migrate blaise_setjmp .s to inline asm 2026-06-25 14:53:30 +01:00
Graeme Geldenhuys fe7b016119 feat(rtl): migrate blaise_atomic .s to inline asm 2026-06-25 14:53:30 +01:00
Graeme Geldenhuys de27984647 feat(rtl): extract TPlatformLayout port for per-target struct/constants
Move the OS integer constants (O_*/S_*/SEEK_*/CLOCK_REALTIME/WNOHANG) and the
struct stat layout out of rtl.platform.posix and behind a new TPlatformLayout
port. struct stat is now an opaque sized buffer whose Size/Mtime/Mode fields are
read at target offsets via StatSize/StatMtime/StatMode, rather than a record
that bakes in one layout — FreeBSD's stat differs in both offsets and size.

The Linux adapter (rtl.platform.layout.linux, TPlatformLayoutLinuxX86_64) supplies
today's values verbatim; struct stat offsets verified against sys/stat.h
(st_mode=24, st_size=48, st_mtim=88, sizeof=144). struct tm is left shared — its
used fields are identical on Linux/FreeBSD amd64.

The POSIX unit's one per-target wire is its 'uses rtl.platform.layout.linux';
the FreeBSD RTL archive will compose its own layout. No conditional compilation.

Step 0b of docs/native-target-architecture.adoc. Behaviour-preserving:
FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK, WARMCACHE_FIXPOINT_OK;
full suite 3761 tests green; file I/O (FileExists/DirectoryExists/FileAge/
ReadFile) exercised end-to-end through the new accessors.
2026-06-25 14:53:30 +01:00
Graeme Geldenhuys 7ac533ca15 build(rtl): assemble Pascal RTL units with the internal assembler
The runtime Makefile assembled each Pascal unit's object via `cc -c` (gcc as a
plain assembler).  Pass `--assembler internal` so Blaise's own in-process
assembler produces the .o instead — removing gcc from all twelve Pascal RTL
unit builds.

The four hand-written .s files in src/main/asm still go through `cc -c` until
Blaise can assemble a standalone .s file (or the asm moves into inline `asm`
blocks); the Pascal units no longer touch gcc.

Verified: all four fixpoints (which rebuild + install the RTL via this rule)
pass, and both QBE- and native-built test runners pass (3758 tests).
2026-06-25 00:01:20 +01:00
Graeme Geldenhuys 626ee4c9f8 fix(linker): ship own _start; drop system CRT startup objects (#142)
The native internal linker linked every program against system CRT startup
objects — Scrt1.o for _start plus crtbeginS.o/crtendS.o/crti.o/crtn.o —
located by scanning a hard-coded list of multiarch and versioned gcc
directories (/usr/lib/gcc/<triple>/<ver>/).  That layout varies by distro, so
the scan failed with "internal linker: CRT startup objects not found" on
distributions whose triple/gcc version was not listed (issue #142, Mageia 9).

Asking gcc (cc -print-file-name) would locate them portably but reintroduces a
link-time gcc dependency, defeating the internal linker's purpose
(toolchain independence).  Instead, follow FPC's approach: the runtime now
supplies its OWN _start (runtime/src/main/asm/blaise_start_x86_64.s), which
marshals argc/argv and tail-calls __libc_start_main(main, ...) — the modern
glibc convention (init/fini = NULL; glibc runs the init array itself).

Blaise consumes nothing else from the CRT set: main is a plain C-style entry,
unit init runs explicitly from main, and the emitted code references no
crtbegin/crtend symbol.  So the internal linker now needs no gcc-provided
object and no versioned gcc directory; FindCrtObjects and its directory lists
are removed.

The new _start ships inside blaise_rtl.a; AddArchive includes all members
unconditionally, so the entry symbol is always present.

Tests: two TInternalAsmE2ETests cases drive the REAL compiler binary through
the internal assembler + internal linker (the only harness that exercises the
runtime _start; the e2e suite links via cc) and assert the two things _start
owns — exit-code propagation (Halt(7)) and argc/argv forwarding (ParamCount/
ParamStr). The native fixpoints already link the compiler itself via this path.

Design note: docs/self-contained-start-design.adoc.
2026-06-24 23:39:11 +01:00
Graeme Geldenhuys 909684c57f chore: remove stale info from code comment. 2026-06-20 15:09:09 +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 0d47c5f996 feat(backend): default to native backend; fix codegen bugs exposed by the switch
The native x86-64 backend is now the default (bkNative).  QBE remains
available via --backend qbe.  Several codegen bugs that were invisible
while QBE was the default are fixed in this commit:

Native backend fixes:
- EmitLeaqGlobal helper: threadvar globals now emit the correct
  fs:0 + @tpoff TLS sequence instead of bare %rip-relative leaq.
  Fixes static array write (FreeLists), string subscript assignment,
  EmitExprAddr, record memcpy, method-ptr cast, and open-array push
  paths that all used raw leaq %s(%rip).
- EmitIncDec: unified local/global paths through VarOperand so
  threadvar scalar Inc/Dec (e.g. LargeFreeCount) gets TLS addressing.
- EmitIncDec FAE.Base: Inc(P^.Field) where P is a pointer now
  evaluates the base expression instead of leaq on the stack slot.
- FinalizeEmit: data section (.data/.bss/.rodata) now emits once at
  the end of unit compilation via FinalizeEmit, not inside each
  EmitUnit call.  Fixes duplicate/missing string literals in sep-compile.
- EmitFunctionDef AExported: implementation-only helpers in monolithic
  mode stay file-local (.globl suppressed) to avoid symbol collisions
  with the RTL archive.

QBE backend fixes:
- AppendUnit typeinfo: export prefix added to data $typeinfo_TObject,
  $typeinfo_TCustomAttribute, $vtable_TObject, $vtable_TCustomAttribute
  so native-built RTL can resolve them.
- FSuppressSystemDefs: decoupled from FExportAll so CreateUnitCodeGen
  can export all symbols without suppressing system defs.

Infrastructure:
- runtime/Makefile: BLAISE_FLAGS variable for passing extra compiler
  flags (e.g. --backend qbe) from build scripts.
- fixpoint.sh: make clean before RTL rebuild to pick up backend change.
2026-06-19 04:27:41 +01:00
Graeme Geldenhuys 07aac3322f feat(string): writable string subscript S[i] := ch with copy-on-write
S[I] := <byte> was rejected by the semantic pass ('is not a static array
or dynamic array') for any string — local, global, or var-param — on both
backends.  It is now supported as the symmetric counterpart of the S[I]
read: an in-place byte store into a 0-based UTF-8 string.

Because strings are reference-counted and literals are immortal (stored in
read-only memory; the native backend put them in .rodata, so a naive store
segfaulted, while QBE silently mutated the shared literal), the write
performs copy-on-write.  New RTL helper _StringUnique(S) returns S when it
is uniquely owned (rc=1) and otherwise allocates a fresh rc=1 copy, releases
the old reference, and returns the copy.  Both backends emit
_StringUnique -> write the result back to the slot -> storeb, so the slot
keeps exactly one owned reference and mutating one alias never disturbs
another (Delphi/FPC UniqueString semantics).

The RHS accepts a numeric ordinal, Chr(n), or a single-character literal
(the byte-shaped forms used in place of a Char type).  PChar subscript
writes keep their existing in-place path (raw pointer, no ARC header).

Dual-backend e2e tests (write, copy-on-write aliasing + literal reuse,
var-param) in cp.test.e2e.stringops.  Updates language-rationale.adoc
(writable subscript + COW) and grammar.ebnf (string as a subscript-assign
base).
2026-06-18 15:39:38 +01:00
Andrew Haines 4e61cb1e8f fix(rtl): run one-time RTL setup via a _BlaiseInit startup hook
A unit's initialization section only runs when that unit is compiled
into the program's own translation unit; units linked from a prebuilt
archive never get their _init called.  blaise_weak relies on its
initialization section to set up the weak-reference table mutex, so for
archive builds that setup never happened.  A zero-filled mutex object is
a valid unlocked mutex on some platforms, which kept this latent — but
that is not guaranteed everywhere.

Add a _BlaiseInit RTL entry point that performs the one-time setup
(currently the weak-table mutex, guarded to stay idempotent) and have
each backend's $main emit a call to it right after _SetArgs, before any
user code runs.  The mutex is now initialised explicitly instead of by
relying on a zero-filled object happening to be valid.

Regression: cp.test.codegen TestMain_CallsBlaiseInit asserts the call.
2026-06-17 00:10:53 +01:00
Graeme Geldenhuys 07a1236896 feat(sets): subset/superset operators; reject enum-member shadowing
Two related set/enum fixes from shaking the sets cluster.

1. Set subset (<=) and superset (>=) operators, e.g. `if s <= t`:
   - Semantic: <= and >= on two sets yield Boolean (alongside =, <>).
   - Codegen both backends: non-jumbo sets test (s and not t)=0 for <=
     (operands swapped for >=); jumbo sets call a new RTL _SetSubset
     (added to blaise_set.pas) which returns whether every bit of A is in B.
   Completes the set-operator suite (+, -, *, in, =, <>, <=, >= now all work).

2. Reject a variable that shadows a visible enum member (e.g. `var c` when
   an enum member `C` is visible — Pascal is case-insensitive). Such a
   shadow silently retargeted the member in a set literal `[A, C, D]` to the
   variable: QBE errored with "Set literal element 'c' is not a constant",
   and — worse — the native backend produced a WRONG bitmask with no error
   (the member's bit was dropped). This surfaced as a corrupt for-in over a
   set when the loop variable collided with a member. Now rejected at the
   declaration with a clear message, mirroring the existing type-name-shadow
   rule (FPC/Delphi allow the shadow; Blaise does not).

Verified subset/superset truth tables and for-in over a set (no shadow) on
both backends, and that the shadow is now a compile error. Adds
TE2ESetOpsTests.TestRun_Set_SubsetSuperset and
TSemanticTests.TestVarDecl_ShadowsEnumMember_RaisesError. Rationale's
"Variable Names May Not Shadow Type Names" section extended to enum members.
2026-06-16 17:00:09 +01:00
Graeme Geldenhuys 267e8db702 fix(rtl): WriteDecimal mishandles Low(Int64), printing only '-'
WriteDecimal negated N before extracting digits. For Low(Int64) =
-9223372036854775808 the negation overflows (no positive counterpart in
two's complement) and leaves the value negative, so the 'while AbsN > 0'
loop ran zero times and only the sign was emitted.

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

Adds e2e regression TestRun_Int64_MinValue.
2026-06-15 16:05:37 +01:00
Graeme Geldenhuys 7c26598bb8 feat(format): float support in Format() — %f/%e/%g with width/precision (QBE)
Format() previously handled only %d, %s and %%; any float directive rendered
verbatim, and on the QBE backend a float argument failed to compile because the
double value was stored with 'storel <d-temp>', which QBE rejects.

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

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

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

Tests: IR unit tests assert tag-2 emission and the bit cast. E2E coverage and
the native backend follow in the next commit.
2026-06-15 00:58:37 +01:00
Graeme Geldenhuys fa853e0eb2 feat(sets): jumbo sets — set of enum up to 256 members
Extend set of <enum> from a 64-member cap to 256, Java-EnumSet style. Sets
of 64 members or fewer keep the existing single-register bitmask (QBE w/l);
sets of 65..256 members ('jumbo') become an inline byte-array bitmap of
ceil(N/8) bytes, treated as a value aggregate (passed by reference, returned
via sret, memset/memcpy), with operations performed by new RTL helpers.

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

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

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

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

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

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

Closes the design discussion behind #81.
2026-06-14 23:23:19 +01:00
Graeme Geldenhuys 17c14b37aa perf(qbe): borrowed-local const-string args + lookup/format tail wins
With the default-argument statement bug fixed, the borrowed-local
elision blocked in 91f44ec is sound and enabled: a const-string argument
that is a plain local or by-value/const parameter variable emits no
caller pin at all — the frame's own reference outlives the call.
Exclusions guard the aliasing edges: address-taken locals (explicit @,
passed to var/out params), locals captured by nested procedures (both
via a per-function blocklist rebuilt in EmitVarAllocs), and calls whose
signature has a var/out string param (F(L, L) lets the callee release
L's buffer through the alias).  Nine IR contract tests cover every
shape, including the new exclusion guards.

Tail wins on the remaining profile:
- QBEMangle: fast path returns the input unchanged when it contains no
  mangled characters (1.3M calls were rebuilding clean names one concat
  per character).
- TStringList.FindSorted: compares the probe slot in place through the
  CompareStr/CompareText builtins instead of copying it into a local
  and dispatching through Compare — several ARC ops per binary-search
  step on the hottest lookup path.
- _StringFormatN: the sizing pass uses a pure DecimalWidth count
  instead of rendering every %d argument twice.

Compiler self-compile: 0.78 s -> 0.60 s wall, 8.60G -> 6.45G
instructions.  Verified through two self-hosted generations, gen-2
native emission, both fixpoints and the full suite (2870 tests).
2026-06-10 23:53:01 +01:00
Graeme Geldenhuys 8ce374c0ba perf(runtime): stack buffers for WriteDecimal and Format
WriteDecimal/WriteDecimalU heap-allocated a 22-byte digit scratch on
every call, the IntToStr family allocated a second per-call scratch on
top, and _StringFormatN allocated and freed a buffer per %d argument in
both of its passes — four allocator round-trips per formatted integer
on one of the hottest runtime paths (every emitted temp name goes
through IntToStr).  All nine sites now use fixed stack arrays; one
shared scratch serves both _StringFormatN passes.

Compiler self-compile: 8.73G -> 8.65G instructions.
2026-06-10 19:47:19 +01:00
Graeme Geldenhuys 9d08c7913a perf(runtime): inline header checks into _StringRelease
_StringReleaseCheck was a separate 4-argument call on every string
release (~350M per compiler self-compile) and showed up as 12.7% of all
instructions on its own.  The three sanity checks (double-free, length,
capacity) now run inline in _StringRelease with DiagAbort as the cold
path; the diagnostics remain always-on.

Compiler self-compile: 9.2G -> 8.7G instructions, 0.80 s -> 0.78 s.
2026-06-10 19:30:43 +01:00
Graeme Geldenhuys bb2fe2da75 refactor: rename uCodeGen/uCodeGenQBE to blaise.codegen/blaise.codegen.qbe
Align the QBE backend unit names with the dotted naming convention
already used by the native backend (blaise.codegen.native.*).

  uCodeGen.pas    → blaise.codegen.pas
  uCodeGenQBE.pas → blaise.codegen.qbe.pas

Updated all uses clauses (~70 test files, main compiler units),
documentation references (README + 5 docs/*.adoc files), and
comment references in runtime/stdlib.
2026-06-10 12:12:14 +01:00
Graeme Geldenhuys a9cb4961ea feat(debug): extend leak tracking to strings and dynamic arrays
Track string and dynamic-array buffer leaks in --debug builds.
Strings are registered in _StringAddRef (on the 0→1 transition)
and removed in _StringRelease; likewise for _DynArrayAddRef/Release.
The leak report prints "string" or "dynarray" as the type tag
instead of a class name.

Sentinel-based type tags (GLTTagString=Pointer(2), GLTTagDynArray=
Pointer(3)) distinguish buffer entries from class entries in the
hash map.  The report reads the correct refcount header offset
for each type (12-byte string header, 8-byte dynarray header,
CLASS_HDR for classes).

LT_BUCKETS grows from 1024 to 4096 to accommodate the higher
entry count when strings/dynarrays are tracked.  Report wording
changes from "object(s) not released" to "leak(s) not released".

2768 tests pass (2 new e2e tests), FIXPOINT_OK.
2026-06-10 10:23:05 +01:00
Graeme Geldenhuys c1c9a0851e feat(debug): add allocation-site info (unit:line) to leak tracker
Extend _LeakTrackerRegister from 2 to 4 args (UserPtr, ClassName,
UnitName, Line) so the --debug leak report prints where each leaked
object was allocated.  Both QBE and native backends now emit the
extra unit-name string literal and AST line number at every
_ClassAlloc site.  Leak report output changes from:
  - TBox (rc=1)
to:
  - TBox (rc=1) at LeakSite:14

Runtime: bucket size grows from 16 to 32 bytes; LTInsert stores
unit-name ptr + line; _LeakTrackerReport prints the " at unit:line"
suffix.

Native backend: plumb FDebugMode from TCodeGenNative through
TNativeBackend; emit _LeakTrackerEnable in $main; emit
_LeakTrackerRegister at both constructor-call paths.

QBE backend: add FCurrentUnitName, set in Generate/GenerateUnit/
AppendUnit/AppendProgram; extend both _LeakTrackerRegister call
sites with EmitStrLit(FCurrentUnitName) + node.Line.

2766 tests pass (2 new e2e tests), FIXPOINT_OK.
2026-06-10 10:11:52 +01:00
Andrew Haines f74e5ccf43 fix(qbe): refcount dyn-array, interface, and nested-record fields in records
Records with value semantics may contain fields with reference semantics
(String, dynamic array, interface, nested managed record).  The QBE
backend's field walkers only covered the String and Class cases, so:

  * A record whose field is a dynamic array leaked the buffer on every
    copy and return-by-value — _DynArrayAddRef/_DynArrayRelease were not
    invoked from EmitRecordCopy or EmitRecordReleaseFields, and there
    was no scope-exit release for dyn-array locals.
  * A class with a record field whose own fields were managed (String,
    Class, dyn-array, interface, or a deeper record) leaked those
    sub-fields when the class was released — _FieldCleanup_<T> skipped
    record-typed fields entirely.
  * Interface fields inside records were never copied or released.

This commit:

  * Adds _DynArrayAddRef / _DynArrayRelease to blaise_arc.pas alongside
    the existing _StringAddRef/_StringRelease and _ClassAddRef/_ClassRelease.
    They walk the existing [refcount:4][length:4] dyn-array buffer header
    (layout defined by _DynArraySetLength in blaise_str.pas), are nil-safe
    and immortal-safe (rc = -1), and use _AtomicAddInt32 / _AtomicSubInt32
    (lock xadd) so the refcount stays sound under threading — matching the
    other ARC primitives.
  * Extends EmitRecordCopy, EmitRecordReleaseFields, and
    EmitFieldCleanupFn to handle tyDynArray, tyInterface, and nested
    tyRecord fields (the latter by recursing through the shared
    EmitRecordReleaseFields walker).
  * Routes tyDynArray field assignments through the existing IsArc
    path so a field write retains the new buffer and releases the old.
  * Adds tyDynArray to EmitArcCleanup + EmitExcPathArcCleanup so
    dyn-array locals balance their first-assignment retain at scope
    exit (and on exception paths).
  * Adds tyDynArray to EmitAssignment for variable-to-variable
    assignment so b := a between dyn-array vars is refcount-symmetric.

Adds two regression tests in cp.test.e2e.records.pas:

  * TestRun_Record_DynArrayField_ReturnByValue_NoLeak — 5000-iter
    return-by-value of a record with a dyn-array field; asserts the
    buffer contents survive (proves AddRef/Release pairing) and the
    program runs to completion.
  * TestRun_Class_RecordField_NestedClass_FullCleanup — class -> record
    -> class -> record -> class chain with each Create/Destroy bumping
    a global AliveCount; after 100 iterations AliveCount must be 0,
    proving _FieldCleanup recurses through nested record fields.

TestRunner: OK (2663 tests).  Self-bootstrap from a master-built stage-1
through stage-2/3/4 reaches a clean stage-3 == stage-4 IR fixpoint.
2026-06-07 20:28:16 -04:00
Graeme Geldenhuys c26e198180 perf(rtl): SIMD-accelerated UTF-8 codepoint counting (SSE2 + AVX2)
CodePointLength now delegates to _Utf8CountCodePoints in hand-written
x86_64 assembly.  Processes 32 bytes/iteration with AVX2 (runtime-
detected via CPUID) and 16 bytes/iteration with SSE2, falling back to
scalar for tail bytes.  The algorithm counts non-continuation bytes
(bytes where (b & 0xC0) != 0x80) using signed pcmpgtb against -65.
2026-06-07 23:56:12 +01:00
Graeme Geldenhuys d06cd40a0c feat(rtl): add _Utf8DecodeAt helper for UTF-8 codepoint decoding
Decodes one UTF-8 codepoint at a given byte index in a string. Returns
both the codepoint value and byte count packed into a single Int64:
(ByteCount shl 32) or CodePoint. This will be used by the for-in
codepoint iteration codegen and the StrUtils codepoint functions.
2026-06-07 23:31:01 +01:00
Graeme Geldenhuys 58f6a050db feat(punit): add AssertEquals overload for UInt64, register UInt64ToStr built-in
PtrUInt (= UInt64) had no matching AssertEquals overload, causing
ambiguous-overload errors in test_blaise_mem. Added the UInt64 overload
to punit and registered UInt64ToStr in the symbol table (it was already
handled in semantic + codegen but missing from the built-in registry).

Reverts the Pointer-cast workaround from the previous commit — the test
now uses PtrUInt directly as intended.
2026-06-07 12:31:04 +01:00
Graeme Geldenhuys 64060a542d fix(rtl): resolve overload ambiguity in test_blaise_mem
PtrUInt maps to UInt64 which has no matching AssertEquals overload,
causing an ambiguous-overload error. Use Pointer cast instead since
the test is comparing pointer identity — the Pointer overload is the
correct match.
2026-06-07 12:26:34 +01:00
Graeme Geldenhuys fcd654b58c fix(rtl): add mandatory () to bare calls in punit test framework
The punit test framework had bare zero-arg function calls
(GetTestError, CheckInactive) that are now caught by the semantic
diagnostic. Added () to all call sites.
2026-06-07 12:16:12 +01:00
Graeme Geldenhuys 57c0660a37 refactor(lang): remove IsNoArgFuncCall dead code, fix remaining bare calls
With mandatory () on all zero-argument calls (51ebf23), the
IsNoArgFuncCall/NoArgFuncDecl fields on TIdentExpr and the
synthesised TFuncCallExpr codegen path are dead code. Remove them.

This exposed ~300 bare function calls missed by the original
migration script across compiler, runtime, stdlib, tests, and
embedded Blaise source strings. Fix all of them.

Add IsProcFieldCall support to TMethodCallExpr so that
H.Handler() works for procedure-type fields — the parser creates
TMethodCallExpr for Obj.Name(), and the semantic pass now detects
when 'Name' is a procedural-typed field rather than a method,
routing through the indirect-call codegen path.

Update fixpoint.sh to fall back to an existing blaise_rtl.a when
the release binary is too old to build the runtime.

2627 tests pass, FIXPOINT_OK.
2026-06-06 20:28:13 +01:00
Graeme Geldenhuys 51ebf2350a feat(lang): require mandatory () on all zero-argument calls
Make parentheses mandatory on every function, procedure, method, and
constructor call — even those with zero arguments.  A bare identifier
or field access is now unambiguously a variable/field/property read;
appending () makes it a call.

Mechanically migrated all 144 source files (compiler, runtime, stdlib,
tests, kanban tool).  Fixed several latent bugs exposed by the AST node
transition from TFieldAccessExpr.IsMethodCall to TMethodCallExpr:

- IsBuiltinToString applied to record methods (added tyClass guard)
- IsVarParam not set for value record/static-array parameters in
  AnalyseMethodCallExpr (extended to check skParameter + aggregate type)
- Native backend used movq (pointer load) for record receivers instead
  of leaq (address-of) in EmitMethodCallExpr
- ResolveDiamond now handles TMethodCallExpr for diamond-operator
  constructor calls

Updated grammar.ebnf (MethodCall, ProcCall, Factor rules) and
language-rationale.adoc with the design decision.  Marked the
future-improvements.adoc entry as implemented.

All 2627 tests pass.  Fixpoint verified (FIXPOINT_OK).
2026-06-06 19:14:47 +01:00
Graeme Geldenhuys 70a22267cc feat(rtl): mutex-protect weak reference table for thread safety
Add a pthread mutex around all weak reference table operations
(_WeakAssign, _WeakClear, _WeakZeroSlots).  The table is a shared
global data structure, and concurrent _ClassRelease calls from
multiple threads could corrupt the bucket chains without locking.

The mutex is coarse-grained (one lock for the entire table) but
sufficient because weak references are rare relative to strong ARC
operations.
2026-06-06 12:48:08 +01:00
Graeme Geldenhuys e093be0b70 feat(rtl): atomic ARC refcount operations for thread safety
Replace plain read-modify-write refcount operations with atomic
lock xadd instructions via a new blaise_atomic_x86_64.s assembly
stub.  _AtomicAddInt32 and _AtomicSubInt32 return the previous
value, so exactly one thread sees the transition to zero and
triggers destruction -- eliminating the TOCTOU race in
_ClassRelease and _StringRelease.

Affected operations:
  - _ClassAddRef: atomic increment
  - _ClassRelease: atomic decrement, destroy on old_rc == 1
  - _StringAddRef: atomic increment (immortal check unchanged)
  - _StringRelease: atomic decrement, free on old_rc == 1
2026-06-06 12:44:53 +01:00
Graeme Geldenhuys 51a00a5038 feat(rtl): per-thread memory allocator via threadvar
Convert the allocator's global state (FreeLists, ArenaHead,
LargeFreeHead, LargeFreeCount) from var to threadvar, giving each
thread its own independent arenas and freelists with zero contention
and no locks required.

Also fix the native backend .tbss emission for aggregate types
(tyRecord, tyStaticArray) — the threadvar section was using
IntByteSize which returns 4 for unknown types, instead of RawSize
which returns the actual byte size of the type.
2026-06-06 12:35:46 +01:00
Graeme Geldenhuys 43f5e49d9e feat(kanban): detect and merge external file changes in TUI
Add file-change monitoring to the kanban TUI so that tasks added via CLI
(or any external process) while the TUI is running are automatically
merged into the live view instead of being silently overwritten on save.

The implementation has three layers:

1. New FileAge built-in (compiler + runtime): returns the file's mtime as
   Int64 via stat(), or -1 if the file does not exist.  Follows the same
   pattern as FileExists — symbol table registration, semantic analysis,
   codegen, and POSIX ABI stub.

2. Merge-aware TBoard (kanban.data): tracks last-known mtime, deleted IDs
   (so re-reads don't resurrect user-deleted tasks), and provides
   HasExternalChanges/MergeFromDisk.  Save() now auto-merges before
   writing.  ID conflicts (same ID, different task) are resolved by
   reassigning the in-memory task to a fresh ID.

3. Idle polling in TKanbanUI.Run: every ~2 seconds (20 idle ReadKey
   cycles at VTIME=1), checks HasExternalChanges and merges new tasks
   with a status bar notification.
2026-06-06 12:08:57 +01:00
Graeme Geldenhuys 34a1c4feab feat(rtl): make exception globals thread-local
Change g_exc_top and g_current_exception from var to threadvar in
blaise_exc.pas, giving each thread its own exception chain. This is
required for safe multi-threaded exception handling.
2026-06-06 12:06:05 +01:00
Graeme Geldenhuys d69fcacaca feat(rtl): WriteLn prints True/False for Boolean values
Add _SysWriteBool to the runtime platform layer so WriteLn(Boolean)
prints 'True' or 'False' instead of '1' or '0', matching Delphi/FPC
behaviour. Both QBE and native x86_64 backends emit the new call for
tyBoolean arguments. Updated ~65 assertions across 13 test files.
2026-06-05 18:08:40 +01:00
Graeme Geldenhuys 0c5910195c refactor(rtl): drop legacy platform aliases, derive constants from target
Remove sLineBreak (Delphi alias for LineEnding) and PathDelim (Delphi
alias for DirectorySeparator).  Blaise keeps exactly three platform
constants: LineEnding, DirectorySeparator, PathSeparator.

Platform constant values are now derived from GTarget (the compilation
target) via TargetLineEnding/TargetDirectorySeparator/TargetPathSeparator
in blaise.codegen.target, so cross-compilation produces the correct
values without conditional compilation.
2026-06-05 17:01:04 +01:00
Graeme Geldenhuys 3addd9630f chore(runtime): switch RTL build to unit-as-top-level; drop build-driver shims
Now that the compiler supports `blaise --source Unit.pas --output Unit.o`
directly, the build-driver pattern (a thin program wrapper + IR sed strip)
is no longer needed.  Replace every multi-step rule with a single invocation
and delete the eight *_build_driver.pas files.

This commit must follow the merge of blaise_clean_split in git history so
that the rolling-bootstrap script can build the merge commit using the
previous binary (which still uses the old Makefile), then build this commit
using the freshly-merged binary (which understands unit-as-top-level).
2026-06-03 23:49:29 +01:00
Graeme Geldenhuys f4baabb9ce fix(codegen): WriteLn(Double/Single) crashed QBE with type mismatch
EmitWrite had no tyDouble/tySingle case; floats fell through to
_SysWriteInt with a d-typed temp, which QBE rejected as an invalid
argument type.

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

Two new e2e tests (TestRun_WriteLn_Double_Direct, TestRun_WriteLn_Single_Direct)
verify direct float output; 2375 tests pass.
2026-06-03 13:22:10 +01:00
Graeme Geldenhuys 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 ebe131da15 feat(debug): show refcount per object in leak report
Each leaked object line now reports its refcount: `- ClassName (rc=N)`.
This distinguishes the two leak modes at a glance:

  rc=1  reference created but never released (dangling owner)
  rc>1  over-retain — an AddRef without a matching Release (double-AddRef)

Reads the refcount from the class header at UserPtr - CLASS_HDR.  Adds a
WriteSignedInt helper since immortal objects report rc=-1.
2026-06-01 23:55:51 +01:00
Graeme Geldenhuys 043602a0a4 fix(arc): eliminate double-AddRef on function-return class assignments
Three fixes that work together to correct ARC refcount accounting:

1. _ClassFree now delegates to _ClassRelease instead of bypassing ARC
   with a raw _BlaiseFreeMem.  This lets users call .Free (out of habit)
   or rely on ARC — both paths go through the same refcount logic.

2. ExprOwnsRef helper in codegen suppresses the call-site _ClassAddRef
   when the RHS expression already owns a +1 reference (function/method
   return values).  Covers TFuncCallExpr (with ResolvedDecl guard to
   exclude type casts), TMethodCallExpr, TFieldAccessExpr.IsMethodCall,
   and TIdentExpr.IsNoArgFuncCall / IsImplicitSelfMethod.

3. Allow Pointer() and PtrUInt() casts on tyString arguments, needed by
   rtl.platform.posix for Pointer(APath) where APath is a string.

The double-AddRef bug inflated refcounts on every class-returning
function call: the callee's Result := X emitted AddRef (correct — the
Result slot is not released at scope exit), and the caller's assignment
emitted a second AddRef (incorrect — the +1 already transferred).
2026-06-01 23:49:39 +01:00
Graeme Geldenhuys a231c7d043 feat(debug): add --debug leak reporter + Pointer/PtrUInt cast pairs
Runtime leak tracker (blaise_arc.pas):
- Open-addressing hash map (1024 buckets, 16 bytes each) tracks every
  live class instance; zero overhead when disabled.
- _LeakTrackerEnable() activates tracking and registers _LeakTrackerReport
  via atexit; called from $main when --debug is passed.
- _LeakTrackerRegister(UserPtr, ClassName) inserts into the map; called
  from codegen at every constructor site (both TFieldAccessExpr and
  TMethodCallExpr paths).
- _ClassRelease unregisters on refcount-zero free via LTDelete.
- Report written to stderr lists count + class name of each survivor.

Compiler (--debug flag):
- New --debug CLI flag sets FDebugMode on TCodeGenQBE; emits
  call $_LeakTrackerEnable() in EmitMainHeader after unit init calls,
  and call $_LeakTrackerRegister(...) after every _ClassAlloc site.
- --debug-opdf remains independent (OPDF only, no leak tracking).
- SetDebugMode(Bool) added to TCodeGenQBE public API for e2e tests.

Pointer/PtrUInt cast pairs (semantic + codegen):
- Pointer(intOrPtrExpr) validated in AnalyseFuncCallExpr; accepts
  integer, pointer, class, metaclass, procedural, and nil sources.
- PtrUInt(intOrPtrExpr) same validation; resolves to tyUInt64.
- Codegen: w-to-l widening uses extuw for Pointer/PChar targets
  (zero-extend, not sign-extend) to preserve address semantics.
- Four new unit tests in cp.test.pointers.pas.

E2E test infrastructure:
- CompileAndRunWithRTLDebug added to TE2ETestCase base for in-process
  debug-mode compilation without recursive overload ambiguity.
- cp.test.e2e.leakcheck.pas: 5 tests covering no-leak silence,
  single leak, multiple leaks, reference cycles, and debug-off.

2258 tests pass; fixpoint verified.
2026-06-01 16:37:02 +01:00
Graeme Geldenhuys 7dbb981665 build(runtime): enable pipefail so compiler errors fail the build
The Pascal RTL recipes pipe $(BLAISE) --emit-ir through sed to strip
the program section before writing the .ssa file.  Without pipefail,
sed's exit code masks a failing compiler invocation: the recipe
succeeds with an empty .ssa, QBE emits an empty .s, gcc archives an
empty .o, and the first symptom is a downstream link failure with
"no symbols".

Set SHELL=/bin/bash and .SHELLFLAGS=-o pipefail -c at the top of the
Makefile so every recipe inherits pipefail.  Verified by injecting a
parse error into a build driver: make now exits with Error 1 and
prints the compiler's diagnostic, instead of silently producing an
empty archive member.
2026-05-22 15:39:08 +01:00
Graeme Geldenhuys b43a999f80 feat(types): add UInt64 / QWord type
Adds a real 64-bit unsigned integer type with two equivalent names:
UInt64 (Delphi style, matches the existing Int64) and QWord (FPC
style).  PtrUInt now aliases UInt64 too — it's the natural pointer-
sized unsigned on 64-bit.

Language semantics:
- Arithmetic on UInt64 uses udiv/urem; add/sub/mul/and/or/xor/shl/shr
  are bit-identical to their signed counterparts.
- Comparisons use unsigned QBE ops (cultl, cugtl, ...).
- Int64 <-> UInt64 mixing requires an explicit cast; the two types are
  not implicitly convertible in either direction.
- Decimal/hex literals in the (2^63, 2^64-1) range are typed as
  UInt64.  Smaller literals stay Integer/Int64.
- SizeOf(UInt64) = SizeOf(QWord) = 8.

Runtime:
- New _UInt64ToStr in blaise_str.pas plus a WriteDecimalU helper that
  uses UInt64 arithmetic.
- SysWriteUInt64 added to the platform abstraction and implemented in
  the POSIX layer.  WriteLn(UInt64) routes through it.
- IntToStr(UInt64) routes to _UInt64ToStr; explicit UInt64ToStr is
  also exposed as a builtin.

Bootstrap notes:
- Older release binaries cannot compile the runtime any more because
  rtl.platform.pas declares SysWriteUInt64.  A stage-2 rebuild from a
  fresh stage-1 is required after this commit on any worktree with an
  older stage-1, per CLAUDE.md.
- The parser stages literal Value through local var-params rather than
  writing directly to the new TIntLiteral.IsUInt64 field via class-field
  out-params.  Working around a stage-1 codegen bug where var-param
  calls that target a class field silently fail to write back.

Docs:
- docs/grammar.ebnf: built-in type list expanded with UInt64/QWord,
  integer-literal typing rules documented.
- docs/language-rationale.adoc: integer types table updated with the
  unsigned variants, Int64<->UInt64 strict conversion rule explained.

Tests:
- 15 new tests in cp.test.uint64.pas covering symbol-table
  registration, codegen instruction picking (udiv/urem/cultl/cugtl),
  literal-range typing, and e2e round-trips.
- Full suite: 2151 tests pass (up from 2132).
- Fixpoint clean at stage-3/stage-4 (expected: type-system change).
2026-05-22 08:08:35 +01:00
Graeme Geldenhuys 4969b74810 refactor(runtime): replace blaise_exc.c with Pascal + x86_64 assembly
Port all exception frame management, type identity, and runtime check
functions from C to pure Pascal (blaise_exc.pas).  Replace libc
setjmp/longjmp with a minimal custom implementation in x86_64 assembly
(blaise_setjmp_x86_64.s, ~40 lines) that saves only the 8 callee-saved
registers (64-byte jmp_buf vs libc's 200-byte one).

This eliminates the last C source file from the runtime build.  The only
non-Pascal code in the runtime is now the assembly setjmp stub.

Changes:
- runtime/src/main/asm/blaise_setjmp_x86_64.s: _blaise_setjmp/_blaise_longjmp
- runtime/src/main/pascal/blaise_exc.pas: _PushExcFrame, _PopExcFrame,
  _Raise, _Reraise, _CurrentException, _CurrentExceptionMessage,
  _IsInstance, _ImplementsInterface, _GetItab, _Raise_InvalidCast,
  _CheckNil — all ported from blaise_exc.c
- uCodeGenQBE.pas: emit call $_blaise_setjmp instead of call $setjmp
- cp.test.exceptions.pas: IR assertion updated for new symbol name
- runtime/Makefile: assembly rule, Pascal blaise_exc build, C rules removed
- .gitignore: whitelist runtime/src/main/asm/*.s
- runtime/src/main/c/blaise_exc.c: deleted

2083 tests pass, fixpoint verified (stage-3/stage-4).
2026-05-21 01:13:17 +01:00