← board

The cap and the kind arrive by different roads, and only a window joins them

The two sites

{ pasparser_expr.inc:3663 -- reads the global AT USE }
szDeclTk := ParseTypeKind;
if (szDeclTk = tyRecord) and (LastTypeRecId <> REC_NONE) then
  prevTok := RecSize(LastTypeRecId)
else
  prevTok := SizeOfSlot(szDeclTk, LastTypeStrCap);

{ pasparser_decl.inc:1033 -- CAPTURES into locals first, which is the safe form }
fileElemTk  := ParseTypeKind();
fileElemRec := LastTypeRecId;
fileElemCap := LastTypeStrCap;

The second is what discipline looks like and nothing makes it the rule. Both read three globals that ParseTypeKind set as a side effect; the first reads them at the point of use, the second within three lines of the call.

Why this is not the other four routes

route pair comes from checkable at the call site?
1 same record one Syms[i] / Alias*[ai] yes — a mismatch is not expressible
2 same lookup pair two arrays, one index yes
3 paired resolver one function returning both yes
4 parameter + global a call, and a global channel no
5 state machine the parse's own history no, but it has ONE owner

Route 5 at least has a single owner for its history. Route 4's invariant has none: the setter is ParseTypeKind, the reader is any caller, and the contract between them is a comment in defs.inc.

The fix that already worked once

Not a gate row and not a tighter window — a dedicated carrier, which is what ArrTypeElemStrCap is and why it exists. The same move applies here: have ParseTypeKind return the cap (or fill a small record) rather than leave it in a global for the caller to pick up in time.

Done when

SizeOfSlot's companion is reachable from the same expression that produced its kind at all thirteen call sites, so route 4 is empty and the taxonomy's unmechanisable half is routes 5 only.

Done — 2026-09-09, and the fixture's own controls are the finding

ParseTypeKindSized(var recId, strCap): TTypeKind sits beside ParseTypeKind in pasparser_decl.inc and returns all three from one call. Both route-4 sites now take it (pasparser_expr.inc's SizeOf type-name arm, pasparser_decl.inc's file of element), so route 4 is empty and the taxonomy's unmechanisable half is route 5 only. Wired: test/test_sizeof_of_a_frozen_string_type_name.pastest_sizeof_frozen_name26, byte-identical to fpc 3.2.2.

TWO POSITIVE CONTROLS FAILED TO FIRE BEFORE ONE DID, AND BOTH FAILURES WERE THE FIXTURE'S FAULT, NOT THE REFACTOR'S. They are worth more than the row.

First: the fixture was AIMED AT A SITE IT NEVER REACHES. Its four SizeOf(<name>) rows do not enter the changed resolver at all. Measured with a probe registered as PXXDBG=p.sized: it fires exactly twice for the whole program, both times from the file of element site. The SizeOf re-parse arm is gated by BuiltinTypeNameNeedsDecl, which holds six namesshortstring, textfile, file, pchar, pansichar, pwidechar — and S10 is a user alias, answered by the alias arm at pasparser_expr.inc:3987, which already takes AliasTk[i] with AliasStrCap[i] from one indexed carrier. That is route 1. The rows were pinning a path that never had this defect, and the ticket's own repro sketch is what made that look like the changed one. They are kept, labelled, and are not a control.

Second, and the general one: the readout COLLAPSED the two answers. The file row does reach the changed site, and with the alias cap write deleted from ParseTypeKind it still printed 3 cc 3 — correct count, correct value, correct position, wrong element width. A typed file is written and read through the same width, so FileSize, the value and FilePos are all invariant to that width being wrong. Only the BYTE length separates them, and the fixture was not printing it. Reopening both files as file of Byte gives bytes 33 12 correct against bytes 24 12 under the control — the row now fails on the control binary and passes on the real one, both asserted.

So this fixture ran green through two deliberate breakages of the code it was written for. It is A GUARD THAT CANNOT FAIL IS NOT A GUARD and CHOOSE A PROBE WHOSE RIGHT ANSWER DIFFERS FROM THE DEFAULT arriving together, and neither would have been noticed from the output: a self-consistently wrong width is indistinguishable from a correct one at every readout except the one outside the file's own type.

Residual, stated rather than closed: the refactor removes the window at the two sites that had it; it does not make a FUTURE caller unable to open one, because LastTypeStrCap and friends still exist for the arms that set them. No Pascal program can sample "a caller added later", so nothing here is a control for that and the fixture says so in its own header.

Log

One near-miss worth a line, because it nearly landed: the CONTROL binary — built before the Erase calls existed — was run once from the repo root, and git add -A staged sizeoffrozen.dat and sizeoffrozen2.dat as new files. Caught by reading git status rather than by any check. A fixture that writes to its CWD produces artefacts that a blanket add will commit, and the ones that get you are from the throwaway binaries you built to break it, not from the fixture you are shipping. The shipped fixture Erases both files however it is wired; nothing enforces that for the next one.