> "The purpose of a type system is not to make programming harder, but to make programs easier to understand."
In This Chapter
- 12.1 Enumerated Types: Naming Your Values
- 12.2 Using Enumerations Effectively
- 12.3 Subrange Types: Restricting Valid Values
- 12.4 Sets: Pascal's Secret Weapon
- 12.5 Set Operations in Detail
- 12.6 How Sets Work: Bit Vectors Under the Hood
- 12.7 Practical Applications of Sets and Enumerations
- 12.8 Sets vs. Other Languages: Pascal Did It First
- 12.9 Project Checkpoint: PennyWise Enums and Sets
- 12.10 Chapter Summary
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.
Navigating Enumerations: Succ, Pred, Low, High
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 forAnsiChar, uses 32 bytes)set of 0..255— validset of Integer— invalid (too many possible values)set of WideChar— invalid (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.Collectionsunit
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:
- Enumerations name your values. They replace magic numbers and give the compiler something to check.
- Subranges constrain your values. They encode business rules ("month must be 1-12") in the type system.
- 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
inoperator - 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
casestatement covers all enumeration values
Common Mistakes to Avoid
-
Forgetting
{$R+}during development. Without range checking, subrange violations are silent. Always enable range checking while developing and testing. -
Using
Succon the last value orPredon the first. These cause runtime errors. Always check boundaries:if Day < High(TDay) then Day := Succ(Day). -
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. -
Forgetting that
inputs the element on the left. Writeif x in S, notif S in x. The element is tested against the set, not the other way around. -
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.
-
Assuming set order matters.
[1, 2, 3] = [3, 1, 2]isTrue. 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.