mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-08 18:51:24 +03:00
165 lines
5.0 KiB
JavaScript
165 lines
5.0 KiB
JavaScript
import luaparse from "luaparse"
|
|
import {
|
|
LuaClosure,
|
|
LuaCoroutine,
|
|
LuaError,
|
|
LuaTable,
|
|
NativeFunction,
|
|
arith,
|
|
compare,
|
|
concat,
|
|
getMetatable,
|
|
index,
|
|
len,
|
|
luaToNumber,
|
|
newindex,
|
|
shortSrc,
|
|
truthy,
|
|
typeName,
|
|
} from "v8lua/src/runtime.js"
|
|
import { Call, runFrames } from "./evaluate.js"
|
|
|
|
const boundaryError = "attempt to yield across metamethod/C-call boundary"
|
|
|
|
export class VM {
|
|
ops = {
|
|
table: () => new LuaTable(),
|
|
closure: (...args) => new LuaClosure(...args),
|
|
error: (message) => new LuaError(message),
|
|
stackOverflow: () => new LuaError("stack overflow"),
|
|
number: luaToNumber,
|
|
truthy,
|
|
index: (a, b) => this.atomic(index(a, b)),
|
|
newindex: (a, b, c) => this.atomic(newindex(a, b, c)),
|
|
len: (a) => this.atomic(len(a)),
|
|
arith: (op, a, b) => this.atomic(arith(op, a, b)),
|
|
compare: (op, a, b) => this.atomic(compare(op, a, b)),
|
|
concat: (a, b) => this.atomic(concat(a, b)),
|
|
}
|
|
poll() {
|
|
return false
|
|
}
|
|
charge() {}
|
|
*iterate(fn, args) {
|
|
return this.atomic(this.invoke(fn, args))
|
|
}
|
|
frameLimit = 20000
|
|
isClosure(fn) {
|
|
return fn instanceof LuaClosure
|
|
}
|
|
resolveCall(call) {
|
|
const fn = call.fn
|
|
if (fn instanceof LuaClosure || fn instanceof NativeFunction) return call
|
|
const handler = getMetatable(fn)?.get("__call")
|
|
if (!(handler instanceof LuaClosure || handler instanceof NativeFunction))
|
|
throw new LuaError(`attempt to call a ${typeName(fn)} value`)
|
|
return new Call(handler, [fn, ...call.args])
|
|
}
|
|
*native(fn, args) {
|
|
if (fn !== this.yieldFn) return this.atomic(fn.fn(this, args)) ?? []
|
|
if (this.thread === this.main || this.thread.barriers) throw new LuaError(boundaryError)
|
|
return yield args
|
|
}
|
|
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
|
|
try {
|
|
return yield* runFrames(this, frames, new Call(fn, args))
|
|
} 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)
|
|
}
|