Commit graph

190 commits

Author SHA1 Message Date
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 2708a6209e feat(const): compile-time integer constant expressions (#96)
A const declaration may now take a compile-time integer expression on the
right-hand side, not just a single literal:

  const A = 2 * 3;            // 6
        B = 2 + 3 * 4;        // 14 (precedence)
        C = (2 + 3) * 4;      // 20 (parentheses)
        D = 100 div 7;        // 14
        E = Base * 2 + 1;     // references a prior const

Previously only single literals and a flat, single-precedence bit-op chain
(a or b or c) were accepted; 2 * 3 failed with "Expected ';' but got '*'"
and (2 * 3) with "Expected numeric or string constant".

Parser: when the const RHS starts an integer expression (a leading '(', an
integer literal or leading-minus literal followed by a binary operator, or an
ident followed by a numeric operator), parse it with the existing
full-precedence ParseExpr into a normal expression AST stored on
TConstDecl.IntValueExpr. A bare literal keeps the fast IntVal path; '+' on an
ident is left to the string-concat path for backward compatibility.

Semantic: EvalConstIntExpr recursively folds the AST to an Int64, resolving
named-constant references against the symbol table. Folding in the semantic
pass (after all consts are registered) is what makes forward references work
regardless of declaration order. Wired into both the const-block and the
initialised-variable (var G = expr) fold sites. Operator precedence and
grouping come from the AST shape, superseding the precedence-unaware bit-op
token chain.

Scope: folds to integer. Float constant arithmetic and using a const as a
static-array bound remain separate, pre-existing limitations (noted in the
rationale).

Tests: eight IR/fold cases in cp.test.constants (multiply, parens, precedence,
div/mod, named-ref, unary minus, mixed arith+bitwise); one e2e case on both
backends in cp.test.e2e.misc. Docs: grammar.ebnf gains the ConstIntExpr rules;
language-rationale gains a Constant Expressions section.
2026-06-13 19:14:14 +01:00
Graeme Geldenhuys 9a511a22e2 feat(const): multi-dimensional array constants
Range-indexed array constants now support multiple dimensions in both the
comma form (array[0..1, 0..2] of Integer) and the equivalent nested form
(array[0..1] of array[0..1] of Integer), with nested initialiser groups
((1,2,3),(4,5,6)). Previously the const-declaration parser had its own
single-dimension array-type and value-list parsers, so any multi-dim const
failed at the type (Expected ']' but got ',') or the nested value group.

Parser: ParseConstArrayType walks one-or-more 'array[...] of' headers,
reading comma-separated ranges per header and recursing on a nested 'array';
each dimension's bounds go to CD.ArrayDimLows/ArrayDimHighs. The value
parser is now recursive (ParseConstArrayGroup/ParseConstArrayScalar),
flattening nested groups into ArrayElements in row-major order. Dim-0 bounds
mirror onto the legacy ArrayLowBound/ArrayHighBound so the single-dim path
is unchanged.

Semantic: BuildConstArrayType builds the nested static-array type
innermost-first and validates the flat element count equals the product of
dimension extents. Shared by the program/unit and class-const sites.

Codegen: the QBE emitter already lays a flat row-major blob, so it needs no
change for integer/string elements. The native emitter now drills through
nested static-array types to the innermost scalar to pick the element
directive.

Tests: parse/semantic/IR in cp.test.constants; e2e (comma, nested, 3-D) on
both backends via AssertRunsOnAll in cp.test.e2e.staticarray. Docs:
grammar.ebnf gains ConstArrayType/ConstArrayValue rules; language-rationale
updated (the stale 'enum index only' constraint corrected to document
range and multi-dimensional const arrays).
2026-06-13 18:48:28 +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 28bd5cdb38 docs(rationale): position on loosely-typed parameter passing
Record Blaise's stance on the four conflated "pass a loosely-typed value"
features: untyped const/var parameters, array of const, varargs, and Variant.

Blaise adopts only array of const - the one mechanism overloads and generics
cannot replace (arbitrary-length, mixed-type argument lists, as used by
Format-style APIs); it is a compiler-bounded, per-element-tagged call construct
rather than a free-floating dynamic value.

Untyped parameters are omitted on footgun grounds: they implicitly take an
address and erase the type, whereas a typed pointer (PByte / ^T) does the same
job explicitly and keeps type information for indexing/field access. varargs is
omitted as C-interop-only and untagged; Variant is omitted in favour of
overloads, generics, and explicit tagged unions that keep checking at compile
time.
2026-06-13 15:29:20 +01:00
Graeme Geldenhuys 54d7d2a573 feat(types): inline set types in any type position
A set type may now be written inline anywhere a type is expected (var,
parameter, record field, ...), not only in a named type declaration.  The
element type may be a named enumeration or an anonymous enumeration written
in place:

  var
    Days:  set of TWeekday;       // named enum element
    Flags: set of (fA, fB, fC);   // anonymous enum element

Implementation resolves an inline set entirely from its canonical type-name
string, with no parser-side state:
- Parser: ParseTypeName gains a 'set of' branch.  A named element yields
  'set of <Name>'; an anonymous element is encoded verbatim as
  'set of (a,b,c)' (ParseAnonEnumName), self-contained in the type string.
- Semantic: FindTypeOrInstantiate recognises the 'set of ' prefix and builds
  the TSetTypeDesc on demand (as it already does for 'array of'/'^'/'class
  of').  For the anonymous form, SynthAnonEnum synthesises an enum type from
  the encoded member list - registering each member as an enum constant - and
  reuses an identical inline enum if its members are already defined.  Set
  types compare structurally, so the synthetic enum name need only be unique.

Encoding the enum in the type string rather than carrying parser state makes
the feature work uniformly in every type position with no declaration-ordering
or ownership concerns.

Tests: cp.test.inlineset (parser/semantic) and cp.test.e2e.inlineset (compile
+ run on both backends).  Grammar and language rationale documented.
2026-06-13 13:45:13 +01:00
Graeme Geldenhuys ba51a53365 feat(vars): initialised global variables (var G: T = value)
A global variable declaration may now carry an initialiser whose value is
folded at compile time and emitted into the data section, so the variable
holds its initial value before the program body runs (matches FPC/Delphi):

  var
    G: Integer = 42;
    S: string = 'hello';
    A: array[0..2] of Integer = (10, 20, 30);

Implementation reuses the typed-constant pipeline end to end:
- Parser: the const value scanner is factored into ParseConstValue, shared by
  const declarations and the new var-initialiser path.  TVarDecl carries an
  owned InitConst: TConstDecl (TConstDecl moved ahead of TVarDecl so no
  forward declaration is needed).
- Semantic: AnalyseVarInitializer folds the value and type-checks it against
  the declared type; array initialisers derive element type and bounds from
  the resolved static-array type and mint a data label.
- Codegen: EmitGlobalVarInit (QBE) and EmitGlobalInitData (native) emit the
  folded value as a typed data slot - a single field for scalars/strings (a
  string global points at an immortal static header __sN + 12) and an inline
  element list for arrays.  Covers program- and unit-level globals on both
  backends.

Restrictions, each rejected with a clear diagnostic: global scope only (no
local initialisers), a single name per initialised declaration, and arrays
only for aggregates (record and inline-set initialisers are deferred - no
record-constant machinery yet, and a set initialiser would clash with the
const symbol the set folder defines).

Tests: cp.test.varinit (IR + parser/semantic) and cp.test.e2e.varinit
(compile+run on both backends).  Grammar and language rationale documented.
2026-06-13 13:29:43 +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 a2d263e0e8 feat(lang): guarantee zero-initialisation of all variables
Blaise now guarantees zero-initialisation of every variable as a language
semantic — local variables, globals, record fields, static-array elements,
threadvars, and Result.  The QBE backend already satisfied this; the native
x86-64 backend was the only gap (scalar locals were uninitialised on stack).

Native backend changes (blaise.codegen.native.x86_64.pas):
- Replaced the ARC-only zero-init loop in EmitFunctionDef with an
  exhaustive case over all TTypeKind values, covering every scalar
  type (integer family, float, boolean, pointer, enum, set, procedural)
  in addition to the already-handled managed types.  An else-raise clause
  ensures any future new type kind is caught at compile time rather than
  silently skipped.
- Fixed AddSlot to allocate 16 bytes for method-pointer locals (Code +
  Data slots), matching the 16-byte allocation the QBE backend already
  used.  Previously only 8 bytes were reserved, which would corrupt an
  adjacent frame slot if a local method-pointer was written.

10 new E2E tests run on both backends via AssertRunsOnAll, using a
Dirty() helper that pre-fills the stack with 0xDEADBEEF to prove zero-init
comes from the prologue and not from lucky stack layout:
  TestRun_ZeroInit_ScalarIntegers, FloatLocals, BooleanAndChar,
  PointerLocals, EnumLocal, SetLocal (QBE-only — native crashes on sets,
  pre-existing bug), RecordWithMixedFields, StaticArray, ThreadVar,
  GlobalVars.

Documented in docs/language-rationale.adoc (decision, alternatives
rejected, implementation notes, future noinit/definite-assignment roadmap).
2026-06-12 16:09:02 +01:00
Graeme Geldenhuys ff5c481ec0 test: add assembler/ELF writer tests and internal linker design doc
- TAsmEncodingTests: byte-level regression tests against AssembleToBytes
  (bare (%reg) operands, .quad symbol relocations, TLS prefix ordering,
  PC32 addend with trailing immediates, branch relocations,
  .note.GNU-stack presence, REX.X for extended index registers, imm16
  width, unknown-directive/duplicate-label errors, line-numbered
  diagnostics).
- TElfWriterTests: ELF header fields, section append/align/offset
  tracking, symbol definition and lookup, BSS reservation.
- TInternalAsmE2ETests: compile->link->run through the compiler CLI
  with --backend native --assembler internal, including class virtual
  dispatch (vtable .quad relocations) and float arithmetic (SSE
  spills).  A compile failure now Fails the test rather than being
  masked as a missing toolchain.
- docs/internal-linker-design.adoc: design for the next
  toolchain-independence phase — an internal ELF linker built into the
  blaise binary (PIE output, eager binding, .rela.dyn/R_X86_64_RELATIVE,
  TLS, CRT discovery, FreeBSD/macOS outlook, OPDF pass-through).
2026-06-12 09:55:44 +01:00
Graeme Geldenhuys 1a5a881d6c feat(semantic): reserve module names as identifiers
The program/unit's own name and the names of directly used units can
no longer be redeclared by top-level declarations, matching FPC and
Delphi (Duplicate identifier / E2004).  Previously the module name was
stored on the AST node and never entered the symbol table, so
'program P; var P: Integer;' compiled by accident of omission.

The semantic pass plants skModule marker symbols in the scopes that
receive top-level declarations (global + program-block scope for
programs, unit scope for units); the ordinary duplicate check on
TSymbolTable.Define then rejects redeclarations.  Lookup treats a
marker hit as unresolvable, so the reserved name is not a value and
inner scopes can still shadow it, as in FPC.  The const paths get an
explicit marker check because their Define-failure branch otherwise
tolerates the clash silently (cross-unit const shadowing).

One deliberate divergence: FPC accepts a procedure named after the
program (an accident of its overload machinery); Blaise rejects all
declaration forms uniformly.

Grammar is unchanged — this is a name-resolution rule, not syntax —
so grammar.ebnf is untouched; the decision is recorded in
language-rationale.adoc.  44 test programs named 'program P' that also
declared an identifier P are renamed to 'program Prg'.

Closes #84
2026-06-11 23:51:11 +01:00
Graeme Geldenhuys ca21b884bc feat(debug): link --debug-opdf binaries as PIE
pdr now resolves the ASLR slide correctly (load base from the
binary's offset-0 mapping), so the -no-pie guard in both native link
paths is no longer needed.  Debug binaries are position-independent
again, matching the platform default.

Verified under live ASLR: breakpoints, var-param drilldown, captured
vars, dynamic arrays, TList<T> inspection, callstack and stepping all
work against PIE binaries.

Remove the 'PIE (ASLR) support in PDR' section from
future-improvements.adoc — implemented.
2026-06-11 15:43:02 +01:00
Graeme Geldenhuys b9e0ea7a9e docs+test: collection ownership conventions — generic retention pinned
Resolves the TStringList.Objects / TList<T> retention question from the
bug backlog as an explicit design decision (language-rationale,
'Collection Ownership'):

- TList<T>/TStack<T>/TQueue<T> managed elements ARE retained on store —
  this already works (generic stores lower to ARC-aware pointer writes);
  TE2ETListTests.TestRun_TList_ClassElements_RetainedAcrossScope now
  pins it on both backends (object survives its creating scope).

- TStringList.Objects[] stays NON-OWNING by design: the API type is
  Pointer and the integer-cast idiom (TObject(PtrUInt(N)), used in 27
  places in the compiler itself) makes blind retention impossible.
  Convention documented at the declaration and in the rationale.

- Known limitation recorded: generic Clear/Destroy do not yet release
  remaining managed elements (needs a Default(T)-style zero-store);
  tracked in the leak backlog.

Suite: 2946 OK; FIXPOINT_OK; NATIVE_FIXPOINT_OK.
2026-06-11 11:55:34 +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 1b366f4c97 feat: implicit integer→float widening for math builtins and float typecasts
Sin(12) and Tanh(I) now compile: the float compiler builtins (Sqrt,
Ceil/Floor/Round/Trunc, Ln/Log2/Log10, Power, the trig family, IsNaN/
IsInfinite) accept integer-family arguments and widen them to Double —
matching FPC/Delphi and Blaise's own implicit int→float assignment
rule.  The trig builtins return Double for integer arguments (still
Single→Single / Double→Double for float arguments).  Power previously
type-checked nothing and emitted invalid IR for integer (and Single)
arguments; it now validates and coerces both to double.

Double(I) / Single(I) typecasts were lowered as bit copies: QBE emitted
an integer temp into 'stored' (rejected by qbe), native stored an
integer register — so the Tanh(Single(I)) workaround failed too.  Float
casts now emit real conversions on both backends (swtof/sltof/ultof/
uwtof + exts/truncd on QBE; cvtsi2sd/ss + cvtss2sd/cvtsd2ss natively),
including float→float width changes.

Fixing this exposed two pre-existing native float-width bugs: a Single
RHS assigned to a Double variable was stored without cvtss2sd, and
mixed-width binary operands (s * 1000 — integer literals are emitted as
.double constants) ran the Single binary path at the wrong width.
Added EmitXmm0WidthAdjust and applied it at assignment and to both
binary operands.

Decision recorded in docs/language-rationale.adoc (Implicit
Integer→Float Widening).  Tests: 8 IR/semantic tests in cp.test.math
(the two RejectInteger tests inverted to acceptance) + 2 e2e tests on
both backends in cp.test.e2e.math.  Suite: 2930 OK on working and
fixpoint binaries; FIXPOINT_OK; NATIVE_FIXPOINT_OK.
2026-06-11 10:17:00 +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 7b4db4d11d fix: record elements in arrays — subscript-LHS chains, deep copy, element reads
Dynamic arrays of records were broken in three interlocking ways:

- Parser: a[i].Field := v (and a[i].Method, chained a[i].F.G := v) on
  the statement LHS raised "Expected ':=' but got '.'" — the subscript
  statement branch only accepted ':=' directly after ']'.  It now
  builds a subscript-rooted postfix chain ending in a TFieldAssignment
  (via ObjExpr) or TMethodCallStmt (via ObjExpr).  grammar.ebnf gains
  SubscriptFieldAssign / SubscriptMethodCall rules.

- Both backends: a[i] := r stored the ADDRESS of r into the element
  instead of copying the record, and element reads loaded the first
  8 bytes of the element as if it were a pointer.  The two bugs masked
  each other (elements aliased r — the TElfSection workaround comment
  in uElfObject.pas documents the symptom).  Record-element subscript
  reads now yield the element address (dyn/open/static arrays) and
  writes do an ARC-aware fieldwise copy: EmitRecordCopy on QBE,
  retain-src/release-dest/memcpy on native.  Native static arrays of
  records had the same read/write bug and are fixed too.

- QBE backend: Exit inside the SECOND (or later) try block of a
  function skipped _PopExcFrame, leaving a stale g_exc_top that
  corrupted later raises/pops.  EmitTryFinallyStmt/EmitTryExceptStmt
  emitted normal and exception paths sequentially but decremented the
  codegen-time FExcDepth on both (net -1 per try statement).  Ported
  the native backend's rebalancing (restore depth before emitting the
  exception path).  This is the likely root cause of the historical
  'avoid bare Exit inside try' convention.

Tests: 3 IR tests (cp.test.dynarray), 4 e2e tests on both backends
(cp.test.e2e.records), IR pop-count + e2e regression for the exc-frame
bug (cp.test.exceptions, cp.test.e2e.exceptions).  Suite: 2912 OK on
working and fixpoint binaries; FIXPOINT_OK; NATIVE_FIXPOINT_OK.
2026-06-11 08:38:08 +01:00
Graeme Geldenhuys 939f03eb0e docs: native const-arg ARC port design — mark implemented
Records how the landed implementation differs from the plan: a single
unified hoist region (EmitArgHoist) for open-array literals,
record-call sret buffers and string pin slots, and shape-based
protection at unknown-signature sites instead of a TInterfaceTypeDesc
const-flag extension.
2026-06-11 07:36:13 +01:00
Graeme Geldenhuys bb2fe2da75 refactor: rename uCodeGen/uCodeGenQBE to blaise.codegen/blaise.codegen.qbe
Align the QBE backend unit names with the dotted naming convention
already used by the native backend (blaise.codegen.native.*).

  uCodeGen.pas    → blaise.codegen.pas
  uCodeGenQBE.pas → blaise.codegen.qbe.pas

Updated all uses clauses (~70 test files, main compiler units),
documentation references (README + 5 docs/*.adoc files), and
comment references in runtime/stdlib.
2026-06-10 12:12:14 +01:00
Graeme Geldenhuys fec6fa9ddd docs: update language rationale and grammar for codepoint iteration
Document the for-in string iteration dual-mode semantics: Byte loop
variable iterates raw UTF-8 bytes, Integer iterates codepoints via
_Utf8DecodeAt.  Remove "deferred to future Runes(S) iterator" notes
since CodePointAt and for-in Integer are now implemented.
2026-06-07 23:58:06 +01:00
Graeme Geldenhuys f6ea10194f feat(lang): support enum sets with up to 64 members (fixes #81)
Sets with 33–64 enum members now use 8-byte (QBE 'l' / x86-64 64-bit)
storage instead of silently truncating to 32 bits. Both QBE and native
x86-64 backends emit correct instructions for all set operations:
literals, in, Include/Exclude, union/difference/intersection, equality,
and for-in iteration. Enumerations with more than 64 members in a
set-of declaration are rejected with a clear semantic error.

Also fixes tkThreadvar missing from CheckUnitNamePart, which broke
parsing of unit names containing 'threadvar' in self-hosted builds.
2026-06-07 13:52:27 +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 0aa69e3bfb fix(stdlib): TThread.Create(False) now joins on Free via ARC
TThread.Create(False) appeared to silently skip execution (GitHub #73).
Root cause: Start took an extra _ClassAddRef so the trampoline could
release it on exit. This kept the refcount at 1 after Free, preventing
Destroy (and its WaitFor/pthread_join) from running — the main program
exited before the thread finished.

Fix: remove the trampoline's ARC reference entirely. The caller's
reference is the only one; releasing it triggers Destroy → WaitFor →
pthread_join, which blocks until the thread has fully exited. This is
safe because pthread_join guarantees the trampoline has returned before
Destroy frees the object.

Also remove FreeOnTerminate — ARC makes it redundant. In Delphi/FPC it
exists because manual Free is the only cleanup path; in Blaise, scope
exit or reassignment automatically joins and frees the thread. The
migration analyser can flag this for ported code.

Document the TThread ARC lifetime model in language-rationale.adoc and
add class/method documentation comments to classes.pas.
2026-06-06 14:56:18 +01:00
Graeme Geldenhuys 3bf220c7f4 feat(lang): add threadvar support for thread-local storage
Add the `threadvar` keyword for declaring thread-local storage variables.
Each thread gets its own zero-initialised copy. Only allowed at program
or unit scope.

Lexer/Parser: new tkThreadVar token; ParseVarBlock accepts threadvar.
AST: IsThreadVar field on TVarDecl, TAssignment, TIdentExpr.
Symbol table: IsThreadVar on TSymbol.
Semantic: propagates IsThreadVar through analysis; rejects local scope.
QBE backend: emits `export thread data` declarations and `thread $Name`
references for correct TLS access (local-exec model via %fs:@tpoff).
Native backend: emits .tbss section and %fs:Name@tpoff addressing.
Unit interface: serialises IsThreadVar through .bif files.

Tests: 8 unit tests (parser, semantic, IR emission) + 2 E2E tests
(compile and run programs using threadvars).
2026-06-06 12:03:36 +01:00
Graeme Geldenhuys e11e5dad5e feat: extend not operator to integer types (bitwise complement)
The `not` operator previously only accepted Boolean operands. It now
accepts all integer types (Byte, SmallInt, Word, Integer, Int64, UInt64)
and performs bitwise complement, enabling bitmask patterns like
`Flags := Flags and (not MASK)`.

Semantic pass promotes narrow types to Integer, preserves Int64/UInt64.
QBE backend emits `xor -1` (w or l suffix by width). Native backend
emits `notl`/`notq`. Also adds missing bitwise binary ops (and, or,
xor, shl, shr) and nested record field read/write support to the
native backend.
2026-06-06 00:21:59 +01:00
Graeme Geldenhuys 216e8704f5 feat(generics): support generic records (monomorphization)
Extend the generics system to support record types alongside classes and
interfaces.  Generic records use the same <T> syntax and monomorphization
strategy: each instantiation (e.g. TMyVal<Integer>) creates a concrete
TRecordTypeDesc with substituted field types and re-analysed method bodies.

Unlike generic classes, generic records do not emit typeinfo, vtable, or
field-cleanup data — they are value types with no class metadata.

Parser, semantic, QBE codegen, and native codegen all updated.  15 new
unit tests (parser + semantic + codegen) and 4 E2E tests.  Fixpoint OK.
2026-06-05 18:57:21 +01:00
Graeme Geldenhuys d69fcacaca feat(rtl): WriteLn prints True/False for Boolean values
Add _SysWriteBool to the runtime platform layer so WriteLn(Boolean)
prints 'True' or 'False' instead of '1' or '0', matching Delphi/FPC
behaviour. Both QBE and native x86_64 backends emit the new call for
tyBoolean arguments. Updated ~65 assertions across 13 test files.
2026-06-05 18:08:40 +01:00
Graeme Geldenhuys 0c5910195c refactor(rtl): drop legacy platform aliases, derive constants from target
Remove sLineBreak (Delphi alias for LineEnding) and PathDelim (Delphi
alias for DirectorySeparator).  Blaise keeps exactly three platform
constants: LineEnding, DirectorySeparator, PathSeparator.

Platform constant values are now derived from GTarget (the compilation
target) via TargetLineEnding/TargetDirectorySeparator/TargetPathSeparator
in blaise.codegen.target, so cross-compilation produces the correct
values without conditional compilation.
2026-06-05 17:01:04 +01:00
Graeme Geldenhuys a5f01c1cd1 perf(arc): elide const-param retain/release, retain transient args
Reapply the const-parameter ARC elision that was reverted in 03b59e8,
now paired with the caller-side transient retain that makes it sound.

The four entry/exit ARC loops skip IsConstParam again (a const parameter's
object is kept alive by the caller for the whole call, so the callee needs no
_StringAddRef/_StringRelease or _ClassAddRef/_ClassRelease pair). The earlier
revert was because that premise fails for a TEMPORARY bound to a const param
(e.g. `Use(A + ' ' + B)`): the concat result is +0, its only reference is the
argument slot, and with the callee retain elided it was freed mid-call.

The fix (Andrew Haines, cherry-picked from the llvm branch — commits 00fcf41 +
e94e70544) adds the missing reference at the CALL SITE: EnsureConstStringRef
emits _StringAddRef before the call and ReleaseConstStringArgs emits
_StringRelease after, for each value-mode argument to a const-string parameter.
The pair is a no-op on immortal literals and nets to zero on owned strings, so
the overhead falls only on the transients that actually need it.

Tests: restored the elision IR tests (string + interface const params),
Andrew's caller-retains-transient IR tests and valgrind e2e
(TestRun_ConstStringParam_TransientRetained_Valgrind), alongside the existing
TestRun_ConstStringTemp_StaysAlive_Valgrind. docs/future-improvements.adoc marks
the optimisation shipped. Full suite 0 failures; FIXPOINT_OK; the original
metaclass-ref crash (`C := TFoo`) compiles cleanly.
2026-06-05 08:08:19 +01:00
Graeme Geldenhuys 03b59e8ff0 Revert "perf(arc): elide retain/release for const string and class params"
This reverts commit 5a5b5d4. The optimisation elided the callee-side
ARC retain/release for const string/class/interface value params on the
premise that the caller keeps the argument alive for the whole call. That
premise fails for a TEMPORARY bound to a const param (e.g. `Use(A + ' ' + B)`):
the concatenation result's only reference is the argument slot, so without the
callee-side retain its refcount hits zero at the call boundary and it is freed
before the callee reads it — a use-after-free.

This bit the RTL hardest: `_StringCopy` / `StrHead` take `const string` params
and are called with built-at-runtime temporaries, so the emitted RTL was
miscompiled. Under self-hosting the defect is self-reproducing and only
manifests at the SECOND generation (the compiler that emits the broken RTL is
itself fine), which is why a one-step fixpoint did not expose it and the
compiler's own sources did not reliably trigger it. The symptom was a
deterministic crash compiling any program that uses a metaclass reference
(`C := TFoo`) or HasClassAttribute — which is why TestRunner (via
blaise.testing.runner.text) could not be built, blocking the whole suite.

The original change's valgrind e2e test passed only because it bound a string
LITERAL (immortal) to the const param, not a temporary.

Removed the now-invalid IR/e2e tests that asserted the elided behaviour
(string const params, and the interface-const variant from 088d12f which
relied on this commit's IsConstParam guard). Added
TestRun_ConstStringTemp_StaysAlive_Valgrind, which passes a concatenation
result as a const string param and reads it in the callee — the exact case the
optimisation broke. docs/future-improvements.adoc records how to re-attempt the
elision safely (condition on the argument, not the parameter; retain temporaries
either caller- or callee-side).

Verified: stage-2 build clean, TestRunner builds, full suite 0 failures,
FIXPOINT_OK.
2026-06-04 23:21:25 +01:00
Graeme Geldenhuys 41a16ad6cc feat(params): preserve calling-convention directives on routine declarations
cdecl/stdcall/register/pascal/safecall directives were recognised by the
parser but silently discarded. Record them on TMethodDecl.CallingConv (both
the standalone-routine and class-method directive loops), copy through
CloneMethodDecl, propagate into TRoutineSig.CallingConv in BuildRoutineSig,
and persist in the free-routine .bif format (appended per routine record,
symmetric writer/reader) so the convention survives separate compilation.

Codegen is unchanged: every routine still emits the System V AMD64 convention
(which is the C ABI on Linux x86_64, so cdecl and the default already agree).
The directive is metadata only — the prerequisite for a future Windows/x86
target where stdcall and cdecl differ, and for faithful FFI/debugger tooling.

Replace the pending-placeholder TestCallingConv_Cdecl_Preserved with a real
assertion that 'procedure Beep; cdecl;' yields CallingConv='cdecl'.

Grammar and rationale updated: MethodDirective notes the retained conventions
and a new rationale subsection records the metadata-only decision.

The compiler test suite is now fully green (2518 tests, 0 failures).
2026-06-04 00:56:24 +01:00
Graeme Geldenhuys 7dbf6c232c feat(params): track 'out' parameter mode distinctly via IsOutParam
'out' was parsed as a synonym for 'var' — by-reference, but indistinguishable
from 'var' afterwards. Add TMethodParam.IsOutParam, set alongside IsVarParam
when the 'out' keyword is present, and carry it through CloneMethodParam and
the .bif param-flags pack (new bit 3) so the loader's TRoutineSig and any
future tooling can recover the declared mode.

Codegen is unchanged: 'out' still lowers identically to 'var' (a pointer
parameter). The flag is metadata only — the prerequisite for a future
read-before-write lint and/or zero-on-entry semantics.

Replace the pending-placeholder TestParam_ModeOut_Preserved with a real
assertion that 'out' yields IsOutParam=True, IsVarParam=True, IsConstParam=False.

Grammar and rationale updated to match: ParamGroup gains the OUT alternative
and the out-parameter section documents the preserved-metadata decision.
2026-06-04 00:50:15 +01:00
Andrew Haines a1fdd9a0cb docs: name mangling reference 2026-06-03 19:40:08 +01:00
Andrew Haines 1723494326 docs: .bif file format reference 2026-06-03 19:40:08 +01:00
Andrew Haines d611314cc9 docs: extending the AST and .bif format — checklist + worked example
Captures the rule of thumb that every AST shape change must touch
three places (uAST.pas / uSemantic.pas / uUnitInterfaceIO.pas), with
codegen as a fourth when the node has runtime semantics.

Worked example walks through adding a hypothetical 'when' statement
end-to-end: parser, semantic, QBE codegen, .bif encoder/decoder, and
test surface.  Plus a shorter checklist for plain field additions,
common-pitfalls section, and the rationale behind the positional
(rather than tagged) .bif format.
2026-06-03 19:40:08 +01:00
Graeme Geldenhuys bb1cfb7676 feat(codegen): native backend M7a — record-returning functions (sret)
Implement the sret calling convention for functions/procedures that
return a record type, matching the QBE backend's hidden-first-pointer
approach:

Callee side (EmitFunctionDef):
- FSretFunc flag set in BuildFrame when ResolvedReturnType.Kind = tyRecord
- Result slot becomes an 8-byte pointer slot (nil type = pointer-size)
- Prologue spills the hidden sret %rdi into the Result slot (IntIdx starts
  at 1 so normal params continue at %rsi, %rdx, ...)
- No Result initialisation (caller's buffer is already zeroed by caller)
- Epilogue emits plain ret (no return value in %rax/%xmm0)

Field writes through Result (TFieldAssignment with RecordName='Result'):
- Load the sret pointer from the Result slot into %rcx
- Write through %rcx + field offset

Caller side (EmitSretCall):
- leaq dest → %r10 before arg evaluation (survives clobbers)
- Call memset(%r10, 0, TotalSize) to zero the destination buffer
- Reload %r10 after memset (caller-saves may be clobbered)
- Evaluate normal args and push; pop into %rsi/%rdx/... (index 1+)
- movq %r10, %rdi to place sret pointer as hidden first arg
- callq function

TAssignment detection: when LHS is a record and RHS is a record-returning
TFuncCallExpr, dispatch to EmitSretCall instead of EmitExprToEax.

TestRun_Native_RecordReturnFunction promoted from Ignore to AssertRunsOnBoth.
2383 tests pass; FIXPOINT_OK.
2026-06-03 13:57:14 +01:00
Graeme Geldenhuys d4f9f9e4a3 feat(lang): diamond operator — infer generic type args from LHS in assignments
TFoo<>.Create on the RHS of an assignment infers all type arguments from
the declared type of the LHS variable, eliminating the redundant repetition:

  var S: TStack<string>;
  S := TStack<>.Create;          { was: TStack<string>.Create }

  var D: TDictionary<string, Integer>;
  D := TDictionary<>.Create;     { infers both K and V }

Works for any number of type parameters.

Implementation:
- Parser: detects IDENT tkNotEquals DOT (the lexer folds '<>' into a single
  tkNotEquals token) in expression context and stores 'TFoo<>' as the sentinel
  RecordName in TFieldAccessExpr.
- Semantic: ResolveDiamond() in AnalyseAssignment replaces the '<>' sentinel
  with the full concrete type name from the resolved LHS type, before the
  normal constructor-call analysis proceeds.

Docs: grammar.ebnf and language-rationale.adoc updated.

Tests: 6 IR-level tests in cp.test.generics (parser sentinel, semantic
inference for 1 and 2 type params, IR identity with explicit form);
2 E2E tests in cp.test.e2e.misc (single-arg and two-arg, compile+run).
2026-06-03 11:29:29 +01:00
Graeme Geldenhuys 0f63626397 feat(lang): Exit(Value) function-result shorthand
Inside a function, Exit(X) now assigns X to Result and returns, matching
Delphi/FPC:

  function Classify(n: Integer): Integer;
  begin
    if n < 0 then Exit(-1);
    if n = 0 then Exit(0);
    Result := 1
  end;

Pipeline:
- AST: TExitStmt gains Value (the parsed X) and ResultAssign (a
  synthesised 'Result := X' built by semantic). CloneStmt copies them.
- Parser: the tkExit branch parses an optional (Expr) after Exit.
- Semantic: Exit(X) is valid only inside a function (Result in scope);
  it is rewritten into a 'Result := X' TAssignment that is analysed like
  any assignment, so it inherits return-type compatibility checking and
  the widening / ARC handling for string and class returns. Exit(X) in a
  procedure, or with a type-incompatible X, is a clear error.
- Codegen: emits the synthesised assignment (via EmitAssignment) before
  the normal exit jump; CollectAddressTakenStmt walks it too. Bare Exit
  is unchanged.

Tests: 5 IR/semantic cases in cp.test.flowjumps (parse attaches value;
function OK; procedure + type-mismatch errors; codegen stores Result
then jumps) and an e2e in cp.test.e2e.controlflow covering int and
string (ARC) returns plus fall-through. Grammar (ExitStmt) and
language-rationale updated. Full suite 2341 tests pass; fixpoint clean.
2026-06-03 00:06:52 +01:00
Graeme Geldenhuys 703e44dfa1 feat(semantic): accept set literals as 'set of' arguments
A bracket literal can now be passed directly where a `set of <enum>`
parameter is expected, e.g. Configure([optA, optC]) or Report([]) — no
named intermediate variable needed.

Previously such a call failed overload resolution: a bare [a, b] is
analysed (lacking set context) as an open-array of the enum, which does
not match the set parameter. Now:

- ArgMatchScore matches a TArrayLiteralExpr argument against a set
  parameter — non-empty when the members share the param's base enum,
  and the empty [] against any set — checked before the nil-arg bail so
  [] (which has no resolved type) is handled.
- AnalyseArrayLiteralExpr defers an empty [] to a nil type instead of
  erroring outright; it is only meaningful with a set target.
- After overload resolution, RetypeSetLiteralArgs re-points each matched
  set-literal argument's ResolvedType at the parameter's set type, so
  codegen emits the bitmask (EmitArrayLiteralExpr dispatches on tySet).
- CheckTypesMatch gained a nil-actual guard (a clean "no value type"
  error instead of a segfault on a stray []), and the assignment path
  rejects an empty [] assigned to a non-set LHS.

Fixes a second, latent bug exposed by passing sets by value: a `set of`
value parameter of <=32 members (QBE type w) was spilled in the prologue
with storel, which QBE rejects. Added a tySet case to both param-spill
sites (storew for w sets, storel for l sets).

Tests: 6 IR/semantic cases in cp.test.sets (arg OK / empty / wrong-enum
/ empty-non-set-assign fail, bitmask fold, w-width param spill) and an
e2e in cp.test.e2e.misc. language-rationale updated. Full suite 2335
tests pass; fixpoint clean.
2026-06-02 20:04:17 +01:00
Graeme Geldenhuys 62165dac3d feat(const): set-valued constants (const X = [a, b])
Allow a const declaration to take a set literal on its RHS, e.g.

  const
    Primary       = [cRed, cBlue];        // type inferred: set of TColor
    Both: TDirSet = [dNorth, dEast];      // type annotated
    None: TDirSet = [];                   // empty set

Parser: a tkLBracket branch in ParseConstBlock parses the member
identifier list (or empty []) onto new TConstDecl.IsSet / SetElements.

Semantic: set consts are resolved in the second constant pass
(AnalyseArrayConstDecls), after AnalyseTypeDecls has registered the enum
members. AnalyseSetConstDecl resolves each member to its enum ordinal,
ORs (1 shl ord) into the bitmask, and registers the const with a tySet
type — the declared set type when annotated (members checked against its
base enum), otherwise the inferred 'set of <Enum>' (found-or-created and
defined globally). Members must share one enum; a non-enum member or an
empty unannotated set is a clear error.

CheckTypesMatch now treats two tySet types over the same base enum as the
same type, so an inferred 'set of TDir' const assigns to a TDirSet
variable — set values are structural, not nominal.

Codegen needs no change: a set const is an integer bitmask, emitted by
the existing constant-ident path.

Tests: 9 IR/semantic cases in cp.test.sets (parse, inferred/annotated/
empty OK, mixed-enum / non-enum / empty-unannotated failures, bitmask
fold), plus an e2e in cp.test.e2e.misc. Grammar (ConstRhs) and
language-rationale updated. Full suite 2328 tests pass; fixpoint clean.
2026-06-02 19:48:27 +01:00
Graeme Geldenhuys 7efae6d6e0 docs(benchmark): add QBE 1.3 measurement entry
Records allocator microbenchmarks and compile-step timing after
updating the vendored QBE from v1.2 to v1.3.  The compile step
(QBE translating IR to machine code) is 3.5% faster in the stage-2
binary assembled by QBE 1.3, attributed to the new GVN/GCM passes.
2026-06-02 17:17:54 +01:00
Graeme Geldenhuys 0ce9507775 docs: rewrite 'Function calls requiring ()' section as proper design entry
Replace the raw conversation fragment with a structured improvement entry
covering the motivation (TIdentExpr.IsNoArgFuncCall ambiguity), the
interaction with the Result-variable convention, migration impact, and
effort estimate.
2026-06-02 09:54:14 +01:00
Graeme Geldenhuys 61264da0bf feat(lang): add [Unretained] non-owning reference attribute
[Unretained] is an unsafe non-owning class/interface reference (like Swift's
unowned(unsafe)): no reference counting and no weak-table registration.  The
field assignment is a plain pointer store and field cleanup is a no-op for it.
Use only when the referent is guaranteed to outlive the field.

This complements [Weak] (safe, auto-nils on referent free, has weak-registry
cost).  The two are mutually exclusive on a declaration.

Pipeline:
- semantic: HasUnretainedAttribute + resolution on field decls, with
  class/interface-only and not-both-with-Weak checks; IsUnretained propagated
  to TFieldInfo.
- codegen: EmitFieldAssignment and the implicit-Self field path emit a bare
  storel for unretained class fields; EmitFieldCleanupFn skips them.

Apply [Unretained] to the compiler's own non-owning class fields — every
field documented "not owned" across uAST (ResolvedType, ResolvedMethod,
ResolvedDecl, FieldInfo back-pointers, …) and uSymbolTable (type-desc
references, FParent back-pointers, NextOverload, …).  These pointed into
the symbol table's type pool / the AST tree, which the codegen was
needlessly ARC-managing — inflating shared TTypeDesc refcounts.  Marking them
unretained de-inflates those (TTypeDesc dropped from rc=4/6 toward rc=2) at
zero per-assignment cost.

Tests: 5 new cases in cp.test.weakref.pas (semantic rejection on non-class
and on [Weak]+[Unretained]; codegen asserts no addref on store, no
_WeakAssign, no release/clear in field cleanup).  Docs: language-rationale
gains a [Weak] vs [Unretained] section with a comparison table.

Fixpoint clean; 2269 tests pass.
2026-06-02 08:41:12 +01:00
Graeme Geldenhuys 8b524340ce fix(codegen): storel for proc-pointer static-array slots; add Expr()() postfix call
Two related bugs found while testing procedural types as open-array elements:

1. Static-array element stores for tyProcedural used 'storew' (32-bit) instead
   of 'storel' (64-bit), truncating the function pointer and causing a segfault
   at the indirect call site.  Fixed by adding tyProcedural to the storel branch
   in the static-array element-store path in EmitArraySubscriptStmt.

2. Calling through an array subscript expression (Fns[I]()) was rejected by the
   parser with "Expected 'end' but got '('".  The postfix chaining loop only
   handled '.', '[', and '^'; it did not recognise '(' as a postfix call on a
   non-identifier expression.

   Fixed by introducing TIndirectFuncCallExpr (callee is an arbitrary TASTExpr),
   extending the postfix loop in ParseFactor to emit it when '(' follows any
   expression, and adding the corresponding semantic analysis and codegen paths.
   The codegen uses the callee value directly as the call target — no extra loadl,
   since the subscript load already yields the function pointer value.

Adds E2E test TestRun_OpenArray_ProcType_CallEach covering a static array of
TIntFn passed as an open-array parameter and called element-by-element via the
direct Fns[I]() syntax.
2026-05-22 17:10:02 +01:00
Graeme Geldenhuys a208906601 docs: update future-improvements.adoc removing completed items. 2026-05-22 16:57:19 +01:00
Graeme Geldenhuys aeb3e4ae8d feat(lang): extend High/Low to ordinal types; targeted float error
High and Low previously accepted only arrays and strings.  They now
also accept any ordinal type — Integer, Int64, UInt32, UInt64,
SmallInt, Word, Byte, Boolean, and enums — as either a type name or
an expression.  The result type matches the argument type so
High(Int64) round-trips through 64-bit code paths without truncation.
Bounds are folded at compile time to a literal QBE copy.

Floating-point arguments now produce a targeted error message
("not defined for floating-point types; use MaxDouble/MinDouble or
Math.Infinity") instead of the generic "must be an array or string".

docs/language-rationale.adoc records the decision; docs/grammar.ebnf
is updated to reflect the broadened intrinsic signatures.
2026-05-22 14:40:06 +01:00
Graeme Geldenhuys 2b501cdbde fix(lang): treat / as real division, distinct from div
The parser previously mapped both `/` and `div` to a single `boDiv` op,
so `Integer / Integer` was evaluated as integer division and returned
an Integer. `Trunc(Y / X)` and `Round(Y / X)` with Integer operands
were therefore rejected with "requires a float argument", and
`Round(7 / 2)` would have silently produced 3 instead of 4.

Introduce a separate `boSlash` AST op for `/`. Semantic gives it a
float result type unconditionally (Single only when both operands are
Single, Double otherwise); codegen reuses the existing float
arithmetic path. As a follow-on, `div` now rejects float operands
explicitly instead of accepting them silently.

Includes IR tests, e2e tests, and a rationale section documenting the
`/` vs `div` split.
2026-05-22 13:59:33 +01:00
Graeme Geldenhuys 34297e8317 feat(lang): add sar arithmetic-right-shift operator
`shr` stays logical (zero-fill) on all integer types, matching
Delphi/FPC semantics. The new `sar` keyword emits QBE's arithmetic
shift, preserving the sign bit on signed operands.

Closes BUG-003 (previously: signed Int64 `shr` silently discarded
the sign).  Resolved by adding a new operator instead of changing
`shr` semantics, so existing code ported from FPC/Delphi continues
to behave identically.
2026-05-22 13:40:25 +01:00
Graeme Geldenhuys e732c71cd1 feat(types): add packed record qualifier
Introduces the `packed record ... end` syntax.  Field layout in a
packed record skips natural-alignment padding between fields and
skips the record's tail padding, so SizeOf equals the cumulative
byte size of the fields.  ARC-managed field types (string, class,
interface, dynamic array) keep their natural 8-byte alignment so
that _StringRelease / _ClassRelease etc. can keep using aligned
64-bit loads through the field pointer.

`packed` is only legal directly before `record`.  `packed class`
and `packed array` are parse errors — neither has a meaningful
implementation in Blaise's heap-allocated class model nor in its
existing tightly-strided array layout.

Implementation:
  - Lexer: tkPacked token, PACKED keyword
  - AST: TRecordTypeDef.IsPacked, propagated through CloneTypeDef
  - Parser: optional PACKED prefix before RECORD; rejects other
    forms with a clear error message
  - uSymbolTable: TRecordTypeDesc.IsPacked + new FieldAlign helper;
    AddField / PackedSize / TotalSize / MaxAlign honour it
  - uSemantic: propagates IsPacked from def to desc in pass 1
  - OPDF: no change — emits whatever TotalSize reports

Tests: 11 IR-level + 2 e2e in cp.test.packedrecord.pas, plus the
TTokenKind audit bumped from 82 to 83.

Grammar and language-rationale updated.
2026-05-22 11:31:42 +01:00
Graeme Geldenhuys 5d092cafee feat(types): add SmallInt / Word 16-bit integer types
Introduces tySmallInt and tyWord first-class types alongside the
existing Integer / Int64 / Byte / UInt32 / UInt64 family.  Storage is
2 bytes (storeh / loadsh / loaduh) but values are widened to QBE 'w'
in registers, matching the Byte pattern.  Int16 and UInt16 are
accepted as aliases.

Implicit widening into Integer, Int64, UInt32 and UInt64 is permitted;
all 16-bit values fit losslessly in those wider types.

Tests: 11 IR-level + 5 e2e in cp.test.smallint_word.pas.

Grammar and language-rationale updated to remove the "deferred" note
on SmallInt/Word.
2026-05-22 08:51:21 +01:00
Graeme Geldenhuys b43a999f80 feat(types): add UInt64 / QWord type
Adds a real 64-bit unsigned integer type with two equivalent names:
UInt64 (Delphi style, matches the existing Int64) and QWord (FPC
style).  PtrUInt now aliases UInt64 too — it's the natural pointer-
sized unsigned on 64-bit.

Language semantics:
- Arithmetic on UInt64 uses udiv/urem; add/sub/mul/and/or/xor/shl/shr
  are bit-identical to their signed counterparts.
- Comparisons use unsigned QBE ops (cultl, cugtl, ...).
- Int64 <-> UInt64 mixing requires an explicit cast; the two types are
  not implicitly convertible in either direction.
- Decimal/hex literals in the (2^63, 2^64-1) range are typed as
  UInt64.  Smaller literals stay Integer/Int64.
- SizeOf(UInt64) = SizeOf(QWord) = 8.

Runtime:
- New _UInt64ToStr in blaise_str.pas plus a WriteDecimalU helper that
  uses UInt64 arithmetic.
- SysWriteUInt64 added to the platform abstraction and implemented in
  the POSIX layer.  WriteLn(UInt64) routes through it.
- IntToStr(UInt64) routes to _UInt64ToStr; explicit UInt64ToStr is
  also exposed as a builtin.

Bootstrap notes:
- Older release binaries cannot compile the runtime any more because
  rtl.platform.pas declares SysWriteUInt64.  A stage-2 rebuild from a
  fresh stage-1 is required after this commit on any worktree with an
  older stage-1, per CLAUDE.md.
- The parser stages literal Value through local var-params rather than
  writing directly to the new TIntLiteral.IsUInt64 field via class-field
  out-params.  Working around a stage-1 codegen bug where var-param
  calls that target a class field silently fail to write back.

Docs:
- docs/grammar.ebnf: built-in type list expanded with UInt64/QWord,
  integer-literal typing rules documented.
- docs/language-rationale.adoc: integer types table updated with the
  unsigned variants, Int64<->UInt64 strict conversion rule explained.

Tests:
- 15 new tests in cp.test.uint64.pas covering symbol-table
  registration, codegen instruction picking (udiv/urem/cultl/cugtl),
  literal-range typing, and e2e round-trips.
- Full suite: 2151 tests pass (up from 2132).
- Fixpoint clean at stage-3/stage-4 (expected: type-system change).
2026-05-22 08:08:35 +01:00
Graeme Geldenhuys 8f692cb0f8 feat(math): add ArcSin, ArcCos, Sinh, Cosh, Tanh builtins with Single dispatch
Registers five new trig intrinsics in the symbol table, semantic analyser,
and codegen. Single arguments emit the *f libc variants (asinf, acosf,
sinhf, coshf, tanhf); Double arguments emit the unprefixed variants.
Removes the now-implemented section from docs/future-improvements.adoc.
2026-05-17 01:56:35 +01:00
Graeme Geldenhuys f1d18755e8 docs(grammar): add INT_LIT/FLOAT_LIT token rules with all bases and underscores
INT_LIT previously only covered plain decimal.  Expand the Tokens section
to formally define all four bases ($hex, &octal, %binary, decimal) and
the underscore-separator placement rules.  Add FLOAT_LIT as an explicit
token production (it was absent entirely).  Character-class helpers
(dec_digit, hex_digit, oct_digit, bin_digit) are defined in the
accompanying block comment, consistent with the existing style for
letter/char/any_char.
2026-05-17 01:11:39 +01:00
Graeme Geldenhuys da2c5d2459 docs(benchmark): post-cutover measurements and libc baseline bench
Add bench_libc_malloc.pas with explicit external malloc/free/realloc
bindings so the libc baseline can still be measured after the cutover.
The original bench_blaise_mem.pas now measures blaise_mem too (because
the GetMem builtin emits _BlaiseGetMem), so it is no longer suitable
as a baseline.

Log the post-cutover state: blaise_mem now beats libc on small, mixed,
and retain-free-all workloads.  Realloc growth remains 1.6x.  The
compile-time win on the real test suite (~10%) exceeds the microbench
gap, suggesting real workloads are dominated by short-lived small
allocations where the freelist pop/push beats glibc tcache.
2026-05-16 18:36:55 +01:00
Graeme Geldenhuys a8a442e5f7 docs(benchmark): log post-phase-2-inlining R workload improvement
R workload moved 10-11 ms -> 8 ms (-20 to -27%) after enabling case
statements and larger bodies in the inliner.  M and H also improved
modestly.  S is essentially unchanged.  R now 1.6x malloc, down from
2.1x.
2026-05-16 16:37:36 +01:00
Graeme Geldenhuys f387bbf2b7 docs(benchmark): log post-inlining state + bootstrap refresh
Records the cumulative effect of the IsLarge fix, pointer-promotion
codegen, inline-candidate analyser, and leaf-function inlining
landed this session.  Bootstrap binary refreshed in-place;
releases/v0.8.0/blaise is now the verified stage-3 fixpoint of
the current source, so fixpoint converges at stage-2/3.

Headline numbers vs the original 2026-05-16 baseline:
  Small alloc/free:    14 -> 9 ms  (-36%)
  Mixed sizes:          8 -> 5 ms  (-38%)
  Realloc growth:      13 -> 10 ms (-23%)
  Large alloc/free:    33 -> 0 ms  (matches malloc)
  Retain + free-all:    5 -> 5 ms  (unchanged)
2026-05-16 14:45:37 +01:00
Graeme Geldenhuys 3e039bccea docs(benchmark): log codegen-limited finding on in-place realloc
Records the negative result from today's in-place arena-tail growth
experiment in blaise_mem: 100% hit rate but a 2x regression
(13 ms -> 23 ms) on the R workload.  Root cause is Blaise's
codegen lacking inlining and register allocation for locals — the
added checks cost more memory traffic than the saved memcpy of
16-128 byte payloads.

The note documents the lesson so future allocator-perf work can
skip re-doing the experiment: closing the malloc gap on
small-realloc workloads requires compiler-side improvements
(inlining + register allocation) first.
2026-05-16 10:29:07 +01:00
Graeme Geldenhuys 9847369e71 fix(rtl): restore large-alloc LIFO cache in blaise_mem
IsLarge() was reading the small-header Flags field at offset Ptr-4,
but TLargeHeader laid its AllocSize: Int64 across Ptr-8..Ptr-1, so
the Flags probe overlapped with the high half of AllocSize and was
always zero.  Every large block therefore routed through the small
free path and the LIFO cache was never populated, forcing a fresh
mmap on each large allocation.

Restructured TLargeHeader to:
  TotalMapped: Int64    (Ptr-16..Ptr-9)
  AllocSize:   Integer  (Ptr-8..Ptr-5)
  Flags:       Integer  (Ptr-4..Ptr-1)

LargeGetMem now writes Flags := FLAG_LARGE, IsLarge() returns the
correct value, and the cache reaches ~100% hit rate on the large
alloc/free workload (32 ms -> 0 ms for 10k x 64KB).

Also adds the two benchmark programs (bench_blaise_mem.pas for the
malloc baseline and bench_blaise_mem_custom.pas for blaise_mem) and
reformats docs/benchmark.txt as a dated log so future runs can be
tracked over time.
2026-05-16 10:09:20 +01:00
Graeme Geldenhuys 3123ecff4d docs: add testing strategy document
Captures the three-layer test architecture (compiler IR, compiler E2E,
library runtime) and the rationale for framework selection: blaise.testing
for compiler and stdlib tests, punit for low-level runtime tests.
2026-05-16 00:38:58 +01:00
Graeme Geldenhuys c0149496b6 feat(compiler): record methods + DateUtils RTL (five-type date/time model)
Records can now declare methods alongside fields.  Self is passed by
pointer (var-param at the QBE ABI level) so the language preserves value
semantics while methods can mutate the caller's record.

Compiler changes:
- AST: TRecordTypeDef.Methods; TMethodDecl.IsRecordMethod flag
- Parser: ParseRecordDef accepts function/procedure declarations
- Semantic: record methods analysed with Self as skVarParameter; tyRecord
  allowed as method-call receiver; Int64 operand promotes binary result
  type (fixes -Int64 truncation in DurationNegate)
- Codegen: EmitMethodDefs and AppendUnit emit record method bodies;
  EmitFieldAssignment handles record-typed fields via EmitRecordCopy or
  EmitRecordCallSret; EmitRecordCopy recurses for nested records;
  method-call sites distinguish record var-params from class variables
  when computing Self

RTL — new DateUtils unit (replaces TDateTime-as-Double):
- TDate, TTime, TDateTime: integer calendar fields, no timezone
- TInstant: signed Int64 nanoseconds since Unix epoch (UTC point-in-time)
- TDuration: signed Int64 nanoseconds (interval)
- TTimeZoneOffset: fixed +HH:MM/-HH:MM (full IANA tzdata deferred)
- C shim blaise_time.c uses clock_gettime, gmtime_r, timegm
- 36 e2e tests, fixpoint clean
2026-05-15 19:46:46 +01:00
Graeme Geldenhuys 51fdcad4b0 feat(compiler): range-indexed array constants (array[0..N] of T = (...))
The typed-constant parser only accepted enum-indexed arrays. Extend the
parser, semantic analyser, and AST to support integer-range indices so
`const X: array[0..6] of string = (...)` works alongside the existing
enum form. Update grammar.ebnf with the new production.
2026-05-15 08:31:46 +01:00
Graeme Geldenhuys a33d5aa879 docs(streams): record Stream I/O design rationale; remove from roadmap
Adds a "Stream I/O" section to docs/language-rationale.adoc covering:
the decision (Go/Okio-inspired shape, one-direction abstract bases plus
capability interfaces), rationale (cross-language survey lessons from
Java/Go/Rust/Okio/.NET/Python), why capability interfaces sit alongside
abstract classes (TBuffer as both source and sink), UTF-8 only in v1,
alternatives rejected (single-TStream root, TFilterStream decorator,
async-from-day-one), and TODOs flagged in code (segment-pool thread
safety, CopyStream fast paths, non-identifier interface arguments).

Removes the "Formal TStream decorator hierarchy" section from
docs/future-improvements.adoc — the streams subsystem is now
implemented across phases 1-5 (releases of the past few commits).
The implementation departs from the sketched TStream/TFilterStream
shape in favour of one-direction abstract bases + capability
interfaces, for the reasons documented in the rationale.

./scripts/fixpoint.sh clean.
2026-05-15 01:10:29 +01:00
Graeme Geldenhuys 3547d5a2ce feat(parser): array-of-enum typed constants
Supports constants of the form:
  const Names: array[TEnum] of string = ('A', 'B', 'C');
  const Costs: array[TDir]  of Integer = (1, 1, 2, 2);

The index type must be an enum; element count must match enum member
count (semantic error otherwise). Array const elements are emitted as
pre-initialised QBE data objects. String elements use immortal string
pointer references ($__sN + 12). Integer elements use w-typed words.

Changes:
  uAST.pas: IsArrayConst, ArrayIndexType, ArrayElemType, ArrayElements
    fields on TConstDecl; ArrayElements freed in destructor
  uSymbolTable.pas: ConstArray: TStringList on TSymbol (owned)
  uParser.pas: parse array[T] of E type annotation and (...) value list
  uSemantic.pas: AnalyseArrayConstDecls (second pass after type decls)
    called after AnalyseTypeDecls at all five analysis call sites
  uCodeGenQBE.pas: EmitGlobalConstData emits data objects for array
    consts; IsConstant guard for tyStaticArray routes through the
    global aggregate address path
  docs/grammar.ebnf: ConstDecl, ConstTypeAnnotation, ConstRhs updated
  docs/language-rationale.adoc: new Array-of-enum Typed Constants
    section; Supported types list updated in Typed Constants section

cp.test.constants.pas: 7 new tests. 1837 tests pass. Fixpoint OK.
2026-05-14 16:33:59 +01:00
Graeme Geldenhuys f6626f3707 feat(parser): typed constants — const Name: Type = Value
Supports type annotations on constant declarations for all scalar
types (Integer, Int64, Double, Single, Boolean, string).

Changes:
  uAST.pas: add TypeName field to TConstDecl
  uParser.pas: detect ':' after const name, read type identifier
  uSemantic.pas: when TypeName is set resolve via FindType; report
    error for unknown type names; value codegen unchanged
  math.pas: Pi updated to typed form (Pi: Double = ...)
  docs/grammar.ebnf: ConstDecl and ConstExpr updated
  docs/language-rationale.adoc: new Typed Constants section

cp.test.constants.pas: 11 new tests covering Integer, Int64, Double,
  Single, Boolean, string, negative values, unit-level typed consts,
  and type annotation verification via symbol table inspection.
  1830 tests pass. Fixpoint OK.
2026-05-14 15:45:47 +01:00
Graeme Geldenhuys 2a018f7989 feat(dynarray): implement dynamic arrays (array of T)
Adds full support for heap-allocated, reference-counted dynamic arrays
across all compiler layers and the RTL.

- Parser: distinguish array[L..H] of T (static) from array of T (dynamic)
- Symbol table: tyDynArray kind + TDynArrayTypeDesc + NewDynArrayType
- Semantic: type resolution, Length(), SetLength(), subscript read/write
- Codegen: pointer-slot alloc, nil init, RTL calls, element read/write;
  QbeTypeOf returns 'l' for tyDynArray; Int64 element sign-extension
- RTL: _DynArraySetLength and _DynArrayLength in blaise_str.pas
- OPDF: EmitTypeDesc and EmitArray handle tyDynArray (IsDynamic=1)
- Grammar: DynArrayType rule added to grammar.ebnf
- Docs: dynamic array design decision in language-rationale.adoc
- Tests: cp.test.dynarray (14 tests: parser, semantic, codegen)

Fixpoint verified at stage-3/stage-4. All 1712 tests pass.
2026-05-14 11:05:04 +01:00
Graeme Geldenhuys b359ed4da7 docs(future): add Enhanced Enumerations section to future-improvements.adoc
Documents the design space for richer enum models with reference examples
from Java, Swift, Go, C#, and Oxygene (RemObjects), each showing both the
type definition and realistic application-code usage.

Options documented:
- Option B: built-in string name lookup (compiler-generated table)
- Option C: enum class with per-variant fields and methods (Java-style)

Recommended progression from done (explicit ordinals) through near-term
(Ord/Succ/Pred intrinsics), medium-term (Option B), and long-term (Option C).
2026-05-14 01:10:58 +01:00
Graeme Geldenhuys 85aa270c4c feat(enum): support explicit ordinal values in enum definitions
Enum members can now be assigned explicit integer ordinals, with
auto-continuation for unlabelled members:

  type TStatus = (Idle=10, Running=20, Done=30);
  type TCode = (A=100, B, C);   { B=101, C=102 }
  type TOffset = (Before=-1, At=0, After=1);

The ordinal value is stored in TEnumTypeDef.Members.Objects[I] as a
tagged pointer (PtrUInt), avoiding a dynamic array field that v0.7.0
cannot compile. TEnumTypeDef.OrdinalAt(I) is the typed accessor.

The semantic pass reads OrdinalAt(K) instead of the positional index K,
so ConstValue on each enum-member symbol reflects the declared ordinal.
Codegen already emits copy <ConstValue> — no codegen changes needed.

grammar.ebnf: add the EnumDef and EnumMember rules (previously missing).
2026-05-14 01:10:19 +01:00
Graeme Geldenhuys 285057c8a1 feat(parser): support named array type aliases in type section
type TArr = array[L..H] of T previously failed with a parse error because
ParseTypeDecl did not handle tkArray on the right-hand side of a type alias.

Parser fix: add tkArray to the existing tkCaret|tkIdent branch so all three
forms (array alias, pointer alias, simple name alias) share the same
TTypeAliasDef + ParseTypeName path.

Semantic fix: in the simple-alias resolution path, fall through to
FindTypeOrInstantiate when FTable.Lookup returns nil, so that
'array[L..H] of T' style names (which are created on demand, not
pre-registered) resolve correctly.  Previously the path raised 'Unknown type'
immediately without trying the on-demand instantiation route.

Tests: 9 new IR/semantic tests in cp.test.staticarray.pas.  New dedicated
e2e unit cp.test.e2e.staticarray.pas with 8 tests covering anonymous and
named alias forms, zero/non-zero base, Length/Low/High, multiple vars sharing
a type, and global+local var combinations.  Docs updated in grammar.ebnf and
language-rationale.adoc.  Fixpoint verified at stage-3/stage-4.
2026-05-13 17:16:42 +01:00
Graeme Geldenhuys efcee02363 docs(rationale): add Generic Interfaces and Collections Framework section
Documents IMap<K,V> design decisions: generic interface syntax, class
implements generic interface wiring, var-param flags on TInterfaceTypeDesc,
itab/impllist codegen for generic instances, RTL collections policy, and
alternatives rejected.
2026-05-13 13:59:34 +01:00
Graeme Geldenhuys 47e96c3872 docs(migration): add TObjectList → TList<TObject> detection rule 2026-05-13 13:59:34 +01:00
Graeme Geldenhuys 04a3eb6df5 feat(forin): implement for..in iteration over set types
Adds a new for-in collection kind: set of TEnum.  The loop iterates
members in ascending ordinal order by scanning the bitmask one bit at
a time.  The set expression is evaluated once before iteration begins.

- uAST: IsSetIter, SetBitCount, SetMaskVarName fields on TForInStmt
- uSemantic: tySet branch validates ordinal loop var, injects synthetic
  __setmask_N and __idx_N slots
- uCodeGenQBE: bit-scan loop using shr + and 1; break/continue wired to
  forin_end / forin_next labels
- 7 IR unit tests (cp.test.forin) + 1 e2e test (set [Red,Blue] → 0,2)
- language-rationale and grammar.ebnf updated; generic forin marked done
2026-05-13 13:59:33 +01:00
Graeme Geldenhuys 110e000de0 feat(intrinsic): add Supports() compiler intrinsic for interfaces
Supports(Obj, IFoo) returns Boolean — calls _ImplementsInterface at
runtime to test whether Obj's class implements IFoo.

Supports(Obj, IFoo, Ref) does the same test and, on success, populates
the fat-pointer slots (obj + itab via _GetItab) of the out-variable Ref
under full ARC (retain new obj, release old obj slot).

The second argument is a type name, not a runtime value, so Supports is
a compiler intrinsic parsed specially in ParseFactor rather than a
library function. The third argument (when present) is the name of an
interface-typed out-variable.

Includes:
- TSupportsExpr AST node (uAST.pas)
- Parser intercept for Supports( in ParseFactor (uParser.pas)
- Semantic analysis: validates obj type, interface type, out-var type
  (uSemantic.pas)
- QBE IR emission for both 2-arg and 3-arg forms (uCodeGenQBE.pas)
- 7 unit tests covering parse, semantic, and IR codegen
- 2 e2e tests verifying runtime Boolean result and method dispatch
  through the assigned interface fat pointer
- Grammar and language-rationale documentation updates

Fixpoint verified at stage-3/stage-4.
2026-05-13 13:59:33 +01:00
Graeme Geldenhuys 1763d4f2d0 docs(lang): document that Ord() is not needed for string processing
In FPC/Delphi, for..in over a string yields Char, requiring Ord() to get
the numeric value.  In Blaise, S[N] and for B in S already yield Byte
directly, so Ord() is superfluous for strings.  Added a note to the
No Char Type section explaining this, and clarifying that Ord() remains
relevant for enumerations.
2026-05-12 18:05:25 +01:00
Graeme Geldenhuys f8abb16791 feat(lang): Low/High on strings + document for..in string iteration
Low(S) always returns 0; High(S) returns Length(S)-1, consistent with
0-based string indexing.  The semantic pass previously rejected both with
"must be an array"; codegen for High(S) reads the ARC header length field
directly (data_ptr-8, loadsw) and subtracts 1.

for B in S (B: Byte or Integer) was already implemented; this commit
documents the decision in language-rationale.adoc and grammar.ebnf.

language-rationale.adoc:
- Fix stale "1-based" description in String Subscript section
- Add "Low and High on Strings" section (decision, rationale, alternatives)
- Add "for B in S — String Byte Iteration" section

grammar.ebnf:
- Extend Low/High signatures to show Array | String
- Add ForStmt element-type annotation block

future-improvements.adoc:
- Add migration analyser suggestion to recommend for..in as replacement
  for pure character-walk index loops

7 new tests in cp.test.stringops.pas, all passing (1378 total, 0 failures).
Fixpoint verified.
2026-05-12 17:51:22 +01:00
Graeme Geldenhuys 079a06ea82 feat(lang): switch to 0-based string indexing throughout compiler and RTL
Blaise strings are now 0-based: S[0] is the first character, Pos returns
a 0-based index (-1 = not found), Copy takes a 0-based From argument.

Compiler changes:
- Add uStrCompat.pas bootstrap shim with StrAt, StrHead, StrCopyFrom,
  StrCopyTail, StrPos — thin wrappers that translate between FPC's
  1-based and Blaise's 0-based conventions
- Convert all string operations in Blaise.pas, uLexer.pas, uCodeGenQBE.pas,
  and uSemantic.pas to use the 0-based shims
- Add PosOrd/PosSubstr shims in uPasTokeniser.pas to keep its internal
  1-based FPos convention while translating at the boundary
- Fix UpCase codegen to extract ordinal via OrdAt when argument is a string
- Fix vtable emission in AppendUnit to use StrAt/StrCopyTail instead of
  1-based E.ImplName[1] and Copy

ARC fixes uncovered during self-hosting:
- Add string param AddRef/Release to EmitMethodDef (was only on EmitFuncDef)
- Add string and class ARC to var/out parameter assignment path

RTL changes:
- _StringPos, _StringPosEx: return 0-based index, -1 for not found
- _StringCopy, _StringDelete: accept 0-based From/Idx
- _OrdAt: accept 0-based index
- SplitIntoList in classes.pas: convert to 0-based loop

Tests: add coverage for method string param ARC, var-param string ARC,
constructor prefix matching (CreateFmt), pointer type aliases, metaclass
aliases. Update E2E tests for 0-based Copy/Pos semantics.

Docs: add 0-based string rationale to language-rationale.adoc and
migration analyser checklist to future-improvements.adoc.

Self-hosting fixpoint verified (114758 lines, stage-2 == stage-3).
2026-05-11 17:50:35 +01:00
Graeme Geldenhuys e3732807fd docs: expand concurrency section with ARC + threading design
Add detailed design considerations for TThread under Blaise's automatic
reference counting: the fire-and-forget safety problem and the recommended
self-referential threading solution. Include practical code examples,
async/await vs TThread comparison table, state-machine explanations for
async/await transformation, and effort estimates for implementation.
2026-05-10 23:36:05 +01:00
Graeme Geldenhuys 3e4d591c1d chore(doc): PasBuild rename --fpc to --compiler [already done] 2026-05-07 23:25:19 +01:00
Graeme Geldenhuys 9300fe8a46 chore(doc): new future improvement - If Conditional Operator (ternary operator) 2026-05-07 23:19:03 +01:00
Graeme Geldenhuys db7660a95e chore(docs): Remove Multi-Line string literal from future-improvements
It already landed in the compiler.
2026-05-07 23:18:24 +01:00
Graeme Geldenhuys 11f657caef feat(lexer): implement triple single-quote text blocks
Add '''...''' multi-line string literal syntax to the lexer and
tokeniser.  Opening ''' followed by a newline starts a text block;
closing ''' on its own line sets the indentation baseline for margin
stripping.  Single quotes inside the block require no escaping.

Disambiguation: ''' followed by a newline opens a text block; followed
by any other character falls back to the classic '' escape parse.

11 new tests in cp.test.textblock.pas cover basic blocks, margin
stripping, embedded quotes, empty blocks, relative indentation
preservation, disambiguation from '''', trailing content, tabs,
and blank line preservation.  All 1264 tests pass; fixpoint verified.
2026-05-07 19:52:23 +01:00
Graeme Geldenhuys 8f0a844d07 feat(compiler,rtl): implement TObject.InheritsFrom
Add _InheritsFrom RTL helper in blaise_arc.pas that walks the typeinfo
parent chain to check class identity. Wire IsBuiltinInheritsFrom through
the semantic analyser (tyPointer, tyMetaClass, tyClass receivers) and
codegen (emits call $_InheritsFrom with correct typeinfo loading).

Update punit with AssertInheritsFrom, AssertInheritsFromClass, and class
identity checks in AssertException and RunTestHandler. Update testpunit2
so DoTest21 correctly fails on a class mismatch (EError expected, EFail
raised). Add 5 unit tests and 6 e2e tests; all 1253 tests pass, fixpoint
confirmed.
2026-05-07 09:53:47 +01:00
Graeme Geldenhuys f3f0036381 docs: remove implemented entries from future-improvements.adoc
Removes const sections, Abs(), ClassName/ClassType (all confirmed
implemented). Updates Str() workaround note now that DoubleToStr/
SingleToStr exist. Updates InheritsFrom effort note now that ClassType
vmt slots are in place.
2026-05-07 08:36:28 +01:00
Graeme Geldenhuys 9613135345 feat(lang): case-on-string selector
Step 11f prerequisite.  The 'case' statement now accepts a string-
typed selector.  Each branch label must also resolve to a string
expression (typically a string literal); the runtime comparison
lowers to a call to _StringEquals against the selector value, so
existing string-equality semantics apply unchanged.

[source,pascal]
----
case Tag of
  'w': StoreInstr := 'storew';
  'd': StoreInstr := 'stored';
  's': StoreInstr := 'stores';
else   StoreInstr := 'storel';
end;
----

This unblocks self-hosted compilation of uCodeGenQBE.pas, which
already uses case-on-QType in four sites.  The internal motivation
matters: forcing those sites into hand-rolled if-elseif chains just
to make the compiler compilable by itself was rejected as a step
backwards from idiomatic Object Pascal.

Implementation:
  * uAST:  TCaseStmt gains an IsStringCase: Boolean flag set by the
            analyser so codegen can pick the right comparison.
  * uSem:  AnalyseCaseStmt now allows tyString selectors in addition
            to ordinal types; per-branch CheckTypesMatch already does
            the right thing for string vs string.
  * uCG:   EmitCaseStmt emits a 'call $_StringEquals(l SelTemp, l
            ValTemp)' result and reuses the existing 'jnz' fall-
            through pattern.  Ordinal cases still use 'ceqw'.

Tests added to cp.test.caseenum.pas:
  TestSemantic_CaseString_AcceptsStringSelector
  TestSemantic_CaseString_RejectsIntLabelOnStringSelector
  TestCodegen_CaseString_EmitsStringEqualsCalls
  TestCodegen_CaseString_OrdinalCaseStillUsesCEQW

Rationale documented in docs/language-rationale.adoc.

All 1242 tests pass (1238 + 4 new).
2026-05-06 08:00:00 +01:00
Graeme Geldenhuys 30753f818d docs(lang): grammar + rationale for ClassCreate (Step 11e)
Document the new ClassCreate builtin landed in 1fb2648:

  * grammar.ebnf — add ClassCreate and MethodAddress to the built-in
    functions reference table.
  * language-rationale.adoc — new section describing the decision
    (split RTL helper + static constructor call), why a per-class
    factory pointer was rejected, why metaclass.Create(args) wasn't
    made a method-call shape, and why we don't introduce a virtual
    constructor slot.
2026-05-06 07:37:18 +01:00
Graeme Geldenhuys bee768f290 feat(rtl): bcl.testing v0 — fpcunit runtime surface ported to Blaise
Step 11d.  Direct port of fpcunit's runtime surface, scoped to the
slice the 54 cp.test.*.pas regression units actually use.

bcl.testing.pas (compiler/src/main/pascal/) provides:

  * TTest, TAssert (AssertTrue/False, AssertEquals overloads for
    Integer/Int64/string/Pointer/Boolean, AssertNotNull/Null/Same,
    Fail), TTestCase (SetUp/TearDown virtual, RunTest dispatching
    via MethodAddress + TMethod cast to TRunMethod), TTestResult
    (counters + failure/error TStringLists + Summary),
    TTestCaseClass = class of TTestCase, EAssertionFailed
    (extends TObject; ToString override).

  * RegisterTest stores classes in a global TStringList for the
    runner (Step 11e) to enumerate.  Per-class published-method
    walking via metaclass-based virtual construction is deferred
    to 11e.

  * AssertException / ExpectException intentionally absent — no
    cp.test.* unit currently relies on them.

testbcl.pas smoke driver declares one TTestCase fixture with two
published methods (one passes, one fails), runs each via the
MethodAddress + procedure-of-object dispatch hot path, and prints
PASS/FAIL summary.  Compiles and runs end-to-end through the
QBE+gcc pipeline.

Two compiler bugfixes folded into the same step, both uncovered
while building the smoke driver and both with regression tests:

  1. ParseUnit now accepts dotted unit-name headers
     ('unit bcl.testing;') matching the UsesClause shape.  The
     parser only allowed a single IDENT after 'unit', which
     prevented any dotted-name unit (Generics.Collections, etc.)
     from being loaded through the unit loader.  grammar.ebnf
     updated to use UnitName in both positions.

  2. AnalyseFieldAccessExpr's chained Base<>nil branch was
     skipping AnalyseExpr on PropIndexExpr for method-backed
     indexed properties.  PropIndexExpr.ResolvedType remained
     nil and codegen segfaulted dereferencing it — visible as
     "Code generation error: Access violation" on inputs like
     R.Failures.Strings[I].  Constant indices ([0]) worked by
     accident because the literal-emission path didn't require
     ResolvedType.  Fixed: chained-base method-backed property
     reads now validate IndexParamName and analyse PropIndexExpr
     in the same shape as the non-chained and implicit-Self
     paths.  Regression test
     TestCodegen_IndexedProperty_ChainedBase_VarIndex_Compiles
     added to cp.test.properties.pas.

All 1235 compiler tests pass (1234 prior + 1 regression).
2026-05-06 00:32:11 +01:00
Graeme Geldenhuys a936ed1bd8 docs(lang): grammar + rationale for procedure of object (Step 11c)
grammar.ebnf:
* ProceduralType extended with the optional 'of object' modifier on
  both function and procedure variants.  Comment block describes the
  16-byte (Code, Data) layout, the byte-equivalence with TMethod that
  makes the TRunMethod(m) cast a no-op, and the call-site convention
  (Data passed as the implicit first argument).

language-rationale.adoc:
* Procedural Types section rewritten to cover both bare and method-
  pointer forms instead of listing 'of object' as deferred.  Records
  the IsMethodPtr compatibility rule (bare vs method pointers do not
  inter-operate even with identical signatures), the byte-equivalence
  with TMethod, and the canonical xUnit dispatch pattern.
* 'reference to' (closures) and calling-convention markers remain
  deferred; rationale updated accordingly.
2026-05-05 19:10:50 +01:00
Graeme Geldenhuys 6a22aa7828 feat(lang): default parameter values + metaclass refs uncovered by testpunit2
Three compiler additions, exercised end-to-end by testpunit2.pas:

* Default parameter values — single-name, non-var, non-open-array params
  may carry '= literal-or-named-constant'. Overload resolution accepts
  any arity in [MinArity, ParamCount]; the resolver tie-breaks toward
  the candidate that needs fewer defaulted slots. AnalyseProcCall and
  AnalyseFuncCallExpr clone the default expression into the call's Args
  list. Defaults declared on a unit-interface forward decl are
  ownership-transferred to the matching impl param at reconciliation.
* Metaclass references — a bare class type identifier in a value
  position (e.g. 'EError' as an argument or in 'Pointer(EError)') is
  now retyped to Pointer with codegen emitting '$typeinfo_<Name>' as
  an immediate. Matches the value held by vtable[0], so 'A.ClassType =
  EError' is true exactly when A is an instance of that exact class.
* Numeric widening at call sites — the existing CoerceArg helper now
  covers Integer→Double (swtof/sltof), Single→Double (exts) and
  Integer→Single, in addition to the pre-existing w→l. Inherited-,
  constructor-, method-, and function-call sites all route through it.

punit.pas gains DefaultDoubleDelta, two Double AssertEquals overloads,
and '= ''' defaults on the four-arity AddTest declarations.

Docs: grammar.ebnf extends ParamGroup with the optional default; a new
DefaultValue rule lists the permitted forms. language-rationale.adoc
adds 'Default Parameter Values' and 'Metaclass References' sections
recording the decision, alternatives rejected, and the overload
tie-break formula.

All 1194 existing compiler tests pass; testpunit2 compiles, assembles,
links and runs (31 tests; the expected pass/fail/inactive pattern).
2026-05-05 17:08:51 +01:00
Graeme Geldenhuys e6b8e1e532 fix(semantic): type-check indirect-call arguments; doc record-param ABI
Indirect calls through a procedural-typed variable (the 'IsIndirectCall'
path added when 'RunTest(@DoTest)' first compiled) only validated arg
count.  The argument types were never checked against the procedural
signature, so e.g. 'H("oops")' against 'procedure(N: Integer)' compiled
silently and miscompiled at the QBE level.

Both call sites — TProcCall (statement form) and TFuncCallExpr
(expression form) — now run each actual through CheckTypesMatch against
the corresponding TProcParamInfo, and reject non-L-value actuals for
var-typed parameters with the same diagnostic as the regular-call path.

Two semantic tests in cp.test.proctypes.pas pin the behaviour: one for
the statement form, one for the expression form.

Docs

- language-rationale.adoc: update the overload-mangling example so the
  emitted symbol matches what codegen actually produces ($Log_D_Si, not
  $Log$Si), and document the QBEMangle escape table for the three
  sigil characters that the QBE symbol grammar disallows mid-identifier.
- design.adoc: clarify the var-parameter ABI entry — record and static
  array value parameters share the by-pointer convention with var-params,
  which is what makes IsVarParam the right flag for codegen to key off.
2026-05-05 00:42:11 +01:00
Graeme Geldenhuys 3bc91bd035 feat(lang): standalone function overloading — Phase A (arity)
Adds the 'overload' directive and Delphi-style overload semantics for
standalone procedures and functions. Phase A resolves overload sets by
parameter arity only; the QBE mangling uses a temporary '$N<arity>'
suffix that will be replaced by full type-code mangling in Phase B.

- TMethodDecl gains IsOverload and ResolvedQbeName.
- TSymbol gains IsOverload, NextOverload (chain), and Decl back-pointer.
- TScope.Define appends to the overload chain when both old and new
  symbols carry IsOverload; mixing overload + plain duplicates is a
  duplicate-identifier error.
- ResolveStandaloneOverload picks the matching arity from FProcIndex
  and is wired into AnalyseProcCall and AnalyseFuncCallExpr.
- Codegen emits via TMethodDecl.ResolvedQbeName at all four standalone-
  call sites and at EmitFuncDef.
- Grammar: 'overload' added to MethodDirective.
- Rationale: documents Delphi semantics, type-code mangling, alternatives
  rejected.
- Tests: cp.test.overload.pas, 7 cases covering parser flag, chain
  acceptance, mixing rejection, no-matching-arity error, and codegen.
2026-05-04 10:22:21 +01:00
Graeme Geldenhuys faee6e6bef feat(lang): type aliases, floats, Abs, ClassName + global record fix
Six missing language features added:

1. type PFoo = ^TFoo — pointer and simple type aliases in type sections.
   Parser dispatches tkCaret/tkIdent to new TTypeAliasDef AST node;
   semantic pass resolves to TPointerTypeDesc or the aliased type.

2. Double / Single float types — lexer emits tkFloatLit; TFloatLiteral
   AST node; tyDouble/tySingle in the type system; QBE 'd'/'s' emit;
   arithmetic, comparison, and integer promotion in codegen;
   DoubleToStr, SingleToStr, StrToDouble, Abs(Double) built-ins;
   _DoubleToStr/_SingleToStr/_StrToDouble/_AbsInt/_AbsInt64 in RTL
   (new blaise_float.c). QBE generates SSE2 instructions automatically.
   Float const declarations supported.

3. Abs() — built-in for Integer, Int64, Double, Single.

4. TObject.ClassName — typeinfo gains a third slot (offset 16) holding
   a pointer to an immortal class-name string ($__cn_TFoo + 12).
   obj.ClassName loads vtable[0] (typeinfo), then typeinfo[16] (nameptr).
   EmitClassNameRef() emits the data-section label+offset relocation.

5. Global record field bug fix — FieldPtr() now accepts AIsGlobal and
   uses VarRef() so $RecordVar is used for global records instead of
   %_var_RecordVar. Both assignment and read paths fixed.

6. future-improvements.adoc — implemented items marked; Currency and
   BigDecimal deferred to BCL packages section added.

Tests: 1155 pass, 5 pre-existing errors (TUnitTests AV), 0 new failures.
2026-05-04 02:02:06 +01:00
Graeme Geldenhuys f256052a2b feat(types): bare procedural types (function/procedure pointers)
Adds support for declaring named procedural types that hold pointers to
standalone functions and procedures:

  type
    TIntFn   = function: Integer;
    TStrFn   = function(const S: string): Integer;
    TLogProc = procedure(Level: Integer; const Msg: string);

  var F: TIntFn;
  begin F := @MyFn; X := F(); end;

A procedural variable is stored as a single QBE 'l' (8-byte code
pointer). @FuncName produces a value of the matching procedural type.
Indirect calls F(args) load the pointer and emit a QBE indirect call.

Compatibility requires return types to match (both nil or both same
TTypeDesc) and parameter lists to match pairwise on type and parameter
mode (var/const/value); names do not participate.

Compiler additions:
* tyProcedural TTypeKind + TProcParamInfo + TProceduralTypeDesc
  (with IsCompatibleWith)
* TProceduralTypeDef AST node
* Parser: type T = function/procedure ... ; reuses ParseParamList
* Semantic: ResolveProceduralTypeDef in pass 2; AnalyseAddrOfExpr
  short-circuits @FuncName to a procedural-typed value;
  AnalyseFuncCallExpr accepts procedural-typed variables as
  indirect-call targets; CheckTypesMatch allows compatible
  procedural assignment
* Codegen: QbeTypeOf(tyProcedural) -> 'l'; EmitVarAllocs emits an
  8-byte slot; EmitAddrOfExpr emits $FuncName for @FuncName;
  EmitFuncCallExpr emits 'call %tmp(...)' for indirect calls,
  placed before the ResolvedDecl=nil type-cast branch

Out of scope (deferred until a use case requires them):
* function ... of object (method pointers — fat pointer ABI)
* reference to function/procedure (anonymous methods / closures)
* cdecl/stdcall calling-convention markers on procedural types

Tests: cp.test.proctypes.pas — 14 tests covering parser (kinds,
return types, params, var/const flags), semantic (compat accept/
reject on return type and arg count), and codegen (var allocation,
@FuncName emission, indirect-call emission).

Grammar and rationale: docs/grammar.ebnf adds the ProceduralType
production; docs/language-rationale.adoc captures the decision and
deferred items.

Motivation: prerequisite for porting Michael Van Canneyt's punit test
framework into rtl/src/test/pascal/, where every test, every
Setup/TearDown, and every hook handler is stored as a function
pointer.

1155 tests pass (1141 pre-existing + 14 new), no regressions.
2026-05-03 23:15:42 +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 5af72daac7 feat(exceptions): typed except handlers and bare re-raise (Step 8)
Implements `on E: TClass do` dispatch inside except blocks and correct
bare `raise` propagation inside typed handlers.

New AST node TExceptHandlerClause carries VarName, TypeName, and Body.
TTryExceptStmt gains a Handlers list and ElseBody for the typed case;
ExceptBody remains for the legacy catch-all form.

Parser detects the 'on' identifier after 'except' to switch between the
typed-handler path and the plain catch-all path.  The no-binding form
`on TFoo do` is also supported.

Semantic pass validates that handler types resolve to classes, injects
a synthetic TVarDecl into the enclosing block for each bound variable
(so EmitVarAllocs allocates a stack slot at function entry), and opens a
local scope so the variable is accessible inside the handler body.

Codegen emits a _CurrentException call (before _PopExcFrame) followed by
an _IsInstance check per handler.  The first matching handler stores the
exception pointer into the variable slot and runs the body.  If no handler
matches and there is no else clause, _Reraise propagates to the enclosing
handler.

RTL: _CurrentException now reads g_current_exception (set by _Raise at
raise time) rather than g_exc_top->exception.  This is correct because
_PopExcFrame has already unwound the handler's own frame by the time a
bare raise is executed inside the handler body.

EmitRaiseStmt bare-raise path now calls _CurrentException then _Reraise
instead of _Raise(0), which previously cleared g_current_exception.

22 new tests (17 unit + 5 E2E): parser, semantic, codegen, and runtime
dispatch (correct handler selection, subclass matching, unmatched reraise,
bare raise propagation, else clause execution).  Total: 1131 tests, 0 failures.
2026-05-03 10:30:15 +01:00
Graeme Geldenhuys 1931b84bc8 fix(compiler): self-hosting fixpoint unblocks — Cardinal, PtrUInt, xor, _ClassRelease
Seven source-compatibility fixes required to get the Blaise compiler to
parse and analyse its own source during the self-hosting fixpoint attempt.
None of these affect compiled output for existing programs; all 1105 tests
still pass.

  Cardinal alias (uSymbolTable): maps to UInt32; used in uDebugOPDF.pas
  PtrUInt alias (uSymbolTable):  maps to Int64; used in uDebugOPDF.pas
  xor operator (uLexer/uAST/uParser/uSemantic/uCodeGenQBE): tkXor/boXor
    emit QBE 'xor'; used in uDebugOPDF.pas FNV hash
  _ClassRelease external (blaise_arc.pas interface): C function declared
    callable from Pascal; contnrs.pas calls it directly
  contnrs.pas uses blaise_arc: makes _ClassRelease visible
  not 7 → -8 (uDebugOPDF.pas): Blaise 'not' is Boolean-only; -8 = not 7
  TProcess.Options removed (Blaise.pas): Blaise RTL TProcess has no
    Options property; pipes are always active
  FOutput.Strings[] (uDebugOPDF.pas): Blaise TStringList has no default
    [] property; must use explicit .Strings[I] form

Stage-2 IR now generates: 88,526 lines, exit 0.
Remaining blocker: %_var_FProgram QBE error (see handover.txt).
2026-05-02 19:59:04 +01:00
Graeme Geldenhuys 5083beb38f docs: document string data-pointer ABI decision and rationale
Adds a detailed implementation note under the String Subscript section
explaining the data-pointer memory layout, why Blaise chose this over
header-pointer, the relationship to FPC's convention, and the implication
for the Phase 8 Migration Analyser when porting FPC code.
2026-05-02 12:33:04 +01:00
Graeme Geldenhuys 4f82c7cc91 feat(compiler): for..in — string byte iteration (Step 7c)
Adds 'for B in S do' iteration over the raw UTF-8 bytes of a string,
consistent with S[N] returning Byte.  Loop variable may be any ordinal
type (Byte, Integer, etc.).

- uAST: TForInStmt gains IsStringIter field
- uSemantic: tyString branch in for-in dispatch — requires ordinal loop
  variable; injects synthetic __idx_N (Integer) slot into block Decls
- uCodeGenQBE: IsStringIter path — 0-based index, condition reads
  length from header at ptr+4 (csltw), element loaded via
  loadub(ptr+12+idx); loop variable assigned with storew

Language layout reminder: string header is [refcount(4)][length(4)]
[capacity(4)][data...] so length is at offset +4 and char data at +12.

Docs: language-rationale.adoc updated — string byte iteration now listed
as supported under 'for..in'; set iteration and generic collection
iteration noted as deferred.

Tests: 8 new tests (3 semantic + 5 codegen); 1105 total, all passing.
2026-05-02 11:12:52 +01:00
Graeme Geldenhuys 4ddc1944bd feat(compiler): for..in loop — class-based enumerator protocol (Step 7a)
Implements `for X in Collection do` iteration using the GetEnumerator
protocol, matching FPC/Delphi semantics.  This first commit covers
class-based enumerators; static array and byte-string iteration are
deferred to follow-on commits.

Compiler changes:
- uAST: new TForInStmt node (VarName, CollExpr, Body; semantic
  annotations: EnumVarName, ResolvedVarType, GetEnumDecl, MoveNextDecl,
  CurrentDecl)
- uParser: ParseForStmt returns TASTStmt; detects 'for X in' vs
  'for X :=' and produces TForInStmt or TForStmt respectively
- uSemantic: AnalyseStmt handles TForInStmt — verifies GetEnumerator/
  MoveNext/Current protocol, checks type compatibility, injects a
  synthetic TVarDecl (__forin_N) into the enclosing block so
  EmitVarAllocs allocates the slot and EmitArcCleanup releases it
- uCodeGenQBE: EmitForInStmt — calls GetEnumerator, ARC-stores result
  in synthetic slot, loops via MoveNext, reads Current, assigns to
  loop variable; handles both static and virtual dispatch
- uDebugOPDF: CollectStmtLines extended with TForInStmt case

RTL additions:
- classes.pas: TStringListEnumerator + TStringList.GetEnumerator
- contnrs.pas: TObjectListEnumerator + TObjectList.GetEnumerator

Docs:
- grammar.ebnf: ForStmt gains the 'FOR IDENT IN Expr DO Stmt' alternative
- language-rationale.adoc: new Iteration section documenting the protocol
  choice and what is deferred

Tests: 17 new tests in cp.test.forin (parser, semantic, codegen);
1089 total, all passing.
2026-05-02 10:21:12 +01:00
Graeme Geldenhuys 95f71e1acc feat(test): PDR integration test suite and pasbuild-integration-test plugin
Adds compiler/src/it/ (Maven-convention integration test directory) with
a Blaise-specific PDR driver. Four initial tests ported from the OPDF
integration suite: breakpoint+next, local variables, locals command,
and step-over. Test programs are adapted for Blaise (no FPC directives),
line numbers preserved to match the original commands files.

The pasbuild-integration-test plugin (phase: none) can be invoked as
'pasbuild integration-test'; it verifies pdr and the Blaise binary are
present, then delegates to compiler/src/it/run_tests.sh.

All four tests currently fail — line info in the OPDF section maps every
statement to the function-start address (per-stmt addresses require QBE
changes, tracked in future-improvements.adoc). The aspirational expected
files show exactly what each test should produce once OPDF is complete,
making failures a clear roadmap rather than noise.

Also adds PIE/ASLR support entry to future-improvements.adoc (Linux,
FreeBSD, macOS load-base strategies for the PDR debugger).
2026-05-02 00:35:00 +01:00
Graeme Geldenhuys 179d2e1b50 feat(compiler): external declaration support — Step 9a
Add the 'external' directive to proc/function/method declarations,
enabling Pascal code to bind to C symbols without a body:

  procedure Foo; external;
  function Bar: Integer; external name 'c_bar';

- uLexer: tkExternal token; mapped in the identifier fast-path
- uParser: ParseMethodDecl and ParseForwardDecl consume 'external'
  and optional 'name <string>'; body-trigger check is suppressed for
  external declarations so the following 'begin' is not consumed as a
  method body
- uAST: TMethodDecl.IsExternal, TMethodDecl.ExternalName
- uSemantic: external interface decls are exempt from the
  "no implementation" check; AnalyseStandaloneDecl skips AnalyseBlock
  when IsExternal is set
- uCodeGenQBE: EmitProcCall and TFuncCallExpr codegen use ExternalName
  as the QBE symbol when set; no function body emitted for external decls
- grammar.ebnf: MethodDirective / ExternalDirective rules; MethodDecl
  and StandaloneDecl updated to use { MethodDirective SEMICOLON }

11 new tests in cp.test.external — all 1035 tests pass.
2026-05-01 13:47:24 +01:00
Graeme Geldenhuys 7ff298165b feat(compiler): full const support — method bodies, class-level, grammar
- Fix ParseMethodDecl: add tkConst to body-trigger condition so const
  blocks are parsed inside method, procedure, and function bodies
- ParseConstBlock now accepts TObjectList directly (was TBlock) so it
  can be called uniformly from all scope positions
- Class body loop: handle CONST sections (→ TClassTypeDef.ConstDecls)
  and VAR keyword before field declarations
- TClassTypeDef.ConstDecls: new owned TObjectList of TConstDecl
- TFieldAccessExpr: new IsConstant/ConstValue/ConstString fields set by
  uSemantic when TypeName.ConstName resolves to a class-level constant
- uSemantic: register class constants as both unqualified and qualified
  (TFoo.MaxItems) symbols; resolve TypeName.ConstName in field-access
  analysis before raising "Unknown class method" error
- uCodeGenQBE: emit integer copy or _StringRetain for IsConstant paths
- docs/grammar.ebnf: add ConstSection/ConstDecl/ConstExpr rules;
  update Block, InterfaceSection, ImplementationSection, ClassDef, and
  GenericClassDef to allow ConstSection

All 1024 tests pass including 15 new const-scope regression tests.
2026-05-01 13:08:26 +01:00