26 min read

> "The purpose of a type system is not to make programming harder, but to make programs easier to understand."

Chapter 12: Sets and Enumerations: Pascal's Unique Type System

"The purpose of a type system is not to make programming harder, but to make programs easier to understand." — Niklaus Wirth

Every programming language gives you integers, strings, and booleans. But Pascal goes further. It gives you the power to define exactly the values a variable can hold — not just "some integer" but "a day of the week," not just "some collection" but "a set of permissions." In this chapter, we explore three of Pascal's most distinctive features: enumerated types, subrange types, and sets. These features existed in Pascal decades before other languages adopted similar ideas, and they remain some of the most elegant tools in any programmer's toolkit.

If you have used Python's enum module or C#'s [Flags] attribute, you have been using ideas that Pascal pioneered in the 1970s. But Pascal's versions are simpler, more integrated into the language, and — we would argue — more elegant. By the end of this chapter, you will write code that is not only more readable but more correct, because the compiler itself will catch mistakes that would otherwise become runtime bugs.

This chapter brings together ideas from several earlier chapters. In Chapter 3, we learned about constants and how naming values improves code clarity. In Chapter 5, we saw how case statements provide structured branching. And in Chapter 11, we began working with arrays as our first structured data type. Enumerations, subranges, and sets build on all three ideas: enumerations are named constants taken to their logical extreme, they pair naturally with case statements, and they serve as powerful array indices. Together, these three features form a cohesive system that makes Pascal programs remarkably expressive for their size.


12.1 Enumerated Types: Naming Your Values

The Problem with Magic Numbers

Consider the following code that tracks the days of the week:

var
  Day: Integer;
begin
  Day := 3;  { What day is this? Wednesday? Thursday? }
  if Day = 7 then
    WriteLn('Weekend!');
end.

What does 3 mean? Is Monday 0 or 1? Is Sunday 7 or 0? This kind of code — where bare numbers carry hidden meaning — is a perennial source of bugs. In Chapter 3, we learned about constants as one remedy. But Pascal offers something far more powerful: enumerated types.

Defining Enumerated Types

An enumerated type declares a brand-new type by listing every possible value by name:

type
  TDay = (Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday);

That single line creates seven new identifiers — Monday through Sunday — and a type TDay that can hold exactly one of them. Nothing else. Not an integer, not a string — only a day of the week.

Now our code becomes self-documenting:

var
  Today: TDay;
begin
  Today := Wednesday;
  if Today = Sunday then
    WriteLn('Weekend!')
  else
    WriteLn('Keep working!');
end.

No ambiguity. No magic numbers. The code reads like English.

The Naming Convention

Throughout this textbook, we prefix type names with T (for "Type") — TDay, TColor, TSeason. This is a widespread Pascal convention, especially in the Delphi and Free Pascal communities. The enumeration values themselves are not prefixed unless there is a risk of name collision, which we will discuss shortly.

Ordinal Values: Enumerations Are Ordered

Every enumerated value has an associated ordinal number, starting from zero. The built-in Ord function reveals it:

WriteLn(Ord(Monday));     { 0 }
WriteLn(Ord(Tuesday));    { 1 }
WriteLn(Ord(Wednesday));  { 2 }
WriteLn(Ord(Sunday));     { 6 }

This ordering means enumerated types support comparison operators:

if Today < Saturday then
  WriteLn('Weekday')
else
  WriteLn('Weekend');

Since Saturday has ordinal 5 and Sunday has ordinal 6, any day with ordinal less than 5 is a weekday. The comparison works because enumerations are ordinal types in Pascal — types with a defined sequence.

Pascal provides built-in functions for working with ordinal types:

Function Description Example
Succ(x) Next value (successor) Succ(Monday) = Tuesday
Pred(x) Previous value (predecessor) Pred(Wednesday) = Tuesday
Ord(x) Ordinal position (0-based) Ord(Friday) = 4
Low(T) First value of type Low(TDay) = Monday
High(T) Last value of type High(TDay) = Sunday

Warning

Calling Succ on the last value or Pred on the first value causes a runtime error if range checking is enabled. Always guard these calls.

Iterating Over Enumerations

Because enumerations are ordinal, you can use them directly in for loops:

var
  Day: TDay;
begin
  for Day := Monday to Sunday do
    WriteLn('Day #', Ord(Day), ': ', Day);
end.

Wait — does WriteLn know how to print an enumeration? In standard Pascal, no. But Free Pascal (with {$mode objfpc}` or `{$mode delphi}) can write enumeration names if you use the TypInfo unit. Alternatively, you can write a helper function:

function DayToStr(D: TDay): string;
begin
  case D of
    Monday:    Result := 'Monday';
    Tuesday:   Result := 'Tuesday';
    Wednesday: Result := 'Wednesday';
    Thursday:  Result := 'Thursday';
    Friday:    Result := 'Friday';
    Saturday:  Result := 'Saturday';
    Sunday:    Result := 'Sunday';
  end;
end;

This is a common pattern, and we will use it throughout the chapter. Every enumerated type you create will typically have a corresponding XxxToStr function. Some programmers put these in a utility unit so they can be reused across an entire project.

Why Enumerations Matter: The Real-World Case

To see why enumerations matter, consider a student grading system. Without enumerations, you might write:

var
  Status: Integer;  { 0=enrolled, 1=withdrawn, 2=completed, 3=incomplete }
begin
  Status := 2;
  if Status = 3 then  { Wait, is 3 incomplete or completed? }
    WriteLn('Incomplete');
end.

Six months later, you (or worse, a colleague) look at this code and have no idea what 2 means without hunting down the comment. With enumerations:

type
  TStudentStatus = (ssEnrolled, ssWithdrawn, ssCompleted, ssIncomplete);
var
  Status: TStudentStatus;
begin
  Status := ssCompleted;
  if Status = ssIncomplete then
    WriteLn('Incomplete');
end.

The intent is obvious. The code is self-documenting. And if you accidentally type ssCompeted instead of ssCompleted, the compiler catches the typo immediately — something that would never happen with a mistyped integer constant.

Specifying Ordinal Values

Free Pascal allows you to assign specific ordinal values to enumeration members:

type
  THttpStatus = (
    hsOK         = 200,
    hsNotFound   = 404,
    hsServerError = 500
  );

This is useful for interoperability with external systems, but use it sparingly — it breaks the guarantee that ordinal values are contiguous, which means Succ and Pred may not behave as expected, and for loops over such types can be problematic.


12.2 Using Enumerations Effectively

CASE Statements and Enumerations: A Natural Pair

Enumerations and case statements are made for each other. In Chapter 5, we learned about case as a structured alternative to nested if chains. With enumerations, case becomes even more powerful because the compiler can verify that you have handled every possible value:

type
  TSeason = (Spring, Summer, Autumn, Winter);

function SeasonGreeting(S: TSeason): string;
begin
  case S of
    Spring: Result := 'Flowers are blooming!';
    Summer: Result := 'Time for the beach!';
    Autumn: Result := 'Leaves are falling!';
    Winter: Result := 'Bundle up!';
  end;
end;

If you later add a fifth season (say, Monsoon for a tropical variant), the compiler will warn you that the case statement does not cover all values. This is exhaustiveness checking, and it is one of the most valuable features of combining enumerations with case.

Tip

Enable the compiler warning -Wcase (or use {$WARN 5037 on} in Free Pascal) to get warnings about incomplete case statements. This turns the compiler into a proofreader that catches missing cases at compile time rather than runtime.

Type Safety: The Compiler as Guardian

Enumerations are distinct types. You cannot accidentally mix them:

type
  TColor  = (Red, Green, Blue);
  TSeason = (Spring, Summer, Autumn, Winter);
var
  C: TColor;
  S: TSeason;
begin
  C := Red;       { Fine }
  S := Spring;    { Fine }
  C := Spring;    { COMPILE ERROR: incompatible types }
  S := Red;       { COMPILE ERROR: incompatible types }
end.

This is exactly the kind of bug that magic numbers allow and enumerations prevent. With integers, Day := Color would compile without complaint and cause silent, baffling bugs.

The Name Collision Problem

Enumeration identifiers live in the global namespace (in standard Pascal mode). This can cause problems:

type
  TTrafficLight = (Red, Yellow, Green);
  TColor = (Red, Orange, Yellow, Green, Blue, Indigo, Violet);
  { COMPILE ERROR: Red, Yellow, Green already declared }

The standard solution is prefixing:

type
  TTrafficLight = (tlRed, tlYellow, tlGreen);
  TColor = (clRed, clOrange, clYellow, clGreen, clBlue, clIndigo, clViolet);

In Free Pascal's {$mode delphi}` or with `{$scopedenums on}, you can use scoped enumerations where values are accessed through the type name:

{$scopedenums on}
type
  TTrafficLight = (Red, Yellow, Green);
  TColor = (Red, Orange, Yellow, Green, Blue, Indigo, Violet);
var
  Light: TTrafficLight;
  Paint: TColor;
begin
  Light := TTrafficLight.Red;
  Paint := TColor.Red;
end.

Scoped enumerations are the modern approach and avoid name collisions entirely. However, most existing Pascal code uses the prefix convention, so you should be comfortable with both styles.

Enumerations as Array Indices

One of Pascal's most elegant features is using enumerations as array indices:

type
  TDay = (Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday);
var
  Hours: array[TDay] of Integer;
  Day: TDay;
begin
  Hours[Monday] := 8;
  Hours[Tuesday] := 7;
  { ... }
  for Day := Monday to Sunday do
    WriteLn(DayToStr(Day), ': ', Hours[Day], ' hours');
end.

The array is indexed by days, not by arbitrary integers. You literally cannot write Hours[42] — the compiler would reject it. This is another example of Pascal's philosophy: let the type system prevent errors rather than relying on the programmer's discipline.

You can even use constant arrays indexed by enumerations to create lookup tables:

type
  TDay = (Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday);
const
  DayNames: array[TDay] of string = (
    'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'
  );
  IsWeekend: array[TDay] of Boolean = (
    False, False, False, False, False, True, True
  );

With these constant arrays, converting an enumeration to a string is simply DayNames[Today], and checking for the weekend is IsWeekend[Today]. No function call, no case statement — just a direct array lookup. This pattern is especially useful when you have many enumerations and want to avoid writing a case-based conversion function for each one.

Enumerations as Function Parameters and Return Values

Enumerations shine as function parameters because they make calling code self-documenting:

type
  TAlignment = (alLeft, alCenter, alRight);
  TFontStyle = (fsNormal, fsBold, fsItalic, fsBoldItalic);

procedure FormatText(const Text: string; Align: TAlignment; Style: TFontStyle);
begin
  { Implementation here }
end;

begin
  FormatText('Hello', alCenter, fsBold);  { Crystal clear }
  { Compare to: FormatText('Hello', 1, 2);  — what do 1 and 2 mean? }
end.

When you read FormatText('Hello', alCenter, fsBold), you know exactly what each argument does without checking the function signature. This self-documenting quality is why experienced Pascal programmers define enumerations even when the number of options is small — even a boolean parameter becomes clearer as an enumeration when the meaning is not obvious from context.


12.3 Subrange Types: Restricting Valid Values

Defining Subranges

A subrange type restricts a variable to a contiguous portion of an existing ordinal type:

type
  TMonth     = 1..12;
  TPercent   = 0..100;
  TUpperCase = 'A'..'Z';
  TWeekday   = Monday..Friday;   { Subrange of TDay }

A variable of type TMonth can hold only the integers 1 through 12. Attempting to assign 13 to it is an error — if range checking is enabled.

Range Checking: The {$R+} Directive

By default, Free Pascal does not check range violations at runtime (for performance reasons). You enable range checking with the {$R+} compiler directive:

{$R+}
var
  Month: TMonth;
begin
  Month := 6;    { Fine }
  Month := 13;   { Runtime error 201: Range check error }
end.

Best Practice: Always compile with {$R+}` during development. Only disable it (`{$R-}) in performance-critical production code, and only after thorough testing. The small performance cost is almost always worth the safety.

You can also enable range checking globally with the -Cr compiler flag:

fpc -Cr myprogram.pas

Subranges of Enumerations

Subranges work with any ordinal type, including enumerations:

type
  TDay     = (Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday);
  TWeekday = Monday..Friday;
  TWeekend = Saturday..Sunday;

procedure PrintWorkSchedule(Day: TWeekday);
begin
  WriteLn('Working on ', DayToStr(Day));
end;

Now PrintWorkSchedule(Saturday) would cause a range check error — the function literally cannot accept weekend days. The type system encodes the business rule that work schedules only apply to weekdays.

Subranges and Arrays

Subranges make excellent array indices:

type
  TGrade = 'A'..'F';
var
  GradeCount: array[TGrade] of Integer;
  Ch: Char;
begin
  { Initialize }
  for Ch := 'A' to 'F' do
    GradeCount[Ch] := 0;

  { Count grades from input }
  GradeCount['A'] := 15;
  GradeCount['B'] := 22;
  { ... }
end.

The array has exactly six elements — one for each valid grade. No wasted memory, no possibility of accessing an invalid index.

The Cost of Not Using Subranges

To understand why subranges matter, consider a common bug pattern. A function accepts an integer month parameter:

function MonthName(Month: Integer): string;
begin
  case Month of
    1: Result := 'January';
    { ... }
    12: Result := 'December';
  else
    Result := '???';  { What should we do here? }
  end;
end;

The else clause is a red flag. It exists only because the function can receive invalid input (13, -1, 0, 99). With a subrange, the invalid input is impossible:

function MonthName(Month: TMonth): string;  { TMonth = 1..12 }

No else clause needed. No error handling for impossible values. The type system has eliminated an entire category of bugs. This is what we mean when we say Pascal "teaches right" (Theme 1): the language encourages you to express constraints in the type system rather than patching them with defensive code.

Subranges and Assignment Compatibility

An important detail: a subrange type is assignment compatible with its base type. You can assign a TMonth to an Integer, or assign an Integer to a TMonth (subject to range checking):

var
  M: TMonth;
  I: Integer;
begin
  M := 6;
  I := M;      { Fine: I = 6 }
  I := 15;
  M := I;      { Runtime error with {$R+}: 15 is out of range 1..12 }
end.

This means you can use subrange variables anywhere their base type is expected, making them easy to adopt incrementally in existing code.

Combining Subranges and Enumerations

Here is a practical example combining several features:

type
  TMonth = (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec);
  TQuarter = 1..4;

function MonthToQuarter(M: TMonth): TQuarter;
begin
  case M of
    Jan, Feb, Mar: Result := 1;
    Apr, May, Jun: Result := 2;
    Jul, Aug, Sep: Result := 3;
    Oct, Nov, Dec: Result := 4;
  end;
end;

Every month maps to exactly one quarter. The return type guarantees the result is 1 through 4. The case statement covers all twelve months. Three separate type-system features work together to make the function bulletproof.


12.4 Sets: Pascal's Secret Weapon

What Is a Set?

In mathematics, a set is an unordered collection of distinct elements. Pascal brings this concept directly into the language with the set of type. If you have ever used a Python set or checked flags in C, you have used a weaker version of what Pascal provides natively.

A Pascal set can contain any combination of values from a base ordinal type:

type
  TDay = (Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday);
  TDays = set of TDay;
var
  Weekend, Weekdays, MyDaysOff: TDays;
begin
  Weekend  := [Saturday, Sunday];
  Weekdays := [Monday..Friday];
  MyDaysOff := [Wednesday, Saturday, Sunday];
end.

The syntax is clean and intuitive. Square brackets denote set literals. Ranges (Monday..Friday) work inside set constructors.

The IN Operator: Membership Testing

The most common set operation is testing whether a value belongs to a set:

var
  Today: TDay;
  Weekend: TDays;
begin
  Today := Wednesday;
  Weekend := [Saturday, Sunday];

  if Today in Weekend then
    WriteLn('Relax!')
  else
    WriteLn('Get to work!');
end.

Compare this to the alternative without sets:

if (Today = Saturday) or (Today = Sunday) then
  WriteLn('Relax!');

With two values, the difference is small. But what about checking whether a character is a vowel?

{ Without sets: }
if (Ch = 'A') or (Ch = 'E') or (Ch = 'I') or (Ch = 'O') or (Ch = 'U') or
   (Ch = 'a') or (Ch = 'e') or (Ch = 'i') or (Ch = 'o') or (Ch = 'u') then
  WriteLn('Vowel');

{ With sets: }
if Ch in ['A','E','I','O','U','a','e','i','o','u'] then
  WriteLn('Vowel');

The set version is shorter, clearer, and — as we will see in Section 12.6 — faster.

Set Operators: Union, Intersection, Difference

Pascal provides three arithmetic-like operators for combining sets:

Operator Meaning Mathematical Symbol Example
+ Union A [1,2,3] + [3,4,5] = [1,2,3,4,5]
* Intersection A [1,2,3] * [3,4,5] = [3]
- Difference A \ B [1,2,3] - [3,4,5] = [1,2]

These operators work exactly like their mathematical counterparts:

var
  A, B, C: set of 1..10;
begin
  A := [1, 3, 5, 7, 9];   { Odd numbers }
  B := [2, 4, 6, 8, 10];  { Even numbers }

  C := A + B;    { [1..10] — union: all numbers }
  C := A * B;    { []      — intersection: no overlap }
  C := A - B;    { [1,3,5,7,9] — difference: odds minus evens = odds }
end.

A practical example — computing working days:

var
  AllDays, Holidays, Vacation, WorkDays: TDays;
begin
  AllDays  := [Monday..Friday];
  Holidays := [Monday];            { Long weekend }
  Vacation := [Thursday, Friday];  { Taking Thu-Fri off }

  WorkDays := AllDays - Holidays - Vacation;
  { WorkDays = [Tuesday, Wednesday] }
end.

Sets of Char: An Everyday Workhorse

One of the most practical uses of sets is set of Char (or more precisely, set of AnsiChar in Free Pascal). Character sets make input validation, parsing, and text processing dramatically cleaner:

const
  Vowels     = ['A','E','I','O','U','a','e','i','o','u'];
  Digits     = ['0'..'9'];
  WhiteSpace = [' ', #9, #10, #13];   { space, tab, LF, CR }
  AlphaNum   = ['A'..'Z', 'a'..'z', '0'..'9'];

var
  Ch: Char;
begin
  Write('Enter a character: ');
  ReadLn(Ch);

  if Ch in Vowels then
    WriteLn('Vowel')
  else if Ch in Digits then
    WriteLn('Digit')
  else if Ch in WhiteSpace then
    WriteLn('Whitespace')
  else
    WriteLn('Other character');
end.

This pattern replaces dozens of individual character comparisons with clean, readable set membership tests.

Building Sets Incrementally

You do not have to define a set all at once. Sets can be built up incrementally using the + operator or the Include procedure:

var
  AllowedChars: set of Char;
begin
  AllowedChars := ['A'..'Z', 'a'..'z'];   { Start with letters }
  AllowedChars := AllowedChars + ['0'..'9'];   { Add digits }
  AllowedChars := AllowedChars + ['_', '-', '.'];  { Add special chars }
end.

This incremental construction is useful when different parts of your program contribute different elements to a set, or when the set contents depend on configuration options.

Sets in Boolean Expressions

Sets integrate cleanly with Pascal's boolean operators. You can combine set membership tests with and, or, and not:

{ Check if a character is a letter but not a vowel (i.e., a consonant) }
if (Ch in ['A'..'Z', 'a'..'z']) and not (Ch in Vowels) then
  WriteLn('Consonant');

{ Check if a day is either a weekend day or a holiday }
if (Day in Weekend) or (Day in Holidays) then
  WriteLn('Day off');

However, when you find yourself writing complex boolean expressions involving multiple set membership tests, consider whether a single set operation would be clearer. The consonant check above is better written as:

if Ch in (['A'..'Z', 'a'..'z'] - Vowels) then
  WriteLn('Consonant');

12.5 Set Operations in Detail

Set Comparison Operators

Pascal supports several comparison operators for sets:

Operator Meaning Example
= Equal (same elements) [1,2,3] = [3,2,1] is True
<> Not equal [1,2] <> [1,3] is True
<= Subset (or equal) [1,2] <= [1,2,3] is True
>= Superset (or equal) [1,2,3] >= [1,2] is True

Note that sets are unordered[1,2,3] = [3,1,2] is True. Order does not matter; only membership matters.

The subset operator is particularly useful for checking whether one set of requirements is met by another:

type
  TPermission = (pRead, pWrite, pExecute, pDelete, pAdmin);
  TPermissions = set of TPermission;

function HasRequiredPermissions(UserPerms, Required: TPermissions): Boolean;
begin
  Result := Required <= UserPerms;
end;

If every permission in Required is also in UserPerms, the function returns True. One line of code replaces what would be a loop or a chain of and conditions in other languages.

Include and Exclude: Modifying Sets

The Include and Exclude procedures add or remove a single element from a set:

var
  Perms: TPermissions;
begin
  Perms := [];                    { Empty set }
  Include(Perms, pRead);          { Perms = [pRead] }
  Include(Perms, pWrite);         { Perms = [pRead, pWrite] }
  Exclude(Perms, pRead);          { Perms = [pWrite] }
end.

You could achieve the same thing with + and -:

Perms := Perms + [pRead];     { Same as Include(Perms, pRead) }
Perms := Perms - [pRead];     { Same as Exclude(Perms, pRead) }

However, Include and Exclude are slightly more efficient because they operate on a single element without constructing a temporary set. In tight loops, this can make a measurable difference.

The Empty Set

The empty set is denoted []:

var
  S: TDays;
begin
  S := [];                      { No days selected }
  if S = [] then
    WriteLn('No days selected');
end.

You can test whether a set is empty with = [], or equivalently, whether two sets have no overlap:

if A * B = [] then
  WriteLn('A and B have no elements in common');

This is the set-theoretic concept of disjointness: two sets are disjoint if their intersection is empty.

Symmetric Difference

Pascal does not have a built-in symmetric difference operator (elements in either set but not both), but you can compute it easily:

function SymmetricDifference(A, B: TDays): TDays;
begin
  Result := (A + B) - (A * B);
  { or equivalently: Result := (A - B) + (B - A); }
end;

Iterating Over Set Members

Pascal does not provide a direct "for each element in set" syntax. The standard idiom is to iterate over all possible values and test membership:

var
  WorkDays: TDays;
  Day: TDay;
begin
  WorkDays := [Monday, Wednesday, Friday];

  for Day := Low(TDay) to High(TDay) do
    if Day in WorkDays then
      WriteLn(DayToStr(Day), ' is a work day');
end.

This is efficient because the in test is a single machine instruction (as we will see in Section 12.6).


12.6 How Sets Work: Bit Vectors Under the Hood

The Bit Vector Representation

Understanding how Pascal implements sets gives you insight into both their power and their limitations. A Pascal set is stored as a bit vector — a sequence of bits where each bit represents one possible element. If the bit is 1, the element is in the set; if 0, it is not.

For a set of TDay (7 possible values), the compiler uses a single byte:

Bit:     6       5       4       3       2       1       0
Day:   Sunday Saturday Friday Thursday Wednesday Tuesday Monday
Value:   0       1       0       0       0       0       1

The bit pattern above represents the set [Monday, Saturday].

Why Set Operations Are Fast

Because sets are bit vectors, set operations map directly to CPU instructions:

Set Operation CPU Operation Speed
A + B (union) Bitwise OR 1 clock cycle
A * B (intersection) Bitwise AND 1 clock cycle
A - B (difference) Bitwise AND NOT 1-2 clock cycles
x in S (membership) Bit test 1 clock cycle
A = B (equality) Compare 1 clock cycle
A <= B (subset) AND + Compare 2 clock cycles

This is why if Ch in ['0'..'9'] is not just more readable than a chain of comparisons — it is actually faster. The compiler translates the set membership test into a single bit-test instruction.

Size Limitations

The bit vector representation imposes a practical limit: a set can contain at most 256 elements in Free Pascal (ordinal values 0 through 255). This means:

  • set of Byte — valid (256 values, uses 32 bytes)
  • set of Char — valid (256 values for AnsiChar, uses 32 bytes)
  • set of 0..255 — valid
  • set of Integerinvalid (too many possible values)
  • set of WideCharinvalid (65,536 possible values)

The base type must be an ordinal type with at most 256 values. This is why sets work beautifully with enumerations (which rarely have more than a few dozen values) and characters, but not with integers or strings.

Memory Layout

The size of a set variable depends on the range of the base type:

Base Type Range Set Size
set of TDay 0..6 1 byte (padded to 4 on some platforms)
set of Char 0..255 32 bytes
set of Byte 0..255 32 bytes
set of 0..31 0..31 4 bytes

For small enumerations, the set fits in a single machine word, making operations extraordinarily efficient. Even set of Char at 32 bytes is small enough to be handled efficiently.

Implications for Design

The 256-element limit means you cannot create a set of Integer. If you need set-like behavior for larger collections, you would use:

  • Dynamic arrays with search functions
  • Sorted lists with binary search
  • Hash sets from Free Pascal's Generics.Collections unit

But for the many, many cases where your domain has a small, fixed set of possibilities — days of the week, file permissions, character classes, error codes, menu options — Pascal's built-in sets are unbeatable in clarity and performance.

A Concrete Example: Tracing a Set Operation

Let us trace through a set operation at the bit level to make this concrete. Suppose we have:

type
  TDay = (Mon, Tue, Wed, Thu, Fri, Sat, Sun);
var
  A, B, C: set of TDay;
begin
  A := [Mon, Wed, Fri];    { Binary: 0010101 }
  B := [Wed, Thu, Fri];    { Binary: 0011100 }
  C := A * B;              { AND:    0010100 = [Wed, Fri] }
end.

Here is the bit-by-bit trace:

        Sun Sat Fri Thu Wed Tue Mon
A:       0   0   1   0   1   0   1    = [Mon, Wed, Fri]
B:       0   0   1   1   1   0   0    = [Wed, Thu, Fri]
A AND B: 0   0   1   0   1   0   0    = [Wed, Fri]
A OR B:  0   0   1   1   1   0   1    = [Mon, Wed, Thu, Fri]
A ANDNOT B: 0 0 0   0   0   0   1    = [Mon]

The intersection (*) keeps only the days present in both sets. The union (+) keeps days present in either set. The difference (-) keeps days in A that are not in B. Each operation is a single CPU instruction on these small sets. This is why Pascal sets are often faster than the alternative approaches used in languages without native set support.


12.7 Practical Applications of Sets and Enumerations

Application 1: Input Validation

Sets make input validation concise and robust:

function IsValidIdentifier(const S: string): Boolean;
var
  I: Integer;
  ValidFirst: set of Char;
  ValidRest: set of Char;
begin
  ValidFirst := ['A'..'Z', 'a'..'z', '_'];
  ValidRest  := ValidFirst + ['0'..'9'];

  Result := False;
  if Length(S) = 0 then Exit;
  if not (S[1] in ValidFirst) then Exit;

  for I := 2 to Length(S) do
    if not (S[I] in ValidRest) then Exit;

  Result := True;
end;

This function checks whether a string is a valid Pascal identifier. The set-based approach is cleaner than any alternative using individual character comparisons.

Application 2: Parsing and Tokenizing

When building a simple parser or tokenizer, character sets define the rules for what constitutes a valid token. Here is a basic number parser:

function ParseNumber(const S: string; var Pos: Integer): Real;
var
  NumStr: string;
begin
  NumStr := '';

  { Skip leading whitespace }
  while (Pos <= Length(S)) and (S[Pos] in [' ', #9]) do
    Inc(Pos);

  { Optional sign }
  if (Pos <= Length(S)) and (S[Pos] in ['+', '-']) then
  begin
    NumStr := S[Pos];
    Inc(Pos);
  end;

  { Integer part }
  while (Pos <= Length(S)) and (S[Pos] in ['0'..'9']) do
  begin
    NumStr := NumStr + S[Pos];
    Inc(Pos);
  end;

  { Optional decimal part }
  if (Pos <= Length(S)) and (S[Pos] = '.') then
  begin
    NumStr := NumStr + '.';
    Inc(Pos);
    while (Pos <= Length(S)) and (S[Pos] in ['0'..'9']) do
    begin
      NumStr := NumStr + S[Pos];
      Inc(Pos);
    end;
  end;

  Val(NumStr, Result);
end;

Every character classification in this parser — whitespace, signs, digits, decimal point — is a set membership test. The logic reads naturally: "while the current character is in the set of digits, keep consuming." This is the parsing pattern you will see in compilers, interpreters, and data processors everywhere.

Application 3: Feature Flags and Configuration

Enumerations and sets are perfect for feature flags:

type
  TFeature = (fDarkMode, fNotifications, fAutoSave, fSpellCheck,
              fCloudSync, fTwoFactor, fBetaFeatures);
  TFeatures = set of TFeature;

var
  UserSettings: TFeatures;
begin
  UserSettings := [fDarkMode, fAutoSave, fCloudSync];

  if fTwoFactor in UserSettings then
    WriteLn('Two-factor authentication is enabled')
  else
    WriteLn('Consider enabling two-factor authentication');

  { Enable a feature }
  Include(UserSettings, fNotifications);

  { Check multiple features at once }
  if [fCloudSync, fAutoSave] <= UserSettings then
    WriteLn('Your data is safely backed up');
end.

Application 4: State Machines with Enumerations

Enumerations are the natural choice for representing states in a state machine. A state machine is a model of computation where a system is always in exactly one of a finite number of states, and transitions between states are triggered by events. Enumerations map to this concept perfectly — the type is the set of possible states:

type
  TOrderState = (osCreated, osConfirmed, osPaid, osShipped, osDelivered, osCancelled);

function NextState(Current: TOrderState): TOrderState;
begin
  case Current of
    osCreated:   Result := osConfirmed;
    osConfirmed: Result := osPaid;
    osPaid:      Result := osShipped;
    osShipped:   Result := osDelivered;
    osDelivered: Result := osDelivered;  { Terminal state }
    osCancelled: Result := osCancelled;  { Terminal state }
  end;
end;

function CanCancel(State: TOrderState): Boolean;
begin
  Result := State in [osCreated, osConfirmed, osPaid];
  { Cannot cancel once shipped }
end;

The CanCancel function uses a set literal to express the business rule in a single, readable line. Compare this with the equivalent without sets:

Result := (State = osCreated) or (State = osConfirmed) or (State = osPaid);

The set version is not only shorter — it is easier to modify. Adding a new cancellable state is as simple as adding it to the set literal.

State machines with enumerations appear in many real-world applications: network protocol handlers (connecting, authenticating, transferring, disconnecting), UI workflow managers (browsing, editing, saving, reviewing), game logic (menu, playing, paused, game-over), and more. The combination of enumerated states and set-based transition rules creates code that is both readable and provably correct.

Application 5: Menu Systems

Sets can track which menu options are available:

type
  TMenuOption = (moNew, moOpen, moSave, moSaveAs, moPrint, moExport, moQuit);
  TMenuOptions = set of TMenuOption;

procedure DisplayMenu(Available: TMenuOptions);
var
  Opt: TMenuOption;
  Names: array[TMenuOption] of string = (
    'New', 'Open', 'Save', 'Save As', 'Print', 'Export', 'Quit'
  );
begin
  WriteLn('Available options:');
  for Opt := Low(TMenuOption) to High(TMenuOption) do
    if Opt in Available then
      WriteLn('  [', Ord(Opt) + 1, '] ', Names[Opt]);
end;

You can adjust the available options based on the current state:

var
  Options: TMenuOptions;
begin
  Options := [moNew, moOpen, moQuit];  { No file open }

  if FileIsOpen then
    Options := Options + [moSave, moSaveAs, moPrint, moExport];

  if HasUnsavedChanges then
    Exclude(Options, moQuit);  { Force save before quit }

  DisplayMenu(Options);
end.

12.8 Sets vs. Other Languages: Pascal Did It First

Python Sets

Python gained a built-in set type in version 2.4 (2004). Python sets are hash-based, meaning they can hold any hashable object and have no size limit. But they pay for this generality with overhead:

# Python
vowels = {'a', 'e', 'i', 'o', 'u'}
if ch in vowels:
    print("Vowel")

Pascal's equivalent:

{ Pascal }
const Vowels = ['a','e','i','o','u'];
if Ch in Vowels then
  WriteLn('Vowel');

Pascal's version is compiled to a single bit-test instruction. Python's version involves a hash computation, bucket lookup, and pointer comparison. For small, fixed sets — which is the common case — Pascal's approach is orders of magnitude faster.

However, Python's sets can hold strings, tuples, or any hashable object. Pascal's sets are limited to ordinal types with at most 256 values. Different tools for different jobs.

C and Bit Flags

C uses integer constants and bitwise operations to simulate sets:

/* C */
#define PERM_READ    0x01
#define PERM_WRITE   0x02
#define PERM_EXECUTE 0x04
#define PERM_DELETE  0x08

unsigned int perms = PERM_READ | PERM_WRITE;
if (perms & PERM_READ) {
    printf("Can read\n");
}

Pascal's equivalent:

{ Pascal }
type
  TPermission = (pRead, pWrite, pExecute, pDelete);
  TPermissions = set of TPermission;
var
  Perms: TPermissions;
begin
  Perms := [pRead, pWrite];
  if pRead in Perms then
    WriteLn('Can read');
end.

Under the hood, both produce identical machine code. But Pascal's version is type-safe (you cannot accidentally OR a permission with a file mode), self-documenting (no magic hex constants), and compiler-checked (misspelling pRead is a compile error; misspelling PERM_RAED in C might silently compile as 0).

C# Flags Enums

C# adopted Pascal-like flags with the [Flags] attribute:

// C#
[Flags]
enum Permission { Read = 1, Write = 2, Execute = 4, Delete = 8 }
var perms = Permission.Read | Permission.Write;
if (perms.HasFlag(Permission.Read)) { ... }

This works, but the programmer must manually assign powers of two. Pascal handles this automatically.

Java EnumSet

Java's EnumSet (added in Java 5, 2004) is the closest equivalent to Pascal's sets:

// Java
EnumSet<Day> weekend = EnumSet.of(Day.SATURDAY, Day.SUNDAY);
if (weekend.contains(today)) { ... }

Like Pascal, EnumSet is implemented as a bit vector and is highly efficient. But it required a class library, method calls, and arrived 34 years after Pascal. Wirth got it right in 1970.

Go and the iota Pattern

Go uses a const block with iota to simulate enumerations and bitwise operations for flag sets:

// Go
const (
    Read    = 1 << iota  // 1
    Write                // 2
    Execute              // 4
    Delete               // 8
)
perms := Read | Write
if perms & Read != 0 { ... }

This is the same manual bit manipulation as C, with slightly more convenient syntax. It is still error-prone — there is no type preventing you from combining a permission flag with an unrelated integer.

The Lesson

Pascal's sets are not a historical curiosity — they are a design that was ahead of its time. Every major language has eventually adopted some form of enum-based flag sets, often with more syntax and less elegance. When you use Pascal sets, you are using the original and, arguably, still the best implementation of this concept.

The table below summarizes the comparison:

Feature Pascal C Python Java C#
Named enum values Built-in #define / enum enum.Enum enum enum
Type-safe enums Yes No Partial Yes Yes
Native set type set of No set() EnumSet [Flags]
Bit-vector backed Yes Manual No (hash) Yes Manual
Set operators + * - in | & ~ & | & - in Methods | & ~ &
Compile-time safety Full None None Partial Partial
Year introduced 1970 1972 2004 2004 2002

Pascal had it all in 1970. Other languages needed 30-35 years to catch up, and most still require more boilerplate to achieve the same level of type safety and expressiveness.


12.9 Project Checkpoint: PennyWise Enums and Sets

It is time to apply enumerations and sets to our PennyWise personal budget tracker. In previous chapters, we stored expense categories as strings — fragile, error-prone, and hard to validate. Now we will replace them with proper enumerated types and use sets for filtering.

Step 1: Define the Expense Category Enumeration

type
  TExpenseCategory = (
    ecFood,
    ecTransport,
    ecHousing,
    ecUtilities,
    ecEntertainment,
    ecHealth,
    ecOther
  );

We prefix each value with ec (expense category) to avoid name collisions. ecFood is more specific than bare Food, which might conflict with identifiers in a meal-planning module.

Step 2: Category-to-String Conversion

function CategoryToStr(Cat: TExpenseCategory): string;
begin
  case Cat of
    ecFood:          Result := 'Food & Groceries';
    ecTransport:     Result := 'Transportation';
    ecHousing:       Result := 'Housing & Rent';
    ecUtilities:     Result := 'Utilities';
    ecEntertainment: Result := 'Entertainment';
    ecHealth:        Result := 'Health & Medical';
    ecOther:         Result := 'Other';
  end;
end;

Step 3: Use Sets for Filtering

A key PennyWise feature is filtering the expense report by category. With sets, this becomes trivial:

type
  TCategoryFilter = set of TExpenseCategory;

procedure PrintFilteredReport(const Expenses: array of TExpense;
                               Filter: TCategoryFilter);
var
  I: Integer;
  Total: Real;
begin
  Total := 0;
  WriteLn('--- Filtered Expense Report ---');

  for I := 0 to High(Expenses) do
    if Expenses[I].Category in Filter then
    begin
      WriteLn(CategoryToStr(Expenses[I].Category):20,
              Expenses[I].Amount:10:2);
      Total := Total + Expenses[I].Amount;
    end;

  WriteLn('-------------------------------');
  WriteLn('Total:':20, Total:10:2);
end;

Now users can create flexible filters:

var
  EssentialFilter: TCategoryFilter;
  DiscretionaryFilter: TCategoryFilter;
begin
  EssentialFilter := [ecFood, ecHousing, ecUtilities, ecHealth];
  DiscretionaryFilter := [ecEntertainment, ecTransport, ecOther];

  WriteLn('=== Essential Expenses ===');
  PrintFilteredReport(MyExpenses, EssentialFilter);

  WriteLn;
  WriteLn('=== Discretionary Expenses ===');
  PrintFilteredReport(MyExpenses, DiscretionaryFilter);

  WriteLn;
  WriteLn('=== All Expenses ===');
  PrintFilteredReport(MyExpenses, [ecFood..ecOther]);
end.

Step 4: String-to-Enum Conversion for User Input

When the user types a category name, we need to convert the string back to an enumeration value. This is the reverse of CategoryToStr:

function StrToCategory(const S: string; var Cat: TExpenseCategory): Boolean;
var
  LowerS: string;
begin
  LowerS := LowerCase(S);
  Result := True;
  if (LowerS = 'food') or (LowerS = '1') then Cat := ecFood
  else if (LowerS = 'transport') or (LowerS = '2') then Cat := ecTransport
  else if (LowerS = 'housing') or (LowerS = '3') then Cat := ecHousing
  else if (LowerS = 'utilities') or (LowerS = '4') then Cat := ecUtilities
  else if (LowerS = 'entertainment') or (LowerS = '5') then Cat := ecEntertainment
  else if (LowerS = 'health') or (LowerS = '6') then Cat := ecHealth
  else if (LowerS = 'other') or (LowerS = '7') then Cat := ecOther
  else Result := False;  { Invalid input }
end;

Notice that once the string is converted to a TExpenseCategory, all subsequent code works with the type-safe enumeration. The string-to-enum boundary is the only place where invalid input can enter, and we handle it explicitly with the Boolean return value. This is a fundamental pattern: validate at the boundary, then work with strong types internally.

Step 5: Defining Preset Filters

PennyWise can offer preset filters for common reporting needs:

const
  ESSENTIAL_FILTER: TCategoryFilter = [ecFood, ecHousing, ecUtilities, ecHealth];
  DISCRETIONARY_FILTER: TCategoryFilter = [ecEntertainment, ecTransport, ecOther];
  ALL_FILTER: TCategoryFilter = [ecFood..ecOther];

Users can select a preset filter or build a custom one by toggling categories on and off. The set operations make combining filters trivial:

{ User wants essentials plus entertainment }
var
  MyFilter: TCategoryFilter;
begin
  MyFilter := ESSENTIAL_FILTER + [ecEntertainment];
  PrintFilteredReport(MyExpenses, MyFilter);
end.

Step 6: Category Summary with Enum-Indexed Arrays

We can use the enumeration to index a summary array:

var
  CategoryTotals: array[TExpenseCategory] of Real;
  Cat: TExpenseCategory;
  I: Integer;
begin
  { Initialize }
  for Cat := Low(TExpenseCategory) to High(TExpenseCategory) do
    CategoryTotals[Cat] := 0;

  { Accumulate }
  for I := 0 to High(Expenses) do
    CategoryTotals[Expenses[I].Category] :=
      CategoryTotals[Expenses[I].Category] + Expenses[I].Amount;

  { Display }
  WriteLn('Category':20, 'Total':10);
  WriteLn('':30);
  for Cat := Low(TExpenseCategory) to High(TExpenseCategory) do
    WriteLn(CategoryToStr(Cat):20, CategoryTotals[Cat]:10:2);
end.

No searching, no string comparisons, no hash lookups. The enumeration value is the array index. This is clean, fast, and impossible to get wrong.

Step 7: Budget Analysis with Set Operations

We can use set operations to analyze spending patterns:

procedure AnalyzeSpending(const Expenses: array of TExpense);
var
  CategoriesUsed: TCategoryFilter;
  CategoriesUnused: TCategoryFilter;
  Cat: TExpenseCategory;
  I: Integer;
  EssentialTotal, DiscretionaryTotal: Real;
begin
  CategoriesUsed := [];
  EssentialTotal := 0;
  DiscretionaryTotal := 0;

  for I := 0 to High(Expenses) do
  begin
    Include(CategoriesUsed, Expenses[I].Category);
    if Expenses[I].Category in ESSENTIAL_FILTER then
      EssentialTotal := EssentialTotal + Expenses[I].Amount
    else
      DiscretionaryTotal := DiscretionaryTotal + Expenses[I].Amount;
  end;

  { Which categories had no spending? }
  CategoriesUnused := ALL_FILTER - CategoriesUsed;

  WriteLn('Essential spending:     $', EssentialTotal:0:2);
  WriteLn('Discretionary spending: $', DiscretionaryTotal:0:2);

  if CategoriesUnused <> [] then
  begin
    WriteLn('Categories with no expenses:');
    for Cat := Low(TExpenseCategory) to High(TExpenseCategory) do
      if Cat in CategoriesUnused then
        WriteLn('  - ', CategoryToStr(Cat));
  end;
end;

The set difference ALL_FILTER - CategoriesUsed instantly tells us which categories have no expenses this month — a useful insight for budget analysis. Without sets, finding unused categories would require nested loops or boolean flags.

What Changed in PennyWise

Before (Chapters 8-11) After (Chapter 12)
Category: string Category: TExpenseCategory
String comparisons for filtering Set membership (in) for filtering
No validation on category input Compiler enforces valid categories
Ad hoc category totals Enum-indexed array for totals
No filter combinations Set union/intersection for complex filters

The PennyWise project checkpoint code (see code/project-checkpoint.pas) contains the complete implementation.


12.10 Chapter Summary

We have covered a lot of ground in this chapter. Let us step back and see the big picture.

This chapter introduced three of Pascal's most distinctive features — enumerated types, subrange types, and sets — and showed how they work together to create code that is simultaneously more readable, more correct, and more efficient.

Enumerated types replace magic numbers with meaningful names. They create new types that the compiler can check, they work with case statements for exhaustiveness checking, and they serve as array indices for self-documenting data structures.

Subrange types restrict existing types to valid ranges. Combined with the {$R+} directive, they turn range violations from silent bugs into immediate errors. They express constraints — "this value must be between 1 and 12" — in the type system itself, where the compiler can enforce them.

Sets bring mathematical set theory into the language with extraordinary elegance. Union, intersection, difference, and membership testing map to single CPU instructions, making sets both the clearest and the fastest way to work with collections of flags, options, or categories. The in operator alone justifies learning Pascal sets — it replaces chains of comparisons with a single, readable expression.

Together, these features embody Pascal's philosophy: the type system should work for you, catching errors at compile time and making code self-documenting. Other languages have adopted these ideas — Python's enums, Java's EnumSet, C#'s flags — but none with the simplicity and integration that Pascal achieved in 1970.

In our PennyWise project, we replaced fragile string-based categories with proper enumerations and used sets for flexible, efficient filtering. This is a pattern you will see repeatedly in real-world Pascal programs: define the domain with enumerations, constrain it with subranges, and manipulate it with sets.

The three features form a hierarchy of increasing power:

  1. Enumerations name your values. They replace magic numbers and give the compiler something to check.
  2. Subranges constrain your values. They encode business rules ("month must be 1-12") in the type system.
  3. Sets collect your values. They let you work with groups of values as first-class entities — testing membership, computing unions and intersections, and filtering data with remarkable conciseness.

Each feature is useful on its own, but they are most powerful in combination. An enumeration defines the vocabulary; a subrange restricts it to what is valid in a particular context; and a set lets you work with arbitrary subsets of the vocabulary. This is Wirth's "Algorithms + Data Structures = Programs" (Theme 5) in action: choosing the right data structure — in this case, the right type — simplifies the algorithm so much that the code nearly writes itself.

Key Terms

  • Enumerated type — A user-defined ordinal type consisting of a list of named values
  • Ordinal value — The integer position of an enumerated value (starting from 0)
  • Ordinal type — Any type with a defined sequence, supporting Ord, Succ, Pred, Low, High
  • Subrange type — A type restricted to a contiguous subset of an ordinal type
  • Range checking — Runtime verification that a value falls within its type's valid range ({$R+})
  • Set — An unordered collection of distinct elements from an ordinal base type
  • Bit vector — The internal representation of a set, where each bit represents one possible element
  • Set membership — Testing whether a value is in a set using the in operator
  • Union — The combination of two sets (+), containing elements from either or both
  • Intersection — The overlap of two sets (*), containing only elements in both
  • Difference — Elements in one set but not another (-)
  • Subset — A set where every element is also in another set (<=)
  • Disjoint — Two sets with no elements in common (intersection is empty)
  • Scoped enumeration — An enumeration where values must be qualified with the type name
  • Exhaustiveness checking — Compiler verification that a case statement covers all enumeration values

Common Mistakes to Avoid

  1. Forgetting {$R+} during development. Without range checking, subrange violations are silent. Always enable range checking while developing and testing.

  2. Using Succ on the last value or Pred on the first. These cause runtime errors. Always check boundaries: if Day < High(TDay) then Day := Succ(Day).

  3. Trying to create a set of Integer. Sets are limited to ordinal types with at most 256 values. Use enumerations or byte-range subranges as set base types.

  4. Forgetting that in puts the element on the left. Write if x in S, not if S in x. The element is tested against the set, not the other way around.

  5. Not handling the string-to-enum boundary. Enumerations are not strings. You need explicit conversion functions to go between user-facing strings and internal enumeration values.

  6. Assuming set order matters. [1, 2, 3] = [3, 1, 2] is True. Sets are unordered collections. If order matters, use an array instead.


Spaced Review

These questions revisit material from earlier chapters to strengthen long-term retention.

From Chapter 10: What function finds the position of a substring within a string?

The Pos function returns the position of a substring within a string. Pos('world', 'Hello world') returns 7. It returns 0 if the substring is not found.

From Chapter 8: What is variable shadowing and why is it a problem?

Variable shadowing occurs when a local variable in an inner scope has the same name as a variable in an outer scope. The inner variable "shadows" the outer one, making it inaccessible. This is a problem because it can lead to subtle bugs where you think you are modifying the outer variable but are actually modifying the local copy.


In the next chapter, we turn to records — Pascal's way of grouping related data of different types into a single structure. Combined with the enumerations and sets from this chapter, records will give our PennyWise project the structured data model it needs.