Commit graph

542 commits

Author SHA1 Message Date
Graeme Geldenhuys f19a3e4c7d feat(compiler): add --dump-ast flag for post-semantic AST inspection
Prints the resolved AST tree to stdout after semantic analysis, then
exits. Shows node types, resolved types with sizes, field offsets, and
semantic flags (IsImplicitSelf, IsClassAccess, IsVarParam, VarParam,
ImplicitSelfField, etc.) that are critical for diagnosing codegen bugs
where the wrong IR or assembly is emitted due to an incorrect flag or
missing field offset.
2026-06-06 17:02:34 +01:00
Graeme Geldenhuys a7d5240086 fix(codegen): @RecordField inside methods and pointer-to-record deref
Address-of an implicit-self record field (@Pt inside a method where Pt
is a class field) emitted a reference to a non-existent local %_var_Pt.
QBE codegen now delegates TIdentExpr addr-of to EmitVarArgAddr which
already computes Self + field offset. Also added IsImplicitSelf handling
to EmitLValueAddr for TFieldAccessExpr.

Native backend: added @ImplicitSelfField path before the generic
@Variable path, computing Self + offset like the QBE backend.

Native backend: fixed pointer-to-record dereference (PP^.X) crash.
EmitExprToEax for TDerefExpr was loading the pointed-to value even for
record types, causing a double-dereference when followed by field
access. Records are address-represented, so P^ on a ^TRecord just
returns the pointer value (the record address).
2026-06-06 16:48:18 +01:00
Graeme Geldenhuys 230ba1ee87 fix(compiler): support record method calls in statement position
The semantic pass only allowed tyClass/tyInterface receivers for
statement-position method calls (TMethodCallStmt), rejecting record
variables with "'X' is not a class or interface variable". Expression-
position record method calls already worked. Extended AnalyseMethodCall
to accept tyRecord receivers.

QBE codegen: record Self is a pointer-to-stack (not a heap pointer), so
the receiver address is passed directly without a pointer dereference.
Added record-method branches to EmitMethodCall for local, var-param,
and implicit-self cases, with a tyRecord guard to skip the class-pointer
load.

Native x86-64 codegen: added parallel handling for record methods across
five sites — EmitMethodCallStmt Self-loading, TIdentExpr.IsImplicitSelf
(bare field reads inside methods), TAssignment.ImplicitSelfField (bare
field writes), and three TFieldAccessExpr.IsImplicitSelf sites that were
missing ImplicitBaseInfo.Offset and IsClassAccess deref.

Also fixed a nil-deref crash in EmitMethodCall when IsProcFieldCall is
set (ResolvedMethod is nil) by guarding MDecl.IsRecordMethod checks.
2026-06-06 16:40:13 +01:00
Graeme Geldenhuys f9e6bcdfcf feat(native): implement for-in and case statements in x86-64 backend
Add EmitForInStmt with all five iteration strategies: static array,
dynamic array, string byte-iteration, set bit-scan, and class
enumerator protocol (GetEnumerator/MoveNext/Current with vtable
dispatch).  Add EmitCaseStmt with linear comparison chain supporting
both integer/enum and string selectors via _StringEquals.

Also add set literal expression support (TArrayLiteralExpr with tySet)
to EmitExprToEax, computing the bitmask at compile time.

Extend CountTryStmts to recurse into TForInStmt and TCaseStmt bodies
for correct exception frame pre-allocation.

Upgrade 10 existing QBE-only e2e tests to AssertRunsOnBoth, covering
all for-in paths (string, static array, dynamic array, set, class
enumerator) and case statements (integer branch, else branch, enum
selector).

2622 tests pass; FIXPOINT_OK (stage-3/stage-4).
2026-06-06 15:54:13 +01:00
Graeme Geldenhuys 0aa69e3bfb fix(stdlib): TThread.Create(False) now joins on Free via ARC
TThread.Create(False) appeared to silently skip execution (GitHub #73).
Root cause: Start took an extra _ClassAddRef so the trampoline could
release it on exit. This kept the refcount at 1 after Free, preventing
Destroy (and its WaitFor/pthread_join) from running — the main program
exited before the thread finished.

Fix: remove the trampoline's ARC reference entirely. The caller's
reference is the only one; releasing it triggers Destroy → WaitFor →
pthread_join, which blocks until the thread has fully exited. This is
safe because pthread_join guarantees the trampoline has returned before
Destroy frees the object.

Also remove FreeOnTerminate — ARC makes it redundant. In Delphi/FPC it
exists because manual Free is the only cleanup path; in Blaise, scope
exit or reassignment automatically joins and frees the thread. The
migration analyser can flag this for ported code.

Document the TThread ARC lifetime model in language-rationale.adoc and
add class/method documentation comments to classes.pas.
2026-06-06 14:56:18 +01:00
Graeme Geldenhuys 66c000405b fix(compiler): resolve all EImportError crashes in warm incremental compile
The unit-cache warm path (--incremental --unit-cache) crashed on import
because the BIF interface reader could not handle several type patterns
and registration gaps:

- Add two-pass type registration (pre-register class/record/interface
  names in pass 1, fill details in pass 2) so forward references within
  the same unit resolve correctly
- Add ResolveInlineTypeName helper for field types stored as raw strings
  (^Pointer, array[L..H] of T, array of T, class of T)
- Add RegisterProcType for TProceduralTypeDef import (e.g. procedure-of-
  object types like TFieldCleanupProc)
- Fix 0-based string indexing in RegisterAlias (StrAt instead of [1])
- Register imported class methods in FMethodIndex so call-site resolution
  works during warm compile
- Serialize/deserialize TPropertyDecl through BIF (clone, export, import)
  so property access resolves on cached units
- Add unit-cache directory to search paths so .o files are found
- Include exception class name in compiler error output
2026-06-06 14:25:01 +01:00
Graeme Geldenhuys 43c5a9d637 feat(compiler): parallel incremental compilation via worker threads
Refactor the --incremental compilation path to use worker threads.
Each dependency unit now gets its own TCompileWorker thread that
independently handles codegen (fresh TCodeGenQBE, read-only symbol
table), IR file writing, and external tool invocation (qbe + cc).
All workers run concurrently and are joined before the main codegen
phase.

Also extract CompileUnitToObjectSafe which returns an error string
instead of calling Halt, making it safe for use in worker threads.
2026-06-06 12:57:10 +01:00
Graeme Geldenhuys 70a22267cc feat(rtl): mutex-protect weak reference table for thread safety
Add a pthread mutex around all weak reference table operations
(_WeakAssign, _WeakClear, _WeakZeroSlots).  The table is a shared
global data structure, and concurrent _ClassRelease calls from
multiple threads could corrupt the bucket chains without locking.

The mutex is coarse-grained (one lock for the entire table) but
sufficient because weak references are rare relative to strong ARC
operations.
2026-06-06 12:48:08 +01:00
Graeme Geldenhuys e093be0b70 feat(rtl): atomic ARC refcount operations for thread safety
Replace plain read-modify-write refcount operations with atomic
lock xadd instructions via a new blaise_atomic_x86_64.s assembly
stub.  _AtomicAddInt32 and _AtomicSubInt32 return the previous
value, so exactly one thread sees the transition to zero and
triggers destruction -- eliminating the TOCTOU race in
_ClassRelease and _StringRelease.

Affected operations:
  - _ClassAddRef: atomic increment
  - _ClassRelease: atomic decrement, destroy on old_rc == 1
  - _StringAddRef: atomic increment (immortal check unchanged)
  - _StringRelease: atomic decrement, free on old_rc == 1
2026-06-06 12:44:53 +01:00
Graeme Geldenhuys 51a00a5038 feat(rtl): per-thread memory allocator via threadvar
Convert the allocator's global state (FreeLists, ArenaHead,
LargeFreeHead, LargeFreeCount) from var to threadvar, giving each
thread its own independent arenas and freelists with zero contention
and no locks required.

Also fix the native backend .tbss emission for aggregate types
(tyRecord, tyStaticArray) — the threadvar section was using
IntByteSize which returns 4 for unknown types, instead of RawSize
which returns the actual byte size of the type.
2026-06-06 12:35:46 +01:00
Graeme Geldenhuys 53817d1b64 fix(native): emit .data section unconditionally before globals loop
The threadvar refactor moved .data emission to be conditional inside the
globals loop, which meant when no regular globals existed but exception
frame slots did, the frames were emitted without a .data directive and
landed in .note.GNU-stack — causing linker errors for any program with
try/finally blocks but no global variables.

Move .data emission before the loop, unconditionally when HasData is true.
2026-06-06 12:27:35 +01:00
Graeme Geldenhuys 66d7a3a10f fix(config): silence stderr warning for unknown config keys
The warning leaked into test runner output, making it look like a real problem.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-06 12:18:49 +01:00
Graeme Geldenhuys 43f5e49d9e feat(kanban): detect and merge external file changes in TUI
Add file-change monitoring to the kanban TUI so that tasks added via CLI
(or any external process) while the TUI is running are automatically
merged into the live view instead of being silently overwritten on save.

The implementation has three layers:

1. New FileAge built-in (compiler + runtime): returns the file's mtime as
   Int64 via stat(), or -1 if the file does not exist.  Follows the same
   pattern as FileExists — symbol table registration, semantic analysis,
   codegen, and POSIX ABI stub.

2. Merge-aware TBoard (kanban.data): tracks last-known mtime, deleted IDs
   (so re-reads don't resurrect user-deleted tasks), and provides
   HasExternalChanges/MergeFromDisk.  Save() now auto-merges before
   writing.  ID conflicts (same ID, different task) are resolved by
   reassigning the in-memory task to a fresh ID.

3. Idle polling in TKanbanUI.Run: every ~2 seconds (20 idle ReadKey
   cycles at VTIME=1), checks HasExternalChanges and merges new tasks
   with a status bar notification.
2026-06-06 12:08:57 +01:00
Graeme Geldenhuys 34a1c4feab feat(rtl): make exception globals thread-local
Change g_exc_top and g_current_exception from var to threadvar in
blaise_exc.pas, giving each thread its own exception chain. This is
required for safe multi-threaded exception handling.
2026-06-06 12:06:05 +01:00
Graeme Geldenhuys 3bf220c7f4 feat(lang): add threadvar support for thread-local storage
Add the `threadvar` keyword for declaring thread-local storage variables.
Each thread gets its own zero-initialised copy. Only allowed at program
or unit scope.

Lexer/Parser: new tkThreadVar token; ParseVarBlock accepts threadvar.
AST: IsThreadVar field on TVarDecl, TAssignment, TIdentExpr.
Symbol table: IsThreadVar on TSymbol.
Semantic: propagates IsThreadVar through analysis; rejects local scope.
QBE backend: emits `export thread data` declarations and `thread $Name`
references for correct TLS access (local-exec model via %fs:@tpoff).
Native backend: emits .tbss section and %fs:Name@tpoff addressing.
Unit interface: serialises IsThreadVar through .bif files.

Tests: 8 unit tests (parser, semantic, IR emission) + 2 E2E tests
(compile and run programs using threadvars).
2026-06-06 12:03:36 +01:00
Graeme Geldenhuys 577febbad5 feat(native): implement Inc/Dec built-ins and class-local ARC in native backend
Add inline Inc(x)/Inc(x,n)/Dec(x)/Dec(x,n) lowering for both 32-bit
and 64-bit operands — previously these emitted an empty callq that
caused assembler errors.

Zero-initialise class-typed local variables in function prologues and
release them in epilogues, matching the QBE backend's ARC behaviour.
Without this, the first assignment to a class local tried to
_ClassRelease uninitialised stack garbage.

Handle .Free on expression-based receivers (e.g. TThing(P).Free)
where the receiver is a temporary with no named slot to nil-out.
2026-06-06 10:50:50 +01:00
Graeme Geldenhuys e122aa48f8 feat(native): implement .Free intrinsic in native backend
The QBE backend lowers O.Free to _ClassRelease + nil-out-slot, but
the native backend had no handling for it — Free has no ResolvedMethod
(it is a compiler intrinsic, not a real symbol table entry) so
EmitMethodCallStmt raised an error.

Added the same lowering: load slot value, call _ClassRelease, store
zero.  Handles both local/global variables and implicit-Self field
access (Self.FChild.Free).
2026-06-06 10:38:33 +01:00
Graeme Geldenhuys 0a5bc44150 fix(arc): release old value when assigning nil to a class variable
Assigning nil to a class-typed variable (O := nil) did not call
_ClassRelease on the old value in either backend.  The nil literal
has ResolvedType.Kind = tyNil which bypassed the tyClass ARC branch
in EmitAssignment, falling through to the generic scalar store that
performs no reference counting.

QBE backend: added a class-nil branch in EmitAssignment that loads the
old value, calls _ClassRelease, and stores zero.  Handles strong, weak,
and promoted-local cases.

Native backend: added the same class-nil assignment branch, plus two
further fixes:
- EmitExprToEax now handles TNilLiteral (emits xorq %rax, %rax)
- EmitFieldCleanupFn now emits the destructor call and ARC field
  releases instead of being an empty stub, bringing it to parity with
  the QBE backend's FieldCleanup emission.

Updated leak-check tests whose leak scenarios relied on the broken
nil-assignment behaviour — they now use _ClassAddRef to create genuine
unbalanced reference leaks.

Updated test count in README (2293 -> 2606).
2026-06-06 10:33:43 +01:00
Graeme Geldenhuys 4447a67211 fix(incremental): export symbols from per-unit .o files and use correct symbol table
Two bugs prevented --incremental from producing linkable per-unit .o files:

1. Per-unit codegen used Semantic.GetSymbolTable which returns nil after
   Analyse() (the table is transferred to Prog.SymbolTable). This caused
   all class metadata (typeinfo, vtables, field cleanup) to be silently
   skipped. Fixed by using Prog.SymbolTable.

2. All functions and data emitted by AppendUnit lacked the QBE `export`
   keyword, making symbols file-local in each .o — the linker could not
   resolve cross-unit references. Added FExportAll flag and ExportPrefix
   helper to TCodeGenQBE; the incremental path sets SetExportAll(True).

Additionally, system defs (TObject/TCustomAttribute typeinfo, vtable,
FieldCleanup stubs) are now skipped in per-unit .o mode (they would
cause multiple-definition errors) and always exported in the program
path so all per-unit .o files can reference them.
2026-06-06 09:31:05 +01:00
Graeme Geldenhuys 3ca4c5291d refactor(kanban): move from examples/ to tools/ as a PasBuild module
The kanban board is now a proper tool alongside migration-analyser.
Adds tools/kanban/project.xml (blaise-kanban module) and registers
it in the root aggregator.  Removes the standalone build.sh script
since PasBuild handles compilation.
2026-06-06 00:54:26 +01:00
Graeme Geldenhuys d517cc3ae7 feat(kanban): store UTC datetime in .kanban files, display as local
Store created timestamps as ISO 8601 UTC (YYYY-MM-DDTHH:MM:SS) in
.kanban files instead of local date-only strings.  The detail view
converts UTC to the local date via CreatedToLocalDate before display.
2026-06-06 00:47:42 +01:00
Graeme Geldenhuys ac67611472 refactor(kanban): replace raw time bindings with DateUtils
Use InstantNow, SystemOffset, and TDate.ToString from the DateUtils unit
instead of calling _TimeNow/_TimeLocalOffsetSecs/_TimeSplit directly and
manually formatting the date string.
2026-06-06 00:39:20 +01:00
Graeme Geldenhuys 1ffb6b457a style(kanban): use rounded box-drawing corners for columns 2026-06-06 00:31:02 +01:00
Graeme Geldenhuys 1e4d80b3ae fix(kanban): colour the full box border, not just the top edge
DrawBox was losing its foreground colour after rendering the bold
title text because ResetAttr cleared all attributes. Now DrawBox
takes a Color parameter, sets it up front, and re-applies it after
the title reset so side borders and bottom are drawn in colour too.
2026-06-06 00:31:02 +01:00
Graeme Geldenhuys 6867678003 docs(kanban): document CLI mode and created-date field 2026-06-06 00:31:02 +01:00
Graeme Geldenhuys bb57fcc9cd feat(kanban): add CLI mode for non-interactive task creation
Adds --add flag to create tasks from the command line without
launching the TUI. Supports --detail, --detail-file, --priority,
--status, and --help options.

Examples:
  kanban board.kanban --add "Fix bug" --priority high
  kanban board.kanban --add "Task" --detail-file notes.txt
2026-06-06 00:31:02 +01:00
Graeme Geldenhuys e900a8e680 feat(examples): add TUI Kanban board application
First example application written in Blaise — a three-column terminal
Kanban board (Todo / In Progress / Done) with vim-style keybindings.

Features: task CRUD, move between columns, priority tagging, per-task
detail files (editable via $EDITOR), ISO-8601 created dates, persistent
.kanban file format, ANSI colour output with box-drawing characters.

The terminal unit uses pure POSIX bindings via `external name`
(tcgetattr, tcsetattr, ioctl, read) — no C code required.
2026-06-06 00:31:02 +01:00
Graeme Geldenhuys cee6cef934 test(e2e): add nested record field assign + method call tests
Covers the codegen fix for TFieldAccessExpr on record-typed fields,
which previously emitted a scalar load instead of returning the field
address. Tests both QBE (D.ToString via string return) and native
(D.Sum via integer return) backends, verifying chained field access
(DT.Date), record-to-record assignment, and method calls on nested
record fields.
2026-06-06 00:28:04 +01:00
Graeme Geldenhuys e11e5dad5e feat: extend not operator to integer types (bitwise complement)
The `not` operator previously only accepted Boolean operands. It now
accepts all integer types (Byte, SmallInt, Word, Integer, Int64, UInt64)
and performs bitwise complement, enabling bitmask patterns like
`Flags := Flags and (not MASK)`.

Semantic pass promotes narrow types to Integer, preserves Int64/UInt64.
QBE backend emits `xor -1` (w or l suffix by width). Native backend
emits `notl`/`notq`. Also adds missing bitwise binary ops (and, or,
xor, shl, shr) and nested record field read/write support to the
native backend.
2026-06-06 00:21:59 +01:00
Graeme Geldenhuys 5b15406a18 feat(native): implement TAddrOfExpr, TDerefExpr and TPointerWriteStmt
Full address-of expression support in the x86-64 native backend:
- @Variable (local and global)
- @VarParam (load pointer value from param slot)
- @StaticArray[I], @OpenArray[I], @DynArray[I] (element address)
- @Rec.Arr[I] (field array element address, all access patterns)
- @FuncName (code address with mangled name via ResolvedFreeRoutine)
- @Obj.Method (method-pointer construction at assignment level)
- @Rec.Field (general field address)

Also adds TDerefExpr (P^ read) and TPointerWriteStmt (P^ := val)
with ARC handling for string and class element types.

Includes five e2e tests covering @Variable, @StaticArr[I],
@DynArr[I], @Rec.Arr[I], and @Obj.Method patterns.
2026-06-05 23:48:54 +01:00
Graeme Geldenhuys 04a0b275dd feat(native): ARC retain/release for string/class/interface value params
Retain string, class, and interface value params on function entry and
release them on exit, matching the QBE backend's ARC semantics. Const,
var, out, and open-array params are skipped. Includes e2e tests for
string and class value param passing.
2026-06-05 20:17:10 +01:00
Graeme Geldenhuys f6315b8ac3 feat(native): ARC retain/release for class and string field assignments
Add proper ARC handling when storing to class or string fields via
class-access or implicit-self paths. [Unretained] fields store-only
with optional RHS release, [Weak] fields call _WeakAssign, normal
class fields do addref(new)/release(old)/store, and string fields
do _StringAddRef/_StringRelease. Adds NativeExprOwnsRef helper.

Includes e2e tests for class field store+read and string field
store+read.
2026-06-05 20:14:03 +01:00
Graeme Geldenhuys d96eef10bb feat(native): array field subscript read (R.Arr[I]) and var-param validation
Implement IsArrayAccess handling in the native x86-64 backend for
record field reads. Supports static arrays, dynamic arrays, and open
arrays with correct element-address computation (base + field offset +
index * element size). Also fix var-param type validation to skip the
unsupported-type check for var/out params (always pointer-sized).
2026-06-05 20:04:55 +01:00
Graeme Geldenhuys 52731f4642 feat(native): full generics support in x86-64 backend
Implement monomorphised generic emission for all four generic kinds:
generic classes (typeinfo, vtable, field cleanup, name strings, method
bodies), generic records (method bodies), generic interfaces (typeinfo),
generic standalone functions, and class-to-interface itab/impllist.

Also fixes three latent native backend bugs exposed by generic code
paths: NativeMangle was not applied in FuncSymbolFromDecl/FuncSymbolOf
(raw angle brackets in assembly symbols), TFieldAccessExpr.IsMethodCall
was unhandled (zero-arg method calls like B.GetVal), and IsVarParam
field access was not dereferenced (record Self passed by pointer).

Four new e2e tests verify generic records, classes, standalone functions,
and class-implementing-interface all produce correct output on both QBE
and native backends.
2026-06-05 19:26:50 +01:00
Graeme Geldenhuys deb09f5b56 fix(codegen): mangle generic class name in itab symbol for interface assignment
QBEMangle was not applied to the class name portion of itab symbols when
emitting class-to-interface assignments for generic classes. This caused
QBE to reject symbols containing raw angle brackets (e.g.
$itab_TBox<Integer>_IValue). Now emits $itab_TBox_Integer_IValue.
2026-06-05 19:26:39 +01:00
Graeme Geldenhuys 216e8704f5 feat(generics): support generic records (monomorphization)
Extend the generics system to support record types alongside classes and
interfaces.  Generic records use the same <T> syntax and monomorphization
strategy: each instantiation (e.g. TMyVal<Integer>) creates a concrete
TRecordTypeDesc with substituted field types and re-analysed method bodies.

Unlike generic classes, generic records do not emit typeinfo, vtable, or
field-cleanup data — they are value types with no class metadata.

Parser, semantic, QBE codegen, and native codegen all updated.  15 new
unit tests (parser + semantic + codegen) and 4 E2E tests.  Fixpoint OK.
2026-06-05 18:57:21 +01:00
Graeme Geldenhuys d69fcacaca feat(rtl): WriteLn prints True/False for Boolean values
Add _SysWriteBool to the runtime platform layer so WriteLn(Boolean)
prints 'True' or 'False' instead of '1' or '0', matching Delphi/FPC
behaviour. Both QBE and native x86_64 backends emit the new call for
tyBoolean arguments. Updated ~65 assertions across 13 test files.
2026-06-05 18:08:40 +01:00
Graeme Geldenhuys 0c5910195c refactor(rtl): drop legacy platform aliases, derive constants from target
Remove sLineBreak (Delphi alias for LineEnding) and PathDelim (Delphi
alias for DirectorySeparator).  Blaise keeps exactly three platform
constants: LineEnding, DirectorySeparator, PathSeparator.

Platform constant values are now derived from GTarget (the compilation
target) via TargetLineEnding/TargetDirectorySeparator/TargetPathSeparator
in blaise.codegen.target, so cross-compilation produces the correct
values without conditional compilation.
2026-06-05 17:01:04 +01:00
Graeme Geldenhuys fe63b2559d fix(arc): release owned RHS when storing to [Unretained] class field
Assigning a function return (owned +1 ref) to an [Unretained] field
leaked the temporary — the field borrows but nobody consumed the +1.
Both EmitAssignment (implicit-Self path) and EmitFieldAssignment now
call _ClassRelease on the value when ExprOwnsRef indicates ownership.
2026-06-05 15:17:47 +01:00
Graeme Geldenhuys 8f6cf04481 fix(codegen): address-of operator on array field elements (@R.Arr[I])
The parser absorbs [I] in R.Arr[I] into TFieldAccessExpr.PropIndexExpr,
which the semantic pass and codegen only handled for indexed properties
and string char access.  Array-typed fields (dynamic, static, open) were
left unresolved, producing a type mismatch ("^array of T" instead of
"^ElementType").

Add IsArrayAccess flag to TFieldAccessExpr, resolve it in all three
PropIndexExpr sites in uSemantic (Base-chained, RecordName-leaf, and
trailing field path), and emit the element-address computation in
uCodeGenQBE for both EmitExpr and EmitAddrOfExpr.

Native backend has TODO markers for the new flag.
2026-06-05 15:02:15 +01:00
Graeme Geldenhuys cd136b63d0 feat(native): interface parameters in method/constructor/inherited calls
Interface parameters are fat pointers (obj + itab = 2 × 8 bytes) that
consume two consecutive integer register slots in the x86-64 SysV ABI.
Previously the native backend rejected any function with an interface-typed
parameter with "unsupported parameter type".

Changes in blaise.codegen.native.x86_64.pas:

BuildFrame: interface params now get a 16-byte slot (same as interface
locals) and advance IntIdx twice, matching the two-register convention.

EmitFunctionDef prologue: spill both incoming registers into the 16-byte
slot — obj register into slot base (VarOperand), itab register into
base+8 (IntfItabOperand).

Type validation gate: add tyInterface to the accepted parameter kinds.

EmitMethodArgPush: new interface branch pushes obj first then itab (pop
loop runs high-to-low so itab lands in the higher register).  Handles
TIdentExpr (load from _obj/_itab slots), TAsExpr (runtime _GetItab),
implicit-Self field (load from Self+FieldOffset), and class expressions
(static itab symbol).

CountArgSlots: new helper that counts register slots rather than logical
argument positions; interface and open-array params each count as 2.

EmitMethodCallExpr: add IsConstructorCall path (TMethodCallExpr) with
full arg loop.  Pop loop now uses CountArgSlots.

EmitMethodCallStmt / EmitInheritedCall: pop loops use CountArgSlots;
inherited call removes the "not yet supported" error.

EmitInterfaceCall / EmitInterfaceFieldCall: arg loops push two values for
interface-typed arguments; slot-count pop replaces arg-count pop.

EmitCall (standalone function): slot-count detection and push loop both
handle interface params as 2-slot args.

Changes in uCodeGenQBE.pas:

All arg loops that call InterfaceArgFragment or EmitInterfaceExprPair
now intercept the case where the actual argument is a class expression
(ResolvedType.Kind = tyClass) and emit the static itab symbol directly,
instead of routing through EmitInterfaceExprPair which only handles
TIdentExpr and TAsExpr.  Affected sites: EmitMethodCall, EmitInheritedCall,
EmitProcCall (two arg loops).

Tests: five new AssertRunsOnBoth cases in cp.test.e2e.native.pas covering
interface arg to a standalone procedure, class method, constructor,
inherited call, and a class expression passed directly as an interface
parameter.  TestRun_Native_InterfaceField_ShadowsGlobal upgraded from
QBE-only to AssertRunsOnBoth now that interface params work on native.
2026-06-05 13:30:46 +01:00
Graeme Geldenhuys 4a2c54639d fix(semantic): class fields shadow same-named program globals in method bodies
Inside a method, an unqualified name should resolve as:
  local/param > implicit Self.field > unit/program global

The semantic analyser (AnalyseAssignment, AnalyseProcCall, and the
TIdentExpr read path) only fell back to the class-field lookup when
the symbol table returned nil.  When a same-named program-level global
existed, Lookup() returned it — bypassing the field altogether.

Fix: in all three resolution sites, also try the class-field path when
the symbol found is a global (IsGlobal=true), mirroring Pascal's
field-shadowing rules.

Also fix the implicit-Self interface-field branch of AnalyseProcCall: it
was using ResolveMethodOverload for interface-typed fields, which doesn't
know about interface method descriptors.  Now uses HasMethod + interface
dispatch (nil ResolvedMethod), matching the non-implicit path.

QBE codegen fixes:
- EmitAssignment/ImplicitSelfField: add tyInterface case — stores both
  fat-pointer slots (obj+8, itab+8) with ARC, via new
  EmitInterfaceToFieldSlots helper.
- EmitProcCall interface dispatch: when IsImplicitSelf, load obj/itab
  from Self+FieldOffset rather than from %_var_ObjectName slots.
- EmitExpr constructor call arg loop: detect tyInterface params and emit
  the fat-pointer pair (obj + static itab) instead of a single word.
- EmitInterfaceExprPair: handle implicit-Self TIdentExpr (loads from
  Self+FieldOffset) and class expressions (runtime _GetItab lookup).

Native x86_64 backend fixes:
- EmitInterfaceAssign: handle ImplicitSelfField — compute Self+offset
  into %r15 (callee-saved) so the fat-pointer stores survive intervening
  _ClassAddRef / _ClassRelease calls.
- New EmitInterfaceFieldCall: dispatches interface methods through a class
  field (Self+FieldOffset) rather than a variable slot.
- EmitMethodCallStmt interface dispatch: route through EmitInterfaceFieldCall
  when IsImplicitSelf.

Regression tests: cp.test.interfaces TestSemantic_InterfaceField_ShadowsGlobal_OK
(unit), cp.test.e2e.classes2 TestRun_InterfaceField_ShadowsGlobal_Dispatches (QBE
e2e), cp.test.e2e.native TestRun_Native_InterfaceField_ShadowsGlobal (QBE path only
— native backend interface-arg passing is a separate open item, documented in
bugs.txt).

Closes issue #64.
2026-06-05 12:46:52 +01:00
Graeme Geldenhuys 5dbb3a2584 test(arc): guard Pointer-rvalue-to-class-local lifetime
Bring in Andrew Haines's regression test (from his bf8d94a) that assigns a
Pointer-returning rvalue (TObjectList.Items[0]) into a TObject local and asserts
the list-held object survives the borrowing procedure (Destroy counter 0 after
Borrow, 1 after L.Free).

Only the test is brought in, not the accompanying code change: bf8d94a switched
three assignment ARC dispatch sites from RHS-type to LHS-type to fix a net -1
imbalance, but that bug does not reproduce on our master — our EmitAssignment
already retains when a Pointer rvalue (including an explicit TThing(P) cast) is
stored into a class-typed local, so all the repro variants are balanced. The
test passes as-is and now guards that behaviour from regressing.

Full suite 0 failures. (Test-only change — no compiler source touched, so
fixpoint is unaffected.)
2026-06-05 08:14:05 +01:00
Graeme Geldenhuys a5f01c1cd1 perf(arc): elide const-param retain/release, retain transient args
Reapply the const-parameter ARC elision that was reverted in 03b59e8,
now paired with the caller-side transient retain that makes it sound.

The four entry/exit ARC loops skip IsConstParam again (a const parameter's
object is kept alive by the caller for the whole call, so the callee needs no
_StringAddRef/_StringRelease or _ClassAddRef/_ClassRelease pair). The earlier
revert was because that premise fails for a TEMPORARY bound to a const param
(e.g. `Use(A + ' ' + B)`): the concat result is +0, its only reference is the
argument slot, and with the callee retain elided it was freed mid-call.

The fix (Andrew Haines, cherry-picked from the llvm branch — commits 00fcf41 +
e94e70544) adds the missing reference at the CALL SITE: EnsureConstStringRef
emits _StringAddRef before the call and ReleaseConstStringArgs emits
_StringRelease after, for each value-mode argument to a const-string parameter.
The pair is a no-op on immortal literals and nets to zero on owned strings, so
the overhead falls only on the transients that actually need it.

Tests: restored the elision IR tests (string + interface const params),
Andrew's caller-retains-transient IR tests and valgrind e2e
(TestRun_ConstStringParam_TransientRetained_Valgrind), alongside the existing
TestRun_ConstStringTemp_StaysAlive_Valgrind. docs/future-improvements.adoc marks
the optimisation shipped. Full suite 0 failures; FIXPOINT_OK; the original
metaclass-ref crash (`C := TFoo`) compiles cleanly.
2026-06-05 08:08:19 +01:00
Graeme Geldenhuys 988bb24d35 feat(native): M8 — var/out arguments to method calls
The native method-call paths (EmitMethodCallStmt, EmitMethodCallExpr,
EmitInheritedCall) pushed every argument by value, so a `var`/`out` parameter
to a method received a copy and the caller's variable was not mutated — silently
wrong (e.g. `F.Bump(N)` left N unchanged). The standalone EmitCall path already
passed var args by address; the method paths did not.

Add EmitMethodArgPush: for a var/out param it pushes the argument's address
(leaq for a local/global, or the stored pointer when the arg is itself a var
param), otherwise the value. Use it in all three method-call paths. This also
lets EmitInheritedCall accept var args (the earlier "not yet supported" guard is
now only for interface args, which the native call ABI still cannot pass).

Two AssertRunsOnBoth e2e tests (a single var param mutated; a two-var-param
swap) match the QBE oracle. Native suite 95 tests; full suite 0 failures;
FIXPOINT_OK.
2026-06-05 07:49:41 +01:00
Graeme Geldenhuys d53226ed7b feat(native): M8 — inherited calls (TInheritedCallStmt)
The native backend rejected `inherited Method[(args)]` with "unsupported
statement TInheritedCallStmt".  Add EmitInheritedCall: a direct static call to
the parent method (bypassing the vtable, as inherited must), with Self taken
from the current method's Self slot.  Value arguments are pushed/popped into the
SysV registers as for a regular method call; a value-returning parent stores its
result into the Result slot so `inherited F;` as a statement seeds Result.

var/out and interface arguments to an inherited call raise a clear
"not yet supported" — the native call ABI does not yet pass arguments by address
or as interface fat pointers, so failing loudly beats passing the wrong thing.

Two AssertRunsOnBoth e2e tests (Inherited_Proc; Inherited_FuncSetsResult, which
exercises the Result-seeding path) match the QBE oracle.  Native suite 93 tests;
full suite 0 failures; FIXPOINT_OK.
2026-06-05 07:45:05 +01:00
Graeme Geldenhuys 03b59e8ff0 Revert "perf(arc): elide retain/release for const string and class params"
This reverts commit 5a5b5d4. The optimisation elided the callee-side
ARC retain/release for const string/class/interface value params on the
premise that the caller keeps the argument alive for the whole call. That
premise fails for a TEMPORARY bound to a const param (e.g. `Use(A + ' ' + B)`):
the concatenation result's only reference is the argument slot, so without the
callee-side retain its refcount hits zero at the call boundary and it is freed
before the callee reads it — a use-after-free.

This bit the RTL hardest: `_StringCopy` / `StrHead` take `const string` params
and are called with built-at-runtime temporaries, so the emitted RTL was
miscompiled. Under self-hosting the defect is self-reproducing and only
manifests at the SECOND generation (the compiler that emits the broken RTL is
itself fine), which is why a one-step fixpoint did not expose it and the
compiler's own sources did not reliably trigger it. The symptom was a
deterministic crash compiling any program that uses a metaclass reference
(`C := TFoo`) or HasClassAttribute — which is why TestRunner (via
blaise.testing.runner.text) could not be built, blocking the whole suite.

The original change's valgrind e2e test passed only because it bound a string
LITERAL (immortal) to the const param, not a temporary.

Removed the now-invalid IR/e2e tests that asserted the elided behaviour
(string const params, and the interface-const variant from 088d12f which
relied on this commit's IsConstParam guard). Added
TestRun_ConstStringTemp_StaysAlive_Valgrind, which passes a concatenation
result as a const string param and reads it in the callee — the exact case the
optimisation broke. docs/future-improvements.adoc records how to re-attempt the
elision safely (condition on the argument, not the parameter; retain temporaries
either caller- or callee-side).

Verified: stage-2 build clean, TestRunner builds, full suite 0 failures,
FIXPOINT_OK.
2026-06-04 23:21:25 +01:00
Graeme Geldenhuys 4df81d49d0 fix(codegen): pass var/out params by reference in inherited calls
EmitInheritedCall's argument loop had no var-param branch: it always
evaluated the argument and passed its loaded value, so `inherited Foo(X)`
where the parent method declares `var X` passed a `w` value where the callee
expects an `l` address.  The parent then dereferenced a small integer as a
pointer and the program segfaulted.

Pass EmitLValueAddr(arg) as an `l` for var/out params, matching every other
method-dispatch arg loop.

e2e test: a TDer.Bump override that calls `inherited Bump(X)` on a `var X`
param must mutate the caller's variable (5 -> 16). FIXPOINT_OK.
2026-06-04 17:41:15 +01:00
Graeme Geldenhuys 790fa4c3f5 fix(codegen): pass a method interface param as a fat pointer
A *method* (OwnerTypeName<>'') taking a by-value interface parameter was
emitted as a single `w` slot instead of the two-slot fat pointer (obj +
itab) the standalone-routine path already uses.  EmitMethodDef's signature
and EmitParamAllocs both fell into the generic `else`, so the callee's
`storel %_par_X` was invalid QBE ("invalid type for first operand") and the
method failed to compile; dispatch inside it would also have read an
undefined %_var_X_obj.

Split the interface param into `l %_par_X_obj, l %_par_X_itab` in the method
signature and alloc both `%_var_X_obj`/`%_var_X_itab` local slots, mirroring
the standalone-routine path.  The call sites must pass both halves too: add
an InterfaceArgFragment helper (wraps EmitInterfaceExprPair) and use it in
the five method-call arg loops (sret method-call expr, non-sret method-call
expr, the value-returning and statement method dispatch, and inherited
calls) so an interface argument is forwarded as the obj+itab pair.

Latent, not a regression: the compiler's own source declares no method with
a by-value interface param, so the fixpoint stayed clean; it bit user code
only.  Two IR unit tests (method signature splits the param; the call site
passes both slots) and one e2e test (a class method taking an interface and
dispatching through it returns the right value).  Full interface + ARC
suites green; FIXPOINT_OK.
2026-06-04 17:39:17 +01:00
Graeme Geldenhuys 8d648ef3d9 feat(native): M7f complete — interface dispatch through itab
Interface vars become fat pointers: locals a contiguous 16-byte slot
(obj@+0, itab@+8); globals two .data labels Name_obj/Name_itab, matching
the QBE backend's $Name_obj/$Name_itab. AddSlot reserves 16 bytes for
tyInterface; IntfObjOperand/IntfItabOperand return the two halves for
locals (rbp-relative) or globals (RIP labels).

EmitInterfaceCall lowers a dispatch: push args, load obj->%r10 and
itab->%rax, index the itab by MethodIndex*8 (a flat method-pointer array,
no leading typeinfo slot unlike a vtable), load the code ptr->%r11, pop
args into %rsi.. (shifted for Self in %rdi), then callq *%r11. Wired into
EmitExprToEax (TFieldAccessExpr.IsInterfaceCall = zero-arg),
EmitMethodCallExpr and EmitMethodCallStmt (ResolvedClassType = tyInterface,
with args).

EmitInterfaceAssign handles four RHS forms, strong refs only: := nil
(release obj, zero both slots); class->interface (static itab_Class_Intf,
addref/release obj); interface->interface copy (load src obj+itab,
addref/release, store both); T as IFoo (_GetItab(obj, typeinfo_Intf); nil
result -> _Raise_InvalidCast; else addref/release/store). ARC operands are
kept on the stack across the AddRef/Release calls, so no callee-saved
register is relied upon.

EmitInterfaceDefs emits typeinfo_<Intf> (.quad 0 identity token), flat
itab_<Class>_<Intf> method-pointer arrays (abstract slots ->
_AbstractMethodError), and NULL-terminated impllist_<Class> (typeinfo,itab)
pairs — mirrors the QBE EmitInterfaceDefs. Run after EmitClassSection so the
method symbols and class-name strings it references exist. EmitDataSection
emits Name_obj/Name_itab for interface globals; the function prologue
zero-inits string and interface locals (both fat-pointer halves) so the
first assignment's release-old step sees nil; the epilogue releases
interface locals via _ClassRelease on the obj slot.

6 new AssertRunsOnBoth e2e tests (ZeroArg, Arg, Proc, IntfToIntfCopy,
AsCast, NilClear) — all pass on QBE and native; valgrind clean on the
as-cast program. FIXPOINT_OK.

Weak interface refs (_WeakAssign/_WeakClear) deferred. Pointer size is the
literal 8 here, matching the rest of this x86_64-only unit; i386/arm64 will
be separate TNativeBackend subclasses.
2026-06-04 17:21:12 +01:00