Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

need API to enumerate names/bindings of a Function's local variables #538

Closed
hungtcs opened this issue Apr 9, 2024 · 7 comments · Fixed by #539
Closed

need API to enumerate names/bindings of a Function's local variables #538

hungtcs opened this issue Apr 9, 2024 · 7 comments · Fixed by #539

Comments

@hungtcs
Copy link

hungtcs commented Apr 9, 2024

Hi all,

I am trying to access the context variable that calls the function in a custom built-in function, can I do it?

var code = `
name = "hungtcs"
hello();
`

func main() {
  var predeclared = starlark.StringDict{
    "hello": starlark.NewBuiltin("hello", func(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
      // Question: How do I access global variables here?
      // fmt.Printf("hello %s\n", name)
      return starlark.None, nil
    }),
  }

  globals, err := starlark.ExecFile(
    &starlark.Thread{},
    "main.star",
    code,
    predeclared,
  )
  if err != nil {
    panic(err)
  }

  fmt.Printf("globals: %v\n", globals)
}

My idea was to implement code similar to the following, but using go to writing the hello method:

def hello():
  print("hello %s" % name)
name = "hungtcs"
hello();
@hungtcs hungtcs changed the title 有办法在 NewBuiltin 函数中访问全局变量么? Question: Is there a way to access global variables in the NewBuiltin function? Apr 9, 2024
@adonovan
Copy link
Collaborator

adonovan commented Apr 9, 2024

Builtins are not associated with any module, and do not have special access to any variables that were not passed to them as arguments; this is as it should be.

It is possible (from Go) to set a thread-local value that affects the behavior of certain built-ins called from that thread. In this way, built-ins can communicate with each other through Go data structures, and communicate with the host application as a whole.

It is also possible to use the debug interface to walk up the call stack to find any enclosing active calls to Starlark functions, and find the their associated modules and global variables (thread.DebugFrame(i).Callable.(*Function).Globals()["name"]). But overuse of this technique leads to confusion, surprises, and built-in functions that are hard to accurately explain.

Why not give the hello function a name parameter?

@hungtcs
Copy link
Author

hungtcs commented Apr 10, 2024

Thank you very much for your answer!
I'm trying to implement a function similar to JS string interpolation like this:

let name = 'hungtcs'
let message = `hello ${ name }`

I'm aiming to implement something similar in starlark

name = 'hungtcs'
message = format("${ name }") # format is a starlark.NewBuiltin

Although starlark already provides functions such as String::format, I wanted to keep it as simple as possible when providing it to the user.

Thanks for the thread.DebugFrame(i).Callable.(*Function).Globals()["name"] solution, which looks like it will solve my needs at the moment, although it may make the code look less elegant.

@hungtcs hungtcs closed this as completed Apr 10, 2024
@hungtcs
Copy link
Author

hungtcs commented Apr 10, 2024

Sorry I just realized that I can't get local variables inside functions this way.
Should I consider it untenable to implement something like a js interpolating function?

name = "hungtcs"

def test():
	foo = "abc"
	print(format("hello {name},, {foo}"))

test()
var predeclared = starlark.StringDict{
	"format": starlark.NewBuiltin("format", func(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
		var globals = thread.DebugFrame(1).Callable().(*starlark.Function).Globals()

		var string = args[0].(starlark.String)
		var format *starlark.Builtin
		if val, err := string.Attr("format"); err != nil {
			return nil, err
		} else {
			format = val.(*starlark.Builtin)
		}

		var formatKwargs = make([]starlark.Tuple, 0)
		for key, val := range globals {
			formatKwargs = append(formatKwargs, starlark.Tuple{starlark.String(key), val})
		}

		var result, err = format.CallInternal(thread, starlark.Tuple{}, formatKwargs)
		if err != nil {
			return nil, err
		}

		return result, nil
	}),
}

@hungtcs hungtcs reopened this Apr 10, 2024
@hungtcs hungtcs changed the title Question: Is there a way to access global variables in the NewBuiltin function? Question: Is there a way to access function call context variables in the NewBuiltin function? Apr 10, 2024
@adonovan
Copy link
Collaborator

It's possible to access the values of local variables through the debug interface: thread.DebugFrame(i).Callable.(*Function).Local(i). The tricky thing is that the information about each local variable (its name and binding location) are available only through internal functions (Function.funcode.Locals). Only the first few local variables corresponding to parameters are currently accessible using public APIs (Function.Param(i)).

@hungtcs
Copy link
Author

hungtcs commented Apr 10, 2024

@adonovan Yes, thanks for your perspective, I added a line of code to starlark/value.go to expose funcode.Locals so that I could get the local bindings of the function.

func (fn *Function) Globals() StringDict { return fn.module.makeGlobalDict() }

func (fn *Function) Locals() []compile.Binding { return fn.funcode.Locals }

But how to get Value via binding ? Then I found this function, and now I can get the corresponding value:

for idx, local := range locals {
    fmt.Println(thread.DebugFrame(1).Local(idx))
}

Here's my current format function, which seems to be working just fine,
Do you have an opinion on this, I'm not sure it's as correct as I think it is (thanks).

var predeclared = starlark.StringDict{
	"format": starlark.NewBuiltin("format", func(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
		var function = thread.DebugFrame(1).Callable().(*starlark.Function)

		var locals = function.Locals()
		var globals = function.Globals()

		var formatArgs = make(map[string]starlark.Value)
		for key, val := range globals {
			formatArgs[key] = val
		}

		for idx, local := range locals {
			formatArgs[local.Name] = thread.DebugFrame(1).Local(idx)
		}

		var string = args[0].(starlark.String)
		var format *starlark.Builtin
		if val, err := string.Attr("format"); err != nil {
			return nil, err
		} else {
			format = val.(*starlark.Builtin)
		}

		var formatKwargs = make([]starlark.Tuple, 0)
		for key, val := range formatArgs {
			formatKwargs = append(formatKwargs, starlark.Tuple{starlark.String(key), val})
		}

		var result, err = format.CallInternal(thread, starlark.Tuple{}, formatKwargs)
		if err != nil {
			return nil, err
		}

		return result, nil
	}),
}

@adonovan
Copy link
Collaborator

adonovan commented Apr 10, 2024

var function = thread.DebugFrame(1).Callable().(*starlark.Function)

You need to handle the case where the Callable is something other than a *Function.

  var string = args[0].(starlark.String)

Again here you need to handle a type error.

  if val, err := string.Attr("format"); err != nil {
  	return nil, err

This can be a panic, since we know that every string has a format method.

  	format = val.(*starlark.Builtin)

Not needed. val is fine.

  	formatKwargs = append(formatKwargs, starlark.Tuple{starlark.String(key), val})

key is already a string.

  var result, err = format.CallInternal(thread, starlark.Tuple{}, formatKwargs)

Never call CallInternal directly. Use Call.

You can pass nil for the Tuple.

You can return Call(...) directly; there's no need for "if err".

The main problem is that Function.Locals isn't public API. We could add it (or something like it), but we'd need to be very careful not to expose anything about the representation of compiled programs.

@hungtcs
Copy link
Author

hungtcs commented Apr 11, 2024

Thanks @adonovan
I'm basically sure it works now.
Please expose these internal interfaces in a suitable way to make the implementation of this function possible.
most people probably won't use this feature, but it's very useful for library developers or people who want to implement certain magic functions.

@adonovan adonovan changed the title Question: Is there a way to access function call context variables in the NewBuiltin function? need API to enumerate names/bindings of a Function's local variables Apr 11, 2024
adonovan added a commit that referenced this issue Apr 11, 2024
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
@adonovan adonovan reopened this Apr 11, 2024
adonovan added a commit that referenced this issue Apr 11, 2024
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants