fix(parser): accept named integer subrange types (issue #130 bug1)

`type TByte = 0..255;` failed to parse — ParseTypeDecl had no case for an
integer-literal subrange, so the RHS fell through to the generic "expected
record/class/..." error.

Blaise does not range-check, so a named subrange is treated as an alias to
the narrowest STANDARD integer type that holds both bounds (0..255 -> Byte,
-10..10 -> SmallInt, etc.).  This keeps record/array element layout correct
(TByte is byte-sized) while the value behaves as an ordinary integer.  Two
parser helpers do the work: SubrangeAhead (lookahead: IntLit.. or -IntLit..)
and ParseIntegerSubrangeBaseType (parse lo..hi, pick the base type, reject a
descending range).  Note Blaise has no 8-bit signed alias, so a signed
subrange that would fit in ShortInt widens to SmallInt.

Only integer-literal bounds form a named type; identifier/enum-bounded
subranges (TLow..THigh, red..blue) are intentionally not handled here (they
are ambiguous as a named-type form).

Tests: parser tests (named subrange, negative bounds, descending-is-error)
and dual-backend e2e tests (named subrange runs; subrange as a record field
and array element with a negative range).  grammar.ebnf gains the
IntSubrange rule.

Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite
green on both the QBE-built and native-built runners (3691 tests).
This commit is contained in:
Graeme Geldenhuys 2026-06-20 15:56:18 +01:00
parent 3e94044550
commit 8c68b09245
4 changed files with 161 additions and 1 deletions

View file

@ -58,6 +58,8 @@ type
function Check(AKind: TTokenKind): Boolean;
function CheckUnitNamePart: Boolean;
function ParseTypeName: string; { reads Ident optionally followed by '<' ArgList '>' }
function SubrangeAhead: Boolean; { current token starts an integer-literal subrange: IntLit.. or -IntLit.. }
function ParseIntegerSubrangeBaseType: string; { parse lo..hi, return narrowest fitting standard integer type }
function ReadConstBoundText: string; { read an array bound: literal, ident, or expr }
function ParseAnonEnumName: string; { parse '(a,b,c)' → encoded member-list string }
@ -409,6 +411,70 @@ begin
end;
end;
function TParser.SubrangeAhead: Boolean;
begin
{ An integer-literal subrange in a type RHS: `0..255` or `-10..10`.
Recognised by a leading integer literal (optionally a minus sign) followed
by the '..' token. Pure lookahead consumes nothing. }
if Check(tkIntLit) then
Result := (PeekKind() = tkDotDot)
else if Check(tkMinus) then
Result := (PeekKind() = tkIntLit) and (PeekKind2() = tkDotDot)
else
Result := False;
end;
function TParser.ParseIntegerSubrangeBaseType: string;
var
Lo, Hi: Int64;
Neg: Boolean;
begin
{ Parse `lo..hi` (each an integer literal, lo optionally negative) and return
the name of the narrowest STANDARD integer type that holds both bounds.
Blaise performs no range checking, so a named subrange is just an alias to
this base type; the choice keeps record/array element layout correct
(e.g. 0..255 is byte-sized). }
Neg := False;
if Check(tkMinus) then begin Neg := True; Advance(); end;
if not Check(tkIntLit) then
raise EParseError.Create(Format(
'Expected integer literal as subrange lower bound at line %d col %d in %s',
[FCurrent.Line, FCurrent.Col, FLexer.Filename]));
Lo := ParseIntLiteral(FCurrent.Value);
if Neg then Lo := -Lo;
Advance();
Expect(tkDotDot);
Neg := False;
if Check(tkMinus) then begin Neg := True; Advance(); end;
if not Check(tkIntLit) then
raise EParseError.Create(Format(
'Expected integer literal as subrange upper bound at line %d col %d in %s',
[FCurrent.Line, FCurrent.Col, FLexer.Filename]));
Hi := ParseIntLiteral(FCurrent.Value);
if Neg then Hi := -Hi;
Advance();
if Hi < Lo then
raise EParseError.Create(Format(
'Subrange %d..%d is descending at line %d col %d in %s',
[Lo, Hi, FCurrent.Line, FCurrent.Col, FLexer.Filename]));
{ Pick the narrowest standard type covering [Lo, Hi]. }
if Lo >= 0 then
begin
if Hi <= 255 then Result := 'Byte'
else if Hi <= 65535 then Result := 'Word'
else if Hi <= 4294967295 then Result := 'Cardinal'
else Result := 'UInt64';
end
else
begin
{ Blaise has no 8-bit signed alias name (no ShortInt), so the narrowest
signed alias target is SmallInt (16-bit). }
if (Lo >= -32768) and (Hi <= 32767) then Result := 'SmallInt'
else if (Lo >= -2147483648) and (Hi <= 2147483647) then Result := 'Integer'
else Result := 'Int64';
end;
end;
function TParser.ParseAnonEnumName: string;
begin
{ Encode an inline '(a, b, c)' enumeration as the literal text '(a,b,c)'
@ -784,6 +850,17 @@ begin
TD.Def := ParseSetDef()
else if Check(tkFunction) or Check(tkProcedure) then
TD.Def := ParseProceduralTypeDef()
else if (Check(tkIntLit) or Check(tkMinus)) and SubrangeAhead() then
begin
{ Named integer subrange: type TByte = 0..255; type TIdx = -10..10;
Blaise does not range-check, so a subrange is an alias to the
narrowest standard integer type that holds both bounds. This keeps
record/array layout correct (TByte is byte-sized) while the value
behaves as an ordinary integer. }
AD := TTypeAliasDef.Create();
AD.TypeName := Self.ParseIntegerSubrangeBaseType();
TD.Def := AD;
end
else if Check(tkArray) or Check(tkCaret) or Check(tkIdent) then
begin
{ Array alias: type TArr = array[L..H] of T;
@ -795,7 +872,7 @@ begin
end
else
raise EParseError.Create(Format(
'Expected ''record'', ''class'', ''interface'', ''('', ''set'', ''function'', ''procedure'', or type name at line %d col %d in %s',
'Expected ''record'', ''class'', ''interface'', ''('', ''set'', ''function'', ''procedure'', subrange, or type name at line %d col %d in %s',
[FCurrent.Line, FCurrent.Col, FLexer.Filename]));
end;
Expect(tkSemicolon);

View file

@ -112,6 +112,12 @@ type
{ Nested generic type arguments: TList<TList<Integer>>. Was a parser gap
(type-arg list did not recurse, in both type and constructor position). }
procedure TestRun_NestedGenericTypeArgs;
{ Named integer subrange type (issue #130 bug1): the type-decl parser had
no integer-literal subrange case. A named subrange aliases the narrowest
standard integer type and carries no range checking. }
procedure TestRun_Subrange_NamedType;
procedure TestRun_Subrange_InRecordAndArray;
end;
implementation
@ -1177,6 +1183,43 @@ begin
AssertRunsOnAll(SrcNestedGeneric, '7' + LE, 0);
end;
procedure TE2EMiscTests.TestRun_Subrange_NamedType;
const
Src = '''
program P;
type TByte = 0..255;
var b: TByte;
begin b := 5; WriteLn(b) end.
''';
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
AssertRunsOnAll(Src, '5' + LE, 0);
end;
procedure TE2EMiscTests.TestRun_Subrange_InRecordAndArray;
const
{ A subrange as a record field and as an array element type, plus a negative
subrange exercises that the aliased base type sizes layout correctly. }
Src = '''
program P;
type
TByte = 0..255;
TIdx = -10..10;
TRec = record b: TByte; i: TIdx; end;
var
r: TRec;
a: array[0..2] of TByte;
begin
r.b := 200; r.i := -7;
a[0] := 1; a[1] := 250; a[2] := 99;
WriteLn(r.b, ' ', r.i, ' ', a[1])
end.
''';
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
AssertRunsOnAll(Src, '200 -7 250' + LE, 0);
end;
initialization
RegisterTest(TE2EMiscTests);

View file

@ -54,6 +54,11 @@ type
procedure TestChain_MethodThenMethodThenField;
procedure TestChain_FuncCallThenSubscript;
{ Named integer subrange types (issue #130 bug1) }
procedure TestSubrange_NamedType_Parses;
procedure TestSubrange_NegativeBounds_Parses;
procedure TestSubrange_Descending_Error;
{ Error cases }
procedure TestError_MissingProgramKeyword;
procedure TestError_MissingDot;
@ -527,6 +532,29 @@ begin
end;
end;
procedure TParserTests.TestSubrange_NamedType_Parses;
begin
{ type TByte = 0..255; must parse (issue #130 bug1). }
ParseSource('program P; type TByte = 0..255; var b: TByte; ' +
'begin b := 5 end.').Free();
end;
procedure TParserTests.TestSubrange_NegativeBounds_Parses;
begin
ParseSource('program P; type TIdx = -10..10; var i: TIdx; ' +
'begin i := -3 end.').Free();
end;
procedure TParserTests.TestSubrange_Descending_Error;
begin
try
ParseSource('program P; type TBad = 10..0; begin end.').Free();
Fail('Expected EParseError for descending subrange');
except
on E: EParseError do ; { expected }
end;
end;
initialization
RegisterTest(TParserTests);

View file

@ -237,9 +237,21 @@ TypeDef
| EnumDef
| SetType
| ProceduralType
| IntSubrange (* named integer subrange, e.g. TByte = 0..255 *)
| TypeName (* covers ArrayType, pointer alias ^T, simple alias, metaclass *)
;
IntSubrange
= [ MINUS ] INT_LIT DOTDOT [ MINUS ] INT_LIT
;
(* A named integer subrange is an alias to the narrowest STANDARD integer type
* that holds both bounds (0..255 -> Byte, -10..10 -> ShortInt, etc.). Blaise
* does not range-check, so the subrange carries no run-time bound enforcement;
* the base-type choice exists only to keep record/array element layout correct.
* Only integer-literal bounds are accepted as a named type; identifier- or
* enum-bounded subranges (TLow..THigh, red..blue) are not a named-type form. *)
RecordDef
= [ PACKED ] RECORD { FieldDecl | MethodDecl } END
;