Commit graph

311 commits

Author SHA1 Message Date
Graeme Geldenhuys 25c1f83cd9 feat(threading): add TThread, TCriticalSection and POSIX thread bindings
Add blaise_thread.pas runtime unit wrapping pthread_create, pthread_join,
pthread_mutex_init/lock/unlock/destroy and sysconf for GetCPUCount.

Implement TThread (Create, Start, Terminate, WaitFor, Execute virtual,
FreeOnTerminate) and TCriticalSection (Enter/Leave) in classes.pas using
ARC-safe ref counting in the thread trampoline.

Fix EmitFieldCleanupFn to walk the parent chain when emitting the Destroy
call — previously a subclass without its own Destroy would generate an
empty cleanup, causing leaked resources from parent destructors.

Link -lpthread in both the compiler driver and E2E test base.

Include 7 E2E tests covering basic execution, WaitFor blocking, Terminate
flag, Finished flag, multiple threads, mutex-protected counter, and
inherited Destroy cleanup.
2026-05-20 08:31:15 +01:00
Graeme Geldenhuys a8a6e2b35e fix(codegen): emit correct QBE types for Single float arithmetic and comparisons
Binary expressions involving Single operands always emitted d-typed (Double)
QBE instructions, causing QBE to reject mixed s/d operands.  Now the codegen
respects the resolved float type: Single op Single emits =s ops, Single op
Double widens the Single operand via exts before =d ops, and Single comparisons
use s-suffixed comparison opcodes (clts, cgts, etc.).
2026-05-20 00:32:35 +01:00
Graeme Geldenhuys 28d1cb009a fix(semantic): allow implicit integer-to-float assignment
Delphi and FPC allow assigning an integer value to a Double or Single
variable without an explicit cast.  Blaise previously rejected this as a
type mismatch.  Add an IsFloat/IsNumeric widening rule in the semantic
pass and emit the correct QBE conversions (swtof, sltof) in codegen for
both Double and Single targets.
2026-05-20 00:22:59 +01:00
Graeme Geldenhuys 95c07ca580 feat(runtime): port blaise_str_fmt.c to pure Pascal _StringFormatN
Replace the C variadic _StringFormat with a pure Pascal _StringFormatN
that takes a format string, a pointer to an array of tag/value pairs,
and a count. The codegen allocates the arg array on the stack via
alloc8, stores (tag:l, value:l) pairs, and calls the non-variadic
function — eliminating the last dependency on C va_list.

This required a multi-stage bootstrap: release binary compiled stage-1
(with new codegen), stage-1 compiled stage-2 (self-hosted with new ABI),
then the C file could be safely removed. Integer args are widened via
extsw when w-typed to satisfy QBE's storel type checking.

Only 2 C files remain in the runtime: blaise_exc.c (setjmp/longjmp)
and blaise_arc_class.c (function-pointer dispatch).
2026-05-19 23:58:58 +01:00
Graeme Geldenhuys 802decba69 feat(runtime): port blaise_float.c to pure Pascal Grisu1 implementation
Replace the C-based float support (_DoubleToStr, _SingleToStr,
_StrToDouble, _AbsInt, _AbsInt64) with a pure Pascal implementation
using a simplified Grisu1 algorithm (Loitsch 2010) for float-to-string
conversion and native double arithmetic for string-to-float.

The new implementation has no libc dependency for the conversion logic
itself (memcpy is still used in BlaiseAllocStr as a temporary measure).
This is a step toward full platform independence.

Also fix a codegen bug where Int64-to-Double promotion in mixed
arithmetic/comparison expressions emitted swtof (32-bit truncation)
instead of sltof (64-bit). This affected three sites in EmitBinExpr
and one in EmitAssignment.
2026-05-19 23:19:52 +01:00
Graeme Geldenhuys 86b87b6c27 feat(runtime): port blaise_weak.c to Pascal
Replace the C weak-reference table implementation with an equivalent
Pascal unit (blaise_weak.pas).  Same data structures — open-chained
hash table with linked slot lists — using _BlaiseGetMem/_BlaiseFreeMem
and ^Pointer locals for the void** dereference pattern.

Deletes blaise_weak.c; runtime C files now down to 4 (blaise_arc_class,
blaise_exc, blaise_float, blaise_str_fmt).

All 2023 tests pass including E2E weak-ref Valgrind test.  Fixpoint OK.
2026-05-19 16:11:14 +01:00
Graeme Geldenhuys d45f8196db feat(runtime): port blaise_io / blaise_process / blaise_sys / blaise_time to Pascal
Step 13 (partial): replace four C shims with self-hosted Pascal units that
bind directly to libc syscall surfaces. blaise_exc.c, blaise_arc_class.c,
blaise_weak.c, blaise_str_fmt.c, and blaise_float.c remain in C — they
either need setjmp/longjmp, function-pointer fields in records, or
variadic interfaces that QBE / Blaise cannot express today.

Ports:
- blaise_io.c → blaise_io.pas
  File I/O, env vars, working directory, ParamStr, mkstemp, sleep,
  process ID. Uses libc bindings (open/read/write/stat/...) declared in
  the interface section per the migration rule in CLAUDE.md.
- blaise_process.c → blaise_process.pas
  fork/exec/pipe/waitpid wrapped as TProcess; same libc-binding pattern.
- blaise_sys_posix.c → folded into blaise_sys.pas
  Tiny shim (just _SysWrite + _SysWriteNewline); now writes directly via
  posix_write. No separate C file needed.
- blaise_time.c → blaise_time.pas
  clock_gettime + date arithmetic. The local typed-array-const Days
  triggered the codegen bug fixed in fa69ae1.

Each unit is built via a thin build_driver.pas program (see existing
blaise_str_build_driver.pas pattern): the Makefile compiles the driver,
strips everything from the program section onward, then assembles + links
the unit-only IR into the runtime archive.

Outcome:
- C_SRCS shrinks from 9 → 5 files; runtime archive size unchanged in
  spirit but the dependency surface contracts.
- Standalone units, not yet folded into rtl.platform.posix.pas — that
  consolidation can happen later when the platform layer matures and a
  second backend (windows / darwin) appears to justify the abstraction.
- All 2023 tests pass; fixpoint OK.
2026-05-19 01:04:17 +01:00
Graeme Geldenhuys fa69ae1b1c fix(codegen): emit data items for typed array consts inside function bodies
A typed array constant declared inside a function body (or class method
body) had its element values held in TConstDecl.ArrayElements but no
`data $Name = { ... }` item was emitted in the data section.  The function
code referenced `$Name` (e.g. `add $Days, %_t`), which produced
`undefined reference to Name` at link time.

EmitGlobalConstData walked only the top-level program/unit blocks; method
bodies were never visited.  Add EmitLocalArrayConstsInBlock /
EmitLocalArrayConstsInTypeDecls / EmitLocalArrayConstsInProgram /
EmitLocalArrayConstsInUnit which sweep standalone procs and class/record
methods and emit any locally-declared typed array constants.

Wired into all four IR-emission sites: Generate / AppendProgram /
GenerateUnit / AppendUnit.  GenerateUnit was also missing the existing
EmitGlobalConstData calls for IntfBlock/ImplBlock — added those for
consistency.

Tests:
- IR: TestArrayConst_LocalInFunction_EmitsDataItem,
       TestArrayConst_LocalInProcedure_EmitsDataItem,
       TestArrayConst_LocalInMethod_EmitsDataItem assert the IR contains
       `data $Name =` for each scope.
- E2E: TestRun_Const_LocalArrayInFunction compiles+links+runs a program
       whose function uses a local typed array constant and reads three
       elements, verifying the full toolchain catches the link error.

Unblocks 36 previously-failing DateUtils e2e tests (blaise_time.pas
declared `Days: array[1..12] of Integer` inside _TimeDaysInMonth).
2026-05-19 00:55:55 +01:00
Graeme Geldenhuys 4b4e560b27 fix(codegen): fold single-char string literal in byte-store RHS
P[I] := #N (and any single-char string literal like 'A') in a byte-store
context lowered through the normal string-literal path, emitting a
1-byte string data item and storing the low byte of its data pointer
into the target slot — yielding the address byte, not Ord(N).

This corrupted NUL terminators written by RTL helpers such as StrAlloc
and StrFromCStr in blaise_io.pas, causing _FileExists (and any libc
function reading NUL-terminated strings) to see one byte of garbage
past the intended end.

Extend EmitByteRhs to recognise TStringLiteral of length 1 and emit
the Ord as a `=w copy N` constant, mirroring the existing Chr(N)
short-circuit. Both traps share the same root cause and now share
the same fix.

Tests:
- IR: TestCodegen_PCharSubscript_HashCharLiteralShortCircuit asserts
  the IR contains no `storeb $__s` or `add $__s` patterns for
  `p[0] := #0`.
- E2E: TestRun_PCharSubscript_HashCharLiteralAssignment compiles and
  runs a program that builds 'ABCD' + #0 via per-byte writes and
  verifies the resulting C string.
2026-05-18 22:17:43 +01:00
Graeme Geldenhuys 6d537238e7 feat(forin): extend for..in to support dynamic arrays
Dynamic arrays are now valid collection expressions in for..in loops.
Iteration is 0-based (Low=0, High=Length-1), guarded by a runtime
call to $_DynArrayLength so empty/nil arrays iterate zero times.

- uAST: add IsDynArrayIter flag to TForInStmt
- uSemantic: add tyDynArray branch (validates element/var type, injects
  synthetic __idx_N slot, same pattern as static array path)
- uCodeGenQBE: add IsDynArrayIter codegen path (csltw index < length,
  data-pointer arithmetic identical to static array)
- cp.test.forin: 7 new unit tests (semantic + IR codegen)
- cp.test.e2e.staticarray: 2 new E2E tests (filled array; empty/nil array)
2026-05-18 17:53:15 +01:00
Graeme Geldenhuys 19056cffbf fix(semantic): normalise all identifier references to declared casing
Pascal is case-insensitive but QBE symbols are case-sensitive.  The
parser stores source-level casing on AST nodes, and the codegen emits
those names as QBE symbols.  When a variable declared as 'inFile' was
referenced as 'infile.Size', the definition emitted $inFile but the
reference emitted $infile, causing a linker error.

The previous commit (952667e) fixed this for method call names only.
This commit extends the fix to all remaining AST name fields that flow
to QBE symbol emission: TFieldAccessExpr.RecordName, TFieldAssignStmt
.RecordName, TMethodCallExpr.ObjectName, TMethodCallStmt.ObjectName,
TProcCallStmt.Name, TFuncCallExpr.Name, TForStmt.VarName, TForInStmt
.VarName (both paths), TArrayAssignStmt.ArrayName, TSupportsExpr
.OutVarName, and the @-operator path in AnalyseAddrOfExpr.
2026-05-18 17:43:28 +01:00
Graeme Geldenhuys 7d4b852ec4 feat(config): add blaise.cfg for automatic unit-path discovery
The compiler now reads unit-path entries from a config file so users
no longer need to pass --unit-path for the runtime and stdlib on every
invocation.  Search order: next to the binary, then ~/.blaise.cfg.
Relative paths are resolved against the config file's directory.

Includes a dev-layout config in compiler/src/main/resources/ (PasBuild
copies it to compiler/target/ automatically), a new uConfig unit with
the parsing logic, and 12 unit tests for config parsing.
2026-05-18 17:22:24 +01:00
Graeme Geldenhuys 952667ee67 fix(codegen): use declared method name for zero-arg call symbols
Two code paths in EmitExpr built method-call symbols by concatenating
FldAccess.FieldName (the raw source-code casing) instead of routing
through MethodEmitName which resolves via ResolvedQbeName.  This caused
linker errors when a call site used different casing than the declaration
(e.g. Reader.readline vs ReadLine), since the emitted symbol name
wouldn't match the function definition.

Route both paths through MethodEmitName and add a fallback that uses
AMDecl.Name (the declared name) when ResolvedQbeName is unset.
2026-05-18 07:19:15 +01:00
Graeme Geldenhuys 196f992ec6 fix(semantic,codegen): accept High/Low on dynamic array types
High() and Low() rejected tyDynArray variables with a semantic error.
Added tyDynArray to the allowed-type sets in both builtins, and added a
tyDynArray codegen branch for High() that calls _DynArrayLength and
subtracts 1. Low() already falls through to the constant-zero path.
2026-05-17 21:01:17 +01:00
Graeme Geldenhuys 0300d84004 fix(semantic): reject implicit float↔integer assignment
Assigning a Double or Single to an Integer variable (or vice-versa)
was silently accepted because CheckTypesMatch treated all numeric types
as interchangeable.  This caused the raw bit-pattern of the float to
be reinterpreted as an integer, producing garbage values at runtime
(e.g. PI*2 assigned to Integer yielded 1413754136).

Split the numeric-widening rule into two:
- integers ↔ integers: still allowed (widening/narrowing)
- floats ↔ floats: allowed (Single ↔ Double promotion)
- float ↔ integer: now a type-mismatch error, matching FPC/Delphi

Callers that legitimately need float→integer conversion must use
Trunc, Round, Floor, or Ceil; integer→float conversion must use
Double() or Single() casts.

Fixes graemeg/blaise#33
2026-05-17 11:14:22 +01:00
Graeme Geldenhuys 8f692cb0f8 feat(math): add ArcSin, ArcCos, Sinh, Cosh, Tanh builtins with Single dispatch
Registers five new trig intrinsics in the symbol table, semantic analyser,
and codegen. Single arguments emit the *f libc variants (asinf, acosf,
sinhf, coshf, tanhf); Double arguments emit the unprefixed variants.
Removes the now-implemented section from docs/future-improvements.adoc.
2026-05-17 01:56:35 +01:00
Graeme Geldenhuys f1d18755e8 docs(grammar): add INT_LIT/FLOAT_LIT token rules with all bases and underscores
INT_LIT previously only covered plain decimal.  Expand the Tokens section
to formally define all four bases ($hex, &octal, %binary, decimal) and
the underscore-separator placement rules.  Add FLOAT_LIT as an explicit
token production (it was absent entirely).  Character-class helpers
(dec_digit, hex_digit, oct_digit, bin_digit) are defined in the
accompanying block comment, consistent with the existing style for
letter/char/any_char.
2026-05-17 01:11:39 +01:00
Graeme Geldenhuys 7b24fa754d benchmark: add literal underscores for better readability in code.
Unit comment block was stale and still referenced the old values.
2026-05-17 01:05:28 +01:00
Graeme Geldenhuys 772bf55eee feat(lexer,parser): octal/binary literals and underscore separators in numbers
Octal (&377) and binary (%11111111) integer literals were tokenised
correctly but silently misconverted at parse time because StrToInt64
does not accept & or % prefixes.  A new ParseIntLiteral helper in
uStrCompat handles all four bases (decimal, $hex, %binary, &octal)
with explicit digit-by-digit conversion for binary and octal.

Numeric literals now also accept underscore separators between digits
(1_000_000, $FF_EC, %0010_0101, &3_77, 3.14_15) following the same
rules as Delphi and FPC: underscores may appear only between digits,
not at the start of the digit run, not adjacent to a decimal point, and
not at the end of a literal.  The tokeniser (uPasTokeniser) accepts the
underscore in all four number branches; ParseIntLiteral and
StripUnderscores (for float values) strip them before conversion.

Fixed a secondary bug in uLexer: the float/int classification checked
for 'e'/'E' anywhere in the token, which caused hex literals containing
those letters (e.g. $FF_EC) to be misclassified as tkFloatLit.  The
check is now gated on the absence of a $ / % / & prefix.

uStrCompat rewritten without {$IFDEF FPC} dead code; all callers
compiled by Blaise only since v0.8.0.

Adds 21 new tests: lexer token shape tests for all bases with/without
underscores, and TParseIntLiteralTests covering conversion correctness
and invalid-placement rejection.
2026-05-17 00:56:29 +01:00
Graeme Geldenhuys a9bfd37ab7 fix(compiler): report missing units even when no --unit-path is given
The unit-loading block was guarded by `SearchPaths.Count > 0`, so a
program with a `uses` clause compiled silently when no search paths were
provided — missing units were never checked (issue #31).

Remove the spurious count guard so TUnitLoader.LoadAll is always called
when the program has used units. With an empty search-path list, Locate
returns '' for every unit name and EUnitNotFound is raised as expected.

Adds a regression test: TestUnitLoader_MissingUnit_NoSearchPaths_RaisesError.
2026-05-17 00:16:32 +01:00
Graeme Geldenhuys 08beb33689 fix(semantic): reject duplicate identifiers across const and var sections
A var declaration shadowing a same-block const (or a const declared
twice in the same block) was silently accepted instead of raising a
Duplicate identifier error.

Root cause: AnalyseBlock registers consts in the outer scope then pushes
a new inner scope before calling AnalyseVarDecls, so FTable.Define
(which only checks the current scope) could not detect the clash.

Fix: AnalyseVarDecls now scans the block's own ConstDecls list before
defining each var name.  AnalyseConstDecls now treats a duplicate as an
error when the collision originates from an earlier const in the same
block; cross-unit const shadowing (e.g. a unit redefining a system.pas
constant) continues to be silently accepted.

Also removes the redundant LineEnding const from blaise.testing.pas —
it is already exported by system.pas.

Fixes: https://github.com/graemeg/blaise/issues/30
2026-05-17 00:02:18 +01:00
Graeme Geldenhuys c17a8b027c chore: begin v0.9.0-dev cycle 2026-05-16 18:42:13 +01:00
Graeme Geldenhuys 02656bc7eb release: v0.8.0
First release with a Pascal-native memory allocator: the compiler RTL,
codegen builtins, and all C runtime helpers now route through blaise_mem
(_BlaiseGetMem / _BlaiseFreeMem / _BlaiseReallocMem) instead of libc
malloc.  Self-hosting fixpoint achieved on the multi-file source —
stage-2 and stage-3 IR both 135,311 lines, byte-identical.
2026-05-16 18:41:53 +01:00
Graeme Geldenhuys da2c5d2459 docs(benchmark): post-cutover measurements and libc baseline bench
Add bench_libc_malloc.pas with explicit external malloc/free/realloc
bindings so the libc baseline can still be measured after the cutover.
The original bench_blaise_mem.pas now measures blaise_mem too (because
the GetMem builtin emits _BlaiseGetMem), so it is no longer suitable
as a baseline.

Log the post-cutover state: blaise_mem now beats libc on small, mixed,
and retain-free-all workloads.  Realloc growth remains 1.6x.  The
compile-time win on the real test suite (~10%) exceeds the microbench
gap, suggesting real workloads are dominated by short-lived small
allocations where the freelist pop/push beats glibc tcache.
2026-05-16 18:36:55 +01:00
Graeme Geldenhuys d7e9caaf86 feat(rtl,codegen): cut over to blaise_mem allocator
Replace libc malloc/free/realloc with _BlaiseGetMem/_BlaiseFreeMem/
_BlaiseReallocMem across the full string + class + dynamic-array path:

Pascal RTL:
  blaise_str.pas — StrAlloc, _IntToStr, _Int64ToStr, _DynArraySetLength
  blaise_arc.pas — _StringRelease, _StringConcat, _ClassFree

C RTL (string/class allocators only — internal scratch buffers
        remain libc-paired):
  blaise_arc_class.c — _ClassAlloc (calloc -> _BlaiseGetMem + memset)
                       _ClassRelease (free -> _BlaiseFreeMem)
  blaise_str_fmt.c   — _StringFormat result
  blaise_io.c        — io_str_alloc (ParamStr, ReadFile, etc.)
  blaise_exc.c       — exc_str_from_cstr
  blaise_float.c     — blaise_alloc_str
  blaise_process.c   — proc_str_alloc

Codegen:
  uCodeGenQBE.pas — GetMem/FreeMem/ReallocMem builtins now emit
                    $_BlaiseGetMem/$_BlaiseFreeMem/$_BlaiseReallocMem.

Tests updated to match the new IR substrings:
  cp.test.pointers, cp.test.tlist, cp.test.tstack, cp.test.tqueue,
  cp.test.tset, cp.test.collections.

All 1934 tests pass; fixpoint clean.
2026-05-16 18:01:57 +01:00
Graeme Geldenhuys a2c55be812 feat(rtl): sanity-check string headers in _StringRelease
When _StringRelease decrements the refcount, also validate that the
header looks plausible: refcount >= -1 (else double-free), length in
[0, 1 GiB], and capacity >= length and below 1 GiB.  On corruption,
write a clear message to stderr and abort instead of returning to
the wild segfault that would follow.

Added so the upcoming blaise_mem cutover investigation has a clear
failure signal when string ownership crosses an allocator boundary.
Benign on healthy programs.

All 1934 tests pass; fixpoint clean.
2026-05-16 17:47:18 +01:00
Graeme Geldenhuys b815793443 fix(codegen): use storel/loadl for tyPChar elements in static arrays
Static-array element stores omitted tyPChar from the storel case,
falling through to the default storew (32-bit). Writing a PChar
heap pointer through Arr[I] := P silently truncated to its low 32
bits; reading Arr[I] later produced a wild pointer and segfault.

Add tyPChar to all five static-array load/store case statements
(EmitGlobalVarData, EmitConstArrayElem load, open-array load,
dynamic-array load, static-array store, and array-literal alloc/
store). The default branches were already 'l' or 'storel' so no
silent misalignment elsewhere.

Tests:
- cp.test.e2e.pointers.TestRun_StaticArrayOfPChar_ElementPreservesAllBits
  asserts a round-trip through array[0..1] of PChar preserves the
  full 64-bit pointer.

All 1934 tests pass; fixpoint clean.
2026-05-16 17:21:13 +01:00
Graeme Geldenhuys 283078038b fix(codegen): short-circuit Chr(N) in byte-store contexts
P[I] := Chr(N) used to emit `call $_Chr` (which allocates a heap string)
and then storeb of the pointer's low byte, yielding garbage instead of
N. Same bug applied to dynamic-array, static-array, and pointer-deref
byte stores.

Add EmitByteRhs helper that detects Chr(N) and emits the argument
directly as a w-typed value. Use it at all four storeb sites:
- PChar subscript write (P[I])
- dynamic-array byte/bool subscript write
- static-array byte/bool subscript write
- pointer write to byte/bool typed pointer (P^ :=)

Tests:
- cp.test.pchar.TestCodegen_PCharSubscript_ChrByteShortCircuit
  asserts the IR has storeb and no call $_Chr
- cp.test.e2e.pointers.TestRun_PCharSubscript_ChrAssignment
  asserts ABCD output instead of garbage

All 1933 tests pass.
2026-05-16 17:10:04 +01:00
Graeme Geldenhuys a8a442e5f7 docs(benchmark): log post-phase-2-inlining R workload improvement
R workload moved 10-11 ms -> 8 ms (-20 to -27%) after enabling case
statements and larger bodies in the inliner.  M and H also improved
modestly.  S is essentially unchanged.  R now 1.6x malloc, down from
2.1x.
2026-05-16 16:37:36 +01:00
Graeme Geldenhuys 13bfbd3817 feat(semantic): allow case statements and larger bodies in inline candidates
Phase 2 of the inliner: extend the analyser to walk TCaseStmt branches
(was previously a hard-reject) and raise the body-statement cap from
8 to 24. This unlocks SizeClassIndex (8 if/then/else) and SizeClassBytes
(case) for inlining inside blaise_mem.

All 1931 tests pass.
2026-05-16 16:31:30 +01:00
Graeme Geldenhuys f387bbf2b7 docs(benchmark): log post-inlining state + bootstrap refresh
Records the cumulative effect of the IsLarge fix, pointer-promotion
codegen, inline-candidate analyser, and leaf-function inlining
landed this session.  Bootstrap binary refreshed in-place;
releases/v0.8.0/blaise is now the verified stage-3 fixpoint of
the current source, so fixpoint converges at stage-2/3.

Headline numbers vs the original 2026-05-16 baseline:
  Small alloc/free:    14 -> 9 ms  (-36%)
  Mixed sizes:          8 -> 5 ms  (-38%)
  Realloc growth:      13 -> 10 ms (-23%)
  Large alloc/free:    33 -> 0 ms  (matches malloc)
  Retain + free-all:    5 -> 5 ms  (unchanged)
2026-05-16 14:45:37 +01:00
Graeme Geldenhuys c16249ae18 feat(codegen): inline small leaf functions at call sites
Implements phase 1 inlining per docs/inlining-design.adoc.  At each
TFuncCallExpr emission, if the callee's IsInlineCandidate flag is
set and the call shape qualifies, the codegen re-runs the body's
statements with a per-call-site name-remap frame instead of
emitting a regular `call $name(...)` instruction.

Mechanism:
  - TCodeGenQBE gets a stack of inline frames (FInlineParamNames,
    FInlineParamTemps, FInlineResultTemps, FInlineEndLabels,
    FInlineResultQTypes, FInlineDepth).
  - TryEmitInlineCall evaluates each argument to a caller-side temp,
    allocates an inline-result alloc4/alloc8 slot, pushes a frame,
    walks the callee body, then loads the result-slot into a fresh
    temp for the caller.
  - EmitExpr(TIdentExpr) consults the active frame: parameter names
    remap to argument temps; references to Result load from the
    inline-result slot.
  - EmitAssignment to Result stores into the inline-result slot.
  - EmitStmt(TExitStmt) inside an inline frame jumps to the
    per-call-site @inline_end_N label instead of @func_exit.

The inline-result is a stack slot (not a register temp) so that
multiple writes inside conditional branches of the inlined body
do not violate QBE's SSA model when the call result later feeds a
phi (e.g., the RHS of a short-circuit `and`).

Inlining is suppressed in two cases to avoid QBE-level hazards:
  - Inside loop bodies (FBreakLabels/FContinueLabels non-empty).
    Each inline call emits alloc4/alloc8 at the call site; QBE
    materialises non-@start allocs as a dynamic stack bump, which
    accumulates 8 bytes per iteration and overflows on hot loops
    like the benchmark's 1M-iteration small-alloc workload.
  - Inlining depth >= 2.  Caps blowup from chained inlines.

Analyser refinements (uSemantic.pas):
  - AssignmentTargetsParameter rejects functions that assign to a
    parameter; the simple inliner does not yet remap parameter writes.

Test impact:
  - cp.test.procs.TestCodegen_FuncCall_EmitsTypedCall asserted
    `call $Add` substring on a function that now inlines.  Added a
    local variable to Add so the function falls out of the inline
    candidate set, restoring the test's intent (verify regular
    call emission).  The existing test stays valid as a regression
    check for the non-inline call path.

Validation:
  - 1931 tests pass (full test suite).
  - Fixpoint clean at stage-3/stage-4.
  - bench_blaise_mem_custom.pas R workload: 12-13 ms -> 10-11 ms
    (-15%).  S workload: 10 ms -> 9 ms.  No regressions on M/L/H.
  - Stage-2 IR grew from 132450 to 135012 lines from inlined bodies.
2026-05-16 14:27:27 +01:00
Graeme Geldenhuys 314b537f68 feat(semantic): mark inline candidates after body analysis
Adds the inlinability analyser per docs/inlining-design.adoc phase 1.
After all standalone bodies are analysed, MarkInlineCandidates walks
each TMethodDecl and sets IsInlineCandidate when the function meets
all of:

  - has a body (not external, not forward)
  - not a class/record method (phase 1 scope)
  - not a generic template
  - not virtual or abstract
  - primitive-only return type (int family, float, pointer, PChar)
  - primitive-only by-value parameters (no var/open-array/interface)
  - no local declarations (only the implicit Result)
  - body contains no loops, try/except/finally, raise, break/continue,
    case, method calls, pointer-write, or self-recursion
  - body has at most 8 statements (MAX_STMTS)

Codegen does not yet consume IsInlineCandidate — that lands in a
follow-up commit.  This commit only adds the analyser plus its AST
field; semantic and codegen behaviour is otherwise unchanged.

Validation:
  - 1931 tests pass (full test suite).
  - Fixpoint clean at stage-3/stage-4.
  - Stage-2 IR size unchanged (since codegen doesn't read the flag yet).
2026-05-16 13:22:10 +01:00
Graeme Geldenhuys b4ec7121ca feat(codegen): promote tyPointer and tyPChar locals to QBE temps
Extends the existing mem2reg-style local-promotion pass to cover
pointer-typed locals (Pointer, PChar, and user-typed pointer kinds
like PArena, PBlockHeader).  Previously every local pointer routed
through an alloc8 stack slot with loadl/storel on each access; now
non-address-taken pointer locals live as direct QBE temporaries,
letting QBE's own SSA conversion + register allocator keep them in
registers.

Patches the PChar and dynamic-array subscript-write paths in
EmitIndexedAssign to consult IsPromoted before emitting loadl —
those were the only loadl sites for local user names not already
guarded by an IsPromoted check.  All other loadl %_var_X sites
target parameter slots, Self, Result, or var-param slots, none of
which are part of phase A promotion.

tyClass, tyMetaClass, tyString remain excluded — they thread
through ARC retain/release sites that need a separate audit
(phase B).

Validation:
  - 1931 tests pass (full test suite via pasbuild).
  - fixpoint clean (achieved at stage-3/stage-4).
  - 28/28 punit memory-allocator correctness tests pass.
  - bench_blaise_mem_custom.pas shows no regression (S=10, M=5,
    R=12-13, L=0, H=5 ms).
  - Generated stage-2 IR shrunk by 68 lines (allocated slots and
    matching load/stores eliminated).

The microbenchmark gain is below the millisecond resolution of the
current timer, but the change is foundational: every subsequent
codegen improvement (e.g. inlining of leaf functions) now operates
on register-resident pointer locals rather than memory traffic.
2026-05-16 12:31:16 +01:00
Graeme Geldenhuys 3e039bccea docs(benchmark): log codegen-limited finding on in-place realloc
Records the negative result from today's in-place arena-tail growth
experiment in blaise_mem: 100% hit rate but a 2x regression
(13 ms -> 23 ms) on the R workload.  Root cause is Blaise's
codegen lacking inlining and register allocation for locals — the
added checks cost more memory traffic than the saved memcpy of
16-128 byte payloads.

The note documents the lesson so future allocator-perf work can
skip re-doing the experiment: closing the malloc gap on
small-realloc workloads requires compiler-side improvements
(inlining + register allocation) first.
2026-05-16 10:29:07 +01:00
Graeme Geldenhuys aa712f1104 test(rtl): expand blaise_mem correctness coverage from 14 to 28 tests
Adds 14 new punit tests covering allocator edges that the original
suite did not exercise:

  - SizeClass_Boundaries     — every class boundary +/-1
  - Realloc_SameClass        — in-place when class is unchanged
  - Realloc_Shrink           — preserves bytes across class shrink
  - Realloc_Grow_Steps       — preserves bytes across stepwise grow
  - Realloc_SmallToLarge     — crosses LARGE_THRESHOLD upward
  - Realloc_LargeToSmall     — crosses LARGE_THRESHOLD downward
  - Realloc_Large_SamePage   — same-page-count fast path
  - Realloc_Large_GrowPages  — mremap path
  - Large_Cache_Reuse        — LIFO cache returns the freed block
  - Large_Cache_Eviction     — workload exceeds LARGE_CACHE_MAX
  - Retain_Many_Small        — pattern preservation across 200 blocks
  - Interleaved_Mixed        — small+large interleaved
  - Realloc_Churn            — repeated grow/shrink preserves bytes
  - Alignment_All_Classes    — 8-byte alignment for every class

Each grow/shrink test fills a known byte pattern and asserts every
byte survives the realloc.  All 28 tests pass against the current
blaise_mem after the IsLarge() fix.
2026-05-16 10:18:28 +01:00
Graeme Geldenhuys 9847369e71 fix(rtl): restore large-alloc LIFO cache in blaise_mem
IsLarge() was reading the small-header Flags field at offset Ptr-4,
but TLargeHeader laid its AllocSize: Int64 across Ptr-8..Ptr-1, so
the Flags probe overlapped with the high half of AllocSize and was
always zero.  Every large block therefore routed through the small
free path and the LIFO cache was never populated, forcing a fresh
mmap on each large allocation.

Restructured TLargeHeader to:
  TotalMapped: Int64    (Ptr-16..Ptr-9)
  AllocSize:   Integer  (Ptr-8..Ptr-5)
  Flags:       Integer  (Ptr-4..Ptr-1)

LargeGetMem now writes Flags := FLAG_LARGE, IsLarge() returns the
correct value, and the cache reaches ~100% hit rate on the large
alloc/free workload (32 ms -> 0 ms for 10k x 64KB).

Also adds the two benchmark programs (bench_blaise_mem.pas for the
malloc baseline and bench_blaise_mem_custom.pas for blaise_mem) and
reformats docs/benchmark.txt as a dated log so future runs can be
tracked over time.
2026-05-16 10:09:20 +01:00
Graeme Geldenhuys b1cb9d05bb feat(rtl): add Pascal memory allocator (blaise_mem) backed by mmap
Arena-based allocator with per-size-class freelists for small blocks
(up to 2048 bytes) and direct mmap/munmap for large blocks. All
returned pointers are 8-byte aligned. Self-contained: no dependency
on strings, ARC, or stdlib.

14 punit tests covering basic alloc/free, realloc, alignment, size
classes, large allocations, and freelist reuse. Allocator is in the
RTL archive but nothing depends on it yet — GetMem/FreeMem still
route to libc malloc/free.
2026-05-16 01:42:17 +01:00
Graeme Geldenhuys ba38aefca3 fix(tests): update stale 'rtl' directory check to 'runtime' in E2E tests
Two test files had their own ProjectRoot functions still looking for
'rtl/' instead of 'runtime/', causing the directory walk to fail and
all 7 E2E tests in those files to report <toolchain-missing>.
2026-05-16 01:17:03 +01:00
Graeme Geldenhuys 48b6773eff refactor(rtl): port path manipulation functions from C to Pascal
Move seven pure-string path functions (_ChangeFileExt, _ExtractFileName,
_ExtractFilePath, _ExtractFileDir, _ExtractFileExt,
_IncludeTrailingPathDelimiter, _ExcludeTrailingPathDelimiter) from
blaise_io.c to blaise_str.pas, reducing C code by ~130 lines while
keeping all OS-level functions (rmdir, rename, chdir) in C.
2026-05-16 01:13:00 +01:00
Graeme Geldenhuys 923b94c541 refactor: split rtl/ into runtime/ + stdlib/, rename bcl.testing to blaise.testing
Separate always-linked runtime code (system.pas, ARC, strings, platform
abstraction) from opt-in standard library units (sysutils, classes, math,
dateutils, generics, etc.).  This mirrors the Go runtime/ vs stdlib, Rust
core vs std, and Swift SwiftRuntime vs SwiftCore patterns.

- runtime/ contains code linked into every binary + C shims
- stdlib/ contains units imported explicitly via uses clause
- Single platform abstraction layer (rtl.platform.*) in runtime/,
  shared by both runtime/ and stdlib/ units
- bcl.testing renamed to blaise.testing across all 95+ test files
- Updated PasBuild project.xml, fixpoint.sh, README, unit search paths
- All 1931 tests pass, fixpoint OK (stage-3/stage-4)
2026-05-16 00:51:18 +01:00
Graeme Geldenhuys 3123ecff4d docs: add testing strategy document
Captures the three-layer test architecture (compiler IR, compiler E2E,
library runtime) and the rationale for framework selection: blaise.testing
for compiler and stdlib tests, punit for low-level runtime tests.
2026-05-16 00:38:58 +01:00
Graeme Geldenhuys c0149496b6 feat(compiler): record methods + DateUtils RTL (five-type date/time model)
Records can now declare methods alongside fields.  Self is passed by
pointer (var-param at the QBE ABI level) so the language preserves value
semantics while methods can mutate the caller's record.

Compiler changes:
- AST: TRecordTypeDef.Methods; TMethodDecl.IsRecordMethod flag
- Parser: ParseRecordDef accepts function/procedure declarations
- Semantic: record methods analysed with Self as skVarParameter; tyRecord
  allowed as method-call receiver; Int64 operand promotes binary result
  type (fixes -Int64 truncation in DurationNegate)
- Codegen: EmitMethodDefs and AppendUnit emit record method bodies;
  EmitFieldAssignment handles record-typed fields via EmitRecordCopy or
  EmitRecordCallSret; EmitRecordCopy recurses for nested records;
  method-call sites distinguish record var-params from class variables
  when computing Self

RTL — new DateUtils unit (replaces TDateTime-as-Double):
- TDate, TTime, TDateTime: integer calendar fields, no timezone
- TInstant: signed Int64 nanoseconds since Unix epoch (UTC point-in-time)
- TDuration: signed Int64 nanoseconds (interval)
- TTimeZoneOffset: fixed +HH:MM/-HH:MM (full IANA tzdata deferred)
- C shim blaise_time.c uses clock_gettime, gmtime_r, timegm
- 36 e2e tests, fixpoint clean
2026-05-15 19:46:46 +01:00
Graeme Geldenhuys 336033ca2e feat(rtl): TComponent + TStringList.CustomSort + CommaText
Add TComponent to classes.pas (Name, Owner, Components[], ComponentCount,
Create(AOwner) ownership pattern — Destroy frees children automatically).
Children stored as a raw ^Pointer array to avoid a circular dependency with
generics in the same unit.

Add TStringList.CustomSort (bottom-up merge sort with caller-supplied
TStringListSortCompare function; strings and paired objects move together)
and CommaText property (getter quotes items containing commas/spaces/
double-quotes; setter parses comma-separated text with double-quote grouping).

6 new e2e tests in cp.test.e2e.tcomponent.pas; 4 new e2e tests in
cp.test.e2e.tstringlist.pas. 1895 tests pass; fixpoint OK.
2026-05-15 13:49:19 +01:00
Graeme Geldenhuys a86b0b1f11 feat(compiler): class-level array constants (const inside class body)
Support array-typed constants declared inside a class definition, matching
FPC behaviour.  Access works via instance (T.Days[I]), type-qualified
(TMyDays.Days[I]), or unqualified within class methods.  Class array
consts are emitted as global data with a mangled label (ClassName_Name)
to avoid collisions with unit-level consts of the same name.
2026-05-15 08:58:57 +01:00
Graeme Geldenhuys 51fdcad4b0 feat(compiler): range-indexed array constants (array[0..N] of T = (...))
The typed-constant parser only accepted enum-indexed arrays. Extend the
parser, semantic analyser, and AST to support integer-range indices so
`const X: array[0..6] of string = (...)` works alongside the existing
enum form. Update grammar.ebnf with the new production.
2026-05-15 08:31:46 +01:00
Graeme Geldenhuys 77534562a5 fix(compiler): generic vars at unit scope + non-identifier interface args
Two compiler gaps fixed:

1. Generic instantiation at unit-global var scope now works. Unit-level
   var declarations (both interface and implementation sections) use
   FindTypeOrInstantiate instead of FTable.FindType, matching local var
   behaviour. TUnit gains GenericInstances lists; GenerateUnit emits
   their typeinfo, vtables, and method bodies.

2. Non-identifier interface argument expressions (e.g. `F(Obj as IFoo)`)
   now compile correctly. New EmitInterfaceExprPair helper decomposes
   TIdentExpr and TAsExpr into obj+itab temps. All call sites in
   EmitProcCall and EmitExpr updated. Unsupported forms (field access)
   produce a clear error naming the expression type.
2026-05-15 08:05:50 +01:00
Graeme Geldenhuys a33d5aa879 docs(streams): record Stream I/O design rationale; remove from roadmap
Adds a "Stream I/O" section to docs/language-rationale.adoc covering:
the decision (Go/Okio-inspired shape, one-direction abstract bases plus
capability interfaces), rationale (cross-language survey lessons from
Java/Go/Rust/Okio/.NET/Python), why capability interfaces sit alongside
abstract classes (TBuffer as both source and sink), UTF-8 only in v1,
alternatives rejected (single-TStream root, TFilterStream decorator,
async-from-day-one), and TODOs flagged in code (segment-pool thread
safety, CopyStream fast paths, non-identifier interface arguments).

Removes the "Formal TStream decorator hierarchy" section from
docs/future-improvements.adoc — the streams subsystem is now
implemented across phases 1-5 (releases of the past few commits).
The implementation departs from the sketched TStream/TFilterStream
shape in favour of one-direction abstract bases + capability
interfaces, for the reasons documented in the rationale.

./scripts/fixpoint.sh clean.
2026-05-15 01:10:29 +01:00
Graeme Geldenhuys dade93c7cc feat(rtl): Phase 5 streams — CopyStream + capability markers; interface ABI fixes
Adds CopyStream(Src: TInputStream; Dst: TOutputStream): Int64 — the
free function callers reach for whenever they want to drain one stream
into another.  Discovers capability at runtime in this preference order:

  1. Dst implements IReaderFrom  → dispatch to Dst.ReadFrom(Src).
  2. Src implements IWriterTo    → dispatch to Src.WriteTo(Dst).
  3. Fallback: 8 KiB byte-loop through a stack buffer.

The two marker interfaces ship empty in v1 — no concrete fast-path
implementations yet.  Adding IReaderFrom to TFileOutputStream (with
sendfile(2) on Linux) or IWriterTo to TBuffer (with TransferTo) is a
future, non-breaking change: existing CopyStream callers automatically
pick up the speedup.  This is the Go io.Copy idiom.

Streams unit cleanup: every concrete class now explicitly lists the
interfaces it implements (IInputStream / IOutputStream / ICloseable),
which was missing in earlier phases.  Required for the new
Supports() queries to work correctly.

Compiler fixes — interface ABI and arg analysis:

  * AnalyseMethodCall (statement form): the tyInterface branch never
    called AnalyseExpr on argument expressions, so codegen later
    crashed when an arg like @Buf[0] referenced a TIdentExpr with
    nil ResolvedType.  Fixed by calling AnalyseExpr on each arg
    (overload validation isn't possible without param info, but
    type resolution is needed).

  * Interface parameters now use a two-slot fat-pointer ABI:
    `function F(I: IFoo)` lowers to `function $F(l %_par_I_obj, l
    %_par_I_itab)`, with matching `_var_I_obj` / `_var_I_itab`
    local slots in the prologue.  Call sites for TIdentExpr args
    decompose `Os` into `l (loadl _var_Os_obj), l (loadl
    _var_Os_itab)`.  Previously interface params were emitted as a
    single `w` slot (QBE rejected the storel into them).

    Remaining gap: non-identifier interface arguments (function
    returns, casts, field accesses) raise ECodeGenError with a
    clear message.  Tracked in the project's perf-focus memory note.

Test coverage: four new e2e cases —
  * Interface dispatch via Supports (the inline Phase 1 workaround
    pattern, now using real interface dispatch).
  * Interface as function parameter (exercises the new ABI).
  * CopyStream memory → memory and file → file (both through the
    fallback byte-loop path).

Suite is now 1868 (was 1864); ./scripts/fixpoint.sh clean.
2026-05-15 00:54:27 +01:00
Graeme Geldenhuys 395175bf7c feat(rtl): Phase 4 streams — TBuffer segment rope + byte deref fixes
Adds TBuffer — an Okio-inspired segmented in-memory buffer that is
both an IInputStream and an IOutputStream.  Internal representation
is a singly-linked list of fixed-size 8 KiB segments drawn from a
module-level intrusive freelist.

The defining operation is TransferTo(Dst, Count): whole segments are
unlinked from this buffer's head and re-linked onto Dst's tail, with
no byte copy.  Multi-layer pipelines (decode → buffer → forward) drop
from N memcpys per byte to ~1.  Partial-prefix moves and unaligned
slice boundaries still memcpy, but only for the head segment.

IndexOf(B) scans without consuming bytes — useful for line/frame
parsers that need look-ahead before deciding how many bytes to
consume.

TBuffer implements both IInputStream and IOutputStream directly,
which is the case the capability-interface design (over
single-inheritance abstract classes) was built for.

TODO: the segment pool is currently single-threaded.  Pool ops need a
mutex or per-thread freelist once Blaise gains threads.  Documented
in the unit; tracked in the project's perf-focus memory note.

Compiler fixes — byte pointer dereference and store:

  TDerefExpr lowering for ^Byte / ^Boolean was emitting `loadw`
  (4-byte load), corrupting values with adjacent memory.  Symmetric
  bug in TPointerWriteStmt used `storew`, clobbering 3 bytes past
  the target.  Both paths now select `loadub` / `storeb` when the
  pointed-to type is tyByte or tyBoolean.

  TBuffer.IndexOf is the regression that surfaced these: scanning
  byte-by-byte through `PB^ = B` read junk because the load was
  reading 4 bytes and the comparison missed.  Without the fix,
  IndexOf(65) returned -1 instead of 0 for a buffer starting with
  byte 65.  Fix verified with both the original IndexOf case and a
  minimal `var Buf: array[0..3] of Byte; PB^ := 42` reproducer.

Test coverage: three new e2e cases — write+read round-trip,
TransferTo prefix split, IndexOf hits and misses.  Existing 1861
tests still pass; suite is now 1864.

Verified: ./scripts/fixpoint.sh clean.
2026-05-15 00:30:52 +01:00