changeset 775:1a68fc55a80c

simplify dir structure
author Franklin Schmidt <fschmidt@gmail.com>
date Fri, 26 Aug 2016 14:36:40 -0600
parents 3e30cf310e56
children 815c119dac7a
files core/src/luan/DeepCloneable.java core/src/luan/DeepCloner.java core/src/luan/Luan.java core/src/luan/LuanException.java core/src/luan/LuanFunction.java core/src/luan/LuanJava.java core/src/luan/LuanJavaFunction.java core/src/luan/LuanMeta.java core/src/luan/LuanMethod.java core/src/luan/LuanPropertyMeta.java core/src/luan/LuanRuntimeException.java core/src/luan/LuanState.java core/src/luan/LuanTable.java core/src/luan/cmd_line.luan core/src/luan/impl/Closure.java core/src/luan/impl/LuanCompiler.java core/src/luan/impl/LuanImpl.java core/src/luan/impl/LuanJavaCompiler.java core/src/luan/impl/LuanParser.java core/src/luan/impl/ParseException.java core/src/luan/impl/Parser.java core/src/luan/impl/Pointer.java core/src/luan/impl/TableField.java core/src/luan/modules/BasicLuan.java core/src/luan/modules/Binary.luan core/src/luan/modules/BinaryLuan.java core/src/luan/modules/Html.luan core/src/luan/modules/HtmlLuan.java core/src/luan/modules/Io.luan core/src/luan/modules/IoLuan.java core/src/luan/modules/JavaLuan.java core/src/luan/modules/Luan.luan core/src/luan/modules/Math.luan core/src/luan/modules/MathLuan.java core/src/luan/modules/Number.luan core/src/luan/modules/Package.luan core/src/luan/modules/PackageLuan.java core/src/luan/modules/Parsers.luan core/src/luan/modules/Rpc.luan core/src/luan/modules/RpcLuan.java core/src/luan/modules/String.luan core/src/luan/modules/StringLuan.java core/src/luan/modules/Table.luan core/src/luan/modules/TableLuan.java core/src/luan/modules/Thread.luan core/src/luan/modules/ThreadLuan.java core/src/luan/modules/Time.luan core/src/luan/modules/Utils.java core/src/luan/modules/Which_mod.luan core/src/luan/modules/host/Hosting.luan core/src/luan/modules/host/backup.luan core/src/luan/modules/host/delete.luan core/src/luan/modules/host/push.luan core/src/luan/modules/host/restore.luan core/src/luan/modules/luan_to_java.luan core/src/luan/modules/mmake.luan core/src/luan/modules/parsers/BBCode.java core/src/luan/modules/parsers/Csv.java core/src/luan/modules/parsers/Html.java core/src/luan/modules/parsers/Json.java core/src/luan/modules/parsers/ParseException.java core/src/luan/modules/parsers/Parser.java core/src/luan/modules/parsers/Theme.java core/src/luan/modules/theme_to_luan.luan core/src/luan/modules/url/LuanUrl.java core/src/luan/modules/url/MultiPartOutputStream.java core/src/luan/modules/url/MultipartClient.java core/src/luan/modules/url/UrlCall.java core/src/luan/modules/which.luan http/ext/jetty-continuation-8.1.15.v20140411.jar http/ext/jetty-http-8.1.15.v20140411.jar http/ext/jetty-io-8.1.15.v20140411.jar http/ext/jetty-server-8.1.15.v20140411.jar http/ext/jetty-util-8.1.15.v20140411.jar http/ext/servlet-api-3.0.jar http/src/luan/modules/http/AuthenticationHandler.java http/src/luan/modules/http/Dump_mod.luan http/src/luan/modules/http/Http.luan http/src/luan/modules/http/HttpServicer.java http/src/luan/modules/http/Http_test.luan http/src/luan/modules/http/LuanHandler.java http/src/luan/modules/http/NotFound.java http/src/luan/modules/http/Server.luan http/src/luan/modules/http/Shell_mod.luan http/src/luan/modules/http/dump.luan http/src/luan/modules/http/run.luan http/src/luan/modules/http/serve.luan http/src/luan/modules/http/shell.luan http/src/luan/modules/http/test.luan lib/javax.mail.jar lib/jetty-continuation-8.1.15.v20140411.jar lib/jetty-http-8.1.15.v20140411.jar lib/jetty-io-8.1.15.v20140411.jar lib/jetty-server-8.1.15.v20140411.jar lib/jetty-util-8.1.15.v20140411.jar lib/log4j-1.2.17.jar lib/lucene-analyzers-common-4.9.0.jar lib/lucene-core-4.9.0.jar lib/lucene-highlighter-4.9.0.jar lib/lucene-memory-4.9.0.jar lib/lucene-queries-4.9.0.jar lib/servlet-api-3.0.jar lib/slf4j-api-1.6.4.jar lib/slf4j-log4j12-1.6.4.jar logging/ext/log4j-1.2.17.jar logging/ext/slf4j-api-1.6.4.jar logging/ext/slf4j-log4j12-1.6.4.jar logging/src/luan/modules/logging/Logging.luan logging/src/luan/modules/logging/LuanLogger.java logging/src/luan/modules/logging/init.luan lucene/ext/lucene-analyzers-common-4.9.0.jar lucene/ext/lucene-core-4.9.0.jar lucene/ext/lucene-highlighter-4.9.0.jar lucene/ext/lucene-memory-4.9.0.jar lucene/ext/lucene-queries-4.9.0.jar lucene/src/luan/modules/lucene/Ab_testing.luan lucene/src/luan/modules/lucene/Lucene.luan lucene/src/luan/modules/lucene/LuceneIndex.java lucene/src/luan/modules/lucene/Versioning.luan lucene/src/luan/modules/lucene/Web_search.luan lucene/src/luan/modules/lucene/queryparser/FieldParser.java lucene/src/luan/modules/lucene/queryparser/MultiFieldParser.java lucene/src/luan/modules/lucene/queryparser/NumberFieldParser.java lucene/src/luan/modules/lucene/queryparser/ParseException.java lucene/src/luan/modules/lucene/queryparser/Parser.java lucene/src/luan/modules/lucene/queryparser/SaneQueryParser.java lucene/src/luan/modules/lucene/queryparser/StringFieldParser.java lucene/src/luan/modules/lucene/queryparser/SynonymParser.java mail/ext/javax.mail.jar mail/src/luan/modules/mail/Mail.luan mail/src/luan/modules/mail/SmtpCon.java scripts/build-luan.sh scripts/cp-luan src/luan/DeepCloneable.java src/luan/DeepCloner.java src/luan/Luan.java src/luan/LuanException.java src/luan/LuanFunction.java src/luan/LuanJava.java src/luan/LuanJavaFunction.java src/luan/LuanMeta.java src/luan/LuanMethod.java src/luan/LuanPropertyMeta.java src/luan/LuanRuntimeException.java src/luan/LuanState.java src/luan/LuanTable.java src/luan/cmd_line.luan src/luan/impl/Closure.java src/luan/impl/LuanCompiler.java src/luan/impl/LuanImpl.java src/luan/impl/LuanJavaCompiler.java src/luan/impl/LuanParser.java src/luan/impl/ParseException.java src/luan/impl/Parser.java src/luan/impl/Pointer.java src/luan/impl/TableField.java src/luan/modules/BasicLuan.java src/luan/modules/Binary.luan src/luan/modules/BinaryLuan.java src/luan/modules/Html.luan src/luan/modules/HtmlLuan.java src/luan/modules/Io.luan src/luan/modules/IoLuan.java src/luan/modules/JavaLuan.java src/luan/modules/Luan.luan src/luan/modules/Math.luan src/luan/modules/MathLuan.java src/luan/modules/Number.luan src/luan/modules/Package.luan src/luan/modules/PackageLuan.java src/luan/modules/Parsers.luan src/luan/modules/Rpc.luan src/luan/modules/RpcLuan.java src/luan/modules/String.luan src/luan/modules/StringLuan.java src/luan/modules/Table.luan src/luan/modules/TableLuan.java src/luan/modules/Thread.luan src/luan/modules/ThreadLuan.java src/luan/modules/Time.luan src/luan/modules/Utils.java src/luan/modules/Which_mod.luan src/luan/modules/host/Hosting.luan src/luan/modules/host/backup.luan src/luan/modules/host/delete.luan src/luan/modules/host/push.luan src/luan/modules/host/restore.luan src/luan/modules/http/AuthenticationHandler.java src/luan/modules/http/Dump_mod.luan src/luan/modules/http/Http.luan src/luan/modules/http/HttpServicer.java src/luan/modules/http/Http_test.luan src/luan/modules/http/LuanHandler.java src/luan/modules/http/NotFound.java src/luan/modules/http/Server.luan src/luan/modules/http/Shell_mod.luan src/luan/modules/http/dump.luan src/luan/modules/http/run.luan src/luan/modules/http/serve.luan src/luan/modules/http/shell.luan src/luan/modules/http/test.luan src/luan/modules/logging/Logging.luan src/luan/modules/logging/LuanLogger.java src/luan/modules/logging/init.luan src/luan/modules/luan_to_java.luan src/luan/modules/lucene/Ab_testing.luan src/luan/modules/lucene/Lucene.luan src/luan/modules/lucene/LuceneIndex.java src/luan/modules/lucene/Versioning.luan src/luan/modules/lucene/Web_search.luan src/luan/modules/lucene/queryparser/FieldParser.java src/luan/modules/lucene/queryparser/MultiFieldParser.java src/luan/modules/lucene/queryparser/NumberFieldParser.java src/luan/modules/lucene/queryparser/ParseException.java src/luan/modules/lucene/queryparser/Parser.java src/luan/modules/lucene/queryparser/SaneQueryParser.java src/luan/modules/lucene/queryparser/StringFieldParser.java src/luan/modules/lucene/queryparser/SynonymParser.java src/luan/modules/mail/Mail.luan src/luan/modules/mail/SmtpCon.java src/luan/modules/mmake.luan src/luan/modules/parsers/BBCode.java src/luan/modules/parsers/Csv.java src/luan/modules/parsers/Html.java src/luan/modules/parsers/Json.java src/luan/modules/parsers/ParseException.java src/luan/modules/parsers/Parser.java src/luan/modules/parsers/Theme.java src/luan/modules/theme_to_luan.luan src/luan/modules/url/LuanUrl.java src/luan/modules/url/MultiPartOutputStream.java src/luan/modules/url/MultipartClient.java src/luan/modules/url/UrlCall.java src/luan/modules/which.luan
diffstat 234 files changed, 13958 insertions(+), 14007 deletions(-) [+]
line wrap: on
line diff
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/DeepCloneable.java
--- a/core/src/luan/DeepCloneable.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,7 +0,0 @@
-package luan;
-
-
-public interface DeepCloneable {
-	public DeepCloneable shallowClone();
-	public void deepenClone(DeepCloneable clone,DeepCloner cloner);
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/DeepCloner.java
--- a/core/src/luan/DeepCloner.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,60 +0,0 @@
-package luan;
-
-import java.util.Map;
-import java.util.HashMap;
-import java.util.IdentityHashMap;
-
-
-public final class DeepCloner {
-	private final Map cloned = new IdentityHashMap();
-
-	public DeepCloneable deepClone(DeepCloneable obj) {
-		if( obj==null )
-			return null;
-		DeepCloneable rtn = (DeepCloneable)cloned.get(obj);
-		if( rtn == null ) {
-			rtn = obj.shallowClone();
-			cloned.put(obj,rtn);
-			obj.deepenClone(rtn,this);
-		}
-		return rtn;
-	}
-
-	public Object[] deepClone(Object[] obj) {
-		if( obj.length == 0 )
-			return obj;
-		Object[] rtn = (Object[])cloned.get(obj);
-		if( rtn == null ) {
-			rtn = obj.clone();
-			cloned.put(obj,rtn);
-			for( int i=0; i<rtn.length; i++ ) {
-				rtn[i] = get(rtn[i]);
-			}
-		}
-		return rtn;
-	}
-
-	public Map deepClone(Map obj) {
-		if( !obj.getClass().equals(HashMap.class) )
-			throw new RuntimeException("can only clone HashMap");
-		Map rtn = (Map)cloned.get(obj);
-		if( rtn == null ) {
-			rtn = new HashMap();
-			for( Object stupid : obj.entrySet() ) {
-				Map.Entry entry = (Map.Entry)stupid;
-				rtn.put( get(entry.getKey()), get(entry.getValue()) );
-			}
-		}
-		return rtn;
-	}
-
-	public Object get(Object obj) {
-		if( obj instanceof DeepCloneable )
-			return deepClone((DeepCloneable)obj);
-		if( obj instanceof Object[] )
-			return deepClone((Object[])obj);
-		if( obj instanceof Map )
-			return deepClone((Map)obj);
-		return obj;
-	}
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/Luan.java
--- a/core/src/luan/Luan.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,157 +0,0 @@
-package luan;
-
-import java.util.List;
-import luan.modules.BasicLuan;
-import luan.impl.LuanCompiler;
-
-
-public final class Luan {
-
-	public static void main(String[] args) throws LuanException {
-		LuanState luan = new LuanState();
-		LuanFunction standalone = (LuanFunction)BasicLuan.load_file(luan,"classpath:luan/cmd_line.luan");
-		standalone.call(luan,args);
-	}
-
-	public static Object first(Object obj) {
-		if( !(obj instanceof Object[]) )
-			return obj;
-		Object[] a = (Object[])obj;
-		return a.length==0 ? null : a[0];
-	}
-
-	public static Object[] array(Object obj) {
-		return obj instanceof Object[] ? (Object[])obj : new Object[]{obj};
-	}
-
-	public static String type(Object obj) {
-		if( obj == null )
-			return "nil";
-		if( obj instanceof String )
-			return "string";
-		if( obj instanceof Boolean )
-			return "boolean";
-		if( obj instanceof Number )
-			return "number";
-		if( obj instanceof LuanTable )
-			return "table";
-		if( obj instanceof LuanFunction )
-			return "function";
-		if( obj instanceof byte[] )
-			return "binary";
-		return "java";
-	}
-
-	public static String toString(Number n) {
-		if( n instanceof Integer )
-			return n.toString();
-		int i = n.intValue();
-		if( i == n.doubleValue() )
-			return Integer.toString(i);
-		String s = n.toString();
-		int iE = s.indexOf('E');
-		String ending  = null;
-		if( iE != -1 ) {
-			ending = s.substring(iE);
-			s = s.substring(0,iE);
-		}
-		if( s.endsWith(".0") )
-			s = s.substring(0,s.length()-2);
-		if( ending != null )
-			s += ending;
-		return s;
-	}
-
-	public static Integer asInteger(Object obj) {
-		if( obj instanceof Integer )
-			return (Integer)obj;
-		if( !(obj instanceof Number) )
-			return null;
-		Number n = (Number)obj;
-		int i = n.intValue();
-		return i==n.doubleValue() ? Integer.valueOf(i) : null;
-	}
-
-	public static String stringEncode(String s) {
-		s = s.replace("\\","\\\\");
-		s = s.replace("\u0007","\\a");
-		s = s.replace("\b","\\b");
-		s = s.replace("\f","\\f");
-		s = s.replace("\n","\\n");
-		s = s.replace("\r","\\r");
-		s = s.replace("\t","\\t");
-		s = s.replace("\u000b","\\v");
-		s = s.replace("\"","\\\"");
-		s = s.replace("\'","\\'");
-		return s;
-	}
-
-
-	// from LuanState
-
-	public static Boolean checkBoolean(Object obj) throws LuanException {
-		if( obj instanceof Boolean )
-			return (Boolean)obj;
-		throw new LuanException("attempt to use a " + Luan.type(obj) + " value as a boolean" );
-	}
-
-	public static String checkString(Object obj) throws LuanException {
-		if( obj instanceof String )
-			return (String)obj;
-		throw new LuanException("attempt to use a " + Luan.type(obj) + " value as a string" );
-	}
-
-	public static LuanFunction checkFunction(Object obj) throws LuanException {
-		if( obj instanceof LuanFunction )
-			return (LuanFunction)obj;
-		throw new LuanException("attempt to call a " + Luan.type(obj) + " value" );
-	}
-
-	public static boolean isLessThan(LuanState luan,Object o1,Object o2) throws LuanException {
-		if( o1 instanceof Number && o2 instanceof Number ) {
-			Number n1 = (Number)o1;
-			Number n2 = (Number)o2;
-			return n1.doubleValue() < n2.doubleValue();
-		}
-		if( o1 instanceof String && o2 instanceof String ) {
-			String s1 = (String)o1;
-			String s2 = (String)o2;
-			return s1.compareTo(s2) < 0;
-		}
-		LuanFunction fn = getBinHandler("__lt",o1,o2);
-		if( fn != null )
-			return checkBoolean( Luan.first(fn.call(luan,new Object[]{o1,o2})) );
-		throw new LuanException( "attempt to compare " + Luan.type(o1) + " with " + Luan.type(o2) );
-	}
-
-	public static LuanFunction getBinHandler(String op,Object o1,Object o2) throws LuanException {
-		if( o1 instanceof LuanTable ) {
-			LuanFunction f1 = getHandlerFunction(op,(LuanTable)o1);
-			if( f1 != null )
-				return f1;
-		}
-		return o2 instanceof LuanTable ? getHandlerFunction(op,(LuanTable)o2) : null;
-	}
-
-	public static LuanFunction getHandlerFunction(String op,LuanTable t) throws LuanException {
-		Object f = t.getHandler(op);
-		if( f == null )
-			return null;
-		return checkFunction(f);
-	}
-
-	public static LuanFunction load(String text,String sourceName,LuanTable env)
-		throws LuanException
-	{
-		return LuanCompiler.compile(text,sourceName,env);
-	}
-
-	public static LuanFunction load(String text,String sourceName)
-		throws LuanException
-	{
-		return load(text,sourceName,null);
-	}
-
-
-	private Luan() {}  // never
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/LuanException.java
--- a/core/src/luan/LuanException.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,151 +0,0 @@
-package luan;
-
-import java.io.StringWriter;
-import java.io.PrintWriter;
-import java.util.List;
-import java.util.ArrayList;
-
-
-public final class LuanException extends Exception implements DeepCloneable {
-	private LuanTable table;
-
-	public LuanException(String msg,Throwable cause) {
-		super(msg,cause);
-		initTable();
-	}
-
-	public LuanException(String msg) {
-		super(msg);
-		initTable();
-	}
-
-	public LuanException(Throwable cause) {
-		super(cause);
-		initTable();
-	}
-
-	private LuanException(String msg,Throwable cause,int nonsense) {
-		super(msg,cause);
-	}
-
-	@Override public LuanException shallowClone() {
-		return new LuanException(getMessage(),getCause(),99);
-	}
-
-	@Override public void deepenClone(DeepCloneable dc,DeepCloner cloner) {
-		LuanException clone = (LuanException)dc;
-		clone.table = (LuanTable)cloner.get(table);
-	}
-
-	public LuanTable table() {
-		return table;
-	}
-
-	private void initTable() {
-		table = new LuanTable();
-		table.rawPut( "java", this );
-		LuanTable metatable = new LuanTable();
-		table.setMetatable(metatable);
-		try {
-			table.rawPut( "get_message", new LuanJavaFunction(
-				LuanException.class.getMethod( "getMessage" ), this
-			) );
-			table.rawPut( "throw", new LuanJavaFunction(
-				LuanException.class.getMethod( "throwThis" ), this
-			) );
-			table.rawPut( "get_java_stack_trace_string", new LuanJavaFunction(
-				LuanException.class.getMethod( "getJavaStackTraceString" ), this
-			) );
-			metatable.rawPut( "__to_string", new LuanJavaFunction(
-				LuanException.class.getMethod( "getFullMessage" ), this
-			) );
-		} catch(NoSuchMethodException e) {
-			throw new RuntimeException(e);
-		}
-	}
-
-	public void throwThis() throws LuanException {
-		throw this;
-	}
-
-	public String getFullMessage() {
-		return getLuanStackTraceString();
-//		return getLuanStackTraceString()+"\n"+getJavaStackTraceString();
-/*
-		StringBuilder buf = new StringBuilder();
-
-		Object msg = table.rawGet("message");
-		String msgStr = (String)table.rawGet("message_string");
-		buf.append( msgStr );
-
-		for( int i = table.rawLength(); i>=1; i-- ) {
-			LuanTable tbl = (LuanTable)table.rawGet(i);
-			buf.append( "\n\t" ).append( tbl.rawGet("source") ).append( " line " ).append( tbl.rawGet("line") );
-			Object callTo = tbl.rawGet("call_to");
-			if( callTo != null )
-				buf.append( " in call to '" ).append( callTo ).append( "'" );
-		}
-
-		if( msg instanceof Throwable ) {
-			buf.append( "\nCaused by: " );
-			Throwable cause = (Throwable)msg;
-			StringWriter sw = new StringWriter();
-			cause.printStackTrace(new PrintWriter(sw));
-			buf.append( sw );
-		}
-
-		return buf.toString();
-*/
-	}
-
-	public String getJavaStackTraceString() {
-		return getJavaStackTraceString(this);
-	}
-
-	private static String getJavaStackTraceString(Throwable th) {
-		StringWriter sw = new StringWriter();
-		th.printStackTrace(new PrintWriter(sw));
-		return sw.toString();
-	}
-
-	public static List<StackTraceElement> justLuan(StackTraceElement[] orig) {
-		List<StackTraceElement> list = new ArrayList<StackTraceElement>();
-		for( int i=0; i<orig.length; i++ ) {
-			StackTraceElement ste = orig[i];
-			if( !ste.getClassName().startsWith("luan.impl.EXP") )
-				continue;
-			list.add(ste);
-			if( !ste.getMethodName().equals("doCall") )
-				i++;
-		}
-		return list;
-	}
-
-	public static String toString(StackTraceElement ste) {
-		StringBuilder sb = new StringBuilder();
-		sb.append( ste.getFileName() ).append( " line " ).append( ste.getLineNumber() );
-		String method = ste.getMethodName();
-		if( !method.equals("doCall") )
-			sb.append( " in function '" ).append( method.substring(1) ).append( "'" );
-		return sb.toString();
-	}
-
-	public String getLuanStackTraceString() {
-		StringBuilder sb = new StringBuilder();
-		sb.append( getMessage() );
-		for( StackTraceElement ste : justLuan(getStackTrace()) ) {
-			sb.append( "\n\t" ).append( toString(ste) );
-		}
-		Throwable cause = getCause();
-		if( cause != null )
-			sb.append( "\nCaused by: " ).append( getJavaStackTraceString(cause) );
-		return sb.toString();
-	}
-
-	public static String currentSource() {
-		LuanException ex = new LuanException("currentSource");
-		List<StackTraceElement> st = ex.justLuan(ex.getStackTrace());
-		return st.isEmpty() ? null : st.get(0).getFileName();
-	}
-
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/LuanFunction.java
--- a/core/src/luan/LuanFunction.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,18 +0,0 @@
-package luan;
-
-
-public abstract class LuanFunction {
-
-	public abstract Object call(LuanState luan,Object[] args) throws LuanException;
-
-	public static final Object[] NOTHING = new Object[0];
-
-	public final Object call(LuanState luan) throws LuanException {
-		return call(luan,NOTHING);
-	}
-
-	@Override public String toString() {
-		return "function: " + Integer.toHexString(hashCode());
-	}
-
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/LuanJava.java
--- a/core/src/luan/LuanJava.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,17 +0,0 @@
-package luan;
-
-import luan.DeepCloneable;
-import luan.DeepCloner;
-
-
-public final class LuanJava implements DeepCloneable {
-	public boolean ok = false;
-
-	@Override public LuanJava shallowClone() {
-		LuanJava java = new LuanJava();
-		java.ok = ok;
-		return java;
-	}
-
-	@Override public void deepenClone(DeepCloneable clone,DeepCloner cloner) {}
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/LuanJavaFunction.java
--- a/core/src/luan/LuanJavaFunction.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,572 +0,0 @@
-package luan;
-
-import java.lang.reflect.Array;
-import java.lang.reflect.Method;
-import java.lang.reflect.Constructor;
-import java.lang.reflect.InvocationTargetException;
-import java.util.List;
-import java.util.ArrayList;
-import java.util.Map;
-import java.util.HashMap;
-import java.util.Set;
-import java.util.Arrays;
-import java.util.Collection;
-
-
-public final class LuanJavaFunction extends LuanFunction {
-	private final JavaMethod method;
-	private Object obj;
-	private final RtnConverter rtnConverter;
-	private final boolean takesLuaState;
-	private final ArgConverter[] argConverters;
-	private final Class varArgCls;
-
-	public LuanJavaFunction(Method method,Object obj) {
-		this( JavaMethod.of(method), obj );
-	}
-
-	public LuanJavaFunction(Constructor constr,Object obj) {
-		this( JavaMethod.of(constr), obj );
-	}
-
-	private LuanJavaFunction(JavaMethod method,Object obj) {
-		this.method = method;
-		this.obj = obj;
-		this.rtnConverter = getRtnConverter(method);
-		this.takesLuaState = takesLuaState(method);
-		this.argConverters = getArgConverters(takesLuaState,method);
-		if( method.isVarArgs() ) {
-			Class[] paramTypes = method.getParameterTypes();
-			this.varArgCls = paramTypes[paramTypes.length-1].getComponentType();
-		} else {
-			this.varArgCls = null;
-		}
-	}
-/*
-	private LuanJavaFunction(LuanJavaFunction f) {
-		this.method = f.method;
-		this.rtnConverter = f.rtnConverter;
-		this.takesLuaState = f.takesLuaState;
-		this.argConverters = f.argConverters;
-		this.varArgCls = f.varArgCls;
-	}
-
-	@Override public LuanJavaFunction shallowClone() {
-		return obj==null ? this : new LuanJavaFunction(this);
-	}
-
-	@Override public void deepenClone(LuanJavaFunction clone,DeepCloner cloner) {
-		clone.obj = cloner.get(obj);
-	}
-*/
-	@Override public String toString() {
-		return "java-function: " + method;
-	}
-
-	public Class[] getParameterTypes() {
-		return method.getParameterTypes();
-	}
-
-	@Override public Object call(LuanState luan,Object[] args) throws LuanException {
-		try {
-			args = fixArgs(luan,args);
-			return doCall(args);
-		} catch(IllegalArgumentException e) {
-			checkArgs(args);
-			throw e;
-		}
-	}
-
-	public Object rawCall(LuanState luan,Object[] args) throws LuanException {
-		args = fixArgs(luan,args);
-		return doCall(args);
-	}
-
-	private Object doCall(Object[] args) throws LuanException {
-		Object rtn;
-		try {
-			rtn = method.invoke(obj,args);
-		} catch(IllegalAccessException e) {
-			throw new RuntimeException("method = "+method,e);
-		} catch(InvocationTargetException e) {
-			Throwable cause = e.getCause();
-			if( cause instanceof Error )
-				throw (Error)cause;
-			if( cause instanceof LuanException )
-				throw (LuanException)cause;
-			throw new LuanException(cause);
-		} catch(InstantiationException e) {
-			throw new RuntimeException(e);
-		}
-		return rtnConverter.convert(rtn);
-	}
-
-	private static final Map primitiveMap = new HashMap();
-	static {
-		primitiveMap.put(Boolean.TYPE,Boolean.class);
-		primitiveMap.put(Character.TYPE,Character.class);
-		primitiveMap.put(Byte.TYPE,Byte.class);
-		primitiveMap.put(Short.TYPE,Short.class);
-		primitiveMap.put(Integer.TYPE,Integer.class);
-		primitiveMap.put(Long.TYPE,Long.class);
-		primitiveMap.put(Float.TYPE,Float.class);
-		primitiveMap.put(Double.TYPE,Double.class);
-		primitiveMap.put(Void.TYPE,Void.class);
-	}
-
-	private void checkArgs(Object[] args) throws LuanException {
-		Class[] a = getParameterTypes();
-		int start = takesLuaState ? 1 : 0;
-		for( int i=start; i<a.length; i++ ) {
-			Class paramType = a[i];
-			Class type = paramType;
-			if( type.isPrimitive() )
-				type = (Class)primitiveMap.get(type);
-			Object arg = args[i];
-			if( !type.isInstance(arg) ) {
-				String expected;
-				if( i==a.length-1 && method.isVarArgs() )
-					expected = fixType(paramType.getComponentType().getSimpleName())+"...";
-				else
-					expected = fixType(paramType.getSimpleName());
-				if( arg==null ) {
-					if( paramType.isPrimitive() )
-						throw new LuanException("bad argument #"+(i+1-start)+" ("+expected+" expected, got nil)");
-				} else {
-					String got = fixType(arg.getClass().getSimpleName());
-					throw new LuanException("bad argument #"+(i+1-start)+" ("+expected+" expected, got "+got+")");
-				}
-			}
-		}
-	}
-
-	private static String fixType(String type) {
-		if( type.equals("byte[]") )
-			return "binary";
-		if( type.equals("Double") )
-			return "number";
-		if( type.equals("LuanTable") )
-			return "table";
-		if( type.equals("Boolean") )
-			return "boolean";
-		if( type.equals("String") )
-			return "string";
-		if( type.equals("Closure") )
-			return "function";
-		if( type.equals("LuanJavaFunction") )
-			return "function";
-		return type;
-	}
-
-	private Object[] fixArgs(LuanState luan,Object[] args) throws LuanException {
-		int n = argConverters.length;
-		Object[] rtn;
-		int start = 0;
-		if( !takesLuaState && varArgCls==null && args.length == n ) {
-			rtn = args;
-		} else {
-			if( takesLuaState )
-				n++;
-			rtn = new Object[n];
-			if( takesLuaState ) {
-				rtn[start++] = luan;
-			}
-			n = argConverters.length;
-			if( varArgCls != null ) {
-				n--;
-				if( args.length < argConverters.length ) {
-					rtn[rtn.length-1] = Array.newInstance(varArgCls,0);
-				} else {
-					int len = args.length - n;
-					Object varArgs = Array.newInstance(varArgCls,len);
-					ArgConverter ac = argConverters[n];
-					for( int i=0; i<len; i++ ) {
-						Array.set( varArgs, i, ac.convert(luan,args[n+i]) );
-					}
-					rtn[rtn.length-1] = varArgs;
-				}
-			}
-			System.arraycopy(args,0,rtn,start,Math.min(args.length,n));
-		}
-		for( int i=0; i<n; i++ ) {
-			rtn[start+i] = argConverters[i].convert(luan,rtn[start+i]);
-		}
-		return rtn;
-	}
-
-
-	private interface RtnConverter {
-		public Object convert(Object obj);
-	}
-
-	private static final RtnConverter RTN_NOTHING = new RtnConverter() {
-		@Override public Object[] convert(Object obj) {
-			return NOTHING;
-		}
-	};
-
-	private static final RtnConverter RTN_SAME = new RtnConverter() {
-		@Override public Object convert(Object obj) {
-			return obj;
-		}
-	};
-
-	private static final RtnConverter RTN_ARRAY = new RtnConverter() {
-		@Override public Object convert(Object obj) {
-			if( obj == null )
-				return null;
-			Object[] a = new Object[Array.getLength(obj)];
-			for( int i=0; i<a.length; i++ ) {
-				a[i] = Array.get(obj,i);
-			}
-			return new LuanTable(new ArrayList<Object>(Arrays.asList(a)));
-		}
-	};
-
-	private static RtnConverter getRtnConverter(JavaMethod m) {
-		Class rtnType = m.getReturnType();
-		if( rtnType == Void.TYPE )
-			return RTN_NOTHING;
-		if( !m.isLuan() && rtnType.isArray() && !rtnType.getComponentType().isPrimitive() ) {
-//System.out.println("qqqqqq "+m);
-			return RTN_ARRAY;
-		}
-		return RTN_SAME;
-	}
-
-	private static boolean isNumber(Class rtnType) {
-		return rtnType == Short.TYPE
-			|| rtnType == Integer.TYPE
-			|| rtnType == Long.TYPE
-			|| rtnType == Float.TYPE
-			|| rtnType == Double.TYPE
-		;
-	}
-
-	private interface ArgConverter {
-		public Object convert(LuanState luan,Object obj) throws LuanException;
-	}
-
-	private static final ArgConverter ARG_SAME = new ArgConverter() {
-		public Object convert(LuanState luan,Object obj) {
-			return obj;
-		}
-		@Override public String toString() {
-			return "ARG_SAME";
-		}
-	};
-
-	private static final ArgConverter ARG_DOUBLE = new ArgConverter() {
-		public Object convert(LuanState luan,Object obj) {
-			if( obj instanceof Double )
-				return obj;
-			if( obj instanceof Number ) {
-				Number n = (Number)obj;
-				return n.doubleValue();
-			}
-			return obj;
-		}
-		@Override public String toString() {
-			return "ARG_DOUBLE";
-		}
-	};
-
-	private static final ArgConverter ARG_FLOAT = new ArgConverter() {
-		public Object convert(LuanState luan,Object obj) {
-			if( obj instanceof Float )
-				return obj;
-			if( obj instanceof Number ) {
-				Number n = (Number)obj;
-				return n.floatValue();
-			}
-			return obj;
-		}
-		@Override public String toString() {
-			return "ARG_FLOAT";
-		}
-	};
-
-	private static final ArgConverter ARG_LONG = new ArgConverter() {
-		public Object convert(LuanState luan,Object obj) {
-			if( obj instanceof Long )
-				return obj;
-			if( obj instanceof Number ) {
-				Number n = (Number)obj;
-				long r = n.longValue();
-				if( r==n.doubleValue() )
-					return r;
-			}
-			return obj;
-		}
-		@Override public String toString() {
-			return "ARG_LONG";
-		}
-	};
-
-	private static final ArgConverter ARG_INTEGER = new ArgConverter() {
-		public Object convert(LuanState luan,Object obj) {
-			if( obj instanceof Integer )
-				return obj;
-			if( obj instanceof Number ) {
-				Number n = (Number)obj;
-				int r = n.intValue();
-				if( r==n.doubleValue() )
-					return r;
-			}
-			return obj;
-		}
-		@Override public String toString() {
-			return "ARG_INTEGER";
-		}
-	};
-
-	private static final ArgConverter ARG_SHORT = new ArgConverter() {
-		public Object convert(LuanState luan,Object obj) {
-			if( obj instanceof Short )
-				return obj;
-			if( obj instanceof Number ) {
-				Number n = (Number)obj;
-				short r = n.shortValue();
-				if( r==n.doubleValue() )
-					return r;
-			}
-			return obj;
-		}
-		@Override public String toString() {
-			return "ARG_SHORT";
-		}
-	};
-
-	private static final ArgConverter ARG_BYTE = new ArgConverter() {
-		public Object convert(LuanState luan,Object obj) {
-			if( obj instanceof Byte )
-				return obj;
-			if( obj instanceof Number ) {
-				Number n = (Number)obj;
-				byte r = n.byteValue();
-				if( r==n.doubleValue() )
-					return r;
-			}
-			return obj;
-		}
-		@Override public String toString() {
-			return "ARG_BYTE";
-		}
-	};
-
-	private static final ArgConverter ARG_TABLE = new ArgConverter() {
-		public Object convert(LuanState luan,Object obj) {
-			if( obj == null )
-				return null;
-			if( obj instanceof List ) {
-				return new LuanTable((List)obj);
-			}
-			if( obj instanceof Map ) {
-				return new LuanTable((Map)obj);
-			}
-			if( obj instanceof Set ) {
-				return new LuanTable((Set)obj);
-			}
-			Class cls = obj.getClass();
-			if( cls.isArray() && !cls.getComponentType().isPrimitive() ) {
-				Object[] a = (Object[])obj;
-				return new LuanTable(Arrays.asList(a));
-			}
-			return obj;
-		}
-		@Override public String toString() {
-			return "ARG_TABLE";
-		}
-	};
-
-	private static final ArgConverter ARG_MAP = new ArgConverter() {
-		public Object convert(LuanState luan,Object obj) throws LuanException {
-			if( obj instanceof LuanTable ) {
-				LuanTable t = (LuanTable)obj;
-				return t.asMap(luan);
-			}
-			return obj;
-		}
-		@Override public String toString() {
-			return "ARG_MAP";
-		}
-	};
-
-	private static final ArgConverter ARG_LIST = new ArgConverter() {
-		public Object convert(LuanState luan,Object obj) {
-			if( obj instanceof LuanTable ) {
-				LuanTable t = (LuanTable)obj;
-				if( t.isList() )
-					return t.asList();
-			}
-			return obj;
-		}
-		@Override public String toString() {
-			return "ARG_LIST";
-		}
-	};
-
-	private static final ArgConverter ARG_SET = new ArgConverter() {
-		public Object convert(LuanState luan,Object obj) throws LuanException {
-			if( obj instanceof LuanTable ) {
-				LuanTable t = (LuanTable)obj;
-				if( t.isSet(luan) )
-					return t.asSet(luan);
-			}
-			return obj;
-		}
-		@Override public String toString() {
-			return "ARG_SET";
-		}
-	};
-
-	private static final ArgConverter ARG_COLLECTION = new ArgConverter() {
-		public Object convert(LuanState luan,Object obj) throws LuanException {
-			if( obj instanceof LuanTable ) {
-				LuanTable t = (LuanTable)obj;
-				if( t.isList() )
-					return t.asList();
-				if( t.isSet(luan) )
-					return t.asSet(luan);
-			}
-			return obj;
-		}
-		@Override public String toString() {
-			return "ARG_COLLECTION";
-		}
-	};
-
-	private static class ArgArray implements ArgConverter {
-		private final Object[] a;
-
-		ArgArray(Class cls) {
-			a = (Object[])Array.newInstance(cls.getComponentType(),0);
-		}
-
-		public Object convert(LuanState luan,Object obj) {
-			if( obj instanceof LuanTable ) {
-				LuanTable t = (LuanTable)obj;
-				if( t.isList() ) {
-					try {
-						return t.asList().toArray(a);
-					} catch(ArrayStoreException e) {}
-				}
-			}
-			return obj;
-		}
-	}
-
-	private static boolean takesLuaState(JavaMethod m) {
-		Class[] paramTypes = m.getParameterTypes();
-		return paramTypes.length > 0 && paramTypes[0].equals(LuanState.class);
-	}
-
-	private static ArgConverter[] getArgConverters(boolean takesLuaState,JavaMethod m) {
-		final boolean isVarArgs = m.isVarArgs();
-		Class[] paramTypes = m.getParameterTypes();
-		if( takesLuaState ) {
-			Class[] t = new Class[paramTypes.length-1];
-			System.arraycopy(paramTypes,1,t,0,t.length);
-			paramTypes = t;
-		}
-		ArgConverter[] a = new ArgConverter[paramTypes.length];
-		for( int i=0; i<a.length; i++ ) {
-			Class paramType = paramTypes[i];
-			if( isVarArgs && i == a.length-1 )
-				paramType = paramType.getComponentType();
-			a[i] = getArgConverter(paramType);
-		}
-		return a;
-	}
-
-	private static ArgConverter getArgConverter(Class cls) {
-		if( cls == Double.TYPE || cls.equals(Double.class) )
-			return ARG_DOUBLE;
-		if( cls == Float.TYPE || cls.equals(Float.class) )
-			return ARG_FLOAT;
-		if( cls == Long.TYPE || cls.equals(Long.class) )
-			return ARG_LONG;
-		if( cls == Integer.TYPE || cls.equals(Integer.class) )
-			return ARG_INTEGER;
-		if( cls == Short.TYPE || cls.equals(Short.class) )
-			return ARG_SHORT;
-		if( cls == Byte.TYPE || cls.equals(Byte.class) )
-			return ARG_BYTE;
-		if( cls.equals(LuanTable.class) )
-			return ARG_TABLE;
-		if( cls.equals(Map.class) )
-			return ARG_MAP;
-		if( cls.equals(List.class) )
-			return ARG_LIST;
-		if( cls.equals(Set.class) )
-			return ARG_SET;
-		if( cls.equals(Collection.class) )
-			return ARG_COLLECTION;
-		if( cls.isArray() && !cls.getComponentType().isPrimitive() )
-			return new ArgArray(cls);
-		return ARG_SAME;
-	}
-
-
-
-	private static abstract class JavaMethod {
-		abstract boolean isVarArgs();
-		abstract Class[] getParameterTypes();
-		abstract Object invoke(Object obj,Object... args)
-			throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, InstantiationException;
-		abstract Class getReturnType();
-		abstract boolean isLuan();
-	
-		static JavaMethod of(final Method m) {
-			return new JavaMethod() {
-				@Override boolean isVarArgs() {
-					return m.isVarArgs();
-				}
-				@Override Class[] getParameterTypes() {
-					return m.getParameterTypes();
-				}
-				@Override Object invoke(Object obj,Object... args)
-					throws IllegalAccessException, IllegalArgumentException, InvocationTargetException
-				{
-					return m.invoke(obj,args);
-				}
-				@Override Class getReturnType() {
-					return m.getReturnType();
-				}
-				@Override boolean isLuan() {
-					return m.getAnnotation(LuanMethod.class) != null;
-				}
-				@Override public String toString() {
-					return m.toString();
-				}
-			};
-		}
-	
-		static JavaMethod of(final Constructor c) {
-			return new JavaMethod() {
-				@Override boolean isVarArgs() {
-					return c.isVarArgs();
-				}
-				@Override Class[] getParameterTypes() {
-					return c.getParameterTypes();
-				}
-				@Override Object invoke(Object obj,Object... args)
-					throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, InstantiationException
-				{
-					return c.newInstance(args);
-				}
-				@Override Class getReturnType() {
-					return c.getDeclaringClass();
-				}
-				@Override boolean isLuan() {
-					return false;
-				}
-				@Override public String toString() {
-					return c.toString();
-				}
-			};
-		}
-	
-	}
-
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/LuanMeta.java
--- a/core/src/luan/LuanMeta.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,69 +0,0 @@
-package luan;
-
-import java.util.Map;
-import java.util.Iterator;
-import java.util.Set;
-import java.util.HashSet;
-
-
-public abstract class LuanMeta {
-
-	public abstract Object __index(LuanState luan,LuanTable tbl,Object key) throws LuanException;
-
-	protected abstract Iterator keys(LuanTable tbl);
-
-	public LuanFunction __pairs(final LuanState luan,final LuanTable tbl) {
-		return new LuanFunction() {
-			final Iterator<Map.Entry<Object,Object>> iter1 = tbl.rawIterator();
-			final Iterator<Object> iter2 = keys(tbl);
-			final Set<Object> set = new HashSet<Object>();
-
-			@Override public Object[] call(LuanState luan,Object[] args) throws LuanException {
-				if( iter1.hasNext() ) {
-					Map.Entry<Object,Object> entry = iter1.next();
-					Object key = entry.getKey();
-					set.add(key);
-					return new Object[]{key,entry.getValue()};
-				}
-				while( iter2.hasNext() ) {
-					Object key = iter2.next();
-					if( set.add(key) ) {
-						Object value = __index(luan,tbl,key);
-						return new Object[]{key,value};
-					}
-				}
-				return LuanFunction.NOTHING;
-			}
-		};
-	}
-
-	public boolean canNewindex() {
-		return false;
-	}
-
-	public void __new_index(LuanState luan,LuanTable tbl,Object key,Object value) throws LuanException {
-		throw new UnsupportedOperationException();
-	}
-
-	protected abstract String type(LuanTable tbl);
-
-	public String __to_string(LuanState luan,LuanTable tbl) throws LuanException {
-		return type(tbl) + "-" + tbl.rawToString();
-	}
-
-	public LuanTable newMetatable() {
-		LuanTable mt = new LuanTable();
-		mt.rawPut( "__index", this );
-		mt.rawPut( "__pairs", this );
-		mt.rawPut( "__to_string", this );
-		if( canNewindex() )
-			mt.rawPut( "__new_index", this );
-		return mt;
-	}
-
-	public LuanTable newTable() {
-		LuanTable tbl = new LuanTable();
-		tbl.setMetatable( newMetatable() );
-		return tbl;
-	}
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/LuanMethod.java
--- a/core/src/luan/LuanMethod.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,8 +0,0 @@
-package luan;
-
-import java.lang.annotation.*;
-
-
-@Target(ElementType.METHOD)
-@Retention(RetentionPolicy.RUNTIME)
-public @interface LuanMethod {}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/LuanPropertyMeta.java
--- a/core/src/luan/LuanPropertyMeta.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,75 +0,0 @@
-package luan;
-
-import java.util.Map;
-import java.util.Iterator;
-
-
-public final class LuanPropertyMeta extends LuanMeta {
-	public static final LuanPropertyMeta INSTANCE = new LuanPropertyMeta();
-
-	private LuanPropertyMeta() {}
-
-	public LuanTable getters(LuanTable tbl) {
-		return (LuanTable)tbl.getMetatable().rawGet("get");
-	}
-
-	public LuanTable setters(LuanTable tbl) {
-		return (LuanTable)tbl.getMetatable().rawGet("set");
-	}
-
-	protected String type(LuanTable tbl) {
-		return (String)tbl.getMetatable().rawGet("type");
-	}
-
-	@Override public Object __index(LuanState luan,LuanTable tbl,Object key) throws LuanException {
-		Object obj = getters(tbl).rawGet(key);
-		if( obj == null )
-			return null;
-		if( !(obj instanceof LuanFunction) )
-			throw new LuanException("get for '"+key+"' isn't a function");
-		LuanFunction fn = (LuanFunction)obj;
-		return fn.call(luan);
-	}
-
-	@Override protected Iterator keys(final LuanTable tbl) {
-		return new Iterator() {
-			final Iterator<Map.Entry<Object,Object>> iter = getters(tbl).rawIterator();
-
-			@Override public boolean hasNext() {
-				return iter.hasNext();
-			}
-			@Override public Object next() {
-				return iter.next().getKey();
-			}
-			@Override public void remove() {
-				throw new UnsupportedOperationException();
-			}
-		};
-	}
-
-
-	@Override public boolean canNewindex() {
-		return true;
-	}
-
-	@Override public void __new_index(LuanState luan,LuanTable tbl,Object key,Object value) throws LuanException {
-		Object obj = setters(tbl).rawGet(key);
-		if( obj == null ) {
-			tbl.rawPut(key,value);
-			return;
-		}
-		if( !(obj instanceof LuanFunction) )
-			throw new LuanException("set for '"+key+"' isn't a function");
-		LuanFunction fn = (LuanFunction)obj;
-		fn.call(luan,new Object[]{value});
-	}
-
-	@Override public LuanTable newMetatable() {
-		LuanTable mt = super.newMetatable();
-		mt.rawPut( "get", new LuanTable() );
-		mt.rawPut( "set", new LuanTable() );
-		mt.rawPut( "type", "property" );
-		return mt;
-	}
-
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/LuanRuntimeException.java
--- a/core/src/luan/LuanRuntimeException.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,8 +0,0 @@
-package luan;
-
-
-public final class LuanRuntimeException extends RuntimeException {
-	public LuanRuntimeException(LuanException e) {
-		super(e);
-	}
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/LuanState.java
--- a/core/src/luan/LuanState.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,97 +0,0 @@
-package luan;
-
-import java.io.Closeable;
-import java.io.IOException;
-import java.lang.ref.Reference;
-import java.lang.ref.WeakReference;
-import java.util.List;
-import java.util.ArrayList;
-import java.util.Map;
-import java.util.HashMap;
-import luan.impl.LuanCompiler;
-import luan.modules.BasicLuan;
-import luan.modules.JavaLuan;
-
-
-public final class LuanState implements DeepCloneable {
-
-	public LuanJava java;
-	private Map registry;
-	private final List<Reference<Closeable>> onClose = new ArrayList<Reference<Closeable>>();
-
-	public LuanState() {
-		java = new LuanJava();
-		registry = new HashMap();
-	}
-
-	private LuanState(LuanState luan) {}
-
-	@Override public LuanState shallowClone() {
-		return new LuanState(this);
-	}
-
-	@Override public void deepenClone(DeepCloneable dc,DeepCloner cloner) {
-		LuanState clone = (LuanState)dc;
-		clone.registry = cloner.deepClone(registry);
-		clone.java = (LuanJava)cloner.deepClone(java);
-	}
-
-	public final Map registry() {
-		return registry;
-	}
-
-	public void onClose(Closeable c) {
-		onClose.add(new WeakReference<Closeable>(c));
-	}
-
-	public void close() throws IOException {
-		for( Reference<Closeable> ref : onClose ) {
-			Closeable c = ref.get();
-			if( c != null )
-				c.close();
-		}
-		onClose.clear();
-	}
-/*
-	public final Object eval(String cmd) throws LuanException {
-		return eval(cmd,new LuanTable());
-	}
-
-	public final Object eval(String cmd,LuanTable env) throws LuanException {
-		LuanFunction fn = BasicLuan.load(this,cmd,"eval",env,true);
-		return fn.call(this);
-	}
-*/
-
-	public String toString(Object obj) throws LuanException {
-		if( obj instanceof LuanTable ) {
-			LuanTable tbl = (LuanTable)obj;
-			return tbl.toString(this);
-		}
-		if( obj == null )
-			return "nil";
-		if( obj instanceof Number )
-			return Luan.toString((Number)obj);
-		if( obj instanceof byte[] )
-			return "binary: " + Integer.toHexString(obj.hashCode());
-		return obj.toString();
-	}
-
-	public Object index(Object obj,Object key) throws LuanException {
-		if( obj instanceof LuanTable ) {
-			LuanTable tbl = (LuanTable)obj;
-			return tbl.get(this,key);
-		}
-		if( obj != null && java.ok )
-			return JavaLuan.__index(this,obj,key,false);
-		throw new LuanException("attempt to index a " + Luan.type(obj) + " value" );
-	}
-
-/*
-	public Number checkNumber(Object obj) throws LuanException {
-		if( obj instanceof Number )
-			return (Number)obj;
-		throw new LuanException( "attempt to perform arithmetic on '"+context()+"' (a " + Luan.type(obj) + " value)" );
-	}
-*/
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/LuanTable.java
--- a/core/src/luan/LuanTable.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,451 +0,0 @@
-package luan;
-
-import java.util.Iterator;
-import java.util.ListIterator;
-import java.util.Map;
-import java.util.AbstractMap;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.Set;
-import java.util.HashSet;
-
-
-public final class LuanTable implements DeepCloneable {
-	private Map map = null;
-	private List list = null;
-	private LuanTable metatable = null;
-	public LuanJava java;
-
-	public LuanTable() {}
-
-	public LuanTable(List list) {
-		int n = list.size();
-		for( int i=0; i<n; i++ ) {
-			Object val = list.get(i);
-			if( val != null )
-				rawPut(i+1,val);
-		}
-	}
-
-	public LuanTable(Map map) {
-		for( Object stupid : map.entrySet() ) {
-			Map.Entry entry = (Map.Entry)stupid;
-			Object key = entry.getKey();
-			Object value = entry.getValue();
-			if( key != null && value != null )
-				rawPut(key,value);
-		}
-	}
-
-	public LuanTable(Set set) {
-		for( Object el : set ) {
-			if( el != null )
-				rawPut(el,Boolean.TRUE);
-		}
-	}
-
-	public LuanTable(LuanTable tbl) {
-		if( tbl.map != null && !tbl.map.isEmpty() )
-			this.map = new LinkedHashMap<Object,Object>(tbl.map);
-		if( tbl.rawLength() > 0 )
-			this.list = new ArrayList<Object>(tbl.list);
-		this.metatable = tbl.metatable;
-	}
-
-	@Override public LuanTable shallowClone() {
-		return new LuanTable();
-	}
-
-	@Override public void deepenClone(DeepCloneable dc,DeepCloner cloner) {
-		LuanTable clone = (LuanTable)dc;
-		if( map != null ) {
-			clone.map = newMap();
-			for( Object stupid : map.entrySet() ) {
-				Map.Entry entry = (Map.Entry)stupid;
-				clone.map.put( cloner.get(entry.getKey()), cloner.get(entry.getValue()) );
-			}
-		}
-		if( list != null ) {
-			clone.list = new ArrayList<Object>();
-			for( Object obj : list ) {
-				clone.list.add( cloner.get(obj) );
-			}
-		}
-		if( metatable != null )
-			clone.metatable = (LuanTable)cloner.get(metatable);
-		clone.java = (LuanJava)cloner.deepClone(java);
-	}
-
-	public boolean isList() {
-		return map==null || map.isEmpty();
-	}
-
-	public List<Object> asList() {
-		return list!=null ? list : Collections.emptyList();
-	}
-
-	public String toString(LuanState luan) throws LuanException {
-		Object h = getHandler("__to_string");
-		if( h == null )
-			return rawToString();
-		if( h instanceof LuanMeta ) {
-			LuanMeta meta = (LuanMeta)h;
-			return meta.__to_string(luan,this);
-		}
-		LuanFunction fn = Luan.checkFunction(h);
-		return Luan.checkString( Luan.first( fn.call(luan,new Object[]{this}) ) );
-	}
-
-	public String rawToString() {
-		return "table: " + Integer.toHexString(hashCode());
-	}
-
-	public Object get(LuanState luan,Object key) throws LuanException {
-		Object value = rawGet(key);
-		if( value != null )
-			return value;
-		Object h = getHandler("__index");
-		if( h==null )
-			return null;
-		if( h instanceof LuanFunction ) {
-			LuanFunction fn = (LuanFunction)h;
-			return Luan.first(fn.call(luan,new Object[]{this,key}));
-		}
-		if( h instanceof LuanMeta ) {
-			LuanMeta meta = (LuanMeta)h;
-			return meta.__index(luan,this,key);
-		}
-		return luan.index(h,key);
-	}
-
-	public Object rawGet(Object key) {
-		if( list != null ) {
-			Integer iT = Luan.asInteger(key);
-			if( iT != null ) {
-				int i = iT - 1;
-				if( i>=0 && i<list.size() )
-					return list.get(i);
-			}
-		}
-		if( map==null )
-			return null;
-		if( key instanceof Number && !(key instanceof Double) ) {
-			Number n = (Number)key;
-			key = Double.valueOf(n.doubleValue());
-		}
-		return map.get(key);
-	}
-
-	public void put(LuanState luan,Object key,Object value) throws LuanException {
-		Object h = getHandler("__new_index");
-		if( h==null || rawGet(key)!=null ) {
-			rawPut(key,value);
-			return;
-		}
-		if( h instanceof LuanFunction ) {
-			LuanFunction fn = (LuanFunction)h;
-			fn.call(luan,new Object[]{this,key,value});
-			return;
-		}
-		if( h instanceof LuanMeta ) {
-			LuanMeta meta = (LuanMeta)h;
-			meta.__new_index(luan,this,key,value);
-			return;
-		}
-		if( h instanceof LuanTable ) {
-			LuanTable tbl = (LuanTable)h;
-			tbl.put(luan,key,value);
-			return;
-		}
-		throw new LuanException("invalid type "+Luan.type(h)+" for metamethod __new_index");
-	}
-
-	public void rawPut(Object key,Object val) {
-		Integer iT = Luan.asInteger(key);
-		if( iT != null ) {
-			int i = iT - 1;
-			if( list != null || i == 0 ) {
-				if( i == list().size() ) {
-					if( val != null ) {
-						list.add(val);
-						mapToList();
-					}
-					return;
-				} else if( i>=0 && i<list.size() ) {
-					list.set(i,val);
-					if( val == null ) {
-						listToMap(i);
-					}
-					return;
-				}
-			}
-		}
-		if( map==null )
-			map = newMap();
-		if( key instanceof Number && !(key instanceof Double) ) {
-			Number n = (Number)key;
-			key = Double.valueOf(n.doubleValue());
-		}
-		if( val == null ) {
-			map.remove(key);
-		} else {
-			map.put(key,val);
-		}
-	}
-
-	private void mapToList() {
-		if( map != null ) {
-			while(true) {
-				Object v = map.remove(Double.valueOf(list.size()+1));
-				if( v == null )
-					break;
-				list.add(v);
-			}
-		}
-	}
-
-	private void listToMap(int from) {
-		if( list != null ) {
-			while( list.size() > from ) {
-				int i = list.size() - 1;
-				Object v = list.remove(i);
-				if( v != null ) {
-					if( map==null )
-						map = newMap();
-					map.put(i+1,v);
-				}
-			}
-		}
-	}
-
-	private List<Object> list() {
-		if( list == null ) {
-			list = new ArrayList<Object>();
-			mapToList();
-		}
-		return list;
-	}
-
-	public void rawInsert(int pos,Object value) {
-		if( value==null )
-			throw new IllegalArgumentException("can't insert a nil value");
-		list().add(pos-1,value);
-		mapToList();
-	}
-
-	public Object rawRemove(int pos) {
-		return list().remove(pos-1);
-	}
-
-	public void rawSort(Comparator<Object> cmp) {
-		Collections.sort(list(),cmp);
-	}
-
-	public int length(LuanState luan) throws LuanException {
-		Object h = getHandler("__len");
-		if( h != null ) {
-			LuanFunction fn = Luan.checkFunction(h);
-			return (Integer)Luan.first(fn.call(luan,new Object[]{this}));
-		}
-		return rawLength();
-	}
-
-	public int rawLength() {
-		return list==null ? 0 : list.size();
-	}
-
-	public Iterable<Map.Entry<Object,Object>> iterable(LuanState luan) throws LuanException {
-		final Iterator<Map.Entry<Object,Object>> iter = iterator(luan);
-		return new Iterable<Map.Entry<Object,Object>>() {
-			public Iterator<Map.Entry<Object,Object>> iterator() {
-				return iter;
-			}
-		};
-	}
-
-	public Iterable<Map.Entry<Object,Object>> rawIterable() throws LuanException {
-		final Iterator<Map.Entry<Object,Object>> iter = rawIterator();
-		return new Iterable<Map.Entry<Object,Object>>() {
-			public Iterator<Map.Entry<Object,Object>> iterator() {
-				return iter;
-			}
-		};
-	}
-
-	public Iterator<Map.Entry<Object,Object>> iterator(final LuanState luan) throws LuanException {
-		if( getHandler("__pairs") == null )
-			return rawIterator();
-		final LuanFunction fn = pairs(luan);
-		return new Iterator<Map.Entry<Object,Object>>() {
-			private Map.Entry<Object,Object> next = getNext();
-
-			private Map.Entry<Object,Object> getNext() {
-				try {
-					Object obj = fn.call(luan);
-					if( obj==null )
-						return null;
-					Object[] a = (Object[])obj;
-					if( a.length == 0 || a[0]==null )
-						return null;
-					return new AbstractMap.SimpleEntry<Object,Object>(a[0],a[1]);
-				} catch(LuanException e) {
-					throw new LuanRuntimeException(e);
-				}
-			}
-
-			public boolean hasNext() {
-				return next != null;
-			}
-
-			public Map.Entry<Object,Object> next() {
-				Map.Entry<Object,Object> rtn = next;
-				next = getNext();
-				return rtn;
-			}
-
-			public void remove() {
-				throw new UnsupportedOperationException();
-			}
-		};
-	}
-
-	public LuanFunction pairs(LuanState luan) throws LuanException {
-		Object h = getHandler("__pairs");
-		if( h != null ) {
-			if( h instanceof LuanFunction ) {
-				LuanFunction fn = (LuanFunction)h;
-				Object obj = Luan.first(fn.call(luan,new Object[]{this}));
-				if( !(obj instanceof LuanFunction) )
-					throw new LuanException( "metamethod __pairs should return function but returned " + Luan.type(obj) );
-				return (LuanFunction)obj;
-			}
-			if( h instanceof LuanMeta ) {
-				LuanMeta meta = (LuanMeta)h;
-				return meta.__pairs(luan,this);
-			}
-			throw new LuanException( "invalid type of metamethod __pairs: " + Luan.type(h) );
-		}
-		return rawPairs();
-	}
-
-	private LuanFunction rawPairs() {
-		return new LuanFunction() {
-			final Iterator<Map.Entry<Object,Object>> iter = rawIterator();
-
-			@Override public Object[] call(LuanState luan,Object[] args) {
-				if( !iter.hasNext() )
-					return LuanFunction.NOTHING;
-				Map.Entry<Object,Object> entry = iter.next();
-				return new Object[]{entry.getKey(),entry.getValue()};
-			}
-		};
-	}
-
-	public Iterator<Map.Entry<Object,Object>> rawIterator() {
-		if( list == null ) {
-			if( map == null )
-				return Collections.<Map.Entry<Object,Object>>emptyList().iterator();
-			return map.entrySet().iterator();
-		}
-		if( map == null )
-			return listIterator();
-		return new Iterator<Map.Entry<Object,Object>>() {
-			Iterator<Map.Entry<Object,Object>> iter = listIterator();
-			boolean isList = true;
-
-			public boolean hasNext() {
-				boolean b = iter.hasNext();
-				if( !b && isList ) {
-					iter = map.entrySet().iterator();
-					isList = false;
-					b = iter.hasNext();
-				}
-				return b;
-			}
-
-			public Map.Entry<Object,Object> next() {
-				return iter.next();
-			}
-
-			public void remove() {
-				throw new UnsupportedOperationException();
-			}
-		};
-	}
-
-	private Iterator<Map.Entry<Object,Object>> listIterator() {
-		if( list == null )
-			return Collections.<Map.Entry<Object,Object>>emptyList().iterator();
-		final ListIterator iter = list.listIterator();
-		return new Iterator<Map.Entry<Object,Object>>() {
-
-			public boolean hasNext() {
-				return iter.hasNext();
-			}
-
-			public Map.Entry<Object,Object> next() {
-				Integer key = iter.nextIndex()+1;
-				return new AbstractMap.SimpleEntry<Object,Object>(key,iter.next());
-			}
-
-			public void remove() {
-				throw new UnsupportedOperationException();
-			}
-		};
-	}
-
-	public LuanTable rawSubList(int from,int to) {
-		LuanTable tbl = shallowClone();
-		tbl.list = new ArrayList<Object>(list().subList(from-1,to-1));
-		return tbl;
-	}
-
-	public LuanTable getMetatable() {
-		return metatable;
-	}
-
-	public void setMetatable(LuanTable metatable) {
-		this.metatable = metatable;
-	}
-
-	public Object getHandler(String op) {
-		return metatable==null ? null : metatable.rawGet(op);
-	}
-
-	private Map<Object,Object> newMap() {
-		return new LinkedHashMap<Object,Object>();
-	}
-
-	public boolean isSet(LuanState luan) throws LuanException {
-		for( Map.Entry<Object,Object> entry : iterable(luan) ) {
-			if( !entry.getValue().equals(Boolean.TRUE) )
-				return false;
-		}
-		return true;
-	}
-
-	public Set<Object> asSet(LuanState luan) throws LuanException {
-		Set<Object> set = new HashSet<Object>();
-		for( Map.Entry<Object,Object> entry : iterable(luan) ) {
-			set.add(entry.getKey());
-		}
-		return set;
-	}
-
-	public Map<Object,Object> asMap(LuanState luan) throws LuanException {
-		Map<Object,Object> map = newMap();
-		for( Map.Entry<Object,Object> entry : iterable(luan) ) {
-			map.put(entry.getKey(),entry.getValue());
-		}
-		return map;
-	}
-
-	public void rawClear() {
-		map = null;
-		list = null;
-	}
-
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/cmd_line.luan
--- a/core/src/luan/cmd_line.luan	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,31 +0,0 @@
-local Luan = require "luan:Luan.luan"
-local error = Luan.error
-local ipairs = Luan.ipairs or error()
-local load_file = Luan.load_file or error()
-local try = Luan.try or error()
-local Table = require "luan:Table.luan"
-local Io = require "luan:Io.luan"
-local print = Io.print or error()
-
-
-local args = {...}
-if #args == 0 then
-	print("Luan "..Luan.VERSION)
-	Io.debug("> ")
-else
-	local file = args[1]
-	Luan.arg = {}
-	for j,v in ipairs(args) do
-		Luan.arg[j-1] = v
-	end
-	try {
-		function()
-			local main_file = load_file(file)
-			print( main_file( Table.unpack(Luan.arg) ) )
-		end;
-		catch = function(e)
---			java(); e.java.printStackTrace();
-			Io.print_to(Io.stderr, e )
-		end;
-	}
-end
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/impl/Closure.java
--- a/core/src/luan/impl/Closure.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,48 +0,0 @@
-package luan.impl;
-
-import luan.Luan;
-import luan.LuanFunction;
-import luan.LuanState;
-import luan.LuanException;
-import luan.DeepCloner;
-import luan.DeepCloneable;
-import luan.LuanJava;
-
-
-public abstract class Closure extends LuanFunction implements DeepCloneable, Cloneable {
-	public Pointer[] upValues;
-	public LuanJava java;
-
-	public Closure(int nUpValues,LuanJava java) throws LuanException {
-		this.upValues = new Pointer[nUpValues];
-		this.java = java;
-	}
-
-	@Override public Closure shallowClone() {
-		try {
-			return (Closure)clone();
-		} catch(CloneNotSupportedException e) {
-			throw new RuntimeException(e);
-		}
-	}
-
-	@Override public void deepenClone(DeepCloneable dc,DeepCloner cloner) {
-		Closure clone = (Closure)dc;
-		clone.upValues = (Pointer[])cloner.deepClone(upValues);
-		clone.java = (LuanJava)cloner.deepClone(java);
-	}
-
-	@Override public final Object call(LuanState luan,Object[] args) throws LuanException {
-		LuanJava old = luan.java;
-		luan.java = java;
-		try {
-			return doCall(luan,args);
-		} catch(StackOverflowError e) {
-			throw new LuanException( "stack overflow" );
-		} finally {
-			luan.java = old;
-		}	
-	}
-
-	public abstract Object doCall(LuanState luan,Object[] args) throws LuanException;
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/impl/LuanCompiler.java
--- a/core/src/luan/impl/LuanCompiler.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,87 +0,0 @@
-package luan.impl;
-
-import java.lang.ref.WeakReference;
-import java.lang.reflect.InvocationTargetException;
-import java.util.Map;
-import java.util.HashMap;
-import luan.LuanFunction;
-import luan.LuanState;
-import luan.LuanException;
-import luan.LuanTable;
-import luan.LuanJava;
-import luan.modules.JavaLuan;
-import luan.modules.PackageLuan;
-
-
-public final class LuanCompiler {
-	private static final Map<String,WeakReference<Class>> map = new HashMap<String,WeakReference<Class>>();
-
-	public static LuanFunction compile(String sourceText,String sourceName,LuanTable env) throws LuanException {
-		Class fnClass = env==null ? getClass(sourceText,sourceName) : getClass(sourceText,sourceName,env);
-		LuanJava java;
-		if( env == null ) {
-			java = new LuanJava();
-		} else {
-			java = env.java;
-			if( java == null ) {
-				java = new LuanJava();
-				env.java = java;
-			}
-		}
-		Closure closure;
-		try {
-			closure = (Closure)fnClass.getConstructor(LuanJava.class).newInstance(java);
-		} catch(NoSuchMethodException e) {
-			throw new RuntimeException(e);
-		} catch(InstantiationException e) {
-			throw new RuntimeException(e);
-		} catch(IllegalAccessException e) {
-			throw new RuntimeException(e);
-		} catch(InvocationTargetException e) {
-			throw new RuntimeException(e);
-		}
-		closure.upValues[0].o = JavaLuan.javaFn;
-		closure.upValues[1].o = PackageLuan.requireFn;
-		if( env != null )  closure.upValues[2].o = env;
-		return closure;
-	}
-
-	private static synchronized Class getClass(String sourceText,String sourceName) throws LuanException {
-		String key = sourceName + "~~~" + sourceText;
-		WeakReference<Class> ref = map.get(key);
-		if( ref != null ) {
-			Class cls = ref.get();
-			if( cls != null )
-				return cls;
-		}
-		Class cls = getClass(sourceText,sourceName,null);
-		map.put(key,new WeakReference<Class>(cls));
-		return cls;
-	}
-
-	private static Class getClass(String sourceText,String sourceName,LuanTable env) throws LuanException {
-		LuanParser parser = new LuanParser(sourceText,sourceName);
-		parser.addVar( "java" );
-		parser.addVar( "require" );
-		if( env != null )  parser.addVar( "_ENV" );
-		try {
-			return parser.RequiredModule();
-		} catch(ParseException e) {
-//e.printStackTrace();
-			throw new LuanException( e.getFancyMessage() );
-		}
-	}
-
-	public static String toJava(String sourceText,String sourceName) throws LuanException {
-		LuanParser parser = new LuanParser(sourceText,sourceName);
-		parser.addVar( "java" );
-		parser.addVar( "require" );
-		try {
-			return parser.RequiredModuleSource();
-		} catch(ParseException e) {
-			throw new LuanException( e.getFancyMessage() );
-		}
-	}
-
-	private LuanCompiler() {}  // never
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/impl/LuanImpl.java
--- a/core/src/luan/impl/LuanImpl.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,250 +0,0 @@
-package luan.impl;
-
-import java.util.Arrays;
-import java.util.List;
-import java.util.ArrayList;
-import luan.Luan;
-import luan.LuanState;
-import luan.LuanTable;
-import luan.LuanFunction;
-import luan.LuanException;
-import luan.modules.JavaLuan;
-
-
-public final class LuanImpl {
-	private LuanImpl() {}  // never
-
-	public static int len(LuanState luan,Object o) throws LuanException {
-		if( o instanceof String ) {
-			String s = (String)o;
-			return s.length();
-		}
-		if( o instanceof byte[] ) {
-			byte[] a = (byte[])o;
-			return a.length;
-		}
-		if( o instanceof LuanTable ) {
-			LuanTable t = (LuanTable)o;
-			return t.length(luan);
-		}
-		throw new LuanException( "attempt to get length of a " + Luan.type(o) + " value" );
-	}
-
-	public static Object unm(LuanState luan,Object o) throws LuanException {
-		if( o instanceof Number )
-			return -((Number)o).doubleValue();
-		if( o instanceof LuanTable ) {
-			LuanFunction fn = Luan.getHandlerFunction("__unm",(LuanTable)o);
-			if( fn != null ) {
-				return Luan.first(fn.call(luan,new Object[]{o}));
-			}
-		}
-		throw new LuanException("attempt to perform arithmetic on a "+Luan.type(o)+" value");
-	}
-
-	private static Object arithmetic(LuanState luan,String op,Object o1,Object o2) throws LuanException {
-		LuanFunction fn = Luan.getBinHandler(op,o1,o2);
-		if( fn != null )
-			return Luan.first(fn.call(luan,new Object[]{o1,o2}));
-		String type = !(o1 instanceof Number) ? Luan.type(o1) : Luan.type(o2);
-		throw new LuanException("attempt to perform arithmetic on a "+type+" value");
-	}
-
-	public static Object pow(LuanState luan,Object o1,Object o2) throws LuanException {
-		if( o1 instanceof Number && o2 instanceof Number )
-			return Math.pow( ((Number)o1).doubleValue(), ((Number)o2).doubleValue() );
-		return arithmetic(luan,"__pow",o1,o2);
-	}
-
-	public static Object mul(LuanState luan,Object o1,Object o2) throws LuanException {
-		if( o1 instanceof Number && o2 instanceof Number )
-			return ((Number)o1).doubleValue() * ((Number)o2).doubleValue();
-		return arithmetic(luan,"__mul",o1,o2);
-	}
-
-	public static Object div(LuanState luan,Object o1,Object o2) throws LuanException {
-		if( o1 instanceof Number && o2 instanceof Number )
-			return ((Number)o1).doubleValue() / ((Number)o2).doubleValue();
-		return arithmetic(luan,"__div",o1,o2);
-	}
-
-	public static Object mod(LuanState luan,Object o1,Object o2) throws LuanException {
-		if( o1 instanceof Number && o2 instanceof Number ) {
-			double d1 = ((Number)o1).doubleValue();
-			double d2 = ((Number)o2).doubleValue();
-			return d1 - Math.floor(d1/d2)*d2;
-		}
-		return arithmetic(luan,"__mod",o1,o2);
-	}
-
-	public static Object add(LuanState luan,Object o1,Object o2) throws LuanException {
-		if( o1 instanceof Number && o2 instanceof Number )
-			return ((Number)o1).doubleValue() + ((Number)o2).doubleValue();
-		return arithmetic(luan,"__add",o1,o2);
-	}
-
-	public static Object sub(LuanState luan,Object o1,Object o2) throws LuanException {
-		if( o1 instanceof Number && o2 instanceof Number )
-			return ((Number)o1).doubleValue() - ((Number)o2).doubleValue();
-		return arithmetic(luan,"__sub",o1,o2);
-	}
-
-	public static Object concat(LuanState luan,Object o1,Object o2) throws LuanException {
-		LuanFunction fn = Luan.getBinHandler("__concat",o1,o2);
-		if( fn != null )
-			return Luan.first(fn.call(luan,new Object[]{o1,o2}));
-		String s1 = luan.toString(o1);
-		String s2 = luan.toString(o2);
-		return s1 + s2;
-	}
-
-	public static boolean eq(LuanState luan,Object o1,Object o2) throws LuanException {
-		if( o1 == o2 || o1 != null && o1.equals(o2) )
-			return true;
-		if( o1 instanceof Number && o2 instanceof Number ) {
-			Number n1 = (Number)o1;
-			Number n2 = (Number)o2;
-			return n1.doubleValue() == n2.doubleValue();
-		}
-		if( o1 instanceof byte[] && o2 instanceof byte[] ) {
-			byte[] b1 = (byte[])o1;
-			byte[] b2 = (byte[])o2;
-			return Arrays.equals(b1,b2);
-		}
-		if( !(o1 instanceof LuanTable && o2 instanceof LuanTable) )
-			return false;
-		LuanTable t1 = (LuanTable)o1;
-		LuanTable t2 = (LuanTable)o2;
-		LuanTable mt1 = t1.getMetatable();
-		LuanTable mt2 = t2.getMetatable();
-		if( mt1==null || mt2==null )
-			return false;
-		Object f = mt1.rawGet("__eq");
-		if( f == null || !f.equals(mt2.rawGet("__eq")) )
-			return false;
-		LuanFunction fn = Luan.checkFunction(f);
-		return Luan.checkBoolean( Luan.first(fn.call(luan,new Object[]{o1,o2})) );
-	}
-
-	public static boolean le(LuanState luan,Object o1,Object o2) throws LuanException {
-		if( o1 instanceof Number && o2 instanceof Number ) {
-			Number n1 = (Number)o1;
-			Number n2 = (Number)o2;
-			return n1.doubleValue() <= n2.doubleValue();
-		}
-		if( o1 instanceof String && o2 instanceof String ) {
-			String s1 = (String)o1;
-			String s2 = (String)o2;
-			return s1.compareTo(s2) <= 0;
-		}
-		LuanFunction fn = Luan.getBinHandler("__le",o1,o2);
-		if( fn != null )
-			return Luan.checkBoolean( Luan.first(fn.call(luan,new Object[]{o1,o2})) );
-		fn = Luan.getBinHandler("__lt",o1,o2);
-		if( fn != null )
-			return !Luan.checkBoolean( Luan.first(fn.call(luan,new Object[]{o2,o1})) );
-		throw new LuanException( "attempt to compare " + Luan.type(o1) + " with " + Luan.type(o2) );
-	}
-
-	public static boolean lt(LuanState luan,Object o1,Object o2) throws LuanException {
-		return Luan.isLessThan(luan,o1,o2);
-	}
-
-	public static boolean cnd(Object o) throws LuanException {
-		return !(o == null || Boolean.FALSE.equals(o));
-	}
-
-	public static void nop(Object o) {}
-
-	public static void put(LuanState luan,Object t,Object key,Object value) throws LuanException {
-		if( t instanceof LuanTable ) {
-			LuanTable tbl = (LuanTable)t;
-			tbl.put(luan,key,value);
-			return;
-		}
-		if( t != null && luan.java.ok )
-			JavaLuan.__new_index(luan,t,key,value);
-		else
-			throw new LuanException( "attempt to index a " + Luan.type(t) + " value" );
-	}
-
-	public static Object pick(Object o,int i) {
-		if( i < 1 )
-			throw new RuntimeException();
-		if( !(o instanceof Object[]) )
-			return null;
-		Object[] a = (Object[])o;
-		return i<a.length ? a[i] : null;
-	}
-
-	public static Object[] varArgs(Object[] a,int i) {
-		if( i >= a.length )
-			return LuanFunction.NOTHING;
-		Object[] rtn = new Object[a.length - i];
-		System.arraycopy(a,i,rtn,0,rtn.length);
-		return rtn;
-	}
-
-	public static Object[] concatArgs(Object o1,Object o2) {
-		if( o1 instanceof Object[] ) {
-			Object[] a1 = (Object[])o1;
-			if( o2 instanceof Object[] ) {
-				Object[] a2 = (Object[])o2;
-				Object[] rtn = new Object[a1.length+a2.length];
-				System.arraycopy(a1,0,rtn,0,a1.length);
-				System.arraycopy(a2,0,rtn,a1.length,a2.length);
-				return rtn;
-			} else {
-				Object[] rtn = new Object[a1.length+1];
-				System.arraycopy(a1,0,rtn,0,a1.length);
-				rtn[a1.length] = o2;
-				return rtn;
-			}
-		} else {
-			if( o2 instanceof Object[] ) {
-				Object[] a2 = (Object[])o2;
-				Object[] rtn = new Object[1+a2.length];
-				rtn[0] = o1;
-				System.arraycopy(a2,0,rtn,1,a2.length);
-				return rtn;
-			} else {
-				Object[] rtn = new Object[2];
-				rtn[0] = o1;
-				rtn[2] = o2;
-				return rtn;
-			}
-		}
-	}
-
-	public static LuanTable table(Object[] a) {
-		LuanTable table = new LuanTable();
-		int i = 0;
-		for( Object fld : a ) {
-			if( fld instanceof TableField ) {
-				TableField tblFld = (TableField)fld;
-				Object key = tblFld.key;
-				Object value = tblFld.value;
-				if( key != null && value != null )
-					table.rawPut(key,value);
-			} else {
-				i++;
-				if( fld != null )
-					table.rawPut(i,fld);
-			}
-		}
-		return table;
-	}
-
-	public static Object first(Object[] a) {
-		return a.length==0 ? null : a[0];
-	}
-
-	public static String strconcat(String... a) {
-		StringBuilder sb = new StringBuilder();
-		for( String s : a ) {
-			sb.append(s);
-		}
-		return sb.toString();
-	}
-
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/impl/LuanJavaCompiler.java
--- a/core/src/luan/impl/LuanJavaCompiler.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,100 +0,0 @@
-package luan.impl;
-
-import java.io.OutputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.StringWriter;
-import java.io.IOException;
-import java.net.URI;
-import java.util.Collections;
-import java.util.Map;
-import java.util.HashMap;
-import javax.tools.FileObject;
-import javax.tools.JavaFileObject;
-import javax.tools.SimpleJavaFileObject;
-import javax.tools.JavaCompiler;
-import javax.tools.ToolProvider;
-import javax.tools.JavaFileManager;
-import javax.tools.StandardJavaFileManager;
-import javax.tools.ForwardingJavaFileManager;
-
-
-public final class LuanJavaCompiler {
-	private LuanJavaCompiler() {}  // never
-
-	private static class MyJavaFileObject extends SimpleJavaFileObject {
-		final ByteArrayOutputStream baos = new ByteArrayOutputStream();
-
-		MyJavaFileObject() {
-			super(URI.create("whatever"),JavaFileObject.Kind.CLASS);
-		}
-
-		@Override public OutputStream openOutputStream() {
-			return baos;
-		}
-
-		byte[] byteCode(String sourceName) {
-			byte[] byteCode = baos.toByteArray();
-			final int len = sourceName.length();
-			int max = byteCode.length-len-3;
-			outer:
-			for( int i=0; true; i++ ) {
-				if( i > max )
-					throw new RuntimeException("len="+len);
-				if( byteCode[i]==1 && (byteCode[i+1] << 8 | 0xFF & byteCode[i+2]) == len ) {
-					for( int j=i+3; j<i+3+len; j++ ) {
-						if( byteCode[j] != '$' )
-							continue outer;
-					}
-					System.arraycopy(sourceName.getBytes(),0,byteCode,i+3,len);
-					break;
-				}
-			}
-			return byteCode;
-		}
-	}
-
-	public static Class compile(final String className,final String sourceName,final String code) throws ClassNotFoundException {
-		final int len = sourceName.length();
-		StringBuilder sb = new StringBuilder(sourceName);
-		for( int i=0; i<len; i++ )
-			sb.setCharAt(i,'$');
-		JavaFileObject sourceFile = new SimpleJavaFileObject(URI.create(sb.toString()),JavaFileObject.Kind.SOURCE) {
-			@Override public CharSequence getCharContent(boolean ignoreEncodingErrors) {
-				return code;
-			}
-			@Override public String getName() {
-				return sourceName;
-			}
-			@Override public boolean isNameCompatible(String simpleName,JavaFileObject.Kind kind) {
-				return true;
-			}
-		};
-		final Map<String,MyJavaFileObject> map = new HashMap<String,MyJavaFileObject>();
-		JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
-		StandardJavaFileManager sjfm = compiler.getStandardFileManager(null,null,null);
-		ForwardingJavaFileManager fjfm = new ForwardingJavaFileManager(sjfm) {
-			@Override public JavaFileObject getJavaFileForOutput(JavaFileManager.Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException {
-				if( map.containsKey(className) )
-					throw new RuntimeException(className);
-				MyJavaFileObject classFile = new MyJavaFileObject();
-				map.put(className,classFile);
-				return classFile;
-			}
-		};
-		StringWriter out = new StringWriter();
-		boolean b = compiler.getTask(out, fjfm, null, null, null, Collections.singletonList(sourceFile)).call();
-		if( !b )
-			throw new RuntimeException("\n"+out+"\ncode:\n"+code+"\n");
-		ClassLoader cl = new ClassLoader() {
-			@Override protected Class<?> findClass(String name) throws ClassNotFoundException {
-				MyJavaFileObject jfo = map.get(name);
-				if( jfo != null ) {
-					byte[] byteCode = jfo.byteCode(sourceName);
-					return defineClass(name, byteCode, 0, byteCode.length);
-				}
-				return super.findClass(name);
-			}
-		};
-		return cl.loadClass(className);
-	}
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/impl/LuanParser.java
--- a/core/src/luan/impl/LuanParser.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,2004 +0,0 @@
-package luan.impl;
-
-//import java.io.StringWriter;
-//import java.io.PrintWriter;
-import java.util.Set;
-import java.util.HashSet;
-import java.util.Arrays;
-import java.util.List;
-import java.util.ArrayList;
-import java.util.concurrent.atomic.AtomicInteger;
-import luan.Luan;
-import luan.LuanState;
-import luan.LuanTable;
-import luan.modules.PackageLuan;
-
-
-final class LuanParser {
-
-	private interface Sym {
-		public Expr exp();
-	}
-
-	private int symCounter = 0;
-
-	private class LocalSym implements Sym {
-		final String name;
-		final String javaName;
-		boolean isPointer = false;
-
-		LocalSym(String name) {
-			this.name = name;
-			this.javaName = name + "_" + (++symCounter);
-		}
-
-		Stmts declaration(Expr value) {
-			Stmts stmt = new Stmts();
-			if( value==null ) {
-				stmt.add( new Object() {
-					@Override public String toString() {
-						if( !isPointer )
-							return "Object " + javaName + ";  ";
-						else
-							return "final Pointer " + javaName + " = new Pointer();  ";
-					}
-				} );
-			} else {
-				if( value.valType != Val.SINGLE )  throw new RuntimeException();
-				stmt.add( new Object() {
-					@Override public String toString() {
-						if( !isPointer )
-							return "Object " + javaName + " = ";
-						else
-							return "final Pointer " + javaName + " = new Pointer(";
-					}
-				} );
-				stmt.addAll(value);
-				stmt.add( new Object() {
-					@Override public String toString() {
-						if( !isPointer )
-							return ";  ";
-						else
-							return ");  ";
-					}
-				} );
-			}
-			return stmt;
-		}
-
-		@Override public Expr exp() {
-			Expr exp = new Expr(Val.SINGLE,false);
-			exp.add( new Object() {
-				@Override public String toString() {
-					if( !isPointer )
-						return javaName;
-					else
-						return javaName + ".o";
-				}
-			} );
-			return exp;
-		}
-	}
-
-	private class UpSym implements Sym {
-		final String name;
-		final int i;
-		final String value;
-
-		UpSym(String name,int i,String value) {
-			this.name = name;
-			this.i = i;
-			this.value = value;
-		}
-
-		String init() {
-			return "upValues[" + i + "] = " + value + ";  ";
-		}
-
-		@Override public Expr exp() {
-			Expr exp = new Expr(Val.SINGLE,false);
-			exp.add( new Object() {
-				@Override public String toString() {
-					return "upValues[" + i + "].o";
-				}
-			} );
-			return exp;
-		}
-	}
-
-	private final class Frame {
-		final Frame parent;
-		final List<LocalSym> symbols = new ArrayList<LocalSym>();
-		int loops = 0;
-		boolean isVarArg = false;
-		final List<UpSym> upValueSymbols = new ArrayList<UpSym>();
-
-		Frame() {
-			this.parent = null;
-		}
-
-		Frame(Frame parent) {
-			this.parent = parent;
-		}
-
-		LocalSym addLocalSym(String name) {
-			LocalSym sym = new LocalSym(name);
-			symbols.add(sym);
-			return sym;
-		}
-
-		UpSym addUpSym(String name,String value) {
-			UpSym sym = new UpSym( name, upValueSymbols.size(), value );
-			upValueSymbols.add(sym);
-			return sym;
-		}
-
-		LocalSym getLocalSym(String name) {
-			int i = symbols.size();
-			while( --i >= 0 ) {
-				LocalSym sym = symbols.get(i);
-				if( sym.name.equals(name) )
-					return sym;
-			}
-			return null;
-		}
-
-		UpSym getUpSym(String name) {
-			for( UpSym upSym : upValueSymbols ) {
-				if( upSym.name.equals(name) )
-					return upSym;
-			}
-			if( parent != null ) {
-				LocalSym sym = parent.getLocalSym(name);
-				if( sym != null ) {
-					sym.isPointer = true;
-					return addUpSym(name,sym.javaName);
-				}
-				UpSym upSym = parent.getUpSym(name);
-				if( upSym != null ) {
-					return addUpSym(name,"parentUpValues["+upSym.i+"]");
-				}
-			}
-			return null;
-		}
-
-		Sym getSym(String name) {
-			Sym sym = getLocalSym(name);
-			return sym != null ? sym : getUpSym(name);
-		}
-
-	}
-
-	private static class In {
-		static final In NOTHING = new In(false);
-
-		final boolean template;
-
-		private In(boolean template) {
-			this.template = template;
-		}
-
-		In template() {
-			return template ? this : new In(true);
-		}
-	}
-
-	private Frame frame;
-	private final Parser parser;
-	private final Stmts top;
-
-	LuanParser(String sourceText,String sourceName) {
-		this.frame = new Frame();
-		this.parser = new Parser(sourceText,sourceName);
-		this.top = new Stmts();
-	}
-
-	void addVar(String name) {
-		UpSym upSym = frame.addUpSym( "-ADDED-" ,"new Pointer()");
-		LocalSym sym = frame.addLocalSym( name );
-		sym.isPointer = true;
-		top.add( "final Pointer " + sym.javaName + " = upValues[" + upSym.i + "];  " );
-	}
-
-	private int symbolsSize() {
-		return frame.symbols.size();
-	}
-
-	private Stmts addSymbol(String name,Expr value) {
-		final LocalSym sym = frame.addLocalSym(name);
-		return sym.declaration(value);
-	}
-
-	private Sym getSym(String name) {
-		return frame.getSym(name);
-	}
-
-	private void popSymbols(int n) {
-		List<LocalSym> symbols = frame.symbols;
-		while( n-- > 0 ) {
-			symbols.remove(symbols.size()-1);
-		}
-	}
-
-	private void incLoops() {
-		frame.loops++;
-	}
-
-	private void decLoops() {
-		frame.loops--;
-	}
-
-	private <T> T required(T t) throws ParseException {
-		if( t==null )
-			throw parser.exception();
-		return t;
-	}
-
-	private <T> T required(T t,String msg) throws ParseException {
-		if( t==null )
-			throw parser.exception(msg);
-		return t;
-	}
-
-	private Class newFnClass(Stmts stmt) {
-		return toFnClass( stmt, frame.upValueSymbols );
-	}
-
-	private Expr newFnExp(Stmts stmt,String name) {
-		return toFnExp( stmt, frame.upValueSymbols, name );
-	}
-/*
-	Class Expression() throws ParseException {
-		Spaces();
-		parser.begin();
-		Expr expr = ExprZ(In.NOTHING);
-		if( expr != null && parser.endOfInput() ) {
-			top.add( "return " );
-			top.addAll( expr );
-			top.add( ";  " );
-			top.hasReturn = true;
-			return parser.success(newFnClass(top));
-		}
-		return parser.failure(null);
-	}
-*/
-	Class RequiredModule() throws ParseException {
-		GetRequiredModule();
-		return newFnClass(top);
-	}
-
-	String RequiredModuleSource() throws ParseException {
-		GetRequiredModule();
-		return toFnString( top, frame.upValueSymbols );
-	}
-
-	void GetRequiredModule() throws ParseException {
-		Spaces();
-		parser.begin();
-		frame.isVarArg = true;
-		top.add( "final Object[] varArgs = LuanImpl.varArgs(args,0);  " );
-		Stmts block = RequiredBlock();
-		top.addAll( block );
-		top.hasReturn = block.hasReturn;
-		if( !parser.endOfInput() )
-			throw parser.exception();
-		parser.success();
-	}
-
-	private Stmts RequiredBlock() throws ParseException {
-		Stmts stmts = new Stmts();
-		int stackStart = symbolsSize();
-		do {
-			Spaces();
-			stmts.addNewLines();
-			Stmts stmt = Stmt();
-			if( stmt != null ) {
-				stmts.addAll(stmt);
-				stmts.hasReturn = stmt.hasReturn;
-			}
-		} while( !stmts.hasReturn && (StmtSep() || TemplateSep(stmts)) );
-		Spaces();
-		while( StmtSep() )
-			Spaces();
-		stmts.addNewLines();
-		int stackEnd = symbolsSize();
-		popSymbols( stackEnd - stackStart );
-		return stmts;
-	}
-
-	private boolean StmtSep() throws ParseException {
-		return parser.match( ';' ) || EndOfLine();
-	}
-
-	private boolean TemplateSep(Stmts stmts) throws ParseException {
-		Stmts stmt = TemplateStmt();
-		if( stmt != null ) {
-			stmts.addAll(stmt);
-			return true;
-		}
-		return false;
-	}
-
-	private boolean EndOfLine() {
-		if( MatchEndOfLine() ) {
-			parser.sb().append('\n');
-			return true;
-		} else {
-			return false;
-		}
-	}
-
-	private boolean MatchEndOfLine() {
-		return parser.match( "\r\n" ) || parser.match( '\r' ) || parser.match( '\n' );
-	}
-
-	private Stmts Stmt() throws ParseException {
-		Stmts stmt;
-		if( (stmt=ReturnStmt()) != null
-			|| (stmt=FunctionStmt()) != null
-			|| (stmt=LocalStmt()) != null
-			|| (stmt=LocalFunctionStmt()) != null
-			|| (stmt=BreakStmt()) != null
-			|| (stmt=ForStmt()) != null
-			|| (stmt=DoStmt()) != null
-			|| (stmt=WhileStmt()) != null
-			|| (stmt=RepeatStmt()) != null
-			|| (stmt=IfStmt()) != null
-			|| (stmt=SetStmt()) != null
-			|| (stmt=ExpressionsStmt()) != null
-		) {
-			return stmt;
-		}
-		return null;
-	}
-
-	private Expr indexExpStr(Expr exp1,Expr exp2) {
-		Expr exp = new Expr(Val.SINGLE,false);
-		exp.add( "luan.index(" );
-		exp.addAll( exp1.single() );
-		exp.add( "," );
-		exp.addAll( exp2.single() );
-		exp.add( ")" );
-		return exp;
-	}
-
-	private Expr callExpStr(Expr fn,Expr args) {
-		Expr exp = new Expr(null,true);
-		exp.add( "Luan.checkFunction(" );
-		exp.addAll( fn.single() );
-		exp.add( ").call(luan," );
-		exp.addAll( args.array() );
-		exp.add( ")" );
-		return exp;
-	}
-
-	private Stmts TemplateStmt() throws ParseException {
-		Expr exprs = TemplateExpressions(In.NOTHING);
-		if( exprs == null )
-			return null;
-		Stmts stmt = new Stmts();
-		stmt.add( "Luan.checkFunction(luan.index(PackageLuan.require(luan,\"luan:Io.luan\"),\"template_write\")).call(luan," );
-		stmt.addAll( exprs.array() );
-		stmt.add( ");  " );
-		return stmt;
-	}
-
-	private Expr TemplateExpressions(In in) throws ParseException {
-		if( in.template )
-			return null;
-		int start = parser.begin();
-		if( !parser.match( "%>" ) )
-			return parser.failure(null);
-		EndOfLine();
-		In inTemplate = in.template();
-		List<Expr> builder = new ArrayList<Expr>();
-		while(true) {
-			if( parser.match( "<%=" ) ) {
-				Spaces();
-				Expr exp = new Expr(Val.SINGLE,false);
-				exp.addAll( RequiredExpr(inTemplate).single() );
-				builder.add(exp);
-				RequiredMatch( "%>" );
-			} else if( parser.match( "<%" ) ) {
-				Spaces();
-				return parser.success(expString(builder));
-			} else {
-				Expr exp = new Expr(Val.SINGLE,false);
-				int i = parser.currentIndex();
-				do {
-					if( parser.match( "%>" ) )
-						throw parser.exception("'%>' unexpected");
-					if( !(EndOfLine() || parser.anyChar()) )
-						throw parser.exception("Unclosed template expression");
-				} while( !parser.test( "<%" ) );
-				String match = parser.textFrom(i);
-				String rtns = parser.sb().toString();
-				parser.sb().setLength(0);
-				exp.addAll( constExpStr(match) );
-				if( rtns.length() > 0 )
-					exp.add(rtns);
-				builder.add(exp);
-			}
-		}
-	}
-
-	private Stmts ReturnStmt() throws ParseException {
-		parser.begin();
-		if( !Keyword("return") )
-			return parser.failure(null);
-		Expr exprs = ExpStringList(In.NOTHING);
-		Stmts stmt = new Stmts();
-		stmt.add( "return " );
-		if( exprs != null )
-			stmt.addAll( exprs );
-		else
-			stmt.add( "LuanFunction.NOTHING" );
-		stmt.add( ";  " );
-		stmt.hasReturn = true;
-		return parser.success( stmt );
-	}
-
-	private Stmts FunctionStmt() throws ParseException {
-		parser.begin();
-		if( !Keyword("function") )
-			return parser.failure(null);
-
-		parser.currentIndex();
-		String name = RequiredName();
-		Var var = nameVar(name);
-		while( parser.match( '.' ) ) {
-			Spaces();
-//			Expr exp = NameExpr();
-			name = Name();
-			if( name==null )
-				return parser.failure(null);
-			var = indexVar( var.exp(), constExpStr(name) );
-		}
-
-		Expr fnDef = RequiredFunction(name);
-		return parser.success( var.set(fnDef) );
-	}
-
-	private Stmts LocalFunctionStmt() throws ParseException {
-		parser.begin();
-		if( !(Keyword("local") && Keyword("function")) )
-			return parser.failure(null);
-		Stmts stmt = new Stmts();
-		String name = RequiredName();
-		stmt.addAll( addSymbol(name,null) );
-		Expr fnDef = RequiredFunction(name);
-		stmt.addAll( nameVar(name).set(fnDef) );
-		return parser.success( stmt );
-	}
-
-	private Stmts BreakStmt() throws ParseException {
-		parser.begin();
-		if( !Keyword("break") )
-			return parser.failure(null);
-		if( frame.loops <= 0 )
-			throw parser.exception("'break' outside of loop");
-		Stmts stmt = new Stmts();
-		stmt.add( "break;  " );
-		return parser.success( stmt );
-	}
-
-	int forCounter = 0;
-
-	private Stmts ForStmt() throws ParseException {
-		parser.begin();
-		int stackStart = symbolsSize();
-		if( !Keyword("for") )
-			return parser.failure(null);
-		List<String> names = RequiredNameList();
-		if( !Keyword("in") )
-			return parser.failure(null);
-		Expr expr = RequiredExpr(In.NOTHING).single();
-		RequiredKeyword("do");
-
-		String fnVar = "fn"+ ++forCounter;
-		Expr fnExp = new Expr(null,false);
-		fnExp.add( fnVar + ".call(luan)" );
-		Stmts stmt = new Stmts();
-		stmt.add( ""
-			+"LuanFunction "+fnVar+" = Luan.checkFunction("
-		);
-		stmt.addAll( expr );
-		stmt.add( ");  " );
-		stmt.add( "while(true) {  " );
-		stmt.addAll( makeLocalSetStmt(names,fnExp) );
-		stmt.add( "if( " );
-		stmt.addAll( nameVar(names.get(0)).exp() );
- 		stmt.add( "==null )  break;  " );
-		Stmts loop = RequiredLoopBlock();
-		RequiredKeyword("end");
-		stmt.addAll( loop );
-		stmt.add( "}  " );
-		popSymbols( symbolsSize() - stackStart );
-		return parser.success(stmt);
-	}
-
-	private Stmts DoStmt() throws ParseException {
-		parser.begin();
-		if( !Keyword("do") )
-			return parser.failure(null);
-		Stmts stmt = RequiredBlock();
-		RequiredKeyword("end");
-		return parser.success(stmt);
-	}
-
-	private Stmts LocalStmt() throws ParseException {
-		parser.begin();
-		if( !Keyword("local") )
-			return parser.failure(null);
-		List<String> names = NameList();
-		if( names==null ) {
-			if( Keyword("function") )
-				return parser.failure(null);  // handled later
-			throw parser.exception("Invalid local statement");
-		}
-		Stmts stmt = new Stmts();
-		if( parser.match( '=' ) ) {
-			Spaces();
-			Expr values = ExpStringList(In.NOTHING);
-			if( values==null )
-				throw parser.exception("Expressions expected");
-			stmt.addAll( makeLocalSetStmt(names,values) );
-		} else {
-			Expr value = new Expr(Val.SINGLE,false);
-			value.add( "null" );
-			for( String name : names ) {
-				stmt.addAll( addSymbol(name,value) );
-			}
-		}
-		return parser.success(stmt);
-	}
-
-	private List<String> RequiredNameList() throws ParseException {
-		parser.begin();
-		List<String> names = NameList();
-		if( names==null )
-			throw parser.exception("Name expected");
-		return parser.success(names);
-	}
-
-	private List<String> NameList() throws ParseException {
-		String name = Name();
-		if( name==null )
-			return null;
-		List<String> names = new ArrayList<String>();
-		names.add(name);
-		while( (name=anotherName()) != null ) {
-			names.add(name);
-		}
-		return names;
-	}
-
-	private String anotherName() throws ParseException {
-		parser.begin();
-		if( !parser.match( ',' ) )
-			return parser.failure(null);
-		Spaces();
-		String name = Name();
-		if( name==null )
-			return parser.failure(null);
-		return parser.success(name);
-	}
-
-	private Stmts WhileStmt() throws ParseException {
-		parser.begin();
-		if( !Keyword("while") )
-			return parser.failure(null);
-		Expr cnd = RequiredExpr(In.NOTHING).single();
-		RequiredKeyword("do");
-		Stmts loop = RequiredLoopBlock();
-		RequiredKeyword("end");
-		Stmts stmt = new Stmts();
-		stmt.add( "while( Luan.checkBoolean(" );
-		stmt.addAll( cnd );
-		stmt.add( ") ) {  " );
-		stmt.addAll( loop );
-		stmt.add( "}  " );
-		return parser.success( stmt );
-	}
-
-	private Stmts RepeatStmt() throws ParseException {
-		parser.begin();
-		if( !Keyword("repeat") )
-			return parser.failure(null);
-		Stmts loop =RequiredLoopBlock();
-		RequiredKeyword("until");
-		Expr cnd = RequiredExpr(In.NOTHING).single();
-		Stmts stmt = new Stmts();
-		stmt.add( "do {  " );
-		stmt.addAll( loop );
-		stmt.add( "} while( !Luan.checkBoolean(" );
-		stmt.addAll( cnd );
-		stmt.add( ") );  " );
-		return parser.success( stmt );
-	}
-
-	private Stmts RequiredLoopBlock() throws ParseException {
-		incLoops();
-		Stmts stmt = RequiredBlock();
-		decLoops();
-		return stmt;
-	}
-
-	private Stmts IfStmt() throws ParseException {
-		parser.begin();
-		if( !Keyword("if") )
-			return parser.failure(null);
-		Stmts stmt = new Stmts();
-		Expr cnd;
-		Stmts block;
-		boolean hasReturn = true;
-		cnd = RequiredExpr(In.NOTHING).single();
-		RequiredKeyword("then");
-		block = RequiredBlock();
-		stmt.add( "if( Luan.checkBoolean(" );
-		stmt.addAll( cnd );
-		stmt.add( ") ) {  " );
-		stmt.addAll( block );
-		if( !block.hasReturn )
-			hasReturn = false;
-		while( Keyword("elseif") ) {
-			cnd = RequiredExpr(In.NOTHING).single();
-			RequiredKeyword("then");
-			block = RequiredBlock();
-			stmt.add( "} else if( Luan.checkBoolean(" );
-			stmt.addAll( cnd );
-			stmt.add( ") ) {  " );
-			stmt.addAll( block );
-			if( !block.hasReturn )
-				hasReturn = false;
-		}
-		if( Keyword("else") ) {
-			block = RequiredBlock();
-			stmt.add( "} else {  " );
-			stmt.addAll( block );
-			if( !block.hasReturn )
-				hasReturn = false;
-		} else {
-			hasReturn = false;
-		}
-		RequiredKeyword("end");
-		stmt.add( "}  " );
-		stmt.hasReturn = hasReturn;
-		return parser.success( stmt );
-	}
-
-	private Stmts SetStmt() throws ParseException {
-		parser.begin();
-		List<Var> vars = new ArrayList<Var>();
-		Var v = SettableVar();
-		if( v == null )
-			return parser.failure(null);
-		vars.add(v);
-		while( parser.match( ',' ) ) {
-			Spaces();
-			v = SettableVar();
-			if( v == null )
-				return parser.failure(null);
-			vars.add(v);
-		}
-		if( !parser.match( '=' ) )
-			return parser.failure(null);
-		Spaces();
-		Expr values = ExpStringList(In.NOTHING);
-		if( values==null )
-//			throw parser.exception("Expressions expected");
-			return parser.failure(null);
-		return parser.success( makeSetStmt(vars,values) );
-	}
-
-	private Stmts makeSetStmt(List<Var> vars,Expr values) throws ParseException {
-		int n = vars.size();
-		if( n == 1 )
-			return vars.get(0).set(values);
-		Stmts stmt = new Stmts();
-		String varName = values.valType==Val.ARRAY ? "a" : "t";
-		stmt.add( varName + " = " );
-		stmt.addAll( values );
-		stmt.add( ";  " );
-		Expr t = new Expr(values.valType,false);
-		t.add( varName );
-		t = t.single();
-		stmt.addAll( vars.get(0).set(t) );
-		for( int i=1; i<n; i++ ) {
-			t.clear();
-			t.add( "LuanImpl.pick(" + varName + ","+i+")" );
-			stmt.addAll( vars.get(i).set(t) );
-		}
-		return stmt;
-	}
-
-	private Stmts makeLocalSetStmt(List<String> names,Expr values) throws ParseException {
-		int n = names.size();
-		if( n == 1 )
-			return addSymbol(names.get(0),values.single());
-		Stmts stmt = new Stmts();
-		String varName = values.valType==Val.ARRAY ? "a" : "t";
-		stmt.add( varName + " = " );
-		stmt.addAll( values );
-		stmt.add( ";  " );
-		Expr t = new Expr(values.valType,false);
-		t.add( varName );
-		t = t.single();
-		stmt.addAll( addSymbol(names.get(0),t) );
-		for( int i=1; i<n; i++ ) {
-			t.clear();
-			t.add( "LuanImpl.pick(" + varName + ","+i+")" );
-			stmt.addAll( addSymbol(names.get(i),t) );
-		}
-		return stmt;
-	}
-
-	private Stmts ExpressionsStmt() throws ParseException {
-		parser.begin();
-		Expr exp = ExprZ(In.NOTHING);
-		if( exp != null && exp.isStmt ) {
-			Stmts stmt = new Stmts();
-			if( exp.valType==Val.SINGLE ) {
-				stmt.add( "LuanImpl.nop(" );
-				stmt.addAll( exp );
-				stmt.add( ")" );
-			} else {
-				stmt.addAll( exp );
-			}
-			stmt.add( ";  " );
-			return parser.success( stmt );
-		}
-		return parser.failure(null);
-	}
-
-	private Var SettableVar() throws ParseException {
-		int start = parser.begin();
-		Var var = VarZ(In.NOTHING);
-		if( var==null || !var.isSettable() )
-			return parser.failure(null);
-		return parser.success( var );
-	}
-
-	private Expr RequiredExpr(In in) throws ParseException {
-		parser.begin();
-		return parser.success(required(ExprZ(in),"Bad expression"));
-	}
-
-	private Expr ExprZ(In in) throws ParseException {
-		return OrExpr(in);
-	}
-
-	private Expr OrExpr(In in) throws ParseException {
-		parser.begin();
-		Expr exp = AndExpr(in);
-		if( exp==null )
-			return parser.failure(null);
-		while( Keyword("or") ) {
-			exp = exp.single();
-			Expr exp2 = required(AndExpr(in)).single();
-			Expr newExp = new Expr(Val.SINGLE,true);
-			newExp.add( "(LuanImpl.cnd(t = " );
-			newExp.addAll( exp );
-			newExp.add( ") ? t : (" );
-			newExp.addAll( exp2 );
-			newExp.add( "))" );
-			exp = newExp;
-		}
-		return parser.success(exp);
-	}
-
-	private Expr AndExpr(In in) throws ParseException {
-		parser.begin();
-		Expr exp = RelExpr(in);
-		if( exp==null )
-			return parser.failure(null);
-		while( Keyword("and") ) {
-			exp = exp.single();
-			Expr exp2 = required(RelExpr(in)).single();
-			Expr newExp = new Expr(Val.SINGLE,true);
-			newExp.add( "(LuanImpl.cnd(t = " );
-			newExp.addAll( exp );
-			newExp.add( ") ? (" );
-			newExp.addAll( exp2 );
-			newExp.add( ") : t)" );
-			exp = newExp;
-		}
-		return parser.success(exp);
-	}
-
-	private Expr RelExpr(In in) throws ParseException {
-		parser.begin();
-		Expr exp = ConcatExpr(in);
-		if( exp==null )
-			return parser.failure(null);
-		while(true) {
-			if( parser.match("==") ) {
-				Spaces();
-				exp = exp.single();
-				Expr exp2 = required(ConcatExpr(in)).single();
-				Expr newExp = new Expr(Val.SINGLE,false);
-				newExp.add( "LuanImpl.eq(luan," );
-				newExp.addAll( exp );
-				newExp.add( "," );
-				newExp.addAll( exp2 );
-				newExp.add( ")" );
-				exp = newExp;
-			} else if( parser.match("~=") ) {
-				Spaces();
-				exp = exp.single();
-				Expr exp2 = required(ConcatExpr(in)).single();
-				Expr newExp = new Expr(Val.SINGLE,false);
-				newExp.add( "!LuanImpl.eq(luan," );
-				newExp.addAll( exp );
-				newExp.add( "," );
-				newExp.addAll( exp2 );
-				newExp.add( ")" );
-				exp = newExp;
-			} else if( parser.match("<=") ) {
-				Spaces();
-				exp = exp.single();
-				Expr exp2 = required(ConcatExpr(in)).single();
-				Expr newExp = new Expr(Val.SINGLE,false);
-				newExp.add( "LuanImpl.le(luan," );
-				newExp.addAll( exp );
-				newExp.add( "," );
-				newExp.addAll( exp2 );
-				newExp.add( ")" );
-				exp = newExp;
-			} else if( parser.match(">=") ) {
-				Spaces();
-				exp = exp.single();
-				Expr exp2 = required(ConcatExpr(in)).single();
-				Expr newExp = new Expr(Val.SINGLE,false);
-				newExp.add( "LuanImpl.le(luan," );
-				newExp.addAll( exp2 );
-				newExp.add( "," );
-				newExp.addAll( exp );
-				newExp.add( ")" );
-				exp = newExp;
-			} else if( parser.match("<") ) {
-				Spaces();
-				exp = exp.single();
-				Expr exp2 = required(ConcatExpr(in)).single();
-				Expr newExp = new Expr(Val.SINGLE,false);
-				newExp.add( "LuanImpl.lt(luan," );
-				newExp.addAll( exp );
-				newExp.add( "," );
-				newExp.addAll( exp2 );
-				newExp.add( ")" );
-				exp = newExp;
-			} else if( parser.match(">") ) {
-				Spaces();
-				exp = exp.single();
-				Expr exp2 = required(ConcatExpr(in)).single();
-				Expr newExp = new Expr(Val.SINGLE,false);
-				newExp.add( "LuanImpl.lt(luan," );
-				newExp.addAll( exp2 );
-				newExp.add( "," );
-				newExp.addAll( exp );
-				newExp.add( ")" );
-				exp = newExp;
-			} else
-				break;
-		}
-		return parser.success(exp);
-	}
-
-	private Expr ConcatExpr(In in) throws ParseException {
-		parser.begin();
-		Expr exp = SumExpr(in);
-		if( exp==null )
-			return parser.failure(null);
-		if( parser.match("..") ) {
-			Spaces();
-			exp = exp.single();
-			Expr exp2 = required(ConcatExpr(in)).single();
-			Expr newExp = new Expr(Val.SINGLE,false);
-			newExp.add( "LuanImpl.concat(luan," );
-			newExp.addAll( exp );
-			newExp.add( "," );
-			newExp.addAll( exp2 );
-			newExp.add( ")" );
-			exp = newExp;
-		}
-		return parser.success(exp);
-	}
-
-	private Expr SumExpr(In in) throws ParseException {
-		parser.begin();
-		Expr exp = TermExpr(in);
-		if( exp==null )
-			return parser.failure(null);
-		while(true) {
-			if( parser.match('+') ) {
-				Spaces();
-				exp = exp.single();
-				Expr exp2 = required(TermExpr(in)).single();
-				Expr newExp = new Expr(Val.SINGLE,false);
-				newExp.add( "LuanImpl.add(luan," );
-				newExp.addAll( exp );
-				newExp.add( "," );
-				newExp.addAll( exp2 );
-				newExp.add( ")" );
-				exp = newExp;
-			} else if( Minus() ) {
-				Spaces();
-				exp = exp.single();
-				Expr exp2 = required(TermExpr(in)).single();
-				Expr newExp = new Expr(Val.SINGLE,false);
-				newExp.add( "LuanImpl.sub(luan," );
-				newExp.addAll( exp );
-				newExp.add( "," );
-				newExp.addAll( exp2 );
-				newExp.add( ")" );
-				exp = newExp;
-			} else
-				break;
-		}
-		return parser.success(exp);
-	}
-
-	private boolean Minus() {
-		parser.begin();
-		return parser.match('-') && !parser.match('-') ? parser.success() : parser.failure();
-	}
-
-	private Expr TermExpr(In in) throws ParseException {
-		parser.begin();
-		Expr exp = UnaryExpr(in);
-		if( exp==null )
-			return parser.failure(null);
-		while(true) {
-			if( parser.match('*') ) {
-				Spaces();
-				exp = exp.single();
-				Expr exp2 = required(UnaryExpr(in)).single();
-				Expr newExp = new Expr(Val.SINGLE,false);
-				newExp.add( "LuanImpl.mul(luan," );
-				newExp.addAll( exp );
-				newExp.add( "," );
-				newExp.addAll( exp2 );
-				newExp.add( ")" );
-				exp = newExp;
-			} else if( parser.match('/') ) {
-				Spaces();
-				exp = exp.single();
-				Expr exp2 = required(UnaryExpr(in)).single();
-				Expr newExp = new Expr(Val.SINGLE,false);
-				newExp.add( "LuanImpl.div(luan," );
-				newExp.addAll( exp );
-				newExp.add( "," );
-				newExp.addAll( exp2 );
-				newExp.add( ")" );
-				exp = newExp;
-			} else if( Mod() ) {
-				Spaces();
-				exp = exp.single();
-				Expr exp2 = required(UnaryExpr(in)).single();
-				Expr newExp = new Expr(Val.SINGLE,false);
-				newExp.add( "LuanImpl.mod(luan," );
-				newExp.addAll( exp );
-				newExp.add( "," );
-				newExp.addAll( exp2 );
-				newExp.add( ")" );
-				exp = newExp;
-			} else
-				break;
-		}
-		return parser.success(exp);
-	}
-
-	private boolean Mod() {
-		parser.begin();
-		return parser.match('%') && !parser.match('>') ? parser.success() : parser.failure();
-	}
-
-	private Expr UnaryExpr(In in) throws ParseException {
-		parser.begin();
-		if( parser.match('#') ) {
-			Spaces();
-			Expr exp = required(UnaryExpr(in)).single();
-			Expr newExp = new Expr(Val.SINGLE,false);
-			newExp.add( "LuanImpl.len(luan," );
-			newExp.addAll( exp );
-			newExp.add( ")" );
-			return parser.success(newExp);
-		}
-		if( Minus() ) {
-			Spaces();
-			Expr exp = required(UnaryExpr(in)).single();
-			Expr newExp = new Expr(Val.SINGLE,false);
-			newExp.add( "LuanImpl.unm(luan," );
-			newExp.addAll( exp );
-			newExp.add( ")" );
-			return parser.success(newExp);
-		}
-		if( Keyword("not") ) {
-			Spaces();
-			Expr exp = required(UnaryExpr(in)).single();
-			Expr newExp = new Expr(Val.SINGLE,false);
-			newExp.add( "!Luan.checkBoolean(" );
-			newExp.addAll( exp );
-			newExp.add( ")" );
-			return parser.success(newExp);
-		}
-		Expr exp = PowExpr(in);
-		if( exp==null )
-			return parser.failure(null);
-		return parser.success(exp);
-	}
-
-	private Expr PowExpr(In in) throws ParseException {
-		parser.begin();
-		Expr exp1 = SingleExpr(in);
-		if( exp1==null )
-			return parser.failure(null);
-		if( parser.match('^') ) {
-			Spaces();
-			Expr exp2 = required(PowExpr(in));
-			Expr newExp = new Expr(Val.SINGLE,false);
-			newExp.add( "LuanImpl.pow(luan," );
-			newExp.addAll( exp1.single() );
-			newExp.add( "," );
-			newExp.addAll( exp2.single() );
-			newExp.add( ")" );
-			exp1 = newExp;
-		}
-		return parser.success(exp1);
-	}
-
-	private Expr SingleExpr(In in) throws ParseException {
-		parser.begin();
-		Expr exp = FunctionExpr();
-		if( exp != null )
-			return parser.success(exp);
-		exp = VarExp(in);
-		if( exp != null )
-			return parser.success(exp);
-		exp = VarArgs();
-		if( exp != null )
-			return parser.success(exp);
-		return parser.failure(null);
-	}
-
-	private Expr FunctionExpr() throws ParseException {
-		if( !Keyword("function") )
-			return null;
-		return RequiredFunction(null);
-	}
-
-	private Expr RequiredFunction(String name) throws ParseException {
-		parser.begin();
-		RequiredMatch('(');
-		Spaces();
-		frame = new Frame(frame);
-		Stmts stmt = new Stmts();
-		List<String> names = NameList();
-		if( names != null ) {
-			Expr args = new Expr(Val.ARRAY,false);
-			args.add( "args" );
-			stmt.addAll( makeLocalSetStmt(names,args) );
-			if( parser.match(',') ) {
-				Spaces();
-				if( !parser.match("...") )
-					throw parser.exception();
-				Spaces();
-				frame.isVarArg = true;
-				stmt.add( "final Object[] varArgs = LuanImpl.varArgs(args," + names.size() + ");  " );
-			}
-		} else if( parser.match("...") ) {
-			Spaces();
-			frame.isVarArg = true;
-			stmt.add( "final Object[] varArgs = LuanImpl.varArgs(args,0);  " );
-		}
-		RequiredMatch(')');
-		Spaces();
-		Stmts block = RequiredBlock();
-		stmt.addAll( block );
-		stmt.hasReturn = block.hasReturn;
-		Expr fnDef = newFnExp(stmt,name);
-		RequiredKeyword("end");
-		frame = frame.parent;
-		return parser.success(fnDef);
-	}
-
-	private Expr VarArgs() throws ParseException {
-		parser.begin();
-		if( !frame.isVarArg || !parser.match("...") )
-			return parser.failure(null);
-		Spaces();
-		Expr exp = new Expr(Val.ARRAY,false);
-		exp.add("varArgs");
-		return parser.success(exp);
-	}
-
-	private Expr TableExpr() throws ParseException {
-		parser.begin();
-		if( !parser.match('{') )
-			return parser.failure(null);
-		Expr tblExp = new Expr(Val.SINGLE,false);
-		tblExp.add( "LuanImpl.table(" );
-		Expr lastExp = tblExp;
-		List<Expr> builder = new ArrayList<Expr>();
-/*
-		Spaces();
-		Field(builder);
-		while( FieldSep() ) {
-			Spaces();
-			Field(builder);
-		}
-*/
-		do {
-			Spaces();  lastExp.addNewLines();
-			Expr exp = Field();
-			if( exp != null ) {
-				builder.add(exp);
-				lastExp = exp;
-				Spaces();  lastExp.addNewLines();
-			}
-		} while( FieldSep() );
-		Expr exp = TemplateExpressions(In.NOTHING);
-		if( exp != null )
-			builder.add(exp);
-		if( !parser.match('}') )
-			throw parser.exception("Expected table element or '}'");
-		tblExp.addAll( expString(builder).array() );
-		tblExp.add( ")" );
-		Spaces();
-		tblExp.addNewLines();
-		return parser.success( tblExp );
-	}
-
-	private boolean FieldSep() throws ParseException {
-		return parser.anyOf(",;") || EndOfLine();
-	}
-
-	private Expr Field() throws ParseException {
-		parser.begin();
-		Expr exp = SubExpr(In.NOTHING);
-		if( exp==null )
-			exp = NameExpr();
-		if( exp!=null && parser.match('=') ) {
-			Spaces();
-			Expr val = RequiredExpr(In.NOTHING).single();
-			Expr newExp = new Expr(Val.SINGLE,false);
-			newExp.add( "new TableField(" );
-			newExp.addAll( exp );
-			newExp.add( "," );
-			newExp.addAll( val );
-			newExp.add( ")" );
-			return parser.success(newExp);
-		}
-		parser.rollback();
-		Expr exprs = ExprZ(In.NOTHING);
-		if( exprs != null ) {
-			return parser.success(exprs);
-		}
-		return parser.failure(null);
-	}
-
-	private Expr VarExp(In in) throws ParseException {
-		Var var = VarZ(in);
-		return var==null ? null : var.exp();
-	}
-
-	private Var VarZ(In in) throws ParseException {
-		parser.begin();
-		Var var = VarStart(in);
-		if( var==null )
-			return parser.failure(null);
-		Var var2;
-		while( (var2=Var2(in,var.exp())) != null ) {
-			var = var2;
-		}
-		return parser.success(var);
-	}
-
-	private Var VarStart(In in) throws ParseException {
-		if( parser.match('(') ) {
-			Spaces();
-			Expr exp = RequiredExpr(in).single();
-			RequiredMatch(')');
-			Spaces();
-			return exprVar(exp);
-		}
-		String name = Name();
-		if( name != null )
-			return nameVar(name);
-		Expr exp;
-		exp = TableExpr();
-		if( exp != null )
-			return exprVar(exp);
-		exp = Literal();
-		if( exp != null )
-			return exprVar(exp);
-		return null;
-	}
-
-	private Var Var2(In in,Expr exp1) throws ParseException {
-		parser.begin();
-		Expr exp2 = SubExpr(in);
-		if( exp2 != null )
-			return parser.success(indexVar(exp1,exp2));
-		if( parser.match('.') ) {
-			Spaces();
-			exp2 = NameExpr();
-			if( exp2!=null )
-				return parser.success(indexVar(exp1,exp2));
-			return parser.failure(null);
-		}
-		Expr fnCall = Args( in, exp1, new ArrayList<Expr>() );
-		if( fnCall != null )
-			return parser.success(exprVar(fnCall));
-		return parser.failure(null);
-	}
-
-	private interface Var {
-		public Expr exp() throws ParseException;
-//		public Settable settable() throws ParseException;
-		public boolean isSettable();
-		public Stmts set(Expr val) throws ParseException;
-	}
-
-	private Expr env() {
-		Sym sym = getSym("_ENV");
-		if( sym != null )
-			return sym.exp();
-		return null;
-	}
-
-	private Var nameVar(final String name) {
-		return new Var() {
-
-			public Expr exp() throws ParseException {
-				Sym sym = getSym(name);
-				if( sym != null )
-					return sym.exp();
-				Expr envExpr = env();
-				if( envExpr != null )
-					return indexExpStr( envExpr, constExpStr(name) );
-				parser.failure(null);
-				throw parser.exception("name '"+name+"' not defined");
-			}
-
-			public boolean isSettable() {
-				return true;
-			}
-
-			public Stmts set(Expr val) throws ParseException {
-				Sym sym = getSym(name);
-				if( sym != null ) {
-					Stmts stmt = new Stmts();
-					stmt.addAll( sym.exp() );
-					stmt.add( " = " );
-					stmt.addAll( val.single() );
-					stmt.add( ";  " );
-					return stmt;
-				}
-				Expr envExpr = env();
-				if( envExpr != null )
-					return indexVar( envExpr, constExpStr(name) ).set(val);
-				parser.failure(null);
-				throw parser.exception("name '"+name+"' not defined");
-			}
-		};
-	}
-
-	private Var exprVar(final Expr expr) {
-		return new Var() {
-
-			public Expr exp() {
-				return expr;
-			}
-
-			public boolean isSettable() {
-				return false;
-			}
-
-			public Stmts set(Expr val) {
-				throw new RuntimeException();
-			}
-		};
-	}
-
-	private Var indexVar(final Expr table,final Expr key) {
-		return new Var() {
-
-			public Expr exp() {
-				return indexExpStr( table, key );
-			}
-
-			public boolean isSettable() {
-				return true;
-			}
-
-			public Stmts set(Expr val) {
-				Stmts stmt = new Stmts();
-				stmt.add( "LuanImpl.put(luan," );
-				stmt.addAll( table.single() );
-				stmt.add( "," );
-				stmt.addAll( key.single() );
-				stmt.add( "," );
-				stmt.addAll( val.single() );
-				stmt.add( ");  " );
-				return stmt;
-			}
-		};
-	}
-
-	private Expr Args(In in,Expr fn,List<Expr> builder) throws ParseException {
-		parser.begin();
-		return args(in,builder)
-			? parser.success( callExpStr( fn, expString(builder) ) )
-			: parser.failure((Expr)null);
-	}
-
-	private boolean args(In in,List<Expr> builder) throws ParseException {
-		parser.begin();
-		if( parser.match('(') ) {
-			Spaces();
-			ExpList(in,builder);  // optional
-			if( !parser.match(')') )
-				throw parser.exception("Expression or ')' expected");
-			Spaces();
-			return parser.success();
-		}
-		Expr exp = TableExpr();
-		if( exp != null ) {
-			builder.add(exp);
-			return parser.success();
-		}
-		exp = StringLiteral();
-		if( exp != null ) {
-			builder.add(exp);
-			return parser.success();
-		}
-		return parser.failure();
-	}
-
-	private Expr ExpStringList(In in) throws ParseException {
-		List<Expr> builder = new ArrayList<Expr>();
-		return ExpList(in,builder) ? expString(builder) : null;
-	}
-
-	private boolean ExpList(In in,List<Expr> builder) throws ParseException {
-		parser.begin();
-		Expr exp = TemplateExpressions(in);
-		if( exp != null ) {
-			builder.add(exp);
-			return parser.success();
-		}
-		exp = ExprZ(in);
-		if( exp==null )
-			return parser.failure();
-		exp.addNewLines();
-		builder.add(exp);
-		while( parser.match(',') ) {
-			Spaces();
-			exp = TemplateExpressions(in);
-			if( exp != null ) {
-				builder.add(exp);
-				return parser.success();
-			}
-			exp = RequiredExpr(in);
-			exp.addNewLines();
-			builder.add(exp);
-		}
-		return parser.success();
-	}
-
-	private Expr SubExpr(In in) throws ParseException {
-		parser.begin();
-		if( !parser.match('[') || parser.test("[") || parser.test("=") )
-			return parser.failure(null);
-		Spaces();
-		Expr exp = RequiredExpr(In.NOTHING).single();
-		RequiredMatch(']');
-		Spaces();
-		return parser.success(exp);
-	}
-
-	private Expr NameExpr() throws ParseException {
-		parser.begin();
-		String name = Name();
-		if( name==null )
-			return parser.failure(null);
-		return parser.success(constExpStr(name));
-	}
-
-	private String RequiredName() throws ParseException {
-		parser.begin();
-		String name = Name();
-		if( name==null )
-			throw parser.exception("Name expected");
-		return parser.success(name);
-	}
-
-	private String Name() throws ParseException {
-		int start = parser.begin();
-		if( !NameFirstChar() )
-			return parser.failure(null);
-		while( NameChar() );
-		String match = parser.textFrom(start);
-		if( keywords.contains(match) )
-			return parser.failure(null);
-		Spaces();
-		return parser.success(match);
-	}
-
-	private boolean NameChar() {
-		return NameFirstChar() || Digit();
-	}
-
-	private boolean NameFirstChar() {
-		return parser.inCharRange('a', 'z') || parser.inCharRange('A', 'Z') || parser.match('_');
-	}
-
-	private void RequiredMatch(char c) throws ParseException {
-		if( !parser.match(c) )
-			throw parser.exception("'"+c+"' expected");
-	}
-
-	private void RequiredMatch(String s) throws ParseException {
-		if( !parser.match(s) )
-			throw parser.exception("'"+s+"' expected");
-	}
-
-	private void RequiredKeyword(String keyword) throws ParseException {
-		if( !Keyword(keyword) )
-			throw parser.exception("'"+keyword+"' expected");
-	}
-
-	private boolean Keyword(String keyword) throws ParseException {
-		parser.begin();
-		if( !parser.match(keyword) || NameChar() )
-			return parser.failure();
-		Spaces();
-		return parser.success();
-	}
-
-	private static final Set<String> keywords = new HashSet<String>(Arrays.asList(
-		"and",
-		"break",
-		"do",
-		"else",
-		"elseif",
-		"end",
-		"false",
-		"for",
-		"function",
-		"goto",
-		"if",
-		"in",
-		"local",
-		"nil",
-		"not",
-		"or",
-		"repeat",
-		"return",
-		"then",
-		"true",
-		"until",
-		"while"
-	));
-
-	private Expr Literal() throws ParseException {
-		parser.begin();
-		if( NilLiteral() ) {
-			Expr exp = new Expr(Val.SINGLE,false);
-			exp.add( "null" );
-			return parser.success(exp);
-		}
-		Boolean b = BooleanLiteral();
-		if( b != null ) {
-			Expr exp = new Expr(Val.SINGLE,false);
-			exp.add( b.toString() );
-			return parser.success(exp);
-		}
-		Number n = NumberLiteral();
-		if( n != null ) {
-			String s = n.toString();
-			if( n instanceof Long )
-				s += "L";
-			Expr exp = new Expr(Val.SINGLE,false);
-			exp.add( s );
-			return parser.success(exp);
-		}
-		Expr s = StringLiteral();
-		if( s != null )
-			return parser.success(s);
-		return parser.failure(null);
-	}
-
-	private static int STR_LIM = 65000;
-
-	private Expr constExpStr(String s) {
-		s = s
-			.replace("\\","\\\\")
-			.replace("\"","\\\"")
-			.replace("\n","\\n")
-			.replace("\r","\\r")
-			.replace("\t","\\t")
-			.replace("\b","\\b")
-		;
-		if( s.length() > STR_LIM ) {
-			int len = s.length();
-			StringBuilder sb = new StringBuilder();
-			sb.append( "LuanImpl.strconcat(" );
-			int start = 0;
-			while(true) {
-				int end = start + STR_LIM;
-				if( end >= len )
-					break;
-				sb.append( "\"" ).append( s.substring(start,end) ).append( "\"," );
-				start = end;
-			}
-			sb.append( "\"" ).append( s.substring(start) ).append( "\")" );
-			s = sb.toString();
-		} else
-			s = "\"" + s + "\"";
-		Expr exp = new Expr(Val.SINGLE,false);
-		exp.add( s );
-		return exp;
-	}
-
-	private boolean NilLiteral() throws ParseException {
-		return Keyword("nil");
-	}
-
-	private Boolean BooleanLiteral() throws ParseException {
-		if( Keyword("true") )
-			return true;
-		if( Keyword("false") )
-			return false;
-		return null;
-	}
-
-	private Number NumberLiteral() throws ParseException {
-		parser.begin();
-		Number n;
-		if( parser.matchIgnoreCase("0x") ) {
-			n = HexNumber();
-		} else {
-			n = DecNumber();
-		}
-		if( n==null || NameChar() )
-			return parser.failure(null);
-		Spaces();
-		return parser.success(n);
-	}
-
-	private Number DecNumber() {
-		int start = parser.begin();
-		boolean isInt = true;
-		if( Int() ) {
-			if( parser.match('.') ) {
-				isInt = false;
-				Int();  // optional
-			}
-		} else if( parser.match('.') && Int() ) {
-			// ok
-			isInt = false;
-		} else
-			return parser.failure(null);
-		if( Exponent() )  // optional
-			isInt = false;
-		String s = parser.textFrom(start);
-		if( isInt ) {
-			try {
-				return parser.success(Integer.valueOf(s));
-			} catch(NumberFormatException e) {}
-			try {
-				return parser.success(Long.valueOf(s));
-			} catch(NumberFormatException e) {}
-		}
-		return parser.success(Double.valueOf(s));
-	}
-
-	private boolean Exponent() {
-		parser.begin();
-		if( !parser.matchIgnoreCase("e") )
-			return parser.failure();
-		parser.anyOf("+-");  // optional
-		if( !Int() )
-			return parser.failure();
-		return parser.success();
-	}
-
-	private boolean Int() {
-		if( !Digit() )
-			return false;
-		while( Digit() );
-		return true;
-	}
-
-	private boolean Digit() {
-		return parser.inCharRange('0', '9');
-	}
-
-	private Number HexNumber() {
-		int start = parser.begin();
-		long nLong = 0;
-		double n;
-		if( HexInt() ) {
-			nLong = Long.parseLong(parser.textFrom(start),16);
-			n = (double)nLong;
-			if( parser.match('.') ) {
-				start = parser.currentIndex();
-				if( HexInt() ) {
-					String dec = parser.textFrom(start);
-					n += (double)Long.parseLong(dec,16) / Math.pow(16,dec.length());
-				}
-			}
-		} else if( parser.match('.') && HexInt() ) {
-			String dec = parser.textFrom(start+1);
-			n = (double)Long.parseLong(dec,16) / Math.pow(16,dec.length());
-		} else {
-			return parser.failure(null);
-		}
-		if( parser.matchIgnoreCase("p") ) {
-			parser.anyOf("+-");  // optional
-			start = parser.currentIndex();
-			if( !HexInt() )
-				return parser.failure(null);
-			n *= Math.pow(2,(double)Long.parseLong(parser.textFrom(start)));
-		}
-		if( nLong == n ) {
-			int nInt = (int)nLong;
-			if( nInt == nLong )
-				return parser.success(Integer.valueOf(nInt));
-			return parser.success(Long.valueOf(nLong));
-		}
-		return parser.success(Double.valueOf(n));
-	}
-
-	private boolean HexInt() {
-		if( !HexDigit() )
-			return false;
-		while( HexDigit() );
-		return true;
-	}
-
-
-	private boolean HexDigit() {
-		return Digit() || parser.anyOf("abcdefABCDEF");
-	}
-
-	private Expr StringLiteral() throws ParseException {
-		Expr s;
-		if( (s=QuotedString('"'))==null
-			&& (s=QuotedString('\''))==null
-			&& (s=LongString())==null
-		)
-			return null;
-		Spaces();
-		return s;
-	}
-
-	private Expr LongString() throws ParseException {
-		parser.begin();
-		if( !parser.match('[') )
-			return parser.failure(null);
-		int start = parser.currentIndex();
-		while( parser.match('=') );
-		int nEquals = parser.currentIndex() - start;
-		if( !parser.match('[') )
-			return parser.failure(null);
-		EndOfLine();
-		start = parser.currentIndex();
-		while( !LongBracketsEnd(nEquals) ) {
-			if( !(EndOfLine() || parser.anyChar()) )
-				throw parser.exception("Unclosed long string");
-		}
-		String s = parser.text.substring( start, parser.currentIndex() - nEquals - 2 );
-		String rtns = parser.sb().toString();
-		parser.sb().setLength(0);
-		Expr exp = constExpStr(s);
-		if( rtns.length() > 0 )
-			exp.add(rtns);
-		return parser.success(exp);
-	}
-
-	private Expr QuotedString(char quote) throws ParseException {
-		parser.begin();
-		if( !parser.match(quote) )
-			return parser.failure(null);
-		StringBuilder buf = new StringBuilder();
-		while( !parser.match(quote) ) {
-			Character c = EscSeq();
-			if( c != null ) {
-				buf.append(c);
-			} else {
-				if( parser.test('\r') || parser.test('\n') || !parser.anyChar() )
-					throw parser.exception("Unclosed string");
-				buf.append(parser.lastChar());
-			}
-		}
-		return parser.success(constExpStr(buf.toString()));
-	}
-
-	private Character EscSeq() {
-		parser.begin();
-		if( !parser.match('\\') )
-			return parser.failure(null);
-		if( parser.match('a') )  return parser.success('\u0007');
-		if( parser.match('b') )  return parser.success('\b');
-		if( parser.match('f') )  return parser.success('\f');
-		if( parser.match('n') )  return parser.success('\n');
-		if( parser.match('r') )  return parser.success('\r');
-		if( parser.match('t') )  return parser.success('\t');
-		if( parser.match('v') )  return parser.success('\u000b');
-		if( parser.match('\\') )  return parser.success('\\');
-		if( parser.match('"') )  return parser.success('"');
-		if( parser.match('\'') )  return parser.success('\'');
-		int start = parser.currentIndex();
-		if( parser.match('x') && HexDigit() && HexDigit() )
-			return parser.success((char)Integer.parseInt(parser.textFrom(start+1),16));
-		if( parser.match('u') && HexDigit() && HexDigit() && HexDigit() && HexDigit() )
-			return parser.success((char)Integer.parseInt(parser.textFrom(start+1),16));
-		if( Digit() ) {
-			if( Digit() ) Digit();  // optional
-			return parser.success((char)Integer.parseInt(parser.textFrom(start)));
-		}
-		if( MatchEndOfLine() ) {
-			return parser.success('\n');
-		}
-		return parser.failure(null);
-	}
-
-	private void Spaces() throws ParseException {
-		while( parser.anyOf(" \t") || Comment() || ContinueOnNextLine() );
-	}
-
-	private boolean ContinueOnNextLine() {
-		parser.begin();
-		if( parser.match('\\') && EndOfLine() ) {
-			parser.upSb();
-			return parser.success();
-		} else
-			return parser.failure();
-	}
-
-	private boolean Comment() throws ParseException {
-		if( LongComment() )
-			return true;
-		if( parser.match("--") ) {
-			while( parser.noneOf("\r\n") );
-			return true;
-		}
-		return false;
-	}
-
-	private boolean LongComment() throws ParseException {
-		parser.begin();
-		if( !parser.match("--[") )
-			return parser.failure();
-		int start = parser.currentIndex();
-		while( parser.match('=') );
-		int nEquals = parser.currentIndex() - start;
-		if( !parser.match('[') )
-			return parser.failure();
-		while( !LongBracketsEnd(nEquals) ) {
-			if( !(EndOfLine() || parser.anyChar()) )
-				throw parser.exception("Unclosed comment");
-		}
-		parser.upSb();
-		return parser.success();
-	}
-
-	private boolean LongBracketsEnd(int nEquals) {
-		parser.begin();
-		if( !parser.match(']') )
-			return parser.failure();
-		while( nEquals-- > 0 ) {
-			if( !parser.match('=') )
-				return parser.failure();
-		}
-		if( !parser.match(']') )
-			return parser.failure();
-		return parser.success();
-	}
-
-
-
-	private class ParseList extends ArrayList {
-
-		void addNewLines() {
-			if( parser.sb().length() > 0 ) {
-				add( parser.sb().toString() );
-				parser.sb().setLength(0);
-/*
-if( parser.sourceName.equals("stdin") ) {
-	StringWriter sw = new StringWriter();
-	new Throwable().printStackTrace(new PrintWriter(sw,true));
-//	add(sw.toString());
-}
-*/
-			}
-		}
-
-		ParseList() {
-			addNewLines();
-		}
-
-		@Override public boolean add(Object obj) {
-			if( obj instanceof List )  throw new RuntimeException();
-			return super.add(obj);
-		}
-
-		@Override public void add(int index,Object obj) {
-			if( obj instanceof List )  throw new RuntimeException();
-			super.add(index,obj);
-		}
-
-		@Override public String toString() {
-			StringBuilder sb = new StringBuilder();
-			for( Object o : this ) {
-				sb.append( o.toString() );
-			}
-			return sb.toString();
-		}
-	}
-
-
-	private static AtomicInteger classCounter = new AtomicInteger();
-
-	private enum Val { SINGLE, ARRAY }
-
-	private class Expr extends ParseList {
-		final Val valType;
-		final boolean isStmt;
-
-		Expr(Val valType,boolean isStmt) {
-			this.valType = valType;
-			this.isStmt = isStmt;
-		}
-
-		Expr single() {
-			if( valType==Val.SINGLE )
-				return this;
-			Expr exp = new Expr(Val.SINGLE,isStmt);
-			exp.add( valType==Val.ARRAY ? "LuanImpl.first(" : "Luan.first(" );
-			exp.addAll( this );
-			exp.add( ")" );
-			return exp;
-		}
-
-		Expr array() {
-			if( valType==Val.ARRAY )
-				return this;
-			Expr exp = new Expr(Val.ARRAY,isStmt);
-			if( valType==Val.SINGLE ) {
-				exp.add( "new Object[]{" );
-				exp.addAll( this );
-				exp.add( "}" );
-			} else {
-				exp.add( "Luan.array(" );
-				exp.addAll( this );
-				exp.add( ")" );
-			}
-			return exp;
-		}
-
-	}
-
-	private Expr expString(List<Expr> list) {
-		Expr exp = new Expr(Val.ARRAY,false);
-		switch(list.size()) {
-		case 0:
-			exp.add("LuanFunction.NOTHING");
-			return exp;
-		case 1:
-			return list.get(0);
-		default:
-			int lastI = list.size() - 1;
-			exp.add( "new Object[]{" );
-			for( int i=0; i<lastI; i++ ) {
-				exp.addAll( list.get(i).single() );
-				exp.add( "," );
-			}
-			Expr last = list.get(lastI);
-			if( last.valType==Val.SINGLE ) {
-				exp.addAll( last );
-				exp.add( "}" );
-			} else {
-				exp.add( "}" );
-				exp.add( 0, "LuanImpl.concatArgs(" );
-				exp.add( "," );
-				exp.addAll( last );
-				exp.add( ")" );
-			}
-			return exp;
-		}
-	}
-
-	private class Stmts extends ParseList {
-		boolean hasReturn = false;
-	}
-
-	private Class toFnClass(Stmts stmts,List<UpSym> upValueSymbols) {
-		String className = "EXP" + classCounter.incrementAndGet();
-		String classCode = toFnString(stmts,upValueSymbols,className);
-		try {
-//System.out.println(parser.sourceName);
-//System.out.println(classCode);
-			return LuanJavaCompiler.compile("luan.impl."+className,parser.sourceName,classCode);
-		} catch(ClassNotFoundException e) {
-			throw new RuntimeException(e);
-		}
-	}
-
-	private String toFnString(Stmts stmts,List<UpSym> upValueSymbols) {
-		String className = "EXP" + classCounter.incrementAndGet();
-		return toFnString(stmts,upValueSymbols,className);
-	}
-
-	private String toFnString(Stmts stmts,List<UpSym> upValueSymbols,String className) {
-		if( !stmts.hasReturn )
-			stmts.add( "\nreturn LuanFunction.NOTHING;" );
-		return ""
-			+"package luan.impl;  "
-			+"import luan.Luan;  "
-			+"import luan.LuanFunction;  "
-			+"import luan.LuanState;  "
-			+"import luan.LuanJava;  "
-			+"import luan.LuanException;  "
-			+"import luan.modules.PackageLuan;  "
-
-			+"public class " + className +" extends Closure {  "
-				+"public "+className+"(LuanJava java) throws LuanException {  "
-					+"super("+upValueSymbols.size()+",java);  "
-					+ init(upValueSymbols)
-				+"}  "
-
-				+"@Override public Object doCall(LuanState luan,Object[] args) throws LuanException {  "
-					+"final Pointer[] parentUpValues = upValues;  "
-					+"Object t;  "
-					+"Object[] a;  "
-					+ stmts
-				+"\n}  "
-			+"}\n"
-		;
-	}
-
-	private Expr toFnExp(Stmts stmt,List<UpSym> upValueSymbols,String name) {
-		stmt.addNewLines();
-		if( !stmt.hasReturn )
-			stmt.add( "return LuanFunction.NOTHING;  " );
-		Expr exp = new Expr(Val.SINGLE,false);
-		exp.add( ""
-			+"new Closure("+upValueSymbols.size()+",java) {  "
-				+"{  "
-				+ init(upValueSymbols)
-				+"}  "
-				+"@Override public Object doCall(LuanState luan,Object[] args) throws LuanException {  "
-		);
-		if( name != null ) {
-			exp.add( ""
-					+"return _" + name + "(luan,args);  "
-				+"}  "
-				+"private Object _" + name + "(LuanState luan,Object[] args) throws LuanException {  "
-			);
-		}
-		exp.add( ""
-					+"final Pointer[] parentUpValues = upValues;  "
-					+"Object t;  "
-					+"Object[] a;  "
-		);
-		exp.addAll( stmt );
-		exp.add( ""
-				+"}  "
-			+"}  "
-		);
-		return exp;
-	}
-
-	private static String init(List<UpSym> upValueSymbols) {
-		StringBuilder sb = new StringBuilder();
-		for( UpSym upSym : upValueSymbols ) {
-			sb.append( upSym.init() );
-		}
-		return sb.toString();
-	}
-
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/impl/ParseException.java
--- a/core/src/luan/impl/ParseException.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,57 +0,0 @@
-package luan.impl;
-
-
-public final class ParseException extends Exception {
-//	public final LuanSource src;
-	public final String sourceName;
-	public final String text;
-	public final int iCurrent;
-	public final int iHigh;
-
-	ParseException(String msg,String text,String sourceName,int iCurrent,int iHigh) {
-		super(msg);
-//		this.src = src;
-		this.text = text;
-		this.sourceName = sourceName;
-		this.iCurrent = iCurrent;
-		this.iHigh = iHigh;
-//System.out.println("iCurrent = "+iCurrent);
-//System.out.println("iHigh = "+iHigh);
-	}
-
-	private class Location {
-		final int line;
-		final int pos;
-
-		Location(int index) {
-			int line = 0;
-			int i = -1;
-			while(true) {
-				int j = text.indexOf('\n',i+1);
-				if( j == -1 || j >= index )
-					break;
-				i = j;
-				line++;
-			}
-			this.line = line;
-			this.pos = index - i - 1;
-		}
-	}
-
-	private String[] lines() {
-		return text.split("\n",-1);
-	}
-
-	public String getFancyMessage() {
-		Location loc = new Location(iCurrent);
-		String line = lines()[loc.line];
-		String msg = getMessage() +  " (line " + (loc.line+1) + ", pos " + (loc.pos+1) + ") in " + sourceName + "\n";
-		StringBuilder sb = new StringBuilder(msg);
-		sb.append( line + "\n" );
-		for( int i=0; i<loc.pos; i++ ) {
-			sb.append( line.charAt(i)=='\t' ? '\t' : ' ' );
-		}
-		sb.append("^\n");
-		return sb.toString();
-	}
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/impl/Parser.java
--- a/core/src/luan/impl/Parser.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,190 +0,0 @@
-package luan.impl;
-
-
-final class Parser {
-
-	private static class Frame {
-		int i;
-		StringBuilder sb;
-	}
-
-//	private final LuanSource src;
-	public final String text;
-	public final String sourceName;
-	private final int len;
-	private Frame[] stack = new Frame[256];
-	private int frame = 0;
-	private int iHigh;
-
-	public Parser(String text,String sourceName) {
-//		this.src = src;
-		this.text = text;
-		this.sourceName = sourceName;
-		this.len = text.length();
-		stack[0] = new Frame();
-	}
-
-	private int i() {
-		return stack[frame].i;
-	}
-
-	private void i(int i) {
-		Frame f = stack[frame];
-		f.i += i;
-		if( iHigh < f.i )
-			iHigh = f.i;
-	}
-
-	public int begin() {
-		frame++;
-		if( frame == stack.length ) {
-			Frame[] a = new Frame[2*frame];
-			System.arraycopy(stack,0,a,0,frame);
-			stack = a;
-		}
-		Frame f = new Frame();
-		f.i = stack[frame-1].i;
-		stack[frame] = f;
-		return i();
-	}
-
-	public void rollback() {
-		Frame f = stack[frame];
-		f.i = stack[frame-1].i;
-		f.sb = null;
-	}
-
-	public <T> T success(T t) {
-		success();
-		return t;
-	}
-
-	public boolean success() {
-		Frame f = stack[frame];
-		if( f.sb != null && f.sb.length() > 0 )  throw new RuntimeException("sb not emtpy");
-		frame--;
-		stack[frame].i = f.i;
-		return true;
-	}
-
-	public <T> T failure(T t) {
-		failure();
-		return t;
-	}
-
-	public boolean failure() {
-		frame--;
-		return false;
-	}
-
-	public ParseException exception(String msg) {
-		return new ParseException(msg,text,sourceName,i(),iHigh);
-	}
-
-	public ParseException exception() {
-		return exception("Invalid input");
-	}
-
-	public StringBuilder sb() {
-		Frame f = stack[frame];
-		if( f.sb == null )
-			f.sb = new StringBuilder();
-		return f.sb;
-	}
-
-	public void upSb() {
-		Frame f = stack[frame];
-		StringBuilder sb = f.sb;
-		if( sb != null && sb.length() > 0 ) {
-			Frame fUp = stack[frame-1];
-			if( fUp.sb == null )
-				fUp.sb = sb;
-			else
-				fUp.sb.append(sb.toString());
-			f.sb = null;
-		}
-	}
-
-	public int currentIndex() {
-		return i();
-	}
-
-	public char lastChar() {
-		return text.charAt(i()-1);
-	}
-
-	public char currentChar() {
-		return text.charAt(i());
-	}
-
-	public boolean endOfInput() {
-		return i() >= len;
-	}
-
-	public boolean match(char c) {
-		if( endOfInput() || text.charAt(i()) != c )
-			return false;
-		i(1);
-		return true;
-	}
-
-	public boolean match(String s) {
-		int n = s.length();
-		if( !text.regionMatches(i(),s,0,n) )
-			return false;
-		i(n);
-		return true;
-	}
-
-	public boolean matchIgnoreCase(String s) {
-		int n = s.length();
-		if( !text.regionMatches(true,i(),s,0,n) )
-			return false;
-		i(n);
-		return true;
-	}
-
-	public boolean anyOf(String s) {
-		if( endOfInput() || s.indexOf(text.charAt(i())) == -1 )
-			return false;
-		i(1);
-		return true;
-	}
-
-	public boolean noneOf(String s) {
-		if( endOfInput() || s.indexOf(text.charAt(i())) != -1 )
-			return false;
-		i(1);
-		return true;
-	}
-
-	public boolean inCharRange(char cLow, char cHigh) {
-		if( endOfInput() )
-			return false;
-		char c = text.charAt(i());
-		if( !(cLow <= c && c <= cHigh) )
-			return false;
-		i(1);
-		return true;
-	}
-
-	public boolean anyChar() {
-		if( endOfInput() )
-			return false;
-		i(1);
-		return true;
-	}
-
-	public boolean test(char c) {
-		return !endOfInput() && text.charAt(i()) == c;
-	}
-
-	public boolean test(String s) {
-		return text.regionMatches(i(),s,0,s.length());
-	}
-
-	public String textFrom(int start) {
-		return text.substring(start,i());
-	}
-
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/impl/Pointer.java
--- a/core/src/luan/impl/Pointer.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-package luan.impl;
-
-import luan.DeepCloneable;
-import luan.DeepCloner;
-
-
-public final class Pointer implements DeepCloneable {
-	public Object o;
-
-	public Pointer() {}
-
-	public Pointer(Object o) {
-		this.o = o;
-	}
-
-	@Override public Pointer shallowClone() {
-		return new Pointer();
-	}
-
-	@Override public void deepenClone(DeepCloneable clone,DeepCloner cloner) {
-		((Pointer)clone).o = cloner.get(o);
-	}
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/impl/TableField.java
--- a/core/src/luan/impl/TableField.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,12 +0,0 @@
-package luan.impl;
-
-
-public final class TableField {
-	final Object key;
-	final Object value;
-
-	public TableField(Object key,Object value) {
-		this.key = key;
-		this.value = value;
-	}
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/BasicLuan.java
--- a/core/src/luan/modules/BasicLuan.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,244 +0,0 @@
-package luan.modules;
-
-import java.io.InputStreamReader;
-import java.io.IOException;
-import java.lang.reflect.Method;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.List;
-import java.util.ArrayList;
-import luan.Luan;
-import luan.LuanState;
-import luan.LuanTable;
-import luan.LuanFunction;
-import luan.LuanException;
-import luan.LuanMethod;
-import luan.LuanMeta;
-
-
-public final class BasicLuan {
-
-	public static String type(Object obj) {
-		return Luan.type(obj);
-	}
-
-	public static LuanFunction load(String text,String sourceName,LuanTable env)
-		throws LuanException
-	{
-		Utils.checkNotNull(text);
-		Utils.checkNotNull(sourceName,1);
-		return Luan.load(text,sourceName,env);
-	}
-
-	public static LuanFunction load_file(LuanState luan,String fileName) throws LuanException {
-		if( fileName == null ) {
-			fileName = "stdin:";
-		} else if( fileName.indexOf(':') == -1 ) {
-			fileName = "file:" + fileName;
-		}
-		String src = PackageLuan.read(luan,fileName);
-		if( src == null )
-			throw new LuanException("file '"+fileName+"' not found" );
-		return load(src,fileName,null);
-	}
-
-	public static LuanFunction pairs(final LuanState luan,final LuanTable t) throws LuanException {
-		Utils.checkNotNull(t);
-		return t.pairs(luan);
-	}
-
-	public static LuanFunction ipairs(final LuanTable t) throws LuanException {
-		Utils.checkNotNull(t);
-		return new LuanFunction() {
-			List<Object> list = t.asList();
-			int i = 0;
-			final int size = list.size();
-
-			@Override public Object[] call(LuanState luan,Object[] args) {
-				if( i >= size )
-					return LuanFunction.NOTHING;
-				Object val = list.get(i++);
-				return new Object[]{i,val};
-			}
-		};
-	}
-
-	public static Object get_metatable(LuanTable table) throws LuanException {
-		Utils.checkNotNull(table);
-		LuanTable metatable = table.getMetatable();
-		if( metatable == null )
-			return null;
-		Object obj = metatable.rawGet("__metatable");
-		return obj!=null ? obj : metatable;
-	}
-
-	public static void set_metatable(LuanTable table,LuanTable metatable) throws LuanException {
-		Utils.checkNotNull(table);
-		if( table.getHandler("__metatable") != null )
-			throw new LuanException("cannot change a protected metatable");
-		table.setMetatable(metatable);
-	}
-
-	public static boolean raw_equal(Object v1,Object v2) {
-		return v1 == v2 || v1 != null && v1.equals(v2);
-	}
-
-	public static Object raw_get(LuanTable table,Object index) {
-		return table.rawGet(index);
-	}
-
-	public static void raw_set(LuanTable table,Object index,Object value) {
-		table.rawPut(index,value);
-	}
-
-	public static int raw_len(Object v) throws LuanException {
-		if( v instanceof String ) {
-			String s = (String)v;
-			return s.length();
-		}
-		if( v instanceof LuanTable ) {
-			LuanTable t = (LuanTable)v;
-			return t.rawLength();
-		}
-		throw new LuanException( "bad argument #1 to 'raw_len' (table or string expected)" );
-	}
-
-	public static String to_string(LuanState luan,Object v) throws LuanException {
-		return luan.toString(v);
-	}
-
-	public static LuanTable new_error(LuanState luan,Object msg) throws LuanException {
-		String s = luan.toString(msg);
-		LuanTable tbl = new LuanException(s).table();
-		tbl.rawPut( "message", msg );
-		return tbl;
-	}
-
-	public static String assert_string(String v) throws LuanException {
-		Utils.checkNotNull(v);
-		return v;
-	}
-
-	public static Number assert_number(Number v) throws LuanException {
-		Utils.checkNotNull(v);
-		return v;
-	}
-
-	public static LuanTable assert_table(LuanTable v) throws LuanException {
-		Utils.checkNotNull(v);
-		return v;
-	}
-
-	public static boolean assert_boolean(boolean v) throws LuanException {
-		return v;
-	}
-
-	public static int assert_integer(int v) throws LuanException {
-		return v;
-	}
-
-	public static long assert_long(long v) throws LuanException {
-		return v;
-	}
-
-	public static double assert_double(double v) throws LuanException {
-		return v;
-	}
-
-	@LuanMethod public static byte[] assert_binary(byte[] v) throws LuanException {
-		Utils.checkNotNull(v);
-		return v;
-	}
-
-	public static LuanFunction assert_function(LuanFunction v) throws LuanException {
-		Utils.checkNotNull(v);
-		return v;
-	}
-
-	public static LuanFunction range(final double from,final double to,Double stepV) throws LuanException {
-		final double step = stepV==null ? 1.0 : stepV;
-		if( step == 0.0 )
-			throw new LuanException("bad argument #3 (step may not be zero)");
-		return new LuanFunction() {
-			double v = from;
-
-			@Override public Object call(LuanState luan,Object[] args) {
-				if( step > 0.0 && v > to || step < 0.0 && v < to )
-					return LuanFunction.NOTHING;
-				double rtn = v;
-				v += step;
-				return rtn;
-			}
-		};
-	}
-
-	public static LuanFunction values(final Object... args) throws LuanException {
-		return new LuanFunction() {
-			int i = 0;
-
-			@Override public Object call(LuanState luan,Object[] unused) {
-				if( i >= args.length )
-					return LuanFunction.NOTHING;
-				return args[i++];
-			}
-		};
-	}
-
-	private LuanFunction fn(Object obj) {
-		return obj instanceof LuanFunction ? (LuanFunction)obj : null;
-	}
-
-	public static Object try_(LuanState luan,LuanTable blocks,Object... args) throws LuanException {
-		Utils.checkNotNull(blocks);
-		Object obj = blocks.get(luan,1);
-		if( obj == null )
-			throw new LuanException("missing 'try' value");
-		if( !(obj instanceof LuanFunction) )
-			throw new LuanException("bad 'try' value (function expected, got "+Luan.type(obj)+")");
-		LuanFunction tryFn = (LuanFunction)obj;
-		LuanFunction catchFn = null;
-		obj = blocks.get(luan,"catch");
-		if( obj != null ) {
-			if( !(obj instanceof LuanFunction) )
-				throw new LuanException("bad 'catch' value (function expected, got "+Luan.type(obj)+")");
-			catchFn = (LuanFunction)obj;
-		}
-		LuanFunction finallyFn = null;
-		obj = blocks.get(luan,"finally");
-		if( obj != null ) {
-			if( !(obj instanceof LuanFunction) )
-				throw new LuanException("bad 'finally' value (function expected, got "+Luan.type(obj)+")");
-			finallyFn = (LuanFunction)obj;
-		}
-		try {
-			return tryFn.call(luan,args);
-		} catch(LuanException e) {
-			if( catchFn == null )
-				throw e;
-			return catchFn.call(luan,new Object[]{e.table()});
-		} finally {
-			if( finallyFn != null )
-				finallyFn.call(luan);
-		}
-	}
-
-	@LuanMethod public static Object[] pcall(LuanState luan,LuanFunction f,Object... args) {
-		try {
-			Object[] r = Luan.array(f.call(luan,args));
-			Object[] rtn = new Object[r.length+1];
-			rtn[0] = true;
-			for( int i=0; i<r.length; i++ ) {
-				rtn[i+1] = r[i];
-			}
-			return rtn;
-		} catch(LuanException e) {
-			return new Object[]{false,e.table()};
-		}
-	}
-
-	public static String number_type(Number v) throws LuanException {
-		Utils.checkNotNull(v);
-		return v.getClass().getSimpleName().toLowerCase();
-	}
-
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/Binary.luan
--- a/core/src/luan/modules/Binary.luan	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,11 +0,0 @@
-java()
-local BinaryLuan = require "java:luan.modules.BinaryLuan"
-
-
-local M = {}
-
-M.binary = BinaryLuan.binary
-M.byte = BinaryLuan.byte_
-M.to_string = BinaryLuan.to_string
-
-return M
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/BinaryLuan.java
--- a/core/src/luan/modules/BinaryLuan.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,50 +0,0 @@
-package luan.modules;
-
-import luan.Luan;
-import luan.LuanState;
-import luan.LuanTable;
-import luan.LuanFunction;
-import luan.LuanException;
-import luan.LuanMethod;
-
-
-public final class BinaryLuan {
-
-	static int start(byte[] binary,int i) {
-		int len = binary.length;
-		return i==0 ? 0 : i > 0 ? Math.min(i-1,len) : Math.max(len+i,0);
-	}
-
-	static int start(byte[] binary,Integer i,int dflt) {
-		return i==null ? dflt : start(binary,i);
-	}
-
-	static int end(byte[] binary,int i) {
-		int len = binary.length;
-		return i==0 ? 0 : i > 0 ? Math.min(i,len) : Math.max(len+i+1,0);
-	}
-
-	static int end(byte[] binary,Integer i,int dflt) {
-		return i==null ? dflt : end(binary,i);
-	}
-
-	@LuanMethod public static Byte[] byte_(byte[] binary,Integer i,Integer j) throws LuanException {
-		Utils.checkNotNull(binary);
-		int start = start(binary,i,1);
-		int end = end(binary,j,start+1);
-		Byte[] bytes = new Byte[end-start];
-		for( int k=0; k<bytes.length; k++ ) {
-			bytes[k] = binary[start+k];
-		}
-		return bytes;
-	}
-
-	@LuanMethod public static byte[] binary(byte... bytes) {
-		return bytes;
-	}
-
-	public static String to_string(byte[] binary) {
-		return new String(binary);
-	}
-
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/Html.luan
--- a/core/src/luan/modules/Html.luan	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,76 +0,0 @@
-java()
-local HtmlLuan = require "java:luan.modules.HtmlLuan"
-local HtmlParser = require "java:luan.modules.parsers.Html"
-local URLEncoder = require "java:java.net.URLEncoder"
-local Luan = require "luan:Luan.luan"
-local error = Luan.error
-local ipairs = Luan.ipairs or error()
-local pairs = Luan.pairs or error()
-local type = Luan.type or error()
-local Io = require "luan:Io.luan"
-local output_of = Io.output_of or error()
-
-
-local M = {}
-
-M.encode = HtmlLuan.encode
-
-local quote = HtmlLuan.quote
-M.quote = quote
-
-function M.parse(text,container_tags)
-	text or error "text required"
-	container_tags = container_tags or {"script","style","textarea"}
-	return HtmlParser.toList(text,container_tags)
-end
-
-function M.url_encode(s)
-	return URLEncoder.encode(s,"UTF-8")
-end
-
-local function output_tag(tag)
-	%><<%= tag.name %><%
-	local attributes = tag.attributes
-	for name, value in pairs(attributes) do
-		%> <%= name %><%
-		if value ~= true then
-			%>=<%= quote(value) %><%
-		end
-	end
-	if tag.is_empty then
-		%>/<%
-	end
-	%>><%
-end
-
-function M.to_string(list)
-	return output_of( function()
-		for _, obj in ipairs(list) do
-			local tp = type(obj)
-			if tp == "string" then
-				%><%= obj %><%
-			elseif tp == "table" then
-				tp = obj.type
-				if tp == nil then
-					error "no type in element of table for 'Html.to_string'"
-				elseif tp == "comment" then
-					%><!--<%= obj.text %>--><%
-				elseif tp == "cdata" then
-					%><![CDATA[<%= obj.text %>]]><%
-				elseif tp == "tag" then
-					output_tag(obj)
-				elseif tp == "container" then
-					local tag = obj.tag
-					output_tag(tag)
-					%><%= obj.text %></<%= tag.name %>><%
-				else
-					error "invalid element type for 'Html.to_string'"
-				end
-			else
-				error("invalid value ("..tp..") in list for 'Html.to_string'")
-			end
-		end
-	end )
-end
-
-return M
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/HtmlLuan.java
--- a/core/src/luan/modules/HtmlLuan.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,348 +0,0 @@
-package luan.modules;
-
-import java.util.List;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Set;
-import java.util.HashSet;
-import java.util.Map;
-import luan.Luan;
-import luan.LuanState;
-import luan.LuanTable;
-import luan.LuanException;
-
-
-public final class HtmlLuan {
-
-	public static String encode(String s) throws LuanException {
-		Utils.checkNotNull(s);
-		char[] a = s.toCharArray();
-		StringBuilder buf = new StringBuilder();
-		for( int i=0; i<a.length; i++ ) {
-			char c = a[i];
-			switch(c) {
-			case '&':
-				buf.append("&amp;");
-				break;
-			case '<':
-				buf.append("&lt;");
-				break;
-			case '>':
-				buf.append("&gt;");
-				break;
-			case '"':
-				buf.append("&quot;");
-				break;
-			default:
-				buf.append(c);
-			}
-		}
-		return buf.toString();
-	}
-
-/*
-//	public static final String TEXTAREA = "textarea";
-	public static final String SCRIPT = "script";
-	public static final String STYLE = "style";
-
-	public static Set<String> containerTags = new HashSet<String>(Arrays.asList(SCRIPT,STYLE));
-*/
-/*
-	public static LuanTable parse(LuanState luan,String text,LuanTable containerTagsTbl)
-		throws LuanException
-	{
-		Utils.checkNotNull(luan,text);
-		Utils.checkNotNull(luan,containerTagsTbl);
-		Set<String> containerTags = new HashSet<String>();
-		for( Object v : containerTagsTbl.asList() ) {
-			containerTags.add((String)v);
-		}
-		List<Object> html = new ArrayList<Object>();
-		int len = text.length();
-		int i = 0;
-outer:
-		while( i < len ) {
-			int i2 = text.indexOf('<',i);
-			while( i2 != -1 && i2+1 < len ) {
-				char c = text.charAt(i2+1);
-				if( Character.isLetter(c) || c=='/' || c=='!' )
-					break;
-				i2 = text.indexOf('<',i2+1);
-			}
-			if( i2 == -1 ) {
-				html.add( text.substring(i) );
-				break;
-			}
-			if( i < i2 )
-				html.add( text.substring(i,i2) );
-			if( text.startsWith("<!--",i2) ) {
-				i = text.indexOf("-->",i2+4);
-				if( i == -1 ) {
-					html.add( text.substring(i2) );
-					break;
-				}
-				html.add( comment( text.substring(i2+4,i) ) );
-				i += 3;
-			} else if( text.startsWith("<![CDATA[",i2) ) {
-				i = text.indexOf("]]>",i2+9);
-				if( i == -1 ) {
-					html.add( text.substring(i2) );
-					break;
-				}
-				html.add( cdata( text.substring(i2+9,i) ) );
-				i += 3;
-			} else {
-				i = text.indexOf('>',i2);
-				if( i == -1 ) {
-					html.add( text.substring(i2) );
-					break;
-				}
-				String tagText = text.substring(i2+1,i);
-				try {
-					LuanTable tag = parseTag(tagText);
-					String tagName = (String)tag.rawGet("name");
-					if( containerTags.contains(tagName) ) {
-						i2 = i;
-						String endTagName = '/' + tagName;
-						while(true) {
-							i2 = text.indexOf('<',i2+1);
-							if( i2 == -1 )
-								break;
-							int i3 = text.indexOf('>',i2);
-							if( i3 == -1 )
-								break;
-							int j = i2+1;
-							while( j<i3 && !Character.isWhitespace(text.charAt(j)) )  j++;
-							String s = text.substring(i2+1,j);
-							if( s.equalsIgnoreCase(endTagName) ) {
-								String text2 = text.substring(i+1,i2);
-								LuanTable textContainer = textContainer(tag,text2);
-								html.add( textContainer );
-								i = i3 + 1;
-								continue outer;
-							}
-						}
-//						logger.warn("unclosed "+tagName);
-					}
-					i += 1;
-					html.add( tag );
-				} catch(BadTag e) {
-//					logger.debug("bad tag",e);
-					i += 1;
-//					if( !removeBadTags ) {
-						html.add( "&lt;" );
-						html.add( encode(luan,tagText) );
-						html.add( "&gt;" );
-//					}
-				}
-			}
-		}
-		return new LuanTable(html);
-	}
-
-	static LuanTable comment(String text) {
-		LuanTable tbl = new LuanTable();
-		tbl.rawPut("type","comment");
-		tbl.rawPut("text",text);
-		return tbl;
-	}
-
-	static LuanTable cdata(String text) {
-		LuanTable tbl = new LuanTable();
-		tbl.rawPut("type","cdata");
-		tbl.rawPut("text",text);
-		return tbl;
-	}
-
-	static LuanTable textContainer(LuanTable tag,String text) {
-		LuanTable tbl = new LuanTable();
-		tbl.rawPut("type","container");
-		tbl.rawPut("tag",tag);
-		tbl.rawPut("text",text);
-		return tbl;
-	}
-
-
-
-	static final class BadTag extends RuntimeException {
-		private BadTag(String msg) {
-			super(msg);
-		}
-	}
-
-	static LuanTable parseTag(String text) {
-		LuanTable tbl = new LuanTable();
-		tbl.rawPut("type","tag");
-		if( text.endsWith("/") ) {
-			text = text.substring(0,text.length()-1);
-			tbl.rawPut("is_empty",true);
-		} else {
-			tbl.rawPut("is_empty",false);
-		}
-		int len = text.length();
-		int i = 0;
-		int i2 = i;
-		if( i2<len && text.charAt(i2)=='/' )
-			i2++;
-		while( i2<len ) {
-			char c = text.charAt(i2);
-			if( Character.isWhitespace(c) )
-				break;
-			if( !( Character.isLetterOrDigit(c) || c=='_' || c=='.' || c=='-' || c==':' ) )
-				throw new BadTag("invalid tag name for <"+text+">");
-			i2++;
-		}
-		String name = text.substring(i,i2).toLowerCase();
-		tbl.rawPut("name",name);
-		LuanTable attributes = new LuanTable();
-		tbl.rawPut("attributes",attributes);
-		i = i2;
-		while( i<len && Character.isWhitespace(text.charAt(i)) )  i++;
-		while( i<len ) {
-			i2 = toEndName(text,i,len);
-			String attrName = unquote(text.substring(i,i2).toLowerCase());
-			if( attributes.rawGet(attrName) != null )
-				throw new BadTag("duplicate attribute: "+attrName);
-			i = i2;
-			while( i<len && Character.isWhitespace(text.charAt(i)) )  i++;
-			if( i<len && text.charAt(i) == '=' ) {
-				i++;
-				i2 = i;
-				while( i<len && Character.isWhitespace(text.charAt(i)) )  i++;
-				i2 = toEndValue(text,i,len);
-				String attrValue = text.substring(i,i2);
-				if( attrValue.indexOf('<') != -1 || attrValue.indexOf('>') != -1 )
-					throw new BadTag("invalid attribute value: "+attrValue);
-				attrValue = unquote(attrValue);
-				attributes.rawPut(attrName,attrValue);
-				i = i2;
-				while( i<len && Character.isWhitespace(text.charAt(i)) )  i++;
-			} else {
-				attributes.rawPut(attrName,true);
-			}
-		}
-		return tbl;
-	}
-
-	private static int toEndName(String text,int i,int len) {
-		if( i==len )
-			return i;
-		char c = text.charAt(i);
-		switch(c) {
-		case '"':
-		case '\'':
-			i = text.indexOf(c,i+1);
-			return i==-1 ? len : i+1;
-		default:
-			if( Character.isWhitespace(c) ) {
-				throw new RuntimeException("text="+text+" i="+i);
-			}
-			do {
-				i++;
-			} while( i<len && (c=text.charAt(i))!='=' && !Character.isWhitespace(c) );
-			return i;
-		}
-	}
-
-	private static int toEndValue(String text,int i,int len) {
-		if( i==len )
-			return i;
-		char c = text.charAt(i);
-		switch(c) {
-		case '"':
-		case '\'':
-			i = text.indexOf(c,i+1);
-			return i==-1 ? len : i+1;
-		default:
-			if( Character.isWhitespace(c) ) {
-				throw new RuntimeException("text="+text+" i="+i);
-			}
-			do {
-				i++;
-			} while( i<len && !Character.isWhitespace(text.charAt(i)) );
-			return i;
-		}
-	}
-
-	public static String unquote(String s) {
-		if( s==null || s.length()<=1 )
-			return s;
-		char c = s.charAt(0);
-		return (c=='"' || c=='\'') && s.charAt(s.length()-1)==c
-			? s.substring(1,s.length()-1) : s;
-	}
-*/
-
-
-/*
-	public static String to_string(LuanState luan,LuanTable tbl) throws LuanException {
-		List<Object> html = tbl.asList();
-		StringBuilder buf = new StringBuilder();
-		for( Object o : html ) {
-			if( o instanceof String ) {
-				buf.append( o );
-			} else if( o instanceof LuanTable ) {
-				LuanTable t = (LuanTable)o;
-				String type = (String)t.get(luan,"type");
-				if( type==null )
-					throw new LuanException(luan, "no type in element of table for 'Html.to_string'" );
-				if( type.equals("comment") ) {
-					buf.append( "<!--" ).append( t.get(luan,"text") ).append( "-->" );
-				} else if( type.equals("cdata") ) {
-					buf.append( "<![CDATA[" ).append( t.get(luan,"text") ).append( "]]" );
-				} else if( type.equals("tag") ) {
-					buf.append( tagToString(luan,t) );
-				} else if( type.equals("container") ) {
-					LuanTable tag  = (LuanTable)t.get(luan,"tag");
-					buf.append( tagToString(luan,tag) );
-					buf.append( t.get(luan,"text") );
-					buf.append( "</" ).append( tag.get(luan,"name") ).append( ">" );
-				} else {
-					throw new LuanException(luan, "invalid element type for 'Html.to_string'" );
-				}
-			} else 
-				throw new LuanException(luan, "invalid value ("+Luan.type(o)+") in table for 'Html.to_string'" );
-		}
-		return buf.toString();
-	}
-
-	private static String tagToString(LuanState luan,LuanTable tbl) throws LuanException {
-		StringBuilder buf = new StringBuilder();
-		buf.append('<');
-		buf.append(tbl.get(luan,"name"));
-		LuanTable attributes = (LuanTable)tbl.get(luan,"attributes");
-		for( Map.Entry<Object,Object> attr : attributes.iterable(luan) ) {
-			buf.append( ' ' );
-			buf.append( attr.getKey() );
-			Object val = attr.getValue();
-			if( !val.equals(Boolean.TRUE) ) {
-				buf.append( '=' );
-				buf.append( quote((String)val) );
-			}
-		}
-		if( tbl.get(luan,"is_empty").equals(Boolean.TRUE) )
-			buf.append('/');
-		buf.append('>');
-		return buf.toString();
-	}
-*/
-	public static String quote(String s) {
-		StringBuilder buf = new StringBuilder();
-		buf.append('"');
-		int i = 0;
-		while(true) {
-			int i2 = s.indexOf('"',i);
-			if( i2 == -1 ) {
-				buf.append(s.substring(i));
-				break;
-			} else {
-				buf.append(s.substring(i,i2));
-				buf.append("&quot;");
-				i = i2 + 1;
-			}
-		}
-		buf.append('"');
-		return buf.toString();
-	}
-
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/Io.luan
--- a/core/src/luan/modules/Io.luan	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,185 +0,0 @@
-java()
-local IoLuan = require "java:luan.modules.IoLuan"
-local System = require "java:java.lang.System"
-
-local M = {}
-
-M.ip = IoLuan.ip
-M.my_ips = IoLuan.my_ips
-M.read_console_line = IoLuan.read_console_line
-M.schemes = IoLuan.newSchemes()
-M.uri = IoLuan.uri
-M.stdin = IoLuan.defaultStdin.table()
-M.socket_server = IoLuan.socket_server
-M.stdout = IoLuan.textWriter(System.out)
-M.stderr = IoLuan.textWriter(System.err)
-
--- used by http and rpc
-M.password = "password"
-
-local Luan = require "luan:Luan.luan"
-local error = Luan.error
-local to_string = Luan.to_string or error()
-local type = Luan.type or error()
-local try = Luan.try or error()
-local ipairs = Luan.ipairs or error()
-local pairs = Luan.pairs or error()
-local values = Luan.values or error()
-local load = Luan.load or error()
-local Table = require "luan:Table.luan"
-local unpack = Table.unpack or error()
-local String = require "luan:String.luan"
-local encode = String.encode or error()
-local matches = String.matches or error()
-
-
--- do not change
-function M.template_write(...)
-	return M.stdout.write(...)
-end
-
-
-function M.print_to(out,...)
-	local list = {}
-	for v in values(...) do
-		list[#list+1] = to_string(v)
-		list[#list+1] = '\t'
-	end
-	if #list > 0 then
-		list[#list] = '\n'
-		out.write( unpack(list) )
-	end
-end
-
-function M.print(...)
-	M.print_to(M.stdout,...)
-end
-
-
-function M.output_to(out,fn,...)
-	local old_out = M.stdout
-	return try( {
-		function(...)
-			M.stdout = out
-			fn(...)
-		end;
-		finally = function()
-			M.stdout = old_out
-		end;
-	}, ... )
-end
-
-local uri = M.uri  -- make local
-
-function M.output_of(fn,...)
-	local string_uri = uri "string:"
-	local out = string_uri.text_writer()
-	M.output_to(out,fn,...)
-	out.close()
-	return string_uri.read_text()
-end
-
-
--- repr
-
-local function do_repr(out,obj,strict,done)
-	local tp = type(obj)
-	if tp == "table" then
-		if done[obj] == true then
-			strict and error "circular reference"
-			out.write "<circular reference>"
-			return
-		end
-		done[obj] = true
-		out.write( "{" )
-		local is_first = true
-		local in_list = {}
-		for key, value in ipairs(obj) do
-			if is_first then is_first = false else out.write ", " end
-			do_repr(out,value,strict,done)
-			in_list[key] = true
-		end
-		for key, value in pairs(obj) do
-			if in_list[key] ~= true then
-				if is_first then is_first = false else out.write ", " end
-				if type(key) == "string" and matches(key,"^[a-zA-Z_][a-zA-Z_0-9]*$") ~= nil then
-					out.write( key )
-				elseif type(key) == "table" then
-					out.write( "[<", key, ">]" )
-				else
-					out.write "["
-					do_repr(out,key,strict,done)
-					out.write "]"
-				end
-				out.write "="
-				do_repr(out,value,strict,done)
-			end
-		end
-		out.write "}"
-	elseif tp == "string" then
-		out.write( '"', encode(obj), '"' )
-	elseif tp == "nil" or tp == "boolean" or tp == "number" then
-		out.write( obj )
-	else
-		strict and error("can't repr type '"..tp.."' of "..obj)
-		out.write( "<", obj, ">" )
-	end
-end
-
-function M.repr(obj,strict)
-	local string_uri = uri "string:"
-	local out = string_uri.text_writer()
-	do_repr(out,obj,strict or false,{})
-	out.close()
-	return string_uri.read_text()
-end
-
-
--- useful for SimplyHTML responsiveness
-
-local NO = {}
-M.NO = NO
-
-function M.dont_write_when_no(write_fn)
-	return function(...)
-		for v in values(...) do
-			if v == NO then
-				return
-			end
-		end
-		write_fn(...)
-	end
-end
-
-
--- debug
-
-function M.debug(prompt)
-	prompt = prompt or "luan_debug> "
-	local function console()
-		return M.read_console_line(prompt)
-	end
-	local env = {}
-	for line in console do
-		try {
-			function()
-				local fn
-				try {
-					function()
-						fn = load("return "..line,"stdin",env)
-					end
-					catch = function(e)
-						fn = load(line,"stdin",env)
-					end
-				}
-				M.print( fn() )
-			end
-			catch = function(e)
-				M.print(e)
-			end
-		}
-	end
-end
-
-
-return M
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/IoLuan.java
--- a/core/src/luan/modules/IoLuan.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,904 +0,0 @@
-package luan.modules;
-
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.PrintStream;
-import java.io.Reader;
-import java.io.Writer;
-import java.io.StringReader;
-import java.io.BufferedReader;
-import java.io.BufferedWriter;
-import java.io.BufferedInputStream;
-import java.io.BufferedOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStreamWriter;
-import java.io.ByteArrayInputStream;
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.StringWriter;
-import java.io.IOException;
-import java.io.FileNotFoundException;
-import java.net.URL;
-import java.net.Socket;
-import java.net.ServerSocket;
-import java.net.InetAddress;
-import java.net.Inet4Address;
-import java.net.NetworkInterface;
-import java.net.MalformedURLException;
-import java.net.UnknownHostException;
-import java.util.Enumeration;
-import java.util.List;
-import java.util.Map;
-import luan.Luan;
-import luan.LuanState;
-import luan.LuanTable;
-import luan.LuanFunction;
-import luan.LuanJavaFunction;
-import luan.LuanException;
-import luan.modules.url.LuanUrl;
-
-
-public final class IoLuan {
-
-	private static void add(LuanTable t,String method,Class... parameterTypes) throws NoSuchMethodException {
-		t.rawPut( method, new LuanJavaFunction(IoLuan.class.getMethod(method,parameterTypes),null) );
-	}
-
-	public static String read_console_line(String prompt) throws IOException {
-		if( prompt==null )
-			prompt = "> ";
-		return System.console().readLine(prompt);
-	}
-
-
-	public interface LuanWriter {
-		public void write(LuanState luan,Object... args) throws LuanException, IOException;
-		public void close() throws IOException;
-	}
-
-	public static LuanTable textWriter(final PrintStream out) {
-		LuanWriter luanWriter = new LuanWriter() {
-
-			public void write(LuanState luan,Object... args) throws LuanException {
-				for( Object obj : args ) {
-					out.print( luan.toString(obj) );
-				}
-			}
-
-			public void close() {
-				out.close();
-			}
-		};
-		return writer(luanWriter);
-	}
-
-	public static LuanTable textWriter(final Writer out) {
-		LuanWriter luanWriter = new LuanWriter() {
-
-			public void write(LuanState luan,Object... args) throws LuanException, IOException {
-				for( Object obj : args ) {
-					out.write( luan.toString(obj) );
-				}
-			}
-
-			public void close() throws IOException {
-				out.close();
-			}
-		};
-		return writer(luanWriter);
-	}
-
-	private static LuanTable writer(LuanWriter luanWriter) {
-		LuanTable writer = new LuanTable();
-		try {
-			writer.rawPut( "write", new LuanJavaFunction(
-				LuanWriter.class.getMethod( "write", LuanState.class, new Object[0].getClass() ), luanWriter
-			) );
-			writer.rawPut( "close", new LuanJavaFunction(
-				LuanWriter.class.getMethod( "close" ), luanWriter
-			) );
-		} catch(NoSuchMethodException e) {
-			throw new RuntimeException(e);
-		}
-		return writer;
-	}
-
-
-	public static LuanTable binaryWriter(final OutputStream out) {
-		LuanTable writer = new LuanTable();
-		try {
-			writer.rawPut( "write", new LuanJavaFunction(
-				OutputStream.class.getMethod( "write", new byte[0].getClass() ), out
-			) );
-			writer.rawPut( "close", new LuanJavaFunction(
-				OutputStream.class.getMethod( "close" ), out
-			) );
-		} catch(NoSuchMethodException e) {
-			throw new RuntimeException(e);
-		}
-		return writer;
-	}
-
-	static LuanFunction lines(final BufferedReader in) {
-		return new LuanFunction() {
-			@Override public Object call(LuanState luan,Object[] args) throws LuanException {
-				try {
-					if( args.length > 0 ) {
-						if( args.length > 1 || !"close".equals(args[0]) )
-							throw new LuanException( "the only argument allowed is 'close'" );
-						in.close();
-						return null;
-					}
-					String rtn = in.readLine();
-					if( rtn==null )
-						in.close();
-					return rtn;
-				} catch(IOException e) {
-					throw new LuanException(e);
-				}
-			}
-		};
-	}
-
-	static LuanFunction blocks(final InputStream in,final int blockSize) {
-		return new LuanFunction() {
-			final byte[] a = new byte[blockSize];
-
-			@Override public Object call(LuanState luan,Object[] args) throws LuanException {
-				try {
-					if( args.length > 0 ) {
-						if( args.length > 1 || !"close".equals(args[0]) )
-							throw new LuanException( "the only argument allowed is 'close'" );
-						in.close();
-						return null;
-					}
-					if( in.read(a) == -1 ) {
-						in.close();
-						return null;
-					}
-					return a;
-				} catch(IOException e) {
-					throw new LuanException(e);
-				}
-			}
-		};
-	}
-
-
-	private static File objToFile(Object obj) {
-		if( obj instanceof String ) {
-			return new File((String)obj);
-		}
-		if( obj instanceof LuanTable ) {
-			LuanTable t = (LuanTable)obj;
-			Object java = t.rawGet("java");
-			if( java instanceof LuanFile ) {
-				LuanFile luanFile = (LuanFile)java;
-				return luanFile.file;
-			}
-		}
-		return null;
-	}
-
-
-	public static abstract class LuanIn {
-		public abstract InputStream inputStream() throws IOException, LuanException;
-		public abstract String to_string();
-		public abstract String to_uri_string();
-
-		public Reader reader() throws IOException, LuanException {
-			return new InputStreamReader(inputStream());
-		}
-
-		public String read_text() throws IOException, LuanException {
-			Reader in = reader();
-			String s = Utils.readAll(in);
-			in.close();
-			return s;
-		}
-
-		public byte[] read_binary() throws IOException, LuanException {
-			InputStream in = inputStream();
-			byte[] a = Utils.readAll(in);
-			in.close();
-			return a;
-		}
-
-		public LuanFunction read_lines() throws IOException, LuanException {
-			return lines(new BufferedReader(reader()));
-		}
-
-		public LuanFunction read_blocks(Integer blockSize) throws IOException, LuanException {
-			int n = blockSize!=null ? blockSize : Utils.bufSize;
-			return blocks(inputStream(),n);
-		}
-
-		public boolean exists() throws IOException, LuanException {
-			try {
-				inputStream().close();
-				return true;
-			} catch(FileNotFoundException e) {
-				return false;
-			}
-		}
-
-		public LuanTable table() {
-			LuanTable tbl = new LuanTable();
-			try {
-				tbl.rawPut( "java", this );
-				tbl.rawPut( "to_string", new LuanJavaFunction(
-					LuanIn.class.getMethod( "to_string" ), this
-				) );
-				tbl.rawPut( "to_uri_string", new LuanJavaFunction(
-					LuanIn.class.getMethod( "to_uri_string" ), this
-				) );
-				tbl.rawPut( "read_text", new LuanJavaFunction(
-					LuanIn.class.getMethod( "read_text" ), this
-				) );
-				tbl.rawPut( "read_binary", new LuanJavaFunction(
-					LuanIn.class.getMethod( "read_binary" ), this
-				) );
-				tbl.rawPut( "read_lines", new LuanJavaFunction(
-					LuanIn.class.getMethod( "read_lines" ), this
-				) );
-				tbl.rawPut( "read_blocks", new LuanJavaFunction(
-					LuanIn.class.getMethod( "read_blocks", Integer.class ), this
-				) );
-				tbl.rawPut( "exists", new LuanJavaFunction(
-					LuanIn.class.getMethod( "exists" ), this
-				) );
-			} catch(NoSuchMethodException e) {
-				throw new RuntimeException(e);
-			}
-			return tbl;
-		}
-	}
-
-	public static final LuanIn defaultStdin = new LuanIn() {
-
-		@Override public InputStream inputStream() {
-			return System.in;
-		}
-
-		@Override public String to_string() {
-			return "<stdin>";
-		}
-
-		@Override public String to_uri_string() {
-			return "stdin:";
-		}
-
-		@Override public String read_text() throws IOException {
-			return Utils.readAll(new InputStreamReader(System.in));
-		}
-
-		@Override public byte[] read_binary() throws IOException {
-			return Utils.readAll(System.in);
-		}
-
-		@Override public boolean exists() {
-			return true;
-		}
-	};
-
-	public static abstract class LuanIO extends LuanIn {
-		abstract OutputStream outputStream() throws IOException;
-
-		public void write(Object obj) throws LuanException, IOException {
-			if( obj instanceof String ) {
-				String s = (String)obj;
-				Writer out = new OutputStreamWriter(outputStream());
-				out.write(s);
-				out.close();
-				return;
-			}
-			if( obj instanceof byte[] ) {
-				byte[] a = (byte[])obj;
-				OutputStream out = outputStream();
-				Utils.copyAll(new ByteArrayInputStream(a),out);
-				out.close();
-				return;
-			}
-			if( obj instanceof LuanTable ) {
-				LuanTable t = (LuanTable)obj;
-				Object java = t.rawGet("java");
-				if( java instanceof LuanIn ) {
-					LuanIn luanIn = (LuanIn)java;
-					InputStream in = luanIn.inputStream();
-					OutputStream out = outputStream();
-					Utils.copyAll(in,out);
-					out.close();
-					in.close();
-					return;
-				}
-			}
-			throw new LuanException( "bad argument #1 to 'write' (string or binary or Io.uri expected)" );
-		}
-
-		public LuanTable text_writer() throws IOException {
-			return textWriter(new BufferedWriter(new OutputStreamWriter(outputStream())));
-		}
-
-		public LuanTable binary_writer() throws IOException {
-			return binaryWriter(new BufferedOutputStream(outputStream()));
-		}
-
-		@Override public LuanTable table() {
-			LuanTable tbl = super.table();
-			try {
-				tbl.rawPut( "write", new LuanJavaFunction(
-					LuanIO.class.getMethod( "write", Object.class ), this
-				) );
-				tbl.rawPut( "text_writer", new LuanJavaFunction(
-					LuanIO.class.getMethod( "text_writer" ), this
-				) );
-				tbl.rawPut( "binary_writer", new LuanJavaFunction(
-					LuanIO.class.getMethod( "binary_writer" ), this
-				) );
-			} catch(NoSuchMethodException e) {
-				throw new RuntimeException(e);
-			}
-			return tbl;
-		}
-	}
-
-	private static final LuanIO nullIO = new LuanIO() {
-		private final InputStream in = new InputStream() {
-			@Override public int read() {
-				return -1;
-			}
-		};
-		private final OutputStream out = new OutputStream() {
-			@Override public void write(int b) {}
-		};
-
-		@Override public InputStream inputStream() {
-			return in;
-		}
-
-		@Override OutputStream outputStream() {
-			return out;
-		}
-
-		@Override public String to_string() {
-			return "<null>";
-		}
-
-		@Override public String to_uri_string() {
-			return "null:";
-		}
-
-	};
-
-	public static final class LuanString extends LuanIO {
-		private String s;
-
-		private LuanString(String s) {
-			this.s = s;
-		}
-
-		@Override public InputStream inputStream() {
-			throw new UnsupportedOperationException();
-		}
-
-		@Override OutputStream outputStream() {
-			throw new UnsupportedOperationException();
-		}
-
-		@Override public String to_string() {
-			return "<string>";
-		}
-
-		@Override public String to_uri_string() {
-			return "string:" + s;
-		}
-
-		@Override public Reader reader() {
-			return new StringReader(s);
-		}
-
-		@Override public String read_text() {
-			return s;
-		}
-
-		@Override public boolean exists() {
-			return true;
-		}
-
-		@Override public LuanTable text_writer() throws IOException {
-			LuanWriter luanWriter = new LuanWriter() {
-				private final Writer out = new StringWriter();
-	
-				public void write(LuanState luan,Object... args) throws LuanException, IOException {
-					for( Object obj : args ) {
-						out.write( luan.toString(obj) );
-					}
-				}
-	
-				public void close() throws IOException {
-					s = out.toString();
-				}
-			};
-			return writer(luanWriter);
-		}
-	}
-
-	public static final class LuanFile extends LuanIO {
-		public final File file;
-
-		private LuanFile(LuanState luan,File file) throws LuanException {
-			this(file);
-			check(luan,"file:"+file.toString());
-		}
-
-		private LuanFile(File file) {
-			this.file = file;
-		}
-
-		@Override public InputStream inputStream() throws IOException {
-			return new FileInputStream(file);
-		}
-
-		@Override OutputStream outputStream() throws IOException {
-			return new FileOutputStream(file);
-		}
-
-		@Override public String to_string() {
-			return file.toString();
-		}
-
-		@Override public String to_uri_string() {
-			return "file:" + file.toString();
-		}
-
-		public LuanTable child(LuanState luan,String name) throws LuanException {
-			return new LuanFile(luan,new File(file,name)).table();
-		}
-
-		public LuanTable children(LuanState luan) throws LuanException {
-			File[] files = file.listFiles();
-			if( files==null )
-				return null;
-			LuanTable list = new LuanTable();
-			for( File f : files ) {
-				list.rawPut(list.rawLength()+1,new LuanFile(luan,f).table());
-			}
-			return list;
-		}
-
-		public LuanTable parent(LuanState luan) throws LuanException, IOException {
-			File parent = file.getParentFile();
-			if( parent==null )
-				parent = file.getCanonicalFile().getParentFile();
-			return new LuanFile(luan,parent).table();
-		}
-
-		@Override public boolean exists() {
-			return file.exists();
-		}
-
-		public void rename_to(Object destObj) throws LuanException {
-			File dest = objToFile(destObj);
-			if( dest==null )
-				throw new LuanException( "bad argument #1 to 'objToFile' (string or file table expected)" );
-			if( !file.renameTo(dest) )
-				throw new LuanException("couldn't rename file "+file+" to "+dest);
-		}
-
-		public LuanTable canonical(LuanState luan) throws LuanException, IOException {
-			return new LuanFile(luan,file.getCanonicalFile()).table();
-		}
-
-		public LuanTable create_temp_file(LuanState luan,String prefix,String suffix) throws LuanException, IOException {
-			File tmp = File.createTempFile(prefix,suffix,file);
-			return new LuanFile(luan,tmp).table();
-		}
-
-		public void delete() throws LuanException {
-			if( file.exists() )
-				delete(file);
-		}
-
-		private static void delete(File file) throws LuanException {
-			File[] children = file.listFiles();
-			if( children != null ) {
-				for( File child : children ) {
-					delete(child);
-				}
-			}
-			if( !file.delete() )
-				throw new LuanException("couldn't delete file "+file);
-		}
-
-		public void mkdir() throws LuanException {
-			if( !file.isDirectory() ) {
-				if( !file.mkdirs() )
-					throw new LuanException("couldn't make directory "+file);
-			}
-		}
-
-		public void set_last_modified(long time) throws LuanException {
-			if( !file.setLastModified(time) )
-				throw new LuanException("couldn't set_last_modified on "+file);
-		}
-
-		@Override public LuanTable table() {
-			LuanTable tbl = super.table();
-			try {
-				tbl.rawPut( "name", new LuanJavaFunction(
-					File.class.getMethod( "getName" ), file
-				) );
-				tbl.rawPut( "is_directory", new LuanJavaFunction(
-					File.class.getMethod( "isDirectory" ), file
-				) );
-				tbl.rawPut( "is_file", new LuanJavaFunction(
-					File.class.getMethod( "isFile" ), file
-				) );
-				tbl.rawPut( "delete", new LuanJavaFunction(
-					LuanFile.class.getMethod( "delete" ), this
-				) );
-				tbl.rawPut( "delete_on_exit", new LuanJavaFunction(
-					File.class.getMethod( "deleteOnExit" ), file
-				) );
-				tbl.rawPut( "mkdir", new LuanJavaFunction(
-					LuanFile.class.getMethod( "mkdir" ), this
-				) );
-				tbl.rawPut( "last_modified", new LuanJavaFunction(
-					File.class.getMethod( "lastModified" ), file
-				) );
-				tbl.rawPut( "set_last_modified", new LuanJavaFunction(
-					LuanFile.class.getMethod( "set_last_modified", Long.TYPE ), this
-				) );
-				tbl.rawPut( "length", new LuanJavaFunction(
-					File.class.getMethod( "length" ), file
-				) );
-				tbl.rawPut( "child", new LuanJavaFunction(
-					LuanFile.class.getMethod( "child", LuanState.class, String.class ), this
-				) );
-				tbl.rawPut( "children", new LuanJavaFunction(
-					LuanFile.class.getMethod( "children", LuanState.class ), this
-				) );
-				tbl.rawPut( "parent", new LuanJavaFunction(
-					LuanFile.class.getMethod( "parent", LuanState.class ), this
-				) );
-				tbl.rawPut( "rename_to", new LuanJavaFunction(
-					LuanFile.class.getMethod( "rename_to", Object.class ), this
-				) );
-				tbl.rawPut( "canonical", new LuanJavaFunction(
-					LuanFile.class.getMethod( "canonical", LuanState.class ), this
-				) );
-				tbl.rawPut( "create_temp_file", new LuanJavaFunction(
-					LuanFile.class.getMethod( "create_temp_file", LuanState.class, String.class, String.class ), this
-				) );
-			} catch(NoSuchMethodException e) {
-				throw new RuntimeException(e);
-			}
-			return tbl;
-		}
-	}
-
-	public static LuanTable null_() {
-		return nullIO.table();
-	}
-
-	public static LuanTable string(String s) throws LuanException {
-		Utils.checkNotNull(s);
-		return new LuanString(s).table();
-	}
-
-	public static LuanTable file(LuanState luan,String name) throws LuanException {
-		File file = new File(name);
-		return new LuanFile(file).table();
-	}
-
-	public static LuanTable classpath(LuanState luan,String name) throws LuanException {
-		if( name.contains("//") )
-			return null;
-		String path = name;
-		check(luan,"classpath:"+path);
-		URL url;
-		if( !path.contains("#") ) {
-			url = ClassLoader.getSystemResource(path);
-		} else {
-			String[] a = path.split("#");
-			url = ClassLoader.getSystemResource(a[0]);
-			if( url==null ) {
-				for( int i=1; i<a.length; i++ ) {
-					url = ClassLoader.getSystemResource(a[0]+"/"+a[i]);
-					if( url != null ) {
-						try {
-							url = new URL(url,".");
-						} catch(MalformedURLException e) {
-							throw new RuntimeException(e);
-						}
-						break;
-					}
-				}
-			}
-		}
-		if( url != null )
-			return new LuanUrl(luan,url,null).table();
-
-		return null;
-	}
-
-	private static LuanTable url(LuanState luan,String url,LuanTable options) throws IOException, LuanException {
-		return new LuanUrl(luan,new URL(url),options).table();
-	}
-
-	public static LuanTable http(LuanState luan,String path,LuanTable options) throws IOException, LuanException {
-		return url(luan,"http:"+path,options);
-	}
-
-	public static LuanTable https(LuanState luan,String path,LuanTable options) throws IOException, LuanException {
-		return url(luan,"https:"+path,options);
-	}
-
-	public static LuanTable luan(LuanState luan,String path) throws LuanException {
-		return classpath( luan, "luan/modules/" + path );
-	}
-
-	public static LuanTable stdin(LuanState luan) throws LuanException {
-		LuanTable io = (LuanTable)PackageLuan.require(luan,"luan:Io.luan");
-		return (LuanTable)io.get(luan,"stdin");
-	}
-
-	public static LuanTable newSchemes() {
-		LuanTable schemes = new LuanTable();
-		try {
-			schemes.rawPut( "null", new LuanJavaFunction(IoLuan.class.getMethod("null_"),null) );
-			add( schemes, "string", String.class );
-			add( schemes, "file", LuanState.class, String.class );
-			add( schemes, "classpath", LuanState.class, String.class );
-			add( schemes, "socket", String.class );
-			add( schemes, "http", LuanState.class, String.class, LuanTable.class );
-			add( schemes, "https", LuanState.class, String.class, LuanTable.class );
-			add( schemes, "luan", LuanState.class, String.class );
-			add( schemes, "stdin", LuanState.class );
-			add( schemes, "os", LuanState.class, String.class, LuanTable.class );
-		} catch(NoSuchMethodException e) {
-			throw new RuntimeException(e);
-		}
-		return schemes;
-	}
-
-	private static LuanTable schemes(LuanState luan) throws LuanException {
-		LuanTable t = (LuanTable)PackageLuan.loaded(luan).rawGet("luan:Io.luan");
-		if( t == null )
-			return newSchemes();
-		t = (LuanTable)t.get(luan,"schemes");
-		if( t == null )
-			return newSchemes();
-		return t;
-	}
-
-	public static LuanTable uri(LuanState luan,String name,LuanTable options) throws LuanException {
-		int i = name.indexOf(':');
-		if( i == -1 )
-			throw new LuanException( "invalid Io.uri name '"+name+"', missing scheme" );
-		String scheme = name.substring(0,i);
-		String location = name.substring(i+1);
-		LuanTable schemes = schemes(luan);
-		LuanFunction opener = (LuanFunction)schemes.get(luan,scheme);
-		if( opener == null )
-			throw new LuanException( "invalid scheme '"+scheme+"' in '"+name+"'" );
-		return (LuanTable)Luan.first(opener.call(luan,new Object[]{location,options}));
-	}
-
-	public static final class LuanSocket extends LuanIO {
-		public final Socket socket;
-
-		private LuanSocket(String host,int port) throws LuanException {
-			try {
-				this.socket = new Socket(host,port);
-			} catch(IOException e) {
-				throw new LuanException(e.toString());
-			}
-		}
-
-		private LuanSocket(Socket socket) {
-			this.socket = socket;
-		}
-
-		@Override public InputStream inputStream() throws IOException {
-			return socket.getInputStream();
-		}
-
-		@Override OutputStream outputStream() throws IOException {
-			return socket.getOutputStream();
-		}
-
-		@Override public String to_string() {
-			return socket.toString();
-		}
-
-		@Override public String to_uri_string() {
-			throw new UnsupportedOperationException();
-		}
-	}
-
-	public static LuanTable socket(String name) throws LuanException, IOException {
-		int i = name.indexOf(':');
-		if( i == -1 )
-			throw new LuanException( "invalid socket '"+name+"', format is: <host>:<port>" );
-		String host = name.substring(0,i);
-		String portStr = name.substring(i+1);
-		int port = Integer.parseInt(portStr);
-		return new LuanSocket(host,port).table();
-	}
-
-	public static LuanFunction socket_server(int port) throws IOException {
-		final ServerSocket ss = new ServerSocket(port);
-		return new LuanFunction() {
-			@Override public Object call(LuanState luan,Object[] args) throws LuanException {
-				try {
-					if( args.length > 0 ) {
-						if( args.length > 1 || !"close".equals(args[0]) )
-							throw new LuanException( "the only argument allowed is 'close'" );
-						ss.close();
-						return null;
-					}
-					return new LuanSocket(ss.accept()).table();
-				} catch(IOException e) {
-					throw new LuanException(e);
-				}
-			}
-		};
-	}
-
-
-	public static final class LuanOs extends LuanIO {
-		private final String cmd;
-		private final Process proc;
-
-		private LuanOs(LuanState luan,String cmd,LuanTable options) throws IOException, LuanException {
-			this.cmd = cmd;
-			File dir = null;
-			if( options != null ) {
-				Map map = options.asMap(luan);
-				Object obj = map.remove("dir");
-				dir = objToFile(obj);
-				if( dir==null )
-					throw new LuanException( "bad option 'dir' (string or file table expected)" );
-				if( !map.isEmpty() )
-					throw new LuanException( "unrecognized options: "+map );
-			}
-			this.proc = Runtime.getRuntime().exec(cmd,null,dir);
-		}
-
-		@Override public InputStream inputStream() throws IOException {
-			return proc.getInputStream();
-		}
-
-		@Override OutputStream outputStream() throws IOException {
-			return proc.getOutputStream();
-		}
-
-		@Override public String to_string() {
-			return proc.toString();
-		}
-
-		@Override public String to_uri_string() {
-			throw new UnsupportedOperationException();
-		}
-
-		@Override public boolean exists() {
-			return true;
-		}
-
-		public void wait_for()
-			throws IOException, LuanException
-		{
-			try {
-				proc.waitFor();
-			} catch(InterruptedException e) {
-				throw new RuntimeException(e);
-			}
-			int exitVal = proc.exitValue();
-			if( exitVal != 0 ) {
-				Reader err = new InputStreamReader(proc.getErrorStream());
-				String error = "error in: "+cmd+"\n"+Utils.readAll(err);
-				err.close();
-				throw new LuanException(error);
-			}
-		}
-
-		@Override public String read_text() throws IOException, LuanException {
-			String s = super.read_text();
-			wait_for();
-			return s;
-		}
-
-		@Override public LuanTable table() {
-			LuanTable tbl = super.table();
-			try {
-				tbl.rawPut( "wait_for", new LuanJavaFunction(
-					LuanOs.class.getMethod( "wait_for" ), this
-				) );
-			} catch(NoSuchMethodException e) {
-				throw new RuntimeException(e);
-			}
-			return tbl;
-		}
-	}
-
-	public static LuanTable os(LuanState luan,String cmd,LuanTable options) throws IOException, LuanException {
-		return new LuanOs(luan,cmd,options).table();
-	}
-
-
-	public static String ip(String domain) {
-		try {
-			return InetAddress.getByName(domain).getHostAddress();
-		} catch(UnknownHostException e) {
-			return null;
-		}
-	}
-
-	public static LuanTable my_ips() throws IOException {
-		LuanTable tbl = new LuanTable();
-		for( Enumeration<NetworkInterface> e1 = NetworkInterface.getNetworkInterfaces(); e1.hasMoreElements(); ) {
-			NetworkInterface ni = e1.nextElement();
-			for( Enumeration<InetAddress> e2 = ni.getInetAddresses(); e2.hasMoreElements(); ) {
-				InetAddress ia = e2.nextElement();
-				if( ia instanceof Inet4Address )
-					tbl.rawPut(ia.getHostAddress(),true);
-			}
-		}
-		return tbl;
-	}
-
-/*
-	// files maps zip name to uri
-	public static void zip(LuanState luan,String zipUri,LuanTable files) throws LuanException, IOException {
-		Object obj = uri(luan,zipUri,null).rawGet("java");
-		if( !(obj instanceof LuanIO) )
-			throw new LuanException("invalid uri for zip");
-		LuanIO zipIo = (LuanIO)obj;
-		ZipOutputStream out = new ZipOutputStream(zipIo.outputStream());
-		for( Map.Entry<Object,Object> entry : files.iterable(luan) ) {
-			obj = entry.getKey();
-			if( !(obj instanceof String) )
-				throw new LuanException("zip file table keys must be strings");
-			String fileName = (String)obj;
-			obj = entry.getValue();
-			if( !(obj instanceof String) )
-				throw new LuanException("zip file table values must be strings");
-			String uriStr = (String)obj;
-			out.putNextEntry(new ZipEntry(fileName));
-			obj = uri(luan,uriStr,null).rawGet("java");
-			if( !(obj instanceof LuanIn) )
-				throw new LuanException("invalid uri for zip");
-			LuanIn zipIn = (LuanIn)obj;
-			InputStream in = zipIn.inputStream();
-			Utils.copyAll(in,out);
-			in.close();
-			out.closeEntry();
-		}
-		out.close();
-	}
-*/
-
-	// security
-
-	public interface Security {
-		public void check(LuanState luan,String name) throws LuanException;
-	}
-
-	private static String SECURITY_KEY = "Io.Security";
-
-	private static void check(LuanState luan,String name) throws LuanException {
-		Security s = (Security)luan.registry().get(SECURITY_KEY);
-		if( s!=null )
-			s.check(luan,name);
-	}
-
-	public static void setSecurity(LuanState luan,Security s) {
-		luan.registry().put(SECURITY_KEY,s);
-	}
-
-	private void IoLuan() {}  // never
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/JavaLuan.java
--- a/core/src/luan/modules/JavaLuan.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,475 +0,0 @@
-package luan.modules;
-
-import java.lang.reflect.Array;
-import java.lang.reflect.AccessibleObject;
-import java.lang.reflect.Member;
-import java.lang.reflect.Field;
-import java.lang.reflect.Method;
-import java.lang.reflect.Constructor;
-import java.lang.reflect.Modifier;
-import java.lang.reflect.InvocationHandler;
-import java.lang.reflect.Proxy;
-import java.util.Map;
-import java.util.HashMap;
-import java.util.List;
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.Collections;
-import java.util.Arrays;
-import luan.Luan;
-import luan.LuanState;
-import luan.LuanTable;
-import luan.LuanException;
-import luan.LuanFunction;
-import luan.LuanJavaFunction;
-
-
-public final class JavaLuan {
-
-	public static void java(LuanState luan) throws LuanException {
-		check(luan,LuanException.currentSource());
-		luan.java.ok = true;
-	}
-
-	public static final LuanFunction javaFn;
-	static {
-		try {
-			javaFn = new LuanJavaFunction(JavaLuan.class.getMethod("java",LuanState.class),null);
-		} catch(NoSuchMethodException e) {
-			throw new RuntimeException(e);
-		}
-	}
-
-	private static void checkJava(LuanState luan) throws LuanException {
-		if( !luan.java.ok )
-			throw new LuanException("Java isn't allowed");
-	}
-
-	static final Object FAIL = new Object();
-
-	public static Object __index(LuanState luan,Object obj,Object key,boolean canReturnFail) throws LuanException {
-		checkJava(luan);
-		Class cls;
-		if( obj instanceof Static ) {
-			Static st = (Static)obj;
-			cls = st.cls;
-			if( key instanceof String ) {
-				String name = (String)key;
-				if( "class".equals(name) ) {
-					return cls;
-				} else if( "new".equals(name) ) {
-					Constructor[] constructors = cls.getConstructors();
-					if( constructors.length > 0 ) {
-						if( constructors.length==1 ) {
-							return new LuanJavaFunction(constructors[0],null);
-						} else {
-							List<LuanJavaFunction> fns = new ArrayList<LuanJavaFunction>();
-							for( Constructor constructor : constructors ) {
-								fns.add(new LuanJavaFunction(constructor,null));
-							}
-							return new AmbiguousJavaFunction(fns);
-						}
-					}
-/*
-				} else if( "assert".equals(name) ) {
-					return new LuanJavaFunction(assertClass,new AssertClass(cls));
-*/
-				} else if( "luan_proxy".equals(name) ) {
-					return new LuanJavaFunction(luan_proxyMethod,st);
-				} else {
-					List<Member> members = getStaticMembers(cls,name);
-					if( !members.isEmpty() ) {
-						return member(null,members);
-					}
-				}
-			}
-		} else {
-			cls = obj.getClass();
-			if( cls.isArray() ) {
-				if( "length".equals(key) ) {
-					return Array.getLength(obj);
-				}
-				Integer i = Luan.asInteger(key);
-				if( i != null ) {
-					return Array.get(obj,i);
-				}
-//				throw new LuanException(luan,"invalid member '"+key+"' for java array: "+obj);
-			} else if( key instanceof String ) {
-				String name = (String)key;
-				if( "instanceof".equals(name) ) {
-					return new LuanJavaFunction(instanceOf,new InstanceOf(obj));
-				} else {
-					List<Member> members = getMembers(cls,name);
-					if( !members.isEmpty() ) {
-						return member(obj,members);
-					}
-				}
-			}
-		}
-//System.out.println("invalid member '"+key+"' for java object: "+obj);
-		if( canReturnFail )
-			return FAIL;
-		throw new LuanException( "invalid index '"+key+"' for java "+cls );
-	}
-
-	private static Object member(Object obj,List<Member> members) throws LuanException {
-		try {
-			if( members.size()==1 ) {
-				Member member = members.get(0);
-				if( member instanceof Static ) {
-					return member;
-				} else if( member instanceof Field ) {
-					Field field = (Field)member;
-					Object rtn = field.get(obj);
-					return rtn instanceof Object[] ? Arrays.asList((Object[])rtn) : rtn;
-				} else {
-					Method method = (Method)member;
-					return new LuanJavaFunction(method,obj);
-				}
-			} else {
-				List<LuanJavaFunction> fns = new ArrayList<LuanJavaFunction>();
-				for( Member member : members ) {
-					Method method = (Method)member;
-					fns.add(new LuanJavaFunction(method,obj));
-				}
-				return new AmbiguousJavaFunction(fns);
-			}
-		} catch(IllegalAccessException e) {
-			throw new RuntimeException(e);
-		}
-	}
-
-	public static void __new_index(LuanState luan,Object obj,Object key,Object value) throws LuanException {
-		checkJava(luan);
-		Class cls;
-		if( obj instanceof Static ) {
-			Static st = (Static)obj;
-			cls = st.cls;
-			if( key instanceof String ) {
-				String name = (String)key;
-				List<Member> members = getStaticMembers(cls,name);
-				if( !members.isEmpty() ) {
-					if( members.size() != 1 )
-						throw new RuntimeException("not field '"+name+"' of "+obj);
-					setMember(obj,members,value);
-					return;
-				}
-			}
-//			throw new LuanException(luan,"invalid member '"+key+"' for: "+obj);
-		} else {
-			cls = obj.getClass();
-			if( cls.isArray() ) {
-				Integer i = Luan.asInteger(key);
-				if( i != null ) {
-					Array.set(obj,i,value);
-					return;
-				}
-//				throw new LuanException(luan,"invalid member '"+key+"' for java array: "+obj);
-			} else if( key instanceof String ) {
-				String name = (String)key;
-				List<Member> members = getMembers(cls,name);
-				if( !members.isEmpty() ) {
-					if( members.size() != 1 )
-						throw new RuntimeException("not field '"+name+"' of "+obj);
-					setMember(obj,members,value);
-					return;
-				}
-			}
-		}
-		throw new LuanException( "invalid index for java "+cls );
-	}
-
-	private static void setMember(Object obj,List<Member> members,Object value) {
-		Field field = (Field)members.get(0);
-		try {
-			try {
-				field.set(obj,value);
-			} catch(IllegalArgumentException e) {
-				Class cls = field.getType();
-				if( value instanceof Number ) {
-					Number n = (Number)value;
-					if( cls.equals(Integer.TYPE) || cls.equals(Integer.class) ) {
-						int r = n.intValue();
-						if( r==n.doubleValue() ) {
-							field.setInt(obj,r);
-							return;
-						}
-					}
-				}
-				throw e;
-			}
-		} catch(IllegalAccessException e) {
-			throw new RuntimeException(e);
-		}
-	}
-
-	public static boolean privateAccess = false;
-	private static Map<Class,Map<String,List<Member>>> memberMap = new HashMap<Class,Map<String,List<Member>>>();
-
-	private static synchronized List<Member> getMembers(Class cls,String name) {
-		Map<String,List<Member>> clsMap = memberMap.get(cls);
-		if( clsMap == null ) {
-			clsMap = new HashMap<String,List<Member>>();
-			for( Class c : cls.getClasses() ) {
-				String s = c.getSimpleName();
-				List<Member> list = new ArrayList<Member>();
-				clsMap.put(s,list);
-				list.add(new Static(c));
-			}
-			for( Field field : cls.getFields() ) {
-				String s = field.getName();
-				try {
-					if( !cls.getField(s).equals(field) )
-						continue;  // not accessible
-				} catch(NoSuchFieldException e) {
-					throw new RuntimeException(e);
-				}
-				List<Member> list = new ArrayList<Member>();
-				clsMap.put(s,list);
-				list.add(field);
-			}
-			for( Method method : cls.getMethods() ) {
-				String s = method.getName();
-				List<Member> list = clsMap.get(s);
-				if( list == null || !(list.get(0) instanceof Method) ) {
-					list = new ArrayList<Member>();
-					clsMap.put(s,list);
-				}
-				list.add(method);
-			}
-			if( privateAccess ) {
-				for( Method method : cls.getDeclaredMethods() ) {
-					String s = method.getName();
-					List<Member> list = clsMap.get(s);
-					if( list == null ) {
-						list = new ArrayList<Member>();
-						clsMap.put(s,list);
-					} else if( !(list.get(0) instanceof Method) )
-						continue;
-					if( !list.contains(method) ) {
-						list.add(method);
-					}
-				}
-				for( Field field : cls.getDeclaredFields() ) {
-					String s = field.getName();
-					List<Member> list = clsMap.get(s);
-					if( list == null ) {
-						list = new ArrayList<Member>();
-						clsMap.put(s,list);
-						list.add(field);
-					}
-				}
-			}
-			for( List<Member> members : clsMap.values() ) {
-				for( Member m : members ) {
-					if( m instanceof AccessibleObject )
-						((AccessibleObject)m).setAccessible(true);
-				}
-			}
-			memberMap.put(cls,clsMap);
-		}
-		List<Member> rtn = clsMap.get(name);
-		if( rtn==null )
-			rtn = Collections.emptyList();
-		return rtn;
-	}
-
-	private static synchronized List<Member> getStaticMembers(Class cls,String name) {
-		List<Member> staticMembers = new ArrayList<Member>();
-		for( Member m : getMembers(cls,name) ) {
-			if( Modifier.isStatic(m.getModifiers()) )
-				staticMembers.add(m);
-		}
-		return staticMembers;
-	}
-
-	static final class Static implements Member {
-		final Class cls;
-
-		Static(Class cls) {
-			this.cls = cls;
-		}
-
-		@Override public String toString() {
-			return cls.toString();
-		}
-
-		@Override public Class getDeclaringClass() {
-			return cls.getDeclaringClass();
-		}
-
-		@Override public String getName() {
-			return cls.getName();
-		}
-
-		@Override public int getModifiers() {
-			return cls.getModifiers();
-		}
-
-		@Override public boolean isSynthetic() {
-			return cls.isSynthetic();
-		}
-
-		public Object luan_proxy(final LuanState luan,final LuanTable t) throws LuanException {
-			return Proxy.newProxyInstance(
-				cls.getClassLoader(),
-				new Class[]{cls},
-				new InvocationHandler() {
-					public Object invoke(Object proxy,Method method, Object[] args)
-						throws Throwable
-					{
-						if( args==null )
-							args = new Object[0];
-						String name = method.getName();
-						Object fnObj = t.get(luan,name);
-						if( fnObj == null )
-							throw new NullPointerException("luan_proxy couldn't find method '"+name+"'");
-						LuanFunction fn = Luan.checkFunction(fnObj);
-						return Luan.first(fn.call(luan,args));
-					}
-				}
-			);
-		}
-	}
-	private static final Method luan_proxyMethod;
-	static {
-		try {
-			luan_proxyMethod = Static.class.getMethod("luan_proxy",LuanState.class,LuanTable.class);
-			luan_proxyMethod.setAccessible(true);
-		} catch(NoSuchMethodException e) {
-			throw new RuntimeException(e);
-		}
-	}
-
-	public static Static load(LuanState luan,String name) throws LuanException {
-		checkJava(luan);
-		Class cls;
-		try {
-			cls = Class.forName(name);
-		} catch(ClassNotFoundException e) {
-			try {
-				cls = Thread.currentThread().getContextClassLoader().loadClass(name);
-			} catch(ClassNotFoundException e2) {
-				return null;
-			}
-		}
-		return new Static(cls);
-	}
-
-	private static class AmbiguousJavaFunction extends LuanFunction {
-		private final Map<Integer,List<LuanJavaFunction>> fnMap = new HashMap<Integer,List<LuanJavaFunction>>();
-
-		AmbiguousJavaFunction(List<LuanJavaFunction> fns) {
-			for( LuanJavaFunction fn : fns ) {
-				Integer n = fn.getParameterTypes().length;
-				List<LuanJavaFunction> list = fnMap.get(n);
-				if( list==null ) {
-					list = new ArrayList<LuanJavaFunction>();
-					fnMap.put(n,list);
-				}
-				list.add(fn);
-			}
-		}
-
-		@Override public Object call(LuanState luan,Object[] args) throws LuanException {
-			for( LuanJavaFunction fn : fnMap.get(args.length) ) {
-				try {
-					return fn.rawCall(luan,args);
-				} catch(IllegalArgumentException e) {}
-			}
-			throw new LuanException("no method matched args: "+Arrays.asList(args));
-		}
-	}
-
-	private static class InstanceOf {
-		private final Object obj;
-
-		InstanceOf(Object obj) {
-			this.obj = obj;
-		}
-
-		public boolean instanceOf(Static st) {
-			return st.cls.isInstance(obj);
-		}
-	}
-	private static final Method instanceOf;
-	static {
-		try {
-			instanceOf = InstanceOf.class.getMethod("instanceOf",Static.class);
-			instanceOf.setAccessible(true);
-		} catch(NoSuchMethodException e) {
-			throw new RuntimeException(e);
-		}
-	}
-
-/*
-	private static class AssertClass {
-		private final Class cls;
-
-		AssertClass(Class cls) {
-			this.cls = cls;
-		}
-
-		public Object assertClass(LuanState luan,Object v) throws LuanException {
-			if( !cls.isInstance(v) ) {
-				String got = v.getClass().getSimpleName();
-				String expected = cls.getSimpleName();
-				throw new LuanException(luan,"bad argument #1 ("+expected+" expected, got "+got+")");
-			}
-			return v;
-		}
-	}
-	private static final Method assertClass;
-	static {
-		try {
-			assertClass = AssertClass.class.getMethod("assertClass",LuanState.class,Object.class);
-			assertClass.setAccessible(true);
-		} catch(NoSuchMethodException e) {
-			throw new RuntimeException(e);
-		}
-	}
-
-
-	public static Object proxy(final LuanState luan,Static st,final LuanTable t,final Object base) throws LuanException {
-		return Proxy.newProxyInstance(
-			st.cls.getClassLoader(),
-			new Class[]{st.cls},
-			new InvocationHandler() {
-				public Object invoke(Object proxy,Method method, Object[] args)
-					throws Throwable
-				{
-					if( args==null )
-						args = new Object[0];
-					String name = method.getName();
-					Object fnObj = t.get(name);
-					if( fnObj==null && base!=null )
-						return method.invoke(base,args);
-					LuanFunction fn = luan.checkFunction(fnObj);
-					return Luan.first(luan.call(fn,name,args));
-				}
-			}
-		);
-	}
-*/
-
-
-
-	// security
-
-	public interface Security {
-		public void check(LuanState luan,String name) throws LuanException;
-	}
-
-	private static String SECURITY_KEY = "Java.Security";
-
-	private static void check(LuanState luan,String name) throws LuanException {
-		Security s = (Security)luan.registry().get(SECURITY_KEY);
-		if( s!=null )
-			s.check(luan,name);
-	}
-
-	public static void setSecurity(LuanState luan,Security s) {
-		luan.registry().put(SECURITY_KEY,s);
-	}
-
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/Luan.luan
--- a/core/src/luan/modules/Luan.luan	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,50 +0,0 @@
-java()
-local BasicLuan = require "java:luan.modules.BasicLuan"
-
-local M = {}
-
-M.assert_binary = BasicLuan.assert_binary
-M.assert_boolean = BasicLuan.assert_boolean
-M.assert_function = BasicLuan.assert_function
-M.assert_integer = BasicLuan.assert_integer
-M.assert_long = BasicLuan.assert_long
-M.assert_number = BasicLuan.assert_number
-M.assert_string = BasicLuan.assert_string
-M.assert_table = BasicLuan.assert_table
-M.get_metatable = BasicLuan.get_metatable
-M.ipairs = BasicLuan.ipairs
-M.load = BasicLuan.load
-M.load_file = BasicLuan.load_file
-M.new_error = BasicLuan.new_error
-M.pairs = BasicLuan.pairs
-M.pcall = BasicLuan.pcall
-M.range = BasicLuan.range
-M.raw_equal = BasicLuan.raw_equal
-M.raw_get = BasicLuan.raw_get
-M.raw_len = BasicLuan.raw_len
-M.raw_set = BasicLuan.raw_set
-M.set_metatable = BasicLuan.set_metatable
-M.to_string = BasicLuan.to_string
-M.try = BasicLuan.try_
-M.type = BasicLuan.type
-M.values = BasicLuan.values
-
-function M.do_file(uri)
-	return M.load_file(uri)()
-end
-
-M.VERSION = M.do_file "classpath:luan/version.luan"
-
-function M.error(message)
-	M.new_error(message).throw()
-end
-
-function M.assert(v,message)
-	return v or M.error(message or "assertion failed!")
-end
-
-function M.eval(s,source_name)
-	return M.load( "return "..s, source_name or "eval" )()
-end
-
-return M
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/Math.luan
--- a/core/src/luan/modules/Math.luan	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,36 +0,0 @@
-java()
-local MathLuan = require "java:luan.modules.MathLuan"
-local JavaMath = require "java:java.lang.Math"
-local Integer = require "java:java.lang.Integer"
-local Double = require "java:java.lang.Double"
-
-
-local M = {}
-
-M.abs = MathLuan.abs
-M.acos = MathLuan.acos
-M.asin = MathLuan.asin
-M.atan = MathLuan.atan
-M.atan2 = MathLuan.atan2
-M.ceil = MathLuan.ceil
-M.cos = MathLuan.cos
-M.cosh = MathLuan.cosh
-M.deg = MathLuan.deg
-M.exp = MathLuan.exp
-M.floor = MathLuan.floor
-M.fmod = MathLuan.fmod
-M.huge = Double.POSITIVE_INFINITY
-M.log = MathLuan.log
-M.max = MathLuan.max
-M.max_integer = Integer.MAX_VALUE
-M.min = MathLuan.min
-M.min_integer = Integer.MIN_VALUE
-M.modf = MathLuan.modf
-M.pi = JavaMath.PI
-M.rad = MathLuan.rad
-M.random = MathLuan.random
-M.sin = MathLuan.sin
-M.sqrt = MathLuan.sqrt
-M.tan = MathLuan.tan
-
-return M
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/MathLuan.java
--- a/core/src/luan/modules/MathLuan.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,113 +0,0 @@
-package luan.modules;
-
-import luan.Luan;
-import luan.LuanState;
-import luan.LuanTable;
-import luan.LuanFunction;
-import luan.LuanException;
-
-
-public final class MathLuan {
-
-	public static double abs(double x) {
-		return Math.abs(x);
-	}
-
-	public static double acos(double x) {
-		return Math.acos(x);
-	}
-
-	public static double asin(double x) {
-		return Math.asin(x);
-	}
-
-	public static double atan(double x) {
-		return Math.atan(x);
-	}
-
-	public static double atan2(double y,double x) {
-		return Math.atan2(y,x);
-	}
-
-	public static double ceil(double x) {
-		return Math.ceil(x);
-	}
-
-	public static double cos(double x) {
-		return Math.cos(x);
-	}
-
-	public static double cosh(double x) {
-		return Math.cosh(x);
-	}
-
-	public static double deg(double x) {
-		return Math.toDegrees(x);
-	}
-
-	public static double exp(double x) {
-		return Math.exp(x);
-	}
-
-	public static double floor(double x) {
-		return Math.floor(x);
-	}
-
-	public static double fmod(double x,double y) {
-		return x % y;
-	}
-
-	public static double log(double x,Double base) {
-		return base==null ? Math.log(x) : Math.log(x)/Math.log(base);
-	}
-
-	public static double min(double x,double... a) {
-		for( double d : a ) {
-			if( x > d )
-				x = d;
-		}
-		return x;
-	}
-
-	public static double max(double x,double... a) {
-		for( double d : a ) {
-			if( x < d )
-				x = d;
-		}
-		return x;
-	}
-
-	public static double[] modf(double x) {
-		double i = (int)x;
-		return new double[]{i,x-i};
-	}
-
-	public static double rad(double x) {
-		return Math.toRadians(x);
-	}
-
-	public static double random(Integer m,Integer n) {
-		if( m==null )
-			return Math.random();
-		if( n==null )
-			return Math.floor(m*Math.random()) + 1;
-		return Math.floor((n-m+1)*Math.random()) + m;
-	}
-
-	public static double sin(double x) {
-		return Math.sin(x);
-	}
-
-	public static double sqrt(double x) {
-		return Math.sqrt(x);
-	}
-
-	public static double tan(double x) {
-		return Math.tan(x);
-	}
-
-	public static String long_to_string(long i,int radix) {
-		return Long.toString(i,radix);
-	}
-
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/Number.luan
--- a/core/src/luan/modules/Number.luan	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,14 +0,0 @@
-java()
-local BasicLuan = require "java:luan.modules.BasicLuan"
-local MathLuan = require "java:luan.modules.MathLuan"
-
-
-local M = {}
-
-M.double = BasicLuan.assert_double
-M.integer = BasicLuan.assert_integer
-M.long = BasicLuan.assert_long
-M.long_to_string = MathLuan.long_to_string
-M.type = BasicLuan.number_type
-
-return M
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/Package.luan
--- a/core/src/luan/modules/Package.luan	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,9 +0,0 @@
-java()
-local PackageLuan = require "java:luan.modules.PackageLuan"
-
-local M = {}
-
-M.loaded = PackageLuan.loaded()
-M.load = PackageLuan.load
-
-return M
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/PackageLuan.java
--- a/core/src/luan/modules/PackageLuan.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,79 +0,0 @@
-package luan.modules;
-
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.Collections;
-import luan.Luan;
-import luan.LuanState;
-import luan.LuanTable;
-import luan.LuanFunction;
-import luan.LuanJavaFunction;
-import luan.LuanException;
-
-
-public final class PackageLuan {
-
-	public static final LuanFunction requireFn;
-	static {
-		try {
-			requireFn = new LuanJavaFunction(PackageLuan.class.getMethod("require",LuanState.class,String.class),null);
-		} catch(NoSuchMethodException e) {
-			throw new RuntimeException(e);
-		}
-	}
-
-	public static LuanTable loaded(LuanState luan) {
-		LuanTable tbl = (LuanTable)luan.registry().get("Package.loaded");
-		if( tbl == null ) {
-			tbl = new LuanTable();
-			luan.registry().put("Package.loaded",tbl);
-		}
-		return tbl;
-	}
-
-	public static Object require(LuanState luan,String modName) throws LuanException {
-		Object mod = load(luan,modName);
-		if( mod==null )
-			throw new LuanException( "module '"+modName+"' not found" );
-		return mod;
-	}
-
-	public static Object load(LuanState luan,String modName) throws LuanException {
-		LuanTable loaded = loaded(luan);
-		Object mod = loaded.rawGet(modName);
-		if( mod == null ) {
-			if( modName.startsWith("java:") ) {
-				mod = JavaLuan.load(luan,modName.substring(5));
-			} else {
-				String src = read(luan,modName);
-				if( src == null )
-					return null;
-				LuanFunction loader = Luan.load(src,modName);
-				mod = Luan.first(
-					loader.call(luan,new Object[]{modName})
-				);
-				if( mod == null ) {
-					mod = loaded.rawGet(modName);
-					if( mod != null )
-						return mod;
-					throw new LuanException( "module '"+modName+"' returned nil" );
-				}
-			}
-			loaded.rawPut(modName,mod);
-		}
-		return mod;
-	}
-
-	static String read(LuanState luan,String uri) throws LuanException {
-		LuanTable t = IoLuan.uri(luan,uri,null);
-		if( t == null )
-			return null;
-		LuanFunction existsFn = (LuanFunction)t.get(luan,"exists");
-		boolean exists = (Boolean)Luan.first(existsFn.call(luan));
-		if( !exists )
-			return null;
-		LuanFunction reader = (LuanFunction)t.get(luan,"read_text");
-		return (String)Luan.first(reader.call(luan));
-	}
-
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/Parsers.luan
--- a/core/src/luan/modules/Parsers.luan	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,15 +0,0 @@
-java()
-local BBCode = require "java:luan.modules.parsers.BBCode"
-local Csv = require "java:luan.modules.parsers.Csv"
-local Theme = require "java:luan.modules.parsers.Theme"
-local Json = require "java:luan.modules.parsers.Json"
-
-local M = {}
-
-M.bbcode_to_html = BBCode.toHtml
-M.bbcode_to_text = BBCode.toText
-M.csv_to_list = Csv.toList
-M.json_parse = Json.parse
-M.theme_to_luan = Theme.toLuan
-
-return M
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/Rpc.luan
--- a/core/src/luan/modules/Rpc.luan	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,68 +0,0 @@
-java()
-local RpcLuan = require "java:luan.modules.RpcLuan"
-local Luan = require "luan:Luan.luan"
-local error = Luan.error
-local set_metatable = Luan.set_metatable or error()
-local try = Luan.try or error()
-local Io = require "luan:Io.luan"
-local Thread = require "luan:Thread.luan"
-local Logging = require "luan:logging/Logging.luan"  -- external dependency
-local logger = Logging.logger "Rpc"
-
-
-local M = {}
-
-M.port = 9101
-
-M.call = RpcLuan.call  -- Rpc.call(socket,fn_name,...)
-
-M.functions = {}
-
-function M.respond(socket,fns)
-	RpcLuan.respond( socket, fns or M.functions )
-end
-
-function M.remote_socket(socket_uri)
-	local mt = {}
-	function mt.__index(_,key)
-		return function(...)
-			local socket = Io.uri(socket_uri)
-			return M.call(socket,key,...)
-		end
-	end
-	local t = {}
-	set_metatable(t,mt)
-	return t
-end
-
-function M.remote(domain)
-	local socket = "socket:" .. domain .. ":" .. M.port
-	return M.remote_socket(socket)
-end
-
-function M.serve(port,fns)
-	local server = Io.socket_server(port or M.port)
-	while true do
-		try {
-			function()
-				local socket = server()
-				local function respond()
-					try {
-						function()
-							M.respond(socket,fns)
-						end
-						catch = function(e)
-							logger.error(e)
-						end
-					}
-				end
-				Thread.fork(respond)
-			end
-			catch = function(e)
-				logger.error(e)
-			end
-		}
-	end
-end
-
-return M
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/RpcLuan.java
--- a/core/src/luan/modules/RpcLuan.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,310 +0,0 @@
-package luan.modules;
-
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.BufferedInputStream;
-import java.io.BufferedOutputStream;
-import java.io.InputStreamReader;
-import java.io.FilterInputStream;
-import java.io.IOException;
-import java.io.EOFException;
-import java.net.Socket;
-import java.nio.charset.StandardCharsets;
-import java.util.Set;
-import java.util.IdentityHashMap;
-import java.util.Collections;
-import java.util.Map;
-import luan.Luan;
-import luan.LuanState;
-import luan.LuanTable;
-import luan.LuanFunction;
-import luan.LuanException;
-import luan.LuanMethod;
-import luan.modules.parsers.Json;
-import luan.modules.parsers.ParseException;
-
-
-public final class RpcLuan {
-	private static final int NIL = 0;
-	private static final int STRING = 1;
-	private static final int BOOLEAN = 2;
-	private static final int NUMBER = 3;
-	private static final int BINARY = 4;
-	private static final int TABLE = 5;
-	private static final int IO = 6;
-	private static final int LONG = 7;
-
-	@LuanMethod public static Object[] call(LuanState luan,LuanTable socketTbl,String fnName,Object... args)
-		throws LuanException, IOException
-	{
-		IoLuan.LuanSocket luanSocket = (IoLuan.LuanSocket)socketTbl.rawGet("java");
-		Socket socket = luanSocket.socket;
-		InputStream in = new BufferedInputStream(socket.getInputStream());
-		OutputStream out = new BufferedOutputStream(socket.getOutputStream());
-		Close close = new Close();
-		try {
-			writeString(out,fnName);
-			writeObjs(out,luan,args);
-			out.flush();
-			socket.shutdownOutput();
-			boolean ok = readBoolean(in);
-			if( ok ) {
-				return readObjs(in,luan,close);
-			} else {
-				String msg = readString(in);
-				throw new LuanException(msg);
-			}
-		} finally {
-			if( close.b) {
-				socket.close();
-			}
-		}
-	}
-
-	public static void respond(LuanState luan,LuanTable socketTbl,LuanTable fns)
-		throws IOException, LuanException
-	{
-		IoLuan.LuanSocket luanSocket = (IoLuan.LuanSocket)socketTbl.rawGet("java");
-		Socket socket = luanSocket.socket;
-		InputStream in = new BufferedInputStream(socket.getInputStream());
-		OutputStream out = new BufferedOutputStream(socket.getOutputStream());
-		try {
-			Object[] rtn;
-			try {
-				String fnName = readString(in);
-				Object[] args = readObjs(in,luan,null);
-				LuanFunction fn = (LuanFunction)fns.get(luan,fnName);
-				if( fn == null )
-					throw new LuanException( "function not found: " + fnName );
-				rtn = Luan.array(fn.call(luan,args));
-			} catch(LuanException e) {
-				writeBoolean(out,false);
-				writeString(out,e.getFullMessage());
-				out.flush();
-				return;
-			}
-			writeBoolean(out,true);
-			writeObjs(out,luan,rtn);
-			out.flush();
-		} finally {
-			socket.close();
-		}
-	}
-
-	private static void writeObjs(OutputStream out,LuanState luan,Object[] a) throws IOException, LuanException {
-		IoLuan.LuanIn luanIn = null;
-		writeInt(out,a.length);
-		for( Object obj : a ) {
-			if( obj instanceof LuanTable ) {
-				LuanTable tbl = (LuanTable)obj;
-				Object java = tbl.rawGet("java");
-				if( java instanceof IoLuan.LuanIn ) {
-					if( luanIn != null )
-						throw new LuanException("can't have multiple IO params");
-					luanIn = (IoLuan.LuanIn)java;
-					out.write(IO);
-					continue;
-				}
-			}
-			writeObj(out,luan,obj);
-		}
-		if( luanIn != null ) {
-			InputStream in = luanIn.inputStream();
-			Utils.copyAll(in,out);
-		}
-	}
-
-	private static Object[] readObjs(InputStream in,LuanState luan,Close close) throws IOException, LuanException {
-		int n = readInt(in);
-		Object[] rtn = new Object[n];
-		for( int i=0; i<n; i++ ) {
-			rtn[i] = readObj(in,luan,close);
-		}
-		return rtn;
-	}
-
-	private static void writeObj(OutputStream out,LuanState luan,Object obj) throws IOException, LuanException {
-		if( obj == null ) {
-			out.write(NIL);
-		}
-		else if( obj instanceof String ) {
-			out.write(STRING);
-			writeString(out,(String)obj);
-		}
-		else if( obj instanceof Boolean ) {
-			out.write(BOOLEAN);
-			writeBoolean(out,(Boolean)obj);
-		}
-		else if( obj instanceof Long ) {
-			out.write(LONG);
-			writeString(out,obj.toString());
-		}
-		else if( obj instanceof Number ) {
-			out.write(NUMBER);
-			writeString(out,obj.toString());
-		}
-		else if( obj instanceof byte[] ) {
-			byte[] a = (byte[])obj;
-			out.write(BINARY);
-			writeInt(out,a.length);
-			out.write(a);
-		}
-		else if( obj instanceof LuanTable ) {
-			out.write(TABLE);
-//			String s = pickle( luan, obj, Collections.newSetFromMap(new IdentityHashMap<LuanTable,Boolean>()) );
-			String s = Json.toString(obj);
-			writeString(out,s);
-		}
-		else
-			throw new LuanException( "invalid type: " + obj.getClass() );
-	}
-
-	private static Object readObj(InputStream in,LuanState luan,Close close) throws IOException, LuanException {
-		int type = in.read();
-		switch(type) {
-		case NIL:
-			return null;
-		case STRING:
-			return readString(in);
-		case BOOLEAN:
-			return readBoolean(in);
-		case LONG:
-			return Long.valueOf(readString(in));
-		case NUMBER:
-			return Double.valueOf(readString(in));
-		case BINARY:
-			return readBinary(in,readInt(in));
-		case TABLE:
-			String s = readString(in);
-/*
-			LuanFunction fn = Luan.load("return "+s,"rpc-reader");
-			return fn.call(luan);
-*/
-			try {
-				return Json.parse(s);
-			} catch(ParseException e) {
-				throw new LuanException(e);
-			}
-		case IO:
-			return new LuanInputStream(in,close).table();
-		default:
-			throw new LuanException( "invalid type: " + type );
-		}
-	}
-
-	private static Boolean readBoolean(InputStream in) throws IOException {
-		return Boolean.valueOf(readString(in));
-	}
-
-	private static String readString(InputStream in) throws IOException {
-		int len = readInt(in);
-		byte[] a = readBinary(in,len);
-		return new String(a,StandardCharsets.UTF_8);
-	}
-
-	private static int readInt(InputStream in) throws IOException {
-		int ch1 = in.read();
-		int ch2 = in.read();
-		int ch3 = in.read();
-		int ch4 = in.read();
-		if ((ch1 | ch2 | ch3 | ch4) < 0)
-			throw new EOFException();
-		return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4 << 0));
-	}
-
-	private static byte[] readBinary(InputStream in,int size) throws IOException {
-		byte[] a = new byte[size];
-		int i = 0;
-		while( i < size ) {
-			int n = in.read(a,i,size-i);
-			if( n == -1 )
-				throw new EOFException();
-			i += n;
-		}
-		return a;
-	}
-
-	private static void writeBoolean(OutputStream out,Boolean b) throws IOException {
-		writeString(out,b.toString());
-	}
-
-	private static void writeString(OutputStream out,String s) throws IOException {
-		byte[] a = s.getBytes(StandardCharsets.UTF_8);
-		writeInt(out,a.length);
-		out.write(a);
-	}
-
-	private static void writeInt(OutputStream out,int v) throws IOException {
-        out.write((v >>> 24) & 0xFF);
-        out.write((v >>> 16) & 0xFF);
-        out.write((v >>>  8) & 0xFF);
-        out.write((v >>>  0) & 0xFF);
-	}
-
-/*
-	private static String pickle(LuanState luan,Object obj,Set<LuanTable> set) throws LuanException {
-		if( obj == null )
-			return "nil";
-		if( obj instanceof Boolean )
-			return obj.toString();
-		if( obj instanceof Number )
-			return Luan.toString((Number)obj);
-		if( obj instanceof String )
-			return "\"" + Luan.stringEncode((String)obj) + "\"";
-		if( obj instanceof LuanTable ) {
-			LuanTable tbl = (LuanTable)obj;
-			if( !set.add(tbl) ) {
-				throw new LuanException( "circular reference in table" );
-			}
-			StringBuilder sb = new StringBuilder();
-			sb.append( "{" );
-			for( Map.Entry<Object,Object> entry : tbl.iterable(luan) ) {
-				sb.append( "[" );
-				sb.append( pickle(luan,entry.getKey(),set) );
-				sb.append( "]=" );
-				sb.append( pickle(luan,entry.getValue(),set) );
-				sb.append( ", " );
-			}
-			sb.append( "}" );
-			return sb.toString();
-		}
-		throw new LuanException( "invalid type: " + obj.getClass() );
-	}
-*/
-
-	private static class Close {
-		boolean b = true;
-	}
-
-	private static class LuanInputStream extends IoLuan.LuanIn {
-		private final InputStream in;
-		private final boolean close;
-
-		public LuanInputStream(InputStream in,Close close) {
-			this.in = in;
-			this.close = close!=null && close.b;
-			if(this.close)  close.b = false;
-		}
-
-		@Override public InputStream inputStream() {
-			return new FilterInputStream(in) {
-				@Override public void close() throws IOException {
-					if(close)  super.close();
-				}
-			};
-		}
-
-		@Override public String to_string() {
-			return "<input_stream>";
-		}
-
-		@Override public String to_uri_string() {
-			throw new UnsupportedOperationException();
-		}
-
-		@Override public boolean exists() {
-			return true;
-		}
-	};
-
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/String.luan
--- a/core/src/luan/modules/String.luan	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,28 +0,0 @@
-java()
-local StringLuan = require "java:luan.modules.StringLuan"
-local Pattern = require "java:java.util.regex.Pattern"
-
-local M = {}
-
-M.char = StringLuan.char_
-M.concat = StringLuan.concat
-M.encode = StringLuan.encode
-M.find = StringLuan.find
-M.format = StringLuan.format
-M.gmatch = StringLuan.gmatch
-M.gsub = StringLuan.gsub
-M.literal = Pattern.quote
-M.lower = StringLuan.lower
-M.match = StringLuan.match
-M.matches = StringLuan.matches
-M.rep = StringLuan.rep
-M.reverse = StringLuan.reverse
-M.split = StringLuan.split
-M.sub = StringLuan.sub
-M.to_binary = StringLuan.to_binary
-M.to_number = StringLuan.to_number
-M.trim = StringLuan.trim
-M.unicode = StringLuan.unicode
-M.upper = StringLuan.upper
-
-return M
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/StringLuan.java
--- a/core/src/luan/modules/StringLuan.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,247 +0,0 @@
-package luan.modules;
-
-import java.util.Arrays;
-import java.util.regex.Pattern;
-import java.util.regex.Matcher;
-import luan.Luan;
-import luan.LuanState;
-import luan.LuanTable;
-import luan.LuanFunction;
-import luan.LuanException;
-import luan.LuanMethod;
-
-
-public final class StringLuan {
-
-	static int start(String s,int i) {
-		int len = s.length();
-		return i==0 ? 0 : i > 0 ? Math.min(i-1,len) : Math.max(len+i,0);
-	}
-
-	static int start(String s,Integer i,int dflt) {
-		return i==null ? dflt : start(s,i);
-	}
-
-	static int end(String s,int i) {
-		int len = s.length();
-		return i==0 ? 0 : i > 0 ? Math.min(i,len) : Math.max(len+i+1,0);
-	}
-
-	static int end(String s,Integer i,int dflt) {
-		return i==null ? dflt : end(s,i);
-	}
-
-	@LuanMethod public static Integer[] unicode(String s,Integer i,Integer j) throws LuanException {
-		Utils.checkNotNull(s);
-		int start = start(s,i,1);
-		int end = end(s,j,start+1);
-		Integer[] chars = new Integer[end-start];
-		for( int k=0; k<chars.length; k++ ) {
-			chars[k] = (int)s.charAt(start+k);
-		}
-		return chars;
-	}
-
-	public static String char_(int... chars) {
-		char[] a = new char[chars.length];
-		for( int i=0; i<chars.length; i++ ) {
-			a[i] = (char)chars[i];
-		}
-		return new String(a);
-	}
-
-	@LuanMethod public static byte[] to_binary(String s) {
-		return s.getBytes();
-	}
-
-	public static String lower(String s) throws LuanException {
-		Utils.checkNotNull(s);
-		return s.toLowerCase();
-	}
-
-	public static String upper(String s) throws LuanException {
-		Utils.checkNotNull(s);
-		return s.toUpperCase();
-	}
-
-	public static String trim(String s) throws LuanException {
-		Utils.checkNotNull(s);
-		return s.trim();
-	}
-
-	public static String reverse(String s) throws LuanException {
-		Utils.checkNotNull(s);
-		return new StringBuilder(s).reverse().toString();
-	}
-
-	public static String rep(String s,int n,String sep) {
-		if( n < 1 )
-			return "";
-		StringBuilder buf = new StringBuilder(s);
-		while( --n > 0 ) {
-			if( sep != null )
-				buf.append(sep);
-			buf.append(s);
-		}
-		return buf.toString();
-	}
-
-	public static String sub(String s,int i,Integer j) throws LuanException {
-		Utils.checkNotNull(s);
-		int start = start(s,i);
-		int end = end(s,j,s.length());
-		return s.substring(start,end);
-	}
-
-	@LuanMethod public static Object[] find(String s,String pattern,Integer init,Boolean plain) {
-		int start = start(s,init,0);
-		if( Boolean.TRUE.equals(plain) ) {
-			int i = s.indexOf(pattern,start);
-			return i == -1 ? null : new Integer[]{i+1,i+pattern.length()};
-		}
-		Matcher m = Pattern.compile(pattern).matcher(s);
-		if( !m.find(start) )
-			return null;
-		int n = m.groupCount();
-		Object[] rtn = new Object[2+n];
-		rtn[0] = m.start() + 1;
-		rtn[1] = m.end();
-		for( int i=0; i<n; i++ ) {
-			rtn[2+i] = m.group(i+1);
-		}
-		return rtn;
-	}
-
-	@LuanMethod public static String[] match(String s,String pattern,Integer init) {
-		int start = start(s,init,0);
-		Matcher m = Pattern.compile(pattern).matcher(s);
-		if( !m.find(start) )
-			return null;
-		int n = m.groupCount();
-		if( n == 0 )
-			return new String[]{m.group()};
-		String[] rtn = new String[n];
-		for( int i=0; i<n; i++ ) {
-			rtn[i] = m.group(i+1);
-		}
-		return rtn;
-	}
-
-	public static LuanFunction gmatch(String s,String pattern) throws LuanException {
-		Utils.checkNotNull(s);
-		final Matcher m = Pattern.compile(pattern).matcher(s);
-		return new LuanFunction() {
-			@Override public Object call(LuanState luan,Object[] args) {
-				if( !m.find() )
-					return null;
-				final int n = m.groupCount();
-				if( n == 0 )
-					return m.group();
-				String[] rtn = new String[n];
-				for( int i=0; i<n; i++ ) {
-					rtn[i] = m.group(i+1);
-				}
-				return rtn;
-			}
-		};
-	}
-
-	@LuanMethod public static Object[] gsub(LuanState luan,String s,String pattern,Object repl,Integer n) throws LuanException {
-		Utils.checkNotNull(s);
-		int max = n==null ? Integer.MAX_VALUE : n;
-		final Matcher m = Pattern.compile(pattern).matcher(s);
-		if( repl instanceof String ) {
-			String replacement = (String)repl;
-			int i = 0;
-			StringBuffer sb = new StringBuffer();
-			while( i<max && m.find() ) {
-				m.appendReplacement(sb,replacement);
-				i++;
-			}
-			m.appendTail(sb);
-			return new Object[]{ sb.toString(), i };
-		}
-		if( repl instanceof LuanTable ) {
-			LuanTable t = (LuanTable)repl;
-			int i = 0;
-			StringBuffer sb = new StringBuffer();
-			while( i<max && m.find() ) {
-				String match = m.groupCount()==0 ? m.group() : m.group(1);
-				Object val = t.get(luan,match);
-				if( val != null ) {
-					String replacement = luan.toString(val);
-					m.appendReplacement(sb,replacement);
-				}
-				i++;
-			}
-			m.appendTail(sb);
-			return new Object[]{ sb.toString(), i };
-		}
-		if( repl instanceof LuanFunction ) {
-			LuanFunction fn = (LuanFunction)repl;
-			int i = 0;
-			StringBuffer sb = new StringBuffer();
-			while( i<max && m.find() ) {
-				Object[] args;
-				final int count = m.groupCount();
-				if( count == 0 ) {
-					args = new String[]{m.group()};
-				} else {
-					args = new String[count];
-					for( int j=0; j<count; j++ ) {
-						args[j] = m.group(j+1);
-					}
-				}
-				Object val = Luan.first( fn.call(luan,args) );
-				if( val != null ) {
-					String replacement = luan.toString(val);
-					m.appendReplacement(sb,replacement);
-				}
-				i++;
-			}
-			m.appendTail(sb);
-			return new Object[]{ sb.toString(), i };
-		}
-		throw new LuanException( "bad argument #3 to 'gsub' (string/function/table expected)" );
-	}
-
-	// note - String.format() is too stupid to convert between ints and floats.
-	public static String format(String format,Object... args) {
-		return String.format(format,args);
-	}
-
-	public static String concat(LuanState luan,Object... args) throws LuanException {
-		StringBuilder sb = new StringBuilder();
-		for( Object arg : args ) {
-			sb.append( luan.toString(arg) );
-		}
-		return sb.toString();
-	}
-
-	public static String encode(String s) {
-		return Luan.stringEncode(s);
-	}
-
-	public static Number to_number(String s,Integer base) throws LuanException {
-		Utils.checkNotNull(s);
-		try {
-			if( base == null ) {
-				return Double.valueOf(s);
-			} else {
-				return Long.valueOf(s,base);
-			}
-		} catch(NumberFormatException e) {}
-		return null;
-	}
-
-	public static boolean matches(String s,String pattern) throws LuanException {
-		Utils.checkNotNull(s);
-		return Pattern.compile(pattern).matcher(s).find();
-	}
-
-	public static LuanTable split(String s,String pattern) throws LuanException {
-		Utils.checkNotNull(s);
-		return new LuanTable(Arrays.asList(s.split(pattern)));
-	}
-
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/Table.luan
--- a/core/src/luan/modules/Table.luan	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,24 +0,0 @@
-java()
-local TableLuan = require "java:luan.modules.TableLuan"
-
-local M = {}
-
-M.clear = TableLuan.clear
-M.concat = TableLuan.concat
-M.copy = TableLuan.copy
-M.insert = TableLuan.insert
-M.new_property_table = TableLuan.new_property_table
-M.pack = TableLuan.pack
-M.remove = TableLuan.remove
-M.sort = TableLuan.sort
-M.unpack = TableLuan.unpack
-
-
-local Luan = require "luan:Luan.luan"
-local pairs = Luan.pairs
-
-function M.is_empty(t)
-	return pairs(t)() == nil
-end
-
-return M
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/TableLuan.java
--- a/core/src/luan/modules/TableLuan.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,120 +0,0 @@
-package luan.modules;
-
-import java.util.Comparator;
-import java.util.List;
-import java.util.ArrayList;
-import java.util.Arrays;
-import luan.Luan;
-import luan.LuanState;
-import luan.LuanTable;
-import luan.LuanFunction;
-import luan.LuanException;
-import luan.LuanRuntimeException;
-import luan.LuanMethod;
-import luan.LuanPropertyMeta;
-
-
-public final class TableLuan {
-
-	public static String concat(LuanState luan,LuanTable list,String sep,Integer i,Integer j) throws LuanException {
-		int first = i==null ? 1 : i;
-		int last = j==null ? list.length(luan) : j;
-		StringBuilder buf = new StringBuilder();
-		for( int k=first; k<=last; k++ ) {
-			Object val = list.get(luan,k);
-			if( val==null )
-				break;
-			if( sep!=null && k > first )
-				buf.append(sep);
-			String s = luan.toString(val);
-			buf.append(s);
-		}
-		return buf.toString();
-	}
-
-	public static void insert(LuanTable list,int pos,Object value) throws LuanException {
-		Utils.checkNotNull(list);
-		if( list.getMetatable() != null )
-			throw new LuanException("can't insert into a table with a metatable");
-		list.rawInsert(pos,value);
-	}
-
-	public static Object remove(LuanTable list,int pos) throws LuanException {
-		if( list.getMetatable() != null )
-			throw new LuanException("can't remove from a table with a metatable");
-		return list.rawRemove(pos);
-	}
-
-	private static interface LessThan {
-		public boolean isLessThan(Object o1,Object o2);
-	}
-
-	public static void sort(final LuanState luan,LuanTable list,final LuanFunction comp) throws LuanException {
-		if( list.getMetatable() != null )
-			throw new LuanException("can't sort a table with a metatable");
-		final LessThan lt;
-		if( comp==null ) {
-			lt = new LessThan() {
-				public boolean isLessThan(Object o1,Object o2) {
-					try {
-						return Luan.isLessThan(luan,o1,o2);
-					} catch(LuanException e) {
-						throw new LuanRuntimeException(e);
-					}
-				}
-			};
-		} else {
-			lt = new LessThan() {
-				public boolean isLessThan(Object o1,Object o2) {
-					try {
-						return Luan.checkBoolean(Luan.first(comp.call(luan,new Object[]{o1,o2})));
-					} catch(LuanException e) {
-						throw new LuanRuntimeException(e);
-					}
-				}
-			};
-		}
-		try {
-			list.rawSort( new Comparator<Object>() {
-				public int compare(Object o1,Object o2) {
-					return lt.isLessThan(o1,o2) ? -1 : lt.isLessThan(o2,o1) ? 1 : 0;
-				}
-			} );
-		} catch(LuanRuntimeException e) {
-			throw (LuanException)e.getCause();
-		}
-	}
-
-	public static LuanTable pack(Object... args) {
-		LuanTable tbl = new LuanTable(Arrays.asList(args));
-		tbl.rawPut( "n", args.length );
-		return tbl;
-	}
-
-	@LuanMethod public static Object[] unpack(LuanState luan,LuanTable tbl,Integer iFrom,Integer iTo) throws LuanException {
-		int from = iFrom!=null ? iFrom : 1;
-		int to = iTo!=null ? iTo : tbl.length(luan);
-		List<Object> list = new ArrayList<Object>();
-		for( int i=from; i<=to; i++ ) {
-			list.add( tbl.get(luan,i) );
-		}
-		return list.toArray();
-	}
-
-	public static LuanTable copy(LuanTable list,Integer from,Integer to) {
-		if( from == null )
-			return new LuanTable(list);
-		if( to == null )
-			to = list.rawLength();
-		return list.rawSubList(from,to);
-	}
-
-	public static LuanTable new_property_table() {
-		return LuanPropertyMeta.INSTANCE.newTable();
-	}
-
-	public static void clear(LuanTable tbl) {
-		tbl.rawClear();
-	}
-
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/Thread.luan
--- a/core/src/luan/modules/Thread.luan	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,10 +0,0 @@
-java()
-local ThreadLuan = require "java:luan.modules.ThreadLuan"
-
-local M = {}
-
-M.fork = ThreadLuan.fork
-M.schedule = ThreadLuan.schedule
-M.synchronized = ThreadLuan.synchronized_
-
-return M
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/ThreadLuan.java
--- a/core/src/luan/modules/ThreadLuan.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,71 +0,0 @@
-package luan.modules;
-
-import java.io.Closeable;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
-import luan.Luan;
-import luan.LuanState;
-import luan.LuanFunction;
-import luan.LuanTable;
-import luan.LuanException;
-import luan.DeepCloner;
-
-
-public final class ThreadLuan {
-	private static final Executor exec = Executors.newCachedThreadPool();
-	private static final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
-
-	public static void fork(LuanState luan,LuanFunction fn,Object... args) {
-		DeepCloner cloner = new DeepCloner();
-		final LuanState newLuan = (LuanState)cloner.deepClone(luan);
-		final LuanFunction newFn = (LuanFunction)cloner.get(fn);
-		final Object[] newArgs = cloner.deepClone(args);
-		exec.execute(new Runnable(){public void run() {
-			try {
-				newFn.call(newLuan,newArgs);
-			} catch(LuanException e) {
-				e.printStackTrace();
-			}
-		}});
-	}
-
-	public static LuanFunction synchronized_(final LuanState luan,final LuanFunction fn) throws LuanException {
-		Utils.checkNotNull(fn);
-		return new LuanFunction() {
-			@Override public Object call(LuanState ingored,Object[] args) throws LuanException {
-				synchronized(luan) {
-					return fn.call(luan,args);
-				}
-			}
-		};
-	}
-
-	public static void schedule(LuanState luan,long delay,boolean repeat,LuanFunction fn,Object... args) {
-		DeepCloner cloner = new DeepCloner();
-		final LuanState newLuan = (LuanState)cloner.deepClone(luan);
-		final LuanFunction newFn = (LuanFunction)cloner.get(fn);
-		final Object[] newArgs = cloner.deepClone(args);
-		Runnable r = new Runnable(){public void run() {
-			try {
-				newFn.call(newLuan,newArgs);
-			} catch(LuanException e) {
-				e.printStackTrace();
-			}
-		}};
-		final ScheduledFuture sf;
-		if( repeat ) {
-			sf = scheduler.scheduleWithFixedDelay(r,delay,delay,TimeUnit.MILLISECONDS);
-		} else {
-			sf = scheduler.schedule(r,delay,TimeUnit.MILLISECONDS);
-		}
-		final Closeable c = new Closeable(){public void close(){
-			boolean b = sf.cancel(false);
-		}};
-		luan.registry().put(c,c);  // prevent gc
-		luan.onClose(c);
-	}
-
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/Time.luan
--- a/core/src/luan/modules/Time.luan	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,71 +0,0 @@
--- incomplete, will add as needed
-
-java()
-local Luan = require "luan:Luan.luan"
-local error = Luan.error
-local ipairs = Luan.ipairs or error()
-local Table = require "luan:Table.luan"
-local System = require "java:java.lang.System"
-local Calendar = require "java:java.util.Calendar"
-local Date = require "java:java.util.Date"
-local TimeZone = require "java:java.util.TimeZone"
-local SimpleDateFormat = require "java:java.text.SimpleDateFormat"
-
-local M = {}
-
-function M.now()
-	return System.currentTimeMillis()
-end
-
--- add more as needed
-local fields = {
-	year = Calendar.YEAR;
-	month = Calendar.MONTH;
-	day_of_month = Calendar.DAY_OF_MONTH;
-}
-
-function M.get( time, ... )
-	local cal = Calendar.getInstance()
-	cal.setTimeInMillis(time)
-	local rtn = {}
-	for i, v in ipairs{...} do
-		local fld = fields[v.lower()]
-		fld or error("invalid field: "+v)
-		local n = cal.get(fld)
-		if fld == "month" then
-			n = n + 1
-		end
-		rtn[i] = n
-	end
-	return Table.unpack(rtn)
-end
-
-function M.format(time,pattern)
-	pattern = pattern or "yyyy-MM-dd HH:mm:ss"
-	return SimpleDateFormat.new(pattern).format(Date.new(time))
-end
-
-function M.on( year, month, day, hour, minute, second, millis )
-	month = month - 1
-	local cal = Calendar.getInstance()
-	cal.setLenient(false)
-	cal.set( year, month, day, hour or 0, minute or 0, second or 0 )
-	cal.set( Calendar.MILLISECOND, millis or 0 )
-	return cal.getTimeInMillis()
-end
-
-function M.period( t )
-	local cal = Calendar.getInstance()
-	cal.setTimeZone(TimeZone.getTimeZone("GMT"))
-	local days = t.days or 0
-	days = days + 1
-	cal.set( 1970, 0, days, t.hours or 0, t.minutes or 0, t.seconds or 0 )
-	cal.set( Calendar.MILLISECOND, t.millis or 0 )
-	return cal.getTimeInMillis()
-end
-
-function M.parse( pattern, source )
-	return SimpleDateFormat.new(pattern).parse(source).getTime()
-end
-
-return M
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/Utils.java
--- a/core/src/luan/modules/Utils.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,200 +0,0 @@
-package luan.modules;
-
-import java.io.Reader;
-import java.io.IOException;
-import java.io.ByteArrayOutputStream;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.File;
-import java.io.InputStreamReader;
-import java.net.URL;
-import java.net.MalformedURLException;
-import luan.LuanException;
-import luan.LuanTable;
-import luan.LuanFunction;
-
-
-public final class Utils {
-	private Utils() {}  // never
-
-	static final int bufSize = 8192;
-
-	private static void checkNotNull(Object v,String expected,int pos) throws LuanException {
-		if( v == null )
-			throw new LuanException("bad argument #"+pos+" ("+expected+" expected, got nil)");
-	}
-
-	public static void checkNotNull(String s,int pos) throws LuanException {
-		checkNotNull(s,"string",pos);
-	}
-
-	public static void checkNotNull(String s) throws LuanException {
-		checkNotNull(s,1);
-	}
-
-	public static void checkNotNull(byte[] b,int pos) throws LuanException {
-		checkNotNull(b,"binary",pos);
-	}
-
-	public static void checkNotNull(byte[] b) throws LuanException {
-		checkNotNull(b,1);
-	}
-
-	public static void checkNotNull(LuanTable t,int pos) throws LuanException {
-		checkNotNull(t,"table",pos);
-	}
-
-	public static void checkNotNull(LuanTable t) throws LuanException {
-		checkNotNull(t,1);
-	}
-
-	public static void checkNotNull(Number n,int pos) throws LuanException {
-		checkNotNull(n,"number",pos);
-	}
-
-	public static void checkNotNull(Number n) throws LuanException {
-		checkNotNull(n,1);
-	}
-
-	public static void checkNotNull(LuanFunction fn,int pos) throws LuanException {
-		checkNotNull(fn,"function",pos);
-	}
-
-	public static void checkNotNull(LuanFunction fn) throws LuanException {
-		checkNotNull(fn,1);
-	}
-
-	public static String readAll(Reader in)
-		throws IOException
-	{
-		char[] a = new char[bufSize];
-		StringBuilder buf = new StringBuilder();
-		int n;
-		while( (n=in.read(a)) != -1 ) {
-			buf.append(a,0,n);
-		}
-		return buf.toString();
-	}
-
-	public static void copyAll(InputStream in,OutputStream out)
-		throws IOException
-	{
-		byte[] a = new byte[bufSize];
-		int n;
-		while( (n=in.read(a)) != -1 ) {
-			out.write(a,0,n);
-		}
-	}
-
-	public static byte[] readAll(InputStream in)
-		throws IOException
-	{
-		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		copyAll(in,out);
-		return out.toByteArray();
-	}
-/*
-	public static boolean exists(File file) {
-		try {
-			return file.exists() && file.getName().equals(file.getCanonicalFile().getName());
-		} catch(IOException e) {
-			throw new RuntimeException(e);
-		}
-	}
-*/
-/*
-	private static File toFile(String path) {
-		if( path.contains("//") )
-			return null;
-		File file = new File(path);
-		return file.exists() ? file : null;
-	}
-
-	private static URL toUrl(String path) {
-		if( path.indexOf(':') == -1 )
-			return null;
-		if( path.startsWith("classpath:") ) {
-			path = path.substring(10);
-			if( path.contains("//") )
-				return null;
-			URL url;
-			if( !path.contains("#") ) {
-				url = ClassLoader.getSystemResource(path);
-			} else {
-				String[] a = path.split("#");
-				url = ClassLoader.getSystemResource(a[0]);
-				if( url==null ) {
-					for( int i=1; i<a.length; i++ ) {
-						url = ClassLoader.getSystemResource(a[0]+"/"+a[i]);
-						if( url != null ) {
-							try {
-								url = new URL(url,".");
-							} catch(MalformedURLException e) {
-								throw new RuntimeException(e);
-							}
-							break;
-						}
-					}
-				}
-			}
-			return url==null ? null : url;
-		}
-		try {
-			return new URL(path);
-		} catch(MalformedURLException e) {}
-		return null;
-	}
-
-	static boolean exists(String path) {
-		return toFile(path)!=null || toUrl(path)!=null;
-	}
-*/
-
-
-
-/*	replace by uri"os:..."
-
-	// process
-
-	public static class ProcessException extends IOException {
-		private ProcessException(String msg) {
-			super(msg);
-		}
-	}
-
-	public static void checkProcess(Process proc)
-		throws IOException
-	{
-		try {
-			proc.waitFor();
-		} catch(InterruptedException e) {
-			throw new RuntimeException(e);
-		}
-		int exitVal = proc.exitValue();
-		if( exitVal != 0 ) {
-			Reader err = new InputStreamReader(proc.getErrorStream());
-			String error = readAll(err);
-			err.close();
-			throw new ProcessException(error);
-		}
-	}
-
-	public static String getOutput(Process proc)
-		throws IOException
-	{
-		Reader in = new InputStreamReader(proc.getInputStream());
-		String s = readAll(in);
-		in.close();
-		return s;
-	}
-
-	public static String execProcess(String... cmd)
-		throws IOException
-	{
-		Process proc = Runtime.getRuntime().exec(cmd);
-		String s = getOutput(proc);
-		checkProcess(proc);
-		return s;
-	}
-*/
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/Which_mod.luan
--- a/core/src/luan/modules/Which_mod.luan	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,53 +0,0 @@
-local Luan = require "luan:Luan.luan"
-local error = Luan.error
-local ipairs = Luan.ipairs or error()
-local pairs = Luan.pairs or error()
-local type = Luan.type or error()
-local String = require "luan:String.luan"
-local literal = String.literal or error()
-local matches = String.matches or error()
-local Io = require "luan:Io.luan"
-local print = Io.print or error()
-
-
-local M = {}
-
-M.uris = {
-	"luan:Luan"
-	"luan:Binary"
-	"luan:Html"
-	"luan:Io"
-	"luan:Math"
-	"luan:Package"
-	"luan:String"
-	"luan:Table"
-	"luan:Thread"
-	"luan:Time"
-	"luan:host/Hosting"
-	"luan:http/Http"
-	"luan:http/Server"
-	"luan:lucene/Lucene"
-	"luan:lucene/Versioning"
-	"luan:mail/Mail"
-	"luan:logging/Logging"
-	"luan:stripe/Stripe"
-}
-
-function M.which(name)
-	local ptn = "[:./]"..literal(name).."$"
-	for _, uri in ipairs(M.uris) do
-		local mod = require(uri)
-		if matches(uri,ptn) then
-			print(uri)
-		end
-		if type(mod) == "table" then
-			for key in pairs(mod) do
-				if key == name then
-					print(uri.." "..key)
-				end
-			end
-		end
-	end
-end
-
-return M
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/host/Hosting.luan
--- a/core/src/luan/modules/host/Hosting.luan	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,97 +0,0 @@
--- Hosting
-
-local Luan = require "luan:Luan.luan"
-local error = Luan.error
-local ipairs = Luan.ipairs or error()
-local pairs = Luan.pairs or error()
-local set_metatable = Luan.set_metatable or error()
-local Io = require "luan:Io.luan"
-local print = Io.print or error()
-local Rpc = require "luan:Rpc.luan"
-local String = require "luan:String.luan"
-local matches = String.matches or error()
-
-
-local M = {}
-
-
-function M.push(domain,password,dir)
-	local my_dir = Io.uri("file:"..dir)
-	my_dir.exists() or error("directory '"..dir.."' not found")
-	my_dir.is_directory() or error("'"..dir.."' is not a directory")
-	local host = Rpc.remote(domain)
-	local tree = host.get(domain,password)
-	if tree == nil then
-		print("creating "..domain)
-		tree = host.create(domain,password)
-	end
-
-	local function process(there_parent,there,here)
-		if here.is_file() then
-			if there == nil or there.last_modified < here.last_modified() then
-				print("copying "..here.to_string())
-				host.copy_file(domain,password,there_parent.path,here.name(),here.read_binary())
-			end
-		elseif here.is_directory() then
-			if here.name() == "local" then
-				return
-			end
-			if there == nil then
-				there = host.mkdir(domain,password,there_parent.path,here.name())
-			end
-			for _, here_child in ipairs(here.children()) do
-				local name = here_child.name()
-				if not matches(name,[[^\.]]) then
-					process(there,there.children[name],here_child)
-					there.children[name] = nil
-				end
-			end
-			for _, there_child in pairs(there.children) do
-				if host.delete_unused(domain,password,there_child.path)==true then   -- remove ==true later
-					print("deleted "..there_child.name)
-				end
-			end
-		else
-			error "not file or dir"
-		end
-	end
-
-	process( nil, tree, my_dir )
-
-	host.update_handler(domain,password)
-end
-
-function M.delete(domain,password)
-	local host = Rpc.remote(domain)
-	host.delete(domain,password)
-end
-
-function M.exists(domain)
-	local host = Rpc.remote(domain)
-	return host.exists(domain)
-end
-
-function M.change_domain(old_domain,new_domain,password)
-	local host = Rpc.remote(new_domain)
-	return host.change_domain(old_domain,new_domain,password)
-end
-
-function M.change_password(domain,old_password,new_password)
-	local host = Rpc.remote(domain)
-	return host.change_password(domain,old_password,new_password)
-end
-
-function M.caller(domain)
-	local host = Rpc.remote(domain)
-	local mt = {}
-	function mt.__index(_,key)
-		return function(...)
-			return host.call(domain,key,...)
-		end
-	end
-	local t = {}
-	set_metatable(t,mt)
-	return t
-end
-
-return M
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/host/backup.luan
--- a/core/src/luan/modules/host/backup.luan	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,18 +0,0 @@
-local Luan = require "luan:Luan.luan"
-local error = Luan.error
-local Io = require "luan:Io.luan"
-local print = Io.print or error()
-local uri = Io.uri or error()
-local Hosting = require "luan:host/Hosting.luan"
-
-if #{...} ~= 2 then
-	Io.stderr.write "usage: luan luan:host/backup.luan domain password\n"
-	return
-end
-
-local domain, password = ...
-
-local zip = Hosting.caller(domain).lucene_backup(password)
-uri("file:backup.zip").write(zip)
-
-print("backed up lucene from "..domain.." to backup.zip")
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/host/delete.luan
--- a/core/src/luan/modules/host/delete.luan	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,12 +0,0 @@
-local Io = require "luan:Io.luan"
-local print = Io.print
-local Hosting = require "luan:host/Hosting.luan"
-
-if #{...} ~= 2 then
-	Io.stderr.write "usage: luan luan:host/delete.luan domain password\n"
-	return
-end
-
-Hosting.delete(...)
-
-print("deleted "..(...))
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/host/push.luan
--- a/core/src/luan/modules/host/push.luan	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,12 +0,0 @@
-local Io = require "luan:Io.luan"
-local print = Io.print
-local Hosting = require "luan:host/Hosting.luan"
-
-if #{...} ~= 3 then
-	Io.stderr.write "usage: luan luan:host/push.luan domain password dir\n"
-	return
-end
-
-Hosting.push(...)
-
-print("done with "..(...))
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/host/restore.luan
--- a/core/src/luan/modules/host/restore.luan	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,19 +0,0 @@
-local Luan = require "luan:Luan.luan"
-local error = Luan.error
-local Io = require "luan:Io.luan"
-local print = Io.print or error()
-local uri = Io.uri or error()
-local Hosting = require "luan:host/Hosting.luan"
-
-if #{...} ~= 2 then
-	Io.stderr.write "usage: luan luan:host/restore.luan domain password\n"
-	return
-end
-
-local domain, password = ...
-
-local zip_file = uri("file:backup.zip")
-zip_file.exists() or error "backup.zip not found"
-Hosting.caller(domain).lucene_restore(password,zip_file)
-
-print("restored lucene from backup.zip to "..domain)
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/luan_to_java.luan
--- a/core/src/luan/modules/luan_to_java.luan	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,5 +0,0 @@
-java()
-local LuanCompiler = require "java:luan.impl.LuanCompiler"
-local Io = require "luan:Io.luan"
-
-Io.stdout.write( LuanCompiler.toJava( Io.stdin.read_text(), "stdin" ) )
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/mmake.luan
--- a/core/src/luan/modules/mmake.luan	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,59 +0,0 @@
-local Luan = require "luan:Luan.luan"
-local ipairs = Luan.ipairs
-local Table = require "luan:Table.luan"
-local Io = require "luan:Io.luan"
-local print = Io.print
-local String = require "luan:String.luan"
-local Time = require "luan:Time.luan"
-
-
-local compiler = Table.concat( { "javac -g -encoding UTF8", ... }, " " )
-
-
-local function header()
-	return 	%>
-# Makefile created on <%=Time.format(Time.now())%> by Mmake
-
-.SUFFIXES: .java .class
-
-.java.class:
-	<%=compiler%> '$<'
-
-all: <%
-end
-
-
-local function mmake(dir)
-	local javas = {}
-	local dirs = {}
-	for _, file in ipairs(dir.children()) do
-		local name = file.name()
-		if String.matches(name,[[\.java$]]) then
-			javas[#javas+1] = String.sub(name,1,-6)
-		end
-		if file.is_directory() and mmake(file) then
-			dirs[#dirs+1] = name
-		end
-	end
-	if #javas == 0 and #dirs == 0 then
-		return false;
-	end
-	local out = dir.child("Makefile").text_writer()
-	out.write( header() )
-	for _, s in ipairs(javas) do
-		s = String.gsub(s,[[\$]],[[\$\$]])
-		out.write( "\\\n\t\t",  s , ".class" )
-	end
-	for _, s in ipairs(dirs) do
-		out.write( "\n\tcd ", s, ";  make all" )
-	end
-	out.write "\n\nclean:\n\trm -f *.class\n"
-	for _, s in ipairs(dirs) do
-		out.write( "\tcd ", s, ";  make clean\n" )
-	end
-	out.close()
-	print(dir.to_string())
-	return true
-end
-
-mmake(Io.schemes.file ".")
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/parsers/BBCode.java
--- a/core/src/luan/modules/parsers/BBCode.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,311 +0,0 @@
-package luan.modules.parsers;
-
-import java.util.List;
-import java.util.ArrayList;
-import luan.Luan;
-import luan.LuanState;
-import luan.LuanFunction;
-import luan.LuanException;
-import luan.modules.Utils;
-
-
-public final class BBCode {
-
-	public static String toHtml(LuanState luan,String bbcode,LuanFunction quoter) throws LuanException {
-		return new BBCode(luan,bbcode,quoter,true).parse();
-	}
-
-	public static String toText(LuanState luan,String bbcode,LuanFunction quoter) throws LuanException {
-		return new BBCode(luan,bbcode,quoter,false).parse();
-	}
-
-	private final LuanState luan;
-	private final Parser parser;
-	private final LuanFunction quoter;
-	private final boolean toHtml;
-
-	private BBCode(LuanState luan,String text,LuanFunction quoter,boolean toHtml) throws LuanException {
-		Utils.checkNotNull(text,1);
-		Utils.checkNotNull(quoter,2);
-		this.luan = luan;
-		this.parser = new Parser(text);
-		this.quoter = quoter;
-		this.toHtml = toHtml;
-	}
-
-	private String parse() throws LuanException {
-		StringBuilder sb = new StringBuilder();
-		while( !parser.endOfInput() ) {
-			String block = parseBlock();
-			if( block != null )
-				sb.append(block);
-			else {
-				sb.append( parser.currentChar() );
-				parser.anyChar();
-			}
-		}
-		return sb.toString();
-	}
-
-	private String parseWellFormed() throws LuanException {
-		StringBuilder sb = new StringBuilder();
-		while( !parser.endOfInput() ) {
-			String block = parseBlock();
-			if( block != null ) {
-				sb.append(block);
-				continue;
-			}
-			if( couldBeTag() )
-				break;
-			sb.append( parser.currentChar() );
-			parser.anyChar();
-		}
-		return sb.toString();
-	}
-
-	private boolean couldBeTag() {
-		if( parser.currentChar() != '[' )
-			return false;
-		return parser.testIgnoreCase("[b]")
-			|| parser.testIgnoreCase("[/b]")
-			|| parser.testIgnoreCase("[i]")
-			|| parser.testIgnoreCase("[/i]")
-			|| parser.testIgnoreCase("[u]")
-			|| parser.testIgnoreCase("[/u]")
-			|| parser.testIgnoreCase("[url]")
-			|| parser.testIgnoreCase("[url=")
-			|| parser.testIgnoreCase("[/url]")
-			|| parser.testIgnoreCase("[code]")
-			|| parser.testIgnoreCase("[/code]")
-			|| parser.testIgnoreCase("[img]")
-			|| parser.testIgnoreCase("[/img]")
-			|| parser.testIgnoreCase("[color=")
-			|| parser.testIgnoreCase("[/color]")
-			|| parser.testIgnoreCase("[size=")
-			|| parser.testIgnoreCase("[/size]")
-			|| parser.testIgnoreCase("[youtube]")
-			|| parser.testIgnoreCase("[/youtube]")
-			|| parser.testIgnoreCase("[quote]")
-			|| parser.testIgnoreCase("[quote=")
-			|| parser.testIgnoreCase("[/quote]")
-		;
-	}
-
-	private String parseBlock() throws LuanException {
-		if( parser.currentChar() != '[' )
-			return null;
-		String s;
-		s = parseB();  if(s!=null) return s;
-		s = parseI();  if(s!=null) return s;
-		s = parseU();  if(s!=null) return s;
-		s = parseUrl1();  if(s!=null) return s;
-		s = parseUrl2();  if(s!=null) return s;
-		s = parseCode();  if(s!=null) return s;
-		s = parseImg();  if(s!=null) return s;
-		s = parseColor();  if(s!=null) return s;
-		s = parseSize();  if(s!=null) return s;
-		s = parseYouTube();  if(s!=null) return s;
-		s = parseQuote1();  if(s!=null) return s;
-		s = parseQuote2();  if(s!=null) return s;
-		return null;
-	}
-
-	private String parseB() throws LuanException {
-		parser.begin();
-		if( !parser.matchIgnoreCase("[b]") )
-			return parser.failure(null);
-		String content = parseWellFormed();
-		if( !parser.matchIgnoreCase("[/b]") )
-			return parser.failure(null);
-		String rtn = toHtml ? "<b>"+content+"</b>" : content;
-		return parser.success(rtn);
-	}
-
-	private String parseI() throws LuanException {
-		parser.begin();
-		if( !parser.matchIgnoreCase("[i]") )
-			return parser.failure(null);
-		String content = parseWellFormed();
-		if( !parser.matchIgnoreCase("[/i]") )
-			return parser.failure(null);
-		String rtn = toHtml ? "<i>"+content+"</i>" : content;
-		return parser.success(rtn);
-	}
-
-	private String parseU() throws LuanException {
-		parser.begin();
-		if( !parser.matchIgnoreCase("[u]") )
-			return parser.failure(null);
-		String content = parseWellFormed();
-		if( !parser.matchIgnoreCase("[/u]") )
-			return parser.failure(null);
-		String rtn = toHtml ? "<u>"+content+"</u>" : content;
-		return parser.success(rtn);
-	}
-
-	private String parseUrl1() {
-		parser.begin();
-		if( !parser.matchIgnoreCase("[url]") )
-			return parser.failure(null);
-		String url = parseRealUrl();
-		if( !parser.matchIgnoreCase("[/url]") )
-			return parser.failure(null);
-		String rtn = toHtml ? "<a href='"+url+"'>"+url+"</u>" : url;
-		return parser.success(rtn);
-	}
-
-	private String parseUrl2() throws LuanException {
-		parser.begin();
-		if( !parser.matchIgnoreCase("[url=") )
-			return parser.failure(null);
-		String url = parseRealUrl();
-		if( !parser.match(']') )
-			return parser.failure(null);
-		String content = parseWellFormed();
-		if( !parser.matchIgnoreCase("[/url]") )
-			return parser.failure(null);
-		String rtn = toHtml ? "<a href='"+url+"'>"+content+"</u>" : content;
-		return parser.success(rtn);
-	}
-
-	private String parseRealUrl() {
-		parser.begin();
-		while( parser.match(' ') );
-		int start = parser.currentIndex();
-		if( !parser.matchIgnoreCase("http") )
-			return parser.failure(null);
-		parser.matchIgnoreCase("s");
-		if( !parser.matchIgnoreCase("://") )
-			return parser.failure(null);
-		while( parser.noneOf(" []'") );
-		String url = parser.textFrom(start);
-		while( parser.match(' ') );
-		return parser.success(url);
-	}
-
-	private String parseCode() {
-		parser.begin();
-		if( !parser.matchIgnoreCase("[code]") )
-			return parser.failure(null);
-		int start = parser.currentIndex();
-		while( !parser.testIgnoreCase("[/code]") ) {
-			if( !parser.anyChar() )
-				return parser.failure(null);
-		}
-		String content = parser.textFrom(start);
-		if( !parser.matchIgnoreCase("[/code]") ) throw new RuntimeException();
-		String rtn = toHtml ? "<code>"+content+"</code>" : content;
-		return parser.success(rtn);
-	}
-
-	private String parseImg() {
-		parser.begin();
-		if( !parser.matchIgnoreCase("[img]") )
-			return parser.failure(null);
-		String url = parseRealUrl();
-		if( !parser.matchIgnoreCase("[/img]") )
-			return parser.failure(null);
-		String rtn = toHtml ? "<img src='"+url+"'>" : "";
-		return parser.success(rtn);
-	}
-
-	private String parseColor() throws LuanException {
-		parser.begin();
-		if( !parser.matchIgnoreCase("[color=") )
-			return parser.failure(null);
-		int start = parser.currentIndex();
-		parser.match('#');
-		while( parser.inCharRange('0','9')
-			|| parser.inCharRange('a','z')
-			|| parser.inCharRange('A','Z')
-		);
-		String color = parser.textFrom(start);
-		if( !parser.match(']') )
-			return parser.failure(null);
-		String content = parseWellFormed();
-		if( !parser.matchIgnoreCase("[/color]") )
-			return parser.failure(null);
-		String rtn = toHtml ? "<span style='color: "+color+"'>"+content+"</span>" : content;
-		return parser.success(rtn);
-	}
-
-	private String parseSize() throws LuanException {
-		parser.begin();
-		if( !parser.matchIgnoreCase("[size=") )
-			return parser.failure(null);
-		int start = parser.currentIndex();
-		while( parser.match('.') || parser.inCharRange('0','9') );
-		String size = parser.textFrom(start);
-		if( !parser.match(']') )
-			return parser.failure(null);
-		String content = parseWellFormed();
-		if( !parser.matchIgnoreCase("[/size]") )
-			return parser.failure(null);
-		String rtn = toHtml ? "<span style='font-size: "+size+"em'>"+content+"</span>" : content;
-		return parser.success(rtn);
-	}
-
-	private String parseYouTube() {
-		parser.begin();
-		if( !parser.matchIgnoreCase("[youtube]") )
-			return parser.failure(null);
-		int start = parser.currentIndex();
-		while( parser.inCharRange('0','9')
-			|| parser.inCharRange('a','z')
-			|| parser.inCharRange('A','Z')
-			|| parser.match('-')
-			|| parser.match('_')
-		);
-		String id = parser.textFrom(start);
-		if( id.length()==0 || !parser.matchIgnoreCase("[/youtube]") )
-			return parser.failure(null);
-		String rtn = toHtml ? "<iframe width='420' height='315' src='https://www.youtube.com/embed/"+id+"' frameborder='0' allowfullscreen></iframe>" : "";
-		return parser.success(rtn);
-	}
-
-	private String quote(Object... args) throws LuanException {
-		Object obj = quoter.call(luan,args);
-		if( !(obj instanceof String) )
-			throw new LuanException("BBCode quoter function returned "+Luan.type(obj)+" but string required");
-		return (String)obj;
-	}
-
-	private String parseQuote1() throws LuanException {
-		parser.begin();
-		if( !parser.matchIgnoreCase("[quote]") )
-			return parser.failure(null);
-		String content = parseWellFormed();
-		if( !parser.matchIgnoreCase("[/quote]") )
-			return parser.failure(null);
-		String rtn = quote(content);
-		return parser.success(rtn);
-	}
-
-	private String parseQuote2() throws LuanException {
-		parser.begin();
-		if( !parser.matchIgnoreCase("[quote=") )
-			return parser.failure(null);
-		List args = new ArrayList();
-		int start = parser.currentIndex();
-		while( parser.noneOf("[];") );
-		String name = parser.textFrom(start).trim();
-		if( name.length() == 0 )
-			return parser.failure(null);
-		args.add(name);
-		while( parser.match(';') ) {
-			start = parser.currentIndex();
-			while( parser.noneOf("[];'") );
-			String src = parser.textFrom(start).trim();
-			args.add(src);
-		}
-		if( !parser.match(']') )
-			return parser.failure(null);
-		String content = parseWellFormed();
-		args.add(0,content);
-		if( !parser.matchIgnoreCase("[/quote]") )
-			return parser.failure(null);
-		String rtn = quote(args.toArray());
-		return parser.success(rtn);
-	}
-
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/parsers/Csv.java
--- a/core/src/luan/modules/parsers/Csv.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,61 +0,0 @@
-package luan.modules.parsers;
-
-import luan.LuanTable;
-
-
-public final class Csv {
-
-	public static LuanTable toList(String line) throws ParseException {
-		return new Csv(line).parse();
-	}
-
-	private final Parser parser;
-
-	private Csv(String line) {
-		this.parser = new Parser(line);
-	}
-
-	private ParseException exception(String msg) {
-		return new ParseException(parser,msg);
-	}
-
-	private LuanTable parse() throws ParseException {
-		LuanTable list = new LuanTable();
-		while(true) {
-			Spaces();
-			String field = parseField();
-			list.rawPut(list.rawLength()+1,field);
-			Spaces();
-			if( parser.endOfInput() )
-				return list;
-			if( !parser.match(',') )
-				throw exception("unexpected char");
-		}
-	}
-
-	private String parseField() throws ParseException {
-		parser.begin();
-		String rtn;
-		if( parser.match('"') ) {
-			int start = parser.currentIndex();
-			do {
-				if( parser.endOfInput() ) {
-					parser.failure();
-					throw exception("unclosed quote");
-				}
-			} while( parser.noneOf("\"") );
-			rtn = parser.textFrom(start);
-			parser.match('"');
-		} else {
-			int start = parser.currentIndex();
-			while( !parser.endOfInput() && parser.noneOf(",") );
-			rtn = parser.textFrom(start).trim();
-		}
-		return parser.success(rtn);
-	}
-
-	private void Spaces() {
-		while( parser.anyOf(" \t") );
-	}
-
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/parsers/Html.java
--- a/core/src/luan/modules/parsers/Html.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,197 +0,0 @@
-package luan.modules.parsers;
-
-import java.util.List;
-import java.util.ArrayList;
-import java.util.Set;
-import java.util.HashSet;
-import luan.LuanTable;
-
-
-public final class Html {
-
-	public static LuanTable toList(String text,LuanTable containerTagsTbl) throws ParseException {
-		return new Html(text,containerTagsTbl).parse();
-	}
-
-	private final Parser parser;
-	private final Set<String> containerTags = new HashSet<String>();
-
-	private Html(String text,LuanTable containerTagsTbl) {
-		this.parser = new Parser(text);
-		for( Object v : containerTagsTbl.asList() ) {
-			containerTags.add((String)v);
-		}
-	}
-
-	private LuanTable parse() throws ParseException {
-		List list = new ArrayList();
-		StringBuilder sb = new StringBuilder();
-		while( !parser.endOfInput() ) {
-			if( parser.test('<') ) {
-				LuanTable tbl = parseTag();
-				if( tbl != null ) {
-					String tagName = (String)tbl.rawGet("name");
-					if( containerTags.contains(tagName) ) {
-						LuanTable container = parseContainer(tbl);
-						if( container != null )
-							tbl = container;
-					}
-					if( tbl != null 
-						|| (tbl = parseComment()) != null
-						|| (tbl = parseCdata()) != null
-					) {
-						if( sb.length() > 0 ) {
-							list.add(sb.toString());
-							sb.setLength(0);
-						}
-						list.add(tbl);
-						continue;
-					}
-				}
-			}
-			sb.append( parser.currentChar() );
-			parser.anyChar();
-		}
-		if( sb.length() > 0 )
-			list.add(sb.toString());
-		return new LuanTable(list);
-	}
-
-	private LuanTable parseComment() {
-		parser.begin();
-		if( !parser.match("<!--") )
-			return parser.failure(null);
-		int start = parser.currentIndex();
-		while( !parser.test("-->") ) {
-			if( !parser.anyChar() )
-				return parser.failure(null);
-		}
-		String text = parser.textFrom(start);
-		LuanTable tbl = new LuanTable();
-		tbl.rawPut("type","comment");
-		tbl.rawPut("text",text);
-		return parser.success(tbl);
-	}
-
-	private LuanTable parseCdata() {
-		parser.begin();
-		if( !parser.match("<![CDATA[") )
-			return parser.failure(null);
-		int start = parser.currentIndex();
-		while( !parser.test("]]>") ) {
-			if( !parser.anyChar() )
-				return parser.failure(null);
-		}
-		String text = parser.textFrom(start);
-		LuanTable tbl = new LuanTable();
-		tbl.rawPut("type","cdata");
-		tbl.rawPut("text",text);
-		return parser.success(tbl);
-	}
-
-	private LuanTable parseContainer(LuanTable tag) {
-		String endTagName = '/' + (String)tag.rawGet("name");
-		int start = parser.begin();
-		int end;
-		while(true) {
-			if( parser.test('<') ) {
-				end = parser.currentIndex();
-				LuanTable tag2 = parseTag();
-				String s = (String)tag2.rawGet("name");
-				if( s.equals(endTagName) )
-					break;
-			}
-			if( !parser.anyChar() )
-				return parser.failure(null);
-		}
-		String text = parser.text.substring(start,end);
-		LuanTable tbl = new LuanTable();
-		tbl.rawPut("type","container");
-		tbl.rawPut("tag",tag);
-		tbl.rawPut("text",text);
-		return parser.success(tbl);
-	}
-
-	private LuanTable parseTag() {
-		parser.begin();
-		if( !parser.match('<') )
-			return parser.failure(null);
-		int start = parser.currentIndex();
-		parser.match('/');
-		if( !matchNameChar() )
-			return parser.failure(null);
-		while( matchNameChar() );
-		String name = parser.textFrom(start).toLowerCase();
-		LuanTable attributes = new LuanTable();
-		String attrName;
-		while( (attrName = parseAttrName()) != null ) {
-			String attrValue = parseAttrValue();
-			attributes.rawPut( attrName, attrValue!=null ? attrValue : true );
-		}
-		while( matchSpace() );
-		boolean isEmpty = parser.match('/');
-		if( !parser.match('>') )
-			return parser.failure(null);
-		LuanTable tbl = new LuanTable();
-		tbl.rawPut("type","tag");
-		tbl.rawPut("name",name);
-		tbl.rawPut("attributes",attributes);
-		tbl.rawPut("is_empty",isEmpty);
-		return parser.success(tbl);
-	}
-
-	private String parseAttrName() {
-		parser.begin();
-		if( !matchSpace() )
-			return parser.failure(null);
-		while( matchSpace() );
-		int start = parser.currentIndex();
-		if( !matchNameChar() )
-			return parser.failure(null);
-		while( matchNameChar() );
-		String name = parser.textFrom(start);
-		return parser.success(name);
-	}
-
-	private String parseAttrValue() {
-		parser.begin();
-		while( matchSpace() );
-		if( !parser.match('=') )
-			return parser.failure(null);
-		while( matchSpace() );
-		if( parser.anyOf("\"'") ) {
-			char quote = parser.lastChar();
-			int start = parser.currentIndex();
-			while( !parser.test(quote) ) {
-				if( !parser.anyChar() )
-					return parser.failure(null);
-			}
-			String value = parser.textFrom(start);
-			parser.match(quote);
-			return parser.success(value);
-		}
-		int start = parser.currentIndex();
-		if( !matchValueChar() )
-			return parser.failure(null);
-		while( matchValueChar() );
-		String value = parser.textFrom(start);
-		return parser.success(value);
-	}
-
-	private boolean matchNameChar() {
-		return parser.inCharRange('a','z')
-			|| parser.inCharRange('A','Z')
-			|| parser.inCharRange('0','9')
-			|| parser.anyOf("_.-:")
-		;
-	}
-
-	private boolean matchValueChar() {
-		return parser.noneOf(" \t\r\n\"'>/=");
-	}
-
-	private boolean matchSpace() {
-		return parser.anyOf(" \t\r\n");
-	}
-
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/parsers/Json.java
--- a/core/src/luan/modules/parsers/Json.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,313 +0,0 @@
-package luan.modules.parsers;
-
-import java.util.List;
-import java.util.ArrayList;
-import java.util.Map;
-import java.util.LinkedHashMap;
-import java.util.Iterator;
-import luan.LuanTable;
-import luan.LuanException;
-
-
-public final class Json {
-
-	public static Object parse(String text) throws ParseException {
-		return new Json(text).parse();
-	}
-
-	private final Parser parser;
-
-	private Json(String text) {
-		this.parser = new Parser(text);
-	}
-
-	private ParseException exception(String msg) {
-		return new ParseException(parser,msg);
-	}
-
-	private Object parse() throws ParseException {
-		spaces();
-		Object value = value();
-		spaces();
-		if( !parser.endOfInput() )
-			throw exception("unexpected text");
-		return value;
-	}
-
-	private Object value() throws ParseException {
-		if( parser.match("null") )
-			return null;
-		if( parser.match("true") )
-			return Boolean.TRUE;
-		if( parser.match("false") )
-			return Boolean.FALSE;
-		String s = string();
-		if( s != null )
-			return s;
-		Number n = number();
-		if( n != null )
-			return n;
-		LuanTable a = array();
-		if( a != null )
-			return a;
-		LuanTable o = object();
-		if( o != null )
-			return o;
-		throw exception("invalid value");
-	}
-
-	private String string() throws ParseException {
-		parser.begin();
-		if( !parser.match('"') )
-			return parser.failure(null);
-		StringBuilder sb = new StringBuilder();
-		while( parser.anyChar() ) {
-			char c = parser.lastChar();
-			switch(c) {
-			case '"':
-				return parser.success(sb.toString());
-			case '\\':
-				if( parser.anyChar() ) {
-					c = parser.lastChar();
-					switch(c) {
-					case '"':
-					case '\\':
-					case '/':
-						sb.append(c);
-						continue;
-					case 'b':
-						sb.append('\b');
-						continue;
-					case 'f':
-						sb.append('\f');
-						continue;
-					case 'n':
-						sb.append('\n');
-						continue;
-					case 'r':
-						sb.append('\r');
-						continue;
-					case 't':
-						sb.append('\t');
-						continue;
-					case 'u':
-						int n = 0;
-						for( int i=0; i<4; i++ ) {
-							int d;
-							if( parser.inCharRange('0','9') ) {
-								d = parser.lastChar() - '0';
-							} else if( parser.inCharRange('a','f') ) {
-								d = parser.lastChar() - 'a' + 10;
-							} else if( parser.inCharRange('A','F') ) {
-								d = parser.lastChar() - 'A' + 10;
-							} else {
-								throw exception("invalid hex digit");
-							}
-							n = 16*n + d;
-						}
-						sb.append((char)n);
-						continue;
-					}
-				}
-				throw exception("invalid escape char");
-			default:
-				sb.append(c);
-			}
-		}
-		parser.failure();
-		throw exception("unclosed string");
-	}
-
-	private Number number() {
-		int start = parser.begin();
-		boolean isFloat = false;
-		parser.match('-');
-		if( !parser.match('0') ) {
-			if( !parser.inCharRange('1','9') )
-				return parser.failure(null);
-			while( parser.inCharRange('0','9') );
-		}
-		if( parser.match('.') ) {
-			if( !parser.inCharRange('0','9') )
-				return parser.failure(null);
-			while( parser.inCharRange('0','9') );
-			isFloat = true;
-		}
-		if( parser.anyOf("eE") ) {
-			parser.anyOf("+-");
-			if( !parser.inCharRange('0','9') )
-				return parser.failure(null);
-			while( parser.inCharRange('0','9') );
-			isFloat = true;
-		}
-		String s = parser.textFrom(start);
-		Number n;
-		if(isFloat)
-			n = Double.valueOf(s);
-		else
-			n = Long.valueOf(s);
-		return parser.success(n);
-	}
-
-	private LuanTable array() throws ParseException {
-		parser.begin();
-		if( !parser.match('[') )
-			return parser.failure(null);
-		spaces();
-		if( parser.match(']') )
-			return parser.success(new LuanTable());
-		List list = new ArrayList();
-		list.add( value() );
-		spaces();
-		while( parser.match(',') ) {
-			spaces();
-			list.add( value() );
-			spaces();
-		}
-		if( parser.match(']') )
-			return parser.success(new LuanTable(list));
-		if( parser.endOfInput() ) {
-			parser.failure();
-			throw exception("unclosed array");
-		}
-		throw exception("unexpected text in array");
-	}
-
-	private LuanTable object() throws ParseException {
-		parser.begin();
-		if( !parser.match('{') )
-			return parser.failure(null);
-		spaces();
-		if( parser.match('}') )
-			return parser.success(new LuanTable());
-		Map map = new LinkedHashMap();
-		addEntry(map);
-		while( parser.match(',') ) {
-			spaces();
-			addEntry(map);
-		}
-		if( parser.match('}') )
-			return parser.success(new LuanTable(map));
-		if( parser.endOfInput() ) {
-			parser.failure();
-			throw exception("unclosed object");
-		}
-		throw exception("unexpected text in object");
-	}
-
-	private void addEntry(Map map) throws ParseException {
-		String key = string();
-		if( key==null )
-			throw exception("invalid object key");
-		spaces();
-		if( !parser.match(':') )
-			throw exception("':' expected");
-		spaces();
-		Object value = value();
-		spaces();
-		map.put(key,value);
-	}
-
-	private void spaces() {
-		while( parser.anyOf(" \t\r\n") );
-	}
-
-
-
-
-
-
-
-
-
-	public static String toString(Object obj) throws LuanException {
-		StringBuilder sb = new StringBuilder();
-		toString(obj,sb);
-		return sb.toString();
-	}
-
-	private static void toString(Object obj,StringBuilder sb) throws LuanException {
-		if( obj == null || obj instanceof Boolean || obj instanceof Number ) {
-			sb.append(obj);
-			return;
-		}
-		if( obj instanceof String ) {
-			toString((String)obj,sb);
-			return;
-		}
-		if( obj instanceof LuanTable ) {
-			toString((LuanTable)obj,sb);
-			return;
-		}
-		throw new LuanException("can't handle type "+obj.getClass().getName());
-	}
-
-	private static void toString(final String s,StringBuilder sb) {
-		sb.append('"');
-		for( int i=0; i<s.length(); i++ ) {
-			char c = s.charAt(i);
-			switch(c) {
-			case '"':
-				sb.append("\\\"");
-				break;
-			case '\\':
-				sb.append("\\\\");
-				break;
-			case '\b':
-				sb.append("\\b");
-				break;
-			case '\f':
-				sb.append("\\f");
-				break;
-			case '\n':
-				sb.append("\\n");
-				break;
-			case '\r':
-				sb.append("\\r");
-				break;
-			case '\t':
-				sb.append("\\t");
-				break;
-			default:
-				sb.append(c);
-			}
-		}
-		sb.append('"');
-	}
-
-	private static void toString(LuanTable t,StringBuilder sb) throws LuanException {
-		if( t.isList() ) {
-			final List list = t.asList();
-			if( list.isEmpty() ) {
-				sb.append("{}");
-				return;
-			}
-			sb.append('[');
-			toString(list.get(0),sb);
-			for( int i=1; i<list.size(); i++ ) {
-				sb.append(',');
-				toString(list.get(i),sb);
-			}
-			sb.append(']');
-			return;
-		}
-		sb.append('{');
-		Iterator<Map.Entry<Object,Object>> i = t.rawIterator();
-		toString(i.next(),sb);
-		while( i.hasNext() ) {
-			sb.append(',');
-			toString(i.next(),sb);
-		}
-		sb.append('}');
-	}
-
-	private static void toString(Map.Entry<Object,Object> entry,StringBuilder sb) throws LuanException {
-		Object key = entry.getKey();
-		if( !(key instanceof String) )
-			throw new LuanException("table keys must be strings");
-		toString((String)key,sb);
-		sb.append(':');
-		toString(entry.getValue(),sb);
-	}
-
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/parsers/ParseException.java
--- a/core/src/luan/modules/parsers/ParseException.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,51 +0,0 @@
-package luan.modules.parsers;
-
-
-public final class ParseException extends Exception {
-	public final String text;
-	public final int errorIndex;
-	public final int highIndex;
-
-	ParseException(Parser parser,String msg) {
-		super(msg);
-		this.text = parser.text;
-		this.errorIndex = parser.currentIndex();
-		this.highIndex = parser.highIndex();
-	}
-
-	private class Location {
-		final int line;
-		final int pos;
-
-		Location(int index) {
-			int line = 0;
-			int i = -1;
-			while(true) {
-				int j = text.indexOf('\n',i+1);
-				if( j == -1 || j >= index )
-					break;
-				i = j;
-				line++;
-			}
-			this.line = line;
-			this.pos = index - i - 1;
-		}
-	}
-
-	private String[] lines() {
-		return text.split("\n",-1);
-	}
-
-	@Override public String getMessage() {
-		Location loc = new Location(errorIndex);
-		String line = lines()[loc.line];
-		String msg = super.getMessage() +  " (line " + (loc.line+1) + ", pos " + (loc.pos+1) + ")\n";
-		StringBuilder sb = new StringBuilder(msg);
-		sb.append( line + "\n" );
-		for( int i=0; i<loc.pos; i++ ) {
-			sb.append( line.charAt(i)=='\t' ? '\t' : ' ' );
-		}
-		sb.append("^\n");
-		return sb.toString();
-	}
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/parsers/Parser.java
--- a/core/src/luan/modules/parsers/Parser.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,156 +0,0 @@
-package luan.modules.parsers;
-
-
-public class Parser {
-	public final String text;
-	private final int len;
-	private int[] stack = new int[256];
-	private int frame = 0;
-	private int iHigh;
-
-	Parser(String text) {
-		this.text = text;
-		this.len = text.length();
-	}
-
-	private int i() {
-		return stack[frame];
-	}
-
-	private void i(int i) {
-		stack[frame] += i;
-		if( iHigh < stack[frame] )
-			iHigh = stack[frame];
-	}
-
-	public int begin() {
-		frame++;
-		if( frame == stack.length ) {
-			int[] a = new int[2*frame];
-			System.arraycopy(stack,0,a,0,frame);
-			stack = a;
-		}
-		stack[frame] = stack[frame-1];
-		return i();
-	}
-
-	public void rollback() {
-		stack[frame] = stack[frame-1];
-	}
-
-	public <T> T success(T t) {
-		success();
-		return t;
-	}
-
-	public boolean success() {
-		frame--;
-		stack[frame] = stack[frame+1];
-		return true;
-	}
-
-	public <T> T failure(T t) {
-		failure();
-		return t;
-	}
-
-	public boolean failure() {
-		frame--;
-		return false;
-	}
-
-	public int currentIndex() {
-		return i();
-	}
-/*
-	public int errorIndex() {
-		return frame > 0 ? stack[frame-1] : 0;
-	}
-*/
-	public int highIndex() {
-		return iHigh;
-	}
-
-	public char lastChar() {
-		return text.charAt(i()-1);
-	}
-
-	public char currentChar() {
-		return text.charAt(i());
-	}
-
-	public boolean endOfInput() {
-		return i() >= len;
-	}
-
-	public boolean match(char c) {
-		if( endOfInput() || text.charAt(i()) != c )
-			return false;
-		i(1);
-		return true;
-	}
-
-	public boolean match(String s) {
-		int n = s.length();
-		if( !text.regionMatches(i(),s,0,n) )
-			return false;
-		i(n);
-		return true;
-	}
-
-	public boolean matchIgnoreCase(String s) {
-		int n = s.length();
-		if( !text.regionMatches(true,i(),s,0,n) )
-			return false;
-		i(n);
-		return true;
-	}
-
-	public boolean anyOf(String s) {
-		if( endOfInput() || s.indexOf(text.charAt(i())) == -1 )
-			return false;
-		i(1);
-		return true;
-	}
-
-	public boolean noneOf(String s) {
-		if( endOfInput() || s.indexOf(text.charAt(i())) != -1 )
-			return false;
-		i(1);
-		return true;
-	}
-
-	public boolean inCharRange(char cLow, char cHigh) {
-		if( endOfInput() )
-			return false;
-		char c = text.charAt(i());
-		if( !(cLow <= c && c <= cHigh) )
-			return false;
-		i(1);
-		return true;
-	}
-
-	public boolean anyChar() {
-		if( endOfInput() )
-			return false;
-		i(1);
-		return true;
-	}
-
-	public boolean test(char c) {
-		return !endOfInput() && text.charAt(i()) == c;
-	}
-
-	public boolean test(String s) {
-		return text.regionMatches(i(),s,0,s.length());
-	}
-
-	public boolean testIgnoreCase(String s) {
-		return text.regionMatches(true,i(),s,0,s.length());
-	}
-
-	public String textFrom(int start) {
-		return text.substring(start,i());
-	}
-
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/parsers/Theme.java
--- a/core/src/luan/modules/parsers/Theme.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,202 +0,0 @@
-package luan.modules.parsers;
-
-import luan.LuanException;
-
-
-public final class Theme {
-
-	public static String toLuan(String source) throws LuanException {
-		try {
-			return new Theme(source).parse();
-		} catch(ParseException e) {
-			throw new LuanException(e.getMessage());
-		}
-	}
-
-	private final Parser parser;
-
-	private Theme(String source) {
-		this.parser = new Parser(source);
-	}
-
-	private ParseException exception(String msg) {
-//		parser.failure();
-		return new ParseException(parser,msg);
-	}
-
-	private String parse() throws ParseException {
-		StringBuilder stmts = new StringBuilder();
-		stmts.append( "local M = {};  " );
-		while( !parser.endOfInput() ) {
-			String def = parseDef();
-			if( def != null ) {
-				stmts.append(def);
-			} else {
-//				parser.anyChar();
-				stmts.append(parsePadding());
-			}
-		}
-		stmts.append( "\n\nreturn M\n" );
-		return stmts.toString();
-	}
-
-	private String parsePadding() throws ParseException {
-		int start = parser.currentIndex();
-		if( parser.match("--") ) {
-			while( parser.noneOf("\r\n") );
-		} else if( !parser.anyOf(" \t\r\n") ) {
-			throw exception("unexpected text");
-		}
-		return parser.textFrom(start);
-	}
-
-	private String parseDef() throws ParseException {
-		int start = parser.begin();
-		if( !parser.match('{') )
-			return parser.failure(null);
-		spaces();
-		if( !parser.match("define:") )
-			return parser.failure(null);
-		String name = parseName();
-		if( name==null )
-			throw exception("invalid block name");
-		spaces();
-		if( !parser.match('}') )
-			throw exception("unclosed define tag");
-		String block = parseBody("define:"+name);
-		String rtn = "function M." + name + "(env) " + block + " end;  ";
-		return parser.success(rtn);
-	}
-
-	private String parseBody(String tagName) throws ParseException {
-		StringBuilder stmts = new StringBuilder();
-		int start = parser.currentIndex();
-		int end = start;
-		while( !matchEndTag(tagName) ) {
-			if( parser.endOfInput() ) {
-				parser.failure();
-				throw exception("unclosed block");
-			}
-			String block = parseBlock();
-			if( block != null ) {
-				addText(start,end,stmts);
-				start = parser.currentIndex();
-				stmts.append(block);
-				continue;
-			}
-			String simpleTag = parseSimpleTag();
-			if( simpleTag != null ) {
-				addText(start,end,stmts);
-				start = parser.currentIndex();
-				stmts.append(simpleTag);
-				continue;
-			}
-			if( parser.match("<%") ) {
-				addText(start,end,stmts);
-				start = parser.currentIndex();
-				stmts.append("%><%='<%'%><%");
-				continue;
-			}
-			parser.anyChar();
-			end = parser.currentIndex();
-		}
-		addText(start,end,stmts);
-		return stmts.toString();
-	}
-
-	private boolean matchEndTag(String tagName) {
-		parser.begin();
-		if( !parser.match('{') )
-			return parser.failure();
-		spaces();
-		if( !(parser.match('/') && parser.match(tagName)) )
-			return parser.failure();
-		spaces();
-		if( !parser.match('}') )
-			return parser.failure();
-		return parser.success();
-	}
-
-	private void addText(int start,int end,StringBuilder stmts) {
-		if( start < end ) {
-			stmts.append( "%>" ).append( parser.text.substring(start,end) ).append( "<%" );
-		}
-	}
-
-	private String parseBlock() throws ParseException {
-		int start = parser.begin();
-		if( !parser.match('{') )
-			return parser.failure(null);
-		spaces();
-		if( !parser.match("block:") )
-			return parser.failure(null);
-		String name = parseName();
-		if( name==null ) {
-			parser.failure();
-			throw exception("invalid block name");
-		}
-		spaces();
-		if( !parser.match('}') )
-			return parser.failure(null);
-		String block = parseBody("block:"+name);
-		String rtn = " env."+ name + "( env, function(env) " + block + "end); ";
-//		String rtn = "<% env." + tag.name + "(" + (tag.attrs.isEmpty() ? "nil" : table(tag.attrs)) + ",env,function(env) %>" + block + "<% end) %>";
-		return parser.success(rtn);
-	}
-
-	private String parseSimpleTag() throws ParseException {
-		int start = parser.begin();
-		if( !parser.match('{') )
-			return parser.failure(null);
-		spaces();
-		String name = parseName();
-		if( name==null )
-			return parser.failure(null);
-		spaces();
-		if( !parser.match('}') )
-			return parser.failure(null);
-//		rtn = "<% env." + name + (attrs.isEmpty() ? "()" : table(attrs)) + " %>";
-		String rtn = " env." + name + "(env); ";
-		return parser.success(rtn);
-	}
-
-	private boolean BlankLine() {
-		parser.begin();
-		while( parser.anyOf(" \t") );
-		return EndOfLine() ? parser.success() : parser.failure();
-	}
-
-	private boolean EndOfLine() {
-		return parser.match( "\r\n" ) || parser.match( '\r' ) || parser.match( '\n' );
-	}
-
-	private String parseName() throws ParseException {
-		int start = parser.begin();
-		if( parser.match('/') ) {
-			parser.failure();
-			throw exception("bad closing tag");
-		}
-		if( parser.match("define:") ) {
-			parser.failure();
-			throw exception("unexpected definition");
-		}
-		if( !FirstNameChar() )
-			return parser.failure(null);
-		while( NameChar() );
-		String match = parser.textFrom(start);
-		return parser.success(match);
-	}
-
-	private boolean FirstNameChar() {
-		return parser.inCharRange('a', 'z') || parser.inCharRange('A', 'Z') || parser.match('_');
-	}
-
-	private boolean NameChar() {
-		return FirstNameChar() || parser.inCharRange('0', '9');
-	}
-
-	private void spaces() {
-		while( parser.anyOf(" \t") );
-	}
-
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/theme_to_luan.luan
--- a/core/src/luan/modules/theme_to_luan.luan	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,4 +0,0 @@
-local Io = require "luan:Io.luan"
-local Parsers = require "luan:Parsers.luan"
-
-Io.stdout.write( Parsers.theme_to_luan( Io.stdin.read_text(), "stdin" ) )
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/url/LuanUrl.java
--- a/core/src/luan/modules/url/LuanUrl.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,302 +0,0 @@
-package luan.modules.url;
-
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.io.Reader;
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.net.URL;
-import java.net.URLConnection;
-import java.net.HttpURLConnection;
-import java.net.URLEncoder;
-import java.util.Map;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Base64;
-import luan.Luan;
-import luan.LuanState;
-import luan.LuanTable;
-import luan.LuanJavaFunction;
-import luan.LuanException;
-import luan.modules.IoLuan;
-import luan.modules.Utils;
-
-
-public final class LuanUrl extends IoLuan.LuanIn {
-
-	private static enum Method { GET, POST, DELETE }
-
-	private URL url;
-	private Method method = Method.GET;
-	private Map headers;
-	private String content = null;
-	private MultipartClient multipart = null;
-	private int timeout = 0;
-
-	public LuanUrl(LuanState luan,URL url,LuanTable options) throws LuanException {
-		this.url = url;
-		if( options != null ) {
-			Map map = options.asMap(luan);
-			String methodStr = getString(map,"method");
-			if( methodStr != null ) {
-				methodStr = methodStr.toUpperCase();
-				try {
-					this.method = Method.valueOf(methodStr);
-				} catch(IllegalArgumentException e) {
-					throw new LuanException( "invalid method: "+methodStr );
-				}
-			}
-			Map headerMap = getMap(luan,map,"headers");
-			if( headerMap != null ) {
-				headers = new HashMap();
-				for( Object hack : headerMap.entrySet() ) {
-					Map.Entry entry = (Map.Entry)hack;
-					String key = (String)entry.getKey();
-					Object val = entry.getValue();
-					String name = toHttpHeaderName(key);
-					if( val instanceof String ) {
-						headers.put(name,val);
-					} else {
-						if( !(val instanceof LuanTable) )
-							throw new LuanException( "header '"+key+"' must be string or table" );
-						LuanTable t = (LuanTable)val;
-						if( !t.isList() )
-							throw new LuanException( "header '"+key+"' table must be list" );
-						headers.put(name,t.asList());
-					}
-				}
-			}
-			Map auth = getMap(luan,map,"authorization");
-			if( auth != null ) {
-				if( headers!=null && headers.containsKey("Authorization") )
-					throw new LuanException( "can't define authorization with header 'Authorization' defined" );
-				String user = getString(auth,"user");
-				String password = getString(auth,"password");
-				if( !auth.isEmpty() )
-					throw new LuanException( "unrecognized authorization options: "+auth );
-				StringBuilder sb = new StringBuilder();
-				if( user != null )
-					sb.append(user);
-				sb.append(':');
-				if( password != null )
-					sb.append(password);
-				String val = "Basic " + Base64.getEncoder().encodeToString(sb.toString().getBytes());
-				if( headers == null )
-					headers = new HashMap();
-				headers.put("Authorization",val);
-			}
-			Map params = getMap(luan,map,"parameters");
-			String enctype = getString(map,"enctype");
-			if( enctype != null ) {
-				if( !enctype.equals("multipart/form-data") )
-					throw new LuanException( "unrecognized enctype: "+enctype );
-				if( this.method!=Method.POST )
-					throw new LuanException( "multipart/form-data can only be used with POST" );
-				if( params==null )
-					throw new LuanException( "multipart/form-data requires parameters" );
-				if( params.isEmpty() )
-					throw new LuanException( "multipart/form-data parameters can't be empty" );
-				multipart = new MultipartClient(params);
-			}
-			else if( params != null ) {
-				StringBuilder sb = new StringBuilder();
-				for( Object hack : params.entrySet() ) {
-					Map.Entry entry = (Map.Entry)hack;
-					String key = (String)entry.getKey();
-					Object val = entry.getValue();
-					String keyEnc = encode(key);
-					if( val instanceof String ) {
-						and(sb);
-						sb.append( keyEnc ).append( '=' ).append( encode((String)val) );
-					} else {
-						if( !(val instanceof LuanTable) )
-							throw new LuanException( "parameter '"+key+"' must be string or table" );
-						LuanTable t = (LuanTable)val;
-						if( !t.isList() )
-							throw new LuanException( "parameter '"+key+"' table must be list" );
-						for( Object obj : t.asList() ) {
-							if( !(obj instanceof String) )
-								throw new LuanException( "parameter '"+key+"' values must be strings" );
-							and(sb);
-							sb.append( keyEnc ).append( '=' ).append( encode((String)obj) );
-						}
-					}
-				}
-				if( this.method==Method.DELETE )
-					throw new LuanException( "the DELETE method cannot take parameters" );
-				if( this.method==Method.POST ) {
-					content = sb.toString();
-				} else { // GET
-					String urlS = this.url.toString();
-					if( urlS.indexOf('?') == -1 ) {
-						urlS += '?';
-					} else {
-						urlS += '&';
-					}
-					urlS += sb;
-					try {
-						this.url = new URL(urlS);
-					} catch(IOException e) {
-						throw new RuntimeException(e);
-					}
-				}
-			}
-			Integer timeout = getInt(map,"time_out");
-			if( timeout != null )
-				this.timeout = timeout;
-			if( !map.isEmpty() )
-				throw new LuanException( "unrecognized options: "+map );
-		}
-	}
-
-	public static String toHttpHeaderName(String luanName) {
-		luanName = luanName.toLowerCase();
-		StringBuilder buf = new StringBuilder();
-		boolean capitalize = true;
-		char[] a = luanName.toCharArray();
-		for( int i=0; i<a.length; i++ ) {
-			char c = a[i];
-			if( c == '_'  || c == '-' ) {
-				a[i] = '-';
-				capitalize = true;
-			} else if( capitalize ) {
-				a[i] = Character.toUpperCase(c);
-				capitalize = false;
-			}
-		}
-		return String.valueOf(a);
-	}
-
-	private static void and(StringBuilder sb) {
-		if( sb.length() > 0 )
-			sb.append('&');
-	}
-
-	private static String encode(String s) {
-		try {
-			return URLEncoder.encode(s,"UTF-8");
-		} catch(UnsupportedEncodingException e) {
-			throw new RuntimeException(e);
-		}
-	}
-
-	private static String getString(Map map,String key) throws LuanException {
-		Object val = map.remove(key);
-		if( val!=null && !(val instanceof String) )
-			throw new LuanException( "parameter '"+key+"' must be a string" );
-		return (String)val;
-	}
-
-	private static Integer getInt(Map map,String key) throws LuanException {
-		Object val = map.remove(key);
-		if( val==null )
-			return null;
-		Integer i = Luan.asInteger(val);
-		if( i==null )
-			throw new LuanException( "parameter '"+key+"' must be an integer" );
-		return i;
-	}
-
-	private static LuanTable getTable(Map map,String key) throws LuanException {
-		Object val = map.remove(key);
-		if( val!=null && !(val instanceof LuanTable) )
-			throw new LuanException( "parameter '"+key+"' must be a table" );
-		return (LuanTable)val;
-	}
-
-	private static Map getMap(LuanState luan,Map map,String key) throws LuanException {
-		LuanTable t = getTable(map,key);
-		return t==null ? null : t.asMap(luan);
-	}
-
-	@Override public InputStream inputStream() throws IOException, LuanException {
-		URLConnection con = url.openConnection();
-		if( timeout != 0 ) {
-			con.setConnectTimeout(timeout);
-			con.setReadTimeout(timeout);
-		}
-		if( headers != null ) {
-			for( Object hack : headers.entrySet() ) {
-				Map.Entry entry = (Map.Entry)hack;
-				String key = (String)entry.getKey();
-				Object val = entry.getValue();
-				if( val instanceof String ) {
-					con.addRequestProperty(key,(String)val);
-				} else {
-					List list = (List)val;
-					for( Object obj : list ) {
-						con.addRequestProperty(key,(String)obj);
-					}
-				}
-			}
-		}
-		if( method==Method.GET ) {
-			return con.getInputStream();
-		}
-
-		HttpURLConnection httpCon = (HttpURLConnection)con;
-
-		if( method==Method.DELETE ) {
-			httpCon.setRequestMethod("DELETE");
-			return httpCon.getInputStream();
-		}
-
-		// POST
-
-//		httpCon.setRequestProperty("Content-Type","application/x-www-form-urlencoded");
-		httpCon.setDoOutput(true);
-		httpCon.setRequestMethod("POST");
-
-		OutputStream out;
-		if( multipart != null ) {
-			out = multipart.write(httpCon);
-		} else {
-			byte[] post = content.getBytes();
-//			httpCon.setRequestProperty("Content-Length",Integer.toString(post.length));
-			out = httpCon.getOutputStream();
-			out.write(post);
-		}
-		out.flush();
-		try {
-			try {
-				return httpCon.getInputStream();
-			} catch(IOException e) {
-				InputStream is = httpCon.getErrorStream();
-				if( is == null )
-					throw e;
-				Reader in = new InputStreamReader(is);
-				String msg = Utils.readAll(in);
-				in.close();
-				throw new LuanException(msg,e);
-			}
-		} finally {
-			out.close();
-		}
-	}
-
-	@Override public String to_string() {
-		return url.toString();
-	}
-
-	@Override public String to_uri_string() {
-		return url.toString();
-	}
-/*
-	public String post(String postS) throws IOException {
-		return new UrlCall(url).post(postS);
-	}
-
-	@Override public LuanTable table() {
-		LuanTable tbl = super.table();
-		try {
-			tbl.rawPut( "post", new LuanJavaFunction(
-				LuanUrl.class.getMethod( "post", String.class ), this
-			) );
-		} catch(NoSuchMethodException e) {
-			throw new RuntimeException(e);
-		}
-		return tbl;
-	}
-*/
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/url/MultiPartOutputStream.java
--- a/core/src/luan/modules/url/MultiPartOutputStream.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,146 +0,0 @@
-//
-//  ========================================================================
-//  Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd.
-//  ------------------------------------------------------------------------
-//  All rights reserved. This program and the accompanying materials
-//  are made available under the terms of the Eclipse Public License v1.0
-//  and Apache License v2.0 which accompanies this distribution.
-//
-//      The Eclipse Public License is available at
-//      http://www.eclipse.org/legal/epl-v10.html
-//
-//      The Apache License v2.0 is available at
-//      http://www.opensource.org/licenses/apache2.0.php
-//
-//  You may elect to redistribute this code under either of these licenses.
-//  ========================================================================
-//
-//  This horrible broken code from jetty is just here for me to look at.  It isn't used.  -fschmidt
-
-package luan.modules.url;
-
-import java.io.FilterOutputStream;
-import java.io.IOException;
-import java.io.OutputStream;
-
-
-/* ================================================================ */
-/** Handle a multipart MIME response.
- *
- * 
- * 
-*/
-public class MultiPartOutputStream extends FilterOutputStream
-{
-    /* ------------------------------------------------------------ */
-    private static final byte[] __CRLF={'\r','\n'};
-    private static final byte[] __DASHDASH={'-','-'};
-    
-    public static String MULTIPART_MIXED="multipart/mixed";
-    public static String MULTIPART_X_MIXED_REPLACE="multipart/x-mixed-replace";
-    public static final String __ISO_8859_1="ISO-8859-1";
-
-	public static String newBoundary(Object obj) {
-        return "jetty"+System.identityHashCode(obj)+
-        Long.toString(System.currentTimeMillis(),36);
-	}
-    
-    /* ------------------------------------------------------------ */
-    private final String boundary;
-    private final byte[] boundaryBytes;
-
-    /* ------------------------------------------------------------ */
-    private boolean inPart=false;    
-    
-    /* ------------------------------------------------------------ */
-    public MultiPartOutputStream(OutputStream out,String boundary)
-    throws IOException
-    {
-        super(out);
-
-		this.boundary = boundary;
-        boundaryBytes=boundary.getBytes(__ISO_8859_1);
-
-        inPart=false;
-    }
-
-    
-
-    /* ------------------------------------------------------------ */
-    /** End the current part.
-     * @exception IOException IOException
-     */
-    @Override
-    public void close()
-         throws IOException
-    {
-        if (inPart)
-            out.write(__CRLF);
-        out.write(__DASHDASH);
-        out.write(boundaryBytes);
-        out.write(__DASHDASH);
-        out.write(__CRLF);
-        inPart=false;
-        super.close();
-    }
-    
-    /* ------------------------------------------------------------ */
-    public String getBoundary()
-    {
-        return boundary;
-    }
-
-    public OutputStream getOut() {return out;}
-    
-    /* ------------------------------------------------------------ */
-    /** Start creation of the next Content.
-     */
-    public void startPart(String contentType)
-         throws IOException
-    {
-        if (inPart)
-            out.write(__CRLF);
-        inPart=true;
-        out.write(__DASHDASH);
-        out.write(boundaryBytes);
-        out.write(__CRLF);
-        if (contentType != null)
-            out.write(("Content-Type: "+contentType).getBytes(__ISO_8859_1));
-        out.write(__CRLF);
-        out.write(__CRLF);
-    }
-        
-    /* ------------------------------------------------------------ */
-    /** Start creation of the next Content.
-     */
-    public void startPart(String contentType, String[] headers)
-         throws IOException
-    {
-        if (inPart)
-            out.write(__CRLF);
-        inPart=true;
-        out.write(__DASHDASH);
-        out.write(boundaryBytes);
-        out.write(__CRLF);
-        if (contentType != null)
-            out.write(("Content-Type: "+contentType).getBytes(__ISO_8859_1));
-        out.write(__CRLF);
-        for (int i=0;headers!=null && i<headers.length;i++)
-        {
-            out.write(headers[i].getBytes(__ISO_8859_1));
-            out.write(__CRLF);
-        }
-        out.write(__CRLF);
-    }
-    
-    /* ------------------------------------------------------------ */
-    @Override
-    public void write(byte[] b, int off, int len) throws IOException
-    {
-        out.write(b,off,len);
-    }
-}
-
-
-
-
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/url/MultipartClient.java
--- a/core/src/luan/modules/url/MultipartClient.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,73 +0,0 @@
-package luan.modules.url;
-
-import java.io.OutputStream;
-import java.io.IOException;
-import java.net.HttpURLConnection;
-import java.util.List;
-import java.util.ArrayList;
-import java.util.Map;
-import java.util.HashMap;
-import luan.LuanTable;
-import luan.LuanException;
-
-
-public final class MultipartClient {
-	private static final byte[] __CRLF = {'\r','\n'};
-	private static final byte[] __DASHDASH = {'-','-'};
-	private static final String __ISO_8859_1 = "ISO-8859-1";
-
-	private final Map params = new HashMap();
-
-	MultipartClient(Map params) throws LuanException {
-		for( Object hack : params.entrySet() ) {
-			Map.Entry entry = (Map.Entry)hack;
-			String key = (String)entry.getKey();
-			Object val = entry.getValue();
-			List list = new ArrayList();
-			if( val instanceof String ) {
-				list.add(val);
-			} else {
-				if( !(val instanceof LuanTable) )
-					throw new LuanException( "parameter '"+key+"' must be string or table" );
-				LuanTable t = (LuanTable)val;
-				if( !t.isList() )
-					throw new LuanException( "parameter '"+key+"' table must be list" );
-				for( Object obj : t.asList() ) {
-					if( !(obj instanceof String) )
-						throw new LuanException( "parameter '"+key+"' values must be strings" );
-					list.add(obj);
-				}
-			}
-			this.params.put(key,list);
-		}
-	}
-
-	public OutputStream write(HttpURLConnection httpCon) throws IOException {
-		String boundary = "luan" + System.identityHashCode(this) + Long.toString(System.currentTimeMillis(),36);
-		byte[] boundaryBytes = boundary.getBytes(__ISO_8859_1);
-
-		httpCon.setRequestProperty("Content-Type","multipart/form-data; boundary="+boundary);
-		OutputStream out = httpCon.getOutputStream();
-		for( Object hack : params.entrySet() ) {
-			Map.Entry entry = (Map.Entry)hack;
-			String name = (String)entry.getKey();
-			List list = (List)entry.getValue();
-			for( Object obj : list ) {
-				String val = (String)obj;
-		        out.write(__DASHDASH);
-		        out.write(boundaryBytes);
-		        out.write(__CRLF);
-	            out.write(("Content-Disposition: form-data; name=\""+name+"\"").getBytes(__ISO_8859_1));
-	            out.write(__CRLF);
-		        out.write(__CRLF);
-				out.write(val.getBytes());
-		        out.write(__CRLF);
-			}
-		}
-		out.write(__DASHDASH);
-		out.write(boundaryBytes);
-		out.write(__DASHDASH);
-		out.write(__CRLF);
-		return out;
-	}
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/url/UrlCall.java
--- a/core/src/luan/modules/url/UrlCall.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,84 +0,0 @@
-// not used, just for reference
-
-package luan.modules.url;
-
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.io.Reader;
-import java.io.IOException;
-import java.net.URLConnection;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.util.Map;
-import java.util.HashMap;
-import luan.modules.Utils;
-
-
-public final class UrlCall {
-	public final URLConnection connection;
-
-	public UrlCall(String url) throws IOException {
-		this(new URL(url));
-	}
-
-	public UrlCall(URL url) throws IOException {
-		connection = url.openConnection();
-	}
-
-	public void acceptJson() {
-		connection.setRequestProperty("accept","application/json");
-	}
-
-	public String get() throws IOException {
-		Reader in = new InputStreamReader(connection.getInputStream());
-		String rtn = Utils.readAll(in);
-		in.close();
-		return rtn;
-	}
-
-	public String post(String content,String contentType) throws IOException {
-		HttpURLConnection connection = (HttpURLConnection)this.connection;
-
-		connection.setRequestProperty("Content-type",contentType);
-		connection.setDoOutput(true);
-		connection.setRequestMethod("POST");
-
-		byte[] post = content.getBytes();
-		connection.setRequestProperty("Content-Length",Integer.toString(post.length));
-		OutputStream out = connection.getOutputStream();
-		out.write(post);
-		out.flush();
-
-		Reader in;
-		try {
-			in = new InputStreamReader(connection.getInputStream());
-		} catch(IOException e) {
-			InputStream is = connection.getErrorStream();
-			if( is == null )
-				throw e;
-			in = new InputStreamReader(is);
-			String msg = Utils.readAll(in);
-			in.close();
-			throw new UrlCallException(msg,e);
-		}
-		String rtn = Utils.readAll(in);
-		in.close();
-		out.close();
-		return rtn;
-	}
-
-	public String post(String content) throws IOException {
-		return post(content,"application/x-www-form-urlencoded");
-	}
-
-	public String postJson(String content) throws IOException {
-		return post(content,"application/json");
-	}
-
-	public static final class UrlCallException extends IOException {
-		UrlCallException(String msg,IOException e) {
-			super(msg,e);
-		}
-	}
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c core/src/luan/modules/which.luan
--- a/core/src/luan/modules/which.luan	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,9 +0,0 @@
-local Luan = require "luan:Luan.luan"
-local error = Luan.error
-local values = Luan.values or error()
-local Which_mod = require "luan:Which_mod.luan"
-local which = Which_mod.which or error()
-
-for name in values(...) do
-	which(name)
-end
diff -r 3e30cf310e56 -r 1a68fc55a80c http/ext/jetty-continuation-8.1.15.v20140411.jar
Binary file http/ext/jetty-continuation-8.1.15.v20140411.jar has changed
diff -r 3e30cf310e56 -r 1a68fc55a80c http/ext/jetty-http-8.1.15.v20140411.jar
Binary file http/ext/jetty-http-8.1.15.v20140411.jar has changed
diff -r 3e30cf310e56 -r 1a68fc55a80c http/ext/jetty-io-8.1.15.v20140411.jar
Binary file http/ext/jetty-io-8.1.15.v20140411.jar has changed
diff -r 3e30cf310e56 -r 1a68fc55a80c http/ext/jetty-server-8.1.15.v20140411.jar
Binary file http/ext/jetty-server-8.1.15.v20140411.jar has changed
diff -r 3e30cf310e56 -r 1a68fc55a80c http/ext/jetty-util-8.1.15.v20140411.jar
Binary file http/ext/jetty-util-8.1.15.v20140411.jar has changed
diff -r 3e30cf310e56 -r 1a68fc55a80c http/ext/servlet-api-3.0.jar
Binary file http/ext/servlet-api-3.0.jar has changed
diff -r 3e30cf310e56 -r 1a68fc55a80c http/src/luan/modules/http/AuthenticationHandler.java
--- a/http/src/luan/modules/http/AuthenticationHandler.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,53 +0,0 @@
-package luan.modules.http;
-
-import java.io.IOException;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import org.eclipse.jetty.server.Request;
-import org.eclipse.jetty.server.handler.AbstractHandler;
-import org.eclipse.jetty.util.B64Code;
-
-
-public class AuthenticationHandler extends AbstractHandler {
-	private final String path;
-	private String password = "password";
-
-	public AuthenticationHandler(String path) {
-		this.path = path;
-	}
-
-	public void setPassword(String password) {
-		this.password = password;
-	}
-
-	public void handle(String target,Request baseRequest,HttpServletRequest request,HttpServletResponse response) 
-		throws IOException
-	{
-		if( !target.startsWith(path) )
-			return;
-		String pwd = getPassword(request);
-		if( password.equals(pwd) )
-			return;
-		response.setHeader("WWW-Authenticate","Basic realm=\""+path+"\"");
-		response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
-		baseRequest.setHandled(true);
-	}
-
-	private static String getPassword(HttpServletRequest request) {
-		String auth = request.getHeader("Authorization");
-		if( auth==null )
-			return null;
-		String[] a = auth.split(" +");
-		if( a.length != 2 )
-			throw new RuntimeException("auth = "+auth);
-		if( !a[0].equals("Basic") )
-			throw new RuntimeException("auth = "+auth);
-		auth = new String(B64Code.decode(a[1]));
-		a = auth.split(":");
-		if( a.length != 2 )
-			throw new RuntimeException("auth = "+auth);
-		return a[1];
-	}
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c http/src/luan/modules/http/Dump_mod.luan
--- a/http/src/luan/modules/http/Dump_mod.luan	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,50 +0,0 @@
-local Luan = require "luan:Luan.luan"
-local pairs = Luan.pairs
-local ipairs = Luan.ipairs
-local Io = require "luan:Io.luan"
-local Http = require "luan:http/Http.luan"
-java()
-local HttpServicer = require "java:luan.modules.http.HttpServicer"
-
-local M = {}
-
-local to_http_header_name = HttpServicer.toHttpHeaderName
-M.to_http_header_name = to_http_header_name
-
-function M.respond()
-	Http.response.header.content_type = "text/plain"
-	Io.stdout = Http.response.text_writer()
-
-	local method = Http.request.method
-	local path = Http.request.path
-	local query = Http.request.query_string()
-	if method ~= "POST" and query ~= nil then
-		path = path.."?"..query
-	end
-%>
-<%=method%> <%=path%> <%=Http.request.protocol%> 
-<%
-	M.dump_headers(Http.request.headers)
-%>
-
-<%
-	if method == "POST" and query ~= nil then
-%>
-<%=query%>
-<%
-	end
-end
-
-
-function M.dump_headers(headers)
-	for name, values in pairs(headers) do
-		local header_name = to_http_header_name(name)
-		for _, value in ipairs(values) do
-%>
-<%=header_name%>: <%=value%>
-<%
-		end
-	end
-end
-
-return M
diff -r 3e30cf310e56 -r 1a68fc55a80c http/src/luan/modules/http/Http.luan
--- a/http/src/luan/modules/http/Http.luan	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,170 +0,0 @@
-java()
-local Luan = require "luan:Luan.luan"
-local error = Luan.error
-local ipairs = Luan.ipairs or error()
-local pairs = Luan.pairs or error()
-local set_metatable = Luan.set_metatable or error()
-local Io = require "luan:Io.luan"
-local Html = require "luan:Html.luan"
-local url_encode = Html.url_encode or error()
-local Table = require "luan:Table.luan"
-local clear = Table.clear or error()
-local Package = require "luan:Package.luan"
-local String = require "luan:String.luan"
-local matches = String.matches or error()
-local HttpServicer = require "java:luan.modules.http.HttpServicer"
-local IoLuan = require "java:luan.modules.IoLuan"
-
-local M = {}
-
-local singular_metatable = {}
-
-function singular_metatable.__index(table,key)
-	local list = table.__plural[key]
-	return list and list[1]
-end
-
-function singular_metatable.__new_index(table,key,value)
-	table.__plural[key] = value and {value}
-end
-
-function singular_metatable.__pairs(table)
-	local iter = pairs(table.__plural)
-	return function()
-		local key, value = iter()
-		return key, value and value[1]
-	end
-end
-
-local function sent_error()
-	error "headers are not accessible after you start writing content"
-end
-
-local sent_error_metatable = { __index=sent_error, __new_index=sent_error }
-
-function M.sent_headers(headers)
-	clear(headers)
-	set_metatable(headers,sent_error_metatable)
-end
-
-
-local function new_common(this)
-	this = this or {}
-	this.headers = {}
-	this.header = {__plural=this.headers}
-	set_metatable(this.header,singular_metatable)
-	return this
-end
-
-
-function M.new_request(this)
-	this = new_common(this)
-	this.method = "GET"  -- default
-	-- this.path
-	-- this.protocol
-	this.scheme = "http"  -- default
-	this.port = 80  -- default
-	this.parameters = {}
-	this.parameter = {__plural=this.parameters}
-	set_metatable(this.parameter,singular_metatable)
-	this.cookie = {}
-
-	function this.query_string()
-		local string_uri = Io.uri "string:"
-		local out = string_uri.text_writer()
-		local and_char = ""
-		for name, values in pairs(this.parameters) do
-			for _, value in ipairs(values) do
-				out.write( and_char, url_encode(name), "=", url_encode(value) )
-				and_char = "&"
-			end
-		end
-		out.close()
-		local s = string_uri.read_text()
-		return s ~= "" and s or nil
-	end
-
-	function this.url()
-		local url = this.scheme.."://"..this.header.host..this.path
-		if this.method ~= "POST" then
-			local query = this.query_string()
-			if query ~= nil then
-				url = url.."?"..query
-			end
-		end
-		return url
-	end
-
-	return this
-end
-
-local STATUS = {
-	OK = 200;
-	-- add more as needed
-}
-M.STATUS = STATUS
-
-function M.new_response(this)
-	this = new_common(this)
-	this.status = STATUS.OK
-	if this.java ~= nil then
-		this.send_redirect = this.java.sendRedirect
-		this.send_error = this.java.sendError
-
-		function this.set_cookie(name,value,is_persistent,domain)
-			HttpServicer.setCookie(M.request.java,this.java,name,value,is_persistent,domain)
-		end
-
-		function this.remove_cookie(name,domain)
-			HttpServicer.removeCookie(M.request.java,this.java,name,domain)
-		end
-
-		function this.set()
-			HttpServicer.setResponse(this,this.java)
-			M.sent_headers(this.headers)
-		end
-
-		function this.text_writer()
-			this.set()
-			return IoLuan.textWriter(this.java.getWriter())
-		end
-
-		function this.binary_writer()
-			this.set()
-			return IoLuan.binaryWriter(this.java.getOutputStream())
-		end
-
-		function this.reset()
-			this.java.reset()
-			set_metatable(this.headers,nil)
-		end
-	end
-	return this
-end
-
--- request = new_request{}  -- filled in by HttpServicer
--- response = new_response{}  -- filled in by HttpServicer
-
-
-M.per_session_pages = {}
-
-function M.per_session(page)
-	M.per_session_pages[page] = true
-end
-
-function M.clear_session()
-	M.request.java.getSession().removeAttribute("luan")
-end
-
-
-function M.uncache_site()
-	for k in pairs(Table.copy(Package.loaded)) do
-		if matches(k,"^site:") then
-			Package.loaded[k] = nil
-		end
-	end
-end
-
-M.run_later = HttpServicer.run_later
-
-return M
diff -r 3e30cf310e56 -r 1a68fc55a80c http/src/luan/modules/http/HttpServicer.java
--- a/http/src/luan/modules/http/HttpServicer.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,302 +0,0 @@
-package luan.modules.http;
-
-import java.io.InputStream;
-import java.io.BufferedInputStream;
-import java.io.PrintWriter;
-import java.io.IOException;
-import java.util.Map;
-import java.util.Set;
-import java.util.List;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Iterator;
-import java.util.Enumeration;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-import javax.servlet.ServletOutputStream;
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
-import javax.servlet.http.Part;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.eclipse.jetty.util.MultiPartInputStream;
-import luan.Luan;
-import luan.LuanState;
-import luan.LuanFunction;
-import luan.LuanException;
-import luan.LuanTable;
-import luan.LuanMeta;
-import luan.LuanPropertyMeta;
-import luan.DeepCloner;
-import luan.modules.PackageLuan;
-import luan.modules.IoLuan;
-import luan.modules.TableLuan;
-import luan.modules.Utils;
-import luan.modules.url.LuanUrl;
-
-
-public final class HttpServicer {
-	private static final Logger logger = LoggerFactory.getLogger(HttpServicer.class);
-
-	public static boolean service(LuanState luan,HttpServletRequest request,HttpServletResponse response,String modName)
-		throws LuanException
-	{
-		LuanFunction fn;
-		synchronized(luan) {
-			LuanTable module = (LuanTable)PackageLuan.require(luan,"luan:http/Http.luan");
-			LuanTable per_session_pages = (LuanTable)module.rawGet("per_session_pages");
-			Object mod = PackageLuan.load(luan,modName);
-			if( mod==null )
-				return false;
-			if( !(mod instanceof LuanFunction) )
-				throw new LuanException( "module '"+modName+"' must return a function" );
-			if( Boolean.TRUE.equals(per_session_pages.rawGet(mod)) ) {
-				HttpSession session = request.getSession();
-				LuanState sessionLuan = (LuanState)session.getAttribute("luan");
-				if( sessionLuan!=null ) {
-					luan = sessionLuan;
-				} else {
-					DeepCloner cloner = new DeepCloner();
-					luan = (LuanState)cloner.deepClone(luan);
-					session.setAttribute("luan",luan);
-				}
-				fn = (LuanFunction)PackageLuan.require(luan,modName);
-			} else {
-				DeepCloner cloner = new DeepCloner();
-				luan = (LuanState)cloner.deepClone(luan);
-				fn = (LuanFunction)cloner.get(mod);
-			}
-		}
-
-		LuanTable module = (LuanTable)PackageLuan.require(luan,"luan:http/Http.luan");
-
-		// request
-		LuanFunction newRequestFn = (LuanFunction)module.rawGet("new_request");
-		LuanTable requestTbl = (LuanTable)newRequestFn.call(luan);
-		module.rawPut("request",requestTbl);
-		requestTbl.rawPut("java",request);
-		requestTbl.rawPut("method",request.getMethod());
-		requestTbl.rawPut("path",request.getRequestURI());
-		requestTbl.rawPut("protocol",request.getProtocol());
-		requestTbl.rawPut("scheme",request.getScheme());
-		requestTbl.rawPut("port",request.getServerPort());
-
-		LuanTable headersTbl = (LuanTable)requestTbl.rawGet("headers");
-		for( Enumeration<String> enKeys = request.getHeaderNames(); enKeys.hasMoreElements(); ) {
-			String key = enKeys.nextElement();
-			LuanTable values = new LuanTable();
-			for( Enumeration<String> en = request.getHeaders(key); en.hasMoreElements(); ) {
-				values.rawPut(values.rawLength()+1,en.nextElement());
-			}
-			key = toLuanHeaderName(key);
-			headersTbl.rawPut(key,values);
-		}
-
-		LuanTable parametersTbl = (LuanTable)requestTbl.rawGet("parameters");
-		String contentType = request.getContentType();
-		if( contentType==null || !contentType.startsWith("multipart/form-data") ) {
-			for( Map.Entry<String,String[]> entry : request.getParameterMap().entrySet() ) {
-				parametersTbl.rawPut(entry.getKey(),new LuanTable(Arrays.asList(entry.getValue())));
-			}
-		} else {  // multipart
-			try {
-				InputStream in = new BufferedInputStream(request.getInputStream());
-				final MultiPartInputStream mpis = new MultiPartInputStream(in,contentType,null,null);
-				mpis.setDeleteOnExit(true);
-				for( Part p : mpis.getParts() ) {
-					final MultiPartInputStream.MultiPart part = (MultiPartInputStream.MultiPart)p;
-					String name = part.getName();
-/*
-System.out.println("name = "+name);
-System.out.println("getContentType = "+part.getContentType());
-System.out.println("getHeaderNames = "+part.getHeaderNames());
-System.out.println("content-disposition = "+part.getHeader("content-disposition"));
-System.out.println();
-*/
-					Object value;
-					String filename = part.getContentDispositionFilename();
-					if( filename == null ) {
-						value = new String(part.getBytes());
-					} else {
-						LuanTable partTbl = LuanPropertyMeta.INSTANCE.newTable();
-						partTbl.rawPut("filename",filename);
-						partTbl.rawPut("content_type",part.getContentType());
-						LuanPropertyMeta.INSTANCE.getters(partTbl).rawPut( "content", new LuanFunction() {
-							@Override public Object call(LuanState luan,Object[] args) throws LuanException {
-								try {
-									InputStream in = part.getInputStream();
-									byte[] content = Utils.readAll(in);
-									in.close();
-									return content;
-								} catch(IOException e) {
-									throw new RuntimeException(e);
-								}
-							}
-						} );
-						value = partTbl;
-					}
-					LuanTable list = (LuanTable)parametersTbl.rawGet(name);
-					if( list == null ) {
-						list = new LuanTable();
-						parametersTbl.rawPut(name,list);
-					}
-					list.rawPut(parametersTbl.rawLength()+1,value);
-				}
-			} catch(IOException e) {
-				throw new RuntimeException(e);
-			} catch(ServletException e) {
-				throw new RuntimeException(e);
-			}
-		}
-
-		LuanTable cookieTbl = (LuanTable)requestTbl.rawGet("cookie");
-		for( Cookie cookie : request.getCookies() ) {
-			cookieTbl.rawPut( cookie.getName(), unescape(cookie.getValue()) );
-		}
-
-
-		// response
-		LuanTable responseTbl = new LuanTable();
-		responseTbl.rawPut("java",response);
-		LuanFunction newResponseFn = (LuanFunction)module.rawGet("new_response");
-		newResponseFn.call( luan, new Object[]{responseTbl} );
-		module.rawPut("response",responseTbl);
-
-		fn.call(luan);
-		handle_run_later(luan);
-		return true;
-	}
-
-	public static void setResponse(LuanTable responseTbl,HttpServletResponse response) throws LuanException {
-		int status = Luan.asInteger(responseTbl.rawGet("status"));
-		response.setStatus(status);
-		LuanTable responseHeaders = (LuanTable)responseTbl.rawGet("headers");
-		for( Map.Entry<Object,Object> entry : responseHeaders.rawIterable() ) {
-			String name = (String)entry.getKey();
-			name = toHttpHeaderName(name);
-			LuanTable values = (LuanTable)entry.getValue();
-			for( Object value : values.asList() ) {
-				if( value instanceof String ) {
-					response.setHeader(name,(String)value);
-					continue;
-				}
-				Integer i = Luan.asInteger(value);
-				if( i != null ) {
-					response.setIntHeader(name,i);
-					continue;
-				}
-				throw new IllegalArgumentException("value must be string or integer for headers table");
-			}
-		}
-	}
-
-
-
-	// static utils
-
-	public static String toLuanHeaderName(String httpName) {
-		return httpName.toLowerCase().replace('-','_');
-	}
-
-	public static String toHttpHeaderName(String luanName) {
-/*
-		StringBuilder buf = new StringBuilder();
-		boolean capitalize = true;
-		char[] a = luanName.toCharArray();
-		for( int i=0; i<a.length; i++ ) {
-			char c = a[i];
-			if( c == '_' ) {
-				a[i] = '-';
-				capitalize = true;
-			} else if( capitalize ) {
-				a[i] = Character.toUpperCase(c);
-				capitalize = false;
-			}
-		}
-		return String.valueOf(a);
-*/
-		return LuanUrl.toHttpHeaderName(luanName);
-	}
-
-	private static String escape(String value) {
-		return value.replaceAll(";", "%3B");
-	}
-
-	private static String unescape(String value) {
-		return value.replaceAll("%3B", ";");
-	}
-
-	private static Cookie getCookie(HttpServletRequest request,String name) {
-		Cookie[] cookies = request.getCookies();
-		if( cookies == null )
-			return null;
-		for (Cookie cookie : cookies) {
-			if (cookie.getName().equals(name))
-				return cookie;
-		}
-		return null;
-	}
-
-	public static void setCookie(HttpServletRequest request,HttpServletResponse response,String name,String value,boolean isPersistent, String domain) {
-		Cookie cookie = getCookie(request,name);
-		if( cookie==null || !cookie.getValue().equals(value) ) {
-			cookie = new Cookie(name, escape(value));
-			cookie.setPath("/");
-			if (domain != null && domain.length() > 0)
-				cookie.setDomain(domain);
-			if( isPersistent )
-				cookie.setMaxAge(10000000);
-			response.addCookie(cookie);
-		}
-	}
-
-	public static void removeCookie(HttpServletRequest request,
-									HttpServletResponse response,
-									String name,
-									String domain
-	) {
-		Cookie cookie = getCookie(request, name);
-		if(cookie != null) {
-			Cookie delCookie = new Cookie(name, "delete");
-			delCookie.setPath("/");
-			delCookie.setMaxAge(0);
-			if (domain != null && domain.length() > 0)
-				delCookie.setDomain(domain);
-			response.addCookie(delCookie);
-		}
-	}
-
-
-
-	private static String RUN_LATER_KEY = "Http.run_later";
-	private static final Executor exec = Executors.newSingleThreadExecutor();
-
-	public static void run_later(final LuanState luan,final LuanFunction fn,final Object... args) {
-		List list = (List)luan.registry().get(RUN_LATER_KEY);
-		if( list == null ) {
-			list = new ArrayList();
-			luan.registry().put(RUN_LATER_KEY,list);
-		}
-		list.add(
-			new Runnable(){public void run() {
-				try {
-					fn.call(luan,args);
-				} catch(LuanException e) {
-					e.printStackTrace();
-				}
-			}}
-		);
-	}
-
-	private static void handle_run_later(LuanState luan) {
-		List list = (List)luan.registry().get(RUN_LATER_KEY);
-		if( list==null )
-			return;
-		for( Object obj : list ) {
-			exec.execute((Runnable)obj);
-		}
-	}
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c http/src/luan/modules/http/Http_test.luan
--- a/http/src/luan/modules/http/Http_test.luan	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,75 +0,0 @@
-local Luan = require "luan:Luan.luan"
-local error = Luan.error
-local set_metatable = Luan.set_metatable or error()
-local try = Luan.try or error()
-local Package = require "luan:Package.luan"
-local Io = require "luan:Io.luan"
-local String = require "luan:String.luan"
-local matches = String.matches or error()
-local Http = require "luan:http/Http.luan"
-
-
-local M = {}
-
-M.welcome_file = "index.html"
-M.cookie = {}
-
-function M.get_page(path)
-	Http.request.path = path
-	if M.welcome_file ~= nil and matches(path,"/$") then
-		path = path .. M.welcome_file
-	end
-	local old_out = Io.stdout
-	try {
-		function()
-			local mod = Package.load("site:"..path..".luan")
-			if mod ~= nil then
-				mod()
-			else
-				local not_found = Package.load("site:/not_found.luan")
-				not_found or error(path.." not found")
-				not_found()
-			end
-			M.text_writer.close()
-		end
-		finally = function()
-			Io.stdout = old_out
-		end
-	}
-	return M.result.read_text()
-end
-
-function M.init()
-	Http.request = Http.new_request{}
-	Http.request.cookie = M.cookie
-
-	Http.response = Http.new_response{
-
-		text_writer = function()
-			Http.sent_headers(Http.response.headers)
-			M.result = Io.uri "string:"
-			M.text_writer = M.result.text_writer()
-			return M.text_writer
-		end
-
-		set_cookie = function(name,value)
-			M.cookie[name] = value
-		end
-
-		remove_cookie = function(name)
-			M.cookie[name] = nil
-		end
-
-		send_redirect = function(url)
-			Http.response.redirect = url
-		end
-
-		send_error = function(code)
-			error("sent error "..code)
-		end
-
-	}
-
-end
-
-return M
diff -r 3e30cf310e56 -r 1a68fc55a80c http/src/luan/modules/http/LuanHandler.java
--- a/http/src/luan/modules/http/LuanHandler.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,83 +0,0 @@
-package luan.modules.http;
-
-import java.io.IOException;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.eclipse.jetty.server.Request;
-import org.eclipse.jetty.server.handler.AbstractHandler;
-import luan.LuanState;
-import luan.LuanTable;
-import luan.LuanFunction;
-import luan.DeepCloner;
-import luan.LuanException;
-import luan.modules.PackageLuan;
-
-
-public class LuanHandler extends AbstractHandler {
-	private final LuanState luan;
-	private final Logger logger;
-	private String welcomeFile = "index.html";
-
-	public LuanHandler(LuanState luan,String loggerRoot) {
-		this.luan = luan;
-		if( loggerRoot==null )
-			loggerRoot = "";
-		logger = LoggerFactory.getLogger(loggerRoot+LuanHandler.class.getName());
-	}
-
-	@Override public void handle(String target,Request baseRequest,HttpServletRequest request,HttpServletResponse response) 
-		throws IOException
-	{
-		if( target.endsWith("/") )
-			target += welcomeFile;
-		try {
-			if( !HttpServicer.service(luan,request,response,"site:"+target+".luan") )
-				return;
-		} catch(LuanException e) {
-			String err = e.getFullMessage();
-			logger.error(err);
-			response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,err);
-		}
-		baseRequest.setHandled(true);
-	}
-
-	public void setWelcomeFile(String welcomeFile) {
-		this.welcomeFile = welcomeFile;
-	}
-
-	@Override protected void doStop() throws Exception {
-		synchronized(luan) {
-			luan.close();
-		}
-//System.out.println("qqqqqqqqqqqqqqqqqqqq doStop "+this);
-		super.doStop();
-	}
-/*
-	@Override public void destroy() {
-System.out.println("qqqqqqqqqqqqqqqqqqqq destroy "+this);
-		super.destroy();
-	}
-*/
-
-	public Object call_rpc(String fnName,Object... args) throws LuanException {
-		return callRpc(luan,fnName,args);
-	}
-
-	public static Object callRpc(LuanState luan,String fnName,Object... args) throws LuanException {
-		synchronized(luan) {
-			DeepCloner cloner = new DeepCloner();
-			luan = (LuanState)cloner.deepClone(luan);
-		}
-		LuanTable rpc = (LuanTable)PackageLuan.require(luan,"luan:Rpc.luan");
-		LuanTable fns = (LuanTable)rpc.get(luan,"functions");
-		LuanFunction fn = (LuanFunction)fns.get(luan,fnName);
-		if( fn == null )
-			throw new LuanException( "function not found: " + fnName );
-		return fn.call(luan,args);
-	}
-
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c http/src/luan/modules/http/NotFound.java
--- a/http/src/luan/modules/http/NotFound.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,22 +0,0 @@
-package luan.modules.http;
-
-import java.io.IOException;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import org.eclipse.jetty.server.Request;
-import luan.LuanState;
-
-
-public class NotFound extends LuanHandler {
-
-	public NotFound(LuanState luan,String loggerRoot) {
-		super(luan,loggerRoot);
-	}
-
-	@Override public void handle(String target,Request baseRequest,HttpServletRequest request,HttpServletResponse response) 
-		throws IOException
-	{
-		super.handle("/not_found",baseRequest,request,response);
-	}
-
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c http/src/luan/modules/http/Server.luan
--- a/http/src/luan/modules/http/Server.luan	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,118 +0,0 @@
-local Luan = require "luan:Luan.luan"
-local error = Luan.error
-local String = require "luan:String.luan"
-local gsub = String.gsub or error()
-local matches = String.matches or error()
-local Io = require "luan:Io.luan"
-local Package = require "luan:Package.luan"
-local Rpc = require "luan:Rpc.luan"
-local Thread = require "luan:Thread.luan"
-local Http = require "luan:http/Http.luan"
-require "luan:logging/init.luan"  -- initialize logging
-local Logging = require "luan:logging/Logging.luan"
-local logger = Logging.logger "http/Server"
-
-java()
-local Server = require "java:org.eclipse.jetty.server.Server"
-local NCSARequestLog = require "java:org.eclipse.jetty.server.NCSARequestLog"
-local DefaultHandler = require "java:org.eclipse.jetty.server.handler.DefaultHandler"
-local HandlerList = require "java:org.eclipse.jetty.server.handler.HandlerList"
-local HandlerCollection = require "java:org.eclipse.jetty.server.handler.HandlerCollection"
-local ResourceHandler = require "java:org.eclipse.jetty.server.handler.ResourceHandler"
-local RequestLogHandler = require "java:org.eclipse.jetty.server.handler.RequestLogHandler"
-local ContextHandler = require "java:org.eclipse.jetty.server.handler.ContextHandler"
-local GzipHandler = require "java:org.eclipse.jetty.server.handler.GzipHandler"
-local HandlerWrapper = require "java:org.eclipse.jetty.server.handler.HandlerWrapper"
-local SessionHandler = require "java:org.eclipse.jetty.server.session.SessionHandler"
-local AuthenticationHandler = require "java:luan.modules.http.AuthenticationHandler"
-local LuanHandler = require "java:luan.modules.http.LuanHandler"
-local NotFound = require "java:luan.modules.http.NotFound"
-
-local M = {}
-
-M.port = 8080
-
-M.welcome_file = "index.html"
-
-
-M.authentication_handler = AuthenticationHandler.new("/private/")
-
-M.luan_handler = LuanHandler.new()
-
-M.resource_handler = ResourceHandler.new()
-M.resource_handler.setDirectoriesListed(true)
-
-M.handlers = HandlerList.new()
-M.handlers.setHandlers { M.authentication_handler, M.luan_handler, M.resource_handler }
-
-function M.add_folder(context,dir)
-	local rh = ResourceHandler.new()
-	rh.setResourceBase(dir)
-	rh.setDirectoriesListed(true)
-	local ch = ContextHandler.new(context)
-	ch.setHandler(rh)
-	M.handlers.addHandler(ch)
-	return rh
-end
-
-M.handler_wrapper = HandlerWrapper.new()
-M.handler_wrapper.setHandler(M.handlers)
-
-function M.zip()
-	local h = GzipHandler.new()
-	h.setHandler(M.handler_wrapper.getHandler())
-	M.handler_wrapper.setHandler(h)
-end
-
-M.log = NCSARequestLog.new()
-M.log.setExtended(false)
-M.log_handler = RequestLogHandler.new()
-M.log_handler.setRequestLog(M.log)
-
-function M.set_log_file(file_name)
-	M.log.setFilename(file_name)
-end
-
-local hc = HandlerCollection.new()
-hc.setHandlers { SessionHandler.new(), M.handler_wrapper, DefaultHandler.new(), M.log_handler }
-
-
-function M.init(dir)
-	dir = gsub(dir,"/$","")  -- remove trailing '/' if any
-	Http.dir = dir
-	function Io.schemes.site(path)
-		return Io.uri( dir..path )
-	end
-	M.authentication_handler.setPassword(Io.password)
-	local base = dir
-	if matches(base,"^classpath:") then
-		base = dir.."#"..M.welcome_file.."#"..M.welcome_file..".luan"
-	end
-	M.resource_handler.setResourceBase(Io.uri(base).to_string())
-	M.resource_handler.setWelcomeFiles {M.welcome_file}
-	M.luan_handler.setWelcomeFile(M.welcome_file)
-	M.handlers.addHandler(NotFound.new())
-	M.server = Server.new(M.port)
-	M.server.setHandler(hc)
-	Package.load("site:/init.luan")
-end
-
-function M.start()
-	M.server.start()
-end
-
-function M.start_rpc()
-	function Rpc.functions.call(domain,fn_name,...)
-		return M.luan_handler.call_rpc(fn_name,...)
-	end
-
-	Thread.fork(Rpc.serve)
-end
-
-function M.serve(dir)
-	M.init(dir)
-	M.start()
-	M.start_rpc()
-end
-
-return M
diff -r 3e30cf310e56 -r 1a68fc55a80c http/src/luan/modules/http/Shell_mod.luan
--- a/http/src/luan/modules/http/Shell_mod.luan	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,108 +0,0 @@
-local Luan = require "luan:Luan.luan"
-local error = Luan.error
-local ipairs = Luan.ipairs or error()
-local load = Luan.load or error()
-local try = Luan.try or error()
-local Io = require "luan:Io.luan"
-local print = Io.print or error()
-local Http = require "luan:http/Http.luan"
-
-
-local M = {}
-
-local history = {}
-M.env = {}
-
-function M.respond()
-	if Http.request.parameter.clear ~= nil then
-		Http.clear_session()
-		Http.response.send_redirect(Http.request.path)  -- reload page
-		return
-	else
-		local cmd = Http.request.parameter.cmd
-		if cmd ~= nil then
-			Io.stdout = {}
-			function Io.stdout.write(...)
-				for v in Luan.values(...) do
-					history[#history+1] = v
-				end
-			end
-			print( "% "..cmd )
-			try {
-				function()
-					local line
-					try {
-						function()
-							line = load("return "..cmd,"<web_shell>",M.env)
-						end
-						catch = function(e)
-							line = load(cmd,"<web_shell>",M.env)
-						end
-					}
-					print( line() )
-				end
-				catch = function(e)
-					Io.print_to(Io.stderr,e)
-					print(e)
-				end
-			}
-		end
-	end
-
-	Io.stdout = Http.response.text_writer()
-%>
-<html>
-	<head>
-		<title>Luan Shell</title>
-		<style>
-			body {
-				font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
-				margin: 2em 5% 0 5%;
-			}
-			pre {
-				font: inherit;
-			}
-			input[type="text"] {
-				font: inherit;
-				padding: .5em .8em;
-				border-radius: 8px;
-				border-style: groove;
-			}
-			input[type="text"]:focus {
-				border-color: #66afe9;
-				outline: none;
-			}
-			input[type="submit"] {
-				color: white;
-				background: #337ab7;
-				border-color: #337ab7;
-				font: inherit;
-				padding: .5em;
-				border-radius: 4px;
-			}
-			input[type="submit"]:hover {
-				background: #236aa7 !important;
-			}
-		</style>
-	</head>
-	<body>
-		<h2>Luan Shell</h2>
-		<p>This is a command shell.  Enter commands below.</p>
-		<pre><%
-		for _,v in ipairs(history) do
-			Io.stdout.write(v)
-		end
-		%></pre>
-		<form name='form0' method='post'>
-			% <input type="text" name='cmd' size="80" autofocus>
-			<input type="submit" value="run">
-			<input type="submit" name="clear" value="clear">
-		</form>
-	</body>
-</html>
-<%
-end
-
-Http.per_session(M.respond)
-
-return M
diff -r 3e30cf310e56 -r 1a68fc55a80c http/src/luan/modules/http/dump.luan
--- a/http/src/luan/modules/http/dump.luan	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-return require("luan:http/Dump_mod.luan").respond
diff -r 3e30cf310e56 -r 1a68fc55a80c http/src/luan/modules/http/run.luan
--- a/http/src/luan/modules/http/run.luan	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,106 +0,0 @@
-local Luan = require "luan:Luan.luan"
-local error = Luan.error
-local load = Luan.load or error()
-local try = Luan.try or error()
-local Io = require "luan:Io.luan"
-local print = Io.print or error()
-local String = require "luan:String.luan"
-local gmatch = String.gmatch or error()
-local Http = require "luan:http/Http.luan"
-
-
-local function lines(s)
-	local matcher = gmatch(s,"([^\n]*)\n|([^\n])+$")
-	return function()
-		local m1, m2 = matcher()
-		return m1 or m2
-	end
-end
-
-local function print_with_line_numbers(s)
-	local i = 1
-	for line in lines(s) do
-		print(i,line)
-		i = i + 1
-	end
-end
-
-local function form() %>
-<html>
-	<head>
-		<title>Run Luan Code</title>
-		<style>
-			body {
-				font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
-				text-align: center;
-				margin-top: 1em;
-			}
-			h2 {
-				margin-bottom: .3em;
-				font-weight: normal;
-			}
-			textarea {
-				font: inherit;
-				border-radius: 4px;
-				padding: .5em .8em;
-			}
-			input[type="submit"] {
-				margin-top: .3em;
-				color: white;
-				background: #337ab7;
-				border-color: #337ab7;
-				font: inherit;
-				padding: .5em;
-				border-radius: 4px;
-			}
-			input[type="submit"]:hover {
-				background: #236aa7 !important;
-			}
-		</style>
-	</head>
-	<body>
-		<h2>Run Luan Code</h2>
-		<form name="form0" method="post">
-			<input type="hidden" name="content_type" value="text/plain" />
-			<div>
-				<textarea name="code" rows="20" cols="90" wrap="off" autofocus></textarea>
-			</div>
-			<div>
-				<input type="submit" value="Execute Luan Code"/>
-			</div>
-		</form>
-	</body>
-</html>
-<% end
-
-return function()
-	local content_type = Http.request.parameter.content_type
-	if content_type ~= nil then
-		Http.response.header.content_type = content_type
-	end
-	Io.stdout = Http.response.text_writer()
-	local code = Http.request.parameter.code
-	if code == nil then
-		form()
-		return
-	end
-	local env = {
-		request = Http.request;
-		response = Http.response;
-	}
-	try {
-		function()
-			local run = load(code,"<web_run>",env)
-			run()
-		end;
-		catch = function(e)
-			Http.response.reset()
-			Http.response.header.content_type = "text/plain"
-			Io.stdout = Http.response.text_writer()
-			print(e)
-			print""
-			print""
-			print_with_line_numbers(code)
-		end;
-	}
-end
diff -r 3e30cf310e56 -r 1a68fc55a80c http/src/luan/modules/http/serve.luan
--- a/http/src/luan/modules/http/serve.luan	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,9 +0,0 @@
-local Io = require "luan:Io.luan"
-local Server = require "luan:http/Server.luan"
-
-if #{...} ~= 1 then
-	Io.stderr.write "usage: luan luan:http/serve dir-URI\n"
-	return
-end
-
-Server.serve(...)
diff -r 3e30cf310e56 -r 1a68fc55a80c http/src/luan/modules/http/shell.luan
--- a/http/src/luan/modules/http/shell.luan	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-return require("luan:http/Shell_mod.luan").respond
diff -r 3e30cf310e56 -r 1a68fc55a80c http/src/luan/modules/http/test.luan
--- a/http/src/luan/modules/http/test.luan	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,12 +0,0 @@
-local Luan = require "luan:Luan.luan"
-local Io = require "luan:Io.luan"
-local Server = require "luan:http/Server.luan"
-
-if #{...} ~= 2 then
-	Io.stderr.write "usage: luan luan:http/serve dir-URI test-URI\n"
-	return
-end
-
-local dir, test = ...
-Server.init(dir)
-Luan.do_file(test)
diff -r 3e30cf310e56 -r 1a68fc55a80c lib/javax.mail.jar
Binary file lib/javax.mail.jar has changed
diff -r 3e30cf310e56 -r 1a68fc55a80c lib/jetty-continuation-8.1.15.v20140411.jar
Binary file lib/jetty-continuation-8.1.15.v20140411.jar has changed
diff -r 3e30cf310e56 -r 1a68fc55a80c lib/jetty-http-8.1.15.v20140411.jar
Binary file lib/jetty-http-8.1.15.v20140411.jar has changed
diff -r 3e30cf310e56 -r 1a68fc55a80c lib/jetty-io-8.1.15.v20140411.jar
Binary file lib/jetty-io-8.1.15.v20140411.jar has changed
diff -r 3e30cf310e56 -r 1a68fc55a80c lib/jetty-server-8.1.15.v20140411.jar
Binary file lib/jetty-server-8.1.15.v20140411.jar has changed
diff -r 3e30cf310e56 -r 1a68fc55a80c lib/jetty-util-8.1.15.v20140411.jar
Binary file lib/jetty-util-8.1.15.v20140411.jar has changed
diff -r 3e30cf310e56 -r 1a68fc55a80c lib/log4j-1.2.17.jar
Binary file lib/log4j-1.2.17.jar has changed
diff -r 3e30cf310e56 -r 1a68fc55a80c lib/lucene-analyzers-common-4.9.0.jar
Binary file lib/lucene-analyzers-common-4.9.0.jar has changed
diff -r 3e30cf310e56 -r 1a68fc55a80c lib/lucene-core-4.9.0.jar
Binary file lib/lucene-core-4.9.0.jar has changed
diff -r 3e30cf310e56 -r 1a68fc55a80c lib/lucene-highlighter-4.9.0.jar
Binary file lib/lucene-highlighter-4.9.0.jar has changed
diff -r 3e30cf310e56 -r 1a68fc55a80c lib/lucene-memory-4.9.0.jar
Binary file lib/lucene-memory-4.9.0.jar has changed
diff -r 3e30cf310e56 -r 1a68fc55a80c lib/lucene-queries-4.9.0.jar
Binary file lib/lucene-queries-4.9.0.jar has changed
diff -r 3e30cf310e56 -r 1a68fc55a80c lib/servlet-api-3.0.jar
Binary file lib/servlet-api-3.0.jar has changed
diff -r 3e30cf310e56 -r 1a68fc55a80c lib/slf4j-api-1.6.4.jar
Binary file lib/slf4j-api-1.6.4.jar has changed
diff -r 3e30cf310e56 -r 1a68fc55a80c lib/slf4j-log4j12-1.6.4.jar
Binary file lib/slf4j-log4j12-1.6.4.jar has changed
diff -r 3e30cf310e56 -r 1a68fc55a80c logging/ext/log4j-1.2.17.jar
Binary file logging/ext/log4j-1.2.17.jar has changed
diff -r 3e30cf310e56 -r 1a68fc55a80c logging/ext/slf4j-api-1.6.4.jar
Binary file logging/ext/slf4j-api-1.6.4.jar has changed
diff -r 3e30cf310e56 -r 1a68fc55a80c logging/ext/slf4j-log4j12-1.6.4.jar
Binary file logging/ext/slf4j-log4j12-1.6.4.jar has changed
diff -r 3e30cf310e56 -r 1a68fc55a80c logging/src/luan/modules/logging/Logging.luan
--- a/logging/src/luan/modules/logging/Logging.luan	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,90 +0,0 @@
-java()
-local Logger = require "java:org.apache.log4j.Logger"
-local EnhancedPatternLayout = require "java:org.apache.log4j.EnhancedPatternLayout"
-local ConsoleAppender = require "java:org.apache.log4j.ConsoleAppender"
-local Level = require "java:org.apache.log4j.Level"
-local RollingFileAppender = require "java:org.apache.log4j.RollingFileAppender"
-local LuanLogger = require "java:luan.modules.logging.LuanLogger"
-
-local M = {}
-
-M.layout = "%d %-5p %c - %m%n"
-
-M.level = "INFO"
-
-M.console = "System.err"  -- or "System.out" or set to nil for no console
-
-M.file = nil  -- set to file name if you want logging to a file
-
-M.max_file_size = nil  -- by default is "10MB"
-
-
-M.log4j_root_logger = Logger.getRootLogger()
-
-local function to_level(level)
-	return level and Level.toLevel(level)
-end
-
-function M.log_to_file(file,logger_name)  -- logger_name is optional, defaults to root logger
-	local appender = RollingFileAppender.new(M.ptn_layout, file)
-	appender.setMaxFileSize(M.max_file_size)
-	local logger = logger_name and Logger.getLogger(logger_name) or M.log4j_root_logger
-	logger.addAppender(appender)
-	return appender
-end
-
-function M.init()
-	M.log4j_root_logger.removeAllAppenders()
-	M.log4j_root_logger.setLevel( to_level(M.level) )
-	M.ptn_layout = EnhancedPatternLayout.new(M.layout)
-
-	if M.console ~= nil then
-		M.console_appender = ConsoleAppender.new(M.ptn_layout,M.console)
-		M.log4j_root_logger.addAppender(M.console_appender)
-	else
-		M.console_appender = nil
-	end
-
-	if M.file ~= nil then
-		M.file_appender = M.log_to_file(M.file)
-	else
-		M.file_appender = nil
-	end
-end
-
-
-local function to_luan_logger(log4j_logger)
-	local tbl = {}
-
-	local luanLogger = LuanLogger.new(log4j_logger)
-	tbl.error = luanLogger.error
-	tbl.warn = luanLogger.warn
-	tbl.info = luanLogger.info
-	tbl.debug = luanLogger.debug
-
-	function tbl.get_level()
-		local level = log4j_logger.getLevel()
-		return level and level.toString()
-	end
-
-	function tbl.get_effective_level()
-		local level = log4j_logger.getEffectiveLevel()
-		return level and level.toString()
-	end
-
-	function tbl.set_level(level)
-		log4j_logger.setLevel( to_level(level) )
-	end
-
-	return tbl
-end
-
-function M.logger(name)
-	return to_luan_logger( Logger.getLogger(name) )
-end
-
-function M.root_logger()
-	return to_luan_logger( Logger.getRootLogger() )
-end
-
-return M
diff -r 3e30cf310e56 -r 1a68fc55a80c logging/src/luan/modules/logging/LuanLogger.java
--- a/logging/src/luan/modules/logging/LuanLogger.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,33 +0,0 @@
-package luan.modules.logging;
-
-import org.apache.log4j.Logger;
-import luan.Luan;
-import luan.LuanState;
-import luan.LuanException;
-import luan.LuanTable;
-
-
-public final class LuanLogger {
-	private final Logger logger;
-
-	public LuanLogger(Logger logger) {
-		this.logger = logger;
-	}
-
-	public void error(LuanState luan,Object obj) throws LuanException {
-		logger.error( luan.toString(obj) );
-	}
-
-	public void warn(LuanState luan,Object obj) throws LuanException {
-		logger.warn( luan.toString(obj) );
-	}
-
-	public void info(LuanState luan,Object obj) throws LuanException {
-		logger.info( luan.toString(obj) );
-	}
-
-	public void debug(LuanState luan,Object obj) throws LuanException {
-		logger.debug( luan.toString(obj) );
-	}
-
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c logging/src/luan/modules/logging/init.luan
--- a/logging/src/luan/modules/logging/init.luan	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,2 +0,0 @@
-require "luan:logging/Logging.luan".init()
-return true
diff -r 3e30cf310e56 -r 1a68fc55a80c lucene/ext/lucene-analyzers-common-4.9.0.jar
Binary file lucene/ext/lucene-analyzers-common-4.9.0.jar has changed
diff -r 3e30cf310e56 -r 1a68fc55a80c lucene/ext/lucene-core-4.9.0.jar
Binary file lucene/ext/lucene-core-4.9.0.jar has changed
diff -r 3e30cf310e56 -r 1a68fc55a80c lucene/ext/lucene-highlighter-4.9.0.jar
Binary file lucene/ext/lucene-highlighter-4.9.0.jar has changed
diff -r 3e30cf310e56 -r 1a68fc55a80c lucene/ext/lucene-memory-4.9.0.jar
Binary file lucene/ext/lucene-memory-4.9.0.jar has changed
diff -r 3e30cf310e56 -r 1a68fc55a80c lucene/ext/lucene-queries-4.9.0.jar
Binary file lucene/ext/lucene-queries-4.9.0.jar has changed
diff -r 3e30cf310e56 -r 1a68fc55a80c lucene/src/luan/modules/lucene/Ab_testing.luan
--- a/lucene/src/luan/modules/lucene/Ab_testing.luan	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,272 +0,0 @@
-local Luan = require "luan:Luan.luan"
-local pairs = Luan.pairs
-local ipairs = Luan.ipairs
-local error = Luan.error
-local range = Luan.range
-local Math = require "luan:Math.luan"
-local Table = require "luan:Table.luan"
-local String = require "luan:String.luan"
-local gsub = String.gsub
-local Io = require "luan:Io.luan"
-local Http = require "luan:http/Http.luan"
-local Logging = require "luan:logging/Logging.luan"
-local Lucene = require "luan:lucene/Lucene.luan"
-
-local M = {}
-
-local logger = Logging.logger "Ab_testing"
-
-function M.of(index)
-
-	local ab_testing = {}
-
-	ab_testing.test_map = {}
-
-	function ab_testing.test(test)
-		test.name or error "name not defined"
-		test.values or error "values not defined"
-
-		-- list of event names
-		test.events or error "events not defined"
-
-		-- map of event name to aggregator factory
-		test.aggregator_factories or error "aggregator_factories not defined"
-
-		-- test.date_field is optional
-
-		local field = "ab_test_" .. test.name
-		index.indexed_fields[field] == nil or error("test "+test.name+" already defined")
-		index.indexed_fields[field] = Lucene.type.string
-		test.field = field
-
-		-- returns map of event name to (map of value to result) and "start_date"
-		function test.results()
-			local results = {}
-			for name in pairs(test.aggregator_factories) do
-				results[name] = {}
-			end
-			local date_field = test.date_field
-			local start_date = nil
-			for _, value in ipairs(test.values) do
-				local aggregators = {}
-				for name, factory in pairs(test.aggregator_factories) do
-					aggregators[name] = factory()
-				end
-				local query = field..":"..value
-				index.advanced_search(query, function(_,doc_fn)
-					local doc = doc_fn()
-					for _, aggregator in pairs(aggregators) do
-						aggregator.aggregate(doc)
-					end
-					if date_field ~= nil then
-						local date = doc[date_field]
-						if date ~= nil and (start_date==nil or start_date > date) then
-							start_date = date
-						end
-					end
-				end)
-				for name, aggregator in pairs(aggregators) do
-					results[name][value] = aggregator.result
-				end
-			end
-			results.start_date = start_date
-			return results
-		end
-
-		function test.fancy_results()
-			local events = test.events
-			local results = test.results()
-			local fancy = {}
-			fancy.start_date = results.start_date
-			local event = events[1]
-			fancy[event] = {}
-			for value, count in pairs(results[event]) do
-				fancy[event][value] = {}
-				fancy[event][value].count = count
-				fancy[event][value].pct_of_total = 100
-				fancy[event][value].pct_of_prev = 100
-			end
-			local all = results[event]
-			local prev = all
-			for i in range(2,#events) do
-				event = events[i]
-				fancy[event] = {}
-				for value, count in pairs(results[event]) do
-					fancy[event][value] = {}
-					fancy[event][value].count = count
-					fancy[event][value].pct_of_total = M.percent(count,all[value])
-					fancy[event][value].pct_of_prev = M.percent(count,prev[value])
-				end
-				prev = results[event]
-			end
-			return fancy
-		end
-
-		ab_testing.test_map[test.name] = test
-
-		return test
-	end
-
-	function ab_testing.value(test_name,values)
-		return values[test_name] or ab_testing.test_map[test_name].values[1]
-	end
-
-	-- returns map from test name to value
-	function ab_testing.from_doc(doc)
-		local values = {}
-		for _, test in pairs(ab_testing.test_map) do
-			values[test.name] = doc[test.field]
-		end
-		return values
-	end
-
-	function ab_testing.to_doc(doc,values,tests)
-		tests = tests or ab_testing.test_map
-		if values == nil then
-			values = {}
-			for _, test in pairs(tests) do
-				values[test.name] = test.values[Math.random(#test.values)]
-			end
-		end
-		for _, test in pairs(tests) do
-			doc[test.field] = values[test.name]
-		end
-		return values
-	end
-
-	function ab_testing.web_page(test_names)
-		return function()
-			local results = {}
-			for _, name in ipairs(test_names) do
-				local test = ab_testing.test_map[name]
-				test or error("test not found: "..name)
-				results[name] = test.fancy_results()
-			end
-			Io.stdout = Http.response.text_writer()
-			M.html(test_names,ab_testing.test_map,results)
-		end
-	end
-
-	return ab_testing
-end
-
-
--- aggregator factories
-
--- fn(doc) should return boolean whether doc should be counted
-function M.count(fn)
-	return function()
-		local aggregator = {}
-		aggregator.result = 0
-		function aggregator.aggregate(doc)
-			if fn(doc) then
-				aggregator.result = aggregator.result + 1
-			end
-		end
-		return aggregator
-	end
-end
-
-M.count_all = M.count( function(doc) return true end )
-
--- fn(doc) should return number to add to result, return 0 for nothing
-function M.sum(fn)
-	return function()
-		local aggregator = {}
-		aggregator.result = 0
-		function aggregator.aggregate(doc)
-			aggregator.result = aggregator.result + fn(doc)
-		end
-		return aggregator
-	end
-end
-
-
-
-function M.percent(x,total)
-	if total==0 then
-		return 0
-	else
-		return 100 * x / total
-	end
-end
-
--- I will change this to use SimplyHTML when this is used again.
-local function basic_style() %>
-	body {font-family:'Arial',sans-serif;font-size:16px;padding:1em 2em}
-	h1 {font-weight:bold;font-size:20px}
-	h2 {margin:2em 0 0em;font-size:18px;color:#3589B1}
-	table.results {margin-top:.5em;border-collapse:collapse;font-size:90%}
-	table.results th {background:#eee}
-	table.results th,table.results td {border-left:1px solid #bbb;padding:.4em 2em}
-	table.results tr:nth-child(odd) td {background:#f8f8f8}
-<% end
-
-local function format(v)
-	v = v .. ''
-	return gsub( v, [[(\d+\.\d{1})\d+]], '$1' )
-end
-
-function M.html(test_names,tests,results) %>
-<!DOCTYPE html>
-<html lang="en">
-	<head>
-		<title>A/B Test Results</title>
-		<style><% basic_style() %></style>
-	</head>
-	<body>
-		<h1>A/B Test Results</h1>
-		<%
-		for _, test_name in ipairs(test_names) do
-			local test = tests[test_name]
-			local result = results[test_name]
-			local n = #test.values
-			%>
-			<h2><%=test_name%></h2>
-			<table class="results">
-				<tr>
-					<th>Event</th>
-					<th class="top" colspan="<%=n%>">Count</th>
-					<th class="top" colspan="<%=n%>">% of total</th>
-					<th class="top" colspan="<%=n%>">% of prev</th>
-				</tr>
-				<tr>
-					<th></th>
-					<%
-					for _ in range(1,3) do
-						for _, value in ipairs(test.values) do
-							%><th class="top"><%=value%></th><%
-						end
-					end
-					%>
-				</tr>
-				<%
-				for _, event in ipairs(test.events) do
-					local event_values = result[event]
-					%>
-					<tr>
-						<td><%=event%></td>
-						<%
-						for _, value in ipairs(test.values) do
-							%><td><%=format(event_values[value].count)%></th><%
-						end
-						for _, value in ipairs(test.values) do
-							%><td><%=format(event_values[value].pct_of_total)%></th><%
-						end
-						for _, value in ipairs(test.values) do
-							%><td><%=format(event_values[value].pct_of_prev)%></th><%
-						end
-						%>
-					</tr>
-					<%
-				end
-				%>
-			</table>
-			<%
-		end
-		%>
-	</body>
-</html>
-<% end
-
-return M
diff -r 3e30cf310e56 -r 1a68fc55a80c lucene/src/luan/modules/lucene/Lucene.luan
--- a/lucene/src/luan/modules/lucene/Lucene.luan	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,161 +0,0 @@
-java()
-local Luan = require "luan:Luan.luan"
-local error = Luan.error
-local ipairs = Luan.ipairs or error()
-local type = Luan.type or error()
-local Html = require "luan:Html.luan"
-local Io = require "luan:Io.luan"
-local uri = Io.uri or error()
-local String = require "luan:String.luan"
-local matches = String.matches or error()
-local Rpc = require "luan:Rpc.luan"
-local LuceneIndex = require "java:luan.modules.lucene.LuceneIndex"
-local NumberFieldParser = require "java:luan.modules.lucene.queryparser.NumberFieldParser"
-local StringFieldParser = require "java:luan.modules.lucene.queryparser.StringFieldParser"
-local SaneQueryParser = require "java:luan.modules.lucene.queryparser.SaneQueryParser"
-local Version = require "java:org.apache.lucene.util.Version"
-local EnglishAnalyzer = require "java:org.apache.lucene.analysis.en.EnglishAnalyzer"
-
-
-local M = {}
-
-M.instances = {}
-
-M.type = {
-	string = LuceneIndex.STRING_FIELD_PARSER;
-	integer = NumberFieldParser.INT;
-	long = NumberFieldParser.LONG;
-	double = NumberFieldParser.DOUBLE;
-
-	english = StringFieldParser.new(EnglishAnalyzer.new(Version.LUCENE_CURRENT))
-}
-
-M.literal = SaneQueryParser.literal
-
-function M.index(index_dir,default_type,default_fields)
-	local index = {}
-	index.dir = index_dir
-	local java_index = LuceneIndex.new(index_dir,default_type,default_fields)
-	index.indexed_fields = java_index.indexedFieldsMeta.newTable()
-
-	-- index.indexed_only_fields[type][field] = fn(doc)
-	index.indexed_only_fields = java_index.indexed_only_fields
-
-	index.to_string = java_index.to_string
---	index.backup = java_index.backup
-	index.snapshot = java_index.snapshot
-	index.advanced_search = java_index.advanced_search
-	index.search_in_transaction = java_index.search_in_transaction
-	index.delete_all = java_index.delete_all
-	index.delete = java_index.delete
-	index.save = java_index.save
-	index.update_in_transaction = java_index.update_in_transaction
---	index.close = java_index.close
-	index.ensure_open = java_index.ensure_open
-	index.next_id = java_index.nextId
-	index.highlighter = java_index.highlighter
-
-	M.instances[index] = true
-
-	function index.close()
-		M.instances[index] = nil
-		java_index.close()
-	end
-
-	function index.search(query, from, to, sort)
-		from or error "missing 'from' parameter"
-		to or error "missing 'to' parameter"
-		local results = {}
-		local function fn(i,doc_fn)
-			if i >= from then
-				results[#results+1] = doc_fn()
-			end
-		end
-		local total_hits = index.advanced_search(query,fn,to,sort)
-		return results, total_hits
-	end
-
-	function index.get_document(query)
-		local doc
-		local function fn(_,doc_fn)
-			doc = doc_fn()
-		end
-		local total_hits = index.advanced_search(query,fn,1)
-		total_hits <= 1 or error( "found " .. total_hits .. " documents" )
-		return doc
-	end
-
-	function index.count(query)
-		return index.advanced_search(query)
-	end
-
-	function index.html_highlighter(query,formatter,container_tags)
-		local highlighter = index.highlighter(query,formatter)
-		return function(html)
-			local list = Html.parse(html,container_tags)
-			local result = {}
-			for _, obj in ipairs(list) do
-				if type(obj) == "string" then
-					obj = highlighter(obj)
-				end
-				result[#result+1] = obj
-			end
-			return Html.to_string(result)
-		end
-	end
-
-	function index.zip(zip_file)
-		index.snapshot( function(dir_path,file_names)
-			zip_file.delete()
-			local zip_path = zip_file.canonical().to_string()
-			local dir = uri("file:"..dir_path)
-			local dir_name = dir.name()
-			local options = {dir=dir.parent()}
-			for _, file_name in ipairs(file_names) do
-				local cmd = "zip "..zip_path.." "..dir_name.."/"..file_name
-				Io.uri("os:"..cmd,options).read_text()
-			end
-		end )
-	end
-
-	function index.restore(zip_file)
-		java_index.run_in_lock( function()
-			local lucene_dir = uri("file:"..index.dir)
-			local before_restore = lucene_dir.parent().child("before_restore.zip")
-			index.zip(before_restore)
-			java_index.close()
-			lucene_dir.delete()
-			Io.uri("os:unzip "..zip_file.canonical().to_string(),{dir=lucene_dir.parent()}).read_text()
-			java_index.reopen()
-		end )
-	end
-
-	local function multi_error()
-		error "multiple lucene instances"
-	end
-
-	if Rpc.functions.backup == nil then
-
-		function Rpc.functions.lucene_backup(password)
-			Io.password == password or error "wrong password"
-			local zip_file = uri("file:"..index.dir).parent().child("backup.zip")
-			index.zip(zip_file)
-			return zip_file
-		end
-
-		function Rpc.functions.lucene_restore(password,zip_file)
-			Io.password == password or error "wrong password"
-			local backup_zip = uri("file:"..index.dir).parent().child("backup.zip")
-			backup_zip.write(zip_file)
-			index.restore(backup_zip)
-		end
-
-	else
-		Rpc.functions.lucene_backup = multi_error
-		Rpc.functions.lucene_restore = multi_error
-	end
-
-	return index
-end
-
-return M
diff -r 3e30cf310e56 -r 1a68fc55a80c lucene/src/luan/modules/lucene/LuceneIndex.java
--- a/lucene/src/luan/modules/lucene/LuceneIndex.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,631 +0,0 @@
-package luan.modules.lucene;
-
-import java.io.Closeable;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.List;
-import java.util.ArrayList;
-import java.util.Set;
-import java.util.HashSet;
-import java.util.Collections;
-import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
-import java.util.zip.ZipOutputStream;
-import java.util.zip.ZipEntry;
-import org.apache.lucene.analysis.Analyzer;
-import org.apache.lucene.analysis.core.KeywordAnalyzer;
-import org.apache.lucene.document.Document;
-import org.apache.lucene.document.Field;
-import org.apache.lucene.document.StoredField;
-import org.apache.lucene.document.StringField;
-import org.apache.lucene.document.TextField;
-import org.apache.lucene.document.IntField;
-import org.apache.lucene.document.LongField;
-import org.apache.lucene.document.DoubleField;
-import org.apache.lucene.index.IndexableField;
-import org.apache.lucene.index.IndexWriter;
-import org.apache.lucene.index.IndexWriterConfig;
-import org.apache.lucene.index.DirectoryReader;
-import org.apache.lucene.index.Term;
-import org.apache.lucene.index.SnapshotDeletionPolicy;
-import org.apache.lucene.index.IndexCommit;
-import org.apache.lucene.index.AtomicReaderContext;
-import org.apache.lucene.store.Directory;
-import org.apache.lucene.store.FSDirectory;
-import org.apache.lucene.util.Version;
-import org.apache.lucene.util.BytesRef;
-import org.apache.lucene.util.NumericUtils;
-import org.apache.lucene.search.Query;
-import org.apache.lucene.search.TermQuery;
-import org.apache.lucene.search.TopDocs;
-import org.apache.lucene.search.Sort;
-import org.apache.lucene.search.SortField;
-import org.apache.lucene.search.IndexSearcher;
-import org.apache.lucene.search.TotalHitCountCollector;
-import org.apache.lucene.search.ScoreDoc;
-import org.apache.lucene.search.Collector;
-import org.apache.lucene.search.Scorer;
-import org.apache.lucene.search.BooleanClause;
-import org.apache.lucene.search.highlight.Formatter;
-import org.apache.lucene.search.highlight.Highlighter;
-import org.apache.lucene.search.highlight.InvalidTokenOffsetsException;
-import org.apache.lucene.search.highlight.NullFragmenter;
-import org.apache.lucene.search.highlight.QueryScorer;
-import org.apache.lucene.search.highlight.TokenGroup;
-import luan.modules.lucene.queryparser.SaneQueryParser;
-import luan.modules.lucene.queryparser.FieldParser;
-import luan.modules.lucene.queryparser.MultiFieldParser;
-import luan.modules.lucene.queryparser.StringFieldParser;
-import luan.modules.lucene.queryparser.NumberFieldParser;
-import luan.modules.lucene.queryparser.ParseException;
-import luan.modules.Utils;
-import luan.Luan;
-import luan.LuanState;
-import luan.LuanTable;
-import luan.LuanFunction;
-import luan.LuanException;
-import luan.LuanMeta;
-import luan.LuanRuntimeException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-
-public final class LuceneIndex implements Closeable {
-	private static final Logger logger = LoggerFactory.getLogger(LuceneIndex.class);
-
-	private static final String FLD_NEXT_ID = "nextId";
-	public static final StringFieldParser STRING_FIELD_PARSER = new StringFieldParser(new KeywordAnalyzer());
-
-	private static final Version version = Version.LUCENE_4_9;
-	private final ReentrantLock writeLock = new ReentrantLock();
-	private final File indexDir;
-	private SnapshotDeletionPolicy snapshotDeletionPolicy;
-	private IndexWriter writer;
-	private DirectoryReader reader;
-	private IndexSearcher searcher;
-	private final ThreadLocal<IndexSearcher> threadLocalSearcher = new ThreadLocal<IndexSearcher>();
-	private boolean isClosed = true;
-	private final MultiFieldParser mfp;
-	public final LuanTable indexed_only_fields = new LuanTable();
-	private final Analyzer analyzer;
-
-	private static ConcurrentMap<File,AtomicInteger> globalWriteCounters = new ConcurrentHashMap<File,AtomicInteger>();
-	private File fileDir;
-	private int writeCount;
-
-	public LuceneIndex(LuanState luan,String indexDirStr,FieldParser defaultFieldParser,String[] defaultFields) throws LuanException, IOException {
-		mfp = defaultFieldParser==null ? new MultiFieldParser() : new MultiFieldParser(defaultFieldParser,defaultFields);
-		mfp.fields.put( "type", STRING_FIELD_PARSER );
-		mfp.fields.put( "id", NumberFieldParser.LONG );
-		File indexDir = new File(indexDirStr);
-		this.indexDir = indexDir;
-		Analyzer analyzer = STRING_FIELD_PARSER.analyzer;
-		if( defaultFieldParser instanceof StringFieldParser ) {
-			StringFieldParser sfp = (StringFieldParser)defaultFieldParser;
-			analyzer = sfp.analyzer;
-		}
-		this.analyzer = analyzer;
-		luan.onClose(this);
-		reopen();
-	}
-
-	public void reopen() throws LuanException, IOException {
-		if( !isClosed )  throw new RuntimeException();
-		isClosed = false;
-		IndexWriterConfig conf = new IndexWriterConfig(version,analyzer);
-		snapshotDeletionPolicy = new SnapshotDeletionPolicy(conf.getIndexDeletionPolicy());
-		conf.setIndexDeletionPolicy(snapshotDeletionPolicy);
-		FSDirectory dir = FSDirectory.open(indexDir);
-		fileDir = dir.getDirectory();
-		globalWriteCounters.putIfAbsent(fileDir,new AtomicInteger());
-		writer = new IndexWriter(dir,conf);
-		writer.commit();  // commit index creation
-		reader = DirectoryReader.open(dir);
-		searcher = new IndexSearcher(reader);
-		initId();
-	}
-
-	private int globalWriteCount() {
-		return globalWriteCounters.get(fileDir).get();
-	}
-
-	private void wrote() {
-		globalWriteCounters.get(fileDir).incrementAndGet();
-	}
-
-	public void delete_all() throws IOException {
-		boolean commit = !writeLock.isHeldByCurrentThread();
-		writeLock.lock();
-		try {
-			writer.deleteAll();
-			id = idLim = 0;
-			if(commit) writer.commit();
-		} finally {
-			wrote();
-			writeLock.unlock();
-		}
-	}
-
-	private static Term term(String key,long value) {
-		BytesRef br = new BytesRef();
-		NumericUtils.longToPrefixCoded(value,0,br);
-		return new Term(key,br);
-	}
-
-	public void delete(LuanState luan,String queryStr) throws LuanException, IOException, ParseException {
-		Query query = SaneQueryParser.parseQuery(mfp,queryStr);
-
-		boolean commit = !writeLock.isHeldByCurrentThread();
-		writeLock.lock();
-		try {
-			writer.deleteDocuments(query);
-			if(commit) writer.commit();
-		} finally {
-			wrote();
-			writeLock.unlock();
-		}
-	}
-
-	public void save(LuanState luan,LuanTable doc) throws LuanException, IOException {
-		Set indexedOnlySet = new HashSet();
-		Object typeObj = doc.get(luan,"type");
-		if( typeObj==null )
-			throw new LuanException("missing 'type' field");
-		if( !(typeObj instanceof String) )
-			throw new LuanException("type must be string");
-		String type = (String)typeObj;
-		Object indexedOnlyObj = indexed_only_fields.get(luan,type);
-		if( indexedOnlyObj != null ) {
-			if( !(indexedOnlyObj instanceof LuanTable) )
-				throw new LuanException("indexed_only_fields elements must be tables");
-			LuanTable indexedOnly = (LuanTable)indexedOnlyObj;
-			for( Map.Entry<Object,Object> entry : indexedOnly.iterable(luan) ) {
-				Object key = entry.getKey();
-				if( !(key instanceof String) )
-					throw new LuanException("indexed_only_fields."+type+" entries must be strings");
-				String name = (String)key;
-				Object value = entry.getValue();
-				if( !(value instanceof LuanFunction) )
-					throw new LuanException("indexed_only_fields."+type+" values must be functions");
-				LuanFunction fn = (LuanFunction)value;
-				value = Luan.first(fn.call(luan,new Object[]{doc}));
-				doc.put(luan, name, value );
-				indexedOnlySet.add(name);
-			}
-		}
-		Object obj = doc.get(luan,"id");
-		Long id;
-		try {
-			id = (Long)obj;
-		} catch(ClassCastException e) {
-			throw new LuanException("id should be Long but is "+obj.getClass().getSimpleName());
-		}
-
-		boolean commit = !writeLock.isHeldByCurrentThread();
-		writeLock.lock();
-		try {
-			if( id == null ) {
-				id = nextId(luan);
-				doc.put(luan,"id",id);
-				writer.addDocument(toLucene(luan,doc,indexedOnlySet));
-			} else {
-				writer.updateDocument( term("id",id), toLucene(luan,doc,indexedOnlySet) );
-			}
-			if(commit) writer.commit();
-		} finally {
-			wrote();
-			writeLock.unlock();
-		}
-	}
-
-	public void update_in_transaction(LuanState luan,LuanFunction fn) throws IOException, LuanException {
-		boolean commit = !writeLock.isHeldByCurrentThread();
-		writeLock.lock();
-		try {
-			fn.call(luan);
-			if(commit) writer.commit();
-		} finally {
-			wrote();
-			writeLock.unlock();
-		}
-	}
-
-	public void run_in_lock(LuanState luan,LuanFunction fn) throws IOException, LuanException {
-		if( writeLock.isHeldByCurrentThread() )
-			throw new RuntimeException();
-		writeLock.lock();
-		try {
-			synchronized(this) {
-				fn.call(luan);
-			}
-		} finally {
-			wrote();
-			writeLock.unlock();
-		}
-	}
-
-
-	private long id;
-	private long idLim;
-	private final int idBatch = 10;
-
-	private void initId() throws LuanException, IOException {
-		TopDocs td = searcher.search(new TermQuery(new Term("type","next_id")),1);
-		switch(td.totalHits) {
-		case 0:
-			id = 0;
-			idLim = 0;
-			break;
-		case 1:
-			idLim = (Long)searcher.doc(td.scoreDocs[0].doc).getField(FLD_NEXT_ID).numericValue();
-			id = idLim;
-			break;
-		default:
-			throw new RuntimeException();
-		}
-	}
-
-	public synchronized long nextId(LuanState luan) throws LuanException, IOException {
-		if( ++id > idLim ) {
-			idLim += idBatch;
-			LuanTable doc = new LuanTable();
-			doc.rawPut( "type", "next_id" );
-			doc.rawPut( FLD_NEXT_ID, idLim );
-			writer.updateDocument(new Term("type","next_id"),toLucene(luan,doc,Collections.EMPTY_SET));
-			wrote();
-		}
-		return id;
-	}
-
-/*
-	public void backup(String zipFile) throws LuanException, IOException {
-		if( !zipFile.endsWith(".zip") )
-			throw new LuanException("file "+zipFile+" doesn't end with '.zip'");
-		IndexCommit ic = snapshotDeletionPolicy.snapshot();
-		try {
-			ZipOutputStream out = new ZipOutputStream(new FileOutputStream(zipFile));
-			for( String fileName : ic.getFileNames() ) {
-				out.putNextEntry(new ZipEntry(fileName));
-				FileInputStream in = new FileInputStream(new File(indexDir,fileName));
-				Utils.copyAll(in,out);
-				in.close();
-				out.closeEntry();
-			}
-			out.close();
-		} finally {
-			snapshotDeletionPolicy.release(ic);
-		}
-	}
-*/
-	public Object snapshot(LuanState luan,LuanFunction fn) throws LuanException, IOException {
-		IndexCommit ic = snapshotDeletionPolicy.snapshot();
-		try {
-			String dir = fileDir.toString();
-			LuanTable fileNames = new LuanTable(new ArrayList(ic.getFileNames()));
-			return fn.call(luan,new Object[]{dir,fileNames});
-		} finally {
-			snapshotDeletionPolicy.release(ic);
-		}
-	}
-
-
-
-	public String to_string() {
-		return writer.getDirectory().toString();
-	}
-
-	public void close() throws IOException {
-		if( !isClosed ) {
-			writer.close();
-			reader.close();
-			isClosed = true;
-		}
-	}
-
-	protected void finalize() throws Throwable {
-		if( !isClosed ) {
-			logger.error("not closed");
-			close();
-		}
-		super.finalize();
-	}
-
-
-
-	private static class DocFn extends LuanFunction {
-		final IndexSearcher searcher;
-		int docID;
-
-		DocFn(IndexSearcher searcher) {
-			this.searcher = searcher;
-		}
-
-		@Override public Object call(LuanState luan,Object[] args) throws LuanException {
-			try {
-				return toTable(searcher.doc(docID));
-			} catch(IOException e) {
-				throw new LuanException(e);
-			}
-		}
-	}
-
-	private static abstract class MyCollector extends Collector {
-		int docBase;
-		int i = 0;
-
-		@Override public void setScorer(Scorer scorer) {}
-		@Override public void setNextReader(AtomicReaderContext context) {
-			this.docBase = context.docBase;
-		}
-		@Override public boolean acceptsDocsOutOfOrder() {
-			return true;
-		}
-	}
-
-	private synchronized IndexSearcher openSearcher() throws IOException {
-		int gwc = globalWriteCount();
-		if( writeCount != gwc ) {
-			writeCount = gwc;
-			DirectoryReader newReader = DirectoryReader.openIfChanged(reader);
-			if( newReader != null ) {
-				reader.decRef();
-				reader = newReader;
-				searcher = new IndexSearcher(reader);
-			}
-		}
-		reader.incRef();
-		return searcher;
-	}
-
-	// call in finally block
-	private static void close(IndexSearcher searcher) throws IOException {
-		searcher.getIndexReader().decRef();
-	}
-
-	public void ensure_open() throws IOException {
-		close(openSearcher());
-	}
-
-	public int advanced_search( final LuanState luan, String queryStr, LuanFunction fn, Integer n, String sortStr ) throws LuanException, IOException, ParseException {
-		Utils.checkNotNull(queryStr);
-		Query query = SaneQueryParser.parseQuery(mfp,queryStr);
-		IndexSearcher searcher = threadLocalSearcher.get();
-		boolean inTransaction = searcher != null;
-		if( !inTransaction )
-			searcher = openSearcher();
-		try {
-			if( fn!=null && n==null ) {
-				if( sortStr != null )
-					throw new LuanException("sort must be nil when n is nil");
-				final DocFn docFn = new DocFn(searcher);
-				MyCollector col = new MyCollector() {
-					@Override public void collect(int doc) {
-						try {
-							docFn.docID = docBase + doc;
-							fn.call(luan,new Object[]{++i,docFn});
-						} catch(LuanException e) {
-							throw new LuanRuntimeException(e);
-						}
-					}
-				};
-				try {
-					searcher.search(query,col);
-				} catch(LuanRuntimeException e) {
-					throw (LuanException)e.getCause();
-				}
-				return col.i;
-			}
-			if( fn==null || n==0 ) {
-				TotalHitCountCollector thcc = new TotalHitCountCollector();
-				searcher.search(query,thcc);
-				return thcc.getTotalHits();
-			}
-			Sort sort = sortStr==null ? null : SaneQueryParser.parseSort(mfp,sortStr);
-			TopDocs td = sort==null ? searcher.search(query,n) : searcher.search(query,n,sort);
-			final ScoreDoc[] scoreDocs = td.scoreDocs;
-			DocFn docFn = new DocFn(searcher);
-			for( int i=0; i<scoreDocs.length; i++ ) {
-				docFn.docID = scoreDocs[i].doc;
-				fn.call(luan,new Object[]{i+1,docFn});
-			}
-			return td.totalHits;
-		} finally {
-			if( !inTransaction )
-				close(searcher);
-		}
-	}
-
-	public Object search_in_transaction(LuanState luan,LuanFunction fn) throws LuanException, IOException {
-		if( threadLocalSearcher.get() != null )
-			throw new LuanException("can't nest search_in_transaction calls");
-		IndexSearcher searcher = openSearcher();
-		threadLocalSearcher.set(searcher);
-		try {
-			return fn.call(luan);
-		} finally {
-			threadLocalSearcher.set(null);
-			close(searcher);
-		}
-	}
-
-
-
-	public final LuanMeta indexedFieldsMeta = new LuanMeta() {
-
-		@Override public boolean canNewindex() {
-			return true;
-		}
-
-		@Override public Object __index(LuanState luan,LuanTable tbl,Object key) {
-			return mfp.fields.get(key);
-		}
-
-		@Override public void __new_index(LuanState luan,LuanTable tbl,Object key,Object value) throws LuanException {
-			if( !(key instanceof String) )
-				throw new LuanException("key must be string");
-			String field = (String)key;
-			if( value==null ) {  // delete
-				mfp.fields.remove(field);
-				return;
-			}
-			if( !(value instanceof FieldParser) )
-				throw new LuanException("value must be FieldParser like the values of Lucene.type");
-			FieldParser parser = (FieldParser)value;
-			mfp.fields.put( field, parser );
-		}
-
-		@Override public final Iterator keys(LuanTable tbl) {
-			return mfp.fields.keySet().iterator();
-		}
-
-		@Override protected String type(LuanTable tbl) {
-			return "lucene-indexed-fields";
-		}
-
-	};
-
-
-
-	private IndexableField newField(String name,Object value,Field.Store store,Set<String> indexed)
-		throws LuanException
-	{
-		if( value instanceof String ) {
-			String s = (String)value;
-			FieldParser fp = mfp.fields.get(name);
-			if( fp != null ) {
-				if( fp instanceof StringFieldParser && fp != STRING_FIELD_PARSER ) {
-					return new TextField(name, s, store);
-				} else {
-					return new StringField(name, s, store);
-				}
-			} else {
-				return new StoredField(name, s);
-			}
-		} else if( value instanceof Integer ) {
-			int i = (Integer)value;
-			if( indexed.contains(name) ) {
-				return new IntField(name, i, store);
-			} else {
-				return new StoredField(name, i);
-			}
-		} else if( value instanceof Long ) {
-			long i = (Long)value;
-			if( indexed.contains(name) ) {
-				return new LongField(name, i, store);
-			} else {
-				return new StoredField(name, i);
-			}
-		} else if( value instanceof Double ) {
-			double i = (Double)value;
-			if( indexed.contains(name) ) {
-				return new DoubleField(name, i, store);
-			} else {
-				return new StoredField(name, i);
-			}
-		} else if( value instanceof byte[] ) {
-			byte[] b = (byte[])value;
-			return new StoredField(name, b);
-		} else
-			throw new LuanException("invalid value type "+value.getClass()+"' for '"+name+"'");
-	}
-
-	private Document toLucene(LuanState luan,LuanTable table,Set indexOnly) throws LuanException {
-		Set<String> indexed = mfp.fields.keySet();
-		Document doc = new Document();
-		for( Map.Entry<Object,Object> entry : table.iterable(luan) ) {
-			Object key = entry.getKey();
-			if( !(key instanceof String) )
-				throw new LuanException("key must be string");
-			String name = (String)key;
-			Object value = entry.getValue();
-			Field.Store store = indexOnly.contains(name) ? Field.Store.NO : Field.Store.YES;
-			if( !(value instanceof LuanTable) ) {
-				doc.add(newField(name, value, store, indexed));
-			} else { // list
-				LuanTable list = (LuanTable)value;
-				for( Object el : list.asList() ) {
-					doc.add(newField(name, el, store, indexed));
-				}
-			}
-		}
-		return doc;
-	}
-
-	private static Object getValue(IndexableField ifld) throws LuanException {
-		BytesRef br = ifld.binaryValue();
-		if( br != null )
-			return br.bytes;
-		Number n = ifld.numericValue();
-		if( n != null )
-			return n;
-		String s = ifld.stringValue();
-		if( s != null )
-			return s;
-		throw new LuanException("invalid field type for "+ifld);
-	}
-
-	private static LuanTable toTable(Document doc) throws LuanException {
-		if( doc==null )
-			return null;
-		LuanTable table = new LuanTable();
-		for( IndexableField ifld : doc ) {
-			String name = ifld.name();
-			Object value = getValue(ifld);
-			Object old = table.rawGet(name);
-			if( old == null ) {
-				table.rawPut(name,value);
-			} else {
-				LuanTable list;
-				if( old instanceof LuanTable ) {
-					list = (LuanTable)old;
-				} else {
-					list = new LuanTable();
-					list.rawPut(1,old);
-					table.rawPut(name,list);
-				}
-				list.rawPut(list.rawLength()+1,value);
-			}
-		}
-		return table;
-	}
-
-
-	public LuanFunction highlighter(LuanState luan,String queryStr,LuanFunction formatter) throws ParseException {
-		Query query = SaneQueryParser.parseQuery(mfp,queryStr);
-		Formatter fmt = new Formatter() {
-			public String highlightTerm(String originalText,TokenGroup tokenGroup) {
-				if( tokenGroup.getTotalScore() <= 0 )
-					return originalText;
-				try {
-					return (String)Luan.first(formatter.call(luan,new Object[]{originalText}));
-				} catch(LuanException e) {
-					throw new LuanRuntimeException(e);
-				}
-			}
-		};
-		Highlighter hl = new Highlighter( fmt, new QueryScorer(query) );
-		hl.setTextFragmenter( new NullFragmenter() );
-		return new LuanFunction() {
-			@Override public String call(LuanState luan,Object[] args) throws LuanException {
-				String text = (String)args[0];
-				try {
-					String s = hl.getBestFragment(analyzer,null,text);
-					return s!=null ? s : text;
-				} catch(LuanRuntimeException e) {
-					throw (LuanException)e.getCause();
-				} catch(IOException e) {
-					throw new RuntimeException(e);
-				} catch(InvalidTokenOffsetsException e) {
-					throw new RuntimeException(e);
-				}
-			}
-		};
-	}
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c lucene/src/luan/modules/lucene/Versioning.luan
--- a/lucene/src/luan/modules/lucene/Versioning.luan	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,56 +0,0 @@
-local Luan = require "luan:Luan.luan"
-local error = Luan.error
-local pairs = Luan.pairs or error()
-local Number = require "luan:Number.luan"
-local integer = Number.integer or error()
-local long = Number.long or error()
-local String = require "luan:String.luan"
-local matches = String.matches or error()
-local sub = String.sub or error()
-local string_to_number = String.to_number or error()
-local Table = require "luan:Table.luan"
-local copy = Table.copy or error()
-local Lucene = require "luan:lucene/Lucene.luan"
-require "luan:logging/init.luan"
-local Logging = require "luan:logging/Logging.luan"
-
-local logger = Logging.logger "lucene versioning"
-
-
-local M = {}
-
-function M.update(db,steps,version)
-	local doc = db.get_document"type:version" or { type="version", version=integer(0) }
-	while doc.version < version do
-		doc.version = integer(doc.version + 1)
-		logger.error("step "..doc.version)
-		db.update_in_transaction( function()
-			local step = steps[doc.version]
-			step and step(db)
-			db.save(doc)
-		end )
-	end
-end
-
-
--- hack to deal with latest changes
-function M.a_big_step(db)
-	db.indexed_fields["id index"] = Lucene.type.string
-	db.advanced_search( Lucene.literal"id index" .. ":*", function(_,doc_fn)
-		local doc = doc_fn()
-		for field, value in pairs(copy(doc)) do
-			if matches(field," index$") then
-				local new_field = sub(field,1,-7)
-				db.indexed_fields[new_field] or error("field '"..new_field.."' not indexed")
-				doc[new_field] = value
-				doc[field] = nil
-			end
-		end
-		doc.id = long(string_to_number(doc.id))
-		db.save(doc)
-	end )
-	db.indexed_fields["type index"] = Lucene.type.string
-	db.delete( Lucene.literal"type index" .. ":*" )
-end
-
-return M
diff -r 3e30cf310e56 -r 1a68fc55a80c lucene/src/luan/modules/lucene/Web_search.luan
--- a/lucene/src/luan/modules/lucene/Web_search.luan	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,189 +0,0 @@
- local Luan = require "luan:Luan.luan"
-local error = Luan.error
-local pairs = Luan.pairs or error()
-local ipairs = Luan.ipairs or error()
-local range = Luan.range or error()
-local to_string = Luan.to_string or error()
-local Io = require "luan:Io.luan"
-local repr = Io.repr or error()
-local Http = require "luan:http/Http.luan"
-local String = require "luan:String.luan"
-local string_to_number = String.to_number or error()
-local Html = require "luan:Html.luan"
-
-
-local M = {}
-
-local function style() %>
-			body {
-				font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
-				margin: 2em 5%;
-			}
-			h2 {
-				margin-bottom: .5em;
-			}
-			label {
-				text-align: right;
-				min-width: 6em;
-				display: inline-block;
-				margin-right: .5em;
-			}
-<%
-end
-
-local function form() %>
-<html>
-	<head>
-		<title>Lucene Query</title>
-		<style>
-			<% style() %>
-			input {
-				margin-top: 1em;
-			}
-			input[type="text"] {
-				font: inherit;
-				padding: .5em .8em;
-				border-radius: 8px;
-				border-style: groove;
-			}
-			input[type="text"]:focus {
-				border-color: #66afe9;
-				outline: none;
-			}
-			span[tip] {
-				color: #888;
-				font-size: smaller;
-				margin-left: .5em;
-			}
-			input[type="submit"] {
-				color: white;
-				background: #337ab7;
-				border-color: #337ab7;
-				font: inherit;
-				padding: .5em;
-				border-radius: 4px;
-			}
-			input[type="submit"]:hover {
-				background: #236aa7 !important;
-			}
-		</style>
-	</head>
-	<body>
-		<h2>Lucene Query</h2>
-		<form horizontal name="form0" method="post">
-			<div>
-				<label>Query:</label>
-				<input type=text name="query" size="80" autofocus />
-			</div>
-			<div>
-				<label></label>
-				<span tip>Query examples: <i>type:user</i> or <i>+type:user +name:Joe"</i></span>
-			</div>
-			<div>
-				<label>Max Rows:</label>
-				<input type=text name="rows" value="100" size="3" maxlength="5" />
-			</div>
-			<div>
-				<label>Sort:</label>
-				<input type=text name="sort" size="60" />
-			</div>
-			<div>
-				<label></label>
-				<span tip>Sort examples: <i>name, id</i></span>
-			</div>
-			<div>
-				<label></label>
-				<input type="submit" />
-			</div>
-		</form>
-	</body>
-</html>
-<% end
-
-
-local function result(query,sort,headers,table) %>
-<html>
-	<head>
-		<title>Lucene Query</title>
-		<style>
-			<% style() %>
-			table {
-				border-collapse: collapse;
-				width: 100%;
-				font-size: smaller;
-			}
-			th, td {
-				text-align: left;
-				padding: .5em;
-				border: solid 1px #ddd;
-			}
-		</style>
-	</head>
-	<body>
-		<h2>Lucene Query Results</h2>
-		<p><label>Query:</label> <b><%=Html.encode(to_string(query))%></b></p>
-		<p><label>Sort:</label> <b><%=Html.encode(to_string(sort))%></b></p>
-		<table>
-			<tr>
-				<th></th>
-				<% for _, header in ipairs(headers) do %>
-					<th><%=header%></th>
-				<% end %>
-			</tr>
-			<% for i, row in ipairs(table) do %>
-				<tr>
-					<td><%=i%></td>
-					<%
-					for col in range(1, #headers) do
-						local val = row[col]
-						%><td><%= val and repr(val) or "" %></td><%
-					end
-					%>
-				</tr>
-			<% end %>
-		</table>
-	</body>
-</html>
-<% end
-
-
-local function index_of(tbl,val)
-	for i, v in ipairs(tbl) do
-		if v == val then
-			return i
-		end
-	end
-	local n = #tbl + 1
-	tbl[n] = val
-	return n
-end
-
-
-function M.of(index)
-	index or error "index is nil"
-
-	return function()
-		Io.stdout = Http.response.text_writer()
-		local query = Http.request.parameter.query
-		if query == nil then
-			form()
-			return
-		end
-		local rows = string_to_number(Http.request.parameter.rows)
-		local sort = Http.request.parameter.sort
-		local results = index.search(query,1,rows,sort)
-		local headers = {}
-		local table = {}
-		for _, doc in ipairs(results) do
-			local row = {}
-			for field, value in pairs(doc) do
-				row[index_of(headers,field)] = value
-			end
-			table[#table+1] = row
-		end
-		result(query,sort,headers,table)
-	end
-
-end
-
-return M
diff -r 3e30cf310e56 -r 1a68fc55a80c lucene/src/luan/modules/lucene/queryparser/FieldParser.java
--- a/lucene/src/luan/modules/lucene/queryparser/FieldParser.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,11 +0,0 @@
-package luan.modules.lucene.queryparser;
-
-import org.apache.lucene.search.Query;
-import org.apache.lucene.search.SortField;
-
-
-public interface FieldParser {
-	public Query getQuery(SaneQueryParser qp,String field,String query) throws ParseException;
-	public Query getRangeQuery(SaneQueryParser qp,String field,String minQuery,String maxQuery,boolean includeMin,boolean includeMax) throws ParseException;
-	public SortField getSortField(SaneQueryParser qp,String field,boolean reverse) throws ParseException;
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c lucene/src/luan/modules/lucene/queryparser/MultiFieldParser.java
--- a/lucene/src/luan/modules/lucene/queryparser/MultiFieldParser.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,85 +0,0 @@
-package luan.modules.lucene.queryparser;
-
-import java.util.Map;
-import java.util.HashMap;
-import org.apache.lucene.search.Query;
-import org.apache.lucene.search.BooleanQuery;
-import org.apache.lucene.search.BooleanClause;
-import org.apache.lucene.search.SortField;
-
-
-public class MultiFieldParser implements FieldParser {
-
-	/**
-	 * maps field name to FieldParser
-	 */
-	public final Map<String,FieldParser> fields = new HashMap<String,FieldParser>();
-	public boolean allowUnspecifiedFields = false;
-	private final FieldParser defaultFieldParser;
-	private final String[] defaultFields;
-
-	public MultiFieldParser() {
-		this.defaultFieldParser = null;
-		this.defaultFields = null;
-	}
-
-	public MultiFieldParser(FieldParser defaultFieldParser,String... defaultFields) {
-		this.defaultFieldParser = defaultFieldParser;
-		this.defaultFields = defaultFields;
-		for( String field : defaultFields ) {
-			fields.put(field,defaultFieldParser);
-		}
-	}
-
-	@Override public Query getQuery(SaneQueryParser qp,String field,String query) throws ParseException {
-		if( field == null ) {
-			if( defaultFieldParser == null )
-				throw new ParseException(qp,"no defaults were specified, so a field is required");
-			if( defaultFields.length == 1 )
-				return defaultFieldParser.getQuery(qp,defaultFields[0],query);
-			BooleanQuery bq = new BooleanQuery();
-			for( String f : defaultFields ) {
-				bq.add( defaultFieldParser.getQuery(qp,f,query), BooleanClause.Occur.SHOULD );
-			}
-			return bq;
-		} else {
-			FieldParser fp = fields.get(field);
-			if( fp != null )
-				return fp.getQuery(qp,field,query);
-			if( allowUnspecifiedFields )
-				return defaultFieldParser.getQuery(qp,field,query);
-			throw new ParseException(qp,"unrecognized field '"+field+"'");
-		}
-	}
-
-	@Override public Query getRangeQuery(SaneQueryParser qp,String field,String minQuery,String maxQuery,boolean includeMin,boolean includeMax) throws ParseException {
-		if( field == null ) {
-			if( defaultFieldParser == null )
-				throw new ParseException(qp,"no defaults were specified, so a field is required");
-			if( defaultFields.length == 1 )
-				return defaultFieldParser.getRangeQuery(qp,defaultFields[0],minQuery,maxQuery,includeMin,includeMax);
-			BooleanQuery bq = new BooleanQuery();
-			for( String f : defaultFields ) {
-				bq.add( defaultFieldParser.getRangeQuery(qp,f,minQuery,maxQuery,includeMin,includeMax), BooleanClause.Occur.SHOULD );
-			}
-			return bq;
-		} else {
-			FieldParser fp = fields.get(field);
-			if( fp != null )
-				return fp.getRangeQuery(qp,field,minQuery,maxQuery,includeMin,includeMax);
-			if( allowUnspecifiedFields )
-				return defaultFieldParser.getRangeQuery(qp,field,minQuery,maxQuery,includeMin,includeMax);
-			throw new ParseException(qp,"field '"+field+"' not specified");
-		}
-	}
-
-	@Override public SortField getSortField(SaneQueryParser qp,String field,boolean reverse) throws ParseException {
-		FieldParser fp = fields.get(field);
-		if( fp != null )
-			return fp.getSortField(qp,field,reverse);
-		if( allowUnspecifiedFields )
-			return defaultFieldParser.getSortField(qp,field,reverse);
-		throw new ParseException(qp,"field '"+field+"' not specified");
-	}
-
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c lucene/src/luan/modules/lucene/queryparser/NumberFieldParser.java
--- a/lucene/src/luan/modules/lucene/queryparser/NumberFieldParser.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,83 +0,0 @@
-package luan.modules.lucene.queryparser;
-
-import org.apache.lucene.search.Query;
-import org.apache.lucene.search.NumericRangeQuery;
-import org.apache.lucene.search.SortField;
-
-
-public abstract class NumberFieldParser implements FieldParser {
-
-	@Override public final Query getQuery(SaneQueryParser qp,String field,String query) throws ParseException {
-		return getRangeQuery(qp,field,query,query,true,true);
-	}
-
-	@Override public final Query getRangeQuery(SaneQueryParser qp,String field,String minQuery,String maxQuery,boolean includeMin,boolean includeMax) throws ParseException {
-		try {
-			return getRangeQuery(field,minQuery,maxQuery,includeMin,includeMax);
-		} catch(NumberFormatException e) {
-			throw new ParseException(qp,e);
-		}
-	}
-
-	abstract protected Query getRangeQuery(String field,String minQuery,String maxQuery,boolean includeMin,boolean includeMax);
-
-	@Override public SortField getSortField(SaneQueryParser qp,String field,boolean reverse) {
-		return new SortField( field, sortType(), reverse );
-	}
-
-	abstract protected SortField.Type sortType();
-
-
-	public static final FieldParser INT = new NumberFieldParser() {
-
-		@Override protected Query getRangeQuery(String field,String minQuery,String maxQuery,boolean includeMin,boolean includeMax) {
-			int min = Integer.parseInt(minQuery);
-			int max = Integer.parseInt(maxQuery);
-			return NumericRangeQuery.newIntRange(field,min,max,includeMin,includeMax);
-		}
-
-		@Override protected SortField.Type sortType() {
-			return SortField.Type.INT;
-		}
-	};
-
-	public static final FieldParser LONG = new NumberFieldParser() {
-
-		@Override protected Query getRangeQuery(String field,String minQuery,String maxQuery,boolean includeMin,boolean includeMax) {
-			long min = Long.parseLong(minQuery);
-			long max = Long.parseLong(maxQuery);
-			return NumericRangeQuery.newLongRange(field,min,max,includeMin,includeMax);
-		}
-
-		@Override protected SortField.Type sortType() {
-			return SortField.Type.LONG;
-		}
-	};
-
-	public static final FieldParser FLOAT = new NumberFieldParser() {
-
-		@Override protected Query getRangeQuery(String field,String minQuery,String maxQuery,boolean includeMin,boolean includeMax) {
-			float min = Float.parseFloat(minQuery);
-			float max = Float.parseFloat(maxQuery);
-			return NumericRangeQuery.newFloatRange(field,min,max,includeMin,includeMax);
-		}
-
-		@Override protected SortField.Type sortType() {
-			return SortField.Type.FLOAT;
-		}
-	};
-
-	public static final FieldParser DOUBLE = new NumberFieldParser() {
-
-		@Override protected Query getRangeQuery(String field,String minQuery,String maxQuery,boolean includeMin,boolean includeMax) {
-			double min = Double.parseDouble(minQuery);
-			double max = Double.parseDouble(maxQuery);
-			return NumericRangeQuery.newDoubleRange(field,min,max,includeMin,includeMax);
-		}
-
-		@Override protected SortField.Type sortType() {
-			return SortField.Type.DOUBLE;
-		}
-	};
-
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c lucene/src/luan/modules/lucene/queryparser/ParseException.java
--- a/lucene/src/luan/modules/lucene/queryparser/ParseException.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,33 +0,0 @@
-package luan.modules.lucene.queryparser;
-
-
-public final class ParseException extends Exception {
-	public final String text;
-	public final int errorIndex;
-	public final int highIndex;
-
-	public ParseException(SaneQueryParser qp) {
-		this(qp,"Invalid input");
-	}
-
-	public ParseException(SaneQueryParser qp,String msg) {
-		super(msg+" at position "+(qp.parser.errorIndex()+1));
-		Parser parser = qp.parser;
-		this.text = parser.text;
-		this.errorIndex = parser.errorIndex();
-		this.highIndex = parser.highIndex();
-	}
-
-	public ParseException(SaneQueryParser qp,Exception cause) {
-		this(qp,cause.getMessage(),cause);
-	}
-
-	public ParseException(SaneQueryParser qp,String msg,Exception cause) {
-		super(msg+" at position "+(qp.parser.errorIndex()+1),cause);
-		Parser parser = qp.parser;
-		this.text = parser.text;
-		this.errorIndex = parser.errorIndex();
-		this.highIndex = parser.highIndex();
-	}
-
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c lucene/src/luan/modules/lucene/queryparser/Parser.java
--- a/lucene/src/luan/modules/lucene/queryparser/Parser.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,155 +0,0 @@
-package luan.modules.lucene.queryparser;
-
-/**
- * A general parser utility class.
- * This class is not needed to use SaneQueryParser.
- */
-public class Parser {
-	public final String text;
-	private final int len;
-	private int[] stack = new int[256];
-	private int frame = 0;
-	private int iHigh;
-
-	Parser(String text) {
-		this.text = text;
-		this.len = text.length();
-	}
-
-	private int i() {
-		return stack[frame];
-	}
-
-	private void i(int i) {
-		stack[frame] += i;
-		if( iHigh < stack[frame] )
-			iHigh = stack[frame];
-	}
-
-	public int begin() {
-		frame++;
-		if( frame == stack.length ) {
-			int[] a = new int[2*frame];
-			System.arraycopy(stack,0,a,0,frame);
-			stack = a;
-		}
-		stack[frame] = stack[frame-1];
-		return i();
-	}
-
-	public void rollback() {
-		stack[frame] = stack[frame-1];
-	}
-
-	public <T> T success(T t) {
-		success();
-		return t;
-	}
-
-	public boolean success() {
-		frame--;
-		stack[frame] = stack[frame+1];
-		return true;
-	}
-
-	public <T> T failure(T t) {
-		failure();
-		return t;
-	}
-
-	public boolean failure() {
-		frame--;
-		return false;
-	}
-
-	public int currentIndex() {
-		return i();
-	}
-
-	public int errorIndex() {
-		return frame > 0 ? stack[frame-1] : 0;
-	}
-
-	public int highIndex() {
-		return iHigh;
-	}
-
-	public char lastChar() {
-		return text.charAt(i()-1);
-	}
-
-	public char currentChar() {
-		return text.charAt(i());
-	}
-
-	public boolean endOfInput() {
-		return i() >= len;
-	}
-
-	public boolean match(char c) {
-		if( endOfInput() || text.charAt(i()) != c )
-			return false;
-		i(1);
-		return true;
-	}
-
-	public boolean match(String s) {
-		int n = s.length();
-		if( !text.regionMatches(i(),s,0,n) )
-			return false;
-		i(n);
-		return true;
-	}
-
-	public boolean matchIgnoreCase(String s) {
-		int n = s.length();
-		if( !text.regionMatches(true,i(),s,0,n) )
-			return false;
-		i(n);
-		return true;
-	}
-
-	public boolean anyOf(String s) {
-		if( endOfInput() || s.indexOf(text.charAt(i())) == -1 )
-			return false;
-		i(1);
-		return true;
-	}
-
-	public boolean noneOf(String s) {
-		if( endOfInput() || s.indexOf(text.charAt(i())) != -1 )
-			return false;
-		i(1);
-		return true;
-	}
-
-	public boolean inCharRange(char cLow, char cHigh) {
-		if( endOfInput() )
-			return false;
-		char c = text.charAt(i());
-		if( !(cLow <= c && c <= cHigh) )
-			return false;
-		i(1);
-		return true;
-	}
-
-	public boolean anyChar() {
-		if( endOfInput() )
-			return false;
-		i(1);
-		return true;
-	}
-
-	public boolean test(char c) {
-		return !endOfInput() && text.charAt(i()) == c;
-	}
-
-	public boolean test(String s) {
-		return text.regionMatches(i(),s,0,s.length());
-	}
-
-	public String textFrom(int start) {
-		return text.substring(start,i());
-	}
-
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c lucene/src/luan/modules/lucene/queryparser/SaneQueryParser.java
--- a/lucene/src/luan/modules/lucene/queryparser/SaneQueryParser.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,247 +0,0 @@
-package luan.modules.lucene.queryparser;
-
-import java.util.List;
-import java.util.ArrayList;
-import java.util.regex.Pattern;
-import org.apache.lucene.search.Query;
-import org.apache.lucene.search.MatchAllDocsQuery;
-import org.apache.lucene.search.BooleanClause;
-import org.apache.lucene.search.BooleanQuery;
-import org.apache.lucene.search.Sort;
-import org.apache.lucene.search.SortField;
-
-
-public class SaneQueryParser {
-
-	public static Query parseQuery(FieldParser fieldParser,String query) throws ParseException {
-		return new SaneQueryParser(fieldParser,query).parseQuery();
-	}
-
-	private static Pattern specialChar = Pattern.compile("[ \\t\\r\\n\":\\[\\]{}^+\\-(),?*\\\\]");
-
-	public static String literal(String s) {
-		return specialChar.matcher(s).replaceAll("\\\\$0");
-	}
-
-	public static Sort parseSort(FieldParser fieldParser,String sort) throws ParseException {
-		return new SaneQueryParser(fieldParser,sort).parseSort();
-	}
-
-
-	private static final String NOT_IN_TERM = " \t\r\n\":[]{}^+-()";
-	private static final String NOT_IN_FIELD = NOT_IN_TERM + ",";
-	private final FieldParser fieldParser;
-	final Parser parser;
-
-	private SaneQueryParser(FieldParser fieldParser,String query) {
-		this.fieldParser = fieldParser;
-		this.parser = new Parser(query);
-	}
-
-	private Query parseQuery() throws ParseException {
-		Spaces();
-		BooleanQuery bq = new BooleanQuery();
-		while( !parser.endOfInput() ) {
-			bq.add( Term(null) );
-		}
-		BooleanClause[] clauses = bq.getClauses();
-		switch( clauses.length ) {
-		case 0:
-			return new MatchAllDocsQuery();
-		case 1:
-			{
-				BooleanClause bc = clauses[0];
-				if( bc.getOccur() != BooleanClause.Occur.MUST_NOT )
-					return bc.getQuery();
-			}
-		default:
-			return bq;
-		}
-	}
-
-	private BooleanClause Term(String defaultField) throws ParseException {
-		BooleanClause.Occur occur;
-		if( parser.match('+') ) {
-			occur = BooleanClause.Occur.MUST;
-			Spaces();
-		} else if( parser.match('-') ) {
-			occur = BooleanClause.Occur.MUST_NOT;
-			Spaces();
-		} else {
-			occur = BooleanClause.Occur.SHOULD;
-		}
-		String field = QueryField();
-		if( field == null )
-			field = defaultField;
-		Query query = NestedTerm(field);
-		if( query == null )
-			query = RangeTerm(field);
-		if( query == null ) {
-			parser.begin();
-			String match = SimpleTerm();
-			query = fieldParser.getQuery(this,field,match);
-			parser.success();
-		}
-		if( parser.match('^') ) {
-			Spaces();
-			int start = parser.begin();
-			try {
-				while( parser.anyOf("0123456789.") );
-				String match = parser.textFrom(start);
-				float boost = Float.parseFloat(match);
-				query.setBoost(boost);
-			} catch(NumberFormatException e) {
-				throw new ParseException(this,e);
-			}
-			parser.success();
-			Spaces();
-		}
-		BooleanClause bc = new BooleanClause(query,occur);
-		return bc;
-	}
-
-	private Query NestedTerm(String field) throws ParseException {
-		parser.begin();
-		if( !parser.match('(') )
-			return parser.failure(null);
-		BooleanQuery bq = new BooleanQuery();
-		while( !parser.match(')') ) {
-			if( parser.endOfInput() )
-				throw new ParseException(this,"unclosed parentheses");
-			bq.add( Term(field) );
-		}
-		Spaces();
-		BooleanClause[] clauses = bq.getClauses();
-		switch( clauses.length ) {
-		case 0:
-			throw new ParseException(this,"empty parentheses");
-		case 1:
-			{
-				BooleanClause bc = clauses[0];
-				if( bc.getOccur() != BooleanClause.Occur.MUST_NOT )
-					return parser.success(bc.getQuery());
-			}
-		default:
-			return parser.success(bq);
-		}
-	}
-
-	private Query RangeTerm(String field) throws ParseException {
-		parser.begin();
-		if( !parser.anyOf("[{") )
-			return parser.failure(null);
-		boolean includeMin = parser.lastChar() == '[';
-		Spaces();
-		String minQuery = SimpleTerm();
-		TO();
-		String maxQuery = SimpleTerm();
-		if( !parser.anyOf("]}") )
-			throw new ParseException(this,"unclosed range");
-		boolean includeMax = parser.lastChar() == ']';
-		Spaces();
-		Query query = fieldParser.getRangeQuery(this,field,minQuery,maxQuery,includeMin,includeMax);
-		return parser.success(query);
-	}
-
-	private void TO() throws ParseException {
-		parser.begin();
-		if( !(parser.match("TO") && Space()) )
-			throw new ParseException(this,"'TO' expected");
-		Spaces();
-		parser.success();
-	}
-
-	private String SimpleTerm() throws ParseException {
-		parser.begin();
-		String match;
-		if( parser.match('"') ) {
-			int start = parser.currentIndex() - 1;
-			while( !parser.match('"') ) {
-				if( parser.endOfInput() )
-					throw new ParseException(this,"unclosed quotes");
-				parser.anyChar();
-				checkEscape();
-			}
-			match = parser.textFrom(start);
-			Spaces();
-		} else {
-			match = Unquoted(NOT_IN_TERM);
-		}
-		if( match.length() == 0 )
-			throw new ParseException(this);
-		return parser.success(match);
-	}
-
-	private String QueryField() throws ParseException {
-		parser.begin();
-		String match = Field();
-		if( match==null || !parser.match(':') )
-			return parser.failure((String)null);
-		Spaces();
-		return parser.success(match);
-	}
-
-	private String Field() throws ParseException {
-		parser.begin();
-		String match = Unquoted(NOT_IN_FIELD);
-		if( match.length()==0 )
-			return parser.failure((String)null);
-		match = StringFieldParser.escape(this,match);
-		return parser.success(match);
-	}
-
-	private String Unquoted(String exclude) throws ParseException {
-		int start = parser.begin();
-		while( parser.noneOf(exclude) ) {
-			checkEscape();
-		}
-		String match = parser.textFrom(start);
-		Spaces();
-		return parser.success(match);
-	}
-
-	private void checkEscape() {
-		if( parser.lastChar() == '\\' )
-			parser.anyChar();
-	}
-
-	private void Spaces() {
-		while( Space() );
-	}
-
-	private boolean Space() {
-		return parser.anyOf(" \t\r\n");
-	}
-
-
-	// sort
-
-	private Sort parseSort() throws ParseException {
-		Spaces();
-		if( parser.endOfInput() )
-			return null;
-		List<SortField> list = new ArrayList<SortField>();
-		list.add( SortField() );
-		while( !parser.endOfInput() ) {
-			parser.begin();
-			if( !parser.match(',') )
-				throw new ParseException(this,"',' expected");
-			Spaces();
-			parser.success();
-			list.add( SortField() );
-		}
-		return new Sort(list.toArray(new SortField[0]));
-	}
-
-	private SortField SortField() throws ParseException {
-		parser.begin();
-		String field = Field();
-		if( field==null )
-			throw new ParseException(this);
-		boolean reverse = !parser.matchIgnoreCase("asc") && parser.matchIgnoreCase("desc");
-		Spaces();
-		SortField sf = fieldParser.getSortField(this,field,reverse);
-		return parser.success(sf);
-	}
-
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c lucene/src/luan/modules/lucene/queryparser/StringFieldParser.java
--- a/lucene/src/luan/modules/lucene/queryparser/StringFieldParser.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,112 +0,0 @@
-package luan.modules.lucene.queryparser;
-
-import java.io.StringReader;
-import java.io.IOException;
-import org.apache.lucene.analysis.Analyzer;
-import org.apache.lucene.analysis.TokenStream;
-import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
-import org.apache.lucene.analysis.tokenattributes.PositionIncrementAttribute;
-import org.apache.lucene.search.Query;
-import org.apache.lucene.search.TermQuery;
-import org.apache.lucene.search.TermRangeQuery;
-import org.apache.lucene.search.PhraseQuery;
-import org.apache.lucene.search.WildcardQuery;
-import org.apache.lucene.search.PrefixQuery;
-import org.apache.lucene.search.SortField;
-import org.apache.lucene.index.Term;
-
-
-public class StringFieldParser implements FieldParser {
-	public int slop = 0;
-	public final Analyzer analyzer;
-
-	public StringFieldParser(Analyzer analyzer) {
-		this.analyzer = analyzer;
-	}
-
-	@Override public Query getQuery(SaneQueryParser qp,String field,String query) throws ParseException {
-		String wildcard = wildcard(qp,query);
-		if( wildcard != null )
-			return new WildcardQuery(new Term(field,wildcard));
-		if( query.endsWith("*") && !query.endsWith("\\*") )
-			return new PrefixQuery(new Term(field,query.substring(0,query.length()-1)));
-		query = escape(qp,query);
-		PhraseQuery pq = new PhraseQuery();
-		try {
-			TokenStream ts = analyzer.tokenStream(field,new StringReader(query));
-			CharTermAttribute termAttr = ts.addAttribute(CharTermAttribute.class);
-			PositionIncrementAttribute posAttr = ts.addAttribute(PositionIncrementAttribute.class);
-			ts.reset();
-			int pos = -1;
-			while( ts.incrementToken() ) {
-				pos += posAttr.getPositionIncrement();
-				pq.add( new Term(field,termAttr.toString()), pos );
-			}
-			ts.end();
-			ts.close();
-		} catch(IOException e) {
-			throw new RuntimeException(e);
-		}
-		Term[] terms = pq.getTerms();
-		if( terms.length==1 && pq.getPositions()[0]==0 )
-			return new TermQuery(terms[0]);
-		return pq;
-	}
-
-	@Override public Query getRangeQuery(SaneQueryParser qp,String field,String minQuery,String maxQuery,boolean includeMin,boolean includeMax) throws ParseException {
-		minQuery = escape(qp,minQuery);
-		maxQuery = escape(qp,maxQuery);
-		return TermRangeQuery.newStringRange(field,minQuery,maxQuery,includeMin,includeMax);
-	}
-
-	static String escape(SaneQueryParser qp,String s) throws ParseException {
-		final char[] a = s.toCharArray();
-		int i, n;
-		if( a[0] == '"' ) {
-			if( a[a.length-1] != '"' )  throw new RuntimeException();
-			i = 1;
-			n = a.length - 1;
-		} else {
-			i = 0;
-			n = a.length;
-		}
-		StringBuilder sb = new StringBuilder();
-		for( ; i<n; i++ ) {
-			char c = a[i];
-			if( c == '\\' ) {
-				if( ++i == a.length )
-					throw new ParseException(qp,"ends with '\\'");
-				c = a[i];
-			}
-			sb.append(c);
-		}
-		return sb.toString();
-	}
-
-	private static String wildcard(SaneQueryParser qp,String s) throws ParseException {
-		final char[] a = s.toCharArray();
-		if( a[0] == '"' )
-			return null;
-		boolean hasWildcard = false;
-		StringBuilder sb = new StringBuilder();
-		for( int i=0; i<a.length; i++ ) {
-			char c = a[i];
-			if( c=='?' || c=='*' && i<a.length-1 )
-				hasWildcard = true;
-			if( c == '\\' ) {
-				if( ++i == a.length )
-					throw new ParseException(qp,"ends with '\\'");
-				c = a[i];
-				if( c=='?' || c=='*' )
-					sb.append('\\');
-			}
-			sb.append(c);
-		}
-		return hasWildcard ? sb.toString() : null;
-	}
-
-	@Override public SortField getSortField(SaneQueryParser qp,String field,boolean reverse) {
-		return new SortField( field, SortField.Type.STRING, reverse );
-	}
-
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c lucene/src/luan/modules/lucene/queryparser/SynonymParser.java
--- a/lucene/src/luan/modules/lucene/queryparser/SynonymParser.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,42 +0,0 @@
-package luan.modules.lucene.queryparser;
-
-import java.util.Map;
-import org.apache.lucene.search.Query;
-import org.apache.lucene.search.BooleanQuery;
-import org.apache.lucene.search.BooleanClause;
-import org.apache.lucene.search.SortField;
-
-
-public class SynonymParser implements FieldParser {
-	private final FieldParser fp;
-	private final Map<String,String[]> synonymMap;
-
-	public SynonymParser(FieldParser fp,Map<String,String[]> synonymMap) {
-		this.fp = fp;
-		this.synonymMap = synonymMap;
-	}
-
-	protected String[] getSynonyms(String query) {
-		return synonymMap.get(query);
-	}
-
-	public Query getQuery(SaneQueryParser qp,String field,String query) throws ParseException {
-		String[] synonyms = getSynonyms(query);
-		if( synonyms == null )
-			return fp.getQuery(qp,field,query);
-		BooleanQuery bq = new BooleanQuery();
-		bq.add( fp.getQuery(qp,field,query), BooleanClause.Occur.SHOULD );
-		for( String s : synonyms ) {
-			bq.add( fp.getQuery(qp,field,s), BooleanClause.Occur.SHOULD );
-		}
-		return bq;
-	}
-
-	public Query getRangeQuery(SaneQueryParser qp,String field,String minQuery,String maxQuery,boolean includeMin,boolean includeMax) throws ParseException {
-		return fp.getRangeQuery(qp,field,minQuery,maxQuery,includeMin,includeMax);
-	}
-
-	public SortField getSortField(SaneQueryParser qp,String field,boolean reverse) throws ParseException {
-		return fp.getSortField(qp,field,reverse);
-	}
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c mail/ext/javax.mail.jar
Binary file mail/ext/javax.mail.jar has changed
diff -r 3e30cf310e56 -r 1a68fc55a80c mail/src/luan/modules/mail/Mail.luan
--- a/mail/src/luan/modules/mail/Mail.luan	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,17 +0,0 @@
-java()
-local Luan = require "luan:Luan.luan"
-local assert_table = Luan.assert_table
-local System = require "java:java.lang.System"
-local SmtpCon = require "java:luan.modules.mail.SmtpCon"
-
-local M = {}
-
-System.setProperty( "mail.mime.charset", "UTF-8" )
-
-function M.Sender(params)
-	assert_table(params)
-	local smtpCon = SmtpCon.new(params)
-	return { send = smtpCon.send }
-end
-
-return M
diff -r 3e30cf310e56 -r 1a68fc55a80c mail/src/luan/modules/mail/SmtpCon.java
--- a/mail/src/luan/modules/mail/SmtpCon.java	Fri Aug 26 03:15:41 2016 -0600
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,187 +0,0 @@
-package luan.modules.mail;
-
-import java.io.IOException;
-import java.util.Map;
-import java.util.HashMap;
-import java.util.Properties;
-import javax.mail.Authenticator;
-import javax.mail.PasswordAuthentication;
-import javax.mail.Session;
-import javax.mail.Transport;
-import javax.mail.Message;
-import javax.mail.MessagingException;
-import javax.mail.Part;
-import javax.mail.internet.MimeMessage;
-import javax.mail.internet.MimeMultipart;
-import javax.mail.internet.MimeBodyPart;
-import luan.Luan;
-import luan.LuanState;
-import luan.LuanTable;
-import luan.LuanException;
-
-
-public final class SmtpCon {
-	private final Session session;
-
-	public SmtpCon(LuanState luan,LuanTable paramsTbl) throws LuanException {
-		Map<Object,Object> params = new HashMap<Object,Object>(paramsTbl.asMap(luan));
-		Properties props = new Properties(System.getProperties());
-
-		String host = getString(params,"host");
-		if( host==null )
-			throw new LuanException( "parameter 'host' is required" );
-		props.setProperty("mail.smtp.host",host);
-
-		Object port = params.remove("port");
-		if( port != null ) {
-			String s;
-			if( port instanceof String ) {
-				s = (String)port;
-			} else if( port instanceof Number ) {
-				Integer i = Luan.asInteger(port);
-				if( i == null )
-					throw new LuanException( "parameter 'port' must be an integer" );
-				s = i.toString();
-			} else {
-				throw new LuanException( "parameter 'port' must be an integer" );
-			}
-			props.setProperty("mail.smtp.socketFactory.port", s);
-			props.setProperty("mail.smtp.port", s);
-		}
-
-		String username = getString(params,"username");
-		if( username == null ) {
-			session = Session.getInstance(props);
-		} else {
-			String password = getString(params,"password");
-			if( password==null )
-				throw new LuanException( "parameter 'password' is required with 'username'" );
-			props.setProperty("mail.smtp.auth","true");
-			final PasswordAuthentication pa = new PasswordAuthentication(username,password);
-			Authenticator auth = new Authenticator() {
-				protected PasswordAuthentication getPasswordAuthentication() {
-					return pa;
-				}
-			};
-			session = Session.getInstance(props,auth);
-		}
-
-		if( !params.isEmpty() )
-			throw new LuanException( "unrecognized parameters: "+params );
-	}
-
-	private String getString(Map<Object,Object> params,String key) throws LuanException {
-		Object val = params.remove(key);
-		if( val!=null && !(val instanceof String) )
-			throw new LuanException( "parameter '"+key+"' must be a string" );
-		return (String)val;
-	}
-
-
-	public void send(LuanState luan,LuanTable mailTbl) throws LuanException {
-		try {
-			Map<Object,Object> mailParams = new HashMap<Object,Object>(mailTbl.asMap(luan));
-			MimeMessage msg = new MimeMessage(session);
-
-			String from = getString(mailParams,"from");
-			if( from != null )
-				msg.setFrom(from);
-
-			String to = getString(mailParams,"to");
-			if( to != null )
-				msg.setRecipients(Message.RecipientType.TO,to);
-
-			String cc = getString(mailParams,"cc");
-			if( cc != null )
-				msg.setRecipients(Message.RecipientType.CC,cc);
-
-			String subject = getString(mailParams,"subject");
-			if( subject != null )
-				msg.setSubject(subject);
-
-			Object body = mailParams.remove("body");
-			Object attachments = mailParams.remove("attachments");
-			Part bodyPart = attachments==null ? msg : new MimeBodyPart();
-
-			if( body != null ) {
-				if( body instanceof String ) {
-					bodyPart.setText((String)body);
-				} else if( body instanceof LuanTable ) {
-					LuanTable bodyTbl = (LuanTable)body;
-					Map<Object,Object> map = new HashMap<Object,Object>(bodyTbl.asMap(luan));
-					MimeMultipart mp = new MimeMultipart("alternative");
-					String text = (String)map.remove("text");
-					if( text != null ) {
-						MimeBodyPart part = new MimeBodyPart();
-						part.setText(text);
-						mp.addBodyPart(part);
-					}
-					String html = (String)map.remove("html");
-					if( html != null ) {
-						MimeBodyPart part = new MimeBodyPart();
-						part.setContent(html,"text/html");
-						mp.addBodyPart(part);
-					}
-					if( !map.isEmpty() )
-						throw new LuanException( "invalid body types: " + map );
-					bodyPart.setContent(mp);
-				} else
-					throw new LuanException( "parameter 'body' must be a string or table" );
-			}
-
-			if( attachments != null ) {
-				if( !(attachments instanceof LuanTable) )
-					throw new LuanException( "parameter 'attachments' must be a table" );
-				LuanTable attachmentsTbl = (LuanTable)attachments;
-				if( !attachmentsTbl.isList() )
-					throw new LuanException( "parameter 'attachments' must be a list" );
-				MimeMultipart mp = new MimeMultipart("mixed");
-				if( body != null )
-					mp.addBodyPart((MimeBodyPart)bodyPart);
-				for( Object attachment : attachmentsTbl.asList() ) {
-					if( !(attachment instanceof LuanTable) )
-						throw new LuanException( "each attachment must be a table" );
-					Map<Object,Object> attachmentMap = new HashMap<Object,Object>(((LuanTable)attachment).asMap(luan));
-					Object obj;
-
-					obj = attachmentMap.remove("filename");
-					if( obj==null )
-						throw new LuanException( "an attachment is missing 'filename'" );
-					if( !(obj instanceof String) )
-						throw new LuanException( "an attachment filename must be a string" );
-					String filename = (String)obj;
-
-					obj = attachmentMap.remove("content_type");
-					if( obj==null )
-						throw new LuanException( "an attachment is missing 'content_type'" );
-					if( !(obj instanceof String) )
-						throw new LuanException( "an attachment content_type must be a string" );
-					String content_type = (String)obj;
-
-					Object content = attachmentMap.remove("content");
-					if( content==null )
-						throw new LuanException( "an attachment is missing 'content'" );
-					if( content_type.startsWith("text/") && content instanceof byte[] )
-						content = new String((byte[])content);
-
-					if( !attachmentMap.isEmpty() )
-						throw new LuanException( "unrecognized attachment parameters: "+attachmentMap );
-
-					MimeBodyPart part = new MimeBodyPart();
-					part.setContent(content,content_type);
-					part.setFileName(filename);
-					mp.addBodyPart(part);
-				}
-				msg.setContent(mp);
-			}
-
-			if( !mailParams.isEmpty() )
-				throw new LuanException( "unrecognized parameters: "+mailParams );
-
-			Transport.send(msg);
-		} catch(MessagingException e) {
-			throw new LuanException(e);
-		}
-	}
-
-}
diff -r 3e30cf310e56 -r 1a68fc55a80c scripts/build-luan.sh
--- a/scripts/build-luan.sh	Fri Aug 26 03:15:41 2016 -0600
+++ b/scripts/build-luan.sh	Fri Aug 26 14:36:40 2016 -0600
@@ -13,51 +13,15 @@
 mkdir $LUAN_BUILD/luan/jars
 
 cd $LUAN_HOME
-echo "return '$VERSION'" >core/src/luan/version.luan
+echo "return '$VERSION'" >src/luan/version.luan
 find . -name *.class -delete
 
-cd $LUAN_HOME
-SRC=core/src
-CLASSPATH=$LUAN_HOME/$SRC
-javac -classpath $CLASSPATH `find $SRC -name *.java`
-cd $SRC
+CLASSPATH=$LUAN_HOME/src
+for i in $LUAN_HOME/lib/* ; do CLASSPATH=$CLASSPATH:$i ; done
+cd src
+javac -classpath $CLASSPATH `find . -name *.java`
 jar cvf $LUAN_BUILD/luan/jars/luan-$VERSION.jar `find . -name *.class -o -name *.luan`
-
-cd $LUAN_HOME
-SRC=http/src
-CLASSPATH=$LUAN_HOME/core/src:$LUAN_HOME/$SRC:$SLF4J
-for i in $LUAN_HOME/http/ext/* ; do CLASSPATH=$CLASSPATH:$i ; done
-javac -classpath $CLASSPATH `find $SRC -name *.java`
-cd $SRC
-jar uvf $LUAN_BUILD/luan/jars/luan-$VERSION.jar `find . -name *.class -o -name *.luan`
-cp $LUAN_HOME/http/ext/* $LUAN_BUILD/luan/jars
-
-cd $LUAN_HOME
-SRC=logging/src
-CLASSPATH=$LUAN_HOME/core/src:$LUAN_HOME/$SRC
-for i in $LUAN_HOME/logging/ext/* ; do CLASSPATH=$CLASSPATH:$i ; done
-javac -classpath $CLASSPATH `find $SRC -name *.java`
-cd $SRC
-jar uvf $LUAN_BUILD/luan/jars/luan-$VERSION.jar `find . -name *.class -o -name *.luan`
-cp $LUAN_HOME/logging/ext/* $LUAN_BUILD/luan/jars
-
-cd $LUAN_HOME
-SRC=mail/src
-CLASSPATH=$LUAN_HOME/core/src:$LUAN_HOME/$SRC
-for i in $LUAN_HOME/mail/ext/* ; do CLASSPATH=$CLASSPATH:$i ; done
-javac -classpath $CLASSPATH `find $SRC -name *.java`
-cd $SRC
-jar uvf $LUAN_BUILD/luan/jars/luan-$VERSION.jar `find . -name *.class -o -name *.luan`
-cp $LUAN_HOME/mail/ext/* $LUAN_BUILD/luan/jars
-
-cd $LUAN_HOME
-SRC=lucene/src
-CLASSPATH=$LUAN_HOME/core/src:$LUAN_HOME/$SRC:$SLF4J
-for i in $LUAN_HOME/lucene/ext/* ; do CLASSPATH=$CLASSPATH:$i ; done
-javac -classpath $CLASSPATH `find $SRC -name *.java`
-cd $SRC
-jar uvf $LUAN_BUILD/luan/jars/luan-$VERSION.jar `find . -name *.class -o -name *.luan`
-cp $LUAN_HOME/lucene/ext/* $LUAN_BUILD/luan/jars
+cp $LUAN_HOME/lib/* $LUAN_BUILD/luan/jars
 
 cp $LUAN_HOME/scripts/install.sh $LUAN_BUILD/luan
 chmod +x $LUAN_BUILD/luan/install.sh
diff -r 3e30cf310e56 -r 1a68fc55a80c scripts/cp-luan
--- a/scripts/cp-luan	Fri Aug 26 03:15:41 2016 -0600
+++ b/scripts/cp-luan	Fri Aug 26 14:36:40 2016 -0600
@@ -1,20 +1,7 @@
 LUAN_HOME=~/hg/luan
 
-CLASSPATH=$LUAN_HOME/core/src
-
-CLASSPATH=$CLASSPATH:$LUAN_HOME/logging/src
-for i in $LUAN_HOME/logging/ext/* ; do CLASSPATH=$CLASSPATH:$i ; done
-
-CLASSPATH=$CLASSPATH:$LUAN_HOME/http/src
-for i in $LUAN_HOME/http/ext/* ; do CLASSPATH=$CLASSPATH:$i ; done
+CLASSPATH=$LUAN_HOME/src
 
-CLASSPATH=$CLASSPATH:$LUAN_HOME/mail/src
-for i in $LUAN_HOME/mail/ext/* ; do CLASSPATH=$CLASSPATH:$i ; done
-
-CLASSPATH=$CLASSPATH:$LUAN_HOME/lucene/src
-for i in $LUAN_HOME/lucene/ext/* ; do CLASSPATH=$CLASSPATH:$i ; done
-
-CLASSPATH=$CLASSPATH:$LUAN_HOME/stripe/src
-for i in $LUAN_HOME/stripe/ext/* ; do CLASSPATH=$CLASSPATH:$i ; done
+for i in $LUAN_HOME/lib/* ; do CLASSPATH=$CLASSPATH:$i ; done
 
 export CLASSPATH
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/DeepCloneable.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/DeepCloneable.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,7 @@
+package luan;
+
+
+public interface DeepCloneable {
+	public DeepCloneable shallowClone();
+	public void deepenClone(DeepCloneable clone,DeepCloner cloner);
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/DeepCloner.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/DeepCloner.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,60 @@
+package luan;
+
+import java.util.Map;
+import java.util.HashMap;
+import java.util.IdentityHashMap;
+
+
+public final class DeepCloner {
+	private final Map cloned = new IdentityHashMap();
+
+	public DeepCloneable deepClone(DeepCloneable obj) {
+		if( obj==null )
+			return null;
+		DeepCloneable rtn = (DeepCloneable)cloned.get(obj);
+		if( rtn == null ) {
+			rtn = obj.shallowClone();
+			cloned.put(obj,rtn);
+			obj.deepenClone(rtn,this);
+		}
+		return rtn;
+	}
+
+	public Object[] deepClone(Object[] obj) {
+		if( obj.length == 0 )
+			return obj;
+		Object[] rtn = (Object[])cloned.get(obj);
+		if( rtn == null ) {
+			rtn = obj.clone();
+			cloned.put(obj,rtn);
+			for( int i=0; i<rtn.length; i++ ) {
+				rtn[i] = get(rtn[i]);
+			}
+		}
+		return rtn;
+	}
+
+	public Map deepClone(Map obj) {
+		if( !obj.getClass().equals(HashMap.class) )
+			throw new RuntimeException("can only clone HashMap");
+		Map rtn = (Map)cloned.get(obj);
+		if( rtn == null ) {
+			rtn = new HashMap();
+			for( Object stupid : obj.entrySet() ) {
+				Map.Entry entry = (Map.Entry)stupid;
+				rtn.put( get(entry.getKey()), get(entry.getValue()) );
+			}
+		}
+		return rtn;
+	}
+
+	public Object get(Object obj) {
+		if( obj instanceof DeepCloneable )
+			return deepClone((DeepCloneable)obj);
+		if( obj instanceof Object[] )
+			return deepClone((Object[])obj);
+		if( obj instanceof Map )
+			return deepClone((Map)obj);
+		return obj;
+	}
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/Luan.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/Luan.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,157 @@
+package luan;
+
+import java.util.List;
+import luan.modules.BasicLuan;
+import luan.impl.LuanCompiler;
+
+
+public final class Luan {
+
+	public static void main(String[] args) throws LuanException {
+		LuanState luan = new LuanState();
+		LuanFunction standalone = (LuanFunction)BasicLuan.load_file(luan,"classpath:luan/cmd_line.luan");
+		standalone.call(luan,args);
+	}
+
+	public static Object first(Object obj) {
+		if( !(obj instanceof Object[]) )
+			return obj;
+		Object[] a = (Object[])obj;
+		return a.length==0 ? null : a[0];
+	}
+
+	public static Object[] array(Object obj) {
+		return obj instanceof Object[] ? (Object[])obj : new Object[]{obj};
+	}
+
+	public static String type(Object obj) {
+		if( obj == null )
+			return "nil";
+		if( obj instanceof String )
+			return "string";
+		if( obj instanceof Boolean )
+			return "boolean";
+		if( obj instanceof Number )
+			return "number";
+		if( obj instanceof LuanTable )
+			return "table";
+		if( obj instanceof LuanFunction )
+			return "function";
+		if( obj instanceof byte[] )
+			return "binary";
+		return "java";
+	}
+
+	public static String toString(Number n) {
+		if( n instanceof Integer )
+			return n.toString();
+		int i = n.intValue();
+		if( i == n.doubleValue() )
+			return Integer.toString(i);
+		String s = n.toString();
+		int iE = s.indexOf('E');
+		String ending  = null;
+		if( iE != -1 ) {
+			ending = s.substring(iE);
+			s = s.substring(0,iE);
+		}
+		if( s.endsWith(".0") )
+			s = s.substring(0,s.length()-2);
+		if( ending != null )
+			s += ending;
+		return s;
+	}
+
+	public static Integer asInteger(Object obj) {
+		if( obj instanceof Integer )
+			return (Integer)obj;
+		if( !(obj instanceof Number) )
+			return null;
+		Number n = (Number)obj;
+		int i = n.intValue();
+		return i==n.doubleValue() ? Integer.valueOf(i) : null;
+	}
+
+	public static String stringEncode(String s) {
+		s = s.replace("\\","\\\\");
+		s = s.replace("\u0007","\\a");
+		s = s.replace("\b","\\b");
+		s = s.replace("\f","\\f");
+		s = s.replace("\n","\\n");
+		s = s.replace("\r","\\r");
+		s = s.replace("\t","\\t");
+		s = s.replace("\u000b","\\v");
+		s = s.replace("\"","\\\"");
+		s = s.replace("\'","\\'");
+		return s;
+	}
+
+
+	// from LuanState
+
+	public static Boolean checkBoolean(Object obj) throws LuanException {
+		if( obj instanceof Boolean )
+			return (Boolean)obj;
+		throw new LuanException("attempt to use a " + Luan.type(obj) + " value as a boolean" );
+	}
+
+	public static String checkString(Object obj) throws LuanException {
+		if( obj instanceof String )
+			return (String)obj;
+		throw new LuanException("attempt to use a " + Luan.type(obj) + " value as a string" );
+	}
+
+	public static LuanFunction checkFunction(Object obj) throws LuanException {
+		if( obj instanceof LuanFunction )
+			return (LuanFunction)obj;
+		throw new LuanException("attempt to call a " + Luan.type(obj) + " value" );
+	}
+
+	public static boolean isLessThan(LuanState luan,Object o1,Object o2) throws LuanException {
+		if( o1 instanceof Number && o2 instanceof Number ) {
+			Number n1 = (Number)o1;
+			Number n2 = (Number)o2;
+			return n1.doubleValue() < n2.doubleValue();
+		}
+		if( o1 instanceof String && o2 instanceof String ) {
+			String s1 = (String)o1;
+			String s2 = (String)o2;
+			return s1.compareTo(s2) < 0;
+		}
+		LuanFunction fn = getBinHandler("__lt",o1,o2);
+		if( fn != null )
+			return checkBoolean( Luan.first(fn.call(luan,new Object[]{o1,o2})) );
+		throw new LuanException( "attempt to compare " + Luan.type(o1) + " with " + Luan.type(o2) );
+	}
+
+	public static LuanFunction getBinHandler(String op,Object o1,Object o2) throws LuanException {
+		if( o1 instanceof LuanTable ) {
+			LuanFunction f1 = getHandlerFunction(op,(LuanTable)o1);
+			if( f1 != null )
+				return f1;
+		}
+		return o2 instanceof LuanTable ? getHandlerFunction(op,(LuanTable)o2) : null;
+	}
+
+	public static LuanFunction getHandlerFunction(String op,LuanTable t) throws LuanException {
+		Object f = t.getHandler(op);
+		if( f == null )
+			return null;
+		return checkFunction(f);
+	}
+
+	public static LuanFunction load(String text,String sourceName,LuanTable env)
+		throws LuanException
+	{
+		return LuanCompiler.compile(text,sourceName,env);
+	}
+
+	public static LuanFunction load(String text,String sourceName)
+		throws LuanException
+	{
+		return load(text,sourceName,null);
+	}
+
+
+	private Luan() {}  // never
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/LuanException.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/LuanException.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,151 @@
+package luan;
+
+import java.io.StringWriter;
+import java.io.PrintWriter;
+import java.util.List;
+import java.util.ArrayList;
+
+
+public final class LuanException extends Exception implements DeepCloneable {
+	private LuanTable table;
+
+	public LuanException(String msg,Throwable cause) {
+		super(msg,cause);
+		initTable();
+	}
+
+	public LuanException(String msg) {
+		super(msg);
+		initTable();
+	}
+
+	public LuanException(Throwable cause) {
+		super(cause);
+		initTable();
+	}
+
+	private LuanException(String msg,Throwable cause,int nonsense) {
+		super(msg,cause);
+	}
+
+	@Override public LuanException shallowClone() {
+		return new LuanException(getMessage(),getCause(),99);
+	}
+
+	@Override public void deepenClone(DeepCloneable dc,DeepCloner cloner) {
+		LuanException clone = (LuanException)dc;
+		clone.table = (LuanTable)cloner.get(table);
+	}
+
+	public LuanTable table() {
+		return table;
+	}
+
+	private void initTable() {
+		table = new LuanTable();
+		table.rawPut( "java", this );
+		LuanTable metatable = new LuanTable();
+		table.setMetatable(metatable);
+		try {
+			table.rawPut( "get_message", new LuanJavaFunction(
+				LuanException.class.getMethod( "getMessage" ), this
+			) );
+			table.rawPut( "throw", new LuanJavaFunction(
+				LuanException.class.getMethod( "throwThis" ), this
+			) );
+			table.rawPut( "get_java_stack_trace_string", new LuanJavaFunction(
+				LuanException.class.getMethod( "getJavaStackTraceString" ), this
+			) );
+			metatable.rawPut( "__to_string", new LuanJavaFunction(
+				LuanException.class.getMethod( "getFullMessage" ), this
+			) );
+		} catch(NoSuchMethodException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
+	public void throwThis() throws LuanException {
+		throw this;
+	}
+
+	public String getFullMessage() {
+		return getLuanStackTraceString();
+//		return getLuanStackTraceString()+"\n"+getJavaStackTraceString();
+/*
+		StringBuilder buf = new StringBuilder();
+
+		Object msg = table.rawGet("message");
+		String msgStr = (String)table.rawGet("message_string");
+		buf.append( msgStr );
+
+		for( int i = table.rawLength(); i>=1; i-- ) {
+			LuanTable tbl = (LuanTable)table.rawGet(i);
+			buf.append( "\n\t" ).append( tbl.rawGet("source") ).append( " line " ).append( tbl.rawGet("line") );
+			Object callTo = tbl.rawGet("call_to");
+			if( callTo != null )
+				buf.append( " in call to '" ).append( callTo ).append( "'" );
+		}
+
+		if( msg instanceof Throwable ) {
+			buf.append( "\nCaused by: " );
+			Throwable cause = (Throwable)msg;
+			StringWriter sw = new StringWriter();
+			cause.printStackTrace(new PrintWriter(sw));
+			buf.append( sw );
+		}
+
+		return buf.toString();
+*/
+	}
+
+	public String getJavaStackTraceString() {
+		return getJavaStackTraceString(this);
+	}
+
+	private static String getJavaStackTraceString(Throwable th) {
+		StringWriter sw = new StringWriter();
+		th.printStackTrace(new PrintWriter(sw));
+		return sw.toString();
+	}
+
+	public static List<StackTraceElement> justLuan(StackTraceElement[] orig) {
+		List<StackTraceElement> list = new ArrayList<StackTraceElement>();
+		for( int i=0; i<orig.length; i++ ) {
+			StackTraceElement ste = orig[i];
+			if( !ste.getClassName().startsWith("luan.impl.EXP") )
+				continue;
+			list.add(ste);
+			if( !ste.getMethodName().equals("doCall") )
+				i++;
+		}
+		return list;
+	}
+
+	public static String toString(StackTraceElement ste) {
+		StringBuilder sb = new StringBuilder();
+		sb.append( ste.getFileName() ).append( " line " ).append( ste.getLineNumber() );
+		String method = ste.getMethodName();
+		if( !method.equals("doCall") )
+			sb.append( " in function '" ).append( method.substring(1) ).append( "'" );
+		return sb.toString();
+	}
+
+	public String getLuanStackTraceString() {
+		StringBuilder sb = new StringBuilder();
+		sb.append( getMessage() );
+		for( StackTraceElement ste : justLuan(getStackTrace()) ) {
+			sb.append( "\n\t" ).append( toString(ste) );
+		}
+		Throwable cause = getCause();
+		if( cause != null )
+			sb.append( "\nCaused by: " ).append( getJavaStackTraceString(cause) );
+		return sb.toString();
+	}
+
+	public static String currentSource() {
+		LuanException ex = new LuanException("currentSource");
+		List<StackTraceElement> st = ex.justLuan(ex.getStackTrace());
+		return st.isEmpty() ? null : st.get(0).getFileName();
+	}
+
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/LuanFunction.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/LuanFunction.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,18 @@
+package luan;
+
+
+public abstract class LuanFunction {
+
+	public abstract Object call(LuanState luan,Object[] args) throws LuanException;
+
+	public static final Object[] NOTHING = new Object[0];
+
+	public final Object call(LuanState luan) throws LuanException {
+		return call(luan,NOTHING);
+	}
+
+	@Override public String toString() {
+		return "function: " + Integer.toHexString(hashCode());
+	}
+
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/LuanJava.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/LuanJava.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,17 @@
+package luan;
+
+import luan.DeepCloneable;
+import luan.DeepCloner;
+
+
+public final class LuanJava implements DeepCloneable {
+	public boolean ok = false;
+
+	@Override public LuanJava shallowClone() {
+		LuanJava java = new LuanJava();
+		java.ok = ok;
+		return java;
+	}
+
+	@Override public void deepenClone(DeepCloneable clone,DeepCloner cloner) {}
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/LuanJavaFunction.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/LuanJavaFunction.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,572 @@
+package luan;
+
+import java.lang.reflect.Array;
+import java.lang.reflect.Method;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.Set;
+import java.util.Arrays;
+import java.util.Collection;
+
+
+public final class LuanJavaFunction extends LuanFunction {
+	private final JavaMethod method;
+	private Object obj;
+	private final RtnConverter rtnConverter;
+	private final boolean takesLuaState;
+	private final ArgConverter[] argConverters;
+	private final Class varArgCls;
+
+	public LuanJavaFunction(Method method,Object obj) {
+		this( JavaMethod.of(method), obj );
+	}
+
+	public LuanJavaFunction(Constructor constr,Object obj) {
+		this( JavaMethod.of(constr), obj );
+	}
+
+	private LuanJavaFunction(JavaMethod method,Object obj) {
+		this.method = method;
+		this.obj = obj;
+		this.rtnConverter = getRtnConverter(method);
+		this.takesLuaState = takesLuaState(method);
+		this.argConverters = getArgConverters(takesLuaState,method);
+		if( method.isVarArgs() ) {
+			Class[] paramTypes = method.getParameterTypes();
+			this.varArgCls = paramTypes[paramTypes.length-1].getComponentType();
+		} else {
+			this.varArgCls = null;
+		}
+	}
+/*
+	private LuanJavaFunction(LuanJavaFunction f) {
+		this.method = f.method;
+		this.rtnConverter = f.rtnConverter;
+		this.takesLuaState = f.takesLuaState;
+		this.argConverters = f.argConverters;
+		this.varArgCls = f.varArgCls;
+	}
+
+	@Override public LuanJavaFunction shallowClone() {
+		return obj==null ? this : new LuanJavaFunction(this);
+	}
+
+	@Override public void deepenClone(LuanJavaFunction clone,DeepCloner cloner) {
+		clone.obj = cloner.get(obj);
+	}
+*/
+	@Override public String toString() {
+		return "java-function: " + method;
+	}
+
+	public Class[] getParameterTypes() {
+		return method.getParameterTypes();
+	}
+
+	@Override public Object call(LuanState luan,Object[] args) throws LuanException {
+		try {
+			args = fixArgs(luan,args);
+			return doCall(args);
+		} catch(IllegalArgumentException e) {
+			checkArgs(args);
+			throw e;
+		}
+	}
+
+	public Object rawCall(LuanState luan,Object[] args) throws LuanException {
+		args = fixArgs(luan,args);
+		return doCall(args);
+	}
+
+	private Object doCall(Object[] args) throws LuanException {
+		Object rtn;
+		try {
+			rtn = method.invoke(obj,args);
+		} catch(IllegalAccessException e) {
+			throw new RuntimeException("method = "+method,e);
+		} catch(InvocationTargetException e) {
+			Throwable cause = e.getCause();
+			if( cause instanceof Error )
+				throw (Error)cause;
+			if( cause instanceof LuanException )
+				throw (LuanException)cause;
+			throw new LuanException(cause);
+		} catch(InstantiationException e) {
+			throw new RuntimeException(e);
+		}
+		return rtnConverter.convert(rtn);
+	}
+
+	private static final Map primitiveMap = new HashMap();
+	static {
+		primitiveMap.put(Boolean.TYPE,Boolean.class);
+		primitiveMap.put(Character.TYPE,Character.class);
+		primitiveMap.put(Byte.TYPE,Byte.class);
+		primitiveMap.put(Short.TYPE,Short.class);
+		primitiveMap.put(Integer.TYPE,Integer.class);
+		primitiveMap.put(Long.TYPE,Long.class);
+		primitiveMap.put(Float.TYPE,Float.class);
+		primitiveMap.put(Double.TYPE,Double.class);
+		primitiveMap.put(Void.TYPE,Void.class);
+	}
+
+	private void checkArgs(Object[] args) throws LuanException {
+		Class[] a = getParameterTypes();
+		int start = takesLuaState ? 1 : 0;
+		for( int i=start; i<a.length; i++ ) {
+			Class paramType = a[i];
+			Class type = paramType;
+			if( type.isPrimitive() )
+				type = (Class)primitiveMap.get(type);
+			Object arg = args[i];
+			if( !type.isInstance(arg) ) {
+				String expected;
+				if( i==a.length-1 && method.isVarArgs() )
+					expected = fixType(paramType.getComponentType().getSimpleName())+"...";
+				else
+					expected = fixType(paramType.getSimpleName());
+				if( arg==null ) {
+					if( paramType.isPrimitive() )
+						throw new LuanException("bad argument #"+(i+1-start)+" ("+expected+" expected, got nil)");
+				} else {
+					String got = fixType(arg.getClass().getSimpleName());
+					throw new LuanException("bad argument #"+(i+1-start)+" ("+expected+" expected, got "+got+")");
+				}
+			}
+		}
+	}
+
+	private static String fixType(String type) {
+		if( type.equals("byte[]") )
+			return "binary";
+		if( type.equals("Double") )
+			return "number";
+		if( type.equals("LuanTable") )
+			return "table";
+		if( type.equals("Boolean") )
+			return "boolean";
+		if( type.equals("String") )
+			return "string";
+		if( type.equals("Closure") )
+			return "function";
+		if( type.equals("LuanJavaFunction") )
+			return "function";
+		return type;
+	}
+
+	private Object[] fixArgs(LuanState luan,Object[] args) throws LuanException {
+		int n = argConverters.length;
+		Object[] rtn;
+		int start = 0;
+		if( !takesLuaState && varArgCls==null && args.length == n ) {
+			rtn = args;
+		} else {
+			if( takesLuaState )
+				n++;
+			rtn = new Object[n];
+			if( takesLuaState ) {
+				rtn[start++] = luan;
+			}
+			n = argConverters.length;
+			if( varArgCls != null ) {
+				n--;
+				if( args.length < argConverters.length ) {
+					rtn[rtn.length-1] = Array.newInstance(varArgCls,0);
+				} else {
+					int len = args.length - n;
+					Object varArgs = Array.newInstance(varArgCls,len);
+					ArgConverter ac = argConverters[n];
+					for( int i=0; i<len; i++ ) {
+						Array.set( varArgs, i, ac.convert(luan,args[n+i]) );
+					}
+					rtn[rtn.length-1] = varArgs;
+				}
+			}
+			System.arraycopy(args,0,rtn,start,Math.min(args.length,n));
+		}
+		for( int i=0; i<n; i++ ) {
+			rtn[start+i] = argConverters[i].convert(luan,rtn[start+i]);
+		}
+		return rtn;
+	}
+
+
+	private interface RtnConverter {
+		public Object convert(Object obj);
+	}
+
+	private static final RtnConverter RTN_NOTHING = new RtnConverter() {
+		@Override public Object[] convert(Object obj) {
+			return NOTHING;
+		}
+	};
+
+	private static final RtnConverter RTN_SAME = new RtnConverter() {
+		@Override public Object convert(Object obj) {
+			return obj;
+		}
+	};
+
+	private static final RtnConverter RTN_ARRAY = new RtnConverter() {
+		@Override public Object convert(Object obj) {
+			if( obj == null )
+				return null;
+			Object[] a = new Object[Array.getLength(obj)];
+			for( int i=0; i<a.length; i++ ) {
+				a[i] = Array.get(obj,i);
+			}
+			return new LuanTable(new ArrayList<Object>(Arrays.asList(a)));
+		}
+	};
+
+	private static RtnConverter getRtnConverter(JavaMethod m) {
+		Class rtnType = m.getReturnType();
+		if( rtnType == Void.TYPE )
+			return RTN_NOTHING;
+		if( !m.isLuan() && rtnType.isArray() && !rtnType.getComponentType().isPrimitive() ) {
+//System.out.println("qqqqqq "+m);
+			return RTN_ARRAY;
+		}
+		return RTN_SAME;
+	}
+
+	private static boolean isNumber(Class rtnType) {
+		return rtnType == Short.TYPE
+			|| rtnType == Integer.TYPE
+			|| rtnType == Long.TYPE
+			|| rtnType == Float.TYPE
+			|| rtnType == Double.TYPE
+		;
+	}
+
+	private interface ArgConverter {
+		public Object convert(LuanState luan,Object obj) throws LuanException;
+	}
+
+	private static final ArgConverter ARG_SAME = new ArgConverter() {
+		public Object convert(LuanState luan,Object obj) {
+			return obj;
+		}
+		@Override public String toString() {
+			return "ARG_SAME";
+		}
+	};
+
+	private static final ArgConverter ARG_DOUBLE = new ArgConverter() {
+		public Object convert(LuanState luan,Object obj) {
+			if( obj instanceof Double )
+				return obj;
+			if( obj instanceof Number ) {
+				Number n = (Number)obj;
+				return n.doubleValue();
+			}
+			return obj;
+		}
+		@Override public String toString() {
+			return "ARG_DOUBLE";
+		}
+	};
+
+	private static final ArgConverter ARG_FLOAT = new ArgConverter() {
+		public Object convert(LuanState luan,Object obj) {
+			if( obj instanceof Float )
+				return obj;
+			if( obj instanceof Number ) {
+				Number n = (Number)obj;
+				return n.floatValue();
+			}
+			return obj;
+		}
+		@Override public String toString() {
+			return "ARG_FLOAT";
+		}
+	};
+
+	private static final ArgConverter ARG_LONG = new ArgConverter() {
+		public Object convert(LuanState luan,Object obj) {
+			if( obj instanceof Long )
+				return obj;
+			if( obj instanceof Number ) {
+				Number n = (Number)obj;
+				long r = n.longValue();
+				if( r==n.doubleValue() )
+					return r;
+			}
+			return obj;
+		}
+		@Override public String toString() {
+			return "ARG_LONG";
+		}
+	};
+
+	private static final ArgConverter ARG_INTEGER = new ArgConverter() {
+		public Object convert(LuanState luan,Object obj) {
+			if( obj instanceof Integer )
+				return obj;
+			if( obj instanceof Number ) {
+				Number n = (Number)obj;
+				int r = n.intValue();
+				if( r==n.doubleValue() )
+					return r;
+			}
+			return obj;
+		}
+		@Override public String toString() {
+			return "ARG_INTEGER";
+		}
+	};
+
+	private static final ArgConverter ARG_SHORT = new ArgConverter() {
+		public Object convert(LuanState luan,Object obj) {
+			if( obj instanceof Short )
+				return obj;
+			if( obj instanceof Number ) {
+				Number n = (Number)obj;
+				short r = n.shortValue();
+				if( r==n.doubleValue() )
+					return r;
+			}
+			return obj;
+		}
+		@Override public String toString() {
+			return "ARG_SHORT";
+		}
+	};
+
+	private static final ArgConverter ARG_BYTE = new ArgConverter() {
+		public Object convert(LuanState luan,Object obj) {
+			if( obj instanceof Byte )
+				return obj;
+			if( obj instanceof Number ) {
+				Number n = (Number)obj;
+				byte r = n.byteValue();
+				if( r==n.doubleValue() )
+					return r;
+			}
+			return obj;
+		}
+		@Override public String toString() {
+			return "ARG_BYTE";
+		}
+	};
+
+	private static final ArgConverter ARG_TABLE = new ArgConverter() {
+		public Object convert(LuanState luan,Object obj) {
+			if( obj == null )
+				return null;
+			if( obj instanceof List ) {
+				return new LuanTable((List)obj);
+			}
+			if( obj instanceof Map ) {
+				return new LuanTable((Map)obj);
+			}
+			if( obj instanceof Set ) {
+				return new LuanTable((Set)obj);
+			}
+			Class cls = obj.getClass();
+			if( cls.isArray() && !cls.getComponentType().isPrimitive() ) {
+				Object[] a = (Object[])obj;
+				return new LuanTable(Arrays.asList(a));
+			}
+			return obj;
+		}
+		@Override public String toString() {
+			return "ARG_TABLE";
+		}
+	};
+
+	private static final ArgConverter ARG_MAP = new ArgConverter() {
+		public Object convert(LuanState luan,Object obj) throws LuanException {
+			if( obj instanceof LuanTable ) {
+				LuanTable t = (LuanTable)obj;
+				return t.asMap(luan);
+			}
+			return obj;
+		}
+		@Override public String toString() {
+			return "ARG_MAP";
+		}
+	};
+
+	private static final ArgConverter ARG_LIST = new ArgConverter() {
+		public Object convert(LuanState luan,Object obj) {
+			if( obj instanceof LuanTable ) {
+				LuanTable t = (LuanTable)obj;
+				if( t.isList() )
+					return t.asList();
+			}
+			return obj;
+		}
+		@Override public String toString() {
+			return "ARG_LIST";
+		}
+	};
+
+	private static final ArgConverter ARG_SET = new ArgConverter() {
+		public Object convert(LuanState luan,Object obj) throws LuanException {
+			if( obj instanceof LuanTable ) {
+				LuanTable t = (LuanTable)obj;
+				if( t.isSet(luan) )
+					return t.asSet(luan);
+			}
+			return obj;
+		}
+		@Override public String toString() {
+			return "ARG_SET";
+		}
+	};
+
+	private static final ArgConverter ARG_COLLECTION = new ArgConverter() {
+		public Object convert(LuanState luan,Object obj) throws LuanException {
+			if( obj instanceof LuanTable ) {
+				LuanTable t = (LuanTable)obj;
+				if( t.isList() )
+					return t.asList();
+				if( t.isSet(luan) )
+					return t.asSet(luan);
+			}
+			return obj;
+		}
+		@Override public String toString() {
+			return "ARG_COLLECTION";
+		}
+	};
+
+	private static class ArgArray implements ArgConverter {
+		private final Object[] a;
+
+		ArgArray(Class cls) {
+			a = (Object[])Array.newInstance(cls.getComponentType(),0);
+		}
+
+		public Object convert(LuanState luan,Object obj) {
+			if( obj instanceof LuanTable ) {
+				LuanTable t = (LuanTable)obj;
+				if( t.isList() ) {
+					try {
+						return t.asList().toArray(a);
+					} catch(ArrayStoreException e) {}
+				}
+			}
+			return obj;
+		}
+	}
+
+	private static boolean takesLuaState(JavaMethod m) {
+		Class[] paramTypes = m.getParameterTypes();
+		return paramTypes.length > 0 && paramTypes[0].equals(LuanState.class);
+	}
+
+	private static ArgConverter[] getArgConverters(boolean takesLuaState,JavaMethod m) {
+		final boolean isVarArgs = m.isVarArgs();
+		Class[] paramTypes = m.getParameterTypes();
+		if( takesLuaState ) {
+			Class[] t = new Class[paramTypes.length-1];
+			System.arraycopy(paramTypes,1,t,0,t.length);
+			paramTypes = t;
+		}
+		ArgConverter[] a = new ArgConverter[paramTypes.length];
+		for( int i=0; i<a.length; i++ ) {
+			Class paramType = paramTypes[i];
+			if( isVarArgs && i == a.length-1 )
+				paramType = paramType.getComponentType();
+			a[i] = getArgConverter(paramType);
+		}
+		return a;
+	}
+
+	private static ArgConverter getArgConverter(Class cls) {
+		if( cls == Double.TYPE || cls.equals(Double.class) )
+			return ARG_DOUBLE;
+		if( cls == Float.TYPE || cls.equals(Float.class) )
+			return ARG_FLOAT;
+		if( cls == Long.TYPE || cls.equals(Long.class) )
+			return ARG_LONG;
+		if( cls == Integer.TYPE || cls.equals(Integer.class) )
+			return ARG_INTEGER;
+		if( cls == Short.TYPE || cls.equals(Short.class) )
+			return ARG_SHORT;
+		if( cls == Byte.TYPE || cls.equals(Byte.class) )
+			return ARG_BYTE;
+		if( cls.equals(LuanTable.class) )
+			return ARG_TABLE;
+		if( cls.equals(Map.class) )
+			return ARG_MAP;
+		if( cls.equals(List.class) )
+			return ARG_LIST;
+		if( cls.equals(Set.class) )
+			return ARG_SET;
+		if( cls.equals(Collection.class) )
+			return ARG_COLLECTION;
+		if( cls.isArray() && !cls.getComponentType().isPrimitive() )
+			return new ArgArray(cls);
+		return ARG_SAME;
+	}
+
+
+
+	private static abstract class JavaMethod {
+		abstract boolean isVarArgs();
+		abstract Class[] getParameterTypes();
+		abstract Object invoke(Object obj,Object... args)
+			throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, InstantiationException;
+		abstract Class getReturnType();
+		abstract boolean isLuan();
+	
+		static JavaMethod of(final Method m) {
+			return new JavaMethod() {
+				@Override boolean isVarArgs() {
+					return m.isVarArgs();
+				}
+				@Override Class[] getParameterTypes() {
+					return m.getParameterTypes();
+				}
+				@Override Object invoke(Object obj,Object... args)
+					throws IllegalAccessException, IllegalArgumentException, InvocationTargetException
+				{
+					return m.invoke(obj,args);
+				}
+				@Override Class getReturnType() {
+					return m.getReturnType();
+				}
+				@Override boolean isLuan() {
+					return m.getAnnotation(LuanMethod.class) != null;
+				}
+				@Override public String toString() {
+					return m.toString();
+				}
+			};
+		}
+	
+		static JavaMethod of(final Constructor c) {
+			return new JavaMethod() {
+				@Override boolean isVarArgs() {
+					return c.isVarArgs();
+				}
+				@Override Class[] getParameterTypes() {
+					return c.getParameterTypes();
+				}
+				@Override Object invoke(Object obj,Object... args)
+					throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, InstantiationException
+				{
+					return c.newInstance(args);
+				}
+				@Override Class getReturnType() {
+					return c.getDeclaringClass();
+				}
+				@Override boolean isLuan() {
+					return false;
+				}
+				@Override public String toString() {
+					return c.toString();
+				}
+			};
+		}
+	
+	}
+
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/LuanMeta.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/LuanMeta.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,69 @@
+package luan;
+
+import java.util.Map;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.HashSet;
+
+
+public abstract class LuanMeta {
+
+	public abstract Object __index(LuanState luan,LuanTable tbl,Object key) throws LuanException;
+
+	protected abstract Iterator keys(LuanTable tbl);
+
+	public LuanFunction __pairs(final LuanState luan,final LuanTable tbl) {
+		return new LuanFunction() {
+			final Iterator<Map.Entry<Object,Object>> iter1 = tbl.rawIterator();
+			final Iterator<Object> iter2 = keys(tbl);
+			final Set<Object> set = new HashSet<Object>();
+
+			@Override public Object[] call(LuanState luan,Object[] args) throws LuanException {
+				if( iter1.hasNext() ) {
+					Map.Entry<Object,Object> entry = iter1.next();
+					Object key = entry.getKey();
+					set.add(key);
+					return new Object[]{key,entry.getValue()};
+				}
+				while( iter2.hasNext() ) {
+					Object key = iter2.next();
+					if( set.add(key) ) {
+						Object value = __index(luan,tbl,key);
+						return new Object[]{key,value};
+					}
+				}
+				return LuanFunction.NOTHING;
+			}
+		};
+	}
+
+	public boolean canNewindex() {
+		return false;
+	}
+
+	public void __new_index(LuanState luan,LuanTable tbl,Object key,Object value) throws LuanException {
+		throw new UnsupportedOperationException();
+	}
+
+	protected abstract String type(LuanTable tbl);
+
+	public String __to_string(LuanState luan,LuanTable tbl) throws LuanException {
+		return type(tbl) + "-" + tbl.rawToString();
+	}
+
+	public LuanTable newMetatable() {
+		LuanTable mt = new LuanTable();
+		mt.rawPut( "__index", this );
+		mt.rawPut( "__pairs", this );
+		mt.rawPut( "__to_string", this );
+		if( canNewindex() )
+			mt.rawPut( "__new_index", this );
+		return mt;
+	}
+
+	public LuanTable newTable() {
+		LuanTable tbl = new LuanTable();
+		tbl.setMetatable( newMetatable() );
+		return tbl;
+	}
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/LuanMethod.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/LuanMethod.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,8 @@
+package luan;
+
+import java.lang.annotation.*;
+
+
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface LuanMethod {}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/LuanPropertyMeta.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/LuanPropertyMeta.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,75 @@
+package luan;
+
+import java.util.Map;
+import java.util.Iterator;
+
+
+public final class LuanPropertyMeta extends LuanMeta {
+	public static final LuanPropertyMeta INSTANCE = new LuanPropertyMeta();
+
+	private LuanPropertyMeta() {}
+
+	public LuanTable getters(LuanTable tbl) {
+		return (LuanTable)tbl.getMetatable().rawGet("get");
+	}
+
+	public LuanTable setters(LuanTable tbl) {
+		return (LuanTable)tbl.getMetatable().rawGet("set");
+	}
+
+	protected String type(LuanTable tbl) {
+		return (String)tbl.getMetatable().rawGet("type");
+	}
+
+	@Override public Object __index(LuanState luan,LuanTable tbl,Object key) throws LuanException {
+		Object obj = getters(tbl).rawGet(key);
+		if( obj == null )
+			return null;
+		if( !(obj instanceof LuanFunction) )
+			throw new LuanException("get for '"+key+"' isn't a function");
+		LuanFunction fn = (LuanFunction)obj;
+		return fn.call(luan);
+	}
+
+	@Override protected Iterator keys(final LuanTable tbl) {
+		return new Iterator() {
+			final Iterator<Map.Entry<Object,Object>> iter = getters(tbl).rawIterator();
+
+			@Override public boolean hasNext() {
+				return iter.hasNext();
+			}
+			@Override public Object next() {
+				return iter.next().getKey();
+			}
+			@Override public void remove() {
+				throw new UnsupportedOperationException();
+			}
+		};
+	}
+
+
+	@Override public boolean canNewindex() {
+		return true;
+	}
+
+	@Override public void __new_index(LuanState luan,LuanTable tbl,Object key,Object value) throws LuanException {
+		Object obj = setters(tbl).rawGet(key);
+		if( obj == null ) {
+			tbl.rawPut(key,value);
+			return;
+		}
+		if( !(obj instanceof LuanFunction) )
+			throw new LuanException("set for '"+key+"' isn't a function");
+		LuanFunction fn = (LuanFunction)obj;
+		fn.call(luan,new Object[]{value});
+	}
+
+	@Override public LuanTable newMetatable() {
+		LuanTable mt = super.newMetatable();
+		mt.rawPut( "get", new LuanTable() );
+		mt.rawPut( "set", new LuanTable() );
+		mt.rawPut( "type", "property" );
+		return mt;
+	}
+
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/LuanRuntimeException.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/LuanRuntimeException.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,8 @@
+package luan;
+
+
+public final class LuanRuntimeException extends RuntimeException {
+	public LuanRuntimeException(LuanException e) {
+		super(e);
+	}
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/LuanState.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/LuanState.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,97 @@
+package luan;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.lang.ref.Reference;
+import java.lang.ref.WeakReference;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.HashMap;
+import luan.impl.LuanCompiler;
+import luan.modules.BasicLuan;
+import luan.modules.JavaLuan;
+
+
+public final class LuanState implements DeepCloneable {
+
+	public LuanJava java;
+	private Map registry;
+	private final List<Reference<Closeable>> onClose = new ArrayList<Reference<Closeable>>();
+
+	public LuanState() {
+		java = new LuanJava();
+		registry = new HashMap();
+	}
+
+	private LuanState(LuanState luan) {}
+
+	@Override public LuanState shallowClone() {
+		return new LuanState(this);
+	}
+
+	@Override public void deepenClone(DeepCloneable dc,DeepCloner cloner) {
+		LuanState clone = (LuanState)dc;
+		clone.registry = cloner.deepClone(registry);
+		clone.java = (LuanJava)cloner.deepClone(java);
+	}
+
+	public final Map registry() {
+		return registry;
+	}
+
+	public void onClose(Closeable c) {
+		onClose.add(new WeakReference<Closeable>(c));
+	}
+
+	public void close() throws IOException {
+		for( Reference<Closeable> ref : onClose ) {
+			Closeable c = ref.get();
+			if( c != null )
+				c.close();
+		}
+		onClose.clear();
+	}
+/*
+	public final Object eval(String cmd) throws LuanException {
+		return eval(cmd,new LuanTable());
+	}
+
+	public final Object eval(String cmd,LuanTable env) throws LuanException {
+		LuanFunction fn = BasicLuan.load(this,cmd,"eval",env,true);
+		return fn.call(this);
+	}
+*/
+
+	public String toString(Object obj) throws LuanException {
+		if( obj instanceof LuanTable ) {
+			LuanTable tbl = (LuanTable)obj;
+			return tbl.toString(this);
+		}
+		if( obj == null )
+			return "nil";
+		if( obj instanceof Number )
+			return Luan.toString((Number)obj);
+		if( obj instanceof byte[] )
+			return "binary: " + Integer.toHexString(obj.hashCode());
+		return obj.toString();
+	}
+
+	public Object index(Object obj,Object key) throws LuanException {
+		if( obj instanceof LuanTable ) {
+			LuanTable tbl = (LuanTable)obj;
+			return tbl.get(this,key);
+		}
+		if( obj != null && java.ok )
+			return JavaLuan.__index(this,obj,key,false);
+		throw new LuanException("attempt to index a " + Luan.type(obj) + " value" );
+	}
+
+/*
+	public Number checkNumber(Object obj) throws LuanException {
+		if( obj instanceof Number )
+			return (Number)obj;
+		throw new LuanException( "attempt to perform arithmetic on '"+context()+"' (a " + Luan.type(obj) + " value)" );
+	}
+*/
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/LuanTable.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/LuanTable.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,451 @@
+package luan;
+
+import java.util.Iterator;
+import java.util.ListIterator;
+import java.util.Map;
+import java.util.AbstractMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Set;
+import java.util.HashSet;
+
+
+public final class LuanTable implements DeepCloneable {
+	private Map map = null;
+	private List list = null;
+	private LuanTable metatable = null;
+	public LuanJava java;
+
+	public LuanTable() {}
+
+	public LuanTable(List list) {
+		int n = list.size();
+		for( int i=0; i<n; i++ ) {
+			Object val = list.get(i);
+			if( val != null )
+				rawPut(i+1,val);
+		}
+	}
+
+	public LuanTable(Map map) {
+		for( Object stupid : map.entrySet() ) {
+			Map.Entry entry = (Map.Entry)stupid;
+			Object key = entry.getKey();
+			Object value = entry.getValue();
+			if( key != null && value != null )
+				rawPut(key,value);
+		}
+	}
+
+	public LuanTable(Set set) {
+		for( Object el : set ) {
+			if( el != null )
+				rawPut(el,Boolean.TRUE);
+		}
+	}
+
+	public LuanTable(LuanTable tbl) {
+		if( tbl.map != null && !tbl.map.isEmpty() )
+			this.map = new LinkedHashMap<Object,Object>(tbl.map);
+		if( tbl.rawLength() > 0 )
+			this.list = new ArrayList<Object>(tbl.list);
+		this.metatable = tbl.metatable;
+	}
+
+	@Override public LuanTable shallowClone() {
+		return new LuanTable();
+	}
+
+	@Override public void deepenClone(DeepCloneable dc,DeepCloner cloner) {
+		LuanTable clone = (LuanTable)dc;
+		if( map != null ) {
+			clone.map = newMap();
+			for( Object stupid : map.entrySet() ) {
+				Map.Entry entry = (Map.Entry)stupid;
+				clone.map.put( cloner.get(entry.getKey()), cloner.get(entry.getValue()) );
+			}
+		}
+		if( list != null ) {
+			clone.list = new ArrayList<Object>();
+			for( Object obj : list ) {
+				clone.list.add( cloner.get(obj) );
+			}
+		}
+		if( metatable != null )
+			clone.metatable = (LuanTable)cloner.get(metatable);
+		clone.java = (LuanJava)cloner.deepClone(java);
+	}
+
+	public boolean isList() {
+		return map==null || map.isEmpty();
+	}
+
+	public List<Object> asList() {
+		return list!=null ? list : Collections.emptyList();
+	}
+
+	public String toString(LuanState luan) throws LuanException {
+		Object h = getHandler("__to_string");
+		if( h == null )
+			return rawToString();
+		if( h instanceof LuanMeta ) {
+			LuanMeta meta = (LuanMeta)h;
+			return meta.__to_string(luan,this);
+		}
+		LuanFunction fn = Luan.checkFunction(h);
+		return Luan.checkString( Luan.first( fn.call(luan,new Object[]{this}) ) );
+	}
+
+	public String rawToString() {
+		return "table: " + Integer.toHexString(hashCode());
+	}
+
+	public Object get(LuanState luan,Object key) throws LuanException {
+		Object value = rawGet(key);
+		if( value != null )
+			return value;
+		Object h = getHandler("__index");
+		if( h==null )
+			return null;
+		if( h instanceof LuanFunction ) {
+			LuanFunction fn = (LuanFunction)h;
+			return Luan.first(fn.call(luan,new Object[]{this,key}));
+		}
+		if( h instanceof LuanMeta ) {
+			LuanMeta meta = (LuanMeta)h;
+			return meta.__index(luan,this,key);
+		}
+		return luan.index(h,key);
+	}
+
+	public Object rawGet(Object key) {
+		if( list != null ) {
+			Integer iT = Luan.asInteger(key);
+			if( iT != null ) {
+				int i = iT - 1;
+				if( i>=0 && i<list.size() )
+					return list.get(i);
+			}
+		}
+		if( map==null )
+			return null;
+		if( key instanceof Number && !(key instanceof Double) ) {
+			Number n = (Number)key;
+			key = Double.valueOf(n.doubleValue());
+		}
+		return map.get(key);
+	}
+
+	public void put(LuanState luan,Object key,Object value) throws LuanException {
+		Object h = getHandler("__new_index");
+		if( h==null || rawGet(key)!=null ) {
+			rawPut(key,value);
+			return;
+		}
+		if( h instanceof LuanFunction ) {
+			LuanFunction fn = (LuanFunction)h;
+			fn.call(luan,new Object[]{this,key,value});
+			return;
+		}
+		if( h instanceof LuanMeta ) {
+			LuanMeta meta = (LuanMeta)h;
+			meta.__new_index(luan,this,key,value);
+			return;
+		}
+		if( h instanceof LuanTable ) {
+			LuanTable tbl = (LuanTable)h;
+			tbl.put(luan,key,value);
+			return;
+		}
+		throw new LuanException("invalid type "+Luan.type(h)+" for metamethod __new_index");
+	}
+
+	public void rawPut(Object key,Object val) {
+		Integer iT = Luan.asInteger(key);
+		if( iT != null ) {
+			int i = iT - 1;
+			if( list != null || i == 0 ) {
+				if( i == list().size() ) {
+					if( val != null ) {
+						list.add(val);
+						mapToList();
+					}
+					return;
+				} else if( i>=0 && i<list.size() ) {
+					list.set(i,val);
+					if( val == null ) {
+						listToMap(i);
+					}
+					return;
+				}
+			}
+		}
+		if( map==null )
+			map = newMap();
+		if( key instanceof Number && !(key instanceof Double) ) {
+			Number n = (Number)key;
+			key = Double.valueOf(n.doubleValue());
+		}
+		if( val == null ) {
+			map.remove(key);
+		} else {
+			map.put(key,val);
+		}
+	}
+
+	private void mapToList() {
+		if( map != null ) {
+			while(true) {
+				Object v = map.remove(Double.valueOf(list.size()+1));
+				if( v == null )
+					break;
+				list.add(v);
+			}
+		}
+	}
+
+	private void listToMap(int from) {
+		if( list != null ) {
+			while( list.size() > from ) {
+				int i = list.size() - 1;
+				Object v = list.remove(i);
+				if( v != null ) {
+					if( map==null )
+						map = newMap();
+					map.put(i+1,v);
+				}
+			}
+		}
+	}
+
+	private List<Object> list() {
+		if( list == null ) {
+			list = new ArrayList<Object>();
+			mapToList();
+		}
+		return list;
+	}
+
+	public void rawInsert(int pos,Object value) {
+		if( value==null )
+			throw new IllegalArgumentException("can't insert a nil value");
+		list().add(pos-1,value);
+		mapToList();
+	}
+
+	public Object rawRemove(int pos) {
+		return list().remove(pos-1);
+	}
+
+	public void rawSort(Comparator<Object> cmp) {
+		Collections.sort(list(),cmp);
+	}
+
+	public int length(LuanState luan) throws LuanException {
+		Object h = getHandler("__len");
+		if( h != null ) {
+			LuanFunction fn = Luan.checkFunction(h);
+			return (Integer)Luan.first(fn.call(luan,new Object[]{this}));
+		}
+		return rawLength();
+	}
+
+	public int rawLength() {
+		return list==null ? 0 : list.size();
+	}
+
+	public Iterable<Map.Entry<Object,Object>> iterable(LuanState luan) throws LuanException {
+		final Iterator<Map.Entry<Object,Object>> iter = iterator(luan);
+		return new Iterable<Map.Entry<Object,Object>>() {
+			public Iterator<Map.Entry<Object,Object>> iterator() {
+				return iter;
+			}
+		};
+	}
+
+	public Iterable<Map.Entry<Object,Object>> rawIterable() throws LuanException {
+		final Iterator<Map.Entry<Object,Object>> iter = rawIterator();
+		return new Iterable<Map.Entry<Object,Object>>() {
+			public Iterator<Map.Entry<Object,Object>> iterator() {
+				return iter;
+			}
+		};
+	}
+
+	public Iterator<Map.Entry<Object,Object>> iterator(final LuanState luan) throws LuanException {
+		if( getHandler("__pairs") == null )
+			return rawIterator();
+		final LuanFunction fn = pairs(luan);
+		return new Iterator<Map.Entry<Object,Object>>() {
+			private Map.Entry<Object,Object> next = getNext();
+
+			private Map.Entry<Object,Object> getNext() {
+				try {
+					Object obj = fn.call(luan);
+					if( obj==null )
+						return null;
+					Object[] a = (Object[])obj;
+					if( a.length == 0 || a[0]==null )
+						return null;
+					return new AbstractMap.SimpleEntry<Object,Object>(a[0],a[1]);
+				} catch(LuanException e) {
+					throw new LuanRuntimeException(e);
+				}
+			}
+
+			public boolean hasNext() {
+				return next != null;
+			}
+
+			public Map.Entry<Object,Object> next() {
+				Map.Entry<Object,Object> rtn = next;
+				next = getNext();
+				return rtn;
+			}
+
+			public void remove() {
+				throw new UnsupportedOperationException();
+			}
+		};
+	}
+
+	public LuanFunction pairs(LuanState luan) throws LuanException {
+		Object h = getHandler("__pairs");
+		if( h != null ) {
+			if( h instanceof LuanFunction ) {
+				LuanFunction fn = (LuanFunction)h;
+				Object obj = Luan.first(fn.call(luan,new Object[]{this}));
+				if( !(obj instanceof LuanFunction) )
+					throw new LuanException( "metamethod __pairs should return function but returned " + Luan.type(obj) );
+				return (LuanFunction)obj;
+			}
+			if( h instanceof LuanMeta ) {
+				LuanMeta meta = (LuanMeta)h;
+				return meta.__pairs(luan,this);
+			}
+			throw new LuanException( "invalid type of metamethod __pairs: " + Luan.type(h) );
+		}
+		return rawPairs();
+	}
+
+	private LuanFunction rawPairs() {
+		return new LuanFunction() {
+			final Iterator<Map.Entry<Object,Object>> iter = rawIterator();
+
+			@Override public Object[] call(LuanState luan,Object[] args) {
+				if( !iter.hasNext() )
+					return LuanFunction.NOTHING;
+				Map.Entry<Object,Object> entry = iter.next();
+				return new Object[]{entry.getKey(),entry.getValue()};
+			}
+		};
+	}
+
+	public Iterator<Map.Entry<Object,Object>> rawIterator() {
+		if( list == null ) {
+			if( map == null )
+				return Collections.<Map.Entry<Object,Object>>emptyList().iterator();
+			return map.entrySet().iterator();
+		}
+		if( map == null )
+			return listIterator();
+		return new Iterator<Map.Entry<Object,Object>>() {
+			Iterator<Map.Entry<Object,Object>> iter = listIterator();
+			boolean isList = true;
+
+			public boolean hasNext() {
+				boolean b = iter.hasNext();
+				if( !b && isList ) {
+					iter = map.entrySet().iterator();
+					isList = false;
+					b = iter.hasNext();
+				}
+				return b;
+			}
+
+			public Map.Entry<Object,Object> next() {
+				return iter.next();
+			}
+
+			public void remove() {
+				throw new UnsupportedOperationException();
+			}
+		};
+	}
+
+	private Iterator<Map.Entry<Object,Object>> listIterator() {
+		if( list == null )
+			return Collections.<Map.Entry<Object,Object>>emptyList().iterator();
+		final ListIterator iter = list.listIterator();
+		return new Iterator<Map.Entry<Object,Object>>() {
+
+			public boolean hasNext() {
+				return iter.hasNext();
+			}
+
+			public Map.Entry<Object,Object> next() {
+				Integer key = iter.nextIndex()+1;
+				return new AbstractMap.SimpleEntry<Object,Object>(key,iter.next());
+			}
+
+			public void remove() {
+				throw new UnsupportedOperationException();
+			}
+		};
+	}
+
+	public LuanTable rawSubList(int from,int to) {
+		LuanTable tbl = shallowClone();
+		tbl.list = new ArrayList<Object>(list().subList(from-1,to-1));
+		return tbl;
+	}
+
+	public LuanTable getMetatable() {
+		return metatable;
+	}
+
+	public void setMetatable(LuanTable metatable) {
+		this.metatable = metatable;
+	}
+
+	public Object getHandler(String op) {
+		return metatable==null ? null : metatable.rawGet(op);
+	}
+
+	private Map<Object,Object> newMap() {
+		return new LinkedHashMap<Object,Object>();
+	}
+
+	public boolean isSet(LuanState luan) throws LuanException {
+		for( Map.Entry<Object,Object> entry : iterable(luan) ) {
+			if( !entry.getValue().equals(Boolean.TRUE) )
+				return false;
+		}
+		return true;
+	}
+
+	public Set<Object> asSet(LuanState luan) throws LuanException {
+		Set<Object> set = new HashSet<Object>();
+		for( Map.Entry<Object,Object> entry : iterable(luan) ) {
+			set.add(entry.getKey());
+		}
+		return set;
+	}
+
+	public Map<Object,Object> asMap(LuanState luan) throws LuanException {
+		Map<Object,Object> map = newMap();
+		for( Map.Entry<Object,Object> entry : iterable(luan) ) {
+			map.put(entry.getKey(),entry.getValue());
+		}
+		return map;
+	}
+
+	public void rawClear() {
+		map = null;
+		list = null;
+	}
+
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/cmd_line.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/cmd_line.luan	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,31 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local ipairs = Luan.ipairs or error()
+local load_file = Luan.load_file or error()
+local try = Luan.try or error()
+local Table = require "luan:Table.luan"
+local Io = require "luan:Io.luan"
+local print = Io.print or error()
+
+
+local args = {...}
+if #args == 0 then
+	print("Luan "..Luan.VERSION)
+	Io.debug("> ")
+else
+	local file = args[1]
+	Luan.arg = {}
+	for j,v in ipairs(args) do
+		Luan.arg[j-1] = v
+	end
+	try {
+		function()
+			local main_file = load_file(file)
+			print( main_file( Table.unpack(Luan.arg) ) )
+		end;
+		catch = function(e)
+--			java(); e.java.printStackTrace();
+			Io.print_to(Io.stderr, e )
+		end;
+	}
+end
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/impl/Closure.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/impl/Closure.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,48 @@
+package luan.impl;
+
+import luan.Luan;
+import luan.LuanFunction;
+import luan.LuanState;
+import luan.LuanException;
+import luan.DeepCloner;
+import luan.DeepCloneable;
+import luan.LuanJava;
+
+
+public abstract class Closure extends LuanFunction implements DeepCloneable, Cloneable {
+	public Pointer[] upValues;
+	public LuanJava java;
+
+	public Closure(int nUpValues,LuanJava java) throws LuanException {
+		this.upValues = new Pointer[nUpValues];
+		this.java = java;
+	}
+
+	@Override public Closure shallowClone() {
+		try {
+			return (Closure)clone();
+		} catch(CloneNotSupportedException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
+	@Override public void deepenClone(DeepCloneable dc,DeepCloner cloner) {
+		Closure clone = (Closure)dc;
+		clone.upValues = (Pointer[])cloner.deepClone(upValues);
+		clone.java = (LuanJava)cloner.deepClone(java);
+	}
+
+	@Override public final Object call(LuanState luan,Object[] args) throws LuanException {
+		LuanJava old = luan.java;
+		luan.java = java;
+		try {
+			return doCall(luan,args);
+		} catch(StackOverflowError e) {
+			throw new LuanException( "stack overflow" );
+		} finally {
+			luan.java = old;
+		}	
+	}
+
+	public abstract Object doCall(LuanState luan,Object[] args) throws LuanException;
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/impl/LuanCompiler.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/impl/LuanCompiler.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,87 @@
+package luan.impl;
+
+import java.lang.ref.WeakReference;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Map;
+import java.util.HashMap;
+import luan.LuanFunction;
+import luan.LuanState;
+import luan.LuanException;
+import luan.LuanTable;
+import luan.LuanJava;
+import luan.modules.JavaLuan;
+import luan.modules.PackageLuan;
+
+
+public final class LuanCompiler {
+	private static final Map<String,WeakReference<Class>> map = new HashMap<String,WeakReference<Class>>();
+
+	public static LuanFunction compile(String sourceText,String sourceName,LuanTable env) throws LuanException {
+		Class fnClass = env==null ? getClass(sourceText,sourceName) : getClass(sourceText,sourceName,env);
+		LuanJava java;
+		if( env == null ) {
+			java = new LuanJava();
+		} else {
+			java = env.java;
+			if( java == null ) {
+				java = new LuanJava();
+				env.java = java;
+			}
+		}
+		Closure closure;
+		try {
+			closure = (Closure)fnClass.getConstructor(LuanJava.class).newInstance(java);
+		} catch(NoSuchMethodException e) {
+			throw new RuntimeException(e);
+		} catch(InstantiationException e) {
+			throw new RuntimeException(e);
+		} catch(IllegalAccessException e) {
+			throw new RuntimeException(e);
+		} catch(InvocationTargetException e) {
+			throw new RuntimeException(e);
+		}
+		closure.upValues[0].o = JavaLuan.javaFn;
+		closure.upValues[1].o = PackageLuan.requireFn;
+		if( env != null )  closure.upValues[2].o = env;
+		return closure;
+	}
+
+	private static synchronized Class getClass(String sourceText,String sourceName) throws LuanException {
+		String key = sourceName + "~~~" + sourceText;
+		WeakReference<Class> ref = map.get(key);
+		if( ref != null ) {
+			Class cls = ref.get();
+			if( cls != null )
+				return cls;
+		}
+		Class cls = getClass(sourceText,sourceName,null);
+		map.put(key,new WeakReference<Class>(cls));
+		return cls;
+	}
+
+	private static Class getClass(String sourceText,String sourceName,LuanTable env) throws LuanException {
+		LuanParser parser = new LuanParser(sourceText,sourceName);
+		parser.addVar( "java" );
+		parser.addVar( "require" );
+		if( env != null )  parser.addVar( "_ENV" );
+		try {
+			return parser.RequiredModule();
+		} catch(ParseException e) {
+//e.printStackTrace();
+			throw new LuanException( e.getFancyMessage() );
+		}
+	}
+
+	public static String toJava(String sourceText,String sourceName) throws LuanException {
+		LuanParser parser = new LuanParser(sourceText,sourceName);
+		parser.addVar( "java" );
+		parser.addVar( "require" );
+		try {
+			return parser.RequiredModuleSource();
+		} catch(ParseException e) {
+			throw new LuanException( e.getFancyMessage() );
+		}
+	}
+
+	private LuanCompiler() {}  // never
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/impl/LuanImpl.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/impl/LuanImpl.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,250 @@
+package luan.impl;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.ArrayList;
+import luan.Luan;
+import luan.LuanState;
+import luan.LuanTable;
+import luan.LuanFunction;
+import luan.LuanException;
+import luan.modules.JavaLuan;
+
+
+public final class LuanImpl {
+	private LuanImpl() {}  // never
+
+	public static int len(LuanState luan,Object o) throws LuanException {
+		if( o instanceof String ) {
+			String s = (String)o;
+			return s.length();
+		}
+		if( o instanceof byte[] ) {
+			byte[] a = (byte[])o;
+			return a.length;
+		}
+		if( o instanceof LuanTable ) {
+			LuanTable t = (LuanTable)o;
+			return t.length(luan);
+		}
+		throw new LuanException( "attempt to get length of a " + Luan.type(o) + " value" );
+	}
+
+	public static Object unm(LuanState luan,Object o) throws LuanException {
+		if( o instanceof Number )
+			return -((Number)o).doubleValue();
+		if( o instanceof LuanTable ) {
+			LuanFunction fn = Luan.getHandlerFunction("__unm",(LuanTable)o);
+			if( fn != null ) {
+				return Luan.first(fn.call(luan,new Object[]{o}));
+			}
+		}
+		throw new LuanException("attempt to perform arithmetic on a "+Luan.type(o)+" value");
+	}
+
+	private static Object arithmetic(LuanState luan,String op,Object o1,Object o2) throws LuanException {
+		LuanFunction fn = Luan.getBinHandler(op,o1,o2);
+		if( fn != null )
+			return Luan.first(fn.call(luan,new Object[]{o1,o2}));
+		String type = !(o1 instanceof Number) ? Luan.type(o1) : Luan.type(o2);
+		throw new LuanException("attempt to perform arithmetic on a "+type+" value");
+	}
+
+	public static Object pow(LuanState luan,Object o1,Object o2) throws LuanException {
+		if( o1 instanceof Number && o2 instanceof Number )
+			return Math.pow( ((Number)o1).doubleValue(), ((Number)o2).doubleValue() );
+		return arithmetic(luan,"__pow",o1,o2);
+	}
+
+	public static Object mul(LuanState luan,Object o1,Object o2) throws LuanException {
+		if( o1 instanceof Number && o2 instanceof Number )
+			return ((Number)o1).doubleValue() * ((Number)o2).doubleValue();
+		return arithmetic(luan,"__mul",o1,o2);
+	}
+
+	public static Object div(LuanState luan,Object o1,Object o2) throws LuanException {
+		if( o1 instanceof Number && o2 instanceof Number )
+			return ((Number)o1).doubleValue() / ((Number)o2).doubleValue();
+		return arithmetic(luan,"__div",o1,o2);
+	}
+
+	public static Object mod(LuanState luan,Object o1,Object o2) throws LuanException {
+		if( o1 instanceof Number && o2 instanceof Number ) {
+			double d1 = ((Number)o1).doubleValue();
+			double d2 = ((Number)o2).doubleValue();
+			return d1 - Math.floor(d1/d2)*d2;
+		}
+		return arithmetic(luan,"__mod",o1,o2);
+	}
+
+	public static Object add(LuanState luan,Object o1,Object o2) throws LuanException {
+		if( o1 instanceof Number && o2 instanceof Number )
+			return ((Number)o1).doubleValue() + ((Number)o2).doubleValue();
+		return arithmetic(luan,"__add",o1,o2);
+	}
+
+	public static Object sub(LuanState luan,Object o1,Object o2) throws LuanException {
+		if( o1 instanceof Number && o2 instanceof Number )
+			return ((Number)o1).doubleValue() - ((Number)o2).doubleValue();
+		return arithmetic(luan,"__sub",o1,o2);
+	}
+
+	public static Object concat(LuanState luan,Object o1,Object o2) throws LuanException {
+		LuanFunction fn = Luan.getBinHandler("__concat",o1,o2);
+		if( fn != null )
+			return Luan.first(fn.call(luan,new Object[]{o1,o2}));
+		String s1 = luan.toString(o1);
+		String s2 = luan.toString(o2);
+		return s1 + s2;
+	}
+
+	public static boolean eq(LuanState luan,Object o1,Object o2) throws LuanException {
+		if( o1 == o2 || o1 != null && o1.equals(o2) )
+			return true;
+		if( o1 instanceof Number && o2 instanceof Number ) {
+			Number n1 = (Number)o1;
+			Number n2 = (Number)o2;
+			return n1.doubleValue() == n2.doubleValue();
+		}
+		if( o1 instanceof byte[] && o2 instanceof byte[] ) {
+			byte[] b1 = (byte[])o1;
+			byte[] b2 = (byte[])o2;
+			return Arrays.equals(b1,b2);
+		}
+		if( !(o1 instanceof LuanTable && o2 instanceof LuanTable) )
+			return false;
+		LuanTable t1 = (LuanTable)o1;
+		LuanTable t2 = (LuanTable)o2;
+		LuanTable mt1 = t1.getMetatable();
+		LuanTable mt2 = t2.getMetatable();
+		if( mt1==null || mt2==null )
+			return false;
+		Object f = mt1.rawGet("__eq");
+		if( f == null || !f.equals(mt2.rawGet("__eq")) )
+			return false;
+		LuanFunction fn = Luan.checkFunction(f);
+		return Luan.checkBoolean( Luan.first(fn.call(luan,new Object[]{o1,o2})) );
+	}
+
+	public static boolean le(LuanState luan,Object o1,Object o2) throws LuanException {
+		if( o1 instanceof Number && o2 instanceof Number ) {
+			Number n1 = (Number)o1;
+			Number n2 = (Number)o2;
+			return n1.doubleValue() <= n2.doubleValue();
+		}
+		if( o1 instanceof String && o2 instanceof String ) {
+			String s1 = (String)o1;
+			String s2 = (String)o2;
+			return s1.compareTo(s2) <= 0;
+		}
+		LuanFunction fn = Luan.getBinHandler("__le",o1,o2);
+		if( fn != null )
+			return Luan.checkBoolean( Luan.first(fn.call(luan,new Object[]{o1,o2})) );
+		fn = Luan.getBinHandler("__lt",o1,o2);
+		if( fn != null )
+			return !Luan.checkBoolean( Luan.first(fn.call(luan,new Object[]{o2,o1})) );
+		throw new LuanException( "attempt to compare " + Luan.type(o1) + " with " + Luan.type(o2) );
+	}
+
+	public static boolean lt(LuanState luan,Object o1,Object o2) throws LuanException {
+		return Luan.isLessThan(luan,o1,o2);
+	}
+
+	public static boolean cnd(Object o) throws LuanException {
+		return !(o == null || Boolean.FALSE.equals(o));
+	}
+
+	public static void nop(Object o) {}
+
+	public static void put(LuanState luan,Object t,Object key,Object value) throws LuanException {
+		if( t instanceof LuanTable ) {
+			LuanTable tbl = (LuanTable)t;
+			tbl.put(luan,key,value);
+			return;
+		}
+		if( t != null && luan.java.ok )
+			JavaLuan.__new_index(luan,t,key,value);
+		else
+			throw new LuanException( "attempt to index a " + Luan.type(t) + " value" );
+	}
+
+	public static Object pick(Object o,int i) {
+		if( i < 1 )
+			throw new RuntimeException();
+		if( !(o instanceof Object[]) )
+			return null;
+		Object[] a = (Object[])o;
+		return i<a.length ? a[i] : null;
+	}
+
+	public static Object[] varArgs(Object[] a,int i) {
+		if( i >= a.length )
+			return LuanFunction.NOTHING;
+		Object[] rtn = new Object[a.length - i];
+		System.arraycopy(a,i,rtn,0,rtn.length);
+		return rtn;
+	}
+
+	public static Object[] concatArgs(Object o1,Object o2) {
+		if( o1 instanceof Object[] ) {
+			Object[] a1 = (Object[])o1;
+			if( o2 instanceof Object[] ) {
+				Object[] a2 = (Object[])o2;
+				Object[] rtn = new Object[a1.length+a2.length];
+				System.arraycopy(a1,0,rtn,0,a1.length);
+				System.arraycopy(a2,0,rtn,a1.length,a2.length);
+				return rtn;
+			} else {
+				Object[] rtn = new Object[a1.length+1];
+				System.arraycopy(a1,0,rtn,0,a1.length);
+				rtn[a1.length] = o2;
+				return rtn;
+			}
+		} else {
+			if( o2 instanceof Object[] ) {
+				Object[] a2 = (Object[])o2;
+				Object[] rtn = new Object[1+a2.length];
+				rtn[0] = o1;
+				System.arraycopy(a2,0,rtn,1,a2.length);
+				return rtn;
+			} else {
+				Object[] rtn = new Object[2];
+				rtn[0] = o1;
+				rtn[2] = o2;
+				return rtn;
+			}
+		}
+	}
+
+	public static LuanTable table(Object[] a) {
+		LuanTable table = new LuanTable();
+		int i = 0;
+		for( Object fld : a ) {
+			if( fld instanceof TableField ) {
+				TableField tblFld = (TableField)fld;
+				Object key = tblFld.key;
+				Object value = tblFld.value;
+				if( key != null && value != null )
+					table.rawPut(key,value);
+			} else {
+				i++;
+				if( fld != null )
+					table.rawPut(i,fld);
+			}
+		}
+		return table;
+	}
+
+	public static Object first(Object[] a) {
+		return a.length==0 ? null : a[0];
+	}
+
+	public static String strconcat(String... a) {
+		StringBuilder sb = new StringBuilder();
+		for( String s : a ) {
+			sb.append(s);
+		}
+		return sb.toString();
+	}
+
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/impl/LuanJavaCompiler.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/impl/LuanJavaCompiler.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,100 @@
+package luan.impl;
+
+import java.io.OutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.StringWriter;
+import java.io.IOException;
+import java.net.URI;
+import java.util.Collections;
+import java.util.Map;
+import java.util.HashMap;
+import javax.tools.FileObject;
+import javax.tools.JavaFileObject;
+import javax.tools.SimpleJavaFileObject;
+import javax.tools.JavaCompiler;
+import javax.tools.ToolProvider;
+import javax.tools.JavaFileManager;
+import javax.tools.StandardJavaFileManager;
+import javax.tools.ForwardingJavaFileManager;
+
+
+public final class LuanJavaCompiler {
+	private LuanJavaCompiler() {}  // never
+
+	private static class MyJavaFileObject extends SimpleJavaFileObject {
+		final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+
+		MyJavaFileObject() {
+			super(URI.create("whatever"),JavaFileObject.Kind.CLASS);
+		}
+
+		@Override public OutputStream openOutputStream() {
+			return baos;
+		}
+
+		byte[] byteCode(String sourceName) {
+			byte[] byteCode = baos.toByteArray();
+			final int len = sourceName.length();
+			int max = byteCode.length-len-3;
+			outer:
+			for( int i=0; true; i++ ) {
+				if( i > max )
+					throw new RuntimeException("len="+len);
+				if( byteCode[i]==1 && (byteCode[i+1] << 8 | 0xFF & byteCode[i+2]) == len ) {
+					for( int j=i+3; j<i+3+len; j++ ) {
+						if( byteCode[j] != '$' )
+							continue outer;
+					}
+					System.arraycopy(sourceName.getBytes(),0,byteCode,i+3,len);
+					break;
+				}
+			}
+			return byteCode;
+		}
+	}
+
+	public static Class compile(final String className,final String sourceName,final String code) throws ClassNotFoundException {
+		final int len = sourceName.length();
+		StringBuilder sb = new StringBuilder(sourceName);
+		for( int i=0; i<len; i++ )
+			sb.setCharAt(i,'$');
+		JavaFileObject sourceFile = new SimpleJavaFileObject(URI.create(sb.toString()),JavaFileObject.Kind.SOURCE) {
+			@Override public CharSequence getCharContent(boolean ignoreEncodingErrors) {
+				return code;
+			}
+			@Override public String getName() {
+				return sourceName;
+			}
+			@Override public boolean isNameCompatible(String simpleName,JavaFileObject.Kind kind) {
+				return true;
+			}
+		};
+		final Map<String,MyJavaFileObject> map = new HashMap<String,MyJavaFileObject>();
+		JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
+		StandardJavaFileManager sjfm = compiler.getStandardFileManager(null,null,null);
+		ForwardingJavaFileManager fjfm = new ForwardingJavaFileManager(sjfm) {
+			@Override public JavaFileObject getJavaFileForOutput(JavaFileManager.Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException {
+				if( map.containsKey(className) )
+					throw new RuntimeException(className);
+				MyJavaFileObject classFile = new MyJavaFileObject();
+				map.put(className,classFile);
+				return classFile;
+			}
+		};
+		StringWriter out = new StringWriter();
+		boolean b = compiler.getTask(out, fjfm, null, null, null, Collections.singletonList(sourceFile)).call();
+		if( !b )
+			throw new RuntimeException("\n"+out+"\ncode:\n"+code+"\n");
+		ClassLoader cl = new ClassLoader() {
+			@Override protected Class<?> findClass(String name) throws ClassNotFoundException {
+				MyJavaFileObject jfo = map.get(name);
+				if( jfo != null ) {
+					byte[] byteCode = jfo.byteCode(sourceName);
+					return defineClass(name, byteCode, 0, byteCode.length);
+				}
+				return super.findClass(name);
+			}
+		};
+		return cl.loadClass(className);
+	}
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/impl/LuanParser.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/impl/LuanParser.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,2004 @@
+package luan.impl;
+
+//import java.io.StringWriter;
+//import java.io.PrintWriter;
+import java.util.Set;
+import java.util.HashSet;
+import java.util.Arrays;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.concurrent.atomic.AtomicInteger;
+import luan.Luan;
+import luan.LuanState;
+import luan.LuanTable;
+import luan.modules.PackageLuan;
+
+
+final class LuanParser {
+
+	private interface Sym {
+		public Expr exp();
+	}
+
+	private int symCounter = 0;
+
+	private class LocalSym implements Sym {
+		final String name;
+		final String javaName;
+		boolean isPointer = false;
+
+		LocalSym(String name) {
+			this.name = name;
+			this.javaName = name + "_" + (++symCounter);
+		}
+
+		Stmts declaration(Expr value) {
+			Stmts stmt = new Stmts();
+			if( value==null ) {
+				stmt.add( new Object() {
+					@Override public String toString() {
+						if( !isPointer )
+							return "Object " + javaName + ";  ";
+						else
+							return "final Pointer " + javaName + " = new Pointer();  ";
+					}
+				} );
+			} else {
+				if( value.valType != Val.SINGLE )  throw new RuntimeException();
+				stmt.add( new Object() {
+					@Override public String toString() {
+						if( !isPointer )
+							return "Object " + javaName + " = ";
+						else
+							return "final Pointer " + javaName + " = new Pointer(";
+					}
+				} );
+				stmt.addAll(value);
+				stmt.add( new Object() {
+					@Override public String toString() {
+						if( !isPointer )
+							return ";  ";
+						else
+							return ");  ";
+					}
+				} );
+			}
+			return stmt;
+		}
+
+		@Override public Expr exp() {
+			Expr exp = new Expr(Val.SINGLE,false);
+			exp.add( new Object() {
+				@Override public String toString() {
+					if( !isPointer )
+						return javaName;
+					else
+						return javaName + ".o";
+				}
+			} );
+			return exp;
+		}
+	}
+
+	private class UpSym implements Sym {
+		final String name;
+		final int i;
+		final String value;
+
+		UpSym(String name,int i,String value) {
+			this.name = name;
+			this.i = i;
+			this.value = value;
+		}
+
+		String init() {
+			return "upValues[" + i + "] = " + value + ";  ";
+		}
+
+		@Override public Expr exp() {
+			Expr exp = new Expr(Val.SINGLE,false);
+			exp.add( new Object() {
+				@Override public String toString() {
+					return "upValues[" + i + "].o";
+				}
+			} );
+			return exp;
+		}
+	}
+
+	private final class Frame {
+		final Frame parent;
+		final List<LocalSym> symbols = new ArrayList<LocalSym>();
+		int loops = 0;
+		boolean isVarArg = false;
+		final List<UpSym> upValueSymbols = new ArrayList<UpSym>();
+
+		Frame() {
+			this.parent = null;
+		}
+
+		Frame(Frame parent) {
+			this.parent = parent;
+		}
+
+		LocalSym addLocalSym(String name) {
+			LocalSym sym = new LocalSym(name);
+			symbols.add(sym);
+			return sym;
+		}
+
+		UpSym addUpSym(String name,String value) {
+			UpSym sym = new UpSym( name, upValueSymbols.size(), value );
+			upValueSymbols.add(sym);
+			return sym;
+		}
+
+		LocalSym getLocalSym(String name) {
+			int i = symbols.size();
+			while( --i >= 0 ) {
+				LocalSym sym = symbols.get(i);
+				if( sym.name.equals(name) )
+					return sym;
+			}
+			return null;
+		}
+
+		UpSym getUpSym(String name) {
+			for( UpSym upSym : upValueSymbols ) {
+				if( upSym.name.equals(name) )
+					return upSym;
+			}
+			if( parent != null ) {
+				LocalSym sym = parent.getLocalSym(name);
+				if( sym != null ) {
+					sym.isPointer = true;
+					return addUpSym(name,sym.javaName);
+				}
+				UpSym upSym = parent.getUpSym(name);
+				if( upSym != null ) {
+					return addUpSym(name,"parentUpValues["+upSym.i+"]");
+				}
+			}
+			return null;
+		}
+
+		Sym getSym(String name) {
+			Sym sym = getLocalSym(name);
+			return sym != null ? sym : getUpSym(name);
+		}
+
+	}
+
+	private static class In {
+		static final In NOTHING = new In(false);
+
+		final boolean template;
+
+		private In(boolean template) {
+			this.template = template;
+		}
+
+		In template() {
+			return template ? this : new In(true);
+		}
+	}
+
+	private Frame frame;
+	private final Parser parser;
+	private final Stmts top;
+
+	LuanParser(String sourceText,String sourceName) {
+		this.frame = new Frame();
+		this.parser = new Parser(sourceText,sourceName);
+		this.top = new Stmts();
+	}
+
+	void addVar(String name) {
+		UpSym upSym = frame.addUpSym( "-ADDED-" ,"new Pointer()");
+		LocalSym sym = frame.addLocalSym( name );
+		sym.isPointer = true;
+		top.add( "final Pointer " + sym.javaName + " = upValues[" + upSym.i + "];  " );
+	}
+
+	private int symbolsSize() {
+		return frame.symbols.size();
+	}
+
+	private Stmts addSymbol(String name,Expr value) {
+		final LocalSym sym = frame.addLocalSym(name);
+		return sym.declaration(value);
+	}
+
+	private Sym getSym(String name) {
+		return frame.getSym(name);
+	}
+
+	private void popSymbols(int n) {
+		List<LocalSym> symbols = frame.symbols;
+		while( n-- > 0 ) {
+			symbols.remove(symbols.size()-1);
+		}
+	}
+
+	private void incLoops() {
+		frame.loops++;
+	}
+
+	private void decLoops() {
+		frame.loops--;
+	}
+
+	private <T> T required(T t) throws ParseException {
+		if( t==null )
+			throw parser.exception();
+		return t;
+	}
+
+	private <T> T required(T t,String msg) throws ParseException {
+		if( t==null )
+			throw parser.exception(msg);
+		return t;
+	}
+
+	private Class newFnClass(Stmts stmt) {
+		return toFnClass( stmt, frame.upValueSymbols );
+	}
+
+	private Expr newFnExp(Stmts stmt,String name) {
+		return toFnExp( stmt, frame.upValueSymbols, name );
+	}
+/*
+	Class Expression() throws ParseException {
+		Spaces();
+		parser.begin();
+		Expr expr = ExprZ(In.NOTHING);
+		if( expr != null && parser.endOfInput() ) {
+			top.add( "return " );
+			top.addAll( expr );
+			top.add( ";  " );
+			top.hasReturn = true;
+			return parser.success(newFnClass(top));
+		}
+		return parser.failure(null);
+	}
+*/
+	Class RequiredModule() throws ParseException {
+		GetRequiredModule();
+		return newFnClass(top);
+	}
+
+	String RequiredModuleSource() throws ParseException {
+		GetRequiredModule();
+		return toFnString( top, frame.upValueSymbols );
+	}
+
+	void GetRequiredModule() throws ParseException {
+		Spaces();
+		parser.begin();
+		frame.isVarArg = true;
+		top.add( "final Object[] varArgs = LuanImpl.varArgs(args,0);  " );
+		Stmts block = RequiredBlock();
+		top.addAll( block );
+		top.hasReturn = block.hasReturn;
+		if( !parser.endOfInput() )
+			throw parser.exception();
+		parser.success();
+	}
+
+	private Stmts RequiredBlock() throws ParseException {
+		Stmts stmts = new Stmts();
+		int stackStart = symbolsSize();
+		do {
+			Spaces();
+			stmts.addNewLines();
+			Stmts stmt = Stmt();
+			if( stmt != null ) {
+				stmts.addAll(stmt);
+				stmts.hasReturn = stmt.hasReturn;
+			}
+		} while( !stmts.hasReturn && (StmtSep() || TemplateSep(stmts)) );
+		Spaces();
+		while( StmtSep() )
+			Spaces();
+		stmts.addNewLines();
+		int stackEnd = symbolsSize();
+		popSymbols( stackEnd - stackStart );
+		return stmts;
+	}
+
+	private boolean StmtSep() throws ParseException {
+		return parser.match( ';' ) || EndOfLine();
+	}
+
+	private boolean TemplateSep(Stmts stmts) throws ParseException {
+		Stmts stmt = TemplateStmt();
+		if( stmt != null ) {
+			stmts.addAll(stmt);
+			return true;
+		}
+		return false;
+	}
+
+	private boolean EndOfLine() {
+		if( MatchEndOfLine() ) {
+			parser.sb().append('\n');
+			return true;
+		} else {
+			return false;
+		}
+	}
+
+	private boolean MatchEndOfLine() {
+		return parser.match( "\r\n" ) || parser.match( '\r' ) || parser.match( '\n' );
+	}
+
+	private Stmts Stmt() throws ParseException {
+		Stmts stmt;
+		if( (stmt=ReturnStmt()) != null
+			|| (stmt=FunctionStmt()) != null
+			|| (stmt=LocalStmt()) != null
+			|| (stmt=LocalFunctionStmt()) != null
+			|| (stmt=BreakStmt()) != null
+			|| (stmt=ForStmt()) != null
+			|| (stmt=DoStmt()) != null
+			|| (stmt=WhileStmt()) != null
+			|| (stmt=RepeatStmt()) != null
+			|| (stmt=IfStmt()) != null
+			|| (stmt=SetStmt()) != null
+			|| (stmt=ExpressionsStmt()) != null
+		) {
+			return stmt;
+		}
+		return null;
+	}
+
+	private Expr indexExpStr(Expr exp1,Expr exp2) {
+		Expr exp = new Expr(Val.SINGLE,false);
+		exp.add( "luan.index(" );
+		exp.addAll( exp1.single() );
+		exp.add( "," );
+		exp.addAll( exp2.single() );
+		exp.add( ")" );
+		return exp;
+	}
+
+	private Expr callExpStr(Expr fn,Expr args) {
+		Expr exp = new Expr(null,true);
+		exp.add( "Luan.checkFunction(" );
+		exp.addAll( fn.single() );
+		exp.add( ").call(luan," );
+		exp.addAll( args.array() );
+		exp.add( ")" );
+		return exp;
+	}
+
+	private Stmts TemplateStmt() throws ParseException {
+		Expr exprs = TemplateExpressions(In.NOTHING);
+		if( exprs == null )
+			return null;
+		Stmts stmt = new Stmts();
+		stmt.add( "Luan.checkFunction(luan.index(PackageLuan.require(luan,\"luan:Io.luan\"),\"template_write\")).call(luan," );
+		stmt.addAll( exprs.array() );
+		stmt.add( ");  " );
+		return stmt;
+	}
+
+	private Expr TemplateExpressions(In in) throws ParseException {
+		if( in.template )
+			return null;
+		int start = parser.begin();
+		if( !parser.match( "%>" ) )
+			return parser.failure(null);
+		EndOfLine();
+		In inTemplate = in.template();
+		List<Expr> builder = new ArrayList<Expr>();
+		while(true) {
+			if( parser.match( "<%=" ) ) {
+				Spaces();
+				Expr exp = new Expr(Val.SINGLE,false);
+				exp.addAll( RequiredExpr(inTemplate).single() );
+				builder.add(exp);
+				RequiredMatch( "%>" );
+			} else if( parser.match( "<%" ) ) {
+				Spaces();
+				return parser.success(expString(builder));
+			} else {
+				Expr exp = new Expr(Val.SINGLE,false);
+				int i = parser.currentIndex();
+				do {
+					if( parser.match( "%>" ) )
+						throw parser.exception("'%>' unexpected");
+					if( !(EndOfLine() || parser.anyChar()) )
+						throw parser.exception("Unclosed template expression");
+				} while( !parser.test( "<%" ) );
+				String match = parser.textFrom(i);
+				String rtns = parser.sb().toString();
+				parser.sb().setLength(0);
+				exp.addAll( constExpStr(match) );
+				if( rtns.length() > 0 )
+					exp.add(rtns);
+				builder.add(exp);
+			}
+		}
+	}
+
+	private Stmts ReturnStmt() throws ParseException {
+		parser.begin();
+		if( !Keyword("return") )
+			return parser.failure(null);
+		Expr exprs = ExpStringList(In.NOTHING);
+		Stmts stmt = new Stmts();
+		stmt.add( "return " );
+		if( exprs != null )
+			stmt.addAll( exprs );
+		else
+			stmt.add( "LuanFunction.NOTHING" );
+		stmt.add( ";  " );
+		stmt.hasReturn = true;
+		return parser.success( stmt );
+	}
+
+	private Stmts FunctionStmt() throws ParseException {
+		parser.begin();
+		if( !Keyword("function") )
+			return parser.failure(null);
+
+		parser.currentIndex();
+		String name = RequiredName();
+		Var var = nameVar(name);
+		while( parser.match( '.' ) ) {
+			Spaces();
+//			Expr exp = NameExpr();
+			name = Name();
+			if( name==null )
+				return parser.failure(null);
+			var = indexVar( var.exp(), constExpStr(name) );
+		}
+
+		Expr fnDef = RequiredFunction(name);
+		return parser.success( var.set(fnDef) );
+	}
+
+	private Stmts LocalFunctionStmt() throws ParseException {
+		parser.begin();
+		if( !(Keyword("local") && Keyword("function")) )
+			return parser.failure(null);
+		Stmts stmt = new Stmts();
+		String name = RequiredName();
+		stmt.addAll( addSymbol(name,null) );
+		Expr fnDef = RequiredFunction(name);
+		stmt.addAll( nameVar(name).set(fnDef) );
+		return parser.success( stmt );
+	}
+
+	private Stmts BreakStmt() throws ParseException {
+		parser.begin();
+		if( !Keyword("break") )
+			return parser.failure(null);
+		if( frame.loops <= 0 )
+			throw parser.exception("'break' outside of loop");
+		Stmts stmt = new Stmts();
+		stmt.add( "break;  " );
+		return parser.success( stmt );
+	}
+
+	int forCounter = 0;
+
+	private Stmts ForStmt() throws ParseException {
+		parser.begin();
+		int stackStart = symbolsSize();
+		if( !Keyword("for") )
+			return parser.failure(null);
+		List<String> names = RequiredNameList();
+		if( !Keyword("in") )
+			return parser.failure(null);
+		Expr expr = RequiredExpr(In.NOTHING).single();
+		RequiredKeyword("do");
+
+		String fnVar = "fn"+ ++forCounter;
+		Expr fnExp = new Expr(null,false);
+		fnExp.add( fnVar + ".call(luan)" );
+		Stmts stmt = new Stmts();
+		stmt.add( ""
+			+"LuanFunction "+fnVar+" = Luan.checkFunction("
+		);
+		stmt.addAll( expr );
+		stmt.add( ");  " );
+		stmt.add( "while(true) {  " );
+		stmt.addAll( makeLocalSetStmt(names,fnExp) );
+		stmt.add( "if( " );
+		stmt.addAll( nameVar(names.get(0)).exp() );
+ 		stmt.add( "==null )  break;  " );
+		Stmts loop = RequiredLoopBlock();
+		RequiredKeyword("end");
+		stmt.addAll( loop );
+		stmt.add( "}  " );
+		popSymbols( symbolsSize() - stackStart );
+		return parser.success(stmt);
+	}
+
+	private Stmts DoStmt() throws ParseException {
+		parser.begin();
+		if( !Keyword("do") )
+			return parser.failure(null);
+		Stmts stmt = RequiredBlock();
+		RequiredKeyword("end");
+		return parser.success(stmt);
+	}
+
+	private Stmts LocalStmt() throws ParseException {
+		parser.begin();
+		if( !Keyword("local") )
+			return parser.failure(null);
+		List<String> names = NameList();
+		if( names==null ) {
+			if( Keyword("function") )
+				return parser.failure(null);  // handled later
+			throw parser.exception("Invalid local statement");
+		}
+		Stmts stmt = new Stmts();
+		if( parser.match( '=' ) ) {
+			Spaces();
+			Expr values = ExpStringList(In.NOTHING);
+			if( values==null )
+				throw parser.exception("Expressions expected");
+			stmt.addAll( makeLocalSetStmt(names,values) );
+		} else {
+			Expr value = new Expr(Val.SINGLE,false);
+			value.add( "null" );
+			for( String name : names ) {
+				stmt.addAll( addSymbol(name,value) );
+			}
+		}
+		return parser.success(stmt);
+	}
+
+	private List<String> RequiredNameList() throws ParseException {
+		parser.begin();
+		List<String> names = NameList();
+		if( names==null )
+			throw parser.exception("Name expected");
+		return parser.success(names);
+	}
+
+	private List<String> NameList() throws ParseException {
+		String name = Name();
+		if( name==null )
+			return null;
+		List<String> names = new ArrayList<String>();
+		names.add(name);
+		while( (name=anotherName()) != null ) {
+			names.add(name);
+		}
+		return names;
+	}
+
+	private String anotherName() throws ParseException {
+		parser.begin();
+		if( !parser.match( ',' ) )
+			return parser.failure(null);
+		Spaces();
+		String name = Name();
+		if( name==null )
+			return parser.failure(null);
+		return parser.success(name);
+	}
+
+	private Stmts WhileStmt() throws ParseException {
+		parser.begin();
+		if( !Keyword("while") )
+			return parser.failure(null);
+		Expr cnd = RequiredExpr(In.NOTHING).single();
+		RequiredKeyword("do");
+		Stmts loop = RequiredLoopBlock();
+		RequiredKeyword("end");
+		Stmts stmt = new Stmts();
+		stmt.add( "while( Luan.checkBoolean(" );
+		stmt.addAll( cnd );
+		stmt.add( ") ) {  " );
+		stmt.addAll( loop );
+		stmt.add( "}  " );
+		return parser.success( stmt );
+	}
+
+	private Stmts RepeatStmt() throws ParseException {
+		parser.begin();
+		if( !Keyword("repeat") )
+			return parser.failure(null);
+		Stmts loop =RequiredLoopBlock();
+		RequiredKeyword("until");
+		Expr cnd = RequiredExpr(In.NOTHING).single();
+		Stmts stmt = new Stmts();
+		stmt.add( "do {  " );
+		stmt.addAll( loop );
+		stmt.add( "} while( !Luan.checkBoolean(" );
+		stmt.addAll( cnd );
+		stmt.add( ") );  " );
+		return parser.success( stmt );
+	}
+
+	private Stmts RequiredLoopBlock() throws ParseException {
+		incLoops();
+		Stmts stmt = RequiredBlock();
+		decLoops();
+		return stmt;
+	}
+
+	private Stmts IfStmt() throws ParseException {
+		parser.begin();
+		if( !Keyword("if") )
+			return parser.failure(null);
+		Stmts stmt = new Stmts();
+		Expr cnd;
+		Stmts block;
+		boolean hasReturn = true;
+		cnd = RequiredExpr(In.NOTHING).single();
+		RequiredKeyword("then");
+		block = RequiredBlock();
+		stmt.add( "if( Luan.checkBoolean(" );
+		stmt.addAll( cnd );
+		stmt.add( ") ) {  " );
+		stmt.addAll( block );
+		if( !block.hasReturn )
+			hasReturn = false;
+		while( Keyword("elseif") ) {
+			cnd = RequiredExpr(In.NOTHING).single();
+			RequiredKeyword("then");
+			block = RequiredBlock();
+			stmt.add( "} else if( Luan.checkBoolean(" );
+			stmt.addAll( cnd );
+			stmt.add( ") ) {  " );
+			stmt.addAll( block );
+			if( !block.hasReturn )
+				hasReturn = false;
+		}
+		if( Keyword("else") ) {
+			block = RequiredBlock();
+			stmt.add( "} else {  " );
+			stmt.addAll( block );
+			if( !block.hasReturn )
+				hasReturn = false;
+		} else {
+			hasReturn = false;
+		}
+		RequiredKeyword("end");
+		stmt.add( "}  " );
+		stmt.hasReturn = hasReturn;
+		return parser.success( stmt );
+	}
+
+	private Stmts SetStmt() throws ParseException {
+		parser.begin();
+		List<Var> vars = new ArrayList<Var>();
+		Var v = SettableVar();
+		if( v == null )
+			return parser.failure(null);
+		vars.add(v);
+		while( parser.match( ',' ) ) {
+			Spaces();
+			v = SettableVar();
+			if( v == null )
+				return parser.failure(null);
+			vars.add(v);
+		}
+		if( !parser.match( '=' ) )
+			return parser.failure(null);
+		Spaces();
+		Expr values = ExpStringList(In.NOTHING);
+		if( values==null )
+//			throw parser.exception("Expressions expected");
+			return parser.failure(null);
+		return parser.success( makeSetStmt(vars,values) );
+	}
+
+	private Stmts makeSetStmt(List<Var> vars,Expr values) throws ParseException {
+		int n = vars.size();
+		if( n == 1 )
+			return vars.get(0).set(values);
+		Stmts stmt = new Stmts();
+		String varName = values.valType==Val.ARRAY ? "a" : "t";
+		stmt.add( varName + " = " );
+		stmt.addAll( values );
+		stmt.add( ";  " );
+		Expr t = new Expr(values.valType,false);
+		t.add( varName );
+		t = t.single();
+		stmt.addAll( vars.get(0).set(t) );
+		for( int i=1; i<n; i++ ) {
+			t.clear();
+			t.add( "LuanImpl.pick(" + varName + ","+i+")" );
+			stmt.addAll( vars.get(i).set(t) );
+		}
+		return stmt;
+	}
+
+	private Stmts makeLocalSetStmt(List<String> names,Expr values) throws ParseException {
+		int n = names.size();
+		if( n == 1 )
+			return addSymbol(names.get(0),values.single());
+		Stmts stmt = new Stmts();
+		String varName = values.valType==Val.ARRAY ? "a" : "t";
+		stmt.add( varName + " = " );
+		stmt.addAll( values );
+		stmt.add( ";  " );
+		Expr t = new Expr(values.valType,false);
+		t.add( varName );
+		t = t.single();
+		stmt.addAll( addSymbol(names.get(0),t) );
+		for( int i=1; i<n; i++ ) {
+			t.clear();
+			t.add( "LuanImpl.pick(" + varName + ","+i+")" );
+			stmt.addAll( addSymbol(names.get(i),t) );
+		}
+		return stmt;
+	}
+
+	private Stmts ExpressionsStmt() throws ParseException {
+		parser.begin();
+		Expr exp = ExprZ(In.NOTHING);
+		if( exp != null && exp.isStmt ) {
+			Stmts stmt = new Stmts();
+			if( exp.valType==Val.SINGLE ) {
+				stmt.add( "LuanImpl.nop(" );
+				stmt.addAll( exp );
+				stmt.add( ")" );
+			} else {
+				stmt.addAll( exp );
+			}
+			stmt.add( ";  " );
+			return parser.success( stmt );
+		}
+		return parser.failure(null);
+	}
+
+	private Var SettableVar() throws ParseException {
+		int start = parser.begin();
+		Var var = VarZ(In.NOTHING);
+		if( var==null || !var.isSettable() )
+			return parser.failure(null);
+		return parser.success( var );
+	}
+
+	private Expr RequiredExpr(In in) throws ParseException {
+		parser.begin();
+		return parser.success(required(ExprZ(in),"Bad expression"));
+	}
+
+	private Expr ExprZ(In in) throws ParseException {
+		return OrExpr(in);
+	}
+
+	private Expr OrExpr(In in) throws ParseException {
+		parser.begin();
+		Expr exp = AndExpr(in);
+		if( exp==null )
+			return parser.failure(null);
+		while( Keyword("or") ) {
+			exp = exp.single();
+			Expr exp2 = required(AndExpr(in)).single();
+			Expr newExp = new Expr(Val.SINGLE,true);
+			newExp.add( "(LuanImpl.cnd(t = " );
+			newExp.addAll( exp );
+			newExp.add( ") ? t : (" );
+			newExp.addAll( exp2 );
+			newExp.add( "))" );
+			exp = newExp;
+		}
+		return parser.success(exp);
+	}
+
+	private Expr AndExpr(In in) throws ParseException {
+		parser.begin();
+		Expr exp = RelExpr(in);
+		if( exp==null )
+			return parser.failure(null);
+		while( Keyword("and") ) {
+			exp = exp.single();
+			Expr exp2 = required(RelExpr(in)).single();
+			Expr newExp = new Expr(Val.SINGLE,true);
+			newExp.add( "(LuanImpl.cnd(t = " );
+			newExp.addAll( exp );
+			newExp.add( ") ? (" );
+			newExp.addAll( exp2 );
+			newExp.add( ") : t)" );
+			exp = newExp;
+		}
+		return parser.success(exp);
+	}
+
+	private Expr RelExpr(In in) throws ParseException {
+		parser.begin();
+		Expr exp = ConcatExpr(in);
+		if( exp==null )
+			return parser.failure(null);
+		while(true) {
+			if( parser.match("==") ) {
+				Spaces();
+				exp = exp.single();
+				Expr exp2 = required(ConcatExpr(in)).single();
+				Expr newExp = new Expr(Val.SINGLE,false);
+				newExp.add( "LuanImpl.eq(luan," );
+				newExp.addAll( exp );
+				newExp.add( "," );
+				newExp.addAll( exp2 );
+				newExp.add( ")" );
+				exp = newExp;
+			} else if( parser.match("~=") ) {
+				Spaces();
+				exp = exp.single();
+				Expr exp2 = required(ConcatExpr(in)).single();
+				Expr newExp = new Expr(Val.SINGLE,false);
+				newExp.add( "!LuanImpl.eq(luan," );
+				newExp.addAll( exp );
+				newExp.add( "," );
+				newExp.addAll( exp2 );
+				newExp.add( ")" );
+				exp = newExp;
+			} else if( parser.match("<=") ) {
+				Spaces();
+				exp = exp.single();
+				Expr exp2 = required(ConcatExpr(in)).single();
+				Expr newExp = new Expr(Val.SINGLE,false);
+				newExp.add( "LuanImpl.le(luan," );
+				newExp.addAll( exp );
+				newExp.add( "," );
+				newExp.addAll( exp2 );
+				newExp.add( ")" );
+				exp = newExp;
+			} else if( parser.match(">=") ) {
+				Spaces();
+				exp = exp.single();
+				Expr exp2 = required(ConcatExpr(in)).single();
+				Expr newExp = new Expr(Val.SINGLE,false);
+				newExp.add( "LuanImpl.le(luan," );
+				newExp.addAll( exp2 );
+				newExp.add( "," );
+				newExp.addAll( exp );
+				newExp.add( ")" );
+				exp = newExp;
+			} else if( parser.match("<") ) {
+				Spaces();
+				exp = exp.single();
+				Expr exp2 = required(ConcatExpr(in)).single();
+				Expr newExp = new Expr(Val.SINGLE,false);
+				newExp.add( "LuanImpl.lt(luan," );
+				newExp.addAll( exp );
+				newExp.add( "," );
+				newExp.addAll( exp2 );
+				newExp.add( ")" );
+				exp = newExp;
+			} else if( parser.match(">") ) {
+				Spaces();
+				exp = exp.single();
+				Expr exp2 = required(ConcatExpr(in)).single();
+				Expr newExp = new Expr(Val.SINGLE,false);
+				newExp.add( "LuanImpl.lt(luan," );
+				newExp.addAll( exp2 );
+				newExp.add( "," );
+				newExp.addAll( exp );
+				newExp.add( ")" );
+				exp = newExp;
+			} else
+				break;
+		}
+		return parser.success(exp);
+	}
+
+	private Expr ConcatExpr(In in) throws ParseException {
+		parser.begin();
+		Expr exp = SumExpr(in);
+		if( exp==null )
+			return parser.failure(null);
+		if( parser.match("..") ) {
+			Spaces();
+			exp = exp.single();
+			Expr exp2 = required(ConcatExpr(in)).single();
+			Expr newExp = new Expr(Val.SINGLE,false);
+			newExp.add( "LuanImpl.concat(luan," );
+			newExp.addAll( exp );
+			newExp.add( "," );
+			newExp.addAll( exp2 );
+			newExp.add( ")" );
+			exp = newExp;
+		}
+		return parser.success(exp);
+	}
+
+	private Expr SumExpr(In in) throws ParseException {
+		parser.begin();
+		Expr exp = TermExpr(in);
+		if( exp==null )
+			return parser.failure(null);
+		while(true) {
+			if( parser.match('+') ) {
+				Spaces();
+				exp = exp.single();
+				Expr exp2 = required(TermExpr(in)).single();
+				Expr newExp = new Expr(Val.SINGLE,false);
+				newExp.add( "LuanImpl.add(luan," );
+				newExp.addAll( exp );
+				newExp.add( "," );
+				newExp.addAll( exp2 );
+				newExp.add( ")" );
+				exp = newExp;
+			} else if( Minus() ) {
+				Spaces();
+				exp = exp.single();
+				Expr exp2 = required(TermExpr(in)).single();
+				Expr newExp = new Expr(Val.SINGLE,false);
+				newExp.add( "LuanImpl.sub(luan," );
+				newExp.addAll( exp );
+				newExp.add( "," );
+				newExp.addAll( exp2 );
+				newExp.add( ")" );
+				exp = newExp;
+			} else
+				break;
+		}
+		return parser.success(exp);
+	}
+
+	private boolean Minus() {
+		parser.begin();
+		return parser.match('-') && !parser.match('-') ? parser.success() : parser.failure();
+	}
+
+	private Expr TermExpr(In in) throws ParseException {
+		parser.begin();
+		Expr exp = UnaryExpr(in);
+		if( exp==null )
+			return parser.failure(null);
+		while(true) {
+			if( parser.match('*') ) {
+				Spaces();
+				exp = exp.single();
+				Expr exp2 = required(UnaryExpr(in)).single();
+				Expr newExp = new Expr(Val.SINGLE,false);
+				newExp.add( "LuanImpl.mul(luan," );
+				newExp.addAll( exp );
+				newExp.add( "," );
+				newExp.addAll( exp2 );
+				newExp.add( ")" );
+				exp = newExp;
+			} else if( parser.match('/') ) {
+				Spaces();
+				exp = exp.single();
+				Expr exp2 = required(UnaryExpr(in)).single();
+				Expr newExp = new Expr(Val.SINGLE,false);
+				newExp.add( "LuanImpl.div(luan," );
+				newExp.addAll( exp );
+				newExp.add( "," );
+				newExp.addAll( exp2 );
+				newExp.add( ")" );
+				exp = newExp;
+			} else if( Mod() ) {
+				Spaces();
+				exp = exp.single();
+				Expr exp2 = required(UnaryExpr(in)).single();
+				Expr newExp = new Expr(Val.SINGLE,false);
+				newExp.add( "LuanImpl.mod(luan," );
+				newExp.addAll( exp );
+				newExp.add( "," );
+				newExp.addAll( exp2 );
+				newExp.add( ")" );
+				exp = newExp;
+			} else
+				break;
+		}
+		return parser.success(exp);
+	}
+
+	private boolean Mod() {
+		parser.begin();
+		return parser.match('%') && !parser.match('>') ? parser.success() : parser.failure();
+	}
+
+	private Expr UnaryExpr(In in) throws ParseException {
+		parser.begin();
+		if( parser.match('#') ) {
+			Spaces();
+			Expr exp = required(UnaryExpr(in)).single();
+			Expr newExp = new Expr(Val.SINGLE,false);
+			newExp.add( "LuanImpl.len(luan," );
+			newExp.addAll( exp );
+			newExp.add( ")" );
+			return parser.success(newExp);
+		}
+		if( Minus() ) {
+			Spaces();
+			Expr exp = required(UnaryExpr(in)).single();
+			Expr newExp = new Expr(Val.SINGLE,false);
+			newExp.add( "LuanImpl.unm(luan," );
+			newExp.addAll( exp );
+			newExp.add( ")" );
+			return parser.success(newExp);
+		}
+		if( Keyword("not") ) {
+			Spaces();
+			Expr exp = required(UnaryExpr(in)).single();
+			Expr newExp = new Expr(Val.SINGLE,false);
+			newExp.add( "!Luan.checkBoolean(" );
+			newExp.addAll( exp );
+			newExp.add( ")" );
+			return parser.success(newExp);
+		}
+		Expr exp = PowExpr(in);
+		if( exp==null )
+			return parser.failure(null);
+		return parser.success(exp);
+	}
+
+	private Expr PowExpr(In in) throws ParseException {
+		parser.begin();
+		Expr exp1 = SingleExpr(in);
+		if( exp1==null )
+			return parser.failure(null);
+		if( parser.match('^') ) {
+			Spaces();
+			Expr exp2 = required(PowExpr(in));
+			Expr newExp = new Expr(Val.SINGLE,false);
+			newExp.add( "LuanImpl.pow(luan," );
+			newExp.addAll( exp1.single() );
+			newExp.add( "," );
+			newExp.addAll( exp2.single() );
+			newExp.add( ")" );
+			exp1 = newExp;
+		}
+		return parser.success(exp1);
+	}
+
+	private Expr SingleExpr(In in) throws ParseException {
+		parser.begin();
+		Expr exp = FunctionExpr();
+		if( exp != null )
+			return parser.success(exp);
+		exp = VarExp(in);
+		if( exp != null )
+			return parser.success(exp);
+		exp = VarArgs();
+		if( exp != null )
+			return parser.success(exp);
+		return parser.failure(null);
+	}
+
+	private Expr FunctionExpr() throws ParseException {
+		if( !Keyword("function") )
+			return null;
+		return RequiredFunction(null);
+	}
+
+	private Expr RequiredFunction(String name) throws ParseException {
+		parser.begin();
+		RequiredMatch('(');
+		Spaces();
+		frame = new Frame(frame);
+		Stmts stmt = new Stmts();
+		List<String> names = NameList();
+		if( names != null ) {
+			Expr args = new Expr(Val.ARRAY,false);
+			args.add( "args" );
+			stmt.addAll( makeLocalSetStmt(names,args) );
+			if( parser.match(',') ) {
+				Spaces();
+				if( !parser.match("...") )
+					throw parser.exception();
+				Spaces();
+				frame.isVarArg = true;
+				stmt.add( "final Object[] varArgs = LuanImpl.varArgs(args," + names.size() + ");  " );
+			}
+		} else if( parser.match("...") ) {
+			Spaces();
+			frame.isVarArg = true;
+			stmt.add( "final Object[] varArgs = LuanImpl.varArgs(args,0);  " );
+		}
+		RequiredMatch(')');
+		Spaces();
+		Stmts block = RequiredBlock();
+		stmt.addAll( block );
+		stmt.hasReturn = block.hasReturn;
+		Expr fnDef = newFnExp(stmt,name);
+		RequiredKeyword("end");
+		frame = frame.parent;
+		return parser.success(fnDef);
+	}
+
+	private Expr VarArgs() throws ParseException {
+		parser.begin();
+		if( !frame.isVarArg || !parser.match("...") )
+			return parser.failure(null);
+		Spaces();
+		Expr exp = new Expr(Val.ARRAY,false);
+		exp.add("varArgs");
+		return parser.success(exp);
+	}
+
+	private Expr TableExpr() throws ParseException {
+		parser.begin();
+		if( !parser.match('{') )
+			return parser.failure(null);
+		Expr tblExp = new Expr(Val.SINGLE,false);
+		tblExp.add( "LuanImpl.table(" );
+		Expr lastExp = tblExp;
+		List<Expr> builder = new ArrayList<Expr>();
+/*
+		Spaces();
+		Field(builder);
+		while( FieldSep() ) {
+			Spaces();
+			Field(builder);
+		}
+*/
+		do {
+			Spaces();  lastExp.addNewLines();
+			Expr exp = Field();
+			if( exp != null ) {
+				builder.add(exp);
+				lastExp = exp;
+				Spaces();  lastExp.addNewLines();
+			}
+		} while( FieldSep() );
+		Expr exp = TemplateExpressions(In.NOTHING);
+		if( exp != null )
+			builder.add(exp);
+		if( !parser.match('}') )
+			throw parser.exception("Expected table element or '}'");
+		tblExp.addAll( expString(builder).array() );
+		tblExp.add( ")" );
+		Spaces();
+		tblExp.addNewLines();
+		return parser.success( tblExp );
+	}
+
+	private boolean FieldSep() throws ParseException {
+		return parser.anyOf(",;") || EndOfLine();
+	}
+
+	private Expr Field() throws ParseException {
+		parser.begin();
+		Expr exp = SubExpr(In.NOTHING);
+		if( exp==null )
+			exp = NameExpr();
+		if( exp!=null && parser.match('=') ) {
+			Spaces();
+			Expr val = RequiredExpr(In.NOTHING).single();
+			Expr newExp = new Expr(Val.SINGLE,false);
+			newExp.add( "new TableField(" );
+			newExp.addAll( exp );
+			newExp.add( "," );
+			newExp.addAll( val );
+			newExp.add( ")" );
+			return parser.success(newExp);
+		}
+		parser.rollback();
+		Expr exprs = ExprZ(In.NOTHING);
+		if( exprs != null ) {
+			return parser.success(exprs);
+		}
+		return parser.failure(null);
+	}
+
+	private Expr VarExp(In in) throws ParseException {
+		Var var = VarZ(in);
+		return var==null ? null : var.exp();
+	}
+
+	private Var VarZ(In in) throws ParseException {
+		parser.begin();
+		Var var = VarStart(in);
+		if( var==null )
+			return parser.failure(null);
+		Var var2;
+		while( (var2=Var2(in,var.exp())) != null ) {
+			var = var2;
+		}
+		return parser.success(var);
+	}
+
+	private Var VarStart(In in) throws ParseException {
+		if( parser.match('(') ) {
+			Spaces();
+			Expr exp = RequiredExpr(in).single();
+			RequiredMatch(')');
+			Spaces();
+			return exprVar(exp);
+		}
+		String name = Name();
+		if( name != null )
+			return nameVar(name);
+		Expr exp;
+		exp = TableExpr();
+		if( exp != null )
+			return exprVar(exp);
+		exp = Literal();
+		if( exp != null )
+			return exprVar(exp);
+		return null;
+	}
+
+	private Var Var2(In in,Expr exp1) throws ParseException {
+		parser.begin();
+		Expr exp2 = SubExpr(in);
+		if( exp2 != null )
+			return parser.success(indexVar(exp1,exp2));
+		if( parser.match('.') ) {
+			Spaces();
+			exp2 = NameExpr();
+			if( exp2!=null )
+				return parser.success(indexVar(exp1,exp2));
+			return parser.failure(null);
+		}
+		Expr fnCall = Args( in, exp1, new ArrayList<Expr>() );
+		if( fnCall != null )
+			return parser.success(exprVar(fnCall));
+		return parser.failure(null);
+	}
+
+	private interface Var {
+		public Expr exp() throws ParseException;
+//		public Settable settable() throws ParseException;
+		public boolean isSettable();
+		public Stmts set(Expr val) throws ParseException;
+	}
+
+	private Expr env() {
+		Sym sym = getSym("_ENV");
+		if( sym != null )
+			return sym.exp();
+		return null;
+	}
+
+	private Var nameVar(final String name) {
+		return new Var() {
+
+			public Expr exp() throws ParseException {
+				Sym sym = getSym(name);
+				if( sym != null )
+					return sym.exp();
+				Expr envExpr = env();
+				if( envExpr != null )
+					return indexExpStr( envExpr, constExpStr(name) );
+				parser.failure(null);
+				throw parser.exception("name '"+name+"' not defined");
+			}
+
+			public boolean isSettable() {
+				return true;
+			}
+
+			public Stmts set(Expr val) throws ParseException {
+				Sym sym = getSym(name);
+				if( sym != null ) {
+					Stmts stmt = new Stmts();
+					stmt.addAll( sym.exp() );
+					stmt.add( " = " );
+					stmt.addAll( val.single() );
+					stmt.add( ";  " );
+					return stmt;
+				}
+				Expr envExpr = env();
+				if( envExpr != null )
+					return indexVar( envExpr, constExpStr(name) ).set(val);
+				parser.failure(null);
+				throw parser.exception("name '"+name+"' not defined");
+			}
+		};
+	}
+
+	private Var exprVar(final Expr expr) {
+		return new Var() {
+
+			public Expr exp() {
+				return expr;
+			}
+
+			public boolean isSettable() {
+				return false;
+			}
+
+			public Stmts set(Expr val) {
+				throw new RuntimeException();
+			}
+		};
+	}
+
+	private Var indexVar(final Expr table,final Expr key) {
+		return new Var() {
+
+			public Expr exp() {
+				return indexExpStr( table, key );
+			}
+
+			public boolean isSettable() {
+				return true;
+			}
+
+			public Stmts set(Expr val) {
+				Stmts stmt = new Stmts();
+				stmt.add( "LuanImpl.put(luan," );
+				stmt.addAll( table.single() );
+				stmt.add( "," );
+				stmt.addAll( key.single() );
+				stmt.add( "," );
+				stmt.addAll( val.single() );
+				stmt.add( ");  " );
+				return stmt;
+			}
+		};
+	}
+
+	private Expr Args(In in,Expr fn,List<Expr> builder) throws ParseException {
+		parser.begin();
+		return args(in,builder)
+			? parser.success( callExpStr( fn, expString(builder) ) )
+			: parser.failure((Expr)null);
+	}
+
+	private boolean args(In in,List<Expr> builder) throws ParseException {
+		parser.begin();
+		if( parser.match('(') ) {
+			Spaces();
+			ExpList(in,builder);  // optional
+			if( !parser.match(')') )
+				throw parser.exception("Expression or ')' expected");
+			Spaces();
+			return parser.success();
+		}
+		Expr exp = TableExpr();
+		if( exp != null ) {
+			builder.add(exp);
+			return parser.success();
+		}
+		exp = StringLiteral();
+		if( exp != null ) {
+			builder.add(exp);
+			return parser.success();
+		}
+		return parser.failure();
+	}
+
+	private Expr ExpStringList(In in) throws ParseException {
+		List<Expr> builder = new ArrayList<Expr>();
+		return ExpList(in,builder) ? expString(builder) : null;
+	}
+
+	private boolean ExpList(In in,List<Expr> builder) throws ParseException {
+		parser.begin();
+		Expr exp = TemplateExpressions(in);
+		if( exp != null ) {
+			builder.add(exp);
+			return parser.success();
+		}
+		exp = ExprZ(in);
+		if( exp==null )
+			return parser.failure();
+		exp.addNewLines();
+		builder.add(exp);
+		while( parser.match(',') ) {
+			Spaces();
+			exp = TemplateExpressions(in);
+			if( exp != null ) {
+				builder.add(exp);
+				return parser.success();
+			}
+			exp = RequiredExpr(in);
+			exp.addNewLines();
+			builder.add(exp);
+		}
+		return parser.success();
+	}
+
+	private Expr SubExpr(In in) throws ParseException {
+		parser.begin();
+		if( !parser.match('[') || parser.test("[") || parser.test("=") )
+			return parser.failure(null);
+		Spaces();
+		Expr exp = RequiredExpr(In.NOTHING).single();
+		RequiredMatch(']');
+		Spaces();
+		return parser.success(exp);
+	}
+
+	private Expr NameExpr() throws ParseException {
+		parser.begin();
+		String name = Name();
+		if( name==null )
+			return parser.failure(null);
+		return parser.success(constExpStr(name));
+	}
+
+	private String RequiredName() throws ParseException {
+		parser.begin();
+		String name = Name();
+		if( name==null )
+			throw parser.exception("Name expected");
+		return parser.success(name);
+	}
+
+	private String Name() throws ParseException {
+		int start = parser.begin();
+		if( !NameFirstChar() )
+			return parser.failure(null);
+		while( NameChar() );
+		String match = parser.textFrom(start);
+		if( keywords.contains(match) )
+			return parser.failure(null);
+		Spaces();
+		return parser.success(match);
+	}
+
+	private boolean NameChar() {
+		return NameFirstChar() || Digit();
+	}
+
+	private boolean NameFirstChar() {
+		return parser.inCharRange('a', 'z') || parser.inCharRange('A', 'Z') || parser.match('_');
+	}
+
+	private void RequiredMatch(char c) throws ParseException {
+		if( !parser.match(c) )
+			throw parser.exception("'"+c+"' expected");
+	}
+
+	private void RequiredMatch(String s) throws ParseException {
+		if( !parser.match(s) )
+			throw parser.exception("'"+s+"' expected");
+	}
+
+	private void RequiredKeyword(String keyword) throws ParseException {
+		if( !Keyword(keyword) )
+			throw parser.exception("'"+keyword+"' expected");
+	}
+
+	private boolean Keyword(String keyword) throws ParseException {
+		parser.begin();
+		if( !parser.match(keyword) || NameChar() )
+			return parser.failure();
+		Spaces();
+		return parser.success();
+	}
+
+	private static final Set<String> keywords = new HashSet<String>(Arrays.asList(
+		"and",
+		"break",
+		"do",
+		"else",
+		"elseif",
+		"end",
+		"false",
+		"for",
+		"function",
+		"goto",
+		"if",
+		"in",
+		"local",
+		"nil",
+		"not",
+		"or",
+		"repeat",
+		"return",
+		"then",
+		"true",
+		"until",
+		"while"
+	));
+
+	private Expr Literal() throws ParseException {
+		parser.begin();
+		if( NilLiteral() ) {
+			Expr exp = new Expr(Val.SINGLE,false);
+			exp.add( "null" );
+			return parser.success(exp);
+		}
+		Boolean b = BooleanLiteral();
+		if( b != null ) {
+			Expr exp = new Expr(Val.SINGLE,false);
+			exp.add( b.toString() );
+			return parser.success(exp);
+		}
+		Number n = NumberLiteral();
+		if( n != null ) {
+			String s = n.toString();
+			if( n instanceof Long )
+				s += "L";
+			Expr exp = new Expr(Val.SINGLE,false);
+			exp.add( s );
+			return parser.success(exp);
+		}
+		Expr s = StringLiteral();
+		if( s != null )
+			return parser.success(s);
+		return parser.failure(null);
+	}
+
+	private static int STR_LIM = 65000;
+
+	private Expr constExpStr(String s) {
+		s = s
+			.replace("\\","\\\\")
+			.replace("\"","\\\"")
+			.replace("\n","\\n")
+			.replace("\r","\\r")
+			.replace("\t","\\t")
+			.replace("\b","\\b")
+		;
+		if( s.length() > STR_LIM ) {
+			int len = s.length();
+			StringBuilder sb = new StringBuilder();
+			sb.append( "LuanImpl.strconcat(" );
+			int start = 0;
+			while(true) {
+				int end = start + STR_LIM;
+				if( end >= len )
+					break;
+				sb.append( "\"" ).append( s.substring(start,end) ).append( "\"," );
+				start = end;
+			}
+			sb.append( "\"" ).append( s.substring(start) ).append( "\")" );
+			s = sb.toString();
+		} else
+			s = "\"" + s + "\"";
+		Expr exp = new Expr(Val.SINGLE,false);
+		exp.add( s );
+		return exp;
+	}
+
+	private boolean NilLiteral() throws ParseException {
+		return Keyword("nil");
+	}
+
+	private Boolean BooleanLiteral() throws ParseException {
+		if( Keyword("true") )
+			return true;
+		if( Keyword("false") )
+			return false;
+		return null;
+	}
+
+	private Number NumberLiteral() throws ParseException {
+		parser.begin();
+		Number n;
+		if( parser.matchIgnoreCase("0x") ) {
+			n = HexNumber();
+		} else {
+			n = DecNumber();
+		}
+		if( n==null || NameChar() )
+			return parser.failure(null);
+		Spaces();
+		return parser.success(n);
+	}
+
+	private Number DecNumber() {
+		int start = parser.begin();
+		boolean isInt = true;
+		if( Int() ) {
+			if( parser.match('.') ) {
+				isInt = false;
+				Int();  // optional
+			}
+		} else if( parser.match('.') && Int() ) {
+			// ok
+			isInt = false;
+		} else
+			return parser.failure(null);
+		if( Exponent() )  // optional
+			isInt = false;
+		String s = parser.textFrom(start);
+		if( isInt ) {
+			try {
+				return parser.success(Integer.valueOf(s));
+			} catch(NumberFormatException e) {}
+			try {
+				return parser.success(Long.valueOf(s));
+			} catch(NumberFormatException e) {}
+		}
+		return parser.success(Double.valueOf(s));
+	}
+
+	private boolean Exponent() {
+		parser.begin();
+		if( !parser.matchIgnoreCase("e") )
+			return parser.failure();
+		parser.anyOf("+-");  // optional
+		if( !Int() )
+			return parser.failure();
+		return parser.success();
+	}
+
+	private boolean Int() {
+		if( !Digit() )
+			return false;
+		while( Digit() );
+		return true;
+	}
+
+	private boolean Digit() {
+		return parser.inCharRange('0', '9');
+	}
+
+	private Number HexNumber() {
+		int start = parser.begin();
+		long nLong = 0;
+		double n;
+		if( HexInt() ) {
+			nLong = Long.parseLong(parser.textFrom(start),16);
+			n = (double)nLong;
+			if( parser.match('.') ) {
+				start = parser.currentIndex();
+				if( HexInt() ) {
+					String dec = parser.textFrom(start);
+					n += (double)Long.parseLong(dec,16) / Math.pow(16,dec.length());
+				}
+			}
+		} else if( parser.match('.') && HexInt() ) {
+			String dec = parser.textFrom(start+1);
+			n = (double)Long.parseLong(dec,16) / Math.pow(16,dec.length());
+		} else {
+			return parser.failure(null);
+		}
+		if( parser.matchIgnoreCase("p") ) {
+			parser.anyOf("+-");  // optional
+			start = parser.currentIndex();
+			if( !HexInt() )
+				return parser.failure(null);
+			n *= Math.pow(2,(double)Long.parseLong(parser.textFrom(start)));
+		}
+		if( nLong == n ) {
+			int nInt = (int)nLong;
+			if( nInt == nLong )
+				return parser.success(Integer.valueOf(nInt));
+			return parser.success(Long.valueOf(nLong));
+		}
+		return parser.success(Double.valueOf(n));
+	}
+
+	private boolean HexInt() {
+		if( !HexDigit() )
+			return false;
+		while( HexDigit() );
+		return true;
+	}
+
+
+	private boolean HexDigit() {
+		return Digit() || parser.anyOf("abcdefABCDEF");
+	}
+
+	private Expr StringLiteral() throws ParseException {
+		Expr s;
+		if( (s=QuotedString('"'))==null
+			&& (s=QuotedString('\''))==null
+			&& (s=LongString())==null
+		)
+			return null;
+		Spaces();
+		return s;
+	}
+
+	private Expr LongString() throws ParseException {
+		parser.begin();
+		if( !parser.match('[') )
+			return parser.failure(null);
+		int start = parser.currentIndex();
+		while( parser.match('=') );
+		int nEquals = parser.currentIndex() - start;
+		if( !parser.match('[') )
+			return parser.failure(null);
+		EndOfLine();
+		start = parser.currentIndex();
+		while( !LongBracketsEnd(nEquals) ) {
+			if( !(EndOfLine() || parser.anyChar()) )
+				throw parser.exception("Unclosed long string");
+		}
+		String s = parser.text.substring( start, parser.currentIndex() - nEquals - 2 );
+		String rtns = parser.sb().toString();
+		parser.sb().setLength(0);
+		Expr exp = constExpStr(s);
+		if( rtns.length() > 0 )
+			exp.add(rtns);
+		return parser.success(exp);
+	}
+
+	private Expr QuotedString(char quote) throws ParseException {
+		parser.begin();
+		if( !parser.match(quote) )
+			return parser.failure(null);
+		StringBuilder buf = new StringBuilder();
+		while( !parser.match(quote) ) {
+			Character c = EscSeq();
+			if( c != null ) {
+				buf.append(c);
+			} else {
+				if( parser.test('\r') || parser.test('\n') || !parser.anyChar() )
+					throw parser.exception("Unclosed string");
+				buf.append(parser.lastChar());
+			}
+		}
+		return parser.success(constExpStr(buf.toString()));
+	}
+
+	private Character EscSeq() {
+		parser.begin();
+		if( !parser.match('\\') )
+			return parser.failure(null);
+		if( parser.match('a') )  return parser.success('\u0007');
+		if( parser.match('b') )  return parser.success('\b');
+		if( parser.match('f') )  return parser.success('\f');
+		if( parser.match('n') )  return parser.success('\n');
+		if( parser.match('r') )  return parser.success('\r');
+		if( parser.match('t') )  return parser.success('\t');
+		if( parser.match('v') )  return parser.success('\u000b');
+		if( parser.match('\\') )  return parser.success('\\');
+		if( parser.match('"') )  return parser.success('"');
+		if( parser.match('\'') )  return parser.success('\'');
+		int start = parser.currentIndex();
+		if( parser.match('x') && HexDigit() && HexDigit() )
+			return parser.success((char)Integer.parseInt(parser.textFrom(start+1),16));
+		if( parser.match('u') && HexDigit() && HexDigit() && HexDigit() && HexDigit() )
+			return parser.success((char)Integer.parseInt(parser.textFrom(start+1),16));
+		if( Digit() ) {
+			if( Digit() ) Digit();  // optional
+			return parser.success((char)Integer.parseInt(parser.textFrom(start)));
+		}
+		if( MatchEndOfLine() ) {
+			return parser.success('\n');
+		}
+		return parser.failure(null);
+	}
+
+	private void Spaces() throws ParseException {
+		while( parser.anyOf(" \t") || Comment() || ContinueOnNextLine() );
+	}
+
+	private boolean ContinueOnNextLine() {
+		parser.begin();
+		if( parser.match('\\') && EndOfLine() ) {
+			parser.upSb();
+			return parser.success();
+		} else
+			return parser.failure();
+	}
+
+	private boolean Comment() throws ParseException {
+		if( LongComment() )
+			return true;
+		if( parser.match("--") ) {
+			while( parser.noneOf("\r\n") );
+			return true;
+		}
+		return false;
+	}
+
+	private boolean LongComment() throws ParseException {
+		parser.begin();
+		if( !parser.match("--[") )
+			return parser.failure();
+		int start = parser.currentIndex();
+		while( parser.match('=') );
+		int nEquals = parser.currentIndex() - start;
+		if( !parser.match('[') )
+			return parser.failure();
+		while( !LongBracketsEnd(nEquals) ) {
+			if( !(EndOfLine() || parser.anyChar()) )
+				throw parser.exception("Unclosed comment");
+		}
+		parser.upSb();
+		return parser.success();
+	}
+
+	private boolean LongBracketsEnd(int nEquals) {
+		parser.begin();
+		if( !parser.match(']') )
+			return parser.failure();
+		while( nEquals-- > 0 ) {
+			if( !parser.match('=') )
+				return parser.failure();
+		}
+		if( !parser.match(']') )
+			return parser.failure();
+		return parser.success();
+	}
+
+
+
+	private class ParseList extends ArrayList {
+
+		void addNewLines() {
+			if( parser.sb().length() > 0 ) {
+				add( parser.sb().toString() );
+				parser.sb().setLength(0);
+/*
+if( parser.sourceName.equals("stdin") ) {
+	StringWriter sw = new StringWriter();
+	new Throwable().printStackTrace(new PrintWriter(sw,true));
+//	add(sw.toString());
+}
+*/
+			}
+		}
+
+		ParseList() {
+			addNewLines();
+		}
+
+		@Override public boolean add(Object obj) {
+			if( obj instanceof List )  throw new RuntimeException();
+			return super.add(obj);
+		}
+
+		@Override public void add(int index,Object obj) {
+			if( obj instanceof List )  throw new RuntimeException();
+			super.add(index,obj);
+		}
+
+		@Override public String toString() {
+			StringBuilder sb = new StringBuilder();
+			for( Object o : this ) {
+				sb.append( o.toString() );
+			}
+			return sb.toString();
+		}
+	}
+
+
+	private static AtomicInteger classCounter = new AtomicInteger();
+
+	private enum Val { SINGLE, ARRAY }
+
+	private class Expr extends ParseList {
+		final Val valType;
+		final boolean isStmt;
+
+		Expr(Val valType,boolean isStmt) {
+			this.valType = valType;
+			this.isStmt = isStmt;
+		}
+
+		Expr single() {
+			if( valType==Val.SINGLE )
+				return this;
+			Expr exp = new Expr(Val.SINGLE,isStmt);
+			exp.add( valType==Val.ARRAY ? "LuanImpl.first(" : "Luan.first(" );
+			exp.addAll( this );
+			exp.add( ")" );
+			return exp;
+		}
+
+		Expr array() {
+			if( valType==Val.ARRAY )
+				return this;
+			Expr exp = new Expr(Val.ARRAY,isStmt);
+			if( valType==Val.SINGLE ) {
+				exp.add( "new Object[]{" );
+				exp.addAll( this );
+				exp.add( "}" );
+			} else {
+				exp.add( "Luan.array(" );
+				exp.addAll( this );
+				exp.add( ")" );
+			}
+			return exp;
+		}
+
+	}
+
+	private Expr expString(List<Expr> list) {
+		Expr exp = new Expr(Val.ARRAY,false);
+		switch(list.size()) {
+		case 0:
+			exp.add("LuanFunction.NOTHING");
+			return exp;
+		case 1:
+			return list.get(0);
+		default:
+			int lastI = list.size() - 1;
+			exp.add( "new Object[]{" );
+			for( int i=0; i<lastI; i++ ) {
+				exp.addAll( list.get(i).single() );
+				exp.add( "," );
+			}
+			Expr last = list.get(lastI);
+			if( last.valType==Val.SINGLE ) {
+				exp.addAll( last );
+				exp.add( "}" );
+			} else {
+				exp.add( "}" );
+				exp.add( 0, "LuanImpl.concatArgs(" );
+				exp.add( "," );
+				exp.addAll( last );
+				exp.add( ")" );
+			}
+			return exp;
+		}
+	}
+
+	private class Stmts extends ParseList {
+		boolean hasReturn = false;
+	}
+
+	private Class toFnClass(Stmts stmts,List<UpSym> upValueSymbols) {
+		String className = "EXP" + classCounter.incrementAndGet();
+		String classCode = toFnString(stmts,upValueSymbols,className);
+		try {
+//System.out.println(parser.sourceName);
+//System.out.println(classCode);
+			return LuanJavaCompiler.compile("luan.impl."+className,parser.sourceName,classCode);
+		} catch(ClassNotFoundException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
+	private String toFnString(Stmts stmts,List<UpSym> upValueSymbols) {
+		String className = "EXP" + classCounter.incrementAndGet();
+		return toFnString(stmts,upValueSymbols,className);
+	}
+
+	private String toFnString(Stmts stmts,List<UpSym> upValueSymbols,String className) {
+		if( !stmts.hasReturn )
+			stmts.add( "\nreturn LuanFunction.NOTHING;" );
+		return ""
+			+"package luan.impl;  "
+			+"import luan.Luan;  "
+			+"import luan.LuanFunction;  "
+			+"import luan.LuanState;  "
+			+"import luan.LuanJava;  "
+			+"import luan.LuanException;  "
+			+"import luan.modules.PackageLuan;  "
+
+			+"public class " + className +" extends Closure {  "
+				+"public "+className+"(LuanJava java) throws LuanException {  "
+					+"super("+upValueSymbols.size()+",java);  "
+					+ init(upValueSymbols)
+				+"}  "
+
+				+"@Override public Object doCall(LuanState luan,Object[] args) throws LuanException {  "
+					+"final Pointer[] parentUpValues = upValues;  "
+					+"Object t;  "
+					+"Object[] a;  "
+					+ stmts
+				+"\n}  "
+			+"}\n"
+		;
+	}
+
+	private Expr toFnExp(Stmts stmt,List<UpSym> upValueSymbols,String name) {
+		stmt.addNewLines();
+		if( !stmt.hasReturn )
+			stmt.add( "return LuanFunction.NOTHING;  " );
+		Expr exp = new Expr(Val.SINGLE,false);
+		exp.add( ""
+			+"new Closure("+upValueSymbols.size()+",java) {  "
+				+"{  "
+				+ init(upValueSymbols)
+				+"}  "
+				+"@Override public Object doCall(LuanState luan,Object[] args) throws LuanException {  "
+		);
+		if( name != null ) {
+			exp.add( ""
+					+"return _" + name + "(luan,args);  "
+				+"}  "
+				+"private Object _" + name + "(LuanState luan,Object[] args) throws LuanException {  "
+			);
+		}
+		exp.add( ""
+					+"final Pointer[] parentUpValues = upValues;  "
+					+"Object t;  "
+					+"Object[] a;  "
+		);
+		exp.addAll( stmt );
+		exp.add( ""
+				+"}  "
+			+"}  "
+		);
+		return exp;
+	}
+
+	private static String init(List<UpSym> upValueSymbols) {
+		StringBuilder sb = new StringBuilder();
+		for( UpSym upSym : upValueSymbols ) {
+			sb.append( upSym.init() );
+		}
+		return sb.toString();
+	}
+
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/impl/ParseException.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/impl/ParseException.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,57 @@
+package luan.impl;
+
+
+public final class ParseException extends Exception {
+//	public final LuanSource src;
+	public final String sourceName;
+	public final String text;
+	public final int iCurrent;
+	public final int iHigh;
+
+	ParseException(String msg,String text,String sourceName,int iCurrent,int iHigh) {
+		super(msg);
+//		this.src = src;
+		this.text = text;
+		this.sourceName = sourceName;
+		this.iCurrent = iCurrent;
+		this.iHigh = iHigh;
+//System.out.println("iCurrent = "+iCurrent);
+//System.out.println("iHigh = "+iHigh);
+	}
+
+	private class Location {
+		final int line;
+		final int pos;
+
+		Location(int index) {
+			int line = 0;
+			int i = -1;
+			while(true) {
+				int j = text.indexOf('\n',i+1);
+				if( j == -1 || j >= index )
+					break;
+				i = j;
+				line++;
+			}
+			this.line = line;
+			this.pos = index - i - 1;
+		}
+	}
+
+	private String[] lines() {
+		return text.split("\n",-1);
+	}
+
+	public String getFancyMessage() {
+		Location loc = new Location(iCurrent);
+		String line = lines()[loc.line];
+		String msg = getMessage() +  " (line " + (loc.line+1) + ", pos " + (loc.pos+1) + ") in " + sourceName + "\n";
+		StringBuilder sb = new StringBuilder(msg);
+		sb.append( line + "\n" );
+		for( int i=0; i<loc.pos; i++ ) {
+			sb.append( line.charAt(i)=='\t' ? '\t' : ' ' );
+		}
+		sb.append("^\n");
+		return sb.toString();
+	}
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/impl/Parser.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/impl/Parser.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,190 @@
+package luan.impl;
+
+
+final class Parser {
+
+	private static class Frame {
+		int i;
+		StringBuilder sb;
+	}
+
+//	private final LuanSource src;
+	public final String text;
+	public final String sourceName;
+	private final int len;
+	private Frame[] stack = new Frame[256];
+	private int frame = 0;
+	private int iHigh;
+
+	public Parser(String text,String sourceName) {
+//		this.src = src;
+		this.text = text;
+		this.sourceName = sourceName;
+		this.len = text.length();
+		stack[0] = new Frame();
+	}
+
+	private int i() {
+		return stack[frame].i;
+	}
+
+	private void i(int i) {
+		Frame f = stack[frame];
+		f.i += i;
+		if( iHigh < f.i )
+			iHigh = f.i;
+	}
+
+	public int begin() {
+		frame++;
+		if( frame == stack.length ) {
+			Frame[] a = new Frame[2*frame];
+			System.arraycopy(stack,0,a,0,frame);
+			stack = a;
+		}
+		Frame f = new Frame();
+		f.i = stack[frame-1].i;
+		stack[frame] = f;
+		return i();
+	}
+
+	public void rollback() {
+		Frame f = stack[frame];
+		f.i = stack[frame-1].i;
+		f.sb = null;
+	}
+
+	public <T> T success(T t) {
+		success();
+		return t;
+	}
+
+	public boolean success() {
+		Frame f = stack[frame];
+		if( f.sb != null && f.sb.length() > 0 )  throw new RuntimeException("sb not emtpy");
+		frame--;
+		stack[frame].i = f.i;
+		return true;
+	}
+
+	public <T> T failure(T t) {
+		failure();
+		return t;
+	}
+
+	public boolean failure() {
+		frame--;
+		return false;
+	}
+
+	public ParseException exception(String msg) {
+		return new ParseException(msg,text,sourceName,i(),iHigh);
+	}
+
+	public ParseException exception() {
+		return exception("Invalid input");
+	}
+
+	public StringBuilder sb() {
+		Frame f = stack[frame];
+		if( f.sb == null )
+			f.sb = new StringBuilder();
+		return f.sb;
+	}
+
+	public void upSb() {
+		Frame f = stack[frame];
+		StringBuilder sb = f.sb;
+		if( sb != null && sb.length() > 0 ) {
+			Frame fUp = stack[frame-1];
+			if( fUp.sb == null )
+				fUp.sb = sb;
+			else
+				fUp.sb.append(sb.toString());
+			f.sb = null;
+		}
+	}
+
+	public int currentIndex() {
+		return i();
+	}
+
+	public char lastChar() {
+		return text.charAt(i()-1);
+	}
+
+	public char currentChar() {
+		return text.charAt(i());
+	}
+
+	public boolean endOfInput() {
+		return i() >= len;
+	}
+
+	public boolean match(char c) {
+		if( endOfInput() || text.charAt(i()) != c )
+			return false;
+		i(1);
+		return true;
+	}
+
+	public boolean match(String s) {
+		int n = s.length();
+		if( !text.regionMatches(i(),s,0,n) )
+			return false;
+		i(n);
+		return true;
+	}
+
+	public boolean matchIgnoreCase(String s) {
+		int n = s.length();
+		if( !text.regionMatches(true,i(),s,0,n) )
+			return false;
+		i(n);
+		return true;
+	}
+
+	public boolean anyOf(String s) {
+		if( endOfInput() || s.indexOf(text.charAt(i())) == -1 )
+			return false;
+		i(1);
+		return true;
+	}
+
+	public boolean noneOf(String s) {
+		if( endOfInput() || s.indexOf(text.charAt(i())) != -1 )
+			return false;
+		i(1);
+		return true;
+	}
+
+	public boolean inCharRange(char cLow, char cHigh) {
+		if( endOfInput() )
+			return false;
+		char c = text.charAt(i());
+		if( !(cLow <= c && c <= cHigh) )
+			return false;
+		i(1);
+		return true;
+	}
+
+	public boolean anyChar() {
+		if( endOfInput() )
+			return false;
+		i(1);
+		return true;
+	}
+
+	public boolean test(char c) {
+		return !endOfInput() && text.charAt(i()) == c;
+	}
+
+	public boolean test(String s) {
+		return text.regionMatches(i(),s,0,s.length());
+	}
+
+	public String textFrom(int start) {
+		return text.substring(start,i());
+	}
+
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/impl/Pointer.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/impl/Pointer.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,23 @@
+package luan.impl;
+
+import luan.DeepCloneable;
+import luan.DeepCloner;
+
+
+public final class Pointer implements DeepCloneable {
+	public Object o;
+
+	public Pointer() {}
+
+	public Pointer(Object o) {
+		this.o = o;
+	}
+
+	@Override public Pointer shallowClone() {
+		return new Pointer();
+	}
+
+	@Override public void deepenClone(DeepCloneable clone,DeepCloner cloner) {
+		((Pointer)clone).o = cloner.get(o);
+	}
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/impl/TableField.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/impl/TableField.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,12 @@
+package luan.impl;
+
+
+public final class TableField {
+	final Object key;
+	final Object value;
+
+	public TableField(Object key,Object value) {
+		this.key = key;
+		this.value = value;
+	}
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/BasicLuan.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/BasicLuan.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,244 @@
+package luan.modules;
+
+import java.io.InputStreamReader;
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.List;
+import java.util.ArrayList;
+import luan.Luan;
+import luan.LuanState;
+import luan.LuanTable;
+import luan.LuanFunction;
+import luan.LuanException;
+import luan.LuanMethod;
+import luan.LuanMeta;
+
+
+public final class BasicLuan {
+
+	public static String type(Object obj) {
+		return Luan.type(obj);
+	}
+
+	public static LuanFunction load(String text,String sourceName,LuanTable env)
+		throws LuanException
+	{
+		Utils.checkNotNull(text);
+		Utils.checkNotNull(sourceName,1);
+		return Luan.load(text,sourceName,env);
+	}
+
+	public static LuanFunction load_file(LuanState luan,String fileName) throws LuanException {
+		if( fileName == null ) {
+			fileName = "stdin:";
+		} else if( fileName.indexOf(':') == -1 ) {
+			fileName = "file:" + fileName;
+		}
+		String src = PackageLuan.read(luan,fileName);
+		if( src == null )
+			throw new LuanException("file '"+fileName+"' not found" );
+		return load(src,fileName,null);
+	}
+
+	public static LuanFunction pairs(final LuanState luan,final LuanTable t) throws LuanException {
+		Utils.checkNotNull(t);
+		return t.pairs(luan);
+	}
+
+	public static LuanFunction ipairs(final LuanTable t) throws LuanException {
+		Utils.checkNotNull(t);
+		return new LuanFunction() {
+			List<Object> list = t.asList();
+			int i = 0;
+			final int size = list.size();
+
+			@Override public Object[] call(LuanState luan,Object[] args) {
+				if( i >= size )
+					return LuanFunction.NOTHING;
+				Object val = list.get(i++);
+				return new Object[]{i,val};
+			}
+		};
+	}
+
+	public static Object get_metatable(LuanTable table) throws LuanException {
+		Utils.checkNotNull(table);
+		LuanTable metatable = table.getMetatable();
+		if( metatable == null )
+			return null;
+		Object obj = metatable.rawGet("__metatable");
+		return obj!=null ? obj : metatable;
+	}
+
+	public static void set_metatable(LuanTable table,LuanTable metatable) throws LuanException {
+		Utils.checkNotNull(table);
+		if( table.getHandler("__metatable") != null )
+			throw new LuanException("cannot change a protected metatable");
+		table.setMetatable(metatable);
+	}
+
+	public static boolean raw_equal(Object v1,Object v2) {
+		return v1 == v2 || v1 != null && v1.equals(v2);
+	}
+
+	public static Object raw_get(LuanTable table,Object index) {
+		return table.rawGet(index);
+	}
+
+	public static void raw_set(LuanTable table,Object index,Object value) {
+		table.rawPut(index,value);
+	}
+
+	public static int raw_len(Object v) throws LuanException {
+		if( v instanceof String ) {
+			String s = (String)v;
+			return s.length();
+		}
+		if( v instanceof LuanTable ) {
+			LuanTable t = (LuanTable)v;
+			return t.rawLength();
+		}
+		throw new LuanException( "bad argument #1 to 'raw_len' (table or string expected)" );
+	}
+
+	public static String to_string(LuanState luan,Object v) throws LuanException {
+		return luan.toString(v);
+	}
+
+	public static LuanTable new_error(LuanState luan,Object msg) throws LuanException {
+		String s = luan.toString(msg);
+		LuanTable tbl = new LuanException(s).table();
+		tbl.rawPut( "message", msg );
+		return tbl;
+	}
+
+	public static String assert_string(String v) throws LuanException {
+		Utils.checkNotNull(v);
+		return v;
+	}
+
+	public static Number assert_number(Number v) throws LuanException {
+		Utils.checkNotNull(v);
+		return v;
+	}
+
+	public static LuanTable assert_table(LuanTable v) throws LuanException {
+		Utils.checkNotNull(v);
+		return v;
+	}
+
+	public static boolean assert_boolean(boolean v) throws LuanException {
+		return v;
+	}
+
+	public static int assert_integer(int v) throws LuanException {
+		return v;
+	}
+
+	public static long assert_long(long v) throws LuanException {
+		return v;
+	}
+
+	public static double assert_double(double v) throws LuanException {
+		return v;
+	}
+
+	@LuanMethod public static byte[] assert_binary(byte[] v) throws LuanException {
+		Utils.checkNotNull(v);
+		return v;
+	}
+
+	public static LuanFunction assert_function(LuanFunction v) throws LuanException {
+		Utils.checkNotNull(v);
+		return v;
+	}
+
+	public static LuanFunction range(final double from,final double to,Double stepV) throws LuanException {
+		final double step = stepV==null ? 1.0 : stepV;
+		if( step == 0.0 )
+			throw new LuanException("bad argument #3 (step may not be zero)");
+		return new LuanFunction() {
+			double v = from;
+
+			@Override public Object call(LuanState luan,Object[] args) {
+				if( step > 0.0 && v > to || step < 0.0 && v < to )
+					return LuanFunction.NOTHING;
+				double rtn = v;
+				v += step;
+				return rtn;
+			}
+		};
+	}
+
+	public static LuanFunction values(final Object... args) throws LuanException {
+		return new LuanFunction() {
+			int i = 0;
+
+			@Override public Object call(LuanState luan,Object[] unused) {
+				if( i >= args.length )
+					return LuanFunction.NOTHING;
+				return args[i++];
+			}
+		};
+	}
+
+	private LuanFunction fn(Object obj) {
+		return obj instanceof LuanFunction ? (LuanFunction)obj : null;
+	}
+
+	public static Object try_(LuanState luan,LuanTable blocks,Object... args) throws LuanException {
+		Utils.checkNotNull(blocks);
+		Object obj = blocks.get(luan,1);
+		if( obj == null )
+			throw new LuanException("missing 'try' value");
+		if( !(obj instanceof LuanFunction) )
+			throw new LuanException("bad 'try' value (function expected, got "+Luan.type(obj)+")");
+		LuanFunction tryFn = (LuanFunction)obj;
+		LuanFunction catchFn = null;
+		obj = blocks.get(luan,"catch");
+		if( obj != null ) {
+			if( !(obj instanceof LuanFunction) )
+				throw new LuanException("bad 'catch' value (function expected, got "+Luan.type(obj)+")");
+			catchFn = (LuanFunction)obj;
+		}
+		LuanFunction finallyFn = null;
+		obj = blocks.get(luan,"finally");
+		if( obj != null ) {
+			if( !(obj instanceof LuanFunction) )
+				throw new LuanException("bad 'finally' value (function expected, got "+Luan.type(obj)+")");
+			finallyFn = (LuanFunction)obj;
+		}
+		try {
+			return tryFn.call(luan,args);
+		} catch(LuanException e) {
+			if( catchFn == null )
+				throw e;
+			return catchFn.call(luan,new Object[]{e.table()});
+		} finally {
+			if( finallyFn != null )
+				finallyFn.call(luan);
+		}
+	}
+
+	@LuanMethod public static Object[] pcall(LuanState luan,LuanFunction f,Object... args) {
+		try {
+			Object[] r = Luan.array(f.call(luan,args));
+			Object[] rtn = new Object[r.length+1];
+			rtn[0] = true;
+			for( int i=0; i<r.length; i++ ) {
+				rtn[i+1] = r[i];
+			}
+			return rtn;
+		} catch(LuanException e) {
+			return new Object[]{false,e.table()};
+		}
+	}
+
+	public static String number_type(Number v) throws LuanException {
+		Utils.checkNotNull(v);
+		return v.getClass().getSimpleName().toLowerCase();
+	}
+
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/Binary.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/Binary.luan	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,11 @@
+java()
+local BinaryLuan = require "java:luan.modules.BinaryLuan"
+
+
+local M = {}
+
+M.binary = BinaryLuan.binary
+M.byte = BinaryLuan.byte_
+M.to_string = BinaryLuan.to_string
+
+return M
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/BinaryLuan.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/BinaryLuan.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,50 @@
+package luan.modules;
+
+import luan.Luan;
+import luan.LuanState;
+import luan.LuanTable;
+import luan.LuanFunction;
+import luan.LuanException;
+import luan.LuanMethod;
+
+
+public final class BinaryLuan {
+
+	static int start(byte[] binary,int i) {
+		int len = binary.length;
+		return i==0 ? 0 : i > 0 ? Math.min(i-1,len) : Math.max(len+i,0);
+	}
+
+	static int start(byte[] binary,Integer i,int dflt) {
+		return i==null ? dflt : start(binary,i);
+	}
+
+	static int end(byte[] binary,int i) {
+		int len = binary.length;
+		return i==0 ? 0 : i > 0 ? Math.min(i,len) : Math.max(len+i+1,0);
+	}
+
+	static int end(byte[] binary,Integer i,int dflt) {
+		return i==null ? dflt : end(binary,i);
+	}
+
+	@LuanMethod public static Byte[] byte_(byte[] binary,Integer i,Integer j) throws LuanException {
+		Utils.checkNotNull(binary);
+		int start = start(binary,i,1);
+		int end = end(binary,j,start+1);
+		Byte[] bytes = new Byte[end-start];
+		for( int k=0; k<bytes.length; k++ ) {
+			bytes[k] = binary[start+k];
+		}
+		return bytes;
+	}
+
+	@LuanMethod public static byte[] binary(byte... bytes) {
+		return bytes;
+	}
+
+	public static String to_string(byte[] binary) {
+		return new String(binary);
+	}
+
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/Html.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/Html.luan	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,76 @@
+java()
+local HtmlLuan = require "java:luan.modules.HtmlLuan"
+local HtmlParser = require "java:luan.modules.parsers.Html"
+local URLEncoder = require "java:java.net.URLEncoder"
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local ipairs = Luan.ipairs or error()
+local pairs = Luan.pairs or error()
+local type = Luan.type or error()
+local Io = require "luan:Io.luan"
+local output_of = Io.output_of or error()
+
+
+local M = {}
+
+M.encode = HtmlLuan.encode
+
+local quote = HtmlLuan.quote
+M.quote = quote
+
+function M.parse(text,container_tags)
+	text or error "text required"
+	container_tags = container_tags or {"script","style","textarea"}
+	return HtmlParser.toList(text,container_tags)
+end
+
+function M.url_encode(s)
+	return URLEncoder.encode(s,"UTF-8")
+end
+
+local function output_tag(tag)
+	%><<%= tag.name %><%
+	local attributes = tag.attributes
+	for name, value in pairs(attributes) do
+		%> <%= name %><%
+		if value ~= true then
+			%>=<%= quote(value) %><%
+		end
+	end
+	if tag.is_empty then
+		%>/<%
+	end
+	%>><%
+end
+
+function M.to_string(list)
+	return output_of( function()
+		for _, obj in ipairs(list) do
+			local tp = type(obj)
+			if tp == "string" then
+				%><%= obj %><%
+			elseif tp == "table" then
+				tp = obj.type
+				if tp == nil then
+					error "no type in element of table for 'Html.to_string'"
+				elseif tp == "comment" then
+					%><!--<%= obj.text %>--><%
+				elseif tp == "cdata" then
+					%><![CDATA[<%= obj.text %>]]><%
+				elseif tp == "tag" then
+					output_tag(obj)
+				elseif tp == "container" then
+					local tag = obj.tag
+					output_tag(tag)
+					%><%= obj.text %></<%= tag.name %>><%
+				else
+					error "invalid element type for 'Html.to_string'"
+				end
+			else
+				error("invalid value ("..tp..") in list for 'Html.to_string'")
+			end
+		end
+	end )
+end
+
+return M
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/HtmlLuan.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/HtmlLuan.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,348 @@
+package luan.modules;
+
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Set;
+import java.util.HashSet;
+import java.util.Map;
+import luan.Luan;
+import luan.LuanState;
+import luan.LuanTable;
+import luan.LuanException;
+
+
+public final class HtmlLuan {
+
+	public static String encode(String s) throws LuanException {
+		Utils.checkNotNull(s);
+		char[] a = s.toCharArray();
+		StringBuilder buf = new StringBuilder();
+		for( int i=0; i<a.length; i++ ) {
+			char c = a[i];
+			switch(c) {
+			case '&':
+				buf.append("&amp;");
+				break;
+			case '<':
+				buf.append("&lt;");
+				break;
+			case '>':
+				buf.append("&gt;");
+				break;
+			case '"':
+				buf.append("&quot;");
+				break;
+			default:
+				buf.append(c);
+			}
+		}
+		return buf.toString();
+	}
+
+/*
+//	public static final String TEXTAREA = "textarea";
+	public static final String SCRIPT = "script";
+	public static final String STYLE = "style";
+
+	public static Set<String> containerTags = new HashSet<String>(Arrays.asList(SCRIPT,STYLE));
+*/
+/*
+	public static LuanTable parse(LuanState luan,String text,LuanTable containerTagsTbl)
+		throws LuanException
+	{
+		Utils.checkNotNull(luan,text);
+		Utils.checkNotNull(luan,containerTagsTbl);
+		Set<String> containerTags = new HashSet<String>();
+		for( Object v : containerTagsTbl.asList() ) {
+			containerTags.add((String)v);
+		}
+		List<Object> html = new ArrayList<Object>();
+		int len = text.length();
+		int i = 0;
+outer:
+		while( i < len ) {
+			int i2 = text.indexOf('<',i);
+			while( i2 != -1 && i2+1 < len ) {
+				char c = text.charAt(i2+1);
+				if( Character.isLetter(c) || c=='/' || c=='!' )
+					break;
+				i2 = text.indexOf('<',i2+1);
+			}
+			if( i2 == -1 ) {
+				html.add( text.substring(i) );
+				break;
+			}
+			if( i < i2 )
+				html.add( text.substring(i,i2) );
+			if( text.startsWith("<!--",i2) ) {
+				i = text.indexOf("-->",i2+4);
+				if( i == -1 ) {
+					html.add( text.substring(i2) );
+					break;
+				}
+				html.add( comment( text.substring(i2+4,i) ) );
+				i += 3;
+			} else if( text.startsWith("<![CDATA[",i2) ) {
+				i = text.indexOf("]]>",i2+9);
+				if( i == -1 ) {
+					html.add( text.substring(i2) );
+					break;
+				}
+				html.add( cdata( text.substring(i2+9,i) ) );
+				i += 3;
+			} else {
+				i = text.indexOf('>',i2);
+				if( i == -1 ) {
+					html.add( text.substring(i2) );
+					break;
+				}
+				String tagText = text.substring(i2+1,i);
+				try {
+					LuanTable tag = parseTag(tagText);
+					String tagName = (String)tag.rawGet("name");
+					if( containerTags.contains(tagName) ) {
+						i2 = i;
+						String endTagName = '/' + tagName;
+						while(true) {
+							i2 = text.indexOf('<',i2+1);
+							if( i2 == -1 )
+								break;
+							int i3 = text.indexOf('>',i2);
+							if( i3 == -1 )
+								break;
+							int j = i2+1;
+							while( j<i3 && !Character.isWhitespace(text.charAt(j)) )  j++;
+							String s = text.substring(i2+1,j);
+							if( s.equalsIgnoreCase(endTagName) ) {
+								String text2 = text.substring(i+1,i2);
+								LuanTable textContainer = textContainer(tag,text2);
+								html.add( textContainer );
+								i = i3 + 1;
+								continue outer;
+							}
+						}
+//						logger.warn("unclosed "+tagName);
+					}
+					i += 1;
+					html.add( tag );
+				} catch(BadTag e) {
+//					logger.debug("bad tag",e);
+					i += 1;
+//					if( !removeBadTags ) {
+						html.add( "&lt;" );
+						html.add( encode(luan,tagText) );
+						html.add( "&gt;" );
+//					}
+				}
+			}
+		}
+		return new LuanTable(html);
+	}
+
+	static LuanTable comment(String text) {
+		LuanTable tbl = new LuanTable();
+		tbl.rawPut("type","comment");
+		tbl.rawPut("text",text);
+		return tbl;
+	}
+
+	static LuanTable cdata(String text) {
+		LuanTable tbl = new LuanTable();
+		tbl.rawPut("type","cdata");
+		tbl.rawPut("text",text);
+		return tbl;
+	}
+
+	static LuanTable textContainer(LuanTable tag,String text) {
+		LuanTable tbl = new LuanTable();
+		tbl.rawPut("type","container");
+		tbl.rawPut("tag",tag);
+		tbl.rawPut("text",text);
+		return tbl;
+	}
+
+
+
+	static final class BadTag extends RuntimeException {
+		private BadTag(String msg) {
+			super(msg);
+		}
+	}
+
+	static LuanTable parseTag(String text) {
+		LuanTable tbl = new LuanTable();
+		tbl.rawPut("type","tag");
+		if( text.endsWith("/") ) {
+			text = text.substring(0,text.length()-1);
+			tbl.rawPut("is_empty",true);
+		} else {
+			tbl.rawPut("is_empty",false);
+		}
+		int len = text.length();
+		int i = 0;
+		int i2 = i;
+		if( i2<len && text.charAt(i2)=='/' )
+			i2++;
+		while( i2<len ) {
+			char c = text.charAt(i2);
+			if( Character.isWhitespace(c) )
+				break;
+			if( !( Character.isLetterOrDigit(c) || c=='_' || c=='.' || c=='-' || c==':' ) )
+				throw new BadTag("invalid tag name for <"+text+">");
+			i2++;
+		}
+		String name = text.substring(i,i2).toLowerCase();
+		tbl.rawPut("name",name);
+		LuanTable attributes = new LuanTable();
+		tbl.rawPut("attributes",attributes);
+		i = i2;
+		while( i<len && Character.isWhitespace(text.charAt(i)) )  i++;
+		while( i<len ) {
+			i2 = toEndName(text,i,len);
+			String attrName = unquote(text.substring(i,i2).toLowerCase());
+			if( attributes.rawGet(attrName) != null )
+				throw new BadTag("duplicate attribute: "+attrName);
+			i = i2;
+			while( i<len && Character.isWhitespace(text.charAt(i)) )  i++;
+			if( i<len && text.charAt(i) == '=' ) {
+				i++;
+				i2 = i;
+				while( i<len && Character.isWhitespace(text.charAt(i)) )  i++;
+				i2 = toEndValue(text,i,len);
+				String attrValue = text.substring(i,i2);
+				if( attrValue.indexOf('<') != -1 || attrValue.indexOf('>') != -1 )
+					throw new BadTag("invalid attribute value: "+attrValue);
+				attrValue = unquote(attrValue);
+				attributes.rawPut(attrName,attrValue);
+				i = i2;
+				while( i<len && Character.isWhitespace(text.charAt(i)) )  i++;
+			} else {
+				attributes.rawPut(attrName,true);
+			}
+		}
+		return tbl;
+	}
+
+	private static int toEndName(String text,int i,int len) {
+		if( i==len )
+			return i;
+		char c = text.charAt(i);
+		switch(c) {
+		case '"':
+		case '\'':
+			i = text.indexOf(c,i+1);
+			return i==-1 ? len : i+1;
+		default:
+			if( Character.isWhitespace(c) ) {
+				throw new RuntimeException("text="+text+" i="+i);
+			}
+			do {
+				i++;
+			} while( i<len && (c=text.charAt(i))!='=' && !Character.isWhitespace(c) );
+			return i;
+		}
+	}
+
+	private static int toEndValue(String text,int i,int len) {
+		if( i==len )
+			return i;
+		char c = text.charAt(i);
+		switch(c) {
+		case '"':
+		case '\'':
+			i = text.indexOf(c,i+1);
+			return i==-1 ? len : i+1;
+		default:
+			if( Character.isWhitespace(c) ) {
+				throw new RuntimeException("text="+text+" i="+i);
+			}
+			do {
+				i++;
+			} while( i<len && !Character.isWhitespace(text.charAt(i)) );
+			return i;
+		}
+	}
+
+	public static String unquote(String s) {
+		if( s==null || s.length()<=1 )
+			return s;
+		char c = s.charAt(0);
+		return (c=='"' || c=='\'') && s.charAt(s.length()-1)==c
+			? s.substring(1,s.length()-1) : s;
+	}
+*/
+
+
+/*
+	public static String to_string(LuanState luan,LuanTable tbl) throws LuanException {
+		List<Object> html = tbl.asList();
+		StringBuilder buf = new StringBuilder();
+		for( Object o : html ) {
+			if( o instanceof String ) {
+				buf.append( o );
+			} else if( o instanceof LuanTable ) {
+				LuanTable t = (LuanTable)o;
+				String type = (String)t.get(luan,"type");
+				if( type==null )
+					throw new LuanException(luan, "no type in element of table for 'Html.to_string'" );
+				if( type.equals("comment") ) {
+					buf.append( "<!--" ).append( t.get(luan,"text") ).append( "-->" );
+				} else if( type.equals("cdata") ) {
+					buf.append( "<![CDATA[" ).append( t.get(luan,"text") ).append( "]]" );
+				} else if( type.equals("tag") ) {
+					buf.append( tagToString(luan,t) );
+				} else if( type.equals("container") ) {
+					LuanTable tag  = (LuanTable)t.get(luan,"tag");
+					buf.append( tagToString(luan,tag) );
+					buf.append( t.get(luan,"text") );
+					buf.append( "</" ).append( tag.get(luan,"name") ).append( ">" );
+				} else {
+					throw new LuanException(luan, "invalid element type for 'Html.to_string'" );
+				}
+			} else 
+				throw new LuanException(luan, "invalid value ("+Luan.type(o)+") in table for 'Html.to_string'" );
+		}
+		return buf.toString();
+	}
+
+	private static String tagToString(LuanState luan,LuanTable tbl) throws LuanException {
+		StringBuilder buf = new StringBuilder();
+		buf.append('<');
+		buf.append(tbl.get(luan,"name"));
+		LuanTable attributes = (LuanTable)tbl.get(luan,"attributes");
+		for( Map.Entry<Object,Object> attr : attributes.iterable(luan) ) {
+			buf.append( ' ' );
+			buf.append( attr.getKey() );
+			Object val = attr.getValue();
+			if( !val.equals(Boolean.TRUE) ) {
+				buf.append( '=' );
+				buf.append( quote((String)val) );
+			}
+		}
+		if( tbl.get(luan,"is_empty").equals(Boolean.TRUE) )
+			buf.append('/');
+		buf.append('>');
+		return buf.toString();
+	}
+*/
+	public static String quote(String s) {
+		StringBuilder buf = new StringBuilder();
+		buf.append('"');
+		int i = 0;
+		while(true) {
+			int i2 = s.indexOf('"',i);
+			if( i2 == -1 ) {
+				buf.append(s.substring(i));
+				break;
+			} else {
+				buf.append(s.substring(i,i2));
+				buf.append("&quot;");
+				i = i2 + 1;
+			}
+		}
+		buf.append('"');
+		return buf.toString();
+	}
+
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/Io.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/Io.luan	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,185 @@
+java()
+local IoLuan = require "java:luan.modules.IoLuan"
+local System = require "java:java.lang.System"
+
+local M = {}
+
+M.ip = IoLuan.ip
+M.my_ips = IoLuan.my_ips
+M.read_console_line = IoLuan.read_console_line
+M.schemes = IoLuan.newSchemes()
+M.uri = IoLuan.uri
+M.stdin = IoLuan.defaultStdin.table()
+M.socket_server = IoLuan.socket_server
+M.stdout = IoLuan.textWriter(System.out)
+M.stderr = IoLuan.textWriter(System.err)
+
+-- used by http and rpc
+M.password = "password"
+
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local to_string = Luan.to_string or error()
+local type = Luan.type or error()
+local try = Luan.try or error()
+local ipairs = Luan.ipairs or error()
+local pairs = Luan.pairs or error()
+local values = Luan.values or error()
+local load = Luan.load or error()
+local Table = require "luan:Table.luan"
+local unpack = Table.unpack or error()
+local String = require "luan:String.luan"
+local encode = String.encode or error()
+local matches = String.matches or error()
+
+
+-- do not change
+function M.template_write(...)
+	return M.stdout.write(...)
+end
+
+
+function M.print_to(out,...)
+	local list = {}
+	for v in values(...) do
+		list[#list+1] = to_string(v)
+		list[#list+1] = '\t'
+	end
+	if #list > 0 then
+		list[#list] = '\n'
+		out.write( unpack(list) )
+	end
+end
+
+function M.print(...)
+	M.print_to(M.stdout,...)
+end
+
+
+function M.output_to(out,fn,...)
+	local old_out = M.stdout
+	return try( {
+		function(...)
+			M.stdout = out
+			fn(...)
+		end;
+		finally = function()
+			M.stdout = old_out
+		end;
+	}, ... )
+end
+
+local uri = M.uri  -- make local
+
+function M.output_of(fn,...)
+	local string_uri = uri "string:"
+	local out = string_uri.text_writer()
+	M.output_to(out,fn,...)
+	out.close()
+	return string_uri.read_text()
+end
+
+
+-- repr
+
+local function do_repr(out,obj,strict,done)
+	local tp = type(obj)
+	if tp == "table" then
+		if done[obj] == true then
+			strict and error "circular reference"
+			out.write "<circular reference>"
+			return
+		end
+		done[obj] = true
+		out.write( "{" )
+		local is_first = true
+		local in_list = {}
+		for key, value in ipairs(obj) do
+			if is_first then is_first = false else out.write ", " end
+			do_repr(out,value,strict,done)
+			in_list[key] = true
+		end
+		for key, value in pairs(obj) do
+			if in_list[key] ~= true then
+				if is_first then is_first = false else out.write ", " end
+				if type(key) == "string" and matches(key,"^[a-zA-Z_][a-zA-Z_0-9]*$") ~= nil then
+					out.write( key )
+				elseif type(key) == "table" then
+					out.write( "[<", key, ">]" )
+				else
+					out.write "["
+					do_repr(out,key,strict,done)
+					out.write "]"
+				end
+				out.write "="
+				do_repr(out,value,strict,done)
+			end
+		end
+		out.write "}"
+	elseif tp == "string" then
+		out.write( '"', encode(obj), '"' )
+	elseif tp == "nil" or tp == "boolean" or tp == "number" then
+		out.write( obj )
+	else
+		strict and error("can't repr type '"..tp.."' of "..obj)
+		out.write( "<", obj, ">" )
+	end
+end
+
+function M.repr(obj,strict)
+	local string_uri = uri "string:"
+	local out = string_uri.text_writer()
+	do_repr(out,obj,strict or false,{})
+	out.close()
+	return string_uri.read_text()
+end
+
+
+-- useful for SimplyHTML responsiveness
+
+local NO = {}
+M.NO = NO
+
+function M.dont_write_when_no(write_fn)
+	return function(...)
+		for v in values(...) do
+			if v == NO then
+				return
+			end
+		end
+		write_fn(...)
+	end
+end
+
+
+-- debug
+
+function M.debug(prompt)
+	prompt = prompt or "luan_debug> "
+	local function console()
+		return M.read_console_line(prompt)
+	end
+	local env = {}
+	for line in console do
+		try {
+			function()
+				local fn
+				try {
+					function()
+						fn = load("return "..line,"stdin",env)
+					end
+					catch = function(e)
+						fn = load(line,"stdin",env)
+					end
+				}
+				M.print( fn() )
+			end
+			catch = function(e)
+				M.print(e)
+			end
+		}
+	end
+end
+
+
+return M
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/IoLuan.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/IoLuan.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,904 @@
+package luan.modules;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.io.Reader;
+import java.io.Writer;
+import java.io.StringReader;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.StringWriter;
+import java.io.IOException;
+import java.io.FileNotFoundException;
+import java.net.URL;
+import java.net.Socket;
+import java.net.ServerSocket;
+import java.net.InetAddress;
+import java.net.Inet4Address;
+import java.net.NetworkInterface;
+import java.net.MalformedURLException;
+import java.net.UnknownHostException;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Map;
+import luan.Luan;
+import luan.LuanState;
+import luan.LuanTable;
+import luan.LuanFunction;
+import luan.LuanJavaFunction;
+import luan.LuanException;
+import luan.modules.url.LuanUrl;
+
+
+public final class IoLuan {
+
+	private static void add(LuanTable t,String method,Class... parameterTypes) throws NoSuchMethodException {
+		t.rawPut( method, new LuanJavaFunction(IoLuan.class.getMethod(method,parameterTypes),null) );
+	}
+
+	public static String read_console_line(String prompt) throws IOException {
+		if( prompt==null )
+			prompt = "> ";
+		return System.console().readLine(prompt);
+	}
+
+
+	public interface LuanWriter {
+		public void write(LuanState luan,Object... args) throws LuanException, IOException;
+		public void close() throws IOException;
+	}
+
+	public static LuanTable textWriter(final PrintStream out) {
+		LuanWriter luanWriter = new LuanWriter() {
+
+			public void write(LuanState luan,Object... args) throws LuanException {
+				for( Object obj : args ) {
+					out.print( luan.toString(obj) );
+				}
+			}
+
+			public void close() {
+				out.close();
+			}
+		};
+		return writer(luanWriter);
+	}
+
+	public static LuanTable textWriter(final Writer out) {
+		LuanWriter luanWriter = new LuanWriter() {
+
+			public void write(LuanState luan,Object... args) throws LuanException, IOException {
+				for( Object obj : args ) {
+					out.write( luan.toString(obj) );
+				}
+			}
+
+			public void close() throws IOException {
+				out.close();
+			}
+		};
+		return writer(luanWriter);
+	}
+
+	private static LuanTable writer(LuanWriter luanWriter) {
+		LuanTable writer = new LuanTable();
+		try {
+			writer.rawPut( "write", new LuanJavaFunction(
+				LuanWriter.class.getMethod( "write", LuanState.class, new Object[0].getClass() ), luanWriter
+			) );
+			writer.rawPut( "close", new LuanJavaFunction(
+				LuanWriter.class.getMethod( "close" ), luanWriter
+			) );
+		} catch(NoSuchMethodException e) {
+			throw new RuntimeException(e);
+		}
+		return writer;
+	}
+
+
+	public static LuanTable binaryWriter(final OutputStream out) {
+		LuanTable writer = new LuanTable();
+		try {
+			writer.rawPut( "write", new LuanJavaFunction(
+				OutputStream.class.getMethod( "write", new byte[0].getClass() ), out
+			) );
+			writer.rawPut( "close", new LuanJavaFunction(
+				OutputStream.class.getMethod( "close" ), out
+			) );
+		} catch(NoSuchMethodException e) {
+			throw new RuntimeException(e);
+		}
+		return writer;
+	}
+
+	static LuanFunction lines(final BufferedReader in) {
+		return new LuanFunction() {
+			@Override public Object call(LuanState luan,Object[] args) throws LuanException {
+				try {
+					if( args.length > 0 ) {
+						if( args.length > 1 || !"close".equals(args[0]) )
+							throw new LuanException( "the only argument allowed is 'close'" );
+						in.close();
+						return null;
+					}
+					String rtn = in.readLine();
+					if( rtn==null )
+						in.close();
+					return rtn;
+				} catch(IOException e) {
+					throw new LuanException(e);
+				}
+			}
+		};
+	}
+
+	static LuanFunction blocks(final InputStream in,final int blockSize) {
+		return new LuanFunction() {
+			final byte[] a = new byte[blockSize];
+
+			@Override public Object call(LuanState luan,Object[] args) throws LuanException {
+				try {
+					if( args.length > 0 ) {
+						if( args.length > 1 || !"close".equals(args[0]) )
+							throw new LuanException( "the only argument allowed is 'close'" );
+						in.close();
+						return null;
+					}
+					if( in.read(a) == -1 ) {
+						in.close();
+						return null;
+					}
+					return a;
+				} catch(IOException e) {
+					throw new LuanException(e);
+				}
+			}
+		};
+	}
+
+
+	private static File objToFile(Object obj) {
+		if( obj instanceof String ) {
+			return new File((String)obj);
+		}
+		if( obj instanceof LuanTable ) {
+			LuanTable t = (LuanTable)obj;
+			Object java = t.rawGet("java");
+			if( java instanceof LuanFile ) {
+				LuanFile luanFile = (LuanFile)java;
+				return luanFile.file;
+			}
+		}
+		return null;
+	}
+
+
+	public static abstract class LuanIn {
+		public abstract InputStream inputStream() throws IOException, LuanException;
+		public abstract String to_string();
+		public abstract String to_uri_string();
+
+		public Reader reader() throws IOException, LuanException {
+			return new InputStreamReader(inputStream());
+		}
+
+		public String read_text() throws IOException, LuanException {
+			Reader in = reader();
+			String s = Utils.readAll(in);
+			in.close();
+			return s;
+		}
+
+		public byte[] read_binary() throws IOException, LuanException {
+			InputStream in = inputStream();
+			byte[] a = Utils.readAll(in);
+			in.close();
+			return a;
+		}
+
+		public LuanFunction read_lines() throws IOException, LuanException {
+			return lines(new BufferedReader(reader()));
+		}
+
+		public LuanFunction read_blocks(Integer blockSize) throws IOException, LuanException {
+			int n = blockSize!=null ? blockSize : Utils.bufSize;
+			return blocks(inputStream(),n);
+		}
+
+		public boolean exists() throws IOException, LuanException {
+			try {
+				inputStream().close();
+				return true;
+			} catch(FileNotFoundException e) {
+				return false;
+			}
+		}
+
+		public LuanTable table() {
+			LuanTable tbl = new LuanTable();
+			try {
+				tbl.rawPut( "java", this );
+				tbl.rawPut( "to_string", new LuanJavaFunction(
+					LuanIn.class.getMethod( "to_string" ), this
+				) );
+				tbl.rawPut( "to_uri_string", new LuanJavaFunction(
+					LuanIn.class.getMethod( "to_uri_string" ), this
+				) );
+				tbl.rawPut( "read_text", new LuanJavaFunction(
+					LuanIn.class.getMethod( "read_text" ), this
+				) );
+				tbl.rawPut( "read_binary", new LuanJavaFunction(
+					LuanIn.class.getMethod( "read_binary" ), this
+				) );
+				tbl.rawPut( "read_lines", new LuanJavaFunction(
+					LuanIn.class.getMethod( "read_lines" ), this
+				) );
+				tbl.rawPut( "read_blocks", new LuanJavaFunction(
+					LuanIn.class.getMethod( "read_blocks", Integer.class ), this
+				) );
+				tbl.rawPut( "exists", new LuanJavaFunction(
+					LuanIn.class.getMethod( "exists" ), this
+				) );
+			} catch(NoSuchMethodException e) {
+				throw new RuntimeException(e);
+			}
+			return tbl;
+		}
+	}
+
+	public static final LuanIn defaultStdin = new LuanIn() {
+
+		@Override public InputStream inputStream() {
+			return System.in;
+		}
+
+		@Override public String to_string() {
+			return "<stdin>";
+		}
+
+		@Override public String to_uri_string() {
+			return "stdin:";
+		}
+
+		@Override public String read_text() throws IOException {
+			return Utils.readAll(new InputStreamReader(System.in));
+		}
+
+		@Override public byte[] read_binary() throws IOException {
+			return Utils.readAll(System.in);
+		}
+
+		@Override public boolean exists() {
+			return true;
+		}
+	};
+
+	public static abstract class LuanIO extends LuanIn {
+		abstract OutputStream outputStream() throws IOException;
+
+		public void write(Object obj) throws LuanException, IOException {
+			if( obj instanceof String ) {
+				String s = (String)obj;
+				Writer out = new OutputStreamWriter(outputStream());
+				out.write(s);
+				out.close();
+				return;
+			}
+			if( obj instanceof byte[] ) {
+				byte[] a = (byte[])obj;
+				OutputStream out = outputStream();
+				Utils.copyAll(new ByteArrayInputStream(a),out);
+				out.close();
+				return;
+			}
+			if( obj instanceof LuanTable ) {
+				LuanTable t = (LuanTable)obj;
+				Object java = t.rawGet("java");
+				if( java instanceof LuanIn ) {
+					LuanIn luanIn = (LuanIn)java;
+					InputStream in = luanIn.inputStream();
+					OutputStream out = outputStream();
+					Utils.copyAll(in,out);
+					out.close();
+					in.close();
+					return;
+				}
+			}
+			throw new LuanException( "bad argument #1 to 'write' (string or binary or Io.uri expected)" );
+		}
+
+		public LuanTable text_writer() throws IOException {
+			return textWriter(new BufferedWriter(new OutputStreamWriter(outputStream())));
+		}
+
+		public LuanTable binary_writer() throws IOException {
+			return binaryWriter(new BufferedOutputStream(outputStream()));
+		}
+
+		@Override public LuanTable table() {
+			LuanTable tbl = super.table();
+			try {
+				tbl.rawPut( "write", new LuanJavaFunction(
+					LuanIO.class.getMethod( "write", Object.class ), this
+				) );
+				tbl.rawPut( "text_writer", new LuanJavaFunction(
+					LuanIO.class.getMethod( "text_writer" ), this
+				) );
+				tbl.rawPut( "binary_writer", new LuanJavaFunction(
+					LuanIO.class.getMethod( "binary_writer" ), this
+				) );
+			} catch(NoSuchMethodException e) {
+				throw new RuntimeException(e);
+			}
+			return tbl;
+		}
+	}
+
+	private static final LuanIO nullIO = new LuanIO() {
+		private final InputStream in = new InputStream() {
+			@Override public int read() {
+				return -1;
+			}
+		};
+		private final OutputStream out = new OutputStream() {
+			@Override public void write(int b) {}
+		};
+
+		@Override public InputStream inputStream() {
+			return in;
+		}
+
+		@Override OutputStream outputStream() {
+			return out;
+		}
+
+		@Override public String to_string() {
+			return "<null>";
+		}
+
+		@Override public String to_uri_string() {
+			return "null:";
+		}
+
+	};
+
+	public static final class LuanString extends LuanIO {
+		private String s;
+
+		private LuanString(String s) {
+			this.s = s;
+		}
+
+		@Override public InputStream inputStream() {
+			throw new UnsupportedOperationException();
+		}
+
+		@Override OutputStream outputStream() {
+			throw new UnsupportedOperationException();
+		}
+
+		@Override public String to_string() {
+			return "<string>";
+		}
+
+		@Override public String to_uri_string() {
+			return "string:" + s;
+		}
+
+		@Override public Reader reader() {
+			return new StringReader(s);
+		}
+
+		@Override public String read_text() {
+			return s;
+		}
+
+		@Override public boolean exists() {
+			return true;
+		}
+
+		@Override public LuanTable text_writer() throws IOException {
+			LuanWriter luanWriter = new LuanWriter() {
+				private final Writer out = new StringWriter();
+	
+				public void write(LuanState luan,Object... args) throws LuanException, IOException {
+					for( Object obj : args ) {
+						out.write( luan.toString(obj) );
+					}
+				}
+	
+				public void close() throws IOException {
+					s = out.toString();
+				}
+			};
+			return writer(luanWriter);
+		}
+	}
+
+	public static final class LuanFile extends LuanIO {
+		public final File file;
+
+		private LuanFile(LuanState luan,File file) throws LuanException {
+			this(file);
+			check(luan,"file:"+file.toString());
+		}
+
+		private LuanFile(File file) {
+			this.file = file;
+		}
+
+		@Override public InputStream inputStream() throws IOException {
+			return new FileInputStream(file);
+		}
+
+		@Override OutputStream outputStream() throws IOException {
+			return new FileOutputStream(file);
+		}
+
+		@Override public String to_string() {
+			return file.toString();
+		}
+
+		@Override public String to_uri_string() {
+			return "file:" + file.toString();
+		}
+
+		public LuanTable child(LuanState luan,String name) throws LuanException {
+			return new LuanFile(luan,new File(file,name)).table();
+		}
+
+		public LuanTable children(LuanState luan) throws LuanException {
+			File[] files = file.listFiles();
+			if( files==null )
+				return null;
+			LuanTable list = new LuanTable();
+			for( File f : files ) {
+				list.rawPut(list.rawLength()+1,new LuanFile(luan,f).table());
+			}
+			return list;
+		}
+
+		public LuanTable parent(LuanState luan) throws LuanException, IOException {
+			File parent = file.getParentFile();
+			if( parent==null )
+				parent = file.getCanonicalFile().getParentFile();
+			return new LuanFile(luan,parent).table();
+		}
+
+		@Override public boolean exists() {
+			return file.exists();
+		}
+
+		public void rename_to(Object destObj) throws LuanException {
+			File dest = objToFile(destObj);
+			if( dest==null )
+				throw new LuanException( "bad argument #1 to 'objToFile' (string or file table expected)" );
+			if( !file.renameTo(dest) )
+				throw new LuanException("couldn't rename file "+file+" to "+dest);
+		}
+
+		public LuanTable canonical(LuanState luan) throws LuanException, IOException {
+			return new LuanFile(luan,file.getCanonicalFile()).table();
+		}
+
+		public LuanTable create_temp_file(LuanState luan,String prefix,String suffix) throws LuanException, IOException {
+			File tmp = File.createTempFile(prefix,suffix,file);
+			return new LuanFile(luan,tmp).table();
+		}
+
+		public void delete() throws LuanException {
+			if( file.exists() )
+				delete(file);
+		}
+
+		private static void delete(File file) throws LuanException {
+			File[] children = file.listFiles();
+			if( children != null ) {
+				for( File child : children ) {
+					delete(child);
+				}
+			}
+			if( !file.delete() )
+				throw new LuanException("couldn't delete file "+file);
+		}
+
+		public void mkdir() throws LuanException {
+			if( !file.isDirectory() ) {
+				if( !file.mkdirs() )
+					throw new LuanException("couldn't make directory "+file);
+			}
+		}
+
+		public void set_last_modified(long time) throws LuanException {
+			if( !file.setLastModified(time) )
+				throw new LuanException("couldn't set_last_modified on "+file);
+		}
+
+		@Override public LuanTable table() {
+			LuanTable tbl = super.table();
+			try {
+				tbl.rawPut( "name", new LuanJavaFunction(
+					File.class.getMethod( "getName" ), file
+				) );
+				tbl.rawPut( "is_directory", new LuanJavaFunction(
+					File.class.getMethod( "isDirectory" ), file
+				) );
+				tbl.rawPut( "is_file", new LuanJavaFunction(
+					File.class.getMethod( "isFile" ), file
+				) );
+				tbl.rawPut( "delete", new LuanJavaFunction(
+					LuanFile.class.getMethod( "delete" ), this
+				) );
+				tbl.rawPut( "delete_on_exit", new LuanJavaFunction(
+					File.class.getMethod( "deleteOnExit" ), file
+				) );
+				tbl.rawPut( "mkdir", new LuanJavaFunction(
+					LuanFile.class.getMethod( "mkdir" ), this
+				) );
+				tbl.rawPut( "last_modified", new LuanJavaFunction(
+					File.class.getMethod( "lastModified" ), file
+				) );
+				tbl.rawPut( "set_last_modified", new LuanJavaFunction(
+					LuanFile.class.getMethod( "set_last_modified", Long.TYPE ), this
+				) );
+				tbl.rawPut( "length", new LuanJavaFunction(
+					File.class.getMethod( "length" ), file
+				) );
+				tbl.rawPut( "child", new LuanJavaFunction(
+					LuanFile.class.getMethod( "child", LuanState.class, String.class ), this
+				) );
+				tbl.rawPut( "children", new LuanJavaFunction(
+					LuanFile.class.getMethod( "children", LuanState.class ), this
+				) );
+				tbl.rawPut( "parent", new LuanJavaFunction(
+					LuanFile.class.getMethod( "parent", LuanState.class ), this
+				) );
+				tbl.rawPut( "rename_to", new LuanJavaFunction(
+					LuanFile.class.getMethod( "rename_to", Object.class ), this
+				) );
+				tbl.rawPut( "canonical", new LuanJavaFunction(
+					LuanFile.class.getMethod( "canonical", LuanState.class ), this
+				) );
+				tbl.rawPut( "create_temp_file", new LuanJavaFunction(
+					LuanFile.class.getMethod( "create_temp_file", LuanState.class, String.class, String.class ), this
+				) );
+			} catch(NoSuchMethodException e) {
+				throw new RuntimeException(e);
+			}
+			return tbl;
+		}
+	}
+
+	public static LuanTable null_() {
+		return nullIO.table();
+	}
+
+	public static LuanTable string(String s) throws LuanException {
+		Utils.checkNotNull(s);
+		return new LuanString(s).table();
+	}
+
+	public static LuanTable file(LuanState luan,String name) throws LuanException {
+		File file = new File(name);
+		return new LuanFile(file).table();
+	}
+
+	public static LuanTable classpath(LuanState luan,String name) throws LuanException {
+		if( name.contains("//") )
+			return null;
+		String path = name;
+		check(luan,"classpath:"+path);
+		URL url;
+		if( !path.contains("#") ) {
+			url = ClassLoader.getSystemResource(path);
+		} else {
+			String[] a = path.split("#");
+			url = ClassLoader.getSystemResource(a[0]);
+			if( url==null ) {
+				for( int i=1; i<a.length; i++ ) {
+					url = ClassLoader.getSystemResource(a[0]+"/"+a[i]);
+					if( url != null ) {
+						try {
+							url = new URL(url,".");
+						} catch(MalformedURLException e) {
+							throw new RuntimeException(e);
+						}
+						break;
+					}
+				}
+			}
+		}
+		if( url != null )
+			return new LuanUrl(luan,url,null).table();
+
+		return null;
+	}
+
+	private static LuanTable url(LuanState luan,String url,LuanTable options) throws IOException, LuanException {
+		return new LuanUrl(luan,new URL(url),options).table();
+	}
+
+	public static LuanTable http(LuanState luan,String path,LuanTable options) throws IOException, LuanException {
+		return url(luan,"http:"+path,options);
+	}
+
+	public static LuanTable https(LuanState luan,String path,LuanTable options) throws IOException, LuanException {
+		return url(luan,"https:"+path,options);
+	}
+
+	public static LuanTable luan(LuanState luan,String path) throws LuanException {
+		return classpath( luan, "luan/modules/" + path );
+	}
+
+	public static LuanTable stdin(LuanState luan) throws LuanException {
+		LuanTable io = (LuanTable)PackageLuan.require(luan,"luan:Io.luan");
+		return (LuanTable)io.get(luan,"stdin");
+	}
+
+	public static LuanTable newSchemes() {
+		LuanTable schemes = new LuanTable();
+		try {
+			schemes.rawPut( "null", new LuanJavaFunction(IoLuan.class.getMethod("null_"),null) );
+			add( schemes, "string", String.class );
+			add( schemes, "file", LuanState.class, String.class );
+			add( schemes, "classpath", LuanState.class, String.class );
+			add( schemes, "socket", String.class );
+			add( schemes, "http", LuanState.class, String.class, LuanTable.class );
+			add( schemes, "https", LuanState.class, String.class, LuanTable.class );
+			add( schemes, "luan", LuanState.class, String.class );
+			add( schemes, "stdin", LuanState.class );
+			add( schemes, "os", LuanState.class, String.class, LuanTable.class );
+		} catch(NoSuchMethodException e) {
+			throw new RuntimeException(e);
+		}
+		return schemes;
+	}
+
+	private static LuanTable schemes(LuanState luan) throws LuanException {
+		LuanTable t = (LuanTable)PackageLuan.loaded(luan).rawGet("luan:Io.luan");
+		if( t == null )
+			return newSchemes();
+		t = (LuanTable)t.get(luan,"schemes");
+		if( t == null )
+			return newSchemes();
+		return t;
+	}
+
+	public static LuanTable uri(LuanState luan,String name,LuanTable options) throws LuanException {
+		int i = name.indexOf(':');
+		if( i == -1 )
+			throw new LuanException( "invalid Io.uri name '"+name+"', missing scheme" );
+		String scheme = name.substring(0,i);
+		String location = name.substring(i+1);
+		LuanTable schemes = schemes(luan);
+		LuanFunction opener = (LuanFunction)schemes.get(luan,scheme);
+		if( opener == null )
+			throw new LuanException( "invalid scheme '"+scheme+"' in '"+name+"'" );
+		return (LuanTable)Luan.first(opener.call(luan,new Object[]{location,options}));
+	}
+
+	public static final class LuanSocket extends LuanIO {
+		public final Socket socket;
+
+		private LuanSocket(String host,int port) throws LuanException {
+			try {
+				this.socket = new Socket(host,port);
+			} catch(IOException e) {
+				throw new LuanException(e.toString());
+			}
+		}
+
+		private LuanSocket(Socket socket) {
+			this.socket = socket;
+		}
+
+		@Override public InputStream inputStream() throws IOException {
+			return socket.getInputStream();
+		}
+
+		@Override OutputStream outputStream() throws IOException {
+			return socket.getOutputStream();
+		}
+
+		@Override public String to_string() {
+			return socket.toString();
+		}
+
+		@Override public String to_uri_string() {
+			throw new UnsupportedOperationException();
+		}
+	}
+
+	public static LuanTable socket(String name) throws LuanException, IOException {
+		int i = name.indexOf(':');
+		if( i == -1 )
+			throw new LuanException( "invalid socket '"+name+"', format is: <host>:<port>" );
+		String host = name.substring(0,i);
+		String portStr = name.substring(i+1);
+		int port = Integer.parseInt(portStr);
+		return new LuanSocket(host,port).table();
+	}
+
+	public static LuanFunction socket_server(int port) throws IOException {
+		final ServerSocket ss = new ServerSocket(port);
+		return new LuanFunction() {
+			@Override public Object call(LuanState luan,Object[] args) throws LuanException {
+				try {
+					if( args.length > 0 ) {
+						if( args.length > 1 || !"close".equals(args[0]) )
+							throw new LuanException( "the only argument allowed is 'close'" );
+						ss.close();
+						return null;
+					}
+					return new LuanSocket(ss.accept()).table();
+				} catch(IOException e) {
+					throw new LuanException(e);
+				}
+			}
+		};
+	}
+
+
+	public static final class LuanOs extends LuanIO {
+		private final String cmd;
+		private final Process proc;
+
+		private LuanOs(LuanState luan,String cmd,LuanTable options) throws IOException, LuanException {
+			this.cmd = cmd;
+			File dir = null;
+			if( options != null ) {
+				Map map = options.asMap(luan);
+				Object obj = map.remove("dir");
+				dir = objToFile(obj);
+				if( dir==null )
+					throw new LuanException( "bad option 'dir' (string or file table expected)" );
+				if( !map.isEmpty() )
+					throw new LuanException( "unrecognized options: "+map );
+			}
+			this.proc = Runtime.getRuntime().exec(cmd,null,dir);
+		}
+
+		@Override public InputStream inputStream() throws IOException {
+			return proc.getInputStream();
+		}
+
+		@Override OutputStream outputStream() throws IOException {
+			return proc.getOutputStream();
+		}
+
+		@Override public String to_string() {
+			return proc.toString();
+		}
+
+		@Override public String to_uri_string() {
+			throw new UnsupportedOperationException();
+		}
+
+		@Override public boolean exists() {
+			return true;
+		}
+
+		public void wait_for()
+			throws IOException, LuanException
+		{
+			try {
+				proc.waitFor();
+			} catch(InterruptedException e) {
+				throw new RuntimeException(e);
+			}
+			int exitVal = proc.exitValue();
+			if( exitVal != 0 ) {
+				Reader err = new InputStreamReader(proc.getErrorStream());
+				String error = "error in: "+cmd+"\n"+Utils.readAll(err);
+				err.close();
+				throw new LuanException(error);
+			}
+		}
+
+		@Override public String read_text() throws IOException, LuanException {
+			String s = super.read_text();
+			wait_for();
+			return s;
+		}
+
+		@Override public LuanTable table() {
+			LuanTable tbl = super.table();
+			try {
+				tbl.rawPut( "wait_for", new LuanJavaFunction(
+					LuanOs.class.getMethod( "wait_for" ), this
+				) );
+			} catch(NoSuchMethodException e) {
+				throw new RuntimeException(e);
+			}
+			return tbl;
+		}
+	}
+
+	public static LuanTable os(LuanState luan,String cmd,LuanTable options) throws IOException, LuanException {
+		return new LuanOs(luan,cmd,options).table();
+	}
+
+
+	public static String ip(String domain) {
+		try {
+			return InetAddress.getByName(domain).getHostAddress();
+		} catch(UnknownHostException e) {
+			return null;
+		}
+	}
+
+	public static LuanTable my_ips() throws IOException {
+		LuanTable tbl = new LuanTable();
+		for( Enumeration<NetworkInterface> e1 = NetworkInterface.getNetworkInterfaces(); e1.hasMoreElements(); ) {
+			NetworkInterface ni = e1.nextElement();
+			for( Enumeration<InetAddress> e2 = ni.getInetAddresses(); e2.hasMoreElements(); ) {
+				InetAddress ia = e2.nextElement();
+				if( ia instanceof Inet4Address )
+					tbl.rawPut(ia.getHostAddress(),true);
+			}
+		}
+		return tbl;
+	}
+
+/*
+	// files maps zip name to uri
+	public static void zip(LuanState luan,String zipUri,LuanTable files) throws LuanException, IOException {
+		Object obj = uri(luan,zipUri,null).rawGet("java");
+		if( !(obj instanceof LuanIO) )
+			throw new LuanException("invalid uri for zip");
+		LuanIO zipIo = (LuanIO)obj;
+		ZipOutputStream out = new ZipOutputStream(zipIo.outputStream());
+		for( Map.Entry<Object,Object> entry : files.iterable(luan) ) {
+			obj = entry.getKey();
+			if( !(obj instanceof String) )
+				throw new LuanException("zip file table keys must be strings");
+			String fileName = (String)obj;
+			obj = entry.getValue();
+			if( !(obj instanceof String) )
+				throw new LuanException("zip file table values must be strings");
+			String uriStr = (String)obj;
+			out.putNextEntry(new ZipEntry(fileName));
+			obj = uri(luan,uriStr,null).rawGet("java");
+			if( !(obj instanceof LuanIn) )
+				throw new LuanException("invalid uri for zip");
+			LuanIn zipIn = (LuanIn)obj;
+			InputStream in = zipIn.inputStream();
+			Utils.copyAll(in,out);
+			in.close();
+			out.closeEntry();
+		}
+		out.close();
+	}
+*/
+
+	// security
+
+	public interface Security {
+		public void check(LuanState luan,String name) throws LuanException;
+	}
+
+	private static String SECURITY_KEY = "Io.Security";
+
+	private static void check(LuanState luan,String name) throws LuanException {
+		Security s = (Security)luan.registry().get(SECURITY_KEY);
+		if( s!=null )
+			s.check(luan,name);
+	}
+
+	public static void setSecurity(LuanState luan,Security s) {
+		luan.registry().put(SECURITY_KEY,s);
+	}
+
+	private void IoLuan() {}  // never
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/JavaLuan.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/JavaLuan.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,475 @@
+package luan.modules;
+
+import java.lang.reflect.Array;
+import java.lang.reflect.AccessibleObject;
+import java.lang.reflect.Member;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Proxy;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.Collections;
+import java.util.Arrays;
+import luan.Luan;
+import luan.LuanState;
+import luan.LuanTable;
+import luan.LuanException;
+import luan.LuanFunction;
+import luan.LuanJavaFunction;
+
+
+public final class JavaLuan {
+
+	public static void java(LuanState luan) throws LuanException {
+		check(luan,LuanException.currentSource());
+		luan.java.ok = true;
+	}
+
+	public static final LuanFunction javaFn;
+	static {
+		try {
+			javaFn = new LuanJavaFunction(JavaLuan.class.getMethod("java",LuanState.class),null);
+		} catch(NoSuchMethodException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
+	private static void checkJava(LuanState luan) throws LuanException {
+		if( !luan.java.ok )
+			throw new LuanException("Java isn't allowed");
+	}
+
+	static final Object FAIL = new Object();
+
+	public static Object __index(LuanState luan,Object obj,Object key,boolean canReturnFail) throws LuanException {
+		checkJava(luan);
+		Class cls;
+		if( obj instanceof Static ) {
+			Static st = (Static)obj;
+			cls = st.cls;
+			if( key instanceof String ) {
+				String name = (String)key;
+				if( "class".equals(name) ) {
+					return cls;
+				} else if( "new".equals(name) ) {
+					Constructor[] constructors = cls.getConstructors();
+					if( constructors.length > 0 ) {
+						if( constructors.length==1 ) {
+							return new LuanJavaFunction(constructors[0],null);
+						} else {
+							List<LuanJavaFunction> fns = new ArrayList<LuanJavaFunction>();
+							for( Constructor constructor : constructors ) {
+								fns.add(new LuanJavaFunction(constructor,null));
+							}
+							return new AmbiguousJavaFunction(fns);
+						}
+					}
+/*
+				} else if( "assert".equals(name) ) {
+					return new LuanJavaFunction(assertClass,new AssertClass(cls));
+*/
+				} else if( "luan_proxy".equals(name) ) {
+					return new LuanJavaFunction(luan_proxyMethod,st);
+				} else {
+					List<Member> members = getStaticMembers(cls,name);
+					if( !members.isEmpty() ) {
+						return member(null,members);
+					}
+				}
+			}
+		} else {
+			cls = obj.getClass();
+			if( cls.isArray() ) {
+				if( "length".equals(key) ) {
+					return Array.getLength(obj);
+				}
+				Integer i = Luan.asInteger(key);
+				if( i != null ) {
+					return Array.get(obj,i);
+				}
+//				throw new LuanException(luan,"invalid member '"+key+"' for java array: "+obj);
+			} else if( key instanceof String ) {
+				String name = (String)key;
+				if( "instanceof".equals(name) ) {
+					return new LuanJavaFunction(instanceOf,new InstanceOf(obj));
+				} else {
+					List<Member> members = getMembers(cls,name);
+					if( !members.isEmpty() ) {
+						return member(obj,members);
+					}
+				}
+			}
+		}
+//System.out.println("invalid member '"+key+"' for java object: "+obj);
+		if( canReturnFail )
+			return FAIL;
+		throw new LuanException( "invalid index '"+key+"' for java "+cls );
+	}
+
+	private static Object member(Object obj,List<Member> members) throws LuanException {
+		try {
+			if( members.size()==1 ) {
+				Member member = members.get(0);
+				if( member instanceof Static ) {
+					return member;
+				} else if( member instanceof Field ) {
+					Field field = (Field)member;
+					Object rtn = field.get(obj);
+					return rtn instanceof Object[] ? Arrays.asList((Object[])rtn) : rtn;
+				} else {
+					Method method = (Method)member;
+					return new LuanJavaFunction(method,obj);
+				}
+			} else {
+				List<LuanJavaFunction> fns = new ArrayList<LuanJavaFunction>();
+				for( Member member : members ) {
+					Method method = (Method)member;
+					fns.add(new LuanJavaFunction(method,obj));
+				}
+				return new AmbiguousJavaFunction(fns);
+			}
+		} catch(IllegalAccessException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
+	public static void __new_index(LuanState luan,Object obj,Object key,Object value) throws LuanException {
+		checkJava(luan);
+		Class cls;
+		if( obj instanceof Static ) {
+			Static st = (Static)obj;
+			cls = st.cls;
+			if( key instanceof String ) {
+				String name = (String)key;
+				List<Member> members = getStaticMembers(cls,name);
+				if( !members.isEmpty() ) {
+					if( members.size() != 1 )
+						throw new RuntimeException("not field '"+name+"' of "+obj);
+					setMember(obj,members,value);
+					return;
+				}
+			}
+//			throw new LuanException(luan,"invalid member '"+key+"' for: "+obj);
+		} else {
+			cls = obj.getClass();
+			if( cls.isArray() ) {
+				Integer i = Luan.asInteger(key);
+				if( i != null ) {
+					Array.set(obj,i,value);
+					return;
+				}
+//				throw new LuanException(luan,"invalid member '"+key+"' for java array: "+obj);
+			} else if( key instanceof String ) {
+				String name = (String)key;
+				List<Member> members = getMembers(cls,name);
+				if( !members.isEmpty() ) {
+					if( members.size() != 1 )
+						throw new RuntimeException("not field '"+name+"' of "+obj);
+					setMember(obj,members,value);
+					return;
+				}
+			}
+		}
+		throw new LuanException( "invalid index for java "+cls );
+	}
+
+	private static void setMember(Object obj,List<Member> members,Object value) {
+		Field field = (Field)members.get(0);
+		try {
+			try {
+				field.set(obj,value);
+			} catch(IllegalArgumentException e) {
+				Class cls = field.getType();
+				if( value instanceof Number ) {
+					Number n = (Number)value;
+					if( cls.equals(Integer.TYPE) || cls.equals(Integer.class) ) {
+						int r = n.intValue();
+						if( r==n.doubleValue() ) {
+							field.setInt(obj,r);
+							return;
+						}
+					}
+				}
+				throw e;
+			}
+		} catch(IllegalAccessException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
+	public static boolean privateAccess = false;
+	private static Map<Class,Map<String,List<Member>>> memberMap = new HashMap<Class,Map<String,List<Member>>>();
+
+	private static synchronized List<Member> getMembers(Class cls,String name) {
+		Map<String,List<Member>> clsMap = memberMap.get(cls);
+		if( clsMap == null ) {
+			clsMap = new HashMap<String,List<Member>>();
+			for( Class c : cls.getClasses() ) {
+				String s = c.getSimpleName();
+				List<Member> list = new ArrayList<Member>();
+				clsMap.put(s,list);
+				list.add(new Static(c));
+			}
+			for( Field field : cls.getFields() ) {
+				String s = field.getName();
+				try {
+					if( !cls.getField(s).equals(field) )
+						continue;  // not accessible
+				} catch(NoSuchFieldException e) {
+					throw new RuntimeException(e);
+				}
+				List<Member> list = new ArrayList<Member>();
+				clsMap.put(s,list);
+				list.add(field);
+			}
+			for( Method method : cls.getMethods() ) {
+				String s = method.getName();
+				List<Member> list = clsMap.get(s);
+				if( list == null || !(list.get(0) instanceof Method) ) {
+					list = new ArrayList<Member>();
+					clsMap.put(s,list);
+				}
+				list.add(method);
+			}
+			if( privateAccess ) {
+				for( Method method : cls.getDeclaredMethods() ) {
+					String s = method.getName();
+					List<Member> list = clsMap.get(s);
+					if( list == null ) {
+						list = new ArrayList<Member>();
+						clsMap.put(s,list);
+					} else if( !(list.get(0) instanceof Method) )
+						continue;
+					if( !list.contains(method) ) {
+						list.add(method);
+					}
+				}
+				for( Field field : cls.getDeclaredFields() ) {
+					String s = field.getName();
+					List<Member> list = clsMap.get(s);
+					if( list == null ) {
+						list = new ArrayList<Member>();
+						clsMap.put(s,list);
+						list.add(field);
+					}
+				}
+			}
+			for( List<Member> members : clsMap.values() ) {
+				for( Member m : members ) {
+					if( m instanceof AccessibleObject )
+						((AccessibleObject)m).setAccessible(true);
+				}
+			}
+			memberMap.put(cls,clsMap);
+		}
+		List<Member> rtn = clsMap.get(name);
+		if( rtn==null )
+			rtn = Collections.emptyList();
+		return rtn;
+	}
+
+	private static synchronized List<Member> getStaticMembers(Class cls,String name) {
+		List<Member> staticMembers = new ArrayList<Member>();
+		for( Member m : getMembers(cls,name) ) {
+			if( Modifier.isStatic(m.getModifiers()) )
+				staticMembers.add(m);
+		}
+		return staticMembers;
+	}
+
+	static final class Static implements Member {
+		final Class cls;
+
+		Static(Class cls) {
+			this.cls = cls;
+		}
+
+		@Override public String toString() {
+			return cls.toString();
+		}
+
+		@Override public Class getDeclaringClass() {
+			return cls.getDeclaringClass();
+		}
+
+		@Override public String getName() {
+			return cls.getName();
+		}
+
+		@Override public int getModifiers() {
+			return cls.getModifiers();
+		}
+
+		@Override public boolean isSynthetic() {
+			return cls.isSynthetic();
+		}
+
+		public Object luan_proxy(final LuanState luan,final LuanTable t) throws LuanException {
+			return Proxy.newProxyInstance(
+				cls.getClassLoader(),
+				new Class[]{cls},
+				new InvocationHandler() {
+					public Object invoke(Object proxy,Method method, Object[] args)
+						throws Throwable
+					{
+						if( args==null )
+							args = new Object[0];
+						String name = method.getName();
+						Object fnObj = t.get(luan,name);
+						if( fnObj == null )
+							throw new NullPointerException("luan_proxy couldn't find method '"+name+"'");
+						LuanFunction fn = Luan.checkFunction(fnObj);
+						return Luan.first(fn.call(luan,args));
+					}
+				}
+			);
+		}
+	}
+	private static final Method luan_proxyMethod;
+	static {
+		try {
+			luan_proxyMethod = Static.class.getMethod("luan_proxy",LuanState.class,LuanTable.class);
+			luan_proxyMethod.setAccessible(true);
+		} catch(NoSuchMethodException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
+	public static Static load(LuanState luan,String name) throws LuanException {
+		checkJava(luan);
+		Class cls;
+		try {
+			cls = Class.forName(name);
+		} catch(ClassNotFoundException e) {
+			try {
+				cls = Thread.currentThread().getContextClassLoader().loadClass(name);
+			} catch(ClassNotFoundException e2) {
+				return null;
+			}
+		}
+		return new Static(cls);
+	}
+
+	private static class AmbiguousJavaFunction extends LuanFunction {
+		private final Map<Integer,List<LuanJavaFunction>> fnMap = new HashMap<Integer,List<LuanJavaFunction>>();
+
+		AmbiguousJavaFunction(List<LuanJavaFunction> fns) {
+			for( LuanJavaFunction fn : fns ) {
+				Integer n = fn.getParameterTypes().length;
+				List<LuanJavaFunction> list = fnMap.get(n);
+				if( list==null ) {
+					list = new ArrayList<LuanJavaFunction>();
+					fnMap.put(n,list);
+				}
+				list.add(fn);
+			}
+		}
+
+		@Override public Object call(LuanState luan,Object[] args) throws LuanException {
+			for( LuanJavaFunction fn : fnMap.get(args.length) ) {
+				try {
+					return fn.rawCall(luan,args);
+				} catch(IllegalArgumentException e) {}
+			}
+			throw new LuanException("no method matched args: "+Arrays.asList(args));
+		}
+	}
+
+	private static class InstanceOf {
+		private final Object obj;
+
+		InstanceOf(Object obj) {
+			this.obj = obj;
+		}
+
+		public boolean instanceOf(Static st) {
+			return st.cls.isInstance(obj);
+		}
+	}
+	private static final Method instanceOf;
+	static {
+		try {
+			instanceOf = InstanceOf.class.getMethod("instanceOf",Static.class);
+			instanceOf.setAccessible(true);
+		} catch(NoSuchMethodException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
+/*
+	private static class AssertClass {
+		private final Class cls;
+
+		AssertClass(Class cls) {
+			this.cls = cls;
+		}
+
+		public Object assertClass(LuanState luan,Object v) throws LuanException {
+			if( !cls.isInstance(v) ) {
+				String got = v.getClass().getSimpleName();
+				String expected = cls.getSimpleName();
+				throw new LuanException(luan,"bad argument #1 ("+expected+" expected, got "+got+")");
+			}
+			return v;
+		}
+	}
+	private static final Method assertClass;
+	static {
+		try {
+			assertClass = AssertClass.class.getMethod("assertClass",LuanState.class,Object.class);
+			assertClass.setAccessible(true);
+		} catch(NoSuchMethodException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
+
+	public static Object proxy(final LuanState luan,Static st,final LuanTable t,final Object base) throws LuanException {
+		return Proxy.newProxyInstance(
+			st.cls.getClassLoader(),
+			new Class[]{st.cls},
+			new InvocationHandler() {
+				public Object invoke(Object proxy,Method method, Object[] args)
+					throws Throwable
+				{
+					if( args==null )
+						args = new Object[0];
+					String name = method.getName();
+					Object fnObj = t.get(name);
+					if( fnObj==null && base!=null )
+						return method.invoke(base,args);
+					LuanFunction fn = luan.checkFunction(fnObj);
+					return Luan.first(luan.call(fn,name,args));
+				}
+			}
+		);
+	}
+*/
+
+
+
+	// security
+
+	public interface Security {
+		public void check(LuanState luan,String name) throws LuanException;
+	}
+
+	private static String SECURITY_KEY = "Java.Security";
+
+	private static void check(LuanState luan,String name) throws LuanException {
+		Security s = (Security)luan.registry().get(SECURITY_KEY);
+		if( s!=null )
+			s.check(luan,name);
+	}
+
+	public static void setSecurity(LuanState luan,Security s) {
+		luan.registry().put(SECURITY_KEY,s);
+	}
+
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/Luan.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/Luan.luan	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,50 @@
+java()
+local BasicLuan = require "java:luan.modules.BasicLuan"
+
+local M = {}
+
+M.assert_binary = BasicLuan.assert_binary
+M.assert_boolean = BasicLuan.assert_boolean
+M.assert_function = BasicLuan.assert_function
+M.assert_integer = BasicLuan.assert_integer
+M.assert_long = BasicLuan.assert_long
+M.assert_number = BasicLuan.assert_number
+M.assert_string = BasicLuan.assert_string
+M.assert_table = BasicLuan.assert_table
+M.get_metatable = BasicLuan.get_metatable
+M.ipairs = BasicLuan.ipairs
+M.load = BasicLuan.load
+M.load_file = BasicLuan.load_file
+M.new_error = BasicLuan.new_error
+M.pairs = BasicLuan.pairs
+M.pcall = BasicLuan.pcall
+M.range = BasicLuan.range
+M.raw_equal = BasicLuan.raw_equal
+M.raw_get = BasicLuan.raw_get
+M.raw_len = BasicLuan.raw_len
+M.raw_set = BasicLuan.raw_set
+M.set_metatable = BasicLuan.set_metatable
+M.to_string = BasicLuan.to_string
+M.try = BasicLuan.try_
+M.type = BasicLuan.type
+M.values = BasicLuan.values
+
+function M.do_file(uri)
+	return M.load_file(uri)()
+end
+
+M.VERSION = M.do_file "classpath:luan/version.luan"
+
+function M.error(message)
+	M.new_error(message).throw()
+end
+
+function M.assert(v,message)
+	return v or M.error(message or "assertion failed!")
+end
+
+function M.eval(s,source_name)
+	return M.load( "return "..s, source_name or "eval" )()
+end
+
+return M
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/Math.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/Math.luan	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,36 @@
+java()
+local MathLuan = require "java:luan.modules.MathLuan"
+local JavaMath = require "java:java.lang.Math"
+local Integer = require "java:java.lang.Integer"
+local Double = require "java:java.lang.Double"
+
+
+local M = {}
+
+M.abs = MathLuan.abs
+M.acos = MathLuan.acos
+M.asin = MathLuan.asin
+M.atan = MathLuan.atan
+M.atan2 = MathLuan.atan2
+M.ceil = MathLuan.ceil
+M.cos = MathLuan.cos
+M.cosh = MathLuan.cosh
+M.deg = MathLuan.deg
+M.exp = MathLuan.exp
+M.floor = MathLuan.floor
+M.fmod = MathLuan.fmod
+M.huge = Double.POSITIVE_INFINITY
+M.log = MathLuan.log
+M.max = MathLuan.max
+M.max_integer = Integer.MAX_VALUE
+M.min = MathLuan.min
+M.min_integer = Integer.MIN_VALUE
+M.modf = MathLuan.modf
+M.pi = JavaMath.PI
+M.rad = MathLuan.rad
+M.random = MathLuan.random
+M.sin = MathLuan.sin
+M.sqrt = MathLuan.sqrt
+M.tan = MathLuan.tan
+
+return M
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/MathLuan.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/MathLuan.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,113 @@
+package luan.modules;
+
+import luan.Luan;
+import luan.LuanState;
+import luan.LuanTable;
+import luan.LuanFunction;
+import luan.LuanException;
+
+
+public final class MathLuan {
+
+	public static double abs(double x) {
+		return Math.abs(x);
+	}
+
+	public static double acos(double x) {
+		return Math.acos(x);
+	}
+
+	public static double asin(double x) {
+		return Math.asin(x);
+	}
+
+	public static double atan(double x) {
+		return Math.atan(x);
+	}
+
+	public static double atan2(double y,double x) {
+		return Math.atan2(y,x);
+	}
+
+	public static double ceil(double x) {
+		return Math.ceil(x);
+	}
+
+	public static double cos(double x) {
+		return Math.cos(x);
+	}
+
+	public static double cosh(double x) {
+		return Math.cosh(x);
+	}
+
+	public static double deg(double x) {
+		return Math.toDegrees(x);
+	}
+
+	public static double exp(double x) {
+		return Math.exp(x);
+	}
+
+	public static double floor(double x) {
+		return Math.floor(x);
+	}
+
+	public static double fmod(double x,double y) {
+		return x % y;
+	}
+
+	public static double log(double x,Double base) {
+		return base==null ? Math.log(x) : Math.log(x)/Math.log(base);
+	}
+
+	public static double min(double x,double... a) {
+		for( double d : a ) {
+			if( x > d )
+				x = d;
+		}
+		return x;
+	}
+
+	public static double max(double x,double... a) {
+		for( double d : a ) {
+			if( x < d )
+				x = d;
+		}
+		return x;
+	}
+
+	public static double[] modf(double x) {
+		double i = (int)x;
+		return new double[]{i,x-i};
+	}
+
+	public static double rad(double x) {
+		return Math.toRadians(x);
+	}
+
+	public static double random(Integer m,Integer n) {
+		if( m==null )
+			return Math.random();
+		if( n==null )
+			return Math.floor(m*Math.random()) + 1;
+		return Math.floor((n-m+1)*Math.random()) + m;
+	}
+
+	public static double sin(double x) {
+		return Math.sin(x);
+	}
+
+	public static double sqrt(double x) {
+		return Math.sqrt(x);
+	}
+
+	public static double tan(double x) {
+		return Math.tan(x);
+	}
+
+	public static String long_to_string(long i,int radix) {
+		return Long.toString(i,radix);
+	}
+
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/Number.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/Number.luan	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,14 @@
+java()
+local BasicLuan = require "java:luan.modules.BasicLuan"
+local MathLuan = require "java:luan.modules.MathLuan"
+
+
+local M = {}
+
+M.double = BasicLuan.assert_double
+M.integer = BasicLuan.assert_integer
+M.long = BasicLuan.assert_long
+M.long_to_string = MathLuan.long_to_string
+M.type = BasicLuan.number_type
+
+return M
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/Package.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/Package.luan	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,9 @@
+java()
+local PackageLuan = require "java:luan.modules.PackageLuan"
+
+local M = {}
+
+M.loaded = PackageLuan.loaded()
+M.load = PackageLuan.load
+
+return M
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/PackageLuan.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/PackageLuan.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,79 @@
+package luan.modules;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import luan.Luan;
+import luan.LuanState;
+import luan.LuanTable;
+import luan.LuanFunction;
+import luan.LuanJavaFunction;
+import luan.LuanException;
+
+
+public final class PackageLuan {
+
+	public static final LuanFunction requireFn;
+	static {
+		try {
+			requireFn = new LuanJavaFunction(PackageLuan.class.getMethod("require",LuanState.class,String.class),null);
+		} catch(NoSuchMethodException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
+	public static LuanTable loaded(LuanState luan) {
+		LuanTable tbl = (LuanTable)luan.registry().get("Package.loaded");
+		if( tbl == null ) {
+			tbl = new LuanTable();
+			luan.registry().put("Package.loaded",tbl);
+		}
+		return tbl;
+	}
+
+	public static Object require(LuanState luan,String modName) throws LuanException {
+		Object mod = load(luan,modName);
+		if( mod==null )
+			throw new LuanException( "module '"+modName+"' not found" );
+		return mod;
+	}
+
+	public static Object load(LuanState luan,String modName) throws LuanException {
+		LuanTable loaded = loaded(luan);
+		Object mod = loaded.rawGet(modName);
+		if( mod == null ) {
+			if( modName.startsWith("java:") ) {
+				mod = JavaLuan.load(luan,modName.substring(5));
+			} else {
+				String src = read(luan,modName);
+				if( src == null )
+					return null;
+				LuanFunction loader = Luan.load(src,modName);
+				mod = Luan.first(
+					loader.call(luan,new Object[]{modName})
+				);
+				if( mod == null ) {
+					mod = loaded.rawGet(modName);
+					if( mod != null )
+						return mod;
+					throw new LuanException( "module '"+modName+"' returned nil" );
+				}
+			}
+			loaded.rawPut(modName,mod);
+		}
+		return mod;
+	}
+
+	static String read(LuanState luan,String uri) throws LuanException {
+		LuanTable t = IoLuan.uri(luan,uri,null);
+		if( t == null )
+			return null;
+		LuanFunction existsFn = (LuanFunction)t.get(luan,"exists");
+		boolean exists = (Boolean)Luan.first(existsFn.call(luan));
+		if( !exists )
+			return null;
+		LuanFunction reader = (LuanFunction)t.get(luan,"read_text");
+		return (String)Luan.first(reader.call(luan));
+	}
+
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/Parsers.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/Parsers.luan	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,15 @@
+java()
+local BBCode = require "java:luan.modules.parsers.BBCode"
+local Csv = require "java:luan.modules.parsers.Csv"
+local Theme = require "java:luan.modules.parsers.Theme"
+local Json = require "java:luan.modules.parsers.Json"
+
+local M = {}
+
+M.bbcode_to_html = BBCode.toHtml
+M.bbcode_to_text = BBCode.toText
+M.csv_to_list = Csv.toList
+M.json_parse = Json.parse
+M.theme_to_luan = Theme.toLuan
+
+return M
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/Rpc.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/Rpc.luan	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,68 @@
+java()
+local RpcLuan = require "java:luan.modules.RpcLuan"
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local set_metatable = Luan.set_metatable or error()
+local try = Luan.try or error()
+local Io = require "luan:Io.luan"
+local Thread = require "luan:Thread.luan"
+local Logging = require "luan:logging/Logging.luan"  -- external dependency
+local logger = Logging.logger "Rpc"
+
+
+local M = {}
+
+M.port = 9101
+
+M.call = RpcLuan.call  -- Rpc.call(socket,fn_name,...)
+
+M.functions = {}
+
+function M.respond(socket,fns)
+	RpcLuan.respond( socket, fns or M.functions )
+end
+
+function M.remote_socket(socket_uri)
+	local mt = {}
+	function mt.__index(_,key)
+		return function(...)
+			local socket = Io.uri(socket_uri)
+			return M.call(socket,key,...)
+		end
+	end
+	local t = {}
+	set_metatable(t,mt)
+	return t
+end
+
+function M.remote(domain)
+	local socket = "socket:" .. domain .. ":" .. M.port
+	return M.remote_socket(socket)
+end
+
+function M.serve(port,fns)
+	local server = Io.socket_server(port or M.port)
+	while true do
+		try {
+			function()
+				local socket = server()
+				local function respond()
+					try {
+						function()
+							M.respond(socket,fns)
+						end
+						catch = function(e)
+							logger.error(e)
+						end
+					}
+				end
+				Thread.fork(respond)
+			end
+			catch = function(e)
+				logger.error(e)
+			end
+		}
+	end
+end
+
+return M
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/RpcLuan.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/RpcLuan.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,310 @@
+package luan.modules;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.InputStreamReader;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.EOFException;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.util.Set;
+import java.util.IdentityHashMap;
+import java.util.Collections;
+import java.util.Map;
+import luan.Luan;
+import luan.LuanState;
+import luan.LuanTable;
+import luan.LuanFunction;
+import luan.LuanException;
+import luan.LuanMethod;
+import luan.modules.parsers.Json;
+import luan.modules.parsers.ParseException;
+
+
+public final class RpcLuan {
+	private static final int NIL = 0;
+	private static final int STRING = 1;
+	private static final int BOOLEAN = 2;
+	private static final int NUMBER = 3;
+	private static final int BINARY = 4;
+	private static final int TABLE = 5;
+	private static final int IO = 6;
+	private static final int LONG = 7;
+
+	@LuanMethod public static Object[] call(LuanState luan,LuanTable socketTbl,String fnName,Object... args)
+		throws LuanException, IOException
+	{
+		IoLuan.LuanSocket luanSocket = (IoLuan.LuanSocket)socketTbl.rawGet("java");
+		Socket socket = luanSocket.socket;
+		InputStream in = new BufferedInputStream(socket.getInputStream());
+		OutputStream out = new BufferedOutputStream(socket.getOutputStream());
+		Close close = new Close();
+		try {
+			writeString(out,fnName);
+			writeObjs(out,luan,args);
+			out.flush();
+			socket.shutdownOutput();
+			boolean ok = readBoolean(in);
+			if( ok ) {
+				return readObjs(in,luan,close);
+			} else {
+				String msg = readString(in);
+				throw new LuanException(msg);
+			}
+		} finally {
+			if( close.b) {
+				socket.close();
+			}
+		}
+	}
+
+	public static void respond(LuanState luan,LuanTable socketTbl,LuanTable fns)
+		throws IOException, LuanException
+	{
+		IoLuan.LuanSocket luanSocket = (IoLuan.LuanSocket)socketTbl.rawGet("java");
+		Socket socket = luanSocket.socket;
+		InputStream in = new BufferedInputStream(socket.getInputStream());
+		OutputStream out = new BufferedOutputStream(socket.getOutputStream());
+		try {
+			Object[] rtn;
+			try {
+				String fnName = readString(in);
+				Object[] args = readObjs(in,luan,null);
+				LuanFunction fn = (LuanFunction)fns.get(luan,fnName);
+				if( fn == null )
+					throw new LuanException( "function not found: " + fnName );
+				rtn = Luan.array(fn.call(luan,args));
+			} catch(LuanException e) {
+				writeBoolean(out,false);
+				writeString(out,e.getFullMessage());
+				out.flush();
+				return;
+			}
+			writeBoolean(out,true);
+			writeObjs(out,luan,rtn);
+			out.flush();
+		} finally {
+			socket.close();
+		}
+	}
+
+	private static void writeObjs(OutputStream out,LuanState luan,Object[] a) throws IOException, LuanException {
+		IoLuan.LuanIn luanIn = null;
+		writeInt(out,a.length);
+		for( Object obj : a ) {
+			if( obj instanceof LuanTable ) {
+				LuanTable tbl = (LuanTable)obj;
+				Object java = tbl.rawGet("java");
+				if( java instanceof IoLuan.LuanIn ) {
+					if( luanIn != null )
+						throw new LuanException("can't have multiple IO params");
+					luanIn = (IoLuan.LuanIn)java;
+					out.write(IO);
+					continue;
+				}
+			}
+			writeObj(out,luan,obj);
+		}
+		if( luanIn != null ) {
+			InputStream in = luanIn.inputStream();
+			Utils.copyAll(in,out);
+		}
+	}
+
+	private static Object[] readObjs(InputStream in,LuanState luan,Close close) throws IOException, LuanException {
+		int n = readInt(in);
+		Object[] rtn = new Object[n];
+		for( int i=0; i<n; i++ ) {
+			rtn[i] = readObj(in,luan,close);
+		}
+		return rtn;
+	}
+
+	private static void writeObj(OutputStream out,LuanState luan,Object obj) throws IOException, LuanException {
+		if( obj == null ) {
+			out.write(NIL);
+		}
+		else if( obj instanceof String ) {
+			out.write(STRING);
+			writeString(out,(String)obj);
+		}
+		else if( obj instanceof Boolean ) {
+			out.write(BOOLEAN);
+			writeBoolean(out,(Boolean)obj);
+		}
+		else if( obj instanceof Long ) {
+			out.write(LONG);
+			writeString(out,obj.toString());
+		}
+		else if( obj instanceof Number ) {
+			out.write(NUMBER);
+			writeString(out,obj.toString());
+		}
+		else if( obj instanceof byte[] ) {
+			byte[] a = (byte[])obj;
+			out.write(BINARY);
+			writeInt(out,a.length);
+			out.write(a);
+		}
+		else if( obj instanceof LuanTable ) {
+			out.write(TABLE);
+//			String s = pickle( luan, obj, Collections.newSetFromMap(new IdentityHashMap<LuanTable,Boolean>()) );
+			String s = Json.toString(obj);
+			writeString(out,s);
+		}
+		else
+			throw new LuanException( "invalid type: " + obj.getClass() );
+	}
+
+	private static Object readObj(InputStream in,LuanState luan,Close close) throws IOException, LuanException {
+		int type = in.read();
+		switch(type) {
+		case NIL:
+			return null;
+		case STRING:
+			return readString(in);
+		case BOOLEAN:
+			return readBoolean(in);
+		case LONG:
+			return Long.valueOf(readString(in));
+		case NUMBER:
+			return Double.valueOf(readString(in));
+		case BINARY:
+			return readBinary(in,readInt(in));
+		case TABLE:
+			String s = readString(in);
+/*
+			LuanFunction fn = Luan.load("return "+s,"rpc-reader");
+			return fn.call(luan);
+*/
+			try {
+				return Json.parse(s);
+			} catch(ParseException e) {
+				throw new LuanException(e);
+			}
+		case IO:
+			return new LuanInputStream(in,close).table();
+		default:
+			throw new LuanException( "invalid type: " + type );
+		}
+	}
+
+	private static Boolean readBoolean(InputStream in) throws IOException {
+		return Boolean.valueOf(readString(in));
+	}
+
+	private static String readString(InputStream in) throws IOException {
+		int len = readInt(in);
+		byte[] a = readBinary(in,len);
+		return new String(a,StandardCharsets.UTF_8);
+	}
+
+	private static int readInt(InputStream in) throws IOException {
+		int ch1 = in.read();
+		int ch2 = in.read();
+		int ch3 = in.read();
+		int ch4 = in.read();
+		if ((ch1 | ch2 | ch3 | ch4) < 0)
+			throw new EOFException();
+		return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + (ch4 << 0));
+	}
+
+	private static byte[] readBinary(InputStream in,int size) throws IOException {
+		byte[] a = new byte[size];
+		int i = 0;
+		while( i < size ) {
+			int n = in.read(a,i,size-i);
+			if( n == -1 )
+				throw new EOFException();
+			i += n;
+		}
+		return a;
+	}
+
+	private static void writeBoolean(OutputStream out,Boolean b) throws IOException {
+		writeString(out,b.toString());
+	}
+
+	private static void writeString(OutputStream out,String s) throws IOException {
+		byte[] a = s.getBytes(StandardCharsets.UTF_8);
+		writeInt(out,a.length);
+		out.write(a);
+	}
+
+	private static void writeInt(OutputStream out,int v) throws IOException {
+        out.write((v >>> 24) & 0xFF);
+        out.write((v >>> 16) & 0xFF);
+        out.write((v >>>  8) & 0xFF);
+        out.write((v >>>  0) & 0xFF);
+	}
+
+/*
+	private static String pickle(LuanState luan,Object obj,Set<LuanTable> set) throws LuanException {
+		if( obj == null )
+			return "nil";
+		if( obj instanceof Boolean )
+			return obj.toString();
+		if( obj instanceof Number )
+			return Luan.toString((Number)obj);
+		if( obj instanceof String )
+			return "\"" + Luan.stringEncode((String)obj) + "\"";
+		if( obj instanceof LuanTable ) {
+			LuanTable tbl = (LuanTable)obj;
+			if( !set.add(tbl) ) {
+				throw new LuanException( "circular reference in table" );
+			}
+			StringBuilder sb = new StringBuilder();
+			sb.append( "{" );
+			for( Map.Entry<Object,Object> entry : tbl.iterable(luan) ) {
+				sb.append( "[" );
+				sb.append( pickle(luan,entry.getKey(),set) );
+				sb.append( "]=" );
+				sb.append( pickle(luan,entry.getValue(),set) );
+				sb.append( ", " );
+			}
+			sb.append( "}" );
+			return sb.toString();
+		}
+		throw new LuanException( "invalid type: " + obj.getClass() );
+	}
+*/
+
+	private static class Close {
+		boolean b = true;
+	}
+
+	private static class LuanInputStream extends IoLuan.LuanIn {
+		private final InputStream in;
+		private final boolean close;
+
+		public LuanInputStream(InputStream in,Close close) {
+			this.in = in;
+			this.close = close!=null && close.b;
+			if(this.close)  close.b = false;
+		}
+
+		@Override public InputStream inputStream() {
+			return new FilterInputStream(in) {
+				@Override public void close() throws IOException {
+					if(close)  super.close();
+				}
+			};
+		}
+
+		@Override public String to_string() {
+			return "<input_stream>";
+		}
+
+		@Override public String to_uri_string() {
+			throw new UnsupportedOperationException();
+		}
+
+		@Override public boolean exists() {
+			return true;
+		}
+	};
+
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/String.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/String.luan	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,28 @@
+java()
+local StringLuan = require "java:luan.modules.StringLuan"
+local Pattern = require "java:java.util.regex.Pattern"
+
+local M = {}
+
+M.char = StringLuan.char_
+M.concat = StringLuan.concat
+M.encode = StringLuan.encode
+M.find = StringLuan.find
+M.format = StringLuan.format
+M.gmatch = StringLuan.gmatch
+M.gsub = StringLuan.gsub
+M.literal = Pattern.quote
+M.lower = StringLuan.lower
+M.match = StringLuan.match
+M.matches = StringLuan.matches
+M.rep = StringLuan.rep
+M.reverse = StringLuan.reverse
+M.split = StringLuan.split
+M.sub = StringLuan.sub
+M.to_binary = StringLuan.to_binary
+M.to_number = StringLuan.to_number
+M.trim = StringLuan.trim
+M.unicode = StringLuan.unicode
+M.upper = StringLuan.upper
+
+return M
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/StringLuan.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/StringLuan.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,247 @@
+package luan.modules;
+
+import java.util.Arrays;
+import java.util.regex.Pattern;
+import java.util.regex.Matcher;
+import luan.Luan;
+import luan.LuanState;
+import luan.LuanTable;
+import luan.LuanFunction;
+import luan.LuanException;
+import luan.LuanMethod;
+
+
+public final class StringLuan {
+
+	static int start(String s,int i) {
+		int len = s.length();
+		return i==0 ? 0 : i > 0 ? Math.min(i-1,len) : Math.max(len+i,0);
+	}
+
+	static int start(String s,Integer i,int dflt) {
+		return i==null ? dflt : start(s,i);
+	}
+
+	static int end(String s,int i) {
+		int len = s.length();
+		return i==0 ? 0 : i > 0 ? Math.min(i,len) : Math.max(len+i+1,0);
+	}
+
+	static int end(String s,Integer i,int dflt) {
+		return i==null ? dflt : end(s,i);
+	}
+
+	@LuanMethod public static Integer[] unicode(String s,Integer i,Integer j) throws LuanException {
+		Utils.checkNotNull(s);
+		int start = start(s,i,1);
+		int end = end(s,j,start+1);
+		Integer[] chars = new Integer[end-start];
+		for( int k=0; k<chars.length; k++ ) {
+			chars[k] = (int)s.charAt(start+k);
+		}
+		return chars;
+	}
+
+	public static String char_(int... chars) {
+		char[] a = new char[chars.length];
+		for( int i=0; i<chars.length; i++ ) {
+			a[i] = (char)chars[i];
+		}
+		return new String(a);
+	}
+
+	@LuanMethod public static byte[] to_binary(String s) {
+		return s.getBytes();
+	}
+
+	public static String lower(String s) throws LuanException {
+		Utils.checkNotNull(s);
+		return s.toLowerCase();
+	}
+
+	public static String upper(String s) throws LuanException {
+		Utils.checkNotNull(s);
+		return s.toUpperCase();
+	}
+
+	public static String trim(String s) throws LuanException {
+		Utils.checkNotNull(s);
+		return s.trim();
+	}
+
+	public static String reverse(String s) throws LuanException {
+		Utils.checkNotNull(s);
+		return new StringBuilder(s).reverse().toString();
+	}
+
+	public static String rep(String s,int n,String sep) {
+		if( n < 1 )
+			return "";
+		StringBuilder buf = new StringBuilder(s);
+		while( --n > 0 ) {
+			if( sep != null )
+				buf.append(sep);
+			buf.append(s);
+		}
+		return buf.toString();
+	}
+
+	public static String sub(String s,int i,Integer j) throws LuanException {
+		Utils.checkNotNull(s);
+		int start = start(s,i);
+		int end = end(s,j,s.length());
+		return s.substring(start,end);
+	}
+
+	@LuanMethod public static Object[] find(String s,String pattern,Integer init,Boolean plain) {
+		int start = start(s,init,0);
+		if( Boolean.TRUE.equals(plain) ) {
+			int i = s.indexOf(pattern,start);
+			return i == -1 ? null : new Integer[]{i+1,i+pattern.length()};
+		}
+		Matcher m = Pattern.compile(pattern).matcher(s);
+		if( !m.find(start) )
+			return null;
+		int n = m.groupCount();
+		Object[] rtn = new Object[2+n];
+		rtn[0] = m.start() + 1;
+		rtn[1] = m.end();
+		for( int i=0; i<n; i++ ) {
+			rtn[2+i] = m.group(i+1);
+		}
+		return rtn;
+	}
+
+	@LuanMethod public static String[] match(String s,String pattern,Integer init) {
+		int start = start(s,init,0);
+		Matcher m = Pattern.compile(pattern).matcher(s);
+		if( !m.find(start) )
+			return null;
+		int n = m.groupCount();
+		if( n == 0 )
+			return new String[]{m.group()};
+		String[] rtn = new String[n];
+		for( int i=0; i<n; i++ ) {
+			rtn[i] = m.group(i+1);
+		}
+		return rtn;
+	}
+
+	public static LuanFunction gmatch(String s,String pattern) throws LuanException {
+		Utils.checkNotNull(s);
+		final Matcher m = Pattern.compile(pattern).matcher(s);
+		return new LuanFunction() {
+			@Override public Object call(LuanState luan,Object[] args) {
+				if( !m.find() )
+					return null;
+				final int n = m.groupCount();
+				if( n == 0 )
+					return m.group();
+				String[] rtn = new String[n];
+				for( int i=0; i<n; i++ ) {
+					rtn[i] = m.group(i+1);
+				}
+				return rtn;
+			}
+		};
+	}
+
+	@LuanMethod public static Object[] gsub(LuanState luan,String s,String pattern,Object repl,Integer n) throws LuanException {
+		Utils.checkNotNull(s);
+		int max = n==null ? Integer.MAX_VALUE : n;
+		final Matcher m = Pattern.compile(pattern).matcher(s);
+		if( repl instanceof String ) {
+			String replacement = (String)repl;
+			int i = 0;
+			StringBuffer sb = new StringBuffer();
+			while( i<max && m.find() ) {
+				m.appendReplacement(sb,replacement);
+				i++;
+			}
+			m.appendTail(sb);
+			return new Object[]{ sb.toString(), i };
+		}
+		if( repl instanceof LuanTable ) {
+			LuanTable t = (LuanTable)repl;
+			int i = 0;
+			StringBuffer sb = new StringBuffer();
+			while( i<max && m.find() ) {
+				String match = m.groupCount()==0 ? m.group() : m.group(1);
+				Object val = t.get(luan,match);
+				if( val != null ) {
+					String replacement = luan.toString(val);
+					m.appendReplacement(sb,replacement);
+				}
+				i++;
+			}
+			m.appendTail(sb);
+			return new Object[]{ sb.toString(), i };
+		}
+		if( repl instanceof LuanFunction ) {
+			LuanFunction fn = (LuanFunction)repl;
+			int i = 0;
+			StringBuffer sb = new StringBuffer();
+			while( i<max && m.find() ) {
+				Object[] args;
+				final int count = m.groupCount();
+				if( count == 0 ) {
+					args = new String[]{m.group()};
+				} else {
+					args = new String[count];
+					for( int j=0; j<count; j++ ) {
+						args[j] = m.group(j+1);
+					}
+				}
+				Object val = Luan.first( fn.call(luan,args) );
+				if( val != null ) {
+					String replacement = luan.toString(val);
+					m.appendReplacement(sb,replacement);
+				}
+				i++;
+			}
+			m.appendTail(sb);
+			return new Object[]{ sb.toString(), i };
+		}
+		throw new LuanException( "bad argument #3 to 'gsub' (string/function/table expected)" );
+	}
+
+	// note - String.format() is too stupid to convert between ints and floats.
+	public static String format(String format,Object... args) {
+		return String.format(format,args);
+	}
+
+	public static String concat(LuanState luan,Object... args) throws LuanException {
+		StringBuilder sb = new StringBuilder();
+		for( Object arg : args ) {
+			sb.append( luan.toString(arg) );
+		}
+		return sb.toString();
+	}
+
+	public static String encode(String s) {
+		return Luan.stringEncode(s);
+	}
+
+	public static Number to_number(String s,Integer base) throws LuanException {
+		Utils.checkNotNull(s);
+		try {
+			if( base == null ) {
+				return Double.valueOf(s);
+			} else {
+				return Long.valueOf(s,base);
+			}
+		} catch(NumberFormatException e) {}
+		return null;
+	}
+
+	public static boolean matches(String s,String pattern) throws LuanException {
+		Utils.checkNotNull(s);
+		return Pattern.compile(pattern).matcher(s).find();
+	}
+
+	public static LuanTable split(String s,String pattern) throws LuanException {
+		Utils.checkNotNull(s);
+		return new LuanTable(Arrays.asList(s.split(pattern)));
+	}
+
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/Table.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/Table.luan	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,24 @@
+java()
+local TableLuan = require "java:luan.modules.TableLuan"
+
+local M = {}
+
+M.clear = TableLuan.clear
+M.concat = TableLuan.concat
+M.copy = TableLuan.copy
+M.insert = TableLuan.insert
+M.new_property_table = TableLuan.new_property_table
+M.pack = TableLuan.pack
+M.remove = TableLuan.remove
+M.sort = TableLuan.sort
+M.unpack = TableLuan.unpack
+
+
+local Luan = require "luan:Luan.luan"
+local pairs = Luan.pairs
+
+function M.is_empty(t)
+	return pairs(t)() == nil
+end
+
+return M
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/TableLuan.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/TableLuan.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,120 @@
+package luan.modules;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Arrays;
+import luan.Luan;
+import luan.LuanState;
+import luan.LuanTable;
+import luan.LuanFunction;
+import luan.LuanException;
+import luan.LuanRuntimeException;
+import luan.LuanMethod;
+import luan.LuanPropertyMeta;
+
+
+public final class TableLuan {
+
+	public static String concat(LuanState luan,LuanTable list,String sep,Integer i,Integer j) throws LuanException {
+		int first = i==null ? 1 : i;
+		int last = j==null ? list.length(luan) : j;
+		StringBuilder buf = new StringBuilder();
+		for( int k=first; k<=last; k++ ) {
+			Object val = list.get(luan,k);
+			if( val==null )
+				break;
+			if( sep!=null && k > first )
+				buf.append(sep);
+			String s = luan.toString(val);
+			buf.append(s);
+		}
+		return buf.toString();
+	}
+
+	public static void insert(LuanTable list,int pos,Object value) throws LuanException {
+		Utils.checkNotNull(list);
+		if( list.getMetatable() != null )
+			throw new LuanException("can't insert into a table with a metatable");
+		list.rawInsert(pos,value);
+	}
+
+	public static Object remove(LuanTable list,int pos) throws LuanException {
+		if( list.getMetatable() != null )
+			throw new LuanException("can't remove from a table with a metatable");
+		return list.rawRemove(pos);
+	}
+
+	private static interface LessThan {
+		public boolean isLessThan(Object o1,Object o2);
+	}
+
+	public static void sort(final LuanState luan,LuanTable list,final LuanFunction comp) throws LuanException {
+		if( list.getMetatable() != null )
+			throw new LuanException("can't sort a table with a metatable");
+		final LessThan lt;
+		if( comp==null ) {
+			lt = new LessThan() {
+				public boolean isLessThan(Object o1,Object o2) {
+					try {
+						return Luan.isLessThan(luan,o1,o2);
+					} catch(LuanException e) {
+						throw new LuanRuntimeException(e);
+					}
+				}
+			};
+		} else {
+			lt = new LessThan() {
+				public boolean isLessThan(Object o1,Object o2) {
+					try {
+						return Luan.checkBoolean(Luan.first(comp.call(luan,new Object[]{o1,o2})));
+					} catch(LuanException e) {
+						throw new LuanRuntimeException(e);
+					}
+				}
+			};
+		}
+		try {
+			list.rawSort( new Comparator<Object>() {
+				public int compare(Object o1,Object o2) {
+					return lt.isLessThan(o1,o2) ? -1 : lt.isLessThan(o2,o1) ? 1 : 0;
+				}
+			} );
+		} catch(LuanRuntimeException e) {
+			throw (LuanException)e.getCause();
+		}
+	}
+
+	public static LuanTable pack(Object... args) {
+		LuanTable tbl = new LuanTable(Arrays.asList(args));
+		tbl.rawPut( "n", args.length );
+		return tbl;
+	}
+
+	@LuanMethod public static Object[] unpack(LuanState luan,LuanTable tbl,Integer iFrom,Integer iTo) throws LuanException {
+		int from = iFrom!=null ? iFrom : 1;
+		int to = iTo!=null ? iTo : tbl.length(luan);
+		List<Object> list = new ArrayList<Object>();
+		for( int i=from; i<=to; i++ ) {
+			list.add( tbl.get(luan,i) );
+		}
+		return list.toArray();
+	}
+
+	public static LuanTable copy(LuanTable list,Integer from,Integer to) {
+		if( from == null )
+			return new LuanTable(list);
+		if( to == null )
+			to = list.rawLength();
+		return list.rawSubList(from,to);
+	}
+
+	public static LuanTable new_property_table() {
+		return LuanPropertyMeta.INSTANCE.newTable();
+	}
+
+	public static void clear(LuanTable tbl) {
+		tbl.rawClear();
+	}
+
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/Thread.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/Thread.luan	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,10 @@
+java()
+local ThreadLuan = require "java:luan.modules.ThreadLuan"
+
+local M = {}
+
+M.fork = ThreadLuan.fork
+M.schedule = ThreadLuan.schedule
+M.synchronized = ThreadLuan.synchronized_
+
+return M
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/ThreadLuan.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/ThreadLuan.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,71 @@
+package luan.modules;
+
+import java.io.Closeable;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import luan.Luan;
+import luan.LuanState;
+import luan.LuanFunction;
+import luan.LuanTable;
+import luan.LuanException;
+import luan.DeepCloner;
+
+
+public final class ThreadLuan {
+	private static final Executor exec = Executors.newCachedThreadPool();
+	private static final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
+
+	public static void fork(LuanState luan,LuanFunction fn,Object... args) {
+		DeepCloner cloner = new DeepCloner();
+		final LuanState newLuan = (LuanState)cloner.deepClone(luan);
+		final LuanFunction newFn = (LuanFunction)cloner.get(fn);
+		final Object[] newArgs = cloner.deepClone(args);
+		exec.execute(new Runnable(){public void run() {
+			try {
+				newFn.call(newLuan,newArgs);
+			} catch(LuanException e) {
+				e.printStackTrace();
+			}
+		}});
+	}
+
+	public static LuanFunction synchronized_(final LuanState luan,final LuanFunction fn) throws LuanException {
+		Utils.checkNotNull(fn);
+		return new LuanFunction() {
+			@Override public Object call(LuanState ingored,Object[] args) throws LuanException {
+				synchronized(luan) {
+					return fn.call(luan,args);
+				}
+			}
+		};
+	}
+
+	public static void schedule(LuanState luan,long delay,boolean repeat,LuanFunction fn,Object... args) {
+		DeepCloner cloner = new DeepCloner();
+		final LuanState newLuan = (LuanState)cloner.deepClone(luan);
+		final LuanFunction newFn = (LuanFunction)cloner.get(fn);
+		final Object[] newArgs = cloner.deepClone(args);
+		Runnable r = new Runnable(){public void run() {
+			try {
+				newFn.call(newLuan,newArgs);
+			} catch(LuanException e) {
+				e.printStackTrace();
+			}
+		}};
+		final ScheduledFuture sf;
+		if( repeat ) {
+			sf = scheduler.scheduleWithFixedDelay(r,delay,delay,TimeUnit.MILLISECONDS);
+		} else {
+			sf = scheduler.schedule(r,delay,TimeUnit.MILLISECONDS);
+		}
+		final Closeable c = new Closeable(){public void close(){
+			boolean b = sf.cancel(false);
+		}};
+		luan.registry().put(c,c);  // prevent gc
+		luan.onClose(c);
+	}
+
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/Time.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/Time.luan	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,71 @@
+-- incomplete, will add as needed
+
+java()
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local ipairs = Luan.ipairs or error()
+local Table = require "luan:Table.luan"
+local System = require "java:java.lang.System"
+local Calendar = require "java:java.util.Calendar"
+local Date = require "java:java.util.Date"
+local TimeZone = require "java:java.util.TimeZone"
+local SimpleDateFormat = require "java:java.text.SimpleDateFormat"
+
+local M = {}
+
+function M.now()
+	return System.currentTimeMillis()
+end
+
+-- add more as needed
+local fields = {
+	year = Calendar.YEAR;
+	month = Calendar.MONTH;
+	day_of_month = Calendar.DAY_OF_MONTH;
+}
+
+function M.get( time, ... )
+	local cal = Calendar.getInstance()
+	cal.setTimeInMillis(time)
+	local rtn = {}
+	for i, v in ipairs{...} do
+		local fld = fields[v.lower()]
+		fld or error("invalid field: "+v)
+		local n = cal.get(fld)
+		if fld == "month" then
+			n = n + 1
+		end
+		rtn[i] = n
+	end
+	return Table.unpack(rtn)
+end
+
+function M.format(time,pattern)
+	pattern = pattern or "yyyy-MM-dd HH:mm:ss"
+	return SimpleDateFormat.new(pattern).format(Date.new(time))
+end
+
+function M.on( year, month, day, hour, minute, second, millis )
+	month = month - 1
+	local cal = Calendar.getInstance()
+	cal.setLenient(false)
+	cal.set( year, month, day, hour or 0, minute or 0, second or 0 )
+	cal.set( Calendar.MILLISECOND, millis or 0 )
+	return cal.getTimeInMillis()
+end
+
+function M.period( t )
+	local cal = Calendar.getInstance()
+	cal.setTimeZone(TimeZone.getTimeZone("GMT"))
+	local days = t.days or 0
+	days = days + 1
+	cal.set( 1970, 0, days, t.hours or 0, t.minutes or 0, t.seconds or 0 )
+	cal.set( Calendar.MILLISECOND, t.millis or 0 )
+	return cal.getTimeInMillis()
+end
+
+function M.parse( pattern, source )
+	return SimpleDateFormat.new(pattern).parse(source).getTime()
+end
+
+return M
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/Utils.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/Utils.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,200 @@
+package luan.modules;
+
+import java.io.Reader;
+import java.io.IOException;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.File;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.net.MalformedURLException;
+import luan.LuanException;
+import luan.LuanTable;
+import luan.LuanFunction;
+
+
+public final class Utils {
+	private Utils() {}  // never
+
+	static final int bufSize = 8192;
+
+	private static void checkNotNull(Object v,String expected,int pos) throws LuanException {
+		if( v == null )
+			throw new LuanException("bad argument #"+pos+" ("+expected+" expected, got nil)");
+	}
+
+	public static void checkNotNull(String s,int pos) throws LuanException {
+		checkNotNull(s,"string",pos);
+	}
+
+	public static void checkNotNull(String s) throws LuanException {
+		checkNotNull(s,1);
+	}
+
+	public static void checkNotNull(byte[] b,int pos) throws LuanException {
+		checkNotNull(b,"binary",pos);
+	}
+
+	public static void checkNotNull(byte[] b) throws LuanException {
+		checkNotNull(b,1);
+	}
+
+	public static void checkNotNull(LuanTable t,int pos) throws LuanException {
+		checkNotNull(t,"table",pos);
+	}
+
+	public static void checkNotNull(LuanTable t) throws LuanException {
+		checkNotNull(t,1);
+	}
+
+	public static void checkNotNull(Number n,int pos) throws LuanException {
+		checkNotNull(n,"number",pos);
+	}
+
+	public static void checkNotNull(Number n) throws LuanException {
+		checkNotNull(n,1);
+	}
+
+	public static void checkNotNull(LuanFunction fn,int pos) throws LuanException {
+		checkNotNull(fn,"function",pos);
+	}
+
+	public static void checkNotNull(LuanFunction fn) throws LuanException {
+		checkNotNull(fn,1);
+	}
+
+	public static String readAll(Reader in)
+		throws IOException
+	{
+		char[] a = new char[bufSize];
+		StringBuilder buf = new StringBuilder();
+		int n;
+		while( (n=in.read(a)) != -1 ) {
+			buf.append(a,0,n);
+		}
+		return buf.toString();
+	}
+
+	public static void copyAll(InputStream in,OutputStream out)
+		throws IOException
+	{
+		byte[] a = new byte[bufSize];
+		int n;
+		while( (n=in.read(a)) != -1 ) {
+			out.write(a,0,n);
+		}
+	}
+
+	public static byte[] readAll(InputStream in)
+		throws IOException
+	{
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		copyAll(in,out);
+		return out.toByteArray();
+	}
+/*
+	public static boolean exists(File file) {
+		try {
+			return file.exists() && file.getName().equals(file.getCanonicalFile().getName());
+		} catch(IOException e) {
+			throw new RuntimeException(e);
+		}
+	}
+*/
+/*
+	private static File toFile(String path) {
+		if( path.contains("//") )
+			return null;
+		File file = new File(path);
+		return file.exists() ? file : null;
+	}
+
+	private static URL toUrl(String path) {
+		if( path.indexOf(':') == -1 )
+			return null;
+		if( path.startsWith("classpath:") ) {
+			path = path.substring(10);
+			if( path.contains("//") )
+				return null;
+			URL url;
+			if( !path.contains("#") ) {
+				url = ClassLoader.getSystemResource(path);
+			} else {
+				String[] a = path.split("#");
+				url = ClassLoader.getSystemResource(a[0]);
+				if( url==null ) {
+					for( int i=1; i<a.length; i++ ) {
+						url = ClassLoader.getSystemResource(a[0]+"/"+a[i]);
+						if( url != null ) {
+							try {
+								url = new URL(url,".");
+							} catch(MalformedURLException e) {
+								throw new RuntimeException(e);
+							}
+							break;
+						}
+					}
+				}
+			}
+			return url==null ? null : url;
+		}
+		try {
+			return new URL(path);
+		} catch(MalformedURLException e) {}
+		return null;
+	}
+
+	static boolean exists(String path) {
+		return toFile(path)!=null || toUrl(path)!=null;
+	}
+*/
+
+
+
+/*	replace by uri"os:..."
+
+	// process
+
+	public static class ProcessException extends IOException {
+		private ProcessException(String msg) {
+			super(msg);
+		}
+	}
+
+	public static void checkProcess(Process proc)
+		throws IOException
+	{
+		try {
+			proc.waitFor();
+		} catch(InterruptedException e) {
+			throw new RuntimeException(e);
+		}
+		int exitVal = proc.exitValue();
+		if( exitVal != 0 ) {
+			Reader err = new InputStreamReader(proc.getErrorStream());
+			String error = readAll(err);
+			err.close();
+			throw new ProcessException(error);
+		}
+	}
+
+	public static String getOutput(Process proc)
+		throws IOException
+	{
+		Reader in = new InputStreamReader(proc.getInputStream());
+		String s = readAll(in);
+		in.close();
+		return s;
+	}
+
+	public static String execProcess(String... cmd)
+		throws IOException
+	{
+		Process proc = Runtime.getRuntime().exec(cmd);
+		String s = getOutput(proc);
+		checkProcess(proc);
+		return s;
+	}
+*/
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/Which_mod.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/Which_mod.luan	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,53 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local ipairs = Luan.ipairs or error()
+local pairs = Luan.pairs or error()
+local type = Luan.type or error()
+local String = require "luan:String.luan"
+local literal = String.literal or error()
+local matches = String.matches or error()
+local Io = require "luan:Io.luan"
+local print = Io.print or error()
+
+
+local M = {}
+
+M.uris = {
+	"luan:Luan"
+	"luan:Binary"
+	"luan:Html"
+	"luan:Io"
+	"luan:Math"
+	"luan:Package"
+	"luan:String"
+	"luan:Table"
+	"luan:Thread"
+	"luan:Time"
+	"luan:host/Hosting"
+	"luan:http/Http"
+	"luan:http/Server"
+	"luan:lucene/Lucene"
+	"luan:lucene/Versioning"
+	"luan:mail/Mail"
+	"luan:logging/Logging"
+	"luan:stripe/Stripe"
+}
+
+function M.which(name)
+	local ptn = "[:./]"..literal(name).."$"
+	for _, uri in ipairs(M.uris) do
+		local mod = require(uri)
+		if matches(uri,ptn) then
+			print(uri)
+		end
+		if type(mod) == "table" then
+			for key in pairs(mod) do
+				if key == name then
+					print(uri.." "..key)
+				end
+			end
+		end
+	end
+end
+
+return M
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/host/Hosting.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/host/Hosting.luan	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,97 @@
+-- Hosting
+
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local ipairs = Luan.ipairs or error()
+local pairs = Luan.pairs or error()
+local set_metatable = Luan.set_metatable or error()
+local Io = require "luan:Io.luan"
+local print = Io.print or error()
+local Rpc = require "luan:Rpc.luan"
+local String = require "luan:String.luan"
+local matches = String.matches or error()
+
+
+local M = {}
+
+
+function M.push(domain,password,dir)
+	local my_dir = Io.uri("file:"..dir)
+	my_dir.exists() or error("directory '"..dir.."' not found")
+	my_dir.is_directory() or error("'"..dir.."' is not a directory")
+	local host = Rpc.remote(domain)
+	local tree = host.get(domain,password)
+	if tree == nil then
+		print("creating "..domain)
+		tree = host.create(domain,password)
+	end
+
+	local function process(there_parent,there,here)
+		if here.is_file() then
+			if there == nil or there.last_modified < here.last_modified() then
+				print("copying "..here.to_string())
+				host.copy_file(domain,password,there_parent.path,here.name(),here.read_binary())
+			end
+		elseif here.is_directory() then
+			if here.name() == "local" then
+				return
+			end
+			if there == nil then
+				there = host.mkdir(domain,password,there_parent.path,here.name())
+			end
+			for _, here_child in ipairs(here.children()) do
+				local name = here_child.name()
+				if not matches(name,[[^\.]]) then
+					process(there,there.children[name],here_child)
+					there.children[name] = nil
+				end
+			end
+			for _, there_child in pairs(there.children) do
+				if host.delete_unused(domain,password,there_child.path)==true then   -- remove ==true later
+					print("deleted "..there_child.name)
+				end
+			end
+		else
+			error "not file or dir"
+		end
+	end
+
+	process( nil, tree, my_dir )
+
+	host.update_handler(domain,password)
+end
+
+function M.delete(domain,password)
+	local host = Rpc.remote(domain)
+	host.delete(domain,password)
+end
+
+function M.exists(domain)
+	local host = Rpc.remote(domain)
+	return host.exists(domain)
+end
+
+function M.change_domain(old_domain,new_domain,password)
+	local host = Rpc.remote(new_domain)
+	return host.change_domain(old_domain,new_domain,password)
+end
+
+function M.change_password(domain,old_password,new_password)
+	local host = Rpc.remote(domain)
+	return host.change_password(domain,old_password,new_password)
+end
+
+function M.caller(domain)
+	local host = Rpc.remote(domain)
+	local mt = {}
+	function mt.__index(_,key)
+		return function(...)
+			return host.call(domain,key,...)
+		end
+	end
+	local t = {}
+	set_metatable(t,mt)
+	return t
+end
+
+return M
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/host/backup.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/host/backup.luan	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,18 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Io = require "luan:Io.luan"
+local print = Io.print or error()
+local uri = Io.uri or error()
+local Hosting = require "luan:host/Hosting.luan"
+
+if #{...} ~= 2 then
+	Io.stderr.write "usage: luan luan:host/backup.luan domain password\n"
+	return
+end
+
+local domain, password = ...
+
+local zip = Hosting.caller(domain).lucene_backup(password)
+uri("file:backup.zip").write(zip)
+
+print("backed up lucene from "..domain.." to backup.zip")
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/host/delete.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/host/delete.luan	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,12 @@
+local Io = require "luan:Io.luan"
+local print = Io.print
+local Hosting = require "luan:host/Hosting.luan"
+
+if #{...} ~= 2 then
+	Io.stderr.write "usage: luan luan:host/delete.luan domain password\n"
+	return
+end
+
+Hosting.delete(...)
+
+print("deleted "..(...))
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/host/push.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/host/push.luan	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,12 @@
+local Io = require "luan:Io.luan"
+local print = Io.print
+local Hosting = require "luan:host/Hosting.luan"
+
+if #{...} ~= 3 then
+	Io.stderr.write "usage: luan luan:host/push.luan domain password dir\n"
+	return
+end
+
+Hosting.push(...)
+
+print("done with "..(...))
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/host/restore.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/host/restore.luan	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,19 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local Io = require "luan:Io.luan"
+local print = Io.print or error()
+local uri = Io.uri or error()
+local Hosting = require "luan:host/Hosting.luan"
+
+if #{...} ~= 2 then
+	Io.stderr.write "usage: luan luan:host/restore.luan domain password\n"
+	return
+end
+
+local domain, password = ...
+
+local zip_file = uri("file:backup.zip")
+zip_file.exists() or error "backup.zip not found"
+Hosting.caller(domain).lucene_restore(password,zip_file)
+
+print("restored lucene from backup.zip to "..domain)
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/http/AuthenticationHandler.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/http/AuthenticationHandler.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,53 @@
+package luan.modules.http;
+
+import java.io.IOException;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.util.B64Code;
+
+
+public class AuthenticationHandler extends AbstractHandler {
+	private final String path;
+	private String password = "password";
+
+	public AuthenticationHandler(String path) {
+		this.path = path;
+	}
+
+	public void setPassword(String password) {
+		this.password = password;
+	}
+
+	public void handle(String target,Request baseRequest,HttpServletRequest request,HttpServletResponse response) 
+		throws IOException
+	{
+		if( !target.startsWith(path) )
+			return;
+		String pwd = getPassword(request);
+		if( password.equals(pwd) )
+			return;
+		response.setHeader("WWW-Authenticate","Basic realm=\""+path+"\"");
+		response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+		baseRequest.setHandled(true);
+	}
+
+	private static String getPassword(HttpServletRequest request) {
+		String auth = request.getHeader("Authorization");
+		if( auth==null )
+			return null;
+		String[] a = auth.split(" +");
+		if( a.length != 2 )
+			throw new RuntimeException("auth = "+auth);
+		if( !a[0].equals("Basic") )
+			throw new RuntimeException("auth = "+auth);
+		auth = new String(B64Code.decode(a[1]));
+		a = auth.split(":");
+		if( a.length != 2 )
+			throw new RuntimeException("auth = "+auth);
+		return a[1];
+	}
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/http/Dump_mod.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/http/Dump_mod.luan	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,50 @@
+local Luan = require "luan:Luan.luan"
+local pairs = Luan.pairs
+local ipairs = Luan.ipairs
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+java()
+local HttpServicer = require "java:luan.modules.http.HttpServicer"
+
+local M = {}
+
+local to_http_header_name = HttpServicer.toHttpHeaderName
+M.to_http_header_name = to_http_header_name
+
+function M.respond()
+	Http.response.header.content_type = "text/plain"
+	Io.stdout = Http.response.text_writer()
+
+	local method = Http.request.method
+	local path = Http.request.path
+	local query = Http.request.query_string()
+	if method ~= "POST" and query ~= nil then
+		path = path.."?"..query
+	end
+%>
+<%=method%> <%=path%> <%=Http.request.protocol%> 
+<%
+	M.dump_headers(Http.request.headers)
+%>
+
+<%
+	if method == "POST" and query ~= nil then
+%>
+<%=query%>
+<%
+	end
+end
+
+
+function M.dump_headers(headers)
+	for name, values in pairs(headers) do
+		local header_name = to_http_header_name(name)
+		for _, value in ipairs(values) do
+%>
+<%=header_name%>: <%=value%>
+<%
+		end
+	end
+end
+
+return M
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/http/Http.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/http/Http.luan	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,170 @@
+java()
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local ipairs = Luan.ipairs or error()
+local pairs = Luan.pairs or error()
+local set_metatable = Luan.set_metatable or error()
+local Io = require "luan:Io.luan"
+local Html = require "luan:Html.luan"
+local url_encode = Html.url_encode or error()
+local Table = require "luan:Table.luan"
+local clear = Table.clear or error()
+local Package = require "luan:Package.luan"
+local String = require "luan:String.luan"
+local matches = String.matches or error()
+local HttpServicer = require "java:luan.modules.http.HttpServicer"
+local IoLuan = require "java:luan.modules.IoLuan"
+
+local M = {}
+
+local singular_metatable = {}
+
+function singular_metatable.__index(table,key)
+	local list = table.__plural[key]
+	return list and list[1]
+end
+
+function singular_metatable.__new_index(table,key,value)
+	table.__plural[key] = value and {value}
+end
+
+function singular_metatable.__pairs(table)
+	local iter = pairs(table.__plural)
+	return function()
+		local key, value = iter()
+		return key, value and value[1]
+	end
+end
+
+local function sent_error()
+	error "headers are not accessible after you start writing content"
+end
+
+local sent_error_metatable = { __index=sent_error, __new_index=sent_error }
+
+function M.sent_headers(headers)
+	clear(headers)
+	set_metatable(headers,sent_error_metatable)
+end
+
+
+local function new_common(this)
+	this = this or {}
+	this.headers = {}
+	this.header = {__plural=this.headers}
+	set_metatable(this.header,singular_metatable)
+	return this
+end
+
+
+function M.new_request(this)
+	this = new_common(this)
+	this.method = "GET"  -- default
+	-- this.path
+	-- this.protocol
+	this.scheme = "http"  -- default
+	this.port = 80  -- default
+	this.parameters = {}
+	this.parameter = {__plural=this.parameters}
+	set_metatable(this.parameter,singular_metatable)
+	this.cookie = {}
+
+	function this.query_string()
+		local string_uri = Io.uri "string:"
+		local out = string_uri.text_writer()
+		local and_char = ""
+		for name, values in pairs(this.parameters) do
+			for _, value in ipairs(values) do
+				out.write( and_char, url_encode(name), "=", url_encode(value) )
+				and_char = "&"
+			end
+		end
+		out.close()
+		local s = string_uri.read_text()
+		return s ~= "" and s or nil
+	end
+
+	function this.url()
+		local url = this.scheme.."://"..this.header.host..this.path
+		if this.method ~= "POST" then
+			local query = this.query_string()
+			if query ~= nil then
+				url = url.."?"..query
+			end
+		end
+		return url
+	end
+
+	return this
+end
+
+local STATUS = {
+	OK = 200;
+	-- add more as needed
+}
+M.STATUS = STATUS
+
+function M.new_response(this)
+	this = new_common(this)
+	this.status = STATUS.OK
+	if this.java ~= nil then
+		this.send_redirect = this.java.sendRedirect
+		this.send_error = this.java.sendError
+
+		function this.set_cookie(name,value,is_persistent,domain)
+			HttpServicer.setCookie(M.request.java,this.java,name,value,is_persistent,domain)
+		end
+
+		function this.remove_cookie(name,domain)
+			HttpServicer.removeCookie(M.request.java,this.java,name,domain)
+		end
+
+		function this.set()
+			HttpServicer.setResponse(this,this.java)
+			M.sent_headers(this.headers)
+		end
+
+		function this.text_writer()
+			this.set()
+			return IoLuan.textWriter(this.java.getWriter())
+		end
+
+		function this.binary_writer()
+			this.set()
+			return IoLuan.binaryWriter(this.java.getOutputStream())
+		end
+
+		function this.reset()
+			this.java.reset()
+			set_metatable(this.headers,nil)
+		end
+	end
+	return this
+end
+
+-- request = new_request{}  -- filled in by HttpServicer
+-- response = new_response{}  -- filled in by HttpServicer
+
+
+M.per_session_pages = {}
+
+function M.per_session(page)
+	M.per_session_pages[page] = true
+end
+
+function M.clear_session()
+	M.request.java.getSession().removeAttribute("luan")
+end
+
+
+function M.uncache_site()
+	for k in pairs(Table.copy(Package.loaded)) do
+		if matches(k,"^site:") then
+			Package.loaded[k] = nil
+		end
+	end
+end
+
+M.run_later = HttpServicer.run_later
+
+return M
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/http/HttpServicer.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/http/HttpServicer.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,302 @@
+package luan.modules.http;
+
+import java.io.InputStream;
+import java.io.BufferedInputStream;
+import java.io.PrintWriter;
+import java.io.IOException;
+import java.util.Map;
+import java.util.Set;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.Enumeration;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.ServletException;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import javax.servlet.http.Part;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.eclipse.jetty.util.MultiPartInputStream;
+import luan.Luan;
+import luan.LuanState;
+import luan.LuanFunction;
+import luan.LuanException;
+import luan.LuanTable;
+import luan.LuanMeta;
+import luan.LuanPropertyMeta;
+import luan.DeepCloner;
+import luan.modules.PackageLuan;
+import luan.modules.IoLuan;
+import luan.modules.TableLuan;
+import luan.modules.Utils;
+import luan.modules.url.LuanUrl;
+
+
+public final class HttpServicer {
+	private static final Logger logger = LoggerFactory.getLogger(HttpServicer.class);
+
+	public static boolean service(LuanState luan,HttpServletRequest request,HttpServletResponse response,String modName)
+		throws LuanException
+	{
+		LuanFunction fn;
+		synchronized(luan) {
+			LuanTable module = (LuanTable)PackageLuan.require(luan,"luan:http/Http.luan");
+			LuanTable per_session_pages = (LuanTable)module.rawGet("per_session_pages");
+			Object mod = PackageLuan.load(luan,modName);
+			if( mod==null )
+				return false;
+			if( !(mod instanceof LuanFunction) )
+				throw new LuanException( "module '"+modName+"' must return a function" );
+			if( Boolean.TRUE.equals(per_session_pages.rawGet(mod)) ) {
+				HttpSession session = request.getSession();
+				LuanState sessionLuan = (LuanState)session.getAttribute("luan");
+				if( sessionLuan!=null ) {
+					luan = sessionLuan;
+				} else {
+					DeepCloner cloner = new DeepCloner();
+					luan = (LuanState)cloner.deepClone(luan);
+					session.setAttribute("luan",luan);
+				}
+				fn = (LuanFunction)PackageLuan.require(luan,modName);
+			} else {
+				DeepCloner cloner = new DeepCloner();
+				luan = (LuanState)cloner.deepClone(luan);
+				fn = (LuanFunction)cloner.get(mod);
+			}
+		}
+
+		LuanTable module = (LuanTable)PackageLuan.require(luan,"luan:http/Http.luan");
+
+		// request
+		LuanFunction newRequestFn = (LuanFunction)module.rawGet("new_request");
+		LuanTable requestTbl = (LuanTable)newRequestFn.call(luan);
+		module.rawPut("request",requestTbl);
+		requestTbl.rawPut("java",request);
+		requestTbl.rawPut("method",request.getMethod());
+		requestTbl.rawPut("path",request.getRequestURI());
+		requestTbl.rawPut("protocol",request.getProtocol());
+		requestTbl.rawPut("scheme",request.getScheme());
+		requestTbl.rawPut("port",request.getServerPort());
+
+		LuanTable headersTbl = (LuanTable)requestTbl.rawGet("headers");
+		for( Enumeration<String> enKeys = request.getHeaderNames(); enKeys.hasMoreElements(); ) {
+			String key = enKeys.nextElement();
+			LuanTable values = new LuanTable();
+			for( Enumeration<String> en = request.getHeaders(key); en.hasMoreElements(); ) {
+				values.rawPut(values.rawLength()+1,en.nextElement());
+			}
+			key = toLuanHeaderName(key);
+			headersTbl.rawPut(key,values);
+		}
+
+		LuanTable parametersTbl = (LuanTable)requestTbl.rawGet("parameters");
+		String contentType = request.getContentType();
+		if( contentType==null || !contentType.startsWith("multipart/form-data") ) {
+			for( Map.Entry<String,String[]> entry : request.getParameterMap().entrySet() ) {
+				parametersTbl.rawPut(entry.getKey(),new LuanTable(Arrays.asList(entry.getValue())));
+			}
+		} else {  // multipart
+			try {
+				InputStream in = new BufferedInputStream(request.getInputStream());
+				final MultiPartInputStream mpis = new MultiPartInputStream(in,contentType,null,null);
+				mpis.setDeleteOnExit(true);
+				for( Part p : mpis.getParts() ) {
+					final MultiPartInputStream.MultiPart part = (MultiPartInputStream.MultiPart)p;
+					String name = part.getName();
+/*
+System.out.println("name = "+name);
+System.out.println("getContentType = "+part.getContentType());
+System.out.println("getHeaderNames = "+part.getHeaderNames());
+System.out.println("content-disposition = "+part.getHeader("content-disposition"));
+System.out.println();
+*/
+					Object value;
+					String filename = part.getContentDispositionFilename();
+					if( filename == null ) {
+						value = new String(part.getBytes());
+					} else {
+						LuanTable partTbl = LuanPropertyMeta.INSTANCE.newTable();
+						partTbl.rawPut("filename",filename);
+						partTbl.rawPut("content_type",part.getContentType());
+						LuanPropertyMeta.INSTANCE.getters(partTbl).rawPut( "content", new LuanFunction() {
+							@Override public Object call(LuanState luan,Object[] args) throws LuanException {
+								try {
+									InputStream in = part.getInputStream();
+									byte[] content = Utils.readAll(in);
+									in.close();
+									return content;
+								} catch(IOException e) {
+									throw new RuntimeException(e);
+								}
+							}
+						} );
+						value = partTbl;
+					}
+					LuanTable list = (LuanTable)parametersTbl.rawGet(name);
+					if( list == null ) {
+						list = new LuanTable();
+						parametersTbl.rawPut(name,list);
+					}
+					list.rawPut(parametersTbl.rawLength()+1,value);
+				}
+			} catch(IOException e) {
+				throw new RuntimeException(e);
+			} catch(ServletException e) {
+				throw new RuntimeException(e);
+			}
+		}
+
+		LuanTable cookieTbl = (LuanTable)requestTbl.rawGet("cookie");
+		for( Cookie cookie : request.getCookies() ) {
+			cookieTbl.rawPut( cookie.getName(), unescape(cookie.getValue()) );
+		}
+
+
+		// response
+		LuanTable responseTbl = new LuanTable();
+		responseTbl.rawPut("java",response);
+		LuanFunction newResponseFn = (LuanFunction)module.rawGet("new_response");
+		newResponseFn.call( luan, new Object[]{responseTbl} );
+		module.rawPut("response",responseTbl);
+
+		fn.call(luan);
+		handle_run_later(luan);
+		return true;
+	}
+
+	public static void setResponse(LuanTable responseTbl,HttpServletResponse response) throws LuanException {
+		int status = Luan.asInteger(responseTbl.rawGet("status"));
+		response.setStatus(status);
+		LuanTable responseHeaders = (LuanTable)responseTbl.rawGet("headers");
+		for( Map.Entry<Object,Object> entry : responseHeaders.rawIterable() ) {
+			String name = (String)entry.getKey();
+			name = toHttpHeaderName(name);
+			LuanTable values = (LuanTable)entry.getValue();
+			for( Object value : values.asList() ) {
+				if( value instanceof String ) {
+					response.setHeader(name,(String)value);
+					continue;
+				}
+				Integer i = Luan.asInteger(value);
+				if( i != null ) {
+					response.setIntHeader(name,i);
+					continue;
+				}
+				throw new IllegalArgumentException("value must be string or integer for headers table");
+			}
+		}
+	}
+
+
+
+	// static utils
+
+	public static String toLuanHeaderName(String httpName) {
+		return httpName.toLowerCase().replace('-','_');
+	}
+
+	public static String toHttpHeaderName(String luanName) {
+/*
+		StringBuilder buf = new StringBuilder();
+		boolean capitalize = true;
+		char[] a = luanName.toCharArray();
+		for( int i=0; i<a.length; i++ ) {
+			char c = a[i];
+			if( c == '_' ) {
+				a[i] = '-';
+				capitalize = true;
+			} else if( capitalize ) {
+				a[i] = Character.toUpperCase(c);
+				capitalize = false;
+			}
+		}
+		return String.valueOf(a);
+*/
+		return LuanUrl.toHttpHeaderName(luanName);
+	}
+
+	private static String escape(String value) {
+		return value.replaceAll(";", "%3B");
+	}
+
+	private static String unescape(String value) {
+		return value.replaceAll("%3B", ";");
+	}
+
+	private static Cookie getCookie(HttpServletRequest request,String name) {
+		Cookie[] cookies = request.getCookies();
+		if( cookies == null )
+			return null;
+		for (Cookie cookie : cookies) {
+			if (cookie.getName().equals(name))
+				return cookie;
+		}
+		return null;
+	}
+
+	public static void setCookie(HttpServletRequest request,HttpServletResponse response,String name,String value,boolean isPersistent, String domain) {
+		Cookie cookie = getCookie(request,name);
+		if( cookie==null || !cookie.getValue().equals(value) ) {
+			cookie = new Cookie(name, escape(value));
+			cookie.setPath("/");
+			if (domain != null && domain.length() > 0)
+				cookie.setDomain(domain);
+			if( isPersistent )
+				cookie.setMaxAge(10000000);
+			response.addCookie(cookie);
+		}
+	}
+
+	public static void removeCookie(HttpServletRequest request,
+									HttpServletResponse response,
+									String name,
+									String domain
+	) {
+		Cookie cookie = getCookie(request, name);
+		if(cookie != null) {
+			Cookie delCookie = new Cookie(name, "delete");
+			delCookie.setPath("/");
+			delCookie.setMaxAge(0);
+			if (domain != null && domain.length() > 0)
+				delCookie.setDomain(domain);
+			response.addCookie(delCookie);
+		}
+	}
+
+
+
+	private static String RUN_LATER_KEY = "Http.run_later";
+	private static final Executor exec = Executors.newSingleThreadExecutor();
+
+	public static void run_later(final LuanState luan,final LuanFunction fn,final Object... args) {
+		List list = (List)luan.registry().get(RUN_LATER_KEY);
+		if( list == null ) {
+			list = new ArrayList();
+			luan.registry().put(RUN_LATER_KEY,list);
+		}
+		list.add(
+			new Runnable(){public void run() {
+				try {
+					fn.call(luan,args);
+				} catch(LuanException e) {
+					e.printStackTrace();
+				}
+			}}
+		);
+	}
+
+	private static void handle_run_later(LuanState luan) {
+		List list = (List)luan.registry().get(RUN_LATER_KEY);
+		if( list==null )
+			return;
+		for( Object obj : list ) {
+			exec.execute((Runnable)obj);
+		}
+	}
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/http/Http_test.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/http/Http_test.luan	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,75 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local set_metatable = Luan.set_metatable or error()
+local try = Luan.try or error()
+local Package = require "luan:Package.luan"
+local Io = require "luan:Io.luan"
+local String = require "luan:String.luan"
+local matches = String.matches or error()
+local Http = require "luan:http/Http.luan"
+
+
+local M = {}
+
+M.welcome_file = "index.html"
+M.cookie = {}
+
+function M.get_page(path)
+	Http.request.path = path
+	if M.welcome_file ~= nil and matches(path,"/$") then
+		path = path .. M.welcome_file
+	end
+	local old_out = Io.stdout
+	try {
+		function()
+			local mod = Package.load("site:"..path..".luan")
+			if mod ~= nil then
+				mod()
+			else
+				local not_found = Package.load("site:/not_found.luan")
+				not_found or error(path.." not found")
+				not_found()
+			end
+			M.text_writer.close()
+		end
+		finally = function()
+			Io.stdout = old_out
+		end
+	}
+	return M.result.read_text()
+end
+
+function M.init()
+	Http.request = Http.new_request{}
+	Http.request.cookie = M.cookie
+
+	Http.response = Http.new_response{
+
+		text_writer = function()
+			Http.sent_headers(Http.response.headers)
+			M.result = Io.uri "string:"
+			M.text_writer = M.result.text_writer()
+			return M.text_writer
+		end
+
+		set_cookie = function(name,value)
+			M.cookie[name] = value
+		end
+
+		remove_cookie = function(name)
+			M.cookie[name] = nil
+		end
+
+		send_redirect = function(url)
+			Http.response.redirect = url
+		end
+
+		send_error = function(code)
+			error("sent error "..code)
+		end
+
+	}
+
+end
+
+return M
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/http/LuanHandler.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/http/LuanHandler.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,83 @@
+package luan.modules.http;
+
+import java.io.IOException;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import luan.LuanState;
+import luan.LuanTable;
+import luan.LuanFunction;
+import luan.DeepCloner;
+import luan.LuanException;
+import luan.modules.PackageLuan;
+
+
+public class LuanHandler extends AbstractHandler {
+	private final LuanState luan;
+	private final Logger logger;
+	private String welcomeFile = "index.html";
+
+	public LuanHandler(LuanState luan,String loggerRoot) {
+		this.luan = luan;
+		if( loggerRoot==null )
+			loggerRoot = "";
+		logger = LoggerFactory.getLogger(loggerRoot+LuanHandler.class.getName());
+	}
+
+	@Override public void handle(String target,Request baseRequest,HttpServletRequest request,HttpServletResponse response) 
+		throws IOException
+	{
+		if( target.endsWith("/") )
+			target += welcomeFile;
+		try {
+			if( !HttpServicer.service(luan,request,response,"site:"+target+".luan") )
+				return;
+		} catch(LuanException e) {
+			String err = e.getFullMessage();
+			logger.error(err);
+			response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,err);
+		}
+		baseRequest.setHandled(true);
+	}
+
+	public void setWelcomeFile(String welcomeFile) {
+		this.welcomeFile = welcomeFile;
+	}
+
+	@Override protected void doStop() throws Exception {
+		synchronized(luan) {
+			luan.close();
+		}
+//System.out.println("qqqqqqqqqqqqqqqqqqqq doStop "+this);
+		super.doStop();
+	}
+/*
+	@Override public void destroy() {
+System.out.println("qqqqqqqqqqqqqqqqqqqq destroy "+this);
+		super.destroy();
+	}
+*/
+
+	public Object call_rpc(String fnName,Object... args) throws LuanException {
+		return callRpc(luan,fnName,args);
+	}
+
+	public static Object callRpc(LuanState luan,String fnName,Object... args) throws LuanException {
+		synchronized(luan) {
+			DeepCloner cloner = new DeepCloner();
+			luan = (LuanState)cloner.deepClone(luan);
+		}
+		LuanTable rpc = (LuanTable)PackageLuan.require(luan,"luan:Rpc.luan");
+		LuanTable fns = (LuanTable)rpc.get(luan,"functions");
+		LuanFunction fn = (LuanFunction)fns.get(luan,fnName);
+		if( fn == null )
+			throw new LuanException( "function not found: " + fnName );
+		return fn.call(luan,args);
+	}
+
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/http/NotFound.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/http/NotFound.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,22 @@
+package luan.modules.http;
+
+import java.io.IOException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jetty.server.Request;
+import luan.LuanState;
+
+
+public class NotFound extends LuanHandler {
+
+	public NotFound(LuanState luan,String loggerRoot) {
+		super(luan,loggerRoot);
+	}
+
+	@Override public void handle(String target,Request baseRequest,HttpServletRequest request,HttpServletResponse response) 
+		throws IOException
+	{
+		super.handle("/not_found",baseRequest,request,response);
+	}
+
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/http/Server.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/http/Server.luan	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,118 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local String = require "luan:String.luan"
+local gsub = String.gsub or error()
+local matches = String.matches or error()
+local Io = require "luan:Io.luan"
+local Package = require "luan:Package.luan"
+local Rpc = require "luan:Rpc.luan"
+local Thread = require "luan:Thread.luan"
+local Http = require "luan:http/Http.luan"
+require "luan:logging/init.luan"  -- initialize logging
+local Logging = require "luan:logging/Logging.luan"
+local logger = Logging.logger "http/Server"
+
+java()
+local Server = require "java:org.eclipse.jetty.server.Server"
+local NCSARequestLog = require "java:org.eclipse.jetty.server.NCSARequestLog"
+local DefaultHandler = require "java:org.eclipse.jetty.server.handler.DefaultHandler"
+local HandlerList = require "java:org.eclipse.jetty.server.handler.HandlerList"
+local HandlerCollection = require "java:org.eclipse.jetty.server.handler.HandlerCollection"
+local ResourceHandler = require "java:org.eclipse.jetty.server.handler.ResourceHandler"
+local RequestLogHandler = require "java:org.eclipse.jetty.server.handler.RequestLogHandler"
+local ContextHandler = require "java:org.eclipse.jetty.server.handler.ContextHandler"
+local GzipHandler = require "java:org.eclipse.jetty.server.handler.GzipHandler"
+local HandlerWrapper = require "java:org.eclipse.jetty.server.handler.HandlerWrapper"
+local SessionHandler = require "java:org.eclipse.jetty.server.session.SessionHandler"
+local AuthenticationHandler = require "java:luan.modules.http.AuthenticationHandler"
+local LuanHandler = require "java:luan.modules.http.LuanHandler"
+local NotFound = require "java:luan.modules.http.NotFound"
+
+local M = {}
+
+M.port = 8080
+
+M.welcome_file = "index.html"
+
+
+M.authentication_handler = AuthenticationHandler.new("/private/")
+
+M.luan_handler = LuanHandler.new()
+
+M.resource_handler = ResourceHandler.new()
+M.resource_handler.setDirectoriesListed(true)
+
+M.handlers = HandlerList.new()
+M.handlers.setHandlers { M.authentication_handler, M.luan_handler, M.resource_handler }
+
+function M.add_folder(context,dir)
+	local rh = ResourceHandler.new()
+	rh.setResourceBase(dir)
+	rh.setDirectoriesListed(true)
+	local ch = ContextHandler.new(context)
+	ch.setHandler(rh)
+	M.handlers.addHandler(ch)
+	return rh
+end
+
+M.handler_wrapper = HandlerWrapper.new()
+M.handler_wrapper.setHandler(M.handlers)
+
+function M.zip()
+	local h = GzipHandler.new()
+	h.setHandler(M.handler_wrapper.getHandler())
+	M.handler_wrapper.setHandler(h)
+end
+
+M.log = NCSARequestLog.new()
+M.log.setExtended(false)
+M.log_handler = RequestLogHandler.new()
+M.log_handler.setRequestLog(M.log)
+
+function M.set_log_file(file_name)
+	M.log.setFilename(file_name)
+end
+
+local hc = HandlerCollection.new()
+hc.setHandlers { SessionHandler.new(), M.handler_wrapper, DefaultHandler.new(), M.log_handler }
+
+
+function M.init(dir)
+	dir = gsub(dir,"/$","")  -- remove trailing '/' if any
+	Http.dir = dir
+	function Io.schemes.site(path)
+		return Io.uri( dir..path )
+	end
+	M.authentication_handler.setPassword(Io.password)
+	local base = dir
+	if matches(base,"^classpath:") then
+		base = dir.."#"..M.welcome_file.."#"..M.welcome_file..".luan"
+	end
+	M.resource_handler.setResourceBase(Io.uri(base).to_string())
+	M.resource_handler.setWelcomeFiles {M.welcome_file}
+	M.luan_handler.setWelcomeFile(M.welcome_file)
+	M.handlers.addHandler(NotFound.new())
+	M.server = Server.new(M.port)
+	M.server.setHandler(hc)
+	Package.load("site:/init.luan")
+end
+
+function M.start()
+	M.server.start()
+end
+
+function M.start_rpc()
+	function Rpc.functions.call(domain,fn_name,...)
+		return M.luan_handler.call_rpc(fn_name,...)
+	end
+
+	Thread.fork(Rpc.serve)
+end
+
+function M.serve(dir)
+	M.init(dir)
+	M.start()
+	M.start_rpc()
+end
+
+return M
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/http/Shell_mod.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/http/Shell_mod.luan	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,108 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local ipairs = Luan.ipairs or error()
+local load = Luan.load or error()
+local try = Luan.try or error()
+local Io = require "luan:Io.luan"
+local print = Io.print or error()
+local Http = require "luan:http/Http.luan"
+
+
+local M = {}
+
+local history = {}
+M.env = {}
+
+function M.respond()
+	if Http.request.parameter.clear ~= nil then
+		Http.clear_session()
+		Http.response.send_redirect(Http.request.path)  -- reload page
+		return
+	else
+		local cmd = Http.request.parameter.cmd
+		if cmd ~= nil then
+			Io.stdout = {}
+			function Io.stdout.write(...)
+				for v in Luan.values(...) do
+					history[#history+1] = v
+				end
+			end
+			print( "% "..cmd )
+			try {
+				function()
+					local line
+					try {
+						function()
+							line = load("return "..cmd,"<web_shell>",M.env)
+						end
+						catch = function(e)
+							line = load(cmd,"<web_shell>",M.env)
+						end
+					}
+					print( line() )
+				end
+				catch = function(e)
+					Io.print_to(Io.stderr,e)
+					print(e)
+				end
+			}
+		end
+	end
+
+	Io.stdout = Http.response.text_writer()
+%>
+<html>
+	<head>
+		<title>Luan Shell</title>
+		<style>
+			body {
+				font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+				margin: 2em 5% 0 5%;
+			}
+			pre {
+				font: inherit;
+			}
+			input[type="text"] {
+				font: inherit;
+				padding: .5em .8em;
+				border-radius: 8px;
+				border-style: groove;
+			}
+			input[type="text"]:focus {
+				border-color: #66afe9;
+				outline: none;
+			}
+			input[type="submit"] {
+				color: white;
+				background: #337ab7;
+				border-color: #337ab7;
+				font: inherit;
+				padding: .5em;
+				border-radius: 4px;
+			}
+			input[type="submit"]:hover {
+				background: #236aa7 !important;
+			}
+		</style>
+	</head>
+	<body>
+		<h2>Luan Shell</h2>
+		<p>This is a command shell.  Enter commands below.</p>
+		<pre><%
+		for _,v in ipairs(history) do
+			Io.stdout.write(v)
+		end
+		%></pre>
+		<form name='form0' method='post'>
+			% <input type="text" name='cmd' size="80" autofocus>
+			<input type="submit" value="run">
+			<input type="submit" name="clear" value="clear">
+		</form>
+	</body>
+</html>
+<%
+end
+
+Http.per_session(M.respond)
+
+return M
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/http/dump.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/http/dump.luan	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,1 @@
+return require("luan:http/Dump_mod.luan").respond
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/http/run.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/http/run.luan	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,106 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local load = Luan.load or error()
+local try = Luan.try or error()
+local Io = require "luan:Io.luan"
+local print = Io.print or error()
+local String = require "luan:String.luan"
+local gmatch = String.gmatch or error()
+local Http = require "luan:http/Http.luan"
+
+
+local function lines(s)
+	local matcher = gmatch(s,"([^\n]*)\n|([^\n])+$")
+	return function()
+		local m1, m2 = matcher()
+		return m1 or m2
+	end
+end
+
+local function print_with_line_numbers(s)
+	local i = 1
+	for line in lines(s) do
+		print(i,line)
+		i = i + 1
+	end
+end
+
+local function form() %>
+<html>
+	<head>
+		<title>Run Luan Code</title>
+		<style>
+			body {
+				font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+				text-align: center;
+				margin-top: 1em;
+			}
+			h2 {
+				margin-bottom: .3em;
+				font-weight: normal;
+			}
+			textarea {
+				font: inherit;
+				border-radius: 4px;
+				padding: .5em .8em;
+			}
+			input[type="submit"] {
+				margin-top: .3em;
+				color: white;
+				background: #337ab7;
+				border-color: #337ab7;
+				font: inherit;
+				padding: .5em;
+				border-radius: 4px;
+			}
+			input[type="submit"]:hover {
+				background: #236aa7 !important;
+			}
+		</style>
+	</head>
+	<body>
+		<h2>Run Luan Code</h2>
+		<form name="form0" method="post">
+			<input type="hidden" name="content_type" value="text/plain" />
+			<div>
+				<textarea name="code" rows="20" cols="90" wrap="off" autofocus></textarea>
+			</div>
+			<div>
+				<input type="submit" value="Execute Luan Code"/>
+			</div>
+		</form>
+	</body>
+</html>
+<% end
+
+return function()
+	local content_type = Http.request.parameter.content_type
+	if content_type ~= nil then
+		Http.response.header.content_type = content_type
+	end
+	Io.stdout = Http.response.text_writer()
+	local code = Http.request.parameter.code
+	if code == nil then
+		form()
+		return
+	end
+	local env = {
+		request = Http.request;
+		response = Http.response;
+	}
+	try {
+		function()
+			local run = load(code,"<web_run>",env)
+			run()
+		end;
+		catch = function(e)
+			Http.response.reset()
+			Http.response.header.content_type = "text/plain"
+			Io.stdout = Http.response.text_writer()
+			print(e)
+			print""
+			print""
+			print_with_line_numbers(code)
+		end;
+	}
+end
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/http/serve.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/http/serve.luan	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,9 @@
+local Io = require "luan:Io.luan"
+local Server = require "luan:http/Server.luan"
+
+if #{...} ~= 1 then
+	Io.stderr.write "usage: luan luan:http/serve dir-URI\n"
+	return
+end
+
+Server.serve(...)
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/http/shell.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/http/shell.luan	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,1 @@
+return require("luan:http/Shell_mod.luan").respond
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/http/test.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/http/test.luan	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,12 @@
+local Luan = require "luan:Luan.luan"
+local Io = require "luan:Io.luan"
+local Server = require "luan:http/Server.luan"
+
+if #{...} ~= 2 then
+	Io.stderr.write "usage: luan luan:http/serve dir-URI test-URI\n"
+	return
+end
+
+local dir, test = ...
+Server.init(dir)
+Luan.do_file(test)
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/logging/Logging.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/logging/Logging.luan	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,90 @@
+java()
+local Logger = require "java:org.apache.log4j.Logger"
+local EnhancedPatternLayout = require "java:org.apache.log4j.EnhancedPatternLayout"
+local ConsoleAppender = require "java:org.apache.log4j.ConsoleAppender"
+local Level = require "java:org.apache.log4j.Level"
+local RollingFileAppender = require "java:org.apache.log4j.RollingFileAppender"
+local LuanLogger = require "java:luan.modules.logging.LuanLogger"
+
+local M = {}
+
+M.layout = "%d %-5p %c - %m%n"
+
+M.level = "INFO"
+
+M.console = "System.err"  -- or "System.out" or set to nil for no console
+
+M.file = nil  -- set to file name if you want logging to a file
+
+M.max_file_size = nil  -- by default is "10MB"
+
+
+M.log4j_root_logger = Logger.getRootLogger()
+
+local function to_level(level)
+	return level and Level.toLevel(level)
+end
+
+function M.log_to_file(file,logger_name)  -- logger_name is optional, defaults to root logger
+	local appender = RollingFileAppender.new(M.ptn_layout, file)
+	appender.setMaxFileSize(M.max_file_size)
+	local logger = logger_name and Logger.getLogger(logger_name) or M.log4j_root_logger
+	logger.addAppender(appender)
+	return appender
+end
+
+function M.init()
+	M.log4j_root_logger.removeAllAppenders()
+	M.log4j_root_logger.setLevel( to_level(M.level) )
+	M.ptn_layout = EnhancedPatternLayout.new(M.layout)
+
+	if M.console ~= nil then
+		M.console_appender = ConsoleAppender.new(M.ptn_layout,M.console)
+		M.log4j_root_logger.addAppender(M.console_appender)
+	else
+		M.console_appender = nil
+	end
+
+	if M.file ~= nil then
+		M.file_appender = M.log_to_file(M.file)
+	else
+		M.file_appender = nil
+	end
+end
+
+
+local function to_luan_logger(log4j_logger)
+	local tbl = {}
+
+	local luanLogger = LuanLogger.new(log4j_logger)
+	tbl.error = luanLogger.error
+	tbl.warn = luanLogger.warn
+	tbl.info = luanLogger.info
+	tbl.debug = luanLogger.debug
+
+	function tbl.get_level()
+		local level = log4j_logger.getLevel()
+		return level and level.toString()
+	end
+
+	function tbl.get_effective_level()
+		local level = log4j_logger.getEffectiveLevel()
+		return level and level.toString()
+	end
+
+	function tbl.set_level(level)
+		log4j_logger.setLevel( to_level(level) )
+	end
+
+	return tbl
+end
+
+function M.logger(name)
+	return to_luan_logger( Logger.getLogger(name) )
+end
+
+function M.root_logger()
+	return to_luan_logger( Logger.getRootLogger() )
+end
+
+return M
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/logging/LuanLogger.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/logging/LuanLogger.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,33 @@
+package luan.modules.logging;
+
+import org.apache.log4j.Logger;
+import luan.Luan;
+import luan.LuanState;
+import luan.LuanException;
+import luan.LuanTable;
+
+
+public final class LuanLogger {
+	private final Logger logger;
+
+	public LuanLogger(Logger logger) {
+		this.logger = logger;
+	}
+
+	public void error(LuanState luan,Object obj) throws LuanException {
+		logger.error( luan.toString(obj) );
+	}
+
+	public void warn(LuanState luan,Object obj) throws LuanException {
+		logger.warn( luan.toString(obj) );
+	}
+
+	public void info(LuanState luan,Object obj) throws LuanException {
+		logger.info( luan.toString(obj) );
+	}
+
+	public void debug(LuanState luan,Object obj) throws LuanException {
+		logger.debug( luan.toString(obj) );
+	}
+
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/logging/init.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/logging/init.luan	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,2 @@
+require "luan:logging/Logging.luan".init()
+return true
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/luan_to_java.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/luan_to_java.luan	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,5 @@
+java()
+local LuanCompiler = require "java:luan.impl.LuanCompiler"
+local Io = require "luan:Io.luan"
+
+Io.stdout.write( LuanCompiler.toJava( Io.stdin.read_text(), "stdin" ) )
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/lucene/Ab_testing.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/lucene/Ab_testing.luan	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,272 @@
+local Luan = require "luan:Luan.luan"
+local pairs = Luan.pairs
+local ipairs = Luan.ipairs
+local error = Luan.error
+local range = Luan.range
+local Math = require "luan:Math.luan"
+local Table = require "luan:Table.luan"
+local String = require "luan:String.luan"
+local gsub = String.gsub
+local Io = require "luan:Io.luan"
+local Http = require "luan:http/Http.luan"
+local Logging = require "luan:logging/Logging.luan"
+local Lucene = require "luan:lucene/Lucene.luan"
+
+local M = {}
+
+local logger = Logging.logger "Ab_testing"
+
+function M.of(index)
+
+	local ab_testing = {}
+
+	ab_testing.test_map = {}
+
+	function ab_testing.test(test)
+		test.name or error "name not defined"
+		test.values or error "values not defined"
+
+		-- list of event names
+		test.events or error "events not defined"
+
+		-- map of event name to aggregator factory
+		test.aggregator_factories or error "aggregator_factories not defined"
+
+		-- test.date_field is optional
+
+		local field = "ab_test_" .. test.name
+		index.indexed_fields[field] == nil or error("test "+test.name+" already defined")
+		index.indexed_fields[field] = Lucene.type.string
+		test.field = field
+
+		-- returns map of event name to (map of value to result) and "start_date"
+		function test.results()
+			local results = {}
+			for name in pairs(test.aggregator_factories) do
+				results[name] = {}
+			end
+			local date_field = test.date_field
+			local start_date = nil
+			for _, value in ipairs(test.values) do
+				local aggregators = {}
+				for name, factory in pairs(test.aggregator_factories) do
+					aggregators[name] = factory()
+				end
+				local query = field..":"..value
+				index.advanced_search(query, function(_,doc_fn)
+					local doc = doc_fn()
+					for _, aggregator in pairs(aggregators) do
+						aggregator.aggregate(doc)
+					end
+					if date_field ~= nil then
+						local date = doc[date_field]
+						if date ~= nil and (start_date==nil or start_date > date) then
+							start_date = date
+						end
+					end
+				end)
+				for name, aggregator in pairs(aggregators) do
+					results[name][value] = aggregator.result
+				end
+			end
+			results.start_date = start_date
+			return results
+		end
+
+		function test.fancy_results()
+			local events = test.events
+			local results = test.results()
+			local fancy = {}
+			fancy.start_date = results.start_date
+			local event = events[1]
+			fancy[event] = {}
+			for value, count in pairs(results[event]) do
+				fancy[event][value] = {}
+				fancy[event][value].count = count
+				fancy[event][value].pct_of_total = 100
+				fancy[event][value].pct_of_prev = 100
+			end
+			local all = results[event]
+			local prev = all
+			for i in range(2,#events) do
+				event = events[i]
+				fancy[event] = {}
+				for value, count in pairs(results[event]) do
+					fancy[event][value] = {}
+					fancy[event][value].count = count
+					fancy[event][value].pct_of_total = M.percent(count,all[value])
+					fancy[event][value].pct_of_prev = M.percent(count,prev[value])
+				end
+				prev = results[event]
+			end
+			return fancy
+		end
+
+		ab_testing.test_map[test.name] = test
+
+		return test
+	end
+
+	function ab_testing.value(test_name,values)
+		return values[test_name] or ab_testing.test_map[test_name].values[1]
+	end
+
+	-- returns map from test name to value
+	function ab_testing.from_doc(doc)
+		local values = {}
+		for _, test in pairs(ab_testing.test_map) do
+			values[test.name] = doc[test.field]
+		end
+		return values
+	end
+
+	function ab_testing.to_doc(doc,values,tests)
+		tests = tests or ab_testing.test_map
+		if values == nil then
+			values = {}
+			for _, test in pairs(tests) do
+				values[test.name] = test.values[Math.random(#test.values)]
+			end
+		end
+		for _, test in pairs(tests) do
+			doc[test.field] = values[test.name]
+		end
+		return values
+	end
+
+	function ab_testing.web_page(test_names)
+		return function()
+			local results = {}
+			for _, name in ipairs(test_names) do
+				local test = ab_testing.test_map[name]
+				test or error("test not found: "..name)
+				results[name] = test.fancy_results()
+			end
+			Io.stdout = Http.response.text_writer()
+			M.html(test_names,ab_testing.test_map,results)
+		end
+	end
+
+	return ab_testing
+end
+
+
+-- aggregator factories
+
+-- fn(doc) should return boolean whether doc should be counted
+function M.count(fn)
+	return function()
+		local aggregator = {}
+		aggregator.result = 0
+		function aggregator.aggregate(doc)
+			if fn(doc) then
+				aggregator.result = aggregator.result + 1
+			end
+		end
+		return aggregator
+	end
+end
+
+M.count_all = M.count( function(doc) return true end )
+
+-- fn(doc) should return number to add to result, return 0 for nothing
+function M.sum(fn)
+	return function()
+		local aggregator = {}
+		aggregator.result = 0
+		function aggregator.aggregate(doc)
+			aggregator.result = aggregator.result + fn(doc)
+		end
+		return aggregator
+	end
+end
+
+
+
+function M.percent(x,total)
+	if total==0 then
+		return 0
+	else
+		return 100 * x / total
+	end
+end
+
+-- I will change this to use SimplyHTML when this is used again.
+local function basic_style() %>
+	body {font-family:'Arial',sans-serif;font-size:16px;padding:1em 2em}
+	h1 {font-weight:bold;font-size:20px}
+	h2 {margin:2em 0 0em;font-size:18px;color:#3589B1}
+	table.results {margin-top:.5em;border-collapse:collapse;font-size:90%}
+	table.results th {background:#eee}
+	table.results th,table.results td {border-left:1px solid #bbb;padding:.4em 2em}
+	table.results tr:nth-child(odd) td {background:#f8f8f8}
+<% end
+
+local function format(v)
+	v = v .. ''
+	return gsub( v, [[(\d+\.\d{1})\d+]], '$1' )
+end
+
+function M.html(test_names,tests,results) %>
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>A/B Test Results</title>
+		<style><% basic_style() %></style>
+	</head>
+	<body>
+		<h1>A/B Test Results</h1>
+		<%
+		for _, test_name in ipairs(test_names) do
+			local test = tests[test_name]
+			local result = results[test_name]
+			local n = #test.values
+			%>
+			<h2><%=test_name%></h2>
+			<table class="results">
+				<tr>
+					<th>Event</th>
+					<th class="top" colspan="<%=n%>">Count</th>
+					<th class="top" colspan="<%=n%>">% of total</th>
+					<th class="top" colspan="<%=n%>">% of prev</th>
+				</tr>
+				<tr>
+					<th></th>
+					<%
+					for _ in range(1,3) do
+						for _, value in ipairs(test.values) do
+							%><th class="top"><%=value%></th><%
+						end
+					end
+					%>
+				</tr>
+				<%
+				for _, event in ipairs(test.events) do
+					local event_values = result[event]
+					%>
+					<tr>
+						<td><%=event%></td>
+						<%
+						for _, value in ipairs(test.values) do
+							%><td><%=format(event_values[value].count)%></th><%
+						end
+						for _, value in ipairs(test.values) do
+							%><td><%=format(event_values[value].pct_of_total)%></th><%
+						end
+						for _, value in ipairs(test.values) do
+							%><td><%=format(event_values[value].pct_of_prev)%></th><%
+						end
+						%>
+					</tr>
+					<%
+				end
+				%>
+			</table>
+			<%
+		end
+		%>
+	</body>
+</html>
+<% end
+
+return M
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/lucene/Lucene.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/lucene/Lucene.luan	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,161 @@
+java()
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local ipairs = Luan.ipairs or error()
+local type = Luan.type or error()
+local Html = require "luan:Html.luan"
+local Io = require "luan:Io.luan"
+local uri = Io.uri or error()
+local String = require "luan:String.luan"
+local matches = String.matches or error()
+local Rpc = require "luan:Rpc.luan"
+local LuceneIndex = require "java:luan.modules.lucene.LuceneIndex"
+local NumberFieldParser = require "java:luan.modules.lucene.queryparser.NumberFieldParser"
+local StringFieldParser = require "java:luan.modules.lucene.queryparser.StringFieldParser"
+local SaneQueryParser = require "java:luan.modules.lucene.queryparser.SaneQueryParser"
+local Version = require "java:org.apache.lucene.util.Version"
+local EnglishAnalyzer = require "java:org.apache.lucene.analysis.en.EnglishAnalyzer"
+
+
+local M = {}
+
+M.instances = {}
+
+M.type = {
+	string = LuceneIndex.STRING_FIELD_PARSER;
+	integer = NumberFieldParser.INT;
+	long = NumberFieldParser.LONG;
+	double = NumberFieldParser.DOUBLE;
+
+	english = StringFieldParser.new(EnglishAnalyzer.new(Version.LUCENE_CURRENT))
+}
+
+M.literal = SaneQueryParser.literal
+
+function M.index(index_dir,default_type,default_fields)
+	local index = {}
+	index.dir = index_dir
+	local java_index = LuceneIndex.new(index_dir,default_type,default_fields)
+	index.indexed_fields = java_index.indexedFieldsMeta.newTable()
+
+	-- index.indexed_only_fields[type][field] = fn(doc)
+	index.indexed_only_fields = java_index.indexed_only_fields
+
+	index.to_string = java_index.to_string
+--	index.backup = java_index.backup
+	index.snapshot = java_index.snapshot
+	index.advanced_search = java_index.advanced_search
+	index.search_in_transaction = java_index.search_in_transaction
+	index.delete_all = java_index.delete_all
+	index.delete = java_index.delete
+	index.save = java_index.save
+	index.update_in_transaction = java_index.update_in_transaction
+--	index.close = java_index.close
+	index.ensure_open = java_index.ensure_open
+	index.next_id = java_index.nextId
+	index.highlighter = java_index.highlighter
+
+	M.instances[index] = true
+
+	function index.close()
+		M.instances[index] = nil
+		java_index.close()
+	end
+
+	function index.search(query, from, to, sort)
+		from or error "missing 'from' parameter"
+		to or error "missing 'to' parameter"
+		local results = {}
+		local function fn(i,doc_fn)
+			if i >= from then
+				results[#results+1] = doc_fn()
+			end
+		end
+		local total_hits = index.advanced_search(query,fn,to,sort)
+		return results, total_hits
+	end
+
+	function index.get_document(query)
+		local doc
+		local function fn(_,doc_fn)
+			doc = doc_fn()
+		end
+		local total_hits = index.advanced_search(query,fn,1)
+		total_hits <= 1 or error( "found " .. total_hits .. " documents" )
+		return doc
+	end
+
+	function index.count(query)
+		return index.advanced_search(query)
+	end
+
+	function index.html_highlighter(query,formatter,container_tags)
+		local highlighter = index.highlighter(query,formatter)
+		return function(html)
+			local list = Html.parse(html,container_tags)
+			local result = {}
+			for _, obj in ipairs(list) do
+				if type(obj) == "string" then
+					obj = highlighter(obj)
+				end
+				result[#result+1] = obj
+			end
+			return Html.to_string(result)
+		end
+	end
+
+	function index.zip(zip_file)
+		index.snapshot( function(dir_path,file_names)
+			zip_file.delete()
+			local zip_path = zip_file.canonical().to_string()
+			local dir = uri("file:"..dir_path)
+			local dir_name = dir.name()
+			local options = {dir=dir.parent()}
+			for _, file_name in ipairs(file_names) do
+				local cmd = "zip "..zip_path.." "..dir_name.."/"..file_name
+				Io.uri("os:"..cmd,options).read_text()
+			end
+		end )
+	end
+
+	function index.restore(zip_file)
+		java_index.run_in_lock( function()
+			local lucene_dir = uri("file:"..index.dir)
+			local before_restore = lucene_dir.parent().child("before_restore.zip")
+			index.zip(before_restore)
+			java_index.close()
+			lucene_dir.delete()
+			Io.uri("os:unzip "..zip_file.canonical().to_string(),{dir=lucene_dir.parent()}).read_text()
+			java_index.reopen()
+		end )
+	end
+
+	local function multi_error()
+		error "multiple lucene instances"
+	end
+
+	if Rpc.functions.backup == nil then
+
+		function Rpc.functions.lucene_backup(password)
+			Io.password == password or error "wrong password"
+			local zip_file = uri("file:"..index.dir).parent().child("backup.zip")
+			index.zip(zip_file)
+			return zip_file
+		end
+
+		function Rpc.functions.lucene_restore(password,zip_file)
+			Io.password == password or error "wrong password"
+			local backup_zip = uri("file:"..index.dir).parent().child("backup.zip")
+			backup_zip.write(zip_file)
+			index.restore(backup_zip)
+		end
+
+	else
+		Rpc.functions.lucene_backup = multi_error
+		Rpc.functions.lucene_restore = multi_error
+	end
+
+	return index
+end
+
+return M
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/lucene/LuceneIndex.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/lucene/LuceneIndex.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,631 @@
+package luan.modules.lucene;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Set;
+import java.util.HashSet;
+import java.util.Collections;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.zip.ZipOutputStream;
+import java.util.zip.ZipEntry;
+import org.apache.lucene.analysis.Analyzer;
+import org.apache.lucene.analysis.core.KeywordAnalyzer;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.document.StoredField;
+import org.apache.lucene.document.StringField;
+import org.apache.lucene.document.TextField;
+import org.apache.lucene.document.IntField;
+import org.apache.lucene.document.LongField;
+import org.apache.lucene.document.DoubleField;
+import org.apache.lucene.index.IndexableField;
+import org.apache.lucene.index.IndexWriter;
+import org.apache.lucene.index.IndexWriterConfig;
+import org.apache.lucene.index.DirectoryReader;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.index.SnapshotDeletionPolicy;
+import org.apache.lucene.index.IndexCommit;
+import org.apache.lucene.index.AtomicReaderContext;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.store.FSDirectory;
+import org.apache.lucene.util.Version;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.NumericUtils;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.TermQuery;
+import org.apache.lucene.search.TopDocs;
+import org.apache.lucene.search.Sort;
+import org.apache.lucene.search.SortField;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.TotalHitCountCollector;
+import org.apache.lucene.search.ScoreDoc;
+import org.apache.lucene.search.Collector;
+import org.apache.lucene.search.Scorer;
+import org.apache.lucene.search.BooleanClause;
+import org.apache.lucene.search.highlight.Formatter;
+import org.apache.lucene.search.highlight.Highlighter;
+import org.apache.lucene.search.highlight.InvalidTokenOffsetsException;
+import org.apache.lucene.search.highlight.NullFragmenter;
+import org.apache.lucene.search.highlight.QueryScorer;
+import org.apache.lucene.search.highlight.TokenGroup;
+import luan.modules.lucene.queryparser.SaneQueryParser;
+import luan.modules.lucene.queryparser.FieldParser;
+import luan.modules.lucene.queryparser.MultiFieldParser;
+import luan.modules.lucene.queryparser.StringFieldParser;
+import luan.modules.lucene.queryparser.NumberFieldParser;
+import luan.modules.lucene.queryparser.ParseException;
+import luan.modules.Utils;
+import luan.Luan;
+import luan.LuanState;
+import luan.LuanTable;
+import luan.LuanFunction;
+import luan.LuanException;
+import luan.LuanMeta;
+import luan.LuanRuntimeException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+public final class LuceneIndex implements Closeable {
+	private static final Logger logger = LoggerFactory.getLogger(LuceneIndex.class);
+
+	private static final String FLD_NEXT_ID = "nextId";
+	public static final StringFieldParser STRING_FIELD_PARSER = new StringFieldParser(new KeywordAnalyzer());
+
+	private static final Version version = Version.LUCENE_4_9;
+	private final ReentrantLock writeLock = new ReentrantLock();
+	private final File indexDir;
+	private SnapshotDeletionPolicy snapshotDeletionPolicy;
+	private IndexWriter writer;
+	private DirectoryReader reader;
+	private IndexSearcher searcher;
+	private final ThreadLocal<IndexSearcher> threadLocalSearcher = new ThreadLocal<IndexSearcher>();
+	private boolean isClosed = true;
+	private final MultiFieldParser mfp;
+	public final LuanTable indexed_only_fields = new LuanTable();
+	private final Analyzer analyzer;
+
+	private static ConcurrentMap<File,AtomicInteger> globalWriteCounters = new ConcurrentHashMap<File,AtomicInteger>();
+	private File fileDir;
+	private int writeCount;
+
+	public LuceneIndex(LuanState luan,String indexDirStr,FieldParser defaultFieldParser,String[] defaultFields) throws LuanException, IOException {
+		mfp = defaultFieldParser==null ? new MultiFieldParser() : new MultiFieldParser(defaultFieldParser,defaultFields);
+		mfp.fields.put( "type", STRING_FIELD_PARSER );
+		mfp.fields.put( "id", NumberFieldParser.LONG );
+		File indexDir = new File(indexDirStr);
+		this.indexDir = indexDir;
+		Analyzer analyzer = STRING_FIELD_PARSER.analyzer;
+		if( defaultFieldParser instanceof StringFieldParser ) {
+			StringFieldParser sfp = (StringFieldParser)defaultFieldParser;
+			analyzer = sfp.analyzer;
+		}
+		this.analyzer = analyzer;
+		luan.onClose(this);
+		reopen();
+	}
+
+	public void reopen() throws LuanException, IOException {
+		if( !isClosed )  throw new RuntimeException();
+		isClosed = false;
+		IndexWriterConfig conf = new IndexWriterConfig(version,analyzer);
+		snapshotDeletionPolicy = new SnapshotDeletionPolicy(conf.getIndexDeletionPolicy());
+		conf.setIndexDeletionPolicy(snapshotDeletionPolicy);
+		FSDirectory dir = FSDirectory.open(indexDir);
+		fileDir = dir.getDirectory();
+		globalWriteCounters.putIfAbsent(fileDir,new AtomicInteger());
+		writer = new IndexWriter(dir,conf);
+		writer.commit();  // commit index creation
+		reader = DirectoryReader.open(dir);
+		searcher = new IndexSearcher(reader);
+		initId();
+	}
+
+	private int globalWriteCount() {
+		return globalWriteCounters.get(fileDir).get();
+	}
+
+	private void wrote() {
+		globalWriteCounters.get(fileDir).incrementAndGet();
+	}
+
+	public void delete_all() throws IOException {
+		boolean commit = !writeLock.isHeldByCurrentThread();
+		writeLock.lock();
+		try {
+			writer.deleteAll();
+			id = idLim = 0;
+			if(commit) writer.commit();
+		} finally {
+			wrote();
+			writeLock.unlock();
+		}
+	}
+
+	private static Term term(String key,long value) {
+		BytesRef br = new BytesRef();
+		NumericUtils.longToPrefixCoded(value,0,br);
+		return new Term(key,br);
+	}
+
+	public void delete(LuanState luan,String queryStr) throws LuanException, IOException, ParseException {
+		Query query = SaneQueryParser.parseQuery(mfp,queryStr);
+
+		boolean commit = !writeLock.isHeldByCurrentThread();
+		writeLock.lock();
+		try {
+			writer.deleteDocuments(query);
+			if(commit) writer.commit();
+		} finally {
+			wrote();
+			writeLock.unlock();
+		}
+	}
+
+	public void save(LuanState luan,LuanTable doc) throws LuanException, IOException {
+		Set indexedOnlySet = new HashSet();
+		Object typeObj = doc.get(luan,"type");
+		if( typeObj==null )
+			throw new LuanException("missing 'type' field");
+		if( !(typeObj instanceof String) )
+			throw new LuanException("type must be string");
+		String type = (String)typeObj;
+		Object indexedOnlyObj = indexed_only_fields.get(luan,type);
+		if( indexedOnlyObj != null ) {
+			if( !(indexedOnlyObj instanceof LuanTable) )
+				throw new LuanException("indexed_only_fields elements must be tables");
+			LuanTable indexedOnly = (LuanTable)indexedOnlyObj;
+			for( Map.Entry<Object,Object> entry : indexedOnly.iterable(luan) ) {
+				Object key = entry.getKey();
+				if( !(key instanceof String) )
+					throw new LuanException("indexed_only_fields."+type+" entries must be strings");
+				String name = (String)key;
+				Object value = entry.getValue();
+				if( !(value instanceof LuanFunction) )
+					throw new LuanException("indexed_only_fields."+type+" values must be functions");
+				LuanFunction fn = (LuanFunction)value;
+				value = Luan.first(fn.call(luan,new Object[]{doc}));
+				doc.put(luan, name, value );
+				indexedOnlySet.add(name);
+			}
+		}
+		Object obj = doc.get(luan,"id");
+		Long id;
+		try {
+			id = (Long)obj;
+		} catch(ClassCastException e) {
+			throw new LuanException("id should be Long but is "+obj.getClass().getSimpleName());
+		}
+
+		boolean commit = !writeLock.isHeldByCurrentThread();
+		writeLock.lock();
+		try {
+			if( id == null ) {
+				id = nextId(luan);
+				doc.put(luan,"id",id);
+				writer.addDocument(toLucene(luan,doc,indexedOnlySet));
+			} else {
+				writer.updateDocument( term("id",id), toLucene(luan,doc,indexedOnlySet) );
+			}
+			if(commit) writer.commit();
+		} finally {
+			wrote();
+			writeLock.unlock();
+		}
+	}
+
+	public void update_in_transaction(LuanState luan,LuanFunction fn) throws IOException, LuanException {
+		boolean commit = !writeLock.isHeldByCurrentThread();
+		writeLock.lock();
+		try {
+			fn.call(luan);
+			if(commit) writer.commit();
+		} finally {
+			wrote();
+			writeLock.unlock();
+		}
+	}
+
+	public void run_in_lock(LuanState luan,LuanFunction fn) throws IOException, LuanException {
+		if( writeLock.isHeldByCurrentThread() )
+			throw new RuntimeException();
+		writeLock.lock();
+		try {
+			synchronized(this) {
+				fn.call(luan);
+			}
+		} finally {
+			wrote();
+			writeLock.unlock();
+		}
+	}
+
+
+	private long id;
+	private long idLim;
+	private final int idBatch = 10;
+
+	private void initId() throws LuanException, IOException {
+		TopDocs td = searcher.search(new TermQuery(new Term("type","next_id")),1);
+		switch(td.totalHits) {
+		case 0:
+			id = 0;
+			idLim = 0;
+			break;
+		case 1:
+			idLim = (Long)searcher.doc(td.scoreDocs[0].doc).getField(FLD_NEXT_ID).numericValue();
+			id = idLim;
+			break;
+		default:
+			throw new RuntimeException();
+		}
+	}
+
+	public synchronized long nextId(LuanState luan) throws LuanException, IOException {
+		if( ++id > idLim ) {
+			idLim += idBatch;
+			LuanTable doc = new LuanTable();
+			doc.rawPut( "type", "next_id" );
+			doc.rawPut( FLD_NEXT_ID, idLim );
+			writer.updateDocument(new Term("type","next_id"),toLucene(luan,doc,Collections.EMPTY_SET));
+			wrote();
+		}
+		return id;
+	}
+
+/*
+	public void backup(String zipFile) throws LuanException, IOException {
+		if( !zipFile.endsWith(".zip") )
+			throw new LuanException("file "+zipFile+" doesn't end with '.zip'");
+		IndexCommit ic = snapshotDeletionPolicy.snapshot();
+		try {
+			ZipOutputStream out = new ZipOutputStream(new FileOutputStream(zipFile));
+			for( String fileName : ic.getFileNames() ) {
+				out.putNextEntry(new ZipEntry(fileName));
+				FileInputStream in = new FileInputStream(new File(indexDir,fileName));
+				Utils.copyAll(in,out);
+				in.close();
+				out.closeEntry();
+			}
+			out.close();
+		} finally {
+			snapshotDeletionPolicy.release(ic);
+		}
+	}
+*/
+	public Object snapshot(LuanState luan,LuanFunction fn) throws LuanException, IOException {
+		IndexCommit ic = snapshotDeletionPolicy.snapshot();
+		try {
+			String dir = fileDir.toString();
+			LuanTable fileNames = new LuanTable(new ArrayList(ic.getFileNames()));
+			return fn.call(luan,new Object[]{dir,fileNames});
+		} finally {
+			snapshotDeletionPolicy.release(ic);
+		}
+	}
+
+
+
+	public String to_string() {
+		return writer.getDirectory().toString();
+	}
+
+	public void close() throws IOException {
+		if( !isClosed ) {
+			writer.close();
+			reader.close();
+			isClosed = true;
+		}
+	}
+
+	protected void finalize() throws Throwable {
+		if( !isClosed ) {
+			logger.error("not closed");
+			close();
+		}
+		super.finalize();
+	}
+
+
+
+	private static class DocFn extends LuanFunction {
+		final IndexSearcher searcher;
+		int docID;
+
+		DocFn(IndexSearcher searcher) {
+			this.searcher = searcher;
+		}
+
+		@Override public Object call(LuanState luan,Object[] args) throws LuanException {
+			try {
+				return toTable(searcher.doc(docID));
+			} catch(IOException e) {
+				throw new LuanException(e);
+			}
+		}
+	}
+
+	private static abstract class MyCollector extends Collector {
+		int docBase;
+		int i = 0;
+
+		@Override public void setScorer(Scorer scorer) {}
+		@Override public void setNextReader(AtomicReaderContext context) {
+			this.docBase = context.docBase;
+		}
+		@Override public boolean acceptsDocsOutOfOrder() {
+			return true;
+		}
+	}
+
+	private synchronized IndexSearcher openSearcher() throws IOException {
+		int gwc = globalWriteCount();
+		if( writeCount != gwc ) {
+			writeCount = gwc;
+			DirectoryReader newReader = DirectoryReader.openIfChanged(reader);
+			if( newReader != null ) {
+				reader.decRef();
+				reader = newReader;
+				searcher = new IndexSearcher(reader);
+			}
+		}
+		reader.incRef();
+		return searcher;
+	}
+
+	// call in finally block
+	private static void close(IndexSearcher searcher) throws IOException {
+		searcher.getIndexReader().decRef();
+	}
+
+	public void ensure_open() throws IOException {
+		close(openSearcher());
+	}
+
+	public int advanced_search( final LuanState luan, String queryStr, LuanFunction fn, Integer n, String sortStr ) throws LuanException, IOException, ParseException {
+		Utils.checkNotNull(queryStr);
+		Query query = SaneQueryParser.parseQuery(mfp,queryStr);
+		IndexSearcher searcher = threadLocalSearcher.get();
+		boolean inTransaction = searcher != null;
+		if( !inTransaction )
+			searcher = openSearcher();
+		try {
+			if( fn!=null && n==null ) {
+				if( sortStr != null )
+					throw new LuanException("sort must be nil when n is nil");
+				final DocFn docFn = new DocFn(searcher);
+				MyCollector col = new MyCollector() {
+					@Override public void collect(int doc) {
+						try {
+							docFn.docID = docBase + doc;
+							fn.call(luan,new Object[]{++i,docFn});
+						} catch(LuanException e) {
+							throw new LuanRuntimeException(e);
+						}
+					}
+				};
+				try {
+					searcher.search(query,col);
+				} catch(LuanRuntimeException e) {
+					throw (LuanException)e.getCause();
+				}
+				return col.i;
+			}
+			if( fn==null || n==0 ) {
+				TotalHitCountCollector thcc = new TotalHitCountCollector();
+				searcher.search(query,thcc);
+				return thcc.getTotalHits();
+			}
+			Sort sort = sortStr==null ? null : SaneQueryParser.parseSort(mfp,sortStr);
+			TopDocs td = sort==null ? searcher.search(query,n) : searcher.search(query,n,sort);
+			final ScoreDoc[] scoreDocs = td.scoreDocs;
+			DocFn docFn = new DocFn(searcher);
+			for( int i=0; i<scoreDocs.length; i++ ) {
+				docFn.docID = scoreDocs[i].doc;
+				fn.call(luan,new Object[]{i+1,docFn});
+			}
+			return td.totalHits;
+		} finally {
+			if( !inTransaction )
+				close(searcher);
+		}
+	}
+
+	public Object search_in_transaction(LuanState luan,LuanFunction fn) throws LuanException, IOException {
+		if( threadLocalSearcher.get() != null )
+			throw new LuanException("can't nest search_in_transaction calls");
+		IndexSearcher searcher = openSearcher();
+		threadLocalSearcher.set(searcher);
+		try {
+			return fn.call(luan);
+		} finally {
+			threadLocalSearcher.set(null);
+			close(searcher);
+		}
+	}
+
+
+
+	public final LuanMeta indexedFieldsMeta = new LuanMeta() {
+
+		@Override public boolean canNewindex() {
+			return true;
+		}
+
+		@Override public Object __index(LuanState luan,LuanTable tbl,Object key) {
+			return mfp.fields.get(key);
+		}
+
+		@Override public void __new_index(LuanState luan,LuanTable tbl,Object key,Object value) throws LuanException {
+			if( !(key instanceof String) )
+				throw new LuanException("key must be string");
+			String field = (String)key;
+			if( value==null ) {  // delete
+				mfp.fields.remove(field);
+				return;
+			}
+			if( !(value instanceof FieldParser) )
+				throw new LuanException("value must be FieldParser like the values of Lucene.type");
+			FieldParser parser = (FieldParser)value;
+			mfp.fields.put( field, parser );
+		}
+
+		@Override public final Iterator keys(LuanTable tbl) {
+			return mfp.fields.keySet().iterator();
+		}
+
+		@Override protected String type(LuanTable tbl) {
+			return "lucene-indexed-fields";
+		}
+
+	};
+
+
+
+	private IndexableField newField(String name,Object value,Field.Store store,Set<String> indexed)
+		throws LuanException
+	{
+		if( value instanceof String ) {
+			String s = (String)value;
+			FieldParser fp = mfp.fields.get(name);
+			if( fp != null ) {
+				if( fp instanceof StringFieldParser && fp != STRING_FIELD_PARSER ) {
+					return new TextField(name, s, store);
+				} else {
+					return new StringField(name, s, store);
+				}
+			} else {
+				return new StoredField(name, s);
+			}
+		} else if( value instanceof Integer ) {
+			int i = (Integer)value;
+			if( indexed.contains(name) ) {
+				return new IntField(name, i, store);
+			} else {
+				return new StoredField(name, i);
+			}
+		} else if( value instanceof Long ) {
+			long i = (Long)value;
+			if( indexed.contains(name) ) {
+				return new LongField(name, i, store);
+			} else {
+				return new StoredField(name, i);
+			}
+		} else if( value instanceof Double ) {
+			double i = (Double)value;
+			if( indexed.contains(name) ) {
+				return new DoubleField(name, i, store);
+			} else {
+				return new StoredField(name, i);
+			}
+		} else if( value instanceof byte[] ) {
+			byte[] b = (byte[])value;
+			return new StoredField(name, b);
+		} else
+			throw new LuanException("invalid value type "+value.getClass()+"' for '"+name+"'");
+	}
+
+	private Document toLucene(LuanState luan,LuanTable table,Set indexOnly) throws LuanException {
+		Set<String> indexed = mfp.fields.keySet();
+		Document doc = new Document();
+		for( Map.Entry<Object,Object> entry : table.iterable(luan) ) {
+			Object key = entry.getKey();
+			if( !(key instanceof String) )
+				throw new LuanException("key must be string");
+			String name = (String)key;
+			Object value = entry.getValue();
+			Field.Store store = indexOnly.contains(name) ? Field.Store.NO : Field.Store.YES;
+			if( !(value instanceof LuanTable) ) {
+				doc.add(newField(name, value, store, indexed));
+			} else { // list
+				LuanTable list = (LuanTable)value;
+				for( Object el : list.asList() ) {
+					doc.add(newField(name, el, store, indexed));
+				}
+			}
+		}
+		return doc;
+	}
+
+	private static Object getValue(IndexableField ifld) throws LuanException {
+		BytesRef br = ifld.binaryValue();
+		if( br != null )
+			return br.bytes;
+		Number n = ifld.numericValue();
+		if( n != null )
+			return n;
+		String s = ifld.stringValue();
+		if( s != null )
+			return s;
+		throw new LuanException("invalid field type for "+ifld);
+	}
+
+	private static LuanTable toTable(Document doc) throws LuanException {
+		if( doc==null )
+			return null;
+		LuanTable table = new LuanTable();
+		for( IndexableField ifld : doc ) {
+			String name = ifld.name();
+			Object value = getValue(ifld);
+			Object old = table.rawGet(name);
+			if( old == null ) {
+				table.rawPut(name,value);
+			} else {
+				LuanTable list;
+				if( old instanceof LuanTable ) {
+					list = (LuanTable)old;
+				} else {
+					list = new LuanTable();
+					list.rawPut(1,old);
+					table.rawPut(name,list);
+				}
+				list.rawPut(list.rawLength()+1,value);
+			}
+		}
+		return table;
+	}
+
+
+	public LuanFunction highlighter(LuanState luan,String queryStr,LuanFunction formatter) throws ParseException {
+		Query query = SaneQueryParser.parseQuery(mfp,queryStr);
+		Formatter fmt = new Formatter() {
+			public String highlightTerm(String originalText,TokenGroup tokenGroup) {
+				if( tokenGroup.getTotalScore() <= 0 )
+					return originalText;
+				try {
+					return (String)Luan.first(formatter.call(luan,new Object[]{originalText}));
+				} catch(LuanException e) {
+					throw new LuanRuntimeException(e);
+				}
+			}
+		};
+		Highlighter hl = new Highlighter( fmt, new QueryScorer(query) );
+		hl.setTextFragmenter( new NullFragmenter() );
+		return new LuanFunction() {
+			@Override public String call(LuanState luan,Object[] args) throws LuanException {
+				String text = (String)args[0];
+				try {
+					String s = hl.getBestFragment(analyzer,null,text);
+					return s!=null ? s : text;
+				} catch(LuanRuntimeException e) {
+					throw (LuanException)e.getCause();
+				} catch(IOException e) {
+					throw new RuntimeException(e);
+				} catch(InvalidTokenOffsetsException e) {
+					throw new RuntimeException(e);
+				}
+			}
+		};
+	}
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/lucene/Versioning.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/lucene/Versioning.luan	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,56 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local pairs = Luan.pairs or error()
+local Number = require "luan:Number.luan"
+local integer = Number.integer or error()
+local long = Number.long or error()
+local String = require "luan:String.luan"
+local matches = String.matches or error()
+local sub = String.sub or error()
+local string_to_number = String.to_number or error()
+local Table = require "luan:Table.luan"
+local copy = Table.copy or error()
+local Lucene = require "luan:lucene/Lucene.luan"
+require "luan:logging/init.luan"
+local Logging = require "luan:logging/Logging.luan"
+
+local logger = Logging.logger "lucene versioning"
+
+
+local M = {}
+
+function M.update(db,steps,version)
+	local doc = db.get_document"type:version" or { type="version", version=integer(0) }
+	while doc.version < version do
+		doc.version = integer(doc.version + 1)
+		logger.error("step "..doc.version)
+		db.update_in_transaction( function()
+			local step = steps[doc.version]
+			step and step(db)
+			db.save(doc)
+		end )
+	end
+end
+
+
+-- hack to deal with latest changes
+function M.a_big_step(db)
+	db.indexed_fields["id index"] = Lucene.type.string
+	db.advanced_search( Lucene.literal"id index" .. ":*", function(_,doc_fn)
+		local doc = doc_fn()
+		for field, value in pairs(copy(doc)) do
+			if matches(field," index$") then
+				local new_field = sub(field,1,-7)
+				db.indexed_fields[new_field] or error("field '"..new_field.."' not indexed")
+				doc[new_field] = value
+				doc[field] = nil
+			end
+		end
+		doc.id = long(string_to_number(doc.id))
+		db.save(doc)
+	end )
+	db.indexed_fields["type index"] = Lucene.type.string
+	db.delete( Lucene.literal"type index" .. ":*" )
+end
+
+return M
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/lucene/Web_search.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/lucene/Web_search.luan	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,189 @@
+ local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local pairs = Luan.pairs or error()
+local ipairs = Luan.ipairs or error()
+local range = Luan.range or error()
+local to_string = Luan.to_string or error()
+local Io = require "luan:Io.luan"
+local repr = Io.repr or error()
+local Http = require "luan:http/Http.luan"
+local String = require "luan:String.luan"
+local string_to_number = String.to_number or error()
+local Html = require "luan:Html.luan"
+
+
+local M = {}
+
+local function style() %>
+			body {
+				font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+				margin: 2em 5%;
+			}
+			h2 {
+				margin-bottom: .5em;
+			}
+			label {
+				text-align: right;
+				min-width: 6em;
+				display: inline-block;
+				margin-right: .5em;
+			}
+<%
+end
+
+local function form() %>
+<html>
+	<head>
+		<title>Lucene Query</title>
+		<style>
+			<% style() %>
+			input {
+				margin-top: 1em;
+			}
+			input[type="text"] {
+				font: inherit;
+				padding: .5em .8em;
+				border-radius: 8px;
+				border-style: groove;
+			}
+			input[type="text"]:focus {
+				border-color: #66afe9;
+				outline: none;
+			}
+			span[tip] {
+				color: #888;
+				font-size: smaller;
+				margin-left: .5em;
+			}
+			input[type="submit"] {
+				color: white;
+				background: #337ab7;
+				border-color: #337ab7;
+				font: inherit;
+				padding: .5em;
+				border-radius: 4px;
+			}
+			input[type="submit"]:hover {
+				background: #236aa7 !important;
+			}
+		</style>
+	</head>
+	<body>
+		<h2>Lucene Query</h2>
+		<form horizontal name="form0" method="post">
+			<div>
+				<label>Query:</label>
+				<input type=text name="query" size="80" autofocus />
+			</div>
+			<div>
+				<label></label>
+				<span tip>Query examples: <i>type:user</i> or <i>+type:user +name:Joe"</i></span>
+			</div>
+			<div>
+				<label>Max Rows:</label>
+				<input type=text name="rows" value="100" size="3" maxlength="5" />
+			</div>
+			<div>
+				<label>Sort:</label>
+				<input type=text name="sort" size="60" />
+			</div>
+			<div>
+				<label></label>
+				<span tip>Sort examples: <i>name, id</i></span>
+			</div>
+			<div>
+				<label></label>
+				<input type="submit" />
+			</div>
+		</form>
+	</body>
+</html>
+<% end
+
+
+local function result(query,sort,headers,table) %>
+<html>
+	<head>
+		<title>Lucene Query</title>
+		<style>
+			<% style() %>
+			table {
+				border-collapse: collapse;
+				width: 100%;
+				font-size: smaller;
+			}
+			th, td {
+				text-align: left;
+				padding: .5em;
+				border: solid 1px #ddd;
+			}
+		</style>
+	</head>
+	<body>
+		<h2>Lucene Query Results</h2>
+		<p><label>Query:</label> <b><%=Html.encode(to_string(query))%></b></p>
+		<p><label>Sort:</label> <b><%=Html.encode(to_string(sort))%></b></p>
+		<table>
+			<tr>
+				<th></th>
+				<% for _, header in ipairs(headers) do %>
+					<th><%=header%></th>
+				<% end %>
+			</tr>
+			<% for i, row in ipairs(table) do %>
+				<tr>
+					<td><%=i%></td>
+					<%
+					for col in range(1, #headers) do
+						local val = row[col]
+						%><td><%= val and repr(val) or "" %></td><%
+					end
+					%>
+				</tr>
+			<% end %>
+		</table>
+	</body>
+</html>
+<% end
+
+
+local function index_of(tbl,val)
+	for i, v in ipairs(tbl) do
+		if v == val then
+			return i
+		end
+	end
+	local n = #tbl + 1
+	tbl[n] = val
+	return n
+end
+
+
+function M.of(index)
+	index or error "index is nil"
+
+	return function()
+		Io.stdout = Http.response.text_writer()
+		local query = Http.request.parameter.query
+		if query == nil then
+			form()
+			return
+		end
+		local rows = string_to_number(Http.request.parameter.rows)
+		local sort = Http.request.parameter.sort
+		local results = index.search(query,1,rows,sort)
+		local headers = {}
+		local table = {}
+		for _, doc in ipairs(results) do
+			local row = {}
+			for field, value in pairs(doc) do
+				row[index_of(headers,field)] = value
+			end
+			table[#table+1] = row
+		end
+		result(query,sort,headers,table)
+	end
+
+end
+
+return M
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/lucene/queryparser/FieldParser.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/lucene/queryparser/FieldParser.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,11 @@
+package luan.modules.lucene.queryparser;
+
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.SortField;
+
+
+public interface FieldParser {
+	public Query getQuery(SaneQueryParser qp,String field,String query) throws ParseException;
+	public Query getRangeQuery(SaneQueryParser qp,String field,String minQuery,String maxQuery,boolean includeMin,boolean includeMax) throws ParseException;
+	public SortField getSortField(SaneQueryParser qp,String field,boolean reverse) throws ParseException;
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/lucene/queryparser/MultiFieldParser.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/lucene/queryparser/MultiFieldParser.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,85 @@
+package luan.modules.lucene.queryparser;
+
+import java.util.Map;
+import java.util.HashMap;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.BooleanQuery;
+import org.apache.lucene.search.BooleanClause;
+import org.apache.lucene.search.SortField;
+
+
+public class MultiFieldParser implements FieldParser {
+
+	/**
+	 * maps field name to FieldParser
+	 */
+	public final Map<String,FieldParser> fields = new HashMap<String,FieldParser>();
+	public boolean allowUnspecifiedFields = false;
+	private final FieldParser defaultFieldParser;
+	private final String[] defaultFields;
+
+	public MultiFieldParser() {
+		this.defaultFieldParser = null;
+		this.defaultFields = null;
+	}
+
+	public MultiFieldParser(FieldParser defaultFieldParser,String... defaultFields) {
+		this.defaultFieldParser = defaultFieldParser;
+		this.defaultFields = defaultFields;
+		for( String field : defaultFields ) {
+			fields.put(field,defaultFieldParser);
+		}
+	}
+
+	@Override public Query getQuery(SaneQueryParser qp,String field,String query) throws ParseException {
+		if( field == null ) {
+			if( defaultFieldParser == null )
+				throw new ParseException(qp,"no defaults were specified, so a field is required");
+			if( defaultFields.length == 1 )
+				return defaultFieldParser.getQuery(qp,defaultFields[0],query);
+			BooleanQuery bq = new BooleanQuery();
+			for( String f : defaultFields ) {
+				bq.add( defaultFieldParser.getQuery(qp,f,query), BooleanClause.Occur.SHOULD );
+			}
+			return bq;
+		} else {
+			FieldParser fp = fields.get(field);
+			if( fp != null )
+				return fp.getQuery(qp,field,query);
+			if( allowUnspecifiedFields )
+				return defaultFieldParser.getQuery(qp,field,query);
+			throw new ParseException(qp,"unrecognized field '"+field+"'");
+		}
+	}
+
+	@Override public Query getRangeQuery(SaneQueryParser qp,String field,String minQuery,String maxQuery,boolean includeMin,boolean includeMax) throws ParseException {
+		if( field == null ) {
+			if( defaultFieldParser == null )
+				throw new ParseException(qp,"no defaults were specified, so a field is required");
+			if( defaultFields.length == 1 )
+				return defaultFieldParser.getRangeQuery(qp,defaultFields[0],minQuery,maxQuery,includeMin,includeMax);
+			BooleanQuery bq = new BooleanQuery();
+			for( String f : defaultFields ) {
+				bq.add( defaultFieldParser.getRangeQuery(qp,f,minQuery,maxQuery,includeMin,includeMax), BooleanClause.Occur.SHOULD );
+			}
+			return bq;
+		} else {
+			FieldParser fp = fields.get(field);
+			if( fp != null )
+				return fp.getRangeQuery(qp,field,minQuery,maxQuery,includeMin,includeMax);
+			if( allowUnspecifiedFields )
+				return defaultFieldParser.getRangeQuery(qp,field,minQuery,maxQuery,includeMin,includeMax);
+			throw new ParseException(qp,"field '"+field+"' not specified");
+		}
+	}
+
+	@Override public SortField getSortField(SaneQueryParser qp,String field,boolean reverse) throws ParseException {
+		FieldParser fp = fields.get(field);
+		if( fp != null )
+			return fp.getSortField(qp,field,reverse);
+		if( allowUnspecifiedFields )
+			return defaultFieldParser.getSortField(qp,field,reverse);
+		throw new ParseException(qp,"field '"+field+"' not specified");
+	}
+
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/lucene/queryparser/NumberFieldParser.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/lucene/queryparser/NumberFieldParser.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,83 @@
+package luan.modules.lucene.queryparser;
+
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.NumericRangeQuery;
+import org.apache.lucene.search.SortField;
+
+
+public abstract class NumberFieldParser implements FieldParser {
+
+	@Override public final Query getQuery(SaneQueryParser qp,String field,String query) throws ParseException {
+		return getRangeQuery(qp,field,query,query,true,true);
+	}
+
+	@Override public final Query getRangeQuery(SaneQueryParser qp,String field,String minQuery,String maxQuery,boolean includeMin,boolean includeMax) throws ParseException {
+		try {
+			return getRangeQuery(field,minQuery,maxQuery,includeMin,includeMax);
+		} catch(NumberFormatException e) {
+			throw new ParseException(qp,e);
+		}
+	}
+
+	abstract protected Query getRangeQuery(String field,String minQuery,String maxQuery,boolean includeMin,boolean includeMax);
+
+	@Override public SortField getSortField(SaneQueryParser qp,String field,boolean reverse) {
+		return new SortField( field, sortType(), reverse );
+	}
+
+	abstract protected SortField.Type sortType();
+
+
+	public static final FieldParser INT = new NumberFieldParser() {
+
+		@Override protected Query getRangeQuery(String field,String minQuery,String maxQuery,boolean includeMin,boolean includeMax) {
+			int min = Integer.parseInt(minQuery);
+			int max = Integer.parseInt(maxQuery);
+			return NumericRangeQuery.newIntRange(field,min,max,includeMin,includeMax);
+		}
+
+		@Override protected SortField.Type sortType() {
+			return SortField.Type.INT;
+		}
+	};
+
+	public static final FieldParser LONG = new NumberFieldParser() {
+
+		@Override protected Query getRangeQuery(String field,String minQuery,String maxQuery,boolean includeMin,boolean includeMax) {
+			long min = Long.parseLong(minQuery);
+			long max = Long.parseLong(maxQuery);
+			return NumericRangeQuery.newLongRange(field,min,max,includeMin,includeMax);
+		}
+
+		@Override protected SortField.Type sortType() {
+			return SortField.Type.LONG;
+		}
+	};
+
+	public static final FieldParser FLOAT = new NumberFieldParser() {
+
+		@Override protected Query getRangeQuery(String field,String minQuery,String maxQuery,boolean includeMin,boolean includeMax) {
+			float min = Float.parseFloat(minQuery);
+			float max = Float.parseFloat(maxQuery);
+			return NumericRangeQuery.newFloatRange(field,min,max,includeMin,includeMax);
+		}
+
+		@Override protected SortField.Type sortType() {
+			return SortField.Type.FLOAT;
+		}
+	};
+
+	public static final FieldParser DOUBLE = new NumberFieldParser() {
+
+		@Override protected Query getRangeQuery(String field,String minQuery,String maxQuery,boolean includeMin,boolean includeMax) {
+			double min = Double.parseDouble(minQuery);
+			double max = Double.parseDouble(maxQuery);
+			return NumericRangeQuery.newDoubleRange(field,min,max,includeMin,includeMax);
+		}
+
+		@Override protected SortField.Type sortType() {
+			return SortField.Type.DOUBLE;
+		}
+	};
+
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/lucene/queryparser/ParseException.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/lucene/queryparser/ParseException.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,33 @@
+package luan.modules.lucene.queryparser;
+
+
+public final class ParseException extends Exception {
+	public final String text;
+	public final int errorIndex;
+	public final int highIndex;
+
+	public ParseException(SaneQueryParser qp) {
+		this(qp,"Invalid input");
+	}
+
+	public ParseException(SaneQueryParser qp,String msg) {
+		super(msg+" at position "+(qp.parser.errorIndex()+1));
+		Parser parser = qp.parser;
+		this.text = parser.text;
+		this.errorIndex = parser.errorIndex();
+		this.highIndex = parser.highIndex();
+	}
+
+	public ParseException(SaneQueryParser qp,Exception cause) {
+		this(qp,cause.getMessage(),cause);
+	}
+
+	public ParseException(SaneQueryParser qp,String msg,Exception cause) {
+		super(msg+" at position "+(qp.parser.errorIndex()+1),cause);
+		Parser parser = qp.parser;
+		this.text = parser.text;
+		this.errorIndex = parser.errorIndex();
+		this.highIndex = parser.highIndex();
+	}
+
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/lucene/queryparser/Parser.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/lucene/queryparser/Parser.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,155 @@
+package luan.modules.lucene.queryparser;
+
+/**
+ * A general parser utility class.
+ * This class is not needed to use SaneQueryParser.
+ */
+public class Parser {
+	public final String text;
+	private final int len;
+	private int[] stack = new int[256];
+	private int frame = 0;
+	private int iHigh;
+
+	Parser(String text) {
+		this.text = text;
+		this.len = text.length();
+	}
+
+	private int i() {
+		return stack[frame];
+	}
+
+	private void i(int i) {
+		stack[frame] += i;
+		if( iHigh < stack[frame] )
+			iHigh = stack[frame];
+	}
+
+	public int begin() {
+		frame++;
+		if( frame == stack.length ) {
+			int[] a = new int[2*frame];
+			System.arraycopy(stack,0,a,0,frame);
+			stack = a;
+		}
+		stack[frame] = stack[frame-1];
+		return i();
+	}
+
+	public void rollback() {
+		stack[frame] = stack[frame-1];
+	}
+
+	public <T> T success(T t) {
+		success();
+		return t;
+	}
+
+	public boolean success() {
+		frame--;
+		stack[frame] = stack[frame+1];
+		return true;
+	}
+
+	public <T> T failure(T t) {
+		failure();
+		return t;
+	}
+
+	public boolean failure() {
+		frame--;
+		return false;
+	}
+
+	public int currentIndex() {
+		return i();
+	}
+
+	public int errorIndex() {
+		return frame > 0 ? stack[frame-1] : 0;
+	}
+
+	public int highIndex() {
+		return iHigh;
+	}
+
+	public char lastChar() {
+		return text.charAt(i()-1);
+	}
+
+	public char currentChar() {
+		return text.charAt(i());
+	}
+
+	public boolean endOfInput() {
+		return i() >= len;
+	}
+
+	public boolean match(char c) {
+		if( endOfInput() || text.charAt(i()) != c )
+			return false;
+		i(1);
+		return true;
+	}
+
+	public boolean match(String s) {
+		int n = s.length();
+		if( !text.regionMatches(i(),s,0,n) )
+			return false;
+		i(n);
+		return true;
+	}
+
+	public boolean matchIgnoreCase(String s) {
+		int n = s.length();
+		if( !text.regionMatches(true,i(),s,0,n) )
+			return false;
+		i(n);
+		return true;
+	}
+
+	public boolean anyOf(String s) {
+		if( endOfInput() || s.indexOf(text.charAt(i())) == -1 )
+			return false;
+		i(1);
+		return true;
+	}
+
+	public boolean noneOf(String s) {
+		if( endOfInput() || s.indexOf(text.charAt(i())) != -1 )
+			return false;
+		i(1);
+		return true;
+	}
+
+	public boolean inCharRange(char cLow, char cHigh) {
+		if( endOfInput() )
+			return false;
+		char c = text.charAt(i());
+		if( !(cLow <= c && c <= cHigh) )
+			return false;
+		i(1);
+		return true;
+	}
+
+	public boolean anyChar() {
+		if( endOfInput() )
+			return false;
+		i(1);
+		return true;
+	}
+
+	public boolean test(char c) {
+		return !endOfInput() && text.charAt(i()) == c;
+	}
+
+	public boolean test(String s) {
+		return text.regionMatches(i(),s,0,s.length());
+	}
+
+	public String textFrom(int start) {
+		return text.substring(start,i());
+	}
+
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/lucene/queryparser/SaneQueryParser.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/lucene/queryparser/SaneQueryParser.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,247 @@
+package luan.modules.lucene.queryparser;
+
+import java.util.List;
+import java.util.ArrayList;
+import java.util.regex.Pattern;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.MatchAllDocsQuery;
+import org.apache.lucene.search.BooleanClause;
+import org.apache.lucene.search.BooleanQuery;
+import org.apache.lucene.search.Sort;
+import org.apache.lucene.search.SortField;
+
+
+public class SaneQueryParser {
+
+	public static Query parseQuery(FieldParser fieldParser,String query) throws ParseException {
+		return new SaneQueryParser(fieldParser,query).parseQuery();
+	}
+
+	private static Pattern specialChar = Pattern.compile("[ \\t\\r\\n\":\\[\\]{}^+\\-(),?*\\\\]");
+
+	public static String literal(String s) {
+		return specialChar.matcher(s).replaceAll("\\\\$0");
+	}
+
+	public static Sort parseSort(FieldParser fieldParser,String sort) throws ParseException {
+		return new SaneQueryParser(fieldParser,sort).parseSort();
+	}
+
+
+	private static final String NOT_IN_TERM = " \t\r\n\":[]{}^+-()";
+	private static final String NOT_IN_FIELD = NOT_IN_TERM + ",";
+	private final FieldParser fieldParser;
+	final Parser parser;
+
+	private SaneQueryParser(FieldParser fieldParser,String query) {
+		this.fieldParser = fieldParser;
+		this.parser = new Parser(query);
+	}
+
+	private Query parseQuery() throws ParseException {
+		Spaces();
+		BooleanQuery bq = new BooleanQuery();
+		while( !parser.endOfInput() ) {
+			bq.add( Term(null) );
+		}
+		BooleanClause[] clauses = bq.getClauses();
+		switch( clauses.length ) {
+		case 0:
+			return new MatchAllDocsQuery();
+		case 1:
+			{
+				BooleanClause bc = clauses[0];
+				if( bc.getOccur() != BooleanClause.Occur.MUST_NOT )
+					return bc.getQuery();
+			}
+		default:
+			return bq;
+		}
+	}
+
+	private BooleanClause Term(String defaultField) throws ParseException {
+		BooleanClause.Occur occur;
+		if( parser.match('+') ) {
+			occur = BooleanClause.Occur.MUST;
+			Spaces();
+		} else if( parser.match('-') ) {
+			occur = BooleanClause.Occur.MUST_NOT;
+			Spaces();
+		} else {
+			occur = BooleanClause.Occur.SHOULD;
+		}
+		String field = QueryField();
+		if( field == null )
+			field = defaultField;
+		Query query = NestedTerm(field);
+		if( query == null )
+			query = RangeTerm(field);
+		if( query == null ) {
+			parser.begin();
+			String match = SimpleTerm();
+			query = fieldParser.getQuery(this,field,match);
+			parser.success();
+		}
+		if( parser.match('^') ) {
+			Spaces();
+			int start = parser.begin();
+			try {
+				while( parser.anyOf("0123456789.") );
+				String match = parser.textFrom(start);
+				float boost = Float.parseFloat(match);
+				query.setBoost(boost);
+			} catch(NumberFormatException e) {
+				throw new ParseException(this,e);
+			}
+			parser.success();
+			Spaces();
+		}
+		BooleanClause bc = new BooleanClause(query,occur);
+		return bc;
+	}
+
+	private Query NestedTerm(String field) throws ParseException {
+		parser.begin();
+		if( !parser.match('(') )
+			return parser.failure(null);
+		BooleanQuery bq = new BooleanQuery();
+		while( !parser.match(')') ) {
+			if( parser.endOfInput() )
+				throw new ParseException(this,"unclosed parentheses");
+			bq.add( Term(field) );
+		}
+		Spaces();
+		BooleanClause[] clauses = bq.getClauses();
+		switch( clauses.length ) {
+		case 0:
+			throw new ParseException(this,"empty parentheses");
+		case 1:
+			{
+				BooleanClause bc = clauses[0];
+				if( bc.getOccur() != BooleanClause.Occur.MUST_NOT )
+					return parser.success(bc.getQuery());
+			}
+		default:
+			return parser.success(bq);
+		}
+	}
+
+	private Query RangeTerm(String field) throws ParseException {
+		parser.begin();
+		if( !parser.anyOf("[{") )
+			return parser.failure(null);
+		boolean includeMin = parser.lastChar() == '[';
+		Spaces();
+		String minQuery = SimpleTerm();
+		TO();
+		String maxQuery = SimpleTerm();
+		if( !parser.anyOf("]}") )
+			throw new ParseException(this,"unclosed range");
+		boolean includeMax = parser.lastChar() == ']';
+		Spaces();
+		Query query = fieldParser.getRangeQuery(this,field,minQuery,maxQuery,includeMin,includeMax);
+		return parser.success(query);
+	}
+
+	private void TO() throws ParseException {
+		parser.begin();
+		if( !(parser.match("TO") && Space()) )
+			throw new ParseException(this,"'TO' expected");
+		Spaces();
+		parser.success();
+	}
+
+	private String SimpleTerm() throws ParseException {
+		parser.begin();
+		String match;
+		if( parser.match('"') ) {
+			int start = parser.currentIndex() - 1;
+			while( !parser.match('"') ) {
+				if( parser.endOfInput() )
+					throw new ParseException(this,"unclosed quotes");
+				parser.anyChar();
+				checkEscape();
+			}
+			match = parser.textFrom(start);
+			Spaces();
+		} else {
+			match = Unquoted(NOT_IN_TERM);
+		}
+		if( match.length() == 0 )
+			throw new ParseException(this);
+		return parser.success(match);
+	}
+
+	private String QueryField() throws ParseException {
+		parser.begin();
+		String match = Field();
+		if( match==null || !parser.match(':') )
+			return parser.failure((String)null);
+		Spaces();
+		return parser.success(match);
+	}
+
+	private String Field() throws ParseException {
+		parser.begin();
+		String match = Unquoted(NOT_IN_FIELD);
+		if( match.length()==0 )
+			return parser.failure((String)null);
+		match = StringFieldParser.escape(this,match);
+		return parser.success(match);
+	}
+
+	private String Unquoted(String exclude) throws ParseException {
+		int start = parser.begin();
+		while( parser.noneOf(exclude) ) {
+			checkEscape();
+		}
+		String match = parser.textFrom(start);
+		Spaces();
+		return parser.success(match);
+	}
+
+	private void checkEscape() {
+		if( parser.lastChar() == '\\' )
+			parser.anyChar();
+	}
+
+	private void Spaces() {
+		while( Space() );
+	}
+
+	private boolean Space() {
+		return parser.anyOf(" \t\r\n");
+	}
+
+
+	// sort
+
+	private Sort parseSort() throws ParseException {
+		Spaces();
+		if( parser.endOfInput() )
+			return null;
+		List<SortField> list = new ArrayList<SortField>();
+		list.add( SortField() );
+		while( !parser.endOfInput() ) {
+			parser.begin();
+			if( !parser.match(',') )
+				throw new ParseException(this,"',' expected");
+			Spaces();
+			parser.success();
+			list.add( SortField() );
+		}
+		return new Sort(list.toArray(new SortField[0]));
+	}
+
+	private SortField SortField() throws ParseException {
+		parser.begin();
+		String field = Field();
+		if( field==null )
+			throw new ParseException(this);
+		boolean reverse = !parser.matchIgnoreCase("asc") && parser.matchIgnoreCase("desc");
+		Spaces();
+		SortField sf = fieldParser.getSortField(this,field,reverse);
+		return parser.success(sf);
+	}
+
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/lucene/queryparser/StringFieldParser.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/lucene/queryparser/StringFieldParser.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,112 @@
+package luan.modules.lucene.queryparser;
+
+import java.io.StringReader;
+import java.io.IOException;
+import org.apache.lucene.analysis.Analyzer;
+import org.apache.lucene.analysis.TokenStream;
+import org.apache.lucene.analysis.tokenattributes.CharTermAttribute;
+import org.apache.lucene.analysis.tokenattributes.PositionIncrementAttribute;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.TermQuery;
+import org.apache.lucene.search.TermRangeQuery;
+import org.apache.lucene.search.PhraseQuery;
+import org.apache.lucene.search.WildcardQuery;
+import org.apache.lucene.search.PrefixQuery;
+import org.apache.lucene.search.SortField;
+import org.apache.lucene.index.Term;
+
+
+public class StringFieldParser implements FieldParser {
+	public int slop = 0;
+	public final Analyzer analyzer;
+
+	public StringFieldParser(Analyzer analyzer) {
+		this.analyzer = analyzer;
+	}
+
+	@Override public Query getQuery(SaneQueryParser qp,String field,String query) throws ParseException {
+		String wildcard = wildcard(qp,query);
+		if( wildcard != null )
+			return new WildcardQuery(new Term(field,wildcard));
+		if( query.endsWith("*") && !query.endsWith("\\*") )
+			return new PrefixQuery(new Term(field,query.substring(0,query.length()-1)));
+		query = escape(qp,query);
+		PhraseQuery pq = new PhraseQuery();
+		try {
+			TokenStream ts = analyzer.tokenStream(field,new StringReader(query));
+			CharTermAttribute termAttr = ts.addAttribute(CharTermAttribute.class);
+			PositionIncrementAttribute posAttr = ts.addAttribute(PositionIncrementAttribute.class);
+			ts.reset();
+			int pos = -1;
+			while( ts.incrementToken() ) {
+				pos += posAttr.getPositionIncrement();
+				pq.add( new Term(field,termAttr.toString()), pos );
+			}
+			ts.end();
+			ts.close();
+		} catch(IOException e) {
+			throw new RuntimeException(e);
+		}
+		Term[] terms = pq.getTerms();
+		if( terms.length==1 && pq.getPositions()[0]==0 )
+			return new TermQuery(terms[0]);
+		return pq;
+	}
+
+	@Override public Query getRangeQuery(SaneQueryParser qp,String field,String minQuery,String maxQuery,boolean includeMin,boolean includeMax) throws ParseException {
+		minQuery = escape(qp,minQuery);
+		maxQuery = escape(qp,maxQuery);
+		return TermRangeQuery.newStringRange(field,minQuery,maxQuery,includeMin,includeMax);
+	}
+
+	static String escape(SaneQueryParser qp,String s) throws ParseException {
+		final char[] a = s.toCharArray();
+		int i, n;
+		if( a[0] == '"' ) {
+			if( a[a.length-1] != '"' )  throw new RuntimeException();
+			i = 1;
+			n = a.length - 1;
+		} else {
+			i = 0;
+			n = a.length;
+		}
+		StringBuilder sb = new StringBuilder();
+		for( ; i<n; i++ ) {
+			char c = a[i];
+			if( c == '\\' ) {
+				if( ++i == a.length )
+					throw new ParseException(qp,"ends with '\\'");
+				c = a[i];
+			}
+			sb.append(c);
+		}
+		return sb.toString();
+	}
+
+	private static String wildcard(SaneQueryParser qp,String s) throws ParseException {
+		final char[] a = s.toCharArray();
+		if( a[0] == '"' )
+			return null;
+		boolean hasWildcard = false;
+		StringBuilder sb = new StringBuilder();
+		for( int i=0; i<a.length; i++ ) {
+			char c = a[i];
+			if( c=='?' || c=='*' && i<a.length-1 )
+				hasWildcard = true;
+			if( c == '\\' ) {
+				if( ++i == a.length )
+					throw new ParseException(qp,"ends with '\\'");
+				c = a[i];
+				if( c=='?' || c=='*' )
+					sb.append('\\');
+			}
+			sb.append(c);
+		}
+		return hasWildcard ? sb.toString() : null;
+	}
+
+	@Override public SortField getSortField(SaneQueryParser qp,String field,boolean reverse) {
+		return new SortField( field, SortField.Type.STRING, reverse );
+	}
+
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/lucene/queryparser/SynonymParser.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/lucene/queryparser/SynonymParser.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,42 @@
+package luan.modules.lucene.queryparser;
+
+import java.util.Map;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.BooleanQuery;
+import org.apache.lucene.search.BooleanClause;
+import org.apache.lucene.search.SortField;
+
+
+public class SynonymParser implements FieldParser {
+	private final FieldParser fp;
+	private final Map<String,String[]> synonymMap;
+
+	public SynonymParser(FieldParser fp,Map<String,String[]> synonymMap) {
+		this.fp = fp;
+		this.synonymMap = synonymMap;
+	}
+
+	protected String[] getSynonyms(String query) {
+		return synonymMap.get(query);
+	}
+
+	public Query getQuery(SaneQueryParser qp,String field,String query) throws ParseException {
+		String[] synonyms = getSynonyms(query);
+		if( synonyms == null )
+			return fp.getQuery(qp,field,query);
+		BooleanQuery bq = new BooleanQuery();
+		bq.add( fp.getQuery(qp,field,query), BooleanClause.Occur.SHOULD );
+		for( String s : synonyms ) {
+			bq.add( fp.getQuery(qp,field,s), BooleanClause.Occur.SHOULD );
+		}
+		return bq;
+	}
+
+	public Query getRangeQuery(SaneQueryParser qp,String field,String minQuery,String maxQuery,boolean includeMin,boolean includeMax) throws ParseException {
+		return fp.getRangeQuery(qp,field,minQuery,maxQuery,includeMin,includeMax);
+	}
+
+	public SortField getSortField(SaneQueryParser qp,String field,boolean reverse) throws ParseException {
+		return fp.getSortField(qp,field,reverse);
+	}
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/mail/Mail.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/mail/Mail.luan	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,17 @@
+java()
+local Luan = require "luan:Luan.luan"
+local assert_table = Luan.assert_table
+local System = require "java:java.lang.System"
+local SmtpCon = require "java:luan.modules.mail.SmtpCon"
+
+local M = {}
+
+System.setProperty( "mail.mime.charset", "UTF-8" )
+
+function M.Sender(params)
+	assert_table(params)
+	local smtpCon = SmtpCon.new(params)
+	return { send = smtpCon.send }
+end
+
+return M
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/mail/SmtpCon.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/mail/SmtpCon.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,187 @@
+package luan.modules.mail;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.Properties;
+import javax.mail.Authenticator;
+import javax.mail.PasswordAuthentication;
+import javax.mail.Session;
+import javax.mail.Transport;
+import javax.mail.Message;
+import javax.mail.MessagingException;
+import javax.mail.Part;
+import javax.mail.internet.MimeMessage;
+import javax.mail.internet.MimeMultipart;
+import javax.mail.internet.MimeBodyPart;
+import luan.Luan;
+import luan.LuanState;
+import luan.LuanTable;
+import luan.LuanException;
+
+
+public final class SmtpCon {
+	private final Session session;
+
+	public SmtpCon(LuanState luan,LuanTable paramsTbl) throws LuanException {
+		Map<Object,Object> params = new HashMap<Object,Object>(paramsTbl.asMap(luan));
+		Properties props = new Properties(System.getProperties());
+
+		String host = getString(params,"host");
+		if( host==null )
+			throw new LuanException( "parameter 'host' is required" );
+		props.setProperty("mail.smtp.host",host);
+
+		Object port = params.remove("port");
+		if( port != null ) {
+			String s;
+			if( port instanceof String ) {
+				s = (String)port;
+			} else if( port instanceof Number ) {
+				Integer i = Luan.asInteger(port);
+				if( i == null )
+					throw new LuanException( "parameter 'port' must be an integer" );
+				s = i.toString();
+			} else {
+				throw new LuanException( "parameter 'port' must be an integer" );
+			}
+			props.setProperty("mail.smtp.socketFactory.port", s);
+			props.setProperty("mail.smtp.port", s);
+		}
+
+		String username = getString(params,"username");
+		if( username == null ) {
+			session = Session.getInstance(props);
+		} else {
+			String password = getString(params,"password");
+			if( password==null )
+				throw new LuanException( "parameter 'password' is required with 'username'" );
+			props.setProperty("mail.smtp.auth","true");
+			final PasswordAuthentication pa = new PasswordAuthentication(username,password);
+			Authenticator auth = new Authenticator() {
+				protected PasswordAuthentication getPasswordAuthentication() {
+					return pa;
+				}
+			};
+			session = Session.getInstance(props,auth);
+		}
+
+		if( !params.isEmpty() )
+			throw new LuanException( "unrecognized parameters: "+params );
+	}
+
+	private String getString(Map<Object,Object> params,String key) throws LuanException {
+		Object val = params.remove(key);
+		if( val!=null && !(val instanceof String) )
+			throw new LuanException( "parameter '"+key+"' must be a string" );
+		return (String)val;
+	}
+
+
+	public void send(LuanState luan,LuanTable mailTbl) throws LuanException {
+		try {
+			Map<Object,Object> mailParams = new HashMap<Object,Object>(mailTbl.asMap(luan));
+			MimeMessage msg = new MimeMessage(session);
+
+			String from = getString(mailParams,"from");
+			if( from != null )
+				msg.setFrom(from);
+
+			String to = getString(mailParams,"to");
+			if( to != null )
+				msg.setRecipients(Message.RecipientType.TO,to);
+
+			String cc = getString(mailParams,"cc");
+			if( cc != null )
+				msg.setRecipients(Message.RecipientType.CC,cc);
+
+			String subject = getString(mailParams,"subject");
+			if( subject != null )
+				msg.setSubject(subject);
+
+			Object body = mailParams.remove("body");
+			Object attachments = mailParams.remove("attachments");
+			Part bodyPart = attachments==null ? msg : new MimeBodyPart();
+
+			if( body != null ) {
+				if( body instanceof String ) {
+					bodyPart.setText((String)body);
+				} else if( body instanceof LuanTable ) {
+					LuanTable bodyTbl = (LuanTable)body;
+					Map<Object,Object> map = new HashMap<Object,Object>(bodyTbl.asMap(luan));
+					MimeMultipart mp = new MimeMultipart("alternative");
+					String text = (String)map.remove("text");
+					if( text != null ) {
+						MimeBodyPart part = new MimeBodyPart();
+						part.setText(text);
+						mp.addBodyPart(part);
+					}
+					String html = (String)map.remove("html");
+					if( html != null ) {
+						MimeBodyPart part = new MimeBodyPart();
+						part.setContent(html,"text/html");
+						mp.addBodyPart(part);
+					}
+					if( !map.isEmpty() )
+						throw new LuanException( "invalid body types: " + map );
+					bodyPart.setContent(mp);
+				} else
+					throw new LuanException( "parameter 'body' must be a string or table" );
+			}
+
+			if( attachments != null ) {
+				if( !(attachments instanceof LuanTable) )
+					throw new LuanException( "parameter 'attachments' must be a table" );
+				LuanTable attachmentsTbl = (LuanTable)attachments;
+				if( !attachmentsTbl.isList() )
+					throw new LuanException( "parameter 'attachments' must be a list" );
+				MimeMultipart mp = new MimeMultipart("mixed");
+				if( body != null )
+					mp.addBodyPart((MimeBodyPart)bodyPart);
+				for( Object attachment : attachmentsTbl.asList() ) {
+					if( !(attachment instanceof LuanTable) )
+						throw new LuanException( "each attachment must be a table" );
+					Map<Object,Object> attachmentMap = new HashMap<Object,Object>(((LuanTable)attachment).asMap(luan));
+					Object obj;
+
+					obj = attachmentMap.remove("filename");
+					if( obj==null )
+						throw new LuanException( "an attachment is missing 'filename'" );
+					if( !(obj instanceof String) )
+						throw new LuanException( "an attachment filename must be a string" );
+					String filename = (String)obj;
+
+					obj = attachmentMap.remove("content_type");
+					if( obj==null )
+						throw new LuanException( "an attachment is missing 'content_type'" );
+					if( !(obj instanceof String) )
+						throw new LuanException( "an attachment content_type must be a string" );
+					String content_type = (String)obj;
+
+					Object content = attachmentMap.remove("content");
+					if( content==null )
+						throw new LuanException( "an attachment is missing 'content'" );
+					if( content_type.startsWith("text/") && content instanceof byte[] )
+						content = new String((byte[])content);
+
+					if( !attachmentMap.isEmpty() )
+						throw new LuanException( "unrecognized attachment parameters: "+attachmentMap );
+
+					MimeBodyPart part = new MimeBodyPart();
+					part.setContent(content,content_type);
+					part.setFileName(filename);
+					mp.addBodyPart(part);
+				}
+				msg.setContent(mp);
+			}
+
+			if( !mailParams.isEmpty() )
+				throw new LuanException( "unrecognized parameters: "+mailParams );
+
+			Transport.send(msg);
+		} catch(MessagingException e) {
+			throw new LuanException(e);
+		}
+	}
+
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/mmake.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/mmake.luan	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,59 @@
+local Luan = require "luan:Luan.luan"
+local ipairs = Luan.ipairs
+local Table = require "luan:Table.luan"
+local Io = require "luan:Io.luan"
+local print = Io.print
+local String = require "luan:String.luan"
+local Time = require "luan:Time.luan"
+
+
+local compiler = Table.concat( { "javac -g -encoding UTF8", ... }, " " )
+
+
+local function header()
+	return 	%>
+# Makefile created on <%=Time.format(Time.now())%> by Mmake
+
+.SUFFIXES: .java .class
+
+.java.class:
+	<%=compiler%> '$<'
+
+all: <%
+end
+
+
+local function mmake(dir)
+	local javas = {}
+	local dirs = {}
+	for _, file in ipairs(dir.children()) do
+		local name = file.name()
+		if String.matches(name,[[\.java$]]) then
+			javas[#javas+1] = String.sub(name,1,-6)
+		end
+		if file.is_directory() and mmake(file) then
+			dirs[#dirs+1] = name
+		end
+	end
+	if #javas == 0 and #dirs == 0 then
+		return false;
+	end
+	local out = dir.child("Makefile").text_writer()
+	out.write( header() )
+	for _, s in ipairs(javas) do
+		s = String.gsub(s,[[\$]],[[\$\$]])
+		out.write( "\\\n\t\t",  s , ".class" )
+	end
+	for _, s in ipairs(dirs) do
+		out.write( "\n\tcd ", s, ";  make all" )
+	end
+	out.write "\n\nclean:\n\trm -f *.class\n"
+	for _, s in ipairs(dirs) do
+		out.write( "\tcd ", s, ";  make clean\n" )
+	end
+	out.close()
+	print(dir.to_string())
+	return true
+end
+
+mmake(Io.schemes.file ".")
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/parsers/BBCode.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/parsers/BBCode.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,311 @@
+package luan.modules.parsers;
+
+import java.util.List;
+import java.util.ArrayList;
+import luan.Luan;
+import luan.LuanState;
+import luan.LuanFunction;
+import luan.LuanException;
+import luan.modules.Utils;
+
+
+public final class BBCode {
+
+	public static String toHtml(LuanState luan,String bbcode,LuanFunction quoter) throws LuanException {
+		return new BBCode(luan,bbcode,quoter,true).parse();
+	}
+
+	public static String toText(LuanState luan,String bbcode,LuanFunction quoter) throws LuanException {
+		return new BBCode(luan,bbcode,quoter,false).parse();
+	}
+
+	private final LuanState luan;
+	private final Parser parser;
+	private final LuanFunction quoter;
+	private final boolean toHtml;
+
+	private BBCode(LuanState luan,String text,LuanFunction quoter,boolean toHtml) throws LuanException {
+		Utils.checkNotNull(text,1);
+		Utils.checkNotNull(quoter,2);
+		this.luan = luan;
+		this.parser = new Parser(text);
+		this.quoter = quoter;
+		this.toHtml = toHtml;
+	}
+
+	private String parse() throws LuanException {
+		StringBuilder sb = new StringBuilder();
+		while( !parser.endOfInput() ) {
+			String block = parseBlock();
+			if( block != null )
+				sb.append(block);
+			else {
+				sb.append( parser.currentChar() );
+				parser.anyChar();
+			}
+		}
+		return sb.toString();
+	}
+
+	private String parseWellFormed() throws LuanException {
+		StringBuilder sb = new StringBuilder();
+		while( !parser.endOfInput() ) {
+			String block = parseBlock();
+			if( block != null ) {
+				sb.append(block);
+				continue;
+			}
+			if( couldBeTag() )
+				break;
+			sb.append( parser.currentChar() );
+			parser.anyChar();
+		}
+		return sb.toString();
+	}
+
+	private boolean couldBeTag() {
+		if( parser.currentChar() != '[' )
+			return false;
+		return parser.testIgnoreCase("[b]")
+			|| parser.testIgnoreCase("[/b]")
+			|| parser.testIgnoreCase("[i]")
+			|| parser.testIgnoreCase("[/i]")
+			|| parser.testIgnoreCase("[u]")
+			|| parser.testIgnoreCase("[/u]")
+			|| parser.testIgnoreCase("[url]")
+			|| parser.testIgnoreCase("[url=")
+			|| parser.testIgnoreCase("[/url]")
+			|| parser.testIgnoreCase("[code]")
+			|| parser.testIgnoreCase("[/code]")
+			|| parser.testIgnoreCase("[img]")
+			|| parser.testIgnoreCase("[/img]")
+			|| parser.testIgnoreCase("[color=")
+			|| parser.testIgnoreCase("[/color]")
+			|| parser.testIgnoreCase("[size=")
+			|| parser.testIgnoreCase("[/size]")
+			|| parser.testIgnoreCase("[youtube]")
+			|| parser.testIgnoreCase("[/youtube]")
+			|| parser.testIgnoreCase("[quote]")
+			|| parser.testIgnoreCase("[quote=")
+			|| parser.testIgnoreCase("[/quote]")
+		;
+	}
+
+	private String parseBlock() throws LuanException {
+		if( parser.currentChar() != '[' )
+			return null;
+		String s;
+		s = parseB();  if(s!=null) return s;
+		s = parseI();  if(s!=null) return s;
+		s = parseU();  if(s!=null) return s;
+		s = parseUrl1();  if(s!=null) return s;
+		s = parseUrl2();  if(s!=null) return s;
+		s = parseCode();  if(s!=null) return s;
+		s = parseImg();  if(s!=null) return s;
+		s = parseColor();  if(s!=null) return s;
+		s = parseSize();  if(s!=null) return s;
+		s = parseYouTube();  if(s!=null) return s;
+		s = parseQuote1();  if(s!=null) return s;
+		s = parseQuote2();  if(s!=null) return s;
+		return null;
+	}
+
+	private String parseB() throws LuanException {
+		parser.begin();
+		if( !parser.matchIgnoreCase("[b]") )
+			return parser.failure(null);
+		String content = parseWellFormed();
+		if( !parser.matchIgnoreCase("[/b]") )
+			return parser.failure(null);
+		String rtn = toHtml ? "<b>"+content+"</b>" : content;
+		return parser.success(rtn);
+	}
+
+	private String parseI() throws LuanException {
+		parser.begin();
+		if( !parser.matchIgnoreCase("[i]") )
+			return parser.failure(null);
+		String content = parseWellFormed();
+		if( !parser.matchIgnoreCase("[/i]") )
+			return parser.failure(null);
+		String rtn = toHtml ? "<i>"+content+"</i>" : content;
+		return parser.success(rtn);
+	}
+
+	private String parseU() throws LuanException {
+		parser.begin();
+		if( !parser.matchIgnoreCase("[u]") )
+			return parser.failure(null);
+		String content = parseWellFormed();
+		if( !parser.matchIgnoreCase("[/u]") )
+			return parser.failure(null);
+		String rtn = toHtml ? "<u>"+content+"</u>" : content;
+		return parser.success(rtn);
+	}
+
+	private String parseUrl1() {
+		parser.begin();
+		if( !parser.matchIgnoreCase("[url]") )
+			return parser.failure(null);
+		String url = parseRealUrl();
+		if( !parser.matchIgnoreCase("[/url]") )
+			return parser.failure(null);
+		String rtn = toHtml ? "<a href='"+url+"'>"+url+"</u>" : url;
+		return parser.success(rtn);
+	}
+
+	private String parseUrl2() throws LuanException {
+		parser.begin();
+		if( !parser.matchIgnoreCase("[url=") )
+			return parser.failure(null);
+		String url = parseRealUrl();
+		if( !parser.match(']') )
+			return parser.failure(null);
+		String content = parseWellFormed();
+		if( !parser.matchIgnoreCase("[/url]") )
+			return parser.failure(null);
+		String rtn = toHtml ? "<a href='"+url+"'>"+content+"</u>" : content;
+		return parser.success(rtn);
+	}
+
+	private String parseRealUrl() {
+		parser.begin();
+		while( parser.match(' ') );
+		int start = parser.currentIndex();
+		if( !parser.matchIgnoreCase("http") )
+			return parser.failure(null);
+		parser.matchIgnoreCase("s");
+		if( !parser.matchIgnoreCase("://") )
+			return parser.failure(null);
+		while( parser.noneOf(" []'") );
+		String url = parser.textFrom(start);
+		while( parser.match(' ') );
+		return parser.success(url);
+	}
+
+	private String parseCode() {
+		parser.begin();
+		if( !parser.matchIgnoreCase("[code]") )
+			return parser.failure(null);
+		int start = parser.currentIndex();
+		while( !parser.testIgnoreCase("[/code]") ) {
+			if( !parser.anyChar() )
+				return parser.failure(null);
+		}
+		String content = parser.textFrom(start);
+		if( !parser.matchIgnoreCase("[/code]") ) throw new RuntimeException();
+		String rtn = toHtml ? "<code>"+content+"</code>" : content;
+		return parser.success(rtn);
+	}
+
+	private String parseImg() {
+		parser.begin();
+		if( !parser.matchIgnoreCase("[img]") )
+			return parser.failure(null);
+		String url = parseRealUrl();
+		if( !parser.matchIgnoreCase("[/img]") )
+			return parser.failure(null);
+		String rtn = toHtml ? "<img src='"+url+"'>" : "";
+		return parser.success(rtn);
+	}
+
+	private String parseColor() throws LuanException {
+		parser.begin();
+		if( !parser.matchIgnoreCase("[color=") )
+			return parser.failure(null);
+		int start = parser.currentIndex();
+		parser.match('#');
+		while( parser.inCharRange('0','9')
+			|| parser.inCharRange('a','z')
+			|| parser.inCharRange('A','Z')
+		);
+		String color = parser.textFrom(start);
+		if( !parser.match(']') )
+			return parser.failure(null);
+		String content = parseWellFormed();
+		if( !parser.matchIgnoreCase("[/color]") )
+			return parser.failure(null);
+		String rtn = toHtml ? "<span style='color: "+color+"'>"+content+"</span>" : content;
+		return parser.success(rtn);
+	}
+
+	private String parseSize() throws LuanException {
+		parser.begin();
+		if( !parser.matchIgnoreCase("[size=") )
+			return parser.failure(null);
+		int start = parser.currentIndex();
+		while( parser.match('.') || parser.inCharRange('0','9') );
+		String size = parser.textFrom(start);
+		if( !parser.match(']') )
+			return parser.failure(null);
+		String content = parseWellFormed();
+		if( !parser.matchIgnoreCase("[/size]") )
+			return parser.failure(null);
+		String rtn = toHtml ? "<span style='font-size: "+size+"em'>"+content+"</span>" : content;
+		return parser.success(rtn);
+	}
+
+	private String parseYouTube() {
+		parser.begin();
+		if( !parser.matchIgnoreCase("[youtube]") )
+			return parser.failure(null);
+		int start = parser.currentIndex();
+		while( parser.inCharRange('0','9')
+			|| parser.inCharRange('a','z')
+			|| parser.inCharRange('A','Z')
+			|| parser.match('-')
+			|| parser.match('_')
+		);
+		String id = parser.textFrom(start);
+		if( id.length()==0 || !parser.matchIgnoreCase("[/youtube]") )
+			return parser.failure(null);
+		String rtn = toHtml ? "<iframe width='420' height='315' src='https://www.youtube.com/embed/"+id+"' frameborder='0' allowfullscreen></iframe>" : "";
+		return parser.success(rtn);
+	}
+
+	private String quote(Object... args) throws LuanException {
+		Object obj = quoter.call(luan,args);
+		if( !(obj instanceof String) )
+			throw new LuanException("BBCode quoter function returned "+Luan.type(obj)+" but string required");
+		return (String)obj;
+	}
+
+	private String parseQuote1() throws LuanException {
+		parser.begin();
+		if( !parser.matchIgnoreCase("[quote]") )
+			return parser.failure(null);
+		String content = parseWellFormed();
+		if( !parser.matchIgnoreCase("[/quote]") )
+			return parser.failure(null);
+		String rtn = quote(content);
+		return parser.success(rtn);
+	}
+
+	private String parseQuote2() throws LuanException {
+		parser.begin();
+		if( !parser.matchIgnoreCase("[quote=") )
+			return parser.failure(null);
+		List args = new ArrayList();
+		int start = parser.currentIndex();
+		while( parser.noneOf("[];") );
+		String name = parser.textFrom(start).trim();
+		if( name.length() == 0 )
+			return parser.failure(null);
+		args.add(name);
+		while( parser.match(';') ) {
+			start = parser.currentIndex();
+			while( parser.noneOf("[];'") );
+			String src = parser.textFrom(start).trim();
+			args.add(src);
+		}
+		if( !parser.match(']') )
+			return parser.failure(null);
+		String content = parseWellFormed();
+		args.add(0,content);
+		if( !parser.matchIgnoreCase("[/quote]") )
+			return parser.failure(null);
+		String rtn = quote(args.toArray());
+		return parser.success(rtn);
+	}
+
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/parsers/Csv.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/parsers/Csv.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,61 @@
+package luan.modules.parsers;
+
+import luan.LuanTable;
+
+
+public final class Csv {
+
+	public static LuanTable toList(String line) throws ParseException {
+		return new Csv(line).parse();
+	}
+
+	private final Parser parser;
+
+	private Csv(String line) {
+		this.parser = new Parser(line);
+	}
+
+	private ParseException exception(String msg) {
+		return new ParseException(parser,msg);
+	}
+
+	private LuanTable parse() throws ParseException {
+		LuanTable list = new LuanTable();
+		while(true) {
+			Spaces();
+			String field = parseField();
+			list.rawPut(list.rawLength()+1,field);
+			Spaces();
+			if( parser.endOfInput() )
+				return list;
+			if( !parser.match(',') )
+				throw exception("unexpected char");
+		}
+	}
+
+	private String parseField() throws ParseException {
+		parser.begin();
+		String rtn;
+		if( parser.match('"') ) {
+			int start = parser.currentIndex();
+			do {
+				if( parser.endOfInput() ) {
+					parser.failure();
+					throw exception("unclosed quote");
+				}
+			} while( parser.noneOf("\"") );
+			rtn = parser.textFrom(start);
+			parser.match('"');
+		} else {
+			int start = parser.currentIndex();
+			while( !parser.endOfInput() && parser.noneOf(",") );
+			rtn = parser.textFrom(start).trim();
+		}
+		return parser.success(rtn);
+	}
+
+	private void Spaces() {
+		while( parser.anyOf(" \t") );
+	}
+
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/parsers/Html.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/parsers/Html.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,197 @@
+package luan.modules.parsers;
+
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Set;
+import java.util.HashSet;
+import luan.LuanTable;
+
+
+public final class Html {
+
+	public static LuanTable toList(String text,LuanTable containerTagsTbl) throws ParseException {
+		return new Html(text,containerTagsTbl).parse();
+	}
+
+	private final Parser parser;
+	private final Set<String> containerTags = new HashSet<String>();
+
+	private Html(String text,LuanTable containerTagsTbl) {
+		this.parser = new Parser(text);
+		for( Object v : containerTagsTbl.asList() ) {
+			containerTags.add((String)v);
+		}
+	}
+
+	private LuanTable parse() throws ParseException {
+		List list = new ArrayList();
+		StringBuilder sb = new StringBuilder();
+		while( !parser.endOfInput() ) {
+			if( parser.test('<') ) {
+				LuanTable tbl = parseTag();
+				if( tbl != null ) {
+					String tagName = (String)tbl.rawGet("name");
+					if( containerTags.contains(tagName) ) {
+						LuanTable container = parseContainer(tbl);
+						if( container != null )
+							tbl = container;
+					}
+					if( tbl != null 
+						|| (tbl = parseComment()) != null
+						|| (tbl = parseCdata()) != null
+					) {
+						if( sb.length() > 0 ) {
+							list.add(sb.toString());
+							sb.setLength(0);
+						}
+						list.add(tbl);
+						continue;
+					}
+				}
+			}
+			sb.append( parser.currentChar() );
+			parser.anyChar();
+		}
+		if( sb.length() > 0 )
+			list.add(sb.toString());
+		return new LuanTable(list);
+	}
+
+	private LuanTable parseComment() {
+		parser.begin();
+		if( !parser.match("<!--") )
+			return parser.failure(null);
+		int start = parser.currentIndex();
+		while( !parser.test("-->") ) {
+			if( !parser.anyChar() )
+				return parser.failure(null);
+		}
+		String text = parser.textFrom(start);
+		LuanTable tbl = new LuanTable();
+		tbl.rawPut("type","comment");
+		tbl.rawPut("text",text);
+		return parser.success(tbl);
+	}
+
+	private LuanTable parseCdata() {
+		parser.begin();
+		if( !parser.match("<![CDATA[") )
+			return parser.failure(null);
+		int start = parser.currentIndex();
+		while( !parser.test("]]>") ) {
+			if( !parser.anyChar() )
+				return parser.failure(null);
+		}
+		String text = parser.textFrom(start);
+		LuanTable tbl = new LuanTable();
+		tbl.rawPut("type","cdata");
+		tbl.rawPut("text",text);
+		return parser.success(tbl);
+	}
+
+	private LuanTable parseContainer(LuanTable tag) {
+		String endTagName = '/' + (String)tag.rawGet("name");
+		int start = parser.begin();
+		int end;
+		while(true) {
+			if( parser.test('<') ) {
+				end = parser.currentIndex();
+				LuanTable tag2 = parseTag();
+				String s = (String)tag2.rawGet("name");
+				if( s.equals(endTagName) )
+					break;
+			}
+			if( !parser.anyChar() )
+				return parser.failure(null);
+		}
+		String text = parser.text.substring(start,end);
+		LuanTable tbl = new LuanTable();
+		tbl.rawPut("type","container");
+		tbl.rawPut("tag",tag);
+		tbl.rawPut("text",text);
+		return parser.success(tbl);
+	}
+
+	private LuanTable parseTag() {
+		parser.begin();
+		if( !parser.match('<') )
+			return parser.failure(null);
+		int start = parser.currentIndex();
+		parser.match('/');
+		if( !matchNameChar() )
+			return parser.failure(null);
+		while( matchNameChar() );
+		String name = parser.textFrom(start).toLowerCase();
+		LuanTable attributes = new LuanTable();
+		String attrName;
+		while( (attrName = parseAttrName()) != null ) {
+			String attrValue = parseAttrValue();
+			attributes.rawPut( attrName, attrValue!=null ? attrValue : true );
+		}
+		while( matchSpace() );
+		boolean isEmpty = parser.match('/');
+		if( !parser.match('>') )
+			return parser.failure(null);
+		LuanTable tbl = new LuanTable();
+		tbl.rawPut("type","tag");
+		tbl.rawPut("name",name);
+		tbl.rawPut("attributes",attributes);
+		tbl.rawPut("is_empty",isEmpty);
+		return parser.success(tbl);
+	}
+
+	private String parseAttrName() {
+		parser.begin();
+		if( !matchSpace() )
+			return parser.failure(null);
+		while( matchSpace() );
+		int start = parser.currentIndex();
+		if( !matchNameChar() )
+			return parser.failure(null);
+		while( matchNameChar() );
+		String name = parser.textFrom(start);
+		return parser.success(name);
+	}
+
+	private String parseAttrValue() {
+		parser.begin();
+		while( matchSpace() );
+		if( !parser.match('=') )
+			return parser.failure(null);
+		while( matchSpace() );
+		if( parser.anyOf("\"'") ) {
+			char quote = parser.lastChar();
+			int start = parser.currentIndex();
+			while( !parser.test(quote) ) {
+				if( !parser.anyChar() )
+					return parser.failure(null);
+			}
+			String value = parser.textFrom(start);
+			parser.match(quote);
+			return parser.success(value);
+		}
+		int start = parser.currentIndex();
+		if( !matchValueChar() )
+			return parser.failure(null);
+		while( matchValueChar() );
+		String value = parser.textFrom(start);
+		return parser.success(value);
+	}
+
+	private boolean matchNameChar() {
+		return parser.inCharRange('a','z')
+			|| parser.inCharRange('A','Z')
+			|| parser.inCharRange('0','9')
+			|| parser.anyOf("_.-:")
+		;
+	}
+
+	private boolean matchValueChar() {
+		return parser.noneOf(" \t\r\n\"'>/=");
+	}
+
+	private boolean matchSpace() {
+		return parser.anyOf(" \t\r\n");
+	}
+
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/parsers/Json.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/parsers/Json.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,313 @@
+package luan.modules.parsers;
+
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.LinkedHashMap;
+import java.util.Iterator;
+import luan.LuanTable;
+import luan.LuanException;
+
+
+public final class Json {
+
+	public static Object parse(String text) throws ParseException {
+		return new Json(text).parse();
+	}
+
+	private final Parser parser;
+
+	private Json(String text) {
+		this.parser = new Parser(text);
+	}
+
+	private ParseException exception(String msg) {
+		return new ParseException(parser,msg);
+	}
+
+	private Object parse() throws ParseException {
+		spaces();
+		Object value = value();
+		spaces();
+		if( !parser.endOfInput() )
+			throw exception("unexpected text");
+		return value;
+	}
+
+	private Object value() throws ParseException {
+		if( parser.match("null") )
+			return null;
+		if( parser.match("true") )
+			return Boolean.TRUE;
+		if( parser.match("false") )
+			return Boolean.FALSE;
+		String s = string();
+		if( s != null )
+			return s;
+		Number n = number();
+		if( n != null )
+			return n;
+		LuanTable a = array();
+		if( a != null )
+			return a;
+		LuanTable o = object();
+		if( o != null )
+			return o;
+		throw exception("invalid value");
+	}
+
+	private String string() throws ParseException {
+		parser.begin();
+		if( !parser.match('"') )
+			return parser.failure(null);
+		StringBuilder sb = new StringBuilder();
+		while( parser.anyChar() ) {
+			char c = parser.lastChar();
+			switch(c) {
+			case '"':
+				return parser.success(sb.toString());
+			case '\\':
+				if( parser.anyChar() ) {
+					c = parser.lastChar();
+					switch(c) {
+					case '"':
+					case '\\':
+					case '/':
+						sb.append(c);
+						continue;
+					case 'b':
+						sb.append('\b');
+						continue;
+					case 'f':
+						sb.append('\f');
+						continue;
+					case 'n':
+						sb.append('\n');
+						continue;
+					case 'r':
+						sb.append('\r');
+						continue;
+					case 't':
+						sb.append('\t');
+						continue;
+					case 'u':
+						int n = 0;
+						for( int i=0; i<4; i++ ) {
+							int d;
+							if( parser.inCharRange('0','9') ) {
+								d = parser.lastChar() - '0';
+							} else if( parser.inCharRange('a','f') ) {
+								d = parser.lastChar() - 'a' + 10;
+							} else if( parser.inCharRange('A','F') ) {
+								d = parser.lastChar() - 'A' + 10;
+							} else {
+								throw exception("invalid hex digit");
+							}
+							n = 16*n + d;
+						}
+						sb.append((char)n);
+						continue;
+					}
+				}
+				throw exception("invalid escape char");
+			default:
+				sb.append(c);
+			}
+		}
+		parser.failure();
+		throw exception("unclosed string");
+	}
+
+	private Number number() {
+		int start = parser.begin();
+		boolean isFloat = false;
+		parser.match('-');
+		if( !parser.match('0') ) {
+			if( !parser.inCharRange('1','9') )
+				return parser.failure(null);
+			while( parser.inCharRange('0','9') );
+		}
+		if( parser.match('.') ) {
+			if( !parser.inCharRange('0','9') )
+				return parser.failure(null);
+			while( parser.inCharRange('0','9') );
+			isFloat = true;
+		}
+		if( parser.anyOf("eE") ) {
+			parser.anyOf("+-");
+			if( !parser.inCharRange('0','9') )
+				return parser.failure(null);
+			while( parser.inCharRange('0','9') );
+			isFloat = true;
+		}
+		String s = parser.textFrom(start);
+		Number n;
+		if(isFloat)
+			n = Double.valueOf(s);
+		else
+			n = Long.valueOf(s);
+		return parser.success(n);
+	}
+
+	private LuanTable array() throws ParseException {
+		parser.begin();
+		if( !parser.match('[') )
+			return parser.failure(null);
+		spaces();
+		if( parser.match(']') )
+			return parser.success(new LuanTable());
+		List list = new ArrayList();
+		list.add( value() );
+		spaces();
+		while( parser.match(',') ) {
+			spaces();
+			list.add( value() );
+			spaces();
+		}
+		if( parser.match(']') )
+			return parser.success(new LuanTable(list));
+		if( parser.endOfInput() ) {
+			parser.failure();
+			throw exception("unclosed array");
+		}
+		throw exception("unexpected text in array");
+	}
+
+	private LuanTable object() throws ParseException {
+		parser.begin();
+		if( !parser.match('{') )
+			return parser.failure(null);
+		spaces();
+		if( parser.match('}') )
+			return parser.success(new LuanTable());
+		Map map = new LinkedHashMap();
+		addEntry(map);
+		while( parser.match(',') ) {
+			spaces();
+			addEntry(map);
+		}
+		if( parser.match('}') )
+			return parser.success(new LuanTable(map));
+		if( parser.endOfInput() ) {
+			parser.failure();
+			throw exception("unclosed object");
+		}
+		throw exception("unexpected text in object");
+	}
+
+	private void addEntry(Map map) throws ParseException {
+		String key = string();
+		if( key==null )
+			throw exception("invalid object key");
+		spaces();
+		if( !parser.match(':') )
+			throw exception("':' expected");
+		spaces();
+		Object value = value();
+		spaces();
+		map.put(key,value);
+	}
+
+	private void spaces() {
+		while( parser.anyOf(" \t\r\n") );
+	}
+
+
+
+
+
+
+
+
+
+	public static String toString(Object obj) throws LuanException {
+		StringBuilder sb = new StringBuilder();
+		toString(obj,sb);
+		return sb.toString();
+	}
+
+	private static void toString(Object obj,StringBuilder sb) throws LuanException {
+		if( obj == null || obj instanceof Boolean || obj instanceof Number ) {
+			sb.append(obj);
+			return;
+		}
+		if( obj instanceof String ) {
+			toString((String)obj,sb);
+			return;
+		}
+		if( obj instanceof LuanTable ) {
+			toString((LuanTable)obj,sb);
+			return;
+		}
+		throw new LuanException("can't handle type "+obj.getClass().getName());
+	}
+
+	private static void toString(final String s,StringBuilder sb) {
+		sb.append('"');
+		for( int i=0; i<s.length(); i++ ) {
+			char c = s.charAt(i);
+			switch(c) {
+			case '"':
+				sb.append("\\\"");
+				break;
+			case '\\':
+				sb.append("\\\\");
+				break;
+			case '\b':
+				sb.append("\\b");
+				break;
+			case '\f':
+				sb.append("\\f");
+				break;
+			case '\n':
+				sb.append("\\n");
+				break;
+			case '\r':
+				sb.append("\\r");
+				break;
+			case '\t':
+				sb.append("\\t");
+				break;
+			default:
+				sb.append(c);
+			}
+		}
+		sb.append('"');
+	}
+
+	private static void toString(LuanTable t,StringBuilder sb) throws LuanException {
+		if( t.isList() ) {
+			final List list = t.asList();
+			if( list.isEmpty() ) {
+				sb.append("{}");
+				return;
+			}
+			sb.append('[');
+			toString(list.get(0),sb);
+			for( int i=1; i<list.size(); i++ ) {
+				sb.append(',');
+				toString(list.get(i),sb);
+			}
+			sb.append(']');
+			return;
+		}
+		sb.append('{');
+		Iterator<Map.Entry<Object,Object>> i = t.rawIterator();
+		toString(i.next(),sb);
+		while( i.hasNext() ) {
+			sb.append(',');
+			toString(i.next(),sb);
+		}
+		sb.append('}');
+	}
+
+	private static void toString(Map.Entry<Object,Object> entry,StringBuilder sb) throws LuanException {
+		Object key = entry.getKey();
+		if( !(key instanceof String) )
+			throw new LuanException("table keys must be strings");
+		toString((String)key,sb);
+		sb.append(':');
+		toString(entry.getValue(),sb);
+	}
+
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/parsers/ParseException.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/parsers/ParseException.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,51 @@
+package luan.modules.parsers;
+
+
+public final class ParseException extends Exception {
+	public final String text;
+	public final int errorIndex;
+	public final int highIndex;
+
+	ParseException(Parser parser,String msg) {
+		super(msg);
+		this.text = parser.text;
+		this.errorIndex = parser.currentIndex();
+		this.highIndex = parser.highIndex();
+	}
+
+	private class Location {
+		final int line;
+		final int pos;
+
+		Location(int index) {
+			int line = 0;
+			int i = -1;
+			while(true) {
+				int j = text.indexOf('\n',i+1);
+				if( j == -1 || j >= index )
+					break;
+				i = j;
+				line++;
+			}
+			this.line = line;
+			this.pos = index - i - 1;
+		}
+	}
+
+	private String[] lines() {
+		return text.split("\n",-1);
+	}
+
+	@Override public String getMessage() {
+		Location loc = new Location(errorIndex);
+		String line = lines()[loc.line];
+		String msg = super.getMessage() +  " (line " + (loc.line+1) + ", pos " + (loc.pos+1) + ")\n";
+		StringBuilder sb = new StringBuilder(msg);
+		sb.append( line + "\n" );
+		for( int i=0; i<loc.pos; i++ ) {
+			sb.append( line.charAt(i)=='\t' ? '\t' : ' ' );
+		}
+		sb.append("^\n");
+		return sb.toString();
+	}
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/parsers/Parser.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/parsers/Parser.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,156 @@
+package luan.modules.parsers;
+
+
+public class Parser {
+	public final String text;
+	private final int len;
+	private int[] stack = new int[256];
+	private int frame = 0;
+	private int iHigh;
+
+	Parser(String text) {
+		this.text = text;
+		this.len = text.length();
+	}
+
+	private int i() {
+		return stack[frame];
+	}
+
+	private void i(int i) {
+		stack[frame] += i;
+		if( iHigh < stack[frame] )
+			iHigh = stack[frame];
+	}
+
+	public int begin() {
+		frame++;
+		if( frame == stack.length ) {
+			int[] a = new int[2*frame];
+			System.arraycopy(stack,0,a,0,frame);
+			stack = a;
+		}
+		stack[frame] = stack[frame-1];
+		return i();
+	}
+
+	public void rollback() {
+		stack[frame] = stack[frame-1];
+	}
+
+	public <T> T success(T t) {
+		success();
+		return t;
+	}
+
+	public boolean success() {
+		frame--;
+		stack[frame] = stack[frame+1];
+		return true;
+	}
+
+	public <T> T failure(T t) {
+		failure();
+		return t;
+	}
+
+	public boolean failure() {
+		frame--;
+		return false;
+	}
+
+	public int currentIndex() {
+		return i();
+	}
+/*
+	public int errorIndex() {
+		return frame > 0 ? stack[frame-1] : 0;
+	}
+*/
+	public int highIndex() {
+		return iHigh;
+	}
+
+	public char lastChar() {
+		return text.charAt(i()-1);
+	}
+
+	public char currentChar() {
+		return text.charAt(i());
+	}
+
+	public boolean endOfInput() {
+		return i() >= len;
+	}
+
+	public boolean match(char c) {
+		if( endOfInput() || text.charAt(i()) != c )
+			return false;
+		i(1);
+		return true;
+	}
+
+	public boolean match(String s) {
+		int n = s.length();
+		if( !text.regionMatches(i(),s,0,n) )
+			return false;
+		i(n);
+		return true;
+	}
+
+	public boolean matchIgnoreCase(String s) {
+		int n = s.length();
+		if( !text.regionMatches(true,i(),s,0,n) )
+			return false;
+		i(n);
+		return true;
+	}
+
+	public boolean anyOf(String s) {
+		if( endOfInput() || s.indexOf(text.charAt(i())) == -1 )
+			return false;
+		i(1);
+		return true;
+	}
+
+	public boolean noneOf(String s) {
+		if( endOfInput() || s.indexOf(text.charAt(i())) != -1 )
+			return false;
+		i(1);
+		return true;
+	}
+
+	public boolean inCharRange(char cLow, char cHigh) {
+		if( endOfInput() )
+			return false;
+		char c = text.charAt(i());
+		if( !(cLow <= c && c <= cHigh) )
+			return false;
+		i(1);
+		return true;
+	}
+
+	public boolean anyChar() {
+		if( endOfInput() )
+			return false;
+		i(1);
+		return true;
+	}
+
+	public boolean test(char c) {
+		return !endOfInput() && text.charAt(i()) == c;
+	}
+
+	public boolean test(String s) {
+		return text.regionMatches(i(),s,0,s.length());
+	}
+
+	public boolean testIgnoreCase(String s) {
+		return text.regionMatches(true,i(),s,0,s.length());
+	}
+
+	public String textFrom(int start) {
+		return text.substring(start,i());
+	}
+
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/parsers/Theme.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/parsers/Theme.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,202 @@
+package luan.modules.parsers;
+
+import luan.LuanException;
+
+
+public final class Theme {
+
+	public static String toLuan(String source) throws LuanException {
+		try {
+			return new Theme(source).parse();
+		} catch(ParseException e) {
+			throw new LuanException(e.getMessage());
+		}
+	}
+
+	private final Parser parser;
+
+	private Theme(String source) {
+		this.parser = new Parser(source);
+	}
+
+	private ParseException exception(String msg) {
+//		parser.failure();
+		return new ParseException(parser,msg);
+	}
+
+	private String parse() throws ParseException {
+		StringBuilder stmts = new StringBuilder();
+		stmts.append( "local M = {};  " );
+		while( !parser.endOfInput() ) {
+			String def = parseDef();
+			if( def != null ) {
+				stmts.append(def);
+			} else {
+//				parser.anyChar();
+				stmts.append(parsePadding());
+			}
+		}
+		stmts.append( "\n\nreturn M\n" );
+		return stmts.toString();
+	}
+
+	private String parsePadding() throws ParseException {
+		int start = parser.currentIndex();
+		if( parser.match("--") ) {
+			while( parser.noneOf("\r\n") );
+		} else if( !parser.anyOf(" \t\r\n") ) {
+			throw exception("unexpected text");
+		}
+		return parser.textFrom(start);
+	}
+
+	private String parseDef() throws ParseException {
+		int start = parser.begin();
+		if( !parser.match('{') )
+			return parser.failure(null);
+		spaces();
+		if( !parser.match("define:") )
+			return parser.failure(null);
+		String name = parseName();
+		if( name==null )
+			throw exception("invalid block name");
+		spaces();
+		if( !parser.match('}') )
+			throw exception("unclosed define tag");
+		String block = parseBody("define:"+name);
+		String rtn = "function M." + name + "(env) " + block + " end;  ";
+		return parser.success(rtn);
+	}
+
+	private String parseBody(String tagName) throws ParseException {
+		StringBuilder stmts = new StringBuilder();
+		int start = parser.currentIndex();
+		int end = start;
+		while( !matchEndTag(tagName) ) {
+			if( parser.endOfInput() ) {
+				parser.failure();
+				throw exception("unclosed block");
+			}
+			String block = parseBlock();
+			if( block != null ) {
+				addText(start,end,stmts);
+				start = parser.currentIndex();
+				stmts.append(block);
+				continue;
+			}
+			String simpleTag = parseSimpleTag();
+			if( simpleTag != null ) {
+				addText(start,end,stmts);
+				start = parser.currentIndex();
+				stmts.append(simpleTag);
+				continue;
+			}
+			if( parser.match("<%") ) {
+				addText(start,end,stmts);
+				start = parser.currentIndex();
+				stmts.append("%><%='<%'%><%");
+				continue;
+			}
+			parser.anyChar();
+			end = parser.currentIndex();
+		}
+		addText(start,end,stmts);
+		return stmts.toString();
+	}
+
+	private boolean matchEndTag(String tagName) {
+		parser.begin();
+		if( !parser.match('{') )
+			return parser.failure();
+		spaces();
+		if( !(parser.match('/') && parser.match(tagName)) )
+			return parser.failure();
+		spaces();
+		if( !parser.match('}') )
+			return parser.failure();
+		return parser.success();
+	}
+
+	private void addText(int start,int end,StringBuilder stmts) {
+		if( start < end ) {
+			stmts.append( "%>" ).append( parser.text.substring(start,end) ).append( "<%" );
+		}
+	}
+
+	private String parseBlock() throws ParseException {
+		int start = parser.begin();
+		if( !parser.match('{') )
+			return parser.failure(null);
+		spaces();
+		if( !parser.match("block:") )
+			return parser.failure(null);
+		String name = parseName();
+		if( name==null ) {
+			parser.failure();
+			throw exception("invalid block name");
+		}
+		spaces();
+		if( !parser.match('}') )
+			return parser.failure(null);
+		String block = parseBody("block:"+name);
+		String rtn = " env."+ name + "( env, function(env) " + block + "end); ";
+//		String rtn = "<% env." + tag.name + "(" + (tag.attrs.isEmpty() ? "nil" : table(tag.attrs)) + ",env,function(env) %>" + block + "<% end) %>";
+		return parser.success(rtn);
+	}
+
+	private String parseSimpleTag() throws ParseException {
+		int start = parser.begin();
+		if( !parser.match('{') )
+			return parser.failure(null);
+		spaces();
+		String name = parseName();
+		if( name==null )
+			return parser.failure(null);
+		spaces();
+		if( !parser.match('}') )
+			return parser.failure(null);
+//		rtn = "<% env." + name + (attrs.isEmpty() ? "()" : table(attrs)) + " %>";
+		String rtn = " env." + name + "(env); ";
+		return parser.success(rtn);
+	}
+
+	private boolean BlankLine() {
+		parser.begin();
+		while( parser.anyOf(" \t") );
+		return EndOfLine() ? parser.success() : parser.failure();
+	}
+
+	private boolean EndOfLine() {
+		return parser.match( "\r\n" ) || parser.match( '\r' ) || parser.match( '\n' );
+	}
+
+	private String parseName() throws ParseException {
+		int start = parser.begin();
+		if( parser.match('/') ) {
+			parser.failure();
+			throw exception("bad closing tag");
+		}
+		if( parser.match("define:") ) {
+			parser.failure();
+			throw exception("unexpected definition");
+		}
+		if( !FirstNameChar() )
+			return parser.failure(null);
+		while( NameChar() );
+		String match = parser.textFrom(start);
+		return parser.success(match);
+	}
+
+	private boolean FirstNameChar() {
+		return parser.inCharRange('a', 'z') || parser.inCharRange('A', 'Z') || parser.match('_');
+	}
+
+	private boolean NameChar() {
+		return FirstNameChar() || parser.inCharRange('0', '9');
+	}
+
+	private void spaces() {
+		while( parser.anyOf(" \t") );
+	}
+
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/theme_to_luan.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/theme_to_luan.luan	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,4 @@
+local Io = require "luan:Io.luan"
+local Parsers = require "luan:Parsers.luan"
+
+Io.stdout.write( Parsers.theme_to_luan( Io.stdin.read_text(), "stdin" ) )
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/url/LuanUrl.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/url/LuanUrl.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,302 @@
+package luan.modules.url;
+
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.HttpURLConnection;
+import java.net.URLEncoder;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Base64;
+import luan.Luan;
+import luan.LuanState;
+import luan.LuanTable;
+import luan.LuanJavaFunction;
+import luan.LuanException;
+import luan.modules.IoLuan;
+import luan.modules.Utils;
+
+
+public final class LuanUrl extends IoLuan.LuanIn {
+
+	private static enum Method { GET, POST, DELETE }
+
+	private URL url;
+	private Method method = Method.GET;
+	private Map headers;
+	private String content = null;
+	private MultipartClient multipart = null;
+	private int timeout = 0;
+
+	public LuanUrl(LuanState luan,URL url,LuanTable options) throws LuanException {
+		this.url = url;
+		if( options != null ) {
+			Map map = options.asMap(luan);
+			String methodStr = getString(map,"method");
+			if( methodStr != null ) {
+				methodStr = methodStr.toUpperCase();
+				try {
+					this.method = Method.valueOf(methodStr);
+				} catch(IllegalArgumentException e) {
+					throw new LuanException( "invalid method: "+methodStr );
+				}
+			}
+			Map headerMap = getMap(luan,map,"headers");
+			if( headerMap != null ) {
+				headers = new HashMap();
+				for( Object hack : headerMap.entrySet() ) {
+					Map.Entry entry = (Map.Entry)hack;
+					String key = (String)entry.getKey();
+					Object val = entry.getValue();
+					String name = toHttpHeaderName(key);
+					if( val instanceof String ) {
+						headers.put(name,val);
+					} else {
+						if( !(val instanceof LuanTable) )
+							throw new LuanException( "header '"+key+"' must be string or table" );
+						LuanTable t = (LuanTable)val;
+						if( !t.isList() )
+							throw new LuanException( "header '"+key+"' table must be list" );
+						headers.put(name,t.asList());
+					}
+				}
+			}
+			Map auth = getMap(luan,map,"authorization");
+			if( auth != null ) {
+				if( headers!=null && headers.containsKey("Authorization") )
+					throw new LuanException( "can't define authorization with header 'Authorization' defined" );
+				String user = getString(auth,"user");
+				String password = getString(auth,"password");
+				if( !auth.isEmpty() )
+					throw new LuanException( "unrecognized authorization options: "+auth );
+				StringBuilder sb = new StringBuilder();
+				if( user != null )
+					sb.append(user);
+				sb.append(':');
+				if( password != null )
+					sb.append(password);
+				String val = "Basic " + Base64.getEncoder().encodeToString(sb.toString().getBytes());
+				if( headers == null )
+					headers = new HashMap();
+				headers.put("Authorization",val);
+			}
+			Map params = getMap(luan,map,"parameters");
+			String enctype = getString(map,"enctype");
+			if( enctype != null ) {
+				if( !enctype.equals("multipart/form-data") )
+					throw new LuanException( "unrecognized enctype: "+enctype );
+				if( this.method!=Method.POST )
+					throw new LuanException( "multipart/form-data can only be used with POST" );
+				if( params==null )
+					throw new LuanException( "multipart/form-data requires parameters" );
+				if( params.isEmpty() )
+					throw new LuanException( "multipart/form-data parameters can't be empty" );
+				multipart = new MultipartClient(params);
+			}
+			else if( params != null ) {
+				StringBuilder sb = new StringBuilder();
+				for( Object hack : params.entrySet() ) {
+					Map.Entry entry = (Map.Entry)hack;
+					String key = (String)entry.getKey();
+					Object val = entry.getValue();
+					String keyEnc = encode(key);
+					if( val instanceof String ) {
+						and(sb);
+						sb.append( keyEnc ).append( '=' ).append( encode((String)val) );
+					} else {
+						if( !(val instanceof LuanTable) )
+							throw new LuanException( "parameter '"+key+"' must be string or table" );
+						LuanTable t = (LuanTable)val;
+						if( !t.isList() )
+							throw new LuanException( "parameter '"+key+"' table must be list" );
+						for( Object obj : t.asList() ) {
+							if( !(obj instanceof String) )
+								throw new LuanException( "parameter '"+key+"' values must be strings" );
+							and(sb);
+							sb.append( keyEnc ).append( '=' ).append( encode((String)obj) );
+						}
+					}
+				}
+				if( this.method==Method.DELETE )
+					throw new LuanException( "the DELETE method cannot take parameters" );
+				if( this.method==Method.POST ) {
+					content = sb.toString();
+				} else { // GET
+					String urlS = this.url.toString();
+					if( urlS.indexOf('?') == -1 ) {
+						urlS += '?';
+					} else {
+						urlS += '&';
+					}
+					urlS += sb;
+					try {
+						this.url = new URL(urlS);
+					} catch(IOException e) {
+						throw new RuntimeException(e);
+					}
+				}
+			}
+			Integer timeout = getInt(map,"time_out");
+			if( timeout != null )
+				this.timeout = timeout;
+			if( !map.isEmpty() )
+				throw new LuanException( "unrecognized options: "+map );
+		}
+	}
+
+	public static String toHttpHeaderName(String luanName) {
+		luanName = luanName.toLowerCase();
+		StringBuilder buf = new StringBuilder();
+		boolean capitalize = true;
+		char[] a = luanName.toCharArray();
+		for( int i=0; i<a.length; i++ ) {
+			char c = a[i];
+			if( c == '_'  || c == '-' ) {
+				a[i] = '-';
+				capitalize = true;
+			} else if( capitalize ) {
+				a[i] = Character.toUpperCase(c);
+				capitalize = false;
+			}
+		}
+		return String.valueOf(a);
+	}
+
+	private static void and(StringBuilder sb) {
+		if( sb.length() > 0 )
+			sb.append('&');
+	}
+
+	private static String encode(String s) {
+		try {
+			return URLEncoder.encode(s,"UTF-8");
+		} catch(UnsupportedEncodingException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
+	private static String getString(Map map,String key) throws LuanException {
+		Object val = map.remove(key);
+		if( val!=null && !(val instanceof String) )
+			throw new LuanException( "parameter '"+key+"' must be a string" );
+		return (String)val;
+	}
+
+	private static Integer getInt(Map map,String key) throws LuanException {
+		Object val = map.remove(key);
+		if( val==null )
+			return null;
+		Integer i = Luan.asInteger(val);
+		if( i==null )
+			throw new LuanException( "parameter '"+key+"' must be an integer" );
+		return i;
+	}
+
+	private static LuanTable getTable(Map map,String key) throws LuanException {
+		Object val = map.remove(key);
+		if( val!=null && !(val instanceof LuanTable) )
+			throw new LuanException( "parameter '"+key+"' must be a table" );
+		return (LuanTable)val;
+	}
+
+	private static Map getMap(LuanState luan,Map map,String key) throws LuanException {
+		LuanTable t = getTable(map,key);
+		return t==null ? null : t.asMap(luan);
+	}
+
+	@Override public InputStream inputStream() throws IOException, LuanException {
+		URLConnection con = url.openConnection();
+		if( timeout != 0 ) {
+			con.setConnectTimeout(timeout);
+			con.setReadTimeout(timeout);
+		}
+		if( headers != null ) {
+			for( Object hack : headers.entrySet() ) {
+				Map.Entry entry = (Map.Entry)hack;
+				String key = (String)entry.getKey();
+				Object val = entry.getValue();
+				if( val instanceof String ) {
+					con.addRequestProperty(key,(String)val);
+				} else {
+					List list = (List)val;
+					for( Object obj : list ) {
+						con.addRequestProperty(key,(String)obj);
+					}
+				}
+			}
+		}
+		if( method==Method.GET ) {
+			return con.getInputStream();
+		}
+
+		HttpURLConnection httpCon = (HttpURLConnection)con;
+
+		if( method==Method.DELETE ) {
+			httpCon.setRequestMethod("DELETE");
+			return httpCon.getInputStream();
+		}
+
+		// POST
+
+//		httpCon.setRequestProperty("Content-Type","application/x-www-form-urlencoded");
+		httpCon.setDoOutput(true);
+		httpCon.setRequestMethod("POST");
+
+		OutputStream out;
+		if( multipart != null ) {
+			out = multipart.write(httpCon);
+		} else {
+			byte[] post = content.getBytes();
+//			httpCon.setRequestProperty("Content-Length",Integer.toString(post.length));
+			out = httpCon.getOutputStream();
+			out.write(post);
+		}
+		out.flush();
+		try {
+			try {
+				return httpCon.getInputStream();
+			} catch(IOException e) {
+				InputStream is = httpCon.getErrorStream();
+				if( is == null )
+					throw e;
+				Reader in = new InputStreamReader(is);
+				String msg = Utils.readAll(in);
+				in.close();
+				throw new LuanException(msg,e);
+			}
+		} finally {
+			out.close();
+		}
+	}
+
+	@Override public String to_string() {
+		return url.toString();
+	}
+
+	@Override public String to_uri_string() {
+		return url.toString();
+	}
+/*
+	public String post(String postS) throws IOException {
+		return new UrlCall(url).post(postS);
+	}
+
+	@Override public LuanTable table() {
+		LuanTable tbl = super.table();
+		try {
+			tbl.rawPut( "post", new LuanJavaFunction(
+				LuanUrl.class.getMethod( "post", String.class ), this
+			) );
+		} catch(NoSuchMethodException e) {
+			throw new RuntimeException(e);
+		}
+		return tbl;
+	}
+*/
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/url/MultiPartOutputStream.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/url/MultiPartOutputStream.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,146 @@
+//
+//  ========================================================================
+//  Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd.
+//  ------------------------------------------------------------------------
+//  All rights reserved. This program and the accompanying materials
+//  are made available under the terms of the Eclipse Public License v1.0
+//  and Apache License v2.0 which accompanies this distribution.
+//
+//      The Eclipse Public License is available at
+//      http://www.eclipse.org/legal/epl-v10.html
+//
+//      The Apache License v2.0 is available at
+//      http://www.opensource.org/licenses/apache2.0.php
+//
+//  You may elect to redistribute this code under either of these licenses.
+//  ========================================================================
+//
+//  This horrible broken code from jetty is just here for me to look at.  It isn't used.  -fschmidt
+
+package luan.modules.url;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+
+/* ================================================================ */
+/** Handle a multipart MIME response.
+ *
+ * 
+ * 
+*/
+public class MultiPartOutputStream extends FilterOutputStream
+{
+    /* ------------------------------------------------------------ */
+    private static final byte[] __CRLF={'\r','\n'};
+    private static final byte[] __DASHDASH={'-','-'};
+    
+    public static String MULTIPART_MIXED="multipart/mixed";
+    public static String MULTIPART_X_MIXED_REPLACE="multipart/x-mixed-replace";
+    public static final String __ISO_8859_1="ISO-8859-1";
+
+	public static String newBoundary(Object obj) {
+        return "jetty"+System.identityHashCode(obj)+
+        Long.toString(System.currentTimeMillis(),36);
+	}
+    
+    /* ------------------------------------------------------------ */
+    private final String boundary;
+    private final byte[] boundaryBytes;
+
+    /* ------------------------------------------------------------ */
+    private boolean inPart=false;    
+    
+    /* ------------------------------------------------------------ */
+    public MultiPartOutputStream(OutputStream out,String boundary)
+    throws IOException
+    {
+        super(out);
+
+		this.boundary = boundary;
+        boundaryBytes=boundary.getBytes(__ISO_8859_1);
+
+        inPart=false;
+    }
+
+    
+
+    /* ------------------------------------------------------------ */
+    /** End the current part.
+     * @exception IOException IOException
+     */
+    @Override
+    public void close()
+         throws IOException
+    {
+        if (inPart)
+            out.write(__CRLF);
+        out.write(__DASHDASH);
+        out.write(boundaryBytes);
+        out.write(__DASHDASH);
+        out.write(__CRLF);
+        inPart=false;
+        super.close();
+    }
+    
+    /* ------------------------------------------------------------ */
+    public String getBoundary()
+    {
+        return boundary;
+    }
+
+    public OutputStream getOut() {return out;}
+    
+    /* ------------------------------------------------------------ */
+    /** Start creation of the next Content.
+     */
+    public void startPart(String contentType)
+         throws IOException
+    {
+        if (inPart)
+            out.write(__CRLF);
+        inPart=true;
+        out.write(__DASHDASH);
+        out.write(boundaryBytes);
+        out.write(__CRLF);
+        if (contentType != null)
+            out.write(("Content-Type: "+contentType).getBytes(__ISO_8859_1));
+        out.write(__CRLF);
+        out.write(__CRLF);
+    }
+        
+    /* ------------------------------------------------------------ */
+    /** Start creation of the next Content.
+     */
+    public void startPart(String contentType, String[] headers)
+         throws IOException
+    {
+        if (inPart)
+            out.write(__CRLF);
+        inPart=true;
+        out.write(__DASHDASH);
+        out.write(boundaryBytes);
+        out.write(__CRLF);
+        if (contentType != null)
+            out.write(("Content-Type: "+contentType).getBytes(__ISO_8859_1));
+        out.write(__CRLF);
+        for (int i=0;headers!=null && i<headers.length;i++)
+        {
+            out.write(headers[i].getBytes(__ISO_8859_1));
+            out.write(__CRLF);
+        }
+        out.write(__CRLF);
+    }
+    
+    /* ------------------------------------------------------------ */
+    @Override
+    public void write(byte[] b, int off, int len) throws IOException
+    {
+        out.write(b,off,len);
+    }
+}
+
+
+
+
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/url/MultipartClient.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/url/MultipartClient.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,73 @@
+package luan.modules.url;
+
+import java.io.OutputStream;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.HashMap;
+import luan.LuanTable;
+import luan.LuanException;
+
+
+public final class MultipartClient {
+	private static final byte[] __CRLF = {'\r','\n'};
+	private static final byte[] __DASHDASH = {'-','-'};
+	private static final String __ISO_8859_1 = "ISO-8859-1";
+
+	private final Map params = new HashMap();
+
+	MultipartClient(Map params) throws LuanException {
+		for( Object hack : params.entrySet() ) {
+			Map.Entry entry = (Map.Entry)hack;
+			String key = (String)entry.getKey();
+			Object val = entry.getValue();
+			List list = new ArrayList();
+			if( val instanceof String ) {
+				list.add(val);
+			} else {
+				if( !(val instanceof LuanTable) )
+					throw new LuanException( "parameter '"+key+"' must be string or table" );
+				LuanTable t = (LuanTable)val;
+				if( !t.isList() )
+					throw new LuanException( "parameter '"+key+"' table must be list" );
+				for( Object obj : t.asList() ) {
+					if( !(obj instanceof String) )
+						throw new LuanException( "parameter '"+key+"' values must be strings" );
+					list.add(obj);
+				}
+			}
+			this.params.put(key,list);
+		}
+	}
+
+	public OutputStream write(HttpURLConnection httpCon) throws IOException {
+		String boundary = "luan" + System.identityHashCode(this) + Long.toString(System.currentTimeMillis(),36);
+		byte[] boundaryBytes = boundary.getBytes(__ISO_8859_1);
+
+		httpCon.setRequestProperty("Content-Type","multipart/form-data; boundary="+boundary);
+		OutputStream out = httpCon.getOutputStream();
+		for( Object hack : params.entrySet() ) {
+			Map.Entry entry = (Map.Entry)hack;
+			String name = (String)entry.getKey();
+			List list = (List)entry.getValue();
+			for( Object obj : list ) {
+				String val = (String)obj;
+		        out.write(__DASHDASH);
+		        out.write(boundaryBytes);
+		        out.write(__CRLF);
+	            out.write(("Content-Disposition: form-data; name=\""+name+"\"").getBytes(__ISO_8859_1));
+	            out.write(__CRLF);
+		        out.write(__CRLF);
+				out.write(val.getBytes());
+		        out.write(__CRLF);
+			}
+		}
+		out.write(__DASHDASH);
+		out.write(boundaryBytes);
+		out.write(__DASHDASH);
+		out.write(__CRLF);
+		return out;
+	}
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/url/UrlCall.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/url/UrlCall.java	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,84 @@
+// not used, just for reference
+
+package luan.modules.url;
+
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.io.IOException;
+import java.net.URLConnection;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Map;
+import java.util.HashMap;
+import luan.modules.Utils;
+
+
+public final class UrlCall {
+	public final URLConnection connection;
+
+	public UrlCall(String url) throws IOException {
+		this(new URL(url));
+	}
+
+	public UrlCall(URL url) throws IOException {
+		connection = url.openConnection();
+	}
+
+	public void acceptJson() {
+		connection.setRequestProperty("accept","application/json");
+	}
+
+	public String get() throws IOException {
+		Reader in = new InputStreamReader(connection.getInputStream());
+		String rtn = Utils.readAll(in);
+		in.close();
+		return rtn;
+	}
+
+	public String post(String content,String contentType) throws IOException {
+		HttpURLConnection connection = (HttpURLConnection)this.connection;
+
+		connection.setRequestProperty("Content-type",contentType);
+		connection.setDoOutput(true);
+		connection.setRequestMethod("POST");
+
+		byte[] post = content.getBytes();
+		connection.setRequestProperty("Content-Length",Integer.toString(post.length));
+		OutputStream out = connection.getOutputStream();
+		out.write(post);
+		out.flush();
+
+		Reader in;
+		try {
+			in = new InputStreamReader(connection.getInputStream());
+		} catch(IOException e) {
+			InputStream is = connection.getErrorStream();
+			if( is == null )
+				throw e;
+			in = new InputStreamReader(is);
+			String msg = Utils.readAll(in);
+			in.close();
+			throw new UrlCallException(msg,e);
+		}
+		String rtn = Utils.readAll(in);
+		in.close();
+		out.close();
+		return rtn;
+	}
+
+	public String post(String content) throws IOException {
+		return post(content,"application/x-www-form-urlencoded");
+	}
+
+	public String postJson(String content) throws IOException {
+		return post(content,"application/json");
+	}
+
+	public static final class UrlCallException extends IOException {
+		UrlCallException(String msg,IOException e) {
+			super(msg,e);
+		}
+	}
+}
diff -r 3e30cf310e56 -r 1a68fc55a80c src/luan/modules/which.luan
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/luan/modules/which.luan	Fri Aug 26 14:36:40 2016 -0600
@@ -0,0 +1,9 @@
+local Luan = require "luan:Luan.luan"
+local error = Luan.error
+local values = Luan.values or error()
+local Which_mod = require "luan:Which_mod.luan"
+local which = Which_mod.which or error()
+
+for name in values(...) do
+	which(name)
+end