A unit-qualified type-member reference written as a field access —
`Unit.TEnum.Member` (a type-qualified enum member) or `Unit.TFoo.StaticVar` —
resolved its base type through the flat uses-chain lookup, i.e. the cross-unit
last-wins winner, ignoring the unit qualifier. With two used units exporting a
same-named enum/type this bound to the wrong one: `ea.TPalette.paThree` failed
"Enum 'TPalette' has no member 'paThree'" because it resolved TPalette to eb.
TFieldAccessExpr now carries the parser-collapsed unit qualifier (like
TIdentExpr and TMethodCallExpr already do), and AnalyseFieldAccess resolves the
base via the directed ResolveQualified when it is set — so the reference binds
to the named unit's own type, independent of `uses` order.
Also completes the .bif round-trip for the qualifier fields: TFieldAccessExpr
.QualifierUnit and the previously-unserialised TMethodCallExpr.QualifierUnit are
now encoded/decoded, and bif-coverage.status records all the qualifier/owner
fields added across the cross-unit work (serialise vs safe).
Test: e2e cross-unit qualified enum member, order-independent.
`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.
Imported methods always had IsOverload=False: the .bif method layouts
round-tripped IsVirtual/IsOverride/IsStatic but not IsOverload, and the
importers never set it. ResolveMethodOverload's hiding walk stops at the
first non-overload candidate, so an overload set split across an imported
class and an imported ancestor would be truncated to the more-derived
level (latent today because the known overload sets live on a single
class).
Add IsOverload to both method-encoding paths and bump BLAISE-IFACE 5 -> 6:
* TRoutineSig (class methods): field added in uUnitInterface, set in
BuildRoutineSig, encoded/decoded in EncodeMethodSig/ReadMethodSig,
propagated in SynthesiseMethodDecl.
* TMethodDecl (interface and generic-template methods, the
EncodeMethodDecl/ReadMethodDecl path): the same flag was dropped there
too, so an overloaded interface method lost its directive across the
.bif.
Round-trip tests for both paths: TestRoundTrip_Class_WithOverloadedMethods
and TestRoundTrip_Interface_WithOverloadedMethods. bif-coverage status
gains TRoutineSig.IsOverload.
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.
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.
bif-coverage verified that every AST node field round-trips through the .bif
encoder/decoder, but the .bif interface-container types (TRoutineSig,
TUnitInterface, TMethodParam, TConstEntry, TVarEntry) were hand-serialised in
WriteMeta/EncodeMethodSig/etc. with no drift guard. Every cached-rebuild bug
just fixed was a serialised field on one of those types dropped from one side
of the round-trip — invisible to the tool, surfacing only as a runtime
miscompile.
Generalise the class scanner to ScanClassFile(path, names, objs, allowList);
add ScanInterfaceTypes() over an allow-list of the container types in
uUnitInterface.pas. Split uUnitInterfaceIO.pas into encoder-side and
decoder-side text and assert each serialised interface-type field's identifier
appears in both — the same looseness as the AST mechanism. Status file gains
the interface-type entries (serialise/safe); mutator-repopulated owning
collections are { no-bif }-exempt (their element data round-trips through the
per-entry encoders).
Negative test confirmed: dropping ImplUsedUnits/HasInitialization/VTableSlot
from either side is now reported as a gap with exit 1. Clean tree exits 0.
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.
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 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.
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.
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).
- Add TIndirectFuncCallExpr encoder/decoder to uUnitInterfaceIO.pas
(was missing entirely, causing --incremental to silently drop
indirect call nodes)
- Bump COMPILER_ID to blaise-0.11.0-dev+bif1
- Fix version check to compare base version only (strip -SNAPSHOT
and -dev suffixes before comparing)
- Change test to Ignore() when binary missing (module is not
activeByDefault)
- Remove redundant <version> from tool project.xml (inherits root)
- Regenerate bif-coverage.status for current AST (68 classes,
38 encoder/decoder cases)
Static-analysis tool that cross-checks uAST.pas against
uUnitInterfaceIO.pas and the root project.xml. For every TASTStmt /
TASTExpr subclass it confirms the class has a dispatch case in
EncodeStmt/EncodeExpr and ReadStmt/ReadExpr, then walks the public
fields and ensures each is either referenced from both encoder and
decoder (`serialise`) or explicitly excluded (`safe`).
Truth is checked-in: bif-coverage.status is a flat file with one
`<TClass>.<Field> <serialise|safe>` line per field. The default
invocation diffs the live sources against the status file and reports:
[version] COMPILER_ID does not match root project <version>
[encoder] missing (Class.Field, uAST.pas line)
[decoder] missing (Class.Field, uAST.pas line)
[new] field exists in AST but is not in the status file
[stale] status names a field or class the AST no longer has
[broken] serialise field missing from encoder or decoder
[drift] safe field has crept into encoder/decoder (with the
offending uUnitInterfaceIO.pas line)
`bif-coverage --reset` regenerates the status file from current state,
inferring `serialise` when the encoder references the field and `safe`
otherwise. Use after deliberate AST or .bif format changes to
re-baseline.