Files

204 lines
7.0 KiB
JavaScript

import {
LuaClosure,
LuaCoroutine,
LuaError,
LuaTable,
NativeFunction,
getMetatable,
luaToNumber,
numberToString,
shortSrc,
tostringMM,
truthy,
typeName,
} from "v8lua/src/runtime.js"
import installMath from "v8lua/src/lib/math.js"
import installString from "v8lua/src/lib/string.js"
import installTable from "v8lua/src/lib/table.js"
import { checkNum, checkStr, checkTable } from "v8lua/src/lib/helpers.js"
function native(name, fn, required = 0) {
return new NativeFunction(name, function* (_, args) {
if (args.length < required) throw new LuaError(`bad argument #${required} to '${name}' (value expected)`)
return fn(...args)
})
}
function rethrow(value) {
const e = new LuaError(value)
e.positioned = true
throw e
}
export function install(vm) {
installMath(vm)
installString(vm)
installTable(vm)
const G = vm.globals
const table = G.get("table")
G.set("unpack", table.get("unpack"))
table.set("unpack", undefined)
G.get("string").set("dump", undefined) // no v8lua AST dumps or binary chunk loader
G.set("_VERSION", "Lua 5.1")
G.set("_G", G)
function load(source, name) {
try {
return [vm.compile(source, name)]
} catch (e) {
if (e instanceof LuaError) return [undefined, e.luaMessage]
throw e
}
}
function protect(fn, args) {
try {
return [true, ...vm.atomic(vm.invoke(fn, args))]
} catch (e) {
if (e instanceof LuaError) return [false, e.luaMessage]
throw e
}
}
function envTarget(target) {
if (target instanceof LuaClosure || target instanceof NativeFunction) return target
if (target === 0) return vm.thread
const frame = Number.isInteger(target) && target > 0 && vm.thread.frames.at(-target)
if (!frame) throw new LuaError("invalid function or stack level")
return frame.closure
}
const next = native("next", (t, key) => checkTable(t, 1, "next").next(key) ?? [undefined])
const inext = native("ipairs iterator", (t, key) => {
const i = checkNum(key, 2, "ipairs") + 1
const v = checkTable(t, 1, "ipairs").get(i)
return v === undefined ? [] : [i, v]
})
G.set("next", next)
const base = {
type: (value) => [typeName(value)],
tostring: (v) => [vm.atomic(tostringMM(v))],
tonumber: (v, base = 10) => {
const radix = Math.trunc(checkNum(base, 2, "tonumber"))
if (radix === 10) return [luaToNumber(v)]
if (!(radix >= 2 && radix <= 36)) throw new LuaError("base out of range")
return [luaToNumber(checkStr(v, 1, "tonumber"), radix)]
},
print: (...args) => {
console.log(args.map((v) => checkStr(vm.atomic(vm.invoke(G.get("tostring"), [v]))[0], 1, "print")).join("\t"))
return []
},
error: (value, level = 1) => {
if (checkNum(level, 2, "error") <= 0) rethrow(value)
const msg = typeof value === "number" ? numberToString(value) : value
const frame = level > 0 && vm.thread.frames.at(-level)
rethrow(frame && typeof msg === "string" ? `${shortSrc(frame.closure.chunkname)}:${frame.line}: ${msg}` : msg)
},
assert: (...args) => {
if (!truthy(args[0])) throw new LuaError(checkStr(args[1] ?? "assertion failed!", 2, "assert"))
return args
},
pcall: (fn, ...args) => protect(fn, args),
xpcall: (fn, handler) => {
const result = protect(fn, [])
if (result[0]) return result
const handled = protect(handler, [result[1]])
return [false, handled[0] ? handled[1] : "error in error handling"]
},
pairs: (v) => [next, checkTable(v, 1, "pairs"), undefined],
ipairs: (v) => [inext, checkTable(v, 1, "ipairs"), 0],
select: (n, ...args) => {
if (n === "#") return [args.length]
const i = Math.trunc(checkNum(n, 1, "select"))
if (i === 0 || i < -args.length) throw new LuaError("index out of range")
return args.slice(i < 0 ? i : i - 1)
},
rawget: (t, k) => [checkTable(t, 1, "rawget").get(k)],
rawset: (t, k, v) => {
checkTable(t, 1, "rawset").set(k, v)
return [t]
},
rawequal: (a, b) => [a === b],
getmetatable: (v) => {
const mt = getMetatable(v)
return [mt?.get("__metatable") ?? mt]
},
setmetatable: (t, mt) => {
checkTable(t, 1, "setmetatable")
if (mt !== undefined) checkTable(mt, 2, "setmetatable")
if (t.metatable?.get("__metatable") !== undefined) throw new LuaError("cannot change a protected metatable")
t.metatable = mt
return [t]
},
getfenv: (f = 1) => [envTarget(f).env ?? vm.thread.env ?? G],
setfenv: (f, t) => {
const target = envTarget(f)
if (target instanceof NativeFunction) throw new LuaError("cannot change environment of given object")
target.env = checkTable(t, 2, "setfenv")
return f === 0 ? [] : [target]
},
loadstring: (source, name) => {
const s = checkStr(source, 1, "loadstring")
return load(s, name ?? s)
},
load: (reader, name) => {
if (typeName(reader) !== "function") throw new LuaError("reader function expected")
const parts = []
for (;;) {
const result = protect(reader, [])
if (!result[0]) return [undefined, result[1]]
if (result[1] === undefined || result[1] === "") return load(parts.join(""), name ?? "=(load)")
if (typeof result[1] !== "string") return [undefined, "reader function must return a string"]
parts.push(result[1])
}
},
collectgarbage: (mode = "collect") => {
if (mode === "step") return [false]
if (["count", "collect", "stop", "restart", "setpause", "setstepmul"].includes(mode)) return [0]
throw new LuaError("invalid collectgarbage option")
},
}
// An explicit nil is a valid value; a missing argument isn't. The typed
// library arguments already have their own checks (e.g. checkTable).
const required = {
type: 1,
tonumber: 1,
tostring: 1,
assert: 1,
pcall: 1,
xpcall: 2,
rawget: 2,
rawset: 3,
rawequal: 2,
getmetatable: 1,
setmetatable: 2,
}
for (const [name, fn] of Object.entries(base)) G.set(name, native(name, fn, required[name]))
const co = new LuaTable()
const methods = {
create: (fn) => [vm.create(fn)],
resume: (thread, ...args) => vm.resume(thread, args),
running: () => [vm.thread === vm.main ? undefined : vm.thread],
status: (thread) => {
if (!(thread instanceof LuaCoroutine)) throw new LuaError("thread expected")
return [thread.status]
},
wrap: (fn) => {
const thread = vm.create(fn)
return [
native("wrapped", (...args) => {
const [ok, ...values] = vm.resume(thread, args)
if (!ok) throw new LuaError(values[0])
return values
}),
]
},
// VM.invoke handles a direct call by identity. Reaching this native body
// means a library callback (e.g. __index) called yield across a C boundary.
yield: () => {
throw new LuaError("attempt to yield across metamethod/C-call boundary")
},
}
for (const [name, fn] of Object.entries(methods)) co.set(name, native(name, fn))
vm.yieldFn = co.get("yield")
G.set("coroutine", co)
}