Differences
This shows you the differences between two versions of the page.
| Both sides previous revision Previous revision Next revision | Previous revision | ||
| creating:operators [2025/05/20 18:31] – Parameterized operator evaluation ahelwer | creating:operators [2025/06/19 14:50] (current) – ALL_MAP_TO interpretation splits out Object value = ahelwer | ||
|---|---|---|---|
| Line 1: | Line 1: | ||
| - | ======= Operators | + | ======= |
| We now rejoin //Crafting Interpreters// | We now rejoin //Crafting Interpreters// | ||
| Line 101: | Line 101: | ||
| In [[https:// | In [[https:// | ||
| + | First, add some imports to the top of '' | ||
| + | <code java [highlight_lines_extra=" | ||
| + | import java.util.Set; | ||
| + | import java.util.HashSet; | ||
| + | import java.util.ArrayList; | ||
| + | import java.util.List; | ||
| + | import java.util.Map; | ||
| + | import java.util.HashMap; | ||
| + | </ | ||
| - | First let's implement | + | Implement |
| <code java> | <code java> | ||
| @Override | @Override | ||
| Line 232: | Line 241: | ||
| We've already implemented all our native TLA⁺ functions in the // | We've already implemented all our native TLA⁺ functions in the // | ||
| Due to the '' | Due to the '' | ||
| - | <code java [highlight_lines_extra=" | + | <code java [highlight_lines_extra=" |
| class Interpreter implements Expr.Visitor< | class Interpreter implements Expr.Visitor< | ||
| | | ||
| final Environment globals; | final Environment globals; | ||
| private Environment environment; | private Environment environment; | ||
| - | private final PrintStream out; | ||
| - | public Interpreter(PrintStream out, boolean replMode) { | + | public Interpreter(boolean replMode) { |
| this.globals = new Environment(replMode); | this.globals = new Environment(replMode); | ||
| this.environment = this.globals; | this.environment = this.globals; | ||
| - | this.out = out; | ||
| } | } | ||
| </ | </ | ||
| Line 290: | Line 297: | ||
| <code java [highlight_lines_extra=" | <code java [highlight_lines_extra=" | ||
| defineAst(outputDir, | defineAst(outputDir, | ||
| - | " | + | " |
| " | " | ||
| )); | )); | ||
| Line 297: | Line 304: | ||
| We can now parse operators with parameters! | We can now parse operators with parameters! | ||
| On to functions. | On to functions. | ||
| - | This requires defining a new expression type in the '' | + | This requires defining a new expression type in the '' |
| <code java [highlight_lines_extra=" | <code java [highlight_lines_extra=" | ||
| defineAst(outputDir, | defineAst(outputDir, | ||
| Line 431: | Line 438: | ||
| return null; | return null; | ||
| } | } | ||
| + | </ | ||
| + | |||
| + | Additional TLA⁺-specific validation is necessary here. | ||
| + | While we allow redefining operators // | ||
| + | So, if an operator of name '' | ||
| + | We should also disallow operator definitions with duplicate parameter names like '' | ||
| + | Define a new helper for this near the bottom of the '' | ||
| + | <code java> | ||
| + | private void checkNotDefined(List< | ||
| + | for (Token name : names) { | ||
| + | if (environment.isDefined(name)) { | ||
| + | throw new RuntimeError(name, | ||
| + | } | ||
| + | } | ||
| + | |||
| + | for (int i = 0; i < names.size() - 1; i++) { | ||
| + | for (int j = i + 1; j < names.size(); | ||
| + | if (names.get(i).lexeme.equals(names.get(j).lexeme)) { | ||
| + | throw new RuntimeError(names.get(i), | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | This helper requires a new method in the '' | ||
| + | <code java> | ||
| + | boolean isDefined(Token name) { | ||
| + | return values.containsKey(name.lexeme) | ||
| + | || (enclosing != null && enclosing.isDefined(name)); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | Add a '' | ||
| + | <code java [highlight_lines_extra=" | ||
| + | public Void visitOpDefStmt(Stmt.OpDef stmt) { | ||
| + | checkNotDefined(stmt.params); | ||
| + | for (Token param : stmt.params) { | ||
| + | if (param.lexeme.equals(stmt.name.lexeme)) { | ||
| + | throw new RuntimeError(param, | ||
| + | } | ||
| + | } | ||
| + | |||
| + | TlaOperator op = new TlaOperator(stmt); | ||
| </ | </ | ||
| And that takes care of operators! | And that takes care of operators! | ||
| + | Now to handle functions. | ||
| + | We still need to implement our '' | ||
| + | Similar to other operator evaluation methods like '' | ||
| + | We also ensure the function parameter names are not shadowing an existing identifier: | ||
| + | <code java> | ||
| + | @Override | ||
| + | public Object visitQuantFnExpr(Expr.QuantFn expr) { | ||
| + | checkNotDefined(expr.params); | ||
| + | Object set = evaluate(expr.set); | ||
| + | checkSetOperand(expr.op, | ||
| + | switch (expr.op.type) { | ||
| + | case ALL_MAP_TO: { | ||
| + | |||
| + | } case FOR_ALL: { | ||
| + | |||
| + | } case EXISTS: { | ||
| + | |||
| + | } default: { | ||
| + | // Unreachable. | ||
| + | return null; | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | Thinking about it, the code to implement these operators is fairly tricky. | ||
| + | We want to generate a series of bindings of set elements to identifiers. | ||
| + | Let's fill out the '' | ||
| + | <code java [highlight_lines_extra=" | ||
| + | } case FOR_ALL: { | ||
| + | for (Environment binding : bindings) { | ||
| + | Object result = executeBlock(expr.body, | ||
| + | checkBooleanOperand(expr.op, | ||
| + | if (!(Boolean)result) return false; | ||
| + | } | ||
| + | return true; | ||
| + | } case EXISTS: { | ||
| + | </ | ||
| + | This is nice, concise code. | ||
| + | We iterate over a series of bindings - in the form of '' | ||
| + | But what is '' | ||
| + | This is one of the more algorithmically tricky parts of this entire tutorial series, but it is also very fun! | ||
| + | We are going to encapsulate the binding generation logic in a new class file, '' | ||
| + | <code java> | ||
| + | package tla; | ||
| + | |||
| + | import java.util.ArrayList; | ||
| + | import java.util.Iterator; | ||
| + | import java.util.List; | ||
| + | import java.util.Set; | ||
| + | |||
| + | class BindingGenerator implements Iterator< | ||
| + | Iterable< | ||
| + | private final List< | ||
| + | private final List< | ||
| + | private final Environment parent; | ||
| + | |||
| + | BindingGenerator(List< | ||
| + | this.vars = vars; | ||
| + | this.set = new ArrayList<> | ||
| + | this.parent = parent; | ||
| + | } | ||
| + | |||
| + | @Override | ||
| + | public Iterator< | ||
| + | return this; | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | Implementing the '' | ||
| + | So that's what '' | ||
| + | A '' | ||
| + | |||
| + | Before getting too far into the weeds with the '' | ||
| + | Construct a '' | ||
| + | <code java [highlight_lines_extra=" | ||
| + | public Object visitQuantFnExpr(Expr.QuantFn expr) { | ||
| + | Object set = evaluate(expr.set); | ||
| + | checkSetOperand(expr.op, | ||
| + | BindingGenerator bindings = new BindingGenerator(expr.params, | ||
| + | switch (expr.op.type) { | ||
| + | </ | ||
| + | One important thing to note is that unlike in '' | ||
| + | <code haskell> | ||
| + | op(x) == \A y \in 0 .. 2 : y < x | ||
| + | </ | ||
| + | Here's how we implement the '' | ||
| + | <code java [highlight_lines_extra=" | ||
| + | } case EXISTS: { | ||
| + | boolean result = false; | ||
| + | for (Environment binding : bindings) { | ||
| + | Object junctResult = executeBlock(expr.body, | ||
| + | checkBooleanOperand(expr.op, | ||
| + | result |= (boolean)junctResult; | ||
| + | } | ||
| + | return result; | ||
| + | } default: { | ||
| + | </ | ||
| + | Finally, here's how we implement the '' | ||
| + | <code java [highlight_lines_extra=" | ||
| + | case ALL_MAP_TO: { | ||
| + | Token param = expr.params.get(0); | ||
| + | Map< | ||
| + | for (Environment binding : bindings) { | ||
| + | Object value = executeBlock(expr.body, | ||
| + | function.put(binding.get(param), | ||
| + | } | ||
| + | return function; | ||
| + | } case FOR_ALL: { | ||
| + | </ | ||
| + | |||
| + | ====== Set Enumeration ====== | ||
| + | |||
| + | Now that we've finished all that, let's get to the real difficult part: implementing an enumeration algorithm in '' | ||
| + | A moment of consideration is in order. | ||
| + | If you had to bind a set of variables to every combination of values from a set, how would you do it? | ||
| + | To computer scientists, the connection between enumerating every possible combination of values and incrementing numbers in an arbitrary base is readily made. | ||
| + | For example, imagine you had a set of three values '' | ||
| + | This is isomorphic to iterating through all possible values of a two-digit number in base three. | ||
| + | The number of digits corresponds to the number of variables to bind, and the base corresponds to the number of elements in the set. | ||
| + | |||
| + | This is confusing, but we will walk through it step by step. | ||
| + | First we must assign an arbitrary order to the set so we can map single digits in base three to an element of the set: | ||
| + | ^ Digit ^ Element ^ | ||
| + | | 0 | a | | ||
| + | | 1 | b | | ||
| + | | 2 | c | | ||
| + | |||
| + | Using that mapping, here's a table showing how counting up through all two-digit base three numbers decomposes into binding all possible combinations of values to '' | ||
| + | ^ Number (base 3) ^ As set elements ^ x (0th-significant digit) ^ y (1st-significant digit) ^ | ||
| + | | 00 | aa | a | a | | ||
| + | | 01 | ab | b | a | | ||
| + | | 02 | ac | c | a | | ||
| + | | 10 | ba | a | b | | ||
| + | | 11 | bb | b | b | | ||
| + | | 12 | bc | c | b | | ||
| + | | 20 | ca | a | c | | ||
| + | | 21 | cb | b | c | | ||
| + | | 22 | cc | c | c | | ||
| + | |||
| + | As you can see, we generated every possible binding of '' | ||
| + | Each identifier is assigned a particular digit in the number, and their value in a binding is given by that digit' | ||
| + | |||
| + | We're almost there, but not quite; Java doesn' | ||
| + | We will need to increment a regular '' | ||
| + | Actually the well-known [[https:// | ||
| + | It is quite elegant; given a number '' | ||
| + | Then, assign '' | ||
| + | Confused? Here's how it looks as a table for converting the enumeration value 6 to a binding from our previous base 3 example: | ||
| + | ^ Iteration ^ '' | ||
| + | | 0 | 6 | 0 | 2 | | ||
| + | | 1 | 2 | 2 | 0 | | ||
| + | |||
| + | Note that the value of '' | ||
| + | Reading the '' | ||
| + | Consulting our tables, we see " | ||
| + | '' | ||
| + | '' | ||
| + | |||
| + | The iteration number actually corresponds to digit significance. | ||
| + | So, in iteration 0, '' | ||
| + | In iteration 1, '' | ||
| + | If this is still too abstract, try some other values to convince yourself that our algorithm successfully calculates the binding! | ||
| + | |||
| + | Now to implement it. | ||
| + | First, add another field to the '' | ||
| + | <code java [highlight_lines_extra=" | ||
| + | class BindingGenerator implements Iterator< | ||
| + | Iterable< | ||
| + | private final List< | ||
| + | private final List< | ||
| + | private final Environment parent; | ||
| + | private int enumerationIndex = 0; | ||
| + | </ | ||
| + | |||
| + | Then, implement '' | ||
| + | This is required by the '' | ||
| + | In our case, we test that '' | ||
| + | For '' | ||
| + | <code java> | ||
| + | @Override | ||
| + | public boolean hasNext() { | ||
| + | return enumerationIndex < Math.pow(set.size(), | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | Now for the main event. | ||
| + | Here's the strikingly simple code implementing the change-of-base algorithm in the '' | ||
| + | <code java> | ||
| + | @Override | ||
| + | public Environment next() { | ||
| + | int current = enumerationIndex++; | ||
| + | Environment bindings = new Environment(parent); | ||
| + | for (Token var : vars) { | ||
| + | bindings.define(var, | ||
| + | current /= set.size(); | ||
| + | } | ||
| + | |||
| + | return bindings; | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | Note that '' | ||
| + | A nice little terse code trick. | ||
| + | |||
| + | And we're done! | ||
| + | You can now run your interpreter and enjoy the full power of functions & operators with parameters, along with the new universal & existential quantification expressions! | ||
| + | See the current expected state of your source code [[https:// | ||
| + | On to the next chapter: [[creating: | ||
| + | |||
| + | ====== Challenges ====== | ||
| + | |||
| + | - In the full TLA⁺ language, quantified functions can be even more complicated; | ||
| + | - Modify the change-of-base algorithm to handle enumerating multiple sets of varying cardinality, | ||
| + | - Our enumeration algorithm is a " | ||
| + | - Often, sets in TLA⁺ models are very large. Instead of immediately evaluating sets constructed with the '' | ||
| + | |||
| + | [[creating: | ||
| + | |||