← board

The shape

const
  c1 = 'A';            { a one-character untyped const -> a CHAR const }
  s1 = 'String1';      { a multi-character one         -> a StrConst span }
resourcestring
  R1 = 'Res1';         { a VARIABLE with storage, not a const at all }
const
  A : array[0..1] of shortstring = (s1, s1);   { array constant: expected a string or char literal }
  B : shortstring                = s1;          { typed string constant: expected a string or char literal }

FPC accepts all of these. tstring3.pp is the array form over all three source kinds at once; tinterface6.pp is the scalar form with a CORBA interface name on the right (its string UID).

Where

Three sites, one concept, all spelled if CurTok.Kind <> tkString then Error(...):

Each then does scOff := Tokens[TokPos-1].SOffset; scLen := ...; Next; and records a Kind=1 (string-literal span) init. The span is the only thing they need, and FindStrConst(name) already returns a row carrying StrConstSOff / StrConstSLen — the identical pair. So the fix is a shared TakeStrLitSpan(var scOff, scLen): Boolean that accepts a tkString token OR a tkIdent naming a StrConst, used at all three sites.

Three copies of one predicate is the smell normalise-dont-special-case.md names; the helper should replace them rather than growing a fourth arm.

Three sources, and only ONE of them is a StrConst

This is the part to measure before writing code, because the ticket that says "accept an identifier" is under-specified:

written how it is stored what the sites need
const s1 = 'String1' StrConst row: name + source span the span — free
const c1 = 'A' a CHAR const (AddConst(name, tyChar, ord)) — deliberately, bug-pascal-ord-of-a-one-char-string-const-is-its-address an ORDINAL, so it needs a char->string init kind, not a span
resourcestring R1 = 'Res1' a real initialised VARIABLE (DeclareInitialisedStringVar) — FPC's runtime-replaceable resourcestring a copy of the variable's value at init time, which is a different init kind again

tstring3 uses all three in one program, so it does not close until all three are handled. The scalar form (tinterface6) closes on the first row alone.

Do not read "one-character constants are char" as a defect on the way past — it is deliberate and has its own ticket's worth of evidence behind it.

Fixed 2026-09-05 (frankB)

compiler/pasparser_decl.inc. The ticket's own diagnosis was right and complete — three sites asking CurTok.Kind <> tkString with the span sitting in StrConstSOff/StrConstSLen — and there turned out to be a fourth: the + concatenation arm, which consumes CHARACTERS rather than a span and so could not share the same helper. Fixing only the three would have left const s1 = 'a'; X: ShortString = s1 + 'b' refused while X: ShortString = s1 compiled — one arm of a double case, with the sibling one line below.

So: TakeStrInitSpan (points at an existing span) and AppendStrInitText (builds a fresh one), and four copies of a token test become two functions rather than five.

Why this was a defect and not an unimplemented feature, which is the distinction the scope rule turns on: const n1 = 7; A: array[0..0] of Integer = (n1) compiled the whole time. Named constants ARE folded in initialisers. Only the string path never looked, so the compiler was already holding the text it said it could not find.

Both negative controls are ! steps that grep the error TEXT, not the exit code. A refusal has no output to diff, and a step asserting only "it failed" would pass if the fix made it fail for a new and worse reason. The declared-but-Integer row (n1) is the load-bearing one: an undeclared name is refused by almost any guard, while n1 is refused only by a guard that asks whether the name is a STRING constant — which is exactly what a loosening to "any identifier" would break, and it would break silently, with every positive row still green.

Log