ffff
A tiny stack-based esoteric language in Rust, where every command is spelled with f, u and a.
Overview
ffff is a tiny stack-based esoteric language. A program is a list of words such as fufufafa and ff, separated by spaces or new lines. It runs from the command line with ffff FILE, or from Rust code through one run function.
Problem
I wanted to understand how an interpreter works from end to end: reading source, checking it, and running it. A real language is too big to finish while learning.
A small, silly language kept the rules short, so all of my attention went to the interpreter itself.
Requirements
- Reject a bad program before any of it runs
- Report every error with its cause, and its position when there is one
- Never crash on bad input or bad arithmetic
- Work as a library as well as a command-line tool
Architecture
The source is split on whitespace into tokens. The parser turns each token into an instruction and resolves every loop start to its matching end, producing a validated program. The interpreter runs that program on a stack of 64-bit integers and reads and writes through any input and output streams it is given.
+--------+ tokens +---------+ program +-------------+
| source | ---------> | parser | ----------> | interpreter |
| text | | + loop | | (i64 stack) |
+--------+ | jumps | +-------------+
+---------+ | ^
output | | input
v |
any Write any BufReadTechnology choices
- Rust
- Enums and pattern matching fit an instruction set naturally.
- thiserror
- Clear, typed errors with readable messages for each failure.
Implementation
The parser keeps a stack of open loop starts. When it reaches a loop end, it pops the matching start and stores the jump target in both instructions, so the interpreter never searches for a match while running.
- Arithmetic uses checked operations, so overflow and division by zero become errors.
- A value is checked before it prints as an ASCII character.
- An operation checks the stack before it changes anything, so a failure leaves the operands in place.
Major challenges
- Matching nested loops in one pass over the source
- Keeping the stack unchanged when an operation fails halfway
Testing
Five integration tests cover the language: the hello example from end to end, loops with their precomputed jumps, the stack commands, programs the parser must reject, and runtime errors that must leave the stack untouched.
Results
- 13 commands, including loops and number input
- One crate that works as a command-line tool and as a library
- Every error names its cause, and parse errors name the token position
What I'd improve
- Add an interactive mode for trying commands one at a time
- Show the line and column of an error, not only the token number