← board

Measured 2026-09-06, compiler 2b936bf5b43f (commit e8bbdae43)

Two units, one body, differing only in generic:

unit ugr;  { plain and generic side by side, same method text }
{$R-}
procedure TPlain.test; begin l := 1234; {$R+} field := l; {$R-}  end;
procedure TGen.test;   begin l := 1234; {$R+} field := l; {$R-}  end;
program {$R-}, calls TPlain.test   -> Runtime error 201     CORRECT
program {$R-}, calls TGen<byte>    -> "gen stored 210"      WRONG (check dropped)
program {$R+}, calls TGen<byte>    -> Runtime error 201     right answer, wrong reason

The third line is the tell: flipping a directive in the program changes whether a check inside a library body fires. The body's own {$R+} is never consulted. The inverse confirms it from the other side — ugr2, whose body is entirely under {$R-}, raises 201 when specialized under {$R+}.

Where

lexer.inc ShiftTokParallel fills the inserted gap from src := atPos - 1, i.e. the splice site. Its comment already states the limit:

"A caller that HAS real spans cannot say so through this procedure, so it does not try to: the specializer overwrites these two slots itself, right after this shift."

pasparser_generic.inc SpecializeStreamAt does that overwrite for TokSrcOff/TokSrcLen (~line 790) and for nothing else. The nine positional directive states — TokPackRecords, TokQChecks, TokAssertions, TokRChecks, TokIChecks, TokNChecks, TokScopedEnums, TokPackEnum, TokHMinus — are owed the same carry and never got it. Per-token, not per-template: ugeneric7 flips {$R} twice inside one method.

Capture the nine states in CaptureTemplateTokenFrom (~line 873, beside the existing TemplateSrcOff capture) into nine Template* arrays; give the spec buffer one new column, SpecTmplIdx[i] = the originating template index, set at the same ~5 sites that already set SpecSrcOff; have SpecializeStreamAt copy the nine from Template*[SpecTmplIdx[i]]. Substituted tokens keep their template index (the state is about the source POSITION, which survives substitution); only genuinely synthesized tokens take -1 and fall back to ShiftTokParallel's lexical answer, which is correct for them.

Sequencing, and a trap that decides it (frankH, relayed 2026-09-06)

MAX_TEMPLATE_TOKENS is the small end of the conversion problem, not the MAX_IR end: six arrays in two parallel trios, two cap sites, ten refs of which eight are declarations. So converting it is one Ensure growing six arrays — and these ten columns then become ten SetLengths inside it rather than ten more fixed tables to keep in lockstep by discipline. Land this AFTER that conversion, not before.

The trap, and it is why "after" is not merely tidier: the Specialize trio has NO cap site of its own. It is bounded implicitly through the Template pool's — SpecializeToBuffer runs while i < count with subCount advancing at most once per iteration, so subCount <= count <= MAX_TEMPLATE_TOKENS with no guard anywhere. Converting the Template trio ALONE therefore silently un-bounds the Specialize trio: count can exceed 65536, six arrays get written at subCount, nothing fails, and the symptom surfaces somewhere else entirely. All six move together, with an explicit Spec guard added in the same change. Anyone adding columns here inherits that constraint. See [[feature-dynamic-compiler-tables]].

The blocked-by edge was added later, and its absence is the finding (frank-coordinator, 2026-09-06). This ticket carried blocked-by: [] while its own body said "Land this AFTER that conversion, not before" and gave a mechanism — the implicitly-bounded Specialize trio — for why the order is load-bearing rather than tidy. Sequencing stated only in prose is invisible to every instrument that ranks work. ready/next would have handed this out as unblocked; the ranker would not have carried its prio up to the conversion that gates it; and the one reader who needs the constraint is whoever picks it up without reading down this far. The edge now says it. Class filed as [[bug-t-a-ranked-ticket-that-blocks-itself-in-prose-is-invisible-to-every-check]] — tools/progress.py check has no matcher for "land after"-shaped language, so nothing flags the gap.

Ten new columns, and that is the reason this is ranked rather than landed: MAX_TEMPLATE_TOKENS pools are still fixed array[0..N-1] tables, and [[feature-dynamic-compiler-tables]] (frankH) is actively removing fixed tables from defs.inc — adding ten worth ~2.6 MB of BSS into that lane wants its owner's sequencing, not a unilateral landing. Do not use a record for the nine: a new record type in the compiler's own source risks the CheckBuiltinRecSize wall that makes TGenericFunc unmodifiable.

What it buys

tgeneric7.pp in the fpc-testsuite corpus (skip reason already rewritten to this mechanism, e8bbdae43). The {$R} face is the one with a corpus row; the other eight states are the same defect and have no test asserting them either way.