TASTClass.Fields was a TStringList storing field names in the string slot and
source line numbers in Objects[] behind Pointer(PtrUInt(line)) /
Integer(PtrUInt(...)) casts. Split it into Fields: TList<String> +
FieldLines: TList<Integer> (the parallel-list idiom already used elsewhere in
this tool), removing both unsafe casts. Behaviour unchanged: coverage run
reports OK, 71 AST classes scanned.
Showcase generics + ARC across the test framework and tools. Replace
TStringList with TList<String> wherever only the basic list API was used
(Create/Add/Get/Count/Clear/Delete/IndexOf), rewriting .Strings[i] to .Get(i),
and remove manual .Free calls since Blaise reference-counts objects.
Lists genuinely needing TStringList-only API stay as TStringList: file I/O
(LoadFromFile/SaveToFile/Text), object association (AddObject/Objects[] in the
test registry and bif-coverage AST), and dup control (Duplicates). Destructors
that only freed fields are deleted.
stdlib 47 tests, compiler 3743 tests (20 pre-existing toolchain failures
unchanged), varcheck 18 regression tests all 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)
Master's mandatory-parens enforcement (51ebf23) now flags every bare
zero-arg function reference. Sweep BifCoverage.pas and its TestRunner
wrapper, and swap sLineBreak for LineEnding.
Previously the binary used `../../compiler/src/main/pascal/...` and
`bif-coverage.status` directly, locking it to invocation from
tools/bif-coverage/. Now it walks up from CWD looking for a directory
that contains both `compiler/src/main/pascal/uAST.pas` and
`project.xml`, then resolves every other path under that root. Lets
the verifier be invoked from the project root, from any subdir, or
from a test runner's CWD (compiler/) without setup.
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.