From d7c9a23df952e03372b5b0e2be1b6d9c286ec3fd Mon Sep 17 00:00:00 2001
From: dgelessus <dgelessus@users.noreply.github.com>
Date: Mon, 8 Nov 2021 15:37:45 +0100
Subject: [PATCH] Fix local variables in Event-B mode

Event-B/Rodin does not support LET like ProB does, so we need to use
some workarounds to provide similar functionality in Event-B mode.
---
 CHANGELOG.md                                  |   1 +
 notebooks/tests/let.ipynb                     | 171 ++++++++++++++++++
 .../java/de/prob2/jupyter/CommandUtils.java   |  27 ++-
 .../java/de/prob2/jupyter/ProBKernel.java     |  83 ++++++++-
 .../prob2/jupyter/commands/AssertCommand.java |   4 +-
 .../prob2/jupyter/commands/EvalCommand.java   |   4 +-
 .../de/prob2/jupyter/commands/LetCommand.java |   6 +-
 .../prob2/jupyter/commands/SolveCommand.java  |   2 +-
 8 files changed, 291 insertions(+), 7 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1ca4148..205ba9b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,7 @@
 	* This is an internal change and should not affect any user-visible behavior. That is, all inputs that were accepted by previous versions should still be accepted - if any previously valid inputs are no longer accepted, this is a bug.
 	* As a side effect, the inspection and code completion features now work better in a few edge cases.
 * Fixed a bug where interrupting a command could make the kernel completely stop responding, requiring a manual restart.
+* Fixed syntax errors when using local variables (`:let`) in Event-B mode.
 * Fixed the `:trace` command sometimes displaying transitions as `null`.
 
 ## [1.2.0](https://www3.hhu.de/stups/downloads/prob2-jupyter/prob2-jupyter-kernel-1.2.0-all.jar)
diff --git a/notebooks/tests/let.ipynb b/notebooks/tests/let.ipynb
index 5db7904..1b4533e 100644
--- a/notebooks/tests/let.ipynb
+++ b/notebooks/tests/let.ipynb
@@ -355,6 +355,177 @@
    "source": [
     "hello"
    ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Local variables can be used in Event-B mode."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 16,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "Changed language for user input to Event-B (forced)"
+      ]
+     },
+     "execution_count": 16,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    ":language event_b"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 17,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/markdown": [
+       "$\\{1,2,3,4,5,10\\}$"
+      ],
+      "text/plain": [
+       "{1,2,3,4,5,10}"
+      ]
+     },
+     "execution_count": 17,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "1..5 \\/ {10}"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 18,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/markdown": [
+       "$\\{1,2,3,4,5,10\\}$"
+      ],
+      "text/plain": [
+       "{1,2,3,4,5,10}"
+      ]
+     },
+     "execution_count": 18,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    ":let hello 1..5 \\/ {10}"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 19,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/markdown": [
+       "$\\{1,2,3,4,5,10\\}$"
+      ],
+      "text/plain": [
+       "{1,2,3,4,5,10}"
+      ]
+     },
+     "execution_count": 19,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "hello"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 20,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/markdown": [
+       "$\\mathit{FALSE}$"
+      ],
+      "text/plain": [
+       "FALSE"
+      ]
+     },
+     "execution_count": 20,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "hello = {}"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 21,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/markdown": [
+       "$\\mathit{TRUE}$"
+      ],
+      "text/plain": [
+       "TRUE"
+      ]
+     },
+     "execution_count": 21,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "hello /= {}"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 22,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/markdown": [
+       "$\\mathit{TRUE}$\n",
+       "\n",
+       "**Solution:**\n",
+       "* $\\mathit{A} = \\{0,1\\}$"
+      ],
+      "text/plain": [
+       "TRUE\n",
+       "\n",
+       "Solution:\n",
+       "\tA = {0,1}"
+      ]
+     },
+     "execution_count": 22,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "card(A) = 2 & hello /= A"
+   ]
   }
  ],
  "metadata": {
diff --git a/src/main/java/de/prob2/jupyter/CommandUtils.java b/src/main/java/de/prob2/jupyter/CommandUtils.java
index 659e297..450b68b 100644
--- a/src/main/java/de/prob2/jupyter/CommandUtils.java
+++ b/src/main/java/de/prob2/jupyter/CommandUtils.java
@@ -47,6 +47,7 @@ public final class CommandUtils {
 	private static final @NotNull Pattern BODY_SPLIT_PATTERN = Pattern.compile("\\n");
 	public static final @NotNull Pattern ARG_SPLIT_PATTERN = Pattern.compile("\\s+");
 	private static final @NotNull Pattern B_IDENTIFIER_PATTERN = Pattern.compile("[A-Za-z][A-Za-z0-9_]*");
+	public static final @NotNull String JUPYTER_RESULT_VARIABLE_NAME = "__jUpYtEr_rEsUlT__";
 	
 	private CommandUtils() {
 		super();
@@ -219,7 +220,7 @@ public final class CommandUtils {
 		withSourceCode(formula.getCode(), action);
 	}
 	
-	public static @NotNull String insertLetVariables(final @NotNull String code, final @NotNull Map<@NotNull String, @NotNull String> variables) {
+	public static @NotNull String insertClassicalBLetVariables(final @NotNull String code, final @NotNull Map<@NotNull String, @NotNull String> variables) {
 		if (variables.isEmpty()) {
 			return code;
 		} else {
@@ -230,6 +231,30 @@ public final class CommandUtils {
 		}
 	}
 	
+	public static @NotNull String insertEventBPredicateLetVariables(final @NotNull String predicate, final @NotNull Map<@NotNull String, @NotNull String> variables) {
+		if (variables.isEmpty()) {
+			return predicate;
+		} else {
+			final String varNames = String.join(",", variables.keySet());
+			final PredicateBuilder varAssignments = new PredicateBuilder();
+			varAssignments.addMap(variables);
+			varAssignments.add(predicate);
+			return String.format("#%s.(\n%s\n)", varNames, varAssignments);
+		}
+	}
+	
+	public static @NotNull String insertEventBExpressionLetVariables(final @NotNull String expression, final @NotNull Map<@NotNull String, @NotNull String> variables) {
+		if (variables.isEmpty()) {
+			return expression;
+		} else {
+			final String varNames = String.join(",", variables.keySet());
+			final PredicateBuilder varAssignments = new PredicateBuilder();
+			varAssignments.addMap(variables);
+			varAssignments.add(JUPYTER_RESULT_VARIABLE_NAME, expression);
+			return String.format("#%s.(\n%s\n)", varNames, varAssignments);
+		}
+	}
+	
 	public static @NotNull DisplayData displayDataForEvalResult(final @NotNull AbstractEvalResult aer) {
 		final StringBuilder sb = new StringBuilder();
 		final StringBuilder sbMarkdown = new StringBuilder();
diff --git a/src/main/java/de/prob2/jupyter/ProBKernel.java b/src/main/java/de/prob2/jupyter/ProBKernel.java
index bbfbf75..d564bc9 100644
--- a/src/main/java/de/prob2/jupyter/ProBKernel.java
+++ b/src/main/java/de/prob2/jupyter/ProBKernel.java
@@ -34,12 +34,16 @@ import com.google.inject.Injector;
 import com.google.inject.Singleton;
 
 import de.prob.animator.ReusableAnimator;
+import de.prob.animator.domainobjects.AbstractEvalResult;
 import de.prob.animator.domainobjects.ClassicalB;
 import de.prob.animator.domainobjects.ErrorItem;
+import de.prob.animator.domainobjects.EvalElementType;
+import de.prob.animator.domainobjects.EvalResult;
 import de.prob.animator.domainobjects.EventB;
 import de.prob.animator.domainobjects.FormulaExpand;
 import de.prob.animator.domainobjects.IEvalElement;
 import de.prob.exception.ProBError;
+import de.prob.model.eventb.EventBModel;
 import de.prob.scripting.ClassicalBFactory;
 import de.prob.statespace.AnimationSelector;
 import de.prob.statespace.StateSpace;
@@ -344,6 +348,17 @@ public final class ProBKernel extends BaseKernel {
 		this.setCurrentMachineDirectory(machineDirectory);
 	}
 	
+	/**
+	 * Return whether user-entered formulas are currently being parsed using Event-B syntax.
+	 * This is true if the user has explicitly enabled Event-B mode,
+	 * or when the default mode is used while an Event-B model is loaded.
+	 * 
+	 * @return whether formulas are currently parsed as Event-B
+	 */
+	public boolean isEventBMode() {
+		return this.getCurrentFormulaLanguage() == FormulaLanguage.EVENT_B || this.animationSelector.getCurrentTrace().getModel() instanceof EventBModel;
+	}
+	
 	/**
 	 * Parse the given formula code into an {@link IEvalElement}.
 	 * The language used for parsing depends on the current formula language (see {@link #getCurrentFormulaLanguage()}.
@@ -381,7 +396,33 @@ public final class ProBKernel extends BaseKernel {
 	 * @return the parsed formula
 	 */
 	public IEvalElement parseFormula(final String code, final FormulaExpand expand) {
-		return this.parseFormulaWithoutLetVariables(CommandUtils.insertLetVariables(code, this.getVariables()), expand);
+		if (this.getVariables().isEmpty()) {
+			// Shortcut: if no local variables are defined,
+			// the code doesn't need to be changed at all.
+			return this.parseFormulaWithoutLetVariables(code, expand);
+		} else if (this.isEventBMode()) {
+			// Implementing local variables in Event-B mode is not straightforward,
+			// because Rodin does not support LET expressions/predicates
+			// like ProB does for classical B.
+			// For predicates,
+			// we can use an existential quantifier in place of LET.
+			// For expressions this is not possible,
+			// so as a workaround we construct a predicate with a new free variable equal to the expression.
+			// This helper predicate is unpacked again after evaluation.
+			final String predicateCode = CommandUtils.insertEventBPredicateLetVariables(code, this.getVariables());
+			final EventB predicate = (EventB)this.parseFormulaWithoutLetVariables(predicateCode, FormulaExpand.TRUNCATE);
+			if (predicate.getKind() == EvalElementType.PREDICATE) {
+				return predicate;
+			} else {
+				final String expressionAsPredicateCode = CommandUtils.insertEventBExpressionLetVariables(code, this.getVariables());
+				return this.parseFormulaWithoutLetVariables(expressionAsPredicateCode, FormulaExpand.TRUNCATE);
+			}
+		} else {
+			// Assume that we are using classical B syntax.
+			// This is the case for most other supported languages
+			// (the main exception is CSP).
+			return this.parseFormulaWithoutLetVariables(CommandUtils.insertClassicalBLetVariables(code, this.getVariables()), expand);
+		}
 	}
 	
 	public @NotNull DisplayData executeOperation(final @NotNull String name, final @Nullable String predicate) {
@@ -402,6 +443,46 @@ public final class ProBKernel extends BaseKernel {
 		return new DisplayData(String.format("Executed operation: %s", op.getPrettyRep()));
 	}
 	
+	/**
+	 * Post-process an evaluation result returned by ProB.
+	 * Currently this only handles some special cases when evaluating Event-B formulas while local variables are defined.
+	 * In all other cases,
+	 * the result is returned unmodified.
+	 * 
+	 * @param aer the evaluation result to postprocess
+	 * @return the processed evaluation result
+	 */
+	public @NotNull AbstractEvalResult postprocessEvalResult(final @NotNull AbstractEvalResult aer) {
+		if (!this.getVariables().isEmpty() && this.isEventBMode() && aer instanceof EvalResult) {
+			final EvalResult result = (EvalResult)aer;
+			
+			final Map<String, String> solutionValues = new HashMap<>(result.getSolutions());
+			
+			final String resultValue;
+			if (result.getValue().equals(EvalResult.TRUE.getValue()) && result.getSolutions().containsKey(CommandUtils.JUPYTER_RESULT_VARIABLE_NAME)) {
+				// Special case for expressions rewritten to predicates by CommandUtils.insertEventBExpressionLetVariables:
+				// extract the special result variable and use it in place of the predicate result (TRUE).
+				resultValue = solutionValues.remove(CommandUtils.JUPYTER_RESULT_VARIABLE_NAME);
+			} else {
+				resultValue = result.getValue();
+			}
+			
+			// Special case for predicates or expressions
+			// that had local variables inserted using an existential quantification
+			// (by insertEventBPredicateLetVariables or insertEventBExpressionLetVariables).
+			// ProB sometimes treats variables in a top-level existential quantification
+			// as if they were free variables
+			// and returns their values as part of the solution.
+			// We don't want this for local variables,
+			// so manually remove them from the solution again.
+			solutionValues.keySet().removeAll(this.getVariables().keySet());
+			
+			return new EvalResult(resultValue, solutionValues);
+		}
+		
+		return aer;
+	}
+	
 	@Override
 	public @NotNull String getBanner() {
 		return "ProB Interactive Expression and Predicate Evaluator (on Jupyter)\nType \":help\" for more information.";
diff --git a/src/main/java/de/prob2/jupyter/commands/AssertCommand.java b/src/main/java/de/prob2/jupyter/commands/AssertCommand.java
index 1ba3378..31a2f55 100644
--- a/src/main/java/de/prob2/jupyter/commands/AssertCommand.java
+++ b/src/main/java/de/prob2/jupyter/commands/AssertCommand.java
@@ -70,7 +70,9 @@ public final class AssertCommand implements Command {
 	public @NotNull DisplayData run(final @NotNull ParsedArguments args) {
 		final ProBKernel kernel = this.kernelProvider.get();
 		final IEvalElement formula = kernel.parseFormula(args.get(FORMULA_PARAM), FormulaExpand.TRUNCATE);
-		final AbstractEvalResult result = CommandUtils.withSourceCode(formula, () -> this.animationSelector.getCurrentTrace().evalCurrent(formula));
+		final AbstractEvalResult result = CommandUtils.withSourceCode(formula, () -> 
+			kernel.postprocessEvalResult(this.animationSelector.getCurrentTrace().evalCurrent(formula))
+		);
 		if (result instanceof EvalResult && "TRUE".equals(((EvalResult)result).getValue())) {
 			// Use EvalResult.TRUE instead of the real result so that solution variables are not displayed.
 			return CommandUtils.displayDataForEvalResult(EvalResult.TRUE);
diff --git a/src/main/java/de/prob2/jupyter/commands/EvalCommand.java b/src/main/java/de/prob2/jupyter/commands/EvalCommand.java
index bc6271f..1693bf0 100644
--- a/src/main/java/de/prob2/jupyter/commands/EvalCommand.java
+++ b/src/main/java/de/prob2/jupyter/commands/EvalCommand.java
@@ -65,7 +65,9 @@ public final class EvalCommand implements Command {
 	public @NotNull DisplayData run(final @NotNull ParsedArguments args) {
 		final ProBKernel kernel = this.injector.getInstance(ProBKernel.class);
 		final IEvalElement formula = kernel.parseFormula(args.get(FORMULA_PARAM), FormulaExpand.EXPAND);
-		return CommandUtils.displayDataForEvalResult(CommandUtils.withSourceCode(formula, () -> this.animationSelector.getCurrentTrace().evalCurrent(formula)));
+		return CommandUtils.displayDataForEvalResult(CommandUtils.withSourceCode(formula, () ->
+			kernel.postprocessEvalResult(this.animationSelector.getCurrentTrace().evalCurrent(formula))
+		));
 	}
 	
 	@Override
diff --git a/src/main/java/de/prob2/jupyter/commands/LetCommand.java b/src/main/java/de/prob2/jupyter/commands/LetCommand.java
index 0d0a059..1ee993d 100644
--- a/src/main/java/de/prob2/jupyter/commands/LetCommand.java
+++ b/src/main/java/de/prob2/jupyter/commands/LetCommand.java
@@ -70,9 +70,11 @@ public final class LetCommand implements Command {
 		final String name = args.get(NAME_PARAM);
 		final ProBKernel kernel = this.kernelProvider.get();
 		final IEvalElement formula = kernel.parseFormula(args.get(EXPRESSION_PARAM), FormulaExpand.EXPAND);
-		final AbstractEvalResult evaluated = CommandUtils.withSourceCode(formula, () -> this.animationSelector.getCurrentTrace().evalCurrent(formula));
+		final AbstractEvalResult evaluated = CommandUtils.withSourceCode(formula, () ->
+			kernel.postprocessEvalResult(this.animationSelector.getCurrentTrace().evalCurrent(formula))
+		);
 		if (evaluated instanceof EvalResult) {
-			kernel.getVariables().put(name, evaluated.toString());
+			kernel.getVariables().put(name, ((EvalResult)evaluated).getValue());
 		}
 		return CommandUtils.displayDataForEvalResult(evaluated);
 	}
diff --git a/src/main/java/de/prob2/jupyter/commands/SolveCommand.java b/src/main/java/de/prob2/jupyter/commands/SolveCommand.java
index c92210b..d8e59a3 100644
--- a/src/main/java/de/prob2/jupyter/commands/SolveCommand.java
+++ b/src/main/java/de/prob2/jupyter/commands/SolveCommand.java
@@ -90,7 +90,7 @@ public final class SolveCommand implements Command {
 		
 		final CbcSolveCommand cmd = new CbcSolveCommand(predicate, solver, this.animationSelector.getCurrentTrace().getCurrentState());
 		trace.getStateSpace().execute(cmd);
-		return CommandUtils.displayDataForEvalResult(cmd.getValue());
+		return CommandUtils.displayDataForEvalResult(kernel.postprocessEvalResult(cmd.getValue()));
 	}
 	
 	@Override
-- 
GitLab