S[I] := <byte> was rejected by the semantic pass ('is not a static array
or dynamic array') for any string — local, global, or var-param — on both
backends. It is now supported as the symmetric counterpart of the S[I]
read: an in-place byte store into a 0-based UTF-8 string.
Because strings are reference-counted and literals are immortal (stored in
read-only memory; the native backend put them in .rodata, so a naive store
segfaulted, while QBE silently mutated the shared literal), the write
performs copy-on-write. New RTL helper _StringUnique(S) returns S when it
is uniquely owned (rc=1) and otherwise allocates a fresh rc=1 copy, releases
the old reference, and returns the copy. Both backends emit
_StringUnique -> write the result back to the slot -> storeb, so the slot
keeps exactly one owned reference and mutating one alias never disturbs
another (Delphi/FPC UniqueString semantics).
The RHS accepts a numeric ordinal, Chr(n), or a single-character literal
(the byte-shaped forms used in place of a Char type). PChar subscript
writes keep their existing in-place path (raw pointer, no ARC header).
Dual-backend e2e tests (write, copy-on-write aliasing + literal reuse,
var-param) in cp.test.e2e.stringops. Updates language-rationale.adoc
(writable subscript + COW) and grammar.ebnf (string as a subscript-assign
base).
A unit's initialization section only runs when that unit is compiled
into the program's own translation unit; units linked from a prebuilt
archive never get their _init called. blaise_weak relies on its
initialization section to set up the weak-reference table mutex, so for
archive builds that setup never happened. A zero-filled mutex object is
a valid unlocked mutex on some platforms, which kept this latent — but
that is not guaranteed everywhere.
Add a _BlaiseInit RTL entry point that performs the one-time setup
(currently the weak-table mutex, guarded to stay idempotent) and have
each backend's $main emit a call to it right after _SetArgs, before any
user code runs. The mutex is now initialised explicitly instead of by
relying on a zero-filled object happening to be valid.
Regression: cp.test.codegen TestMain_CallsBlaiseInit asserts the call.
Two related set/enum fixes from shaking the sets cluster.
1. Set subset (<=) and superset (>=) operators, e.g. `if s <= t`:
- Semantic: <= and >= on two sets yield Boolean (alongside =, <>).
- Codegen both backends: non-jumbo sets test (s and not t)=0 for <=
(operands swapped for >=); jumbo sets call a new RTL _SetSubset
(added to blaise_set.pas) which returns whether every bit of A is in B.
Completes the set-operator suite (+, -, *, in, =, <>, <=, >= now all work).
2. Reject a variable that shadows a visible enum member (e.g. `var c` when
an enum member `C` is visible — Pascal is case-insensitive). Such a
shadow silently retargeted the member in a set literal `[A, C, D]` to the
variable: QBE errored with "Set literal element 'c' is not a constant",
and — worse — the native backend produced a WRONG bitmask with no error
(the member's bit was dropped). This surfaced as a corrupt for-in over a
set when the loop variable collided with a member. Now rejected at the
declaration with a clear message, mirroring the existing type-name-shadow
rule (FPC/Delphi allow the shadow; Blaise does not).
Verified subset/superset truth tables and for-in over a set (no shadow) on
both backends, and that the shadow is now a compile error. Adds
TE2ESetOpsTests.TestRun_Set_SubsetSuperset and
TSemanticTests.TestVarDecl_ShadowsEnumMember_RaisesError. Rationale's
"Variable Names May Not Shadow Type Names" section extended to enum members.
WriteDecimal negated N before extracting digits. For Low(Int64) =
-9223372036854775808 the negation overflows (no positive counterpart in
two's complement) and leaves the value negative, so the 'while AbsN > 0'
loop ran zero times and only the sign was emitted.
Extract digits while keeping the value negative — the negative range
reaches Low(Int64) — and negate each remainder. Also fixes
Low(Integer), which routes through the same function.
Adds e2e regression TestRun_Int64_MinValue.
Format() previously handled only %d, %s and %%; any float directive rendered
verbatim, and on the QBE backend a float argument failed to compile because the
double value was stored with 'storel <d-temp>', which QBE rejects.
RTL (blaise_float.pas): add _FormatFloatSpec(V, Spec, Prec) — a precision-aware
renderer for %f/%F, %e/%E and %g/%G built on the existing Grisu1 digit
generator, with round-half-up at the precision boundary (including all-nines
carry) and printf-style exponent formatting.
RTL (blaise_str.pas): rewrite _StringFormatN to parse the full
%[-][width][.prec]<conv> specifier syntax, render each argument (int/string/
float) into a growable buffer, and apply field-width space padding. A new
arg tag (2) carries the IEEE-754 binary64 bit pattern for float values.
QBE codegen: float Format arguments now emit tag 2 and reinterpret the double
bits to an integer via 'cast' before storel, fixing the compile error.
Tests: IR unit tests assert tag-2 emission and the bit cast. E2E coverage and
the native backend follow in the next commit.
Extend set of <enum> from a 64-member cap to 256, Java-EnumSet style. Sets
of 64 members or fewer keep the existing single-register bitmask (QBE w/l);
sets of 65..256 members ('jumbo') become an inline byte-array bitmap of
ceil(N/8) bytes, treated as a value aggregate (passed by reference, returned
via sret, memset/memcpy), with operations performed by new RTL helpers.
Representation (uSymbolTable): TSetTypeDesc.IsJumbo (BitCount > 64) and
RawByteSize; RawSize/ByteSize/AllocAlign sized accordingly. TSymbol gains
ConstSetBytes/ConstSetQbe for jumbo constants (can't fit an Int64 mask).
RTL: new runtime/src/main/pascal/blaise_set.pas — _SetIn/_SetInclude/
_SetExclude/_SetUnion/_SetInter/_SetDiff/_SetEqual/_SetCopy over byte-array
bitmaps (overlap-safe). Wired into runtime/Makefile.
Semantic: the four >64 caps become >256. AnalyseSetConstDecl folds a jumbo
const to a byte array. The anonymous set for 'X in [a,b,c]' is sized to the
largest listed ordinal (when constant), not the full enum — keeping the
common low-ordinal membership test (incl. the compiler's own TokenKind
tests) on the fast register path. This also fixes a latent miscompile: the
old fixed-l representation silently dropped any listed ordinal >= 64.
Codegen (both backends): jumbo branches for literal, in, +/-/*, =/<>,
Include/Exclude, for-in, assignment, params (pmJumboSetValue ABI), and sret
returns. The register 'in' gained a range guard for literal-sized sets.
Native reserves two 32-byte scratch slots per frame (and .bss in main) for
set-op/literal result buffers. QBE adds IsAggregateAddrType so jumbo sets
ride the record/static-array address paths and are never promoted.
OPDF: no format change — recSet SizeInBytes (1 byte) already covers <=32.
Verified: FIXPOINT_OK and NATIVE_FIXPOINT_OK; full suite (3172 tests) green
built by the stage-2 binary, the QBE fixpoint binary, and the native
fixpoint binary; bif-coverage OK. Tests: cp.test.jumboset (12 IR) and
cp.test.e2e.jumboset (6 e2e, both backends via AssertRunsOnAll); the
>256-member rejection test in cp.test.sets updated. Docs: grammar.ebnf and
language-rationale set-type sections updated for the 256 cap and literal
sizing.
Closes the design discussion behind #81.
With the default-argument statement bug fixed, the borrowed-local
elision blocked in 91f44ec is sound and enabled: a const-string argument
that is a plain local or by-value/const parameter variable emits no
caller pin at all — the frame's own reference outlives the call.
Exclusions guard the aliasing edges: address-taken locals (explicit @,
passed to var/out params), locals captured by nested procedures (both
via a per-function blocklist rebuilt in EmitVarAllocs), and calls whose
signature has a var/out string param (F(L, L) lets the callee release
L's buffer through the alias). Nine IR contract tests cover every
shape, including the new exclusion guards.
Tail wins on the remaining profile:
- QBEMangle: fast path returns the input unchanged when it contains no
mangled characters (1.3M calls were rebuilding clean names one concat
per character).
- TStringList.FindSorted: compares the probe slot in place through the
CompareStr/CompareText builtins instead of copying it into a local
and dispatching through Compare — several ARC ops per binary-search
step on the hottest lookup path.
- _StringFormatN: the sizing pass uses a pure DecimalWidth count
instead of rendering every %d argument twice.
Compiler self-compile: 0.78 s -> 0.60 s wall, 8.60G -> 6.45G
instructions. Verified through two self-hosted generations, gen-2
native emission, both fixpoints and the full suite (2870 tests).
WriteDecimal/WriteDecimalU heap-allocated a 22-byte digit scratch on
every call, the IntToStr family allocated a second per-call scratch on
top, and _StringFormatN allocated and freed a buffer per %d argument in
both of its passes — four allocator round-trips per formatted integer
on one of the hottest runtime paths (every emitted temp name goes
through IntToStr). All nine sites now use fixed stack arrays; one
shared scratch serves both _StringFormatN passes.
Compiler self-compile: 8.73G -> 8.65G instructions.
_StringReleaseCheck was a separate 4-argument call on every string
release (~350M per compiler self-compile) and showed up as 12.7% of all
instructions on its own. The three sanity checks (double-free, length,
capacity) now run inline in _StringRelease with DiagAbort as the cold
path; the diagnostics remain always-on.
Compiler self-compile: 9.2G -> 8.7G instructions, 0.80 s -> 0.78 s.
Align the QBE backend unit names with the dotted naming convention
already used by the native backend (blaise.codegen.native.*).
uCodeGen.pas → blaise.codegen.pas
uCodeGenQBE.pas → blaise.codegen.qbe.pas
Updated all uses clauses (~70 test files, main compiler units),
documentation references (README + 5 docs/*.adoc files), and
comment references in runtime/stdlib.
Track string and dynamic-array buffer leaks in --debug builds.
Strings are registered in _StringAddRef (on the 0→1 transition)
and removed in _StringRelease; likewise for _DynArrayAddRef/Release.
The leak report prints "string" or "dynarray" as the type tag
instead of a class name.
Sentinel-based type tags (GLTTagString=Pointer(2), GLTTagDynArray=
Pointer(3)) distinguish buffer entries from class entries in the
hash map. The report reads the correct refcount header offset
for each type (12-byte string header, 8-byte dynarray header,
CLASS_HDR for classes).
LT_BUCKETS grows from 1024 to 4096 to accommodate the higher
entry count when strings/dynarrays are tracked. Report wording
changes from "object(s) not released" to "leak(s) not released".
2768 tests pass (2 new e2e tests), FIXPOINT_OK.
Extend _LeakTrackerRegister from 2 to 4 args (UserPtr, ClassName,
UnitName, Line) so the --debug leak report prints where each leaked
object was allocated. Both QBE and native backends now emit the
extra unit-name string literal and AST line number at every
_ClassAlloc site. Leak report output changes from:
- TBox (rc=1)
to:
- TBox (rc=1) at LeakSite:14
Runtime: bucket size grows from 16 to 32 bytes; LTInsert stores
unit-name ptr + line; _LeakTrackerReport prints the " at unit:line"
suffix.
Native backend: plumb FDebugMode from TCodeGenNative through
TNativeBackend; emit _LeakTrackerEnable in $main; emit
_LeakTrackerRegister at both constructor-call paths.
QBE backend: add FCurrentUnitName, set in Generate/GenerateUnit/
AppendUnit/AppendProgram; extend both _LeakTrackerRegister call
sites with EmitStrLit(FCurrentUnitName) + node.Line.
2766 tests pass (2 new e2e tests), FIXPOINT_OK.
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.
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.
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.
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.
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).
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.
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 _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.
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).
EmitWrite had no tyDouble/tySingle case; floats fell through to
_SysWriteInt with a d-typed temp, which QBE rejected as an invalid
argument type.
Add _SysWriteDouble and _SysWriteSingle RTL stubs in rtl.platform.pas
and rtl.platform.posix.pas (using the existing _DoubleToStr/_SingleToStr
functions from blaise_float.pas). EmitWrite now dispatches to these
stubs so WriteLn(someDouble) and WriteLn(someSingle) compile and run
correctly without requiring DoubleToStr at the call site.
Two new e2e tests (TestRun_WriteLn_Double_Direct, TestRun_WriteLn_Single_Direct)
verify direct float output; 2375 tests pass.
Now that Exit(X) is supported (0f63626), replace the historical
'Result := X; Exit;' workaround with the Exit(X) function-result shorthand
across compiler, runtime, and stdlib (305 sites).
Only same-indentation pairs are merged: a 'Result := X;' immediately followed
by an 'Exit;' at the same indentation level. Sites where the trailing 'Exit;'
is less-indented than the assignment are left untouched — there the Exit
belongs to an enclosing for/else/if block, not to the assignment, so merging
would change control flow (e.g. the xor/and numeric-result branch in
uSemantic falling through to the Boolean-operand check). 14 such sites are
deliberately not converted.
Three converted sites sit inside a real try/finally (uSemantic overload/
generic resolution); each try body already contained bare Exit; statements
and its finally body is a plain local-list Free, so control flow is unchanged.
A new e2e test (TestRun_ExitValueThroughFinally) confirms Exit(X) from inside
a try/finally runs the finally body and returns the value.
Self-hosting fixpoint verified clean (stage-2 IR == stage-3 IR).
Each leaked object line now reports its refcount: `- ClassName (rc=N)`.
This distinguishes the two leak modes at a glance:
rc=1 reference created but never released (dangling owner)
rc>1 over-retain — an AddRef without a matching Release (double-AddRef)
Reads the refcount from the class header at UserPtr - CLASS_HDR. Adds a
WriteSignedInt helper since immortal objects report rc=-1.
Three fixes that work together to correct ARC refcount accounting:
1. _ClassFree now delegates to _ClassRelease instead of bypassing ARC
with a raw _BlaiseFreeMem. This lets users call .Free (out of habit)
or rely on ARC — both paths go through the same refcount logic.
2. ExprOwnsRef helper in codegen suppresses the call-site _ClassAddRef
when the RHS expression already owns a +1 reference (function/method
return values). Covers TFuncCallExpr (with ResolvedDecl guard to
exclude type casts), TMethodCallExpr, TFieldAccessExpr.IsMethodCall,
and TIdentExpr.IsNoArgFuncCall / IsImplicitSelfMethod.
3. Allow Pointer() and PtrUInt() casts on tyString arguments, needed by
rtl.platform.posix for Pointer(APath) where APath is a string.
The double-AddRef bug inflated refcounts on every class-returning
function call: the callee's Result := X emitted AddRef (correct — the
Result slot is not released at scope exit), and the caller's assignment
emitted a second AddRef (incorrect — the +1 already transferred).
Runtime leak tracker (blaise_arc.pas):
- Open-addressing hash map (1024 buckets, 16 bytes each) tracks every
live class instance; zero overhead when disabled.
- _LeakTrackerEnable() activates tracking and registers _LeakTrackerReport
via atexit; called from $main when --debug is passed.
- _LeakTrackerRegister(UserPtr, ClassName) inserts into the map; called
from codegen at every constructor site (both TFieldAccessExpr and
TMethodCallExpr paths).
- _ClassRelease unregisters on refcount-zero free via LTDelete.
- Report written to stderr lists count + class name of each survivor.
Compiler (--debug flag):
- New --debug CLI flag sets FDebugMode on TCodeGenQBE; emits
call $_LeakTrackerEnable() in EmitMainHeader after unit init calls,
and call $_LeakTrackerRegister(...) after every _ClassAlloc site.
- --debug-opdf remains independent (OPDF only, no leak tracking).
- SetDebugMode(Bool) added to TCodeGenQBE public API for e2e tests.
Pointer/PtrUInt cast pairs (semantic + codegen):
- Pointer(intOrPtrExpr) validated in AnalyseFuncCallExpr; accepts
integer, pointer, class, metaclass, procedural, and nil sources.
- PtrUInt(intOrPtrExpr) same validation; resolves to tyUInt64.
- Codegen: w-to-l widening uses extuw for Pointer/PChar targets
(zero-extend, not sign-extend) to preserve address semantics.
- Four new unit tests in cp.test.pointers.pas.
E2E test infrastructure:
- CompileAndRunWithRTLDebug added to TE2ETestCase base for in-process
debug-mode compilation without recursive overload ambiguity.
- cp.test.e2e.leakcheck.pas: 5 tests covering no-leak silence,
single leak, multiple leaks, reference cycles, and debug-off.
2258 tests pass; fixpoint verified.
Adds a real 64-bit unsigned integer type with two equivalent names:
UInt64 (Delphi style, matches the existing Int64) and QWord (FPC
style). PtrUInt now aliases UInt64 too — it's the natural pointer-
sized unsigned on 64-bit.
Language semantics:
- Arithmetic on UInt64 uses udiv/urem; add/sub/mul/and/or/xor/shl/shr
are bit-identical to their signed counterparts.
- Comparisons use unsigned QBE ops (cultl, cugtl, ...).
- Int64 <-> UInt64 mixing requires an explicit cast; the two types are
not implicitly convertible in either direction.
- Decimal/hex literals in the (2^63, 2^64-1) range are typed as
UInt64. Smaller literals stay Integer/Int64.
- SizeOf(UInt64) = SizeOf(QWord) = 8.
Runtime:
- New _UInt64ToStr in blaise_str.pas plus a WriteDecimalU helper that
uses UInt64 arithmetic.
- SysWriteUInt64 added to the platform abstraction and implemented in
the POSIX layer. WriteLn(UInt64) routes through it.
- IntToStr(UInt64) routes to _UInt64ToStr; explicit UInt64ToStr is
also exposed as a builtin.
Bootstrap notes:
- Older release binaries cannot compile the runtime any more because
rtl.platform.pas declares SysWriteUInt64. A stage-2 rebuild from a
fresh stage-1 is required after this commit on any worktree with an
older stage-1, per CLAUDE.md.
- The parser stages literal Value through local var-params rather than
writing directly to the new TIntLiteral.IsUInt64 field via class-field
out-params. Working around a stage-1 codegen bug where var-param
calls that target a class field silently fail to write back.
Docs:
- docs/grammar.ebnf: built-in type list expanded with UInt64/QWord,
integer-literal typing rules documented.
- docs/language-rationale.adoc: integer types table updated with the
unsigned variants, Int64<->UInt64 strict conversion rule explained.
Tests:
- 15 new tests in cp.test.uint64.pas covering symbol-table
registration, codegen instruction picking (udiv/urem/cultl/cugtl),
literal-range typing, and e2e round-trips.
- Full suite: 2151 tests pass (up from 2132).
- Fixpoint clean at stage-3/stage-4 (expected: type-system change).
Port all exception frame management, type identity, and runtime check
functions from C to pure Pascal (blaise_exc.pas). Replace libc
setjmp/longjmp with a minimal custom implementation in x86_64 assembly
(blaise_setjmp_x86_64.s, ~40 lines) that saves only the 8 callee-saved
registers (64-byte jmp_buf vs libc's 200-byte one).
This eliminates the last C source file from the runtime build. The only
non-Pascal code in the runtime is now the assembly setjmp stub.
Changes:
- runtime/src/main/asm/blaise_setjmp_x86_64.s: _blaise_setjmp/_blaise_longjmp
- runtime/src/main/pascal/blaise_exc.pas: _PushExcFrame, _PopExcFrame,
_Raise, _Reraise, _CurrentException, _CurrentExceptionMessage,
_IsInstance, _ImplementsInterface, _GetItab, _Raise_InvalidCast,
_CheckNil — all ported from blaise_exc.c
- uCodeGenQBE.pas: emit call $_blaise_setjmp instead of call $setjmp
- cp.test.exceptions.pas: IR assertion updated for new symbol name
- runtime/Makefile: assembly rule, Pascal blaise_exc build, C rules removed
- .gitignore: whitelist runtime/src/main/asm/*.s
- runtime/src/main/c/blaise_exc.c: deleted
2083 tests pass, fixpoint verified (stage-3/stage-4).
Move _ClassAlloc, _ClassRelease, _StringReleaseCheck, and
_AbstractMethodError from C into blaise_arc.pas. Uses
TFieldCleanupProc procedure type to call the compiler-emitted
field cleanup function pointer from the class header.
Only blaise_exc.c (setjmp/longjmp) remains in C — everything
else in the runtime is now pure Pascal.
All 2083 tests pass. Fixpoint verified.
Merge blaise_io.pas, blaise_process.pas, blaise_sys.pas, and
blaise_time.pas into the OO platform facade (TRtlPlatformPosix).
All underscore ABI stubs now delegate through GRtlPlatform, enabling
future macOS/Windows/FreeBSD support via a single new TRtlPlatformXxx
subclass with no ifdefs or scattered platform files.
Key design decisions:
- _SetArgs creates GRtlPlatform eagerly (runs before unit _init)
- Path utilities stay in blaise_str.pas (platform-independent)
- Makefile sed strip keeps TObject typeinfo from program section
- Shared string helpers (StrAlloc, StrFromCStr) deduplicated
Deleted 8 files (4 units + 4 build drivers), replaced by the
expanded rtl.platform.posix.pas and its single build driver.
All 2083 tests pass. Fixpoint verified.
Implement Delphi-style custom attributes: TCustomAttribute base class,
[AttrName] syntax on class declarations, attribute RTTI emission, and
the HasClassAttribute(AClass, AAttrClass) builtin for runtime queries.
Pipeline changes:
- uAST: add Attributes: TStringList to TClassTypeDef
- uParser: parse [Attr] before type names (ParseTypeSection loop now
accepts tkLBracket; ParseTypeDecl collects and copies to class def)
- uSymbolTable: register TCustomAttribute builtin; add ClassAttributeCount/
ClassAttributeAt accessors to TRecordTypeDesc
- uSemantic: resolve attribute names using Delphi suffix convention
([Threaded] matches ThreadedAttribute); semantic error for unknown attrs;
[Weak] remains compiler-intrinsic and is skipped; add HasClassAttribute
builtin handler in AnalyseFuncCallExpr
- uCodeGenQBE: extend typeinfo from 7 to 8 l-slots (slot 7 = $attrs_T or 0);
emit $attrs_T count+typeinfo-ptr tables; emit TCustomAttribute vtable and
field-cleanup stubs unconditionally; emit HasClassAttribute as a call to
$_HasClassAttribute
- blaise_arc: implement _HasClassAttribute — reads slot 7, scans the attrs
table, walks the parent chain so inherited attributes are visible
19 tests in cp.test.attributes.pas covering parser, semantic, codegen, and
four E2E round-trips. Updated cp.test.publishedrtti.pas for 8-slot layout.
Add blaise_thread.pas runtime unit wrapping pthread_create, pthread_join,
pthread_mutex_init/lock/unlock/destroy and sysconf for GetCPUCount.
Implement TThread (Create, Start, Terminate, WaitFor, Execute virtual,
FreeOnTerminate) and TCriticalSection (Enter/Leave) in classes.pas using
ARC-safe ref counting in the thread trampoline.
Fix EmitFieldCleanupFn to walk the parent chain when emitting the Destroy
call — previously a subclass without its own Destroy would generate an
empty cleanup, causing leaked resources from parent destructors.
Link -lpthread in both the compiler driver and E2E test base.
Include 7 E2E tests covering basic execution, WaitFor blocking, Terminate
flag, Finished flag, multiple threads, mutex-protected counter, and
inherited Destroy cleanup.
Replace the C variadic _StringFormat with a pure Pascal _StringFormatN
that takes a format string, a pointer to an array of tag/value pairs,
and a count. The codegen allocates the arg array on the stack via
alloc8, stores (tag:l, value:l) pairs, and calls the non-variadic
function — eliminating the last dependency on C va_list.
This required a multi-stage bootstrap: release binary compiled stage-1
(with new codegen), stage-1 compiled stage-2 (self-hosted with new ABI),
then the C file could be safely removed. Integer args are widened via
extsw when w-typed to satisfy QBE's storel type checking.
Only 2 C files remain in the runtime: blaise_exc.c (setjmp/longjmp)
and blaise_arc_class.c (function-pointer dispatch).
Replace the C-based float support (_DoubleToStr, _SingleToStr,
_StrToDouble, _AbsInt, _AbsInt64) with a pure Pascal implementation
using a simplified Grisu1 algorithm (Loitsch 2010) for float-to-string
conversion and native double arithmetic for string-to-float.
The new implementation has no libc dependency for the conversion logic
itself (memcpy is still used in BlaiseAllocStr as a temporary measure).
This is a step toward full platform independence.
Also fix a codegen bug where Int64-to-Double promotion in mixed
arithmetic/comparison expressions emitted swtof (32-bit truncation)
instead of sltof (64-bit). This affected three sites in EmitBinExpr
and one in EmitAssignment.
Replace the C weak-reference table implementation with an equivalent
Pascal unit (blaise_weak.pas). Same data structures — open-chained
hash table with linked slot lists — using _BlaiseGetMem/_BlaiseFreeMem
and ^Pointer locals for the void** dereference pattern.
Deletes blaise_weak.c; runtime C files now down to 4 (blaise_arc_class,
blaise_exc, blaise_float, blaise_str_fmt).
All 2023 tests pass including E2E weak-ref Valgrind test. Fixpoint OK.
Step 13 (partial): replace four C shims with self-hosted Pascal units that
bind directly to libc syscall surfaces. blaise_exc.c, blaise_arc_class.c,
blaise_weak.c, blaise_str_fmt.c, and blaise_float.c remain in C — they
either need setjmp/longjmp, function-pointer fields in records, or
variadic interfaces that QBE / Blaise cannot express today.
Ports:
- blaise_io.c → blaise_io.pas
File I/O, env vars, working directory, ParamStr, mkstemp, sleep,
process ID. Uses libc bindings (open/read/write/stat/...) declared in
the interface section per the migration rule in CLAUDE.md.
- blaise_process.c → blaise_process.pas
fork/exec/pipe/waitpid wrapped as TProcess; same libc-binding pattern.
- blaise_sys_posix.c → folded into blaise_sys.pas
Tiny shim (just _SysWrite + _SysWriteNewline); now writes directly via
posix_write. No separate C file needed.
- blaise_time.c → blaise_time.pas
clock_gettime + date arithmetic. The local typed-array-const Days
triggered the codegen bug fixed in fa69ae1.
Each unit is built via a thin build_driver.pas program (see existing
blaise_str_build_driver.pas pattern): the Makefile compiles the driver,
strips everything from the program section onward, then assembles + links
the unit-only IR into the runtime archive.
Outcome:
- C_SRCS shrinks from 9 → 5 files; runtime archive size unchanged in
spirit but the dependency surface contracts.
- Standalone units, not yet folded into rtl.platform.posix.pas — that
consolidation can happen later when the platform layer matures and a
second backend (windows / darwin) appears to justify the abstraction.
- All 2023 tests pass; fixpoint OK.
Add bench_libc_malloc.pas with explicit external malloc/free/realloc
bindings so the libc baseline can still be measured after the cutover.
The original bench_blaise_mem.pas now measures blaise_mem too (because
the GetMem builtin emits _BlaiseGetMem), so it is no longer suitable
as a baseline.
Log the post-cutover state: blaise_mem now beats libc on small, mixed,
and retain-free-all workloads. Realloc growth remains 1.6x. The
compile-time win on the real test suite (~10%) exceeds the microbench
gap, suggesting real workloads are dominated by short-lived small
allocations where the freelist pop/push beats glibc tcache.
When _StringRelease decrements the refcount, also validate that the
header looks plausible: refcount >= -1 (else double-free), length in
[0, 1 GiB], and capacity >= length and below 1 GiB. On corruption,
write a clear message to stderr and abort instead of returning to
the wild segfault that would follow.
Added so the upcoming blaise_mem cutover investigation has a clear
failure signal when string ownership crosses an allocator boundary.
Benign on healthy programs.
All 1934 tests pass; fixpoint clean.
Adds 14 new punit tests covering allocator edges that the original
suite did not exercise:
- SizeClass_Boundaries — every class boundary +/-1
- Realloc_SameClass — in-place when class is unchanged
- Realloc_Shrink — preserves bytes across class shrink
- Realloc_Grow_Steps — preserves bytes across stepwise grow
- Realloc_SmallToLarge — crosses LARGE_THRESHOLD upward
- Realloc_LargeToSmall — crosses LARGE_THRESHOLD downward
- Realloc_Large_SamePage — same-page-count fast path
- Realloc_Large_GrowPages — mremap path
- Large_Cache_Reuse — LIFO cache returns the freed block
- Large_Cache_Eviction — workload exceeds LARGE_CACHE_MAX
- Retain_Many_Small — pattern preservation across 200 blocks
- Interleaved_Mixed — small+large interleaved
- Realloc_Churn — repeated grow/shrink preserves bytes
- Alignment_All_Classes — 8-byte alignment for every class
Each grow/shrink test fills a known byte pattern and asserts every
byte survives the realloc. All 28 tests pass against the current
blaise_mem after the IsLarge() fix.
IsLarge() was reading the small-header Flags field at offset Ptr-4,
but TLargeHeader laid its AllocSize: Int64 across Ptr-8..Ptr-1, so
the Flags probe overlapped with the high half of AllocSize and was
always zero. Every large block therefore routed through the small
free path and the LIFO cache was never populated, forcing a fresh
mmap on each large allocation.
Restructured TLargeHeader to:
TotalMapped: Int64 (Ptr-16..Ptr-9)
AllocSize: Integer (Ptr-8..Ptr-5)
Flags: Integer (Ptr-4..Ptr-1)
LargeGetMem now writes Flags := FLAG_LARGE, IsLarge() returns the
correct value, and the cache reaches ~100% hit rate on the large
alloc/free workload (32 ms -> 0 ms for 10k x 64KB).
Also adds the two benchmark programs (bench_blaise_mem.pas for the
malloc baseline and bench_blaise_mem_custom.pas for blaise_mem) and
reformats docs/benchmark.txt as a dated log so future runs can be
tracked over time.
Arena-based allocator with per-size-class freelists for small blocks
(up to 2048 bytes) and direct mmap/munmap for large blocks. All
returned pointers are 8-byte aligned. Self-contained: no dependency
on strings, ARC, or stdlib.
14 punit tests covering basic alloc/free, realloc, alignment, size
classes, large allocations, and freelist reuse. Allocator is in the
RTL archive but nothing depends on it yet — GetMem/FreeMem still
route to libc malloc/free.