/* 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.alloy4compiler.parser;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
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.Err;
import edu.mit.csail.sdg.alloy4.ErrorFatal;
import edu.mit.csail.sdg.alloy4.ErrorSyntax;
import edu.mit.csail.sdg.alloy4.Pos;
import edu.mit.csail.sdg.alloy4.Util;
import edu.mit.csail.sdg.alloy4compiler.ast.Command;
import edu.mit.csail.sdg.alloy4compiler.ast.Expr;
import edu.mit.csail.sdg.alloy4compiler.ast.Module;
import edu.mit.csail.sdg.alloy4compiler.parser.CompModule.Open;
/** This class provides convenience methods for calling the parser and the compiler. */
public final class CompUtil {
/** Constructor is private, since this class never needs to be instantiated. */
private CompUtil() { }
//=============================================================================================================//
/** Go up the directory hierachy 0 or more times.
* <br> For example, on a UNIX machine, goUp("/home/abc/def",1) will return "/home/abc"
* <br> For example, on a UNIX machine, goUp("/home/abc/def",2) will return "/home"
* @param filepath - this must be an absolute path
* @param numberOfSteps - the number of steps to go up
*/
private static String up(String filepath, int numberOfSteps) {
while(numberOfSteps > 0) {
numberOfSteps--;
int i=filepath.lastIndexOf(File.separatorChar);
if (i<=0) return "";
filepath=filepath.substring(0,i);
}
return filepath;
}
//=============================================================================================================//
/** Given the name of a module, and the filename for that module, compute the filename for another module
* @param moduleA - must be a legal Alloy modulepath (eg. name) (eg. name/name/name) (must not start or end in '/')
* @param fileA - the filename corresponding to moduleA
* @param moduleB - must be a legal Alloy modulepath (eg. name) (eg. name/name/name) (must not start or end in '/')
* @return the filename corresponding to moduleB
*/
private static String computeModulePath(String moduleA, String fileA, String moduleB) {
fileA=Util.canon(fileA); // Make sure it's a canonical absolute path
if (moduleA.length()==0) moduleA="anything"; // Harmonizes the boundary case
while(moduleA.length()>0 && moduleB.length()>0) {
int a=moduleA.indexOf('/'), b=moduleB.indexOf('/');
String headOfA = (a>=0) ? moduleA.substring(0,a) : moduleA;
String headOfB = (b>=0) ? moduleB.substring(0,b) : moduleB;
if (!headOfA.equals(headOfB) || a<0 || b<0) {
// eg. util/boolean==/home/models/util/boolean.als, then test=>/home/models/test.als"
// eg. util/boolean==/home/models/util/boolean.als, then sub/test=>/home/models/sub/test.als
// eg. main==/home/models/main.als, then test=>/home/models/test.als
// eg. main==/home/models/main.als, then sub/test=>/home/models/sub/test.als"
int numberOfSlash=0;
for(int i=0; i<moduleA.length(); i++) if (moduleA.charAt(i)=='/') numberOfSlash++;
return up(fileA, numberOfSlash+1)+File.separatorChar+moduleB.replace('/',File.separatorChar)+".als";
}
moduleA=moduleA.substring(a+1);
moduleB=moduleB.substring(b+1);
}
return ""; // This shouldn't happen, since there should always be some character after '/' in the module name
}
//=============================================================================================================//
/** Helper method that recursively parse a file and all its included subfiles
* @param loaded - this stores the text files we've loaded while parsing; cannot be null
* @param fc - if a file cannot be found, we consult this cache first before attempting to load it from disk/jar; cannot be null
* @param pos - the position of the "open" statement
* @param filename - the filename to open
* @param root - the root module (this field is ignored if prefix=="")
* @param prefix - the prefix for the file we are about to parse
* @param thispath - the set of filenames involved in the current chain_of_file_opening
*/
private static CompModule parseRecursively
(List<Object> seenDollar, Map<String,String> loaded, Map<String,String> fc, Pos pos, String filename, CompModule root, String prefix, Set<String> thispath, int initialResolution)
throws Err, FileNotFoundException, IOException {
// Add the filename into a ArrayList, so that we can detect cycles in the module import graph
// How? I'll argue that (filename appears > 1 time along a chain) <=> (infinite loop in the import graph)
// => As you descend down the chain via OPEN, if you see the same FILE twice, then
// you will go into an infinite loop (since, regardless of the instantiating parameter,
// that file will attempt to OPEN the exact same set of files. leading back to itself, etc. etc.)
// <= If there is an infinite loop, that means there is at least 1 infinite chain of OPEN (from root).
// Since the number of files is finite, at least 1 filename will be repeated.
if (thispath.contains(filename))
throw new ErrorSyntax(pos,
"Circular dependency in module import. The file \""+(new File(filename)).getName()+"\" is imported infinitely often.");
thispath.add(filename);
// No cycle detected so far. So now we parse the file.
CompModule u = CompParser.alloy_parseStream(seenDollar, loaded, fc, root, 0, filename, prefix, initialResolution);
if (prefix.length()==0) root = u;
// Here, we recursively open the included files
for(Open x: u.getOpens()) {
String cp=Util.canon(computeModulePath(u.getModelName(), filename, x.filename)), content=fc.get(cp);
try {
if (content==null) { content=loaded.get(cp); }
if (content==null) { content=fc.get(x.filename); if (content!=null) cp=x.filename; }
if (content==null) { content=loaded.get(x.filename); if (content!=null) cp=x.filename; }
if (content==null) { content=Util.readAll(cp); }
} catch(IOException ex1) {
try {
String newCp = (Util.jarPrefix()+"models/"+x.filename+".als").replace('/', File.separatorChar);
content = Util.readAll(newCp);
cp = newCp;
} catch(IOException ex) {
throw new ErrorSyntax(x.pos,
"This module cannot be found.\nIt is not a built-in library module, and it cannot be found at \""+cp+"\".\n");
}
}
loaded.put(cp, content);
CompModule y = parseRecursively(seenDollar, loaded, fc, x.pos, cp, root, (prefix.length()==0 ? x.alias : prefix+"/"+x.alias), thispath, initialResolution);
x.connect(y);
}
thispath.remove(filename); // Remove this file from the CYCLE DETECTION LIST.
return u;
}
//=============================================================================================================//
/** Parses 1 module from the input string (without loading any subfiles)
* @return an array of 0 or more Command if no error occurred
*/
public static ConstList<Command> parseOneModule_fromString(String content) throws Err {
try {
Map<String,String> fc = new LinkedHashMap<String,String>();
fc.put("", content);
CompModule u=CompParser.alloy_parseStream(new ArrayList<Object>(), null, fc, null, 0, "", "", 1);
return ConstList.make(u.getAllCommands());
} catch(IOException ex) {
throw new ErrorFatal("IOException occurred: "+ex.getMessage(), ex);
} catch(Throwable ex) {
if (ex instanceof Err) throw (Err)ex; else throw new ErrorFatal("Unknown exception occurred: "+ex, ex);
}
}
//=============================================================================================================//
/** Parses 1 module from the file (without loading any subfiles)
* @return an array of 0 or more Command if no error occurred
*/
public static ConstList<Command> parseOneModule_fromFile(String filename) throws Err {
try {
CompModule u = CompParser.alloy_parseStream(new ArrayList<Object>(), null, null, null, 0, filename, "", 1);
return ConstList.make(u.getAllCommands());
} catch(IOException ex) {
throw new ErrorFatal("IOException occurred: "+ex.getMessage(), ex);
} catch(Throwable ex) {
if (ex instanceof Err) throw (Err)ex; else throw new ErrorFatal("Unknown exception occurred: "+ex, ex);
}
}
//=============================================================================================================//
/** Parses then typecheck the given input String as an Alloy expression from that world
* @return the fully-typechecked expression if no error occurred
* @throws Err if world==null or if any other error occurred
*/
public static Expr parseOneExpression_fromString (Module world, String input) throws Err {
try {
if (world==null) throw new ErrorFatal("Cannot parse an expression with null world.");
return world.parseOneExpressionFromString(input);
} catch(IOException ex) {
throw new ErrorFatal("IOException occurred: "+ex.getMessage(), ex);
} catch(Throwable ex) {
if (ex instanceof Err) throw (Err)ex; else throw new ErrorFatal("Unknown exception occurred: "+ex, ex);
}
}
//=============================================================================================================//
/** Read everything from "file" and parse it; if it mentions submodules, open them and parse them too.
* @param rep - if nonnull, we will report compilation progress messages to it
* @param loaded - a cache of files that have been pre-fetched (can be null if there were no prefetching)
* @param filename - the main module we are parsing
* @return the root Module which contains pointers to all submodules
* @throws Err if an error occurred
* <p> And if loaded!=null, it will contain all the files needed for this parse, and furthermore, other entries will be deleted.
*/
public static CompModule parseEverything_fromFile (A4Reporter rep, Map<String,String> loaded, String filename) throws Err {
try {
filename = Util.canon(filename);
Set<String> thispath = new LinkedHashSet<String>();
if (loaded==null) loaded = new LinkedHashMap<String,String>();
Map<String,String> fc = new LinkedHashMap<String,String>(loaded);
loaded.clear();
List<Object> seenDollar = new ArrayList<Object>();
CompModule root = parseRecursively(seenDollar, loaded, fc, new Pos(filename,1,1), filename, null, "", thispath, 1);
root.seenDollar = seenDollar.size()>0;
return CompModule.resolveAll(rep==null ? A4Reporter.NOP : rep, root);
} catch(FileNotFoundException ex) {
throw new ErrorSyntax("File cannot be found.\n"+ex.getMessage(), ex);
} catch(IOException ex) {
throw new ErrorFatal("IOException occurred: "+ex.getMessage(), ex);
} catch(Throwable ex) {
if (ex instanceof Err) throw (Err)ex; else throw new ErrorFatal("Unknown exception occurred: "+ex, ex);
}
}
/** Read everything from "file" and parse it; if it mentions submodules, open them and parse them too.
* @param rep - if nonnull, we will report compilation progress messages to it
* @param loaded - a cache of files that have been pre-fetched (can be null if there were no prefetching)
* @param filename - the main module we are parsing
* @param initialResolutionMode - use 1 for the historical behavior, and 2 for Alloy 4.2's new "universal implicit this" name resolution behavior
* @return the root CompModule which contains pointers to all submodules
* @throws Err if an error occurred
* <p> And if loaded!=null, it will contain all the files needed for this parse, and furthermore, other entries will be deleted.
*/
public static CompModule parseEverything_fromFile (A4Reporter rep, Map<String,String> loaded, String filename, int initialResolutionMode) throws Err {
try {
filename = Util.canon(filename);
Set<String> thispath = new LinkedHashSet<String>();
if (loaded==null) loaded = new LinkedHashMap<String,String>();
Map<String,String> fc = new LinkedHashMap<String,String>(loaded);
loaded.clear();
List<Object> seenDollar = new ArrayList<Object>();
CompModule root = parseRecursively(seenDollar, loaded, fc, new Pos(filename,1,1), filename, null, "", thispath, initialResolutionMode);
root.seenDollar = seenDollar.size()>0;
return CompModule.resolveAll(rep==null ? A4Reporter.NOP : rep, root);
} catch(FileNotFoundException ex) {
throw new ErrorSyntax("File cannot be found.\n"+ex.getMessage(), ex);
} catch(IOException ex) {
throw new ErrorFatal("IOException occurred: "+ex.getMessage(), ex);
} catch(Throwable ex) {
if (ex instanceof Err) throw (Err)ex; else throw new ErrorFatal("Unknown exception occurred: "+ex, ex);
}
}
}