The last Step-4 slice: real POSIX threads for a static FreeBSD build,
plus the TLS setup Step 3 deferred (threads need per-thread %fs TLS, and
the main thread needs it too for threadvar access).
runtime.syscall.freebsd gains four FreeBSD-specific leaves (all numbers
verified against release/14.0.0 sys/sys/syscall.h): sysarch=165 (set the
%fs base for TLS via AMD64_SET_FSBASE=129, passing a POINTER to the base
value), _umtx_op=454 (the futex analogue), thr_new=455 (the clone
analogue), thr_exit=431 (terminate ONE thread — NOT exit(2)/SYS_exit,
which kills the process).
runtime.start.static.freebsd gains the full TLS machinery from its Linux
sibling: the auxv PT_TLS walk (AT_PHDR/PHENT/PHNUM and PT_TLS are the same
values on FreeBSD as Linux), the variant-II block build, BuildThreadTLS/
FreeThreadTLS, and _BlaiseStartC now calls SetupTLS(auxv) before main.
The one difference from Linux: the %fs base is set via
sysarch(AMD64_SET_FSBASE, @base) rather than arch_prctl(ARCH_SET_FS, base).
runtime.thread.static.freebsd (new) is the FreeBSD sibling of
runtime.thread.static.linux — it DEFINES the bare pthread_* names the RTL
imports. The 3-state Drepper mutex (CAS/Xchg) is target-invariant and
copied verbatim; only the wait/wake primitive is swapped
futex -> _umtx_op(UMTX_OP_WAIT_UINT_PRIVATE=15 / WAKE_PRIVATE=16). Thread
create fills a 104-byte struct thr_param at the amd64 offsets
(start_func@0 arg@8 stack_base@16 stack_size@24 tls_base@32 tls_size@40
child_tid@48 parent_tid@56 flags@64 rtp@72), memset-zeroed first so flags/
rtp/spare are 0, and calls thr_new. child_tid/parent_tid point at an
Int64 (FreeBSD long*, not Linux's 4-byte). FreeBSD has no
CLONE_CHILD_CLEARTID, so join uses a dedicated JoinWord: the child
trampoline runs StartRoutine, sets JoinWord:=1, _umtx_op-wakes it, then
thr_exit(nil); pthread_join _umtx_op-waits while JoinWord=0.
Both new/edited units are standalone (nothing in the Linux build graph
uses them), so the Linux self-host is untouched. Test
TestLink_FreeBSDThreadLeaf_SyscallNumbers asserts thr_new(455)/_umtx_op
(454)/thr_exit(431) bytes are present and Linux's clone(56)/futex(202)
are absent. Step 4 (the FreeBSD syscall leaf) is now complete.
The Step-4b RTL leaves for a static FreeBSD build. memcpy/memset/memcmp/
strlen are already target-invariant (runtime.cstub, pure Pascal — shared),
and the x86-64 jmp_buf is ABI-invariant (runtime.setjmp — shared), so this
step is small:
* runtime.syscall.freebsd gains getrandom (FreeBSD SYS 563, not Linux's
318) and an mremap stub. FreeBSD has no in-place-remap syscall, so
mremap is a plain function returning MAP_FAILED (-1); runtime.mem's
large-realloc path already treats an mremap failure as "grow the slow
way" (alloc a fresh region, memcpy, free the old), so exporting a failing
mremap gives the allocator its no-mremap fallback with zero allocator
changes.
* runtime.libc2.freebsd — the FreeBSD sibling of runtime.libc2.linux: the
atexit registry + `exit`, __cxa_atexit, abort, freestanding UTC
civil-calendar gmtime_r/localtime_r/timegm, mkstemp, and system. The
calendar math, atexit registry, and struct tm layout are target-invariant
(tm is identical on Linux/FreeBSD amd64), so the only differences from the
Linux unit are the syscall leaf it builds on (runtime.syscall.freebsd) and
the FreeBSD O_* flag values (O_CREAT=$0200, O_EXCL=$0800 — not Linux's
$40/$80).
Both units are standalone (nothing in the Linux build graph uses them), so
the Linux self-host is untouched — confirmed by the clean fixpoints/suite.
Test TestLink_FreeBSDSyscallLeaf_GetrandomAndMremapStub asserts getrandom
uses 563 (and NOT Linux's 318) and the mremap stub returns -1. Threads
(thr_new/_umtx_op) are Step 4c.
Grow runtime.syscall.freebsd.pas from the Step-3 minimum (_exit/write) to
the file/process syscall leaves, mirroring runtime.syscall.linux but with
FreeBSD syscall numbers and FreeBSD's carry-flag error convention.
Added leaves: open close read lseek fstat stat mkdir rmdir unlink rename
chdir chmod getpid dup2 pipe mmap munmap nanosleep clock_gettime fork
execve wait4 kill getcwd. Each emits the raw `syscall` (number in %rax,
args rdi/rsi/rdx/r10/r8/r9, %rcx->%r10 for >=4 args) and translates
FreeBSD's error convention (CF set, positive errno in %rax) into the
-errno the Pascal call sites in rtl.platform.posix expect:
movq $N, %rax ; syscall ; jae .Lok ; negq %rax ; .Lok: ret
Syscall numbers are the FreeBSD 14 ino64 ABI (mmap=477 lseek=478
fstat=551, NOT the freebsd6/11 compat 197/199/189). FreeBSD's ino64 ABI
has no plain `stat` syscall, so stat(path,buf) is a wrapper over
fstatat(AT_FDCWD,path,buf,0)=552; pipe(fds) wraps pipe2(fds,0)=542. All
numbers verified against release/14.0.0 sys/sys/syscall.h.
The unit is standalone (only runtime.start.static.freebsd references it,
itself outside the Linux build graph), so the Linux self-host is
untouched. Test TestLink_FreeBSDSyscallLeaf_SymbolsAndNumbers links a
fixture and asserts the bare POSIX symbols are defined and the FreeBSD
syscall-number bytes + negq (CF xlate) + mov %rcx,%r10 are present.
Scope: file/process only; memcpy/date/allocator/threads are Step 4b/4c.
grammar.ebnf: the ARRAY [IDENT] OF T form is an ordinal-indexed array;
clarify it admits both an enumeration type and a named integer subrange
as the index, with a note on how each folds (enum -> array[0..N-1],
subrange -> array[lo..hi]).
language-rationale.adoc: add the array[TSubrange] of T row to the
supported array-index table, noting the subrange supplies the index
range while its values remain ordinary unchecked integers.
`type TIdx = lo..hi; TBuf = array[TIdx] of T;` now resolves to
`array[lo..hi] of T`, parallel to the existing `array[TEnum] of T`
ordinal-index folding. Previously a named subrange used as an array
index was rejected with "'TIdx' is not an enumeration type", because a
subrange was stored as a transparent alias to its narrowest standard
integer type with its bounds discarded, and the array-index resolver
only accepted enum index types.
A named subrange now keeps its lo..hi bounds as compile-time metadata on
a DISTINCT TTypeDesc (IsSubrange/SubrangeLow/SubrangeHigh). Its storage
and ABI are unchanged — it is still its underlying narrowest int, values
behave as ordinary unchecked integers (Blaise does no range checking) —
only the array-index resolver consults the retained bounds, folding
array[TIdx] to array[lo..hi]. Low/High on such an array then work via
the folded static-array bounds.
The bounds round-trip through the .bif META 'alias' record (three
trailing fields after TypeName), so a subrange exported from a unit and
used as `array[OtherUnit.TIdx]` resolves correctly across separate
compilation. IFACE_VERSION 7 -> 8 (the alias record grew; v7 readers
must reject and recompile). CloneTypeDef copies the new fields so the
export/clone path does not drop IsSubrange.
Tests: two e2e cases (0-based and 2..4 non-zero base, asserting element
reads + Length/Low/High on every backend), two IR/semantic cases
(the index folds to the right static-array bounds), and a .bif
round-trip case (IsSubrange + bounds survive write->read); the
magic/version assertion updated to v8.
A unit-qualified type-member reference written as a field access —
`Unit.TEnum.Member` (a type-qualified enum member) or `Unit.TFoo.StaticVar` —
resolved its base type through the flat uses-chain lookup, i.e. the cross-unit
last-wins winner, ignoring the unit qualifier. With two used units exporting a
same-named enum/type this bound to the wrong one: `ea.TPalette.paThree` failed
"Enum 'TPalette' has no member 'paThree'" because it resolved TPalette to eb.
TFieldAccessExpr now carries the parser-collapsed unit qualifier (like
TIdentExpr and TMethodCallExpr already do), and AnalyseFieldAccess resolves the
base via the directed ResolveQualified when it is set — so the reference binds
to the named unit's own type, independent of `uses` order.
Also completes the .bif round-trip for the qualifier fields: TFieldAccessExpr
.QualifierUnit and the previously-unserialised TMethodCallExpr.QualifierUnit are
now encoded/decoded, and bif-coverage.status records all the qualifier/owner
fields added across the cross-unit work (serialise vs safe).
Test: e2e cross-unit qualified enum member, order-independent.
Two used units exporting a class of the same name previously errored with
"Duplicate type name". Give types the same cross-unit semantics as consts
and vars: the units coexist, a bare reference binds to the unit later in
`uses` (last-in-uses wins), and a qualified `Unit.TName` reference binds to
that specific unit's own type — with distinct typeinfo, vtable, field
cleanup and method dispatch.
Semantic:
- DefineTypeLastWins detaches the prior unit's type on a cross-unit collision
(kept in the per-unit cache for qualified access) and installs the later
unit as the flat winner; same-unit redeclaration stays a hard error. The
duplicate-method and impl-body-link guards skip foreign-owned entries (with
a signature fallback so a generic instance whose template lives in another
unit still links). FindMethodDecl and a one-shot owner hint on
ResolveMethodOverload bind a call to the method on the type that actually
resolved, not the first-registered. FindTypeOrInstantiate resolves a
qualified type name through the directed per-unit lookup BEFORE the flat
FindType (which would strip the qualifier to the uses-chain winner).
- TTypeDesc gains OwningUnit (stamped on Define); the AST type decl gains
ResolvedDesc; a method-call expr gains QualifierUnit (set by the parser for
`Unit.Type.Create`), so a qualified constructor resolves against its unit.
Codegen (QBE and native): a type's storage symbols (typeinfo / vtable /
_FieldCleanup / __cn) are mangled by the unit currently being emitted, and
qualified reference sites mangle by the resolved descriptor's owner — so two
used units' same-named types emit and are referenced under distinct,
non-colliding symbols. Generic instances stay unprefixed.
Tests: e2e cross-unit type last-wins (+ reversed) and qualified
disambiguation; an IR-level check that the two types emit distinct symbols.
Two used units could not export the same module-scope global var name:
the second Define raised "Duplicate identifier", and even where one slot
existed a qualified reference (Unit.V) was lowered by bare name, so it
could not pick a specific unit's storage. Bare references also resolved
through the flat table (analysis order) rather than the uses chain,
disagreeing with the last-in-uses rule that consts already follow.
Give vars the same cross-unit semantics as consts:
- DefineGlobalLastWins detaches the prior unit's var on a cross-unit
collision (keeping it alive in the per-unit cache so Unit.V still
reaches it) and installs the later unit as the flat winner; same-unit
redeclaration and module-name markers stay hard errors. RegisterVars
mirrors this on the prebuilt-import path, as RegisterConsts does.
- The analyser stamps the resolved owning unit onto each global-var
reference and assignment target (the uses-chain winner for a bare ref,
the named unit for a qualified one). Codegen mangles the storage
symbol with that owner instead of re-looking-up the bare name, so the
definition (keyed on the unit being compiled) and every reference
agree. A bare reference now follows uses order like a const; a
qualified reference always hits its own unit's slot.
Tests: e2e last-wins (+ reversed) and qualified disambiguation across two
units exporting `var V`, plus an IR-level check that qualified loads emit
distinct owner-prefixed symbols.
Module-scope global variables are now emitted under a unit-prefixed
symbol (GlobalVarUnitPrefix), so same-named globals in different units
(e.g. GRegistry in two units) do not collide at link. The definition
(EmitGlobalVarData) and every reference (VarRef) run the identical
owner-resolution + allowlist, so they always agree.
A static class-var (e.g. RegModU.TReg.FCount) is already emitted under
its fully unit+class-qualified symbol RegModU_TReg_FCount. That name is
passed to GlobalVarUnitPrefix verbatim; Lookup does not find it, so the
owner fell back to the current unit and the prefix was applied a SECOND
time (RegModU_RegModU_TReg_FCount), so the reference no longer matched
the definition and the link failed. Add an idempotence guard: if the
name already starts with the resolved prefix it is carried through
unchanged rather than re-prefixed.
A 'Unit.Symbol' reference must resolve against that specific unit's
exports — never the flat global table or the uses chain — so a
same-named symbol in another used unit can neither shadow it nor be
shadowed by it. Previously the unit prefix was discarded at three
independent sites (idents, type names, statement targets), collapsing
every qualified reference to a bare lookup; a shadowed const/type in an
earlier-in-uses unit was then unreachable even when named explicitly.
Introduce a single directed-lookup primitive, ResolveQualified(Unit,
Name): per-unit cache first (the authoritative map of what each unit
exported, where a cross-unit collision loser is retained after being
evicted from the flat table), then a lenient flat-table fallback for
harnesses that populate the global but not the cache. Route the three
sites through it: the parser now preserves the matched unit on
TIdentExpr.QualifierUnit (serialised into the .bif), the type resolver
consults it for dotted type names, and a unit-qualified class ancestor
'class(Unit.TParent)' resolves through the type machinery so inheritance
binds to the named unit's type.
The const last-wins path now stashes the shadowed const in the per-unit
cache (it claimed to but never did), so 'Unit.Const' reaches the
declaring unit's own value regardless of which unit won the bare slot.
Parent/implements names parse via ParseTypeName (which already absorbs
the unit qualifier and nested generics) instead of ParseGenericName.
Tests: qualified-const disambiguation (ua.Foo=100 / ub.Foo=200 read
independently of the bare last-wins winner) and qualified inheritance
(class(ua.TParent) inherits across units).
When two used units export a constant of the same name, the importer
silently kept whichever was registered first and destroyed the other,
so the later unit in the `uses` clause could never shadow the earlier
one. Pascal's rule is last-in-uses wins.
Add TScope.ExtractLocal / TSymbolTable.ExtractLocal, which detach a
symbol from a scope without freeing it (the owning list keeps the
object alive for any per-unit cache that still references it). On a
cross-unit const collision, both the source-analysis path
(AnalyseConstDecls) and the prebuilt-import path (RegisterConsts) now
extract the prior unit's const and install the later unit's as the
flat-scope winner, instead of dropping the newcomer. Same-block and
unit-name-marker duplicates remain hard errors.
Adds a two-written-units e2e helper (CompileAndRunWithUnits) and
regression tests asserting the `uses` order decides the winner.
Enum members are no longer registered as bare global constants. They are
held in a type-keyed reverse index (member name -> list of declaring
enum, ordinal, declaration order), so two enums may share a member name
without colliding in the global scope.
A bare member reference resolves by a fixed cascade:
1. a real symbol of that name wins (backwards compatible);
2. otherwise the expected type at the use site selects the enum;
3. otherwise a single unique candidate is accepted.
When a bare member is ambiguous and no type context is available, this is
now a hard error that names the declaring enums and tells the user to
qualify it as <EnumType>.Member, replacing the previous last-wins
warning. The separate duplicate-member warning channel is removed.
Expected-type context is supplied at every site that knows its target
type without changing the AnalyseExpr signature: variable/field/element
assignment (static, dynamic, open and multi-dimensional arrays, plus
implicit-Self and default-property writes), pointer-target writes, case
values and ranges, set elements and ranges, the `in` left operand, for
loop bounds, standalone and method call arguments (including
implicit-Self, qualified, metaclass and constructor calls), procedural-
type indirect-call arguments, and function results via Result and Exit.
Qualified forms (TEnum.Member, Unit.Member, Unit.TEnum.Member) continue
to resolve unchanged.
Tests cover each context above plus the ambiguity error, in both the
semantic/IR suite and the end-to-end suite.
A bare or type-qualified enum member (`Color.cRed`, or `cRed` where the
target type is known) now resolves correctly.
Also adds TSymbolTable.InCodegen, set by both backends around code
emission, which disables the analysis-time impl-private suppression in
Lookup. That guard exists only to stop an implementation-section symbol
of unit A leaking into a different unit B DURING ANALYSIS; at codegen a
backend legitimately resolves a unit's own impl-section classes to emit
their typeinfo/vtable/_FieldCleanup. FDefineOwningUnit can drift to a
dependency unit mid-emit, which previously made the suppression wrongly
fire and silently drop an impl-section class's typeinfo — leaving a
dangling `typeinfo_<Unit>_<Class>` reference the linker bound to a garbage
address (an out-of-range-metaclass crash, e.g. via RegisterTest). This
is the robust, both-backend root-cause fix for that crash, which this
commit's layout change re-exposed; it supersedes the narrower
EmitClassSection DefineOwningUnit-pin added earlier.
The libraries collected from `external 'lib'` declarations (the program's
and every used unit's TUnitInterface.LinkLibs) are unioned into
TBackendOpts.LinkLibs and emitted on the toolchain link line as one
-l<name> each, additive to the always-needed -lm/-lpthread. Both the QBE
and native (external-linker) paths route through LinkViaToolchain and so
gain the flags.
The in-process internal linker (--linker internal) links only Blaise ELF
objects + the source-built RTL and has no concept of -l<name> system
libraries. Rather than silently drop a declared dependency and emit a
binary with unresolved symbols, LinkViaInternalLinker now fails loudly
when LinkLibs is non-empty, pointing the user at --linker external.
Tests: TestExternalLib_MissingLib_FailsLink_{QBE,Native} drive the real
compiler binary end to end (the e2e harness links with a hardcoded
`cc ... -lm -lpthread` and never calls LinkViaToolchain, so it cannot
exercise this). A program declaring `external 'nosuchlib...'` — never
called, but collected into LinkLibs at parse time — must fail the link
with the linker's "cannot find -lnosuchlib...", which only happens if the
driver actually emitted the flag.
`external 'c' name 'strlen'` now records the bare library name on the
declaration (TMethodDecl.ExternalLib) and hoists it into the owning
unit/program's LinkLibs set, so the link layer can expand it to a
-l<name> dependency. The library clause is optional and parsed before
the existing `name '...'` clause, so `external name 'x'` and bare
`external 'c'` both still parse.
TUnitInterface.LinkLibs is serialised in the .bif META block (one
EncodeStringList after ImplUsedUnits, before HasInitialization) so a
unit's external-library dependencies round-trip across separate
compilation. IFACE_VERSION is bumped 6 -> 7: the META layout grew, so
a v6 reader must reject these .bif and recompile (without the bump a v6
binary would misparse the extra field and read past the END marker).
Tests: a parser test asserting `external 'c' name 'strlen'` populates
ExternalLib + the program's LinkLibs; a .bif round-trip test for
TUnitInterface.LinkLibs; the magic/version assertion updated to v7; and
the existing e2e strlen case. bif-coverage.status records
TUnitInterface.LinkLibs as serialised.
Assigning @Obj.Method to a bare (implicit-Self) method-pointer field —
FFn := @Self.M with no Self. on the left-hand side — was mishandled by
both the QBE and native backends. The assignment emitter routes all
implicit-Self field stores through a dedicated block that returns before
the method-pointer assignment handler runs, so a 16-byte (Code, Data)
method pointer fell through to the scalar-store path:
- QBE stored only the 8-byte pointer to the source block, leaving the
Data half garbage; a later call then dispatched on a bad Self and
crashed.
- native reached the @Obj.Method address-of path, which is statement
level only, and raised "@Obj.Method must be used in assignment
context".
Add a method-pointer case to the implicit-Self field-assignment path in
each backend, mirroring the existing simple-variable and explicit-field
handlers: copy the whole 16-byte block into Self + field offset, with the
Code half resolved through the instance vtable for a virtual/override
method so the dynamic override is captured.
Explicit Self.FFn := @Self.M and the implicit-Self call FFn(...) were
already correct; only the implicit-Self assignment was affected.
Tests: cp.test.proctypes_ofobject gains an IR test asserting the
implicit-Self method-ptr field store emits a 16-byte memcpy;
cp.test.e2e.misc gains an end-to-end test that assigns via implicit Self
then calls back through the field (reading another field via Self, so a
wrong Data half would crash). Both fail before this change and pass
after.
# Conflicts:
# compiler/src/test/pascal/cp.test.e2e.misc.pas
A `function ... of object` return value is a 16-byte [Code, Data]
method pointer, ABI-identical to `record Code, Data: Pointer end`. It
was returned as a scalar (only the Code half), so a method captured from
a call (`M := Obj.GetFn()`) and later invoked dropped the Data/Self half
— a call through M then dereferenced a null/garbage Self.
Route the return through the record-return ABI: a canonical 16-byte
[Code, Data] two-pointer record classifies as two integer eightbytes ->
rcInt2 (rax:rdx) on SysV, matching a direct record return. Both the
QBE and native x86-64 backends are fixed.
The native side mirrors the QBE approach via a lazily-built canonical
`_BlaiseMethodPtr` record (distinct GNative* singletons so the two
backends' identically-named globals do not collide when both are linked
into the self-hosting compiler), wired through BuildFrame's return
classification (rcInt2, not sret), the epilogue (loads both halves into
rax:rdx), the Result-slot zero-init (both 16 bytes), and every call-site
capture: variable- and field-destination assignment of a
method-ptr-returning call, and the immediate-invoke path
(`Obj.GetFn()(args)`) which materialises the 16-byte block and dispatches
through it so Data lands in %rdi.
Tests: an IR-level assertion that the return uses the aggregate ABI, plus
e2e cases. The new TestRun_MethodPtrReturn_ReadsSelf returns a method
that READS Self (an instance field) — unlike the X+Y case, which never
touches Self and so could not catch a dropped Data half — and asserts the
right value on every backend.
Taking the address of a virtual method through a receiver
(`M := @Obj.Method`) stored the statically-resolved declared-type
method address as the Code half of the (Code, Data) method pointer.
A later call through M therefore always ran the declared type's method,
ignoring the receiver's dynamic override — unlike a direct `Obj.Method()`
call, which dispatches through the vtable.
Resolve the Code half through the instance's vtable when the method is
virtual/override (VTableSlot >= 0): load the vptr from the object,
index slot (VTableSlot + 1)*8 (slot 0 is typeinfo), and store the
resulting code pointer. Non-virtual methods keep the static label.
Both the QBE and x86-64 backends had the same defect; the native side
fixes it at both the variable- and field-destination assignment sites.
Additionally fixes a pre-existing native-backend bug this change exposed:
EmitClassSection re-asserts FSymTable.DefineOwningUnit to the unit being
emitted before every FindType. Resolving an earlier class in the loop
(its parent/field/method types via ClassSymName -> Lookup -> the
uses-chain walk) could leave DefineOwningUnit pointing at a dependency
unit; FindType for one of this unit's own implementation-section
(IsImplPrivate) classes was then suppressed by Lookup's cross-unit-leak
guard and returned nil, so the class was skipped and its typeinfo /
vtable / _FieldCleanup were never emitted. The dangling
`typeinfo_<Unit>_<Class>` reference (e.g. a metaclass value passed to
RegisterTest from the init block) then bound to a garbage address,
producing a layout-sensitive out-of-range-metaclass crash at runtime.
Tests: an IR-level assertion that the capture no longer stores the
static method label, plus two e2e cases (variable and field
destination) asserting the override runs on every backend.
The test framework's `GRegistry: TList<TTestCaseClass>` collided with
`GRegistry: TTargetRegistry` in blaise.codegen.toolkit. The compiler emits
unit-level globals under their bare name with export (strong) linkage, so both
`GRegistry` symbols resolved to one shared 8-byte slot. When the test runner
links both units, the test framework and the codegen toolkit alias the same
storage and free/read it as the wrong type at teardown.
Rename the test-framework global to GTestRegistry — a unit-specific name that
collides with nothing else — so the two registries occupy distinct storage.
`tkAs` was a member of two enums: TToolKind (uToolchain, ordinal 1) and
uLexer.TTokenKind (the lexer token enum, ordinal 40). In ResolveAssembler,
the bare assignment `Result.Kind := tkAs` should bind to TToolKind, but
bare-member resolution is cross-unit order-fragile and could bind it to
TTokenKind.tkAs (ordinal 40) instead. 40 is out of range for the 4-member
TToolKind, so any downstream code that switches or indexes on the resolved
Kind runs off the end into a wild pointer — a layout-sensitive crash that
manifested intermittently in the test runner (a metaclass typeinfo symbol
truncation surfacing as a SIGSEGV during teardown).
Renaming the member to tkGnuAs removes the ambiguity entirely: `tkAs` now
exists only in uLexer.TTokenKind. This is bootstrap-safe (no qualified-enum
syntax, which the release binary does not yet support). Verified the
ResolveAssembler codegen now bakes Ord(tkGnuAs)=1 (was 40).
Float-typed variables (Double, Single) were invisible in pdr — locals,
print, gl and every other inspection command failed for floats while
integers, strings, classes and records-of-int-and-string worked. The OPDF
emitter never produced a recPrimitive for tyDouble/tySingle: EmitTypeDesc
did not dispatch the float kinds, so a float variable's GlobalVar/LocalVar
record referenced a TypeID with no corresponding type record, and pdr could
not resolve the type to read the value.
EmitTypeDesc now routes tyDouble/tySingle to EmitPrimitive, which emits the
record with SizeInBytes (8/4), IsSigned=1, and a new SubKind=SK_FLOAT (4).
The pdr side already handled this fully: the adapter maps SubKind skFloat to
Category tcFloat and TFloatEvaluator reads the IEEE 754 value by size — it
was only waiting for the compiler to emit the record.
Verified end to end with pdr: a global, a local, and a Double record field
all print their values (d = 3.14159, s = 2.5, pt.Z = 9.81), and locals lists
them. IR tests assert the Double/Single recPrimitive carries SubKind=4 and
the correct size.
Commit 50b8d75e made parentheses mandatory on statement-position calls,
but kanban_app.pas was not updated, so 'PrintUsage' (a bare reference in
the --help branch) failed to parse with 'Bare reference to PrintUsage
requires () for a call'. This broke a plain 'pasbuild compile', which
reaches blaise-kanban before blaise-bif-coverage. Add the parens.
The tool is now activeByDefault, so it is reached by a plain
'pasbuild compile'. With manualUnitPaths=true it only searches the listed
paths, which lacked the stdlib directory where SysUtils and the other
units it uses live, so the default build failed with 'Unit SysUtils not
found'. Add ../../stdlib/src/main/pascal, mirroring the compiler module's
own stdlib unit path.
Imported methods always had IsOverload=False: the .bif method layouts
round-tripped IsVirtual/IsOverride/IsStatic but not IsOverload, and the
importers never set it. ResolveMethodOverload's hiding walk stops at the
first non-overload candidate, so an overload set split across an imported
class and an imported ancestor would be truncated to the more-derived
level (latent today because the known overload sets live on a single
class).
Add IsOverload to both method-encoding paths and bump BLAISE-IFACE 5 -> 6:
* TRoutineSig (class methods): field added in uUnitInterface, set in
BuildRoutineSig, encoded/decoded in EncodeMethodSig/ReadMethodSig,
propagated in SynthesiseMethodDecl.
* TMethodDecl (interface and generic-template methods, the
EncodeMethodDecl/ReadMethodDecl path): the same flag was dropped there
too, so an overloaded interface method lost its directive across the
.bif.
Round-trip tests for both paths: TestRoundTrip_Class_WithOverloadedMethods
and TestRoundTrip_Interface_WithOverloadedMethods. bif-coverage status
gains TRoutineSig.IsOverload.
The recProperty corruption (garbled Name/ReadMethod/WriteMethod string
fields) was fixed in c6d22aac, but the only property test asserted just
the '# recProperty' comment — it would not have caught the RecSize/layout
mismatch that caused the reader to overshoot into the next record.
Add TestOPDF_Property_MethodBacked_LayoutIntact: for a method-backed
property it asserts the exact RecSize (32-byte fixed payload + the three
string lengths), the getter/setter symbol strings, and the three length
words. This pins the writer's declared size to the actual emitted bytes,
which is the invariant the reader (SizeOf(TDefProperty)=32 then three
length-prefixed strings) depends on.
The cross-unit static-members tests (QBE + native) already drive a static
var, static method and static const through the warm --unit-cache .bif
boundary. Add a static property (a getter-backed 'static property Counter
read Next') to the shared test unit so TPropertyDecl.IsStatic surviving the
.bif round-trip is guarded too: a non-static import would dispatch the
getter with a bogus Self. Expected stdout updated to include the extra
Counter read.
'TFoo.X.Field := V' and 'TFoo.X.Method()' failed semantic analysis with
"Field access requires a record or class base, got 'class of TFoo'".
The read form (TFoo.X.Field as an expression) already worked.
When a qualified static var is the base of a further chain on an l-value,
the parser shapes it as a Base-form field access whose base is the bare
class-name ident resolving to the 'class of TFoo' metaclass, rather than
the simple RecordName form. AnalyseFieldAccess's chained-base path
rejected a metaclass base outright.
Semantic: in the chained-base path, when the base type is a metaclass,
resolve the field as a static var (IsClassVarRead) or static property
(IsStaticPropGet) of the metaclass's base class, mirroring the RecordName
static-member read path, so the result type feeds the outer chain.
QBE codegen: EmitInstancePtr loads the instance pointer from the static
var's global slot for an IsClassVarRead base, and the field-access reader
checks IsClassVarRead before the chained-base branch so a Base-form static
var is loaded directly rather than treated as a chained field of the
metaclass. The native backend already produced correct code.
Tests cover the read, a scalar field write, and a method call through the
chained static-var base on both backends.
A static var of interface type round-tripped through static methods
(THolder.SetIt(X: IThing) / THolder.GetIt: IThing) was miscompiled on
both backends. The 2-slot fat-pointer global slot itself was correct;
the bugs were in the static-CALL path.
QBE: a static method whose first parameter is interface-typed emitted
'call $T_M(, l obj, l itab)'. The interface argument fragment carries a
leading ', ' (it is normally appended after Self or a preceding arg), and
the static-call argument loop did not strip it, so the first argument
began with a comma and QBE rejected it as an invalid class specifier.
Strip the leading ', ' in that branch, matching the existing
EmitMethodCall var-arg/interface precedent.
Native: a static method returning an interface routed through
EmitClassIntfSretMethodCall (ResolvedClassType is the owning class, a
class type), which loaded the bare type name as a global Self and emitted
an undefined reference to the class symbol at external-cc link time (the
internal/driver linker masked it). Add an IsStaticCall branch that emits
the plain free-function interface-sret ABI with no receiver, mirroring
the non-Self arm of EmitIntfSretCall.
Tests: TestIR_StaticCall_InterfaceArg_NoLeadingComma asserts the QBE call
has no leading comma; TestRun_StaticVar_InterfaceStore compiles and runs
the SetIt/GetIt round-trip on both backends.
A qualified write to a class-level static var was parsed and visibility-checked
but not lowered — semantic reported it as "not yet supported". It now lowers
to a plain store of the shared global slot, identical to the bare form a static
method writes (StaticVar := V).
Semantic marks the TFieldAssignment with IsClassVarWrite + ClassVarEmitName +
ClassVarLhsType after enforcing visibility. Both backends delegate to their
existing global-store path (EmitAssignment) via a borrowed synthetic
TAssignment — the Expr is shared and nilled before the temp is freed — so the
scalar, class-ARC, and pointer store logic is reused verbatim rather than
duplicated.
Verified on both backends: scalar (int/enum/bool/float), class with ARC
(assign + nil-release). Adds e2e TestRun_StaticVar_QualifiedWrite_Scalar and
_ClassARC (each compiled+run under native and QBE).
Two adjacent limitations remain, both pre-existing and independent of this
change (documented in bugs.txt): interface-typed static vars mishandle the
2-slot fat-pointer store (the bare form is equally affected), and a chained
l-value base through a qualified static var (TFoo.X.field := V) is unresolved
in the assignment-receiver path while the read form works.
The initial visibility pass checked the qualified form (TFoo.StaticVar) but not
the bare unqualified form, which resolves to the same shared global. A
strict-private static var was therefore still reachable by name from another
type, the program body, or a unit's initialization section — contrary to the
design (a strict-private member is reachable only from its declaring type's own
methods).
Enforce member visibility at the bare static-var read (AnalyseIdentExpr) and
write (AnalyseAssignment) sites, and at the qualified read (AnalyseFieldAccess),
matching the qualified-write site added earlier.
A static method leaves FCurrentClass nil (so implicit-Self refs fail cleanly),
which would wrongly reject a strict-private static var accessed from its own
type's static method — the singleton's `static function TFoo.Instance` reading
bare FInstance. Track FCurrentMethodOwner (the declaring type of the current
method body, set for static and instance methods alike) and use it as the
"from" class for static-var visibility via the new AssertStaticVarVisible, so
own-type static methods keep access while non-method contexts (program body,
unit init/final) do not.
Adds cp.test.visibility cases for bare-from-other-type, from-program-body, and
non-strict-private-from-program-body (the unit-init-scope analogue), plus a
public static var qualified cross-type read.
Visibility modifiers on class and record members are now enforced, not merely
parsed. A `private` member is reachable only within the declaring unit; a
`protected` member additionally within descendant types; `public`/`published`
everywhere the type is. Adds `strict private` and `strict protected`, which
narrow visibility to the declaring type itself (and, for strict protected, its
descendants) rather than the whole unit. `strict` composes with `static`.
Parser: track the current visibility section in class/record bodies and the
contextual `strict` keyword (only before private/protected); carry the
visibility onto each field, method, and property declaration. `strict public`,
`strict published`, and a bare `strict` are rejected.
Semantic: every qualified and unqualified member-access site checks visibility
via MemberVisibleTo / AssertMemberVisibleV, using the member's declaring unit
and declaring type. Static (class-level) vars now carry Visibility and
OwnerTypeName on their TSymbol so a qualified static-var access enforces the
same rules; a strict/private static var written from another type is rejected
with a "not accessible" diagnostic. Qualified static-var writes from a
permitted context are reported as not-yet-lowered rather than mis-resolved
(permitted writes use the unqualified form inside a static method).
Cross-unit: member visibility and declaring-type/unit origin are carried across
separately-compiled units in the .bif interface (BLAISE-IFACE version 5) so the
checks hold for imported types.
Updates docs/grammar.ebnf with the visibility-section grammar and adds
cp.test.visibility (parse + semantic enforcement) plus thread-test fixes that
switched two TThread subclasses from private FTerminated/FFinished fields to
the public Terminated/Finished properties.
A class declared in a unit's implementation section (IsImplPrivate) referenced
as a metaclass value in that same unit's initialization block — e.g.
RegisterTest(TFoo) — could be emitted under a bare, unqualified, undefined
typeinfo symbol, linking to garbage and crashing at runtime.
Per-unit codegen sets the symbol-table viewing context (DefineOwningUnit) to the
unit being emitted, so TSymbolTable.Lookup's cross-unit-leak guard does not
suppress the unit's own impl-section types. But earlier emit passes in EmitUnit
(method-body type resolution that walks the uses chain) leave DefineOwningUnit
pointing at a dependency unit. By the time the class-data section and the
initialization block are emitted, a metaclass reference to an impl-section class
resolved to nil and ClassSymName/ClassUnitPrefix fell back to the bare class
name. Only per-unit separate compilation exposed this; whole-program emit-ir
mode kept the context stable, so all fixpoints and the IR harness passed while
the linked binary held a garbage typeinfo pointer.
Re-assert DefineOwningUnit := AUnit.Name immediately before the class-data
section emission and before the initialization-block emission, in both the
native x86-64 and QBE backends.
Add Test{Native,QBE}ImplSectionMetaclassInInit_Runs to cp.test.e2e.sepcompile,
exercising an impl-section class assigned to a class-of global in the unit's own
init on both backends.
Follow-up to the within-unit static-members feature (0977dc16). Static
class/record members declared in one unit can now be used from another through
the compiled `.bif` interface (the `--unit-cache` / separate-compilation path),
not only when every unit is recompiled from source.
The `.bif` wire format already carried most static facts; the values were being
dropped by consumers around the serialiser:
* Export clones — CloneFieldDecl / ClonePropertyDecl / CloneMethodDecl now copy
IsClassVar / ClassVarEmitName, property IsStatic, and method IsStatic. The
same clone gap silently broke the pre-existing IsWeak / IsDefault round-trip
([Weak] fields and `default` properties exported as plain) — fixed here too.
* Method static-ness — TRoutineSig gained an IsStatic field (a final non-virtual
instance method and a static method both carry VTableSlot = -1, so a dedicated
flag is required). Populated on export, encoded/decoded symmetrically
(BLAISE-IFACE version 3 -> 4), and applied to the synthesised TMethodDecl on
import so `TypeName.StaticMethod()` resolution succeeds.
* Import — ImportClassEntry / ImportRecordEntry register an imported static var
as the shared global (bare + qualified skVariable symbols carrying the
*decoded* GlobalEmitName, never recomputed — the importing unit's prefix
differs), carry property IsStatic onto TPropertyInfo, and import the type's
static ConstDecls so `TFoo.MaxItems` resolves.
* Parser — out-of-line `static function T.M` bodies in a unit's implementation
section are now parsed (mirrors the program-level standalone path).
Tests: TestStaticMembers_CrossUnit_QBE / _Native in cp.test.e2e.sepcompile
(cold + warm `--unit-cache` round-trip, both backends), and
TestRoundTrip_StaticMembers_Preserved /
TestRoundTrip_WeakField_And_DefaultProperty_Preserved in cp.test.unitinterface.
Full suite green on QBE- and native-built runners (3912 tests); all fixpoints
pass (incl. warmcache, which exercises the .bif round-trip); bif-coverage clean.
Introduce `static` (class-level) members to the Blaise language using the
`static` keyword — never an overloaded `class` keyword. A `static` member is
type-associated, not instance-associated: static methods take no implicit
Self, and static vars/consts are a single shared storage slot.
Surface, on classes and records:
* `static var` / `static const` — section form (`private static var`) or as a
bare `static` continuing the current visibility. Static vars lower to one
shared global slot (mangled `<Unit><Type>_<Name>`), zero-initialised, NOT an
instance field. Class- and interface-typed static vars are supported (the
canonical singleton storage) with store-time ARC and a program-exit release;
string and dynamic-array static vars remain deferred.
* `static function` / `static procedure` — per-member prefix or section form;
no implicit Self. Out-of-line bodies are `static function T.M`.
* `static property` — sugar over a static getter (no Self at the call site).
* record `static function` — the factory / namespaced-function form
(`TPoint.Make(x, y): TPoint`), required to be marked `static` explicitly.
There is no `static constructor` / `static destructor` (rejected at parse):
the zero-initialisation guarantee covers nil singletons, and eager setup
belongs in a unit's `initialization`/`finalization` (the Swift/Rust/Go model,
not Java/C#/Delphi). `class` is never a member qualifier.
Implementation spans the full pipeline:
* parser — `static` is a soft keyword; section qualifier (followed by
var/const) and per-member prefix forms; `static constructor/destructor`
rejected.
* semantic — static vars register a shared global (bare + qualified) under a
mangled emit label; static methods skip the Self binding; qualified
`Type.StaticVar` / `Type.StaticProp` / `Type.StaticMethod()` resolution.
* QBE + native x86-64 codegen — no-Self method signatures and call sites
(including the record-return sret and >6-arg paths), shared global data
slots, qualified static var/property reads, and class/interface static-var
release at program exit.
* `.bif` interface format — IsClassVar/ClassVarEmitName, property IsStatic, and
record/class const decls are encoded (BLAISE-IFACE version 2 -> 3).
* OPDF debug info — static vars are emitted as `recGlobalVar`s under their
mangled label so a debugger can print `TFoo.FInstance`.
Static members currently work within a single program/unit; carrying them
across separately-compiled units (export clone, import, TRoutineSig.IsStatic)
is a tracked follow-up — the .bif wire format is already in place for it.
Tests: cp.test.staticmembers (parser + semantic + IR) and
cp.test.e2e.staticmembers (compile+run on both backends). Full suite green on
QBE- and native-built runners (3908 tests); all fixpoints pass; bif-coverage
clean. docs/grammar.ebnf and docs/language-rationale.adoc updated.
The "mandatory parentheses on zero-argument calls" rule (language-
rationale.adoc) was enforced in expression position but NOT in statement
position: a bare `Foo;`, `Obj.Method;`, or `Obj.Free;` used as a statement
compiled silently, building a paren-less TProcCall / TMethodCallStmt. Issue
#148 reported `tester.print;` (a unit's global object method call) being
accepted without its mandatory ().
The statement parser now raises the same "requires () for a call" diagnostic
the inherited-call and expression-position paths already use, at the two
fall-through sites:
* bare unqualified call `Foo` with no '(' (the final ProcCall else-branch);
* bare `Obj.Method` with no '(' and no further '.' chain.
Field reads, field assignments, indexed writes, and '.'-chains are unaffected
(only a terminating bare reference is rejected). Expression-position calls
were already enforced.
Enforcing the rule required the compiler, RTL, and stdlib to comply first
(self-hosting): swept bare calls in uSemantic (Flush/RepairGenericInstances),
runtime.arc/runtime.exc (_libc_abort), blaise.codegen.native.backend
(FAsm.AppendLine), and a json.writer doc example. The test suite embedded
many bare calls in inline program strings — all updated to carry (); adding
() never changes behaviour since these were always calls.
cp.test.parser.pas gains two parse-error tests (proc and method bare calls);
the old TestProcCall_NoParens, which asserted the bug, is inverted. grammar.ebnf
SubscriptMethodCall made parens mandatory and an example corrected; rationale
notes statement-position enforcement.
`for x := 0 to N do;` and `while C do;` have an empty statement as their
body, which the parser represents as a nil statement (its convention for
"no statement here" — every statement-list builder already filters nil).
A loop's single Body field is not filtered, so the nil reached EmitStmt:
* Native backend: EmitStmt fell through every `is` check to the
unsupported-statement fallback, which dereferenced AStmt.ClassName on
nil — segfaulting the compiler (issue #150 was reported against the
default native backend).
* QBE backend: EmitStmt raised "EmitStmt called with nil statement",
rejecting a legal empty body.
Both backends now treat a nil statement as a no-op, matching the parser's
nil-as-empty convention and the way AnalyseStmt already tolerates it. An
empty loop body compiles to a loop that does nothing.
E2E regression in cp.test.e2e.controlflow.pas asserts both `for ... do;`
and `while ... do;` compile + run as a no-op on every backend.
The OPDF emitter now writes one recRuntimeHelper (= 25) record per binary
for _StringRelease (kind 0) and _DynArrayRelease (kind 1), each carrying
the linker-resolved entry-point address.
These let the debugger inject a call to the matching RTL release routine
to free the +1 transient an injected property getter returns — without
them, `print x.Bar` on a method-getter property leaks one string/dynarray
per call in the debuggee's exit leak report. The records are emitted only
by the program object (the helpers are global RTL symbols, not per-unit),
alongside the existing recUnitDirectory.
IR tests assert both records, their address quads, and the kind ordinals
(which must match opdf_types.TRuntimeHelperKind). The reader side lives
in the opdebugger repo.
The OPDF recProperty record written by EmitProperties did not match the reader's
TDefProperty layout: it emitted only the property Name and omitted the
ReadMethodNameLen / WriteMethodNameLen length words and the two method-name
strings the reader expects. A consumer (pdr) therefore read a misaligned
record — a garbage property name and an empty getter symbol — so a method-backed
property such as `property Bar: String read GetBar` could not be resolved
(`print x.Bar` reported "field or property not found").
EmitProperties now writes the full format the reader specifies: the three length
words (ReadMethodNameLen, WriteMethodNameLen, NameLen) followed by the
ReadMethodName, WriteMethodName and Name strings. For a method-backed accessor
the method-name string is the mangled getter/setter symbol — the same symbol
written as ReadAddr/WriteAddr and emitted as the recFunctionScope name, so the
debugger can resolve it via FindFunctionByName and inject the getter call.
Verified end-to-end against the OPDF reference debugger (pdr): `print x.Bar`
now invokes the getter and prints the value; the pdr integration suite is green.
A string-returning function, method or property getter used DIRECTLY as a
Write/WriteLn argument — WriteLn(GetBar) — returns a fresh +1 string that
_SysWriteStr only borrows. EmitWrite emitted the write call but never released
that transient, leaking one string per call on BOTH backends. (A normal
procedure's const-string parameter already released it; only the Write/WriteLn
built-in path was missing the release. Assigning the result to a variable
first never leaked.)
EmitWrite now releases the string argument after the write when the argument
expression OWNS its reference (ExprOwnsRef / NativeExprOwnsRef) — a call or
concat result. Plain variables, literals and PChars are borrowed and are not
released, so no double-free. Native stashes the pointer across the
_SysWriteStr call and releases it after.
E2E regression (both backends, --debug leak tracker):
TE2ELeakCheckTests.TestDebug_WriteLnCallArg_NoLeak.
An interface (itab-dispatched) method returning a RECORD was broken on both
backends, both when the result was assigned and when discarded in statement
position, for memory-class (sret) and register-class records alike. The itab
dispatch had no record-return ABI: it emitted a scalar call, so for a
memory-class return the first argument register doubled as the sret pointer
(the callee received a garbage argument and wrote the record over caller
memory — "_StringRelease corrupted header"), and a register-class return was
dereferenced as a pointer. Assignment was equally broken because
IsRecordCall / IsNativeRecordCall keyed off ResolvedMethod, which is nil for
itab dispatch.
QBE:
* IsRecordCall recognises an itab record call (ResolvedClassType = interface +
ResolvedType in [record, static-array]) for both the args form
(TMethodCallExpr) and the zero-arg form (TFieldAccessExpr.IsInterfaceCall).
* EmitIntfSretDispatch builds the visible args (Self, method args) and delegates
to EmitRecordReturnCallSite, which applies the correct ABI from
ClassifyRecordReturn (sret pointer vs register capture) — instead of always
passing an sret buffer.
* The discarded-statement path (EmitMethodCall) gained a record/static-array
branch: a throwaway zeroed buffer, the classified call, then
EmitRecordReleaseFields on the buffer.
Native:
* IsNativeRecordCall recognises the itab record case.
* New EmitIntfRecordSretDispatch performs itab dispatch with the full
record-return ABI: an sret buffer in %rdi for a memory-class record, or
EmitRecordRegReturnCapture (a new helper covering rcInt1/rcInt2/rcSSE1/
rcSSE2/rcIntSSE/rcSSEInt) storing the register result into the destination.
Wired into EmitRecordCallSretAt, both record-assignment branches (implicit-Self
field and plain local/var/global), and the discarded-statement path.
E2E regressions (both backends, via AssertRunsOnAll):
TE2EInterfaceTests.TestRun_InterfaceMethod_ReturnsSretRecord and
_ReturnsRegisterRecord — each exercises an assigned and a discarded call.
A local `array[0..N] of IFoo` stored interface elements but never released
them when the array went out of scope, leaking one obj reference per element
on BOTH backends (the scope-exit ARC sweep had no static-array case).
Both backends now release each interface element of a static-array-of-
interface local at scope teardown, via shared helpers
EmitStaticArrayReleaseElems / EmitManagedReleaseAt (per-backend). The QBE
exception-path cleanup zeroes each released slot so an outer handler's cleanup
stays a no-op.
Scope is restricted to INTERFACE elements on purpose. Static-array-of-
class/string/record locals are NOT released here: the element store retains
unconditionally while owning code manages some such arrays manually (the ELF
writer frees each `RelaBuf: array[0..5] of TByteBuf` element with `.Free`), so
a blanket scope-exit release double-frees and corrupts the allocator — it
crashed the native self-hosted compiler (caught by fixpoint-warmcache.sh).
EmitRecordReleaseFields likewise still skips static-array fields, to stay
symmetric with the retain/copy paths. Both gaps are documented for a later,
symmetric fix.
E2E regression (both backends):
TE2ELeakCheckTests.TestDebug_StaticArrayOfInterface_NoLeak.
A nested procedure that captured a `var` parameter (record or static
array) of its enclosing proc emitted the parameter as a global symbol
reference instead of reaching it through the closure pointer, so the
program either failed to link or read/wrote the wrong storage.
Three layers:
* Semantic (uSemantic): CollectCaptures now takes the enclosing
TMethodDecl and seeds the outer-name set from its PARAMETERS as well
as its locals, so a captured var-param is detected. Capture detection
also descends through TFieldAssignment, TStaticSubscriptAssign,
TFieldAccessExpr and TStringSubscriptExpr, so `R.F := ...`, `A[i] := ...`
and their reads contribute the receiver/array name. The repeated
"add if outer and not already captured" idiom is hoisted to
MaybeCaptureName.
* QBE backend: VarRef routes a captured name through `%_cap_<Name>`.
The aggregate-ident read distinguishes a captured plain local (the
`_cap_` slot IS the aggregate address) from a captured var/out param
(the slot holds the caller's pointer, needing one extra load).
* Native backend: base resolution is unified through EmitVarBaseToReg,
which checks IsCaptured first. The static-array element paths and the
aggregate-ident path now treat a captured var-param array like the
record field paths: AWantAddress=False for a var-param capture (one
deref to the data pointer), True for a plain-local capture.
E2E regression tests (run on both backends): capture of a var record
param, a plain local record, and a var static-array param.
A class (or record-with-methods) declared in a unit's `implementation`
section is now fully supported: its methods are accepted, its bodies are
type-checked, and its type metadata (typeinfo, vtable, _FieldCleanup) is
emitted on both backends so the unit links and runs.
Three layered problems, fixed together:
1. Semantic ordering + body analysis (uSemantic.AnalyseUnitForExport):
impl-section TYPE decls are registered before LinkClassMethodImpls (so an
impl-section class has its methods in FMethodGroups), and ImplBlock method
bodies are now analysed alongside IntfBlock ones (otherwise field/param
references carried no ResolvedType and codegen aborted).
2. Codegen emission: native EmitUnit and QBE AppendUnit walked only
IntfBlock.TypeDecls. They now emit ImplBlock classes too — method bodies,
class section (typeinfo/vtable/_FieldCleanup) and interface defs — under the
owning unit's mangling prefix. Each backend establishes the emitted unit as
the symbol-table viewing context so the impl-private class resolves during
emission and the reference/definition symbol names agree.
3. Cross-unit leak (the blocker): impl-section symbols were registered in the
shared global scope and leaked into unrelated units via TSymbolTable.Lookup's
flat-table fallback — a class in unit A's implementation section resolved
inside an unrelated unit B that never `uses` A (and self-built the compiler
into a SIGSEGV). Impl-section symbols are now tagged IsImplPrivate and
Lookup suppresses them whenever the viewing unit is not the owner. Interface
symbols and transitive interface-uses visibility are unchanged.
This also closes the corresponding latent leak: an implementation-section
declaration of any unit no longer resolves in units that do not use it.
Tests: cp.test.e2e.sepcompile gains TestNativeImplSectionClass_Compiles,
TestQBEImplSectionClass_Compiles, and TestImplSectionClass_DoesNotLeakCrossUnit.
Rationale recorded in docs/language-rationale.adoc.
All four fixpoints (QBE, native, internal-asm, warm-cache) green; full suite
3859 tests on both the QBE-built and native-built test runner.
A record-to-record assignment whose type has managed fields (string,
class, interface, dyn-array, or a nested record thereof) lowered to a bare
memcpy in the catch-all record-copy branch — no _StringAddRef on the
source and no _StringRelease on the destination's prior contents. The two
records then shared one buffer at refcount 1, so mutating or churning
either side dropped the shared buffer to 0 and freed it under the other
(use-after-free, then `_StringRelease double-free (refcount < -1)` abort
once the heap slot was reused).
var a, b: TPerson; { TPerson = record Name: string; ... }
a.Name := 'Alice';
b := a; { b.Name shared a.Name's buffer, rc still 1 }
a.Name := 'Bob'; { freed the buffer b still points at }
WriteLn(b.Name) { use-after-free }
Fix: in the record/static-array copy branch, when the record type is not
managed-clean, release the destination's managed fields and — unless the
source already transferred ownership (+1, guarded by NativeExprOwnsRef, as
the string/dyn-array assignment paths do) — retain the source's managed
fields, then memcpy. Mirrors the existing record-field-store and
dyn-array-element-store copy paths. QBE was already correct; native-only.
Test: cp.test.e2e.records TestRun_Record_VarToVarCopy_StringField_ARC —
copies a record with a string field, churns the source through 200 heap
reallocations, and asserts the copy still reads its own 'Alice-42' rather
than freed/reused memory. Aborts before the fix, passes after.
QBEMangle (TCodeGenQBE method) and NativeMangle (native free function)
applied the same metacharacter mapping: '<' ',' ' ' -> '_', drop '>', and
'$' '@' '^' -> '_D_' '_V_' '_P_'. Two copies of the symbol-naming rule is
a link-time-breakage risk if one side gains a case the other lacks.
Hoist a single CodegenMangle into blaise.codegen (distinct name, per the
F4 method-binding gotcha); both backends delegate. The shared version
keeps QBEMangle's clean-name fast-path (no per-character concat when no
metacharacter is present) and reads bytes via StrAt — so NativeMangle now
also uses the stage-stable StrAt instead of the Ord(S[i]) idiom the house
rules warn against.
Cross-backend dedup, no behaviour change: native .s AND QBE IR both
byte-identical on a generics + overload program (TList<Integer>,
TList<string>, Show$i/Show$S → TList_Integer_*, Show_D_i/Show_D_S). All
four fixpoints — which mangle the entire compiler's generic/overload
symbol set — and the full suite on both test runners are unchanged.
ExprOwnsRef (QBE backend) and NativeExprOwnsRef (native backend) were
byte-identical twins: a pure walk over the AST node + its resolved type
deciding whether an r-value leaves an ARC-managed value at refcount +1
that the consuming site must not AddRef again. Two drift-prone copies of
an ARC decision is exactly the failure mode that leaks/UAFs when one side
is updated and the other is not.
Hoist the single source of truth into blaise.codegen as ArcExprOwnsRef
(distinct name so an unqualified call inside a method cannot bind to a
same-named method — the F4 gotcha). Both backends keep their existing
free function as a one-line delegator, so every call site is unchanged.
Cross-backend dedup, no behaviour change: native .s AND QBE IR both
byte-identical on an ARC-heavy program (function-returned class, string
property read via getter, method call returning a class, dynamic-array
return). All four fixpoints and the full suite (QBE-built and native-built
test runners) unchanged.
The per-argument SysV slot classification — open-array and by-value
interface occupy two integer slots, a by-value Double/Single occupies one
xmm slot, everything else one integer slot — was duplicated verbatim in
EmitPopMethodArgsToRegs and EmitMethodOverflowLoad (the <=6-slot pop path
and the >6-slot overflow-load path). Hoist the classification loop into
BuildArgSlotClasses(AParams, AArgs, AList); each caller still prepends its
own Self slot (overflow-load adds slot 0, the pop path handles Self via
AIntBase) and consumes the list as before.
Pure dedup: byte-identical native .s verified on a method-call program
exercising float args, integer overflow (>6 slots), open-array, and
interface params. All four fixpoints and the full suite (QBE-built and
native-built test runners) unchanged.