Files

160 lines
5.1 KiB
JavaScript

import luaparse from "luaparse"
import {
LuaClosure,
LuaCoroutine,
LuaError,
LuaTable,
NativeFunction,
getMetatable,
shortSrc,
typeName,
} from "v8lua/src/runtime.js"
import { Call, Frame } from "./evaluate.js"
const boundaryError = "attempt to yield across metamethod/C-call boundary"
export class VM {
globals = new LuaTable()
main = { frames: [], barriers: 0 }
thread = this.main
hook = null // v8lua native library callbacks do not run debug hooks.
compile(source, chunkname) {
let proto
try {
proto = luaparse.parse(source, {
luaVersion: "5.1",
encodingMode: "pseudo-latin1",
locations: true,
comments: false,
onCreateNode(node) {
if (node.type === "FunctionDeclaration" && node.parameters.at(-1)?.type === "VarargLiteral") {
node.legacyArg = !usesVarargs(node.body)
}
},
})
} catch (e) {
if (!(e instanceof SyntaxError)) throw e
throw new LuaError(`${shortSrc(chunkname)}:${e.line}: ${e.message}`)
}
const chunk = new LuaClosure(proto, null, chunkname, this)
chunk.env = this.thread.env ?? this.globals
chunk.chunkname = chunkname
return chunk
}
// A synchronous library or metamethod callback may run Lua but cannot yield
// to its caller. Resuming a *different* thread starts with its own barriers.
atomic(code) {
const thread = this.thread
thread.barriers++
try {
const result = code.next()
if (!result.done) throw new Error("unexpected suspension across VM boundary")
return result.value
} finally {
thread.barriers--
}
}
*invoke(fn, args) {
const thread = this.thread
const frames = thread.frames
const base = frames.length
let call = new Call(fn, args)
let values
try {
for (;;) {
if (call) {
let { fn, args } = call
if (!(fn instanceof LuaClosure || fn instanceof NativeFunction)) {
const handler = getMetatable(fn)?.get("__call")
if (!(handler instanceof LuaClosure || handler instanceof NativeFunction))
throw new LuaError(`attempt to call a ${typeName(fn)} value`)
args = [fn, ...args]
fn = handler
}
if (fn instanceof LuaClosure) {
if (frames.length > base && frames.at(-1).tail) frames.pop()
if (frames.length >= 20000) throw new LuaError("stack overflow")
frames.push(new Frame(this, fn, args))
values = undefined
} else if (fn === this.yieldFn) {
if (thread === this.main || thread.barriers) throw new LuaError(boundaryError)
values = yield args
} else {
values = this.atomic(fn.fn(this, args)) ?? []
}
call = undefined
}
if (frames.length === base) return values
const frame = frames.at(-1)
if (frame.tail) {
frames.pop() // a tail-called native just returned
continue
}
const step = frame.code.next(values)
if (step.value instanceof Call) {
frame.tail = step.done
call = step.value
continue
}
if (!step.done) throw new Error("invalid Lua frame suspension")
values = step.value ?? []
frames.pop()
}
} catch (e) {
const err = e instanceof RangeError ? new LuaError("stack overflow") : e
if (err instanceof LuaError && !err.positioned && frames.length > base) {
const frame = frames.at(-1)
if (typeof err.luaMessage === "string")
err.message = err.luaMessage = `${shortSrc(frame.closure.chunkname)}:${frame.line}: ${err.luaMessage}`
err.positioned = true
}
throw err
} finally {
frames.length = base
}
}
create(fn) {
if (!(fn instanceof LuaClosure)) throw new LuaError("Lua function expected")
const thread = new LuaCoroutine(fn)
thread.frames = []
thread.barriers = 0
thread.env = this.thread.env ?? this.globals
return thread
}
resume(thread, args) {
if (!(thread instanceof LuaCoroutine)) throw new LuaError("thread expected")
if (thread.status !== "suspended") return [false, `cannot resume ${thread.status} coroutine`]
const parent = this.thread
parent.status = "normal"
thread.status = "running"
this.thread = thread
try {
thread.it ??= this.invoke(thread.fn, args)
const result = thread.it.next(args)
thread.status = result.done ? "dead" : "suspended"
return [true, ...result.value]
} catch (e) {
thread.status = "dead"
if (e instanceof LuaError) return [false, e.luaMessage]
throw e
} finally {
this.thread = parent
parent.status = "running"
if (thread.status === "dead") thread.it = thread.fn = null
}
}
}
// Lua 5.1 compatibility: a vararg function gets a local arg table only
// when it doesn't use `...` itself. Nested function bodies don't count.
function usesVarargs(node) {
if (node === null || typeof node !== "object" || node.type === "FunctionDeclaration") return false
return node.type === "VarargLiteral" || Object.values(node).some(usesVarargs)
}