mirror of https://github.com/k3s-io/k3s
1498 lines
38 KiB
Go
1498 lines
38 KiB
Go
// Copyright 2017 The Bazel Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package starlark
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"math"
|
|
"math/big"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
|
|
"go.starlark.net/internal/compile"
|
|
"go.starlark.net/internal/spell"
|
|
"go.starlark.net/resolve"
|
|
"go.starlark.net/syntax"
|
|
)
|
|
|
|
// A Thread contains the state of a Starlark thread,
|
|
// such as its call stack and thread-local storage.
|
|
// The Thread is threaded throughout the evaluator.
|
|
type Thread struct {
|
|
// Name is an optional name that describes the thread, for debugging.
|
|
Name string
|
|
|
|
// stack is the stack of (internal) call frames.
|
|
stack []*frame
|
|
|
|
// Print is the client-supplied implementation of the Starlark
|
|
// 'print' function. If nil, fmt.Fprintln(os.Stderr, msg) is
|
|
// used instead.
|
|
Print func(thread *Thread, msg string)
|
|
|
|
// Load is the client-supplied implementation of module loading.
|
|
// Repeated calls with the same module name must return the same
|
|
// module environment or error.
|
|
// The error message need not include the module name.
|
|
//
|
|
// See example_test.go for some example implementations of Load.
|
|
Load func(thread *Thread, module string) (StringDict, error)
|
|
|
|
// locals holds arbitrary "thread-local" Go values belonging to the client.
|
|
// They are accessible to the client but not to any Starlark program.
|
|
locals map[string]interface{}
|
|
|
|
// proftime holds the accumulated execution time since the last profile event.
|
|
proftime time.Duration
|
|
}
|
|
|
|
// SetLocal sets the thread-local value associated with the specified key.
|
|
// It must not be called after execution begins.
|
|
func (thread *Thread) SetLocal(key string, value interface{}) {
|
|
if thread.locals == nil {
|
|
thread.locals = make(map[string]interface{})
|
|
}
|
|
thread.locals[key] = value
|
|
}
|
|
|
|
// Local returns the thread-local value associated with the specified key.
|
|
func (thread *Thread) Local(key string) interface{} {
|
|
return thread.locals[key]
|
|
}
|
|
|
|
// CallFrame returns a copy of the specified frame of the callstack.
|
|
// It should only be used in built-ins called from Starlark code.
|
|
// Depth 0 means the frame of the built-in itself, 1 is its caller, and so on.
|
|
//
|
|
// It is equivalent to CallStack().At(depth), but more efficient.
|
|
func (thread *Thread) CallFrame(depth int) CallFrame {
|
|
return thread.frameAt(depth).asCallFrame()
|
|
}
|
|
|
|
func (thread *Thread) frameAt(depth int) *frame {
|
|
return thread.stack[len(thread.stack)-1-depth]
|
|
}
|
|
|
|
// CallStack returns a new slice containing the thread's stack of call frames.
|
|
func (thread *Thread) CallStack() CallStack {
|
|
frames := make([]CallFrame, len(thread.stack))
|
|
for i, fr := range thread.stack {
|
|
frames[i] = fr.asCallFrame()
|
|
}
|
|
return frames
|
|
}
|
|
|
|
// CallStackDepth returns the number of frames in the current call stack.
|
|
func (thread *Thread) CallStackDepth() int { return len(thread.stack) }
|
|
|
|
// A StringDict is a mapping from names to values, and represents
|
|
// an environment such as the global variables of a module.
|
|
// It is not a true starlark.Value.
|
|
type StringDict map[string]Value
|
|
|
|
// Keys returns a new sorted slice of d's keys.
|
|
func (d StringDict) Keys() []string {
|
|
names := make([]string, 0, len(d))
|
|
for name := range d {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
return names
|
|
}
|
|
|
|
func (d StringDict) String() string {
|
|
buf := new(strings.Builder)
|
|
buf.WriteByte('{')
|
|
sep := ""
|
|
for _, name := range d.Keys() {
|
|
buf.WriteString(sep)
|
|
buf.WriteString(name)
|
|
buf.WriteString(": ")
|
|
writeValue(buf, d[name], nil)
|
|
sep = ", "
|
|
}
|
|
buf.WriteByte('}')
|
|
return buf.String()
|
|
}
|
|
|
|
func (d StringDict) Freeze() {
|
|
for _, v := range d {
|
|
v.Freeze()
|
|
}
|
|
}
|
|
|
|
// Has reports whether the dictionary contains the specified key.
|
|
func (d StringDict) Has(key string) bool { _, ok := d[key]; return ok }
|
|
|
|
// A frame records a call to a Starlark function (including module toplevel)
|
|
// or a built-in function or method.
|
|
type frame struct {
|
|
callable Callable // current function (or toplevel) or built-in
|
|
pc uint32 // program counter (Starlark frames only)
|
|
locals []Value // local variables (Starlark frames only)
|
|
spanStart int64 // start time of current profiler span
|
|
}
|
|
|
|
// Position returns the source position of the current point of execution in this frame.
|
|
func (fr *frame) Position() syntax.Position {
|
|
switch c := fr.callable.(type) {
|
|
case *Function:
|
|
// Starlark function
|
|
return c.funcode.Position(fr.pc)
|
|
case callableWithPosition:
|
|
// If a built-in Callable defines
|
|
// a Position method, use it.
|
|
return c.Position()
|
|
}
|
|
return syntax.MakePosition(&builtinFilename, 0, 0)
|
|
}
|
|
|
|
var builtinFilename = "<builtin>"
|
|
|
|
// Function returns the frame's function or built-in.
|
|
func (fr *frame) Callable() Callable { return fr.callable }
|
|
|
|
// A CallStack is a stack of call frames, outermost first.
|
|
type CallStack []CallFrame
|
|
|
|
// At returns a copy of the frame at depth i.
|
|
// At(0) returns the topmost frame.
|
|
func (stack CallStack) At(i int) CallFrame { return stack[len(stack)-1-i] }
|
|
|
|
// Pop removes and returns the topmost frame.
|
|
func (stack *CallStack) Pop() CallFrame {
|
|
last := len(*stack) - 1
|
|
top := (*stack)[last]
|
|
*stack = (*stack)[:last]
|
|
return top
|
|
}
|
|
|
|
// String returns a user-friendly description of the stack.
|
|
func (stack CallStack) String() string {
|
|
out := new(strings.Builder)
|
|
fmt.Fprintf(out, "Traceback (most recent call last):\n")
|
|
for _, fr := range stack {
|
|
fmt.Fprintf(out, " %s: in %s\n", fr.Pos, fr.Name)
|
|
}
|
|
return out.String()
|
|
}
|
|
|
|
// An EvalError is a Starlark evaluation error and
|
|
// a copy of the thread's stack at the moment of the error.
|
|
type EvalError struct {
|
|
Msg string
|
|
CallStack CallStack
|
|
cause error
|
|
}
|
|
|
|
// A CallFrame represents the function name and current
|
|
// position of execution of an enclosing call frame.
|
|
type CallFrame struct {
|
|
Name string
|
|
Pos syntax.Position
|
|
}
|
|
|
|
func (fr *frame) asCallFrame() CallFrame {
|
|
return CallFrame{
|
|
Name: fr.Callable().Name(),
|
|
Pos: fr.Position(),
|
|
}
|
|
}
|
|
|
|
func (thread *Thread) evalError(err error) *EvalError {
|
|
return &EvalError{
|
|
Msg: err.Error(),
|
|
CallStack: thread.CallStack(),
|
|
cause: err,
|
|
}
|
|
}
|
|
|
|
func (e *EvalError) Error() string { return e.Msg }
|
|
|
|
// Backtrace returns a user-friendly error message describing the stack
|
|
// of calls that led to this error.
|
|
func (e *EvalError) Backtrace() string {
|
|
return fmt.Sprintf("%sError: %s", e.CallStack, e.Msg)
|
|
}
|
|
|
|
func (e *EvalError) Unwrap() error { return e.cause }
|
|
|
|
// A Program is a compiled Starlark program.
|
|
//
|
|
// Programs are immutable, and contain no Values.
|
|
// A Program may be created by parsing a source file (see SourceProgram)
|
|
// or by loading a previously saved compiled program (see CompiledProgram).
|
|
type Program struct {
|
|
compiled *compile.Program
|
|
}
|
|
|
|
// CompilerVersion is the version number of the protocol for compiled
|
|
// files. Applications must not run programs compiled by one version
|
|
// with an interpreter at another version, and should thus incorporate
|
|
// the compiler version into the cache key when reusing compiled code.
|
|
const CompilerVersion = compile.Version
|
|
|
|
// Filename returns the name of the file from which this program was loaded.
|
|
func (prog *Program) Filename() string { return prog.compiled.Toplevel.Pos.Filename() }
|
|
|
|
func (prog *Program) String() string { return prog.Filename() }
|
|
|
|
// NumLoads returns the number of load statements in the compiled program.
|
|
func (prog *Program) NumLoads() int { return len(prog.compiled.Loads) }
|
|
|
|
// Load(i) returns the name and position of the i'th module directly
|
|
// loaded by this one, where 0 <= i < NumLoads().
|
|
// The name is unresolved---exactly as it appears in the source.
|
|
func (prog *Program) Load(i int) (string, syntax.Position) {
|
|
id := prog.compiled.Loads[i]
|
|
return id.Name, id.Pos
|
|
}
|
|
|
|
// WriteTo writes the compiled module to the specified output stream.
|
|
func (prog *Program) Write(out io.Writer) error {
|
|
data := prog.compiled.Encode()
|
|
_, err := out.Write(data)
|
|
return err
|
|
}
|
|
|
|
// ExecFile parses, resolves, and executes a Starlark file in the
|
|
// specified global environment, which may be modified during execution.
|
|
//
|
|
// Thread is the state associated with the Starlark thread.
|
|
//
|
|
// The filename and src parameters are as for syntax.Parse:
|
|
// filename is the name of the file to execute,
|
|
// and the name that appears in error messages;
|
|
// src is an optional source of bytes to use
|
|
// instead of filename.
|
|
//
|
|
// predeclared defines the predeclared names specific to this module.
|
|
// Execution does not modify this dictionary, though it may mutate
|
|
// its values.
|
|
//
|
|
// If ExecFile fails during evaluation, it returns an *EvalError
|
|
// containing a backtrace.
|
|
func ExecFile(thread *Thread, filename string, src interface{}, predeclared StringDict) (StringDict, error) {
|
|
// Parse, resolve, and compile a Starlark source file.
|
|
_, mod, err := SourceProgram(filename, src, predeclared.Has)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
g, err := mod.Init(thread, predeclared)
|
|
g.Freeze()
|
|
return g, err
|
|
}
|
|
|
|
// SourceProgram produces a new program by parsing, resolving,
|
|
// and compiling a Starlark source file.
|
|
// On success, it returns the parsed file and the compiled program.
|
|
// The filename and src parameters are as for syntax.Parse.
|
|
//
|
|
// The isPredeclared predicate reports whether a name is
|
|
// a pre-declared identifier of the current module.
|
|
// Its typical value is predeclared.Has,
|
|
// where predeclared is a StringDict of pre-declared values.
|
|
func SourceProgram(filename string, src interface{}, isPredeclared func(string) bool) (*syntax.File, *Program, error) {
|
|
f, err := syntax.Parse(filename, src, 0)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
prog, err := FileProgram(f, isPredeclared)
|
|
return f, prog, err
|
|
}
|
|
|
|
// FileProgram produces a new program by resolving,
|
|
// and compiling the Starlark source file syntax tree.
|
|
// On success, it returns the compiled program.
|
|
//
|
|
// Resolving a syntax tree mutates it.
|
|
// Do not call FileProgram more than once on the same file.
|
|
//
|
|
// The isPredeclared predicate reports whether a name is
|
|
// a pre-declared identifier of the current module.
|
|
// Its typical value is predeclared.Has,
|
|
// where predeclared is a StringDict of pre-declared values.
|
|
func FileProgram(f *syntax.File, isPredeclared func(string) bool) (*Program, error) {
|
|
if err := resolve.File(f, isPredeclared, Universe.Has); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var pos syntax.Position
|
|
if len(f.Stmts) > 0 {
|
|
pos = syntax.Start(f.Stmts[0])
|
|
} else {
|
|
pos = syntax.MakePosition(&f.Path, 1, 1)
|
|
}
|
|
|
|
module := f.Module.(*resolve.Module)
|
|
compiled := compile.File(f.Stmts, pos, "<toplevel>", module.Locals, module.Globals)
|
|
|
|
return &Program{compiled}, nil
|
|
}
|
|
|
|
// CompiledProgram produces a new program from the representation
|
|
// of a compiled program previously saved by Program.Write.
|
|
func CompiledProgram(in io.Reader) (*Program, error) {
|
|
data, err := ioutil.ReadAll(in)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
compiled, err := compile.DecodeProgram(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &Program{compiled}, nil
|
|
}
|
|
|
|
// Init creates a set of global variables for the program,
|
|
// executes the toplevel code of the specified program,
|
|
// and returns a new, unfrozen dictionary of the globals.
|
|
func (prog *Program) Init(thread *Thread, predeclared StringDict) (StringDict, error) {
|
|
toplevel := makeToplevelFunction(prog.compiled, predeclared)
|
|
|
|
_, err := Call(thread, toplevel, nil, nil)
|
|
|
|
// Convert the global environment to a map.
|
|
// We return a (partial) map even in case of error.
|
|
return toplevel.Globals(), err
|
|
}
|
|
|
|
// ExecREPLChunk compiles and executes file f in the specified thread
|
|
// and global environment. This is a variant of ExecFile specialized to
|
|
// the needs of a REPL, in which a sequence of input chunks, each
|
|
// syntactically a File, manipulates the same set of module globals,
|
|
// which are not frozen after execution.
|
|
//
|
|
// This function is intended to support only go.starlark.net/repl.
|
|
// Its API stability is not guaranteed.
|
|
func ExecREPLChunk(f *syntax.File, thread *Thread, globals StringDict) error {
|
|
var predeclared StringDict
|
|
|
|
// -- variant of FileProgram --
|
|
|
|
if err := resolve.REPLChunk(f, globals.Has, predeclared.Has, Universe.Has); err != nil {
|
|
return err
|
|
}
|
|
|
|
var pos syntax.Position
|
|
if len(f.Stmts) > 0 {
|
|
pos = syntax.Start(f.Stmts[0])
|
|
} else {
|
|
pos = syntax.MakePosition(&f.Path, 1, 1)
|
|
}
|
|
|
|
module := f.Module.(*resolve.Module)
|
|
compiled := compile.File(f.Stmts, pos, "<toplevel>", module.Locals, module.Globals)
|
|
prog := &Program{compiled}
|
|
|
|
// -- variant of Program.Init --
|
|
|
|
toplevel := makeToplevelFunction(prog.compiled, predeclared)
|
|
|
|
// Initialize module globals from parameter.
|
|
for i, id := range prog.compiled.Globals {
|
|
if v := globals[id.Name]; v != nil {
|
|
toplevel.module.globals[i] = v
|
|
}
|
|
}
|
|
|
|
_, err := Call(thread, toplevel, nil, nil)
|
|
|
|
// Reflect changes to globals back to parameter, even after an error.
|
|
for i, id := range prog.compiled.Globals {
|
|
if v := toplevel.module.globals[i]; v != nil {
|
|
globals[id.Name] = v
|
|
}
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func makeToplevelFunction(prog *compile.Program, predeclared StringDict) *Function {
|
|
// Create the Starlark value denoted by each program constant c.
|
|
constants := make([]Value, len(prog.Constants))
|
|
for i, c := range prog.Constants {
|
|
var v Value
|
|
switch c := c.(type) {
|
|
case int64:
|
|
v = MakeInt64(c)
|
|
case *big.Int:
|
|
v = MakeBigInt(c)
|
|
case string:
|
|
v = String(c)
|
|
case float64:
|
|
v = Float(c)
|
|
default:
|
|
log.Panicf("unexpected constant %T: %v", c, c)
|
|
}
|
|
constants[i] = v
|
|
}
|
|
|
|
return &Function{
|
|
funcode: prog.Toplevel,
|
|
module: &module{
|
|
program: prog,
|
|
predeclared: predeclared,
|
|
globals: make([]Value, len(prog.Globals)),
|
|
constants: constants,
|
|
},
|
|
}
|
|
}
|
|
|
|
// Eval parses, resolves, and evaluates an expression within the
|
|
// specified (predeclared) environment.
|
|
//
|
|
// Evaluation cannot mutate the environment dictionary itself,
|
|
// though it may modify variables reachable from the dictionary.
|
|
//
|
|
// The filename and src parameters are as for syntax.Parse.
|
|
//
|
|
// If Eval fails during evaluation, it returns an *EvalError
|
|
// containing a backtrace.
|
|
func Eval(thread *Thread, filename string, src interface{}, env StringDict) (Value, error) {
|
|
expr, err := syntax.ParseExpr(filename, src, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
f, err := makeExprFunc(expr, env)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return Call(thread, f, nil, nil)
|
|
}
|
|
|
|
// EvalExpr resolves and evaluates an expression within the
|
|
// specified (predeclared) environment.
|
|
// Evaluating a comma-separated list of expressions yields a tuple value.
|
|
//
|
|
// Resolving an expression mutates it.
|
|
// Do not call EvalExpr more than once for the same expression.
|
|
//
|
|
// Evaluation cannot mutate the environment dictionary itself,
|
|
// though it may modify variables reachable from the dictionary.
|
|
//
|
|
// If Eval fails during evaluation, it returns an *EvalError
|
|
// containing a backtrace.
|
|
func EvalExpr(thread *Thread, expr syntax.Expr, env StringDict) (Value, error) {
|
|
fn, err := makeExprFunc(expr, env)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return Call(thread, fn, nil, nil)
|
|
}
|
|
|
|
// ExprFunc returns a no-argument function
|
|
// that evaluates the expression whose source is src.
|
|
func ExprFunc(filename string, src interface{}, env StringDict) (*Function, error) {
|
|
expr, err := syntax.ParseExpr(filename, src, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return makeExprFunc(expr, env)
|
|
}
|
|
|
|
// makeExprFunc returns a no-argument function whose body is expr.
|
|
func makeExprFunc(expr syntax.Expr, env StringDict) (*Function, error) {
|
|
locals, err := resolve.Expr(expr, env.Has, Universe.Has)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return makeToplevelFunction(compile.Expr(expr, "<expr>", locals), env), nil
|
|
}
|
|
|
|
// The following functions are primitive operations of the byte code interpreter.
|
|
|
|
// list += iterable
|
|
func listExtend(x *List, y Iterable) {
|
|
if ylist, ok := y.(*List); ok {
|
|
// fast path: list += list
|
|
x.elems = append(x.elems, ylist.elems...)
|
|
} else {
|
|
iter := y.Iterate()
|
|
defer iter.Done()
|
|
var z Value
|
|
for iter.Next(&z) {
|
|
x.elems = append(x.elems, z)
|
|
}
|
|
}
|
|
}
|
|
|
|
// getAttr implements x.dot.
|
|
func getAttr(x Value, name string) (Value, error) {
|
|
hasAttr, ok := x.(HasAttrs)
|
|
if !ok {
|
|
return nil, fmt.Errorf("%s has no .%s field or method", x.Type(), name)
|
|
}
|
|
|
|
var errmsg string
|
|
v, err := hasAttr.Attr(name)
|
|
if err == nil {
|
|
if v != nil {
|
|
return v, nil // success
|
|
}
|
|
// (nil, nil) => generic error
|
|
errmsg = fmt.Sprintf("%s has no .%s field or method", x.Type(), name)
|
|
} else if nsa, ok := err.(NoSuchAttrError); ok {
|
|
errmsg = string(nsa)
|
|
} else {
|
|
return nil, err // return error as is
|
|
}
|
|
|
|
// add spelling hint
|
|
if n := spell.Nearest(name, hasAttr.AttrNames()); n != "" {
|
|
errmsg = fmt.Sprintf("%s (did you mean .%s?)", errmsg, n)
|
|
}
|
|
|
|
return nil, fmt.Errorf("%s", errmsg)
|
|
}
|
|
|
|
// setField implements x.name = y.
|
|
func setField(x Value, name string, y Value) error {
|
|
if x, ok := x.(HasSetField); ok {
|
|
err := x.SetField(name, y)
|
|
if _, ok := err.(NoSuchAttrError); ok {
|
|
// No such field: check spelling.
|
|
if n := spell.Nearest(name, x.AttrNames()); n != "" {
|
|
err = fmt.Errorf("%s (did you mean .%s?)", err, n)
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
return fmt.Errorf("can't assign to .%s field of %s", name, x.Type())
|
|
}
|
|
|
|
// getIndex implements x[y].
|
|
func getIndex(x, y Value) (Value, error) {
|
|
switch x := x.(type) {
|
|
case Mapping: // dict
|
|
z, found, err := x.Get(y)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !found {
|
|
return nil, fmt.Errorf("key %v not in %s", y, x.Type())
|
|
}
|
|
return z, nil
|
|
|
|
case Indexable: // string, list, tuple
|
|
n := x.Len()
|
|
i, err := AsInt32(y)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s index: %s", x.Type(), err)
|
|
}
|
|
origI := i
|
|
if i < 0 {
|
|
i += n
|
|
}
|
|
if i < 0 || i >= n {
|
|
return nil, outOfRange(origI, n, x)
|
|
}
|
|
return x.Index(i), nil
|
|
}
|
|
return nil, fmt.Errorf("unhandled index operation %s[%s]", x.Type(), y.Type())
|
|
}
|
|
|
|
func outOfRange(i, n int, x Value) error {
|
|
if n == 0 {
|
|
return fmt.Errorf("index %d out of range: empty %s", i, x.Type())
|
|
} else {
|
|
return fmt.Errorf("%s index %d out of range [%d:%d]", x.Type(), i, -n, n-1)
|
|
}
|
|
}
|
|
|
|
// setIndex implements x[y] = z.
|
|
func setIndex(x, y, z Value) error {
|
|
switch x := x.(type) {
|
|
case HasSetKey:
|
|
if err := x.SetKey(y, z); err != nil {
|
|
return err
|
|
}
|
|
|
|
case HasSetIndex:
|
|
n := x.Len()
|
|
i, err := AsInt32(y)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
origI := i
|
|
if i < 0 {
|
|
i += n
|
|
}
|
|
if i < 0 || i >= n {
|
|
return outOfRange(origI, n, x)
|
|
}
|
|
return x.SetIndex(i, z)
|
|
|
|
default:
|
|
return fmt.Errorf("%s value does not support item assignment", x.Type())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Unary applies a unary operator (+, -, ~, not) to its operand.
|
|
func Unary(op syntax.Token, x Value) (Value, error) {
|
|
// The NOT operator is not customizable.
|
|
if op == syntax.NOT {
|
|
return !x.Truth(), nil
|
|
}
|
|
|
|
// Int, Float, and user-defined types
|
|
if x, ok := x.(HasUnary); ok {
|
|
// (nil, nil) => unhandled
|
|
y, err := x.Unary(op)
|
|
if y != nil || err != nil {
|
|
return y, err
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("unknown unary op: %s %s", op, x.Type())
|
|
}
|
|
|
|
// Binary applies a strict binary operator (not AND or OR) to its operands.
|
|
// For equality tests or ordered comparisons, use Compare instead.
|
|
func Binary(op syntax.Token, x, y Value) (Value, error) {
|
|
switch op {
|
|
case syntax.PLUS:
|
|
switch x := x.(type) {
|
|
case String:
|
|
if y, ok := y.(String); ok {
|
|
return x + y, nil
|
|
}
|
|
case Int:
|
|
switch y := y.(type) {
|
|
case Int:
|
|
return x.Add(y), nil
|
|
case Float:
|
|
return x.Float() + y, nil
|
|
}
|
|
case Float:
|
|
switch y := y.(type) {
|
|
case Float:
|
|
return x + y, nil
|
|
case Int:
|
|
return x + y.Float(), nil
|
|
}
|
|
case *List:
|
|
if y, ok := y.(*List); ok {
|
|
z := make([]Value, 0, x.Len()+y.Len())
|
|
z = append(z, x.elems...)
|
|
z = append(z, y.elems...)
|
|
return NewList(z), nil
|
|
}
|
|
case Tuple:
|
|
if y, ok := y.(Tuple); ok {
|
|
z := make(Tuple, 0, len(x)+len(y))
|
|
z = append(z, x...)
|
|
z = append(z, y...)
|
|
return z, nil
|
|
}
|
|
}
|
|
|
|
case syntax.MINUS:
|
|
switch x := x.(type) {
|
|
case Int:
|
|
switch y := y.(type) {
|
|
case Int:
|
|
return x.Sub(y), nil
|
|
case Float:
|
|
return x.Float() - y, nil
|
|
}
|
|
case Float:
|
|
switch y := y.(type) {
|
|
case Float:
|
|
return x - y, nil
|
|
case Int:
|
|
return x - y.Float(), nil
|
|
}
|
|
}
|
|
|
|
case syntax.STAR:
|
|
switch x := x.(type) {
|
|
case Int:
|
|
switch y := y.(type) {
|
|
case Int:
|
|
return x.Mul(y), nil
|
|
case Float:
|
|
return x.Float() * y, nil
|
|
case String:
|
|
return stringRepeat(y, x)
|
|
case *List:
|
|
elems, err := tupleRepeat(Tuple(y.elems), x)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return NewList(elems), nil
|
|
case Tuple:
|
|
return tupleRepeat(y, x)
|
|
}
|
|
case Float:
|
|
switch y := y.(type) {
|
|
case Float:
|
|
return x * y, nil
|
|
case Int:
|
|
return x * y.Float(), nil
|
|
}
|
|
case String:
|
|
if y, ok := y.(Int); ok {
|
|
return stringRepeat(x, y)
|
|
}
|
|
case *List:
|
|
if y, ok := y.(Int); ok {
|
|
elems, err := tupleRepeat(Tuple(x.elems), y)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return NewList(elems), nil
|
|
}
|
|
case Tuple:
|
|
if y, ok := y.(Int); ok {
|
|
return tupleRepeat(x, y)
|
|
}
|
|
|
|
}
|
|
|
|
case syntax.SLASH:
|
|
switch x := x.(type) {
|
|
case Int:
|
|
switch y := y.(type) {
|
|
case Int:
|
|
yf := y.Float()
|
|
if yf == 0.0 {
|
|
return nil, fmt.Errorf("real division by zero")
|
|
}
|
|
return x.Float() / yf, nil
|
|
case Float:
|
|
if y == 0.0 {
|
|
return nil, fmt.Errorf("real division by zero")
|
|
}
|
|
return x.Float() / y, nil
|
|
}
|
|
case Float:
|
|
switch y := y.(type) {
|
|
case Float:
|
|
if y == 0.0 {
|
|
return nil, fmt.Errorf("real division by zero")
|
|
}
|
|
return x / y, nil
|
|
case Int:
|
|
yf := y.Float()
|
|
if yf == 0.0 {
|
|
return nil, fmt.Errorf("real division by zero")
|
|
}
|
|
return x / yf, nil
|
|
}
|
|
}
|
|
|
|
case syntax.SLASHSLASH:
|
|
switch x := x.(type) {
|
|
case Int:
|
|
switch y := y.(type) {
|
|
case Int:
|
|
if y.Sign() == 0 {
|
|
return nil, fmt.Errorf("floored division by zero")
|
|
}
|
|
return x.Div(y), nil
|
|
case Float:
|
|
if y == 0.0 {
|
|
return nil, fmt.Errorf("floored division by zero")
|
|
}
|
|
return floor((x.Float() / y)), nil
|
|
}
|
|
case Float:
|
|
switch y := y.(type) {
|
|
case Float:
|
|
if y == 0.0 {
|
|
return nil, fmt.Errorf("floored division by zero")
|
|
}
|
|
return floor(x / y), nil
|
|
case Int:
|
|
yf := y.Float()
|
|
if yf == 0.0 {
|
|
return nil, fmt.Errorf("floored division by zero")
|
|
}
|
|
return floor(x / yf), nil
|
|
}
|
|
}
|
|
|
|
case syntax.PERCENT:
|
|
switch x := x.(type) {
|
|
case Int:
|
|
switch y := y.(type) {
|
|
case Int:
|
|
if y.Sign() == 0 {
|
|
return nil, fmt.Errorf("integer modulo by zero")
|
|
}
|
|
return x.Mod(y), nil
|
|
case Float:
|
|
if y == 0 {
|
|
return nil, fmt.Errorf("float modulo by zero")
|
|
}
|
|
return x.Float().Mod(y), nil
|
|
}
|
|
case Float:
|
|
switch y := y.(type) {
|
|
case Float:
|
|
if y == 0.0 {
|
|
return nil, fmt.Errorf("float modulo by zero")
|
|
}
|
|
return Float(math.Mod(float64(x), float64(y))), nil
|
|
case Int:
|
|
if y.Sign() == 0 {
|
|
return nil, fmt.Errorf("float modulo by zero")
|
|
}
|
|
return x.Mod(y.Float()), nil
|
|
}
|
|
case String:
|
|
return interpolate(string(x), y)
|
|
}
|
|
|
|
case syntax.NOT_IN:
|
|
z, err := Binary(syntax.IN, x, y)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return !z.Truth(), nil
|
|
|
|
case syntax.IN:
|
|
switch y := y.(type) {
|
|
case *List:
|
|
for _, elem := range y.elems {
|
|
if eq, err := Equal(elem, x); err != nil {
|
|
return nil, err
|
|
} else if eq {
|
|
return True, nil
|
|
}
|
|
}
|
|
return False, nil
|
|
case Tuple:
|
|
for _, elem := range y {
|
|
if eq, err := Equal(elem, x); err != nil {
|
|
return nil, err
|
|
} else if eq {
|
|
return True, nil
|
|
}
|
|
}
|
|
return False, nil
|
|
case Mapping: // e.g. dict
|
|
// Ignore error from Get as we cannot distinguish true
|
|
// errors (value cycle, type error) from "key not found".
|
|
_, found, _ := y.Get(x)
|
|
return Bool(found), nil
|
|
case *Set:
|
|
ok, err := y.Has(x)
|
|
return Bool(ok), err
|
|
case String:
|
|
needle, ok := x.(String)
|
|
if !ok {
|
|
return nil, fmt.Errorf("'in <string>' requires string as left operand, not %s", x.Type())
|
|
}
|
|
return Bool(strings.Contains(string(y), string(needle))), nil
|
|
case rangeValue:
|
|
i, err := NumberToInt(x)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("'in <range>' requires integer as left operand, not %s", x.Type())
|
|
}
|
|
return Bool(y.contains(i)), nil
|
|
}
|
|
|
|
case syntax.PIPE:
|
|
switch x := x.(type) {
|
|
case Int:
|
|
if y, ok := y.(Int); ok {
|
|
return x.Or(y), nil
|
|
}
|
|
case *Set: // union
|
|
if y, ok := y.(*Set); ok {
|
|
iter := Iterate(y)
|
|
defer iter.Done()
|
|
return x.Union(iter)
|
|
}
|
|
}
|
|
|
|
case syntax.AMP:
|
|
switch x := x.(type) {
|
|
case Int:
|
|
if y, ok := y.(Int); ok {
|
|
return x.And(y), nil
|
|
}
|
|
case *Set: // intersection
|
|
if y, ok := y.(*Set); ok {
|
|
set := new(Set)
|
|
if x.Len() > y.Len() {
|
|
x, y = y, x // opt: range over smaller set
|
|
}
|
|
for _, xelem := range x.elems() {
|
|
// Has, Insert cannot fail here.
|
|
if found, _ := y.Has(xelem); found {
|
|
set.Insert(xelem)
|
|
}
|
|
}
|
|
return set, nil
|
|
}
|
|
}
|
|
|
|
case syntax.CIRCUMFLEX:
|
|
switch x := x.(type) {
|
|
case Int:
|
|
if y, ok := y.(Int); ok {
|
|
return x.Xor(y), nil
|
|
}
|
|
case *Set: // symmetric difference
|
|
if y, ok := y.(*Set); ok {
|
|
set := new(Set)
|
|
for _, xelem := range x.elems() {
|
|
if found, _ := y.Has(xelem); !found {
|
|
set.Insert(xelem)
|
|
}
|
|
}
|
|
for _, yelem := range y.elems() {
|
|
if found, _ := x.Has(yelem); !found {
|
|
set.Insert(yelem)
|
|
}
|
|
}
|
|
return set, nil
|
|
}
|
|
}
|
|
|
|
case syntax.LTLT, syntax.GTGT:
|
|
if x, ok := x.(Int); ok {
|
|
y, err := AsInt32(y)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if y < 0 {
|
|
return nil, fmt.Errorf("negative shift count: %v", y)
|
|
}
|
|
if op == syntax.LTLT {
|
|
if y >= 512 {
|
|
return nil, fmt.Errorf("shift count too large: %v", y)
|
|
}
|
|
return x.Lsh(uint(y)), nil
|
|
} else {
|
|
return x.Rsh(uint(y)), nil
|
|
}
|
|
}
|
|
|
|
default:
|
|
// unknown operator
|
|
goto unknown
|
|
}
|
|
|
|
// user-defined types
|
|
// (nil, nil) => unhandled
|
|
if x, ok := x.(HasBinary); ok {
|
|
z, err := x.Binary(op, y, Left)
|
|
if z != nil || err != nil {
|
|
return z, err
|
|
}
|
|
}
|
|
if y, ok := y.(HasBinary); ok {
|
|
z, err := y.Binary(op, x, Right)
|
|
if z != nil || err != nil {
|
|
return z, err
|
|
}
|
|
}
|
|
|
|
// unsupported operand types
|
|
unknown:
|
|
return nil, fmt.Errorf("unknown binary op: %s %s %s", x.Type(), op, y.Type())
|
|
}
|
|
|
|
// It's always possible to overeat in small bites but we'll
|
|
// try to stop someone swallowing the world in one gulp.
|
|
const maxAlloc = 1 << 30
|
|
|
|
func tupleRepeat(elems Tuple, n Int) (Tuple, error) {
|
|
if len(elems) == 0 {
|
|
return nil, nil
|
|
}
|
|
i, err := AsInt32(n)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("repeat count %s too large", n)
|
|
}
|
|
if i < 1 {
|
|
return nil, nil
|
|
}
|
|
// Inv: i > 0, len > 0
|
|
sz := len(elems) * i
|
|
if sz < 0 || sz >= maxAlloc { // sz < 0 => overflow
|
|
return nil, fmt.Errorf("excessive repeat (%d elements)", sz)
|
|
}
|
|
res := make([]Value, sz)
|
|
// copy elems into res, doubling each time
|
|
x := copy(res, elems)
|
|
for x < len(res) {
|
|
copy(res[x:], res[:x])
|
|
x *= 2
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func stringRepeat(s String, n Int) (String, error) {
|
|
if s == "" {
|
|
return "", nil
|
|
}
|
|
i, err := AsInt32(n)
|
|
if err != nil {
|
|
return "", fmt.Errorf("repeat count %s too large", n)
|
|
}
|
|
if i < 1 {
|
|
return "", nil
|
|
}
|
|
// Inv: i > 0, len > 0
|
|
sz := len(s) * i
|
|
if sz < 0 || sz >= maxAlloc { // sz < 0 => overflow
|
|
return "", fmt.Errorf("excessive repeat (%d elements)", sz)
|
|
}
|
|
return String(strings.Repeat(string(s), i)), nil
|
|
}
|
|
|
|
// Call calls the function fn with the specified positional and keyword arguments.
|
|
func Call(thread *Thread, fn Value, args Tuple, kwargs []Tuple) (Value, error) {
|
|
c, ok := fn.(Callable)
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid call of non-function (%s)", fn.Type())
|
|
}
|
|
|
|
// Allocate and push a new frame.
|
|
var fr *frame
|
|
// Optimization: use slack portion of thread.stack
|
|
// slice as a freelist of empty frames.
|
|
if n := len(thread.stack); n < cap(thread.stack) {
|
|
fr = thread.stack[n : n+1][0]
|
|
}
|
|
if fr == nil {
|
|
fr = new(frame)
|
|
}
|
|
thread.stack = append(thread.stack, fr) // push
|
|
|
|
fr.callable = c
|
|
|
|
thread.beginProfSpan()
|
|
result, err := c.CallInternal(thread, args, kwargs)
|
|
thread.endProfSpan()
|
|
|
|
// Sanity check: nil is not a valid Starlark value.
|
|
if result == nil && err == nil {
|
|
err = fmt.Errorf("internal error: nil (not None) returned from %s", fn)
|
|
}
|
|
|
|
// Always return an EvalError with an accurate frame.
|
|
if err != nil {
|
|
if _, ok := err.(*EvalError); !ok {
|
|
err = thread.evalError(err)
|
|
}
|
|
}
|
|
|
|
*fr = frame{} // clear out any references
|
|
thread.stack = thread.stack[:len(thread.stack)-1] // pop
|
|
|
|
return result, err
|
|
}
|
|
|
|
func slice(x, lo, hi, step_ Value) (Value, error) {
|
|
sliceable, ok := x.(Sliceable)
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid slice operand %s", x.Type())
|
|
}
|
|
|
|
n := sliceable.Len()
|
|
step := 1
|
|
if step_ != None {
|
|
var err error
|
|
step, err = AsInt32(step_)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("got %s for slice step, want int", step_.Type())
|
|
}
|
|
if step == 0 {
|
|
return nil, fmt.Errorf("zero is not a valid slice step")
|
|
}
|
|
}
|
|
|
|
// TODO(adonovan): opt: preallocate result array.
|
|
|
|
var start, end int
|
|
if step > 0 {
|
|
// positive stride
|
|
// default indices are [0:n].
|
|
var err error
|
|
start, end, err = indices(lo, hi, n)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if end < start {
|
|
end = start // => empty result
|
|
}
|
|
} else {
|
|
// negative stride
|
|
// default indices are effectively [n-1:-1], though to
|
|
// get this effect using explicit indices requires
|
|
// [n-1:-1-n:-1] because of the treatment of -ve values.
|
|
start = n - 1
|
|
if err := asIndex(lo, n, &start); err != nil {
|
|
return nil, fmt.Errorf("invalid start index: %s", err)
|
|
}
|
|
if start >= n {
|
|
start = n - 1
|
|
}
|
|
|
|
end = -1
|
|
if err := asIndex(hi, n, &end); err != nil {
|
|
return nil, fmt.Errorf("invalid end index: %s", err)
|
|
}
|
|
if end < -1 {
|
|
end = -1
|
|
}
|
|
|
|
if start < end {
|
|
start = end // => empty result
|
|
}
|
|
}
|
|
|
|
return sliceable.Slice(start, end, step), nil
|
|
}
|
|
|
|
// From Hacker's Delight, section 2.8.
|
|
func signum64(x int64) int { return int(uint64(x>>63) | uint64(-x)>>63) }
|
|
func signum(x int) int { return signum64(int64(x)) }
|
|
|
|
// indices converts start_ and end_ to indices in the range [0:len].
|
|
// The start index defaults to 0 and the end index defaults to len.
|
|
// An index -len < i < 0 is treated like i+len.
|
|
// All other indices outside the range are clamped to the nearest value in the range.
|
|
// Beware: start may be greater than end.
|
|
// This function is suitable only for slices with positive strides.
|
|
func indices(start_, end_ Value, len int) (start, end int, err error) {
|
|
start = 0
|
|
if err := asIndex(start_, len, &start); err != nil {
|
|
return 0, 0, fmt.Errorf("invalid start index: %s", err)
|
|
}
|
|
// Clamp to [0:len].
|
|
if start < 0 {
|
|
start = 0
|
|
} else if start > len {
|
|
start = len
|
|
}
|
|
|
|
end = len
|
|
if err := asIndex(end_, len, &end); err != nil {
|
|
return 0, 0, fmt.Errorf("invalid end index: %s", err)
|
|
}
|
|
// Clamp to [0:len].
|
|
if end < 0 {
|
|
end = 0
|
|
} else if end > len {
|
|
end = len
|
|
}
|
|
|
|
return start, end, nil
|
|
}
|
|
|
|
// asIndex sets *result to the integer value of v, adding len to it
|
|
// if it is negative. If v is nil or None, *result is unchanged.
|
|
func asIndex(v Value, len int, result *int) error {
|
|
if v != nil && v != None {
|
|
var err error
|
|
*result, err = AsInt32(v)
|
|
if err != nil {
|
|
return fmt.Errorf("got %s, want int", v.Type())
|
|
}
|
|
if *result < 0 {
|
|
*result += len
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// setArgs sets the values of the formal parameters of function fn in
|
|
// based on the actual parameter values in args and kwargs.
|
|
func setArgs(locals []Value, fn *Function, args Tuple, kwargs []Tuple) error {
|
|
|
|
// This is the general schema of a function:
|
|
//
|
|
// def f(p1, p2=dp2, p3=dp3, *args, k1, k2=dk2, k3, **kwargs)
|
|
//
|
|
// The p parameters are non-kwonly, and may be specified positionally.
|
|
// The k parameters are kwonly, and must be specified by name.
|
|
// The defaults tuple is (dp2, dp3, mandatory, dk2, mandatory).
|
|
//
|
|
// Arguments are processed as follows:
|
|
// - positional arguments are bound to a prefix of [p1, p2, p3].
|
|
// - surplus positional arguments are bound to *args.
|
|
// - keyword arguments are bound to any of {p1, p2, p3, k1, k2, k3};
|
|
// duplicate bindings are rejected.
|
|
// - surplus keyword arguments are bound to **kwargs.
|
|
// - defaults are bound to each parameter from p2 to k3 if no value was set.
|
|
// default values come from the tuple above.
|
|
// It is an error if the tuple entry for an unset parameter is 'mandatory'.
|
|
|
|
// Nullary function?
|
|
if fn.NumParams() == 0 {
|
|
if nactual := len(args) + len(kwargs); nactual > 0 {
|
|
return fmt.Errorf("function %s accepts no arguments (%d given)", fn.Name(), nactual)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
cond := func(x bool, y, z interface{}) interface{} {
|
|
if x {
|
|
return y
|
|
}
|
|
return z
|
|
}
|
|
|
|
// nparams is the number of ordinary parameters (sans *args and **kwargs).
|
|
nparams := fn.NumParams()
|
|
var kwdict *Dict
|
|
if fn.HasKwargs() {
|
|
nparams--
|
|
kwdict = new(Dict)
|
|
locals[nparams] = kwdict
|
|
}
|
|
if fn.HasVarargs() {
|
|
nparams--
|
|
}
|
|
|
|
// nonkwonly is the number of non-kwonly parameters.
|
|
nonkwonly := nparams - fn.NumKwonlyParams()
|
|
|
|
// Too many positional args?
|
|
n := len(args)
|
|
if len(args) > nonkwonly {
|
|
if !fn.HasVarargs() {
|
|
return fmt.Errorf("function %s accepts %s%d positional argument%s (%d given)",
|
|
fn.Name(),
|
|
cond(len(fn.defaults) > fn.NumKwonlyParams(), "at most ", ""),
|
|
nonkwonly,
|
|
cond(nonkwonly == 1, "", "s"),
|
|
len(args))
|
|
}
|
|
n = nonkwonly
|
|
}
|
|
|
|
// Bind positional arguments to non-kwonly parameters.
|
|
for i := 0; i < n; i++ {
|
|
locals[i] = args[i]
|
|
}
|
|
|
|
// Bind surplus positional arguments to *args parameter.
|
|
if fn.HasVarargs() {
|
|
tuple := make(Tuple, len(args)-n)
|
|
for i := n; i < len(args); i++ {
|
|
tuple[i-n] = args[i]
|
|
}
|
|
locals[nparams] = tuple
|
|
}
|
|
|
|
// Bind keyword arguments to parameters.
|
|
paramIdents := fn.funcode.Locals[:nparams]
|
|
for _, pair := range kwargs {
|
|
k, v := pair[0].(String), pair[1]
|
|
if i := findParam(paramIdents, string(k)); i >= 0 {
|
|
if locals[i] != nil {
|
|
return fmt.Errorf("function %s got multiple values for parameter %s", fn.Name(), k)
|
|
}
|
|
locals[i] = v
|
|
continue
|
|
}
|
|
if kwdict == nil {
|
|
return fmt.Errorf("function %s got an unexpected keyword argument %s", fn.Name(), k)
|
|
}
|
|
oldlen := kwdict.Len()
|
|
kwdict.SetKey(k, v)
|
|
if kwdict.Len() == oldlen {
|
|
return fmt.Errorf("function %s got multiple values for parameter %s", fn.Name(), k)
|
|
}
|
|
}
|
|
|
|
// Are defaults required?
|
|
if n < nparams || fn.NumKwonlyParams() > 0 {
|
|
m := nparams - len(fn.defaults) // first default
|
|
|
|
// Report errors for missing required arguments.
|
|
var missing []string
|
|
var i int
|
|
for i = n; i < m; i++ {
|
|
if locals[i] == nil {
|
|
missing = append(missing, paramIdents[i].Name)
|
|
}
|
|
}
|
|
|
|
// Bind default values to parameters.
|
|
for ; i < nparams; i++ {
|
|
if locals[i] == nil {
|
|
dflt := fn.defaults[i-m]
|
|
if _, ok := dflt.(mandatory); ok {
|
|
missing = append(missing, paramIdents[i].Name)
|
|
continue
|
|
}
|
|
locals[i] = dflt
|
|
}
|
|
}
|
|
|
|
if missing != nil {
|
|
return fmt.Errorf("function %s missing %d argument%s (%s)",
|
|
fn.Name(), len(missing), cond(len(missing) > 1, "s", ""), strings.Join(missing, ", "))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func findParam(params []compile.Binding, name string) int {
|
|
for i, param := range params {
|
|
if param.Name == name {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
// https://github.com/google/starlark-go/blob/master/doc/spec.md#string-interpolation
|
|
func interpolate(format string, x Value) (Value, error) {
|
|
buf := new(strings.Builder)
|
|
index := 0
|
|
nargs := 1
|
|
if tuple, ok := x.(Tuple); ok {
|
|
nargs = len(tuple)
|
|
}
|
|
for {
|
|
i := strings.IndexByte(format, '%')
|
|
if i < 0 {
|
|
buf.WriteString(format)
|
|
break
|
|
}
|
|
buf.WriteString(format[:i])
|
|
format = format[i+1:]
|
|
|
|
if format != "" && format[0] == '%' {
|
|
buf.WriteByte('%')
|
|
format = format[1:]
|
|
continue
|
|
}
|
|
|
|
var arg Value
|
|
if format != "" && format[0] == '(' {
|
|
// keyword argument: %(name)s.
|
|
format = format[1:]
|
|
j := strings.IndexByte(format, ')')
|
|
if j < 0 {
|
|
return nil, fmt.Errorf("incomplete format key")
|
|
}
|
|
key := format[:j]
|
|
if dict, ok := x.(Mapping); !ok {
|
|
return nil, fmt.Errorf("format requires a mapping")
|
|
} else if v, found, _ := dict.Get(String(key)); found {
|
|
arg = v
|
|
} else {
|
|
return nil, fmt.Errorf("key not found: %s", key)
|
|
}
|
|
format = format[j+1:]
|
|
} else {
|
|
// positional argument: %s.
|
|
if index >= nargs {
|
|
return nil, fmt.Errorf("not enough arguments for format string")
|
|
}
|
|
if tuple, ok := x.(Tuple); ok {
|
|
arg = tuple[index]
|
|
} else {
|
|
arg = x
|
|
}
|
|
}
|
|
|
|
// NOTE: Starlark does not support any of these optional Python features:
|
|
// - optional conversion flags: [#0- +], etc.
|
|
// - optional minimum field width (number or *).
|
|
// - optional precision (.123 or *)
|
|
// - optional length modifier
|
|
|
|
// conversion type
|
|
if format == "" {
|
|
return nil, fmt.Errorf("incomplete format")
|
|
}
|
|
switch c := format[0]; c {
|
|
case 's', 'r':
|
|
if str, ok := AsString(arg); ok && c == 's' {
|
|
buf.WriteString(str)
|
|
} else {
|
|
writeValue(buf, arg, nil)
|
|
}
|
|
case 'd', 'i', 'o', 'x', 'X':
|
|
i, err := NumberToInt(arg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%%%c format requires integer: %v", c, err)
|
|
}
|
|
switch c {
|
|
case 'd', 'i':
|
|
fmt.Fprintf(buf, "%d", i)
|
|
case 'o':
|
|
fmt.Fprintf(buf, "%o", i)
|
|
case 'x':
|
|
fmt.Fprintf(buf, "%x", i)
|
|
case 'X':
|
|
fmt.Fprintf(buf, "%X", i)
|
|
}
|
|
case 'e', 'f', 'g', 'E', 'F', 'G':
|
|
f, ok := AsFloat(arg)
|
|
if !ok {
|
|
return nil, fmt.Errorf("%%%c format requires float, not %s", c, arg.Type())
|
|
}
|
|
switch c {
|
|
case 'e':
|
|
fmt.Fprintf(buf, "%e", f)
|
|
case 'f':
|
|
fmt.Fprintf(buf, "%f", f)
|
|
case 'g':
|
|
fmt.Fprintf(buf, "%g", f)
|
|
case 'E':
|
|
fmt.Fprintf(buf, "%E", f)
|
|
case 'F':
|
|
fmt.Fprintf(buf, "%F", f)
|
|
case 'G':
|
|
fmt.Fprintf(buf, "%G", f)
|
|
}
|
|
case 'c':
|
|
switch arg := arg.(type) {
|
|
case Int:
|
|
// chr(int)
|
|
r, err := AsInt32(arg)
|
|
if err != nil || r < 0 || r > unicode.MaxRune {
|
|
return nil, fmt.Errorf("%%c format requires a valid Unicode code point, got %s", arg)
|
|
}
|
|
buf.WriteRune(rune(r))
|
|
case String:
|
|
r, size := utf8.DecodeRuneInString(string(arg))
|
|
if size != len(arg) || len(arg) == 0 {
|
|
return nil, fmt.Errorf("%%c format requires a single-character string")
|
|
}
|
|
buf.WriteRune(r)
|
|
default:
|
|
return nil, fmt.Errorf("%%c format requires int or single-character string, not %s", arg.Type())
|
|
}
|
|
case '%':
|
|
buf.WriteByte('%')
|
|
default:
|
|
return nil, fmt.Errorf("unknown conversion %%%c", c)
|
|
}
|
|
format = format[1:]
|
|
index++
|
|
}
|
|
|
|
if index < nargs {
|
|
return nil, fmt.Errorf("too many arguments for format string")
|
|
}
|
|
|
|
return String(buf.String()), nil
|
|
}
|