Interface vars become fat pointers: locals a contiguous 16-byte slot
(obj@+0, itab@+8); globals two .data labels Name_obj/Name_itab, matching
the QBE backend's $Name_obj/$Name_itab. AddSlot reserves 16 bytes for
tyInterface; IntfObjOperand/IntfItabOperand return the two halves for
locals (rbp-relative) or globals (RIP labels).
EmitInterfaceCall lowers a dispatch: push args, load obj->%r10 and
itab->%rax, index the itab by MethodIndex*8 (a flat method-pointer array,
no leading typeinfo slot unlike a vtable), load the code ptr->%r11, pop
args into %rsi.. (shifted for Self in %rdi), then callq *%r11. Wired into
EmitExprToEax (TFieldAccessExpr.IsInterfaceCall = zero-arg),
EmitMethodCallExpr and EmitMethodCallStmt (ResolvedClassType = tyInterface,
with args).
EmitInterfaceAssign handles four RHS forms, strong refs only: := nil
(release obj, zero both slots); class->interface (static itab_Class_Intf,
addref/release obj); interface->interface copy (load src obj+itab,
addref/release, store both); T as IFoo (_GetItab(obj, typeinfo_Intf); nil
result -> _Raise_InvalidCast; else addref/release/store). ARC operands are
kept on the stack across the AddRef/Release calls, so no callee-saved
register is relied upon.
EmitInterfaceDefs emits typeinfo_<Intf> (.quad 0 identity token), flat
itab_<Class>_<Intf> method-pointer arrays (abstract slots ->
_AbstractMethodError), and NULL-terminated impllist_<Class> (typeinfo,itab)
pairs — mirrors the QBE EmitInterfaceDefs. Run after EmitClassSection so the
method symbols and class-name strings it references exist. EmitDataSection
emits Name_obj/Name_itab for interface globals; the function prologue
zero-inits string and interface locals (both fat-pointer halves) so the
first assignment's release-old step sees nil; the epilogue releases
interface locals via _ClassRelease on the obj slot.
6 new AssertRunsOnBoth e2e tests (ZeroArg, Arg, Proc, IntfToIntfCopy,
AsCast, NilClear) — all pass on QBE and native; valgrind clean on the
as-cast program. FIXPOINT_OK.
Weak interface refs (_WeakAssign/_WeakClear) deferred. Pointer size is the
literal 8 here, matching the rest of this x86_64-only unit; i386/arm64 will
be separate TNativeBackend subclasses.
By-value interface params were never retained on entry or released on exit:
the QBE entry/exit ARC loops only handled tyString and tyClass. A callee
could therefore drop the caller's sole reference mid-call and then use the
param as a freed object. Add a tyInterface branch to all four loops,
AddRef/Release on the object slot of the fat pointer (the itab is static).
const and var interface params correctly emit no retain/release — the
const skip is the IsConstParam guard, and var params never reached the
class/interface branches.
Two IR unit tests (value param retained; const/var not) and one e2e
Valgrind test that drops the caller's reference mid-call and reuses the
param, proving the retain prevents a use-after-free and the release
balances it.
The native x86_64 backend still does not retain value params at all; its
TODO is updated to cover interfaces alongside string/class.
A const parameter guarantees the caller keeps the argument alive for the
whole call, so the callee-side _StringAddRef/_StringRelease and
_ClassAddRef/_ClassRelease pair is redundant. Skip it in all four ARC
entry/exit loops (method + standalone routine) by adding IsConstParam to
the existing IsVarParam/IsOpenArray guard.
The native x86_64 backend does not yet retain/release value params at all,
so there is nothing to elide there today; a TODO marks that it must respect
IsConstParam when that retain/release is added.
Covered by two IR unit tests and one e2e Valgrind test confirming the
elision is balanced (no leak, no use-after-free).
fixpoint.sh's runtime build (step 1) relied on the runtime Makefile's default
BLAISE=../compiler/target/blaise. After scripts/rolling-bootstrap.sh — which
installs only to releases/ and leaves the live compiler/target/ empty — that
path does not exist, so `cd runtime && make` failed with
"compiler/target/blaise: No such file or directory" (RUNTIME_FAIL) even though
the release binary was perfectly usable.
Pass BLAISE=<abs stage-1 path> into both the runtime `make` and `make install`
so the build never depends on a pre-existing compiler/target/blaise. Resolve
the RTL archive into $RTL_ARCHIVE once (installed compiler/target copy, falling
back to runtime/target/), and use it at both link sites.
Verified by running the patched script from a fresh clone with no
compiler/target/ present (the exact post-rolling-bootstrap state): reaches
FIXPOINT_OK where it previously aborted at step 1.
cdecl/stdcall/register/pascal/safecall directives were recognised by the
parser but silently discarded. Record them on TMethodDecl.CallingConv (both
the standalone-routine and class-method directive loops), copy through
CloneMethodDecl, propagate into TRoutineSig.CallingConv in BuildRoutineSig,
and persist in the free-routine .bif format (appended per routine record,
symmetric writer/reader) so the convention survives separate compilation.
Codegen is unchanged: every routine still emits the System V AMD64 convention
(which is the C ABI on Linux x86_64, so cdecl and the default already agree).
The directive is metadata only — the prerequisite for a future Windows/x86
target where stdcall and cdecl differ, and for faithful FFI/debugger tooling.
Replace the pending-placeholder TestCallingConv_Cdecl_Preserved with a real
assertion that 'procedure Beep; cdecl;' yields CallingConv='cdecl'.
Grammar and rationale updated: MethodDirective notes the retained conventions
and a new rationale subsection records the metadata-only decision.
The compiler test suite is now fully green (2518 tests, 0 failures).
'out' was parsed as a synonym for 'var' — by-reference, but indistinguishable
from 'var' afterwards. Add TMethodParam.IsOutParam, set alongside IsVarParam
when the 'out' keyword is present, and carry it through CloneMethodParam and
the .bif param-flags pack (new bit 3) so the loader's TRoutineSig and any
future tooling can recover the declared mode.
Codegen is unchanged: 'out' still lowers identically to 'var' (a pointer
parameter). The flag is metadata only — the prerequisite for a future
read-before-write lint and/or zero-on-entry semantics.
Replace the pending-placeholder TestParam_ModeOut_Preserved with a real
assertion that 'out' yields IsOutParam=True, IsVarParam=True, IsConstParam=False.
Grammar and rationale updated to match: ParamGroup gains the OUT alternative
and the out-parameter section documents the preserved-metadata decision.
In --debug mode EmitExpr registers each newly allocated instance with the leak
tracker by loading the class-name pointer from $typeinfo_<Class>+16. Both
register sites built that symbol with a bare QBEMangle(Name), omitting the
prefix that ClassSymName applies. The typeinfo data itself is defined as
$typeinfo_<prefix><Class> (e.g. $typeinfo_P_TBox for a program-scope class),
so the reference resolved to an undefined $typeinfo_TBox and every --debug
build failed to link.
Route both leak-tracker references through ClassSymName(QBEMangle(Name)), the
same form the adjacent _ClassAlloc and vtable-store lines already use.
Fixes all six TE2ELeakCheckTests, which compile and run real --debug binaries
and so exercise the link step the IR-only harness cannot see.
EmitForInStmt built the GetEnumerator/MoveNext/GetCurrent call names with a
bare QBEMangle(OwnerTypeName + '_' + Name), omitting any unit prefix. For an
enumerable class defined in a unit (e.g. TStringList in Classes) the method
def is emitted as classes_TStringList_GetEnumerator while the for-in call site
emitted TStringList_GetEnumerator, yielding undefined-reference link errors.
Route all three calls through MethodEmitName, which prefers the method's
ResolvedQbeName and otherwise falls back to ClassUnitPrefix-qualified names —
the same resolution used by ordinary method calls.
Covered by the existing e2e test TE2ETStringListTests.TestRun_ForIn, which
iterates a real Classes.TStringList; the IR-only harness compiles a single
program and cannot model a unit-defined enumerable, so the e2e layer is the
correct guard here.
InstantiateGeneric mangled each generic-instance method's ResolvedQbeName
with MangleUnitPrefix(Sym.OwningUnit). For program-scope generics OwningUnit
is the program name (e.g. 'P'), yielding a spurious 'P_' prefix. The method
def and itab then used '$P_TBox_Integer_SetValue' while the property
setter/getter call path used ClassUnitPrefix (which returns '' for program
scope), producing 'undefined reference' link errors.
Use CurrentUnitPrefix instead: '' for programs, the mangled unit prefix for
units — consistent with the property-call path on both sides.
Also pin the generic instance's DestroyResolvedQbeName to the same prefix as
the method def, so the emitted $_FieldCleanup_<T> calls the destructor symbol
that actually exists. Previously it was left empty and the cleanup fell back
to ClassUnitPrefix(), which disagreed for program-scope generics and broke
the TList<T> e2e link tests.
Update the IR-level test assertions across the generic suites to expect the
unprefixed symbol names, and cp.test.unitinterface.pas to expect the unit
prefix for unit-scope classes.
Now that the compiler supports `blaise --source Unit.pas --output Unit.o`
directly, the build-driver pattern (a thin program wrapper + IR sed strip)
is no longer needed. Replace every multi-step rule with a single invocation
and delete the eight *_build_driver.pas files.
This commit must follow the merge of blaise_clean_split in git history so
that the rolling-bootstrap script can build the merge commit using the
previous binary (which still uses the old Makefile), then build this commit
using the freshly-merged binary (which understands unit-as-top-level).
The Write* helpers in uUnitInterfaceIO.pas all build their output as
TStringList instances and serialise via SL.Text. That's quadratic
under the covers (every Add reallocates the joined view) and ties the
exact line ending to whatever TStringList chooses. Switch the
accumulator to stdlib TStringBuilder.AppendLine, which appends #10
explicitly and avoids the join cost on every Add.
No on-disk format change — the wire output is byte-identical because
both paths emit #10-separated records. Self-bootstrap and TestRunner
(2346 tests) pass.
Spawn the actual blaise binary end-to-end:
1. blaise --source MyDep.pas --output MyDep.o
-> ELF .o with .blaise.iface embedded
2. MyDep.pas removed from disk
3. blaise --source use_mydep.pas --output use_mydep --unit-path <dir>
-> auto-discovers MyDep.o, reads the iface, links
4. ./use_mydep prints the expected line
Documents the headline feature in the main test gate. Anything that
needs the source TUnit at codegen time (currently: generic and inline
bodies) is still covered by cp.test.unitinterface; this test focuses
on the free-routine round-trip which already works.
The TUnitInterface and import/export tests lived in a parallel
loader-testrunner/ project so they could iterate without churning
TestRunner.pas during development. Now that the loader interfaces
have stabilised, move them into the main test gate alongside the
rest of cp.test.*.
Drops the standalone loader-testrunner/ project entirely and adds
cp.test.unitinterface to TestRunner.pas's uses clause. Also fixes
a stale reference in the test file (TUnitInterface.CompilerVersion
was renamed to CompilerId by the validation-header commit before the
file moved into the gate).
TestRunner reports OK (2344 tests) — 2252 from the existing suite
plus 92 newly-gated loader tests.
Drop the per-routine ResolvedQbeName field from the .bif ROUT block
now that the uses-chain rewrite tracks OwningUnit on every
TMethodDecl. SynthesiseMethodDecl takes the iface's unit name as a
new parameter and computes ResolvedQbeName := MangleUnitPrefix(...) +
ASig.Name directly, so import and codegen agree on the same global
symbol the exporting unit defines without round-tripping the name
through the on-disk format.
Expose MangleUnitPrefix in uSemantic's interface section so
uSemanticImport can call it without touching internals.
Net effect on .bif: one fewer Lpstr per free routine, no behaviour
change. TestRunner 2252/0; separate-compile end-to-end check
(MyDep.pas -> built/MyDep.o, then build a program against built/ with
source hidden) prints the expected output.
Regression test for task #44 step 10. Three programs with no
`uses` clause must still resolve WriteLn / IntToStr / Length —
proving the implicit `System` prepend in BuildUsesChain produces
a reachable chain entry, not just a name in a list.
- SrcWriteLnInt: WriteLn(42) → '42\n'
- SrcIntToStr: IntToStr(123) → '123\n'
- SrcLength: Length('hello') → '5\n'
Each shells out to the freshly built blaise compiler via the
test framework's CompileAndRun. TestRunner: 2245 → 2248 (+3),
zero failures. Fixpoint OK, loader-testrunner 92/92.
Add FUnitSymbols: TStringList on TSemanticAnalyser keyed by
'UnitName' + #1 + 'SymbolName', Objects[I] = TSymbol (non-owning;
lifetime stays with FTable's global scope today). Populated by
RegisterUnitIface walking FTable's global scope after each import
or AnalyseUnitForExport completes and absorbing every symbol whose
OwningUnit matches the unit being registered.
LookupViaUsesChain now queries the per-unit cache directly instead
of doing a flat FTable.Lookup and filtering by OwningUnit, with the
old path retained as a fallback for the in-flight unit whose
absorption hasn't run yet.
This is the data substrate that lets two units export same-named
symbols once the flat-merge duplicate-check is later relaxed — the
chain walker no longer collapses on the global table. The flat
merge itself stays in place as a fallback / for the current-unit-
being-analysed; full removal is a follow-up once import-time
chain context is wired so cross-unit type refs can resolve through
the chain rather than via flat-merged FTable.Lookup.
Adds TScope.SymbolCount/SymbolAt accessors and TSymbolTable.GlobalScope
so the analyser can iterate scope 0 without threading itself
through uSemanticImport helpers.
TestRunner 2249/0, fixpoint OK, loader-testrunner 92/92.
Add OwningUnit: string to TMethodDecl in uAST. Auto-populate via a
new TSemanticAnalyser.RegisterProcDecl wrapper that sets the field
from FCurrentUnitName when not already set, then forwards to
FProcIndex.AddObject.
Migrate the six source-side AddObject call sites (interface section,
impl-section overload extension, AnalyseUnit's two equivalents,
generic instantiation, AnalyseStandaloneDecl) through the wrapper.
The seventh site is RegisterImportedRoutine — kept direct because
uSemanticImport already pre-sets MDecl.OwningUnit to the iface name
before calling, and FCurrentUnitName is not reliable during import.
Sets a tag for cross-unit prefix mangling (#43) and for the per-unit
FProcIndex partitioning that lands as part of step 9. No observable
behavior change today since no test exercises cross-unit name
duplicates. TestRunner 2249/0, fixpoint OK, loader-testrunner 92/92.
TSymbolTable.Lookup now walks resolution layers in canonical Object
Pascal order:
1. Pushed scopes (locals, params, nested-routine outer locals)
2. Class context — still handled by caller-side fallback
(FCurrentClass.FindField) because TFieldInfo isn't a TSymbol;
promotion lands with the step-9 refactor of caller flow
3. Current compilation unit's own symbols
4. Uses chain, right-to-left
5. System — prepended to the chain at index 0, falls out of layer 4
6. True global — builtins and (today) any flat-merged residue
Layer 3 fires when a scope-0 hit's OwningUnit matches the unit/program
currently being analysed. Layer 6 returns whatever scope 0 produced
as the bottom-of-chain fallback so builtins (OwningUnit='') remain
reachable.
Also tag program globals with the program's name in Analyse() so
layer 3 also activates during program compilation, not just unit-
for-export analysis.
No observable change today since flat-merge errors prevent
cross-unit name collisions; once step 9 removes the flat merge,
this ordering becomes load-bearing. TestRunner 2249/0, fixpoint OK,
loader-testrunner 92/92.
TSymbolTable.Lookup now walks scopes in three layers:
1. Pushed scopes (1..N): locals, params, for-loop vars — these
don't carry OwningUnit and aren't subject to uses-chain
filtering.
2. Uses-chain provider: TSymbolTable.UsesChainProvider, set by
the analyser to itself, walks FCurrentUsesChain right-to-left
and returns the first visible hit.
3. Global scope (index 0): language builtins (Length, IntToStr,
…) registered by RegisterBuiltins plus today's flat-merged
unit imports (the redundancy goes away in step 9).
The chain provider is wired through a new abstract base class,
TUsesChainProvider, that TSemanticAnalyser inherits and overrides.
A method-pointer-of-object hook would have been more Delphi-idiomatic
but @MethodName produces a plain function pointer in Blaise; the
abstract base sidesteps that.
A FBypassUsesChain flag breaks the recursion when the chain walker
itself calls Lookup to retrieve the canonical TSymbol from the flat
FTable.
FindType also now routes through Lookup() so type-name resolution
participates in the chain.
No breakage in the gate as predicted: today's flat FTable holds
exactly one TSymbol per name (the analyser already errors on
duplicates), so the chain and the global agree on every hit. The
visible-vs-leaking distinction only starts mattering once step 9
removes the flat merge. TestRunner 2249/0, fixpoint OK,
loader-testrunner 92/92.
Add the structural seam for class-member access control:
- IsVisibleFromUnit overload taking a member's owning-unit
string directly (avoids synthesising a TSymbol at sites
where we have a TMethodDecl/TFieldInfo).
- AssertMemberVisible(OwningUnit, ClassContext, MemberName,
Line, Col): hard-error wrapper that raises ESemanticError
on a False filter result. This embodies the
project_per_unit_visibility.md rule: an invisible *qualified*
member is a hard error, not a fall-through to a same-named
free symbol elsewhere.
Wire one canonical call site at FindMethodDecl using the class
type's OwningUnit as the member's effective owner. Until
TMethodDecl gains a per-member OwningUnit, members of class TFoo
are gated at the same boundary as TFoo itself — which is the
right semantics for the unit-scope private/protected work that
will follow.
Filter still returns True so this is a no-op today; the seam is
the deliverable. TestRunner 2249/0, fixpoint OK,
loader-testrunner 92/92.
Add the uses-chain lookup helper for unqualified identifiers:
- TUnitInterface.HasSymbol(AName): Boolean
Probe across types/consts/vars/routines/generic bodies and
walks enum defs for member names (each enum member is its
own top-level identifier, matching what uSemanticImport
publishes).
- TSemanticAnalyser.LookupViaUsesChain(AName): TSymbol
Walks FCurrentUsesChain right-to-left ("last in uses wins").
For each unit whose iface exports AName, retrieves the
canonical TSymbol from FTable and applies IsVisibleFromUnit.
Returns the first visible hit, or nil.
Also: BuildUsesChain now prepends the implicit `System` unit (every
unit's effective `uses` is "Uses System(hidden), Classes;"), except
when the unit being analysed IS System — a unit cannot use itself.
User-written `uses System` (case-insensitive) deduplicates so the
right-to-left walk doesn't shadow the implicit entry.
No call sites consult LookupViaUsesChain yet — the behavioural
switch happens in step 7 when TSymbolTable.Lookup is rewired. No
breakage expected even then, since today's compiler errors on name
duplicates so no working test code holds a cross-unit conflict.
TestRunner 2249/0, fixpoint OK, loader-testrunner 92/92.
Add the 3-arg visibility chokepoint:
function TSemanticAnalyser.IsVisibleFromUnit(
ASym: TSymbol;
const AFromUnit: string;
AFromClass: TRecordTypeDesc): Boolean;
Stub returns True unconditionally — private/protected modifiers
don't exist on Blaise class members yet. When they land, this
seam plugs in without changing call sites: private gates on
(AFromUnit = ASym.OwningUnit); protected additionally accepts
AFromClass walking up ParentClass to ASym's declaring class.
The 3-arg signature matters more than the body: AFromUnit needs
no special context to populate (it's FCurrentUnitName), and
AFromClass uses TRecordTypeDesc since Blaise represents classes
as record descriptors.
No call sites yet — wired in later steps. TestRunner 2249/0,
fixpoint OK, loader-testrunner 92/92.
Add per-analyser TUnitInterface registry, keyed by unit name
(case-insensitive). Populated in Blaise.pas's existing iface paths:
- prebuilt .bif ifaces (auto-discovery from .o sections): registered
immediately after ImportUnitInterface
- source-compiled deps: registered after ExportUnitInterface returns
the freshly-built iface
The analyser does NOT own the TUnitInterface objects — caller retains
lifetime (Loader.PrebuiltIfaces for .bif ones, UnitIfaces for the
source-built ones). Last-write-wins on duplicate names, matching
the eventual uses-chain semantics.
Plumbing only — no lookup path consults the registry yet. Adds
uUnitInterface to uSemantic's uses (no circular dep — uUnitInterface
imports uAST only). TestRunner 2249/0, fixpoint OK,
loader-testrunner 92/92.
Add per-compilation FCurrentUsesChain (TStringList, owned) to
TSemanticAnalyser, populated from Prog.UsedUnits / AUnit.UsedUnits
at the entry of Analyse() and AnalyseUnitForExport().
In Blaise, language builtins live in the global symbol table from
TSymbolTable.RegisterBuiltins (no separately-named "System" unit),
so the chain holds only the user's `uses` entries in source order;
they'll be walked right-to-left in a later step and a non-hit falls
back to the global builtins.
Pure plumbing — field populated but not yet consulted. TestRunner
2249/0, fixpoint OK, loader-testrunner 92/92.
Add TSymbolTable.DefineOwningUnit property; Define()/DefineGlobal()
auto-populate ASymbol.OwningUnit from it when (a) the symbol has no
explicit value and (b) we're at scope depth 1 (global only — locals
must stay OwningUnit='').
Wire AnalyseUnitForExport to set the property at entry and clear on
exit, so source-compiled deps tag their interface symbols the same
way uSemanticImport already tags .bif-loaded ones. Also wire
ImportUnitInterface to set+restore the property as belt-and-braces
in case a future helper grows a Define site we missed.
Net behavior change: zero. TestRunner 2249/0, fixpoint OK,
loader-testrunner 92/92.
Add an OwningUnit string to TSymbol, populated by uSemanticImport when
registering an imported unit interface (consts, vars, routines, enum
members, sets, aliases, classes, records, interfaces). Default empty
for builtins, locals, and program-level symbols.
This is plumbing only — the field is written but not yet read.
Consumed by per-unit visibility (uses-chain lookup) and by unit-prefix
mangling. Net behavior change: zero (TestRunner 2249/0, fixpoint OK,
loader-testrunner 92/92).
Captures the rule of thumb that every AST shape change must touch
three places (uAST.pas / uSemantic.pas / uUnitInterfaceIO.pas), with
codegen as a fourth when the node has runtime semantics.
Worked example walks through adding a hypothetical 'when' statement
end-to-end: parser, semantic, QBE codegen, .bif encoder/decoder, and
test surface. Plus a shorter checklist for plain field additions,
common-pitfalls section, and the rationale behind the positional
(rather than tagged) .bif format.
Add validation to the .bif loader so stale or cross-compiler-version
ifaces are rejected instead of silently producing wrong-shape symbol
tables.
.bif format v2 (bumped from v1; v1 .bifs now rejected):
- Magic 'BLAISE-IFACE' and validation header on the first line.
- META block now carries:
SourceFile (was already there)
SourceHash (NEW: FNV-1a 64 hex of source content)
CompilerId (renamed from CompilerVersion; populated now)
SourceModTime (NEW: reserved Int64, currently always 0 —
no RTL FileAge yet; populated when a
TRtlPlatform.FileAge method lands)
New unit uCompilerId.pas owns COMPILER_ID — the build-time identity
string baked into every .bif written by this compiler. Convention:
'blaise-<semver>+<short-suffix>'; bump suffix on codegen-affecting
changes. Initial value 'blaise-0.9.0-dev+6c-N'.
Load-time validation in TUnitLoader.ValidateIface, called after
LoadIfaceFromObject succeeds:
- Source .pas present on path:
hash-compare current source against AIface.SourceHash.
Match → accept iface (fast path: skip reparse).
Mismatch → discard, fall through to source-compile path.
Empty SourceHash on iface → treat as mismatch (forces
rebuild for any iface written before this format).
- Source .pas absent:
CompilerId exact match → accept.
CompilerId differs or empty → reject with a clear error
message ('compiled by X, this compiler is Y, source
unavailable to rebuild').
This is the safety net for the binary-only-deps case (RTL shipped
without source, third-party precompiled units): the only signal
the iface is safe is that the exact compiler wrote it. Any change
that bumps COMPILER_ID forces re-emit of every iface — desired
behavior whenever codegen or .bif schema changes.
Implementation notes:
- FNV-1a 64-bit content hash exposed as ContentHashFnv1a64 from
uUnitInterfaceIO; assembled from 32-bit halves because Blaise's
literal parser caps at Int64.
- Hash populated by WriteUnitInterfaceToFile (where we have the
source path on disk). Best-effort: empty hash on IO failure.
TestRunner 2248/0, fixpoint OK (stage-3/stage-4), loader-testrunner
92/92.
Cold-start completion of the separate-compilation flow. Without
this, a fresh source tree could only reach auto-discovery after a
user manually compiled each unit to a .o first. With it, the
program build does that as a side effect.
--incremental flag:
After deps are loaded + semantic-analysed (the existing flow
unchanged), but before the main codegen, walk Units and for
each one:
1. Fresh TCodeGenQBE, shared symbol table.
2. AppendUnit(U); GetOutput → per-unit IR.
3. Write IR to <unitcache>/<unit>.o.ssa.tmp.
4. WriteUnitInterfaceToFile to <unit>.o.bif.tmp.
5. CompileUnitToObject runs qbe + cc -c + objcopy embed.
6. Delete the two temps; add <unit>.o to PrebuiltObjPaths.
Then SkipDepCodegen := True so the main IR no longer carries
dep bodies — they're linked from the per-unit .o instead.
--unit-cache <dir> picks the per-unit .o directory. Default:
ExtractFilePath(OutputFile), so the .o lands alongside the main
output.
Cold-start dogfood (clean /tmp/incdemo):
$ blaise --source use_mydep.pas --output use_mydep \
--unit-path . --incremental
$ ls
mydep.o MyDep.pas use_mydep use_mydep.pas
$ ./use_mydep
21
$ rm MyDep.pas
$ blaise --source use_mydep.pas --output use_mydep \
--unit-path . # no --incremental needed
$ ./use_mydep # auto-discovered mydep.o
21
Note: --incremental implies the codegen-side dep skip, so the
main IR shape changes from the default (now smaller). Existing
non-incremental builds are unaffected; the default path inlines
dep bodies as before.
Pre-commit gate:
- mcp compile: OK
- TestRunner: 2249 tests, 2 pre-existing failures only
- fixpoint.sh: FIXPOINT_OK at stage-3/stage-4
- loader-testrunner: 92 tests, all green
The .o and its .bif now travel together: every unit-mode compile
embeds the TUnitInterface bytes into a non-loaded section of the
output .o, and the loader picks them up automatically when it
encounters a unit whose source is unavailable but whose .o sits
on the search path.
This eliminates the "mismatched .bif/.o pair on disk" failure
mode — the iface is *inside* the .o, you can't desync them by
copying one without the other.
New unit uIfaceObject:
- TObjectFormat enum (ofELF today, ofPECOFF placeholder).
- EmbedBifInObject / ExtractBifFromObject / LoadEmbeddedBifString
shell out to objcopy --add-section / --dump-section with
section names per format ('.blaise.iface' on ELF, '.bliface'
on PE/COFF when we cross that bridge). Section is flagged
'noload,readonly' so ld drops it at link time and it never
reaches the executable.
Blaise.pas (unit-mode compile):
- Always exports the iface to a sibling .bif.tmp regardless of
--emit-iface, so CompileUnitToObject can embed it.
- CompileUnitToObject gains an ABifFile parameter and calls
EmbedBifInObject after cc -c finishes.
uUnitLoader (auto-discovery):
- New LocateObject(AName) — scans search paths for <name>.o
(lowercase first, exact-case fallback).
- New LoadIfaceFromObject(APath) — extracts the iface section
and runs ReadUnitInterface on the bytes. Malformed iface
surfaces a warning and falls back to the .pas.
- LoadTransitive prefers a .o with embedded iface over the
.pas source. When found, the dep is added to PrebuiltIfaces
(with paths in PrebuiltObjectPaths) and the .pas is *not*
parsed. Recursion walks the iface's UsedUnits.
- Two new public properties: PrebuiltIfaces, PrebuiltObjectPaths.
Blaise.pas (consumer side):
- After LoadAll, walks Loader.PrebuiltIfaces and calls
ImportUnitInterface(iface, table, Semantic) — the new
ASemantic arg is the next change.
- PrebuiltObjPaths captured off the loader before Loader.Free
so the post-finally link-step dispatch can pass them to
CompileToNative.
- CompileToNative gains an optional AExtraObjects: TStringList
that gets appended to the cc command line.
uSemanticImport:
- ImportUnitInterface signature grows an optional ASemantic
parameter. When non-nil, RegisterRoutines synthesises a
TMethodDecl per free routine (carries Name +
ResolvedReturnType + cloned Params with ResolvedType +
ResolvedQbeName) and registers it via
ASemantic.RegisterImportedRoutine — needed because
AnalyseFuncCall looks up callees in FProcIndex rather than
going through the symbol table. The synthesised decls are
owned by ATable.OwnImportedDecl.
uSemantic:
- New public RegisterImportedRoutine(AName, ADecl): pushes
into the previously-private FProcIndex.
uSymbolTable:
- New FImportedDecls: TObjectList owning synthesised method
decls + OwnImportedDecl(ADecl) helper.
End-to-end auto-discovery verified:
$ blaise --source MyDep.pas --output mydep.o # embeds iface
$ rm /tmp/MyDep.pas # hide source
$ blaise --source UseMyDep.pas --unit-path /tmp \
--output /tmp/use_mydep
$ /tmp/use_mydep
21
Pre-commit gate:
- mcp compile: OK
- TestRunner: 2249 tests, 2 pre-existing failures only
- fixpoint.sh: FIXPOINT_OK at stage-3/stage-4
- loader-testrunner: 92 tests, all green
First on-disk artifact: Blaise.pas, when invoked with
--emit-iface <DIR>, writes each dep unit's TUnitInterface to
<DIR>/<unitname>.bif (lowercased) right after ExportUnitInterface
builds it.
Dogfood: compiling a program that 'uses SysUtils' against the
real stdlib produces a 539-byte sysutils.bif carrying the
Exception class hierarchy, PathDelim const, and BoolToStr +
ExpandFileName signatures. Spot-checked the file by eye — same
format the TIfaceIOTests round-trip in-memory.
ParseArgs gains an EmitIfaceDir out-param (default '' = off).
Usage line added. No change to existing semantics: the flag is
purely additive and the compile pipeline still runs codegen +
link as before.
This is the *write* half of separate compilation. The read half —
loading a .bif from disk in place of parsing + analysing the
source — is task #31's substitutive piece and remains future
work; codegen still walks the source TUnit list, so freeing the
deps in the live driver would crash.
Pre-commit gate:
- mcp compile: OK
- TestRunner: 2249 tests, 2 pre-existing failures only
- fixpoint.sh: FIXPOINT_OK at stage-3/stage-4
- loader-testrunner: 85 tests, all green
Final wire-format piece. TUnitInterface.InlineBodies is a list of
(RoutineName, Block) emitted for every interface-section routine
marked 'inline'. Writer emits an INLINE block per entry; reader
reconstitutes TInlineBody and pushes onto AIface.InlineBodies via
AddInlineBody (which also indexes by routine name for the
existing FindInlineBody lookup).
Block payload uses the AST body serialiser from the previous
commit — same EncodeBlock / ReadBlock pair the GENROUT block
uses.
One new test: 'function Square(N: Integer): Integer; inline;'
survives parse → analyse → export → write → read with the body
attached and findable via Round.FindInlineBody('Square').
The importer (uSemanticImport.ImportUnitInterface) does NOT yet
consume InlineBodies — the wire side stores the data but
ImportUnitInterface stops short of attaching it to the imported
routine's Sym.Decl. Wiring that up is the next consumer-side
piece; the disk format now carries everything ImportUnitInterface
would ever need.
Pre-commit gate:
- mcp compile: OK
- TestRunner: 2249 tests, 2 pre-existing failures only
- fixpoint.sh: FIXPOINT_OK at stage-3/stage-4
- loader-testrunner: 92 tests, all green
Inlines TBlock / TASTStmt / TASTExpr trees in the wire format,
unblocking GENROUT bodies through the disk path. Every node
parsed by CloneStmt / CloneExpr in uAST is covered:
Expressions (17): int, float, str, nillit, id, bin (with op
ordinal), not, call, mcall, field, deref, addr, ssub, alit,
isop, asop, supp.
Statements (18): asn, comp, if, while, rep, for, forin, tryfin,
tryex (with handler list), raise, exit, brk, cont, case (with
branch list), fasn, ssasn, pw, pcall, mcs, inh.
Blocks (TBlock): a 'block' kind tag followed by the flattened
Stmts list. Local var / const / type / proc decls inside a
block are intentionally not serialised yet — generic routine
bodies in practice don't carry nested type or proc decls; add
later if a real workload needs them.
nil children encoded as a single 'nil' lpstr and the readers
return nil straight back. TBinaryExpr.Op is encoded as the
TBinaryOp ordinal — keeps the format stable across re-ordering
the enum case constants (anything new added needs a version
bump anyway).
GENROUT block now also writes G.MethodDecl.Body and the reader
restores MD.Body with OwnBody=True so the importer downstream
can clone it for instantiation.
Test:
- 'function Identity<T>(V: T): T; begin Result := V; end;'
survives the disk path with the assignment body intact —
walks the (possibly TCompoundStmt-wrapped) block, recovers
the TAssignment with LHS 'Result' and RHS TIdentExpr 'V'.
Out of scope (next):
- Inline bodies (TUnitInterface.InlineBodies). The wire side
of this is mechanical — same EncodeBlock — but the importer
doesn't consume them through ImportUnitInterface yet either.
- Local var/const/type decls inside a TBlock.
Pre-commit gate:
- mcp compile: OK
- TestRunner: 2249 tests, 2 pre-existing failures only
- fixpoint.sh: FIXPOINT_OK at stage-3/stage-4
- loader-testrunner: 91 tests, all green
Three related changes that together let WriteUnitInterface →
ReadUnitInterface → ImportUnitInterface end-to-end produce a
TSymbolTable equivalent to the source-build path for everything
the format covers.
1. GENROUT block: serialises generic free routines. Generic
class/interface templates already ride through the TYPE block
('generic-class' / 'generic-interface' kinds) — emitting them
in GENROUT too would double-register. GENROUT carries
<name><type-param-list><method-decl>; the MethodDecl payload
omits Body for now (AST body serialiser is future work) and
the reader copies TypeParams back onto the rebuilt MethodDecl
so a downstream instantiation sees a full template shape.
2. Enum-ordinal fidelity: TEnumTypeDef.AddMember stashes the
explicit ordinal in Members.Objects[i] as a pointer-cast int —
OrdinalAt reads it back. The naive name-only encoding I had
came back with ordinal 0 for every member. Switched to
'name=ord' pairs joined by ',' and a new LoadEnumMembers helper
that splits + calls AddMember with the explicit value. Bare
names without '=' are tolerated as a sequential fallback (for
hand-written .bif testing).
3. Five new disk-path tests verifying the full pipeline against
ImportUnitInterface for: const, enum (ordinals!), class
(vtable slots intact), generic-class template, generic-routine
template. These exercise WriteUnitInterface → ReadUnitInterface
→ ImportUnitInterface in sequence rather than the in-memory
shortcut.
Pre-commit gate:
- mcp compile: OK
- TestRunner: 2249 tests, 2 pre-existing failures only
- fixpoint.sh: FIXPOINT_OK at stage-3/stage-4
- loader-testrunner: 90 tests, all green
Two new TYPE kinds — 'generic-class' for TGenericTypeDef and
'generic-interface' for TGenericInterfaceDef.
Payload shape (shared shape, divergent body):
<type-param-list> = <count>(<name lpstr><constraint lpstr>)*
generic-class: <type-param-list> + <inner-class-payload>
where the inner class part is identical to the regular 'class'
payload — uSemanticExport runs PopulateClassEntry on the
template's inner ClassDef, so the AEntry-level ParentClass /
Implements / Attributes / Methods are already populated.
InstanceSize stays 0 (no concrete layout until instantiation).
generic-interface: <type-param-list> + <ParentName lpstr> +
<method-decl-list>
Inner IntfDef.Methods is the source of truth, matching
uSemanticImport.RegisterInterface which walks the same list.
Reader rebuilds:
- TGenericTypeDef with the auto-created blank ClassDef freed
and a freshly populated one substituted; sets AEntry.IsGeneric.
- TGenericInterfaceDef likewise for the IntfDef; sets
AEntry.IsGeneric.
Two new tests:
- TBox<T> = class V: T; end → IsGeneric, ParamNames=['T'],
inner ClassDef carries the one field.
- IBox<T> = interface function Get: T; end → IsGeneric,
ParamNames=['T'], inner IntfDef has the one method.
Still out of scope: inline + generic *bodies* (TBlock AST trees).
Those need a statement/expression serialiser which is a much
bigger undertaking — covered by 'no AST body serializer yet'
elsewhere in the loader notes.
Pre-commit gate:
- mcp compile: OK
- TestRunner: 2249 tests, 2 pre-existing failures only
- fixpoint.sh: FIXPOINT_OK at stage-3/stage-4
- loader-testrunner: 85 tests, all green
Two small additions that close out the non-generic, non-body
surface of the wire format.
TYPE block: new 'proc' kind for TProceduralTypeDef. Payload:
<IsFunction bool><IsMethodPtr bool><ReturnTypeName lpstr>
<param-count>(<pname><ptype><flags>)*
META block (new, sits between the unit name and TYPE):
META\n
<SourceFile lpstr><SourceHash lpstr><CompilerVersion lpstr>
<UsedUnits string-list>
END\n
SourceHash + CompilerVersion are still empty strings at construction
time (reserved fields per the original TUnitInterface header
comment); the metadata block ensures they survive round-trip so a
later patch that starts populating them lights up immediately.
Two new tests:
- 'TCallback = procedure(N: Integer) of object' round-trips:
IsFunction=false, IsMethodPtr=true, 1 param.
- Hand-built interface with SourceFile + SourceHash +
CompilerVersion + 2 UsedUnits ('SysUtils', 'Classes') survives
write+read with all fields exact.
Pre-commit gate:
- mcp compile: OK
- TestRunner: 2249 tests, 2 pre-existing failures only
- fixpoint.sh: FIXPOINT_OK at stage-3/stage-4
- loader-testrunner: 83 tests, all green
Extends the TYPE block with 'record', 'class', and 'interface'
kinds, completing the simple-shape coverage of the wire format.
Record payload:
<IsPacked bool><field-list>
Class payload:
<parent-qualref><instance-size lpstr>
<attribute string-list><implements string-list>
<field-list><method-list>
Interface payload:
<ParentName lpstr><method-decl-list>
Helpers:
* EncodeCount / DecodeCount — uniform stringified-int over lpstr,
keeps the reader primitive set to ReadLpstrAt only.
* EncodeBool / DecodeBool — '1'/'0' single-char lpstrs.
* EncodeStringList / ReadStringListBlock — count + lpstrs.
* EncodeFieldList / ReadFieldList — flattens multi-name decls
('X, Y: Integer') to one TFieldDecl per name on the wire,
matching the way symbol-table AddField is called per-name.
* EncodeMethodSig / ReadMethodSig — for class methods on the
TTypeEntry.Methods side; carries VTableSlot, ResolvedQbeName,
IsVirtual, IsOverride.
* EncodeMethodDecl / ReadMethodDecl — for interface methods on
the Def.Methods (AST) side; keeps the writer and the importer
(uSemanticImport.RegisterInterface, which walks Def.Methods)
symmetric.
Three new tests, all green:
- TPoint record: 2 fields preserved (flattened from 'X, Y: Integer').
- TFoo class with virtual Speak: parent-empty sentinel
round-trips, InstanceSize > 0, ResolvedQbeName 'TFoo_Speak'
and VTableSlot >= 0 recovered.
- IGreeter interface: two methods round-trip on Def.Methods.
Out of scope (next):
- Procedural-type kind (rare, easy add).
- Generic types — both 'generic-class' and 'generic-interface'
payloads need template parameter lists.
- Inline + generic bodies (require AST block serialiser — large).
- UsedUnits + SourceHash + CompilerVersion metadata.
Pre-commit gate:
- mcp compile: OK
- TestRunner: 2249 tests, 2 pre-existing failures only
- fixpoint.sh: FIXPOINT_OK at stage-3/stage-4
- loader-testrunner: 81 tests, all green
Extends the 6c-E text wire format with VAR, TYPE (enum/set/alias),
and ROUT record blocks, plus thin TStringList-based file wrappers.
VAR block:
VAR <count>\n
<name lpstr><typeref lpstr>
...
END
TYPE block:
TYPE <count>\n
<kind lpstr><name lpstr><payload lpstr>
...
END
Where kind is 'enum' / 'set' / 'alias'. Payload encoding:
- enum: comma-joined member names
- set: base enum type name
- alias: target type name
Record / class / interface / procedural / generic kinds emit
nothing yet (TypeEntryKind returns '' and the writer skips them).
Those need richer payloads (fields with offsets, vtable slots,
attribute lists) and land in follow-up commits.
ROUT block:
ROUT <count>\n
<name><isfn 0/1><return-typeref><paramcount>(<pname><ptype><flags>)*
END
Param flags pack IsVar/IsConst/IsOpenArray as a 3-bit lpstr.
DefaultValue expressions are intentionally not serialised — that
would require an AST writer.
File API:
WriteUnitInterfaceToFile / ReadUnitInterfaceFromFile route the
string-based serializer through TStringList.SaveToFile /
LoadFromFile so platform line-ending behaviour matches the rest
of the compiler's I/O.
Two new tests on top of the existing six:
- real-source pipeline round-trip: parse + analyse + export +
write + read recovers consts, vars, all three simple types,
and the function signature.
- file round-trip via /tmp.
Pre-commit gate:
- mcp compile: OK
- TestRunner: 2249 tests, 2 pre-existing failures only
- fixpoint.sh: FIXPOINT_OK at stage-3/stage-4
- loader-testrunner: 78 tests, all green
Phase 6c-E starter — wire format for separate-compilation .bif (or
similar) artifacts. Lays the plumbing for disk-cached unit
interfaces; this commit covers const records only, the rest is
mechanical extension.
Format (v1):
BLAISE-IFACE 1\n
<unit-name lpstr>\n
CONST <count>\n
<name lpstr><typeref lpstr><int64 lpstr><strval lpstr><flag byte>
...
END\n
Where lpstr = '<decimal-len>:' + <len raw bytes>. Length-driven
consumption sidesteps escaping — newlines and ':' bytes pass
through cleanly, which the awkward-chars test exercises.
TypeRef is rendered as 'UnitName.TypeName' inside an lpstr so the
'$builtin' / '' sentinels survive without special-case parsing.
Text not binary, by design: easier to diff during development, no
endianness concerns, no bit-twiddling. When the layout stabilises
we can re-encode as a compact binary form without changing the
public surface.
API: WriteUnitInterface(AIface): string and
ReadUnitInterface(AText): TUnitInterface; EIfaceFormatError on
malformed input or version mismatch.
Six tests, all green:
- magic + version prefix on output
- unit name round-trip
- int-const round-trip (value, type ref)
- string-const round-trip with embedded newlines + colons
- empty interface round-trip
- version mismatch raises EIfaceFormatError
Two Blaise compiler quirks worked around in this unit (memory
notes added/applicable):
- Strings are 0-indexed everywhere; Pos returns 0-based offset,
-1 for not found; Copy treats start as 0-based. Cursor
arithmetic was rewritten accordingly mid-implementation.
- Routines that take a multi-string record as a const param and
read its fields segfault (project_record_const_param_crash.md).
EncodeQualRef and DecodeQualRef therefore take the unit-name
and type-name strings separately.
Out of scope (next):
- Vars, types (enums / sets / aliases / records / classes /
interfaces), routines, inline bodies, generic bodies.
- File I/O wrappers (Read/WriteFromFile).
- SourceHash + CompilerVersion metadata.
Pre-commit gate:
- mcp compile: OK
- TestRunner: 2249 tests, 2 pre-existing failures only
- fixpoint.sh: FIXPOINT_OK at stage-3/stage-4
- loader-testrunner: 76 tests, all green
Closes the last documented pending: ExportUnitInterface now emits
one TVarEntry per name declared in an interface-section var block
(walking AUnit.IntfBlock.Decls). Multi-name declarations like
'X, Y: Integer' expand into one TVarEntry per name, matching how
AnalyseUnitForExport registers them as separate skVariable
symbols.
Re-activates the pending TImportRoundTripTests.TestImport_GlobalVar_
MarkedIsGlobal — Counter declared in a dep's interface is now
recoverable from the imported FTable as an IsGlobal skVariable of
type Integer.
Pre-commit gate:
- mcp compile: OK
- TestRunner: 2249 tests, 2 pre-existing failures only
- fixpoint.sh: FIXPOINT_OK at stage-3/stage-4
- loader-testrunner: 70 tests, all green (no pendings)
Adds the most decisive proof of the 6c work to date: a main program
can be semantically analysed against a TSymbolTable that was
populated by ImportUnitInterface alone, after the dep unit's source
TUnit has been freed.
Two new TImportRoundTripTests:
- dep exports a const, source freed, main program uses the const
in an assignment statement — must analyse cleanly and the
transferred Prog.SymbolTable must still resolve the const.
- dep exports a class type, source freed, main program declares
a variable of that type — must analyse and FindType must
still hit.
These cover the semantic-side substitutive proof of 6c-D — the
source TUnit really is unnecessary downstream of ImportUnitInterface
for semantic analysis. Codegen still walks dep ASTs (and would
crash if they were freed before Blaise.pas's codegen pass);
migrating codegen onto TUnitInterface is the remaining substitutive
work and belongs in its own commit.
Pre-commit gate:
- mcp compile: OK
- TestRunner: 2249 tests, 2 pre-existing failures only
- fixpoint.sh: FIXPOINT_OK at stage-3/stage-4
- loader-testrunner: 70 tests, only documented pendings fail
Unblocks the previously-pending TestGenericRoutine_BodyExported and
adds end-to-end registration through TSymbolTable.
Parser change (uParser.ParseForwardDecl):
Accept '<TypeParams[: constraint], …>' after the routine name in
unit-interface forward decls. Mirrors the subset of ParseMethodDecl's
logic that applies to forwards (no '.MethodName' continuation here —
those land in the implementation section).
Semantic plumbing (uSemantic):
Generic free routines now slip through the interface-section
forward-decl loop, the impl-side decl loop, and the impl-body
loop: param/return resolution and global-symbol registration are
deferred to instantiation time, matching how impl-side
AnalyseStandaloneDecl already handles them.
Interface verification ('has no implementation' check) skips
generics — their template lives in FGenericFuncTemplates, not
FProcIndex.
AnalyseStandaloneDecl additionally mirrors the registration onto
TSymbolTable.RegisterGenericRoutine so the lookup surface is
shared between in-unit templates and imports. InstantiateGenericFunc
checks both lists.
TSymbolTable (uSymbolTable):
New FGenericRoutines TStringList plus public RegisterGenericRoutine
/ FindGenericRoutine. Case-insensitive (matches identifier
resolution elsewhere).
Export side (uSemanticExport):
TGenericBody gains a MethodDecl field — a CloneMethodDecl of the
impl-side TMethodDecl (with body) — so the import has a fully-
formed AST template to hand to RegisterGenericRoutine without
synthesising one at import time.
Import side (uSemanticImport):
New RegisterGenericRoutines step walks GenericBodies and pushes
each non-IsType entry's MethodDecl onto FTable's routine list.
Tests:
- Re-flips loader-testrunner's pending TestGenericRoutine_BodyExported
to green (export-side already supported it; parser was the
blocker).
- New TImportRoundTripTests.TestImport_GenericRoutine_RegisteredOnTable
asserts FindGenericRoutine('Identity') yields a TMethodDecl with
TypeParams + Params + Body intact.
Pre-commit gate:
- mcp compile: OK
- TestRunner: 2249 tests, 2 pre-existing failures only — no
regressions from the parser/semantic changes.
- fixpoint.sh: FIXPOINT_OK at stage-3/stage-4
- loader-testrunner: 68 tests, only documented pendings fail.
Walking AIface.Types: when an entry is generic (TGenericTypeDef or
TGenericInterfaceDef), register the AST template through
TSymbolTable.RegisterGeneric. Matches uSemantic.AnalyseTypeDecls
pass-1 — instantiation later goes through FindGeneric, which the
consumer's FindTypeOrInstantiate path already uses.
Added uAST.CloneGenericInterfaceDef + wired it into CloneTypeDef
dispatch — was missing alongside the existing CloneGenericTypeDef.
Without it, exporting any generic interface used to raise
"unsupported type def" inside Export.
Two new tests:
- TBox<T> = class V: T; end → FindGeneric('TBox') yields
TGenericTypeDef with ParamNames = ['T'].
- IBox<T> = interface … end → FindGeneric('IBox') yields
TGenericInterfaceDef with ParamNames = ['T'].
Out of scope (next 6c-C continuation):
- Generic free routines (TUnitInterface.GenericBodies has them
but uSemantic stores them in a private FGenericFuncTemplates
list — needs a public API on TSemanticAnalyser, or a tracker
on TSymbolTable, to plumb through).
- Full instantiation round-trip (would need a separate "compile
against imports" test that actually walks codegen).
Pre-commit gate:
- mcp compile: OK
- TestRunner: 2249 tests, 2 pre-existing failures only
- fixpoint.sh: FIXPOINT_OK at stage-3/stage-4
- loader-testrunner: 67 tests, only the documented pending fails
Three remaining 6c-B pieces, all going through the same
TUnitInterface that ExportUnitInterface already builds:
* RegisterInterface — NewInterfaceType + parent resolution +
per-method AddMethod (using ParamVarFlags to rebuild the
comma-separated var-flag string AddMethod expects).
* RegisterClass — walks AEntry.Implements, strips any 'Unit.'
qualification prefix, looks up the interface descriptor, and
calls RT.AddImplements.
* RegisterClass — walks AEntry.Attributes and calls
AddClassAttribute, appending the 'Attribute' suffix when the
raw name lacks it (uSemanticExport copies the raw attribute
names verbatim; downstream wants the resolved form). A cleaner
fix would be to have uSemanticExport pull resolved names off
the source TRecordTypeDesc.ClassAttributes; left as an audit
item.
Three new tests, all green:
- interface with two methods exported + imported, MethodCount=2
- class(TObject, IGreeter) → RT.ImplementsCount=1, name 'IGreeter'
- class marked [Marker] (with a locally-defined MarkerAttribute)
→ RT.ClassAttributeAt(0) = 'MarkerAttribute'
Pre-commit gate:
- mcp compile: OK
- TestRunner: 2249 tests, 2 pre-existing failures only
- fixpoint.sh: FIXPOINT_OK at stage-3/stage-4
- loader-testrunner: 65 tests, only the documented pending fails
TRoutineSig gains ResolvedQbeName (mangled symbol label set by
semantic), IsVirtual, and IsOverride. uSemanticExport copies these
straight off the source TMethodDecl.
RegisterClass now walks AEntry.Methods; for each virtual it
AddVTableSlots with ImplName = '$' + ResolvedQbeName, and for each
override it OverrideVTableSlot at the parent's slot. Static (non-
virtual, non-override) methods are no-ops on the type descriptor —
they only matter when downstream code actually calls them, which
requires importing them as global symbols (deferred along with
overloaded class methods).
Tests:
- Virtual method registers a new slot with the expected
'$TFoo_Speak' ImplName.
- Override on a derived class reuses the parent's slot index,
rewrites only the derived's ImplName, leaves parent intact.
Out of scope (next):
- Overloaded class methods (mangled-key lookup).
- Interface implements (data already exported as qualified names).
- Class attributes (export emits raw names; need resolved names).
- Properties (not currently exported).
Pre-commit gate:
- mcp compile: OK
- TestRunner: 2249 tests, 2 pre-existing failures only
- fixpoint.sh: FIXPOINT_OK at stage-3/stage-4
- loader-testrunner: 62 tests, only the documented pending fails
Extends uSemanticImport with RegisterClass — reconstructs a class's
TRecordTypeDesc with parent chain and field layout matching what
AnalyseTypeDecls produces.
Parent resolution mirrors semantic pass-2:
- Explicit parent: look up by name in the symbol table.
- Empty parent on a non-TObject class: implicit TObject.
Field layout is built by copying parent fields first (so storage
offsets continue past the parent's tail), then appending the
class's own fields. RT.AddField handles the alignment + offset
computation, identical to the from-scratch semantic path.
Three new tests:
- implicit TObject parent + inherited vtable shape
- own field at offset 8 (after the parent's vptr)
- explicit parent chain: TBase + TDerived, inherited field A at 8
and own field B at 12.
Methods on classes are deferred — TRoutineSig does not yet carry
ImplName (the QBE/LLVM vtable symbol label) and AddVTableSlot
requires it. Imported classes are usable for layout-only
consumers (typeinfo, field access); method dispatch awaits a
TRoutineSig.ImplName field plus export-side population.
Same workaround as the previous loader commit: the parent-class
resolver was originally a `const ARef: TQualTypeRef` parameter,
which segfaults on string-field read (memory:
project_record_const_param_crash.md). Reworked to take the parent
name directly.
Pre-commit gate:
- mcp compile: OK
- TestRunner: 2249 tests, 2 pre-existing failures only
- fixpoint.sh: FIXPOINT_OK at stage-3/stage-4
- loader-testrunner: 60 tests, only documented pendings fail
Extends uSemanticImport with RegisterRecord — reconstructs a
TRecordTypeDesc from the TUnitInterface.TTypeEntry's cloned
TRecordTypeDef. Fields are resolved by name against the symbol
table and added via TRecordTypeDesc.AddField, which handles the
offset + alignment computation the same way AnalyseTypeDecls does.
One new TImportRoundTripTests assertion exercising a two-Integer
record — verifies field count, names, offsets (0 and 4), and that
the field TypeDesc points at the symbol table's Integer.
Class import is deliberately still gated with EImportError — it
needs parent-chain resolution, vtable inheritance, attribute
resolution, and method registration on the class descriptor; that
belongs in its own commit.
Pre-commit gate:
- mcp compile: OK
- TestRunner: 2249 tests, 2 pre-existing failures only
- fixpoint.sh: FIXPOINT_OK at stage-3/stage-4
- loader-testrunner: 57 tests, only documented pendings fail