Commit graph

85 commits

Author SHA1 Message Date
mzhoot 94ae8c354c Рабочие синонимы 2026-07-04 21:44:54 +03:00
Graeme Geldenhuys ac6e0b0d88 docs: document array[NamedSubrange] index type
grammar.ebnf: the ARRAY [IDENT] OF T form is an ordinal-indexed array;
clarify it admits both an enumeration type and a named integer subrange
as the index, with a note on how each folds (enum -> array[0..N-1],
subrange -> array[lo..hi]).

language-rationale.adoc: add the array[TSubrange] of T row to the
supported array-index table, noting the subrange supplies the index
range while its values remain ordinary unchecked integers.
2026-06-30 21:23:35 +01:00
Graeme Geldenhuys a4190d73cf feat(lang): enforce member visibility — private/protected/strict
Visibility modifiers on class and record members are now enforced, not merely
parsed.  A `private` member is reachable only within the declaring unit; a
`protected` member additionally within descendant types; `public`/`published`
everywhere the type is.  Adds `strict private` and `strict protected`, which
narrow visibility to the declaring type itself (and, for strict protected, its
descendants) rather than the whole unit.  `strict` composes with `static`.

Parser: track the current visibility section in class/record bodies and the
contextual `strict` keyword (only before private/protected); carry the
visibility onto each field, method, and property declaration.  `strict public`,
`strict published`, and a bare `strict` are rejected.

Semantic: every qualified and unqualified member-access site checks visibility
via MemberVisibleTo / AssertMemberVisibleV, using the member's declaring unit
and declaring type.  Static (class-level) vars now carry Visibility and
OwnerTypeName on their TSymbol so a qualified static-var access enforces the
same rules; a strict/private static var written from another type is rejected
with a "not accessible" diagnostic.  Qualified static-var writes from a
permitted context are reported as not-yet-lowered rather than mis-resolved
(permitted writes use the unqualified form inside a static method).

Cross-unit: member visibility and declaring-type/unit origin are carried across
separately-compiled units in the .bif interface (BLAISE-IFACE version 5) so the
checks hold for imported types.

Updates docs/grammar.ebnf with the visibility-section grammar and adds
cp.test.visibility (parse + semantic enforcement) plus thread-test fixes that
switched two TThread subclasses from private FTerminated/FFinished fields to
the public Terminated/Finished properties.
2026-06-28 23:46:52 +01:00
Graeme Geldenhuys 0977dc16cb feat(lang): static class/record members (within-unit)
Introduce `static` (class-level) members to the Blaise language using the
`static` keyword — never an overloaded `class` keyword. A `static` member is
type-associated, not instance-associated: static methods take no implicit
Self, and static vars/consts are a single shared storage slot.

Surface, on classes and records:

* `static var` / `static const` — section form (`private static var`) or as a
  bare `static` continuing the current visibility. Static vars lower to one
  shared global slot (mangled `<Unit><Type>_<Name>`), zero-initialised, NOT an
  instance field. Class- and interface-typed static vars are supported (the
  canonical singleton storage) with store-time ARC and a program-exit release;
  string and dynamic-array static vars remain deferred.
* `static function` / `static procedure` — per-member prefix or section form;
  no implicit Self. Out-of-line bodies are `static function T.M`.
* `static property` — sugar over a static getter (no Self at the call site).
* record `static function` — the factory / namespaced-function form
  (`TPoint.Make(x, y): TPoint`), required to be marked `static` explicitly.

There is no `static constructor` / `static destructor` (rejected at parse):
the zero-initialisation guarantee covers nil singletons, and eager setup
belongs in a unit's `initialization`/`finalization` (the Swift/Rust/Go model,
not Java/C#/Delphi). `class` is never a member qualifier.

Implementation spans the full pipeline:

* parser — `static` is a soft keyword; section qualifier (followed by
  var/const) and per-member prefix forms; `static constructor/destructor`
  rejected.
* semantic — static vars register a shared global (bare + qualified) under a
  mangled emit label; static methods skip the Self binding; qualified
  `Type.StaticVar` / `Type.StaticProp` / `Type.StaticMethod()` resolution.
* QBE + native x86-64 codegen — no-Self method signatures and call sites
  (including the record-return sret and >6-arg paths), shared global data
  slots, qualified static var/property reads, and class/interface static-var
  release at program exit.
* `.bif` interface format — IsClassVar/ClassVarEmitName, property IsStatic, and
  record/class const decls are encoded (BLAISE-IFACE version 2 -> 3).
* OPDF debug info — static vars are emitted as `recGlobalVar`s under their
  mangled label so a debugger can print `TFoo.FInstance`.

Static members currently work within a single program/unit; carrying them
across separately-compiled units (export clone, import, TRoutineSig.IsStatic)
is a tracked follow-up — the .bif wire format is already in place for it.

Tests: cp.test.staticmembers (parser + semantic + IR) and
cp.test.e2e.staticmembers (compile+run on both backends). Full suite green on
QBE- and native-built runners (3908 tests); all fixpoints pass; bif-coverage
clean. docs/grammar.ebnf and docs/language-rationale.adoc updated.
2026-06-28 16:11:40 +01:00
Graeme Geldenhuys 50b8d75e6d fix(parser): enforce mandatory parentheses on statement-position calls (#148)
The "mandatory parentheses on zero-argument calls" rule (language-
rationale.adoc) was enforced in expression position but NOT in statement
position: a bare `Foo;`, `Obj.Method;`, or `Obj.Free;` used as a statement
compiled silently, building a paren-less TProcCall / TMethodCallStmt. Issue
#148 reported `tester.print;` (a unit's global object method call) being
accepted without its mandatory ().

The statement parser now raises the same "requires () for a call" diagnostic
the inherited-call and expression-position paths already use, at the two
fall-through sites:

  * bare unqualified call `Foo` with no '(' (the final ProcCall else-branch);
  * bare `Obj.Method` with no '(' and no further '.' chain.

Field reads, field assignments, indexed writes, and '.'-chains are unaffected
(only a terminating bare reference is rejected). Expression-position calls
were already enforced.

Enforcing the rule required the compiler, RTL, and stdlib to comply first
(self-hosting): swept bare calls in uSemantic (Flush/RepairGenericInstances),
runtime.arc/runtime.exc (_libc_abort), blaise.codegen.native.backend
(FAsm.AppendLine), and a json.writer doc example. The test suite embedded
many bare calls in inline program strings — all updated to carry (); adding
() never changes behaviour since these were always calls.

cp.test.parser.pas gains two parse-error tests (proc and method bare calls);
the old TestProcCall_NoParens, which asserted the bug, is inverted. grammar.ebnf
SubscriptMethodCall made parens mandatory and an example corrected; rationale
notes statement-position enforcement.
2026-06-28 10:15:39 +01:00
Graeme Geldenhuys d7460cb069 feat(parser): require parentheses on inherited calls; fix metaclass diagnostic
The mandatory-parentheses rule ("every function, procedure, method, and
constructor call requires parentheses, even with no arguments") made no
exception for 'inherited'.  A bare 'inherited Create' is a method call and
should be rejected like any other parenless call, but the parser silently
accepted it.

Enforce the rule in both 'inherited' parse paths (statement and expression
position): a bare 'inherited Method' now raises the same "requires () for a
call" diagnostic as any other parenless call.

Also fix a misleading semantic error for the metaclass-variable case.  A bare
'C.Create' on a 'class of T' variable parses as a field access, which reached
the "'C' is not a record or class" guard — confusing, since C is a valid
metaclass variable.  AnalyseFieldAccess now detects a constructor/method name
on a metaclass receiver and emits the "requires () for a call" diagnostic
instead.  (The parenthesised 'C.Create()' already worked: it parses as a
TMethodCallExpr, which has the metaclass-dispatch branch.)

Migrate every bare 'inherited Method;' call site in stdlib, the bif-coverage
tool, and embedded e2e test programs to the parenthesised form.  The
parenthesised form compiles on the prior compiler too, so the migration and the
enforcement land together without breaking the rolling-bootstrap chain.

Docs: update language-rationale.adoc (inherited section + mandatory-parens
section) and grammar.ebnf (expression-position inherited rule).

Tests: add TestSemantic_MetaclassVar_BareCreate_RequiresParens and
TestParse_Inherited_Bare{Stmt,Expr}_RequiresParens.
2026-06-25 13:36:27 +01:00
Graeme Geldenhuys 0396ed7b1c feat(lang): inline assembler blocks (asm … end routine bodies)
A routine body may now be written as inline assembly:

    function GetSelf: Pointer; assembler; nostackframe;
    asm
        movq %rdi, %rax
        ret
    end;

The block is opaque GNU/AT&T assembly: the lexer captures the whole asm … end
as one tkAsmBlock token (verbatim text, never tokenised as Pascal), the parser
wraps it in a TAsmStmt, the semantic pass treats it as a black box, and the
native backend emits it verbatim into the assembly stream where the existing
internal/external assembler parses it.  `nostackframe` suppresses the compiler
prologue/epilogue so the asm body owns the whole frame.  asm routines mix
freely with ordinary Pascal routines in a standard .pas unit (no .inc needed).

This is the FPC model (rtl/linux/x86_64/si_c.inc) and the path to retiring the
hand-written runtime/src/main/asm/*.s files (assembled by `cc -c` today) — once
each body moves into an asm routine the RTL builds with no external assembler.

Design follows ports-and-adapters: x86-64 knowledge stays at the backend/
assembler edge, the portable core never interprets the block.  The QBE backend
rejects asm bodies (it emits no assembly text); native is the inline-asm target
and the default.  TAsmStmt round-trips through the .bif unit cache.

Pipeline: lexer (tkAsmBlock + ReadAsmBody raw capture), uAST (TAsmStmt,
TMethodDecl.NoStackFrame), parser (nostackframe directive + asm-body path),
semantic (opaque no-op), native codegen (verbatim emit + nostackframe null-frame
guard), QBE rejection, uUnitInterfaceIO encode/decode, bif-coverage entry.
`asm` becomes a reserved word (one local var named Asm renamed in a test).

Fixes a native sret-Result field-read codegen bug the feature exposed: reading a
field of an sret function's Result at offset 0 (e.g. `Result.Kind` in a record-
returning function) read the Result frame slot DIRECTLY instead of dereferencing
the caller-buffer pointer it holds, so `Result.Field = const` was always false.
The offset-0 fast path in the integer field-read leaf now routes through
EmitLocalRecordBase like the offset>0 path, so the sret indirection happens in
both.  QBE was already correct; this was a native-only divergence.

Tests: lexer raw-capture (3), native verbatim-body/no-prologue IR test (1),
internal-assembler e2e returns-value + adds-two-args (2).  All four fixpoints
and both QBE- and native-built test runners pass (3764 tests).

Design note: docs/inline-asm-design.adoc; grammar + rationale updated.
2026-06-25 01:01:41 +01:00
Graeme Geldenhuys 8ece742cb0 docs: update for v0.12.0 — native default, expanded stdlib, syntax additions
README.adoc:
- Native x86-64 is the default backend since v0.12.0; QBE is opt-in
  (--backend qbe).  Reframed the intro, project status, bootstrap, and
  single-file compile examples accordingly (native one-step build leads; QBE
  shown as the opt-in multi-step path).
- Test count 3474 -> 3800+ (3744 compiler + 57 stdlib).
- Added a Standard library status line (generics collections, JSON, SHA-1,
  Base64, GUIDs, sockets, WebSockets, HTTP server, blaise.testing).
- Bootstrap example resolves the newest releases/v* binary instead of the
  stale v0.7.0 path, and uses the native --output build.
- OPDF now debugs incrementally-compiled multi-unit programs.

grammar.ebnf (CLAUDE.md sync rule):
- TypeName accepts a unit-qualified, possibly-dotted QualIdent
  (UnitName.TypeName, System.SysUtils.TFormatSettings).
- FieldAssignment l-value extended with a Selector chain
  (.Field / [Index] / .Method(...)) so Self.F[i].Sub := value parses.

language-rationale.adoc:
- New entries "Qualified type names" and "Chained l-value assignment"
  recording the decision, reasoning, and alternatives for the two
  conservative FPC/Delphi-compatible parser extensions made this cycle.
2026-06-23 23:32:08 +01:00
Graeme Geldenhuys d91d9ee65a feat(lexer): conditional compilation with predefined BLAISE (issue #131)
Implements real symbol-presence conditional compilation in the lexer.
Previously {$IFDEF} was hardcoded false (always took {$ELSE}) and
{$DEFINE}/{$UNDEF} were silently consumed, with no define table and no
command-line flag.

- A case-insensitive define table on TLexer.  {$DEFINE sym} / {$UNDEF sym}
  add and remove symbols; {$IFDEF sym} keeps its body when defined (and
  {$IFNDEF sym} when not), with an optional {$ELSE} and a closing {$ENDIF}.
  IFDEF/IFNDEF blocks nest (the existing depth-tracking skip helpers are
  reused, now driven by the real truth value).
- Predefined symbols, seeded in every lexer: BLAISE (the headline
  cross-compiler use case — {$IFDEF BLAISE} ... {$ELSE} ... {$ENDIF}) plus
  the target CPU/OS symbols CPUX86_64, CPUAMD64, LINUX, UNIX.  No version
  macro yet.
- A -d / --define <sym> command-line flag (FPC -dSYM / Delphi -D), carried
  on TFrontEndOpts and threaded to the program's lexer AND to every unit the
  TUnitLoader compiles, so {$IFDEF} resolves consistently across the program
  and its units.

Tests:
- cp.test.lexer.pas: predefined BLAISE keeps the body, undefined takes ELSE,
  IFNDEF, DEFINE-then-IFDEF, UNDEF-then-IFDEF, and AddDefine (the -d path).
- cp.test.e2e.misc.pas (dual-backend): the {$IFDEF BLAISE} cross-compiler
  pattern, and a combined DEFINE/UNDEF/IFNDEF/CPU-OS/nested program.

Docs: grammar.ebnf documents the directives and predefines; language-
rationale.adoc records the decision (symbol-presence only, no {$IF} expr
form yet; BLAISE + CPU/OS predefined, no version macro) and alternatives.

Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite
green on both the QBE-built and native-built runners (3715 tests).
2026-06-20 20:16:16 +01:00
Graeme Geldenhuys 8c68b09245 fix(parser): accept named integer subrange types (issue #130 bug1)
`type TByte = 0..255;` failed to parse — ParseTypeDecl had no case for an
integer-literal subrange, so the RHS fell through to the generic "expected
record/class/..." error.

Blaise does not range-check, so a named subrange is treated as an alias to
the narrowest STANDARD integer type that holds both bounds (0..255 -> Byte,
-10..10 -> SmallInt, etc.).  This keeps record/array element layout correct
(TByte is byte-sized) while the value behaves as an ordinary integer.  Two
parser helpers do the work: SubrangeAhead (lookahead: IntLit.. or -IntLit..)
and ParseIntegerSubrangeBaseType (parse lo..hi, pick the base type, reject a
descending range).  Note Blaise has no 8-bit signed alias, so a signed
subrange that would fit in ShortInt widens to SmallInt.

Only integer-literal bounds form a named type; identifier/enum-bounded
subranges (TLow..THigh, red..blue) are intentionally not handled here (they
are ambiguous as a named-type form).

Tests: parser tests (named subrange, negative bounds, descending-is-error)
and dual-backend e2e tests (named subrange runs; subrange as a record field
and array element with a negative range).  grammar.ebnf gains the
IntSubrange rule.

Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite
green on both the QBE-built and native-built runners (3691 tests).
2026-06-20 15:56:18 +01:00
Graeme Geldenhuys 07aac3322f feat(string): writable string subscript S[i] := ch with copy-on-write
S[I] := <byte> was rejected by the semantic pass ('is not a static array
or dynamic array') for any string — local, global, or var-param — on both
backends.  It is now supported as the symmetric counterpart of the S[I]
read: an in-place byte store into a 0-based UTF-8 string.

Because strings are reference-counted and literals are immortal (stored in
read-only memory; the native backend put them in .rodata, so a naive store
segfaulted, while QBE silently mutated the shared literal), the write
performs copy-on-write.  New RTL helper _StringUnique(S) returns S when it
is uniquely owned (rc=1) and otherwise allocates a fresh rc=1 copy, releases
the old reference, and returns the copy.  Both backends emit
_StringUnique -> write the result back to the slot -> storeb, so the slot
keeps exactly one owned reference and mutating one alias never disturbs
another (Delphi/FPC UniqueString semantics).

The RHS accepts a numeric ordinal, Chr(n), or a single-character literal
(the byte-shaped forms used in place of a Char type).  PChar subscript
writes keep their existing in-place path (raw pointer, no ARC header).

Dual-backend e2e tests (write, copy-on-write aliasing + literal reuse,
var-param) in cp.test.e2e.stringops.  Updates language-rationale.adoc
(writable subscript + COW) and grammar.ebnf (string as a subscript-assign
base).
2026-06-18 15:39:38 +01:00
Graeme Geldenhuys fa106ceeee feat(sets): support integer-subrange set base (set of 0..255) and Boolean operands
Accept an anonymous integer subrange as a set base type, e.g.
'set of 0..255' or 'set of 1..10', in both the inline (var/param/field)
and 'type T = set of L..H' declaration positions. The subrange lowers to
the same bitmap machinery as 'set of Byte': the bitmap is sized to H+1
bits and member ordinals are the integer values themselves. Bounds may be
integer literals, named constants, or constant expressions; the lower
bound must be >= 0, the upper <= 255, and the range ascending.

Also fix 'set of Boolean' so a Boolean operand is accepted directly on
either side of 'in' and as the second argument of Include/Exclude, without
an intervening Ord().

Parser handles both 'set of' paths (ParseTypeName and ParseSetDef);
semantic resolution adds ResolveSubrangeSetType for on-demand and
type-decl paths. Adds dual-backend e2e tests and semantic unit tests.
Updates grammar.ebnf and language-rationale.adoc per the language-decision
rule, and removes the now-implemented item from future-improvements.adoc.
2026-06-18 13:25:22 +01:00
Graeme Geldenhuys 8c7f5c9cbc feat(arrays): enum-indexed static arrays in var/type declarations (#114)
Static-array declarations in var and type sections now accept an enum type
as the index: array[TColor] of Integer.  The array is sized 0..N-1 where
N is the enum member count.  Subscript access uses enum ordinal values —
no codegen changes were needed.

Parser: detects array[Ident] (ident followed by ']' with no '..') and
encodes it as 'array[@TEnum] of T' in the type-name string.

Semantic: a new path in FindTypeOrInstantiate resolves the '@' marker,
looks up the enum type, reads Members.Count, and creates a standard
TStaticArrayTypeDesc with bounds 0..Count-1.

This completes the enum-indexed array story — const arrays already
supported this form; var/type declarations now match.

Tests: 4 unit tests + 2 E2E tests (both backends).
Grammar and language rationale updated.
2026-06-17 19:16:12 +01:00
Graeme Geldenhuys 2dcfe8e196 feat(arrays): accept named constants and expressions as static-array bounds (#109)
Static-array bounds now accept named integer constants and compile-time
integer expressions, not only integer literals.  All of these are valid:

  const N = 10;
  type TBuf = array[0..N-1] of Byte;
  var A: array[0..N] of Integer;
  const Days: array[0..N-1] of string = (...);

Parser: ReadConstBoundText collects tokens forming a bound expression
(integers, identifiers, arithmetic operators, parentheses) into a string
embedded in the type name.

Semantic: ResolveArrayBound resolves the bound text — plain integers via
StrToInt, named constants via symbol-table lookup, expressions via
mini-parse + EvalConstIntExpr.  The canonical type name always uses
resolved integer values for cache consistency.

Both the var/type declaration path (FindTypeOrInstantiate) and the
const-array path (BuildConstArrayType / ReadConstArrayDim) are updated.

Tests: 5 unit tests + 3 E2E tests (both backends).
Grammar and language rationale updated.
2026-06-17 19:07:26 +01:00
Graeme Geldenhuys 38c77c8a93 feat(const): fold floating-point constant expressions at compile time (#108)
Constant declarations now accept arithmetic expressions involving float
literals, named float constants, and the '/' operator.  The semantic pass
detects float-containing expressions and folds them to a single Double
value at compile time, storing the result as a string — the same format
used for bare float literals.

The '/' operator always yields a float result even with integer operands,
matching Delphi/FPC semantics (e.g. const X = 10 / 4 produces 2.5).

Parser: extended ConstRhsStartsIntExpr to detect float-led and
slash-containing expressions; added tkSlash to IsConstExprOp.

Semantic: added IsFloatConstExpr (detects float leaves or '/' usage) and
EvalConstFloatExpr (folds via libc strtod/snprintf to avoid a known
self-hosting ABI issue with the Blaise StrToDouble built-in).

Tests: 8 IR unit tests + 4 E2E tests (both backends).
Grammar and language rationale updated.
2026-06-17 18:48:04 +01:00
Graeme Geldenhuys f6d488bca4 feat(sets): support ordinal-based set types — set of Byte / Boolean (#105)
Widen set base types from enum-only to also accept Byte (256-bit jumbo)
and Boolean (2-bit small).  Set literals accept integer literals and
range syntax [lo..hi], expanded at compile time via EvalConstIntExpr.
Include/Exclude and the in operator accept numeric arguments for ordinal
base types.  Both QBE and native backends handle TIntLiteral elements in
set mask computation.

15 new IR unit tests and 4 new E2E tests (both backends via
AssertRunsOnAll).  All 3444 tests pass; FIXPOINT_OK,
NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK.
2026-06-17 18:21:56 +01:00
Graeme Geldenhuys 215082afc3 feat(codegen): implicit-virtual constructor dispatch via metaclass
Constructors are now auto-slotted into the vtable by the semantic pass.
When a constructor is called through a metaclass-typed variable
(C.Create(args)), the compiler emits _ClassCreate for allocation
followed by a vtable-indirect call to the most-derived constructor
body. Direct calls (TFoo.Create) remain fully static.

Both backends (QBE and native x86-64) emit correct dispatch for:
- MetaclassVar.Create(args) syntax
- ClassCreate(MetaclassVar, args) builtin
- Zero-arg and multi-arg constructor signatures

This is a layout-changing commit (adds constructor vtable slots).
Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK,
3370 tests pass.
2026-06-17 10:07:47 +01:00
Graeme Geldenhuys 9b45a6bbff feat(generics): out-of-line impl form for generic methods + arg checking
Completes generic methods: the out-of-line implementation form now parses
and links, alongside the inline-body form added previously.

    type TUtil = class
      function Pick<T>(cond: Boolean; a, b: T): T;   // declaration
    end;
    function TUtil.Pick<T>(cond: Boolean; a, b: T): T;  // out-of-line body
    begin if cond then Result := a else Result := b end;

- Parser: a method-level <T> appearing AFTER the qualified name
  (Owner.Method<T>) is now parsed into TypeParams. (The <T> before the dot
  remains the owner's type params, as for TList<T>.Add.)
- Semantic: LinkClassMethodImpls detects a generic-method impl
  (TypeParams <> nil) and transfers its body onto the in-class
  generic-method template, instead of trying to resolve its T-typed params.

Also fixes a real hole found via the out-of-line probe: generic-method
call arguments were not type-checked against the parameters (the path
bypasses ResolveMethodOverload), so e.g. passing a string for a Boolean
parameter compiled silently. The call site now validates argument count
and types against the monomorphised signature.

Verified out-of-line impl (string + Integer instantiations) on both
backends, and that a mismatched argument is now rejected. Adds
TE2EGenericsTests.TestRun_GenericMethod_OutOfLineImpl. Grammar and
language-rationale updated to drop the inline-only caveat.
2026-06-16 16:30:07 +01:00
Graeme Geldenhuys 3490a03aa7 feat(generics): generic methods (method-level type parameters)
A class or record method may now declare its own type parameters,
independent of the enclosing type, and is monomorphised per call site:

    type TUtil = class
      function Pick<T>(cond: Boolean; a, b: T): T;
        begin if cond then Result := a else Result := b end;
    end;
    ...
    u.Pick<Integer>(True, 7, 9);     // -> 7
    u.Pick<string>(False, 'a', 'b'); // -> 'b'

Each distinct set of explicit type arguments produces one concrete body
named <Owner>_<Method>_<Args> (e.g. TUtil_Pick_Integer); the implicit
Self is preserved, so a generic method can read the receiver's fields and
call its other methods. This is distinct from (and composes with) methods
of a generic CLASS.

Implementation mirrors the existing generic free-function machinery:
- Parser: a method-call folds an explicit <...> type-arg list into the
  method name (Pick<Integer>), using the same two-token '<' lookahead as
  generic free-function calls.
- Semantic: generic-method templates (TypeParams <> nil) are registered
  by Owner.Method and skipped from signature/vtable/body analysis;
  InstantiateGenericMethod clones the template, substitutes the type
  params, keeps OwnerTypeName + Self, analyses via AnalyseMethodDecl, and
  records a TGenericMethodInstance (deduplicated per owner+args). The
  call site resolves obj.M<T>(...) to the instance.
- Codegen (both backends): GenericMethodInstances are emitted as ordinary
  methods (Self param + mangled ResolvedQbeName).

Verified pick/echo, two distinct instantiations (Integer + string), use
of a Self field, and two type parameters on both backends. Adds IR tests
(TGenericFuncTests.TestCodegen_GenericMethod_{Body,Call}Emitted) and
dual-backend e2e tests (TE2EGenericsTests.TestRun_GenericMethod_*).
Grammar and language-rationale updated.

Limitation (logged in bugs.txt): only the inline-body declaration form is
supported; the out-of-line  function TOwner.Method<T>(...)  form is not
yet parsed.
2026-06-16 16:13:12 +01:00
Graeme Geldenhuys 798df8eb15 feat(properties): default array property — Obj[I] sugar
Adds the `default` directive on an indexed property, enabling subscript
sugar on the object itself:

    property Items[I: Integer]: T read Get write Put; default;
    ...
    V[0] := 10;        // lowers to V.Put(0, 10)
    WriteLn(V[0]);     // lowers to V.Get(0)

This is the mechanism behind the familiar List[i] syntax and was a real
foundational gap (the directive did not even parse — "Expected ':'").

Implementation:
- Parser: accept the trailing `default;` directive after a property
  declaration; set TPropertyDecl.IsDefault.
- AST / symbol table: IsDefault on TPropertyDecl and TPropertyInfo;
  TRecordTypeDesc.FindDefaultProperty walks the inheritance chain.
- Semantic: Obj[I] read (AnalyseStringSubscriptExpr) synthesises the
  default property's field access and reuses the indexed-property read
  path; Obj[I] := V write (AnalyseStaticSubscriptAssign) records the
  setter on the TStaticSubscriptAssign node.
- Codegen (both backends): the write path emits the setter call via the
  existing PropAccessorTarget / EmitPropAccessorCallNative helpers, so it
  honours virtual/override on the accessor; the read path delegates the
  TStringSubscriptExpr to its folded property-read field access.
- Cross-unit: IsDefault is serialised in the .bif interface so a default
  property declared in one unit keeps its subscript sugar elsewhere.

Verified read, write, string-element, inherited, and cross-unit cases on
both backends. Adds IR tests (TPropertyTests.TestCodegen_DefaultProperty_
{Read,Write}) and dual-backend e2e tests (TE2EPropertyTests.TestRun_
DefaultProperty_{ReadWrite,StringElement,Inherited}). Grammar and
language-rationale updated; the two new TStaticSubscriptAssign fields are
marked safe in bif-coverage.status.
2026-06-16 14:48:38 +01:00
Graeme Geldenhuys e2b715b53b fix(parser): support nested generic type arguments
TList<TList<Integer>> and TBox<TPair<Integer, string>> failed to parse:
each type argument was read as a single bare identifier, with no recursion
into a nested <...>, so a nested '<' produced "Expected '>' but got '<'"
(type position) or "Expected '.' or '(' after generic type arguments"
(constructor/expression position).

Both type-argument parse sites now recurse through ParseTypeName, so a type
argument may itself be a generic specialisation, to arbitrary depth
(TList<TList<TList<Integer>>> works). The expression-position heuristic also
accepts tkLessThan as the lookahead-2 token (the first arg being itself
generic). The comparison-operator disambiguation (a < b, (a < b) and (b < c))
is unaffected.

Found by the e2e generics hardening sweep. Regression:
TE2EMiscTests.TestRun_NestedGenericTypeArgs (TBox<TBox<Integer>>, both
backends). docs/grammar.ebnf TypeArgList rule updated to recurse via
GenericName.

All three fixpoints + full suite (3224 tests) pass.
2026-06-16 10:13:45 +01:00
Graeme Geldenhuys 236736a3be fix(parser): accept parenthesised lvalue as assignment target
A statement beginning with '(' was rejected ("Expected statement"), so a
parenthesised cast could not be an assignment TARGET:
    (a as TB).FX := 42;   -> Parse error
Reading the same expression worked, and the hard-cast target TB(a).FX := 42
worked, so only this statement-parser entry point was missing.

ParseStmt now handles a leading '(' by parsing the parenthesised expression
and requiring a '.Field := Expr' suffix, building a TFieldAssignment whose
receiver is that expression (ObjExpr) — the same AST + semantic + codegen path
already used for element-field writes (a[i].F := v). No AST/semantic/codegen
change was needed; the gap was purely the parser.

Found by the e2e test-hardening sweep of the inheritance cluster. Regression:
TE2EMiscTests.TestRun_ParenCastAsAssignmentTarget, run on BOTH backends.
docs/grammar.ebnf FieldAssignment rule updated.

All three fixpoints + full suite (3223 tests) pass.
2026-06-16 10:06:40 +01:00
Graeme Geldenhuys 74f71023da feat(oop): support 'inherited Method()' in expression position
`inherited` previously parsed only as a statement, so calling an inherited
FUNCTION and using its result failed to parse:
    Result := inherited Value() + 100;   -> "Parse error: Expected expression"
This blocked the normal OOP pattern of an overriding function extending its
parent's result.

Adds the expression form alongside the existing statement form:
* uAST: new TInheritedCallExpr (sibling of TInheritedCallStmt) + CloneExpr case.
* uParser: tkInherited handled in ParseFactor (primary expression).
* uSemantic: AnalyseInheritedCallExpr resolves to the parent method (must be a
  non-void function) and sets ResolvedType to the return type.
* Codegen: static (non-virtual) call to the parent slot, result returned as a
  value. QBE EmitInheritedCallExpr; native EmitInheritedCallSeq (shared by the
  statement and expression forms) leaves the result in %rax/%xmm0.
* uUnitInterfaceIO: 'inhc' encode/decode so inherited-expr in an inline method
  body round-trips through the .bif unit-interface cache; bif-coverage.status
  marks the two fields serialise (bif-coverage OK).
* docs/grammar.ebnf + docs/language-rationale.adoc updated (same commit).

Found by the e2e test-hardening sweep of the inheritance feature cluster.
Regression: TE2EMiscTests.TestRun_InheritedFunctionCall_InExpression covers a
value-returning inherited call and an inherited call with an argument, run on
BOTH backends via AssertRunsOnAll.

All three fixpoints (FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK) and
the full suite (3222 tests) pass.
2026-06-16 10:01:26 +01:00
Graeme Geldenhuys 9362114ee1 feat(sets): range syntax in set literals — [lo..hi] (#105)
A set literal element may now be a constant range:

    e := [Red..Blue];        { {Red, Green, Blue} }
    e := [m0, m2..m4, m7];   { ranges mix with single members }

The parser builds a transient TSetRangeExpr for each lo..hi element; the
semantic pass (AnalyseSetLiteralExpr) expands it into the individual member
idents before any other consumer runs, so overload resolution, the bitmask
folder, the jumbo-set path, and both code generators see an ordinary member
list and need no range-specific logic.

Both bounds must be compile-time constants of the set's base type. A
reversed constant range [Blue..Red] is a compile-time error rather than a
silently empty set — matching Blaise's preference for rejecting
confusing-but-legal constructs over FPC's empty-set-with-warning behaviour.
Variable bounds [lo..hi] are rejected ("set range bound must be a
constant"); runtime-variable ranges are deferred.

TSetRangeExpr is wired into the .bif serialiser and the AST cloner so a
generic template body containing an unexpanded range round-trips through
separate compilation (verified by hand: a generic returning [m1..m3]
compiled to .o, then instantiated from the .bif with source hidden).

Set base types remain enumerations; the issue's set-of-byte example needs
ordinal-base set types, tracked separately in docs/future-improvements.adoc.
Range expansion is base-type-agnostic, so ranges will work for those
automatically once they land.

Docs: grammar.ebnf SetElement rule; language-rationale.adoc decision +
alternatives. bif-coverage.status regenerated for the new node.

FIXPOINT_OK + NATIVE_FIXPOINT_OK; full suite OK (3201 tests, 11 new).
2026-06-15 17:14:05 +01:00
Graeme Geldenhuys fa853e0eb2 feat(sets): jumbo sets — set of enum up to 256 members
Extend set of <enum> from a 64-member cap to 256, Java-EnumSet style. Sets
of 64 members or fewer keep the existing single-register bitmask (QBE w/l);
sets of 65..256 members ('jumbo') become an inline byte-array bitmap of
ceil(N/8) bytes, treated as a value aggregate (passed by reference, returned
via sret, memset/memcpy), with operations performed by new RTL helpers.

Representation (uSymbolTable): TSetTypeDesc.IsJumbo (BitCount > 64) and
RawByteSize; RawSize/ByteSize/AllocAlign sized accordingly. TSymbol gains
ConstSetBytes/ConstSetQbe for jumbo constants (can't fit an Int64 mask).

RTL: new runtime/src/main/pascal/blaise_set.pas — _SetIn/_SetInclude/
_SetExclude/_SetUnion/_SetInter/_SetDiff/_SetEqual/_SetCopy over byte-array
bitmaps (overlap-safe). Wired into runtime/Makefile.

Semantic: the four >64 caps become >256. AnalyseSetConstDecl folds a jumbo
const to a byte array. The anonymous set for 'X in [a,b,c]' is sized to the
largest listed ordinal (when constant), not the full enum — keeping the
common low-ordinal membership test (incl. the compiler's own TokenKind
tests) on the fast register path. This also fixes a latent miscompile: the
old fixed-l representation silently dropped any listed ordinal >= 64.

Codegen (both backends): jumbo branches for literal, in, +/-/*, =/<>,
Include/Exclude, for-in, assignment, params (pmJumboSetValue ABI), and sret
returns. The register 'in' gained a range guard for literal-sized sets.
Native reserves two 32-byte scratch slots per frame (and .bss in main) for
set-op/literal result buffers. QBE adds IsAggregateAddrType so jumbo sets
ride the record/static-array address paths and are never promoted.

OPDF: no format change — recSet SizeInBytes (1 byte) already covers <=32.

Verified: FIXPOINT_OK and NATIVE_FIXPOINT_OK; full suite (3172 tests) green
built by the stage-2 binary, the QBE fixpoint binary, and the native
fixpoint binary; bif-coverage OK. Tests: cp.test.jumboset (12 IR) and
cp.test.e2e.jumboset (6 e2e, both backends via AssertRunsOnAll); the
>256-member rejection test in cp.test.sets updated. Docs: grammar.ebnf and
language-rationale set-type sections updated for the 256 cap and literal
sizing.

Closes the design discussion behind #81.
2026-06-14 23:23:19 +01:00
Graeme Geldenhuys 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 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 f8d75e586d feat: interface properties
Interfaces may declare properties (FPC/Delphi parity):

  IValued = interface
    function GetValue(): Integer;
    procedure SetValue(AValue: Integer);
    property Value: Integer read GetValue write SetValue;
  end;

Accessors must be methods of the interface or an inherited parent
(interfaces have no fields), validated at registration.  I.Value reads
lower to the existing zero-arg getter itab dispatch; I.Value := X
lowers to the setter dispatch with X as the single argument — pure
compile-time sugar, no itab slots, no layout change.  Child interfaces
see inherited properties via the parent chain.  Wired end to end:
parser, TInterfaceTypeDef.Properties (AST + clone), TInterfaceTypeDesc
property registry, semantic read/write resolution, both backends, .bif
serialisation and import registration.  v1 limits (recorded in
language-rationale): plain interface-typed receivers; no indexed/
default array properties.

Two pre-existing linking bugs surfaced by the dogfood program and are
fixed alongside:

- Property accessor names written in a different case than the method
  declaration (read getValue for GetValue) produced unresolved symbols —
  accessor names are now normalised to the declared casing at
  registration (classes and interfaces, compile and import paths).

- A program-level class implementing an interface failed to link
  whenever the program had a uses clause: program-scope methods carry
  bare symbol names (uSemantic.CurrentUnitPrefix) but itabs and
  property-setter call sites prefixed them with the program name via
  Sym.OwningUnit.  ClassUnitPrefix (QBE) / ClassSymName (native) now
  skip the prefix for program-owned classes (new FProgramName field).

Tests: 7 in cp.test.interfaces (parse, registration, accessor
validation, read-only enforcement, inheritance, IR dispatch), bif
round-trip in cp.test.unitinterface, 2 e2e suites on both backends in
cp.test.e2e.classes2 (interface read/write incl. compound assignment
and inherited dispatch; case-mismatch + uses regression).  Suite: 2940
OK on working and fixpoint binaries; FIXPOINT_OK; NATIVE_FIXPOINT_OK.
2026-06-11 11:04:47 +01:00
Graeme Geldenhuys 5f0fbb7643 fix: element access through array-typed fields — r.A[i], c.N.A[i], SetLength(r.A)
Array-typed FIELDS were second-class citizens for element access; this
lands the full variation family on both backends:

- Semantic: r.A[i] := v dropped the subscript from the LHS type — the
  parser stores it in TFieldAssignment.PropIndexExpr (the indexed-
  property slot) and semantic ignored it for real fields, demanding the
  whole array type on the RHS.  A subscript on a real dyn/static-array
  field is now an ELEMENT write (new semantic-set IsElemWrite flag);
  both backends emit the element store with the standard ARC and
  record-copy rules.

- Parser: c.N.A[i] := v failed with "Expected 'end' but got '['" —
  the chained L-value walker now accepts a terminating Field[idx] :=,
  and the subscript-chain path accepts arr[i].A[j] := v (subscript
  directly over another subscript stays a clear parse error).

- Bare implicit-Self: A[i] := v inside a method raised "Undeclared
  variable 'A'" — TStaticSubscriptAssign now resolves array-typed
  fields of Self (IsImplicitSelf + ImplicitFieldInfo).

- SetLength(r.A, n): QBE refused ("first argument must be a
  variable"); native silently emitted NO code for field receivers and
  mis-stored through var-param receivers.  QBE routes through
  EmitLValueAddr; native gains EmitLValueSlotAddr covering field,
  var-param and implicit-Self receivers for dyn-array and string
  SetLength.

- Read side: c.A[i] through a class variable computed the element base
  as if c were an inline record (missing object-pointer load) and
  segfaulted; implicit-Self bases had the same gap; native missed
  chained reads (c.N.A[i]) entirely.  All base shapes are handled in
  the IsArrayAccess read paths of both backends now.

bif-coverage.status regenerated for the new semantic-set AST fields
(safe).  Tests: 4 IR tests (cp.test.dynarray) + 6 e2e tests on both
backends (cp.test.e2e.records) covering record/class/implicit-Self
receivers, nested chains, static-array fields and string-element ARC.
Suite: 2922 OK on working and fixpoint binaries; FIXPOINT_OK (single
round); NATIVE_FIXPOINT_OK.
2026-06-11 09:19:35 +01:00
Graeme Geldenhuys 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 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 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 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
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 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 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 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