The punit test framework had bare zero-arg function calls
(GetTestError, CheckInactive) that are now caught by the semantic
diagnostic. Added () to all call sites.
The mandatory-() migration (commit 57c0660) missed ~1000 bare zero-arg
function and method calls across the compiler, stdlib, and test suite.
Without (), these were silently treated as variable reads, producing
broken QBE IR (%_var_ temporaries instead of call instructions).
Added semantic error diagnostics in uSemantic.pas to catch bare references
to functions/procedures that require () for a call. This prevents silent
miscompilation and gives users a clear error message.
Fixed all bare calls across 69 files: compiler pipeline (uParser,
uSemantic, uCodeGenQBE, uLexer, uPasTokeniser, etc.), stdlib (classes),
and the full test suite (inline test programs and test framework code).
FIXPOINT_OK at 262,220 lines. 2627 tests pass. Rolling bootstrap from
v0.10.0 verified.
Self-hosting fixpoint verified on 262,202 lines of QBE IR.
Mandatory () on all zero-argument calls, parallel incremental
compilation, thread-safe ARC, and 2627 tests passing.
With mandatory () on all zero-argument calls (51ebf23), the
IsNoArgFuncCall/NoArgFuncDecl fields on TIdentExpr and the
synthesised TFuncCallExpr codegen path are dead code. Remove them.
This exposed ~300 bare function calls missed by the original
migration script across compiler, runtime, stdlib, tests, and
embedded Blaise source strings. Fix all of them.
Add IsProcFieldCall support to TMethodCallExpr so that
H.Handler() works for procedure-type fields — the parser creates
TMethodCallExpr for Obj.Name(), and the semantic pass now detects
when 'Name' is a procedural-typed field rather than a method,
routing through the indirect-call codegen path.
Update fixpoint.sh to fall back to an existing blaise_rtl.a when
the release binary is too old to build the runtime.
2627 tests pass, FIXPOINT_OK.
Make parentheses mandatory on every function, procedure, method, and
constructor call — even those with zero arguments. A bare identifier
or field access is now unambiguously a variable/field/property read;
appending () makes it a call.
Mechanically migrated all 144 source files (compiler, runtime, stdlib,
tests, kanban tool). Fixed several latent bugs exposed by the AST node
transition from TFieldAccessExpr.IsMethodCall to TMethodCallExpr:
- IsBuiltinToString applied to record methods (added tyClass guard)
- IsVarParam not set for value record/static-array parameters in
AnalyseMethodCallExpr (extended to check skParameter + aggregate type)
- Native backend used movq (pointer load) for record receivers instead
of leaq (address-of) in EmitMethodCallExpr
- ResolveDiamond now handles TMethodCallExpr for diamond-operator
constructor calls
Updated grammar.ebnf (MethodCall, ProcCall, Factor rules) and
language-rationale.adoc with the design decision. Marked the
future-improvements.adoc entry as implemented.
All 2627 tests pass. Fixpoint verified (FIXPOINT_OK).
Adds TestRun_Record_PointerDerefFieldAccess covering PP^.X / PP^.Y
at program scope (no class/implicit-self). Runs on both QBE and native
backends via AssertRunsOnBoth.
When an e2e test fails with unexpected exit code, the failure message
now includes the program's actual stdout. Previously it only showed
"exit code Expected: 0 Actual: 1" with no indication of what the
program printed (often the compilation error or runtime output).
Prints the resolved AST tree to stdout after semantic analysis, then
exits. Shows node types, resolved types with sizes, field offsets, and
semantic flags (IsImplicitSelf, IsClassAccess, IsVarParam, VarParam,
ImplicitSelfField, etc.) that are critical for diagnosing codegen bugs
where the wrong IR or assembly is emitted due to an incorrect flag or
missing field offset.
Address-of an implicit-self record field (@Pt inside a method where Pt
is a class field) emitted a reference to a non-existent local %_var_Pt.
QBE codegen now delegates TIdentExpr addr-of to EmitVarArgAddr which
already computes Self + field offset. Also added IsImplicitSelf handling
to EmitLValueAddr for TFieldAccessExpr.
Native backend: added @ImplicitSelfField path before the generic
@Variable path, computing Self + offset like the QBE backend.
Native backend: fixed pointer-to-record dereference (PP^.X) crash.
EmitExprToEax for TDerefExpr was loading the pointed-to value even for
record types, causing a double-dereference when followed by field
access. Records are address-represented, so P^ on a ^TRecord just
returns the pointer value (the record address).
The semantic pass only allowed tyClass/tyInterface receivers for
statement-position method calls (TMethodCallStmt), rejecting record
variables with "'X' is not a class or interface variable". Expression-
position record method calls already worked. Extended AnalyseMethodCall
to accept tyRecord receivers.
QBE codegen: record Self is a pointer-to-stack (not a heap pointer), so
the receiver address is passed directly without a pointer dereference.
Added record-method branches to EmitMethodCall for local, var-param,
and implicit-self cases, with a tyRecord guard to skip the class-pointer
load.
Native x86-64 codegen: added parallel handling for record methods across
five sites — EmitMethodCallStmt Self-loading, TIdentExpr.IsImplicitSelf
(bare field reads inside methods), TAssignment.ImplicitSelfField (bare
field writes), and three TFieldAccessExpr.IsImplicitSelf sites that were
missing ImplicitBaseInfo.Offset and IsClassAccess deref.
Also fixed a nil-deref crash in EmitMethodCall when IsProcFieldCall is
set (ResolvedMethod is nil) by guarding MDecl.IsRecordMethod checks.
Add EmitForInStmt with all five iteration strategies: static array,
dynamic array, string byte-iteration, set bit-scan, and class
enumerator protocol (GetEnumerator/MoveNext/Current with vtable
dispatch). Add EmitCaseStmt with linear comparison chain supporting
both integer/enum and string selectors via _StringEquals.
Also add set literal expression support (TArrayLiteralExpr with tySet)
to EmitExprToEax, computing the bitmask at compile time.
Extend CountTryStmts to recurse into TForInStmt and TCaseStmt bodies
for correct exception frame pre-allocation.
Upgrade 10 existing QBE-only e2e tests to AssertRunsOnBoth, covering
all for-in paths (string, static array, dynamic array, set, class
enumerator) and case statements (integer branch, else branch, enum
selector).
2622 tests pass; FIXPOINT_OK (stage-3/stage-4).
TThread.Create(False) appeared to silently skip execution (GitHub #73).
Root cause: Start took an extra _ClassAddRef so the trampoline could
release it on exit. This kept the refcount at 1 after Free, preventing
Destroy (and its WaitFor/pthread_join) from running — the main program
exited before the thread finished.
Fix: remove the trampoline's ARC reference entirely. The caller's
reference is the only one; releasing it triggers Destroy → WaitFor →
pthread_join, which blocks until the thread has fully exited. This is
safe because pthread_join guarantees the trampoline has returned before
Destroy frees the object.
Also remove FreeOnTerminate — ARC makes it redundant. In Delphi/FPC it
exists because manual Free is the only cleanup path; in Blaise, scope
exit or reassignment automatically joins and frees the thread. The
migration analyser can flag this for ported code.
Document the TThread ARC lifetime model in language-rationale.adoc and
add class/method documentation comments to classes.pas.
The unit-cache warm path (--incremental --unit-cache) crashed on import
because the BIF interface reader could not handle several type patterns
and registration gaps:
- Add two-pass type registration (pre-register class/record/interface
names in pass 1, fill details in pass 2) so forward references within
the same unit resolve correctly
- Add ResolveInlineTypeName helper for field types stored as raw strings
(^Pointer, array[L..H] of T, array of T, class of T)
- Add RegisterProcType for TProceduralTypeDef import (e.g. procedure-of-
object types like TFieldCleanupProc)
- Fix 0-based string indexing in RegisterAlias (StrAt instead of [1])
- Register imported class methods in FMethodIndex so call-site resolution
works during warm compile
- Serialize/deserialize TPropertyDecl through BIF (clone, export, import)
so property access resolves on cached units
- Add unit-cache directory to search paths so .o files are found
- Include exception class name in compiler error output
Refactor the --incremental compilation path to use worker threads.
Each dependency unit now gets its own TCompileWorker thread that
independently handles codegen (fresh TCodeGenQBE, read-only symbol
table), IR file writing, and external tool invocation (qbe + cc).
All workers run concurrently and are joined before the main codegen
phase.
Also extract CompileUnitToObjectSafe which returns an error string
instead of calling Halt, making it safe for use in worker threads.
Add a pthread mutex around all weak reference table operations
(_WeakAssign, _WeakClear, _WeakZeroSlots). The table is a shared
global data structure, and concurrent _ClassRelease calls from
multiple threads could corrupt the bucket chains without locking.
The mutex is coarse-grained (one lock for the entire table) but
sufficient because weak references are rare relative to strong ARC
operations.
Replace plain read-modify-write refcount operations with atomic
lock xadd instructions via a new blaise_atomic_x86_64.s assembly
stub. _AtomicAddInt32 and _AtomicSubInt32 return the previous
value, so exactly one thread sees the transition to zero and
triggers destruction -- eliminating the TOCTOU race in
_ClassRelease and _StringRelease.
Affected operations:
- _ClassAddRef: atomic increment
- _ClassRelease: atomic decrement, destroy on old_rc == 1
- _StringAddRef: atomic increment (immortal check unchanged)
- _StringRelease: atomic decrement, free on old_rc == 1
Convert the allocator's global state (FreeLists, ArenaHead,
LargeFreeHead, LargeFreeCount) from var to threadvar, giving each
thread its own independent arenas and freelists with zero contention
and no locks required.
Also fix the native backend .tbss emission for aggregate types
(tyRecord, tyStaticArray) — the threadvar section was using
IntByteSize which returns 4 for unknown types, instead of RawSize
which returns the actual byte size of the type.
The threadvar refactor moved .data emission to be conditional inside the
globals loop, which meant when no regular globals existed but exception
frame slots did, the frames were emitted without a .data directive and
landed in .note.GNU-stack — causing linker errors for any program with
try/finally blocks but no global variables.
Move .data emission before the loop, unconditionally when HasData is true.
Add file-change monitoring to the kanban TUI so that tasks added via CLI
(or any external process) while the TUI is running are automatically
merged into the live view instead of being silently overwritten on save.
The implementation has three layers:
1. New FileAge built-in (compiler + runtime): returns the file's mtime as
Int64 via stat(), or -1 if the file does not exist. Follows the same
pattern as FileExists — symbol table registration, semantic analysis,
codegen, and POSIX ABI stub.
2. Merge-aware TBoard (kanban.data): tracks last-known mtime, deleted IDs
(so re-reads don't resurrect user-deleted tasks), and provides
HasExternalChanges/MergeFromDisk. Save() now auto-merges before
writing. ID conflicts (same ID, different task) are resolved by
reassigning the in-memory task to a fresh ID.
3. Idle polling in TKanbanUI.Run: every ~2 seconds (20 idle ReadKey
cycles at VTIME=1), checks HasExternalChanges and merges new tasks
with a status bar notification.
Change g_exc_top and g_current_exception from var to threadvar in
blaise_exc.pas, giving each thread its own exception chain. This is
required for safe multi-threaded exception handling.
Add the `threadvar` keyword for declaring thread-local storage variables.
Each thread gets its own zero-initialised copy. Only allowed at program
or unit scope.
Lexer/Parser: new tkThreadVar token; ParseVarBlock accepts threadvar.
AST: IsThreadVar field on TVarDecl, TAssignment, TIdentExpr.
Symbol table: IsThreadVar on TSymbol.
Semantic: propagates IsThreadVar through analysis; rejects local scope.
QBE backend: emits `export thread data` declarations and `thread $Name`
references for correct TLS access (local-exec model via %fs:@tpoff).
Native backend: emits .tbss section and %fs:Name@tpoff addressing.
Unit interface: serialises IsThreadVar through .bif files.
Tests: 8 unit tests (parser, semantic, IR emission) + 2 E2E tests
(compile and run programs using threadvars).
Add inline Inc(x)/Inc(x,n)/Dec(x)/Dec(x,n) lowering for both 32-bit
and 64-bit operands — previously these emitted an empty callq that
caused assembler errors.
Zero-initialise class-typed local variables in function prologues and
release them in epilogues, matching the QBE backend's ARC behaviour.
Without this, the first assignment to a class local tried to
_ClassRelease uninitialised stack garbage.
Handle .Free on expression-based receivers (e.g. TThing(P).Free)
where the receiver is a temporary with no named slot to nil-out.
The QBE backend lowers O.Free to _ClassRelease + nil-out-slot, but
the native backend had no handling for it — Free has no ResolvedMethod
(it is a compiler intrinsic, not a real symbol table entry) so
EmitMethodCallStmt raised an error.
Added the same lowering: load slot value, call _ClassRelease, store
zero. Handles both local/global variables and implicit-Self field
access (Self.FChild.Free).
Assigning nil to a class-typed variable (O := nil) did not call
_ClassRelease on the old value in either backend. The nil literal
has ResolvedType.Kind = tyNil which bypassed the tyClass ARC branch
in EmitAssignment, falling through to the generic scalar store that
performs no reference counting.
QBE backend: added a class-nil branch in EmitAssignment that loads the
old value, calls _ClassRelease, and stores zero. Handles strong, weak,
and promoted-local cases.
Native backend: added the same class-nil assignment branch, plus two
further fixes:
- EmitExprToEax now handles TNilLiteral (emits xorq %rax, %rax)
- EmitFieldCleanupFn now emits the destructor call and ARC field
releases instead of being an empty stub, bringing it to parity with
the QBE backend's FieldCleanup emission.
Updated leak-check tests whose leak scenarios relied on the broken
nil-assignment behaviour — they now use _ClassAddRef to create genuine
unbalanced reference leaks.
Updated test count in README (2293 -> 2606).
Two bugs prevented --incremental from producing linkable per-unit .o files:
1. Per-unit codegen used Semantic.GetSymbolTable which returns nil after
Analyse() (the table is transferred to Prog.SymbolTable). This caused
all class metadata (typeinfo, vtables, field cleanup) to be silently
skipped. Fixed by using Prog.SymbolTable.
2. All functions and data emitted by AppendUnit lacked the QBE `export`
keyword, making symbols file-local in each .o — the linker could not
resolve cross-unit references. Added FExportAll flag and ExportPrefix
helper to TCodeGenQBE; the incremental path sets SetExportAll(True).
Additionally, system defs (TObject/TCustomAttribute typeinfo, vtable,
FieldCleanup stubs) are now skipped in per-unit .o mode (they would
cause multiple-definition errors) and always exported in the program
path so all per-unit .o files can reference them.
The kanban board is now a proper tool alongside migration-analyser.
Adds tools/kanban/project.xml (blaise-kanban module) and registers
it in the root aggregator. Removes the standalone build.sh script
since PasBuild handles compilation.
Store created timestamps as ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS) in
.kanban files instead of local date-only strings. The detail view
converts UTC to the local date via CreatedToLocalDate before display.
Use InstantNow, SystemOffset, and TDate.ToString from the DateUtils unit
instead of calling _TimeNow/_TimeLocalOffsetSecs/_TimeSplit directly and
manually formatting the date string.
DrawBox was losing its foreground colour after rendering the bold
title text because ResetAttr cleared all attributes. Now DrawBox
takes a Color parameter, sets it up front, and re-applies it after
the title reset so side borders and bottom are drawn in colour too.
Adds --add flag to create tasks from the command line without
launching the TUI. Supports --detail, --detail-file, --priority,
--status, and --help options.
Examples:
kanban board.kanban --add "Fix bug" --priority high
kanban board.kanban --add "Task" --detail-file notes.txt
First example application written in Blaise — a three-column terminal
Kanban board (Todo / In Progress / Done) with vim-style keybindings.
Features: task CRUD, move between columns, priority tagging, per-task
detail files (editable via $EDITOR), ISO-8601 created dates, persistent
.kanban file format, ANSI colour output with box-drawing characters.
The terminal unit uses pure POSIX bindings via `external name`
(tcgetattr, tcsetattr, ioctl, read) — no C code required.
Covers the codegen fix for TFieldAccessExpr on record-typed fields,
which previously emitted a scalar load instead of returning the field
address. Tests both QBE (D.ToString via string return) and native
(D.Sum via integer return) backends, verifying chained field access
(DT.Date), record-to-record assignment, and method calls on nested
record fields.
The `not` operator previously only accepted Boolean operands. It now
accepts all integer types (Byte, SmallInt, Word, Integer, Int64, UInt64)
and performs bitwise complement, enabling bitmask patterns like
`Flags := Flags and (not MASK)`.
Semantic pass promotes narrow types to Integer, preserves Int64/UInt64.
QBE backend emits `xor -1` (w or l suffix by width). Native backend
emits `notl`/`notq`. Also adds missing bitwise binary ops (and, or,
xor, shl, shr) and nested record field read/write support to the
native backend.
Full address-of expression support in the x86-64 native backend:
- @Variable (local and global)
- @VarParam (load pointer value from param slot)
- @StaticArray[I], @OpenArray[I], @DynArray[I] (element address)
- @Rec.Arr[I] (field array element address, all access patterns)
- @FuncName (code address with mangled name via ResolvedFreeRoutine)
- @Obj.Method (method-pointer construction at assignment level)
- @Rec.Field (general field address)
Also adds TDerefExpr (P^ read) and TPointerWriteStmt (P^ := val)
with ARC handling for string and class element types.
Includes five e2e tests covering @Variable, @StaticArr[I],
@DynArr[I], @Rec.Arr[I], and @Obj.Method patterns.
Retain string, class, and interface value params on function entry and
release them on exit, matching the QBE backend's ARC semantics. Const,
var, out, and open-array params are skipped. Includes e2e tests for
string and class value param passing.
Add proper ARC handling when storing to class or string fields via
class-access or implicit-self paths. [Unretained] fields store-only
with optional RHS release, [Weak] fields call _WeakAssign, normal
class fields do addref(new)/release(old)/store, and string fields
do _StringAddRef/_StringRelease. Adds NativeExprOwnsRef helper.
Includes e2e tests for class field store+read and string field
store+read.
Implement IsArrayAccess handling in the native x86-64 backend for
record field reads. Supports static arrays, dynamic arrays, and open
arrays with correct element-address computation (base + field offset +
index * element size). Also fix var-param type validation to skip the
unsupported-type check for var/out params (always pointer-sized).
Implement monomorphised generic emission for all four generic kinds:
generic classes (typeinfo, vtable, field cleanup, name strings, method
bodies), generic records (method bodies), generic interfaces (typeinfo),
generic standalone functions, and class-to-interface itab/impllist.
Also fixes three latent native backend bugs exposed by generic code
paths: NativeMangle was not applied in FuncSymbolFromDecl/FuncSymbolOf
(raw angle brackets in assembly symbols), TFieldAccessExpr.IsMethodCall
was unhandled (zero-arg method calls like B.GetVal), and IsVarParam
field access was not dereferenced (record Self passed by pointer).
Four new e2e tests verify generic records, classes, standalone functions,
and class-implementing-interface all produce correct output on both QBE
and native backends.
QBEMangle was not applied to the class name portion of itab symbols when
emitting class-to-interface assignments for generic classes. This caused
QBE to reject symbols containing raw angle brackets (e.g.
$itab_TBox<Integer>_IValue). Now emits $itab_TBox_Integer_IValue.
Extend the generics system to support record types alongside classes and
interfaces. Generic records use the same <T> syntax and monomorphization
strategy: each instantiation (e.g. TMyVal<Integer>) creates a concrete
TRecordTypeDesc with substituted field types and re-analysed method bodies.
Unlike generic classes, generic records do not emit typeinfo, vtable, or
field-cleanup data — they are value types with no class metadata.
Parser, semantic, QBE codegen, and native codegen all updated. 15 new
unit tests (parser + semantic + codegen) and 4 E2E tests. Fixpoint OK.
Add _SysWriteBool to the runtime platform layer so WriteLn(Boolean)
prints 'True' or 'False' instead of '1' or '0', matching Delphi/FPC
behaviour. Both QBE and native x86_64 backends emit the new call for
tyBoolean arguments. Updated ~65 assertions across 13 test files.
Remove sLineBreak (Delphi alias for LineEnding) and PathDelim (Delphi
alias for DirectorySeparator). Blaise keeps exactly three platform
constants: LineEnding, DirectorySeparator, PathSeparator.
Platform constant values are now derived from GTarget (the compilation
target) via TargetLineEnding/TargetDirectorySeparator/TargetPathSeparator
in blaise.codegen.target, so cross-compilation produces the correct
values without conditional compilation.
Assigning a function return (owned +1 ref) to an [Unretained] field
leaked the temporary — the field borrows but nobody consumed the +1.
Both EmitAssignment (implicit-Self path) and EmitFieldAssignment now
call _ClassRelease on the value when ExprOwnsRef indicates ownership.
The parser absorbs [I] in R.Arr[I] into TFieldAccessExpr.PropIndexExpr,
which the semantic pass and codegen only handled for indexed properties
and string char access. Array-typed fields (dynamic, static, open) were
left unresolved, producing a type mismatch ("^array of T" instead of
"^ElementType").
Add IsArrayAccess flag to TFieldAccessExpr, resolve it in all three
PropIndexExpr sites in uSemantic (Base-chained, RecordName-leaf, and
trailing field path), and emit the element-address computation in
uCodeGenQBE for both EmitExpr and EmitAddrOfExpr.
Native backend has TODO markers for the new flag.