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/06/19 21:36] (current) – Finished probable final draft of actions tutorial 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 final PrintStream out; | ||
- | private State state = new State(); | ||
- | </ | ||
- | This requires a new class! | + | private final Map< |
- | Create a file called '' | + | |
- | < | + | private Map< |
- | package tla; | + | </code> |
- | import java.util.HashMap; | + | We choose to initialize the '' |
- | import java.util.Map; | + | The system' |
- | + | ||
- | 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 |
- | Now that we can declare variables, what value are they initialized to? | + | To resolve |
- | We've successfully dodged this issue for a very long time in this tutorial series, but unfortunately the initial value of these variables is undefined. | + | <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' | + | |
- | Alas we now depart this wonderful land. | + | |
- | Create a new class, '' | + | |
- | <code java> | + | |
- | package tla; | + | |
- | + | ||
- | record UnboundVariable(Token name, boolean primed) { | + | |
- | @Override | + | |
- | public String toString() { | + | |
- | return name.lexeme + (primed ? "'" | + | |
- | } | + | |
- | } | + | |
- | </ | + | |
- | + | ||
- | In the '' | + | |
- | <code java [highlight_lines_extra=" | + | |
- | class State { | + | |
- | private Map< | + | |
- | private Map< | + | |
- | private Map< | + | |
- | </ | + | |
- | + | ||
- | Then, in '' | + | |
- | <code java [highlight_lines_extra=" | + | |
- | variables.put(name.lexeme, | + | |
- | current.put(name.lexeme, | + | |
- | next.put(name.lexeme, | + | |
- | } | + | |
- | </ | + | |
- | + | ||
- | ===== Getting State Variables ===== | + | |
- | + | ||
- | To retrieve | + | |
- | <code java [highlight_lines_extra=" | + | |
public Object visitVariableExpr(Expr.Variable expr) { | public Object visitVariableExpr(Expr.Variable expr) { | ||
- | Object referent = getValue(expr.name); | + | Object referent = |
- | + | variables.containsKey(expr.name.lexeme) | |
- | if (referent instanceof UnboundVariable) { | + | ? |
- | | + | |
- | } | + | |
if (!(referent instanceof TlaCallable)) { | if (!(referent instanceof TlaCallable)) { | ||
</ | </ | ||
- | The new '' | + | This makes use of yet another field we must add to the '' |
- | <code java> | + | <code java [highlight_lines_extra=" |
- | private Object | + | private |
- | if (state.isDeclared(name)) { | + | |
- | return state.getValue(name); | + | |
- | } | + | |
- | return environment.get(name); | + | public Interpreter(boolean replMode) { |
- | } | + | |
</ | </ | ||
- | To define | + | The '' |
- | For this there must be some way of tracking whether we are currently | + | When inside a primed expression, variables |
- | Not just variables | + | The '' |
- | The meaning of a primed | + | We can now finally define the prime operator for real, in '' |
- | This is tracked with a boolean class variable | + | <code java [highlight_lines_extra=" |
- | <code java [highlight_lines_extra=" | + | |
- | class State { | + | |
- | | + | case PRIME: { |
- | | + | if (primed) { |
+ | throw new RuntimeError(expr.operator, | ||
+ | " | ||
+ | } try { | ||
+ | primed | ||
+ | return evaluate(expr.expr); | ||
+ | } finally { | ||
+ | primed = false; | ||
+ | } | ||
+ | } case ENABLED: { | ||
</ | </ | ||
- | Now define '' | + | Double-priming an expression or priming an expression |
- | If '' | + | 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. |
- | <code java> | + | This is why the prime operator somewhat uniquely modifies the state of the '' |
- | Object getValue(Token name) { | + | Similar to in '' |
- | return (primed ? next : current).get(name.lexeme); | + | |
- | } | + | |
- | </ | + | |
===== Setting State Variables ===== | ===== Setting State Variables ===== | ||
We can retrieve state variable values, but what about assigning them? | We can retrieve state variable values, but what about assigning them? | ||
- | To start, let's handle the simple case of expressions of the form '' | + | To start, let's handle the simple case of expressions of the form '' |
This requires modifying the '' | This requires modifying the '' | ||
If the '' | If the '' | ||
<code java [highlight_lines_extra=" | <code java [highlight_lines_extra=" | ||
case EQUAL: | case EQUAL: | ||
- | if (left instanceof UnboundVariable) { | + | if (left instanceof |
UnboundVariable var = (UnboundVariable)left; | UnboundVariable var = (UnboundVariable)left; | ||
- | | + | |
return true; | return true; | ||
} | } | ||
Line 231: | Line 208: | ||
</ | </ | ||
- | Define | + | We only modify variable values in '' |
+ | |||
+ | Although we won't yet use it, now is also a good time to define our '' | ||
<code java> | <code java> | ||
- | void bindValue(UnboundVariable var, Object value) { | + | void step(Token location) { |
- | (var.primed() ? next : current).put(var.name().lexeme, value); | + | |
+ | throw new RuntimeError(location, | ||
+ | " | ||
+ | } | ||
+ | |||
+ | primed = false; | ||
+ | current = next; | ||
+ | clearNext(); | ||
} | } | ||
- | </ | ||
- | ===== The Prime Operator ===== | + | private void isComplete() { |
+ | return !variables.isEmpty() && next.values().stream() | ||
+ | .noneMatch(v -> v instanceof UnboundVariable); | ||
+ | } | ||
- | We now have everything necessary to define the prime operator. | + | private void clearNext() { |
- | Here's how it should work: in '' | + | next = new HashMap<>(); |
- | <code java> | + | |
- | public Object visitUnaryExpr(Expr.Unary expr) { | + | |
- | | + | |
- | | + | |
- | try { | + | |
- | return evaluate(expr.expr); | + | |
- | } finally { | + | |
- | state.unprime(); | + | |
- | } | + | |
} | } | ||
+ | } | ||
</ | </ | ||
- | Note that we wrap the evaluation in a '' | ||
- | We need to put the code at the top of '' | + | '' |
- | Let's define | + | |
+ | 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 | ||
+ | <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> | <code java> | ||
- | | + | |
- | if (primed) { | + | |
- | throw new RuntimeError(op, | + | |
- | } | + | |
- | primed = true; | ||
} | } | ||
+ | </ | ||
- | void unprime() { | + | '' |
- | | + | - 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< | ||
+ | | ||
+ | clearNext(); | ||
+ | possibleNext.add(new HashMap<> | ||
+ | try { | ||
+ | while (!possibleNext.isEmpty()) { | ||
+ | |||
+ | } | ||
+ | } finally { | ||
+ | clearNext(); | ||
+ | } | ||
+ | |||
+ | return new ArrayList<> | ||
} | } | ||
</ | </ | ||
- | Double-priming | + | 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 | ||
+ | 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⁺ | ||
+ | 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 ===== | ===== 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) { | ||
Line 298: | Line 446: | ||
if (left instanceof UnboundVariable) { | if (left instanceof UnboundVariable) { | ||
UnboundVariable var = (UnboundVariable)left; | UnboundVariable var = (UnboundVariable)left; | ||
- | | + | |
- | | + | |
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 460: | ||
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) { | ||
+ | tryStep((Stmt.Print)statements.get(0)); | ||
+ | } 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.setNextState(nextState); | ||
+ | interpreter.step(action.location); | ||
+ | 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, in '' | ||
+ | <code java> | ||
+ | void setNextState(Map< | ||
+ | this.next = next; | ||
+ | } | ||
+ | </ | ||
+ | |||
+ | The other, in '' | ||
+ | If there are multiple possible next states, | ||
+ | <code java> | ||
+ | private static Map< | ||
+ | if (nextStates.size() == 1) { | ||
+ | return nextStates.get(0); | ||
+ | } else { | ||
+ | System.out.println(" | ||
+ | for (int i = 0; i < nextStates.size(); i++) { | ||
+ | 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: | ||
+ | |||
+ | [[creating: | ||