docs: update for v0.12.0 — native default, expanded stdlib, syntax additions

README.adoc:
- Native x86-64 is the default backend since v0.12.0; QBE is opt-in
  (--backend qbe).  Reframed the intro, project status, bootstrap, and
  single-file compile examples accordingly (native one-step build leads; QBE
  shown as the opt-in multi-step path).
- Test count 3474 -> 3800+ (3744 compiler + 57 stdlib).
- Added a Standard library status line (generics collections, JSON, SHA-1,
  Base64, GUIDs, sockets, WebSockets, HTTP server, blaise.testing).
- Bootstrap example resolves the newest releases/v* binary instead of the
  stale v0.7.0 path, and uses the native --output build.
- OPDF now debugs incrementally-compiled multi-unit programs.

grammar.ebnf (CLAUDE.md sync rule):
- TypeName accepts a unit-qualified, possibly-dotted QualIdent
  (UnitName.TypeName, System.SysUtils.TFormatSettings).
- FieldAssignment l-value extended with a Selector chain
  (.Field / [Index] / .Method(...)) so Self.F[i].Sub := value parses.

language-rationale.adoc:
- New entries "Qualified type names" and "Chained l-value assignment"
  recording the decision, reasoning, and alternatives for the two
  conservative FPC/Delphi-compatible parser extensions made this cycle.
This commit is contained in:
Graeme Geldenhuys 2026-06-23 23:32:08 +01:00
parent 4547a9e174
commit 8ece742cb0
3 changed files with 111 additions and 29 deletions

View file

@ -32,18 +32,20 @@ See link:docs/design.adoc[docs/design.adoc] for the full architecture and
implementation plan.
The result — A modern, cross-platform Object Pascal compiler targeting native code
via two backends: https://c9x.me/compile/[QBE] (the default, production-proven
backend) and a direct native x86-64 code generator (in active development,
approaching feature parity). Single language mode, single string type,
zero-GUID interfaces, reified generics, and first-class
https://github.com/graemeg/opdebugger[OPDF] debug format support.
via two backends: a direct native x86-64 code generator (the default since
v0.12.0) and https://c9x.me/compile/[QBE] (now opt-in via `--backend qbe`).
Single language mode, single string type, zero-GUID interfaces, reified
generics, and first-class
https://github.com/graemeg/opdebugger[OPDF] debug format support — including
full source-level debugging of incrementally-compiled multi-unit programs.
== 🚀 Project Status
* **Self-Hosting:** Yes. Blaise bootstraps and recompiles itself with byte-for-byte fixpoint. FPC is no longer required — the entire toolchain runs on Blaise alone.
* **Testing:** 3474 tests and growing (Test-Driven Development from day one). The test suite itself compiles under Blaise.
* **Backends:** Two code-generation backends — QBE (default, production) and a direct native x86-64 backend (in active development, approaching feature parity with QBE).
* **Testing:** 3800+ tests and growing (Test-Driven Development from day one). The test suite itself compiles under Blaise.
* **Backends:** Two code-generation backends — a direct native x86-64 backend (the default since v0.12.0) and QBE (opt-in via `--backend qbe`). Both pass the full fixpoint and test suite.
* **Standard library:** A growing opt-in stdlib — generics collections, JSON (DOM + parser + writer), SHA-1 and Base64, RFC 4122 GUIDs, TCP sockets, WebSockets, a minimal HTTP/1.1 server, and a `blaise.testing` unit-test framework.
[cols="1,3,1", options="header"]
@ -187,29 +189,30 @@ passed explicitly via the `BLAISE` make variable.
[source,shell]
----
# 0. Build the vendored QBE backend (once-off — not needed again unless
# you update vendor/qbe/)
cd vendor/qbe && make && cd ../..
# Resolve the newest release binary (substitute the path below for $RELEASE).
RELEASE=$(ls -d releases/v*/ | sort -V | tail -1)blaise # e.g. releases/v0.12.0/blaise
# 1. Build the runtime using the release binary (BLAISE= avoids chicken-and-egg)
cd runtime && make BLAISE=../releases/v0.7.0/blaise && make install && cd ..
cd runtime && make BLAISE=../$RELEASE && make install && cd ..
# 2. Compile the compiler using the latest release binary
releases/v0.7.0/blaise \
# 2. Compile the compiler using the release binary (native backend — the
# default since v0.12.0 — needs no external assembler or QBE)
$RELEASE \
--source compiler/src/main/pascal/Blaise.pas \
--unit-path compiler/src/main/pascal \
--unit-path runtime/src/main/pascal \
--unit-path stdlib/src/main/pascal \
--emit-ir > /tmp/blaise.ssa
# 3. Assemble and link
vendor/qbe/qbe -o /tmp/blaise.s /tmp/blaise.ssa
gcc -o compiler/target/blaise /tmp/blaise.s compiler/target/blaise_rtl.a
--output compiler/target/blaise
----
Once `compiler/target/blaise` exists, subsequent RTL rebuilds (`make && make install`)
work without the override. The QBE build in step 0 is a one-off and does not
need to be repeated when rebuilding the compiler.
work without the override.
The native backend (default) emits and links a binary directly — no external
tools. To bootstrap via the opt-in QBE backend instead, build the vendored QBE
once (`cd vendor/qbe && make`), add `--backend qbe --emit-ir > /tmp/blaise.ssa`
to step 2, then `vendor/qbe/qbe -o /tmp/blaise.s /tmp/blaise.ssa` and
`gcc -o compiler/target/blaise /tmp/blaise.s compiler/target/blaise_rtl.a`.
=== Bootstrap a development checkout
@ -257,14 +260,14 @@ Once built, the compiler binary is at `compiler/target/blaise`.
[source,shell]
----
# Compile a single-file program (QBE backend — default)
compiler/target/blaise --source Hello.pas --emit-ir > Hello.ssa
# Compile a single-file program (native backend — default, no external tools)
compiler/target/blaise --source Hello.pas --output Hello
# Compile via the opt-in QBE backend (emits QBE IR, assembled + linked separately)
compiler/target/blaise --source Hello.pas --backend qbe --emit-ir > Hello.ssa
vendor/qbe/qbe -o Hello.s Hello.ssa
gcc -o Hello Hello.s compiler/target/blaise_rtl.a
# Compile via the native x86-64 backend (no QBE step)
compiler/target/blaise --source Hello.pas --backend native --output Hello
# Compile with unit search paths
compiler/target/blaise --source MyApp.pas \
--unit-path src/units \

View file

@ -652,8 +652,17 @@ TypeName
| DynArrayType (* dynamic array: array of T *)
| SetType (* bit-set over an enum base type *)
| CARET TypeName (* pointer-to type: ^Integer, ^T, ^^T *)
| IDENT LESS TypeArgList GREATER (* generic instantiation: TList<Integer> *)
| IDENT (* simple type or built-in *)
| QualIdent LESS TypeArgList GREATER (* generic instantiation: TList<Integer> *)
| QualIdent (* simple type, built-in, or UnitName.TypeName *)
;
(* A type may be written with a unit qualifier, e.g. SysUtils.TFormatSettings,
* and the qualifier may itself be dotted (System.SysUtils.TFormatSettings).
* The parser reads the full dotted path; a type is always named by its FINAL
* component, with the leading components naming a unit already brought in by
* the uses clause. See language-rationale.adoc "Qualified type names". *)
QualIdent
= IDENT { DOT IDENT }
;
(* A static array may declare multiple dimensions either as a comma-separated
@ -925,8 +934,20 @@ PointerWriteStmt
;
FieldAssignment
= IDENT DOT IDENT [ LBRACKET Expr RBRACKET ] ASSIGN Expr
| LPAREN Expr RPAREN DOT IDENT ASSIGN Expr (* parenthesised lvalue, e.g. (a as TB).FX := v *)
= IDENT DOT IDENT { Selector } ASSIGN Expr
| LPAREN Expr RPAREN DOT IDENT { Selector } ASSIGN Expr (* parenthesised lvalue, e.g. (a as TB).FX := v *)
;
(* A Selector continues an l-value path after the first member: a further
* .Field, an [Index] subscript (indexed property or array-typed field
* element), or a .Method(...) call whose result is itself selected into.
* This covers chains such as Self.FStack[0].Count := value and
* r.A[0] := 10 — the subscript-then-field chain the parser added in
* baf5079. See language-rationale.adoc "Chained l-value assignment". *)
Selector
= DOT IDENT
| LBRACKET Expr RBRACKET
| DOT IDENT LPAREN [ ArgList ] RPAREN
;
MethodCall

View file

@ -5151,3 +5151,61 @@ interaction with type checking.
no concrete use for it until the language stabilises; `BLAISE` alone serves
the cross-compiler case. CPU/OS symbols are added because portable source
routinely keys on them.
== Qualified Type Names — `UnitName.TypeName`
=== Decision
A type reference may be written with a unit qualifier, e.g.
`SysUtils.TFormatSettings`, and the qualifier may itself be dotted, e.g.
`System.SysUtils.TFormatSettings`. The parser reads the full dotted path;
a type is always named by its *final* component, and the leading components
name a unit already brought in by the `uses` clause. The qualifier is
informational — it does not select between units at resolution time; the
final identifier is resolved against the symbol table as usual.
=== Rationale
Object Pascal source in the wild routinely qualifies type names with their
declaring unit, both for documentation and to disambiguate when two used
units export the same name. Rejecting `SysUtils.TFormatSettings` as a parse
error blocked porting otherwise-portable FPC/Delphi code for no language
benefit. Accepting the qualifier and resolving by the final component matches
how FPC and Delphi treat a unit-qualified type in the common (non-ambiguous)
case, while keeping resolution simple.
=== Alternatives considered
* *Full unit-scoped resolution (the qualifier selects the unit).* Deferred.
It requires per-unit symbol scoping at the resolution site and only matters
when two used units export the same type name — rare, and the duplicate
case can be resolved today by ordering the `uses` clause. Resolving by the
final identifier covers the motivating real-world code without that
machinery, and can be tightened later without changing the syntax.
== Chained L-value Assignment — `Self.F[i].Sub := value`
=== Decision
The left-hand side of an assignment may be a selector chain: after the first
member, any sequence of `.Field`, `[Index]` (indexed-property or array-field
element), and `.Method(...)` selectors, terminated by `:=`. For example
`Self.FStack[0].Count := value`, `r.A[0] := 10`, and `obj.Items[i].Flag := True`.
The bare implicit-`Self` field form (`A[0] := v` inside a method) follows the
same rule.
=== Rationale
The earlier parser accepted a single `.Field` (optionally one subscript)
before `:=` but rejected a *chain* — `Self.FStack[0].Count := value` failed
because the `Ident.Ident[Index]` path unconditionally expected `]:=` after the
subscript. That is a routine and idiomatic Object Pascal l-value; rejecting it
forced awkward temporaries and surprised anyone porting normal code. The chain
loop after the subscript makes the read and write l-value grammars symmetric:
anything that can be read as a designator can be assigned to.
=== Alternatives considered
* *Require an intermediate variable (status quo).* Rejected — it is a
gratuitous restriction with no semantic justification; the read side already
supported the full chain, so write-side parity is the consistent choice.