Stoop
Stoop is a series of parsers and interpreters that incrementally build a simple programming language with syntax (and semantics) that closely resemble Scala 3. Stoop is intended for educational purposes. The languages are simple, and the implementations are slow.
The languages follow the progression in Shriram Krishnamurthi's book Programming Languages: Application and Interpretation (PLAI):
-
Arithmetic
, the first language, works with simple integer arithmetic, with the operators+
,-
,/
,*
and parenthesized expressions. -
Expression
adds boolean values and logical operators, inequalities, and equality. It adds boolean literalstrue
andfalse
, and operators>
,<
,>=
,<=
,==
,&&
and||
. -
Conditional
adds conditional (if
) expressions. The syntax is the same as Scala 3:if <expr> then <expr> else <expr>
. Unlike in Scala, theelse
branch is mandatory. -
Local
adds local definitions, using the syntaxlet <name> = <expr> in <expr>
. There is no simple equivalent to this construct in Scala 3, so we use the same syntax as OCaml. -
Function
adds functions (lambda expressions) and function application. The syntax for functions is the same as Scala ((<param>) => <expr>
) except functions can only have a single parameter and there are mandatory parentheses around the parameter. Function application is the usualf(x)
syntax.
The rest of this document gives guidance for students who want to implement interpreters for these languages. It starts by describing the structure of the project and the code, then gives some advice on tackling the project, and finishes with some recommended extensions for those who want more challenge.
Project Structure
The project structure is a bit more complicated than you may be used to. The sbt project (in build.sbt
) defines several sub-projects:
core
, which contains the code you'll work with;docs
, which contains this documentation; and- the
root
project, which aggregates and depends oncore
.
From your point of view, the only relevant detail is that the code you'll work with is all within the core
directory. So core/src/main/scala
for code, and core/src/test/scala
for tests.
You don't need to change into the core
sub-project within sbt. Any commands you run on the default root
project (such as compile
, or test
) will also run on the core
sub-project.
You may want to use the build
sbt command, which in addition to compiling and testing the code, will format it, add missing copyright headers, and more. It's not necessary to use this command, but you might be interested in how professional code is developed; this is the same command used in all the Creative Scala projects to perform basic checks on the code.
Code Structure
The implementation is divided into parsers and interpreters. Each lives in their own package: the parsers in the parse
package and the interpreters in the eval
package. (An evaluator is another word for an interpreter.)
Parsers
The parsers take String
input, such as "1 + 2"
and turn it into an algebraic data type that we can manipulate in code. Here's an example doing just that.
import stoop.parse.*
Arithmetic.parser.parse("1 + 2")
// res0: Result[String, Expr] = Success(
// x = Add(l = Integer(value = 1), r = Integer(value = 2))
// )
In the world of programming languages this representation is usually called an abstract syntax tree.
You'll notice that there is a parser for each language described above. The parsers become more complex as the languages increase in complexity, but you don't need to worry about how they work. There are two things you do need to know:
-
They all define an
Expr
algebraic data type to represent programs. The definition ofExpr
is a little bit more complicated than you may be used to, but the end result is just an algebraic data type. -
They all contain an object called
parser
that has a methodparse
that takes aString
input and produces anExpr
(wrapped in aResult
; parsing can fail).
See the Parser
trait for a little bit more detail.
Here are some examples, showing the parsers for different languages.
Conditional.parser.parse("1 < 2")
// res1: Result[String, Expr] = Success(
// x = Lt(l = Integer(value = 1), r = Integer(value = 2))
// )
Function.parser.parse("(x) => x + 2")
// res2: Result[String, Expr] = Success(
// x = Lambda(
// param = "x",
// body = Add(l = Name(name = "x"), r = Integer(value = 2))
// )
// )
If the syntax is incorrect, the output offers some useful hints.
Arithmetic.parser.parse("1 ? 2")
// res3: Result[String, Expr] = Failure(
// = """(line 1, column 3):
// unexpected "?"
// expected "*", "+", "-", "/", or end of input
// >1 ? 2
// ^"""
// )
If you want to explore extending the parsers, they are built using the Parsley Scala library.
Interpreters
The intention is that you will implement the interpreters. They should all implement the Interpreter
trait. Doing so will allow your implementations to work with the tests we've created.
The Interpreter
trait only requires you implement one method: eval
. It takes an input of type Program
(which will the Expr
type from whichever parser you are using), and will produce a result of type Value
. What concrete type you should use for Value
? It depends on the language you're implementing. For Arithmetic
you can use Int
. For the later languages until Function
, we can use Scala 3's union types to return Int | Boolean
. For Function
we need add a representation of functions to the Value
type.
Challenges
Your mission, should you choose to accept it, is to implement interpreters for each parser. To guide you, you have the following:
- the text above;
- the test cases; and
- PLAI.
We strongly recommend you read PLAI as you work through the implementations. PLAI has many insights about the design space of programming languages. While learning how to implement simple interpreters is useful, it's learning about programming languages that is likely to be the most important knowledge you'll get from this exercise. If you fail to read PLAI you'll miss out on much of this.
If you really get stuck, you can find solutions on the solution
branch.
Where Next?
If you finish the five interpreters and want to take things further, here are some suggestions:
-
Read further in PLAI, and create new languages that extend
Function
with the feature you encounter. PLAI discusses objects, types, macros, and more. -
PLAI lists several books you may read next, to go further in programming language theory.
-
You might want to focus on implementation techniques, for which there are two paths you could choose. The first path is to continue working with interpreters, in which case Crafting Interpreters is a good starting point. The first part of that book covers the implementation technique we're used: a tree-walking interpreter. The second part looks at more efficient bytecode interpreters. For a different approach, see the Graal Truffle framework. The other path is to implement a compiler. Here Jeremy Siek's book, Essential of Compilation, is a good introduction. PDFs are available from his homepage or you can buy a printed copy.