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/06/19 21:36] (current) – Finished probable final draft of actions tutorial 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 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, | ||
+ | " | ||
+ | } 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 '' | ||
+ | |||
+ | ===== Setting State Variables ===== | ||
+ | |||
+ | 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 State.UnboundVariable) { | ||
+ | UnboundVariable var = (UnboundVariable)left; | ||
+ | next.put(var.name().lexeme, | ||
+ | return true; | ||
+ | } | ||
+ | return left.equals(right); | ||
+ | </ | ||
+ | |||
+ | We only modify variable values in '' | ||
+ | |||
+ | Although we won't yet use it, now is also a good time to define our '' | ||
+ | <code java> | ||
+ | void step(Token location) { | ||
+ | if (!isComplete()) { | ||
+ | throw new RuntimeError(location, | ||
+ | " | ||
+ | } | ||
+ | |||
+ | primed = false; | ||
+ | current = next; | ||
+ | clearNext(); | ||
+ | } | ||
+ | |||
+ | private void isComplete() { | ||
+ | return !variables.isEmpty() && next.values().stream() | ||
+ | .noneMatch(v -> v instanceof UnboundVariable); | ||
+ | } | ||
+ | |||
+ | private void clearNext() { | ||
+ | next = new HashMap<> | ||
+ | for (Token variable : variables.values()) { | ||
+ | next.put(variable.lexeme, | ||
+ | } | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | '' | ||
+ | |||
+ | Now we see things taking shape: '' | ||
+ | The system' | ||
+ | Then, '' | ||
+ | The values of variables 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) { | ||
+ | </ | ||
+ | |||
+ | We also update '' | ||
+ | <code java [highlight_lines_extra=" | ||
+ | private void clearNext() { | ||
+ | possibleNext = new HashSet<> | ||
+ | next = new HashMap<> | ||
+ | </ | ||
+ | |||
+ | 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 '' | ||
+ | |||
+ | Now we can write the central loop: | ||
+ | <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 '' | ||
+ | |||
+ | 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 State.UnboundVariable) { | ||
+ | UnboundVariable var = (UnboundVariable)left; | ||
+ | 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) { | ||
+ | UnboundVariable var = (UnboundVariable)operand; | ||
+ | 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) { | ||
+ | UnboundVariable var = (UnboundVariable)left; | ||
+ | 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) { | ||
+ | tryStep((Stmt.Print)statements.get(0)); | ||
+ | } 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.setNextState(nextState); | ||
+ | interpreter.step(action.location); | ||
+ | System.out.println(true); | ||
+ | System.out.println(nextState); | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | We also print out '' | ||
+ | This called two methods; one, in '' | ||
+ | <code java> | ||
+ | void setNextState(Map< | ||
+ | this.next = next; | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | 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:// | ||
+ | See you in the [[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. | + | |