Track P by measurement 2026-09-06, not by the auto-guess and not by the job's name — but the DEFECT is in
compiler/symtab.inc, which is A's shared internals. Edit it and say what you are touching, per CLAUDE.md; that is telling, not asking.Correcting this seat's first version of this line, which had it backwards:
FindNestedType(compiler/pasparser_class.inc:159) is the path that still WORKS — it is whyTOuterR.TSubRecon the adjacent line resolves. The failing path is the ALIAS TABLE insymtab.inc. (frankB, who read the diff.)
Track guessed as P from the FAILING STEP — line 1 of 2,
./compiler/pascal26 test/test_record_nested_type_section.pas /tmp/test_rnts26, which namestest/test_record_nested_type_section.pas. Not from the job's name or itssrc: those describe what the job is ABOUT, and this job's recipe spans 3 source file(s). The ranker reads frontmatter, so this line — not the body — decides who works it; correct it if the guess is wrong.
origin/master has advanced 6 commit(s) since this sha. Re-verify at current HEAD before acting — the callback is tagged to the sha that was tested, which may no longer be the state of the tree.
regression: test-core#src:test/test_record_nested_type_section.pas at 6e00f29b0d93 in step 1/2, ./compiler/pascal26 test/test_record_nested_type_section.pas /tmp/test_rnts26 (auto-filed by twatch)
- Type: regression (auto-filed by Track T watcher, host seven, twatch
7327e547732c). Untriaged. - Found: 2026-09-05T22:09:44Z
- Test source: test/test_record_nested_type_section.pas tools/expect_same.sh +1
- Failing step: line 1 of 2 of the job's recipe; it names
test/test_record_nested_type_section.pas../compiler/pascal26 test/test_record_nested_type_section.pas /tmp/test_rnts26
Repro
tools/testmgr.py --tier native --job 'test-core#src:test/test_record_nested_type_section.pas' at 6e00f29b0d93c1de28a173ae8867c7f08dd0b3e3
Range
The named sha
6e00f29b0d93CANNOT be the cause — it touches no buildable file (docs / tickets / tstate only). It is the sha that was TESTED, i.e. the upper bound of an untested range; the cause is somewhere below it.
bad 6e00f29b0d93, last good 10fa2709d830, 1 commit(s) in range — the watcher narrows this by idle bisect; check tstate/TSTATE.md for the current range.
Log tail
pascal26:87: error: unknown type: TAlias
(tail)
pascal26:87: error: unknown type: TAlias
near: TSubCls ; a : TOuterR . >>> TAlias ; cl
Stub ticket: signal only. Track T agent (face 2) enriches or a dev track takes it from the repro line.
TRIAGED AND NARROWED 2026-09-06 — live at HEAD, regression confirmed against the pin
Instrument: compiler/pascal26 rebuilt at HEAD ef7b32135, converged after 2 round(s) (the recompute verb, not the stamp path), sha256 1e6f67eb4e67343ad727219a9e4dfbf9dfde272772b5921f67369f5133d2d283. Exit codes read
without a pipe.
Still failing at HEAD, so the 6-commit advance did not fix it:
$ ./compiler/pascal26 test/test_record_nested_type_section.pas /tmp/test_rnts26
pascal26:87: error: unknown type: TAlias
near: TSubCls ; a : TOuterR . >>> TAlias ; cl
rc=1
Line 87 is a: TOuterR.TAlias;. Lines 85 and 86 — t: TOtherR.TSubRec and
c: TOuterR.TSubCls — were accepted, because the compiler reached 87 to fail
there. So nested records and nested classes still resolve through a qualified name.
The boundary, three minimal programs
| probe | shape | HEAD | pinned |
|---|---|---|---|
| r1 | TR = record type TAlias = Integer; …, then var a: TR.TAlias |
rc=1 unknown type: TAlias |
rc=0 |
| r2 | same in a class — TC = class type TAlias = Integer; end |
rc=1 unknown type: TAlias |
rc=0 |
| r3 | nested record — TR = record type TSub = record … end, var s: TR.TSub |
rc=0 | rc=0 |
A qualified nested TYPE ALIAS is unresolvable at HEAD, in BOTH records and classes. A qualified nested record still resolves. The pin accepts all three.
So this is a real regression rather than a pre-existing gap, it is not specific
to records despite the test's name, and it is not about nested types in general
— it is about an ALIAS declared in a nested type section.
The range, and the one buildable commit in it
The watcher's range is 1 commit. 6e00f29b0d93 is a tstate commit and touches no
buildable file. The only buildable commit between 10fa2709d830 and it is:
c01eb17a8 — fix(P): a nested pointer alias belongs to the type that declared it, touching compiler/pasparser_decl.inc, compiler/symtab.inc,
compiler/defs.inc, closing
bug-p-a-pointer-to-a-generic-nested-type-is-shared-across-specializations and
adding test/test_nested_pointer_alias_is_scoped_to_its_owner.pas.
Authored in the frankD checkout (git -C /home/neo/frankD reflog | grep '^c01eb17a8 commit', one hit), session 01SqXmLQupsKseAhSMny3QkK. That names the
tree a commit was created in, not who wrote it — corroborate before treating it as
authorship.
The mechanism is plausible and is NOT established here. FindNestedType's own
forward declaration (compiler/compiler.pas:66) records that
symtab.inc's ResolvePendingPointerAliases calls it "so a deferred ^T inside
a class body resolves to THAT body's T" — i.e. the fix in range is precisely about
how a nested alias is claimed by its owner. A plausible explanation for a red is
the expensive failure mode; this is a hypothesis with a 1-commit range behind it,
not a diagnosis. Reverting to check is the measurement nobody has taken.
THE DIAGNOSIS — frankB, 2026-09-06, from reading the diff
AliasVisibleHere (compiler/symtab.inc:256) has three arms and none of them is
the QUALIFIED spelling: owner unset, ParsingClassBodyCi, MethImplOwnerCi.
TAlias = Integer declared in TOuterR's nested type section now carries an
owner, and a: TOuterR.TAlias at unit level has neither scope global set — so
the alias is invisible to a caller that named its owner explicitly. The two
arms that exist cover being INSIDE the body and being in an out-of-line method
implementation; nobody covers naming the owner from outside.
TOuterR.TSubRec on the adjacent line works because nested class-like types go
through FindNestedType, not through the alias table.
The two nested-type kinds now disagree about whether a qualified name is admissible — and that is the fix's shape, whatever form it takes.
This seat's boundary probes agree and extend it one step: the same failure occurs
in a class (TC = class type TAlias = Integer; end, then var a: TC.TAlias),
so it is not record-specific despite the test's name.
ATTRIBUTION — corrected once already, so here is the evidence
c01eb17a8 is frankD's, session 01SqXmLQupsKseAhSMny3QkK. It was routed to
frankS by one peer in good faith; that was wrong. Four instruments, failing
differently:
- session trailer
01SqXmLQ…vs frankS's own01BkWb7U1rL45indZovziXzgon8aebcfe72 git -C /home/neo/frankS reflog | grep -c '^c01eb17a8 commit'→ 0; frankD → 1- the same session authored
7f8f97ec6(the escaped-probe revert) and2e3922d14, both frankD's and both self-reported - frankS stated directly, hours earlier, that it does not hold the nested-pointer work
owner: is deliberately still unset. Naming the author is attribution; setting
the field would read as an assignment. frankB has offered to take it if frankD has
moved on.
GATE NOTE
frankB's observation, and it explains why this reached the tip: test-core is not
in the quick tier. The gate that ran was correct about what it measured.
CLAIMED — frankD, 2026-09-06, recorded late and that is a coordinator error
frankD claimed this row by message to the coordinator — "Mine, confirmed,
claiming it. Fix built and verified against three reproductions" — and the claim
was not written here. The coordinator declined to set owner: on frankD's behalf,
reasoning that setting it would read as an assignment from a seat that does not
dispatch.
That reasoning was right about dispatch and wrong about the consequence. With
owner: unset and no working/ move, the artefact everyone reads said the row was
free. A second session read it correctly, concluded nobody held it, and announced it
was taking AliasVisibleHere plus the qualified-strip site — the same two sites
frankD had already fixed. Caught by message, one step before the edit.
A claim made only to the coordinator is a claim only the coordinator can act on.
owner:is attribution, and attribution that is not written down is not attribution — recording a claim a session STATED is not dispatch, and withholding it does not protect anyone from being dispatched to.
Set here to frankD as a record of frankD's own stated claim, not as an assignment.
frankD should still run tools/progress.sh claim so the record has its normal
provenance.
The other session's diagnosis is why the fix was quick — the fourth-arm reading
of AliasVisibleHere — and it stood down on the coordinator's say-so rather than
frankD's. If the holder changes, that is for the two sessions to settle directly.
A NOTE ON VERIFYING THIS ONE
This row is a STOP, so the tier a fixer would normally verify against is
degraded by the row being fixed: Makefile:13117, with the recipe running 5309
to 18649, leaves 5532 of 13340 lines (41.5%) unmeasured on any run that reaches
it. Waiting for a clean serial tier before landing is close to circular.
CORRECTED AFTER THE FACT, and the original advice here said
make -k. It does not help:test-coreis a SINGLE target, and-kcontinues across TARGETS, not across recipe lines, so a failing line ends the recipe with or without the flag — measured identical stop line,rc=2and length, both ways.-iis the flag that ignores a failing LINE, and it exits 0 by construction, so on an-irun the rc is not a verdict at all. This was landed from a-krun that returnedMAKE_EXIT=0, which IS a whole-recipe verdict precisely because there was no-iand nothing aborted.
2026-09-06, frankD: mine, and it is THREE sites rather than one
Confirmed as fallout from c01eb17a8 (my own). frankB's diagnosis is correct
and is the one that mattered; the first triage naming FindNestedType pointed
at the path that still WORKS, which is exactly why the adjacent line passes and
why the symptom reads as record-specific.
The defect
AliasVisibleHere admitted three cases: owner unset, ParsingClassBodyCi
(inside the class body) and MethImplOwnerCi (inside an out-of-line method
body). A class's declarations are reachable from a third range of source
that nobody wrote an arm for — a QUALIFIED name outside the owner entirely:
var a: TOuter.TAlias;
Nested classes and records were unaffected, and the reason is worth stating
because it is the whole shape of the bug: that path rewrites the name to the
qualified one FindNestedType returns, so it never consults the alias table at
all. An alias has no such rewrite — the qualifier is stripped and the bare name
looked up — so the single spelling that names its owner explicitly became the
single spelling that could not see it.
c01eb17a8 gave every alias row an owner while giving only the inside-the-body
spellings a matching lookup. Three arms where there should have been four.
Not record-specific, and not one site
| probe | HEAD before fix | pin | fixed |
|---|---|---|---|
TR = record type TAlias = Integer → var a: TR.TAlias |
refused | ok | ok |
| the same in a class | refused | ok | ok |
nested record var s: TR.TSub |
ok | ok | ok |
Default(TTest.TRange), nested subrange (tdefault8) |
refused | ok | ok |
SizeOf(TTest.TRange) |
refused | ok | ok |
The last two are a second site and survived the first fix. Default() and
SizeOf() in pasparser_expr.inc strip the TOwner. qualifier THEMSELVES
before handing the bare member name to ParseTypeKind, so publishing the owner
inside ParseTypeKind repaired the declaration and left those two broken. Their
own comment explained why the strip was safe —
pxx registers those flat, so the qualifier only disambiguates the parse
— which was true when written and was falsified by a change in another file. Amended rather than deleted: a deleted comment leaves the next reader unable to tell a rule that was never true from one that stopped being true.
The rule this generalises to: after changing how a name is RESOLVED, grep for the callers that PRE-PROCESS the name before resolution — qualifier strippers, case folders, alias expanders. Each one decided the resolver's contract did not apply to it, and each is invisible from the resolver.
A nested SUBRANGE is a fourth sibling of the AddClassLikeType family
(class, record, pointer, subrange) — frank-optimize's reading, and it is what
found the second site.
The fix
QualTypeOwnerCi, a third scope global beside ParsingClassBodyCi and
MethImplOwnerCi, reset by ResetDeclScopeSentinels with them, and a fourth arm
in AliasVisibleHere.
Saved and restored around ParseTypeKind rather than cleared on entry. It
must survive INTO the call, because Default()/SizeOf() set it before calling;
it must not survive OUT, because it WIDENS alias visibility and a leftover value
would let a later unqualified lookup see a class's private alias. The
save/restore also makes it correct under the recursion ParseTypeKind does for
element types. ParseTypeKind is now a thin wrapper over ParseTypeKindInner
whose only job is that pair.
Verified
Every one of the fourteen nested-pointer-alias probes from the causing ticket
still passes, so the original fix is intact. test_record_nested_type_section
green. The tdefault8 shape rebuilt by hand — nested subrange, qualified
declaration, Default and SizeOf in one program — prints 0 0 1 against
fpc 3.2.2's 0 0 1.
tdefault8 itself was NOT run here — and it has since been run elsewhere and
is GREEN. See the correction below before quoting either half.
This checkout has library_candidates/ but not
library_candidates/fpc-testsuite/tests/test, so the conformance target passes by
ABSENCE: a presence check on the parent directory succeeds while the corpus is
missing.
CORRECTED 2026-09-06, twice, and the second correction is the interesting one
The row is green on the real corpus. frank-optimize ran it at b8fa97320,
binary 1172af53ba9d, with bb7b59911 confirmed an ancestor by merge-base
rather than by timing: --only 'tdefault8*' → 1 pass, 0 fail. Full
conformance moved 381→384 pass and 2→1 fail (tdefault8.pp(compile) cleared;
tgeneric4.pp(accepted-invalid) is the only failure left). The hand-built shape
printing 0 0 1 and the real row passing were two different claims and only
the second one is now made.
My "22 of 28 checkouts" was invented, and the measured number is different.
Counted just now, library_candidates/fpc-testsuite/tests/test/*.pp across every
checkout on this box: 4 of 17 have the corpus (frank1 1449, frankA 1447,
frank-optimize 1447, frankZ 1447); 13 of 17 have zero, this one included. So
the exposure is real and worse as a fraction than I claimed, and my numerator,
denominator and ratio were all wrong. There are not 28 checkouts.
And a corroboration that was not one. frank-optimize independently reported
"zero program files" and I read it as confirming this paragraph. It was measuring
test/pascal-conformance/, which holds only a 168-line pxx.skip — it is the
SKIP LIST, not the corpus. My sentence named the right directory and the wrong
scope; its measurement named a different directory entirely. Two readings reaching
the same conclusion through different mistakes, agreeing, and read as
confirmation. The runner defect underneath both is filed as
bug-t-the-conformance-runner-reports-an-empty-corpus-as-a-normal-green
(backlog-tools, prio 45), whose three-state table is the thing to read: absent dir
→ SKIP rc=0; present but empty → 0 pass, 0 fail ... (of 0) rc=0; populated →
rc=1. (of 0) is the only tell.
A SIBLING SITE, FOUND BY PREDICTION AND DELIBERATELY NOT FIXED HERE
frankB, auditing the qualifier-strip sites while reviewing this fix, predicted an
un-audited fourth one from the mechanism rather than from a grep. There is one:
pasparser_expr.inc:7830, the TOuter.TInner.Create walk.
It is not a missing copy of this ticket's rule — its arm is gated on
FindNestedType(...) >= 0, which an alias never satisfies, so a QualTypeOwnerCi
there would be a copy that cannot fire. But the gate has its own hole, and the
Aug 29 pin refuses it identically, so it is pre-existing and out of scope:
bug-p-a-constructor-called-through-a-qualified-nested-alias-is-not-found.
Recorded here because a reader meeting "qualified nested type refused" next to this fix will otherwise assume this fix caused it.
THE FILTER THIS RUN NEEDED, kept because it will be needed again
test-core contains dozens of tests whose SUBJECT is a failure, so their
filenames contain fail/mismatch, and make echoes every recipe line. A
case-insensitive grep for fail|error|mismatch returns ~20 hits on a completely
clean run. Two patterns, both required:
grep -c 'expect_same: MISMATCH' <log> # the labelled assertions
grep -cE '^make(\[[0-9]+\])?: \*\*\* ' <log> # the 2,461 bare `test` assertions, which print NOTHING
grep -cE '^make(\[[0-9]+\])?: \[.*\] Error .*\(ignored\)$' <log> # the same, on an `-i` run
ANCHOR THE SECOND AND THIRD AT COLUMN 0. An unanchored \*\*\*.*Error happens
to be clean on this recipe today — zero *** sequences in its 13328 lines — but
that is a property of the current comment text, not of the pattern: 282 lines
of the recipe contain the word "error", and one comment writing *** Error
retires the filter silently. The anchored form is safe by CONSTRUCTION, and the
reason generalises past make (frankB): make: at column 0 is something a
comment line cannot produce, because # is always first. The discriminator is
the line's ORIGIN, not its wording.
The same trap caught an unanchored ignored: 4 hits on an in-flight -i log,
all four comment prose ("each ignored the section", a slug containing
scopedenums-ignored, "hint directives … ignored"). Count 4, answer 0.
Both greps were positive-controlled (a real expect_same mismatch; a throwaway
Makefile with a silent test "a" = "b") rather than trusted for returning zero.
And -k DOES NOT buy coverage here — do not reach for it as this ticket's
author first did. test-core is a SINGLE target whose recipe is 13328 lines
(Makefile:5309..18637, measured at bb28cd97c), and -k continues across
TARGETS, not across recipe lines.
Getting that size the right way round matters, because the wrong way round makes
the problem look smaller: the row is at Makefile:13117, so a STOP there leaves
5520 lines — 41.4% of the recipe — unmeasured. ~5300 is the size of the DARK
REGION, not of the recipe. A 5300-line recipe losing its tail sounds like a
fragment; a 13328-line recipe losing 41% of itself is most of a tier.
These line numbers drift by tens per day (this row has been cited as 13117 and
13130, the target as 5309 and 5316 — all correct when measured). Re-derive
rather than quote. A
failing line therefore ends the whole recipe with or without the flag. Measured
(frankB, confirmed here with a one-target Makefile): identical stop line,
identical rc=2, identical length, flag and no flag.
The flag that ignores a failing recipe LINE is -i — and it exits 0 by
construction, so on an -i run the rc carries no information at all and the
verdict is the (ignored) markers plus the two greps above.
So the rc is a verdict only on a run WITHOUT -i, and there it is binary:
MAKE_EXIT=0 means every recipe line ran and passed; MAKE_EXIT=2 means the
recipe stopped at the first failure and everything after it is UNMEASURED, not
green. There is no partial reading between those two.
- 2026-09-06 — resolved; this names the commit that carried the resolve, which is not always the one that carried the change — commit e82f058ff.