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.
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).
`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.
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.
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.
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.
A bare `TFoo = class;` / `IFoo = interface;` (the keyword immediately
followed by `;`, with no ancestor clause and no body) is a forward
declaration: it registers a placeholder type that a later full
declaration of the same name completes within the same declaration
scope. This is the standard Object Pascal idiom for mutually
referential types, e.g. a class declared between the forward and its
full declaration may name the forward-declared type.
Previously the front end registered the bare `class;` as a complete
type and then rejected the full declaration as a duplicate type name,
so the idiom could not be used at all.
The parser flags the stub via TClassTypeDef.IsForward /
TInterfaceTypeDef.IsForward. AnalyseTypeDecls registers the placeholder
in pass 1, lets the matching full declaration complete it (reusing the
placeholder descriptor) instead of erroring, skips the stub in pass 2,
and after resolution drops the now-redundant stub from the AST so later
phases see a single declaration. A forward never completed in the scope
is reported as "Forward type not resolved"; a type forward-declared
twice is "Duplicate forward type declaration".
Tests: IR/semantic coverage for completion, mutual reference,
unresolved, and double-forward errors, plus a single-vtable codegen
check (the stub must not double-emit); an e2e test compiles and runs a
mutually-referential class pair with a managed field.
Inside a method, an unqualified FFn(args) -- no 'Self.' prefix -- where
FFn is a procedural-typed field of the current class failed with
"Undeclared function/procedure 'FFn'". Only the explicit Self.FFn(args)
form resolved.
Both call analysers now fall through to an implicit-Self field lookup
after the method lookup fails: a procedural-typed field of the current
class is dispatched through its stored pointer, validating argument count
and types (and the l-value requirement for var/out arguments) against the
field signature, exactly like the explicit-receiver path.
Codegen:
* QBE: a shared EmitImplicitSelfProcFieldCall loads Self, indexes the
field, and dispatches; var/out arguments are passed by reference and
'of object' fields pass the captured Data as the implicit first arg.
Used by both the statement and expression forms.
* Native x86-64: both forms reuse EmitProcFieldCall with Self as the
receiver.
The two new AST flags (TProcCall.IsProcFieldCall,
TFuncCallExpr.IsProcFieldCall) are recorded in the bif-coverage status
inventory as semantic-only (safe, not serialised), matching the existing
IsIndirectCall flags.
Covered by an IR test (loads Self, indirect dispatch) and end-to-end
tests on both backends for the expression and statement forms.
A routine body may now be written as inline assembly:
function GetSelf: Pointer; assembler; nostackframe;
asm
movq %rdi, %rax
ret
end;
The block is opaque GNU/AT&T assembly: the lexer captures the whole asm … end
as one tkAsmBlock token (verbatim text, never tokenised as Pascal), the parser
wraps it in a TAsmStmt, the semantic pass treats it as a black box, and the
native backend emits it verbatim into the assembly stream where the existing
internal/external assembler parses it. `nostackframe` suppresses the compiler
prologue/epilogue so the asm body owns the whole frame. asm routines mix
freely with ordinary Pascal routines in a standard .pas unit (no .inc needed).
This is the FPC model (rtl/linux/x86_64/si_c.inc) and the path to retiring the
hand-written runtime/src/main/asm/*.s files (assembled by `cc -c` today) — once
each body moves into an asm routine the RTL builds with no external assembler.
Design follows ports-and-adapters: x86-64 knowledge stays at the backend/
assembler edge, the portable core never interprets the block. The QBE backend
rejects asm bodies (it emits no assembly text); native is the inline-asm target
and the default. TAsmStmt round-trips through the .bif unit cache.
Pipeline: lexer (tkAsmBlock + ReadAsmBody raw capture), uAST (TAsmStmt,
TMethodDecl.NoStackFrame), parser (nostackframe directive + asm-body path),
semantic (opaque no-op), native codegen (verbatim emit + nostackframe null-frame
guard), QBE rejection, uUnitInterfaceIO encode/decode, bif-coverage entry.
`asm` becomes a reserved word (one local var named Asm renamed in a test).
Fixes a native sret-Result field-read codegen bug the feature exposed: reading a
field of an sret function's Result at offset 0 (e.g. `Result.Kind` in a record-
returning function) read the Result frame slot DIRECTLY instead of dereferencing
the caller-buffer pointer it holds, so `Result.Field = const` was always false.
The offset-0 fast path in the integer field-read leaf now routes through
EmitLocalRecordBase like the offset>0 path, so the sret indirection happens in
both. QBE was already correct; this was a native-only divergence.
Tests: lexer raw-capture (3), native verbatim-body/no-prologue IR test (1),
internal-assembler e2e returns-value + adds-two-args (2). All four fixpoints
and both QBE- and native-built test runners pass (3764 tests).
Design note: docs/inline-asm-design.adoc; grammar + rationale updated.
Compiling a multi-unit program whose uses-clause pulls in every runtime
unit (the auto-generated bootstrap_program used by 'pasbuild compile -m
blaise-runtime') failed in the semantic pass. Several runtime units each
privately bind the same C/RTL symbol (e.g. 'external name '_BlaiseGetMem'',
'external name 'abort'') or define a private implementation-section helper
of the same name (DiagAbort in both blaise_arc and blaise_exc). In the
flat-merge these all share one global scope and overload group, producing
spurious 'Duplicate identifier' and 'Ambiguous overload' errors.
Three coordinated changes:
* SameLinkSymbol/EffectiveLinkName: collapse candidates that denote the
same underlying link symbol when at least one side is an external
binding — covering both two-units-bind-same-C-symbol and a binding that
targets a real function exported by an unmangled RTL unit (blaise_*/rtl.*,
whose exports keep their bare Pascal name). SameExternalDecl and the
overload-collapse / zero-arity paths now route through it.
* BenignDuplicateExternal: when an interface-section Define collides in the
global scope, tolerate it if the colliding symbol is the same link symbol.
* IsImplOnly flag on TMethodDecl: marks implementation-section-only
routines as private to their unit; ResolveStandaloneOverload excludes
cross-unit private candidates so each unit's call binds its own helper.
Adds three regression tests in cp.test.multifile.pas. All four fixpoints
(QBE, native, internal-assembler, warm-cache) and the full suite (3750
tests, QBE-built and native-built runners) pass.
Incremental compilation (per-unit .o + embedded .bif, the default) produced
silently corrupt binaries when rebuilding into a populated --unit-cache: the
clean build worked, but any rebuild that loaded a unit from its cached .bif
instead of source dropped or mishandled state the in-memory path carried.
Root cause is uniform across every symptom: the .bif round-trip was not a
complete projection of the unit interface, and the cached-load path in the
loader/driver diverged from the source-load path.
.bif format bumped to version 3 (IFACE_VERSION; CompilerId +bif tag) so stale
caches are cleanly rejected and recompiled from source.
Fixes (each was a field/decision present on the source path, absent on the
cached path):
- Loader: a unit's implementation-section `uses` were never recorded in the
.bif, so on rebuild the impl-only dependency's object was dropped from the
link (undefined symbol). Record ImplUsedUnits in the .bif and collect those
objects for LINK ONLY (CollectLinkOnlyObject) — not semantic import, which
would break the leaf-first iface import on interface/impl-use cycles.
- Native codegen: a global var owned by a cached unit was re-defined by the
recompiled top program (`multiple definition of GTarget`). The program now
references imported-unit globals as external (FImportedUnits / IsImportedGlobal
gate in EmitDataSection); NoteDepInitUnit records every imported unit.
- Unit initialization: <Unit>_init calls for cached deps were never emitted, so
initialization sections silently never ran. Record HasInitialization in the
.bif and note prebuilt-iface inits in dependency order at program codegen.
- SynthesiseMethodDecl dropped VTableSlot/IsVirtual/IsOverride from cached
method imports, degrading virtual dispatch to direct calls into the abstract
base (SIGABRT at run time). Propagate them.
- Generic-instance field/param types now resolve via the analyser's generic-
aware resolver on import (ResolveImportedTypeName); generic instantiation
during import no longer nil-derefs FProg/FCurrentUnit (pending-instance flush).
- .bif round-trip completeness: serialise block-local var decls, Exit(value)
return values, parameter default values, free-routine ResolvedQbeName,
generic-class template properties, and fix implements-interface name
stripping (strip to the last dot, not the first).
Adds TestIncrementalRebuild_ImplOnlyUses_LinksDependency (e2e) and updates the
magic/version assertion. Full suite OK (3722 tests); QBE, native, and
internal-assembler fixpoints pass; clean and warm-cache rebuilds both produce
correct binaries.
Two related defects in integer constant handling:
- A large hex literal that needs more than 32 bits (e.g. $080808080808 =
8830587504648) was silently TRUNCATED to 32 bits (printed 134744072). An
untyped integer constant was unconditionally typed as Integer regardless of
its magnitude, so codegen treated it as a 32-bit word.
- A literal above High(Int64) (e.g. $8080808080808080) was REJECTED at parse
time with "Integer literal exceeds Int64 range", even though it is a valid
64-bit bit pattern and is accepted by FPC/Delphi.
Fixes:
- uParser: the bare-integer-literal const path now uses ParseIntOrUInt64Literal
(which already existed for the tolerant case) instead of ParseIntLiteral, so a
value in the UInt64 range is captured as a bit pattern and flagged via a new
TConstDecl.IsUInt64.
- uSemantic (AnalyseConstDecls): an untyped integer constant now picks its type
by magnitude — Integer when it fits in signed 32-bit, Int64 when larger, and
UInt64 when the value exceeds High(Int64) (matching Delphi, which prints
$8080808080808080 as 9259542123273814144). A typed const (e.g. A: Int64 =
$8080808080808080) keeps its declared type and stores the bit pattern, so it
prints -9187201950435737472.
Tests (cp.test.e2e.misc.pas, dual-backend): large hex not truncated, typed
Int64 bit pattern, typed UInt64 bit pattern, and untyped-above-Int64 widening
to UInt64.
Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite green
on both the QBE-built and native-built runners (3704 tests).
Constructors are now auto-slotted into the vtable by the semantic pass.
When a constructor is called through a metaclass-typed variable
(C.Create(args)), the compiler emits _ClassCreate for allocation
followed by a vtable-indirect call to the most-derived constructor
body. Direct calls (TFoo.Create) remain fully static.
Both backends (QBE and native x86-64) emit correct dispatch for:
- MetaclassVar.Create(args) syntax
- ClassCreate(MetaclassVar, args) builtin
- Zero-arg and multi-arg constructor signatures
This is a layout-changing commit (adds constructor vtable slots).
Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK,
3370 tests pass.
A class or record method may now declare its own type parameters,
independent of the enclosing type, and is monomorphised per call site:
type TUtil = class
function Pick<T>(cond: Boolean; a, b: T): T;
begin if cond then Result := a else Result := b end;
end;
...
u.Pick<Integer>(True, 7, 9); // -> 7
u.Pick<string>(False, 'a', 'b'); // -> 'b'
Each distinct set of explicit type arguments produces one concrete body
named <Owner>_<Method>_<Args> (e.g. TUtil_Pick_Integer); the implicit
Self is preserved, so a generic method can read the receiver's fields and
call its other methods. This is distinct from (and composes with) methods
of a generic CLASS.
Implementation mirrors the existing generic free-function machinery:
- Parser: a method-call folds an explicit <...> type-arg list into the
method name (Pick<Integer>), using the same two-token '<' lookahead as
generic free-function calls.
- Semantic: generic-method templates (TypeParams <> nil) are registered
by Owner.Method and skipped from signature/vtable/body analysis;
InstantiateGenericMethod clones the template, substitutes the type
params, keeps OwnerTypeName + Self, analyses via AnalyseMethodDecl, and
records a TGenericMethodInstance (deduplicated per owner+args). The
call site resolves obj.M<T>(...) to the instance.
- Codegen (both backends): GenericMethodInstances are emitted as ordinary
methods (Self param + mangled ResolvedQbeName).
Verified pick/echo, two distinct instantiations (Integer + string), use
of a Self field, and two type parameters on both backends. Adds IR tests
(TGenericFuncTests.TestCodegen_GenericMethod_{Body,Call}Emitted) and
dual-backend e2e tests (TE2EGenericsTests.TestRun_GenericMethod_*).
Grammar and language-rationale updated.
Limitation (logged in bugs.txt): only the inline-body declaration form is
supported; the out-of-line function TOwner.Method<T>(...) form is not
yet parsed.
Adds the `default` directive on an indexed property, enabling subscript
sugar on the object itself:
property Items[I: Integer]: T read Get write Put; default;
...
V[0] := 10; // lowers to V.Put(0, 10)
WriteLn(V[0]); // lowers to V.Get(0)
This is the mechanism behind the familiar List[i] syntax and was a real
foundational gap (the directive did not even parse — "Expected ':'").
Implementation:
- Parser: accept the trailing `default;` directive after a property
declaration; set TPropertyDecl.IsDefault.
- AST / symbol table: IsDefault on TPropertyDecl and TPropertyInfo;
TRecordTypeDesc.FindDefaultProperty walks the inheritance chain.
- Semantic: Obj[I] read (AnalyseStringSubscriptExpr) synthesises the
default property's field access and reuses the indexed-property read
path; Obj[I] := V write (AnalyseStaticSubscriptAssign) records the
setter on the TStaticSubscriptAssign node.
- Codegen (both backends): the write path emits the setter call via the
existing PropAccessorTarget / EmitPropAccessorCallNative helpers, so it
honours virtual/override on the accessor; the read path delegates the
TStringSubscriptExpr to its folded property-read field access.
- Cross-unit: IsDefault is serialised in the .bif interface so a default
property declared in one unit keeps its subscript sugar elsewhere.
Verified read, write, string-element, inherited, and cross-unit cases on
both backends. Adds IR tests (TPropertyTests.TestCodegen_DefaultProperty_
{Read,Write}) and dual-backend e2e tests (TE2EPropertyTests.TestRun_
DefaultProperty_{ReadWrite,StringElement,Inherited}). Grammar and
language-rationale updated; the two new TStaticSubscriptAssign fields are
marked safe in bif-coverage.status.
A property whose getter or setter is declared `virtual` did not dispatch
through the vtable: reading or writing the property through a base-typed
variable holding a derived instance called the BASE accessor, not the
override. A direct b.GetVal() call dispatched correctly, but b.Val (the
property over the same virtual getter) did not.
TBase = class
function GetVal: Integer; virtual; begin Result := 1; end;
property Val: Integer read GetVal;
end;
TDerived = class(TBase)
function GetVal: Integer; override; begin Result := 99; end;
end;
b: TBase := TDerived.Create;
WriteLn(b.Val); // was 1, now 99
The property read/write lowering always emitted a static call to the
accessor's declaring class. Now the semantic pass records the accessor's
vtable slot on the AST node (PropAccessorVSlot, -1 when the accessor is
not virtual), and codegen dispatches through the vtable when the slot is
>= 0, exactly as a direct method call does.
QBE: PropAccessorTarget computes the call target — emitting the vptr+slot
loads and returning the function-pointer temp for a virtual accessor, or
the static mangled symbol otherwise — and each call site emits its own
`call <target>(...)`. (A single emit-and-return helper was tried first
but tripped a latent native-backend miscompile on the self-compile; see
bugs.txt. The target-string shape avoids it.) Native:
EmitPropAccessorCallNative dispatches through the vtable or statically.
Covers getter and setter, on both backends. Adds
TE2EInheritTests.TestRun_VirtualProperty{Getter,Setter}_Dispatches
(dual-backend). The two new AST fields are marked `safe` in
bif-coverage.status (semantic-set, not serialised).
`inherited` previously parsed only as a statement, so calling an inherited
FUNCTION and using its result failed to parse:
Result := inherited Value() + 100; -> "Parse error: Expected expression"
This blocked the normal OOP pattern of an overriding function extending its
parent's result.
Adds the expression form alongside the existing statement form:
* uAST: new TInheritedCallExpr (sibling of TInheritedCallStmt) + CloneExpr case.
* uParser: tkInherited handled in ParseFactor (primary expression).
* uSemantic: AnalyseInheritedCallExpr resolves to the parent method (must be a
non-void function) and sets ResolvedType to the return type.
* Codegen: static (non-virtual) call to the parent slot, result returned as a
value. QBE EmitInheritedCallExpr; native EmitInheritedCallSeq (shared by the
statement and expression forms) leaves the result in %rax/%xmm0.
* uUnitInterfaceIO: 'inhc' encode/decode so inherited-expr in an inline method
body round-trips through the .bif unit-interface cache; bif-coverage.status
marks the two fields serialise (bif-coverage OK).
* docs/grammar.ebnf + docs/language-rationale.adoc updated (same commit).
Found by the e2e test-hardening sweep of the inheritance feature cluster.
Regression: TE2EMiscTests.TestRun_InheritedFunctionCall_InExpression covers a
value-returning inherited call and an inherited call with an argument, run on
BOTH backends via AssertRunsOnAll.
All three fixpoints (FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK) and
the full suite (3222 tests) pass.
A set literal element may now be a constant range:
e := [Red..Blue]; { {Red, Green, Blue} }
e := [m0, m2..m4, m7]; { ranges mix with single members }
The parser builds a transient TSetRangeExpr for each lo..hi element; the
semantic pass (AnalyseSetLiteralExpr) expands it into the individual member
idents before any other consumer runs, so overload resolution, the bitmask
folder, the jumbo-set path, and both code generators see an ordinary member
list and need no range-specific logic.
Both bounds must be compile-time constants of the set's base type. A
reversed constant range [Blue..Red] is a compile-time error rather than a
silently empty set — matching Blaise's preference for rejecting
confusing-but-legal constructs over FPC's empty-set-with-warning behaviour.
Variable bounds [lo..hi] are rejected ("set range bound must be a
constant"); runtime-variable ranges are deferred.
TSetRangeExpr is wired into the .bif serialiser and the AST cloner so a
generic template body containing an unexpanded range round-trips through
separate compilation (verified by hand: a generic returning [m1..m3]
compiled to .o, then instantiated from the .bif with source hidden).
Set base types remain enumerations; the issue's set-of-byte example needs
ordinal-base set types, tracked separately in docs/future-improvements.adoc.
Range expansion is base-type-agnostic, so ranges will work for those
automatically once they land.
Docs: grammar.ebnf SetElement rule; language-rationale.adoc decision +
alternatives. bif-coverage.status regenerated for the new node.
FIXPOINT_OK + NATIVE_FIXPOINT_OK; full suite OK (3201 tests, 11 new).
Extend set of <enum> from a 64-member cap to 256, Java-EnumSet style. Sets
of 64 members or fewer keep the existing single-register bitmask (QBE w/l);
sets of 65..256 members ('jumbo') become an inline byte-array bitmap of
ceil(N/8) bytes, treated as a value aggregate (passed by reference, returned
via sret, memset/memcpy), with operations performed by new RTL helpers.
Representation (uSymbolTable): TSetTypeDesc.IsJumbo (BitCount > 64) and
RawByteSize; RawSize/ByteSize/AllocAlign sized accordingly. TSymbol gains
ConstSetBytes/ConstSetQbe for jumbo constants (can't fit an Int64 mask).
RTL: new runtime/src/main/pascal/blaise_set.pas — _SetIn/_SetInclude/
_SetExclude/_SetUnion/_SetInter/_SetDiff/_SetEqual/_SetCopy over byte-array
bitmaps (overlap-safe). Wired into runtime/Makefile.
Semantic: the four >64 caps become >256. AnalyseSetConstDecl folds a jumbo
const to a byte array. The anonymous set for 'X in [a,b,c]' is sized to the
largest listed ordinal (when constant), not the full enum — keeping the
common low-ordinal membership test (incl. the compiler's own TokenKind
tests) on the fast register path. This also fixes a latent miscompile: the
old fixed-l representation silently dropped any listed ordinal >= 64.
Codegen (both backends): jumbo branches for literal, in, +/-/*, =/<>,
Include/Exclude, for-in, assignment, params (pmJumboSetValue ABI), and sret
returns. The register 'in' gained a range guard for literal-sized sets.
Native reserves two 32-byte scratch slots per frame (and .bss in main) for
set-op/literal result buffers. QBE adds IsAggregateAddrType so jumbo sets
ride the record/static-array address paths and are never promoted.
OPDF: no format change — recSet SizeInBytes (1 byte) already covers <=32.
Verified: FIXPOINT_OK and NATIVE_FIXPOINT_OK; full suite (3172 tests) green
built by the stage-2 binary, the QBE fixpoint binary, and the native
fixpoint binary; bif-coverage OK. Tests: cp.test.jumboset (12 IR) and
cp.test.e2e.jumboset (6 e2e, both backends via AssertRunsOnAll); the
>256-member rejection test in cp.test.sets updated. Docs: grammar.ebnf and
language-rationale set-type sections updated for the 256 cap and literal
sizing.
Closes the design discussion behind #81.
A const declaration may now take a compile-time integer expression on the
right-hand side, not just a single literal:
const A = 2 * 3; // 6
B = 2 + 3 * 4; // 14 (precedence)
C = (2 + 3) * 4; // 20 (parentheses)
D = 100 div 7; // 14
E = Base * 2 + 1; // references a prior const
Previously only single literals and a flat, single-precedence bit-op chain
(a or b or c) were accepted; 2 * 3 failed with "Expected ';' but got '*'"
and (2 * 3) with "Expected numeric or string constant".
Parser: when the const RHS starts an integer expression (a leading '(', an
integer literal or leading-minus literal followed by a binary operator, or an
ident followed by a numeric operator), parse it with the existing
full-precedence ParseExpr into a normal expression AST stored on
TConstDecl.IntValueExpr. A bare literal keeps the fast IntVal path; '+' on an
ident is left to the string-concat path for backward compatibility.
Semantic: EvalConstIntExpr recursively folds the AST to an Int64, resolving
named-constant references against the symbol table. Folding in the semantic
pass (after all consts are registered) is what makes forward references work
regardless of declaration order. Wired into both the const-block and the
initialised-variable (var G = expr) fold sites. Operator precedence and
grouping come from the AST shape, superseding the precedence-unaware bit-op
token chain.
Scope: folds to integer. Float constant arithmetic and using a const as a
static-array bound remain separate, pre-existing limitations (noted in the
rationale).
Tests: eight IR/fold cases in cp.test.constants (multiply, parens, precedence,
div/mod, named-ref, unary minus, mixed arith+bitwise); one e2e case on both
backends in cp.test.e2e.misc. Docs: grammar.ebnf gains the ConstIntExpr rules;
language-rationale gains a Constant Expressions section.
Range-indexed array constants now support multiple dimensions in both the
comma form (array[0..1, 0..2] of Integer) and the equivalent nested form
(array[0..1] of array[0..1] of Integer), with nested initialiser groups
((1,2,3),(4,5,6)). Previously the const-declaration parser had its own
single-dimension array-type and value-list parsers, so any multi-dim const
failed at the type (Expected ']' but got ',') or the nested value group.
Parser: ParseConstArrayType walks one-or-more 'array[...] of' headers,
reading comma-separated ranges per header and recursing on a nested 'array';
each dimension's bounds go to CD.ArrayDimLows/ArrayDimHighs. The value
parser is now recursive (ParseConstArrayGroup/ParseConstArrayScalar),
flattening nested groups into ArrayElements in row-major order. Dim-0 bounds
mirror onto the legacy ArrayLowBound/ArrayHighBound so the single-dim path
is unchanged.
Semantic: BuildConstArrayType builds the nested static-array type
innermost-first and validates the flat element count equals the product of
dimension extents. Shared by the program/unit and class-const sites.
Codegen: the QBE emitter already lays a flat row-major blob, so it needs no
change for integer/string elements. The native emitter now drills through
nested static-array types to the innermost scalar to pick the element
directive.
Tests: parse/semantic/IR in cp.test.constants; e2e (comma, nested, 3-D) on
both backends via AssertRunsOnAll in cp.test.e2e.staticarray. Docs:
grammar.ebnf gains ConstArrayType/ConstArrayValue rules; language-rationale
updated (the stale 'enum index only' constraint corrected to document
range and multi-dimensional const arrays).
A parameter declared 'array of const' accepts a single call-site bracket list
of mixed-type values, boxed into an array of the intrinsic record TVarRec and
passed via the existing open-array ABI:
procedure Log(args: array of const);
...
Log([42, 'hi', 3.5, True]);
This is the one loosely-typed-passing mechanism Blaise adopts (see the
rationale section); untyped params, varargs, and Variant remain omitted.
- RTL/builtins: TVarRec is registered as a compiler-intrinsic record
{ VType: Byte; VValue: Pointer } (16-byte layout, mirroring TMethod), with
vt* discriminant constants - all available with no uses clause, matching
Delphi's auto-available System.TVarRec. Blaise has no record variant parts,
so the callee reads each element by reinterpret-casting the single VValue
slot (Integer(v.VValue), string(PChar(v.VValue)), PDouble(v.VValue)^, ...).
- Parser: 'array of const' parses as an open array whose element is TVarRec.
- Semantic: a heterogeneous bracket literal is typed 'array of TVarRec' rather
than rejected; overload resolution binds it (and homogeneous / empty
literals) to an array-of-const formal; retyping runs for proc, func, and
method calls.
- Codegen (both backends): EmitConstArrayLiteral builds one 16-byte TVarRec
per element, tagging by inferred type. Borrow semantics (FPC) - strings and
objects are stored without AddRef. Doubles are heap-boxed via _BlaiseGetMem
(vtExtended holds a PDouble) since a double does not fit the pointer slot.
- Native: also fixes a pre-existing gap - reading a float through a pointer
deref (PDouble^) in EmitExprToXmm0 - needed for vtExtended read-back.
Tests: cp.test.arrayofconst (parser/semantic/IR) and cp.test.e2e.arrayofconst
(compile + run on both backends, including value read-back, empty/homogeneous
lists, and string-variable borrow). Grammar and language rationale documented.
A global variable declaration may now carry an initialiser whose value is
folded at compile time and emitted into the data section, so the variable
holds its initial value before the program body runs (matches FPC/Delphi):
var
G: Integer = 42;
S: string = 'hello';
A: array[0..2] of Integer = (10, 20, 30);
Implementation reuses the typed-constant pipeline end to end:
- Parser: the const value scanner is factored into ParseConstValue, shared by
const declarations and the new var-initialiser path. TVarDecl carries an
owned InitConst: TConstDecl (TConstDecl moved ahead of TVarDecl so no
forward declaration is needed).
- Semantic: AnalyseVarInitializer folds the value and type-checks it against
the declared type; array initialisers derive element type and bounds from
the resolved static-array type and mint a data label.
- Codegen: EmitGlobalVarInit (QBE) and EmitGlobalInitData (native) emit the
folded value as a typed data slot - a single field for scalars/strings (a
string global points at an immortal static header __sN + 12) and an inline
element list for arrays. Covers program- and unit-level globals on both
backends.
Restrictions, each rejected with a clear diagnostic: global scope only (no
local initialisers), a single name per initialised declaration, and arrays
only for aggregates (record and inline-set initialisers are deferred - no
record-constant machinery yet, and a set initialiser would clash with the
const symbol the set folder defines).
Tests: cp.test.varinit (IR + parser/semantic) and cp.test.e2e.varinit
(compile+run on both backends). Grammar and language rationale documented.
Add multi-dimensional static array syntax, both the comma form
(array[0..1, 0..2] of Integer; A[i, j]) and the equivalent nested/chained
form (array[0..1] of array[0..2] of Integer; A[i][j]). The comma forms are
syntactic sugar: the parser desugars array[a, b] of T into the nested
array[a] of array[b] of T, and A[i, j] into chained subscripts A[i][j], so
the two notations are fully interchangeable in every position.
Layers:
- uParser: comma loops in ParseTypeName and subscript reads; the statement
LHS now lowers A[i, j] := v and A[i][j] := v to a TStaticSubscriptAssign
carrying a new BaseExpr (the inner-array address expression). The previous
"chained base not yet supported" rejection is removed.
- uAST: TStaticSubscriptAssign.BaseExpr (owned); wired into CloneStmt and the
.bif encoder/decoder (uUnitInterfaceIO); bif-coverage status updated.
- uSemantic: BaseExpr branch resolves the inner static-array type and checks
the index and value element type.
- Codegen (both backends): a nested static-array element now evaluates to its
inline address (mirroring record/interface elements) so a further subscript
indexes into it; the static-subscript store reads its base from BaseExpr
when set. Nested arrays are a flat row-major contiguous block.
- OPDF: no new record needed - each dimension emits one recArray whose element
points at the next inner recArray; pdr follows the chain and renders the
value as a true multi-dimensional structure.
Tests: IR unit tests (cp.test.staticarray), e2e tests on both backends via
AssertRunsOnAll (cp.test.e2e.staticarray), and a nested-recArray OPDF test
(cp.test.opdf). Grammar and language rationale documented.
The --debug leak tracker reports allocation sites as '<unit>:<line>'.
For objects allocated inside generic method bodies the two halves of
that pair came from different sources: the line number is the cloned
template AST's Line field (which refers to the template's source file),
but the unit name was FCurrentUnitName — the unit or program being
emitted when the generic instance's method bodies were generated, i.e.
the INSTANTIATING unit. A TListEnumerator<Integer> created inside
TList<T>.GetEnumerator (generics.collections.pas:316) was therefore
reported as 'P:316' for a 33-line program P.
Fix: record the declaring unit on TGenericTypeDef when the template is
registered (both the direct semantic path and the .bif import path),
copy it onto TGenericInstance at instantiation, and have both backends
temporarily switch FCurrentUnitName to GI.DefUnitName around the
emission of generic-instance method bodies, restoring it afterwards.
The report now reads 'Generics.Collections:316'.
The new DefUnitName fields are populated by the semantic pass at
runtime and are intentionally not serialised; bif-coverage passes
unchanged. Generic record and generic function instances still carry
the old mismatch and are tracked separately.
New e2e tests (QBE + native): TestDebug_GenericAllocSite_ReportsDefiningUnit
and TestDebug_GenericAllocSite_ReportsDefiningUnit_Native.
Four itab-dispatch ABI gaps reported from Andrew Haines'
unify_backend_interface branch, fixed on both backends:
- Record by const/value through interface dispatch (QBE) passed the
record as 'w <addr>' — truncating to the low 8 bytes and shifting
every later argument. Dispatch sites now use the same :_ffi_<Name>
aggregate ABI as direct calls. (Native already passed the address.)
- var/out parameters through interface dispatch loaded the VALUE
instead of passing the slot address. Root causes: a 1-based loop
over the 0-based var-flag string in MethodParamIsVar (parameter 0's
flag was never seen), the QBE statement path ignoring var flags, and
the native push loops never emitting addresses. Strings and
dynarrays covered.
- TFuncCallExpr receivers (GetDriver(x).Info()) previously raised
fail-loud. The receiver call is sret-evaluated into a temporary fat
pair; the owned +1 obj is released right after the consuming call
(QBE: pending-release list flushed by every call emitter; native:
pair kept above the hoist region, released preserving result regs).
- Discarded interface-returning itab calls in statement position
clobbered memory through the register the callee expects to hold the
sret buffer (QBE: missing buffer; native: receiver passed where the
buffer belongs, shifting Self). Statement calls now get a throwaway
sret buffer and release the returned obj. Semantic records the itab
return type on TMethodCallStmt (bif-coverage clean).
e2e tests run on both backends (cp.test.e2e.imap); IR tests pin the
aggregate ABI, slot-address passing, and the discard-sret+release
sequence (cp.test.interfaces).
Diagnosis credit: Andrew Haines (issue #89) correctly identified that
the subscript-assign path loads through a var/out parameter's slot only
once — the slot holds the ADDRESS of the caller's variable, so element
writes landed in the wrong memory and corrupted the heap. His patch
covered the QBE dyn-array and class cases; the class half had already
landed independently (248dccf). This implements the remaining cases
across BOTH backends in the current tree.
Subscript writes through var/out params (TStaticSubscriptAssign gains
IsVarParam, set by the semantic pass; bif-coverage status updated):
- dynamic arrays: one extra dereference to reach the data pointer
(writes were lost and stray stores corrupted the heap; SetLength and
element reads already worked),
- static arrays: load the array address from the slot instead of
offsetting the slot itself (QBE wrote into the parameter slot region;
the native ident READ also produced garbage — pmVar now treated like
the other by-ref param modes),
- PChar: extra dereference before the byte store (writes were lost).
Interface var/out parameters (previously did not even compile: QBE
emitted loads from a non-existent %_var_G_obj; native mis-spilled the
single incoming pointer as a two-register fat pointer):
- interface variables now occupy ONE contiguous 16-byte fat-pointer
block (obj at +0, itab at +8). QBE locals: a single alloc8 16 with
the _itab name derived at +8; QBE globals: a single 16-byte data item
$Name_obj with the itab half addressed as $Name_obj + 8 (the separate
$Name_itab item could not be guaranteed adjacent). Native locals and
globals were already contiguous.
- new IntfObjAddr/IntfItabAddr helpers route every QBE obj/itab access
(assignment variants incl. weak, dispatch, expr-pair reads, as-out
binding); var/out params dereference the slot first.
- native: var-param-aware receiver load in EmitInterfaceCall, var-param
LHS in EmitInterfaceAssign (shares the sret-Result pointer path),
interface globals usable as var args (leaq Name_obj), and the call
slot counter treats a var interface arg as ONE pointer slot (it was
counted as two, desynchronising the argument register pops).
IR assertions updated for the new global fat-pointer layout. New e2e
tests: TestRun_VarParamDynArray_WriteAndGrow,
TestRun_VarParamStaticArray_PChar,
TestRun_VarParamInterface_DispatchAndReassign.
Closes#89.
Interfaces may declare properties (FPC/Delphi parity):
IValued = interface
function GetValue(): Integer;
procedure SetValue(AValue: Integer);
property Value: Integer read GetValue write SetValue;
end;
Accessors must be methods of the interface or an inherited parent
(interfaces have no fields), validated at registration. I.Value reads
lower to the existing zero-arg getter itab dispatch; I.Value := X
lowers to the setter dispatch with X as the single argument — pure
compile-time sugar, no itab slots, no layout change. Child interfaces
see inherited properties via the parent chain. Wired end to end:
parser, TInterfaceTypeDef.Properties (AST + clone), TInterfaceTypeDesc
property registry, semantic read/write resolution, both backends, .bif
serialisation and import registration. v1 limits (recorded in
language-rationale): plain interface-typed receivers; no indexed/
default array properties.
Two pre-existing linking bugs surfaced by the dogfood program and are
fixed alongside:
- Property accessor names written in a different case than the method
declaration (read getValue for GetValue) produced unresolved symbols —
accessor names are now normalised to the declared casing at
registration (classes and interfaces, compile and import paths).
- A program-level class implementing an interface failed to link
whenever the program had a uses clause: program-scope methods carry
bare symbol names (uSemantic.CurrentUnitPrefix) but itabs and
property-setter call sites prefixed them with the program name via
Sym.OwningUnit. ClassUnitPrefix (QBE) / ClassSymName (native) now
skip the prefix for program-owned classes (new FProgramName field).
Tests: 7 in cp.test.interfaces (parse, registration, accessor
validation, read-only enforcement, inheritance, IR dispatch), bif
round-trip in cp.test.unitinterface, 2 e2e suites on both backends in
cp.test.e2e.classes2 (interface read/write incl. compound assignment
and inherited dispatch; case-mismatch + uses regression). Suite: 2940
OK on working and fixpoint binaries; FIXPOINT_OK; NATIVE_FIXPOINT_OK.
Array-typed FIELDS were second-class citizens for element access; this
lands the full variation family on both backends:
- Semantic: r.A[i] := v dropped the subscript from the LHS type — the
parser stores it in TFieldAssignment.PropIndexExpr (the indexed-
property slot) and semantic ignored it for real fields, demanding the
whole array type on the RHS. A subscript on a real dyn/static-array
field is now an ELEMENT write (new semantic-set IsElemWrite flag);
both backends emit the element store with the standard ARC and
record-copy rules.
- Parser: c.N.A[i] := v failed with "Expected 'end' but got '['" —
the chained L-value walker now accepts a terminating Field[idx] :=,
and the subscript-chain path accepts arr[i].A[j] := v (subscript
directly over another subscript stays a clear parse error).
- Bare implicit-Self: A[i] := v inside a method raised "Undeclared
variable 'A'" — TStaticSubscriptAssign now resolves array-typed
fields of Self (IsImplicitSelf + ImplicitFieldInfo).
- SetLength(r.A, n): QBE refused ("first argument must be a
variable"); native silently emitted NO code for field receivers and
mis-stored through var-param receivers. QBE routes through
EmitLValueAddr; native gains EmitLValueSlotAddr covering field,
var-param and implicit-Self receivers for dyn-array and string
SetLength.
- Read side: c.A[i] through a class variable computed the element base
as if c were an inline record (missing object-pointer load) and
segfaulted; implicit-Self bases had the same gap; native missed
chained reads (c.N.A[i]) entirely. All base shapes are handled in
the IsArrayAccess read paths of both backends now.
bif-coverage.status regenerated for the new semantic-set AST fields
(safe). Tests: 4 IR tests (cp.test.dynarray) + 6 e2e tests on both
backends (cp.test.e2e.records) covering record/class/implicit-Self
receivers, nested chains, static-array fields and string-element ARC.
Suite: 2922 OK on working and fixpoint binaries; FIXPOINT_OK (single
round); NATIVE_FIXPOINT_OK.
Introduce TParamMode (pmNone, pmVar, pmRecordValue, pmStaticArrayValue)
on TIdentExpr so the semantic pass records the precise param-slot
classification once instead of merging three cases into a Boolean.
The QBE backend treats all three modes identically (ParamMode <> pmNone),
preserving existing semantics. The native x86-64 and LLVM backends can
now dispatch on the enum directly instead of re-deriving the distinction
from Sym.Kind and TypeDesc.Kind at every reader site.
Other IsVarParam fields (TFieldAccessExpr, TMethodCallExpr, TAssignment,
TMethodParam, TMethodCallStmt) are unchanged.
Based on Andrew Haines' proposal and patch (blaise_llvm d16f877).
When the loop variable is Byte, for-in iterates raw UTF-8 bytes (existing
behaviour). When the loop variable is Integer, for-in now iterates Unicode
codepoints by calling _Utf8DecodeAt and advancing by the decoded byte
count. Any other loop variable type is a semantic error.
Both QBE and native x86-64 backends implement the new codegen path using
a synthetic __adv_N variable to preserve the byte advance across the loop
body. Includes E2E tests for 2-byte and 3-byte UTF-8 sequences, unit
tests for the semantic restriction, and 8 new E2E tests for the StrUtils
codepoint functions.
With mandatory () on all zero-argument calls (51ebf23), the
IsNoArgFuncCall/NoArgFuncDecl fields on TIdentExpr and the
synthesised TFuncCallExpr codegen path are dead code. Remove them.
This exposed ~300 bare function calls missed by the original
migration script across compiler, runtime, stdlib, tests, and
embedded Blaise source strings. Fix all of them.
Add IsProcFieldCall support to TMethodCallExpr so that
H.Handler() works for procedure-type fields — the parser creates
TMethodCallExpr for Obj.Name(), and the semantic pass now detects
when 'Name' is a procedural-typed field rather than a method,
routing through the indirect-call codegen path.
Update fixpoint.sh to fall back to an existing blaise_rtl.a when
the release binary is too old to build the runtime.
2627 tests pass, FIXPOINT_OK.
Make parentheses mandatory on every function, procedure, method, and
constructor call — even those with zero arguments. A bare identifier
or field access is now unambiguously a variable/field/property read;
appending () makes it a call.
Mechanically migrated all 144 source files (compiler, runtime, stdlib,
tests, kanban tool). Fixed several latent bugs exposed by the AST node
transition from TFieldAccessExpr.IsMethodCall to TMethodCallExpr:
- IsBuiltinToString applied to record methods (added tyClass guard)
- IsVarParam not set for value record/static-array parameters in
AnalyseMethodCallExpr (extended to check skParameter + aggregate type)
- Native backend used movq (pointer load) for record receivers instead
of leaq (address-of) in EmitMethodCallExpr
- ResolveDiamond now handles TMethodCallExpr for diamond-operator
constructor calls
Updated grammar.ebnf (MethodCall, ProcCall, Factor rules) and
language-rationale.adoc with the design decision. Marked the
future-improvements.adoc entry as implemented.
All 2627 tests pass. Fixpoint verified (FIXPOINT_OK).
The unit-cache warm path (--incremental --unit-cache) crashed on import
because the BIF interface reader could not handle several type patterns
and registration gaps:
- Add two-pass type registration (pre-register class/record/interface
names in pass 1, fill details in pass 2) so forward references within
the same unit resolve correctly
- Add ResolveInlineTypeName helper for field types stored as raw strings
(^Pointer, array[L..H] of T, array of T, class of T)
- Add RegisterProcType for TProceduralTypeDef import (e.g. procedure-of-
object types like TFieldCleanupProc)
- Fix 0-based string indexing in RegisterAlias (StrAt instead of [1])
- Register imported class methods in FMethodIndex so call-site resolution
works during warm compile
- Serialize/deserialize TPropertyDecl through BIF (clone, export, import)
so property access resolves on cached units
- Add unit-cache directory to search paths so .o files are found
- Include exception class name in compiler error output
Add the `threadvar` keyword for declaring thread-local storage variables.
Each thread gets its own zero-initialised copy. Only allowed at program
or unit scope.
Lexer/Parser: new tkThreadVar token; ParseVarBlock accepts threadvar.
AST: IsThreadVar field on TVarDecl, TAssignment, TIdentExpr.
Symbol table: IsThreadVar on TSymbol.
Semantic: propagates IsThreadVar through analysis; rejects local scope.
QBE backend: emits `export thread data` declarations and `thread $Name`
references for correct TLS access (local-exec model via %fs:@tpoff).
Native backend: emits .tbss section and %fs:Name@tpoff addressing.
Unit interface: serialises IsThreadVar through .bif files.
Tests: 8 unit tests (parser, semantic, IR emission) + 2 E2E tests
(compile and run programs using threadvars).
Extend the generics system to support record types alongside classes and
interfaces. Generic records use the same <T> syntax and monomorphization
strategy: each instantiation (e.g. TMyVal<Integer>) creates a concrete
TRecordTypeDesc with substituted field types and re-analysed method bodies.
Unlike generic classes, generic records do not emit typeinfo, vtable, or
field-cleanup data — they are value types with no class metadata.
Parser, semantic, QBE codegen, and native codegen all updated. 15 new
unit tests (parser + semantic + codegen) and 4 E2E tests. Fixpoint OK.
The parser absorbs [I] in R.Arr[I] into TFieldAccessExpr.PropIndexExpr,
which the semantic pass and codegen only handled for indexed properties
and string char access. Array-typed fields (dynamic, static, open) were
left unresolved, producing a type mismatch ("^array of T" instead of
"^ElementType").
Add IsArrayAccess flag to TFieldAccessExpr, resolve it in all three
PropIndexExpr sites in uSemantic (Base-chained, RecordName-leaf, and
trailing field path), and emit the element-address computation in
uCodeGenQBE for both EmitExpr and EmitAddrOfExpr.
Native backend has TODO markers for the new flag.
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.
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.
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
Phase 3 of the TUnitInterface work — populates the class-specific
fields on TTypeEntry that consumers need to lay out instances and
resolve method calls without seeing the source class declaration.
PopulateClassEntry walks TClassTypeDef and writes:
- ParentClass — TQualTypeRef resolved against ADeps
- Implements — list of qualified 'Unit.Name' (or local 'Name')
interface names
- Methods — list of TRoutineSig per declared method, sigs
only (bodies stay in the source unit's .o)
- Attributes — class-level custom-attribute names verbatim
Forward-declared classes ('type TFoo = class;' in interface +
'type TFoo = class ... end' in implementation) merge: the exported
entry carries the full body, not the stub. This is the documented
impl-leak called out in the TUnitInterface unit header — Blaise
allows the pattern and consumers need the full layout.
Generic classes (TGenericTypeDef wrapping TClassTypeDef) get their
class-body fields populated the same way.
Out of scope (Phase 4):
- VTableSlot population — requires uSemantic to have assigned
slots before Export runs. PopulateClassEntry leaves VTableLayout
empty until that pipeline is wired.
- InstanceSize computation — same dependency on uSemantic.
Method-level IsPublished propagates through BuildRoutineSig now;
private/protected/public are still silently merged into the
non-published bucket by the parser.
51 loader-testrunner tests now passing: 45 green, 2 explicitly
pending Phase 4 semantic data, 2 pending uAST feature gaps (out
params, calling conventions), 1 pending parser support for generic
free routines in unit interface sections.
Pre-commit gate:
- compiler rebuilt clean
- TestRunner: 2249 tests, same 2 pre-existing failures
- fixpoint.sh: FIXPOINT_OK at stage-3/stage-4
Foundation for letting downstream units compile against a self-contained
description of an upstream unit's interface section, without keeping the
upstream's source TUnit alive. This is Phase 1+2 of the loader split
plan: produce the artifact, not yet consume it. No existing pipeline
behavior changes — Blaise.pas still drives the full source TUnit chain
through semantic and codegen as before.
New units:
uUnitInterface — TUnitInterface + TTypeEntry/TConstEntry/TVarEntry
/TRoutineSig/TInlineBody/TGenericBody, plus the
TQualTypeRef '(unit, type)' name-pair used for every
cross-unit reference. Case-insensitive lookups by
default; constructor takes ACaseSensitive: Boolean
for exact-match use cases.
uSemanticExport — ExportUnitInterface(TUnit, ADeps: TObjectList): walks
the parsed AST and produces a self-contained
TUnitInterface. Phase 2 scope covers types, consts,
free routines, inline bodies, generic types/routines,
used units, and cross-unit type-ref resolution.
Class layout (parent, methods, vtable, instance size,
attributes) is intentionally out of scope until
Phase 3 wires semantic-resolved data.
Supporting changes:
uAST.pas — TUnit.ImplUsedUnits new field, separates impl-section uses
from interface-section uses (an aspirational comment that
was previously never honoured); TMethodDecl.IsInline new
field captures the 'inline' directive at parse time;
CloneTypeDecl/Const/MethodDecl/MethodParam/TypeDef promoted
from forward decls to interface so uSemanticExport can
reuse them; CloneTypeDef extended to handle top-level
class/generic/interface/procedural defs (previously raised
on those, since it only saw nested defs inside method
bodies); CloneClass/Generic/Interface/Procedural TypeDef
added.
uParser.pas — implementation-section uses go to ImplUsedUnits;
'inline' directive sets TMethodDecl.IsInline in both
ParseForwardDecl and ParseMethodDecl.
uUnitLoader.pas — transitive load walks both UsedUnits and
ImplUsedUnits, since the parser now splits them.
Tests live in a new top-level module:
loader-testrunner/ — separate from compiler/TestRunner so the loader
work can iterate without churn against the main
test set; mirrors the [Threaded] subprocess
fan-out pattern. 39 green tests across structural
carry-over, lookups, cross-unit refs, body
pairing, and self-containment-after-free. Eight
stubs remain pending Phase 3 class export; two
pending uAST feature gaps (out params, calling
conventions); one pending parser support for
generic free routines in interface section.
Pre-commit gate:
- compiler rebuilt clean
- TestRunner: 2249 tests, 2 pre-existing failures (Const_LocalArray*,
ConstArray_RangeIndexed_Strings — latter documented in memory)
- fixpoint.sh: FIXPOINT_OK at stage-3/stage-4
Cross-unit symbol uniqueness via unit-name prefixing. Routines,
methods, vtables, typeinfo, impllist, itab, _FieldCleanup_ and
__cn_ data items all get a leading <unit>_ when their owning unit
isn't in the unmangled allowlist (System / rtl.* / blaise_*).
Generic instantiations keep their existing unprefixed names —
their stitched-together instantiation names are already unique.
Plumbing:
uSymbolTable:
- TSymbol gains an OwningUnit: string field.
- TSymbolTable gains a DefineOwningUnit field; when non-empty,
Define() auto-tags any incoming symbol that doesn't already
carry an owner.
uAST:
- TMethodDecl gains OwningUnit.
- TAddrOfExpr gains ResolvedFreeRoutine: TObject — a TMethodDecl
pointer populated by semantic when the inner expression is a
bare identifier naming a standalone routine. Codegen reads
MD.ResolvedQbeName through this without re-walking the symbol
table, and the field lives where it's used (the address-of
node) rather than polluting TIdentExpr.
uSemantic:
- IsUnmangledUnit allowlist (System, rtl.*, blaise_*, empty)
and MangleUnitPrefix helper.
- TSemanticAnalyser.CurrentUnitPrefix — '' in program mode,
MangleUnitPrefix(FCurrentUnitName) in unit mode (FProg=nil).
- AnalyseUnitForExport sets FTable.DefineOwningUnit := AUnit.Name
for the duration of the pass.
- AnalyseAddrOfExpr stashes MD on AExpr.ResolvedFreeRoutine.
uCodeGenQBE:
- ClassUnitPrefix(AClassName) consults FSymTable.Lookup for the
class's TSymbol.OwningUnit and applies the same allowlist
semantics as uSemantic.MangleUnitPrefix.
- ClassSymName(AClassName) = ClassUnitPrefix + AClassName — the
identifying suffix used everywhere class-data symbols are
constructed.
Behavior:
Free routines and class methods: ResolvedQbeName construction sites
in both AnalyseUnit and AnalyseUnitForExport prepend
CurrentUnitPrefix; vtable slot values get the same prefix.
Class data emissions ($typeinfo_, $vtable_, $impllist_, $itab_,
$__cn_, $_FieldCleanup_) all go through ClassSymName(TD.Name).
EmitInterfaceDefs threads ClassSymName(TD.Name) into the itab's
method-name slot and ClassSymName(IntfDesc.Name) into the
impllist's typeinfo slot — without this, vtable references the
prefixed name while the methods/typeinfos themselves carry the
bare name and link fails.
@-of-routine references read TMethodDecl(ResolvedFreeRoutine)
.ResolvedQbeName when available, falling back to the bare source
name (for any TAddrOfExpr constructed post-semantic).
End-to-end verified:
- Streams program (TFileOutputStream et al.) compiles + runs.
- TObject program compiles + runs.
- Cross-unit free-routine separate compile:
blaise MyDep.pas -> mydep.o (exports MyDep_Triple)
blaise UseMyDep.pas --skip-dep-codegen + qbe + cc
link succeeds, program prints 21.
- Compiler self-bootstraps clean (TThread.Start's
@ThreadTrampoline emits as $Classes_ThreadTrampoline,
matching the prefixed definition).
When a Pointer-typed expression (e.g. from TStringList.Objects[]) is
assigned to a class-typed ARC-managed local, the codegen previously fell
through to a plain scalar store — emitting no _ClassAddRef. The variable
then received a spurious _ClassRelease at scope exit, imbalancing the
refcount.
This was the root cause of the 3rd-generic-instantiation linker error:
FindGeneric returns TObject from FGenerics.Objects[] (Pointer-typed), so
each call to InstantiateGenericInterface emitted one extra _ClassRelease
on the template object. After two calls the template was destroyed; the
third call received a dangling pointer.
Fix: add a branch in EmitAssignment for
ResolvedLhsType.Kind = tyClass AND Expr.ResolvedType.Kind = tyPointer
that mirrors the existing tyClass→tyClass path (retain new, release old,
store), always emitting _ClassAddRef since a raw Pointer value never
carries a pre-existing owned reference.
Also remove the debug WriteLn trace left in TGenericInterfaceDef.Destroy.
Tests added:
- cp.test.arc: IR-level check that Pointer→class assign emits _ClassAddRef
- cp.test.e2e.arc: three instantiations of the same generic class all
compile, link, and produce correct output
Three fixes that were causing six e2e failures:
Local array consts collided at link time. Function/block/unit-level
`const X: array[...] = (...)` were emitted as `export data $X` with no
mangling. The RTL's own `Days` const (rtl.platform.posix.pas) then
clashed with any user program declaring a `Days` const
(`multiple definition of 'Days'`). Local array consts now use a mangled,
file-local label (`__bac_N_Name`): the mangled name is set in semantic
(AnalyseArrayConstDecls -> ResolvedQbeName / ConstArrayQbe), carried to
the bare-ident read path via TIdentExpr.ConstArraySymbol, and emitted as
non-exported `data` so two separately-compiled objects cannot clash.
Class/record consts keep their exported, type-qualified label.
TComponent fought ARC and crashed. The destructor force-Freed children
it held only as raw pointers, while a caller's variable still referenced
them — a use-after-free at scope exit, surfacing as infinite
Destroy->RemoveComponent->ClassRelease recursion. Ownership is now honest
with ARC: FOwner is [Unretained]; AddComponent takes a strong ref via
_ClassAddRef; Destroy releases the owner's hold rather than Freeing, so a
child the caller still holds survives and an unheld child is destroyed;
RemoveComponent only unlinks the slot (the dying child's ref already
reached zero).
Deflaked TestRun_Thread_Terminate_Flag. The worker could finish via its
`I > 1000` break before the main thread's Terminate landed, racing the
printed flag between 0 and 1. The loop now exits only via the terminate
flag (with a high safety cap), so FTerminated is deterministically True.
Unit tests in cp.test.constants.pas updated to assert the mangled,
non-exported label.
Inside a function, Exit(X) now assigns X to Result and returns, matching
Delphi/FPC:
function Classify(n: Integer): Integer;
begin
if n < 0 then Exit(-1);
if n = 0 then Exit(0);
Result := 1
end;
Pipeline:
- AST: TExitStmt gains Value (the parsed X) and ResultAssign (a
synthesised 'Result := X' built by semantic). CloneStmt copies them.
- Parser: the tkExit branch parses an optional (Expr) after Exit.
- Semantic: Exit(X) is valid only inside a function (Result in scope);
it is rewritten into a 'Result := X' TAssignment that is analysed like
any assignment, so it inherits return-type compatibility checking and
the widening / ARC handling for string and class returns. Exit(X) in a
procedure, or with a type-incompatible X, is a clear error.
- Codegen: emits the synthesised assignment (via EmitAssignment) before
the normal exit jump; CollectAddressTakenStmt walks it too. Bare Exit
is unchanged.
Tests: 5 IR/semantic cases in cp.test.flowjumps (parse attaches value;
function OK; procedure + type-mismatch errors; codegen stores Result
then jumps) and an e2e in cp.test.e2e.controlflow covering int and
string (ARC) returns plus fall-through. Grammar (ExitStmt) and
language-rationale updated. Full suite 2341 tests pass; fixpoint clean.
Allow a const declaration to take a set literal on its RHS, e.g.
const
Primary = [cRed, cBlue]; // type inferred: set of TColor
Both: TDirSet = [dNorth, dEast]; // type annotated
None: TDirSet = []; // empty set
Parser: a tkLBracket branch in ParseConstBlock parses the member
identifier list (or empty []) onto new TConstDecl.IsSet / SetElements.
Semantic: set consts are resolved in the second constant pass
(AnalyseArrayConstDecls), after AnalyseTypeDecls has registered the enum
members. AnalyseSetConstDecl resolves each member to its enum ordinal,
ORs (1 shl ord) into the bitmask, and registers the const with a tySet
type — the declared set type when annotated (members checked against its
base enum), otherwise the inferred 'set of <Enum>' (found-or-created and
defined globally). Members must share one enum; a non-enum member or an
empty unannotated set is a clear error.
CheckTypesMatch now treats two tySet types over the same base enum as the
same type, so an inferred 'set of TDir' const assigns to a TDirSet
variable — set values are structural, not nominal.
Codegen needs no change: a set const is an integer bitmask, emitted by
the existing constant-ident path.
Tests: 9 IR/semantic cases in cp.test.sets (parse, inferred/annotated/
empty OK, mixed-enum / non-enum / empty-unannotated failures, bitmask
fold), plus an e2e in cp.test.e2e.misc. Grammar (ConstRhs) and
language-rationale updated. Full suite 2328 tests pass; fixpoint clean.