First increment of the libc-free direct-syscall RTL (Linux as the proving
ground for FreeBSD; docs/linux-syscall-migration.adoc to follow). The default
build is unchanged — libc-backed dynamic ELF; everything here is behind the new
--static flag.
New kernel-leaf units (selected by EnsureRTLObjects when --static):
- runtime.syscall.linux — raw `syscall` stubs DEFINING the bare POSIX names the
RTL imports (open/read/write/lseek/close/fstat/stat/mkdir/rmdir/unlink/rename/
chdir/getpid/dup2/pipe/mmap/munmap/nanosleep/clock_gettime/exit/_exit).
`write`/`exit` are Pascal-reserved, so their Pascal names are _sys_* and the
asm body emits a bare label alias. 4th syscall arg moved %rcx->%r10.
- runtime.cstub — freestanding memcpy/memset/memcmp/strlen (no Char type; PChar
byte indexing).
- runtime.start.static.linux — freestanding _start (argc/argv off %rsp, call
main, exit_group) replacing the libc __libc_start_main entry.
Wiring: --static -> TBackendOpts.Static; EnsureRTLObjects swaps runtime.start
for runtime.start.static.linux and adds the syscall+cstub leaves; the native
internal linker calls SetDynamic(False) for a non-PIE ET_EXEC.
Internal linker static-mode fixes (these are correct in any ET_EXEC, not just
PIE — they were wrongly gated to dynamic-only):
- R_X86_64_64 against a defined symbol resolves to the symbol's absolute address
(no dynamic reloc); only PIE/dynamic mode also emits the .rela.dyn RELATIVE.
- R_X86_64_TPOFF32 (Local-Exec TLS) resolves at link time — it IS the static TLS
model. Updated TestReloc_Quad64_* to assert the now-correct resolution.
A --static link currently reaches symbol resolution and reports the remaining
libc leaves to implement (mremap/getcwd/fork/execvp/waitpid/time/getenv/sysconf/
gmtime_r/localtime_r/timegm/__cxa_atexit/abort/mkstemp/system/pthread_mutex_*) —
the next increments. Full suite green (3829); dynamic native/QBE unaffected.
A record passed BY VALUE to a function (e.g. `const AParsed: TParsedLine`)
lowers to a QBE `:_ffi_<Name>` aggregate type whose field list was built by
FlattenRecordToAggLetters inlining each field's type letter back-to-back, with
nested records expanded letter-by-letter. That dropped the per-field and nested-
record tail padding, so QBE laid out the struct SMALLER than Blaise's record and
with shifted field offsets.
For TParsedLine this put the trailing HasLock byte at QBE offset 244 (struct size
248) while the function body reads it at the real Pascal offset 252 (size 256).
A by-value read at +252 then lands 4 bytes PAST the passed struct, in caller
stack garbage. When that garbage was non-zero, the assembler's
`if AParsed.HasLock then CBEmit($F0)` fired on EVERY instruction, prepending a
spurious lock prefix — `lock ret` is illegal, so the produced binary SIGILL'd.
This silently corrupted large native binaries built by a self-hosted compiler
(the assembler unit is where it bit hardest), and it was the real cause of the
"--static binaries are poisoned" and "stage-2 TestRunner SIGILLs" symptoms.
It hid from all four fixpoints: the QBE fixpoint compares IR text (the IR was
always correct — `loadub +252` is right; QBE miscompiled the by-value param
layout), and the native fixpoints pipe through the EXTERNAL assembler.
Fix: FlattenRecordToAggLetters now walks fields by their real TFieldInfo.Offset,
inserting explicit `b` byte padding before each field and after the last one up
to TotalSize, so the QBE aggregate's field offsets and size match the Pascal
record exactly. Nested records / static arrays / sets are emitted as their whole
byte span (the by-value ABI only needs correct size and field positions).
Verified: QBE-built compiler now produces 0 spurious lock prefixes; static
threads binary runs (counter=80000 PASS); compiler/target/blaise self-compiles
clean; FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK.
Calling a record-returning method as a statement (result discarded) omitted
the hidden sret pointer required by the SysV x86-64 ABI. Self was passed
in %rdi where the sret buffer should be, so the callee wrote the returned
record through the object pointer, corrupting its fields.
Semantic pass: set ResolvedReturnTypeDesc on TMethodCallStmt at all three
class-method resolution sites so both backends can detect the record return.
QBE backend: allocate a temporary buffer via alloc8, zero it, route through
EmitRecordReturnCallSite, then release any managed fields (strings, classes,
interfaces, dynamic arrays). Both the regular and ObjExpr receiver paths.
Native backend: reserve an aligned stack buffer, shift Self from %rdi to
%rsi, place the sret pointer in %rdi, and clean up managed fields after the
call. Handles the <=6-slot path.
A `#`-prefixed numeric literal now denotes a Unicode codepoint and contributes
its UTF-8 encoding to the surrounding string constant — the coherent semantics
for Blaise's Char-less, UTF-8-native string type:
#65 -> 'A' (1 byte $41)
#$20AC -> the euro sign (3 bytes E2 82 AC)
#$1F600 -> the grinning face(4 bytes F0 9F 98 80)
The literal is always a string, and adjacent `#` and '...' literals merge into
one compile-time constant (#72#73'!' -> 'HI!'), matching Pascal's literal-
merging tradition. Decimal (#nnnn) and hexadecimal (#$hhhh) are both accepted;
the underlying tokeniser already captured #$-hex runs, so the change is confined
to UnescapeString + a new CodepointToUtf8.
Codepoints outside 0..U+10FFFF and the surrogate range U+D800..U+DFFF (not valid
Unicode scalar values) are a compile error, so emitted string constants are
always valid UTF-8.
Tests cover decimal ASCII, hex BMP (3-byte), astral (4-byte), and #+string
merging. All four fixpoints green; full suite 3835.
After the RTL-unification move, stdlib units (classes, contnrs) explicitly
`uses runtime.arc` and friends. The compiler driver already source-builds the
RTL from compiler/src/main/pascal at link time, but the FRONT-END unit loader
was never told where that source lives — so compiling any program that uses a
stdlib unit (or any RTL unit transitively) failed with
"Unit 'runtime.arc' not found in search paths" unless the user manually passed
--unit-path compiler/src/main/pascal.
Add the RTL source directory to the loader's search paths automatically,
resolved the same way EnsureRTLObjects resolves it for linking: --rtl-src, then
$BLAISE_RTL_SRC, then the binary/CWD-relative RTLSourceDir default. (stdlib
itself stays an explicit --unit-path, as before — it is an opt-in library, not
implicit like the RTL.)
Regression introduced by the RTL-unification land; reported by a user.
Verified: a `uses classes` program now compiles + runs with only the stdlib
unit-path, from any CWD. All four fixpoints green; full suite 3831.
An implicit-Self method call — a bare `Method(...)` inside another method that
resolves to a method of the current class — loaded Self plus every argument
into the System V integer argument registers unconditionally. When Self and the
arguments together needed more than the six available integer registers,
codegen aborted with "System V integer arg register index 6 out of range".
The explicit-receiver path (obj.Method() / Self.Method()) already split on slot
count and spilled the overflow to the stack; only the implicit-Self expression
path was missing it. Add the same split: keep the push/pop-into-registers fast
path for <= 6 slots, and for more, evaluate the arguments into a stack slot
block, load the first six slots into registers, and spill the rest per the ABI
(EmitImplicitSelfCallOverflow, mirroring EmitMethodCallExpr's overflow branch).
Tests: an assembly-level test asserts the slot-block spill shape and dispatch,
and an end-to-end test runs a program with an 8-argument non-virtual and a
7-argument virtual implicit-Self call and verifies every argument — including
the spilled ones — arrives intact.
Partial Stage-3 teardown — the safe removals that do not affect the bootstrap
chain (the runtime Makefile/ar stay until a release ships a source-build-capable
binary, since the v0.12.0-rooted rolling-bootstrap replay needs them for its
pre-source-build commits):
- Remove FindRTLArchive + TToolchain.RTLPath from uToolchain — dead since the
link paths source-build the RTL; the field was set but never read.
- TInternalLinkerE2ETests gated on blaise_rtl.a existing, so it silently skipped
once the archive was gone; gate on the RTL source instead. It now runs (5
tests) — the last automated test still tied to the archive.
- Refresh the runtime mem test/bench build-instruction comments to the
source-built, archive-free invocation (blaise --output, no qbe+gcc+.a).
Full suite green (3829 tests, 1 ignored) — TInternalLinkerE2ETests no longer
skipped.
The rolling-bootstrap smoke probe ran `$cc --source s.pas --output s` with no
unit-path, relying on the binary finding the RTL beside itself. That worked
while the RTL was an archive (FindRTL looks beside the binary), but once a
commit makes the compiler source-build the RTL, the boot/carried binary is not
beside its compiler/ source tree, so RTLSourceDir (binary-relative, then
CWD-relative) misses and the probe fails with "RTL source directory not found".
Point the probe at the worktree's RTL source via $BLAISE_RTL_SRC and the unit
path. Backward-compatible: pre-source-build commits ignore $BLAISE_RTL_SRC and
still link the archive copied beside the binary; the extra unit paths are
harmless for a program that uses no units.
The E2E test helpers linked Blaise programs against the shipped blaise_rtl.a.
With the archive on its way out (RTL-unification Stage 3), migrate them to the
source-built RTL:
- cp.test.e2e.base gains LinkWithRTL: assembles the program object, builds the
RTL from source via scripts/build-rtl-objects.sh (--exclude-defined-by drops
the RTL units the whole-program assembly already inlined), and links the loose
objects. Its five cc-link sites and the ToolchainAvailable gate now use it /
the RTL source instead of the archive.
- New cp.test.rtllink unit factors the same link helper for the standalone
helpers (publishedrtti, proctypes_ofobject, attributes) that lower programs
themselves rather than via the base class.
- cp.test.cli and cp.test.assembler drive an external blaise binary that
source-builds its own RTL, so their gates now require the binary + RTL source,
not the archive.
- cp.test.linker's TestArchive_ParsesRTL kept its archive-parser coverage
(including the GNU long-name member) by building a throwaway .a from the
source-built objects with ar — a test-time-only ar use, not a product one.
build-rtl-objects.sh resolves its RTL source relative to its own location (so it
works from any CWD — the fixpoint scripts run from the project root, the test
runner from compiler/), caches a per-object .syms list to keep repeated
--exclude-defined-by calls cheap, and skips rebuilding fresh objects.
Full suite green (3829 tests) on both QBE- and native-built runners; the
previously-skipped TInternalAsmE2ETests / TCLIContractTests and the archive
parser test now run.
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.