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)
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.
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).
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.
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.
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.
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.
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.
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>.
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.
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)
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.
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
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.
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.
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.
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.
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.
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.
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.
Adds two text-layer decorators over the byte streams.
TStreamReader — ReadLine, ReadAll, EndOfStream. Line terminators on
read are tolerant (LF, CRLF, lone CR all end a
line); returned strings exclude the terminator.
EndOfStream peeks one byte so callers can distinguish
a genuinely empty line from end-of-input.
TStreamWriter — WriteString, WriteLine. WriteLine appends LF;
matches LineEnding/sLineBreak in system.pas.
UTF-8 only in v1. The Encoding parameter is intentionally not exposed
yet — Blaise's string type is already UTF-8, so the wrappers are a
pass-through. Other encodings deferred until there is a real consumer.
Composition model is explicit, like Okio: pass a TBufferedInputStream
between the file and the reader for line-oriented work. TStreamReader
itself reads byte-by-byte from its inner; the buffered layer is what
makes it cheap. Documented in the unit's TStreamReader doc comment
with an example.
OwnsInner=True (default) tears the whole chain down on Close. Close
on the writer side flushes through any buffered layer below it before
closing.
Test coverage: three new e2e cases —
* Full 4-layer pipeline round-trip (writer → buffered → file → file
→ buffered → reader → application), 3 lines.
* Mixed line endings (LF, CRLF, lone CR, no terminator) all parse to
4 lines with terminators stripped.
* ReadAll returns the entire remaining stream.
Suite is now 1861 (was 1858); ./scripts/fixpoint.sh clean.
Adds TBufferedInputStream and TBufferedOutputStream — decorators that
wrap any TInputStream/TOutputStream and reduce per-call overhead by
batching reads/writes through an internal 8 KiB byte buffer (size
configurable via overloaded constructor).
Ownership model: OwnsInner=True by default — closing the buffered
wrapper closes the inner too. OwnsInner=False keeps the inner alive
past the wrapper's Close (for the helper-returns-wrapper pattern).
Buffered output always flushes on Close before tearing down the chain,
so buffered data is never silently lost.
Error propagation: introduces EStreamError (extends SysUtils.Exception).
File open / read / write / short-write failures now raise instead of
returning silent failure codes — closing the Rust BufWriter loophole
called out in the design doc. ARC-driven Destroy still swallows the
error (a finaliser cannot meaningfully propagate); callers who care
must Close explicitly inside a try-finally.
Compiler fix — virtual dispatch on receiver expressions:
EmitMethodCall's "call on arbitrary receiver expression" branch
always emitted a static call (line ~3500), even when the method was
virtual. This broke the moment a buffered wrapper called
`Self.FInner.Write(...)` against a TOutputStream-typed field whose
concrete instance was a TFileOutputStream — the static call resolved
to \$TOutputStream_Write, which doesn't exist (abstract). Fix mirrors
the same vtable-load sequence used by the IsImplicitSelf and
ObjectName branches: when MDecl.VTableSlot >= 0, load vptr from
Self[0], index by slot, and indirect-call the resolved fptr.
Test coverage: three new e2e cases in cp.test.e2e.streams —
buffered file round-trip, flush-on-close (Close drains the buffer
without explicit Flush), and missing-file open raises EStreamError.
Existing 1855 tests still pass; suite is now 1858.
Verified: ./scripts/fixpoint.sh clean.
Adds the Streams unit (rtl/src/main/pascal/streams.pas) — Phase 1 of the
new stream I/O subsystem. Departs from the FPC/Delphi single-TStream
hierarchy: separate TInputStream and TOutputStream abstract bases keep
each class single-purpose, while ICloseable / IInputStream / IOutputStream /
ISeekable capability interfaces let consumers accept "anything readable"
without forcing single inheritance.
Ships in this phase: TFileInputStream, TFileOutputStream, TMemoryInputStream,
TMemoryOutputStream, plus fd-level C primitives (_FdOpenRead/Write/Append,
_FdRead, _FdWrite, _FdSeek, _FdSize, _FdClose) in blaise_io.c.
Compiler change — abstract method tombstones:
When an abstract class declares an interface, its itab references method
symbols that have no body on that class. The unit-emission path for
vtables silently emitted these symbols (latent bug in rtl.platform.pas,
hidden because every consumer overrode every method); itab emission did
the same. Both paths now route abstract slots to $_AbstractMethodError,
a new C-RTL tombstone in blaise_arc_class.c that abort()s if dispatched.
Moving the stub to C also dedupes it across program + unit translation
units, so the linker sees exactly one definition.
Test coverage: cp.test.streams (IR-level assertions on abstract-class
itab/vtable emission) and cp.test.e2e.streams (compile+run programs that
exercise the streams through the real RTL). Existing abstract-method
vtable tests in cp.test.vtable updated to assert against the new
$_AbstractMethodError symbol name.
Verified: all 1855 unit/e2e tests pass; ./scripts/fixpoint.sh clean.
Implements the 'abstract' method directive, following FPC/Delphi
semantics:
procedure Draw; virtual; abstract;
Abstract methods must not have an implementation body (semantic
error if one is provided). A class that has or inherits any
un-overridden abstract method is implicitly abstract and cannot
be instantiated (compile-time error on TFoo.Create).
Abstract vtable slots point to a $__abstract_method_error stub
(calls abort()) as a defence-in-depth measure.
Changes:
uAST.pas: IsAbstract field on TMethodDecl
uSymbolTable.pas: IsAbstract on TVTableEntry; HasAbstractMethods
on TRecordTypeDesc; CopyVTableFrom now propagates IsAbstract
uParser.pas: 'abstract' directive sets IsAbstract (and IsVirtual)
uSemantic.pas: abstract slot tracking in vtable pre-pass;
post-vtable check for inherited-but-unoverridden abstract slots;
AnalyseMethodDecl rejects abstract methods with bodies;
TFoo.Create raises ESemanticError for abstract classes (both
constructor call sites)
uCodeGenQBE.pas: EmitVTableDefs emits $__abstract_method_error
stub when needed; abstract slots reference it in the vtable
cp.test.vtable.pas: 9 new tests covering parse flags, semantic
validation, and codegen. 1846 tests pass. Fixpoint OK.
Supports constants of the form:
const Names: array[TEnum] of string = ('A', 'B', 'C');
const Costs: array[TDir] of Integer = (1, 1, 2, 2);
The index type must be an enum; element count must match enum member
count (semantic error otherwise). Array const elements are emitted as
pre-initialised QBE data objects. String elements use immortal string
pointer references ($__sN + 12). Integer elements use w-typed words.
Changes:
uAST.pas: IsArrayConst, ArrayIndexType, ArrayElemType, ArrayElements
fields on TConstDecl; ArrayElements freed in destructor
uSymbolTable.pas: ConstArray: TStringList on TSymbol (owned)
uParser.pas: parse array[T] of E type annotation and (...) value list
uSemantic.pas: AnalyseArrayConstDecls (second pass after type decls)
called after AnalyseTypeDecls at all five analysis call sites
uCodeGenQBE.pas: EmitGlobalConstData emits data objects for array
consts; IsConstant guard for tyStaticArray routes through the
global aggregate address path
docs/grammar.ebnf: ConstDecl, ConstTypeAnnotation, ConstRhs updated
docs/language-rationale.adoc: new Array-of-enum Typed Constants
section; Supported types list updated in Typed Constants section
cp.test.constants.pas: 7 new tests. 1837 tests pass. Fixpoint OK.
Supports type annotations on constant declarations for all scalar
types (Integer, Int64, Double, Single, Boolean, string).
Changes:
uAST.pas: add TypeName field to TConstDecl
uParser.pas: detect ':' after const name, read type identifier
uSemantic.pas: when TypeName is set resolve via FindType; report
error for unknown type names; value codegen unchanged
math.pas: Pi updated to typed form (Pi: Double = ...)
docs/grammar.ebnf: ConstDecl and ConstExpr updated
docs/language-rationale.adoc: new Typed Constants section
cp.test.constants.pas: 11 new tests covering Integer, Int64, Double,
Single, Boolean, string, negative values, unit-level typed consts,
and type annotation verification via symbol table inspection.
1830 tests pass. Fixpoint OK.
Implements math.pas RTL unit and math compiler builtins with
Single/Double dispatch as described in future-improvements.adoc.
Compiler builtins (no unit required):
Sqrt, Ceil, Floor, Round, Trunc, Ln, Log2, Log10, Power,
Sin, Cos, Tan, ArcTan, ArcTan2, IsNaN, IsInfinite.
Trig functions dispatch to *f variants for Single arguments.
math.pas RTL unit (uses Math):
Min, Max (Integer/Int64/Double overloads), Sign (Integer/Int64/Double),
DivMod, InRange, EnsureRange (Integer/Double), Pi constant.
Compiler fixes included in this commit:
- uCodeGenQBE: fix Float (Double/Single) return type in EmitFuncDef
and EmitMethodDef -- Result slot was initialised and loaded with
wrong QBE type (storel/loadl instead of stored/loadd).
- uSemantic: fix overload impl matching -- impl-section functions
without 'overload' keyword now correctly match overloaded interface
forward decls using mangled-signature lookup.
- uUnitLoader: remove Math from the built-in skip list.
- uSymbolTable: register math builtins in RegisterBuiltins.
- Blaise.pas: pass -lm to link step.
- bcl.testing: add AssertNotNil(AMsg, APtr) overloads.
- cp.test.e2e.base: pass -lm to cc link step in all CompileAndRun paths.
Tests: 66 IR unit tests (cp.test.math.pas) + 41 E2E tests
(cp.test.e2e.math.pas). 1819 total tests pass. Fixpoint OK.
Adds full support for heap-allocated, reference-counted dynamic arrays
across all compiler layers and the RTL.
- Parser: distinguish array[L..H] of T (static) from array of T (dynamic)
- Symbol table: tyDynArray kind + TDynArrayTypeDesc + NewDynArrayType
- Semantic: type resolution, Length(), SetLength(), subscript read/write
- Codegen: pointer-slot alloc, nil init, RTL calls, element read/write;
QbeTypeOf returns 'l' for tyDynArray; Int64 element sign-extension
- RTL: _DynArraySetLength and _DynArrayLength in blaise_str.pas
- OPDF: EmitTypeDesc and EmitArray handle tyDynArray (IsDynamic=1)
- Grammar: DynArrayType rule added to grammar.ebnf
- Docs: dynamic array design decision in language-rationale.adoc
- Tests: cp.test.dynarray (14 tests: parser, semantic, codegen)
Fixpoint verified at stage-3/stage-4. All 1712 tests pass.
Rewrites strutils.pas from an empty stub into a complete pure Blaise Pascal
implementation with no external C dependencies. All helpers previously
planned for blaise_str.pas are implemented directly in StrUtils using only
compiler built-ins (Length, Pos, LowerCase, Copy, SetLength, PosEx).
- ContainsStr/ContainsText, StartsStr/StartsText, EndsStr/EndsText
- LeftStr, RightStr, MidStr
- IndexStr, IndexText
- ReplaceStr, ReplaceText
- DupeString, ReverseString, StuffString
- TrimLeft, TrimRight
- PadLeft, PadRight
- CountOccurrences, RemovePrefix, RemoveSuffix
- IsEmptyOrWhitespace
- JoinStr
- TStringBuilder — efficient incremental string builder (mirrors TIRBuffer)
Adds ExpandFileName to sysutils.pas as a pure Pascal path normaliser that
resolves '.' and '..' segments without symlink following, matching
Delphi/FPC behaviour.
Fixes cp.test.multiwrite.pas: unqualified CountOccurrences calls now use
Self. prefix to prevent name shadowing by StrUtils.CountOccurrences, which
has reversed parameter order (Sub, S vs Haystack, Needle).
Adds cp.test.strutils.pas (IR + type-check unit tests) and
cp.test.e2e.strutils.pas (compile→run E2E tests) covering all new
functions. Total test count: 1698.
Fixpoint verified at stage-3/stage-4.
Documents the design space for richer enum models with reference examples
from Java, Swift, Go, C#, and Oxygene (RemObjects), each showing both the
type definition and realistic application-code usage.
Options documented:
- Option B: built-in string name lookup (compiler-generated table)
- Option C: enum class with per-variant fields and methods (Java-style)
Recommended progression from done (explicit ordinals) through near-term
(Ord/Succ/Pred intrinsics), medium-term (Option B), and long-term (Option C).
Enum members can now be assigned explicit integer ordinals, with
auto-continuation for unlabelled members:
type TStatus = (Idle=10, Running=20, Done=30);
type TCode = (A=100, B, C); { B=101, C=102 }
type TOffset = (Before=-1, At=0, After=1);
The ordinal value is stored in TEnumTypeDef.Members.Objects[I] as a
tagged pointer (PtrUInt), avoiding a dynamic array field that v0.7.0
cannot compile. TEnumTypeDef.OrdinalAt(I) is the typed accessor.
The semantic pass reads OrdinalAt(K) instead of the positional index K,
so ConstValue on each enum-member symbol reflects the declared ordinal.
Codegen already emits copy <ConstValue> — no codegen changes needed.
grammar.ebnf: add the EnumDef and EnumMember rules (previously missing).
These are platform fundamentals that belong in the system unit, matching
FPC's design where LineEnding, sLineBreak, and PathSeparator live in system.
system.pas gains:
LineEnding = #10 (canonical line ending constant)
sLineBreak = LineEnding (Delphi compat alias)
DirectorySeparator = '/' (canonical path separator)
PathSeparator = ':' (PATH env var separator)
sysutils.pas retains only:
PathDelim = '/' (Delphi compat alias for DirectorySeparator)
uSymbolTable.RegisterBuiltins pre-seeds all four constants with POSIX
defaults so they resolve as builtins regardless of unit load order.
The correct extension point for cross-compilation is a future --target
flag that overrides these values at compiler startup — no {$IFDEF}
in source, no platform-specific system unit variants needed.
PathDelim uses literal '/' rather than DirectorySeparator to stay
compatible with the v0.7.0 release bootstrap binary.
1590 tests, all passing. Fixpoint OK (stage-3/stage-4).
type TArr = array[L..H] of T previously failed with a parse error because
ParseTypeDecl did not handle tkArray on the right-hand side of a type alias.
Parser fix: add tkArray to the existing tkCaret|tkIdent branch so all three
forms (array alias, pointer alias, simple name alias) share the same
TTypeAliasDef + ParseTypeName path.
Semantic fix: in the simple-alias resolution path, fall through to
FindTypeOrInstantiate when FTable.Lookup returns nil, so that
'array[L..H] of T' style names (which are created on demand, not
pre-registered) resolve correctly. Previously the path raised 'Unknown type'
immediately without trying the on-demand instantiation route.
Tests: 9 new IR/semantic tests in cp.test.staticarray.pas. New dedicated
e2e unit cp.test.e2e.staticarray.pas with 8 tests covering anonymous and
named alias forms, zero/non-zero base, Length/Low/High, multiple vars sharing
a type, and global+local var combinations. Docs updated in grammar.ebnf and
language-rationale.adoc. Fixpoint verified at stage-3/stage-4.
Passing a named static-array variable (array[lo..hi] of T) to a procedure
expecting an open-array parameter (array of T) previously failed with 'No
matching overload' because ArgMatchScore and IsSubtypeOf had no rule for this
coercion. Inline array literals worked because they resolve to tyOpenArray
directly; named variables do not.
Fix: add a tyStaticArray→tyOpenArray widening rule (score 1) in both
ArgMatchScore and IsSubtypeOf when element types match. In codegen, add a
third branch in all four open-array argument-emission sites: when the arg is
a tyStaticArray, emit the base pointer plus (HighBound - LowBound) as the
compile-time high index instead of loading a runtime _high slot.
Tests: 5 new IR/semantic tests in cp.test.openarray.pas. New dedicated e2e
unit cp.test.e2e.openarray.pas with 8 tests (3 moved from the monolithic e2e
unit + 5 new static-array coercion tests covering Length, Sum, non-zero base,
and nested forwarding). Fixpoint verified at stage-3/stage-4.
Introduce TE2ETestCase in cp.test.e2e.base.pas as a shared base for all
e2e test suites. It centralises ProjectRoot detection, toolchain setup,
scratch directory management, RunProc/RunProcNoArgs (as methods, removing
the per-unit _XX suffix workaround), CompileAndRun for inline-class programs,
and CompileAndRunWithRTL for programs that use RTL units via TUnitLoader +
AppendUnit/AppendProgram.
Add cp.test.e2e.tstringlist.pas with 17 e2e tests covering the real RTL
Classes unit: Add/Get, Count, IndexOf (found/not-found), Delete, Insert,
Clear, Put (Strings[] write), AddStrings, for..in enumerator, Text get/set,
Grow past initial capacity, Sorted mode, CaseSensitive flag, and
Duplicates=dupIgnore. The two TStringList tests previously in the
monolithic cp.test.e2e.pas are removed and superseded by this unit.
Refactor cp.test.e2e.tstack, tqueue, tset, tordereddictionary, and imap
to inherit TE2ETestCase: SetUp reduces to one SetUpScratch call, all
duplicated infrastructure is removed (~100 lines per unit), and + LE +
string concatenation is converted to text blocks throughout.
Fixpoint verified at stage-3/stage-4. All 1556 tests pass.
Re-enabled cp.test.lexer, cp.test.parser, cp.test.codegen,
cp.test.symtable, cp.test.semantic, cp.test.records in TestRunner.pas —
all six were commented out when the test runner was migrated from FPC to
Blaise, reducing the suite from ~1433 to ~1376 tests.
Changes needed to make all six units compile and pass (1541 tests total):
- Remove leftover `Classes, SysUtils` imports from all six units (FPC
fpcunit era; Blaise has neither unit).
- Replace default-property bracket subscripts (TObjectList[i],
TStringList[i]) with explicit .Items[i] / .Strings[i] calls, since
Blaise does not yet implement the `default` indexed property keyword.
- codegen: add tyRecord branch in EmitStaticSubscriptAssign — record
elements in static arrays must use EmitRecordCopy, not a raw storew.
- codegen: add tyRecord early-return in EmitStringSubscriptExpr static
array branch — return element pointer directly (records are by-value
via pointer).
- codegen: fix EmitInstancePtr to delegate to EmitExpr when the field
access is property-backed (PropRead != nil), avoiding a nil FieldInfo
assertion.
- test: fix IRContains to use Pos(...) >= 0 (Blaise Pos is 0-based,
returns -1 on no-match, not 0 like FPC).
_POSIX_C_SOURCE 199309L only enables POSIX.1-1993 symbols; mkstemp
requires _POSIX_C_SOURCE >= 200809L (or _XOPEN_SOURCE >= 500).
The value was already 200809L in blaise_process.c — align blaise_io.c
to the same level.
Fixes: https://github.com/graemeg/blaise/issues/25
- Add IMap<K,V> generic interface to generics.collections.pas with Add,
TryGetValue, ContainsKey, Remove, GetCount; wire TDictionary<K,V> and
TOrderedDictionary<K,V> to implement it
- Fix generic class instantiation to detect interface-typed ParentName and
move it to ImplementsNames, then call AddImplements so type-compat checks
recognise class-to-interface assignment (uSemantic.pas InstantiateGeneric)
- Add TFieldAccessExpr.IsInterfaceCall + ResolvedClassType fields (uAST.pas)
and wire AnalyseFieldAccess to set them for zero-arg interface method calls
- Add codegen for TFieldAccessExpr.IsInterfaceCall: load obj+itab and
dispatch through itab slot (uCodeGenQBE.pas)
- Fix EmitMethodCall interface dispatch to emit arguments after Self instead
of calling with only Self; determine arg types from expression ResolvedType
- Extend TInterfaceTypeDesc with per-method var-param flags (FParamIsVar):
MethodParamIsVar/MethodParamVarFlagsStr/AddMethod(name, ret, varflags)
so interface call sites can emit var params as addresses, not values
- Fix expression-level interface dispatch to use var-param flags for correct
l/w selection per argument (TryGetValue var param was passed as w, causing
segfaults)
- Emit itab + impllist blocks for generic class instances that implement
interfaces (EmitInterfaceDefs in uCodeGenQBE.pas)
- Update EmitTypeInfoDefs to set impllist pointer for generic instances
- Apply QBEMangle to both class and interface names in itab symbol names
- Set AExpr.ResolvedType in interface method call expression path
- Convert cp.test.e2e.imap.pas source constants to triple-quoted textblocks
- Add IR unit tests (cp.test.imap.pas: 22 tests) and E2E tests
(cp.test.e2e.imap.pas: 6 tests) for IMap interface dispatch
- Add IR + E2E tests for TStack, TQueue, TSet, TOrderedDictionary
- Fix AssertContains: Pos returns -1 for not-found (0-based), not 0
- All 1376 tests pass; fixpoint verified at stage-3/stage-4
New assertion helpers on TAssert:
- AssertNotEquals — Integer, Int64, string, Boolean, Pointer overloads;
reports the shared value on failure so tests no longer need to
convert to AssertTrue(a <> b) which loses the comparison output
- AssertContains(msg, needle, haystack) — replaces the widespread
AssertTrue('...', Pos(x, y) > 0) pattern with a failure message
that quotes both the needle and the haystack
Verbose output (--verbose flag on TestRunner):
- TTestResult gains a Verbose property
- StartTest/EndTest now carry the class name and outcome string
- Each test prints "ClassName.MethodName ... OK/FAIL/ERROR/IGNORED"
as it runs when --verbose is active
Better error reporting:
- Unhandled Exception descendants now report ClassName + Message
instead of the generic "Unhandled exception" string; requires
SysUtils in bcl.testing uses clause
- TObject catch-all still present for non-Exception throwables
Runner:
- ParseSuiteArg replaced by ParseArgs which also handles --verbose
- RunFilteredTests takes an AVerbose parameter and seeds the result
- ClassCreate'd instances get SetClassName called so verbose output
shows the full ClassName.MethodName path
Adds a new for-in collection kind: set of TEnum. The loop iterates
members in ascending ordinal order by scanning the bitmask one bit at
a time. The set expression is evaluated once before iteration begins.
- uAST: IsSetIter, SetBitCount, SetMaskVarName fields on TForInStmt
- uSemantic: tySet branch validates ordinal loop var, injects synthetic
__setmask_N and __idx_N slots
- uCodeGenQBE: bit-scan loop using shr + and 1; break/continue wired to
forin_end / forin_next labels
- 7 IR unit tests (cp.test.forin) + 1 e2e test (set [Red,Blue] → 0,2)
- language-rationale and grammar.ebnf updated; generic forin marked done
Supports(Obj, IFoo) returns Boolean — calls _ImplementsInterface at
runtime to test whether Obj's class implements IFoo.
Supports(Obj, IFoo, Ref) does the same test and, on success, populates
the fat-pointer slots (obj + itab via _GetItab) of the out-variable Ref
under full ARC (retain new obj, release old obj slot).
The second argument is a type name, not a runtime value, so Supports is
a compiler intrinsic parsed specially in ParseFactor rather than a
library function. The third argument (when present) is the name of an
interface-typed out-variable.
Includes:
- TSupportsExpr AST node (uAST.pas)
- Parser intercept for Supports( in ParseFactor (uParser.pas)
- Semantic analysis: validates obj type, interface type, out-var type
(uSemantic.pas)
- QBE IR emission for both 2-arg and 3-arg forms (uCodeGenQBE.pas)
- 7 unit tests covering parse, semantic, and IR codegen
- 2 e2e tests verifying runtime Boolean result and method dispatch
through the assigned interface fat pointer
- Grammar and language-rationale documentation updates
Fixpoint verified at stage-3/stage-4.