Commit graph

32 commits

Author SHA1 Message Date
Graeme Geldenhuys c45fcbca35 bif-coverage: replace TStringList-with-Objects[] casts with parallel generics
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.
2026-06-23 19:50:37 +01:00
Graeme Geldenhuys 676cee0cae Modernise stdlib testing + tools: TList<String> over TStringList, drop .Free
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.
2026-06-23 19:21:09 +01:00
Graeme Geldenhuys 74da3eadcc test(bif-coverage): extend drift guard to .bif interface types
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.
2026-06-22 12:38:28 +01:00
Graeme Geldenhuys 215082afc3 feat(codegen): implicit-virtual constructor dispatch via metaclass
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.
2026-06-17 10:07:47 +01:00
Graeme Geldenhuys 798df8eb15 feat(properties): default array property — Obj[I] sugar
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.
2026-06-16 14:48:38 +01:00
Graeme Geldenhuys 519aefdc5c fix(codegen): virtual-dispatch property accessors through the vtable
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).
2026-06-16 12:59:46 +01:00
Graeme Geldenhuys 74f71023da feat(oop): support 'inherited Method()' in expression position
`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.
2026-06-16 10:01:26 +01:00
Graeme Geldenhuys 9362114ee1 feat(sets): range syntax in set literals — [lo..hi] (#105)
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).
2026-06-15 17:14:05 +01:00
Graeme Geldenhuys fa853e0eb2 feat(sets): jumbo sets — set of enum up to 256 members
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.
2026-06-14 23:23:19 +01:00
Graeme Geldenhuys f830a9f1ac feat(params): array of const (heterogeneous variadic parameters)
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.
2026-06-13 17:37:59 +01:00
Graeme Geldenhuys ccd109ce77 feat(arrays): support multi-dimensional static arrays
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.
2026-06-13 11:56:38 +01:00
Graeme Geldenhuys 840b053812 fix: var parameters of dynamic-array, static-array, PChar and interface types
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.
2026-06-11 18:50:50 +01:00
Graeme Geldenhuys f8d75e586d feat: interface properties
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.
2026-06-11 11:04:47 +01:00
Graeme Geldenhuys 5f0fbb7643 fix: element access through array-typed fields — r.A[i], c.N.A[i], SetLength(r.A)
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.
2026-06-11 09:19:35 +01:00
Graeme Geldenhuys 3404ac13ea chore: adopt -SNAPSHOT version suffix, update README
Align the compiler's internal version strings with PasBuild's
convention: 0.11.0-dev → 0.11.0-SNAPSHOT (Blaise.pas, uCompilerId.pas).
Update rolling-bootstrap.sh to strip -SNAPSHOT instead of -dev.

README updates:
- Document the native x86-64 backend alongside QBE
- Update Phase 7 from "LLVM" to native backend parity
- Fix runtime layout (main/asm/, not main/c/)
- Add --backend native usage example
- Update test count to 2768
2026-06-10 10:39:14 +01:00
Graeme Geldenhuys 40755b987a refactor(ast): replace TIdentExpr.IsVarParam with TParamMode enum
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).
2026-06-10 02:06:43 +01:00
Graeme Geldenhuys 328493baf2 fix(bif): resolve bif-coverage gaps for current master
- 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)
2026-06-08 00:34:12 +01:00
Andrew Haines 00ca54afc4 fix(tools): add () to bare zero-arg calls in bif-coverage
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.
2026-06-08 00:27:24 +01:00
Andrew Haines 565fbea9bc fix(tools): bif-coverage resolves paths by walking up from CWD
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.
2026-06-08 00:27:24 +01:00
Andrew Haines 56c3fe1d0b feat(tools): add bif-coverage verifier for AST/encoder/decoder drift
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.
2026-06-08 00:27:24 +01:00
Graeme Geldenhuys 0a3a42f982 fix(kanban): add missing () on SystemOffset call in kanban.data.pas 2026-06-06 21:30:07 +01:00
Graeme Geldenhuys 57c0660a37 refactor(lang): remove IsNoArgFuncCall dead code, fix remaining bare calls
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.
2026-06-06 20:28:13 +01:00
Graeme Geldenhuys 51ebf2350a feat(lang): require mandatory () on all zero-argument calls
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).
2026-06-06 19:14:47 +01:00
Graeme Geldenhuys 43f5e49d9e feat(kanban): detect and merge external file changes in TUI
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.
2026-06-06 12:08:57 +01:00
Graeme Geldenhuys 3ca4c5291d refactor(kanban): move from examples/ to tools/ as a PasBuild module
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.
2026-06-06 00:54:26 +01:00
Graeme Geldenhuys 506ada7916 chore: remove dead {$IFDEF FPC} blocks, --cache-dir, and migrate_full.py
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.
2026-06-01 02:07:44 +01:00
Graeme Geldenhuys d8c0ef4ef6 fix: we don't have any code for migration-analyser yet. Disable tests for it. 2026-05-08 00:52:44 +01:00
Graeme Geldenhuys 5894411a35 chore(license): relicense from BSD-3-Clause to Apache 2.0 + Runtime Library Exception
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.
2026-05-03 19:45:29 +01:00
Graeme Geldenhuys 2cc192cd57 Stub program unit for the migration analyser. 2026-04-24 08:12:30 +01:00
Graeme Geldenhuys f66260093e Achieve self-hosting for hello-world compilation
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.
2026-04-24 00:21:43 +01:00
Graeme Geldenhuys 3fce5f86de Phase 1 bootstrap: Blaise compiler skeleton
Renames the project from 'Clean Pascal' to Blaise (after Blaise Pascal).
Adds the Phase 1 compiler pipeline with TDD test suite:

- uPasTokeniser: general Pascal tokeniser (ported from fpGUI IDE)
- uLexer: compiler-specific adapter — filters whitespace/comments,
  maps to TTokenKind, unescapes string literals
- uAST: typed AST node hierarchy (TProgram, TBlock, TBinaryExpr, etc.)
- uParser: recursive-descent parser for the Phase 1 BNF grammar
- uCodeGenQBE: QBE IR emitter — WriteLn/Write built-ins, integer
  variables, arithmetic, string literals
- Blaise.pas: compiler driver — parses flags, shells to qbe+cc
- FPCUnit test suites for lexer, parser, and code generator
2026-04-20 15:35:50 +01:00
Graeme Geldenhuys fd79a7db98 Initial project scaffold
PasBuild multi-module layout with three modules: compiler (application),
rtl (library), and tools/migration-analyser (application). Includes root
aggregator project.xml, debug/release build profiles, BSD 3-Clause licence,
README, .gitignore, and design document. QBE vendored as the Phase 1
backend. Bootstrap requires FPC 3.2.2.
2026-04-20 14:22:10 +01:00