diff --git a/src/addon/java/lua/addon/luacompat/CoroutinesLib.java b/src/addon/java/lua/addon/luacompat/CoroutinesLib.java new file mode 100644 index 00000000..c0e34de6 --- /dev/null +++ b/src/addon/java/lua/addon/luacompat/CoroutinesLib.java @@ -0,0 +1,95 @@ +package lua.addon.luacompat; + +import lua.GlobalState; +import lua.VM; +import lua.io.Closure; +import lua.value.LFunction; +import lua.value.LTable; +import lua.value.LThread; + + +public class CoroutinesLib extends LFunction { + + public static void install() { + LTable lib = new LTable(0,6); + lib.put("create", new CoroutinesLib(1)); + lib.put("resume", new CoroutinesLib(2)); + lib.put("running", new CoroutinesLib(3)); + lib.put("status", new CoroutinesLib(4)); + lib.put("wrap", new CoroutinesLib(5)); + lib.put("yield", new CoroutinesLib(6)); + GlobalState.getGlobalsTable().put("coroutine",lib); + } + + private int id = 0; + + private static LThread running; + + public CoroutinesLib() { + this(0); + } + + private CoroutinesLib( int id ) { + this.id = id; + } + + public boolean luaStackCall( VM vm ) { + switch ( id ) { + case 0: { // load lib + install(); + vm.pushnil(); + break; + } + case 1: { // create + Closure c = (Closure) vm.topointer(2); + vm.pushlvalue( new LThread(c) ); + break; + } + case 2: {// resume + LThread t = (LThread) vm.topointer(2); + LThread prior = running; + try { + // whatever is left on the stack by the resumeFrom() implementation + // becomes return values! + running = t; + t.resumeFrom( vm, prior ); + return false; + } finally { + running = prior; + } + } + case 3: { // running + if ( running != null ) { + vm.pushlvalue( running ); + } else { + vm.pushnil(); + } + break; + } + case 4: { // status + vm.pushstring( ((LThread) vm.topointer(2)).getStatus() ); + break; + } + case 5: { // wrap + vm.error( "wrap() not supported" ); + return false; + } + case 6: { // yield + if ( running == null ) + vm.error("main thread can't yield"); + else { + return running.yield(); + } + } + case 7: { // wrapped resume + vm.error( "wrap() not supported" ); + return false; + } + } + vm.insert(1); + vm.settop(1); + return false; + } + + +} diff --git a/src/addon/java/lua/addon/luacompat/LuaCompat.java b/src/addon/java/lua/addon/luacompat/LuaCompat.java index dd4c6423..9195ad62 100644 --- a/src/addon/java/lua/addon/luacompat/LuaCompat.java +++ b/src/addon/java/lua/addon/luacompat/LuaCompat.java @@ -55,6 +55,9 @@ public class LuaCompat extends LFunction { LTable table = new LTable(); installNames( pckg, TABLE_NAMES, TABLES_BASE ); globals.put( "table", pckg ); + + // coroutines + CoroutinesLib.install(); } private static void installNames( LTable table, String[] names, int indexBase ) { diff --git a/src/main/java/lua/VM.java b/src/main/java/lua/VM.java index bbbd1898..d3675080 100644 --- a/src/main/java/lua/VM.java +++ b/src/main/java/lua/VM.java @@ -1382,7 +1382,7 @@ public interface VM { * This function pops n values from the stack * from, and pushes them onto the stack to. */ - public void xmove(StackState to, int n); + public void xmove(VM to, int n); /** * Yields a coroutine. [-?, +?, -] diff --git a/src/main/java/lua/io/Closure.java b/src/main/java/lua/io/Closure.java index 10fefc29..6fab028e 100644 --- a/src/main/java/lua/io/Closure.java +++ b/src/main/java/lua/io/Closure.java @@ -3,20 +3,35 @@ package lua.io; import lua.StackState; import lua.VM; import lua.value.LFunction; -import lua.value.LValue; +import lua.value.LTable; public class Closure extends LFunction { - public LValue env; + public LTable env; public Proto p; public UpVal[] upVals; - // TODO: change arg type to VM? + /** + * @deprecated construct with environment instead + * @param state + * @param p + */ public Closure(StackState state, Proto p) { this.env = state._G; this.p = p; upVals = new UpVal[p.nups]; } + /** + * Construct using a prototype and initial environment. + * @param p + * @param env + */ + public Closure(Proto p, LTable env) { + this.p = p; + this.env = env; + upVals = new UpVal[p.nups]; + } + // called by vm when there is an OP_CALL // in this case, we are on the stack, // and simply need to cue the VM to treat it as a stack call diff --git a/src/main/java/lua/value/LThread.java b/src/main/java/lua/value/LThread.java index e6cf7010..3c47cd17 100644 --- a/src/main/java/lua/value/LThread.java +++ b/src/main/java/lua/value/LThread.java @@ -1,9 +1,33 @@ package lua.value; import lua.Lua; +import lua.StackState; +import lua.VM; +import lua.io.Closure; public class LThread extends LValue { + + private static final int STATUS_SUSPENDED = 1; + private static final int STATUS_NORMAL = 2; + private static final int STATUS_ACTIVE = 3; + private static final int STATUS_DEAD = 4; + private static final String[] NAMES = { + "suspended", + "normal", + "active", + "dead" }; + private int status = STATUS_SUSPENDED; + + private StackState threadVm; + + + public LThread(Closure c) { + // TODO: inherit globals! + threadVm = new StackState(); + threadVm.pushlvalue(new Closure(c.p, threadVm._G)); + } + public int luaGetType() { return Lua.LUA_TTHREAD; } @@ -11,4 +35,74 @@ public class LThread extends LValue { public String toJavaString() { return "thread: "+hashCode(); } + + public String getStatus() { + return NAMES[status]; + } + + /** This needs to leave any values returned by yield in the corouting + * on the calling vm stack + * @param vm + * @param prior + */ + public void resumeFrom(VM vm, LThread prior) { + + if ( status == STATUS_DEAD ) { + vm.settop(0); + vm.pushboolean(false); + vm.pushstring("cannot resume dead coroutine"); + return; + } + + // set prior thread to normal status while we are running + if ( prior != null ) + prior.status = STATUS_NORMAL; + + try { + // copy args in + if ( threadVm.cc < 0 ) { + vm.xmove(threadVm, vm.gettop() - 2); + threadVm.prepStackCall(); + } else { + threadVm.settop(0); + vm.xmove(threadVm, vm.gettop() - 2); + } + + // run this vm until it yields + status = STATUS_ACTIVE; + while ( threadVm.cc >= 0 && status == STATUS_ACTIVE ) + threadVm.exec(); + + // copy return values from yielding stack state + vm.settop(0); + vm.pushboolean(true); + if ( threadVm.cc >= 0 ) { + threadVm.xmove(vm, threadVm.gettop() - 1); + } else { + threadVm.base = 0; + threadVm.xmove(vm, threadVm.gettop()); + } + + } catch ( Throwable t ) { + status = STATUS_DEAD; + vm.settop(0); + vm.pushboolean(false); + vm.pushstring("thread: "+t); + + } finally { + if ( threadVm.cc < 0 ) + status = STATUS_DEAD; + + // reset prior thread status + if ( prior != null ) + prior.status = STATUS_ACTIVE; + } + + } + + public boolean yield() { + if ( status == STATUS_ACTIVE ) + status = STATUS_SUSPENDED; + return true; + } } diff --git a/src/test/java/lua/LuaJTest.java b/src/test/java/lua/LuaJTest.java index 520b9445..f463ba71 100644 --- a/src/test/java/lua/LuaJTest.java +++ b/src/test/java/lua/LuaJTest.java @@ -60,6 +60,10 @@ public class LuaJTest extends TestCase { runTest( "coercions" ); } + public void testCoroutines() throws IOException, InterruptedException { + runTest( "coroutines" ); + } + public void testCompare() throws IOException, InterruptedException { runTest( "compare" ); } diff --git a/src/test/res/coroutines.lua b/src/test/res/coroutines.lua new file mode 100644 index 00000000..9de0fc7c --- /dev/null +++ b/src/test/res/coroutines.lua @@ -0,0 +1,18 @@ +function foo (a) + print("foo", a) + return coroutine.yield(2*a) +end + +co = coroutine.create(function (a,b) + print("co-body", a, b) + local r = foo(a+1) + print("co-body", r) + local r, s = coroutine.yield(a+b, a-b) + print("co-body", r, s) + return b, "end" +end) + +print("main", coroutine.resume(co, 1, 10)) +print("main", coroutine.resume(co, "r")) +print("main", coroutine.resume(co, "x", "y")) +print("main", coroutine.resume(co, "x", "y")) \ No newline at end of file diff --git a/src/test/res/coroutines.luac b/src/test/res/coroutines.luac new file mode 100644 index 00000000..a450f1f8 Binary files /dev/null and b/src/test/res/coroutines.luac differ