EmitFunctionDef loaded the function Result into %rax (or %xmm0) at the top of
the epilogue, then ran the local/param ARC release pass — which calls
_ClassRelease / _StringRelease / _DynArrayRelease. Those calls clobber %rax, so
any value-returning function that also releases an ARC-managed parameter or
local returned garbage.
Most visibly, a method returning Integer but taking an interface (or class)
value parameter releases that parameter on exit, so `U.Use(I)` returned 3
instead of 55; a String-returning function taking a String parameter returned a
clobbered pointer.
Move the Result load to AFTER every ARC release call, immediately before
leave/ret, so nothing can clobber it. sret (record-returning) functions are
unaffected — they return via the caller's buffer and load no Result. The $main
epilogue already sets %eax=0 after its global-release pass, so it was correct.
Two e2e regression tests on both backends: TestRun_Native_RetValSurvivesArcRelease
(value return past a class-param release) and TestRun_Native_IntfArgToMethod
(interface value arg to a method, dispatched in the callee).
Full suite OK (2681), FIXPOINT_OK (native-backend-only change).
The OPDF class record stores each class's VMTAddress (the debugger reads an
object's VMT pointer at runtime and matches it to identify the dynamic type).
The QBE backend emits user-class and generic-instance vtables as LOCAL symbols
(`data $vtable_X`), while the .opdf debug section — assembled as a separate
object file — references `vtable_X`. A local symbol is not visible across
object files, so every `--debug-opdf` build of a program containing a class
failed to link with "undefined reference to vtable_X".
Add an OPDF-mode flag to the codegen (ICodeGen.SetOpdfMode, wired from the
driver's OPDFEnabled). When on, the QBE backend emits user/generic vtables as
`export data` so the .opdf object can resolve them; when off (every normal
build) the output is byte-for-byte unchanged — verified: non-OPDF IR still emits
`data $vtable_TThing`, --debug-opdf emits `export data $vtable_TThing`. The
native backend already emits its vtables `.globl`, so its SetOpdfMode is a no-op.
Scope is deliberately gated on OPDF mode to avoid duplicate-symbol errors in the
separate-compilation (per-unit) path, where the same generic vtable can appear
in multiple objects; OPDF is only used with whole-program --source compiles.
Result: `--debug-opdf` now links and runs class programs, and the OPDF reference
debugger loads them. Full suite OK (2679), FIXPOINT_OK (output unchanged for
non-OPDF builds).
Two related gaps with interfaces held in a record/class field, across both
backends.
Gap 1 — native dispatch through a non-Self interface field (H.G.Greet()):
EmitInterfaceCall assumed a named interface receiver and emitted bare
_obj/_itab operands (empty name prefix) for a field receiver, failing to link.
It now takes an optional receiver expression; when that is an interface-typed
TFieldAccessExpr the obj/itab are loaded from the field's contiguous fat
pointer via the new EmitInterfaceFieldAddr helper. The TMethodCallExpr
interface-dispatch site passes ACall.ObjExpr.
Gap 2 — reading an interface out of a field into an interface local (G := H.G),
which failed on BOTH backends:
* QBE: the interface-to-interface assignment branch required a TIdentExpr RHS,
so a TFieldAccessExpr source fell through to the scalar store path and
emitted a single-slot `storew ..., $F` against a name that has only
$F_obj/$F_itab data definitions — an undefined-symbol link error. It now
accepts a TFieldAccessExpr RHS and resolves obj/itab via
EmitInterfaceExprPair (which already handles the field-access shape).
* Native: EmitInterfaceAssign gained a TFieldAccessExpr RHS case that loads
the source field's contiguous fat pointer via EmitInterfaceFieldAddr.
e2e: TestRun_Native_IntfFieldDispatch (dispatch with an argument through a
field) and TestRun_Native_IntfFieldReadIntoLocal (read field into local, then
dispatch) — both run on native and QBE via AssertRunsOnBoth.
Full suite OK (2679), FIXPOINT_OK.
The native x86-64 backend had no Chr builtin case. Chr(N) used in a String
context — `s := Chr(66)`, `'A' + Chr(66) + 'C'`, `s := s + Chr(i)` — emitted the
integer N where a String pointer was expected, so the following _StringAddRef
dereferenced the raw integer and segfaulted.
Add a Chr case to the native builtin-function dispatch that lowers Chr(N) to
_Chr(N), which allocates and returns a one-character heap String. The result is
a String pointer and flows through the existing String ARC assignment/concat
paths unchanged — matching the QBE backend.
e2e: TestRun_Native_String_ChrConcat covers assignment and a concatenation loop,
run on both backends via AssertRunsOnBoth. Full suite OK (2677), FIXPOINT_OK,
valgrind-clean.
The native x86-64 backend lacked the three recent QBE ARC fixes (f74e5cc,
96514ee, 935bd52) and carried two adjacent ARC gaps of its own. Managed
record/class fields, dyn-array variables, interface fields, and global
objects leaked or were never released, diverging from the QBE backend's
behaviour.
Ported the three QBE fixes:
* NativeExprOwnsRef broadened from tyClass-only to also cover tyString and
tyDynArray, so a String/dyn-array call result assigned into a variable or
field consumes its transferred +1 instead of being retained again
(mirrors 96514ee). Class retains stay unconditional — some method calls
return a borrowed class reference that NativeExprOwnsRef reports as owning,
so eliding the retain would release a reference the slot never acquired.
* EmitRecordFieldReleases (new): a field-kind walker that releases string,
class, dyn-array and interface-obj fields, clears weak fields, skips
unretained ones, and recurses into nested record fields. Used by
_FieldCleanup_<T> and by record-local scope-exit cleanup, so a class with
a record field whose own fields are managed no longer leaks them
(mirrors f74e5cc). Adds dyn-array variable assignment and dyn-array
local scope-exit release.
* EmitInterfaceToFieldSlotsAt (new): stores an interface into a record/class
field's contiguous 16-byte fat pointer (obj at +0, itab at +8) with ARC on
the obj slot — class source references the static itab_<Class>_<Interface>
symbol, interface-ident source copies both slots (mirrors 935bd52). A
unified interface-field block computes the receiver base for each shape and
dispatches to it.
Fixed two adjacent native-only ARC gaps:
* Program-exit global release (EmitGlobalReleases): the native $main epilogue
released no globals, so a global object's _FieldCleanup (and destructor)
never ran. The QBE backend releases each managed global at @main_exit;
the native epilogue now does the same for string/class/interface/dyn-array
and record-field globals.
* Chained field stores through a receiver expression (H.R.Obj := X) emitted a
plain store with no ARC; managed fields now retain the new value and release
the old, matching direct field stores.
The implicit-Self managed-field store (FName := x inside a method) was a plain
store with no ARC and was also intercepted by the type-keyed variable branches,
which treated the field name as a variable. It is now hoisted to the top of
the assignment handler and ARC-managed; interface implicit-Self fields continue
to route through EmitInterfaceAssign.
The ARC decision logic (NativeExprOwnsRef, the field-kind walks) is
target-independent; only the leaf emission is CPU-specific. A header comment in
blaise.codegen.native.backend.pas records that these walkers should lift into
the base class as template methods when the i386/arm64 backends land.
Tests: five cross-backend e2e tests in cp.test.e2e.native.pas
(TestRun_Native_Arc{DynArrayField, InterfaceField, NestedRecordField,
StringReturnToField, ImplicitSelfStringField}) — AssertRunsOnBoth compiles and
runs each program on both the native and QBE backends and asserts identical
stdout. They observe ARC through destructor side effects and read-back values.
Full suite OK (2676 tests), valgrind-clean on a combined stress program,
FIXPOINT_OK (the native backend is not on the self-hosting path).
Compiling even a tiny program leaked ~339 heap objects (TSymbol ×129,
TObjectList ×156, TTypeDesc ×14, plus the TProgram, TSymbolTable and
TSemanticAnalyser singletons), despite the driver correctly calling
Prog.Free() and Semantic.Free().
Root cause was a strong reference cycle: TSemanticAnalyser owns its
TSymbolTable (FTable), and the table held a plain strong reference back to
the analyser via FUsesChainProvider (set in Analyse so Lookup can consult
per-unit visibility). Under refcount ARC the two keep each other alive:
Semantic.Free() drops the analyser to rc=1 (the table still references it),
the analyser destructor never runs, FTable.Free() is never called, and the
entire reachable graph — every scope, symbol, builtin type descriptor, and
the whole AST — leaks as one unit.
Marking FUsesChainProvider [Unretained] breaks the cycle. The provider is
the analyser that owns the table and always outlives it (its destructor is
what frees the table), so a non-owning reference cannot dangle.
Impact: --debug leak count on a tiny compile drops from 339 to 14 (~96%
reclaimed). The remaining 14 are a separate, smaller global-scope fragment.
Full suite OK (2671 tests), self-compile clean, FIXPOINT_OK.
This adjusts TSymbolTable's field layout (one ARC slot becomes non-managed),
so it is a layout-affecting change validated via stage-2 fixpoint.
An interface stored in a record/class field could not be used: assigning a
class into such a field dropped the itab and skipped ARC, calling a method
through the field emitted an undefined %_var__obj temp that QBE rejected, and
the field was sized 8 bytes so a following field overlapped the itab slot.
Root cause: interfaces have two in-memory representations. A named interface
local/global uses two SEPARATELY-allocated slots (%_var_X_obj, %_var_X_itab);
an interface stored in a record/class field uses a CONTIGUOUS 16-byte fat
pointer (obj@+0, itab@+8). The codegen only knew how to read/write the
split-slot form, so any field-access receiver fell through to the split-slot
path and emitted undefined temps, and the field was never sized for 16 bytes.
This commit:
* uSymbolTable.pas — adds tyInterface: Result := 16 to RawSize and ByteSize
(fat pointer). Record/class field offsets and TotalSize flow from
ByteSize, so a following field now sits past the itab. AllocAlign stays 8
(two 8-byte slots). Interface LOCALS are unaffected — their split slots
are allocated by hardcoded codegen, not from these sizes. LAYOUT-CHANGING:
requires a stage-2 rebuild to validate.
* EmitInterfaceExprPair — adds a TFieldAccessExpr case that loads obj/itab
from the field's contiguous memory (addr / addr+8), so an interface read
from a record field can be passed as an argument or dispatched on.
* Both interface method-dispatch sites (EmitMethodCall and the
TMethodCallExpr emitter) route an expression receiver through
EmitInterfaceExprPair instead of the %_var_<ObjectName>_obj/_itab naming
that only a named local has.
* EmitFieldAssignment — adds a tyInterface case that stores the fat pointer
(obj@Ptr, itab@Ptr+8) with ARC via EmitInterfaceToFieldSlots, skipping the
single-value EmitExpr that would drop the itab.
* EmitInterfaceToFieldSlots — takes the destination interface type and, for a
class source, references the static $itab_<Class>_<Interface> symbol (as
the interface direct-assignment path does) instead of a bogus _GetItab call
keyed only by the class typeinfo, which returned the wrong itab.
Verified: interface-in-record assign + method dispatch + record copy all run
correctly; plain interface locals unchanged; full suite OK (2671 tests),
self-compile clean, --debug leak count unchanged at 339, FIXPOINT_OK.
Tests: e2e TestRun_Record_InterfaceField_AssignCallAndCopy plus IR tests
asserting no undefined %_var__obj temp, use of the static itab, and the
16-byte field layout (Tag at offset 16).
A function or method returning a String or dynamic array transfers a +1
reference to the caller (the callee AddRef'd on `Result := x` and did not
release Result at scope exit). The string and dyn-array assignment branches
nonetheless emitted an unconditional _StringAddRef / _DynArrayAddRef on the
call result, so every `r := MakeString()` / `r := MakeArray()` leaked one
buffer per call. Argument passing leaked the same way: a string/dyn-array
call-result passed by value was never released after the call.
Root cause: ExprOwnsRef — the helper that detects an already-owned +1
transient so the assignment site can skip the retain — was gated to tyClass
only. For strings and dyn-arrays it always returned False, so the retain
was never elided.
This commit:
* Broadens ExprOwnsRef to tyString and tyDynArray. The body logic
(function/method/property-getter calls own +1; variable/field reads and
casts are borrowed) is type-agnostic and already correct for these kinds.
* Guards the AddRef with `if not ExprOwnsRef` in the string and dyn-array
branches of variable assignment, var-param assignment, implicit-Self
field assignment, and field assignment — mirroring the class branches.
* Rewrites EmitOwnedArgReleases to dispatch the transient release on the
argument's ARC kind (_StringRelease / _DynArrayRelease / _ClassRelease —
the headers sit at different offsets, so a wrong-kind release corrupts
the heap) and to skip const-string params, which are already balanced by
EnsureConstStringRef + ReleaseConstStringArgs. Threads the param list
through all call sites.
The class field-assignment retain is deliberately left unconditional: some
method calls return a borrowed class reference without an AddRef yet
ExprOwnsRef reports them as owning, so guarding that retain would release a
reference the field never acquired (a use-after-free the self-hosting
compiler hits in its own codegen). That remaining transient leak is the
safe direction and is tracked among the known-leak backlog.
Verified: string-return leak 14MB->1.3MB, dyn-array-return leak 207MB->1.3MB,
string-arg leak bounded, all at 100k iterations. Full suite OK (2668 tests),
self-compile clean, FIXPOINT_OK.
Adds four IR regression tests in cp.test.arc.pas asserting the call-result
assignment elides the retain while a borrowed variable assignment still
retains, for both String and dyn-array.
Records with value semantics may contain fields with reference semantics
(String, dynamic array, interface, nested managed record). The QBE
backend's field walkers only covered the String and Class cases, so:
* A record whose field is a dynamic array leaked the buffer on every
copy and return-by-value — _DynArrayAddRef/_DynArrayRelease were not
invoked from EmitRecordCopy or EmitRecordReleaseFields, and there
was no scope-exit release for dyn-array locals.
* A class with a record field whose own fields were managed (String,
Class, dyn-array, interface, or a deeper record) leaked those
sub-fields when the class was released — _FieldCleanup_<T> skipped
record-typed fields entirely.
* Interface fields inside records were never copied or released.
This commit:
* Adds _DynArrayAddRef / _DynArrayRelease to blaise_arc.pas alongside
the existing _StringAddRef/_StringRelease and _ClassAddRef/_ClassRelease.
They walk the existing [refcount:4][length:4] dyn-array buffer header
(layout defined by _DynArraySetLength in blaise_str.pas), are nil-safe
and immortal-safe (rc = -1), and use _AtomicAddInt32 / _AtomicSubInt32
(lock xadd) so the refcount stays sound under threading — matching the
other ARC primitives.
* Extends EmitRecordCopy, EmitRecordReleaseFields, and
EmitFieldCleanupFn to handle tyDynArray, tyInterface, and nested
tyRecord fields (the latter by recursing through the shared
EmitRecordReleaseFields walker).
* Routes tyDynArray field assignments through the existing IsArc
path so a field write retains the new buffer and releases the old.
* Adds tyDynArray to EmitArcCleanup + EmitExcPathArcCleanup so
dyn-array locals balance their first-assignment retain at scope
exit (and on exception paths).
* Adds tyDynArray to EmitAssignment for variable-to-variable
assignment so b := a between dyn-array vars is refcount-symmetric.
Adds two regression tests in cp.test.e2e.records.pas:
* TestRun_Record_DynArrayField_ReturnByValue_NoLeak — 5000-iter
return-by-value of a record with a dyn-array field; asserts the
buffer contents survive (proves AddRef/Release pairing) and the
program runs to completion.
* TestRun_Class_RecordField_NestedClass_FullCleanup — class -> record
-> class -> record -> class chain with each Create/Destroy bumping
a global AliveCount; after 100 iterations AliveCount must be 0,
proving _FieldCleanup recurses through nested record fields.
TestRunner: OK (2663 tests). Self-bootstrap from a master-built stage-1
through stage-2/3/4 reaches a clean stage-3 == stage-4 IR fixpoint.
- Add TIndirectFuncCallExpr encoder/decoder to uUnitInterfaceIO.pas
(was missing entirely, causing --incremental to silently drop
indirect call nodes)
- Bump COMPILER_ID to blaise-0.11.0-dev+bif1
- Fix version check to compare base version only (strip -SNAPSHOT
and -dev suffixes before comparing)
- Change test to Ignore() when binary missing (module is not
activeByDefault)
- Remove redundant <version> from tool project.xml (inherits root)
- Regenerate bif-coverage.status for current AST (68 classes,
38 encoder/decoder cases)
Master's mandatory-parens enforcement (51ebf23) now flags every bare
zero-arg function reference. Sweep BifCoverage.pas and its TestRunner
wrapper, and swap sLineBreak for LineEnding.
Shell out to tools/bif-coverage/target/bif-coverage and assert exit 0.
Hard-fails (not skips) when the binary is missing - a working
verifier is a prerequisite for landing AST changes, and silently
skipping would let drift accumulate. No chdir or env setup needed
since bif-coverage now resolves its own paths from CWD.
Previously the binary used `../../compiler/src/main/pascal/...` and
`bif-coverage.status` directly, locking it to invocation from
tools/bif-coverage/. Now it walks up from CWD looking for a directory
that contains both `compiler/src/main/pascal/uAST.pas` and
`project.xml`, then resolves every other path under that root. Lets
the verifier be invoked from the project root, from any subdir, or
from a test runner's CWD (compiler/) without setup.
Static-analysis tool that cross-checks uAST.pas against
uUnitInterfaceIO.pas and the root project.xml. For every TASTStmt /
TASTExpr subclass it confirms the class has a dispatch case in
EncodeStmt/EncodeExpr and ReadStmt/ReadExpr, then walks the public
fields and ensures each is either referenced from both encoder and
decoder (`serialise`) or explicitly excluded (`safe`).
Truth is checked-in: bif-coverage.status is a flat file with one
`<TClass>.<Field> <serialise|safe>` line per field. The default
invocation diffs the live sources against the status file and reports:
[version] COMPILER_ID does not match root project <version>
[encoder] missing (Class.Field, uAST.pas line)
[decoder] missing (Class.Field, uAST.pas line)
[new] field exists in AST but is not in the status file
[stale] status names a field or class the AST no longer has
[broken] serialise field missing from encoder or decoder
[drift] safe field has crept into encoder/decoder (with the
offending uUnitInterfaceIO.pas line)
`bif-coverage --reset` regenerates the status file from current state,
inferring `serialise` when the encoder references the field and `safe`
otherwise. Use after deliberate AST or .bif format changes to
re-baseline.
Replace UseNative: Boolean with Backend: TBackend (bkQBE, bkNative).
The enum makes the driver's intent explicit — picking one of N codegen
pipelines — and adding a third backend (LLVM) becomes a one-line type
change plus exhaustiveness checks at the two dispatch sites.
Document the for-in string iteration dual-mode semantics: Byte loop
variable iterates raw UTF-8 bytes, Integer iterates codepoints via
_Utf8DecodeAt. Remove "deferred to future Runes(S) iterator" notes
since CodePointAt and for-in Integer are now implemented.
CodePointLength now delegates to _Utf8CountCodePoints in hand-written
x86_64 assembly. Processes 32 bytes/iteration with AVX2 (runtime-
detected via CPUID) and 16 bytes/iteration with SSE2, falling back to
scalar for tail bytes. The algorithm counts non-continuation bytes
(bytes where (b & 0xC0) != 0x80) using signed pcmpgtb against -65.
When the loop variable is Byte, for-in iterates raw UTF-8 bytes (existing
behaviour). When the loop variable is Integer, for-in now iterates Unicode
codepoints by calling _Utf8DecodeAt and advancing by the decoded byte
count. Any other loop variable type is a semantic error.
Both QBE and native x86-64 backends implement the new codegen path using
a synthetic __adv_N variable to preserve the byte advance across the loop
body. Includes E2E tests for 2-byte and 3-byte UTF-8 sequences, unit
tests for the semantic restriction, and 8 new E2E tests for the StrUtils
codepoint functions.
Seven new functions for Unicode-aware string operations:
- CodePointSize: byte width of a UTF-8 sequence from its lead byte
- CodePointLength: count codepoints in a string (O(n) scan)
- CodePointCopy: extract substring by codepoint index and count
- CodePointAt: codepoint value at a codepoint position
- CodePointPos: find substring, return codepoint index
- CodePointByteIndex: convert codepoint index to byte index
- CodePointFromByteIndex: decode codepoint at a byte position (O(1))
All functions use PChar arithmetic for zero-allocation performance.
Decodes one UTF-8 codepoint at a given byte index in a string. Returns
both the codepoint value and byte count packed into a single Int64:
(ByteCount shl 32) or CodePoint. This will be used by the for-in
codepoint iteration codegen and the StrUtils codepoint functions.
Both QBE and native backends were emitting \C3 format (bare backslash +
hex digits) for non-ASCII bytes in string constants. GNU as only
recognises \xNN as hex escapes; bare \NN is treated as an unknown
escape and the characters are output literally. This caused WriteLn to
print hex representations instead of raw UTF-8 bytes (e.g. 'Pâques'
displayed as 'PC3A2ques'). Fix: emit \xC3 instead of \C3.
The native backend was missing a handler for SizeOf() in EmitExprToEax,
causing it to fall through to the generic function call path and produce
garbage values. Emit movq $N, %rax where N is the resolved type's
ByteSize(), matching the QBE backend's compile-time constant folding.
Sets with 33–64 enum members now use 8-byte (QBE 'l' / x86-64 64-bit)
storage instead of silently truncating to 32 bits. Both QBE and native
x86-64 backends emit correct instructions for all set operations:
literals, in, Include/Exclude, union/difference/intersection, equality,
and for-in iteration. Enumerations with more than 64 members in a
set-of declaration are rejected with a clear semantic error.
Also fixes tkThreadvar missing from CheckUnitNamePart, which broke
parsing of unit names containing 'threadvar' in self-hosted builds.
PtrUInt (= UInt64) had no matching AssertEquals overload, causing
ambiguous-overload errors in test_blaise_mem. Added the UInt64 overload
to punit and registered UInt64ToStr in the symbol table (it was already
handled in semantic + codegen but missing from the built-in registry).
Reverts the Pointer-cast workaround from the previous commit — the test
now uses PtrUInt directly as intended.
PtrUInt maps to UInt64 which has no matching AssertEquals overload,
causing an ambiguous-overload error. Use Pointer cast instead since
the test is comparing pointer identity — the Pointer overload is the
correct match.
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.