package nl.utwente.viskell.ghcj; import com.google.common.collect.EvictingQueue; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.util.concurrent.AbstractExecutionThreadService; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import nl.utwente.viskell.haskell.env.Environment; import nl.utwente.viskell.haskell.env.HaskellCatalog; import nl.utwente.viskell.haskell.expr.Expression; import nl.utwente.viskell.haskell.type.Type; import nl.utwente.viskell.ui.Main; import java.io.IOException; import java.util.AbstractMap; import java.util.EnumSet; import java.util.List; import java.util.Objects; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ExecutionException; import java.util.prefs.Preferences; /** * A conversation with an instance of ghci. * * Public methods are safe to use from multiple threads. */ public class GhciSession extends AbstractExecutionThreadService { /** Work queue. */ private ArrayBlockingQueue<AbstractMap.SimpleEntry<String, SettableFuture<String>>> queue; /** Stuff this into the work queue to stop running. */ private final static String POISON = null; /** The evaluator this GhciSession will communicate with. */ private Evaluator ghci; /** Gets filled with a HaskellCatalog instance when ghci is ready. */ private static HaskellCatalog catalog; /** Gets filled with up to LOG_SIZE errors. */ private EvictingQueue<String> errors; /** The number of errors to keep. */ private final static int LOG_SIZE = 16; public enum Backend { GHCi, Clash, } /** * Builds a new communication session with ghci. * * Starting the backend is delayed until startAsync() is called. */ public GhciSession() { super(); queue = new ArrayBlockingQueue<>(1024); errors = EvictingQueue.create(LOG_SIZE); switch (pickBackend()) { case Clash: this.catalog = new HaskellCatalog("/catalog/clash.xml"); break; default: this.catalog = new HaskellCatalog("/catalog/haskell.xml"); break; } } @Override protected void run() throws Exception { while (true) { AbstractMap.SimpleEntry<String, SettableFuture<String>> x = queue.take(); String expr = x.getKey(); SettableFuture<String> future = x.getValue(); if (Objects.equals(expr, POISON)) { // Something wants us to quit - do so. break; } else { try { String result = this.ghci.eval(expr); future.set(result.trim()); } catch (HaskellException e) { future.setException(e); errors.add(e.getMessage()); } } } } /** * Uploads a new let binding to ghci * @param name The name of the new function. * @param func The actual function. */ public ListenableFuture<String> push(final String name, final Expression func) { String let = String.format("let %s = %s", name, func.toHaskell()); return pullRaw(let); } /** * Returns the result of evaluating a Haskell expression. * @param expr The expression to evaluate. * @return The result of the evaluation. */ public ListenableFuture<String> pull(final Expression expr) { return pullRaw(expr.toHaskell()); } /** * Returns the result of evaluating something in ghci. * Should only be used for testing purposes or for a known valid Haskell expression. * @param expr The string representation of the expression to evaluate. * @return The result of the evaluation. */ public ListenableFuture<String> pullRaw(final String expr) { SettableFuture<String> result = SettableFuture.create(); AbstractMap.SimpleEntry<String, SettableFuture<String>> entry = new AbstractMap.SimpleEntry<>(expr, result); try { queue.put(entry); } catch (InterruptedException e) { result.setException(e); } return result; } /** * Ask ghci for the type of an expression * @param expr The expression String to determine the type of. * @param env The environment in which the type will be resolved * @return The parsed Haskell type * @throws HaskellException when ghci encountered an error or the type could not be parsed. */ public Type pullType(final String expr, Environment env) throws HaskellException { try { String[] parts = this.pullRaw(":t " + expr).get().split(" :: "); if (parts.length < 2) { throw new HaskellException("ghci could not determine the type of:\n" + expr); } return env.buildType(parts[1].trim()); } catch (InterruptedException | ExecutionException e) { throw new HaskellException(e); } } @Override protected void triggerShutdown() { queue.offer(new AbstractMap.SimpleEntry<>(POISON, null)); } /** * @return a String representation of this GhciSession. */ public String toString() { return "GhciSession{" + this.ghci + "}"; } @Override public void shutDown() throws IOException { try { this.ghci.close(); this.ghci = null; } catch (HaskellException e) { e.printStackTrace(); } } /** Build a new Evaluator, closing the old one if it exists. */ @Override public void startUp() throws HaskellException { if (this.ghci != null) { this.ghci.close(); } this.ghci = evaluatorFactory(pickBackend()); } /** Build the Evaluator that corresponds to the given Backend identifier. */ private Evaluator evaluatorFactory(Backend evaluator) throws HaskellException { switch (evaluator) { case GHCi: return new GhciEvaluator(); case Clash: return new ClashEvaluator(); default: return new GhciEvaluator(); } } /** @return the Backend in the preferences, or GHCi otherwise. */ public static Backend pickBackend() { Preferences prefs = Preferences.userNodeForPackage(Main.class); String name = prefs.get("ghci", Backend.GHCi.name()); return Backend.valueOf(name); } /** @return the available backend identifiers. */ public static List<Backend> getBackends() { return Lists.newArrayList(EnumSet.allOf(Backend.class)); } // FIXME this is a bit of a hack to have a static method to get the catalog from static deserialization methods // either that or we pass the catalog as a parameter to ALL deserialization methods so that have the same signature. public static HaskellCatalog getHaskellCatalog() { return catalog; } public HaskellCatalog getCatalog() { return catalog; } /** @return an immutable list of the last LOG_SIZE runtime errors. */ public List<String> getErrors() { return ImmutableList.copyOf(errors); } }