google / starlark-go

Starlark in Go: the Starlark configuration language, implemented in Go
BSD 3-Clause "New" or "Revised" License
2.26k stars 204 forks source link

Getting stringified function call inside a binding #425

Closed gbouv closed 1 year ago

gbouv commented 1 year ago

Hey team!

I've started looking into starlark-go not too long ago, but pardon me if the question is dumb. I would like to know if there's a way to get a string representation of the function call inside a binding?

For example, for this simple starlark code:

s = "Hello World!"
process_custom(s)

With a custom binding for process_custom like:

hello := func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
    // HERE get a string representation of the call the param being resolved ->>`process_custom("Hello World!")`
    return starlark.None, nil
}

In this case I can easily generate it, but when we start having more complex parameters, it becomes more complex fairly quickly. So I want to make sure I'm not missing anything before implementing it.

If that's not something we can do inside the bindings, is there another function I could use?

Thanks in advance for the help!

adonovan commented 1 year ago

You can inspect the thread's call stack, read the enclosing file, and extract the relevant source line (or parse the file and extract the call expression).

But in general, the behavior of a function should be determined by the function itself and its arguments, and possibly by state associated with the thread (though this can easily be abused), but not by who called it.

gbouv commented 1 year ago

@adonovan I don't think that would work because the argument would not be rendered right? If I understand correctly, what you suggest would return something like process_custom(s) in my above example, where I am looking for process_custom("Hello World!") (unless I am misunderstanding what you mean by "read the enclosing file")

adonovan commented 1 year ago

The arguments ("Hello World!") are right there in the args tuple. The call stack can be obtained from the Thread. The name associated with the built-in function value is b.Name. But there is no way to get at the actual call syntax. Consider

f, x, y = print, 1, 2
f(x, y)

In this case, b.Name will be "print" and args will be (1, 2), and the call stack will contain [toplevel, print]. But the call expression is f(x, y), and there's no way to get "f" or "x" or "y" from inside the built-in function without reading the source file.

gbouv commented 1 year ago

Oh ok I see what you mean! I actually do not care too much about f, x or y here, being able to reconstruct print(1, 2) is exactly what I need. Basically I am trying to render a version of any given script with a simple but potentially very long list of low level instructions. So, in your example here, the lowest level I can hope for is print(1, 2).

But I see what you mean. Basically you're saying we have the name of the function being called from b, and all arguments rendered directly in the args tuple, so we would do something as simple as:

renderedArgs := make([]string, len(args))
for i, arg := range args {
    renderedArgs[i] = arg.String() // provided that Value.String() is always correct - TBC
}
renderedInstruction := b.Name() + "(" + strings.Join(renderedArgs, ", ") + ")"

I think that would work for me, thanks a lot for the guidance, I really appreciate the help!

adonovan commented 1 year ago

That's essentially what Tuple.String does, so your code snippet could be simplified to renderedInstruction := b.Name() + args.String().

Glad I could help.