Adds a "Stream I/O" section to docs/language-rationale.adoc covering:
the decision (Go/Okio-inspired shape, one-direction abstract bases plus
capability interfaces), rationale (cross-language survey lessons from
Java/Go/Rust/Okio/.NET/Python), why capability interfaces sit alongside
abstract classes (TBuffer as both source and sink), UTF-8 only in v1,
alternatives rejected (single-TStream root, TFilterStream decorator,
async-from-day-one), and TODOs flagged in code (segment-pool thread
safety, CopyStream fast paths, non-identifier interface arguments).
Removes the "Formal TStream decorator hierarchy" section from
docs/future-improvements.adoc — the streams subsystem is now
implemented across phases 1-5 (releases of the past few commits).
The implementation departs from the sketched TStream/TFilterStream
shape in favour of one-direction abstract bases + capability
interfaces, for the reasons documented in the rationale.
./scripts/fixpoint.sh clean.
Adds CopyStream(Src: TInputStream; Dst: TOutputStream): Int64 — the
free function callers reach for whenever they want to drain one stream
into another. Discovers capability at runtime in this preference order:
1. Dst implements IReaderFrom → dispatch to Dst.ReadFrom(Src).
2. Src implements IWriterTo → dispatch to Src.WriteTo(Dst).
3. Fallback: 8 KiB byte-loop through a stack buffer.
The two marker interfaces ship empty in v1 — no concrete fast-path
implementations yet. Adding IReaderFrom to TFileOutputStream (with
sendfile(2) on Linux) or IWriterTo to TBuffer (with TransferTo) is a
future, non-breaking change: existing CopyStream callers automatically
pick up the speedup. This is the Go io.Copy idiom.
Streams unit cleanup: every concrete class now explicitly lists the
interfaces it implements (IInputStream / IOutputStream / ICloseable),
which was missing in earlier phases. Required for the new
Supports() queries to work correctly.
Compiler fixes — interface ABI and arg analysis:
* AnalyseMethodCall (statement form): the tyInterface branch never
called AnalyseExpr on argument expressions, so codegen later
crashed when an arg like @Buf[0] referenced a TIdentExpr with
nil ResolvedType. Fixed by calling AnalyseExpr on each arg
(overload validation isn't possible without param info, but
type resolution is needed).
* Interface parameters now use a two-slot fat-pointer ABI:
`function F(I: IFoo)` lowers to `function $F(l %_par_I_obj, l
%_par_I_itab)`, with matching `_var_I_obj` / `_var_I_itab`
local slots in the prologue. Call sites for TIdentExpr args
decompose `Os` into `l (loadl _var_Os_obj), l (loadl
_var_Os_itab)`. Previously interface params were emitted as a
single `w` slot (QBE rejected the storel into them).
Remaining gap: non-identifier interface arguments (function
returns, casts, field accesses) raise ECodeGenError with a
clear message. Tracked in the project's perf-focus memory note.
Test coverage: four new e2e cases —
* Interface dispatch via Supports (the inline Phase 1 workaround
pattern, now using real interface dispatch).
* Interface as function parameter (exercises the new ABI).
* CopyStream memory → memory and file → file (both through the
fallback byte-loop path).
Suite is now 1868 (was 1864); ./scripts/fixpoint.sh clean.
Adds TBuffer — an Okio-inspired segmented in-memory buffer that is
both an IInputStream and an IOutputStream. Internal representation
is a singly-linked list of fixed-size 8 KiB segments drawn from a
module-level intrusive freelist.
The defining operation is TransferTo(Dst, Count): whole segments are
unlinked from this buffer's head and re-linked onto Dst's tail, with
no byte copy. Multi-layer pipelines (decode → buffer → forward) drop
from N memcpys per byte to ~1. Partial-prefix moves and unaligned
slice boundaries still memcpy, but only for the head segment.
IndexOf(B) scans without consuming bytes — useful for line/frame
parsers that need look-ahead before deciding how many bytes to
consume.
TBuffer implements both IInputStream and IOutputStream directly,
which is the case the capability-interface design (over
single-inheritance abstract classes) was built for.
TODO: the segment pool is currently single-threaded. Pool ops need a
mutex or per-thread freelist once Blaise gains threads. Documented
in the unit; tracked in the project's perf-focus memory note.
Compiler fixes — byte pointer dereference and store:
TDerefExpr lowering for ^Byte / ^Boolean was emitting `loadw`
(4-byte load), corrupting values with adjacent memory. Symmetric
bug in TPointerWriteStmt used `storew`, clobbering 3 bytes past
the target. Both paths now select `loadub` / `storeb` when the
pointed-to type is tyByte or tyBoolean.
TBuffer.IndexOf is the regression that surfaced these: scanning
byte-by-byte through `PB^ = B` read junk because the load was
reading 4 bytes and the comparison missed. Without the fix,
IndexOf(65) returned -1 instead of 0 for a buffer starting with
byte 65. Fix verified with both the original IndexOf case and a
minimal `var Buf: array[0..3] of Byte; PB^ := 42` reproducer.
Test coverage: three new e2e cases — write+read round-trip,
TransferTo prefix split, IndexOf hits and misses. Existing 1861
tests still pass; suite is now 1864.
Verified: ./scripts/fixpoint.sh clean.
Adds two text-layer decorators over the byte streams.
TStreamReader — ReadLine, ReadAll, EndOfStream. Line terminators on
read are tolerant (LF, CRLF, lone CR all end a
line); returned strings exclude the terminator.
EndOfStream peeks one byte so callers can distinguish
a genuinely empty line from end-of-input.
TStreamWriter — WriteString, WriteLine. WriteLine appends LF;
matches LineEnding/sLineBreak in system.pas.
UTF-8 only in v1. The Encoding parameter is intentionally not exposed
yet — Blaise's string type is already UTF-8, so the wrappers are a
pass-through. Other encodings deferred until there is a real consumer.
Composition model is explicit, like Okio: pass a TBufferedInputStream
between the file and the reader for line-oriented work. TStreamReader
itself reads byte-by-byte from its inner; the buffered layer is what
makes it cheap. Documented in the unit's TStreamReader doc comment
with an example.
OwnsInner=True (default) tears the whole chain down on Close. Close
on the writer side flushes through any buffered layer below it before
closing.
Test coverage: three new e2e cases —
* Full 4-layer pipeline round-trip (writer → buffered → file → file
→ buffered → reader → application), 3 lines.
* Mixed line endings (LF, CRLF, lone CR, no terminator) all parse to
4 lines with terminators stripped.
* ReadAll returns the entire remaining stream.
Suite is now 1861 (was 1858); ./scripts/fixpoint.sh clean.
Adds TBufferedInputStream and TBufferedOutputStream — decorators that
wrap any TInputStream/TOutputStream and reduce per-call overhead by
batching reads/writes through an internal 8 KiB byte buffer (size
configurable via overloaded constructor).
Ownership model: OwnsInner=True by default — closing the buffered
wrapper closes the inner too. OwnsInner=False keeps the inner alive
past the wrapper's Close (for the helper-returns-wrapper pattern).
Buffered output always flushes on Close before tearing down the chain,
so buffered data is never silently lost.
Error propagation: introduces EStreamError (extends SysUtils.Exception).
File open / read / write / short-write failures now raise instead of
returning silent failure codes — closing the Rust BufWriter loophole
called out in the design doc. ARC-driven Destroy still swallows the
error (a finaliser cannot meaningfully propagate); callers who care
must Close explicitly inside a try-finally.
Compiler fix — virtual dispatch on receiver expressions:
EmitMethodCall's "call on arbitrary receiver expression" branch
always emitted a static call (line ~3500), even when the method was
virtual. This broke the moment a buffered wrapper called
`Self.FInner.Write(...)` against a TOutputStream-typed field whose
concrete instance was a TFileOutputStream — the static call resolved
to \$TOutputStream_Write, which doesn't exist (abstract). Fix mirrors
the same vtable-load sequence used by the IsImplicitSelf and
ObjectName branches: when MDecl.VTableSlot >= 0, load vptr from
Self[0], index by slot, and indirect-call the resolved fptr.
Test coverage: three new e2e cases in cp.test.e2e.streams —
buffered file round-trip, flush-on-close (Close drains the buffer
without explicit Flush), and missing-file open raises EStreamError.
Existing 1855 tests still pass; suite is now 1858.
Verified: ./scripts/fixpoint.sh clean.
Adds the Streams unit (rtl/src/main/pascal/streams.pas) — Phase 1 of the
new stream I/O subsystem. Departs from the FPC/Delphi single-TStream
hierarchy: separate TInputStream and TOutputStream abstract bases keep
each class single-purpose, while ICloseable / IInputStream / IOutputStream /
ISeekable capability interfaces let consumers accept "anything readable"
without forcing single inheritance.
Ships in this phase: TFileInputStream, TFileOutputStream, TMemoryInputStream,
TMemoryOutputStream, plus fd-level C primitives (_FdOpenRead/Write/Append,
_FdRead, _FdWrite, _FdSeek, _FdSize, _FdClose) in blaise_io.c.
Compiler change — abstract method tombstones:
When an abstract class declares an interface, its itab references method
symbols that have no body on that class. The unit-emission path for
vtables silently emitted these symbols (latent bug in rtl.platform.pas,
hidden because every consumer overrode every method); itab emission did
the same. Both paths now route abstract slots to $_AbstractMethodError,
a new C-RTL tombstone in blaise_arc_class.c that abort()s if dispatched.
Moving the stub to C also dedupes it across program + unit translation
units, so the linker sees exactly one definition.
Test coverage: cp.test.streams (IR-level assertions on abstract-class
itab/vtable emission) and cp.test.e2e.streams (compile+run programs that
exercise the streams through the real RTL). Existing abstract-method
vtable tests in cp.test.vtable updated to assert against the new
$_AbstractMethodError symbol name.
Verified: all 1855 unit/e2e tests pass; ./scripts/fixpoint.sh clean.
Implements the 'abstract' method directive, following FPC/Delphi
semantics:
procedure Draw; virtual; abstract;
Abstract methods must not have an implementation body (semantic
error if one is provided). A class that has or inherits any
un-overridden abstract method is implicitly abstract and cannot
be instantiated (compile-time error on TFoo.Create).
Abstract vtable slots point to a $__abstract_method_error stub
(calls abort()) as a defence-in-depth measure.
Changes:
uAST.pas: IsAbstract field on TMethodDecl
uSymbolTable.pas: IsAbstract on TVTableEntry; HasAbstractMethods
on TRecordTypeDesc; CopyVTableFrom now propagates IsAbstract
uParser.pas: 'abstract' directive sets IsAbstract (and IsVirtual)
uSemantic.pas: abstract slot tracking in vtable pre-pass;
post-vtable check for inherited-but-unoverridden abstract slots;
AnalyseMethodDecl rejects abstract methods with bodies;
TFoo.Create raises ESemanticError for abstract classes (both
constructor call sites)
uCodeGenQBE.pas: EmitVTableDefs emits $__abstract_method_error
stub when needed; abstract slots reference it in the vtable
cp.test.vtable.pas: 9 new tests covering parse flags, semantic
validation, and codegen. 1846 tests pass. Fixpoint OK.
Supports constants of the form:
const Names: array[TEnum] of string = ('A', 'B', 'C');
const Costs: array[TDir] of Integer = (1, 1, 2, 2);
The index type must be an enum; element count must match enum member
count (semantic error otherwise). Array const elements are emitted as
pre-initialised QBE data objects. String elements use immortal string
pointer references ($__sN + 12). Integer elements use w-typed words.
Changes:
uAST.pas: IsArrayConst, ArrayIndexType, ArrayElemType, ArrayElements
fields on TConstDecl; ArrayElements freed in destructor
uSymbolTable.pas: ConstArray: TStringList on TSymbol (owned)
uParser.pas: parse array[T] of E type annotation and (...) value list
uSemantic.pas: AnalyseArrayConstDecls (second pass after type decls)
called after AnalyseTypeDecls at all five analysis call sites
uCodeGenQBE.pas: EmitGlobalConstData emits data objects for array
consts; IsConstant guard for tyStaticArray routes through the
global aggregate address path
docs/grammar.ebnf: ConstDecl, ConstTypeAnnotation, ConstRhs updated
docs/language-rationale.adoc: new Array-of-enum Typed Constants
section; Supported types list updated in Typed Constants section
cp.test.constants.pas: 7 new tests. 1837 tests pass. Fixpoint OK.
Supports type annotations on constant declarations for all scalar
types (Integer, Int64, Double, Single, Boolean, string).
Changes:
uAST.pas: add TypeName field to TConstDecl
uParser.pas: detect ':' after const name, read type identifier
uSemantic.pas: when TypeName is set resolve via FindType; report
error for unknown type names; value codegen unchanged
math.pas: Pi updated to typed form (Pi: Double = ...)
docs/grammar.ebnf: ConstDecl and ConstExpr updated
docs/language-rationale.adoc: new Typed Constants section
cp.test.constants.pas: 11 new tests covering Integer, Int64, Double,
Single, Boolean, string, negative values, unit-level typed consts,
and type annotation verification via symbol table inspection.
1830 tests pass. Fixpoint OK.
Implements math.pas RTL unit and math compiler builtins with
Single/Double dispatch as described in future-improvements.adoc.
Compiler builtins (no unit required):
Sqrt, Ceil, Floor, Round, Trunc, Ln, Log2, Log10, Power,
Sin, Cos, Tan, ArcTan, ArcTan2, IsNaN, IsInfinite.
Trig functions dispatch to *f variants for Single arguments.
math.pas RTL unit (uses Math):
Min, Max (Integer/Int64/Double overloads), Sign (Integer/Int64/Double),
DivMod, InRange, EnsureRange (Integer/Double), Pi constant.
Compiler fixes included in this commit:
- uCodeGenQBE: fix Float (Double/Single) return type in EmitFuncDef
and EmitMethodDef -- Result slot was initialised and loaded with
wrong QBE type (storel/loadl instead of stored/loadd).
- uSemantic: fix overload impl matching -- impl-section functions
without 'overload' keyword now correctly match overloaded interface
forward decls using mangled-signature lookup.
- uUnitLoader: remove Math from the built-in skip list.
- uSymbolTable: register math builtins in RegisterBuiltins.
- Blaise.pas: pass -lm to link step.
- bcl.testing: add AssertNotNil(AMsg, APtr) overloads.
- cp.test.e2e.base: pass -lm to cc link step in all CompileAndRun paths.
Tests: 66 IR unit tests (cp.test.math.pas) + 41 E2E tests
(cp.test.e2e.math.pas). 1819 total tests pass. Fixpoint OK.
Adds full support for heap-allocated, reference-counted dynamic arrays
across all compiler layers and the RTL.
- Parser: distinguish array[L..H] of T (static) from array of T (dynamic)
- Symbol table: tyDynArray kind + TDynArrayTypeDesc + NewDynArrayType
- Semantic: type resolution, Length(), SetLength(), subscript read/write
- Codegen: pointer-slot alloc, nil init, RTL calls, element read/write;
QbeTypeOf returns 'l' for tyDynArray; Int64 element sign-extension
- RTL: _DynArraySetLength and _DynArrayLength in blaise_str.pas
- OPDF: EmitTypeDesc and EmitArray handle tyDynArray (IsDynamic=1)
- Grammar: DynArrayType rule added to grammar.ebnf
- Docs: dynamic array design decision in language-rationale.adoc
- Tests: cp.test.dynarray (14 tests: parser, semantic, codegen)
Fixpoint verified at stage-3/stage-4. All 1712 tests pass.
Rewrites strutils.pas from an empty stub into a complete pure Blaise Pascal
implementation with no external C dependencies. All helpers previously
planned for blaise_str.pas are implemented directly in StrUtils using only
compiler built-ins (Length, Pos, LowerCase, Copy, SetLength, PosEx).
- ContainsStr/ContainsText, StartsStr/StartsText, EndsStr/EndsText
- LeftStr, RightStr, MidStr
- IndexStr, IndexText
- ReplaceStr, ReplaceText
- DupeString, ReverseString, StuffString
- TrimLeft, TrimRight
- PadLeft, PadRight
- CountOccurrences, RemovePrefix, RemoveSuffix
- IsEmptyOrWhitespace
- JoinStr
- TStringBuilder — efficient incremental string builder (mirrors TIRBuffer)
Adds ExpandFileName to sysutils.pas as a pure Pascal path normaliser that
resolves '.' and '..' segments without symlink following, matching
Delphi/FPC behaviour.
Fixes cp.test.multiwrite.pas: unqualified CountOccurrences calls now use
Self. prefix to prevent name shadowing by StrUtils.CountOccurrences, which
has reversed parameter order (Sub, S vs Haystack, Needle).
Adds cp.test.strutils.pas (IR + type-check unit tests) and
cp.test.e2e.strutils.pas (compile→run E2E tests) covering all new
functions. Total test count: 1698.
Fixpoint verified at stage-3/stage-4.
Documents the design space for richer enum models with reference examples
from Java, Swift, Go, C#, and Oxygene (RemObjects), each showing both the
type definition and realistic application-code usage.
Options documented:
- Option B: built-in string name lookup (compiler-generated table)
- Option C: enum class with per-variant fields and methods (Java-style)
Recommended progression from done (explicit ordinals) through near-term
(Ord/Succ/Pred intrinsics), medium-term (Option B), and long-term (Option C).
Enum members can now be assigned explicit integer ordinals, with
auto-continuation for unlabelled members:
type TStatus = (Idle=10, Running=20, Done=30);
type TCode = (A=100, B, C); { B=101, C=102 }
type TOffset = (Before=-1, At=0, After=1);
The ordinal value is stored in TEnumTypeDef.Members.Objects[I] as a
tagged pointer (PtrUInt), avoiding a dynamic array field that v0.7.0
cannot compile. TEnumTypeDef.OrdinalAt(I) is the typed accessor.
The semantic pass reads OrdinalAt(K) instead of the positional index K,
so ConstValue on each enum-member symbol reflects the declared ordinal.
Codegen already emits copy <ConstValue> — no codegen changes needed.
grammar.ebnf: add the EnumDef and EnumMember rules (previously missing).
These are platform fundamentals that belong in the system unit, matching
FPC's design where LineEnding, sLineBreak, and PathSeparator live in system.
system.pas gains:
LineEnding = #10 (canonical line ending constant)
sLineBreak = LineEnding (Delphi compat alias)
DirectorySeparator = '/' (canonical path separator)
PathSeparator = ':' (PATH env var separator)
sysutils.pas retains only:
PathDelim = '/' (Delphi compat alias for DirectorySeparator)
uSymbolTable.RegisterBuiltins pre-seeds all four constants with POSIX
defaults so they resolve as builtins regardless of unit load order.
The correct extension point for cross-compilation is a future --target
flag that overrides these values at compiler startup — no {$IFDEF}
in source, no platform-specific system unit variants needed.
PathDelim uses literal '/' rather than DirectorySeparator to stay
compatible with the v0.7.0 release bootstrap binary.
1590 tests, all passing. Fixpoint OK (stage-3/stage-4).
type TArr = array[L..H] of T previously failed with a parse error because
ParseTypeDecl did not handle tkArray on the right-hand side of a type alias.
Parser fix: add tkArray to the existing tkCaret|tkIdent branch so all three
forms (array alias, pointer alias, simple name alias) share the same
TTypeAliasDef + ParseTypeName path.
Semantic fix: in the simple-alias resolution path, fall through to
FindTypeOrInstantiate when FTable.Lookup returns nil, so that
'array[L..H] of T' style names (which are created on demand, not
pre-registered) resolve correctly. Previously the path raised 'Unknown type'
immediately without trying the on-demand instantiation route.
Tests: 9 new IR/semantic tests in cp.test.staticarray.pas. New dedicated
e2e unit cp.test.e2e.staticarray.pas with 8 tests covering anonymous and
named alias forms, zero/non-zero base, Length/Low/High, multiple vars sharing
a type, and global+local var combinations. Docs updated in grammar.ebnf and
language-rationale.adoc. Fixpoint verified at stage-3/stage-4.
Passing a named static-array variable (array[lo..hi] of T) to a procedure
expecting an open-array parameter (array of T) previously failed with 'No
matching overload' because ArgMatchScore and IsSubtypeOf had no rule for this
coercion. Inline array literals worked because they resolve to tyOpenArray
directly; named variables do not.
Fix: add a tyStaticArray→tyOpenArray widening rule (score 1) in both
ArgMatchScore and IsSubtypeOf when element types match. In codegen, add a
third branch in all four open-array argument-emission sites: when the arg is
a tyStaticArray, emit the base pointer plus (HighBound - LowBound) as the
compile-time high index instead of loading a runtime _high slot.
Tests: 5 new IR/semantic tests in cp.test.openarray.pas. New dedicated e2e
unit cp.test.e2e.openarray.pas with 8 tests (3 moved from the monolithic e2e
unit + 5 new static-array coercion tests covering Length, Sum, non-zero base,
and nested forwarding). Fixpoint verified at stage-3/stage-4.
Introduce TE2ETestCase in cp.test.e2e.base.pas as a shared base for all
e2e test suites. It centralises ProjectRoot detection, toolchain setup,
scratch directory management, RunProc/RunProcNoArgs (as methods, removing
the per-unit _XX suffix workaround), CompileAndRun for inline-class programs,
and CompileAndRunWithRTL for programs that use RTL units via TUnitLoader +
AppendUnit/AppendProgram.
Add cp.test.e2e.tstringlist.pas with 17 e2e tests covering the real RTL
Classes unit: Add/Get, Count, IndexOf (found/not-found), Delete, Insert,
Clear, Put (Strings[] write), AddStrings, for..in enumerator, Text get/set,
Grow past initial capacity, Sorted mode, CaseSensitive flag, and
Duplicates=dupIgnore. The two TStringList tests previously in the
monolithic cp.test.e2e.pas are removed and superseded by this unit.
Refactor cp.test.e2e.tstack, tqueue, tset, tordereddictionary, and imap
to inherit TE2ETestCase: SetUp reduces to one SetUpScratch call, all
duplicated infrastructure is removed (~100 lines per unit), and + LE +
string concatenation is converted to text blocks throughout.
Fixpoint verified at stage-3/stage-4. All 1556 tests pass.
Re-enabled cp.test.lexer, cp.test.parser, cp.test.codegen,
cp.test.symtable, cp.test.semantic, cp.test.records in TestRunner.pas —
all six were commented out when the test runner was migrated from FPC to
Blaise, reducing the suite from ~1433 to ~1376 tests.
Changes needed to make all six units compile and pass (1541 tests total):
- Remove leftover `Classes, SysUtils` imports from all six units (FPC
fpcunit era; Blaise has neither unit).
- Replace default-property bracket subscripts (TObjectList[i],
TStringList[i]) with explicit .Items[i] / .Strings[i] calls, since
Blaise does not yet implement the `default` indexed property keyword.
- codegen: add tyRecord branch in EmitStaticSubscriptAssign — record
elements in static arrays must use EmitRecordCopy, not a raw storew.
- codegen: add tyRecord early-return in EmitStringSubscriptExpr static
array branch — return element pointer directly (records are by-value
via pointer).
- codegen: fix EmitInstancePtr to delegate to EmitExpr when the field
access is property-backed (PropRead != nil), avoiding a nil FieldInfo
assertion.
- test: fix IRContains to use Pos(...) >= 0 (Blaise Pos is 0-based,
returns -1 on no-match, not 0 like FPC).
_POSIX_C_SOURCE 199309L only enables POSIX.1-1993 symbols; mkstemp
requires _POSIX_C_SOURCE >= 200809L (or _XOPEN_SOURCE >= 500).
The value was already 200809L in blaise_process.c — align blaise_io.c
to the same level.
Fixes: https://github.com/graemeg/blaise/issues/25
- Add IMap<K,V> generic interface to generics.collections.pas with Add,
TryGetValue, ContainsKey, Remove, GetCount; wire TDictionary<K,V> and
TOrderedDictionary<K,V> to implement it
- Fix generic class instantiation to detect interface-typed ParentName and
move it to ImplementsNames, then call AddImplements so type-compat checks
recognise class-to-interface assignment (uSemantic.pas InstantiateGeneric)
- Add TFieldAccessExpr.IsInterfaceCall + ResolvedClassType fields (uAST.pas)
and wire AnalyseFieldAccess to set them for zero-arg interface method calls
- Add codegen for TFieldAccessExpr.IsInterfaceCall: load obj+itab and
dispatch through itab slot (uCodeGenQBE.pas)
- Fix EmitMethodCall interface dispatch to emit arguments after Self instead
of calling with only Self; determine arg types from expression ResolvedType
- Extend TInterfaceTypeDesc with per-method var-param flags (FParamIsVar):
MethodParamIsVar/MethodParamVarFlagsStr/AddMethod(name, ret, varflags)
so interface call sites can emit var params as addresses, not values
- Fix expression-level interface dispatch to use var-param flags for correct
l/w selection per argument (TryGetValue var param was passed as w, causing
segfaults)
- Emit itab + impllist blocks for generic class instances that implement
interfaces (EmitInterfaceDefs in uCodeGenQBE.pas)
- Update EmitTypeInfoDefs to set impllist pointer for generic instances
- Apply QBEMangle to both class and interface names in itab symbol names
- Set AExpr.ResolvedType in interface method call expression path
- Convert cp.test.e2e.imap.pas source constants to triple-quoted textblocks
- Add IR unit tests (cp.test.imap.pas: 22 tests) and E2E tests
(cp.test.e2e.imap.pas: 6 tests) for IMap interface dispatch
- Add IR + E2E tests for TStack, TQueue, TSet, TOrderedDictionary
- Fix AssertContains: Pos returns -1 for not-found (0-based), not 0
- All 1376 tests pass; fixpoint verified at stage-3/stage-4
New assertion helpers on TAssert:
- AssertNotEquals — Integer, Int64, string, Boolean, Pointer overloads;
reports the shared value on failure so tests no longer need to
convert to AssertTrue(a <> b) which loses the comparison output
- AssertContains(msg, needle, haystack) — replaces the widespread
AssertTrue('...', Pos(x, y) > 0) pattern with a failure message
that quotes both the needle and the haystack
Verbose output (--verbose flag on TestRunner):
- TTestResult gains a Verbose property
- StartTest/EndTest now carry the class name and outcome string
- Each test prints "ClassName.MethodName ... OK/FAIL/ERROR/IGNORED"
as it runs when --verbose is active
Better error reporting:
- Unhandled Exception descendants now report ClassName + Message
instead of the generic "Unhandled exception" string; requires
SysUtils in bcl.testing uses clause
- TObject catch-all still present for non-Exception throwables
Runner:
- ParseSuiteArg replaced by ParseArgs which also handles --verbose
- RunFilteredTests takes an AVerbose parameter and seeds the result
- ClassCreate'd instances get SetClassName called so verbose output
shows the full ClassName.MethodName path
Adds a new for-in collection kind: set of TEnum. The loop iterates
members in ascending ordinal order by scanning the bitmask one bit at
a time. The set expression is evaluated once before iteration begins.
- uAST: IsSetIter, SetBitCount, SetMaskVarName fields on TForInStmt
- uSemantic: tySet branch validates ordinal loop var, injects synthetic
__setmask_N and __idx_N slots
- uCodeGenQBE: bit-scan loop using shr + and 1; break/continue wired to
forin_end / forin_next labels
- 7 IR unit tests (cp.test.forin) + 1 e2e test (set [Red,Blue] → 0,2)
- language-rationale and grammar.ebnf updated; generic forin marked done
Supports(Obj, IFoo) returns Boolean — calls _ImplementsInterface at
runtime to test whether Obj's class implements IFoo.
Supports(Obj, IFoo, Ref) does the same test and, on success, populates
the fat-pointer slots (obj + itab via _GetItab) of the out-variable Ref
under full ARC (retain new obj, release old obj slot).
The second argument is a type name, not a runtime value, so Supports is
a compiler intrinsic parsed specially in ParseFactor rather than a
library function. The third argument (when present) is the name of an
interface-typed out-variable.
Includes:
- TSupportsExpr AST node (uAST.pas)
- Parser intercept for Supports( in ParseFactor (uParser.pas)
- Semantic analysis: validates obj type, interface type, out-var type
(uSemantic.pas)
- QBE IR emission for both 2-arg and 3-arg forms (uCodeGenQBE.pas)
- 7 unit tests covering parse, semantic, and IR codegen
- 2 e2e tests verifying runtime Boolean result and method dispatch
through the assigned interface fat pointer
- Grammar and language-rationale documentation updates
Fixpoint verified at stage-3/stage-4.
Length() previously only accepted strings — calling it on an open-array
parameter raised "Length argument must be a string".
Semantic: allow tyOpenArray and tyStaticArray in addition to tyString.
Codegen:
- tyOpenArray: load the _high slot and add 1 (same pattern as High()+1)
- tyStaticArray: emit HighBound - LowBound + 1 as a compile-time constant
- tyString: unchanged — delegates to _StringLength RTL call
Tests: 4 IR-level tests in cp.test.openarray.pas and 1 e2e run test.
When a method call appears in expression context (result assigned or
passed as an argument) and the receiver is an implicit-self field, the
analyser was calling FindMethodDecl — which returns the first registered
match — instead of ResolveMethodOverload. This caused the arity check
to fire against the wrong overload (e.g. a 4-param open-array overload
was selected for a 3-arg call, producing a spurious "expects 4 args but
got 3" error).
Fix: mirror the statement path — analyse args first, then call
ResolveMethodOverload to score and pick the correct candidate.
Regression test added in cp.test.overload.pas. As a result, the
CompileAndRunNoArgs workaround in cp.test.e2e.pas is reverted: the
no-args variant is now a proper overload of CompileAndRun.
On a clean checkout, compiler/target/blaise does not exist yet, so
plain `make` in rtl/ produces empty .ssa files and a silently broken
RTL archive. The fix is to pass BLAISE=../releases/v0.7.0/blaise to
make on the first RTL build. Document this in both README.adoc and
CLAUDE.md with the correct three-step cold-bootstrap sequence.
Replace FPC prerequisites with Blaise release binary. Update build
instructions to show bootstrap-from-release workflow. Update test
count to 1268 and note that the test suite compiles under Blaise.
When stage-1 is an older release with different codegen, stage-2 and
stage-3 IR will differ. The script now automatically extends to
stage-3 -> stage-4 and checks that fixpoint instead of failing.
Bump version to 0.7.0. Update fixpoint.sh to use the latest release
binary as stage-1 instead of FPC — FPC is no longer needed anywhere
in the toolchain.
Five fixes that together bring the Blaise-compiled test suite from
1053 to 1268 tests with zero failures:
- Blaise.pas: remove {$IFDEF FPC} guards; cast exception variable to
Exception(E) for Blaise compatibility
- uCodeGenQBE.pas: fix implicit-self method calls that pass var/out or
open-array parameters (two symmetric paths in EmitProcCall and
EmitExpr TFuncCallExpr)
- cp.test.e2e.pas: rename CompileAndRun no-args overload to
CompileAndRunNoArgs (overload-resolution workaround); lowercase unit
names for case-sensitive loader; fix 0-based Pos/Copy indexing in
TObjectList test assertion; revert debug DeleteFile guard
- blaise_io.c: fix buffer overflow in _GetTempFileName when dir is
empty and TMPDIR is set
- process.pas: rename from Process.pas for case-sensitive unit loader
Fixpoint verified: stage-2 IR == stage-3 IR (116462 lines).
Switch pasbuild test goal to use Blaise as compiler by adding the RTL
unit path to the build <unitPaths> in compiler/project.xml. This mirrors
the fixpoint pipeline exactly: Blaise compiles itself, then compiles the
test runner.
Core fixes to make the Blaise-compiled test suite green:
bcl.testing.pas: rename duplicate on E: variable names (EAF/EIT/ETO)
to prevent ARC triple-release of the caught exception object (KI-3).
bcl.testing.runner.text.pas: implement --suite / --suite Class.Method
CLI filtering (Step 15); also adds TestClassName() via typeinfo[2]
read for the class name lookup. Fixes 0-based Pos split for --suite
argument parsing.
uParser.pas, uSemantic.pas: guard explicit-receiver .Free + reassign
patterns with { FPC} so ARC does not double-release the field
slot (KI-2). Three call sites: GID.IntfDef, GD.ClassDef in the
parser, and GII.IntfDef in InstantiateGenericInterface.
cp.test.sets.pas: rename duplicate on ESE/EEx exception variables.
cp.test.exceptions.pas, cp.test.multiwrite.pas: fix PosEx loop
termination — Blaise returns -1 (not 0) for not-found; start index
is 0-based.
cp.test.arc.pas and seven other test files: fix absence checks that
used Pos(...) = 0 (FPC not-found sentinel); changed to < 0.
cp.test.stringops.pas: replace emoji literal '😀' with 'AB' to work
around parser limitation with bytes > 127 in string literals (KI-1);
test still exercises the same CoerceToCharOrd semantic-error path.
Result: pasbuild test -m blaise-compiler --compiler ./compiler/target/blaise
passes with 1053 tests, 0 failures, 0 errors.
Chained field access like Prog.Block.TypeDecls[0] was passing the
intermediate base pointer (TBlock) directly to TObjectList_Get instead
of first loading the TypeDecls field value. Fixes the EmitExpr chained
path to match the ImplicitSelf path which already handled this correctly.
- Move RTL unitPath from <build> to <test> section in project.xml —
FPC picks up Blaise's system.pas when RTL is on the build path
- Guard IR caching code (TSearchRec/FindFirst) with { FPC} —
Blaise doesn't support these SysUtils types
- Add LineEnding constant under { FPC} in bcl.testing for
Blaise-compiled test units
- Replace empty array literal [] with 3-arg CompileAndRun overload
- Migrate cp.test.tokenkindname from fpcunit to bcl.testing; replace
Format/Low/High with Blaise-compatible alternatives
- Increase QBE NString from 80 to 160 — method names like
TInterfaceTests_TestSemantic_ClassWithInterfaceAsFirstParent_
InheritsFromTObject exceed the old limit
Nested record field assignment (O.A.V := 10):
EmitFieldAssignment called EmitExpr on ObjExpr for record-typed bases,
which loaded the contents instead of returning the storage address.
Fixed to use EmitInstancePtr, which correctly returns the address of
inline record storage and the heap pointer for class-typed bases.
Int64 literals exceeding int32 range:
TIntLiteral always emitted =w copy N even for values like 3000000000
that overflow 32-bit signed range. The semantic pass also always
assigned TypeInteger to int literals regardless of value. Both now
check whether the value fits in [-2147483648..2147483647] and use
TypeInt64 / =l copy N when it does not.
Byte(X) type cast not truncating:
The type-cast emit path (ResolvedDecl=nil) used =w copy for all
word-typed targets. Byte casts now emit =w and %t, 255 to mask to
8 bits, matching the expected truncation semantics.
inherited call not setting Result:
EmitInheritedCall always emitted a plain call statement, discarding
the return value of function overrides. It now captures the return
value and stores it into %_var_Result when the parent method has a
non-void return type.
@Obj.MethodName compiler crash:
AnalyseAddrOfExpr only handled TIdentExpr; TFieldAccessExpr with a
method field caused AnalyseFieldAccess to return nil (procedure has
no return type), then crashed on nil.Name. Added a branch that
recognises both RecordName (simple) and Base (chained) forms, builds
a method-pointer TProceduralTypeDesc (IsMethodPtr=True), and sets it
on the expression. EmitAddrOfExpr now allocates a 16-byte block and
stores the static code pointer and loaded object pointer into it.
Also adds ~50 e2e tests covering these features and more:
control flow, records (nested, string fields), pointers, text blocks,
sets, procedural types, default params, open arrays, var/const params,
string ops, Int64, type casts, is/as, inheritance, and interfaces.
Add four end-to-end for..in tests (string/Byte, string/Integer, array,
class enumerator) that compile through QBE+gcc and assert stdout.
IR-substring tests cannot detect promoted-scalar or invalid-pointer
bugs because QBE only rejects invalid IR at assembly time.
The array test exposed two pre-existing global static array bugs:
- EmitStaticSubscriptAssign hardcoded %%_var_ prefix instead of
VarRef(name, IsGlobal), breaking writes to global array variables.
- EmitGlobalVarData had no tyStaticArray case, emitting { l 0 }
(8 bytes) instead of { z N } for the correct element storage size.
Both fixed; all 1382 tests pass and fixpoint verified.
The synthetic index variable and user loop variable in for..in array
and string iterations are scalar integers/bytes that EmitVarAllocs
promotes to QBE register mode (=w copy 0). The old codegen
unconditionally emitted storew/loadw, which require a memory pointer
and cause a QBE error when the variable is a promoted register.
Apply the same IsPromoted checks used by the regular for loop: emit
=w copy for promoted scalars, storew/loadw for memory-backed ones.
In FPC/Delphi, for..in over a string yields Char, requiring Ord() to get
the numeric value. In Blaise, S[N] and for B in S already yield Byte
directly, so Ord() is superfluous for strings. Added a note to the
No Char Type section explaining this, and clarifying that Ord() remains
relevant for enumerations.