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 runtime Makefile assembled each Pascal unit's object via `cc -c` (gcc as a
plain assembler). Pass `--assembler internal` so Blaise's own in-process
assembler produces the .o instead — removing gcc from all twelve Pascal RTL
unit builds.
The four hand-written .s files in src/main/asm still go through `cc -c` until
Blaise can assemble a standalone .s file (or the asm moves into inline `asm`
blocks); the Pascal units no longer touch gcc.
Verified: all four fixpoints (which rebuild + install the RTL via this rule)
pass, and both QBE- and native-built test runners pass (3758 tests).
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.
The bootstrap chain is native-default and needs no QBE, but the E2E test
suite still shells out to it (the QBE arm of AssertRunsOnAll plus the
toolchain-gated round-trip tests). The workflow never built QBE, so ~218
tests failed with <toolchain-missing> / [qbe] compile+run once the
native-default switch landed. Build the vendored QBE after the toolchain
step and point BLAISE_QBE at it.
- archive the NATIVE stage-2 binary (/tmp/fpn_blaise2), not the QBE one
- COMPILER_ID drops -SNAPSHOT at release, re-gains it at the dev-cycle bump;
keep base version synced with project.xml or bif-coverage fails
- add fixpoint-warmcache.sh (WARMCACHE_FIXPOINT_OK) to the verification set
- rename the -pre bootstrap dir to the next cycle (not refresh-in-place)
- define the changelog/community-post range as <prev-tag>..v<X.Y.Z>
- note the CI STAGE1_TAG / rolling-bootstrap anchor bump
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).
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.
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.