// ex: se sts=4 sw=4 expandtab:
/*
* Yeti language compiler java bytecode generator.
*
* Copyright (c) 2007-2013 Madis Janson
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. The name of the author may not be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package yeti.lang.compiler;
import yeti.renamed.asmx.*;
import java.io.*;
import java.util.*;
import java.net.URL;
import java.net.URLClassLoader;
import yeti.lang.Fun;
import yeti.lang.Struct3;
import yeti.lang.Core;
final class Compiler implements Opcodes {
static final int CF_RESOLVE_MODULE = 1;
static final int CF_PRINT_PARSE_TREE = 2;
static final int CF_EVAL = 4;
static final int CF_EVAL_RESOLVE = 8;
static final int CF_EVAL_STORE = 32;
static final int CF_EVAL_BIND = 40;
static final int CF_EXPECT_MODULE = 128;
static final int CF_EXPECT_PROGRAM = 256;
// hack to force getting yetidoc on doc generation
static final int CF_FORCE_COMPILE = 512;
// used with CF_RESOLVE_MODULE on preload
static final int CF_IGNORE_CLASSPATH = 1024;
// global flags
static final int GF_NO_IMPORT = 16;
static final int GF_DOC = 64;
static final String[] PRELOAD =
new String[] { "yeti/lang/std", "yeti/lang/io" };
static final ThreadLocal currentCompiler = new ThreadLocal();
private static ClassLoader JAVAC;
Fun writer;
String depDestDir; // used to read already compiled classes
private Map compiled = new HashMap();
private List warnings = new ArrayList();
private String currentSrc;
private Map definedClasses = new HashMap();
final List postGen = new ArrayList();
boolean isGCJ;
String sourceCharset = "UTF-8";
private String[] sourcePath = {};
Fun customReader;
ClassFinder classPath;
final Map types = new HashMap();
final Map opaqueTypes = new HashMap();
final Map javaTypeCache = new HashMap();
String[] preload = PRELOAD;
int classWriterFlags = ClassWriter.COMPUTE_FRAMES;
int globalFlags;
Compiler() {
// GCJ bytecode verifier is overly strict about INVOKEINTERFACE
isGCJ = System.getProperty("java.vm.name").indexOf("gcj") >= 0;
// isGCJ = true;
}
void warn(CompileException ex) {
ex.fn = currentSrc;
warnings.add(ex);
}
String createClassName(Ctx ctx, String outerClass, String nameBase) {
boolean anon = nameBase == "" && ctx != null;
nameBase = outerClass + '$' + nameBase;
String lower = nameBase.toLowerCase(), name = lower;
int counter = -1;
if (anon)
name = lower + (counter = ctx.constants.anonymousClassCounter);
while (definedClasses.containsKey(name))
name = lower + ++counter;
if (anon)
ctx.constants.anonymousClassCounter = counter + 1;
return counter < 0 ? nameBase : nameBase + counter;
}
public void enumWarns(Fun f) {
for (int i = 0, cnt = warnings.size(); i < cnt; ++i)
f.apply(warnings.get(i));
}
private void generateModuleAccessors(Map fields, Ctx ctx, Map direct) {
if (ctx.compilation.isGCJ)
ctx.typeInsn(CHECKCAST, "yeti/lang/Struct");
for (Iterator i = fields.entrySet().iterator(); i.hasNext();) {
Map.Entry entry = (Map.Entry) i.next();
String name = (String) entry.getKey();
String jname = Code.mangle(name);
String fname = name.equals("eval") ? "eval$" : jname;
String type = Code.javaType((YType) entry.getValue());
String descr = "()L" + type + ';';
Ctx m = ctx.newMethod(ACC_PUBLIC | ACC_STATIC, fname, descr);
Code v = (Code) direct.get(name);
if (v != null) { // constant
v.gen(m);
m.typeInsn(CHECKCAST, type);
} else if (direct.containsKey(name)) { // mutable
m.methodInsn(INVOKESTATIC, ctx.className, "eval",
"()Ljava/lang/Object;");
if (ctx.compilation.isGCJ)
m.typeInsn(CHECKCAST, "yeti/lang/Struct");
m.ldcInsn(name);
m.methodInsn(INVOKEINTERFACE, "yeti/lang/Struct", "get",
"(Ljava/lang/String;)Ljava/lang/Object;");
m.typeInsn(CHECKCAST, type);
} else { // through static field
descr = descr.substring(2);
ctx.cw.visitField(ACC_PRIVATE | ACC_STATIC, jname,
descr, null, null).visitEnd();
ctx.insn(DUP);
ctx.ldcInsn(name);
ctx.methodInsn(INVOKEINTERFACE, "yeti/lang/Struct", "get",
"(Ljava/lang/String;)Ljava/lang/Object;");
ctx.typeInsn(CHECKCAST, type);
ctx.fieldInsn(PUTSTATIC, ctx.className, jname, descr);
genFastInit(m);
m.fieldInsn(GETSTATIC, ctx.className, jname, descr);
}
m.insn(ARETURN);
m.closeMethod();
}
}
String compileAll(String[] sources, int flags, String[] javaArg)
throws Exception {
List java = null;
int i, yetiCount = 0;
for (i = 0; i < sources.length; ++i)
if (sources[i].endsWith(".java")) {
char[] s = readSourceFile(null, sources[i], new YetiAnalyzer());
new JavaSource(sources[i], s, classPath.parsed);
if (java == null) {
java = new ArrayList();
boolean debug = true;
for (int j = 0; j < javaArg.length; ++j) {
if (javaArg[j].startsWith("-g"))
debug = false;
java.add(javaArg[j]);
}
if (!java.contains("-encoding")) {
java.add("-encoding");
java.add("utf-8");
}
if (debug)
java.add("-g");
if (classPath.pathStr.length() != 0) {
java.add("-classpath");
String path = classPath.pathStr;
if (depDestDir != null)
path = path.length() == 0 ? depDestDir
: path + File.pathSeparator + depDestDir;
java.add(path);
}
}
java.add(sources[i]);
} else {
sources[yetiCount++] = sources[i];
}
String mainClass = null;
for (i = 0; i < yetiCount; ++i) {
String className = compile(sources[i], null, flags).name;
if (!types.containsKey(className))
mainClass = className;
}
if (java != null) {
javaArg = (String[]) java.toArray(new String[javaArg.length]);
Class javac = null;
try {
javac = Class.forName("com.sun.tools.javac.Main", true,
getClass().getClassLoader());
} catch (Exception ex) {
}
java.lang.reflect.Method m;
try {
if (javac == null) { // find javac...
synchronized (currentCompiler) {
if (JAVAC == null) {
File f = new File(System.getProperty("java.home"),
"../lib/tools.jar");
if (!f.exists())
f = new File(System.getenv("JAVA_HOME"),
"lib/tools.jar");
JAVAC = new URLClassLoader(
new URL[] { f.toURI().toURL() });
}
}
javac =
Class.forName("com.sun.tools.javac.Main", true, JAVAC);
}
m = javac.getMethod("compile", new Class[] { String[].class });
} catch (Exception ex) {
throw new CompileException(null, "Couldn't find Java compiler");
}
Object o = javac.newInstance();
if (((Integer) m.invoke(o, new Object[] {javaArg})).intValue() != 0)
throw new CompileException(null,
"Error while compiling Java sources");
}
return yetiCount != 0 ? mainClass : "";
}
void setSourcePath(String[] path) throws IOException {
String[] sp = new String[path.length];
for (int i = 0, j, cnt; i < path.length; ++i) {
String s = path[i];
char c = ' '; // check URI
for (j = 0, cnt = s.length(); j < cnt; ++j)
if (((c = s.charAt(j)) < 'a' || c > 'z') &&
(c < '0' || c > '9')) break;
sp[i] = j > 1 && c == ':' ? s : new File(s).getCanonicalPath();
}
sourcePath = sp;
}
private char[] readSourceFile(String parent, String fn,
YetiAnalyzer analyzer) throws IOException {
if (customReader != null) {
Struct3 arg = new Struct3(new String[] { "name", "parent" }, null);
arg._0 = fn;
arg._1 = parent == null ? Core.UNDEF_STR : parent;
String result = (String) customReader.apply(arg);
if (result != Core.UNDEF_STR) {
analyzer.canonicalFile = (String) arg._0;
analyzer.sourceFile = null;
if (compiled.containsKey(analyzer.canonicalFile))
return null;
return result.toCharArray();
}
}
File f = new File(parent, fn);
analyzer.sourceFile = f.getName();
if (parent == null) { // !loadModule
f = f.getCanonicalFile();
analyzer.canonicalFile = f.getPath();
if (compiled.containsKey(analyzer.canonicalFile))
return null;
}
char[] buf = new char[0x8000];
InputStream stream = new FileInputStream(f);
Reader reader = null;
try {
reader = new java.io.InputStreamReader(stream, sourceCharset);
int n, l = 0;
while ((n = reader.read(buf, l, buf.length - l)) >= 0)
if (buf.length - (l += n) < 0x1000) {
char[] tmp = new char[buf.length << 1];
System.arraycopy(buf, 0, tmp, 0, l);
buf = tmp;
}
} finally {
if (reader != null)
reader.close();
else
stream.close();
}
if (parent != null)
analyzer.canonicalFile = f.getCanonicalPath();
analyzer.sourceTime = f.lastModified();
return buf;
}
private void verifyModuleCase(YetiAnalyzer analyzer) {
int l = analyzer.canonicalFile.length() - analyzer.sourceName.length();
if (l < 0)
return;
String cf = analyzer.canonicalFile.substring(l);
if (!analyzer.sourceName.equals(cf) &&
analyzer.sourceName.equalsIgnoreCase(cf))
throw new CompileException(0, 0,
"Module file name case doesn't match the requested name");
}
// if loadModule is true, the file is searched from the source path
private char[] readSource(YetiAnalyzer analyzer) {
try {
if ((analyzer.flags & CF_RESOLVE_MODULE) == 0)
return readSourceFile(null, analyzer.sourceName, analyzer);
// Search from path. The localName is slashed package name.
final String name = analyzer.sourceName;
String fn = analyzer.sourceName = name + ".yeti";
if (sourcePath.length == 0)
throw new IOException("no source path");
int sep = fn.lastIndexOf('/');
for (;;) {
// search _with_ packageName
for (int i = 0; i < sourcePath.length; ++i)
try {
char[] r = readSourceFile(sourcePath[i], fn, analyzer);
analyzer.sourceDir = sourcePath[i];
verifyModuleCase(analyzer);
return r;
} catch (IOException ex) {
}
if (sep != -2 && (analyzer.flags & CF_IGNORE_CLASSPATH) == 0
&& (analyzer.resolvedType = moduleType(name)) != null)
return null;
if (sep <= 0) // no package path, fail
throw new CompileException(0, 0, "Module " +
name.replace('/', '.') + " not found");
fn = fn.substring(sep + 1); // try without package path
sep = -2; // fail next time, without rechecking classpath
}
} catch (IOException e) {
throw new CompileException(0, 0,
analyzer.sourceName + ": " + e.getMessage());
}
}
ModuleType moduleType(String name) throws IOException {
String cname = name.toLowerCase();
long[] lastModified = { -1 };
InputStream in = classPath.findClass(cname + ".class", lastModified);
if (in == null)
return null;
ModuleType t = YetiTypeVisitor.readType(this, in);
if (t != null) {
t.name = cname;
t.lastModified = lastModified[0];
types.put(cname, t);
}
return t;
}
void deriveName(YetiParser.Parser parser, YetiAnalyzer analyzer) {
if ((analyzer.flags & (CF_EVAL | CF_RESOLVE_MODULE)) == CF_EVAL) {
if (parser.moduleName == null)
parser.moduleName = "code";
if (sourcePath.length == 0)
sourcePath = new String[] { new File("").getAbsolutePath() };
return;
}
//System.err.println("Module name before derive: " + parser.moduleName);
// derive or verify the module name
String cf = analyzer.canonicalFile, name = null;
int i, lastlen = -1, l = -1;
i = cf.length() - 5;
if (i > 0 && cf.substring(i).equalsIgnoreCase(".yeti"))
cf = cf.substring(0, i);
else if (parser.isModule)
throw new CompileException(0, 0,
"Yeti module source file must have a .yeti suffix");
boolean ok = parser.moduleName == null;
String shortName = parser.moduleName;
if (shortName != null) {
l = shortName.lastIndexOf('/');
shortName = l > 0 ? shortName.substring(l + 1) : null;
}
String[] path = analyzer.sourceDir == null ? sourcePath :
new String[] { analyzer.sourceDir };
for (i = 0; i < path.length; ++i) {
l = path[i].length();
if (l <= lastlen || cf.length() <= l ||
cf.charAt(l) != File.separatorChar ||
!path[i].equals(cf.substring(0, l)))
continue;
name = cf.substring(l + 1).replace(File.separatorChar, '/');
if (!ok && (name.equalsIgnoreCase(parser.moduleName) ||
name.equalsIgnoreCase(shortName))) {
ok = true;
break;
}
lastlen = l;
}
if (name == null)
name = new File(cf).getName();
//System.err.println("SPATH:" + java.util.Arrays.asList(path) +
// "; cf:" + cf + "; name:" + name + "; shortName:" + shortName +
// "; lastlen:" + lastlen);
if (!ok && (lastlen != -1 || !name.equalsIgnoreCase(shortName) &&
!name.equalsIgnoreCase(parser.moduleName)))
throw new CompileException(parser.moduleNameLine, 0,
(parser.isModule ? "module " : "program ") +
parser.moduleName.replace('/', '.') +
" is not allowed to be in file named '" +
analyzer.canonicalFile + "'");
if (parser.moduleName != null)
name = parser.moduleName;
parser.moduleName = parser.isModule ? name.toLowerCase() : name;
//System.err.println("Derived module name: " + parser.moduleName);
/* Derive the source path IMPLICITLY as a single directory:
* + If the the canonical path ends with /foo/bar/baz(.yeti) matching
* the module/program NAME foo.bar.baz (case insensitive),
* the SOURCEPATH is the preceding part of the canonical path.
* + Otherwise the SOURCEPATH is the directory of source file. */
if (sourcePath.length == 0) {
l = cf.length() - (name.length() + 1);
if (l >= 0) {
name = cf.substring(l)
.replace(File.separatorChar, '/');
if (l == 0)
l = 1;
if (name.charAt(0) != '/' ||
!name.substring(1).equalsIgnoreCase(parser.moduleName))
l = -1;
}
name = l < 0 ? new File(cf).getParent() : cf.substring(0, l);
if (name == null)
name = new File("").getAbsolutePath();
sourcePath = new String[] { name };
}
name = parser.moduleName.toLowerCase();
if (definedClasses.containsKey(name))
throw new CompileException(0, 0, (definedClasses.get(name) == null
? "Circular module dependency: "
: "Duplicate module name: ") + name.replace('/', '.'));
if (depDestDir != null && (analyzer.flags & CF_FORCE_COMPILE) == 0) {
analyzer.targetFile =
new File(depDestDir, parser.moduleName.concat(".class"));
analyzer.targetTime = analyzer.targetFile.lastModified();
}
}
ModuleType compile(String sourceName, char[] code, int flags)
throws Exception {
YetiAnalyzer anal = new YetiAnalyzer();
anal.flags = flags;
anal.compiler = this;
anal.sourceName = sourceName;
if (code == null) {
code = readSource(anal);
if (code == null)
return anal.resolvedType != null ? anal.resolvedType :
(ModuleType) compiled.get(anal.canonicalFile);
}
RootClosure codeTree;
Object oldCompiler = currentCompiler.get();
currentCompiler.set(this);
String oldCurrentSrc = currentSrc;
currentSrc = anal.sourceName;
try {
try {
anal.preload = preload;
codeTree = anal.toCode(code);
if (codeTree == null) {
ModuleType t = anal.resolvedType;
if (t == null) { // module, type from class
t = YetiTypeVisitor.readType(this,
new FileInputStream(anal.targetFile));
types.put(t.name, t);
t.topDoc = anal.topDoc;
}
t.lastModified = anal.targetTime;
t.hasSource = true;
compiled.put(anal.canonicalFile, t);
//System.err.println(t.name + " already compiled.");
return t;
}
} finally {
currentCompiler.set(oldCompiler);
}
final String name = codeTree.moduleType.name;
if (name == null)
throw new CompileException(0, 0,
"internal error: module/program name undefined");
ModuleType exists = (ModuleType) types.get(name);
// If source set has multiple modules with same name (for some
// crazy reason), it would be useful to use just one.
if (exists != null && (flags & CF_FORCE_COMPILE) == 0)
return exists;
if (codeTree.isModule)
types.put(name, codeTree.moduleType);
if (writer != null)
generateCode(anal, codeTree);
compiled.put(anal.canonicalFile, codeTree.moduleType);
classPath.existsCache.clear();
currentSrc = oldCurrentSrc;
return codeTree.moduleType;
} catch (CompileException ex) {
if (ex.fn == null)
ex.fn = anal.sourceName;
throw ex;
}
}
private void generateCode(YetiAnalyzer anal, RootClosure codeTree)
throws Exception {
String name = codeTree.moduleType.name;
Constants constants = new Constants(anal.sourceName, anal.sourceFile);
Ctx ctx = new Ctx(this, constants, null, null).newClass(ACC_PUBLIC |
ACC_SUPER | (codeTree.isModule && codeTree.moduleType.deprecated
? ACC_DEPRECATED : 0), name, (anal.flags & CF_EVAL) != 0
? "yeti/lang/Fun" : null, null, codeTree.line);
constants.ctx = ctx;
if (codeTree.isModule) {
moduleEval(codeTree, ctx, name);
} else if ((anal.flags & CF_EVAL) != 0) {
ctx.createInit(ACC_PUBLIC, "yeti/lang/Fun");
ctx = ctx.newMethod(ACC_PUBLIC, "apply",
"(Ljava/lang/Object;)Ljava/lang/Object;");
codeTree.gen(ctx);
ctx.insn(ARETURN);
ctx.closeMethod();
} else {
ctx = ctx.newMethod(ACC_PUBLIC | ACC_STATIC, "main",
"([Ljava/lang/String;)V");
ctx.localVarCount++;
ctx.load(0).methodInsn(INVOKESTATIC, "yeti/lang/Core",
"setArgv", "([Ljava/lang/String;)V");
Label codeStart = new Label();
ctx.visitLabel(codeStart);
codeTree.gen(ctx);
ctx.insn(POP);
ctx.insn(RETURN);
Label exitStart = new Label();
ctx.tryCatchBlock(codeStart, exitStart, exitStart,
"yeti/lang/ExitError");
ctx.visitLabel(exitStart);
ctx.methodInsn(INVOKEVIRTUAL, "yeti/lang/ExitError",
"getExitCode", "()I");
ctx.methodInsn(INVOKESTATIC, "java/lang/System", "exit", "(I)V");
ctx.insn(RETURN);
ctx.closeMethod();
}
constants.close();
write(constants.unstoredClasses);
}
private void moduleEval(RootClosure codeTree, Ctx mctx, String name) {
mctx.cw.visitField(ACC_PRIVATE | ACC_STATIC, "$",
"Ljava/lang/Object;", null, null).visitEnd();
mctx.cw.visitField(ACC_PRIVATE | ACC_STATIC | ACC_VOLATILE,
"_$", "I", null, null);
Ctx ctx = mctx.newMethod(ACC_PUBLIC | ACC_STATIC | ACC_SYNCHRONIZED,
"eval", "()Ljava/lang/Object;");
ctx.fieldInsn(GETSTATIC, name, "_$", "I");
Label eval = new Label();
ctx.jumpInsn(IFLE, eval);
ctx.fieldInsn(GETSTATIC, name, "$", "Ljava/lang/Object;");
ctx.insn(ARETURN);
ctx.visitLabel(eval);
ctx.intConst(-1); // mark in eval
ctx.fieldInsn(PUTSTATIC, name, "_$", "I");
Code codeTail = codeTree.body;
while (codeTail instanceof SeqExpr)
codeTail = ((SeqExpr) codeTail).result;
Map direct = Collections.EMPTY_MAP;
if (codeTail instanceof StructConstructor) {
((StructConstructor) codeTail).publish();
codeTree.gen(ctx);
direct = ((StructConstructor) codeTail).getDirect();
} else {
codeTree.gen(ctx);
}
ctx.cw.visitAttribute(new TypeAttr(codeTree.moduleType, this));
if (codeTree.type.type == YetiType.STRUCT)
generateModuleAccessors(codeTree.type.allowedMembers, ctx, direct);
ctx.insn(DUP);
ctx.fieldInsn(PUTSTATIC, name, "$", "Ljava/lang/Object;");
ctx.intConst(1);
ctx.fieldInsn(PUTSTATIC, name, "_$", "I");
ctx.insn(ARETURN);
ctx.closeMethod();
ctx = mctx.newMethod(ACC_PUBLIC | ACC_STATIC, "init", "()V");
genFastInit(ctx);
ctx.insn(RETURN);
ctx.closeMethod();
}
private void genFastInit(Ctx ctx) {
ctx.fieldInsn(GETSTATIC, ctx.className, "_$", "I");
Label ret = new Label();
ctx.jumpInsn(IFNE, ret);
ctx.methodInsn(INVOKESTATIC, ctx.className,
"eval", "()Ljava/lang/Object;");
ctx.insn(POP);
ctx.visitLabel(ret);
}
void addClass(String name, Ctx ctx, int line) {
if (definedClasses.put(name.toLowerCase(), ctx) != null) {
throw new CompileException(line, 0,
"Duplicate class: " + name.replace('/', '.'));
}
if (ctx != null)
ctx.constants.unstoredClasses.add(ctx);
}
private void write(List unstoredClasses) throws Exception {
if (writer == null)
return;
int i, cnt = postGen.size();
for (i = 0; i < cnt; ++i)
((Runnable) postGen.get(i)).run();
postGen.clear();
cnt = unstoredClasses.size();
for (i = 0; i < cnt; ++i) {
Ctx c = (Ctx) unstoredClasses.get(i);
definedClasses.put(c.className.toLowerCase(), "");
String name = c.className + ".class";
byte[] content = c.cw.toByteArray();
writer.apply(name, content);
classPath.define(name, content);
}
}
}
final class YClassWriter extends ClassWriter {
YClassWriter(int flags) {
super(COMPUTE_MAXS | flags);
}
// Overload to avoid using reflection on non-standard-library classes
protected String getCommonSuperClass(String type1, String type2) {
if (type1.equals(type2))
return type1;
if (type1.startsWith("java/lang/") && type2.startsWith("java/lang/") ||
type1.startsWith("yeti/lang/") && type2.startsWith("yeti/lang/"))
return super.getCommonSuperClass(type1, type2);
return "java/lang/Object";
}
}