creating:operators

This is an old revision of the document!


PHP's gd library is missing or unable to create PNG images

Operators & Parameters

We now rejoin Crafting Interpreters for Chapter 10. The chapter is titled Functions, but TLA⁺ actually has two related concepts to handle here: functions, and operators. It's worth taking some time to conceptually delineate them.

Functions in TLA⁺ are what in other languages are called dictionaries, maps, or associative arrays. They are values, which we'll implement in the interpreter as instances of Map<Object, Object>. They have a defined domain, and attempting to apply a function to a value outside of its domain results in a runtime error.

In contrast, TLA⁺ operators are more similar to macros: they don't have a defined domain, and the body of a TLA⁺ operator is whatever results from replacing parameter references with whatever expression was provided for them. If that replacement process results in a totally nonsensical expression, well, that's the user's fault and we raise a runtime error.

We previously implemented the ability to directly bind values to identifiers, which in this chapter will be recognized as zero-parameter operators. Functions have not yet been defined in any form, but will be later in this tutorial.

Section 10.1: Operator Calls

Section 10.1 takes us through adding calling syntax to the parser. We have two separate calling syntaxes to support: function application, which use square brackets like f[x], and operator calls, which use parentheses like op(x). Let's go over function application first.

Similar to the book, we add a new expression type in the GenerateAst class main() method, which we'll call FnApply:

    defineAst(outputDir, "Expr", Arrays.asList(
      "Binary   : Expr left, Token operator, Expr right",
      "FnApply  : Expr fn, Token bracket, Expr argument",
      "Grouping : Expr expression",

For simplicity we restrict function application to a single parameter. While the full TLA⁺ language does support constructing & calling functions with multiple parameters (underneath, bundled into a tuple), this feature is easily replicated by nesting single-parameter functions within functions - which we will support.

To support operator calls, we actually augment our existing Expr.Variable class with a list of arguments:

    defineAst(outputDir, "Expr", Arrays.asList(
      "Binary   : Expr left, Token operator, Expr right",
      "FnApply  : Expr fn, Token bracket, Expr argument",
      "Grouping : Expr expression",
      "Literal  : Object value",
      "Variable : Token name, List<Expr> arguments",
      "Unary    : Token operator, Expr expr",

While functions in TLA⁺ are values that can be passed around and constructed, operators have to be directly mentioned by name. That is why FnApply has an Expr instance to derive the function to apply, while operators use Variable which has a Token instance to record the operator name. Operators can accept multiple arguments.

To parse function application we need to splice a method into our recursive descent precedence chain. At the top of operatorExpression() in the Parser class, replace the call to primary() with a call to call():

  private Expr operatorExpression(int prec) {
    if (prec == 16) return call();
 
    Operator op;

Then, define the call() method similar to the book (differences highlighted):

  private Expr call() {
    Expr expr = primary();
 
    while (match(LEFT_BRACKET)) {
      Expr argument = expression();
      consume(RIGHT_BRACKET, "Require ']' to conclude function call");
      expr = new Expr.FnApply(expr, previous(), argument);
    }
 
    return expr;
  }

Our parsing task is simpler so we don't need a separate finishCall() method as suggested by the book. You'll note that parsing function application is strikingly similar to parsing associative postfix operators.

For operator calling, we augment our handling of identifiers in primary():

      return new Expr.Literal(previous().literal);
    }
 
    if (match(IDENTIFIER)) {
      Token identifier = previous();
      List<Expr> arguments = new ArrayList<>();
      if (match(LEFT_PAREN)) {
        do {
          arguments.add(expression());
        } while (match(COMMA));
        consume(RIGHT_PAREN, "Require ')' to conclude operator call");
      }
 
      return new Expr.Variable(identifier, arguments);
    }
 
    if (match(LEFT_PAREN)) {

This do/while loop is very similar to our existing set literal parsing code for handling comma-separated expressions, so it should be familiar to you.

  • creating/operators.1747258900.txt.gz
  • Last modified: 2025/05/14 21:41
  • by ahelwer