Differences
This shows you the differences between two versions of the page.
| Both sides previous revision Previous revision Next revision | Previous revision | ||
| creating:actions [2025/06/06 22:01] – Handle basic x' = expr case ahelwer | creating:actions [2025/11/03 23:08] (current) – One more instanceof-cast ahelwer | ||
|---|---|---|---|
| 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 | + | |
| + | private Map< | ||
| + | private | ||
| </ | </ | ||
| - | This requires a new class! | + | We choose to initialize the '' |
| - | Create a file called | + | The system' |
| - | <code java> | + | |
| - | package tla; | + | |
| - | + | ||
| - | import java.util.HashMap; | + | |
| - | import java.util.Map; | + | |
| - | + | ||
| - | class State { | + | |
| - | private Map< | + | |
| - | + | ||
| - | boolean isDeclared(Token name) { | + | |
| - | return variables.containsKey(name.lexeme); | + | |
| - | } | + | |
| - | + | ||
| - | 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, | ||
| </ | </ | ||
| Line 130: | Line 140: | ||
| } | } | ||
| - | if (state.isDeclared(name)) { | + | if (variables.containsKey(name.lexeme)) { |
| throw new RuntimeError(name, | throw new RuntimeError(name, | ||
| } | } | ||
| Line 136: | Line 146: | ||
| </ | </ | ||
| - | ===== Initializing | + | ===== Reading, Priming, & Setting |
| - | Now that we can declare variables, what value are they initialized to? | + | To resolve variable values, modify |
| - | We've successfully dodged this issue for a very long time in this tutorial series, but unfortunately | + | <code java [highlight_lines_extra=" |
| - | That means we will need a null value type. | + | |
| - | We've avoided null types thus far, which means we haven't had to do as many checks before using values in the interpreter - a real luxury! | + | Object referent = |
| - | Alas we now depart this wonderful land. | + | variables.containsKey(expr.name.lexeme) |
| - | Create a new class, '' | + | ? (primed ? next : current).get(expr.name.lexeme) |
| - | <code java> | + | : environment.get(expr.name); |
| - | package tla; | + | |
| - | record UnboundVariable(Token name, boolean primed) { | + | if (!(referent instanceof TlaCallable)) { |
| - | @Override | + | |
| - | public String toString() { | + | |
| - | return name.lexeme + (primed ? "'" | + | |
| - | } | + | |
| - | } | + | |
| </ | </ | ||
| - | In the '' | + | This makes use of yet another field we must add to the '' |
| - | <code java [highlight_lines_extra=" | + | <code java [highlight_lines_extra=" |
| - | class State { | + | |
| - | private Map< | + | |
| - | private Map< | + | |
| private Map< | private Map< | ||
| + | private boolean primed = true; | ||
| + | |||
| + | public Interpreter(boolean replMode) { | ||
| </ | </ | ||
| - | Then, in '' | + | The '' |
| - | <code java [highlight_lines_extra=" | + | When inside a primed expression, variables are pulled from '' |
| - | | + | The '' |
| - | | + | We can now finally define the prime operator for real, in '' |
| - | next.put(name.lexeme, new UnboundVariable(name, true)); | + | <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 | ||
| + | This is why the prime operator somewhat uniquely modifies the state of the '' | ||
| + | Similar | ||
| + | |||
| + | 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 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; for example: | ||
| + | <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< | ||
| } | } | ||
| </ | </ | ||
| - | ===== Getting State Variables ===== | + | '' |
| + | - Clear the '' | ||
| + | - While states remain in '' | ||
| + | - If '' | ||
| + | - Evaluating '' | ||
| + | - Return the set of confirmed next states | ||
| - | To retrieve variable values, modify the ever-more-complicated '' | + | Here's how we set up everything outside |
| - | <code java [highlight_lines_extra=" | + | <code java [highlight_lines_extra=" |
| - | | + | |
| - | Object | + | |
| + | clearNext(); | ||
| + | possibleNext.add(new HashMap<> | ||
| + | try { | ||
| + | while (!possibleNext.isEmpty()) { | ||
| - | if (referent instanceof UnboundVariable) | + | } |
| - | | + | } finally |
| + | | ||
| } | } | ||
| - | | + | |
| + | } | ||
| </ | </ | ||
| - | The new '' | + | 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> | <code java> | ||
| - | private | + | private |
| - | | + | |
| - | | + | next = new HashMap<> |
| + | for (Token variable : variables.values()) { | ||
| + | | ||
| } | } | ||
| - | |||
| - | return environment.get(name); | ||
| } | } | ||
| </ | </ | ||
| - | To define | + | Now we can write the central |
| - | For this there must be some way of tracking whether we are currently inside a primed expression. | + | <code java [highlight_lines_extra=" |
| - | Not just variables can be primed in TLA⁺, entire expressions can be! | + | while (!possibleNext.isEmpty()) |
| - | The meaning of a primed expression is that every state variable within that expression is primed. | + | Map< |
| - | This is tracked with a boolean class variable in '' | + | next = new HashMap<> |
| - | <code java [highlight_lines_extra=" | + | Object satisfied = evaluate(action); |
| - | class State { | + | checkBooleanOperand(location, |
| - | | + | if ((boolean)satisfied && isComplete()) confirmedNext.add(next); |
| - | private | + | possibleNext.remove(trunk); |
| + | } | ||
| </ | </ | ||
| - | Now define | + | Note again the judicious use of '' |
| - | If '' | + | This code requires yet another |
| <code java> | <code java> | ||
| - | | + | |
| - | return (primed ? next : current).get(name.lexeme); | + | return |
| + | | ||
| } | } | ||
| </ | </ | ||
| - | ===== Setting State Variables ===== | + | 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, | ||
| - | We can retrieve state variable values, but what about assigning them? | + | There is also the question |
| - | To start, let's handle | + | What is preventing |
| - | This requires modifying the ''EQUAL'' | + | The answer is that we only add a state to '' |
| - | If the '' | + | Thus the number of variables bound to concrete values in '' |
| - | <code java [highlight_lines_extra=" | + | |
| - | case EQUAL: | + | 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: | ||
| + | | ||
| + | Map< | ||
| + | for (Expr disjunct : expr.parameters) { | ||
| + | | ||
| + | | ||
| + | checkBooleanOperand(expr.operator, junctResult); | ||
| + | | ||
| + | result |= (boolean)junctResult; | ||
| } | } | ||
| - | return | + | return |
| + | default: | ||
| </ | </ | ||
| - | Define | + | There are two other operators to support for binding state variables: the set membership |
| - | <code java> | + | Actions like '' |
| - | void bindValue(UnboundVariable var, Object value) { | + | Add the following code to the '' |
| - | (var.primed() ? next : current).put(var.name().lexeme, | + | <code java [highlight_lines_extra=" |
| - | } | + | case IN: |
| + | checkSetOperand(expr.operator, | ||
| + | if (left instanceof | ||
| + | | ||
| + | for (Object element : (Set<?>)right) { | ||
| + | | ||
| + | next.put(var.name().lexeme, | ||
| + | left = element; | ||
| + | possibleNext.add(new HashMap<> | ||
| + | | ||
| + | return true; | ||
| + | } | ||
| + | return ((Set<?> | ||
| </ | </ | ||
| - | ===== The Prime Operator ===== | + | Then update |
| - | + | <code java [highlight_lines_extra=" | |
| - | We now have everything necessary to define | + | } case EXISTS: { |
| - | Here's how it should work: in '' | + | boolean result = false; |
| - | <code java> | + | State current |
| - | | + | Map<String, Object> trunk = next; |
| - | | + | |
| - | | + | next = new HashMap<> |
| - | try { | + | |
| - | return evaluate(expr.expr); | + | |
| - | } finally { | + | |
| - | state.unprime(); | + | |
| - | } | + | } |
| - | } | + | return result; |
| </ | </ | ||
| - | Note that we wrap the evaluation in a '' | ||
| - | We need to put the code at the top of '' | + | We can now generate all nondeterministic next states by calling |
| - | Let's define the '' | + | |
| - | <code java> | + | |
| - | void prime(Token op) { | + | |
| - | if (primed) { | + | |
| - | throw new RuntimeError(op, | + | |
| - | } | + | |
| - | primed | + | ===== The ENABLED Operator ===== |
| - | } | + | |
| - | void unprime() { | + | 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: | ||
| + | | ||
| + | | ||
| + | try { | ||
| + | clearNext(); | ||
| + | return !getNextStates(expr.operator, | ||
| + | | ||
| + | next = oldNext; | ||
| + | possibleNext = oldPossibleNext; | ||
| + | } | ||
| + | } case NOT: { | ||
| </ | </ | ||
| - | Double-priming an expression | + | 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 ===== | ===== Error Checking ===== | ||
| Our code is nice & simple, but unfortunately it has more holes than Swiss cheese. | Our code is nice & simple, but unfortunately it has more holes than Swiss cheese. | ||
| - | Let' | + | Let' |
| Null value types like '' | Null value types like '' | ||
| - | Thankfully many null checks are already handled by our existing stable of '' | + | Thankfully many null checks are already handled by our existing stable of '' |
| - | Near the bottom of the '' | + | Near the bottom of the '' |
| <code java> | <code java> | ||
| - | private void checkIsDefined(Object... operands) { | + | private void checkIsValue(Object... operands) { |
| for (Object operand : operands) { | for (Object operand : operands) { | ||
| - | if (operand instanceof UnboundVariable) { | + | if (operand instanceof UnboundVariable |
| - | UnboundVariable var = (UnboundVariable)operand; | + | |
| throw new RuntimeError(var.name(), | throw new RuntimeError(var.name(), | ||
| } | } | ||
| Line 294: | Line 417: | ||
| In our '' | In our '' | ||
| Also, two values can only be compared for equality if they are both defined: | Also, two values can only be compared for equality if they are both defined: | ||
| - | <code java [highlight_lines_extra=" | + | <code java [highlight_lines_extra=" |
| case EQUAL: | case EQUAL: | ||
| - | if (left instanceof UnboundVariable) { | + | if (left instanceof UnboundVariable |
| - | | + | |
| - | checkIsDefined(right); | + | |
| - | | + | |
| return true; | return true; | ||
| } | } | ||
| - | | + | |
| return left.equals(right); | return left.equals(right); | ||
| </ | </ | ||
| - | A null check also must be added to the '' | + | A null check must also must be added to the '' |
| <code java [highlight_lines_extra=" | <code java [highlight_lines_extra=" | ||
| case LEFT_BRACE: | case LEFT_BRACE: | ||
| Line 312: | Line 434: | ||
| for (Expr parameter : expr.parameters) { | for (Expr parameter : expr.parameters) { | ||
| Object value = evaluate(parameter); | Object value = evaluate(parameter); | ||
| - | | + | |
| set.add(value); | set.add(value); | ||
| } | } | ||
| </ | </ | ||
| - | First, we have to think about initial | + | The '' |
| - | Not all components | + | <code java [highlight_lines_extra=" |
| - | What if we are in the initial state and process an expression containing a primed variable? | + | case ALL_MAP_TO: { |
| - | It should | + | Token param = expr.params.get(0); |
| - | Add another field to '' | + | 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 | ||
| + | <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 | ||
| + | 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=" | <code java [highlight_lines_extra=" | ||
| - | class State { | + | import java.util.List; |
| - | private boolean initialState = true; | + | import java.util.Map; |
| - | | + | |
| + | public class TlaPlus { | ||
| </ | </ | ||
| - | In '' | + | At this point all that remains is to handle the case of what happens when the '' |
| - | <code java [highlight_lines_extra=" | + | The REPL should choose one next state and make '' |
| - | void bindValue(UnboundVariable var, Object | + | <code java [highlight_lines_extra=" |
| - | if (var.primed() && initialState) { | + | |
| - | | + | interpreter.getNextStates(action.location, |
| - | " | + | if (nextStates.isEmpty()) { |
| + | | ||
| + | | ||
| } | } | ||
| + | |||
| + | Map< | ||
| + | interpreter.goToState(nextState); | ||
| + | System.out.println(true); | ||
| + | System.out.println(nextState); | ||
| + | } | ||
| </ | </ | ||
| - | Later we'll look at how to transition beyond | + | 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() == 1) { | ||
| + | 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: | ||
| + | |||
| + | [[creating: | ||