feat(sets): support integer-subrange set base (set of 0..255) and Boolean operands

Accept an anonymous integer subrange as a set base type, e.g.
'set of 0..255' or 'set of 1..10', in both the inline (var/param/field)
and 'type T = set of L..H' declaration positions. The subrange lowers to
the same bitmap machinery as 'set of Byte': the bitmap is sized to H+1
bits and member ordinals are the integer values themselves. Bounds may be
integer literals, named constants, or constant expressions; the lower
bound must be >= 0, the upper <= 255, and the range ascending.

Also fix 'set of Boolean' so a Boolean operand is accepted directly on
either side of 'in' and as the second argument of Include/Exclude, without
an intervening Ord().

Parser handles both 'set of' paths (ParseTypeName and ParseSetDef);
semantic resolution adds ResolveSubrangeSetType for on-demand and
type-decl paths. Adds dual-backend e2e tests and semantic unit tests.
Updates grammar.ebnf and language-rationale.adoc per the language-decision
rule, and removes the now-implemented item from future-improvements.adoc.
This commit is contained in:
Graeme Geldenhuys 2026-06-18 13:25:22 +01:00
parent f107b0dabc
commit fa106ceeee
7 changed files with 361 additions and 85 deletions

View file

@ -347,16 +347,36 @@ begin
the set from this string on demand — no parser-side symbol table is
needed, and the encoding is self-contained. }
Exit('set of ' + Self.ParseAnonEnumName())
else
else if Check(tkIdent) and (PeekKind() <> tkDotDot) then
begin
if not Check(tkIdent) then
raise EParseError.Create(Format(
'Expected enum type or ''('' after ''set of'' at line %d col %d in %s',
[FCurrent.Line, FCurrent.Col, FLexer.Filename]));
{ Named base type: 'set of Byte', 'set of Boolean', 'set of TEnum'. }
Result := 'set of ' + FCurrent.Value;
Advance();
Exit;
end;
end
else if Check(tkIntLit) or Check(tkIdent) then
begin
{ Integer subrange base type: 'set of 0..255', 'set of 1..10', or a named
constant bound ('set of Lo..Hi'). Each bound is a single integer literal
or identifier; encode as 'set of lo..hi' for the semantic pass to
validate (0..255) and size. (A bracket-context bound reader is wrong
here — the subrange is not delimited by ']'/','.) }
LStr := FCurrent.Value;
Advance();
Expect(tkDotDot);
if not (Check(tkIntLit) or Check(tkIdent)) then
raise EParseError.Create(Format(
'Expected set subrange upper bound at line %d col %d in %s',
[FCurrent.Line, FCurrent.Col, FLexer.Filename]));
HStr := FCurrent.Value;
Advance();
Exit('set of ' + LStr + '..' + HStr);
end
else
raise EParseError.Create(Format(
'Expected enum type, integer subrange, or ''('' after ''set of'' ' +
'at line %d col %d in %s',
[FCurrent.Line, FCurrent.Col, FLexer.Filename]));
end;
if not Check(tkIdent) then
raise EParseError.Create(Format('Expected type name at line %d col %d in %s',
@ -1508,6 +1528,8 @@ begin
end;
function TParser.ParseSetDef: TSetTypeDef;
var
Lo, Hi: string;
begin
Result := TSetTypeDef.Create();
try
@ -1515,12 +1537,32 @@ begin
Result.Col := FCurrent.Col;
Expect(tkSet);
Expect(tkOf);
if not Check(tkIdent) then
if Check(tkIdent) and (PeekKind() <> tkDotDot) then
begin
{ Named base type: set of Byte / Boolean / TEnum. }
Result.BaseTypeName := FCurrent.Value;
Advance();
end
else if Check(tkIntLit) or Check(tkIdent) then
begin
{ Integer subrange base type: set of 0..255 / 1..10 / Lo..Hi. Stored as
'lo..hi' in BaseTypeName; the semantic pass resolves and validates it. }
Lo := FCurrent.Value;
Advance();
Expect(tkDotDot);
if not (Check(tkIntLit) or Check(tkIdent)) then
raise EParseError.Create(Format(
'Expected set subrange upper bound at line %d col %d in %s',
[FCurrent.Line, FCurrent.Col, FLexer.Filename]));
Hi := FCurrent.Value;
Advance();
Result.BaseTypeName := Lo + '..' + Hi;
end
else
raise EParseError.Create(Format(
'Expected base type name after ''set of'' at line %d col %d in %s',
'Expected base type or integer subrange after ''set of'' ' +
'at line %d col %d in %s',
[FCurrent.Line, FCurrent.Col, FLexer.Filename]));
Result.BaseTypeName := FCurrent.Value;
Advance();
except
Result.Free();
raise;

View file

@ -179,6 +179,7 @@ type
function EvalConstFloatExpr(AExpr: TASTExpr; ALine, ACol: Integer): string;
function IsFloatConstExpr(AExpr: TASTExpr): Boolean;
function ResolveArrayBound(const ABoundText: string): Integer;
function ResolveSubrangeSetType(const ASubrange: string): TSetTypeDesc;
function ResolveConstArrayElem(const AElem: string; AElemType: TTypeDesc;
ALine, ACol: Integer): string;
procedure AnalyseTypeDecls(ABlock: TBlock);
@ -2474,6 +2475,16 @@ begin
if (Length(AName) > 7) and (StrHead(AName, 7) = 'set of ') then
begin
BaseName := StrCopyTail(AName, 7);
{ Integer-subrange base type 'set of lo..hi' (e.g. set of 0..255). Like
Delphi/FPC, the element ordinals are the integer values themselves and the
type is capped at 256 elements (ordinals 0..255). Resolve and validate
the bounds, then build an ordinal set sized by the high bound — reusing the
same Byte-set machinery (bits 0..hi). }
if StrPos('..', BaseName) >= 0 then
begin
Result := Self.ResolveSubrangeSetType(BaseName);
Exit;
end;
{ Anonymous enum element 'set of (a,b,c)' — synthesise the enum from the
encoded member list; otherwise resolve a named element type. }
if (BaseName <> '') and (StrAt(BaseName, 0) = Ord('(')) then
@ -3951,6 +3962,40 @@ begin
end;
end;
function TSemanticAnalyser.ResolveSubrangeSetType(const ASubrange: string): TSetTypeDesc;
{ Resolve a 'set of lo..hi' integer-subrange set type, e.g. 'set of 0..255'.
Element ordinals are the integer values themselves; like Delphi/FPC the type is
capped at 256 elements (ordinals 0..255). The bitmap is sized by the high
bound (bits 0..hi), reusing the Byte-backed ordinal-set machinery — a low bound
> 0 simply leaves the lower bits unused, matching FPC. }
var
DDotPos, Lo, Hi: Integer;
LStr, HStr, CanonName: string;
begin
DDotPos := StrPos('..', ASubrange);
LStr := StrCopyFrom(ASubrange, 0, DDotPos);
HStr := StrCopyTail(ASubrange, DDotPos + 2);
Lo := ResolveArrayBound(LStr);
Hi := ResolveArrayBound(HStr);
if Lo < 0 then
SemanticError(Format(
'Set subrange ''%s'': lower bound must be >= 0', [ASubrange]), 0, 0);
if Hi > 255 then
SemanticError(Format(
'Set subrange ''%s'': upper bound must be <= 255 (a set has at most 256 ' +
'elements)', [ASubrange]), 0, 0);
if Hi < Lo then
SemanticError(Format(
'Set subrange ''%s'' is descending', [ASubrange]), 0, 0);
CanonName := Format('set of %d..%d', [Lo, Hi]);
Result := TSetTypeDesc(FTable.FindType(CanonName));
if Result = nil then
begin
Result := FTable.NewOrdinalSetType(CanonName, FTable.TypeByte, Hi + 1);
FTable.DefineGlobal(TSymbol.Create(CanonName, skType, Result));
end;
end;
function TSemanticAnalyser.ResolveConstArrayElem(const AElem: string;
AElemType: TTypeDesc; ALine, ACol: Integer): string;
{ Resolve one array-const element to the numeric string codegen needs. The
@ -4460,6 +4505,7 @@ var
EnumDesc: TEnumTypeDesc;
EnumDef: TEnumTypeDef;
SetDesc: TSetTypeDesc;
SetSubDesc: TSetTypeDesc;
SetDef: TSetTypeDef;
AttrIdx: Integer;
RawAttr: string;
@ -4549,6 +4595,23 @@ begin
else if TD.Def is TSetTypeDef then
begin
SetDef := TSetTypeDef(TD.Def);
{ Integer-subrange base type ('lo..hi', e.g. set of 0..255): build an
ordinal set sized by the high bound (validated 0..255), bound to this
type's name. }
if StrPos('..', SetDef.BaseTypeName) >= 0 then
begin
SetSubDesc := Self.ResolveSubrangeSetType(SetDef.BaseTypeName);
SetDesc := FTable.NewOrdinalSetType(TD.Name, SetSubDesc.BaseType,
SetSubDesc.BitCount);
Sym := TSymbol.Create(TD.Name, skType, SetDesc);
if not FTable.Define(Sym) then
begin
Sym.Free();
SemanticError(Format('Duplicate type name ''%s''', [TD.Name]),
TD.Line, TD.Col);
end;
Continue;
end;
BaseSym := FTable.Lookup(SetDef.BaseTypeName);
if (BaseSym = nil) or (BaseSym.Kind <> skType) then
SemanticError(
@ -8507,9 +8570,13 @@ begin
if TSetTypeDesc(TASTExpr(ACall.Args.Items[0]).ResolvedType).BaseType.Kind
in [tyByte, tyBoolean] then
begin
if not ArgType.IsNumeric() then
{ Ordinal-base set: accept a numeric ordinal, and a Boolean operand
for a Boolean-base set (Include(s, True)). }
if not (ArgType.IsNumeric() or
((TSetTypeDesc(TASTExpr(ACall.Args.Items[0]).ResolvedType).BaseType.Kind = tyBoolean) and
(ArgType.Kind = tyBoolean))) then
SemanticError(
Format('Second argument of ''%s'' must be numeric for ''set of %s'', got ''%s''',
Format('Second argument of ''%s'' must be ordinal for ''set of %s'', got ''%s''',
[ACall.Name,
TSetTypeDesc(TASTExpr(ACall.Args.Items[0]).ResolvedType).BaseType.Name,
ArgType.Name]),
@ -10725,10 +10792,16 @@ begin
ABin.Line, ABin.Col);
if TSetTypeDesc(RType).BaseType.Kind in [tyByte, tyBoolean] then
begin
if not LType.IsNumeric() then
{ Ordinal-base set (Byte/Boolean or an integer subrange): the left operand
is the element ordinal. Accept a numeric operand, and — for a Boolean
base — a Boolean operand (True/False), since `True in s` is the natural
form. Both lower to the operand's ordinal. }
if not (LType.IsNumeric() or
((TSetTypeDesc(RType).BaseType.Kind = tyBoolean) and
(LType.Kind = tyBoolean))) then
SemanticError(
Format('Left operand of ''in'' must be numeric for ''set of %s'', got ''%s''',
[TSetTypeDesc(RType).BaseType.Name, LType.Name]),
Format('Left operand of ''in'' must be %s for ''set of %s'', got ''%s''',
['ordinal', TSetTypeDesc(RType).BaseType.Name, LType.Name]),
ABin.Line, ABin.Col);
end
else if LType <> TSetTypeDesc(RType).BaseType then

View file

@ -60,6 +60,13 @@ type
procedure TestRun_ConstSet_MixedRangeAndLiteral;
procedure TestRun_ConstSet_JumboWithRange;
procedure TestRun_ConstSet_EnumStillWorks;
{ Integer-subrange base type: set of 0..255 / set of 1..10 (issue from
future-improvements) + set of Boolean membership }
procedure TestRun_SetOfSubrange_0to255;
procedure TestRun_SetOfSubrange_TypeDecl;
procedure TestRun_SetOfSubrange_NamedConstBounds;
procedure TestRun_SetOfBoolean_Membership;
end;
implementation
@ -564,6 +571,79 @@ begin
AssertRunsOnAll(Src, 'y' + LE + 'n' + LE, 0);
end;
procedure TE2ESetOpsTests.TestRun_SetOfSubrange_0to255;
{ Integer-subrange base type 'set of 0..255' — equivalent to set of Byte. }
const Src = '''
program P;
var s: set of 0..255;
begin
s := [10, 20, 200];
if 20 in s then WriteLn('y') else WriteLn('n');
if 50 in s then WriteLn('y') else WriteLn('n');
Include(s, 50);
if 50 in s then WriteLn('y') else WriteLn('n')
end.
''';
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
AssertRunsOnAll(Src, 'y' + LE + 'n' + LE + 'y' + LE, 0);
end;
procedure TE2ESetOpsTests.TestRun_SetOfSubrange_TypeDecl;
{ 'type T = set of 0..63' (small, <=64-bit path) + range literal. }
const Src = '''
program P;
type TS = set of 0..63;
var s: TS;
begin
s := [3..7];
if 5 in s then WriteLn('y') else WriteLn('n');
if 63 in s then WriteLn('y') else WriteLn('n');
Include(s, 63);
if 63 in s then WriteLn('y') else WriteLn('n')
end.
''';
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
AssertRunsOnAll(Src, 'y' + LE + 'n' + LE + 'y' + LE, 0);
end;
procedure TE2ESetOpsTests.TestRun_SetOfSubrange_NamedConstBounds;
{ Subrange bounds may be named constants: set of Lo..Hi. }
const Src = '''
program P;
const Lo = 0; Hi = 100;
var s: set of Lo..Hi;
begin
s := [50];
if 50 in s then WriteLn('y') else WriteLn('n')
end.
''';
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
AssertRunsOnAll(Src, 'y' + LE, 0);
end;
procedure TE2ESetOpsTests.TestRun_SetOfBoolean_Membership;
{ set of Boolean: a Boolean operand on the left of 'in' (True in s) and as the
Include/Exclude element. }
const Src = '''
program P;
var s: set of Boolean; b: Boolean;
begin
s := [True];
if True in s then WriteLn('y') else WriteLn('n');
if False in s then WriteLn('y') else WriteLn('n');
Include(s, False);
b := False;
if b in s then WriteLn('y') else WriteLn('n')
end.
''';
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
AssertRunsOnAll(Src, 'y' + LE + 'n' + LE + 'y' + LE, 0);
end;
initialization
RegisterTest(TE2ESetOpsTests);

View file

@ -130,6 +130,14 @@ type
procedure TestCodegen_SetOfByte_Union;
procedure TestCodegen_SetOfByte_IsJumbo;
{ ------------------------------------------------------------------ }
{ Integer-subrange base type: set of 0..255 / set of lo..hi }
{ ------------------------------------------------------------------ }
procedure TestSemantic_SetOfSubrange_VarDecl_OK;
procedure TestSemantic_SetOfSubrange_TypeDecl_OK;
procedure TestSemantic_SetOfSubrange_OverBound_Fails;
procedure TestSemantic_SetOfSubrange_Descending_Fails;
{ ------------------------------------------------------------------ }
{ 64-bit sets (>32 members) }
{ ------------------------------------------------------------------ }
@ -1380,6 +1388,28 @@ const
end.
''';
SrcSetOfSubrangeVar =
'''
program P;
var s: set of 0..255;
begin
s := [10, 200]
end.
''';
SrcSetOfSubrangeType =
'''
program P;
type TS = set of 0..63;
var s: TS;
begin
s := [3, 63]
end.
''';
SrcSetOfSubrangeOverBound =
'program P; var s: set of 0..1000; begin s := [1] end.';
SrcSetOfSubrangeDescending =
'program P; var s: set of 5..3; begin s := [] end.';
SrcSetOfByteRange =
'''
program P;
@ -1577,6 +1607,30 @@ begin
end;
end;
procedure TSetTests.TestSemantic_SetOfSubrange_VarDecl_OK;
begin
{ 'set of 0..255' (integer-subrange base) is accepted. }
SemanticOK(SrcSetOfSubrangeVar);
end;
procedure TSetTests.TestSemantic_SetOfSubrange_TypeDecl_OK;
begin
{ 'type TS = set of 0..63' is accepted. }
SemanticOK(SrcSetOfSubrangeType);
end;
procedure TSetTests.TestSemantic_SetOfSubrange_OverBound_Fails;
begin
{ A set has at most 256 elements (ordinals 0..255); 0..1000 is rejected. }
SemanticFail(SrcSetOfSubrangeOverBound);
end;
procedure TSetTests.TestSemantic_SetOfSubrange_Descending_Fails;
begin
{ A descending subrange (5..3) is a mistake, not a silent empty set. }
SemanticFail(SrcSetOfSubrangeDescending);
end;
initialization
RegisterTest(TSetTests);

View file

@ -34,30 +34,51 @@ prioritise them.
- reference to function/procedure (anonymous methods)
- cdecl/stdcall calling-convention markers on procedural types
=== Set types over an integer subrange (`set of 0..255`)
`set of byte` and `set of Boolean` are *implemented* (issue #105): the named
ordinal base types lower to a bit set — `set of byte` uses the 256-bit jumbo
path, membership/Include/Exclude and `[lo..hi]` range literals all work on both
backends. See `docs/language-rationale.adoc` and the `set of Byte` tests in
`cp.test.sets.pas` / `cp.test.e2e.sets.pas`.
What remains is the *anonymous integer-subrange* base form,
`set of 0..255` / `set of 1..10`, which is still rejected at parse time
("Expected enum type or '(' after 'set of'"). Standard Pascal accepts a
subrange there; the bit count is the range width and element ordinals are the
integer values themselves — the same machinery `set of byte` already uses, so
this is mostly parser + base-type-resolution work, not new codegen.
Minor follow-on: for `set of Boolean`, a Boolean operand on the left of `in`
(e.g. `True in s`) is rejected by the `in` type-check ("operand must be
numeric"); `Ord(b) in s` works. The check should accept a Boolean (or any
ordinal) operand against a matching ordinal-base set.
=== Method level attributes
Blaise only has class-level attribute RTTI today — there's no per-method attribute reflection, and adding it is a layout-changing compiler change (new method-table slot, stage-2 rebuild).
=== If Conditional Operator (Ternary Conditional Operator)
More details here: https://en.wikipedia.org/wiki/Ternary_conditional_operator
Many languages use the ?: syntax as follows:
`result = condition ? value_if_true : value_if_false`
But that's not very pascal-like. Oxygene language has implemented this years
ago with te following syntax:
For instance
[source]
----
var s := if (a<>0) then Sin(1/a) else 1;
----
being equivalent to
[source,pascal]
----
var s : Float;
if (a<>0) then
s := Sin(1/a)
else s := 1;
----
So for Blaise we could do similar:
X := if Left < 100 then 22 else 45;
// the operator can be used as part of any expression, as follows:
ShowMessage(if Left < 100 then 'Small' else 'Big');
NOTE: If the two types do not match and are incompatible, the compiler should issue an error message
NOTE: It is important to note that the if-then-else expression differs from the if-then-else statement, the expression has a single type. When the type of the then and else expressions differ, the expression's type is the common type of the two expressions' types; also known as the Least Upper Bound (LUB) type.
See also Delphi's documentation: https://docwiki.embarcadero.com/RADStudio/Florence/en/Conditional_Operators_(Delphi)
== Language — Features Required to Complete the punit Port
The following features were identified as missing from Blaise while porting
@ -142,7 +163,7 @@ record's own memory. Used before `FreeMem` when the record was heap-allocated.
allocated records that contain strings leak the string's reference count.
*Current workaround in punit.* The `Finalize` calls were dropped. In Blaise,
ARC on assignment should automatically manage string fields inside records, so
ARC on assfignment should automatically manage string fields inside records, so
the string is released when the record's string field is overwritten or when the
last reference to the containing heap block is released. The correctness of
this assumption depends on the ARC implementation for record fields — if the
@ -157,46 +178,6 @@ local record variables at scope exit.
*Effort.* Small to moderate — shares infrastructure with ARC scope-exit emit.
=== If Conditional Operator (Ternary Conditional Operator)
More details here: https://en.wikipedia.org/wiki/Ternary_conditional_operator
Many languages use the ?: syntax as follows:
`result = condition ? value_if_true : value_if_false`
But that's not very pascal-like. Oxygene language has implemented this years
ago with te following syntax:
For instance
[source]
----
var s := if (a<>0) then Sin(1/a) else 1;
----
being equivalent to
[source,pascal]
----
var s : Float;
if (a<>0) then
s := Sin(1/a)
else s := 1;
----
So for Blaise we could do similar:
X := if Left < 100 then 22 else 45;
// the operator can be used as part of any expression, as follows:
ShowMessage(if Left < 100 then 'Small' else 'Big');
NOTE: If the two types do not match and are incompatible, the compiler should issue an error message
NOTE: It is important to note that the if-then-else expression differs from the if-then-else statement, the expression has a single type. When the type of the then and else expressions differ, the expression's type is the common type of the two expressions' types; also known as the Least Upper Bound (LUB) type.
See also Delphi's documentation: https://docwiki.embarcadero.com/RADStudio/Florence/en/Conditional_Operators_(Delphi)
== StdLib Packages — Numeric Types
=== `Currency` and `BigDecimal` as StdLib units

View file

@ -673,7 +673,11 @@ EnumMember
* The anonymous enum form synthesises an enum type whose members become
* ordinary enum constants in scope. *)
SetType
= SET OF ( IDENT | EnumDef )
= SET OF ( IDENT | EnumDef | Subrange )
;
Subrange
= ConstExpr DOTDOT ConstExpr
;
(* The base type must have at most 256 ordinal values. Storage scales with
@ -685,9 +689,14 @@ SetType
* aggregate (record/static array)
* Value ordinal N maps to bit (N mod 8) of byte (N div 8) in all forms.
* Types with more than 256 possible values (e.g. Integer, Word) are rejected
* by the semantic analyser — only Byte (256 values), Boolean (2 values), and
* enumerations with at most 256 members are accepted. Set literals with
* ordinal base types accept integer literals and ranges: [1, 2, 10..20]. *)
* by the semantic analyser — only Byte (256 values), Boolean (2 values),
* enumerations with at most 256 members, and integer subranges within
* 0..255 are accepted. The subrange form `set of L..H` (e.g. `set of 0..255`,
* `set of 1..10`) sizes the bitmap to H+1 bits; both bounds may be integer
* literals, named constants, or constant expressions. L must be >= 0, H must
* be <= 255, and the range must be ascending (H >= L) — a descending or
* out-of-range subrange is a semantic error. Set literals with ordinal base
* types accept integer literals and ranges: [1, 2, 10..20]. *)
(* Procedural type — bare or method pointer.
*

View file

@ -1293,15 +1293,16 @@ code.
_Deferred to Phase 6 (LLVM backend + Windows target). No implementation
required for the current Linux x86_64 target._
=== Set Types (`set of TEnum` / `set of Byte`)
=== Set Types (`set of TEnum` / `set of Byte` / `set of 0..255`)
==== Decision
Blaise supports Pascal bit-set types declared as `set of BaseType`, where
`BaseType` is either a previously-defined enumeration or one of the small
ordinal types `Byte` (256 values, 0..255) or `Boolean` (2 values). A set
variable holds a bitmask in which bit N is set when the member with ordinal N
is included.
`BaseType` is a previously-defined enumeration, one of the small ordinal
types `Byte` (256 values, 0..255) or `Boolean` (2 values), or an integer
subrange within 0..255 (e.g. `0..9`, `1..10`, `0..255`). A set variable
holds a bitmask in which bit N is set when the member with ordinal N is
included.
==== Ordinal-based sets
@ -1328,9 +1329,45 @@ Integer range expressions `[lo..hi]` are expanded to individual members at
compile time using `EvalConstIntExpr`. Bounds are validated against the
set's bit count (0..255 for Byte, 0..1 for Boolean).
A Boolean operand is accepted on either side of `in` and as the second
argument of `Include`/`Exclude` for a `set of Boolean`; the operand only
needs to be ordinal-compatible with the set's base type, not strictly
numeric. `True in s` and `Include(s, False)` therefore work directly,
without an intervening `Ord()`.
Larger ordinal types (`Integer`, `Word`, `Int64`) are rejected because they
would require impractically large bitmaps.
==== Integer-subrange base (`set of 0..255`)
The set base may also be an anonymous integer subrange, `set of L..H`, as
in standard Pascal, Delphi, and FPC:
[source,pascal]
----
type TDigits = set of 0..9; (* type-declaration form *)
var Small: set of 1..10; (* inline form in a var declaration *)
begin
Small := [1, 5, 8];
if 5 in Small then WriteLn('yes');
end.
----
The subrange lowers to the same bitmap machinery as `set of Byte`: the
bitmap is sized to `H + 1` bits (member ordinals are the integer values
themselves), so `set of 0..63` is a 64-bit small set and `set of 0..255` is
a 256-bit jumbo set. Both bounds may be integer literals, named constants,
or constant expressions, resolved through the same `ResolveArrayBound` path
used for static-array bounds.
The lower bound must be `>= 0`, the upper bound `<= 255`, and the range must
be ascending (`H >= L`). A descending subrange (`set of 5..3`) is a
semantic error rather than a silently-empty set, and an upper bound above
255 is rejected for the same 256-element ceiling that applies to every set.
The bitmap always starts at ordinal 0 — `set of 1..10` still reserves bits
for 0 — which keeps the representation identical to `set of Byte` and avoids
a base-offset in the `_Set*` RTL helpers.
==== Storage model
Storage scales with the base type's value count, so the common small-set