Align the QBE backend unit names with the dotted naming convention
already used by the native backend (blaise.codegen.native.*).
uCodeGen.pas → blaise.codegen.pas
uCodeGenQBE.pas → blaise.codegen.qbe.pas
Updated all uses clauses (~70 test files, main compiler units),
documentation references (README + 5 docs/*.adoc files), and
comment references in runtime/stdlib.
Indirect calls through a procedural-typed variable (the 'IsIndirectCall'
path added when 'RunTest(@DoTest)' first compiled) only validated arg
count. The argument types were never checked against the procedural
signature, so e.g. 'H("oops")' against 'procedure(N: Integer)' compiled
silently and miscompiled at the QBE level.
Both call sites — TProcCall (statement form) and TFuncCallExpr
(expression form) — now run each actual through CheckTypesMatch against
the corresponding TProcParamInfo, and reject non-L-value actuals for
var-typed parameters with the same diagnostic as the regular-call path.
Two semantic tests in cp.test.proctypes.pas pin the behaviour: one for
the statement form, one for the expression form.
Docs
- language-rationale.adoc: update the overload-mangling example so the
emitted symbol matches what codegen actually produces ($Log_D_Si, not
$Log$Si), and document the QBEMangle escape table for the three
sigil characters that the QBE symbol grammar disallows mid-identifier.
- design.adoc: clarify the var-parameter ABI entry — record and static
array value parameters share the by-pointer convention with var-params,
which is what makes IsVarParam the right flag for codegen to key off.
Implement whole-programme multi-file compilation. The compiler now
resolves `uses` clauses, locates unit source files via `--unit-path`
search directories, compiles them, and merges exported symbols into
the programme scope. Combined QBE IR is emitted in dependency order.
New components:
- uUnitLoader: post-order DFS unit loader with cycle detection
(EUnitNotFound, ECircularDependency); skips FPC RTL builtins
- TSemanticAnalyser.AnalyseUnitForExport: promotes unit interface
symbols to global scope; implementation symbols stay scoped
- TCodeGenQBE.AppendUnit / AppendProgram: accumulate combined IR;
FStrLitsEmitted tracks emitted string literals to avoid duplicates
- 8 new tests in cp.test.multifile covering loader, semantic export,
and combined codegen
Language additions:
- `out` parameter modifier (treated as var — pass by reference)
- implementation-section `uses` clause in units
Blaise.pas wired to use loader pipeline; --unit-path flag is
repeatable; -Fu<path> FPC-style flags are honoured.
Design doc updated: Phase 4 (multi-file), phases 5–8 renumbered;
--unit-path added to CLI table; build-tool-drives-compiler principle
documented in constraints.
End-to-end verified: two-unit programme compiles and executes correctly.
Record the decision (option A) in docs/design.adoc with full pros/cons
analysis of the three options considered. Update the Phase 3 status
table so interface-ref ARC reads "Planned — option A chosen" rather
than "Deferred — design-blocked". Mirror the commitment in README.adoc
under Design Philosophy and list the TObject/TInterfacedObject split
as dropped.
The exception frame was allocated as alloc16 32 (32 × 16 = 512 bytes on paper,
but QBE's alloc16 N means N items of 16 bytes: so 32 × 16 = 512 is actually
correct). Wait - re-reading: alloc16 N in QBE allocates N bytes aligned to 16.
So alloc16 32 was only 32 bytes — far less than sizeof(BlaiseExcFrame) which
needs ~216 bytes on Linux x86_64 (200-byte jmp_buf + two pointer fields).
setjmp writes its full jmp_buf into the undersized slot, silently corrupting
whatever sat above it on the stack: saved registers, local variables, and the
virtual method pointer loaded for virtual dispatch. Any try block followed by
a virtual method call in expression position crashed with a bad function pointer.
Fix: alloc16 32 → alloc16 512 in both EmitTryFinallyStmt and EmitTryExceptStmt,
matching the RTL contract documented in blaise_exc.c lines 8-13.
Two new regression tests assert alloc16 512 appears in emitted IR for both
try forms. 628 tests pass (was 626).
Also adds tests/phase2_milestone.pas: linked list with virtual dispatch,
inheritance, try/finally, and 'is' type test — zero valgrind leaks.
Updates design.adoc Phase 3 status table to reflect current implementation.
Parse generic class declarations (`TBox<T>`) as TGenericTypeDef nodes.
Introduce one-token parser lookahead to disambiguate `<` in type
positions vs. comparison operators.
Demand-driven instantiation: when a var declaration references
`TBox<Integer>`, InstantiateGeneric clones the class AST with type-param
substitution, resolves all field/method types, analyses method bodies
with the concrete class type in scope, and registers the instance in
TProgram.GenericInstances. Instantiated type symbols are defined in
the global scope so they survive inner scope pops.
Codegen emits typeinfo, vtable, and method bodies for each generic
instance using QBEMangle (`TBox<Integer>` → `TBox_Integer`).
19 new tests in cp.test.generics cover parser, semantic, and codegen
paths. All 513 tests pass.
Mark is/as interface operators, IInterface built-in, and 2-field typeinfo
(with impllist) as Done. Expand TYPEID entry to cover the impllist
data layout.
Parser: TInterfaceTypeDef AST node; ParseInterfaceDef; ParseClassDef extended
to parse comma-separated implements list (first name = parent class, rest =
interfaces); ParseMethodDecl body remains optional.
Symbol table: tyInterface added to TTypeKind; TInterfaceTypeDesc with unsorted
(declaration-order) method list for correct itab slot indexing; FImplements
non-owning list on TRecordTypeDesc tracks class→interface pairs; TObject
pre-registered as built-in root class.
Semantic: AnalyseTypeDecls handles TInterfaceTypeDef (register, inherit parent
methods); verifies class implements all interface methods; CheckTypesMatch
extended for class→interface assignment; AnalyseMethodCall handles tyInterface
object vars via itab dispatch path; TAssignment carries ResolvedLhsType.
Codegen: EmitInterfaceDefs emits $typeinfo_IFoo and $itab_TFoo_IFoo data blocks;
EmitVarAllocs allocates two-slot fat pointer (_obj + _itab) for interface vars;
EmitAssignment stores both obj pointer and itab address on interface assignment;
EmitMethodCall dispatches via itab on tyInterface receiver.
15 new tests, 484 total, 0 failures.
Covers interfaces (10 items) and generics (6 items), each as a separate
subsection with Pending status rows and implementation notes. Records the
agreed implementation order (interfaces before generics) and the Phase 3
milestone (TList<Integer> + TDictionary<string,Integer>, zero valgrind leaks).
Cannot be tested on Linux; Darwin linking has enough quirks (libSystem,
fat-binary RTL) that shipping untested support would just break the first
macOS user. Phase 5 is the right home — LLVM backend work and CI/CD pipeline
for all platforms are already there.
Phase 2 is now fully complete on Linux x86_64.
Separate method implementations: `procedure TFoo.Bar(...)` can now appear
outside the class definition. Parser detects qualified names in ParseMethodDecl
and makes the body optional for forward-only class declarations. Semantic pass
links standalone bodies back to class method declarations via LinkClassMethodImpls
before AnalyseMethodBodies runs, so all existing method analysis and codegen paths
are reused unchanged.
Free built-in: `Obj.Free` with no user-defined Free method emits `call $free(l ptr)`.
Semantic pass recognises the call and sets ResolvedMethod := nil as a signal;
codegen handles nil method as a built-in free before the normal dispatch path.
Phase 2 milestone verified: a linked list using TNode (TObject subclass) with
separate method impls, Create, and Free compiles and runs with zero valgrind errors
(3 allocs, 3 frees, 0 bytes in use at exit).
Design doc updated: new rows for is/as, ARC exception cleanup, separate method
impls, and Free built-in; Immediate Next Steps trimmed to macOS ARM64 only.
469 tests, 0 failures.
- Add blaise_exc.c to RTL: _PushExcFrame, _PopExcFrame, _Raise,
_CurrentException, _Reraise using thread-local setjmp/longjmp frames
- Update rtl/Makefile to compile and archive blaise_exc.c
- Rewrite EmitTryFinallyStmt: alloc16 frame on stack, push+setjmp at try
entry, finally body emitted on both normal and exception paths, _Reraise
propagates to enclosing handler on exception path
- Rewrite EmitTryExceptStmt: same frame setup, except handler entered via
jnz on setjmp return, _PopExcFrame before handler body
- Add 6 codegen tests for setjmp-based dispatch patterns (460 tests total)
- Update design doc: mark try/except/finally as Done, update next steps
Reflect completed work: class methods, var parameters, unit
interface/implementation, full ARC (compiler + RTL), and string
concatenation. Mark try/except/finally as partial. Update immediate
next steps to virtual dispatch, real exception unwinding, and macOS target.
Symbol table:
- Added tyClass kind and NewClassType factory; TRecordTypeDesc now
accepts an optional kind parameter so records and classes share the
same descriptor with distinct semantics.
Semantic analyser:
- AnalyseTypeDecls runs before PushScope so type symbols land in global
scope and survive PopScope.
- Handles TRecordTypeDef and TClassTypeDef; sets IsConstructorCall on
TypeName.Create expressions and IsClassAccess on class-variable field
access and assignment nodes.
Code generator (QBE IR):
- Class variables: 8-byte pointer slot, zeroed on entry.
- Constructor calls: malloc(sizeof fields), store pointer.
- Class field access/write: load pointer, add field offset, load/store.
- ARC string assignment: AddRef new value, Release old value, then store.
- Block exit: Release every string variable in scope (EmitStringCleanup).
Lexer / Parser / AST:
- Added tkClass keyword and ParseClassDef; FieldDecl parsing refactored
into ParseFieldDecl(AFields) shared by both record and class.
- AST gains TClassTypeDef, and IsConstructorCall/IsClassAccess flags on
TFieldAccessExpr and TFieldAssignment.
RTL:
- blaise_arc.c: Phase 2 no-op stubs for _StringAddRef / _StringRelease.
- rtl/Makefile: builds blaise_rtl.a; `make install` copies it next to
the compiler binary for automatic discovery by FindRTL.
- Blaise.pas driver: FindRTL checks BLAISE_RTL env var then binary dir;
links the RTL archive when present.
Tests:
- 180 unit tests (records ×23, classes ×22, ARC ×9, codegen ×9, …).
- 4 end-to-end integration tests in tests/integration/test_arc_strings.sh
covering string assignment, two-var cleanup, reassignment cycle, and
empty-program linkage.
Docs:
- design.adoc updated with Phase 2 implementation status table.
- docs/grammar.ebnf: new authoritative EBNF grammar for the Blaise
language as implemented, including ARC semantic annotations.