Commit graph

211 commits

Author SHA1 Message Date
Graeme Geldenhuys 079a06ea82 feat(lang): switch to 0-based string indexing throughout compiler and RTL
Blaise strings are now 0-based: S[0] is the first character, Pos returns
a 0-based index (-1 = not found), Copy takes a 0-based From argument.

Compiler changes:
- Add uStrCompat.pas bootstrap shim with StrAt, StrHead, StrCopyFrom,
  StrCopyTail, StrPos — thin wrappers that translate between FPC's
  1-based and Blaise's 0-based conventions
- Convert all string operations in Blaise.pas, uLexer.pas, uCodeGenQBE.pas,
  and uSemantic.pas to use the 0-based shims
- Add PosOrd/PosSubstr shims in uPasTokeniser.pas to keep its internal
  1-based FPos convention while translating at the boundary
- Fix UpCase codegen to extract ordinal via OrdAt when argument is a string
- Fix vtable emission in AppendUnit to use StrAt/StrCopyTail instead of
  1-based E.ImplName[1] and Copy

ARC fixes uncovered during self-hosting:
- Add string param AddRef/Release to EmitMethodDef (was only on EmitFuncDef)
- Add string and class ARC to var/out parameter assignment path

RTL changes:
- _StringPos, _StringPosEx: return 0-based index, -1 for not found
- _StringCopy, _StringDelete: accept 0-based From/Idx
- _OrdAt: accept 0-based index
- SplitIntoList in classes.pas: convert to 0-based loop

Tests: add coverage for method string param ARC, var-param string ARC,
constructor prefix matching (CreateFmt), pointer type aliases, metaclass
aliases. Update E2E tests for 0-based Copy/Pos semantics.

Docs: add 0-based string rationale to language-rationale.adoc and
migration analyser checklist to future-improvements.adoc.

Self-hosting fixpoint verified (114758 lines, stage-2 == stage-3).
2026-05-11 17:50:35 +01:00
Graeme Geldenhuys 024b3d86a0 fix(semantic): TFoo = class now correctly sets TObject as implicit parent
A class declared without an explicit parent (TFoo = class) was copying
TObject's vtable slot but leaving RT.Parent nil. This caused the typeinfo
parent pointer to be zero, breaking is/as/InheritsFrom checks against
TObject and any ancestor. Set RT.Parent := TObject's descriptor in the
implicit-parent branch, making TFoo = class identical to TFoo = class(TObject).

Updated the existing typeinfo test to assert the correct parent pointer.
2026-05-11 00:20:36 +01:00
Graeme Geldenhuys 41a94751cb feat(codegen+rtl): emit nil-guard before method dispatch on class variables
Calling a method on an uninitialised (nil) class variable silently
succeeded when the method body didn't dereference Self. Add _CheckNil()
to the RTL (blaise_exc.c) which prints a clear error and aborts if the
pointer is nil. Codegen now emits call $_CheckNil(l %self) immediately
after loading the object pointer for every explicit variable method call.

One new codegen test verifies _CheckNil appears in the IR.
2026-05-11 00:13:38 +01:00
Graeme Geldenhuys 1b73a82f27 feat(semantic): allow class(IFoo) syntax — interface as implicit first parent
When a class declaration lists only interface names in its parent list
(e.g. TFoo = class(IFoo)), the first name was incorrectly rejected with
'Unknown parent class'. Fix: if the first name in class(...) resolves to
an interface type rather than a class, move it to ImplementsNames and
clear ParentName, allowing the existing implicit-TObject logic to fire.

Two new semantic tests cover the fix.
2026-05-11 00:05:51 +01:00
Graeme Geldenhuys bc9434f54b feat(parser): replace raw token ordinals with human-readable names in parse errors
Add TokenKindName() to uLexer, mapping every TTokenKind to a readable
string (e.g. ';', '.', 'begin', 'identifier'). Update TParser.Expect to
use it — errors now read "Expected ';' but got 'var'" instead of
"Expected token 78 but got 79". The actual source value is appended only
when it differs from the kind name (e.g. 'identifier' ('Foo')).

Add cp.test.tokenkindname with 105 tests: a max-ordinal guard on tkAt=82
that fails if a new kind is added without updating TokenKindName, two
completeness sweeps, and a spot-check per token kind.
2026-05-10 23:52:41 +01:00
Graeme Geldenhuys e3732807fd docs: expand concurrency section with ARC + threading design
Add detailed design considerations for TThread under Blaise's automatic
reference counting: the fire-and-forget safety problem and the recommended
self-referential threading solution. Include practical code examples,
async/await vs TThread comparison table, state-machine explanations for
async/await transformation, and effort estimates for implementation.
2026-05-10 23:36:05 +01:00
Graeme Geldenhuys 5504c4a802 perf(codegen): replace TStringList IR output with TIRBuffer byte buffer
gprof showed that 99.95% of stage-2 time was in MemCopy (via _StringFormat),
called 186,116 times from EmitLine(Format(...)).  The previous fix replaced the
byte-loop with libc memcpy (2.5x speedup).  This change eliminates the
_StringFormat calls entirely.

Replace FOutput: TStringList with TIRBuffer — a plain growable byte buffer.
EmitLine now appends raw bytes with a direct memcpy, bypassing all string
allocation and reference counting.  AppendBuffer bulk-copies sub-buffers in
one call, replacing the line-by-line TStringList.AddStrings pattern.  GetOutput
returns FOutput.Text which does a single SetLength + memcpy.

Portability:
- FPC: uses Move() via {$IFDEF FPC} guards in BufCopy.
- Blaise: uses _ir_memcpy (external 'memcpy') since Move is not yet a Blaise
  built-in; {$linklib c} ensures the symbol is available at link time.

Result:
  stage-2 self-hosting compile time: 1m 20s -> 0.40s  (200x speedup)
  Combined with the MemCopy RTL fix:  3m 18s -> 0.40s  (490x total)

FIXPOINT_OK confirmed. All 1275 tests pass.
2026-05-09 13:20:55 +01:00
Graeme Geldenhuys 8191e58bf2 perf(rtl): replace byte-loop MemCopy/MemCompare with libc memcpy/memcmp
gprof profiling of the stage-2 self-hosted binary revealed that 99.95%
of execution time was in MemCopy, called via _StringFormat from every
EmitLine(Format(...)) call in the codegen (186,116 calls per compile).

Both blaise_str.pas and blaise_arc.pas had hand-rolled byte-copy loops:

  for I := 0 to N - 1 do D[I] := S[I];

Replace with external bindings to libc memcpy/memcmp, which use SIMD
instructions and are orders of magnitude faster for typical string sizes.

Result: stage-2 self-hosting compile time drops from ~3m 18s to ~1m 20s
— a 2.5× speedup with a 4-line change.

FIXPOINT_OK confirmed after this change.
2026-05-09 12:54:37 +01:00
Graeme Geldenhuys e558965a78 feat(codegen): mem2reg — promote scalar locals to QBE SSA temporaries
Replace the alloc+load+store pattern with direct QBE temp reuse for
scalar local variables.  QBE supports non-SSA IR natively: assigning
the same temp name multiple times is valid and QBE inserts phi nodes
internally.

Promoted types: Integer, UInt32, Boolean, Byte, Enum, Int64, Double,
Single, Set.  Excluded (address-taken or ARC-complex): tyClass,
tyString, tyPointer, tyPChar, tyMetaClass — these require a loadl from
the slot in ~30 call sites that are not yet promotion-aware.

Pre-pass per function:
- BlockHasTry: suppresses promotion entirely when the block contains
  try/finally or try/except — promoted temps live in registers and do
  not survive setjmp/longjmp used by the exception frame.
- CollectAddressTaken: marks locals whose address is taken (@X or
  passed as var-arg) as non-promotable.

Updated codegen sites:
- EmitVarAllocs: emits '%_var_X =QT copy 0' instead of alloc+store.
- EmitAssignment: writes use '%_var_X =QT copy val' instead of store*.
- EmitArcCleanup: reads use 'copy %_var_X' instead of loadl.
- EmitExpr (TIdentExpr): reads use 'copy %_var_X' instead of load*.
- EmitForStmt: loop variable init/test/step use copy instead of
  load/store.
- EmitProcCall (Inc/Dec): in-place add/sub uses copy instead of
  load/store for promoted locals.
- Return value: 'ret %_var_Result' uses copy instead of loadw/loadl.

Fixpoint: stage-2 IR = stage-3 IR (FIXPOINT_OK).
All 1275 tests pass.
2026-05-09 11:49:56 +01:00
Graeme Geldenhuys f2e78bd357 fix(semantic): normalise identifier casing to declared symbol name
Symbol table lookup is case-insensitive, but the AST node retained the
as-typed casing from source. The code generator used that casing to emit
QBE temp and global names, producing mismatched names (e.g. %_var_result
vs %_var_Result) that QBE rejected with "invalid type for second operand".

Fix: after a successful Lookup, copy Sym.Name back onto the AST node for
both TAssignment (AAssign.Name) and TIdentExpr (TIdentExpr(AExpr).Name).

Add two regression tests in cp.test.codegen:
- TestResult_LowercaseAssign_CompilesOK: 'result := x + y' compiles cleanly
- TestIdent_WrongCase_NormalisedInIR: 'n := 42' uses the declared $N global

Reported by the community: https://github.com/graemeg/blaise/discussions/15
2026-05-09 10:03:30 +01:00
Graeme Geldenhuys 9643911c2e fix(semantic): integer literal scores exact match only against integer types
An integer literal constant (untyped) should score 2 (exact) against
integer-family types (Integer, Int64, UInt32, Byte) and score 1 (widening)
against floating-point types (Double, Single).

Previously the literal scored 2 against *any* numeric type, so F(42) with
overloads F(Int64) and F(Double) was incorrectly flagged as ambiguous —
both scored 2. Now Int64 wins over Double as expected.

Update SrcAmbiguousOverload in cp.test.overload.pas to use Double vs Single
(two float overloads) which remain equally-scored from an integer literal,
preserving the ambiguity test intent.
2026-05-09 09:59:09 +01:00
Graeme Geldenhuys 7e52677781 fix: QBE has no truncl; assigning an l value to a w temp implicitly truncates. 2026-05-09 09:51:51 +01:00
Graeme Geldenhuys 6207bb9761 feat: compiler and RTL improvements from bcl.testing migration work
Compiler changes:
- uSemantic: SemanticError now includes unit/program name for easier debugging
- uSemantic: implicit-self method calls use ResolveMethodOverload so overloaded
  methods (e.g. AssertEquals variants) resolve to the correct overload
- uSemantic: integer literals score exact match against any numeric type in
  overload resolution; secondary tiebreaker on exact-match count breaks ties
- uSemantic: PosEx, GetCurrentDir, ExtractFileDir, ExcludeTrailingPathDelimiter,
  GetTempFileName builtins added
- uSymbolTable: register PosEx, GetCurrentDir, GetTempFileName,
  ExcludeTrailingPathDelimiter, ExtractFileDir, RemoveDir symbols
- uCodeGenQBE: codegen for PosEx, GetCurrentDir, ExtractFileDir,
  ExcludeTrailingPathDelimiter, GetTempFileName, ForceDirectories (stmt), RemoveDir
- uParser: fix nil-statement bug in except handler — bare "on E: T do ;" added nil
  to the handler statement list, causing a codegen crash on empty except clauses
- bcl.testing: add Ignore/EIgnoredTest support; TTestResult tracks ignored count

RTL additions:
- blaise_str.pas: _StringPosEx (3-arg Pos with start offset)
- blaise_io.c: _GetCurrentDir, _ExtractFileDir, _ExcludeTrailingPathDelimiter,
  _RemoveDir, _GetTempFileName
2026-05-09 09:51:41 +01:00
Graeme Geldenhuys a1a2f1fa71 test(parser): add chained postfix access tests
Four tests covering Obj.Method().Field, Arr[i].Field,
A.GetB().GetC().Val, and GetList()[0] patterns.
2026-05-08 08:56:30 +01:00
Graeme Geldenhuys 3476f6c9a4 feat(parser): add postfix chaining loop for Expr.Field, Expr[i], Expr^
Adds a unified postfix chaining loop at the end of ParseFactor that
handles .Field, .Method(...), [subscript], and ^ dereference after any
expression. Previously Ident.Method(...) and Expr[i] exited without
checking for further postfix access, so patterns like Obj.GetRec().X,
Tokens[0].Kind, and A.GetB().GetC().Val failed to parse.
2026-05-08 08:56:20 +01:00
Graeme Geldenhuys d8c0ef4ef6 fix: we don't have any code for migration-analyser yet. Disable tests for it. 2026-05-08 00:52:44 +01:00
Graeme Geldenhuys eacaf1e192 fix(compiler): restore poUsePipes for FPC TProcess in RunProcess
The poUsePipes option was removed during self-hosting adaptation since
the Blaise RTL TProcess always uses pipes. FPC's TProcess requires
the option explicitly, causing an access violation on Output.Read when
compiling with --output.
2026-05-08 00:33:50 +01:00
Graeme Geldenhuys da857ecc8e refactor(e2e): remove TStringStream from RunProc, replace sLineBreak with #10
Simplify RunProc() output capture: accumulate pipe bytes directly into a
string via SetString instead of routing through TStringStream.  Replace
sLineBreak with #10 in error messages to reduce SysUtils dependencies
ahead of the Blaise test-suite migration.
2026-05-07 23:55:13 +01:00
Graeme Geldenhuys 3e4d591c1d chore(doc): PasBuild rename --fpc to --compiler [already done] 2026-05-07 23:25:19 +01:00
Graeme Geldenhuys 06882bf432 feat(compiler,rtl): add OS utility builtins for test suite migration
Add GetProcessID, DirectoryExists, GetTempDir, ForceDirectories, and
Sleep as compiler builtins backed by C RTL functions. These are needed
by the e2e test infrastructure (cp.test.e2e.pas) before switching the
test suite from FPC to Blaise compilation (v0.6.0 Step 2a).
2026-05-07 23:19:52 +01:00
Graeme Geldenhuys 9300fe8a46 chore(doc): new future improvement - If Conditional Operator (ternary operator) 2026-05-07 23:19:03 +01:00
Graeme Geldenhuys db7660a95e chore(docs): Remove Multi-Line string literal from future-improvements
It already landed in the compiler.
2026-05-07 23:18:24 +01:00
Graeme Geldenhuys 11f657caef feat(lexer): implement triple single-quote text blocks
Add '''...''' multi-line string literal syntax to the lexer and
tokeniser.  Opening ''' followed by a newline starts a text block;
closing ''' on its own line sets the indentation baseline for margin
stripping.  Single quotes inside the block require no escaping.

Disambiguation: ''' followed by a newline opens a text block; followed
by any other character falls back to the classic '' escape parse.

11 new tests in cp.test.textblock.pas cover basic blocks, margin
stripping, embedded quotes, empty blocks, relative indentation
preservation, disambiguation from '''', trailing content, tabs,
and blank line preservation.  All 1264 tests pass; fixpoint verified.
2026-05-07 19:52:23 +01:00
Graeme Geldenhuys 8f0a844d07 feat(compiler,rtl): implement TObject.InheritsFrom
Add _InheritsFrom RTL helper in blaise_arc.pas that walks the typeinfo
parent chain to check class identity. Wire IsBuiltinInheritsFrom through
the semantic analyser (tyPointer, tyMetaClass, tyClass receivers) and
codegen (emits call $_InheritsFrom with correct typeinfo loading).

Update punit with AssertInheritsFrom, AssertInheritsFromClass, and class
identity checks in AssertException and RunTestHandler. Update testpunit2
so DoTest21 correctly fails on a class mismatch (EError expected, EFail
raised). Add 5 unit tests and 6 e2e tests; all 1253 tests pass, fixpoint
confirmed.
2026-05-07 09:53:47 +01:00
Graeme Geldenhuys f3f0036381 docs: remove implemented entries from future-improvements.adoc
Removes const sections, Abs(), ClassName/ClassType (all confirmed
implemented). Updates Str() workaround note now that DoubleToStr/
SingleToStr exist. Updates InheritsFrom effort note now that ClassType
vmt slots are in place.
2026-05-07 08:36:28 +01:00
Graeme Geldenhuys 2f28051956 chore: begin v0.6.0-dev cycle 2026-05-07 08:25:10 +01:00
Graeme Geldenhuys d2209afd0e release: v0.5.0
Fixpoint achieved on the full multi-file source (105865 lines of QBE IR).
Ships indexed-property getter codegen and string-field char byte-load, fixing
the self-hosting crash introduced by overloading support in v0.4.0.
2026-05-07 08:24:50 +01:00
Graeme Geldenhuys ded893493a fix(semantic,codegen): emit char byte-load for string field subscripts
When the parser sees Rec.Field[N] and Field is a string, it stores the
[N] subscript in TFieldAccessExpr.PropIndexExpr.  The semantic analyser
was ignoring this index and returning tyString, so Field[N] = '$' was
compiled as a string equality comparison (_StringEquals) instead of a
byte-load comparison (loadub + ceqw 36).

Add IsCharAccess flag to TFieldAccessExpr.  In AnalyseFieldAccess, when
PropIndexExpr is non-nil and the field type is a string, set IsCharAccess
and return tyInteger so that comparison with a char literal triggers the
existing char-coercion path in AnalyseBinaryExpr.

In the codegen, handle IsCharAccess by loading the string field pointer
and emitting loadub at the 0-based byte offset (N-1), matching the
existing EmitStringSubscriptExpr logic for string character access.

This fixes the vtable emission bug where E.ImplName[1] = '$' was being
compiled as _StringEquals(ImplName, "$"), causing inherited TObject vtable
slots to be emitted as _D_TObject_Destroy instead of $TObject_Destroy
when running stage-2 → stage-3.  Fixpoint now produces identical IR.
2026-05-07 08:06:06 +01:00
Graeme Geldenhuys 4061e4e249 fix(semantic): emit indexed-property getter for subscripted class fields
When the parser sees Rec.Field[I] and Field is a direct class field
(not itself a property), the semantic analyser was silently ignoring
the [I] subscript and returning the field value — causing the codegen
to emit a raw field load instead of calling the field type's indexed
property getter.

Add TRecordTypeDesc.FindIndexedProperty to locate the first indexed
read-method property on a class type.  In AnalyseFieldAccess, after
resolving a direct field, if PropIndexExpr is non-nil and the field
type is a class with an indexed property, redirect to that property's
getter and keep FieldInfo set so the codegen knows to load the field
value first before calling the getter.

Update the codegen's PropRead path to handle the combined case: when
both PropRead and FieldInfo are set, load the field from the record
first and use that loaded value as Self for the getter call.

This fixes the self-hosting crash where TES.Handlers[I] in uSemantic.pas
was passing the TObjectList itself (instead of element I) as the TypeName
string to FindTypeOrInstantiate, corrupting the argument with the list's
FCount/FCapacity word.
2026-05-07 07:49:00 +01:00
Graeme Geldenhuys b7aa65e9fb fix(codegen): pop exception frames on early control-flow exits
`exit`, `break`, and `continue` inside a `try…finally` or
`try…except` body jumped directly to the target label without
popping the exception frames they were nested inside. That left
`g_exc_top` pointing to dead stack memory, and the next push of an
unrelated frame at the same address would set `prev` to itself,
turning the chain into a self-loop. The next pop then read garbage
back into `g_exc_top`, eventually segfaulting in `_PopExcFrame`.

Track active exc-frame depth in `FExcDepth` and emit `_PopExcFrame()`
for each nesting level we're unwinding past. The loop label stack
now records the depth at loop entry via `AddObject` so `break` and
`continue` pop only the frames added inside the loop, while `exit`
unwinds back to depth 0.

Caught while debugging the self-hosting fixpoint: stage-2
`AnalyseUnitForExport` segfaulted in `_PopExcFrame` after
`ResolveMethodOverload` returned via `if TotalCnt = 0 then Exit;`
(uSemantic.pas:2500) inside its `try…finally`.
2026-05-06 17:29:44 +01:00
Graeme Geldenhuys 4dd71ba16f docs: ditch the TOC in the readme.
It looks weird on Github.
2026-05-06 13:10:56 +01:00
Graeme Geldenhuys 6af11d2a09 doc: reorder the README sections.
More technical details at the end.
2026-05-06 13:09:11 +01:00
Graeme Geldenhuys 36e16752f3 docs(readme): update README for public repo launch
- Rewrite intro with tagline and project summary
- Fix licence: BSD 3-Clause → Apache 2.0 with Runtime Library Exception
- Fix LICENSE filename reference (American spelling matches actual file)
- Update test count to 1200+
- Add status snapshot (self-hosting, test coverage, backend)
- Correct phase 4 and 5 status to Complete / In-Progress
- Add Community section linking to GitHub Discussions
- Add closing attribution line
2026-05-06 12:53:36 +01:00
Graeme Geldenhuys 5b9bc375ea fix(codegen): eliminate dynamic stack allocs — phi for short-circuit bools, hoist exc frames
Two codegen improvements that eliminate all non-@start `alloc`
instructions from the emitted QBE IR.  Both are prerequisite work for
the self-hosting fixpoint (currently being debugged).

(1) Short-circuit boolean operators now use QBE phi instead of alloc4.

Previously `A and B` / `A or B` emitted:
  %slot =l alloc4 1          <- dynamic alloc, grows stack per execution
  storew %L, %slot
  jnz %L, @rhs, @end
  @rhs  storew %R, %slot  jmp @end
  @end  %T =w loadw %slot

The slot was allocated in whichever basic block the expression appeared
in.  QBE emits a runtime `sub $16, %rsp` for every alloc outside @start
— inside a while-condition this ran on every loop iteration, growing the
stack by 16 bytes per iteration indefinitely.

New form uses a phi node at the join block:
  jnz %L, @rhs, @end
  @rhs  (evaluate R)  jmp @end
  @end  %T =w phi @lhs %L, @rhs %R

No alloc needed; %T is always well-defined at @end by SSA.
FCurrentBlockLabel tracks the current basic block name so the phi can
correctly name its predecessors.

(2) Exception frames pre-allocated at @start via EmitExcFrameAllocs.

Previously EmitTryFinallyStmt / EmitTryExceptStmt emitted `alloc16 512`
inline at the point of the try statement.  Inside a loop (e.g. `for I
:= 0 to N-1 do begin try ... finally ... end end`), each iteration
allocated 512 bytes of stack that was never released until the function
returned.  Over hundreds of iterations this drifted the stack pointer
into parent exception frame territory, corrupting the exc-frame chain.

New approach: CountTryStmts recursively counts all try blocks in the
function body, EmitExcFrameAllocs pre-allocates %_exc_frame_0 …
%_exc_frame_N at @start (QBE hoists these to the static prologue sub),
and EmitTryFinallyStmt/Except index into the pool via FExcFrameNext.

Result: zero alloc instructions outside @start blocks in the emitted IR.

All 1242 tests pass.  Stage-1 IR and stage-2 IR are now byte-for-byte
identical (md5 verified) — the fixpoint is correct at the IR level.
The stage-2 BINARY still crashes during stage-3 generation; this is
under active investigation and may be a QBE vs FPC code-generation
quality issue for the same IR.
2026-05-06 11:47:52 +01:00
Graeme Geldenhuys 9613135345 feat(lang): case-on-string selector
Step 11f prerequisite.  The 'case' statement now accepts a string-
typed selector.  Each branch label must also resolve to a string
expression (typically a string literal); the runtime comparison
lowers to a call to _StringEquals against the selector value, so
existing string-equality semantics apply unchanged.

[source,pascal]
----
case Tag of
  'w': StoreInstr := 'storew';
  'd': StoreInstr := 'stored';
  's': StoreInstr := 'stores';
else   StoreInstr := 'storel';
end;
----

This unblocks self-hosted compilation of uCodeGenQBE.pas, which
already uses case-on-QType in four sites.  The internal motivation
matters: forcing those sites into hand-rolled if-elseif chains just
to make the compiler compilable by itself was rejected as a step
backwards from idiomatic Object Pascal.

Implementation:
  * uAST:  TCaseStmt gains an IsStringCase: Boolean flag set by the
            analyser so codegen can pick the right comparison.
  * uSem:  AnalyseCaseStmt now allows tyString selectors in addition
            to ordinal types; per-branch CheckTypesMatch already does
            the right thing for string vs string.
  * uCG:   EmitCaseStmt emits a 'call $_StringEquals(l SelTemp, l
            ValTemp)' result and reuses the existing 'jnz' fall-
            through pattern.  Ordinal cases still use 'ceqw'.

Tests added to cp.test.caseenum.pas:
  TestSemantic_CaseString_AcceptsStringSelector
  TestSemantic_CaseString_RejectsIntLabelOnStringSelector
  TestCodegen_CaseString_EmitsStringEqualsCalls
  TestCodegen_CaseString_OrdinalCaseStillUsesCEQW

Rationale documented in docs/language-rationale.adoc.

All 1242 tests pass (1238 + 4 new).
2026-05-06 08:00:00 +01:00
Graeme Geldenhuys 630082f36f feat(rtl): bcl.testing.runner.text — plain-text xUnit runner (Step 11e)
The fourth and final logical step of Step 11e.  New unit at
compiler/src/main/pascal/bcl.testing.runner.text.pas drives the
bcl.testing framework end-to-end:

  * PublishedMethodCount / PublishedMethodName walk the class's
    typeinfo[3] published-method table inline, climbing the parent
    chain so inherited test methods are visible.  Same loop shape as
    blaise_arc's _MethodAddress.

  * RunRegisteredTests iterates the GRegistry list of TTestCaseClass
    values that bcl.testing.RegisterTest collected, ClassCreate's one
    instance per published method (with the method name as the
    constructor arg), and dispatches via the standard TTestCase.Run
    path landed in 11d.

  * PrintSummary writes the OK / FAIL header line, plus per-failure
    and per-error detail.

  * RunAll wraps the two: program ends with 'Halt(RunAll)' for a
    clean shell-driven exit code (0 = all green, 1 = anything else).

ARC: TTestCase instances are released at loop-iteration scope end;
no explicit Free call.  This matches the behaviour the user noted —
Blaise's full ARC makes Free unnecessary for short-lived locals.

testbcl.pas rewritten to be the canonical user shape:
RegisterTest(TSampleTests) + Halt(RunAll).  The previous manual
two-instance dance is gone.  Verified scenarios:

  * Mixed pass/fail   — prints failures + exit 1
  * All green         — prints 'OK (N tests)' + exit 0
  * Inherited methods — TChildTests sees TBaseTests's published
                        methods plus its own

All 1238 compiler tests still pass.

Filtering (--suite / --test) is intentionally deferred — once Step
11f rewires the cp.test.* uses-clauses, the same RunAll entry point
serves TestRunner.pas with ~50 fixtures, at which point ParamStr-
driven filters become more useful.
2026-05-06 07:45:50 +01:00
Graeme Geldenhuys 30753f818d docs(lang): grammar + rationale for ClassCreate (Step 11e)
Document the new ClassCreate builtin landed in 1fb2648:

  * grammar.ebnf — add ClassCreate and MethodAddress to the built-in
    functions reference table.
  * language-rationale.adoc — new section describing the decision
    (split RTL helper + static constructor call), why a per-class
    factory pointer was rejected, why metaclass.Create(args) wasn't
    made a method-call shape, and why we don't introduce a virtual
    constructor slot.
2026-05-06 07:37:18 +01:00
Graeme Geldenhuys 1fb2648f66 feat(lang): ClassCreate(Cls, ...args) builtin for runtime construction
Step 11e, part 3.  New compiler builtin that constructs an instance
from a metaclass value at runtime.  Lowers to:

  %p =l call $_ClassCreate(l <classvalue>)
  call $<BaseClass>_Create(l %p, args...)
  result = %p

The first argument must be tyMetaClass; remaining args are forwarded
to the resolved constructor.  Result type is the metaclass's BaseClass,
so 'F := ClassCreate(C, 7)' is well-typed when C: class of TFoo and
F: TFoo.  The constructor lookup is currently by name 'Create' (no
overload resolution yet — fpcunit's Create(string) and TObject's
parameter-less Create are the only shapes the bcl.testing runner
needs).  When the resolved class declares no Create method, only
the alloc call is emitted; fields stay in their _ClassAlloc-zeroed
default state.

This is the missing half that makes 'class of TTestCase' useful: the
runner (Step 11e, part 4) will iterate the published-method table
of each registered test class and call ClassCreate(Cls, MethodName)
to instantiate one TTestCase per test method, exactly the shape
fpcunit's class-method 'Suite' produces.

Symbol table: 'ClassCreate' added as skFunction in uSymbolTable.pas
              (return type filled in semantically).
Semantic   : AnalyseFuncCallExpr handles SameText('ClassCreate') —
              validates first arg is metaclass, analyses remaining
              args, resolves Create on BaseClass, stores it on
              ResolvedDecl, returns BaseClass as result type.
Codegen    : EmitExpr handles SameText('ClassCreate') — emits the
              two-call sequence above; reuses the ConstructorCall
              arg-coercion pattern (CoerceArg).

Three regression tests added to cp.test.classof.pas:
  TestSemantic_ClassCreate_RejectsNonMetaclassFirstArg
  TestCodegen_ClassCreate_EmitsAllocAndCtorCall
  TestCodegen_ClassCreate_NoCtor_OnlyAllocCalled

Verified end-to-end: ClassCreate(TCounter, 100); .Bump; .Bump;
prints 102 — argument flowed through the constructor, the instance
runs methods, and Free cleans up correctly.

All 1238 tests pass (1235 prior + 3 new).
2026-05-06 07:36:00 +01:00
Graeme Geldenhuys 113baa71f3 feat(rtl): add _ClassCreate(TInfo) helper
Step 11e, part 2.  New function in blaise_arc.pas that performs at
runtime what EmitConstructorCall does inline for the static
'TFoo.Create' form: read totalsize / fieldcleanup / vtable from the
expanded typeinfo (slots 4-6, landed in 58ff2a4), call _ClassAlloc,
install the vtable pointer at instance[0], bump the refcount once,
return the user pointer.

Caller responsibility: a metaclass value (the typeinfo pointer for
'class of T') is passed in; the caller subsequently invokes the
constructor via a static $T_Create call against the returned pointer.
This split keeps _ClassCreate independent of any specific
constructor signature; the upcoming ClassCreate compiler builtin
will glue the two halves together.

Verified end-to-end via a hand-written program that calls the helper
through 'class of TFoo' typecast and exercises the resulting
instance through both a non-virtual method and Free / Destroy.
All 1235 compiler tests still pass; testbcl smoke green.
2026-05-06 01:00:22 +01:00
Graeme Geldenhuys 6a41ec1c5e fix(self-hosting): parser cast-chain indexed prop + drop TList in uSemantic
Two incidental fixes uncovered while running the self-hosting fixpoint
check after the typeinfo expansion in 58ff2a4.  Neither blocks any
test that uses fpcunit (FPC has both already), but both block
Blaise compiling its own source after the 11d strict-PropIndexExpr
analyser change.

(1) uParser.pas:

The 'FuncOrCast(...).A.B.C' chain branch in ParseAtom never picked up
'[idx]' after each chained member, leaving indexed-property writes of
the form 'TClassTypeDef(TD.Def).Properties.Items[J]' to fall through
to TStringSubscriptExpr — which the analyser rewrites into a
PropIndexExpr-less FieldAccess and now (post-11d) flags as 'Indexed
property requires an index expression'.

The fix mirrors the pattern already in the regular A.B.C chain branch:
after each '.IDENT', check for '[' and attach the parsed index to the
just-built FieldAccess as PropIndexExpr.

(2) uSemantic.pas:

Two ResolveOverload helpers used Classes.TList for a temporary
TMethodDecl candidate list.  TList is FPC-only (Blaise's RTL
Classes unit only declares TStringList).  Swap to TObjectList(False)
from Contnrs (which Blaise both vendors and uSemantic already
imports) and switch [K] indexing to .Items[K] (Blaise has no default
indexed-property directive yet).

Both changes are net-neutral under FPC and unblock self-hosted
compilation of uParser/uSemantic on Blaise.
2026-05-06 00:58:22 +01:00
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