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.
The Pascal RTL recipes pipe $(BLAISE) --emit-ir through sed to strip
the program section before writing the .ssa file. Without pipefail,
sed's exit code masks a failing compiler invocation: the recipe
succeeds with an empty .ssa, QBE emits an empty .s, gcc archives an
empty .o, and the first symptom is a downstream link failure with
"no symbols".
Set SHELL=/bin/bash and .SHELLFLAGS=-o pipefail -c at the top of the
Makefile so every recipe inherits pipefail. Verified by injecting a
parse error into a build driver: make now exits with Error 1 and
prints the compiler's diagnostic, instead of silently producing an
empty archive member.
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.
Move seven pure-string path functions (_ChangeFileExt, _ExtractFileName,
_ExtractFilePath, _ExtractFileDir, _ExtractFileExt,
_IncludeTrailingPathDelimiter, _ExcludeTrailingPathDelimiter) from
blaise_io.c to blaise_str.pas, reducing C code by ~130 lines while
keeping all OS-level functions (rmdir, rename, chdir) in C.
Separate always-linked runtime code (system.pas, ARC, strings, platform
abstraction) from opt-in standard library units (sysutils, classes, math,
dateutils, generics, etc.). This mirrors the Go runtime/ vs stdlib, Rust
core vs std, and Swift SwiftRuntime vs SwiftCore patterns.
- runtime/ contains code linked into every binary + C shims
- stdlib/ contains units imported explicitly via uses clause
- Single platform abstraction layer (rtl.platform.*) in runtime/,
shared by both runtime/ and stdlib/ units
- bcl.testing renamed to blaise.testing across all 95+ test files
- Updated PasBuild project.xml, fixpoint.sh, README, unit search paths
- All 1931 tests pass, fixpoint OK (stage-3/stage-4)