package nl.utwente.viskell.ghcj; import com.google.common.base.Joiner; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.List; /** * Evaluator class. Haskell expressions (strings) go in, results (strings) * come out. Results that look like errors or exceptions cause a HaskellException. * * Uses an actual interpreter as a subprocess. Which interpreter that is is * decided by a subclass. Which subclass to pick is decided by GhciSession. * * Not to be used from multiple threads. */ abstract public class Evaluator { /** Responses from ghci are terminated by a null byte. */ protected static final char SENTINEL = 0; /** All communication is done over UTF_8. */ protected static final Charset UTF_8 = StandardCharsets.UTF_8; /** Raw input stream for result data from ghci to the application. */ protected InputStream in; /** Raw output stream from the application to ghci. */ protected OutputStream out; /** A newline character. */ protected final String NL; public Evaluator() throws HaskellException { this.NL = System.getProperty("line.separator"); try { /* The ghci process to use. */ Process process = new ProcessBuilder(getCommand()) .redirectErrorStream(true) .start(); this.in = process.getInputStream(); this.out = process.getOutputStream(); } catch (IOException io) { // Try an alternative ghci command if available. List<String> altCommand = this.getAltCommand(); if (! altCommand.isEmpty()) { try { Process process = new ProcessBuilder(altCommand).redirectErrorStream(true).start(); this.in = process.getInputStream(); this.out = process.getOutputStream(); } catch (IOException io2) { throw new HaskellException(io2); } } else { throw new HaskellException(io); } } /* Make it so that GHCi prints a null byte to its standard output when it expects input. By setting the prompt to a zero byte, GHCi will print a zero byte whenever it expects the user (that's us) to enter the next expression. In UTF-8, zero bytes are not part of any character except for NUL, the zero character, which makes them a useful sentinel. */ this.eval(":set prompt " + SENTINEL); /* Load some useful modules. */ this.eval(":module " + Joiner.on(" ").join(getModules())); /* Make it so that GHCi resets bindings after every command. This makes it slightly less likely that GHCi state will affect our results. */ this.eval(":set +r"); } /** * Destroys the ghci instance and closes communications channels. * @throws HaskellException when closing the channels fails. */ public final void close() throws HaskellException { try { this.in.close(); this.out.close(); } catch (IOException e) { throw new HaskellException(e); } } /** * Evaluates a Haskell expression and wait for it to compute. * * @param cmd The (complete) Haskell * @return the result, including newline, as a string. * @throws HaskellException when ghci is not ready to evaluate, or expression can not be computed. */ public final String eval(final String cmd) throws HaskellException { StringBuilder responseBuilder = new StringBuilder(); try { // Send the expression to ghci. this.out.write(cmd.getBytes(UTF_8)); this.out.write('\n'); this.out.flush(); // Wait for the sentinel. int input; while ((input = this.in.read()) != 0) { responseBuilder.append((char) input); } } catch (IOException e) { throw new HaskellException(e); } String response = responseBuilder.toString(); // Check for hints that something went wrong // To do: Make this better String exceptionHeader = "*** Exception: "; String parseErrorHeader = "<interactive>"; List<String> lines = Splitter.on(this.NL).splitToList(response); for (int i = 0; i < lines.size(); i++) { String line = lines.get(i); if (line.startsWith(exceptionHeader)) { String msg = line.substring(exceptionHeader.length()); throw new HaskellException(msg); } if (line.startsWith(parseErrorHeader)) { List<String> sublines = lines.subList(i, lines.size()); String msg = Joiner.on(this.NL).join(sublines); throw new HaskellException(msg); } } return response; } /** @return the command and arguments for the subprocess. */ protected abstract List<String> getCommand(); /** @return the alternative command and arguments for the subprocess. */ protected List<String> getAltCommand() { return ImmutableList.of(); } /** @return the list of modules to load automatically. */ protected abstract List<String> getModules(); }