Files
uopi/internal/controllogic/lua.go
T
Martino Ferrari 519c1f2df4 controllogic: make expr evaluator value-polymorphic (arrays, indexing, array funcs)
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>
2026-06-24 14:53:40 +02:00

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())
}
}