Commit graph

171 commits

Author SHA1 Message Date
Graeme Geldenhuys 58ff2a429c feat(codegen): expand typeinfo with totalsize, fieldcleanup, vtable
Step 11e foundation.  Class typeinfo data blocks now carry three new
l-slots after the existing { parent, impllist, name, methods } prefix:

  Slot 4 (offset 32): total instance size in bytes (vptr + fields)
  Slot 5 (offset 40): pointer to $_FieldCleanup_<T> for this class
  Slot 6 (offset 48): pointer to $vtable_<T> for this class

These are the inputs needed by the upcoming _ClassCreate(TInfo) RTL
helper to allocate, install the vtable, and arrange ARC field cleanup
on release — the runtime equivalent of the inline lowering
EmitConstructorCall produces for the static 'TFoo.Create' form.

TObject also gets a vtable stub ($vtable_TObject = { l $typeinfo_TObject,
l $TObject_Destroy, l $TObject_ToString }) and an empty no-op
$_FieldCleanup_TObject function so its typeinfo can name them.  Both
were previously unemitted because TObject is built-in, not a user
type declaration; the new typeinfo slots force them to be real
linker-visible symbols.

Tests pinning the old four-slot layout updated to assert on the
prefix rather than the full record:
  cp.test.publishedrtti — TestCodegen_TypeInfo_HasFourSlots,
                          TestCodegen_NoPublishedMethods_MethodsSlotZero,
                          TestCodegen_PublishedMethods_TableEmitted
  cp.test.typetests    — TestCodegen_TypeInfo_ParentPtr_IsZero_ForRoot,
                          TestCodegen_TypeInfo_ParentPtr_ForDerived
  cp.test.interfaces   — TestCodegen_Typeinfo_ClassHasImpllistField

All 1235 tests pass.  testbcl smoke driver still green.
2026-05-06 00:57:20 +01:00
Graeme Geldenhuys bee768f290 feat(rtl): bcl.testing v0 — fpcunit runtime surface ported to Blaise
Step 11d.  Direct port of fpcunit's runtime surface, scoped to the
slice the 54 cp.test.*.pas regression units actually use.

bcl.testing.pas (compiler/src/main/pascal/) provides:

  * TTest, TAssert (AssertTrue/False, AssertEquals overloads for
    Integer/Int64/string/Pointer/Boolean, AssertNotNull/Null/Same,
    Fail), TTestCase (SetUp/TearDown virtual, RunTest dispatching
    via MethodAddress + TMethod cast to TRunMethod), TTestResult
    (counters + failure/error TStringLists + Summary),
    TTestCaseClass = class of TTestCase, EAssertionFailed
    (extends TObject; ToString override).

  * RegisterTest stores classes in a global TStringList for the
    runner (Step 11e) to enumerate.  Per-class published-method
    walking via metaclass-based virtual construction is deferred
    to 11e.

  * AssertException / ExpectException intentionally absent — no
    cp.test.* unit currently relies on them.

testbcl.pas smoke driver declares one TTestCase fixture with two
published methods (one passes, one fails), runs each via the
MethodAddress + procedure-of-object dispatch hot path, and prints
PASS/FAIL summary.  Compiles and runs end-to-end through the
QBE+gcc pipeline.

Two compiler bugfixes folded into the same step, both uncovered
while building the smoke driver and both with regression tests:

  1. ParseUnit now accepts dotted unit-name headers
     ('unit bcl.testing;') matching the UsesClause shape.  The
     parser only allowed a single IDENT after 'unit', which
     prevented any dotted-name unit (Generics.Collections, etc.)
     from being loaded through the unit loader.  grammar.ebnf
     updated to use UnitName in both positions.

  2. AnalyseFieldAccessExpr's chained Base<>nil branch was
     skipping AnalyseExpr on PropIndexExpr for method-backed
     indexed properties.  PropIndexExpr.ResolvedType remained
     nil and codegen segfaulted dereferencing it — visible as
     "Code generation error: Access violation" on inputs like
     R.Failures.Strings[I].  Constant indices ([0]) worked by
     accident because the literal-emission path didn't require
     ResolvedType.  Fixed: chained-base method-backed property
     reads now validate IndexParamName and analyse PropIndexExpr
     in the same shape as the non-chained and implicit-Self
     paths.  Regression test
     TestCodegen_IndexedProperty_ChainedBase_VarIndex_Compiles
     added to cp.test.properties.pas.

All 1235 compiler tests pass (1234 prior + 1 regression).
2026-05-06 00:32:11 +01:00
Graeme Geldenhuys a936ed1bd8 docs(lang): grammar + rationale for procedure of object (Step 11c)
grammar.ebnf:
* ProceduralType extended with the optional 'of object' modifier on
  both function and procedure variants.  Comment block describes the
  16-byte (Code, Data) layout, the byte-equivalence with TMethod that
  makes the TRunMethod(m) cast a no-op, and the call-site convention
  (Data passed as the implicit first argument).

language-rationale.adoc:
* Procedural Types section rewritten to cover both bare and method-
  pointer forms instead of listing 'of object' as deferred.  Records
  the IsMethodPtr compatibility rule (bare vs method pointers do not
  inter-operate even with identical signatures), the byte-equivalence
  with TMethod, and the canonical xUnit dispatch pattern.
* 'reference to' (closures) and calling-convention markers remain
  deferred; rationale updated accordingly.
2026-05-05 19:10:50 +01:00
Graeme Geldenhuys 3c5d466f78 feat(lang): procedure of object + TMethod record — Step 11c
Method-pointer types are now first-class.  Both fpcunit and fptest
dispatch test methods via 'procedure of object' values; this is the
abstraction that binds (Code, Data) into a callable.  With this in
place the bcl.testing port can reproduce fpcunit's RunTest line for
line — no inline-asm trampoline, no work-around RTL primitive.

Layout: a method-pointer value is a 16-byte block with Code at offset
0 and Data (Self) at offset 8.  This is byte-for-byte identical to
TMethod, so the cast 'TMyMethod(m)' (m: TMethod) is a no-op at the
QBE level.  A method-pointer call site loads both halves and emits
'call code(l data, args...)' — Data passes as the implicit first
argument so the callee sees it as Self.

Parser:
* ParseProceduralTypeDef now accepts an optional 'of object' suffix
  after the parameter list and (for functions) the return type.  Sets
  TProceduralTypeDef.IsMethodPtr := True.

Symbol table:
* TProceduralTypeDesc gains IsMethodPtr.  IsCompatibleWith requires
  the flag to match (a bare proc and a method-ptr are incompatible
  even with identical signatures).
* TMethod registered as an intrinsic record type (Code, Data: Pointer)
  alongside the TObject root class.  Reachable from any program
  without 'uses System' — same convention as TObject, Integer, etc.

Semantic:
* ResolveProceduralTypeDef propagates IsMethodPtr from the AST def
  to the type descriptor.

Codegen:
* Method-pointer locals: 'alloc8 16' + memset(0).  The variable's
  QBE name refers to the address of the 16-byte block, mirroring
  records.
* Method-pointer globals: 'export data $X = { z 16 }'.
* Method-pointer assignment: 'memcpy(dest_addr, src_addr, 16)' —
  works for both method-ptr-to-method-ptr and TRunMethod(TMethod)
  cast assignments since the layouts match.
* Indirect calls (both procedure-call and function-call expression
  forms) check IsMethodPtr: if set, load Code at slot+0 and Data at
  slot+8, then prepend Data as the first argument.

Tests: 13 new tests in cp.test.proctypes_ofobject.pas covering
parser shapes (of-object on procedure and function variants),
semantic propagation (TMethod registration, field offsets,
IsMethodPtr on the descriptor), codegen (alloc-16, global-data
shape, memcpy on assignment, Code+Data load on call), and three
end-to-end tests that compile-link-run a method-pointer dispatch
with no args, with args, and verifying Self is bound.

Total tests: 1234 (1221 + 13), all passing.  testpunit2 still
produces its expected pass/fail/inactive pattern.
2026-05-05 19:09:23 +01:00
Graeme Geldenhuys 31415c90df feat(lang): published-method RTTI + MethodAddress builtin — Step 11b
Adds the minimum-viable RTTI surface that fpcunit's discovery model
needs: a per-class table mapping published-method names to code
addresses, plus a MethodAddress(Obj, Name) builtin that walks the
typeinfo chain at runtime.

This is the only compiler-side work the bcl.testing port still
needed before the framework itself can be written.

Parser:
* TMethodDecl.IsPublished: Boolean.  ParseClassDef tracks the current
  visibility section and stamps each method declared inside a
  'published' block.

Codegen:
* Class typeinfo gains a 4th slot: l methods (or 0 for none).  Layout
  is now { l parent, l impllist, l name, l methods }.
* For each class with at least one published method, emit
    data $methods_<TName> = { l count, [l name, l addr] x count }
  where 'name' is a Blaise immortal string-data pointer (reusing
  EmitClassNameRef — same pattern as class-name strings) and 'addr'
  is the method's $TName_Method symbol.
* Three pre-existing tests asserted the literal three-slot typeinfo
  shape; updated to expect the new four-slot form.

Symbol table:
* MethodAddress registered as a builtin function returning Pointer.

Semantic:
* AnalyseFuncCallExpr handles MethodAddress(Obj, Name): asserts arity
  2, first arg tyClass, second arg tyString, return type Pointer.

RTL (blaise_arc.pas):
* _MethodAddress(Self, Name): Pointer walks
    Self -> vptr -> vtable[0] -> typeinfo
  then for each class on the parent chain reads typeinfo[3] (the
  methods table) and linearly searches its (name, addr) entries
  using _StringEquals.  Returns nil when not found.

Tests: 13 new tests in cp.test.publishedrtti.pas.  Parser tests
verify visibility tagging including the published->public boundary.
Codegen tests verify the typeinfo's 4th slot, the methods-table
shape (count, name+addr pairs), and the MethodAddress call site.
Four end-to-end tests compile the program with QBE+cc and exercise
the runtime helper: name found, name not found, parent-chain walk
across an inheritance pair, and distinct methods having distinct
addresses.

Total tests: 1221 (1208 + 13), all passing.  testpunit2 still
produces its expected pass/fail/inactive pattern.
2026-05-05 18:28:58 +01:00
Graeme Geldenhuys 1af933f526 feat(lang): class of TFoo metaclass type — Step 11a of bcl.testing port
Adds first-class metaclass types so 'class of TBase' is a real
type-system concept, not a typedef alias for Pointer.  This is the
foundation for fpcunit-style 'TTestCaseClass = class of TTestCase'
discovery patterns that the test-suite migration in Step 11 needs.

Symbol table:
* New tyMetaClass type kind and TMetaClassTypeDesc(BaseClass).
* NewMetaClassType factory mirrors NewPointerType.

Parser:
* ParseTypeName now recognises 'class of TName' and encodes it as
  the type-name string 'class of <Name>'.
* Type-decl 'type TFooClass = class of TFoo;' disambiguates 'class' +
  'of' from a class-definition body via a single-token PeekKind.

Semantic:
* FindTypeOrInstantiate creates TMetaClassTypeDesc on demand from any
  'class of <Name>' type-name string; rejects non-class bases.
* Type-alias resolution routes 'class of T' aliases through the same
  on-demand path.
* CheckTypesMatch admits metaclass-of-TDerived → metaclass-of-TBase
  where TDerived is-a TBase, and metaclass ↔ untyped Pointer in both
  directions (so 'AClass: Pointer' parameters keep accepting class
  identifiers as before).
* Comparison rules accept (metaclass, metaclass), (metaclass, Pointer),
  and (metaclass, nil) operand pairs.
* The IsMetaclassRef branch now types the bare class identifier as
  'class of TFoo' instead of untyped Pointer; backwards-compat with the
  old Pointer typing flows through the new metaclass↔Pointer rule.

Codegen:
* QbeTypeOf(tyMetaClass) = 'l'.
* Var allocation, parameter spill, static-array element load/store,
  global-data emission and array-literal allocation all extended to
  accept tyMetaClass as a pointer-shaped value.
* The integer-comparison branch in EmitBinaryExpr now picks ceql/cnel
  whenever EITHER operand is pointer-shaped (class/nil/Pointer/
  metaclass), not just the left operand — fixes a latent issue where
  'X = SomeClass' with X on the left as Pointer compiled to ceqw.

Tests: 14 new tests in cp.test.classof.pas covering parser shapes
(alias / var-decl / field), semantic rules (kind, base, accepted /
rejected assignments, Pointer-arg interop, comparisons), and codegen
(typeinfo emission, global-data shape, ceql for equality).

1208 tests total (1194 + 14), all passing.  testpunit2 still produces
its expected pass/fail/inactive pattern.
2026-05-05 17:59:42 +01:00
Graeme Geldenhuys 6a22aa7828 feat(lang): default parameter values + metaclass refs uncovered by testpunit2
Three compiler additions, exercised end-to-end by testpunit2.pas:

* Default parameter values — single-name, non-var, non-open-array params
  may carry '= literal-or-named-constant'. Overload resolution accepts
  any arity in [MinArity, ParamCount]; the resolver tie-breaks toward
  the candidate that needs fewer defaulted slots. AnalyseProcCall and
  AnalyseFuncCallExpr clone the default expression into the call's Args
  list. Defaults declared on a unit-interface forward decl are
  ownership-transferred to the matching impl param at reconciliation.
* Metaclass references — a bare class type identifier in a value
  position (e.g. 'EError' as an argument or in 'Pointer(EError)') is
  now retyped to Pointer with codegen emitting '$typeinfo_<Name>' as
  an immediate. Matches the value held by vtable[0], so 'A.ClassType =
  EError' is true exactly when A is an instance of that exact class.
* Numeric widening at call sites — the existing CoerceArg helper now
  covers Integer→Double (swtof/sltof), Single→Double (exts) and
  Integer→Single, in addition to the pre-existing w→l. Inherited-,
  constructor-, method-, and function-call sites all route through it.

punit.pas gains DefaultDoubleDelta, two Double AssertEquals overloads,
and '= ''' defaults on the four-arity AddTest declarations.

Docs: grammar.ebnf extends ParamGroup with the optional default; a new
DefaultValue rule lists the permitted forms. language-rationale.adoc
adds 'Default Parameter Values' and 'Metaclass References' sections
recording the decision, alternatives rejected, and the overload
tie-break formula.

All 1194 existing compiler tests pass; testpunit2 compiles, assembles,
links and runs (31 tests; the expected pass/fail/inactive pattern).
2026-05-05 17:08:51 +01:00
Graeme Geldenhuys e6b8e1e532 fix(semantic): type-check indirect-call arguments; doc record-param ABI
Indirect calls through a procedural-typed variable (the 'IsIndirectCall'
path added when 'RunTest(@DoTest)' first compiled) only validated arg
count.  The argument types were never checked against the procedural
signature, so e.g. 'H("oops")' against 'procedure(N: Integer)' compiled
silently and miscompiled at the QBE level.

Both call sites — TProcCall (statement form) and TFuncCallExpr
(expression form) — now run each actual through CheckTypesMatch against
the corresponding TProcParamInfo, and reject non-L-value actuals for
var-typed parameters with the same diagnostic as the regular-call path.

Two semantic tests in cp.test.proctypes.pas pin the behaviour: one for
the statement form, one for the expression form.

Docs

- language-rationale.adoc: update the overload-mangling example so the
  emitted symbol matches what codegen actually produces ($Log_D_Si, not
  $Log$Si), and document the QBEMangle escape table for the three
  sigil characters that the QBE symbol grammar disallows mid-identifier.
- design.adoc: clarify the var-parameter ABI entry — record and static
  array value parameters share the by-pointer convention with var-params,
  which is what makes IsVarParam the right flag for codegen to key off.
2026-05-05 00:42:11 +01:00
Graeme Geldenhuys d324c6b07f fix(compiler): record-param ABI, virtual ToString dispatch, mangling consistency
Three classes of bugs surfaced while getting punit's testpunit1 to run.

Record params passed by reference (ABI fix)

The QBE ABI passes record/static-array parameters as pointers regardless
of var/const/value semantics, so the local _var_X slot holds a pointer,
not the aggregate bytes. Codegen was only dereferencing for var-params,
producing silently-wrong code for value-record params: F.A := 42 wrote
into the local 8-byte slot, ZeroMem(@ARun, SizeOf(...)) corrupted the
stack via 100+-byte memset, and indirect calls passed &slot rather than
*slot.

Semantic now sets IsVarParam on TIdentExpr, TFieldAccessExpr, and
TFieldAssignment for both skVarParameter and skParameter-of-aggregate.
Codegen dereferences the slot in EmitFieldAssignment, EmitInstancePtr,
EmitLValueAddr, EmitAddrOfExpr, and the TIdentExpr aggregate path.

Virtual dispatch on Obj.Method (no parens)

The TFieldAccessExpr IsMethodCall path emitted a static call ignoring
MDecl.VTableSlot. With user-overridden ToString and static type TFoo /
runtime type TBar, B.ToString resolved statically to TFoo's body. Now
checks VTableSlot >= 0 and emits vtable[(slot+1)*8] dispatch. Affects
every zero-arg virtual method called via Obj.Method, not just ToString.

Mangling consistency

EmitMethodDef and the two vtable emission sites bypassed QBEMangle, so
method definitions wrote \$TFoo_Show\$i while call sites wrote
\$TFoo_Show_D_i — a latent link error. All three sites now route through
QBEMangle (preserving the leading \$ sigil); cp.test.overload.pas
expectations updated to the consistent _D_ separator.

Three e2e tests in cp.test.e2e.pas pin the ToString behaviour: default
returns class name, override is reached through static base type,
inherited override still resolves at runtime. 1192 tests pass.

Other items folded in (prior WIP, surfaced together):
- Parser propagates Line/Col onto TBinaryExpr for diagnostics
- Pass 3 forward pointer alias resolution (PFoo = ^TFoo before TFoo)
- TProceduralTypeDesc structural pointer equivalence
- Indirect call statements through procedural variables
- Init/finalization stmt analysis
- Int64 boAnd/boOr operand extension
- TObject vtable slot 1 = ToString; RTL TObject_ToString helper
- EmitGlobalVarData for unit interface block
- punit workarounds (Copy for indexed string, lifted nested proc)
2026-05-05 00:12:22 +01:00
Graeme Geldenhuys 1a1614012d feat(lang): Delete + SetLength + ClassType builtins, plus L-value var-arg widening
Adds three builtins requested by punit's port and the language-feature
gaps surfaced while wiring them through:

Delete(var S: string; Idx, Count: Integer)
  RTL helper _StringDelete returns a new string with the slice removed
  (1-based Idx; out-of-range / non-positive Count yields a copy).
  Codegen emits the standard string-mutator ARC sequence: load old →
  call helper → addref new → release old → store new through the var
  slot.  Factored out as TCodeGenQBE.EmitStringMutator for reuse.

SetLength(var S: string; N: Integer)
  RTL helper _StringSetLength returns a new string of length N (truncate
  or NUL-pad).  Same codegen path as Delete via EmitStringMutator.

obj.ClassType : Pointer
  TClass registered as a built-in alias of Pointer (placeholder until
  a real tyMetaClass arrives).  AnalyseFieldAccess detects ClassType
  on tyClass receivers and sets TFieldAccessExpr.IsClassTypeAccess.
  Codegen emits two indirections — instance → vtable → typeinfo —
  yielding the typeinfo data pointer.  Equality comparison of two
  ClassType results correctly distinguishes dynamic types.

Assigned(P) : Boolean
  Added as a sibling of the above because it's the natural companion
  for the metaclass / pointer code in punit (and otherwise a missing
  standard Pascal builtin).  Emits a 'cnel %ptr, 0' comparison.

Incidentals (bug fixes uncovered while testing):
  - AnalyseFieldAssignment now routes set-literal RHS into a tySet
    field through AnalyseSetLiteralExpr, so 'R.Options := []' and
    'P^.Options := []' are accepted.  Previously the empty literal
    was rejected as an array literal with zero elements.
  - Var-argument validation: adds TDerefExpr to the accepted L-value
    forms (P^ as a var argument) alongside TIdentExpr and
    TFieldAccessExpr.  TCodeGenQBE.EmitLValueAddr generalises
    EmitVarArgAddr to cover all three forms; var-arg call sites in
    EmitProcCall, EmitFuncCall paths, and EmitInheritedCall now go
    through it.
  - CheckTypesMatch: nil literal now compatible with tyProcedural
    parameter slots (already done in Phase D for procedural-handler
    nil-clear idioms; unchanged here, included for completeness in
    the var-arg/builtin context).

Tests: 1189 → 1192 pass, 0 errors, 0 failures.
  - cp.test.stringops: Delete/SetLength semantic OK + non-string
    rejection + codegen calls _StringDelete / _StringSetLength.
  - cp.test.typetests: ClassType semantic OK, resolves to tyPointer,
    TClass alias usable, codegen emits loadl chain.
2026-05-04 11:26:01 +01:00
Graeme Geldenhuys dbe1f37b65 feat(unit/lang): unit infrastructure fixes uncovered by punit re-port (Phase D)
Re-restoring 'overload' directives across punit.pas (the Phase A
blocker called out by the handover) surfaced a series of unit-section
gaps that were silently broken because no test exercised them.  Each
fix is small and bounded; together they let punit progress past the
overload duplicate-identifier wall and ten subsequent declarations,
into the next genuine missing-feature blocker (Delete-as-builtin).

Compiler changes:

- ParseForwardDecl (uParser.pas): now accepts the same directive set
  as ParseMethodDecl — overload, external, inline, stdcall, cdecl,
  register, pascal, safecall, forward, deprecated, platform,
  experimental.  Previously only 'external' was recognised, so any
  unit interface forward decl carrying 'overload' raised
  "Expected token tkImplementation".
- AnalyseUnit / AnalyseUnitForExport (uSemantic.pas): symbol
  registration for both forward and impl-only standalone proc/func
  decls now propagates IsOverload, sets ResolvedQbeName, and uses
  signature-aware matching to pair an impl with its forward decl
  when both are overloaded.
- AnalyseUnit: now also processes interface-section variable decls
  (registering them as IsGlobal symbols visible to impl bodies),
  interface-section const decls, impl-section const + type decls,
  and impl-section global var decls.  AnalyseUnitForExport got the
  matching impl-section type-decl + interface-section var-decl
  passes.  Without these, any unit whose impl bodies referenced an
  impl-section enum or an interface-section global silently raised
  "Unknown type" / "Undeclared variable".
- AnalyseStandaloneDecl: skip body analysis when ADecl.Body = nil.
  Prevents an AV when a forward-only decl reaches the body pass
  (e.g. a forward overload whose impl lives in another section).
- TFieldAssignment: accept skVarParameter (in addition to skVariable
  and skParameter) as a valid receiver kind so 'Suites.Count := 0'
  is permitted inside 'procedure InitSuiteList(var Suites: TSuiteList)'.
- Var-argument check: an L-value var argument may now be a
  TFieldAccessExpr (R.F or P^.F) in addition to a plain TIdentExpr.
  The existing field-access typing already produces ResolvedType so
  the subsequent CheckTypesMatch handles it correctly.
- CheckTypesMatch: nil literal now compatible with tyProcedural
  parameter slots (procedural-type fields are routinely nil-cleared
  via SetXxxHandler(nil) idioms).

punit changes:

- Re-applied 'overload;' to every interface and implementation
  declaration of the 17 names that participate in overload sets
  (AddSuite, AddTest, AssertEquals, AssertPassed, DoRunSysTests,
  ExpectMessage, FreeSuiteList, GetSuite, GetSuiteCount,
  GetSuiteIndex, GetTest, GetTestCount, GetTestIndex, RunSuite,
  RunTest, SetTestResult, SetTestResultRec).

Tests: 1179 pass, 0 errors, 0 failures.  New regression coverage:

- cp.test.varparams: var-arg accepts R.F and P^.F field accesses.
- cp.test.units: interface var visible in impl, impl-section type
  decl in scope, forward decl with 'overload' parses + analyses.

Punit still does not compile end-to-end: the next blocker is the
Delete(S, Idx, Count) string built-in (line 844), unrelated to
overloading.  That is a separate language-feature task.
2026-05-04 11:06:12 +01:00
Graeme Geldenhuys 0298a92e97 feat(lang): function overloading — Phase C (class methods + virtual)
Class method overloading and overloaded virtual/override dispatch.

Forward / impl matching now keys on (TypeName, Name, ParamSig) instead
of just (TypeName, Name): LinkClassMethodImpls walks all FMethodIndex
entries with the matching key and pairs each impl with the forward
decl whose signature equals the impl's.  Method registration enforces
the 'overload' directive — same-class same-name pairs without it now
produce a clean "Duplicate method" error rather than the misleading
"already has an inline body".

Each (Name, ParamSig) pair becomes its own vtable slot, so two virtual
overloads dispatch independently and overrides target the slot whose
mangled signature matches.  Required moving parameter-type resolution
ahead of the vtable pre-pass — Pass 1 of AnalyseTypeDecls already
registers every type name, so referencing other types in method
parameters before the host class's own fields are resolved is safe.

ResolveMethodOverload mirrors ResolveStandaloneOverload: walks the
inheritance chain, filters by arity, scores by argument type using
the existing ArgMatchScore, picks the best match, raises on ambiguity.
Three call sites in AnalyseMethodCall (receiver-expression,
implicit-self, variable receiver) and AnalyseMethodCallExpr now go
through it; the redundant CheckTypesMatch loops afterwards are gone
since the resolver already enforced compatibility.

Codegen: a small MethodEmitName helper centralises 'use ResolvedQbeName
when set, otherwise <Owner>_<Name>'.  Eight method-call emit sites
(static dispatch, virtual dispatch, sret returns, field-method calls,
inherited calls) now go through it.

Tests: 5 new cases in cp.test.overload.pas — class overload registration,
distinct mangled QBE names, mangled call sites, dup-without-overload
rejection, and (virtual + overload + override) producing distinct
vtable slots.  All 1174 tests pass.

End-to-end verified: 'B := TChild.Create; B.Greet(99); B.Greet(''hi'')'
where B's static type is TBase dispatches both overloads through the
overloaded vtable to TChild's overrides.
2026-05-04 10:47:59 +01:00
Graeme Geldenhuys 89ae192060 fix(semantic): TSemanticAnalyser.AnalyseUnit transfers symbol-table ownership
AnalyseUnit (the test-only entry point that analyses a unit in
isolation, distinct from the production AnalyseUnitForExport +
Analyse(Prog) two-phase pipeline) used to leave the symbol table
behind on the analyser.  When the caller freed the analyser, the
TTypeDesc instances pointed at by Par.ResolvedType /
ResolvedReturnType went with it — leaving every AST node holding a
dangling pointer.  A subsequent QbeTypeOf(AType) inside GenerateUnit
then dereferenced that garbage and segfaulted.

Five TUnitTests (TestCodegen_Unit_NoMainFunction,
TestCodegen_Unit_IntfFunctionsExported,
TestCodegen_Unit_FunctionBodyInIR,
TestCodegen_Unit_ImplOnlyFuncNotExported,
TestCodegen_Unit_CorrectArithmetic) all crashed at this point.

Fix: add TUnit.SymbolTable, transfer ownership at the end of
AnalyseUnit (mirroring TProgram), and free it in TUnit.Destroy.  The
production multi-file pipeline never touches AnalyseUnit and is
unaffected (the program owns the shared table).

All 1169 tests now pass — zero errors, zero failures.
2026-05-04 10:36:32 +01:00
Graeme Geldenhuys 813d4a1fe1 fix(codegen): spill Double/Single params with stored/stores, not storel
EmitFuncDef and EmitMethodDef both fell through to a 'storel' catch-all
when spilling parameters into local slots, so QBE rejected any function
with a Double or Single parameter ('invalid type for first operand
%_par_X in storel').  Each float kind needs its own QBE store width:
'd' parameters use 'stored' over an alloc8, 's' parameters use 'stores'
over an alloc4.

This was masked previously because no test exercised a float parameter
in a callable position end-to-end — the bug surfaced when wiring up an
overload demo with three F(...) variants over Integer/Double/string.

Adds two regression tests in cp.test.procs.pas asserting that the spill
emits the type-correct store opcode.
2026-05-04 10:32:04 +01:00
Graeme Geldenhuys 91ee6919b9 feat(lang): function overloading — Phase B (type-distinct + Delphi scoring)
Replaces Phase A's arity-only resolution with full Delphi-style
overload resolution: exact-type match (score 2) is preferred over
widening (score 1); ambiguous ties at the top score raise a compile-
time error.  QBE mangling switches from the temporary '$N<arity>'
suffix to a type-code suffix:

  i  Integer       l  Int64       u  UInt32      y  Byte
  b  Boolean       d  Double      s  Single      S  string
  C  PChar         p  untyped Pointer            E<Name>  enum
  R<Name>  record  K<Name>  class           I<Name>  interface
  ^X  pointer-to-X            A<X>  open-array of X
  T<Name>  set    F<Name>  procedural type    @X  var/out X

So 'procedure F(N: Integer)' emits as $F$i and 'procedure F(D: Double)'
as $F$d.  The fall-through branch in ArgMatchScore probes the existing
CheckTypesMatch, picking up nil-literal compatibility, class-subtype
widening, untyped-Pointer interop, enum/integer crossover, procedural
signature compat, and open-array forwarding without duplicating that
logic.

- AnalyseProcCall and AnalyseFuncCallExpr now analyse all arguments
  before resolving, so the resolver has Arg.ResolvedType available
  for scoring.
- Non-var argument compatibility is now established by the resolver;
  the redundant CheckTypesMatch loop afterwards is removed.
- Tests: 5 new cases — type-distinct registration, distinct mangled
  QBE names, exact-beats-widening, widening fallback, ambiguous error.
- Phase A tests updated for the new mangling ('$' for zero-arg, '$i'
  for Integer).
2026-05-04 10:31:50 +01:00
Graeme Geldenhuys 3bc91bd035 feat(lang): standalone function overloading — Phase A (arity)
Adds the 'overload' directive and Delphi-style overload semantics for
standalone procedures and functions. Phase A resolves overload sets by
parameter arity only; the QBE mangling uses a temporary '$N<arity>'
suffix that will be replaced by full type-code mangling in Phase B.

- TMethodDecl gains IsOverload and ResolvedQbeName.
- TSymbol gains IsOverload, NextOverload (chain), and Decl back-pointer.
- TScope.Define appends to the overload chain when both old and new
  symbols carry IsOverload; mixing overload + plain duplicates is a
  duplicate-identifier error.
- ResolveStandaloneOverload picks the matching arity from FProcIndex
  and is wired into AnalyseProcCall and AnalyseFuncCallExpr.
- Codegen emits via TMethodDecl.ResolvedQbeName at all four standalone-
  call sites and at EmitFuncDef.
- Grammar: 'overload' added to MethodDirective.
- Rationale: documents Delphi semantics, type-code mangling, alternatives
  rejected.
- Tests: cp.test.overload.pas, 7 cases covering parser flag, chain
  acceptance, mixing rejection, no-matching-arity error, and codegen.
2026-05-04 10:22:21 +01:00
Graeme Geldenhuys 0133f62822 feat(lang): P^.Field deref+access, initialization/finalization, TClass, more
Additional language features and fixes built on the previous commit:

P^.Field — pointer dereference followed by field access in both
  expression and assignment positions. Parser builds TDerefExpr as the
  Base of TFieldAccessExpr; codegen returns the record address directly
  (no double-load for tyRecord base types).

initialization / finalization sections in units — lexer adds
  tkInitialization / tkFinalization tokens; unit parser collects
  statements into TUnit.InitStmts / FinalStmts; codegen emits
  export functions $<Unit>_init() / $<Unit>_fini(); EmitMainHeader
  calls each $<Unit>_init() in import order.

TClass as Pointer alias — punit and testpunit adapted; TClass replaced
  with Pointer where RTTI is not yet available.

punit.pas adapted further:
  * PTest / PSuite / PResultRecord etc. restored as type aliases
  * BlockGet/BlockSet use PPointer typed local variables
  * ^TFoo(expr) casts replaced with PFoo(expr) using new type aliases
  * local type/const sections with typed array constants replaced with
    plain integer stage constants + case functions
  * Exit(expr) replaced with Result := expr; Exit
  * local typed const array (BStrs) replaced with if/else
  * RunSuiteSetup/TearDown/RunTestHandler extract function pointers
    to local vars before calling (workaround for ^.FnField() calls)
  * EFail.ToString changed from override to virtual

Tests: 1155 pass, 5 pre-existing errors, 0 new failures.
2026-05-04 02:33:09 +01:00
Graeme Geldenhuys faee6e6bef feat(lang): type aliases, floats, Abs, ClassName + global record fix
Six missing language features added:

1. type PFoo = ^TFoo — pointer and simple type aliases in type sections.
   Parser dispatches tkCaret/tkIdent to new TTypeAliasDef AST node;
   semantic pass resolves to TPointerTypeDesc or the aliased type.

2. Double / Single float types — lexer emits tkFloatLit; TFloatLiteral
   AST node; tyDouble/tySingle in the type system; QBE 'd'/'s' emit;
   arithmetic, comparison, and integer promotion in codegen;
   DoubleToStr, SingleToStr, StrToDouble, Abs(Double) built-ins;
   _DoubleToStr/_SingleToStr/_StrToDouble/_AbsInt/_AbsInt64 in RTL
   (new blaise_float.c). QBE generates SSE2 instructions automatically.
   Float const declarations supported.

3. Abs() — built-in for Integer, Int64, Double, Single.

4. TObject.ClassName — typeinfo gains a third slot (offset 16) holding
   a pointer to an immortal class-name string ($__cn_TFoo + 12).
   obj.ClassName loads vtable[0] (typeinfo), then typeinfo[16] (nameptr).
   EmitClassNameRef() emits the data-section label+offset relocation.

5. Global record field bug fix — FieldPtr() now accepts AIsGlobal and
   uses VarRef() so $RecordVar is used for global records instead of
   %_var_RecordVar. Both assignment and read paths fixed.

6. future-improvements.adoc — implemented items marked; Currency and
   BigDecimal deferred to BCL packages section added.

Tests: 1155 pass, 5 pre-existing errors (TUnitTests AV), 0 new failures.
2026-05-04 02:02:06 +01:00
Graeme Geldenhuys f256052a2b feat(types): bare procedural types (function/procedure pointers)
Adds support for declaring named procedural types that hold pointers to
standalone functions and procedures:

  type
    TIntFn   = function: Integer;
    TStrFn   = function(const S: string): Integer;
    TLogProc = procedure(Level: Integer; const Msg: string);

  var F: TIntFn;
  begin F := @MyFn; X := F(); end;

A procedural variable is stored as a single QBE 'l' (8-byte code
pointer). @FuncName produces a value of the matching procedural type.
Indirect calls F(args) load the pointer and emit a QBE indirect call.

Compatibility requires return types to match (both nil or both same
TTypeDesc) and parameter lists to match pairwise on type and parameter
mode (var/const/value); names do not participate.

Compiler additions:
* tyProcedural TTypeKind + TProcParamInfo + TProceduralTypeDesc
  (with IsCompatibleWith)
* TProceduralTypeDef AST node
* Parser: type T = function/procedure ... ; reuses ParseParamList
* Semantic: ResolveProceduralTypeDef in pass 2; AnalyseAddrOfExpr
  short-circuits @FuncName to a procedural-typed value;
  AnalyseFuncCallExpr accepts procedural-typed variables as
  indirect-call targets; CheckTypesMatch allows compatible
  procedural assignment
* Codegen: QbeTypeOf(tyProcedural) -> 'l'; EmitVarAllocs emits an
  8-byte slot; EmitAddrOfExpr emits $FuncName for @FuncName;
  EmitFuncCallExpr emits 'call %tmp(...)' for indirect calls,
  placed before the ResolvedDecl=nil type-cast branch

Out of scope (deferred until a use case requires them):
* function ... of object (method pointers — fat pointer ABI)
* reference to function/procedure (anonymous methods / closures)
* cdecl/stdcall calling-convention markers on procedural types

Tests: cp.test.proctypes.pas — 14 tests covering parser (kinds,
return types, params, var/const flags), semantic (compat accept/
reject on return type and arg count), and codegen (var allocation,
@FuncName emission, indirect-call emission).

Grammar and rationale: docs/grammar.ebnf adds the ProceduralType
production; docs/language-rationale.adoc captures the decision and
deferred items.

Motivation: prerequisite for porting Michael Van Canneyt's punit test
framework into rtl/src/test/pascal/, where every test, every
Setup/TearDown, and every hook handler is stored as a function
pointer.

1155 tests pass (1141 pre-existing + 14 new), no regressions.
2026-05-03 23:15:42 +01:00
Graeme Geldenhuys 5894411a35 chore(license): relicense from BSD-3-Clause to Apache 2.0 + Runtime Library Exception
Adopts the Swift/LLVM model: Apache License 2.0 with the Runtime Library
Exception text used verbatim by the Swift project. SPDX identifier:
Apache-2.0 WITH Swift-exception.

Apache 2.0 brings an explicit patent grant and patent-retaliation clause
that BSD-3 lacks. The Runtime Library Exception ensures binaries
produced by the Blaise compiler do not inherit attribution obligations
from the linked-in RTL.

* LICENSE — full Apache 2.0 + RLE text (replaces the deleted LICENCE).
* NOTICE — project header plus QBE attribution (MIT, vendored).
* docs/language-rationale.adoc — new Project Governance section
  capturing the decision, alternatives considered, and rationale.
* SPDX headers updated across all .pas, .pp, .inc, .c source files,
  build scripts, PasBuild plugins, and the Makefile.
* project.xml license field updated.
* tools/migrate_full.py HEADER template updated so generated
  self-hosting source carries the new licence.
2026-05-03 19:45:29 +01:00
Graeme Geldenhuys aafd004973 feat(generics): for..in support on generic collections (Step 9b)
Three fixes to unblock for..in over generic collection types:

1. SubstTypeParam: extend to substitute type params inside generic
   instantiation names.  Previously only bare 'T' and '^T' were
   handled; 'SomeName<T>' was left unsubstituted, so a method
   returning 'TListEnumerator<T>' kept the raw param name after
   cloning.

2. InstantiateGeneric: clone TClassTypeDef.Properties with type-param
   substitution (same pattern as fields and methods).  Without this
   the 'Current: T' property was invisible on instantiated enumerator
   types and FindProperty returned nil during for..in semantic checks.

3. AnalyseFieldAccess + ResolveScopeBoundTypeParams: when a generic
   RecordName such as 'TGenEnum<T>' isn't found in the symbol table,
   resolve each type argument against the current scope (where
   T=Integer is pushed during method body analysis) and retry the
   lookup with the concrete name 'TGenEnum<Integer>'.

New: cp.test.genericforin.pas — 10 tests covering property visibility,
property type after instantiation, and end-to-end for..in IR emission.
1141 tests total, all passing.
2026-05-03 16:33:08 +01:00
Graeme Geldenhuys 5af72daac7 feat(exceptions): typed except handlers and bare re-raise (Step 8)
Implements `on E: TClass do` dispatch inside except blocks and correct
bare `raise` propagation inside typed handlers.

New AST node TExceptHandlerClause carries VarName, TypeName, and Body.
TTryExceptStmt gains a Handlers list and ElseBody for the typed case;
ExceptBody remains for the legacy catch-all form.

Parser detects the 'on' identifier after 'except' to switch between the
typed-handler path and the plain catch-all path.  The no-binding form
`on TFoo do` is also supported.

Semantic pass validates that handler types resolve to classes, injects
a synthetic TVarDecl into the enclosing block for each bound variable
(so EmitVarAllocs allocates a stack slot at function entry), and opens a
local scope so the variable is accessible inside the handler body.

Codegen emits a _CurrentException call (before _PopExcFrame) followed by
an _IsInstance check per handler.  The first matching handler stores the
exception pointer into the variable slot and runs the body.  If no handler
matches and there is no else clause, _Reraise propagates to the enclosing
handler.

RTL: _CurrentException now reads g_current_exception (set by _Raise at
raise time) rather than g_exc_top->exception.  This is correct because
_PopExcFrame has already unwound the handler's own frame by the time a
bare raise is executed inside the handler body.

EmitRaiseStmt bare-raise path now calls _CurrentException then _Reraise
instead of _Raise(0), which previously cleared g_current_exception.

22 new tests (17 unit + 5 E2E): parser, semantic, codegen, and runtime
dispatch (correct handler selection, subclass matching, unmatched reraise,
bare raise propagation, else clause execution).  Total: 1131 tests, 0 failures.
2026-05-03 10:30:15 +01:00
Graeme Geldenhuys 5df182da86 chore: begin v0.5.0-dev cycle 2026-05-02 23:14:30 +01:00
Graeme Geldenhuys 8240569a7f release: v0.4.0
Self-hosting fixpoint achieved on the multi-file source after the
Int64-correctness and implicit-Self chained-access bugs were fixed.
releases/v0.4.0/blaise (Blaise-compiled) recompiles Blaise.pas and
produces byte-identical IR (88,883 lines) to the FPC-built stage-1.
2026-05-02 23:13:04 +01:00
Graeme Geldenhuys a2d0f2cbe4 fix(self-host): three Int64-correctness bugs unblock fixpoint
The self-built compiler segfaulted in MemCopy/_StringCopy when given any
multi-unit input, and even when it ran it produced IR that diverged from
the FPC-built compiler's output. Three independent bugs all rooted in
Int32-vs-Int64 confusion:

1. _StringCopy(S, From, MaxInt) — the Copy(S, N, MaxInt) "rest of string"
   idiom — performed `(Start + Count) > SLen` to clamp Count, which
   silently overflowed for Count = MaxInt. The check now compares
   `Count > SLen - Start` instead, which is overflow-safe.

2. _StrToInt / _StrToInt64 only accepted decimal digits, silently
   returning 0 for the FPC-compatible '$xx' hex form. The compiler uses
   hex for FNV constants ($811C9DC5 etc.), so the self-built binary
   produced 0 where it should produce a real FNV offset. Both routines
   now accept '$' followed by [0-9A-Fa-f]+.

3. Comparing an Int64 against any non-Int64 operand (notably the literal
   0 in `if N < 0`) emitted `csltw` — a 32-bit comparison — even when one
   side was 64-bit. _Int64ToStr's negative-sign check therefore decided
   sign by inspecting the low 32 bits of N, so any Int64 whose low 32
   bits had the sign bit set printed as a wrapped negative. The codegen
   now picks `csltl/csgtl/cslel/csgel/ceql/cnel` and sign-extends the
   non-Int64 side when either operand is Int64.

Two ConstValue emit sites also switched from `%d` (truncated to int32 by
the C `_StringFormat` runtime) to `IntToStr(...)` + `%s`, so Int64
constants survive the trip through Format.

Result: stage-2 (FPC-built) and stage-3 (self-built) IR are now
byte-identical at 88,883 lines — fixpoint achieved.

Three new e2e regression tests cover the StrToInt hex form, the
Copy MaxInt idiom, and the Int64 sign-handling case.
2026-05-02 21:22:39 +01:00
Graeme Geldenhuys 6bc88f2cde fix(codegen): implicit-Self field as base of chained access
EmitInstancePtr handled the IsImplicitSelf flag on TIdentExpr but not on
TFieldAccessExpr leaves (Base=nil, RecordName a field of Self). For a
chain such as FInner.Leaf.Value inside a method, the inner FInner.Leaf
node was emitted as `loadl %_var_FInner` — a phantom local — causing
QBE to reject the IR with `invalid type for first operand`.

This blocked self-hosting: TOPDFEmitter accesses FProgram.Block.* and
FProgram.SymbolTable.* in several methods, all of which produced
%_var_FProgram references that QBE refused, silently dropping every
function defined after the first error and breaking the link step.

Add the equivalent IsImplicitSelf branch in the TFieldAccessExpr leaf
path, mirroring the TIdentExpr handling. Add a regression test using a
3-level chain (the minimum that exercises the buggy path).
2026-05-02 20:28:28 +01:00
Graeme Geldenhuys 1931b84bc8 fix(compiler): self-hosting fixpoint unblocks — Cardinal, PtrUInt, xor, _ClassRelease
Seven source-compatibility fixes required to get the Blaise compiler to
parse and analyse its own source during the self-hosting fixpoint attempt.
None of these affect compiled output for existing programs; all 1105 tests
still pass.

  Cardinal alias (uSymbolTable): maps to UInt32; used in uDebugOPDF.pas
  PtrUInt alias (uSymbolTable):  maps to Int64; used in uDebugOPDF.pas
  xor operator (uLexer/uAST/uParser/uSemantic/uCodeGenQBE): tkXor/boXor
    emit QBE 'xor'; used in uDebugOPDF.pas FNV hash
  _ClassRelease external (blaise_arc.pas interface): C function declared
    callable from Pascal; contnrs.pas calls it directly
  contnrs.pas uses blaise_arc: makes _ClassRelease visible
  not 7 → -8 (uDebugOPDF.pas): Blaise 'not' is Boolean-only; -8 = not 7
  TProcess.Options removed (Blaise.pas): Blaise RTL TProcess has no
    Options property; pipes are always active
  FOutput.Strings[] (uDebugOPDF.pas): Blaise TStringList has no default
    [] property; must use explicit .Strings[I] form

Stage-2 IR now generates: 88,526 lines, exit 0.
Remaining blocker: %_var_FProgram QBE error (see handover.txt).
2026-05-02 19:59:04 +01:00
Graeme Geldenhuys 5083beb38f docs: document string data-pointer ABI decision and rationale
Adds a detailed implementation note under the String Subscript section
explaining the data-pointer memory layout, why Blaise chose this over
header-pointer, the relationship to FPC's convention, and the implication
for the Phase 8 Migration Analyser when porting FPC code.
2026-05-02 12:33:04 +01:00
Graeme Geldenhuys 2b8c071219 feat(opdf): replace recAnsiStr with recUtf8Str for Blaise strings
Blaise's single string type is UTF-8 with no code-page or element-size
fields — recAnsiStr is the wrong record type for it.  Emit recUtf8Str
(type 23) instead, with three signed offsets (RefCountOffset=-12,
LengthOffset=-8, CapacityOffset=-4) relative to the data pointer.

Changes in Blaise compiler:
- uDebugOPDF: EmitAnsiStr → EmitUtf8Str; REC_UTF8STR=23; canonical
  name 'Utf8String'; record size 12 bytes (no CodePage/ElementSize)
- CanonicalName: tyString now maps to 'Utf8String'

Test: TestOPDF_AnsiStr_Record updated to check '# recUtf8Str' and
'.ascii "Utf8String"'.
2026-05-02 12:31:47 +01:00
Graeme Geldenhuys c657cccc11 feat(rtl+compiler): string data-pointer ABI — consistency with class objects
Changes Blaise's string variable convention from header-pointer to
data-pointer, matching how class objects work: the pointer stored in a
variable slot points to the first useful byte, with the ARC bookkeeping
header immediately before it at negative offsets.

New layout (data-pointer convention):
  data_ptr - 12  RefCount  (4 bytes, Integer)
  data_ptr -  8  Length    (4 bytes, Integer)
  data_ptr -  4  Capacity  (4 bytes, Integer)
  data_ptr +  0  char data (UTF-8 bytes + NUL terminator)

Old layout (header-pointer convention):
  header_ptr +  0  RefCount
  header_ptr +  4  Length
  header_ptr +  8  Capacity
  header_ptr + 12  char data

Benefits:
- Consistency: both strings and class objects use "pointer to content"
  convention; ARC header hidden at negative offsets for both
- S[N] subscript: simplifies from (ptr + N + 11) to (ptr + N - 1)
- PChar(str) cast: becomes an identity — str IS already the data pointer
- C interop: zero-cost on non-Win32 targets (Linux, macOS POSIX)
- OPDF: recAnsiStr now emits correct negative LengthOffset/RefCountOffset

RTL changes (all six string-layer files):
  blaise_str.pas   StrAlloc returns base+12; StrLen reads ptr-8;
                   StrData is identity; all callers updated
  blaise_arc.pas   _StringAddRef/Release access ptr-HDR_SIZE;
                   _StringEquals/_StringConcat updated
  blaise_sys.pas   _SysWriteStr/Int/Int64 read len at ptr-8
  blaise_str_fmt.c str_alloc returns base+HDR_SIZE; str_data identity;
                   str_len reads ptr-8
  blaise_io.c      io_str_alloc/data/len updated; all callers fixed
  blaise_process.c proc_str_alloc/data/from_cstr updated

Compiler codegen:
  EmitStrLit    adds 'add $__sN, 12' to convert header label to data ptr
  S[N]          'add idx, 11' → 'sub idx, 1'
  PChar(str)    '+12 add' removed — identity pass-through
  for B in S    length from ptr-8; data from ptr+0 (no skip offset)

OPDF emitter: LengthOffset=-8, RefCountOffset=-12 (was +4, +0)

Tests: 2 test assertions updated to match new ABI semantics;
1105 total, all passing.
2026-05-02 12:26:13 +01:00
Graeme Geldenhuys 4f82c7cc91 feat(compiler): for..in — string byte iteration (Step 7c)
Adds 'for B in S do' iteration over the raw UTF-8 bytes of a string,
consistent with S[N] returning Byte.  Loop variable may be any ordinal
type (Byte, Integer, etc.).

- uAST: TForInStmt gains IsStringIter field
- uSemantic: tyString branch in for-in dispatch — requires ordinal loop
  variable; injects synthetic __idx_N (Integer) slot into block Decls
- uCodeGenQBE: IsStringIter path — 0-based index, condition reads
  length from header at ptr+4 (csltw), element loaded via
  loadub(ptr+12+idx); loop variable assigned with storew

Language layout reminder: string header is [refcount(4)][length(4)]
[capacity(4)][data...] so length is at offset +4 and char data at +12.

Docs: language-rationale.adoc updated — string byte iteration now listed
as supported under 'for..in'; set iteration and generic collection
iteration noted as deferred.

Tests: 8 new tests (3 semantic + 5 codegen); 1105 total, all passing.
2026-05-02 11:12:52 +01:00
Graeme Geldenhuys 244dfe3259 feat(compiler): for..in — static array iteration (Step 7b)
Extends TForInStmt with an IsArrayIter path for 'for X in Arr do'
where Arr is a static array[L..H] of T.

- uAST: TForInStmt gains IsArrayIter, IdxVarName, ArrayLow, ArrayHigh
- uSemantic: collection-kind dispatch — tyStaticArray goes to the
  index-based path; tyClass goes to the enumerator protocol path;
  anything else is an error.  The static-array path injects a synthetic
  __idx_N (Integer) slot into the enclosing block for EmitVarAllocs.
- uCodeGenQBE: EmitForInStmt opens with an IsArrayIter branch that
  emits an index-counter loop: init to ArrayLow, cslew to ArrayHigh,
  element load via (base + (idx-low)*elemSize), assign to loop var
  with ARC handling for string/class elements, increment + jmp back.
  Non-zero-based ranges subtract LowBound before the multiply.

Tests: 8 new tests (5 semantic, 3 codegen for arrays); 1097 total,
all passing.
2026-05-02 11:01:57 +01:00
Graeme Geldenhuys 4ddc1944bd feat(compiler): for..in loop — class-based enumerator protocol (Step 7a)
Implements `for X in Collection do` iteration using the GetEnumerator
protocol, matching FPC/Delphi semantics.  This first commit covers
class-based enumerators; static array and byte-string iteration are
deferred to follow-on commits.

Compiler changes:
- uAST: new TForInStmt node (VarName, CollExpr, Body; semantic
  annotations: EnumVarName, ResolvedVarType, GetEnumDecl, MoveNextDecl,
  CurrentDecl)
- uParser: ParseForStmt returns TASTStmt; detects 'for X in' vs
  'for X :=' and produces TForInStmt or TForStmt respectively
- uSemantic: AnalyseStmt handles TForInStmt — verifies GetEnumerator/
  MoveNext/Current protocol, checks type compatibility, injects a
  synthetic TVarDecl (__forin_N) into the enclosing block so
  EmitVarAllocs allocates the slot and EmitArcCleanup releases it
- uCodeGenQBE: EmitForInStmt — calls GetEnumerator, ARC-stores result
  in synthetic slot, loops via MoveNext, reads Current, assigns to
  loop variable; handles both static and virtual dispatch
- uDebugOPDF: CollectStmtLines extended with TForInStmt case

RTL additions:
- classes.pas: TStringListEnumerator + TStringList.GetEnumerator
- contnrs.pas: TObjectListEnumerator + TObjectList.GetEnumerator

Docs:
- grammar.ebnf: ForStmt gains the 'FOR IDENT IN Expr DO Stmt' alternative
- language-rationale.adoc: new Iteration section documenting the protocol
  choice and what is deferred

Tests: 17 new tests in cp.test.forin (parser, semantic, codegen);
1089 total, all passing.
2026-05-02 10:21:12 +01:00
Graeme Geldenhuys 95f71e1acc feat(test): PDR integration test suite and pasbuild-integration-test plugin
Adds compiler/src/it/ (Maven-convention integration test directory) with
a Blaise-specific PDR driver. Four initial tests ported from the OPDF
integration suite: breakpoint+next, local variables, locals command,
and step-over. Test programs are adapted for Blaise (no FPC directives),
line numbers preserved to match the original commands files.

The pasbuild-integration-test plugin (phase: none) can be invoked as
'pasbuild integration-test'; it verifies pdr and the Blaise binary are
present, then delegates to compiler/src/it/run_tests.sh.

All four tests currently fail — line info in the OPDF section maps every
statement to the function-start address (per-stmt addresses require QBE
changes, tracked in future-improvements.adoc). The aspirational expected
files show exactly what each test should produce once OPDF is complete,
making failures a clear roadmap rather than noise.

Also adds PIE/ASLR support entry to future-improvements.adoc (Linux,
FreeBSD, macOS load-base strategies for the PDR debugger).
2026-05-02 00:35:00 +01:00
Graeme Geldenhuys 0e08dc3a53 feat(debug): OPDF Step 6c — recPointer, recArray, recSet, recProperty, recInterface, recConstant, recUnitDirectory
- recPointer: emitted for typed/untyped pointer fields; TargetTypeID links to
  base type; CanonicalName extended for tyStaticArray and tyOpenArray
- recArray: static arrays emit IsDynamic=0 with TArrayBound (LowerBound,
  UpperBound as Int64 quads); dynamic/open arrays emit IsDynamic=1 without bounds
- recSet: BaseTypeID references the base enum; SizeInBytes from RawSize
- recProperty: emitted after each recClass; field-backed uses numeric offset
  (patField), method-backed uses linker label (patMethod)
- recInterface: TypeID + ParentTypeID + zeroed GUID + method descriptors with
  ReturnTypeID and ParamCount per method
- recConstant: iterates program-block ConstDecls; ckOrd embeds 8-byte Int64,
  ckString embeds raw bytes
- recUnitDirectory: first record after header; RecordOffset computed via GAS
  label arithmetic (.Lopdf_unit0_start - .Lopdf_start); RecordCount patched at
  end; header Flags set to OPDF_FLAG_HAS_DIRECTORY = 1
- uSymbolTable: add Properties: TObjectList read property to TRecordTypeDesc
- 12 new tests; 37 OPDF tests total; 1072 compiler tests all passing
2026-05-01 23:37:07 +01:00
Graeme Geldenhuys 2e8eb590eb fix(debug): emit main program scope and fix PIE breakpoint addresses
- uDebugOPDF: add EmitFunctionScope_Main to emit recFunctionScope keyed on
  label 'main' for the program body, including per-line recLineInfo records
- uDebugOPDF: refactor EmitLineInfoForScope into EmitLineInfoForBlock so
  both procedure scopes and the main body share one implementation
- Blaise: pass -no-pie to gcc when linking a debug-opdf build so OPDF
  .quad addresses are absolute runtime addresses; PIE relocation produces
  relative offsets that the debugger cannot use without the load base
- Tests: three new assertions covering recFunctionScope, LowPC, and line
  info for the main program body (28 OPDF tests total, all passing)
2026-05-01 23:16:31 +01:00
Graeme Geldenhuys 7a177f4609 feat(compiler): export standalone functions and global data — Step 6b
Standalone procedures/functions now emit 'export function' in QBE IR
instead of bare 'function'. Global data items now emit 'export data'.
This makes these symbols globally visible (.globl in assembly output),
which is required for the OPDF companion file to reference them via
.quad directives at link time.

Without export, QBE emits local-only labels that the linker cannot
resolve from a separate object file (.opdf.s).
2026-05-01 22:54:15 +01:00
Graeme Geldenhuys 914ef1289f feat(compiler): OPDF debug info emitter — Step 6a
New unit uDebugOPDF.pas with TOPDFEmitter:
- EmitHeader, EmitSection: writes .opdf ELF section with OPDF magic header
- EmitPrimitive, EmitEnum, EmitRecord, EmitClass, EmitAnsiStr: type records
- EmitGlobalVar: recGlobalVar with linker-resolved .quad address
- EmitFunctionScope: recFunctionScope with LowPC/HighPC labels
- EmitParameters: recParameter for each proc/func parameter
- EmitLocalVars: recLocalVar with RBP-relative stack offsets
- EmitLineInfoForScope: recLineInfo records from AST line numbers
  (uses function-start label as address; per-stmt addr needs QBE changes)
- CollectStmtLines: recursive AST walker for unique (line, col) pairs
- FNV-1a 32-bit TypeID allocation; ScopeID counter for scope linking
- DoEmit/PatchTotalRecords: idempotent emission with TotalRecords patching

Blaise.pas pipeline:
- --debug-opdf / -g flag: creates companion .opdf.s alongside output binary
- CompileToNative passes companion file to linker
- RunProcess: add poUsePipes so TProcess.Output is valid (pre-existing crash)

Tests: cp.test.opdf.pas with 22 tests covering all record types, fields,
addresses, and line info. Validated end-to-end with opdf_dump.

{$H+} is required for FPC compilation (string = AnsiString); not needed
when compiled by Blaise itself (Blaise defaults to AnsiString).
2026-05-01 22:53:56 +01:00
Graeme Geldenhuys b573aa2d99 feat(rtl): restore TDuplicates enum in Classes unit — Step 5
TDuplicates = (dupAccept, dupIgnore, dupError) replaces the integer
constant workaround that was needed before enum type support existed.
FDuplicates field and Duplicates property are now typed TDuplicates;
the constructor initialises with dupAccept and Add() compares against
dupIgnore by name rather than the magic literal 1.

All 1035 compiler tests pass.
2026-05-01 17:48:56 +01:00
Graeme Geldenhuys 345489fec1 feat(rtl): eliminate libc printf/fprintf and most pure-C helpers — Step 9 (80%)
Write/WriteLn now call _SysWriteStr/_SysWriteInt/_SysWriteInt64/
_SysWriteNewline (blaise_sys.pas + blaise_sys_posix.c) instead of
printf/fprintf.  _SysWriteNewline uses POSIX write(2) directly;
no format strings, no libc I/O buffering.

Pure Pascal implementations replace these libc calls in the RTL:
  - toupper / tolower  (inline ASCII arithmetic in blaise_str.pas)
  - strlen             (NUL-scan loop in blaise_str.pas)
  - memcpy             (byte-copy loop in both blaise_str.pas and blaise_arc.pas)
  - memcmp             (byte-compare loop in blaise_arc.pas)

Remaining libc dependency: malloc/free (needs Pascal allocator — Step 9d).

All 1035 compiler tests pass.
2026-05-01 17:22:07 +01:00
Graeme Geldenhuys 584f0378e2 feat(rtl): migrate blaise_arc.c → blaise_arc.pas — Step 9c
Port all non-function-pointer ARC functions from C to Pascal:
  _StringAddRef, _StringRelease, _StringEquals, _StringConcat,
  TObject_Destroy, _ClassAddRef, _ClassFree

Remaining in C (blaise_arc_class.c — renamed from blaise_arc.c):
  _ClassAlloc and _ClassRelease — store and call a function pointer
  (the $_FieldCleanup_TypeName hook), which Blaise cannot express yet.

Compiler fixes required to support this port:
- uCodeGenQBE: EmitFuncDef now exits early for IsExternal declarations;
  previously it crashed (access violation) when iterating a nil body
- uSymbolTable: remove _ClassAddRef and _ClassRelease from RegisterBuiltins;
  they are internal RTL symbols emitted directly by the codegen, not
  user-callable builtins, and pre-registering them caused "Duplicate
  identifier" errors when the unit interface re-declared them

RTL Makefile: blaise_arc_class.c replaces blaise_arc.c in C_SRCS;
blaise_arc.pas added to PAS_OBJS with its own build driver.

All 1035 compiler tests pass.
2026-05-01 16:26:17 +01:00
Graeme Geldenhuys d60958dc25 feat(rtl): replace blaise_str.c with Pascal-compiled blaise_str.pas in RTL build
Remove blaise_str.c from the RTL archive and replace it with object code
compiled from blaise_str.pas via the Blaise compiler itself. The variadic
_StringFormat cannot be expressed in Pascal so it moves to blaise_str_fmt.c.

Makefile changes:
- blaise_str.c removed from C_SRCS; blaise_str_fmt.c added in its place
- New Pascal build rules: .pas → .ssa (unit IR via build driver + sed strip)
  → .s (QBE) → .o (gcc -c) → included in blaise_rtl.a
- blaise_str_build_driver.pas: minimal program driver used by the Makefile
  to compile the unit; the program section is stripped before assembly

All 1035 compiler tests pass with the new RTL.
2026-05-01 14:42:29 +01:00
Graeme Geldenhuys f68620e0b5 feat(compiler): mod operator, param assignment, PChar arithmetic, blaise_str.pas — Step 9b
- Add `mod` operator: tkMod/boMod in lexer+AST+parser, QBE `rem` in codegen
- Allow assignment to value parameters (skParameter) in semantic checker
- PChar + Integer arithmetic: semantic and codegen now accept PChar on pointer-arithmetic LHS
- PChar nil comparison: nil is now compatible with PChar in all comparison/assignment contexts
- Untyped Pointer ↔ PChar: CheckTypesMatch accepts PChar where Pointer expected and vice-versa
- Type cast widening fix: Int64(IntExpr) emits `extsw` instead of invalid `l copy w`
- CoerceArg helper: sign-extends w arguments to l at all call sites (proc/func/method)
- blaise_str.pas: Pascal port of blaise_str.c — compiles cleanly and passes runtime test

All 1035 tests pass. blaise_str driver produces correct output for IntToStr, StrToInt,
StringLength, StringUpperCase, StringLowerCase, StringTrim, StringPos, StringCopy, Chr.
2026-05-01 14:33:37 +01:00
Graeme Geldenhuys 179d2e1b50 feat(compiler): external declaration support — Step 9a
Add the 'external' directive to proc/function/method declarations,
enabling Pascal code to bind to C symbols without a body:

  procedure Foo; external;
  function Bar: Integer; external name 'c_bar';

- uLexer: tkExternal token; mapped in the identifier fast-path
- uParser: ParseMethodDecl and ParseForwardDecl consume 'external'
  and optional 'name <string>'; body-trigger check is suppressed for
  external declarations so the following 'begin' is not consumed as a
  method body
- uAST: TMethodDecl.IsExternal, TMethodDecl.ExternalName
- uSemantic: external interface decls are exempt from the
  "no implementation" check; AnalyseStandaloneDecl skips AnalyseBlock
  when IsExternal is set
- uCodeGenQBE: EmitProcCall and TFuncCallExpr codegen use ExternalName
  as the QBE symbol when set; no function body emitted for external decls
- grammar.ebnf: MethodDirective / ExternalDirective rules; MethodDecl
  and StandaloneDecl updated to use { MethodDirective SEMICOLON }

11 new tests in cp.test.external — all 1035 tests pass.
2026-05-01 13:47:24 +01:00
Graeme Geldenhuys 7ff298165b feat(compiler): full const support — method bodies, class-level, grammar
- Fix ParseMethodDecl: add tkConst to body-trigger condition so const
  blocks are parsed inside method, procedure, and function bodies
- ParseConstBlock now accepts TObjectList directly (was TBlock) so it
  can be called uniformly from all scope positions
- Class body loop: handle CONST sections (→ TClassTypeDef.ConstDecls)
  and VAR keyword before field declarations
- TClassTypeDef.ConstDecls: new owned TObjectList of TConstDecl
- TFieldAccessExpr: new IsConstant/ConstValue/ConstString fields set by
  uSemantic when TypeName.ConstName resolves to a class-level constant
- uSemantic: register class constants as both unqualified and qualified
  (TFoo.MaxItems) symbols; resolve TypeName.ConstName in field-access
  analysis before raising "Unknown class method" error
- uCodeGenQBE: emit integer copy or _StringRetain for IsConstant paths
- docs/grammar.ebnf: add ConstSection/ConstDecl/ConstExpr rules;
  update Block, InterfaceSection, ImplementationSection, ClassDef, and
  GenericClassDef to allow ConstSection

All 1024 tests pass including 15 new const-scope regression tests.
2026-05-01 13:08:26 +01:00
Graeme Geldenhuys f34cf31c17 test(constants): add regression tests for const in all scopes
10 tests covering integer consts, negative consts, string consts,
multi-const blocks, two const blocks in one scope, unit interface
const block parsing, unit implementation const block parsing, and
cross-unit export visibility (interface const visible in importing
program via AnalyseUnitForExport).

All pre-existing 1009 tests continue to pass (1019 total).
Updates future-improvements.adoc: const support is complete.
2026-05-01 12:47:04 +01:00
Graeme Geldenhuys 6be78b2a06 feat(plugin): add pasbuild-blaise-compile plugin
Standalone pasbuild plugin (phase: none) that builds the Blaise compiler
using the Blaise bootstrap binary rather than FPC:

  pasbuild blaise-compile

Steps: RTL make+install → emit IR via bootstrap binary → qbe assemble
→ gcc link → compiler/target/blaise

Bootstrap binary resolved from BLAISE_BOOTSTRAP env var or the newest
releases/vX.Y.Z/blaise binary found in the project tree.
2026-05-01 11:18:36 +01:00
Graeme Geldenhuys 530f5ba827 chore: begin v0.4.0-dev cycle 2026-05-01 10:54:28 +01:00
Graeme Geldenhuys 8207139a15 release: v0.3.0
Blaise is now fully self-hosting on the true multi-file source.
Fixpoint confirmed: releases/v0.3.0/blaise (Blaise-compiled) recompiles
Blaise.pas and produces byte-identical IR to the FPC-built stage-1.
2026-05-01 10:53:09 +01:00
Graeme Geldenhuys a070727348 docs: correct PasBuild Blaise backend entry — --fpc rename only
Blaise already handles FPC-style invocations (IsFPCStyleInvocation,
HandleFPCInfoQuery, ParseFPCArgs), so pasbuild --fpc releases/v0.3.0/blaise
works today. The only remaining improvement is renaming --fpc to --compiler
in PasBuild's CLI for clarity.
2026-05-01 10:42:06 +01:00