go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/starlark/builtins/native.go (about) 1 // Copyright 2020 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package builtins 16 17 import ( 18 "fmt" 19 20 "go.starlark.net/starlark" 21 ) 22 23 // ToGoNative takes a Starlark value and returns native Go value for it. 24 // 25 // E.g. it takes *starlark.Dict and returns map[string]any. Works 26 // recursively. Supports only built-in Starlark types. 27 func ToGoNative(v starlark.Value) (any, error) { 28 return toGoNative(v, visitingSet{}) 29 } 30 31 // visitingSet is a set of containers we currently have recursed into. 32 // 33 // Used to detect cycles. Note that it is not a stack because we prefer O(1) 34 // lookup and we know there can't be duplicates in it (so 'remove' is not 35 // ambiguous). 36 type visitingSet map[any]struct{} 37 38 func (v visitingSet) add(container any) error { 39 if _, haveIt := v[container]; haveIt { 40 return fmt.Errorf("detected recursion in the data structure") 41 } 42 v[container] = struct{}{} 43 return nil 44 } 45 46 func (v visitingSet) remove(container any) { 47 delete(v, container) 48 } 49 50 // toGoNative implements ToGoNative. 51 // 52 // Uses given 'visiting' set to detect cycles in the value being converted, to 53 // avoid stack overflows due to unbounded recursion. 54 func toGoNative(v starlark.Value, visiting visitingSet) (any, error) { 55 // Add containers to 'visiting' set right away. Note that Tuples are special, 56 // since they are not hashable (being a slice). We add a pointer instead. 57 var container any 58 switch val := v.(type) { 59 case starlark.Tuple: 60 container = &val 61 case *starlark.List, *starlark.Dict, *starlark.Set: 62 container = val 63 } 64 if container != nil { 65 if err := visiting.add(container); err != nil { 66 return nil, err 67 } 68 defer visiting.remove(container) 69 } 70 71 switch val := v.(type) { 72 case starlark.NoneType: 73 return nil, nil 74 case starlark.Bool: 75 return bool(val), nil 76 case starlark.Float: 77 return float64(val), nil 78 case starlark.String: 79 return string(val), nil 80 case starlark.Int: 81 i, ok := val.Int64() 82 if !ok { 83 return nil, fmt.Errorf("can't convert %q to int64", val.String()) 84 } 85 return i, nil 86 case *starlark.Dict: 87 pairs := val.Items() 88 out := make(map[string]any, len(pairs)) 89 for _, pair := range pairs { 90 if len(pair) != 2 { 91 panic("impossible") 92 } 93 key, ok := pair[0].(starlark.String) 94 if !ok { 95 return nil, fmt.Errorf("dict keys should be strings, got %s", pair[0].Type()) 96 } 97 val, err := toGoNative(pair[1], visiting) 98 if err != nil { 99 return nil, err 100 } 101 out[string(key)] = val 102 } 103 return out, nil 104 } 105 106 // This covers *List, Tuple and *Set. 107 if iterable, ok := v.(starlark.Iterable); ok { 108 iter := iterable.Iterate() 109 defer iter.Done() 110 out := []any{} 111 var val starlark.Value 112 for iter.Next(&val) { 113 native, err := toGoNative(val, visiting) 114 if err != nil { 115 return nil, err 116 } 117 out = append(out, native) 118 } 119 return out, nil 120 } 121 122 return nil, fmt.Errorf("unsupported type %s", v.Type()) 123 }