= Blaise Language Design Rationale :toc: left :toclevels: 3 :sectnums: This document records design decisions made for the Blaise Pascal dialect, together with the alternatives considered and the reasons each choice was made. It is not a language reference or user guide. Its purpose is to preserve the reasoning behind decisions so that: * future contributors can understand constraints without reconstructing them from git history; * the eventual full language guide can be written from a solid foundation rather than from code archaeology. Entries are added as decisions are made. Each entry records the decision, the alternatives that were rejected, and the rationale. --- == Type System === String ==== Decision Blaise has exactly one string type: a reference-counted, UTF-8 encoded byte sequence, compatible with FPC's `AnsiString` memory layout (12-byte header: refcount / length / capacity, followed by character data and a null terminator). ==== Alternatives rejected * *Multiple string types* (ShortString, AnsiString, WideString, UnicodeString as in FPC/Delphi) — rejected. The primary source of confusion in FPC codebases is implicit conversion between string kinds and the mismatch between storage width and semantic meaning. A single type eliminates the entire class of bugs and portability issues that arise from that multiplicity. * *UTF-16 internal encoding* (as used by Java, C#, Delphi's UnicodeString, Swift prior to 5) — rejected. UTF-8 is the dominant encoding for files, network protocols, and operating system interfaces on all target platforms. Storing UTF-16 internally would require conversion at every system boundary. UTF-8 also preserves ASCII compatibility, which matters for the common case of identifier-like strings in compiler and tool code. * *Immutable value-type strings* (as in Go, Rust `str`) — deferred. ARC reference counting gives near-value semantics for most practical purposes without the allocation cost of copy-on-write. Revisit if concurrency becomes a first-class concern. ==== Open questions * Code page annotation on the string header exists in the current layout (inherited from FPC) but is not yet used. Blaise strings are always UTF-8; whether the code page field should be removed or remain as a compatibility shim is undecided. --- === 0-Based String Indexing ==== Decision String indexing in Blaise is 0-based. `S[0]` is the first character. The built-in functions `Pos`, `Copy`, `Delete`, `PosEx`, and `OrdAt` all use 0-based positions. `Pos` returns `-1` when the substring is not found. ==== Rationale 1-based string indexing originates from Niklaus Wirth's original Pascal (1970), where `String` was a `packed array [1..N] of Char`. The `[1]` lower bound was simply the array's natural lower bound — there was nothing semantically meaningful about starting at 1. Borland's introduction of `ShortString` (with a length byte at index 0 and characters at indices 1..255) then locked the convention in for backward-compatibility reasons. Every subsequent implementation — Turbo Pascal, Delphi, FPC — carried it forward unchanged. Blaise has no `ShortString`, no legacy length-byte layout, and no existing code base to be backward-compatible with. The data-pointer convention used by Blaise strings (`str_ptr + 0` is the first character byte) maps directly to 0-based indexing. Supporting 1-based access would require a `-1` adjustment in `EmitStringSubscriptExpr` and every RTL string function for no benefit other than mimicking an accidental 1970s convention. The rest of the language is already consistently 0-based: dynamic arrays start at 0, open-array parameters are 0-based, and pointer arithmetic is byte-addressed from a base pointer. Adopting 0-based strings removes the sole exception and eliminates an entire category of off-by-one errors that arise when mixing string indices with array indices. The `-1` not-found sentinel for `Pos` aligns with the convention used by most modern languages (Python `str.find`, Go `strings.Index`, Rust `str.find`, Java `String.indexOf`). Explicitly ranged arrays (`array[5..10] of T`) are unaffected: a user who declares a lower bound of 5 expects `A[5]` to be the first element. That indexing is explicit and intentional, not an inherited convention. ==== Alternatives rejected * *Keep 1-based indexing for compatibility with FPC/Delphi* — rejected. There is no existing Blaise code base to protect. Carrying 1-based indexing forward would permanently embed a known design mistake into a language that is explicitly trying to clean up Pascal's baggage. * *Offset transparently in the compiler, expose 1-based to the user* — rejected. This would make pointer arithmetic on strings unpredictable and would require every string function to document an exception to the otherwise-universal 0-based convention. ==== Bootstrap and migration notes The Blaise compiler source is compiled by FPC during development (as the stage-1 bootstrap compiler) and must therefore work under both FPC (1-based) and Blaise (0-based) semantics simultaneously. The shim unit `uStrCompat.pas` provides thin wrapper functions (`StrAt`, `StrHead`, `StrCopyFrom`, `StrCopyTail`, `StrPos`) that translate between the two conventions under FPC and pass through directly under Blaise. Once a Blaise release exists that is self-hosting on a 0-based string runtime, the shim can be dropped and the calls replaced with plain built-ins. The migration analyser (`tools/migration-analyser`) must flag code ported from FPC/Delphi that uses 1-based string idioms. See `docs/future-improvements.adoc` for the enumerated checklist. --- === No Char Type ==== Decision Blaise does not have a `Char` type. String subscript (`S[N]`) returns `Byte`. Single-quoted ASCII literals coerce to their `Ord` value (a `Byte`) when used in a context that expects a numeric type. ==== Alternatives rejected * *`Char` as a one-byte alias for `Byte`* — rejected. The name `Char` implies a Unicode character (a codepoint), but a one-byte value can only represent code points U+0000..U+00FF. Naming it `Char` reproduces the original sin of FPC's `AnsiChar`: code that operates on `Char` values silently breaks on any input outside the ASCII range. `Byte` is honest about what is actually being held. * *`Char` as a 16-bit UTF-16 code unit* (as in Java, C#, Delphi `WideChar`) — rejected for the same reason, one level higher. Java's experience demonstrates that a 16-bit `char` that cannot represent supplementary characters (U+10000 and above) creates persistent confusion: `String.charAt(n)` returns a UTF-16 code unit, not a codepoint, and most Java code silently ignores the distinction. * *`Char` as a full Unicode codepoint (21-bit, stored as 32-bit integer)* — rejected for now. A codepoint subscript on a UTF-8 string requires an O(N) walk to find the Nth codepoint, which is incompatible with the mental model that subscript is O(1). If codepoint-level access is needed, use `CodePointAt(S, N)` from `StrUtils` or iterate with `for CP: Integer in S do`. ==== Consequences for porting Code ported from FPC that declares `var C: Char` and assigns `C := S[1]` will require mechanical substitution of `Char` with `Byte`. This is intentional friction: it forces the porting author to reason about whether byte access is actually what is intended. ==== `Ord()` is not needed for strings In FPC and Delphi, `for C: Char in S do` yields a `Char` value, so developers must call `Ord(C)` to obtain the numeric byte value. In Blaise, `for B in S do` already yields a `Byte` (or any compatible ordinal), and `S[N]` already returns `Byte` — the numeric value is available directly. There is nothing to convert. `Ord()` remains meaningful in Blaise for enumerations (obtaining the underlying integer of an enumeration value), but it has no role in string processing. ==== Related See <<_string_subscript_s_n>> and <<_char_literal_coercion>>. --- === String Subscript S[N] ==== Decision `S[N]` returns the Nth byte of the string (0-based), as `Byte` (stored as `Integer` in the current implementation). Indexing is into raw UTF-8 bytes, not into Unicode codepoints. ==== Rationale UTF-8 encodes ASCII characters (U+0000..U+007F) as single bytes that never appear as part of a multi-byte sequence. The overwhelming majority of string subscript operations in systems and compiler code check for or compare against ASCII characters (delimiters, flags, punctuation). Byte indexing is correct for all such uses. Byte indexing also matches Go's `string[N]` semantics, which has proven workable in practice. ==== Writable subscript — `S[N] := ` `S[N]` is also assignable: it overwrites the Nth byte of the string in place. The right-hand side is a single byte ordinal — a numeric value, a `Chr(n)` call, or a one-character string literal (Blaise has no `Char` type, so these are the byte-shaped forms used in its place), symmetric with what the read side yields. Because Blaise strings are reference-counted and string literals are immortal (RefCount = −1, stored in read-only memory), an in-place byte write must not touch a buffer that is shared or constant. `S[N] := b` therefore performs *copy-on-write*: the RTL helper `_StringUnique(S)` returns the string unchanged when it is uniquely owned (RefCount = 1), and otherwise allocates a fresh RefCount = 1 copy, releases the old reference, and returns the copy. The compiler stores the (possibly new) pointer back into the variable slot before the `storeb`, so the slot keeps exactly one owned reference. This matches Delphi/FPC `UniqueString` semantics: mutating one alias never disturbs another, and a literal reused elsewhere stays pristine. Both backends emit the same `_StringUnique` → write-back → `storeb` sequence. ==== What is not supported Codepoint-level indexing is not supported via subscript syntax. Use `CodePointAt(S, N)` from the `StrUtils` unit for O(N) codepoint-indexed access, or `for CP: Integer in S do` for sequential codepoint iteration. Assigning a multi-byte value through `S[N] :=` is not meaningful: the subscript addresses a single byte, so only a byte-sized right-hand side is accepted. ==== Implementation note — string memory layout and pointer convention Blaise uses the *data-pointer convention*: the 8-byte slot in a variable or field holds a pointer directly to the first character byte. The 12-byte ARC header (refcount, length, capacity) lives immediately before that pointer at negative offsets: ---- data_ptr − 12 RefCount (Integer, 4 bytes; −1 = immortal literal) data_ptr − 8 Length (Integer, 4 bytes; byte count) data_ptr − 4 Capacity (Integer, 4 bytes) data_ptr + 0 char data (Length UTF-8 bytes, null-terminated) ---- `S[N]` therefore emits: `byte_ptr = data_ptr + (N − 1)`, followed by a `loadub` instruction. This is simpler than the header-pointer form because there is no 12-byte skip — the pointer already is the character data. `PChar(s)` is an identity operation: the data pointer is already a valid C `char*` with no arithmetic needed. This layout matches how class instance pointers work (the user pointer points to the first user field; the ARC header is at negative offsets before it), making the two reference-counted types fully consistent. ==== Why data-pointer rather than header-pointer The original implementation used a *header-pointer* convention (the variable slot held a pointer to the start of the ARC header block, with character data 12 bytes later). That was changed for several reasons: * *Consistency with class objects* — class instances already use the data/user-pointer convention (`user_ptr` points to the vtable; the ARC header lives at `user_ptr − 16`). Strings following the same idiom means there is one rule for all reference-counted types, not two. * *Simpler subscript arithmetic* — `S[N]` with header-pointer required `ptr + 12 + (N − 1)`. With data-pointer it is `ptr + (N − 1)`, matching C array indexing. * *Zero-cost `PChar` cast* — the `PChar(str)` cast is an identity operation; no `+ 12` is needed. This matters for cross-platform C interop even on non-Win32 targets: POSIX `write(2)`, `fopen`, and similar system calls all take `char*`. ==== Why not the FPC convention FPC also uses a data-pointer convention, but the signed offsets differ: FPC's header on x86-64 places length at `data_ptr − 8` (SizeInt, 8 bytes), refcount at `data_ptr − 12`, codepage at `data_ptr − 16`, and element-size at `data_ptr − 14`. Blaise's header is smaller (12 bytes, no codepage/elemsize) and length is always a 32-bit `Integer` because Blaise has a single string type with no codepage. FPC's choice of data-pointer was driven by Delphi compatibility and Win32 API usage where `PChar(s)` had to be a zero-cost cast — exactly the same motivation applies to Blaise's cross-platform C interop requirement. The header layout differs, but the pointer-to-content invariant is shared. ==== Implication for the Migration Analyser (Phase 8) FPC code that manually accesses the `AnsiString` header via pointer arithmetic (e.g. `PInteger(PChar(s) - 12)^` to read the refcount) uses negative offsets which are correct in Blaise too. Code that used the old Blaise header-pointer form (positive offsets from a header pointer) is a porting error that the Migration Analyser should flag. --- === `Low` and `High` on Strings ==== Decision `Low(S)` always returns the constant `0`. `High(S)` returns `Length(S) - 1`, i.e. the index of the last valid byte. Both return `Integer`. For an empty string, `High(S)` returns `-1`; a standard `for I := Low(S) to High(S)` loop executes zero iterations, which is the correct behaviour. ==== Rationale Blaise strings are 0-based. `Low` and `High` exist on arrays to make `for I := Low(A) to High(A)` idiomatic regardless of whether the array is zero-based or non-zero-based. Extending the same intrinsics to strings means that developers do not need to remember a different idiom when iterating a string by index: [source,pascal] ---- for I := Low(S) to High(S) do Process(S[I]); ---- `Low(S)` is a compile-time constant `0` — no runtime overhead. `High(S)` loads the length field from the ARC header (`data_ptr - 8`) and subtracts one, identical to `Length(S) - 1`. ==== Preferred idiom For simple byte iteration, prefer `for B in S do` (see <<_for_in_loop_class_based_enumerators>>), which avoids index arithmetic entirely and reads the intent clearly. `Low`/`High` are intended for cases where the index itself is needed (e.g. comparisons, slice computations, or passing the position to another function). ==== Alternatives rejected * *`Low(S)` returning `1`* — rejected. Blaise is 0-based; returning `1` would be a lie that silently reintroduces the 1-based confusion the 0-based design was adopted to eliminate. * *Not supporting `Low`/`High` on strings at all* — rejected. The intrinsics already exist for arrays; omitting them for strings would create an arbitrary inconsistency that forces developers to write `0` and `Length(S) - 1` as magic constants. --- === `for B in S` — String Iteration (Bytes and Codepoints) ==== Decision The loop variable's declared type selects the iteration mode: * `Byte` — iterates raw UTF-8 bytes (one byte per iteration) * `Integer` — iterates Unicode codepoints (one codepoint per iteration, advancing 1–4 bytes as needed) * Any other type — compile-time error There is no `Char` type in Blaise — both modes expose numeric values. ==== Rationale Since Blaise has no `Char` type (see <<_no_char_type>>), the natural element type for string iteration is `Byte` (raw bytes) or `Integer` (codepoints). Byte iteration is consistent with `S[N]` returning `Byte`. Codepoint iteration provides the API users expect for Unicode text processing without requiring a separate function or iterator wrapper. The loop variable type acts as a discriminator: `Byte` signals intent to work with raw encoding units (systems code, protocol parsing), while `Integer` signals intent to work with Unicode characters (text processing). ==== Byte iteration The compiler desugars `for B: Byte in S do Body` as: [source,pascal] ---- __idx := 0; while __idx < Length(S) do begin B := S[__idx]; Body; Inc(__idx); end; ---- The synthetic `__idx` variable is injected into the enclosing function's local variable scope. No heap allocation is required; iteration is a simple index-and-load loop. ==== Codepoint iteration The compiler desugars `for CP: Integer in S do Body` as: [source,pascal] ---- __idx := 0; while __idx < Length(S) do begin packed := _Utf8DecodeAt(PChar(S), __idx); CP := packed and $FFFFFFFF; { low 32 bits = codepoint value } __adv := packed shr 32; { high 32 bits = byte advance } Body; __idx := __idx + __adv; end; ---- `_Utf8DecodeAt` is a runtime helper that decodes one UTF-8 codepoint at a given byte offset. It returns both the codepoint value and the byte count packed into a single `Int64` (byte count in upper 32 bits, codepoint in lower 32 bits). This avoids a double call and keeps the loop to one function-call per iteration. Both synthetic variables (`__idx` and `__adv`) are injected into the local scope. ==== Alternatives rejected * *Loop variable typed as `Char`* — rejected. Blaise has no `Char` type. See <<_no_char_type>> for the full rationale. * *Separate `Runes(S)` iterator function* — rejected. The type-based dispatch is simpler to use and does not require importing a separate unit or learning a new API. * *Allowing `Word`, `SmallInt`, or other ordinals for codepoint iteration* — rejected. Codepoints range from 0 to U+10FFFF (1,114,111), which does not fit in 16-bit types. Restricting to `Integer` avoids silent truncation bugs. --- === Char Literal Coercion ==== Decision A single-quoted string literal that contains exactly one byte (i.e. a single ASCII character, U+0000..U+007F) is implicitly coerced to its `Ord` value when used in a comparison with a `Byte` (or `Integer`) value. A single-quoted literal that encodes to more than one byte (e.g. an emoji or any non-ASCII codepoint) in that context is a compile-time error. ==== Rationale This allows ported FPC code of the form `if S[1] = '-' then` to compile unchanged, without introducing a `Char` type. The coercion is always safe because: . The literal value is known at compile time. . Whether the literal fits in one byte is checkable at compile time. . An emoji or non-ASCII character in this position is almost certainly a mistake; making it an error is the safest default. ==== Example error [source,pascal] ---- if S[1] = '😀' then { Error: literal '😀' is 4 bytes; cannot coerce to Byte } ---- --- === Numeric Char Literals (#N) ==== Decision `#N` (where N is a decimal integer in 0..255) produces a `Byte` value equal to N. There is no separate `Char` type for these literals. ==== Implementation note Standalone `#N` literals work without any additional machinery. The lexer's `UnescapeString` already converts `#N` to the actual byte value (as it does for `#N` embedded inside string literals). When a `#N` literal appears in a comparison with a string subscript result, the existing `CoerceToCharOrd` path validates the byte length and emits `copy N` in the QBE IR. --- === PChar ==== Decision `PChar` is a built-in opaque pointer type that maps to a 64-bit pointer (`l` in QBE IR). It is not a string type. It exists solely for C interoperability. * `PChar(str)` — built-in cast; returns the pointer to the string's character data (the data pointer, not the header pointer). Because the Blaise RTL guarantees null termination, the result is a valid C string. * `string(pchar)` — built-in conversion; constructs an ARC string from a null-terminated byte sequence by measuring `strlen` and copying. ==== Implementation `PChar` is a distinct type kind (`tyPChar`) registered as a built-in type. It maps to `l` (64-bit pointer) in QBE IR. Variable declarations (`var p: PChar`) allocate an 8-byte nil-initialised pointer slot. `PChar(str)` emits `add str_ptr, 12` — skipping the 12-byte ARC header (4 bytes refcount + 4 bytes length + 4 bytes capacity) to reach the character data. Because the RTL null-terminates all strings, the result is a valid C `char*`. `string(pchar)` calls `_StringFromPChar(p: char*): void*` in `blaise_str.c`. That function measures `strlen`, allocates a new ARC string via `str_alloc`, copies the bytes, and returns the header pointer with `RefCount = 0`. The compiler's ARC machinery calls `_StringAddRef` at the assignment site, bringing the refcount to 1. `PChar(pchar_expr)` is an identity cast: the value is returned unchanged. This allows code that obtains a `PChar` from one source to pass it onwards without an explicit type annotation. ==== Alternatives rejected * *No `PChar` at all* — rejected. The compiler itself calls external processes (QBE, cc) and reads/writes files, all of which ultimately require null-terminated byte pointers at the C boundary. Eliminating `PChar` would require an alternative C-interop mechanism. * *`PChar` as an alias for `^Byte`* — rejected for now. Functionally equivalent but requires the typed-pointer dereferencing machinery, which is not yet in place. `tyPChar` is a simpler, opaque type that covers the immediate C-interop use cases without pointer arithmetic. --- === Integer Types ==== Decision Blaise defines five integer types with fixed, platform-independent widths: [cols="1,1,2,2", options="header"] |=== | Type | Width | QBE IR type | Aliases | `Integer` | 32-bit signed | `w` | | `Int64` | 64-bit signed | `l` | | `UInt32` | 32-bit unsigned | `w` | `Cardinal` | `UInt64` | 64-bit unsigned | `l` | `QWord`, `PtrUInt` | `SmallInt`| 16-bit signed | `w` (2 bytes in storage) | `Int16` | `Word` | 16-bit unsigned | `w` (2 bytes in storage) | `UInt16` | `Byte` | 8-bit unsigned | `w` (1 byte in storage) | |=== `Integer` is 32-bit on all platforms. This matches FPC's behaviour and makes arithmetic portable across Blaise's supported targets (linux-x86_64, macos-arm64) without requiring a platform-sized integer type. `Int64` is 64-bit signed on all platforms. It is used wherever values may exceed the 32-bit range. `UInt32` and `UInt64` are the unsigned counterparts of `Integer` and `Int64`. `Cardinal` is provided as an alias for `UInt32` and `QWord` as an alias for `UInt64` so code ported from either FPC or Delphi compiles unchanged. `PtrUInt` is the pointer-sized unsigned integer; on 64-bit targets it is identical to `UInt64`. `Byte` is 8-bit unsigned. In registers and temporaries it is widened to `w` (32-bit QBE word). Field and array element storage uses 1 byte per `Byte` element, consistent with FPC. ==== Int64 ↔ UInt64 conversion is explicit `Int64` and `UInt64` are not implicitly compatible — mixing them in an arithmetic or comparison expression is a type error. An explicit cast (`UInt64(x)` or `Int64(x)`) is required, and the cast reinterprets the bit pattern rather than performing a numeric conversion. This matches FPC's strict-mode behaviour and avoids the silent footgun where a negative `Int64` becomes a huge `UInt64` (or vice versa) without warning. All narrower integer types (`Integer`, `UInt32`, `Byte`) widen implicitly into both `Int64` and `UInt64`. ==== 16-bit integer types: SmallInt / Word `SmallInt` (16-bit signed, range −32768..32767) and `Word` (16-bit unsigned, range 0..65535) are provided alongside the established 32-/64-bit integer types so that code ported from FPC or Delphi compiles unchanged and so that `record` field layouts can match fixed-width binary formats without packing tricks. In registers and temporaries both types are widened to `w` (32-bit QBE word); only field and array element storage uses 2 bytes per element (`storeh` / `loadsh` for `SmallInt`, `loaduh` for `Word`). Implicit widening into `Integer`, `Int64`, `UInt32` and `UInt64` is permitted in the same way `Byte` widens, since 16-bit values fit losslessly in all of those. `Int16` and `UInt16` are accepted as aliases for `SmallInt` and `Word` respectively. ==== Integer literal typing Untyped integer literals are typed by magnitude: * `[MinInt32..MaxInt32]` → `Integer` * outside Int32 range but in `[MinInt64..MaxInt64]` → `Int64` * in `(MaxInt64..MaxUInt64]` → `UInt64` The bit pattern is preserved in every case; the literal type only selects which signed/unsigned interpretation the surrounding expression sees. ==== Radix prefixes Integer literals may be written in four radixes, following FPC (not Delphi): * decimal — `42` * hexadecimal — `$2A` (also Delphi) * binary — `%101010` (Delphi 11+ also accepts this) * octal — `&52` (FPC only; Delphi has never supported octal — there `&` is the identifier-escape prefix) Underscore digit separators are permitted in any radix (`$FF_FF`, `%1010_1010`). All four forms are accepted everywhere an integer literal is — scalar and typed constants, expressions, static-array bounds, and *array-constant element lists*. The last was the gap behind issue #129: `array[0..1] of Int64 = ($1, $2)` rejected the hex elements while the scalar form `= $1` accepted them. Array-constant elements now fold through the same `ParseIntLiteral` radix parser the rest of the compiler uses, so every radix behaves identically inside an initialiser. The decision to keep FPC's `&` octal (rather than Delphi's no-octal / `&`-as-escape) follows the project rule of preferring FPC syntax where the two dialects diverge; the lexer already tokenised all four radixes, so the fix only made the array-constant path consistent with the rest of the language. ==== Built-in constant `MaxInt: Integer = 2147483647` is a compiler built-in constant. It represents the maximum value of a 32-bit signed integer (2^31 − 1). In Blaise source it is used exclusively as a sentinel meaning "copy to end of string" in `Copy(S, N, MaxInt)` calls; every such call passes `2147483647` as the `count` argument to the RTL's `_StringCopy`, which clamps any count ≥ remaining length to the actual remainder. This differs from FPC, where `MaxInt = High(SizeInt) = 9223372036854775807` (the 64-bit maximum on 64-bit targets). The Blaise value is deliberately kept 32-bit: the compiler's Integer type is 32-bit, `Copy`'s `count` parameter is `Integer`, and passing a 64-bit literal through the QBE `w` calling convention truncated it to −1, causing `_StringCopy` to return an empty string. The 32-bit value avoids this without loss of expressiveness for any realistic string length. ==== Conversion built-ins [cols="2,1,2", options="header"] |=== | Built-in | Signature | Notes | `IntToStr` | `(Integer): string` | 32-bit integer to decimal string; when the argument resolves to `Int64` the codegen automatically routes the call to `_Int64ToStr`, and when it resolves to `UInt64` / `QWord` to `_UInt64ToStr`, matching FPC's overloaded resolution | `Int64ToStr` | `(Int64): string` | 64-bit signed integer to decimal string | `UInt64ToStr` | `(UInt64): string` | 64-bit unsigned integer to decimal string | `StrToInt` | `(string): Integer` | Decimal string to 32-bit integer; truncates silently on overflow | `StrToInt64` | `(string): Int64` | Decimal string to 64-bit signed integer |=== ==== Alternatives rejected * *Platform-native integer width* — rejected. A 64-bit `Integer` on 64-bit targets would break FPC ABI compatibility and cause silent data loss when interoperating with existing 32-bit FPC code or C functions that expect `int32_t`. Explicit `Int64` covers the cases that genuinely need 64 bits. * *`LongInt` alias* — deferred. The FPC/Delphi `LongInt` name maps to a 32-bit signed integer (identical to Blaise's `Integer`) and could be added as a plain type alias in a future release. `SmallInt`, `Int16`, `Word` and `UInt16` are now first-class 16-bit types, and `Cardinal` remains an alias for `UInt32`. --- === Packed Records ==== Decision Blaise supports the `packed record` qualifier with strict semantics: * `packed record ... end` lays out fields at successive byte offsets with no alignment padding between them, and applies no tail padding to the record as a whole. * `packed` is permitted only directly in front of `record`. The forms `packed class` and `packed array`, accepted (often as no-ops) by FPC and Delphi, are parse errors in Blaise. * ARC-managed field types — `string`, class references, interfaces, and dynamic arrays — retain their natural 8-byte alignment inside a packed record. Other field types (`Byte`, `Word`/`SmallInt`, `Integer`/`UInt32`, `Int64`/`UInt64`, `Double`, enums, sets, static arrays of these, and nested packed/non-packed records) pack tight. * `MaxAlign` of a packed record collapses to `1` unless it contains an ARC-managed field (in which case it stays at `8`). Arrays of fully POD packed records therefore stride at the record's exact field-byte sum. ==== Rationale The intended use of `packed` is interop with binary formats whose field layout is byte-addressed: file headers, network protocol records, and ABI structures defined by external libraries. In every such case the user wants byte-exact layout that matches an external specification, and any silent padding the compiler inserts is a bug from the caller's point of view. The decision to *forbid* `packed class` and `packed array` rather than silently accept them is deliberate: a `class` in Blaise carries a vptr and lives on the heap behind an `_ClassAlloc` header, so "packing" it would either lie (the qualifier is ignored) or break runtime invariants. Arrays in Blaise are already either dynamic (heap-managed, no inter-element padding) or static (element-strided by the element's own size). Pretending `packed` applies to them gives users false confidence in code that won't behave the way the keyword suggests. The exception for ARC-managed field types preserves a hard runtime invariant: `_StringRelease`, `_ClassRelease`, `_IntfRelease`, and `_DynArrayRelease` all perform 64-bit aligned loads through the field pointer. Honouring a 1-byte-aligned ARC field would silently slow x86_64 code and outright fault on stricter ARM targets. A 1-byte-stride layout for these types would also break the existing ARC invariant that every reference-holding slot is loadable as a single 8-byte word. Users who genuinely need a byte-exact layout that includes a string field should use `Byte` arrays or a typed pointer instead. ==== Examples [source,pascal] ---- type TPlain = record A: Byte; B: Int64; end; { SizeOf = 16 (A at 0, pad to 8, B at 8, no tail) } TPacked = packed record A: Byte; B: Int64; end; { SizeOf = 9 (A at 0, B at 1, no tail) } THybrid = packed record A: Byte; S: string; end; { SizeOf = 16 (A at 0, pad to 8, S at 8) — S stays aligned } ---- ==== Alternatives rejected * *Always pack, even ARC-managed fields* — rejected. Unaligned 64-bit loads through ARC pointers would either slow down or trap at runtime. * *Per-field `packed` annotation* — rejected. The Delphi/FPC syntax already covers the common case; finer-grained control is rare and can be expressed with manual `Byte` arrays. * *`{$ALIGN n}` / `{$PACKRECORDS n}` directives* — deferred. Blaise does not yet have a compiler directive subsystem; if a use case for intermediate alignments (2, 4) emerges, this is the obvious place to add it later. --- === Indexed Properties ==== Decision Blaise supports indexed properties — a class property with a single index parameter: ---- property Items[Index: Integer]: Pointer read Get write Put; ---- The index parameter is declared inside square brackets in the property declaration and is automatically passed to the getter and setter accessors. An indexed property read `Obj.Items[I]` emits a call to the getter with `(Self, I)`. An indexed write `Obj.Items[I] := V` emits a call to the setter with `(Self, I, V)`. Only a single index parameter is supported. Multi-index properties (as in Delphi) are not implemented. ==== Rationale Indexed properties are required for `TObjectList.Items[i]`, `TStringList.Strings[i]`, and `TStringList.Objects[i]` — the three list-access patterns used throughout the multi-file compiler source. Without them, every list access requires a direct method call rather than the idiomatic subscript syntax the compiler source already uses. ==== Alternatives rejected * *Array-typed plain properties* — the getter and setter approach is how FPC and Delphi implement all container properties. Returning an array would require value-copy semantics and would not allow write access through the subscript. * *Untyped `__getitem__`/`__setitem__` hooks* — the explicit getter/setter names in the declaration are more transparent and consistent with the existing non-indexed property design. ==== Default array property An indexed property may carry the `default` directive, marking it as the class's default array property: ---- property Items[I: Integer]: T read Get write Put; default; ---- A subscript applied directly to an object of that class — `Obj[I]` — is then sugar for `Obj.Items[I]`: it lowers to the getter on read and the setter on write, exactly as the named form does. This is the mechanism behind the familiar `List[i]` syntax. Resolution walks the inheritance chain, so a default property declared on a base class is usable through a derived class. The `default` flag is carried across units in the `.bif` interface so a default property declared in one unit keeps its subscript sugar when the unit is used elsewhere. Only one default property per class is meaningful (the first one found wins); the directive is only valid on an indexed property. The accessor dispatch honours `virtual`/`override` on the getter/setter, the same as a named indexed property access. --- === Constructor and Destructor Keywords ==== Decision Blaise recognises `constructor` and `destructor` as reserved keywords. Both are treated as aliases for `procedure` in the method declaration and implementation position: the compiler parses and emits them identically to `procedure`. The "constructor" nature of a call is determined at the call site (when `TypeName.Create(...)` is parsed), not from the method declaration keyword. ==== Rationale FPC and Delphi use `constructor` and `destructor` as distinct keywords. The Blaise RTL (`classes.pas`, `contnrs.pas`, `sysutils.pas`) follows FPC conventions so that units can be compiled by either compiler without source modification. Without keyword support the parser treated `constructor` as an identifier, causing the next token (`Create`) to be misread as the start of a field declaration. Treating `constructor` as a pure alias for `procedure` is sufficient for the current compilation model: object allocation is emitted by the call-site codegen (via `_ClassAlloc`) regardless of how the method was declared, and `destructor Destroy` is called the same way as any other method. ==== Alternatives Rejected * *Encoding constructor semantics in the declaration* — if the method declaration keyword drove allocation, every constructor override and inherited call would need special handling. The current call-site model is simpler and consistent with how FPC's codegen works at the IR level. * *Treating `constructor`/`destructor` as soft keywords* — would require context-sensitive disambiguation in the lexer; reserved-keyword status is unambiguous and matches FPC behaviour. --- == Memory Management === Automatic Reference Counting (ARC) ==== Decision Blaise uses Automatic Reference Counting (ARC) for all class instances and strings. There is no garbage collector. `Free` is retained as a manual release operation (it calls `_StringRelease` / decrements the refcount) to ease porting, but under ARC it is optional for correctly written code. ==== Alternatives rejected * *Manual memory management* (as in C, original FPC default) — rejected. The primary motivation for Blaise is a safer, more maintainable Pascal dialect. Requiring explicit `Free` everywhere is the dominant source of bugs and leaks in FPC codebases. * *Tracing garbage collection* — rejected. GC introduces unpredictable pause times and makes the runtime significantly more complex. ARC gives deterministic destruction timing, which matters for resource management (file handles, network connections) and is more predictable in latency-sensitive or systems contexts. ==== Known limitation: reference cycles ARC cannot automatically collect reference cycles. Blaise provides two non-owning reference attributes to break cycles and to model borrowed references — `[Weak]` and `[Unretained]` — applied to class- or interface-typed fields and local variables. Identifying cycles in a codebase is the principal migration cost when porting from manual-free FPC code. ===== `[Weak]` `[Weak]` (analogous to `weak` in Swift) is a *safe* non-owning reference. The runtime registers each weak slot in a weak table; when the referent is freed, every weak slot pointing at it is automatically zeroed. Reading a weak slot therefore yields either a live object or `nil` — never a dangling pointer. [source,pascal] ---- TObserver = class [Weak] FSubject: TSubject; { does not keep FSubject alive } end; ---- Assignment lowers to `_WeakAssign` (which registers the slot) and scope exit / field cleanup lowers to `_WeakClear` (which unregisters it). There is a per-assignment cost for the registry bookkeeping. Use `[Weak]` when the referent may be destroyed while the reference is still reachable, and the holder must observe that (the slot going `nil`). ===== `[Unretained]` `[Unretained]` (analogous to `unowned(unsafe)` in Swift) is an *unsafe* non-owning reference. It performs *no* reference counting and *no* weak-table registration: the field assignment is a plain pointer store, and field cleanup does nothing for it. There is zero per-assignment overhead. [source,pascal] ---- TNode = class [Unretained] Parent: TNode; { back-pointer; parent outlives the child } end; ---- Because there is no auto-nil, an `[Unretained]` field becomes a dangling pointer if the referent is freed first. Use it *only* when the referent is guaranteed to outlive the field — for example a back-pointer to an owner, or a reference into a long-lived pool (the Blaise compiler uses it for the `ResolvedType` set by semantic analysis, which points into the symbol table's type pool that outlives the AST). `[Weak]` and `[Unretained]` are mutually exclusive on the same declaration. ===== Choosing between them [cols="1,1,1", options="header"] |=== | | `[Weak]` | `[Unretained]` | Keeps referent alive | no | no | Auto-nil on referent free | yes | no (dangles) | Per-assignment cost | weak-table register | none (plain store) | Use when | referent may die first | referent outlives the field |=== ==== Porting note The migration analyser (Phase 8) is intended to identify `try..finally Obj.Free` patterns and flag them for removal. The `Free` call itself is harmless under ARC (it simply triggers an early release) but is unnecessary and misleading in new code. --- == Parameters === `out` Parameters ==== Decision `out` parameters are parsed and treated identically to `var` parameters at the code-generation level: both are passed by pointer (the caller passes the address of the variable; the callee writes through it). The `out` mode is, however, recorded distinctly from `var` on the AST: `TMethodParam.IsOutParam` is set in addition to `IsVarParam`. The flag is carried through unit-interface export/import (bit 3 of the param-flags pack in the `.bif` format) so that tooling — the loader's `TRoutineSig`, the debugger, and any future static-analysis pass — can recover the original declared mode rather than seeing every by-reference parameter as `var`. ==== Rationale In FPC/Delphi, the semantic distinction between `var` and `out` is: * `var` — the callee may read the initial value before writing. * `out` — the initial value is undefined; the callee must write before reading. Some compilers zero-initialise `out` parameters on entry. At the machine level both are pointer parameters, so they produce identical QBE IR. The distinction is purely a static-analysis / documentation concern and is not yet enforced — but it is now *preserved* in the AST and interface metadata (`IsOutParam`), which is the prerequisite for enforcing it later. ==== Future work A future lint pass could warn when an `out` parameter is read before it has been assigned within the callee, and could zero-initialise `out` parameters on entry. Both are now unblocked by the `IsOutParam` flag. --- === Open Array Parameters (`const A: array of T`) ==== Decision Blaise supports open-array parameters using the standard two-register ABI inherited from FPC and Delphi: * `l %_par_A` — pointer to the first element (a `l` / pointer-width value). * `l %_par_A_high` — the high index (count − 1); negative means the array is empty. Both registers are passed as separate QBE parameters and allocated to local variables (`%_var_A` and `%_var_A_high`) at function entry. The `const` qualifier is syntactically required for open-array parameters in the current implementation. It is advisory only: no mutation check is performed. This matches FPC's treatment, where `const` on an open array prevents re-assignment of the parameter variable but does not enforce deep immutability of the referenced elements. `Low(A)` always returns the constant `0`. `High(A)` loads and truncates `%_var_A_high` to `Integer` (`w` in QBE). Element access `A[I]` computes `base + I × sizeof(element)` using QBE pointer arithmetic and emits the appropriate load instruction for the element type. ==== ABI rationale The two-register layout matches FPC/Delphi so that Blaise can eventually call RTL functions that accept open arrays, and so that ABI knowledge transfers directly from existing Pascal documentation. An alternative would be a single-pointer struct (pointer + length), but this would require a hidden temporary at every call site and would diverge from the established Pascal ABI without benefit. ==== Forwarding Passing an open-array parameter `A` to another open-array formal forwards both `%_var_A` (data pointer) and `%_var_A_high` (high index) as two separate QBE arguments, preserving the ABI invariant. ==== ARC exclusion Open-array parameters are excluded from the ARC addref/release loops emitted at function entry and exit. The array elements are owned by the caller; the callee holds only a borrowed pointer. ==== Array literal call sites Array literals `['a', 'b', 'c']` may be passed directly as arguments to open-array formals. The literal is a stack-allocated temporary: * A buffer of `N × sizeof(element)` bytes is allocated on the stack using QBE's `alloc8` (for pointer-sized elements) or `alloc4` (for 32-bit elements) at function entry. * Each element expression is evaluated and stored into the buffer at the corresponding offset. * The high index `N − 1` is a compile-time constant; no hidden slot is needed. * The call receives two QBE arguments: the buffer pointer and the literal integer high index. The element type is inferred from the first element. All elements must resolve to the same type. Empty literals `[]` are a semantic error because the element type cannot be inferred without type-context propagation (which is deferred). --- === Calling-Convention Directives (`cdecl`, `stdcall`, …) ==== Decision A routine or method declaration may carry a calling-convention directive — `cdecl`, `stdcall`, `register`, `pascal`, or `safecall`. The directive is recorded on the declaration (`TMethodDecl.CallingConv`), propagated into the exported interface signature (`TRoutineSig.CallingConv`) and persisted in the `.bif` interface metadata so it survives separate compilation. ==== Rationale The directive is currently *metadata only*. The native backend emits the System V AMD64 convention for every routine regardless of the directive; on Linux x86_64 that is already the C ABI, so `cdecl` and the default agree and external C calls link correctly without honouring the keyword at the ABI level. Preserving the directive rather than discarding it keeps the door open for: * a future Windows/x86 target where `stdcall` and `cdecl` differ in stack cleanup and must be emitted distinctly; * FFI tooling and the debugger, which need the declared convention to describe a symbol faithfully. Discarding the keyword at parse time — the previous behaviour — would have forced a parser and interface-format change later to recover information that was present in the source all along. Recording it now is cheap and avoids that churn. --- === Static Arrays (`array[L..H] of T`) ==== Decision Blaise supports fixed-size, stack-allocated arrays declared as `array[L..H] of T`, where `L` and `H` are compile-time integer literals and `T` is any scalar or record type. * The array occupies a contiguous block of `(H − L + 1) × sizeof(T)` bytes on the stack, allocated with QBE's `alloc4` or `alloc8` instruction. * The block is zero-initialised with `memset` at function entry (Pascal default initialisation semantics; also required by QBE's SSA checker). * Element access uses `base + (I − L) × sizeof(T)`. Non-zero-based ranges are supported: `array[5..9] of Integer` stores 5 elements; `A[5]` maps to offset 0. * `sizeof(T)` for array element sizing uses the _raw_ byte footprint of the element type: 1 for `Byte`/`Boolean`, 4 for `Integer`/`UInt32`, 8 for `Int64`/`UInt64`/`String`/pointer types. This differs from the standalone-variable allocation size on the stack, which always allocates at least 4 bytes (QBE's smallest `alloc4`), because array elements are packed consecutively. * Write access `A[I] := val` is parsed as a `TStaticSubscriptAssign` statement node distinct from `TAssignment`, because the LHS is not a bare identifier. * Read access `A[I]` in an expression is handled by the existing `TStringSubscriptExpr` / `EmitStringSubscriptExpr` dispatch, extended with a `tyStaticArray` branch before the `tyOpenArray` branch. ==== Syntax variants and scope [cols="2,1"] |=== | Syntax | Status | `array[0..N] of T` — zero-based | Supported | `array[L..H] of T` — non-zero-based | Supported | `array[TEnum] of T` — enum-type index | Supported | `array[L..H, M..N] of T` — multi-dimensional | Supported | `array of array of T` — multi-dim open array | Separate feature | `type TArr = array[L..H] of T` — named array type alias in type section | Supported |=== ==== Named array type aliases A named type alias for a static array may be declared in a `type` section: [source,pascal] ---- type TByteBuffer = array[0..63] of Byte; TIntRange = array[1..10] of Integer; var Buf: TByteBuffer; R: TIntRange; ---- The alias name is registered as a `tyStaticArray` symbol in the symbol table, identical to an inline anonymous declaration. Any context that accepts an inline `array[L..H] of T` declaration also accepts a named alias. *Decision rationale:* The parser already handled `array[L..H] of T` as part of `ParseTypeName` (used in variable declarations, parameter types, and record fields), but the `ParseTypeDecl` path did not include `tkArray` as a valid token on the right-hand side of a `type` section alias. The fix adds `tkArray` to the existing `tkCaret | tkIdent` branch (which handles pointer aliases and simple name aliases) so all three use the same `TTypeAliasDef` → `ParseTypeName` path. The semantic pass already resolved `'array[L..H] of T'` style names via `FindTypeOrInstantiate`; the only change needed there was to try that function when `FTable.Lookup` returns nil for a type-alias name, rather than immediately raising an error. *Alternatives rejected:* * Storing the array alias as a `TStaticArrayTypeDef` node: would require a new AST node type and a new semantic pass. The alias-via-string-name approach reuses the existing on-demand instantiation machinery at zero code cost. * Requiring the alias to use a distinct keyword or syntax: rejected because FPC and Delphi both use the plain `type TArr = array[...] of T` form. ==== Deferred items * `Low(A)` and `High(A)` for static arrays (returns `LowBound`/`HighBound`). * `@A[I]` — address-of subscript. * Global static array variables (require QBE data-section layout for variable-size blocks). * Static arrays as value parameters (pass-by-copy semantics). * Chained subscript on record fields: `Rec.Arr[I]` (local arrays only for now). --- == Compiler / Build System Integration === Build-Tool-Drives-Compiler ==== Decision The Blaise compiler accepts `--unit-path ` (repeatable) to specify unit search directories, and resolves `uses` clauses by searching those directories for matching `.pas` files. No project file format is baked into the compiler. The build tool (PasBuild, or any Makefile / shell script) is responsible for passing the correct `--unit-path` flags. This mirrors the approach taken by Go (`go build` passes `--importcfg`), Rust (`cargo` passes `--extern`), and Swift (`swiftc` takes `-I` paths). ==== Alternatives rejected * *`--project project.xml`* — rejected. A compiler that requires a project manifest couples the compiler to a specific build tool and file format. Changes to the manifest format require compiler changes. All modern compiled-language toolchains avoid this coupling. ==== FPC-style invocation When invoked with FPC-style single-dash flags (as PasBuild does when `--fpc /path/to/blaise` is configured), the compiler translates `-Fu` to `--unit-path` entries internally. This allows PasBuild to drive the Blaise compiler without modification. --- === Unit File Name Casing ==== Decision Unit file names are stored in lowercase on disk. The unit loader (`TUnitLoader.Locate`) normalises the name from the `uses` clause to lowercase before appending `.pas`, with a fallback to the exact case as written. This means `uses Classes`, `uses classes`, and `uses CLASSES` all resolve to `classes.pas` on a case-sensitive filesystem. ==== Rationale Pascal is a case-insensitive language. Delphi runs on Windows (case-insensitive filesystem) so `uses Classes` finds `Classes.pas` or `classes.pas` equally. Linux and FreeBSD filesystems are case-sensitive: without normalisation, `uses Classes` would fail to find `classes.pas`. FPC on Linux searches in this order: exact case, then lowercase, then all-uppercase. Blaise simplifies this to: lowercase first, exact case as fallback. Because the convention is that all Blaise-native unit files use lowercase names, the lowercase probe succeeds for every well-formed unit and the fallback is only needed for files authored outside the convention (e.g. a user's legacy `MyUnit.pas`). ==== Alternatives rejected * *Case-sensitive lookup only* — rejected. Would break valid Pascal code the moment a developer writes `uses Classes` and the file is `classes.pas`. Pascal programmers should not have to think about disk capitalisation. * *Full directory scan for case-insensitive match* — rejected. Scanning each directory on every lookup is O(n) in directory size. Lowercase normalisation is O(1) and sufficient given the lowercase convention. --- == Platform Interoperability === POSIX and Xlib (Linux, macOS BSD layer) POSIX APIs accept `char*` — null-terminated byte strings. Modern Linux and macOS (POSIX layer) are universally UTF-8. Blaise's `PChar(str)` cast returns a pointer to the string's character data, which is already null-terminated by the RTL. This is a valid `char*` with no conversion required. Xlib provides two generations of text functions. The older functions (e.g. `XDrawString`) treat strings as Latin-1 bytes. The modern `Xutf8*` family (e.g. `Xutf8DrawString`) accept UTF-8 directly. Blaise code targeting Xlib should use the `Xutf8*` functions; `PChar(str)` is the correct way to obtain the argument. fpGUI already targets the Xlib UTF-8 functions on Linux. No conversion layer is needed. === Windows Win32 / GDI Win32 provides two API variants: * `A` (ANSI) — accepts code-page-dependent byte strings. Not suitable for Unicode-correct code; should not be targeted. * `W` (Wide) — accepts `wchar_t*`, which is UTF-16LE null-terminated. Blaise always targets the `W` variants. Since Blaise strings are UTF-8 internally, a UTF-8 to UTF-16LE conversion is required at each Windows API call site. ==== Chosen approach: RTL boundary conversion The conversion is handled entirely by the RTL, not the language. The language does not gain a UTF-16 string type. Instead: * The RTL provides a `PWideChar` pointer type (an opaque 64-bit pointer, analogous to `PChar`). * An RTL function `_StringToWideZ(str: string): PWideChar` converts a Blaise string to a temporary null-terminated UTF-16LE buffer using the platform's `MultiByteToWideChar` or equivalent. * Windows RTL wrapper functions accept Blaise strings directly and perform the conversion internally before forwarding to the `W` API. User code calls the RTL wrapper and passes a Blaise string; the UTF-16 buffer is created, used, and freed within the wrapper. The conversion never surfaces in user code. ==== Rationale for not adding a UTF-16 string type Every major cross-platform toolkit (Qt, Java, .NET, Go, Python, Swift) that adopted UTF-16 internally has accumulated conversion overhead and complexity at POSIX boundaries. The boundary conversion problem does not disappear — it simply moves from the Windows boundary to the POSIX boundary. Keeping UTF-8 as the single internal encoding and converting only at the Windows API boundary is strictly simpler and is the approach now recommended by Microsoft's own documentation for new cross-platform code. ==== Status _Deferred to Phase 6 (LLVM backend + Windows target). No implementation required for the current Linux x86_64 target._ === Set Types (`set of TEnum` / `set of Byte` / `set of 0..255`) ==== Decision Blaise supports Pascal bit-set types declared as `set of BaseType`, where `BaseType` is a previously-defined enumeration, one of the small ordinal types `Byte` (256 values, 0..255) or `Boolean` (2 values), or an integer subrange within 0..255 (e.g. `0..9`, `1..10`, `0..255`). A set variable holds a bitmask in which bit N is set when the member with ordinal N is included. ==== Ordinal-based sets In addition to enum-based sets, Blaise supports `set of Byte` and `set of Boolean`. These ordinal-base sets use the same bitmap representation as enum sets: `set of Boolean` is a 2-bit (1-byte) small set, while `set of Byte` is a 256-bit (32-byte) jumbo set. Set literals for ordinal-base sets accept integer literals and range expressions: [source,pascal] ---- type TByteFlags = set of Byte; var F: TByteFlags; begin F := [1, 5, 10..20]; Include(F, 200); if 15 in F then WriteLn('yes'); end. ---- Integer range expressions `[lo..hi]` are expanded to individual members at compile time using `EvalConstIntExpr`. Bounds are validated against the set's bit count (0..255 for Byte, 0..1 for Boolean). A Boolean operand is accepted on either side of `in` and as the second argument of `Include`/`Exclude` for a `set of Boolean`; the operand only needs to be ordinal-compatible with the set's base type, not strictly numeric. `True in s` and `Include(s, False)` therefore work directly, without an intervening `Ord()`. Larger ordinal types (`Integer`, `Word`, `Int64`) are rejected because they would require impractically large bitmaps. ==== Integer-subrange base (`set of 0..255`) The set base may also be an anonymous integer subrange, `set of L..H`, as in standard Pascal, Delphi, and FPC: [source,pascal] ---- type TDigits = set of 0..9; (* type-declaration form *) var Small: set of 1..10; (* inline form in a var declaration *) begin Small := [1, 5, 8]; if 5 in Small then WriteLn('yes'); end. ---- The subrange lowers to the same bitmap machinery as `set of Byte`: the bitmap is sized to `H + 1` bits (member ordinals are the integer values themselves), so `set of 0..63` is a 64-bit small set and `set of 0..255` is a 256-bit jumbo set. Both bounds may be integer literals, named constants, or constant expressions, resolved through the same `ResolveArrayBound` path used for static-array bounds. The lower bound must be `>= 0`, the upper bound `<= 255`, and the range must be ascending (`H >= L`). A descending subrange (`set of 5..3`) is a semantic error rather than a silently-empty set, and an upper bound above 255 is rejected for the same 256-element ceiling that applies to every set. The bitmap always starts at ordinal 0 — `set of 1..10` still reserves bits for 0 — which keeps the representation identical to `set of Byte` and avoids a base-offset in the `_Set*` RTL helpers. ==== Storage model Storage scales with the base type's value count, so the common small-set case stays a single fast machine register and larger sets remain correct: - ≤ 32 values: a single QBE `w` (32-bit word) register bitmask. - 33–64 values: a single QBE `l` (64-bit long) register bitmask. - 65–256 values ("jumbo" sets): an inline byte-array bitmap of `ceil(N/8)` bytes (≤ 32 for the 256-value ceiling). QBE has no integer type wider than `l`, so a jumbo set cannot live in one register; it is treated as a value aggregate (like a record or static array) — passed by reference, returned via sret, zero-initialised with `memset`, copied with `memcpy` — and its operations (`in`, `+`, `-`, `*`, `=`, `<>`, `Include`, `Exclude`, `for..in`) are performed by the `_Set*` RTL helpers in `blaise_set.pas` rather than inline register instructions. - More than 256 values: rejected by the semantic analyser. 256 is the classic Pascal maximum — a byte-indexed set covering ordinals 0..255 — matching FPC's 32-byte fixed-size set. In every form, member ordinal N occupies bit `N mod 8` of byte `N div 8`, so the bitmap layout is identical whether the set lives in a register or memory. Sets are value types (copied on assignment), not heap-allocated. The Java `EnumSet` design is the closest analogue: ≤ 64 members use a single machine word (`RegularEnumSet`), and a wider universe transparently switches to a multi-word bitmap (`JumboEnumSet`) with the same operations applied across the words. ==== Set literal sizing (`X in [a, b, c]`) The anonymous set type synthesised for an `in`-literal is sized to the **largest ordinal actually listed** (when every element is a compile-time constant), not the base enum's full member count. So `Token in [tkBegin, tkEnd]` over an 86-member token enum stays a fast register set, and only widens to a jumbo set if a listed member's own ordinal is ≥ 64. The register membership test carries a range guard so that testing an element whose ordinal exceeds the set's width correctly yields `False` (rather than relying on an out-of-range shift). This keeps the compiler's own pervasive `Kind in [...]` token tests off the jumbo path and avoids a latent miscompile that the old fixed-`l` representation hid (any listed ordinal ≥ 64 was silently dropped). ==== Set literals and disambiguation Set literals use the same `[elem, ...]` bracket notation as array literals. The semantic pass resolves the ambiguity from the target type: - *Assignment* — when the LHS is a set type, the bracket expression is analysed as a set literal; otherwise it is an array literal. - *Call argument* — a bracket literal passed where a `set of ` parameter is expected is treated as a set constructor. Overload resolution matches it when the members share the parameter's base enum (or the literal is empty); the chosen call then re-types the argument to the parameter's set type so the bitmask is emitted. This means a set value can be passed inline, e.g. `Configure([optA, optC])`, without a named intermediate variable. An empty literal `[]` carries no element type of its own, so it is only valid where a set target supplies the type — a set assignment or a `set of` argument. Used anywhere else (e.g. assigned to a non-set variable) it is a semantic error, not a silent no-op. ==== Set operators [cols="1,2,2"] |=== | Operator | Meaning | QBE IR | `S1 + S2` | union | `or` | `S1 - S2` | difference | `xor` then `and` | `S1 * S2` | intersection | `and` | `elem in S` | membership test | `shr` then `and 1` | `S1 = S2` | equality | `ceqw` | `S1 <> S2` | inequality | `cnew` |=== ==== Include and Exclude `Include(S, elem)` and `Exclude(S, elem)` are built-in procedures (not language keywords) that modify a set variable in-place using bitwise OR and AND-NOT respectively. This matches FPC's implementation model. ==== Set-valued constants A `const` may take a set literal on its right-hand side: [source,pascal] ---- type TDir = (dNorth, dSouth, dEast, dWest); TDirSet = set of TDir; const Both = [dNorth, dEast]; // type inferred: set of TDir Horizontal : TDirSet = [dEast, dWest]; // type annotated None : TDirSet = []; // empty set ---- The members must all belong to a single enumeration. The constant's type is that enum's set type: when the declaration is annotated (`: TDirSet`) that declared set type is used (and the members are checked against its base enum); otherwise the type is the inferred `set of `. The bitmask is folded at compile time into the constant's value, so a set const is emitted exactly like any other integer set value — referencing it costs nothing at runtime. Because set values are structural (not nominal), an inferred `set of TDir` constant is assignment-compatible with a variable declared as the named alias `TDirSet`; `CheckTypesMatch` treats two `tySet` types over the same base enum as the same type. The empty set `[]` is only allowed with a type annotation: with no members and no declared type there is nothing to infer the base enum from, so `const Bad = [];` is a semantic error. Set-const resolution runs in the second constant-analysis pass (alongside array constants), after enum types and their members have been registered — the member identifiers are not in scope during the first pass. ==== Rejected alternatives - *Heap-allocated bitsets* — rejected. All current set uses are flag sets over small enumerations (≤ 32 members). A single word is the correct choice. - *`not` for set complement* — deferred. Not required for current self-hosting targets. Can be added as a unary `not` on set operands when needed. - *`set of Integer`* — rejected. Integer has 2^32 possible values; a bitmap would require 512 MB. Only Byte (256 values) and Boolean (2 values) are accepted as ordinal base types alongside enumerations. - *Integer-literal set constants* — deferred. The `const X = [1, 2, 3]` parser path currently only accepts identifier (enum) members. Ordinal-base set literals work in var/assignment/argument context; set constants with integer members require parser extensions. --- === Procedural Types (`function`/`procedure` pointers) ==== Decision Blaise supports two procedural type forms: - *Bare procedural types* — pointers to standalone (non-method) functions. Storage: a single QBE `l` (8-byte code pointer). - *Method-pointer types* — declared with the `of object` suffix. Pointers to instance methods carry both a code pointer and a `Self` pointer. Storage: a 16-byte block, `Code` at offset 0, `Data` at offset 8. [source,pascal] ---- type TIntFn = function: Integer; TStrFn = function(const S: string): Integer; TLogProc = procedure(Level: Integer; const Msg: string); TRunMethod = procedure of object; TIsTrue = function (X: Integer): Boolean of object; var F: TIntFn; G: TRunMethod; begin F := @MyFn; X := F(); // indirect bare-pointer call G := TRunMethod(SomeMethod); // assigned via TMethod cast G; // calls Code(Data, ...) end; ---- A bare-procedural variable holds a single function code pointer. An indirect call `F(args)` loads the pointer from `F` and emits a QBE indirect call. A method-pointer variable holds a 16-byte block. An indirect call loads both halves, then emits `call code(l data, args...)` — `Data` is passed as the implicit first argument so the callee sees it as `Self`. The intrinsic record `TMethod = record Code, Data: Pointer end` has the exact same byte layout, so the cast `TMyMethod(m)` for `m: TMethod` is a no-op at the QBE level. This is the canonical xUnit dispatch pattern: [source,pascal] ---- m.Code := MethodAddress(Self, FName); m.Data := Self; RunMethod := TRunMethod(m); RunMethod; ---- Type compatibility requires return types to match (both nil or both the same `TTypeDesc`), parameter lists to match pairwise on type and mode (`var`/`const`/value), AND the `IsMethodPtr` flag to match — a bare procedural pointer and a method pointer with otherwise-identical signatures are *not* compatible. Parameter names do not participate. ==== Rejected / deferred - *`reference to function/procedure`* (anonymous methods / closures) — *deferred.* Closures need an environment record carrying captured locals, far beyond what a (Code, Data) pair represents. No current Blaise feature needs them. - *Calling-convention markers* (`cdecl`, `stdcall`) on procedural types — *deferred.* The current self-hosting target uses the System V x86_64 ABI uniformly; no use case yet requires non-default conventions on procedural pointers. ==== Rationale Bare procedural types are sufficient for callback APIs that do not need instance state (the original punit RTL test framework's `TTestRun` / `TTestSetup` hooks, sort comparators, signal handlers). Method pointers are required by every xUnit-style test framework: both fpcunit (`TRunMethod = procedure of object`) and fptest (`TTestMethod = procedure of object`) dispatch their tests through them. The 16-byte fat-pointer representation matches FPC and Delphi exactly; the byte-equivalence with `TMethod` lets the canonical `TRunMethod(LMethod)` cast pattern work unchanged in ported code. A single `IsMethodPtr` flag on the type descriptor keeps both forms in one descriptor class. The flag participates in `IsCompatibleWith` so a method pointer cannot accidentally bind to a bare-procedural variable, preventing ABI mismatches at call sites that would otherwise corrupt arguments. The `reference to` form is deliberately scoped out: closures interact with ARC lifetimes through the environment record's captured class references, which would push the design into territory that no current Blaise consumer requires. --- == File Path Manipulation Built-ins === Decision `ChangeFileExt`, `ExtractFileName`, `ExtractFilePath`, and `IncludeTrailingPathDelimiter` are implemented as compiler built-in functions backed by C RTL helpers (`_ChangeFileExt`, `_ExtractFileName`, `_ExtractFilePath`, `_IncludeTrailingPathDelimiter` in `blaise_io.c`). === Rationale These are the four `SysUtils` path functions required by the self-hosting compiler source (`Blaise.pas` and related units). The todo notes acknowledged that a Blaise-native `SysUtils` shim unit would be a cleaner long-term design, but that approach requires multi-unit compilation support, which is not yet available. Implementing them as compiler built-ins matches the precedent set by `GetEnvVar`/`GetEnvironmentVariable` (step 10) and unblocks the multi-source rebuild immediately. Once the compiler can compile multiple units, these built-ins can be migrated to a Blaise-native `SysUtils.pas` shim; at that point the compiler knowledge of these names should be removed. === Semantics All four functions take `string` arguments and return `string`: [cols="2,1,3"] |=== | Function | Args | Behaviour | `ChangeFileExt(Path, Ext)` | 2 | Replaces extension in `Path` with `Ext` (which should include the leading dot, or be empty to strip the extension). Only the last dot in the base-name portion (after the final `/`) is replaced. | `ExtractFileName(Path)` | 1 | Returns everything after the last `/`, or `Path` unchanged when no `/` is present. | `ExtractFilePath(Path)` | 1 | Returns everything up to and including the last `/`, or an empty string when `Path` contains no directory separator. | `IncludeTrailingPathDelimiter(Path)` | 1 | Returns `Path` with a trailing `/` appended; returns `Path` unchanged when it already ends with `/`. |=== === Grammar These built-ins are resolved as regular function calls; no grammar change is required. --- == Iteration === `for..in` Loop — Class-Based Enumerators ==== Decision Blaise supports `for X in Collection do` iteration via the _GetEnumerator protocol_, matching FPC and Delphi semantics exactly: . The collection expression must be a class instance. . The class must have a zero-argument method `GetEnumerator` returning a class. . The returned enumerator class must have a `MoveNext: Boolean` method and a `Current: T` property whose getter yields the element type. . The loop variable must be compatible with the element type `T`. The compiler desugars the loop as follows: [source,pascal] ---- { for X in Collection do Body } __enum := Collection.GetEnumerator; while __enum.MoveNext do begin X := __enum.Current; Body end; ---- The synthetic `__enum` variable is injected into the enclosing function's local variable scope by the semantic pass so that ARC scope-exit cleanup releases the enumerator automatically. ==== What is supported * Class-based enumerators (Step 7a): any class with `GetEnumerator` / `MoveNext` / `Current` property — covers `TStringList`, `TObjectList`, and user-defined collections. * Static arrays (Step 7b): `for X in Arr do` where `Arr: array[L..H] of T` uses index-based iteration; non-zero-based ranges are supported. * String byte iteration (Step 7c): `for B in S do` iterates the raw UTF-8 bytes of `S`. The loop variable must be an ordinal type (`Byte`, `Integer`, etc.); each iteration yields one byte via `loadub`. There is no `Char` type in Blaise — `S[N]` and `for B in S do` both expose bytes, not codepoints. This is consistent with the language-rationale decision documented under _No Char Type_. * Generic collection iteration (Step 9b): `for X in List do` where `List` is a generic class instantiation (e.g. `TList`) — requires `GetEnumerator` / `MoveNext` / `Current` property on the instantiated type. Property cloning in `InstantiateGeneric` is implemented; works for any generic class that exposes the enumerator protocol. * Set iteration (Step 7d): `for X in S do` where `S` is a `set of TEnum` value — iterates the members of the set in ascending ordinal order. The set expression is evaluated once before the loop begins. The loop variable must be an ordinal type compatible with the set's base enum. ==== What is not yet supported All known for..in collection kinds are now implemented. ==== Set iteration desugaring `for X in S do Body` where `S: set of TEnum` desugars to a bit-scan loop: [source,pascal] ---- { for X in S do Body } __setmask := S; { evaluate S exactly once } __idx := 0; while __idx < BitCount do begin if (__setmask shr __idx) and 1 <> 0 then begin X := TEnum(__idx); Body end; __idx := __idx + 1 end; ---- The set expression is evaluated into a synthetic `__setmask` slot before the loop so that side effects (if any) occur once, matching Delphi semantics. Iteration is always in ascending ordinal order (bit 0 first), regardless of the order in which members were added to the set literal. The loop variable must be an ordinal type; using a non-ordinal variable (e.g. `string`) is a semantic error. Assigning to an `Integer` variable is permitted (enum/integer ordinal compatibility applies, as elsewhere in Blaise). ==== Alternatives rejected * *FPC-style `IEnumerable` interface* — Blaise does not require the collection to implement an interface. The structural protocol (GetEnumerator / MoveNext / Current) is checked by the semantic pass via name lookup. Requiring an interface declaration would add boilerplate for simple use cases. * *Index-based desugaring for arrays* — deferred to a follow-on commit; using the same protocol for all iterable types keeps the compiler simpler. --- == Exception Handling === Typed `except` Handlers (`on E: TClass do`) ==== Decision Blaise supports typed exception dispatch inside `except` blocks using the `on [Var :] TypeName do Stmt` syntax, matching FPC and Delphi semantics. Multiple `on` clauses are checked in order; an optional `else` clause runs if no typed handler matches. A catch-all `except ... end` block (no `on` clauses) remains supported for simple cases. ==== Grammar [source,ebnf] ---- TryExceptStmt = TRY StmtList EXCEPT ExceptBody END ; ExceptBody = OnClause { ";" OnClause } [ ";" ] [ ELSE StmtList ] | StmtList ; OnClause = "on" [ IDENT ":" ] TypeRef DO Stmt ; ---- ==== Semantics * The type after `:` must resolve to a class type; primitive types are rejected by the semantic pass with an error. * When a variable binding is present (`on E: EFoo do`), a synthetic local variable of the handler's type is injected into the enclosing function's declaration block so that `EmitVarAllocs` allocates a stack slot at function entry. The slot is initialised to the current exception pointer at the start of each matched handler body. * Handler matching uses `_IsInstance` (the same C helper used by the `is` operator), which walks the vtable TypeInfo parent chain — so `on E: EBase do` correctly catches instances of any subclass of `EBase`. * If no `on` clause matches and there is no `else` clause, the exception is re-raised to the enclosing handler via `_Reraise`. * Bare `raise` inside a typed handler retrieves the live exception from `g_current_exception` (via `_CurrentException()`) and re-raises it. Using `g_current_exception` (set by `_Raise` at raise time) rather than `g_exc_top->exception` (which reads the _current_ frame's field) is critical for correctness: by the time the handler body runs, `_PopExcFrame` has already unlinked the handler's own frame, so `g_exc_top` either points to an enclosing frame (with `exception = NULL`) or is NULL at the outermost level. ==== Alternatives Rejected * *Storing exception in the frame before popping* — the frame is on the stack and its lifetime ends with `_PopExcFrame`. Capturing via `g_current_exception` (a thread-local global) avoids frame lifetime issues. * *Requiring `on E: T` to always have a variable binding* — Delphi and FPC both permit the no-binding form `on T do`; Blaise follows suit. === Integer division by zero ==== Decision Integer `div` and `mod` by zero raise a catchable `EDivByZero` exception (declared in `SysUtils`, a subclass of `Exception`) rather than trapping in hardware. The code generator emits a divisor-equals-zero check before each integer `idiv`; when the divisor is zero it calls the runtime helper `_RaiseDivByZero`, which raises `EDivByZero('Division by zero')` through the normal exception machinery. Both backends (QBE and native x86-64) emit the guard. The guard is emitted *only when `SysUtils` is in scope* — i.e. when `EDivByZero` resolves through the active symbol table. A program that does not use `SysUtils` cannot name the exception class anyway, and its integer division falls through to the hardware behaviour (a `SIGFPE` trap) as before. This mirrors Delphi, where `EDivByZero` lives in `System.SysUtils` and the catchable behaviour is tied to that unit. ==== Rationale A bare hardware trap is uncatchable: a `try`/`except` around `a div b` cannot recover from a zero divisor, so a single bad input terminates the whole process. Routing division by zero through the exception system makes it recoverable, matching standard Pascal/Delphi semantics and the expectations of code that validates input by catching the exception rather than pre-checking every divisor. ==== Alternatives Rejected * *Leave it as a hardware trap* — zero runtime cost, but a zero divisor crashes hard and uncatchably; unacceptable for code that handles untrusted input. * *Define `EDivByZero` in the always-linked runtime so the guard is unconditional* — the runtime (`blaise_exc.pas`) deliberately does not depend on stdlib, and exception classes (with their vtable + TypeInfo) are a stdlib concern. Gating the guard on `SysUtils` keeps the runtime free of class machinery and matches Delphi's unit placement. --- == Project Governance === Licensing ==== Decision The Blaise compiler, RTL, and bundled tooling are licensed under the *Apache License, Version 2.0 with Runtime Library Exception*. The SPDX identifier used in source headers and project metadata is: Apache-2.0 WITH Swift-exception The Runtime Library Exception text used is the one defined by the Swift project, reproduced verbatim at the foot of `LICENSE` in the project root. Binaries produced by the Blaise compiler that incorporate portions of the RTL do not inherit attribution obligations from the licence. ==== Alternatives considered *BSD-3-Clause* — the project's original licence, in use during early development. Permissive, short, and well-recognised. * Rejected for one specific reason: it grants no patent licence. As Blaise grows and accepts external contributions, every contributor becomes a potential future patent risk under BSD-3. For a *compiler* — which by nature implements algorithms (parsing, optimisation, code generation) that may be patent-encumbered — this is the wrong default. *BSD-3-Clause + custom patent grant + custom Runtime Library Exception* — a bespoke combination, ~50 lines. * Rejected because it is not OSI-approved, has no SPDX shorthand, and trades concise text for unfamiliar text. A custom licence is *more* friction for adopters' legal review than a 12-page Apache 2.0, because every clause must be read from scratch rather than recognised from precedent. *BSD-2-Clause-Patent* — an OSI-approved short licence with patent grant. * Rejected because it is not widely adopted in compiler/runtime land and would still require a custom Runtime Library Exception bolted on, putting us back in bespoke territory. *Modified-LGPL with linking exception* — the Free Pascal Compiler's licence, familiar to the Pascal community. * Rejected because it carries weak-copyleft obligations on the file level. Apache 2.0 is fully permissive and avoids any ambiguity for commercial adopters. *MIT* — minimal permissive licence. * Same patent-grant gap as BSD-3, but shorter. Same rejection. ==== Rationale The Apache 2.0 + Runtime Library Exception combination is the modern consensus for compiler/runtime projects: * *Swift* (Apple) — Apache 2.0 with their Runtime Library Exception text. The wording Blaise uses is theirs, verbatim. * *LLVM* — Apache 2.0 with the LLVM Exceptions (relicensed in 2019 from a BSD-style licence specifically to gain the patent grant). * *Rust* — dual MIT / Apache 2.0; Apache 2.0 is the patent-grant path. Specific properties that mattered for this decision: . *Explicit patent grant* (Apache 2.0 Section 3). Each contributor grants users a patent licence covering their contributions. A contributor who later sues a user over patents in their contribution loses their grant. This is the headline feature versus BSD-3. . *Patent retaliation clause* (Apache 2.0 Section 3, second sentence). Anyone who sues an Apache 2.0 user over the project loses their licence. This deters submarine patent claims. . *Runtime Library Exception*. Without an exception, every binary compiled by Blaise would technically inherit Apache 2.0 obligations because the RTL is linked in. The Swift-style exception explicitly releases that obligation, allowing closed-source applications to ship binaries built with Blaise without Apache attribution noise. . *NOTICE file mechanic* (Apache 2.0 Section 4(d)). Concentrates third-party attribution requirements (currently QBE) in one well-known file that downstream redistributors must preserve. . *OSI-approved and SPDX-listed*. Tooling (FOSSology, ScanCode, GitHub licence detection) recognises both the licence and the exception identifier without configuration. . *Battle-tested in legal review*. Major compiler projects use exactly this combination; corporate legal teams have pre-approved it. ==== Notes * The relicensing happened pre-public-release. No external contributor consent was needed because Graeme Geldenhuys was the sole contributor at the time of the change. * All `.pas`, `.pp`, and `.inc` source files in `compiler/`, `rtl/`, `tools/`, and `tests/` carry an `SPDX-License-Identifier: Apache-2.0 WITH Swift-exception` header. * `LICENSE` at the project root contains the full Apache 2.0 text followed by the Runtime Library Exception clause. * `NOTICE` at the project root contains attribution for vendored third-party code (currently QBE under MIT). * `vendor/qbe/` retains its original MIT licence; QBE is invoked as a subprocess and is not statically linked into the Blaise compiler. --- == Function and Method Overloading === Decision Blaise supports overloading of standalone procedures, standalone functions, and class methods. The `overload` directive is *required* on every declaration that participates in an overload set: omitting the directive on any same-named declaration is a compile-time "duplicate identifier" error. Resolution at the call site follows Delphi semantics — exact-type match is preferred over widening match, and an unresolvable tie is a compile-time "ambiguous overload" error. === Rationale Requiring the directive on every overload (rather than inferring it from duplicate names, as FPC's `{$mode objfpc}` does) keeps overload sets explicit at the source level: a reader can see at a glance that a declaration is intended to participate in dispatch, and accidental shadowing of an unrelated symbol cannot silently introduce an overload. Delphi's preference for exact match avoids the "promotion ambiguity" pitfalls that surface when an `Integer` argument matches both an `Integer` and a `Double` overload — exact match always wins, ambiguity is escalated to the user. === Overload sets merge across inheritance When a derived class declares an `overload` method whose name matches an overload inherited from a base class, the two sets *merge*: both the inherited and the newly declared variants remain callable on the derived type. [source,pascal] ---- TBase = class function F(x: Integer): string; overload; end; TDerived = class(TBase) function F(s: string): string; overload; // adds to, does not replace end; var d: TDerived; begin d.F(5); // resolves to TBase.F(Integer) d.F('a'); // resolves to TDerived.F(string) end; ---- This follows Delphi: a derived method marked `overload` extends the inherited overload set rather than hiding it. A derived method declared *without* `overload` still hides every inherited variant of that name (the conventional "redeclaration hides" rule) — only the `overload` directive opts into merging. Resolution collects candidates up the inheritance chain until it reaches a level that declares the name without `overload`, then scores the combined set with the normal exact-over-widening rules. === Name mangling Each overload is emitted under a distinct QBE symbol name derived from its parameter signature. The mangling scheme uses short type codes appended after `$`: [cols="1,3"] |=== | Code | Meaning | `i` | Integer (32-bit signed) | `l` | Int64 | `d` | Double | `s` | Single | `b` | Boolean | `y` | Byte | `S` | string | `p` | Pointer / PChar | `R` | record or class type | `^X` | typed pointer to X | `@X` | var/out parameter of X |=== Example: `procedure Log(const S: string; N: Integer); overload;` is named `Log$Si` after type-code mangling, then escaped through `QBEMangle` to the QBE symbol `$Log_D_Si` (the leading `$` sigil is preserved; any embedded sigil characters are escaped as listed below). Phase A of the implementation uses an arity-only suffix (`$N`) as a temporary mangling — the type-code scheme above becomes the permanent form once Phase B (type-distinct resolution) lands. The mangled name still contains the original sigil characters (`$`, `@`, `^`) which conflict with the QBE symbol grammar in some downstream toolchains. `QBEMangle` rewrites them as alphabetic escapes before the name is emitted to IR: [cols="1,3"] |=== | Sigil | Escape | `$` | `_D_` (overload delimiter) | `@` | `_V_` (var-param prefix) | `^` | `_P_` (pointer prefix) |=== Function definitions, call sites, and vtable data entries all route through `QBEMangle`, so the same overload always reaches the same QBE symbol — a previous mismatch between definition and call site is now covered by the regression tests in `cp.test.overload.pas`. === Alternatives rejected * *Implicit overloading on duplicate names.* FPC's objfpc mode allows this. Rejected because it makes accidental shadowing indistinguishable from intentional overloading. * *Numeric suffixes (`Greet$1`, `Greet$2`).* Simpler to implement but obscures the signature in debugger backtraces and IR diffs. Type codes are deterministic and human-decodable. * *FPC-style "first-fit" resolution.* Picks the first declared overload whose parameters can be coerced. Rejected in favour of Delphi's exact-match preference, which surfaces ambiguity rather than silently picking an order-dependent answer. === Class methods A virtual method that is overloaded must carry both `virtual` and `overload`. Overrides in descendants must carry both `override` and `overload`. Each `(virtual, overload)` pair is a *distinct* vtable slot — the override targets the slot whose mangled signature matches its own. This means you cannot override the wrong overload by accident, and the compiler will report a missing override target if the descendant's signature matches no base-class slot. --- == Default Parameter Values === Decision Trailing parameters of a procedure, function, constructor, destructor, or class method may carry a default value introduced with `=`. A call site may omit any trailing argument whose corresponding parameter has a default. The compiler fills the missing slots with a fresh AST clone of the default expression at each call site. A default value is permitted only when *all three* conditions hold: * the parameter group declares a single name (no comma list); * the parameter is not `var`/`out`; * the parameter is not an open array. A default value must be a literal of the parameter's type or a named constant whose resolved type matches. Permitted forms: integer literal, float literal, string literal, the `nil` keyword, and a bare identifier that resolves to a `skConstant` symbol. Arbitrary constant expressions are *not* permitted in this iteration. === Rationale Default values close the most common ergonomics gap between FPC and Blaise: porting library code (such as the `punit` test framework's `AddTest('name', @proc)` four-arity wrapper around the three-arity core) requires either a small explicit overload pyramid or default arguments. The pyramid bloats the source and the QBE IR by a constant factor; default values let one declaration cover all the omission patterns Delphi callers expect. Restricting the default to literals and named constants keeps the constant-expression evaluator out of the parameter-decl path. An arbitrary constant expression would require running the full constant folder during forward-decl analysis, which currently happens only during statement analysis. Literals and named constants suffice for the FPC RTL, the Blaise RTL, and every port we have surveyed; the restriction can be relaxed later without invalidating earlier code. Restricting to single-name groups keeps the syntax unambiguous — `A, B, C : Integer = 0` could plausibly mean "all three default to zero" or "only `C` has a default", and the compiler refuses to guess. === Forward declarations and overrides A default value declared on a *forward* (unit interface) declaration is transferred by ownership to the matching parameter on the *implementation* declaration during interface↔impl reconciliation (`TransferDefaultValues` in `uSemantic`). This means a unit author writes the default once, on the interface, and the implementation may either repeat it or omit it — both are accepted. Defaults are not inherited by overrides: a descendant's overriding method must repeat any defaults it wants to expose. This matches Delphi semantics and avoids surprising callers of an override that the defaults silently changed underneath them. === `inherited` in expression position `inherited Method(args)` is accepted both as a statement and as an *expression*. The statement form (`inherited Setup();`) calls the parent method for its side effects; the expression form lets an overriding *function* build on its parent's result: [source,pascal] ---- function TDerived.Value: Integer; override; begin Result := inherited Value() + 100; // call parent, then extend end; ---- Both forms lower to the same thing — a *static* (non-virtual) call to the parent's method slot, bypassing vtable dispatch (that is the whole point of `inherited`: reach the specific parent implementation, not re-dispatch to `Self`). The only difference is what happens to the result: the statement form stores it into the current method's `Result` slot (so a bare `inherited F;` still sets `Result`), while the expression form yields the value to the surrounding expression. The expression form was added because the statement-only form forced an awkward two-step (`inherited F; X := Result + 1;`) and could not be used inside a larger expression at all. AST: `TInheritedCallExpr` (the sibling of `TInheritedCallStmt`); both resolve and lower through the same parent-method lookup and the same static-dispatch call sequence (`EmitInheritedCallSeq` on native, `EmitInheritedCallExpr` on QBE). === Overload resolution and tie-break The arity filter in `ResolveStandaloneOverload` and `ResolveMethodOverload` accepts a candidate when `MinArity(Cand) <= ArgCount <= ParamCount(Cand)`, where `MinArity` is the count of leading parameters before the first one carrying a default. Where two candidates score equally on type compatibility, the one needing fewer defaulted slots wins: [source] ---- CompositeScore = (TypeScore * 16) - (ParamCount - ArgCount) ---- So given: [source,delphi] ---- function F(A: Integer; B: Integer = 0): Integer; overload; function F(A: Integer): Integer; overload; ---- the call `F(42)` resolves to the second overload (no defaulting needed), not the first — even though both type-score equally. === Alternatives rejected * *No default values; force explicit overload pyramids.* Every port that wants `AddTest('name', @proc)` short-form would have to either add an explicit two-arg overload or rewrite the call site. For a feature whose semantic and codegen impact is small (literal clone + arity-range filter + tie-break), the porting cost outweighed the saving in compiler complexity. * *Default values as arbitrary constant expressions.* Requires hoisting the constant folder into forward-decl analysis. Deferred until a real use case appears. * *Defaults stored as a token sequence and re-parsed at each call site.* Would avoid the AST-clone step. Rejected because it fragments the semantic model — every other AST node is fully parsed before semantic analysis begins. --- == Metaclass References === Decision A bare class type identifier (e.g. `EError`) used in a value position is a *metaclass reference*: a compile-time constant of type `Pointer` whose value is the class's typeinfo address. This matches the value that `vtable[0]` holds for any instance of that class, so the identity comparison `Obj.ClassType = EError` is true exactly when `Obj` is an instance of (the same exact) `EError`. The semantic analyser sets `IsMetaclassRef := True` on the `TIdentExpr`, retypes the expression to `FTable.TypePointer`, and codegen emits `$typeinfo_` as an `l copy` immediate. === Rationale A class identifier in value position is the canonical Pascal/Delphi spelling for the runtime class object: `class of TObject` (i.e. `TClass`) values, `Obj is EError` checks, exception filters in `try ... except on E: EError do`, and the `AClass: TClass` parameter pattern all expect to receive the typeinfo pointer. Without the metaclass ref, every such site has to be rewritten as `EError.ClassType` (which is a method call requiring an instance) or otherwise hand-rolled — none of which port cleanly from existing FPC/Delphi sources. The result type is `Pointer` rather than a dedicated `TClass`/`tyMetaclass` kind because most receiving signatures already use `Pointer` (the `AClass: Pointer` parameter of punit's `ExpectException`, the `TClass(EError)` cast result, etc.). A dedicated `tyMetaclass` kind would be more precise and would enable RTTI dispatch on the metaclass itself, but is left for a future iteration. === Examples [source,delphi] ---- ExpectException('msg', EError); // metaclass ref → Pointer P := Pointer(EFail); // explicit cast — same value if A.ClassType = EError then … // identity match ---- === Alternatives rejected * *Require explicit `EError.ClassType` everywhere.* Loses Delphi source-level compatibility; every port has to be rewritten. * *Treat the bare identifier as type `tyClass` and rely on `tyClass`– `tyPointer` compatibility at call sites.* Rejected because codegen would still try to load it through a non-existent variable slot. The semantic mark-up has to drive the codegen path explicitly. * *Introduce a dedicated `tyMetaclass` kind right now.* Defers the feature until the symbol-table descriptor work lands and is not necessary for the present porting needs. --- == `ClassCreate` — Runtime Construction from a Metaclass Value === Decision The compiler exposes a built-in function `ClassCreate(Cls, ...args)` that allocates an instance of the class denoted by `Cls` (a value of type `class of T`) and dispatches the constructor body via vtable, so the most-derived override runs. [source,pascal] ---- var Cls: class of TTestCase; Inst: TTestCase; begin Cls := TFooTests; // metaclass = typeinfo pointer Inst := ClassCreate(Cls, 'TestSomething'); // vtable dispatch → TFooTests.Create end; ---- The first argument is required and must be of type `tyMetaClass`. Any remaining arguments are passed to the constructor on `Cls.BaseClass`. The result type is `Cls.BaseClass`, so the call expression assigns cleanly to a variable of the base class type. `ClassCreate` is the lower-level primitive; for most code the `MetaclassVar.Create(args)` syntax (see next section) is preferred. === Rationale The xUnit pattern needs to instantiate one test fixture per published test method, but the fixture class is only known through a `class of TTestCase` registered earlier. Lowering this to the standard `TFoo.Create(args)` codegen path is impossible — that lowering bakes the concrete TotalSize, vtable label, and field-cleanup label into the emitted IR, all of which must come from the runtime class value. The split design — a generic RTL helper `_ClassCreate(TInfo)` that allocates and installs the vtable, plus a vtable-indirect call to the constructor body — dispatches to the most-derived constructor without the caller knowing the concrete class. The helper reads the three additional typeinfo slots (totalsize, fieldcleanup, vtable) added in Step 11e for exactly this purpose. The constructor lookup is by name (`Create`). Overload resolution on the constructor signature is not yet implemented; in practice the only two shapes used are `TObject.Create` (zero args) and `TTestCase.Create (AName: string)`, neither of which collide. When a richer constructor mix appears, the resolution path can be widened without changing the ClassCreate surface. === Alternatives rejected * *Use a factory function pointer per class.* Forces every test class declaration to add `, @TFoo.Create` (or similar) at the `RegisterTest` call site, polluting the cp.test.* migration mechanically required by Step 11f. The runtime construction path exists either way; this just moves where the boilerplate lives. --- == Implicit-Virtual Constructor Dispatch via Metaclass === Decision When a constructor is called through a metaclass-typed variable (`C.Create(args)` where `C: class of TBase`), the compiler emits runtime vtable dispatch so the most-derived constructor body runs. Direct calls (`TFoo.Create(args)` where `TFoo` is a concrete type name) remain fully static. Constructors are auto-slotted into the vtable by the semantic pass. A constructor name that matches an ancestor's constructor overrides the ancestor's vtable slot; new constructor names get a fresh slot. No `virtual` or `override` keywords are required on constructors — the dispatch is implicit and transparent. [source,pascal] ---- type TAnimal = class(TObject) constructor Create; end; TDog = class(TAnimal) constructor Create; // implicitly overrides TAnimal.Create's vtable slot end; var C: class of TAnimal; begin C := TDog; C.Create(); // dispatches to TDog.Create via vtable TAnimal.Create(); // static call to TAnimal.Create (no dispatch) end; ---- The lowering is: 1. `_ClassCreate(C)` — reads totalsize, fieldcleanup, and vtable from the typeinfo pointer `C`; allocates and zeroes the instance; installs the vtable; calls `_ClassAddRef`. 2. Load the vtable pointer from the new instance (`[instance+0]`), index into the constructor's slot, and call indirectly. Both the `C.Create(args)` syntax and `ClassCreate(C, args)` use this path. === Rationale The xUnit test runner instantiates fixtures through `class of TTestCase` and must run the derived class's constructor, not the base. Without virtual dispatch, `ClassCreate` called the base constructor statically — a derived class with initialisation in its own `Create` would silently skip it. Making constructors implicitly virtual only through metaclass-typed receivers preserves the zero-overhead guarantee for direct `TFoo.Create` calls (the overwhelming majority) while giving metaclass dispatch the polymorphic behaviour users expect. Requiring explicit `virtual`/`override` on constructors was considered and rejected: it would force boilerplate on every class in the hierarchy, and the mental model of "metaclass dispatch = polymorphic" is intuitive without it. The vtable slot is assigned by constructor name (same name = same slot, new name = new slot), matching the existing virtual-method slot assignment algorithm. === Alternatives rejected * *Require `virtual`/`override` on constructors.* Imposes annotation burden on every class for a behaviour that only manifests through metaclass dispatch. Direct `TFoo.Create` does not benefit from the annotation. The implicit model is simpler and matches Delphi's `virtual constructor` behaviour without the keyword noise. * *Dispatch constructors through a separate RTTI table instead of the vtable.* Adds a parallel lookup mechanism that must be kept in sync. Constructors are regular methods with a `Self` parameter; the vtable is the natural dispatch vector. * *Keep constructors static even through metaclass dispatch.* This was the original `ClassCreate` implementation. It fails the test-runner use case: the derived fixture's constructor never runs, so the test fixture is uninitialised. --- == `case` Statement on String Selector === Decision A `case` statement may use a `string`-typed selector. Each branch label must be a string-typed expression (typically a string literal); the runtime comparison uses `_StringEquals` against the selector value. [source,pascal] ---- case S of 'foo': HandleFoo; 'bar': HandleBar; 'baz': HandleBaz; else HandleOther; end; ---- The semantics are equivalent to a chain of `if S = 'foo' then ... else if S = 'bar' then ...` tests, evaluated in source order. === Rationale The compiler's own source uses small one-letter strings (`'w'`, `'l'`, `'d'`, `'s'`) as QBE type tags and switches on them in several places. Forcing those sites into hand-rolled if/else chains for the sake of self-hosting was rejected as a step backwards — Object Pascal users have always read `case`-on-string as idiomatic, and Delphi/FPC accept it. Implementation cost is small (lower to a chained equality test; no jump tables) and the codegen is local to `EmitCaseStmt`. The selector is evaluated exactly once. Each branch label is evaluated when its turn comes; because string literals are immortal, order has no observable side effect. === Alternatives rejected * *Refactor every internal `case S of` to if/elseif chains.* Hides intent at the call site, repeats the selector expression, and would have to be re-applied every time a new tag-switch site appears. The language feature pays for itself the first time it's reused. * *Restrict labels to string literals only.* Would simplify the semantic check, but ruins compositions like `case Tag of EmptyName, WildcardName: ... end`. Allow any string-typed expression; the runtime comparison handles whatever value flows in. * *Build a hash table for large dispatches.* Premature. All current uses are 2–6 branches, where chained equality is faster than any hash overhead. Revisit if a case ever has dozens of labels. --- == Triple Single-Quote Text Blocks === Decision Blaise supports multi-line string literals delimited by triple single quotes (`+'''...'''+`). The opening `'''` must be followed immediately by a newline; the closing `'''` must appear on its own line (possibly preceded by whitespace). The column position of the closing `'''` sets the indentation baseline: all content lines are de-dented by that many columns. Single quotes inside the block require no escaping. Example: [source,pascal] ---- const Src = ''' program Hello; begin WriteLn('world'); end. '''; ---- The value of `Src` is the four-line program with no leading whitespace, because the closing `'''` is at column 5 (four spaces of indent), and all content lines have at least four leading spaces stripped. === Rationale Pascal is a single-quote language; triple-quoting follows naturally from the existing `''` escape convention. The design mirrors Java 13+ text blocks — one of Java's most well-received modern additions. The primary motivation is the compiler test suite: every `cp.test.*.pas` file constructs multi-line source strings with `'line' + LineEnding + 'line'` concatenation. Text blocks eliminate this concatenation noise, making inline source snippets readable and maintaining them trivial. Margin stripping based on the closing delimiter position keeps text blocks tidy inside indented method bodies without accumulating spurious leading whitespace in the string value. Embedded single quotes (e.g. `'John'`, `#10`) require no escaping, which is essential for Pascal source literals in the test suite. === Disambiguation When the lexer encounters three consecutive single-quote characters, it peeks at the next character: * If the next character is a newline (LF, CR, or end-of-file), it opens a text block. * If the next character is anything else — including another `'`, a letter, a digit, or any printable character — it falls back to the classic string parse. This means `'''text'''` (a string containing `'text'` via the standard `''` escape) continues to work unchanged. This is a one-character lookahead — no backtracking or multi-token lookahead is required. === Alternatives rejected * *Heredoc syntax* (`<<'SENTINEL' ... SENTINEL`) — alien to Pascal; requires sentinel matching rather than simple delimiter detection. * *Backtick raw strings* (`` `...` ``) — cannot embed backtick characters; backtick has no history in Pascal. * *Triple curly braces* (`{{{ ... }}}`) — visually heavy; conflicts with the comment delimiter namespace. * *Keyword heredoc* (`beginstring SENTINEL ... SENTINEL`) — verbose; adds a new keyword. This is a Blaise-only language feature — not FPC/Delphi compatible — and that is intentional: it is the kind of feature that makes Blaise worth using rather than just being "FPC with a different name". --- == `Supports()` — Runtime Interface Membership Test Blaise provides `Supports()` as a compiler intrinsic with two overloaded forms: [source,pascal] ---- function Supports(Obj: TObject; IntfType: ): Boolean; function Supports(Obj: TObject; IntfType: ; out OutVar: IntfType): Boolean; ---- The second argument is a *type name*, not a runtime value. This is why `Supports` is a compiler intrinsic rather than a library function: the parser treats the second (and optional third) argument positionally rather than as an expression. === Decision `Supports()` is a compiler intrinsic parsed specially in `ParseFactor` when the identifier `Supports` is immediately followed by `(`. The second argument is consumed as a bare identifier (interface type name), and the optional third argument is consumed as a bare identifier (output variable name). === Rationale * *No GUIDs* — Blaise does not have GUID-based interface identity (Delphi `IID` dispatch). Interface type identity is determined structurally via the `typeinfo_*` symbols emitted by the compiler. Passing a GUID at runtime would require a separate GUID declaration infrastructure. * *Intrinsic vs. library function* — A library `Supports(Obj, IFoo)` would require `IFoo` to be a runtime value, which it is not; it is a compile-time type name. Making it an intrinsic keeps the call site idiomatic without requiring special "type value" syntax. * *Two forms* — The 2-arg form is a pure Boolean test: it calls the RTL `_ImplementsInterface(obj, typeinfo_IFoo)`. The 3-arg form additionally obtains the correct `itab` via `_GetItab` and writes both the obj and itab pointers into the output variable's fat-pointer slots under full ARC (retain new, release old). * *Delphi compatibility at the call site* — Delphi's `Supports` is a library function in `SysUtils` with GUID overloads. Blaise's form is syntactically identical for the common use-cases (`Supports(X, IFoo)` and `Supports(X, IFoo, Ref)`) even though the underlying mechanism differs. === Alternatives Rejected * *`is` operator extended to assignment* — Considered `if (Obj is IFoo) := Ref then` syntax, rejected as non-standard and confusing. * *Explicit `QueryInterface` method* — Requires the programmer to manage typeinfo pointers manually; the intrinsic is safer and more ergonomic. == Generic Interfaces and the Collections Framework === Decision Blaise supports generic interfaces — interface types with type parameters — using the same `` syntax as generic classes: [source,pascal] ---- type IMap = interface procedure Add(Key: K; Value: V); function TryGetValue(Key: K; var Value: V): Boolean; function ContainsKey(Key: K): Boolean; procedure Remove(Key: K); function GetCount: Integer; end; ---- A generic class implements a generic interface by listing the instantiated interface name in its parent clause: [source,pascal] ---- TDictionary = class(IMap) ... end; ---- The RTL (`rtl/src/main/pascal/generics.collections.pas`) provides the canonical collection types. New code — tests, compiler passes, and RTL units — must use these types rather than writing inline collection logic: [cols="1,2", options="header"] |=== | Type | Purpose | `TList` | Ordered, growable list | `TStack` | LIFO stack | `TQueue` | FIFO queue | `TSet` | Unordered membership set | `TDictionary` | Key→value map (unordered) | `TOrderedDictionary` | Key→value map (insertion-ordered) | `IMap` | Map interface abstracting both dictionary types |=== === Rationale * *Interface abstraction over concrete types* — `IMap` lets callers accept either `TDictionary` or `TOrderedDictionary` without caring which implementation is behind the variable. This is the primary use-case for interfaces in a generics-era codebase. * *No `var` parameter type inference gap* — Interface method signatures carry var-param flags in `TInterfaceTypeDesc.FParamIsVar` (a per-method comma-separated `0`/`1` string built during semantic analysis). The codegen uses these flags to pass var params as addresses (`l`) rather than values (`w`) at interface dispatch sites, where no concrete `TMethodDecl` is available. * *Reuse over reinvention* — Inline collection logic written for one test or compiler pass is never tested beyond that narrow context. Routing all collection use through the RTL types means every new feature exercises the same code paths and bugs surface early. * *Structural interface identity* — As with plain interfaces, generic interface identity is based on the `typeinfo_*` symbol emitted at instantiation time, not on GUIDs. `IMap` and `IMap` are distinct types with distinct typeinfo pointers. === Implementation notes When the semantic analyser instantiates a generic class that lists a generic interface in its parent clause (e.g. `TDictionary = class(IMap)`), `InstantiateGeneric` detects that the substituted `ParentName` resolves to `tyInterface`, moves it to `ImplementsNames`, and calls `AddImplements` on the concrete `TRecordTypeDesc`. This wires the type-compatibility check that allows `M: IMap := TDictionary.Create`. The codegen emits itab and impllist data for generic class instances in `EmitInterfaceDefs`, parallel to the existing path for non-generic classes. Both class and interface names in itab symbols are passed through `QBEMangle` to produce valid QBE identifiers (e.g. `$itab_TDictionary_Integer_Integer_IMap_Integer_Integer`). A non-generic class may also *inherit* from a generic-class instance — e.g. `TIntBox = class(TBox)`. The first heritage entry is instantiated and then classified: a `tyInterface` result becomes an implements entry (above), any other (a class instance) is the parent class. Because the generic instance is emitted under its `QBEMangle`'d symbol (`typeinfo_TBox_Integer`), the inheriting class's typeinfo must reference the parent through `QBEMangle` too, not the unmangled name — otherwise the emitted QBE identifier contains the illegal `<`/`>` characters. === Alternatives Rejected * *Structural duck-typing without interfaces* — Blaise already uses structural protocols for `for..in` (GetEnumerator/MoveNext/Current). Extending that pattern to maps would mean callers cannot name the abstraction or store it in a typed variable. An explicit interface is clearer and more composable. * *Non-generic `IMap` with `TObject` keys and values* — Pre-generics workaround; loses type safety and requires casts at every use site. == Dynamic Arrays (`array of T`) === Decision Blaise supports heap-allocated, reference-counted dynamic arrays using the Delphi/FPC syntax `array of T`. A dynamic array variable holds a pointer to element 0; the two 4-byte integers immediately before element 0 store the reference count and the current length (the "header"). Memory layout (contiguous block):: `[refcount:4][length:4][element 0][element 1]…[element N-1]` A nil pointer (zero value) represents an unassigned array. The variable slot is 8 bytes on all platforms (a pointer). Supported built-ins:: * `SetLength(A, N)` — resize the array to `N` elements; copies existing data. * `Length(A)` — returns the current element count. * `A[I]` — zero-based element read/write. === Supported element types All scalar element types are fully supported: [cols="1,1,1"] |=== |Type | RawSize | QBE store/load |`Byte`, `Boolean` |1 byte |`storeb` / `loadub` |`Integer`, `UInt32`, `enum` |4 bytes |`storew` / `loadw` |`Int64`, `string`, `class`, `pointer` |8 bytes |`storel` / `loadl` |=== Record element types (`array of TMyRecord`) are structurally supported at the offset-computation level (element size = `TotalSize`), but element assignment currently emits a single `storel` rather than a `memcpy`, so only pointer-sized records are safe. Full record-element support requires a `memcpy` emit path and is deferred. === RTL contract Two RTL functions implement resize and length queries: `_DynArraySetLength(Ptr, NewLen, ElemSize)`:: Allocates `DA_HDR + NewLen × ElemSize` bytes, writes refcount=1 and the new length into the header, zeroes the element area, copies `min(OldLen, NewLen)` existing elements, frees the old block, and returns a pointer to element 0. `_DynArrayLength(Ptr)`:: Reads the 4-byte integer at `Ptr − 4`. Returns 0 if `Ptr` is nil. Both functions are implemented in pure Pascal in `blaise_str.pas` and compiled into the RTL archive. === Rationale *Heap allocation, not stack*: dynamic arrays grow at runtime, so stack allocation is not viable. The pointer-to-element-0 convention matches Delphi and makes zero-based indexing natural: `A[I] = *(Ptr + I × ElemSize)`. *Header before element 0*: placing the header immediately before the data avoids a separate allocation and keeps cache locality for length checks (`Ptr − 4`). Delphi uses the same layout. *`SetLength` allocates fresh on every call*: reference counting on arrays is deferred to Phase 3 (ARC). The current implementation always allocates a new block and copies, which is correct and safe, though not optimal for in-place growth. === Grammar ---- DynArrayType = ARRAY OF TypeName ; ---- This rule extends `TypeName`. After consuming the `array` keyword, the parser checks for a `[` bracket. If present it follows the static-array path (`ArrayType`); if absent it expects `of` and follows the dynamic-array path (`DynArrayType`). === Alternatives rejected *Open-array parameters as the general dynamic-array type* — open-array parameters use a two-register ABI (data pointer + high index) and cannot be stored in a variable. Dynamic arrays require a single-pointer variable form that can be resized independently. *`TList` instead of native dynamic arrays* — `TList` already exists for ordered, resizable sequences. Native `array of T` is needed for ABI compatibility (passing slices to C, inter-op with RTL functions, future vectorised operations) and for conciseness in low-level code. == Stream I/O === Decision The Blaise RTL provides a stream subsystem in the `Streams` unit modelled on Go's `io` interfaces and Square's Okio rather than on Delphi/FPC's single-class `TStream` hierarchy. The shape is: * Two abstract base classes, *one direction each*: `TInputStream` and `TOutputStream`. Bidirectional read+write on a single handle is rare enough (database engines, file-format backpatching) that it does not justify polluting the common API; a future `TRandomAccessFile` can cover it. * Three capability interfaces alongside the abstract classes: `ICloseable` (every stream supports it), `IInputStream`, `IOutputStream`. Concrete classes implement both the abstract base *and* the matching capability interface. Two capabilities orthogonal to read/write — `ISeekable` (file/memory streams), `IReaderFrom` / `IWriterTo` (optional zero-copy markers consumed by `CopyStream`) — round out the surface. * The wrappers are decorators: `TBufferedInputStream` wraps any `TInputStream`; `TStreamReader` wraps that for line-oriented reads; similarly on the write side. Each wrapper owns its inner stream by default (`OwnsInner: Boolean = True`); closing the outer wrapper tears the chain down. * `TBuffer` is an in-memory segmented buffer (Okio-style segment rope, 8 KiB segments from a shared pool) that implements *both* `IInputStream` and `IOutputStream`. Its `TransferTo` operation re-links segments between buffers without copying bytes, so multi-layer pipelines (decode → buffer → forward) drop from O(N) memcpys per byte to O(1). * `CopyStream(Src, Dst): Int64` is a free function that runtime-checks for `IReaderFrom` / `IWriterTo` markers and dispatches to the optimised path, with a byte-loop fallback. Adding a fast-path implementation (e.g. `sendfile(2)` between two `TFileOutputStream`s) later is a non-breaking change — existing callers automatically benefit. === Rationale The design draws explicitly from a cross-language survey (Java, Go, Rust, Swift, Kotlin/Okio, C#, Oxygene, Python). The recurring lessons: * *Java's `java.io` is the textbook bad case of GoF Decorator* — 70+ classes, two parallel byte/char hierarchies, wrapping order silently load-bearing (forget the buffer → 100× slower). Decorator works *if the decorated interface is tiny* (Go's `io.Reader` has one method; Okio's `Source` has three). Blaise's abstract bases follow that constraint: `TInputStream` has `Read` and `Close`, and that is the whole protocol. * *Go's `io.Copy` capability discovery is near-free to provide and hugely valuable.* Callers write `CopyStream(src, dst)` once; the framework upgrades to zero-copy when both sides cooperate. The Go team consider this the single most impactful design choice in the package. * *Okio's segment-rope buffer is the modern answer to multi-layer buffering churn.* Naive layers each `memcpy` between their own byte arrays; segments are reassigned by pointer, turning the dominant cost into a no-op. We pay for it with ~300 LOC of pool/segment bookkeeping, which is reasonable. * *.NET's `StreamReader`-owns-the-inner-stream behaviour is a surprising footgun.* Blaise wrappers are explicit about ownership (`OwnsInner` parameter), defaulting to True for ergonomics but opting out cleanly when a helper returns a wrapper. * *Rust's `BufWriter` ignoring drop errors is the cautionary tale on finaliser-based cleanup.* Blaise's ARC `Destroy` is best-effort: it flushes but absorbs errors silently. Callers who care about correctness call `Close` explicitly inside a try-finally, which raises `EStreamError` on failure. * *Python's three-layer `io` (raw / buffered / text) is the right shape for composition* — short reads are a layer-1 concern, charset is a layer-3 concern, mixing them (as `java.io` did) confuses the semantics. Blaise mirrors this: byte streams (Layer 1+2), buffered decorators (Layer 2/3), text wrappers (Layer 4), segmented buffer (Layer 5), copy framework (Layer 6). === Why capability interfaces alongside abstract classes A defining moment in the design was recognising that `TBuffer` is both a source and a sink. With Blaise's single inheritance, it cannot extend both `TInputStream` and `TOutputStream`. Two solutions exist: * *Pure interfaces, no abstract classes* — would force every concrete stream to re-implement common machinery (close idempotency, default flush, etc.). * *Capability interfaces alongside abstract classes* — abstract classes serve implementers (default code, less boilerplate); the interfaces serve consumers (accept "anything readable" without caring about the inheritance chain). `TBuffer` implements both interfaces directly without picking a parent. Blaise chose the latter, matching the existing `IMap` + `TDictionary` pattern in the collections. === UTF-8 only in v1 `TStreamReader` and `TStreamWriter` are UTF-8 only. Strings in Blaise are already UTF-8, so the wrappers are effectively pass-through. The constructors accept an `Encoding` parameter for forward compatibility (reserved); other encodings are deferred until there is a real consumer. This avoids Java's parallel byte/char hierarchy mistake — charset is a parameter, not a type-system axis. === Alternatives Rejected * *Delphi-style single `TStream` root* — rejected. Accumulates too many methods (`Read`/`Write`/`Seek`/`Size`/`Position`), forces every implementer to override slots that make no sense for their stream (pipes have no size; sockets cannot seek). Splits cleanly along read/write *and* seekability axes, which is what the abstract bases + capability interfaces achieve. * *`TFilterStream` decorator root* — rejected. Decoration is structural (hold an `FInner`), not nominal. A separate class adds no shared behaviour and forces an inheritance commitment that excludes other useful parents. * *Stream-of-bytes only* — rejected for v1. `TBuffer.IndexOf` and the future `TBuffer.TransferTo` cover the framed-I/O use case (parsers, protocol handling) that pure byte streams handle awkwardly. The upgrade path to a full `TPipeReader` (System.IO.Pipelines style) remains open. * *Async-ready interfaces from day one* — deferred. Blaise has no async runtime today. The byte interface is shaped so an async variant could be added later without a parallel hierarchy (Rust's `std::io` vs `tokio::io` split is the cautionary tale). === TODOs flagged in code * `TBuffer`'s segment pool is single-threaded. The comment on `AcquireSegment` / `ReleaseSegment` flags the audit needed once Blaise gains thread support — likely a per-thread freelist or a mutex. * `CopyStream`'s fast paths (`IReaderFrom` on `TFileOutputStream` via `sendfile(2)`; `IWriterTo` on `TBuffer` via `TransferTo`) are intentionally not implemented in v1. Adding them is non-breaking; benchmark first. * Interface argument expressions other than plain identifiers (function returns, casts, field accesses) are not yet supported as parameters by the codegen. Assign to a local interface variable first. == Open Language Questions These items are under active consideration but not yet decided. === Integer Width *Resolved:* `Integer` is 32-bit on all platforms, matching FPC. `Int64` is the explicit 64-bit type. See the _Integer Types_ section in the Type System chapter for the full decision, rationale, and alternatives rejected. === Enumeration Underlying Type *Resolved:* Enumeration values are stored as QBE `w` (32-bit word) on all platforms, matching the current Blaise `Integer` type. FPC's smallest-type-that-fits approach is not followed: the compiler always uses `w` for simplicity and ABI predictability. Set types also use `w` for sets with ≤ 32 members and `l` for 33–64 members. === `Result` vs Return Variable Blaise currently follows FPC's `Result` convention inside functions. No change is planned, but this should be formally documented. ==== `Exit(Value)` function-result shorthand Inside a function, `Exit(X)` is shorthand for `Result := X; Exit;` — it assigns `X` to `Result` and returns immediately. This matches Delphi/FPC and makes early returns with a value concise: [source,pascal] ---- function Classify(N: Integer): Integer; begin if N < 0 then Exit(-1); if N = 0 then Exit(0); Result := 1 end; ---- `Exit(X)` is only valid inside a function (a procedure has no `Result`); using it in a procedure is a semantic error. `X` must be assignment-compatible with the return type — the same check (and the same widening / ARC handling for string and class returns) as a direct `Result := X`. Implementation: the parser attaches the parsed `X` to the `TExitStmt`; the semantic pass rewrites it into a synthesised `Result := X` assignment (so it reuses the full assignment-analysis path) which codegen emits immediately before the normal exit jump. A bare `Exit` is unchanged. == Typed Constants === Decision Blaise supports typed constant declarations with the form: [source,pascal] ---- const Pi: Double = 3.14159265358979323846; MaxItems: Integer = 100; Greeting: string = 'hello'; ---- The type annotation is optional; untyped constants (`const N = 42`) continue to work unchanged with inferred types. === Supported types Scalar typed constants: Integer, Int64, Double, Single, Boolean, string. Array typed constants: `array[EnumType] of ScalarType` — see _Array-of-enum typed constants_ below. Record, class, and pointer typed constants are not supported — they require compound value syntax (record literals, address-of) that adds significant parser complexity for rare use cases. === Rationale Typed constants give the programmer explicit control over the type of a constant value. Without them, `const Pi = 3.14159` infers `Double`, which is correct, but `const Scale = 2` infers `Integer` — there is no way to declare a `Single` or `Int64` constant without a typed form. The primary motivator was `Pi: Double` in `math.pas`, where the untyped form would have ambiguous type inference across platforms and makes the intent opaque. === Alternatives Rejected *Using type-cast syntax* (`const Pi = Double(3.14159)`) — requires the parser to distinguish a type-cast expression from a function call inside a const initialiser, which is more complex than a simple colon annotation and is not the FPC/Delphi convention. == Constant Expressions === Decision A constant declaration may take a compile-time *integer expression* on the right-hand side, not just a single literal: [source] ---- const A = 2 * 3; // 6 B = 2 + 3 * 4; // 14 — '*' binds tighter than '+' C = (2 + 3) * 4; // 20 — parentheses override precedence D = 100 div 7; // 14 Base = 10; Derived = Base * 2 + 1; // 21 — references a prior constant Flags = (1 shl 4) or 3; // 19 — arithmetic and bitwise mixed ---- Supported operators are `+ - * div mod` and the bitwise/shift set `and or xor shl shr`, together with unary minus and parentheses, using the same precedence as the executable-expression grammar. Operands are integer literals and references to previously declared integer constants. === Rationale The right-hand side is parsed with the existing full-precedence expression parser into a normal expression AST, then folded to a single `Int64` by the semantic pass. Folding in the semantic pass (rather than at parse time) is what makes forward references to other constants work: every constant in scope is registered before any expression is folded, so `Derived = Base * 2 + 1` resolves `Base` cleanly regardless of declaration order within the block. Reusing the executable-expression parser means operator precedence, associativity, and parenthesised grouping are encoded in the AST shape for free — there is no separate, precedence-unaware constant evaluator to keep in sync. This supersedes the earlier flat bit-op token-chain representation, which only handled a single left-to-right precedence level and could not express `2 + 3 * 4` correctly. === Scope Constant expressions fold to an integer. Floating-point constant arithmetic (`const X = 3.14 * 2`) and string concatenation beyond the existing literal/ named-constant `+` form are not folded by this path; a multi-token float or string expression is left for a future extension. Compile-time constants are not yet accepted as static-array bounds (`array[0..N-1]`), which is a separate parser limitation in the type grammar, independent of constant-expression folding. == Array-of-enum Typed Constants === Decision Blaise supports array-typed constants whose index is an enum type: [source,pascal] ---- type TWeather = (wtSunny, wtCloudy, wtRainy); const WeatherNames: array[TWeather] of string = ('Sunny', 'Cloudy', 'Rainy'); WeatherCost: array[TWeather] of Integer = (0, 1, 2); ---- The element count in the value list must exactly match the number of enum members; a mismatch is a semantic error. The index type must be an enum declared in the same or an enclosing scope. Supported element types: the same scalar types supported by scalar typed constants (Integer, Int64, Double, Single, Boolean, string). === Implementation Array-typed constants are emitted as pre-initialised global data objects in the QBE data section. String elements are stored as immortal string pointer references (`$__sN + 12`); integer elements as `w`-typed words. The index type must be resolved before the const can be validated, so array const analysis is deferred to a second pass (`AnalyseArrayConstDecls`) that runs after `AnalyseTypeDecls` in every scope. Scalar const analysis remains in the first pass (before type decls) so that constant values remain usable inside type declarations. === Rationale The canonical Pascal idiom for mapping enum values to strings is a `case` statement helper function, which is verbose and must be kept in sync with the enum manually. An array-of-enum constant is more concise, checked at compile time (wrong element count is an error), and matches FPC/Delphi syntax exactly. This was the primary motivator: making the following program work as written, without any workaround: [source,pascal] ---- const WetherNames: array[TWetherType] of string = ('Sunny', 'Cloudy', 'Rainy'); var t1: TWetherType; begin t1 := wtSunny; WriteLn('Wether: ', WetherNames[t1]); end. ---- === Index forms: enum, integer range, and multi-dimensional Both enum and integer-range indices are supported for const arrays: [source] ---- const Days: array[0..6] of string = ('Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'); const Wether: array[TWetherType] of string = ('Sunny', 'Cloudy', 'Rainy'); ---- Range-indexed const arrays extend to multiple dimensions, in either the comma form or the equivalent nested form: [source] ---- const M: array[0..1, 0..2] of Integer = ((1, 2, 3), (4, 5, 6)); const N: array[0..1] of array[0..1] of Integer = ((10, 20), (30, 40)); ---- A multi-dimensional const desugars to a nested static-array type (the comma and nested forms are interchangeable) and its initialiser values are stored flattened in row-major order — the same contiguous layout a `var`-declared multi-dimensional array uses. The compiler validates that the number of initialiser leaves equals the product of the dimension extents. Enum indexing is intentionally limited to a single dimension: it ties the element count to a type whose cardinality the compiler already knows, and mixing enum and range dimensions buys little over the uniform range form. == Record Methods Blaise records may declare methods alongside fields: [source,pascal] ---- type TPoint = record X, Y: Integer; function Distance(Other: TPoint): Integer; function ToString: string; end; function TPoint.Distance(Other: TPoint): Integer; begin Result := Abs(Self.X - Other.X) + Abs(Self.Y - Other.Y) end; ---- === Calling Convention Record methods receive `Self` by pointer (functionally a `var` parameter at the QBE ABI level). The language preserves value semantics: assignments copy the record content, parameters pass by value unless `var` is specified. The pointer-pass for `Self` is an implementation detail — within a method body `Self.Field := V` mutates the caller's record, the same as in any imperative record method. === Restrictions Records cannot have constructors, destructors, or virtual methods. There is no inheritance: a record `type` declaration cannot list a parent. Records do not participate in the vtable / RTTI system; they have no class identity. === Why no constructors Pascal records have always been initialisable by struct-style assignment (`P.X := 1; P.Y := 2`) or by `MakePoint(1, 2)`-style factory functions. Adding constructor syntax would either require introducing a *new* two-keyword construction form (Delphi's `record constructor`) or overloading the existing `Create` method name with new semantics — both add complexity without enabling anything that factory functions don't already cover. The five-type date/time RTL (`DateUtils`) settled the question in practice: the `Make*` factory pattern reads cleanly, threads through `with` blocks, and keeps record literal construction (the common case) free of method-call overhead. === Alternatives Rejected *Record inheritance* (Delphi extended records with parent) — would require either a vtable per record (defeating the cost model — records are meant to be value types, fast to copy, no indirection) or a single-inheritance chain without polymorphism (no semantic advantage over composition). Classes already cover the cases where inheritance is wanted. *Virtual methods on records* — same as above: needs a vtable, conflicts with copy-by-value, and the design pressure for it (polymorphism) is satisfied by interfaces over classes. == Date and Time RTL — Five-Type Model The `DateUtils` unit replaces FPC/Delphi's `TDateTime`-as-`Double` with five distinct record types: `TDate`, `TTime`, `TDateTime`, `TInstant`, and `TDuration`, plus a `TTimeZoneOffset` helper. === Why not `TDateTime` as Double The Delphi/FPC convention encodes `TDateTime` as a `Double`: integer part is days since 1899-12-30, fractional part is the time of day. This has three well-known problems: * *Precision loss.* A `Double` has 53 bits of mantissa. Operations on dates more than a few centuries from the epoch lose milliseconds and eventually whole seconds. Arithmetic round-trips do not preserve the original value. * *Type confusion.* The same type is used for a calendar date, a local wall-clock instant, and a UTC point-in-time. There is no way for the type system to flag a function that adds a local-time value to a UTC instant — both compile to `Double + Double`. * *No timezone metadata.* The bare `Double` carries no information about whether the encoded value is local time, UTC, or naive. Reasoning about DST transitions or cross-timezone arithmetic requires out-of-band knowledge. The five-type model addresses all three: every value uses integer storage (no precision drift), the types name their semantic role, and timezone is explicit where it matters. === Type semantics `TDate` — calendar date (Year, Month, Day) without time or timezone. `TTime` — time of day (Hour, Minute, Second, Nanosecond) without date or timezone. `TDateTime` — local date+time without timezone. Use when wall-clock fields are what matter and the timezone is implied or irrelevant. `TInstant` — unique point on the UTC timeline, stored as nanoseconds since the Unix epoch (signed `Int64`). The right type for "when did this happen" across timezones, log timestamps, scheduling. `TDuration` — signed nanosecond interval (`Int64`). Closed under addition and negation; not anchored to any point in time. === Timezone scope `TTimeZoneOffset` represents a fixed UTC offset (`+HH:MM` or `-HH:MM`). Full IANA tzdata / DST rules are deferred to a future phase — they require a runtime tzdata reader and DST-aware arithmetic that would significantly expand the unit's scope. The fixed-offset model covers ISO 8601 timestamp formatting and a substantial fraction of real-world timezone needs. === Why integer storage everywhere `TInstant` and `TDuration` use signed `Int64` nanoseconds — large enough to cover ±292 years from epoch with nanosecond precision. `TDate` / `TTime` / `TDateTime` use `Integer` per sub-field. `Byte`-sized fields now pack at their natural 1-byte stride, but the existing public API is locked in on `Integer` for the per-field accessors, so changing the storage type would break call sites for no observable benefit. === Alternatives Rejected *Single 128-bit instant type* — covers nanosecond precision over the full ICU calendar range, but requires emulated 128-bit arithmetic for every operation. `Int64` nanoseconds with a ±292-year range is sufficient for all practical engineering uses. *ISO 8601 string as the canonical type* — readable but slow and allocation-heavy. The wire format is ISO 8601 (`ToString` methods produce it); the in-memory representation is structured. == `sar` — Arithmetic Right Shift Operator === Decision Blaise distinguishes logical and arithmetic right shift with two separate keywords: * `shr` — logical (zero-fill) right shift. The vacated high bits are filled with zero regardless of the operand's signedness. This matches the documented semantics in Delphi and Free Pascal. * `sar` — arithmetic (sign-preserving) right shift. The vacated high bits are filled with a copy of the original sign bit. On unsigned types `sar` produces the same result as `shr`; on signed types with the sign bit set, `sar` preserves the sign while `shr` discards it. The compiler maps `shr` to QBE's `shr` instruction and `sar` to QBE's `sar` instruction. Both are accepted at the same precedence level as `*`, `/`, `div`, `mod`, `and`, `xor`, and `shl` (term-level). === Rationale A signed right shift by one is the natural way to divide by two with sign-preservation: `-16 sar 1 = -8`. Using `shr` for the same operation on a signed value produces a large positive number because the sign bit is treated as a data bit and shifted right. Both operations are useful — arithmetic shift for sign-preserving division and bit-field extraction on signed values; logical shift for unsigned bit manipulation and packed-field access — and conflating them under a single keyword forces the programmer to cast through an unsigned type whenever the other interpretation is required. Delphi and Free Pascal define `shr` as logical regardless of the operand type and expose no separate arithmetic-shift operator. C and C++ leave the choice implementation-defined. Java and Rust expose both as distinct operators (`>>` arithmetic vs `>>>` logical in Java; `>>` follows operand signedness in Rust). Blaise follows the two-operator model so the source code names the intent explicitly. === Alternatives Rejected *Make `shr` sign-preserving on signed types* — diverges from documented Delphi/FPC semantics, silently changes the meaning of existing source ported from those compilers, and removes the ability to perform a logical shift on a signed value without casting. *Reject `shr` on signed types* — forces an explicit cast through `UInt64` (or another unsigned type) for every logical shift on a signed value. Verbose and noisy in bit-manipulation code. *Use a symbolic operator (e.g. `>>` and `>>>`)* — symbols are concise but read less naturally next to Pascal's other keyword operators (`shl`, `shr`, `and`, `or`). Three-letter mnemonic keywords match the established Pascal style. == `/` vs `div` — Real Division and Integer Division === Decision Blaise distinguishes real division from integer division with two separate operators: * `/` — real division. Always yields a floating-point result, even when both operands are integers. Integer operands are promoted to `Double` before the division. The result type is `Single` only when both operands are `Single`; in every other case the result is `Double`. * `div` — integer division. Both operands must be integer types; passing a `Single` or `Double` operand is a semantic error. This matches standard Pascal, Delphi, and Free Pascal. === Rationale Conflating `/` and `div` produces silently-wrong code in numerical expressions. A user writing `Round(Y / X)` where `X` and `Y` are `Integer` reasonably expects the same result as `Round(Y / X)` written with floating-point operands — i.e. the rational quotient rounded to the nearest integer. Treating `/` as integer division when both operands are integers makes `Round(10 / 3)` evaluate as `Round(3) = 3` rather than `Round(3.333…) = 3`; the example happens to coincide, but `Round(7 / 2)` produces `3` instead of `4`. The operator's name also becomes misleading: `/` reads as the mathematical division symbol. Keeping `/` and `div` as distinct operators makes the programmer's intent explicit and matches every other Pascal dialect. `Trunc`, `Round`, `Ceil`, and `Floor` continue to require a float argument; the ergonomic case for integer-quotient rounding is now `Round(Y / X)`, which naturally has a float subexpression and therefore type-checks. === Implementation `uParser.pas` maps the `/` token to `boSlash` and the `div` keyword to `boDiv`. `uSemantic.pas` (`AnalyseBinaryExpr`) gives `boSlash` a float result type unconditionally; `boDiv` retains the existing integer-promotion rules and now rejects float operands explicitly. `blaise.codegen.qbe.pas` lowers both `boSlash` and `boDiv` to QBE's `div` instruction; the difference is whether the operands have already been promoted to a float type (`d`/`s`) on the float arithmetic path. === Alternatives Rejected *Map both operators to a single `boDiv` and infer the kind from operand types* — this is what the compiler did before this change. The inference path that says "two integer operands → integer division" is exactly the bug: Pascal source distinguishes the two operators deliberately, and the AST should preserve that distinction. *Make `Trunc`/`Round` accept Integer arguments* — this would mask the underlying bug rather than fix it. `Round(10 / 3)` would still evaluate `10 / 3` as integer division to `3` before passing it to `Round`, so the result would silently be `3` regardless of whether the rational answer rounds up or down. == `High` and `Low` on Ordinal Types === Decision `High(T)` and `Low(T)` accept any ordinal type — `Integer`, `Int64`, `UInt32`, `UInt64`, `SmallInt`, `Word`, `Byte`, `Boolean`, and any user-declared `enum` — in addition to the array and string forms documented above. The argument may be either a type name (e.g. `High(Integer)`) or a value of that type (e.g. `High(SomeVar)`). The result type is the argument's own type, not `Integer`, so `High(Int64)` has type `Int64` and round-trips through 64-bit code paths without truncation. Floating-point types are explicitly rejected with a targeted error message that points the user at `MaxDouble`/`MinDouble` and the `Math.Infinity` constants. === Rationale The idiom `for I := Low(T) to High(T)` is one of the few places in Pascal where the bounds of a primitive type are expressed without a magic number. Restricting `High`/`Low` to arrays and strings forced users to write `2147483647` or `MaxInt` in scan loops and saturation guards, which is both noisier and easier to get wrong on the `Int64`/`UInt64` end of the range. The result type matches the argument deliberately. Returning `Integer` for every form would make `High(Int64)` lossy and would also break overload resolution against `Int64`-typed call sites. Returning the argument type composes naturally with assignment and comparison. The bounds are folded at compile time — `High(Integer)` emits a single QBE `=w copy 2147483647` instruction. No runtime call, no extra memory. === Floating-point types `High(Double)` is a category error in Pascal: ordinal bounds and floating-point extremes are different concepts. IEEE 754 has `+inf`/`-inf`, finite `MaxDouble`/`-MaxDouble`, a smallest positive normal `MinDouble`, and `NaN` — none of which `High`/`Low` could disambiguate. The fix is to expose these as named constants in the `math` unit (`Math.Infinity`, `Math.NegInfinity`, `Math.NaN`, `MaxDouble`, `MinDouble`). The compile-time error tells the user exactly that. === Alternatives Rejected *Return `Integer` for every ordinal form* — rejected. `High(Int64)` would lose its top bit at the call site, silently producing wrong results in 64-bit arithmetic. *Generalise to floats as `High(Double) = MaxDouble`* — rejected. Conflates two distinct concepts (ordinal upper bound vs. largest finite float) and doesn't generalise to `+inf` or `NaN` semantics. Named constants on the `math` unit are unambiguous and discoverable. *Leave the existing array/string-only behaviour and tell users to write `2147483647`* — rejected. Discoverability is the point: every Pascal programmer reaches for `High`/`Low` first, and finding a helpful targeted error message ("use `Math.Infinity`...") only on floats is a much better DX than a generic "must be an array" error on every primitive. == Diamond Operator — Generic Type-Argument Inference === Decision Blaise supports the diamond operator `<>` in constructor assignment expressions. When a variable is declared with a fully-specified generic type, the constructor call on the right-hand side may omit the type arguments: [source,pascal] ---- var S: TStack; S := TStack<>.Create; // equivalent to TStack.Create var D: TDictionary; D := TDictionary<>.Create; // infers both K and V from the LHS ---- The operator works for any number of type parameters — single-argument generics (`TStack`, `TList`) and multi-argument generics (`TDictionary`, `TPair`) alike. === Rationale Repeating the full type argument list at every constructor call site is redundant when the declared variable type already carries the complete instantiation. Java introduced the diamond operator in Java 7 for the same reason; the pattern is well understood. The restriction to assignment context keeps the implementation minimal and unambiguous: the semantic pass has the declared LHS type available at the point it encounters the RHS constructor call, so inference requires no lookahead or backtracking. Inference in other contexts (parameter passing, return values) would require a more general type-propagation pass and is left to a future decision. === Alternatives considered *Require explicit type arguments everywhere* — rejected. Writing `TStack.Create` in both the `var` declaration and the constructor call is mechanical duplication that adds noise without information. *Infer at all call sites, not just assignments* — deferred. Inference from parameter and return-type context requires propagating the expected type through the expression tree before the call is analysed. The assignment case covers the common pattern at zero additional complexity. === Implementation The lexer folds `<>` into the single token `tkNotEquals`. The expression parser detects `IDENT tkNotEquals DOT` as the diamond pattern and stores the sentinel string `TFoo<>` in `TFieldAccessExpr.RecordName`. The semantic pass — in `AnalyseAssignment`, after resolving the LHS type — calls `ResolveDiamond`, which replaces the sentinel with the full concrete type name taken from `ResolvedLhsType.Name`. Subsequent semantic analysis proceeds identically to an explicit constructor call. == Generic Records === Decision Blaise supports generic records using the same `` syntax as generic classes and interfaces: [source,pascal] ---- type TMyVal = record Value: T; function GetValue: T; begin Result := Self.Value end; end; var V: TMyVal; begin V.Value := 42; WriteLn(V.GetValue()); end. ---- Generic records are monomorphized exactly like generic classes: each distinct instantiation (`TMyVal`, `TMyVal`, etc.) creates a new concrete `TRecordTypeDesc` with its own field layout and method bodies. Type parameter constraints apply identically. === Alternatives considered * *Classes only* — The initial generics implementation supported only classes and interfaces. This meant lightweight value-type wrappers (tagged unions, option types, result types) had to be classes with heap allocation and ARC overhead. Generic records give zero-overhead value-type generics. * *Reuse `TGenericTypeDef` / `TGenericInstance` for both classes and records* — Rejected because `TGenericTypeDef.ClassDef` is specifically a `TClassTypeDef` and the two AST node types have meaningfully different fields (records lack `ParentName`, `ImplementsNames`, `Properties`, `Attributes`). Separate `TGenericRecordDef` and `TGenericRecordInstance` types keep the AST honest and avoid nil-field ambiguity. === Key differences from generic classes Records are value types, so generic record instances do *not* emit: * Typeinfo blocks (`$typeinfo_`) * Vtable data (`$vtable_`) * Field cleanup functions (`$_FieldCleanup_`) * Inheritance chains or interface implementation tables They emit only field layout (offsets computed by the semantic analyser) and method bodies (one function per method per instantiation). ARC-managed fields inside generic records are handled by the existing record-cleanup logic at variable scope exit, not by class-style field cleanup. == Generic Methods === Decision A class or record method may declare its *own* type parameters, independent of any type parameters on the enclosing type: [source,pascal] ---- type TUtil = class function Pick(cond: Boolean; a, b: T): T; begin if cond then Result := a else Result := b end; end; ... u.Pick(True, 7, 9); // -> 7 u.Pick(False, 'a', 'b'); // -> 'b' ---- The method is *monomorphised* on first use: each distinct set of explicit type arguments at a call site produces one concrete method body, named `__` (e.g. `TUtil_Pick_Integer`). The implicit `Self` is preserved, so a generic method may read the receiver's fields and call its other methods. Type arguments are explicit at the call site (`u.Pick(...)`); they are not inferred from the value arguments. This is distinct from a method of a generic *class* (`TBox` and its `function Get: T`), where the type parameter `T` is bound by the class instantiation, not the method. The two compose: a generic class may have a method with its own additional type parameters. === Rationale Generic methods reuse the existing generic free-function machinery: the same on-demand instantiation, the same `` type-parameter scoping during body analysis, and the same `QBEMangle`/`NativeMangle` name mangling (`<`/`,` -> `_`, `>` dropped). The only addition over a free function is carrying the owner type and the implicit `Self` through to codegen, so the instance emits as an ordinary method. Monomorphisation (rather than type erasure) keeps the model uniform with generic classes and records and needs no runtime type descriptors. Both declaration forms are accepted: the inline body written inside the class, and the out-of-line implementation `function TUtil.Pick(...)` with the body separated from the class declaration (the body is transferred onto the in-class generic-method template before instantiation). == Platform constants — one name per concept Delphi and FPC carry multiple aliases for the same platform constant: * `LineEnding` (FPC), `sLineBreak` (Delphi), and sometimes `LineBreak` * `DirectorySeparator` (both), `PathDelim` (Delphi SysUtils alias) * `PathSeparator` (both) — no alias, the one exception Blaise retains exactly three names, one per concept: [cols="1,1,1"] |=== | Constant | POSIX value | Windows value | `LineEnding` | `#10` | `#13#10` | `DirectorySeparator` | `/` | `\` | `PathSeparator` | `:` | `;` |=== *Dropped:* `sLineBreak`, `PathDelim`. They were Delphi compatibility aliases that added no meaning — two names for the same value is a maintenance trap, not backwards compatibility. All three constants are defined in `system.pas` and pre-seeded as compiler built-ins in `RegisterBuiltins`. Their values are derived from the compilation target (`GTarget` in `blaise.codegen.target`), so cross-compilation to a different OS produces the correct constants without conditional compilation. == `not` — Boolean and Bitwise NOT === Decision The `not` operator accepts both Boolean and integer operands: * On a Boolean operand, `not` performs logical negation. The result type is `Boolean`. * On an integer operand (`Byte`, `SmallInt`, `Word`, `Integer`, `Int64`, `UInt64`), `not` performs bitwise complement — every bit is inverted. The result type is `Integer` for operands narrower than 64 bits, or `Int64` / `UInt64` for 64-bit operands. * On a floating-point operand (`Single`, `Double`), `not` is a semantic error. This matches Delphi and Free Pascal, where `not` is overloaded on the operand type. === Rationale Bitwise complement is essential for bitmask patterns such as `Flags := Flags and (not MASK)` (clear specific bits). Without `not` on integers, the programmer must use `xor` with a hand-computed all-ones constant — error-prone and width-dependent. Narrower-than-32-bit operands widen to `Integer` because the complement of a `Byte` value is meaningful as a 32-bit mask, not as a byte that wraps modulo 256. This follows the same integer-promotion rule that Blaise applies to other arithmetic on narrow types. === Alternatives Rejected *Require a separate `bitnot` keyword* — adds a keyword that no Pascal dialect uses. The overloaded `not` is unambiguous because the operand type determines the operation, and both Delphi and FPC programmers expect this behaviour. *Return the same type as the operand for narrow types* — a `Byte` result of `not B` where `B = 3` would be `252`, which is surprising when the value is used as a bitmask in a wider expression. Promoting to `Integer` is consistent with how other arithmetic operators handle narrow operands. == Boolean formatting — `WriteLn` prints `True` / `False` Both Delphi and FPC print `TRUE` / `FALSE` (case varies by RTL version) when a Boolean value is passed to `Write` / `WriteLn`. Early Blaise treated booleans as plain integers, so `WriteLn(True)` printed `1` and `WriteLn(False)` printed `0`. This was a codegen shortcut, not a deliberate design choice. It surprised users familiar with any other Pascal dialect and made program output harder to read. Blaise now emits a dedicated `_SysWriteBool` call for Boolean-typed arguments. The runtime writes the literal string `True` or `False` — no heap allocation, no string conversion. `BoolToStr` in `sysutils.pas` is unaffected and continues to return `'True'` / `'False'` as a string value. *Alternatives considered:* * `TRUE` / `FALSE` (all-caps, matching Delphi) — rejected; Blaise identifiers are `True` / `False` (title-case), and the output should match the language. * Keeping `1` / `0` and requiring explicit `BoolToStr` — rejected; the implicit integer rendering was a bug, not a feature. == `threadvar` — Thread-Local Storage *Decision:* Blaise supports the `threadvar` keyword for declaring thread-local storage variables. Syntactically it mirrors `var` but places the variable in TLS rather than the global data section. Each thread receives its own zero-initialised copy. *Scope restriction:* `threadvar` is only allowed at program or unit scope (not inside procedures or methods). Attempting to use it at local scope produces a parse error. *QBE backend:* The QBE IR uses QBE's native `thread` linkage qualifier. Data declarations emit `export thread data $Name = { ... }`, and all load/store references use `thread $Name`. QBE generates correct local-exec TLS access patterns on Linux x86_64 (`%fs:Name@tpoff`). *Native backend:* The x86_64 native backend emits threadvars in `.section .tbss,"awT",@nobits` and accesses them via `%fs:Name@tpoff` (local-exec TLS model). *Unit interface serialisation:* The `IsThreadVar` flag is serialised through `.bif` files so that importing units correctly mark symbols as thread-local. *Alternatives considered:* * POSIX `pthread_key_t` / `pthread_getspecific` — more portable but slower (function call per access vs. a single `%fs:` load). The native TLS model is the correct choice for a compiler targeting Linux x86_64. * Implicit TLS via runtime per-thread tables — rejected; the explicit keyword makes the intent clear and matches established Pascal practice (Delphi, FPC). == `TThread` Lifetime and ARC — No `FreeOnTerminate` === Decision Blaise's `TThread` does not provide the `FreeOnTerminate` property found in Delphi and FPC. Thread lifetime is managed entirely through ARC: when the last reference to a `TThread` instance is released, `Destroy` calls `WaitFor` (which calls `pthread_join`), blocking until the OS thread has fully exited before the object's memory is freed. The trampoline (the free procedure passed to `pthread_create`) does *not* hold an ARC reference to the `TThread` instance. It accesses the object only while the thread is running; `pthread_join` in `Destroy` guarantees the trampoline has returned before the object is deallocated, so there is no use-after-free. === Rationale In Delphi and FPC, object lifetime is manual. `Free` directly calls `Destroy`, and if the programmer forgets to call it the object leaks. `FreeOnTerminate` exists to handle the "fire-and-forget" pattern: when a thread is spawned and the caller discards the reference, the thread must clean itself up on completion or the `TThread` instance leaks. In Blaise, ARC eliminates this class of leak. Every relevant scenario is already handled: * *Explicit Free* — `Free` lowers to `_ClassRelease`. If this drops the refcount to zero, `Destroy` runs, which calls `WaitFor`. The caller blocks until the thread finishes, then the object is freed. * *Scope exit* — when a local `TThread` variable goes out of scope, ARC emits `_ClassRelease`. Same chain: `Destroy` → `WaitFor` → join → free. * *Field in an owning object* — when the owner is destroyed, the field is released. Same chain. * *Reassignment* — `T := nil` or `T := SomeOtherThread` releases the old reference. Same chain. In every case, ARC ensures the `TThread` is joined and freed exactly once, without any explicit cleanup code from the programmer. === Why the trampoline must not hold an ARC reference The original implementation had `Start` call `_ClassAddRef` on the `TThread` instance, and the trampoline call `_ClassRelease` in its `finally` block. This created a reference-count of 2 (one for the caller, one for the trampoline). The consequence: when the caller released its reference (via `Free`, scope exit, or reassignment), the refcount dropped to 1 — not zero — so `Destroy` did not run, `WaitFor` was never called, and the main program could exit before the thread finished. Removing the trampoline's ARC reference restores the correct behaviour: the caller's reference is the *only* reference, so releasing it triggers `Destroy` → `WaitFor`. The `pthread_join` call is the synchronisation barrier that guarantees the trampoline has fully returned before the object is deallocated. === Migration note Code ported from Delphi or FPC that sets `FreeOnTerminate := True` will fail to compile. The fix is to simply delete the assignment — ARC handles the lifetime automatically. The Blaise migration analyser flags this pattern and explains the removal. === Alternatives considered * *Keep `FreeOnTerminate` for compatibility* — rejected. The property introduces a second, redundant lifetime model that conflicts with ARC semantics. Keeping it would require the trampoline to hold an extra ARC reference (breaking the join-on-free guarantee) or special-case logic in `Destroy` (complexity for a feature that ARC already covers). * *Detached threads (no join)* — not yet supported. If a future use-case requires truly detached threads (where the caller never waits), this would be a separate API (`TThread.Detach` or a `TDetachedThread` subclass) with explicit opt-in semantics, not a boolean property on the base class. == Mandatory parentheses on zero-argument calls *Decision*: every function, procedure, method, and constructor call requires parentheses, even when there are no arguments. `Obj.Free()`, `Create()`, `WriteLn()`, `Foo()` — never `Obj.Free`, `Create`, or `Foo` without `()`. The `@` (address-of) operator is the sole exception: `@Obj.Method` takes a method reference and does not invoke the method, so no `()` is written. In standard Object Pascal (Delphi and FPC), a bare identifier that resolves to a parameterless function or procedure is ambiguous: it can be either a call or a reference to the function's `Result` variable (inside the function body) or a method reference (with `@`). The compiler resolves the ambiguity by context — statement position means call, expression position means call unless `@` is present — but this creates several problems: * *Readability*: `F := Foo;` — is `Foo` a variable, a constant, or a function call? The reader must consult the declaration to know. * *Compiler complexity*: the compiler must synthesise invisible call nodes for zero-argument functions and track `IsNoArgFuncCall` / `NoArgFuncDecl` flags through the AST to distinguish calls from variable reads. This introduces a class of latent bugs whenever a new AST consumer (codegen backend, semantic pass) forgets to check the flag. * *Grep-ability*: searching for all call sites of `Foo` requires semantic analysis; a textual search for `Foo(` misses the zero-arg calls. Requiring `()` eliminates all three problems. The syntax is unambiguous at the lexical level: `Foo()` is always a call, `Foo` is always a read. The compiler AST becomes simpler — `TMethodCallExpr` for calls with explicit parentheses, `TFieldAccessExpr` only for field/property reads. === Alternatives considered * *Deprecation warning period* — rejected. Blaise is in alpha; there is no stable public API to preserve. A deprecation cycle adds complexity for no practical benefit when the migration is a mechanical find-and-replace. * *Optional parentheses (status quo)* — rejected. The ambiguity is the root cause of multiple latent codegen bugs discovered during this change: `IsBuiltinToString` applied to record methods, `IsVarParam` not set for record value parameters in `TMethodCallExpr`, and native backend missing `leaq` for record receivers. All were masked by the old `TFieldAccessExpr.IsMethodCall` path. == Implicit Integer→Float Widening — Builtins and Typecasts === Decision Integer-family values (`Integer`, `Int64`, `UInt32`, `UInt64`, `SmallInt`, `Word`, `Byte`) widen implicitly to `Double` wherever a float is expected: * Assignment to a `Single` or `Double` variable (`d := i`). * Arguments to the float compiler builtins: `Sqrt`, `Ceil`, `Floor`, `Round`, `Trunc`, `Ln`, `Log2`, `Log10`, `Power`, `Sin`, `Cos`, `Tan`, `ArcTan`, `ArcTan2`, `ArcSin`, `ArcCos`, `Sinh`, `Cosh`, `Tanh`, `IsNaN`, `IsInfinite`. `Sin(12)` and `Tanh(I)` are valid; the result type of the value-returning trig builtins is `Double` for integer arguments (for float arguments it continues to match the argument type — `Single`→`Single`, `Double`→`Double`). * Explicit float typecasts: `Double(I)` and `Single(I)` perform a real numeric conversion (never a bit reinterpretation). Float-to-float casts likewise convert: `Single(D)` narrows, `Double(S)` widens. === Rationale Both FPC and Delphi accept integer arguments at every float call site via implicit widening; rejecting them made Blaise's builtins stricter than the rest of the language for no benefit — plain assignment already widened implicitly, so `d := i; d := Tanh(d)` compiled while `d := Tanh(i)` did not. Mixed integer/float arithmetic (`s * 1000`) was likewise already accepted by the semantic pass. The widening is always to `Double` (not `Single`): it is the lossless choice for every 32-bit integer and matches the C math library signatures the builtins lower to. === Alternatives Rejected * *Require explicit float literals/casts* (status quo) — rejected: it contradicts both Pascal tradition and Blaise's own assignment rule, and the recommended workaround (`Tanh(Single(I))`) did not work because float typecasts were lowered as bit copies. * *Widen to `Single` for small integer types* — rejected: `Double` represents every `Integer` exactly; `Single` does not (24-bit mantissa). Precision should not silently depend on the argument's declared integer width. == Interface Properties === Decision Interfaces may declare properties. Accessors must be methods of the interface itself or an inherited parent interface — interfaces have no fields, so field-backed accessors cannot exist: [source,pascal] ---- type IValued = interface function GetValue(): Integer; procedure SetValue(AValue: Integer); property Value: Integer read GetValue write SetValue; end; ---- `I.Value` lowers to the getter dispatched through the itab; `I.Value := X` lowers to the setter dispatch with `X` as the single argument. Properties are pure compile-time sugar: they occupy no itab slots, change no layout, and have no ARC implications beyond the underlying method calls. Child interfaces see inherited properties. Read-only (`read` only) and write-only (`write` only) properties are supported; accessing the missing direction is a compile-time error. === Rationale Both FPC and Delphi support interface properties with identical syntax — there is no dialect fork to choose from. Without them, interface consumers are forced back to raw getter/setter calls even when the implementing class exposes a property, which defeats the purpose of publishing the interface as the API surface. === Limitations (v1) * Property access requires a plain interface-typed variable as the receiver; access through record/class fields holding interfaces or through chained expressions is not yet wired. * Indexed and default array properties in interfaces are not yet supported. == Collection Ownership — ARC Conventions === Decision The collection types follow three explicit ownership conventions: * `TObjectList` — ownership is opt-in: `Create(True)` retains on add and releases (destroying at refcount zero) on remove/clear/destroy; `Create(False)` stores borrowed references. * `TList`, `TStack`, `TQueue` and the other generic collections — managed elements (`string`, class, dynamic array) ARE retained on store and released when an element slot is overwritten (stores lower to ARC-aware pointer writes). An object whose only other reference was a local in an exited routine therefore survives in the collection. * `TStringList.Objects[]` — NON-OWNING raw pointer slots, by design. The API type is `Pointer`, and storing integer payloads cast to pointers (`TObject(PtrUInt(N))`) is an accepted idiom (the compiler itself uses it); blind retention would corrupt such entries. Callers storing real objects must hold a strong reference elsewhere for the entry's lifetime. === Rationale Retention for typed generic elements is safe because `T` is statically known — only genuinely managed types get ARC operations. `Objects[]` cannot distinguish a pointer-encoded integer from an object reference at runtime, so it keeps FPC/Delphi's borrowed-pointer contract. === Known limitation (leak backlog) `Clear`/`Destroy` on the generic collections do not yet release the retained references of remaining managed elements (releasing requires a `Default(T)`-style generic zero-store that the language does not provide yet). Until then, collections of managed elements leak their contents when destroyed non-empty; element overwrite and `Delete` shifting are ARC-balanced. == Module Names Are Reserved Identifiers *Decision*: the name of the module being compiled (the `program` or `unit` header name) and the names of all directly used units are reserved identifiers within that module. No top-level declaration — variable, constant, type, procedure, or function — may redeclare them. Inner scopes (routine bodies) may shadow them freely. The module name is not a value: referencing it in an expression reports an undeclared identifier. This matches Delphi and FPC, both of which reject the redeclaration with `Duplicate identifier` (Delphi error E2004 documents exactly this case: `program Tests; var Tests: Integer;`). === Rationale Before this rule (issue #84), Blaise accepted: [source,pascal] ---- program blaisefault; var blaisefault: Integer; { accepted; rejected by FPC and Delphi } begin blaiseFault := 1; end. ---- The acceptance was an accident of implementation, not a decision: the parser stored the module name on the AST node without registering it in the symbol table, so the duplicate-identifier check had nothing to collide with. The behaviour was benign only because Blaise currently has no module-qualified identifier syntax (`unitname.identifier`) and emits no linker symbol named after the module. Reserving the name now was chosen over documenting the accident as a feature for three reasons: * *Design freedom*: module-qualified access is the standard escape hatch when two used units export the same name. If it is ever added, module names must be resolvable identifiers; programs relying on the old behaviour would break or silently change meaning. Reserving the name early keeps that door open at near-zero cost. * *Migration expectations*: code arriving from FPC or Delphi already obeys the rule, and programmers from those compilers expect the error. * *Consistency*: the rule extends uniformly to directly used unit names, again matching FPC (`uses sysutils; var sysutils: Integer;` is rejected). === Implementation The semantic pass plants an `skModule` marker symbol for the module's own name and each directly used unit name in the scope that receives top-level declarations (the global scope and the program block scope for programs; the unit scope for units). The ordinary duplicate check on `TSymbolTable.Define` then rejects redeclarations with the standard `Duplicate identifier` error. `TSymbolTable.Lookup` treats a marker hit as unresolvable, so the reserved name is not usable as a value and blocks same-named symbols in lower resolution layers, while pushed inner scopes shadow it naturally. One deliberate divergence: FPC accepts a *procedure* whose name matches the program name (an apparent accident of its overload machinery). Blaise rejects all declaration forms uniformly. The grammar is unchanged — this is a name-resolution rule, not a syntax rule — so `docs/grammar.ebnf` is unaffected. == Zero-Initialisation of Variables === Decision Every variable in Blaise is guaranteed to hold its zero value before any explicit assignment. This is a *language semantic*, not an implementation detail. The guarantee covers all storage categories and all type kinds: [cols="2,1", options="header"] |=== | Type kind | Zero value | Integer, Int64, Byte, Word, SmallInt, UInt32, UInt64 | `0` | Single, Double | `0.0` (positive zero — all bits 0) | Boolean | `False` | Byte (used as Char) | `#0` | Enum | ordinal `0` (first declared member) | Set | empty set (all bits 0) | Pointer, typed pointer, PChar | `nil` | Class reference | `nil` | Interface (both fat-pointer slots) | `nil` (`obj = 0` and `itab = 0`) | Metaclass (class-of) | `nil` | Procedural / method pointer | `nil` (both slots for method pointers) | String | `''` (nil data pointer) | Dynamic array | `nil` (length 0) | Static array | every element zero-initialised | Record | every field recursively zeroed |=== The guarantee applies to: local variables, global variables, class fields (at `ClassAlloc`), record fields (at declaration/allocation), static-array elements, threadvars, and the `Result` variable of value-returning functions. Open-array parameters are excluded — they are caller-owned and the callee receives a pointer to the caller's storage. The grammar is unchanged. === Rationale Zero-initialisation removes an entire category of undefined-behaviour bugs (reading an uninitialised variable). It also simplifies ARC correctness: every managed slot starts `nil`, so a `Release(nil)` on the first-write path is always safe. Blaise is a systems-capable language but not a C replacement. The audience expects the safety of a managed language first and the performance escape hatches of a systems language second. The Principle of Least Surprise favours predictable zeroes over garbage. === Alternatives Considered and Rejected *Garbage-by-default for performance.* C/C++ leave locals uninitialised. The performance argument is real for large hot static arrays, but profiling shows the cost is negligible in practice: the QBE backend's dead-store elimination removes the zero store whenever the first use is an assignment; the native backend emits one extra store per scalar local (acceptable). If a future profiling run ever identifies a hot large array as a bottleneck, the agreed escape hatch is a `{$noinit}` attribute — *not implemented now*; mention here serves as the single authoritative record of that decision. *Definite-assignment analysis instead of zero-initialisation.* A compiler that rejects programs reading a variable before explicit assignment provides the same safety with no runtime cost. This is the approach taken by Java, C#, and Rust. Blaise does not rule it out — a future diagnostic pass could warn on read-before-explicit-assignment as a lint on top of the zero-init guarantee — but it is not a substitute for it. Zero-init is a runtime guarantee; definite-assignment analysis is a static check that does not cover all cases (pointers, out-params, aliasing). === Implementation * The QBE backend zeroes every local in `EmitVarAllocs` (alloc + store/memset). QBE's dead-store elimination removes the store when the first use is an assignment. * The native x86-64 backend zeroes every local in `EmitFunctionDef` using `movl $0` / `movq $0` / `xorpd + movsd` / `memset` as appropriate for the type width. An exhaustive `case` with an `else raise` ensures no new type kind is silently skipped. * Global variables are placed in the `.data` section with explicit zero directives (`.quad 0`, `.long 0`, etc.); threadvars are placed in `.tbss` (zero-initialised thread-local storage). * Class instances are zeroed by `_ClassAlloc` via `memset` before the constructor runs. === Future Roadmap (not implemented) * `{$noinit}` attribute — opt-out for a specific variable when the profiler identifies the zero-store as a measurable hot-path cost. Agreed as the escape hatch if ever needed; not to be added speculatively. * Definite-assignment warning — a lint diagnostic for read-before-explicit-assignment. Zero-init is the hard guarantee; the warning is advisory and will be implemented as a separate analysis pass. == Multi-Dimensional Static Arrays === Decision Static arrays may declare and index multiple dimensions using either a comma-separated form or nested single-dimension arrays, and the two are exactly equivalent: [source,pascal] ---- var A: array[0..1, 0..2] of Integer; // comma form B: array[0..1] of array[0..2] of Integer; // nested form begin A[i, j] := v; // comma indexing A[i][j] := v; // chained indexing — identical to the line above end; ---- The comma form is *syntactic sugar*: the parser desugars `array[L1..H1, L2..H2] of T` into `array[L1..H1] of array[L2..H2] of T`, and a comma index list `A[i, j]` into chained subscripts `A[i][j]`. Dimensions extend to any depth (`array[0..1, 0..1, 0..1] of T`). === Rationale Representing a multi-dimensional array as nested single-dimension arrays keeps the type system, symbol table, sizing, ARC, and the OPDF debug emitter free of any special "rank" concept. An inner dimension is simply the element type of the outer one, so a 2-D array is laid out as a flat, row-major block of contiguous inline storage (no per-row indirection), which is the conventional Pascal layout and the most cache-friendly. The element-address computation already existed for single-dimension arrays; the only codegen gap was that a static-array *element* was loaded as a value rather than returned as an address, which broke a further subscript on it. Both backends now return the inline sub-array's address (mirroring how record and interface elements are already handled), so a chained subscript indexes into it correctly. Because the comma forms desugar to the nested forms before semantic analysis, both notations are interchangeable in every position: a value declared with one form can be indexed with the other. === Alternatives considered * *A dedicated multi-dimensional array type descriptor carrying a list of bounds.* Rejected: it would duplicate the element-address logic, require a new OPDF record shape, and complicate sizing/ARC for no behavioural gain over the nested representation. * *Supporting only the comma form (no bare chained writes).* Rejected as inconsistent: the desugared target `A[i][j] := v` would then be a form the user could not write directly, even though the read form `A[i][j]` was always legal. === OPDF debug emission No new debug record is required. A nested array emits one `recArray` (`Dimensions = 1`) per dimension, with each dimension's element type pointing at the next inner `recArray`. The reference debugger (pdr) follows the element chain and renders the value as a true multi-dimensional structure, e.g. `array[0..1] of array[0..2] of Integer = [[0,0,0],[0,0,0]]`. === Constant initialisers Multi-dimensional *const* arrays use the same nested desugaring and row-major layout. Both the comma form (`array[0..1, 0..2] of Integer`) and the nested form (`array[0..1] of array[0..1] of Integer`) are accepted in a const type annotation, and the initialiser nests one parenthesised group per dimension (`((1, 2, 3), (4, 5, 6))`). The values are flattened to row-major order at parse time, so codegen emits a single contiguous data blob identical to what the equivalent runtime assignments would produce. See the "Array-of-enum Typed Constants" section for the value-count validation rule. === Dynamic-array nesting (jagged arrays) A *dynamic* array of dynamic arrays (`array of array of T`) supports the same chained subscript forms for both reading and writing: [source,pascal] ---- var m: array of array of Integer; begin SetLength(m, 2); SetLength(m[0], 3); // each row sized independently — rows may be jagged m[0][1] := 5; // chained element write WriteLn(m[0][1]); // chained element read end; ---- The representation differs from the static case: each level is a separate heap allocation, so an inner row is reached through a pointer rather than inline storage. The chained-write codegen therefore evaluates the base expression (`m[i]`) to the inner array's *data pointer* and indexes into it, whereas the static path computes an inline offset. This indirection is what makes jagged arrays possible — distinct rows may have distinct lengths, set independently with `SetLength(m[i], n)`. The element type of a chained write may be any managed or unmanaged type (integers, strings, records, classes); the store path applies the same ARC retain/release discipline used for a single-dimension dynamic-array element write. == Initialised Global Variables === Decision A global variable declaration may carry an initialiser: [source,pascal] ---- var G: Integer = 42; S: string = 'hello'; A: array[0..2] of Integer = (10, 20, 30); ---- The value is folded at compile time and emitted directly into the data section, so the variable holds its initial value before the program body runs. This matches FPC and Delphi. The initialiser uses the same value grammar as a typed constant — a scalar, string, boolean, real, or a parenthesised aggregate list — and is restricted as follows: * *Global scope only.* Initialisers are accepted on program-level and unit-level variables. A local variable initialiser is rejected: locals are zero-initialised and then assigned in the body, keeping the data-section path the single source of truth for static initial values. * *Single name.* `var A, B: Integer = 1` is a parse error — an initialiser applies to exactly one variable, avoiding the ambiguity of whether the value is shared or per-name. * *Arrays only for aggregates.* Array initialisers reuse the existing array-constant machinery. Record and inline-set initialisers are not yet supported (Blaise has no record-constant infrastructure, and a set initialiser would collide with the const-symbol the set folder defines); both are rejected with a clear diagnostic rather than silently mis-emitted. === Rationale Reusing the constant pipeline (parse → fold → data-section emit) keeps the feature small and consistent: an initialised global is, in effect, a constant value placed in mutable storage. The folded value is emitted as a typed data slot in both backends — a single field for scalars/strings (a string global points at an immortal static header, `__sN + 12`), and an inline element list for arrays (identical layout to an array constant). No runtime initialiser code is generated; the loader provides the value. === Alternatives considered * *Emit an assignment at program start instead of static data.* Rejected: it defeats the point (the variable would be zero until the assignment runs) and bloats the entry path. Static data is what FPC/Delphi emit and what the debugger expects. * *Allow local initialisers (assignment at scope entry).* Deferred. It is a reasonable future extension but mixes two emission models; the global data-section path is the clean baseline. * *Support record/set initialisers now.* Deferred until record-constant infrastructure exists; rejecting them keeps the feature honest. == Inline Set Types === Decision A set type may be written inline anywhere a type is expected — a variable, parameter, record field, etc. — not only in a named type declaration. The element type may be a named enumeration or an anonymous enumeration written in place: [source,pascal] ---- var Days: set of TWeekday; // named enum element Flags: set of (fA, fB, fC); // anonymous enum element ---- Previously a set type had to be named in the `type` section (`type TS = set of TWeekday; var S: TS;`). Both inline forms now match FPC and Delphi. === Rationale The implementation resolves an inline set entirely from its canonical type-name string, with no special parser state. The parser encodes the type as `set of ` (named element) or `set of (a,b,c)` (anonymous element); the semantic type resolver (`FindTypeOrInstantiate`) recognises the `set of ` prefix and constructs the `TSetTypeDesc` on demand, exactly as it already does for `array of T`, `^T`, and `class of T`. For the anonymous form it first synthesises an enum type from the encoded member list — registering each member as an ordinary enum constant — then builds the set over it. Because set types compare structurally, the synthetic enum's name need only be unique, and an identical inline enum encountered again reuses the existing one (keyed on its already-defined member constants). Encoding the enum in the type string rather than carrying parser-side state keeps the feature self-contained and makes it work uniformly in every position a type can appear, with no ordering or ownership concerns. === Alternatives considered * *Synthesise the enum as a side effect in the parser and stage it for transfer into the enclosing block.* Rejected: it makes `ParseTypeName` mutate shared state, requires every declaration site to flush the staging area, and introduces declaration-ordering hazards between the synthesised enum and any type that references it. The string-encoding approach has none of these problems. * *Require named set types (status quo).* Rejected: inline set types are common in ported FPC/Delphi code and the named-type requirement is friction with no compensating benefit. == Loosely-Typed Parameter Passing — `array of const`, untyped params, `varargs`, `Variant` Four Object Pascal features share the theme of passing a value whose type is not fully fixed at the call site. They are frequently conflated but are distinct mechanisms with very different safety profiles. Blaise adopts only one of them. [cols="1,1,1,2", options="header"] |=== | Feature | Runtime type tag | Count known | Purpose | Untyped `const Data` / `var Data` | none (raw bytes) | n/a (single argument) | Low-level memory primitives (`Move`, `FillChar`) | `array of const` | yes — per element (`TVarRec.VType`) | yes (it is an array) | Heterogeneous argument lists (`Format`-style APIs) | `varargs` | none | no | C ABI interop (`printf`) | `Variant` | yes (`TVarData.VType`) | n/a (a storeable value) | Dynamic / late-bound values (historically COM/OLE) |=== === Decision Blaise implements *only* `array of const`. Untyped parameters, `varargs`, and `Variant` are deliberately omitted. === Untyped parameters (`const Data`, `var Data`) — omitted An untyped parameter discards the argument's type entirely; the compiler implicitly takes the variable's address, and the callee operates on raw bytes (`PByte(@Data)`). It exists for call-site ergonomics on raw-memory routines: `Move(Source, Dest, Count)` reads more cleanly than `Move(@Source, @Dest, Count)`. This is rejected on footgun grounds, which is the consistent thread through Blaise's type-system decisions. `DoSomething(Rec)` *looks* like it passes a value, but silently passes an address and erases the type — the caller gets no signal that raw, uncheckable memory access is happening. The same job is done by an ordinary typed-pointer parameter with no new language machinery: [source,pascal] ---- procedure DoSomething(const Data: PByte); // or ^TMyRecord ... DoSomething(@Rec); // explicit, honest ---- The explicit `@` at the call site signals "raw memory, be careful," and a *typed* pointer (`PByte`, `^TMyRecord`) keeps type information — it permits indexing and field access without a cast, which is strictly more useful than a fully untyped blob. The only thing lost is the `@`-free call syntax on a handful of RTL primitives; that is an ergonomics cost, arguably a clarity gain, and not a capability loss — a typed pointer can express everything an untyped parameter can. Blaise's RTL writes these primitives against `Pointer` / `PByte` instead. === `varargs` — omitted `varargs` is the untyped, C-ABI variadic form (FPC's `cdecl; varargs`). It carries no type tags and no argument count; the callee must recover both from an external convention (e.g. a `printf` format string). It exists almost exclusively to call C variadic functions. Blaise has no need for it today, and it is the least safe option of the four — omitted unless direct C-variadic interop becomes a requirement. === `Variant` — omitted `Variant` is a storeable, self-describing value type with runtime conversion rules, late binding, and (historically) COM/OLE Automation dispatch. It is the weakest fit for Blaise: its coercions can fail at runtime, and its primary historical motivation (COM) is irrelevant to Blaise's targets. Blaise prefers the statically-checkable alternatives — method overloads for the fixed-arity cases, generics for parametric code, and explicit discriminated unions (`case`-based records) for genuinely dynamic data. A clean tagged union expresses "one of a known set of types" with compile-time checking; `Variant` trades that checking for runtime flexibility Blaise does not want to encourage. === `array of const` — adopted `array of const` is the one mechanism that overloads and generics cannot cleanly replace, and it is therefore worth its cost. Its purpose is a single call accepting an *arbitrary-length, mixed-type* argument list: [source,pascal] ---- WriteLn(Format('%s is %d years old', [Name, Age])); Log(['count=', N, ' ratio=', R, ' ok=', Flag]); ---- No finite set of overloads can express "any number of arguments, each of any type" — the arity and type combinations are unbounded. This is the feature that makes `Format`-style APIs feel native, which is the expected Pascal idiom. Unlike `Variant`, `array of const` is a compiler-generated *call construct*, not a free-floating dynamic value type. The compiler builds an array of tagged records (`TVarRec`: a `VType` discriminator plus a pointer-sized `VValue` slot) at the call site from the bracket literal, and the callee dispatches on `VType`. The tag set is fixed and known at compile time, the array length is known, and the construct cannot leak into arbitrary storage the way a `Variant` value can — so it admits the heterogeneous-list ergonomics without importing `Variant`'s runtime-coercion surface. ==== Implementation A parameter declared `array of const` is an open array whose element type is the intrinsic record `TVarRec`. `TVarRec` is registered as a compiler builtin (like `TObject` and `TMethod`), so it and the `vt*` discriminant constants are available with no `uses` clause — matching Delphi, where `array of const` lives in the auto-available System unit. Its layout is a 16-byte record: a `VType` byte at offset 0 and a pointer-sized `VValue` at offset 8. Because Blaise has no record variant parts (a deliberate omission), `TVarRec` does not expose Delphi's overlapping union fields (`VInteger`, `VAnsiString`, …). Instead the callee reads each element by reinterpret-casting the single `VValue` slot, switching on `VType`: [source,pascal] ---- procedure Log(args: array of const); var i: Integer; begin for i := 0 to High(args) do case args[i].VType of vtInteger: WriteLn(Integer(args[i].VValue)); vtBoolean: WriteLn(Boolean(args[i].VValue)); vtAnsiString: WriteLn(string(PChar(args[i].VValue))); vtExtended: WriteLn(PDouble(args[i].VValue)^); vtObject: ...TObject(args[i].VValue)...; end; end; ---- The supported element kinds and their tags are `vtInteger` (also boolean as `vtBoolean`, enum ordinals as `vtEnum`), `vtInt64`, `vtExtended` (`Double`), `vtAnsiString` (the UTF-8 string), `vtObject`, and `vtPointer`. Two representation choices follow from the pointer-sized value slot: * *Doubles are heap-boxed.* A `Double` does not fit a reinterpret-cast through the pointer slot (it is a different register class, and `Double(ptr)` is not a bit-reinterpretation). So a `vtExtended` element stores a pointer to a heap-allocated `Double`; the callee dereferences it (`PDouble(VValue)^`). This mirrors FPC's `vtExtended: PExtended`. * *Strings and objects are borrowed, not retained.* The boxed value is the string's data pointer / the object pointer, stored without an `AddRef` (FPC semantics). The `array of const` array borrows its elements for the duration of the call; the caller's source values outlive the call, and the callee must not retain a reference beyond it. Borrowing avoids the ARC traffic and lifetime entanglement that owning each element would impose on a transient stack array. A heterogeneous bracket literal is accepted only in `array of const` position: the array-literal analyser, on finding mixed element types, types the literal as `array of TVarRec` instead of rejecting it, and overload resolution binds it to an `array of const` formal (or fails with "no matching overload"). A homogeneous literal (`[1, 2, 3]`) and the empty literal (`[]`) bind too. === Alternatives considered * *Adopt untyped parameters for RTL ergonomics.* Rejected — typed pointers do the same job explicitly and keep type information; see above. * *Adopt `Variant` for dynamic values.* Rejected in favour of overloads, generics, and explicit tagged unions, which keep checking at compile time. * *Reject `array of const` too, and require callers to build an explicit array/record.* Rejected — it would make `Format`-style APIs verbose and un-idiomatic, and the construct is compiler-bounded and tagged, so it does not carry the open-ended risks of the omitted three. == Variable Names May Not Shadow Type Names === Decision A variable declaration may not share its identifier with any type name that is visible at the point of declaration — whether the type is built-in (`Integer`, `string`), declared in the same block, declared in an enclosing scope, or imported from another unit. Pascal is case-insensitive, so the shadowing is detected regardless of letter case: [source,pascal] ---- type Iface = interface procedure Test; end; var iface: Iface; // ERROR: 'iface' and the type 'Iface' are the same identifier ---- The diagnostic is `Duplicate identifier 'X' — a type with this name is already visible`, reported at the variable's declaration site. This rule is stricter than FPC `mode objfpc`, which reports a duplicate only when the variable clashes with a type declared in the *same* scope, and silently permits a variable to shadow a built-in or outer-scope type (`var Integer: Int64` compiles in FPC). The same rule applies to *enum members*: a variable may not share its name with a visible enum constant. [source,pascal] ---- type TC = (A, B, C, D); var c: TC; // ERROR: 'c' and the enum member 'C' are the same identifier ---- Without this rule the variable shadows the member, so a set literal that names the member — `s := [A, C, D]` — silently resolves `C` to the variable (not the constant). That is not a compile-time constant, so the QBE backend rejects it with a cryptic message while the native backend produced a *wrong bitmap with no error at all* (the member's bit was simply dropped). Rejecting the shadow at the declaration, with `Duplicate identifier 'X' — an enum member with this name is already visible`, removes both failure modes (FPC/Delphi allow the shadow; Blaise does not, consistent with the type-name rule above). === Rationale A variable that bears the same name as a type is almost always a mistake, and when it is not, it is a readability trap: every subsequent use of the identifier in that scope means the variable, so the type becomes unreferenceable by its own name and later declarations like `x: Iface` resolve in surprising ways. Issue #102 reported exactly this confusion — a program declaring both an `Iface` interface and an `iface` variable compiled without complaint on both backends. Blaise already rejects variable-vs-variable, variable-vs-constant, and type-vs-type clashes in the same scope. The variable-vs-type case slipped through only because of an implementation detail: type declarations are registered in the block's enclosing scope so they remain visible after the block scope is popped, while variable declarations are registered one scope deeper. The duplicate check at definition time therefore never saw the type. Closing the gap with an explicit type-name lookup restores the symmetry users expect: no two declarations in overlapping scopes may claim the same identifier. === Alternatives considered * *Match FPC exactly (same-scope clash only; built-in/outer shadowing allowed).* Rejected. It fixes the reported case but keeps the underlying footgun: `var Integer: Int64` would still compile and silently rebind the type name for the rest of the scope. Blaise favours a clean rule over FPC bug-for-bug compatibility (cf. the deliberate omission of `Char`, the uniform 0-based indexing, and the required `overload` keyword). * *Warn instead of error.* Rejected. The construct has no legitimate use that a rename would not serve more clearly, and a warning that is routinely ignored would leave the confusion in place. * *Restrict the rule to reference types (classes, interfaces) only.* Rejected as arbitrary — the confusion is identical for records, enums, aliases, and built-ins, and a name-based rule is simpler to explain than a kind-based one. == Set Literal Ranges === Decision A set literal element may be a range `lo..hi`, expanding to every member from `lo` to `hi` inclusive: [source,pascal] ---- type TColor = (Red, Green, Blue, Yellow, Cyan); TColorSet = set of TColor; var e: TColorSet; begin e := [Red..Blue]; // {Red, Green, Blue} e := [Red, Blue..Cyan]; // {Red, Blue, Yellow, Cyan} — ranges mix with singles end; ---- Both bounds must be compile-time constants of the set's base type. A reversed range (`[Blue..Red]`, low ordinal greater than high) is a compile-time error, not a silently empty set. Variable bounds (`[lo..hi]` where `lo`/`hi` are variables) are rejected with "set range bound must be a constant". This matches the range form FPC and Delphi accept in set constructors, with two deliberate restrictions (see below). === Rationale `[1..10]` (or `[Red..Cyan]`) is far clearer than spelling out every member, and the construct is standard Pascal — the reporter of issue #105 reasonably expected it to work. The implementation expands each range into individual member nodes during semantic analysis, so the rest of the pipeline — overload resolution, the bitmask folder, the jumbo-set path, and both code generators — sees an ordinary member list and needs no range-specific logic. A transient `TSetRangeExpr` AST node carries the unexpanded range from the parser to that expansion point; it is also wired into the `.bif` serialiser and the AST cloner because a generic template body may contain an unexpanded range that is serialised and re-instantiated later. === Alternatives considered * *Treat a reversed constant range as an empty set (FPC behaviour).* Rejected. FPC accepts `[5..3]` as empty (with a warning); when both bounds are compile-time constants there is no legitimate reason to write a reversed range — it is a typo, and `[]` already expresses the empty set unambiguously. Erroring matches Blaise's preference for rejecting confusing-but-legal constructs (cf. variable/type name clashes). * *Support variable range bounds `[lo..hi]`.* Deferred. Runtime-variable bounds require emitting a bit-setting loop in both backends across the small-set and jumbo-set paths; the constant form covers the reported case and the overwhelmingly common usage. The restriction is enforced with a clear diagnostic rather than silently mis-compiling. * *Support ranges over an ordinal base type (`set of byte`, `[1..3]`).* Out of scope: Blaise set base types are enumerations today. Ordinal-based set types are tracked separately (see `docs/future-improvements.adoc`); because range expansion is base-type-agnostic, `[1..3]` will work automatically once they land. == Floating-Point Constant Expressions === Decision Constant declarations accept compile-time floating-point expressions, not only bare float literals. The following forms are supported: [source,pascal] ---- const Pi = 3.14159; { bare literal — already supported } Tau = Pi * 2.0; { named const × literal } Half = 10 / 4; { integer / integer → float (2.5) } Mixed = 2 * 1.5 + 0.5; { integer × float + float → float } Neg = -1.5 * 2.0; { unary minus propagates through float } ---- The `/` operator (real division) always produces a float result, even when both operands are integers. This matches Delphi and FPC semantics and matches the existing runtime behaviour of the `/` operator on variables. Integer division is expressed via `div`. === Rationale Before this change, any expression involving a float literal or the `/` operator in a constant declaration was rejected — the constant folding path only handled integer arithmetic. Users had to inline magic numbers or break them into separate `var` initialisations, losing the self-documenting benefit of named constants. The semantic pass detects a float expression (at least one float literal, a named float constant, or use of `/`) and dispatches to a float-specific constant folder. The folder performs arithmetic using the host's IEEE 754 `Double` type and stores the result as a string in the constant declaration, exactly as bare float literals are stored. === Alternatives considered * *Defer to runtime evaluation.* Rejected. Constants must be folded at compile time; emitting runtime arithmetic for `const X = 3.14 * 2.0` would violate the constant contract and prevent their use in contexts requiring true constants (e.g. array bounds, case labels). * *Support only the four arithmetic operators.* Adopted. Bitwise operators (`and`, `or`, `xor`, `shl`, `shr`) have no meaningful float semantics. The `mod` operator could theoretically map to `fmod`, but there is no demonstrated need. Integer-only operators remain available in integer constant expressions. == Named Constants as Static-Array Bounds === Decision Static-array bounds accept named constants and compile-time integer expressions, not only integer literals. The following forms are all valid: [source,pascal] ---- const N = 10; const Lo = 1; const Hi = 100; type TBuf = array[0..N-1] of Byte; { expression as bound } TArr = array[Lo..Hi] of Integer; { named constants as both bounds } var A: array[0..N] of Integer; { named constant as upper bound } const Days: array[0..N-1] of string = (...); { const-array declarations too } ---- === Rationale Before this change, all array bounds had to be integer literals. This forced users to duplicate magic numbers and prevented writing self-sizing array types based on named constants — a fundamental use case in systems programming. The parser now reads a constant expression (integer literals, named identifiers, and the arithmetic operators `+`, `-`, `*`, `div`, `mod`, `shl`, `shr` with parentheses) as each array bound, embedding the text in the type-name string. The semantic pass resolves each bound to a concrete integer: plain integers are converted directly, named constants are looked up in the symbol table, and expressions are parsed and folded via the existing `EvalConstIntExpr` machinery. === Alternatives considered * *Accept only bare named constants, not expressions.* Rejected. `array[0..N-1]` is the most common idiom; supporting `array[0..N]` but not `array[0..N-1]` would be a constant source of frustration. * *Resolve bounds at parse time.* Rejected. The parser runs before the semantic pass, so named constants are not yet resolved. Deferring to the semantic pass is the natural point where all constant values are available. == Enum-Indexed Static Arrays in Var and Type Declarations === Decision Static-array declarations in `var` and `type` sections accept an enumeration type as the index, matching the form already supported for const-array declarations: [source,pascal] ---- type TColor = (Red, Green, Blue); TColorNames = array[TColor] of string; { type declaration } var Costs: array[TColor] of Integer; { var declaration } ---- The array is sized `0..High(TColor)` (i.e. `0..Members.Count-1`), and subscript access uses the enum ordinal value directly. === Rationale Enum-indexed const arrays (`const Names: array[TColor] of string = (...)`) were already supported; extending the same syntax to `var` and `type` declarations completes the feature and eliminates the asymmetry. The codegen required no changes — enum values are integer ordinals and the subscript arithmetic already handles the zero-based static-array case correctly. The parser detects the `array[Ident]` pattern (identifier followed by `]` with no `..`) and encodes it as a special marker in the type-name string. The semantic pass resolves the enum type, reads its member count, and creates a standard `TStaticArrayTypeDesc` with bounds `0..N-1`. === Alternatives considered * *Support non-enum ordinal index types (`array[Byte]`, `array[Boolean]`).* Deferred. These require a different size-computation path (256 elements for `Byte`, 2 for `Boolean`) and the enum form covers the primary use case. * *Store the enum type in `TStaticArrayTypeDesc`.* Rejected. The type descriptor erases the index origin after semantic analysis, storing only integer bounds. This simplifies the codegen and is consistent with how const-array enum indexing already works. == Native Backend and Internal Toolchain as Default === Decision As of v0.12.0, the *native* x86-64 backend is the default code generator, and the in-process *internal assembler* (`blaise.assembler.x86_64`) and *internal linker* (`blaise.linker.elf`) are the default toolchain. A complete source-to-executable build invokes no external tools — only the host's C runtime startup objects (`crt1.o`, `crti.o`, `crtn.o` from the distribution's libc development package) are read from disk. The QBE backend (`--backend qbe`) is retained but *deprecated*. It continues to serve as a differential oracle for the native backend in the e2e test suite (every `AssertRunsOnAll` program is compiled and run under both backends, and their output is asserted equal), and the QBE self-hosting fixpoint (`scripts/fixpoint.sh`) remains a guard. QBE is scheduled for removal in a later release once the native backend has accumulated sufficient field exposure. === Rationale * *Toolchain independence.* The original pipeline shelled out to `qbe`, then `cc` to assemble, then `cc`/`ld` to link. Each is an external dependency a user must install and keep version-compatible. The native backend plus internal assembler and linker reduce the hard dependency to the C runtime startup objects alone, which simplifies installation and makes the compiler's output reproducible independent of the host toolchain version. * *Exact debug information.* Only the native backend records precise codegen facts (real frame-pointer offsets, per-statement line labels) consumed by the OPDF debugger. The QBE backend emits approximate AST-walk debug records. Making native the default aligns the default build with the debuggable build. * *Self-hosting closure.* The native pipeline reaches a clean self-hosting fixpoint (`scripts/fixpoint-native.sh` — stage-2 and stage-3 native assembly are byte-identical), so the compiler can rebuild itself end to end without QBE. === Alternatives considered * *Remove QBE immediately.* Rejected for v0.12.0. QBE's value as an independent second implementation — the parity oracle that surfaces native codegen bugs the single-backend tests cannot — is highest precisely while the native backend is still maturing. Removal is deferred to a later release. * *Keep QBE as the default and ship native opt-in.* Rejected. This was the prior state; it left the default build path different from the debuggable build path and kept the external-tool dependency on the common path. === Escape hatches The internal toolchain can be bypassed per invocation when diagnosing a suspected internal-assembler or internal-linker defect: * `--assembler external` — assemble the native `.s` with the host `cc` instead of the internal assembler. * `--linker external` — link with the host `cc`/`ld` instead of the internal linker. * `--backend qbe` — fall back to the QBE pipeline entirely (deprecated). The default native build emits a single whole-program object; `--incremental` opts in to per-unit object emission with a warm cache. == Decimal Arithmetic — One `TDecimal`, No `Currency` Primitive === Decision Exact decimal arithmetic is provided by a single StdLib type, `TDecimal` (`Numerics.Decimal`), not by a compiler primitive and not by a family of types. There is deliberately no fixed-point `Currency`/`Comp` type and no separate `BigDecimal`; the one `TDecimal` serves money, billing, tax and every other exact-decimal need. Scientific and engineering computation is explicitly out of scope for `TDecimal` and remains the domain of `Double` and the `Math` unit. `TDecimal` is a value-type record holding an unsigned magnitude, a signed scale, and a sign, with a compact `Int64` fast path that inflates to an arbitrary- precision decimal-digit magnitude only when a value exceeds `Int64` range. Equality is value-based: `2.0` and `2.00` are equal and hash identically. === Rationale The string-consolidation precedent applies directly. Just as Blaise replaced the `string`/`AnsiString`/`WideString`/`ShortString` family with one `string`, it replaces the `Currency`/`Comp`/`Extended`/`Real` numeric proliferation — which came from hardware limitations and legacy concerns that no longer apply — with one exact-decimal type plus the two float primitives (`Double`, `Single`). The single-type choice matches the dominant industry pattern. .NET (`decimal`), Swift (`Decimal`), Rust (`rust_decimal`) and Python (`decimal.Decimal`) each ship exactly one decimal type and treat "money" as decimal + a currency tag in a separate library layer. Java is the outlier in having no built-in money type at all (`java.util.Currency` is only an ISO-4217 descriptor); `BigDecimal` is its de-facto money type, and even Java has no second fixed type. A value-type record gives immutable value semantics without Java `BigDecimal`'s per-operation heap garbage: the common money case lives entirely in the inline `Int64` and allocates nothing, while still presenting a pure, immutable API. The design deliberately fixes the well-known `BigDecimal` pitfalls: value-based equality (so the type is safe in hash containers), a safe-by-default float constructor (`DecFromFloat` takes the shortest decimal; the binary-exact path is the explicitly-named `DecFromFloatExact`), mandatory explicit rounding on every division (no throw-on-`1/3` and no silent truncation), and plain-string formatting that never emits scientific notation. Rounding is injected through a layered design — a `TRoundingMode` enum for the eight standard modes (banker's rounding the default) over an `IRoundingStrategy` interface for custom algorithms — so jurisdiction-specific rules (e.g. cash rounding) need no change to the library. === Alternatives considered * *A fixed-point `Currency` type (Int64 scaled by 10000), as in Delphi/FPC.* Rejected as a separate type. Hard-coding four decimal places is a footgun (JPY has zero, crypto/FX want eight), and a hidden `*10000` representation invites scale confusion. Its one virtue — speed — is captured instead by the compact `Int64` fast path inside the single `TDecimal`, with no second type and no cross-type conversion friction. * *A separate arbitrary-precision `BigDecimal` alongside `Currency`.* Rejected. Two types double the API surface and force conversions at their boundary for no functional gain; one type with an internal fast path covers both roles. * *Making `TDecimal` cover scientific use too.* Rejected. Scientific work wants hardware floating point and transcendental functions over irrational results, where exact decimal representation is impossible and pointless. `Double` + `Math` already serve this; `TDecimal` ships no such functions. * *Baking currency (ISO-4217) into the numeric core.* Deferred to the `Numerics.Money` wrapper (see the next section). Currency is a policy layer (which currencies, how to format, per-jurisdiction rounding) that should not be frozen into the numeric type — matching Moneta / money-gem / rusty-money, which all wrap a decimal. == Currency Amounts — `TMoney` Wraps `TDecimal`, Currency Is a String Tag === Decision Currency-aware monetary amounts are provided by `TMoney` (`Numerics.Money`), a thin value-type wrapper that pairs an exact `TDecimal` amount with an ISO-4217 currency tag. The numeric core (`TDecimal`) stays currency- and locale-agnostic; currency policy lives entirely in this wrapper. Three rules define `TMoney`: * The currency tag is an ISO-4217 alphabetic *string* code ('USD', 'JPY', 'KWD'), upper-cased on construction. The set of currencies is open: a code the built-in registry does not list is still accepted and uses the fallback minor- unit scale of two. * Each currency has a default minor-unit scale (JPY 0, USD 2, KWD 3, fallback 2). Every `TMoney` is normalised to its currency's scale on construction and after every arithmetic operation. The default rounding is banker's (`rmHalfEven`), but it is not hard-wired: each constructor and rounding-sensitive operation has overloads taking an explicit `TRoundingMode` or a custom `IRoundingStrategy` (the same eight modes and extension point as `TDecimal`), so the rounding policy is swappable per call — e.g. `rmHalfUp` for a jurisdiction that rounds halves up, or an `IRoundingStrategy` implementing Swedish/Swiss 0.05 cash rounding. * Arithmetic across different currencies (`Add`, `Subtract`, `Compare`) raises `EMoneyMismatch`. There is no implicit conversion — exchange rates are a higher policy layer, not a property of the type. (`Equals` is the one exception: it returns `False` for different currencies rather than raising, so it is total and safe in containers.) `TMoney` has immutable value semantics: every operation returns a fresh value and never mutates `Self`, exactly as `TDecimal` does. === Rationale This is the universal industry pattern. Moneta (Java), the money gem (Ruby) and rusty-money (Rust) each wrap a decimal with a currency tag rather than baking currency into the numeric type. Keeping currency out of `TDecimal` preserves the "one exact-decimal type" decision (see the previous section) and keeps locale and jurisdiction policy — formatting, which currencies exist, per-currency rounding — in a layer that can evolve without touching the numeric core. A *string* currency code (rather than a closed `enum`) keeps the type open: new or non-standard codes (crypto, historical, regional) work without a library edit, matching money gem and rusty-money. The small loss of compile-time checking is the right trade for an inventory that changes outside the compiler's release cycle; an unknown code degrades gracefully to the fallback scale instead of failing to compile. Per-currency scale normalisation with banker's rounding on every operation makes `TMoney` behave like real money: a JPY amount never carries a fraction, a USD amount is always two places, and ties round without the upward bias of round-half-up. Doing it on *every* operation (not just on display) keeps stored values canonical, so equality and hashing are well-defined. === Alternatives considered * *A `TCurrency` enum instead of a string code.* Rejected. It is type-safe but closes the currency set: every new or non-standard code would require editing and re-releasing the library. The open string code matches the dominant money libraries and lets unknown codes degrade to the fallback scale. * *Preserving full `TDecimal` scale and rounding only on display.* Rejected as the default. Lazy rounding lets non-canonical values (e.g. an un-rounded division result) leak into stored amounts and breaks value equality between amounts that should be equal. Normalising eagerly keeps every `TMoney` at its currency scale. * *Baking exchange-rate conversion into `TMoney`.* Rejected. Rates are time-varying external data, not a property of an amount; mixing them in would make arithmetic depend on hidden state. Cross-currency arithmetic raises instead, leaving conversion to an explicit higher layer. * *A fixed-point money type (Int64 scaled).* Already rejected for `TDecimal` (see the previous section); `TMoney` inherits `TDecimal`'s compact `Int64` fast path for the common case, so it needs no separate fixed-point design. == Conditional Compilation — Symbol-Presence `{$IFDEF}`, Predefined `BLAISE` === Decision Blaise supports symbol-presence conditional compilation in the lexer: `{$DEFINE sym}`, `{$UNDEF sym}`, `{$IFDEF sym}`/`{$IFNDEF sym}` with an optional `{$ELSE}` and a closing `{$ENDIF}`, and a `-d`/`--define` command-line flag. Symbols are case-insensitive. The compiler always predefines `BLAISE` and the target's CPU/OS symbols (`CPUX86_64`, `CPUAMD64`, `LINUX`, `UNIX` on Linux/x86-64). There is deliberately no expression form (`{$IF expr}`) — only whether a symbol is defined. === Rationale The feature was held back on purpose (the project builds itself without it, delegating build-time configuration to PasBuild), but it is invaluable for *end-user* source that must compile under FPC, Delphi and Blaise from one file: `{$IFDEF BLAISE} ... {$ELSE} ... {$ENDIF}`. That cross-compiler use case is the whole point of predefining `BLAISE`, and the CPU/OS predefines mirror the FPC family so existing portable source compiles unchanged. This is the same "every mainstream native compiler has it" argument the language applies to parenthesised calls — conditional compilation is a near-universal expectation for native Pascal, even though it is absent from managed languages like Java. Implementing it in the lexer (not the parser) keeps excluded source from ever reaching the parser or semantic pass, matching FPC/Delphi and avoiding any interaction with type checking. === Alternatives considered * *No conditional compilation (status quo, rely on PasBuild).* Rejected for end-user code: writing a whole project with custom build paths to toggle one line is disproportionate, and it does not help a single source file shared across compilers. * *An expression form `{$IF declared(X) and (Y > 2)}`.* Deferred. It needs a constant-expression evaluator in the lexer and a `declared()` intrinsic; symbol-presence `{$IFDEF}` covers the cross-compiler and feature-flag cases that motivated the request. Can be added later without breaking `{$IFDEF}`. * *A version macro predefine (e.g. `BLAISE_0_12_0`).* Not added yet — there is no concrete use for it until the language stabilises; `BLAISE` alone serves the cross-compiler case. CPU/OS symbols are added because portable source routinely keys on them. == Qualified Type Names — `UnitName.TypeName` === Decision A type reference may be written with a unit qualifier, e.g. `SysUtils.TFormatSettings`, and the qualifier may itself be dotted, e.g. `System.SysUtils.TFormatSettings`. The parser reads the full dotted path; a type is always named by its *final* component, and the leading components name a unit already brought in by the `uses` clause. The qualifier is informational — it does not select between units at resolution time; the final identifier is resolved against the symbol table as usual. === Rationale Object Pascal source in the wild routinely qualifies type names with their declaring unit, both for documentation and to disambiguate when two used units export the same name. Rejecting `SysUtils.TFormatSettings` as a parse error blocked porting otherwise-portable FPC/Delphi code for no language benefit. Accepting the qualifier and resolving by the final component matches how FPC and Delphi treat a unit-qualified type in the common (non-ambiguous) case, while keeping resolution simple. === Alternatives considered * *Full unit-scoped resolution (the qualifier selects the unit).* Deferred. It requires per-unit symbol scoping at the resolution site and only matters when two used units export the same type name — rare, and the duplicate case can be resolved today by ordering the `uses` clause. Resolving by the final identifier covers the motivating real-world code without that machinery, and can be tightened later without changing the syntax. == Chained L-value Assignment — `Self.F[i].Sub := value` === Decision The left-hand side of an assignment may be a selector chain: after the first member, any sequence of `.Field`, `[Index]` (indexed-property or array-field element), and `.Method(...)` selectors, terminated by `:=`. For example `Self.FStack[0].Count := value`, `r.A[0] := 10`, and `obj.Items[i].Flag := True`. The bare implicit-`Self` field form (`A[0] := v` inside a method) follows the same rule. === Rationale The earlier parser accepted a single `.Field` (optionally one subscript) before `:=` but rejected a *chain* — `Self.FStack[0].Count := value` failed because the `Ident.Ident[Index]` path unconditionally expected `]:=` after the subscript. That is a routine and idiomatic Object Pascal l-value; rejecting it forced awkward temporaries and surprised anyone porting normal code. The chain loop after the subscript makes the read and write l-value grammars symmetric: anything that can be read as a designator can be assigned to. === Alternatives considered * *Require an intermediate variable (status quo).* Rejected — it is a gratuitous restriction with no semantic justification; the read side already supported the full chain, so write-side parity is the consistent choice.