/* Alloy Analyzer 4 -- Copyright (c) 2006-2009, Felix Chang
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files
* (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify,
* merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
* OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package edu.mit.csail.sdg.alloy4whole;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import edu.mit.csail.sdg.alloy4.A4Reporter;
import edu.mit.csail.sdg.alloy4.ConstList;
import edu.mit.csail.sdg.alloy4.ConstMap;
import edu.mit.csail.sdg.alloy4.Err;
import edu.mit.csail.sdg.alloy4.ErrorSyntax;
import edu.mit.csail.sdg.alloy4.ErrorType;
import edu.mit.csail.sdg.alloy4.ErrorWarning;
import edu.mit.csail.sdg.alloy4.MailBug;
import edu.mit.csail.sdg.alloy4.OurDialog;
import edu.mit.csail.sdg.alloy4.Pair;
import edu.mit.csail.sdg.alloy4.Pos;
import edu.mit.csail.sdg.alloy4.Util;
import edu.mit.csail.sdg.alloy4.Version;
import edu.mit.csail.sdg.alloy4.XMLNode;
import edu.mit.csail.sdg.alloy4.WorkerEngine.WorkerCallback;
import edu.mit.csail.sdg.alloy4.WorkerEngine.WorkerTask;
import edu.mit.csail.sdg.alloy4compiler.ast.Command;
import edu.mit.csail.sdg.alloy4compiler.ast.Sig;
import edu.mit.csail.sdg.alloy4compiler.ast.Module;
import edu.mit.csail.sdg.alloy4compiler.parser.CompUtil;
import edu.mit.csail.sdg.alloy4compiler.translator.A4Options;
import edu.mit.csail.sdg.alloy4compiler.translator.A4Solution;
import edu.mit.csail.sdg.alloy4compiler.translator.A4SolutionReader;
import edu.mit.csail.sdg.alloy4compiler.translator.A4SolutionWriter;
import edu.mit.csail.sdg.alloy4compiler.translator.TranslateAlloyToKodkod;
import edu.mit.csail.sdg.alloy4viz.StaticInstanceReader;
import edu.mit.csail.sdg.alloy4viz.VizGUI;
/** This helper method is used by SimpleGUI. */
final class SimpleReporter extends A4Reporter {
public static final class SimpleCallback1 implements WorkerCallback {
private final SimpleGUI gui;
private final VizGUI viz;
private final SwingLogPanel span;
private final Set<ErrorWarning> warnings = new HashSet<ErrorWarning>();
private final List<String> results = new ArrayList<String>();
private int len2=0, len3=0, verbosity=0;
private final String latestName;
private final int latestVersion;
public SimpleCallback1(SimpleGUI gui, VizGUI viz, SwingLogPanel span, int verbosity, String latestName, int latestVersion) {
this.gui=gui; this.viz=viz; this.span=span; this.verbosity=verbosity;
this.latestName=latestName; this.latestVersion=latestVersion;
len2 = len3 = span.getLength();
}
public void done() { if (viz!=null) span.setLength(len2); else span.logDivider(); span.flush(); gui.doStop(0); }
public void fail() { span.logBold("\nAn error has occurred!\n"); span.logDivider(); span.flush(); gui.doStop(1); }
public void callback(Object msg) {
if (msg==null) { span.logBold("Done\n"); span.flush(); return; }
if (msg instanceof String) { span.logBold( ((String)msg).trim() + "\n" ); span.flush(); return; }
if (msg instanceof Throwable) {
for(Throwable ex = (Throwable)msg; ex!=null; ex=ex.getCause()) {
if (ex instanceof OutOfMemoryError) {
span.logBold("\nFatal Error: the solver ran out of memory!\n" + "Try simplifying your model or reducing the scope,\n" + "or increase memory under the Options menu.\n");
return;
}
if (ex instanceof StackOverflowError) {
span.logBold("\nFatal Error: the solver ran out of stack space!\n" + "Try simplifying your model or reducing the scope,\n" + "or increase stack under the Options menu.\n");
return;
}
}
}
if (msg instanceof Err) {
Err ex = (Err)msg;
String text = "fatal";
boolean fatal = false;
if (ex instanceof ErrorSyntax) text="syntax"; else if (ex instanceof ErrorType) text="type"; else fatal=true;
if (ex.pos==Pos.UNKNOWN)
span.logBold("A "+text+" error has occurred: ");
else
span.logLink("A "+text+" error has occurred: ", "POS: "+ex.pos.x+" "+ex.pos.y+" "+ex.pos.x2+" "+ex.pos.y2+" "+ex.pos.filename);
if (verbosity>2) {
span.log("(see the "); span.logLink("stacktrace", "MSG: "+ex.dump()); span.log(")\n");
} else {
span.log("\n");
}
span.logIndented(ex.msg.trim());
span.log("\n");
if (fatal && latestVersion>Version.buildNumber())
span.logBold(
"\nNote: You are running Alloy build#"+Version.buildNumber()+
",\nbut the most recent is Alloy build#"+latestVersion+
":\n( version "+latestName+" )\nPlease try to upgrade to the newest version,"+
"\nas the problem may have been fixed already.\n");
span.flush();
if (!fatal) gui.doVisualize("POS: "+ex.pos.x+" "+ex.pos.y+" "+ex.pos.x2+" "+ex.pos.y2+" "+ex.pos.filename);
return;
}
if (msg instanceof Throwable) { Throwable ex = (Throwable)msg; span.logBold(ex.toString().trim()+"\n"); span.flush(); return; }
if (!(msg instanceof Object[])) return;
Object[] array = (Object[]) msg;
if (array[0].equals("pop")) { span.setLength(len2); String x=(String)(array[1]); if (viz!=null && x.length()>0) OurDialog.alert(x); }
if (array[0].equals("declare")) { gui.doSetLatest((String)(array[1])); }
if (array[0].equals("S2")) { len3=len2=span.getLength(); span.logBold(""+array[1]); }
if (array[0].equals("R3")) { span.setLength(len3); span.log(""+array[1]); }
if (array[0].equals("link")) { span.logLink((String)(array[1]), (String)(array[2])); }
if (array[0].equals("bold")) { span.logBold(""+array[1]); }
if (array[0].equals("")) { span.log(""+array[1]); }
if (array[0].equals("scope") && verbosity>0) { span.log(" " + array[1]); }
if (array[0].equals("bound") && verbosity>1) { span.log(" " + array[1]); }
if (array[0].equals("resultCNF")) { results.add(null); span.setLength(len3); span.log(" File written to "+array[1]+"\n\n"); }
if (array[0].equals("debug") && verbosity>2) { span.log(" "+array[1]+"\n"); len2=len3=span.getLength(); }
if (array[0].equals("translate")) { span.log(" " + array[1]); len3 = span.getLength(); span.logBold(" Generating CNF...\n"); }
if (array[0].equals("solve")) { span.setLength(len3); span.log(" " + array[1]); len3=span.getLength(); span.logBold(" Solving...\n"); }
if (array[0].equals("warnings")) {
if (warnings.size()==0) span.setLength(len2);
else if (warnings.size()>1) span.logBold("Note: There were "+warnings.size()+" compilation warnings. Please scroll up to see them.\n\n");
else span.logBold("Note: There was 1 compilation warning. Please scroll up to see them.\n\n");
if (warnings.size()>0 && Boolean.FALSE.equals(array[1])) {
Pos e = warnings.iterator().next().pos;
gui.doVisualize("POS: "+e.x+" "+e.y+" "+e.x2+" "+e.y2+" "+e.filename);
span.logBold("Warnings often indicate errors in the model.\n"
+"Some warnings can affect the soundness of the analysis.\n"
+"To proceed despite the warnings, go to the Options menu.\n");
}
}
if (array[0].equals("warning")) {
ErrorWarning e = (ErrorWarning)(array[1]);
if (!warnings.add(e)) return;
Pos p=e.pos;
span.logLink("Warning #"+warnings.size(), "POS: "+p.x+" "+p.y+" "+p.x2+" "+p.y2+" "+p.filename);
span.log("\n"); span.logIndented(e.msg.trim()); span.log("\n\n");
}
if (array[0].equals("sat")) {
boolean chk = Boolean.TRUE.equals(array[1]);
int expects = (Integer) (array[2]);
String filename = (String) (array[3]), formula = (String) (array[4]);
results.add(filename);
(new File(filename)).deleteOnExit();
gui.doSetLatest(filename);
span.setLength(len3);
span.log(" ");
span.logLink(chk ? "Counterexample" : "Instance", "XML: "+filename);
span.log(" found. ");
span.logLink(chk?"Assertion":"Predicate", formula); span.log(chk?" is invalid":" is consistent");
if (expects==0) span.log(", contrary to expectation"); else if (expects==1) span.log(", as expected");
span.log(". "+array[5]+"ms.\n\n");
}
if (array[0].equals("metamodel")) {
String outf = (String) (array[1]);
span.setLength(len2);
(new File(outf)).deleteOnExit();
gui.doSetLatest(outf);
span.logLink("Metamodel", "XML: "+outf);
span.log(" successfully generated.\n\n");
}
if (array[0].equals("minimizing")) {
boolean chk = Boolean.TRUE.equals(array[1]);
int expects = (Integer) (array[2]);
span.setLength(len3);
span.log(chk ? " No counterexample found." : " No instance found.");
if (chk) span.log(" Assertion may be valid"); else span.log(" Predicate may be inconsistent");
if (expects==1) span.log(", contrary to expectation"); else if (expects==0) span.log(", as expected");
span.log(". "+array[4]+"ms.\n");
span.logBold(" Minimizing the unsat core of "+array[3]+" entries...\n");
}
if (array[0].equals("unsat")) {
boolean chk = Boolean.TRUE.equals(array[1]);
int expects = (Integer) (array[2]);
String formula = (String) (array[4]);
span.setLength(len3);
span.log(chk ? " No counterexample found. " : " No instance found. ");
span.logLink(chk ? "Assertion" : "Predicate", formula);
span.log(chk? " may be valid" : " may be inconsistent");
if (expects==1) span.log(", contrary to expectation"); else if (expects==0) span.log(", as expected");
if (array.length==5) { span.log(". "+array[3]+"ms.\n\n"); span.flush(); return; }
String core = (String) (array[5]);
int mbefore = (Integer) (array[6]), mafter = (Integer) (array[7]);
span.log(". "+array[3]+"ms.\n");
if (core.length()==0) { results.add(""); span.log(" No unsat core is available in this case. "+array[8]+"ms.\n\n"); span.flush(); return; }
results.add(core);
(new File(core)).deleteOnExit();
span.log(" ");
span.logLink("Core", core);
if (mbefore<=mafter) span.log(" contains "+mafter+" top-level formulas. "+array[8]+"ms.\n\n");
else span.log(" reduced from "+mbefore+" to "+mafter+" top-level formulas. "+array[8]+"ms.\n\n");
}
span.flush();
}
}
private void cb(Serializable... objs) { cb.callback(objs); }
/** {@inheritDoc} */
@Override public void resultCNF(final String filename) { cb("resultCNF", filename); }
/** {@inheritDoc} */
@Override public void warning(final ErrorWarning ex) { warn++; cb("warning", ex); }
/** {@inheritDoc} */
@Override public void scope(final String msg) { cb("scope", msg); }
/** {@inheritDoc} */
@Override public void bound(final String msg) { cb("bound", msg); }
/** {@inheritDoc} */
@Override public void debug(final String msg) { cb("debug", msg.trim()); }
/** {@inheritDoc} */
@Override public void translate(String solver, int bitwidth, int maxseq, int skolemDepth, int symmetry) {
lastTime = System.currentTimeMillis();
cb("translate", "Solver="+solver+" Bitwidth="+bitwidth+" MaxSeq="+maxseq
+ (skolemDepth==0?"":" SkolemDepth="+skolemDepth)
+ " Symmetry="+(symmetry>0 ? (""+symmetry) : "OFF")+'\n');
}
/** {@inheritDoc} */
@Override public void solve(final int primaryVars, final int totalVars, final int clauses) {
minimized=0;
cb("solve", ""+totalVars+" vars. "+primaryVars+" primary vars. "+clauses+" clauses. "+(System.currentTimeMillis()-lastTime)+"ms.\n");
lastTime = System.currentTimeMillis();
}
/** {@inheritDoc} */
@Override public void resultSAT(Object command, long solvingTime, Object solution) {
if (!(solution instanceof A4Solution) || !(command instanceof Command)) return;
A4Solution sol = (A4Solution)solution;
Command cmd = (Command)command;
String formula = recordKodkod ? sol.debugExtractKInput() : "";
String filename = tempfile+".xml";
synchronized(SimpleReporter.class) {
try {
cb("R3", " Writing the XML file...");
if (latestModule!=null) writeXML(this, latestModule, filename, sol, latestKodkodSRC);
} catch(Throwable ex) {
cb("bold", "\n" + (ex.toString().trim()) + "\nStackTrace:\n" + (MailBug.dump(ex).trim()) + "\n");
return;
}
latestKodkods.clear();
latestKodkods.add(sol.toString());
latestKodkod=sol;
latestKodkodXML=filename;
}
String formulafilename = "";
if (formula.length()>0 && tempfile!=null) {
formulafilename = tempfile+".java";
try { Util.writeAll(formulafilename, formula); formulafilename="CNF: "+formulafilename; } catch(Throwable ex) { formulafilename=""; }
}
cb("sat", cmd.check, cmd.expects, filename, formulafilename, System.currentTimeMillis()-lastTime);
}
/** {@inheritDoc} */
@Override public void minimizing(Object command, int before) {
if (!(command instanceof Command)) return;
Command cmd = (Command)command;
minimized = System.currentTimeMillis();
cb("minimizing", cmd.check, cmd.expects, before, minimized-lastTime);
}
/** {@inheritDoc} */
@Override public void minimized(Object command, int before, int after) { minimizedBefore=before; minimizedAfter=after; }
/** {@inheritDoc} */
@Override public void resultUNSAT(Object command, long solvingTime, Object solution) {
if (!(solution instanceof A4Solution) || !(command instanceof Command)) return;
A4Solution sol = (A4Solution)solution;
Command cmd = (Command)command;
String originalFormula = recordKodkod ? sol.debugExtractKInput() : "";
String corefilename="", formulafilename="";
if (originalFormula.length()>0 && tempfile!=null) {
formulafilename=tempfile+".java";
try { Util.writeAll(formulafilename, originalFormula); formulafilename="CNF: "+formulafilename; } catch(Throwable ex) { formulafilename=""; }
}
Pair<Set<Pos>,Set<Pos>> core = sol.highLevelCore();
if ((core.a.size()>0 || core.b.size()>0) && tempfile!=null) {
corefilename=tempfile+".core";
OutputStream fs=null;
ObjectOutputStream os=null;
try {
fs=new FileOutputStream(corefilename);
os=new ObjectOutputStream(fs);
os.writeObject(core);
os.writeObject(sol.lowLevelCore());
corefilename="CORE: "+corefilename;
} catch(Throwable ex) {
corefilename="";
} finally {
Util.close(os);
Util.close(fs);
}
}
if (minimized==0) cb("unsat", cmd.check, cmd.expects, (System.currentTimeMillis()-lastTime), formulafilename);
else cb("unsat", cmd.check, cmd.expects, minimized-lastTime, formulafilename, corefilename, minimizedBefore, minimizedAfter, (System.currentTimeMillis()-minimized));
}
private final WorkerCallback cb;
//========== These fields should be set each time we execute a set of commands
/** Whether we should record Kodkod input/output. */
private final boolean recordKodkod;
/** The time that the last action began; we subtract it from System.currentTimeMillis() to determine the elapsed time. */
private long lastTime=0;
/** If we performed unsat core minimization, then this is the start of the minimization, else this is 0. */
private long minimized = 0;
/** The unsat core size before minimization. */
private int minimizedBefore;
/** The unsat core size after minimization. */
private int minimizedAfter;
/** The filename where we can write a temporary Java file or Core file. */
private String tempfile=null;
//========== These fields may be altered as each successful command generates a Kodkod or Metamodel instance
/** The set of Strings already enumerated for this current solution. */
private static final Set<String> latestKodkods=new LinkedHashSet<String>();
/** The A4Solution corresponding to the latest solution generated by Kodkod; this field must be synchronized. */
private static A4Solution latestKodkod=null;
/** The root Module corresponding to this.latestKodkod; this field must be synchronized. */
private static Module latestModule=null;
/** The source code corresponding to the latest solution generated by Kodkod; this field must be synchronized. */
private static ConstMap<String,String> latestKodkodSRC = null;
/** The XML filename corresponding to the latest solution generated by Kodkod; this field must be synchronized. */
private static String latestKodkodXML=null;
/** The XML filename corresponding to the latest metamodel generated by TranslateAlloyToMetamodel; this field must be synchronized. */
private static String latestMetamodelXML=null;
/** Constructor is private. */
private SimpleReporter(WorkerCallback cb, boolean recordKodkod) { this.cb=cb; this.recordKodkod=recordKodkod; }
/** Helper method to write out a full XML file. */
private static void writeXML(A4Reporter rep, Module mod, String filename, A4Solution sol, Map<String,String> sources) throws Exception {
sol.writeXML(rep, filename, mod.getAllFunc(), sources);
if ("yes".equals(System.getProperty("debug"))) validate(filename);
}
private int warn=0;
/** Task that performs solution enumeration. */
static final class SimpleTask2 implements WorkerTask {
private static final long serialVersionUID = 0;
public String filename = "";
public transient WorkerCallback out = null;
private void cb(Object... objs) throws Exception { out.callback(objs); }
public void run(WorkerCallback out) throws Exception {
this.out = out;
cb("S2", "Enumerating...\n");
A4Solution sol;
Module mod;
synchronized(SimpleReporter.class) {
if (latestMetamodelXML!=null && latestMetamodelXML.equals(filename))
{cb("pop", "You cannot enumerate a metamodel.\n"); return;}
if (latestKodkodXML==null || !latestKodkodXML.equals(filename))
{cb("pop", "You can only enumerate the solutions of the most-recently-solved command."); return;}
if (latestKodkod==null || latestModule==null || latestKodkodSRC==null)
{cb("pop", "Error: the SAT solver that generated the instance has exited,\nso we cannot enumerate unless you re-solve that command.\n"); return;}
sol=latestKodkod;
mod=latestModule;
}
if (!sol.satisfiable())
{cb("pop", "Error: This command is unsatisfiable,\nso there are no solutions to enumerate."); return;}
if (!sol.isIncremental())
{cb("pop", "Error: This solution was not generated by an incremental SAT solver.\n" +
"Currently only MiniSat and SAT4J are supported."); return;}
int tries=0;
while(true) {
sol=sol.next();
if (!sol.satisfiable())
{cb("pop", "There are no more satisfying instances.\n\n" +
"Note: due to symmetry breaking and other optimizations,\n" +
"some equivalent solutions may have been omitted."); return;}
String toString = sol.toString();
synchronized(SimpleReporter.class) {
if (!latestKodkods.add(toString)) if (tries<100) { tries++; continue; }
// The counter is needed to avoid a Kodkod bug where sometimes we might repeat the same solution infinitely number of times; this at least allows the user to keep going
writeXML(null, mod, filename, sol, latestKodkodSRC); latestKodkod=sol;
}
cb("declare", filename);
return;
}
}
}
/** Validate the given filename to see if it is a valid Alloy XML instance file. */
private static void validate(String filename) throws Exception {
A4SolutionReader.read(new ArrayList<Sig>(), new XMLNode(new File(filename))).toString();
StaticInstanceReader.parseInstance(new File(filename));
}
/** Task that perform one command. */
static final class SimpleTask1 implements WorkerTask {
private static final long serialVersionUID = 0;
public A4Options options;
public String tempdir;
public boolean bundleWarningNonFatal;
public int bundleIndex;
public int resolutionMode;
public Map<String,String> map;
public SimpleTask1() { }
public void cb(WorkerCallback out, Object... objs) throws IOException { out.callback(objs); }
public void run(WorkerCallback out) throws Exception {
cb(out, "S2", "Starting the solver...\n\n");
final SimpleReporter rep = new SimpleReporter(out, options.recordKodkod);
final Module world = CompUtil.parseEverything_fromFile(rep, map, options.originalFilename, resolutionMode);
final List<Sig> sigs = world.getAllReachableSigs();
final ConstList<Command> cmds = world.getAllCommands();
cb(out, "warnings", bundleWarningNonFatal);
if (rep.warn>0 && !bundleWarningNonFatal) return;
List<String> result = new ArrayList<String>(cmds.size());
if (bundleIndex==-2) {
final String outf=tempdir+File.separatorChar+"m.xml";
cb(out, "S2", "Generating the metamodel...\n");
PrintWriter of = new PrintWriter(outf, "UTF-8");
Util.encodeXMLs(of, "\n<alloy builddate=\"", Version.buildDate(), "\">\n\n");
A4SolutionWriter.writeMetamodel(ConstList.make(sigs), options.originalFilename, of);
Util.encodeXMLs(of, "\n</alloy>");
Util.close(of);
if ("yes".equals(System.getProperty("debug"))) validate(outf);
cb(out, "metamodel", outf);
synchronized(SimpleReporter.class) { latestMetamodelXML=outf; }
} else for(int i=0; i<cmds.size(); i++) if (bundleIndex<0 || i==bundleIndex) {
synchronized(SimpleReporter.class) { latestModule=world; latestKodkodSRC=ConstMap.make(map); }
final String tempXML=tempdir+File.separatorChar+i+".cnf.xml";
final String tempCNF=tempdir+File.separatorChar+i+".cnf";
final Command cmd=cmds.get(i);
rep.tempfile=tempCNF;
cb(out, "bold", "Executing \""+cmd+"\"\n");
A4Solution ai=TranslateAlloyToKodkod.execute_commandFromBook(rep, world.getAllReachableSigs(), cmd, options);
if (ai==null) result.add(null);
else if (ai.satisfiable()) result.add(tempXML);
else if (ai.highLevelCore().a.size()>0) result.add(tempCNF+".core");
else result.add("");
}
(new File(tempdir)).delete(); // In case it was UNSAT, or canceled...
if (result.size()>1) {
rep.cb("bold", "" + result.size() + " commands were executed. The results are:\n");
for(int i=0; i<result.size(); i++) {
Command r=world.getAllCommands().get(i);
if (result.get(i)==null) { rep.cb("", " #"+(i+1)+": Unknown.\n"); continue; }
if (result.get(i).endsWith(".xml")) {
rep.cb("", " #"+(i+1)+": ");
rep.cb("link", r.check?"Counterexample found. ":"Instance found. ", "XML: "+result.get(i));
rep.cb("", r.label+(r.check?" is invalid":" is consistent"));
if (r.expects==0) rep.cb("", ", contrary to expectation");
else if (r.expects==1) rep.cb("", ", as expected");
} else if (result.get(i).endsWith(".core")) {
rep.cb("", " #"+(i+1)+": ");
rep.cb("link", r.check?"No counterexample found. ":"No instance found. ", "CORE: "+result.get(i));
rep.cb("", r.label+(r.check?" may be valid":" may be inconsistent"));
if (r.expects==1) rep.cb("", ", contrary to expectation");
else if (r.expects==0) rep.cb("", ", as expected");
} else {
if (r.check) rep.cb("", " #"+(i+1)+": No counterexample found. "+r.label+" may be valid");
else rep.cb("", " #"+(i+1)+": No instance found. "+r.label+" may be inconsistent");
if (r.expects==1) rep.cb("", ", contrary to expectation");
else if (r.expects==0) rep.cb("", ", as expected");
}
rep.cb("", ".\n");
}
rep.cb("", "\n");
}
if (rep.warn>1) rep.cb("bold", "Note: There were "+rep.warn+" compilation warnings. Please scroll up to see them.\n");
if (rep.warn==1) rep.cb("bold", "Note: There was 1 compilation warning. Please scroll up to see it.\n");
}
}
}