Skip to content

Commit

Permalink
starlark package (#197)
Browse files Browse the repository at this point in the history
  • Loading branch information
samyfodil authored Jun 27, 2024
1 parent 458e4ae commit 918114a
Show file tree
Hide file tree
Showing 17 changed files with 886 additions and 3 deletions.
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,9 @@ require (
github.com/tetratelabs/wazero v1.6.0
github.com/vbauerster/mpb/v8 v8.7.3
github.com/whyrusleeping/multiaddr-filter v0.0.0-20160516205228-e903e4adabd7
go.starlark.net v0.0.0-20240520160348-046347dcd104
google.golang.org/grpc v1.60.1
google.golang.org/protobuf v1.32.0
google.golang.org/protobuf v1.33.0
)

require (
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -892,6 +892,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
go.starlark.net v0.0.0-20240520160348-046347dcd104 h1:3qhteRISupnJvaWshOmeqEUs2y9oc/+/ePPvDh3Eygg=
go.starlark.net v0.0.0-20240520160348-046347dcd104/go.mod h1:YKMCv9b1WrfWmeqdV5MAuEHWsu5iC+fe6kYl2sQjdI8=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
Expand Down Expand Up @@ -1330,8 +1332,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
41 changes: 41 additions & 0 deletions pkg/starlark/context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package starlark

import (
"fmt"

"go.starlark.net/starlark"
)

func (c *ctx) Call(functionName string, args ...starlark.Value) (starlark.Value, error) {
fn, ok := c.globals[functionName].(*starlark.Function)
if !ok {
return nil, fmt.Errorf("function %s not found", functionName)
}
result, err := starlark.Call(c.thread, fn, starlark.Tuple(args), nil)
if err != nil {
return nil, fmt.Errorf("failed to call function %s: %w", functionName, err)
}
return result, nil
}

func (c *ctx) CallWithNative(functionName string, args ...any) (any, error) {
fn, ok := c.globals[functionName].(*starlark.Function)
if !ok {
return nil, fmt.Errorf("function %s not found", functionName)
}

// Convert native Go arguments to Starlark values
starlarkArgs := make([]starlark.Value, len(args))
for i, arg := range args {
starlarkArgs[i] = convertToStarlark(arg)
}

// Call the Starlark function
result, err := starlark.Call(c.thread, fn, starlark.Tuple(starlarkArgs), nil)
if err != nil {
return nil, fmt.Errorf("failed to call function %s: %w", functionName, err)
}

// Convert the result back to a Go value
return convertFromStarlarkBasedOnValue(result), nil
}
102 changes: 102 additions & 0 deletions pkg/starlark/conv.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package starlark

import (
"reflect"

"go.starlark.net/starlark"
)

func convertFromStarlarkBasedOnValue(val starlark.Value) any {
switch v := val.(type) {
case starlark.Int:
intVal, _ := v.Int64()
return intVal
case starlark.Float:
return float64(v)
case starlark.String:
return string(v)
case starlark.Bool:
return bool(v)
case *starlark.List:
result := make([]any, v.Len())
for i := 0; i < v.Len(); i++ {
result[i] = convertFromStarlarkBasedOnValue(v.Index(i))
}
return result
case *starlark.Dict:
result := make(map[any]any)
for _, item := range v.Items() {
key := convertFromStarlarkBasedOnValue(item[0])
value := convertFromStarlarkBasedOnValue(item[1])
result[key] = value
}
return result
case starlark.NoneType:
return nil
default:
return val
}
}

func convertFromStarlark(val starlark.Value, targetType reflect.Type) interface{} {
switch targetType.Kind() {
case reflect.Int:
intVal, _ := starlark.AsInt32(val)
return int(intVal)
case reflect.Float64:
floatVal, _ := starlark.AsFloat(val)
return floatVal
case reflect.String:
return string(val.(starlark.String))
case reflect.Bool:
return bool(val.(starlark.Bool))
case reflect.Slice:
list := val.(*starlark.List)
slice := reflect.MakeSlice(targetType, list.Len(), list.Len())
for i := 0; i < list.Len(); i++ {
slice.Index(i).Set(reflect.ValueOf(convertFromStarlark(list.Index(i), targetType.Elem())))
}
return slice.Interface()
case reflect.Map:
dict := val.(*starlark.Dict)
mapType := reflect.MapOf(targetType.Key(), targetType.Elem())
mapVal := reflect.MakeMap(mapType)
for _, item := range dict.Items() {
key := convertFromStarlark(item[0], targetType.Key())
value := convertFromStarlark(item[1], targetType.Elem())
mapVal.SetMapIndex(reflect.ValueOf(key), reflect.ValueOf(value))
}
return mapVal.Interface()
default:
return val
}
}

func convertToStarlark(val interface{}) starlark.Value {
switch v := val.(type) {
case int:
return starlark.MakeInt(v)
case float64:
return starlark.Float(v)
case string:
return starlark.String(v)
case bool:
return starlark.Bool(v)
case []interface{}:
list := &starlark.List{}
for _, item := range v {
list.Append(convertToStarlark(item))
}
return list
case map[interface{}]interface{}:
dict := starlark.NewDict(len(v))
for key, value := range v {
dict.SetKey(convertToStarlark(key), convertToStarlark(value))
}
return dict
case nil:
return starlark.None
default:
return nil
}
}
18 changes: 18 additions & 0 deletions pkg/starlark/ifaces.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package starlark

import "go.starlark.net/starlark"

type Module interface {
Name() string
}

type VM interface {
Module(mod Module)
Modules(mod ...Module)
File(module string) (Context, error)
}

type Context interface {
Call(functionName string, args ...starlark.Value) (starlark.Value, error)
CallWithNative(functionName string, args ...any) (any, error)
}
37 changes: 37 additions & 0 deletions pkg/starlark/load.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package starlark

import (
"fmt"
"io/fs"

"go.starlark.net/starlark"
"go.starlark.net/starlarkstruct"
"go.starlark.net/syntax"
)

func makeLoadFunc(v *vm) func(thread *starlark.Thread, module string) (starlark.StringDict, error) {
return func(thread *starlark.Thread, module string) (starlark.StringDict, error) {
for name, dict := range v.builtins {
if module == name+".star" {
return dict, nil
}
}

// nothing builtin, check file system
var lastErr error
for _, fsys := range v.filesystems {
script, err := fs.ReadFile(fsys, module)
if err != nil {
lastErr = err
continue // Try next file system
}
predeclared := starlark.StringDict{
"struct": starlark.NewBuiltin("struct", starlarkstruct.Make),
}
opts := syntax.FileOptions{}
return starlark.ExecFileOptions(&opts, thread, module, script, predeclared)
}

return nil, fmt.Errorf("failed to load module %s: %w", module, lastErr) // No file system had the module
}
}
109 changes: 109 additions & 0 deletions pkg/starlark/module.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package starlark

import (
"fmt"
"reflect"
"strings"

"go.starlark.net/starlark"
"go.starlark.net/starlarkstruct"
)

// Builtin registers methods from a given struct that have names starting with 'E_'
// will override an existing module with the same name
// E_ followed by a capital letter export a function with native go types
func (v *vm) Module(mod Module) {
v.builtins[mod.Name()] = starlark.StringDict{
mod.Name(): starlarkstruct.FromStringDict(starlarkstruct.Default, registerMethods(mod)),
}
}

func (v *vm) Modules(mods ...Module) {
for _, mod := range mods {
v.builtins[mod.Name()] = starlark.StringDict{
mod.Name(): starlarkstruct.FromStringDict(starlarkstruct.Default, registerMethods(mod)),
}
}
}

func registerMethods(obj interface{}) starlark.StringDict {
methods := make(starlark.StringDict)
val := reflect.ValueOf(obj)
typ := val.Type()

for i := 0; i < typ.NumMethod(); i++ {
method := val.Method(i)
methodName := typ.Method(i).Name

if strings.HasPrefix(methodName, "E_") && len(methodName) > 2 {
starlarkName := strings.TrimPrefix(methodName, "E_")

if strings.ToUpper(string(methodName[2])) == string(methodName[2]) {
starlarkName = strings.ToLower(string(starlarkName[0])) + starlarkName[1:]
methods[starlarkName] = makeGoFunc(method)
} else if validateMethodSignature(method.Type()) {
methods[starlarkName] = makeStarlarkFunc(method)
}
}
}

return methods
}

func validateMethodSignature(t reflect.Type) bool {
return t.NumIn() == 4 &&
t.In(0).AssignableTo(reflect.TypeOf((*starlark.Thread)(nil))) &&
t.In(1).AssignableTo(reflect.TypeOf((*starlark.Builtin)(nil))) &&
t.In(2).AssignableTo(reflect.TypeOf(starlark.Tuple(nil))) &&
t.In(3).AssignableTo(reflect.TypeOf([]starlark.Tuple(nil))) &&
t.NumOut() == 2 &&
t.Out(0).AssignableTo(reflect.TypeOf((*starlark.Value)(nil)).Elem()) &&
t.Out(1).Implements(reflect.TypeOf((*error)(nil)).Elem())
}

func makeStarlarkFunc(method reflect.Value) *starlark.Builtin {
return starlark.NewBuiltin(method.Type().Name(), func(thread *starlark.Thread, builtin *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
retValues := method.Call([]reflect.Value{
reflect.ValueOf(thread),
reflect.ValueOf(builtin),
reflect.ValueOf(args),
reflect.ValueOf(kwargs),
})
result := retValues[0].Interface()
err := retValues[1].Interface()
if err != nil {
return nil, err.(error)
}
return result.(starlark.Value), nil
})
}

func makeGoFunc(method reflect.Value) *starlark.Builtin {
return starlark.NewBuiltin(method.Type().Name(), func(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, _ []starlark.Tuple) (starlark.Value, error) {
methodType := method.Type()
if args.Len() != methodType.NumIn() {
return nil, fmt.Errorf("expected %d arguments, got %d", methodType.NumIn(), args.Len())
}

in := make([]reflect.Value, args.Len())
for i := 0; i < args.Len(); i++ {
arg := args.Index(i)
in[i] = reflect.ValueOf(convertFromStarlark(arg, methodType.In(i)))
}

retValues := method.Call(in)
starlarkRet := make(starlark.Tuple, 0, len(retValues))

for _, ret := range retValues {
starlarkRet = append(starlarkRet, convertToStarlark(ret.Interface()))
}

if len(starlarkRet) == 0 {
return starlark.None, nil
} else if len(starlarkRet) == 1 {
return starlarkRet[0], nil
}

return starlarkRet, nil
})
}
Loading

0 comments on commit 918114a

Please sign in to comment.