`type TByte = 0..255;` failed to parse — ParseTypeDecl had no case for an
integer-literal subrange, so the RHS fell through to the generic "expected
record/class/..." error.
Blaise does not range-check, so a named subrange is treated as an alias to
the narrowest STANDARD integer type that holds both bounds (0..255 -> Byte,
-10..10 -> SmallInt, etc.). This keeps record/array element layout correct
(TByte is byte-sized) while the value behaves as an ordinary integer. Two
parser helpers do the work: SubrangeAhead (lookahead: IntLit.. or -IntLit..)
and ParseIntegerSubrangeBaseType (parse lo..hi, pick the base type, reject a
descending range). Note Blaise has no 8-bit signed alias, so a signed
subrange that would fit in ShortInt widens to SmallInt.
Only integer-literal bounds form a named type; identifier/enum-bounded
subranges (TLow..THigh, red..blue) are intentionally not handled here (they
are ambiguous as a named-type form).
Tests: parser tests (named subrange, negative bounds, descending-is-error)
and dual-backend e2e tests (named subrange runs; subrange as a record field
and array element with a negative range). grammar.ebnf gains the
IntSubrange rule.
Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite
green on both the QBE-built and native-built runners (3691 tests).
The rolling-bootstrap workflow only triggered on master, so a risky change
could not be proven against the bootstrap chain before merging — and a bad
commit on master cannot be removed without a force-push on a public repo.
Trigger the workflow on push to ci/**, feature/** and bootstrap/** branches
(plus master and the existing workflow_dispatch). Branch runs are
VALIDATE-ONLY:
- The carry-forward cache (stage + save steps) is written ONLY on
refs/heads/master, so a branch never overwrites master's resume anchor and
a broken branch's -pre binary can never leak into a later master run.
- Branch runs still restore the cache read-only, so a branch off recent
master resumes from master's latest good -pre and replays only its own new
commits; a diverged branch falls back to the cold-start anchor as before.
- The -pre artifact upload still runs on branches (keyed by commit SHA, so it
never collides with master's and is pull-only).
concurrency is already keyed on github.ref, so per-branch runs don't cancel
each other or master.
Note: this only takes effect once pushed, so like the other Phase 4 CI
changes it cannot be exercised until the v0.12.0 cut.
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).
EmitSretCall (the sret path for free-function record returns) pushed one
stack slot per LOGICAL argument, so an interface argument — a fat pointer
that occupies TWO integer-register slots (obj + itab) — had its second slot
dropped. The arguments landed in the wrong registers and the stack was
left unbalanced, crashing. This is the free-function sibling of the method
fix in 077f13b (EmitMethodSretCall).
Fix: in EmitSretCall's <=5-slot register path, push interface args as two
slots (obj then itab, via EmitMethodArgPush for the direct case and the
saved fat pointer for the hoisted akIntfConsume case), track bytes pushed so
the hoisted-value reloads stay correct, and pop one register per SLOT. The
register-vs-spill guard now uses a new SretUserSlots helper that counts
interface params as two. The >5-slot overflow path raises a precise error
for the (not-yet-supported, untested) interface-in-overflow combination
rather than silently mis-placing registers.
Surfaced by Numerics.Money's rounding overloads: MoneyFromStr(amount,
currency, IRoundingStrategy) is a free function returning a record (TMoney)
with an interface argument, which hit this path.
Regression tests in cp.test.e2e.recordret.pas cover a free function
returning an sret record with an interface arg, with and without a leading
scalar arg, via AssertRunsOnAll (QBE + native parity).
Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite
green on both the QBE-built and native-built runners (3686 tests).
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).
EmitRecordCopy copied every non-managed scalar field with loadw/storew (or
loadl/storel), ignoring the field's actual width. A sub-word field —
Boolean or Byte (1 byte), SmallInt or Word (2 bytes) — was therefore
stored with a 4-byte storew that overran into the following field. When a
Boolean sat immediately before a managed (string / class / dynarray) field,
the wide store clobbered the low bytes of that pointer; the subsequent
_StringRelease / _DynArrayRelease then dereferenced a garbage address and
crashed.
This is exactly TDecimal's layout (FNegative/FInflated Booleans ahead of a
carrier with a managed field), so it blocked Numerics.Money on QBE:
MakeMoney rounds into a TDecimal local and stores it into TMoney.FAmount,
copying the record by value. The native backend was always width-correct.
Fix: route scalar field copies through LoadInstrFor/StoreInstrFor (the same
width-correct helpers already used for element and variable loads), and copy
inline aggregate fields (static array / jumbo-set bitmap) with memcpy.
Regression test in cp.test.e2e.records.pas reproduces the TDecimal-shaped
layout (Boolean flags before a string field, copied through an sret Result
field) and asserts QBE/native parity via AssertRunsOnAll.
Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite
green on both the QBE-built and native-built runners (3625 tests).
A record-returning method that takes an interface parameter crashed (sret
return) or produced garbage (register return) on the native backend. An
interface is a fat pointer (obj + itab) occupying TWO integer-register
slots, but EmitMethodSretCall popped one register per LOGICAL argument
instead of per ABI slot. The interface's second slot was never consumed:
the user arguments landed in the wrong registers, and in the sret case a
stack slot was left unpopped, unbalancing the frame and corrupting return
addresses (the observed 0x1 / 0xffffffffffffffff backtrace).
Fix: in both EmitMethodSretCall register-passing paths (register-return
record and sret-return record, <=4-arg form), pop CountArgSlots(Params)
registers — counting interface params as two — into the integer registers
after sret/Self, rather than ACall.Args.Count. The <=4 register-path
guard is likewise changed to gate on slot count (UserSlots + 2 <= 6), so
an interface arg cannot silently overflow the register file.
This is the root cause of the TDecimal.Divide / RoundTo / SetScale native
crash: each takes a `const IRoundingStrategy` (interface) and returns a
record. With the fix, the full division/rounding battery — all eight
rounding modes, banker's rounding, custom IRoundingStrategy, rmUnnecessary,
the arbitrary-precision inflated path, and div-by-zero — runs identically
on native and QBE.
Tests:
- cp.test.e2e.recordret.pas: three new regression tests for an interface
argument to a record-returning method (sret return, register return,
and a scalar-before-interface ordering check), via AssertRunsOnAll.
- cp.test.e2e.numerics.decimal.pas: the 14 Divide/RoundTo/SetScale/float
e2e tests that were CompileAndRunWithRTLQBEOnly now run dual-backend
(CompileAndRunWithRTL), giving native parity coverage for the whole
decimal arithmetic surface.
Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite
green on both the QBE-built and native-built test runners (3624 tests).
A method call whose receiver is itself a record-returning call —
A.Plus(B).Val() — crashed on the native backend. The receiver
expression was lowered with EmitExprToEax, leaving the record VALUE
(the register-return payload, or the first bytes of the sret buffer) in
%rax, which was then used directly as the Self POINTER. Dereferencing
that garbage segfaulted. QBE handled the chained form correctly, so the
dual-backend e2e harness flagged it as a native/QBE parity divergence.
Fix: when a record method's receiver is a record-returning call,
materialise the call result into a stack buffer and pass its ADDRESS as
Self. The buffer address is carried in callee-saved %rbx so it survives
the argument push/pop sequence; both are freed after the call. Applied
to both EmitMethodCallExpr paths (<=6 and >6 arg slots) and all four
receiver sites in EmitMethodSretCall.
For the sret path, a forwarded %rsp-relative destination (the nested
chain A.Plus(B).Plus(A), where EmitRecordCallSretAt forwards '(%rsp)')
is resolved to an absolute pointer in callee-saved %r14 BEFORE the
receiver-materialisation prologue moves %rsp, so the destination no
longer drifts.
Regression tests in cp.test.e2e.recordret.pas cover the register-return
chain, the sret-record chain, the >5-arg overflow path, the nested
double chain, and the managed-field (TDecimal-shaped) record — all via
AssertRunsOnAll (QBE + native parity).
Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full
suite green on both the QBE-built and native-built test runners
(3621 tests).
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).
Compiling a UNIT (top source is a `unit`, so the pipeline runs in unit-mode and
Prog stays nil) via the default incremental path segfaulted: the incremental
worker setup unconditionally read `Prog.SymbolTable` to seed each dep worker,
dereferencing nil. In unit-mode the symbol table comes from the semantic pass,
not a program node. This is exactly the invocation the runtime Makefile drives
(`blaise --source X.pas --output X.o` per RTL unit), so it broke `make` in
runtime/ once incremental became the default; it was a latent pre-existing bug
in the opt-in incremental path (reproduces at de0ea5d with --incremental).
Fix: seed the worker symbol table from Semantic.GetSymbolTable() when Prog is
nil, matching the non-incremental unit-mode path.
Also pin the runtime Makefile to --no-incremental. That Makefile is itself a
hand-managed separate-compilation system (one object per unit + an explicit
archive list); the compiler's incremental mode would additionally write
per-dependency side-effect objects and skip inlining a used unit's bodies, but
those side-effect objects are not in the archive list — so a cross-unit symbol
(e.g. typeinfo_TRtlPlatform, referenced by the derived TRtlPlatformPosix in a
different unit) was left undefined at link. Whole-program per unit keeps each
unit object self-contained.
Regression test (cp.test.e2e.sepcompile.pas): compile a unit that uses another
unit in unit-mode via the default incremental path, then build and run a
program over the emitted object.
A float (Double/Single) by-value argument passed to a function or method that
returns a record with a managed field (dynamic array / string, i.e. the true
sret return path) was mis-handled by the native backend: every argument was
materialised via EmitExprToEax, the integer-only emitter. A float LITERAL hit
"unsupported expression form TFloatLiteral", and a float VARIABLE would have
been routed through %rax into an integer argument register instead of an %xmm
register — wrong per the SysV ABI. QBE compiled these correctly, so it was a
native/QBE parity divergence.
This blocked DecFromFloat / DecFromFloatExact (TDecimal has a dynamic-array
field) and any MoneyFromFloat-style constructor in Numerics.Money.
Fix: both EmitSretCall (free functions) and EmitMethodSretCall (methods) now
detect a float-typed by-value argument and, when present, evaluate arguments
into stack slots and load them into registers per the SysV ABI — integer args
into the integer argument registers (after %rdi=sret and, for methods,
%rsi=Self), float args into %xmm0.. with an independent counter, and %al set to
the vector-register count. The pure-integer path is unchanged, so the common
case (and native self-compile) is byte-for-byte identical.
Adds three e2e regression tests (cp.test.e2e.recordret.pas), run on both
backends via AssertRunsOnAll: single float arg to a managed-record-returning
function, two float args, and a single float arg to a managed-record-returning
method.
Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; the new tests
pass on both the QBE and native arms.
Incremental compilation (per-unit .o emission with embedded .bif) is now the
default for every build; --no-incremental opts out and builds a single
whole-program object. The old --incremental opt-in flag is removed.
Fix a latent incremental-mode bug the flip exposed: in incremental /
--skip-dep-codegen mode the dependency unit BODIES are compiled into their own
objects and AppendUnit is skipped for them in the main program codegen. But
AppendUnit was also where a unit's initialization section got registered into
the init-call list, so the program startup ($main) emitted no
`call <Unit>_init` for any dep — every dependency's initialization section
silently never ran. This affected BOTH backends and was why a native-built
TestRunner (built incrementally by default) registered zero tests: each
cp.test.* unit's `initialization`/RegisterTest never executed.
Fix: add ICodeGen.NoteDepInitUnit(name, hasInit), implemented on the QBE
backend and the native backend (virtual on TNativeBackend, overridden by the
x86_64 backend), which records a dep unit's init symbol in dependency order
without emitting its body. The driver calls it for each skipped dep in
incremental mode, so $main emits the calls and they resolve against the
<Unit>_init symbols exported by the per-unit objects. Verified across the full
matrix (native/qbe x incremental/--no-incremental), including chained
multi-unit init ordering (a dep that uses another dep sees the other's init
having run first).
Guard the incremental block so it never runs in --emit-ir / --emit-asm /
--dump-ast modes (those produce stdout text and no objects); this keeps
fixpoint.sh, fixpoint-native.sh and rolling-bootstrap.sh — all of which use
--emit-ir/--emit-asm — unaffected.
Tests: replace the --incremental e2e test with one that exercises the default
incremental path (no flag), plus a new --no-incremental test asserting the
whole-program path leaves no per-unit .o behind.
Remove the eight TestCodegen_Win64_* record-return IR tests and the GenIRWin64
helper. They pinned aspirational QBE Win64-ABI IR for a target the native
backend does not support (only linux-x86_64 is implemented) and that is being
deprecated along with QBE. The native backend miscompiled the in-process
GTarget-global Win64 path these tests drove; rather than carry IR tests for an
unsupported, unrunnable, soon-to-be-removed target, they are dropped. Native
Win64 will be addressed when Windows support is actually built.
Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite
3534 tests / 0 failures on both the QBE-built and native-built test runners.
Phase 3 of the native-default drive — the locally-verifiable items (CI and
rolling-bootstrap changes are deferred to Phase 4, where they become correct
once v0.12.0 is the rolling-bootstrap anchor):
- Help text now marks the backends explicitly: "qbe (deprecated) | native
(default)". QBE is retained as the e2e parity oracle and QBE-fixpoint guard;
removal is a later release.
- fixpoint-native-internal.sh now drives the FULL internal pipeline (internal
assembler + internal linker) against an all-external (gcc assemble + gcc link)
reference, instead of only differing on the assembler. Both internal tools are
now the default toolchain, so the conformance guard must exercise both. Header
comments updated to reflect that it guards the internal linker too and why it
stays a differential probe (the internal assembler buffers the whole object in
memory and OOMs on the compiler's own ~631k-line .s).
- language-rationale.adoc records the decision: native backend + internal
assembler + internal linker as the default toolchain (no external tools beyond
the C runtime startup objects), why QBE is kept-but-deprecated, and the
per-invocation escape hatches (--assembler external, --linker external,
--backend qbe, --incremental).
Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite
3541 tests / 0 failures on both the QBE-built and native-built test runners.
The native dynamic-array SetLength lowering loaded the array's current data
pointer into %rdi (the first argument to _DynArraySetLength) BEFORE evaluating
the new-length expression. When that expression itself emitted a call — most
commonly SetLength(Result, Length(A)), where Length(A) lowers to a
_DynArrayLength call — the call clobbered %rdi, so _DynArraySetLength resized
the wrong array (the length-expression's operand) instead of the intended one.
The visible symptom: a function taking a dynamic-array parameter and returning
a dynamic array had its Result aliased to the first array parameter's buffer.
SetLength(Result, Length(A)) resized A instead of Result, every Result[I] write
was lost, and the caller got back the first argument unchanged. QBE was correct
throughout; this was a native-only parity divergence.
Fix: evaluate the length expression into %esi first, then load the array
pointer into %rdi immediately before the call — matching the safe ordering the
string and field-receiver SetLength paths already used.
Adds five e2e regression tests (cp.test.e2e.dynarray.pas) covering the result-
from-param-length case, the const-array-param subtract, global-from-global
length, local-from-local length, and length-from-a-user-function-call. All run
on both backends via AssertRunsOnAll; the native arm would have failed before
this fix. This closes a test-coverage gap: no existing test exercised a
SetLength whose length argument emitted a call.
The internal assembler had two TLS-related bugs that caused undefined
symbols in .o files compiled from runtime units using threadvars:
1. sym@tpoff(%reg) operands: the parser included '@tpoff' as part of
the symbol name instead of recognising it as a relocation modifier.
Fixed by stopping symbol-name accumulation at '@', detecting the
'tpoff' suffix, and setting a TpOff flag that emits R_X86_64_TPOFF32.
2. %fs:0 (thread base read): the parser fell through to symbol parsing
and created an undefined reference to literal "0". Fixed by checking
for a numeric displacement before attempting symbol parsing in the
%fs: handler.
Also fix the --help text to show 'native (default)' instead of
'qbe (default)' now that the native backend is the default.
The native backend now uses --assembler internal and --linker internal
by default when the user does not explicitly pass these flags. This
eliminates the dependency on external tools (gcc/as/ld) for native
backend compilation.
Also fixes a linker bug where writing to a currently-executing binary
on Linux would fail with EStreamError — the linker now calls
DeleteFile before creating the output file.
The internal assembler had two bugs in .ascii directive parsing:
1. Comment stripping treated '#' as a comment start even inside quoted
strings, silently truncating string literals containing '#'. This
corrupted .rodata for any program with '#' in string constants
(e.g. QBE IR format strings like "# String literals").
2. The \xHH hex escape syntax was not handled. Bytes encoded as \xE2
etc. were emitted as literal ASCII 'xE2' (3 bytes) instead of the
single byte 0xE2, corrupting UTF-8 string data.
Both bugs were invisible when using the external assembler (gcc) and
only manifested when --assembler internal was used on large programs
with non-ASCII string literals or '#' characters in strings.
Four tests exercise the full compiler-driver pipeline with
--backend native --assembler internal --linker internal:
HelloWorld, StringOps (Format/UpperCase/IntToStr), exception
handling (raise + try/except), and virtual method dispatch.
These catch regressions in TLS relocation, CRT discovery,
duplicate-symbol handling, and PIE emission.
Add --linker internal|external option to the native backend driver,
routing the link step through TLinker instead of shelling out to cc.
FindCrtObjects discovers CRT startup objects (Scrt1.o, crti.o, crtn.o,
crtbeginS.o, crtendS.o) on Debian and RedHat layouts.
Fix SHF_TLS constant from $200 to the correct ELF spec value $400 in
both the reader and writer — the wrong value caused .tbss sections to
be placed twice (once in the normal NOBITS loop, once in the TLS
section), producing doubled negative TPOFF32 offsets and segfaults.
Fix FTlsAddr to use the post-alignment address from PlaceSection rather
than the pre-alignment cursor, which caused off-by-padding TPOFF values.
Change duplicate global symbol handling from error to first-wins
semantics (matching ld behaviour), needed because the main program
object and archive members may both define system runtime functions.
Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK,
3532 tests pass on both QBE-built and native-built test runners.
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.
The native x86-64 backend is now the default (bkNative). QBE remains
available via --backend qbe. Several codegen bugs that were invisible
while QBE was the default are fixed in this commit:
Native backend fixes:
- EmitLeaqGlobal helper: threadvar globals now emit the correct
fs:0 + @tpoff TLS sequence instead of bare %rip-relative leaq.
Fixes static array write (FreeLists), string subscript assignment,
EmitExprAddr, record memcpy, method-ptr cast, and open-array push
paths that all used raw leaq %s(%rip).
- EmitIncDec: unified local/global paths through VarOperand so
threadvar scalar Inc/Dec (e.g. LargeFreeCount) gets TLS addressing.
- EmitIncDec FAE.Base: Inc(P^.Field) where P is a pointer now
evaluates the base expression instead of leaq on the stack slot.
- FinalizeEmit: data section (.data/.bss/.rodata) now emits once at
the end of unit compilation via FinalizeEmit, not inside each
EmitUnit call. Fixes duplicate/missing string literals in sep-compile.
- EmitFunctionDef AExported: implementation-only helpers in monolithic
mode stay file-local (.globl suppressed) to avoid symbol collisions
with the RTL archive.
QBE backend fixes:
- AppendUnit typeinfo: export prefix added to data $typeinfo_TObject,
$typeinfo_TCustomAttribute, $vtable_TObject, $vtable_TCustomAttribute
so native-built RTL can resolve them.
- FSuppressSystemDefs: decoupled from FExportAll so CreateUnitCodeGen
can export all symbols without suppressing system defs.
Infrastructure:
- runtime/Makefile: BLAISE_FLAGS variable for passing extra compiler
flags (e.g. --backend qbe) from build scripts.
- fixpoint.sh: make clean before RTL rebuild to pick up backend change.
Flip the compiler's default --backend from qbe to native. Native has full
feature parity with QBE (the v0.11.0 cycle's 116 fixes plus the dual-backend
e2e harness brought them level), self-hosts, and gives no external codegen
dependency, source-level debugging, and a path to cross-compilation — so it
is the right default to exercise everywhere.
QBE stays fully supported via --backend qbe and remains the cold-bootstrap
root: --emit-ir still routes to QBE regardless of the default (PickTopDriver
ignores --backend for emit modes), so scripts/rolling-bootstrap.sh,
fixpoint.sh, and CI — all of which pass --emit-ir explicitly — are unaffected
by this change. The only behaviour change is that a plain
'blaise --source X --output Y' (and a plain 'make' in runtime/) now uses the
native backend.
Flipping the default surfaced and fixed a real native gap first (the
unit-to-.o duplicate-system-defs link collision, previous commit); the full
suite is green with the TestRunner built by BOTH a QBE-built and a
native-built compiler, and all three fixpoints pass.
Compiling a unit to a standalone .o with the native backend
(blaise --source U.pas --output U.o) emitted the built-in system
definitions — typeinfo_/vtable_/_FieldCleanup_ for TObject and
TCustomAttribute — into the unit .o. Linking that .o against a consumer
program (which emits its own copies) failed with
'multiple definition of vtable_TCustomAttribute'.
Two gaps fed this:
- The unit-as-top-level path in Blaise.pas built its codegen via
Driver.CreateCodeGen (the program path, separate-compile = False) instead
of Driver.CreateUnitCodeGen (separate-compile = True), so the backend's
FSeparateCompile gate (which already suppresses the system defs) never
engaged. Switched the IsUnitMode branch to CreateUnitCodeGen, with a
CreateCodeGen fallback for any backend lacking a unit codegen.
- TCodeGenNative.GenerateUnit did not propagate FSeparateCompile to the
backend (AppendUnit already did); added the missing propagation for
defensive correctness.
The QBE backend was unaffected (its unit path already suppresses these).
Adds TestNativeBackend_RoundTrip_NoDuplicateSystemDefs in
cp.test.e2e.sepcompile, which pins --backend native so the guard holds
regardless of the compiler's default backend.
EmitByteRhsToEax lowered a single-character string-literal RHS via
Ord(TStringLiteral(AExpr).Value[0]). That subscript-then-Ord idiom is
miscompiled by the self-hosted native stage: a native-built compiler baked a
pointer-sized garbage immediate (movl $0xd291f9ec) instead of the byte, so
S[i] := 'c' wrote junk. It only surfaced in CI, where the test runner is
built by the native-bootstrapped -pre binary; local verification used the
QBE-built fixpoint binary, which lowers the idiom correctly, so the bug was
invisible.
Use StrAt(Value, 0) — the OrdAt-backed byte-read helper already used
elsewhere in this backend and stage-stable — instead of Ord(Value[0]).
Matches the QBE backend, which uses StrAt and was never affected.
Verified by building the test runner with a native-built compiler (the CI
configuration): full suite passes; all three fixpoints clean.
S[I] := <byte> was rejected by the semantic pass ('is not a static array
or dynamic array') for any string — local, global, or var-param — on both
backends. It is now supported as the symmetric counterpart of the S[I]
read: an in-place byte store into a 0-based UTF-8 string.
Because strings are reference-counted and literals are immortal (stored in
read-only memory; the native backend put them in .rodata, so a naive store
segfaulted, while QBE silently mutated the shared literal), the write
performs copy-on-write. New RTL helper _StringUnique(S) returns S when it
is uniquely owned (rc=1) and otherwise allocates a fresh rc=1 copy, releases
the old reference, and returns the copy. Both backends emit
_StringUnique -> write the result back to the slot -> storeb, so the slot
keeps exactly one owned reference and mutating one alias never disturbs
another (Delphi/FPC UniqueString semantics).
The RHS accepts a numeric ordinal, Chr(n), or a single-character literal
(the byte-shaped forms used in place of a Char type). PChar subscript
writes keep their existing in-place path (raw pointer, no ARC header).
Dual-backend e2e tests (write, copy-on-write aliasing + literal reuse,
var-param) in cp.test.e2e.stringops. Updates language-rationale.adoc
(writable subscript + COW) and grammar.ebnf (string as a subscript-assign
base).
$hex, %binary and &octal integer literals were rejected inside an
array-constant initialiser — 'array[0..1] of Int64 = ($1, $2)' failed
with 'Cannot resolve array-const element' while the scalar form '= $1'
worked. The parser stores array elements verbatim (prefix and all), and
ResolveConstArrayElem only recognised decimal digits, so any radix-prefixed
element fell through to an identifier lookup and errored.
Fold radix-prefixed elements (with optional leading sign) through
ParseIntLiteral — the same canonical parser the rest of the compiler uses
for scalar and typed consts — so all four radixes, plus underscore digit
separators, behave identically inside an array initialiser. Keeps FPC's
&-octal (the project prefers FPC syntax where FPC and Delphi diverge; the
lexer already tokenised all four radixes).
Documents the radix-prefix rule in language-rationale.adoc.
Implement the Phase B internal-linker pipeline (TLinker in
blaise.linker.elf) on top of the Phase A section merger:
- Symbol resolution: build the global symbol table from merged
sections (STB_GLOBAL over STB_WEAK; two globals = duplicate error),
define the five linker-synthesised symbols (_GLOBAL_OFFSET_TABLE_,
__bss_start, _edata, _end, __TMC_END__), resolve weak-undefined
references to 0 and raise on strong-undefined.
- Static relocations: patch R_X86_64_PC32 / R_X86_64_PLT32 as S+A-P
(PLT32 against an internal symbol relaxes to the direct form, no
PLT). Reject the dynamic-only forms (R_X86_64_64, GOT/TLS) and
absolute 32-bit symbol references with a diagnostic — deferred to
Phase C.
- Layout + emission: assign virtual addresses at a fixed base
(0x400000) in an executable run (.text + .rodata, sharing the first
page with the headers) and a writable run (.data + .bss) on a fresh
page; write a non-PIE ET_EXEC with two PT_LOAD program headers, the
relocated section payloads, and a section-header table for tooling;
mark the file executable.
Platform/arch-variable values (Is64, OSABI, EMachine, BaseAddr,
PageSize) live in TLinkTarget rather than being hard-coded, so FreeBSD
and a future i386 ELF target are a new target record and a PE/Mach-O
container would be a sibling writer behind the same symbol/relocation
core.
The linker is not wired into the compiler driver — it is a standalone
unit exercised only by tests; all backends still link via cc. Driver
integration behind --linker internal is Phase D.
Tests: TLinkerTests (symbol resolution, PC32 cross-object displacement,
R_X86_64_64 rejection, ELF-header/entry-point structure) and
TLinkerE2ETests (hand-written write+exit syscall object linked
internally to an ET_EXEC, executed, asserted on stdout + exit code;
readelf -a reports a clean statically-linked x86-64 executable).
Two native-backend codegen constraints were worked around in this unit:
no string-element assignment through a var-string parameter (append-
style byte building + PChar-local in-place writes), and no capturing
nested functions (private methods instead).
An `inherited Method(args)` whose Self plus arguments occupy more than the
six System V integer argument registers aborted native codegen with
"System V integer arg register index 6 out of range". EmitInheritedCallSeq
only implemented the all-in-registers case: it pushed every argument and
popped each into a register, walking past the six available once Self + the
args exceeded six slots. There was no stack-spill path like the regular
method-call and constructor emitters already have.
Give EmitInheritedCallSeq the same >6-slot handling: when Self + args exceed
six register slots, build the six-register prefix plus an overflow region in
one frame, place Self in slot 0, evaluate each argument into its slot, load
registers 0..5, drop the prefix so the overflow sits at the callee's
[rsp+0..], then static-dispatch to the parent. Up to six register slots
keeps the existing push/pop path. QBE was already correct.
This is independent of the call position and of metaclass dispatch — any
inherited call with seven or more register slots (e.g. a method or
constructor with six+ scalar/aggregate args calling its parent) was
affected.
Adds an e2e regression (cp.test.e2e.classes2) exercising an inherited call
with Self + 7 integer args (8 register slots), asserting correct dispatch
and result on every backend.
(cherry picked from commit b1fc1285ff3984eedaf6a68d58e2d568f1e4c793)
Calling a constructor through a metaclass-typed variable as a statement,
with the result discarded — `C.Create(args);` where `C: class of TBase` —
compiled cleanly but segfaulted at runtime on both the QBE and native
x86-64 backends. The same call in expression position (`o := C.Create();`)
worked.
The semantic pass flags the statement node with IsConstructorCall and
IsMetaclassDispatch, but only the expression-position lowering acted on
them (allocating via _ClassCreate and dispatching the ctor through the new
instance's vtable). The statement-position emitters had no such branch, so
the call fell through to the ordinary virtual-method path: it loaded the
metaclass value (a typeinfo pointer) as if it were the receiver object,
read a bogus vtable from the typeinfo's first word, and called a garbage
slot — crashing.
Add the metaclass-constructor branch to both statement emitters
(TCodeGenQBE.EmitMethodCall and TX86_64Backend.EmitMethodCallStmt): load
the metaclass, allocate via _ClassCreate, marshal the args, dispatch the
ctor indirectly through vtable[(slot+1)*8], then release the freshly built
(refcount 1) instance so the discarded result is freed rather than leaked.
A non-metaclass constructor as a statement (TFoo.Create();) is still
rejected earlier by the semantic pass as 'not a variable', so only the
metaclass shape reaches the new branch.
Adds an e2e regression (cp.test.e2e.classes2) covering zero-arg and
multi-arg dispatch and asserting the instance is freed (destructor count).
(cherry picked from commit 3f719213a838ab886b5db74847e7bef8894c404a)
Accept an anonymous integer subrange as a set base type, e.g.
'set of 0..255' or 'set of 1..10', in both the inline (var/param/field)
and 'type T = set of L..H' declaration positions. The subrange lowers to
the same bitmap machinery as 'set of Byte': the bitmap is sized to H+1
bits and member ordinals are the integer values themselves. Bounds may be
integer literals, named constants, or constant expressions; the lower
bound must be >= 0, the upper <= 255, and the range ascending.
Also fix 'set of Boolean' so a Boolean operand is accepted directly on
either side of 'in' and as the second argument of Include/Exclude, without
an intervening Ord().
Parser handles both 'set of' paths (ParseTypeName and ParseSetDef);
semantic resolution adds ResolveSubrangeSetType for on-demand and
type-decl paths. Adds dual-backend e2e tests and semantic unit tests.
Updates grammar.ebnf and language-rationale.adoc per the language-decision
rule, and removes the now-implemented item from future-improvements.adoc.
`set of byte` and `set of Boolean` are implemented (issue #105) — named ordinal
base types lower to a bit set (byte uses the 256-bit jumbo path), with working
membership/Include/Exclude and [lo..hi] range literals on both backends. The
section is rewritten to record that, leaving only the genuinely-remaining work:
* the anonymous integer-subrange base form `set of 0..255` / `set of 1..10`,
still rejected at parse time ("Expected enum type or '(' after 'set of'");
* a minor `set of Boolean` follow-on where `True in s` is rejected by the `in`
type-check (Ord(b) in s works).
The KI-3 ARC triple-release bug — multiple `on E: TYPE do` handlers in one
except block colliding on a single QBE slot — is fixed, so the workaround of
giving each handler a distinct variable name can be reverted. Handler vars are
now the natural `E` again in:
- blaise.testing.pas: the runner's 4-handler dispatch block
(EAF/EIT/E/ETO -> E) — the exact KI-3 scenario, in the test framework's
own outcome classifier.
- cp.test.attributes.pas: ETO -> E.
- cp.test.sets.pas: ESE/EEx -> E.
(There were no {$IFDEF FPC} blocks left to remove — the KI-2 .Free guards and
all other FPC conditionals were already gone from the source tree as part of
the FPC-independence migration; verified by a full-tree directive scan.)
All three fixpoints green; 3501 tests pass, including the framework's own
FAIL/IGNORED/ERROR outcome paths.
An empty bracket literal [] passed to a plain open-array parameter
(`array of T`) failed overload resolution with "No matching overload" on both
backends. A non-empty literal works because AnalyseArrayLiteralExpr infers the
element type and types it `array of <elem>`; an empty [] has nothing to infer
from and was left untyped (nil), so it matched neither the open-array param (nil
arg type) nor the existing set / array-of-const special cases.
Two changes in uSemantic:
* ArgMatchScore now scores an empty TArrayLiteralExpr as an exact (2) match
against any tyOpenArray parameter — an empty open array is valid for any
element type — placed before the nil-arg bail so overload selection accepts it.
* RetypeSetLiteralArgs pins the empty []'s ResolvedType to the chosen formal's
open-array type, so codegen emits a zero-length open array (data=nil, high=-1)
of the right element type.
Overload disambiguation is preserved ([] picks the open-array overload; a scalar
argument still picks the scalar overload), and two equally-matching open-array
overloads still raise a clean "ambiguous overload" error. Works for
`array of string`, `array of Integer`, and `array of const`.
This was the last open item in native-testrunner-investigation.txt; the native
TestRunner now passes the full suite unfiltered. Adds dual-backend e2e tests.
All three fixpoints green; 3501 tests pass.
A multi-dimensional array whose dimensions are enum types — array[TEnum, TEnum]
or array[TEnum, 0..1] — failed to parse with "Expected '..' but got ','". Only
the single-dimension form array[TEnum] was handled; every dimension in the
comma-separated multi-dim path was forced through the lo..hi range parser.
Both the const-array dimension parser (ReadConstArrayDim) and the type-name
parser (ParseTypeName) now recognise an enum-type dimension — a bare identifier
not followed by '..' — and encode it as the range 0 .. @TEnum, reusing the
existing single-dimension '@TEnum' convention. ResolveArrayBound resolves the
'@TEnum' high-bound marker to the enum's last ordinal (member count - 1). This
fixes const, type and var declarations in any dimension position and mix
(enum+range, range+enum, 3-D enum+range+enum).
Adds dual-backend e2e coverage: 2-D enum/enum, mixed dims in both orders,
type+var form, and a 3-D mixed case. All three fixpoints green; 3499 tests pass.
A typed set constant accepted only enum-member identifiers — `const C: TByteSet =
[1, 2, 3]` and `[1..3]` both failed with "Expected set member identifier". The
const-decl set path (a separate, more limited parser than the expression set
literal) is extended to accept:
* integer-literal members (1, 2, 3),
* inclusive ranges `lo..hi` (each endpoint an integer literal, enum member, or
named constant), expanded to all ordinals in range,
* and ordinal base types (set of Byte / Boolean), not just enums.
Parser stores each member verbatim (a range as 'lo..hi'); the semantic pass
(AnalyseSetConstDecl) resolves and expands them via a new ResolveSetMemberOrd
helper, integrating with the existing small-mask and jumbo byte-bitmap const
machinery. Enum-member const sets (the original form) still work.
Adds dual-backend e2e coverage including edge cases: mixed range+literal, a jumbo
(>64-member) set with a range, the empty set, descending-range rejection, and an
enum-set regression. All three fixpoints green; 3495 tests pass.
An array constant with Boolean (or enum, or named-constant) elements emitted the
identifiers verbatim — `(False, True, ...)` became symbol references, failing at
link time on native ("undefined reference to `False'") and in QBE ("unknown
keyword False"). And even once folded, every non-string element was emitted at a
fixed 4-byte stride that did not match the element's actual read width, so a
Boolean array read all-zero.
Two fixes:
* Semantic (AnalyseArrayConstDecls): a new ResolveConstArrayElem folds each
bare-identifier element to its numeric value once the element type is known —
Boolean True/False to 1/0, enum members and named integer/boolean constants to
their ordinal/value. Numeric/float literals pass through. An unresolved
identifier is now a clear semantic error instead of a link failure.
* Codegen (both backends): array-const element data is emitted at the element's
real width — .byte/.word/.long/.quad on native, b/h/w/l in QBE — instead of a
fixed .long/w. Built-in scalar names are mapped directly so the width is
correct even when the codegen has no symbol table set (the e2e in-process path).
Adds dual-backend e2e coverage including edge cases: Byte/Word/Int64 widths,
negative integer pass-through, named-const elements, and a Boolean array indexed
by an enum. All three fixpoints green; 3490 tests pass.
The native backend previously stubbed GenerateUnit and left
SupportsIncremental/SupportsWarmCache False, so --incremental fell back to QBE.
Native now drives the per-unit parallel pipeline itself:
* TCodeGenNative.GenerateUnit emits a single unit in isolation (new
TNativeBackend.GenerateUnit: clear buffer, EmitUnit, return assembly).
* The native driver overrides SupportsIncremental/SupportsWarmCache (True),
CreateUnitCodeGen (separate-compile mode), and LowerToObject (assemble the
unit .s to .o via the internal assembler or `cc -c -x assembler`).
* Separate-compilation mode (FSeparateCompile): unit objects omit the
once-per-program TObject/TCustomAttribute system defs — the program object
provides them — and emit their own file-local string-literal blobs, so a unit
method referencing a literal links cleanly. The system typeinfo/vtable symbols
(typeinfo_TObject, vtable_TObject, and TCustomAttribute) are now .globl so unit
objects can reference the program's single definition.
Works with both the external (`cc -c`) and internal assemblers, and the warm
cache (a second build reuses the per-unit .o). Adds a sepcompile e2e test
building a multi-unit class program with --backend native --incremental.
All three fixpoints green; 3485 tests pass.
A static array of interfaces (Arr: array[0..2] of IGreet) was unusable on the
native backend in three ways, all now fixed:
* Store (Arr[I] := V): the element is a contiguous 16-byte fat pointer, but the
generic static-subscript store wrote only the 8-byte obj and left the itab
garbage. Now computes the element address and delegates to
EmitInterfaceToFieldSlotsAt (obj+itab with ARC for nil/class/interface).
* Read as RHS (G := Arr[I]): EmitInterfaceAssign gained an interface-subscript
source case that copies obj+itab with ARC.
* Method dispatch (Arr[I].M()): EmitInterfaceCall gained a subscript-receiver
case that loads obj/itab from the element address.
A shared EmitIntfStaticElemAddr helper computes the element fat-pointer address
(base + (idx-LowBound)*16), reused by all three paths and by the interface-store
source. EmitInterfaceToFieldSlotsAt also learned an interface-subscript source
(Arr[J] := Arr[I]). Native now matches QBE exactly on the full test
(11/22/22/11/11). TestRun_StaticArrayOfInterface_FatPointer is dual-backend.
(A pre-existing scope-exit leak of interface array elements affects BOTH backends
identically — logged separately in bugs.txt, not a parity gap.)
All three fixpoints green; 3484 tests pass.
Show(MakeFoo(42)), where MakeFoo returns an interface, handed the borrowing
parameter an owned (+1) fat pointer that was never released after the call —
--debug reported one leaked instance on native (QBE already released it).
Added an akIntfConsume hoist kind: EmitArgHoist drives the interface-returning
call in the pre-pass and saves the owned fat pointer (itab then obj, obj on
top); the argument reload pushes both halves as the two interface arg slots; and
EmitHoistEpilogue releases the obj after the call (mirrors akStrConsume). The
2-slot reload is wired into both EmitArgPush and EmitCall's inline argument loop.
--debug leak report is now clean on both backends; two leakcheck tests (QBE +
native) are added. All three fixpoints green; 3484 tests pass.
DateAddDays(MakeDate(...), 5) segfaulted on native while the non-nested form
worked. The args arrived correct — the fault was a stack-alignment bug. The
argument hoist for a record-returning-call argument allocated a 16-byte-aligned
sret buffer (RecArgBufBytes) and then pushed an 8-byte pointer to it, a 24-byte
contribution that left %rsp 8 bytes off 16-alignment. The misalignment was
harmless until the callee chain reached an aligned SSE access — here libm's
mktime (via DateUtils _TimeJoin) executing `movdqa`, which faults on a
misaligned address.
EmitArgHoist now reserves a full 16-byte slot for the saved pointer (subq $8 +
pushq) so the hoist region stays a multiple of 16. The pad sits above the saved
pointer, so reload offsets are unchanged. This is the shared hoist, so it fixes
the alignment for every record-call-argument site, not just DateAddDays.
The DateAddDays e2e test is re-enabled on both backends; a self-contained
regression (record-call result as a record-by-value arg feeding an SSE op) is
added to the gaps suite. All three fixpoints green; 3482 tests pass.
A class-level const array (const Days: array[0..6] of string = (...)) read via
an instance, T.Days[I], returned an empty string on native. The semantic pass
folds the subscript into the field-access node — IsConstant=True with
ConstArraySymbol (the global data label), ConstArrayType (the static-array type)
and PropIndexExpr (the index) — but native's EmitExprToEax IsConstant branch only
handled scalar string/int consts and fell through to an empty string literal.
Added the class-const-array element path: compute
base + (idx - LowBound) * ElemSize and load the element by type (float / string
pointer / narrowed integer), plus a bare-reference case returning the label
address. Mirrors the QBE EmitExpr ConstArraySymbol path. The range- and
enum-indexed class-const-array e2e tests are converted to AssertRunsOnAll.
(Static-array-of-interface element store/read/dispatch remains a separate, larger
follow-up — tracked in bugs.txt; that test stays QBE-only meanwhile.)
All three fixpoints green; 3481 tests pass.
Native lowered Round() with cvtsd2si/cvtss2si, which honour the FPU rounding
mode (round-half-to-even / banker's): Round(2.5)=2, Round(-2.5)=-2. QBE — and
Delphi/FPC — round half away from zero via C99 round(): Round(2.5)=3,
Round(-2.5)=-3. Native now calls round()/roundf() first, then truncates the
integral result, matching QBE exactly. The two Round e2e tests are converted to
AssertRunsOnAll.
All three fixpoints green; 3481 tests pass.
The native backend raised "TMethodCallExpr has no ResolvedMethod" for the two
TObject built-in methods that the semantic pass flags rather than resolving to a
declared method:
* InheritsFrom(C): load the receiver's typeinfo (vtable[0] for a class instance),
evaluate the class-of argument to a typeinfo pointer, and call
_InheritsFrom(self_ti, arg_ti) → Boolean.
* ToString: virtual dispatch through vtable slot 2 (offset 16: typeinfo, Destroy,
ToString), returning the string in %rax — the default returns the class name.
A small EmitMethodReceiverToRax helper loads the instance pointer (expression,
named local/global, or implicit Self). The 7 InheritsFrom tests and the default
ToString test in cp.test.e2e.classes2 are converted to AssertRunsOnAll, pinning
both builtins on QBE and native.
All three fixpoints green; 3481 tests pass.
StrToDouble was lowered only in the integer EmitExprToEax path: it called
_StrToDouble (which returns the Double in %xmm0 per the System V ABI) and left
%rax untouched, so a float assignment (D := StrToDouble(S)) read %rax — a
leftover pointer — and cvtsi2sd'd it into garbage. Native printed values like
95693543141392 instead of the parsed number. Added StrToDouble to the float
emitter (EmitFloatBuiltin) so the result is consumed from %xmm0.
The 7 StrToDouble e2e tests are converted to AssertRunsOnAll, pinning the fix on
both backends. The blanket dual-backend flip of the inline CompileAndRun helper
is documented as deliberately reverted: it breaks on non-deterministic programs
(GetProcessID) and several genuine native gaps remain open (InheritsFrom/ToString
builtins, Round .5-boundary, class const-array, interface-field-assignment RHS,
EmitSretCall record-value arg) — all logged in bugs.txt for individual fixes.
Suites migrate to AssertRunsOnAll as those gaps close.
All three fixpoints green; 3481 tests pass.