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.
v0.12.0 is the clean rolling-bootstrap anchor — the broken v0.11.0->v0.12.0
commit range sits before it, so the replay range (v0.12.0..HEAD) is entirely
native-default with the internal assembler + linker and needs no external tools.
scripts/rolling-bootstrap.sh:
- build_step / smoke_test now compile straight to an executable with the native
default backend (`--source ... --output ...`): no --emit-ir, no QBE, no gcc.
- Dropped the QBE_BIN requirement and the QBE= make var from the RTL build.
.github/workflows/bootstrap.yml:
- STAGE1_TAG v0.10.0 -> v0.12.0 (the new cold-start anchor).
- Removed the "Build QBE" and "Expose QBE to the compiler" steps and the QBE=
make var from the -pre RTL rebuild — the bootstrap chain no longer needs them.
- Branch-scoped validate-only triggers (ci/**, feature/**, bootstrap/**) and
master-only cache writes were already in place (Step 3d-bis).
These reference the v0.12.0 tag/release asset, which the cut creates next.
Language improvements + RTL/StdLib expansion + bug fixing is continuous work,
not a phase that completes — "Ongoing" reflects that better than "In-Progress".
Phase 7 was "Native backend feature parity + Windows + macOS ARM64
(In-Progress)". Native is now the default backend with a working internal
assembler and linker, so:
- Phase 7 is now "Native backend feature parity" — Complete.
- New Phase 8 "Windows + macOS ARM64 targets" — Planned.
- Former phases 8/9 (LSP + VS Code, migration analyser) renumbered to 9/10 so
the sequence is contiguous.
README.adoc:
- Native x86-64 is the default backend since v0.12.0; QBE is opt-in
(--backend qbe). Reframed the intro, project status, bootstrap, and
single-file compile examples accordingly (native one-step build leads; QBE
shown as the opt-in multi-step path).
- Test count 3474 -> 3800+ (3744 compiler + 57 stdlib).
- Added a Standard library status line (generics collections, JSON, SHA-1,
Base64, GUIDs, sockets, WebSockets, HTTP server, blaise.testing).
- Bootstrap example resolves the newest releases/v* binary instead of the
stale v0.7.0 path, and uses the native --output build.
- OPDF now debugs incrementally-compiled multi-unit programs.
grammar.ebnf (CLAUDE.md sync rule):
- TypeName accepts a unit-qualified, possibly-dotted QualIdent
(UnitName.TypeName, System.SysUtils.TFormatSettings).
- FieldAssignment l-value extended with a Selector chain
(.Field / [Index] / .Method(...)) so Self.F[i].Sub := value parses.
language-rationale.adoc:
- New entries "Qualified type names" and "Chained l-value assignment"
recording the decision, reasoning, and alternatives for the two
conservative FPC/Delphi-compatible parser extensions made this cycle.
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.
StrUtils had JoinStr (open-array join) but no splitter at all — a gap any
text-processing code hits. Add SplitChar (split on a delimiter byte),
SplitLines (split on LF, tolerating CRLF), and JoinList (the list-valued
inverse of SplitChar), all returning/taking TList<String>. This pulls
Generics.Collections into StrUtils' interface. Lifted from the Luhmann web
server. Adds 10 tests (57 stdlib tests total).
TASTClass.Fields was a TStringList storing field names in the string slot and
source line numbers in Objects[] behind Pointer(PtrUInt(line)) /
Integer(PtrUInt(...)) casts. Split it into Fields: TList<String> +
FieldLines: TList<Integer> (the parallel-list idiom already used elsewhere in
this tool), removing both unsafe casts. Behaviour unchanged: coverage run
reports OK, 71 AST classes scanned.
GRegistry was a TStringList abused as a list of metaclass pointers — the
string slot was always '' (vestigial) and the class reference lived in
Objects[] behind Pointer()/TTestCaseClass() casts. Now that generics support
metaclass type arguments (alias canonicalisation + space-safe label
mangling), it becomes a plain TList<TTestCaseClass>: RegisterTest just Adds
the class, GetRegisteredTest just Gets it — no dead string, no casts.
47 stdlib + 3744 compiler tests pass (both suites enumerate the registry to
run, so this exercises the metaclass-generic path end to end).
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).
Process arguments are only ever appended and iterated (Add/Get/Count) — none
of the TStringList-specific API is used by any consumer, so TList<string> is
behaviour-identical and expresses 'an ordered list of argument strings' more
honestly. Every call site uses Parameters.Add(...), unchanged. Lets the unit
drop its Classes dependency for Generics.Collections. All consumers (the
codegen/linker driver, the test runner, the compiler test harnesses) compile
and pass unchanged: 3743 compiler + 47 stdlib tests.
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.
The server only used Create/Add/Get/Count/Free on its string lists — none of
the TStringList-specific API — so TList<string> is behaviour-identical and lets
the unit drop its Classes dependency.
Explain why the unit binds libc (the kernel boundary, the syscall doorway), the
common three-layer pattern shared with Indy/Synapse/the JDK, how Java is itself
not libc-free, and what a pure-Pascal syscall-stub version would and would not
buy.
Document that the socket constants/sockaddr_in layout (Net.Sockets) and the
getrandom(2) call (Security.Guid) are Linux x86_64 specifics, with the per-OS
differences and the fact that the public helper API is the stable surface a
future port keeps.
NewGuid returns a canonical lowercase v4 GUID; NewGuidRaw returns the 16 raw
bytes. Randomness comes from the kernel CSPRNG via getrandom(2), with the
version (4) and variant (RFC 4122) bits forced. Tests cover the canonical
format, version/variant bits in both string and raw forms, and uniqueness
(47 tests total).
Single-threaded HTTP/1.1 server lifted from the Luhmann web server and
generalised: THttpRequest/THttpResponse/IRequestHandler/THttpServer plus
ParseRequest/UrlDecode. WebSocket upgrades are handled via Net.WebSockets and
Broadcast(payload) pushes a text frame to every connected socket (the
reload-specific BroadcastReload became a generic Broadcast). Built on
Net.Sockets. Private split helpers are prefixed to avoid colliding with a
consumer's same-named functions. Tests cover parsing, URL decoding, a loopback
request round-trip, and a WebSocket upgrade + broadcast (43 tests total).
Two small, independent units, split along the lines Java and .NET draw:
Security.Crypto SHA-1 (raw 20-byte digest + Sha1Hex), the home for hash
digests (cf. java.security / System.Security.Cryptography).
Encoding.Base64 Base64 encode + decode (RFC 4648), a general text encoding,
not crypto (cf. java.util.Base64 / System.Convert).
Callers compose them, e.g. the WebSocket handshake is
Base64Encode(Sha1(key + GUID)). Both TStringBuilder-backed (O(n)).
Adds Crypto.Tests (SHA-1 FIPS vectors + the RFC 6455 handshake) and
Base64.Tests (RFC 4648 vectors, decode, round-trip) to the stdlib test tree
via Test.Registry; pasbuild test -m blaise-stdlib runs 27 tests.
A read+write JSON library for the standard library:
Json.Writer streaming serialiser — TStringBuilder-backed (O(n)), with a
three-tier .NET-style API: WriteString(name,value) for object
fields, WriteStringValue(value) for array elements, and WriteKey
for nested-container values. Compact + pretty output. Standalone
(no DOM dependency).
Json.Types in-memory document model (DOM): TJSONData + typed nodes, tree
building, accessors; emits through Json.Writer.
Json.Parser RFC 8259 recursive-descent parser into the DOM, with full string
escapes incl. \uXXXX surrogate pairs.
Json.Reader thin GetJSON(text) facade.
Adds the first stdlib test tree under stdlib/src/test/pascal: a self-registering
Json.Tests suite (19 tests), a Test.Registry hub unit, and a TestRunner program,
wired into 'pasbuild test -m blaise-stdlib' via the <test> block in project.xml.
run-tests.sh builds and runs the suite directly.
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.
None of the existing fixpoint scripts exercise the incremental warm-cache
rebuild path: fixpoint.sh uses --emit-ir (no cache), fixpoint-native.sh diffs
.s (no cache), fixpoint-native-internal.sh compiles a tiny program. So a
regression in the cached-interface import/round-trip (dropped field, wrong
vtable slot, duplicate global, missing init) passes all of them and only
surfaces later as a corrupt binary — exactly the class of bug just fixed.
scripts/fixpoint-warmcache.sh builds the whole compiler twice into the same
--unit-cache (clean then warm), asserts the warm rebuild is non-empty and not
drastically smaller than the clean build, and compiles+runs hello-world with
both stage-2 binaries to prove the warm-cache-rebuilt compiler is correct, not
just present. Prints WARMCACHE_FIXPOINT_OK on success.
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).