Commit graph

152 commits

Author SHA1 Message Date
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
Graeme Geldenhuys 2985ff23ef docs: add Tooling section — fptest port and PasBuild Blaise backend
Two new future-improvement entries covering the remaining FPC dependencies
in the development cycle:

- Native Blaise test framework: port fptest (DUnit2-based) to pure Blaise
  so that cp.test.*.pas units compile and run without FPC; eliminates the
  last FPC dependency from pasbuild test
- PasBuild Blaise compiler backend: rename --fpc to --compiler, add backend
  detection and Blaise-style command-line construction so that
  pasbuild compile --compiler releases/v0.3.0/blaise works correctly
2026-05-01 10:40:08 +01:00
Graeme Geldenhuys 73cfddeedd docs: document Step 10/11 deferred items and fixpoint rationale
- language-rationale.adoc: update MaxInt entry — now Integer = 2147483647
  (32-bit) with full rationale explaining the Int64 truncation chain;
  update IntToStr table to note auto-routing to _Int64ToStr for Int64 args
- future-improvements.adoc: add two new entries
  * "Int64 literal range detection in self-hosted binary" — root cause of
    MaxInt workaround; ConstValueInt64 path to restore full Int64 support
  * "const sections in unit interface and implementation blocks" — deferred
    feature blocking dupAccept/dupIgnore/dupError in classes.pas public API
2026-05-01 09:38:07 +01:00
Graeme Geldenhuys 5ec85211bd feat(compiler+rtl): multi-file self-hosting fixpoint — Step 11
Fixes required to reach byte-identical IR between stage-1 and stage-2
when compiling the multi-file Blaise.pas source.

Compiler fixes:
- uCodeGenQBE.pas: QbeEscapeString — replace Format('%02x',[C]) with
  manual hex encoding (Hi := C shr 4; Lo := C and 15; Chr(...));
  Blaise's _StringFormat does not support %x, causing non-ASCII bytes
  (e.g. UTF-8 em-dash U+2014) to be emitted as \%02x instead of \E2\80\94
- uCodeGenQBE.pas: EmitFuncCallExpr — route IntToStr to _Int64ToStr when
  the argument resolves to Int64 (QBE type 'l'), matching FPC's overloaded
  IntToStr resolution; prevents 64-bit values being truncated to Int32
  before string conversion
- uSymbolTable.pas: MaxInt changed from Int64 (9223372036854775807) to
  Integer (2147483647); all uses are Copy(S,N,MaxInt) meaning "rest of
  string", and 2147483647 is handled correctly by _StringCopy; avoids
  Int64 literal truncation/extsw issues in the self-hosted binary

RTL fixes:
- blaise_str.c: _StringCopy — negative count (64-bit MaxInt truncated to
  int32 = -1) now treated as "rest of string" rather than 0
- blaise_str.c: add _UpCase helper
- classes.pas: TStringList.GetText — add trailing #10 to match Delphi/FPC
  TStringList.Text semantics (each line including the last ends with LF);
  SaveToFile updated to not double-add the trailing newline

Tests: 1009 pass
Fixpoint: diff /tmp/multifile.ssa /tmp/multifile2.ssa → empty
2026-05-01 09:26:39 +01:00
Graeme Geldenhuys d20970f27b feat(compiler+rtl): multi-file self-hosting pipeline — Step 10
Fixes required to get the multi-file compiler binary (blaise-multi)
to compile Blaise.pas without crashing.

RTL fixes:
- contnrs.pas: TObjectList ARC — Add/Put/Delete/Clear/Destroy now call
  _ClassAddRef/_ClassRelease so stored objects survive beyond their
  creating scope; fixes premature-free of TTypeDecl nodes during
  AnalyseUnitForExport
- blaise_str.c: _StringCopy — treat negative count as "rest of string"
  instead of 0; 64-bit MaxInt (0x7fffffffffffffff) is truncated to -1
  when passed as a 32-bit w argument to _StringCopy, which was causing
  Copy(S, 2, MaxInt) to return empty string in FindTypeOrInstantiate
- blaise_str.c: add _UpCase C helper
- blaise_arc.c: minor ARC fixes

Compiler fixes:
- uAST.pas: add IsVarParam flag to TMethodCallStmt and TMethodCallExpr
- uSemantic.pas: set IsVarParam when object symbol is skVarParameter
- uCodeGenQBE.pas: emit double-dereference for method calls on var/out
  parameters — the local slot holds the caller's address, not the
  object pointer, so one extra loadl is needed to reach the receiver
- uSymbolTable.pas, uUnitLoader.pas, uParser.pas, uLexer.pas: various
  fixes discovered during multi-file pipeline testing
- Blaise.pas: improved error reporting

Tests: 1009 pass
2026-05-01 06:52:48 +01:00
Graeme Geldenhuys 53235aa6b6 feat(compiler): self-hosting fixpoint reached — Step 10
Parser, lexer, tokeniser, semantic analyser and symbol table extended
to compile the multi-file Blaise source with the Blaise compiler itself.
Stage-2 and stage-3 IR are byte-identical; fixpoint confirmed.

Changes to reach fixpoint:
- uLexer: source filename parameter for actionable parse error messages
- uParser: method directive loop (inline/stdcall/abstract/…), unit
  interface/implementation loops for var/const/type blocks, chained
  field-assignment (A.B.C := v), filename in all EParseError messages
- uPasTokeniser: full rewrite to remove FPC-only features (char sets,
  shr, Exit(value), private/public visibility) so Blaise can compile it
- uSymbolTable: remove unsupported default parameter values
- uSemantic: replace Exit(value) with Result+Exit; expand five `with`
  statement blocks to explicit typed local variables
- uAST, uCodeGenQBE: minor additions supporting the above
- uUnitLoader: pass filename to TLexer.Create
- classes.pas: inline dupIgnore literal (const section deferred)
2026-04-30 13:30:30 +01:00
Graeme Geldenhuys 648d580fbf feat(compiler): remove SysUtils/Classes/Contnrs/Process/StrUtils from FpcRtlUnits skip list — Step 9
These units are now provided by the Blaise RTL, so the unit loader
should resolve and compile them rather than silently treating them
as built-in FPC units.
2026-04-30 11:29:46 +01:00
Graeme Geldenhuys e568ce21f2 feat(rtl+compiler): Process.pas + C helper — Step 8
Add process management infrastructure for multi-file self-hosting:

rtl/src/main/c/blaise_process.c (new)
  - BlaiseProcess struct with per-handle exe, argv, pipe fd, pid
  - _ProcessCreate / _ProcessFree: alloc/dealloc
  - _ProcessSetExe / _ProcessAddArg: configure before Execute
  - _ProcessExecute: fork+exec with stdout+stderr piped
  - _ProcessRunning: WNOHANG waitpid check
  - _ProcessReadOutput: blocking read, returns '' on EOF
  - _ProcessWaitOnExit / _ProcessExitCode: reap child
  - Windows stubs via #ifdef _WIN32

rtl/src/main/pascal/Process.pas (new)
  - TProcess class backed by the above C helpers via built-ins
  - Execute, ReadOutput, WaitOnExit, Running, ExitCode, Parameters

compiler (uSymbolTable / uSemantic / uCodeGenQBE)
  - 9 new process built-ins: ProcessCreate, ProcessSetExe,
    ProcessAddArg, ProcessExecute, ProcessRunning, ProcessReadOutput,
    ProcessWaitOnExit, ProcessExitCode, ProcessFree

compiler/src/main/pascal/Blaise.pas
  - RunProcess: open-array → TStringList; loop uses ReadProcessChunk
    wrapper (FPC-compatible); call sites rebuilt with explicit TStringList
  - ReadProcessChunk wraps FPC's Output.Read for stage-1; will be
    replaced by Proc.ReadOutput at Step 10

Tests
  - cp.test.process.pas: 18 semantic + codegen tests for all built-ins
  - cp.test.e2e.pas: TestRun_ProcessBuiltins_CapturesOutput,
    TestRun_ProcessBuiltins_ExitCode (verify fork/exec/pipe at runtime)
2026-04-29 18:31:55 +01:00
Graeme Geldenhuys 62a803bb88 docs: clean up multi-line string literals section in future-improvements
Add Options D (triple single-quote) and E (keyword heredoc) with
before/after visual examples matching the format of Options A–C.
Remove conversational draft text and duplicate implementation notes
that were left from an earlier session.
Each option now shows the current concatenation form alongside the
proposed syntax for direct readability comparison.
2026-04-29 16:54:58 +01:00
Graeme Geldenhuys aa5ff0495b feat(compiler): replace CreateFmt with Create(Format(...)) — Step 7
Replace all 83 .CreateFmt(...) calls across 5 compiler unit files with
.Create(Format(...)) to eliminate dependency on Exception.CreateFmt
(which requires array-of-const / TVarRec, unsupported in Blaise):

  uLexer.pas:       2 replacements
  uParser.pas:      61 replacements
  uSemantic.pas:    8 replacements
  uCodeGenQBE.pas:  10 replacements
  uUnitLoader.pas:  2 replacements

The Format call retains FPC-compatible [args] array notation so the
multi-file source continues to compile under FPC.  The hand source
(blaise-compiler.pas) already uses Format without array brackets and
is unaffected.

All 975 tests pass.  Fixpoint verified.

Also: docs/future-improvements.adoc — multi-line string literals section
expanded with visual before/after examples for each candidate syntax
(heredoc, backtick, triple-brace).
2026-04-29 16:12:52 +01:00
Graeme Geldenhuys 2f7f551925 feat(rtl): sysutils.pas (Exception class) and strutils.pas (empty stub)
Steps 5 and 6 of v0.3.0 multi-file self-hosting:

- rtl/src/main/pascal/sysutils.pas: Exception base class with
  Create(AMessage: string) constructor and Message property.
- rtl/src/main/pascal/strutils.pas: empty stub unit satisfying
  the unit loader for `uses StrUtils` without any API calls.

To parse these RTL units, the compiler now recognises `constructor`
and `destructor` as reserved keywords (uLexer.pas, uParser.pas),
treating both as aliases for `procedure` in method declarations.
The constructor nature of a call is determined at the call site, not
from the declaration keyword — matching the existing codegen model.

Tests (cp.test.exceptions.pas):
  - TestSemantic_ExceptionSubclass_CreateAndMessage_OK
  - TestCodegen_ExceptionSubclass_CtorCallWithMessage

Hand source (tests/blaise-compiler.pas) synced with all changes.
Fixpoint verified (stage-2 IR == stage-3 IR).

docs/language-rationale.adoc: Constructor/Destructor Keywords section.
docs/grammar.ebnf: CONSTRUCTOR/DESTRUCTOR terminals; MethodDecl updated.
docs/future-improvements.adoc: Stream I/O, macOS debugging, and
  multi-line string literals sections (cleaned up from draft notes).
2026-04-29 14:43:42 +01:00
Graeme Geldenhuys 81920c545a feat(rtl): TStringList — Text property, LoadFromFile, SaveToFile, Strings/Objects indexed properties
- Text: string read GetText write SetText
- SetText: Clear + SplitIntoList(AText, Ord(#10), Self)
- LoadFromFile: SetText(ReadFile(APath))
- SaveToFile: WriteFile(APath, GetText + #10)
- property Strings[Index: Integer]: string read Get write Put
- property Objects[Index: Integer]: Pointer read GetObject write SetObject
- 16 new compiler tests in cp.test.collections covering semantic and
  codegen for all new APIs
2026-04-29 13:07:44 +01:00
Graeme Geldenhuys f5587f18f4 feat(rtl): split TObjectList into contnrs.pas; add Items indexed property
- New rtl/src/main/pascal/contnrs.pas — TObjectList with Items[Index: Integer]
  indexed property (read Get write Put), matching the FPC contnrs layout that
  the compiler units use (uses Contnrs)
- rtl/src/main/pascal/classes.pas — TObjectList removed; Classes now provides
  only TStringList and SplitIntoList
2026-04-29 12:49:06 +01:00
Graeme Geldenhuys 36529f384d docs: document unit file name casing convention and resolution strategy 2026-04-29 12:46:23 +01:00
Graeme Geldenhuys ef4eb10cb8 fix: unit loader — case-insensitive file lookup on case-sensitive filesystems
TUnitLoader.Locate now tries LowerCase(AName) + '.pas' first, then falls
back to the exact case as written in the uses clause.  This means Pascal's
case-insensitive unit names work correctly on Linux and FreeBSD where the
filesystem distinguishes 'Classes.pas' from 'classes.pas'.

Established convention: all Blaise RTL unit files use lowercase names.
Renamed existing RTL files to match (Classes.pas, System.pas,
Generics.Collections.pas, Generics.Defaults.pas).
2026-04-29 12:44:31 +01:00
Graeme Geldenhuys 2f922fb57f docs: integer types, indexed properties — grammar and rationale
- language-rationale.adoc: add Integer Types section (Integer = 32-bit
  fixed, Int64 = 64-bit, Byte = 8-bit; MaxInt constant; IntToStr/Int64ToStr/
  StrToInt/StrToInt64 conversion built-ins; alternatives rejected); add
  Indexed Properties section (decision, rationale, alternatives rejected);
  resolve the open Integer Width question pointing to the new section
- grammar.ebnf: update PropertyDecl to include optional [Param: Type]
  index parameter; update FieldAssignment and Factor to include optional
  [Expr] index on IDENT DOT IDENT; add disambiguation comment
2026-04-29 12:32:37 +01:00
Graeme Geldenhuys ef7a98c687 feat: indexed properties, StrToInt64/Int64ToStr built-ins; restore fixpoint
- Indexed properties fully implemented across parser, semantic analyser,
  symbol table, and codegen (read and write, with index type-checking)
- StrToInt64 and Int64ToStr built-ins added (RTL: _StrToInt64, _Int64ToStr)
- Int literal and const codegen use Int64ToStr to avoid int32 truncation
- QbeEscapeString in hand source now uses manual hex arithmetic instead of
  Format('%02x') which Blaise's _StringFormat does not support
- Copy(..., MaxInt) calls replaced with Copy(..., Length(x)) to avoid RTL
  truncation of the 64-bit sentinel through _StringCopy's int32 parameter
- Stage-3 IR is byte-identical to stage-2: fixpoint holds after all changes
2026-04-29 12:18:45 +01:00
Graeme Geldenhuys bc41daddd7 release: open development branch towards v0.3.0 2026-04-28 23:51:51 +01:00
Graeme Geldenhuys 87597605cc release: promote version to 0.2.0
Second self-hosting fixpoint: stage-3 (hand source compiled by stage-2,
itself compiled by stage-1) produces byte-identical IR to stage-2 at
59974 lines.  New since v0.1.0: string subscript, char literals, const
open arrays, array literals, static arrays, Low/High, PChar, @expr
address-of, set types, GetEnvironmentVariable, and the four SysUtils
path functions.  Also fixes _CurrentExceptionMessage (was always empty
due to _PopExcFrame being called before the except body).
2026-04-28 23:30:06 +01:00
Graeme Geldenhuys 8cd346f676 feat: self-hosting fixpoint — stage-3 IR byte-identical to stage-2
Two fixes to reach the fixpoint milestone:

1. RTL: _CurrentExceptionMessage now reads from a dedicated
   g_current_exception thread-local set in _Raise, rather than from
   g_exc_top->exception.  The codegen calls _PopExcFrame() before
   executing the except body, so g_exc_top was already unwound and
   the exception message was always empty.

2. hand source (tests/blaise-compiler.pas): replace the hand-written
   ExtractFileName and ChangeFileExt helpers with the new compiler
   built-ins (step 11).  Add symbol-table registration, semantic
   type-checking, and codegen for all four path functions.

Result: stage-1 (FPC) → stage-2 → stage-3 → stage-4 are all 59974
lines; diff stage3 vs stage4 is empty.  Fixpoint achieved.
2026-04-28 23:11:15 +01:00