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).
1018 lines
36 KiB
Plaintext
1018 lines
36 KiB
Plaintext
= Blaise Compiler — Future Improvements
|
||
:toc:
|
||
:toc-placement: preamble
|
||
:sectnums:
|
||
|
||
This document records optimisation opportunities, quality-of-life improvements,
|
||
and language enhancements that are worth pursuing but are deliberately deferred
|
||
from the current implementation phase. Each entry states what the improvement is,
|
||
why it matters, and any known implementation notes.
|
||
|
||
Items here are not committed to any release milestone. They exist so that good
|
||
ideas are not lost and so that the rationale is available when the time comes to
|
||
prioritise them.
|
||
|
||
== Generics
|
||
|
||
- TStringList ← TList<String>: Generic class inheritance. Defer until generics
|
||
inheritance is implemented and well tested.
|
||
- Generic class inheritance (class(TList<String>)): Parser reads the
|
||
parent name as a string, but semantic analysis never resolves a generic
|
||
instantiation as a parent — it needs template lookup + monomorphisation.
|
||
Moderate-significant effort.
|
||
|
||
- TComponent mixes a raw pointer array with ARC, which is inherently delicate. A cleaner long-term design would store children
|
||
in TList<TComponent> (per the project's "use Generics.Collections" policy) so the list itself holds ARC refs and the manual
|
||
_ClassAddRef/_ClassRelease bookkeeping disappears entirely. That's a larger refactor than fixing the red suite, so I kept it
|
||
minimal — but it's the right direction if you want to revisit classes.pas.
|
||
|
||
|
||
== Language
|
||
|
||
=== Procedural types
|
||
|
||
- reference to function/procedure (anonymous methods)
|
||
- cdecl/stdcall calling-convention markers on procedural types
|
||
|
||
=== Method level attributes
|
||
|
||
Blaise only has class-level attribute RTTI today — there's no per-method attribute reflection, and adding it is a layout-changing compiler change (new method-table slot, stage-2 rebuild).
|
||
|
||
=== If Conditional Operator (Ternary Conditional Operator)
|
||
|
||
More details here: https://en.wikipedia.org/wiki/Ternary_conditional_operator
|
||
|
||
Many languages use the ?: syntax as follows:
|
||
|
||
`result = condition ? value_if_true : value_if_false`
|
||
|
||
But that's not very pascal-like. Oxygene language has implemented this years
|
||
ago with te following syntax:
|
||
|
||
For instance
|
||
|
||
[source]
|
||
----
|
||
var s := if (a<>0) then Sin(1/a) else 1;
|
||
----
|
||
|
||
being equivalent to
|
||
|
||
[source,pascal]
|
||
----
|
||
var s : Float;
|
||
if (a<>0) then
|
||
s := Sin(1/a)
|
||
else s := 1;
|
||
----
|
||
|
||
So for Blaise we could do similar:
|
||
|
||
X := if Left < 100 then 22 else 45;
|
||
// the operator can be used as part of any expression, as follows:
|
||
ShowMessage(if Left < 100 then 'Small' else 'Big');
|
||
|
||
NOTE: If the two types do not match and are incompatible, the compiler should issue an error message
|
||
|
||
NOTE: It is important to note that the if-then-else expression differs from the if-then-else statement, the expression has a single type. When the type of the then and else expressions differ, the expression's type is the common type of the two expressions' types; also known as the Least Upper Bound (LUB) type.
|
||
|
||
See also Delphi's documentation: https://docwiki.embarcadero.com/RADStudio/Florence/en/Conditional_Operators_(Delphi)
|
||
|
||
|
||
== Language — Features Required to Complete the punit Port
|
||
|
||
The following features were identified as missing from Blaise while porting
|
||
`/data/devel/fpc-3.3.1/src/packages/fcl-base/tests/punit.pp` to
|
||
`rtl/src/test/pascal/punit.pas`. Each one has a concrete workaround in the
|
||
current port, noted below, so none is a blocker; they are collected here so
|
||
they can be prioritised alongside other language work.
|
||
|
||
|
||
=== `Str()` intrinsic — numeric to string conversion
|
||
|
||
*What.* The FPC/Pascal `Str(X, S)` intrinsic writes the string representation
|
||
of a numeric value `X` (integer or float) into the `var` string parameter `S`.
|
||
|
||
*Why it matters.* It is the traditional Pascal way to convert a number to a
|
||
string without pulling in `SysUtils`. Used throughout punit's `SysTimeString`
|
||
helper and the float assertion overloads.
|
||
|
||
*Current workaround in punit.* Integer conversions replaced with `IntToStr()`
|
||
and `Int64ToStr()`. Float→string conversions use `DoubleToStr()` /
|
||
`SingleToStr()`. The FPC-style `Str(X, S)` var-parameter form is still absent.
|
||
|
||
*Implementation notes.* `Str` is a thin wrapper: for integers, alias
|
||
`IntToStr` / `Int64ToStr`; for floats, alias `DoubleToStr` / `SingleToStr`.
|
||
The `var`-parameter assignment can be emitted as a string copy into `S`.
|
||
|
||
*Effort.* Trivial.
|
||
|
||
=== `New()` and `Dispose()` built-ins for typed pointers
|
||
|
||
*What.* `New(P)` allocates `SizeOf(P^)` bytes and returns a typed pointer;
|
||
`Dispose(P)` frees it. Both are traditional Pascal alternatives to manual
|
||
`GetMem` / `FreeMem` with explicit `SizeOf`.
|
||
|
||
*Why it matters.* In the original punit every record allocation uses `New` and
|
||
every deallocation uses `Dispose`. Without them the port must call
|
||
`GetMem(SizeOf(TFoo))` and `ZeroMem` manually, and the developer must remember
|
||
to `FreeMem` at every exit path.
|
||
|
||
*Current workaround in punit.* All uses replaced with `GetMem(SizeOf(T))` +
|
||
`ZeroMem(ptr, SizeOf(T))` + `FreeMem(ptr)`.
|
||
|
||
*Implementation notes.* `New(P)` is a thin wrapper over `GetMem(SizeOf(P^))`
|
||
with the type deduced from the pointer's declared type at the call site.
|
||
`Dispose(P)` calls `FreeMem(P)` after invoking the destructor if the pointed-to
|
||
type is a class (for record pointers, no destructor call is needed under ARC
|
||
provided string fields have been properly finalised first).
|
||
|
||
*Effort.* Small — semantic pass extracts element type, codegen emits the
|
||
appropriate `malloc` / `free` sequence.
|
||
|
||
=== `Default(T)` expression
|
||
|
||
*What.* `Default(T)` returns the zero value for any type `T` — zero for
|
||
integers, empty string for strings, nil for pointers and classes, and so on.
|
||
Commonly used to reset a record variable to its initial state.
|
||
|
||
*Why it matters.* In the original punit it is used to re-initialise
|
||
`TResultRecord` and `TRunSummary` records before each test run.
|
||
|
||
*Current workaround in punit.* Replaced with `ZeroMem(@rec, SizeOf(rec))`.
|
||
This is correct for records whose managed fields (strings, interface refs) are
|
||
already nil, but for a record mid-lifetime it bypasses ARC cleanup of any
|
||
non-nil string fields. For punit's specific use (zeroing a freshly allocated
|
||
or just-finalised record) this is safe; for general use it is not.
|
||
|
||
*Implementation notes.* The semantic pass resolves the type argument; the
|
||
codegen emits a `ZeroMem` (or a field-by-field assignment for managed types
|
||
where ARC release must precede zeroing).
|
||
|
||
*Effort.* Small for unmanaged types; moderate for records containing managed
|
||
fields (requires field-by-field ARC release before memset).
|
||
|
||
=== `Finalize()` procedure for managed records
|
||
|
||
*What.* `Finalize(V)` releases all ARC-managed fields inside a record variable
|
||
`V` (strings, interface references, object references) without freeing the
|
||
record's own memory. Used before `FreeMem` when the record was heap-allocated.
|
||
|
||
*Why it matters.* In the original punit it is called on `TResultRecord^` before
|
||
`FreeMem` to release the `TestMessage: string` field. Without it, heap-
|
||
allocated records that contain strings leak the string's reference count.
|
||
|
||
*Current workaround in punit.* The `Finalize` calls were dropped. In Blaise,
|
||
ARC on assfignment should automatically manage string fields inside records, so
|
||
the string is released when the record's string field is overwritten or when the
|
||
last reference to the containing heap block is released. The correctness of
|
||
this assumption depends on the ARC implementation for record fields — if the
|
||
codegen does not insert `_StringRelease` when a record pointer is freed via
|
||
`FreeMem`, there will be a string leak.
|
||
|
||
*Implementation notes.* `Finalize(V)` is a compiler built-in that walks the
|
||
fields of the record type and emits the appropriate ARC release calls for each
|
||
managed field. The implementation mirrors what the codegen already does for
|
||
local record variables at scope exit.
|
||
|
||
*Effort.* Small to moderate — shares infrastructure with ARC scope-exit emit.
|
||
|
||
|
||
== StdLib Packages — Numeric Types
|
||
|
||
The exact-decimal type (`TDecimal` in `Numerics.Decimal`) and the currency-aware
|
||
wrapper (`TMoney` in `Numerics.Money`) are both implemented; the design
|
||
decisions are recorded in `docs/language-rationale.adoc` ("Decimal Arithmetic —
|
||
One `TDecimal`, No `Currency` Primitive" and "Currency Amounts — `TMoney` Wraps
|
||
`TDecimal`, Currency Is a String Tag"). The following numeric items remain
|
||
genuinely future work.
|
||
|
||
=== Operator overloading for `TDecimal`
|
||
|
||
`TDecimal` currently exposes arithmetic as named methods (`A.Add(B)`,
|
||
`A.Multiply(B)`, `A.Divide(B, Scale, Mode)`) because operator overloading is not
|
||
yet a language feature. Once it lands, `A + B` / `A * B` / comparisons can layer
|
||
on additively (division stays a named method — it requires an explicit scale and
|
||
rounding mode). Operator overloading is a compiler feature in its own right and
|
||
warrants a separate design document.
|
||
|
||
=== Migration-analyser rule for legacy numeric types
|
||
|
||
The migration analyser should flag FPC/Delphi `Currency` and `Comp` and suggest
|
||
`TDecimal`; `Extended` maps to `Double` with a precision-loss warning.
|
||
|
||
== StdLib — String operations
|
||
|
||
=== Regular-expression search and replace
|
||
|
||
`StrUtils` provides literal (non-pattern) replacement only: `Replace` (first
|
||
occurrence) and `ReplaceAll` (every occurrence), both case-sensitive. A
|
||
pattern-based API — `regex` search, `ReplaceMatches`/`ReplaceFirstMatch`, and
|
||
capture-group substitution — is deliberately deferred because it requires a
|
||
regular-expression engine (parser + NFA/DFA matcher) that does not yet exist
|
||
in the stdlib.
|
||
|
||
*Why deferred.* A correct, reasonably performant regex engine is a
|
||
substantial component in its own right. The literal `Replace`/`ReplaceAll`
|
||
functions cover the common cases without it.
|
||
|
||
*Current workarounds.*
|
||
|
||
- Case-insensitive replace: lower-case the inputs with the built-in
|
||
`LowerCase` (or `UpperCase`) before calling `Replace`/`ReplaceAll`. Note
|
||
that this also folds the case of the surrounding text in the result; a
|
||
case-*preserving* insensitive replace is a regex-engine feature.
|
||
- Fixed multi-pattern replacement: chain `ReplaceAll` calls.
|
||
|
||
*Effort.* Large — a regex engine is the bulk of the work; the replace API on
|
||
top of it is small.
|
||
|
||
== macOS — Debugging and Code-Signing
|
||
|
||
=== Debugger entitlement requirements
|
||
|
||
*What.* On macOS, a debugger binary must carry the
|
||
`com.apple.security.get-task-allow` entitlement (and, post-SIP, the binary
|
||
it attaches to must carry the same entitlement) before the kernel permits
|
||
`task_for_pid`. The Blaise debugger will need a documented signing workflow
|
||
for macOS targets.
|
||
|
||
*Why it matters.* Without the entitlement, `ptrace` and Mach exception
|
||
ports are blocked by the kernel even for self-compiled binaries. Open-source
|
||
users who cannot obtain an Apple Developer certificate ($99/year) need a
|
||
supported path.
|
||
|
||
*Recommended approach.*
|
||
|
||
. *Ad-hoc signing* — the user self-signs with a locally generated certificate.
|
||
Works for development; the compiler should emit a post-build `codesign`
|
||
invocation for debug configurations.
|
||
|
||
. *Debug-server architecture* — ship a small, separately signed stub
|
||
(`blaisedbgserver`) that handles `task_for_pid` and communicates over a
|
||
local socket. The main debugger frontend requires no special entitlement.
|
||
This mirrors LLDB’s `debugserver` design and avoids per-binary signing of
|
||
every compiled Pascal program.
|
||
|
||
. *SIP caveat* — SIP blocks attachment to Apple-signed system processes.
|
||
For user-compiled Pascal binaries, SIP is not an obstacle provided the
|
||
binary carries `get-task-allow`.
|
||
|
||
*Debug-format note.* The Blaise compiler’s own debug information format
|
||
(OPDF or equivalent) avoids dependency on Mach-O `dSYM` bundles and
|
||
Apple’s `dsymutil`. This is the correct design: DWARF/dSYM integration adds
|
||
fragile toolchain coupling without benefit for a self-hosted debugger.
|
||
|
||
*Effort.* Small (documentation + build-system integration for `codesign`);
|
||
moderate for the debug-server architecture.
|
||
|
||
== Concurrency
|
||
|
||
=== Threading and modern concurrency models
|
||
|
||
*What.* Blaise needs a concurrency story. The plan is a layered approach:
|
||
a compatibility layer for existing Pascal code, and a modern concurrency
|
||
model that makes Blaise worth choosing over FPC/Delphi for concurrent
|
||
workloads.
|
||
|
||
*Layer 1 — TThread (Delphi/FPC compatibility).*
|
||
|
||
Thin wrapper around `pthread_create` (POSIX) or `CreateThread` (Win32).
|
||
Required for porting existing Pascal codebases. Provides the familiar
|
||
`TThread.Create`, `Execute` override, `Synchronize`, `WaitFor`, and
|
||
`Terminate` / `Terminated` pattern. Not inspiring, but necessary for
|
||
ecosystem compatibility.
|
||
|
||
**Important ARC consideration for TThread:**
|
||
Blaise's automatic reference counting introduces a subtle but critical
|
||
interaction with thread lifetimes. The naive fire-and-forget pattern:
|
||
|
||
[source,pascal]
|
||
----
|
||
var T: TMyThread;
|
||
begin
|
||
T := TMyThread.Create;
|
||
T.FreeOnTerminate := True;
|
||
T.Start;
|
||
end; // <- T is released here; refcount → 0; object freed while thread runs
|
||
----
|
||
|
||
...is unsafe under ARC. When the local variable `T` goes out of scope,
|
||
ARC immediately decrements the refcount. If this is the last reference,
|
||
the object is freed whilst the thread is still executing, causing a
|
||
use-after-free or crash.
|
||
|
||
The recommended solution is **self-referential threads**: each TThread
|
||
increments its own refcount during `Execute` and decrements it during
|
||
finalisation, keeping itself alive for the duration of its work. This
|
||
mirrors modern threading APIs (Rust's `Arc<Thread>`, Go's goroutines).
|
||
|
||
Implementation sketch:
|
||
|
||
[source,pascal]
|
||
----
|
||
procedure TThread.Execute;
|
||
begin
|
||
_ClassAddRef(Self); { Keep self alive during execution }
|
||
try
|
||
// User's Execute override (via virtual dispatch)
|
||
Self.UserExecute;
|
||
finally
|
||
_ClassRelease(Self); { Release self; if FreeOnTerminate, frees object }
|
||
end;
|
||
end;
|
||
----
|
||
|
||
This way the caller can write idiomatic fire-and-forget code and the
|
||
thread automatically cleans itself up once it finishes:
|
||
|
||
[source,pascal]
|
||
----
|
||
var T: TMyThread;
|
||
begin
|
||
T := TMyThread.Create;
|
||
T.FreeOnTerminate := True;
|
||
T.Start; // Thread increments self-refcount in Execute
|
||
end; // T's refcount drops, but thread keeps itself alive
|
||
// Thread finalizer decrements self-refcount, freeing the object
|
||
----
|
||
|
||
*Layer 2 — Modern structured concurrency.*
|
||
|
||
The industry has converged on a few proven models:
|
||
|
||
* *Structured concurrency with async/await* (C#, Rust, JavaScript, Swift,
|
||
Kotlin) — the compiler transforms `async` functions into state machines.
|
||
Works well with I/O-bound code. Requires language-level support in the
|
||
parser and codegen but no complex runtime scheduler.
|
||
|
||
* *Virtual threads / green threads* (Java 21, Go goroutines, Erlang
|
||
processes) — the runtime multiplexes many lightweight threads onto a
|
||
small pool of OS threads. User code looks synchronous. Requires a
|
||
scheduler in the RTL, which is a larger investment.
|
||
|
||
* *Structured task groups* (Swift `TaskGroup`, Java `StructuredTaskScope`)
|
||
— tasks have parent-child relationships with automatic cancellation
|
||
propagation and scope-bound lifetimes. Prevents leaked tasks by
|
||
construction.
|
||
|
||
**Comparison: async/await vs TThread.**
|
||
|
||
[%header, cols="2,5,5"]
|
||
|===
|
||
| Aspect | TThread | Async/Await
|
||
| Concurrency model | OS threads (preemptive) | Cooperative coroutines (event loop)
|
||
| Stack per task | ~1 MB per thread | Minimal; thousands in ~few MB
|
||
| Scheduling | OS kernel; true parallelism on multicore | Single event loop; one task at a time
|
||
| Blocking I/O | Blocks thread; wasteful | Suspends task; loop handles others
|
||
| Data sharing | Requires locks/atomics (races, deadlocks) | Inherently safe; only one coroutine active
|
||
| Cancellation | Difficult (cooperative flags) | Natural (`cancel()` the task)
|
||
| CPU-bound work | Natural fit | Poor fit (no parallelism)
|
||
| Common use | Fire-and-forget background workers | Server handling thousands of clients
|
||
|===
|
||
|
||
The recommended path for Blaise:
|
||
|
||
. Start with `TThread` for compatibility, using self-referential semantics
|
||
to protect thread lifetime under ARC.
|
||
. Add `async`/`await` as the primary modern concurrency primitive for
|
||
I/O-bound workloads. This is the most compiler-friendly approach —
|
||
it is a source-to-source transform during semantic analysis, turning
|
||
async functions into state machine classes. It does not require a
|
||
complex runtime scheduler.
|
||
. Layer structured task groups on top of async/await for fork-join
|
||
parallelism with automatic cancellation.
|
||
. Consider virtual threads as a long-term option if the runtime grows to
|
||
support a user-mode scheduler (significant investment).
|
||
|
||
*Design considerations.*
|
||
|
||
* ARC interaction — both TThread (self-referential) and async/await handle
|
||
lifetime automatically. For async tasks, captured variables are
|
||
referenced; the ARC system must ensure they outlive the task.
|
||
Structured concurrency helps here because task lifetimes are lexically
|
||
scoped.
|
||
* Async/await state machine — the compiler transforms `async` function
|
||
bodies into state-machine classes during semantic analysis. Each
|
||
`await` point becomes a state transition. The state machine holds
|
||
captured variables and a continuation pointer. No runtime scheduler or
|
||
coroutine primitives needed — just ordinary function calls and ARC.
|
||
* QBE constraints — QBE has no coroutine or `setjmp`-based context
|
||
switching primitives. `async`/`await` via state-machine transform
|
||
does not require these — the compiler generates ordinary function calls
|
||
between states. Green threads would require platform-specific context
|
||
switching (`swapcontext` or hand-rolled assembly), which is harder to
|
||
express through QBE.
|
||
* Exception propagation — async tasks need a defined exception model.
|
||
Structured concurrency naturally provides this: exceptions propagate
|
||
to the parent scope and cancel sibling tasks.
|
||
|
||
*Effort.*
|
||
|
||
* TThread self-referential semantics: Small (2–3 days; modifications to
|
||
RTL's TThread base class and entry point).
|
||
* Async/await: Large. This is a significant compiler feature (parser,
|
||
semantic, codegen transforms) that warrants its own design document and
|
||
phased implementation. Estimated 4–6 weeks post-v0.6.0.
|
||
|
||
== Migration Analyser
|
||
|
||
=== Flag `TObjectList` usage and recommend `TList<TObject>`
|
||
|
||
*What.* The migration analyser must detect uses of `TObjectList` from the
|
||
FPC/Delphi `contnrs` unit and recommend the generic `TList<TObject>` from
|
||
`Generics.Collections` as the idiomatic Blaise replacement.
|
||
|
||
*Why it matters.* `TObjectList` is a pre-generics workaround — a pointer
|
||
list that stores `TObject` references without type safety. In the generics
|
||
era, `TList<TObject>` provides the same capability with full type checking.
|
||
In modern languages (Go, Swift, Kotlin, C#) the untyped container pattern
|
||
is considered obsolete; Blaise should steer new code toward the typed form.
|
||
|
||
*Patterns to detect.*
|
||
|
||
[cols="1,2", options="header"]
|
||
|===
|
||
| Pattern | Recommended replacement
|
||
|
||
| `uses Contnrs; ... TObjectList`
|
||
| `uses Generics.Collections; ... TList<TObject>`
|
||
|
||
| `TObjectList.Create(True)` (owned)
|
||
| `TList<TObject>.Create` (ARC handles ownership automatically)
|
||
|
||
| `TObjectList.Create(False)` (unowned)
|
||
| `TList<TObject>.Create` (same — ARC does not double-free)
|
||
|===
|
||
|
||
*Implementation notes.*
|
||
|
||
. Detect `TObjectList` type references in variable declarations, field
|
||
declarations, and `Create` calls.
|
||
. Report each occurrence with the source location and the suggested rewrite.
|
||
. Note that the ownership flag (`AOwnsObjects`) has no equivalent in
|
||
`TList<TObject>` — ARC manages object lifetimes automatically, so the
|
||
flag is meaningless in Blaise. The migration note should explain this.
|
||
|
||
*Effort.* Small — one pattern rule in the type-reference checker.
|
||
|
||
=== Detect 1-based string indexing patterns from FPC/Delphi code
|
||
|
||
*What.* The migration analyser (`tools/migration-analyser`) must detect
|
||
and flag patterns in FPC/Delphi source that rely on 1-based string
|
||
semantics, since Blaise strings are 0-based (see
|
||
`docs/language-rationale.adoc`, section "0-Based String Indexing").
|
||
|
||
*Why it matters.* Ported code that uses 1-based idioms will compile
|
||
without error but produce wrong results silently — off-by-one character
|
||
accesses, missed `Pos` not-found checks, and incorrect `Copy` start
|
||
positions. These are among the most insidious categories of migration
|
||
bugs because they produce plausible-looking output most of the time and
|
||
only misbehave on edge cases.
|
||
|
||
*Patterns to detect.*
|
||
|
||
[cols="1,2,2", options="header"]
|
||
|===
|
||
| Pattern | FPC/Delphi meaning | Blaise equivalent
|
||
|
||
| `S[1]`
|
||
| First character
|
||
| `S[0]`
|
||
|
||
| `S[N]` where `N` is a literal ≥ 1
|
||
| Character at 1-based position N
|
||
| `S[N-1]`
|
||
|
||
| `Copy(S, 1, N)`
|
||
| First N characters
|
||
| `Copy(S, 0, N)`
|
||
|
||
| `Copy(S, K, N)` where `K` is a literal ≥ 2
|
||
| Substring starting at 1-based K
|
||
| `Copy(S, K-1, N)`
|
||
|
||
| `Copy(S, K, MaxInt)` where K ≥ 2
|
||
| Tail from 1-based K
|
||
| `Copy(S, K-1, MaxInt)`
|
||
|
||
| `Pos(Sub, S) > 0`
|
||
| Substring found
|
||
| `Pos(Sub, S) >= 0`
|
||
|
||
| `Pos(Sub, S) = 0`
|
||
| Substring not found
|
||
| `Pos(Sub, S) < 0`
|
||
|
||
| `if Pos(Sub, S) > 0 then ... Pos(Sub, S)` (double call)
|
||
| Find and use position
|
||
| Assign to variable first; compare `>= 0`; use variable as 0-based index
|
||
|
||
| `PosEx(Sub, S, K)` where `K` is a literal ≥ 2
|
||
| Search from 1-based K
|
||
| `PosEx(Sub, S, K-1)`
|
||
|
||
| `OrdAt(S, K)` where `K` is a literal ≥ 1
|
||
| Ordinal at 1-based K (FPC-defined helper)
|
||
| `OrdAt(S, K-1)`
|
||
|
||
| `for I := 1 to Length(S) do ... S[I]`
|
||
| Iterate characters 1-based
|
||
| `for I := 0 to Length(S) - 1 do ... S[I]`
|
||
|===
|
||
|
||
*Implementation path.*
|
||
|
||
. Add a string-index analysis pass to the analyser that walks expression
|
||
trees and identifies the patterns above.
|
||
. Report each occurrence with source location, the offending expression,
|
||
and the suggested rewrite.
|
||
. Special-case `Copy(S, 1, N)` — this is the most common false-positive
|
||
source (copying the whole string) so provide the `Copy(S, 0, N)` rewrite
|
||
without noise.
|
||
. The `Pos(...) > 0` → `>= 0` and `Pos(...) = 0` → `< 0` rewrites are
|
||
purely mechanical and could be offered as auto-apply fixes.
|
||
|
||
*Effort.* Medium — approximately one week to add the expression-walker and
|
||
pattern-matching rules, plus testing against a representative FPC
|
||
codebase.
|
||
|
||
=== Suggest `for B in S` as the idiomatic replacement for index-based string loops
|
||
|
||
*What.* When the migration analyser detects the pattern
|
||
`for I := 1 to Length(S) do ... S[I]` (or the 0-based equivalent
|
||
`for I := 0 to Length(S) - 1 do ... S[I]`), and the loop body only
|
||
accesses the character via `S[I]` without using `I` for any other
|
||
purpose, offer `for B in S do` as the preferred rewrite rather than a
|
||
mechanical index adjustment.
|
||
|
||
*Why it matters.* The index-based loop is a common FPC/Delphi idiom
|
||
that ports verbatim but reads less clearly than the enhanced for form.
|
||
Blaise supports `for B in S do` with `B: Byte` or `B: Integer`, which
|
||
eliminates the index entirely and makes the intent — "process every byte
|
||
of the string" — explicit. Recommending this form during migration
|
||
encourages idiomatic Blaise code rather than a literal transcription of
|
||
the original pattern.
|
||
|
||
*Suggested output.*
|
||
|
||
----
|
||
test.pas:12: for I := 1 to Length(S) do
|
||
note: consider replacing with: for B in S do
|
||
(only applicable if I is not used other than for S[I])
|
||
----
|
||
|
||
*Implementation notes.*
|
||
|
||
. The pattern check runs after the index-adjustment rewrite: if `I` is
|
||
used exclusively as `S[I]` within the loop body, upgrade the
|
||
suggestion from "rewrite index" to "use for-in".
|
||
. The analyser must confirm that the loop variable does not appear in any
|
||
expression other than `S[VarName]` inside the body. A conservative
|
||
check (any other use of `I` suppresses the suggestion) is acceptable
|
||
for the initial version.
|
||
|
||
*Effort.* Small — a follow-on pattern in the same analysis pass.
|
||
|
||
== Enhanced Enumerations
|
||
|
||
Blaise currently supports simple positional enumerations (`type TDir = (dNorth, dSouth, dEast, dWest)`)
|
||
and explicit ordinal assignment (`type TStatus = (Idle=10, Running=20, Done=30)`).
|
||
This section documents richer enum models drawn from other languages, for
|
||
consideration in a future language revision.
|
||
|
||
The reference examples below are included as design inspiration, not as a
|
||
commitment to any particular syntax. Any enhancement must preserve
|
||
`case`/`set` compatibility with simple integer ordinals unless a new,
|
||
distinct type form is introduced.
|
||
|
||
=== Reference: Java enum classes
|
||
|
||
Java enums are named singleton classes with a fixed set of instances.
|
||
Each variant carries typed fields initialised through a constructor.
|
||
Methods can be defined on the enum type and called on any variant.
|
||
|
||
.Java — definition
|
||
[source,java]
|
||
----
|
||
public enum Planet {
|
||
MERCURY(3.303e+23, 2.4397e6),
|
||
VENUS (4.869e+24, 6.0518e6),
|
||
EARTH (5.976e+24, 6.37814e6),
|
||
MARS (6.421e+23, 3.3895e6);
|
||
|
||
private static final double G = 6.67300E-11;
|
||
private final double mass; // kg
|
||
private final double radius; // metres
|
||
|
||
Planet(double mass, double radius) {
|
||
this.mass = mass;
|
||
this.radius = radius;
|
||
}
|
||
|
||
double surfaceGravity() {
|
||
return G * mass / (radius * radius);
|
||
}
|
||
|
||
double surfaceWeight(double otherMass) {
|
||
return otherMass * surfaceGravity();
|
||
}
|
||
}
|
||
----
|
||
|
||
.Java — application usage
|
||
[source,java]
|
||
----
|
||
public class WeightOnPlanets {
|
||
public static void main(String[] args) {
|
||
double earthWeight = 75.0;
|
||
double mass = earthWeight / Planet.EARTH.surfaceGravity();
|
||
|
||
for (Planet p : Planet.values()) {
|
||
System.out.printf("Weight on %s is %6.2f%n",
|
||
p, p.surfaceWeight(mass));
|
||
}
|
||
// Weight on MERCURY is 28.33
|
||
// Weight on VENUS is 67.89
|
||
// Weight on EARTH is 75.00
|
||
// Weight on MARS is 28.46
|
||
}
|
||
}
|
||
----
|
||
|
||
*Key traits.* Each variant is a singleton object; the type is a class
|
||
hierarchy. Methods have access to per-variant state. `values()` returns
|
||
an ordered array of all variants. Pattern matching (Java 21+) allows
|
||
exhaustive `switch` over variants.
|
||
|
||
=== Reference: Swift associated values
|
||
|
||
Swift enums are value types that support per-variant associated data of
|
||
heterogeneous types. There is no shared constructor — each variant
|
||
carries its own payload, extracted via pattern matching.
|
||
|
||
.Swift — definition
|
||
[source,swift]
|
||
----
|
||
enum HTTPResponse {
|
||
case ok(body: String)
|
||
case redirect(location: String, permanent: Bool)
|
||
case error(code: Int, message: String)
|
||
}
|
||
|
||
enum Direction {
|
||
case north, south, east, west
|
||
|
||
var opposite: Direction {
|
||
switch self {
|
||
case .north: return .south
|
||
case .south: return .north
|
||
case .east: return .west
|
||
case .west: return .east
|
||
}
|
||
}
|
||
}
|
||
----
|
||
|
||
.Swift — application usage
|
||
[source,swift]
|
||
----
|
||
func handle(_ response: HTTPResponse) {
|
||
switch response {
|
||
case .ok(let body):
|
||
print("Success: \(body)")
|
||
case .redirect(let url, let permanent):
|
||
let kind = permanent ? "301" : "302"
|
||
print("\(kind) → \(url)")
|
||
case .error(let code, let msg):
|
||
print("Error \(code): \(msg)")
|
||
}
|
||
}
|
||
|
||
let r = HTTPResponse.error(code: 404, message: "Not Found")
|
||
handle(r) // Error 404: Not Found
|
||
----
|
||
|
||
*Key traits.* Associated values are extracted only at the call site via
|
||
`switch`/`if case`. Different variants can carry different types and
|
||
arities. The enum itself remains a value type (stack-allocated).
|
||
|
||
=== Reference: Go typed constants (iota)
|
||
|
||
Go has no dedicated `enum` keyword. Enumerations are typed integer
|
||
constants, often using `iota` for automatic sequencing. Methods are
|
||
attached via receiver functions outside the type declaration.
|
||
|
||
.Go — definition
|
||
[source,go]
|
||
----
|
||
type Weekday int
|
||
|
||
const (
|
||
Sunday Weekday = iota // 0
|
||
Monday // 1
|
||
Tuesday // 2
|
||
Wednesday // 3
|
||
Thursday // 4
|
||
Friday // 5
|
||
Saturday // 6
|
||
)
|
||
|
||
func (d Weekday) String() string {
|
||
names := [...]string{
|
||
"Sunday", "Monday", "Tuesday", "Wednesday",
|
||
"Thursday", "Friday", "Saturday",
|
||
}
|
||
if d < Sunday || d > Saturday {
|
||
return fmt.Sprintf("Weekday(%d)", int(d))
|
||
}
|
||
return names[d]
|
||
}
|
||
|
||
type Permission uint
|
||
|
||
const (
|
||
Read Permission = 1 << iota // 1
|
||
Write // 2
|
||
Execute // 4
|
||
)
|
||
----
|
||
|
||
.Go — application usage
|
||
[source,go]
|
||
----
|
||
func describeDay(d Weekday) string {
|
||
switch d {
|
||
case Saturday, Sunday:
|
||
return "weekend"
|
||
default:
|
||
return "weekday"
|
||
}
|
||
}
|
||
|
||
func checkAccess(p Permission) {
|
||
if p&Read != 0 {
|
||
fmt.Println("can read")
|
||
}
|
||
if p&Write != 0 {
|
||
fmt.Println("can write")
|
||
}
|
||
}
|
||
|
||
func main() {
|
||
fmt.Println(describeDay(Friday)) // weekday
|
||
fmt.Println(Wednesday) // Wednesday (via String())
|
||
checkAccess(Read | Write) // can read / can write
|
||
}
|
||
----
|
||
|
||
*Key traits.* Minimal syntax — enums are just typed constants.
|
||
`iota` auto-increments within a `const` block; bitfield flags use
|
||
`1 << iota`. No associated data per variant; methods are ordinary
|
||
receiver functions.
|
||
|
||
=== Reference: C# enums with extension methods
|
||
|
||
C# enums are integer-backed value types with optional explicit underlying
|
||
type. Methods cannot be declared inside the enum; extension method classes
|
||
provide the equivalent.
|
||
|
||
.C# — definition
|
||
[source,csharp]
|
||
----
|
||
public enum HttpStatus : ushort {
|
||
OK = 200,
|
||
Created = 201,
|
||
NoContent = 204,
|
||
BadRequest = 400,
|
||
NotFound = 404,
|
||
ServerError = 500,
|
||
}
|
||
|
||
public static class HttpStatusExtensions {
|
||
public static bool IsSuccess(this HttpStatus s) =>
|
||
(int)s >= 200 && (int)s < 300;
|
||
|
||
public static string Describe(this HttpStatus s) =>
|
||
s switch {
|
||
HttpStatus.OK => "OK",
|
||
HttpStatus.Created => "Created",
|
||
HttpStatus.NotFound => "Not Found",
|
||
HttpStatus.ServerError => "Internal Server Error",
|
||
_ => $"HTTP {(int)s}",
|
||
};
|
||
}
|
||
----
|
||
|
||
.C# — application usage
|
||
[source,csharp]
|
||
----
|
||
void ProcessResponse(HttpStatus status) {
|
||
Console.WriteLine($"{(int)status} {status.Describe()}");
|
||
|
||
if (status.IsSuccess())
|
||
Console.WriteLine("Request succeeded.");
|
||
else if (status == HttpStatus.NotFound)
|
||
Console.WriteLine("Resource not found.");
|
||
else
|
||
Console.WriteLine("Unexpected status.");
|
||
}
|
||
|
||
ProcessResponse(HttpStatus.NotFound);
|
||
// 404 Not Found
|
||
// Resource not found.
|
||
----
|
||
|
||
*Key traits.* Explicit integral values on any member; underlying storage
|
||
type can be `byte`, `short`, `ushort`, `int`, `long`, etc.
|
||
`Enum.GetValues()` and `Enum.TryParse()` provide runtime reflection.
|
||
No per-variant associated data; extension methods approximate attached
|
||
behaviour.
|
||
|
||
=== Reference: Oxygene (RemObjects) enum methods
|
||
|
||
Oxygene extends the Object Pascal enum with inline method declarations,
|
||
keeping Pascal syntax while adding behaviour directly on the type.
|
||
|
||
.Oxygene — definition
|
||
[source,pascal]
|
||
----
|
||
type
|
||
Status = public enum
|
||
Pending,
|
||
Running,
|
||
Complete,
|
||
Failed;
|
||
|
||
public method IsTerminal: Boolean;
|
||
public method Description: String;
|
||
end;
|
||
|
||
method Status.IsTerminal: Boolean;
|
||
begin
|
||
Result := (Self = Complete) or (Self = Failed);
|
||
end;
|
||
|
||
method Status.Description: String;
|
||
begin
|
||
case Self of
|
||
Pending: Result := 'Not started';
|
||
Running: Result := 'In progress';
|
||
Complete: Result := 'Finished';
|
||
Failed: Result := 'Error occurred';
|
||
end;
|
||
end;
|
||
----
|
||
|
||
.Oxygene — application usage
|
||
[source,pascal]
|
||
----
|
||
procedure ProcessJob(AStatus: Status);
|
||
begin
|
||
WriteLn(AStatus.Description);
|
||
if AStatus.IsTerminal then
|
||
WriteLn('Job has reached a terminal state.');
|
||
case AStatus of
|
||
Pending: WriteLn('Queued');
|
||
Running: WriteLn('Executing');
|
||
Complete: WriteLn('Done');
|
||
Failed: WriteLn('Error');
|
||
end;
|
||
end;
|
||
|
||
begin
|
||
ProcessJob(Running);
|
||
// In progress
|
||
// Executing
|
||
end.
|
||
----
|
||
|
||
*Key traits.* Methods are declared inside the `enum` block and implemented
|
||
as top-level procedures with a `Status.MethodName` prefix — familiar Pascal
|
||
style. No associated data per variant; `case` dispatch remains ordinal.
|
||
Oxygene targets .NET and JVM, so enum method calls compile to interface
|
||
dispatch under the hood.
|
||
|
||
=== Option B — Enum with built-in string names
|
||
|
||
A lightweight extension: the compiler bakes a read-only name lookup table
|
||
into the binary so each variant can produce its display name without user
|
||
code writing a manual `case` statement.
|
||
|
||
.Proposed Blaise syntax — definition
|
||
[source,pascal]
|
||
----
|
||
type
|
||
TDirection = enum
|
||
dNorth = ('North'),
|
||
dSouth = ('South'),
|
||
dEast = ('East'),
|
||
dWest = ('West');
|
||
end;
|
||
----
|
||
|
||
.Proposed Blaise syntax — application usage
|
||
[source,pascal]
|
||
----
|
||
var
|
||
D: TDirection;
|
||
S: string;
|
||
begin
|
||
D := dEast;
|
||
S := TDirection.NameOf(D); // 'East'
|
||
WriteLn('Heading: ' + S); // Heading: East
|
||
|
||
// Reverse lookup (may raise exception if value not found)
|
||
D := TDirection.FromName('West');
|
||
WriteLn(Integer(D)); // 3
|
||
end.
|
||
----
|
||
|
||
*Implementation notes.* The compiler emits a `$typeinfo_TDirection_names`
|
||
data section containing NUL-terminated strings indexed by ordinal.
|
||
`NameOf` and `FromName` are compiler-generated RTL wrappers. This keeps
|
||
enum values as plain integers and does not require object allocation.
|
||
Works naturally with `case` and `set of`.
|
||
|
||
*Constraints.* If ordinals are non-contiguous (e.g. `Idle=10, Done=30`),
|
||
the lookup table must be sparse or indexed via a generated search function.
|
||
The simpler contiguous case (0-based or gapless explicit values) produces
|
||
a flat array.
|
||
|
||
=== Option C — Enum class with per-variant fields
|
||
|
||
A richer extension: enum variants are singleton instances of a
|
||
compiler-generated class. Each variant carries typed fields. Methods
|
||
are declared on the class and dispatched virtually.
|
||
|
||
.Proposed Blaise syntax — definition
|
||
[source,pascal]
|
||
----
|
||
type
|
||
THTTPStatus = enum class
|
||
OK (Code: Integer = 200, Phrase: string = 'OK'),
|
||
Created (Code: Integer = 201, Phrase: string = 'Created'),
|
||
NotFound (Code: Integer = 404, Phrase: string = 'Not Found'),
|
||
ServerError(Code: Integer = 500, Phrase: string = 'Internal Server Error');
|
||
|
||
function IsSuccess: Boolean;
|
||
function ToString: string; override;
|
||
end;
|
||
|
||
function THTTPStatus.IsSuccess: Boolean;
|
||
begin
|
||
Result := (Code >= 200) and (Code < 300);
|
||
end;
|
||
|
||
function THTTPStatus.ToString: string;
|
||
begin
|
||
Result := IntToStr(Code) + ' ' + Phrase;
|
||
end;
|
||
----
|
||
|
||
.Proposed Blaise syntax — application usage
|
||
[source,pascal]
|
||
----
|
||
procedure HandleResponse(S: THTTPStatus);
|
||
begin
|
||
WriteLn(S.ToString);
|
||
if S.IsSuccess then
|
||
WriteLn('Request succeeded.')
|
||
else
|
||
WriteLn('Request failed with code ' + IntToStr(S.Code));
|
||
end;
|
||
|
||
begin
|
||
HandleResponse(THTTPStatus.NotFound);
|
||
// 404 Not Found
|
||
// Request failed with code 404
|
||
end.
|
||
----
|
||
|
||
*Implementation notes.* Each variant becomes a global singleton of the
|
||
compiler-generated class. The variable type is an object pointer (8 bytes).
|
||
`case` on an enum class requires identity comparison (`ceql` on pointer
|
||
values), not ordinal comparison; this is a breaking change from simple enums.
|
||
`set of THTTPStatus` is not supported — sets require small-integer ordinals.
|
||
The compiler must generate a vtable for the class and emit singleton
|
||
initialisers in a dedicated data section.
|
||
|
||
*Trade-off.* The expressiveness is high (Java-equivalent), but the
|
||
implementation cost is significant and it breaks the simplicity guarantee
|
||
that enums are integers. A new keyword or syntax (`enum class`) is needed
|
||
to distinguish this from simple enums so existing code is unaffected.
|
||
|
||
=== Recommended progression
|
||
|
||
. *Done* — Explicit ordinal values (`(A=0, B=10, C=100)`) with
|
||
auto-continuation for unlabelled members.
|
||
. *Near-term* — Enum intrinsics: `Ord(E)`, `Succ(E)`, `Pred(E)`,
|
||
`Low(TEnum)`, `High(TEnum)`.
|
||
. *Medium-term* — Option B (built-in string names) — small compiler
|
||
change, large quality-of-life gain. Compatible with existing `case`/`set`.
|
||
. *Long-term* — Option C (enum class with fields and methods) as a
|
||
distinct `enum class` type form. Breaking from simple enum semantics
|
||
is acceptable provided it is a new keyword.
|
||
|
||
|