← board

A conversion operator's destination string capacity has no carrier

class operator Implicit(const a: TTest): TString80 and the same operator returning TString90 are refused as duplicates. FPC accepts both declarations. The destination of a conversion operator is keyed on the return type's KIND, and every frozen-string kind compares equal (OpConvResultMatches, deliberately, so that string[N] resolving to tyString in one position and tyFixedString in another still matches — toperator93). The declared CAPACITY is what separates TString80 from TString90, and the proc's RESULT has nowhere to put it.

The absent carrier, enumerated

The capacity of a string[N] is carried once per carrier:

carrier declared
variable SymStrCap
type alias AliasStrCap
record field UFldStrCap
parameter ptypesStrCap (pasparser_proc.inc)
routine RESULT absent

Four present, all agreeing, and the fifth never conceived — the exact shape ProcRetSetEnumId's own comment describes for a different fact ("an ABSENT copy has no diff, so reading the five against one another could never have produced it"), and the shape ptypesStrCap's comment describes for the parameter channel ("the FOURTH member of the return-channel family above, and the one that was missing"). This file has now recorded the same pattern three times.

ProcRetPtrAlias is the precedent for the fix's shape: store the alias index rather than a copy of the pointee's geometry.

Needs a defs.inc slot — frankH messaged before it is taken.

The other half: the use-site ambiguity

Adding the capacity to the key is not sufficient on its own, and the reason is the interesting part.

toperator92 and toperator95 are %FAIL rows that PASS today, and they pass by refusing the wrong thing. FPC accepts their declarations and refuses at the USE site — toperator92:32 s := t; is Incompatible types: got "TTest" expected "TString80", i.e. ambiguous. pxx refuses at DECLARATION time, toperator92:28, duplicate conversion operator. Same verdict, different reason, and the harness compares only whether a refusal happened.

So making the key finer must be paired with a use-site ambiguity refusal, or those two rows flip from passing-for-the-wrong-reason to failing. Four rows move together: toperator91 and toperator94 (currently skipped, FPC compiles them) start passing; toperator92 and toperator95 keep passing and start doing it for FPC's reason.

A NEGATIVE RESULT, recorded so nobody repeats it

I censused all 212 %FAIL rows the harness runs and does not skip, comparing pxx's first error LINE against fpc's, on the theory that a wrong-reason refusal shows up as a line mismatch. It does not, and the census cannot answer this question.

The instrument cannot see the thing it was built for, and it would have reported 112 rows of nothing as a finding. The line is the wrong channel; the DIAGNOSTIC is the right one, which is what run_pascal_conformance.sh's own DIAGMAP note already says about the skip-list version of this question ("the diagnostic is the channel that can observe this class; the exit code cannot"). The same sentence holds one level over: for a %FAIL row the exit code cannot observe a wrong-reason refusal, and neither can the line number.

Both confirmed instances (toperator92, toperator95) were found by READING the duplicate-conversion check while working on toperator91, not by the census.

2026-09-09 (frankS) — RESOLVED, measured 2026-09-08. One lookup was answering two questions.

The carrier this ticket is named for landed 2026-09-07 and split EXPLICIT casts by result capacity. What was left is the implicit half, and it was not a missing column at all — it was the DECLARATION-time duplicate check and the USE-site lookup being the same call.

They need different answers:

question capacity
declaration is this the SAME RESULT TYPE as one already declared? compared exactly: String[80], String[90] and ShortString are three results
store which operator SERVES this destination? never filters; ranks only

Conflating them is what refused the pair fpc compiles. And the generic-fallback exemption inside OpConvResultMatches — a result of DEFAULT_STR_CAP matches any frozen-string destination — is correct for the store and wrong for the declaration: with it in force, declaring : TString90 found the ShortString operator and called the pair a duplicate, so a generic conversion could not coexist with a sized one at all. capExact turns it off for the declaration.

The rule at the store, measured cell by cell

Every obvious reading is wrong in some cell, which is why this is a table and not a sentence:

declared destination fpc 3.2.2
sized80 TString80 sized80
sized80 TString90 sized80 — capacity is not a filter
sized80 + generic TString80 generic — an exact capacity match LOSES
sized80 + generic + sized90 TString80 / TString90 generic
sized80 + sized90, no generic either refused, ambiguous

Row 3 rules out "prefer the closest capacity". Row 5 rules out "take the first applicable one" — with nothing to prefer, fpc does not choose. So: a rank plus a TIE TEST, and a tie is a refusal.

The refusal had to land at the store and not at the declaration, which is the whole point of the ticket. Falling through instead was not an option: a store that finds no conversion sends a record into a string RAW, which is a garbage length byte and a segfault in WriteLn — the same fall-through that segfaulted when this family was last touched.

The two rows that were passing for the wrong reason

toperator92 and toperator95 are %FAIL rows that PASSED while pxx refused the wrong thing — the declaration rather than the store. A harness that compares only whether a refusal happened cannot see that, and the earlier census recorded on this ticket established that the error LINE cannot see it either (toperator92's gap was 4 lines, inside the noise band). They now refuse at the store, for fpc's reason.

toperator94 is burned. toperator91 is not — see below.

Two divergences, measured, chosen

What is left, and it is one rule

toperator91 still halts, and the cause is now measured rather than described. Its old skip reason named the ShortString Implicit SIX TIMES ranking; that is not it. s40 := TString40(t) is an EXPLICIT cast to a capacity no Explicit operator has, and fpc falls back to the ShortString Implicit operator where pxx falls back to the ShortString Explicit one. Once that holds, the six implicit assignments follow from the generic preference that just landed.

Deliberately not taken here: it changes the explicit-cast FALLBACK, which is the path whose previous edit segfaulted, and which test_conversion_operator_result_capacity_is_part_of_its_identity.pas pins. One rule, one landing, with that fixture as its control. Filed as [[bug-p-an-explicit-cast-with-no-capacity-match-falls-back-to-the-wrong-conversion-operator]].

Fixtures

Verified, and what was NOT

Compiler 0f14028acc04, converged after 1 round(s).

Log