001package org.clafer.javascript;
002
003import java.io.BufferedReader;
004import java.io.File;
005import java.io.IOException;
006import java.io.InputStreamReader;
007import java.util.Set;
008import javax.script.ScriptException;
009import org.clafer.ast.AstClafer;
010import org.clafer.ast.AstConstraint;
011import org.clafer.ast.AstModel;
012import org.clafer.ast.AstUtil;
013import org.clafer.ast.Asts;
014import org.clafer.collection.Pair;
015import org.clafer.collection.Triple;
016import org.clafer.compiler.ClaferCompiler;
017import org.clafer.compiler.ClaferOptions;
018import org.clafer.compiler.ClaferSearch;
019import org.clafer.compiler.ClaferUnsat;
020import org.clafer.instance.InstanceModel;
021import org.clafer.objective.Objective;
022import org.clafer.scope.Scope;
023import org.mozilla.javascript.Context;
024import org.mozilla.javascript.RhinoException;
025import org.mozilla.javascript.Scriptable;
026
027/**
028 * The Javascript CLI.
029 *
030 * @author jimmy
031 */
032public class JavascriptShell {
033
034    // The Javascript engine.
035    private Scriptable engine = Javascript.newEngine();
036    // The model. Null if the model is not successfully loaded.
037    private AstModel model;
038    // The scope. Null if the model is not successfully loaded.
039    private Scope scope;
040    // The objectives.
041    private Objective[] objectives;
042    // The solver which solved the previous instance.
043    // Null if first instance not yet solved.
044    private ClaferSearch solver;
045    // The last file successfully loaded.
046    private File modelFile;
047    // Configurable options.
048    private final Options options = new Options();
049
050    public void init() throws IOException {
051        Context cxt = Context.enter();
052        cxt.setOptimizationLevel(-1);
053        try {
054            engine.put("rsc", engine, this);
055            // solver.js contains the mapping from Javascript to the methods
056            // in this class.
057            cxt.evaluateReader(engine,
058                    new InputStreamReader(JavascriptShell.class.getResourceAsStream("solver.js")),
059                    "solver.js", 1, null);
060            engine.put("help", engine,
061                    "help             display this helpful message\n"
062                    + "load()           reload the model\n"
063                    + "load(filename)   load a new model\n"
064                    + "solve()          find the next solution\n"
065                    + "maximize(Clafer) find a solution where Clafer.ref is maximal\n"
066                    + "minimize(Clafer) find a solution where Clafer.ref is minimal\n"
067                    + "minUnsat()       find the smallest set of unsatisfiable constraints and a near-miss\n"
068                    + "unsatCore()      find a small set of mutually unsatisfiable constraints\n"
069                    + "stats()          display statistics about the current search\n"
070                    + "options          display and modify the compiler options\n"
071                    + "exit()           stop the session");
072            engine.put("options", engine, options);
073        } finally {
074            Context.exit();
075        }
076    }
077
078    /**
079     * Returns information about the final Choco CSP.
080     *
081     * @return information for debugging
082     */
083    public String internals() {
084        return internals(false);
085    }
086
087    /**
088     * Returns information about the final Choco CSP.
089     *
090     * @param initial debug initial state or current state
091     * @return information for debugging
092     */
093    public String internals(boolean initial) {
094        if (model == null) {
095            return "No model. Use \"load(filename)\" to load in a new model.";
096        }
097        if (initial) {
098            return ClaferCompiler.compile(getModel(), scope).getInternalSolver().toString();
099        }
100        if (solver == null) {
101            return "Solve an instance first. Try \"solve()\".";
102        }
103        return solver.getInternalSolver().toString() + "\n" + solver.getInternalSolver().getMeasures();
104    }
105
106    /**
107     * Execute Javascript.
108     *
109     * @param script executable Javascript
110     * @return the value of the script, or null if it is a statement
111     */
112    String eval(String script) {
113        Context cxt = Context.enter();
114        cxt.setOptimizationLevel(-1);
115        try {
116            Object obj = cxt.evaluateString(engine, script, "commandline", 1, null);
117            if (obj instanceof Messagable) {
118                return ((Messagable) obj).toMessage();
119            }
120            return Context.toString(obj);
121        } finally {
122            Context.exit();
123        }
124    }
125
126    /**
127     * Reload the previous successful file.
128     *
129     * @return a message
130     * @throws IOException something happened to the file
131     */
132    public String load() throws IOException {
133        if (modelFile == null) {
134            return "No model. Use \"load(filename)\" to load in a new model.";
135        }
136        return load(modelFile);
137    }
138
139    /**
140     * Load a new file. The file must contain valid Javascript.
141     *
142     * @param filename the name of the file
143     * @return a message
144     * @throws IOException something happened to the file
145     */
146    public String load(String filename) throws IOException {
147        return load(new File(filename));
148    }
149
150    /**
151     * Load a new file. The file must contain valid Javascript.
152     *
153     * @param file the file
154     * @return a message
155     * @throws IOException something happened to the file
156     */
157    public String load(File file) throws IOException {
158        model = null;
159        scope = null;
160        solver = null;
161        engine = Javascript.newEngine();
162        try {
163            Triple<AstModel, Scope, Objective[]> pair = Javascript.readModel(file, engine);
164            model = pair.getFst();
165            scope = pair.getSnd();
166            objectives = pair.getThd();
167        } finally {
168            init();
169        }
170        modelFile = file;
171        return "Loaded " + AstUtil.getNames(AstUtil.getClafers(model)) + ".";
172    }
173
174    private AstModel getModel() {
175        if (model == null) {
176            throw new JavascriptException("No model. Use \"load(filename)\" to load in a new model.");
177        }
178        return model;
179    }
180
181    /**
182     * Find the first instance or next instances upon subsequent invocations.
183     *
184     * @return an instance
185     */
186    public Object solve() {
187        if (solver == null) {
188            if (objectives.length == 0) {
189                solver = ClaferCompiler.compile(getModel(), scope);
190            } else if (objectives.length == 1) {
191                solver = ClaferCompiler.compile(getModel(), scope, objectives[0]);
192            } else {
193                return "Muliobjective optimization not yet supported.";
194            }
195        }
196        return solver.find() ? solver.instance() : null;
197    }
198
199    /**
200     * Find an instance where the Clafer's value is maximal.
201     *
202     * @param clafer maximize this Clafer's value
203     * @return the maximal value and the optimal instance
204     */
205    public Object maximize(AstClafer clafer) {
206        if (!clafer.hasRef()) {
207            throw new JavascriptException("Cannot maximize " + clafer + ".");
208        }
209        solver = ClaferCompiler.compile(getModel(), scope,
210                Objective.maximize(Asts.sum(Asts.global(clafer))));
211        return solver.find() ? solver.instance() : null;
212
213    }
214
215    /**
216     * Find an instance where the Clafer's value is minimal.
217     *
218     * @param clafer minimize this Clafer's value
219     * @return the minimal value and the optimal instance
220     */
221    public Object minimize(AstClafer clafer) {
222        if (!clafer.hasRef()) {
223            throw new JavascriptException("Cannot minimize " + clafer + ".");
224        }
225        solver = ClaferCompiler.compile(getModel(), scope,
226                Objective.minimize(Asts.sum(Asts.global(clafer))));
227        return solver.find() ? solver.instance() : null;
228    }
229
230    /**
231     * Find the Min-Unsat and near-miss example. The Min-Unsat is empty if the
232     * model is satisfiable, in which case the near-miss example is a real
233     * instance of the model.
234     *
235     * @return the Min-Unsat and near-miss example
236     */
237    public Pair<Set<AstConstraint>, InstanceModel> minUnsat() {
238        ClaferUnsat unsat = ClaferCompiler.compileUnsat(getModel(), scope);
239        return unsat.minUnsat();
240    }
241
242    /**
243     * Find the Min-Unsat-Core. Undefined if the model is satisfiable.
244     *
245     * @return the Min-Unsat-Core
246     */
247    public Set<AstConstraint> unsatCore() {
248        ClaferUnsat unsat = ClaferCompiler.compileUnsat(getModel(), scope);
249        return unsat.unsatCore();
250    }
251
252    public String stats() {
253        if (solver == null) {
254            throw new JavascriptException("No solver. Use \"solve()\" to solve the model.");
255        }
256        return solver.getInternalSolver().getMeasures().toString();
257    }
258
259    /**
260     * Exit the program.
261     *
262     * @return a message
263     */
264    public String exit() {
265        // Use interrupts to signal end.
266        Thread.currentThread().interrupt();
267        return "Exitting ...";
268    }
269
270    public static void main(String[] args) throws IOException, ScriptException {
271        JavascriptShell context = new JavascriptShell();
272        if (args.length > 0) {
273            try {
274                System.out.println(context.load(args[0]));
275            } catch (IOException e) {
276                System.out.println("Error: " + getOriginalMessage(e));
277            } catch (RhinoException e) {
278                System.out.println("Error: " + getOriginalMessage(e));
279            }
280        } else {
281            context.init();
282        }
283
284        System.out.println("Type \"help\" for a list of commands.");
285        System.out.print("> ");
286        System.out.flush();
287        BufferedReader r = new BufferedReader(new InputStreamReader(System.in));
288        String line;
289        while ((line = r.readLine()) != null) {
290            line = line.trim();
291            // Do not evaluate empty lines.
292            if (line.length() > 0) {
293                try {
294                    // Evaluate the Javascript command.
295                    System.out.println(context.eval(line));
296                    if (Thread.interrupted()) {
297                        // Exit was invoked.
298                        break;
299                    }
300                } catch (RhinoException e) {
301                    System.out.println("Error: " + getOriginalMessage(e));
302                }
303            }
304            System.out.print("> ");
305            System.out.flush();
306        }
307        System.out.println();
308    }
309
310    /**
311     * Find the message in the innermost cause. The Rhino engine tends to have
312     * several layers of wrapped exceptions, which do not look great when
313     * printing on the command line.
314     *
315     * @param e the outermost exception
316     * @return the innermost message
317     */
318    private static String getOriginalMessage(Throwable e) {
319        if (e.getCause() != null) {
320            return getOriginalMessage(e.getCause());
321        }
322        return e.getMessage();
323    }
324
325    public static interface Messagable {
326
327        /**
328         * @return a message for the shell
329         */
330        public String toMessage();
331    }
332
333    public static class Options implements Messagable {
334
335        private ClaferOptions options = ClaferOptions.Default;
336
337        public String preferSmallerInstances() {
338            options = options.preferSmallerInstances();
339            return "Updated options.";
340        }
341
342        public String preferLargerInstances() {
343            options = options.preferLargerInstances();
344            return "Updated options.";
345        }
346
347        public String basicSymmetryBreaking() {
348            options = options.basicSymmetryBreaking();
349            return "Updated options.";
350        }
351
352        public String fullSymmetryBreaking() {
353            options = options.fullSymmetryBreaking();
354            return "Updated options.";
355        }
356
357        public String basicOptimizations() {
358            options = options.basicOptimizations();
359            return "Updated options.";
360        }
361
362        public String fullOptimizations() {
363            options = options.fullOptimizations();
364            return "Updated options.";
365        }
366
367        // Convenience function for the toString method.
368        private static String star(boolean bool) {
369            return bool ? " * " : "   ";
370        }
371
372        /**
373         * {@inheritDoc}
374         */
375        @Override
376        public String toMessage() {
377            return "(*) marks the current setting\n"
378                    + star(options.isPreferSmallerInstances()) + "options.preferSmallerInstances() bias the search towards smaller instances\n"
379                    + star(options.isPreferLargerInstances()) + "options.preferLargerInstances()  bias the search towards larger instances\n"
380                    + star(options.isBasicSymmetryBreaking()) + "options.basicSymmetryBreaking()  basic symmetry breaking\n"
381                    + star(options.isFullSymmetryBreaking()) + "options.fullSymmetryBreaking()   full symmetry breaking\n"
382                    + star(options.isBasicOptimizations()) + "options.basicOptimizations()     basic optimizations\n"
383                    + star(options.isFullOptimizations()) + "options.fullOptimizations()      full optimizations";
384        }
385    }
386}