mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-08 18:51:24 +03:00
281 lines
10 KiB
JavaScript
281 lines
10 KiB
JavaScript
import {
|
|
LuaClosure,
|
|
LuaError,
|
|
LuaTable,
|
|
arith,
|
|
compare,
|
|
concat,
|
|
index,
|
|
len,
|
|
luaToNumber,
|
|
newindex,
|
|
truthy,
|
|
} from "v8lua/src/runtime.js"
|
|
|
|
// One generator per active Lua function. Expressions delegate within this frame;
|
|
// calls yield a request to the VM rather than delegating to another Lua frame.
|
|
export class Call {
|
|
constructor(fn, args) {
|
|
this.fn = fn
|
|
this.args = args
|
|
}
|
|
}
|
|
|
|
const stop = Symbol("break")
|
|
const arithmetic = { "+": "add", "-": "sub", "*": "mul", "/": "div", "%": "mod", "^": "pow" }
|
|
|
|
function isCall(node) {
|
|
return node.type === "CallExpression" || node.type === "StringCallExpression" || node.type === "TableCallExpression"
|
|
}
|
|
|
|
// Each declaration extends the lexical chain, never mutating the set of names
|
|
// seen by earlier closures. Assignment changes the value in the existing cell.
|
|
function bind(parent, name, value) {
|
|
return { parent, name, value }
|
|
}
|
|
|
|
function lookup(scope, name) {
|
|
for (let cell = scope; cell; cell = cell.parent) if (cell.name === name) return cell
|
|
}
|
|
|
|
export class Frame {
|
|
constructor(vm, closure, args) {
|
|
this.vm = vm
|
|
this.closure = closure
|
|
this.line = closure.proto.loc.start.line
|
|
let scope = closure.scope
|
|
const proto = closure.proto
|
|
let i = 0
|
|
if (proto.identifier?.indexer === ":") scope = bind(scope, "self", args[i++])
|
|
for (const param of proto.parameters ?? []) {
|
|
if (param.type === "VarargLiteral") break
|
|
scope = bind(scope, param.name, args[i++])
|
|
}
|
|
this.varargs = args.slice(i)
|
|
if (proto.legacyArg) {
|
|
const arg = new LuaTable()
|
|
this.varargs.forEach((v, i) => arg.set(i + 1, v))
|
|
arg.set("n", this.varargs.length)
|
|
scope = bind(scope, "arg", arg)
|
|
} else if (proto.legacyArg === false) scope = bind(scope, "arg", undefined)
|
|
this.code = this.block(proto.body, scope)
|
|
}
|
|
|
|
close(proto, scope) {
|
|
const fn = new LuaClosure(proto, scope, proto.identifier?.name, this.vm)
|
|
fn.chunkname = this.closure.chunkname
|
|
fn.env = this.closure.env
|
|
return fn
|
|
}
|
|
|
|
*args(nodes, scope) {
|
|
const result = []
|
|
for (let i = 0; i < nodes.length; i++) {
|
|
const node = nodes[i]
|
|
if (i === nodes.length - 1 && !node.inParens) {
|
|
if (node.type === "VarargLiteral") return result.concat(this.varargs)
|
|
if (isCall(node)) return result.concat(yield* this.call(node, scope))
|
|
}
|
|
result.push(yield* this.expr(node, scope))
|
|
}
|
|
return result
|
|
}
|
|
|
|
*call(node, scope) {
|
|
const request = yield* this.prepareCall(node, scope)
|
|
return yield request
|
|
}
|
|
|
|
*prepareCall(node, scope) {
|
|
const method = node.base.indexer === ":"
|
|
const self = method ? yield* this.expr(node.base.base, scope) : undefined
|
|
const fn = method ? this.vm.atomic(index(self, node.base.identifier.name)) : yield* this.expr(node.base, scope)
|
|
const args =
|
|
node.type === "CallExpression"
|
|
? yield* this.args(node.arguments, scope)
|
|
: [yield* this.expr(node.argument ?? node.arguments, scope)]
|
|
this.line = node.loc.start.line
|
|
return new Call(fn, method ? [self, ...args] : args)
|
|
}
|
|
|
|
*ref(node, scope) {
|
|
if (node.type === "Identifier") {
|
|
const cell = lookup(scope, node.name)
|
|
return cell ? { cell } : { obj: this.closure.env, key: node.name }
|
|
}
|
|
const obj = yield* this.expr(node.base, scope)
|
|
const key = node.type === "MemberExpression" ? node.identifier.name : yield* this.expr(node.index, scope)
|
|
return { obj, key }
|
|
}
|
|
|
|
store(ref, value) {
|
|
if (ref.cell) ref.cell.value = value
|
|
else this.vm.atomic(newindex(ref.obj, ref.key, value))
|
|
}
|
|
|
|
*expr(node, scope) {
|
|
switch (node.type) {
|
|
case "NilLiteral":
|
|
return undefined
|
|
case "StringLiteral":
|
|
case "NumericLiteral":
|
|
case "BooleanLiteral":
|
|
return node.value
|
|
case "VarargLiteral":
|
|
return this.varargs[0]
|
|
case "Identifier":
|
|
case "IndexExpression":
|
|
case "MemberExpression": {
|
|
const ref = yield* this.ref(node, scope)
|
|
this.line = node.loc.start.line
|
|
return ref.cell ? ref.cell.value : this.vm.atomic(index(ref.obj, ref.key))
|
|
}
|
|
case "FunctionDeclaration":
|
|
return this.close(node, scope)
|
|
case "CallExpression":
|
|
case "TableCallExpression":
|
|
case "StringCallExpression":
|
|
return (yield* this.call(node, scope))[0]
|
|
case "TableConstructorExpression": {
|
|
const table = new LuaTable()
|
|
let at = 1
|
|
for (let i = 0; i < node.fields.length; i++) {
|
|
const field = node.fields[i]
|
|
if (field.type === "TableValue") {
|
|
if (i === node.fields.length - 1) {
|
|
for (const v of yield* this.args([field.value], scope)) table.set(at++, v)
|
|
} else table.set(at++, yield* this.expr(field.value, scope))
|
|
} else {
|
|
const key = field.type === "TableKeyString" ? field.key.name : yield* this.expr(field.key, scope)
|
|
table.set(key, yield* this.expr(field.value, scope))
|
|
}
|
|
}
|
|
return table
|
|
}
|
|
case "UnaryExpression": {
|
|
const value = yield* this.expr(node.argument, scope)
|
|
if (node.operator === "not") return !truthy(value)
|
|
return this.vm.atomic(node.operator === "#" ? len(value) : arith("unm", value, value))
|
|
}
|
|
case "LogicalExpression": {
|
|
const left = yield* this.expr(node.left, scope)
|
|
return truthy(left) === (node.operator === "and") ? yield* this.expr(node.right, scope) : left
|
|
}
|
|
case "BinaryExpression": {
|
|
const a = yield* this.expr(node.left, scope)
|
|
const b = yield* this.expr(node.right, scope)
|
|
this.line = node.loc.start.line
|
|
switch (node.operator) {
|
|
case "..":
|
|
return this.vm.atomic(concat(a, b))
|
|
case "==":
|
|
return this.vm.atomic(compare("eq", a, b))
|
|
case "~=":
|
|
return !this.vm.atomic(compare("eq", a, b))
|
|
case "<":
|
|
return this.vm.atomic(compare("lt", a, b))
|
|
case ">":
|
|
return this.vm.atomic(compare("lt", b, a))
|
|
case "<=":
|
|
return this.vm.atomic(compare("le", a, b))
|
|
case ">=":
|
|
return this.vm.atomic(compare("le", b, a))
|
|
default:
|
|
return this.vm.atomic(arith(arithmetic[node.operator], a, b))
|
|
}
|
|
}
|
|
default:
|
|
throw new Error(`unknown Lua expression: ${node.type}`)
|
|
}
|
|
}
|
|
|
|
*block(nodes, scope, until) {
|
|
for (const node of nodes) {
|
|
this.line = node.loc.start.line
|
|
switch (node.type) {
|
|
case "CallStatement":
|
|
yield* this.call(node.expression, scope)
|
|
break
|
|
case "LocalStatement": {
|
|
const values = yield* this.args(node.init, scope)
|
|
for (const [i, variable] of node.variables.entries()) scope = bind(scope, variable.name, values[i])
|
|
break
|
|
}
|
|
case "AssignmentStatement": {
|
|
const refs = []
|
|
for (const variable of node.variables) refs.push(yield* this.ref(variable, scope))
|
|
const values = yield* this.args(node.init, scope)
|
|
for (let i = refs.length - 1; i >= 0; i--) this.store(refs[i], values[i])
|
|
break
|
|
}
|
|
case "FunctionDeclaration": {
|
|
if (node.isLocal) scope = bind(scope, node.identifier.name, undefined)
|
|
this.store(yield* this.ref(node.identifier, scope), this.close(node, scope))
|
|
break
|
|
}
|
|
case "ReturnStatement": {
|
|
const args = node.arguments
|
|
if (args.length === 1 && isCall(args[0]) && !args[0].inParens) return yield* this.prepareCall(args[0], scope)
|
|
return yield* this.args(args, scope)
|
|
}
|
|
case "BreakStatement":
|
|
return stop
|
|
case "DoStatement": {
|
|
const result = yield* this.block(node.body, scope)
|
|
if (result !== undefined) return result
|
|
break
|
|
}
|
|
case "IfStatement": {
|
|
for (const clause of node.clauses) {
|
|
if (clause.condition && !truthy(yield* this.expr(clause.condition, scope))) continue
|
|
const result = yield* this.block(clause.body, scope)
|
|
if (result !== undefined) return result
|
|
break
|
|
}
|
|
break
|
|
}
|
|
case "WhileStatement":
|
|
case "RepeatStatement": {
|
|
const repeat = node.type === "RepeatStatement"
|
|
while (repeat || truthy(yield* this.expr(node.condition, scope))) {
|
|
const result = yield* this.block(node.body, scope, repeat && node.condition)
|
|
if (result === stop) break
|
|
if (result !== undefined) return result
|
|
}
|
|
break
|
|
}
|
|
case "ForNumericStatement": {
|
|
const start = luaToNumber(yield* this.expr(node.start, scope))
|
|
const limit = luaToNumber(yield* this.expr(node.end, scope))
|
|
const step = node.step ? luaToNumber(yield* this.expr(node.step, scope)) : 1
|
|
if ([start, limit, step].includes(undefined)) throw new LuaError("'for' values must be numbers")
|
|
for (let i = start; step > 0 ? i <= limit : i >= limit; i += step) {
|
|
const result = yield* this.block(node.body, bind(scope, node.variable.name, i))
|
|
if (result === stop) break
|
|
if (result !== undefined) return result
|
|
}
|
|
break
|
|
}
|
|
case "ForGenericStatement": {
|
|
const [iterator, state, first] = yield* this.args(node.iterators, scope)
|
|
let control = first
|
|
for (;;) {
|
|
const values = this.vm.atomic(this.vm.invoke(iterator, [state, control]))
|
|
control = values[0]
|
|
if (control === undefined) break
|
|
const locals = node.variables.reduce((s, v, i) => bind(s, v.name, values[i]), scope)
|
|
const result = yield* this.block(node.body, locals)
|
|
if (result === stop) break
|
|
if (result !== undefined) return result
|
|
}
|
|
break
|
|
}
|
|
default:
|
|
throw new Error(`unknown Lua statement: ${node.type}`)
|
|
}
|
|
}
|
|
// The repeat condition is inside the body's lexical scope.
|
|
if (until && truthy(yield* this.expr(until, scope))) return stop
|
|
}
|
|
}
|