Exercism - Forth
This post shows you how to get Forth exercise of Exercism.
Preparation
Before we click on our next exercise, let’s see what concepts of DART we need to consider

So we need to use the following concepts.
Lists as Stacks
A stack is a data structure that follows Last-In-First-Out (LIFO) principle. Lists in Dart can be used as stacks with add() for push and removeLast() for pop.
void main() {
List<int> stack = [];
// Push (add to end)
stack.add(1);
stack.add(2);
stack.add(3);
print(stack); // [1, 2, 3]
// Pop (remove from end)
int top = stack.removeLast();
print(top); // 3
print(stack); // [1, 2]
// Peek (view top without removing)
int last = stack.last;
print(last); // 2
}
Maps for Definitions
Maps can store word definitions, where keys are word names and values are lists of tokens that define the word.
void main() {
Map<String, List<String>> definitions = {};
// Define a word
definitions['double'] = ['dup', '+'];
// Look up definition
List<String>? def = definitions['double'];
print(def); // [dup, +]
// Check if word exists
if (definitions.containsKey('double')) {
print('Word exists');
}
}
String Parsing and Tokenization
Strings can be split into tokens using split() with regular expressions. This is essential for parsing Forth code.
void main() {
String input = "1 2 + dup";
// Split by whitespace
List<String> tokens = input.split(RegExp(r'\s+'));
print(tokens); // [1, 2, +, dup]
// Convert to lowercase
String lower = input.toLowerCase();
print(lower); // "1 2 + dup"
// Combine operations
List<String> parsed = input.toLowerCase().split(RegExp(r'\s+'));
print(parsed); // [1, 2, +, dup]
}
Integer Parsing
The int.tryParse() method attempts to parse a string as an integer, returning null if it fails. This is useful for distinguishing numbers from words.
void main() {
// Successful parse
int? num1 = int.tryParse('123');
print(num1); // 123
// Failed parse
int? num2 = int.tryParse('abc');
print(num2); // null
// Pattern matching with tryParse
String token = '42';
if (int.tryParse(token) case final num?) {
print('Number: $num'); // Number: 42
} else {
print('Not a number');
}
}
Pattern Matching with Case Expressions
Dart 3.0+ supports pattern matching with case expressions, allowing elegant handling of optional values and switch statements.
void main() {
Map<String, int> map = {'key': 42};
// Pattern matching in if
if (map['key'] case final value?) {
print('Value: $value'); // Value: 42
}
// Pattern matching in switch
String token = 'test';
switch (token) {
case 'test' when token.length > 3:
print('Long test');
case 'test':
print('Short test');
default:
print('Other');
}
}
List Operations
Lists support various operations like sublist(), indexOf(), addAll(), and accessing elements by index.
void main() {
List<String> tokens = ['a', 'b', 'c', 'd', 'e'];
// Get sublist
List<String> sub = tokens.sublist(1, 4);
print(sub); // [b, c, d]
// Find index
int index = tokens.indexOf('c');
print(index); // 2
// Add multiple elements
List<String> newList = [];
newList.addAll(['x', 'y']);
print(newList); // [x, y]
// Access by negative index (from end)
int len = tokens.length;
int secondLast = tokens[len - 2];
print(secondLast); // d
}
Exception Handling
Exceptions are used to signal errors like stack underflow, division by zero, or unknown commands.
void main() {
// Throwing exceptions
void checkStack(List<int> stack, int required) {
if (stack.length < required) {
throw Exception('Stack empty');
}
}
// Catching exceptions
try {
checkStack([], 2);
} catch (e) {
print('Error: $e'); // Error: Exception: Stack empty
}
}
Recursive Processing
Functions can call themselves to process nested structures, like expanding user-defined words in Forth.
void main() {
List<String> process(List<String> tokens) {
List<String> result = [];
for (var token in tokens) {
if (token == 'expand') {
// Recursive call
result.addAll(process(['a', 'b']));
} else {
result.add(token);
}
}
return result;
}
List<String> output = process(['x', 'expand', 'y']);
print(output); // [x, a, b, y]
}
Cascade Operator
The cascade operator (..) allows multiple operations on the same object in sequence.
void main() {
List<int> stack = [];
// Multiple operations with cascade
stack..add(1)..add(2)..add(3);
print(stack); // [1, 2, 3]
// Useful for stack operations
int a = 1, b = 2;
stack..add(b)..add(a);
print(stack); // [1, 2, 3, 2, 1]
}
Introduction
Implement an evaluator for a very simple subset of Forth.
Forth is a stack-based programming language. Implement a very basic evaluator for a small subset of Forth.
Your evaluator has to support the following words:
- Arithmetic:
+,-,*,/(integer arithmetic) - Stack manipulation:
DUP,DROP,SWAP,OVER - Word definitions:
: word-name definition ;(defining new words)
To keep things simple the only data type you need to support is signed integers of at least 16 bits size.
You should use the following rules for the syntax: a number is a sequence of one or more (ASCII) digits, a word is a sequence of one or more letters, digits, symbols or punctuation that is not a number. (Forth probably uses slightly different rules, but this is close enough.)
Words are case-insensitive.
What is Forth?
Forth is a stack-based, concatenative programming language and programming environment. It was created by Charles H. Moore in the 1970s. Forth uses a stack to hold arguments and return values, and words (functions) operate on the stack.
In Forth, you write words (functions) that manipulate a stack. Numbers are pushed onto the stack, and operations consume values from the stack and push results back.
— Programming Languages
How does Forth work?
Forth is a stack-based language where:
- Numbers are pushed onto the stack
- Words (operations) pop values from the stack, perform operations, and push results back
- Stack operations manipulate the stack itself (duplicate, swap, etc.)
- User definitions allow creating new words from existing ones
For example:
1 2 +pushes 1, pushes 2, then adds them (result: 3 on stack): double dup + ;defines a word “double” that duplicates the top value and adds them5 doublepushes 5, then executes “double” (result: 10 on stack)
Examples
1 2 +→ Stack: [3]1 2 3 swap→ Stack: [1, 3, 2] (swaps top two)1 2 3 over→ Stack: [1, 2, 3, 2] (copies second-to-top): double dup + ; 5 double→ Stack: [10]
Solution
class Forth {
final List<int> _stack = [];
final Map<String, List<String>> _definitions = {};
List<int> get stack => List.unmodifiable(_stack);
void evaluate(String input) {
_processTokens(input.toLowerCase().split(RegExp(r'\s+')));
}
void _processTokens(List<String> tokens) {
for (int i = 0; i < tokens.length; i++) {
final token = tokens[i];
if (token == ':') {
final end = tokens.indexOf(';', i);
final name = tokens[i + 1];
if (int.tryParse(name) != null) throw Exception('Invalid definition');
// Expand the definition by resolving existing words NOW
final rawDef = tokens.sublist(i + 2, end);
final expandedDef = _expandDefinition(rawDef);
_definitions[name] = expandedDef;
i = end;
} else if (int.tryParse(token) case final num?) {
_stack.add(num);
} else {
_executeToken(token);
}
}
}
// Expand user-defined words in a definition to their current values
List<String> _expandDefinition(List<String> tokens) {
final expanded = <String>[];
for (final token in tokens) {
if (_definitions.containsKey(token)) {
// Use the current definition of this word
expanded.addAll(_definitions[token]!);
} else {
expanded.add(token);
}
}
return expanded;
}
void _executeToken(String token) {
if (_definitions[token] case final def?) {
return _processTokens(def);
}
void require(int n) {
if (_stack.length < n) throw Exception('Stack empty');
}
int pop() => _stack.removeLast();
switch (token) {
case '+':
require(2);
_stack.add(pop() + pop());
case '-':
require(2);
final b = pop(), a = pop();
_stack.add(a - b);
case '*':
require(2);
_stack.add(pop() * pop());
case '/':
require(2);
final b = pop(), a = pop();
if (b == 0) throw Exception('Division by zero');
_stack.add(a ~/ b);
case 'dup':
require(1);
_stack.add(_stack.last);
case 'drop':
require(1);
pop();
case 'swap':
require(2);
final b = pop(), a = pop();
_stack..add(b)..add(a);
case 'over':
require(2);
_stack.add(_stack[_stack.length - 2]);
default:
throw Exception('Unknown command');
}
}
}
Let’s break down the solution:
-
_stackand_definitions- Private fields:_stack: List storing integer values (the Forth stack)_definitions: Map storing user-defined words (name → list of tokens)
-
evaluate(String input)- Entry point:- Converts input to lowercase (case-insensitive)
- Splits by whitespace using
RegExp(r'\s+') - Processes the tokens
-
_processTokens(List<String> tokens)- Main processing loop:- Iterates through tokens
- Definition handling (
:): When encountering:, finds the matching;, extracts the word name and definition, expands nested definitions, and stores it - Number handling: Uses pattern matching with
int.tryParse()to detect numbers and push them onto the stack - Word execution: Calls
_executeToken()for other tokens
-
_expandDefinition(List<String> tokens)- Expands user-defined words:- Recursively replaces user-defined words with their definitions
- This happens at definition time, not execution time (immediate expansion)
-
_executeToken(String token)- Executes a word:- User-defined words: If found in
_definitions, recursively processes its definition - Built-in words: Uses a switch statement:
- Arithmetic (
+,-,*,/): Pops operands, performs operation, pushes result - Stack operations:
dup: Duplicates top valuedrop: Removes top valueswap: Swaps top two valuesover: Copies second-to-top value
- Arithmetic (
- Error handling:
require()checks stack has enough elements; division checks for zero
- User-defined words: If found in
The solution implements a complete Forth evaluator with stack operations, arithmetic, and user-defined words, using recursive processing to handle nested definitions.
A video tutorial for this exercise is coming soon! In the meantime, check out my YouTube channel for more Dart and Flutter tutorials. 😉
Visit My YouTube Channel