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.
Commit 50b8d75e made parentheses mandatory on statement-position calls,
but kanban_app.pas was not updated, so 'PrintUsage' (a bare reference in
the --help branch) failed to parse with 'Bare reference to PrintUsage
requires () for a call'. This broke a plain 'pasbuild compile', which
reaches blaise-kanban before blaise-bif-coverage. Add the parens.
The tool is now activeByDefault, so it is reached by a plain
'pasbuild compile'. With manualUnitPaths=true it only searches the listed
paths, which lacked the stdlib directory where SysUtils and the other
units it uses live, so the default build failed with 'Unit SysUtils not
found'. Add ../../stdlib/src/main/pascal, mirroring the compiler module's
own stdlib unit path.
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.
The mandatory-parentheses rule ("every function, procedure, method, and
constructor call requires parentheses, even with no arguments") made no
exception for 'inherited'. A bare 'inherited Create' is a method call and
should be rejected like any other parenless call, but the parser silently
accepted it.
Enforce the rule in both 'inherited' parse paths (statement and expression
position): a bare 'inherited Method' now raises the same "requires () for a
call" diagnostic as any other parenless call.
Also fix a misleading semantic error for the metaclass-variable case. A bare
'C.Create' on a 'class of T' variable parses as a field access, which reached
the "'C' is not a record or class" guard — confusing, since C is a valid
metaclass variable. AnalyseFieldAccess now detects a constructor/method name
on a metaclass receiver and emits the "requires () for a call" diagnostic
instead. (The parenthesised 'C.Create()' already worked: it parses as a
TMethodCallExpr, which has the metaclass-dispatch branch.)
Migrate every bare 'inherited Method;' call site in stdlib, the bif-coverage
tool, and embedded e2e test programs to the parenthesised form. The
parenthesised form compiles on the prior compiler too, so the migration and the
enforcement land together without breaking the rolling-bootstrap chain.
Docs: update language-rationale.adoc (inherited section + mandatory-parens
section) and grammar.ebnf (expression-position inherited rule).
Tests: add TestSemantic_MetaclassVar_BareCreate_RequiresParens and
TestParse_Inherited_Bare{Stmt,Expr}_RequiresParens.
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.
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.
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).
Add file-change monitoring to the kanban TUI so that tasks added via CLI
(or any external process) while the TUI is running are automatically
merged into the live view instead of being silently overwritten on save.
The implementation has three layers:
1. New FileAge built-in (compiler + runtime): returns the file's mtime as
Int64 via stat(), or -1 if the file does not exist. Follows the same
pattern as FileExists — symbol table registration, semantic analysis,
codegen, and POSIX ABI stub.
2. Merge-aware TBoard (kanban.data): tracks last-known mtime, deleted IDs
(so re-reads don't resurrect user-deleted tasks), and provides
HasExternalChanges/MergeFromDisk. Save() now auto-merges before
writing. ID conflicts (same ID, different task) are resolved by
reassigning the in-memory task to a fresh ID.
3. Idle polling in TKanbanUI.Run: every ~2 seconds (20 idle ReadKey
cycles at VTIME=1), checks HasExternalChanges and merges new tasks
with a status bar notification.
The kanban board is now a proper tool alongside migration-analyser.
Adds tools/kanban/project.xml (blaise-kanban module) and registers
it in the root aggregator. Removes the standalone build.sh script
since PasBuild handles compilation.
The compiler has been self-hosting since v0.7.0 and FPC is no longer
part of the toolchain. Remove the last FPC-conditional code paths:
- Blaise.pas: drop CacheKeyForFile, LoadCachedIR, StoreCachedIR and
both {$IFDEF FPC} whole-program IR cache blocks; remove --cache-dir
flag and CacheDir/UnitIR/UnitName/UnitPath locals; collapse the
ReadProcessChunk FPC/Blaise fork to the Blaise implementation only;
drop the poUsePipes/poStderrToOutPut Options block.
- tools/migrate_full.py: deleted — the Python migration analyser is no
longer needed now that the toolchain is fully Blaise-native.
Adopts the Swift/LLVM model: Apache License 2.0 with the Runtime Library
Exception text used verbatim by the Swift project. SPDX identifier:
Apache-2.0 WITH Swift-exception.
Apache 2.0 brings an explicit patent grant and patent-retaliation clause
that BSD-3 lacks. The Runtime Library Exception ensures binaries
produced by the Blaise compiler do not inherit attribution obligations
from the linked-in RTL.
* LICENSE — full Apache 2.0 + RLE text (replaces the deleted LICENCE).
* NOTICE — project header plus QBE attribution (MIT, vendored).
* docs/language-rationale.adoc — new Project Governance section
capturing the decision, alternatives considered, and rationale.
* SPDX headers updated across all .pas, .pp, .inc, .c source files,
build scripts, PasBuild plugins, and the Makefile.
* project.xml license field updated.
* tools/migrate_full.py HEADER template updated so generated
self-hosting source carries the new licence.
The blaise compiler now compiles its own source (tests/blaise-compiler.pas)
into a second-stage binary that successfully compiles and runs a hello-world
program. All 834 compiler tests still pass.
Codegen and RTL fixes that unblocked self-hosting:
- String literal interning (uCodeGenQBE): set FStrLits.CaseSensitive := True.
FPC's TStringList.IndexOf is case-insensitive by default, so distinct
literals like 'WRITELN' and 'WriteLn' collapsed to the same label.
- ARC on implicit Self.Field assignments (uCodeGenQBE): EmitAssignment's
implicit-Self branch did a raw storel for class/string fields. Fresh
_ClassAlloc'ed objects start at refcnt 0, so without the addref they were
freed by later local releases. Now addrefs new value and releases old.
- #nn character literals (uLexer): UnescapeString handled only 'text'.
Extended to decode #nn decimal literals and concatenated forms like
'abc'#13#10'def'. Local OrdAt helper lets the body parse under FPC and
the self-hosted compiler.
- Chr() builtin: registered in uSymbolTable, handled in uSemantic, emitted
in uCodeGenQBE as _Chr, implemented in rtl/blaise_str.c.
- LF line separator (Classes.pas): TStringList.GetText used #13#10; QBE
rejects CR in its input. Changed to #10.
Self-hosting source (tests/blaise-compiler.pas) — needs a virtual Destroy
on TASTNode and Exception (post-TObject-stripping they became root classes
without a vtable, breaking `is` checks and exception message layout).
TAST* descendants chain `override`.
Migration tooling: add tools/migrate_full.py — the script that generates
tests/blaise-compiler.pas by flattening the eight compiler units and RTL
Classes unit into a single self-contained Pascal source. The postprocess()
step injects virtual Destroy on TASTNode / TTypeDesc and strips `override`
from non-vtable root classes.