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.
A bare `TFoo = class;` / `IFoo = interface;` (the keyword immediately
followed by `;`, with no ancestor clause and no body) is a forward
declaration: it registers a placeholder type that a later full
declaration of the same name completes within the same declaration
scope. This is the standard Object Pascal idiom for mutually
referential types, e.g. a class declared between the forward and its
full declaration may name the forward-declared type.
Previously the front end registered the bare `class;` as a complete
type and then rejected the full declaration as a duplicate type name,
so the idiom could not be used at all.
The parser flags the stub via TClassTypeDef.IsForward /
TInterfaceTypeDef.IsForward. AnalyseTypeDecls registers the placeholder
in pass 1, lets the matching full declaration complete it (reusing the
placeholder descriptor) instead of erroring, skips the stub in pass 2,
and after resolution drops the now-redundant stub from the AST so later
phases see a single declaration. A forward never completed in the scope
is reported as "Forward type not resolved"; a type forward-declared
twice is "Duplicate forward type declaration".
Tests: IR/semantic coverage for completion, mutual reference,
unresolved, and double-forward errors, plus a single-vtable codegen
check (the stub must not double-emit); an e2e test compiles and runs a
mutually-referential class pair with a managed field.
A reference written `Unit.Symbol` (a procedure/function call, variable, or
constant qualified by a used unit) previously failed to resolve: the parser
built it as a record/field or method-call chain rooted at the unit name, and
the analyser then rejected the unit name as an undeclared variable.
Collapse the qualifier in the parser instead. Each `uses` clause feeds a
name list (plus the implicit System), consulted purely as syntax — no symbol
or type lookup — to recognise a leading run that spells a used unit followed
by a trailing `.Symbol`. On a match the unit prefix is dropped, leaving the
bare symbol, which the existing uses-chain resolution and codegen already
handle. Producing canonical TProcCall / TFuncCallExpr / TIdentExpr nodes
keeps the change entirely in the front end.
The unit name may be dotted to any depth (System.SysUtils.Foo, A.B.C.D.Sym):
an on-demand lookahead buffer scans the whole dotted chain, exposed through
scalar peek accessors (kind/value/line/col) so no transient TToken records —
which carry a managed string — are produced on the matcher's hot path. The
required trailing `.Symbol` is what distinguishes a qualifier from a
same-prefixed record/field chain, so `My.Pkg := 4` (record `My`, field `Pkg`,
with `My.Pkg` also a used unit) is left untouched.
Known limitation: in the rare case where a dotted unit's first component is
also an in-scope variable (`var My` + `uses My.Pkg`, then `My.Pkg.Foo`), FPC
resolves the variable; the parser has no scope and resolves the unit. Single-
identifier units cannot collide this way (a variable sharing a unit's name is
a duplicate-identifier error).
Tests: cp.test.parser asserts the canonical-node collapse at one, two, and
three component depths, plus the two negative cases (non-unit receiver stays a
method call; no-trailing-symbol stays a field write). cp.test.e2e.useschain
compiles and runs qualified System calls and qualified cross-unit calls/vars
through single and dotted unit names.
Field[I] := V inside a method, where Field is a class field of the current
class carrying a writable default array property, reported "Undeclared variable
'Field'": AnalyseStaticSubscriptAssign's implicit-Self branch only handled
array-typed fields, so a class field with a default property fell through.
Extend that branch: when the Self field is a class with a writable default
property, set IsImplicitSelf + ImplicitFieldInfo + PropWriteInfo and lower to
the default property's setter (mirroring the variable-receiver path). Both
codegen backends gain an implicit-Self receiver in the static-subscript
property-write path: load Self, reach the field slot, dereference to the
field's object, then call the setter.
(The implicit-Self read Field[I] already worked.) e2e test writes and reads a
default property through a Self field from inside methods, verified on QBE and
native.
Reading a default array property through a member result already worked
(Recv.Member[idx]); the write form Recv.Member[idx] := V did not. A class field
reported "Field is not an array — cannot assign to a subscript", and a property
reported "Property is read-only", because the write paths only handled array
fields and writable properties directly, never a default property on the
member's result.
Add TryLowerDefaultPropertyWrite: when the assignment target is a class member
(field or property) whose type carries a writable default array property and a
trailing index is present, lower Recv.Member[idx] := V to
(Recv.Member).Default[idx] := V — read the member into an inner object
expression and re-target the assignment at the default property's setter.
Wired into the field path (TryAnalyseFieldElemWrite, so it covers every
receiver form) and the read-only-property path. Class members only; a record
getter result is a by-value temp, so a write through it would be discarded.
The setter receiver is now an arbitrary expression, so both codegen backends
gain an ObjExpr branch in the property-write path (QBE EmitFieldAssignment and
the x86-64 native field-assignment): evaluate the receiver expression to get
the object pointer instead of loading a named var or Self.
e2e tests cover writing through a class field and through a (read-only)
property, verified on QBE and native.
Recv.Prop[idx] where Prop is a (non-indexed) property whose type carries a
default array property previously dropped the index: the parser folds the
trailing [idx] into the field-access PropIndexExpr, and the property-read
branches in AnalyseFieldAccess returned the property's type without consuming
it (Recv.Prop[idx] resolved to the property's class type, e.g. TStringList).
A field with a default property already worked via FindIndexedProperty; a
property read did not.
Add TryLowerDefaultPropertyIndex: when a resolved read property is non-indexed
but has a trailing index and its result type has a default property, rewrite
Recv.Prop[idx] to (Recv.Prop).Default[idx] — moving the property read into an
inner field-access and re-pointing the node at the default property, then
re-analysing (which lands on the existing chained indexed-property path). Wired
into the field-backed and method-backed property branches (chained receiver,
implicit Self, and variable receiver).
Reads only; writing through a property-result default property is a separate
gap (the write path reports the property read-only).
Adds an e2e regression (both backends) that read-modifies Result.A (offset 0)
of a managed sret-returned record mid-body. The native offset-0 field-read
shortcut used to load the Result slot directly — but for an sret function that
slot holds a POINTER to the caller's buffer, so the pointer was read as the
field value (garbage).
The fix already landed (the inline-asm commit routes the offset-0 integer
field-read leaf through EmitLocalRecordBase so the sret-Result deref happens);
this test, from Andrew Haines's parallel fix (236e9525), locks it in. His code
change is omitted as a duplicate of the already-landed fix.
Co-authored-by: Andrew Haines <andrewd207@aol.com>
An override that returns a record by value and sources it from `inherited`
corrupted the heap when the record had a managed (string) field:
function TDeriv.Next: TTok; { TTok = record Kind; Value: string; ... }
begin
Result := inherited Next();
end;
The inherited-call emitters carried their own argument-marshalling, copied
from the normal call path but missing the hidden sret pointer that a
record-returning callee requires. So the call passed Self into the callee's
sret slot — the base then wrote its managed field straight into the Self
object and _StringRelease ran on garbage, segfaulting.
Route inherited record/static-array returns through the same sret machinery
every other record-returning call uses, dispatched statically to the parent:
- QBE: a shared InheritedArgLine helper marshals Self + args once; the
statement, expression, and assignment (IsRecordCall) paths emit through
EmitRecordReturnCallSite with the destination as the hidden first arg.
- native: EmitInheritedRecordSret reuses EmitMethodSretCall via a transient
implicit-Self node, force-static, so the sret pointer / register-return
capture is threaded identically to a normal call.
The assignment form writes straight into the destination slot (no temporary,
no double AddRef); the implicit-Result statement form writes into Result.
Tests: cp.test.recordret asserts the QBE IR threads the sret pointer (Result
slot) + Self with a static parent call; cp.test.e2e.recordret covers the
assignment and statement forms with a managed-field record on both backends.
Inside a method, an unqualified FFn(args) -- no 'Self.' prefix -- where
FFn is a procedural-typed field of the current class failed with
"Undeclared function/procedure 'FFn'". Only the explicit Self.FFn(args)
form resolved.
Both call analysers now fall through to an implicit-Self field lookup
after the method lookup fails: a procedural-typed field of the current
class is dispatched through its stored pointer, validating argument count
and types (and the l-value requirement for var/out arguments) against the
field signature, exactly like the explicit-receiver path.
Codegen:
* QBE: a shared EmitImplicitSelfProcFieldCall loads Self, indexes the
field, and dispatches; var/out arguments are passed by reference and
'of object' fields pass the captured Data as the implicit first arg.
Used by both the statement and expression forms.
* Native x86-64: both forms reuse EmitProcFieldCall with Self as the
receiver.
The two new AST flags (TProcCall.IsProcFieldCall,
TFuncCallExpr.IsProcFieldCall) are recorded in the bif-coverage status
inventory as semantic-only (safe, not serialised), matching the existing
IsIndirectCall flags.
Covered by an IR test (loads Self, indirect dispatch) and end-to-end
tests on both backends for the expression and statement forms.
Assigning a method pointer (procedure/function of object) captured with
@Obj.Method into a class field -- B.FEvt := @S.Handle -- raised
"@Obj.Method must be used in assignment context" on the native x86-64
backend. The simple-variable destination was already handled, but a
field destination fell through to evaluating @Obj.Method as a general
expression, which has no standalone lowering.
The field-assignment path now special-cases a method-pointer field whose
right-hand side is @Obj.Method: it computes the field's containing-object
base per receiver shape (as the interface-field store already does), then
stores the code pointer at offset 0 and the captured object pointer at
offset 8 of the field's 16-byte slot.
This also makes the of-object arm of procedural-field call dispatch
reachable on the native backend (a field must be populated before it can
be called). Covered end-to-end on both backends: capture @Obj.Method
into a field, then dispatch through it.
Calling a function-pointer class field via its receiver (Self.FFn(args))
was broken in three independent ways:
* Semantic analysis gave the call expression a nil result type, so
using it as a value (Result := Self.FFn(S)) failed with "Expression
has no value type in assignment". The three procedural-field call
branches now carry the field signature's return type.
* The QBE backend passed var/out arguments by value instead of by
reference, so a callee writing through an out parameter corrupted
memory and crashed; the expression form also hardcoded the call's
result width to l rather than the signature's return type.
* The native x86-64 backend had no support for procedural-field calls
at all -- both the statement and expression forms raised "has no
ResolvedMethod". EmitProcFieldCall lowers the dispatch by pushing
the arguments first, then loading the (call-free) receiver and the
field's code pointer, mirroring the interface-dispatch shape.
Covered by an IR-level test (indirect dispatch, not a direct call) and
end-to-end tests that run on both backends: value return, statement
call, out parameter, and multiple value arguments.
A routine body may now be written as inline assembly:
function GetSelf: Pointer; assembler; nostackframe;
asm
movq %rdi, %rax
ret
end;
The block is opaque GNU/AT&T assembly: the lexer captures the whole asm … end
as one tkAsmBlock token (verbatim text, never tokenised as Pascal), the parser
wraps it in a TAsmStmt, the semantic pass treats it as a black box, and the
native backend emits it verbatim into the assembly stream where the existing
internal/external assembler parses it. `nostackframe` suppresses the compiler
prologue/epilogue so the asm body owns the whole frame. asm routines mix
freely with ordinary Pascal routines in a standard .pas unit (no .inc needed).
This is the FPC model (rtl/linux/x86_64/si_c.inc) and the path to retiring the
hand-written runtime/src/main/asm/*.s files (assembled by `cc -c` today) — once
each body moves into an asm routine the RTL builds with no external assembler.
Design follows ports-and-adapters: x86-64 knowledge stays at the backend/
assembler edge, the portable core never interprets the block. The QBE backend
rejects asm bodies (it emits no assembly text); native is the inline-asm target
and the default. TAsmStmt round-trips through the .bif unit cache.
Pipeline: lexer (tkAsmBlock + ReadAsmBody raw capture), uAST (TAsmStmt,
TMethodDecl.NoStackFrame), parser (nostackframe directive + asm-body path),
semantic (opaque no-op), native codegen (verbatim emit + nostackframe null-frame
guard), QBE rejection, uUnitInterfaceIO encode/decode, bif-coverage entry.
`asm` becomes a reserved word (one local var named Asm renamed in a test).
Fixes a native sret-Result field-read codegen bug the feature exposed: reading a
field of an sret function's Result at offset 0 (e.g. `Result.Kind` in a record-
returning function) read the Result frame slot DIRECTLY instead of dereferencing
the caller-buffer pointer it holds, so `Result.Field = const` was always false.
The offset-0 fast path in the integer field-read leaf now routes through
EmitLocalRecordBase like the offset>0 path, so the sret indirection happens in
both. QBE was already correct; this was a native-only divergence.
Tests: lexer raw-capture (3), native verbatim-body/no-prologue IR test (1),
internal-assembler e2e returns-value + adds-two-args (2). All four fixpoints
and both QBE- and native-built test runners pass (3764 tests).
Design note: docs/inline-asm-design.adoc; grammar + rationale updated.
The native internal linker linked every program against system CRT startup
objects — Scrt1.o for _start plus crtbeginS.o/crtendS.o/crti.o/crtn.o —
located by scanning a hard-coded list of multiarch and versioned gcc
directories (/usr/lib/gcc/<triple>/<ver>/). That layout varies by distro, so
the scan failed with "internal linker: CRT startup objects not found" on
distributions whose triple/gcc version was not listed (issue #142, Mageia 9).
Asking gcc (cc -print-file-name) would locate them portably but reintroduces a
link-time gcc dependency, defeating the internal linker's purpose
(toolchain independence). Instead, follow FPC's approach: the runtime now
supplies its OWN _start (runtime/src/main/asm/blaise_start_x86_64.s), which
marshals argc/argv and tail-calls __libc_start_main(main, ...) — the modern
glibc convention (init/fini = NULL; glibc runs the init array itself).
Blaise consumes nothing else from the CRT set: main is a plain C-style entry,
unit init runs explicitly from main, and the emitted code references no
crtbegin/crtend symbol. So the internal linker now needs no gcc-provided
object and no versioned gcc directory; FindCrtObjects and its directory lists
are removed.
The new _start ships inside blaise_rtl.a; AddArchive includes all members
unconditionally, so the entry symbol is always present.
Tests: two TInternalAsmE2ETests cases drive the REAL compiler binary through
the internal assembler + internal linker (the only harness that exercises the
runtime _start; the e2e suite links via cc) and assert the two things _start
owns — exit-code propagation (Halt(7)) and argc/argv forwarding (ParamCount/
ParamStr). The native fixpoints already link the compiler itself via this path.
Design note: docs/self-contained-start-design.adoc.
The QBE backend once mis-lowered a value-cast of a >32-bit literal
(cMax = Int64(922337203685477580)), emitting a 'w'-typed operand that QBE
rejected with "invalid type for first operand ... in arg". The width-by-
magnitude literal lowering now emits these as 'l', so the cast is correct on
both backends.
Adds an IR unit test asserting the 'l' (not 'w') lowering and an e2e test
(AssertRunsOnAll, both backends) exercising the const through folding,
arithmetic, and argument positions.
Assigning a record-returning method's result back into the same variable
that is the receiver (M := M.Method(...)) corrupted the computation: the
sret destination buffer aliased Self, so the callee wrote Result (== Self)
while still reading Self, clobbering it mid-flight. Affected both backends.
Both backends now detect the case where the call's receiver is exactly the
destination variable and route the sret call through a fresh zeroed temporary,
moving the constructed result into the destination (release old managed fields,
raw ownership-transfer memcpy) only after the call returns.
Adds an IR unit test (RoutesThroughTemp) and two e2e tests (managed-string
field and scalar fields) covering the self-assign and a non-aliasing
regression guard.
Warm --unit-cache rebuild: a unit recompiled from source could not resolve a
method inherited from a class in a DIFFERENT, still-cached ancestor unit when
that ancestor unit had a DOTTED name (e.g. blaise.testing):
note: ...money.o iface stale vs source on path; recompiling from source
Semantic error: Undeclared procedure 'Ignore' in cp.test.e2e.numerics.money
Root cause: DecodeQualRef in uUnitInterfaceIO split a serialised 'Unit.Type'
reference at the FIRST '.'. A type name never contains a dot, but a unit
qualifier can — so 'blaise.testing.TTestCase' decoded to unit='blaise',
type='testing.TTestCase'. ReadClassPayload then stored the mangled
'testing.TTestCase' as the imported class's ParentName, and the bare Lookup in
ResolveParentClassByName could not find it. The intermediate cached class's
Parent pointer was left nil, so the leaf's inherited-method walk dead-ended one
level early and reported 'Undeclared procedure'.
Fixes:
* DecodeQualRef now splits at the LAST '.', so the type name is always the bare
trailing component and the unit qualifier keeps its full dotted name. This
corrects every qual-ref decode site (consts, vars, fields, return types,
parents), not only class parents.
* ResolveParentClassByName resolves the parent via TSymbolTable.FindType
(qualifier-aware, uses-chain-honouring) instead of a bare Lookup, as defence
in depth for any qualified parent name.
Regression: TSepCompileTests.TestIncrementalRebuild_QualifiedGrandparentMethod
drives a real warm --unit-cache rebuild over a 3-level chain rooted in a dotted
unit; it fails against the pre-fix compiler and passes after. Verified to
reproduce the original 'Undeclared' failure when either fix is reverted.
All four fixpoints (QBE, native, internal-assembler, warm-cache) and the full
suite (3751 tests, QBE-built and native-built runners) pass.
Compiling a multi-unit program whose uses-clause pulls in every runtime
unit (the auto-generated bootstrap_program used by 'pasbuild compile -m
blaise-runtime') failed in the semantic pass. Several runtime units each
privately bind the same C/RTL symbol (e.g. 'external name '_BlaiseGetMem'',
'external name 'abort'') or define a private implementation-section helper
of the same name (DiagAbort in both blaise_arc and blaise_exc). In the
flat-merge these all share one global scope and overload group, producing
spurious 'Duplicate identifier' and 'Ambiguous overload' errors.
Three coordinated changes:
* SameLinkSymbol/EffectiveLinkName: collapse candidates that denote the
same underlying link symbol when at least one side is an external
binding — covering both two-units-bind-same-C-symbol and a binding that
targets a real function exported by an unmangled RTL unit (blaise_*/rtl.*,
whose exports keep their bare Pascal name). SameExternalDecl and the
overload-collapse / zero-arity paths now route through it.
* BenignDuplicateExternal: when an interface-section Define collides in the
global scope, tolerate it if the colliding symbol is the same link symbol.
* IsImplOnly flag on TMethodDecl: marks implementation-section-only
routines as private to their unit; ResolveStandaloneOverload excludes
cross-unit private candidates so each unit's call binds its own helper.
Adds three regression tests in cp.test.multifile.pas. All four fixpoints
(QBE, native, internal-assembler, warm-cache) and the full suite (3750
tests, QBE-built and native-built runners) pass.
When a class inherits its interface implementation from an ancestor and the
implementing method is NON-virtual, the descendant's interface table named
$<descendant>_<method> — a symbol that does not exist — causing a link error
('undefined reference to TDerived_Hello'). A non-virtual method has no vtable
slot, so the itab resolver could not find it via the vtable and fell back to
naming the method after the descendant class instead of the declaring ancestor.
Fix in both backends: when the vtable lookup fails, walk the AST class chain
(self, then ParentName ancestors) to the nearest class that DECLARES the method
and name $<declaringclass>_<method>. QBE gains ItabImplClassName; native
extends ItabMethodRefNative with the same chain walk (it now takes the type-decl
list to resolve ancestors by name).
The existing inherited-interface tests only used virtual methods, which DO get
vtable slots, so this path was uncovered. New regression
TestRun_InheritedInterface_NonVirtualMethod compiles+links+runs on both backends.
Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, full suite green (3747) under both the
QBE-built and native-built test runners.
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.
Native x86-64 is the default backend with a complete internal assembler and
internal linker — compiling and linking with zero external tools (QBE is now
opt-in via --backend qbe, deprecated). Incremental, per-unit compilation is
the default, and per-unit OPDF makes incrementally-built multi-unit programs
fully debuggable in pdr. Ships a real opt-in stdlib: JSON, SHA-1/Base64, RFC
4122 GUIDs, TCP sockets, WebSockets, a minimal HTTP/1.1 server, plus the
blaise.testing framework. .bif format advanced to v2.
Includes two incremental-toolchain fixes folded in before release: the internal
linker now errors instead of silently emitting a runtime-less binary when
blaise_rtl.a is unreachable, and warm --unit-cache rebuilds propagate staleness
to dependents of an edited unit (previously raised EImportError / could crash).
Fixpoint verified at 410477 lines of QBE IR — FIXPOINT_OK, NATIVE_FIXPOINT_OK,
NATIVE_INTERNAL_OK, and WARMCACHE_FIXPOINT_OK all green. 3745 compiler tests
(1 ignored) + 57 stdlib tests passing.
Version 0.12.0 -> 0.13.0-SNAPSHOT (Blaise.pas, project.xml) and COMPILER_ID
-> blaise-0.13.0-SNAPSHOT+bif2 (the .bif format is unchanged, so the +bif2
tag stays; the version prefix tracks the dev cycle).
When a unit's source content changed, it was correctly recompiled from a warm
--unit-cache, but a cached unit that USES it kept its cached interface and was
imported in the cached-iface phase — which runs BEFORE source-recompiled units
are analysed. Importing the cached dependent then failed to resolve a type
defined only by the not-yet-analysed dependency, raising EImportError (empty
message), e.g. editing blaise.codegen.driver and rebuilding the whole compiler
warm broke on blaise.frontend.opts (TBackendKind unresolved). The same
corrupted warm cache also produced an intermittent SIGSEGV in the e2e runner.
uUnitLoader now tracks units taken via the source path (FSourceLoadedNames).
In the cached path, after recursing into a unit's interface-use dependencies,
if any dependency was source-loaded the cached iface is discarded and the unit
is recompiled from source too, so it is analysed after its dependency.
Post-order DFS makes this transitive.
fixpoint-warmcache.sh now edits a dependency unit's content between its two
builds (previously two clean fills that never exercised staleness propagation);
it FAILS on the pre-fix compiler and PASSES on the fixed one.
The internal linker silently emitted a runtime-less binary when blaise_rtl.a
could not be found beside the compiler (RTLPath empty): every RTL symbol became
an undefined dynamic import, so the binary linked but died at runtime with
`undefined symbol: _SetArgs`.
scripts/rolling-bootstrap.sh hit exactly this: it carried each step's binary
forward as _carry_blaise with its RTL as _carry_rtl.a, but FindRTLArchive looks
for `blaise_rtl.a` beside the binary (CompilerBinDir) — a name it never used —
so the next step linked the compiler with no runtime archive and SMOKE_FAILED.
- Carry the binary + RTL into a dedicated _carry/ dir as blaise + blaise_rtl.a,
so FindRTLArchive resolves it. Rolling bootstrap reaches HEAD again.
- Make an empty RTLPath a hard error on both link paths (internal and external
cc) instead of producing a broken binary.
- Regression test TInternalLinkerE2ETests.TestLink_MissingRTL_FailsLoudly:
runs a symlinked compiler from an RTL-free dir and asserts a non-zero exit, a
message naming blaise_rtl.a, and no output binary.
The four fixpoint scripts missed this because fixpoint-native.sh links the .s
via gcc, never exercising the internal-linker + FindRTL path on a carried
binary layout.
The .bif format was bumped 1->2->3 internally across this development cycle, but
the last PUBLIC commit (d56bdbf, release v0.11.x) shipped IFACE_VERSION = 1 with
COMPILER_ID +bif1. None of the intermediate v2/v3 layouts ever reached a public
reader, so the cumulative format change since v1 is a SINGLE public step: collapse
it to v2.
- IFACE_VERSION 3 -> 2; comment now lists all the META/record additions made this
cycle under v2 (ImplUsedUnits + HasInitialization, block local var decls,
Exit value, parameter defaults, free-routine ResolvedQbeName, generic-class
template properties, TRoutineSig vtable facts).
- COMPILER_ID stays blaise-0.12.0-SNAPSHOT+bif2 (now matches IFACE_VERSION).
- Magic-string test updated to expect 'BLAISE-IFACE 2'.
v1 .bif written by v0.11.x are rejected and recompiled from source, as intended.
A generic instantiated over a metaclass type argument (e.g.
TList<class of TFoo>) carries a space in its type name ('class of TFoo'),
which flowed unescaped into emitted assembler labels like
_FieldCleanup_TListEnumerator_class of TFoo — rejected by the assembler.
Map space to '_' in both the native x86_64 and QBE name manglers, alongside
the existing < > , $ @ ^ handling, so metaclass-typed generic instantiations
emit valid labels.
Instantiating a generic with a type alias as the argument (e.g. TList<TAlias>)
produced a different instantiation identity than using the alias's underlying
type, because one path substituted the alias name literally while another
resolved it. Inside the generic body this made TList<T>.GetEnumerator's
declared result type (TListEnumerator<TAlias>) differ from the constructed
value (TListEnumerator<Underlying>), failing an internal assignment — so
TList<AnyAlias> was unusable.
Resolve each generic argument through its alias chain to the canonical
underlying-type name before forming the instantiation key (mirroring the
canonicalisation already done for array-of / pointer / set-of / class-of
element types), only when the resolved name genuinely differs (preserving
existing string/String mangled names). Adds a regression test.
3744 compiler tests (20 pre-existing toolchain failures unchanged), stdlib OK.
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.
Follow-up to the interface free-function fix: the remaining return-type
resolution sites — two method paths and the standalone/program-body function
path — still used FTable.FindType, so a generic return like TList<String> on
a program-level function or certain method paths failed 'Unknown return type'.
Switch them to FindTypeOrInstantiate for consistency; additive, so non-generic
returns are unaffected. 3743 compiler tests pass (20 pre-existing
toolchain-dependent failures unchanged).
A unit interface-section free function returning a generic instantiation
(e.g. function MakeList: TList<string>) failed with 'Unknown return type'
because the free-function return-type resolution used FTable.FindType, which
only matches already-registered types and never instantiates a generic on
demand. Class-method return types already resolved correctly via
FindTypeOrInstantiate; this aligns the free-function paths (interface and
implementation, in both AnalyseUnit and AnalyseUnitForExport) with that.
The change is additive — FindTypeOrInstantiate tries FindType first — so
non-generic returns are unaffected. Adds an e2e regression test.
Incremental compilation is the default, but each dependency unit was compiled
to its own .o with NO OPDF debug info — only the top --source unit got an
.opdf section. pdr could therefore not set breakpoints, show callstacks, or
inspect locals inside any dependency (RTL, stdlib, or a user's own units),
which makes the default pipeline effectively undebuggable.
Mirror FPC's multi-unit OPDF model (which pdr already reads): every object —
each unit AND the program — carries a complete, self-contained .opdf section
[32-byte header][records], and the linker concatenates the same-named sections.
The header's TotalRecords is written as 0 (stream-terminated): readers consume
records to section EOF and skip the embedded per-unit headers (and linker zero
padding). pdr needs NO changes — it already handles exactly this layout.
Layers:
- uDebugOPDF.pas: EmitHeader writes TotalRecords=0 (PatchTotalRecords now a
no-op); new CreateForUnit + unit-mode DoEmit that emits a unit's types /
globals / constants / function scopes / line info from its IntfBlock+ImplBlock
and the unit's symbol table, with NO program main scope and NO unit directory
(directory is program-only; pdr ignores it). Whole-program output is
unchanged apart from TotalRecords=0. MangledClassSym matches the native
backend's class-symbol naming so vtable labels resolve in unit mode.
- Blaise.pas: TCompileWorker.Execute appends a unit-mode OPDF section to each
unit's IR when --debug-opdf and the native codegen produced debug facts, so
the per-unit .o embeds its own .opdf. (QBE has no facts → no per-unit OPDF;
native is the debug backend.)
- blaise.assembler.x86_64.pas: accept `.section .opdf, "aw", @progbits`
(new eskOpdf); `.int` alias for `.long`; `.byte`/`.word` now parse
comma-separated value lists (needed for the OPDF magic + BuildID bytes).
- blaise.elfwriter.pas: emit the .opdf section as SHF_ALLOC|SHF_WRITE PROGBITS
so it lands in the loadable image where pdr finds it; the internal linker's
section merger already concatenates .opdf by name and resolves its
.quad <funclabel> relocations.
Verified: pdr breaks inside a dependency unit, shows the cross-unit callstack,
and inspects locals — under BOTH the internal toolchain (default) and external
gas/ld, incremental AND --no-incremental. Full suite OK (3742) on both build
paths; all four fixpoints pass (normal builds unaffected). Regression test
TestDebugOpdf_PerUnitSection_InDependencyObject asserts the dependency .o
carries an .opdf section.
A class compiled against a CACHED base unit (loaded from target/units .bif)
built a wrong vtable: RegisterClassMethod added a slot only for methods flagged
virtual/override and assembled the table in .bif append order, assuming that
order matched the source slot order. Two things broke that:
- the .bif lists newly-introduced virtuals before inherited overrides, not in
source declaration order; and
- a non-virtual constructor named Create occupies an implicit vtable slot
(metaclass dispatch through a class-of reference) that the importer skipped
entirely.
So a descendant compiled against a cached base dropped the Create slot and
shifted every later virtual up by one. Cls.Create(args) then dispatched into
whatever method fell into Create's old index (e.g. RunTest), the constructor
body never ran, fields stayed nil — which aborted the stdlib JSON test suite
when its TTestCase base came from target/units.
Fix: RegisterClassMethod now places each method carrying a vtable slot at its
authoritative index (ASig.VTableSlot) via the new
TRecordTypeDesc.SetVTableSlotAt, which grows the vtable with empty-named
placeholders as needed. Order-independent; covers virtual, override, and the
implicit-slot constructor. Also fixes a latent SetUp-override resolution
regression the append-order import caused.
Regression test TestIncrementalRebuild_VirtualCtorVtableSlot: a warm-cache
rebuild of a class with a non-virtual Create + a virtual method, instantiated
through a class-of reference, asserts the constructor body actually runs.
Both backends: stdlib cached build OK (19 tests); compiler suite OK (3741);
all four fixpoints pass on both build paths.
Two float-expression gaps, both fixed on native and QBE:
1. Implicit-Self float field READ (Result := FFloat inside a method).
- native: EmitExprToXmm0 had no implicit-Self field case, so the field fell
through to the plain VarOperand path and emitted `movsd FFloat(%rip)` — a
load from a global symbol named after the field (undefined in per-unit .o
codegen). Now loads from Self + field offset. (The store path was
already correct; only the read was missing it.)
- qbe: the implicit-Self field read in EmitExpr hardcoded `loadl` for any
non-word field, so a Double/Single field loaded with loadl into an l temp
and the following `stored`/`ret d` rejected it. Now uses the field type's
load opcode (loadd/loads) with the matching result type.
2. Qualified float-returning method call as a float expression
(d := Obj.GetF()). native EmitExprToXmm0 had no TMethodCallExpr case and
raised "unsupported float expression form"; now dispatches the method (the
SysV ABI leaves the float result in %xmm0).
e2e test in cp.test.e2e.sepcompile compiles a unit with a Double field read via
implicit Self to a .o and runs a consumer over it (the per-unit path that
exposed the native symbol bug), printing the float method-call result on both
backends. Full suite OK (3740); all four fixpoints pass on both build paths.
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.
An unqualified call to a virtual method from within a method body (implicit
Self, Foo(args) with no Self. prefix) was emitted as a static direct call to
the declaring class instead of a vtable dispatch — so an override was silently
skipped (wrong polymorphism), and an abstract base method link-failed
(undefined symbol <Class>_<Method>). Self.Foo(args) already dispatched
correctly; Self. must not change dispatch semantics.
The implicit-Self call paths never reached the `if VTableSlot >= 0` vtable
branch the explicit-Self paths use. Fixed in both backends:
native (blaise.codegen.native.x86_64.pas):
- New EmitSelfDispatch / EmitSelfDispatchVia: vtable dispatch on Self (in %rdi
or an explicit register) when the method is virtual, else a static callq.
Wired into the implicit-Self expr call, statement (TProcCall) call (<=6 and
>6-slot paths), interface-sret call, and record-sret method call.
- New EmitFuncCallSret routes an implicit-Self method TFuncCallExpr that
returns a record/jumbo-set through EmitMethodSretCall (which passes Self and
vtable-dispatches) instead of the free-function EmitSretCall, covering local,
var-param, global, implicit-Self-field and explicit-field assignment targets.
- These implicit-Self arg-marshalling paths also adopted EmitPopMethodArgsToRegs
/ EmitMethodOverflowLoad, so float args to an implicit-Self call are now
passed in xmm registers too (same class of bug as the prior overflow fix).
qbe (blaise.codegen.qbe.pas): the implicit-Self statement and expression call
emitters now resolve the call target via SretMethodCallTarget (a loaded vtable
function pointer for a virtual method, else the static $symbol), matching the
sret path that was already correct.
A non-virtual unqualified call still emits a static direct call (no needless
vtable). Tests in cp.test.e2e.classes2 cover override dispatch, abstract-base
link+dispatch, non-virtual stays static, and interface- and record-returning
virtual implicit-Self calls, all on both backends. Full suite OK (3738); all
four fixpoints pass on both build paths.
Float arguments to a call with more than 6 register slots were mis-classified
on both the caller and callee sides of the native x86-64 SysV ABI, because the
overflow machinery counted every argument as one integer register slot. With
floats interspersed (which take xmm registers, not integer ones) this put
integer args at the wrong overflow address and crashed the callee — affecting
free functions (EmitCall) as well as method/constructor/inherited calls.
Caller side (blaise.codegen.native.x86_64.pas):
- EmitCall (free functions): the overflow tail assumed exactly the first six
contiguous stack slots were register-bound. It now records the actual
integer-overflow slot offsets during the register-load pass and relocates
them to a 16-byte-aligned region at the top of the allocation, so %rsp is
aligned at the call and the lowest-indexed overflow arg is at 0(%rsp).
- New EmitMethodOverflowLoad + OverflowArgIsFloat replace the integer-only
store/load in all five method-family overflow paths (method expr/stmt,
constructor expr/stmt, inherited). Float arg slots are stored as their xmm
bit pattern and loaded into xmm0.., integers into the SysV integer registers
(Self = reg 0), with float-skipping overflow relocation. The former
GuardNoFloatInOverflow stopgap is removed.
Callee side (BuildFrame + prologue spill loop): a by-value float parameter now
consumes an xmm slot (separate XmmIdx2) instead of an integer slot, so it no
longer pushes a following integer parameter onto the stack while the caller
passes it in an integer register. The prologue float spill bound is corrected
from 6 to 8 (SysV has 8 xmm argument registers).
Tests: e2e overflow-with-float cases for free function (cp.test.e2e.native),
and method/constructor/inherited (cp.test.e2e.classes2), all on both backends.
Full suite OK (3733); all four fixpoints pass on both build paths.
The native x86-64 backend marshalled float arguments correctly for free
functions (SysV: floats -> %xmm0..7) but not for instance-method, constructor,
or inherited calls: those push every arg through the integer-register model, so
a Double/Single argument was either a codegen crash (literal -> "unsupported
expression form TFloatLiteral") or a silent ABI miscompile (variable bits
landed in an integer register while the callee read %xmm).
Fix, all in blaise.codegen.native.x86_64.pas:
- EmitMethodArgPush: a by-value float scalar arg is now materialised via
EmitExprToXmm0 (handles literals through the .LF constant path), width-
adjusted to the param type, and pushed as its 8-byte bit pattern.
- New EmitPopMethodArgsToRegs: replaces the integer-only `popq SysVArg64(I+1)`
loops in the <=6-slot path of EmitMethodCallExpr, EmitMethodCallStmt, the
constructor arg loop, EmitInheritedCallSeq, and the sret method path. It
classifies each slot and routes floats to %xmm0.. and integers to the
integer registers, advancing independent counters per SysV (Self still
occupies integer slot 0/1).
- The rare >6-register-slot overflow path remains integer-only; a by-value
float there now raises a clear "not yet supported" error (GuardNoFloatInOverflow)
instead of crashing cryptically or silently miscompiling.
Tests: 6 e2e cases in cp.test.e2e.classes2 (method/ctor x literal/variable,
mixed int+float+string interleaving, multiple floats across xmm0/xmm1) run on
both backends, plus an IR assertion in cp.test.nativeconstarg that the call
site emits movsd into %xmm0.
Assigning a float value to a class field via an implicit Self reference
(e.g. `V := d` in a method/constructor where V: Double) emitted a hardcoded
`storel` on a `d`/`s` SSA value, which QBE rejects ("invalid type for first
operand in storel"). The implicit-Self field-store path in EmitAssignment
fell through to a literal `storel` instead of StoreInstrFor(field type), and
lacked the d<->s width coercion the EmitFieldAssignment path already has.
Use StoreInstrFor(ISFld.TypeDesc) (-> stored/stores for tyDouble/tySingle) and
add the Single<->Double truncd/exts coercion before the store. Surfaced by the
new float-constructor e2e tests, which run on both backends.
bif-coverage verified that every AST node field round-trips through the .bif
encoder/decoder, but the .bif interface-container types (TRoutineSig,
TUnitInterface, TMethodParam, TConstEntry, TVarEntry) were hand-serialised in
WriteMeta/EncodeMethodSig/etc. with no drift guard. Every cached-rebuild bug
just fixed was a serialised field on one of those types dropped from one side
of the round-trip — invisible to the tool, surfacing only as a runtime
miscompile.
Generalise the class scanner to ScanClassFile(path, names, objs, allowList);
add ScanInterfaceTypes() over an allow-list of the container types in
uUnitInterface.pas. Split uUnitInterfaceIO.pas into encoder-side and
decoder-side text and assert each serialised interface-type field's identifier
appears in both — the same looseness as the AST mechanism. Status file gains
the interface-type entries (serialise/safe); mutator-repopulated owning
collections are { no-bif }-exempt (their element data round-trips through the
per-entry encoders).
Negative test confirmed: dropping ImplUsedUnits/HasInitialization/VTableSlot
from either side is now reported as a gap with exit 1. Clean tree exits 0.
Incremental compilation (per-unit .o + embedded .bif, the default) produced
silently corrupt binaries when rebuilding into a populated --unit-cache: the
clean build worked, but any rebuild that loaded a unit from its cached .bif
instead of source dropped or mishandled state the in-memory path carried.
Root cause is uniform across every symptom: the .bif round-trip was not a
complete projection of the unit interface, and the cached-load path in the
loader/driver diverged from the source-load path.
.bif format bumped to version 3 (IFACE_VERSION; CompilerId +bif tag) so stale
caches are cleanly rejected and recompiled from source.
Fixes (each was a field/decision present on the source path, absent on the
cached path):
- Loader: a unit's implementation-section `uses` were never recorded in the
.bif, so on rebuild the impl-only dependency's object was dropped from the
link (undefined symbol). Record ImplUsedUnits in the .bif and collect those
objects for LINK ONLY (CollectLinkOnlyObject) — not semantic import, which
would break the leaf-first iface import on interface/impl-use cycles.
- Native codegen: a global var owned by a cached unit was re-defined by the
recompiled top program (`multiple definition of GTarget`). The program now
references imported-unit globals as external (FImportedUnits / IsImportedGlobal
gate in EmitDataSection); NoteDepInitUnit records every imported unit.
- Unit initialization: <Unit>_init calls for cached deps were never emitted, so
initialization sections silently never ran. Record HasInitialization in the
.bif and note prebuilt-iface inits in dependency order at program codegen.
- SynthesiseMethodDecl dropped VTableSlot/IsVirtual/IsOverride from cached
method imports, degrading virtual dispatch to direct calls into the abstract
base (SIGABRT at run time). Propagate them.
- Generic-instance field/param types now resolve via the analyser's generic-
aware resolver on import (ResolveImportedTypeName); generic instantiation
during import no longer nil-derefs FProg/FCurrentUnit (pending-instance flush).
- .bif round-trip completeness: serialise block-local var decls, Exit(value)
return values, parameter default values, free-routine ResolvedQbeName,
generic-class template properties, and fix implements-interface name
stripping (strip to the last dot, not the first).
Adds TestIncrementalRebuild_ImplOnlyUses_LinksDependency (e2e) and updates the
magic/version assertion. Full suite OK (3722 tests); QBE, native, and
internal-assembler fixpoints pass; clean and warm-cache rebuilds both produce
correct binaries.
The parser's Ident.Ident[Index] path unconditionally expected ']:=' after
the subscript, rejecting chains like Self.FStack[0].Count := value. Add a
chain loop after ']' that handles '.Field :=', '.Method()', and further
'[Index]' suffixes.
Additionally, the QBE backend's EmitInstancePtr did not handle
TFieldAccessExpr nodes with IsArrayAccess set — it returned the field slot
address instead of the dereferenced array element pointer. Delegate to
EmitExpr when IsArrayAccess is true so the dynarray pointer is loaded and
the element offset is computed correctly.
LkZeros built zero-padding strings by appending one byte at a time
(Result := Result + Chr(0)), which for a ~2.7 MB executable produced
trillions of byte copies. Replace with SetLength + PChar fill.
Also fix LkLE to pre-allocate via SetLength instead of concatenating
per-byte, and add an early-exit guard to LkCopyInto.
A type written as 'UnitName.TypeName' (the unit qualifier may itself be
dotted, e.g. System.SysUtils.TFormatSettings) was a parse error: after
reading the leading identifier ParseTypeName only accepted a generic
'<...>' argument list, not a '.' continuation.
Parse the full dotted path into the type name, then resolve it by its
final component — a type is always a single identifier; the leading
components name a unit already loaded by the 'uses' clause. The strip is
centralised in TSymbolTable.FindType (via UnitQualifierTail) so every
type position benefits uniformly — var/field decls, parameters, and the
several routine return-type sites — and FindTypeOrInstantiate strips the
qualifier off a generic instance (Unit.TList<Integer>) before
instantiation. Plain dotted identifier paths only: the bracket/space/
caret/angle encodings for array, pointer, set and generic types are left
untouched.
Tested: dotted-name capture in the parser, semantic resolution of a
qualified builtin (System.Integer), and end-to-end compile+run of a
program using a cross-unit qualified type.
(cherry picked from commit 7e9c3d1f61d42769bc3341195220a25190637e15)
A unit implementation-section routine whose body opens with a nested
procedure/function declaration (before any var/const/type section) was
mis-parsed: ParseUnit called ParseMethodDecl without ACanHaveNestedProcs, so a
leading 'procedure'/'function' was treated as the next top-level decl and
parsing failed at the outer 'begin' ("Expected 'end' but got 'begin'").
Pass ACanHaveNestedProcs=True for the two impl-section ParseMethodDecl calls.
The IsForward guard already present in ParseMethodDecl (our commit 33db0f6)
keeps a forward decl from claiming the following sibling as its nested body, so
only the call-site change was needed — Andrew's branch independently added the
same forward-flag mechanism, which we already have.
Note: this is a PARSER fix (the unit body now parses correctly). Resolving a
CALL to such a nested procedure is a separate, pre-existing semantic-pass gap
that affects program routines too (Inner() reports "Cannot find declaration");
it is out of scope here. Andrew's tests are likewise parse-level.
Tests: cp.test.units.pas parse-level tests (outer routine is one decl with a
body holding the nested proc).
Cherry-pick policy: the forward-flag half was a duplicate of our work and
skipped; the ACanHaveNestedProcs threading + tests are taken.
Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite green
on both the QBE-built and native-built runners (3717 tests).
Implements real symbol-presence conditional compilation in the lexer.
Previously {$IFDEF} was hardcoded false (always took {$ELSE}) and
{$DEFINE}/{$UNDEF} were silently consumed, with no define table and no
command-line flag.
- A case-insensitive define table on TLexer. {$DEFINE sym} / {$UNDEF sym}
add and remove symbols; {$IFDEF sym} keeps its body when defined (and
{$IFNDEF sym} when not), with an optional {$ELSE} and a closing {$ENDIF}.
IFDEF/IFNDEF blocks nest (the existing depth-tracking skip helpers are
reused, now driven by the real truth value).
- Predefined symbols, seeded in every lexer: BLAISE (the headline
cross-compiler use case — {$IFDEF BLAISE} ... {$ELSE} ... {$ENDIF}) plus
the target CPU/OS symbols CPUX86_64, CPUAMD64, LINUX, UNIX. No version
macro yet.
- A -d / --define <sym> command-line flag (FPC -dSYM / Delphi -D), carried
on TFrontEndOpts and threaded to the program's lexer AND to every unit the
TUnitLoader compiles, so {$IFDEF} resolves consistently across the program
and its units.
Tests:
- cp.test.lexer.pas: predefined BLAISE keeps the body, undefined takes ELSE,
IFNDEF, DEFINE-then-IFDEF, UNDEF-then-IFDEF, and AddDefine (the -d path).
- cp.test.e2e.misc.pas (dual-backend): the {$IFDEF BLAISE} cross-compiler
pattern, and a combined DEFINE/UNDEF/IFNDEF/CPU-OS/nested program.
Docs: grammar.ebnf documents the directives and predefines; language-
rationale.adoc records the decision (symbol-presence only, no {$IF} expr
form yet; BLAISE + CPU/OS predefined, no version macro) and alternatives.
Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite
green on both the QBE-built and native-built runners (3715 tests).
The internal assembler routed every `movq`/`movd` to the integer mov encoder,
which reads both operands as general-purpose registers. So `movq %xmm0, %rax`
was emitted as `mov %rax,%rax` (48 89 C0) — register 0 in both fields — silently
discarding the XMM value.
The native backend emits `movq %xmm0, %rax` to box a Double's raw bits into an
array-of-const (the Format() argument path) and to spill float bit patterns, so
ANY float passed to Format() under the internal assembler (now the default
toolchain) arrived as garbage and printed 0.0000. A regression vs v0.11.0,
which used the external assembler. The e2e test harness assembles native via
cc, so only the internal-assembler suite exercises this path.
Fix: `movq`/`movd` with an XMM operand is an SSE move and is routed to a new
EncodeMovqXmm:
gp/mem -> xmm : 66 [REX.W] 0F 6E /r
xmm -> gp/mem : 66 [REX.W] 0F 7E /r
with the XMM operand as the ModRM.reg field and REX.W set for movq (64-bit),
clear for movd (32-bit).
Tests:
- cp.test.assembler.pas TAsmEncodingTests: byte-level encoding of
`movq %xmm0,%rax` (66 48 0F 7E C0, and asserts it is NOT 48 89 C0) and
`movq %rax,%xmm0` (66 48 0F 6E C0).
- TInternalAsmE2ETests.TestFormatFloatArg: Format('%.4f',[1.25]) through the
internal assembler prints 1.2500.
Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite green
on both the QBE-built and native-built runners (3707 tests).
Two related defects in integer constant handling:
- A large hex literal that needs more than 32 bits (e.g. $080808080808 =
8830587504648) was silently TRUNCATED to 32 bits (printed 134744072). An
untyped integer constant was unconditionally typed as Integer regardless of
its magnitude, so codegen treated it as a 32-bit word.
- A literal above High(Int64) (e.g. $8080808080808080) was REJECTED at parse
time with "Integer literal exceeds Int64 range", even though it is a valid
64-bit bit pattern and is accepted by FPC/Delphi.
Fixes:
- uParser: the bare-integer-literal const path now uses ParseIntOrUInt64Literal
(which already existed for the tolerant case) instead of ParseIntLiteral, so a
value in the UInt64 range is captured as a bit pattern and flagged via a new
TConstDecl.IsUInt64.
- uSemantic (AnalyseConstDecls): an untyped integer constant now picks its type
by magnitude — Integer when it fits in signed 32-bit, Int64 when larger, and
UInt64 when the value exceeds High(Int64) (matching Delphi, which prints
$8080808080808080 as 9259542123273814144). A typed const (e.g. A: Int64 =
$8080808080808080) keeps its declared type and stores the bit pattern, so it
prints -9187201950435737472.
Tests (cp.test.e2e.misc.pas, dual-backend): large hex not truncated, typed
Int64 bit pattern, typed UInt64 bit pattern, and untyped-above-Int64 widening
to UInt64.
Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite green
on both the QBE-built and native-built runners (3704 tests).
A `var` open-array parameter was broken three ways; a `const` open array with
the same body worked, so the defects were specific to the var/open-array path.
- Semantic (uSemantic, AnalyseStaticSubscriptAssign): writing an element of an
open-array parameter raised "'a' is not a static array or dynamic array" —
there was no tyOpenArray case. Added one: a var open array allows element
writes; a const open array is rejected with a clear "declare it 'var'"
message.
- QBE codegen (element READ): an open-array ident value-read fell into the
scalar var-param branch, which dereferences the slot twice. An open-array
param slot already holds the data pointer directly (for both const and var),
so the second load read garbage and segfaulted. Added a tyOpenArray case
that loads the data pointer once.
- Native codegen (element WRITE): tyOpenArray fell through to the static-array
store and raised "static subscript assign on non-static-array". Open arrays
now share the dynamic-array element-write path, with the var-param extra
deref suppressed (the open-array slot is already the data pointer).
Tests:
- cp.test.e2e.openarray.pas (dual-backend): element read (the issue repro),
element write mutating caller storage, and read-modify-write mixing var and
const open arrays.
- cp.test.semantic.pas: var open-array element write is accepted; const
open-array element write is rejected.
Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite green
on both the QBE-built and native-built runners (3700 tests).
A class that inherits an interface from its ancestor (the ancestor declares
the interface; the descendant does not re-list it) was not recognised as
implementing it. `g := TLoud.Create` where TLoud < TPerson(IGreeter) failed
with "Type mismatch", and after the semantic gate was opened the codegen
referenced a non-existent itab.
Three coordinated fixes:
- uSemantic (CheckTypesMatch): the class->interface compatibility check
scanned only the class's own ImplementsList. It now walks the whole class
parent chain, so a descendant inherits its ancestors' interface
implementations.
- blaise.codegen.qbe / blaise.codegen.native (EmitInterfaceDefs): itab/impllist
generation likewise only considered a class's own ImplementsList, so no itab
was emitted for an inherited interface. Both backends now:
* skip a class only when neither it nor any ancestor implements an
interface (and, on native, point the typeinfo at the impllist on the same
condition);
* collect implemented interfaces by walking the class parent chain;
* resolve each itab method ref via the vtable slot's ImplName, so an
overridden method points at the descendant's body and an inherited
(non-overridden) method at the ancestor's — uniformly correct across
arbitrary inheritance depth.
Tests (cp.test.e2e.interfaces.pas, dual-backend via AssertRunsOnAll): the
exact issue repro (descendant assigned to an interface var, dispatching to its
override), and a three-level chain through an interface parameter mixing an
overridden method with an inherited non-overridden one.
Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite green
on both the QBE-built and native-built runners (3695 tests).
A `forward;`-declared routine in a program's declaration section swallowed the
following separate implementation (and everything up to the program's `end.`)
as its own body, producing "Expected ';' but got '.'".
Root cause: in ParseMethodDecl, when ACanHaveNestedProcs is true (the
standalone-proc context used for program/unit blocks), a bare
`procedure`/`function` keyword after the signature triggers body parsing so a
nested sub-routine is absorbed into the enclosing routine. A forward decl has
no body, so the keyword that follows it is the SEPARATE implementation — but
the body-trigger fired anyway and consumed it.
Fix: track a local IsForward flag (set when the `forward` directive is seen)
and suppress the body-parse trigger for it, exactly as IsExternal already does.
Mutually-recursive routines declared with forward now parse and run.
Tests: a parser test asserts the forward decl and its implementation are two
separate ProcDecls (forward has no body, impl has one); a dual-backend e2e
test runs mutually-recursive IsEven/IsOdd and checks the computed parity.
Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite green
on both the QBE-built and native-built runners (3693 tests).
`type TByte = 0..255;` failed to parse — ParseTypeDecl had no case for an
integer-literal subrange, so the RHS fell through to the generic "expected
record/class/..." error.
Blaise does not range-check, so a named subrange is treated as an alias to
the narrowest STANDARD integer type that holds both bounds (0..255 -> Byte,
-10..10 -> SmallInt, etc.). This keeps record/array element layout correct
(TByte is byte-sized) while the value behaves as an ordinary integer. Two
parser helpers do the work: SubrangeAhead (lookahead: IntLit.. or -IntLit..)
and ParseIntegerSubrangeBaseType (parse lo..hi, pick the base type, reject a
descending range). Note Blaise has no 8-bit signed alias, so a signed
subrange that would fit in ShortInt widens to SmallInt.
Only integer-literal bounds form a named type; identifier/enum-bounded
subranges (TLow..THigh, red..blue) are intentionally not handled here (they
are ambiguous as a named-type form).
Tests: parser tests (named subrange, negative bounds, descending-is-error)
and dual-backend e2e tests (named subrange runs; subrange as a record field
and array element with a negative range). grammar.ebnf gains the
IntSubrange rule.
Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite
green on both the QBE-built and native-built runners (3691 tests).
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).
EmitSretCall (the sret path for free-function record returns) pushed one
stack slot per LOGICAL argument, so an interface argument — a fat pointer
that occupies TWO integer-register slots (obj + itab) — had its second slot
dropped. The arguments landed in the wrong registers and the stack was
left unbalanced, crashing. This is the free-function sibling of the method
fix in 077f13b (EmitMethodSretCall).
Fix: in EmitSretCall's <=5-slot register path, push interface args as two
slots (obj then itab, via EmitMethodArgPush for the direct case and the
saved fat pointer for the hoisted akIntfConsume case), track bytes pushed so
the hoisted-value reloads stay correct, and pop one register per SLOT. The
register-vs-spill guard now uses a new SretUserSlots helper that counts
interface params as two. The >5-slot overflow path raises a precise error
for the (not-yet-supported, untested) interface-in-overflow combination
rather than silently mis-placing registers.
Surfaced by Numerics.Money's rounding overloads: MoneyFromStr(amount,
currency, IRoundingStrategy) is a free function returning a record (TMoney)
with an interface argument, which hit this path.
Regression tests in cp.test.e2e.recordret.pas cover a free function
returning an sret record with an interface arg, with and without a leading
scalar arg, via AssertRunsOnAll (QBE + native parity).
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).
EmitRecordCopy copied every non-managed scalar field with loadw/storew (or
loadl/storel), ignoring the field's actual width. A sub-word field —
Boolean or Byte (1 byte), SmallInt or Word (2 bytes) — was therefore
stored with a 4-byte storew that overran into the following field. When a
Boolean sat immediately before a managed (string / class / dynarray) field,
the wide store clobbered the low bytes of that pointer; the subsequent
_StringRelease / _DynArrayRelease then dereferenced a garbage address and
crashed.
This is exactly TDecimal's layout (FNegative/FInflated Booleans ahead of a
carrier with a managed field), so it blocked Numerics.Money on QBE:
MakeMoney rounds into a TDecimal local and stores it into TMoney.FAmount,
copying the record by value. The native backend was always width-correct.
Fix: route scalar field copies through LoadInstrFor/StoreInstrFor (the same
width-correct helpers already used for element and variable loads), and copy
inline aggregate fields (static array / jumbo-set bitmap) with memcpy.
Regression test in cp.test.e2e.records.pas reproduces the TDecimal-shaped
layout (Boolean flags before a string field, copied through an sret Result
field) and asserts QBE/native parity via AssertRunsOnAll.
Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite
green on both the QBE-built and native-built runners (3625 tests).
A record-returning method that takes an interface parameter crashed (sret
return) or produced garbage (register return) on the native backend. An
interface is a fat pointer (obj + itab) occupying TWO integer-register
slots, but EmitMethodSretCall popped one register per LOGICAL argument
instead of per ABI slot. The interface's second slot was never consumed:
the user arguments landed in the wrong registers, and in the sret case a
stack slot was left unpopped, unbalancing the frame and corrupting return
addresses (the observed 0x1 / 0xffffffffffffffff backtrace).
Fix: in both EmitMethodSretCall register-passing paths (register-return
record and sret-return record, <=4-arg form), pop CountArgSlots(Params)
registers — counting interface params as two — into the integer registers
after sret/Self, rather than ACall.Args.Count. The <=4 register-path
guard is likewise changed to gate on slot count (UserSlots + 2 <= 6), so
an interface arg cannot silently overflow the register file.
This is the root cause of the TDecimal.Divide / RoundTo / SetScale native
crash: each takes a `const IRoundingStrategy` (interface) and returns a
record. With the fix, the full division/rounding battery — all eight
rounding modes, banker's rounding, custom IRoundingStrategy, rmUnnecessary,
the arbitrary-precision inflated path, and div-by-zero — runs identically
on native and QBE.
Tests:
- cp.test.e2e.recordret.pas: three new regression tests for an interface
argument to a record-returning method (sret return, register return,
and a scalar-before-interface ordering check), via AssertRunsOnAll.
- cp.test.e2e.numerics.decimal.pas: the 14 Divide/RoundTo/SetScale/float
e2e tests that were CompileAndRunWithRTLQBEOnly now run dual-backend
(CompileAndRunWithRTL), giving native parity coverage for the whole
decimal arithmetic surface.
Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite
green on both the QBE-built and native-built test runners (3624 tests).
A method call whose receiver is itself a record-returning call —
A.Plus(B).Val() — crashed on the native backend. The receiver
expression was lowered with EmitExprToEax, leaving the record VALUE
(the register-return payload, or the first bytes of the sret buffer) in
%rax, which was then used directly as the Self POINTER. Dereferencing
that garbage segfaulted. QBE handled the chained form correctly, so the
dual-backend e2e harness flagged it as a native/QBE parity divergence.
Fix: when a record method's receiver is a record-returning call,
materialise the call result into a stack buffer and pass its ADDRESS as
Self. The buffer address is carried in callee-saved %rbx so it survives
the argument push/pop sequence; both are freed after the call. Applied
to both EmitMethodCallExpr paths (<=6 and >6 arg slots) and all four
receiver sites in EmitMethodSretCall.
For the sret path, a forwarded %rsp-relative destination (the nested
chain A.Plus(B).Plus(A), where EmitRecordCallSretAt forwards '(%rsp)')
is resolved to an absolute pointer in callee-saved %r14 BEFORE the
receiver-materialisation prologue moves %rsp, so the destination no
longer drifts.
Regression tests in cp.test.e2e.recordret.pas cover the register-return
chain, the sret-record chain, the >5-arg overflow path, the nested
double chain, and the managed-field (TDecimal-shaped) record — all via
AssertRunsOnAll (QBE + native parity).
Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full
suite green on both the QBE-built and native-built test runners
(3621 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).
Compiling a UNIT (top source is a `unit`, so the pipeline runs in unit-mode and
Prog stays nil) via the default incremental path segfaulted: the incremental
worker setup unconditionally read `Prog.SymbolTable` to seed each dep worker,
dereferencing nil. In unit-mode the symbol table comes from the semantic pass,
not a program node. This is exactly the invocation the runtime Makefile drives
(`blaise --source X.pas --output X.o` per RTL unit), so it broke `make` in
runtime/ once incremental became the default; it was a latent pre-existing bug
in the opt-in incremental path (reproduces at de0ea5d with --incremental).
Fix: seed the worker symbol table from Semantic.GetSymbolTable() when Prog is
nil, matching the non-incremental unit-mode path.
Also pin the runtime Makefile to --no-incremental. That Makefile is itself a
hand-managed separate-compilation system (one object per unit + an explicit
archive list); the compiler's incremental mode would additionally write
per-dependency side-effect objects and skip inlining a used unit's bodies, but
those side-effect objects are not in the archive list — so a cross-unit symbol
(e.g. typeinfo_TRtlPlatform, referenced by the derived TRtlPlatformPosix in a
different unit) was left undefined at link. Whole-program per unit keeps each
unit object self-contained.
Regression test (cp.test.e2e.sepcompile.pas): compile a unit that uses another
unit in unit-mode via the default incremental path, then build and run a
program over the emitted object.
A float (Double/Single) by-value argument passed to a function or method that
returns a record with a managed field (dynamic array / string, i.e. the true
sret return path) was mis-handled by the native backend: every argument was
materialised via EmitExprToEax, the integer-only emitter. A float LITERAL hit
"unsupported expression form TFloatLiteral", and a float VARIABLE would have
been routed through %rax into an integer argument register instead of an %xmm
register — wrong per the SysV ABI. QBE compiled these correctly, so it was a
native/QBE parity divergence.
This blocked DecFromFloat / DecFromFloatExact (TDecimal has a dynamic-array
field) and any MoneyFromFloat-style constructor in Numerics.Money.
Fix: both EmitSretCall (free functions) and EmitMethodSretCall (methods) now
detect a float-typed by-value argument and, when present, evaluate arguments
into stack slots and load them into registers per the SysV ABI — integer args
into the integer argument registers (after %rdi=sret and, for methods,
%rsi=Self), float args into %xmm0.. with an independent counter, and %al set to
the vector-register count. The pure-integer path is unchanged, so the common
case (and native self-compile) is byte-for-byte identical.
Adds three e2e regression tests (cp.test.e2e.recordret.pas), run on both
backends via AssertRunsOnAll: single float arg to a managed-record-returning
function, two float args, and a single float arg to a managed-record-returning
method.
Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; the new tests
pass on both the QBE and native arms.
Incremental compilation (per-unit .o emission with embedded .bif) is now the
default for every build; --no-incremental opts out and builds a single
whole-program object. The old --incremental opt-in flag is removed.
Fix a latent incremental-mode bug the flip exposed: in incremental /
--skip-dep-codegen mode the dependency unit BODIES are compiled into their own
objects and AppendUnit is skipped for them in the main program codegen. But
AppendUnit was also where a unit's initialization section got registered into
the init-call list, so the program startup ($main) emitted no
`call <Unit>_init` for any dep — every dependency's initialization section
silently never ran. This affected BOTH backends and was why a native-built
TestRunner (built incrementally by default) registered zero tests: each
cp.test.* unit's `initialization`/RegisterTest never executed.
Fix: add ICodeGen.NoteDepInitUnit(name, hasInit), implemented on the QBE
backend and the native backend (virtual on TNativeBackend, overridden by the
x86_64 backend), which records a dep unit's init symbol in dependency order
without emitting its body. The driver calls it for each skipped dep in
incremental mode, so $main emits the calls and they resolve against the
<Unit>_init symbols exported by the per-unit objects. Verified across the full
matrix (native/qbe x incremental/--no-incremental), including chained
multi-unit init ordering (a dep that uses another dep sees the other's init
having run first).
Guard the incremental block so it never runs in --emit-ir / --emit-asm /
--dump-ast modes (those produce stdout text and no objects); this keeps
fixpoint.sh, fixpoint-native.sh and rolling-bootstrap.sh — all of which use
--emit-ir/--emit-asm — unaffected.
Tests: replace the --incremental e2e test with one that exercises the default
incremental path (no flag), plus a new --no-incremental test asserting the
whole-program path leaves no per-unit .o behind.
Remove the eight TestCodegen_Win64_* record-return IR tests and the GenIRWin64
helper. They pinned aspirational QBE Win64-ABI IR for a target the native
backend does not support (only linux-x86_64 is implemented) and that is being
deprecated along with QBE. The native backend miscompiled the in-process
GTarget-global Win64 path these tests drove; rather than carry IR tests for an
unsupported, unrunnable, soon-to-be-removed target, they are dropped. Native
Win64 will be addressed when Windows support is actually built.
Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite
3534 tests / 0 failures on both the QBE-built and native-built test runners.
Phase 3 of the native-default drive — the locally-verifiable items (CI and
rolling-bootstrap changes are deferred to Phase 4, where they become correct
once v0.12.0 is the rolling-bootstrap anchor):
- Help text now marks the backends explicitly: "qbe (deprecated) | native
(default)". QBE is retained as the e2e parity oracle and QBE-fixpoint guard;
removal is a later release.
- fixpoint-native-internal.sh now drives the FULL internal pipeline (internal
assembler + internal linker) against an all-external (gcc assemble + gcc link)
reference, instead of only differing on the assembler. Both internal tools are
now the default toolchain, so the conformance guard must exercise both. Header
comments updated to reflect that it guards the internal linker too and why it
stays a differential probe (the internal assembler buffers the whole object in
memory and OOMs on the compiler's own ~631k-line .s).
- language-rationale.adoc records the decision: native backend + internal
assembler + internal linker as the default toolchain (no external tools beyond
the C runtime startup objects), why QBE is kept-but-deprecated, and the
per-invocation escape hatches (--assembler external, --linker external,
--backend qbe, --incremental).
Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite
3541 tests / 0 failures on both the QBE-built and native-built test runners.
The native dynamic-array SetLength lowering loaded the array's current data
pointer into %rdi (the first argument to _DynArraySetLength) BEFORE evaluating
the new-length expression. When that expression itself emitted a call — most
commonly SetLength(Result, Length(A)), where Length(A) lowers to a
_DynArrayLength call — the call clobbered %rdi, so _DynArraySetLength resized
the wrong array (the length-expression's operand) instead of the intended one.
The visible symptom: a function taking a dynamic-array parameter and returning
a dynamic array had its Result aliased to the first array parameter's buffer.
SetLength(Result, Length(A)) resized A instead of Result, every Result[I] write
was lost, and the caller got back the first argument unchanged. QBE was correct
throughout; this was a native-only parity divergence.
Fix: evaluate the length expression into %esi first, then load the array
pointer into %rdi immediately before the call — matching the safe ordering the
string and field-receiver SetLength paths already used.
Adds five e2e regression tests (cp.test.e2e.dynarray.pas) covering the result-
from-param-length case, the const-array-param subtract, global-from-global
length, local-from-local length, and length-from-a-user-function-call. All run
on both backends via AssertRunsOnAll; the native arm would have failed before
this fix. This closes a test-coverage gap: no existing test exercised a
SetLength whose length argument emitted a call.
The internal assembler had two TLS-related bugs that caused undefined
symbols in .o files compiled from runtime units using threadvars:
1. sym@tpoff(%reg) operands: the parser included '@tpoff' as part of
the symbol name instead of recognising it as a relocation modifier.
Fixed by stopping symbol-name accumulation at '@', detecting the
'tpoff' suffix, and setting a TpOff flag that emits R_X86_64_TPOFF32.
2. %fs:0 (thread base read): the parser fell through to symbol parsing
and created an undefined reference to literal "0". Fixed by checking
for a numeric displacement before attempting symbol parsing in the
%fs: handler.
Also fix the --help text to show 'native (default)' instead of
'qbe (default)' now that the native backend is the default.
The native backend now uses --assembler internal and --linker internal
by default when the user does not explicitly pass these flags. This
eliminates the dependency on external tools (gcc/as/ld) for native
backend compilation.
Also fixes a linker bug where writing to a currently-executing binary
on Linux would fail with EStreamError — the linker now calls
DeleteFile before creating the output file.
The internal assembler had two bugs in .ascii directive parsing:
1. Comment stripping treated '#' as a comment start even inside quoted
strings, silently truncating string literals containing '#'. This
corrupted .rodata for any program with '#' in string constants
(e.g. QBE IR format strings like "# String literals").
2. The \xHH hex escape syntax was not handled. Bytes encoded as \xE2
etc. were emitted as literal ASCII 'xE2' (3 bytes) instead of the
single byte 0xE2, corrupting UTF-8 string data.
Both bugs were invisible when using the external assembler (gcc) and
only manifested when --assembler internal was used on large programs
with non-ASCII string literals or '#' characters in strings.
Four tests exercise the full compiler-driver pipeline with
--backend native --assembler internal --linker internal:
HelloWorld, StringOps (Format/UpperCase/IntToStr), exception
handling (raise + try/except), and virtual method dispatch.
These catch regressions in TLS relocation, CRT discovery,
duplicate-symbol handling, and PIE emission.
Add --linker internal|external option to the native backend driver,
routing the link step through TLinker instead of shelling out to cc.
FindCrtObjects discovers CRT startup objects (Scrt1.o, crti.o, crtn.o,
crtbeginS.o, crtendS.o) on Debian and RedHat layouts.
Fix SHF_TLS constant from $200 to the correct ELF spec value $400 in
both the reader and writer — the wrong value caused .tbss sections to
be placed twice (once in the normal NOBITS loop, once in the TLS
section), producing doubled negative TPOFF32 offsets and segfaults.
Fix FTlsAddr to use the post-alignment address from PlaceSection rather
than the pre-alignment cursor, which caused off-by-padding TPOFF values.
Change duplicate global symbol handling from error to first-wins
semantics (matching ld behaviour), needed because the main program
object and archive members may both define system runtime functions.
Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK,
3532 tests pass on both QBE-built and native-built test runners.
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.
The native x86-64 backend is now the default (bkNative). QBE remains
available via --backend qbe. Several codegen bugs that were invisible
while QBE was the default are fixed in this commit:
Native backend fixes:
- EmitLeaqGlobal helper: threadvar globals now emit the correct
fs:0 + @tpoff TLS sequence instead of bare %rip-relative leaq.
Fixes static array write (FreeLists), string subscript assignment,
EmitExprAddr, record memcpy, method-ptr cast, and open-array push
paths that all used raw leaq %s(%rip).
- EmitIncDec: unified local/global paths through VarOperand so
threadvar scalar Inc/Dec (e.g. LargeFreeCount) gets TLS addressing.
- EmitIncDec FAE.Base: Inc(P^.Field) where P is a pointer now
evaluates the base expression instead of leaq on the stack slot.
- FinalizeEmit: data section (.data/.bss/.rodata) now emits once at
the end of unit compilation via FinalizeEmit, not inside each
EmitUnit call. Fixes duplicate/missing string literals in sep-compile.
- EmitFunctionDef AExported: implementation-only helpers in monolithic
mode stay file-local (.globl suppressed) to avoid symbol collisions
with the RTL archive.
QBE backend fixes:
- AppendUnit typeinfo: export prefix added to data $typeinfo_TObject,
$typeinfo_TCustomAttribute, $vtable_TObject, $vtable_TCustomAttribute
so native-built RTL can resolve them.
- FSuppressSystemDefs: decoupled from FExportAll so CreateUnitCodeGen
can export all symbols without suppressing system defs.
Infrastructure:
- runtime/Makefile: BLAISE_FLAGS variable for passing extra compiler
flags (e.g. --backend qbe) from build scripts.
- fixpoint.sh: make clean before RTL rebuild to pick up backend change.
Flip the compiler's default --backend from qbe to native. Native has full
feature parity with QBE (the v0.11.0 cycle's 116 fixes plus the dual-backend
e2e harness brought them level), self-hosts, and gives no external codegen
dependency, source-level debugging, and a path to cross-compilation — so it
is the right default to exercise everywhere.
QBE stays fully supported via --backend qbe and remains the cold-bootstrap
root: --emit-ir still routes to QBE regardless of the default (PickTopDriver
ignores --backend for emit modes), so scripts/rolling-bootstrap.sh,
fixpoint.sh, and CI — all of which pass --emit-ir explicitly — are unaffected
by this change. The only behaviour change is that a plain
'blaise --source X --output Y' (and a plain 'make' in runtime/) now uses the
native backend.
Flipping the default surfaced and fixed a real native gap first (the
unit-to-.o duplicate-system-defs link collision, previous commit); the full
suite is green with the TestRunner built by BOTH a QBE-built and a
native-built compiler, and all three fixpoints pass.
Compiling a unit to a standalone .o with the native backend
(blaise --source U.pas --output U.o) emitted the built-in system
definitions — typeinfo_/vtable_/_FieldCleanup_ for TObject and
TCustomAttribute — into the unit .o. Linking that .o against a consumer
program (which emits its own copies) failed with
'multiple definition of vtable_TCustomAttribute'.
Two gaps fed this:
- The unit-as-top-level path in Blaise.pas built its codegen via
Driver.CreateCodeGen (the program path, separate-compile = False) instead
of Driver.CreateUnitCodeGen (separate-compile = True), so the backend's
FSeparateCompile gate (which already suppresses the system defs) never
engaged. Switched the IsUnitMode branch to CreateUnitCodeGen, with a
CreateCodeGen fallback for any backend lacking a unit codegen.
- TCodeGenNative.GenerateUnit did not propagate FSeparateCompile to the
backend (AppendUnit already did); added the missing propagation for
defensive correctness.
The QBE backend was unaffected (its unit path already suppresses these).
Adds TestNativeBackend_RoundTrip_NoDuplicateSystemDefs in
cp.test.e2e.sepcompile, which pins --backend native so the guard holds
regardless of the compiler's default backend.
EmitByteRhsToEax lowered a single-character string-literal RHS via
Ord(TStringLiteral(AExpr).Value[0]). That subscript-then-Ord idiom is
miscompiled by the self-hosted native stage: a native-built compiler baked a
pointer-sized garbage immediate (movl $0xd291f9ec) instead of the byte, so
S[i] := 'c' wrote junk. It only surfaced in CI, where the test runner is
built by the native-bootstrapped -pre binary; local verification used the
QBE-built fixpoint binary, which lowers the idiom correctly, so the bug was
invisible.
Use StrAt(Value, 0) — the OrdAt-backed byte-read helper already used
elsewhere in this backend and stage-stable — instead of Ord(Value[0]).
Matches the QBE backend, which uses StrAt and was never affected.
Verified by building the test runner with a native-built compiler (the CI
configuration): full suite passes; all three fixpoints clean.
S[I] := <byte> was rejected by the semantic pass ('is not a static array
or dynamic array') for any string — local, global, or var-param — on both
backends. It is now supported as the symmetric counterpart of the S[I]
read: an in-place byte store into a 0-based UTF-8 string.
Because strings are reference-counted and literals are immortal (stored in
read-only memory; the native backend put them in .rodata, so a naive store
segfaulted, while QBE silently mutated the shared literal), the write
performs copy-on-write. New RTL helper _StringUnique(S) returns S when it
is uniquely owned (rc=1) and otherwise allocates a fresh rc=1 copy, releases
the old reference, and returns the copy. Both backends emit
_StringUnique -> write the result back to the slot -> storeb, so the slot
keeps exactly one owned reference and mutating one alias never disturbs
another (Delphi/FPC UniqueString semantics).
The RHS accepts a numeric ordinal, Chr(n), or a single-character literal
(the byte-shaped forms used in place of a Char type). PChar subscript
writes keep their existing in-place path (raw pointer, no ARC header).
Dual-backend e2e tests (write, copy-on-write aliasing + literal reuse,
var-param) in cp.test.e2e.stringops. Updates language-rationale.adoc
(writable subscript + COW) and grammar.ebnf (string as a subscript-assign
base).
$hex, %binary and &octal integer literals were rejected inside an
array-constant initialiser — 'array[0..1] of Int64 = ($1, $2)' failed
with 'Cannot resolve array-const element' while the scalar form '= $1'
worked. The parser stores array elements verbatim (prefix and all), and
ResolveConstArrayElem only recognised decimal digits, so any radix-prefixed
element fell through to an identifier lookup and errored.
Fold radix-prefixed elements (with optional leading sign) through
ParseIntLiteral — the same canonical parser the rest of the compiler uses
for scalar and typed consts — so all four radixes, plus underscore digit
separators, behave identically inside an array initialiser. Keeps FPC's
&-octal (the project prefers FPC syntax where FPC and Delphi diverge; the
lexer already tokenised all four radixes).
Documents the radix-prefix rule in language-rationale.adoc.
Implement the Phase B internal-linker pipeline (TLinker in
blaise.linker.elf) on top of the Phase A section merger:
- Symbol resolution: build the global symbol table from merged
sections (STB_GLOBAL over STB_WEAK; two globals = duplicate error),
define the five linker-synthesised symbols (_GLOBAL_OFFSET_TABLE_,
__bss_start, _edata, _end, __TMC_END__), resolve weak-undefined
references to 0 and raise on strong-undefined.
- Static relocations: patch R_X86_64_PC32 / R_X86_64_PLT32 as S+A-P
(PLT32 against an internal symbol relaxes to the direct form, no
PLT). Reject the dynamic-only forms (R_X86_64_64, GOT/TLS) and
absolute 32-bit symbol references with a diagnostic — deferred to
Phase C.
- Layout + emission: assign virtual addresses at a fixed base
(0x400000) in an executable run (.text + .rodata, sharing the first
page with the headers) and a writable run (.data + .bss) on a fresh
page; write a non-PIE ET_EXEC with two PT_LOAD program headers, the
relocated section payloads, and a section-header table for tooling;
mark the file executable.
Platform/arch-variable values (Is64, OSABI, EMachine, BaseAddr,
PageSize) live in TLinkTarget rather than being hard-coded, so FreeBSD
and a future i386 ELF target are a new target record and a PE/Mach-O
container would be a sibling writer behind the same symbol/relocation
core.
The linker is not wired into the compiler driver — it is a standalone
unit exercised only by tests; all backends still link via cc. Driver
integration behind --linker internal is Phase D.
Tests: TLinkerTests (symbol resolution, PC32 cross-object displacement,
R_X86_64_64 rejection, ELF-header/entry-point structure) and
TLinkerE2ETests (hand-written write+exit syscall object linked
internally to an ET_EXEC, executed, asserted on stdout + exit code;
readelf -a reports a clean statically-linked x86-64 executable).
Two native-backend codegen constraints were worked around in this unit:
no string-element assignment through a var-string parameter (append-
style byte building + PChar-local in-place writes), and no capturing
nested functions (private methods instead).
An `inherited Method(args)` whose Self plus arguments occupy more than the
six System V integer argument registers aborted native codegen with
"System V integer arg register index 6 out of range". EmitInheritedCallSeq
only implemented the all-in-registers case: it pushed every argument and
popped each into a register, walking past the six available once Self + the
args exceeded six slots. There was no stack-spill path like the regular
method-call and constructor emitters already have.
Give EmitInheritedCallSeq the same >6-slot handling: when Self + args exceed
six register slots, build the six-register prefix plus an overflow region in
one frame, place Self in slot 0, evaluate each argument into its slot, load
registers 0..5, drop the prefix so the overflow sits at the callee's
[rsp+0..], then static-dispatch to the parent. Up to six register slots
keeps the existing push/pop path. QBE was already correct.
This is independent of the call position and of metaclass dispatch — any
inherited call with seven or more register slots (e.g. a method or
constructor with six+ scalar/aggregate args calling its parent) was
affected.
Adds an e2e regression (cp.test.e2e.classes2) exercising an inherited call
with Self + 7 integer args (8 register slots), asserting correct dispatch
and result on every backend.
(cherry picked from commit b1fc1285ff3984eedaf6a68d58e2d568f1e4c793)
Calling a constructor through a metaclass-typed variable as a statement,
with the result discarded — `C.Create(args);` where `C: class of TBase` —
compiled cleanly but segfaulted at runtime on both the QBE and native
x86-64 backends. The same call in expression position (`o := C.Create();`)
worked.
The semantic pass flags the statement node with IsConstructorCall and
IsMetaclassDispatch, but only the expression-position lowering acted on
them (allocating via _ClassCreate and dispatching the ctor through the new
instance's vtable). The statement-position emitters had no such branch, so
the call fell through to the ordinary virtual-method path: it loaded the
metaclass value (a typeinfo pointer) as if it were the receiver object,
read a bogus vtable from the typeinfo's first word, and called a garbage
slot — crashing.
Add the metaclass-constructor branch to both statement emitters
(TCodeGenQBE.EmitMethodCall and TX86_64Backend.EmitMethodCallStmt): load
the metaclass, allocate via _ClassCreate, marshal the args, dispatch the
ctor indirectly through vtable[(slot+1)*8], then release the freshly built
(refcount 1) instance so the discarded result is freed rather than leaked.
A non-metaclass constructor as a statement (TFoo.Create();) is still
rejected earlier by the semantic pass as 'not a variable', so only the
metaclass shape reaches the new branch.
Adds an e2e regression (cp.test.e2e.classes2) covering zero-arg and
multi-arg dispatch and asserting the instance is freed (destructor count).
(cherry picked from commit 3f719213a838ab886b5db74847e7bef8894c404a)
Accept an anonymous integer subrange as a set base type, e.g.
'set of 0..255' or 'set of 1..10', in both the inline (var/param/field)
and 'type T = set of L..H' declaration positions. The subrange lowers to
the same bitmap machinery as 'set of Byte': the bitmap is sized to H+1
bits and member ordinals are the integer values themselves. Bounds may be
integer literals, named constants, or constant expressions; the lower
bound must be >= 0, the upper <= 255, and the range ascending.
Also fix 'set of Boolean' so a Boolean operand is accepted directly on
either side of 'in' and as the second argument of Include/Exclude, without
an intervening Ord().
Parser handles both 'set of' paths (ParseTypeName and ParseSetDef);
semantic resolution adds ResolveSubrangeSetType for on-demand and
type-decl paths. Adds dual-backend e2e tests and semantic unit tests.
Updates grammar.ebnf and language-rationale.adoc per the language-decision
rule, and removes the now-implemented item from future-improvements.adoc.
The KI-3 ARC triple-release bug — multiple `on E: TYPE do` handlers in one
except block colliding on a single QBE slot — is fixed, so the workaround of
giving each handler a distinct variable name can be reverted. Handler vars are
now the natural `E` again in:
- blaise.testing.pas: the runner's 4-handler dispatch block
(EAF/EIT/E/ETO -> E) — the exact KI-3 scenario, in the test framework's
own outcome classifier.
- cp.test.attributes.pas: ETO -> E.
- cp.test.sets.pas: ESE/EEx -> E.
(There were no {$IFDEF FPC} blocks left to remove — the KI-2 .Free guards and
all other FPC conditionals were already gone from the source tree as part of
the FPC-independence migration; verified by a full-tree directive scan.)
All three fixpoints green; 3501 tests pass, including the framework's own
FAIL/IGNORED/ERROR outcome paths.
An empty bracket literal [] passed to a plain open-array parameter
(`array of T`) failed overload resolution with "No matching overload" on both
backends. A non-empty literal works because AnalyseArrayLiteralExpr infers the
element type and types it `array of <elem>`; an empty [] has nothing to infer
from and was left untyped (nil), so it matched neither the open-array param (nil
arg type) nor the existing set / array-of-const special cases.
Two changes in uSemantic:
* ArgMatchScore now scores an empty TArrayLiteralExpr as an exact (2) match
against any tyOpenArray parameter — an empty open array is valid for any
element type — placed before the nil-arg bail so overload selection accepts it.
* RetypeSetLiteralArgs pins the empty []'s ResolvedType to the chosen formal's
open-array type, so codegen emits a zero-length open array (data=nil, high=-1)
of the right element type.
Overload disambiguation is preserved ([] picks the open-array overload; a scalar
argument still picks the scalar overload), and two equally-matching open-array
overloads still raise a clean "ambiguous overload" error. Works for
`array of string`, `array of Integer`, and `array of const`.
This was the last open item in native-testrunner-investigation.txt; the native
TestRunner now passes the full suite unfiltered. Adds dual-backend e2e tests.
All three fixpoints green; 3501 tests pass.
A multi-dimensional array whose dimensions are enum types — array[TEnum, TEnum]
or array[TEnum, 0..1] — failed to parse with "Expected '..' but got ','". Only
the single-dimension form array[TEnum] was handled; every dimension in the
comma-separated multi-dim path was forced through the lo..hi range parser.
Both the const-array dimension parser (ReadConstArrayDim) and the type-name
parser (ParseTypeName) now recognise an enum-type dimension — a bare identifier
not followed by '..' — and encode it as the range 0 .. @TEnum, reusing the
existing single-dimension '@TEnum' convention. ResolveArrayBound resolves the
'@TEnum' high-bound marker to the enum's last ordinal (member count - 1). This
fixes const, type and var declarations in any dimension position and mix
(enum+range, range+enum, 3-D enum+range+enum).
Adds dual-backend e2e coverage: 2-D enum/enum, mixed dims in both orders,
type+var form, and a 3-D mixed case. All three fixpoints green; 3499 tests pass.
A typed set constant accepted only enum-member identifiers — `const C: TByteSet =
[1, 2, 3]` and `[1..3]` both failed with "Expected set member identifier". The
const-decl set path (a separate, more limited parser than the expression set
literal) is extended to accept:
* integer-literal members (1, 2, 3),
* inclusive ranges `lo..hi` (each endpoint an integer literal, enum member, or
named constant), expanded to all ordinals in range,
* and ordinal base types (set of Byte / Boolean), not just enums.
Parser stores each member verbatim (a range as 'lo..hi'); the semantic pass
(AnalyseSetConstDecl) resolves and expands them via a new ResolveSetMemberOrd
helper, integrating with the existing small-mask and jumbo byte-bitmap const
machinery. Enum-member const sets (the original form) still work.
Adds dual-backend e2e coverage including edge cases: mixed range+literal, a jumbo
(>64-member) set with a range, the empty set, descending-range rejection, and an
enum-set regression. All three fixpoints green; 3495 tests pass.
An array constant with Boolean (or enum, or named-constant) elements emitted the
identifiers verbatim — `(False, True, ...)` became symbol references, failing at
link time on native ("undefined reference to `False'") and in QBE ("unknown
keyword False"). And even once folded, every non-string element was emitted at a
fixed 4-byte stride that did not match the element's actual read width, so a
Boolean array read all-zero.
Two fixes:
* Semantic (AnalyseArrayConstDecls): a new ResolveConstArrayElem folds each
bare-identifier element to its numeric value once the element type is known —
Boolean True/False to 1/0, enum members and named integer/boolean constants to
their ordinal/value. Numeric/float literals pass through. An unresolved
identifier is now a clear semantic error instead of a link failure.
* Codegen (both backends): array-const element data is emitted at the element's
real width — .byte/.word/.long/.quad on native, b/h/w/l in QBE — instead of a
fixed .long/w. Built-in scalar names are mapped directly so the width is
correct even when the codegen has no symbol table set (the e2e in-process path).
Adds dual-backend e2e coverage including edge cases: Byte/Word/Int64 widths,
negative integer pass-through, named-const elements, and a Boolean array indexed
by an enum. All three fixpoints green; 3490 tests pass.
The native backend previously stubbed GenerateUnit and left
SupportsIncremental/SupportsWarmCache False, so --incremental fell back to QBE.
Native now drives the per-unit parallel pipeline itself:
* TCodeGenNative.GenerateUnit emits a single unit in isolation (new
TNativeBackend.GenerateUnit: clear buffer, EmitUnit, return assembly).
* The native driver overrides SupportsIncremental/SupportsWarmCache (True),
CreateUnitCodeGen (separate-compile mode), and LowerToObject (assemble the
unit .s to .o via the internal assembler or `cc -c -x assembler`).
* Separate-compilation mode (FSeparateCompile): unit objects omit the
once-per-program TObject/TCustomAttribute system defs — the program object
provides them — and emit their own file-local string-literal blobs, so a unit
method referencing a literal links cleanly. The system typeinfo/vtable symbols
(typeinfo_TObject, vtable_TObject, and TCustomAttribute) are now .globl so unit
objects can reference the program's single definition.
Works with both the external (`cc -c`) and internal assemblers, and the warm
cache (a second build reuses the per-unit .o). Adds a sepcompile e2e test
building a multi-unit class program with --backend native --incremental.
All three fixpoints green; 3485 tests pass.
A static array of interfaces (Arr: array[0..2] of IGreet) was unusable on the
native backend in three ways, all now fixed:
* Store (Arr[I] := V): the element is a contiguous 16-byte fat pointer, but the
generic static-subscript store wrote only the 8-byte obj and left the itab
garbage. Now computes the element address and delegates to
EmitInterfaceToFieldSlotsAt (obj+itab with ARC for nil/class/interface).
* Read as RHS (G := Arr[I]): EmitInterfaceAssign gained an interface-subscript
source case that copies obj+itab with ARC.
* Method dispatch (Arr[I].M()): EmitInterfaceCall gained a subscript-receiver
case that loads obj/itab from the element address.
A shared EmitIntfStaticElemAddr helper computes the element fat-pointer address
(base + (idx-LowBound)*16), reused by all three paths and by the interface-store
source. EmitInterfaceToFieldSlotsAt also learned an interface-subscript source
(Arr[J] := Arr[I]). Native now matches QBE exactly on the full test
(11/22/22/11/11). TestRun_StaticArrayOfInterface_FatPointer is dual-backend.
(A pre-existing scope-exit leak of interface array elements affects BOTH backends
identically — logged separately in bugs.txt, not a parity gap.)
All three fixpoints green; 3484 tests pass.
Show(MakeFoo(42)), where MakeFoo returns an interface, handed the borrowing
parameter an owned (+1) fat pointer that was never released after the call —
--debug reported one leaked instance on native (QBE already released it).
Added an akIntfConsume hoist kind: EmitArgHoist drives the interface-returning
call in the pre-pass and saves the owned fat pointer (itab then obj, obj on
top); the argument reload pushes both halves as the two interface arg slots; and
EmitHoistEpilogue releases the obj after the call (mirrors akStrConsume). The
2-slot reload is wired into both EmitArgPush and EmitCall's inline argument loop.
--debug leak report is now clean on both backends; two leakcheck tests (QBE +
native) are added. All three fixpoints green; 3484 tests pass.
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.
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.
A non-generic class could not inherit from a generic-class instance:
TBox<T> = class ... end;
TIntBox = class(TBox<Integer>) ... end; // rejected, then bad IR
Two bugs:
1. Semantic: AnalyseTypeDecls assumed any generic name in the first
heritage slot was an interface — it cast FindTypeOrInstantiate's
result to TInterfaceTypeDesc and moved it to the implements list, so a
generic CLASS parent was rejected as "Unknown interface 'TBox<Integer>'
in implements list". It now classifies the instantiated descriptor: a
tyInterface result is an implements entry, anything else stays as the
parent class for normal class-parent resolution.
2. QBE codegen: the inheriting class's typeinfo referenced its parent as
$typeinfo_TBox<Integer> via ClassSymName (which does not mangle <>),
but the generic instance's typeinfo is defined under the QBEMangle'd
symbol $typeinfo_TBox_Integer — producing invalid QBE IR ("invalid
character <"). The parent reference now uses QBEMangle for a generic
instance, matching its definition. The native backend already mangled
correctly and only needed the semantic fix.
Verified inherited method + field access and a virtual method declared on
the generic base and overridden in the derived class, on both backends.
Adds TE2EGenericsTests.TestRun_InheritFromGenericInstance_{MethodAndField,
VirtualOverride} and a note in docs/language-rationale.adoc.
Accessing a class const through the class type (TThing.MaxCount, not an
instance) was broken differently on each backend:
- Native: EmitExprToEax had no IsConstant case for TFieldAccessExpr, so
the access raised "unsupported expression form TFieldAccessExpr".
- QBE: the string class-const path emitted
`$_StringRetain(l $%tN)` — a non-existent RTL symbol, with the literal
temp wrongly double-prefixed by `$` (invalid IR). This path had never
worked; it was the only reference to the phantom _StringRetain.
Native now emits the inlined constant (movabsq for ordinals,
EmitStrLitAddr for strings), mirroring the IDENT-const path. QBE returns
the string literal's data-pointer temp directly, exactly like a plain
string literal (the consumer manages ARC). Accessing the same const via
an instance (t.MaxCount) already worked on both.
Adds TE2EClasses2Tests.TestRun_ClassConst_{IntViaType,StringViaType,
ViaInstanceAndType} (dual-backend); the string test also concatenates the
const to exercise ARC.
A derived class that declared an `overload` method with the same name as
an overload inherited from its base SHADOWED the inherited variants
instead of merging with them:
TBase = class
function F(x: Integer): string; overload;
end;
TDerived = class(TBase)
function F(s: string): string; overload;
end;
d.F('a'); // ok
d.F(5); // was rejected: "expected string but got Integer"
Two causes, both fixed:
1. ResolveMethodOverload stopped walking the inheritance chain as soon
as the first class declaring the name yielded any arity match, so the
base overloads were never considered. It now unions the overload
groups up the parent chain, stopping only when a level declares the
name WITHOUT `overload` — a non-overload method hides inherited ones
(the conventional redeclaration-hides rule), an `overload` method
extends the set (Delphi semantics).
2. The variable-receiver method-call path used FindMethodDecl (first
match in the chain) and never invoked overload resolution at all. It
now analyses the arguments and calls ResolveMethodOverload, falling
back to FindMethodDecl for the no-overload case. (Arguments must be
analysed before resolution so candidates can be scored against their
resolved types.)
Adds TE2EInheritTests.TestRun_OverloadMergeAcrossInheritance (dual
backend) and an "Overload sets merge across inheritance" subsection to
docs/language-rationale.adoc.
A property whose getter or setter is declared `virtual` did not dispatch
through the vtable: reading or writing the property through a base-typed
variable holding a derived instance called the BASE accessor, not the
override. A direct b.GetVal() call dispatched correctly, but b.Val (the
property over the same virtual getter) did not.
TBase = class
function GetVal: Integer; virtual; begin Result := 1; end;
property Val: Integer read GetVal;
end;
TDerived = class(TBase)
function GetVal: Integer; override; begin Result := 99; end;
end;
b: TBase := TDerived.Create;
WriteLn(b.Val); // was 1, now 99
The property read/write lowering always emitted a static call to the
accessor's declaring class. Now the semantic pass records the accessor's
vtable slot on the AST node (PropAccessorVSlot, -1 when the accessor is
not virtual), and codegen dispatches through the vtable when the slot is
>= 0, exactly as a direct method call does.
QBE: PropAccessorTarget computes the call target — emitting the vptr+slot
loads and returning the function-pointer temp for a virtual accessor, or
the static mangled symbol otherwise — and each call site emits its own
`call <target>(...)`. (A single emit-and-return helper was tried first
but tripped a latent native-backend miscompile on the self-compile; see
bugs.txt. The target-string shape avoids it.) Native:
EmitPropAccessorCallNative dispatches through the vtable or statically.
Covers getter and setter, on both backends. Adds
TE2EInheritTests.TestRun_VirtualProperty{Getter,Setter}_Dispatches
(dual-backend). The two new AST fields are marked `safe` in
bif-coverage.status (semantic-set, not serialised).
A child class that inherits a method-backed property failed to link:
Tbase = class
property classValue: Integer read getValue write setValue;
end;
Tchild = class(Tbase) ... end;
child.classValue := 12; // -> undefined reference to Tchild_setValue
writeln(child.classValue); // -> undefined reference to Tchild_getValue
Property accessors are mangled as <owner>_<method>. The read/write
lowering set PropOwnerType to the static variable's class (Tchild), but
the getter/setter symbols are only emitted for the class that declares
them (Tbase). The call referenced Tchild_getValue / Tchild_setValue,
which never exist, so linking failed on both backends.
Adds PropAccessorOwner, which walks the inheritance chain from the
variable's class upward and returns the class that actually declares the
accessor (falling back to the static type when none is found, leaving
the non-inherited case unchanged). All property read and write lowering
sites — direct, qualified-base, implicit-Self, and indexed-property-on-
field-type — now resolve PropOwnerType through it.
Regression test TestRun_InheritedProperty_AccessorsResolveToParent in
cp.test.e2e.classes2.pas compiles + runs the issue's program on both
backends and asserts stdout '12'.
Fix approach contributed via the issue thread by andrewd207; extended to
cover the indexed-property-on-field-type accessor sites as well.
cp.test.inherit.pas asserted only on generated QBE IR substrings and
never fed the IR to a backend, so virtual dispatch, inherited calls,
multi-level field layout, and destructor chaining were never proven to
run correctly — exactly the kind of gap where plausible-looking IR can
still produce wrong runtime behaviour.
Adds cp.test.e2e.inherit.pas with nine tests that compile + run on BOTH
the QBE and native x86-64 backends and assert on stdout:
- three-level virtual override reached through a base-typed variable
- inherited call inside an override
- constructor chain via inherited Create
- polymorphic dispatch through a base-typed array
- is / as operators with downcast
- virtual dispatch from inside a base constructor (template method)
- four-level field inheritance (offset accumulation)
- double dispatch (override -> inherited -> virtual)
- virtual destructor chain via inherited Destroy
All nine pass on both backends. No compiler source changed.
A chained subscript WRITE on a dynamic array of dynamic arrays —
m[i][j] := v — was rejected at semantic time with "Multi-dimensional
subscript base must be a static array". Only a static-array base was
accepted in the chained-assign path; reading via an intermediate row
variable (row := m[i]; row[j] := v) worked, but the direct write did
not, making jagged 2-D+ dynamic arrays awkward to fill.
Semantic: AnalyseStaticSubscriptAssign now accepts a tyDynArray base in
the BaseExpr (chained) path and derives the element type from
TDynArrayTypeDesc.
Codegen (both backends): when the chained-write base resolves to a
dynamic array, evaluate BaseExpr (m[i]) to the inner array's data
pointer and index into it — heap indirection per level — instead of the
inline-offset static-array path. QBE adds a BaseExpr branch in the
dynarray block of EmitStaticSubscriptAssign. Native stashes the base
pointer below the spilled value on the stack and reloads it at the
base-resolve step, with matching cleanup on every Exit path (float,
record, string/class, and scalar).
Verified 2-D int, 2-D string (with overwrite), and 3-D int nesting on
both backends. Adds three dual-backend e2e tests in
cp.test.e2e.dynarray.pas and a "Dynamic-array nesting (jagged arrays)"
subsection to docs/language-rationale.adoc.
A dynamic-array variable passed where an open-array parameter was
expected was rejected with "No matching overload", and even once the
semantic check accepted it the codegen emitted the wrong (data, high)
pair: it loaded a `<name>_high` slot that only exists for static-array
open-array temporaries.
Semantic pass: CheckTypesMatch and ArgMatchScore now treat tyDynArray
as assignment-compatible with a tyOpenArray parameter when the element
types match.
Codegen (both backends): when a tyDynArray argument is coerced to an
open-array parameter, pass the data pointer plus (runtime length - 1)
as the high index, where the length comes from _DynArrayLength at run
time rather than a compile-time static bound. QBE: OpenArrayArgFragment
gains a dynarray case (using distinct temps for the w-length and the
l-high computation to keep the IR well-typed). Native: the three
open-array argument emission paths (EmitMethodArgPush plus the two
call-site push/slot handlers) each gain a tyDynArray case.
Adds five dual-backend e2e tests in cp.test.e2e.openarray.pas covering
sum, High/Length, empty array, array-of-string, and pass-to-nested.
SetLength on an array element was rejected:
var m: array of array of Integer;
SetLength(m, 2); SetLength(m[0], 3); -> "First argument of 'SetLength'
must be an assignable variable"
This blocked ragged 2-D dynamic arrays (resizing each inner row). The
SetLength first-argument check accepted only a variable or field, not an
array-element l-value.
Semantic: the check now routes through the shared IsVarArgLValue helper, which
also accepts an array-element subscript (the type check already requires the
element to be a string or dynamic array). Codegen: QBE's EmitDynArraySetLength
already used EmitLValueAddr (extended for subscripts in the var-arg fix);
native's EmitLValueSlotAddr now handles a TStringSubscriptExpr element slot
(its address is what @m[i] computes).
Found by the e2e dynamic-array hardening sweep. New file
cp.test.e2e.dynarray.pas (8 tests, all run on BOTH backends): SetLength fill,
Length/High, grow-preserves, array-of-string, reference semantics, return by
value, ragged 2-D via SetLength(m[i], n), fill-inner-via-row.
(Two further dynarray gaps found and logged for follow-up: m[i][j] := v write
on dyn-of-dyn, and open-array param + dynarray arg compatibility.)
All three fixpoints + full suite (3283 tests) pass.
Adds four record e2e tests from the hardening sweep, all run on BOTH backends:
record method mutating Self, record-return-field used inline (Make(V).X),
record-with-static-array-field deep copy (y := x then y.A[i] must not alias x),
and a nested-record method call.
No compiler change — every record case probed (value-copy semantics, by-value
params, return-by-value, nested copy, string-field ARC, static-array-field
deep copy, methods) was already correct on both backends; this locks in the
proof.
Test-only; full suite 3275 tests green.
Adds five pointer e2e tests run on BOTH backends (AssertRunsOnAll), from the
hardening sweep's pointer probing: pointer-to-record-field write, pointer
parameter, pointer equality, pointer-to-array-element, and a GetMem-built
linked list traversal.
No compiler change — pointer codegen was already correct on both backends for
every case probed; this just locks in the proof. (Separately, New/Dispose are
not Blaise builtins — a language-design question logged in bugs.txt, not a bug.)
Test-only; full suite 3271 tests green. Compiler unchanged since 64160a0, so
the three fixpoints from that commit still hold.
Passing an array element to a var/out parameter was rejected:
procedure Swap(var A, B: Integer); ...
Swap(a[0], a[1]); -> "var argument 1 of 'Swap' must be a variable"
An array element is a legitimate addressable l-value (Swap(a[i], a[j]) is
idiomatic Pascal); only a plain variable, field access, or pointer deref were
accepted.
Semantic: the three general var-param l-value checks now route through a shared
IsVarArgLValue helper that also accepts a TStringSubscriptExpr over an array
base (static / dynamic / open array). String and PChar subscripts (a char by
value) are still rejected, as are literals and expressions. The stricter
Delete/SetLength first-argument checks are unchanged.
Codegen: EmitLValueAddr (QBE) and EmitVarArgAddrToRax (native) now handle a
TStringSubscriptExpr actual by reusing the existing element-address logic (via
a transient TAddrOfExpr), so the element's address is passed by reference.
Found by the e2e var/out parameter hardening sweep. New file
cp.test.e2e.varparams.pas (11 tests, all run on BOTH backends): var int/swap/
string, out params, var record, nested var call, var class field, and array
element (static/dynamic/string) as a var actual incl. Swap(a[0], a[1]).
All three fixpoints + full suite (3266 tests) pass.
Reading a static-array field of the current object via implicit Self inside a
method (FD[i], no explicit Self.) segfaulted on QBE:
type TC = class FD: array[0..4] of Integer;
function At(i: Integer): Integer; begin Result := FD[i] end; end;
The implicit-Self bare-field path in EmitExpr returned the field's storage
ADDRESS only for tyRecord; every other field kind was loaded as a value. A
static array lives inline in the object, so loading `FD` dereferenced the
array's first 8 bytes as a pointer; the subsequent FD[i] indexing then ran off
that garbage pointer. Native handled it; only QBE was broken.
Now the address is returned for any aggregate-address type (record, static
array, jumbo set) via IsAggregateAddrType, so `FD` yields &FD and FD[i]
indexes the inline array correctly. This also fixes indexed properties backed
by a static-array field (the common Items[I] pattern), which is how the bug
first surfaced.
Found by the e2e properties hardening sweep. New file
cp.test.e2e.properties.pas (11 tests, all run on BOTH backends): property
read/write, getter/setter, read-only, string, default value, inherited,
indexed; static-array field read/write/sum in a method; static-array-of-record
field.
All three fixpoints + full suite (3255 tests) pass.
Passing a class value to an interface-typed parameter (implicit
class->interface narrowing at the call site) produced a QBE link error,
referencing a nonexistent $Name_obj symbol:
function Describe(N: INamed): string; ...
Describe(t) // t: TThing implementing INamed
-> undefined reference to `t_obj'
The native backend handled it; only QBE was broken — the first
backend-divergence bug of the e2e hardening sweep.
EmitInterfaceExprPair's TIdentExpr branch assumed the source was
interface-typed (split obj/itab slots, addressed as Name_obj/Name_itab).
For a class-typed source there is just a single class pointer, so it emitted
a reference to the non-existent Name_obj. Now a class-typed expression
narrowed to an interface is handled directly: the value is the obj pointer,
paired with the static $itab_<Class>_<Interface> (mirroring the existing
class->interface assignment path). The target interface type is threaded
through InterfaceArgFragment/EmitInterfaceExprPair from the param type at the
call sites that lacked an inline class check.
Covers class var (global + local), constructor result, and procedure
interface params. Found by the e2e interfaces hardening sweep. New file
cp.test.e2e.interfaces.pas (8 tests, all run on BOTH backends).
All three fixpoints + full suite (3244 tests) pass.