fix(arrays): enum-indexed multi-dimensional arrays (#128)

A multi-dimensional array whose dimensions are enum types — array[TEnum, TEnum]
or array[TEnum, 0..1] — failed to parse with "Expected '..' but got ','". Only
the single-dimension form array[TEnum] was handled; every dimension in the
comma-separated multi-dim path was forced through the lo..hi range parser.

Both the const-array dimension parser (ReadConstArrayDim) and the type-name
parser (ParseTypeName) now recognise an enum-type dimension — a bare identifier
not followed by '..' — and encode it as the range 0 .. @TEnum, reusing the
existing single-dimension '@TEnum' convention. ResolveArrayBound resolves the
'@TEnum' high-bound marker to the enum's last ordinal (member count - 1). This
fixes const, type and var declarations in any dimension position and mix
(enum+range, range+enum, 3-D enum+range+enum).

Adds dual-backend e2e coverage: 2-D enum/enum, mixed dims in both orders,
type+var form, and a 3-D mixed case. All three fixpoints green; 3499 tests pass.
This commit is contained in:
Graeme Geldenhuys 2026-06-18 11:25:15 +01:00
parent 517ed26f5d
commit 937146a3ac
3 changed files with 130 additions and 3 deletions

View file

@ -269,9 +269,23 @@ begin
Highs := TStringList.Create();
try
repeat
Lows.Add(Self.ReadConstBoundText());
Expect(tkDotDot);
Highs.Add(Self.ReadConstBoundText());
{ An enum-type dimension is a bare identifier NOT followed by '..'
(e.g. the TEnum dimensions in 'array[TEnum, 0..1]'). Encode it as
0 .. @TEnum so the range-indexed type resolver folds the high bound
to the enum's last ordinal — the same '@TEnum' convention used for
the single-dimension 'array[TEnum]' form above. }
if Check(tkIdent) and (PeekKind() <> tkDotDot) then
begin
Lows.Add('0');
Highs.Add('@' + FCurrent.Value);
Advance();
end
else
begin
Lows.Add(Self.ReadConstBoundText());
Expect(tkDotDot);
Highs.Add(Self.ReadConstBoundText());
end;
if not Check(tkComma) then
Break;
Advance(); { consume ',' — next dimension }
@ -1031,6 +1045,18 @@ procedure TParser.ReadConstArrayDim(CD: TConstDecl);
var
LoText, HiText: string;
begin
{ An enum-type dimension is a bare identifier NOT followed by '..' (e.g. the
TEnum in 'array[TEnum, 0..1]'). Encode it as the range 0 .. @TEnum so the
semantic pass resolves the high bound to the enum's last ordinal — the same
convention used for single-dimension 'array[@TEnum]'. }
if Check(tkIdent) and (PeekKind() <> tkDotDot) then
begin
CD.ArrayDimLows.Add('0');
CD.ArrayDimHighs.Add('@' + FCurrent.Value);
Advance();
CD.ArrayIsRangeIndexed := True;
Exit;
end;
LoText := Self.ReadConstBoundText();
Expect(tkDotDot);
HiText := Self.ReadConstBoundText();

View file

@ -3907,6 +3907,18 @@ var
begin
if IsPlainInt(ABoundText) then
Exit(Integer(StrToInt(ABoundText)));
{ Enum-type dimension marker '@TEnum' (see ReadConstArrayDim): the high bound
of an enum-indexed dimension is the enum's last ordinal (member count - 1).
The low bound is always 0, stored as plain '0'. }
if (Length(ABoundText) > 0) and (ABoundText[0] = '@') then
begin
Sym := FTable.Lookup(Copy(ABoundText, 1, Length(ABoundText) - 1));
if (Sym <> nil) and (Sym.Kind = skType) and (Sym.TypeDesc <> nil) and
(Sym.TypeDesc.Kind = tyEnum) then
Exit(TEnumTypeDesc(Sym.TypeDesc).Members.Count - 1);
SemanticError(Format('Unknown enum type ''%s'' in array dimension',
[Copy(ABoundText, 1, Length(ABoundText) - 1)]), 0, 0);
end;
Sym := FTable.Lookup(ABoundText);
if (Sym <> nil) and (Sym.Kind = skConstant) then
Exit(Integer(Sym.ConstValue));

View file

@ -68,6 +68,11 @@ type
procedure TestRun_ArrayConst_ElementWidths;
procedure TestRun_ArrayConst_NegativeInts;
procedure TestRun_ArrayConst_EnumIndexedBool;
{ Enum-indexed multi-dimensional arrays (GitHub #128) }
procedure TestRun_EnumBidimArray_Const;
procedure TestRun_EnumBidimArray_MixedDims;
procedure TestRun_EnumBidimArray_TypeAndVar;
procedure TestRun_EnumBidimArray_ThreeDims;
{ Static array return by value (GitHub #112) }
procedure TestRun_StaticArrayReturn_12Bytes;
@ -915,6 +920,90 @@ begin
'open' + Chr(10) + 'closed' + Chr(10) + 'open' + Chr(10), 0);
end;
procedure TE2EGapTests.TestRun_EnumBidimArray_Const;
{ GitHub #128: a 2-D array const indexed by an enum in both dimensions —
array[TEnum, TEnum] — previously failed to parse ("Expected '..' but got
','"). }
const
Src =
'''
program P;
type TEnum = (one, two);
const d: array[TEnum, TEnum] of byte = ((1, 2), (3, 4));
begin
WriteLn(d[one, one]);
WriteLn(d[one, two]);
WriteLn(d[two, one]);
WriteLn(d[two, two])
end.
''';
begin
AssertRunsOnAll(Src,
'1' + Chr(10) + '2' + Chr(10) + '3' + Chr(10) + '4' + Chr(10), 0);
end;
procedure TE2EGapTests.TestRun_EnumBidimArray_MixedDims;
{ Mixed dimensions: an enum dimension and an integer-range dimension together,
in both orders. }
const
Src =
'''
program P;
type TE = (a, b);
const
e: array[TE, 0..1] of byte = ((5, 6), (7, 8));
f: array[0..1, TE] of byte = ((10, 20), (30, 40));
begin
WriteLn(e[a, 0]);
WriteLn(e[b, 1]);
WriteLn(f[0, a]);
WriteLn(f[1, b])
end.
''';
begin
AssertRunsOnAll(Src,
'5' + Chr(10) + '8' + Chr(10) + '10' + Chr(10) + '40' + Chr(10), 0);
end;
procedure TE2EGapTests.TestRun_EnumBidimArray_TypeAndVar;
{ The same enum-indexed multi-dim form in a type declaration and a var. }
const
Src =
'''
program P;
type
TE = (a, b, c);
TGrid = array[TE, TE] of byte;
var g: TGrid;
begin
g[a, c] := 5;
g[c, a] := 9;
WriteLn(g[a, c]);
WriteLn(g[c, a])
end.
''';
begin
AssertRunsOnAll(Src, '5' + Chr(10) + '9' + Chr(10), 0);
end;
procedure TE2EGapTests.TestRun_EnumBidimArray_ThreeDims;
{ Three dimensions, enum + range + enum. }
const
Src =
'''
program P;
type TE = (a, b);
const m: array[TE, 0..1, TE] of byte =
(((1,2),(3,4)), ((5,6),(7,8)));
begin
WriteLn(m[a, 0, a]);
WriteLn(m[b, 1, b])
end.
''';
begin
AssertRunsOnAll(Src, '1' + Chr(10) + '8' + Chr(10), 0);
end;
procedure TE2EGapTests.TestRun_IntfSret_SixArgs_DirectClassCall;
{ Class instance whose method returns an interface and takes 6 Integer args.
On native this is the class-sret interface call path (>4 user arg slots);