cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

cat/module-path
012 / internal / macro / run.go
run.go 4.8K
1package macro23import (4 "errors"5 "fmt"6 "strings"7 "sync/atomic"89 "go.starlark.net/resolve"10 "go.starlark.net/starlark"11 "go.starlark.net/syntax"12)1314// DefaultMaxSteps bounds a run: enough for loops over tens of thousands15// of cells, few enough that a script that never ends stops within a16// second or so.17const DefaultMaxSteps = 10_000_0001819// Env is where a run happens.20type Env struct {21 Host Host22 // Do runs fn where Host may be used and returns once it has; nil23 // runs it directly. The UI hands calls to its own goroutine here, so24 // the script can run on another.25 Do func(fn func())26 // Print shows a line printed by the script; called inside Do.27 Print func(msg string)28 // MaxSteps bounds the run; 0 means DefaultMaxSteps.29 MaxSteps uint6430}3132// Stats describe a finished run.33type Stats struct {34 Steps uint64 // Starlark computation steps35 Calls int // calls to the spreadsheet36}3738// ErrCancelled is the reason a cancelled run gives.39var ErrCancelled = errors.New("stopped")4041// Error is a script's failure at a position in it, e.g.42// "Totals:3:5: set: not a cell or range: ZZ".43type Error struct {44 Pos string // name:line:column, or "" when not at a place in the script45 Msg string46 Cancel bool // the run was cancelled47 Limit bool // the run hit its step limit48}4950func (e *Error) Error() string {51 if e.Pos == "" {52 return e.Msg53 }54 return e.Pos + ": " + e.Msg55}5657// options are the language options: top-level if, for and while, so a58// recording can be a plain list of statements and scripts don't need a59// main function, and reassigning globals, as in Python.60var options = &syntax.FileOptions{Set: true, While: true, TopLevelControl: true, GlobalReassign: true}6162// Run is one run of a script.63type Run struct {64 name, src string65 env Env66 thread *starlark.Thread67 calls int68 cancelled atomic.Bool69}7071// New prepares a run of src, named name in error positions.72func New(name, src string, env Env) *Run {73 if env.Do == nil {74 env.Do = func(fn func()) { fn() }75 }76 if env.Print == nil {77 env.Print = func(string) {}78 }79 r := &Run{name: name, src: src, env: env}80 r.thread = &starlark.Thread{Name: name}81 r.thread.Print = func(_ *starlark.Thread, msg string) { r.env.Do(func() { r.env.Print(msg) }) }82 // No Load: scripts can't load modules, so nothing but the builtins83 // below is reachable.84 max := env.MaxSteps85 if max == 0 {86 max = DefaultMaxSteps87 }88 r.thread.SetMaxExecutionSteps(max)89 return r90}9192// Cancel stops the run as soon as the script next takes a step. It is93// safe to call from any goroutine.94func (r *Run) Cancel() {95 r.cancelled.Store(true)96 r.thread.Cancel(ErrCancelled.Error())97}9899// Exec runs the script to its end on the calling goroutine. Host calls100// go through Env.Do.101func (r *Run) Exec() (Stats, error) {102 _, err := starlark.ExecFileOptions(options, r.thread, r.name, r.src, r.builtins())103 st := Stats{Steps: r.thread.ExecutionSteps(), Calls: r.calls}104 return st, r.explain(err)105}106107// Check parses and resolves src without running it, reporting the first108// problem with its position.109func Check(name, src string) error {110 _, _, err := starlark.SourceProgramOptions(options, name, src, func(n string) bool { return predeclared[n] })111 return (&Run{name: name}).explain(err)112}113114// predeclared are the names scripts see besides Starlark's own.115var predeclared = func() map[string]bool {116 out := map[string]bool{}117 for n := range (&Run{}).builtins() {118 out[n] = true119 }120 return out121}()122123// explain turns an error from Starlark into an Error with the script124// position it happened at.125func (r *Run) explain(err error) error {126 if err == nil {127 return nil128 }129 var se syntax.Error130 var re resolve.ErrorList131 var ee *starlark.EvalError132 switch {133 case errors.As(err, &se):134 return &Error{Pos: position(se.Pos), Msg: se.Msg}135 case errors.As(err, &re):136 return &Error{Pos: position(re[0].Pos), Msg: re[0].Msg}137 case errors.As(err, &ee):138 return r.evalError(ee)139 }140 return &Error{Msg: err.Error()}141}142143// evalError places an evaluation error at the innermost line of the144// script, naming the function that failed when it's one of ours.145func (r *Run) evalError(ee *starlark.EvalError) *Error {146 e := &Error{Msg: ee.Msg}147 stack := ee.CallStack148 if n := len(stack); n > 0 && stack[n-1].Pos.Filename() == "<builtin>" {149 if name := stack[n-1].Name; !strings.HasPrefix(e.Msg, name+":") {150 e.Msg = name + ": " + e.Msg151 }152 stack = stack[:n-1]153 }154 if n := len(stack); n > 0 {155 e.Pos = position(stack[n-1].Pos)156 }157 switch {158 case r.cancelled.Load() && strings.Contains(ee.Msg, ErrCancelled.Error()):159 e.Msg, e.Cancel = "stopped with Esc", true160 case strings.Contains(ee.Msg, "too many steps"):161 e.Msg = fmt.Sprintf("stopped after %d steps: does it loop forever?", r.thread.ExecutionSteps())162 e.Limit = true163 }164 return e165}166167func position(p syntax.Position) string {168 return fmt.Sprintf("%s:%d:%d", p.Filename(), p.Line, p.Col)169}