Adopts the Swift/LLVM model: Apache License 2.0 with the Runtime Library Exception text used verbatim by the Swift project. SPDX identifier: Apache-2.0 WITH Swift-exception. Apache 2.0 brings an explicit patent grant and patent-retaliation clause that BSD-3 lacks. The Runtime Library Exception ensures binaries produced by the Blaise compiler do not inherit attribution obligations from the linked-in RTL. * LICENSE — full Apache 2.0 + RLE text (replaces the deleted LICENCE). * NOTICE — project header plus QBE attribution (MIT, vendored). * docs/language-rationale.adoc — new Project Governance section capturing the decision, alternatives considered, and rationale. * SPDX headers updated across all .pas, .pp, .inc, .c source files, build scripts, PasBuild plugins, and the Makefile. * project.xml license field updated. * tools/migrate_full.py HEADER template updated so generated self-hosting source carries the new licence.
1142 lines
44 KiB
Plaintext
1142 lines
44 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.
|
||
|
||
---
|
||
|
||
=== 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, an explicit `CodePointAt(S, N): Integer` function or a
|
||
`Runes(S)` iterator is the appropriate API. Neither is implemented yet.
|
||
|
||
==== 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.
|
||
|
||
==== Related
|
||
|
||
See <<_string_subscript_s_n>> and <<_char_literal_coercion>>.
|
||
|
||
---
|
||
|
||
=== String Subscript S[N]
|
||
|
||
==== Decision
|
||
|
||
`S[N]` returns the Nth byte of the string (1-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.
|
||
|
||
==== What is not supported
|
||
|
||
Codepoint-level indexing is not supported via subscript syntax. It is
|
||
deferred to a future `CodePointAt(S, N)` function or `Runes(S)` iterator.
|
||
These will be O(N) in the codepoint index because UTF-8 is variable-width.
|
||
|
||
==== 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.
|
||
|
||
---
|
||
|
||
=== 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 three integer types with fixed, platform-independent widths:
|
||
|
||
[cols="1,1,2", options="header"]
|
||
|===
|
||
| Type | Width | QBE IR type
|
||
|
||
| `Integer` | 32-bit signed | `w`
|
||
| `Int64` | 64-bit signed | `l`
|
||
| `Byte` | 8-bit unsigned (stored as 32-bit in registers) | `w`
|
||
|===
|
||
|
||
`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.
|
||
|
||
`Byte` is 8-bit unsigned. In registers and temporaries it is widened to
|
||
`w` (32-bit QBE word). Array element allocation uses 1 byte per `Byte`
|
||
element, consistent with FPC.
|
||
|
||
==== 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`, matching FPC's overloaded resolution
|
||
| `Int64ToStr` | `(Int64): string` | 64-bit 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 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`, `SmallInt`, `Word`, `Cardinal` aliases* — deferred. These
|
||
FPC/Delphi aliases map to fixed widths and could be added as type aliases
|
||
in a future release. They are not present in the current implementation
|
||
because the compiler's own source uses only `Integer`, `Int64`, and `Byte`.
|
||
|
||
---
|
||
|
||
=== 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.
|
||
|
||
---
|
||
|
||
=== 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 a
|
||
`weak` reference mechanism (analogous to `weak` in Swift) to break cycles.
|
||
Identifying cycles in a codebase is the principal migration cost when
|
||
porting from manual-free FPC code.
|
||
|
||
==== 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).
|
||
|
||
==== 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. The distinction is
|
||
purely a static-analysis / documentation concern. For the current
|
||
implementation phase the distinction is not enforced; both produce
|
||
identical QBE IR.
|
||
|
||
==== Future work
|
||
|
||
A future lint pass could warn when an `out` parameter is read before it
|
||
has been assigned within the callee.
|
||
|
||
---
|
||
|
||
=== 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).
|
||
|
||
---
|
||
|
||
=== 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`/`String`/pointer types. This differs from the standalone-variable
|
||
allocation size (which pads `Byte` to 4 for alignment), 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[Word] of T` — ordinal-type index
|
||
| Deferred
|
||
|
||
| `array[L..H, M..N] of T` — multi-dimensional
|
||
| Deferred
|
||
|
||
| `array of array of T` — multi-dim open array
|
||
| Separate feature
|
||
|===
|
||
|
||
==== 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`)
|
||
|
||
==== Decision
|
||
|
||
Blaise supports Pascal bit-set types declared as `set of EnumType`, where
|
||
`EnumType` must be a previously-defined enumeration. A set variable holds a
|
||
bitmask in which bit N is set when the enum member with ordinal N is included.
|
||
|
||
==== Storage model
|
||
|
||
- ≤ 32 enum members: stored as a single QBE `w` (32-bit word).
|
||
- 33–64 members: stored as a single QBE `l` (64-bit long).
|
||
- More than 64 members: deferred (no use case in the current codebase).
|
||
|
||
Sets are value types (copied on assignment), not heap-allocated.
|
||
|
||
==== Set literals and disambiguation
|
||
|
||
Set literals use the same `[elem, ...]` bracket notation as array literals.
|
||
The semantic pass resolves the ambiguity by examining the assignment target
|
||
type: when the LHS is a set type, the bracket expression is analysed as a set
|
||
literal; otherwise it is treated as an array literal.
|
||
|
||
An empty literal `[]` is valid only in a set context (the empty set, bitmask 0).
|
||
In an array context, `[]` remains a semantic error (element type cannot be
|
||
inferred without type-context propagation).
|
||
|
||
==== 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.
|
||
|
||
==== 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.
|
||
|
||
---
|
||
|
||
== 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_.
|
||
|
||
==== What is not yet supported
|
||
|
||
* Iterating over sets (`for X in [a, b, c] do`) — deferred.
|
||
* Generic collection iteration (`for X in List do` where `List: TList<T>`) —
|
||
requires Step 9b: property handling in `InstantiateGeneric` plus enumerator
|
||
additions to `generics.collections.pas`.
|
||
|
||
==== 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.
|
||
|
||
---
|
||
|
||
== 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.
|
||
|
||
---
|
||
|
||
== 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.
|