Skip to content

Commit

Permalink
starlark: add debug API for Locals and FreeVars (#539)
Browse files Browse the repository at this point in the history
This change adds the following new API to allow debugging tools
and built-in functions access to the internals of Function values
and call frames:

package starlark

type Binding struct {
       Name string
       Pos  syntax.Position
}

func (fr *frame) NumLocals() int
func (fr *frame) Local(i int) (Binding, Value)

type DebugFrame interface {
    ...
    NumLocals() int
    Local(i int) (Binding, Value)
}

func (fn *Function) NumFreeVars() int
func (fn *Function) FreeVar(i int) (Binding, Value)

This is strictly a breaking change, but the changed functions
(the Local methods) were previously documented as experimental.
The fix is straightforward.

Also, a test of DebugFrame to write an 'env' function in
a similar vein to Python's 'dir' function.

Fixes #538
  • Loading branch information
adonovan committed Apr 11, 2024
1 parent 169c986 commit 9b43f0a
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 21 deletions.
6 changes: 3 additions & 3 deletions internal/compile/compile.go
Expand Up @@ -335,7 +335,7 @@ type Funcode struct {
pclinetab []uint16 // mapping from pc to linenum
Locals []Binding // locals, parameters first
Cells []int // indices of Locals that require cells
Freevars []Binding // for tracing
FreeVars []Binding // for tracing
MaxStack int
NumParams int
NumKwonlyParams int
Expand Down Expand Up @@ -520,7 +520,7 @@ func (pcomp *pcomp) function(name string, pos syntax.Position, stmts []syntax.St
Name: name,
Doc: docStringFromBody(stmts),
Locals: bindings(locals),
Freevars: bindings(freevars),
FreeVars: bindings(freevars),
},
}

Expand Down Expand Up @@ -887,7 +887,7 @@ func PrintOp(fn *Funcode, pc uint32, op Opcode, arg uint32) {
case ATTR, SETFIELD, PREDECLARED, UNIVERSAL:
comment = fn.Prog.Names[arg]
case FREE:
comment = fn.Freevars[arg].Name
comment = fn.FreeVars[arg].Name
case CALL, CALL_VAR, CALL_KW, CALL_VAR_KW:
comment = fmt.Sprintf("%d pos, %d named", arg>>8, arg&0xff)
default:
Expand Down
4 changes: 2 additions & 2 deletions internal/compile/serial.go
Expand Up @@ -195,7 +195,7 @@ func (e *encoder) function(fn *Funcode) {
for _, index := range fn.Cells {
e.int(index)
}
e.bindings(fn.Freevars)
e.bindings(fn.FreeVars)
e.int(fn.MaxStack)
e.int(fn.NumParams)
e.int(fn.NumKwonlyParams)
Expand Down Expand Up @@ -389,7 +389,7 @@ func (d *decoder) function() *Funcode {
pclinetab: pclinetab,
Locals: locals,
Cells: cells,
Freevars: freevars,
FreeVars: freevars,
MaxStack: maxStack,
NumParams: numParams,
NumKwonlyParams: numKwonlyParams,
Expand Down
44 changes: 31 additions & 13 deletions starlark/debug.go
@@ -1,41 +1,59 @@
package starlark

import "go.starlark.net/syntax"
import (
"go.starlark.net/syntax"
)

// This file defines an experimental API for the debugging tools.
// Some of these declarations expose details of internal packages.
// (The debugger makes liberal use of exported fields of unexported types.)
// Breaking changes may occur without notice.

// Local returns the value of the i'th local variable.
// It may be nil if not yet assigned.
// A Binding is the name and position of a binding identifier.
type Binding struct {
Name string
Pos syntax.Position
}

// NumLocals returns the number of local variables of this frame.
// It is zero unless fr.Callable() is a *Function.
func (fr *frame) NumLocals() int { return len(fr.locals) }

// Local returns the binding (name and binding position) and value of
// the i'th local variable of the frame's function.
// Beware: the value may be nil if it has not yet been assigned!
//
// Local may be called only for frames whose Callable is a *Function (a
// function defined by Starlark source code), and only while the frame
// is active; it will panic otherwise.
// The index i must be less than [NumLocals].
// Local may be called only while the frame is active.
//
// This function is provided only for debugging tools.
//
// THIS API IS EXPERIMENTAL AND MAY CHANGE WITHOUT NOTICE.
func (fr *frame) Local(i int) Value { return fr.locals[i] }
func (fr *frame) Local(i int) (Binding, Value) {
return Binding(fr.callable.(*Function).funcode.Locals[i]), fr.locals[i]
}

// DebugFrame is the debugger API for a frame of the interpreter's call stack.
//
// Most applications have no need for this API; use CallFrame instead.
//
// It may be tempting to use this interface when implementing built-in
// functions. Beware that reflection over the call stack is easily
// abused, leading to built-in functions whose behavior is mysterious
// and unpredictable.
//
// Clients must not retain a DebugFrame nor call any of its methods once
// the current built-in call has returned or execution has resumed
// after a breakpoint as this may have unpredictable effects, including
// but not limited to retention of object that would otherwise be garbage.
type DebugFrame interface {
Callable() Callable // returns the frame's function
Local(i int) Value // returns the value of the (Starlark) frame's ith local variable
Position() syntax.Position // returns the current position of execution in this frame
Callable() Callable // returns the frame's function
NumLocals() int // returns the number of local variables in this frame
Local(i int) (Binding, Value) // returns the binding and value of the (Starlark) frame's ith local variable
Position() syntax.Position // returns the current position of execution in this frame
}

// DebugFrame returns the debugger interface for
// the specified frame of the interpreter's call stack.
// Frame numbering is as for Thread.CallFrame.
// Frame numbering is as for Thread.CallFrame: 0 <= depth < thread.CallStackDepth().
//
// This function is intended for use in debugging tools.
// Most applications should have no need for it; use CallFrame instead.
Expand Down
55 changes: 54 additions & 1 deletion starlark/eval_test.go
Expand Up @@ -824,7 +824,8 @@ func TestFrameLocals(t *testing.T) {
buf.WriteString(", ")
}
name, _ := fn.Param(i)
fmt.Fprintf(buf, "%s=%s", name, fr.Local(i))
_, v := fr.Local(i)
fmt.Fprintf(buf, "%s=%s", name, v)
}
} else {
buf.WriteString("...") // a built-in function
Expand Down Expand Up @@ -1056,3 +1057,55 @@ main()
}()
}
}

func TestDebugFrame(t *testing.T) {
predeclared := starlark.StringDict{
"env": starlark.NewBuiltin("env", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
if thread.CallStackDepth() < 2 {
return nil, fmt.Errorf("env must not be called directly")
}
fr := thread.DebugFrame(1) // parent
fn, ok := fr.Callable().(*starlark.Function)
if !ok {
return nil, fmt.Errorf("env must be called from a Starlark function")
}
dict := starlark.NewDict(0)
for i := 0; i < fr.NumLocals(); i++ {
bind, val := fr.Local(i)
if val == nil {
continue
}
dict.SetKey(starlark.String(bind.Name), val) // ignore error
}
for i := 0; i < fn.NumFreeVars(); i++ {
bind, val := fn.FreeVar(i)
dict.SetKey(starlark.String(bind.Name), val) // ignore error
}
dict.Freeze()
return dict, nil
}),
}
const src = `
e = [None]
def f(p):
outer = 3
def g(q):
inner = outer + 1
e[0] = env() # {"q": 2, "inner": 4, "outer": 3}
inner2 = None # not defined at call to env()
g(2)
f(1)
`
thread := new(starlark.Thread)
m, err := starlark.ExecFile(thread, "env.star", src, predeclared)
if err != nil {
t.Fatalf("ExecFile returned error %q, expected panic", err)
}
got := m["e"].(*starlark.List).Index(0).String()
want := `{"q": 2, "inner": 4, "outer": 3}`
if got != want {
t.Errorf("env() returned %s, want %s", got, want)
}
}
4 changes: 2 additions & 2 deletions starlark/interp.go
Expand Up @@ -541,7 +541,7 @@ loop:
case compile.MAKEFUNC:
funcode := f.Prog.Functions[arg]
tuple := stack[sp-1].(Tuple)
n := len(tuple) - len(funcode.Freevars)
n := len(tuple) - len(funcode.FreeVars)
defaults := tuple[:n:n]
freevars := tuple[n:]
stack[sp-1] = &Function{
Expand Down Expand Up @@ -622,7 +622,7 @@ loop:
case compile.FREECELL:
v := fn.freevars[arg].(*cell).v
if v == nil {
err = fmt.Errorf("local variable %s referenced before assignment", f.Freevars[arg].Name)
err = fmt.Errorf("local variable %s referenced before assignment", f.FreeVars[arg].Name)
break loop
}
stack[sp] = v
Expand Down
9 changes: 9 additions & 0 deletions starlark/value.go
Expand Up @@ -775,6 +775,15 @@ func (fn *Function) ParamDefault(i int) Value {
func (fn *Function) HasVarargs() bool { return fn.funcode.HasVarargs }
func (fn *Function) HasKwargs() bool { return fn.funcode.HasKwargs }

// NumFreeVars returns the number of free variables of this function.
func (fn *Function) NumFreeVars() int { return len(fn.funcode.FreeVars) }

// FreeVar returns the binding (name and binding position) and value
// of the i'th free variable of function fn.
func (fn *Function) FreeVar(i int) (Binding, Value) {
return Binding(fn.funcode.FreeVars[i]), fn.freevars[i].(*cell).v
}

// A Builtin is a function implemented in Go.
type Builtin struct {
name string
Expand Down

0 comments on commit 9b43f0a

Please sign in to comment.