← board

pasmith: fuzz the {$Q+}/{$R+} check machinery

Why

The checks were hand-oracle-verified shape by shape, and TWICE the ticket's assumed semantics were wrong until probed (FPC does NOT check Abs/Sqr or subword ops). A generator that sprinkles {$Q+}/{$R+}/{$Q-}/{$R-} regions over its existing arithmetic/array/subrange rungs and counts caught EIntOverflow/ERangeError per checkpoint would differentially pin the whole semantic surface against FPC — including the statement-anchor timing rule (trange4) that a directive between RHS and statement end must not retro-apply.

Sketch

2026-08-16 — DONE. --checks, and it is in --wide

The semantics were MEASURED before a line of the rung was written

The ticket's own history is why: the checks were hand-verified shape by shape and twice the assumed semantics were wrong until probed. So the same discipline applies to building a generator on top of them — a rung built on a deliberate divergence is a noise generator, which is exactly what [[bug-t-fpc-probe-reports-the-deliberate-shl-deviation-as-new]] had just finished being.

pxx vs FPC 3.2.2, seven shapes, all agreeing:

shape both compilers
a + b, a - b, a * b overflow (longint) ERangeError
nested (a * b) + c ERangeError
implicit narrowing smallint := longint ERangeError
array read out of bounds ERangeError
explicit cast smallint(a), byte(a) silent, no raise
non-overflowing arithmetic silent

Two things worth writing down. It is ERangeError, not EIntOverflow, that arrives for arithmetic overflow — so the handler counts BOTH classes rather than assuming, since which one arrives is itself part of what the rung pins. And pxx does honour {$R+} on array indexing, which is what makes an unclamped index safe to generate: had it not, the program would read out of bounds instead of raising, and a fuzzer that can corrupt memory is not measuring anything.

The rung

--checks N adds a checked statement kind:

{$Q+}{$R+}
try
  g0 := (g0 + pv1^.r1n.r1i1) - r0g.r0n.r0i0;
  Mix(int64(g0));
except
  on E: EIntOverflow do Mix(7001);
  on E: ERangeError do Mix(7002);
  on E: Exception do Mix(7099);
end;
{$Q-}{$R-}

Two shapes: arithmetic (+ - *, sometimes nested one deep) into a live target, and an unclamped array read. The caught class folds into the checksum, so a divergence localises like any other checkpoint — and it is tagged checked, so it signs distinctly in the ledger.

No shifts inside a checked region, deliberately. pxx computes shifts at native width by design (2026-08-11), so a shl overflowing 32 bits raises under FPC and not under pxx — a guaranteed false divergence. That is why the rung builds its own operand expression instead of calling self.expr(), which can emit shifts, SafeDiv and casts.

{$Q-}{$R-} restores rather than pops: pasmith never enables these globally, so that is the correct outer state.

In --wide, and why that was the point

Folded into WIDE_DEFAULTS at 1 after it ran clean. The two rungs excluded from --wide (--intfs, --mptrs) are excluded because they diverge on ~100% of seeds against known filed bugs; this one has no known divergence, and leaving it out would mean nothing ever runs it — every real slice uses --wide. The None-vs-0 discipline is preserved: --wide --checks 0 still turns it off.

Doing that exposed that the two --wide implementations — pasmith.py's and pasmith_run.WIDE_DEFAULTS — are separate lists that had already drifted. Both updated, with a note on each pointing at the other.

A pre-existing bug this surfaced, fixed alongside

[[bug-t-pasmith-gen-args-header-omits-half-the-rungs]] — the self-describing gen-args: header listed 13 knobs against the constructor's 21, four of the missing ones set by --wide. localize() rebuilds the subject from that header, so on every wide seed it was tracing a different program (1500 lines against the original's 1770). The new knob would have inherited the same defect, which is how it was found. Header now generated from one list; round-trip verified byte-identical on five seeds.

Verified

Log