Commit graph

384 commits

Author SHA1 Message Date
Graeme Geldenhuys bd57dece6b chore: begin v0.10.0-dev cycle 2026-06-02 09:58:07 +01:00
Graeme Geldenhuys 7e84fe71ac release: v0.9.0
Fixpoint achieved on the multi-file self-hosted source at 166,479 lines
of verified QBE IR. Includes FFI fixes (narrow return masking, Single
alignment, SysV aggregate ABI for record-by-value), ARC field-cleanup
destructor mangling, dynamic-array address-of, and const-initialiser
enhancements (integer typecasts and bit-op folding).
2026-06-02 09:57:17 +01:00
Graeme Geldenhuys 0ce9507775 docs: rewrite 'Function calls requiring ()' section as proper design entry
Replace the raw conversation fragment with a structured improvement entry
covering the motivation (TIdentExpr.IsNoArgFuncCall ambiguity), the
interaction with the Result-variable convention, migration impact, and
effort estimate.
2026-06-02 09:54:14 +01:00
Graeme Geldenhuys 8ea4c79f19 style(codegen): fix indentation of EmitFFIRecordTypeDecls call sites
Both calls to EmitFFIRecordTypeDecls and the following
FOutput.AppendBuffer(Body) in GenerateUnit/AppendUnit were
de-dented one level outside the enclosing try..finally body.
Align them with the surrounding code.
2026-06-02 09:49:40 +01:00
Andrew Haines 7b99e79114 feat(parser,semantic): fold bit-op chains in const initialisers
ParseConstBlock previously accepted only a single operand in the
value position — '5', '-5', 'NamedConst', or a string-concat chain.
'1 or 2', 'FG_BLUE or FG_GREEN', '$FF and 15', '1 shl 8' all failed
to parse, forcing callers to hand-fold flag combinations and bit
masks before declaring them as constants.

Add deferred bit-op support driven from the parser:

  - New TConstDecl.IntExprTokens (TStringList) carries the scalar
    operand/operator chain when at least one bit op is present.
    Operands are tagged on Objects[]: nil = integer literal, non-nil
    = ident reference.  Interleaved operator tokens carry the lower-
    case op name ('or', 'and', 'xor', 'shl', 'shr').
  - New TConstDecl.ArrayElementParts (TObjectList, parallel to
    ArrayElements) carries the same shape per element.  Elements
    that are plain literals/typecasts remain inline in ArrayElements;
    elements that include a bit op get a deferred token list, and
    semantic rewrites the matching ArrayElements entry once it has
    resolved.
  - CollectConstBitOpExpr (parser) builds the token lists.  Operands
    may be integer literals (optionally signed), TypeName(...) casts
    re-using the typecast helper, or named-constant idents.
  - FoldConstBitOpExpr (semantic) walks the token list left-to-right,
    resolves idents through the symbol table, and applies the ops.
    Wired into AnalyseConstDecls (scalar) and AnalyseArrayConstDecls
    (array elements).

Regression tests cover all-literal chains, all-named-const chains,
mixed literal+named chains, and/shl, plus an array-element case.
2026-06-02 09:47:35 +01:00
Andrew Haines 35e0a6cf7e feat(parser): accept integer-type typecast in const initialisers
ParseConstBlock previously only recognised bare literals (optionally
negated), string concatenations, and named-constant idents as const
initialiser values.  TypeName(IntLit) and TypeName(-IntLit) — common
idiom for expressing values like Cardinal(-11) = $FFFFFFF5 — failed
with "Expected ';' but got '('".

Add TryParseConstIntTypecast which recognises Byte / ShortInt /
SmallInt / Int16 / Word / UInt16 / Integer / LongInt / Cardinal /
LongWord / UInt32 / Int64 / UInt64 / QWord / PtrUInt followed by '(',
parses the optional sign and integer literal inside, applies the
target type's bit-width truncation mask, and sign-extends back when
the target is signed.  Both scalar const init and array-element
positions accept the form.

Regression tests cover all five width × sign combinations plus an
array-element case.
2026-06-02 09:47:35 +01:00
Andrew Haines 58249bcb21 fix(codegen): pass records by value through SysV aggregate ABI
Record-by-value parameters were emitted as `l <addr>` — a bare
pointer in an integer register.  For a `cdecl external` callee that
means the C side reads the low bits of the address as the struct
contents (e.g. a 4-byte RGBA record into ClearBackground would
clear the window to whatever colour fell out of the stack-frame
address).  For intra-Blaise calls it accidentally worked because
callees re-dereferenced the pointer.

QBE handles SysV §3.2.3 aggregate classification natively: declare
the record once as `type :_ffi_<Name> = align A { ... }`, then pass
the source pointer with that aggregate type at the call site.  QBE
scatters fields into INTEGER / SSE eightbytes per the ABI, the C
callee sees the struct in the right registers, and inside Pascal
callees QBE re-gathers them into a hidden buffer reachable via the
same pointer-typed parameter — so the existing memcpy-into-`%_var_X`
prologue keeps working.

Applied uniformly across param signatures and every call site
(standalone, implicit-self, indirect, method-call, inherited, ARC
release, FreeAndNil, expression-context func/method calls) — not
gated on IsExternal — so Pascal-to-Pascal calls and C-FFI calls use
the same ABI and callbacks passed to C work without translation.

A latent value-by-reference bug for value-record params disappears
as a side-effect: callees now mutate QBE's hidden gathered buffer
rather than the caller's storage, matching declared value
semantics.

Returns still use sret; aggregate-return is a separate change
because it interacts with ARC ownership transfer on managed
record fields.
2026-06-02 09:47:35 +01:00
Andrew Haines d0128ed645 fix(codegen): allow @ on dynamic-array elements
EmitAddrOfExpr's TStringSubscriptExpr block enumerated only static
and open arrays.  Taking the address of a dyn-array element via
`@A[i]` fell through to the generic L-value path, which didn't know
about TStringSubscriptExpr and raised

  Code generation error: Unsupported L-value form for var argument

Read paths and SetLength already worked; only @-of-subscript broke.
Add a tyDynArray branch that mirrors the open-array shape (no
LowBound, element size from TDynArrayTypeDesc.ElementType.RawSize) —
EmitExpr on a dyn-array var already returns the heap data pointer.

E2E regression test in cp.test.e2e.dynarray.pas writes through the
captured PRec to confirm the address points at the right element.
2026-06-02 09:47:35 +01:00
Andrew Haines f6bb0ee029 fix(codegen): narrow Double arg to Single before FFI call
CoerceArg handled w/l → s sitofp but had no d → s narrowing.  A
double-typed expression (literal or arithmetic result) passed to a
Single-typed parameter reached the assembler as `d` against an `s`
slot, which QBE rejected with `invalid type for first operand in
arg`.  On the wire — had the assembler accepted it — 8 bytes of
double would go into the float-by-value slot and the C
`float`-by-value callee would see the low mantissa half as a single,
i.e. noise (`sinf(1.5707964)` returns -0.998 instead of 1.0).

Adds the d → s arm via `truncd`.  Regression test in
cp.test.external.pas pins the `=s truncd` shape for a double literal
passed to a Single FFI parameter.
2026-06-02 09:47:35 +01:00
Andrew Haines 015388918c fix(symtab): Single fields get 4-byte alignment, not 8
TTypeDesc.AllocAlign had no tySingle case, so it fell through to the
catch-all `else: 8`.  A record of three back-to-back Single fields
then padded each 4-byte field up to an 8-byte slot, reporting SizeOf
as 24 instead of 12 — wrong on the wire for any struct-of-floats
passed to a C library (OpenGL vectors, math libraries, audio frame
structs) and wrong for GET/PUT against expected on-disk layouts.

Adds explicit `tySingle: 4` case.  Regression test in
cp.test.records.pas asserts a TVec3 of three Single fields has
TotalSize = 12.
2026-06-02 09:47:35 +01:00
Andrew Haines a1c742f851 fix(codegen): narrow double literal to float when stored to a Single
record field

Real-typed literals and double sub-expressions land in the SSA at
double width.  Storing one of these into a Single record field
emitted a double-typed store against a float-typed slot — a type
mismatch the assembler rejected outright (`stores d_1.5, ...`).

EmitFieldAssignment already coerced i32 → i64 sign-extensions for
narrow integer fields; mirror the pattern for float widths,
emitting a `truncd` when narrowing double → single and an `exts`
when widening the symmetric direction (single source into a
double field).

Adds two IR-shape regression tests covering both directions.
2026-06-02 09:47:35 +01:00
Andrew Haines 1972c7753c fix(codegen): route ARC field-cleanup destructor call through the
overload-mangled symbol

When a class declares an overloaded Destroy, the destructor is emitted
with an overload-mangled suffix (e.g. '$TFoo_Destroy$' → linker symbol
'TFoo_Destroy_D_').  The synthesised _FieldCleanup_<T> helper, called
from _ClassRelease at refcount zero, was hard-coding the bare
'<Class>_Destroy' label as its call target — a symbol that was never
written for overloaded forms.  Programs that instantiated such a class
failed to link with:

  undefined reference to `TFoo_Destroy'

Stash the no-arg Destroy's resolved emit name on TRecordTypeDesc as
semantic registers the method, and have EmitFieldCleanupFn prefer
that name when set.  Non-overloaded and inherited Destroy keep the
legacy bare-label path.

Adds a regression test covering the link symptom by asserting on the
IR shape.
2026-06-02 09:47:35 +01:00
Andrew Haines 3207324c77 fix(codegen): mask narrow FFI return values to declared width
External (cdecl) functions declared with a sub-int return type
(Byte, Boolean, Word, SmallInt) need their result normalised at
the call site before it enters the i32 value domain.  The C
calling convention leaves the upper bits of the return register
undefined for sub-int returns, so a caller observing the value
as a full word — most commonly `if Foo() <> 0 then` — would see
garbage in those bits and mis-fire silently.

After each external call, emit a width-normalisation:

* Byte / Boolean : and  X, 255
* Word           : and  X, 65535
* SmallInt       : shl  X, 16  ;  sar  X, 16   (sign-extend)
* Integer / Int64 / pointer / ... : no fix-up needed

Gated on MDecl.IsExternal so intra-Blaise calls (which already
live in the wider domain end-to-end) are unaffected.

Adds four IR-shape regression tests covering the four return
type classes above.
2026-06-02 09:47:35 +01:00
Graeme Geldenhuys 28972e39de fix(arc): retain caught exception when binding to handler variable
`on E: ExcType do` bound the caught exception to the handler variable E with
a plain store and no retain.  But E is an ordinary class-typed local, so the
function's scope-exit ARC cleanup releases it.  The exception object is created
at rc=0 by `raise EFoo.Create` (the raise expression is never assigned, so the
constructor's object is never retained to rc=1), so the handler's scope-exit
release drove its refcount negative — a use-after-free that corrupted the heap
once the freed block was reused.

AddRef the exception when binding it to the handler var so the scope-exit
release balances; the exception is then freed exactly once.

Found via a poison/over-release detector in _ClassRelease while investigating
the (separate, still-deferred) field-assignment ExprOwnsRef guard, which had
surfaced this latent imbalance.  Backtrace at discovery: _ClassRelease <-
ArgMatchScore's `try CheckTypesMatch except on E: ESemanticError` handler.

Regression test: cp.test.e2e.leakcheck compiles a handler-bound exception with
--debug and asserts clean exit + correct stdout + no leak report.
Fixpoint clean; 2270 tests pass.
2026-06-02 09:06:30 +01:00
Graeme Geldenhuys c5a26e84e3 refactor(arc): mark remaining Resolved*Type AST fields [Unretained]
Eight more non-owning TTypeDesc reference fields that the initial pass missed
because they carry no "not owned" comment: ResolvedTargetType,
ResolvedIntfType, ResolvedLhsType, ResolvedVarType, ResolvedReturnType, and
the per-node ResolvedType on TVarDecl/TFieldDecl/TMethodParam.  All are
semantic annotations pointing into the symbol table's type pool, never owned
by the AST node.  Marking them [Unretained] removes the spurious retain on
shared type descriptors (the residual TTypeDesc over-retain drops further).

Fixpoint clean; 2269 tests pass.
2026-06-02 08:48:32 +01:00
Graeme Geldenhuys 61264da0bf feat(lang): add [Unretained] non-owning reference attribute
[Unretained] is an unsafe non-owning class/interface reference (like Swift's
unowned(unsafe)): no reference counting and no weak-table registration.  The
field assignment is a plain pointer store and field cleanup is a no-op for it.
Use only when the referent is guaranteed to outlive the field.

This complements [Weak] (safe, auto-nils on referent free, has weak-registry
cost).  The two are mutually exclusive on a declaration.

Pipeline:
- semantic: HasUnretainedAttribute + resolution on field decls, with
  class/interface-only and not-both-with-Weak checks; IsUnretained propagated
  to TFieldInfo.
- codegen: EmitFieldAssignment and the implicit-Self field path emit a bare
  storel for unretained class fields; EmitFieldCleanupFn skips them.

Apply [Unretained] to the compiler's own non-owning class fields — every
field documented "not owned" across uAST (ResolvedType, ResolvedMethod,
ResolvedDecl, FieldInfo back-pointers, …) and uSymbolTable (type-desc
references, FParent back-pointers, NextOverload, …).  These pointed into
the symbol table's type pool / the AST tree, which the codegen was
needlessly ARC-managing — inflating shared TTypeDesc refcounts.  Marking them
unretained de-inflates those (TTypeDesc dropped from rc=4/6 toward rc=2) at
zero per-assignment cost.

Tests: 5 new cases in cp.test.weakref.pas (semantic rejection on non-class
and on [Weak]+[Unretained]; codegen asserts no addref on store, no
_WeakAssign, no release/clear in field cleanup).  Docs: language-rationale
gains a [Weak] vs [Unretained] section with a comparison table.

Fixpoint clean; 2269 tests pass.
2026-06-02 08:41:12 +01:00
Graeme Geldenhuys 4fe8858103 fix(codegen): run finally blocks on non-local exit (Exit/Break/Continue)
A non-local exit out of a try/finally only popped the exception frame and
jumped to the function/loop exit — it never ran the finally body.  So
`try ... Exit ... finally CleanUp end` silently skipped CleanUp, and the
unbalanced frame could later crash in _PopExcFrame.

Track active try/finally bodies in FFinallyStack, aligned 1:1 with the
FExcDepth exception frames (try/except pushes nil to keep the indices in
step).  EmitExcUnwind now, for each frame it crosses, pops the frame and —
when that frame is a try/finally — emits its finally body inline, innermost
first.  This mirrors the finally emission already done on the normal and
exception paths in EmitTryFinallyStmt.

Tests: IR test asserts the finally body is emitted on the exit path
(3 occurrences: normal, exception, exit); three e2e tests cover Exit through
one finally, Exit through nested finallys (innermost first), and Break out of
a loop through a finally.  Fixpoint clean; 2264 tests pass.

Note: the compiler's own source still avoids `Exit` inside `try/finally`
because the current bootstrap release binary predates this fix and would
miscompile it.  Once a release binary carrying this fix is cut, the
free-before-Exit workarounds in EmitMethodCall and the method-call-expr path
can be replaced with proper try/finally.
2026-06-02 01:11:19 +01:00
Graeme Geldenhuys ddde3c8621 fix(arc): wire owned-arg-temp release into remaining call paths
Extend EmitOwnedArgReleases coverage to the method-call statement (both the
arbitrary-receiver ObjExpr path and the named-receiver path), the method-call
expression path, and the standalone function-call expression path.  Each now
tracks value-argument temps in a parallel list and releases the +1-owned ones
after the call, matching the constructor and procedure paths.

Avoided `Exit` inside `try/finally`: that shape miscompiles today — the Exit
branch fails to pop the exception frame and segfaults in _PopExcFrame at
runtime (logged in bugs.txt).  Free the temp list explicitly before each Exit
instead; leaking it on the fatal-exception path is acceptable.

Stage-2 self-compiles, fixpoint clean, 2260 tests pass.
2026-06-02 00:58:15 +01:00
Graeme Geldenhuys dc2ab8fe37 refactor(arc): drop redundant owned-field Free from 50 AST destructors
Swift-style ARC continued: convert every AST destructor whose body only
released owned class fields into an inherited-only destructor.  ARC field
cleanup already releases those fields; the manual Free calls merely zeroed
the slots first, so removing them is behaviour-neutral (fixpoint clean,
stage-2 self-compiles, 2260 tests pass).

TMethodDecl is deliberately left unchanged: it frees Body conditionally
(`if OwnBody then Body.Free`) because cloned generic method stubs share a
body they do not own.  That is genuine conditional ownership, not redundant
cleanup, and converting it would let field cleanup release a borrowed Body.
Tracked as a follow-up: model Body as a properly refcounted shared reference
and retire the OwnBody flag.
2026-06-02 00:50:29 +01:00
Graeme Geldenhuys dfe04a10d6 refactor(arc): drop redundant owned-field Free from symbol-table destructors
Swift-style ARC: a destructor is for side-effects, not for releasing owned
storage.  ARC field cleanup already releases every owned class/string field
(verified in the generated _FieldCleanup_TScope: it releases FSymbols and
FKeys, and _WeakClears the [Weak] FParent).  The manual FKeys.Free /
FSymbols.Free in TScope.Destroy (and the equivalents in TSymbolTable and
TSymbol) duplicated that work — they zeroed the slot so field cleanup's
release became a no-op, netting one release either way.

Remove the redundant frees, leaving the destructors as inherited-only.
Behaviour-neutral (IR-confirmed equivalent); fixpoint clean; 2260 tests pass.
This establishes the pattern; remaining destructors convert separately.
2026-06-02 00:48:41 +01:00
Graeme Geldenhuys 804d8a127e fix(arc): release +1-owned temporaries passed as value arguments
A function/property/method return passed directly as a value argument
(e.g. TScope.Create(CurrentScope), where CurrentScope reads through
GetCurrentScope and returns +1) was never released by the caller.  The
callee takes ownership via its parameter-entry AddRef and releases at scope
exit, so the caller's +1 temporary leaked once per such call.

This was the dominant source of compiler self-compilation leaks: every
PushScope does TScope.Create(CurrentScope), leaking one reference on the
parent scope.  The global scope accumulated a reference per builtin symbol,
so it never reached refcount zero and its entire subtree (scopes, symbols,
their lists) leaked.

Add EmitOwnedArgReleases: after a call, release any value-argument temp
whose expression ExprOwnsRef.  Wire it into the constructor-with-args path
and the user-defined procedure path, tracking value-arg temps in a parallel
list ('' for var/open-array/interface args that are not owned value temps).

Measured: compiling a trivial program dropped from 329 leaked objects to 30.
Fixpoint clean; 2260 tests pass.
2026-06-02 00:43:01 +01:00
Graeme Geldenhuys 5e45c50503 fix(compiler): exit --emit-ir path through normal cleanup, not Halt(0)
The --emit-ir branch in main wrote the IR and immediately called Halt(0),
which lowers to libc exit().  libc exit() runs atexit handlers (so the leak
report still prints) but unwinds no Pascal stack frame, so the main block's
scope-exit ARC releases never run — any local not freed in the finally block
leaked silently, and the leak tracker could not see it.

Restructure so --emit-ir writes the IR and falls through to the end of the
program, letting the main block's scope-exit cleanup release its locals
before the process returns normally.  The native-compile path is now the
else branch.  IR output is byte-identical (verified: fixpoint clean,
2260 tests pass).
2026-06-02 00:31:08 +01:00
Graeme Geldenhuys 5b10bdab7b fix(arc): release +1-owned receiver temporaries after method calls
When a method is invoked on a receiver that is itself a function or
method-backed property return (e.g. CurrentScope.Define(Sym), where
CurrentScope reads through GetCurrentScope), the receiver evaluates to a
+1-owned temporary.  The call consumed the value but nothing released it,
so each such call leaked one reference on the returned object.

ExprOwnsRef now recognises method-backed property reads (PropRead with a
non-empty ReadMethod return +1; field-backed reads do not).  Both the
method-call statement path (EmitMethodCall) and the method-call expression
path (EmitExpr) now release the receiver temporary after the call when
ExprOwnsRef(ObjExpr) holds.

Measured effect: compiling a trivial program drove TScope's leaked
refcount from 129 down to 1 — the per-symbol CurrentScope.Define temporary
is no longer leaked.
2026-06-02 00:10:33 +01:00
Graeme Geldenhuys 857551c7b9 fix(arc): honour [Weak] on implicit-Self class field assignments
The implicit-Self field-store path in EmitAssignment (bare field name like
FParent := X) emitted the strong AddRef/Release pattern unconditionally,
ignoring the field's [Weak] attribute.  Only the explicit-receiver path
(EmitFieldAssignment) checked IsWeak.  As a result a [Weak] back-pointer
assigned via a bare name still took a strong reference, over-retaining the
target.

Route weak class fields through _WeakAssign in the implicit-Self path too,
mirroring EmitFieldAssignment.  Mark TScope.FParent [Weak] — a parent scope
outlives its children, so the back-pointer must not retain it.
2026-06-02 00:03:19 +01:00
Graeme Geldenhuys ebe131da15 feat(debug): show refcount per object in leak report
Each leaked object line now reports its refcount: `- ClassName (rc=N)`.
This distinguishes the two leak modes at a glance:

  rc=1  reference created but never released (dangling owner)
  rc>1  over-retain — an AddRef without a matching Release (double-AddRef)

Reads the refcount from the class header at UserPtr - CLASS_HDR.  Adds a
WriteSignedInt helper since immortal objects report rc=-1.
2026-06-01 23:55:51 +01:00
Graeme Geldenhuys 043602a0a4 fix(arc): eliminate double-AddRef on function-return class assignments
Three fixes that work together to correct ARC refcount accounting:

1. _ClassFree now delegates to _ClassRelease instead of bypassing ARC
   with a raw _BlaiseFreeMem.  This lets users call .Free (out of habit)
   or rely on ARC — both paths go through the same refcount logic.

2. ExprOwnsRef helper in codegen suppresses the call-site _ClassAddRef
   when the RHS expression already owns a +1 reference (function/method
   return values).  Covers TFuncCallExpr (with ResolvedDecl guard to
   exclude type casts), TMethodCallExpr, TFieldAccessExpr.IsMethodCall,
   and TIdentExpr.IsNoArgFuncCall / IsImplicitSelfMethod.

3. Allow Pointer() and PtrUInt() casts on tyString arguments, needed by
   rtl.platform.posix for Pointer(APath) where APath is a string.

The double-AddRef bug inflated refcounts on every class-returning
function call: the callee's Result := X emitted AddRef (correct — the
Result slot is not released at scope exit), and the caller's assignment
emitted a second AddRef (incorrect — the +1 already transferred).
2026-06-01 23:49:39 +01:00
Graeme Geldenhuys 89c8e49ed8 fix(arc): remove double _ClassAddRef in constructor-with-args codegen
When TFoo.Create(args) was compiled via the TMethodCallExpr constructor
path, the codegen emitted _ClassAddRef immediately after _ClassAlloc.
EmitAssignment then added a second _ClassAddRef for the receiving slot,
leaving refcount=2 after the first assignment — so the object could
never reach refcount=0 and was permanently leaked.

The TFieldAccessExpr no-arg constructor path (TFoo.Create without parens)
was already correct: no AddRef in the constructor path, leaving the
single AddRef entirely to EmitAssignment.  The TMethodCallExpr path now
follows the same contract.

Root cause: the constructor path was treating _ClassAddRef as "this
object now exists with refcount 1", but _ClassAlloc already returns
refcount=0, and the assignment site is the sole owner of the retain.

Tests:
- TClassTests.TestCodegen_Constructor_WithArgs_ExactlyOneAddRef: asserts
  exactly one _ClassAddRef in the IR for a constructor-with-args call.
- TE2ELeakCheckTests.TestDebug_ConstructorWithArgs_NoDoubleAddRef: e2e
  test compiles a program with TBox.Create(42) under --debug and asserts
  no leak report is emitted at exit.

2260 tests pass; fixpoint verified (stage-3/stage-4).
2026-06-01 16:52:48 +01:00
Graeme Geldenhuys a231c7d043 feat(debug): add --debug leak reporter + Pointer/PtrUInt cast pairs
Runtime leak tracker (blaise_arc.pas):
- Open-addressing hash map (1024 buckets, 16 bytes each) tracks every
  live class instance; zero overhead when disabled.
- _LeakTrackerEnable() activates tracking and registers _LeakTrackerReport
  via atexit; called from $main when --debug is passed.
- _LeakTrackerRegister(UserPtr, ClassName) inserts into the map; called
  from codegen at every constructor site (both TFieldAccessExpr and
  TMethodCallExpr paths).
- _ClassRelease unregisters on refcount-zero free via LTDelete.
- Report written to stderr lists count + class name of each survivor.

Compiler (--debug flag):
- New --debug CLI flag sets FDebugMode on TCodeGenQBE; emits
  call $_LeakTrackerEnable() in EmitMainHeader after unit init calls,
  and call $_LeakTrackerRegister(...) after every _ClassAlloc site.
- --debug-opdf remains independent (OPDF only, no leak tracking).
- SetDebugMode(Bool) added to TCodeGenQBE public API for e2e tests.

Pointer/PtrUInt cast pairs (semantic + codegen):
- Pointer(intOrPtrExpr) validated in AnalyseFuncCallExpr; accepts
  integer, pointer, class, metaclass, procedural, and nil sources.
- PtrUInt(intOrPtrExpr) same validation; resolves to tyUInt64.
- Codegen: w-to-l widening uses extuw for Pointer/PChar targets
  (zero-extend, not sign-extend) to preserve address semantics.
- Four new unit tests in cp.test.pointers.pas.

E2E test infrastructure:
- CompileAndRunWithRTLDebug added to TE2ETestCase base for in-process
  debug-mode compilation without recursive overload ambiguity.
- cp.test.e2e.leakcheck.pas: 5 tests covering no-leak silence,
  single leak, multiple leaks, reference cycles, and debug-off.

2258 tests pass; fixpoint verified.
2026-06-01 16:37:02 +01:00
Graeme Geldenhuys 506ada7916 chore: remove dead {$IFDEF FPC} blocks, --cache-dir, and migrate_full.py
The compiler has been self-hosting since v0.7.0 and FPC is no longer
part of the toolchain. Remove the last FPC-conditional code paths:

- Blaise.pas: drop CacheKeyForFile, LoadCachedIR, StoreCachedIR and
  both {$IFDEF FPC} whole-program IR cache blocks; remove --cache-dir
  flag and CacheDir/UnitIR/UnitName/UnitPath locals; collapse the
  ReadProcessChunk FPC/Blaise fork to the Blaise implementation only;
  drop the poUsePipes/poStderrToOutPut Options block.

- tools/migrate_full.py: deleted — the Python migration analyser is no
  longer needed now that the toolchain is fully Blaise-native.
2026-06-01 02:07:44 +01:00
Graeme Geldenhuys 8136fcaff7 perf(stdlib): rewrite GetText and GetCommaText to avoid O(N²) concatenation
Both methods accumulated a result string with repeated `Result := Result + …`
inside a loop — each iteration allocates a fresh buffer and copies the entire
accumulator forward, quadratic in total content size.

Rewrite both using TStringBuilder (geometric buffer growth, single ToString
at the end).  On a self-compile via --output the GetText hot path drops from
~17 s to ~1.7 s (~10× speedup); GetCommaText gets the same fix proactively.

Add TestRun_TextGet_ManyLines: 500-line list with mixed empty/non-empty entries,
spot-checks total length and key substrings after the rewrite.
2026-06-01 00:11:39 +01:00
Graeme Geldenhuys e549026c61 fix(semantic): implicit Self member shadows unit-level symbol
Inside a class method, an unqualified reference X (call OR bare ident)
used to bind to a unit-level X of the same name even when the
enclosing class declared X as a method / field / property.  Standard
Delphi/FPC resolution order is:

  1. Local variables / parameters / var-parameters
  2. Implicit Self.member (incl. inherited via class chain)
  3. Unit-level proc / function (program-level + uses-clause exports)

AnalyseProcCall, AnalyseFuncCall, and the TIdentExpr branch all queried
FTable first and only fell back to the implicit-Self lookup when the
global was nil.  So a 'uses strutils' in scope used to bind unqualified
CountOccurrences inside a class method to strutils's version, and a
bare 'Tag' inside a class method bound to a program-level 'Tag'
function even when the enclosing class had its own Tag.

Fixed at all three resolution sites: when FTable returns a local
var/parameter the call stays as an indirect call through it; otherwise
the implicit-Self check now runs before falling back to the unit-level
binding.

Tests pin the priority order and the explicit-qualifier escape hatches.

Contributed by: Andrew Haines <andrewd207@aol.com>
2026-05-31 18:11:37 +01:00
Graeme Geldenhuys d1c55fd0f6 fix(semantic): nested procs with same name in different outer procs no longer ambiguous
Two standalone procedures each containing a nested procedure named 'Inner'
triggered "Ambiguous overload of 'Inner'" because AnalyseStandaloneDecls
was adding all nested proc declarations to the global FProcIndex regardless
of nesting depth.

Fix: skip FProcIndex registration when FCurrentEnclosingDecl != nil (i.e.
we are inside a proc body).  Nested procs are resolved via the scoped symbol
table only; the global index is for top-level standalone procs.

AnalyseProcCall gains an early-exit branch for nested procs: when the
symbol is found in scope (skProcedure/skFunction with Decl.EnclosingDecl != nil)
but absent from FProcIndex, resolve directly from Sym.Decl.

Regression test added to cp.test.procs.pas.

Closes bug https://github.com/graemeg/blaise/issues/59
2026-05-31 17:59:46 +01:00
Graeme Geldenhuys bccb8dcf54 feat(parser,semantic,codegen): implement nested procedures with captured-var support
Nested procedures (a procedure declared inside another procedure's local
declaration section) now compile and run correctly.

Parser: ParseMethodDecl gains an ACanHaveNestedProcs flag (default False)
so that a bare procedure/function keyword after a standalone proc's header
correctly triggers body parsing.  The flag is only set True from
ParseStandaloneDecl; class-body and interface-method declarations
are unaffected.

Semantic: AnalyseStandaloneDecl tracks the FCurrentEnclosingDecl chain
and sets EnclosingDecl on each nested proc.  CollectCaptures walks the
nested proc's body AST (iterative BFS) to find all references to variables
declared in the directly enclosing proc's local block.  Those variables are
recorded in CapturedVars on TMethodDecl.

Codegen: EmitFuncDef recursively emits nested procs before the outer
function, assigning each a mangled QBE name (OuterName_InnerName).
Captured variables are passed as implicit leading pointer parameters
(l %_cap_X), keeping the outer frame's stack slot alive.  EmitVarAllocs
treats captured vars as address-taken so they always get stack slots.
TIdentExpr reads and TAssignment writes to captured vars go directly
through the %_cap_X pointer without an extra dereference.

Tests added: two IR unit tests (nested proc emitted before outer;
captured var passed by pointer) and one E2E test confirming the nested
proc mutates and the outer proc sees the updated value.  2243 tests pass;
fixpoint OK.

Closes: https://github.com/graemeg/blaise/issues/58
2026-05-31 17:45:20 +01:00
Graeme Geldenhuys 5d6abdac61 fix(semantic,codegen): allow direct invocation of proc-typed class fields
Calling a method-pointer field directly — `F.Handler;` or `F.Handler()` —
previously failed with "Class 'TFoo' has no method 'Handler'" because
AnalyseMethodCall went straight to ResolveMethodOverload without first
checking whether the name resolved to a procedural-typed field.

Fix: before the ResolveMethodOverload/error path, detect a tyProcedural
field on the receiver class and set IsProcFieldCall on the TMethodCallStmt.
EmitMethodCall then loads Code from the field slot (Self+Offset) and Data
from slot+8, emitting the same indirect (Code,Data) ABI used everywhere
else for method-pointer dispatch.

New fields on TMethodCallStmt: IsProcFieldCall, ProcFieldInfo,
ResolvedProcType.  No AST node is needed for TMethodCallExpr yet (the
expression form is a separate gap).

Closes: https://github.com/graemeg/blaise/issues/57

Tests added: IR unit test asserting indirect-call emission; E2E test
compiling and running both `F.Handler;` and `F.Handler();` forms.
2240 tests pass; fixpoint OK.
2026-05-31 16:44:55 +01:00
Graeme Geldenhuys 8b524340ce fix(codegen): storel for proc-pointer static-array slots; add Expr()() postfix call
Two related bugs found while testing procedural types as open-array elements:

1. Static-array element stores for tyProcedural used 'storew' (32-bit) instead
   of 'storel' (64-bit), truncating the function pointer and causing a segfault
   at the indirect call site.  Fixed by adding tyProcedural to the storel branch
   in the static-array element-store path in EmitArraySubscriptStmt.

2. Calling through an array subscript expression (Fns[I]()) was rejected by the
   parser with "Expected 'end' but got '('".  The postfix chaining loop only
   handled '.', '[', and '^'; it did not recognise '(' as a postfix call on a
   non-identifier expression.

   Fixed by introducing TIndirectFuncCallExpr (callee is an arbitrary TASTExpr),
   extending the postfix loop in ParseFactor to emit it when '(' follows any
   expression, and adding the corresponding semantic analysis and codegen paths.
   The codegen uses the callee value directly as the call target — no extra loadl,
   since the subscript load already yields the function pointer value.

Adds E2E test TestRun_OpenArray_ProcType_CallEach covering a static array of
TIntFn passed as an open-array parameter and called element-by-element via the
direct Fns[I]() syntax.
2026-05-22 17:10:02 +01:00
Graeme Geldenhuys a208906601 docs: update future-improvements.adoc removing completed items. 2026-05-22 16:57:19 +01:00
Graeme Geldenhuys ca622e258a fix(codegen): load class pointer before offsetting in var-param L-value
EmitLValueAddr handled the leaf TFieldAccessExpr form (no .Base) only
for inline-record and var-record-param cases.  For a class field --
where the variable's slot stores a pointer to the heap object -- the
base was emitted as VarRef(...), so a var-param call site emitted
`add &N, offset` instead of `add (loadl N), offset`.  Reads through
the var pointer hit the variable's own storage and the write never
reached the field.

EmitInstancePtr already had the symmetric branch (Fld.IsClassAccess ->
load through VarRef); add the missing case to EmitLValueAddr so calls
like `Fill(N.Value, N.IsBig)` reach the heap object.

Regression coverage:
  * TVarParamTests.TestCodegen_VarParam_ClassFieldLeaf_LoadsObjectPointer
    asserts the IR contains `loadl $N` and no bare `add $N, ...`.
  * TE2EClasses2Tests.TestRun_VarParam_ClassFields_WritebackVisible
    compiles and runs the documented reproducer and checks stdout
    shows 4096 / 1 instead of 0 / 0.
2026-05-22 16:02:34 +01:00
Graeme Geldenhuys 7dbb981665 build(runtime): enable pipefail so compiler errors fail the build
The Pascal RTL recipes pipe $(BLAISE) --emit-ir through sed to strip
the program section before writing the .ssa file.  Without pipefail,
sed's exit code masks a failing compiler invocation: the recipe
succeeds with an empty .ssa, QBE emits an empty .s, gcc archives an
empty .o, and the first symptom is a downstream link failure with
"no symbols".

Set SHELL=/bin/bash and .SHELLFLAGS=-o pipefail -c at the top of the
Makefile so every recipe inherits pipefail.  Verified by injecting a
parse error into a build driver: make now exits with Error 1 and
prints the compiler's diagnostic, instead of silently producing an
empty archive member.
2026-05-22 15:39:08 +01:00
Graeme Geldenhuys b7b3d9000f test(lexer): regression tests for UTF-8 bytes inside comments
Lock in the current behaviour: high UTF-8 bytes (2-, 3-, and 4-byte
sequences) inside brace, paren-star, and line comments are skipped
cleanly without disturbing the token stream that follows.
2026-05-22 15:33:38 +01:00
Graeme Geldenhuys aeb3e4ae8d feat(lang): extend High/Low to ordinal types; targeted float error
High and Low previously accepted only arrays and strings.  They now
also accept any ordinal type — Integer, Int64, UInt32, UInt64,
SmallInt, Word, Byte, Boolean, and enums — as either a type name or
an expression.  The result type matches the argument type so
High(Int64) round-trips through 64-bit code paths without truncation.
Bounds are folded at compile time to a literal QBE copy.

Floating-point arguments now produce a targeted error message
("not defined for floating-point types; use MaxDouble/MinDouble or
Math.Infinity") instead of the generic "must be an array or string".

docs/language-rationale.adoc records the decision; docs/grammar.ebnf
is updated to reflect the broadened intrinsic signatures.
2026-05-22 14:40:06 +01:00
Graeme Geldenhuys ae07c7476c fix(semantic): handle SizeOf(expression), not only SizeOf(TypeName)
The SizeOf handler only set ResolvedType on its argument when the
argument was a type-name identifier. For a variable or any other typed
expression, the argument's ResolvedType stayed nil and codegen later
crashed dereferencing it for ByteSize. Now non-type arguments are run
through AnalyseExpr so their ResolvedType is populated; the codegen's
compile-time fold to a literal byte size still applies.

Fixes #54.
2026-05-22 14:15:20 +01:00
Graeme Geldenhuys 2b501cdbde fix(lang): treat / as real division, distinct from div
The parser previously mapped both `/` and `div` to a single `boDiv` op,
so `Integer / Integer` was evaluated as integer division and returned
an Integer. `Trunc(Y / X)` and `Round(Y / X)` with Integer operands
were therefore rejected with "requires a float argument", and
`Round(7 / 2)` would have silently produced 3 instead of 4.

Introduce a separate `boSlash` AST op for `/`. Semantic gives it a
float result type unconditionally (Single only when both operands are
Single, Double otherwise); codegen reuses the existing float
arithmetic path. As a follow-on, `div` now rejects float operands
explicitly instead of accepting them silently.

Includes IR tests, e2e tests, and a rationale section documenting the
`/` vs `div` split.
2026-05-22 13:59:33 +01:00
Graeme Geldenhuys 34297e8317 feat(lang): add sar arithmetic-right-shift operator
`shr` stays logical (zero-fill) on all integer types, matching
Delphi/FPC semantics. The new `sar` keyword emits QBE's arithmetic
shift, preserving the sign bit on signed operands.

Closes BUG-003 (previously: signed Int64 `shr` silently discarded
the sign).  Resolved by adding a new operator instead of changing
`shr` semantics, so existing code ported from FPC/Delphi continues
to behave identically.
2026-05-22 13:40:25 +01:00
Graeme Geldenhuys e236e7cbda fix(generics): emit support data for generic classes declared in a unit
A generic class declared in a unit's interface section and instantiated
by code in that same unit failed to link with undefined references to
$_FieldCleanup_<mangled>, $vtable_<mangled>, $typeinfo_<mangled> and
the cloned constructor body.  Two coordinated gaps caused this:

* AppendUnit (the live unit codegen path) never iterated
  AUnit.GenericInstances, so no typeinfo/vtable/cleanup/method-body
  data was emitted for instances registered against the unit rather
  than the program.  Mirrored the program path's emission for each
  generic instance, including itab/impllist for instances that
  implement interfaces.

* LinkGenericClassMethodImpls ran after interface-section globals
  were analysed.  Generic instances clone the template's
  Methods.Body at instantiation time, so any FindTypeOrInstantiate
  call triggered by an interface-section global var produced an
  instance with nil bodies — codegen then emitted nothing for those
  methods.  Moved the linking step to run immediately after
  AnalyseTypeDecls(IntfBlock), before any potential instantiation,
  in both AnalyseUnit and AnalyseUnitForExport.

Added four IR tests in cp.test.generics.pas with a new GenCombinedIR
helper that exercises the real driver path (AnalyseUnitForExport +
Analyse(Prog) + AppendUnit + AppendProgram).  The test source uses a
separated impl-section body so the ordering requirement is also
covered.

Closes #40.
2026-05-22 12:38:18 +01:00
Graeme Geldenhuys e732c71cd1 feat(types): add packed record qualifier
Introduces the `packed record ... end` syntax.  Field layout in a
packed record skips natural-alignment padding between fields and
skips the record's tail padding, so SizeOf equals the cumulative
byte size of the fields.  ARC-managed field types (string, class,
interface, dynamic array) keep their natural 8-byte alignment so
that _StringRelease / _ClassRelease etc. can keep using aligned
64-bit loads through the field pointer.

`packed` is only legal directly before `record`.  `packed class`
and `packed array` are parse errors — neither has a meaningful
implementation in Blaise's heap-allocated class model nor in its
existing tightly-strided array layout.

Implementation:
  - Lexer: tkPacked token, PACKED keyword
  - AST: TRecordTypeDef.IsPacked, propagated through CloneTypeDef
  - Parser: optional PACKED prefix before RECORD; rejects other
    forms with a clear error message
  - uSymbolTable: TRecordTypeDesc.IsPacked + new FieldAlign helper;
    AddField / PackedSize / TotalSize / MaxAlign honour it
  - uSemantic: propagates IsPacked from def to desc in pass 1
  - OPDF: no change — emits whatever TotalSize reports

Tests: 11 IR-level + 2 e2e in cp.test.packedrecord.pas, plus the
TTokenKind audit bumped from 82 to 83.

Grammar and language-rationale updated.
2026-05-22 11:31:42 +01:00
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