TMoney is a thin value-type wrapper layering an ISO-4217 currency tag onto an exact TDecimal amount. The numeric core (TDecimal) stays currency- and locale-agnostic; currency policy lives entirely in this wrapper, matching Moneta / money-gem / rusty-money. Design: - Currency is an upper-cased ISO-4217 string code; the set is open (unknown codes are accepted at the fallback minor-unit scale of 2). - A built-in registry gives each currency its default scale (JPY 0, USD 2, KWD 3, fallback 2); every TMoney is normalised to its currency's scale on construction and after every operation, using banker's rounding. - Cross-currency Add/Subtract/Compare raise EMoneyMismatch (no implicit conversion); Equals is total (False, not raise, across currencies). - Immutable value semantics, mirroring TDecimal. API: free-function constructors (MoneyFromStr / MoneyFromDecimal / MoneyFromInt / MoneyZero), record methods (Amount, CurrencyCode, IsZero, Sign, Add, Subtract, Negate, Multiply, MultiplyInt, Compare, Equals, AmountString, ToString), and the CurrencyScale registry function. Tests: - cp.test.numerics.money.pas: 22 IR/semantic tests (resolution, IR shape, type errors) via TUnitLoader. - cp.test.e2e.numerics.money.pas: 24 dual-backend e2e tests (CompileAndRunWithRTL) covering construction + per-currency normalisation, banker's rounding, case-folding, arithmetic, mismatch raising, Compare/Equals/IsZero/Sign, the registry, and a realistic invoice flow. Docs: language-rationale.adoc gains "Currency Amounts — TMoney Wraps TDecimal, Currency Is a String Tag" (decision + alternatives); future-improvements.adoc marks Numerics.Money as implemented. Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite green on both the QBE-built and native-built runners (3671 tests).
5103 lines
211 KiB
Plaintext
5103 lines
211 KiB
Plaintext
= 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] := <byte>`
|
||
|
||
`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 <dir>` (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<path>`
|
||
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 <enum>` 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 <Enum>`. 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<Integer>`) — 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<Name>` | 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<arity>`) 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_<Name>` 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: <type-name>): Boolean;
|
||
function Supports(Obj: TObject; IntfType: <type-name>; 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 `<K, V>` syntax as generic classes:
|
||
|
||
[source,pascal]
|
||
----
|
||
type
|
||
IMap<K, V> = 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<K, V> = class(IMap<K, V>)
|
||
...
|
||
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<T>` | Ordered, growable list
|
||
| `TStack<T>` | LIFO stack
|
||
| `TQueue<T>` | FIFO queue
|
||
| `TSet<T>` | Unordered membership set
|
||
| `TDictionary<K,V>` | Key→value map (unordered)
|
||
| `TOrderedDictionary<K,V>` | Key→value map (insertion-ordered)
|
||
| `IMap<K,V>` | Map interface abstracting both dictionary types
|
||
|===
|
||
|
||
=== Rationale
|
||
|
||
* *Interface abstraction over concrete types* — `IMap<K,V>` 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<Integer,Integer>` and
|
||
`IMap<String,Integer>` 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<K,V> = class(IMap<K,V>)`),
|
||
`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<Integer,Integer> := TDictionary<Integer,Integer>.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<Integer>)`. 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<T>` instead of native dynamic arrays* — `TList<T>` 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<K,V>` +
|
||
`TDictionary<K,V>` 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<string>;
|
||
S := TStack<>.Create; // equivalent to TStack<string>.Create
|
||
|
||
var D: TDictionary<string, Integer>;
|
||
D := TDictionary<>.Create; // infers both K and V from the LHS
|
||
----
|
||
|
||
The operator works for any number of type parameters — single-argument generics
|
||
(`TStack<T>`, `TList<T>`) and multi-argument generics (`TDictionary<K,V>`,
|
||
`TPair<K,V>`) 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<string>.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 `<T>` syntax as generic classes
|
||
and interfaces:
|
||
|
||
[source,pascal]
|
||
----
|
||
type
|
||
TMyVal<T> = record
|
||
Value: T;
|
||
function GetValue: T;
|
||
begin
|
||
Result := Self.Value
|
||
end;
|
||
end;
|
||
|
||
var
|
||
V: TMyVal<Integer>;
|
||
begin
|
||
V.Value := 42;
|
||
WriteLn(V.GetValue());
|
||
end.
|
||
----
|
||
|
||
Generic records are monomorphized exactly like generic classes: each distinct
|
||
instantiation (`TMyVal<Integer>`, `TMyVal<string>`, 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_<T>`)
|
||
* Vtable data (`$vtable_<T>`)
|
||
* Field cleanup functions (`$_FieldCleanup_<T>`)
|
||
* 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<T>(cond: Boolean; a, b: T): T;
|
||
begin if cond then Result := a else Result := b end;
|
||
end;
|
||
...
|
||
u.Pick<Integer>(True, 7, 9); // -> 7
|
||
u.Pick<string>(False, 'a', 'b'); // -> 'b'
|
||
----
|
||
|
||
The method is *monomorphised* on first use: each distinct set of explicit type
|
||
arguments at a call site produces one concrete method body, named
|
||
`<Owner>_<Method>_<Args>` (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<Integer>(...)`); they are not inferred from the value arguments.
|
||
|
||
This is distinct from a method of a generic *class* (`TBox<T>` 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 `<T>` 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<T>(...)` 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<T>`, `TStack<T>`, `TQueue<T>` 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 <Name>` (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, using banker's rounding (`rmHalfEven`).
|
||
|
||
* 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.
|