Releases: graemeg/blaise
Blaise v0.10.0 (alpha) — Incremental compile, Native Backend and more
Released: 2026-06-07
Language
- Mandatory
()on all zero-argument function/procedure/method/constructor calls - Procedure-field invocation:
Obj.ProcField()dispatches through stored code pointer - Removed
IsNoArgFuncCall/NoArgFuncDecldead code from AST and codegen - Diamond operator: infer generic type args from LHS in assignments (
TList<>.Create()) Exit(Value)shorthand for function-result early returnsnotoperator extended to integer types (bitwise complement)- Set-valued constants (
const X = [a, b]) and set literals asset ofarguments threadvarsupport for thread-local storageoutparameter mode tracked distinctly fromvar- Calling convention directives (
cdecl,stdcall) preserved on routine declarations
Native x86-64 Backend
- M1: empty program compiles and runs
- M2: integer arithmetic, Write/WriteLn
- M3: if/while/repeat control flow
- M4: records, static arrays, integer variables, for loops
- M5: user functions, parameters, recursion, break/continue/exit, var/out params, indirect calls, >6 args via stack, wider integer family
- M6: float (Double/Single) parity
- M7a: record-returning functions (sret)
- M7b: class system — method dispatch, vtables, constructors, destructors, method pointers
- M7c: string operations parity
- M7d: exception handling (try/except/finally)
- M7e: open arrays and dynamic arrays
- M7f: interface dispatch through itab
- M8: var/out arguments to method calls, inherited calls
- Full generics support (monomorphised classes, records, interfaces, free routines)
- For-in and case statements
- Inc/Dec built-ins and class-local ARC
.FreeintrinsicTAddrOfExpr,TDerefExpr,TPointerWriteStmt- ARC retain/release for string/class/interface value params and field assignments
- Array field subscript read and var-param validation
- Interface parameters in method/constructor/inherited calls
ICodeGeninterface for backend dispatch
Incremental Separate Compilation
.bif(Blaise Interface File) format — compact unit-interface serialisation.bifembedded inside.ofiles for zero-extra-file discovery--incrementalflag: skip unchanged units, reuse cached.o+.bif- Parallel incremental compilation via worker threads
- Resolved all
EImportErrorcrashes in warm incremental compile .bifcovers: constants, types (simple, record, class, interface, generic templates), routines, globals, inline bodies, calling conventions.bifvalidation header with compiler ID and source hashTUnitInterfaceexport/import for classes, interfaces, records, generics, virtual methods, override vtable slots, implements lists, attributes- Per-unit symbol cache for uses-chain retrieval
- Layered symbol lookup — current-unit before uses-chain
- Unit-prefix symbol mangling for free routines, classes, and address-of
ARC & Memory
- Atomic ARC refcount operations for thread safety
- Mutex-protected weak reference table for thread safety
- Per-thread memory allocator via threadvar
- Thread-local exception globals
TThread.Create(False)joins onFree()via ARC- Release old value when assigning nil to a class variable
- Emit
_ClassAddRefwhen assigning Pointer to class-typed variable - Release owned RHS when storing to
[Unretained]class field - Retain/release by-value interface params in callee
- Elide retain/release for const string and class params (perf)
- Elide const-param retain/release, retain transient args (perf)
Codegen (QBE)
- Unit-prefix mangling for multi-unit builds
- System-unit defs emitted in unit-mode
- Generic interface-instance typeinfo in multi-unit builds
WriteLn(Double/Single)no longer crashes QBE with type mismatch- Unsigned 32-bit values print as unsigned in WriteLn
@RecordFieldinside methods and pointer-to-record deref- Record method calls in statement position
- Var/out params passed by reference in inherited calls
- Method interface param passed as fat pointer
- Generic class name mangled in itab symbol for interface assignment
- Leak-tracker typeinfo reference uses class unit prefix
- For-in class enumerator calls use method's unit prefix
- Program-scope generic methods don't get unit prefix in ResolvedQbeName
- Class fields shadow same-named program globals in method bodies
Compiler
--dump-astflag for post-semantic AST inspection--skip-dep-codegenfor unit-as-top-level builds- Generic record support (monomorphisation)
- Retain generic templates so repeat instantiation works
- Auto-tag
OwningUniton globalDefine FDefineOwningUnit,FCurrentUsesChain,BuildUsesChain,LookupViaUsesChain- Member-lookup visibility seam (
IsVisibleFromUnit) - Accept set literals as
set ofarguments - Remove FPC conditional-compilation directives
Runtime & Standard Library
WriteLnprintsTrue/Falsefor Boolean valuesTStringList.Text/LoadFromFilepreserve verbatim lines- Drop legacy platform aliases; derive constants from target
- Runtime split: drop 7 build-driver shims, use unit-as-top-level
- RTL build switched to unit-as-top-level
Kanban Tool
- TUI Kanban board application (new)
- CLI mode for non-interactive task creation
- UTC datetime storage with local display
- External file change detection and merge
- Rounded box-drawing corners and coloured borders
- Moved from
examples/totools/as a PasBuild module
Tests
- 2627 tests passing (up from 2293 in v0.9.0)
- Native backend E2E suite runs through both QBE and native backends
- Standalone pointer-to-record field deref test
- Nested record field assign + method call tests
- Separate-compilation round-trip E2E tests
- Implicit System unit reachable without explicit uses
- Test runner includes stdout in e2e exit-code mismatch messages
Tooling
- Rolling-bootstrap script (
scripts/rolling-bootstrap.sh) for development-checkout builds - Fixpoint script drives runtime build with stage-1 binary explicitly
Summary
- 262,202 lines of verified QBE IR at fixpoint
- 2627 tests passing
- FIXPOINT_OK — self-hosting verified
Blaise v0.9.0 (alpha) — Runtime port & bug squashing
Changelog — Blaise v0.9.0
Fixpoint: stage-2 == stage-3 at 166,479 lines of QBE IR (up from 135,311 in v0.8.0).
Test suite: 2,293 tests, 0 failures.
Runtime — C elimination
- Port
blaise_arc_class.cto pure Pascal (blaise_arc.pas) - Port
blaise_exc.cto Pascal + x86_64 assembly (blaise_setjmp_x86_64.s
remains as the only non-Pascal file) - Port
blaise_weak.cto pure Pascal - Port
blaise_float.cto pure Pascal Grisu1 float-to-string implementation - Port
blaise_str_fmt.cto pure Pascal_StringFormatN - Consolidate
blaise_io/blaise_process/blaise_sys/blaise_time
intortl.platform.posixPascal unit - Enable
pipefailin the runtime Makefile so compiler errors fail the build
FFI / Codegen — interop correctness
- Records by value through SysV aggregate ABI: record-by-value parameters
now use QBE:_ffi_<Name>aggregate types so fields are scattered into
INTEGER/SSE registers per the platform ABI. Applied uniformly to all call
sites (external and intra-Blaise), so callbacks are consistent. Win64 ABI
comes from QBE for free. Previously, a barel <addr>was passed and the
callee read the pointer bits as struct contents. Singlefield alignment corrected to 4 bytes (was falling through to
the 8-byte catch-all, doubling the size ofSingle-heavy records and
breaking C struct interop)- Double literal narrowed to
Singleon record field store — previously
emittedstores d_<lit>, ...which QBE rejected as a type mismatch - Double expression narrowed to
Singlebefore FFI call — aDouble-
typed actual passed to aSingleformal put 8 bytes into the float slot;
the C callee read the low mantissa half as IEEE-754 single (noise) - Narrow FFI return values masked to declared width:
Byte/Boolean
returns are ANDed with 255,Wordwith 65535,SmallIntsign-extended
viashl 16/sar 16. C ABI leaves upper bits undefined for sub-int
returns; callers observing the full word saw garbage. @A[i]on dynamic arrays —EmitAddrOfExprnow has atyDynArray
branch mirroring the open-array path. Previously raised "Unsupported
L-value form for var argument".- Overloaded
Destroyin ARC field-cleanup —EmitFieldCleanupFnnow
calls the overload-mangled symbol (stored asDestroyResolvedQbeNameon
TRecordTypeDesc) rather than the bare<Class>_Destroylabel, which was
never emitted for overloaded forms, causing a linker undefined-symbol error. - Fix
storelfor proc-pointer static-array slots; addExpr()()postfix call - Use narrow load/store for implicit-Self
Byte/Word/SmallIntfields - Fix QBE types for
Singlefloat arithmetic and comparisons - Emit correct data items for typed array consts inside function bodies
- Fix ARC retain/release on array element writes
- Allocate 16 bytes for procedure-of-object class fields
- Retain/release class refs stored through typed pointer
- Nil class/string field slots after
.Freeto prevent double-free - Load class pointer before offsetting in var-param L-value
- Preserve ARC variables across
try/finallyre-raise
ARC — correctness
finallyblocks run on non-local exit (Exit,Break,Continue)
viaFFinallyStack— previously skipped entirely on non-local unwinds[Unretained]attribute for non-owning class references (back-pointers,
ASTResolved*Typefields, symtab links) that must not participate in ARC- Release
+1-owned temporaries produced by function/property returns when
used transiently or passed as value arguments - Release
+1-owned receiver temporaries after method calls - Honour
[Weak]on implicit-Self class field assignments - Retain caught exception when binding to handler variable (
except E: T do) - Eliminate double-
_ClassAddRefin constructor-with-args codegen - Eliminate double-
_ClassAddRefon function-return class assignments - Wire owned-arg-temp release into all remaining call paths
- Remove 50+ redundant
Freecalls from AST destructors (field cleanup
already handled by ARC) - Remove redundant
Freecalls from symbol-table destructors
Language — new features
- Nested procedures with captured-variable closure (full access to
enclosing scope locals) - Custom attributes —
[MyAttribute]/[MyAttribute(args)]on any
declaration; compiler validates attribute class and resolves constructor [Unretained]non-owning reference attributeSmallIntandWord— 16-bit signed/unsigned integer types with correct
narrow load/store and Byte/Word field packing at 1-byte / 2-byte strideUInt64/QWord— 64-bit unsigned integer typepacked record— suppresses per-field alignment paddingsararithmetic right-shift operatorHigh/Lowextended to all ordinal types; targeted float error messageSizeOf(expression)— previously onlySizeOf(TypeName)was accepted/is real division — distinct fromdiv(integer division)for..inover dynamic arrays- Octal / binary literals (
0o777,0b1010) and underscore separators
in numeric literals (1_000_000,0xFF_FF) - Integer-type typecasts in const initialisers —
Cardinal(-11),
Byte(255),SmallInt(-1), etc., with correct bit-width truncation and
sign extension - Bit-op chains in const initialisers —
FG_BLUE or FG_GREEN,1 shl 8,
$FF and 15,A xor B, including mixed literal+named chains and array
elements; folded at semantic time ArcSin,ArcCos,Sinh,Cosh,Tanhmath builtins withSingle
dispatch overloads- Extend
High/Lowto dynamic array types - Allow direct invocation of proc-typed class fields
- Allow implicit integer-to-float assignment; reject implicit float↔integer
Semantic — fixes
- Implicit Self member no longer shadows unit-level symbol
- Nested procs with the same name in different outer procs no longer ambiguous
- Clone method bodies per generic instance (was sharing mutable AST nodes)
- Allow two instances of the same generic class in the same scope
- Wire up parent class on generic instances
- Walk parent chain when resolving inherited properties
- Register var decls before analysing method bodies
- Use overload resolution for constructor calls
- Emit support data for generic classes declared in a unit
- Normalise all identifier references to declared casing
- Reject duplicate identifiers across
constandvarsections - Report missing units even when no
--unit-pathis given - Fix
SizeOf(expression)to resolve the expression type
Threading
TThread,TCriticalSection, and full POSIX thread bindings- Self-referential thread lifetime under ARC (thread holds own refcount during
Execute, preventing use-after-free on fire-and-forget patterns) [Threaded]E2E test suites run as parallel subprocesses
Stdlib / Performance
GetTextandGetCommaTextrewritten to avoid O(N²) string concatenationTList<T>.IndexOfaddedTListEnumerator<T>+TList<T>.GetEnumerator—for..inover generic lists_ClassReleaseelision for provably-nil slots (codegen perf)- Remove all dead
{$IFDEF FPC}compiler directives
Developer experience
--debugleak reporter — shows refcount per live object at program exitblaise.cfg— automatic unit-path discovery from project directory- Multiple
--suitefilters accepted by the test runner --emit-irpath now exits through normal cleanup instead ofHalt(0)- UTF-8 bytes in comments: regression tests added to lexer suite
Compiler internals / Build
- Mark
Resolved*TypeAST fields[Unretained]— prevents spurious retain
cycles on type descriptors shared across the AST - Remove all old FPC compiler directives no longer needed in Blaise
--cache-dirflag andmigrate_full.pyremoved (dead code)
Blaise v0.8.0 (alpha) — House Cleaning🧹 & Performance
Blaise Compiler — Changelog for v0.8.0
Released: 2026-05-16
Previous: v0.7.0 (2026-05-13)
Commits: 62 between v0.7.0..v0.8.0
HEADLINE
v0.8.0 is the first release with a Pascal-native memory allocator.
The compiler RTL, codegen builtins, and all C runtime helpers route
through blaise_mem (_BlaiseGetMem / _BlaiseFreeMem / _BlaiseReallocMem)
instead of libc malloc.
blaise_mem beats libc malloc on three of five microbenchmark workloads
(small alloc, mixed sizes, retain+free-all) and delivers a ~10 %
real-world compile-time win on the 1934-test suite (~89 s → ~80 s).
NEW LANGUAGE / SEMANTICS
-
Dynamic arrays (
array of T) — full implementation: SetLength,
Length, Copy, indexed read/write, ARC-managed lifetime. -
Typed constants —
const Name: Type = Valuewith array literals
for both fixed-range and array-of-enum forms. -
Class-level array constants — constant arrays declared inside a
class body, addressable via TypeName.ConstName. -
Range-indexed array constants —
array[0..N] of T = (...). -
Abstract methods and abstract classes —
virtual; abstract;syntax,
with a runtime tombstone (_AbstractMethodError) that aborts with a
clear message if a vtable miss reaches it. -
Explicit ordinal values in enum definitions —
(A = 1, B = 5). -
Record methods — methods declared inside a
recorddefinition. -
for .. inover sets — iterate the members of aset of T. -
Supports()intrinsic for interface queries. -
Open-array coercion — pass static-array variables directly to
array of Tparameters. -
Named array type aliases in the type section —
type TArr = array[..] of T;. -
Length()extended to accept open-array and static-array
parameters in addition to strings and dynamic arrays. -
Five-type date/time model with full
DateUtilsunit (records-based;
TDateTime, TDate, TTime, plus formatting / arithmetic helpers).
Read more. -
Generic vars at unit scope and non-identifier interface arguments
now parse and analyse correctly. -
Overload resolution now considers the implicit-self expression
path, matching Delphi/FPC behaviour.
RUNTIME / STANDARD LIBRARY
-
Streams stack (read more):
- TInputStream / TOutputStream + file and memory streams.
- buffered wrappers with error propagation.
- TStreamReader / TStreamWriter text wrappers.
- TBuffer segment rope + byte-dereference fixes.
- CopyStream and capability markers; interface ABI fixes.
-
Pascal memory allocator (blaise_mem) — mmap-backed, with size-class
freelists for small allocations, a LIFO cache for large allocations,
in-place mremap-based realloc for large blocks. 28 correctness
tests in runtime/src/test/pascal/test_blaise_mem.pas. -
Path manipulation functions ported from C to Pascal (ChangeFileExt,
ExtractFileName, ExtractFilePath, ExtractFileDir, ExtractFileExt,
IncludeTrailingPathDelimiter, ExcludeTrailingPathDelimiter). -
TComponent — minimal ownership/notification class.
-
TStringList — CustomSort + CommaText property.
-
StrUtils — pure-Pascal port.
-
SysUtils — ExpandFileName.
-
Math unit — math compiler builtins (Min, Max, Abs, Sign, etc.).
-
IMap<K, V> interface — generic dictionary contract with itab-based
dispatch for generic→interface use. -
bcl.testing extended with AssertNotEquals, AssertContains, and a
--verbose CLI flag. -
RTL split into runtime/ (always-linked) and stdlib/ (opt-in).
Testing framework renamed bcl.testing → blaise.testing. -
LineEnding, sLineBreak, DirectorySeparator, PathSeparator moved
from rtl.platform to system.pas. -
Platform layer renamed bcl.platform → rtl.platform with expanded
SysUtils.
COMPILER OPTIMISATIONS
-
Function inlining (phase 1) — small leaf functions are inlined at
call sites when address-taken and ARC-free. Inlinability decided
by uSemantic; codegen walks the callee body into the caller frame. -
Function inlining (phase 2) — case statements now allowed inside
inline candidates, and the body-statement cap raised from 8 to 24.
This unlocked inlining of blaise_mem's SizeClassIndex and
SizeClassBytes. -
mem2reg promotion for tyPointer / tyPChar locals — non-address-taken
pointer locals are promoted to QBE temps instead of alloca slots,
yielding direct register access on every read/write. -
Sanity checker in _StringRelease — verifies the string header looks
plausible (refcount, length, capacity within sane bounds) before
treating refcount==0 as a free signal. Aborts with a clear stderr
message on corruption.
BUG FIXES
-
P[I] := Chr(N)no longer stores the low byte of a heap string
pointer. EmitByteRhs short-circuits Chr() in byte-store contexts
and emits the integer value directly, skipping the _Chr allocation.
Applies to PChar subscript writes, dynamic- and static-array byte
subscript writes, and byte-typed pointer writes. -
Static-array element stores of PChar values no longer truncate to
32 bits. tyPChar was missing from the storel / loadl / alloc8
case branches and fell through to storew, silently zeroing the
high 32 bits of every heap pointer. Fixed across five sites in
uCodeGenQBE.pas. -
large-alloc LIFO cache in blaise_mem — IsLarge() was reading the
wrong field of TLargeHeader (AllocSize: Int64 overlapped with the
Flags slot it probed). Fix made the cache reach ~100 % hit rate
on 64 KB allocations, dropping the L workload from 33 ms to 0 ms. -
Static-array variables now coerce correctly when passed to open-
array parameters. -
Stale
rtl/directory references in E2E tests updated to
runtime/after the directory split. -
_POSIX_C_SOURCEbumped to 200809L to expose mkstemp() on glibc.
PERFORMANCE
Allocator microbench (median of 3 runs, milliseconds), 1 M small alloc/
free, 500 k mixed-size, 100 k 5-step realloc, 10 k × 64 KB, 100 k
retain-then-free-all:
Workload | libc malloc | blaise_mem | ratio
-------------------------------+-------------+------------+--------
Small alloc/free (1M × 32 B) | 8 | 7 | 0.88x
Mixed sizes (500k × var) | 5 | 4 | 0.80x
Realloc growth (100k × 5) | 5 | 8 | 1.60x
Large alloc/free (10k × 64KB) | 0 | 0 | —
Retain+free-all (100k × 64B) | 5 | 4 | 0.80x
Real-world compile workload (Blaise test suite, 1934 tests):
Pre-cutover (v0.7.0 baseline): ~89 s
Post-cutover (v0.8.0 release): ~80 s (~10 % faster)
DOCUMENTATION
-
Testing strategy document — explains the unit / E2E / punit
three-layer split and when to use each. -
Stream I/O design rationale.
-
Generic Interfaces and Collections Framework rationale.
-
Cold-bootstrap RTL chicken-and-egg issue documented.
-
Migration analyser rule: detect
TObjectList→TList<TObject>. -
Future-improvements: enhanced enumerations section.
-
benchmark.txt expanded with a long-term tracking format and entries
for each performance milestone in the cycle. -
Run procedure for the bench scripts updated post-cutover — a
third bench source (bench_libc_malloc.pas) uses explicit external
malloc / free / realloc bindings so the libc baseline can still be
measured.
INFRASTRUCTURE / REFACTORING
-
rtl/directory split intoruntime/+stdlib/. -
Monolithic E2E test unit split into 12 topic units (one per
feature area). -
TStringList E2E coverage expanded with a shared base class.
-
Six test units re-enabled after fixes to static-array and Pos
semantics. -
GitHub funding configuration (Patreon + PayPal).
FIXPOINT
stage-2 IR : 135 311 lines
stage-3 IR : 135 311 lines
diff -q : (empty — byte-identical)
1934 tests : pass
Blaise v0.7.0 (alpha) — Independence Day 🎉
Released: 2026-05-13
This release marks a milestone: Blaise is now fully independent of
Free Pascal. FPC is no longer required anywhere in the toolchain.
The bootstrap chain is: released Blaise binary → compiles source →
new Blaise binary. From here on, every release builds the next.
Highlights
FPC Independence
- The compiler is fully self-hosting.
releases/v0.7.0/blaiseis
the new stage-1 bootstrap binary — no FPC installation required. - The fixpoint script (
scripts/fixpoint.sh) auto-picks the latest
release binary as stage-1 and includes a stage-4 fallback for
cross-release bootstrap gaps. - Fixpoint verified: stage-2 IR == stage-3 IR (116,462 lines).
Test Suite Now Runs Under Blaise
- All 1,268 tests compile and run using the Blaise-compiled test
runner. The test runner is no longer built by FPC. - The test framework migrated from fpcunit to
bcl.testing— 57
test units converted in a single pass.
Language & Codegen
Low()andHigh()now work on strings (in addition to arrays).- Fixed implicit-self method calls that pass
var/outor
open-array parameters — two symmetric paths inEmitProcCall
andEmitExpr(uCodeGenQBE.pas). - Fixed global static array codegen bugs affecting
for..inloops. - Fixed
for..inpromoted scalar variable handling.
RTL
- Fixed a buffer overflow in
_GetTempFileNamewhen the directory
is empty andTMPDIRis set. process.pasrenamed to lowercase for the case-sensitive unit
loader.
Build & Infrastructure
README.adocupdated with FPC-free build and bootstrap
instructions, including theBLAISE=make override needed for
cold bootstraps from a clean checkout.blaise_rtl.aincluded in the release archive — end users can
now link their compiled programs without building the RTL from
source.
Download
| File | Description |
|---|---|
blaise-v0.7.0-linux-x86_64.tar.gz |
Compiler binary + RTL + docs (Linux x86-64) |
The archive contains: blaise, blaise_rtl.a, README.adoc,
LICENSE, NOTICE.
Bootstrap
Unpack the release archive into releases/v0.7.0/ inside your
clone of the repository:
mkdir -p releases/v0.7.0
tar -xzf blaise-v0.7.0-linux-x86_64.tar.gz \
--strip-components=1 \
-C releases/v0.7.0
Then build:
# 1. Build the RTL (BLAISE= avoids chicken-and-egg on clean checkout)
cd rtl && make BLAISE=../releases/v0.7.0/blaise && make install && cd ..
# 2. Compile
releases/v0.7.0/blaise \
--source compiler/src/main/pascal/Blaise.pas \
--unit-path compiler/src/main/pascal \
--unit-path rtl/src/main/pascal \
--emit-ir > /tmp/blaise.ssa
vendor/qbe/qbe -o /tmp/blaise.s /tmp/blaise.ssa
gcc -o compiler/target/blaise /tmp/blaise.s compiler/target/blaise_rtl.a