diff --git a/README.md b/README.md index 55845ca0..2cd030bd 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,9 @@ Please read, enjoy and help improve our documentation! | [quick start guide](docs/quick-start-guide.md) | for mgmt developers | | [frequently asked questions](docs/faq.md) | for everyone | | [general documentation](docs/documentation.md) | for everyone | -| [resource guide](docs/resource-guide.md) | for mgmt developers | | [language guide](docs/language-guide.md) | for everyone | +| [function guide](docs/function-guide.md) | for mgmt developers | +| [resource guide](docs/resource-guide.md) | for mgmt developers | | [style guide](docs/style-guide.md) | for mgmt developers | | [godoc API reference](https://godoc.org/github.com/purpleidea/mgmt) | for mgmt developers | | [prometheus guide](docs/prometheus.md) | for everyone | diff --git a/docs/function-guide.md b/docs/function-guide.md index 5337b102..8461814c 100644 --- a/docs/function-guide.md +++ b/docs/function-guide.md @@ -84,6 +84,104 @@ mgmt engine to shutdown. It should be seen as the equivalent to calling a Ideally, your functions should never need to error. You should never cause a real `panic()`, since this could have negative consequences to the system. +## Simple Polymorphic Function API + +Most functions should be implemented using the simple function API. If they need +to have multiple polymorphic forms under the same name, then you can use this +API. This is useful for situations when it would be unhelpful to name the +functions differently, or when the number of possible signatures for the +function would be infinite. + +The canonical example of this is the `len` function which returns the number of +elements in either a `list` or a `map`. Since lists and maps are two different +types, you can see that polymorphism is more convenient than requiring a +`listlen` and `maplen` function. Nevertheless, it is also required because a +`list of int` is a different type than a `list of str`, which is a different +type than a `list of list of str` and so on. As you can see the number of +possible input types for such a `len` function is infinite. + +Another downside to implementing your functions with this API is that they will +*not* be made available for use inside templates. This is a limitation of the +`golang` template library. In the future if this limitation proves to be +significantly annoying, we might consider writing our own template library. + +As with the simple, non-polymorphic API, you can only implement [pure](https://en.wikipedia.org/wiki/Pure_function) +functions, without writing too much boilerplate code. They will be automatically +re-evaluated as needed when their input values change. + +To implement a function, you'll need to create a file in +[`lang/funcs/simplepoly/`](https://github.com/purpleidea/mgmt/tree/master/lang/funcs/simplepoly/). +The function should be implemented as a list of `FuncValue`'s in our type +system. It is then registered with the engine during `init()`. You may also use +the `variant` type in your type definitions. This special type will never be +seen inside a running program, and will get converted to a concrete type if a +suitable match to this signature can be found. Be warned that signatures which +contain too many variants, or which are very general, might be hard for the +compiler to match, and ambiguous type graphs make for user compiler errors. + +An example explains it best: + +### Example + +```golang +package simplepoly + +import ( + "fmt" + + "github.com/purpleidea/mgmt/lang/types" +) + +func init() { + Register("len", []*types.FuncValue{ + { + T: types.NewType("func([]variant) int"), + V: Len, + }, + { + T: types.NewType("func({variant: variant}) int"), + V: Len, + }, + }) +} + +// Len returns the number of elements in a list or the number of key pairs in a +// map. It can operate on either of these types. +func Len(input []types.Value) (types.Value, error) { + var length int + switch k := input[0].Type().Kind; k { + case types.KindList: + length = len(input[0].List()) + case types.KindMap: + length = len(input[0].Map()) + + default: + return nil, fmt.Errorf("unsupported kind: %+v", k) + } + + return &types.IntValue{ + V: int64(length), + }, nil +} +``` + +This simple polymorphic function can accept an infinite number of signatures, of +which there are two basic forms. Both forms return an `int` as is seen above. +The first form takes a `[]variant` which means a `list` of `variant`'s, which +means that it can be a list of any type, since `variant` itself is not a +concrete type. The second form accepts a `{variant: variant}`, which means that +it accepts any form of `map` as input. + +The implementation for both of these forms is the same: it is handled by the +same `Len` function which is clever enough to be able to deal with any of the +type signatures possible from those two patterns. + +At compile time, if your `mcl` code type checks correctly, a concrete type will +be known for each and every usage of the `len` function, and specific values +will be passed in for this code to compute the length of. As usual, make sure to +only write safe code that will not panic! A panic is a bug. If you really cannot +continue, then you must return an error. + ## Function API To implement a reactive function in `mgmt` it must satisfy the @@ -307,6 +405,27 @@ will likely require a language that can expose a C-like API, such as `python` or There are still many ideas for new functions that haven't been written yet. If you'd like to contribute one, please contact us and tell us about your idea! +### Can I generate many different `FuncValue` implementations from one function? + +Yes, you can use a function generator in `golang` to build multiple different +implementations from the same function generator. You just need to implement a +function which *returns* a `golang` type of `func([]types.Value) (types.Value, error)` +which is what `FuncValue` expects. The generator function can use any input it +wants to build the individual functions, thus helping with code re-use. + +### How do I determine the signature of my simple, polymorphic function? + +The determination of the input portion of the function signature can be +determined by inspecting the length of the input, and the specific type each +value has. Length is done in the standard `golang` way, and the type of each +element can be ascertained with the `Type()` method available on every value. + +Knowing the output type is trickier. If it can not be inferred in some manner, +then the only way is to keep track of this yourself. You can use a function +generator to build your `FuncValue` implementations, and pass in the unique +signature to each one as you are building them. Using a generator is a common +technique which was mentioned previously. + ### Where can I find more information about mgmt? Additional blog posts, videos and other material [is available!](https://github.com/purpleidea/mgmt/blob/master/docs/on-the-web.md). diff --git a/examples/lang/len0.mcl b/examples/lang/len0.mcl new file mode 100644 index 00000000..251ec3b1 --- /dev/null +++ b/examples/lang/len0.mcl @@ -0,0 +1,9 @@ +$x1 = ["a", "b", "c", "d",] +print "print4" { + msg => printf("length is: %d", len($x1)), +} + +$x2 = {"a" => 1, "b" => 2, "c" => 3,} +print "print3" { + msg => printf("length is: %d", len($x2)), +} diff --git a/lang/funcs/operator_polyfunc.go b/lang/funcs/operator_polyfunc.go index f2b4992f..29724968 100644 --- a/lang/funcs/operator_polyfunc.go +++ b/lang/funcs/operator_polyfunc.go @@ -488,7 +488,7 @@ func (obj *OperatorPolyFunc) Polymorphisms(partialType *types.Type, partialValue } } - // since built-in functions have their functions explicitly defined, we + // since built-in functions have their signatures explicitly defined, we // can add easy invariants between in/out args and their expected types. results, err := LookupOperator(op, size) if err != nil { diff --git a/lang/funcs/simplepoly/len_polyfunc.go b/lang/funcs/simplepoly/len_polyfunc.go new file mode 100644 index 00000000..ebdcc65a --- /dev/null +++ b/lang/funcs/simplepoly/len_polyfunc.go @@ -0,0 +1,57 @@ +// Mgmt +// Copyright (C) 2013-2018+ James Shubin and the project contributors +// Written by James Shubin and the project contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package simplepoly // TODO: should this be in its own individual package? + +import ( + "fmt" + + "github.com/purpleidea/mgmt/lang/types" +) + +func init() { + Register("len", []*types.FuncValue{ + { + T: types.NewType("func([]variant) int"), + V: Len, + }, + { + T: types.NewType("func({variant: variant}) int"), + V: Len, + }, + // TODO: should we add support for struct or func lengths? + }) +} + +// Len returns the number of elements in a list or the number of key pairs in a +// map. It can operate on either of these types. +func Len(input []types.Value) (types.Value, error) { + var length int + switch k := input[0].Type().Kind; k { + case types.KindList: + length = len(input[0].List()) + case types.KindMap: + length = len(input[0].Map()) + + default: + return nil, fmt.Errorf("unsupported kind: %+v", k) + } + + return &types.IntValue{ + V: int64(length), + }, nil +} diff --git a/lang/funcs/simplepoly/simplepoly.go b/lang/funcs/simplepoly/simplepoly.go new file mode 100644 index 00000000..9f1ebde3 --- /dev/null +++ b/lang/funcs/simplepoly/simplepoly.go @@ -0,0 +1,287 @@ +// Mgmt +// Copyright (C) 2013-2018+ James Shubin and the project contributors +// Written by James Shubin and the project contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package simplepoly + +import ( + "fmt" + + "github.com/purpleidea/mgmt/lang/funcs" + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/lang/types" + + errwrap "github.com/pkg/errors" +) + +// RegisteredFuncs maps a function name to the corresponding static, pure funcs. +var RegisteredFuncs = make(map[string][]*types.FuncValue) // must initialize + +// Register registers a simple, static, pure, polymorphic function. It is easier +// to use than the raw function API, but also limits you to small, finite +// numbers of different polymorphic type signatures per function name. You can +// also register functions which return types containing variants, if you want +// automatic matching based on partial types as well. Some complex patterns are +// not possible with this API. Implementing a function like `printf` would not +// be possible. Implementing a function which counts the number of elements in a +// list would be. +func Register(name string, fns []*types.FuncValue) { + if _, exists := RegisteredFuncs[name]; exists { + panic(fmt.Sprintf("a simple polyfunc named %s is already registered", name)) + } + + // check for uniqueness in type signatures + typs := []*types.Type{} + for _, f := range fns { + if f.T == nil { + panic(fmt.Sprintf("polyfunc %s contains a nil type signature", name)) + } + typs = append(typs, f.T) + } + + if err := hasDuplicateTypes(typs); err != nil { + panic(fmt.Sprintf("polyfunc %s has a duplicate implementation: %+v", name, err)) + } + + RegisteredFuncs[name] = fns // store a copy for ourselves + + // register a copy in the main function database + funcs.Register(name, func() interfaces.Func { return &simplePolyFunc{Fns: fns} }) +} + +// simplePolyFunc is a scaffolding function struct which fulfills the +// boiler-plate for the function API, but that can run a very simple, static, +// pure, polymorphic function. +type simplePolyFunc struct { + Fns []*types.FuncValue // list of possible functions + + fn *types.FuncValue // the concrete version of our chosen function + + init *interfaces.Init + last types.Value // last value received to use for diff + + result types.Value // last calculated output + + closeChan chan struct{} +} + +// Polymorphisms returns the list of possible function signatures available for +// this static polymorphic function. It relies on type and value hints to limit +// the number of returned possibilities. +func (obj *simplePolyFunc) Polymorphisms(partialType *types.Type, partialValues []types.Value) ([]*types.Type, error) { + if len(obj.Fns) == 0 { + return nil, fmt.Errorf("no matching signatures for simple polyfunc") + } + + // filter out anything that's incompatible with the partialType + typs := []*types.Type{} + for _, f := range obj.Fns { + // TODO: if status is "both", should we skip as too difficult? + _, err := f.T.ComplexCmp(partialType) + // XXX: can an f.T with a variant compare with a partial ? + if err != nil { + continue + } + typs = append(typs, f.T) + } + + return typs, nil +} + +// Build is run to turn the polymorphic, undeterminted function, into the +// specific statically type version. It is usually run after Unify completes, +// and must be run before Info() and any of the other Func interface methods are +// used. +func (obj *simplePolyFunc) Build(typ *types.Type) error { + // typ is the KindFunc signature we're trying to build... + if typ.Out == nil { + return fmt.Errorf("return type of function must be specified") + } + + // find typ in obj.Fns + for ix, f := range obj.Fns { + if f.T.HasVariant() { + continue // match these if no direct matches exist + } + // FIXME: can we replace this by the complex matcher down below? + if f.T.Cmp(typ) == nil { + obj.buildFunction(typ, ix) // found match at this index + return nil + } + } + + // match concrete type against our list that might contain a variant + var found bool + for ix, f := range obj.Fns { + _, err := typ.ComplexCmp(f.T) + if err != nil { + continue + } + if found { // already found one... + // TODO: we *could* check that the previous duplicate is + // equivalent, but in this case, it is really a bug that + // the function author had by allowing ambiguity in this + return fmt.Errorf("duplicate match found for build type: %+v", typ) + } + found = true + obj.buildFunction(typ, ix) // found match at this index + } + // ensure there's only one match... + if found { + return nil // w00t! + } + + return fmt.Errorf("unable to find a compatible function for type: %+v", typ) +} + +// buildFunction builds our concrete static function, from the potentially +// abstract, possibly variant containing list of functions. +func (obj *simplePolyFunc) buildFunction(typ *types.Type, ix int) { + obj.fn = obj.Fns[ix].Copy().(*types.FuncValue) + obj.fn.T = typ.Copy() // overwrites any contained "variant" type +} + +// Validate makes sure we've built our struct properly. It is usually unused for +// normal functions that users can use directly. +func (obj *simplePolyFunc) Validate() error { + if len(obj.Fns) == 0 { + return fmt.Errorf("missing list of functions") + } + + // check for uniqueness in type signatures + typs := []*types.Type{} + for _, f := range obj.Fns { + if f.T == nil { + return fmt.Errorf("nil type signature found") + } + typs = append(typs, f.T) + } + + if err := hasDuplicateTypes(typs); err != nil { + return errwrap.Wrapf(err, "duplicate implementation found") + } + + if obj.fn == nil { // build must be run first + return fmt.Errorf("a specific function has not been specified") + } + if obj.fn.T.Kind != types.KindFunc { + return fmt.Errorf("func must be a kind of func") + } + + return nil +} + +// Info returns some static info about itself. +func (obj *simplePolyFunc) Info() *interfaces.Info { + return &interfaces.Info{ + Pure: true, + Memo: false, // TODO: should this be something we specify here? + Sig: obj.fn.Type(), + Err: obj.Validate(), + } +} + +// Init runs some startup code for this function. +func (obj *simplePolyFunc) Init(init *interfaces.Init) error { + obj.init = init + obj.closeChan = make(chan struct{}) + return nil +} + +// Stream returns the changing values that this func has over time. +func (obj *simplePolyFunc) Stream() error { + defer close(obj.init.Output) // the sender closes + for { + select { + case input, ok := <-obj.init.Input: + if !ok { + if len(obj.fn.Type().Ord) > 0 { + return nil // can't output any more + } + // no inputs were expected, pass through once + } + if ok { + //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 + } + + values := []types.Value{} + for _, name := range obj.fn.Type().Ord { + x := input.Struct()[name] + values = append(values, x) + } + + if obj.init.Debug { + obj.init.Logf("Calling function with: %+v", values) + } + result, err := obj.fn.Call(values) // (Value, error) + if err != nil { + if obj.init.Debug { + obj.init.Logf("Function returned error: %+v", err) + } + return errwrap.Wrapf(err, "simple poly function errored") + } + if obj.init.Debug { + obj.init.Logf("Function returned with: %+v", values) + } + + if obj.result == result { + continue // result didn't change + } + obj.result = result // store new result + + case <-obj.closeChan: + return nil + } + + select { + case obj.init.Output <- obj.result: // send + if len(obj.fn.Type().Ord) == 0 { + return nil // no more values, we're a pure func + } + case <-obj.closeChan: + return nil + } + } +} + +// Close runs some shutdown code for this function and turns off the stream. +func (obj *simplePolyFunc) Close() error { + close(obj.closeChan) + return nil +} + +// hasDuplicateTypes returns an error if the list of types is not unique. +func hasDuplicateTypes(typs []*types.Type) error { + // FIXME: do this comparison in < O(n^2) ? + for i, ti := range typs { + for j, tj := range typs { + if i == j { + continue // don't compare to self + } + if ti.Cmp(tj) == nil { + return fmt.Errorf("duplicate type of %+v found", ti) + } + } + } + return nil +} diff --git a/lang/lang.go b/lang/lang.go index c055c975..de897203 100644 --- a/lang/lang.go +++ b/lang/lang.go @@ -27,6 +27,7 @@ import ( _ "github.com/purpleidea/mgmt/lang/funcs/core" // import so the funcs register _ "github.com/purpleidea/mgmt/lang/funcs/facts/core" // import so the facts register _ "github.com/purpleidea/mgmt/lang/funcs/simple" // import so the funcs register + _ "github.com/purpleidea/mgmt/lang/funcs/simplepoly" // import so the funcs register "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/unification" "github.com/purpleidea/mgmt/pgraph" diff --git a/lang/types/type.go b/lang/types/type.go index df1fd460..89b6e75c 100644 --- a/lang/types/type.go +++ b/lang/types/type.go @@ -371,10 +371,21 @@ func NewType(s string) *Type { var key string // arg naming code, which allows for optional arg names - sep := strings.Index(s, " ") - if sep > 0 && !strings.HasSuffix(s[:sep], ",") { - key = s[:sep] // FIXME: check there are no special chars in key - s = s[sep+1:] // what's next + for i, c := range s { // looking for an arg name + if c == ',' { // there was no arg name + break + } + if c == '{' || c == '(' { // not an arg name + break + } + if c == '}' || c == ')' { // unexpected format? + return nil + } + if c == ' ' { // done + key = s[:i] // found a key? + s = s[i+1:] // what's next + break + } } // just name the keys 0, 1, 2, N... @@ -883,3 +894,328 @@ func (obj *Type) HasVariant() bool { panic("malformed type") } + +// ComplexCmp tells us if the input type is compatible with the concrete one. It +// can match against types containing variants, or against partial types. If the +// two types are equivalent, it will return nil. If the input type is identical, +// and concrete, the return status will be the empty string. If this match finds +// a possibility against a partial type, the status will be set to the "partial" +// string, and if it is compatible with the variant type it will be "variant"... +// Comparing to a partial can only match "impossible" (error) or possible (nil). +func (obj *Type) ComplexCmp(typ *Type) (string, error) { + // match simple "placeholder" variants... skip variants w/ sub types + isVariant := func(t *Type) bool { return t.Kind == KindVariant && t.Var == nil } + + if obj == nil { + return "", fmt.Errorf("can't cmp from a nil type") + } + // XXX: can we relax this to allow variants matching against partials? + if obj.HasVariant() { + return "", fmt.Errorf("only input can contain variants") + } + + if typ == nil { // match + return "partial", nil // compatible :) + } + if isVariant(typ) { // match + return "variant", nil // compatible :) + } + + if obj.Kind != typ.Kind { + return "", fmt.Errorf("base kind does not match (%+v != %+v)", obj.Kind, typ.Kind) + } + + // only container types are left to match... + switch obj.Kind { + case KindBool: + return "", nil // regular cmp + case KindStr: + return "", nil + case KindInt: + return "", nil + case KindFloat: + return "", nil + + case KindList: + if obj.Val == nil { + panic("malformed list type") + } + if typ.Val == nil { + return "partial", nil + } + + return obj.Val.ComplexCmp(typ.Val) + + case KindMap: + if obj.Key == nil || obj.Val == nil { + panic("malformed map type") + } + + if typ.Key == nil && typ.Val == nil { + return "partial", nil + } + if typ.Key == nil { + return obj.Val.ComplexCmp(typ.Val) + } + if typ.Val == nil { + return obj.Key.ComplexCmp(typ.Key) + } + + kstatus, kerr := obj.Key.ComplexCmp(typ.Key) + vstatus, verr := obj.Val.ComplexCmp(typ.Val) + if kerr != nil && verr != nil { + return "", multierr.Append(kerr, verr) // two errors + } + if kerr != nil { + return "", kerr + } + if verr != nil { + return "", verr + } + + if kstatus == "" && vstatus == "" { + return "", nil + } else if kstatus != "" && vstatus == "" { + return kstatus, nil + } else if vstatus != "" && kstatus == "" { + return vstatus, nil + } + + // optimization, redundant + //if kstatus == vstatus { // both partial or both variant... + // return kstatus, nil + //} + + var isVariant, isPartial bool + if kstatus == "variant" || vstatus == "variant" { + isVariant = true + } + if kstatus == "partial" || vstatus == "partial" { + isPartial = true + } + if kstatus == "both" || vstatus == "both" { + isVariant = true + isPartial = true + } + + if !isVariant && !isPartial { + return "", nil + } + if isVariant { + return "variant", nil + } + if isPartial { + return "partial", nil + } + + //return "", fmt.Errorf("matches as both partial and variant") + return "both", nil + + case KindStruct: // {a bool; b int} + if obj.Map == nil { + panic("malformed struct type") + } + if typ.Map == nil { + return "partial", nil + } + + if len(obj.Ord) != len(typ.Ord) { + return "", fmt.Errorf("struct field count differs") + } + for i, k := range obj.Ord { + if k != typ.Ord[i] { + return "", fmt.Errorf("struct fields differ") + } + } + var isVariant, isPartial bool + for _, k := range obj.Ord { // loop map in deterministic order + t1, ok := obj.Map[k] + if !ok { + panic("malformed struct order") + } + t2, ok := typ.Map[k] + if !ok { + panic("malformed struct order") + } + if t1 == nil { + panic("malformed struct field") + } + if t2 == nil { + isPartial = true + continue + } + + status, err := t1.ComplexCmp(t2) + if err != nil { + return "", err + } + if status == "variant" { + isVariant = true + } + if status == "partial" { + isPartial = true + } + if status == "both" { + isVariant = true + isPartial = true + } + } + + if !isVariant && !isPartial { + return "", nil + } + if isVariant { + return "variant", nil + } + if isPartial { + return "partial", nil + } + + //return "", fmt.Errorf("matches as both partial and variant") + return "both", nil + + case KindFunc: + if obj.Map == nil { + panic("malformed func type") + } + if typ.Map == nil { + return "partial", nil + } + + if len(obj.Ord) != len(typ.Ord) { + return "", fmt.Errorf("func arg count differs") + } + + // needed for strict cmp only... + //for i, k := range obj.Ord { + // if k != typ.Ord[i] { + // return "", fmt.Errorf("func arg differs") + // } + //} + //var isVariant, isPartial bool + //for _, k := range obj.Ord { // loop map in deterministic order + // t1, ok := obj.Map[k] + // if !ok { + // panic("malformed func order") + // } + // t2, ok := typ.Map[k] + // if !ok { + // panic("malformed func order") + // } + // if t1 == nil { + // panic("malformed func arg") + // } + // if t2 == nil { + // isPartial = true + // continue + // } + // + // status, err := t1.ComplexCmp(t2) + // if err != nil { + // return "", err + // } + // if status == "variant" { + // isVariant = true + // } + // if status == "partial" { + // isPartial = true + // } + // if status == "both" { + // isVariant = true + // isPartial = true + // } + //} + // + //if !isVariant && !isPartial { + // return "", nil + //} + //if isVariant { + // return "variant", nil + //} + //if isPartial { + // return "partial", nil + //} + // + ////return "", fmt.Errorf("matches as both partial and variant") + //return "both", nil + + // if we're not comparing arg names, get the two lists of types + var isVariant, isPartial bool + for i := 0; i < len(obj.Ord); i++ { + t1, ok := obj.Map[obj.Ord[i]] + if !ok { + panic("malformed func order") + } + if t1 == nil { + panic("malformed func arg") + } + + t2, ok := typ.Map[typ.Ord[i]] + if !ok { + panic("malformed func order") + } + if t2 == nil { + isPartial = true + continue + } + + status, err := t1.ComplexCmp(t2) + if err != nil { + return "", err + } + if status == "variant" { + isVariant = true + } + if status == "partial" { + isPartial = true + } + if status == "both" { + isVariant = true + isPartial = true + } + } + + //if obj.Out != nil && typ.Out != nil { // let a nil obj.Out in + if typ.Out != nil { // let a nil obj.Out in + status, err := obj.Out.ComplexCmp(typ.Out) + if err != nil { + return "", err + } + if status == "variant" { + isVariant = true + } + if status == "partial" { + isPartial = true + } + if status == "both" { + isVariant = true + isPartial = true + } + + } else if obj.Out != nil { + // TODO: technically, typ.Out could be unspecified, then + // this is a Cmp fail, not an isPartial = true, but then + // we'd have to support functions without a return value + // since we are functional, it is not a major problem... + isPartial = true + } + //} else if typ.Out != nil { // solve this in the above ComplexCmp instead! + // return "", fmt.Errorf("can't cmp from a nil type") + //} + + if !isVariant && !isPartial { + return "", nil + } + if isVariant { + return "variant", nil + } + if isPartial { + return "partial", nil + } + + //return "", fmt.Errorf("matches as both partial and variant") + return "both", nil + } + + return "", fmt.Errorf("unknown kind: %+v", obj.Kind) +} diff --git a/lang/types/type_test.go b/lang/types/type_test.go index 4b7a21f5..6758c5b4 100644 --- a/lang/types/type_test.go +++ b/lang/types/type_test.go @@ -139,6 +139,33 @@ func TestType1(t *testing.T) { Kind: KindInt, }, }, + "{str: variant}": { + Kind: KindMap, + Key: &Type{ + Kind: KindStr, + }, + Val: &Type{ + Kind: KindVariant, + }, + }, + "{variant: int}": { + Kind: KindMap, + Key: &Type{ + Kind: KindVariant, + }, + Val: &Type{ + Kind: KindInt, + }, + }, + "{variant: variant}": { + Kind: KindMap, + Key: &Type{ + Kind: KindVariant, + }, + Val: &Type{ + Kind: KindVariant, + }, + }, // nested maps "{str: {int: bool}}": { @@ -529,6 +556,110 @@ func TestType1(t *testing.T) { Kind: KindBool, }, }, + "func({str: int}) bool": { + Kind: KindFunc, + // key names are arbitrary... + Map: map[string]*Type{ + "answer": { + Kind: KindMap, + Key: &Type{ + Kind: KindStr, + }, + Val: &Type{ + Kind: KindInt, + }, + }, + }, + Ord: []string{ + "answer", + }, + Out: &Type{ + Kind: KindBool, + }, + }, + "func(bool, {str: int}) bool": { + Kind: KindFunc, + // key names are arbitrary... + Map: map[string]*Type{ + "hello": { + Kind: KindBool, + }, + "answer": { + Kind: KindMap, + Key: &Type{ + Kind: KindStr, + }, + Val: &Type{ + Kind: KindInt, + }, + }, + }, + Ord: []string{ + "hello", + "answer", + }, + Out: &Type{ + Kind: KindBool, + }, + }, + "func(struct{a str; bb int}) bool": { + Kind: KindFunc, + // key names are arbitrary... + Map: map[string]*Type{ + "answer": { + Kind: KindStruct, + Ord: []string{ + "a", + "bb", + }, + Map: map[string]*Type{ + "a": { + Kind: KindStr, + }, + "bb": { + Kind: KindInt, + }, + }, + }, + }, + Ord: []string{ + "answer", + }, + Out: &Type{ + Kind: KindBool, + }, + }, + "func(bool, struct{a str; bb int}) bool": { + Kind: KindFunc, + // key names are arbitrary... + Map: map[string]*Type{ + "hello": { + Kind: KindBool, + }, + "answer": { + Kind: KindStruct, + Ord: []string{ + "a", + "bb", + }, + Map: map[string]*Type{ + "a": { + Kind: KindStr, + }, + "bb": { + Kind: KindInt, + }, + }, + }, + }, + Ord: []string{ + "hello", + "answer", + }, + Out: &Type{ + Kind: KindBool, + }, + }, } for str, val := range values { // run all the tests @@ -1076,6 +1207,21 @@ func TestType3(t *testing.T) { Kind: KindBool, }, }, + "func(a str) bool": { + Kind: KindFunc, + // key names are arbitrary... + Map: map[string]*Type{ + "a": { + Kind: KindStr, + }, + }, + Ord: []string{ + "a", // must match + }, + Out: &Type{ + Kind: KindBool, + }, + }, "func(aaa str, bb int) bool": { Kind: KindFunc, // key names are arbitrary... @@ -1095,6 +1241,27 @@ func TestType3(t *testing.T) { Kind: KindBool, }, }, + "func(aaa {str: int}) bool": { + Kind: KindFunc, + // key names are arbitrary... + Map: map[string]*Type{ + "aaa": { + Kind: KindMap, + Key: &Type{ + Kind: KindStr, + }, + Val: &Type{ + Kind: KindInt, + }, + }, + }, + Ord: []string{ + "aaa", + }, + Out: &Type{ + Kind: KindBool, + }, + }, } for str, val := range values { // run all the tests