From d579787bcd8624bdda52d2fad5281db3e6e8075e Mon Sep 17 00:00:00 2001 From: James Shubin Date: Wed, 26 Feb 2025 19:22:49 -0500 Subject: [PATCH] lang: core: Simplify list and map lookup functions --- examples/lang/lookup.mcl | 22 +++++ lang/core/list_lookup.go | 196 +++++---------------------------------- lang/core/lookup.go | 24 ++++- lang/core/map_lookup.go | 188 +++---------------------------------- 4 files changed, 77 insertions(+), 353 deletions(-) create mode 100644 examples/lang/lookup.mcl diff --git a/examples/lang/lookup.mcl b/examples/lang/lookup.mcl new file mode 100644 index 00000000..c4ec4c63 --- /dev/null +++ b/examples/lang/lookup.mcl @@ -0,0 +1,22 @@ +import "fmt" + +$some_list = ["l", "m", "n",] + +$some_map = { + "ottawa" => 6, + "toronto" => 7, + "montreal" => 8, + "vancouver" => 9, +} + +print "letter" { + msg => fmt.printf("letter: %s", $some_list[1]), + + Meta:autogroup => false, +} + +print "city" { + msg => fmt.printf("city: %d", $some_map["montreal"]), + + Meta:autogroup => false, +} diff --git a/lang/core/list_lookup.go b/lang/core/list_lookup.go index 7baf33fb..bcd70948 100644 --- a/lang/core/list_lookup.go +++ b/lang/core/list_lookup.go @@ -34,191 +34,39 @@ import ( "fmt" "math" - "github.com/purpleidea/mgmt/lang/funcs" - "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/lang/funcs/simple" "github.com/purpleidea/mgmt/lang/types" - "github.com/purpleidea/mgmt/util/errwrap" ) const ( // ListLookupFuncName is the name this function is registered as. ListLookupFuncName = "list_lookup" - - // arg names... - listLookupArgNameList = "list" - listLookupArgNameIndex = "index" ) func init() { - funcs.Register(ListLookupFuncName, func() interfaces.Func { return &ListLookupFunc{} }) // must register the func and name + simple.Register(ListLookupFuncName, &simple.Scaffold{ + T: types.NewType("func(list []?1, index int) ?1"), + F: ListLookup, + }) } -var _ interfaces.BuildableFunc = &ListLookupFunc{} // ensure it meets this expectation +// ListLookup returns the value corresponding to the input index in the list. +func ListLookup(ctx context.Context, input []types.Value) (types.Value, error) { + l := input[0].(*types.ListValue) + index := input[1].Int() + zero := l.Type().Val.New() // the zero value -// ListLookupFunc is a list index lookup function. If you provide a negative -// index, then it will return the zero value for that type. -type ListLookupFunc struct { - Type *types.Type // Kind == List, that is used as the list we lookup in + // TODO: should we handle overflow by returning zero? + if index > math.MaxInt { // max int size varies by arch + return nil, fmt.Errorf("list index overflow, got: %d, max is: %d", index, math.MaxInt) + } + if index < 0 { // lists can't have negative indexes (for now) + return nil, fmt.Errorf("list index negative, got: %d", index) + } - init *interfaces.Init - last types.Value // last value received to use for diff - - result types.Value // last calculated output -} - -// String returns a simple name for this function. This is needed so this struct -// can satisfy the pgraph.Vertex interface. -func (obj *ListLookupFunc) String() string { - return ListLookupFuncName -} - -// ArgGen returns the Nth arg name for this function. -func (obj *ListLookupFunc) ArgGen(index int) (string, error) { - seq := []string{listLookupArgNameList, listLookupArgNameIndex} - if l := len(seq); index >= l { - return "", fmt.Errorf("index %d exceeds arg length of %d", index, l) - } - return seq[index], nil -} - -// helper -func (obj *ListLookupFunc) sig() *types.Type { - // func(list []?1, index int, default ?1) ?1 - v := "?1" - if obj.Type != nil { // don't panic if called speculatively - v = obj.Type.Val.String() - } - return types.NewType(fmt.Sprintf( - "func(%s []%s, %s int) %s", - listLookupArgNameList, v, - listLookupArgNameIndex, - v, - )) -} - -// Build is run to turn the polymorphic, undetermined function, into the -// specific statically typed version. It is usually run after Unify completes, -// and must be run before Info() and any of the other Func interface methods are -// used. This function is idempotent, as long as the arg isn't changed between -// runs. -func (obj *ListLookupFunc) Build(typ *types.Type) (*types.Type, error) { - // typ is the KindFunc signature we're trying to build... - if typ.Kind != types.KindFunc { - return nil, fmt.Errorf("input type must be of kind func") - } - - if len(typ.Ord) != 2 { - return nil, fmt.Errorf("the listlookup function needs exactly two args") - } - if typ.Out == nil { - return nil, fmt.Errorf("return type of function must be specified") - } - if typ.Map == nil { - return nil, fmt.Errorf("invalid input type") - } - - tList, exists := typ.Map[typ.Ord[0]] - if !exists || tList == nil { - return nil, fmt.Errorf("first arg must be specified") - } - - tIndex, exists := typ.Map[typ.Ord[1]] - if !exists || tIndex == nil { - return nil, fmt.Errorf("second arg must be specified") - } - - if tIndex != nil && tIndex.Kind != types.KindInt { - return nil, fmt.Errorf("index must be int kind") - } - - if err := tList.Val.Cmp(typ.Out); err != nil { - return nil, errwrap.Wrapf(err, "return type must match list val type") - } - - obj.Type = tList // list type - return obj.sig(), nil -} - -// Validate tells us if the input struct takes a valid form. -func (obj *ListLookupFunc) Validate() error { - if obj.Type == nil { // build must be run first - return fmt.Errorf("type is still unspecified") - } - if obj.Type.Kind != types.KindList { - return fmt.Errorf("type must be a kind of list") - } - return nil -} - -// Info returns some static info about itself. Build must be called before this -// will return correct data. -func (obj *ListLookupFunc) Info() *interfaces.Info { - return &interfaces.Info{ - Pure: true, - Memo: false, - Sig: obj.sig(), // helper - Err: obj.Validate(), - } -} - -// Init runs some startup code for this function. -func (obj *ListLookupFunc) Init(init *interfaces.Init) error { - obj.init = init - return nil -} - -// Stream returns the changing values that this func has over time. -func (obj *ListLookupFunc) Stream(ctx context.Context) error { - defer close(obj.init.Output) // the sender closes - for { - select { - case input, ok := <-obj.init.Input: - if !ok { - return nil // can't output any more - } - //if err := input.Type().Cmp(obj.Info().Sig.Input); err != nil { - // return errwrap.Wrapf(err, "wrong function input") - //} - - if obj.last != nil && input.Cmp(obj.last) == nil { - continue // value didn't change, skip it - } - obj.last = input // store for next - - l := (input.Struct()[listLookupArgNameList]).(*types.ListValue) - index := input.Struct()[listLookupArgNameIndex].Int() - zero := l.Type().Val.New() // the zero value - - // TODO: should we handle overflow by returning zero? - if index > math.MaxInt { // max int size varies by arch - return fmt.Errorf("list index overflow, got: %d, max is: %d", index, math.MaxInt) - } - - // negative index values are "not found" here! - var result types.Value - val, exists := l.Lookup(int(index)) - if exists { - result = val - } else { - result = zero - } - - // if previous input was `2 + 4`, but now it - // changed to `1 + 5`, the result is still the - // same, so we can skip sending an update... - if obj.result != nil && result.Cmp(obj.result) == nil { - continue // result didn't change - } - obj.result = result // store new result - - case <-ctx.Done(): - return nil - } - - select { - case obj.init.Output <- obj.result: // send - case <-ctx.Done(): - return nil - } - } + val, exists := l.Lookup(int(index)) + if !exists { + return zero, nil + } + return val, nil } diff --git a/lang/core/lookup.go b/lang/core/lookup.go index 523caf9b..355ad721 100644 --- a/lang/core/lookup.go +++ b/lang/core/lookup.go @@ -163,16 +163,30 @@ func (obj *LookupFunc) Build(typ *types.Type) (*types.Type, error) { return nil, fmt.Errorf("first arg must have a type") } + name := "" if tListOrMap.Kind == types.KindList { - obj.fn = &ListLookupFunc{} // set it - return obj.fn.Build(typ) + name = ListLookupFuncName } if tListOrMap.Kind == types.KindMap { - obj.fn = &MapLookupFunc{} // set it - return obj.fn.Build(typ) + name = MapLookupFuncName + } + if name == "" { + return nil, fmt.Errorf("we must lookup from either a list or a map") } - return nil, fmt.Errorf("we must lookup from either a list or a map") + f, err := funcs.Lookup(name) + if err != nil { + // programming error + return nil, err + } + bf, ok := f.(interfaces.BuildableFunc) + if !ok { + // programming error + return nil, fmt.Errorf("not a BuildableFunc") + } + obj.fn = bf + + return obj.fn.Build(typ) } // Validate tells us if the input struct takes a valid form. diff --git a/lang/core/map_lookup.go b/lang/core/map_lookup.go index 3ba65a59..99933ad1 100644 --- a/lang/core/map_lookup.go +++ b/lang/core/map_lookup.go @@ -31,191 +31,31 @@ package core import ( "context" - "fmt" - "github.com/purpleidea/mgmt/lang/funcs" - "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/lang/funcs/simple" "github.com/purpleidea/mgmt/lang/types" - "github.com/purpleidea/mgmt/util/errwrap" ) const ( // MapLookupFuncName is the name this function is registered as. MapLookupFuncName = "map_lookup" - - // arg names... - mapLookupArgNameMap = "map" - mapLookupArgNameKey = "key" ) func init() { - funcs.Register(MapLookupFuncName, func() interfaces.Func { return &MapLookupFunc{} }) // must register the func and name + simple.Register(MapLookupFuncName, &simple.Scaffold{ + T: types.NewType("func(map map{?1: ?2}, key ?1) ?2"), + F: MapLookup, + }) } -var _ interfaces.BuildableFunc = &MapLookupFunc{} // ensure it meets this expectation +// MapLookup returns the value corresponding to the input key in the map. +func MapLookup(ctx context.Context, input []types.Value) (types.Value, error) { + m := input[0].(*types.MapValue) + zero := m.Type().Val.New() // the zero value -// MapLookupFunc is a key map lookup function. If you provide a missing key, -// then it will return the zero value for that type. -type MapLookupFunc struct { - Type *types.Type // Kind == Map, that is used as the map we lookup - - init *interfaces.Init - last types.Value // last value received to use for diff - - result types.Value // last calculated output -} - -// String returns a simple name for this function. This is needed so this struct -// can satisfy the pgraph.Vertex interface. -func (obj *MapLookupFunc) String() string { - return MapLookupFuncName -} - -// ArgGen returns the Nth arg name for this function. -func (obj *MapLookupFunc) ArgGen(index int) (string, error) { - seq := []string{mapLookupArgNameMap, mapLookupArgNameKey} - if l := len(seq); index >= l { - return "", fmt.Errorf("index %d exceeds arg length of %d", index, l) - } - return seq[index], nil -} - -// helper -func (obj *MapLookupFunc) sig() *types.Type { - // func(map map{?1: ?2}, key ?1) ?2 - k := "?1" - v := "?2" - m := fmt.Sprintf("map{%s: %s}", k, v) - if obj.Type != nil { // don't panic if called speculatively - k = obj.Type.Key.String() - v = obj.Type.Val.String() - m = obj.Type.String() - } - return types.NewType(fmt.Sprintf( - "func(%s %s, %s %s) %s", - mapLookupArgNameMap, m, - mapLookupArgNameKey, k, - v, - )) -} - -// Build is run to turn the polymorphic, undetermined function, into the -// specific statically typed version. It is usually run after Unify completes, -// and must be run before Info() and any of the other Func interface methods are -// used. This function is idempotent, as long as the arg isn't changed between -// runs. -func (obj *MapLookupFunc) Build(typ *types.Type) (*types.Type, error) { - // typ is the KindFunc signature we're trying to build... - if typ.Kind != types.KindFunc { - return nil, fmt.Errorf("input type must be of kind func") - } - - if len(typ.Ord) != 2 { - return nil, fmt.Errorf("the maplookup function needs exactly two args") - } - if typ.Out == nil { - return nil, fmt.Errorf("return type of function must be specified") - } - if typ.Map == nil { - return nil, fmt.Errorf("invalid input type") - } - - tMap, exists := typ.Map[typ.Ord[0]] - if !exists || tMap == nil { - return nil, fmt.Errorf("first arg must be specified") - } - - tKey, exists := typ.Map[typ.Ord[1]] - if !exists || tKey == nil { - return nil, fmt.Errorf("second arg must be specified") - } - - if err := tMap.Key.Cmp(tKey); err != nil { - return nil, errwrap.Wrapf(err, "key must match map key type") - } - - if err := tMap.Val.Cmp(typ.Out); err != nil { - return nil, errwrap.Wrapf(err, "return type must match map val type") - } - - obj.Type = tMap // map type - return obj.sig(), nil -} - -// Validate tells us if the input struct takes a valid form. -func (obj *MapLookupFunc) Validate() error { - if obj.Type == nil { // build must be run first - return fmt.Errorf("type is still unspecified") - } - if obj.Type.Kind != types.KindMap { - return fmt.Errorf("type must be a kind of map") - } - return nil -} - -// Info returns some static info about itself. Build must be called before this -// will return correct data. -func (obj *MapLookupFunc) Info() *interfaces.Info { - return &interfaces.Info{ - Pure: true, - Memo: false, - Sig: obj.sig(), // helper - Err: obj.Validate(), - } -} - -// Init runs some startup code for this function. -func (obj *MapLookupFunc) Init(init *interfaces.Init) error { - obj.init = init - return nil -} - -// Stream returns the changing values that this func has over time. -func (obj *MapLookupFunc) Stream(ctx context.Context) error { - defer close(obj.init.Output) // the sender closes - for { - select { - case input, ok := <-obj.init.Input: - if !ok { - return nil // can't output any more - } - //if err := input.Type().Cmp(obj.Info().Sig.Input); err != nil { - // return errwrap.Wrapf(err, "wrong function input") - //} - - if obj.last != nil && input.Cmp(obj.last) == nil { - continue // value didn't change, skip it - } - obj.last = input // store for next - - m := (input.Struct()[mapLookupArgNameMap]).(*types.MapValue) - key := input.Struct()[mapLookupArgNameKey] - zero := m.Type().Val.New() // the zero value - - var result types.Value - val, exists := m.Lookup(key) - if exists { - result = val - } else { - result = zero - } - - // if previous input was `2 + 4`, but now it - // changed to `1 + 5`, the result is still the - // same, so we can skip sending an update... - if obj.result != nil && result.Cmp(obj.result) == nil { - continue // result didn't change - } - obj.result = result // store new result - - case <-ctx.Done(): - return nil - } - - select { - case obj.init.Output <- obj.result: // send - case <-ctx.Done(): - return nil - } - } + val, exists := m.Lookup(input[1]) + if !exists { + return zero, nil + } + return val, nil }