From 567e12ce3f1ef413afa510cb583d6ac8442a7a4a Mon Sep 17 00:00:00 2001 From: Benoit Giannangeli Date: Thu, 23 Feb 2017 08:59:35 +0100 Subject: coroutines --- README.md | 169 +++++++++++++++++++++------------------- src/lapi.js | 130 ++++++++++++++++++------------- src/lauxlib.js | 8 +- src/lcorolib.js | 86 +++++++++++++++++++++ src/ldo.js | 227 +++++++++++++++++++++++++++++++++++++++++++++++++++--- src/linit.js | 5 +- src/lstate.js | 55 ++++++++++--- src/lualib.js | 23 +++++- src/lvm.js | 96 ++++++++++++++++++----- tests/lcorolib.js | 53 +++++++++++++ 10 files changed, 678 insertions(+), 174 deletions(-) create mode 100644 src/lcorolib.js create mode 100644 tests/lcorolib.js diff --git a/README.md b/README.md index f67e193..fd3b3bd 100644 --- a/README.md +++ b/README.md @@ -21,65 +21,73 @@ - [ ] `__tostring` - [ ] `__pairs` - [ ] C API - - [x] lua_version + - [x] lua_absindex - [x] lua_atpanic - - [x] lua_newstate - - [x] lua_pushnil - - [x] lua_gettop - - [x] lua_type - - [x] lua_typename - - [x] lua_pushboolean - - [x] lua_pushinteger - - [x] lua_pushnumber - - [x] lua_pushlstring - - [x] lua_pushstring - - [x] lua_pushvalue - - [x] lua_tointeger - - [x] lua_tointegerx - - [x] lua_tolstring - - [x] lua_tonumber - - [x] lua_tonumberx - - [x] lua_toboolean - - [x] lua_topointer - - [x] lua_pushjsclosure (lua_pushcclosure) - - [x] lua_pushjsfunction (lua_pushcfunction) - - [x] lua_pop - - [x] lua_load - [x] lua_call - [x] lua_callk - - [x] lua_pcall - - [x] lua_setglobal - - [x] lua_upvalueindex - - [x] lua_createtable - - [x] lua_newtable - - [x] lua_gettable - - [x] lua_settable - - [x] lua_absindex - [x] lua_checkstack + - [x] lua_concat + - [x] lua_copy + - [x] lua_createtable + - [x] lua_error - [x] lua_getfield - [x] lua_getglobal - [x] lua_getmetatable - - [x] lua_setmetatable + - [x] lua_gettable + - [x] lua_gettop + - [x] lua_insert + - [x] lua_isstring + - [x] lua_istable + - [x] lua_isyieldable + - [x] lua_load + - [x] lua_newstate + - [x] lua_newtable + - [x] lua_newthread + - [x] lua_next + - [x] lua_pcall + - [x] lua_pop + - [x] lua_pushboolean - [x] lua_pushglobaltable + - [x] lua_pushinteger + - [x] lua_pushjsclosure (lua_pushcclosure) + - [x] lua_pushjsfunction (lua_pushcfunction) - [x] lua_pushliteral + - [x] lua_pushlstring + - [x] lua_pushnil + - [x] lua_pushnumber + - [x] lua_pushstring + - [x] lua_pushvalue + - [x] lua_rawequal - [x] lua_rawget - [x] lua_rawgeti + - [x] lua_rawlen - [x] lua_rawset - - [x] lua_setfield - - [x] lua_settop - - [x] lua_tostring - - [x] lua_rawequal - - [x] lua_error - - [x] lua_concat - - [x] lua_isstring - - [x] lua_istable - [x] lua_remove + - [x] lua_resume - [x] lua_rotate - - [x] lua_insert + - [x] lua_setfield + - [x] lua_setglobal + - [x] lua_setmetatable + - [x] lua_settable + - [x] lua_settop + - [x] lua_status - [x] lua_stringtonumber - - [x] lua_rawlen - - [x] lua_next - - [x] lua_copy + - [x] lua_toboolean + - [x] lua_tointeger + - [x] lua_tointegerx + - [x] lua_tolstring + - [x] lua_tonumber + - [x] lua_tonumberx + - [x] lua_topointer + - [x] lua_tostring + - [x] lua_tothread + - [x] lua_type + - [x] lua_typename + - [x] lua_upvalueindex + - [x] lua_version + - [x] lua_xmove + - [x] lua_yield + - [x] lua_yieldk - [ ] lua_arith - [ ] lua_close - [ ] lua_compare @@ -107,9 +115,7 @@ - [ ] lua_isnumber - [ ] lua_isthread - [ ] lua_isuserdata - - [ ] lua_isyieldable - [ ] lua_len - - [ ] lua_newthread - [ ] lua_newuserdata - [ ] lua_numbertointeger - [ ] lua_pcallk @@ -122,44 +128,39 @@ - [ ] lua_rawsetp - [ ] lua_register - [ ] lua_replace - - [ ] lua_resume - [ ] lua_setallocf - [ ] lua_sethook - [ ] lua_seti - [ ] lua_setlocal - [ ] lua_setupvalue - [ ] lua_setuservalue - - [ ] lua_status - [ ] lua_tocfunction - - [ ] lua_tothread - [ ] lua_touserdata - [ ] lua_upvalueid - [ ] lua_upvaluejoin - - [ ] lua_xmove - - [ ] lua_yield - - [ ] lua_yieldk - [ ] Auxiliary library - - [x] luaL_newstate - - [x] luaL_typename + - [x] luaL_argcheck + - [x] luaL_argerror + - [x] luaL_callmeta - [x] luaL_checkany + - [x] luaL_checkinteger + - [x] luaL_checklstring + - [x] luaL_checkstack - [x] luaL_checktype - - [x] luaL_callmeta + - [x] luaL_error - [x] luaL_getmetafield - - [x] luaL_setfuncs - - [x] luaL_checkstack - - [x] luaL_tolstring - - [x] luaL_openlibs - [x] luaL_getsubtable - - [x] luaL_requiref - - [x] luaL_checkinteger - - [x] luaL_checklstring + - [x] luaL_newlib + - [x] luaL_newstate + - [x] luaL_openlibs - [x] luaL_opt - [x] luaL_optinteger - [x] luaL_optlstring + - [x] luaL_requiref + - [x] luaL_setfuncs + - [x] luaL_tolstring + - [x] luaL_typename - [x] luaL_where - - [x] luaL_argerror - - [x] luaL_argcheck - - [x] luaL_error - [ ] luaL_addchar - [ ] luaL_addlstring - [ ] luaL_addsize @@ -184,7 +185,6 @@ - [ ] luaL_loadfile - [ ] luaL_loadfilex - [ ] luaL_loadstring - - [ ] luaL_newlib - [ ] luaL_newlibtable - [ ] luaL_newmetatable - [ ] luaL_optnumber @@ -200,29 +200,36 @@ - [ ] luaL_unref - [ ] Standard library - [ ] Base lib - - [x] tostring - - [x] print - - [x] getmetatable - - [x] setmetatable - - [x] rawequal - - [x] rawset - - [x] rawget - - [x] type - - [x] error - - [x] pcall - - [x] xpcall + - [x] assert - [x] collectgarbage (unavailable) + - [x] error + - [x] getmetatable - [x] ipairs + - [x] next - [x] pairs + - [x] pcall + - [x] print + - [x] rawequal + - [x] rawget + - [x] rawlen + - [x] rawset - [x] select + - [x] setmetatable - [x] tonumber - - [x] assert - - [x] rawlen - - [x] next + - [x] tostring + - [x] type + - [x] xpcall - [ ] dofile - [ ] loadfile - [ ] load - - [ ] ... + - [ ] Coroutine + - [x] coroutine.create + - [x] coroutine.resume + - [x] coroutine.yield + - [ ] coroutine.isyieldable + - [ ] coroutine.running + - [ ] coroutine.status + - [ ] coroutine.wrap - [ ] Debug (errors) - [ ] DOM API binding - [ ] Parse Lua diff --git a/src/lapi.js b/src/lapi.js index e9f15c2..2f2802f 100644 --- a/src/lapi.js +++ b/src/lapi.js @@ -82,6 +82,19 @@ const lua_checkstack = function(L, n) { return L.stack.length < luaconf.LUAI_MAXSTACK; }; +const lua_xmove = function(from, to, n) { + if (from === to) return; + assert(n < (from.top - from.ci.funcOff), "not enough elements in the stack"); + assert(from.l_G === to.l_G, "moving among independent states"); + assert(to.ci.top - to.top >= n, "stack overflow"); + + from.top -= n; + for (let i = 0; i < n; i++) { + to.stack[to.top] = from.stack[from.top + i]; + to.top++; + } +}; + /* ** basic stack manipulation */ @@ -490,6 +503,11 @@ const lua_tonumber = function(L, idx) { return lvm.tonumber(index2addr(L, idx)) }; +const lua_tothread = function(L, idx) { + let o = index2addr(L, idx); + return o.ttisthread() ? o.value : null; +}; + const lua_topointer = function(L, idx) { let o = index2addr(L, idx); switch (o.ttype()) { @@ -564,8 +582,12 @@ const lua_load = function(L, data, chunckname) { return status; }; +const lua_status = function(L) { + return L.status; +}; + const lua_callk = function(L, nargs, nresults, ctx, k) { - assert(k === null || !(L.ci.callstatus & CIST_LUA), "cannot use continuations inside hooks"); + assert(k === null || !(L.ci.callstatus & lstate.CIST_LUA), "cannot use continuations inside hooks"); assert(nargs + 1 < L.top - L.ci.funcOff, "not enough elements in the stack"); assert(L.status === TS.LUA_OK, "cannot do calls on non-normal thread"); assert(nargs === lua.LUA_MULTRET || (L.ci.top - L.top >= nargs - nresults, "results from function overflow current stack size")); @@ -640,7 +662,6 @@ const lua_pcall = function(L, n, r, f) { return lua_pcallk(L, n, r, f, 0, null); }; - /* ** miscellaneous functions */ @@ -685,66 +706,69 @@ const lua_getextraspace = function () { return null; }; -module.exports.lua_pushvalue = lua_pushvalue; -module.exports.lua_pushnil = lua_pushnil; -module.exports.lua_pushnumber = lua_pushnumber; -module.exports.lua_pushinteger = lua_pushinteger; -module.exports.lua_pushlstring = lua_pushlstring; -module.exports.lua_pushstring = lua_pushstring; -module.exports.lua_pushliteral = lua_pushliteral; -module.exports.lua_pushboolean = lua_pushboolean; -module.exports.lua_pushcclosure = lua_pushcclosure; -module.exports.lua_pushcfunction = lua_pushcfunction; -module.exports.lua_pushjsclosure = lua_pushjsclosure; -module.exports.lua_pushjsfunction = lua_pushjsfunction; -module.exports.lua_version = lua_version; +module.exports.index2addr = index2addr; +module.exports.lua_absindex = lua_absindex; module.exports.lua_atpanic = lua_atpanic; +module.exports.lua_call = lua_call; +module.exports.lua_callk = lua_callk; +module.exports.lua_checkstack = lua_checkstack; +module.exports.lua_concat = lua_concat; +module.exports.lua_copy = lua_copy; +module.exports.lua_createtable = lua_createtable; +module.exports.lua_error = lua_error; +module.exports.lua_gc = lua_gc; +module.exports.lua_getallocf = lua_getallocf; +module.exports.lua_getextraspace = lua_getextraspace; +module.exports.lua_getfield = lua_getfield; +module.exports.lua_getglobal = lua_getglobal; +module.exports.lua_geti = lua_geti; +module.exports.lua_getmetatable = lua_getmetatable; +module.exports.lua_gettable = lua_gettable; module.exports.lua_gettop = lua_gettop; -module.exports.lua_typename = lua_typename; -module.exports.lua_type = lua_type; -module.exports.lua_tonumber = lua_tonumber; -module.exports.lua_tointeger = lua_tointeger; -module.exports.lua_toboolean = lua_toboolean; -module.exports.lua_tolstring = lua_tolstring; -module.exports.lua_tostring = lua_tostring; -module.exports.lua_topointer = lua_topointer; +module.exports.lua_insert = lua_insert; +module.exports.lua_isstring = lua_isstring; +module.exports.lua_istable = lua_istable; module.exports.lua_load = lua_load; -module.exports.lua_callk = lua_callk; -module.exports.lua_call = lua_call; -module.exports.lua_pcallk = lua_pcallk; +module.exports.lua_newtable = lua_newtable; +module.exports.lua_next = lua_next; module.exports.lua_pcall = lua_pcall; +module.exports.lua_pcallk = lua_pcallk; module.exports.lua_pop = lua_pop; -module.exports.lua_setglobal = lua_setglobal; -module.exports.lua_istable = lua_istable; -module.exports.lua_createtable = lua_createtable; -module.exports.lua_newtable = lua_newtable; -module.exports.lua_settable = lua_settable; -module.exports.lua_gettable = lua_gettable; -module.exports.lua_geti = lua_geti; -module.exports.lua_absindex = lua_absindex; -module.exports.index2addr = index2addr; +module.exports.lua_pushboolean = lua_pushboolean; +module.exports.lua_pushcclosure = lua_pushcclosure; +module.exports.lua_pushcfunction = lua_pushcfunction; +module.exports.lua_pushglobaltable = lua_pushglobaltable; +module.exports.lua_pushinteger = lua_pushinteger; +module.exports.lua_pushjsclosure = lua_pushjsclosure; +module.exports.lua_pushjsfunction = lua_pushjsfunction; +module.exports.lua_pushliteral = lua_pushliteral; +module.exports.lua_pushlstring = lua_pushlstring; +module.exports.lua_pushnil = lua_pushnil; +module.exports.lua_pushnumber = lua_pushnumber; +module.exports.lua_pushstring = lua_pushstring; +module.exports.lua_pushvalue = lua_pushvalue; +module.exports.lua_rawequal = lua_rawequal; module.exports.lua_rawget = lua_rawget; -module.exports.lua_rawset = lua_rawset; +module.exports.lua_rawgeti = lua_rawgeti; module.exports.lua_rawlen = lua_rawlen; -module.exports.lua_isstring = lua_isstring; -module.exports.lua_rotate = lua_rotate; +module.exports.lua_rawset = lua_rawset; module.exports.lua_remove = lua_remove; -module.exports.lua_checkstack = lua_checkstack; -module.exports.lua_rawgeti = lua_rawgeti; -module.exports.lua_pushglobaltable = lua_pushglobaltable; +module.exports.lua_rotate = lua_rotate; module.exports.lua_setfield = lua_setfield; -module.exports.lua_getfield = lua_getfield; -module.exports.lua_getglobal = lua_getglobal; -module.exports.lua_getmetatable = lua_getmetatable; +module.exports.lua_setglobal = lua_setglobal; module.exports.lua_setmetatable = lua_setmetatable; +module.exports.lua_settable = lua_settable; module.exports.lua_settop = lua_settop; -module.exports.lua_rawequal = lua_rawequal; -module.exports.lua_concat = lua_concat; -module.exports.lua_error = lua_error; -module.exports.lua_insert = lua_insert; -module.exports.lua_gc = lua_gc; -module.exports.lua_getallocf = lua_getallocf; -module.exports.lua_getextraspace = lua_getextraspace; +module.exports.lua_status = lua_status; module.exports.lua_stringtonumber = lua_stringtonumber; -module.exports.lua_copy = lua_copy; -module.exports.lua_next = lua_next; \ No newline at end of file +module.exports.lua_toboolean = lua_toboolean; +module.exports.lua_tointeger = lua_tointeger; +module.exports.lua_tolstring = lua_tolstring; +module.exports.lua_tonumber = lua_tonumber; +module.exports.lua_topointer = lua_topointer; +module.exports.lua_tostring = lua_tostring; +module.exports.lua_tothread = lua_tothread; +module.exports.lua_type = lua_type; +module.exports.lua_typename = lua_typename; +module.exports.lua_version = lua_version; +module.exports.lua_xmove = lua_xmove; \ No newline at end of file diff --git a/src/lauxlib.js b/src/lauxlib.js index 29b223f..a04f3be 100644 --- a/src/lauxlib.js +++ b/src/lauxlib.js @@ -310,6 +310,11 @@ const luaL_checkstack = function(L, space, msg) { } }; +const luaL_newlib = function(L, l) { + lapi.lua_createtable(L); + luaL_setfuncs(L, l, 0); +}; + module.exports.luaL_newstate = luaL_newstate; module.exports.luaL_typename = luaL_typename; module.exports.luaL_checkany = luaL_checkany; @@ -331,4 +336,5 @@ module.exports.luaL_optinteger = luaL_optinteger; module.exports.luaL_opt = luaL_opt; module.exports.luaL_where = luaL_where; module.exports.luaL_error = luaL_error; -module.exports.luaL_argerror = luaL_argerror; \ No newline at end of file +module.exports.luaL_argerror = luaL_argerror; +module.exports.luaL_newlib = luaL_newlib; \ No newline at end of file diff --git a/src/lcorolib.js b/src/lcorolib.js new file mode 100644 index 0000000..73699a7 --- /dev/null +++ b/src/lcorolib.js @@ -0,0 +1,86 @@ +/* jshint esversion: 6 */ +"use strict"; + +const assert = require('assert'); + +const lua = require('./lua.js'); +const lapi = require('./lapi.js'); +const lauxlib = require('./lauxlib.js'); +const lstate = require('./lstate.js'); +const ldo = require('./ldo.js'); +const CT = lua.constant_types; +const TS = lua.thread_status; + +const getco = function(L) { + let co = lapi.lua_tothread(L, 1); + lauxlib.luaL_argcheck(L, co, 1, "thread expected"); + return co; +}; + +const auxresume = function(L, co, narg) { + if (!lapi.lua_checkstack(co, narg)) { + lapi.lua_pushliteral(L, "too many arguments to resume"); + return -1; /* error flag */ + } + + if (lapi.lua_status(co) === TS.LUA_OK && lapi.lua_gettop(co) === 0) { + lapi.lua_pushliteral(L, "cannot resume dead coroutine"); + return -1; /* error flag */ + } + + lapi.lua_xmove(L, co, narg); + let status = ldo.lua_resume(co, L, narg); + if (status === TS.LUA_OK || status === TS.LUA_YIELD) { + let nres = lapi.lua_gettop(L); + if (!lapi.lua_checkstack(L, nres + 1)) { + lapi.lua_pop(co, nres); /* remove results anyway */ + lapi.lua_pushliteral(L, "too many results to resume"); + return -1; /* error flag */ + } + + lapi.lua_xmove(co, L, nres); /* move yielded values */ + return nres; + } else { + lapi.lua_xmove(co, L, 1); /* move error message */ + return -1; /* error flag */ + } +}; + +const luaB_resume = function(L) { + let co = getco(L); + let r = auxresume(L, co, lapi.lua_gettop(L) - 1); + if (r < 0) { + lapi.lua_pushboolean(L, 0); + lapi.lua_insert(L, -2); + return 2; /* return false + error message */ + } else { + lapi.lua_pushboolean(L, 1); + lapi.lua_insert(L, -(r + 1)); + return r + 1; /* return true + 'resume' returns */ + } +}; + +const luaB_cocreate = function(L) { + lauxlib.luaL_checktype(L, 1, CT.LUA_TFUNCTION); + let NL = lstate.lua_newthread(L); + lapi.lua_pushvalue(L, 1); /* move function to top */ + lapi.lua_xmove(L, NL, 1); /* move function from L to NL */ + return 1; +}; + +const luaB_yield = function(L) { + return ldo.lua_yield(L, lapi.lua_gettop(L)); +}; + +const co_funcs = { + "create": luaB_cocreate, + "yield": luaB_yield, + "resume": luaB_resume +}; + +const luaopen_coroutine = function(L) { + lauxlib.luaL_newlib(L, co_funcs); + return 1; +}; + +module.exports.luaopen_coroutine = luaopen_coroutine; \ No newline at end of file diff --git a/src/ldo.js b/src/ldo.js index 4333f5e..8d12c9f 100644 --- a/src/ldo.js +++ b/src/ldo.js @@ -27,6 +27,7 @@ const seterrorobj = function(L, errcode, oldtop) { } case TS.LUA_ERRERR: { L.stack[oldtop] = new TValue(CT.LUA_TLNGSTR, "error in error handling"); + break; } default: { L.stack[oldtop] = L.stack[L.top - 1]; @@ -79,7 +80,6 @@ const luaD_precall = function(L, off, nresults) { luaD_poscall(L, ci, L.top - n, n); return true; - break; } case CT.LUA_TLCL: { let p = func.p; @@ -260,7 +260,7 @@ const luaD_rawrunprotected = function(L, f, ud) { try { f(L, ud); } catch (e) { - if (lj.status == 0) lj.status = -1; + if (lj.status === 0) lj.status = -1; } L.errorJmp = lj.previous; @@ -270,6 +270,209 @@ const luaD_rawrunprotected = function(L, f, ud) { }; +/* +** Completes the execution of an interrupted C function, calling its +** continuation function. +*/ +const finishCcall = function(L, status) { + let ci = L.ci; + + /* must have a continuation and must be able to call it */ + assert(ci.u.c.k !== null && L.nny === 0); + /* error status can only happen in a protected call */ + assert(ci.callstatus & lstate.CIST_YPCALL || status === TS.LUA_YIELD); + + if (ci.callstatus & TS.CIST_YPCALL) { /* was inside a pcall? */ + ci.callstatus &= ~TS.CIST_YPCALL; /* continuation is also inside it */ + L.errfunc = ci.u.c.old_errfunc; /* with the same error function */ + } + + /* finish 'lua_callk'/'lua_pcall'; CIST_YPCALL and 'errfunc' already + handled */ + if (ci.nresults === LUA_MULTRET && L.ci.top < L.top) L.ci.top = L.top; + let n = ci.u.c.k(L, status, ci.u.c.ctx); /* call continuation function */ + assert(n < (L.top - L.ci.funcOff), "not enough elements in the stack"); + luaD_poscall(L, ci, L.top - n, n); /* finish 'luaD_precall' */ +}; + +/* +** Executes "full continuation" (everything in the stack) of a +** previously interrupted coroutine until the stack is empty (or another +** interruption long-jumps out of the loop). If the coroutine is +** recovering from an error, 'ud' points to the error status, which must +** be passed to the first continuation function (otherwise the default +** status is LUA_YIELD). +*/ +const unroll = function(L, ud) { + if (ud !== null) /* error status? */ + finishCcall(L, ud); /* finish 'lua_pcallk' callee */ + + while (L.ci !== L.base_ci) { /* something in the stack */ + if (!(L.ci.callstatus & lstate.CIST_LUA)) /* C function? */ + finishCcall(L, lstate.LUA_YIELD); /* complete its execution */ + else { /* Lua function */ + lvm.luaV_finishOp(L); /* finish interrupted instruction */ + lvm.luaV_execute(L); /* execute down to higher C 'boundary' */ + } + } +}; + +/* +** Try to find a suspended protected call (a "recover point") for the +** given thread. +*/ +const findpcall = function(L) { + for (let ci = L.ci; ci !== null; ci = ci.previous) { /* search for a pcall */ + if (ci.callstatus & lstate.CIST_YPCALL) + return ci; + } + + return null; /* no pending pcall */ +}; + +/* +** Recovers from an error in a coroutine. Finds a recover point (if +** there is one) and completes the execution of the interrupted +** 'luaD_pcall'. If there is no recover point, returns zero. +*/ +const recover = function(L, status) { + let ci = findpcall(L); + if (ci === null) return 0; /* no recovery point */ + /* "finish" luaD_pcall */ + let oldtop = L.stack[ci.extra]; + lfunc.luaF_close(L, oldtop); + seterrorobj(L, status, oldtop); + L.ci = ci; + L.allowhook = ci.callstatus & lstate.CIST_OAH; /* restore original 'allowhook' */ + L.nny = 0; /* should be zero to be yieldable */ + L.errfunc = ci.u.c.old_errfunc; + return 1; /* continue running the coroutine */ +}; + +/* +** Signal an error in the call to 'lua_resume', not in the execution +** of the coroutine itself. (Such errors should not be handled by any +** coroutine error handler and should not kill the coroutine.) +*/ +const resume_error = function(L, msg, narg) { + L.top -= narg; /* remove args from the stack */ + L.stack[L.top++] = new TValue(CT.LUA_TLNGSTR, msg); /* push error message */ + assert(L.top <= L.ci.top, "stack overflow"); + return TS.LUA_ERRRUN; +}; + +/* +** Do the work for 'lua_resume' in protected mode. Most of the work +** depends on the status of the coroutine: initial state, suspended +** inside a hook, or regularly suspended (optionally with a continuation +** function), plus erroneous cases: non-suspended coroutine or dead +** coroutine. +*/ +const resume = function(L, n) { + let firstArg = L.top - n; /* first argument */ + let ci = L.ci; + if (L.status === TS.LUA_OK) { /* starting a coroutine? */ + if (!luaD_precall(L, firstArg - 1, lua.LUA_MULTRET)) /* Lua function? */ + lvm.luaV_execute(L); /* call it */ + } else { /* resuming from previous yield */ + assert(L.status === TS.LUA_YIELD); + L.status = TS.LUA_OK; /* mark that it is running (again) */ + ci.funcOff = ci.extra; + ci.func = L.stack[ci.funcOff]; + + if (ci.callstatus & lstate.CIST_LUA) /* yielded inside a hook? */ + lvm.luaV_execute(L); /* just continue running Lua code */ + else { /* 'common' yield */ + if (ci.u.c.k !== null) { /* does it have a continuation function? */ + n = ci.u.c.k(L, TS.LUA_YIELD, ci.u.c.ctx); /* call continuation */ + assert(n < (L.top - L.ci.funcOff), "not enough elements in the stack"); + firstArg = L.top - n; /* yield results come from continuation */ + } + + luaD_poscall(L, ci, firstArg, n); /* finish 'luaD_precall' */ + } + + unroll(L, null); /* run continuation */ + } +}; + +const lua_resume = function(L, from, nargs) { + let oldnny = L.nny; /* save "number of non-yieldable" calls */ + + if (L.status === TS.LUA_OK) { /* may be starting a coroutine */ + if (L.ci !== L.base_ci) /* not in base level? */ + return resume_error(L, "cannot resume non-suspended coroutine", nargs); + } else if (L.status !== TS.LUA_YIELD) + return resume_error(L, "cannot resume dead coroutine", nargs); + + L.nCcalls = from ? from.nCcalls + 1 : 1; + if (L.nCcalls >= llimit.LUAI_MAXCCALLS) + return resume_error(L, "JS stack overflow", nargs); + + L.nny = 0; /* allow yields */ + + assert((L.status === TS.LUA_OK ? nargs + 1: nargs) < (L.top - L.ci.funcOff), + "not enough elements in the stack"); + + let status = luaD_rawrunprotected(L, resume, nargs); + if (status === -1) /* error calling 'lua_resume'? */ + status = TS.LUA_ERRRUN; + else { /* continue running after recoverable errors */ + while (status > TS.LUA_YIELD && recover(L, status)) { + /* unroll continuation */ + status = luaD_rawrunprotected(L, unroll, status); + } + + if (status > TS.LUA_YIELD) { /* unrecoverable error? */ + L.status = status; /* mark thread as 'dead' */ + seterrorobj(L, status, L.top); /* push error message */ + L.ci.top = L.top; + } else + assert(status === L.status); /* normal end or yield */ + } + + L.nny = oldnny; /* restore 'nny' */ + L.nCcalls--; + assert(L.nCcalls === (from ? from.nCcalls : 0)); + return status; +}; + +const lua_isyieldable = function(L) { + return L.nny === 0; +}; + +const lua_yieldk = function(L, nresults, ctx, k) { + let ci = L.ci; + assert(nresults < (L.top - L.ci.funcOff), "not enough elements in the stack"); + + if (L.nny > 0) { + if (L !== L.l_G.mainthread) + ldebug.luaG_runerror(L, "attempt to yield across a JS-call boundary"); + else + ldebug.luaG_runerror(L, "attempt to yield from outside a coroutine"); + } + + L.status = TS.LUA_YIELD; + ci.extra = ci.funcOff; /* save current 'func' */ + if (ci.callstatus & lstate.CIST_LUA) /* inside a hook? */ + assert(k === null, "hooks cannot continue after yielding"); + else { + ci.u.c.k = k; + if (k !== null) /* is there a continuation? */ + ci.u.c.ctx = ctx; /* save context */ + ci.funcOff = L.top - nresults - 1; /* protect stack below results */ + ci.func = L.stack[ci.funcOff]; + luaD_throw(L, TS.LUA_YIELD); + } + + assert(ci.callstatus & lstate.CIST_HOOKED); /* must be inside a hook */ + return 0; /* return to 'luaD_hook' */ +}; + +const lua_yield = function(L, n) { + lua_yieldk(L, n, 0, null); +}; + const luaD_pcall = function(L, func, u, old_top, ef) { let old_ci = L.ci; // TODO: lu_byte old_allowhooks = L->allowhook; @@ -321,16 +524,20 @@ const luaD_protectedparser = function(L, data, name) { return status; }; -module.exports.nil = nil; -module.exports.luaD_precall = luaD_precall; -module.exports.luaD_poscall = luaD_poscall; -module.exports.moveresults = moveresults; module.exports.adjust_varargs = adjust_varargs; -module.exports.tryfuncTM = tryfuncTM; -module.exports.stackerror = stackerror; +module.exports.lua_isyieldable = lua_isyieldable; +module.exports.lua_resume = lua_resume; +module.exports.lua_yield = lua_yield; +module.exports.lua_yieldk = lua_yieldk; module.exports.luaD_call = luaD_call; module.exports.luaD_callnoyield = luaD_callnoyield; module.exports.luaD_pcall = luaD_pcall; -module.exports.luaD_throw = luaD_throw; +module.exports.luaD_poscall = luaD_poscall; +module.exports.luaD_precall = luaD_precall; +module.exports.luaD_protectedparser = luaD_protectedparser; module.exports.luaD_rawrunprotected = luaD_rawrunprotected; -module.exports.luaD_protectedparser = luaD_protectedparser; \ No newline at end of file +module.exports.luaD_throw = luaD_throw; +module.exports.moveresults = moveresults; +module.exports.nil = nil; +module.exports.stackerror = stackerror; +module.exports.tryfuncTM = tryfuncTM; \ No newline at end of file diff --git a/src/linit.js b/src/linit.js index fc2a54d..d52b640 100644 --- a/src/linit.js +++ b/src/linit.js @@ -5,10 +5,13 @@ const assert = require('assert'); const lapi = require('./lapi.js'); const lauxlib = require('./lauxlib.js'); +const lualib = require('./lualib.js'); const lbaselib = require('./lbaselib.js'); +const lcorolib = require('./lcorolib.js'); const loadedlibs = { - "_G": lbaselib.luaopen_base + [lualib.LUA_COLIBNAME]: lcorolib.luaopen_coroutine, + "_G": lbaselib.luaopen_base }; const luaL_openlibs = function(L) { diff --git a/src/lstate.js b/src/lstate.js index eec9fae..9a9c9e0 100644 --- a/src/lstate.js +++ b/src/lstate.js @@ -1,8 +1,10 @@ /*jshint esversion: 6 */ "use strict"; +const assert = require('assert'); + const lua = require('./lua.js'); -const Table = require('./lobject.js').Table; +const lobject = require('./lobject.js'); const ldo = require('./ldo.js'); const lapi = require('./lapi.js'); const nil = ldo.nil; @@ -41,9 +43,10 @@ class CallInfo { } -class lua_State { +class lua_State extends lobject.TValue { - constructor(cl) { + constructor() { + super(CT.LUA_TTHREAD, null); this.base_ci = new CallInfo(); // Will be populated later this.top = 0; this.ci = null; @@ -52,12 +55,12 @@ class lua_State { this.openupval = []; this.status = TS.LUA_OK; this.next = null; - this.tt = CT.LUA_TTHREAD; this.twups = [this]; this.errorJmp = null; - // TODO: hooks this.nny = 1; this.errfunc = 0; + + this.value = this; } } @@ -94,10 +97,10 @@ const stack_init = function(L1, L) { ** Create registry table and its predefined values */ const init_registry = function(L, g) { - let registry = new Table(); + let registry = new lobject.Table(); g.l_registry = registry; registry.value.set(lua.LUA_RIDX_MAINTHREAD - 1, L); - registry.value.set(lua.LUA_RIDX_GLOBALS - 1, new Table()); + registry.value.set(lua.LUA_RIDX_GLOBALS - 1, new lobject.Table()); }; /* @@ -113,11 +116,44 @@ const f_luaopen = function(L) { g.version = lapi.lua_version(null); }; +const preinit_thread = function(L, g) { + L.l_G = g; + L.stack = []; + L.ci = null; + L.nci = 0; + L.twups = [L]; /* thread has no upvalues */ + L.errorJmp = null; + L.nCcalls = 0; + L.hook = null; + L.hookmask = 0; + L.basehookcount = 0; + L.allowhook = 1; + L.hookcount = L.basehookcount; + L.openupval = []; + L.nny = 1; + L.status = TS.LUA_OK; + L.errfunc = 0; +}; + +const lua_newthread = function(L) { + let g = L.l_G; + let L1 = new lua_State(); + L.stack[L.top++] = L1; + assert(L.top <= L.ci.top, "stack overflow"); + preinit_thread(L1, g); + L1.hookmask = L.hookmask; + L1.basehookcount = L.basehookcount; + L1.hook = L.hook; + L1.hookcount = L1.basehookcount; + stack_init(L1, L); + return L1; +}; + const lua_newstate = function() { let L = new lua_State(); let g = new global_State(L); - L.l_G = g; + preinit_thread(L, g); if (luaD_rawrunprotected(L, f_luaopen, null) !== TS.LUA_OK) { L = null; @@ -137,4 +173,5 @@ module.exports.CIST_TAIL = (1<<5); /* call was tail called */ module.exports.CIST_HOOKYIELD = (1<<6); /* last hook called yielded */ module.exports.CIST_LEQ = (1<<7); /* using __lt for __le */ module.exports.CIST_FIN = (1<<8); /* call is running a finalizer */ -module.exports.lua_newstate = lua_newstate; \ No newline at end of file +module.exports.lua_newstate = lua_newstate; +module.exports.lua_newthread = lua_newthread; \ No newline at end of file diff --git a/src/lualib.js b/src/lualib.js index 1eff812..d87cfbc 100644 --- a/src/lualib.js +++ b/src/lualib.js @@ -7,5 +7,26 @@ const lua = require('./lua.js'); const LUA_VERSUFFIX = "_" + lua.LUA_VERSION_MAJOR + "_" + lua.LUA_VERSION_MINOR; +const LUA_COLIBNAME = "coroutine"; +const LUA_TABLIBNAME = "table" +const LUA_IOLIBNAME = "io" +const LUA_OSLIBNAME = "os" +const LUA_STRLIBNAME = "string" +const LUA_UTF8LIBNAME = "utf8" +const LUA_BITLIBNAME = "bit32" +const LUA_MATHLIBNAME = "math" +const LUA_DBLIBNAME = "debug" +const LUA_LOADLIBNAME = "package" -module.exports.LUA_VERSUFFIX = LUA_VERSUFFIX; \ No newline at end of file + +module.exports.LUA_BITLIBNAME = LUA_BITLIBNAME; +module.exports.LUA_COLIBNAME = LUA_COLIBNAME; +module.exports.LUA_DBLIBNAME = LUA_DBLIBNAME; +module.exports.LUA_IOLIBNAME = LUA_IOLIBNAME; +module.exports.LUA_LOADLIBNAME = LUA_LOADLIBNAME; +module.exports.LUA_MATHLIBNAME = LUA_MATHLIBNAME; +module.exports.LUA_OSLIBNAME = LUA_OSLIBNAME; +module.exports.LUA_STRLIBNAME = LUA_STRLIBNAME; +module.exports.LUA_TABLIBNAME = LUA_TABLIBNAME; +module.exports.LUA_UTF8LIBNAME = LUA_UTF8LIBNAME; +module.exports.LUA_VERSUFFIX = LUA_VERSUFFIX; \ No newline at end of file diff --git a/src/lvm.js b/src/lvm.js index a31e0c7..15a11e4 100644 --- a/src/lvm.js +++ b/src/lvm.js @@ -22,6 +22,65 @@ const ltm = require('./ltm.js'); const ltable = require('./ltable.js'); const ldebug = require('./ldebug.js'); +/* +** finish execution of an opcode interrupted by an yield +*/ +const luaV_finishOp = function(L) { + let ci = L.ci; + let base = ci.u.l.base; + let inst = ci.u.l.savedpc[ci.pcOff - 1]; /* interrupted instruction */ + let op = OC.OpCodes[inst.opcode]; + + switch (op) { /* finish its execution */ + case "OP_ADD": case "OP_SUB": case "OP_MUL": case "OP_DIV": case "OP_IDIV": + case "OP_BAND": case "OP_BOR": case "OP_BXOR": case "OP_SHL": case "OP_SHR": + case "OP_MOD": case "OP_POW": + case "OP_UNM": case "OP_BNOT": case "OP_LEN": + case "OP_GETTABUP": case "OP_GETTABLE": case "OP_SELF": { + L.stack[base + inst.A] = L.stack[--L.top]; + break; + } + case "OP_LE": case "OP_LT": case "OP_EQ": { + let res = !L.stack[L.top - 1].l_isfalse(); + L.top--; + if (ci.callstatus & lstate.CIST_LEQ) { /* "<=" using "<" instead? */ + assert(op === "OP_LE"); + ci.callstatus ^= lstate.CIST_LEQ; /* clear mark */ + res = res !== 1 ? 1 : 0; /* negate result */ + } + assert(OC.OpCodes[ci.u.l.savedpc[ci.pcOff]] === "OP_JMP"); + if (res !== inst.A) /* condition failed? */ + ci.pcOff++; /* skip jump instruction */ + break; + } + case "OP_CONCAT": { + let top = L.top - 1; /* top when 'luaT_trybinTM' was called */ + let b = inst.B; /* first element to concatenate */ + let total = top - 1 - (base + b); /* yet to concatenate */ + L.stack[L.top - 2] = L.stack[top]; /* put TM result in proper position */ + if (total > 1) { /* are there elements to concat? */ + L.top = top - 1; /* top is one after last element (at top-2) */ + luaV_concat(L, total); /* concat them (may yield again) */ + } + + /* move final result to final position */ + L.stack[ci.u.l.base + inst.A] = L.stack[L.top - 1]; + L.top = ci.top; /* restore top */ + break; + } + case "OP_TFORCALL": { + assert(OC.OpCodes[ci.u.l.savedpc[ci.pcOff]] === "OP_TFORLOOP"); + L.top = ci.top; /* correct top */ + break; + } + case "OP_CALL": { + if (inst.C - 1 >= 0) /* nresults >= 0? */ + L.top = ci.top; /* adjust results */ + break; + } + } +}; + const RA = function(L, base, i) { return base + i.A; }; @@ -1013,28 +1072,29 @@ const luaV_finishset = function(L, t, key, val, slot, recur) { } -module.exports.RA = RA; -module.exports.RB = RB; -module.exports.RC = RC; -module.exports.RKB = RKB; -module.exports.RKC = RKC; -module.exports.luaV_execute = luaV_execute; module.exports.dojump = dojump; module.exports.donextjump = donextjump; -module.exports.luaV_lessequal = luaV_lessequal; -module.exports.luaV_lessthan = luaV_lessthan; -module.exports.luaV_equalobj = luaV_equalobj; module.exports.forlimit = forlimit; -module.exports.luaV_tointeger = luaV_tointeger; -module.exports.tonumber = tonumber; -module.exports.tointeger = tointeger; -module.exports.LTnum = LTnum; -module.exports.LEnum = LEnum; +module.exports.gettable = gettable; +module.exports.l_strcmp = l_strcmp; module.exports.LEintfloat = LEintfloat; +module.exports.LEnum = LEnum; module.exports.LTintfloat = LTintfloat; -module.exports.l_strcmp = l_strcmp; -module.exports.luaV_objlen = luaV_objlen; +module.exports.LTnum = LTnum; +module.exports.luaV_concat = luaV_concat; +module.exports.luaV_equalobj = luaV_equalobj; +module.exports.luaV_execute = luaV_execute; +module.exports.luaV_finishOp = luaV_finishOp; module.exports.luaV_finishset = luaV_finishset; -module.exports.gettable = gettable; +module.exports.luaV_lessequal = luaV_lessequal; +module.exports.luaV_lessthan = luaV_lessthan; +module.exports.luaV_objlen = luaV_objlen; +module.exports.luaV_tointeger = luaV_tointeger; +module.exports.RA = RA; +module.exports.RB = RB; +module.exports.RC = RC; +module.exports.RKB = RKB; +module.exports.RKC = RKC; module.exports.settable = settable; -module.exports.luaV_concat = luaV_concat; \ No newline at end of file +module.exports.tointeger = tointeger; +module.exports.tonumber = tonumber; \ No newline at end of file diff --git a/tests/lcorolib.js b/tests/lcorolib.js new file mode 100644 index 0000000..6b48912 --- /dev/null +++ b/tests/lcorolib.js @@ -0,0 +1,53 @@ +/*jshint esversion: 6 */ +"use strict"; + +const test = require('tape'); +const beautify = require('js-beautify').js_beautify; + +const tests = require("./tests.js"); +const getState = tests.getState; +const toByteCode = tests.toByteCode; + +const VM = require("../src/lvm.js"); +const ldo = require("../src/ldo.js"); +const lapi = require("../src/lapi.js"); +const lauxlib = require("../src/lauxlib.js"); +const lua = require('../src/lua.js'); +const linit = require('../src/linit.js'); +const CT = lua.constant_types; + +test('simple coroutine', function (t) { + let luaCode = ` + local co = coroutine.create(function (start) + local b = coroutine.yield(start * start); + coroutine.yield(b * b) + end) + + local success, pow = coroutine.resume(co, 5) + success, pow = coroutine.resume(co, pow) + + return pow + `, L; + + t.plan(2); + + t.doesNotThrow(function () { + + let bc = toByteCode(luaCode).dataView; + + L = lauxlib.luaL_newstate(); + + linit.luaL_openlibs(L); + + lapi.lua_load(L, bc, "test-coroutine"); + + lapi.lua_call(L, 0, -1); + + }, "JS Lua program ran without error"); + + t.strictEqual( + lapi.lua_tonumber(L, -1), + 625, + "Correct element(s) on the stack" + ); +}); \ No newline at end of file -- cgit v1.2.3-70-g09d2