Commit graph

339 commits

Author SHA1 Message Date
Graeme Geldenhuys 7e613c6853 fix(codegen): use narrow load/store for implicit-Self Byte/Word/SmallInt fields
The two implicit-Self field paths (assignment to bare Field inside a
method, and reading a bare Field name) hardcoded storew/loadw whenever
QbeTypeOf was 'w'.  After SizeOf(Byte)=1 was introduced — and now with
SmallInt / Word at 2 bytes — that over-writes (or over-reads) the
adjacent fields.  Symptom: setting four Byte fields A..D in sequence
left A reading as 0x04030201 instead of 1.

Route both sites through StoreInstrFor / LoadInstrFor, matching the
explicit obj.Field codepath that already calls the helpers.  No change
for Integer / UInt32 / Enum / Boolean / Set (still storew / loadw); the
helpers select storeb / loadub for Byte and storeh / loadsh / loaduh
for the new 16-bit types.

Two e2e tests pin the regression:
  - TestRun_ImplicitSelf_ByteFields_NoBleed (4 adjacent Byte fields)
  - TestRun_ImplicitSelf_SmallIntWord_Fields (SmallInt + Word)
2026-05-22 08:58:16 +01:00
Graeme Geldenhuys 5d092cafee feat(types): add SmallInt / Word 16-bit integer types
Introduces tySmallInt and tyWord first-class types alongside the
existing Integer / Int64 / Byte / UInt32 / UInt64 family.  Storage is
2 bytes (storeh / loadsh / loaduh) but values are widened to QBE 'w'
in registers, matching the Byte pattern.  Int16 and UInt16 are
accepted as aliases.

Implicit widening into Integer, Int64, UInt32 and UInt64 is permitted;
all 16-bit values fit losslessly in those wider types.

Tests: 11 IR-level + 5 e2e in cp.test.smallint_word.pas.

Grammar and language-rationale updated to remove the "deferred" note
on SmallInt/Word.
2026-05-22 08:51:21 +01:00
Graeme Geldenhuys b43a999f80 feat(types): add UInt64 / QWord type
Adds a real 64-bit unsigned integer type with two equivalent names:
UInt64 (Delphi style, matches the existing Int64) and QWord (FPC
style).  PtrUInt now aliases UInt64 too — it's the natural pointer-
sized unsigned on 64-bit.

Language semantics:
- Arithmetic on UInt64 uses udiv/urem; add/sub/mul/and/or/xor/shl/shr
  are bit-identical to their signed counterparts.
- Comparisons use unsigned QBE ops (cultl, cugtl, ...).
- Int64 <-> UInt64 mixing requires an explicit cast; the two types are
  not implicitly convertible in either direction.
- Decimal/hex literals in the (2^63, 2^64-1) range are typed as
  UInt64.  Smaller literals stay Integer/Int64.
- SizeOf(UInt64) = SizeOf(QWord) = 8.

Runtime:
- New _UInt64ToStr in blaise_str.pas plus a WriteDecimalU helper that
  uses UInt64 arithmetic.
- SysWriteUInt64 added to the platform abstraction and implemented in
  the POSIX layer.  WriteLn(UInt64) routes through it.
- IntToStr(UInt64) routes to _UInt64ToStr; explicit UInt64ToStr is
  also exposed as a builtin.

Bootstrap notes:
- Older release binaries cannot compile the runtime any more because
  rtl.platform.pas declares SysWriteUInt64.  A stage-2 rebuild from a
  fresh stage-1 is required after this commit on any worktree with an
  older stage-1, per CLAUDE.md.
- The parser stages literal Value through local var-params rather than
  writing directly to the new TIntLiteral.IsUInt64 field via class-field
  out-params.  Working around a stage-1 codegen bug where var-param
  calls that target a class field silently fail to write back.

Docs:
- docs/grammar.ebnf: built-in type list expanded with UInt64/QWord,
  integer-literal typing rules documented.
- docs/language-rationale.adoc: integer types table updated with the
  unsigned variants, Int64<->UInt64 strict conversion rule explained.

Tests:
- 15 new tests in cp.test.uint64.pas covering symbol-table
  registration, codegen instruction picking (udiv/urem/cultl/cugtl),
  literal-range typing, and e2e round-trips.
- Full suite: 2151 tests pass (up from 2132).
- Fixpoint clean at stage-3/stage-4 (expected: type-system change).
2026-05-22 08:08:35 +01:00
Graeme Geldenhuys a899c937a2 feat(testing): accept multiple --suite filters
Previously --suite could only be passed once; subsequent occurrences
silently overwrote earlier ones, making it hard to run a curated subset
of tests in one invocation.  The runner now collects every --suite
value into a filter list and runs a test if any filter matches.

Each --suite value may also be a comma-delimited list, so

  TestRunner --suite TA --suite TB.m
  TestRunner --suite TA,TB.m

are equivalent.  A filter with no '.' matches any method in that class;
a filter of the form Class.Method pins one specific method.  Empty
entries and surrounding whitespace are ignored.  An empty filter list
runs everything (unchanged default).

The threaded-subprocess fan-out is skipped when filters are supplied —
matching tests run in-process, which is what users want when narrowing
a debug session.

Helpers SplitSuiteSpec, AppendSuiteFilter, and MatchesFilters are now
exposed for direct unit testing in cp.test.runner_filters.pas (13
tests).
2026-05-21 23:27:21 +01:00
Graeme Geldenhuys 525f36e769 fix(codegen): pack Byte/Boolean fields at 1-byte stride
SizeOf(Byte) reported 4 instead of 1, and Byte/Boolean fields inside
records were laid out at 4-byte stride.  Both broke struct interop with
external libraries and produced surprising SizeOf results.

Changes:
- TTypeDesc.ByteSize and AllocAlign return 1 for tyByte / tyBoolean.
- TRecordTypeDesc gains PackedSize (cumulative offset, no tail pad) and
  TotalSize now tail-pads records (but not classes) up to MaxAlign.
  MaxAlign defaults to 1 so all-Byte records report their natural size.
- AddField/PackedSize align each field to its AllocAlign so a Byte
  followed by an Integer correctly pads the Integer to offset 4.
- Codegen gains LoadInstrFor/StoreInstrFor helpers and uses loadub/storeb
  on byte-sized field accesses to avoid over-reading/over-writing into
  adjacent fields.  Open-array element reads use the same helper.

Pointer fields are now correctly aligned to 8 inside records (previously
they could land on a 4-byte boundary after an Integer field).  The
TNode-with-vptr+Integer+pointer test moves from size 20 to size 24 to
reflect this — the old size was wrong on strict-alignment targets.

Tail-padding is intentionally skipped for class instances so InstanceSize
matches FPC/Delphi semantics (classes are never directly stored in
arrays — only references to them are).

Tests:
- TestSemantic_FourByteRecord_TotalSizeIs4
- TestSemantic_ByteThenInteger_AlignsInteger
- TestSemantic_ByteFieldOffsets_Are0123
- TestCodegen_SizeOfByte_Is1
- TestCodegen_SizeOfFourByteRecord_Is4
- TestRun_Record_FourByteFields_PackedAndRoundTrip
- TestRun_Record_ByteThenInteger_RoundTrip
2026-05-21 23:17:10 +01:00
Graeme Geldenhuys 80488bf0a4 test(bench): record TList<TObject> scan speedup from ARC elision
After commit ffe09b3 (nil-slot _ClassRelease elision), TList<TObject>
hand-rolled scan x 1000 went 131 ms -> 90 ms (~30% improvement) on
the same hardware as the previous entry.  Smaller wins on sequential
and random Get for the same workload.
2026-05-21 17:28:44 +01:00
Graeme Geldenhuys a6c87e33ea chore(clean): remove all old FPC compiler directives we don't need in Blaise
There were still from the days when FPC compiled Blaise's tests.
2026-05-21 17:23:38 +01:00
Graeme Geldenhuys ffe09b395f perf(codegen): elide _ClassRelease on provably-nil slots
When the LHS of a class-typed assignment is a local slot that has not
yet been written within the function entry block, EmitVarAllocs has
just zeroed it via `storel 0, %_var_X`.  Releasing nil is a no-op at
runtime but still pays a function call + ret per assignment.  Skip
the load+release entirely in this case; only the addref+store
remain.

Tracking is conservative:
 * `FArcSlotWritten` is cleared at every function start.
 * `ArcSlotIsNil(X)` is True only while we are still in the @start
   block and X has not been written.
 * Any new label (branch/loop/case body) ends the elision window for
   all slots; that prevents leaking a value written along a branch we
   did not see.
 * Globals, var-params and mem2reg-promoted locals are excluded.

Impact on `tests/bench_lists.pas` (Ryzen 7 5800X):

  TList<TObject> hand-rolled scan x 1000:  131 ms -> 90 ms  (~30%)
  Sequential Get(i) x 1_000_000:             6 ms ->  5 ms
  Random      Get(i) x 1_000_000:           13 ms -> 12 ms

The IR for `TList<T>.Get` now shows a single AddRef call instead of
the prior AddRef+Release pair on the Result slot.

Adds three IR tests in cp.test.arc covering: first-store elision,
second-store still-releases, and post-branch still-releases.
All 2112 tests pass; fixpoint clean at stage-3/stage-4.
2026-05-21 17:17:03 +01:00
Graeme Geldenhuys 90b15afce4 feat(stdlib): add TList<T>.IndexOf
Linear scan over FData returning the index of the first equal element
or -1 when not found.  Matches the semantics of TSet<T>.IndexOf which
the rest of the unit already exposes, and saves callers from writing
the hand-rolled scan that bench_lists.pas demonstrates.

Adds two e2e tests covering the integer/found and string/not-found
paths.
2026-05-21 15:23:35 +01:00
Graeme Geldenhuys 07b9b8ddbd test(bench): merge bench_strings + bench_objects into bench_lists
The multi-instance generic miscompile that forced the two-program
split is fixed (b6736d3), so the four workloads now run in a single
program: TStringList, TList<String>, TObjectList, TList<TObject>.

Add a fresh results entry on the post-fix compiler; timings are
within noise of the split run on the same hardware.
2026-05-21 14:37:22 +01:00
Graeme Geldenhuys b6736d3925 fix(semantic): clone method bodies per generic instance
Generic instance method bodies were shared via OwnBody=False, so a
single TBlock AST was annotated by the semantic analyser once for each
instance in turn.  The Resolved* annotations on the shared body ended
up reflecting whichever instance was analysed last, causing call
targets in earlier instances to point at later instances' helpers —
e.g. TList<String>.Add would emit `call $TList_TObject_Grow` and skip
$_StringAddRef on the value parameter.

Add CloneBlock/CloneStmt/CloneExpr in uAST.pas covering all node types
that can appear inside a method body (~26 expr/stmt forms plus block-
level Var/Const/Type/Proc decls).  Annotations are deliberately not
copied — each instance re-runs semantic analysis on its own AST.
Wire the clone into InstantiateGeneric and InstantiateGenericFunction
so each instance owns its body.

Adds TestCodegen_Generic_TwoInstances_MethodCallsResolveToOwnInstance
to lock in: with TBox<Integer> and TBox<String> in one program, the
emitted TBox_Integer_Init body must contain
`call \$TBox_Integer_SetValue` and the TBox_String_Init body must
contain `call \$TBox_String_SetValue`.  Previously both bodies called
the String instance's helper.

Verified: 2107 tests pass; fixpoint clean; the two-instance benchmark
scenario (TList<String> + TList<TObject> in one program) now compiles
and runs correctly.
2026-05-21 14:30:24 +01:00
Graeme Geldenhuys 39136547f5 fix(semantic): allow two instances of the same generic class
A program that instantiated the same generic class with two different
concrete arguments (e.g. TList<String> and TList<TObject>) failed at
semantic analysis with 'Type mismatch: expected ''^T'' but got
''^TObject'''.  Two compounding causes:

- FindTypeOrInstantiate cached compound types ('^T', 'array of T',
  'class of T', 'array[lo..hi] of T') under the literal input string.
  When 'T' was a scope-bound type parameter the cached entry pinned
  the first instance's resolution ('^String'), so a second
  instantiation that bound T to a different type re-used the wrong
  entry.  Key these caches by the canonical element-type name
  ('^String', 'array of TObject', ...) so each (constructor + element)
  pair has its own slot.

- AnalyseMethodCallExpr mutated AExpr.ObjectName to the resolved class
  name ('TFoo<T>' -> 'TFoo<String>') on first analysis.  Because
  cloned generic methods share the body AST (NewMDecl.OwnBody := False
  in InstantiateGeneric), the mutation was visible to the next
  instance's analysis and made it look up the wrong concrete class.
  Only normalise casing when the lookup hit the original spelling;
  otherwise keep the template form and pass the resolved name to
  overload resolution via a local.

A separate codegen issue still affects per-instance method body
emission when the body is shared — each function is emitted but the
shared body's call-target annotations come from whichever instance
was analysed last, so e.g. TList<String>.Add can end up calling
TList<TObject>.Grow.  Tracked separately; needs per-instance body
cloning to fix properly.
2026-05-21 11:16:37 +01:00
Graeme Geldenhuys db84dc8b3f test(bench): TStringList vs TList<String>, TObjectList vs TList<TObject>
Micro-benchmark comparing the legacy list types against the generic
list equivalents introduced for self-hosting.  Logs the median of two
runs to tests/bench_lists_results.txt for trend tracking — append a
new dated section after each meaningful runtime or codegen change.

Phases per container: bulk insert (1M), sequential read, random read
(1M), for..in iteration (strings only), and indexed lookup (IndexOf or
hand-rolled scan).  Initial run on AMD Ryzen 7 5800X shows TList<String>
beating TStringList across the board, while TObjectList still beats
TList<TObject> on writes and on IndexOf — the latter by ~8x due to
direct pointer-scan vs. one Get call per element.
2026-05-21 10:53:13 +01:00
Graeme Geldenhuys 93358ed08a fix(codegen): retain/release class refs stored through typed pointer
EmitPointerWrite emitted retain+release for tyString but had no parallel
case for tyClass.  Storing a class reference through a typed pointer
('PItem^ := SomeObj' where PItem: ^TItem) therefore left the slot
without a strong reference, so the object was freed as soon as the
original holder dropped its ref — subsequent reads returned dangling
memory.

This surfaced when TList<TObject>.Add stored items through its 'FData:
^T' backing buffer: every Get returned garbage once the caller reused
the local variable that had supplied the value.  TList<String> worked
because the string path was already in place.

Add the symmetric tyClass branch.
2026-05-21 10:50:56 +01:00
Graeme Geldenhuys ed7323a618 feat(stdlib): add TListEnumerator<T> + TList<T>.GetEnumerator
Wires up for..in on TList<T> from generics.collections.  Two compiler
fixes were required:

- uSemantic: TClassName<T>.Method(args) inside a generic method body
  failed to resolve because AnalyseMethodCallExpr looked up the literal
  name with the unsubstituted type parameter.  Now mirrors the existing
  field-access path: if the receiver name contains '<' and lookup fails,
  apply ResolveScopeBoundTypeParams + FindTypeOrInstantiate before
  re-trying the lookup.

- uCodeGenQBE: constructor-with-args path emitted
  $_FieldCleanup_TListEnumerator<String> with literal angle brackets
  which QBE rejects.  Mangle the class name like the no-arg path
  already does.
2026-05-21 10:25:19 +01:00
Graeme Geldenhuys 6352c6fcd0 fix(semantic): wire up parent class on generic instances
InstantiateGeneric did not assign RT.Parent, CopyVTableFrom parent, or
copy parent fields onto the synthesised TRecordTypeDesc. With no parent
vtable copied in, RT.HasVTable returned false for every generic
instance, so the codegen skipped emitting $vtable_<T> while the
typeinfo emitter still referenced it — producing an unresolved symbol
at link time for any program that used TList<X>, TStack<X>, TQueue<X>,
TSet<X>, TDictionary<K,V>, etc. from generics.collections.

Mirror the regular class-resolution path in AnalyseProgramTypes: look
up the explicit parent (or fall back to TObject when no parent name is
given), set RT.Parent, CopyVTableFrom the parent so Destroy/ToString
slots are inherited, and copy the parent's fields so FindField sees
them. The order is preserved — parent-wiring runs before the vtable
slot pre-pass which runs before field resolution, so TotalSize
correctly accounts for the 8-byte vptr.

Add cp.test.e2e.tlist.pas — the first e2e coverage of a stdlib
generic class. IR-only tests would not have caught this because the
link step is what failed.
2026-05-21 10:01:28 +01:00
Graeme Geldenhuys 8cced6c91f fix(semantic): walk parent chain when resolving inherited properties
FindProperty and FindIndexedProperty searched only the current class's
own Properties list, so a subclass could not see properties declared on
its ancestors. Inherited fields work because parent fields are copied
into the child during class resolution, but properties are not copied —
they must be discovered via inheritance walking, as FindMethodDecl
already does.

Fixes #45.
2026-05-21 09:44:24 +01:00
Graeme Geldenhuys 1b531e051f fix(semantic): register var decls before analysing method bodies
Class methods in a program could not access program-level globals because
AnalyseBlock analysed method bodies before pushing the block scope and
registering its var declarations. Units were unaffected because
AnalyseUnit already registers impl-section vars first.

Closes #43.
2026-05-21 09:30:33 +01:00
Andrew Haines 56014894d3 fix(semantic): use overload resolution for constructor calls
Constructor calls like TFoo.Create(arg) were using FindMethodDecl, which
returns the first MethodDecl indexed for the TypeName.MethodName key.
With overloaded constructors that share a name (e.g. one-arg Create vs
two-arg Create), the picked candidate may have a higher arity than the
call site.  AppendDefaultArgs then fails on the unfilled parameters
with "No default value for parameter 'X' of 'Create'" — even though a
matching lower-arity overload exists.

Reproduces with a class declaring Create(A, B) before Create(A): the
two-arg overload is indexed first and chosen for a one-arg call site.

Replace FindMethodDecl with ResolveMethodOverload at the constructor
call site; fall back to FindMethodDecl when the resolver returns nil
(extern/RTL constructors not registered in FMethodIndex).

Adds IR tests in cp.test.overload.pas and an E2E test in
cp.test.e2e.classes2.pas that exercise the declaration-order-sensitive
case.

Original contribution from Andrew Haines <andrewd207@aol.com>
(https://github.com/andrewd207/blaise_llvm/commit/c2a3bbed).
2026-05-21 08:48:48 +01:00
Graeme Geldenhuys 2b3840265a fix(codegen): ARC retain/release on array element writes
Subscript assignment into managed-element arrays (string or class)
emitted only a raw store, leaking the new value's refcount and dropping
the old value's release. After two writes to the same slot, the first
value was leaked and the program ran with an unbalanced refcount.

EmitStaticSubscriptAssign now emits the standard retain-new / release-
old pair before the store, for both the dynamic-array branch and the
static-array branch.

Regression coverage in cp.test.staticarray:
- TestCodegen_StaticArray_StringWrite_EmitsARC
- TestCodegen_StaticArray_ClassWrite_EmitsARC
- TestCodegen_DynArray_StringWrite_EmitsARC
- TestCodegen_DynArray_ClassWrite_EmitsARC

Original contribution from Andrew Haines (andrewd207).
2026-05-21 08:36:15 +01:00
Graeme Geldenhuys 8fd356accc fix(codegen): allocate 16 bytes for procedure-of-object class fields
A 'procedure of object' value is a 16-byte TMethod (Code+Data), but
TTypeDesc.ByteSize/RawSize fell through to 8 for procedural fields,
and EmitFieldAssignment stored a single 8-byte pointer instead of
memcpy-ing the 16 bytes inline. Reading the field then dereferenced
freed stack memory and crashed at runtime.

Fix in three places:
- uSymbolTable.ByteSize/RawSize: return 16 for IsMethodPtr
- EmitFieldAssignment: memcpy 16 into the field address
- Field-read paths (class + record): return field address for
  method-ptr fields so callers treat the field storage as the value

Regression coverage in cp.test.proctypes_ofobject:
- TestSemantic_MethodPtr_ByteSizeIs16
- TestSemantic_BareProcPtr_ByteSizeIs8
- TestCodegen_MethodPtrField_AssignUsesMemcpy16
- TestCodegen_MethodPtrField_RecordTotalSizeIncludes16
- TestE2E_MethodPtrField_RoundTrip (class field round-trip)

Original contribution from Andrew Haines (andrewd207).
2026-05-21 08:28:48 +01:00
Graeme Geldenhuys 4fc5d2c5e4 fix(codegen): preserve ARC variables across try/finally re-raise
Remove EmitExcPathArcCleanup from the try/finally exception path.
The cleanup was releasing and nilling all ARC-managed variables (strings,
objects) before re-raising the exception, which destroyed variable state
that the outer except handler needed to read.

Root cause: the finally-exception path ran _StringRelease + storel 0 on
every in-scope ARC variable before calling _Reraise.  When a try/finally
was nested inside a try/except, the outer handler saw nil strings and
objects instead of the values set by the try body and finally body.

The function-exit block already handles final ARC cleanup for all
variables, so the premature cleanup on the exception path was both
redundant and incorrect.

Added E2E test TestRun_FinallyStringMutation_SurvivesReraise that
verifies string mutations in the finally block are visible after
re-raise to the outer except handler (matching FPC behaviour).

2084 tests pass, fixpoint verified (stage-3/stage-4).
2026-05-21 01:13:17 +01:00
Graeme Geldenhuys 4969b74810 refactor(runtime): replace blaise_exc.c with Pascal + x86_64 assembly
Port all exception frame management, type identity, and runtime check
functions from C to pure Pascal (blaise_exc.pas).  Replace libc
setjmp/longjmp with a minimal custom implementation in x86_64 assembly
(blaise_setjmp_x86_64.s, ~40 lines) that saves only the 8 callee-saved
registers (64-byte jmp_buf vs libc's 200-byte one).

This eliminates the last C source file from the runtime build.  The only
non-Pascal code in the runtime is now the assembly setjmp stub.

Changes:
- runtime/src/main/asm/blaise_setjmp_x86_64.s: _blaise_setjmp/_blaise_longjmp
- runtime/src/main/pascal/blaise_exc.pas: _PushExcFrame, _PopExcFrame,
  _Raise, _Reraise, _CurrentException, _CurrentExceptionMessage,
  _IsInstance, _ImplementsInterface, _GetItab, _Raise_InvalidCast,
  _CheckNil — all ported from blaise_exc.c
- uCodeGenQBE.pas: emit call $_blaise_setjmp instead of call $setjmp
- cp.test.exceptions.pas: IR assertion updated for new symbol name
- runtime/Makefile: assembly rule, Pascal blaise_exc build, C rules removed
- .gitignore: whitelist runtime/src/main/asm/*.s
- runtime/src/main/c/blaise_exc.c: deleted

2083 tests pass, fixpoint verified (stage-3/stage-4).
2026-05-21 01:13:17 +01:00
Graeme Geldenhuys 127bc35490 refactor(runtime): port blaise_arc_class.c to pure Pascal
Move _ClassAlloc, _ClassRelease, _StringReleaseCheck, and
_AbstractMethodError from C into blaise_arc.pas. Uses
TFieldCleanupProc procedure type to call the compiler-emitted
field cleanup function pointer from the class header.

Only blaise_exc.c (setjmp/longjmp) remains in C — everything
else in the runtime is now pure Pascal.

All 2083 tests pass. Fixpoint verified.
2026-05-21 01:13:09 +01:00
Graeme Geldenhuys 1507bf9877 refactor(runtime): consolidate IO/process/sys/time into rtl.platform.posix
Merge blaise_io.pas, blaise_process.pas, blaise_sys.pas, and
blaise_time.pas into the OO platform facade (TRtlPlatformPosix).
All underscore ABI stubs now delegate through GRtlPlatform, enabling
future macOS/Windows/FreeBSD support via a single new TRtlPlatformXxx
subclass with no ifdefs or scattered platform files.

Key design decisions:
- _SetArgs creates GRtlPlatform eagerly (runs before unit _init)
- Path utilities stay in blaise_str.pas (platform-independent)
- Makefile sed strip keeps TObject typeinfo from program section
- Shared string helpers (StrAlloc, StrFromCStr) deduplicated

Deleted 8 files (4 units + 4 build drivers), replaced by the
expanded rtl.platform.posix.pas and its single build driver.

All 2083 tests pass. Fixpoint verified.
2026-05-21 01:13:08 +01:00
Graeme Geldenhuys 96245f8ba0 feat(testing): run [Threaded] E2E suites as parallel subprocesses
Mark all 26 E2E test classes with the [Threaded] attribute. The test
runner now launches each [Threaded] suite as an independent subprocess
in parallel, then collects and merges results after in-process tests
finish. Wall-clock test time drops from ~15s to ~3s.

Changes:
- blaise.testing: add ThreadedAttribute, TTestResult.MergeFrom
- blaise.testing.runner.text: launch all [Threaded] suites via TProcess
  up front, run in-process tests, then collect subprocess output and
  parse verbose results back into the main TTestResult
- cp.test.e2e.*.pas: add [Threaded] annotation to all 26 E2E classes
2026-05-21 01:13:08 +01:00
Graeme Geldenhuys 363487b0f9 fix(codegen): nil class/string field slots after .Free to prevent double-free
The destructor and the compiler-generated _FieldCleanup function both
released ARC-managed class fields, causing a double-free that corrupted
the arena freelist. After ~100 create/destroy cycles the corruption
manifested as a garbage string header in an unrelated TStringList.

Two changes:
- EmitFieldCleanupFn: store nil into each field slot after releasing it
- EmitMethodCall ObjExpr .Free path: compute the field address via
  EmitLValueAddr, release, then nil — matching the IsImplicitSelf path

With both sites nilling after release, the second release loads nil and
_ClassRelease returns immediately.
2026-05-21 01:12:58 +01:00
Graeme Geldenhuys 2896204841 feat(attributes): add custom attribute language feature
Implement Delphi-style custom attributes: TCustomAttribute base class,
[AttrName] syntax on class declarations, attribute RTTI emission, and
the HasClassAttribute(AClass, AAttrClass) builtin for runtime queries.

Pipeline changes:
- uAST: add Attributes: TStringList to TClassTypeDef
- uParser: parse [Attr] before type names (ParseTypeSection loop now
  accepts tkLBracket; ParseTypeDecl collects and copies to class def)
- uSymbolTable: register TCustomAttribute builtin; add ClassAttributeCount/
  ClassAttributeAt accessors to TRecordTypeDesc
- uSemantic: resolve attribute names using Delphi suffix convention
  ([Threaded] matches ThreadedAttribute); semantic error for unknown attrs;
  [Weak] remains compiler-intrinsic and is skipped; add HasClassAttribute
  builtin handler in AnalyseFuncCallExpr
- uCodeGenQBE: extend typeinfo from 7 to 8 l-slots (slot 7 = $attrs_T or 0);
  emit $attrs_T count+typeinfo-ptr tables; emit TCustomAttribute vtable and
  field-cleanup stubs unconditionally; emit HasClassAttribute as a call to
  $_HasClassAttribute
- blaise_arc: implement _HasClassAttribute — reads slot 7, scans the attrs
  table, walks the parent chain so inherited attributes are visible

19 tests in cp.test.attributes.pas covering parser, semantic, codegen, and
four E2E round-trips. Updated cp.test.publishedrtti.pas for 8-slot layout.
2026-05-20 11:01:12 +01:00
Graeme Geldenhuys 25c1f83cd9 feat(threading): add TThread, TCriticalSection and POSIX thread bindings
Add blaise_thread.pas runtime unit wrapping pthread_create, pthread_join,
pthread_mutex_init/lock/unlock/destroy and sysconf for GetCPUCount.

Implement TThread (Create, Start, Terminate, WaitFor, Execute virtual,
FreeOnTerminate) and TCriticalSection (Enter/Leave) in classes.pas using
ARC-safe ref counting in the thread trampoline.

Fix EmitFieldCleanupFn to walk the parent chain when emitting the Destroy
call — previously a subclass without its own Destroy would generate an
empty cleanup, causing leaked resources from parent destructors.

Link -lpthread in both the compiler driver and E2E test base.

Include 7 E2E tests covering basic execution, WaitFor blocking, Terminate
flag, Finished flag, multiple threads, mutex-protected counter, and
inherited Destroy cleanup.
2026-05-20 08:31:15 +01:00
Graeme Geldenhuys a8a6e2b35e fix(codegen): emit correct QBE types for Single float arithmetic and comparisons
Binary expressions involving Single operands always emitted d-typed (Double)
QBE instructions, causing QBE to reject mixed s/d operands.  Now the codegen
respects the resolved float type: Single op Single emits =s ops, Single op
Double widens the Single operand via exts before =d ops, and Single comparisons
use s-suffixed comparison opcodes (clts, cgts, etc.).
2026-05-20 00:32:35 +01:00
Graeme Geldenhuys 28d1cb009a fix(semantic): allow implicit integer-to-float assignment
Delphi and FPC allow assigning an integer value to a Double or Single
variable without an explicit cast.  Blaise previously rejected this as a
type mismatch.  Add an IsFloat/IsNumeric widening rule in the semantic
pass and emit the correct QBE conversions (swtof, sltof) in codegen for
both Double and Single targets.
2026-05-20 00:22:59 +01:00
Graeme Geldenhuys 95c07ca580 feat(runtime): port blaise_str_fmt.c to pure Pascal _StringFormatN
Replace the C variadic _StringFormat with a pure Pascal _StringFormatN
that takes a format string, a pointer to an array of tag/value pairs,
and a count. The codegen allocates the arg array on the stack via
alloc8, stores (tag:l, value:l) pairs, and calls the non-variadic
function — eliminating the last dependency on C va_list.

This required a multi-stage bootstrap: release binary compiled stage-1
(with new codegen), stage-1 compiled stage-2 (self-hosted with new ABI),
then the C file could be safely removed. Integer args are widened via
extsw when w-typed to satisfy QBE's storel type checking.

Only 2 C files remain in the runtime: blaise_exc.c (setjmp/longjmp)
and blaise_arc_class.c (function-pointer dispatch).
2026-05-19 23:58:58 +01:00
Graeme Geldenhuys 802decba69 feat(runtime): port blaise_float.c to pure Pascal Grisu1 implementation
Replace the C-based float support (_DoubleToStr, _SingleToStr,
_StrToDouble, _AbsInt, _AbsInt64) with a pure Pascal implementation
using a simplified Grisu1 algorithm (Loitsch 2010) for float-to-string
conversion and native double arithmetic for string-to-float.

The new implementation has no libc dependency for the conversion logic
itself (memcpy is still used in BlaiseAllocStr as a temporary measure).
This is a step toward full platform independence.

Also fix a codegen bug where Int64-to-Double promotion in mixed
arithmetic/comparison expressions emitted swtof (32-bit truncation)
instead of sltof (64-bit). This affected three sites in EmitBinExpr
and one in EmitAssignment.
2026-05-19 23:19:52 +01:00
Graeme Geldenhuys 86b87b6c27 feat(runtime): port blaise_weak.c to Pascal
Replace the C weak-reference table implementation with an equivalent
Pascal unit (blaise_weak.pas).  Same data structures — open-chained
hash table with linked slot lists — using _BlaiseGetMem/_BlaiseFreeMem
and ^Pointer locals for the void** dereference pattern.

Deletes blaise_weak.c; runtime C files now down to 4 (blaise_arc_class,
blaise_exc, blaise_float, blaise_str_fmt).

All 2023 tests pass including E2E weak-ref Valgrind test.  Fixpoint OK.
2026-05-19 16:11:14 +01:00
Graeme Geldenhuys d45f8196db feat(runtime): port blaise_io / blaise_process / blaise_sys / blaise_time to Pascal
Step 13 (partial): replace four C shims with self-hosted Pascal units that
bind directly to libc syscall surfaces. blaise_exc.c, blaise_arc_class.c,
blaise_weak.c, blaise_str_fmt.c, and blaise_float.c remain in C — they
either need setjmp/longjmp, function-pointer fields in records, or
variadic interfaces that QBE / Blaise cannot express today.

Ports:
- blaise_io.c → blaise_io.pas
  File I/O, env vars, working directory, ParamStr, mkstemp, sleep,
  process ID. Uses libc bindings (open/read/write/stat/...) declared in
  the interface section per the migration rule in CLAUDE.md.
- blaise_process.c → blaise_process.pas
  fork/exec/pipe/waitpid wrapped as TProcess; same libc-binding pattern.
- blaise_sys_posix.c → folded into blaise_sys.pas
  Tiny shim (just _SysWrite + _SysWriteNewline); now writes directly via
  posix_write. No separate C file needed.
- blaise_time.c → blaise_time.pas
  clock_gettime + date arithmetic. The local typed-array-const Days
  triggered the codegen bug fixed in fa69ae1.

Each unit is built via a thin build_driver.pas program (see existing
blaise_str_build_driver.pas pattern): the Makefile compiles the driver,
strips everything from the program section onward, then assembles + links
the unit-only IR into the runtime archive.

Outcome:
- C_SRCS shrinks from 9 → 5 files; runtime archive size unchanged in
  spirit but the dependency surface contracts.
- Standalone units, not yet folded into rtl.platform.posix.pas — that
  consolidation can happen later when the platform layer matures and a
  second backend (windows / darwin) appears to justify the abstraction.
- All 2023 tests pass; fixpoint OK.
2026-05-19 01:04:17 +01:00
Graeme Geldenhuys fa69ae1b1c fix(codegen): emit data items for typed array consts inside function bodies
A typed array constant declared inside a function body (or class method
body) had its element values held in TConstDecl.ArrayElements but no
`data $Name = { ... }` item was emitted in the data section.  The function
code referenced `$Name` (e.g. `add $Days, %_t`), which produced
`undefined reference to Name` at link time.

EmitGlobalConstData walked only the top-level program/unit blocks; method
bodies were never visited.  Add EmitLocalArrayConstsInBlock /
EmitLocalArrayConstsInTypeDecls / EmitLocalArrayConstsInProgram /
EmitLocalArrayConstsInUnit which sweep standalone procs and class/record
methods and emit any locally-declared typed array constants.

Wired into all four IR-emission sites: Generate / AppendProgram /
GenerateUnit / AppendUnit.  GenerateUnit was also missing the existing
EmitGlobalConstData calls for IntfBlock/ImplBlock — added those for
consistency.

Tests:
- IR: TestArrayConst_LocalInFunction_EmitsDataItem,
       TestArrayConst_LocalInProcedure_EmitsDataItem,
       TestArrayConst_LocalInMethod_EmitsDataItem assert the IR contains
       `data $Name =` for each scope.
- E2E: TestRun_Const_LocalArrayInFunction compiles+links+runs a program
       whose function uses a local typed array constant and reads three
       elements, verifying the full toolchain catches the link error.

Unblocks 36 previously-failing DateUtils e2e tests (blaise_time.pas
declared `Days: array[1..12] of Integer` inside _TimeDaysInMonth).
2026-05-19 00:55:55 +01:00
Graeme Geldenhuys 4b4e560b27 fix(codegen): fold single-char string literal in byte-store RHS
P[I] := #N (and any single-char string literal like 'A') in a byte-store
context lowered through the normal string-literal path, emitting a
1-byte string data item and storing the low byte of its data pointer
into the target slot — yielding the address byte, not Ord(N).

This corrupted NUL terminators written by RTL helpers such as StrAlloc
and StrFromCStr in blaise_io.pas, causing _FileExists (and any libc
function reading NUL-terminated strings) to see one byte of garbage
past the intended end.

Extend EmitByteRhs to recognise TStringLiteral of length 1 and emit
the Ord as a `=w copy N` constant, mirroring the existing Chr(N)
short-circuit. Both traps share the same root cause and now share
the same fix.

Tests:
- IR: TestCodegen_PCharSubscript_HashCharLiteralShortCircuit asserts
  the IR contains no `storeb $__s` or `add $__s` patterns for
  `p[0] := #0`.
- E2E: TestRun_PCharSubscript_HashCharLiteralAssignment compiles and
  runs a program that builds 'ABCD' + #0 via per-byte writes and
  verifies the resulting C string.
2026-05-18 22:17:43 +01:00
Graeme Geldenhuys 6d537238e7 feat(forin): extend for..in to support dynamic arrays
Dynamic arrays are now valid collection expressions in for..in loops.
Iteration is 0-based (Low=0, High=Length-1), guarded by a runtime
call to $_DynArrayLength so empty/nil arrays iterate zero times.

- uAST: add IsDynArrayIter flag to TForInStmt
- uSemantic: add tyDynArray branch (validates element/var type, injects
  synthetic __idx_N slot, same pattern as static array path)
- uCodeGenQBE: add IsDynArrayIter codegen path (csltw index < length,
  data-pointer arithmetic identical to static array)
- cp.test.forin: 7 new unit tests (semantic + IR codegen)
- cp.test.e2e.staticarray: 2 new E2E tests (filled array; empty/nil array)
2026-05-18 17:53:15 +01:00
Graeme Geldenhuys 19056cffbf fix(semantic): normalise all identifier references to declared casing
Pascal is case-insensitive but QBE symbols are case-sensitive.  The
parser stores source-level casing on AST nodes, and the codegen emits
those names as QBE symbols.  When a variable declared as 'inFile' was
referenced as 'infile.Size', the definition emitted $inFile but the
reference emitted $infile, causing a linker error.

The previous commit (952667e) fixed this for method call names only.
This commit extends the fix to all remaining AST name fields that flow
to QBE symbol emission: TFieldAccessExpr.RecordName, TFieldAssignStmt
.RecordName, TMethodCallExpr.ObjectName, TMethodCallStmt.ObjectName,
TProcCallStmt.Name, TFuncCallExpr.Name, TForStmt.VarName, TForInStmt
.VarName (both paths), TArrayAssignStmt.ArrayName, TSupportsExpr
.OutVarName, and the @-operator path in AnalyseAddrOfExpr.
2026-05-18 17:43:28 +01:00
Graeme Geldenhuys 7d4b852ec4 feat(config): add blaise.cfg for automatic unit-path discovery
The compiler now reads unit-path entries from a config file so users
no longer need to pass --unit-path for the runtime and stdlib on every
invocation.  Search order: next to the binary, then ~/.blaise.cfg.
Relative paths are resolved against the config file's directory.

Includes a dev-layout config in compiler/src/main/resources/ (PasBuild
copies it to compiler/target/ automatically), a new uConfig unit with
the parsing logic, and 12 unit tests for config parsing.
2026-05-18 17:22:24 +01:00
Graeme Geldenhuys 952667ee67 fix(codegen): use declared method name for zero-arg call symbols
Two code paths in EmitExpr built method-call symbols by concatenating
FldAccess.FieldName (the raw source-code casing) instead of routing
through MethodEmitName which resolves via ResolvedQbeName.  This caused
linker errors when a call site used different casing than the declaration
(e.g. Reader.readline vs ReadLine), since the emitted symbol name
wouldn't match the function definition.

Route both paths through MethodEmitName and add a fallback that uses
AMDecl.Name (the declared name) when ResolvedQbeName is unset.
2026-05-18 07:19:15 +01:00
Graeme Geldenhuys 196f992ec6 fix(semantic,codegen): accept High/Low on dynamic array types
High() and Low() rejected tyDynArray variables with a semantic error.
Added tyDynArray to the allowed-type sets in both builtins, and added a
tyDynArray codegen branch for High() that calls _DynArrayLength and
subtracts 1. Low() already falls through to the constant-zero path.
2026-05-17 21:01:17 +01:00
Graeme Geldenhuys 0300d84004 fix(semantic): reject implicit float↔integer assignment
Assigning a Double or Single to an Integer variable (or vice-versa)
was silently accepted because CheckTypesMatch treated all numeric types
as interchangeable.  This caused the raw bit-pattern of the float to
be reinterpreted as an integer, producing garbage values at runtime
(e.g. PI*2 assigned to Integer yielded 1413754136).

Split the numeric-widening rule into two:
- integers ↔ integers: still allowed (widening/narrowing)
- floats ↔ floats: allowed (Single ↔ Double promotion)
- float ↔ integer: now a type-mismatch error, matching FPC/Delphi

Callers that legitimately need float→integer conversion must use
Trunc, Round, Floor, or Ceil; integer→float conversion must use
Double() or Single() casts.

Fixes graemeg/blaise#33
2026-05-17 11:14:22 +01:00
Graeme Geldenhuys 8f692cb0f8 feat(math): add ArcSin, ArcCos, Sinh, Cosh, Tanh builtins with Single dispatch
Registers five new trig intrinsics in the symbol table, semantic analyser,
and codegen. Single arguments emit the *f libc variants (asinf, acosf,
sinhf, coshf, tanhf); Double arguments emit the unprefixed variants.
Removes the now-implemented section from docs/future-improvements.adoc.
2026-05-17 01:56:35 +01:00
Graeme Geldenhuys f1d18755e8 docs(grammar): add INT_LIT/FLOAT_LIT token rules with all bases and underscores
INT_LIT previously only covered plain decimal.  Expand the Tokens section
to formally define all four bases ($hex, &octal, %binary, decimal) and
the underscore-separator placement rules.  Add FLOAT_LIT as an explicit
token production (it was absent entirely).  Character-class helpers
(dec_digit, hex_digit, oct_digit, bin_digit) are defined in the
accompanying block comment, consistent with the existing style for
letter/char/any_char.
2026-05-17 01:11:39 +01:00
Graeme Geldenhuys 7b24fa754d benchmark: add literal underscores for better readability in code.
Unit comment block was stale and still referenced the old values.
2026-05-17 01:05:28 +01:00
Graeme Geldenhuys 772bf55eee feat(lexer,parser): octal/binary literals and underscore separators in numbers
Octal (&377) and binary (%11111111) integer literals were tokenised
correctly but silently misconverted at parse time because StrToInt64
does not accept & or % prefixes.  A new ParseIntLiteral helper in
uStrCompat handles all four bases (decimal, $hex, %binary, &octal)
with explicit digit-by-digit conversion for binary and octal.

Numeric literals now also accept underscore separators between digits
(1_000_000, $FF_EC, %0010_0101, &3_77, 3.14_15) following the same
rules as Delphi and FPC: underscores may appear only between digits,
not at the start of the digit run, not adjacent to a decimal point, and
not at the end of a literal.  The tokeniser (uPasTokeniser) accepts the
underscore in all four number branches; ParseIntLiteral and
StripUnderscores (for float values) strip them before conversion.

Fixed a secondary bug in uLexer: the float/int classification checked
for 'e'/'E' anywhere in the token, which caused hex literals containing
those letters (e.g. $FF_EC) to be misclassified as tkFloatLit.  The
check is now gated on the absence of a $ / % / & prefix.

uStrCompat rewritten without {$IFDEF FPC} dead code; all callers
compiled by Blaise only since v0.8.0.

Adds 21 new tests: lexer token shape tests for all bases with/without
underscores, and TParseIntLiteralTests covering conversion correctness
and invalid-placement rejection.
2026-05-17 00:56:29 +01:00
Graeme Geldenhuys a9bfd37ab7 fix(compiler): report missing units even when no --unit-path is given
The unit-loading block was guarded by `SearchPaths.Count > 0`, so a
program with a `uses` clause compiled silently when no search paths were
provided — missing units were never checked (issue #31).

Remove the spurious count guard so TUnitLoader.LoadAll is always called
when the program has used units. With an empty search-path list, Locate
returns '' for every unit name and EUnitNotFound is raised as expected.

Adds a regression test: TestUnitLoader_MissingUnit_NoSearchPaths_RaisesError.
2026-05-17 00:16:32 +01:00
Graeme Geldenhuys 08beb33689 fix(semantic): reject duplicate identifiers across const and var sections
A var declaration shadowing a same-block const (or a const declared
twice in the same block) was silently accepted instead of raising a
Duplicate identifier error.

Root cause: AnalyseBlock registers consts in the outer scope then pushes
a new inner scope before calling AnalyseVarDecls, so FTable.Define
(which only checks the current scope) could not detect the clash.

Fix: AnalyseVarDecls now scans the block's own ConstDecls list before
defining each var name.  AnalyseConstDecls now treats a duplicate as an
error when the collision originates from an earlier const in the same
block; cross-unit const shadowing (e.g. a unit redefining a system.pas
constant) continues to be silently accepted.

Also removes the redundant LineEnding const from blaise.testing.pas —
it is already exported by system.pas.

Fixes: https://github.com/graemeg/blaise/issues/30
2026-05-17 00:02:18 +01:00
Graeme Geldenhuys c17a8b027c chore: begin v0.9.0-dev cycle 2026-05-16 18:42:13 +01:00