====== Handling Constant TLA⁺ Expressions ======
This tutorial page covers the next three chapters in //Crafting Interpreters//:
- [[https://craftinginterpreters.com/representing-code.html|Chapter 5: Representing Code]]
- [[https://craftinginterpreters.com/parsing-expressions.html|Chapter 6: Parsing Expressions]]
- [[https://craftinginterpreters.com/evaluating-expressions.html|Chapter 7: Evaluating Expressions]]
Same as the book, we //could// build a parser for our entire minimal TLA⁺ language subset before moving on to interpreting it, but that would be boring!
Instead we focus on a simple vertical slice of the language: expressions.
And not just any expressions, constant expressions - expressions that do not contain variables or identifiers that we would have to resolve.
Just primitive literal values stuck together, not too much more advanced than a simple calculator app.
This will give us a skeleton on which to hang the rest of the language.
Each section in this tutorial corresponds to one or more sections of //Crafting Interpreters// (henceforth referred to as "the book").
First read the section of the book, then read the corresponding commentary & modifications given by this tutorial.
===== Chapter 5: Representing Code =====
[[https://craftinginterpreters.com/representing-code.html|Chapter 5]] focuses more on concepts than code, so this section will not have many TLA⁺-specific modifications.
However, since this tutorial is intended to be self-contained code-wise, all necessary code is reproduced.
==== Section 5.1: Context-Free Grammars ====
Everything before [[https://craftinginterpreters.com/representing-code.html#a-grammar-for-lox-expressions|Section 5.1.3: A Grammar for Lox expressions]] applies equally to TLA⁺.
The ambiguous first-draft grammar for Lox expressions can be adapted to TLA⁺:
expression → literal
| unary
| binary
| ternary
| set
| grouping ;
literal → NUMBER | "TRUE" | "FALSE" ;
grouping → "(" expression ")" ;
unary → (( "ENABLED" | "-" ) expression ) | ( expression "'" ) ;
binary → expression operator expression ;
ternary → "IF" expression "THEN" expression "ELSE" expression;
set → "{" ( expression ( "," expression )* )? "}"
operator → "=" | "+" | "-" | ".." | "/\" | "\/" | "<" | "\in" ;
There are a few interesting differences.
The ''unary'' rule now captures both prefix //and// suffix operators, which both only accept a single parameter.
The ''ternary'' rule matches the ''IF''/''THEN''/''ELSE'' operator, with the three parameters being the predicate, the true branch, and the false branch.
There is also the ''set'' rule matching finite set literals like ''{1, 2, 3}'' or the empty set ''{}''.
The operators are changed to the set of operators defined in our TLA⁺ implementation.
These are all the expressions we can use without introducing the concept of identifiers referring to something else.
==== Section 5.2: Implementing Syntax Trees ====
While some programmers might have an aversion to generating code, the approach taken in the book is actually very convenient - as you will discover if you take some time to prototype & play around with different class representations of the parse tree!
In [[https://craftinginterpreters.com/representing-code.html#metaprogramming-the-trees|Section 5.2.2: Metaprogramming the trees]], the main differences in the ''GenerateAst'' class reflect our modification of the Lox grammar in the previous section:
package com.craftinginterpreters.tool;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.List;
public class GenerateAst {
public static void main(String[] args) throws IOException {
if (args.length != 1) {
System.err.println("Usage: generate_ast
In the ''defineAst'' function, we only need to modify the output code so that it uses the package ''.tla'' instead of ''.lox''; add this after the ''main'' method of ''GenerateAst'':
private static void defineAst(
String outputDir, String baseName, List types)
throws IOException {
String path = outputDir + "/" + baseName + ".java";
PrintWriter writer = new PrintWriter(path, "UTF-8");
writer.println("package com.craftinginterpreters.tla;");
writer.println();
writer.println("import java.util.List;");
writer.println();
writer.println("abstract class " + baseName + " {");
// The AST classes.
for (String type : types) {
String className = type.split(":")[0].trim();
String fields = type.split(":")[1].trim();
defineType(writer, baseName, className, fields);
}
writer.println("}");
writer.close();
}
Finally, the ''defineType'' method - added after the ''defineAst'' method - is unchanged:
private static void defineType(
PrintWriter writer, String baseName,
String className, String fieldList) {
writer.println(" static class " + className + " extends " +
baseName + " {");
// Constructor.
writer.println(" " + className + "(" + fieldList + ") {");
// Store parameters in fields.
String[] fields = fieldList.split(", ");
for (String field : fields) {
String name = field.split(" ")[1];
writer.println(" this." + name + " = " + name + ";");
}
writer.println(" }");
// Fields.
writer.println();
for (String field : fields) {
writer.println(" final " + field + ";");
}
writer.println(" }");
}
==== Section 5.3: Working with Trees ====
[[https://craftinginterpreters.com/representing-code.html#working-with-trees|Section 5.3]] introduces the Visitor pattern.
No TLA⁺-specific differences are necessary when modifying ''GenerateAst'' to support it.
Insert this line in ''defineAst()'':
writer.println("abstract class " + baseName + " {");
defineVisitor(writer, baseName, types);
// The AST classes.
This calls the ''defineVisitor'' function which writes the visitor interface, defined as follows:
private static void defineVisitor(
PrintWriter writer, String baseName, List types) {
writer.println(" interface Visitor {");
for (String type : types) {
String typeName = type.split(":")[0].trim();
writer.println(" R visit" + typeName + baseName + "(" +
typeName + " " + baseName.toLowerCase() + ");");
}
writer.println(" }");
}
Again insert some lines in ''defineAst()'' to create the abstract ''accept'' method:
defineType(writer, baseName, className, fields);
}
// The base accept() method.
writer.println();
writer.println(" abstract R accept(Visitor visitor);");
writer.println("}");
Finally, insert some lines in ''defineType'' to add a types-specific ''accept'' method in each output class:
writer.println(" }");
// Visitor pattern.
writer.println();
writer.println(" @Override");
writer.println(" R accept(Visitor visitor) {");
writer.println(" return visitor.visit" +
className + baseName + "(this);");
writer.println(" }");
// Fields.
Our generated syntax tree node types now support the visitor pattern!
==== Section 5.4: A (Not Very) Pretty Printer ====
[[https://craftinginterpreters.com/representing-code.html#a-not-very-pretty-printer|Section 5.4]] provides an implementation of a visitor called ''AstPrinter'' that prints out the parse tree.
There are a few TLA⁺-specific modifications, starting of course with the package name:
package com.craftinginterpreters.tla;
class AstPrinter implements Expr.Visitor {
String print(Expr expr) {
return expr.accept(this);
}
}
We also have some modifications and additions to the ''visit'' methods of the ''AstPrinter'' class, again reflecting our modified TLA⁺-specific grammar:
@Override
public String visitBinaryExpr(Expr.Binary expr) {
return parenthesize(expr.operator.lexeme,
expr.left, expr.right);
}
@Override
public String visitGroupingExpr(Expr.Grouping expr) {
return parenthesize("group", expr.expression);
}
@Override
public String visitLiteralExpr(Expr.Literal expr) {
if (expr.value == null) return "nil";
return expr.value.toString();
}
@Override
public String visitUnaryExpr(Expr.Unary expr) {
return parenthesize(expr.operator.lexeme, expr.expr);
}
@Override
public string visitTernaryExpr(Expr.Ternary expr) {
return parenthesize(expr.operator.lexeme, expr.first,
expr.second, expr.third);
}
@Override
public string visitSetExpr(Expr.Set expr) {
return parenthesize("Set", expr.elements.toArray(Expr[]::new));
}
The ''parenthesize'' method is unchanged from the book and should be inserted after the ''visit'' methods:
private String parenthesize(String name, Expr... exprs) {
StringBuilder builder = new StringBuilder();
builder.append("(").append(name);
for (Expr expr : exprs) {
builder.append(" ");
builder.append(expr.accept(this));
}
builder.append(")");
return builder.toString();
}
It isn't necessary to define a ''main'' method in the ''AstPrinter'' class, but if you'd like to try it out you are free to copy the one given in the book.