C-exclusive lowering has no carved-out file, so Track C cannot be staffed independently
NOT DISPATCHABLE — do not claim. Read this before the slice plan below.
The whole-routine carve-out is complete and its census is DRY (slices 1, 1b, 1c — 2026-08-30). Slices 2-5 in the plan below are all arms, and the arms are deferred by the coordinator's own 2026-08-29 disposition: extract per-arm, ON DEMAND, driven by a queued ticket — never as a sweep.
There is no runnable work in this ticket today. Its next move is a queued C lowering ticket reaching an arm — not an agent reaching for this ticket. It holds prio 60 for the lane it unblocks, which is not the same as being ready.
compiler/ir.inccarries a standing grant to frankC (Track C), recorded in the GRANT section below. The file-lock is idle between slices; the designation is not. If a queued C ticket needs an arm extracted, route it to frankC rather than dispatching this.Marker added 2026-08-30 after frankA was dispatched here and stopped before touching anything. Nothing was lost. The cause: a grant lives in the ticket BODY, where
ready,nextandworking/cannot see it, so the ticket advertised itself as unheld ranked work — which, for the lock, it was.
- Type: refactor (structural / coordination) — Track A (owns
ir.inc). - Found: 2026-08-29 by frankC, working down the Track C queue; measured and confirmed by the coordinator.
The measurement
Track C owns clexer.inc, cparser.inc, cpreproc.inc and lib/crtl. It does
not own a lowering file — there is no cir.inc. C-exclusive lowering lives
in the shared compiler/ir.inc, which carries 40 CProgramMode references.
The consequence, over the six ranked Track C tickets:
| ticket | file it must edit | workable by a C-only agent? |
|---|---|---|
refactor-c-string-literal-decay-belongs-at-the-producer [p50] |
ir.inc |
no — A |
feature-c-diagnostics-name-the-module-they-are-in [p40] |
lexer.inc |
no — A |
refactor-c-the-partial-index-sentinel [p40] |
cparser.inc + ir.inc |
no — C+A |
feature-c-import-a-pascal-unit-under-a-mangled-name [p50] |
cparser.inc + cpreproc.inc |
yes — C only (see correction below) |
idea-c-realworld-test-targets [p60] |
— | no — brainstorm parent |
compat-c-printf-p-of-null [p22] |
lib/crtl |
yes — resolved e885d94ef |
Row corrected 2026-08-30 by frankC, which wrote the original row and the coordinator flagged as stale. The correction is smaller than "stale" suggests and the distinction is the point:
- What was wrong: the row read "no — blocked on the user" with no file
named, which reads as the design is unsettled and the whole ticket waits on a
decision. It was written on 2026-08-29 from the ticket's position, not its
contents. In fact the design was settled by the user on 2026-08-19 — the
ticket says so in its own frontmatter ("this ticket is a SPEC, not a
discussion") — and §1-§5 were built, tested and green ten days ago, entirely
inside
cparser.incandcpreproc.inc. It is one of the strongest examples AGAINST this table's thesis, and it was filed as an example for it. - What was right, and is still true: §6 alone — the bare-name experiment —
is blocked on the user, but on a permission grant
(
PXX_ALLOW_FULL_SUITE=1for four corpus builds), not on a design call. That is a different kind of block and does not make the ticket unworkable; it makes one section of it unrunnable.
Re-measured green at aa78a7faf63a, 11 of 11 tests, 2026-08-30.
ready --track C prints nine items; exactly one was workable, and it is now
done. That gap is why "Track C has a queue and no agent" read as an easy
staffing win on 2026-08-29 and was not one.
Why this is a real defect and not just how it is
The tracks are file-lanes for collision avoidance. A lane whose work predominantly lands in another lane's files is not a lane — it is a label, and it silently converts every C dispatch into a request for the A slot. Compare:
- P had exactly this problem and it was fixed. The 37,249-line
parser.incwas sliced intopasparser_*.incon 2026-08-20 precisely so Pascal frontend work would stop needing A's slot. (Its lexer is still shared — the known residual.) - R and Z own
rparser.inc/zparser.inc. - N owns
pylexer.inc/pyparser.incand is explicitly called out in CLAUDE.md as the low-risk combination because it is carved out.
C is the frontend that got its parser carved out and its lowering left behind.
It is a half-finished migration, not a design choice
frankC's framing, added 2026-08-29 and sharper than the original: C is the only
mainline frontend whose parser was carved out and whose lowering was not. That
makes the asymmetry a migration nobody finished rather than a deliberate split —
which is what turns this into a refactor with a known-good precedent (the
pasparser_* split of 2026-08-20) instead of an open design question. Nobody has
to decide whether C should own its lowering; every other frontend already does.
It also predicts the payoff, which the table above does not. A cir.inc would move
refactor-c-string-literal-decay, refactor-c-the-partial-index-sentinel and
probably feature-c-diagnostics-name-the-module from "needs the A slot" to
"ordinary C work" — three of the four tickets that made the lane unstaffable.
The value is not tidiness; it is that Track C becomes dispatchable in parallel with
Track A, which it is not today.
What to do
Carve C-exclusive lowering out of ir.inc into cir.inc, the way
pasparser_*.inc was carved out of parser.inc. The 40 CProgramMode sites
are the starting inventory, not the definition — some will be genuine shared
decisions that must stay.
Do not treat the count as the scope. The parser.inc split's lesson was
that the machinery which was never Pascal went to its real owner (ast_arena,
inline_expand, ast_syminfer to A; NilPy's forwards to N). Expect the same
here: some of the 40 are C-shaped things that belong to C, and some are shared
lowering with a CProgramMode guard bolted on, which is a different defect.
Until then
Track C is one agent's worth of work at a time, gated on the A slot, not an independently staffable lane. A coordinator staffing C should either pair it with the A slot or expect it to run dry. That is the operational fact this ticket exists to remove.
Prio raised 45 -> 60 by the coordinator, 2026-08-29 — and why it had to be by hand
This is a coordinator call, not the owner's. Recorded here rather than left implicit so it can be vetoed in one edit.
The board's ranking model is "one human prio: propagated down dependency
edges — a blocker inherits the priority of what it unblocks, so you rate goals
and the chain follows." That mechanism cannot fire on this ticket, and the
reason is measurable:
$ grep -rl "c-exclusive-lowering" devdocs/progress/{urgent,backlog,backlog_new,unfinished,blocked}/
$ # zero files
No ticket declares this one as a blocker, so it has no in-edges and inherits nothing. It sat at 45 while ranked below five tickets it is the blocker for — frankC's words, 2026-08-29: "that p45 is the ticket that unblocks my lane, and it is ranked below four things it is blocking."
The edges were not simply forgotten — adding them would be a false claim.
blocked-by: means cannot proceed, and those five C tickets can proceed
perfectly well: by an agent holding the A slot. Marking them blocked would hide
real Track A work from Track A's queue in order to fix a ranking artefact. So
the honest repair is the prio: field, which is what it is for.
Generalisable, and worth more than this ticket: prio propagation is only as good as the edges someone drew, and a structural blocker is exactly the kind that never gets an edge — because it blocks a lane, not a ticket. Nothing in the tooling can see that. A ticket whose beneficiaries are "most of track X" will always under-rank itself, and no checker will flag it, because from the ranker's side an in-degree of zero is indistinguishable from a leaf.
Instance five, which is what moved the number
frankC predicted this ticket's own consequence and then hit it, on
refactor-c-the-partial-index-sentinel-should-not-be-a-type-tag [C p40] — a
ticket the coordinator picked specifically because it looked disjoint:
ParseCPostfixTail compiler/cparser.inc:3696 Track C
CNodePointeeTk compiler/cparser.inc:2051 Track C
IRNodePointerBase compiler/ir.inc:2348 Track A <-- the 12 lines that ARE the refactor
IRPointerStride compiler/ir.inc:2390 Track A
Neither sketched option escapes ir.inc: the ASTSLen-style stamp is read by
IRPointerStride's AN_BINOP arm, and the dedicated-AST-node option is worse
for C, since a new AST node is a Track A ticket C files rather than code C
writes. Landing only the cparser half would write the sentinel twice —
tag and stamp, with A's reader still on the tag — which is strictly worse than
today. The ticket's own gate line ("the tyInt64 special case in
IRNodePointerBase is gone rather than moved") requires the A file by
construction.
The table above therefore reads five of seven ranked C tickets need an A file, not four of six. It has gone up, not down, since the ticket was filed this morning — and it went up on the very ticket chosen to test whether the lane had disjoint work in it.
The disposition matches the precedent already set on
refactor-c-string-literal-decay-belongs-at-the-producer [C p50]: keep
track: C for visibility, note in the body that the edit needs the A slot.
"File-lanes exist for collision avoidance, not as a taxonomy, so the rule is
about the FILE and not the topic." That ruling should not need re-deriving a
sixth time, which is itself an argument for this refactor rather than for
another routing note.
GRANT — compiler/ir.inc to frankC (Track C) for this carve-out, 2026-08-29
Granted by the coordinator. Filed rather than left in message traffic: an authorisation is a finding about what is permitted, and an unfiled grant reads as covered rather than as missing.
Why the holder is a C agent and not an A agent. This is Track A work by file
and Track C work by motive. frankC diagnosed the need, produced the five-of-seven
measurement, and hit instance five itself; it is also the only session that will
know immediately whether a given CProgramMode site is genuinely C-exclusive or
shared lowering with a guard bolted on — which is the distinction the whole
refactor turns on, and the one an A agent would have to rediscover.
Precondition verified at grant time, not assumed. compiler/ir.inc is clean
in all twelve clones. Adjacent-but-disjoint holders: frankA
(pasparser_decl.inc, symtab.inc), frankS (ir_codegen_xtensa.inc, plus the
granted defs.inc line), frank-optimize-b4 (ir_codegen.inc), frank-rust
(pasparser_generic.inc), the T tree (pasparser_proc.inc). ir.inc itself
is held by nobody, and the coordinator holds the slot for frankC until frankC
releases it.
Conditions, and the second one is the load-bearing one:
compiler/ir.incand the newcompiler/cir.inconly. Anything else —symtab.inc,defs.inc, a new AST node or IR op — is a separate ask.- Report the inventory BEFORE moving anything. The 40
CProgramModesites are the starting inventory, not the scope. Theparser.incsplit's actual lesson was that machinery which was never Pascal went to its real owner (ast_arena,inline_expand,ast_syminferto A; NilPy's forwards to N), and the same is expected here: some sites are C-shaped and belong to C, and some are shared lowering with aCProgramModeguard bolted on, which is a different defect and must not be moved as if it were the first kind. Classifying them is the deliverable that survives even if the move stalls. - Land in committed slices, not one move. Slices 1-6 of the speculative-parse
work are the model. A single large
ir.increwrite is unmergeable against five live lanes; a sequence of small ones is not. - Gate is A's:
make compiler/pascal26to fixedpoint per slice,gate.sh quickbefore any pin. Land only green. - Tell the coordinator before touching
ir.incfor anything outside this refactor, and on release.
Expiry: when this ticket resolves, or when frankC reports the slot released —
whichever is first. A later ir.inc change is a later ask.
INVENTORY — frankC, 2026-08-29. Reported before moving anything (grant condition 2). No line of ir.inc has been touched.
Measured at 3e76bb52a. compiler/ir.inc is 13,570 lines and carries 40
CProgramMode matches. The count is wrong in both directions, which is the
headline: it over-counts what is C by 18 and misses 232 lines that are.
Classification of the 40
One is not a site at all. Line 1662 is prose — a comment recording that a
guard was removed ("This arm was gated on CProgramMode for no reason the
guard itself needs", feature-a-typeref-migrate-consumers). 39 real guards.
| class | count | what it is | can it move to cir.inc? |
|---|---|---|---|
| A — C-exclusive | 22 | lowering that only ever runs in C mode | yes, but see the shape problem |
| B — Pascal/NilPy-only, C excluded | 13 | not CProgramMode guarding Pascal code |
no — moving it would be backwards |
| C — shared two-armed dialect decision | 4 | both dialects act, differently | no — it is a shared rule |
| — comment | 1 | records a guard already deleted | n/a |
Class B — 13 sites that are not C lowering at all
2864 7212 8282 8352 8966 8980 9343 9427 9448 9669 12752 12772 12830
Every one is Pascal (or Pascal+NilPy) behaviour with C carved out: the
char-array/string conversions (bug-p-a-char-array-is-not-a-string-in-any-direction,
five sites), the PChar↔string wraps, the record-arithmetic and assignment-type
diagnostics, the cdecl-procvar reject, the enum/widechar write paths. The C
"behaviour" at each is absence. IRCoerceCharArrayArg (2864) is the clearest:
if CProgramMode then Exit as its first statement — a 50-line routine that is
entirely Pascal's.
A naive "move the CProgramMode sites" would relocate a third of the Pascal
frontend into a C file. This is the ticket's own warning ("some are shared
lowering with a guard bolted on, which is a different defect") landing harder
than expected — the guard is not bolted onto shared lowering, it is bolted onto
the other lane's lowering.
Class C — 4 shared decisions
| line | decision |
|---|---|
| 1758 | string-literal index base: -8 (C, 0-based) vs -7 (Pascal, 1-based) |
| 3801 | record argument: C always copies to a temp; Pascal branches on isRefArg |
| 10426 | literal→pointer store: CProgramMode or IsNodePChar(dest) — one rule, two spellings |
| 12074 | ternary arm type: tyPointer (C) vs tyAnsiString (Pascal) |
These are dialect parameters, not C code. 10426 is the interesting one: C says
"any pointer destination", Pascal says "a PChar destination", and they are the
same rule at different strictness — a normalise-dont-special-case candidate in
its own right, and not something to move.
Class A — the 22 that are genuinely C, and why the obvious move fails
| shape | count | lines |
|---|---|---|
| whole routine is C-only | 1 | 2238 |
| C-only tail of a shared routine | 3 | 867, 2153, 2375 |
| C-only early-exit in a shared routine | 1 | 864 |
| C arm inside a shared dispatch | 17 | 1642 2406 2426 2440 2467 3152 3176 7165 7176 7309 7909 9288 10220 10461 10468 10529 10635 |
Seventeen of the twenty-two are arms inside somebody else's case or
if-chain — IRLowerAST's giant AST-kind case, IRLowerAddress (604 lines),
IRPointerStride (182), IRLowerCallArg (1168). They cannot be lifted out
whole, because what surrounds them is the shared dispatch itself.
This is the structural difference from the parser.inc precedent, and it is
why that precedent does not transfer unmodified. Pascal parsing was whole
procedures — ParseClassDecl, ParseGeneric — so slicing them into
pasparser_*.inc was a file move. C lowering is arms, and an arm has no
boundary to cut along.
What the 40-site grep MISSES — 232 lines, six whole routines
CProgramMode does not appear in them, so none is in the inventory. Each is
reached only from a C-guarded call site, verified by checking every caller:
| routine | line | size | reached from |
|---|---|---|---|
IRLowerBitFieldRead |
959 | 70 | 7178, 10228 — both CProgramMode arms |
IRLowerBitFieldStore |
1029 | 50 | 10227 — same arm |
IRLowerCompoundAssign |
2169 | 41 | 9333, under IRAssignIsSharedCompound |
IRAddrMayCall |
2075 | 28 | 2154 only, after if not CProgramMode then Exit |
IRAssignIsSharedCompound |
2225 | 28 | 9331 |
CASTNodeOccursIn |
2210 | 15 | only from IRAssignIsSharedCompound |
| 232 |
CASTNodeOccursIn already carries the C prefix in its name — someone knew.
(cparser.inc:12381 mentions IRLowerBitFieldRead in a comment only; it is not
a call, so nothing outside ir.inc reaches any of the six.)
These six move with zero guard edits, zero behaviour change, and zero risk — they are whole routines with no non-C caller. They are the honest slice 1, and the grep would never have found them.
Proposed shape — extract bodies, keep the dispatch line
For the 17 arms, the move that works is body extraction, not relocation:
{ ir.inc, at the arm }
if CProgramMode and (ASTKind[node] = AN_FIELD) and ... then
Result := CIRLowerFieldArrayDecay(node, left) { cir.inc }
ir.inc keeps a one-line guarded call per arm; cir.inc owns every body. That
is where the work and the future edits are — the guard line is stable, the body
is what refactor-c-string-literal-decay and
refactor-c-the-partial-index-sentinel actually need to change. It does not
get Track C out of ir.inc entirely, and the ticket should stop promising
that: adding a new C arm will always touch the dispatch. It gets C out of
ir.inc for the changes that are actually queued.
Proposed slices
- The six invisible routines (232 lines) into
cir.inc. No guards touched, no behaviour change. Establishes the file and its include point. IRPointerStride's four C arms +IRNodePointerBase's tail — the pointer/decay cluster, which is one coherent subject and is exactly whatrefactor-c-the-partial-index-sentinelneeds to own.IRDiscardValue/IRLowerDestAddresstails — the C assignment-as-expression cluster (864, 867, 2153) plus the load-backs (10461, 10468).- The string-literal
+8family (9288, 10426, 10635, and the call-arg site) — this isrefactor-c-string-literal-decay-belongs-at-the-producer's whole subject; do that refactor at the same time, since the correct fix is to lower it once at the producer rather than move three copies. - Remaining arms, by dispatcher.
Classes B and C are out of scope by measurement, not by preference — 17 of
the 39 real guards stay in ir.inc permanently and that is correct.
Two findings worth their own tickets — NOT filed by me, flagged for the owner
- The 13 Class-B sites are Pascal lowering carrying a C-shaped guard. Several
are the same
bug-p-a-char-array-is-not-a-string-in-any-directionrule repeated at five sites (8282, 9343, 12752, 12830, andIRCoerceCharArrayArg), each with a comment pointing at the others. That is theroot-cause-over-microfix"three copies is a design flaw" count, in Track P's ground, and it is a separate ticket from this one. 10426is one rule spelled two ways (CProgramMode or IsNodePChar(dest)). Normalising it would delete a Class-C entry rather than move it.
Status
Inventory only. compiler/ir.inc is unmodified and the tree is clean. Awaiting
the go-ahead on the slice plan — specifically on whether slice 1 (the six
routines, 232 lines, no behaviour change) should land before the plan for the
arms is agreed, since it is independently safe and independently useful.
Correction to the inventory, and to this ticket's main argument — frankC, 2026-08-29
1. The parser.inc precedent does not carry the weight this ticket puts on it
Stated plainly because it is this ticket's principal argument, it is quoted in the prio raise above, and it is wrong:
Pascal parsing was whole procedures, so slicing parser.inc into
pasparser_*.inc was a file move. C lowering is ARMS inside shared
dispatchers, and an arm has no boundary to cut along. Seventeen of the
twenty-two C-exclusive sites are arms in IRLowerAST's AST-kind case,
IRLowerAddress (604 lines), IRPointerStride (182) and IRLowerCallArg
(1168).
So the promise "a cir.inc gets Track C out of ir.inc" cannot be
delivered, and it should not be restated. Adding a new C arm will always
touch the shared dispatch. The deliverable goal is the narrower one: get C out
of ir.inc for the changes that are queued — which is achievable, and is what
the slices are for.
Disposition on the arms (coordinator, 2026-08-29): extract per-arm, ON DEMAND,
driven by a queued ticket — never as a sweep. The general question ("should all
seventeen read as a one-line stub plus a cir.inc body?") is deliberately left
open until two or three real extractions exist, so it can be decided from a diff
rather than a sketch — and so a bad answer costs three reverts, not seventeen.
A Track U ticket is deliberately NOT filed yet for the same reason.
2. Slice 1 is seven routines and 223 lines, not six and 232
Two corrections to my own numbers, both found while working out the dependency order for the move:
A seventh routine. CASTLValueHasSideEffect (ir.inc:2103, 20 lines) is
called only from IRAssignIsSharedCompound and itself. Same shape as the other
six — C-prefixed name, no CProgramMode in its body, invisible to the grep.
My 232 figure was measured wrong. It used span-to-next-routine-start, which
for IRLowerBitFieldStore swept in the 24-line PROMOTABLE INT lowering comment
block belonging to the routine after it. Real body counts:
| routine | lines |
|---|---|
IRLowerBitFieldRead |
69 |
IRLowerBitFieldStore |
26 (not 50) |
IRAddrMayCall |
27 |
CASTLValueHasSideEffect |
20 |
CASTNodeOccursIn |
14 |
IRAssignIsSharedCompound |
27 |
IRLowerCompoundAssign |
40 |
| total | 223 |
3. "Pure relocation" acquires one forward declaration
The seven have a circular ordering constraint: IRLowerDestAddress (staying in
ir.inc) calls IRAddrMayCall (moving), and IRLowerCompoundAssign (moving)
calls IRLowerDestAddress. One include point cannot satisfy both, so ir.inc
gains one line:
function IRAddrMayCall(n, depth: Integer): Boolean; forward; { compiler/cir.inc }
Zero guard edits and zero behaviour change still hold. Expect this shape in every later slice — the shared dispatchers and the C bodies call each other in both directions, so each extraction is likely to leave a forward behind. That is a real cost and the general-question decision should be made with it in view.
4. The method finding, which outlasts this ticket
Not one of the seven contains the string CProgramMode. The search that
defined the job could not see 223 lines of its own subject, while over-counting
by 18 in the other direction. Two of the seven carry a C prefix in their names
and the grep still missed them: a name is not an index.
The rule that found them, and the one to use for later slices: follow CALLERS, not the guard. The guard says where the decision is made; it never says where the work lives.
SLICE 1 LANDED — frankC, 2026-08-30
Seven C-only routines out of ir.inc into the new compiler/cir.inc:
IRLowerBitFieldRead / IRLowerBitFieldStore, IRAddrMayCall,
CASTLValueHasSideEffect, CASTNodeOccursIn, IRAssignIsSharedCompound,
IRLowerCompoundAssign. ir.inc −230 / +10, compiler.pas +7.
The gate — and why the usual one could not be it
make compiler/pascal26 is structurally blind to this entire slice. Compiling
compiler.pas compiles Pascal; CProgramMode is never set; not one of these
seven routines is ever called. The fixedpoint would have gone green if their
bodies had been deleted. A gate that cannot fail is worse than no gate, because
it launders the change through everyone's trust in it.
The pasparser_* precedent inverts here rather than transferring.
compiler.pas:126 says each of those slices is "a contiguous range re-included
where it sat, which is what makes the carve-out provable by the self-host
fixedpoint" — an argument that works because Pascal parsing is what the
fixedpoint exercises. A C carve-out is the exact case where it fails. That is
the second way this ticket's founding precedent does not carry.
So the gate is a C-side equivalent, and it is the standing gate for this
refactor and every per-arm extraction (adopted by the coordinator, 2026-08-30).
For a relocation claim, output equality is too weak — it would pass a semantic
change. Relocation must produce identical machine code. Ten C tests covering
all seven routines, sha256'd against the pre-move compiler 261e6cd2b58f,
rebuilt after:
canon_bitfield_b310 66f59407f54c cbitfield_arith_precision 78c5eaf6b5f2
cbitfield_longlong_b359 62709c2fc25d cbitfield_mixed_type_pack_b373 4d4e710c39d1
cbitfield_promotion_b358 201cf0c82b2c csigned_bitfield_b306 490e0650460f
cassign_compound_lvalue_once bc0bb454c366 cassign_dest_call_once e873b0aa848c
cassign_value_b43 849b560a76ba cstruct_assign_dest_side_effects ec89940cc167
All ten byte-identical, outputs identical. Plus forwardlint clean, and
gate.sh quick GREEN — including its fpc seed compiles (forward decls) step,
which is what actually exercises the five new forwards.
Forward count: 5, not the 1 predicted
The include position I proposed did not exist. I assumed {$include cir.inc}
could sit inside ir.inc's stream after IRLowerDestAddress, costing one
forward. The flat convention puts it in compiler.pas, which can only place
cir.inc before or after the whole of ir.inc. Both options were measured:
| position | forwards | where they live |
|---|---|---|
cir.inc after ir.inc |
5 | in ir.inc, for what it calls forward |
cir.inc before ir.inc |
6 | in cir.inc, for what it borrows |
After was chosen not on the count but because before is not viable:
cir.inc calls IRLowerAST, which ir.inc already forward-declares at 675, so
placing cir.inc first needs a second forward for it — a duplicate the FPC seed
rejects, and precisely what forwardlint exists to catch.
Carry the 5 into the arms decision, not the 1. And expect worse there: an arm's body reaches back into the locals of the dispatcher it was cut from, which relocating a whole routine never does.
Cross-finding, same evening, other direction (frankA, 2d57b9744)
ParsingClassBodyCi's -1 sentinel was initialised only inside
ParseProgram, the Pascal entry point, so every other frontend ran with the
BSS default 0 — a valid UCls index (TGuid). ALGOL, Erlang, Rust, Zig, C
and asm all carried the hole; only NilPy had the users to expose it.
Same root property as the blind gate, with the roles reversed: the shared gate exercises the Pascal path, so anything true only off that path is unobserved by construction — in the gate's blindness and in the bug's survival alike. Two independent findings in one evening arguing for the same remedy, which is why the C-side byte-identical gate is not a local nicety for this refactor.
Grant, 2026-08-30 — frank-coordinator to frankC: IRPointerStride's AN_FIELD arm
Extends the standing ir.inc grant by ONE routine. IRPointerStride lives in
compiler/ir.inc (Track A's ground); CNodePointeeTk, CDerefDecayStride and
ParseCPostfixTail are already frankC's in cparser.inc.
Granted: separate commit, own ticket, alongside the sentinel refactor.
Why it is safe rather than merely small. Checked, not assumed —
tools/fleet_dirt.sh across all 16 discovered checkouts at the time of the
grant: compiler/ir.inc is held by frankC alone. frankA is in
pasparser_lval/_expr/_stmt, frank-rust in pasparser_generic.inc, frankS in
ir_codegen_xtensa.inc, pxx-songfmt in pyparser.inc. No second lane is in the
file, so the no-concurrent-edit rule the letters exist to enforce is not in play.
Why it is not scope creep. The bug is inside the arm frankC is already re-keying, is not long-long-specific, and produces silent wrong ADDRESSES:
| expression | type | gcc | pxx |
|---|---|---|---|
(char*)(s.m+1)-(char*)s.m |
int m[3][4] |
16 | 4 |
| same | char c[2][8] |
8 | 1 |
| same | double d[2][3] |
24 | 8 |
int (*r)[4] = s.m + 1; r[0][2] |
7 | 0 |
A multi-dim array reached as a struct FIELD decays with the ELEMENT stride. The
AN_IDENT arm directly above it has the multi-dim row rule; the AN_FIELD arm
never got it. Under the compat table in CLAUDE.md this is the silent wrong
behavior escape — a normal bug- ticket in the owning lane, not a compat item.
The condition, and it is the whole point: frankC declined to trim the failing assertion out of its long-long regression test to make the suite pass. That is the correct call and it is what forced the ask instead of a quiet workaround. The grant exists so the complete test can land with the fix rather than the test being cut to fit the permission.
The pattern the grant is really about — parallel AN_IDENT / AN_FIELD arms
Third field-arm bug in one day whose array-arm twin was fixed months ago.
ParseCPostfixTail and IRPointerStride each carry parallel AN_IDENT and
AN_FIELD arms, and every past fix landed on one of the two. Two more readers
walk left to an AN_IDENT and ignore AN_FIELD and are so far unprobed:
CNodePointeeTk's AN_BINOP arm, and CDerefDecayStride.
This is normalise-dont-special-case and a parser that exists twice is one that
gets fixed on one arm, with three landed instances as evidence rather than as a
prediction. Sweep scoped as its own ticket — meeting it a fourth time costs
more than enumerating the readers once. Note what makes it expensive: the two
arms are adjacent in the same routine, so each fix looks local and complete,
and nothing in a diff shows the sibling going unedited.
AN_FIELD reader census to start from (not a completeness claim — the count is
per-file mentions, and the C-side readers are what matters):
ir.inc 44, cparser.inc 17.
Counter-example to this ticket's own measurement — frankC, 2026-08-30
The ratio ("four of six", "five of seven" in later retellings) is this ticket's headline number and I produced it, so the correction is mine to make.
On 2026-08-30 I resolved four Track C tickets and three of them never left Track C's own files.
| ticket | files edited | needed the A slot? |
|---|---|---|
bug-c-has-include-unsupported-so-pdfgen-selects-big-endian |
cpreproc.inc |
no |
bug-c-an-include-nested-deeper-than-16-is-silently-dropped |
cpreproc.inc |
no |
bug-c-a-header-reached-by-uses-...-imports-them-instead (partial) |
cparser.inc |
no |
refactor-c-string-literal-decay-belongs-at-the-producer |
ir.inc |
yes |
And in two of the three I hit an A-owned dependency and filed it rather than
reaching for the slot: bug-a-c-module-attribution-is-sticky-after-a-crtl-impl-pull
(dbg_filetable.inc) and
bug-a-c-preprocessor-include-buffers-are-sixteen-globals-not-an-array
(defs.inc). Both are the real remainder of a C ticket, and both are now A's
ranked work rather than a blocked C agent.
What that does and does not change
It does not overturn the argument. ir.inc still carries C's lowering, the
carve-out is still right, and the one ticket of the four that needed the A slot
needed it for exactly the reason this ticket describes.
What it corrects is the generality. The ratio is not a property of Track C; it is a property of what kind of C work is queued:
- Preprocessor and parser work is self-contained —
#ifevaluation, the include search, the header walk,statichandling. C ownsclexer,cparser,cpreprocoutright, and a whole night of that touched no A file. - Lowering work is not, and never will be until the carve-out lands.
The 2026-08-29 queue happened to be lowering-heavy, and the measurement read as "Track C cannot be staffed" when the defensible claim is "Track C's lowering work cannot be staffed." That is narrower and still sufficient: it is the lowering tickets that keep coming back, and an agent that can only work when the queue happens to be preprocessor-shaped is still not independently staffable.
Recorded because a table that survives its own counter-examples is worth more than one that never met any — and because the next person to quote the ratio should quote the narrower claim, which is the one that holds.
SLICE 1b — the method applied TRANSITIVELY finds what slice 1 missed. frankC, 2026-08-30
Slice 1's own finding was "follow CALLERS, not the guard". Applied again at
HEAD, mechanically this time (callers.py: every routine defined in ir.inc
whose every mention across all of compiler/** is inside ir.inc/cir.inc,
with the enclosing routine of each site named), it turns up something the first
pass could not have:
Slice 1 followed callers ONE level. The property is transitive, and two routines sit at the second level.
IRBitMask is called from IRLowerBitFieldRead and IRLowerBitFieldStore and
from nowhere else. Both of those moved to cir.inc in slice 1 — so IRBitMask
became reachable only from cir.inc as a result of slice 1, and a one-level
scan run before the move could not have seen it. Every extraction can create
new candidates one level below it, which means the census is not a thing you
run once.
IRLowerCSwitchDispatchScan — MOVED
32 lines, ir.inc -> cir.inc. Lowers C's switch dispatch: walks the whole
switch body at any depth assigning IR_LABELs to case/default markers, which is
what makes Duff's device work. Reached only from IRLowerAST's AN_SWITCH arm
and from itself; AN_SWITCH is constructed at exactly one place in the whole
compiler, cparser.inc:6657, so the node kind is C-exclusive by construction and
not merely by current usage.
Costs one more forward in ir.inc (the arm at 11997 calls it, and cir.inc is
included after). That is 6 forwards for the file now, against the 1 originally
predicted — the drift is worth watching, and it is the number to carry into the
arms decision.
IRBitMask — DELIBERATELY NOT MOVED, and this is the sharper half
It qualifies on the stated rule and it should stay, which means the rule was
incomplete. IRBitMask(width) is (1 shl width) - 1 written as a loop: pure
integer arithmetic, no AST, no CProgramMode, no C semantics whatsoever. It is
C-only by who happens to call it, not by what it is.
Moving it would put a general-purpose helper in a frontend's file, so that the
day Pascal wants a bit mask, the answer is either a duplicate or a Pascal
dependency on cir.inc. That is the parser.inc lesson running the other way:
that split sent ast_arena, inline_expand and ast_syminfer to A — not
because Pascal had stopped using them, but because they were never Pascal's
subject. Same test, opposite direction.
So the charter needs its second half, and cir.inc's header now carries it:
Reached only from C is NECESSARY and NOT SUFFICIENT. The routine must also BE C — its subject must be a C-language construct. A general utility that only C happens to call stays where any lane can reach it.
That distinction is exactly what condition 2 of the grant asks for ("C-shaped and belongs to C" vs "shared lowering with a guard bolted on"), met in a third form the condition did not anticipate: shared machinery with no guard at all.
Gate — the C-side byte-identical one, as adopted
make compiler/pascal26 is blind here for the same structural reason as slice 1:
compiling compiler.pas compiles Pascal, CProgramMode is never set, and
IRLowerCSwitchDispatchScan is never called. It would go green if the body were
deleted.
Six C tests built with the pre-move compiler e2ea9034a65e and the post-move
compiler 410f0b9afac4, sha256 of the emitted binaries:
cswitch_b13 28c0b1b9469e
cswitch_nested_case_block_b127 62f66889c1cd
cswitch_noncompound_duff_b207 678ee565010a (Duff's device — the case this routine exists for)
cswitch_unsigned_negative_case_b374 965100a18a35 (the C 6.8.4.2 label conversion inside it)
quick_canary_c ec7f4f4fcd5e
cchar_promotion_contexts 1cf3791036e8
All six byte-identical, outputs and exit codes identical. Plus
forwardlint clean (216,812 lines, no use-before-declaration, no duplicate
forward) and gate.sh quick.
Where the census leaves the file
ir.inc is at 38 CProgramMode matches, down from 40. That number was never
the scope and is now further from it than ever: two of the missing sites left
with slice 1's seven routines, none of which contained the string, and this
slice moves a routine that does not contain it either. The count measures the
guard, and the guard is not where the work lives.
Two gate-methodology notes from this slice, both self-inflicted
1. Do not edit the tree while gate.sh runs. The first run of this slice's
gate went RED on the self-host fixedpoint — "the fixedpoint reached from PINNED
differs from compiler/pascal26 ... two distinct fixedpoints means the binary we
test with is not the one these sources define". Nothing was wrong: I had
appended a comment block to cir.inc mid-run, so the sources genuinely changed
under the comparison. Re-run on a quiescent tree: GREEN, same contents. The
gate's diagnostic is precise and it is worth reading literally — it says the
binary and the sources disagree, which is exactly what an edit-during-gate
produces.
2. The completion notification's exit code was tail's, not the gate's. The
gate was launched as tools/gate.sh quick > log 2>&1; tail -12 log, so the
;-list's status belongs to the last command. The RED run above was reported as
"completed (exit code 0)" while its own final line read gate: RED (exit 1).
Read the log's gate: GREEN|RED line; do not trust a wrapper's status. Launch it
as tools/gate.sh quick > log 2>&1 alone, or capture rc=$? before appending
anything.
SLICE 1c — the candidate a COMMENT had hidden. frankC, 2026-08-30
The census tool was counting mentions, not calls, and in Pascal the prose
lives in the same files as the code. Fixed in fe78e0cb9. It failed in both
directions, and only one of them is visible:
- It invented callers —
cir.inc's own charter prose aboutIRBitMaskscored as two call sites under<top>. Noisy; someone would have caught it. - It suppressed a candidate —
cparser.inc:12547is a comment reading "A long-long bitfield unit is LOADED/STORED as 8 bytes (IRBitStorageTk)". That mention is outsideir.inc/cir.inc, soIRBitStorageTkfailed the reached-only-from-C test and never entered the candidate list at all.
A sentence explaining the routine disqualified it. Two censuses had already run over this file and neither could see it — there was no wrong row to be suspicious of, only a routine that silently was not there. That asymmetry is why the false-negative direction is the expensive one, and it is the same shape as a skip counted as a verdict.
A third bug was mine: is_forward tested line.endswith('forward;'), and the
forward slice 1b added wraps across two lines, so the tool read my own
forward as a definition and attributed the following mentions to it. A tool
that does not model the language it measures.
IRBitStorageTk — MOVED, at zero forward cost
6 lines, ir.inc → cir.inc. Both call sites are already in cir.inc, so
ir.inc loses the routine and gains nothing — cheaper than slice 1b, which
cost the sixth forward.
It moves where IRBitMask stayed, and the pair is the charter's second half
working: both qualify on reached-only-from-C, only this one is C. A general
size→typekind map would be exact; this one rounds, and every rounding step
is a C rule — 3 bytes loads as 4 (C11 6.7.2.1 leaves the storage unit to the
implementation and ours is the next power of two), anything past 4 loads as 8
because a long long bit-field's unit does. Its only input is
RecFieldBitBytes over UFldBitBytes, which defs.inc:4309 documents as "C
named bit-field width in bits". Pascal parses bitpacked as a synonym for
packed (pasparser_generic.inc:512) and allocates no bit-fields — no Pascal
caller to lose, and no Pascal meaning for the rounding to be wrong for.
The gate, and the first attempt was wrong
Same structural blindness as slices 1 and 1b: make compiler/pascal26 compiles
Pascal, CProgramMode is never set, this routine is never called — it would
go green with the body deleted. So: the eight C bitfield tests through both arms.
The first run reported 0/8 identical, every binary ~7 KB larger, and it was
not the move. tools/sync.sh had rebased between the two builds, landing
df98fea47 (explicit data-section alignment) and be4caba1a. I compared two
different upstreams and attributed the delta to six lines — the provenance
failure "verify against a known sha" exists for, arriving as a plausible
result rather than an error.
Redone as one script that stashes → builds → measures → pops → builds → measures at one HEAD, writing both arms to one fixed output path so the output filename could not be a variable either:
pre 62cfb924053f (stashed) post 6e112243efb2 (applied)
8/8 byte-identical, exit codes and outputs identical, both arms self-host in 1 round
BITSTORAGE-DIFFERENTIAL-COMPLETE compared=8 identical=8
That closing line is a positive token the subject must emit, not a status
the harness interprets (frankwasm's repair) — a status can be produced by
something other than the subject; a token the subject prints cannot.
forwardlint clean (218,351 lines).
Where the census stands
Dry. After the move the only remaining "callers all already C-only" row is
IRBitMask, which is the deliberate non-move. There is no further driven
extraction available without a decision about the arms, and slice 2 stays ruled
off pending that.
SLOT: LOCK RELEASED, DESIGNATION RETAINED — frankC, 2026-08-30
Amended after the coordinator's reply, same day — and the amendment is a
distinction, not a reversal. The coordinator asked me to keep the grant, on the
grounds written into it: I am the session that knows on sight whether a given
CProgramMode site is genuinely C-exclusive or shared lowering with a guard
bolted on. That reason is sound and it survives everything below. But it is an
argument about who does the work, not about who holds the file, and those
had been travelling as one thing:
| state | why | |
|---|---|---|
the file-lock on compiler/ir.inc |
released | there is no slice to hold it for; the census is dry and the arms are deferred. A lock over a file with no queued work is the near-miss generator, not a safeguard. |
| the designation for this refactor | retained by frankC | unchanged, for the coordinator's stated reason. Re-take the lock on demand when a queued C ticket reaches an arm. |
Grant condition 5 (tell the coordinator on release) is met by this section and the message citing it. Resuming re-claims first, per CLAUDE.md's rule that a lock is a claim about the present and nothing re-asserts it.
Verified, not asserted: my tree is clean and has been since 06c3cd966
(09:08). Nothing of this campaign is uncommitted in this checkout, and no slice
is half-applied — every one of 1, 1b and 1c landed complete with its own
differential gate.
Released because the driven phase is DONE, not because it was interrupted.
The whole-routine census is dry (slice 1c). Every candidate satisfying both
halves of the charter — reached only from C and C in subject — has moved.
IRBitMask is the one remaining reached-only-from-C row and is a deliberate
non-move, recorded above.
What the next holder must NOT do with this ticket
Do not dispatch this as "continue the carve-out." There is no next slice to pick up, and the shape of the ticket hides that: it reads as an in-flight campaign with slices 1→5 sketched, and slices 2-5 are all arms.
What remains is the seventeen arms, and the coordinator's own disposition (2026-08-29, above) governs them: extract per-arm, on demand, driven by a queued ticket — never as a sweep, with the general question left open until two or three real extractions exist so it can be settled from a diff rather than a sketch. An agent handed this ticket cold finds a dry census and a deferred decision — which is precisely the state that produces the sweep nobody asked for, at seventeen sites, in the shared dispatchers of five live lanes.
The ticket's next move is a queued C lowering ticket reaching an arm, not an agent reaching for this ticket. It sits at prio 60 for the lane it unblocks, not because it has runnable work in it today.
The trap is LIVE for the next census, not historical
Flagged by frankA, 2026-08-30, on the right grounds — commits are not where the next holder looks first — so it is restated here as a standing hazard rather than left as slice 1c's war story:
callers.py counted mentions, not calls, and Pascal keeps prose in the same
files as code. A comment naming a routine from outside ir.inc/cir.inc
therefore fails the reached-only-from-C test and silently removes that routine
from the candidate list. IRBitStorageTk was hidden this way by
cparser.inc:12547 — a sentence explaining the routine disqualified it — through
two consecutive censuses.
fe78e0cb9 is the only thing disarming it. A census re-implemented from
scratch re-arms it, and the failure is invisible in the expensive direction:
there is no wrong row to be suspicious of, only a routine that is not there.
Anyone running the census again should start from that fix, not from the rule.