From 566fc51d2a83daa9361692c23cd83f0b7484dcdf Mon Sep 17 00:00:00 2001 From: Patrick Quist Date: Mon, 31 Aug 2026 17:17:12 +0200 Subject: [PATCH] Accept a constant expression as an array bound An array bound may be any constant expression, but OrdinalType only ever accepted a constant or a type name: const mlab = 4; mlog = 12; type TRanges = record iu: array[mlab + 1..mlog] of Integer; end; failed with 'SquareClose' expected found '+'. OrdinalType decided on a single token of lookahead: an identifier followed by anything but '(' or '..' was a type name, so it read the bound as the type `mlab`, returned, and left ArrayBounds looking for the ']' it found a '+' at. The upper bound already worked, which is what makes the gap easy to miss. `array[mlab..mlog + 1]` sends the first bound to ConstantExpression on the '..' lookahead, and OrdinalType's own trailing '..' branch parses the rest as an expression. Only the first bound of a subrange went down the type-name path. The lookahead now also routes the operators SimpleExpression and Term accept to ConstantExpression. Every one of those tokens is a parse error in this position today, so no input that parses now takes a different path: an identifier in an OrdinalType is followed by ']', ',', '..', 'of' or ';', never by an operator. Set types and variant record tag types go through the same procedure and gain the same forms. A single token is enough here and a full ahead-parse would be worse. Coming from the identifier, SimpleType's `AheadParse.NextToken; AheadParse.Simple- Expression` idiom would meet the ']' of the common `array[TIndex] of Byte` and hand it to Factor as a set constructor. Parenthesized bounds are deliberately left alone. `array[(mlab + 1)..mlog]` goes to EnumeratedType, and dcc32 reads it the same way - it reports "Identifier redeclared: 'mlab'" - so the parser already agrees with the compiler. Test/Snippets/arrayboundexpression.pas covers the first bound, both bounds, two dimensions, `*`, `-` and `shl`, and a set of a computed subrange; dcc32 compiles it clean. Without the parser change it fails with the original 'SquareClose' expected found '+', so it guards the actual bug. Suite: 43 tests, 42 passing, with the pre-existing Serialization.BinaryRoundTrip failure unchanged (line_seq holds a pointer value that does not survive a round trip). Co-Authored-By: Claude Opus 5 (1M context) --- Source/SimpleParser/SimpleParser.pas | 10 ++++++++++ Test/Snippets/arrayboundexpression.pas | 22 ++++++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 Test/Snippets/arrayboundexpression.pas diff --git a/Source/SimpleParser/SimpleParser.pas b/Source/SimpleParser/SimpleParser.pas index 06caf50..f632596 100644 --- a/Source/SimpleParser/SimpleParser.pas +++ b/Source/SimpleParser/SimpleParser.pas @@ -3648,6 +3648,16 @@ procedure TmwSimplePasPar.OrdinalType; begin ConstantExpression; end; + { An operator after the identifier means the bound is a constant + expression, not a type name: array[mlab + 1..mlog]. Reading it as a + type name consumes the identifier alone and leaves the caller at the + operator, where it can only report an error. These are the operators + SimpleExpression and Term accept. } + ptAnd, ptDiv, ptMinus, ptMod, ptOr, ptPlus, ptShl, ptShr, ptSlash, + ptStar, ptXor: + begin + ConstantExpression; + end; else begin TypeID; diff --git a/Test/Snippets/arrayboundexpression.pas b/Test/Snippets/arrayboundexpression.pas new file mode 100644 index 0000000..1d695d1 --- /dev/null +++ b/Test/Snippets/arrayboundexpression.pas @@ -0,0 +1,22 @@ +unit arrayboundexpression; + +interface + +const + mlab = 4; + mlog = 12; + +type + { An array bound is a constant EXPRESSION, not just a constant or a type name. + OrdinalType decided on one token of lookahead and read `mlab` as a type name, + so the whole unit failed to parse at the `+`. } + TRanges = record + iu: array[mlab + 1..mlog] of Integer; + du: array[mlab * 2..mlog - 1, 0..mlab shl 1] of Byte; + end; + + TSetOfExpression = set of mlab + 1..mlog; + +implementation + +end.