519c1f2df4
Rewrites internal/controllogic/expr.go to evaluate to Value (scalar float64 or []Value array): adds array literals [a,b], postfix indexing arr[i], and array functions (len, sum, mean, push, pop, slice, concat, sort, …). Resolver type changes from func(...) float64 to func(...) Value; EvalValue added; EvalExpr/EvalBool retain scalar float64/bool returns. Three temporary shims added in engine.go, lua.go, and expr_test.go pending Tasks 4 and 6. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
118 lines
2.9 KiB
Go
118 lines
2.9 KiB
Go
package controllogic
|
|
|
|
import (
|
|
"sync"
|
|
|
|
lua "github.com/yuin/gopher-lua"
|
|
)
|
|
|
|
// luaRuntime holds a sandboxed Lua VM for one action.lua node. The VM is created
|
|
// lazily and reused; access is serialised so concurrent flow activations of the
|
|
// same node don't share VM state unsafely. Host functions exposed to scripts:
|
|
//
|
|
// get("ds:name") → number read a signal / sys / local value (NaN if absent)
|
|
// set("ds:name", v) write a value (bare name → graph-local var)
|
|
// log(msg) append to the server log
|
|
//
|
|
// The os, io, package, debug and code-loading globals are removed, mirroring the
|
|
// dsp LuaNode sandbox.
|
|
type luaRuntime struct {
|
|
script string
|
|
|
|
mu sync.Mutex
|
|
L *lua.LState
|
|
|
|
// Bound to the current activation while run() holds mu.
|
|
curResolve Resolver
|
|
curSet func(target string, val float64)
|
|
curLog func(msg string)
|
|
}
|
|
|
|
func newLuaRuntime(script string) *luaRuntime {
|
|
return &luaRuntime{script: script}
|
|
}
|
|
|
|
func (lr *luaRuntime) ensure() error {
|
|
if lr.L != nil {
|
|
return nil
|
|
}
|
|
L := lua.NewState(lua.Options{SkipOpenLibs: true})
|
|
for _, pair := range []struct {
|
|
name string
|
|
fn lua.LGFunction
|
|
}{
|
|
{lua.LoadLibName, lua.OpenPackage},
|
|
{lua.BaseLibName, lua.OpenBase},
|
|
{lua.MathLibName, lua.OpenMath},
|
|
{lua.StringLibName, lua.OpenString},
|
|
{lua.TabLibName, lua.OpenTable},
|
|
} {
|
|
if err := L.CallByParam(lua.P{Fn: L.NewFunction(pair.fn), NRet: 0, Protect: true}, lua.LString(pair.name)); err != nil {
|
|
L.Close()
|
|
return err
|
|
}
|
|
}
|
|
L.SetGlobal("load", lua.LNil)
|
|
L.SetGlobal("loadfile", lua.LNil)
|
|
L.SetGlobal("dofile", lua.LNil)
|
|
L.SetGlobal("require", lua.LNil)
|
|
|
|
L.SetGlobal("get", L.NewFunction(func(s *lua.LState) int {
|
|
target := s.CheckString(1)
|
|
ds, name, ok := parseRef(target)
|
|
var v float64
|
|
if ok && lr.curResolve != nil { // shim: Task 6 finalizes lua Value handling
|
|
if f, fok := lr.curResolve(ds, name).(float64); fok {
|
|
v = f
|
|
}
|
|
}
|
|
s.Push(lua.LNumber(v))
|
|
return 1
|
|
}))
|
|
L.SetGlobal("set", L.NewFunction(func(s *lua.LState) int {
|
|
target := s.CheckString(1)
|
|
val := float64(s.CheckNumber(2))
|
|
if lr.curSet != nil {
|
|
lr.curSet(target, val)
|
|
}
|
|
return 0
|
|
}))
|
|
L.SetGlobal("log", L.NewFunction(func(s *lua.LState) int {
|
|
if lr.curLog != nil {
|
|
lr.curLog(s.CheckString(1))
|
|
}
|
|
return 0
|
|
}))
|
|
|
|
lr.L = L
|
|
return nil
|
|
}
|
|
|
|
// run executes the script once with the given host hooks bound.
|
|
func (lr *luaRuntime) run(resolve Resolver, set func(string, float64), logf func(string)) {
|
|
lr.mu.Lock()
|
|
defer lr.mu.Unlock()
|
|
|
|
if err := lr.ensure(); err != nil {
|
|
logf("lua init error: " + err.Error())
|
|
return
|
|
}
|
|
|
|
lr.curResolve = resolve
|
|
lr.curSet = set
|
|
lr.curLog = logf
|
|
defer func() {
|
|
lr.curResolve = nil
|
|
lr.curSet = nil
|
|
lr.curLog = nil
|
|
if r := recover(); r != nil {
|
|
logf("lua panic")
|
|
}
|
|
}()
|
|
|
|
lr.L.SetTop(0)
|
|
if err := lr.L.DoString(lr.script); err != nil {
|
|
logf("lua error: " + err.Error())
|
|
}
|
|
}
|