Commit graph

55 commits

Author SHA1 Message Date
mzhoot 94ae8c354c Рабочие синонимы 2026-07-04 21:44:54 +03:00
Graeme Geldenhuys bfd43f9147 feat(stdlib): add SHA-256, MD5, HMAC-SHA256, ConstantTimeEqual to Security.Crypto
Expand the crypto primitives unit with four new capabilities:

- SHA-256 / Sha256Hex (FIPS 180-4, Rotr32 helper, 64-round K table)
- MD5 / Md5Hex (RFC 1321, little-endian padding and output, T/S tables)
- HMAC-SHA256 / HmacSha256Hex (RFC 2104, key>block-size hashing)
- ConstantTimeEqual (timing-safe XOR-accumulate comparison)

Also refactors Sha1Hex to use a shared DigestToHex private helper,
adds Rotr32 alongside Rotl32, removes unused `uses Classes` import,
and adds MD5 deprecation notice to the unit header.

Tests: 12 crypto tests covering NIST CAVP (SHA-256), RFC 1321 all 7
vectors (MD5), RFC 4231 TC1-3/TC5-6 (HMAC-SHA256 incl. long-key),
and 8 ConstantTimeEqual assertions. E2E smoke test for Sha256Hex
on both backends (cp.test.e2e.crypto.pas).
2026-07-01 01:06:30 +01:00
Graeme Geldenhuys b5f8800817 fix(testing): rename global GRegistry to GTestRegistry to avoid symbol clash
The test framework's `GRegistry: TList<TTestCaseClass>` collided with
`GRegistry: TTargetRegistry` in blaise.codegen.toolkit.  The compiler emits
unit-level globals under their bare name with export (strong) linkage, so both
`GRegistry` symbols resolved to one shared 8-byte slot.  When the test runner
links both units, the test framework and the codegen toolkit alias the same
storage and free/read it as the wrong type at teardown.

Rename the test-framework global to GTestRegistry — a unit-specific name that
collides with nothing else — so the two registries occupy distinct storage.
2026-06-30 01:44:28 +01:00
Graeme Geldenhuys 50b8d75e6d fix(parser): enforce mandatory parentheses on statement-position calls (#148)
The "mandatory parentheses on zero-argument calls" rule (language-
rationale.adoc) was enforced in expression position but NOT in statement
position: a bare `Foo;`, `Obj.Method;`, or `Obj.Free;` used as a statement
compiled silently, building a paren-less TProcCall / TMethodCallStmt. Issue
#148 reported `tester.print;` (a unit's global object method call) being
accepted without its mandatory ().

The statement parser now raises the same "requires () for a call" diagnostic
the inherited-call and expression-position paths already use, at the two
fall-through sites:

  * bare unqualified call `Foo` with no '(' (the final ProcCall else-branch);
  * bare `Obj.Method` with no '(' and no further '.' chain.

Field reads, field assignments, indexed writes, and '.'-chains are unaffected
(only a terminating bare reference is rejected). Expression-position calls
were already enforced.

Enforcing the rule required the compiler, RTL, and stdlib to comply first
(self-hosting): swept bare calls in uSemantic (Flush/RepairGenericInstances),
runtime.arc/runtime.exc (_libc_abort), blaise.codegen.native.backend
(FAsm.AppendLine), and a json.writer doc example. The test suite embedded
many bare calls in inline program strings — all updated to carry (); adding
() never changes behaviour since these were always calls.

cp.test.parser.pas gains two parse-error tests (proc and method bare calls);
the old TestProcCall_NoParens, which asserted the bug, is inverted. grammar.ebnf
SubscriptMethodCall made parens mandatory and an example corrected; rationale
notes statement-position enforcement.
2026-06-28 10:15:39 +01:00
Graeme Geldenhuys a545e86026 fix(stdlib): memory streams use the RTL allocator, not libc malloc/free
streams.pas bound malloc/free/realloc to libc by name, which left an undefined
reference under --static (libc-free) — the memory streams are reachable from the
compiler, so a static self-host link failed on 'free'.

Bind them to the RTL's own mmap-backed allocator
(_BlaiseGetMem/_BlaiseFreeMem/_BlaiseReallocMem) instead.  Same signatures, so
the call sites are unchanged; memcpy stays the C name (libc, or runtime.cstub
under --static).  The streams now allocate from the same heap as everything else
and carry no libc dependency.

With this, free()/malloc()/realloc() are resolved and the compiler links fully
--static.  Verified: blaise --static builds the compiler itself into a libc-free
ET_EXEC (no PT_INTERP, not a dynamic executable) that compiles + runs a program;
FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite 0 assertion
failures (residual EUnitNotFound errors are a local search-path artifact).
2026-06-26 19:30:42 +01:00
Graeme Geldenhuys ac9e2fa0b1 refactor(rtl): relocate RTL units into the compiler tree (dotted-flat names) 2026-06-25 16:05:44 +01:00
Graeme Geldenhuys d7460cb069 feat(parser): require parentheses on inherited calls; fix metaclass diagnostic
The mandatory-parentheses rule ("every function, procedure, method, and
constructor call requires parentheses, even with no arguments") made no
exception for 'inherited'.  A bare 'inherited Create' is a method call and
should be rejected like any other parenless call, but the parser silently
accepted it.

Enforce the rule in both 'inherited' parse paths (statement and expression
position): a bare 'inherited Method' now raises the same "requires () for a
call" diagnostic as any other parenless call.

Also fix a misleading semantic error for the metaclass-variable case.  A bare
'C.Create' on a 'class of T' variable parses as a field access, which reached
the "'C' is not a record or class" guard — confusing, since C is a valid
metaclass variable.  AnalyseFieldAccess now detects a constructor/method name
on a metaclass receiver and emits the "requires () for a call" diagnostic
instead.  (The parenthesised 'C.Create()' already worked: it parses as a
TMethodCallExpr, which has the metaclass-dispatch branch.)

Migrate every bare 'inherited Method;' call site in stdlib, the bif-coverage
tool, and embedded e2e test programs to the parenthesised form.  The
parenthesised form compiles on the prior compiler too, so the migration and the
enforcement land together without breaking the rolling-bootstrap chain.

Docs: update language-rationale.adoc (inherited section + mandatory-parens
section) and grammar.ebnf (expression-position inherited rule).

Tests: add TestSemantic_MetaclassVar_BareCreate_RequiresParens and
TestParse_Inherited_Bare{Stmt,Expr}_RequiresParens.
2026-06-25 13:36:27 +01:00
Andrew Haines bfbcfb0489 fix(stdlib): release managed elements in generics.collections TList
TList<T> stored its elements in a raw GetMem buffer and freed only the
buffer (Destroy did FreeMem with no element cleanup; Clear just reset
the count; Delete's shift left a duplicate ref on the vacated tail
slot). For a managed element type — a class (ARC) or a string — that
leaked every contained value: destroying or clearing the list never
released its items, so their destructors never ran.

Release each managed element by storing a zero-initialised T through
its slot before dropping it: the compiler's ARC discipline on the
managed store releases the previous value, so freeing the list cascades
to its items. For a non-managed T (e.g. Integer) the same store is a
harmless zero write, so TList<Integer> is unaffected.

Applied to Destroy (all elements), Clear (all elements), and Delete
(the vacated tail slot). The sibling containers (TStack, TQueue, TSet,
and the dictionaries) share the same raw-buffer pattern and leak
likewise; left as a follow-up.

Test: an e2e regression on both backends asserts a class element's
destructor fires on Clear and on Free (the cascade), and the existing
TList<Integer> coverage confirms the non-managed path is unchanged.
2026-06-25 01:37:08 +01:00
Andrew Haines 4eeff7c2c0 feat(stdlib): add SameFileName to SysUtils
SameFileName compares two file names for equality. This is a partial,
Unix-only stub: an exact case-sensitive comparison, which is correct on
case-sensitive file systems. Case-insensitive platforms would need the
names folded before comparing, but the stdlib has no platform-variance
facility yet — system.pas hardcodes DirectorySeparator, PathSeparator
and LineEnding to their Unix values — so there is nothing to key the
case-folding on. When a FileNameCaseSensitive constant lands alongside
the other platform constants, the case-insensitive branch can be added.

e2e test asserts identical names match and differing names do not.
2026-06-25 01:34:42 +01:00
Graeme Geldenhuys e72b644a94 fix(stdlib,semantic): TObjectList default property + chained default-subscript crash (#138)
TObjectList.Items lacked the `default` directive, so OL[i] failed with
'String subscript requires a string expression' even though TStringList.Strings
already had it. Add `default;` to TObjectList.Items.

Separately, TStringList(OL[0])[1] — a typecast whose operand is itself a
default-property subscript, indexed again by a default property — crashed the
compiler. AnalyseStringSubscriptExpr rewrites the node in place (swaps StrExpr
for a synthesised field-access, clears IndexExpr) on the default/indexed-property
paths; the outer subscript re-analyses its typecast base, which re-analyses the
already-rewritten inner subscript, recursing without bound. Guard against
re-analysis by returning the cached ResolvedType when already set, and set
ResolvedType on the plain-string exit path so the guard is complete.

Regression test TestRun_DefaultProp_StringList_And_ObjectList compiles+runs the
issue's exact example on both backends with parity.
2026-06-24 12:32:11 +01:00
Graeme Geldenhuys 88a11034b9 stdlib: add SplitChar / SplitLines / JoinList to StrUtils
StrUtils had JoinStr (open-array join) but no splitter at all — a gap any
text-processing code hits. Add SplitChar (split on a delimiter byte),
SplitLines (split on LF, tolerating CRLF), and JoinList (the list-valued
inverse of SplitChar), all returning/taking TList<String>. This pulls
Generics.Collections into StrUtils' interface. Lifted from the Luhmann web
server. Adds 10 tests (57 stdlib tests total).
2026-06-23 20:00:02 +01:00
Graeme Geldenhuys b1abde0126 Make the test registry type-safe: TList<TTestCaseClass>
GRegistry was a TStringList abused as a list of metaclass pointers — the
string slot was always '' (vestigial) and the class reference lived in
Objects[] behind Pointer()/TTestCaseClass() casts. Now that generics support
metaclass type arguments (alias canonicalisation + space-safe label
mangling), it becomes a plain TList<TTestCaseClass>: RegisterTest just Adds
the class, GetRegisteredTest just Gets it — no dead string, no casts.

47 stdlib + 3744 compiler tests pass (both suites enumerate the registry to
run, so this exercises the metaclass-generic path end to end).
2026-06-23 19:47:42 +01:00
Graeme Geldenhuys 676cee0cae Modernise stdlib testing + tools: TList<String> over TStringList, drop .Free
Showcase generics + ARC across the test framework and tools. Replace
TStringList with TList<String> wherever only the basic list API was used
(Create/Add/Get/Count/Clear/Delete/IndexOf), rewriting .Strings[i] to .Get(i),
and remove manual .Free calls since Blaise reference-counts objects.

Lists genuinely needing TStringList-only API stay as TStringList: file I/O
(LoadFromFile/SaveToFile/Text), object association (AddObject/Objects[] in the
test registry and bif-coverage AST), and dup control (Duplicates). Destructors
that only freed fields are deleted.

stdlib 47 tests, compiler 3743 tests (20 pre-existing toolchain failures
unchanged), varcheck 18 regression tests all pass.
2026-06-23 19:21:09 +01:00
Graeme Geldenhuys b8de722038 stdlib: TProcess.Parameters is TList<string>, not TStringList
Process arguments are only ever appended and iterated (Add/Get/Count) — none
of the TStringList-specific API is used by any consumer, so TList<string> is
behaviour-identical and expresses 'an ordered list of argument strings' more
honestly. Every call site uses Parameters.Add(...), unchanged. Lets the unit
drop its Classes dependency for Generics.Collections. All consumers (the
codegen/linker driver, the test runner, the compiler test harnesses) compile
and pass unchanged: 3743 compiler + 47 stdlib tests.
2026-06-23 18:32:37 +01:00
Graeme Geldenhuys 5e42020faf stdlib: use TList<string> instead of TStringList in Net.Http.Server
The server only used Create/Add/Get/Count/Free on its string lists — none of
the TStringList-specific API — so TList<string> is behaviour-identical and lets
the unit drop its Classes dependency.
2026-06-23 17:28:05 +01:00
Graeme Geldenhuys dd635d5140 stdlib: document the libc/kernel layering rationale in Net.Sockets
Explain why the unit binds libc (the kernel boundary, the syscall doorway), the
common three-layer pattern shared with Indy/Synapse/the JDK, how Java is itself
not libc-free, and what a pure-Pascal syscall-stub version would and would not
buy.
2026-06-23 17:21:36 +01:00
Graeme Geldenhuys 3c6fbc3184 stdlib: flag Linux-specific porting boundaries in Net.Sockets and Security.Guid
Document that the socket constants/sockaddr_in layout (Net.Sockets) and the
getrandom(2) call (Security.Guid) are Linux x86_64 specifics, with the per-OS
differences and the fact that the public helper API is the stable surface a
future port keeps.
2026-06-23 00:22:33 +01:00
Graeme Geldenhuys 2ccfd04f69 stdlib: add Security.Guid (RFC 4122 v4 GUIDs)
NewGuid returns a canonical lowercase v4 GUID; NewGuidRaw returns the 16 raw
bytes. Randomness comes from the kernel CSPRNG via getrandom(2), with the
version (4) and variant (RFC 4122) bits forced. Tests cover the canonical
format, version/variant bits in both string and raw forms, and uniqueness
(47 tests total).
2026-06-23 00:15:03 +01:00
Graeme Geldenhuys cc43c4a0c5 stdlib: add Net.Http.Server (minimal HTTP/1.1 + WebSocket)
Single-threaded HTTP/1.1 server lifted from the Luhmann web server and
generalised: THttpRequest/THttpResponse/IRequestHandler/THttpServer plus
ParseRequest/UrlDecode. WebSocket upgrades are handled via Net.WebSockets and
Broadcast(payload) pushes a text frame to every connected socket (the
reload-specific BroadcastReload became a generic Broadcast). Built on
Net.Sockets. Private split helpers are prefixed to avoid colliding with a
consumer's same-named functions. Tests cover parsing, URL decoding, a loopback
request round-trip, and a WebSocket upgrade + broadcast (43 tests total).
2026-06-23 00:07:46 +01:00
Graeme Geldenhuys e52909576a stdlib: add Net.WebSockets (RFC 6455 framing + handshake)
Transport-agnostic WebSocket helpers: WebSocketAccept (handshake key),
EncodeFrame/EncodeTextFrame (server->client, unmasked, up to 2^31-1), and
DecodeFrame (parses one frame, unmasking masked client frames). Pairs with
Net.Sockets. Tests cover the RFC 6455 accept vector, encode length forms,
and masked/unmasked round-trips (38 tests total).
2026-06-22 23:52:38 +01:00
Graeme Geldenhuys 5cbc0613b5 stdlib: add Net.Sockets (IPv4 TCP) with tests
POSIX TCP sockets over libc FFI: raw bindings plus helpers (TcpListen/
TcpConnect, AcceptConn, RecvString, SendAll, MakeNonBlocking, IgnoreSigPipe)
and IPv4/ParseIPv4 address helpers. Tests cover address parsing and a
loopback round-trip (32 tests total).
2026-06-22 20:52:05 +01:00
Graeme Geldenhuys 304504886b feat(stdlib): add Security.Crypto (SHA-1) and Encoding.Base64
Two small, independent units, split along the lines Java and .NET draw:

  Security.Crypto   SHA-1 (raw 20-byte digest + Sha1Hex), the home for hash
                    digests (cf. java.security / System.Security.Cryptography).
  Encoding.Base64   Base64 encode + decode (RFC 4648), a general text encoding,
                    not crypto (cf. java.util.Base64 / System.Convert).

Callers compose them, e.g. the WebSocket handshake is
Base64Encode(Sha1(key + GUID)).  Both TStringBuilder-backed (O(n)).

Adds Crypto.Tests (SHA-1 FIPS vectors + the RFC 6455 handshake) and
Base64.Tests (RFC 4648 vectors, decode, round-trip) to the stdlib test tree
via Test.Registry; pasbuild test -m blaise-stdlib runs 27 tests.
2026-06-22 20:33:55 +01:00
Graeme Geldenhuys 36065ecd7d feat(stdlib): add JSON library (writer, DOM, parser, reader)
A read+write JSON library for the standard library:

  Json.Writer  streaming serialiser — TStringBuilder-backed (O(n)), with a
               three-tier .NET-style API: WriteString(name,value) for object
               fields, WriteStringValue(value) for array elements, and WriteKey
               for nested-container values. Compact + pretty output. Standalone
               (no DOM dependency).
  Json.Types   in-memory document model (DOM): TJSONData + typed nodes, tree
               building, accessors; emits through Json.Writer.
  Json.Parser  RFC 8259 recursive-descent parser into the DOM, with full string
               escapes incl. \uXXXX surrogate pairs.
  Json.Reader  thin GetJSON(text) facade.

Adds the first stdlib test tree under stdlib/src/test/pascal: a self-registering
Json.Tests suite (19 tests), a Test.Registry hub unit, and a TestRunner program,
wired into 'pasbuild test -m blaise-stdlib' via the <test> block in project.xml.
run-tests.sh builds and runs the suite directly.
2026-06-22 20:07:34 +01:00
Graeme Geldenhuys 6d46af93de fix(codegen): native HasClassAttribute — emit the call and the attribute RTTI
HasClassAttribute(AClass, AAttrClass) was unreliable on the native backend:
- The call was never emitted.  The builtin had no native lowering, so it fell
  through to a generic path that left the first metaclass's typeinfo address in
  %rax and read its low byte as the Boolean result — a layout-dependent false
  positive (a plain class reported as having an attribute).
- Even with the call, the attribute table was missing: native typeinfo
  hardcoded slot 7 (attrs) to nil, so a [Threaded]-marked class reported False.

Fixes (blaise.codegen.native.x86_64.pas):
- Add a native lowering for the HasClassAttribute builtin (gated on
  FC.IsBuiltinHasClassAttr): evaluate both metaclass args to typeinfo pointers
  in %rdi/%rsi, call _HasClassAttribute, normalise %al to 0/1.  Mirrors the
  QBE backend.
- Emit the class attribute RTTI table (count + one typeinfo pointer per
  attribute) as attrs_<Class> and reference it from typeinfo slot 7, nil when
  the class has no attributes.  Mirrors the QBE backend's attrs_<Class>.

This is what blaise.testing's runner uses to decide [Threaded] subprocess
dispatch; the false positive forked non-threaded suites and crashed them.

e2e test in cp.test.e2e.gaps (AssertRTLRunsOnAll, both backends): a plain
TTestCase reports False, a [Threaded] one reports True.  Full suite OK (3739);
all four fixpoints pass on both build paths.
2026-06-22 18:05:32 +01:00
Graeme Geldenhuys 3cf5333b16 fix(stdlib): make TStringList.Strings the default array property 2026-06-21 13:31:16 +01:00
Graeme Geldenhuys 008cc12785 feat(stdlib): configurable rounding for TMoney
TMoney previously hard-wired banker's rounding (rmHalfEven) at its single
normalisation point.  The underlying TDecimal already supports all eight
TRoundingMode values plus custom IRoundingStrategy, so expose that through
TMoney: every constructor and rounding-sensitive operation gains overloads
taking an explicit TRoundingMode or an IRoundingStrategy.

  MoneyFromStr('1.005','USD')            -> 1.00  (banker's, unchanged default)
  MoneyFromStr('1.005','USD', rmHalfUp)  -> 1.01
  C := A.Add(B, rmCeiling);
  M := MoneyFromStr('9.999','USD', myCashRoundingStrategy);

Overloaded: MoneyFromStr, MoneyFromDecimal, Add, Subtract, Multiply (each
gains a TRoundingMode form and an IRoundingStrategy form).  MoneyFromInt,
MoneyZero, MultiplyInt and Negate are exact (no fraction introduced) and
need no mode.  The existing no-mode calls are unchanged and keep banker's
rounding, so this is fully backward-compatible.

Internally a single MakeMoneyS(strategy) core does the normalisation; the
mode form delegates via StandardRounding(Mode) and the default via
rmHalfEven — one rounding code path.

Tests: 5 IR/semantic tests and 8 dual-backend e2e tests covering the mode
overloads (HalfUp/Down/Ceiling), the custom-strategy overload, the
default-stays-banker's guarantee, and rounding on Add/Multiply/constructors.

Docs: language-rationale.adoc updated to record that rounding is swappable
per call (default banker's), not hard-wired.

Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite
green on both the QBE-built and native-built runners (3686 tests).
2026-06-20 13:38:40 +01:00
Graeme Geldenhuys 36cad37423 feat(stdlib): add Numerics.Money — currency-aware TMoney
TMoney is a thin value-type wrapper layering an ISO-4217 currency tag onto
an exact TDecimal amount.  The numeric core (TDecimal) stays currency- and
locale-agnostic; currency policy lives entirely in this wrapper, matching
Moneta / money-gem / rusty-money.

Design:
- Currency is an upper-cased ISO-4217 string code; the set is open (unknown
  codes are accepted at the fallback minor-unit scale of 2).
- A built-in registry gives each currency its default scale (JPY 0, USD 2,
  KWD 3, fallback 2); every TMoney is normalised to its currency's scale on
  construction and after every operation, using banker's rounding.
- Cross-currency Add/Subtract/Compare raise EMoneyMismatch (no implicit
  conversion); Equals is total (False, not raise, across currencies).
- Immutable value semantics, mirroring TDecimal.

API: free-function constructors (MoneyFromStr / MoneyFromDecimal /
MoneyFromInt / MoneyZero), record methods (Amount, CurrencyCode, IsZero,
Sign, Add, Subtract, Negate, Multiply, MultiplyInt, Compare, Equals,
AmountString, ToString), and the CurrencyScale registry function.

Tests:
- cp.test.numerics.money.pas: 22 IR/semantic tests (resolution, IR shape,
  type errors) via TUnitLoader.
- cp.test.e2e.numerics.money.pas: 24 dual-backend e2e tests
  (CompileAndRunWithRTL) covering construction + per-currency normalisation,
  banker's rounding, case-folding, arithmetic, mismatch raising,
  Compare/Equals/IsZero/Sign, the registry, and a realistic invoice flow.

Docs: language-rationale.adoc gains "Currency Amounts — TMoney Wraps
TDecimal, Currency Is a String Tag" (decision + alternatives);
future-improvements.adoc marks Numerics.Money as implemented.

Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite
green on both the QBE-built and native-built runners (3671 tests).
2026-06-20 10:33:35 +01:00
Graeme Geldenhuys 718ee45e20 feat(stdlib): add Numerics.Decimal — exact decimal (TDecimal)
A single exact base-10 decimal type for financial / exact-decimal use,
replacing the historical Currency/Comp/Extended proliferation with one type.

  - Construction: DecFromInt/Int64/Str, plus a safe/exact float split
    (DecFromFloat takes the shortest decimal — 0.1 stays 0.1 — while the
    dangerous binary-exact path is the explicitly-named DecFromFloatExact).
  - Value semantics + value-based equality: 2.0 = 2.00 with a consistent hash,
    so it is safe as a dictionary key (unlike Java BigDecimal).
  - Arithmetic: Add/Subtract (scale = max), Multiply (scale = sum), Negate, Abs,
    arbitrary precision via a decimal-digit magnitude (compact Int64 fast path
    inflating on overflow).
  - Division + rounding: a layered design — a TRoundingMode enum (8 modes,
    banker's default) over an IRoundingStrategy interface users can implement
    for custom rounding.  Division always carries an explicit scale + mode.
  - Formatting: ToString/ToPlainString never use scientific notation;
    StripTrailingZeros keeps integer zeros (600 stays 600, never 6E+2).
  - Conversions out: ToDouble (lossy), ToInt64 (truncating).

IR tests (32) + e2e tests (46).  Add/Subtract/Multiply and the value-semantics
run dual-backend; Divide/RoundTo and float conversion run QBE-only pending
native codegen fixes (logic verified on QBE; see bugs.txt).
2026-06-20 02:10:12 +01:00
Graeme Geldenhuys 8f38fd1416 feat(stdlib): add CodePointToString UTF-8 encoder to strutils
Adds CodePointToString(CP: Integer): string, the inverse of CodePointAt /
CodePointFromByteIndex.  It encodes a Unicode codepoint (0..U+10FFFF) as
its 1..4 byte UTF-8 sequence, providing the codepoint-aware replacement for
the Char(n) cast used in other Pascal dialects.  Chr(n) only writes a single
raw byte, which is invalid for codepoints above 127; CodePointToString emits
the correct multi-byte form.  Out-of-range values yield an empty string.

E2E tests cover single-byte ASCII, the two-byte 0xC2 0xB1 encoding of U+00B1,
a round-trip across all four UTF-8 length classes, and out-of-range handling.
2026-06-19 09:28:53 +01:00
Graeme Geldenhuys a82269f48c feat(stdlib): add Items default property to TDictionary and TOrderedDictionary
Add GetItem/SetItem methods and an Items[Key: K] default property to
both TDictionary<K,V> and TOrderedDictionary<K,V>.  This enables the
idiomatic d[key] syntax for reads and writes:

  D['one'] := 1;
  WriteLn(D['one']);

GetItem halts on missing key (consistent with index-out-of-bounds in
TList).  SetItem delegates to Add (add-or-update semantics).

The compiler already had full default property infrastructure (parser,
AST, semantic, both codegens) — only the stdlib declarations were
missing.  This is the first non-Integer-typed default property in the
codebase, exercising the generic index type path end-to-end.

Tests: 2 IR codegen tests (GetItem/SetItem emission) + 3 e2e tests
on both backends (integer keys, string keys, update-via-bracket).
3407 tests pass.  All three fixpoints verified.
2026-06-17 15:05:02 +01:00
Graeme Geldenhuys 2f54707a63 feat(strutils): clean literal replace API — Replace (first) + ReplaceAll
Replace the FPC/Delphi replace surface (ReplaceStr, ReplaceText, and the
proposed StringReplace + TReplaceFlags set) with two self-explanatory
functions:

  Replace(S, Old, New)    — replaces the FIRST occurrence
  ReplaceAll(S, Old, New) — replaces EVERY occurrence

Both are case-sensitive and literal (non-pattern).  This sheds the legacy
redundancy: ReplaceStr/ReplaceText were two names for one operation split by
a 'String vs Text' distinction that is not obvious, and Delphi's StringReplace
encodes the same two booleans (all-vs-first, sensitive-vs-insensitive) as an
awkward flag-set.  Modern languages (Go, Python, Java, Rust) use a plain
first/all split with no case flag.

Case-insensitive replace is expressed by lower-casing the inputs with the
built-in LowerCase/UpperCase before calling Replace/ReplaceAll; a
case-*preserving* insensitive replace and pattern/regex matching are deferred
and recorded in docs/future-improvements.adoc.

The internal worker keeps the existing TStringBuilder-based loop; it gains a
FirstOnly flag and drops the now-unused case-fold path.

No callers of the removed functions existed anywhere in the tree.  IR/semantic
and e2e tests updated to the new names; both fixpoints green (FIXPOINT_OK,
NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK); full suite OK (3346 tests).
2026-06-16 23:49:30 +01:00
Graeme Geldenhuys 712263abd4 feat(rtl): integer div/mod by zero raises catchable EDivByZero
Integer div and mod by zero previously trapped in hardware (SIGFPE),
uncatchable even inside try/except — a single bad divisor terminated the
process.  Both backends now emit a divisor-zero guard before each integer
idiv that raises a catchable EDivByZero exception via the normal exception
machinery, matching standard Pascal/Delphi semantics.

- SysUtils: add EDivByZero = class(Exception) and the _RaiseDivByZero helper
  that raises EDivByZero('Division by zero').
- QBE + native x86-64: emit a divisor==0 check before integer div/rem
  (32- and 64-bit); on zero, call SysUtils__RaiseDivByZero (which longjmps).
- The guard is emitted only when SysUtils is in scope (EDivByZero resolves
  through the symbol table); without SysUtils, division traps as before.
  This mirrors Delphi's placement of EDivByZero in System.SysUtils and keeps
  the always-linked runtime free of stdlib class machinery.
- Tests: in-process e2e proves the guard does not disturb normal division on
  both backends; shell-out cli tests (real compiler + linked stdlib) prove
  catchable div/mod-by-zero on both backends.
- Rationale recorded in language-rationale.adoc.

All three fixpoints green (FIXPOINT_OK, NATIVE_FIXPOINT_OK,
NATIVE_INTERNAL_OK); full suite OK (3345 tests).
2026-06-16 23:04:56 +01:00
Graeme Geldenhuys 42ca9d7535 feat(stdlib): TList<T> default array property — List[i] subscript
TList<T> now exposes a `default` array property, so elements can be read
and written with the familiar subscript syntax instead of .Get(i):

    L[0] := 100;            // L.SetItem(0, 100)
    WriteLn(L[0] + L[1]);   // L.Get(i)

Adds TList<T>.SetItem (a pointer-target store, so the compiler's ARC
discipline releases the old element and retains the new for managed T)
and the property Items[AIndex]: T read Get write SetItem; default.

Also fixes the generic-instance clone path in the semantic analyser: it
copied every property-decl field except IsDefault, so a default property
declared on a generic template (TList<T>) lost its default flag on
instantiation and List[i] did not resolve. The clone now carries
IsDefault.

Verified List[i] read, write, and polymorphic element dispatch
(List[i].Area through a base-typed element) on both backends. Adds
TE2ETListTests.TestRun_TList_DefaultProperty_{ReadWrite,Polymorphic}.

Note: this makes the stdlib depend on the `default` directive (added in
the previous commit), so the pre-release bootstrap binary is refreshed to
a default-aware stage-2 in a follow-up.
2026-06-16 14:57:50 +01:00
Graeme Geldenhuys b9aae4abf5 dateutils: fixes documentation reference in header comment. 2026-06-11 23:12:30 +01:00
Graeme Geldenhuys b9e0ea7a9e docs+test: collection ownership conventions — generic retention pinned
Resolves the TStringList.Objects / TList<T> retention question from the
bug backlog as an explicit design decision (language-rationale,
'Collection Ownership'):

- TList<T>/TStack<T>/TQueue<T> managed elements ARE retained on store —
  this already works (generic stores lower to ARC-aware pointer writes);
  TE2ETListTests.TestRun_TList_ClassElements_RetainedAcrossScope now
  pins it on both backends (object survives its creating scope).

- TStringList.Objects[] stays NON-OWNING by design: the API type is
  Pointer and the integer-cast idiom (TObject(PtrUInt(N)), used in 27
  places in the compiler itself) makes blind retention impossible.
  Convention documented at the declaration and in the rationale.

- Known limitation recorded: generic Clear/Destroy do not yet release
  remaining managed elements (needs a Default(T)-style zero-store);
  tracked in the leak backlog.

Suite: 2946 OK; FIXPOINT_OK; NATIVE_FIXPOINT_OK.
2026-06-11 11:55:34 +01:00
Graeme Geldenhuys 17c14b37aa perf(qbe): borrowed-local const-string args + lookup/format tail wins
With the default-argument statement bug fixed, the borrowed-local
elision blocked in 91f44ec is sound and enabled: a const-string argument
that is a plain local or by-value/const parameter variable emits no
caller pin at all — the frame's own reference outlives the call.
Exclusions guard the aliasing edges: address-taken locals (explicit @,
passed to var/out params), locals captured by nested procedures (both
via a per-function blocklist rebuilt in EmitVarAllocs), and calls whose
signature has a var/out string param (F(L, L) lets the callee release
L's buffer through the alias).  Nine IR contract tests cover every
shape, including the new exclusion guards.

Tail wins on the remaining profile:
- QBEMangle: fast path returns the input unchanged when it contains no
  mangled characters (1.3M calls were rebuilding clean names one concat
  per character).
- TStringList.FindSorted: compares the probe slot in place through the
  CompareStr/CompareText builtins instead of copying it into a local
  and dispatching through Compare — several ARC ops per binary-search
  step on the hottest lookup path.
- _StringFormatN: the sizing pass uses a pure DecimalWidth count
  instead of rendering every %d argument twice.

Compiler self-compile: 0.78 s -> 0.60 s wall, 8.60G -> 6.45G
instructions.  Verified through two self-hosted generations, gen-2
native emission, both fixpoints and the full suite (2870 tests).
2026-06-10 23:53:01 +01:00
Graeme Geldenhuys 91f44ec823 perf(qbe): shape-aware const-string argument ARC
Const-string args were pinned (AddRef before the call, Release after)
regardless of shape.  ConstArgMode now classifies them: string literals
and named consts are immortal and emit no ARC ops at all; +1 owned
temps (function/method/getter returns) emit a single post-call Release
that consumes the transferred reference — previously these leaked one
buffer per call (the pin pair netted to zero and EmitOwnedArgReleases
deliberately skipped const-string params); every other shape keeps the
pin.  The hot TStringList lookup signatures (Compare, KeyEquals,
HashOf, Find, FindSorted, IndexOf) become const.

Borrowed elision for plain local variables — the larger win — is
deliberately NOT enabled: it is sound at the ARC level, but removing
the balanced pin pairs changes caller register allocation and exposes a
latent use-before-def of a callee-saved register in the native
backend's EmitInterfaceCall (a second-generation compiler then reads
the caller's leftover %r13 as an AST pointer and crashes).  The full
repro and bisect trail are recorded in the bug log; the camPin branch
carries a pointer to it.

Verified through two self-hosted generations plus both fixpoints;
contract pinned by 7 IR tests in cp.test.constarg.pas.
2026-06-10 23:18:24 +01:00
Graeme Geldenhuys 3799f1516a perf(stdlib): hash lookup for TDictionary, TOrderedDictionary and TSet
The generic keyed containers used documented linear key scans.  They now
build the same lazy open-addressing index as TStringList: threshold 16,
kept at <= 50% load, maintained on Add, invalidated by Remove/Clear/
Destroy (storage stays insertion-ordered, so TOrderedDictionary indexed
access is unchanged).

Hashing an arbitrary key type works through monomorphisation: the
generic bodies call GCHashOf(Key), which resolves per instantiation
against new overloads (string hashes its bytes FNV-1a, matching the
content semantics of '=' on strings; Integer/Int64/Pointer use a
multiplicative mix).  Instantiating a keyed container with a type that
has no GCHashOf overload is a compile-time error; the threshold const
lives in the interface because generic bodies are analysed at their
instantiation site.

Neutral on the compiler self-compile (its dictionaries are small); this
is stdlib quality for user programs.  Contract pinned by 7 new
in-process tests (cp.test.maphash.pas) written against the linear
implementation first.
2026-06-10 19:41:38 +01:00
Graeme Geldenhuys 4c74506e0f perf(stdlib): hash index for unsorted TStringList.Find/IndexOf
Profiling the compiler self-compile (--emit-ir, callgrind) showed 86% of
all instructions inside linear scans of five unsorted TStringLists
(FUnitSymbols 68%, FMethodIndex 12%, FStrLits 4%, FUnitIfaces 2%);
TStringList.Find averaged 145 Compare calls per lookup, and each
iteration paid six ARC ops through Compare's by-value string params.

Unsorted lists with >= 16 entries now build a lazy open-addressing hash
index on first Find: FNV-1a over the bytes, case-folded when the list is
case-insensitive, kept at <= 50% load so an empty slot terminates every
probe.  Duplicate keys keep the first-added index, preserving the
IndexOf contract that overload registration relies on.  Appends update
the table in place; Delete/Insert/Put/Clear/CustomSort and the
Sorted/CaseSensitive setters invalidate it for lazy rebuild.  Sorted
lists keep using binary search; small lists keep the linear scan.

Compiler self-compile: 4.75 s -> 0.91 s wall (5.2x), 71.6G -> 10.5G
instructions (6.8x).  Behaviour contract pinned by 12 new in-process
tests in cp.test.stringlistfind.pas, written against the old linear
implementation first.
2026-06-10 18:54:37 +01:00
Graeme Geldenhuys bb2fe2da75 refactor: rename uCodeGen/uCodeGenQBE to blaise.codegen/blaise.codegen.qbe
Align the QBE backend unit names with the dotted naming convention
already used by the native backend (blaise.codegen.native.*).

  uCodeGen.pas    → blaise.codegen.pas
  uCodeGenQBE.pas → blaise.codegen.qbe.pas

Updated all uses clauses (~70 test files, main compiler units),
documentation references (README + 5 docs/*.adoc files), and
comment references in runtime/stdlib.
2026-06-10 12:12:14 +01:00
Graeme Geldenhuys c26e198180 perf(rtl): SIMD-accelerated UTF-8 codepoint counting (SSE2 + AVX2)
CodePointLength now delegates to _Utf8CountCodePoints in hand-written
x86_64 assembly.  Processes 32 bytes/iteration with AVX2 (runtime-
detected via CPUID) and 16 bytes/iteration with SSE2, falling back to
scalar for tail bytes.  The algorithm counts non-continuation bytes
(bytes where (b & 0xC0) != 0x80) using signed pcmpgtb against -65.
2026-06-07 23:56:12 +01:00
Graeme Geldenhuys 8c615040b7 feat(stdlib): add UTF-8 codepoint functions to StrUtils
Seven new functions for Unicode-aware string operations:
- CodePointSize: byte width of a UTF-8 sequence from its lead byte
- CodePointLength: count codepoints in a string (O(n) scan)
- CodePointCopy: extract substring by codepoint index and count
- CodePointAt: codepoint value at a codepoint position
- CodePointPos: find substring, return codepoint index
- CodePointByteIndex: convert codepoint index to byte index
- CodePointFromByteIndex: decode codepoint at a byte position (O(1))

All functions use PChar arithmetic for zero-allocation performance.
2026-06-07 23:33:17 +01:00
Graeme Geldenhuys 00596b2ca6 fix(lang): add mandatory () to all bare zero-arg calls, add semantic diagnostic
The mandatory-() migration (commit 57c0660) missed ~1000 bare zero-arg
function and method calls across the compiler, stdlib, and test suite.
Without (), these were silently treated as variable reads, producing
broken QBE IR (%_var_ temporaries instead of call instructions).

Added semantic error diagnostics in uSemantic.pas to catch bare references
to functions/procedures that require () for a call. This prevents silent
miscompilation and gives users a clear error message.

Fixed all bare calls across 69 files: compiler pipeline (uParser,
uSemantic, uCodeGenQBE, uLexer, uPasTokeniser, etc.), stdlib (classes),
and the full test suite (inline test programs and test framework code).

FIXPOINT_OK at 262,220 lines. 2627 tests pass. Rolling bootstrap from
v0.10.0 verified.
2026-06-07 05:46:48 +01:00
Graeme Geldenhuys 57c0660a37 refactor(lang): remove IsNoArgFuncCall dead code, fix remaining bare calls
With mandatory () on all zero-argument calls (51ebf23), the
IsNoArgFuncCall/NoArgFuncDecl fields on TIdentExpr and the
synthesised TFuncCallExpr codegen path are dead code. Remove them.

This exposed ~300 bare function calls missed by the original
migration script across compiler, runtime, stdlib, tests, and
embedded Blaise source strings. Fix all of them.

Add IsProcFieldCall support to TMethodCallExpr so that
H.Handler() works for procedure-type fields — the parser creates
TMethodCallExpr for Obj.Name(), and the semantic pass now detects
when 'Name' is a procedural-typed field rather than a method,
routing through the indirect-call codegen path.

Update fixpoint.sh to fall back to an existing blaise_rtl.a when
the release binary is too old to build the runtime.

2627 tests pass, FIXPOINT_OK.
2026-06-06 20:28:13 +01:00
Graeme Geldenhuys 51ebf2350a feat(lang): require mandatory () on all zero-argument calls
Make parentheses mandatory on every function, procedure, method, and
constructor call — even those with zero arguments.  A bare identifier
or field access is now unambiguously a variable/field/property read;
appending () makes it a call.

Mechanically migrated all 144 source files (compiler, runtime, stdlib,
tests, kanban tool).  Fixed several latent bugs exposed by the AST node
transition from TFieldAccessExpr.IsMethodCall to TMethodCallExpr:

- IsBuiltinToString applied to record methods (added tyClass guard)
- IsVarParam not set for value record/static-array parameters in
  AnalyseMethodCallExpr (extended to check skParameter + aggregate type)
- Native backend used movq (pointer load) for record receivers instead
  of leaq (address-of) in EmitMethodCallExpr
- ResolveDiamond now handles TMethodCallExpr for diamond-operator
  constructor calls

Updated grammar.ebnf (MethodCall, ProcCall, Factor rules) and
language-rationale.adoc with the design decision.  Marked the
future-improvements.adoc entry as implemented.

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

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

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

Document the TThread ARC lifetime model in language-rationale.adoc and
add class/method documentation comments to classes.pas.
2026-06-06 14:56:18 +01:00
Graeme Geldenhuys 0c5910195c refactor(rtl): drop legacy platform aliases, derive constants from target
Remove sLineBreak (Delphi alias for LineEnding) and PathDelim (Delphi
alias for DirectorySeparator).  Blaise keeps exactly three platform
constants: LineEnding, DirectorySeparator, PathSeparator.

Platform constant values are now derived from GTarget (the compilation
target) via TargetLineEnding/TargetDirectorySeparator/TargetPathSeparator
in blaise.codegen.target, so cross-compilation produces the correct
values without conditional compilation.
2026-06-05 17:01:04 +01:00
Graeme Geldenhuys 6711a6cfdd fix: green the test suite — array-const link collisions and TComponent ARC
Three fixes that were causing six e2e failures:

Local array consts collided at link time. Function/block/unit-level
`const X: array[...] = (...)` were emitted as `export data $X` with no
mangling. The RTL's own `Days` const (rtl.platform.posix.pas) then
clashed with any user program declaring a `Days` const
(`multiple definition of 'Days'`). Local array consts now use a mangled,
file-local label (`__bac_N_Name`): the mangled name is set in semantic
(AnalyseArrayConstDecls -> ResolvedQbeName / ConstArrayQbe), carried to
the bare-ident read path via TIdentExpr.ConstArraySymbol, and emitted as
non-exported `data` so two separately-compiled objects cannot clash.
Class/record consts keep their exported, type-qualified label.

TComponent fought ARC and crashed. The destructor force-Freed children
it held only as raw pointers, while a caller's variable still referenced
them — a use-after-free at scope exit, surfacing as infinite
Destroy->RemoveComponent->ClassRelease recursion. Ownership is now honest
with ARC: FOwner is [Unretained]; AddComponent takes a strong ref via
_ClassAddRef; Destroy releases the owner's hold rather than Freeing, so a
child the caller still holds survives and an unheld child is destroyed;
RemoveComponent only unlinks the slot (the dying child's ref already
reached zero).

Deflaked TestRun_Thread_Terminate_Flag. The worker could finish via its
`I > 1000` break before the main thread's Terminate landed, racing the
printed flag between 0 and 1. The loop now exits only via the terminate
flag (with a high safety cap), so FTerminated is deterministically True.

Unit tests in cp.test.constants.pas updated to assert the mangled,
non-exported label.
2026-06-03 01:49:00 +01:00
Graeme Geldenhuys 576f60d807 fix(stdlib): TStringList.Text/LoadFromFile preserve verbatim lines
SplitIntoList — the sole backend for TStringList.SetText and LoadFromFile —
trimmed leading and trailing spaces from each split segment. That is correct
for CSV-style splitting but wrong for line-splitting: a TStringList must store
each line verbatim, including indentation.

Removed the trimming so each segment is added as Copy(S, Start, Len) untouched.
SplitIntoList has exactly one caller (SetText), so no trimming-dependent code
is affected.

This also fixes a latent test-infrastructure bug: the threaded test runner
parses each subprocess suite's stdout via TStringList.Text and identifies
failure-detail lines by their two-space indentation. With indentation stripped,
those lines never matched, AddFailure was never called, and the merged result
reported zero failures — so the plain summary printed OK even when [Threaded]
suites failed. With lines preserved, the summary now correctly reports failures.

Regression test: TestRun_TextSet_PreservesLeadingWhitespace asserts on raw
stdout so the spaces inside [] are checked without round-tripping through Text.

Self-hosting fixpoint verified clean.
2026-06-03 00:40:00 +01:00
Graeme Geldenhuys 99b7761bf4 refactor: use Exit(X) shorthand for Result-then-Exit early returns
Now that Exit(X) is supported (0f63626), replace the historical
'Result := X; Exit;' workaround with the Exit(X) function-result shorthand
across compiler, runtime, and stdlib (305 sites).

Only same-indentation pairs are merged: a 'Result := X;' immediately followed
by an 'Exit;' at the same indentation level. Sites where the trailing 'Exit;'
is less-indented than the assignment are left untouched — there the Exit
belongs to an enclosing for/else/if block, not to the assignment, so merging
would change control flow (e.g. the xor/and numeric-result branch in
uSemantic falling through to the Boolean-operand check). 14 such sites are
deliberately not converted.

Three converted sites sit inside a real try/finally (uSemantic overload/
generic resolution); each try body already contained bare Exit; statements
and its finally body is a plain local-list Free, so control flow is unchanged.
A new e2e test (TestRun_ExitValueThroughFinally) confirms Exit(X) from inside
a try/finally runs the finally body and returns the value.

Self-hosting fixpoint verified clean (stage-2 IR == stage-3 IR).
2026-06-03 00:33:50 +01:00