DateAddDays(MakeDate(...), 5) segfaulted on native while the non-nested form
worked. The args arrived correct — the fault was a stack-alignment bug. The
argument hoist for a record-returning-call argument allocated a 16-byte-aligned
sret buffer (RecArgBufBytes) and then pushed an 8-byte pointer to it, a 24-byte
contribution that left %rsp 8 bytes off 16-alignment. The misalignment was
harmless until the callee chain reached an aligned SSE access — here libm's
mktime (via DateUtils _TimeJoin) executing `movdqa`, which faults on a
misaligned address.
EmitArgHoist now reserves a full 16-byte slot for the saved pointer (subq $8 +
pushq) so the hoist region stays a multiple of 16. The pad sits above the saved
pointer, so reload offsets are unchanged. This is the shared hoist, so it fixes
the alignment for every record-call-argument site, not just DateAddDays.
The DateAddDays e2e test is re-enabled on both backends; a self-contained
regression (record-call result as a record-by-value arg feeding an SSE op) is
added to the gaps suite. All three fixpoints green; 3482 tests pass.
A class-level const array (const Days: array[0..6] of string = (...)) read via
an instance, T.Days[I], returned an empty string on native. The semantic pass
folds the subscript into the field-access node — IsConstant=True with
ConstArraySymbol (the global data label), ConstArrayType (the static-array type)
and PropIndexExpr (the index) — but native's EmitExprToEax IsConstant branch only
handled scalar string/int consts and fell through to an empty string literal.
Added the class-const-array element path: compute
base + (idx - LowBound) * ElemSize and load the element by type (float / string
pointer / narrowed integer), plus a bare-reference case returning the label
address. Mirrors the QBE EmitExpr ConstArraySymbol path. The range- and
enum-indexed class-const-array e2e tests are converted to AssertRunsOnAll.
(Static-array-of-interface element store/read/dispatch remains a separate, larger
follow-up — tracked in bugs.txt; that test stays QBE-only meanwhile.)
All three fixpoints green; 3481 tests pass.
Native lowered Round() with cvtsd2si/cvtss2si, which honour the FPU rounding
mode (round-half-to-even / banker's): Round(2.5)=2, Round(-2.5)=-2. QBE — and
Delphi/FPC — round half away from zero via C99 round(): Round(2.5)=3,
Round(-2.5)=-3. Native now calls round()/roundf() first, then truncates the
integral result, matching QBE exactly. The two Round e2e tests are converted to
AssertRunsOnAll.
All three fixpoints green; 3481 tests pass.
The native backend raised "TMethodCallExpr has no ResolvedMethod" for the two
TObject built-in methods that the semantic pass flags rather than resolving to a
declared method:
* InheritsFrom(C): load the receiver's typeinfo (vtable[0] for a class instance),
evaluate the class-of argument to a typeinfo pointer, and call
_InheritsFrom(self_ti, arg_ti) → Boolean.
* ToString: virtual dispatch through vtable slot 2 (offset 16: typeinfo, Destroy,
ToString), returning the string in %rax — the default returns the class name.
A small EmitMethodReceiverToRax helper loads the instance pointer (expression,
named local/global, or implicit Self). The 7 InheritsFrom tests and the default
ToString test in cp.test.e2e.classes2 are converted to AssertRunsOnAll, pinning
both builtins on QBE and native.
All three fixpoints green; 3481 tests pass.
StrToDouble was lowered only in the integer EmitExprToEax path: it called
_StrToDouble (which returns the Double in %xmm0 per the System V ABI) and left
%rax untouched, so a float assignment (D := StrToDouble(S)) read %rax — a
leftover pointer — and cvtsi2sd'd it into garbage. Native printed values like
95693543141392 instead of the parsed number. Added StrToDouble to the float
emitter (EmitFloatBuiltin) so the result is consumed from %xmm0.
The 7 StrToDouble e2e tests are converted to AssertRunsOnAll, pinning the fix on
both backends. The blanket dual-backend flip of the inline CompileAndRun helper
is documented as deliberately reverted: it breaks on non-deterministic programs
(GetProcessID) and several genuine native gaps remain open (InheritsFrom/ToString
builtins, Round .5-boundary, class const-array, interface-field-assignment RHS,
EmitSretCall record-value arg) — all logged in bugs.txt for individual fixes.
Suites migrate to AssertRunsOnAll as those gaps close.
All three fixpoints green; 3481 tests pass.
Show(MakeFoo(42)), where MakeFoo returns an interface, previously raised
"unsupported interface argument expression" on the native backend. EmitMethodArgPush
now handles a TFuncCallExpr/TMethodCallExpr argument of interface type: it lowers
the call via EmitIntfSretCall/EmitIntfSretMethodCall (which leaves the fat pointer
at (%rsp)), loads obj+itab, drops the sret buffer, and pushes the pair in arg-slot
order. Both backends now print 42. Adds a dual-backend e2e test.
Known follow-up (tracked): the owned (+1) reference of the call result is not yet
released after the call, so --debug reports a single leaked instance for this
specific borrowed-interface-call-result shape. The feature is output-correct; the
ARC release will route through EmitArgHoist (akIntfConsume) like const-string
consume args. All three fixpoints green; 3481 tests pass.
Native backend gaps surfaced by making the RTL/stdlib e2e suites dual-backend:
* 3-arg Supports(Obj, IFoo, OutVar): the native EmitExprToEax only handled the
2-arg boolean form, so the out-var was never populated or registered — a local
out-var read garbage and a global one produced undefined _obj/_itab symbols at
link time. Implemented the 3-arg form (GetItab + ARC retain/release + global
registration), mirroring the QBE EmitSupportsExpr path.
* e2e harness: CompileAndRunWithRTL now compiles and runs on BOTH backends and
asserts parity, instead of QBE only. This brings sysutils, collections,
streams, strutils, tstringlist, tcomponent, threading, arc and the rest of the
RTL/stdlib suites onto the native backend for the first time. New helpers
AssertRTLRunsOnAll/On/One and CompileAndRunWithRTLOn back this; the QBE-only
CompileAndRunWithRTLDebug now delegates to the dual-backend *On implementation.
One pre-existing native bug remains (EmitSretCall mishandles a record-by-value
argument that is itself a record-returning call result, e.g.
DateAddDays(MakeDate(...), 5) — the v0.11.0 release crashes too). That single
case is pinned to QBE via CompileAndRunWithRTLQBEOnly with a tracked TODO; every
other RTL/stdlib e2e now runs on both backends.
All three fixpoints green; 3480 tests pass.
Two related native x86-64 bugs prevented an interface-returning (sret) method
from being called with more register-eligible arguments than the ABI passes in
registers:
1. Caller side: the three sret call emitters (EmitIntfSretCall,
EmitIntfSretMethodCall, EmitClassIntfSretMethodCall) popped every pushed arg
slot into a System V integer register and raised once the index exceeded 5,
instead of spilling the overflow to the stack. A shared EmitSretRegArgs helper
now loads the register-mapped slots and relocates the overflow slots to the
stack in ABI order, returning the byte count the caller reclaims after the
call.
2. Callee side: BuildFrame classified FSretFunc only when allocating the Result
slot, which runs AFTER the parameter-register accounting loop. The loop
therefore read a stale FSretFunc (left over from the previous function) and,
for an sret-returning class/interface method (hidden buffer in %rdi AND Self
in %rsi — two registers), mis-placed the first stack-passed parameter as the
last register parameter. The sret classification is hoisted above the param
loop, and the register count now adds the sret buffer and Self independently.
Both backends now agree (IFoo.Make(1..6) returns 21). Adds three dual-backend
e2e tests. All three fixpoints green; 3480 tests pass.
When two units each privately declared the same C function via
`external name '...'` in their implementation sections, the second unit failed
with "Undeclared function" at the call site.
Root cause: FProcIndex is global across all compiled units, but impl-to-forward
matching used a bare FProcIndex.IndexOf. The second unit's external matched the
FIRST unit's already-indexed decl, so it was treated as that unit's
implementation and was never defined into its own scope.
Fixes:
- Restrict every impl-to-forward match (and the "interface function has no
implementation" checks) to the current unit via a new IndexOfProcInUnit
helper that filters on OwningUnit. The overload-signature loops gain the same
OwningUnit guard.
- When an impl pairs with its forward decl, carry the forward's OwningUnit onto
the impl so the later "has no implementation" scan still finds it.
- Collapse the false "ambiguous overload" that the two same-named externals
produced in the global overload group: SameExternalDecl treats two external
decls with the same link name and arity as one function.
This also clears the obstacle that forced the float-const fix to use only
_DoubleToStr; uSemantic and blaise.assembler.x86_64 may now both declare
_StrToDouble safely.
Regression test: TestDuplicateExternalAcrossUnits_Compiles in
cp.test.e2e.sepcompile (two units declare `external name 'strlen'`, one
invocation, compile + run + assert stdout).
Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite
3477 tests, 0 failures with the QBE fixpoint binary.
RawDoubleToStr formatted folded float constants via libc snprintf, declared
in Pascal with a fixed Double parameter. snprintf is genuinely variadic; the
SysV x86-64 ABI requires %al to hold the XMM-register count for variadic calls,
and neither backend emits that setup (QBE only for its '...' syntax; the native
EmitCall never sets %al). With %al garbage, glibc could read the double from
the wrong place and emit a wrong string -- an environment-dependent
miscompilation of folded float constants that passed locally but failed on the
CI runner (TestConstExpr_FloatParens_Folds expected d_9; TestConstExpr_
NegativeFloat_Folds expected d_-3).
Replace the variadic snprintf call with the RTL's pure-Pascal _DoubleToStr
(FormatFloat(V,15) == %.15g), which is ABI-safe. strtod is not variadic and is
kept as-is.
Add two e2e regression tests (TestRun_FloatConstFold_Parens /
_NegativeMul) that compile AND run folded-float programs and assert stdout.
The IR-only tests could not catch a host-ABI-dependent fold.
Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite
3476 tests, 0 failures with the QBE fixpoint binary.
Self-hosting fixpoint verified on 371,292 lines of QBE IR (QBE, native,
and internal-assembler fixpoints all green). This cycle landed 17 language
features and 30 bug fixes: floating-point constant folding, named-constant
and enum-indexed static-array bounds, generic methods, default array
properties, set-of-ordinal types, case-label ranges, and broad codegen and
ARC hardening across both backends. 3474 tests passing.
When a variable or typed constant declares a Double/Single type but the
initialiser expression is pure-integer (e.g. `var Y: Double = 2 * 3`),
the semantic pass now promotes the folded integer value to a float string
so the QBE backend emits a valid `d_<value>` literal instead of an empty
`d_`.
Static-array declarations in var and type sections now accept an enum type
as the index: array[TColor] of Integer. The array is sized 0..N-1 where
N is the enum member count. Subscript access uses enum ordinal values —
no codegen changes were needed.
Parser: detects array[Ident] (ident followed by ']' with no '..') and
encodes it as 'array[@TEnum] of T' in the type-name string.
Semantic: a new path in FindTypeOrInstantiate resolves the '@' marker,
looks up the enum type, reads Members.Count, and creates a standard
TStaticArrayTypeDesc with bounds 0..Count-1.
This completes the enum-indexed array story — const arrays already
supported this form; var/type declarations now match.
Tests: 4 unit tests + 2 E2E tests (both backends).
Grammar and language rationale updated.
Static-array bounds now accept named integer constants and compile-time
integer expressions, not only integer literals. All of these are valid:
const N = 10;
type TBuf = array[0..N-1] of Byte;
var A: array[0..N] of Integer;
const Days: array[0..N-1] of string = (...);
Parser: ReadConstBoundText collects tokens forming a bound expression
(integers, identifiers, arithmetic operators, parentheses) into a string
embedded in the type name.
Semantic: ResolveArrayBound resolves the bound text — plain integers via
StrToInt, named constants via symbol-table lookup, expressions via
mini-parse + EvalConstIntExpr. The canonical type name always uses
resolved integer values for cache consistency.
Both the var/type declaration path (FindTypeOrInstantiate) and the
const-array path (BuildConstArrayType / ReadConstArrayDim) are updated.
Tests: 5 unit tests + 3 E2E tests (both backends).
Grammar and language rationale updated.
Constant declarations now accept arithmetic expressions involving float
literals, named float constants, and the '/' operator. The semantic pass
detects float-containing expressions and folds them to a single Double
value at compile time, storing the result as a string — the same format
used for bare float literals.
The '/' operator always yields a float result even with integer operands,
matching Delphi/FPC semantics (e.g. const X = 10 / 4 produces 2.5).
Parser: extended ConstRhsStartsIntExpr to detect float-led and
slash-containing expressions; added tkSlash to IsConstExprOp.
Semantic: added IsFloatConstExpr (detects float leaves or '/' usage) and
EvalConstFloatExpr (folds via libc strtod/snprintf to avoid a known
self-hosting ABI issue with the Blaise StrToDouble built-in).
Tests: 8 IR unit tests + 4 E2E tests (both backends).
Grammar and language rationale updated.
Widen set base types from enum-only to also accept Byte (256-bit jumbo)
and Boolean (2-bit small). Set literals accept integer literals and
range syntax [lo..hi], expanded at compile time via EvalConstIntExpr.
Include/Exclude and the in operator accept numeric arguments for ordinal
base types. Both QBE and native backends handle TIntLiteral elements in
set mask computation.
15 new IR unit tests and 4 new E2E tests (both backends via
AssertRunsOnAll). All 3444 tests pass; FIXPOINT_OK,
NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK.
When a named type alias (e.g. TIntHolder = THolder<Integer>) appeared
in the type section before the out-of-line method implementations
(procedure THolder<T>.SetVal), InstantiateGenericRecord cloned the
template methods with Body=nil because LinkGenericClassMethodImpls had
not yet transferred the implementation bodies. Codegen skipped the
bodiless methods, producing undefined-reference link errors.
Add RepairEarlyGenericInstances which runs after LinkGenericClassMethodImpls
in all three analysis paths (AnalyseUnit, AnalyseUnitForExport,
AnalyseBlock). It iterates existing generic record and class instances,
finds methods still missing a body, clones from the now-populated
template, and analyses the repaired methods with correct type-param
scope bindings.
The statement parser's deref-then-field path (p^.field) unconditionally
expected ':=' after consuming the field name, failing when the field
is an array that needs subscripting. Now check for '[' before ':='
to handle p^.da[0] := value, matching the existing r.da[0] := value
path.
Functions returning a static array >8 bytes now use the sret ABI on
both QBE and native x86-64 backends: the caller passes the destination
buffer as a hidden first argument, the callee writes through it, and
returns void. Previously only the first 8 bytes survived (QBE
segfaulted, native rejected the function outright).
Key changes:
- QBE: IsRecordCall, signature, prologue, epilogue, and assignment
dispatch all extended to handle tyStaticArray alongside tyRecord
- Native: allowed return type guard, BuildFrame FSretFunc, sret
call-site dispatch, and subscript-assign Result indirection all
extended for tyStaticArray
A `const A: TArr = (10, 20, 30)` where TArr is a named alias for a
static array type failed to parse as an array constant. The parser
now detects the value-list shape via lookahead when CD.TypeName is set,
and the semantic pass resolves the named type to its underlying
TStaticArrayTypeDesc to extract bounds and element type.
The tyBoolean branch in EmitExpr intercepted boXor (and boAnd/boOr)
before the generic integer path could handle them. The else clause
in the comparison-operator switch defaulted to ceqw (compare-equal),
which is the exact opposite of xor: True xor True produced True
instead of False.
Added boAnd/boOr/boXor cases to the tyBoolean integer-comparison
switch so they emit the correct bitwise operations. The native
backend was already correct (no separate tyBoolean interception).
Fixes GitHub #123.
3411 tests pass. All three fixpoints verified.
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.
Three native backend bugs found and fixed by the IR-emit audit:
1. `sar` operator raised ENativeCodeGenError — added boSar -> sarq.
2. UInt64 comparison used signed setl/setg/setle/setge — max UInt64
compared as negative. Fixed: unsigned types and pointer-like types
now emit setb/seta/setbe/setae.
3. Mixed Single/Double float comparison in EmitCondBranch and
EmitExprToEax determined IsS (single-precision) by checking only
the left operand. A Single compared to a Double literal loaded
double-width data but compared with ucomiss. Fixed: require BOTH
operands to be Single for IsS, and add EmitXmm0WidthAdjust calls.
New e2e test files (all both-backend via AssertRunsOnAll):
- cp.test.e2e.external (10): FFI name binding, narrow-int return
masking, Single param/return, record-by-value, name alias
- cp.test.e2e.genericintfs (7): generic interfaces, two type params,
IEqualityComparer/IComparer, string type arg, polymorphic dispatch
- cp.test.e2e.gaps (15): packed records, sar/shr, UInt64 operations,
[Unretained] back-references, generic records, published RTTI
MethodAddress, multi-arg WriteLn
3402 tests pass. All three fixpoints verified.
Constructors are now auto-slotted into the vtable by the semantic pass.
When a constructor is called through a metaclass-typed variable
(C.Create(args)), the compiler emits _ClassCreate for allocation
followed by a vtable-indirect call to the most-derived constructor
body. Direct calls (TFoo.Create) remain fully static.
Both backends (QBE and native x86-64) emit correct dispatch for:
- MetaclassVar.Create(args) syntax
- ClassCreate(MetaclassVar, args) builtin
- Zero-arg and multi-arg constructor signatures
This is a layout-changing commit (adds constructor vtable slots).
Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK,
3370 tests pass.
The native x86-64 backend corrupted other arguments when a SCALAR FIELD of a
record-returning call result was read inline as a call argument — e.g.
Check('vals', 1, MakeR().A); // 'vals' lost on native; QBE was fine
Reading MakeR().A went through EmitExprToEax, which materialises the call's
sret buffer (a stack subq) and never frees it (the frame epilogue
reclaims it for a standalone expression). Inside the argument push/pop
sequence that stray allocation shifted %rsp, so an already-pushed argument
(typically the const-string) was reloaded from the wrong slot and lost —
crashing later in _StringAddRef when the callee retained the garbage pointer.
Fix: in the chained-field-read path, when the base is a record-returning call
and the field is scalar, allocate the sret buffer, read the field out of it,
then FREE the buffer so the expression nets zero stack change and the argument
protocol stays balanced. Record/static-array fields are excluded — they
return an address INTO the buffer, whose lifetime must outlive the read.
Regression test (both backends) in cp.test.e2e.recordret; the inline
HostTarget().OS assertion in cp.test.toolchain (previously worked around) now
exercises the fixed path directly.
FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite OK (3363).
--emit-ir prints QBE IR and --emit-asm prints native assembly; PickTopDriver
routes each output mode to the backend that produces it, ignoring --backend.
That silent override is acceptable for the default backend, but when the user
EXPLICITLY passes a --backend that cannot produce the requested output it was
silently honoured anyway — e.g. '--backend native --emit-ir' quietly emitted
QBE IR, ignoring the requested native backend.
Track whether --backend was given explicitly (TFrontEndOpts.BackendExplicit)
and, after backend selection, reject the incompatible combinations:
--backend native --emit-ir -> error (use --emit-asm)
--backend qbe --emit-asm -> error (use --emit-ir)
The default-backend cases are unchanged: --emit-ir with no --backend still
resolves to QBE, and --backend qbe --emit-ir / --backend native --emit-asm
remain valid. This matches the driver-owns-its-flags principle of the option
refactor — a backend-specific output mode belongs to its backend.
Tests in cp.test.cli cover both rejections and both still-valid paths.
FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite OK (3362).
The TCLIContractTests and TInternalAsmE2ETests suites resolve their
compiler binary from BLAISE_QBE_COMPILER, falling back to the
/tmp/fp_blaise{3,2} fixpoint binaries. Those only exist after a local
fixpoint.sh run, so in CI both suites silently Ignore'd ~24 tests while
the run still reported green (25 ignored in CI vs 1 locally).
Two changes:
- bootstrap.yml: set BLAISE_QBE_COMPILER to the bootstrapped -pre
binary so both suites actually run in CI.
- CompilerAvailable() in each suite now prints a one-time StdErr note
when it skips, so a missing toolchain is loud instead of a silent
green with hidden ignores.
Generalise external-tool lookup so a cross/host build finds the right
binaries:
- target: HostTarget detects the host OS from ParamStr(0) (a '.exe' suffix
means Windows, incl. under wine) instead of assuming linux; add
ExecutableExtension(target).
- uToolchain: TToolSpec (ordered candidate basenames, a cross-triple
prefix, and a HostTool flag) + ResolveSpec resolve a tool by env
override, then $BLAISE_TOOLCHAIN_PREFIX + name, then the cross prefix on
$PATH, then bare name — each probe appending the host exe extension.
HostTool distinguishes host tools (run anywhere, target via flags) from
target tools (must be the target's own binary; no host fallback when
cross). TrySingleName now uses the real host extension.
- driver: TBackendDriver.DescribeTools(target) declares a backend's tools
(base = the shared linker: cc/clang or the mingw cross-linker for a
Windows target); ToolPath resolves one by name; LinkViaToolchain links
through it. Self.DescribeTools for virtual dispatch.
Native sret codegen fixes (folded in so this commit bootstraps): the
toolchain code is the first in the compiler's own source to build a record
via a function whose body does SetLength on a dynamic-array Result field,
and to pass a record-returning method result directly as a const-record
argument. Both miscompiled on the native x86-64 backend (QBE was correct),
so a native-compiled compiler could not bootstrap at this commit:
- EmitLValueSlotAddr: an l-value field slot on a local record used a raw
leaq, but the sret-function Result slot holds a POINTER — route through
EmitLocalRecordBase so the pointer is loaded before adding the field
offset (fixes SetLength(Result.DynField, N)).
- EmitArgHoist: a record-returning method-call argument was materialised
via EmitExprToEax, which has no sret path for method calls — allocate the
buffer and drive EmitMethodSretCall directly, mirroring the function path.
Tests:
- cp.test.toolchain: ResolveSpec resolution order, host-tool vs target-tool
cross-suppression, fallback naming, and the host/extension helpers.
- cp.test.e2e.recordret: regressions for both native sret fixes (both
backends).
- cp.test.codegen: native backend also emits the _BlaiseInit startup call
(the existing assertion only covered QBE).
A third, pre-existing native sret bug (sret-call result + const-string arg
in the same call) surfaced while writing the toolchain tests; it is
independent of this work and logged in bugs.txt.
Backend-neutral feature; native fixes are x86-64-only.
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.
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).
Relational comparison of strings was unhandled in both backends — only =
and <> were special-cased. The operands fell through to the integer
comparison path:
- QBE emitted a comparison that aborted QBE itself
(amd64/isel.c: selcmp: Assertion `k != Kw' failed).
- Native silently compared the string POINTERS, giving wrong results
(e.g. 'b' > 'a' returned False).
Both backends now route <, >, <=, >= through the existing _StringCompare
RTL helper (strcmp-like: <0 / 0 / >0) and compare its result against 0
with the matching signed condition.
Verified the full truth table ('abc' < 'abd', 'b' > 'a', <=/>= equal
cases, empty-string ordering) on both backends. Adds
TE2EStringOpsTests.TestRun_StringRelational_Order.
An implicit-Self method call in statement position (an unqualified
`Sink(...)` inside a method body) used a register-only fast path that
pushed every argument and popped it into SysVArgRegs64[I+1] — with no
check that the arguments fit the 6 System V integer registers. Self
takes %rdi, leaving 5 registers for arguments, so a call with 6+ value
args (Self + 6 = 7 slots) indexed SysVArgRegs64[6], one past the
[0..5] array. The out-of-bounds read returned an adjacent data-section
value (a garbage string pointer) that was later concatenated, crashing
_StringConcat — which is exactly how the native self-compile segfaulted.
(This was the "latent native self-compile crash" logged in bugs.txt. It
surfaced when a property-accessor helper shifted codegen enough to route
a 6+-slot implicit-Self call through this path; the workaround there
sidestepped it, but the bug was in the call-emission path itself.)
The implicit-Self statement-call path now guards the register fast path
with `CountArgSlots + 1 <= 6` and, when the slots overflow, spills the
surplus to the stack — mirroring the correct handling already present in
EmitMethodCallStmt for qualified method calls.
Also adds SysVArg64(), a bounds-checked accessor for the System V
integer-arg registers; every dynamic SysVArgRegs64[expr] read now goes
through it, so any future over-allocation raises a clear codegen error
("System V integer arg register index N out of range") instead of
silently reading past the array and miscompiling.
Verified: the pre-release segfaults native-compiling the minimal repro
(implicit-Self method, 6 args); the fix compiles and runs it correctly
(21) on both backends, and the full native self-compile succeeds. Adds
TE2ENativeTests.TestRun_Native_ImplicitSelfMethod_{Six,Seven}Args.
Both FNV-1a hash helpers carried over FPC's 1-based string indexing,
which is wrong in Blaise's 0-based world: each loop skipped the first
byte (index 0) and read one byte past the end (index Length).
- uDebugOPDF.FNV1a32 (32-bit; OPDF type IDs)
- uUnitInterfaceIO.ContentHashFnv1a64 (64-bit; .bif source hash)
Both now use 'for B in S' byte iteration, which is bounds-safe and is
also the natural iteration for FNV-1a (defined over the byte stream).
Output validated against a reference FNV-1a32 implementation.
No layout/ABI impact: OPDF type IDs are opaque internal tokens, and the
.bif SourceHash is a staleness cache key. Existing .bif caches will
mismatch once and trigger a one-time recompile, which uUnitLoader
already handles.
actions/checkout v4->v6, actions/cache v4->v5, actions/upload-artifact
v4->v7. Silences the Node.js 20 deprecation warning; cache format is
compatible across these majors so incremental resume still works.
The compiler shells out to 'qbe' and resolves it via the BLAISE_QBE env
var, then PATH (uToolchain.ResolveQBE). The QBE= make variable passed by
rolling-bootstrap.sh only covers the script's own direct qbe calls, not
the compiler's internal invocation when building the RTL. On the runner
qbe is built at vendor/qbe/qbe but not on PATH, so the RTL build failed
with 'qbe error (exit 127)' on the very first replay step.
Set BLAISE_QBE (and prepend vendor/qbe to PATH) after building QBE so all
subsequent steps can find it.
Adds .github/workflows/bootstrap.yml: on push to master (and manual
dispatch), builds vendored QBE, replays the self-hosting chain via
scripts/rolling-bootstrap.sh, builds the RTL + TestRunner with the
resulting -pre binary, and runs the full test suite.
Cold start downloads the v0.10.0 release tarball as stage-1. Subsequent
runs resume the bootstrap from a cached -pre binary (actions/cache keyed
by commit SHA), replaying only commits new since the last successful run.
The -pre binary + blaise_rtl.a are published as workflow artifacts.
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.
Two standard-Pascal ordinal features that were missing, found while
shaking the sets/enums/case cluster:
1. case-label ranges — `2..4:` as a case label:
case i of
0, 1: ...;
2..4: ...; // previously: Parse error "Expected ':' but got '..'"
else ...;
end;
The parser now accepts `lo..hi` per case label (reusing TSetRangeExpr to
represent the range, as set literals already do). Semantic checks both
bounds against the selector type; codegen matches a range with a
(sel >= lo) and (sel <= hi) test on both backends. Singles and ranges may
be mixed in one branch (`5, 7..9:`), and enum ranges work (`Red..Yellow`).
Ranges are rejected for a string selector.
2. Succ(x)/Pred(x) — the next/previous value of an ordinal (enum or integer
family); previously "Undeclared function 'Succ'". The result keeps the
argument's type, so Succ(enumValue) is still that enum. Lowered to +1 / -1
on the ordinal value on both backends.
Verified case ranges (int + enum, mixed with singles, with else) and
Succ/Pred (integer + enum, nested, type-preserved) on both backends. Adds
five dual-backend e2e tests in cp.test.e2e.controlflow.pas.
Completes generic methods: the out-of-line implementation form now parses
and links, alongside the inline-body form added previously.
type TUtil = class
function Pick<T>(cond: Boolean; a, b: T): T; // declaration
end;
function TUtil.Pick<T>(cond: Boolean; a, b: T): T; // out-of-line body
begin if cond then Result := a else Result := b end;
- Parser: a method-level <T> appearing AFTER the qualified name
(Owner.Method<T>) is now parsed into TypeParams. (The <T> before the dot
remains the owner's type params, as for TList<T>.Add.)
- Semantic: LinkClassMethodImpls detects a generic-method impl
(TypeParams <> nil) and transfers its body onto the in-class
generic-method template, instead of trying to resolve its T-typed params.
Also fixes a real hole found via the out-of-line probe: generic-method
call arguments were not type-checked against the parameters (the path
bypasses ResolveMethodOverload), so e.g. passing a string for a Boolean
parameter compiled silently. The call site now validates argument count
and types against the monomorphised signature.
Verified out-of-line impl (string + Integer instantiations) on both
backends, and that a mismatched argument is now rejected. Adds
TE2EGenericsTests.TestRun_GenericMethod_OutOfLineImpl. Grammar and
language-rationale updated to drop the inline-only caveat.
A class or record method may now declare its own type parameters,
independent of the enclosing type, and is monomorphised per call site:
type TUtil = class
function Pick<T>(cond: Boolean; a, b: T): T;
begin if cond then Result := a else Result := b end;
end;
...
u.Pick<Integer>(True, 7, 9); // -> 7
u.Pick<string>(False, 'a', 'b'); // -> 'b'
Each distinct set of explicit type arguments produces one concrete body
named <Owner>_<Method>_<Args> (e.g. TUtil_Pick_Integer); the implicit
Self is preserved, so a generic method can read the receiver's fields and
call its other methods. This is distinct from (and composes with) methods
of a generic CLASS.
Implementation mirrors the existing generic free-function machinery:
- Parser: a method-call folds an explicit <...> type-arg list into the
method name (Pick<Integer>), using the same two-token '<' lookahead as
generic free-function calls.
- Semantic: generic-method templates (TypeParams <> nil) are registered
by Owner.Method and skipped from signature/vtable/body analysis;
InstantiateGenericMethod clones the template, substitutes the type
params, keeps OwnerTypeName + Self, analyses via AnalyseMethodDecl, and
records a TGenericMethodInstance (deduplicated per owner+args). The
call site resolves obj.M<T>(...) to the instance.
- Codegen (both backends): GenericMethodInstances are emitted as ordinary
methods (Self param + mangled ResolvedQbeName).
Verified pick/echo, two distinct instantiations (Integer + string), use
of a Self field, and two type parameters on both backends. Adds IR tests
(TGenericFuncTests.TestCodegen_GenericMethod_{Body,Call}Emitted) and
dual-backend e2e tests (TE2EGenericsTests.TestRun_GenericMethod_*).
Grammar and language-rationale updated.
Limitation (logged in bugs.txt): only the inline-body declaration form is
supported; the out-of-line function TOwner.Method<T>(...) form is not
yet parsed.
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.
Adds the `default` directive on an indexed property, enabling subscript
sugar on the object itself:
property Items[I: Integer]: T read Get write Put; default;
...
V[0] := 10; // lowers to V.Put(0, 10)
WriteLn(V[0]); // lowers to V.Get(0)
This is the mechanism behind the familiar List[i] syntax and was a real
foundational gap (the directive did not even parse — "Expected ':'").
Implementation:
- Parser: accept the trailing `default;` directive after a property
declaration; set TPropertyDecl.IsDefault.
- AST / symbol table: IsDefault on TPropertyDecl and TPropertyInfo;
TRecordTypeDesc.FindDefaultProperty walks the inheritance chain.
- Semantic: Obj[I] read (AnalyseStringSubscriptExpr) synthesises the
default property's field access and reuses the indexed-property read
path; Obj[I] := V write (AnalyseStaticSubscriptAssign) records the
setter on the TStaticSubscriptAssign node.
- Codegen (both backends): the write path emits the setter call via the
existing PropAccessorTarget / EmitPropAccessorCallNative helpers, so it
honours virtual/override on the accessor; the read path delegates the
TStringSubscriptExpr to its folded property-read field access.
- Cross-unit: IsDefault is serialised in the .bif interface so a default
property declared in one unit keeps its subscript sugar elsewhere.
Verified read, write, string-element, inherited, and cross-unit cases on
both backends. Adds IR tests (TPropertyTests.TestCodegen_DefaultProperty_
{Read,Write}) and dual-backend e2e tests (TE2EPropertyTests.TestRun_
DefaultProperty_{ReadWrite,StringElement,Inherited}). Grammar and
language-rationale updated; the two new TStaticSubscriptAssign fields are
marked safe in bif-coverage.status.
A generic class that implements a non-generic interface could not be
assigned to an interface variable:
IVal = interface function Get: Integer; end;
TBox<T> = class(IVal) ... function Get: Integer; ... end;
iv: IVal := TBox<Integer>.Create(77); // expected IVal, got TBox<Integer>
The first heritage entry of class(X) is parsed into ParentName whether X
is a class or an interface. The generic-instance clone path only moved
ParentName to ImplementsNames (so AddImplements would be called) when X
was a GENERIC interface — its name contained '<'. A plain interface name
like IVal failed that test, stayed in ParentName, and the instance's
implements list ended up empty, so class -> interface compatibility
rejected the assignment.
The move now fires for any ParentName that resolves to tyInterface,
generic or not, mirroring the non-generic class path. Verified a generic
class used through its interface, including a method with arguments, on
both backends. Adds TE2EGenericsTests.TestRun_GenericClassImplements
Interface and _MethodArgs.
A class implementing a derived interface could not be narrowed directly
to a base interface:
IDog = interface(IAnimal) ... end;
TDog = class(IDog) ... end;
a: IAnimal := TDog.Create; // link error: undefined itab_TDog_IAnimal
The class only emitted the itab for the interface it directly named
(itab_TDog_IDog), so assigning the instance to a base-interface variable
or parameter referenced an itab that was never generated. (The type
check itself was fixed in 37690de; this is the codegen half.)
EmitInterfaceDefs on both backends now expands each implemented interface
to its full parent chain, deduplicates, and emits an itab plus an
impllist entry for every ancestor. An ancestor's interface methods are a
prefix of the descendant's itab layout, so the same implementation
pointers apply — the base itab is just the leading slots.
Verified class-instance -> base-interface variable and parameter on both
backends. Adds TE2EInterfaceTests.TestRun_ClassImplDerived_To{BaseVar,
BaseParam}.
A value of a derived interface was not accepted where a base interface
was expected:
IAnimal = interface function Name: string; end;
IDog = interface(IAnimal) function Bark: string; end;
d: IDog; a: IAnimal;
a := d; // rejected: "expected IAnimal but got IDog"
Describe(d); // (a: IAnimal) -> "No matching overload"
CheckTypesMatch treated the two interfaces as unrelated. It now walks the
TInterfaceTypeDesc.Parent chain (new helper InterfaceInheritsFrom):
- interface -> base interface: assignable when the expected interface is
on the actual interface's parent chain (IDog is-a IAnimal).
- class -> interface: a class that implements a DESCENDANT of the expected
interface also satisfies it.
ArgMatchScore picks this up automatically through its CheckTypesMatch
fall-through, so parameter passing and overload resolution work too.
Verified derived->base assignment, derived-arg->base-param, and a
three-level interface chain (IC -> IA) on both backends. Adds
TE2EInterfaceTests.TestRun_{DerivedInterface_ToBaseVar,
DerivedInterface_ToBaseParam,ThreeLevelInterfaceChain}.
Note: assigning a class INSTANCE that implements a derived interface
directly to a base-interface variable still needs an itab for the base
that is not yet emitted (logged in bugs.txt); the interface->interface
copy path used by these tests does not need it.