mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-08 18:51:24 +03:00
204 lines
7.0 KiB
JavaScript
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)
|
|
}
|