← board

A nested record's field cannot see a sibling nested type

type
  TFac = class
  public type
    TFacClass = class of TFac;
    TVMT = record __ClassRef: TFacClass; end;   { error: unknown type: TFacClass }
  end;

Declared two lines above, same section. Not a forward reference.

Boundary, varied rather than assumed

shape pin v404 HEAD before fix
nested type used by a nested record field ok unknown type
same, plain alias instead of class of ok unknown type
the same two declarations at unit level ok ok
nested type used by a nested pointer ok ok

The pointer arm working is why this read as already fixed.

Cause

AliasVisibleHere was four arms: owner < 0, = ParsingClassBodyCi, = MethImplOwnerCi, = QualTypeOwnerCi. Parsing a nested type's body does savedPCB := ParsingClassBodyCi; ParsingClassBodyCi := ci (pasparser_decl.inc, both body parsers), so within the nested record's field list the enclosing class is no longer that variable's value.

The arm count had gone 2 → 3 → 4, each added by a session with a failing test and no way to see the ranges it was not testing, and this would have been the fifth. frankB's statement of why that never terminates is worth keeping: "an enumerated predicate makes the arms you HAVE visible and says nothing about the ranges nobody has tested — so it reads as a census while being a log of what has broken so far."

Fix

The nesting chain existed only as savedPCB locals on the parser's call stack — nowhere addressable, so every consumer re-derived it from whichever single variable was in scope. UClsEnclosingCi writes it down at the two points the body parsers already have the enclosing ci in hand, and AliasOwnsThrough walks it. That collapses arms 1 and 2, which were both approximations of "somewhere in the enclosing chain".

Arms 3 and 4 stay exact comparisons, deliberately, and I wrote it the wrong way first. An out-of-line method body is not lexically inside anything, and QualTypeOwnerCi is — frankB's words — "a lookup being told which owner to answer for": FindNestedClassLikeCi sets it around a FindTypeAlias call while walking an ancestor chain, from outside any lexical nesting. Walking a chain from either answers a different question than the caller asked.

Provenance and verification

Bisected over the 63 commits in 5b5fdb0b3 (pin v404) ..de4bf2245, six builds, each seeded from the pin with the touch after the cp, each accepted only on converged after and a built sha differing from the seed. 60666ec36 good, c01eb17a8 bad.

Census 1876 files, 0 rows differ against a baseline rebuilt at the same tree — re-taken because the first comparison was contaminated: sync.sh had rebased other agents' commits in between the two arms, including one that added two test files, so the file count moved and the diff was not about my change. Conformance 384 pass / 1 fail and fgl 7/7, both unchanged.

test/test_nested_alias_visible_through_enclosing_chain.pas has five rows, and row 3 is depth 2 — a record inside a record inside the class, naming a type owned by the outermost. A fifth arm comparing against "the enclosing class" passes row 1 and fails row 3, so that row is what distinguishes the rule from one more special case. Row 4 keeps the pointer spelling so a future fix cannot repair fields by breaking pointers. Controls: the pin prints NESTEDCHAIN OK (the behaviour existed and was lost), and the pre-fix binary refuses to compile it.

What it unblocked, and what it did not

Corpus rung 6a (generics.defaults) advances from :1107 unknown type: PSpoofInterfacedTypeSizeObject to :1865. It is still red, on a second and unrelated regression in the same window: a class method reached through a class-REFERENCE field is parsed as a FIELD READ. The statement spelling errors with statement is neither a call nor an assignment; the EXPRESSION spelling compiles and silently yields garbager := PP(p)^.__ClassRef.Val(3) printed r=-86205216 with the method never entered, where fpc 3.2.2 and pin v404 both print SIDE called n=3 and 42. The pin is correct, so it is a regression too, and it is not this one. Reproducer, measured cause (the parser returns AN_FIELD with the ( unconsumed) and a bisect recipe are in bug-p-a-class-method-through-a-class-ref-field-is-parsed-as-a-field-read.

The ladder had recorded rung 6a green and nobody re-ran it — the file's own standing lesson, which is why the first thing this session did with the ticket was re-run the rung rather than read the table.