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.
Interface parameters are fat pointers (obj + itab = 2 × 8 bytes) that
consume two consecutive integer register slots in the x86-64 SysV ABI.
Previously the native backend rejected any function with an interface-typed
parameter with "unsupported parameter type".
Changes in blaise.codegen.native.x86_64.pas:
BuildFrame: interface params now get a 16-byte slot (same as interface
locals) and advance IntIdx twice, matching the two-register convention.
EmitFunctionDef prologue: spill both incoming registers into the 16-byte
slot — obj register into slot base (VarOperand), itab register into
base+8 (IntfItabOperand).
Type validation gate: add tyInterface to the accepted parameter kinds.
EmitMethodArgPush: new interface branch pushes obj first then itab (pop
loop runs high-to-low so itab lands in the higher register). Handles
TIdentExpr (load from _obj/_itab slots), TAsExpr (runtime _GetItab),
implicit-Self field (load from Self+FieldOffset), and class expressions
(static itab symbol).
CountArgSlots: new helper that counts register slots rather than logical
argument positions; interface and open-array params each count as 2.
EmitMethodCallExpr: add IsConstructorCall path (TMethodCallExpr) with
full arg loop. Pop loop now uses CountArgSlots.
EmitMethodCallStmt / EmitInheritedCall: pop loops use CountArgSlots;
inherited call removes the "not yet supported" error.
EmitInterfaceCall / EmitInterfaceFieldCall: arg loops push two values for
interface-typed arguments; slot-count pop replaces arg-count pop.
EmitCall (standalone function): slot-count detection and push loop both
handle interface params as 2-slot args.
Changes in uCodeGenQBE.pas:
All arg loops that call InterfaceArgFragment or EmitInterfaceExprPair
now intercept the case where the actual argument is a class expression
(ResolvedType.Kind = tyClass) and emit the static itab symbol directly,
instead of routing through EmitInterfaceExprPair which only handles
TIdentExpr and TAsExpr. Affected sites: EmitMethodCall, EmitInheritedCall,
EmitProcCall (two arg loops).
Tests: five new AssertRunsOnBoth cases in cp.test.e2e.native.pas covering
interface arg to a standalone procedure, class method, constructor,
inherited call, and a class expression passed directly as an interface
parameter. TestRun_Native_InterfaceField_ShadowsGlobal upgraded from
QBE-only to AssertRunsOnBoth now that interface params work on native.
Inside a method, an unqualified name should resolve as:
local/param > implicit Self.field > unit/program global
The semantic analyser (AnalyseAssignment, AnalyseProcCall, and the
TIdentExpr read path) only fell back to the class-field lookup when
the symbol table returned nil. When a same-named program-level global
existed, Lookup() returned it — bypassing the field altogether.
Fix: in all three resolution sites, also try the class-field path when
the symbol found is a global (IsGlobal=true), mirroring Pascal's
field-shadowing rules.
Also fix the implicit-Self interface-field branch of AnalyseProcCall: it
was using ResolveMethodOverload for interface-typed fields, which doesn't
know about interface method descriptors. Now uses HasMethod + interface
dispatch (nil ResolvedMethod), matching the non-implicit path.
QBE codegen fixes:
- EmitAssignment/ImplicitSelfField: add tyInterface case — stores both
fat-pointer slots (obj+8, itab+8) with ARC, via new
EmitInterfaceToFieldSlots helper.
- EmitProcCall interface dispatch: when IsImplicitSelf, load obj/itab
from Self+FieldOffset rather than from %_var_ObjectName slots.
- EmitExpr constructor call arg loop: detect tyInterface params and emit
the fat-pointer pair (obj + static itab) instead of a single word.
- EmitInterfaceExprPair: handle implicit-Self TIdentExpr (loads from
Self+FieldOffset) and class expressions (runtime _GetItab lookup).
Native x86_64 backend fixes:
- EmitInterfaceAssign: handle ImplicitSelfField — compute Self+offset
into %r15 (callee-saved) so the fat-pointer stores survive intervening
_ClassAddRef / _ClassRelease calls.
- New EmitInterfaceFieldCall: dispatches interface methods through a class
field (Self+FieldOffset) rather than a variable slot.
- EmitMethodCallStmt interface dispatch: route through EmitInterfaceFieldCall
when IsImplicitSelf.
Regression tests: cp.test.interfaces TestSemantic_InterfaceField_ShadowsGlobal_OK
(unit), cp.test.e2e.classes2 TestRun_InterfaceField_ShadowsGlobal_Dispatches (QBE
e2e), cp.test.e2e.native TestRun_Native_InterfaceField_ShadowsGlobal (QBE path only
— native backend interface-arg passing is a separate open item, documented in
bugs.txt).
Closes issue #64.
Bring in Andrew Haines's regression test (from his bf8d94a) that assigns a
Pointer-returning rvalue (TObjectList.Items[0]) into a TObject local and asserts
the list-held object survives the borrowing procedure (Destroy counter 0 after
Borrow, 1 after L.Free).
Only the test is brought in, not the accompanying code change: bf8d94a switched
three assignment ARC dispatch sites from RHS-type to LHS-type to fix a net -1
imbalance, but that bug does not reproduce on our master — our EmitAssignment
already retains when a Pointer rvalue (including an explicit TThing(P) cast) is
stored into a class-typed local, so all the repro variants are balanced. The
test passes as-is and now guards that behaviour from regressing.
Full suite 0 failures. (Test-only change — no compiler source touched, so
fixpoint is unaffected.)
Reapply the const-parameter ARC elision that was reverted in 03b59e8,
now paired with the caller-side transient retain that makes it sound.
The four entry/exit ARC loops skip IsConstParam again (a const parameter's
object is kept alive by the caller for the whole call, so the callee needs no
_StringAddRef/_StringRelease or _ClassAddRef/_ClassRelease pair). The earlier
revert was because that premise fails for a TEMPORARY bound to a const param
(e.g. `Use(A + ' ' + B)`): the concat result is +0, its only reference is the
argument slot, and with the callee retain elided it was freed mid-call.
The fix (Andrew Haines, cherry-picked from the llvm branch — commits 00fcf41 +
e94e70544) adds the missing reference at the CALL SITE: EnsureConstStringRef
emits _StringAddRef before the call and ReleaseConstStringArgs emits
_StringRelease after, for each value-mode argument to a const-string parameter.
The pair is a no-op on immortal literals and nets to zero on owned strings, so
the overhead falls only on the transients that actually need it.
Tests: restored the elision IR tests (string + interface const params),
Andrew's caller-retains-transient IR tests and valgrind e2e
(TestRun_ConstStringParam_TransientRetained_Valgrind), alongside the existing
TestRun_ConstStringTemp_StaysAlive_Valgrind. docs/future-improvements.adoc marks
the optimisation shipped. Full suite 0 failures; FIXPOINT_OK; the original
metaclass-ref crash (`C := TFoo`) compiles cleanly.
The native method-call paths (EmitMethodCallStmt, EmitMethodCallExpr,
EmitInheritedCall) pushed every argument by value, so a `var`/`out` parameter
to a method received a copy and the caller's variable was not mutated — silently
wrong (e.g. `F.Bump(N)` left N unchanged). The standalone EmitCall path already
passed var args by address; the method paths did not.
Add EmitMethodArgPush: for a var/out param it pushes the argument's address
(leaq for a local/global, or the stored pointer when the arg is itself a var
param), otherwise the value. Use it in all three method-call paths. This also
lets EmitInheritedCall accept var args (the earlier "not yet supported" guard is
now only for interface args, which the native call ABI still cannot pass).
Two AssertRunsOnBoth e2e tests (a single var param mutated; a two-var-param
swap) match the QBE oracle. Native suite 95 tests; full suite 0 failures;
FIXPOINT_OK.
The native backend rejected `inherited Method[(args)]` with "unsupported
statement TInheritedCallStmt". Add EmitInheritedCall: a direct static call to
the parent method (bypassing the vtable, as inherited must), with Self taken
from the current method's Self slot. Value arguments are pushed/popped into the
SysV registers as for a regular method call; a value-returning parent stores its
result into the Result slot so `inherited F;` as a statement seeds Result.
var/out and interface arguments to an inherited call raise a clear
"not yet supported" — the native call ABI does not yet pass arguments by address
or as interface fat pointers, so failing loudly beats passing the wrong thing.
Two AssertRunsOnBoth e2e tests (Inherited_Proc; Inherited_FuncSetsResult, which
exercises the Result-seeding path) match the QBE oracle. Native suite 93 tests;
full suite 0 failures; FIXPOINT_OK.
This reverts commit 5a5b5d4. The optimisation elided the callee-side
ARC retain/release for const string/class/interface value params on the
premise that the caller keeps the argument alive for the whole call. That
premise fails for a TEMPORARY bound to a const param (e.g. `Use(A + ' ' + B)`):
the concatenation result's only reference is the argument slot, so without the
callee-side retain its refcount hits zero at the call boundary and it is freed
before the callee reads it — a use-after-free.
This bit the RTL hardest: `_StringCopy` / `StrHead` take `const string` params
and are called with built-at-runtime temporaries, so the emitted RTL was
miscompiled. Under self-hosting the defect is self-reproducing and only
manifests at the SECOND generation (the compiler that emits the broken RTL is
itself fine), which is why a one-step fixpoint did not expose it and the
compiler's own sources did not reliably trigger it. The symptom was a
deterministic crash compiling any program that uses a metaclass reference
(`C := TFoo`) or HasClassAttribute — which is why TestRunner (via
blaise.testing.runner.text) could not be built, blocking the whole suite.
The original change's valgrind e2e test passed only because it bound a string
LITERAL (immortal) to the const param, not a temporary.
Removed the now-invalid IR/e2e tests that asserted the elided behaviour
(string const params, and the interface-const variant from 088d12f which
relied on this commit's IsConstParam guard). Added
TestRun_ConstStringTemp_StaysAlive_Valgrind, which passes a concatenation
result as a const string param and reads it in the callee — the exact case the
optimisation broke. docs/future-improvements.adoc records how to re-attempt the
elision safely (condition on the argument, not the parameter; retain temporaries
either caller- or callee-side).
Verified: stage-2 build clean, TestRunner builds, full suite 0 failures,
FIXPOINT_OK.
EmitInheritedCall's argument loop had no var-param branch: it always
evaluated the argument and passed its loaded value, so `inherited Foo(X)`
where the parent method declares `var X` passed a `w` value where the callee
expects an `l` address. The parent then dereferenced a small integer as a
pointer and the program segfaulted.
Pass EmitLValueAddr(arg) as an `l` for var/out params, matching every other
method-dispatch arg loop.
e2e test: a TDer.Bump override that calls `inherited Bump(X)` on a `var X`
param must mutate the caller's variable (5 -> 16). FIXPOINT_OK.
A *method* (OwnerTypeName<>'') taking a by-value interface parameter was
emitted as a single `w` slot instead of the two-slot fat pointer (obj +
itab) the standalone-routine path already uses. EmitMethodDef's signature
and EmitParamAllocs both fell into the generic `else`, so the callee's
`storel %_par_X` was invalid QBE ("invalid type for first operand") and the
method failed to compile; dispatch inside it would also have read an
undefined %_var_X_obj.
Split the interface param into `l %_par_X_obj, l %_par_X_itab` in the method
signature and alloc both `%_var_X_obj`/`%_var_X_itab` local slots, mirroring
the standalone-routine path. The call sites must pass both halves too: add
an InterfaceArgFragment helper (wraps EmitInterfaceExprPair) and use it in
the five method-call arg loops (sret method-call expr, non-sret method-call
expr, the value-returning and statement method dispatch, and inherited
calls) so an interface argument is forwarded as the obj+itab pair.
Latent, not a regression: the compiler's own source declares no method with
a by-value interface param, so the fixpoint stayed clean; it bit user code
only. Two IR unit tests (method signature splits the param; the call site
passes both slots) and one e2e test (a class method taking an interface and
dispatching through it returns the right value). Full interface + ARC
suites green; FIXPOINT_OK.
Interface vars become fat pointers: locals a contiguous 16-byte slot
(obj@+0, itab@+8); globals two .data labels Name_obj/Name_itab, matching
the QBE backend's $Name_obj/$Name_itab. AddSlot reserves 16 bytes for
tyInterface; IntfObjOperand/IntfItabOperand return the two halves for
locals (rbp-relative) or globals (RIP labels).
EmitInterfaceCall lowers a dispatch: push args, load obj->%r10 and
itab->%rax, index the itab by MethodIndex*8 (a flat method-pointer array,
no leading typeinfo slot unlike a vtable), load the code ptr->%r11, pop
args into %rsi.. (shifted for Self in %rdi), then callq *%r11. Wired into
EmitExprToEax (TFieldAccessExpr.IsInterfaceCall = zero-arg),
EmitMethodCallExpr and EmitMethodCallStmt (ResolvedClassType = tyInterface,
with args).
EmitInterfaceAssign handles four RHS forms, strong refs only: := nil
(release obj, zero both slots); class->interface (static itab_Class_Intf,
addref/release obj); interface->interface copy (load src obj+itab,
addref/release, store both); T as IFoo (_GetItab(obj, typeinfo_Intf); nil
result -> _Raise_InvalidCast; else addref/release/store). ARC operands are
kept on the stack across the AddRef/Release calls, so no callee-saved
register is relied upon.
EmitInterfaceDefs emits typeinfo_<Intf> (.quad 0 identity token), flat
itab_<Class>_<Intf> method-pointer arrays (abstract slots ->
_AbstractMethodError), and NULL-terminated impllist_<Class> (typeinfo,itab)
pairs — mirrors the QBE EmitInterfaceDefs. Run after EmitClassSection so the
method symbols and class-name strings it references exist. EmitDataSection
emits Name_obj/Name_itab for interface globals; the function prologue
zero-inits string and interface locals (both fat-pointer halves) so the
first assignment's release-old step sees nil; the epilogue releases
interface locals via _ClassRelease on the obj slot.
6 new AssertRunsOnBoth e2e tests (ZeroArg, Arg, Proc, IntfToIntfCopy,
AsCast, NilClear) — all pass on QBE and native; valgrind clean on the
as-cast program. FIXPOINT_OK.
Weak interface refs (_WeakAssign/_WeakClear) deferred. Pointer size is the
literal 8 here, matching the rest of this x86_64-only unit; i386/arm64 will
be separate TNativeBackend subclasses.
By-value interface params were never retained on entry or released on exit:
the QBE entry/exit ARC loops only handled tyString and tyClass. A callee
could therefore drop the caller's sole reference mid-call and then use the
param as a freed object. Add a tyInterface branch to all four loops,
AddRef/Release on the object slot of the fat pointer (the itab is static).
const and var interface params correctly emit no retain/release — the
const skip is the IsConstParam guard, and var params never reached the
class/interface branches.
Two IR unit tests (value param retained; const/var not) and one e2e
Valgrind test that drops the caller's reference mid-call and reuses the
param, proving the retain prevents a use-after-free and the release
balances it.
The native x86_64 backend still does not retain value params at all; its
TODO is updated to cover interfaces alongside string/class.
A const parameter guarantees the caller keeps the argument alive for the
whole call, so the callee-side _StringAddRef/_StringRelease and
_ClassAddRef/_ClassRelease pair is redundant. Skip it in all four ARC
entry/exit loops (method + standalone routine) by adding IsConstParam to
the existing IsVarParam/IsOpenArray guard.
The native x86_64 backend does not yet retain/release value params at all,
so there is nothing to elide there today; a TODO marks that it must respect
IsConstParam when that retain/release is added.
Covered by two IR unit tests and one e2e Valgrind test confirming the
elision is balanced (no leak, no use-after-free).
fixpoint.sh's runtime build (step 1) relied on the runtime Makefile's default
BLAISE=../compiler/target/blaise. After scripts/rolling-bootstrap.sh — which
installs only to releases/ and leaves the live compiler/target/ empty — that
path does not exist, so `cd runtime && make` failed with
"compiler/target/blaise: No such file or directory" (RUNTIME_FAIL) even though
the release binary was perfectly usable.
Pass BLAISE=<abs stage-1 path> into both the runtime `make` and `make install`
so the build never depends on a pre-existing compiler/target/blaise. Resolve
the RTL archive into $RTL_ARCHIVE once (installed compiler/target copy, falling
back to runtime/target/), and use it at both link sites.
Verified by running the patched script from a fresh clone with no
compiler/target/ present (the exact post-rolling-bootstrap state): reaches
FIXPOINT_OK where it previously aborted at step 1.
cdecl/stdcall/register/pascal/safecall directives were recognised by the
parser but silently discarded. Record them on TMethodDecl.CallingConv (both
the standalone-routine and class-method directive loops), copy through
CloneMethodDecl, propagate into TRoutineSig.CallingConv in BuildRoutineSig,
and persist in the free-routine .bif format (appended per routine record,
symmetric writer/reader) so the convention survives separate compilation.
Codegen is unchanged: every routine still emits the System V AMD64 convention
(which is the C ABI on Linux x86_64, so cdecl and the default already agree).
The directive is metadata only — the prerequisite for a future Windows/x86
target where stdcall and cdecl differ, and for faithful FFI/debugger tooling.
Replace the pending-placeholder TestCallingConv_Cdecl_Preserved with a real
assertion that 'procedure Beep; cdecl;' yields CallingConv='cdecl'.
Grammar and rationale updated: MethodDirective notes the retained conventions
and a new rationale subsection records the metadata-only decision.
The compiler test suite is now fully green (2518 tests, 0 failures).
'out' was parsed as a synonym for 'var' — by-reference, but indistinguishable
from 'var' afterwards. Add TMethodParam.IsOutParam, set alongside IsVarParam
when the 'out' keyword is present, and carry it through CloneMethodParam and
the .bif param-flags pack (new bit 3) so the loader's TRoutineSig and any
future tooling can recover the declared mode.
Codegen is unchanged: 'out' still lowers identically to 'var' (a pointer
parameter). The flag is metadata only — the prerequisite for a future
read-before-write lint and/or zero-on-entry semantics.
Replace the pending-placeholder TestParam_ModeOut_Preserved with a real
assertion that 'out' yields IsOutParam=True, IsVarParam=True, IsConstParam=False.
Grammar and rationale updated to match: ParamGroup gains the OUT alternative
and the out-parameter section documents the preserved-metadata decision.
In --debug mode EmitExpr registers each newly allocated instance with the leak
tracker by loading the class-name pointer from $typeinfo_<Class>+16. Both
register sites built that symbol with a bare QBEMangle(Name), omitting the
prefix that ClassSymName applies. The typeinfo data itself is defined as
$typeinfo_<prefix><Class> (e.g. $typeinfo_P_TBox for a program-scope class),
so the reference resolved to an undefined $typeinfo_TBox and every --debug
build failed to link.
Route both leak-tracker references through ClassSymName(QBEMangle(Name)), the
same form the adjacent _ClassAlloc and vtable-store lines already use.
Fixes all six TE2ELeakCheckTests, which compile and run real --debug binaries
and so exercise the link step the IR-only harness cannot see.
EmitForInStmt built the GetEnumerator/MoveNext/GetCurrent call names with a
bare QBEMangle(OwnerTypeName + '_' + Name), omitting any unit prefix. For an
enumerable class defined in a unit (e.g. TStringList in Classes) the method
def is emitted as classes_TStringList_GetEnumerator while the for-in call site
emitted TStringList_GetEnumerator, yielding undefined-reference link errors.
Route all three calls through MethodEmitName, which prefers the method's
ResolvedQbeName and otherwise falls back to ClassUnitPrefix-qualified names —
the same resolution used by ordinary method calls.
Covered by the existing e2e test TE2ETStringListTests.TestRun_ForIn, which
iterates a real Classes.TStringList; the IR-only harness compiles a single
program and cannot model a unit-defined enumerable, so the e2e layer is the
correct guard here.
InstantiateGeneric mangled each generic-instance method's ResolvedQbeName
with MangleUnitPrefix(Sym.OwningUnit). For program-scope generics OwningUnit
is the program name (e.g. 'P'), yielding a spurious 'P_' prefix. The method
def and itab then used '$P_TBox_Integer_SetValue' while the property
setter/getter call path used ClassUnitPrefix (which returns '' for program
scope), producing 'undefined reference' link errors.
Use CurrentUnitPrefix instead: '' for programs, the mangled unit prefix for
units — consistent with the property-call path on both sides.
Also pin the generic instance's DestroyResolvedQbeName to the same prefix as
the method def, so the emitted $_FieldCleanup_<T> calls the destructor symbol
that actually exists. Previously it was left empty and the cleanup fell back
to ClassUnitPrefix(), which disagreed for program-scope generics and broke
the TList<T> e2e link tests.
Update the IR-level test assertions across the generic suites to expect the
unprefixed symbol names, and cp.test.unitinterface.pas to expect the unit
prefix for unit-scope classes.
Now that the compiler supports `blaise --source Unit.pas --output Unit.o`
directly, the build-driver pattern (a thin program wrapper + IR sed strip)
is no longer needed. Replace every multi-step rule with a single invocation
and delete the eight *_build_driver.pas files.
This commit must follow the merge of blaise_clean_split in git history so
that the rolling-bootstrap script can build the merge commit using the
previous binary (which still uses the old Makefile), then build this commit
using the freshly-merged binary (which understands unit-as-top-level).
The Write* helpers in uUnitInterfaceIO.pas all build their output as
TStringList instances and serialise via SL.Text. That's quadratic
under the covers (every Add reallocates the joined view) and ties the
exact line ending to whatever TStringList chooses. Switch the
accumulator to stdlib TStringBuilder.AppendLine, which appends #10
explicitly and avoids the join cost on every Add.
No on-disk format change — the wire output is byte-identical because
both paths emit #10-separated records. Self-bootstrap and TestRunner
(2346 tests) pass.