delve/pkg/terminal/starbind/starlark.go
Alessandro Arzilli 7d8f47674b
terminal/starbind: add online help for starlark (#3388)
Adds a new starlark builtin 'help' that prints the list of available
builtins when called without arguments and help for the specified
builtin when passed an argument.

The help is autogenerated from godoc comments so it isn't always
exactly accurate for starlark (in particular we sometimes refer to the
In structs), but it's better than nothing.
2023-06-12 14:31:31 -07:00

372 lines
11 KiB
Go

package starbind
import (
"context"
"fmt"
"io"
"io/ioutil"
"runtime"
"sort"
"strings"
"sync"
startime "go.starlark.net/lib/time"
"go.starlark.net/resolve"
"go.starlark.net/starlark"
"github.com/go-delve/delve/service"
"github.com/go-delve/delve/service/api"
)
//go:generate go run ../../../_scripts/gen-starlark-bindings.go go ./starlark_mapping.go
//go:generate go run ../../../_scripts/gen-starlark-bindings.go doc ../../../Documentation/cli/starlark.md
const (
dlvCommandBuiltinName = "dlv_command"
readFileBuiltinName = "read_file"
writeFileBuiltinName = "write_file"
commandPrefix = "command_"
dlvContextName = "dlv_context"
curScopeBuiltinName = "cur_scope"
defaultLoadConfigBuiltinName = "default_load_config"
helpBuiltinName = "help"
)
func init() {
resolve.AllowNestedDef = true
resolve.AllowLambda = true
resolve.AllowFloat = true
resolve.AllowSet = true
resolve.AllowBitwise = true
resolve.AllowRecursion = true
resolve.AllowGlobalReassign = true
}
// Context is the context in which starlark scripts are evaluated.
// It contains methods to call API functions, command line commands, etc.
type Context interface {
Client() service.Client
RegisterCommand(name, helpMsg string, cmdfn func(args string) error)
CallCommand(cmdstr string) error
Scope() api.EvalScope
LoadConfig() api.LoadConfig
}
// Env is the environment used to evaluate starlark scripts.
type Env struct {
env starlark.StringDict
contextMu sync.Mutex
thread *starlark.Thread
cancelfn context.CancelFunc
ctx Context
out EchoWriter
}
// New creates a new starlark binding environment.
func New(ctx Context, out EchoWriter) *Env {
env := &Env{}
env.ctx = ctx
env.out = out
// Make the "time" module available to Starlark scripts.
starlark.Universe["time"] = startime.Module
var doc map[string]string
env.env, doc = env.starlarkPredeclare()
builtindoc := func(name, args, descr string) {
doc[name] = name + args + "\n\n" + name + " " + descr
}
env.env[dlvCommandBuiltinName] = starlark.NewBuiltin(dlvCommandBuiltinName, func(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
if err := isCancelled(thread); err != nil {
return starlark.None, err
}
argstrs := make([]string, len(args))
for i := range args {
a, ok := args[i].(starlark.String)
if !ok {
return nil, fmt.Errorf("argument of dlv_command is not a string")
}
argstrs[i] = string(a)
}
err := env.ctx.CallCommand(strings.Join(argstrs, " "))
if err != nil && strings.Contains(err.Error(), " has exited with status ") {
return env.interfaceToStarlarkValue(err), nil
}
return starlark.None, decorateError(thread, err)
})
builtindoc(dlvCommandBuiltinName, "(Command)", "interrupts, continues and steps through the program.")
env.env[readFileBuiltinName] = starlark.NewBuiltin(readFileBuiltinName, func(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
if len(args) != 1 {
return nil, decorateError(thread, fmt.Errorf("wrong number of arguments"))
}
path, ok := args[0].(starlark.String)
if !ok {
return nil, decorateError(thread, fmt.Errorf("argument of read_file was not a string"))
}
buf, err := ioutil.ReadFile(string(path))
if err != nil {
return nil, decorateError(thread, err)
}
return starlark.String(string(buf)), nil
})
builtindoc(readFileBuiltinName, "(Path)", "reads a file.")
env.env[writeFileBuiltinName] = starlark.NewBuiltin(writeFileBuiltinName, func(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
if len(args) != 2 {
return nil, decorateError(thread, fmt.Errorf("wrong number of arguments"))
}
path, ok := args[0].(starlark.String)
if !ok {
return nil, decorateError(thread, fmt.Errorf("first argument of write_file was not a string"))
}
err := ioutil.WriteFile(string(path), []byte(args[1].String()), 0640)
return starlark.None, decorateError(thread, err)
})
builtindoc(writeFileBuiltinName, "(Path, Text)", "writes text to the specified file.")
env.env[curScopeBuiltinName] = starlark.NewBuiltin(curScopeBuiltinName, func(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
return env.interfaceToStarlarkValue(env.ctx.Scope()), nil
})
builtindoc(curScopeBuiltinName, "()", "returns the current scope.")
env.env[defaultLoadConfigBuiltinName] = starlark.NewBuiltin(defaultLoadConfigBuiltinName, func(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
return env.interfaceToStarlarkValue(env.ctx.LoadConfig()), nil
})
builtindoc(defaultLoadConfigBuiltinName, "()", "returns the default load configuration.")
env.env[helpBuiltinName] = starlark.NewBuiltin(helpBuiltinName, func(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
switch len(args) {
case 0:
fmt.Fprintln(env.out, "Available builtins:")
bins := make([]string, 0, len(env.env))
for name, value := range env.env {
switch value.(type) {
case *starlark.Builtin:
bins = append(bins, name)
}
}
sort.Strings(bins)
for _, bin := range bins {
fmt.Fprintf(env.out, "\t%s\n", bin)
}
case 1:
switch x := args[0].(type) {
case *starlark.Builtin:
if doc[x.Name()] != "" {
fmt.Fprintf(env.out, "%s\n", doc[x.Name()])
} else {
fmt.Fprintf(env.out, "no help for builtin %s\n", x.Name())
}
case *starlark.Function:
fmt.Fprintf(env.out, "user defined function %s\n", x.Name())
if doc := x.Doc(); doc != "" {
fmt.Fprintln(env.out, doc)
}
default:
fmt.Fprintf(env.out, "no help for object of type %T\n", args[0])
}
default:
fmt.Fprintln(env.out, "wrong number of arguments ", len(args))
}
return starlark.None, nil
})
builtindoc(helpBuiltinName, "(Object)", "prints help for Object.")
return env
}
// Redirect redirects starlark output to out.
func (env *Env) Redirect(out EchoWriter) {
env.out = out
if env.thread != nil {
env.thread.Print = env.printFunc()
}
}
func (env *Env) printFunc() func(_ *starlark.Thread, msg string) {
return func(_ *starlark.Thread, msg string) { fmt.Fprintln(env.out, msg) }
}
// Execute executes a script. Path is the name of the file to execute and
// source is the source code to execute.
// Source can be either a []byte, a string or a io.Reader. If source is nil
// Execute will execute the file specified by 'path'.
// After the file is executed if a function named mainFnName exists it will be called, passing args to it.
func (env *Env) Execute(path string, source interface{}, mainFnName string, args []interface{}) (_ starlark.Value, _err error) {
defer func() {
err := recover()
if err == nil {
return
}
_err = fmt.Errorf("panic executing starlark script: %v", err)
fmt.Fprintf(env.out, "panic executing starlark script: %v\n", err)
for i := 0; ; i++ {
pc, file, line, ok := runtime.Caller(i)
if !ok {
break
}
fname := "<unknown>"
fn := runtime.FuncForPC(pc)
if fn != nil {
fname = fn.Name()
}
fmt.Fprintf(env.out, "%s\n\tin %s:%d\n", fname, file, line)
}
}()
thread := env.newThread()
globals, err := starlark.ExecFile(thread, path, source, env.env)
if err != nil {
return starlark.None, err
}
err = env.exportGlobals(globals)
if err != nil {
return starlark.None, err
}
return env.callMain(thread, globals, mainFnName, args)
}
// exportGlobals saves globals with a name starting with a capital letter
// into the environment and creates commands from globals with a name
// starting with "command_"
func (env *Env) exportGlobals(globals starlark.StringDict) error {
for name, val := range globals {
switch {
case strings.HasPrefix(name, commandPrefix):
err := env.createCommand(name, val)
if err != nil {
return err
}
case name[0] >= 'A' && name[0] <= 'Z':
env.env[name] = val
}
}
return nil
}
// Cancel cancels the execution of a currently running script or function.
func (env *Env) Cancel() {
if env == nil {
return
}
env.contextMu.Lock()
if env.cancelfn != nil {
env.cancelfn()
env.cancelfn = nil
}
if env.thread != nil {
env.thread.Cancel("user interrupt")
}
env.contextMu.Unlock()
}
func (env *Env) newThread() *starlark.Thread {
thread := &starlark.Thread{
Print: env.printFunc(),
}
env.contextMu.Lock()
var ctx context.Context
ctx, env.cancelfn = context.WithCancel(context.Background())
env.thread = thread
env.contextMu.Unlock()
thread.SetLocal(dlvContextName, ctx)
return thread
}
func (env *Env) createCommand(name string, val starlark.Value) error {
fnval, ok := val.(*starlark.Function)
if !ok {
return nil
}
name = name[len(commandPrefix):]
helpMsg := fnval.Doc()
if helpMsg == "" {
helpMsg = "user defined"
}
if fnval.NumParams() == 1 {
if p0, _ := fnval.Param(0); p0 == "args" {
env.ctx.RegisterCommand(name, helpMsg, func(args string) error {
_, err := starlark.Call(env.newThread(), fnval, starlark.Tuple{starlark.String(args)}, nil)
return err
})
return nil
}
}
env.ctx.RegisterCommand(name, helpMsg, func(args string) error {
thread := env.newThread()
argval, err := starlark.Eval(thread, "<input>", "("+args+")", env.env)
if err != nil {
return err
}
argtuple, ok := argval.(starlark.Tuple)
if !ok {
argtuple = starlark.Tuple{argval}
}
_, err = starlark.Call(thread, fnval, argtuple, nil)
return err
})
return nil
}
// callMain calls the main function in globals, if one was defined.
func (env *Env) callMain(thread *starlark.Thread, globals starlark.StringDict, mainFnName string, args []interface{}) (starlark.Value, error) {
if mainFnName == "" {
return starlark.None, nil
}
mainval := globals[mainFnName]
if mainval == nil {
return starlark.None, nil
}
mainfn, ok := mainval.(*starlark.Function)
if !ok {
return starlark.None, fmt.Errorf("%s is not a function", mainFnName)
}
if mainfn.NumParams() != len(args) {
return starlark.None, fmt.Errorf("wrong number of arguments for %s", mainFnName)
}
argtuple := make(starlark.Tuple, len(args))
for i := range args {
argtuple[i] = env.interfaceToStarlarkValue(args[i])
}
return starlark.Call(thread, mainfn, argtuple, nil)
}
func isCancelled(thread *starlark.Thread) error {
if ctx, ok := thread.Local(dlvContextName).(context.Context); ok {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
}
return nil
}
func decorateError(thread *starlark.Thread, err error) error {
if err == nil {
return nil
}
pos := thread.CallFrame(1).Pos
if pos.Col > 0 {
return fmt.Errorf("%s:%d:%d: %v", pos.Filename(), pos.Line, pos.Col, err)
}
return fmt.Errorf("%s:%d: %v", pos.Filename(), pos.Line, err)
}
type EchoWriter interface {
io.Writer
Echo(string)
Flush()
}