diff --git a/lang/funcs/core/iter/map_func.go b/lang/funcs/core/iter/map_func.go index 69650cf3..3bbebf8c 100644 --- a/lang/funcs/core/iter/map_func.go +++ b/lang/funcs/core/iter/map_func.go @@ -23,6 +23,7 @@ import ( "github.com/purpleidea/mgmt/lang/funcs" "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/types" + "github.com/purpleidea/mgmt/util" "github.com/purpleidea/mgmt/util/errwrap" ) @@ -32,14 +33,18 @@ func init() { } const ( - argNameFunction = "function" argNameInputs = "inputs" + argNameFunction = "function" ) // MapFunc is the standard map iterator function that applies a function to each // element in a list. It returns a list with the same number of elements as the // input list. There is no requirement that the element output type be the same -// as the input element type. +// as the input element type. This implements the signature: +// `func(inputs []T1, function func(T1) T2) []T2` instead of the alternate with +// the two input args swapped, because while the latter is more common with +// languages that support partial function application, the former variant that +// we implemented is much more readable when using an inline lambda. // TODO: should we extend this to support iterating over map's and structs, or // should that be a different function? I think a different function is best. type MapFunc struct { @@ -49,8 +54,8 @@ type MapFunc struct { init *interfaces.Init last types.Value // last value received to use for diff - function func([]types.Value) (types.Value, error) inputs types.Value + function func([]types.Value) (types.Value, error) result types.Value // last calculated output @@ -59,17 +64,291 @@ type MapFunc struct { // ArgGen returns the Nth arg name for this function. func (obj *MapFunc) ArgGen(index int) (string, error) { - seq := []string{argNameFunction, argNameInputs} + seq := []string{argNameInputs, argNameFunction} // inverted for pretty! if l := len(seq); index >= l { return "", fmt.Errorf("index %d exceeds arg length of %d", index, l) } return seq[index], nil } +// Unify returns the list of invariants that this func produces. +func (obj *MapFunc) Unify(expr interfaces.Expr) ([]interfaces.Invariant, error) { + var invariants []interfaces.Invariant + var invar interfaces.Invariant + + // func(inputs []T1, function func(T1) T2) []T2 + + inputsName, err := obj.ArgGen(0) + if err != nil { + return nil, err + } + functionName, err := obj.ArgGen(1) + if err != nil { + return nil, err + } + + dummyArgList := &interfaces.ExprAny{} // corresponds to the input list + dummyArgFunc := &interfaces.ExprAny{} // corresponds to the input func + dummyOutList := &interfaces.ExprAny{} // corresponds to the output list + + // full function + mapped := make(map[string]interfaces.Expr) + ordered := []string{inputsName, functionName} + mapped[inputsName] = dummyArgList + mapped[functionName] = dummyArgFunc + + invar = &interfaces.EqualityWrapFuncInvariant{ + Expr1: expr, // maps directly to us! + Expr2Map: mapped, + Expr2Ord: ordered, + Expr2Out: dummyOutList, + } + invariants = append(invariants, invar) + + // generator function + fn := func(fnInvariants []interfaces.Invariant, solved map[interfaces.Expr]*types.Type) ([]interfaces.Invariant, error) { + for _, invariant := range fnInvariants { + // search for this special type of invariant + cfavInvar, ok := invariant.(*interfaces.CallFuncArgsValueInvariant) + if !ok { + continue + } + // did we find the mapping from us to ExprCall ? + if cfavInvar.Func != expr { + continue + } + // cfavInvar.Expr is the ExprCall! (the return pointer) + // cfavInvar.Args are the args that ExprCall uses! + if l := len(cfavInvar.Args); l != 2 { + return nil, fmt.Errorf("unable to build function with %d args", l) + } + // we must have exactly two args + + var invariants []interfaces.Invariant + var invar interfaces.Invariant + + t1Expr := &interfaces.ExprAny{} // corresponds to the t1 type + t2Expr := &interfaces.ExprAny{} // corresponds to the t2 type + + // add the relationship to the returned value + invar = &interfaces.EqualityInvariant{ + Expr1: cfavInvar.Expr, + Expr2: dummyOutList, + } + invariants = append(invariants, invar) + + // add the relationships to the called args + invar = &interfaces.EqualityInvariant{ + Expr1: cfavInvar.Args[0], + Expr2: dummyArgList, + } + invariants = append(invariants, invar) + + invar = &interfaces.EqualityInvariant{ + Expr1: cfavInvar.Args[1], + Expr2: dummyArgFunc, + } + invariants = append(invariants, invar) + + var t1, t2 *types.Type // as seen in our sig's + var foundArgName string = util.NumToAlpha(0) // XXX: is this a hack? + + // validateArg0 checks: inputs []T1 + validateArg0 := func(typ *types.Type) error { + if typ == nil { // unknown so far + return nil + } + if typ.Kind != types.KindList { + return fmt.Errorf("input type must be of kind list") + } + if typ.Val == nil { // TODO: is this okay to add? + return nil // unknown so far + } + if t1 == nil { // t1 is not yet known, so done! + t1 = typ.Val // learn! + return nil + } + //if err := typ.Val.Cmp(t1); err != nil { + // return errwrap.Wrapf(err, "input type was inconsistent") + //} + //return nil + return errwrap.Wrapf(typ.Val.Cmp(t1), "input type was inconsistent") + } + + // validateArg1 checks: func(T1) T2 + validateArg1 := func(typ *types.Type) error { + if typ == nil { // unknown so far + return nil + } + if typ.Kind != types.KindFunc { + return fmt.Errorf("input type must be of kind func") + } + if len(typ.Map) != 1 || len(typ.Ord) != 1 { + return fmt.Errorf("input type func must have only one input arg") + } + arg, exists := typ.Map[typ.Ord[0]] + if !exists { + // programming error + return fmt.Errorf("input type func first arg is missing") + } + + if t1 != nil { + if err := arg.Cmp(t1); err != nil { + return errwrap.Wrapf(err, "input type func arg was inconsistent") + } + } + if t2 != nil { + if err := typ.Out.Cmp(t2); err != nil { + return errwrap.Wrapf(err, "input type func output was inconsistent") + } + } + + // in case they weren't set already + t1 = arg + t2 = typ.Out + foundArgName = typ.Ord[0] // we found a name! + return nil + } + + if typ, err := cfavInvar.Args[0].Type(); err == nil { // is it known? + // this sets t1 and t2 on success if it learned + if err := validateArg0(typ); err != nil { + return nil, errwrap.Wrapf(err, "first input arg type is inconsistent") + } + } + if typ, exists := solved[cfavInvar.Args[0]]; exists { // alternate way to lookup type + // this sets t1 and t2 on success if it learned + if err := validateArg0(typ); err != nil { + return nil, errwrap.Wrapf(err, "first input arg type is inconsistent") + } + } + // XXX: since we might not yet have association to this + // expression (dummyArgList) yet, we could consider + // returning some of the invariants and a new generator + // and hoping we get a hit on this one the next time. + if typ, exists := solved[dummyArgList]; exists { // alternate way to lookup type + // this sets t1 and t2 on success if it learned + if err := validateArg0(typ); err != nil { + return nil, errwrap.Wrapf(err, "first input arg type is inconsistent") + } + } + + if typ, err := cfavInvar.Args[1].Type(); err == nil { // is it known? + // this sets t1 and t2 on success if it learned + if err := validateArg1(typ); err != nil { + return nil, errwrap.Wrapf(err, "second input arg type is inconsistent") + } + } + if typ, exists := solved[cfavInvar.Args[1]]; exists { // alternate way to lookup type + // this sets t1 and t2 on success if it learned + if err := validateArg1(typ); err != nil { + return nil, errwrap.Wrapf(err, "second input arg type is inconsistent") + } + } + // XXX: since we might not yet have association to this + // expression (dummyArgFunc) yet, we could consider + // returning some of the invariants and a new generator + // and hoping we get a hit on this one the next time. + if typ, exists := solved[dummyArgFunc]; exists { // alternate way to lookup type + // this sets t1 and t2 on success if it learned + if err := validateArg1(typ); err != nil { + return nil, errwrap.Wrapf(err, "second input arg type is inconsistent") + } + } + + // XXX: look for t1 and t2 in other places? + + if t1 != nil { + invar = &interfaces.EqualsInvariant{ + Expr: t1Expr, + Type: t1, + } + invariants = append(invariants, invar) + + invar = &interfaces.EqualityWrapListInvariant{ + Expr1: cfavInvar.Args[0], + Expr2Val: t1Expr, + } + invariants = append(invariants, invar) + // we already have the mapping, but add both in + // case we need to solve these from either side + invar = &interfaces.EqualityWrapListInvariant{ + Expr1: dummyArgList, + Expr2Val: t1Expr, + } + invariants = append(invariants, invar) + } + + if t1 != nil && t2 != nil { + argName := foundArgName // XXX: is this a hack? + mapped := make(map[string]interfaces.Expr) + ordered := []string{argName} + mapped[argName] = t1Expr + + invar = &interfaces.EqualityWrapFuncInvariant{ + Expr1: dummyArgFunc, + Expr2Map: mapped, + Expr2Ord: ordered, + Expr2Out: t2Expr, + } + invariants = append(invariants, invar) + } + + // note, currently, we can't learn t2 without t1 + if t2 != nil { + invar = &interfaces.EqualsInvariant{ + Expr: t2Expr, + Type: t2, + } + invariants = append(invariants, invar) + + invar = &interfaces.EqualityWrapListInvariant{ + Expr1: dummyOutList, + Expr2Val: t2Expr, + } + invariants = append(invariants, invar) + // we already have the mapping, but add both in + // case we need to solve these from either side + invar = &interfaces.EqualityWrapListInvariant{ + Expr1: cfavInvar.Expr, + Expr2Val: t2Expr, + } + invariants = append(invariants, invar) + } + + // We need to require this knowledge to continue! + if t1 == nil || t2 == nil { + return nil, fmt.Errorf("not enough known about function signature") + } + + // TODO: do we return this relationship with ExprCall? + invar = &interfaces.EqualityWrapCallInvariant{ + // TODO: should Expr1 and Expr2 be reversed??? + Expr1: cfavInvar.Expr, + //Expr2Func: cfavInvar.Func, // same as below + Expr2Func: expr, + } + invariants = append(invariants, invar) + + // TODO: are there any other invariants we should build? + return invariants, nil // generator return + } + // We couldn't tell the solver anything it didn't already know! + return nil, fmt.Errorf("couldn't generate new invariants") + } + invar = &interfaces.GeneratorInvariant{ + Func: fn, + } + invariants = append(invariants, invar) + + return invariants, nil +} + // 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 *MapFunc) Polymorphisms(partialType *types.Type, partialValues []types.Value) ([]*types.Type, error) { + // XXX: double check that this works with `func([]int, func(int) str) []str` (when types change!) // TODO: look at partialValues to gleam type information? if partialType == nil { return nil, fmt.Errorf("zero type information given") @@ -100,22 +379,22 @@ func (obj *MapFunc) Polymorphisms(partialType *types.Type, partialValues []types return nil, fmt.Errorf("must have two args in func") } - if tInputs, exists := partialType.Map[ord[1]]; exists && tInputs != nil { + if tInputs, exists := partialType.Map[ord[0]]; exists && tInputs != nil { if tInputs.Kind != types.KindList { - return nil, fmt.Errorf("second input arg must be of kind list") + return nil, fmt.Errorf("first input arg must be of kind list") } t1 = tInputs.Val // found (if not nil) } - if tFunction, exists := partialType.Map[ord[0]]; exists && tFunction != nil { + if tFunction, exists := partialType.Map[ord[1]]; exists && tFunction != nil { if tFunction.Kind != types.KindFunc { - return nil, fmt.Errorf("first input arg must be a func") + return nil, fmt.Errorf("second input arg must be a func") } fOrd := tFunction.Ord if fMap := tFunction.Map; fMap != nil { if len(fOrd) != 1 { - return nil, fmt.Errorf("first input arg func, must have only one arg") + return nil, fmt.Errorf("second input arg func, must have only one arg") } if fIn, exists := fMap[fOrd[0]]; exists && fIn != nil { if err := fIn.Cmp(t1); t1 != nil && err != nil { @@ -127,7 +406,7 @@ func (obj *MapFunc) Polymorphisms(partialType *types.Type, partialValues []types if fOut := tFunction.Out; fOut != nil { if err := fOut.Cmp(t2); t2 != nil && err != nil { - return nil, errwrap.Wrapf(err, "first arg function out type is inconsistent") + return nil, errwrap.Wrapf(err, "second arg function out type is inconsistent") } t2 = fOut // found } @@ -140,7 +419,7 @@ func (obj *MapFunc) Polymorphisms(partialType *types.Type, partialValues []types tI := types.NewType(fmt.Sprintf("[]%s", t1.String())) // in tO := types.NewType(fmt.Sprintf("[]%s", t2.String())) // out tF := types.NewType(fmt.Sprintf("func(%s) %s", t1.String(), t2.String())) - s := fmt.Sprintf("func(%s %s, %s %s) %s", argNameFunction, tF, argNameInputs, tI, tO) + s := fmt.Sprintf("func(%s %s, %s %s) %s", argNameInputs, tI, argNameFunction, tF, tO) typ := types.NewType(s) // yay! // TODO: type check that the partialValues are compatible @@ -166,20 +445,20 @@ func (obj *MapFunc) Build(typ *types.Type) error { return fmt.Errorf("the map is nil") } - tFunction, exists := typ.Map[typ.Ord[0]] - if !exists || tFunction == nil { + tInputs, exists := typ.Map[typ.Ord[0]] + if !exists || tInputs == nil { return fmt.Errorf("first argument was missing") } - tInputs, exists := typ.Map[typ.Ord[1]] - if !exists || tInputs == nil { + tFunction, exists := typ.Map[typ.Ord[1]] + if !exists || tFunction == nil { return fmt.Errorf("second argument was missing") } - if tFunction.Kind != types.KindFunc { - return fmt.Errorf("first argument must be of kind func") - } if tInputs.Kind != types.KindList { - return fmt.Errorf("second argument must be of kind list") + return fmt.Errorf("first argument must be of kind list") + } + if tFunction.Kind != types.KindFunc { + return fmt.Errorf("second argument must be of kind func") } if typ.Out == nil { @@ -242,7 +521,7 @@ func (obj *MapFunc) Info() *interfaces.Info { // type of 1st arg (the function) tF := types.NewType(fmt.Sprintf("func(%s) %s", tIi.String(), tOi.String())) - s := fmt.Sprintf("func(%s %s, %s %s) %s", argNameFunction, tF, argNameInputs, tI, tO) + s := fmt.Sprintf("func(%s %s, %s %s) %s", argNameInputs, tI, argNameFunction, tF, tO) typ := types.NewType(s) // yay! return &interfaces.Info{ diff --git a/lang/interpret_test/TestAstFunc2/map-iterator0.output b/lang/interpret_test/TestAstFunc2/map-iterator0.output new file mode 100644 index 00000000..73b09b24 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/map-iterator0.output @@ -0,0 +1 @@ +Vertex: test[out1: [1 2 3 4 5]] diff --git a/lang/interpret_test/TestAstFunc2/map-iterator0/main.mcl b/lang/interpret_test/TestAstFunc2/map-iterator0/main.mcl new file mode 100644 index 00000000..697b589a --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/map-iterator0/main.mcl @@ -0,0 +1,13 @@ +import "iter" + +$fn = func($x) { # notable because concrete type is fn(t1) t2, where t1 != t2 + len($x) +} + +$in1 = ["a", "bb", "ccc", "dddd", "eeeee",] + +$out1 = iter.xmap($in1, $fn) # XXX: change to map + +$t1 = template("out1: {{ . }}", $out1) + +test $t1 {} diff --git a/lang/interpret_test/TestAstFunc2/map-iterator1/main.mcl b/lang/interpret_test/TestAstFunc2/map-iterator1/main.mcl index f8949f42..137d6e00 100644 --- a/lang/interpret_test/TestAstFunc2/map-iterator1/main.mcl +++ b/lang/interpret_test/TestAstFunc2/map-iterator1/main.mcl @@ -11,10 +11,10 @@ $fn = func($x) { $in1 = [5, 4, 3, 2, 1,] $in2 = ["a", "b", "c", "d", "e",] -$out1 = iter.xmap($fn, $in1) # XXX: change to map -$out2 = iter.xmap($fn, $in2) # XXX: change to map -$out3 = iterxmap($fn, $in1) # XXX: change to map -$out4 = iterxmap($fn, $in2) # XXX: change to map +$out1 = iter.xmap($in1, $fn) # XXX: change to map +$out2 = iter.xmap($in2, $fn) # XXX: change to map +$out3 = iterxmap($in1, $fn) # XXX: change to map +$out4 = iterxmap($in2, $fn) # XXX: change to map $t1 = template("out1: {{ . }}", $out1) $t2 = template("out2: {{ . }}", $out2) diff --git a/lang/interpret_test/TestAstFunc2/map-iterator2/main.mcl b/lang/interpret_test/TestAstFunc2/map-iterator2/main.mcl index 5b79ce65..57e4180e 100644 --- a/lang/interpret_test/TestAstFunc2/map-iterator2/main.mcl +++ b/lang/interpret_test/TestAstFunc2/map-iterator2/main.mcl @@ -6,7 +6,7 @@ $fn = func($x) { # ignore arg $in = [5, 4, 3, 2, 1,] -$out = iter.xmap($fn, $in) # XXX: change to map +$out = iter.xmap($in, $fn) # XXX: change to map $t = template("out: {{ . }}", $out) diff --git a/lang/interpret_test/TestAstFunc2/map-iterator3.output b/lang/interpret_test/TestAstFunc2/map-iterator3.output new file mode 100644 index 00000000..0053f70d --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/map-iterator3.output @@ -0,0 +1 @@ +Vertex: test[out: [1 2 3 4 5]] diff --git a/lang/interpret_test/TestAstFunc2/map-iterator3/main.mcl b/lang/interpret_test/TestAstFunc2/map-iterator3/main.mcl new file mode 100644 index 00000000..c8a5fb1d --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/map-iterator3/main.mcl @@ -0,0 +1,13 @@ +import "iter" + +$fn = func($x) { # type changes from str to int + len($x) +} + +$in = ["a", "bb", "ccc", "dddd", "eeeee",] + +$out = iter.xmap($in, $fn) # XXX: change to map + +$t = template("out: {{ . }}", $out) + +test $t {} diff --git a/lang/interpret_test/TestAstFunc2/map-iterator4.output b/lang/interpret_test/TestAstFunc2/map-iterator4.output new file mode 100644 index 00000000..0053f70d --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/map-iterator4.output @@ -0,0 +1 @@ +Vertex: test[out: [1 2 3 4 5]] diff --git a/lang/interpret_test/TestAstFunc2/map-iterator4/main.mcl b/lang/interpret_test/TestAstFunc2/map-iterator4/main.mcl new file mode 100644 index 00000000..c1dd44c2 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/map-iterator4/main.mcl @@ -0,0 +1,13 @@ +import "iter" + +$in = ["a", "bb", "ccc", "dddd", "eeeee",] + +# the inline lambda format is more readable with the func as the second arg +$out = iter.xmap($in, func($x) { + len($x) + +}) # XXX: change to map + +$t = template("out: {{ . }}", $out) + +test $t {}