fengari
Version:
A Lua VM written in JS ES6 targeting the browser
503 lines (465 loc) • 14.8 kB
JavaScript
;
const {
LUA_MULTRET,
LUA_OK,
LUA_TFUNCTION,
LUA_TNIL,
LUA_TNONE,
LUA_TNUMBER,
LUA_TSTRING,
LUA_TTABLE,
LUA_VERSION,
LUA_YIELD,
lua_call,
lua_callk,
lua_concat,
lua_error,
lua_getglobal,
lua_geti,
lua_getmetatable,
lua_gettop,
lua_insert,
lua_isnil,
lua_isnone,
lua_isstring,
lua_load,
lua_next,
lua_pcallk,
lua_pop,
lua_pushboolean,
lua_pushcfunction,
lua_pushglobaltable,
lua_pushinteger,
lua_pushliteral,
lua_pushnil,
lua_pushstring,
lua_pushvalue,
lua_rawequal,
lua_rawget,
lua_rawlen,
lua_rawset,
lua_remove,
lua_replace,
lua_rotate,
lua_setfield,
lua_setmetatable,
lua_settop,
lua_setupvalue,
lua_stringtonumber,
lua_toboolean,
lua_tolstring,
lua_tostring,
lua_type,
lua_typename
} = require('./lua.js');
const {
luaL_argcheck,
luaL_checkany,
luaL_checkinteger,
luaL_checkoption,
luaL_checkstack,
luaL_checktype,
luaL_error,
luaL_getmetafield,
luaL_loadbufferx,
luaL_loadfile,
luaL_loadfilex,
luaL_optinteger,
luaL_optstring,
luaL_setfuncs,
luaL_tolstring,
luaL_where
} = require('./lauxlib.js');
const {
to_jsstring,
to_luastring
} = require("./fengaricore.js");
let lua_writestring;
let lua_writeline;
if (typeof process === "undefined") {
if (typeof TextDecoder === "function") { /* Older browsers don't have TextDecoder */
let buff = "";
let decoder = new TextDecoder("utf-8");
lua_writestring = function(s) {
buff += decoder.decode(s, {stream: true});
};
let empty = new Uint8Array(0);
lua_writeline = function() {
buff += decoder.decode(empty);
console.log(buff);
buff = "";
};
} else {
let buff = [];
lua_writestring = function(s) {
try {
/* If the string is valid utf8, then we can use to_jsstring */
s = to_jsstring(s);
} catch (e) {
/* otherwise push copy of raw array */
let copy = new Uint8Array(s.length);
copy.set(s);
s = copy;
}
buff.push(s);
};
lua_writeline = function() {
console.log.apply(console.log, buff);
buff = [];
};
}
} else {
lua_writestring = function(s) {
process.stdout.write(Buffer.from(s));
};
lua_writeline = function() {
process.stdout.write("\n");
};
}
const luaB_print = function(L) {
let n = lua_gettop(L); /* number of arguments */
lua_getglobal(L, to_luastring("tostring", true));
for (let i = 1; i <= n; i++) {
lua_pushvalue(L, -1); /* function to be called */
lua_pushvalue(L, i); /* value to print */
lua_call(L, 1, 1);
let s = lua_tolstring(L, -1);
if (s === null)
return luaL_error(L, to_luastring("'tostring' must return a string to 'print'"));
if (i > 1) lua_writestring(to_luastring("\t"));
lua_writestring(s);
lua_pop(L, 1);
}
lua_writeline();
return 0;
};
const luaB_tostring = function(L) {
luaL_checkany(L, 1);
luaL_tolstring(L, 1);
return 1;
};
const luaB_getmetatable = function(L) {
luaL_checkany(L, 1);
if (!lua_getmetatable(L, 1)) {
lua_pushnil(L);
return 1; /* no metatable */
}
luaL_getmetafield(L, 1, to_luastring("__metatable", true));
return 1; /* returns either __metatable field (if present) or metatable */
};
const luaB_setmetatable = function(L) {
let t = lua_type(L, 2);
luaL_checktype(L, 1, LUA_TTABLE);
luaL_argcheck(L, t === LUA_TNIL || t === LUA_TTABLE, 2, "nil or table expected");
if (luaL_getmetafield(L, 1, to_luastring("__metatable", true)) !== LUA_TNIL)
return luaL_error(L, to_luastring("cannot change a protected metatable"));
lua_settop(L, 2);
lua_setmetatable(L, 1);
return 1;
};
const luaB_rawequal = function(L) {
luaL_checkany(L, 1);
luaL_checkany(L, 2);
lua_pushboolean(L, lua_rawequal(L, 1, 2));
return 1;
};
const luaB_rawlen = function(L) {
let t = lua_type(L, 1);
luaL_argcheck(L, t === LUA_TTABLE || t === LUA_TSTRING, 1, "table or string expected");
lua_pushinteger(L, lua_rawlen(L, 1));
return 1;
};
const luaB_rawget = function(L) {
luaL_checktype(L, 1, LUA_TTABLE);
luaL_checkany(L, 2);
lua_settop(L, 2);
lua_rawget(L, 1);
return 1;
};
const luaB_rawset = function(L) {
luaL_checktype(L, 1, LUA_TTABLE);
luaL_checkany(L, 2);
luaL_checkany(L, 3);
lua_settop(L, 3);
lua_rawset(L, 1);
return 1;
};
const opts = [
"stop", "restart", "collect",
"count", "step", "setpause", "setstepmul",
"isrunning"
].map((e) => to_luastring(e));
const luaB_collectgarbage = function(L) {
luaL_checkoption(L, 1, "collect", opts);
luaL_optinteger(L, 2, 0);
luaL_error(L, to_luastring("lua_gc not implemented"));
};
const luaB_type = function(L) {
let t = lua_type(L, 1);
luaL_argcheck(L, t !== LUA_TNONE, 1, "value expected");
lua_pushstring(L, lua_typename(L, t));
return 1;
};
const pairsmeta = function(L, method, iszero, iter) {
luaL_checkany(L, 1);
if (luaL_getmetafield(L, 1, method) === LUA_TNIL) { /* no metamethod? */
lua_pushcfunction(L, iter); /* will return generator, */
lua_pushvalue(L, 1); /* state, */
if (iszero) lua_pushinteger(L, 0); /* and initial value */
else lua_pushnil(L);
} else {
lua_pushvalue(L, 1); /* argument 'self' to metamethod */
lua_call(L, 1, 3); /* get 3 values from metamethod */
}
return 3;
};
const luaB_next = function(L) {
luaL_checktype(L, 1, LUA_TTABLE);
lua_settop(L, 2); /* create a 2nd argument if there isn't one */
if (lua_next(L, 1))
return 2;
else {
lua_pushnil(L);
return 1;
}
};
const luaB_pairs = function(L) {
return pairsmeta(L, to_luastring("__pairs", true), 0, luaB_next);
};
/*
** Traversal function for 'ipairs'
*/
const ipairsaux = function(L) {
let i = luaL_checkinteger(L, 2) + 1;
lua_pushinteger(L, i);
return lua_geti(L, 1, i) === LUA_TNIL ? 1 : 2;
};
/*
** 'ipairs' function. Returns 'ipairsaux', given "table", 0.
** (The given "table" may not be a table.)
*/
const luaB_ipairs = function(L) {
// Lua 5.2
// return pairsmeta(L, "__ipairs", 1, ipairsaux);
luaL_checkany(L, 1);
lua_pushcfunction(L, ipairsaux); /* iteration function */
lua_pushvalue(L, 1); /* state */
lua_pushinteger(L, 0); /* initial value */
return 3;
};
const b_str2int = function(s, base) {
try {
s = to_jsstring(s);
} catch (e) {
return null;
}
let r = /^[\t\v\f \n\r]*([+-]?)0*([0-9A-Za-z]+)[\t\v\f \n\r]*$/.exec(s);
if (!r) return null;
let v = parseInt(r[1]+r[2], base);
if (isNaN(v)) return null;
return v|0;
};
const luaB_tonumber = function(L) {
if (lua_type(L, 2) <= 0) { /* standard conversion? */
luaL_checkany(L, 1);
if (lua_type(L, 1) === LUA_TNUMBER) { /* already a number? */
lua_settop(L, 1);
return 1;
} else {
let s = lua_tostring(L, 1);
if (s !== null && lua_stringtonumber(L, s) === s.length+1)
return 1; /* successful conversion to number */
}
} else {
let base = luaL_checkinteger(L, 2);
luaL_checktype(L, 1, LUA_TSTRING); /* no numbers as strings */
let s = lua_tostring(L, 1);
luaL_argcheck(L, 2 <= base && base <= 36, 2, "base out of range");
let n = b_str2int(s, base);
if (n !== null) {
lua_pushinteger(L, n);
return 1;
}
}
lua_pushnil(L);
return 1;
};
const luaB_error = function(L) {
let level = luaL_optinteger(L, 2, 1);
lua_settop(L, 1);
if (lua_type(L, 1) === LUA_TSTRING && level > 0) {
luaL_where(L, level); /* add extra information */
lua_pushvalue(L, 1);
lua_concat(L, 2);
}
return lua_error(L);
};
const luaB_assert = function(L) {
if (lua_toboolean(L, 1)) /* condition is true? */
return lua_gettop(L); /* return all arguments */
else {
luaL_checkany(L, 1); /* there must be a condition */
lua_remove(L, 1); /* remove it */
lua_pushliteral(L, "assertion failed!"); /* default message */
lua_settop(L, 1); /* leave only message (default if no other one) */
return luaB_error(L); /* call 'error' */
}
};
const luaB_select = function(L) {
let n = lua_gettop(L);
if (lua_type(L, 1) === LUA_TSTRING && lua_tostring(L, 1)[0] === 35 /* '#'.charCodeAt(0) */) {
lua_pushinteger(L, n - 1);
return 1;
} else {
let i = luaL_checkinteger(L, 1);
if (i < 0) i = n + i;
else if (i > n) i = n;
luaL_argcheck(L, 1 <= i, 1, "index out of range");
return n - i;
}
};
/*
** Continuation function for 'pcall' and 'xpcall'. Both functions
** already pushed a 'true' before doing the call, so in case of success
** 'finishpcall' only has to return everything in the stack minus
** 'extra' values (where 'extra' is exactly the number of items to be
** ignored).
*/
const finishpcall = function(L, status, extra) {
if (status !== LUA_OK && status !== LUA_YIELD) { /* error? */
lua_pushboolean(L, 0); /* first result (false) */
lua_pushvalue(L, -2); /* error message */
return 2; /* return false, msg */
} else
return lua_gettop(L) - extra;
};
const luaB_pcall = function(L) {
luaL_checkany(L, 1);
lua_pushboolean(L, 1); /* first result if no errors */
lua_insert(L, 1); /* put it in place */
let status = lua_pcallk(L, lua_gettop(L) - 2, LUA_MULTRET, 0, 0, finishpcall);
return finishpcall(L, status, 0);
};
/*
** Do a protected call with error handling. After 'lua_rotate', the
** stack will have <f, err, true, f, [args...]>; so, the function passes
** 2 to 'finishpcall' to skip the 2 first values when returning results.
*/
const luaB_xpcall = function(L) {
let n = lua_gettop(L);
luaL_checktype(L, 2, LUA_TFUNCTION); /* check error function */
lua_pushboolean(L, 1); /* first result */
lua_pushvalue(L, 1); /* function */
lua_rotate(L, 3, 2); /* move them below function's arguments */
let status = lua_pcallk(L, n - 2, LUA_MULTRET, 2, 2, finishpcall);
return finishpcall(L, status, 2);
};
const load_aux = function(L, status, envidx) {
if (status === LUA_OK) {
if (envidx !== 0) { /* 'env' parameter? */
lua_pushvalue(L, envidx); /* environment for loaded function */
if (!lua_setupvalue(L, -2, 1)) /* set it as 1st upvalue */
lua_pop(L, 1); /* remove 'env' if not used by previous call */
}
return 1;
} else { /* error (message is on top of the stack) */
lua_pushnil(L);
lua_insert(L, -2); /* put before error message */
return 2; /* return nil plus error message */
}
};
/*
** reserved slot, above all arguments, to hold a copy of the returned
** string to avoid it being collected while parsed. 'load' has four
** optional arguments (chunk, source name, mode, and environment).
*/
const RESERVEDSLOT = 5;
/*
** Reader for generic 'load' function: 'lua_load' uses the
** stack for internal stuff, so the reader cannot change the
** stack top. Instead, it keeps its resulting string in a
** reserved slot inside the stack.
*/
const generic_reader = function(L, ud) {
luaL_checkstack(L, 2, "too many nested functions");
lua_pushvalue(L, 1); /* get function */
lua_call(L, 0, 1); /* call it */
if (lua_isnil(L, -1)) {
lua_pop(L, 1); /* pop result */
return null;
} else if (!lua_isstring(L, -1))
luaL_error(L, to_luastring("reader function must return a string"));
lua_replace(L, RESERVEDSLOT); /* save string in reserved slot */
return lua_tostring(L, RESERVEDSLOT);
};
const luaB_load = function(L) {
let s = lua_tostring(L, 1);
let mode = luaL_optstring(L, 3, "bt");
let env = !lua_isnone(L, 4) ? 4 : 0; /* 'env' index or 0 if no 'env' */
let status;
if (s !== null) { /* loading a string? */
let chunkname = luaL_optstring(L, 2, s);
status = luaL_loadbufferx(L, s, s.length, chunkname, mode);
} else { /* loading from a reader function */
let chunkname = luaL_optstring(L, 2, "=(load)");
luaL_checktype(L, 1, LUA_TFUNCTION);
lua_settop(L, RESERVEDSLOT); /* create reserved slot */
status = lua_load(L, generic_reader, null, chunkname, mode);
}
return load_aux(L, status, env);
};
const luaB_loadfile = function(L) {
let fname = luaL_optstring(L, 1, null);
let mode = luaL_optstring(L, 2, null);
let env = !lua_isnone(L, 3) ? 3 : 0; /* 'env' index or 0 if no 'env' */
let status = luaL_loadfilex(L, fname, mode);
return load_aux(L, status, env);
};
const dofilecont = function(L, d1, d2) {
return lua_gettop(L) - 1;
};
const luaB_dofile = function(L) {
let fname = luaL_optstring(L, 1, null);
lua_settop(L, 1);
if (luaL_loadfile(L, fname) !== LUA_OK)
return lua_error(L);
lua_callk(L, 0, LUA_MULTRET, 0, dofilecont);
return dofilecont(L, 0, 0);
};
const base_funcs = {
"assert": luaB_assert,
"collectgarbage": luaB_collectgarbage,
"dofile": luaB_dofile,
"error": luaB_error,
"getmetatable": luaB_getmetatable,
"ipairs": luaB_ipairs,
"load": luaB_load,
"loadfile": luaB_loadfile,
"next": luaB_next,
"pairs": luaB_pairs,
"pcall": luaB_pcall,
"print": luaB_print,
"rawequal": luaB_rawequal,
"rawget": luaB_rawget,
"rawlen": luaB_rawlen,
"rawset": luaB_rawset,
"select": luaB_select,
"setmetatable": luaB_setmetatable,
"tonumber": luaB_tonumber,
"tostring": luaB_tostring,
"type": luaB_type,
"xpcall": luaB_xpcall
};
const luaopen_base = function(L) {
/* open lib into global table */
lua_pushglobaltable(L);
luaL_setfuncs(L, base_funcs, 0);
/* set global _G */
lua_pushvalue(L, -1);
lua_setfield(L, -2, to_luastring("_G"));
/* set global _VERSION */
lua_pushliteral(L, LUA_VERSION);
lua_setfield(L, -2, to_luastring("_VERSION"));
return 1;
};
module.exports.luaopen_base = luaopen_base;