Skip to content
Snippets Groups Projects
Select Git revision
  • 8ecd72e1fe91be39b9fdc7411dae19df6192e6fa
  • master default protected
  • exec_auto_adjust_trace
  • let_variables
  • v1.4.1
  • v1.4.0
  • v1.3.0
  • v1.2.0
  • v1.1.0
  • v1.0.0
10 results

ProBKernel.java

Blame
  • user avatar
    dgelessus authored
    This significantly improves the performance of loading new machines.
    8ecd72e1
    History
    Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    ProBKernel.java 28.17 KiB
    package de.prob2.jupyter;
    
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.io.Reader;
    import java.nio.charset.StandardCharsets;
    import java.nio.file.Path;
    import java.nio.file.Paths;
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.Collection;
    import java.util.Collections;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    import java.util.Optional;
    import java.util.Properties;
    import java.util.concurrent.atomic.AtomicReference;
    import java.util.function.Function;
    import java.util.regex.Matcher;
    import java.util.regex.Pattern;
    import java.util.stream.Collectors;
    
    import com.google.common.base.MoreObjects;
    import com.google.inject.Inject;
    import com.google.inject.Injector;
    import com.google.inject.Singleton;
    
    import de.prob.animator.ReusableAnimator;
    import de.prob.animator.domainobjects.ErrorItem;
    import de.prob.exception.ProBError;
    import de.prob.scripting.ClassicalBFactory;
    import de.prob.statespace.AnimationSelector;
    import de.prob.statespace.StateSpace;
    import de.prob.statespace.Trace;
    import de.prob2.jupyter.commands.AssertCommand;
    import de.prob2.jupyter.commands.BrowseCommand;
    import de.prob2.jupyter.commands.BsymbCommand;
    import de.prob2.jupyter.commands.CheckCommand;
    import de.prob2.jupyter.commands.ConstantsCommand;
    import de.prob2.jupyter.commands.DotCommand;
    import de.prob2.jupyter.commands.EvalCommand;
    import de.prob2.jupyter.commands.ExecCommand;
    import de.prob2.jupyter.commands.FindCommand;
    import de.prob2.jupyter.commands.GotoCommand;
    import de.prob2.jupyter.commands.GroovyCommand;
    import de.prob2.jupyter.commands.HelpCommand;
    import de.prob2.jupyter.commands.InitialiseCommand;
    import de.prob2.jupyter.commands.LetCommand;
    import de.prob2.jupyter.commands.LoadCellCommand;
    import de.prob2.jupyter.commands.LoadFileCommand;
    import de.prob2.jupyter.commands.ModelCheckCommand;
    import de.prob2.jupyter.commands.PrefCommand;
    import de.prob2.jupyter.commands.PrettyPrintCommand;
    import de.prob2.jupyter.commands.RenderCommand;
    import de.prob2.jupyter.commands.ShowCommand;
    import de.prob2.jupyter.commands.SolveCommand;
    import de.prob2.jupyter.commands.StatsCommand;
    import de.prob2.jupyter.commands.TableCommand;
    import de.prob2.jupyter.commands.TimeCommand;
    import de.prob2.jupyter.commands.TraceCommand;
    import de.prob2.jupyter.commands.TypeCommand;
    import de.prob2.jupyter.commands.UnletCommand;
    import de.prob2.jupyter.commands.VersionCommand;
    
    import io.github.spencerpark.jupyter.kernel.BaseKernel;
    import io.github.spencerpark.jupyter.kernel.LanguageInfo;
    import io.github.spencerpark.jupyter.kernel.ReplacementOptions;
    import io.github.spencerpark.jupyter.kernel.display.DisplayData;
    import io.github.spencerpark.jupyter.kernel.display.mime.MIMEType;
    
    import org.jetbrains.annotations.NotNull;
    import org.jetbrains.annotations.Nullable;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    @Singleton
    public final class ProBKernel extends BaseKernel {
    	/**
    	 * Inner class for safe lazy loading of the build info.
    	 */
    	private static final class BuildInfo {
    		static final Properties buildInfo;
    		static {
    			buildInfo = new Properties();
    			try (final Reader reader = new InputStreamReader(
    				ProBKernel.class.getResourceAsStream("/de/prob2/jupyter/build.properties"),
    				StandardCharsets.UTF_8
    			)) {
    				buildInfo.load(reader);
    			} catch (final IOException e) {
    				throw new AssertionError("Failed to load build info", e);
    			}
    		}
    	}
    	
    	private static final class SplitCommandCall {
    		private final @NotNull PositionedString name;
    		private final @NotNull PositionedString argString;
    		
    		private SplitCommandCall(@NotNull final PositionedString name, @NotNull final PositionedString argString) {
    			this.name = name;
    			this.argString = argString;
    		}
    		
    		private @NotNull PositionedString getName() {
    			return this.name;
    		}
    		
    		private @NotNull PositionedString getArgString() {
    			return this.argString;
    		}
    		
    		@Override
    		public String toString() {
    			return MoreObjects.toStringHelper(this)
    				.add("name", this.getName())
    				.add("argString", this.getArgString())
    				.toString();
    		}
    	}
    	
    	private static final @NotNull Logger LOGGER = LoggerFactory.getLogger(ProBKernel.class);
    	
    	private static final @NotNull Pattern COMMAND_PATTERN = Pattern.compile("\\s*(\\:[^\\s]*)(?:\\h*(.*))?", Pattern.DOTALL);
    	private static final @NotNull Pattern MACHINE_CODE_PATTERN = Pattern.compile("MACHINE\\W.*", Pattern.DOTALL);
    	private static final @NotNull Pattern BSYMB_COMMAND_PATTERN = Pattern.compile("\\\\([a-z]+)");
    	private static final @NotNull Pattern LATEX_FORMULA_PATTERN = Pattern.compile("(\\$\\$?)([^\\$]+)\\1");
    	
    	private static final @NotNull Collection<@NotNull Class<? extends Command>> COMMAND_CLASSES = Collections.unmodifiableList(Arrays.asList(
    		AssertCommand.class,
    		BrowseCommand.class,
    		BsymbCommand.class,
    		CheckCommand.class,
    		ConstantsCommand.class,
    		DotCommand.class,
    		EvalCommand.class,
    		ExecCommand.class,
    		FindCommand.class,
    		GotoCommand.class,
    		GroovyCommand.class,
    		HelpCommand.class,
    		InitialiseCommand.class,
    		LetCommand.class,
    		LoadCellCommand.class,
    		LoadFileCommand.class,
    		ModelCheckCommand.class,
    		PrefCommand.class,
    		PrettyPrintCommand.class,
    		RenderCommand.class,
    		ShowCommand.class,
    		SolveCommand.class,
    		StatsCommand.class,
    		TableCommand.class,
    		TimeCommand.class,
    		TraceCommand.class,
    		TypeCommand.class,
    		UnletCommand.class,
    		VersionCommand.class
    	));
    	
    	private static final @NotNull Map<@NotNull String, @NotNull String> BSYMB_COMMAND_DEFINITIONS;
    	static {
    		final Map<String, String> map = new HashMap<>();
    		map.put("bfalse", "\\newcommand{\\bfalse}{\\mathord\\bot}");
    		map.put("btrue", "\\newcommand{\\btrue}{\\mathord\\top}");
    		map.put("limp", "\\newcommand{\\limp}{\\mathbin\\Rightarrow}");
    		map.put("leqv", "\\newcommand{\\leqv}{\\mathbin\\Leftrightarrow}");
    		map.put("qdot", "\\newcommand{\\qdot}{\\mathord{\\mkern1mu\\cdot\\mkern1mu}}");
    		map.put("defi", "\\newcommand{\\defi}{\\mathrel{≙}}");
    		map.put("pow", "\\newcommand{\\pow}{\\mathop{\\mathbb P\\hbox{}}\\nolimits}");
    		map.put("pown", "\\newcommand{\\pown}{\\mathop{\\mathbb P_1}\\nolimits}");
    		map.put("cprod", "\\newcommand{\\cprod}{\\mathbin\\times}");
    		map.put("bunion", "\\newcommand{\\bunion}{\\mathbin{\\mkern1mu\\cup\\mkern1mu}}");
    		map.put("binter", "\\newcommand{\\binter}{\\mathbin{\\mkern1mu\\cap\\mkern1mu}}");
    		map.put("union", "\\newcommand{\\union}{\\mathop{\\mathrm{union}}\\nolimits}");
    		map.put("inter", "\\newcommand{\\inter}{\\mathop{\\mathrm{inter}}\\nolimits}");
    		map.put("Union", "\\newcommand{\\Union}{\\bigcup\\nolimits}");
    		map.put("Inter", "\\newcommand{\\Inter}{\\bigcap\\nolimits}");
    		map.put("emptyset", "\\renewcommand{\\emptyset}{\\mathord\\varnothing}");
    		map.put("rel", "\\newcommand{\\rel}{\\mathbin{<\\mkern-10mu-\\mkern-10mu>}}");
    		map.put("trel", "\\newcommand{\\trel}{\\mathbin{<\\mkern-6mu<\\mkern-10mu-\\mkern-10mu>}}");
    		map.put("srel", "\\newcommand{\\srel}{\\mathbin{<\\mkern-10mu-\\mkern-10mu>\\mkern-6mu>}}");
    		map.put("strel", "\\newcommand{\\strel}{\\mathbin{<\\mkern-6mu<\\mkern-10mu-\\mkern-10mu>\\mkern-6mu>}}");
    		map.put("dom", "\\newcommand{\\dom}{\\mathop{\\mathrm{dom}}\\nolimits}");
    		map.put("ran", "\\newcommand{\\ran}{\\mathop{\\mathrm{ran}}\\nolimits}");
    		map.put("fcomp", "\\newcommand{\\fcomp}{\\mathbin;}");
    		map.put("bcomp", "\\newcommand{\\bcomp}{\\circ}");
    		map.put("id", "\\newcommand{\\id}{\\mathop{\\mathrm{id}}\\nolimits}");
    		map.put("domres", "\\newcommand{\\domres}{\\mathbin◁}");
    		map.put("domsub", "\\newcommand{\\domsub}{\\mathbin⩤}");
    		map.put("ranres", "\\newcommand{\\ranres}{\\mathbin▷}");
    		map.put("ransub", "\\newcommand{\\ransub}{\\mathbin⩥}");
    		map.put("ovl", "\\newcommand{\\ovl}{\\mathbin{<\\mkern-11mu+}}");
    		map.put("dprod", "\\newcommand{\\dprod}{\\mathbin\\otimes}");
    		map.put("prjone", "\\newcommand{\\prjone}{\\mathop{\\mathrm{prj}_1}\\nolimits}");
    		map.put("prjtwo", "\\newcommand{\\prjtwo}{\\mathop{\\mathrm{prj}_2}\\nolimits}");
    		map.put("pprod", "\\newcommand{\\pprod}{\\mathbin\\mid}");
    		map.put("pfun", "\\newcommand{\\pfun}{\\mathbin↦}");
    		map.put("tfun", "\\newcommand{\\tfun}{\\mathbin→}");
    		map.put("pinj", "\\newcommand{\\pinj}{\\mathbin⤔}");
    		map.put("tinj", "\\newcommand{\\tinj}{\\mathbin↣}");
    		map.put("psur", "\\newcommand{\\psur}{\\mathbin⤅}");
    		map.put("tsur", "\\newcommand{\\tsur}{\\mathbin↠}");
    		map.put("tbij", "\\newcommand{\\tbij}{\\mathbin⤖}");
    		map.put("nat", "\\newcommand{\\nat}{\\mathord{\\mathbb N}}");
    		map.put("natn", "\\newcommand{\\natn}{\\mathord{\\mathbb N_1}}");
    		map.put("intg", "\\newcommand{\\intg}{\\mathord{\\mathbb Z}}");
    		map.put("upto", "\\newcommand{\\upto}{\\mathbin{.\\mkern1mu.}}");
    		map.put("finite", "\\newcommand{\\finite}{\\mathop{\\mathrm{finite}}\\nolimits}");
    		map.put("card", "\\newcommand{\\card}{\\mathop{\\mathrm{card}}\\nolimits}");
    		map.put("upred", "\\newcommand{\\upred}{\\mathop{\\mathrm{pred}}\\nolimits}");
    		map.put("usucc", "\\newcommand{\\usucc}{\\mathop{\\mathrm{succ}}\\nolimits}");
    		map.put("expn", "\\newcommand{\\expn}{\\mathbin{\\widehat{\\mkern1em}}}");
    		map.put("Bool", "\\newcommand{\\Bool}{\\mathord{\\mathrm{BOOL}}}");
    		map.put("bool", "\\newcommand{\\bool}{\\mathop{\\mathrm{bool}}\\nolimits}");
    		map.put("bcmeq", "\\newcommand{\\bcmeq}{\\mathrel{:\\mkern1mu=}}");
    		map.put("bcmin", "\\newcommand{\\bcmin}{\\mathrel{:\\mkern1mu\\in}}");
    		map.put("bcmsuch", "\\newcommand{\\bcmsuch}{\\mathrel{:\\mkern1mu\\mid}}");
    		BSYMB_COMMAND_DEFINITIONS = Collections.unmodifiableMap(map);
    	}
    	
    	private final @NotNull AnimationSelector animationSelector;
    	private final @NotNull ReusableAnimator animator;
    	
    	private final @NotNull Map<@NotNull String, @NotNull Command> commands;
    	private final @NotNull AtomicReference<@Nullable Thread> currentEvalThread;
    	private final @NotNull Map<@NotNull String, @NotNull String> variables;
    	
    	private @NotNull Path currentMachineDirectory;
    	
    	@Inject
    	private ProBKernel(final @NotNull Injector injector, final @NotNull ClassicalBFactory classicalBFactory, final @NotNull AnimationSelector animationSelector, final @NotNull ReusableAnimator animator) {
    		super();
    		
    		this.setShouldReplaceStdStreams(false);
    		
    		this.animationSelector = animationSelector;
    		this.animator = animator;
    		
    		this.commands = COMMAND_CLASSES.stream()
    			.map(injector::getInstance)
    			.collect(Collectors.toMap(Command::getName, cmd -> cmd));
    		
    		this.currentEvalThread = new AtomicReference<>(null);
    		this.variables = new HashMap<>();
    		
    		this.switchMachine(Paths.get(""), stateSpace -> {
    			classicalBFactory.create("(initial Jupyter machine)", "MACHINE repl END").loadIntoStateSpace(stateSpace);
    			return new Trace(stateSpace);
    		});
    		this.currentMachineDirectory = Paths.get("");
    	}
    	
    	private static @NotNull Properties getBuildInfo() {
    		return ProBKernel.BuildInfo.buildInfo;
    	}
    	
    	public static @NotNull String getVersion() {
    		return getBuildInfo().getProperty("version");
    	}
    	
    	public static @NotNull String getCommit() {
    		return getBuildInfo().getProperty("commit");
    	}
    	
    	public @NotNull Map<@NotNull String, @NotNull Command> getCommands() {
    		return Collections.unmodifiableMap(this.commands);
    	}
    	
    	public @NotNull Map<@NotNull String, @NotNull String> getVariables() {
    		return this.variables;
    	}
    	
    	public @NotNull Path getCurrentMachineDirectory() {
    		return this.currentMachineDirectory;
    	}
    	
    	public void setCurrentMachineDirectory(final @NotNull Path currentMachineDirectory) {
    		this.currentMachineDirectory = currentMachineDirectory;
    	}
    	
    	public void unloadMachine() {
    		final Trace oldTrace = this.animationSelector.getCurrentTrace();
    		if (oldTrace != null) {
    			assert oldTrace.getStateSpace() == this.animator.getCurrentStateSpace();
    			this.animationSelector.changeCurrentAnimation(null);
    			oldTrace.getStateSpace().kill();
    		}
    		this.setCurrentMachineDirectory(Paths.get(""));
    	}
    	
    	public void switchMachine(final @NotNull Path machineDirectory, final @NotNull Function<@NotNull StateSpace, @NotNull Trace> newTraceCreator) {
    		this.unloadMachine();
    		final StateSpace newStateSpace = this.animator.createStateSpace();
    		try {
    			this.animationSelector.changeCurrentAnimation(newTraceCreator.apply(newStateSpace));
    		} catch (final RuntimeException e) {
    			newStateSpace.kill();
    			throw e;
    		}
    		this.setCurrentMachineDirectory(machineDirectory);
    	}
    	
    	@Override
    	public @NotNull String getBanner() {
    		return "ProB Interactive Expression and Predicate Evaluator (on Jupyter)\nType \":help\" for more information.";
    	}
    	
    	@Override
    	public @NotNull List<LanguageInfo.@NotNull Help> getHelpLinks() {
    		return Collections.singletonList(new LanguageInfo.Help("ProB User Manual", "https://www3.hhu.de/stups/prob/index.php/User_Manual"));
    	}
    	
    	private static @NotNull String addBsymbDefinitions(final @NotNull String markdown) {
    		final StringBuilder defs = new StringBuilder();
    		final Matcher matcher = BSYMB_COMMAND_PATTERN.matcher(markdown);
    		while (matcher.find()) {
    			defs.append(BSYMB_COMMAND_DEFINITIONS.getOrDefault(matcher.group(1), ""));
    		}
    		
    		if (defs.length() > 0) {
    			// Find the first LaTeX formula in the output and add the definitions to it.
    			final Matcher latexFormulaMatcher = LATEX_FORMULA_PATTERN.matcher(markdown);
    			if (latexFormulaMatcher.find()) {
    				// We do this manually instead of using Matcher.replaceFirst to prevent backslashes from being processed.
    				return markdown.substring(0, latexFormulaMatcher.start())
    					+ latexFormulaMatcher.group(1)
    					+ defs
    					+ latexFormulaMatcher.group(2)
    					+ latexFormulaMatcher.group(1)
    					+ markdown.substring(latexFormulaMatcher.end());
    			} else {
    				// No LaTeX formula found, add an extra one at the start.
    				// This can produce an unwanted empty line at the start, so we avoid this method if possible.
    				return "$" + defs + "$\n\n" + markdown;
    			}
    		} else {
    			// No definitions needed, so don't modify the output.
    			return markdown;
    		}
    	}
    	
    	private static @NotNull String addAllBsymbDefinitions() {
    		final StringBuilder defs = new StringBuilder("$");
    		BSYMB_COMMAND_DEFINITIONS.forEach((k, v) -> {
    			defs.append(v);
    		});
    		defs.append("\\text{All bsymb.sty definitions have been loaded.}$");
    		return defs.toString();
    	}
    	
    	private @Nullable DisplayData executeCommand(final @NotNull PositionedString name, final @NotNull PositionedString argString) {
    		final Command command = this.getCommands().get(name.getValue());
    		if (command == null) {
    			throw new NoSuchCommandException(name.getValue());
    		}
    		final DisplayData result;
    		try {
    			result = command.run(CommandUtils.parseArgs(command.getParameters(), argString));
    		} catch (final UserErrorException e) {
    			throw new CommandExecutionException(name.getValue(), e);
    		}
    		
    		if (result != null && result.hasDataForType(MIMEType.TEXT_MARKDOWN)) {
    			final String markdown = (String)result.getData(MIMEType.TEXT_MARKDOWN);
    			if (command instanceof BsymbCommand) {
    				result.putMarkdown(addAllBsymbDefinitions());
    			} else {
    				// Add definitions for any used bsymb LaTeX commands to Markdown output.
    				result.putMarkdown(addBsymbDefinitions(markdown));
    			}
    		}
    		
    		return result;
    	}
    	
    	private static boolean isMachineCode(final @NotNull String code) {
    		return MACHINE_CODE_PATTERN.matcher(code).matches();
    	}
    	
    	/**
    	 * Preprocess the given input by ensuring that it starts with a command name.
    	 * If a command name is already present,
    	 * the input is returned unchanged.
    	 * Otherwise,
    	 * an appropriate command is added based on the type of input.
    	 * 
    	 * @param code the input code to preprocess
    	 * @return the input with a command name prefixed if necessary
    	 */
    	private static @NotNull PositionedString preprocessInput(final @NotNull PositionedString code) {
    		final Matcher commandMatcher = COMMAND_PATTERN.matcher(code.getValue());
    		final String prefix;
    		if (commandMatcher.matches()) {
    			// The input already includes a command, so no prefix needs to be added.
    			prefix = "";
    		} else if (isMachineCode(code.getValue())) {
    			// The input appears to be a machine, add a command to load it.
    			prefix = "::load\n";
    		} else {
    			// By default, assume that the input is an expression that should be evaluated.
    			prefix = ":eval ";
    		}
    		// Add the prefix and adjust the start position.
    		// This means that the characters from the prefix (if any) will have negative positions,
    		// but the characters from the real source code will have the same positions as before.
    		return new PositionedString(prefix + code.getValue(), code.getStartPosition() - prefix.length());
    	}
    	
    	/**
    	 * Split the given input into a command name and argument string.
    	 * The input must include a command name,
    	 * which should be ensured by passing it through {@link #preprocessInput(PositionedString)} first.
    	 * 
    	 * @param code the input, which must contain a command name
    	 * @return the input split into a command name and argument string
    	 */
    	private static @NotNull SplitCommandCall splitCommand(final @NotNull PositionedString code) {
    		final Matcher commandMatcher = COMMAND_PATTERN.matcher(code.getValue());
    		if (!commandMatcher.matches()) {
    			throw new AssertionError("Preprocessed input does not include a command - this should not happen");
    		}
    		final PositionedString name = code.substring(commandMatcher.start(1), commandMatcher.end(1));
    		final PositionedString argString;
    		if (commandMatcher.group(2) == null) {
    			argString = code.substring(code.getValue().length());
    		} else {
    			argString = code.substring(commandMatcher.start(2), commandMatcher.end(2));
    		}
    		return new SplitCommandCall(name, argString);
    	}
    	
    	private @Nullable DisplayData evalInternal(final @NotNull PositionedString code) {
    		final SplitCommandCall split = splitCommand(preprocessInput(code));
    		return this.executeCommand(split.getName(), split.getArgString());
    	}
    	
    	@Override
    	public @Nullable DisplayData eval(final String expr) {
    		assert expr != null;
    		
    		this.currentEvalThread.set(Thread.currentThread());
    		
    		try {
    			return evalInternal(new PositionedString(expr, 0));
    		} finally {
    			this.currentEvalThread.set(null);
    		}
    	}
    	
    	private static @Nullable DisplayData inspectCommandArguments(final @NotNull Command command, final @NotNull PositionedString argString, final int at) {
    		final SplitResult split = CommandUtils.splitArgs(command.getParameters(), argString, at);
    		if (split.getParameterAtPosition().isPresent()) {
    			final Optional<Inspector> inspector = command.getParameterInspectors().getInspectorForParameter(split.getParameterAtPosition().get());
    			if (inspector.isPresent()) {
    				final List<PositionedString> argsAtPosition = split.getArguments().get(split.getParameterAtPosition().get());
    				assert !argsAtPosition.isEmpty();
    				final PositionedString lastArgument = argsAtPosition.get(argsAtPosition.size() - 1);
    				return inspector.get().inspect(lastArgument.getValue(), at - lastArgument.getStartPosition());
    			} else {
    				return null;
    			}
    		} else {
    			return null;
    		}
    	}
    	
    	private @Nullable DisplayData inspectInternal(final @NotNull PositionedString code, final int at) {
    		final SplitCommandCall split = splitCommand(preprocessInput(code));
    		if (this.getCommands().containsKey(split.getName().getValue())) {
    			final Command command = this.getCommands().get(split.getName().getValue());
    			if (at <= split.getName().getEndPosition()) {
    				// The cursor is somewhere in the command name, show help text for the command.
    				return command.renderHelp();
    			} else {
    				// The cursor is somewhere in the command arguments, ask the command to inspect.
    				return inspectCommandArguments(command, split.getArgString(), at);
    			}
    		} else {
    			// Invalid command, can't inspect.
    			return null;
    		}
    	}
    	
    	@Override
    	public @Nullable DisplayData inspect(final @NotNull String code, final int at, final boolean extraDetail) {
    		// Note: We ignore the extraDetail parameter, because in practice it is always false. This is because the inspect_request messages sent by Jupyter Notebook always have their detail_level set to 0.
    		return this.inspectInternal(new PositionedString(code, 0), at);
    	}
    	
    	private static @NotNull ReplacementOptions offsetReplacementOptions(final @NotNull ReplacementOptions replacements, final int offset) {
    		return new ReplacementOptions(
    			replacements.getReplacements(),
    			replacements.getSourceStart() + offset,
    			replacements.getSourceEnd() + offset
    		);
    	}
    	
    	private static @Nullable ReplacementOptions completeCommandArguments(final @NotNull Command command, final @NotNull PositionedString argString, final int at) {
    		final SplitResult split = CommandUtils.splitArgs(command.getParameters(), argString, at);
    		
    		final Parameter<?> parameterToComplete;
    		final PositionedString argumentToComplete;
    		if (split.getParameterAtPosition().isPresent()) {
    			// If any arguments were split,
    			// use the last parameter for completion.
    			parameterToComplete = split.getParameterAtPosition().get();
    			final List<PositionedString> argsAtPosition = split.getArguments().get(parameterToComplete);
    			assert !argsAtPosition.isEmpty();
    			argumentToComplete = argsAtPosition.get(argsAtPosition.size() - 1);
    		} else if (!command.getParameters().getPositionalParameters().isEmpty()) {
    			// Special case: if the command has parameters,
    			// but argument splitting stopped before any arguments were split,
    			// perform completion on the first parameter and an empty string at the cursor.
    			parameterToComplete = command.getParameters().getPositionalParameters().get(0);
    			argumentToComplete = new PositionedString("", at);
    		} else {
    			// Command has no parameters,
    			// so completion in the arguments is not possible.
    			return null;
    		}
    		
    		final Optional<Completer> completer = command.getParameterCompleters().getCompleterForParameter(parameterToComplete);
    		if (completer.isPresent()) {
    			final ReplacementOptions replacements = completer.get().complete(argumentToComplete.getValue(), at - argumentToComplete.getStartPosition());
    			return replacements == null ? null : offsetReplacementOptions(replacements, argumentToComplete.getStartPosition());
    		} else {
    			return null;
    		}
    	}
    	
    	private @Nullable ReplacementOptions completeInternal(final @NotNull PositionedString code, final int at) {
    		final SplitCommandCall split = splitCommand(preprocessInput(code));
    		if (at <= split.getName().getEndPosition()) {
    			// The cursor is somewhere in the command name, provide command completions.
    			final String prefix = split.getName().substring(0, at - split.getName().getStartPosition()).getValue();
    			return new ReplacementOptions(
    				this.getCommands().keySet().stream().filter(s -> s.startsWith(prefix)).sorted().collect(Collectors.toList()),
    				split.getName().getStartPosition(),
    				split.getName().getEndPosition()
    			);
    		} else {
    			// The cursor is somewhere in the command arguments, ask the command to provide completions.
    			if (this.getCommands().containsKey(split.getName().getValue())) {
    				return completeCommandArguments(this.getCommands().get(split.getName().getValue()), split.getArgString(), at);
    			} else {
    				// Invalid command, can't provide any completions.
    				return null;
    			}
    		}
    	}
    	
    	@Override
    	public @Nullable ReplacementOptions complete(final @NotNull String code, final int at) {
    		return this.completeInternal(new PositionedString(code, 0), at);
    	}
    	
    	@Override
    	public String isComplete(final String code) {
    		final Matcher commandMatcher = COMMAND_PATTERN.matcher(code);
    		if (commandMatcher.matches() && commandMatcher.group(1).startsWith("::")) {
    			return code.endsWith("\n") ? IS_COMPLETE_MAYBE : "";
    		} else {
    			// TODO Support line continuation for normal commands
    			return IS_COMPLETE_MAYBE;
    		}
    	}
    	
    	@Override
    	public @NotNull LanguageInfo getLanguageInfo() {
    		return new LanguageInfo.Builder("prob")
    			.mimetype("text/x-prob2-jupyter-repl")
    			.fileExtension(".prob")
    			.codemirror("prob2_jupyter_repl")
    			.build();
    	}
    	
    	@Override
    	public void onShutdown(final boolean isRestarting) {
    		this.animationSelector.getCurrentTrace().getStateSpace().kill();
    	}
    	
    	@Override
    	public void interrupt() {
    		final Thread evalThread = this.currentEvalThread.get();
    		if (evalThread != null) {
    			evalThread.interrupt();
    		}
    		this.animationSelector.getCurrentTrace().getStateSpace().sendInterrupt();
    	}
    	
    	private @NotNull List<@NotNull String> formatErrorSource(final @NotNull List<@NotNull String> sourceLines, final @NotNull ErrorItem.Location location) {
    		if (sourceLines.isEmpty()) {
    			return Collections.singletonList(this.errorStyler.primary("// Source code not known"));
    		}
    		
    		final List<String> out = new ArrayList<>();
    		if (location.getStartLine() < 1 || location.getStartLine() > sourceLines.size()) {
    			out.add(this.errorStyler.secondary(String.format("Error start line %d out of bounds (1..%d)", location.getStartLine(), sourceLines.size())));
    			return out;
    		}
    		if (location.getEndLine() < 1 || location.getEndLine() > sourceLines.size()) {
    			out.add(this.errorStyler.secondary(String.format("Error end line %d out of bounds (1..%d)", location.getEndLine(), sourceLines.size())));
    			return out;
    		}
    		final List<String> errorLines = sourceLines.subList(location.getStartLine()-1, location.getEndLine());
    		assert !errorLines.isEmpty();
    		final String firstLine = errorLines.get(0);
    		final String lastLine = errorLines.get(errorLines.size() - 1);
    		if (location.getStartColumn() < 0 || location.getStartColumn() > firstLine.length()) {
    			out.add(this.errorStyler.secondary(String.format("Error start column %d out of bounds (0..%d)", location.getStartColumn(), firstLine.length()-1)));
    			return out;
    		}
    		if (location.getEndColumn() < 0 || location.getEndColumn() > lastLine.length()) {
    			out.add(this.errorStyler.secondary(String.format("Error end column %d out of bounds (0..%d)", location.getEndColumn(), lastLine.length()-1)));
    			return out;
    		}
    		if (errorLines.size() == 1) {
    			out.add(
    				this.errorStyler.primary(firstLine.substring(0, location.getStartColumn()))
    				+ this.errorStyler.highlight(firstLine.substring(location.getStartColumn(), location.getEndColumn()))
    				+ this.errorStyler.primary(firstLine.substring(location.getEndColumn()))
    			);
    		} else {
    			out.add(
    				this.errorStyler.primary(firstLine.substring(0, location.getStartColumn()))
    				+ this.errorStyler.highlight(firstLine.substring(location.getStartColumn()))
    			);
    			errorLines.subList(1, errorLines.size()-1).stream().map(this.errorStyler::highlight).collect(Collectors.toCollection(() -> out));
    			out.add(
    				this.errorStyler.highlight(lastLine.substring(0, location.getEndColumn()))
    				+ this.errorStyler.primary(lastLine.substring(location.getEndColumn()))
    			);
    		}
    		return out;
    	}
    	
    	@Override
    	public @NotNull List<@NotNull String> formatError(final Exception e) {
    		try {
    			LOGGER.warn("Exception while executing command from user", e);
    			if (e instanceof UserErrorException) {
    				return this.errorStyler.secondaryLines(String.valueOf(e.getMessage()));
    			} else if (e instanceof ProBError || e instanceof WithSourceCodeException) {
    				final List<String> sourceLines;
    				final ProBError proBError;
    				if (e instanceof WithSourceCodeException) {
    					sourceLines = Arrays.asList(((WithSourceCodeException)e).getSourceCode().split("\n"));
    					proBError = ((WithSourceCodeException)e).getCause();
    				} else {
    					sourceLines = Collections.emptyList();
    					proBError = (ProBError)e;
    				}
    				final List<String> out = new ArrayList<>(Arrays.asList((
    					this.errorStyler.primary("Error from ProB: ")
    					+ this.errorStyler.secondary(String.valueOf(proBError.getOriginalMessage()))
    				).split("\n")));
    				if (proBError.getErrors() == null) {
    					// If the errors list is null rather than empty, don't show the "no error messages" message.
    					// (This matches the normal behavior of ProBError.)
    				} else if (proBError.getErrors().isEmpty()) {
    					out.addAll(this.errorStyler.primaryLines("ProB returned no error messages.\n"));
    				} else {
    					if (proBError.getErrors().size() > 1) {
    						out.addAll(this.errorStyler.primaryLines(proBError.getErrors().size() + " errors:\n"));
    					}
    					for (final ErrorItem error : proBError.getErrors()) {
    						out.addAll(this.errorStyler.secondaryLines(error.toString()));
    						for (final ErrorItem.Location location : error.getLocations()) {
    							out.addAll(formatErrorSource(sourceLines, location));
    						}
    					}
    				}
    				return out;
    			} else {
    				return this.errorStyler.secondaryLines(e.toString());
    			}
    		} catch (final RuntimeException e2) {
    			LOGGER.error("Exception in error formatting", e2);
    			throw e2;
    		}
    	}
    	
    	public @NotNull String insertLetVariables(final @NotNull String code) {
    		return CommandUtils.insertLetVariables(code, this.getVariables());
    	}
    }