Differences
This shows you the differences between two versions of the page.
| Next revision | Previous revision | ||
| creating:actions [2025/06/06 18:54] – Parsing & interpreting variable declarations ahelwer | creating:actions [2025/11/03 23:08] (current) – One more instanceof-cast ahelwer | ||
|---|---|---|---|
| Line 31: | Line 31: | ||
| What this means is that we only support certain types of expressions incorporating primed variables, so we restrict ourselves to only model-checking TLA⁺ specs written in a particular idiom. | What this means is that we only support certain types of expressions incorporating primed variables, so we restrict ourselves to only model-checking TLA⁺ specs written in a particular idiom. | ||
| - | ====== Variables and Priming ====== | ||
| Let's start by supporting a simple operation on primed variables, expressions of the form '' | Let's start by supporting a simple operation on primed variables, expressions of the form '' | ||
| There is a fair bit of groundwork necessary to reach that point. | There is a fair bit of groundwork necessary to reach that point. | ||
| - | ===== Variable Declarations | + | ===== Declaring State Variables |
| First off we have to add support for variable declarations; | First off we have to add support for variable declarations; | ||
| Create a new statement type for them in the '' | Create a new statement type for them in the '' | ||
| Line 45: | Line 45: | ||
| )); | )); | ||
| </ | </ | ||
| + | |||
| Then use the old '' | Then use the old '' | ||
| <code java [highlight_lines_extra=" | <code java [highlight_lines_extra=" | ||
| Line 62: | Line 63: | ||
| Before we can define a '' | Before we can define a '' | ||
| - | It's simplest to copy the '' | + | The interpreter must keep track of only two states: |
| - | <code java [highlight_lines_extra=" | + | Each state is a mapping from strings - representing variables - to values. |
| + | The interpreter also must keep track of the set of declared variables. | ||
| + | For this we add three new fields near the top of the '' | ||
| + | <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; | ||
| - | private State state = new State(); | ||
| - | </ | ||
| - | |||
| - | This requires a new class! | ||
| - | Create a file called '' | ||
| - | <code java> | ||
| - | package tla; | ||
| - | |||
| - | import java.util.HashMap; | ||
| - | import java.util.Map; | ||
| - | class State { | + | |
| - | | + | private Map< |
| + | private Map< | ||
| + | </ | ||
| - | boolean isDeclared(Token name) { | + | We choose to initialize the '' |
| - | return variables.containsKey(name.lexeme); | + | The system' |
| - | } | + | |
| - | + | ||
| - | void declareVariable(Token name) { | + | |
| - | if (isDeclared(name)) { | + | |
| - | throw new RuntimeError(name, " | + | |
| - | } | + | |
| - | + | ||
| - | variables.put(name.lexeme, | + | |
| - | } | + | |
| - | } | + | |
| - | </ | + | |
| - | That' | + | The new fields are enough to implement '' |
| <code java> | <code java> | ||
| @Override | @Override | ||
| Line 103: | Line 87: | ||
| checkNotDefined(stmt.names); | checkNotDefined(stmt.names); | ||
| for (Token name : stmt.names) { | for (Token name : stmt.names) { | ||
| - | | + | |
| + | next.put(name.lexeme, | ||
| } | } | ||
| Line 110: | Line 95: | ||
| </ | </ | ||
| - | In intepreter | + | This requires a new datatype! |
| - | Add this check to the '' | + | What is the value of a variable when it is declared but not yet bound to a value? |
| - | <code java [highlight_lines_extra=" | + | It must be a new null type, which we will call '' |
| + | Add '' | ||
| + | <code java [highlight_lines_extra=" | ||
| + | class Interpreter implements Expr.Visitor< | ||
| + | | ||
| + | |||
| + | private record UnboundVariable(Token name) { | ||
| + | @Override | ||
| + | public String toString() { | ||
| + | return name.lexeme; | ||
| + | } | ||
| + | } | ||
| + | |||
| + | final Environment globals; | ||
| + | </ | ||
| + | |||
| + | It's important to keep variable declarations & operator definitions disjoint. | ||
| + | In interpreter | ||
| + | Add the highlighted code to '' | ||
| + | <code java [highlight_lines_extra=" | ||
| public Void visitOpDefStmt(Stmt.OpDef stmt) { | public Void visitOpDefStmt(Stmt.OpDef stmt) { | ||
| - | if (state.isDeclared(stmt.name)) { | + | |
| + | for (Token param : stmt.params) { | ||
| + | | ||
| + | throw new RuntimeError(param, | ||
| + | } | ||
| + | } | ||
| + | |||
| + | if (variables.containsKey(stmt.name.lexeme)) { | ||
| throw new RuntimeError(stmt.name, | throw new RuntimeError(stmt.name, | ||
| } | } | ||
| TlaOperator op = new TlaOperator(stmt); | TlaOperator op = new TlaOperator(stmt); | ||
| - | environment.define(stmt.name, | ||
| </ | </ | ||
| + | |||
| Also augment the '' | Also augment the '' | ||
| <code java [highlight_lines_extra=" | <code java [highlight_lines_extra=" | ||
| Line 129: | Line 140: | ||
| } | } | ||
| - | if (state.isDeclared(name)) { | + | if (variables.containsKey(name.lexeme)) { |
| throw new RuntimeError(name, | throw new RuntimeError(name, | ||
| } | } | ||
| Line 135: | Line 146: | ||
| </ | </ | ||
| - | ===== Variable | + | ===== Reading, Priming, & Setting State Variables ===== |
| + | |||
| + | To resolve variable values, modify the ever-more-complicated '' | ||
| + | <code java [highlight_lines_extra=" | ||
| + | public Object visitVariableExpr(Expr.Variable | ||
| + | Object referent = | ||
| + | variables.containsKey(expr.name.lexeme) | ||
| + | ? (primed ? next : current).get(expr.name.lexeme) | ||
| + | : environment.get(expr.name); | ||
| + | |||
| + | if (!(referent instanceof TlaCallable)) { | ||
| + | </ | ||
| + | |||
| + | This makes use of yet another field we must add to the '' | ||
| + | <code java [highlight_lines_extra=" | ||
| + | private Map< | ||
| + | private boolean primed = true; | ||
| + | |||
| + | public Interpreter(boolean replMode) { | ||
| + | </ | ||
| + | |||
| + | The '' | ||
| + | When inside a primed expression, variables are pulled from '' | ||
| + | The '' | ||
| + | We can now finally define the prime operator for real, in '' | ||
| + | <code java [highlight_lines_extra=" | ||
| + | public Object visitUnaryExpr(Expr.Unary expr) { | ||
| + | switch (expr.operator.type) { | ||
| + | case PRIME: { | ||
| + | if (primed) { | ||
| + | throw new RuntimeError(expr.operator, | ||
| + | current == null | ||
| + | ? " | ||
| + | : " | ||
| + | } try { | ||
| + | primed = true; | ||
| + | return evaluate(expr.expr); | ||
| + | } finally { | ||
| + | primed = false; | ||
| + | } | ||
| + | } case ENABLED: { | ||
| + | </ | ||
| + | |||
| + | Double-priming an expression or priming an expression in the initial state are both fatal errors in TLA⁺. | ||
| + | The meaning of priming an expression is that all state variables within that expression are primed, so the way the expression is evaluated is fundamentally changed. | ||
| + | This is why the prime operator somewhat uniquely modifies the state of the '' | ||
| + | Similar to in '' | ||
| + | |||
| + | We can retrieve state variable values, but what about assigning them? | ||
| + | To start, let's handle the simple case of expressions of the form '' | ||
| + | This requires modifying the '' | ||
| + | If the '' | ||
| + | <code java [highlight_lines_extra=" | ||
| + | case EQUAL: | ||
| + | if (left instanceof UnboundVariable var) { | ||
| + | next.put(var.name().lexeme, | ||
| + | return true; | ||
| + | } | ||
| + | return left.equals(right); | ||
| + | </ | ||
| + | |||
| + | We only modify variable values in '' | ||
| + | |||
| + | ===== Nondeterminism ===== | ||
| + | |||
| + | In TLA⁺, states generally do not have only one successor state. | ||
| + | Instead, there are usually multiple possible states to transition to. | ||
| + | This is called nondeterminism, | ||
| + | Nondeterminism is expressed in TLA⁺ through the use of disjunction; | ||
| + | <code haskell> | ||
| + | Action == | ||
| + | \/ x' = 0 | ||
| + | \/ x' = 1 | ||
| + | </ | ||
| + | The '' | ||
| + | This means '' | ||
| + | |||
| + | Currently, we store the next state in a single field in the '' | ||
| + | How can we represent & compute with multiple possible next states? | ||
| + | The answer is to add another field to the '' | ||
| + | <code java [highlight_lines_extra=" | ||
| + | private Map< | ||
| + | private Map< | ||
| + | private Set< | ||
| + | private boolean primed = true; | ||
| + | |||
| + | public Interpreter(boolean replMode) { | ||
| + | </ | ||
| + | |||
| + | When our interpreter finds multiple possible next states, it tosses them into '' | ||
| + | Then some top-level loop churns through the '' | ||
| + | We will write this very critical top-level loop now; create a new method '' | ||
| + | <code java> | ||
| + | List< | ||
| + | |||
| + | } | ||
| + | </ | ||
| + | |||
| + | '' | ||
| + | - Clear the '' | ||
| + | - While states remain in '' | ||
| + | - If '' | ||
| + | - Evaluating '' | ||
| + | - Return the set of confirmed next states | ||
| + | |||
| + | Here's how we set up everything outside of the central loop: | ||
| + | <code java [highlight_lines_extra=" | ||
| + | List< | ||
| + | Set< | ||
| + | clearNext(); | ||
| + | possibleNext.add(new HashMap<> | ||
| + | try { | ||
| + | while (!possibleNext.isEmpty()) { | ||
| + | |||
| + | } | ||
| + | } finally { | ||
| + | clearNext(); | ||
| + | } | ||
| + | |||
| + | return new ArrayList<> | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | We use the familiar '' | ||
| + | We also use the '' | ||
| + | For more on this peculiar issue, consult the Java '' | ||
| + | |||
| + | This code also requires a new '' | ||
| + | <code java> | ||
| + | private void clearNext() { | ||
| + | possibleNext = new HashSet<> | ||
| + | next = new HashMap<> | ||
| + | for (Token variable : variables.values()) { | ||
| + | next.put(variable.lexeme, | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | Now we can write the central '' | ||
| + | <code java [highlight_lines_extra=" | ||
| + | while (!possibleNext.isEmpty()) { | ||
| + | Map< | ||
| + | next = new HashMap<> | ||
| + | Object satisfied = evaluate(action); | ||
| + | checkBooleanOperand(location, | ||
| + | if ((boolean)satisfied && isComplete()) confirmedNext.add(next); | ||
| + | possibleNext.remove(trunk); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | Note again the judicious use of '' | ||
| + | This code requires yet another '' | ||
| + | <code java> | ||
| + | private boolean isComplete() { | ||
| + | return !variables.isEmpty() && next.values().stream() | ||
| + | .noneMatch(v -> v instanceof UnboundVariable); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | It's worth spending some time considering the '' | ||
| + | From a pure functional perspective the approach is quite displeasing; | ||
| + | This side-effectful method makes it quite a bit harder to reason about the current state of our interpreter, | ||
| + | |||
| + | There is also the question of termination. | ||
| + | What is preventing '' | ||
| + | The answer is that we only add a state to '' | ||
| + | Thus the number of variables bound to concrete values in '' | ||
| + | |||
| + | Let's get concrete. | ||
| + | How do we add states to '' | ||
| + | For each disjunct, we use a fresh copy of the '' | ||
| + | As described above, this state will either already be in '' | ||
| + | We need to evaluate every disjunct to ensure we find all possible next states, which is why TLA⁺ disjunction does not short-circuit. | ||
| + | Modify the '' | ||
| + | <code java [highlight_lines_extra=" | ||
| + | case OR: | ||
| + | boolean result = false; | ||
| + | Map< | ||
| + | for (Expr disjunct : expr.parameters) { | ||
| + | next = new HashMap<> | ||
| + | Object junctResult = evaluate(disjunct); | ||
| + | checkBooleanOperand(expr.operator, | ||
| + | possibleNext.add(new HashMap<> | ||
| + | result |= (boolean)junctResult; | ||
| + | } | ||
| + | return result; | ||
| + | default: | ||
| + | </ | ||
| + | |||
| + | There are two other operators to support for binding state variables: the set membership '' | ||
| + | Actions like '' | ||
| + | Add the following code to the '' | ||
| + | <code java [highlight_lines_extra=" | ||
| + | case IN: | ||
| + | checkSetOperand(expr.operator, | ||
| + | if (left instanceof UnboundVariable var) { | ||
| + | Map< | ||
| + | for (Object element : (Set<?> | ||
| + | next = new HashMap<> | ||
| + | next.put(var.name().lexeme, | ||
| + | left = element; | ||
| + | possibleNext.add(new HashMap<> | ||
| + | } | ||
| + | return true; | ||
| + | } | ||
| + | return ((Set<?> | ||
| + | </ | ||
| + | |||
| + | Then update the '' | ||
| + | <code java [highlight_lines_extra=" | ||
| + | } case EXISTS: { | ||
| + | boolean result = false; | ||
| + | State current = state; | ||
| + | Map< | ||
| + | for (Environment binding : bindings) { | ||
| + | next = new HashMap<> | ||
| + | Object junctResult = executeBlock(expr.body, | ||
| + | checkBooleanOperand(expr.op, | ||
| + | possibleNext.add(new HashMap<> | ||
| + | result |= (boolean)junctResult; | ||
| + | } | ||
| + | return result; | ||
| + | </ | ||
| + | |||
| + | We can now generate all nondeterministic next states by calling '' | ||
| + | |||
| + | ===== The ENABLED Operator ===== | ||
| + | |||
| + | Now that we've written '' | ||
| + | An expression '' | ||
| + | So, as simple as a quick call to '' | ||
| + | Well, not quite - the '' | ||
| + | This means we need some extra ceremony to stash the current values of '' | ||
| + | Add the highlighted code to the '' | ||
| + | <code java [highlight_lines_extra=" | ||
| + | } case ENABLED: { | ||
| + | Map< | ||
| + | Set< | ||
| + | try { | ||
| + | clearNext(); | ||
| + | return !getNextStates(expr.operator, | ||
| + | } finally { | ||
| + | next = oldNext; | ||
| + | possibleNext = oldPossibleNext; | ||
| + | } | ||
| + | } case NOT: { | ||
| + | </ | ||
| + | |||
| + | This is a big milestone: we now have fully defined // | ||
| + | We aren't at the end yet, but it's well within sight. | ||
| + | |||
| + | ===== Error Checking ===== | ||
| + | |||
| + | Our code is nice & simple, but unfortunately it has more holes than Swiss cheese. | ||
| + | Let's add some error checks! | ||
| + | |||
| + | Null value types like '' | ||
| + | Thankfully many null checks are already handled by our existing stable of '' | ||
| + | Near the bottom of the '' | ||
| + | <code java> | ||
| + | private void checkIsValue(Object... operands) { | ||
| + | for (Object operand : operands) { | ||
| + | if (operand instanceof UnboundVariable var) { | ||
| + | throw new RuntimeError(var.name(), | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | In our '' | ||
| + | Also, two values can only be compared for equality if they are both defined: | ||
| + | <code java [highlight_lines_extra=" | ||
| + | case EQUAL: | ||
| + | if (left instanceof UnboundVariable var) { | ||
| + | checkIsValue(right); | ||
| + | next.put(var.name().lexeme, | ||
| + | return true; | ||
| + | } | ||
| + | checkIsValue(left, | ||
| + | return left.equals(right); | ||
| + | </ | ||
| + | |||
| + | A null check must also must be added to the '' | ||
| + | <code java [highlight_lines_extra=" | ||
| + | case LEFT_BRACE: | ||
| + | Set< | ||
| + | for (Expr parameter : expr.parameters) { | ||
| + | Object value = evaluate(parameter); | ||
| + | checkIsValue(value); | ||
| + | set.add(value); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | 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, | ||
| + | checkIsValue(value); | ||
| + | function.put(binding.get(param), | ||
| + | } | ||
| + | return function; | ||
| + | </ | ||
| + | |||
| + | And also the '' | ||
| + | <code java [highlight_lines_extra=" | ||
| + | public Object visitFnApplyExpr(Expr.FnApply expr) { | ||
| + | Object callee = evaluate(expr.fn); | ||
| + | checkFunctionOperand(expr.bracket, | ||
| + | Object argument = evaluate(expr.argument); | ||
| + | checkIsValue(argument); | ||
| + | Map<?, ?> function = (Map<?, ?> | ||
| + | </ | ||
| + | |||
| + | ===== Hooking Up the Interpreter ===== | ||
| + | |||
| + | It takes a bit of thought to consider how we should expose our new state-stepping capabilities through the REPL. | ||
| + | One decent approach is to hijack '' | ||
| + | For this, add the following code to the bottom of the '' | ||
| + | <code java [highlight_lines_extra=" | ||
| + | // Stop if there was a syntax error. | ||
| + | if (hadError) return; | ||
| + | |||
| + | if (replMode && statements.size() == 1 | ||
| + | && statements.get(0) instanceof Stmt.Print action) { | ||
| + | tryStep(action); | ||
| + | } else { | ||
| + | interpreter.interpret(statements); | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | This calls a method we'll define in '' | ||
| + | The '' | ||
| + | However, if '' | ||
| + | If this generates no new states, again treat '' | ||
| + | <code java> | ||
| + | private static void tryStep(Stmt.Print action) { | ||
| + | Object result = interpreter.executeBlock(action.expression, | ||
| + | if (!(result instanceof Boolean)) { | ||
| + | action.accept(interpreter); | ||
| + | return; | ||
| + | } | ||
| + | |||
| + | List< | ||
| + | interpreter.getNextStates(action.location, | ||
| + | if (nextStates.isEmpty()) { | ||
| + | action.accept(interpreter); | ||
| + | return; | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | The '' | ||
| + | <code java [highlight_lines_extra=" | ||
| + | import java.util.List; | ||
| + | import java.util.Map; | ||
| + | |||
| + | public class TlaPlus { | ||
| + | </ | ||
| + | |||
| + | At this point all that remains is to handle the case of what happens when the '' | ||
| + | The REPL should choose one next state and make '' | ||
| + | <code java [highlight_lines_extra=" | ||
| + | List< | ||
| + | interpreter.getNextStates(action.location, | ||
| + | if (nextStates.isEmpty()) { | ||
| + | System.out.println(false); | ||
| + | return; | ||
| + | } | ||
| + | |||
| + | Map< | ||
| + | interpreter.goToState(nextState); | ||
| + | System.out.println(true); | ||
| + | System.out.println(nextState); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | We also print out '' | ||
| + | This called two methods; one, '' | ||
| + | <code java> | ||
| + | void goToState(Map< | ||
| + | current = state; | ||
| + | primed = state == null; | ||
| + | clearNext(); | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | The other, in '' | ||
| + | If there are multiple possible next states, the user is prompted to choose one: | ||
| + | <code java> | ||
| + | private static Map< | ||
| + | if (nextStates.size() | ||
| + | return nextStates.get(0); | ||
| + | } else { | ||
| + | System.out.println(" | ||
| + | for (int i = 0; i < nextStates.size(); | ||
| + | System.out.println(i + ": " + nextStates.get(i)); | ||
| + | } | ||
| + | |||
| + | System.out.print("> | ||
| + | try (java.util.Scanner in = new java.util.Scanner(System.in)) { | ||
| + | return nextStates.get(in.nextInt()); | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | And that's it! | ||
| + | You can now run the REPL and step through the state space! | ||
| + | Try entering expressions like the following: | ||
| + | - '' | ||
| + | - '' | ||
| + | - '' | ||
| + | |||
| + | This was a pivotal chapter; excellent work making it to the end. | ||
| + | You can find the expected state of the codebase in [[https:// | ||
| + | Now we can put it all together and build a real model checker in [[creating: | ||
| - | Now that we can declare variables, what value are they initialized to? | + | [[creating: |
| - | We've successfully dodged this issue for a very long time, but unfortunately the initial value of these variables is undefined. | + | |
| - | That means we will need a null value type. | + | |
| - | We've avoided null types thus far, which means we haven' | + | |
| - | Alas we now depart this wonderful land. | + | |