Builds on the source-built-RTL link path. Two correctness fixes plus the
fixpoint-script migration that takes the self-host pipeline off blaise_rtl.a.
1. Symbol-based dedup. A program that uses an RTL unit (e.g. `uses classes`
pulls runtime.arc) compiles that unit into its own object cache, so
EnsureRTLObjects must not supply a second copy or the link double-defines it.
The dedup now compares the DEFINED symbols of the caller's objects against
each RTL object (via the ELF reader) and skips one only when fully redundant
— matching the old archive's member selection. An earlier filename-only
check wrongly suppressed the real bare-symbol provider when a same-named but
differently-mangled stale object was present.
2. Race-free build. EnsureRTLObjects now builds into a per-process private
directory (rtl/build-<pid>/) and atomically renames each finished object
into the shared cache. Parallel builders (e.g. e2e suites warming a cold
cache) no longer corrupt each other: a reader never sees a half-written
object, and two builders just publish identical results. The private dir is
cleaned up afterwards. getpid/rmdir are declared directly rather than via
GRtlPlatform to avoid a unit-init-order crash in the driver.
3. Fixpoint scripts off the archive. New scripts/build-rtl-objects.sh builds
the RTL from source (the script-level equivalent of EnsureRTLObjects), with
--exclude-defined-by so a whole-program --emit-ir/--emit-asm dump (which
inlines the RTL units the compiler uses) is not handed a duplicate copy.
fixpoint.sh and fixpoint-native.sh link via this helper instead of
blaise_rtl.a; fixpoint-native-internal.sh and fixpoint-warmcache.sh no longer
copy the archive beside the compiler (the driver source-builds the RTL).
All four fixpoints green (FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK,
WARMCACHE_FIXPOINT_OK); full suite green (3829 tests) under QBE- and
native-built runners; four cold-cache parallel suite runs pass.
The runtime Makefile / ar / make install are still present; removing them is the
final Stage-3 step.
The leak tracker registered its end-of-run report via glibc's bare `atexit`.
That symbol lives only in the static libc_nonshared.a; it is NOT a dynamic
export of libc.so.6. The QBE/cc path linked fine because gcc auto-pulls
libc_nonshared.a, but the native backend's linkers (internal ELF linker and
the native external-assembler cc invocation) do not, so a --debug build aborted
at load time with `symbol lookup error: undefined symbol: atexit`.
Register via __cxa_atexit instead — a genuine dynamic libc export that resolves
on every link path. Its third argument is the DSO handle; nil registers the
handler against the main program. The handler takes one (ignored) argument.
Pre-existing bug (reproduces on v0.12.0); affects only --debug builds compiled
with --backend native. Verified on all three link paths (QBE/cc, native
internal linker, native external assembler) plus the full suite and all four
fixpoints.
Stage 3 of the RTL unification: the cc/QBE link line now source-builds the
RTL the same way the native internal linker already did, via a single shared
TBackendDriver.EnsureRTLObjects. Both paths compile the implicit RTL units
(runtime.* + rtl.platform.*) into a per-compiler object cache and link those
.o files directly, instead of linking the pre-built blaise_rtl.a. Verified
by linking a program with the archive moved aside on all three paths
(qbe+cc, native internal linker, native external assembler).
Moving from an archive to explicit .o files on the link line exposed two
pre-existing separate-compilation bugs that the archive's member selection
had masked (an archive member is only pulled to satisfy an undefined symbol,
so duplicate definitions across members never collide):
1. A unit compiled standalone via --source never had SourceFile set (only
loader-loaded dependency units did), so its exported interface carried an
empty SourceHash. A later compile depending on that unit could not
validate the cached .o and silently recompiled it from source, re-inlining
the dependency body. Set TopUnit.SourceFile from the --source path.
2. In unit-as-top-level mode the compiler never told the backend which
dependencies were imported (only the program path called NoteDepInitUnit),
so the backend re-defined imported units' globals (e.g. GPlatformLayout,
owned by rtl.platform) instead of referencing them externally. Mirror the
program path: note prebuilt-iface deps and SkipDepCodegen source deps so
their globals stay external.
EnsureRTLObjects builds the RTL leaf-first through a --unit-cache (so each
unit references its deps' globals externally and intermediate deps such as
rtl.platform are built once) using the native backend + internal assembler
(RTL units carry inline asm). An AIncludeStartup flag controls runtime.start:
the native internal linker includes its bare _start (Blaise owns the entry),
the cc link line omits it (libc's startup provides _start and calls main).
The transitional blaise_rtl.a is still produced by the runtime Makefile and
still consumed by the fixpoint scripts and the driver-bypassing e2e test
helpers; removing those is the remaining Stage 3 work.
All four fixpoints pass (FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK,
WARMCACHE_FIXPOINT_OK); full suite green (3829 tests) under both the QBE-built
and native-built test runner.
RTL-unification Stage 2 (docs/rtl-unification-plan.adoc): the native internal
linker no longer links a pre-built blaise_rtl.a. Instead it compiles the
implicit RTL units (runtime.* + rtl.platform.*) from source into an object cache
beside the compiler (compiler/target/rtl/*.o) and links those, recompiling a
unit only when its cached .o is missing or older than the source.
- EnsureRTLObjects (native driver) drives the per-unit compile via self-invoke
(blaise --no-incremental --assembler internal --source U.pas), the proven
per-unit path; LinkViaInternalLinker adds the cached .o via AddOwnedObject
instead of AddArchive.
- uToolchain.RTLSourceDir resolves the RTL source robustly: binary-relative
(compiler/target -> ../src/main/pascal), then CWD-relative (handles a binary
copied to /tmp but invoked from the project root, as the fixpoints do).
- New --rtl-src DIR flag (TBackendOpts.RTLSrcDir) + $BLAISE_RTL_SRC for a
relocated/release binary; the driver fails loudly when the RTL source is
unreachable rather than emitting a broken binary.
Performance: warm cache adds ~0ms (RTL skipped); a cold full RTL recompile is
~0.4s. Scope: this changes the NATIVE internal-linker path only — the QBE
backend and cc-link paths still use blaise_rtl.a, so the archive remains a build
artifact (the fixpoint scripts and QBE-based tests still build it).
TestLink_MissingRTL_FailsLoudly rewritten for the new contract (--rtl-src at a
nonexistent dir must fail naming the RTL source). All four fixpoints OK; full
suite 3829 green; cold-bootstrap from the existing release works.
Execution plan for folding the runtime into the compiler: single
pasbuild compile -m blaise-compiler, no separate runtime module, no
make install, no ar-built blaise_rtl.a.
Records the constraining facts (programs emit RTL calls as undefined externals
satisfied by the archive; the archive is a Blaise-only ar artifact pasbuild
does not produce; RTL is now pure Pascal), the chosen target shape (RTL units
relocated into the compiler source tree, compiler produces a cached rtl/*.o dir
the internal linker consumes), and the full blast radius — project.xml,
uToolchain/FindRTLArchive, native driver AddArchive, all four fixpoint scripts,
rolling-bootstrap, CI bootstrap.yml, ~12 tests, release artifacts, and docs.
Recommends DOTTED-FLAT unit names (runtime.atomic, runtime.str, …) over a
subdirectory: the unit loader is filename-based (dotted name -> dotted file, no
subdir traversal), so dotted-flat keeps the units on the compiler's existing
unit-path with zero new path entry, at the cost of a one-time uses-clause
rename sweep; emitted runtime symbols are unchanged either way.
Sequences the migration archive-compatible-first (old release keeps cold-
bootstrapping) and only drops the archive once a stage-2 binary produces/
consumes the .o cache, with a full four-fixpoint + cold-bootstrap re-verify
after each stage and a build-time measurement gate before keeping the
compile-per-target follow-up.
blaise_atomic exported a PInteger = ^Integer type that collides with the same
alias in blaise_arc when the RTL units are compiled together into one program
(the pasbuild library / whole-program path), giving 'Duplicate type name
PInteger'. The per-unit Makefile build never saw it. Type the params as
Pointer (the asm body reads through %rdi regardless); the external-name callers
in blaise_arc are unaffected (link is by symbol). Prerequisite for compiling
the RTL units together (RTL-unification).
Records the runtime end-state: blaise embeds the RTL Pascal source once and
compiles it per --target on demand (selecting that target's TPlatformLayout +
kernel-stub adapter set), linking in-process — so a single binary cross-compiles
to any registered target with no blaise_rtl.a, no ar, no make install, and no
per-target .a files. The archive is documented as the transitional mechanism.
Adds an RTL-unification track to the migration sequence (gated on the .s→inline-
asm migration, which is in progress: blaise_atomic + blaise_setjmp done,
blaise_start + blaise_utf8 remaining). Compile-per-target adopted first;
in-memory/unit-cache RTL object cache deferred until build time is measured.
Reconciles the 'two selection moments' and FreeBSD Step 5 with the no-archive
end-state.
LinkViaInternalLinker always built the linker with the default (Linux)
TLinkTarget, so --target freebsd-x86_64 still emitted a SYSV/Linux ELF. Resolve
the target's toolkit and build TLinker with its MakeLinkTarget, so the emitted
ELF carries the right EI_OSABI / machine / load base for the requested target.
--target freebsd-x86_64 now stamps EI_OSABI = 9 (FreeBSD); the default Linux
target is unchanged (EI_OSABI = 0, still compiles+runs). The FreeBSD binary is
not yet runnable (Strategy-B syscall _start stub lands later) — this is the
OSABI/link-target wiring (Step 1 of the FreeBSD design).
Adds TLinkerE2ETests.TestLink_FreeBSDTarget_StampsOSABI locking in that the
FreeBSD link target produces a static ET_EXEC with EI_OSABI=9.
All four fixpoints OK; full suite 3813 green.
Records the Step 0c outcome: a runtime TKernelABI virtual port (30 abstract
methods + a GKernel global, routing all rtl.platform.posix kernel calls through
it with a libc-delegating Linux adapter) was implemented and reverted. It
destabilised self-hosting — binaries built against the modified RTL segfaulted
at startup with an unwindable/overflowed stack, and the failure compounded
across bootstrap stages rather than converging. The Linux adapter was pure libc
delegation, so it carried real self-hosting risk for zero present benefit.
Replacement design: the kernel leaf is a LINK-TIME symbol swap (the pattern
already used for blaise_setjmp/atomic .s objects). The RTL keeps its
'external name' bindings; the FreeBSD archive links a raw-syscall stub object
exporting the same symbols. No runtime virtual dispatch, no new global, no
change to the layout-sensitive RTL units. The kernel mechanism is a build-time
property of a target (libc-linked vs freestanding, never switched at runtime),
so a link-time swap models it exactly while a runtime port does not.
Both docs updated throughout: ports table, units-that-change table, migration
sequence (0a/0b done, kernel-leaf revised, ARC-walker lift deferred as arm64
groundwork), toolkit sketch, summaries. Adds a [#kernel-leaf] rationale note.
Move the OS integer constants (O_*/S_*/SEEK_*/CLOCK_REALTIME/WNOHANG) and the
struct stat layout out of rtl.platform.posix and behind a new TPlatformLayout
port. struct stat is now an opaque sized buffer whose Size/Mtime/Mode fields are
read at target offsets via StatSize/StatMtime/StatMode, rather than a record
that bakes in one layout — FreeBSD's stat differs in both offsets and size.
The Linux adapter (rtl.platform.layout.linux, TPlatformLayoutLinuxX86_64) supplies
today's values verbatim; struct stat offsets verified against sys/stat.h
(st_mode=24, st_size=48, st_mtim=88, sizeof=144). struct tm is left shared — its
used fields are identical on Linux/FreeBSD amd64.
The POSIX unit's one per-target wire is its 'uses rtl.platform.layout.linux';
the FreeBSD RTL archive will compose its own layout. No conditional compilation.
Step 0b of docs/native-target-architecture.adoc. Behaviour-preserving:
FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK, WARMCACHE_FIXPOINT_OK;
full suite 3761 tests green; file I/O (FileExists/DirectoryExists/FileAge/
ReadFile) exercised end-to-end through the new accessors.
Introduce TTargetToolkit (Abstract Factory) and TTargetRegistry so per-target
compiler-side objects (native backend + ELF link target) are resolved by name
rather than hard-coded case statements. CreateNativeBackend now delegates to
the registry; adding a target is a new toolkit + one registration with no edits
to existing dispatch.
Registers linux-x86_64 (unchanged behaviour) and freebsd-x86_64 (link target
with EI_OSABI=9; reuses TX86_64Backend since FreeBSD shares the System V AMD64
ABI). FreeBSD codegen/runtime lands in later steps.
The registry uses TOrderedDictionary<string, TTargetToolkit> (name -> toolkit)
per the Generics.Collections policy. It is built publish-after-populate: the
--incremental compile worker pool is threads, so a half-built global would let
a worker wrongly conclude a target has no backend.
Step 0a of docs/native-target-architecture.adoc. Behaviour-preserving:
FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK, WARMCACHE_FIXPOINT_OK;
full suite 3761 tests green (QBE- and native-built runners).
Design for a freebsd-x86_64 native target via direct syscalls (Strategy B,
static ET_EXEC, no libc/sysroot), and the general ports-and-adapters
architecture (TTargetToolkit Abstract Factory + TTargetRegistry, TKernelABI
and TPlatformLayout ports) that lets multiple targets coexist with no IFDEFs
and target divergence selected at runtime.
In default incremental mode each used unit is compiled to its own .o/.bif
by a worker thread. The worker's output directory was derived as
IncludeTrailingPathDelimiter(ExtractFilePath(OutputFile)). When --output is
a bare filename with no directory part (e.g. `--output mcd`), ExtractFilePath
returns '' and IncludeTrailingPathDelimiter('') yields '/', so the worker
tried to write `/<unit>.o.bif.tmp` at the filesystem root and aborted with
`Worker exception: Cannot open file for writing: /<unit>.o.bif.tmp`.
Resolve the artefact directory once before the worker loop, treating an empty
directory (bare or omitted --output, no --unit-cache) as the current
directory instead of root. Priority is unchanged: --unit-cache, then the
--output directory, then CWD.
Add a CLI regression test that compiles a program using a separate unit with
a bare --output name and asserts no root-path worker failure and a clean
exit.
The mandatory-parentheses rule ("every function, procedure, method, and
constructor call requires parentheses, even with no arguments") made no
exception for 'inherited'. A bare 'inherited Create' is a method call and
should be rejected like any other parenless call, but the parser silently
accepted it.
Enforce the rule in both 'inherited' parse paths (statement and expression
position): a bare 'inherited Method' now raises the same "requires () for a
call" diagnostic as any other parenless call.
Also fix a misleading semantic error for the metaclass-variable case. A bare
'C.Create' on a 'class of T' variable parses as a field access, which reached
the "'C' is not a record or class" guard — confusing, since C is a valid
metaclass variable. AnalyseFieldAccess now detects a constructor/method name
on a metaclass receiver and emits the "requires () for a call" diagnostic
instead. (The parenthesised 'C.Create()' already worked: it parses as a
TMethodCallExpr, which has the metaclass-dispatch branch.)
Migrate every bare 'inherited Method;' call site in stdlib, the bif-coverage
tool, and embedded e2e test programs to the parenthesised form. The
parenthesised form compiles on the prior compiler too, so the migration and the
enforcement land together without breaking the rolling-bootstrap chain.
Docs: update language-rationale.adoc (inherited section + mandatory-parens
section) and grammar.ebnf (expression-position inherited rule).
Tests: add TestSemantic_MetaclassVar_BareCreate_RequiresParens and
TestParse_Inherited_Bare{Stmt,Expr}_RequiresParens.
The unit loader treated any back-edge into a unit still on the load stack
as a circular dependency, rejecting a legal Object Pascal arrangement: a
unit whose implementation uses a unit whose interface (transitively) uses
it back. That is not a real cycle — interfaces are compiled before bodies,
so the back-referenced interface is available by the time the body needs it.
Split cycle detection into two stacks. FLoading still guards re-entry /
infinite recursion across every edge. FIfaceChain tracks only the units
reached along an unbroken chain of interface-section uses; a back-edge is a
true circular dependency exactly when it closes that chain. Implementation-
section uses are traversed with a fresh interface chain, so a back-edge
reached through one is tolerated (the unit is already loading higher up the
stack) instead of raising. An interface↔interface cycle still errors.
Test: a two-unit fixture where BackA's interface uses BackB and BackB's
implementation uses BackA now loads without ECircularDependency, while the
existing interface↔interface DetectsCycle test still raises.
A store into a Double or Single class field never converted an integer
right-hand side to floating point, on either backend:
* QBE never emitted swtof/sltof — the field-store paths only handled
integer widening (w->l) and float-width adjustment (s<->d). An Int64
or Integer RHS was deposited into the float slot as raw integer bits;
on the explicit-field path QBE rejected the `stored` of an integer SSA
value, and on the implicit-Self path a hardcoded `storel` silently
wrote garbage.
* The native x86-64 backend converted an integer RHS to a DOUBLE in
%xmm0 (cvtsi2sdq) but then stored it into a Single field with a bare
movss — writing only the low 32 bits of the double, i.e. garbage. A
Single field needs the value narrowed (cvtsd2ss) first; the Double
field happened to work.
Both backends now promote/convert to the field's width. QBE: swtof/sltof
for an integer RHS plus exts/truncd for a float-width mismatch, in
EmitFieldAssignment and the implicit-Self field branch of EmitAssignment.
Native: an EmitXmm0WidthAdjust after EmitExprToXmm0 in both the
TFieldAssignment and implicit-Self float field-store paths, narrowing a
Double/integer RHS to the Single field width before the movss.
Tests: cp.test.codegen IR assertions for the Self.Field, bare-field and
Single targets; cp.test.e2e.classes2 end-to-end coverage of Int64->Double
and Integer->Single through both Self.Field and bare implicit-Self forms,
verified on QBE and native.
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 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.
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.
- 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