mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-08 18:51:24 +03:00
636 lines
21 KiB
JavaScript
636 lines
21 KiB
JavaScript
import { Buffer } from "node:buffer"
|
|
import luaparse from "luaparse"
|
|
import { Call, checkpoint, runFrames } from "./evaluate.js"
|
|
|
|
// Limits are per invocation, including host arguments, replies and the final
|
|
// result. Work is cumulative (not refunded by deleting a key or tail-calling).
|
|
const sourceLimit = 32768
|
|
const stringLimit = 256 * 1024
|
|
const entriesLimit = 8192
|
|
const depthLimit = 48
|
|
const jsonNull = Symbol("json.null")
|
|
const utf8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true })
|
|
const tick = Object.freeze({ type: "tick" })
|
|
const truthy = (v) => v !== undefined && v !== false
|
|
|
|
export class LuaFault extends Error {
|
|
constructor(kind, message, location) {
|
|
super(message)
|
|
this.kind = kind
|
|
this.location = location
|
|
}
|
|
}
|
|
|
|
function failure(message) {
|
|
const e = new LuaFault("ExecutionFailure", message)
|
|
e.catchable = true
|
|
return e
|
|
}
|
|
|
|
function limit(message) {
|
|
return new LuaFault("ExecutionFailure", message)
|
|
}
|
|
|
|
function invalid(message) {
|
|
const e = new LuaFault("InvalidDataValue", message)
|
|
e.catchable = true
|
|
return e
|
|
}
|
|
|
|
// luaparse's lexer stores its state at module scope. Finish parsing before
|
|
// yielding; never leave a partially parsed chunk suspended during a host call.
|
|
function parse(source) {
|
|
if (typeof source !== "string") throw new LuaFault("ParseError", "Expected Lua source text")
|
|
if (source.length > sourceLimit || Buffer.byteLength(source) > sourceLimit)
|
|
throw new LuaFault("ParseError", "Lua source exceeds the size limit")
|
|
if (!source.isWellFormed()) throw new LuaFault("ParseError", "Lua source must be valid Unicode")
|
|
// Its long-string scanner compares every '=' on every closing bracket.
|
|
if (/\[={33}/.test(source)) throw new LuaFault("ParseError", "Lua long-bracket delimiter exceeds the limit")
|
|
let count = 0
|
|
const depths = new WeakMap()
|
|
try {
|
|
return luaparse.parse(Buffer.from(source).toString("latin1"), {
|
|
luaVersion: "5.1",
|
|
encodingMode: "pseudo-latin1",
|
|
comments: false,
|
|
locations: true,
|
|
onCreateNode(node) {
|
|
if (++count > 8192) throw new LuaFault("ParseError", "Lua syntax tree exceeds the size limit")
|
|
let depth = 1
|
|
for (const child of Object.values(node)) {
|
|
if (Array.isArray(child)) {
|
|
for (const item of child) depth = Math.max(depth, 1 + (depths.get(item) ?? 0))
|
|
} else depth = Math.max(depth, 1 + (depths.get(child) ?? 0))
|
|
}
|
|
if (depth > depthLimit) throw new LuaFault("ParseError", "Lua syntax nesting exceeds the depth limit")
|
|
depths.set(node, depth)
|
|
},
|
|
})
|
|
} catch (e) {
|
|
if (e instanceof LuaFault) throw e
|
|
if (e instanceof RangeError) throw new LuaFault("ParseError", "Lua syntax nesting exceeds the depth limit")
|
|
throw new LuaFault(
|
|
"ParseError",
|
|
e.message.slice(0, 512),
|
|
e.line ? { line: e.line, column: e.column + 1 } : undefined,
|
|
)
|
|
}
|
|
}
|
|
|
|
class Table {
|
|
data = new Map()
|
|
constructor(vm, array = false) {
|
|
vm.charge(8)
|
|
this.vm = vm
|
|
this.array = array
|
|
}
|
|
get(key) {
|
|
this.vm.key(key)
|
|
return this.data.get(key)
|
|
}
|
|
set(key, value) {
|
|
this.vm.key(key)
|
|
if (key === undefined || Number.isNaN(key)) throw failure("Invalid Lua table key")
|
|
if (this.locked) throw failure("Cannot change a read-only Lua table")
|
|
if (value === undefined) return this.data.delete(key)
|
|
if (this.data.size >= entriesLimit && !this.data.has(key)) throw limit("Lua table exceeds the entry limit")
|
|
this.vm.charge(4)
|
|
this.data.set(key, value)
|
|
}
|
|
}
|
|
|
|
class Closure {
|
|
constructor(proto, scope) {
|
|
this.proto = proto
|
|
this.scope = scope
|
|
}
|
|
}
|
|
class Host {
|
|
constructor(path) {
|
|
this.path = path
|
|
}
|
|
}
|
|
class Native {
|
|
constructor(run) {
|
|
this.run = run
|
|
}
|
|
}
|
|
|
|
class LuaHostFailure {
|
|
constructor(message) {
|
|
this.message = message
|
|
}
|
|
}
|
|
export function hostFailure(message) {
|
|
return new LuaHostFailure(message)
|
|
}
|
|
|
|
// Account for JSON escaping as well as UTF-8: a control byte can expand to
|
|
// six ASCII bytes. One request shares its budget across all its arguments.
|
|
class Boundary {
|
|
bytes = 1024 * 1024
|
|
constructor(vm) {
|
|
this.vm = vm
|
|
}
|
|
charge(n) {
|
|
this.bytes -= n
|
|
if (this.bytes < 0) throw new LuaFault("InvalidDataValue", "Lua JSON exceeds the boundary byte limit")
|
|
// Core progress can retain each admitted call's input. Charge every copy,
|
|
// even when the guest reuses the very same value or callable path.
|
|
this.vm.traffic -= n
|
|
if (this.vm.traffic < 0) throw limit("Lua cumulative host traffic limit exceeded")
|
|
}
|
|
string(value) {
|
|
this.charge(Buffer.byteLength(JSON.stringify(value)))
|
|
}
|
|
}
|
|
|
|
function type(v) {
|
|
if (v === undefined) return "nil"
|
|
if (v === jsonNull) return "userdata"
|
|
if (v instanceof Table) return "table"
|
|
if (v instanceof Closure || v instanceof Native || v instanceof Host) return "function"
|
|
return typeof v
|
|
}
|
|
|
|
function text(v) {
|
|
if (v === jsonNull) return "null"
|
|
if (["number", "string", "boolean"].includes(typeof v)) return String(v)
|
|
return type(v)
|
|
}
|
|
|
|
function string(v) {
|
|
if (typeof v !== "string" && typeof v !== "number") throw failure("Expected a Lua string or number")
|
|
return String(v)
|
|
}
|
|
|
|
function table(v) {
|
|
if (!(v instanceof Table)) throw failure("Expected a Lua table")
|
|
return v
|
|
}
|
|
|
|
class Sandbox {
|
|
fuel = 500_000
|
|
slice = 256
|
|
nativeFuel = Infinity
|
|
bytes = 8 * 1024 * 1024
|
|
traffic = 4 * 1024 * 1024
|
|
frameLimit = 96
|
|
globals = new Table(this)
|
|
protected = new Set()
|
|
frames = []
|
|
barriers = 0
|
|
charge(work) {
|
|
this.fuel -= work
|
|
this.slice -= work
|
|
this.nativeFuel -= work
|
|
if (this.nativeFuel < 0) throw limit("Lua native call work limit exceeded")
|
|
if (this.fuel < 0) throw limit("Lua execution work limit exceeded")
|
|
}
|
|
poll(node) {
|
|
this.node = node ?? this.node
|
|
this.charge(1)
|
|
if (this.slice > 0) return false
|
|
this.slice = 256
|
|
return true
|
|
}
|
|
// Charge before materializing a string, including in JS-native operations.
|
|
size(length) {
|
|
if (!Number.isSafeInteger(length) || length < 0 || length > stringLimit)
|
|
throw limit("Lua string exceeds the size limit")
|
|
this.bytes -= length
|
|
if (this.bytes < 0) throw limit("Lua allocation limit exceeded")
|
|
this.charge(1 + Math.ceil(length / 32))
|
|
}
|
|
key(key) {
|
|
this.charge(typeof key === "string" ? 1 + Math.ceil(key.length / 32) : 1)
|
|
}
|
|
number(v) {
|
|
if (typeof v === "number") return v
|
|
if (typeof v !== "string") return undefined
|
|
this.key(v)
|
|
const trimmed = v.trim()
|
|
if (!/^[+-]?(?:0[xX][\da-fA-F]+|(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?)$/.test(trimmed)) return undefined
|
|
if (/^[+-]0[xX]/.test(trimmed)) return (trimmed[0] === "-" ? -1 : 1) * Number(trimmed.slice(1))
|
|
return Number(trimmed)
|
|
}
|
|
numeric(v) {
|
|
const n = this.number(v)
|
|
if (n === undefined) throw failure("Expected a Lua number")
|
|
return n
|
|
}
|
|
length(v) {
|
|
if (typeof v === "string") return v.length
|
|
const seq = table(v)
|
|
let i = 0
|
|
while (seq.get(i + 1) !== undefined) i++
|
|
return i
|
|
}
|
|
ops = {
|
|
truthy,
|
|
error: failure,
|
|
stackOverflow: () => limit("Lua stack overflow"),
|
|
table: () => new Table(this),
|
|
closure: (proto, scope) => new Closure(proto, scope),
|
|
number: (v) => this.number(v),
|
|
len: (v) => this.length(v),
|
|
index: (v, k) => {
|
|
if (v instanceof Table) return v.get(k)
|
|
if (typeof v === "string") return this.strings.get(k)
|
|
if (v instanceof Host && v.path[0] === "tools" && typeof k === "string") {
|
|
if (v.path.length >= 32 || k.length > 256) throw limit("Lua tool path exceeds the size limit")
|
|
this.charge(v.path.length)
|
|
return new Host([...v.path, this.decode(k)])
|
|
}
|
|
throw failure(`Cannot index Lua ${type(v)}`)
|
|
},
|
|
newindex: (v, k, value) => {
|
|
if (v === this.globals && this.protected.has(k)) throw failure("Cannot overwrite a reserved Lua global")
|
|
table(v).set(k, value)
|
|
},
|
|
compare: (op, a, b) => {
|
|
this.key(a)
|
|
this.key(b)
|
|
if (op === "eq") return a === b
|
|
if (typeof a !== typeof b || (typeof a !== "number" && typeof a !== "string"))
|
|
throw failure("Incompatible Lua comparison")
|
|
return op === "lt" ? a < b : a <= b
|
|
},
|
|
concat: (a, b) => {
|
|
const left = string(a),
|
|
right = string(b)
|
|
this.size(left.length + right.length)
|
|
return left + right
|
|
},
|
|
arith: (op, a, b) => {
|
|
const x = this.numeric(a),
|
|
y = this.numeric(b)
|
|
switch (op) {
|
|
case "unm":
|
|
return -x
|
|
case "add":
|
|
return x + y
|
|
case "sub":
|
|
return x - y
|
|
case "mul":
|
|
return x * y
|
|
case "div":
|
|
return x / y
|
|
case "pow":
|
|
return x ** y
|
|
case "mod":
|
|
return x - Math.floor(x / y) * y
|
|
default:
|
|
throw failure("Unknown arithmetic operator")
|
|
}
|
|
},
|
|
}
|
|
isClosure(fn) {
|
|
return fn instanceof Closure
|
|
}
|
|
resolveCall(call) {
|
|
this.charge(1 + call.args.length)
|
|
if (call.args.length > 1024) throw limit("Lua call argument limit exceeded")
|
|
return call
|
|
}
|
|
*iterate(fn, args) {
|
|
return yield new Call(fn, args)
|
|
}
|
|
*native(fn, args) {
|
|
if (fn === this.pcall) {
|
|
// A second trampoline shares the same frame stack and all budgets.
|
|
// Bound nested protection separately: even pcall(pcall, ...) with no
|
|
// Lua activations uses another JS generator delegation.
|
|
if (this.barriers >= 24) throw limit("Lua protected call depth limit exceeded")
|
|
this.barriers++
|
|
const base = this.frames.length
|
|
try {
|
|
const result = yield* runFrames(this, this.frames, new Call(args[0], args.slice(1)))
|
|
this.charge(result.length + 1)
|
|
return [true, ...result]
|
|
} catch (e) {
|
|
if (!(e instanceof LuaFault) || !e.catchable) throw e
|
|
return [false, Object.hasOwn(e, "value") ? e.value : this.encode(e.message)]
|
|
} finally {
|
|
this.barriers--
|
|
this.frames.length = base
|
|
}
|
|
}
|
|
if (fn instanceof Native) {
|
|
// These synchronous allowlisted functions never call back into Lua.
|
|
// Individually bounded inputs can still multiply (e.g. 1,024 large
|
|
// numeric strings supplied to math.max) inside a single scheduler step.
|
|
this.nativeFuel = 32768
|
|
try {
|
|
return fn.run(args)
|
|
} finally {
|
|
this.nativeFuel = Infinity
|
|
}
|
|
}
|
|
if (!(fn instanceof Host)) throw failure(`Cannot call Lua ${type(fn)}`)
|
|
// Even empty calls cause host admission and progress bookkeeping.
|
|
this.charge(256)
|
|
const data = []
|
|
const budget = new Boundary(this)
|
|
budget.charge(2 + Math.max(0, args.length - 1) + 2 + Math.max(0, fn.path.length - 1))
|
|
for (const name of fn.path) budget.string(name)
|
|
for (const arg of args) data.push(yield* this.export(arg, new Set(), 0, budget))
|
|
const reply = yield { type: "call", path: fn.path.slice(), args: data, protected: this.barriers > 0 }
|
|
const inbound = new Boundary(this)
|
|
if (reply instanceof LuaHostFailure) {
|
|
inbound.string(reply.message)
|
|
throw failure(reply.message)
|
|
}
|
|
return [yield* this.import(reply, 0, inbound)]
|
|
}
|
|
encode(s) {
|
|
if (s.length > stringLimit) throw limit("Lua string exceeds the size limit")
|
|
if (!s.isWellFormed()) throw invalid("Lua input must be valid Unicode")
|
|
this.size(Buffer.byteLength(s))
|
|
return Buffer.from(s).toString("latin1")
|
|
}
|
|
decode(s) {
|
|
this.size(s.length)
|
|
try {
|
|
return utf8.decode(Buffer.from(s, "latin1"))
|
|
} catch {
|
|
throw invalid("Lua byte string is not valid UTF-8")
|
|
}
|
|
}
|
|
*import(v, depth, budget) {
|
|
if (this.poll()) yield checkpoint
|
|
if (depth > depthLimit) throw new LuaFault("InvalidDataValue", "Lua input exceeds the depth limit")
|
|
if (v === undefined || v === null || typeof v === "boolean" || Number.isFinite(v)) {
|
|
budget.charge(v === undefined ? 4 : String(v).length)
|
|
return v === null ? jsonNull : v
|
|
}
|
|
if (typeof v === "string") {
|
|
const value = this.encode(v)
|
|
budget.string(v)
|
|
return value
|
|
}
|
|
if (typeof v !== "object") throw invalid("Expected JSON data")
|
|
const array = Array.isArray(v)
|
|
const result = new Table(this, array)
|
|
// The driver owns validating/canonicalizing host objects before it resumes
|
|
// us. Never put the host container or function itself into the Lua heap.
|
|
if (array) {
|
|
if (v.length > entriesLimit) throw new LuaFault("InvalidDataValue", "Lua input exceeds the entry limit")
|
|
budget.charge(2 + Math.max(0, v.length - 1))
|
|
for (let i = 0; i < v.length; i++) result.set(i + 1, yield* this.import(v[i], depth + 1, budget))
|
|
} else {
|
|
budget.charge(2)
|
|
for (const key in v) {
|
|
if (!Object.hasOwn(v, key)) continue
|
|
const name = this.encode(key)
|
|
budget.string(key)
|
|
budget.charge(result.data.size ? 2 : 1)
|
|
result.set(name, yield* this.import(v[key], depth + 1, budget))
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
*export(v, seen, depth, budget) {
|
|
if (this.poll()) yield checkpoint
|
|
if (depth > 32) throw new LuaFault("InvalidDataValue", "Lua result exceeds the depth limit")
|
|
if (v === undefined || v === jsonNull || typeof v === "boolean" || Number.isFinite(v)) {
|
|
budget.charge(v === undefined || v === jsonNull ? 4 : String(v).length)
|
|
return v === jsonNull ? null : v
|
|
}
|
|
if (typeof v === "string") {
|
|
const value = this.decode(v)
|
|
budget.string(value)
|
|
return value
|
|
}
|
|
if (!(v instanceof Table)) throw invalid(`Cannot pass Lua ${type(v)} as JSON`)
|
|
if (seen.has(v)) throw invalid("Circular Lua table cannot cross the JSON boundary")
|
|
seen.add(v)
|
|
const first = v.data.keys().next()
|
|
const array = first.done ? v.array : typeof first.value === "number"
|
|
const out = array ? [] : Object.create(null)
|
|
budget.charge(2 + Math.max(0, v.data.size - 1))
|
|
for (const [k, item] of v.data) {
|
|
// Charge each expanded edge, not just unique VM tables. Never size a
|
|
// JS array from a Lua index: sparse integer keys can be enormous.
|
|
this.charge(16)
|
|
if (array) {
|
|
if (!Number.isSafeInteger(k) || k < 1 || k > v.data.size)
|
|
throw invalid("Expected contiguous Lua array keys from 1")
|
|
out[k - 1] = yield* this.export(item, seen, depth + 1, budget)
|
|
} else {
|
|
if (typeof k !== "string" || k === "__proto__") throw invalid("Invalid Lua JSON object key")
|
|
const key = this.decode(k)
|
|
budget.charge(1)
|
|
budget.string(key)
|
|
out[key] = yield* this.export(item, seen, depth + 1, budget)
|
|
}
|
|
}
|
|
seen.delete(v)
|
|
return out
|
|
}
|
|
}
|
|
|
|
export const luaBuiltins = Object.freeze([
|
|
"assert",
|
|
"error",
|
|
"type",
|
|
"tostring",
|
|
"tonumber",
|
|
"pairs",
|
|
"ipairs",
|
|
"string",
|
|
"table",
|
|
"math",
|
|
"json",
|
|
"pcall",
|
|
])
|
|
|
|
function install(vm) {
|
|
// Case conversion is ASCII-only on byte strings. Avoid a regex callback
|
|
// and temporary match allocation for each individual byte.
|
|
function changeCase(v, start, delta) {
|
|
const s = string(v)
|
|
vm.size(s.length)
|
|
const bytes = Buffer.from(s, "latin1")
|
|
for (let i = 0; i < bytes.length; i++) if (bytes[i] >= start && bytes[i] < start + 26) bytes[i] += delta
|
|
return bytes.toString("latin1")
|
|
}
|
|
function raised(value) {
|
|
// Rendering for diagnostics must not truncate a UTF-8 character into a
|
|
// second error. Protected handlers get the original value, even nil.
|
|
const e = failure(Buffer.from(text(value).slice(0, 512), "latin1").toString("utf8"))
|
|
e.value = value
|
|
return e
|
|
}
|
|
function library(methods) {
|
|
const lib = new Table(vm)
|
|
for (const [k, v] of Object.entries(methods)) lib.set(k, typeof v === "function" ? new Native(v) : v)
|
|
lib.locked = true
|
|
return lib
|
|
}
|
|
const base = {
|
|
type: ([v]) => [type(v)],
|
|
tostring: ([v]) => [text(v)],
|
|
tonumber: ([v, radix]) => {
|
|
if (radix !== undefined && radix !== 10) throw failure("Only base 10 tonumber is supported")
|
|
return [vm.number(v)]
|
|
},
|
|
assert: (args) => {
|
|
if (!truthy(args[0])) throw raised(args[1] ?? "assertion failed!")
|
|
return args
|
|
},
|
|
error: ([value]) => {
|
|
throw raised(value)
|
|
},
|
|
pairs: ([v]) => {
|
|
const seq = table(v)
|
|
vm.charge(seq.data.size)
|
|
// Iterating a live JS Map across arbitrary guest mutations can retain
|
|
// its deleted slots. Snapshot at most entriesLimit keys and skip deletes.
|
|
const keys = [...seq.data.keys()]
|
|
let i = 0
|
|
return [
|
|
new Native(() => {
|
|
while (i < keys.length) {
|
|
const key = keys[i++]
|
|
const value = seq.get(key)
|
|
if (value !== undefined) return [key, value]
|
|
}
|
|
return []
|
|
}),
|
|
v,
|
|
]
|
|
},
|
|
ipairs: ([v]) => {
|
|
const seq = table(v)
|
|
return [
|
|
new Native(([, i]) => {
|
|
const key = vm.numeric(i) + 1
|
|
const value = seq.get(key)
|
|
return value === undefined ? [] : [key, value]
|
|
}),
|
|
seq,
|
|
0,
|
|
]
|
|
},
|
|
}
|
|
for (const [name, fn] of Object.entries(base)) vm.globals.set(name, new Native(fn))
|
|
vm.pcall = new Native()
|
|
vm.globals.set("pcall", vm.pcall)
|
|
vm.strings = library({
|
|
len: ([v]) => [string(v).length],
|
|
sub: ([v, start, end = -1]) => {
|
|
const s = string(v)
|
|
const pos = (v) => {
|
|
const n = Math.trunc(vm.numeric(v))
|
|
return n < 0 ? s.length + n + 1 : n
|
|
}
|
|
const i = Math.max(1, pos(start)),
|
|
j = Math.min(s.length, pos(end))
|
|
vm.size(Math.max(0, j - i + 1))
|
|
return [s.slice(i - 1, Math.max(0, j))]
|
|
},
|
|
rep: ([v, count]) => {
|
|
const s = string(v),
|
|
n = Math.max(0, Math.trunc(vm.numeric(count)))
|
|
vm.size(s.length * n)
|
|
return [s.length ? s.repeat(n) : ""]
|
|
},
|
|
lower: ([v]) => [changeCase(v, 65, 32)],
|
|
upper: ([v]) => [changeCase(v, 97, -32)],
|
|
// Deliberately no Lua patterns, regex translation or printf formatting.
|
|
})
|
|
vm.globals.set("string", vm.strings)
|
|
vm.globals.set(
|
|
"table",
|
|
library({
|
|
insert: ([v, ...args]) => {
|
|
if (args.length !== 1) throw failure("Only table.insert(table, value) is supported")
|
|
table(v).set(vm.length(v) + 1, args[0])
|
|
return []
|
|
},
|
|
concat: ([v, sep = "", first = 1, last]) => {
|
|
const seq = table(v),
|
|
delimiter = string(sep)
|
|
const i = vm.numeric(first),
|
|
j = last === undefined ? vm.length(v) : vm.numeric(last)
|
|
if (!Number.isSafeInteger(i) || !Number.isSafeInteger(j) || j - i >= entriesLimit)
|
|
throw limit("Invalid table.concat range")
|
|
const parts = []
|
|
let length = 0
|
|
for (let k = i; k <= j; k++) {
|
|
const part = string(seq.get(k))
|
|
length += part.length + (parts.length ? delimiter.length : 0)
|
|
if (length > stringLimit) throw limit("Lua string exceeds the size limit")
|
|
parts.push(part)
|
|
}
|
|
vm.size(length)
|
|
return [parts.join(delimiter)]
|
|
},
|
|
}),
|
|
)
|
|
vm.globals.set(
|
|
"json",
|
|
library({
|
|
array: (args) => {
|
|
if (args.length) throw failure("json.array() takes no arguments")
|
|
return [new Table(vm, true)]
|
|
},
|
|
null: jsonNull,
|
|
}),
|
|
)
|
|
vm.globals.set(
|
|
"math",
|
|
library({
|
|
...Object.fromEntries(["abs", "ceil", "floor", "sqrt"].map((k) => [k, ([v]) => [Math[k](vm.numeric(v))]])),
|
|
min: (args) => [Math.min(...args.map((v) => vm.numeric(v)))],
|
|
max: (args) => [Math.max(...args.map((v) => vm.numeric(v)))],
|
|
pi: Math.PI,
|
|
huge: Infinity,
|
|
}),
|
|
)
|
|
for (const k of luaBuiltins) vm.protected.add(k)
|
|
}
|
|
|
|
export function* evaluateLua(source, options) {
|
|
const proto = parse(source)
|
|
const vm = new Sandbox()
|
|
install(vm)
|
|
function global(name, value) {
|
|
if (vm.protected.has(name)) throw failure("Duplicate or reserved Lua global")
|
|
vm.globals.set(name, value)
|
|
vm.protected.add(name)
|
|
}
|
|
// Only 'tools' is an extensible namespace. Every other host function must
|
|
// be explicitly admitted, and indexing it cannot manufacture capabilities.
|
|
for (const path of options.paths) {
|
|
if (path.length !== 1) throw failure("Expected a top-level Lua capability name")
|
|
global(vm.encode(path[0]), new Host([path[0]]))
|
|
}
|
|
const program = new Closure(proto, null)
|
|
program.env = vm.globals
|
|
const run = (function* () {
|
|
const budget = new Boundary(vm)
|
|
for (const name of Object.keys(options.bindings ?? {})) {
|
|
const key = vm.encode(name)
|
|
budget.string(name)
|
|
global(key, yield* vm.import(options.bindings[name], 0, budget))
|
|
}
|
|
const values = yield* runFrames(vm, vm.frames, new Call(program, []))
|
|
return yield* vm.export(values[0], new Set(), 0, new Boundary(vm))
|
|
})()
|
|
try {
|
|
let step = run.next()
|
|
while (!step.done) step = run.next(yield step.value === checkpoint ? tick : step.value)
|
|
return step.value
|
|
} catch (e) {
|
|
if (e instanceof LuaFault && !e.location && vm.node) {
|
|
const loc = vm.node.loc.start
|
|
e.location = { line: loc.line, column: loc.column + 1 }
|
|
}
|
|
throw e
|
|
} finally {
|
|
run.return()
|
|
}
|
|
}
|