The test framework's `GRegistry: TList<TTestCaseClass>` collided with
`GRegistry: TTargetRegistry` in blaise.codegen.toolkit. The compiler emits
unit-level globals under their bare name with export (strong) linkage, so both
`GRegistry` symbols resolved to one shared 8-byte slot. When the test runner
links both units, the test framework and the codegen toolkit alias the same
storage and free/read it as the wrong type at teardown.
Rename the test-framework global to GTestRegistry — a unit-specific name that
collides with nothing else — so the two registries occupy distinct storage.
The "mandatory parentheses on zero-argument calls" rule (language-
rationale.adoc) was enforced in expression position but NOT in statement
position: a bare `Foo;`, `Obj.Method;`, or `Obj.Free;` used as a statement
compiled silently, building a paren-less TProcCall / TMethodCallStmt. Issue
#148 reported `tester.print;` (a unit's global object method call) being
accepted without its mandatory ().
The statement parser now raises the same "requires () for a call" diagnostic
the inherited-call and expression-position paths already use, at the two
fall-through sites:
* bare unqualified call `Foo` with no '(' (the final ProcCall else-branch);
* bare `Obj.Method` with no '(' and no further '.' chain.
Field reads, field assignments, indexed writes, and '.'-chains are unaffected
(only a terminating bare reference is rejected). Expression-position calls
were already enforced.
Enforcing the rule required the compiler, RTL, and stdlib to comply first
(self-hosting): swept bare calls in uSemantic (Flush/RepairGenericInstances),
runtime.arc/runtime.exc (_libc_abort), blaise.codegen.native.backend
(FAsm.AppendLine), and a json.writer doc example. The test suite embedded
many bare calls in inline program strings — all updated to carry (); adding
() never changes behaviour since these were always calls.
cp.test.parser.pas gains two parse-error tests (proc and method bare calls);
the old TestProcCall_NoParens, which asserted the bug, is inverted. grammar.ebnf
SubscriptMethodCall made parens mandatory and an example corrected; rationale
notes statement-position enforcement.
streams.pas bound malloc/free/realloc to libc by name, which left an undefined
reference under --static (libc-free) — the memory streams are reachable from the
compiler, so a static self-host link failed on 'free'.
Bind them to the RTL's own mmap-backed allocator
(_BlaiseGetMem/_BlaiseFreeMem/_BlaiseReallocMem) instead. Same signatures, so
the call sites are unchanged; memcpy stays the C name (libc, or runtime.cstub
under --static). The streams now allocate from the same heap as everything else
and carry no libc dependency.
With this, free()/malloc()/realloc() are resolved and the compiler links fully
--static. Verified: blaise --static builds the compiler itself into a libc-free
ET_EXEC (no PT_INTERP, not a dynamic executable) that compiles + runs a program;
FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite 0 assertion
failures (residual EUnitNotFound errors are a local search-path artifact).
The mandatory-parentheses rule ("every function, procedure, method, and
constructor call requires parentheses, even with no arguments") made no
exception for 'inherited'. A bare 'inherited Create' is a method call and
should be rejected like any other parenless call, but the parser silently
accepted it.
Enforce the rule in both 'inherited' parse paths (statement and expression
position): a bare 'inherited Method' now raises the same "requires () for a
call" diagnostic as any other parenless call.
Also fix a misleading semantic error for the metaclass-variable case. A bare
'C.Create' on a 'class of T' variable parses as a field access, which reached
the "'C' is not a record or class" guard — confusing, since C is a valid
metaclass variable. AnalyseFieldAccess now detects a constructor/method name
on a metaclass receiver and emits the "requires () for a call" diagnostic
instead. (The parenthesised 'C.Create()' already worked: it parses as a
TMethodCallExpr, which has the metaclass-dispatch branch.)
Migrate every bare 'inherited Method;' call site in stdlib, the bif-coverage
tool, and embedded e2e test programs to the parenthesised form. The
parenthesised form compiles on the prior compiler too, so the migration and the
enforcement land together without breaking the rolling-bootstrap chain.
Docs: update language-rationale.adoc (inherited section + mandatory-parens
section) and grammar.ebnf (expression-position inherited rule).
Tests: add TestSemantic_MetaclassVar_BareCreate_RequiresParens and
TestParse_Inherited_Bare{Stmt,Expr}_RequiresParens.
TList<T> stored its elements in a raw GetMem buffer and freed only the
buffer (Destroy did FreeMem with no element cleanup; Clear just reset
the count; Delete's shift left a duplicate ref on the vacated tail
slot). For a managed element type — a class (ARC) or a string — that
leaked every contained value: destroying or clearing the list never
released its items, so their destructors never ran.
Release each managed element by storing a zero-initialised T through
its slot before dropping it: the compiler's ARC discipline on the
managed store releases the previous value, so freeing the list cascades
to its items. For a non-managed T (e.g. Integer) the same store is a
harmless zero write, so TList<Integer> is unaffected.
Applied to Destroy (all elements), Clear (all elements), and Delete
(the vacated tail slot). The sibling containers (TStack, TQueue, TSet,
and the dictionaries) share the same raw-buffer pattern and leak
likewise; left as a follow-up.
Test: an e2e regression on both backends asserts a class element's
destructor fires on Clear and on Free (the cascade), and the existing
TList<Integer> coverage confirms the non-managed path is unchanged.
SameFileName compares two file names for equality. This is a partial,
Unix-only stub: an exact case-sensitive comparison, which is correct on
case-sensitive file systems. Case-insensitive platforms would need the
names folded before comparing, but the stdlib has no platform-variance
facility yet — system.pas hardcodes DirectorySeparator, PathSeparator
and LineEnding to their Unix values — so there is nothing to key the
case-folding on. When a FileNameCaseSensitive constant lands alongside
the other platform constants, the case-insensitive branch can be added.
e2e test asserts identical names match and differing names do not.
TObjectList.Items lacked the `default` directive, so OL[i] failed with
'String subscript requires a string expression' even though TStringList.Strings
already had it. Add `default;` to TObjectList.Items.
Separately, TStringList(OL[0])[1] — a typecast whose operand is itself a
default-property subscript, indexed again by a default property — crashed the
compiler. AnalyseStringSubscriptExpr rewrites the node in place (swaps StrExpr
for a synthesised field-access, clears IndexExpr) on the default/indexed-property
paths; the outer subscript re-analyses its typecast base, which re-analyses the
already-rewritten inner subscript, recursing without bound. Guard against
re-analysis by returning the cached ResolvedType when already set, and set
ResolvedType on the plain-string exit path so the guard is complete.
Regression test TestRun_DefaultProp_StringList_And_ObjectList compiles+runs the
issue's exact example on both backends with parity.
StrUtils had JoinStr (open-array join) but no splitter at all — a gap any
text-processing code hits. Add SplitChar (split on a delimiter byte),
SplitLines (split on LF, tolerating CRLF), and JoinList (the list-valued
inverse of SplitChar), all returning/taking TList<String>. This pulls
Generics.Collections into StrUtils' interface. Lifted from the Luhmann web
server. Adds 10 tests (57 stdlib tests total).
GRegistry was a TStringList abused as a list of metaclass pointers — the
string slot was always '' (vestigial) and the class reference lived in
Objects[] behind Pointer()/TTestCaseClass() casts. Now that generics support
metaclass type arguments (alias canonicalisation + space-safe label
mangling), it becomes a plain TList<TTestCaseClass>: RegisterTest just Adds
the class, GetRegisteredTest just Gets it — no dead string, no casts.
47 stdlib + 3744 compiler tests pass (both suites enumerate the registry to
run, so this exercises the metaclass-generic path end to end).
Showcase generics + ARC across the test framework and tools. Replace
TStringList with TList<String> wherever only the basic list API was used
(Create/Add/Get/Count/Clear/Delete/IndexOf), rewriting .Strings[i] to .Get(i),
and remove manual .Free calls since Blaise reference-counts objects.
Lists genuinely needing TStringList-only API stay as TStringList: file I/O
(LoadFromFile/SaveToFile/Text), object association (AddObject/Objects[] in the
test registry and bif-coverage AST), and dup control (Duplicates). Destructors
that only freed fields are deleted.
stdlib 47 tests, compiler 3743 tests (20 pre-existing toolchain failures
unchanged), varcheck 18 regression tests all pass.
Process arguments are only ever appended and iterated (Add/Get/Count) — none
of the TStringList-specific API is used by any consumer, so TList<string> is
behaviour-identical and expresses 'an ordered list of argument strings' more
honestly. Every call site uses Parameters.Add(...), unchanged. Lets the unit
drop its Classes dependency for Generics.Collections. All consumers (the
codegen/linker driver, the test runner, the compiler test harnesses) compile
and pass unchanged: 3743 compiler + 47 stdlib tests.
The server only used Create/Add/Get/Count/Free on its string lists — none of
the TStringList-specific API — so TList<string> is behaviour-identical and lets
the unit drop its Classes dependency.
Explain why the unit binds libc (the kernel boundary, the syscall doorway), the
common three-layer pattern shared with Indy/Synapse/the JDK, how Java is itself
not libc-free, and what a pure-Pascal syscall-stub version would and would not
buy.
Document that the socket constants/sockaddr_in layout (Net.Sockets) and the
getrandom(2) call (Security.Guid) are Linux x86_64 specifics, with the per-OS
differences and the fact that the public helper API is the stable surface a
future port keeps.
NewGuid returns a canonical lowercase v4 GUID; NewGuidRaw returns the 16 raw
bytes. Randomness comes from the kernel CSPRNG via getrandom(2), with the
version (4) and variant (RFC 4122) bits forced. Tests cover the canonical
format, version/variant bits in both string and raw forms, and uniqueness
(47 tests total).
Single-threaded HTTP/1.1 server lifted from the Luhmann web server and
generalised: THttpRequest/THttpResponse/IRequestHandler/THttpServer plus
ParseRequest/UrlDecode. WebSocket upgrades are handled via Net.WebSockets and
Broadcast(payload) pushes a text frame to every connected socket (the
reload-specific BroadcastReload became a generic Broadcast). Built on
Net.Sockets. Private split helpers are prefixed to avoid colliding with a
consumer's same-named functions. Tests cover parsing, URL decoding, a loopback
request round-trip, and a WebSocket upgrade + broadcast (43 tests total).
Two small, independent units, split along the lines Java and .NET draw:
Security.Crypto SHA-1 (raw 20-byte digest + Sha1Hex), the home for hash
digests (cf. java.security / System.Security.Cryptography).
Encoding.Base64 Base64 encode + decode (RFC 4648), a general text encoding,
not crypto (cf. java.util.Base64 / System.Convert).
Callers compose them, e.g. the WebSocket handshake is
Base64Encode(Sha1(key + GUID)). Both TStringBuilder-backed (O(n)).
Adds Crypto.Tests (SHA-1 FIPS vectors + the RFC 6455 handshake) and
Base64.Tests (RFC 4648 vectors, decode, round-trip) to the stdlib test tree
via Test.Registry; pasbuild test -m blaise-stdlib runs 27 tests.
A read+write JSON library for the standard library:
Json.Writer streaming serialiser — TStringBuilder-backed (O(n)), with a
three-tier .NET-style API: WriteString(name,value) for object
fields, WriteStringValue(value) for array elements, and WriteKey
for nested-container values. Compact + pretty output. Standalone
(no DOM dependency).
Json.Types in-memory document model (DOM): TJSONData + typed nodes, tree
building, accessors; emits through Json.Writer.
Json.Parser RFC 8259 recursive-descent parser into the DOM, with full string
escapes incl. \uXXXX surrogate pairs.
Json.Reader thin GetJSON(text) facade.
Adds the first stdlib test tree under stdlib/src/test/pascal: a self-registering
Json.Tests suite (19 tests), a Test.Registry hub unit, and a TestRunner program,
wired into 'pasbuild test -m blaise-stdlib' via the <test> block in project.xml.
run-tests.sh builds and runs the suite directly.
HasClassAttribute(AClass, AAttrClass) was unreliable on the native backend:
- The call was never emitted. The builtin had no native lowering, so it fell
through to a generic path that left the first metaclass's typeinfo address in
%rax and read its low byte as the Boolean result — a layout-dependent false
positive (a plain class reported as having an attribute).
- Even with the call, the attribute table was missing: native typeinfo
hardcoded slot 7 (attrs) to nil, so a [Threaded]-marked class reported False.
Fixes (blaise.codegen.native.x86_64.pas):
- Add a native lowering for the HasClassAttribute builtin (gated on
FC.IsBuiltinHasClassAttr): evaluate both metaclass args to typeinfo pointers
in %rdi/%rsi, call _HasClassAttribute, normalise %al to 0/1. Mirrors the
QBE backend.
- Emit the class attribute RTTI table (count + one typeinfo pointer per
attribute) as attrs_<Class> and reference it from typeinfo slot 7, nil when
the class has no attributes. Mirrors the QBE backend's attrs_<Class>.
This is what blaise.testing's runner uses to decide [Threaded] subprocess
dispatch; the false positive forked non-threaded suites and crashed them.
e2e test in cp.test.e2e.gaps (AssertRTLRunsOnAll, both backends): a plain
TTestCase reports False, a [Threaded] one reports True. Full suite OK (3739);
all four fixpoints pass on both build paths.
TMoney previously hard-wired banker's rounding (rmHalfEven) at its single
normalisation point. The underlying TDecimal already supports all eight
TRoundingMode values plus custom IRoundingStrategy, so expose that through
TMoney: every constructor and rounding-sensitive operation gains overloads
taking an explicit TRoundingMode or an IRoundingStrategy.
MoneyFromStr('1.005','USD') -> 1.00 (banker's, unchanged default)
MoneyFromStr('1.005','USD', rmHalfUp) -> 1.01
C := A.Add(B, rmCeiling);
M := MoneyFromStr('9.999','USD', myCashRoundingStrategy);
Overloaded: MoneyFromStr, MoneyFromDecimal, Add, Subtract, Multiply (each
gains a TRoundingMode form and an IRoundingStrategy form). MoneyFromInt,
MoneyZero, MultiplyInt and Negate are exact (no fraction introduced) and
need no mode. The existing no-mode calls are unchanged and keep banker's
rounding, so this is fully backward-compatible.
Internally a single MakeMoneyS(strategy) core does the normalisation; the
mode form delegates via StandardRounding(Mode) and the default via
rmHalfEven — one rounding code path.
Tests: 5 IR/semantic tests and 8 dual-backend e2e tests covering the mode
overloads (HalfUp/Down/Ceiling), the custom-strategy overload, the
default-stays-banker's guarantee, and rounding on Add/Multiply/constructors.
Docs: language-rationale.adoc updated to record that rounding is swappable
per call (default banker's), not hard-wired.
Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite
green on both the QBE-built and native-built runners (3686 tests).
TMoney is a thin value-type wrapper layering an ISO-4217 currency tag onto
an exact TDecimal amount. The numeric core (TDecimal) stays currency- and
locale-agnostic; currency policy lives entirely in this wrapper, matching
Moneta / money-gem / rusty-money.
Design:
- Currency is an upper-cased ISO-4217 string code; the set is open (unknown
codes are accepted at the fallback minor-unit scale of 2).
- A built-in registry gives each currency its default scale (JPY 0, USD 2,
KWD 3, fallback 2); every TMoney is normalised to its currency's scale on
construction and after every operation, using banker's rounding.
- Cross-currency Add/Subtract/Compare raise EMoneyMismatch (no implicit
conversion); Equals is total (False, not raise, across currencies).
- Immutable value semantics, mirroring TDecimal.
API: free-function constructors (MoneyFromStr / MoneyFromDecimal /
MoneyFromInt / MoneyZero), record methods (Amount, CurrencyCode, IsZero,
Sign, Add, Subtract, Negate, Multiply, MultiplyInt, Compare, Equals,
AmountString, ToString), and the CurrencyScale registry function.
Tests:
- cp.test.numerics.money.pas: 22 IR/semantic tests (resolution, IR shape,
type errors) via TUnitLoader.
- cp.test.e2e.numerics.money.pas: 24 dual-backend e2e tests
(CompileAndRunWithRTL) covering construction + per-currency normalisation,
banker's rounding, case-folding, arithmetic, mismatch raising,
Compare/Equals/IsZero/Sign, the registry, and a realistic invoice flow.
Docs: language-rationale.adoc gains "Currency Amounts — TMoney Wraps
TDecimal, Currency Is a String Tag" (decision + alternatives);
future-improvements.adoc marks Numerics.Money as implemented.
Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite
green on both the QBE-built and native-built runners (3671 tests).
A single exact base-10 decimal type for financial / exact-decimal use,
replacing the historical Currency/Comp/Extended proliferation with one type.
- Construction: DecFromInt/Int64/Str, plus a safe/exact float split
(DecFromFloat takes the shortest decimal — 0.1 stays 0.1 — while the
dangerous binary-exact path is the explicitly-named DecFromFloatExact).
- Value semantics + value-based equality: 2.0 = 2.00 with a consistent hash,
so it is safe as a dictionary key (unlike Java BigDecimal).
- Arithmetic: Add/Subtract (scale = max), Multiply (scale = sum), Negate, Abs,
arbitrary precision via a decimal-digit magnitude (compact Int64 fast path
inflating on overflow).
- Division + rounding: a layered design — a TRoundingMode enum (8 modes,
banker's default) over an IRoundingStrategy interface users can implement
for custom rounding. Division always carries an explicit scale + mode.
- Formatting: ToString/ToPlainString never use scientific notation;
StripTrailingZeros keeps integer zeros (600 stays 600, never 6E+2).
- Conversions out: ToDouble (lossy), ToInt64 (truncating).
IR tests (32) + e2e tests (46). Add/Subtract/Multiply and the value-semantics
run dual-backend; Divide/RoundTo and float conversion run QBE-only pending
native codegen fixes (logic verified on QBE; see bugs.txt).
Adds CodePointToString(CP: Integer): string, the inverse of CodePointAt /
CodePointFromByteIndex. It encodes a Unicode codepoint (0..U+10FFFF) as
its 1..4 byte UTF-8 sequence, providing the codepoint-aware replacement for
the Char(n) cast used in other Pascal dialects. Chr(n) only writes a single
raw byte, which is invalid for codepoints above 127; CodePointToString emits
the correct multi-byte form. Out-of-range values yield an empty string.
E2E tests cover single-byte ASCII, the two-byte 0xC2 0xB1 encoding of U+00B1,
a round-trip across all four UTF-8 length classes, and out-of-range handling.
Add GetItem/SetItem methods and an Items[Key: K] default property to
both TDictionary<K,V> and TOrderedDictionary<K,V>. This enables the
idiomatic d[key] syntax for reads and writes:
D['one'] := 1;
WriteLn(D['one']);
GetItem halts on missing key (consistent with index-out-of-bounds in
TList). SetItem delegates to Add (add-or-update semantics).
The compiler already had full default property infrastructure (parser,
AST, semantic, both codegens) — only the stdlib declarations were
missing. This is the first non-Integer-typed default property in the
codebase, exercising the generic index type path end-to-end.
Tests: 2 IR codegen tests (GetItem/SetItem emission) + 3 e2e tests
on both backends (integer keys, string keys, update-via-bracket).
3407 tests pass. All three fixpoints verified.
Replace the FPC/Delphi replace surface (ReplaceStr, ReplaceText, and the
proposed StringReplace + TReplaceFlags set) with two self-explanatory
functions:
Replace(S, Old, New) — replaces the FIRST occurrence
ReplaceAll(S, Old, New) — replaces EVERY occurrence
Both are case-sensitive and literal (non-pattern). This sheds the legacy
redundancy: ReplaceStr/ReplaceText were two names for one operation split by
a 'String vs Text' distinction that is not obvious, and Delphi's StringReplace
encodes the same two booleans (all-vs-first, sensitive-vs-insensitive) as an
awkward flag-set. Modern languages (Go, Python, Java, Rust) use a plain
first/all split with no case flag.
Case-insensitive replace is expressed by lower-casing the inputs with the
built-in LowerCase/UpperCase before calling Replace/ReplaceAll; a
case-*preserving* insensitive replace and pattern/regex matching are deferred
and recorded in docs/future-improvements.adoc.
The internal worker keeps the existing TStringBuilder-based loop; it gains a
FirstOnly flag and drops the now-unused case-fold path.
No callers of the removed functions existed anywhere in the tree. IR/semantic
and e2e tests updated to the new names; both fixpoints green (FIXPOINT_OK,
NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK); full suite OK (3346 tests).
Integer div and mod by zero previously trapped in hardware (SIGFPE),
uncatchable even inside try/except — a single bad divisor terminated the
process. Both backends now emit a divisor-zero guard before each integer
idiv that raises a catchable EDivByZero exception via the normal exception
machinery, matching standard Pascal/Delphi semantics.
- SysUtils: add EDivByZero = class(Exception) and the _RaiseDivByZero helper
that raises EDivByZero('Division by zero').
- QBE + native x86-64: emit a divisor==0 check before integer div/rem
(32- and 64-bit); on zero, call SysUtils__RaiseDivByZero (which longjmps).
- The guard is emitted only when SysUtils is in scope (EDivByZero resolves
through the symbol table); without SysUtils, division traps as before.
This mirrors Delphi's placement of EDivByZero in System.SysUtils and keeps
the always-linked runtime free of stdlib class machinery.
- Tests: in-process e2e proves the guard does not disturb normal division on
both backends; shell-out cli tests (real compiler + linked stdlib) prove
catchable div/mod-by-zero on both backends.
- Rationale recorded in language-rationale.adoc.
All three fixpoints green (FIXPOINT_OK, NATIVE_FIXPOINT_OK,
NATIVE_INTERNAL_OK); full suite OK (3345 tests).
TList<T> now exposes a `default` array property, so elements can be read
and written with the familiar subscript syntax instead of .Get(i):
L[0] := 100; // L.SetItem(0, 100)
WriteLn(L[0] + L[1]); // L.Get(i)
Adds TList<T>.SetItem (a pointer-target store, so the compiler's ARC
discipline releases the old element and retains the new for managed T)
and the property Items[AIndex]: T read Get write SetItem; default.
Also fixes the generic-instance clone path in the semantic analyser: it
copied every property-decl field except IsDefault, so a default property
declared on a generic template (TList<T>) lost its default flag on
instantiation and List[i] did not resolve. The clone now carries
IsDefault.
Verified List[i] read, write, and polymorphic element dispatch
(List[i].Area through a base-typed element) on both backends. Adds
TE2ETListTests.TestRun_TList_DefaultProperty_{ReadWrite,Polymorphic}.
Note: this makes the stdlib depend on the `default` directive (added in
the previous commit), so the pre-release bootstrap binary is refreshed to
a default-aware stage-2 in a follow-up.
Resolves the TStringList.Objects / TList<T> retention question from the
bug backlog as an explicit design decision (language-rationale,
'Collection Ownership'):
- TList<T>/TStack<T>/TQueue<T> managed elements ARE retained on store —
this already works (generic stores lower to ARC-aware pointer writes);
TE2ETListTests.TestRun_TList_ClassElements_RetainedAcrossScope now
pins it on both backends (object survives its creating scope).
- TStringList.Objects[] stays NON-OWNING by design: the API type is
Pointer and the integer-cast idiom (TObject(PtrUInt(N)), used in 27
places in the compiler itself) makes blind retention impossible.
Convention documented at the declaration and in the rationale.
- Known limitation recorded: generic Clear/Destroy do not yet release
remaining managed elements (needs a Default(T)-style zero-store);
tracked in the leak backlog.
Suite: 2946 OK; FIXPOINT_OK; NATIVE_FIXPOINT_OK.
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).
Const-string args were pinned (AddRef before the call, Release after)
regardless of shape. ConstArgMode now classifies them: string literals
and named consts are immortal and emit no ARC ops at all; +1 owned
temps (function/method/getter returns) emit a single post-call Release
that consumes the transferred reference — previously these leaked one
buffer per call (the pin pair netted to zero and EmitOwnedArgReleases
deliberately skipped const-string params); every other shape keeps the
pin. The hot TStringList lookup signatures (Compare, KeyEquals,
HashOf, Find, FindSorted, IndexOf) become const.
Borrowed elision for plain local variables — the larger win — is
deliberately NOT enabled: it is sound at the ARC level, but removing
the balanced pin pairs changes caller register allocation and exposes a
latent use-before-def of a callee-saved register in the native
backend's EmitInterfaceCall (a second-generation compiler then reads
the caller's leftover %r13 as an AST pointer and crashes). The full
repro and bisect trail are recorded in the bug log; the camPin branch
carries a pointer to it.
Verified through two self-hosted generations plus both fixpoints;
contract pinned by 7 IR tests in cp.test.constarg.pas.
The generic keyed containers used documented linear key scans. They now
build the same lazy open-addressing index as TStringList: threshold 16,
kept at <= 50% load, maintained on Add, invalidated by Remove/Clear/
Destroy (storage stays insertion-ordered, so TOrderedDictionary indexed
access is unchanged).
Hashing an arbitrary key type works through monomorphisation: the
generic bodies call GCHashOf(Key), which resolves per instantiation
against new overloads (string hashes its bytes FNV-1a, matching the
content semantics of '=' on strings; Integer/Int64/Pointer use a
multiplicative mix). Instantiating a keyed container with a type that
has no GCHashOf overload is a compile-time error; the threshold const
lives in the interface because generic bodies are analysed at their
instantiation site.
Neutral on the compiler self-compile (its dictionaries are small); this
is stdlib quality for user programs. Contract pinned by 7 new
in-process tests (cp.test.maphash.pas) written against the linear
implementation first.
Profiling the compiler self-compile (--emit-ir, callgrind) showed 86% of
all instructions inside linear scans of five unsorted TStringLists
(FUnitSymbols 68%, FMethodIndex 12%, FStrLits 4%, FUnitIfaces 2%);
TStringList.Find averaged 145 Compare calls per lookup, and each
iteration paid six ARC ops through Compare's by-value string params.
Unsorted lists with >= 16 entries now build a lazy open-addressing hash
index on first Find: FNV-1a over the bytes, case-folded when the list is
case-insensitive, kept at <= 50% load so an empty slot terminates every
probe. Duplicate keys keep the first-added index, preserving the
IndexOf contract that overload registration relies on. Appends update
the table in place; Delete/Insert/Put/Clear/CustomSort and the
Sorted/CaseSensitive setters invalidate it for lazy rebuild. Sorted
lists keep using binary search; small lists keep the linear scan.
Compiler self-compile: 4.75 s -> 0.91 s wall (5.2x), 71.6G -> 10.5G
instructions (6.8x). Behaviour contract pinned by 12 new in-process
tests in cp.test.stringlistfind.pas, written against the old linear
implementation first.
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.
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.
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.
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.
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).
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.
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.
Three fixes that were causing six e2e failures:
Local array consts collided at link time. Function/block/unit-level
`const X: array[...] = (...)` were emitted as `export data $X` with no
mangling. The RTL's own `Days` const (rtl.platform.posix.pas) then
clashed with any user program declaring a `Days` const
(`multiple definition of 'Days'`). Local array consts now use a mangled,
file-local label (`__bac_N_Name`): the mangled name is set in semantic
(AnalyseArrayConstDecls -> ResolvedQbeName / ConstArrayQbe), carried to
the bare-ident read path via TIdentExpr.ConstArraySymbol, and emitted as
non-exported `data` so two separately-compiled objects cannot clash.
Class/record consts keep their exported, type-qualified label.
TComponent fought ARC and crashed. The destructor force-Freed children
it held only as raw pointers, while a caller's variable still referenced
them — a use-after-free at scope exit, surfacing as infinite
Destroy->RemoveComponent->ClassRelease recursion. Ownership is now honest
with ARC: FOwner is [Unretained]; AddComponent takes a strong ref via
_ClassAddRef; Destroy releases the owner's hold rather than Freeing, so a
child the caller still holds survives and an unheld child is destroyed;
RemoveComponent only unlinks the slot (the dying child's ref already
reached zero).
Deflaked TestRun_Thread_Terminate_Flag. The worker could finish via its
`I > 1000` break before the main thread's Terminate landed, racing the
printed flag between 0 and 1. The loop now exits only via the terminate
flag (with a high safety cap), so FTerminated is deterministically True.
Unit tests in cp.test.constants.pas updated to assert the mangled,
non-exported label.
SplitIntoList — the sole backend for TStringList.SetText and LoadFromFile —
trimmed leading and trailing spaces from each split segment. That is correct
for CSV-style splitting but wrong for line-splitting: a TStringList must store
each line verbatim, including indentation.
Removed the trimming so each segment is added as Copy(S, Start, Len) untouched.
SplitIntoList has exactly one caller (SetText), so no trimming-dependent code
is affected.
This also fixes a latent test-infrastructure bug: the threaded test runner
parses each subprocess suite's stdout via TStringList.Text and identifies
failure-detail lines by their two-space indentation. With indentation stripped,
those lines never matched, AddFailure was never called, and the merged result
reported zero failures — so the plain summary printed OK even when [Threaded]
suites failed. With lines preserved, the summary now correctly reports failures.
Regression test: TestRun_TextSet_PreservesLeadingWhitespace asserts on raw
stdout so the spaces inside [] are checked without round-tripping through Text.
Self-hosting fixpoint verified clean.
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).