diff --git a/docs/function-guide.md b/docs/function-guide.md index ff1c8658..9197a227 100644 --- a/docs/function-guide.md +++ b/docs/function-guide.md @@ -41,7 +41,7 @@ To implement a function, you'll need to create a file that imports the [`lang/funcs/simple/`](https://github.com/purpleidea/mgmt/tree/master/lang/funcs/simple/) module. It should probably get created in the correct directory inside of: [`lang/core/`](https://github.com/purpleidea/mgmt/tree/master/lang/core/). The -function should be implemented as a `FuncValue` in our type system. It is then +function should be implemented as a `simple.Scaffold` in our API. It is then registered with the engine during `init()`. An example explains it best: ### Example @@ -50,6 +50,7 @@ registered with the engine during `init()`. An example explains it best: package simple import ( + "context" "fmt" "github.com/purpleidea/mgmt/lang/funcs/simple" @@ -59,9 +60,10 @@ import ( // you must register your functions in init when the program starts up func init() { // Example function that squares an int and prints out answer as an str. - simple.ModuleRegister(ModuleName, "talkingsquare", &types.FuncValue{ + + simple.ModuleRegister(ModuleName, "talkingsquare", &simple.Scaffold{ T: types.NewType("func(int) str"), // declare the signature - V: func(input []types.Value) (types.Value, error) { + F: func(ctx context.Context, input []types.Value) (types.Value, error) { i := input[0].Int() // get first arg as an int64 // must return the above specified value return &types.StrValue{ @@ -87,109 +89,41 @@ 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 that imports the -[`lang/funcs/simplepoly/`](https://github.com/purpleidea/mgmt/tree/master/lang/funcs/simplepoly/) -module. It should probably get created in the correct directory inside of: -[`lang/core/`](https://github.com/purpleidea/mgmt/tree/master/lang/core/). 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. The -top-level type must still be a function type, it may only contain variants as -part of its signature. It is probably more difficult to unify a function if its -return type is a variant, as opposed to if one of its args was. - -An example explains it best: - ### Example ```golang +package simple + import ( + "context" "fmt" - "github.com/purpleidea/mgmt/lang/funcs/simplepoly" + "github.com/purpleidea/mgmt/lang/funcs/simple" "github.com/purpleidea/mgmt/lang/types" ) func init() { - // You may use the simplepoly.ModuleRegister method to register your - // function if it's in a module, as seen in the simple function example. - simplepoly.Register("len", []*types.FuncValue{ - { - T: types.NewType("func([]variant) int"), - V: Len, - }, - { - T: types.NewType("func({variant: variant}) int"), - V: Len, - }, + // This is the actual definition of the `len` function. + simple.Register("len", &simple.Scaffold{ + T: types.NewType("func(?1) int"), // contains a unification var + C: simple.TypeMatch([]string{ // match on any of these sigs + "func(str) int", + "func([]?1) int", + "func(map{?1: ?2}) int", + }), + // The implementation is left as an exercise for the reader. + F: 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. +## Simple Polymorphic Function API -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. +Most functions should be implemented using the simple function API. If they need +to have multiple polymorphic forms under the same name, with each resultant type +match needing to be paired to a different implementation, then you can use this +API. This is useful for situations when the functions differ in output type +only. ## Function API @@ -358,23 +292,6 @@ We don't expect this functionality to be particularly useful or common, as it's probably easier and preferable to simply import common golang library code into multiple different functions instead. -## Polymorphic Function API - -The polymorphic function API is an API that lets you implement functions which -do not necessarily have a single static function signature. After compile time, -all functions must have a static function signature. We also know that there -might be different ways you would want to call `printf`, such as: -`printf("the %s is %d", "answer", 42)` or `printf("3 * 2 = %d", 3 * 2)`. Since -you couldn't implement the infinite number of possible signatures, this API lets -you write code which can be coerced into different forms. This makes -implementing what would appear to be generic or polymorphic, instead of -something that is actually static and that still has the static type safety -properties that were guaranteed by the mgmt language. - -Since this is an advanced topic, it is not described in full at this time. For -more information please have a look at the source code comments, some of the -existing implementations, and ask around in the community. - ## Frequently asked questions (Send your questions as a patch to this FAQ! I'll review it, merge it, and diff --git a/docs/language-guide.md b/docs/language-guide.md index dab52a59..0d7e67e8 100644 --- a/docs/language-guide.md +++ b/docs/language-guide.md @@ -639,23 +639,27 @@ so that each `Expr` node in the AST knows what to expect. Type annotation is allowed in situations when you want to explicitly specify a type, or when the compiler cannot deduce it, however, most of it can usually be inferred. -For type inferrence to work, each node in the AST implements a `Unify` method -which is able to return a list of invariants that must hold true. This starts at -the top most AST node, and gets called through to it's children to assemble a -giant list of invariants. The invariants can take different forms. They can -specify that a particular expression must have a particular type, or they can -specify that two expressions must have the same types. More complex invariants -allow you to specify relationships between different types and expressions. -Furthermore, invariants can allow you to specify that only one invariant out of -a set must hold true. +For type inference to work, each `Stmt` node in the AST implements a `TypeCheck` +method which is able to return a list of invariants that must hold true. This +starts at the top most AST node, and gets called through to it's children to +assemble a giant list of invariants. The invariants all have the same form. They +specify that a particular expression corresponds to two particular types which +may both contain unification variables. + +Each `Expr` node in the AST implements an `Infer` and `Check` method. The +`Infer` method returns the type of that node along with a list of invariants as +described above. Unification variables can of course be used throughout. The +`Check` method always uses a generic check implementation and generally doesn't +need to be implemented by the user. Once the list of invariants has been collected, they are run through an invariant solver. The solver can return either return successfully or with an -error. If the solver returns successfully, it means that it has found a trivial +error. If the solver returns successfully, it means that it has found a single mapping between every expression and it's corresponding type. At this point it is a simple task to run `SetType` on every expression so that the types are -known. If the solver returns in error, it is usually due to one of two -possibilities: +known. During this stage, each SetType method verifies that it's a compatible +type that it can use. If either that method or if the solver returns in error, +it is usually due to one of two possibilities: 1. Ambiguity @@ -675,8 +679,8 @@ possibilities: always happens if the user has made a type error in their program. Only one solver currently exists, but it is possible to easily plug in an -alternate implementation if someone more skilled in the art of solver design -would like to propose a more logical or performant variant. +alternate implementation if someone wants to experiment with the art of solver +design and would like to propose a more logical or performant variant. #### Function graph generation @@ -717,8 +721,9 @@ If you'd like to create a built-in, core function, you'll need to implement the function API interface named `Func`. It can be found in [lang/interfaces/func.go](https://github.com/purpleidea/mgmt/tree/master/lang/interfaces/func.go). Your function must have a specific type. For example, a simple math function -might have a signature of `func(x int, y int) int`. As you can see, all the -types are known _before_ compile time. +might have a signature of `func(x int, y int) int`. The simple functions have +their types known _before_ compile time. You may also include unification +variables in the function signature as long as the top-level type is a function. A separate discussion on this matter can be found in the [function guide](function-guide.md). @@ -746,6 +751,12 @@ added in the future. This method is usually called before any other, and should not depend on any other method being called first. Other methods must not depend on this method being called first. +If you use any unification variables in the function signature, then your +function will *not* be made available for use inside templates. This is a +limitation of the `golang` templating library. In the future if this limitation +proves to be significantly annoying, we might consider writing our own template +library. + #### Example ```golang @@ -756,6 +767,18 @@ func (obj *FooFunc) Info() *interfaces.Info { } ``` +#### Example + +This example contains unification variables. + +```golang +func (obj *FooFunc) Info() *interfaces.Info { + return &interfaces.Info{ + Sig: types.NewType("func(a ?1, b ?2, foo [?3]) ?1"), + } +} +``` + ### Init ```golang @@ -818,43 +841,46 @@ Please see the example functions in [lang/core/](https://github.com/purpleidea/mgmt/tree/master/lang/core/). ``` -### Polymorphic Function API +### BuildableFunc Function API -For some functions, it might be helpful to be able to implement a function once, -but to have multiple polymorphic variants that can be chosen at compile time. -For this more advanced topic, you will need to use the -[Polymorphic Function API](#polymorphic-function-api). This will help with code -reuse when you have a small, finite number of possible type signatures, and also -for more complicated cases where you might have an infinite number of possible -type signatures. (eg: `[]str`, or `[][]str`, or `[][][]str`, etc...) +For some functions, it might be helpful to have a function which needs a "build" +step which is run after type unification. This step can be used to build the +function using the determined type, but it may also just be used for checking +that unification picked a valid solution. Suppose you want to implement a function which can assume different type signatures. The mgmt language does not support polymorphic types-- you must use static types throughout the language, however, it is legal to implement a function which can take different specific type signatures based on how it is used. For example, you might wish to add a math function which could take the -form of `func(x int, x int) int` or `func(x float, x float) float` depending on -the input values. You might also want to implement a function which takes an -arbitrary number of input arguments (the number must be statically fixed at the -compile time of your program though) and which returns a string. +form of `func(x int, y int) int` or `func(x float, y float) float` depending on +the input values. For this case you could use a signature containing unification +variables, eg: `func(x ?1, y ?1) ?1`. At the end the buildable function would +need to check that it received a `?1` type of either `int` or `float`, since +this function might not support doing math on strings. Remember that type +unification can only return zero or one solutions, it's not possible to return +more than one, which is why this secondary validation step is a brilliant way to +filter out invalid solutions without needing to encode them as algebraic +conditions during the solver state, which would otherwise make it exponential. -The `PolyFunc` interface adds additional methods which you must implement to -satisfy such a function implementation. If you'd like to implement such a -function, then please notify the project authors, and they will expand this -section with a longer description of the process. +### InferableFunc Function API -#### Examples +You might also want to implement a function which takes an arbitrary number of +input arguments (the number must be statically fixed at the compile time of your +program though) and which returns a string or something else. -What follows are a few examples that might help you understand some of the -language details. +The `InferableFunc` interface adds ad additional `FuncInfer` method which you +must implement to satisfy such a function implementation. This lets you +dynamically generate a type signature (including unification variables) and a +list of invariants before running the type unification solver. It takes as input +a list of the statically known input types and input values (if any) and as well +the number of input arguments specified. This is usually enough information to +generate a fixed type signature of a fixed size. -##### Example Foo - -TODO: please add an example here! - -##### Example Bar - -TODO: please add an example here! +Using this API should generally be pretty rare, but it is how certain special +functions such as `fmt.printf` are built. If you'd like to implement such a +function, then please notify the project authors as we're curious about your +use case. ## Frequently asked questions diff --git a/engine/util/util.go b/engine/util/util.go index f035f06d..928cb127 100644 --- a/engine/util/util.go +++ b/engine/util/util.go @@ -278,6 +278,7 @@ func LangFieldNameToStructFieldName(kind string) (map[string]string, error) { // LangFieldNameToStructType returns the mapping from lang (AST) field names, // and the expected type in our type system for each. +// XXX: Should this return unification variables instead of variant types? func LangFieldNameToStructType(kind string) (map[string]*types.Type, error) { res, err := engine.NewResource(kind) if err != nil { diff --git a/lang/ast/structs.go b/lang/ast/structs.go index 2286ef16..b4fc1981 100644 --- a/lang/ast/structs.go +++ b/lang/ast/structs.go @@ -50,6 +50,7 @@ import ( "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/types" "github.com/purpleidea/mgmt/lang/types/full" + unificationUtil "github.com/purpleidea/mgmt/lang/unification/util" langUtil "github.com/purpleidea/mgmt/lang/util" "github.com/purpleidea/mgmt/pgraph" "github.com/purpleidea/mgmt/util" @@ -181,6 +182,7 @@ var ( type StmtBind struct { Ident string Value interfaces.Expr + Type *types.Type } // String returns a short representation of this statement. @@ -221,6 +223,7 @@ func (obj *StmtBind) Interpolate() (interfaces.Stmt, error) { return &StmtBind{ Ident: obj.Ident, Value: interpolated, + Type: obj.Type, }, nil } @@ -241,6 +244,7 @@ func (obj *StmtBind) Copy() (interfaces.Stmt, error) { return &StmtBind{ Ident: obj.Ident, Value: value, + Type: obj.Type, }, nil } @@ -295,11 +299,33 @@ func (obj *StmtBind) SetScope(scope *interfaces.Scope) error { return obj.Value.SetScope(scope, emptyContext) } -// Unify returns the list of invariants that this node produces. It recursively -// calls Unify on any children elements that exist in the AST, and returns the -// collection to the caller. -func (obj *StmtBind) Unify() ([]interfaces.Invariant, error) { - return obj.Value.Unify() +// TypeCheck returns the list of invariants that this node produces. It does so +// recursively on any children elements that exist in the AST, and returns the +// collection to the caller. It calls TypeCheck for child statements, and +// Infer/Check for child expressions. +func (obj *StmtBind) TypeCheck() ([]*interfaces.UnificationInvariant, error) { + // Don't call obj.Value.Check here! + typ, invariants, err := obj.Value.Infer() + if err != nil { + return nil, err + } + + typExpr := obj.Type + if obj.Type == nil { + typExpr = &types.Type{ + Kind: types.KindUnification, + Uni: types.NewElem(), // unification variable, eg: ?1 + } + } + + invar := &interfaces.UnificationInvariant{ + Expr: obj.Value, + Expect: typExpr, // obj.Type + Actual: typ, + } + invariants = append(invariants, invar) + + return invariants, nil } // Graph returns the reactive function graph which is expressed by this node. It @@ -559,27 +585,26 @@ func (obj *StmtRes) SetScope(scope *interfaces.Scope) error { return nil } -// Unify returns the list of invariants that this node produces. It recursively -// calls Unify on any children elements that exist in the AST, and returns the -// collection to the caller. -func (obj *StmtRes) Unify() ([]interfaces.Invariant, error) { - var invariants []interfaces.Invariant +// TypeCheck returns the list of invariants that this node produces. It does so +// recursively on any children elements that exist in the AST, and returns the +// collection to the caller. It calls TypeCheck for child statements, and +// Infer/Check for child expressions. +func (obj *StmtRes) TypeCheck() ([]*interfaces.UnificationInvariant, error) { + + // Don't call obj.Name.Check here! + typ, invariants, err := obj.Name.Infer() + if err != nil { + return nil, err + } - // collect all the invariants of each field and edge for _, x := range obj.Contents { - invars, err := x.Unify(obj.Kind) // pass in the resource kind + invars, err := x.TypeCheck(obj.Kind) // pass in the resource kind if err != nil { return nil, err } invariants = append(invariants, invars...) } - invars, err := obj.Name.Unify() - if err != nil { - return nil, err - } - invariants = append(invariants, invars...) - // Optimization: If we know it's an str, no need for exclusives! // TODO: Check other cases, like if it's a function call, and we know it // can only return a single string. (Eg: fmt.printf for example.) @@ -594,20 +619,18 @@ func (obj *StmtRes) Unify() ([]interfaces.Invariant, error) { isString = true } } - if isString { - invar := &interfaces.EqualsInvariant{ - Expr: obj.Name, - Type: types.TypeStr, - } - invariants = append(invariants, invar) - return invariants, nil + typExpr := types.TypeListStr // default + + // If we pass here, we only allow []str, no need for exclusives! + if isString { + typExpr = types.TypeStr } - // Down here, we only allow []str, no need for exclusives! - invar := &interfaces.EqualsInvariant{ - Expr: obj.Name, - Type: types.TypeListStr, + invar := &interfaces.UnificationInvariant{ + Expr: obj.Name, + Expect: typExpr, // the name + Actual: typ, } invariants = append(invariants, invar) @@ -1165,7 +1188,7 @@ type StmtResContents interface { Copy() (StmtResContents, error) Ordering(map[string]interfaces.Node) (*pgraph.Graph, map[interfaces.Node]string, error) SetScope(*interfaces.Scope) error - Unify(kind string) ([]interfaces.Invariant, error) // different! + TypeCheck(kind string) ([]*interfaces.UnificationInvariant, error) Graph() (*pgraph.Graph, error) } @@ -1337,37 +1360,38 @@ func (obj *StmtResField) SetScope(scope *interfaces.Scope) error { return nil } -// Unify returns the list of invariants that this node produces. It recursively -// calls Unify on any children elements that exist in the AST, and returns the -// collection to the caller. It is different from the Unify found in the Expr -// and Stmt interfaces because it adds an input parameter. -func (obj *StmtResField) Unify(kind string) ([]interfaces.Invariant, error) { - var invariants []interfaces.Invariant - - invars, err := obj.Value.Unify() +// TypeCheck returns the list of invariants that this node produces. It does so +// recursively on any children elements that exist in the AST, and returns the +// collection to the caller. It calls TypeCheck for child statements, and +// Infer/Check for child expressions. It is different from the TypeCheck method +// found in the Stmt interface because it adds an input parameter. +func (obj *StmtResField) TypeCheck(kind string) ([]*interfaces.UnificationInvariant, error) { + typ, invariants, err := obj.Value.Infer() if err != nil { return nil, err } - invariants = append(invariants, invars...) - // conditional expression might have some children invariants to share + //invars, err := obj.Value.Check(typ) // don't call this here! + if obj.Condition != nil { - condition, err := obj.Condition.Unify() + typ, invars, err := obj.Condition.Infer() if err != nil { return nil, err } - invariants = append(invariants, condition...) + invariants = append(invariants, invars...) - // the condition must ultimately be a boolean - conditionInvar := &interfaces.EqualsInvariant{ - Expr: obj.Condition, - Type: types.TypeBool, + // XXX: Is this needed? + invar := &interfaces.UnificationInvariant{ + Expr: obj.Condition, + Expect: types.TypeBool, + Actual: typ, } - invariants = append(invariants, conditionInvar) + invariants = append(invariants, invar) } // TODO: unfortunately this gets called separately for each field... if // we could cache this, it might be worth looking into for performance! + // XXX: Should this return unification variables instead of variant types? typMap, err := engineUtil.LangFieldNameToStructType(kind) if err != nil { return nil, err @@ -1381,36 +1405,26 @@ func (obj *StmtResField) Unify(kind string) ([]interfaces.Invariant, error) { return nil, fmt.Errorf("field was empty or contained spaces") } - typ, exists := typMap[obj.Field] + typExpr, exists := typMap[obj.Field] if !exists { return nil, fmt.Errorf("field `%s` does not exist in `%s`", obj.Field, kind) } - if typ == nil { + if typExpr == nil { // possible programming error return nil, fmt.Errorf("type for field `%s` in `%s` is nil", obj.Field, kind) } - if typ.Kind == types.KindVariant { // special path, res field has interface{} - if typ.Var == nil { - invar := &interfaces.AnyInvariant{ - Expr: obj.Value, - } - invariants = append(invariants, invar) - return invariants, nil + if typExpr.Kind == types.KindVariant { // special path, res field has interface{} + typExpr = &types.Type{ + Kind: types.KindUnification, + Uni: types.NewElem(), // unification variable, eg: ?1 } - - // in case it is present (nil is okay too) - invar := &interfaces.EqualsInvariant{ - Expr: obj.Value, - Type: typ.Var, // in case it is present (nil is okay too) - } - invariants = append(invariants, invar) - return invariants, nil } // regular scenario - invar := &interfaces.EqualsInvariant{ - Expr: obj.Value, - Type: typ, + invar := &interfaces.UnificationInvariant{ + Expr: obj.Value, + Expect: typExpr, + Actual: typ, } invariants = append(invariants, invar) @@ -1621,33 +1635,33 @@ func (obj *StmtResEdge) SetScope(scope *interfaces.Scope) error { return nil } -// Unify returns the list of invariants that this node produces. It recursively -// calls Unify on any children elements that exist in the AST, and returns the -// collection to the caller. It is different from the Unify found in the Expr -// and Stmt interfaces because it adds an input parameter. -func (obj *StmtResEdge) Unify(kind string) ([]interfaces.Invariant, error) { - var invariants []interfaces.Invariant - - invars, err := obj.EdgeHalf.Unify() +// TypeCheck returns the list of invariants that this node produces. It does so +// recursively on any children elements that exist in the AST, and returns the +// collection to the caller. It calls TypeCheck for child statements, and +// Infer/Check for child expressions. It is different from the TypeCheck method +// found in the Stmt interface because it adds an input parameter. +func (obj *StmtResEdge) TypeCheck(kind string) ([]*interfaces.UnificationInvariant, error) { + invariants, err := obj.EdgeHalf.TypeCheck() if err != nil { return nil, err } - invariants = append(invariants, invars...) - // conditional expression might have some children invariants to share + //invars, err := obj.Value.Check(typ) // don't call this here! + if obj.Condition != nil { - condition, err := obj.Condition.Unify() + typ, invars, err := obj.Condition.Infer() if err != nil { return nil, err } - invariants = append(invariants, condition...) + invariants = append(invariants, invars...) - // the condition must ultimately be a boolean - conditionInvar := &interfaces.EqualsInvariant{ - Expr: obj.Condition, - Type: types.TypeBool, + // XXX: Is this needed? + invar := &interfaces.UnificationInvariant{ + Expr: obj.Condition, + Expect: types.TypeBool, + Actual: typ, } - invariants = append(invariants, conditionInvar) + invariants = append(invariants, invar) } return invariants, nil @@ -1879,89 +1893,88 @@ func (obj *StmtResMeta) SetScope(scope *interfaces.Scope) error { return nil } -// Unify returns the list of invariants that this node produces. It recursively -// calls Unify on any children elements that exist in the AST, and returns the -// collection to the caller. It is different from the Unify found in the Expr -// and Stmt interfaces because it adds an input parameter. -// XXX: Allow specifying partial meta param structs and unify the subset type. -// XXX: The resource fields have the same limitation with field structs. -func (obj *StmtResMeta) Unify(kind string) ([]interfaces.Invariant, error) { - var invariants []interfaces.Invariant +// TypeCheck returns the list of invariants that this node produces. It does so +// recursively on any children elements that exist in the AST, and returns the +// collection to the caller. It calls TypeCheck for child statements, and +// Infer/Check for child expressions. It is different from the TypeCheck method +// found in the Stmt interface because it adds an input parameter. +func (obj *StmtResMeta) TypeCheck(kind string) ([]*interfaces.UnificationInvariant, error) { - invars, err := obj.MetaExpr.Unify() + typ, invariants, err := obj.MetaExpr.Infer() if err != nil { return nil, err } - invariants = append(invariants, invars...) - // conditional expression might have some children invariants to share + //invars, err := obj.MetaExpr.Check(typ) // don't call this here! + if obj.Condition != nil { - condition, err := obj.Condition.Unify() + typ, invars, err := obj.Condition.Infer() if err != nil { return nil, err } - invariants = append(invariants, condition...) - // the condition must ultimately be a boolean - conditionInvar := &interfaces.EqualsInvariant{ - Expr: obj.Condition, - Type: types.TypeBool, + invariants = append(invariants, invars...) + + // XXX: Is this needed? + invar := &interfaces.UnificationInvariant{ + Expr: obj.Condition, + Expect: types.TypeBool, + Actual: typ, } - invariants = append(invariants, conditionInvar) + invariants = append(invariants, invar) } + var typExpr *types.Type + //typExpr = &types.Type{ + // Kind: types.KindUnification, + // Uni: types.NewElem(), // unification variable, eg: ?1 + //} + // add additional invariants based on what's in obj.Property !!! - var invar interfaces.Invariant - static := func(typ *types.Type) interfaces.Invariant { - return &interfaces.EqualsInvariant{ - Expr: obj.MetaExpr, - Type: typ, - } - } switch p := strings.ToLower(obj.Property); p { // TODO: we could add these fields dynamically if we were fancy! case "noop": - invar = static(types.TypeBool) + typExpr = types.TypeBool case "retry": - invar = static(types.TypeInt) + typExpr = types.TypeInt case "retryreset": - invar = static(types.TypeBool) + typExpr = types.TypeBool case "delay": - invar = static(types.TypeInt) + typExpr = types.TypeInt case "poll": - invar = static(types.TypeInt) + typExpr = types.TypeInt case "limit": // rate.Limit - invar = static(types.TypeFloat) + typExpr = types.TypeFloat case "burst": - invar = static(types.TypeInt) + typExpr = types.TypeInt case "reset": - invar = static(types.TypeBool) + typExpr = types.TypeBool case "sema": - invar = static(types.TypeListStr) + typExpr = types.TypeListStr case "rewatch": - invar = static(types.TypeBool) + typExpr = types.TypeBool case "realize": - invar = static(types.TypeBool) + typExpr = types.TypeBool case "reverse": // TODO: We might want more parameters about how to reverse. - invar = static(types.TypeBool) + typExpr = types.TypeBool case "autoedge": - invar = static(types.TypeBool) + typExpr = types.TypeBool case "autogroup": - invar = static(types.TypeBool) + typExpr = types.TypeBool // autoedge and autogroup aren't part of the `MetaRes` interface, but we // can merge them in here for simplicity in the public user interface... @@ -1972,11 +1985,17 @@ func (obj *StmtResMeta) Unify(kind string) ([]interfaces.Invariant, error) { return types.NewType(fmt.Sprintf("struct{noop bool; retry int; retryreset bool; delay int; poll int; limit float; burst int; reset bool; sema []str; rewatch bool; realize bool; reverse %s; autoedge bool; autogroup bool}", reverse.String())) } // TODO: We might want more parameters about how to reverse. - invar = static(wrap(types.TypeBool)) + typExpr = wrap(types.TypeBool) default: return nil, fmt.Errorf("unknown property: %s", p) } + + invar := &interfaces.UnificationInvariant{ + Expr: obj.MetaExpr, + Expect: typExpr, + Actual: typ, + } invariants = append(invariants, invar) return invariants, nil @@ -2162,11 +2181,12 @@ func (obj *StmtEdge) SetScope(scope *interfaces.Scope) error { return nil } -// Unify returns the list of invariants that this node produces. It recursively -// calls Unify on any children elements that exist in the AST, and returns the -// collection to the caller. -func (obj *StmtEdge) Unify() ([]interfaces.Invariant, error) { - var invariants []interfaces.Invariant +// TypeCheck returns the list of invariants that this node produces. It does so +// recursively on any children elements that exist in the AST, and returns the +// collection to the caller. It calls TypeCheck for child statements, and +// Infer/Check for child expressions. +func (obj *StmtEdge) TypeCheck() ([]*interfaces.UnificationInvariant, error) { + // XXX: Should we check the edge lengths here? // TODO: this sort of sideloaded validation could happen in a dedicated // Validate() function, but for now is here for lack of a better place! @@ -2226,12 +2246,14 @@ func (obj *StmtEdge) Unify() ([]interfaces.Invariant, error) { } } + invariants := []*interfaces.UnificationInvariant{} + for _, x := range obj.EdgeHalfList { - if x.SendRecv != "" && len(obj.EdgeHalfList) != 2 { + if x.SendRecv != "" && len(obj.EdgeHalfList) != 2 { // XXX: mod 2? return nil, fmt.Errorf("send/recv edges must come in pairs") } - invars, err := x.Unify() + invars, err := x.TypeCheck() if err != nil { return nil, err } @@ -2444,16 +2466,21 @@ func (obj *StmtEdgeHalf) SetScope(scope *interfaces.Scope) error { return obj.Name.SetScope(scope, map[string]interfaces.Expr{}) } -// Unify returns the list of invariants that this node produces. It recursively -// calls Unify on any children elements that exist in the AST, and returns the -// collection to the caller. -func (obj *StmtEdgeHalf) Unify() ([]interfaces.Invariant, error) { - var invariants []interfaces.Invariant +// TypeCheck returns the list of invariants that this node produces. It does so +// recursively on any children elements that exist in the AST, and returns the +// collection to the caller. It calls TypeCheck for child statements, and +// Infer/Check for child expressions. +func (obj *StmtEdgeHalf) TypeCheck() ([]*interfaces.UnificationInvariant, error) { if obj.Kind == "" { return nil, fmt.Errorf("missing resource kind in edge") } + typ, invariants, err := obj.Name.Infer() + if err != nil { + return nil, err + } + if obj.SendRecv != "" { // FIXME: write this function (get expected type of field) //invar, err := StructFieldInvariant(obj.Kind, obj.SendRecv) @@ -2463,12 +2490,6 @@ func (obj *StmtEdgeHalf) Unify() ([]interfaces.Invariant, error) { //invariants = append(invariants, invar...) } - invars, err := obj.Name.Unify() - if err != nil { - return nil, err - } - invariants = append(invariants, invars...) - // Optimization: If we know it's an str, no need for exclusives! // TODO: Check other cases, like if it's a function call, and we know it // can only return a single string. (Eg: fmt.printf for example.) @@ -2483,20 +2504,18 @@ func (obj *StmtEdgeHalf) Unify() ([]interfaces.Invariant, error) { isString = true } } - if isString { - invar := &interfaces.EqualsInvariant{ - Expr: obj.Name, - Type: types.TypeStr, - } - invariants = append(invariants, invar) - return invariants, nil + typExpr := types.TypeListStr // default + + // If we pass here, we only allow []str, no need for exclusives! + if isString { + typExpr = types.TypeStr } - // Down here, we only allow []str, no need for exclusives! - invar := &interfaces.EqualsInvariant{ - Expr: obj.Name, - Type: types.TypeListStr, + invar := &interfaces.UnificationInvariant{ + Expr: obj.Name, + Expect: typExpr, // the name + Actual: typ, } invariants = append(invariants, invar) @@ -2765,41 +2784,39 @@ func (obj *StmtIf) SetScope(scope *interfaces.Scope) error { return nil } -// Unify returns the list of invariants that this node produces. It recursively -// calls Unify on any children elements that exist in the AST, and returns the -// collection to the caller. -func (obj *StmtIf) Unify() ([]interfaces.Invariant, error) { - var invariants []interfaces.Invariant - - // conditional expression might have some children invariants to share - condition, err := obj.Condition.Unify() +// TypeCheck returns the list of invariants that this node produces. It does so +// recursively on any children elements that exist in the AST, and returns the +// collection to the caller. It calls TypeCheck for child statements, and +// Infer/Check for child expressions. +func (obj *StmtIf) TypeCheck() ([]*interfaces.UnificationInvariant, error) { + // Don't call obj.Condition.Check here! + typ, invariants, err := obj.Condition.Infer() if err != nil { return nil, err } - invariants = append(invariants, condition...) - // the condition must ultimately be a boolean - conditionInvar := &interfaces.EqualsInvariant{ - Expr: obj.Condition, - Type: types.TypeBool, + typExpr := types.TypeBool // default + invar := &interfaces.UnificationInvariant{ + Expr: obj.Condition, + Expect: typExpr, // the condition + Actual: typ, } - invariants = append(invariants, conditionInvar) + invariants = append(invariants, invar) - // recurse into the two branches if obj.ThenBranch != nil { - thenBranch, err := obj.ThenBranch.Unify() + invars, err := obj.ThenBranch.TypeCheck() if err != nil { return nil, err } - invariants = append(invariants, thenBranch...) + invariants = append(invariants, invars...) } if obj.ElseBranch != nil { - elseBranch, err := obj.ElseBranch.Unify() + invars, err := obj.ElseBranch.TypeCheck() if err != nil { return nil, err } - invariants = append(invariants, elseBranch...) + invariants = append(invariants, invars...) } return invariants, nil @@ -3475,7 +3492,7 @@ func (obj *StmtProg) importSystemScope(name string) (*interfaces.Scope, error) { obj.data.Logf("init...") // init and validate the structure of the AST - // some of this might happen *after* interpolate in SetScope or Unify... + // some of this might happen *after* interpolate in SetScope or later... if err := ast.Init(obj.data); err != nil { return nil, errwrap.Wrapf(err, "could not init and validate AST") } @@ -3515,7 +3532,7 @@ func (obj *StmtProg) importSystemScope(name string) (*interfaces.Scope, error) { isEmpty = false // this module/scope isn't empty } - // save a reference to the prog for future usage in Unify/Graph/Etc... + // save a reference to the prog for future usage in TypeCheck/Graph/Etc... // XXX: we don't need to do this if we can combine with Append! obj.importProgs = append(obj.importProgs, prog) @@ -3614,7 +3631,7 @@ func (obj *StmtProg) importScopeWithParsedInputs(input *inputs.ParsedInput, scop Debug: obj.data.Debug, Logf: logf, } - // some of this might happen *after* interpolate in SetScope or Unify... + // some of this might happen *after* interpolate in SetScope or later... if err := ast.Init(data); err != nil { return nil, errwrap.Wrapf(err, "could not init and validate AST") } @@ -3668,7 +3685,7 @@ func (obj *StmtProg) importScopeWithParsedInputs(input *inputs.ParsedInput, scop } } - // save a reference to the prog for future usage in Unify/Graph/Etc... + // save a reference to the prog for future usage in TypeCheck/Graph/Etc... obj.importProgs = append(obj.importProgs, prog) // collecting these here is more elegant (and possibly more efficient!) @@ -4114,26 +4131,32 @@ func (obj *StmtProg) SetScope(scope *interfaces.Scope) error { return nil } -// Unify returns the list of invariants that this node produces. It recursively -// calls Unify on any children elements that exist in the AST, and returns the -// collection to the caller. -func (obj *StmtProg) Unify() ([]interfaces.Invariant, error) { - var invariants []interfaces.Invariant +// TypeCheck returns the list of invariants that this node produces. It does so +// recursively on any children elements that exist in the AST, and returns the +// collection to the caller. It calls TypeCheck for child statements, and +// Infer/Check for child expressions. +func (obj *StmtProg) TypeCheck() ([]*interfaces.UnificationInvariant, error) { + invariants := []*interfaces.UnificationInvariant{} - // collect all the invariants of each sub-expression for _, x := range obj.Body { - // skip over *StmtClass here + // We skip this because it will be instantiated potentially with + // different types. if _, ok := x.(*StmtClass); ok { continue } - if _, ok := x.(*StmtFunc); ok { // TODO: is this correct? + + // We skip this because it will be instantiated potentially with + // different types. + if _, ok := x.(*StmtFunc); ok { continue } - //if _, ok := x.(*StmtBind); ok { // TODO: is this correct? - // continue - //} - invars, err := x.Unify() + // We skip this one too since we pull it in at the use site. + if _, ok := x.(*StmtBind); ok { + continue + } + + invars, err := x.TypeCheck() if err != nil { return nil, err } @@ -4142,7 +4165,7 @@ func (obj *StmtProg) Unify() ([]interfaces.Invariant, error) { // add invariants from SetScope's imported child programs for _, x := range obj.importProgs { - invars, err := x.Unify() + invars, err := x.TypeCheck() if err != nil { return nil, err } @@ -4272,8 +4295,8 @@ func (obj *StmtProg) IsModuleUnsafe() error { // TODO: rename this function? // definition. type StmtFunc struct { Name string - //Func *ExprFunc // TODO: should it be this instead? - Func interfaces.Expr // TODO: is this correct? + Func interfaces.Expr + Type *types.Type } // String returns a short representation of this statement. @@ -4320,6 +4343,7 @@ func (obj *StmtFunc) Interpolate() (interfaces.Stmt, error) { return &StmtFunc{ Name: obj.Name, Func: interpolated, + Type: obj.Type, }, nil } @@ -4340,6 +4364,7 @@ func (obj *StmtFunc) Copy() (interfaces.Stmt, error) { return &StmtFunc{ Name: obj.Name, Func: fn, + Type: obj.Type, }, nil } @@ -4406,18 +4431,48 @@ func (obj *StmtFunc) SetScope(scope *interfaces.Scope) error { return obj.Func.SetScope(scope, map[string]interfaces.Expr{}) } -// Unify returns the list of invariants that this node produces. It recursively -// calls Unify on any children elements that exist in the AST, and returns the -// collection to the caller. -func (obj *StmtFunc) Unify() ([]interfaces.Invariant, error) { +// TypeCheck returns the list of invariants that this node produces. It does so +// recursively on any children elements that exist in the AST, and returns the +// collection to the caller. It calls TypeCheck for child statements, and +// Infer/Check for child expressions. +func (obj *StmtFunc) TypeCheck() ([]*interfaces.UnificationInvariant, error) { if obj.Name == "" { return nil, fmt.Errorf("missing function name") } + + // Don't call obj.Func.Check here! + typ, invariants, err := obj.Func.Infer() + if err != nil { + return nil, err + } + + typExpr := obj.Type + if obj.Type == nil { + typExpr = &types.Type{ + Kind: types.KindUnification, + Uni: types.NewElem(), // unification variable, eg: ?1 + } + } + + invar := &interfaces.UnificationInvariant{ + Expr: obj.Func, + Expect: typExpr, // obj.Type + Actual: typ, + } + invariants = append(invariants, invar) + // I think the invariants should come in from ExprCall instead, because // ExprCall operates on an instatiated copy of the contained ExprFunc // which will have different pointers than what is seen here. - //return obj.Func.Unify() // nope! - return []interfaces.Invariant{}, nil + + // nope! + // Don't call obj.Func.Check here! + //typ, invariants, err := obj.Func.Infer() + //if err != nil { + // return nil, err + //} + + return invariants, nil } // Graph returns the reactive function graph which is expressed by this node. It @@ -4602,16 +4657,23 @@ func (obj *StmtClass) SetScope(scope *interfaces.Scope) error { return nil } -// Unify returns the list of invariants that this node produces. It recursively -// calls Unify on any children elements that exist in the AST, and returns the -// collection to the caller. -func (obj *StmtClass) Unify() ([]interfaces.Invariant, error) { +// TypeCheck returns the list of invariants that this node produces. It does so +// recursively on any children elements that exist in the AST, and returns the +// collection to the caller. It calls TypeCheck for child statements, and +// Infer/Check for child expressions. +func (obj *StmtClass) TypeCheck() ([]*interfaces.UnificationInvariant, error) { if obj.Name == "" { return nil, fmt.Errorf("missing class name") } // TODO: do we need to add anything else here because of the obj.Args ? - return obj.Body.Unify() + + invariants, err := obj.Body.TypeCheck() + if err != nil { + return nil, err + } + + return invariants, nil } // Graph returns the reactive function graph which is expressed by this node. It @@ -4919,10 +4981,11 @@ func (obj *StmtInclude) SetScope(scope *interfaces.Scope) error { return nil } -// Unify returns the list of invariants that this node produces. It recursively -// calls Unify on any children elements that exist in the AST, and returns the -// collection to the caller. -func (obj *StmtInclude) Unify() ([]interfaces.Invariant, error) { +// TypeCheck returns the list of invariants that this node produces. It does so +// recursively on any children elements that exist in the AST, and returns the +// collection to the caller. It calls TypeCheck for child statements, and +// Infer/Check for child expressions. +func (obj *StmtInclude) TypeCheck() ([]*interfaces.UnificationInvariant, error) { if obj.Name == "" { return nil, fmt.Errorf("missing include name") } @@ -4936,29 +4999,29 @@ func (obj *StmtInclude) Unify() ([]interfaces.Invariant, error) { return nil, fmt.Errorf("class `%s` expected %d args but got %d", obj.Name, len(obj.class.Args), len(obj.Args)) } - var invariants []interfaces.Invariant - // do this here because we skip doing it in the StmtProg parent - invars, err := obj.class.Unify() + invariants, err := obj.class.TypeCheck() if err != nil { return nil, err } - invariants = append(invariants, invars...) - // collect all the invariants of each sub-expression for i, x := range obj.Args { - invars, err := x.Unify() + // Don't call x.Check here! + typ, invars, err := x.Infer() if err != nil { return nil, err } invariants = append(invariants, invars...) + // XXX: Should we be doing this stuff here? + // TODO: are additional invariants required? // add invariants between the args and the class - if typ := obj.class.Args[i].Type; typ != nil { - invar := &interfaces.EqualsInvariant{ - Expr: obj.Args[i], - Type: typ, // type of arg + if typExpr := obj.class.Args[i].Type; typExpr != nil { + invar := &interfaces.UnificationInvariant{ + Expr: x, + Expect: typExpr, // type of arg + Actual: typ, } invariants = append(invariants, invar) } @@ -5069,15 +5132,16 @@ func (obj *StmtImport) Ordering(produces map[string]interfaces.Node) (*pgraph.Gr // which it propagates this downwards to. func (obj *StmtImport) SetScope(*interfaces.Scope) error { return nil } -// Unify returns the list of invariants that this node produces. It recursively -// calls Unify on any children elements that exist in the AST, and returns the -// collection to the caller. -func (obj *StmtImport) Unify() ([]interfaces.Invariant, error) { +// TypeCheck returns the list of invariants that this node produces. It does so +// recursively on any children elements that exist in the AST, and returns the +// collection to the caller. It calls TypeCheck for child statements, and +// Infer/Check for child expressions. +func (obj *StmtImport) TypeCheck() ([]*interfaces.UnificationInvariant, error) { if obj.Name == "" { return nil, fmt.Errorf("missing import name") } - return []interfaces.Invariant{}, nil + return []*interfaces.UnificationInvariant{}, nil } // Graph returns the reactive function graph which is expressed by this node. It @@ -5159,11 +5223,12 @@ func (obj *StmtComment) Ordering(produces map[string]interfaces.Node) (*pgraph.G // does not need to know about the parent scope. func (obj *StmtComment) SetScope(*interfaces.Scope) error { return nil } -// Unify returns the list of invariants that this node produces. It recursively -// calls Unify on any children elements that exist in the AST, and returns the -// collection to the caller. -func (obj *StmtComment) Unify() ([]interfaces.Invariant, error) { - return []interfaces.Invariant{}, nil +// TypeCheck returns the list of invariants that this node produces. It does so +// recursively on any children elements that exist in the AST, and returns the +// collection to the caller. It calls TypeCheck for child statements, and +// Infer/Check for child expressions. +func (obj *StmtComment) TypeCheck() ([]*interfaces.UnificationInvariant, error) { + return []*interfaces.UnificationInvariant{}, nil } // Graph returns the reactive function graph which is expressed by this node. It @@ -5251,17 +5316,30 @@ func (obj *ExprBool) SetType(typ *types.Type) error { return types.TypeBool.Cmp( // here. func (obj *ExprBool) Type() (*types.Type, error) { return types.TypeBool, nil } -// Unify returns the list of invariants that this node produces. It recursively -// calls Unify on any children elements that exist in the AST, and returns the -// collection to the caller. -func (obj *ExprBool) Unify() ([]interfaces.Invariant, error) { - invariants := []interfaces.Invariant{ - &interfaces.EqualsInvariant{ - Expr: obj, - Type: types.TypeBool, +// Infer returns the type of itself and a collection of invariants. The returned +// type may contain unification variables. It collects the invariants by calling +// Check on its children expressions. In making those calls, it passes in the +// known type for that child to get it to "Check" it. When the type is not +// known, it should create a new unification variable to pass in to the child +// Check calls. Infer usually only calls Check on things inside of it, and often +// does not call another Infer. +func (obj *ExprBool) Infer() (*types.Type, []*interfaces.UnificationInvariant, error) { + // This adds the obj ptr, so it's seen as an expr that we need to solve. + return types.TypeBool, []*interfaces.UnificationInvariant{ + { + Expr: obj, + Expect: types.TypeBool, + Actual: types.TypeBool, }, - } - return invariants, nil + }, nil +} + +// Check is checking that the input type is equal to the object that Check is +// running on. In doing so, it adds any invariants that are necessary. Check +// must always call Infer to produce the invariant. The implementation can be +// generic for all expressions. +func (obj *ExprBool) Check(typ *types.Type) ([]*interfaces.UnificationInvariant, error) { + return interfaces.GenericCheck(obj, typ) } // Func returns the reactive stream of values that this expression produces. @@ -5435,17 +5513,30 @@ func (obj *ExprStr) SetType(typ *types.Type) error { return types.TypeStr.Cmp(ty // here. func (obj *ExprStr) Type() (*types.Type, error) { return types.TypeStr, nil } -// Unify returns the list of invariants that this node produces. It recursively -// calls Unify on any children elements that exist in the AST, and returns the -// collection to the caller. -func (obj *ExprStr) Unify() ([]interfaces.Invariant, error) { - invariants := []interfaces.Invariant{ - &interfaces.EqualsInvariant{ - Expr: obj, // unique id for this expression (a pointer) - Type: types.TypeStr, +// Infer returns the type of itself and a collection of invariants. The returned +// type may contain unification variables. It collects the invariants by calling +// Check on its children expressions. In making those calls, it passes in the +// known type for that child to get it to "Check" it. When the type is not +// known, it should create a new unification variable to pass in to the child +// Check calls. Infer usually only calls Check on things inside of it, and often +// does not call another Infer. +func (obj *ExprStr) Infer() (*types.Type, []*interfaces.UnificationInvariant, error) { + // This adds the obj ptr, so it's seen as an expr that we need to solve. + return types.TypeStr, []*interfaces.UnificationInvariant{ + { + Expr: obj, + Expect: types.TypeStr, + Actual: types.TypeStr, }, - } - return invariants, nil + }, nil +} + +// Check is checking that the input type is equal to the object that Check is +// running on. In doing so, it adds any invariants that are necessary. Check +// must always call Infer to produce the invariant. The implementation can be +// generic for all expressions. +func (obj *ExprStr) Check(typ *types.Type) ([]*interfaces.UnificationInvariant, error) { + return interfaces.GenericCheck(obj, typ) } // Func returns the reactive stream of values that this expression produces. @@ -5565,17 +5656,30 @@ func (obj *ExprInt) SetType(typ *types.Type) error { return types.TypeInt.Cmp(ty // here. func (obj *ExprInt) Type() (*types.Type, error) { return types.TypeInt, nil } -// Unify returns the list of invariants that this node produces. It recursively -// calls Unify on any children elements that exist in the AST, and returns the -// collection to the caller. -func (obj *ExprInt) Unify() ([]interfaces.Invariant, error) { - invariants := []interfaces.Invariant{ - &interfaces.EqualsInvariant{ - Expr: obj, - Type: types.TypeInt, +// Infer returns the type of itself and a collection of invariants. The returned +// type may contain unification variables. It collects the invariants by calling +// Check on its children expressions. In making those calls, it passes in the +// known type for that child to get it to "Check" it. When the type is not +// known, it should create a new unification variable to pass in to the child +// Check calls. Infer usually only calls Check on things inside of it, and often +// does not call another Infer. +func (obj *ExprInt) Infer() (*types.Type, []*interfaces.UnificationInvariant, error) { + // This adds the obj ptr, so it's seen as an expr that we need to solve. + return types.TypeInt, []*interfaces.UnificationInvariant{ + { + Expr: obj, + Expect: types.TypeInt, + Actual: types.TypeInt, }, - } - return invariants, nil + }, nil +} + +// Check is checking that the input type is equal to the object that Check is +// running on. In doing so, it adds any invariants that are necessary. Check +// must always call Infer to produce the invariant. The implementation can be +// generic for all expressions. +func (obj *ExprInt) Check(typ *types.Type) ([]*interfaces.UnificationInvariant, error) { + return interfaces.GenericCheck(obj, typ) } // Func returns the reactive stream of values that this expression produces. @@ -5697,17 +5801,30 @@ func (obj *ExprFloat) SetType(typ *types.Type) error { return types.TypeFloat.Cm // here. func (obj *ExprFloat) Type() (*types.Type, error) { return types.TypeFloat, nil } -// Unify returns the list of invariants that this node produces. It recursively -// calls Unify on any children elements that exist in the AST, and returns the -// collection to the caller. -func (obj *ExprFloat) Unify() ([]interfaces.Invariant, error) { - invariants := []interfaces.Invariant{ - &interfaces.EqualsInvariant{ - Expr: obj, - Type: types.TypeFloat, +// Infer returns the type of itself and a collection of invariants. The returned +// type may contain unification variables. It collects the invariants by calling +// Check on its children expressions. In making those calls, it passes in the +// known type for that child to get it to "Check" it. When the type is not +// known, it should create a new unification variable to pass in to the child +// Check calls. Infer usually only calls Check on things inside of it, and often +// does not call another Infer. +func (obj *ExprFloat) Infer() (*types.Type, []*interfaces.UnificationInvariant, error) { + // This adds the obj ptr, so it's seen as an expr that we need to solve. + return types.TypeFloat, []*interfaces.UnificationInvariant{ + { + Expr: obj, + Expect: types.TypeFloat, + Actual: types.TypeFloat, }, - } - return invariants, nil + }, nil +} + +// Check is checking that the input type is equal to the object that Check is +// running on. In doing so, it adds any invariants that are necessary. Check +// must always call Infer to produce the invariant. The implementation can be +// generic for all expressions. +func (obj *ExprFloat) Check(typ *types.Type) ([]*interfaces.UnificationInvariant, error) { + return interfaces.GenericCheck(obj, typ) } // Func returns the reactive stream of values that this expression produces. @@ -5949,77 +6066,59 @@ func (obj *ExprList) Type() (*types.Type, error) { return obj.typ, nil } -// Unify returns the list of invariants that this node produces. It recursively -// calls Unify on any children elements that exist in the AST, and returns the -// collection to the caller. -func (obj *ExprList) Unify() ([]interfaces.Invariant, error) { - var invariants []interfaces.Invariant +// Infer returns the type of itself and a collection of invariants. The returned +// type may contain unification variables. It collects the invariants by calling +// Check on its children expressions. In making those calls, it passes in the +// known type for that child to get it to "Check" it. When the type is not +// known, it should create a new unification variable to pass in to the child +// Check calls. Infer usually only calls Check on things inside of it, and often +// does not call another Infer. +func (obj *ExprList) Infer() (*types.Type, []*interfaces.UnificationInvariant, error) { + invariants := []*interfaces.UnificationInvariant{} - // if this was set explicitly by the parser - if obj.typ != nil { - invar := &interfaces.EqualsInvariant{ - Expr: obj, - Type: obj.typ, - } - invariants = append(invariants, invar) + // Same unification var because all values in the list have same type. + typ := &types.Type{ + Kind: types.KindUnification, + Uni: types.NewElem(), // unification variable, eg: ?1 + } + typExpr := &types.Type{ + Kind: types.KindList, + Val: typ, } - // collect all the invariants of each sub-expression for _, x := range obj.Elements { - invars, err := x.Unify() + invars, err := x.Check(typ) // typ of the list element if err != nil { - return nil, err + return nil, nil, err } invariants = append(invariants, invars...) } - // each element must be equal to each other - if len(obj.Elements) > 1 { - invariant := &interfaces.EqualityInvariantList{ - Exprs: obj.Elements, - } - invariants = append(invariants, invariant) + // Every infer call must have this section, because expr var needs this. + typType := typExpr + //if obj.typ == nil { // optional says sam + // obj.typ = typExpr // sam says we could unconditionally do this + //} + if obj.typ != nil { + typType = obj.typ } - - // we should be type list of (type of element) - if len(obj.Elements) > 0 { - invariant := &interfaces.EqualityWrapListInvariant{ - Expr1: obj, // unique id for this expression (a pointer) - Expr2Val: obj.Elements[0], - } - invariants = append(invariants, invariant) + // This must be added even if redundant, so that we collect the obj ptr. + invar := &interfaces.UnificationInvariant{ + Expr: obj, + Expect: typExpr, // This is the type that we return. + Actual: typType, } + invariants = append(invariants, invar) - // make sure this empty list gets an element type somehow - if len(obj.Elements) == 0 { - invariant := &interfaces.AnyInvariant{ - Expr: obj, - } - invariants = append(invariants, invariant) + return typExpr, invariants, nil +} - // build a placeholder expr to represent a contained element... - exprAny := &interfaces.ExprAny{} - invars, err := exprAny.Unify() - if err != nil { - return nil, err - } - invariants = append(invariants, invars...) - - // FIXME: instead of using `ExprAny`, we could actually teach - // our unification engine to ensure that our expr kind is list, - // eg: - //&interfaces.EqualityKindInvariant{ - // Expr1: obj, - // Kind: types.KindList, - //} - invar := &interfaces.EqualityWrapListInvariant{ - Expr1: obj, - Expr2Val: exprAny, // hack - } - invariants = append(invariants, invar) - } - - return invariants, nil +// Check is checking that the input type is equal to the object that Check is +// running on. In doing so, it adds any invariants that are necessary. Check +// must always call Infer to produce the invariant. The implementation can be +// generic for all expressions. +func (obj *ExprList) Check(typ *types.Type) ([]*interfaces.UnificationInvariant, error) { + return interfaces.GenericCheck(obj, typ) } // Func returns the reactive stream of values that this expression produces. @@ -6397,101 +6496,70 @@ func (obj *ExprMap) Type() (*types.Type, error) { return obj.typ, nil } -// Unify returns the list of invariants that this node produces. It recursively -// calls Unify on any children elements that exist in the AST, and returns the -// collection to the caller. -func (obj *ExprMap) Unify() ([]interfaces.Invariant, error) { - var invariants []interfaces.Invariant +// Infer returns the type of itself and a collection of invariants. The returned +// type may contain unification variables. It collects the invariants by calling +// Check on its children expressions. In making those calls, it passes in the +// known type for that child to get it to "Check" it. When the type is not +// known, it should create a new unification variable to pass in to the child +// Check calls. Infer usually only calls Check on things inside of it, and often +// does not call another Infer. +func (obj *ExprMap) Infer() (*types.Type, []*interfaces.UnificationInvariant, error) { + invariants := []*interfaces.UnificationInvariant{} - // if this was set explicitly by the parser - if obj.typ != nil { - invar := &interfaces.EqualsInvariant{ - Expr: obj, - Type: obj.typ, - } - invariants = append(invariants, invar) + // Same unification var because all key/val's in the map have same type. + ktyp := &types.Type{ + Kind: types.KindUnification, + Uni: types.NewElem(), // unification variable, eg: ?1 + } + vtyp := &types.Type{ + Kind: types.KindUnification, + Uni: types.NewElem(), // unification variable, eg: ?2 + } + typExpr := &types.Type{ + Kind: types.KindMap, + Key: ktyp, + Val: vtyp, } - // collect all the invariants of each sub-expression for _, x := range obj.KVs { - keyInvars, err := x.Key.Unify() + keyInvars, err := x.Key.Check(ktyp) // typ of the map key if err != nil { - return nil, err + return nil, nil, err } invariants = append(invariants, keyInvars...) - valInvars, err := x.Val.Unify() + valInvars, err := x.Val.Check(vtyp) // typ of the map val if err != nil { - return nil, err + return nil, nil, err } invariants = append(invariants, valInvars...) } - // all keys must have the same type, all vals must have the same type - if len(obj.KVs) > 1 { - keyExprs, valExprs := []interfaces.Expr{}, []interfaces.Expr{} - for i := range obj.KVs { - keyExprs = append(keyExprs, obj.KVs[i].Key) - valExprs = append(valExprs, obj.KVs[i].Val) - } - - keyInvariant := &interfaces.EqualityInvariantList{ - Exprs: keyExprs, - } - invariants = append(invariants, keyInvariant) - - valInvariant := &interfaces.EqualityInvariantList{ - Exprs: valExprs, - } - invariants = append(invariants, valInvariant) + // Every infer call must have this section, because expr var needs this. + typType := typExpr + //if obj.typ == nil { // optional says sam + // obj.typ = typExpr // sam says we could unconditionally do this + //} + if obj.typ != nil { + typType = obj.typ } - - // we should be type map of (type of element) - if len(obj.KVs) > 0 { - invariant := &interfaces.EqualityWrapMapInvariant{ - Expr1: obj, // unique id for this expression (a pointer) - Expr2Key: obj.KVs[0].Key, - Expr2Val: obj.KVs[0].Val, - } - invariants = append(invariants, invariant) + // This must be added even if redundant, so that we collect the obj ptr. + invar := &interfaces.UnificationInvariant{ + Expr: obj, + Expect: typExpr, // This is the type that we return. + Actual: typType, } + invariants = append(invariants, invar) - // make sure this empty map gets a type for its key/value somehow - if len(obj.KVs) == 0 { - invariant := &interfaces.AnyInvariant{ - Expr: obj, - } - invariants = append(invariants, invariant) + return typExpr, invariants, nil +} - // build a placeholder expr to represent a contained key... - exprAnyKey, exprAnyVal := &interfaces.ExprAny{}, &interfaces.ExprAny{} - invarsKey, err := exprAnyKey.Unify() - if err != nil { - return nil, err - } - invariants = append(invariants, invarsKey...) - invarsVal, err := exprAnyVal.Unify() - if err != nil { - return nil, err - } - invariants = append(invariants, invarsVal...) - - // FIXME: instead of using `ExprAny`, we could actually teach - // our unification engine to ensure that our expr kind is list, - // eg: - //&interfaces.EqualityKindInvariant{ - // Expr1: obj, - // Kind: types.KindMap, - //} - invar := &interfaces.EqualityWrapMapInvariant{ - Expr1: obj, - Expr2Key: exprAnyKey, // hack - Expr2Val: exprAnyVal, // hack - } - invariants = append(invariants, invar) - } - - return invariants, nil +// Check is checking that the input type is equal to the object that Check is +// running on. In doing so, it adds any invariants that are necessary. Check +// must always call Infer to produce the invariant. The implementation can be +// generic for all expressions. +func (obj *ExprMap) Check(typ *types.Type) ([]*interfaces.UnificationInvariant, error) { + return interfaces.GenericCheck(obj, typ) } // Func returns the reactive stream of values that this expression produces. @@ -6855,45 +6923,67 @@ func (obj *ExprStruct) Type() (*types.Type, error) { return obj.typ, nil } -// Unify returns the list of invariants that this node produces. It recursively -// calls Unify on any children elements that exist in the AST, and returns the -// collection to the caller. -func (obj *ExprStruct) Unify() ([]interfaces.Invariant, error) { - var invariants []interfaces.Invariant +// Infer returns the type of itself and a collection of invariants. The returned +// type may contain unification variables. It collects the invariants by calling +// Check on its children expressions. In making those calls, it passes in the +// known type for that child to get it to "Check" it. When the type is not +// known, it should create a new unification variable to pass in to the child +// Check calls. Infer usually only calls Check on things inside of it, and often +// does not call another Infer. +func (obj *ExprStruct) Infer() (*types.Type, []*interfaces.UnificationInvariant, error) { + invariants := []*interfaces.UnificationInvariant{} - // if this was set explicitly by the parser - if obj.typ != nil { - invar := &interfaces.EqualsInvariant{ - Expr: obj, - Type: obj.typ, - } - invariants = append(invariants, invar) - } + m := make(map[string]*types.Type) + ord := []string{} - // collect all the invariants of each sub-expression + // Different unification var for each field in the struct. for _, x := range obj.Fields { - invars, err := x.Value.Unify() + typ := &types.Type{ + Kind: types.KindUnification, + Uni: types.NewElem(), // unification variable, eg: ?1 + } + + m[x.Name] = typ + ord = append(ord, x.Name) + + invars, err := x.Value.Check(typ) // typ of the struct field if err != nil { - return nil, err + return nil, nil, err } invariants = append(invariants, invars...) } - // build the reference to ourself if we have undetermined field types - mapped := make(map[string]interfaces.Expr) - ordered := []string{} - for _, x := range obj.Fields { - mapped[x.Name] = x.Value - ordered = append(ordered, x.Name) + typExpr := &types.Type{ + Kind: types.KindStruct, + Map: m, + Ord: ord, } - invariant := &interfaces.EqualityWrapStructInvariant{ - Expr1: obj, // unique id for this expression (a pointer) - Expr2Map: mapped, - Expr2Ord: ordered, - } - invariants = append(invariants, invariant) - return invariants, nil + // Every infer call must have this section, because expr var needs this. + typType := typExpr + //if obj.typ == nil { // optional says sam + // obj.typ = typExpr // sam says we could unconditionally do this + //} + if obj.typ != nil { + typType = obj.typ + } + // This must be added even if redundant, so that we collect the obj ptr. + invar := &interfaces.UnificationInvariant{ + Expr: obj, + Expect: typExpr, // This is the type that we return. + Actual: typType, + } + invariants = append(invariants, invar) + + return typExpr, invariants, nil +} + +// Check is checking that the input type is equal to the object that Check is +// running on. In doing so, it adds any invariants that are necessary. Check +// must always call Infer to produce the invariant. The implementation can be +// generic for all expressions. +func (obj *ExprStruct) Check(typ *types.Type) ([]*interfaces.UnificationInvariant, error) { + return interfaces.GenericCheck(obj, typ) } // Func returns the reactive stream of values that this expression produces. @@ -7228,14 +7318,31 @@ func (obj *ExprFunc) Copy() (interfaces.Expr, error) { var function interfaces.Func if obj.Function != nil { - function = obj.Function() // force re-build a new pointer here! + // We sometimes copy the ExprFunc because we're using the same + // one in two places, and it might have a different type and + // type unification needs to solve for it in more than one way. + // It also turns out that some functions such as the struct + // lookup function store information that they learned during + // `FuncInfer`, and as a result, if we re-build this, then we + // lose that information and the function can then fail during + // `Build`. As a result, those functions can implement a `Copy` + // method which we will use instead, so they can preserve any + // internal state that they would like to keep. + copyableFunc, isCopyableFunc := obj.function.(interfaces.CopyableFunc) + if obj.function == nil || !isCopyableFunc { + function = obj.Function() // force re-build a new pointer here! + } else { + // is copyable! + function = copyableFunc.Copy() + } + // restore the type we previously set in SetType() if obj.typ != nil { - polyFn, ok := function.(interfaces.PolyFunc) // is it statically polymorphic? + buildableFn, ok := function.(interfaces.BuildableFunc) // is it statically polymorphic? if ok { - newTyp, err := polyFn.Build(obj.typ) + newTyp, err := buildableFn.Build(obj.typ) if err != nil { - return nil, errwrap.Wrapf(err, "could not build expr func") + return nil, err // don't wrap, err is ok } // Cmp doesn't compare arg names. Check it's compatible... if err := obj.typ.Cmp(newTyp); err != nil { @@ -7356,7 +7463,10 @@ func (obj *ExprFunc) SetScope(scope *interfaces.Scope, sctx map[string]interface // make a list as long as obj.Args obj.params = make([]*ExprParam, len(obj.Args)) for i, arg := range obj.Args { - param := &ExprParam{Name: arg.Name, Typ: arg.Type} + param := &ExprParam{ + typ: arg.Type, + Name: arg.Name, + } obj.params[i] = param sctxBody[arg.Name] = param } @@ -7388,16 +7498,17 @@ func (obj *ExprFunc) SetType(typ *types.Type) error { // TODO: should we ensure this is set to a KindFunc ? if obj.Function != nil { - polyFn, ok := obj.function.(interfaces.PolyFunc) // is it statically polymorphic? + // is it buildable? (formerly statically polymorphic) + buildableFn, ok := obj.function.(interfaces.BuildableFunc) if ok { - newTyp, err := polyFn.Build(typ) + newTyp, err := buildableFn.Build(typ) if err != nil { - return errwrap.Wrapf(err, "could not build expr func") + return err // don't wrap, err is ok } // Cmp doesn't compare arg names. typ = newTyp // check it's compatible down below... } else { - // Even if it's not polymorphic, we'd like to use the + // Even if it's not a buildable, we'd like to use the // real arg names of that function, in case they don't // get passed through type unification somehow... // (There can be an AST bug that this would prevent.) @@ -7514,145 +7625,113 @@ func (obj *ExprFunc) Type() (*types.Type, error) { return obj.typ, nil } -// Unify returns the list of invariants that this node produces. It recursively -// calls Unify on any children elements that exist in the AST, and returns the -// collection to the caller. -func (obj *ExprFunc) Unify() ([]interfaces.Invariant, error) { - var invariants []interfaces.Invariant +// Infer returns the type of itself and a collection of invariants. The returned +// type may contain unification variables. It collects the invariants by calling +// Check on its children expressions. In making those calls, it passes in the +// known type for that child to get it to "Check" it. When the type is not +// known, it should create a new unification variable to pass in to the child +// Check calls. Infer usually only calls Check on things inside of it, and often +// does not call another Infer. +func (obj *ExprFunc) Infer() (*types.Type, []*interfaces.UnificationInvariant, error) { + invariants := []*interfaces.UnificationInvariant{} - // if this was set explicitly by the parser - if obj.typ != nil { - invar := &interfaces.EqualsInvariant{ - Expr: obj, - Type: obj.typ, + if i, j := len(obj.Args), len(obj.params); i != j { + // programming error? + if obj.Title == "" { + return nil, nil, fmt.Errorf("func args and params mismatch %d != %d", i, j) } - invariants = append(invariants, invar) + return nil, nil, fmt.Errorf("func `%s` args and params mismatch %d != %d", obj.Title, i, j) } - // if we know the type statically... - // TODO: is this redundant, or do we need something similar elsewhere? - if typ, err := obj.Type(); err == nil { - invar := &interfaces.EqualsInvariant{ - Expr: obj, - Type: typ, - } - invariants = append(invariants, invar) - } + m := make(map[string]*types.Type) + ord := []string{} + var out *types.Type - // collect all the invariants of the body - if obj.Body != nil { - expr2Ord := []string{} - expr2Map := map[string]interfaces.Expr{} - for i, arg := range obj.Args { - expr2Ord = append(expr2Ord, arg.Name) - expr2Map[arg.Name] = obj.params[i] + // This obj.Args stuff is only used for the obj.Body lambda case. + for i, arg := range obj.Args { + typArg := arg.Type // maybe it's nil + if arg.Type == nil { + typArg = &types.Type{ + Kind: types.KindUnification, + Uni: types.NewElem(), // unification variable, eg: ?1 + } } - funcInvariant := &interfaces.EqualityWrapFuncInvariant{ - Expr1: obj, - Expr2Map: expr2Map, - Expr2Ord: expr2Ord, - Expr2Out: obj.Body, - } - invariants = append(invariants, funcInvariant) - invars, err := obj.Body.Unify() + invars, err := obj.params[i].Check(typArg) if err != nil { - return nil, err + return nil, nil, err + } + invariants = append(invariants, invars...) + + m[arg.Name] = typArg + ord = append(ord, arg.Name) + } + + out = obj.Return + if obj.Return == nil { + out = &types.Type{ + Kind: types.KindUnification, + Uni: types.NewElem(), // unification variable, eg: ?1 + } + } + + if obj.Body != nil { + invars, err := obj.Body.Check(out) // typ of the func body + if err != nil { + return nil, nil, err } invariants = append(invariants, invars...) } - // return type must be equal to the body expression - if obj.Body != nil && obj.Return != nil { - invar := &interfaces.EqualsInvariant{ - Expr: obj.Body, - Type: obj.Return, - } - invariants = append(invariants, invar) + typExpr := &types.Type{ + Kind: types.KindFunc, + Map: m, + Ord: ord, + Out: out, } - // TODO: should we try and add invariants for obj.Args? - - // We don't want to Unify against the *original* ExprFunc pointer, since - // we want the copy of it which is what ExprCall points to. We don't - // call this Unify() method from anywhere except from when it's within - // an ExprCall. We don't call it from StmtFunc which is just a container - // for it. This is basically used as a helper function! By the time this - // is called, we've already made an obj.function which is the copied, - // instantiated version of obj.Function that we are going to use. if obj.Function != nil { - fn := obj.function // instantiated copy of obj.Function - polyFn, ok := fn.(interfaces.PolyFunc) // is it statically polymorphic? - if ok { - // We just run the Unify() method of the ExprFunc if it - // happens to have one. Get the list of Invariants, and - // return them directly. - invars, err := polyFn.Unify(obj) - if err != nil { - return nil, err - } - invariants = append(invariants, invars...) - } - - // It's okay to attempt to get a static signature too, if it's - // nil or has a variant (polymorphic funcs) then it's ignored. - sig := fn.Info().Sig - if sig != nil && !sig.HasVariant() { - invar := &interfaces.EqualsInvariant{ - Expr: obj, - Type: sig, - } - invariants = append(invariants, invar) + // Don't call obj.function.(interfaces.InferableFunc).Infer here + // because we wouldn't have information about how we call it + // anyways. This happens in ExprCall instead. We only need to + // ensure this ExprFunc returns a valid unification variable. + typExpr = &types.Type{ + Kind: types.KindUnification, + Uni: types.NewElem(), // unification variable, eg: ?1 } } //if len(obj.Values) > 0 - ors := []interfaces.Invariant{} // solve only one from this list - once := false for _, fn := range obj.Values { - typ := fn.Type() - if typ.Kind != types.KindFunc { - // programming error - return nil, fmt.Errorf("overloaded value was not of kind func") - } - - // NOTE: if we have more than one possibility here, *and* at - // least one of them contains a variant, *and* at least one does - // not, then we *can't* use any of these until the unification - // engine supports variants, because instead of an "OR" between - // multiple possibilities, this will look like fewer - // possibilities exist, and that the answer must be one of them! - // TODO: Previously, we just skipped all of these invariants! If - // we get examples that don't work well, just abandon this part. - if !typ.HasVariant() { - invar := &interfaces.EqualsInvariant{ - Expr: obj, - Type: typ, - } - ors = append(ors, invar) // one solution added! - } else if !once { - // Add at *most* only one any invariant in an exclusive - // set, otherwise two or more possibilities will have - // equivalent answers. - anyInvar := &interfaces.AnyInvariant{ - Expr: obj, - } - ors = append(ors, anyInvar) - once = true - } - - } // end results loop - if len(ors) > 0 { - var invar interfaces.Invariant = &interfaces.ExclusiveInvariant{ - Invariants: ors, // one and only one of these should be true - } - if len(ors) == 1 { - invar = ors[0] // there should only be one - } - invariants = append(invariants, invar) + _ = fn + panic("not implemented") // XXX: not implemented! } - return invariants, nil + // Every infer call must have this section, because expr var needs this. + typType := typExpr + //if obj.typ == nil { // optional says sam + // obj.typ = typExpr // sam says we could unconditionally do this + //} + if obj.typ != nil { + typType = obj.typ + } + // This must be added even if redundant, so that we collect the obj ptr. + invar := &interfaces.UnificationInvariant{ + Expr: obj, + Expect: typExpr, // This is the type that we return. + Actual: typType, + } + invariants = append(invariants, invar) + + return typExpr, invariants, nil +} + +// Check is checking that the input type is equal to the object that Check is +// running on. In doing so, it adds any invariants that are necessary. Check +// must always call Infer to produce the invariant. The implementation can be +// generic for all expressions. +func (obj *ExprFunc) Check(typ *types.Type) ([]*interfaces.UnificationInvariant, error) { + return interfaces.GenericCheck(obj, typ) } // Graph returns the reactive function graph which is expressed by this node. It @@ -7787,7 +7866,9 @@ func (obj *ExprFunc) SetValue(value types.Value) error { // This might get called speculatively (early) during unification to learn more. // This particular value is always known since it is a constant. func (obj *ExprFunc) Value() (types.Value, error) { - panic("ExprFunc does not store its latest value because resources don't yet have function fields.") + // Don't panic because we call Value speculatively for partial values! + // XXX: Not implemented + return nil, fmt.Errorf("error: ExprFunc does not store its latest value because resources don't yet have function fields") //// TODO: implement speculative value lookup (if not already sufficient) //return &full.FuncValue{ // V: obj.V, @@ -8119,7 +8200,7 @@ func (obj *ExprCall) Type() (*types.Type, error) { } // function specific code follows... - fn, isFn := obj.expr.(*ExprFunc) + exprFunc, isFn := obj.expr.(*ExprFunc) if !isFn { if obj.typ == nil { return nil, interfaces.ErrTypeCurrentlyUnknown @@ -8127,7 +8208,7 @@ func (obj *ExprCall) Type() (*types.Type, error) { return obj.typ, nil } - sig, err := fn.Type() + sig, err := exprFunc.Type() if err != nil { return nil, err } @@ -8136,21 +8217,21 @@ func (obj *ExprCall) Type() (*types.Type, error) { } // speculate if a partial return type is known - if fn.Body != nil { - if fn.Return != nil && obj.typ == nil { - return fn.Return, nil + if exprFunc.Body != nil { + if exprFunc.Return != nil && obj.typ == nil { + return exprFunc.Return, nil } - if typ, err := fn.Body.Type(); err == nil && obj.typ == nil { + if typ, err := exprFunc.Body.Type(); err == nil && obj.typ == nil { return typ, nil } } - if fn.Function != nil { - // is it statically polymorphic or not? - _, isPoly := fn.function.(interfaces.PolyFunc) - if !isPoly && obj.typ == nil { - if info := fn.function.Info(); info != nil { + if exprFunc.Function != nil { + // is it buildable? (formerly statically polymorphic) + _, isBuildable := exprFunc.function.(interfaces.BuildableFunc) + if !isBuildable && obj.typ == nil { + if info := exprFunc.function.Info(); info != nil { if sig := info.Sig; sig != nil { if typ := sig.Out; typ != nil && !typ.HasVariant() { return typ, nil // speculate! @@ -8162,9 +8243,9 @@ func (obj *ExprCall) Type() (*types.Type, error) { // consistent return values across all possibilities available } - //if len(fn.Values) > 0 + //if len(exprFunc.Values) > 0 // check to see if we have a unique return type - for _, fn := range fn.Values { + for _, fn := range exprFunc.Values { typ := fn.Type() if typ == nil || typ.Out == nil { continue // skip, not available yet @@ -8180,194 +8261,10 @@ func (obj *ExprCall) Type() (*types.Type, error) { return obj.typ, nil } -// Unify returns the list of invariants that this node produces. It recursively -// calls Unify on any children elements that exist in the AST, and returns the -// collection to the caller. -func (obj *ExprCall) Unify() ([]interfaces.Invariant, error) { - if obj.expr == nil { - // possible programming error - return nil, fmt.Errorf("call doesn't contain an expr pointer yet") - } - - var invariants []interfaces.Invariant - - // if this was set explicitly by the parser - if obj.typ != nil { - invar := &interfaces.EqualsInvariant{ - Expr: obj, - Type: obj.typ, - } - invariants = append(invariants, invar) - } - - //if obj.typ != nil { // XXX: i think this is probably incorrect... - // invar := &interfaces.EqualsInvariant{ - // Expr: obj.expr, - // Type: obj.typ, - // } - // invariants = append(invariants, invar) - //} - - // collect all the invariants of each sub-expression - for _, x := range obj.Args { - invars, err := x.Unify() - if err != nil { - return nil, err - } - invariants = append(invariants, invars...) - } - - // I think I need to associate the func call with the actual func - // expression somehow... This is because when I try to do unification in - // a function like printf, I need to be able to know which args (values) - // this particular version of the function I'm calling is associated - // with. So I need to know the linkage. It has to be added here, since - // ExprFunc doesn't know who's calling it. And why would it even want to - // know who's calling it? - argsCopy := []interfaces.Expr{} - for _, arg := range obj.Args { - argsCopy = append(argsCopy, arg) - } - invar := &interfaces.CallFuncArgsValueInvariant{ - Expr: obj, - Func: trueCallee(obj.expr), - Args: argsCopy, - } - invariants = append(invariants, invar) - - // add the invariants from the actual function that we'll be using... - // don't add them from the pre-copied function, which is never used... - invars, err := obj.expr.Unify() - if err != nil { - return nil, err - } - invariants = append(invariants, invars...) - - anyInvar := &interfaces.AnyInvariant{ // TODO: maybe this isn't needed? - Expr: obj.expr, - } - invariants = append(invariants, anyInvar) - - // our type should equal the return type of the called function, and our - // argument types should be equal to the types of the parameters of the - // function - // arg0, arg1, arg2 - expr2Ord := []string{} - expr2Map := map[string]interfaces.Expr{} - for i, argExpr := range obj.Args { - argName := fmt.Sprintf("arg%d", i) - expr2Ord = append(expr2Ord, argName) - expr2Map[argName] = argExpr - } - funcInvar := &interfaces.EqualityWrapFuncInvariant{ - Expr1: obj.expr, - Expr2Map: expr2Map, - Expr2Ord: expr2Ord, - Expr2Out: obj, - } - invariants = append(invariants, funcInvar) - - // function specific code follows... - fn, isFn := obj.expr.(*ExprFunc) - if !isFn { - return invariants, nil - } - - // if we know the return type, it should match our type - if fn.Body != nil && fn.Return != nil { - invar := &interfaces.EqualsInvariant{ - Expr: obj, // return type from calling the function - Type: fn.Return, // specified return type - } - invariants = append(invariants, invar) - } - - // If ExprFunc is built from mcl code. Note: Unify on fn.Body is called - // from within StmtBind or StmtFunc, depending on whether it's a lambda. - // Instead, we'll block it there, and run it from here instead... - if fn.Body != nil { - if i, j := len(obj.Args), len(fn.Args); i != j { - return nil, fmt.Errorf("func `%s` is being called with %d args, but expected %d args", obj.Name, i, j) - } - - // do the specified args match any specified arg types? - for i, x := range fn.Args { - if x.Type == nil { // unknown type - continue - } - invar := &interfaces.EqualsInvariant{ - Expr: obj.Args[i], - Type: x.Type, - } - invariants = append(invariants, invar) - } - - // do the variables in the body match the arg types ? - // XXX: test this section to ensure it's the right scope (should - // it be getScope(fn) ?) and is it what we want... - for _, x := range fn.Args { - expr, exists := obj.scope.Variables[x.Name] // XXX: test! - if !exists || x.Type == nil { - continue - } - invar := &interfaces.EqualsInvariant{ - Expr: expr, - Type: x.Type, - } - invariants = append(invariants, invar) - } - - // build the reference to ourself if we have undetermined field types - mapped := make(map[string]interfaces.Expr) - ordered := []string{} - for i, x := range fn.Args { - mapped[x.Name] = obj.Args[i] - ordered = append(ordered, x.Name) - } - - // determine the type of the function itself - invariant := &interfaces.EqualityWrapFuncInvariant{ - Expr1: fn, // unique id for this expression (a pointer) - Expr2Map: mapped, - Expr2Ord: ordered, - Expr2Out: fn.Body, - } - invariants = append(invariants, invariant) - - //if fn.Return != nil { - // invariant := &interfaces.EqualityWrapFuncInvariant{ - // Expr1: fn, // unique id for this expression (a pointer) - // Expr2Map: mapped, - // Expr2Ord: ordered, - // Expr2Out: fn.Return, // XXX: ??? - // } - // invariants = append(invariants, invariant) - //} - - // TODO: Do we need to add an EqualityWrapCallInvariant here? - - // the return type of this call expr, should match the body type - invar := &interfaces.EqualityInvariant{ - Expr1: obj, - Expr2: fn.Body, - } - invariants = append(invariants, invar) - - //if fn.Return != nil { - // invar := &interfaces.EqualityInvariant{ - // Expr1: obj, - // Expr2: fn.Return, XXX: ??? - // } - // invariants = append(invariants, invar) - //} - - return invariants, nil - } - - //if fn.Function != nil ... - - var results []*types.Type - +// getPartials is a helper function to aid in building partial types and values. +// Remember that it's not legal to run many of the normal methods like .String() +// on a partial type. +func (obj *ExprCall) getPartials(fn *ExprFunc) (*types.Type, []types.Value, error) { argGen := func(x int) (string, error) { // assume (incorrectly?) for now... return util.NumToAlpha(x), nil @@ -8380,38 +8277,37 @@ func (obj *ExprCall) Unify() ([]interfaces.Invariant, error) { } // build partial type and partial input values to aid in filtering... - argNames := []string{} mapped := make(map[string]*types.Type) - partialValues := []types.Value{} - for i := range obj.Args { + //argNames := []string{} + //partialValues := []types.Value{} + argNames := make([]string, len(obj.Args)) + partialValues := make([]types.Value, len(obj.Args)) + for i, arg := range obj.Args { name, err := argGen(i) // get the Nth arg name if err != nil { - return nil, errwrap.Wrapf(err, "error getting arg name #%d for func `%s`", i, obj.Name) + return nil, nil, errwrap.Wrapf(err, "error getting arg #%d for func `%s`", i, obj.Name) } if name == "" { // possible programming error - return nil, fmt.Errorf("can't get arg name #%d for func `%s`", i, obj.Name) + return nil, nil, fmt.Errorf("can't get arg #%d for func `%s`", i, obj.Name) } - argNames = append(argNames, name) - mapped[name] = nil // unknown type - partialValues = append(partialValues, nil) // XXX: is this safe? + //mapped[name] = nil // unknown type + //argNames = append(argNames, name) + //partialValues = append(partialValues, nil) // placeholder value - // optimization: if zeroth arg is a static string, specify this! - // TODO: this is a more specialized version of the next check... - if x, ok := obj.Args[0].(*ExprStr); i == 0 && ok { // is static? - mapped[name], _ = x.Type() - partialValues[i], _ = x.Value() // store value - } - - // optimization: if type is already known, specify it now! - if t, err := obj.Args[i].Type(); err == nil { // is known? - mapped[name] = t - // if value is completely static, pass it in now! - if v, err := obj.Args[i].Value(); err == nil { - partialValues[i] = v // store value - } + // optimization: if type/value is already known, specify it now! + var err1, err2 error + mapped[name], err1 = arg.Type() // nil type on error + partialValues[i], err2 = arg.Value() // nil value on error + if err1 == nil && err2 == nil && mapped[name].Cmp(partialValues[i].Type()) != nil { + // This can happen when we statically find an issue like + // a printf scenario where it's wrong statically... + t1 := mapped[name] + t2 := partialValues[i].Type() + return nil, nil, fmt.Errorf("type/value inconsistent at arg #%d for func `%s`: %v != %v", i, obj.Name, t1, t2) } } + out, err := obj.Type() // do we know the return type yet? if err != nil { out = nil // just to make sure... @@ -8425,170 +8321,190 @@ func (obj *ExprCall) Unify() ([]interfaces.Invariant, error) { Out: out, // possibly nil } - var polyFn interfaces.PolyFunc - var ok bool - if fn.Function != nil { - polyFn, ok = fn.function.(interfaces.PolyFunc) // is it statically polymorphic? + return partialType, partialValues, nil +} + +// Infer returns the type of itself and a collection of invariants. The returned +// type may contain unification variables. It collects the invariants by calling +// Check on its children expressions. In making those calls, it passes in the +// known type for that child to get it to "Check" it. When the type is not +// known, it should create a new unification variable to pass in to the child +// Check calls. Infer usually only calls Check on things inside of it, and often +// does not call another Infer. +func (obj *ExprCall) Infer() (*types.Type, []*interfaces.UnificationInvariant, error) { + if obj.expr == nil { + // possible programming error + return nil, nil, fmt.Errorf("call doesn't contain an expr pointer yet") } - if fn.Function != nil && ok { - // We just run the Unify() method of the ExprFunc if it happens - // to have one. Get the list of Invariants, and return them - // directly. We want to run this unification inside of ExprCall - // and not in ExprFunc, because it's only in ExprCall that we - // have the instantiated copy of the ExprFunc that we actually - // build and unify against and which has the correct pointer - // now, where as the ExprFunc pointer isn't what we unify with! - invars, err := polyFn.Unify(obj.expr) - if err != nil { - return nil, errwrap.Wrapf(err, "polymorphic unification for func `%s` could not be done", obj.Name) + invariants := []*interfaces.UnificationInvariant{} + + mapped := make(map[string]*types.Type) + ordered := []string{} + var typExpr *types.Type // out + + // Look at what kind of function we are calling... + callee := trueCallee(obj.expr) + exprFunc, isFn := callee.(*ExprFunc) + + argGen := func(x int) (string, error) { + // assume (incorrectly?) for now... + return util.NumToAlpha(x), nil + } + if isFn && exprFunc.Function != nil { + namedArgsFn, ok := exprFunc.function.(interfaces.NamedArgsFunc) // are the args named? + if ok { + argGen = namedArgsFn.ArgGen // func(int) string } + } + + for i, arg := range obj.Args { // []interfaces.Expr + name, err := argGen(i) // get the Nth arg name + if err != nil { + return nil, nil, errwrap.Wrapf(err, "error getting arg name #%d for func `%s`", i, obj.Name) + } + if name == "" { + // possible programming error + return nil, nil, fmt.Errorf("can't get arg name #%d for func `%s`", i, obj.Name) + } + + typ, invars, err := arg.Infer() + if err != nil { + return nil, nil, err + } + // Equivalent: + //typ := &types.Type{ + // Kind: types.KindUnification, + // Uni: types.NewElem(), // unification variable, eg: ?1 + //} + //invars, err := arg.Check(typ) // typ of the arg + //if err != nil { + // return nil, nil, err + //} invariants = append(invariants, invars...) - } else if fn.Function != nil && !ok { - sig := fn.function.Info().Sig - if sig == nil { - // this can happen if it's incorrectly implemented - return nil, errwrap.Wrapf(err, "unification for func `%s` returned nil signature", obj.Name) - } - results = []*types.Type{sig} // only one (non-polymorphic) + mapped[name] = typ + ordered = append(ordered, name) } - // if len(fn.Values) > 0 - for _, f := range fn.Values { - // FIXME: can we filter based on partialValues too? - // TODO: if status is "both", should we skip as too difficult? - _, err := f.T.ComplexCmp(partialType) - if err != nil { - continue - } - results = append(results, f.T) + typExpr = &types.Type{ + Kind: types.KindUnification, + Uni: types.NewElem(), // unification variable, eg: ?1 } - // build invariants from a list of possible types - ors := []interfaces.Invariant{} // solve only one from this list - // each of these is a different possible signature + typFunc := &types.Type{ + Kind: types.KindFunc, + Map: mapped, + Ord: ordered, + Out: typExpr, + } - for _, typ := range results { - if typ.Kind != types.KindFunc { - panic("overloaded result was not of kind func") - } - - // XXX: how do we deal with template returning a variant? - // XXX: i think we need more invariant types, and if it's - // going to be a variant, just return no results, and the - // defaults from the engine should just match it anyways! - if typ.HasVariant() { // XXX: ¯\_(ツ)_/¯ - //continue // XXX: alternate strategy... - //return nil, fmt.Errorf("variant type not yet supported, got: %+v", typ) // XXX: old strategy - } - if typ.Kind == types.KindVariant { // XXX: ¯\_(ツ)_/¯ - // XXX: maybe needed to avoid an oversimplified exclusive! - anyInvar := &interfaces.AnyInvariant{ - Expr: fn, // TODO: fn or obj ? - } - ors = append(ors, anyInvar) - continue // can't deal with raw variant a.t.m. - } - - if i, j := len(typ.Ord), len(obj.Args); i != j { - continue // this signature won't work for us, skip! - } - - // what would a set of invariants for this sig look like? - var invars []interfaces.Invariant - - // use Map and Ord for Input (Kind == Function) - for i, x := range typ.Ord { - if typ.Map[x].HasVariant() { // XXX: ¯\_(ツ)_/¯ - // TODO: maybe this isn't needed? - invar := &interfaces.AnyInvariant{ - Expr: obj.Args[i], - } - invars = append(invars, invar) - continue - } - invar := &interfaces.EqualsInvariant{ - Expr: obj.Args[i], - Type: typ.Map[x], // type of arg - } - invars = append(invars, invar) - } - if typ.Out != nil { - // this expression should equal the output type of the function - if typ.Out.HasVariant() { // XXX: ¯\_(ツ)_/¯ - // TODO: maybe this isn't needed? - invar := &interfaces.AnyInvariant{ - Expr: obj, - } - invars = append(invars, invar) - } else { - invar := &interfaces.EqualsInvariant{ - Expr: obj, - Type: typ.Out, - } - invars = append(invars, invar) - } - } - - // add more invariants to link the partials... - mapped := make(map[string]interfaces.Expr) - ordered := []string{} - for pos, x := range obj.Args { - name := argNames[pos] - mapped[name] = x - ordered = append(ordered, name) - } - - if !typ.HasVariant() { // XXX: ¯\_(ツ)_/¯ - funcInvariant := &interfaces.EqualsInvariant{ - Expr: fn, - Type: typ, - } - invars = append(invars, funcInvariant) - } else { - // XXX: maybe needed to avoid an oversimplified exclusive! - anyInvar := &interfaces.AnyInvariant{ - Expr: fn, // TODO: fn or obj ? - } - invars = append(invars, anyInvar) - } - // Note: The usage of this invariant is different from the other - // wrap* invariants, because in this case, the expression type - // is the return type which is produced, where as the entire - // function itself has its own type which includes the types of - // the input arguments... - invar := &interfaces.EqualityWrapFuncInvariant{ - Expr1: fn, - Expr2Map: mapped, - Expr2Ord: ordered, - Expr2Out: obj, // type of expression is return type of function - } - invars = append(invars, invar) - - // all of these need to be true together - and := &interfaces.ConjunctionInvariant{ - Invariants: invars, - } - - ors = append(ors, and) // one solution added! - } // end results loop - - // don't error here, we might not want to add any invariants! - //if len(results) == 0 { - // return nil, fmt.Errorf("can't find any valid signatures that match func `%s`", obj.Name) + // Every infer call must have this section, because expr var needs this. + typType := typExpr + //if obj.typ == nil { // optional says sam + // obj.typ = typExpr // sam says we could unconditionally do this //} - if len(ors) > 0 { - var invar interfaces.Invariant = &interfaces.ExclusiveInvariant{ - Invariants: ors, // one and only one of these should be true + if obj.typ != nil { + typType = obj.typ + } + // This must be added even if redundant, so that we collect the obj ptr. + invar := &interfaces.UnificationInvariant{ + Expr: obj, + Expect: typExpr, // This is the type that we return. + Actual: typType, + } + invariants = append(invariants, invar) + + // We run this Check for all cases. (So refactor it to here.) + invars, err := obj.expr.Check(typFunc) + if err != nil { + return nil, nil, err + } + invariants = append(invariants, invars...) + + if !isFn { + // legacy case (does this even happen?) + return typExpr, invariants, nil + } + + // Just get ExprFunc.Check to figure it out... + //if exprFunc.Body != nil {} + + if exprFunc.Function != nil { + var typFn *types.Type + fn := exprFunc.function // instantiated copy of exprFunc.Function + // is it inferable? (formerly statically polymorphic) + inferableFn, ok := fn.(interfaces.InferableFunc) + if info := fn.Info(); !ok && info != nil && info.Sig != nil { + if info.Sig.HasVariant() { // XXX: legacy, remove me + // XXX: Look up the obj.Title for obj.expr instead? + return nil, nil, fmt.Errorf("func `%s` contains a variant: %s", obj.Name, info.Sig) + } + + // It's important that we copy the type signature, since + // it may otherwise get used in more than one place for + // type unification when in fact there should be two or + // more different solutions if it's polymorphic and used + // more than once. We could be more careful when passing + // this in here, but it's simple and safe to just always + // do this copy. Sam prefers this approach. + typFn = info.Sig.Copy() + + } else if ok { + partialType, partialValues, err := obj.getPartials(exprFunc) + if err != nil { + return nil, nil, err + } + + // We just run the Infer() method of the ExprFunc if it + // happens to have one. Get the list of Invariants, and + // return them directly. + typ, invars, err := inferableFn.FuncInfer(partialType, partialValues) + if err != nil { + return nil, nil, errwrap.Wrapf(err, "func `%s` infer error", exprFunc.Title) + } + invariants = append(invariants, invars...) + + // It's important that we copy the type signature here. + // See the above comment which explains the reasoning. + typFn = typ.Copy() + + } else { + // programming error + return nil, nil, fmt.Errorf("incorrectly built `%s` function", exprFunc.Title) } - if len(ors) == 1 { - invar = ors[0] // there should only be one + + invar := &interfaces.UnificationInvariant{ + Expr: obj.expr, // this should NOT be obj + Expect: typFunc, // TODO: are these two reversed here? + Actual: typFn, } invariants = append(invariants, invar) + + // TODO: Do we need to link obj.expr to exprFunc, eg: + //invar2 := &interfaces.UnificationInvariant{ + // Expr: exprFunc, // trueCallee variant + // Expect: typFunc, + // Actual: typFn, + //} + //invariants = append(invariants, invar2) } - return invariants, nil + // if len(exprFunc.Values) > 0 + for _, fn := range exprFunc.Values { + _ = fn + panic("not implemented") // XXX: not implemented! + } + + return typExpr, invariants, nil +} + +// Check is checking that the input type is equal to the object that Check is +// running on. In doing so, it adds any invariants that are necessary. Check +// must always call Infer to produce the invariant. The implementation can be +// generic for all expressions. +func (obj *ExprCall) Check(typ *types.Type) ([]*interfaces.UnificationInvariant, error) { + return interfaces.GenericCheck(obj, typ) } // Graph returns the reactive function graph which is expressed by this node. It @@ -8846,42 +8762,45 @@ func (obj *ExprVar) Type() (*types.Type, error) { return obj.typ, nil } -// Unify returns the list of invariants that this node produces. It recursively -// calls Unify on any children elements that exist in the AST, and returns the -// collection to the caller. -func (obj *ExprVar) Unify() ([]interfaces.Invariant, error) { - var invariants []interfaces.Invariant - +// Infer returns the type of itself and a collection of invariants. The returned +// type may contain unification variables. It collects the invariants by calling +// Check on its children expressions. In making those calls, it passes in the +// known type for that child to get it to "Check" it. When the type is not +// known, it should create a new unification variable to pass in to the child +// Check calls. Infer usually only calls Check on things inside of it, and often +// does not call another Infer. This Infer is an exception to that pattern. +func (obj *ExprVar) Infer() (*types.Type, []*interfaces.UnificationInvariant, error) { // lookup value from scope expr, exists := obj.scope.Variables[obj.Name] if !exists { - return nil, fmt.Errorf("var `%s` does not exist in this scope", obj.Name) + return nil, nil, fmt.Errorf("var `%s` does not exist in this scope", obj.Name) } - // if this was set explicitly by the parser - if obj.typ != nil { - invar := &interfaces.EqualsInvariant{ - Expr: obj, - Type: obj.typ, - } - invariants = append(invariants, invar) - } + // This child call to Infer is an outlier to the common pattern where + // "Infer does not call Infer". We really want the indirection here. - invars, err := expr.Unify() + typ, invariants, err := expr.Infer() // this is usually a top level expr if err != nil { - return nil, err + return nil, nil, err } - invariants = append(invariants, invars...) - // this expression's type must be the type of what the var is bound to! - // TODO: does this always cause an identical duplicate invariant? - invar := &interfaces.EqualityInvariant{ - Expr1: obj, - Expr2: expr, + // This adds the obj ptr, so it's seen as an expr that we need to solve. + invar := &interfaces.UnificationInvariant{ + Expr: obj, + Expect: typ, + Actual: typ, } invariants = append(invariants, invar) - return invariants, nil + return typ, invariants, nil +} + +// Check is checking that the input type is equal to the object that Check is +// running on. In doing so, it adds any invariants that are necessary. Check +// must always call Infer to produce the invariant. The implementation can be +// generic for all expressions. +func (obj *ExprVar) Check(typ *types.Type) ([]*interfaces.UnificationInvariant, error) { + return interfaces.GenericCheck(obj, typ) } // Graph returns the reactive function graph which is expressed by this node. It @@ -8949,8 +8868,9 @@ func (obj *ExprVar) Value() (types.Value, error) { // ExprParam represents a parameter to a function. type ExprParam struct { + typ *types.Type + Name string // name of the parameter - Typ *types.Type } // String returns a short representation of this expression. @@ -8976,8 +8896,8 @@ func (obj *ExprParam) Init(*interfaces.Data) error { // on any child elements and builds the new node with those new node contents. func (obj *ExprParam) Interpolate() (interfaces.Expr, error) { return &ExprParam{ + typ: obj.typ, Name: obj.Name, - Typ: obj.Typ, }, nil } @@ -8988,8 +8908,8 @@ func (obj *ExprParam) Interpolate() (interfaces.Expr, error) { // and they won't be able to have different values. func (obj *ExprParam) Copy() (interfaces.Expr, error) { return &ExprParam{ + typ: obj.typ, Name: obj.Name, - Typ: obj.Typ, }, nil } @@ -9032,10 +8952,26 @@ func (obj *ExprParam) SetScope(scope *interfaces.Scope, sctx map[string]interfac // change on expressions, if you attempt to set a different type than what has // previously been set (when not initially known) this will error. func (obj *ExprParam) SetType(typ *types.Type) error { - if obj.Typ != nil { - return obj.Typ.Cmp(typ) // if not set, ensure it doesn't change + if obj.typ != nil { + if obj.typ.Cmp(typ) == nil { // if not set, ensure it doesn't change + return nil + } + + // Redundant: just as expensive as running UnifyCmp below and it + // would fail in that case since we did the above Cmp anyways... + //if !obj.typ.HasUni() { + // return err // err from above obj.Typ + //} + + // Here, obj.typ might be a unification variable, so if we're + // setting it to overwrite it, we need to at least make sure + // that it's compatible. + if err := unificationUtil.UnifyCmp(obj.typ, typ); err != nil { + return err + } + //obj.typ = typ // fallthrough below and set } - obj.Typ = typ // set + obj.typ = typ // set return nil } @@ -9043,28 +8979,56 @@ func (obj *ExprParam) SetType(typ *types.Type) error { func (obj *ExprParam) Type() (*types.Type, error) { // Return the type if it is already known statically... It is useful for // type unification to have some extra info early. - if obj.Typ == nil { + if obj.typ == nil { return nil, interfaces.ErrTypeCurrentlyUnknown } - return obj.Typ, nil + return obj.typ, nil } -// Unify returns the list of invariants that this node produces. It recursively -// calls Unify on any children elements that exist in the AST, and returns the -// collection to the caller. -func (obj *ExprParam) Unify() ([]interfaces.Invariant, error) { - var invariants []interfaces.Invariant +// Infer returns the type of itself and a collection of invariants. The returned +// type may contain unification variables. It collects the invariants by calling +// Check on its children expressions. In making those calls, it passes in the +// known type for that child to get it to "Check" it. When the type is not +// known, it should create a new unification variable to pass in to the child +// Check calls. Infer usually only calls Check on things inside of it, and often +// does not call another Infer. This Infer returns a quasi-equivalent to my +// ExprAny invariant idea. +func (obj *ExprParam) Infer() (*types.Type, []*interfaces.UnificationInvariant, error) { + invariants := []*interfaces.UnificationInvariant{} - // if this was set explicitly by the parser - if obj.Typ != nil { - invar := &interfaces.EqualsInvariant{ - Expr: obj, - Type: obj.Typ, + // We know this has to be something, but we don't know what. Return + // anything, just like my ExprAny invariant would have. + typ := obj.typ + if obj.typ == nil { // XXX: is this correct? + typ = &types.Type{ + Kind: types.KindUnification, + Uni: types.NewElem(), // unification variable, eg: ?1 + } + + // XXX: Every time we call ExprParam.Infer it is generating a + // new unification variable... So we want ?1 the first time, ?2 + // the second... but we never get ?1 solved... SO we want to + // cache this so it only happens once I think. + obj.typ = typ // cache for now + + // This adds the obj ptr, so it's seen as an expr that we need to solve. + invar := &interfaces.UnificationInvariant{ + Expr: obj, + Expect: typ, + Actual: typ, } invariants = append(invariants, invar) } - return invariants, nil + return typ, invariants, nil +} + +// Check is checking that the input type is equal to the object that Check is +// running on. In doing so, it adds any invariants that are necessary. Check +// must always call Infer to produce the invariant. The implementation can be +// generic for all expressions. +func (obj *ExprParam) Check(typ *types.Type) ([]*interfaces.UnificationInvariant, error) { + return interfaces.GenericCheck(obj, typ) } // Graph returns the reactive function graph which is expressed by this node. It @@ -9172,11 +9136,23 @@ func (obj *ExprPoly) Type() (*types.Type, error) { return nil, interfaces.ErrTypeCurrentlyUnknown } -// Unify returns the list of invariants that this node produces. It recursively -// calls Unify on any children elements that exist in the AST, and returns the -// collection to the caller. -func (obj *ExprPoly) Unify() ([]interfaces.Invariant, error) { - panic("ExprPoly.Unify(): should not happen, all ExprPoly expressions should be gone by the time type-checking starts") +// Infer returns the type of itself and a collection of invariants. The returned +// type may contain unification variables. It collects the invariants by calling +// Check on its children expressions. In making those calls, it passes in the +// known type for that child to get it to "Check" it. When the type is not +// known, it should create a new unification variable to pass in to the child +// Check calls. Infer usually only calls Check on things inside of it, and often +// does not call another Infer. This Infer should never be called. +func (obj *ExprPoly) Infer() (*types.Type, []*interfaces.UnificationInvariant, error) { + panic("ExprPoly.Infer(): should not happen, all ExprPoly expressions should be gone by the time type-checking starts") +} + +// Check is checking that the input type is equal to the object that Check is +// running on. In doing so, it adds any invariants that are necessary. Check +// must always call Infer to produce the invariant. The implementation can be +// generic for all expressions. +func (obj *ExprPoly) Check(typ *types.Type) ([]*interfaces.UnificationInvariant, error) { + return interfaces.GenericCheck(obj, typ) } // Graph returns the reactive function graph which is expressed by this node. It @@ -9329,32 +9305,36 @@ func (obj *ExprTopLevel) Type() (*types.Type, error) { return obj.Definition.Type() } -// Unify returns the list of invariants that this node produces. It recursively -// calls Unify on any children elements that exist in the AST, and returns the -// collection to the caller. -func (obj *ExprTopLevel) Unify() ([]interfaces.Invariant, error) { - var invariants []interfaces.Invariant - var invar interfaces.Invariant - - invars, err := obj.Definition.Unify() +// Infer returns the type of itself and a collection of invariants. The returned +// type may contain unification variables. It collects the invariants by calling +// Check on its children expressions. In making those calls, it passes in the +// known type for that child to get it to "Check" it. When the type is not +// known, it should create a new unification variable to pass in to the child +// Check calls. Infer usually only calls Check on things inside of it, and often +// does not call another Infer. This Infer is an exception to that pattern. +func (obj *ExprTopLevel) Infer() (*types.Type, []*interfaces.UnificationInvariant, error) { + typ, invariants, err := obj.Definition.Infer() if err != nil { - return nil, err + return nil, nil, err } - invariants = append(invariants, invars...) - invar = &interfaces.EqualityInvariant{ - Expr1: obj, - Expr2: obj.Definition, + // This adds the obj ptr, so it's seen as an expr that we need to solve. + invar := &interfaces.UnificationInvariant{ + Expr: obj, + Expect: typ, + Actual: typ, } invariants = append(invariants, invar) - // We don't want this to have it's SetType run in the unified solution. - invar = &interfaces.SkipInvariant{ - Expr: obj, - } - invariants = append(invariants, invar) + return typ, invariants, nil +} - return invariants, nil +// Check is checking that the input type is equal to the object that Check is +// running on. In doing so, it adds any invariants that are necessary. Check +// must always call Infer to produce the invariant. The implementation can be +// generic for all expressions. +func (obj *ExprTopLevel) Check(typ *types.Type) ([]*interfaces.UnificationInvariant, error) { + return interfaces.GenericCheck(obj, typ) } // Graph returns the reactive function graph which is expressed by this node. It @@ -9388,6 +9368,7 @@ func (obj *ExprTopLevel) Value() (types.Value, error) { type ExprSingleton struct { Definition interfaces.Expr + singletonType *types.Type singletonGraph *pgraph.Graph singletonExpr interfaces.Func mutex *sync.Mutex // protects singletonGraph and singletonExpr @@ -9428,6 +9409,7 @@ func (obj *ExprSingleton) Interpolate() (interfaces.Expr, error) { return &ExprSingleton{ Definition: definition, + singletonType: nil, // each copy should have its own Type singletonGraph: nil, // each copy should have its own Graph singletonExpr: nil, // each copy should have its own Func mutex: &sync.Mutex{}, @@ -9443,6 +9425,7 @@ func (obj *ExprSingleton) Copy() (interfaces.Expr, error) { return &ExprSingleton{ Definition: definition, + singletonType: nil, // each copy should have its own Type singletonGraph: nil, // each copy should have its own Graph singletonExpr: nil, // each copy should have its own Func mutex: &sync.Mutex{}, @@ -9509,32 +9492,47 @@ func (obj *ExprSingleton) Type() (*types.Type, error) { return obj.Definition.Type() } -// Unify returns the list of invariants that this node produces. It recursively -// calls Unify on any children elements that exist in the AST, and returns the -// collection to the caller. -func (obj *ExprSingleton) Unify() ([]interfaces.Invariant, error) { - var invariants []interfaces.Invariant - var invar interfaces.Invariant +// Infer returns the type of itself and a collection of invariants. The returned +// type may contain unification variables. It collects the invariants by calling +// Check on its children expressions. In making those calls, it passes in the +// known type for that child to get it to "Check" it. When the type is not +// known, it should create a new unification variable to pass in to the child +// Check calls. Infer usually only calls Check on things inside of it, and often +// does not call another Infer. This Infer is an exception to that pattern. +func (obj *ExprSingleton) Infer() (*types.Type, []*interfaces.UnificationInvariant, error) { + // shouldn't run in parallel... + //obj.mutex.Lock() + //defer obj.mutex.Unlock() - invars, err := obj.Definition.Unify() - if err != nil { - return nil, err + if obj.singletonType == nil { + typ, invariants, err := obj.Definition.Infer() + if err != nil { + return nil, nil, err + } + obj.singletonType = typ + + // This adds the obj ptr, so it's seen as an expr that we need + // to solve. + invar := &interfaces.UnificationInvariant{ + Expr: obj, + Expect: typ, + Actual: typ, + } + invariants = append(invariants, invar) + + return obj.singletonType, invariants, nil } - invariants = append(invariants, invars...) - invar = &interfaces.EqualityInvariant{ - Expr1: obj, - Expr2: obj.Definition, - } - invariants = append(invariants, invar) + // We only need to return the invariants the first time, as done above! + return obj.singletonType, []*interfaces.UnificationInvariant{}, nil +} - // We don't want this to have it's SetType run in the unified solution. - invar = &interfaces.SkipInvariant{ - Expr: obj, - } - invariants = append(invariants, invar) - - return invariants, nil +// Check is checking that the input type is equal to the object that Check is +// running on. In doing so, it adds any invariants that are necessary. Check +// must always call Infer to produce the invariant. The implementation can be +// generic for all expressions. +func (obj *ExprSingleton) Check(typ *types.Type) ([]*interfaces.UnificationInvariant, error) { + return interfaces.GenericCheck(obj, typ) } // Graph returns the reactive function graph which is expressed by this node. It @@ -9839,68 +9837,65 @@ func (obj *ExprIf) Type() (*types.Type, error) { return nil, interfaces.ErrTypeCurrentlyUnknown } -// Unify returns the list of invariants that this node produces. It recursively -// calls Unify on any children elements that exist in the AST, and returns the -// collection to the caller. -func (obj *ExprIf) Unify() ([]interfaces.Invariant, error) { - var invariants []interfaces.Invariant +// Infer returns the type of itself and a collection of invariants. The returned +// type may contain unification variables. It collects the invariants by calling +// Check on its children expressions. In making those calls, it passes in the +// known type for that child to get it to "Check" it. When the type is not +// known, it should create a new unification variable to pass in to the child +// Check calls. Infer usually only calls Check on things inside of it, and often +// does not call another Infer. +func (obj *ExprIf) Infer() (*types.Type, []*interfaces.UnificationInvariant, error) { + invariants := []*interfaces.UnificationInvariant{} - // if this was set explicitly by the parser + conditionInvars, err := obj.Condition.Check(types.TypeBool) // bool, yes! + if err != nil { + return nil, nil, err + } + invariants = append(invariants, conditionInvars...) + + // Same unification var because both branches must have the same type. + typExpr := &types.Type{ + Kind: types.KindUnification, + Uni: types.NewElem(), // unification variable, eg: ?1 + } + + thenInvars, err := obj.ThenBranch.Check(typExpr) + if err != nil { + return nil, nil, err + } + invariants = append(invariants, thenInvars...) + + elseInvars, err := obj.ElseBranch.Check(typExpr) + if err != nil { + return nil, nil, err + } + invariants = append(invariants, elseInvars...) + + // Every infer call must have this section, because expr var needs this. + typType := typExpr + //if obj.typ == nil { // optional says sam + // obj.typ = typExpr // sam says we could unconditionally do this + //} if obj.typ != nil { - invar := &interfaces.EqualsInvariant{ - Expr: obj, - Type: obj.typ, - } - invariants = append(invariants, invar) + typType = obj.typ } + // This must be added even if redundant, so that we collect the obj ptr. + invar := &interfaces.UnificationInvariant{ + Expr: obj, + Expect: typExpr, // This is the type that we return. + Actual: typType, + } + invariants = append(invariants, invar) - // conditional expression might have some children invariants to share - condition, err := obj.Condition.Unify() - if err != nil { - return nil, err - } - invariants = append(invariants, condition...) + return typExpr, invariants, nil +} - // the condition must ultimately be a boolean - conditionInvar := &interfaces.EqualsInvariant{ - Expr: obj.Condition, - Type: types.TypeBool, - } - invariants = append(invariants, conditionInvar) - - // recurse into the two branches - thenBranch, err := obj.ThenBranch.Unify() - if err != nil { - return nil, err - } - invariants = append(invariants, thenBranch...) - - elseBranch, err := obj.ElseBranch.Unify() - if err != nil { - return nil, err - } - invariants = append(invariants, elseBranch...) - - // the two branches must be equally typed - branchesInvar := &interfaces.EqualityInvariant{ - Expr1: obj.ThenBranch, - Expr2: obj.ElseBranch, - } - invariants = append(invariants, branchesInvar) - - // the two branches must match the type of the whole expression - thenInvar := &interfaces.EqualityInvariant{ - Expr1: obj, - Expr2: obj.ThenBranch, - } - invariants = append(invariants, thenInvar) - elseInvar := &interfaces.EqualityInvariant{ - Expr1: obj, - Expr2: obj.ElseBranch, - } - invariants = append(invariants, elseInvar) - - return invariants, nil +// Check is checking that the input type is equal to the object that Check is +// running on. In doing so, it adds any invariants that are necessary. Check +// must always call Infer to produce the invariant. The implementation can be +// generic for all expressions. +func (obj *ExprIf) Check(typ *types.Type) ([]*interfaces.UnificationInvariant, error) { + return interfaces.GenericCheck(obj, typ) } // Func returns a function which returns the correct branch based on the ever @@ -10018,7 +10013,6 @@ func getScope(node interfaces.Expr) (*interfaces.Scope, error) { case *ExprIf: return expr.scope, nil - //case *ExprAny: // unexpected! default: return nil, fmt.Errorf("unexpected: %+v", node) } diff --git a/lang/ast/util.go b/lang/ast/util.go index 7399f5d9..bdeac99b 100644 --- a/lang/ast/util.go +++ b/lang/ast/util.go @@ -36,7 +36,6 @@ import ( "github.com/purpleidea/mgmt/lang/funcs" "github.com/purpleidea/mgmt/lang/funcs/simple" - "github.com/purpleidea/mgmt/lang/funcs/simplepoly" "github.com/purpleidea/mgmt/lang/funcs/vars" "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/types" @@ -66,15 +65,15 @@ func FuncPrefixToFunctionsScope(prefix string) map[string]interfaces.Expr { continue } - if st, ok := x.(*simplepoly.WrappedFunc); simplepoly.DirectInterface && ok { - fn := &ExprFunc{ - Title: name, + //if st, ok := x.(*simplepoly.WrappedFunc); simplepoly.DirectInterface && ok { + // fn := &ExprFunc{ + // Title: name, - Values: st.Fns, - } - exprs[name] = fn - continue - } + // Values: st.Fns, + // } + // exprs[name] = fn + // continue + //} fn := &ExprFunc{ Title: name, diff --git a/lang/core/concat_func.go b/lang/core/concat_func.go index 023a7a71..93b8b423 100644 --- a/lang/core/concat_func.go +++ b/lang/core/concat_func.go @@ -43,9 +43,9 @@ const ( ) func init() { - simple.Register(ConcatFuncName, &types.FuncValue{ + simple.Register(ConcatFuncName, &simple.Scaffold{ T: types.NewType("func(a str, b str) str"), - V: Concat, + F: Concat, }) } diff --git a/lang/core/convert/format_bool.go b/lang/core/convert/format_bool.go index 0bf445b4..3c69de64 100644 --- a/lang/core/convert/format_bool.go +++ b/lang/core/convert/format_bool.go @@ -38,9 +38,9 @@ import ( ) func init() { - simple.ModuleRegister(ModuleName, "format_bool", &types.FuncValue{ + simple.ModuleRegister(ModuleName, "format_bool", &simple.Scaffold{ T: types.NewType("func(a bool) str"), - V: FormatBool, + F: FormatBool, }) } diff --git a/lang/core/convert/parse_bool.go b/lang/core/convert/parse_bool.go index fc490f2e..29b1248b 100644 --- a/lang/core/convert/parse_bool.go +++ b/lang/core/convert/parse_bool.go @@ -39,9 +39,9 @@ import ( ) func init() { - simple.ModuleRegister(ModuleName, "parse_bool", &types.FuncValue{ + simple.ModuleRegister(ModuleName, "parse_bool", &simple.Scaffold{ T: types.NewType("func(a str) bool"), - V: ParseBool, + F: ParseBool, }) } diff --git a/lang/core/convert/to_float.go b/lang/core/convert/to_float.go index 959dcbf5..71c0ef0f 100644 --- a/lang/core/convert/to_float.go +++ b/lang/core/convert/to_float.go @@ -37,9 +37,9 @@ import ( ) func init() { - simple.ModuleRegister(ModuleName, "to_float", &types.FuncValue{ + simple.ModuleRegister(ModuleName, "to_float", &simple.Scaffold{ T: types.NewType("func(a int) float"), - V: ToFloat, + F: ToFloat, }) } diff --git a/lang/core/convert/to_int.go b/lang/core/convert/to_int.go index 6eaf67bc..3cc414b8 100644 --- a/lang/core/convert/to_int.go +++ b/lang/core/convert/to_int.go @@ -37,9 +37,9 @@ import ( ) func init() { - simple.ModuleRegister(ModuleName, "to_int", &types.FuncValue{ + simple.ModuleRegister(ModuleName, "to_int", &simple.Scaffold{ T: types.NewType("func(a float) int"), - V: ToInt, + F: ToInt, }) } diff --git a/lang/core/convert/to_str.go b/lang/core/convert/to_str.go index 00b782b8..ca77f59b 100644 --- a/lang/core/convert/to_str.go +++ b/lang/core/convert/to_str.go @@ -38,14 +38,14 @@ import ( ) func init() { - simple.ModuleRegister(ModuleName, "int_to_str", &types.FuncValue{ + simple.ModuleRegister(ModuleName, "int_to_str", &simple.Scaffold{ T: types.NewType("func(a int) str"), - V: IntToStr, + F: IntToStr, }) // TODO: more complicated because of precision, etc... - //simple.ModuleRegister(ModuleName, "float_to_str", &types.FuncValue{ + //simple.ModuleRegister(ModuleName, "float_to_str", &simple.Scaffold{ // T: types.NewType("func(a float, ???) str"), - // V: FloatToStr, + // F: FloatToStr, //}) } diff --git a/lang/core/core_test.go b/lang/core/core_test.go index 10c5c632..9612c2f4 100644 --- a/lang/core/core_test.go +++ b/lang/core/core_test.go @@ -526,18 +526,32 @@ func TestLiveFuncExec0(t *testing.T) { t.Errorf("test #%d: func lookup failed with: %+v", index, err) return } + sig := handle.Info().Sig + if sig.Kind != types.KindFunc { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: must be kind func", index) + return + } + if sig.HasUni() { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: func contains unification vars", index) + return + } + + if buildableFunc, ok := handle.(interfaces.BuildableFunc); ok { + if _, err := buildableFunc.Build(sig); err != nil { + t.Errorf("test #%d: FAIL", index) + t.Errorf("test #%d: can't build function: %v", index, err) + return + } + } + // run validate! if err := handle.Validate(); err != nil { t.Errorf("test #%d: FAIL", index) t.Errorf("test #%d: could not validate Func: %+v", index, err) return } - sig := handle.Info().Sig - if sig.Kind != types.KindFunc { - t.Errorf("test #%d: FAIL", index) - t.Errorf("test #%d: must be kind func: %+v", index, err) - return - } input := make(chan types.Value) // we close this when we're done output := make(chan types.Value) // we create it, func closes it diff --git a/lang/core/datetime/format_func.go b/lang/core/datetime/format_func.go index 6db46985..5f088696 100644 --- a/lang/core/datetime/format_func.go +++ b/lang/core/datetime/format_func.go @@ -39,9 +39,9 @@ import ( ) func init() { - simple.ModuleRegister(ModuleName, "format", &types.FuncValue{ + simple.ModuleRegister(ModuleName, "format", &simple.Scaffold{ T: types.NewType("func(a int, b str) str"), - V: Format, + F: Format, }) } diff --git a/lang/core/datetime/hour_func.go b/lang/core/datetime/hour_func.go index 7beeb4f8..00511787 100644 --- a/lang/core/datetime/hour_func.go +++ b/lang/core/datetime/hour_func.go @@ -39,9 +39,9 @@ import ( ) func init() { - simple.ModuleRegister(ModuleName, "hour", &types.FuncValue{ + simple.ModuleRegister(ModuleName, "hour", &simple.Scaffold{ T: types.NewType("func(a int) int"), - V: Hour, + F: Hour, }) } diff --git a/lang/core/datetime/print_func.go b/lang/core/datetime/print_func.go index e1d3c61d..ed1008d2 100644 --- a/lang/core/datetime/print_func.go +++ b/lang/core/datetime/print_func.go @@ -40,9 +40,9 @@ import ( func init() { // FIXME: consider renaming this to printf, and add in a format string? - simple.ModuleRegister(ModuleName, "print", &types.FuncValue{ + simple.ModuleRegister(ModuleName, "print", &simple.Scaffold{ T: types.NewType("func(a int) str"), - V: func(ctx context.Context, input []types.Value) (types.Value, error) { + F: func(ctx context.Context, input []types.Value) (types.Value, error) { epochDelta := input[0].Int() if epochDelta < 0 { return nil, fmt.Errorf("epoch delta must be positive") diff --git a/lang/core/datetime/weekday_func.go b/lang/core/datetime/weekday_func.go index 62e905e6..bf938b9a 100644 --- a/lang/core/datetime/weekday_func.go +++ b/lang/core/datetime/weekday_func.go @@ -40,9 +40,9 @@ import ( ) func init() { - simple.ModuleRegister(ModuleName, "weekday", &types.FuncValue{ + simple.ModuleRegister(ModuleName, "weekday", &simple.Scaffold{ T: types.NewType("func(a int) str"), - V: Weekday, + F: Weekday, }) } diff --git a/lang/core/embedded/provisioner/provisioner.go b/lang/core/embedded/provisioner/provisioner.go index d7eb6233..5463f35c 100644 --- a/lang/core/embedded/provisioner/provisioner.go +++ b/lang/core/embedded/provisioner/provisioner.go @@ -46,7 +46,7 @@ import ( "github.com/purpleidea/mgmt/lang/embedded" "github.com/purpleidea/mgmt/lang/funcs/simple" "github.com/purpleidea/mgmt/lang/types" - "github.com/purpleidea/mgmt/lang/unification/simplesolver" // TODO: remove me! + "github.com/purpleidea/mgmt/lang/unification/fastsolver" "github.com/purpleidea/mgmt/util" "github.com/purpleidea/mgmt/util/errwrap" "github.com/purpleidea/mgmt/util/password" @@ -394,12 +394,12 @@ func (obj *provisioner) Customize(a interface{}) (*cli.RunArgs, error) { runArgs.RunLang.SkipUnify = false // can't skip if we only unify } - name := simplesolver.Name + name := fastsolver.Name // TODO: Remove these optimizations when the solver is faster overall. runArgs.RunLang.UnifySolver = &name - runArgs.RunLang.UnifyOptimizations = []string{ - simplesolver.OptimizationSkipFuncCmp, - } + //runArgs.RunLang.UnifyOptimizations = []string{ + // fastsolver.TODO, + //} libConfig.TmpPrefix = true libConfig.NoPgp = true @@ -416,9 +416,9 @@ func (obj *provisioner) Register(moduleName string) error { } // Build a few separately... - simple.ModuleRegister(moduleName, "cli_password", &types.FuncValue{ + simple.ModuleRegister(moduleName, "cli_password", &simple.Scaffold{ T: types.NewType("func() str"), - V: func(ctx context.Context, input []types.Value) (types.Value, error) { + F: func(ctx context.Context, input []types.Value) (types.Value, error) { if obj.localArgs == nil { // programming error return nil, fmt.Errorf("could not convert/access our struct") diff --git a/lang/core/example/answer_func.go b/lang/core/example/answer_func.go index 29a41e99..7dca9cdd 100644 --- a/lang/core/example/answer_func.go +++ b/lang/core/example/answer_func.go @@ -40,9 +40,9 @@ import ( const Answer = 42 func init() { - simple.ModuleRegister(ModuleName, "answer", &types.FuncValue{ + simple.ModuleRegister(ModuleName, "answer", &simple.Scaffold{ T: types.NewType("func() int"), - V: func(context.Context, []types.Value) (types.Value, error) { + F: func(context.Context, []types.Value) (types.Value, error) { return &types.IntValue{V: Answer}, nil }, }) diff --git a/lang/core/example/errorbool_func.go b/lang/core/example/errorbool_func.go index 7b5277d8..ae4c58a0 100644 --- a/lang/core/example/errorbool_func.go +++ b/lang/core/example/errorbool_func.go @@ -38,9 +38,9 @@ import ( ) func init() { - simple.ModuleRegister(ModuleName, "errorbool", &types.FuncValue{ + simple.ModuleRegister(ModuleName, "errorbool", &simple.Scaffold{ T: types.NewType("func(a bool) str"), - V: func(ctx context.Context, input []types.Value) (types.Value, error) { + F: func(ctx context.Context, input []types.Value) (types.Value, error) { if input[0].Bool() { return nil, fmt.Errorf("we errored on request") } diff --git a/lang/core/example/int2str_func.go b/lang/core/example/int2str_func.go index cf672374..3cad32b0 100644 --- a/lang/core/example/int2str_func.go +++ b/lang/core/example/int2str_func.go @@ -38,9 +38,9 @@ import ( ) func init() { - simple.ModuleRegister(ModuleName, "int2str", &types.FuncValue{ + simple.ModuleRegister(ModuleName, "int2str", &simple.Scaffold{ T: types.NewType("func(a int) str"), - V: func(ctx context.Context, input []types.Value) (types.Value, error) { + F: func(ctx context.Context, input []types.Value) (types.Value, error) { return &types.StrValue{ V: fmt.Sprintf("%d", input[0].Int()), }, nil diff --git a/lang/core/example/nested/hello_func.go b/lang/core/example/nested/hello_func.go index 3559f219..08f58fbb 100644 --- a/lang/core/example/nested/hello_func.go +++ b/lang/core/example/nested/hello_func.go @@ -38,9 +38,9 @@ import ( ) func init() { - simple.ModuleRegister(coreexample.ModuleName+"/"+ModuleName, "hello", &types.FuncValue{ + simple.ModuleRegister(coreexample.ModuleName+"/"+ModuleName, "hello", &simple.Scaffold{ T: types.NewType("func() str"), - V: Hello, + F: Hello, }) } diff --git a/lang/core/example/plus_func.go b/lang/core/example/plus_func.go index 76dcbc33..bea11147 100644 --- a/lang/core/example/plus_func.go +++ b/lang/core/example/plus_func.go @@ -37,9 +37,9 @@ import ( ) func init() { - simple.ModuleRegister(ModuleName, "plus", &types.FuncValue{ + simple.ModuleRegister(ModuleName, "plus", &simple.Scaffold{ T: types.NewType("func(y str, z str) str"), - V: Plus, + F: Plus, }) } diff --git a/lang/core/example/str2int_func.go b/lang/core/example/str2int_func.go index 1326ff5a..0942d656 100644 --- a/lang/core/example/str2int_func.go +++ b/lang/core/example/str2int_func.go @@ -38,9 +38,9 @@ import ( ) func init() { - simple.ModuleRegister(ModuleName, "str2int", &types.FuncValue{ + simple.ModuleRegister(ModuleName, "str2int", &simple.Scaffold{ T: types.NewType("func(a str) int"), - V: func(ctx context.Context, input []types.Value) (types.Value, error) { + F: func(ctx context.Context, input []types.Value) (types.Value, error) { var i int64 if val, err := strconv.ParseInt(input[0].Str(), 10, 64); err == nil { i = val diff --git a/lang/core/fmt/printf_func.go b/lang/core/fmt/printf_func.go index 2422d29e..7ef76d90 100644 --- a/lang/core/fmt/printf_func.go +++ b/lang/core/fmt/printf_func.go @@ -36,6 +36,7 @@ import ( "github.com/purpleidea/mgmt/lang/funcs" "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/types" + unificationUtil "github.com/purpleidea/mgmt/lang/unification/util" "github.com/purpleidea/mgmt/util" "github.com/purpleidea/mgmt/util/errwrap" ) @@ -49,15 +50,24 @@ const ( // argument (the format string) is not known statically at compile time. // The downside of this is that if it changes while we are running, it // could change from "hello %s" to "hello %d" or "%s %d...". If this - // happens we just generate ugly format strings, instead of preventing - // it all from even running at all. It's useful to allow dynamic strings - // if we were generating custom log messages (for example) where the - // format comes from a database lookup or similar. Of course if we knew - // that such a lookup could be done quickly and statically (maybe it's a - // read from a local key-value config file that's part of our deploy) - // then maybe we can do it before unification speculatively. + // happens we can generate ugly format strings, instead of preventing it + // from even running at all. The behaviour if this happens is determined + // by PrintfAllowFormatError. + // + // NOTE: It's useful to allow dynamic strings if we were generating + // custom log messages (for example) where the format comes from a + // database lookup or similar. Of course if we knew that such a lookup + // could be done quickly and statically (maybe it's a read from a local + // key-value config file that's part of our deploy) then maybe we can do + // it before unification speculatively. PrintfAllowNonStaticFormat = true + // PrintfAllowFormatError will cause the function to shutdown if it has + // an invalid format string. Otherwise this will cause the output of the + // function to return a garbled message. This is similar to golang's + // format errors, eg: https://pkg.go.dev/fmt#hdr-Format_errors + PrintfAllowFormatError = true + printfArgNameFormat = "format" // name of the first arg ) @@ -65,7 +75,7 @@ func init() { funcs.ModuleRegister(ModuleName, PrintfFuncName, func() interfaces.Func { return &PrintfFunc{} }) } -var _ interfaces.PolyFunc = &PrintfFunc{} // ensure it meets this expectation +var _ interfaces.InferableFunc = &PrintfFunc{} // ensure it meets this expectation // PrintfFunc is a static polymorphic function that compiles a format string and // returns the output as a string. It bases its output on the values passed in @@ -101,351 +111,124 @@ func (obj *PrintfFunc) ArgGen(index int) (string, error) { return util.NumToAlpha(index - 1), nil } -// Unify returns the list of invariants that this func produces. -func (obj *PrintfFunc) Unify(expr interfaces.Expr) ([]interfaces.Invariant, error) { - var invariants []interfaces.Invariant - var invar interfaces.Invariant +// FuncInfer takes partial type and value information from the call site of this +// function so that it can build an appropriate type signature for it. The type +// signature may include unification variables. +func (obj *PrintfFunc) FuncInfer(partialType *types.Type, partialValues []types.Value) (*types.Type, []*interfaces.UnificationInvariant, error) { + // func(format str, args... variant) string - // func(format string, args... variant) string - - formatName, err := obj.ArgGen(0) - if err != nil { - return nil, err + if len(partialValues) < 1 { + return nil, nil, fmt.Errorf("must have at least one arg") + } + if len(partialType.Map) < 1 { + // programming error? + return nil, nil, fmt.Errorf("must have at least one arg") + } + if typ := partialType.Map[partialType.Ord[0]]; typ != nil && typ.Cmp(types.TypeStr) != nil { + return nil, nil, fmt.Errorf("format string was a %s", typ) } - dummyFormat := &interfaces.ExprAny{} // corresponds to the format type - dummyOut := &interfaces.ExprAny{} // corresponds to the out string - - // format arg type of string - invar = &interfaces.EqualsInvariant{ - Expr: dummyFormat, - Type: types.TypeStr, + getType := func(i int) *types.Type { // get Nth type, doesn't bound check + if partialValues[i] != nil { + // We don't check that this is consistent with + // partialType, because that's a compiler job. + return partialValues[i].Type() // got it! + } + if partialType == nil || partialType.Map == nil { + return nil // no more information + } + return partialType.Map[partialType.Ord[i]] } - invariants = append(invariants, invar) - // return type of string - invar = &interfaces.EqualsInvariant{ - Expr: dummyOut, - Type: types.TypeStr, + getFormat := func() *string { + if partialValues[0] == nil { + return nil // no more information + } + typ := partialValues[0].Type() + if typ == nil || typ.Cmp(types.TypeStr) != nil { + return nil // no more information + } + + formatString := partialValues[0].Str() + return &formatString } - invariants = append(invariants, invar) - invar = &interfaces.EqualityInvariant{ - Expr1: dummyFormat, - Expr2: dummyOut, + typList := make([]*types.Type, len(partialValues)) // number of args at call site + + for i := range partialValues { // populate initial expected types + typList[i] = getType(i) // nil if missing } - invariants = append(invariants, invar) - // dynamic generator function for when the format string is dynamic - dynamicFn := 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 { + // Do we have type information from the format string? (If it exists!) + if format := getFormat(); format != nil { + // formatList doesn't contain zeroth arg in our typList! + formatList, err := parseFormatToTypeList(*format) + if err != nil { + return nil, nil, errwrap.Wrapf(err, "could not parse format string") + } + for i, x := range typList { + if i == 0 { // format string continue } - // did we find the mapping from us to ExprCall ? - if cfavInvar.Func != expr { + if x == nil { // nothing to check against + typList[i] = formatList[i-1] // use this! continue } - // cfavInvar.Expr is the ExprCall! (the return pointer) - // cfavInvar.Args are the args that ExprCall uses! - if len(cfavInvar.Args) == 0 { - return nil, fmt.Errorf("unable to build function with no args") + + // Assume x does not contain unification variables! + if x.HasUni() { + // programming error (did the compiler change?) + return nil, nil, errwrap.Wrapf(err, "programming error at arg index %d", i) } - - var invariants []interfaces.Invariant - var invar interfaces.Invariant - - // add the relationship to the format string arg - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Expr, - Expr2: dummyFormat, + if err := unificationUtil.UnifyCmp(x, formatList[i-1]); err != nil { + return nil, nil, errwrap.Wrapf(err, "inconsistent type at arg index %d", i) } - invariants = append(invariants, invar) - - // add the relationship to the returned value - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Expr, - Expr2: dummyOut, - } - invariants = append(invariants, invar) - - // add the relationships to the called args - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Args[0], - Expr2: dummyFormat, - } - invariants = append(invariants, invar) - - // first arg must be a string - invar = &interfaces.EqualsInvariant{ - Expr: cfavInvar.Args[0], - Type: types.TypeStr, - } - invariants = append(invariants, invar) - - // full function - mapped := make(map[string]interfaces.Expr) - ordered := []string{} - for i, x := range cfavInvar.Args { - argName, err := obj.ArgGen(i) - if err != nil { - return nil, err - } - - dummyArg := &interfaces.ExprAny{} - if i == 0 { - dummyArg = dummyFormat // use parent one - } - - invar = &interfaces.EqualityInvariant{ - Expr1: x, // cfavInvar.Args[i] - Expr2: dummyArg, - } - invariants = append(invariants, invar) - - mapped[argName] = dummyArg - ordered = append(ordered, argName) - } - - invar = &interfaces.EqualityWrapFuncInvariant{ - Expr1: expr, // maps directly to us! - Expr2Map: mapped, - Expr2Ord: ordered, - Expr2Out: dummyOut, - } - invariants = append(invariants, invar) - - // 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 + // Less general version of the above... + //if err := x.Cmp(formatList[i-1]); err != nil { + // return nil, nil, errwrap.Wrapf(err, "inconsistent type at arg index %d", i) + //} } - // We couldn't tell the solver anything it didn't already know! - return nil, fmt.Errorf("couldn't generate new invariants") + } else if !PrintfAllowNonStaticFormat { + return nil, nil, fmt.Errorf("format string is not known statically") } - // 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 len(cfavInvar.Args) == 0 { - return nil, fmt.Errorf("unable to build function with no args") - } - - var invariants []interfaces.Invariant - var invar interfaces.Invariant - - // add the relationship to the format string arg - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Expr, - Expr2: dummyFormat, - } - invariants = append(invariants, invar) - - // add the relationship to the returned value - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Expr, - Expr2: dummyOut, - } - invariants = append(invariants, invar) - - // add the relationships to the called args - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Args[0], - Expr2: dummyFormat, - } - invariants = append(invariants, invar) - - // first arg must be a string - invar = &interfaces.EqualsInvariant{ - Expr: cfavInvar.Args[0], - Type: types.TypeStr, - } - invariants = append(invariants, invar) - - // Here we try to see if we know the format string - // statically. Perhaps more future Value() invocations - // on simple functions will also return before - // unification and before the function engine runs. - // If we happen to know the value, that's great and we - // can unify very easily. If we don't, then we can - // decide if we want to allow dynamic format strings. - value, err := cfavInvar.Args[0].Value() // is it known? - if err != nil { - if PrintfAllowNonStaticFormat { - return dynamicFn(fnInvariants, solved) - } - return nil, fmt.Errorf("format string is not known statically") - } - - if k := value.Type().Kind; k != types.KindStr { - return nil, fmt.Errorf("unable to build function with 0th arg of kind: %s", k) - } - format := value.Str() // must not panic - typList, err := parseFormatToTypeList(format) - if err != nil { - return nil, errwrap.Wrapf(err, "could not parse format string") - } - - // full function - mapped := make(map[string]interfaces.Expr) - ordered := []string{formatName} - mapped[formatName] = dummyFormat - - for i, x := range typList { - argName, err := obj.ArgGen(i + 1) // skip 0th - if err != nil { - return nil, err - } - if argName == printfArgNameFormat { - return nil, fmt.Errorf("could not build function with %d args", i+1) // +1 for format arg - } - - dummyArg := &interfaces.ExprAny{} - // if it's a variant, we can't add the invariant - if x != types.TypeVariant { - invar = &interfaces.EqualsInvariant{ - Expr: dummyArg, - Type: x, - } - invariants = append(invariants, invar) - } - - // catch situations like `printf("%d%d", 42)` - if len(cfavInvar.Args) <= i+1 { - return nil, fmt.Errorf("more specifiers (%d) than values (%d)", len(typList), len(cfavInvar.Args)-1) - } - - // add the relationships to the called args - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Args[i+1], - Expr2: dummyArg, - } - invariants = append(invariants, invar) - - mapped[argName] = dummyArg - ordered = append(ordered, argName) - } - - invar = &interfaces.EqualityWrapFuncInvariant{ - Expr1: expr, // maps directly to us! - Expr2Map: mapped, - Expr2Ord: ordered, - Expr2Out: dummyOut, - } - invariants = append(invariants, invar) - - // 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") + // Check the format string is consistent with what we've found earlier! + if i := 0; typList[i] != nil && typList[i].Cmp(types.TypeStr) != nil { + return nil, nil, fmt.Errorf("inconsistent type at arg index %d (format string)", i) } - invar = &interfaces.GeneratorInvariant{ - Func: fn, - } - invariants = append(invariants, invar) + typList[0] = types.TypeStr // format string (zeroth arg) - return invariants, nil -} - -// Polymorphisms returns the possible type signature for this function. In this -// case, since the number of arguments can be infinite, it returns the final -// precise type if it can be gleamed from the format argument. If it cannot, it -// is because either the format argument was not known statically, or because it -// had an invalid format string. -// XXX: This version of the function does not handle any variants returned from -// the parseFormatToTypeList helper function. -func (obj *PrintfFunc) Polymorphisms(partialType *types.Type, partialValues []types.Value) ([]*types.Type, error) { - if partialType == nil || len(partialValues) < 1 { - return nil, fmt.Errorf("first argument must be a static format string") - } - - if partialType.Out != nil && partialType.Out.Cmp(types.TypeStr) != nil { - return nil, fmt.Errorf("return value of printf must be str") - } - - ord := partialType.Ord - if partialType.Map != nil { - if len(ord) < 1 { - return nil, fmt.Errorf("must have at least one arg in printf func") - } - if t, exists := partialType.Map[ord[0]]; exists && t != nil { - if t.Cmp(types.TypeStr) != nil { - return nil, fmt.Errorf("first arg for printf must be an str") - } - } - } - - // FIXME: we'd like to pre-compute the interpolation if we can, so that - // we can run this code properly... for now, we can't, so it's a compile - // time error... - if partialValues[0] == nil { - return nil, fmt.Errorf("could not determine type from format string") - } - - format := partialValues[0].Str() // must not panic - typList, err := parseFormatToTypeList(format) - if err != nil { - return nil, errwrap.Wrapf(err, "could not parse format string") - } - - typ := &types.Type{ - Kind: types.KindFunc, // function type - Map: make(map[string]*types.Type), - Ord: []string{}, - Out: types.TypeStr, - } - // add first arg - typ.Map[printfArgNameFormat] = types.TypeStr - typ.Ord = append(typ.Ord, printfArgNameFormat) + mapped := map[string]*types.Type{} + ordered := []string{} for i, x := range typList { - name := util.NumToAlpha(i) // start with a... - if name == printfArgNameFormat { - return nil, fmt.Errorf("could not build function with %d args", i+1) // +1 for format arg + argName, err := obj.ArgGen(i) + if err != nil { + return nil, nil, err } - // if we also had even more partial type information, check it! - if t, exists := partialType.Map[ord[i+1]]; exists && t != nil { - if err := t.Cmp(x); err != nil { - return nil, errwrap.Wrapf(err, "arg %d does not match expected type", i+1) + //if x.HasVariant() { + // x = x.VariantToUni() // converts %[]v style things + //} + if x == nil || x == types.TypeVariant { // a %v or something unknown + x = &types.Type{ + Kind: types.KindUnification, + Uni: types.NewElem(), // unification variable, eg: ?1 } } - typ.Map[name] = x - typ.Ord = append(typ.Ord, name) + mapped[argName] = x + ordered = append(ordered, argName) } - return []*types.Type{typ}, nil // return a list with a single possibility + typ := &types.Type{ // this full function + Kind: types.KindFunc, + Map: mapped, + Ord: ordered, + Out: types.TypeStr, + } + + return typ, []*interfaces.UnificationInvariant{}, nil } // Build takes the now known function signature and stores it so that this @@ -507,6 +290,9 @@ func (obj *PrintfFunc) Validate() error { // Info returns some static info about itself. func (obj *PrintfFunc) Info() *interfaces.Info { + // Since this function implements FuncInfer we want sig to return nil to + // avoid an accidental return of unification variables when we should be + // getting them from FuncInfer, and not from here. (During unification!) return &interfaces.Info{ Pure: true, Memo: false, @@ -644,7 +430,8 @@ func parseFormatToTypeList(format string) ([]*types.Type, error) { // special! case 'v': - typList = append(typList, types.TypeVariant) + //typList = append(typList, types.TypeVariant) // old + typList = append(typList, types.NewType("?1")) // uni! default: return nil, fmt.Errorf("invalid format string at %d", i) @@ -658,11 +445,13 @@ func parseFormatToTypeList(format string) ([]*types.Type, error) { // compileFormatToString takes a format string and a list of values and returns // the compiled/templated output. This can also handle the %v special variant // type in the format string. Of course the corresponding value to those %v -// entries must have a static, fixed, precise type. +// entries must have a static, fixed, precise type. If someone changes the +// format string during runtime, then that's their fault, and this could error. +// Depending on PrintfAllowFormatError, we should NOT error if we have a +// mismatch between the format string and the available args. Return similar to +// golang's EXTRA/MISSING, eg: https://pkg.go.dev/fmt#hdr-Format_errors +// TODO: implement better format errors support // FIXME: add support for more types, and add tests! -// XXX: depending on PrintfAllowNonStaticFormat, we should NOT error if we have -// a mismatch between the format string and the available args. Return similar -// to golang's EXTRA/MISSING, eg: https://pkg.go.dev/fmt#hdr-Format_errors func compileFormatToString(format string, values []types.Value) (string, error) { output := "" ix := 0 @@ -708,12 +497,19 @@ func compileFormatToString(format string, values []types.Value) (string, error) typ = types.TypeVariant default: + // TODO: improve the output of this + if !PrintfAllowFormatError { + return fmt.Sprintf("", format[i], i), nil + } return "", fmt.Errorf("invalid format string at %d", i) } inType = false // done if ix >= len(values) { - // programming error, probably in type unification + // TODO: improve the output of this + if !PrintfAllowFormatError { + return fmt.Sprintf("", ix, i), nil + } return "", fmt.Errorf("more specifiers (%d) than values (%d)", ix+1, len(values)) } diff --git a/lang/core/iter/map_func.go b/lang/core/iter/map_func.go index 3145452b..6fd7d754 100644 --- a/lang/core/iter/map_func.go +++ b/lang/core/iter/map_func.go @@ -38,7 +38,6 @@ import ( "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/types" "github.com/purpleidea/mgmt/lang/types/full" - "github.com/purpleidea/mgmt/util" "github.com/purpleidea/mgmt/util/errwrap" ) @@ -49,19 +48,21 @@ const ( // arg names... mapArgNameInputs = "inputs" mapArgNameFunction = "function" + + mapArgNameArgName = "name-which-can-vary-over-time" // XXX: weird but ok ) func init() { funcs.ModuleRegister(ModuleName, MapFuncName, func() interfaces.Func { return &MapFunc{} }) // must register the func and name } -var _ interfaces.PolyFunc = &MapFunc{} // ensure it meets this expectation +var _ interfaces.BuildableFunc = &MapFunc{} // ensure it meets this expectation // 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. This implements the signature: `func(inputs []T1, -// function func(T1) T2) []T2` instead of the alternate with the two input args +// as the input element type. This implements the signature: `func(inputs []?1, +// function func(?1) ?2) []?2` 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. @@ -101,371 +102,26 @@ func (obj *MapFunc) ArgGen(index int) (string, error) { 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 +// helper +func (obj *MapFunc) sig() *types.Type { + // func(inputs []?1, function func(?1) ?2) []?2 + tIi := "?1" + if obj.Type != nil { + tIi = obj.Type.String() } - functionName, err := obj.ArgGen(1) - if err != nil { - return nil, err + tI := fmt.Sprintf("[]%s", tIi) // type of 1st arg + + tOi := "?2" + if obj.RType != nil { + tOi = obj.RType.String() } + tO := fmt.Sprintf("[]%s", tOi) // return type - dummyArgList := &interfaces.ExprAny{} // corresponds to the input list - dummyArgFunc := &interfaces.ExprAny{} // corresponds to the input func - dummyOutList := &interfaces.ExprAny{} // corresponds to the output list + // type of 2nd arg (the function) + tF := fmt.Sprintf("func(%s %s) %s", mapArgNameArgName, tIi, tOi) - t1Expr := &interfaces.ExprAny{} // corresponds to the t1 type - t2Expr := &interfaces.ExprAny{} // corresponds to the t2 type - - invar = &interfaces.EqualityWrapListInvariant{ - Expr1: dummyArgList, - Expr2Val: t1Expr, - } - invariants = append(invariants, invar) - - invar = &interfaces.EqualityWrapListInvariant{ - Expr1: dummyOutList, - Expr2Val: t2Expr, - } - invariants = append(invariants, invar) - - // 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) - - // relationship between t1 and t2 - argName := util.NumToAlpha(0) // XXX: does the arg name matter? - invar = &interfaces.EqualityWrapFuncInvariant{ - Expr1: dummyArgFunc, - Expr2Map: map[string]interfaces.Expr{ - argName: t1Expr, - }, - Expr2Ord: []string{argName}, - Expr2Out: t2Expr, - } - 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 - - // 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) - - invar = &interfaces.EqualityWrapListInvariant{ - Expr1: cfavInvar.Args[0], - Expr2Val: t1Expr, - } - invariants = append(invariants, invar) - - invar = &interfaces.EqualityWrapListInvariant{ - Expr1: cfavInvar.Expr, - Expr2Val: t2Expr, - } - 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) - } - - if t1 != nil && t2 != nil { - // TODO: if the argName matters, do it here... - _ = foundArgName - //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) - } - - // 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") - } - if partialType.Kind != types.KindFunc { - return nil, fmt.Errorf("partial type must be of kind func") - } - - // If we figure out both of these two types, we'll know the full type... - var t1 *types.Type // type - var t2 *types.Type // rtype - - // Look at the returned "out" type if it's known. - if tOut := partialType.Out; tOut != nil { - if tOut.Kind != types.KindList { - return nil, fmt.Errorf("partial out type must be of kind list") - } - t2 = tOut.Val // found (if not nil) - } - - ord := partialType.Ord - if partialType.Map != nil { - // TODO: is it okay to assume this? - //if len(ord) == 0 { - // return nil, fmt.Errorf("must have two args in func") - //} - if len(ord) != 2 { - return nil, fmt.Errorf("must have two args in func") - } - - if tInputs, exists := partialType.Map[ord[0]]; exists && tInputs != nil { - if tInputs.Kind != types.KindList { - 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[1]]; exists && tFunction != nil { - if tFunction.Kind != types.KindFunc { - 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("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 { - return nil, errwrap.Wrapf(err, "first arg function in type is inconsistent") - } - t1 = fIn // found - } - } - - if fOut := tFunction.Out; fOut != nil { - if err := fOut.Cmp(t2); t2 != nil && err != nil { - return nil, errwrap.Wrapf(err, "second arg function out type is inconsistent") - } - t2 = fOut // found - } - } - } - - if t1 == nil || t2 == nil { - return nil, fmt.Errorf("not enough type information given") - } - 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", mapArgNameInputs, tI, mapArgNameFunction, tF, tO) - typ := types.NewType(s) // yay! - - // TODO: type check that the partialValues are compatible - - return []*types.Type{typ}, nil // solved! + return types.NewType(s) // yay! } // Build is run to turn the polymorphic, undetermined function, into the @@ -547,38 +203,14 @@ func (obj *MapFunc) Validate() error { // Info returns some static info about itself. Build must be called before this // will return correct data. func (obj *MapFunc) Info() *interfaces.Info { - sig := obj.sig() // helper - return &interfaces.Info{ Pure: false, // TODO: what if the input function isn't pure? Memo: false, - Sig: sig, + Sig: obj.sig(), // helper Err: obj.Validate(), } } -// helper -func (obj *MapFunc) sig() *types.Type { - // TODO: what do we put if this is unknown? - tIi := types.TypeVariant - if obj.Type != nil { - tIi = obj.Type - } - tI := types.NewType(fmt.Sprintf("[]%s", tIi.String())) // type of 2nd arg - - tOi := types.TypeVariant - if obj.RType != nil { - tOi = obj.RType - } - tO := types.NewType(fmt.Sprintf("[]%s", tOi.String())) // return type - - // type of 1st arg (the function) - tF := types.NewType(fmt.Sprintf("func(%s %s) %s", "name-which-can-vary-over-time", tIi.String(), tOi.String())) - - s := fmt.Sprintf("func(%s %s, %s %s) %s", mapArgNameInputs, tI, mapArgNameFunction, tF, tO) - return types.NewType(s) // yay! -} - // Init runs some startup code for this function. func (obj *MapFunc) Init(init *interfaces.Init) error { obj.init = init diff --git a/lang/core/len_func.go b/lang/core/len_func.go index fb769a6e..47dc8071 100644 --- a/lang/core/len_func.go +++ b/lang/core/len_func.go @@ -33,25 +33,45 @@ import ( "context" "fmt" - "github.com/purpleidea/mgmt/lang/funcs/simplepoly" + "github.com/purpleidea/mgmt/lang/funcs/simple" "github.com/purpleidea/mgmt/lang/types" ) func init() { - simplepoly.Register("len", []*types.FuncValue{ - { - T: types.NewType("func(str) int"), - V: Len, - }, - { - T: types.NewType("func([]variant) int"), - V: Len, - }, - { - T: types.NewType("func(map{variant: variant}) int"), - V: Len, - }, + simple.Register("len", &simple.Scaffold{ + T: types.NewType("func(?1) int"), // TODO: should we add support for struct or func lengths? + C: simple.TypeMatch([]string{ + "func(str) int", + "func([]?1) int", + "func(map{?1: ?2}) int", + }), + //C: func(typ *types.Type) error { + // if typ == nil { + // return fmt.Errorf("nil type") + // } + // if typ.Kind != types.KindFunc { + // return fmt.Errorf("not a func") + // } + // if len(typ.Map) != 1 || len(typ.Ord) != 1 { + // return fmt.Errorf("arg count wrong") + // } + // if err := typ.Out.Cmp(types.TypeInt); err != nil { + // return err + // } + // t := typ.Map[typ.Ord[0]] + // if t.Cmp(types.TypeStr) == nil { + // return nil // func(str) int + // } + // if t.Kind == types.KindList { + // return nil // func([]?1) int + // } + // if t.Kind == types.KindMap { + // return nil // func(map{?1: ?2}) int + // } + // return fmt.Errorf("can't determine length of %s", t) + //}, + F: Len, }) } diff --git a/lang/core/math/fortytwo_func.go b/lang/core/math/fortytwo_func.go index f6a70e26..5974194d 100644 --- a/lang/core/math/fortytwo_func.go +++ b/lang/core/math/fortytwo_func.go @@ -33,32 +33,48 @@ import ( "context" "fmt" - "github.com/purpleidea/mgmt/lang/funcs/simplepoly" + "github.com/purpleidea/mgmt/lang/funcs/multi" + "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/types" ) func init() { typInt := types.NewType("func() int") typFloat := types.NewType("func() float") - simplepoly.ModuleRegister(ModuleName, "fortytwo", []*types.FuncValue{ - { - T: typInt, - V: fortyTwo(typInt), // generate the correct function here - }, - { - T: typFloat, - V: fortyTwo(typFloat), - }, + multi.ModuleRegister(ModuleName, "fortytwo", &multi.Scaffold{ + T: types.NewType("func() ?1"), + M: multi.TypeMatch(map[string]interfaces.FuncSig{ + "func() int": fortyTwo(typInt), + "func() float": fortyTwo(typFloat), + }), + //M: func(typ *types.Type) (interfaces.FuncSig, error) { + // if typ == nil { + // return nil, fmt.Errorf("nil type") + // } + // if typ.Kind != types.KindFunc { + // return nil, fmt.Errorf("not a func") + // } + // if len(typ.Map) != 0 || len(typ.Ord) != 0 { + // return nil, fmt.Errorf("arg count wrong") + // } + // if err := typ.Out.Cmp(types.TypeInt); err == nil { + // return fortyTwo(typInt), nil + // } + // if err := typ.Out.Cmp(types.TypeFloat); err == nil { + // return fortyTwo(typFloat), nil + // } + // return nil, fmt.Errorf("can't use return type of: %s", typ.Out) + //}, }) } // fortyTwo is a helper function to build the correct function for the desired -// signature, because the simplepoly API doesn't tell the implementing function +// signature, because the multi func API doesn't tell the implementing function // what its signature should be! In the next version of this API, we could pass // in a sig field, like how we demonstrate in the implementation of FortyTwo. If // the API doesn't change, then this is an example of how to build this as a // wrapper. -func fortyTwo(sig *types.Type) func(context.Context, []types.Value) (types.Value, error) { +func fortyTwo(sig *types.Type) interfaces.FuncSig { return func(ctx context.Context, input []types.Value) (types.Value, error) { return FortyTwo(sig, input) } diff --git a/lang/core/math/minus1_func.go b/lang/core/math/minus1_func.go index ee42518b..218c7155 100644 --- a/lang/core/math/minus1_func.go +++ b/lang/core/math/minus1_func.go @@ -37,9 +37,9 @@ import ( ) func init() { - simple.ModuleRegister(ModuleName, "minus1", &types.FuncValue{ + simple.ModuleRegister(ModuleName, "minus1", &simple.Scaffold{ T: types.NewType("func(x int) int"), - V: Minus1, + F: Minus1, }) } diff --git a/lang/core/math/mod_func.go b/lang/core/math/mod_func.go index 002a6793..9d6502af 100644 --- a/lang/core/math/mod_func.go +++ b/lang/core/math/mod_func.go @@ -34,20 +34,18 @@ import ( "fmt" "math" - "github.com/purpleidea/mgmt/lang/funcs/simplepoly" + "github.com/purpleidea/mgmt/lang/funcs/simple" "github.com/purpleidea/mgmt/lang/types" ) func init() { - simplepoly.ModuleRegister(ModuleName, "mod", []*types.FuncValue{ - { - T: types.NewType("func(int, int) int"), - V: Mod, - }, - { - T: types.NewType("func(float, float) float"), - V: Mod, - }, + simple.ModuleRegister(ModuleName, "mod", &simple.Scaffold{ + T: types.NewType("func(?1, ?1) ?1"), // all int or float + C: simple.TypeMatch([]string{ + "func(int, int) int", + "func(float, float) float", + }), + F: Mod, }) } diff --git a/lang/core/math/pow_func.go b/lang/core/math/pow_func.go index 30e51b30..cc9df126 100644 --- a/lang/core/math/pow_func.go +++ b/lang/core/math/pow_func.go @@ -39,9 +39,9 @@ import ( ) func init() { - simple.ModuleRegister(ModuleName, "pow", &types.FuncValue{ + simple.ModuleRegister(ModuleName, "pow", &simple.Scaffold{ T: types.NewType("func(x float, y float) float"), - V: Pow, + F: Pow, }) } diff --git a/lang/core/math/sqrt_func.go b/lang/core/math/sqrt_func.go index 32a1bd88..1724b84c 100644 --- a/lang/core/math/sqrt_func.go +++ b/lang/core/math/sqrt_func.go @@ -39,9 +39,9 @@ import ( ) func init() { - simple.ModuleRegister(ModuleName, "sqrt", &types.FuncValue{ + simple.ModuleRegister(ModuleName, "sqrt", &simple.Scaffold{ T: types.NewType("func(x float) float"), - V: Sqrt, + F: Sqrt, }) } diff --git a/lang/core/net/cidr_to_ip_func.go b/lang/core/net/cidr_to_ip_func.go index 8606419f..7ab211af 100644 --- a/lang/core/net/cidr_to_ip_func.go +++ b/lang/core/net/cidr_to_ip_func.go @@ -39,9 +39,9 @@ import ( ) func init() { - simple.ModuleRegister(ModuleName, "cidr_to_ip", &types.FuncValue{ + simple.ModuleRegister(ModuleName, "cidr_to_ip", &simple.Scaffold{ T: types.NewType("func(a str) str"), - V: CidrToIP, + F: CidrToIP, }) } diff --git a/lang/core/net/macfmt_func.go b/lang/core/net/macfmt_func.go index a775e89a..5465608b 100644 --- a/lang/core/net/macfmt_func.go +++ b/lang/core/net/macfmt_func.go @@ -40,13 +40,13 @@ import ( ) func init() { - simple.ModuleRegister(ModuleName, "macfmt", &types.FuncValue{ + simple.ModuleRegister(ModuleName, "macfmt", &simple.Scaffold{ T: types.NewType("func(a str) str"), - V: MacFmt, + F: MacFmt, }) - simple.ModuleRegister(ModuleName, "oldmacfmt", &types.FuncValue{ + simple.ModuleRegister(ModuleName, "oldmacfmt", &simple.Scaffold{ T: types.NewType("func(a str) str"), - V: OldMacFmt, + F: OldMacFmt, }) } diff --git a/lang/core/os/args_func.go b/lang/core/os/args_func.go index 3d8ee1ea..ab876505 100644 --- a/lang/core/os/args_func.go +++ b/lang/core/os/args_func.go @@ -38,9 +38,9 @@ import ( ) func init() { - simple.ModuleRegister(ModuleName, "args", &types.FuncValue{ + simple.ModuleRegister(ModuleName, "args", &simple.Scaffold{ T: types.NewType("func() []str"), - V: Args, + F: Args, }) } diff --git a/lang/core/os/distro_func.go b/lang/core/os/distro_func.go index 2af702a3..07dedd6b 100644 --- a/lang/core/os/distro_func.go +++ b/lang/core/os/distro_func.go @@ -43,9 +43,9 @@ const structDistroUID = "struct{distro str; version str; arch str}" var typeParseDistroUID = types.NewType(fmt.Sprintf("func(str) %s", structDistroUID)) func init() { - simple.ModuleRegister(ModuleName, "parse_distro_uid", &types.FuncValue{ + simple.ModuleRegister(ModuleName, "parse_distro_uid", &simple.Scaffold{ T: typeParseDistroUID, - V: ParseDistroUID, + F: ParseDistroUID, }) } diff --git a/lang/core/os/family_func.go b/lang/core/os/family_func.go index bd53e1a7..9407bf5e 100644 --- a/lang/core/os/family_func.go +++ b/lang/core/os/family_func.go @@ -39,17 +39,17 @@ import ( func init() { // TODO: Create a family method that will return a giant struct. - simple.ModuleRegister(ModuleName, "is_debian", &types.FuncValue{ + simple.ModuleRegister(ModuleName, "is_debian", &simple.Scaffold{ T: types.NewType("func() bool"), - V: IsDebian, + F: IsDebian, }) - simple.ModuleRegister(ModuleName, "is_redhat", &types.FuncValue{ + simple.ModuleRegister(ModuleName, "is_redhat", &simple.Scaffold{ T: types.NewType("func() bool"), - V: IsRedHat, + F: IsRedHat, }) - simple.ModuleRegister(ModuleName, "is_archlinux", &types.FuncValue{ + simple.ModuleRegister(ModuleName, "is_archlinux", &simple.Scaffold{ T: types.NewType("func() bool"), - V: IsArchLinux, + F: IsArchLinux, }) } diff --git a/lang/core/panic_func.go b/lang/core/panic_func.go index 9719ff71..7c7aaaf0 100644 --- a/lang/core/panic_func.go +++ b/lang/core/panic_func.go @@ -33,20 +33,40 @@ import ( "context" "fmt" - "github.com/purpleidea/mgmt/lang/funcs/simplepoly" + "github.com/purpleidea/mgmt/lang/funcs/simple" "github.com/purpleidea/mgmt/lang/types" ) func init() { - simplepoly.Register("panic", []*types.FuncValue{ - { - T: types.NewType("func(x bool) bool"), - V: Panic, - }, - { - T: types.NewType("func(x str) bool"), - V: Panic, - }, + simple.Register("panic", &simple.Scaffold{ + T: types.NewType("func(x ?1) bool"), // ?1 is bool or str + C: simple.TypeMatch([]string{ + "func(x bool) bool", + "func(x str) bool", + }), + //C: func(typ *types.Type) error { + // if typ == nil { + // return fmt.Errorf("nil type") + // } + // if typ.Kind != types.KindFunc { + // return fmt.Errorf("not a func") + // } + // if len(typ.Map) != 1 || len(typ.Ord) != 1 { + // return fmt.Errorf("arg count wrong") + // } + // if err := typ.Out.Cmp(types.TypeBool); err != nil { + // return err + // } + // t := typ.Map[typ.Ord[0]] + // if t.Cmp(types.TypeBool) == nil { + // return nil // func(bool) int + // } + // if t.Cmp(types.TypeStr) == nil { + // return nil // func(str) int + // } + // return fmt.Errorf("can't check type of: %s", t) + //}, + F: Panic, }) } diff --git a/lang/core/regexp/match_func.go b/lang/core/regexp/match_func.go index 39c026cd..69ba56e8 100644 --- a/lang/core/regexp/match_func.go +++ b/lang/core/regexp/match_func.go @@ -39,9 +39,9 @@ import ( ) func init() { - simple.ModuleRegister(ModuleName, "match", &types.FuncValue{ + simple.ModuleRegister(ModuleName, "match", &simple.Scaffold{ T: types.NewType("func(pattern str, s str) bool"), - V: Match, + F: Match, }) } diff --git a/lang/core/strings/split_func.go b/lang/core/strings/split_func.go index 6409e76d..ef4ef56e 100644 --- a/lang/core/strings/split_func.go +++ b/lang/core/strings/split_func.go @@ -38,9 +38,9 @@ import ( ) func init() { - simple.ModuleRegister(ModuleName, "split", &types.FuncValue{ + simple.ModuleRegister(ModuleName, "split", &simple.Scaffold{ T: types.NewType("func(a str, b str) []str"), - V: Split, + F: Split, }) } diff --git a/lang/core/strings/to_lower_func.go b/lang/core/strings/to_lower_func.go index c1895365..c1053391 100644 --- a/lang/core/strings/to_lower_func.go +++ b/lang/core/strings/to_lower_func.go @@ -38,9 +38,9 @@ import ( ) func init() { - simple.ModuleRegister(ModuleName, "to_lower", &types.FuncValue{ + simple.ModuleRegister(ModuleName, "to_lower", &simple.Scaffold{ T: types.NewType("func(a str) str"), - V: ToLower, + F: ToLower, }) } diff --git a/lang/core/sys/env_func.go b/lang/core/sys/env_func.go index 949f4681..2ef11247 100644 --- a/lang/core/sys/env_func.go +++ b/lang/core/sys/env_func.go @@ -39,21 +39,21 @@ import ( ) func init() { - simple.ModuleRegister(ModuleName, "getenv", &types.FuncValue{ + simple.ModuleRegister(ModuleName, "getenv", &simple.Scaffold{ T: types.NewType("func(str) str"), - V: GetEnv, + F: GetEnv, }) - simple.ModuleRegister(ModuleName, "defaultenv", &types.FuncValue{ + simple.ModuleRegister(ModuleName, "defaultenv", &simple.Scaffold{ T: types.NewType("func(str, str) str"), - V: DefaultEnv, + F: DefaultEnv, }) - simple.ModuleRegister(ModuleName, "hasenv", &types.FuncValue{ + simple.ModuleRegister(ModuleName, "hasenv", &simple.Scaffold{ T: types.NewType("func(str) bool"), - V: HasEnv, + F: HasEnv, }) - simple.ModuleRegister(ModuleName, "env", &types.FuncValue{ + simple.ModuleRegister(ModuleName, "env", &simple.Scaffold{ T: types.NewType("func() map{str: str}"), - V: Env, + F: Env, }) } diff --git a/lang/core/template_func.go b/lang/core/template_func.go index 8bced27c..71b35efd 100644 --- a/lang/core/template_func.go +++ b/lang/core/template_func.go @@ -67,7 +67,7 @@ func init() { funcs.Register(TemplateFuncName, func() interfaces.Func { return &TemplateFunc{} }) } -var _ interfaces.PolyFunc = &TemplateFunc{} // ensure it meets this expectation +var _ interfaces.InferableFunc = &TemplateFunc{} // ensure it meets this expectation // TemplateFunc is a static polymorphic function that compiles a template and // returns the output as a string. It bases its output on the values passed in @@ -104,205 +104,39 @@ func (obj *TemplateFunc) ArgGen(index int) (string, error) { return seq[index], nil } -// Unify returns the list of invariants that this func produces. -func (obj *TemplateFunc) Unify(expr interfaces.Expr) ([]interfaces.Invariant, error) { - var invariants []interfaces.Invariant - var invar interfaces.Invariant - - // func(format string) string - // OR - // func(format string, arg variant) string - - formatName, err := obj.ArgGen(0) - if err != nil { - return nil, err +// helper +func (obj *TemplateFunc) sig() *types.Type { + if obj.Type != nil { + typ := fmt.Sprintf("func(%s str, %s %s) str", templateArgNameTemplate, templateArgNameVars, obj.Type.String()) + return types.NewType(typ) } - dummyFormat := &interfaces.ExprAny{} // corresponds to the format type - dummyOut := &interfaces.ExprAny{} // corresponds to the out string - - // format arg type of string - invar = &interfaces.EqualsInvariant{ - Expr: dummyFormat, - Type: types.TypeStr, - } - invariants = append(invariants, invar) - - // return type of string - invar = &interfaces.EqualsInvariant{ - Expr: dummyOut, - Type: types.TypeStr, - } - 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 len(cfavInvar.Args) == 0 { - return nil, fmt.Errorf("unable to build function with no args") - } - if l := len(cfavInvar.Args); l > 2 { - return nil, fmt.Errorf("unable to build function with %d args", l) - } - // we can either have one arg or two - - var invariants []interfaces.Invariant - var invar interfaces.Invariant - - // add the relationship to the returned value - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Expr, - Expr2: dummyOut, - } - invariants = append(invariants, invar) - - // add the relationships to the called args - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Args[0], - Expr2: dummyFormat, - } - invariants = append(invariants, invar) - - // first arg must be a string - invar = &interfaces.EqualsInvariant{ - Expr: cfavInvar.Args[0], - Type: types.TypeStr, - } - invariants = append(invariants, invar) - - // TODO: if the template is known statically, we could - // parse it to check for variable safety if we wanted! - //value, err := cfavInvar.Args[0].Value() // is it known? - //if err != nil { - //} - - // full function - mapped := make(map[string]interfaces.Expr) - ordered := []string{formatName} - mapped[formatName] = dummyFormat - - if len(cfavInvar.Args) == 2 { // two args is more complex - argName, err := obj.ArgGen(1) // 1st arg after 0 - if err != nil { - return nil, err - } - if argName == templateArgNameTemplate { - return nil, fmt.Errorf("could not build function with %d args", 1) - } - - dummyArg := &interfaces.ExprAny{} - - // speculate about the type? (maybe redundant) - if typ, err := cfavInvar.Args[1].Type(); err == nil { - invar := &interfaces.EqualsInvariant{ - Expr: dummyArg, - Type: typ, - } - invariants = append(invariants, invar) - } - - if typ, exists := solved[cfavInvar.Args[1]]; exists { // alternate way to lookup type - invar := &interfaces.EqualsInvariant{ - Expr: dummyArg, - Type: typ, - } - invariants = append(invariants, invar) - } - - // expression must match type of the input arg - invar := &interfaces.EqualityInvariant{ - Expr1: dummyArg, - Expr2: cfavInvar.Args[1], - } - invariants = append(invariants, invar) - - mapped[argName] = dummyArg - ordered = append(ordered, argName) - } - - invar = &interfaces.EqualityWrapFuncInvariant{ - Expr1: expr, // maps directly to us! - Expr2Map: mapped, - Expr2Ord: ordered, - Expr2Out: dummyOut, - } - invariants = append(invariants, invar) - - // 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 + typ := fmt.Sprintf("func(%s str) str", templateArgNameTemplate) + return types.NewType(typ) } -// Polymorphisms returns the possible type signatures for this template. In this -// case, since the second argument can be an infinite number of values, it -// instead returns either the final precise type (if it can be gleamed from the -// input partials) or if it cannot, it returns a single entry with the complete -// type but with the variable second argument specified as a `variant` type. If -// it encounters any partial type specifications which are not possible, then it -// errors out. This could happen if you specified a non string template arg. -// XXX: is there a better API than returning a buried `variant` type? -func (obj *TemplateFunc) Polymorphisms(partialType *types.Type, partialValues []types.Value) ([]*types.Type, error) { - // TODO: return `variant` as second arg for now -- maybe there's a better way? - str := fmt.Sprintf("func(%s str, %s variant) str", templateArgNameTemplate, templateArgNameVars) - variant := []*types.Type{types.NewType(str)} +// FuncInfer takes partial type and value information from the call site of this +// function so that it can build an appropriate type signature for it. The type +// signature may include unification variables. +func (obj *TemplateFunc) FuncInfer(partialType *types.Type, partialValues []types.Value) (*types.Type, []*interfaces.UnificationInvariant, error) { + // func(format str) str + // OR + // func(format str, arg ?1) str - if partialType == nil { - return variant, nil + if l := len(partialValues); l < 1 || l > 2 { + return nil, nil, fmt.Errorf("must have at either one or two args") } - if partialType.Out != nil && partialType.Out.Cmp(types.TypeStr) != nil { - return nil, fmt.Errorf("return value of template must be str") + var typ *types.Type + if len(partialValues) == 1 { + typ = types.NewType(fmt.Sprintf("func(%s str) str", templateArgNameTemplate)) } - ord := partialType.Ord - if partialType.Map != nil { - if len(ord) != 2 && len(ord) != 1 { - return nil, fmt.Errorf("must have exactly one or two args in template func") - } - if t, exists := partialType.Map[ord[0]]; exists && t != nil { - if t.Cmp(types.TypeStr) != nil { - return nil, fmt.Errorf("first arg for template must be an str") - } - } - if len(ord) == 1 { // no args being passed in (boring template) - return []*types.Type{types.NewType(fmt.Sprintf("func(%s str) str", templateArgNameTemplate))}, nil - - } else if t, exists := partialType.Map[ord[1]]; exists && t != nil { - // known vars type! w00t! - return []*types.Type{types.NewType(fmt.Sprintf("func(%s str, %s %s) str", templateArgNameTemplate, templateArgNameVars, t.String()))}, nil - } + if len(partialValues) == 2 { + typ = types.NewType(fmt.Sprintf("func(%s str, %s ?1) str", templateArgNameTemplate, templateArgNameVars)) } - return variant, nil + return typ, []*interfaces.UnificationInvariant{}, nil } // Build takes the now known function signature and stores it so that this @@ -360,6 +194,9 @@ func (obj *TemplateFunc) Validate() error { // Info returns some static info about itself. func (obj *TemplateFunc) Info() *interfaces.Info { + // Since this function implements FuncInfer we want sig to return nil to + // avoid an accidental return of unification variables when we should be + // getting them from FuncInfer, and not from here. (During unification!) var sig *types.Type if obj.built { sig = obj.sig() // helper @@ -372,17 +209,6 @@ func (obj *TemplateFunc) Info() *interfaces.Info { } } -// helper -func (obj *TemplateFunc) sig() *types.Type { - if obj.Type != nil { // don't panic if called speculatively - str := fmt.Sprintf("func(%s str, %s %s) str", templateArgNameTemplate, templateArgNameVars, obj.Type.String()) - return types.NewType(str) - } - - str := fmt.Sprintf("func(%s str) str", templateArgNameTemplate) - return types.NewType(str) -} - // Init runs some startup code for this function. func (obj *TemplateFunc) Init(init *interfaces.Init) error { obj.init = init @@ -413,7 +239,11 @@ func (obj *TemplateFunc) run(ctx context.Context, templateText string, vars type // function in the simple package? // TODO: loop through this map in a sorted, deterministic order // XXX: should this use the scope instead (so imports are used properly) ? - for name, fn := range simple.RegisteredFuncs { + for name, scaffold := range simple.RegisteredFuncs { + if scaffold.T == nil || scaffold.T.HasUni() { + obj.init.Logf("warning, function named: `%s` is not unified", name) + continue + } name = safename(name) // TODO: rename since we can't include dot if _, exists := funcMap[name]; exists { obj.init.Logf("warning, existing function named: `%s` exists", name) @@ -425,7 +255,7 @@ func (obj *TemplateFunc) run(ctx context.Context, templateText string, vars type // parameter types. Functions meant to apply to arguments of // arbitrary type can use parameters of type interface{} or of // type reflect.Value. - f, err := wrap(ctx, name, fn) // wrap it so that it meets API expectations + f, err := wrap(ctx, name, scaffold) // wrap it so that it meets API expectations if err != nil { obj.init.Logf("warning, skipping function named: `%s`, err: %v", name, err) continue @@ -585,7 +415,7 @@ func safename(name string) string { // function API with what is expected from the reflection API. It returns a // version that includes the optional second error return value so that our // functions can return errors without causing a panic. -func wrap(ctx context.Context, name string, fn *types.FuncValue) (_ interface{}, reterr error) { +func wrap(ctx context.Context, name string, scaffold *simple.Scaffold) (_ interface{}, reterr error) { defer func() { // catch unhandled panics if r := recover(); r != nil { @@ -593,15 +423,21 @@ func wrap(ctx context.Context, name string, fn *types.FuncValue) (_ interface{}, } }() - if fn.T.Map == nil { + if scaffold.T == nil { + panic("malformed type") + } + if scaffold.T.HasUni() { + panic("type not unified") + } + if scaffold.T.Map == nil { panic("malformed func type") } - if len(fn.T.Map) != len(fn.T.Ord) { + if len(scaffold.T.Map) != len(scaffold.T.Ord) { panic("malformed func length") } in := []reflect.Type{} - for _, k := range fn.T.Ord { - t, ok := fn.T.Map[k] + for _, k := range scaffold.T.Ord { + t, ok := scaffold.T.Map[k] if !ok { panic("malformed func order") } @@ -611,7 +447,7 @@ func wrap(ctx context.Context, name string, fn *types.FuncValue) (_ interface{}, in = append(in, t.Reflect()) } - ret := fn.T.Out.Reflect() // this can panic! + ret := scaffold.T.Out.Reflect() // this can panic! out := []reflect.Type{ret, errorType} var variadic = false // currently not supported in our function value typ := reflect.FuncOf(in, out, variadic) @@ -619,7 +455,7 @@ func wrap(ctx context.Context, name string, fn *types.FuncValue) (_ interface{}, // wrap our function with the translation that is necessary f := func(args []reflect.Value) (results []reflect.Value) { // build innerArgs := []types.Value{} - zeroValue := reflect.Zero(fn.T.Out.Reflect()) // zero value of return type + zeroValue := reflect.Zero(scaffold.T.Out.Reflect()) // zero value of return type for _, x := range args { v, err := types.ValueOf(x) // reflect.Value -> Value if err != nil { @@ -633,8 +469,8 @@ func wrap(ctx context.Context, name string, fn *types.FuncValue) (_ interface{}, innerArgs = append(innerArgs, v) } - result, err := fn.Call(ctx, innerArgs) // call it - if err != nil { // function errored :( + result, err := scaffold.F(ctx, innerArgs) // call it + if err != nil { // function errored :( // errwrap is a better way to report errors, if allowed! r := reflect.ValueOf(errwrap.Wrapf(err, "function `%s` errored", name)) if !r.Type().ConvertibleTo(errorType) { // for fun! diff --git a/lang/core/test/oneinstance_fact.go b/lang/core/test/oneinstance_fact.go index f4e71166..cee5e47a 100644 --- a/lang/core/test/oneinstance_fact.go +++ b/lang/core/test/oneinstance_fact.go @@ -121,9 +121,9 @@ func init() { } }) - simple.ModuleRegister(ModuleName, OneInstanceBFuncName, &types.FuncValue{ + simple.ModuleRegister(ModuleName, OneInstanceBFuncName, &simple.Scaffold{ T: types.NewType("func() str"), - V: func(context.Context, []types.Value) (types.Value, error) { + F: func(context.Context, []types.Value) (types.Value, error) { oneInstanceBMutex.Lock() if oneInstanceBFlag { panic("should not get called twice") @@ -133,9 +133,9 @@ func init() { return &types.StrValue{V: msg}, nil }, }) - simple.ModuleRegister(ModuleName, OneInstanceDFuncName, &types.FuncValue{ + simple.ModuleRegister(ModuleName, OneInstanceDFuncName, &simple.Scaffold{ T: types.NewType("func() str"), - V: func(context.Context, []types.Value) (types.Value, error) { + F: func(context.Context, []types.Value) (types.Value, error) { oneInstanceDMutex.Lock() if oneInstanceDFlag { panic("should not get called twice") @@ -145,9 +145,9 @@ func init() { return &types.StrValue{V: msg}, nil }, }) - simple.ModuleRegister(ModuleName, OneInstanceFFuncName, &types.FuncValue{ + simple.ModuleRegister(ModuleName, OneInstanceFFuncName, &simple.Scaffold{ T: types.NewType("func() str"), - V: func(context.Context, []types.Value) (types.Value, error) { + F: func(context.Context, []types.Value) (types.Value, error) { oneInstanceFMutex.Lock() if oneInstanceFFlag { panic("should not get called twice") @@ -157,9 +157,9 @@ func init() { return &types.StrValue{V: msg}, nil }, }) - simple.ModuleRegister(ModuleName, OneInstanceHFuncName, &types.FuncValue{ + simple.ModuleRegister(ModuleName, OneInstanceHFuncName, &simple.Scaffold{ T: types.NewType("func() str"), - V: func(context.Context, []types.Value) (types.Value, error) { + F: func(context.Context, []types.Value) (types.Value, error) { oneInstanceHMutex.Lock() if oneInstanceHFlag { panic("should not get called twice") diff --git a/lang/core/value/get_func.go b/lang/core/value/get_func.go index 8a9c2bea..def3a58e 100644 --- a/lang/core/value/get_func.go +++ b/lang/core/value/get_func.go @@ -111,227 +111,17 @@ func (obj *GetFunc) ArgGen(index int) (string, error) { return seq[index], nil } -// Unify returns the list of invariants that this func produces. -func (obj *GetFunc) Unify(expr interfaces.Expr) ([]interfaces.Invariant, error) { - var invariants []interfaces.Invariant - var invar interfaces.Invariant - - if obj.Type != nil { // if we set the type statically, unify is simple - sig := obj.sig() // helper - invar = &interfaces.EqualsInvariant{ - Expr: expr, - Type: sig, - } - invariants = append(invariants, invar) - - return invariants, nil +// helper +func (obj *GetFunc) sig() *types.Type { + // func(key str) struct{value ?1; ready bool} + typ := "?1" + if obj.Type != nil { + typ = obj.Type.String() } - // func(key str) struct{value T1; ready bool} - - keyName, err := obj.ArgGen(0) - if err != nil { - return nil, err - } - - dummyKey := &interfaces.ExprAny{} // corresponds to the key type - dummyOut := &interfaces.ExprAny{} // corresponds to the out struct - dummyValue := &interfaces.ExprAny{} // corresponds to the value type - dummyReady := &interfaces.ExprAny{} // corresponds to the ready type - - // the known types... - invar = &interfaces.EqualsInvariant{ - Expr: dummyKey, - Type: types.TypeStr, - } - invariants = append(invariants, invar) - - invar = &interfaces.EqualsInvariant{ - Expr: dummyReady, - Type: types.TypeBool, - } - invariants = append(invariants, invar) - - // relationship between Out and T1 - // TODO: do the precise field string names matter or can we cmp anyways? - mapped := make(map[string]interfaces.Expr) - ordered := []string{getFieldNameValue, getFieldNameReady} - mapped[getFieldNameValue] = dummyValue - mapped[getFieldNameReady] = dummyReady - invar = &interfaces.EqualityWrapStructInvariant{ - Expr1: dummyOut, // unique id for this expression (a pointer) - Expr2Map: mapped, - Expr2Ord: ordered, - } - invariants = append(invariants, invar) - - // full function - invar = &interfaces.EqualityWrapFuncInvariant{ - Expr1: expr, // maps directly to us! - Expr2Map: map[string]interfaces.Expr{keyName: dummyKey}, - Expr2Ord: []string{keyName}, - Expr2Out: dummyOut, - } - 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 != 1 { - return nil, fmt.Errorf("unable to build function with %d args", l) - } - - var invariants []interfaces.Invariant - var invar interfaces.Invariant - - // add the relationship to the returned value - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Expr, - Expr2: dummyOut, - } - invariants = append(invariants, invar) - - // add the relationships to the called args - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Args[0], - Expr2: dummyKey, - } - invariants = append(invariants, invar) - - // If we figure out this type, we'll know the full type! - var t1 *types.Type // value type - - // validateArg0 checks: key input - validateArg0 := func(typ *types.Type) error { - if typ == nil { // unknown so far - return nil - } - if typ.Kind != types.KindStr { - return errwrap.Wrapf(err, "input index type was inconsistent") - } - return nil - } - - // validateOut checks: T1 - validateOut := func(typ *types.Type) error { - if typ == nil { // unknown so far - return nil - } - - // we happen to have a struct! - if k := typ.Kind; k != types.KindStruct { - return fmt.Errorf("unable to build function with return type of kind: %s", k) - } - - if typ.Map == nil || len(typ.Ord) == 0 { - // programming error - return fmt.Errorf("return struct is missing type") - } - - // TODO: do the precise field string names - // matter or can we cmp anyways? - tReady, exists := typ.Map[getFieldNameReady] - if !exists { - return fmt.Errorf("return struct is missing ready field") - } - if tReady.Kind != types.KindBool { - return fmt.Errorf("return struct ready field must be bool kind") - } - - tValue, exists := typ.Map[getFieldNameValue] - if !exists { - return fmt.Errorf("return struct is missing value field") - } - - if err := tValue.Cmp(t1); t1 != nil && err != nil { - return errwrap.Wrapf(err, "value type was inconsistent") - } - - // learn! - t1 = tValue - return nil - } - - if typ, err := cfavInvar.Args[0].Type(); err == nil { // is it known? - // this only checks if this is an str - if err := validateArg0(typ); err != nil { - return nil, errwrap.Wrapf(err, "first list arg type is inconsistent") - } - } - if typ, exists := solved[cfavInvar.Args[0]]; exists { // alternate way to lookup type - // this only checks if this is an str - if err := validateArg0(typ); err != nil { - return nil, errwrap.Wrapf(err, "first list arg type is inconsistent") - } - } - - // return type... - if typ, err := cfavInvar.Expr.Type(); err == nil { // is it known? - if err := validateOut(typ); err != nil { - return nil, errwrap.Wrapf(err, "return type is inconsistent") - } - } - if typ, exists := solved[cfavInvar.Expr]; exists { // alternate way to lookup type - if err := validateOut(typ); err != nil { - return nil, errwrap.Wrapf(err, "return type is inconsistent") - } - } - - // XXX: We need to add a relationship somehow here or - // elsewhere between dummyValue and the type we are - // expecting. - // (1) we shouldn't look on disk in the cached storage. - // (2) how can we match on function send/recv values and - // resource fields??? - // (3) worst case scenario we just hope for the best, - // and hope we can infer the type some other way... - - // XXX: if the types aren't know statically? - - if t1 != nil { - invar := &interfaces.EqualsInvariant{ - Expr: dummyValue, - Type: t1, - } - invariants = append(invariants, invar) - } - - // XXX: if t1 is missing, we could also return a new - // generator for later if we learn new information, but - // we'd have to be careful to not do it infinitely. - - // 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 + // output is a struct with two fields: + // value is the zero value if not ready. A bool for that in other field. + return types.NewType(fmt.Sprintf("func(%s str) struct{%s %s; %s bool}", getArgNameKey, getFieldNameValue, typ, getFieldNameReady)) } // Build is run to turn the polymorphic, undetermined function, into the @@ -408,13 +198,6 @@ func (obj *GetFunc) Info() *interfaces.Info { } } -// helper -func (obj *GetFunc) sig() *types.Type { - // output is a struct with two fields: - // value is the zero value if not ready. A bool for that in other field. - return types.NewType(fmt.Sprintf("func(%s str) struct{%s %s; %s bool}", getArgNameKey, getFieldNameValue, obj.Type.String(), getFieldNameReady)) -} - // Init runs some startup code for this function. func (obj *GetFunc) Init(init *interfaces.Init) error { obj.init = init diff --git a/lang/core/world/schedule_func.go b/lang/core/world/schedule_func.go index 6799a4bf..289c4ea3 100644 --- a/lang/core/world/schedule_func.go +++ b/lang/core/world/schedule_func.go @@ -77,7 +77,7 @@ func init() { funcs.ModuleRegister(ModuleName, ScheduleFuncName, func() interfaces.Func { return &ScheduleFunc{} }) } -var _ interfaces.PolyFunc = &ScheduleFunc{} // ensure it meets this expectation +var _ interfaces.BuildableFunc = &ScheduleFunc{} // ensure it meets this expectation // ScheduleFunc is special function which determines where code should run in // the cluster. @@ -122,289 +122,37 @@ func (obj *ScheduleFunc) ArgGen(index int) (string, error) { return seq[index], nil } -// Unify returns the list of invariants that this func produces. -func (obj *ScheduleFunc) Unify(expr interfaces.Expr) ([]interfaces.Invariant, error) { - var invariants []interfaces.Invariant - var invar interfaces.Invariant - - // func(namespace str) []str - // OR - // func(namespace str, opts T1) []str - - namespaceName, err := obj.ArgGen(0) - if err != nil { - return nil, err +// helper +func (obj *ScheduleFunc) sig() *types.Type { + sig := types.NewType(fmt.Sprintf("func(%s str) []str", scheduleArgNameNamespace)) // simplest form + if obj.Type != nil { + sig = types.NewType(fmt.Sprintf("func(%s str, %s %s) []str", scheduleArgNameNamespace, scheduleArgNameOpts, obj.Type.String())) } - - dummyNamespace := &interfaces.ExprAny{} // corresponds to the namespace type - dummyOut := &interfaces.ExprAny{} // corresponds to the out string - - // namespace arg type of string - invar = &interfaces.EqualsInvariant{ - Expr: dummyNamespace, - Type: types.TypeStr, - } - invariants = append(invariants, invar) - - // return type of []string - invar = &interfaces.EqualsInvariant{ - Expr: dummyOut, - Type: types.TypeListStr, - } - 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 len(cfavInvar.Args) == 0 { - return nil, fmt.Errorf("unable to build function with no args") - } - if l := len(cfavInvar.Args); l > 2 { - return nil, fmt.Errorf("unable to build function with %d args", l) - } - // we can either have one arg or two - - var invariants []interfaces.Invariant - var invar interfaces.Invariant - - // add the relationship to the returned value - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Expr, - Expr2: dummyOut, - } - invariants = append(invariants, invar) - - // add the relationships to the called args - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Args[0], - Expr2: dummyNamespace, - } - invariants = append(invariants, invar) - - // first arg must be a string - invar = &interfaces.EqualsInvariant{ - Expr: cfavInvar.Args[0], - Type: types.TypeStr, - } - invariants = append(invariants, invar) - - // full function - mapped := make(map[string]interfaces.Expr) - ordered := []string{namespaceName} - mapped[namespaceName] = dummyNamespace - - if len(cfavInvar.Args) == 2 { // two args is more complex - dummyOpts := &interfaces.ExprAny{} - - optsTypeKnown := false - - // speculate about the type? - if typ, exists := solved[cfavInvar.Args[1]]; exists { - optsTypeKnown = true - if typ.Kind != types.KindStruct { - return nil, fmt.Errorf("second arg must be of kind struct") - } - - // XXX: the problem is that I can't - // currently express the opts struct as - // an invariant, without building a big - // giant, unusable exclusive... - validOpts := obj.validOpts() - - if StrictScheduleOpts { - // strict opts field checking! - for _, name := range typ.Ord { - t := typ.Map[name] - value, exists := validOpts[name] - if !exists { - return nil, fmt.Errorf("unexpected opts field: `%s`", name) - } - - if err := t.Cmp(value); err != nil { - return nil, errwrap.Wrapf(err, "expected different type for opts field: `%s`", name) - } - } - - } else { - // permissive field checking... - validOptsSorted := []string{} - for name := range validOpts { - validOptsSorted = append(validOptsSorted, name) - } - sort.Strings(validOptsSorted) - for _, name := range validOptsSorted { - value := validOpts[name] // type - - t, exists := typ.Map[name] - if !exists { - continue // ignore it - } - - // if it exists, check the type - if err := t.Cmp(value); err != nil { - return nil, errwrap.Wrapf(err, "expected different type for opts field: `%s`", name) - } - } - } - - invar := &interfaces.EqualsInvariant{ - Expr: dummyOpts, - Type: typ, - } - invariants = append(invariants, invar) - } - // redundant? - if typ, err := cfavInvar.Args[1].Type(); err == nil { - invar := &interfaces.EqualsInvariant{ - Expr: cfavInvar.Args[1], - Type: typ, - } - invariants = append(invariants, invar) - } - - // If we're strict, require it, otherwise let - // in whatever, and let Build() deal with it. - if StrictScheduleOpts && !optsTypeKnown { - return nil, fmt.Errorf("the type of the opts struct is not known") - } - - // expression must match type of the input arg - invar := &interfaces.EqualityInvariant{ - Expr1: dummyOpts, - Expr2: cfavInvar.Args[1], - } - invariants = append(invariants, invar) - - mapped[scheduleArgNameOpts] = dummyOpts - ordered = append(ordered, scheduleArgNameOpts) - } - - invar = &interfaces.EqualityWrapFuncInvariant{ - Expr1: expr, // maps directly to us! - Expr2Map: mapped, - Expr2Ord: ordered, - Expr2Out: dummyOut, - } - invariants = append(invariants, invar) - - // 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 + return sig } -// 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 *ScheduleFunc) Polymorphisms(partialType *types.Type, partialValues []types.Value) ([]*types.Type, error) { - // TODO: technically, we could generate all permutations of the struct! - //variant := []*types.Type{} - //t0 := types.NewType("func(namespace str) []str") - //variant = append(variant, t0) - //validOpts := obj.validOpts() - //for ? := ? range { // generate all permutations of the struct... - // t := types.NewType(fmt.Sprintf("func(namespace str, opts %s) []str", ?)) - // variant = append(variant, t) - //} - //if partialType == nil { - // return variant, nil - //} +// FuncInfer takes partial type and value information from the call site of this +// function so that it can build an appropriate type signature for it. The type +// signature may include unification variables. +func (obj *ScheduleFunc) FuncInfer(partialType *types.Type, partialValues []types.Value) (*types.Type, []*interfaces.UnificationInvariant, error) { + // func(namespace str) []str + // OR + // func(namespace str, opts ?1) []str - if partialType == nil { - return nil, fmt.Errorf("zero type information given") + if l := len(partialValues); l < 1 || l > 2 { + return nil, nil, fmt.Errorf("must have at either one or two args") } var typ *types.Type - - if tOut := partialType.Out; tOut != nil { - if err := tOut.Cmp(types.TypeListStr); err != nil { - return nil, errwrap.Wrapf(err, "return type must be a list of strings") - } + if len(partialValues) == 1 { + typ = types.NewType(fmt.Sprintf("func(%s str) []str", scheduleArgNameNamespace)) } - ord := partialType.Ord - if partialType.Map != nil { - if len(ord) == 0 { - return nil, fmt.Errorf("must have at least one arg in schedule func") - } - - if tNamespace, exists := partialType.Map[ord[0]]; exists && tNamespace != nil { - if err := tNamespace.Cmp(types.TypeStr); err != nil { - return nil, errwrap.Wrapf(err, "first arg must be an str") - } - } - if len(ord) == 1 { - return []*types.Type{types.NewType("func(namespace str) []str")}, nil // done! - } - - if len(ord) != 2 { - return nil, fmt.Errorf("must have either one or two args in schedule func") - } - - if tOpts, exists := partialType.Map[ord[1]]; exists { - if tOpts == nil { // usually a `struct{}` - typFunc := types.NewType("func(namespace str, opts variant) []str") - return []*types.Type{typFunc}, nil // solved! - } - - if tOpts.Kind != types.KindStruct { - return nil, fmt.Errorf("second arg must be of kind struct") - } - - validOpts := obj.validOpts() - for _, name := range tOpts.Ord { - t := tOpts.Map[name] - value, exists := validOpts[name] - if !exists { - return nil, fmt.Errorf("unexpected opts field: `%s`", name) - } - - if err := t.Cmp(value); err != nil { - return nil, errwrap.Wrapf(err, "expected different type for opts field: `%s`", name) - } - } - - typ = tOpts // solved - } + if len(partialValues) == 2 { + typ = types.NewType(fmt.Sprintf("func(%s str, %s ?1) []str", scheduleArgNameNamespace, scheduleArgNameOpts)) } - if typ == nil { - return nil, fmt.Errorf("not enough type information") - } - - typFunc := types.NewType(fmt.Sprintf("func(namespace str, opts %s) []str", typ.String())) - - // TODO: type check that the partialValues are compatible - - return []*types.Type{typFunc}, nil // solved! + return typ, []*interfaces.UnificationInvariant{}, nil } // Build is run to turn the polymorphic, undetermined function, into the @@ -509,8 +257,9 @@ func (obj *ScheduleFunc) Validate() error { // Info returns some static info about itself. Build must be called before this // will return correct data. func (obj *ScheduleFunc) Info() *interfaces.Info { - // It's important that you don't return a non-nil sig if this is called - // before you're built. Type unification may call it opportunistically. + // Since this function implements FuncInfer we want sig to return nil to + // avoid an accidental return of unification variables when we should be + // getting them from FuncInfer, and not from here. (During unification!) var sig *types.Type if obj.built { sig = obj.sig() // helper @@ -524,15 +273,6 @@ func (obj *ScheduleFunc) Info() *interfaces.Info { } } -// helper -func (obj *ScheduleFunc) sig() *types.Type { - sig := types.NewType(fmt.Sprintf("func(%s str) []str", scheduleArgNameNamespace)) // simplest form - if obj.Type != nil { - sig = types.NewType(fmt.Sprintf("func(%s str, %s %s) []str", scheduleArgNameNamespace, scheduleArgNameOpts, obj.Type.String())) - } - return sig -} - // Init runs some startup code for this function. func (obj *ScheduleFunc) Init(init *interfaces.Init) error { obj.init = init diff --git a/lang/funcs/contains_func.go b/lang/funcs/contains_func.go index 17a6efac..79f93d85 100644 --- a/lang/funcs/contains_func.go +++ b/lang/funcs/contains_func.go @@ -35,7 +35,6 @@ import ( "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/types" - "github.com/purpleidea/mgmt/util/errwrap" ) const ( @@ -53,7 +52,7 @@ func init() { Register(ContainsFuncName, func() interfaces.Func { return &ContainsFunc{} }) // must register the func and name } -var _ interfaces.PolyFunc = &ContainsFunc{} // ensure it meets this expectation +var _ interfaces.BuildableFunc = &ContainsFunc{} // ensure it meets this expectation // ContainsFunc returns true if a value is found in a list. Otherwise false. type ContainsFunc struct { @@ -80,224 +79,14 @@ func (obj *ContainsFunc) ArgGen(index int) (string, error) { return seq[index], nil } -// Unify returns the list of invariants that this func produces. -func (obj *ContainsFunc) Unify(expr interfaces.Expr) ([]interfaces.Invariant, error) { - var invariants []interfaces.Invariant - var invar interfaces.Invariant - - // func(needle variant, haystack variant) bool - // func(needle %s, haystack []%s) bool - - needleName, err := obj.ArgGen(0) - if err != nil { - return nil, err +// helper +func (obj *ContainsFunc) sig() *types.Type { + // func(needle ?1, haystack []?1) bool + s := "?1" + if obj.Type != nil { // don't panic if called speculatively + s = obj.Type.String() // if solved } - haystackName, err := obj.ArgGen(1) - if err != nil { - return nil, err - } - - dummyNeedle := &interfaces.ExprAny{} // corresponds to the needle type - dummyHaystack := &interfaces.ExprAny{} // corresponds to the haystack type - //dummyHaystackValue := &interfaces.ExprAny{} // corresponds to the haystack list type - dummyOut := &interfaces.ExprAny{} // corresponds to the out boolean - - //invar = &unification.EqualityInvariant{ - // Expr1: dummyNeedle, - // Expr2: dummyHaystackValue, - //} - //invariants = append(invariants, invar) - - // list relationship between needle and haystack - // TODO: did I get this equality backwards? - invar = &interfaces.EqualityWrapListInvariant{ - Expr1: dummyHaystack, - Expr2Val: dummyNeedle, - } - invariants = append(invariants, invar) - - // full function - mapped := make(map[string]interfaces.Expr) - ordered := []string{needleName, haystackName} - mapped[needleName] = dummyNeedle - mapped[haystackName] = dummyHaystack - - invar = &interfaces.EqualityWrapFuncInvariant{ - Expr1: expr, // maps directly to us! - Expr2Map: mapped, - Expr2Ord: ordered, - Expr2Out: dummyOut, - } - invariants = append(invariants, invar) - - // return type of bool - invar = &interfaces.EqualsInvariant{ - Expr: dummyOut, - Type: types.TypeBool, - } - invariants = append(invariants, invar) - - // generator function to link this to the right type - fn := obj.fnBuilder(false, expr, dummyNeedle, dummyHaystack, dummyOut) - invar = &interfaces.GeneratorInvariant{ - Func: fn, - } - invariants = append(invariants, invar) - - return invariants, nil -} - -// fnBuilder builds the function for the generator invariant. It is unique in -// that it can recursively call itself to build a second generation generator -// invariant. This can only happen once, because by then we'll have given all -// the new information we can, and falsely producing redundant information is a -// good way to stall the solver if it thinks it keeps learning more things! -func (obj *ContainsFunc) fnBuilder(recurse bool, expr, dummyNeedle, dummyHaystack, dummyOut interfaces.Expr) func(fnInvariants []interfaces.Invariant, solved map[interfaces.Expr]*types.Type) ([]interfaces.Invariant, error) { - return 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) - } - - var invariants []interfaces.Invariant - var invar interfaces.Invariant - - if !recurse { // only do this once! - // add the relationship to the returned value - invar = &interfaces.EqualityInvariant{ - Expr1: dummyOut, - Expr2: cfavInvar.Expr, - } - invariants = append(invariants, invar) - - // add the relationships to the called args - invar = &interfaces.EqualityInvariant{ - Expr1: dummyNeedle, - Expr2: cfavInvar.Args[0], - } - invariants = append(invariants, invar) - - invar = &interfaces.EqualityInvariant{ - Expr1: dummyHaystack, - Expr2: cfavInvar.Args[1], - } - invariants = append(invariants, invar) - - // 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) - } - - var needleTyp *types.Type - - // Instead of using cfavInvar.Args[*].Type() I think we - // can probably rely on the solved to find this for us! - if typ, exists := solved[cfavInvar.Args[1]]; exists { - if k := typ.Kind; k == types.KindList { - needleTyp = typ.Val // contained element type - } - } - - if typ, exists := solved[cfavInvar.Args[0]]; exists { - if err := needleTyp.Cmp(typ); needleTyp != nil && err != nil { - // inconsistent types! - return nil, errwrap.Wrapf(err, "inconsistent type") - } - - needleTyp = typ - } - - // We only want to recurse once. - if recurse && needleTyp == nil { - // nothing new we can do - return nil, fmt.Errorf("couldn't generate new invariants") - } - - if needleTyp == nil { - // recurse-- we build a new one! - fn := obj.fnBuilder(true, expr, dummyNeedle, dummyHaystack, dummyOut) - invar = &interfaces.GeneratorInvariant{ - Func: fn, - } - invariants = append(invariants, invar) - } - - invar = &interfaces.EqualsInvariant{ - Expr: dummyNeedle, - Type: needleTyp, - } - invariants = append(invariants, invar) - - 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") - } -} - -// 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 *ContainsFunc) Polymorphisms(partialType *types.Type, partialValues []types.Value) ([]*types.Type, error) { - // TODO: return `variant` as arg for now -- maybe there's a better way? - variant := []*types.Type{types.NewType("func(needle variant, haystack variant) bool")} - - if partialType == nil { - return variant, nil - } - - var typ *types.Type - - ord := partialType.Ord - if partialType.Map != nil { - if len(ord) != 2 { - return nil, fmt.Errorf("must have exactly three args in contains func") - } - if tNeedle, exists := partialType.Map[ord[0]]; exists && tNeedle != nil { - typ = tNeedle // solved - } - if tHaystack, exists := partialType.Map[ord[1]]; exists && tHaystack != nil { - if tHaystack.Kind != types.KindList { - return nil, fmt.Errorf("second arg must be of kind list") - } - if typ != nil && typ.Cmp(tHaystack.Val) != nil { - return nil, fmt.Errorf("list contents in second arg for contains must match search type") - } - typ = tHaystack.Val // solved - } - } - - if tOut := partialType.Out; tOut != nil { - if tOut.Kind != types.KindBool { - return nil, fmt.Errorf("return type must be a bool") - } - } - - if typ == nil { - return variant, nil - } - - typFunc := types.NewType(fmt.Sprintf("func(needle %s, haystack []%s) bool", typ.String(), typ.String())) - - // TODO: type check that the partialValues are compatible - - return []*types.Type{typFunc}, nil // solved! + return types.NewType(fmt.Sprintf("func(%s %s, %s []%s) bool", containsArgNameNeedle, s, containsArgNameHaystack, s)) } // Build is run to turn the polymorphic, undetermined function, into the @@ -306,44 +95,15 @@ func (obj *ContainsFunc) Polymorphisms(partialType *types.Type, partialValues [] // used. This function is idempotent, as long as the arg isn't changed between // runs. func (obj *ContainsFunc) 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 contains 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") - } - - tNeedle, exists := typ.Map[typ.Ord[0]] - if !exists || tNeedle == nil { - return nil, fmt.Errorf("first arg must be specified") - } - - tHaystack, exists := typ.Map[typ.Ord[1]] - if !exists || tHaystack == nil { - return nil, fmt.Errorf("second arg must be specified") - } - - if tHaystack.Kind != types.KindList { - return nil, fmt.Errorf("second argument must be of kind list") - } - - if err := tHaystack.Val.Cmp(tNeedle); err != nil { - return nil, errwrap.Wrapf(err, "type of first arg must match type of list elements in second arg") - } - - if err := typ.Out.Cmp(types.TypeBool); err != nil { - return nil, errwrap.Wrapf(err, "return type must be a boolean") - } - - obj.Type = tNeedle // type of value stored in our list + // We don't need to check that this matches, or that .Map has the right + // length, because otherwise it would mean type unification is giving a + // bad solution, which would be a major bug. Check to avoid any panics. + // Other functions might need to check something if they only accept a + // limited subset of the original type unification variables signature. + //if err := unificationUtil.UnifyCmp(typ, obj.sig()); err != nil { + // return nil, err + //} + obj.Type = typ.Map[typ.Ord[0]] // type of value stored in our list return obj.sig(), nil } @@ -358,24 +118,14 @@ func (obj *ContainsFunc) Validate() error { // Info returns some static info about itself. Build must be called before this // will return correct data. func (obj *ContainsFunc) Info() *interfaces.Info { - var sig *types.Type - if obj.Type != nil { // don't panic if called speculatively - sig = obj.sig() // helper - } return &interfaces.Info{ Pure: true, Memo: false, - Sig: sig, // func kind + Sig: obj.sig(), // helper, func kind Err: obj.Validate(), } } -// helper -func (obj *ContainsFunc) sig() *types.Type { - s := obj.Type.String() - return types.NewType(fmt.Sprintf("func(%s %s, %s []%s) bool", containsArgNameNeedle, s, containsArgNameHaystack, s)) -} - // Init runs some startup code for this function. func (obj *ContainsFunc) Init(init *interfaces.Init) error { obj.init = init diff --git a/lang/funcs/funcgen/fixtures/func_base.tpl b/lang/funcs/funcgen/fixtures/func_base.tpl index aa587b1b..d50773d4 100644 --- a/lang/funcs/funcgen/fixtures/func_base.tpl +++ b/lang/funcs/funcgen/fixtures/func_base.tpl @@ -39,29 +39,29 @@ import ( ) func init() { - simple.ModuleRegister("golang/testpkg", "all_kind", &types.FuncValue{ + simple.ModuleRegister("golang/testpkg", "all_kind", &simple.Scaffold{ T: types.NewType("func(x int, y str) float"), - V: TestpkgAllKind, + F: TestpkgAllKind, }) - simple.ModuleRegister("golang/testpkg", "to_upper", &types.FuncValue{ + simple.ModuleRegister("golang/testpkg", "to_upper", &simple.Scaffold{ T: types.NewType("func(s str) str"), - V: TestpkgToUpper, + F: TestpkgToUpper, }) - simple.ModuleRegister("golang/testpkg", "max", &types.FuncValue{ + simple.ModuleRegister("golang/testpkg", "max", &simple.Scaffold{ T: types.NewType("func(x float, y float) float"), - V: TestpkgMax, + F: TestpkgMax, }) - simple.ModuleRegister("golang/testpkg", "with_error", &types.FuncValue{ + simple.ModuleRegister("golang/testpkg", "with_error", &simple.Scaffold{ T: types.NewType("func(s str) str"), - V: TestpkgWithError, + F: TestpkgWithError, }) - simple.ModuleRegister("golang/testpkg", "with_int", &types.FuncValue{ + simple.ModuleRegister("golang/testpkg", "with_int", &simple.Scaffold{ T: types.NewType("func(s float, i int, x int, j int, k int, b bool, t str) str"), - V: TestpkgWithInt, + F: TestpkgWithInt, }) - simple.ModuleRegister("golang/testpkg", "super_byte", &types.FuncValue{ + simple.ModuleRegister("golang/testpkg", "super_byte", &simple.Scaffold{ T: types.NewType("func(s str, t str) str"), - V: TestpkgSuperByte, + F: TestpkgSuperByte, }) } diff --git a/lang/funcs/funcgen/templates/generated_funcs.go.tpl b/lang/funcs/funcgen/templates/generated_funcs.go.tpl index e2c01831..ad33c73e 100644 --- a/lang/funcs/funcgen/templates/generated_funcs.go.tpl +++ b/lang/funcs/funcgen/templates/generated_funcs.go.tpl @@ -39,9 +39,9 @@ import ( ) func init() { -{{ range $i, $func := .Functions }} simple.ModuleRegister("{{$func.MgmtPackage}}", "{{$func.MclName}}", &types.FuncValue{ +{{ range $i, $func := .Functions }} simple.ModuleRegister("{{$func.MgmtPackage}}", "{{$func.MclName}}", &simple.Scaffold{ T: types.NewType("{{$func.Signature}}"), - V: {{$func.InternalName}}, + F: {{$func.InternalName}}, }) {{ end }} } diff --git a/lang/funcs/funcs.go b/lang/funcs/funcs.go index 68d64487..31aceeb6 100644 --- a/lang/funcs/funcs.go +++ b/lang/funcs/funcs.go @@ -65,15 +65,17 @@ const ( // registeredFuncs is a global map of all possible funcs which can be used. You // should never touch this map directly. Use methods like Register instead. It -// includes implementations which also satisfy PolyFunc as well. +// includes implementations which also satisfy BuildableFunc and InferableFunc +// as well. var registeredFuncs = make(map[string]func() interfaces.Func) // must initialize // Register takes a func and its name and makes it available for use. It is // commonly called in the init() method of the func at program startup. There is // no matching Unregister function. You may also register functions which -// satisfy the PolyFunc interface. To register a function which lives in a -// module, you must join the module name to the function name with the ModuleSep -// character. It is defined as a const and is probably the period character. +// satisfy the BuildableFunc and InferableFunc interfaces. To register a +// function which lives in a module, you must join the module name to the +// function name with the ModuleSep character. It is defined as a const and is +// probably the period character. func Register(name string, fn func() interfaces.Func) { if _, exists := registeredFuncs[name]; exists { panic(fmt.Sprintf("a func named %s is already registered", name)) @@ -93,13 +95,6 @@ func Register(name string, fn func() interfaces.Func) { // panic(fmt.Sprintf("a func named %s is invalid", name)) //} - fnx := fn() // check that all functions have migrated to the new API! - if _, ok := fnx.(interfaces.OldPolyFunc); ok { - if _, ok := fnx.(interfaces.PolyFunc); !ok { - panic(fmt.Sprintf("a func named %s implements OldPolyFunc but not PolyFunc", name)) - } - } - //gob.Register(fn()) registeredFuncs[name] = fn } @@ -111,7 +106,8 @@ func ModuleRegister(module, name string, fn func() interfaces.Func) { } // Lookup returns a pointer to the function's struct. It may be convertible to a -// PolyFunc if the particular function implements those additional methods. +// BuildableFunc or InferableFunc if the particular function implements those +// additional methods. func Lookup(name string) (interfaces.Func, error) { f, exists := registeredFuncs[name] if !exists { @@ -180,14 +176,23 @@ func PureFuncExec(handle interfaces.Func, args []types.Value) (types.Value, erro return nil, fmt.Errorf("func is slow") } - if err := handle.Validate(); err != nil { - return nil, errwrap.Wrapf(err, "could not validate func") - } - sig := handle.Info().Sig if sig.Kind != types.KindFunc { return nil, fmt.Errorf("must be kind func") } + if sig.HasUni() { + return nil, fmt.Errorf("func contains unification vars") + } + + if buildableFunc, ok := handle.(interfaces.BuildableFunc); ok { + if _, err := buildableFunc.Build(sig); err != nil { + return nil, fmt.Errorf("can't build function: %v", err) + } + } + + if err := handle.Validate(); err != nil { + return nil, errwrap.Wrapf(err, "could not validate func") + } ord := handle.Info().Sig.Ord if i, j := len(ord), len(args); i != j { diff --git a/lang/funcs/history_func.go b/lang/funcs/history_func.go index 46bfc9bb..5f035b8c 100644 --- a/lang/funcs/history_func.go +++ b/lang/funcs/history_func.go @@ -35,7 +35,6 @@ import ( "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/types" - "github.com/purpleidea/mgmt/util/errwrap" ) const ( @@ -52,7 +51,7 @@ func init() { Register(HistoryFuncName, func() interfaces.Func { return &HistoryFunc{} }) // must register the func and name } -var _ interfaces.PolyFunc = &HistoryFunc{} // ensure it meets this expectation +var _ interfaces.BuildableFunc = &HistoryFunc{} // ensure it meets this expectation // HistoryFunc is special function which returns the Nth oldest value seen. It // must store up incoming values until it gets enough to return the desired one. @@ -90,224 +89,14 @@ func (obj *HistoryFunc) ArgGen(index int) (string, error) { return seq[index], nil } -// Unify returns the list of invariants that this func produces. -func (obj *HistoryFunc) Unify(expr interfaces.Expr) ([]interfaces.Invariant, error) { - var invariants []interfaces.Invariant - var invar interfaces.Invariant - - // func(value T1, index int) T1 - - valueName, err := obj.ArgGen(0) - if err != nil { - return nil, err +// helper +func (obj *HistoryFunc) sig() *types.Type { + // func(value ?1, index int) ?1 + s := "?1" + if obj.Type != nil { + s = obj.Type.String() } - - indexName, err := obj.ArgGen(1) - if err != nil { - return nil, err - } - - dummyValue := &interfaces.ExprAny{} // corresponds to the value type - dummyIndex := &interfaces.ExprAny{} // corresponds to the index type - dummyOut := &interfaces.ExprAny{} // corresponds to the out string - - // index arg type of int - invar = &interfaces.EqualsInvariant{ - Expr: dummyIndex, - Type: types.TypeInt, - } - invariants = append(invariants, invar) - - // index and return are the same type - invar = &interfaces.EqualityInvariant{ - Expr1: dummyValue, - Expr2: dummyOut, - } - invariants = append(invariants, invar) - - // full function - mapped := make(map[string]interfaces.Expr) - ordered := []string{valueName, indexName} - mapped[valueName] = dummyValue - mapped[indexName] = dummyIndex - - invar = &interfaces.EqualityWrapFuncInvariant{ - Expr1: expr, // maps directly to us! - Expr2Map: mapped, - Expr2Ord: ordered, - Expr2Out: dummyOut, - } - 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) - } - - var invariants []interfaces.Invariant - var invar interfaces.Invariant - - // add the relationship to the returned value - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Expr, - Expr2: dummyOut, - } - invariants = append(invariants, invar) - - // second arg must be an int - invar = &interfaces.EqualsInvariant{ - Expr: cfavInvar.Args[1], - Type: types.TypeInt, - } - invariants = append(invariants, invar) - - // add the relationships to the called args - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Args[0], - Expr2: dummyValue, - } - invariants = append(invariants, invar) - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Args[1], - Expr2: dummyIndex, - } - invariants = append(invariants, invar) - - if typ, err := cfavInvar.Args[1].Type(); err == nil { // is it known? - if k := typ.Kind; k != types.KindInt { - return nil, fmt.Errorf("unable to build function with 1st arg of kind: %s", k) - } - } - - // We just need to figure out one type to know the full - // type... - var t1 *types.Type // the value type - - // validateArg0 checks: value T1 - validateArg0 := func(typ *types.Type) error { - if typ == nil { // unknown so far - return nil - } - - if err := typ.Cmp(t1); t1 != nil && err != nil { - return errwrap.Wrapf(err, "input type was inconsistent") - } - - // learn! - t1 = typ - 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 struct 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 struct arg type is inconsistent") - } - } - - // XXX: if the struct type/value isn't know statically? - - if t1 != nil { - invar = &interfaces.EqualsInvariant{ - Expr: dummyValue, - Type: t1, - } - invariants = append(invariants, invar) - - invar = &interfaces.EqualsInvariant{ // bonus - Expr: dummyOut, - Type: t1, - } - invariants = append(invariants, invar) - } - - // 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 possible type signature for this function. In this -// case, since the number of possible types for the first arg can be infinite, -// it returns the final precise type only if it can be gleamed statically. If -// not, it returns that unknown as a variant, which is hopefully solved during -// unification. -func (obj *HistoryFunc) Polymorphisms(partialType *types.Type, partialValues []types.Value) ([]*types.Type, error) { - // TODO: return `variant` as first & return arg for now -- maybe there's a better way? - variant := []*types.Type{types.NewType("func(value variant, index int) variant")} - - if partialType == nil { - return variant, nil - } - - var typ *types.Type // = nil is implied - - ord := partialType.Ord - if partialType.Map != nil { - if len(ord) != 2 { - return nil, fmt.Errorf("must have at exactly two args in history func") - } - if t, exists := partialType.Map[ord[1]]; exists && t != nil { - if t.Cmp(types.TypeInt) != nil { - return nil, fmt.Errorf("second arg for history must be an int") - } - } - - if t, exists := partialType.Map[ord[0]]; exists && t != nil && partialType.Out != nil { - if t.Cmp(partialType.Out) != nil { - return nil, fmt.Errorf("type of first arg for history must match return type") - } - typ = t // it has been found :) - } - } - - if partialType.Out != nil { - typ = partialType.Out // it has been found :) - } - - if typ == nil { - return variant, nil - } - - t := types.NewType(fmt.Sprintf("func(value %s, index int) %s", typ.String(), typ.String())) - - return []*types.Type{t}, nil // return a list with a single possibility + return types.NewType(fmt.Sprintf("func(%s %s, %s int) %s", historyArgNameValue, s, historyArgNameIndex, s)) } // Build takes the now known function signature and stores it so that this @@ -355,24 +144,14 @@ func (obj *HistoryFunc) Validate() error { // Info returns some static info about itself. func (obj *HistoryFunc) Info() *interfaces.Info { - var sig *types.Type - if obj.Type != nil { // don't panic if called speculatively - sig = obj.sig() // helper - } return &interfaces.Info{ Pure: false, // definitely false Memo: false, - Sig: sig, + Sig: obj.sig(), // helper Err: obj.Validate(), } } -// helper -func (obj *HistoryFunc) sig() *types.Type { - s := obj.Type.String() - return types.NewType(fmt.Sprintf("func(%s %s, %s int) %s", historyArgNameValue, s, historyArgNameIndex, s)) -} - // Init runs some startup code for this function. func (obj *HistoryFunc) Init(init *interfaces.Init) error { obj.init = init diff --git a/lang/funcs/list_lookup_default_func.go b/lang/funcs/list_lookup_default_func.go index ad39242d..ac6d7600 100644 --- a/lang/funcs/list_lookup_default_func.go +++ b/lang/funcs/list_lookup_default_func.go @@ -53,12 +53,15 @@ func init() { Register(ListLookupDefaultFuncName, func() interfaces.Func { return &ListLookupDefaultFunc{} }) // must register the func and name } -var _ interfaces.PolyFunc = &ListLookupDefaultFunc{} // ensure it meets this expectation +var _ interfaces.BuildableFunc = &ListLookupDefaultFunc{} // ensure it meets this expectation // ListLookupDefaultFunc is a list index lookup function. If you provide a // negative index, then it will return the default value you specified for this // function. +// TODO: Eventually we will deprecate this function when the function engine can +// support passing a value for erroring functions. (Bad index could be an err!) type ListLookupDefaultFunc struct { + // TODO: Logically should this be ported to be the type of the elements? Type *types.Type // Kind == List, that is used as the list we lookup in init *interfaces.Init @@ -82,274 +85,20 @@ func (obj *ListLookupDefaultFunc) ArgGen(index int) (string, error) { return seq[index], nil } -// Unify returns the list of invariants that this func produces. -func (obj *ListLookupDefaultFunc) Unify(expr interfaces.Expr) ([]interfaces.Invariant, error) { - var invariants []interfaces.Invariant - var invar interfaces.Invariant - - // func(list T1, index int, default T3) T3 - // (list: []T3 => T3 aka T1 => T3) - - listName, err := obj.ArgGen(0) - if err != nil { - return nil, err +// helper +func (obj *ListLookupDefaultFunc) 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() } - - indexName, err := obj.ArgGen(1) - if err != nil { - return nil, err - } - - defaultName, err := obj.ArgGen(2) - if err != nil { - return nil, err - } - - dummyList := &interfaces.ExprAny{} // corresponds to the list type - dummyIndex := &interfaces.ExprAny{} // corresponds to the index type - dummyDefault := &interfaces.ExprAny{} // corresponds to the default type - dummyOut := &interfaces.ExprAny{} // corresponds to the out string - - // default type and out are the same - invar = &interfaces.EqualityInvariant{ - Expr1: dummyDefault, - Expr2: dummyOut, - } - invariants = append(invariants, invar) - - // relationship between T1 and T3 - invar = &interfaces.EqualityWrapListInvariant{ - Expr1: dummyList, - Expr2Val: dummyDefault, - } - invariants = append(invariants, invar) - - // the index has to be an int - invar = &interfaces.EqualsInvariant{ - Expr: dummyIndex, - Type: types.TypeInt, - } - invariants = append(invariants, invar) - - // full function - mapped := make(map[string]interfaces.Expr) - ordered := []string{listName, indexName, defaultName} - mapped[listName] = dummyList - mapped[indexName] = dummyIndex - mapped[defaultName] = dummyDefault - - invar = &interfaces.EqualityWrapFuncInvariant{ - Expr1: expr, // maps directly to us! - Expr2Map: mapped, - Expr2Ord: ordered, - Expr2Out: dummyOut, - } - 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 != 3 { - return nil, fmt.Errorf("unable to build function with %d args", l) - } - - var invariants []interfaces.Invariant - var invar interfaces.Invariant - - // add the relationship to the returned value - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Expr, - Expr2: dummyOut, - } - invariants = append(invariants, invar) - - // add the relationships to the called args - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Args[0], - Expr2: dummyList, - } - invariants = append(invariants, invar) - - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Args[1], - Expr2: dummyIndex, - } - invariants = append(invariants, invar) - - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Args[2], - Expr2: dummyDefault, - } - invariants = append(invariants, invar) - - // If we figure out either of these types, we'll know - // the full type... - var t1 *types.Type // list type - var t3 *types.Type // list val type - - // validateArg0 checks: list T1 - validateArg0 := func(typ *types.Type) error { - if typ == nil { // unknown so far - return nil - } - - // we happen to have a list! - if k := typ.Kind; k != types.KindList { - return fmt.Errorf("unable to build function with 0th arg of kind: %s", k) - } - - if typ.Val == nil { - // programming error - return fmt.Errorf("list is missing type") - } - - if err := typ.Cmp(t1); t1 != nil && err != nil { - return errwrap.Wrapf(err, "input type was inconsistent") - } - if err := typ.Val.Cmp(t3); t3 != nil && err != nil { - return errwrap.Wrapf(err, "input val type was inconsistent") - } - - // learn! - t1 = typ - t3 = typ.Val - return nil - } - - // validateArg1 checks: list index - validateArg1 := func(typ *types.Type) error { - if typ == nil { // unknown so far - return nil - } - if typ.Kind != types.KindInt { - return errwrap.Wrapf(err, "input index type was inconsistent") - } - return nil - } - - // validateArg2 checks: list val T3 - validateArg2 := func(typ *types.Type) error { - if typ == nil { // unknown so far - return nil - } - - if err := typ.Cmp(t3); t3 != nil && err != nil { - return errwrap.Wrapf(err, "input val type was inconsistent") - } - if t1 != nil { - if err := typ.Cmp(t1.Val); err != nil { - return errwrap.Wrapf(err, "input val type was inconsistent") - } - } - t := &types.Type{ // build t1 - Kind: types.KindList, - Val: typ, // t3 - } - if t3 != nil { - if err := t.Cmp(t1); t1 != nil && err != nil { - return errwrap.Wrapf(err, "input type was inconsistent") - } - //t1 = t // learn! - } - - // learn! - t1 = t - t3 = typ - return nil - } - - if typ, err := cfavInvar.Args[0].Type(); err == nil { // is it known? - // this sets t1 and t3 on success if it learned - if err := validateArg0(typ); err != nil { - return nil, errwrap.Wrapf(err, "first list arg type is inconsistent") - } - } - if typ, exists := solved[cfavInvar.Args[0]]; exists { // alternate way to lookup type - // this sets t1 and t3 on success if it learned - if err := validateArg0(typ); err != nil { - return nil, errwrap.Wrapf(err, "first list arg type is inconsistent") - } - } - - if typ, err := cfavInvar.Args[1].Type(); err == nil { // is it known? - // this only checks if this is an int - if err := validateArg1(typ); err != nil { - return nil, errwrap.Wrapf(err, "second index arg type is inconsistent") - } - } - if typ, exists := solved[cfavInvar.Args[1]]; exists { // alternate way to lookup type - // this only checks if this is an int - if err := validateArg1(typ); err != nil { - return nil, errwrap.Wrapf(err, "second index arg type is inconsistent") - } - } - - if typ, err := cfavInvar.Args[2].Type(); err == nil { // is it known? - // this sets t1 and t3 on success if it learned - if err := validateArg2(typ); err != nil { - return nil, errwrap.Wrapf(err, "third default arg type is inconsistent") - } - } - if typ, exists := solved[cfavInvar.Args[2]]; exists { // alternate way to lookup type - // this sets t1 and t3 on success if it learned - if err := validateArg2(typ); err != nil { - return nil, errwrap.Wrapf(err, "third default arg type is inconsistent") - } - } - - // XXX: if the types aren't know statically? - - if t1 != nil { - invar := &interfaces.EqualsInvariant{ - Expr: dummyList, - Type: t1, - } - invariants = append(invariants, invar) - } - if t3 != nil { - invar := &interfaces.EqualsInvariant{ - Expr: dummyDefault, - Type: t3, - } - invariants = append(invariants, invar) - } - - // XXX: if t{1..2} are missing, we could also return a - // new generator for later if we learn new information, - // but we'd have to be careful to not do it infinitely. - - // 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 + return types.NewType(fmt.Sprintf( + "func(%s []%s, %s int, %s %s) %s", + listLookupDefaultArgNameList, v, + listLookupDefaultArgNameIndex, + listLookupDefaultArgNameDefault, v, + v, + )) } // Build is run to turn the polymorphic, undetermined function, into the @@ -418,25 +167,14 @@ func (obj *ListLookupDefaultFunc) Validate() error { // Info returns some static info about itself. Build must be called before this // will return correct data. func (obj *ListLookupDefaultFunc) Info() *interfaces.Info { - var sig *types.Type - if obj.Type != nil { // don't panic if called speculatively - // TODO: can obj.Type.Key or obj.Type.Val be nil (a partial) ? - sig = obj.sig() // helper - } return &interfaces.Info{ Pure: true, Memo: false, - Sig: sig, // func kind + Sig: obj.sig(), // helper Err: obj.Validate(), } } -// helper -func (obj *ListLookupDefaultFunc) sig() *types.Type { - v := obj.Type.Val.String() - return types.NewType(fmt.Sprintf("func(%s %s, %s int, %s %s) %s", listLookupDefaultArgNameList, obj.Type.String(), listLookupDefaultArgNameIndex, listLookupDefaultArgNameDefault, v, v)) -} - // Init runs some startup code for this function. func (obj *ListLookupDefaultFunc) Init(init *interfaces.Init) error { obj.init = init diff --git a/lang/funcs/list_lookup_func.go b/lang/funcs/list_lookup_func.go index 1ffbcc94..efaef979 100644 --- a/lang/funcs/list_lookup_func.go +++ b/lang/funcs/list_lookup_func.go @@ -52,7 +52,7 @@ func init() { Register(ListLookupFuncName, func() interfaces.Func { return &ListLookupFunc{} }) // must register the func and name } -var _ interfaces.PolyFunc = &ListLookupFunc{} // ensure it meets this expectation +var _ interfaces.BuildableFunc = &ListLookupFunc{} // ensure it meets this expectation // ListLookupFunc is a list index lookup function. If you provide a negative // index, then it will return the zero value for that type. @@ -80,210 +80,19 @@ func (obj *ListLookupFunc) ArgGen(index int) (string, error) { return seq[index], nil } -// Unify returns the list of invariants that this func produces. -func (obj *ListLookupFunc) Unify(expr interfaces.Expr) ([]interfaces.Invariant, error) { - var invariants []interfaces.Invariant - var invar interfaces.Invariant - - // func(list T1, index int) T3 - // (list: []T3 => T3 aka T1 => T3) - - listName, err := obj.ArgGen(0) - if err != nil { - return nil, err +// 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() } - - indexName, err := obj.ArgGen(1) - if err != nil { - return nil, err - } - - dummyList := &interfaces.ExprAny{} // corresponds to the list type - dummyIndex := &interfaces.ExprAny{} // corresponds to the index type - dummyOut := &interfaces.ExprAny{} // corresponds to the out string - - // relationship between T1 and T3 - invar = &interfaces.EqualityWrapListInvariant{ - Expr1: dummyList, - Expr2Val: dummyOut, - } - invariants = append(invariants, invar) - - // the index has to be an int - invar = &interfaces.EqualsInvariant{ - Expr: dummyIndex, - Type: types.TypeInt, - } - invariants = append(invariants, invar) - - // full function - mapped := make(map[string]interfaces.Expr) - ordered := []string{listName, indexName} - mapped[listName] = dummyList - mapped[indexName] = dummyIndex - - invar = &interfaces.EqualityWrapFuncInvariant{ - Expr1: expr, // maps directly to us! - Expr2Map: mapped, - Expr2Ord: ordered, - Expr2Out: dummyOut, - } - 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) - } - - var invariants []interfaces.Invariant - var invar interfaces.Invariant - - // add the relationship to the returned value - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Expr, - Expr2: dummyOut, - } - invariants = append(invariants, invar) - - // add the relationships to the called args - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Args[0], - Expr2: dummyList, - } - invariants = append(invariants, invar) - - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Args[1], - Expr2: dummyIndex, - } - invariants = append(invariants, invar) - - // If we figure out either of these types, we'll know - // the full type... - var t1 *types.Type // list type - var t3 *types.Type // list val type - - // validateArg0 checks: list T1 - validateArg0 := func(typ *types.Type) error { - if typ == nil { // unknown so far - return nil - } - - // we happen to have a list! - if k := typ.Kind; k != types.KindList { - return fmt.Errorf("unable to build function with 0th arg of kind: %s", k) - } - - if typ.Val == nil { - // programming error - return fmt.Errorf("list is missing type") - } - - if err := typ.Cmp(t1); t1 != nil && err != nil { - return errwrap.Wrapf(err, "input type was inconsistent") - } - if err := typ.Val.Cmp(t3); t3 != nil && err != nil { - return errwrap.Wrapf(err, "input val type was inconsistent") - } - - // learn! - t1 = typ - t3 = typ.Val - return nil - } - - // validateArg1 checks: list index - validateArg1 := func(typ *types.Type) error { - if typ == nil { // unknown so far - return nil - } - if typ.Kind != types.KindInt { - return errwrap.Wrapf(err, "input index type was inconsistent") - } - return nil - } - - if typ, err := cfavInvar.Args[0].Type(); err == nil { // is it known? - // this sets t1 and t3 on success if it learned - if err := validateArg0(typ); err != nil { - return nil, errwrap.Wrapf(err, "first list arg type is inconsistent") - } - } - if typ, exists := solved[cfavInvar.Args[0]]; exists { // alternate way to lookup type - // this sets t1 and t3 on success if it learned - if err := validateArg0(typ); err != nil { - return nil, errwrap.Wrapf(err, "first list arg type is inconsistent") - } - } - - if typ, err := cfavInvar.Args[1].Type(); err == nil { // is it known? - // this only checks if this is an int - if err := validateArg1(typ); err != nil { - return nil, errwrap.Wrapf(err, "second index arg type is inconsistent") - } - } - if typ, exists := solved[cfavInvar.Args[1]]; exists { // alternate way to lookup type - // this only checks if this is an int - if err := validateArg1(typ); err != nil { - return nil, errwrap.Wrapf(err, "second index arg type is inconsistent") - } - } - - // XXX: if the types aren't know statically? - - if t1 != nil { - invar := &interfaces.EqualsInvariant{ - Expr: dummyList, - Type: t1, - } - invariants = append(invariants, invar) - } - if t3 != nil { - invar := &interfaces.EqualsInvariant{ - Expr: dummyOut, - Type: t3, - } - invariants = append(invariants, invar) - } - - // XXX: if t{1..2} are missing, we could also return a - // new generator for later if we learn new information, - // but we'd have to be careful to not do it infinitely. - - // 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 + 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 @@ -343,25 +152,14 @@ func (obj *ListLookupFunc) Validate() error { // Info returns some static info about itself. Build must be called before this // will return correct data. func (obj *ListLookupFunc) Info() *interfaces.Info { - var sig *types.Type - if obj.Type != nil { // don't panic if called speculatively - // TODO: can obj.Type.Key or obj.Type.Val be nil (a partial) ? - sig = obj.sig() // helper - } return &interfaces.Info{ Pure: true, Memo: false, - Sig: sig, // func kind + Sig: obj.sig(), // helper Err: obj.Validate(), } } -// helper -func (obj *ListLookupFunc) sig() *types.Type { - v := obj.Type.Val.String() - return types.NewType(fmt.Sprintf("func(%s %s, %s int) %s", listLookupArgNameList, obj.Type.String(), listLookupArgNameIndex, v)) -} - // Init runs some startup code for this function. func (obj *ListLookupFunc) Init(init *interfaces.Init) error { obj.init = init diff --git a/lang/funcs/lookup_default_func.go b/lang/funcs/lookup_default_func.go index 90a01bdd..9ea21e89 100644 --- a/lang/funcs/lookup_default_func.go +++ b/lang/funcs/lookup_default_func.go @@ -35,7 +35,6 @@ import ( "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/types" - "github.com/purpleidea/mgmt/util/errwrap" ) const ( @@ -54,7 +53,7 @@ func init() { Register(LookupDefaultFuncName, func() interfaces.Func { return &LookupDefaultFunc{} }) // must register the func and name } -var _ interfaces.PolyFunc = &LookupDefaultFunc{} // ensure it meets this expectation +var _ interfaces.BuildableFunc = &LookupDefaultFunc{} // ensure it meets this expectation // LookupDefaultFunc is a list index or map key lookup function. It does both // because the current syntax in the parser is identical, so it's convenient to @@ -62,11 +61,13 @@ var _ interfaces.PolyFunc = &LookupDefaultFunc{} // ensure it meets this expecta // ListLookupDefaultFunc and MapLookupDefaultFunc implementations. If the index // or key for this input doesn't exist, then it will return the default value // you specified for this function. +// TODO: Eventually we will deprecate this function when the function engine can +// support passing a value for erroring functions. (Bad index could be an err!) type LookupDefaultFunc struct { Type *types.Type // Kind == List OR Map, that is used as the list/map we lookup in //init *interfaces.Init - fn interfaces.PolyFunc // handle to ListLookupDefaultFunc or MapLookupDefaultFunc + fn interfaces.BuildableFunc // handle to ListLookupDefaultFunc or MapLookupDefaultFunc } // String returns a simple name for this function. This is needed so this struct @@ -84,402 +85,6 @@ func (obj *LookupDefaultFunc) ArgGen(index int) (string, error) { return seq[index], nil } -// Unify returns the list of invariants that this func produces. -func (obj *LookupDefaultFunc) Unify(expr interfaces.Expr) ([]interfaces.Invariant, error) { - var invariants []interfaces.Invariant - var invar interfaces.Invariant - - // func(list T1, index int, default T3) T3 - // (list: []T3 => T3 aka T1 => T3) - // OR - // func(map T1, key T2, default T3) T3 - // (map: T2 => T3) - - listOrMapName, err := obj.ArgGen(0) - if err != nil { - return nil, err - } - - indexOrKeyName, err := obj.ArgGen(1) - if err != nil { - return nil, err - } - - defaultName, err := obj.ArgGen(2) - if err != nil { - return nil, err - } - - dummyListOrMap := &interfaces.ExprAny{} // corresponds to the list or map type - dummyIndexOrKey := &interfaces.ExprAny{} // corresponds to the index or key type - dummyDefault := &interfaces.ExprAny{} // corresponds to the default type - dummyOut := &interfaces.ExprAny{} // corresponds to the out string - - // default type and out are the same - invar = &interfaces.EqualityInvariant{ - Expr1: dummyDefault, - Expr2: dummyOut, - } - invariants = append(invariants, invar) - - ors := []interfaces.Invariant{} // solve only one from this list - - var listInvariants []interfaces.Invariant - - // relationship between T1 and T3 - invar = &interfaces.EqualityWrapListInvariant{ - Expr1: dummyListOrMap, - Expr2Val: dummyDefault, - } - listInvariants = append(listInvariants, invar) - - // the index has to be an int - invar = &interfaces.EqualsInvariant{ - Expr: dummyIndexOrKey, - Type: types.TypeInt, - } - listInvariants = append(listInvariants, invar) - - // all of these need to be true together - and := &interfaces.ConjunctionInvariant{ - Invariants: listInvariants, - } - ors = append(ors, and) // one solution added! - - // OR - - // relationship between T1, T2 and T3 - mapInvariant := &interfaces.EqualityWrapMapInvariant{ - Expr1: dummyListOrMap, - Expr2Key: dummyIndexOrKey, - Expr2Val: dummyDefault, - } - ors = append(ors, mapInvariant) // one solution added! - - invar = &interfaces.ExclusiveInvariant{ - Invariants: ors, // one and only one of these should be true - } - invariants = append(invariants, invar) - - // full function - mapped := make(map[string]interfaces.Expr) - ordered := []string{listOrMapName, indexOrKeyName, defaultName} - mapped[listOrMapName] = dummyListOrMap - mapped[indexOrKeyName] = dummyIndexOrKey - mapped[defaultName] = dummyDefault - - invar = &interfaces.EqualityWrapFuncInvariant{ - Expr1: expr, // maps directly to us! - Expr2Map: mapped, - Expr2Ord: ordered, - Expr2Out: dummyOut, - } - 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 != 3 { - return nil, fmt.Errorf("unable to build function with %d args", l) - } - - var invariants []interfaces.Invariant - var invar interfaces.Invariant - - // add the relationship to the returned value - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Expr, - Expr2: dummyOut, - } - invariants = append(invariants, invar) - - // add the relationships to the called args - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Args[0], - Expr2: dummyListOrMap, - } - invariants = append(invariants, invar) - - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Args[1], - Expr2: dummyIndexOrKey, - } - invariants = append(invariants, invar) - - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Args[2], - Expr2: dummyDefault, - } - invariants = append(invariants, invar) - - // If we figure out all of these types, we'll know the - // full type... - var t1 *types.Type // list or map type - var t2 *types.Type // list or map index/key type - var t3 *types.Type // list or map val type - - // validateArg0 checks: list or map T1 - validateArg0 := func(typ *types.Type) error { - if typ == nil { // unknown so far - return nil - } - - // we happen to have a list or a map! - if k := typ.Kind; k != types.KindList && k != types.KindMap { - return fmt.Errorf("unable to build function with 0th arg of kind: %s", k) - } - //isList := typ.Kind == types.KindList - isMap := typ.Kind == types.KindMap - - if isMap && typ.Key == nil { - // programming error - return fmt.Errorf("map is missing type") - } - if typ.Val == nil { // used for list or map - // programming error - return fmt.Errorf("map/list is missing type") - } - - if err := typ.Cmp(t1); t1 != nil && err != nil { - return errwrap.Wrapf(err, "input type was inconsistent") - } - if isMap { - if err := typ.Key.Cmp(t2); t2 != nil && err != nil { - return errwrap.Wrapf(err, "input key type was inconsistent") - } - } - if err := typ.Val.Cmp(t3); t3 != nil && err != nil { - return errwrap.Wrapf(err, "input val type was inconsistent") - } - - // learn! - t1 = typ - if isMap { - t2 = typ.Key - } else if t1 != nil && t3 != nil { - t2 = types.TypeInt - } - t3 = typ.Val - return nil - } - - // validateArg1 checks: list index - validateListArg1 := func(typ *types.Type) error { - if typ == nil { // unknown so far - return nil - } - if typ.Kind != types.KindInt { - return errwrap.Wrapf(err, "input index type was inconsistent") - } - - // learn! - t2 = typ - return nil - } - - // validateArg1 checks: map key T2 - validateMapArg1 := func(typ *types.Type) error { - if typ == nil { // unknown so far - return nil - } - - if err := typ.Cmp(t2); t2 != nil && err != nil { - return errwrap.Wrapf(err, "input key type was inconsistent") - } - if t1 != nil { - if err := typ.Cmp(t1.Key); err != nil { - return errwrap.Wrapf(err, "input key type was inconsistent") - } - } - if t3 != nil { - t := &types.Type{ // build t1 - Kind: types.KindMap, - Key: typ, // t2 - Val: t3, - } - - if err := t.Cmp(t1); t1 != nil && err != nil { - return errwrap.Wrapf(err, "input type was inconsistent") - } - t1 = t // learn! - } - - // learn! - t2 = typ - return nil - } - - // validateArg1 checks: list index - validateArg1 := func(typ *types.Type) error { - if typ == nil { // unknown so far - return nil - } - isList := typ.Kind == types.KindList - isMap := typ.Kind == types.KindMap - - if isList { - return validateListArg1(typ) - } - if isMap { - return validateMapArg1(typ) - } - - return nil - } - - // validateArg2 checks: list or map val T3 - validateArg2 := func(typ *types.Type) error { - if typ == nil { // unknown so far - return nil - } - - if err := typ.Cmp(t3); t3 != nil && err != nil { - return errwrap.Wrapf(err, "input val type was inconsistent") - } - if t1 != nil { - if err := typ.Cmp(t1.Val); err != nil { - return errwrap.Wrapf(err, "input val type was inconsistent") - } - } - isList := typ.Kind == types.KindList - isMap := typ.Kind == types.KindMap - - if isMap && t2 != nil { - t := &types.Type{ // build t1 - Kind: types.KindMap, - Key: t2, - Val: typ, // t3 - } - - if err := t.Cmp(t1); t1 != nil && err != nil { - return errwrap.Wrapf(err, "input type was inconsistent") - } - t1 = t // learn! - } - - t := &types.Type{ // build t1 (for lists) - Kind: types.KindList, - Val: typ, // t3 - } - if isList && t3 != nil { - if err := t.Cmp(t1); t1 != nil && err != nil { - return errwrap.Wrapf(err, "input type was inconsistent") - } - //t1 = t // learn! - } - - // learn! - if isList { - t1 = t - if t1 != nil && t3 != nil { - t2 = types.TypeInt - } - } - t3 = typ - return nil - } - - if typ, err := cfavInvar.Args[0].Type(); err == nil { // is it known? - // this sets t1 and t3 on success (and sometimes t2) if it learned - if err := validateArg0(typ); err != nil { - return nil, errwrap.Wrapf(err, "first arg type is inconsistent") - } - } - if typ, exists := solved[cfavInvar.Args[0]]; exists { // alternate way to lookup type - // this sets t1 and t3 on success (and sometimes t2) if it learned - if err := validateArg0(typ); err != nil { - return nil, errwrap.Wrapf(err, "first arg type is inconsistent") - } - } - - if typ, err := cfavInvar.Args[1].Type(); err == nil { // is it known? - // this sets t2 (and sometimes t1) on success if it learned - if err := validateArg1(typ); err != nil { - return nil, errwrap.Wrapf(err, "second arg type is inconsistent") - } - } - if typ, exists := solved[cfavInvar.Args[1]]; exists { // alternate way to lookup type - // this sets t2 (and sometimes t1) on success if it learned - if err := validateArg1(typ); err != nil { - return nil, errwrap.Wrapf(err, "second arg type is inconsistent") - } - } - - if typ, err := cfavInvar.Args[2].Type(); err == nil { // is it known? - // this sets t3 (and sometimes t1 (and sometimes t2)) on success if it learned - if err := validateArg2(typ); err != nil { - return nil, errwrap.Wrapf(err, "third default arg type is inconsistent") - } - } - if typ, exists := solved[cfavInvar.Args[2]]; exists { // alternate way to lookup type - // this sets t3 (and sometimes t1 (and sometimes t2)) on success if it learned - if err := validateArg2(typ); err != nil { - return nil, errwrap.Wrapf(err, "third default arg type is inconsistent") - } - } - - // XXX: if the types aren't know statically? - - if t1 != nil { - invar := &interfaces.EqualsInvariant{ - Expr: dummyListOrMap, - Type: t1, - } - invariants = append(invariants, invar) - } - if t2 != nil { - invar := &interfaces.EqualsInvariant{ - Expr: dummyIndexOrKey, - Type: t2, - } - invariants = append(invariants, invar) - } - if t3 != nil { - invar := &interfaces.EqualsInvariant{ - Expr: dummyDefault, - Type: t3, - } - invariants = append(invariants, invar) - } - - // XXX: if t{1..2} are missing, we could also return a - // new generator for later if we learn new information, - // but we'd have to be careful to not do it infinitely. - - // 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 -} - // 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 @@ -525,11 +130,14 @@ func (obj *LookupDefaultFunc) Validate() error { // Info returns some static info about itself. Build must be called before this // will return correct data. func (obj *LookupDefaultFunc) Info() *interfaces.Info { + // func(list []?1, index int, default ?1) ?1 + // OR + // func(map map{?1: ?2}, key ?1, default ?2) ?2 if obj.fn == nil { return &interfaces.Info{ Pure: true, Memo: false, - Sig: nil, // func kind + Sig: types.NewType("func(?1, ?2, ?3) ?3"), // func kind Err: obj.Validate(), } } diff --git a/lang/funcs/lookup_func.go b/lang/funcs/lookup_func.go index b3674404..55c7678a 100644 --- a/lang/funcs/lookup_func.go +++ b/lang/funcs/lookup_func.go @@ -35,7 +35,6 @@ import ( "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/types" - "github.com/purpleidea/mgmt/util/errwrap" ) const ( @@ -53,18 +52,20 @@ func init() { Register(LookupFuncName, func() interfaces.Func { return &LookupFunc{} }) // must register the func and name } -var _ interfaces.PolyFunc = &LookupFunc{} // ensure it meets this expectation +var _ interfaces.BuildableFunc = &LookupFunc{} // ensure it meets this expectation // LookupFunc is a list index or map key lookup function. It does both because // the current syntax in the parser is identical, so it's convenient to mix the // two together. This calls out to some of the code in the ListLookupFunc and // MapLookupFunc implementations. If the index or key for this input doesn't // exist, then it will return the zero value for that type. +// TODO: Eventually we will deprecate this function when the function engine can +// support passing a value for erroring functions. (Bad index could be an err!) type LookupFunc struct { Type *types.Type // Kind == List OR Map, that is used as the list/map we lookup in //init *interfaces.Init - fn interfaces.PolyFunc // handle to ListLookupFunc or MapLookupFunc + fn interfaces.BuildableFunc // handle to ListLookupFunc or MapLookupFunc } // String returns a simple name for this function. This is needed so this struct @@ -82,317 +83,6 @@ func (obj *LookupFunc) ArgGen(index int) (string, error) { return seq[index], nil } -// Unify returns the list of invariants that this func produces. -func (obj *LookupFunc) Unify(expr interfaces.Expr) ([]interfaces.Invariant, error) { - var invariants []interfaces.Invariant - var invar interfaces.Invariant - - // func(list T1, index int) T3 - // (list: []T3 => T3 aka T1 => T3) - // OR - // func(map T1, key T2) T3 - // (map: T2 => T3) - - listOrMapName, err := obj.ArgGen(0) - if err != nil { - return nil, err - } - - indexOrKeyName, err := obj.ArgGen(1) - if err != nil { - return nil, err - } - - dummyListOrMap := &interfaces.ExprAny{} // corresponds to the list or map type - dummyIndexOrKey := &interfaces.ExprAny{} // corresponds to the index or key type - dummyOut := &interfaces.ExprAny{} // corresponds to the out string - - ors := []interfaces.Invariant{} // solve only one from this list - - var listInvariants []interfaces.Invariant - - // relationship between T1 and T3 - invar = &interfaces.EqualityWrapListInvariant{ - Expr1: dummyListOrMap, - Expr2Val: dummyOut, - } - listInvariants = append(listInvariants, invar) - - // the index has to be an int - invar = &interfaces.EqualsInvariant{ - Expr: dummyIndexOrKey, - Type: types.TypeInt, - } - listInvariants = append(listInvariants, invar) - - // all of these need to be true together - and := &interfaces.ConjunctionInvariant{ - Invariants: listInvariants, - } - ors = append(ors, and) // one solution added! - - // OR - - // relationship between T1, T2 and T3 - mapInvariant := &interfaces.EqualityWrapMapInvariant{ - Expr1: dummyListOrMap, - Expr2Key: dummyIndexOrKey, - Expr2Val: dummyOut, - } - ors = append(ors, mapInvariant) // one solution added! - - invar = &interfaces.ExclusiveInvariant{ - Invariants: ors, // one and only one of these should be true - } - invariants = append(invariants, invar) - - // full function - mapped := make(map[string]interfaces.Expr) - ordered := []string{listOrMapName, indexOrKeyName} - mapped[listOrMapName] = dummyListOrMap - mapped[indexOrKeyName] = dummyIndexOrKey - - invar = &interfaces.EqualityWrapFuncInvariant{ - Expr1: expr, // maps directly to us! - Expr2Map: mapped, - Expr2Ord: ordered, - Expr2Out: dummyOut, - } - 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) - } - - var invariants []interfaces.Invariant - var invar interfaces.Invariant - - // add the relationship to the returned value - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Expr, - Expr2: dummyOut, - } - invariants = append(invariants, invar) - - // add the relationships to the called args - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Args[0], - Expr2: dummyListOrMap, - } - invariants = append(invariants, invar) - - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Args[1], - Expr2: dummyIndexOrKey, - } - invariants = append(invariants, invar) - - // If we figure out all of these three types, we'll - // know the full type... - var t1 *types.Type // list or map type - var t2 *types.Type // list or map index/key type - var t3 *types.Type // list or map val type - - // validateArg0 checks: list or map T1 - validateArg0 := func(typ *types.Type) error { - if typ == nil { // unknown so far - return nil - } - - // we happen to have a list or a map! - if k := typ.Kind; k != types.KindList && k != types.KindMap { - return fmt.Errorf("unable to build function with 0th arg of kind: %s", k) - } - //isList := typ.Kind == types.KindList - isMap := typ.Kind == types.KindMap - - if isMap && typ.Key == nil { - // programming error - return fmt.Errorf("map is missing type") - } - if typ.Val == nil { // used for list or map - // programming error - return fmt.Errorf("map/list is missing type") - } - - if err := typ.Cmp(t1); t1 != nil && err != nil { - return errwrap.Wrapf(err, "input type was inconsistent") - } - if isMap { - if err := typ.Key.Cmp(t2); t2 != nil && err != nil { - return errwrap.Wrapf(err, "input key type was inconsistent") - } - } - if err := typ.Val.Cmp(t3); t3 != nil && err != nil { - return errwrap.Wrapf(err, "input val type was inconsistent") - } - - // learn! - t1 = typ - if isMap { - t2 = typ.Key - } else if t1 != nil && t3 != nil { - t2 = types.TypeInt - } - t3 = typ.Val - return nil - } - - // validateArg1 checks: list index - validateListArg1 := func(typ *types.Type) error { - if typ == nil { // unknown so far - return nil - } - if typ.Kind != types.KindInt { - return errwrap.Wrapf(err, "input index type was inconsistent") - } - - // learn! - t2 = typ - return nil - } - - // validateArg1 checks: map key T2 - validateMapArg1 := func(typ *types.Type) error { - if typ == nil { // unknown so far - return nil - } - - if err := typ.Cmp(t2); t2 != nil && err != nil { - return errwrap.Wrapf(err, "input key type was inconsistent") - } - if t1 != nil { - if err := typ.Cmp(t1.Key); err != nil { - return errwrap.Wrapf(err, "input key type was inconsistent") - } - } - if t3 != nil { - t := &types.Type{ // build t1 - Kind: types.KindMap, - Key: typ, // t2 - Val: t3, - } - - if err := t.Cmp(t1); t1 != nil && err != nil { - return errwrap.Wrapf(err, "input type was inconsistent") - } - t1 = t // learn! - } - - // learn! - t2 = typ - return nil - } - - // validateArg1 checks: list index - validateArg1 := func(typ *types.Type) error { - if typ == nil { // unknown so far - return nil - } - isList := typ.Kind == types.KindList - isMap := typ.Kind == types.KindMap - - if isList { - return validateListArg1(typ) - } - if isMap { - return validateMapArg1(typ) - } - - return nil - } - - if typ, err := cfavInvar.Args[0].Type(); err == nil { // is it known? - // this sets t1 and t3 on success (and sometimes t2) if it learned - if err := validateArg0(typ); err != nil { - return nil, errwrap.Wrapf(err, "first arg type is inconsistent") - } - } - if typ, exists := solved[cfavInvar.Args[0]]; exists { // alternate way to lookup type - // this sets t1 and t3 on success (and sometimes t2) if it learned - if err := validateArg0(typ); err != nil { - return nil, errwrap.Wrapf(err, "first arg type is inconsistent") - } - } - - if typ, err := cfavInvar.Args[1].Type(); err == nil { // is it known? - // this sets t2 (and sometimes t1) on success if it learned - if err := validateArg1(typ); err != nil { - return nil, errwrap.Wrapf(err, "second arg type is inconsistent") - } - } - if typ, exists := solved[cfavInvar.Args[1]]; exists { // alternate way to lookup type - // this sets t2 (and sometimes t1) on success if it learned - if err := validateArg1(typ); err != nil { - return nil, errwrap.Wrapf(err, "second arg type is inconsistent") - } - } - - // XXX: if the types aren't know statically? - - if t1 != nil { - invar := &interfaces.EqualsInvariant{ - Expr: dummyListOrMap, - Type: t1, - } - invariants = append(invariants, invar) - } - if t2 != nil { - invar := &interfaces.EqualsInvariant{ - Expr: dummyIndexOrKey, - Type: t2, - } - invariants = append(invariants, invar) - } - if t3 != nil { - invar := &interfaces.EqualsInvariant{ - Expr: dummyOut, - Type: t3, - } - invariants = append(invariants, invar) - } - - // XXX: if t{1..2} are missing, we could also return a - // new generator for later if we learn new information, - // but we'd have to be careful to not do it infinitely. - - // 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 -} - // 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 @@ -438,11 +128,14 @@ func (obj *LookupFunc) Validate() error { // Info returns some static info about itself. Build must be called before this // will return correct data. func (obj *LookupFunc) Info() *interfaces.Info { + // func(list []?1, index int) ?1 + // OR + // func(map map{?1: ?2}, key ?1) ?2 if obj.fn == nil { return &interfaces.Info{ Pure: true, Memo: false, - Sig: nil, // func kind + Sig: types.NewType("func(?1, ?2) ?3"), // func kind Err: obj.Validate(), } } diff --git a/lang/funcs/map_lookup_default_func.go b/lang/funcs/map_lookup_default_func.go index 6c2f6004..d71c1cca 100644 --- a/lang/funcs/map_lookup_default_func.go +++ b/lang/funcs/map_lookup_default_func.go @@ -52,10 +52,12 @@ func init() { Register(MapLookupDefaultFuncName, func() interfaces.Func { return &MapLookupDefaultFunc{} }) // must register the func and name } -var _ interfaces.PolyFunc = &MapLookupDefaultFunc{} // ensure it meets this expectation +var _ interfaces.BuildableFunc = &MapLookupDefaultFunc{} // ensure it meets this expectation // MapLookupDefaultFunc is a key map lookup function. If you provide a missing // key, then it will return the default value you specified for this function. +// TODO: Eventually we will deprecate this function when the function engine can +// support passing a value for erroring functions. (Bad index could be an err!) type MapLookupDefaultFunc struct { Type *types.Type // Kind == Map, that is used as the map we lookup @@ -80,399 +82,24 @@ func (obj *MapLookupDefaultFunc) ArgGen(index int) (string, error) { return seq[index], nil } -// Unify returns the list of invariants that this func produces. -func (obj *MapLookupDefaultFunc) Unify(expr interfaces.Expr) ([]interfaces.Invariant, error) { - var invariants []interfaces.Invariant - var invar interfaces.Invariant - - // func(map T1, key T2, default T3) T3 - // (map: T2 => T3) - - mapName, err := obj.ArgGen(0) - if err != nil { - return nil, err +// helper +func (obj *MapLookupDefaultFunc) 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() } - - keyName, err := obj.ArgGen(1) - if err != nil { - return nil, err - } - - defaultName, err := obj.ArgGen(2) - if err != nil { - return nil, err - } - - dummyMap := &interfaces.ExprAny{} // corresponds to the map type - dummyKey := &interfaces.ExprAny{} // corresponds to the key type - dummyDefault := &interfaces.ExprAny{} // corresponds to the default type - dummyOut := &interfaces.ExprAny{} // corresponds to the out string - - // default type and out are the same - invar = &interfaces.EqualityInvariant{ - Expr1: dummyDefault, - Expr2: dummyOut, - } - invariants = append(invariants, invar) - - // relationship between T1, T2 and T3 - invar = &interfaces.EqualityWrapMapInvariant{ - Expr1: dummyMap, - Expr2Key: dummyKey, - Expr2Val: dummyDefault, - } - invariants = append(invariants, invar) - - // full function - mapped := make(map[string]interfaces.Expr) - ordered := []string{mapName, keyName, defaultName} - mapped[mapName] = dummyMap - mapped[keyName] = dummyKey - mapped[defaultName] = dummyDefault - - invar = &interfaces.EqualityWrapFuncInvariant{ - Expr1: expr, // maps directly to us! - Expr2Map: mapped, - Expr2Ord: ordered, - Expr2Out: dummyOut, - } - 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 != 3 { - return nil, fmt.Errorf("unable to build function with %d args", l) - } - - var invariants []interfaces.Invariant - var invar interfaces.Invariant - - // add the relationship to the returned value - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Expr, - Expr2: dummyOut, - } - invariants = append(invariants, invar) - - // add the relationships to the called args - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Args[0], - Expr2: dummyMap, - } - invariants = append(invariants, invar) - - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Args[1], - Expr2: dummyKey, - } - invariants = append(invariants, invar) - - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Args[2], - Expr2: dummyDefault, - } - invariants = append(invariants, invar) - - // If we figure out all of these three types, we'll - // know the full type... - var t1 *types.Type // map type - var t2 *types.Type // map key type - var t3 *types.Type // map val type - - // validateArg0 checks: map T1 - validateArg0 := func(typ *types.Type) error { - if typ == nil { // unknown so far - return nil - } - - // we happen to have a map! - if k := typ.Kind; k != types.KindMap { - return fmt.Errorf("unable to build function with 0th arg of kind: %s", k) - } - - if typ.Key == nil || typ.Val == nil { - // programming error - return fmt.Errorf("map is missing type") - } - - if err := typ.Cmp(t1); t1 != nil && err != nil { - return errwrap.Wrapf(err, "input type was inconsistent") - } - if err := typ.Key.Cmp(t2); t2 != nil && err != nil { - return errwrap.Wrapf(err, "input key type was inconsistent") - } - if err := typ.Val.Cmp(t3); t3 != nil && err != nil { - return errwrap.Wrapf(err, "input val type was inconsistent") - } - - // learn! - t1 = typ - t2 = typ.Key - t3 = typ.Val - return nil - } - - // validateArg1 checks: map key T2 - validateArg1 := func(typ *types.Type) error { - if typ == nil { // unknown so far - return nil - } - - if err := typ.Cmp(t2); t2 != nil && err != nil { - return errwrap.Wrapf(err, "input key type was inconsistent") - } - if t1 != nil { - if err := typ.Cmp(t1.Key); err != nil { - return errwrap.Wrapf(err, "input key type was inconsistent") - } - } - if t3 != nil { - t := &types.Type{ // build t1 - Kind: types.KindMap, - Key: typ, // t2 - Val: t3, - } - - if err := t.Cmp(t1); t1 != nil && err != nil { - return errwrap.Wrapf(err, "input type was inconsistent") - } - t1 = t // learn! - } - - // learn! - t2 = typ - return nil - } - - // validateArg2 checks: map val T3 - validateArg2 := func(typ *types.Type) error { - if typ == nil { // unknown so far - return nil - } - - if err := typ.Cmp(t3); t3 != nil && err != nil { - return errwrap.Wrapf(err, "input val type was inconsistent") - } - if t1 != nil { - if err := typ.Cmp(t1.Val); err != nil { - return errwrap.Wrapf(err, "input val type was inconsistent") - } - } - if t2 != nil { - t := &types.Type{ // build t1 - Kind: types.KindMap, - Key: t2, - Val: typ, // t3 - } - - if err := t.Cmp(t1); t1 != nil && err != nil { - return errwrap.Wrapf(err, "input type was inconsistent") - } - t1 = t // learn! - } - - // learn! - t3 = typ - return nil - } - - if typ, err := cfavInvar.Args[0].Type(); err == nil { // is it known? - // this sets t1 and t2 and t3 on success if it learned - if err := validateArg0(typ); err != nil { - return nil, errwrap.Wrapf(err, "first map arg type is inconsistent") - } - } - if typ, exists := solved[cfavInvar.Args[0]]; exists { // alternate way to lookup type - // this sets t1 and t2 and t3 on success if it learned - if err := validateArg0(typ); err != nil { - return nil, errwrap.Wrapf(err, "first map arg type is inconsistent") - } - } - - if typ, err := cfavInvar.Args[1].Type(); err == nil { // is it known? - // this sets t2 (and sometimes t1) on success if it learned - if err := validateArg1(typ); err != nil { - return nil, errwrap.Wrapf(err, "second key arg type is inconsistent") - } - } - if typ, exists := solved[cfavInvar.Args[1]]; exists { // alternate way to lookup type - // this sets t2 (and sometimes t1) on success if it learned - if err := validateArg1(typ); err != nil { - return nil, errwrap.Wrapf(err, "second key arg type is inconsistent") - } - } - - if typ, err := cfavInvar.Args[2].Type(); err == nil { // is it known? - // this sets t3 (and sometimes t1) on success if it learned - if err := validateArg2(typ); err != nil { - return nil, errwrap.Wrapf(err, "third default arg type is inconsistent") - } - } - if typ, exists := solved[cfavInvar.Args[2]]; exists { // alternate way to lookup type - // this sets t3 (and sometimes t1) on success if it learned - if err := validateArg2(typ); err != nil { - return nil, errwrap.Wrapf(err, "third default arg type is inconsistent") - } - } - - // XXX: if the types aren't know statically? - - if t1 != nil { - invar := &interfaces.EqualsInvariant{ - Expr: dummyMap, - Type: t1, - } - invariants = append(invariants, invar) - } - if t2 != nil { - invar := &interfaces.EqualsInvariant{ - Expr: dummyKey, - Type: t2, - } - invariants = append(invariants, invar) - } - if t3 != nil { - invar := &interfaces.EqualsInvariant{ - Expr: dummyDefault, - Type: t3, - } - invariants = append(invariants, invar) - } - - // XXX: if t{1..3} are missing, we could also return a - // new generator for later if we learn new information, - // but we'd have to be careful to not do it infinitely. - - // 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 *MapLookupDefaultFunc) Polymorphisms(partialType *types.Type, partialValues []types.Value) ([]*types.Type, error) { - // TODO: return `variant` as arg for now -- maybe there's a better way? - variant := []*types.Type{types.NewType("func(map variant, key variant, default variant) variant")} - - if partialType == nil { - return variant, nil - } - - // what's the map type of the first argument? - typ := &types.Type{ - Kind: types.KindMap, - //Key: ???, - //Val: ???, - } - - ord := partialType.Ord - if partialType.Map != nil { - if len(ord) != 3 { - return nil, fmt.Errorf("must have exactly three args in maplookup func") - } - if tMap, exists := partialType.Map[ord[0]]; exists && tMap != nil { - if tMap.Kind != types.KindMap { - return nil, fmt.Errorf("first arg for maplookup must be a map") - } - typ.Key = tMap.Key - typ.Val = tMap.Val - } - if tKey, exists := partialType.Map[ord[1]]; exists && tKey != nil { - if typ.Key != nil && typ.Key.Cmp(tKey) != nil { - return nil, fmt.Errorf("second arg for maplookup must match map's key type") - } - typ.Key = tKey - } - if tDef, exists := partialType.Map[ord[2]]; exists && tDef != nil { - if typ.Val != nil && typ.Val.Cmp(tDef) != nil { - return nil, fmt.Errorf("third arg for maplookup must match map's val type") - } - typ.Val = tDef - - // add this for better error messages - if tOut := partialType.Out; tOut != nil { - if tDef.Cmp(tOut) != nil { - return nil, fmt.Errorf("third arg for maplookup must match return type") - } - } - } - if tOut := partialType.Out; tOut != nil { - if typ.Val != nil && typ.Val.Cmp(tOut) != nil { - return nil, fmt.Errorf("return type for maplookup must match map's val type") - } - typ.Val = tOut - } - } - - // TODO: are we okay adding just the map val type and not the map key type? - //if tOut := partialType.Out; tOut != nil { - // if typ.Val != nil && typ.Val.Cmp(tOut) != nil { - // return nil, fmt.Errorf("return type for maplookup must match map's val type") - // } - // typ.Val = tOut - //} - - typFunc := &types.Type{ - Kind: types.KindFunc, // function type - Map: make(map[string]*types.Type), - Ord: []string{mapLookupDefaultArgNameMap, mapLookupDefaultArgNameKey, mapLookupDefaultArgNameDef}, - Out: nil, - } - typFunc.Map[mapLookupDefaultArgNameMap] = typ - typFunc.Map[mapLookupDefaultArgNameKey] = typ.Key - typFunc.Map[mapLookupDefaultArgNameDef] = typ.Val - typFunc.Out = typ.Val - - // TODO: don't include partial internal func map's for now, allow in future? - if typ.Key == nil || typ.Val == nil { - typFunc.Map = make(map[string]*types.Type) // erase partial - typFunc.Map[mapLookupDefaultArgNameMap] = types.TypeVariant - typFunc.Map[mapLookupDefaultArgNameKey] = types.TypeVariant - typFunc.Map[mapLookupDefaultArgNameDef] = types.TypeVariant - } - if typ.Val == nil { - typFunc.Out = types.TypeVariant - } - - // just returning nothing for now, in case we can't detect a partial map - if typ.Key == nil || typ.Val == nil { - return []*types.Type{typFunc}, nil - } - - // TODO: type check that the partialValues are compatible - - return []*types.Type{typFunc}, nil // solved! + return types.NewType(fmt.Sprintf( + "func(%s %s, %s %s, %s %s) %s", + mapLookupDefaultArgNameMap, m, + mapLookupDefaultArgNameKey, k, + mapLookupDefaultArgNameDef, v, + v, + )) } // Build is run to turn the polymorphic, undetermined function, into the @@ -541,26 +168,14 @@ func (obj *MapLookupDefaultFunc) Validate() error { // Info returns some static info about itself. Build must be called before this // will return correct data. func (obj *MapLookupDefaultFunc) Info() *interfaces.Info { - var sig *types.Type - if obj.Type != nil { // don't panic if called speculatively - // TODO: can obj.Type.Key or obj.Type.Val be nil (a partial) ? - sig = obj.sig() // helper - } return &interfaces.Info{ Pure: true, Memo: false, - Sig: sig, // func kind + Sig: obj.sig(), // helper Err: obj.Validate(), } } -// helper -func (obj *MapLookupDefaultFunc) sig() *types.Type { - k := obj.Type.Key.String() - v := obj.Type.Val.String() - return types.NewType(fmt.Sprintf("func(%s %s, %s %s, %s %s) %s", mapLookupDefaultArgNameMap, obj.Type.String(), mapLookupDefaultArgNameKey, k, mapLookupDefaultArgNameDef, v, v)) -} - // Init runs some startup code for this function. func (obj *MapLookupDefaultFunc) Init(init *interfaces.Init) error { obj.init = init diff --git a/lang/funcs/map_lookup_func.go b/lang/funcs/map_lookup_func.go index 76f31c44..3f5fd851 100644 --- a/lang/funcs/map_lookup_func.go +++ b/lang/funcs/map_lookup_func.go @@ -51,7 +51,7 @@ func init() { Register(MapLookupFuncName, func() interfaces.Func { return &MapLookupFunc{} }) // must register the func and name } -var _ interfaces.PolyFunc = &MapLookupFunc{} // ensure it meets this expectation +var _ interfaces.BuildableFunc = &MapLookupFunc{} // ensure it meets this expectation // MapLookupFunc is a key map lookup function. If you provide a missing key, // then it will return the zero value for that type. @@ -79,237 +79,23 @@ func (obj *MapLookupFunc) ArgGen(index int) (string, error) { return seq[index], nil } -// Unify returns the list of invariants that this func produces. -func (obj *MapLookupFunc) Unify(expr interfaces.Expr) ([]interfaces.Invariant, error) { - var invariants []interfaces.Invariant - var invar interfaces.Invariant - - // func(map T1, key T2) T3 - // (map: T2 => T3) - - mapName, err := obj.ArgGen(0) - if err != nil { - return nil, err +// 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() } - - keyName, err := obj.ArgGen(1) - if err != nil { - return nil, err - } - - dummyMap := &interfaces.ExprAny{} // corresponds to the map type - dummyKey := &interfaces.ExprAny{} // corresponds to the key type - dummyOut := &interfaces.ExprAny{} // corresponds to the out string - - // relationship between T1, T2 and T3 - invar = &interfaces.EqualityWrapMapInvariant{ - Expr1: dummyMap, - Expr2Key: dummyKey, - Expr2Val: dummyOut, - } - invariants = append(invariants, invar) - - // full function - mapped := make(map[string]interfaces.Expr) - ordered := []string{mapName, keyName} - mapped[mapName] = dummyMap - mapped[keyName] = dummyKey - - invar = &interfaces.EqualityWrapFuncInvariant{ - Expr1: expr, // maps directly to us! - Expr2Map: mapped, - Expr2Ord: ordered, - Expr2Out: dummyOut, - } - 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) - } - - var invariants []interfaces.Invariant - var invar interfaces.Invariant - - // add the relationship to the returned value - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Expr, - Expr2: dummyOut, - } - invariants = append(invariants, invar) - - // add the relationships to the called args - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Args[0], - Expr2: dummyMap, - } - invariants = append(invariants, invar) - - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Args[1], - Expr2: dummyKey, - } - invariants = append(invariants, invar) - - // If we figure out all of these three types, we'll - // know the full type... - var t1 *types.Type // map type - var t2 *types.Type // map key type - var t3 *types.Type // map val type - - // validateArg0 checks: map T1 - validateArg0 := func(typ *types.Type) error { - if typ == nil { // unknown so far - return nil - } - - // we happen to have a map! - if k := typ.Kind; k != types.KindMap { - return fmt.Errorf("unable to build function with 0th arg of kind: %s", k) - } - - if typ.Key == nil || typ.Val == nil { - // programming error - return fmt.Errorf("map is missing type") - } - - if err := typ.Cmp(t1); t1 != nil && err != nil { - return errwrap.Wrapf(err, "input type was inconsistent") - } - if err := typ.Key.Cmp(t2); t2 != nil && err != nil { - return errwrap.Wrapf(err, "input key type was inconsistent") - } - if err := typ.Val.Cmp(t3); t3 != nil && err != nil { - return errwrap.Wrapf(err, "input val type was inconsistent") - } - - // learn! - t1 = typ - t2 = typ.Key - t3 = typ.Val - return nil - } - - // validateArg1 checks: map key T2 - validateArg1 := func(typ *types.Type) error { - if typ == nil { // unknown so far - return nil - } - - if err := typ.Cmp(t2); t2 != nil && err != nil { - return errwrap.Wrapf(err, "input key type was inconsistent") - } - if t1 != nil { - if err := typ.Cmp(t1.Key); err != nil { - return errwrap.Wrapf(err, "input key type was inconsistent") - } - } - if t3 != nil { - t := &types.Type{ // build t1 - Kind: types.KindMap, - Key: typ, // t2 - Val: t3, - } - - if err := t.Cmp(t1); t1 != nil && err != nil { - return errwrap.Wrapf(err, "input type was inconsistent") - } - t1 = t // learn! - } - - // learn! - t2 = typ - return nil - } - - if typ, err := cfavInvar.Args[0].Type(); err == nil { // is it known? - // this sets t1 and t2 and t3 on success if it learned - if err := validateArg0(typ); err != nil { - return nil, errwrap.Wrapf(err, "first map arg type is inconsistent") - } - } - if typ, exists := solved[cfavInvar.Args[0]]; exists { // alternate way to lookup type - // this sets t1 and t2 and t3 on success if it learned - if err := validateArg0(typ); err != nil { - return nil, errwrap.Wrapf(err, "first map arg type is inconsistent") - } - } - - if typ, err := cfavInvar.Args[1].Type(); err == nil { // is it known? - // this sets t2 (and sometimes t1) on success if it learned - if err := validateArg1(typ); err != nil { - return nil, errwrap.Wrapf(err, "second key arg type is inconsistent") - } - } - if typ, exists := solved[cfavInvar.Args[1]]; exists { // alternate way to lookup type - // this sets t2 (and sometimes t1) on success if it learned - if err := validateArg1(typ); err != nil { - return nil, errwrap.Wrapf(err, "second key arg type is inconsistent") - } - } - - // XXX: if the types aren't know statically? - - if t1 != nil { - invar := &interfaces.EqualsInvariant{ - Expr: dummyMap, - Type: t1, - } - invariants = append(invariants, invar) - } - if t2 != nil { - invar := &interfaces.EqualsInvariant{ - Expr: dummyKey, - Type: t2, - } - invariants = append(invariants, invar) - } - if t3 != nil { - invar := &interfaces.EqualsInvariant{ - Expr: dummyOut, - Type: t3, - } - invariants = append(invariants, invar) - } - - // XXX: if t{1..3} are missing, we could also return a - // new generator for later if we learn new information, - // but we'd have to be careful to not do it infinitely. - - // 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 + 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 @@ -324,7 +110,7 @@ func (obj *MapLookupFunc) Build(typ *types.Type) (*types.Type, error) { } if len(typ.Ord) != 2 { - return nil, fmt.Errorf("the maplookup function needs exactly three args") + 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") @@ -369,26 +155,14 @@ func (obj *MapLookupFunc) Validate() error { // Info returns some static info about itself. Build must be called before this // will return correct data. func (obj *MapLookupFunc) Info() *interfaces.Info { - var sig *types.Type - if obj.Type != nil { // don't panic if called speculatively - // TODO: can obj.Type.Key or obj.Type.Val be nil (a partial) ? - sig = obj.sig() // helper - } return &interfaces.Info{ Pure: true, Memo: false, - Sig: sig, // func kind + Sig: obj.sig(), // helper Err: obj.Validate(), } } -// helper -func (obj *MapLookupFunc) sig() *types.Type { - k := obj.Type.Key.String() - v := obj.Type.Val.String() - return types.NewType(fmt.Sprintf("func(%s %s, %s %s) %s", mapLookupArgNameMap, obj.Type.String(), mapLookupArgNameKey, k, v)) -} - // Init runs some startup code for this function. func (obj *MapLookupFunc) Init(init *interfaces.Init) error { obj.init = init diff --git a/lang/funcs/multi/multi.go b/lang/funcs/multi/multi.go new file mode 100644 index 00000000..0103cdea --- /dev/null +++ b/lang/funcs/multi/multi.go @@ -0,0 +1,187 @@ +// Mgmt +// Copyright (C) 2013-2024+ 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 . +// +// Additional permission under GNU GPL version 3 section 7 +// +// If you modify this program, or any covered work, by linking or combining it +// with embedded mcl code and modules (and that the embedded mcl code and +// modules which link with this program, contain a copy of their source code in +// the authoritative form) containing parts covered by the terms of any other +// license, the licensors of this program grant you additional permission to +// convey the resulting work. Furthermore, the licensors of this program grant +// the original author, James Shubin, additional permission to update this +// additional permission if he deems it necessary to achieve the goals of this +// additional permission. + +package multi + +import ( + "fmt" + "sort" + + "github.com/purpleidea/mgmt/lang/funcs" + "github.com/purpleidea/mgmt/lang/funcs/wrapped" + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/lang/types" + unificationUtil "github.com/purpleidea/mgmt/lang/unification/util" + "github.com/purpleidea/mgmt/util/errwrap" +) + +// RegisteredFuncs maps a function name to the corresponding function scaffold. +var RegisteredFuncs = make(map[string]*Scaffold) // must initialize + +// Scaffold holds the necessary data to build a (possibly polymorphic) function +// with this API. +type Scaffold struct { + // T is the type of the function. It can include unification variables. + // At a minimum, this must be a `func(?1) ?2` as a naked `?1` is not + // allowed. (TODO: Because of ArgGen.) + T *types.Type + + // M is a build function to run after type unification. It will get + // passed the solved type of this function. It should error if this is + // not an acceptable option. On success, it should return the + // implementing function to use. Of note, this API does not tell the + // implementation what the correct return type should be. If it can't be + // determined from the input types, then a different function API needs + // to be used. XXX: Should we extend this here? + M func(typ *types.Type) (interfaces.FuncSig, error) +} + +// Register registers a simple, static, pure, polymorphic function. It is easier +// to use than the raw function API. It allows you to build and check a function +// based on a type signature that contains unification variables. You may only +// specify a single type signature with the API, so 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, scaffold *Scaffold) { + if _, exists := RegisteredFuncs[name]; exists { + panic(fmt.Sprintf("a simple polyfunc named %s is already registered", name)) + } + + if scaffold == nil { + panic("no scaffold specified for simple polyfunc") + } + if scaffold.T == nil { + panic("no type specified for simple polyfunc") + } + if scaffold.T.Kind != types.KindFunc { + panic("type must be a func") + } + if scaffold.T.HasVariant() { + panic("func contains a variant type signature") + } + if scaffold.M == nil { + panic("no implementation specified for simple polyfunc") + } + + RegisteredFuncs[name] = scaffold // store a copy for ourselves + + // register a copy in the main function database + funcs.Register(name, func() interfaces.Func { + return &Func{ + WrappedFunc: &wrapped.Func{ + Name: name, + // NOTE: It might be more correct to Copy here, + // but we do the copy inside of ExprFunc.Copy() + // instead, so that the same type can be unified + // in more than one way. Doing it here wouldn't + // be harmful, but it's an extra copy we don't + // need to do AFAICT. + Type: scaffold.T, // .Copy(), + }, + Make: scaffold.M, + } + }) +} + +// ModuleRegister is exactly like Register, except that it registers within a +// named module. This is a helper function. +func ModuleRegister(module, name string, scaffold *Scaffold) { + Register(module+funcs.ModuleSep+name, scaffold) +} + +// WrappedFunc is a type alias so that we can embed `wrapped.Func` inside our +// struct, since the Func name collides with our Func field name. +type WrappedFunc = wrapped.Func + +var _ interfaces.BuildableFunc = &Func{} // ensure it meets this expectation + +// Func 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. This function API is unique in that it lets you provide your own +// `Make` builder function to create the function implementation. +type Func struct { + *WrappedFunc // *wrapped.Func as a type alias to pull in the base impl. + + // Make is a build function to run after type unification. It will get + // passed the solved type of this function. It should error if this is + // not an acceptable option. On success, it should return the + // implementing function to use. Of note, this API does not tell the + // implementation what the correct return type should be. If it can't be + // determined from the input types, then a different function API needs + // to be used. XXX: Should we extend this here? + Make func(typ *types.Type) (interfaces.FuncSig, error) +} + +// Build is run to turn the maybe polymorphic, undetermined function, into the +// specific statically typed version. It is usually run after unification +// completes, and must be run before Info() and any of the other Func interface +// methods are used. +func (obj *Func) Build(typ *types.Type) (*types.Type, error) { + // typ is the KindFunc signature we're trying to build... + + f, err := obj.Make(typ) + if err != nil { + return nil, errwrap.Wrapf(err, "can't build %s with %s", obj.Name, typ) + } + + fn := &types.FuncValue{ + T: typ, + V: f, // implementation + } + obj.Fn = fn + return obj.Fn.T, nil +} + +// TypeMatch accepts a map of possible type signatures to corresponding +// implementing functions that we want to check against after type unification. +// On success it returns the function who's corresponding signature matched. +// This helper function returns a function which is suitable for use in the +// scaffold make function field. +func TypeMatch(m map[string]interfaces.FuncSig) func(*types.Type) (interfaces.FuncSig, error) { + return func(typ *types.Type) (interfaces.FuncSig, error) { + // sort for determinism in debugging + keys := []string{} + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + for _, s := range keys { + t := types.NewType(s) + if t == nil { + // TODO: should we panic? + continue // skip + } + if unificationUtil.UnifyCmp(typ, t) == nil { + return m[s], nil + } + } + return nil, fmt.Errorf("did not match") + } +} diff --git a/lang/funcs/operator_func.go b/lang/funcs/operator_func.go deleted file mode 100644 index 76eaa61d..00000000 --- a/lang/funcs/operator_func.go +++ /dev/null @@ -1,1019 +0,0 @@ -// Mgmt -// Copyright (C) 2013-2024+ 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 . -// -// Additional permission under GNU GPL version 3 section 7 -// -// If you modify this program, or any covered work, by linking or combining it -// with embedded mcl code and modules (and that the embedded mcl code and -// modules which link with this program, contain a copy of their source code in -// the authoritative form) containing parts covered by the terms of any other -// license, the licensors of this program grant you additional permission to -// convey the resulting work. Furthermore, the licensors of this program grant -// the original author, James Shubin, additional permission to update this -// additional permission if he deems it necessary to achieve the goals of this -// additional permission. - -package funcs // this is here, in case we allow others to register operators... - -import ( - "context" - "fmt" - "math" - "sort" - - "github.com/purpleidea/mgmt/lang/interfaces" - "github.com/purpleidea/mgmt/lang/types" - "github.com/purpleidea/mgmt/util" - "github.com/purpleidea/mgmt/util/errwrap" -) - -const ( - // OperatorFuncName is the name this function is registered as. This - // starts with an underscore so that it cannot be used from the lexer. - OperatorFuncName = "_operator" - - // operatorArgName is the edge and arg name used for the function's - // operator. - operatorArgName = "op" // something short and arbitrary -) - -func init() { - // concatenation - RegisterOperator("+", &types.FuncValue{ - T: types.NewType("func(a str, b str) str"), - V: func(ctx context.Context, input []types.Value) (types.Value, error) { - return &types.StrValue{ - V: input[0].Str() + input[1].Str(), - }, nil - }, - }) - // addition - RegisterOperator("+", &types.FuncValue{ - T: types.NewType("func(a int, b int) int"), - V: func(ctx context.Context, input []types.Value) (types.Value, error) { - //if l := len(input); l != 2 { - // return nil, fmt.Errorf("expected two inputs, got: %d", l) - //} - // FIXME: check for overflow? - return &types.IntValue{ - V: input[0].Int() + input[1].Int(), - }, nil - }, - }) - // floating-point addition - RegisterOperator("+", &types.FuncValue{ - T: types.NewType("func(a float, b float) float"), - V: func(ctx context.Context, input []types.Value) (types.Value, error) { - return &types.FloatValue{ - V: input[0].Float() + input[1].Float(), - }, nil - }, - }) - - // subtraction - RegisterOperator("-", &types.FuncValue{ - T: types.NewType("func(a int, b int) int"), - V: func(ctx context.Context, input []types.Value) (types.Value, error) { - return &types.IntValue{ - V: input[0].Int() - input[1].Int(), - }, nil - }, - }) - // floating-point subtraction - RegisterOperator("-", &types.FuncValue{ - T: types.NewType("func(a float, b float) float"), - V: func(ctx context.Context, input []types.Value) (types.Value, error) { - return &types.FloatValue{ - V: input[0].Float() - input[1].Float(), - }, nil - }, - }) - - // multiplication - RegisterOperator("*", &types.FuncValue{ - T: types.NewType("func(a int, b int) int"), - V: func(ctx context.Context, input []types.Value) (types.Value, error) { - // FIXME: check for overflow? - return &types.IntValue{ - V: input[0].Int() * input[1].Int(), - }, nil - }, - }) - // floating-point multiplication - RegisterOperator("*", &types.FuncValue{ - T: types.NewType("func(a float, b float) float"), - V: func(ctx context.Context, input []types.Value) (types.Value, error) { - return &types.FloatValue{ - V: input[0].Float() * input[1].Float(), - }, nil - }, - }) - - // don't add: `func(int, float) float` or: `func(float, int) float` - // division - RegisterOperator("/", &types.FuncValue{ - T: types.NewType("func(a int, b int) float"), - V: func(ctx context.Context, input []types.Value) (types.Value, error) { - divisor := input[1].Int() - if divisor == 0 { - return nil, fmt.Errorf("can't divide by zero") - } - return &types.FloatValue{ - V: float64(input[0].Int()) / float64(divisor), - }, nil - }, - }) - // floating-point division - RegisterOperator("/", &types.FuncValue{ - T: types.NewType("func(a float, b float) float"), - V: func(ctx context.Context, input []types.Value) (types.Value, error) { - divisor := input[1].Float() - if divisor == 0.0 { - return nil, fmt.Errorf("can't divide by zero") - } - return &types.FloatValue{ - V: input[0].Float() / divisor, - }, nil - }, - }) - - // string equality - RegisterOperator("==", &types.FuncValue{ - T: types.NewType("func(a str, b str) bool"), - V: func(ctx context.Context, input []types.Value) (types.Value, error) { - return &types.BoolValue{ - V: input[0].Str() == input[1].Str(), - }, nil - }, - }) - // bool equality - RegisterOperator("==", &types.FuncValue{ - T: types.NewType("func(a bool, b bool) bool"), - V: func(ctx context.Context, input []types.Value) (types.Value, error) { - return &types.BoolValue{ - V: input[0].Bool() == input[1].Bool(), - }, nil - }, - }) - // int equality - RegisterOperator("==", &types.FuncValue{ - T: types.NewType("func(a int, b int) bool"), - V: func(ctx context.Context, input []types.Value) (types.Value, error) { - return &types.BoolValue{ - V: input[0].Int() == input[1].Int(), - }, nil - }, - }) - // floating-point equality - RegisterOperator("==", &types.FuncValue{ - T: types.NewType("func(a float, b float) bool"), - V: func(ctx context.Context, input []types.Value) (types.Value, error) { - // TODO: should we do an epsilon check? - return &types.BoolValue{ - V: input[0].Float() == input[1].Float(), - }, nil - }, - }) - - // string in-equality - RegisterOperator("!=", &types.FuncValue{ - T: types.NewType("func(a str, b str) bool"), - V: func(ctx context.Context, input []types.Value) (types.Value, error) { - return &types.BoolValue{ - V: input[0].Str() != input[1].Str(), - }, nil - }, - }) - // bool in-equality - RegisterOperator("!=", &types.FuncValue{ - T: types.NewType("func(a bool, b bool) bool"), - V: func(ctx context.Context, input []types.Value) (types.Value, error) { - return &types.BoolValue{ - V: input[0].Bool() != input[1].Bool(), - }, nil - }, - }) - // int in-equality - RegisterOperator("!=", &types.FuncValue{ - T: types.NewType("func(a int, b int) bool"), - V: func(ctx context.Context, input []types.Value) (types.Value, error) { - return &types.BoolValue{ - V: input[0].Int() != input[1].Int(), - }, nil - }, - }) - // floating-point in-equality - RegisterOperator("!=", &types.FuncValue{ - T: types.NewType("func(a float, b float) bool"), - V: func(ctx context.Context, input []types.Value) (types.Value, error) { - // TODO: should we do an epsilon check? - return &types.BoolValue{ - V: input[0].Float() != input[1].Float(), - }, nil - }, - }) - - // less-than - RegisterOperator("<", &types.FuncValue{ - T: types.NewType("func(a int, b int) bool"), - V: func(ctx context.Context, input []types.Value) (types.Value, error) { - return &types.BoolValue{ - V: input[0].Int() < input[1].Int(), - }, nil - }, - }) - // floating-point less-than - RegisterOperator("<", &types.FuncValue{ - T: types.NewType("func(a float, b float) bool"), - V: func(ctx context.Context, input []types.Value) (types.Value, error) { - // TODO: should we do an epsilon check? - return &types.BoolValue{ - V: input[0].Float() < input[1].Float(), - }, nil - }, - }) - // greater-than - RegisterOperator(">", &types.FuncValue{ - T: types.NewType("func(a int, b int) bool"), - V: func(ctx context.Context, input []types.Value) (types.Value, error) { - return &types.BoolValue{ - V: input[0].Int() > input[1].Int(), - }, nil - }, - }) - // floating-point greater-than - RegisterOperator(">", &types.FuncValue{ - T: types.NewType("func(a float, b float) bool"), - V: func(ctx context.Context, input []types.Value) (types.Value, error) { - // TODO: should we do an epsilon check? - return &types.BoolValue{ - V: input[0].Float() > input[1].Float(), - }, nil - }, - }) - // less-than-equal - RegisterOperator("<=", &types.FuncValue{ - T: types.NewType("func(a int, b int) bool"), - V: func(ctx context.Context, input []types.Value) (types.Value, error) { - return &types.BoolValue{ - V: input[0].Int() <= input[1].Int(), - }, nil - }, - }) - // floating-point less-than-equal - RegisterOperator("<=", &types.FuncValue{ - T: types.NewType("func(a float, b float) bool"), - V: func(ctx context.Context, input []types.Value) (types.Value, error) { - // TODO: should we do an epsilon check? - return &types.BoolValue{ - V: input[0].Float() <= input[1].Float(), - }, nil - }, - }) - // greater-than-equal - RegisterOperator(">=", &types.FuncValue{ - T: types.NewType("func(a int, b int) bool"), - V: func(ctx context.Context, input []types.Value) (types.Value, error) { - return &types.BoolValue{ - V: input[0].Int() >= input[1].Int(), - }, nil - }, - }) - // floating-point greater-than-equal - RegisterOperator(">=", &types.FuncValue{ - T: types.NewType("func(a float, b float) bool"), - V: func(ctx context.Context, input []types.Value) (types.Value, error) { - // TODO: should we do an epsilon check? - return &types.BoolValue{ - V: input[0].Float() >= input[1].Float(), - }, nil - }, - }) - - // logical and - // TODO: is there a way for the engine to have - // short-circuit operators, and does it matter? - RegisterOperator("and", &types.FuncValue{ - T: types.NewType("func(a bool, b bool) bool"), - V: func(ctx context.Context, input []types.Value) (types.Value, error) { - return &types.BoolValue{ - V: input[0].Bool() && input[1].Bool(), - }, nil - }, - }) - // logical or - RegisterOperator("or", &types.FuncValue{ - T: types.NewType("func(a bool, b bool) bool"), - V: func(ctx context.Context, input []types.Value) (types.Value, error) { - return &types.BoolValue{ - V: input[0].Bool() || input[1].Bool(), - }, nil - }, - }) - - // logical not (unary operator) - RegisterOperator("not", &types.FuncValue{ - T: types.NewType("func(a bool) bool"), - V: func(ctx context.Context, input []types.Value) (types.Value, error) { - return &types.BoolValue{ - V: !input[0].Bool(), - }, nil - }, - }) - - // pi operator (this is an easter egg to demo a zero arg operator) - RegisterOperator("π", &types.FuncValue{ - T: types.NewType("func() float"), - V: func(ctx context.Context, input []types.Value) (types.Value, error) { - return &types.FloatValue{ - V: math.Pi, - }, nil - }, - }) - - Register(OperatorFuncName, func() interfaces.Func { return &OperatorFunc{} }) // must register the func and name -} - -var _ interfaces.PolyFunc = &OperatorFunc{} // ensure it meets this expectation - -// OperatorFuncs maps an operator to a list of callable function values. -var OperatorFuncs = make(map[string][]*types.FuncValue) // must initialize - -// RegisterOperator registers the given string operator and function value -// implementation with the mini-database for this generalized, static, -// polymorphic operator implementation. -func RegisterOperator(operator string, fn *types.FuncValue) { - if _, exists := OperatorFuncs[operator]; !exists { - OperatorFuncs[operator] = []*types.FuncValue{} // init - } - - for _, f := range OperatorFuncs[operator] { - if err := f.T.Cmp(fn.T); err == nil { - panic(fmt.Sprintf("operator %s already has an implementation for %+v", operator, f.T)) - } - } - - for i, x := range fn.T.Ord { - if x == operatorArgName { - panic(fmt.Sprintf("can't use `%s` as an argName for operator `%s` with type `%+v`", x, operator, fn.T)) - } - // yes this limits the arg max to 24 (`x`) including operator - // if the operator is `x`... - if s := util.NumToAlpha(i); x != s { - panic(fmt.Sprintf("arg for operator `%s` (index `%d`) should be named `%s`, not `%s`", operator, i, s, x)) - } - } - - OperatorFuncs[operator] = append(OperatorFuncs[operator], fn) -} - -// LookupOperator returns a list of type strings for each operator. An empty -// operator string means return everything. If you specify a size that is less -// than zero, we don't filter by arg length, otherwise we only return signatures -// which have an arg length equal to size. -func LookupOperator(operator string, size int) ([]*types.Type, error) { - fns, exists := OperatorFuncs[operator] - if !exists && operator != "" { - return nil, fmt.Errorf("operator not found") - } - results := []*types.Type{} - - if operator == "" { - var keys []string - for k := range OperatorFuncs { - keys = append(keys, k) - } - sort.Strings(keys) - for _, a := range keys { - fns = append(fns, OperatorFuncs[a]...) - } - } - - for _, fn := range fns { - typ := addOperatorArg(fn.T) // add in the `operatorArgName` arg - - if size >= 0 && len(typ.Ord) != size { - continue - } - results = append(results, typ) - } - - return results, nil -} - -// LookupOperatorShort is similar to LookupOperator except that it returns the -// "short" (standalone) types of the direct functions that are attached to each -// operator. IOW, if you specify "+" and 2, you'll get the sigs for "a" + "b" -// and 1 + 2, without the third "op" as the first argument. -func LookupOperatorShort(operator string, size int) ([]*types.Type, error) { - fns, exists := OperatorFuncs[operator] - if !exists && operator != "" { - return nil, fmt.Errorf("operator not found") - } - results := []*types.Type{} - - for _, fn := range fns { - typ := fn.T - if len(typ.Ord) != size { - continue - } - results = append(results, typ) - } - - return results, nil -} - -// OperatorFunc is an operator function that performs an operation on N values. -type OperatorFunc struct { - Type *types.Type // Kind == Function, including operator arg - - 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 *OperatorFunc) String() string { - // TODO: return the exact operator if we can guarantee it doesn't change - return OperatorFuncName -} - -// argNames returns the maximum list of possible argNames. This can be truncated -// if needed. The first arg name is the operator. -func (obj *OperatorFunc) argNames() ([]string, error) { - // we could just do this statically, but i did it dynamically so that I - // wouldn't ever have to remember to update this list... - max := 0 - for _, fns := range OperatorFuncs { - for _, fn := range fns { - l := len(fn.T.Ord) - if l > max { - max = l - } - } - } - //if length >= 0 && length < max { - // max = length - //} - - args := []string{operatorArgName} - for i := 0; i < max; i++ { - s := util.NumToAlpha(i) - if s == operatorArgName { - return nil, fmt.Errorf("can't use `%s` as arg name", operatorArgName) - } - args = append(args, s) - } - - return args, nil -} - -// findFunc tries to find the first available registered operator function that -// matches the Operator/Type pattern requested. If none is found it returns nil. -func (obj *OperatorFunc) findFunc(operator string) *types.FuncValue { - fns, exists := OperatorFuncs[operator] - if !exists { - return nil - } - typ := removeOperatorArg(obj.Type) // remove operator so we can match... - for _, fn := range fns { - if err := fn.Type().Cmp(typ); err == nil { // found one! - return fn - } - } - return nil -} - -// ArgGen returns the Nth arg name for this function. -func (obj *OperatorFunc) ArgGen(index int) (string, error) { - seq, err := obj.argNames() - if err != nil { - return "", err - } - 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 *OperatorFunc) Unify(expr interfaces.Expr) ([]interfaces.Invariant, error) { - var invariants []interfaces.Invariant - var invar interfaces.Invariant - - // func(operator string, args... variant) string - - operatorName, err := obj.ArgGen(0) - if err != nil { - return nil, err - } - - dummyOperator := &interfaces.ExprAny{} // corresponds to the format type - dummyOut := &interfaces.ExprAny{} // corresponds to the out type - - // operator arg type of string - invar = &interfaces.EqualsInvariant{ - Expr: dummyOperator, - Type: types.TypeStr, - } - invariants = append(invariants, invar) - - // return type is currently unknown - invar = &interfaces.AnyInvariant{ - Expr: dummyOut, // make sure to include it so we know it solves - } - 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 len(cfavInvar.Args) == 0 { - return nil, fmt.Errorf("unable to build function with no args") - } - // our operator is the 0th arg, but that's the minimum! - - var invariants []interfaces.Invariant - var invar interfaces.Invariant - - // add the relationship to the returned value - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Expr, - Expr2: dummyOut, - } - invariants = append(invariants, invar) - - // add the relationships to the called args - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Args[0], - Expr2: dummyOperator, - } - invariants = append(invariants, invar) - - // first arg must be a string - invar = &interfaces.EqualsInvariant{ - Expr: cfavInvar.Args[0], - Type: types.TypeStr, - } - invariants = append(invariants, invar) - - value, err := cfavInvar.Args[0].Value() // is it known? - if err != nil { - return nil, fmt.Errorf("operator string is not known statically") - } - - if k := value.Type().Kind; k != types.KindStr { - return nil, fmt.Errorf("unable to build function with 0th arg of kind: %s", k) - } - op := value.Str() // must not panic - if op == "" { - return nil, fmt.Errorf("unable to build function with empty op") - } - size := len(cfavInvar.Args) - 1 // -1 to remove the op - - // since built-in functions have their signatures - // explicitly defined, we can add easy invariants - // between in/out args and their expected types. - results, err := LookupOperatorShort(op, size) - if err != nil { - return nil, errwrap.Wrapf(err, "error finding signatures for operator `%s`", op) - } - - if len(results) == 0 { - return nil, fmt.Errorf("no matching signatures for operator `%s` could be found", op) - } - - // helper function to build our complex func invariants - buildInvar := func(typ *types.Type) ([]interfaces.Invariant, error) { - var invariants []interfaces.Invariant - var invar interfaces.Invariant - // full function - mapped := make(map[string]interfaces.Expr) - ordered := []string{operatorName} - mapped[operatorName] = dummyOperator - // assume this is a types.KindFunc - for i, x := range typ.Ord { - t := typ.Map[x] - if t == nil { - // programming error - return nil, fmt.Errorf("unexpected func nil arg (%d) type", i) - } - - argName, err := obj.ArgGen(i + 1) // skip 0th - if err != nil { - return nil, err - } - if argName == operatorArgName { - return nil, fmt.Errorf("could not build function with %d args", i+1) // +1 for op arg - } - - dummyArg := &interfaces.ExprAny{} - invar = &interfaces.EqualsInvariant{ - Expr: dummyArg, - Type: t, - } - invariants = append(invariants, invar) - - invar = &interfaces.EqualityInvariant{ - Expr1: dummyArg, - Expr2: cfavInvar.Args[i+1], - } - invariants = append(invariants, invar) - - mapped[argName] = dummyArg - ordered = append(ordered, argName) - } - - invar = &interfaces.EqualityWrapFuncInvariant{ - Expr1: expr, // maps directly to us! - Expr2Map: mapped, - Expr2Ord: ordered, - Expr2Out: dummyOut, - } - invariants = append(invariants, invar) - - if typ.Out == nil { - // programming error - return nil, fmt.Errorf("unexpected func nil return type") - } - - // remember to add the relationship to the - // return type of the functions as well... - invar = &interfaces.EqualsInvariant{ - Expr: dummyOut, - Type: typ.Out, - } - invariants = append(invariants, invar) - - return invariants, nil - } - - // argCmp trims down the list of possible types... - // this makes our exclusive invariants smaller, and - // easier to solve without combinatorial slow recursion - argCmp := func(typ *types.Type) bool { - if len(cfavInvar.Args)-1 != len(typ.Ord) { - return false // arg length differs - } - for i, x := range cfavInvar.Args[1:] { - if t, err := x.Type(); err == nil { - if t.Cmp(typ.Map[typ.Ord[i]]) != nil { - return false // impossible! - } - } - - // is the type already known as solved? - if t, exists := solved[x]; exists { // alternate way to lookup type - if t.Cmp(typ.Map[typ.Ord[i]]) != nil { - return false // impossible! - } - } - } - return true // possible - } - - // 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) - - ors := []interfaces.Invariant{} // solve only one from this list - for _, typ := range results { // operator func types - if typ.Kind != types.KindFunc { - // programming error - return nil, fmt.Errorf("type must be a kind of func") - } - - if !argCmp(typ) { // filter out impossible types - continue // not a possible match - } - - invars, err := buildInvar(typ) - if err != nil { - return nil, err - } - - // all of these need to be true together - and := &interfaces.ConjunctionInvariant{ - Invariants: invars, - } - ors = append(ors, and) // one solution added! - } - if len(ors) == 0 { - return nil, fmt.Errorf("no matching signatures for operator `%s` could be found", op) - } - - invar = &interfaces.ExclusiveInvariant{ - Invariants: ors, // one and only one of these should be true - } - if len(ors) == 1 { - invar = ors[0] // there should only be one - } - 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 *OperatorFunc) Polymorphisms(partialType *types.Type, partialValues []types.Value) ([]*types.Type, error) { - var op string - var size = -1 - - // optimization: if operator happens to already be known statically, - // then we can return a much smaller subset of possible signatures... - if partialType != nil && partialType.Ord != nil { - ord := partialType.Ord - if len(ord) == 0 { - return nil, fmt.Errorf("must have at least one arg in operator func") - } - // optimization: since we know arg length, we can limit the - // signatures that we return... - size = len(ord) // we know size! - if partialType.Map != nil { - if t, exists := partialType.Map[ord[0]]; exists && t != nil { - if t.Cmp(types.TypeStr) != nil { - return nil, fmt.Errorf("first arg for operator func must be an str") - } - if len(partialValues) > 0 && partialValues[0] != nil { - op = partialValues[0].Str() // known str - } - } - } - } - - // 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 { - return nil, errwrap.Wrapf(err, "error finding signatures for operator `%s`", op) - } - - // TODO: we can add additional results filtering here if we'd like... - - if len(results) == 0 { - return nil, fmt.Errorf("no matching signatures for operator `%s` could be found", op) - } - - return results, nil -} - -// 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 *OperatorFunc) Build(typ *types.Type) (*types.Type, error) { - // typ is the KindFunc signature we're trying to build... - if len(typ.Ord) < 1 { - return nil, fmt.Errorf("the operator function needs at least 1 arg") - } - if typ.Out == nil { - return nil, fmt.Errorf("return type of function must be specified") - } - if typ.Kind != types.KindFunc { - return nil, fmt.Errorf("unexpected build kind of: %v", typ.Kind) - } - - // Change arg names to be what we expect... - if _, exists := typ.Map[typ.Ord[0]]; !exists { - return nil, fmt.Errorf("invalid build type") - } - - //newTyp := typ.Copy() - newTyp := &types.Type{ - Kind: typ.Kind, // copy - Map: make(map[string]*types.Type), // new - Ord: []string{}, // new - Out: typ.Out, // copy - } - for i, x := range typ.Ord { // remap arg names - //argName := util.NumToAlpha(i - 1) - //if i == 0 { - // argName = operatorArgName - //} - argName, err := obj.ArgGen(i) - if err != nil { - return nil, err - } - - newTyp.Map[argName] = typ.Map[x] - newTyp.Ord = append(newTyp.Ord, argName) - } - - obj.Type = newTyp // func type - return obj.Type, nil -} - -// Validate tells us if the input struct takes a valid form. -func (obj *OperatorFunc) Validate() error { - if obj.Type == nil { // build must be run first - return fmt.Errorf("type is still unspecified") - } - if obj.Type.Kind != types.KindFunc { - return fmt.Errorf("type must be a kind of func") - } - return nil -} - -// Info returns some static info about itself. Build must be called before this -// will return correct data. -func (obj *OperatorFunc) Info() *interfaces.Info { - return &interfaces.Info{ - Pure: true, - Memo: false, - Sig: obj.Type, // func kind, which includes operator arg as input - Err: obj.Validate(), - } -} - -// Init runs some startup code for this function. -func (obj *OperatorFunc) Init(init *interfaces.Init) error { - obj.init = init - return nil -} - -// Stream returns the changing values that this func has over time. -func (obj *OperatorFunc) Stream(ctx context.Context) error { - var op, lastOp string - var fn *types.FuncValue - 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 - - // programming error safety check... - programmingError := false - keys := []string{} - for k := range input.Struct() { - keys = append(keys, k) - if !util.StrInList(k, obj.Type.Ord) { - programmingError = true - } - } - if programmingError { - return fmt.Errorf("bad args, got: %v, want: %v", keys, obj.Type.Ord) - } - - // build up arg list - args := []types.Value{} - for _, name := range obj.Type.Ord { - v, exists := input.Struct()[name] - if !exists { - // programming error - return fmt.Errorf("function engine was early, missing arg: %s", name) - } - if name == operatorArgName { - op = v.Str() - continue // skip over the operator arg - } - args = append(args, v) - } - - if op == "" { - // programming error - return fmt.Errorf("operator cannot be empty, args: %v", keys) - } - // operator selection is dynamic now, although mostly it - // should not change... to do so is probably uncommon... - if fn == nil || op != lastOp { - fn = obj.findFunc(op) - } - if fn == nil { - return fmt.Errorf("func not found for operator `%s` with sig: `%+v`", op, obj.Type) - } - lastOp = op - - var result types.Value - result, err := fn.Call(ctx, args) // run the function - if err != nil { - return errwrap.Wrapf(err, "problem running function") - } - if result == nil { - return fmt.Errorf("computed function output was nil") - } - - // 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 - } - } -} - -// removeOperatorArg returns a copy of the input KindFunc type, without the -// operator arg which specifies which operator we're using. It *is* idempotent. -func removeOperatorArg(typ *types.Type) *types.Type { - if typ == nil { - return nil - } - if _, exists := typ.Map[operatorArgName]; !exists { - return typ // pass through - } - - m := make(map[string]*types.Type) - ord := []string{} - for _, s := range typ.Ord { - if s == operatorArgName { - continue // remove the operator - } - m[s] = typ.Map[s] - ord = append(ord, s) - } - return &types.Type{ - Kind: types.KindFunc, - Map: m, - Ord: ord, - Out: typ.Out, - } -} - -// addOperatorArg returns a copy of the input KindFunc type, with the operator -// arg which specifies which operator we're using added. This is idempotent. -func addOperatorArg(typ *types.Type) *types.Type { - if typ == nil { - return nil - } - if _, exists := typ.Map[operatorArgName]; exists { - return typ // pass through - } - - m := make(map[string]*types.Type) - m[operatorArgName] = types.TypeStr // add the operator - ord := []string{operatorArgName} // add the operator - for _, s := range typ.Ord { - m[s] = typ.Map[s] - ord = append(ord, s) - } - return &types.Type{ - Kind: types.KindFunc, - Map: m, - Ord: ord, - Out: typ.Out, - } -} diff --git a/lang/funcs/operators/operators.go b/lang/funcs/operators/operators.go new file mode 100644 index 00000000..7799fc67 --- /dev/null +++ b/lang/funcs/operators/operators.go @@ -0,0 +1,799 @@ +// Mgmt +// Copyright (C) 2013-2024+ 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 . +// +// Additional permission under GNU GPL version 3 section 7 +// +// If you modify this program, or any covered work, by linking or combining it +// with embedded mcl code and modules (and that the embedded mcl code and +// modules which link with this program, contain a copy of their source code in +// the authoritative form) containing parts covered by the terms of any other +// license, the licensors of this program grant you additional permission to +// convey the resulting work. Furthermore, the licensors of this program grant +// the original author, James Shubin, additional permission to update this +// additional permission if he deems it necessary to achieve the goals of this +// additional permission. + +// Package operators provides a helper library to load all of the built-in +// operators, which are actually just functions. +package operators // this is here, in case we allow others to register operators + +import ( + "context" + "fmt" + "math" + + "github.com/purpleidea/mgmt/lang/funcs" + "github.com/purpleidea/mgmt/lang/funcs/simple" + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/lang/types" + "github.com/purpleidea/mgmt/util" + "github.com/purpleidea/mgmt/util/errwrap" +) + +const ( + // OperatorFuncName is the name this function is registered as. This + // starts with an underscore so that it cannot be used from the lexer. + OperatorFuncName = "_operator" + + // operatorArgName is the edge and arg name used for the function's + // operator. + operatorArgName = "op" // something short and arbitrary +) + +func init() { + RegisterOperator("+", &simple.Scaffold{ + T: types.NewType("func(?1, ?1) ?1"), + C: simple.TypeMatch([]string{ + "func(str, str) str", // concatenation + "func(int, int) int", // addition + "func(float, float) float", // floating-point addition + }), + F: func(ctx context.Context, input []types.Value) (types.Value, error) { + switch k := input[0].Type().Kind; k { + case types.KindStr: + return &types.StrValue{ + V: input[0].Str() + input[1].Str(), + }, nil + + case types.KindInt: + // FIXME: check for overflow? + return &types.IntValue{ + V: input[0].Int() + input[1].Int(), + }, nil + + case types.KindFloat: + return &types.FloatValue{ + V: input[0].Float() + input[1].Float(), + }, nil + + default: + return nil, fmt.Errorf("unsupported kind: %+v", k) + } + }, + }) + + RegisterOperator("-", &simple.Scaffold{ + T: types.NewType("func(?1, ?1) ?1"), + C: simple.TypeMatch([]string{ + "func(int, int) int", // subtraction + "func(float, float) float", // floating-point subtraction + }), + F: func(ctx context.Context, input []types.Value) (types.Value, error) { + switch k := input[0].Type().Kind; k { + case types.KindInt: + return &types.IntValue{ + V: input[0].Int() - input[1].Int(), + }, nil + + case types.KindFloat: + return &types.FloatValue{ + V: input[0].Float() - input[1].Float(), + }, nil + + default: + return nil, fmt.Errorf("unsupported kind: %+v", k) + } + }, + }) + + RegisterOperator("*", &simple.Scaffold{ + T: types.NewType("func(?1, ?1) ?1"), + C: simple.TypeMatch([]string{ + "func(int, int) int", // multiplication + "func(float, float) float", // floating-point multiplication + }), + F: func(ctx context.Context, input []types.Value) (types.Value, error) { + switch k := input[0].Type().Kind; k { + case types.KindInt: + // FIXME: check for overflow? + return &types.IntValue{ + V: input[0].Int() * input[1].Int(), + }, nil + + case types.KindFloat: + return &types.FloatValue{ + V: input[0].Float() * input[1].Float(), + }, nil + + default: + return nil, fmt.Errorf("unsupported kind: %+v", k) + } + }, + }) + + // don't add: `func(int, float) float` or: `func(float, int) float` + RegisterOperator("/", &simple.Scaffold{ + T: types.NewType("func(?1, ?1) float"), + C: simple.TypeMatch([]string{ + "func(int, int) float", // division + "func(float, float) float", // floating-point division + }), + F: func(ctx context.Context, input []types.Value) (types.Value, error) { + switch k := input[0].Type().Kind; k { + case types.KindInt: + divisor := input[1].Int() + if divisor == 0 { + return nil, fmt.Errorf("can't divide by zero") + } + return &types.FloatValue{ + V: float64(input[0].Int()) / float64(divisor), + }, nil + + case types.KindFloat: + divisor := input[1].Float() + if divisor == 0.0 { + return nil, fmt.Errorf("can't divide by zero") + } + return &types.FloatValue{ + V: input[0].Float() / divisor, + }, nil + + default: + return nil, fmt.Errorf("unsupported kind: %+v", k) + } + }, + }) + + RegisterOperator("==", &simple.Scaffold{ + T: types.NewType("func(?1, ?1) bool"), + C: func(typ *types.Type) error { + //if typ == nil { // happens within iter + // return fmt.Errorf("nil type") + //} + iterFn := func(typ *types.Type) error { + if typ == nil { + return fmt.Errorf("nil type") + } + if !types.IsComparableKind(typ.Kind) { + return fmt.Errorf("not comparable") + } + return nil + } + if err := types.Iter(typ, iterFn); err != nil { + return err + } + + // At this point, we know we can cmp any contained type. + match := simple.TypeMatch([]string{ + //"func(bool, bool) bool", // bool equality + //"func(str, str) bool", // string equality + //"func(int, int) bool", // int equality + //"func(float, float) bool", // floating-point equality + //"func([]?1, []?1) bool", // list equality + //"func(map{?1:?2}, map{?1:?2}) bool", // map equality + // struct in-equality (just skip the entire match function) + "func(?1, ?1) bool", + }) + return match(typ) + }, + F: func(ctx context.Context, input []types.Value) (types.Value, error) { + k := input[0].Type().Kind + // Don't try and compare functions, this will panic! + if !types.IsComparableKind(k) { + return nil, fmt.Errorf("unsupported kind: %+v", k) + } + + return &types.BoolValue{ + V: input[0].Cmp(input[1]) == nil, // equality + }, nil + }, + }) + + RegisterOperator("!=", &simple.Scaffold{ + T: types.NewType("func(?1, ?1) bool"), + C: func(typ *types.Type) error { + //if typ == nil { // happens within iter + // return fmt.Errorf("nil type") + //} + iterFn := func(typ *types.Type) error { + if typ == nil { + return fmt.Errorf("nil type") + } + if !types.IsComparableKind(typ.Kind) { + return fmt.Errorf("not comparable") + } + return nil + } + if err := types.Iter(typ, iterFn); err != nil { + return err + } + + // At this point, we know we can cmp any contained type. + match := simple.TypeMatch([]string{ + //"func(bool, bool) bool", // bool in-equality + //"func(str, str) bool", // string in-equality + //"func(int, int) bool", // int in-equality + //"func(float, float) bool", // floating-point in-equality + //"func([]?1, []?1) bool", // list in-equality + //"func(map{?1:?2}, map{?1:?2}) bool", // map in-equality + // struct in-equality (just skip the entire match function) + "func(?1, ?1) bool", + }) + return match(typ) + }, + F: func(ctx context.Context, input []types.Value) (types.Value, error) { + k := input[0].Type().Kind + // Don't try and compare functions, this will panic! + if !types.IsComparableKind(k) { + return nil, fmt.Errorf("unsupported kind: %+v", k) + } + + return &types.BoolValue{ + V: input[0].Cmp(input[1]) != nil, // in-equality + }, nil + }, + }) + + RegisterOperator("<", &simple.Scaffold{ + T: types.NewType("func(?1, ?1) bool"), + C: simple.TypeMatch([]string{ + "func(int, int) bool", // less-than + "func(float, float) bool", // floating-point less-than + }), + F: func(ctx context.Context, input []types.Value) (types.Value, error) { + switch k := input[0].Type().Kind; k { + case types.KindInt: + return &types.BoolValue{ + V: input[0].Int() < input[1].Int(), + }, nil + + case types.KindFloat: + // TODO: should we do an epsilon check? + return &types.BoolValue{ + V: input[0].Float() < input[1].Float(), + }, nil + + default: + return nil, fmt.Errorf("unsupported kind: %+v", k) + } + }, + }) + + RegisterOperator(">", &simple.Scaffold{ + T: types.NewType("func(?1, ?1) bool"), + C: simple.TypeMatch([]string{ + "func(int, int) bool", // greater-than + "func(float, float) bool", // floating-point greater-than + }), + F: func(ctx context.Context, input []types.Value) (types.Value, error) { + switch k := input[0].Type().Kind; k { + case types.KindInt: + return &types.BoolValue{ + V: input[0].Int() > input[1].Int(), + }, nil + + case types.KindFloat: + // TODO: should we do an epsilon check? + return &types.BoolValue{ + V: input[0].Float() > input[1].Float(), + }, nil + + default: + return nil, fmt.Errorf("unsupported kind: %+v", k) + } + }, + }) + + RegisterOperator("<=", &simple.Scaffold{ + T: types.NewType("func(?1, ?1) bool"), + C: simple.TypeMatch([]string{ + "func(int, int) bool", // less-than-equal + "func(float, float) bool", // floating-point less-than-equal + }), + F: func(ctx context.Context, input []types.Value) (types.Value, error) { + switch k := input[0].Type().Kind; k { + case types.KindInt: + return &types.BoolValue{ + V: input[0].Int() <= input[1].Int(), + }, nil + + case types.KindFloat: + // TODO: should we do an epsilon check? + return &types.BoolValue{ + V: input[0].Float() <= input[1].Float(), + }, nil + + default: + return nil, fmt.Errorf("unsupported kind: %+v", k) + } + }, + }) + + RegisterOperator(">=", &simple.Scaffold{ + T: types.NewType("func(?1, ?1) bool"), + C: simple.TypeMatch([]string{ + "func(int, int) bool", // greater-than-equal + "func(float, float) bool", // floating-point greater-than-equal + }), + F: func(ctx context.Context, input []types.Value) (types.Value, error) { + switch k := input[0].Type().Kind; k { + case types.KindInt: + return &types.BoolValue{ + V: input[0].Int() >= input[1].Int(), + }, nil + + case types.KindFloat: + // TODO: should we do an epsilon check? + return &types.BoolValue{ + V: input[0].Float() >= input[1].Float(), + }, nil + + default: + return nil, fmt.Errorf("unsupported kind: %+v", k) + } + }, + }) + + // logical and + // TODO: is there a way for the engine to have + // short-circuit operators, and does it matter? + RegisterOperator("and", &simple.Scaffold{ + T: types.NewType("func(bool, bool) bool"), + F: func(ctx context.Context, input []types.Value) (types.Value, error) { + return &types.BoolValue{ + V: input[0].Bool() && input[1].Bool(), + }, nil + }, + }) + + // logical or + RegisterOperator("or", &simple.Scaffold{ + T: types.NewType("func(bool, bool) bool"), + F: func(ctx context.Context, input []types.Value) (types.Value, error) { + return &types.BoolValue{ + V: input[0].Bool() || input[1].Bool(), + }, nil + }, + }) + + // logical not (unary operator) + RegisterOperator("not", &simple.Scaffold{ + T: types.NewType("func(bool) bool"), + F: func(ctx context.Context, input []types.Value) (types.Value, error) { + return &types.BoolValue{ + V: !input[0].Bool(), + }, nil + }, + }) + + // pi operator (this is an easter egg to demo a zero arg operator) + RegisterOperator("π", &simple.Scaffold{ + T: types.NewType("func() float"), + F: func(ctx context.Context, input []types.Value) (types.Value, error) { + return &types.FloatValue{ + V: math.Pi, + }, nil + }, + }) + + // register a copy in the main function database + // XXX: use simple.Register instead? + funcs.Register(OperatorFuncName, func() interfaces.Func { return &OperatorFunc{} }) +} + +var _ interfaces.InferableFunc = &OperatorFunc{} // ensure it meets this expectation + +// OperatorFuncs maps an operator to a list of callable function values. +var OperatorFuncs = make(map[string]*simple.Scaffold) // must initialize + +// RegisterOperator registers the given string operator and function value +// implementation with the mini-database for this generalized, static, +// polymorphic operator implementation. +func RegisterOperator(operator string, scaffold *simple.Scaffold) { + if _, exists := OperatorFuncs[operator]; exists { + panic(fmt.Sprintf("operator %s already has an implementation", operator)) + } + + if scaffold == nil { + panic(fmt.Sprintf("no scaffold specified for operator %s", operator)) + } + if scaffold.T == nil { + panic(fmt.Sprintf("no type specified for operator %s", operator)) + } + if scaffold.T.Kind != types.KindFunc { + panic(fmt.Sprintf("operator %s type must be a func", operator)) + } + if scaffold.T.HasVariant() { + panic(fmt.Sprintf("operator %s contains a variant type signature", operator)) + } + // It's okay if scaffold.C is nil. + if scaffold.F == nil { + panic(fmt.Sprintf("no implementation specified for operator %s", operator)) + } + + for _, x := range scaffold.T.Ord { + if x == operatorArgName { + panic(fmt.Sprintf("can't use `%s` as an argName for operator `%s` with type `%+v`", x, operator, scaffold.T)) + } + // yes this limits the arg max to 24 (`x`) including operator + // if the operator is `x`... + //if s := util.NumToAlpha(i); x != s { + // panic(fmt.Sprintf("arg for operator `%s` (index `%d`) should be named `%s`, not `%s`", operator, i, s, x)) + //} + } + + OperatorFuncs[operator] = scaffold // store a copy for ourselves +} + +// LookupOperator returns the type for the operator you looked up. It errors if +// it doesn't exist, or if the arg length isn't equal to size. +func LookupOperator(operator string, size int) (*types.Type, error) { + scaffold, exists := OperatorFuncs[operator] + if !exists { + return nil, fmt.Errorf("operator not found") + } + + typ := addOperatorArg(scaffold.T) // add in the `operatorArgName` arg + if len(typ.Ord) != size { + return nil, fmt.Errorf("operator has wrong size") + } + + return typ, nil +} + +// OperatorFunc is an operator function that performs an operation on N values. +// XXX: Can we wrap SimpleFunc instead of having the boilerplate here ourselves? +type OperatorFunc struct { + Type *types.Type // Kind == Function, including operator arg + + 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 *OperatorFunc) String() string { + // TODO: return the exact operator if we can guarantee it doesn't change + return OperatorFuncName +} + +// argNames returns the maximum list of possible argNames. This can be truncated +// if needed. The first arg name is the operator. +func (obj *OperatorFunc) argNames() ([]string, error) { + // we could just do this statically, but i did it dynamically so that I + // wouldn't ever have to remember to update this list... + m := 0 // max + for _, scaffold := range OperatorFuncs { + m = max(m, len(scaffold.T.Ord)) + } + + args := []string{operatorArgName} + for i := 0; i < m; i++ { + s := util.NumToAlpha(i) + if s == operatorArgName { + return nil, fmt.Errorf("can't use `%s` as arg name", operatorArgName) + } + args = append(args, s) + } + + return args, nil +} + +// findFunc tries to find the first available registered operator function that +// matches the Operator/Type pattern requested. If none is found it returns nil. +func (obj *OperatorFunc) findFunc(operator string) interfaces.FuncSig { + scaffold, exists := OperatorFuncs[operator] + if !exists { + return nil + } + //typ := removeOperatorArg(obj.Type) // remove operator so we can match... + //for _, fn := range fns { + // if err := fn.Type().Cmp(typ); err == nil { // found one! + // return fn + // } + //} + return scaffold.F +} + +// ArgGen returns the Nth arg name for this function. +func (obj *OperatorFunc) ArgGen(index int) (string, error) { + seq, err := obj.argNames() + if err != nil { + return "", err + } + if l := len(seq); index >= l { + return "", fmt.Errorf("index %d exceeds arg length of %d", index, l) + } + return seq[index], nil +} + +// FuncInfer takes partial type and value information from the call site of this +// function so that it can build an appropriate type signature for it. The type +// signature may include unification variables. +func (obj *OperatorFunc) FuncInfer(partialType *types.Type, partialValues []types.Value) (*types.Type, []*interfaces.UnificationInvariant, error) { + // The operator must be known statically to be able to return a result. + if partialType == nil || len(partialValues) == 0 { + return nil, nil, fmt.Errorf("partials must not be nil or empty") + } + // redundant + //if partialType.Map == nil || len(partialType.Map) == 0 { + // return nil, nil, fmt.Errorf("must have at least one arg in operator func") + //} + //if partialType.Ord == nil || len(partialType.Ord) == 0 { + // return nil, nil, fmt.Errorf("must have at least one arg in operator func") + //} + + val := partialValues[0] + if val == nil { + return nil, nil, fmt.Errorf("first arg for operator func must not be nil") + } + + if err := val.Type().Cmp(types.TypeStr); err != nil { // op must be str + return nil, nil, fmt.Errorf("first arg for operator func must be an str") + } + op := val.Str() // known str + size := len(partialType.Ord) // we know size! + + typ, err := LookupOperator(op, size) + if err != nil { + return nil, nil, errwrap.Wrapf(err, "error finding signature for operator `%s`", op) + } + if typ == nil { + return nil, nil, fmt.Errorf("no matching signature for operator `%s` could be found", op) + } + + return typ, []*interfaces.UnificationInvariant{}, nil +} + +// 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 *OperatorFunc) Build(typ *types.Type) (*types.Type, error) { + // typ is the KindFunc signature we're trying to build... + if len(typ.Ord) < 1 { + return nil, fmt.Errorf("the operator function needs at least 1 arg") + } + if typ.Out == nil { + return nil, fmt.Errorf("return type of function must be specified") + } + if typ.Kind != types.KindFunc { + return nil, fmt.Errorf("unexpected build kind of: %v", typ.Kind) + } + + // Change arg names to be what we expect... + if _, exists := typ.Map[typ.Ord[0]]; !exists { + return nil, fmt.Errorf("invalid build type") + } + + //newTyp := typ.Copy() + newTyp := &types.Type{ + Kind: typ.Kind, // copy + Map: make(map[string]*types.Type), // new + Ord: []string{}, // new + Out: typ.Out, // copy + } + for i, x := range typ.Ord { // remap arg names + //argName := util.NumToAlpha(i - 1) + //if i == 0 { + // argName = operatorArgName + //} + argName, err := obj.ArgGen(i) + if err != nil { + return nil, err + } + + newTyp.Map[argName] = typ.Map[x] + newTyp.Ord = append(newTyp.Ord, argName) + } + + obj.Type = newTyp // func type + return obj.Type, nil +} + +// Validate tells us if the input struct takes a valid form. +func (obj *OperatorFunc) Validate() error { + if obj.Type == nil { // build must be run first + return fmt.Errorf("type is still unspecified") + } + if obj.Type.Kind != types.KindFunc { + return fmt.Errorf("type must be a kind of func") + } + return nil +} + +// Info returns some static info about itself. Build must be called before this +// will return correct data. +func (obj *OperatorFunc) Info() *interfaces.Info { + // Since this function implements FuncInfer we want sig to return nil to + // avoid an accidental return of unification variables when we should be + // getting them from FuncInfer, and not from here. (During unification!) + return &interfaces.Info{ + Pure: true, + Memo: false, + Sig: obj.Type, // func kind, which includes operator arg as input + Err: obj.Validate(), + } +} + +// Init runs some startup code for this function. +func (obj *OperatorFunc) Init(init *interfaces.Init) error { + obj.init = init + return nil +} + +// Stream returns the changing values that this func has over time. +func (obj *OperatorFunc) Stream(ctx context.Context) error { + var op, lastOp string + var fn interfaces.FuncSig + 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 + + // programming error safety check... + programmingError := false + keys := []string{} + for k := range input.Struct() { + keys = append(keys, k) + if !util.StrInList(k, obj.Type.Ord) { + programmingError = true + } + } + if programmingError { + return fmt.Errorf("bad args, got: %v, want: %v", keys, obj.Type.Ord) + } + + // build up arg list + args := []types.Value{} + for _, name := range obj.Type.Ord { + v, exists := input.Struct()[name] + if !exists { + // programming error + return fmt.Errorf("function engine was early, missing arg: %s", name) + } + if name == operatorArgName { + op = v.Str() + continue // skip over the operator arg + } + args = append(args, v) + } + + if op == "" { + // programming error + return fmt.Errorf("operator cannot be empty, args: %v", keys) + } + // operator selection is dynamic now, although mostly it + // should not change... to do so is probably uncommon... + if fn == nil { + fn = obj.findFunc(op) + + } else if op != lastOp { + // TODO: check sig is compatible instead? + return fmt.Errorf("op changed from %s to %s", lastOp, op) + } + + if fn == nil { + return fmt.Errorf("func not found for operator `%s` with sig: `%+v`", op, obj.Type) + } + lastOp = op + + var result types.Value + + result, err := fn(ctx, args) // (Value, error) + if err != nil { + return errwrap.Wrapf(err, "problem running function") + } + if result == nil { + return fmt.Errorf("computed function output was nil") + } + + // 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 + } + } +} + +// removeOperatorArg returns a copy of the input KindFunc type, without the +// operator arg which specifies which operator we're using. It *is* idempotent. +func removeOperatorArg(typ *types.Type) *types.Type { + if typ == nil { + return nil + } + if _, exists := typ.Map[operatorArgName]; !exists { + return typ // pass through + } + + m := make(map[string]*types.Type) + ord := []string{} + for _, s := range typ.Ord { + if s == operatorArgName { + continue // remove the operator + } + m[s] = typ.Map[s] + ord = append(ord, s) + } + return &types.Type{ + Kind: types.KindFunc, + Map: m, + Ord: ord, + Out: typ.Out, + } +} + +// addOperatorArg returns a copy of the input KindFunc type, with the operator +// arg which specifies which operator we're using added. This is idempotent. +func addOperatorArg(typ *types.Type) *types.Type { + if typ == nil { + return nil + } + if _, exists := typ.Map[operatorArgName]; exists { + return typ // pass through + } + + m := make(map[string]*types.Type) + m[operatorArgName] = types.TypeStr // add the operator + ord := []string{operatorArgName} // add the operator + for _, s := range typ.Ord { + m[s] = typ.Map[s] + ord = append(ord, s) + } + return &types.Type{ + Kind: types.KindFunc, + Map: m, + Ord: ord, + Out: typ.Out, + } +} diff --git a/lang/funcs/simple/simple.go b/lang/funcs/simple/simple.go index 27a9ad80..e55e2e79 100644 --- a/lang/funcs/simple/simple.go +++ b/lang/funcs/simple/simple.go @@ -36,8 +36,10 @@ import ( "strings" "github.com/purpleidea/mgmt/lang/funcs" + "github.com/purpleidea/mgmt/lang/funcs/wrapped" "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/types" + unificationUtil "github.com/purpleidea/mgmt/lang/unification/util" "github.com/purpleidea/mgmt/util/errwrap" ) @@ -48,154 +50,150 @@ const ( DirectInterface = false // XXX: fix any bugs and set to true! ) -// RegisteredFuncs maps a function name to the corresponding static, pure func. -var RegisteredFuncs = make(map[string]*types.FuncValue) // must initialize +// RegisteredFuncs maps a function name to the corresponding function scaffold. +var RegisteredFuncs = make(map[string]*Scaffold) // must initialize -// Register registers a simple, static, pure function. It is easier to use than -// the raw function API, but also limits you to simple, static, pure functions. -func Register(name string, fn *types.FuncValue) { +// Scaffold holds the necessary data to build a (possibly polymorphic) function +// with this API. +type Scaffold struct { + // T is the type of the function. It can include unification variables. + // At a minimum, this must be a `func(?1) ?2` as a naked `?1` is not + // allowed. (TODO: Because of ArgGen.) + T *types.Type + + // C is a check function to run after type unification. It will get + // passed the solved type of this function. It should error if this is + // not an acceptable option. This function can be omitted. + C func(typ *types.Type) error + + // F is the implementation of the function. The input type can be + // determined by inspecting the values. Of note, this API does not tell + // the implementation what the correct return type should be. If it + // can't be determined from the input types, then a different function + // API needs to be used. XXX: Should we extend this here? + F interfaces.FuncSig +} + +// Register registers a simple, static, pure, polymorphic function. It is easier +// to use than the raw function API. It allows you to build and check a function +// based on a type signature that contains unification variables. You may only +// specify a single type signature with the API, so 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, scaffold *Scaffold) { if _, exists := RegisteredFuncs[name]; exists { panic(fmt.Sprintf("a simple func named %s is already registered", name)) } - if fn == nil { - panic(fmt.Sprintf("simple func %s contains no function body", name)) + + if scaffold == nil { + panic("no scaffold specified for simple func") } - if fn.T == nil { - panic(fmt.Sprintf("simple func %s contains a nil type signature", name)) + if scaffold.T == nil { + panic("no type specified for simple func") } - if fn.T.Kind != types.KindFunc { - panic(fmt.Sprintf("simple func %s must be of kind func", name)) + if scaffold.T.Kind != types.KindFunc { + panic("type must be a func") } - if fn.T.HasVariant() { - panic(fmt.Sprintf("simple func %s contains a variant type signature", name)) + if scaffold.T.HasVariant() { + panic("func contains a variant type signature") + } + // It's okay if scaffold.C is nil. + if scaffold.F == nil { + panic("no implementation specified for simple func") } - RegisteredFuncs[name] = fn // store a copy for ourselves + RegisteredFuncs[name] = scaffold // store a copy for ourselves // register a copy in the main function database - funcs.Register(name, func() interfaces.Func { return &WrappedFunc{Name: name, Fn: fn} }) + funcs.Register(name, func() interfaces.Func { + return &Func{ + WrappedFunc: &wrapped.Func{ + Name: name, + // NOTE: It might be more correct to Copy here, + // but we do the copy inside of ExprFunc.Copy() + // instead, so that the same type can be unified + // in more than one way. Doing it here wouldn't + // be harmful, but it's an extra copy we don't + // need to do AFAICT. + Type: scaffold.T, // .Copy(), + }, + Check: scaffold.C, + Func: scaffold.F, + } + }) } // ModuleRegister is exactly like Register, except that it registers within a // named module. This is a helper function. -func ModuleRegister(module, name string, fn *types.FuncValue) { - Register(module+funcs.ModuleSep+name, fn) +func ModuleRegister(module, name string, scaffold *Scaffold) { + Register(module+funcs.ModuleSep+name, scaffold) } -// WrappedFunc is a scaffolding function struct which fulfills the boiler-plate -// for the function API, but that can run a very simple, static, pure function. -type WrappedFunc struct { - Name string +// WrappedFunc is a type alias so that we can embed `wrapped.Func` inside our +// struct, since the Func name collides with our Func field name. +type WrappedFunc = wrapped.Func - Fn *types.FuncValue +var _ interfaces.BuildableFunc = &Func{} // ensure it meets this expectation - init *interfaces.Init - last types.Value // last value received to use for diff +// Func 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 Func struct { + *WrappedFunc // *wrapped.Func as a type alias to pull in the base impl. - result types.Value // last calculated output + // Check is a check function to run after type unification. It will get + // passed the solved type of this function. It should error if this is + // not an acceptable option. This function can be omitted. + Check func(typ *types.Type) error + + // Func is the implementation of the function. The input type can be + // determined by inspecting the values. Of note, this API does not tell + // the implementation what the correct return type should be. If it + // can't be determined from the input types, then a different function + // API needs to be used. XXX: Should we extend this here? + Func interfaces.FuncSig } -// String returns a simple name for this function. This is needed so this struct -// can satisfy the pgraph.Vertex interface. -func (obj *WrappedFunc) String() string { - return fmt.Sprintf("%s @ %p", obj.Name, obj) // be more unique! -} +// Build is run to turn the maybe polymorphic, undetermined function, into the +// specific statically typed version. It is usually run after unification +// completes, and must be run before Info() and any of the other Func interface +// methods are used. For this function API, it just runs the Check function to +// make sure that the type found during unification is one of the valid ones. +func (obj *Func) Build(typ *types.Type) (*types.Type, error) { + // typ is the KindFunc signature we're trying to build... -// ArgGen returns the Nth arg name for this function. -func (obj *WrappedFunc) ArgGen(index int) (string, error) { - typ := obj.Fn.Type() - if typ.Kind != types.KindFunc { - return "", fmt.Errorf("expected %s, got %s", types.KindFunc, typ.Kind) - } - seq := typ.Ord - if l := len(seq); index >= l { - return "", fmt.Errorf("index %d exceeds arg length of %d", index, l) - } - return seq[index], nil -} - -// Validate makes sure we've built our struct properly. It is usually unused for -// normal functions that users can use directly. -func (obj *WrappedFunc) Validate() error { - if obj.Fn == nil { // build must be run first - return fmt.Errorf("type is still unspecified") - } - return nil -} - -// Info returns some static info about itself. -func (obj *WrappedFunc) Info() *interfaces.Info { - var typ *types.Type - if obj.Fn != nil { // don't panic if called speculatively - typ = obj.Fn.Type() - } - - return &interfaces.Info{ - Pure: true, - Memo: false, // TODO: should this be something we specify here? - Sig: typ, - Err: obj.Validate(), - } -} - -// Init runs some startup code for this function. -func (obj *WrappedFunc) Init(init *interfaces.Init) error { - obj.init = init - return nil -} - -// Stream returns the changing values that this func has over time. -func (obj *WrappedFunc) Stream(ctx context.Context) 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) - } - - result, err := obj.Fn.Call(ctx, values) // (Value, error) - if err != nil { - return errwrap.Wrapf(err, "simple function errored") - } - - // TODO: do we want obj.result to be a pointer instead? - if obj.result == result { - continue // result didn't change - } - obj.result = result // store new result - - case <-ctx.Done(): - return nil + if obj.Check != nil { + if err := obj.Check(typ); err != nil { + return nil, errwrap.Wrapf(err, "can't build %s with %s", obj.Name, typ) } + } - 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 + fn := &types.FuncValue{ + T: typ, + V: obj.Func, // implementation + } + obj.Fn = fn + return obj.Fn.T, nil +} + +// TypeMatch accepts a list of possible type signatures that we want to check +// against after type unification. This helper function returns a function which +// is suitable for use in the scaffold check function field. +func TypeMatch(typeList []string) func(*types.Type) error { + return func(typ *types.Type) error { + for _, s := range typeList { + t := types.NewType(s) + if t == nil { + // TODO: should we panic? + continue // skip + } + if unificationUtil.UnifyCmp(typ, t) == nil { + return nil } - case <-ctx.Done(): - return nil } + return fmt.Errorf("did not match") + } } @@ -242,9 +240,9 @@ func StructRegister(moduleName string, args interface{}) error { } //fmt.Printf("T: %+v\n", typed.String()) // debug - ModuleRegister(moduleName, name, &types.FuncValue{ + ModuleRegister(moduleName, name, &Scaffold{ T: types.NewType(fmt.Sprintf("func() %s", typed.String())), - V: func(ctx context.Context, input []types.Value) (types.Value, error) { + F: func(ctx context.Context, input []types.Value) (types.Value, error) { //if args == nil { // // programming error // return nil, fmt.Errorf("could not convert/access our struct") diff --git a/lang/funcs/simplepoly/simplepoly.go b/lang/funcs/simplepoly/simplepoly.go deleted file mode 100644 index 9ff6eec7..00000000 --- a/lang/funcs/simplepoly/simplepoly.go +++ /dev/null @@ -1,635 +0,0 @@ -// Mgmt -// Copyright (C) 2013-2024+ 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 . -// -// Additional permission under GNU GPL version 3 section 7 -// -// If you modify this program, or any covered work, by linking or combining it -// with embedded mcl code and modules (and that the embedded mcl code and -// modules which link with this program, contain a copy of their source code in -// the authoritative form) containing parts covered by the terms of any other -// license, the licensors of this program grant you additional permission to -// convey the resulting work. Furthermore, the licensors of this program grant -// the original author, James Shubin, additional permission to update this -// additional permission if he deems it necessary to achieve the goals of this -// additional permission. - -package simplepoly - -import ( - "context" - "fmt" - - "github.com/purpleidea/mgmt/lang/funcs" - "github.com/purpleidea/mgmt/lang/interfaces" - "github.com/purpleidea/mgmt/lang/types" - langUtil "github.com/purpleidea/mgmt/lang/util" - "github.com/purpleidea/mgmt/util/errwrap" -) - -const ( - // DirectInterface specifies whether we should use the direct function - // API or not. If we don't use it, then these simple functions are - // wrapped with the struct below. - DirectInterface = false // XXX: fix any bugs and set to true! - - // AllowSimplePolyVariantDefinitions specifies whether we're allowed to - // include the `variant` type in definitons for simple poly functions. - // Long term, it's probably better to have this be false because it adds - // complexity into this simple poly API, and the root of which is the - // argComplexCmp which is only moderately powerful, but I figured I'd - // try and allow this for now because I liked how elegant the definition - // of the len() function was. - AllowSimplePolyVariantDefinitions = true -) - -// 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)) - } - - if len(fns) == 0 { - panic("no functions specified for simple polyfunc") - } - - // check for uniqueness in type signatures - typs := []*types.Type{} - for i, f := range fns { - if f.T == nil { - panic(fmt.Sprintf("polyfunc %s contains a nil type signature", name)) - } - if f.T.Kind != types.KindFunc { // even when this includes a variant - panic(fmt.Sprintf("polyfunc %s must be of kind func", name)) - } - if !AllowSimplePolyVariantDefinitions && f.T.HasVariant() { - panic(fmt.Sprintf("polyfunc %s contains a variant type signature at index: %d", name, i)) - } - typs = append(typs, f.T) - } - - if err := langUtil.HasDuplicateTypes(typs); err != nil { - panic(fmt.Sprintf("polyfunc %s has a duplicate implementation: %+v", name, err)) - } - - _, err := consistentArgs(fns) - if err != nil { - panic(fmt.Sprintf("polyfunc %s has inconsistent arg names: %+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 &WrappedFunc{Name: name, Fns: fns} }) -} - -// ModuleRegister is exactly like Register, except that it registers within a -// named module. This is a helper function. -func ModuleRegister(module, name string, fns []*types.FuncValue) { - Register(module+funcs.ModuleSep+name, fns) -} - -// consistentArgs returns the list of arg names across all the functions or -// errors if one consistent list could not be found. -func consistentArgs(fns []*types.FuncValue) ([]string, error) { - if len(fns) == 0 { - return nil, fmt.Errorf("no functions specified for simple polyfunc") - } - seq := []string{} - for _, x := range fns { - typ := x.Type() - if typ.Kind != types.KindFunc { - return nil, fmt.Errorf("expected %s, got %s", types.KindFunc, typ.Kind) - } - ord := typ.Ord - // check - l := len(seq) - if m := len(ord); m < l { - l = m // min - } - for i := 0; i < l; i++ { // check shorter list - if seq[i] != ord[i] { - return nil, fmt.Errorf("arg name at index %d differs (%s != %s)", i, seq[i], ord[i]) - } - } - seq = ord // keep longer version! - } - return seq, nil -} - -var _ interfaces.PolyFunc = &WrappedFunc{} // ensure it meets this expectation - -// WrappedFunc 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 WrappedFunc struct { - Name string - - 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 -} - -// String returns a simple name for this function. This is needed so this struct -// can satisfy the pgraph.Vertex interface. -func (obj *WrappedFunc) String() string { - return fmt.Sprintf("%s @ %p", obj.Name, obj) // be more unique! -} - -// ArgGen returns the Nth arg name for this function. -func (obj *WrappedFunc) ArgGen(index int) (string, error) { - seq, err := consistentArgs(obj.Fns) - if err != nil { - return "", err - } - 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 *WrappedFunc) Unify(expr interfaces.Expr) ([]interfaces.Invariant, error) { - if len(obj.Fns) == 0 { - return nil, fmt.Errorf("no matching signatures for simple polyfunc") - } - - var invariants []interfaces.Invariant - var invar interfaces.Invariant - - // Special case to help it solve faster. We still include the generator, - // in the chance that the relationship between the args is an important - // linkage that we should be specifying somehow... - if len(obj.Fns) == 1 { - fn := obj.Fns[0] - if fn == nil { - // programming error - return nil, fmt.Errorf("simple poly function value is nil") - } - typ := fn.T - if typ == nil { - // programming error - return nil, fmt.Errorf("simple poly function type is nil") - } - invar = &interfaces.EqualsInvariant{ - Expr: expr, - Type: typ, - } - invariants = append(invariants, invar) - } - - dummyOut := &interfaces.ExprAny{} // corresponds to the out type - - // return type is currently unknown - invar = &interfaces.AnyInvariant{ - Expr: dummyOut, // make sure to include it so we know it solves - } - 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! - // any number of args are permitted - - // helper function to build our complex func invariants - buildInvar := func(typ *types.Type) ([]interfaces.Invariant, error) { - var invariants []interfaces.Invariant - var invar interfaces.Invariant - // full function - mapped := make(map[string]interfaces.Expr) - ordered := []string{} - // assume this is a types.KindFunc - for i, x := range typ.Ord { - t := typ.Map[x] - if t == nil { - // programming error - return nil, fmt.Errorf("unexpected func nil arg (%d) type", i) - } - - argName, err := obj.ArgGen(i) - if err != nil { - return nil, err - } - - dummyArg := &interfaces.ExprAny{} - invar = &interfaces.EqualsInvariant{ - Expr: dummyArg, - Type: t, - } - invariants = append(invariants, invar) - - invar = &interfaces.EqualityInvariant{ - Expr1: dummyArg, - Expr2: cfavInvar.Args[i], - } - invariants = append(invariants, invar) - - mapped[argName] = dummyArg - ordered = append(ordered, argName) - } - - invar = &interfaces.EqualityWrapFuncInvariant{ - Expr1: expr, // maps directly to us! - Expr2Map: mapped, - Expr2Ord: ordered, - Expr2Out: dummyOut, - } - invariants = append(invariants, invar) - - if typ.Out == nil { - // programming error - return nil, fmt.Errorf("unexpected func nil return type") - } - - // remember to add the relationship to the - // return type of the functions as well... - invar = &interfaces.EqualsInvariant{ - Expr: dummyOut, - Type: typ.Out, - } - invariants = append(invariants, invar) - - return invariants, nil - } - - // argCmp trims down the list of possible types... - // this makes our exclusive invariants smaller, and - // easier to solve without combinatorial slow recursion - argCmp := func(typ *types.Type) bool { - if len(cfavInvar.Args) != len(typ.Ord) { - return false // arg length differs - } - for i, x := range cfavInvar.Args { - if t, err := x.Type(); err == nil { - if t.Cmp(typ.Map[typ.Ord[i]]) != nil { - return false // impossible! - } - } - - // is the type already known as solved? - if t, exists := solved[x]; exists { // alternate way to lookup type - if t.Cmp(typ.Map[typ.Ord[i]]) != nil { - return false // impossible! - } - } - } - return true // possible - } - - argComplexCmp := func(typ *types.Type) (*types.Type, bool) { - if !typ.HasVariant() { - return typ, argCmp(typ) - } - - mapped := make(map[string]*types.Type) - ordered := []string{} - out := typ.Out - if len(cfavInvar.Args) != len(typ.Ord) { - return nil, false // arg length differs - } - for i, x := range cfavInvar.Args { - name := typ.Ord[i] - if t, err := x.Type(); err == nil { - if _, err := t.ComplexCmp(typ.Map[typ.Ord[i]]); err != nil { - return nil, false // impossible! - } - mapped[name] = t // found it - } - - // is the type already known as solved? - if t, exists := solved[x]; exists { // alternate way to lookup type - if _, err := t.ComplexCmp(typ.Map[typ.Ord[i]]); err != nil { - return nil, false // impossible! - } - // check it matches the above type - if oldT, exists := mapped[name]; exists && t.Cmp(oldT) != nil { - return nil, false // impossible! - } - mapped[name] = t // found it - } - if _, exists := mapped[name]; !exists { - // impossible, but for a - // different reason: we don't - // have enough information to - // plausibly allow this type to - // pass through, because we'd - // leave a variant in, so skip - // it. We'll probably fail in - // the end with a misleading - // "only recursive solutions - // left" error, but it just - // means we can't solve this! - return nil, false - } - ordered = append(ordered, name) - } - - // if we happen to know the type of the return expr - if t, exists := solved[cfavInvar.Expr]; exists { - if out != nil && t.Cmp(out) != nil { - return nil, false // inconsistent! - } - out = t // learn! - } - - return &types.Type{ - Kind: types.KindFunc, - Map: mapped, - Ord: ordered, - Out: out, - }, true // possible - } - - var invariants []interfaces.Invariant - var invar interfaces.Invariant - - // add the relationship to the returned value - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Expr, - Expr2: dummyOut, - } - invariants = append(invariants, invar) - - ors := []interfaces.Invariant{} // solve only one from this list - for _, f := range obj.Fns { // operator func types - typ := f.T - if typ == nil { - return nil, fmt.Errorf("nil type signature found") - } - if typ.Kind != types.KindFunc { - // programming error - return nil, fmt.Errorf("type must be a kind of func") - } - - // filter out impossible types, and on success, - // use the replacement type that we found here! - // this is because the input might be a variant - // and after processing this, we get a concrete - // type that can be substituted in here instead - if typ, ok = argComplexCmp(typ); !ok { - continue // not a possible match - } - if typ.HasVariant() { - // programming error - return nil, fmt.Errorf("a variant type snuck through: %+v", typ) - } - - invars, err := buildInvar(typ) - if err != nil { - return nil, err - } - - // all of these need to be true together - and := &interfaces.ConjunctionInvariant{ - Invariants: invars, - } - ors = append(ors, and) // one solution added! - } - if len(ors) == 0 { - return nil, fmt.Errorf("no matching signatures for simple poly func could be found") - } - - // TODO: To improve the filtering, it would be - // excellent if we could examine the return type in - // `solved` somehow (if it is known) and use that to - // trim our list of exclusives down even further! The - // smaller the exclusives are, the faster everything in - // the solver can run. - invar = &interfaces.ExclusiveInvariant{ - Invariants: ors, // one and only one of these should be true - } - if len(ors) == 1 { - invar = ors[0] // there should only be one - } - invariants = append(invariants, invar) - - // 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 *WrappedFunc) 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) - // can an f.T with a variant compare with a partial ? (yes) - if err != nil { - continue - } - typs = append(typs, f.T) - } - - return typs, nil -} - -// 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. -func (obj *WrappedFunc) Build(typ *types.Type) (*types.Type, error) { - // typ is the KindFunc signature we're trying to build... - - index, err := langUtil.FnMatch(typ, obj.Fns) - if err != nil { - return nil, err - } - newTyp := obj.buildFunction(typ, index) // found match at this index - - return newTyp, nil -} - -// buildFunction builds our concrete static function, from the potentially -// abstract, possibly variant containing list of functions. -func (obj *WrappedFunc) buildFunction(typ *types.Type, ix int) *types.Type { - cp := obj.Fns[ix].Copy() - fn, ok := cp.(*types.FuncValue) - if !ok { - panic("unexpected type") - } - obj.fn = fn - // FIXME: if obj.fn.T == nil {} // occasionally this is nil, is it a bug? - obj.fn.T = typ.Copy() // overwrites any contained "variant" type - - return obj.fn.T -} - -// Validate makes sure we've built our struct properly. It is usually unused for -// normal functions that users can use directly. -func (obj *WrappedFunc) 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 := langUtil.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 *WrappedFunc) Info() *interfaces.Info { - var typ *types.Type - if obj.fn != nil { // don't panic if called speculatively - typ = obj.fn.Type() - } - - return &interfaces.Info{ - Pure: true, - Memo: false, // TODO: should this be something we specify here? - Sig: typ, - Err: obj.Validate(), - } -} - -// Init runs some startup code for this function. -func (obj *WrappedFunc) Init(init *interfaces.Init) error { - obj.init = init - return nil -} - -// Stream returns the changing values that this func has over time. -func (obj *WrappedFunc) Stream(ctx context.Context) 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(ctx, 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", result) - } - - // TODO: do we want obj.result to be a pointer instead? - if obj.result == result { - continue // result didn't change - } - obj.result = result // store new result - - case <-ctx.Done(): - 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 <-ctx.Done(): - return nil - } - } -} diff --git a/lang/funcs/struct_lookup_func.go b/lang/funcs/struct_lookup_func.go index a3564319..a9c8290e 100644 --- a/lang/funcs/struct_lookup_func.go +++ b/lang/funcs/struct_lookup_func.go @@ -52,13 +52,15 @@ func init() { Register(StructLookupFuncName, func() interfaces.Func { return &StructLookupFunc{} }) // must register the func and name } -var _ interfaces.PolyFunc = &StructLookupFunc{} // ensure it meets this expectation +var _ interfaces.BuildableFunc = &StructLookupFunc{} // ensure it meets this expectation // StructLookupFunc is a struct field lookup function. type StructLookupFunc struct { Type *types.Type // Kind == Struct, that is used as the struct we lookup Out *types.Type // type of field we're extracting + built bool // was this function built yet? + init *interfaces.Init last types.Value // last value received to use for diff field string @@ -81,325 +83,53 @@ func (obj *StructLookupFunc) ArgGen(index int) (string, error) { return seq[index], nil } -// Unify returns the list of invariants that this func produces. -func (obj *StructLookupFunc) Unify(expr interfaces.Expr) ([]interfaces.Invariant, error) { - var invariants []interfaces.Invariant - var invar interfaces.Invariant - - // func(struct T1, field str) T2 - - structName, err := obj.ArgGen(0) - if err != nil { - return nil, err +// helper +func (obj *StructLookupFunc) sig() *types.Type { + st := "?1" + out := "?2" + if obj.Type != nil { + st = obj.Type.String() + } + if obj.Out != nil { + out = obj.Out.String() } - fieldName, err := obj.ArgGen(1) - if err != nil { - return nil, err - } - - dummyStruct := &interfaces.ExprAny{} // corresponds to the struct type - dummyField := &interfaces.ExprAny{} // corresponds to the field type - dummyOut := &interfaces.ExprAny{} // corresponds to the out string - - // field arg type of string - invar = &interfaces.EqualsInvariant{ - Expr: dummyField, - Type: types.TypeStr, - } - invariants = append(invariants, invar) - - // XXX: we could use this relationship *if* our solver could understand - // different fields, and partial struct matches. I guess we'll leave it - // for another day! - //mapped := make(map[string]interfaces.Expr) - //ordered := []string{???} - //mapped[???] = dummyField - //invar = &interfaces.EqualityWrapStructInvariant{ - // Expr1: dummyStruct, - // Expr2Map: mapped, - // Expr2Ord: ordered, - //} - //invariants = append(invariants, invar) - - // full function - mapped := make(map[string]interfaces.Expr) - ordered := []string{structName, fieldName} - mapped[structName] = dummyStruct - mapped[fieldName] = dummyField - - invar = &interfaces.EqualityWrapFuncInvariant{ - Expr1: expr, // maps directly to us! - Expr2Map: mapped, - Expr2Ord: ordered, - Expr2Out: dummyOut, - } - 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) - } - - var invariants []interfaces.Invariant - var invar interfaces.Invariant - - // add the relationship to the returned value - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Expr, - Expr2: dummyOut, - } - invariants = append(invariants, invar) - - // add the relationships to the called args - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Args[0], - Expr2: dummyStruct, - } - invariants = append(invariants, invar) - - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Args[1], - Expr2: dummyField, - } - invariants = append(invariants, invar) - - // second arg must be a string - invar = &interfaces.EqualsInvariant{ - Expr: cfavInvar.Args[1], - Type: types.TypeStr, - } - invariants = append(invariants, invar) - - value, err := cfavInvar.Args[1].Value() // is it known? - if err != nil { - return nil, fmt.Errorf("field string is not known statically") - } - - if k := value.Type().Kind; k != types.KindStr { - return nil, fmt.Errorf("unable to build function with 1st arg of kind: %s", k) - } - field := value.Str() // must not panic - - // If we figure out both of these two types, we'll know - // the full type... - var t1 *types.Type // struct type - var t2 *types.Type // return type - - // validateArg0 checks: struct T1 - validateArg0 := func(typ *types.Type) error { - if typ == nil { // unknown so far - return nil - } - - // we happen to have a struct! - if k := typ.Kind; k != types.KindStruct { - return fmt.Errorf("unable to build function with 0th arg of kind: %s", k) - } - - // check both Ord and Map for safety - found := false - for _, s := range typ.Ord { - if s == field { - found = true - break - } - } - t, exists := typ.Map[field] // type found is T2 - if !exists || !found { - return fmt.Errorf("struct is missing field: %s", field) - } - - if err := typ.Cmp(t1); t1 != nil && err != nil { - return errwrap.Wrapf(err, "input type was inconsistent") - } - - if err := t.Cmp(t2); t2 != nil && err != nil { - return errwrap.Wrapf(err, "input type was inconsistent") - } - - // learn! - t1 = typ - t2 = t - 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 struct 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 struct arg type is inconsistent") - } - } - - // XXX: if the struct type/value isn't know statically? - - if t1 != nil { - invar = &interfaces.EqualsInvariant{ - Expr: dummyStruct, - Type: t1, - } - invariants = append(invariants, invar) - - // We know *some* information about the struct! - // Let's hope the unusedField expr won't trip - // up the solver... - mapped := make(map[string]interfaces.Expr) - ordered := []string{} - for _, x := range t1.Ord { - // We *don't* need to solve unusedField - unusedField := &interfaces.ExprAny{} - mapped[x] = unusedField - if x == field { // the one we care about - mapped[x] = dummyOut - } - ordered = append(ordered, x) - } - // We map to dummyOut which is the return type - // and has the same type of the field we want! - mapped[field] = dummyOut // redundant =D - invar = &interfaces.EqualityWrapStructInvariant{ - Expr1: dummyStruct, - Expr2Map: mapped, - Expr2Ord: ordered, - } - invariants = append(invariants, invar) - } - if t2 != nil { - invar := &interfaces.EqualsInvariant{ - Expr: dummyOut, - Type: t2, - } - invariants = append(invariants, invar) - } - - // XXX: if t1 or t2 are missing, we could also return a - // new generator for later if we learn new information, - // but we'd have to be careful to not do it infinitely. - - // 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 + return types.NewType(fmt.Sprintf( + "func(%s %s, %s str) %s", + structLookupArgNameStruct, st, + structLookupArgNameField, + out, + )) } -// 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 *StructLookupFunc) Polymorphisms(partialType *types.Type, partialValues []types.Value) ([]*types.Type, error) { - // TODO: return `variant` as arg for now -- maybe there's a better way? - variant := []*types.Type{types.NewType("func(struct variant, field str) variant")} +// FuncInfer takes partial type and value information from the call site of this +// function so that it can build an appropriate type signature for it. The type +// signature may include unification variables. +func (obj *StructLookupFunc) FuncInfer(partialType *types.Type, partialValues []types.Value) (*types.Type, []*interfaces.UnificationInvariant, error) { + // func(struct ?1, field str) ?2 - if partialType == nil { - return variant, nil + // This particular function should always get called with a known string + // for the second argument. Without it being known statically, we refuse + // to build this function. + + if l := 2; len(partialValues) != l { + return nil, nil, fmt.Errorf("function must have %d args", l) } - - var typ *types.Type // struct type of the first argument - var out *types.Type // type of the field - - // TODO: if partialValue[0] exists, check it matches the type we expect - ord := partialType.Ord - if partialType.Map != nil { - if len(ord) != 2 { - return nil, fmt.Errorf("must have exactly two args in structlookup func") - } - if tStruct, exists := partialType.Map[ord[0]]; exists && tStruct != nil { - if tStruct.Kind != types.KindStruct { - return nil, fmt.Errorf("first arg for structlookup must be a struct") - } - if !tStruct.HasVariant() { - typ = tStruct // found - } - } - if tField, exists := partialType.Map[ord[1]]; exists && tField != nil { - if tField.Cmp(types.TypeStr) != nil { - return nil, fmt.Errorf("second arg for structlookup must be a string") - } - } - - if len(partialValues) == 2 && partialValues[1] != nil { - if types.TypeStr.Cmp(partialValues[1].Type()) != nil { - return nil, fmt.Errorf("second value must be an str") - } - structType, exists := partialType.Map[ord[0]] - if !exists { - return nil, fmt.Errorf("missing struct field") - } - if structType != nil { - field := partialValues[1].Str() - fieldType, exists := structType.Map[field] - if !exists { - return nil, fmt.Errorf("field: `%s` does not exist in struct", field) - } - if fieldType != nil { - if partialType.Out != nil && fieldType.Cmp(partialType.Out) != nil { - return nil, fmt.Errorf("field `%s` must have same type as return type", field) - } - - out = fieldType // found! - } - } - } - - if tOut := partialType.Out; tOut != nil { - // TODO: we could check that at least one of the types - // in struct.Map was our type, but not very useful... - } + if err := partialValues[1].Type().Cmp(types.TypeStr); err != nil { + return nil, nil, errwrap.Wrapf(err, "function field name must be a str") } - - typFunc := &types.Type{ - Kind: types.KindFunc, // function type - Map: make(map[string]*types.Type), - Ord: []string{structLookupArgNameStruct, structLookupArgNameField}, - Out: out, + s := partialValues[1].Str() // must not panic + if s == "" { + return nil, nil, fmt.Errorf("function must not have an empty field name") } - typFunc.Map[structLookupArgNameStruct] = typ - typFunc.Map[structLookupArgNameField] = types.TypeStr + // This can happen at runtime too, but we save it here for Build()! + obj.field = s // store for later - // set variant instead of nil - if typFunc.Map[structLookupArgNameStruct] == nil { - typFunc.Map[structLookupArgNameStruct] = types.TypeVariant - } - if out == nil { - typFunc.Out = types.TypeVariant - } + // This isn't precise enough because we must guarantee that the field is + // in the struct and that ?1 is actually a struct, but that's okay it is + // something that we'll verify at build time! - return []*types.Type{typFunc}, nil + return obj.sig(), []*interfaces.UnificationInvariant{}, nil } // Build is run to turn the polymorphic, undetermined function, into the @@ -436,18 +166,60 @@ func (obj *StructLookupFunc) Build(typ *types.Type) (*types.Type, error) { return nil, errwrap.Wrapf(err, "field must be an str") } - // NOTE: We actually don't know which field this is, only its type! we - // could have cached the discovered field during Polymorphisms(), but it - // turns out it's not actually necessary for us to know it to build the - // struct. + // NOTE: We actually don't know which field this is yet, only its type! + // We cached the discovered field during Infer(), but it turns out it's + // not actually necessary for us to know it to build the struct. It is + // needed to make sure the lossy Infer unification variables are right. + if tStruct.Kind != types.KindStruct { + return nil, fmt.Errorf("first arg must be of kind struct, got: %s", tStruct.Kind) + } + if obj.field == "" { + // programming error + return nil, fmt.Errorf("did not infer correctly") + } + ix := -1 // not found + for i, x := range tStruct.Ord { + if x != obj.field { + continue + } + // found + if ix != -1 { + // programming error + return nil, fmt.Errorf("duplicate field found") + } + ix = i // found it here! + //break // keep checking for extra safety + } + if ix == -1 { + return nil, fmt.Errorf("field %s was not found in struct", obj.field) + } + obj.Type = tStruct // struct type obj.Out = typ.Out // type of return value + obj.built = true return obj.sig(), nil } +// Copy is implemented so that the obj.field value is not lost if we copy this +// function. That value is learned during FuncInfer, and previously would have +// been lost by the time we used it in Build. +func (obj *StructLookupFunc) Copy() interfaces.Func { + return &StructLookupFunc{ + Type: obj.Type, // don't copy because we use this after unification + Out: obj.Out, + + built: obj.built, + init: obj.init, // likely gets overwritten anyways + field: obj.field, // this we really need! + } +} + // Validate tells us if the input struct takes a valid form. func (obj *StructLookupFunc) Validate() error { + if !obj.built { + return fmt.Errorf("function wasn't built yet") + } if obj.Type == nil { // build must be run first return fmt.Errorf("type is still unspecified") } @@ -469,24 +241,21 @@ func (obj *StructLookupFunc) Validate() error { // Info returns some static info about itself. Build must be called before this // will return correct data. func (obj *StructLookupFunc) Info() *interfaces.Info { + // Since this function implements FuncInfer we want sig to return nil to + // avoid an accidental return of unification variables when we should be + // getting them from FuncInfer, and not from here. (During unification!) var sig *types.Type - if obj.Type != nil { // don't panic if called speculatively - // TODO: can obj.Out be nil (a partial) ? + if obj.built { sig = obj.sig() // helper } return &interfaces.Info{ Pure: true, Memo: false, - Sig: sig, // func kind + Sig: sig, Err: obj.Validate(), } } -// helper -func (obj *StructLookupFunc) sig() *types.Type { - return types.NewType(fmt.Sprintf("func(%s %s, %s str) %s", structLookupArgNameStruct, obj.Type.String(), structLookupArgNameField, obj.Out.String())) -} - // Init runs some startup code for this function. func (obj *StructLookupFunc) Init(init *interfaces.Init) error { obj.init = init @@ -517,19 +286,17 @@ func (obj *StructLookupFunc) Stream(ctx context.Context) error { if field == "" { return fmt.Errorf("received empty field") } - - result, exists := st.Lookup(field) - if !exists { - return fmt.Errorf("could not lookup field: `%s` in struct", field) - } - if obj.field == "" { + // This can happen at compile time too. Bonus! obj.field = field // store first field } - if field != obj.field { return fmt.Errorf("input field changed from: `%s`, to: `%s`", obj.field, field) } + result, exists := st.Lookup(obj.field) + if !exists { + return fmt.Errorf("could not lookup field: `%s` in struct", field) + } // if previous input was `2 + 4`, but now it // changed to `1 + 5`, the result is still the diff --git a/lang/funcs/struct_lookup_optional_func.go b/lang/funcs/struct_lookup_optional_func.go index 7ea1459e..4c8c6f5b 100644 --- a/lang/funcs/struct_lookup_optional_func.go +++ b/lang/funcs/struct_lookup_optional_func.go @@ -54,7 +54,7 @@ func init() { Register(StructLookupOptionalFuncName, func() interfaces.Func { return &StructLookupOptionalFunc{} }) // must register the func and name } -var _ interfaces.PolyFunc = &StructLookupOptionalFunc{} // ensure it meets this expectation +var _ interfaces.InferableFunc = &StructLookupOptionalFunc{} // ensure it meets this expectation // StructLookupOptionalFunc is a struct field lookup function. It does a special // trick in that it will unify on a struct that doesn't have the specified field @@ -65,6 +65,8 @@ type StructLookupOptionalFunc struct { Type *types.Type // Kind == Struct, that is used as the struct we lookup Out *types.Type // type of field we're extracting (also the type of optional) + built bool // was this function built yet? + init *interfaces.Init last types.Value // last value received to use for diff field string @@ -87,315 +89,54 @@ func (obj *StructLookupOptionalFunc) ArgGen(index int) (string, error) { return seq[index], nil } -// Unify returns the list of invariants that this func produces. -func (obj *StructLookupOptionalFunc) Unify(expr interfaces.Expr) ([]interfaces.Invariant, error) { - var invariants []interfaces.Invariant - var invar interfaces.Invariant - - // func(struct T1, field str, optional T2) T2 - - structName, err := obj.ArgGen(0) - if err != nil { - return nil, err +// helper +func (obj *StructLookupOptionalFunc) sig() *types.Type { + st := "?1" + out := "?2" + if obj.Type != nil { + st = obj.Type.String() + } + if obj.Out != nil { + out = obj.Out.String() } - fieldName, err := obj.ArgGen(1) - if err != nil { - return nil, err + return types.NewType(fmt.Sprintf( + "func(%s %s, %s str, %s %s) %s", + structLookupOptionalArgNameStruct, st, + structLookupOptionalArgNameField, + structLookupOptionalArgNameOptional, out, + out, + )) +} + +// FuncInfer takes partial type and value information from the call site of this +// function so that it can build an appropriate type signature for it. The type +// signature may include unification variables. +func (obj *StructLookupOptionalFunc) FuncInfer(partialType *types.Type, partialValues []types.Value) (*types.Type, []*interfaces.UnificationInvariant, error) { + // func(struct ?1, field str, optional ?2) ?2 + + // This particular function should always get called with a known string + // for the second argument. Without it being known statically, we refuse + // to build this function. + + if l := 3; len(partialValues) != l { + return nil, nil, fmt.Errorf("function must have %d args", l) } - - optionalName, err := obj.ArgGen(2) - if err != nil { - return nil, err + if err := partialValues[1].Type().Cmp(types.TypeStr); err != nil { + return nil, nil, errwrap.Wrapf(err, "function field name must be a str") } - - dummyStruct := &interfaces.ExprAny{} // corresponds to the struct type - dummyField := &interfaces.ExprAny{} // corresponds to the field type - dummyOptional := &interfaces.ExprAny{} // corresponds to the optional type - dummyOut := &interfaces.ExprAny{} // corresponds to the out string - - // field arg type of string - invar = &interfaces.EqualsInvariant{ - Expr: dummyField, - Type: types.TypeStr, + s := partialValues[1].Str() // must not panic + if s == "" { + return nil, nil, fmt.Errorf("function must not have an empty field name") } - invariants = append(invariants, invar) + // This can happen at runtime too, but we save it here for Build()! + //obj.field = s // don't store for this optional lookup version! - // XXX: we could use this relationship *if* our solver could understand - // different fields, and partial struct matches. I guess we'll leave it - // for another day! - //mapped := make(map[string]interfaces.Expr) - //ordered := []string{???} - //mapped[???] = dummyField - //invar = &interfaces.EqualityWrapStructInvariant{ - // Expr1: dummyStruct, - // Expr2Map: mapped, - // Expr2Ord: ordered, - //} - //invariants = append(invariants, invar) + // This isn't precise enough because we must guarantee that the field is + // in the struct and that ?1 is actually a struct, but that's okay it is + // something that we'll verify at build time! (Or skip it for optional!) - // These two types should be identical. This is the safest approach. In - // the case where the struct field is missing, then this should be true, - // and when it is present, we'll never use the optional value, but we - // can still enforce it's the same type. - invar = &interfaces.EqualityInvariant{ - Expr1: dummyOptional, - Expr2: dummyOut, - } - invariants = append(invariants, invar) - - // full function - mapped := make(map[string]interfaces.Expr) - ordered := []string{structName, fieldName, optionalName} - mapped[structName] = dummyStruct - mapped[fieldName] = dummyField - mapped[optionalName] = dummyOptional - - invar = &interfaces.EqualityWrapFuncInvariant{ - Expr1: expr, // maps directly to us! - Expr2Map: mapped, - Expr2Ord: ordered, - Expr2Out: dummyOut, - } - 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 != 3 { - return nil, fmt.Errorf("unable to build function with %d args", l) - } - - var invariants []interfaces.Invariant - var invar interfaces.Invariant - - // add the relationship to the returned value - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Expr, - Expr2: dummyOut, - } - invariants = append(invariants, invar) - - // add the relationships to the called args - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Args[0], - Expr2: dummyStruct, - } - invariants = append(invariants, invar) - - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Args[1], - Expr2: dummyField, - } - invariants = append(invariants, invar) - - invar = &interfaces.EqualityInvariant{ - Expr1: cfavInvar.Args[2], - Expr2: dummyOptional, - } - invariants = append(invariants, invar) - - // second arg must be a string - invar = &interfaces.EqualsInvariant{ - Expr: cfavInvar.Args[1], - Type: types.TypeStr, - } - invariants = append(invariants, invar) - - // Not necessary for the field to be known or be static! - var field string - value, err := cfavInvar.Args[1].Value() // is it known? - if err == nil { - if k := value.Type().Kind; k != types.KindStr { - return nil, fmt.Errorf("unable to build function with 1st arg of kind: %s", k) - } - field = value.Str() // must not panic - } - - // If we figure out both of these types, we'll know the - var t1 *types.Type // struct type - var t2 *types.Type // optional / return type - - // validateArg0 checks: struct T1 - validateArg0 := func(typ *types.Type) error { - if typ == nil { // unknown so far - return nil - } - - // we happen to have a struct! - if k := typ.Kind; k != types.KindStruct { - return fmt.Errorf("unable to build function with 0th arg of kind: %s", k) - } - - // check both Ord and Map for safety - found := false - for _, s := range typ.Ord { - if s == field { - found = true - break - } - } - t, exists := typ.Map[field] // type found is T2 - if field != "" { - if !exists || !found { - //fmt.Printf("might be using optional arg, struct is missing field: %s\n", field) - } else if err := t.Cmp(t2); t2 != nil && err != nil { - return errwrap.Wrapf(err, "input type was inconsistent") - } - - // learn! - t2 = t - } - - if err := typ.Cmp(t1); t1 != nil && err != nil { - return errwrap.Wrapf(err, "input type was inconsistent") - } - - // learn! - t1 = typ - return nil - } - - validateArg2OrOut := func(typ *types.Type) error { - if typ == nil { // unknown so far - return nil - } - - if err := typ.Cmp(t2); t2 != nil && err != nil { - return errwrap.Wrapf(err, "input type was inconsistent") - } - - // learn! - t2 = typ - return nil - } - - if typ, err := cfavInvar.Args[0].Type(); err == nil { // is it known? - // this sets t1 (and sometimes t2) on success if it learned - if err := validateArg0(typ); err != nil { - return nil, errwrap.Wrapf(err, "first struct arg type is inconsistent") - } - } - if typ, exists := solved[cfavInvar.Args[0]]; exists { // alternate way to lookup type - // this sets t1 (and sometimes t2) on success if it learned - if err := validateArg0(typ); err != nil { - return nil, errwrap.Wrapf(err, "first struct arg type is inconsistent") - } - } - - if typ, err := cfavInvar.Args[2].Type(); err == nil { // is it known? - // this sets t2 on success if it learned - if err := validateArg2OrOut(typ); err != nil { - return nil, errwrap.Wrapf(err, "third struct arg type is inconsistent") - } - } - if typ, exists := solved[cfavInvar.Args[2]]; exists { // alternate way to lookup type - // this sets t2 on success if it learned - if err := validateArg2OrOut(typ); err != nil { - return nil, errwrap.Wrapf(err, "third struct arg type is inconsistent") - } - } - - // look at the return type too (if known) - if typ, err := cfavInvar.Expr.Type(); err == nil { // is it known? - // this sets t2 on success if it learned - if err := validateArg2OrOut(typ); err != nil { - return nil, errwrap.Wrapf(err, "third struct arg type is inconsistent") - } - } - if typ, exists := solved[cfavInvar.Expr]; exists { // alternate way to lookup type - // this sets t2 on success if it learned - if err := validateArg2OrOut(typ); err != nil { - return nil, errwrap.Wrapf(err, "third struct arg type is inconsistent") - } - } - - // XXX: if the struct type/value isn't know statically? - - if t1 != nil { - invar = &interfaces.EqualsInvariant{ - Expr: dummyStruct, - Type: t1, - } - invariants = append(invariants, invar) - - // We know *some* information about the struct! - // Let's hope the unusedField expr won't trip - // up the solver... - mapped := make(map[string]interfaces.Expr) - ordered := []string{} - for _, x := range t1.Ord { - // We *don't* need to solve unusedField - unusedField := &interfaces.ExprAny{} - mapped[x] = unusedField - if x == field { // the one we care about - mapped[x] = dummyOut - } - ordered = append(ordered, x) - } - // We map to dummyOut which is the return type - // and has the same type of the field we want! - mapped[field] = dummyOut // redundant =D - invar = &interfaces.EqualityWrapStructInvariant{ - Expr1: dummyStruct, - Expr2Map: mapped, - Expr2Ord: ordered, - } - // We only want to add this weird thing if the - // field actually exists. Otherwise ignore it. - if _, exists := t1.Map[field]; field != "" && exists { - invariants = append(invariants, invar) - } - } - if t2 != nil { - invar = &interfaces.EqualsInvariant{ - Expr: dummyOptional, - Type: t2, - } - invariants = append(invariants, invar) - invar = &interfaces.EqualsInvariant{ - Expr: dummyOut, - Type: t2, - } - invariants = append(invariants, invar) - } - - // XXX: if t1 or t2 are missing, we could also return a - // new generator for later if we learn new information, - // but we'd have to be careful to not do it infinitely. - - // 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 + return obj.sig(), []*interfaces.UnificationInvariant{}, nil } // Build is run to turn the polymorphic, undetermined function, into the @@ -440,18 +181,26 @@ func (obj *StructLookupOptionalFunc) Build(typ *types.Type) (*types.Type, error) return nil, errwrap.Wrapf(err, "optional arg must match return type") } - // NOTE: We actually don't know which field this is, only its type! we - // could have cached the discovered field during Polymorphisms(), but it - // turns out it's not actually necessary for us to know it to build the - // struct. + // NOTE: We actually don't know which field this is yet, only its type! + // We don't care, because that's a runtime issue and doesn't need to be + // our problem as long as this is a struct. The only optimization we can + // add is to know statically if we're returning the optional value. + if tStruct.Kind != types.KindStruct { + return nil, fmt.Errorf("first arg must be of kind struct, got: %s", tStruct.Kind) + } + obj.Type = tStruct // struct type obj.Out = typ.Out // type of return value + obj.built = true return obj.sig(), nil } // Validate tells us if the input struct takes a valid form. func (obj *StructLookupOptionalFunc) Validate() error { + if !obj.built { + return fmt.Errorf("function wasn't built yet") + } if obj.Type == nil { // build must be run first return fmt.Errorf("type is still unspecified") } @@ -470,24 +219,21 @@ func (obj *StructLookupOptionalFunc) Validate() error { // Info returns some static info about itself. Build must be called before this // will return correct data. func (obj *StructLookupOptionalFunc) Info() *interfaces.Info { + // Since this function implements FuncInfer we want sig to return nil to + // avoid an accidental return of unification variables when we should be + // getting them from FuncInfer, and not from here. (During unification!) var sig *types.Type - if obj.Type != nil { // don't panic if called speculatively - // TODO: can obj.Out be nil (a partial) ? + if obj.built { sig = obj.sig() // helper } return &interfaces.Info{ Pure: true, Memo: false, - Sig: sig, // func kind + Sig: sig, Err: obj.Validate(), } } -// helper -func (obj *StructLookupOptionalFunc) sig() *types.Type { - return types.NewType(fmt.Sprintf("func(%s %s, %s str, %s %s) %s", structLookupOptionalArgNameStruct, obj.Type.String(), structLookupOptionalArgNameField, structLookupOptionalArgNameOptional, obj.Out.String(), obj.Out.String())) -} - // Init runs some startup code for this function. func (obj *StructLookupOptionalFunc) Init(init *interfaces.Init) error { obj.init = init @@ -520,6 +266,7 @@ func (obj *StructLookupOptionalFunc) Stream(ctx context.Context) error { return fmt.Errorf("received empty field") } if obj.field == "" { + // This can happen at compile time too. Bonus! obj.field = field // store first field } if field != obj.field { @@ -531,7 +278,7 @@ func (obj *StructLookupOptionalFunc) Stream(ctx context.Context) error { // here anyways. Maybe one day there will be a fancy // reason why this might vary over time. var result types.Value - val, exists := st.Lookup(field) + val, exists := st.Lookup(obj.field) if exists { result = val } else { diff --git a/lang/funcs/structs/util.go b/lang/funcs/structs/util.go index cf8b34ae..4e7ace54 100644 --- a/lang/funcs/structs/util.go +++ b/lang/funcs/structs/util.go @@ -30,7 +30,7 @@ package structs import ( - "github.com/purpleidea/mgmt/lang/funcs/simple" + "github.com/purpleidea/mgmt/lang/funcs/wrapped" "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/types" "github.com/purpleidea/mgmt/lang/types/full" @@ -53,7 +53,7 @@ func FuncValueToConstFunc(fv *full.FuncValue) interfaces.Func { // SimpleFnToDirectFunc transforms a name and *types.FuncValue into an // interfaces.Func which is implemented by &simple.WrappedFunc{}. func SimpleFnToDirectFunc(name string, fv *types.FuncValue) interfaces.Func { - return &simple.WrappedFunc{ + return &wrapped.Func{ Name: name, Fn: fv, } diff --git a/lang/funcs/wrapped/wrapped.go b/lang/funcs/wrapped/wrapped.go new file mode 100644 index 00000000..31486c6e --- /dev/null +++ b/lang/funcs/wrapped/wrapped.go @@ -0,0 +1,182 @@ +// Mgmt +// Copyright (C) 2013-2024+ 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 . +// +// Additional permission under GNU GPL version 3 section 7 +// +// If you modify this program, or any covered work, by linking or combining it +// with embedded mcl code and modules (and that the embedded mcl code and +// modules which link with this program, contain a copy of their source code in +// the authoritative form) containing parts covered by the terms of any other +// license, the licensors of this program grant you additional permission to +// convey the resulting work. Furthermore, the licensors of this program grant +// the original author, James Shubin, additional permission to update this +// additional permission if he deems it necessary to achieve the goals of this +// additional permission. + +package wrapped + +import ( + "context" + "fmt" + + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/lang/types" + "github.com/purpleidea/mgmt/util/errwrap" +) + +var _ interfaces.Func = &Func{} // ensure it meets this expectation + +// Func is a wrapped scaffolding function struct which fulfills the boiler-plate +// for the function API, but that can run a very simple, static, pure, function. +// It can be wrapped by other structs that support polymorphism in various ways. +type Func struct { + // Name is a unique string name for the function. + Name string + + // Type is the type of the function. It can include unification + // variables when this struct is wrapped in one that can build this out. + Type *types.Type + + // Fn is the concrete version of our chosen function. + Fn *types.FuncValue + + 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 *Func) String() string { + return fmt.Sprintf("%s @ %p", obj.Name, obj) // be more unique! +} + +// ArgGen returns the Nth arg name for this function. +func (obj *Func) ArgGen(index int) (string, error) { + // If the user specified just a ?1 here, then this might panic if we + // wanted to determine the arg length at compile time. + seq := obj.Type.Ord + if l := len(seq); index >= l { + return "", fmt.Errorf("index %d exceeds arg length of %d", index, l) + } + return seq[index], nil +} + +// Validate makes sure we've built our struct properly. It is usually unused for +// normal functions that users can use directly. +func (obj *Func) Validate() error { + if obj.Fn == nil { // build must be run first + return fmt.Errorf("func has not been built") + } + if obj.Fn.T == nil { + return fmt.Errorf("func type must not be nil") + } + 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 *Func) Info() *interfaces.Info { + var typ *types.Type + // For speculation we still need to return a type with unification vars. + if obj.Type != nil { // && !obj.Type.HasUni() // always return something + typ = obj.Type + } + if obj.Fn != nil { // don't panic if called speculatively + typ = obj.Fn.Type() + } + + return &interfaces.Info{ + Pure: true, + Memo: false, // TODO: should this be something we specify here? + Sig: typ, + Err: obj.Validate(), + } +} + +// Init runs some startup code for this function. +func (obj *Func) Init(init *interfaces.Init) error { + obj.init = init + return nil +} + +// Stream returns the changing values that this func has over time. +func (obj *Func) Stream(ctx context.Context) 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(ctx, values) // (Value, error) + if err != nil { + if obj.init.Debug { + obj.init.Logf("Function returned error: %+v", err) + } + return errwrap.Wrapf(err, "wrapped function errored") + } + if obj.init.Debug { + obj.init.Logf("Function returned with: %+v", result) + } + + // TODO: do we want obj.result to be a pointer instead? + if obj.result == result { + continue // result didn't change + } + obj.result = result // store new result + + case <-ctx.Done(): + 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 <-ctx.Done(): + return nil + } + } +} diff --git a/lang/gapi/gapi.go b/lang/gapi/gapi.go index 3a12733b..8ce3bdf4 100644 --- a/lang/gapi/gapi.go +++ b/lang/gapi/gapi.go @@ -277,9 +277,7 @@ func (obj *GAPI) Cli(info *gapi.Info) (*gapi.Deploy, error) { if !args.SkipUnify { // apply type unification unificationLogf := func(format string, v ...interface{}) { - if debug { // unification only has debug messages... - logf("unification: "+format, v...) - } + logf("unification: "+format, v...) } logf("running type unification...") diff --git a/lang/interfaces/ast.go b/lang/interfaces/ast.go index 5d829e59..929f54d5 100644 --- a/lang/interfaces/ast.go +++ b/lang/interfaces/ast.go @@ -79,10 +79,11 @@ type Stmt interface { // SetScope sets the scope here and propagates it downwards. SetScope(*Scope) error - // Unify returns the list of invariants that this node produces. It does - // so recursively on any children elements that exist in the AST, and - // returns the collection to the caller. - Unify() ([]Invariant, error) + // TypeCheck returns the list of invariants that this node produces. It + // does so recursively on any children elements that exist in the AST, + // and returns the collection to the caller. It calls TypeCheck for + // child statements, and Infer/Check for child expressions. + TypeCheck() ([]*UnificationInvariant, error) // Graph returns the reactive function graph expressed by this node. Graph() (*pgraph.Graph, error) @@ -128,10 +129,21 @@ type Expr interface { // determine it statically. This errors if it is not yet known. Type() (*types.Type, error) - // Unify returns the list of invariants that this node produces. It does - // so recursively on any children elements that exist in the AST, and - // returns the collection to the caller. - Unify() ([]Invariant, error) + // Infer returns the type of itself and a collection of invariants. The + // returned type may contain unification variables. It collects the + // invariants by calling Check on its children expressions. In making + // those calls, it passes in the known type for that child to get it to + // "Check" it. When the type is not known, it should create a new + // unification variable to pass in to the child Check calls. Infer + // usually only calls Check on things inside of it, and often does not + // call another Infer. + Infer() (*types.Type, []*UnificationInvariant, error) + + // Check is checking that the input type is equal to the object that + // Check is running on. In doing so, it adds any invariants that are + // necessary. Check must always call Infer to produce the invariant. The + // implementation can be generic for all expressions. + Check(typ *types.Type) ([]*UnificationInvariant, error) // Graph returns the reactive function graph expressed by this node. It // takes in the environment of any functions in scope. It also returns diff --git a/lang/interfaces/func.go b/lang/interfaces/func.go index 2e26cbf8..d4ef32fc 100644 --- a/lang/interfaces/func.go +++ b/lang/interfaces/func.go @@ -41,7 +41,10 @@ import ( ) // FuncSig is the simple signature that is used throughout our implementations. -type FuncSig = func([]types.Value) (types.Value, error) +type FuncSig = func(context.Context, []types.Value) (types.Value, error) + +// Compile-time guarantee that *types.FuncValue accepts a func of type FuncSig. +var _ = &types.FuncValue{V: FuncSig(nil)} // Info is a static representation of some information about the function. It is // used for static analysis and type checking. If you break this contract, you @@ -90,14 +93,16 @@ type Init struct { type Func interface { fmt.Stringer // so that this can be stored as a Vertex - Validate() error // FIXME: this is only needed for PolyFunc. Get it moved and used! + // Validate ensures that our struct implementing this function was built + // correctly. + Validate() error // Info returns some information about the function in question, which // includes the function signature. For a polymorphic function, this // might not be known until after Build was called. As a result, the - // sig should be allowed to return a partial or variant type if it is - // not known yet. This is because the Info method might be called - // speculatively to aid in type unification. + // sig should be allowed to return a type that includes unification + // variables if it is not known yet. This is because the Info method + // might be called speculatively to aid in type unification elsewhere. Info() *Info // Init passes some important values and references to the function. @@ -113,9 +118,11 @@ type Func interface { Stream(context.Context) error } -// PolyFunc is an interface for functions which are statically polymorphic. In -// other words, they are functions which before compile time are polymorphic, -// but after a successful compilation have a fixed static signature. This makes +// BuildableFunc is an interface for functions which need a Build or Check step. +// These functions need that method called after type unification to either tell +// them the precise type, and/or Check if it's a valid solution. These functions +// are usually polymorphic before compile time. After a successful compilation, +// every function include these, must have a fixed static signature. This makes // implementing what would appear to be generic or polymorphic instead something // that is actually static and that still has the language safety properties. // Our engine requires that by the end of compilation, everything is static. @@ -123,30 +130,13 @@ type Func interface { // their execution. If the types could change, then we wouldn't be able to // safely pass values around. // -// NOTE: This interface is similar to OldPolyFunc, except that it uses a Unify -// method that works differently than the original Polymorphisms method. This -// allows us to build invariants that are used directly by the type unification -// solver. -type PolyFunc interface { +// NOTE: This interface doesn't require any Infer/Check methods because simple +// polymorphism can be achieved by having a type signature that contains +// unification variables. Variants that require fancier extensions can implement +// the InferableFunc interface as well. +type BuildableFunc interface { Func // implement everything in Func but add the additional requirements - // Unify returns the list of invariants that this func produces. It is a - // way for a polymorphic function to describe its type requirements. It - // would be expected for this function to return at least one - // ExclusiveInvariant or GeneratorInvariant, since these are two common - // mechanisms for polymorphic functions to describe their constraints. - // The important realization behind this method is that the collecting - // of possible invariants, must happen *before* the solver runs so that - // the solver can look at all the available logic *simultaneously* to - // find a solution if we want to be able to reliably solve for things. - // The input argument that it receives is the expression pointer that it - // is unifying against-- in other words, the pointer is its own handle. - // This is different than the `obj` reference of this function - // implementation because _that_ handle is not the object/pointer in the - // AST that we're discussing when performing type unification. Put - // another way: the Expr input is the ExprFunc, not the ExprCall. - Unify(Expr) ([]Invariant, error) - // Build takes the known or unified type signature for this function and // finalizes this structure so that it is now determined, and ready to // function as a normal function would. (The normal methods in the Func @@ -159,52 +149,80 @@ type PolyFunc interface { // will use. These are used when constructing the function graphs. This // means that when this is called from SetType, it can set the correct // type arg names, and this will also match what's in function Info(). + // This can also be used as a "check" method to make sure that the + // unification result for this function is one of the valid + // possibilities. This can happen if the specified unification variables + // do not guarantee a valid type. (For example: the sig for the len() + // function is `func(?1) int`, but we can't build the function if ?1 is + // an int or a float. That is checked during Build. Build(*types.Type) (*types.Type, error) } -// OldPolyFunc is an interface for functions which are statically polymorphic. -// In other words, they are functions which before compile time are polymorphic, -// but after a successful compilation have a fixed static signature. This makes -// implementing what would appear to be generic or polymorphic instead something -// that is actually static and that still has the language safety properties. -type OldPolyFunc interface { +// InferableFunc is an interface which extends the BuildableFunc interface by +// adding a new function that can give the user more control over how function +// inference runs. This allows the user to return more precise information for +// type unification from compile-time information, than would otherwise be +// possible. +// +// NOTE: This is the third iteration of this interface which is now incredibly +// well-polished. +type InferableFunc interface { // TODO: Is there a better name for this? + BuildableFunc // includes Build and the base Func stuff... + + // FuncInfer returns the type and the list of invariants that this func + // produces. That type may include unification variables. This is a + // fancy way for a polymorphic function to describe its type + // requirements. It uses compile-time information to help it build the + // correct signature and constraints. This compile time information is + // passed into this method as a list of partial "hints" that take the + // form of a (possible partial) function type signature (with as many + // types in it specified and the rest set to nil) and any known static + // values for the input args. If the partial type is not nil, then the + // Ord parameter must be of the correct arg length. If any types are + // specified, then the array of partial values must be of that length as + // well, with the known ones filled in. Some static polymorphic + // functions require a minimal amount of hinting or they will be unable + // to return any possible unambiguous result. Remember that your result + // can include unification variables, but it should not be a standalone + // ?1 variable. It should at the minimum be of the form `func(?1) ?2`. + // Since this is almost always called by an ExprCall when building + // invariants for type unification, we'll know the precise number of + // args the function is being called with, so you can use this + // information to more correctly discern the correct function you want + // to build. The arg names in your returned func type signatures can be + // in the standardized "a..b..c" format. Use util.NumToAlpha if you want + // to convert easily. These arg names will be replaced by the correct + // ones during the Build step. All of these features and limitations are + // this way so that we can use the standard Union-Fund type unification + // algorithm which runs fairly quickly. + // TODO: Do we ever need to return any invariants? + FuncInfer(partialType *types.Type, partialValues []types.Value) (*types.Type, []*UnificationInvariant, error) +} + +// CopyableFunc is an interface which extends the base Func interface with the +// ability to let our compiler know how to copy a Func if that func deems it's +// needed to be able to do so. +type CopyableFunc interface { Func // implement everything in Func but add the additional requirements - // Polymorphisms returns a list of possible function type signatures. It - // takes as input a list of partial "hints" as to limit the number of - // possible results it returns. These partial hints take the form of a - // function type signature (with as many types in it specified and the - // rest set to nil) and any known static values for the input args. If - // the partial type is not nil, then the Ord parameter must be of the - // correct arg length. If any types are specified, then the array must - // be of that length as well, with the known ones filled in. Some - // static polymorphic functions require a minimal amount of hinting or - // they will be unable to return any possible result that is not - // infinite in length. If you expect to need to return an infinite (or - // very large) amount of results, then you should return an error - // instead. The arg names in your returned func type signatures should - // be in the standardized "a..b..c" format. Use util.NumToAlpha if you - // want to convert easily. - Polymorphisms(*types.Type, []types.Value) ([]*types.Type, error) - - // Build takes the known or unified type signature for this function and - // finalizes this structure so that it is now determined, and ready to - // function as a normal function would. (The normal methods in the Func - // interface are all that should be needed or used after this point.) - // Of note, the names of the specific input args shouldn't matter as - // long as they are unique. Their position doesn't matter. This is so - // that unification can use "arg0", "arg1", "argN"... if they can't be - // determined statically. Build can transform them into it's desired - // form, and must return the type (with the correct arg names) that it - // will use. These are used when constructing the function graphs. This - // means that when this is called from SetType, it can set the correct - // type arg names, and this will also match what's in function Info(). - Build(*types.Type) (*types.Type, error) + // Copy is used because we sometimes copy the ExprFunc with its Copy + // method because we're using the same ExprFunc in two places, and it + // might have a different type and type unification needs to solve for + // it in more than one way. It also turns out that some functions such + // as the struct lookup function store information that they learned + // during `FuncInfer`, and as a result, if we re-build this, then we + // lose that information and the function can then fail during `Build`. + // As a result, those functions can implement a `Copy` method which we + // will use instead, so they can preserve any internal state that they + // would like to keep. + Copy() Func } // NamedArgsFunc is a function that uses non-standard function arg names. If you // don't implement this, then the argnames (if specified) must correspond to the // a, b, c...z, aa, ab...az, ba...bz, and so on sequence. +// XXX: I expect that we can get rid of this since type unification doesn't care +// what the arguments are named, and at the end, we get them from Info or Build. type NamedArgsFunc interface { Func // implement everything in Func but add the additional requirements diff --git a/lang/interfaces/structs.go b/lang/interfaces/structs.go deleted file mode 100644 index fc3f62f1..00000000 --- a/lang/interfaces/structs.go +++ /dev/null @@ -1,206 +0,0 @@ -// Mgmt -// Copyright (C) 2013-2024+ 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 . -// -// Additional permission under GNU GPL version 3 section 7 -// -// If you modify this program, or any covered work, by linking or combining it -// with embedded mcl code and modules (and that the embedded mcl code and -// modules which link with this program, contain a copy of their source code in -// the authoritative form) containing parts covered by the terms of any other -// license, the licensors of this program grant you additional permission to -// convey the resulting work. Furthermore, the licensors of this program grant -// the original author, James Shubin, additional permission to update this -// additional permission if he deems it necessary to achieve the goals of this -// additional permission. - -package interfaces - -import ( - "fmt" - - "github.com/purpleidea/mgmt/lang/types" - "github.com/purpleidea/mgmt/pgraph" -) - -// ExprAny is a placeholder expression that is used for type unification hacks. -type ExprAny struct { - typ *types.Type - - V types.Value // stored value (set with SetValue) -} - -// String returns a short representation of this expression. -func (obj *ExprAny) String() string { return "any" } - -// Apply is a general purpose iterator method that operates on any AST node. It -// is not used as the primary AST traversal function because it is less readable -// and easy to reason about than manually implementing traversal for each node. -// Nevertheless, it is a useful facility for operations that might only apply to -// a select number of node types, since they won't need extra noop iterators... -func (obj *ExprAny) Apply(fn func(Node) error) error { return fn(obj) } - -// Init initializes this branch of the AST, and returns an error if it fails to -// validate. -func (obj *ExprAny) Init(*Data) error { return nil } - -// Interpolate returns a new node (aka a copy) once it has been expanded. This -// generally increases the size of the AST when it is used. It calls Interpolate -// on any child elements and builds the new node with those new node contents. -// Here it simply returns itself, as no interpolation is possible. -func (obj *ExprAny) Interpolate() (Expr, error) { - return &ExprAny{ - typ: obj.typ, - V: obj.V, - }, nil -} - -// Copy returns a light copy of this struct. Anything static will not be copied. -func (obj *ExprAny) Copy() (Expr, error) { - return obj, nil // always static -} - -// Ordering returns a graph of the scope ordering that represents the data flow. -// This can be used in SetScope so that it knows the correct order to run it in. -func (obj *ExprAny) Ordering(produces map[string]Node) (*pgraph.Graph, map[Node]string, error) { - graph, err := pgraph.NewGraph("ordering") - if err != nil { - return nil, nil, err - } - graph.AddVertex(obj) - - cons := make(map[Node]string) - return graph, cons, nil -} - -// SetScope does nothing for this struct, because it has no child nodes, and it -// does not need to know about the parent scope. -func (obj *ExprAny) SetScope(*Scope, map[string]Expr) error { return nil } - -// SetType is used to set the type of this expression once it is known. This -// usually happens during type unification, but it can also happen during -// parsing if a type is specified explicitly. Since types are static and don't -// change on expressions, if you attempt to set a different type than what has -// previously been set (when not initially known) this will error. -func (obj *ExprAny) SetType(typ *types.Type) error { - if obj.typ != nil { - return obj.typ.Cmp(typ) // if not set, ensure it doesn't change - } - if obj.V != nil { - // if there's a value already, ensure the types are the same... - if err := obj.V.Type().Cmp(typ); err != nil { - return err - } - } - obj.typ = typ // set - return nil -} - -// Type returns the type of this expression. -func (obj *ExprAny) Type() (*types.Type, error) { - if obj.typ == nil && obj.V == nil { - return nil, ErrTypeCurrentlyUnknown - } - if obj.typ != nil && obj.V != nil { - if err := obj.V.Type().Cmp(obj.typ); err != nil { - return nil, err - } - return obj.typ, nil - } - if obj.V != nil { - return obj.V.Type(), nil - } - return obj.typ, nil -} - -// Unify returns the list of invariants that this node produces. It recursively -// calls Unify on any children elements that exist in the AST, and returns the -// collection to the caller. -func (obj *ExprAny) Unify() ([]Invariant, error) { - invariants := []Invariant{ - &AnyInvariant{ // it has to be something, anything! - Expr: obj, - }, - } - // TODO: should we return an EqualsInvariant with obj.typ ? - // TODO: should we return a ValueInvariant with obj.V ? - return invariants, nil -} - -// Func returns the reactive stream of values that this expression produces. -func (obj *ExprAny) Func() (Func, error) { - // // XXX: this could be a list too, so improve this code or improve the subgraph code... - // return &structs.ConstFunc{ - // Value: obj.V, - // } - - return nil, fmt.Errorf("programming error using ExprAny") // this should not be called -} - -// Graph returns the reactive function graph which is expressed by this node. It -// includes any vertices produced by this node, and the appropriate edges to any -// vertices that are produced by its children. Nodes which fulfill the Expr -// interface directly produce vertices (and possible children) where as nodes -// that fulfill the Stmt interface do not produces vertices, where as their -// children might. This returns a graph with a single vertex (itself) in it, and -// the edges from all of the child graphs to this. -func (obj *ExprAny) Graph(env map[string]Func) (*pgraph.Graph, Func, error) { - graph, err := pgraph.NewGraph("any") - if err != nil { - return nil, nil, err - } - function, err := obj.Func() - if err != nil { - return nil, nil, err - } - graph.AddVertex(function) - - return graph, function, nil -} - -// SetValue here is a no-op, because algorithmically when this is called from -// the func engine, the child elements (the list elements) will have had this -// done to them first, and as such when we try and retrieve the set value from -// this expression by calling `Value`, it will build it from scratch! - -// SetValue here is used to store a value for this expression node. This value -// is cached and can be retrieved by calling Value. -func (obj *ExprAny) SetValue(value types.Value) error { - typ := value.Type() - if obj.typ != nil { - if err := obj.typ.Cmp(typ); err != nil { - return err - } - } - obj.typ = typ - obj.V = value - return nil -} - -// Value returns the value of this expression in our type system. This will -// usually only be valid once the engine has run and values have been produced. -// This might get called speculatively (early) during unification to learn more. -func (obj *ExprAny) Value() (types.Value, error) { - if obj.V == nil { - return nil, fmt.Errorf("value is not set") - } - return obj.V, nil -} - -// ScopeGraph adds nodes and vertices to the supplied graph. -func (obj *ExprAny) ScopeGraph(g *pgraph.Graph) { - g.AddVertex(obj) -} diff --git a/lang/interfaces/unification.go b/lang/interfaces/unification.go index cab0a16d..59bd854a 100644 --- a/lang/interfaces/unification.go +++ b/lang/interfaces/unification.go @@ -30,11 +30,7 @@ package interfaces import ( - "fmt" - "strings" - "github.com/purpleidea/mgmt/lang/types" - "github.com/purpleidea/mgmt/util/errwrap" ) // UnificationInvariant is the only type of invariant that we currently support. @@ -76,1061 +72,3 @@ func GenericCheck(obj Expr, typ *types.Type) ([]*UnificationInvariant, error) { return invariants, nil } - -// Invariant represents a constraint that is described by the Expr's and Stmt's, -// and which is passed into the unification solver to describe what is known by -// the AST. -// XXX: add the extended methods into sub-interfaces since not each invariant -// uses them... -type Invariant interface { - // TODO: should we add any other methods to this type? - fmt.Stringer - - // ExprList returns the list of valid expressions in this invariant. - ExprList() []Expr - - // Matches returns whether an invariant matches the existing solution. - // If it is inconsistent, then it errors. - Matches(solved map[Expr]*types.Type) (bool, error) - - // Possible returns an error if it is certain that it is NOT possible to - // get a solution with this invariant and the set of partials. In - // certain cases, it might not be able to determine that it's not - // possible, while simultaneously not being able to guarantee a possible - // solution either. In this situation, it should return nil, since this - // is used as a filtering mechanism, and the nil result of possible is - // preferred over eliminating a tricky, but possible one. - Possible(partials []Invariant) error -} - -// EqualsInvariant is an invariant that symbolizes that the expression has a -// known type. -// TODO: is there a better name than EqualsInvariant -type EqualsInvariant struct { - Expr Expr - Type *types.Type -} - -// String returns a representation of this invariant. -func (obj *EqualsInvariant) String() string { - return fmt.Sprintf("%p == %s", obj.Expr, obj.Type) -} - -// ExprList returns the list of valid expressions in this invariant. -func (obj *EqualsInvariant) ExprList() []Expr { - return []Expr{obj.Expr} -} - -// Matches returns whether an invariant matches the existing solution. If it is -// inconsistent, then it errors. -func (obj *EqualsInvariant) Matches(solved map[Expr]*types.Type) (bool, error) { - typ, exists := solved[obj.Expr] - if !exists { - return false, nil - } - if err := typ.Cmp(obj.Type); err != nil { - return false, err - } - return true, nil -} - -// Possible returns an error if it is certain that it is NOT possible to get a -// solution with this invariant and the set of partials. In certain cases, it -// might not be able to determine that it's not possible, while simultaneously -// not being able to guarantee a possible solution either. In this situation, it -// should return nil, since this is used as a filtering mechanism, and the nil -// result of possible is preferred over eliminating a tricky, but possible one. -func (obj *EqualsInvariant) Possible(partials []Invariant) error { - // TODO: we could pass in a solver here - //set := []Invariant{} - //set = append(set, obj) - //set = append(set, partials...) - //_, err := SimpleInvariantSolver(set, ...) - //if err != nil { - // // being ambiguous doesn't guarantee that we're possible - // if err == ErrAmbiguous { - // return nil // might be possible, might not be... - // } - // return err - //} - - // FIXME: This is not right because we want to know if the whole thing - // works together, and as a result, the above solver is better, however, - // the goal is to eliminate easy impossible solutions, so allow this! - // XXX: Double check this is logical. - solved := map[Expr]*types.Type{ - obj.Expr: obj.Type, - } - for _, invar := range partials { // check each one - _, err := invar.Matches(solved) - if err != nil { // inconsistent, so it's not possible - return errwrap.Wrapf(err, "not possible") - } - } - - return nil -} - -// EqualityInvariant is an invariant that symbolizes that the two expressions -// must have equivalent types. -// TODO: is there a better name than EqualityInvariant -type EqualityInvariant struct { - Expr1 Expr - Expr2 Expr -} - -// String returns a representation of this invariant. -func (obj *EqualityInvariant) String() string { - return fmt.Sprintf("%p == %p", obj.Expr1, obj.Expr2) -} - -// ExprList returns the list of valid expressions in this invariant. -func (obj *EqualityInvariant) ExprList() []Expr { - return []Expr{obj.Expr1, obj.Expr2} -} - -// Matches returns whether an invariant matches the existing solution. If it is -// inconsistent, then it errors. -func (obj *EqualityInvariant) Matches(solved map[Expr]*types.Type) (bool, error) { - t1, exists1 := solved[obj.Expr1] - t2, exists2 := solved[obj.Expr2] - if !exists1 || !exists2 { - return false, nil // not matched yet - } - if err := t1.Cmp(t2); err != nil { - return false, err - } - - return true, nil // matched! -} - -// Possible returns an error if it is certain that it is NOT possible to get a -// solution with this invariant and the set of partials. In certain cases, it -// might not be able to determine that it's not possible, while simultaneously -// not being able to guarantee a possible solution either. In this situation, it -// should return nil, since this is used as a filtering mechanism, and the nil -// result of possible is preferred over eliminating a tricky, but possible one. -func (obj *EqualityInvariant) Possible(partials []Invariant) error { - // The idea here is that we look for the expression pointers in the list - // of partial invariants. It's only impossible if we (1) find both of - // them, and (2) that they relate to each other. The second part is - // harder. - var one, two bool - exprs := []Invariant{} - for _, x := range partials { - for _, y := range x.ExprList() { // []Expr - if y == obj.Expr1 { - one = true - exprs = append(exprs, x) - } - if y == obj.Expr2 { - two = true - exprs = append(exprs, x) - } - } - } - - if !one || !two { - return nil // we're unconnected to anything, this is possible! - } - - // we only need to check the connections in this case... - // let's keep this simple, and less perfect for now... - var typ *types.Type - for _, x := range exprs { - eq, ok := x.(*EqualsInvariant) - if !ok { - // XXX: add support for other kinds in the future... - continue - } - - if typ != nil { - if err := typ.Cmp(eq.Type); err != nil { - // we found proof it's not possible - return errwrap.Wrapf(err, "not possible") - } - } - - typ = eq.Type // store for next type - } - - return nil -} - -// EqualityInvariantList is an invariant that symbolizes that all the -// expressions listed must have equivalent types. -type EqualityInvariantList struct { - Exprs []Expr -} - -// String returns a representation of this invariant. -func (obj *EqualityInvariantList) String() string { - var a []string - for _, x := range obj.Exprs { - a = append(a, fmt.Sprintf("%p", x)) - } - return fmt.Sprintf("[%s]", strings.Join(a, ", ")) -} - -// ExprList returns the list of valid expressions in this invariant. -func (obj *EqualityInvariantList) ExprList() []Expr { - return obj.Exprs -} - -// Matches returns whether an invariant matches the existing solution. If it is -// inconsistent, then it errors. -func (obj *EqualityInvariantList) Matches(solved map[Expr]*types.Type) (bool, error) { - found := true // assume true - var typ *types.Type - for _, x := range obj.Exprs { - t, exists := solved[x] - if !exists { - found = false - continue - } - if typ == nil { // set the first time - typ = t - } - if err := typ.Cmp(t); err != nil { - return false, err - } - } - return found, nil -} - -// Possible returns an error if it is certain that it is NOT possible to get a -// solution with this invariant and the set of partials. In certain cases, it -// might not be able to determine that it's not possible, while simultaneously -// not being able to guarantee a possible solution either. In this situation, it -// should return nil, since this is used as a filtering mechanism, and the nil -// result of possible is preferred over eliminating a tricky, but possible one. -func (obj *EqualityInvariantList) Possible(partials []Invariant) error { - // The idea here is that we look for the expression pointers in the list - // of partial invariants. It's only impossible if we (1) find two or - // more, and (2) that any of them relate to each other. The second part - // is harder. - inList := func(needle Expr, haystack []Expr) bool { - for _, x := range haystack { - if x == needle { - return true - } - } - return false - } - - exprs := []Invariant{} - for _, x := range partials { - for _, y := range x.ExprList() { // []Expr - if inList(y, obj.Exprs) { - exprs = append(exprs, x) - } - } - } - - if len(exprs) <= 1 { - return nil // we're unconnected to anything, this is possible! - } - - // we only need to check the connections in this case... - // let's keep this simple, and less perfect for now... - var typ *types.Type - for _, x := range exprs { - eq, ok := x.(*EqualsInvariant) - if !ok { - // XXX: add support for other kinds in the future... - continue - } - - if typ != nil { - if err := typ.Cmp(eq.Type); err != nil { - // we found proof it's not possible - return errwrap.Wrapf(err, "not possible") - } - } - - typ = eq.Type // store for next type - } - - return nil -} - -// EqualityWrapListInvariant expresses that a list in Expr1 must have elements -// that have the same type as the expression in Expr2Val. -type EqualityWrapListInvariant struct { - Expr1 Expr - Expr2Val Expr -} - -// String returns a representation of this invariant. -func (obj *EqualityWrapListInvariant) String() string { - return fmt.Sprintf("%p == [%p]", obj.Expr1, obj.Expr2Val) -} - -// ExprList returns the list of valid expressions in this invariant. -func (obj *EqualityWrapListInvariant) ExprList() []Expr { - return []Expr{obj.Expr1, obj.Expr2Val} -} - -// Matches returns whether an invariant matches the existing solution. If it is -// inconsistent, then it errors. -func (obj *EqualityWrapListInvariant) Matches(solved map[Expr]*types.Type) (bool, error) { - t1, exists1 := solved[obj.Expr1] // list type - t2, exists2 := solved[obj.Expr2Val] - if !exists1 || !exists2 { - return false, nil // not matched yet - } - if t1.Kind != types.KindList { - return false, fmt.Errorf("expected list kind") - } - if err := t1.Val.Cmp(t2); err != nil { - return false, err // inconsistent! - } - return true, nil // matched! -} - -// Possible returns an error if it is certain that it is NOT possible to get a -// solution with this invariant and the set of partials. In certain cases, it -// might not be able to determine that it's not possible, while simultaneously -// not being able to guarantee a possible solution either. In this situation, it -// should return nil, since this is used as a filtering mechanism, and the nil -// result of possible is preferred over eliminating a tricky, but possible one. -// This particular implementation is currently not implemented! -func (obj *EqualityWrapListInvariant) Possible(partials []Invariant) error { - // XXX: not implemented - return nil // safer to return nil than error -} - -// EqualityWrapMapInvariant expresses that a map in Expr1 must have keys that -// match the type of the expression in Expr2Key and values that match the type -// of the expression in Expr2Val. -type EqualityWrapMapInvariant struct { - Expr1 Expr - Expr2Key Expr - Expr2Val Expr -} - -// String returns a representation of this invariant. -func (obj *EqualityWrapMapInvariant) String() string { - return fmt.Sprintf("%p == {%p: %p}", obj.Expr1, obj.Expr2Key, obj.Expr2Val) -} - -// ExprList returns the list of valid expressions in this invariant. -func (obj *EqualityWrapMapInvariant) ExprList() []Expr { - return []Expr{obj.Expr1, obj.Expr2Key, obj.Expr2Val} -} - -// Matches returns whether an invariant matches the existing solution. If it is -// inconsistent, then it errors. -func (obj *EqualityWrapMapInvariant) Matches(solved map[Expr]*types.Type) (bool, error) { - t1, exists1 := solved[obj.Expr1] // map type - t2, exists2 := solved[obj.Expr2Key] - t3, exists3 := solved[obj.Expr2Val] - if !exists1 || !exists2 || !exists3 { - return false, nil // not matched yet - } - if t1.Kind != types.KindMap { - return false, fmt.Errorf("expected map kind") - } - if err := t1.Key.Cmp(t2); err != nil { - return false, err // inconsistent! - } - if err := t1.Val.Cmp(t3); err != nil { - return false, err // inconsistent! - } - return true, nil // matched! -} - -// Possible returns an error if it is certain that it is NOT possible to get a -// solution with this invariant and the set of partials. In certain cases, it -// might not be able to determine that it's not possible, while simultaneously -// not being able to guarantee a possible solution either. In this situation, it -// should return nil, since this is used as a filtering mechanism, and the nil -// result of possible is preferred over eliminating a tricky, but possible one. -// This particular implementation is currently not implemented! -func (obj *EqualityWrapMapInvariant) Possible(partials []Invariant) error { - // XXX: not implemented - return nil // safer to return nil than error -} - -// EqualityWrapStructInvariant expresses that a struct in Expr1 must have fields -// that match the type of the expressions listed in Expr2Map. -type EqualityWrapStructInvariant struct { - Expr1 Expr - Expr2Map map[string]Expr - Expr2Ord []string -} - -// String returns a representation of this invariant. -func (obj *EqualityWrapStructInvariant) String() string { - var s = make([]string, len(obj.Expr2Ord)) - for i, k := range obj.Expr2Ord { - t, ok := obj.Expr2Map[k] - if !ok { - panic("malformed struct order") - } - if t == nil { - panic("malformed struct field") - } - s[i] = fmt.Sprintf("%s %p", k, t) - } - return fmt.Sprintf("%p == struct{%s}", obj.Expr1, strings.Join(s, "; ")) -} - -// ExprList returns the list of valid expressions in this invariant. -func (obj *EqualityWrapStructInvariant) ExprList() []Expr { - exprs := []Expr{obj.Expr1} - for _, x := range obj.Expr2Map { - exprs = append(exprs, x) - } - return exprs -} - -// Matches returns whether an invariant matches the existing solution. If it is -// inconsistent, then it errors. -func (obj *EqualityWrapStructInvariant) Matches(solved map[Expr]*types.Type) (bool, error) { - t1, exists1 := solved[obj.Expr1] // struct type - if !exists1 { - return false, nil // not matched yet - } - if t1.Kind != types.KindStruct { - return false, fmt.Errorf("expected struct kind") - } - - found := true // assume true - for _, key := range obj.Expr2Ord { - _, exists := t1.Map[key] - if !exists { - return false, fmt.Errorf("missing invariant struct key of: `%s`", key) - } - e, exists := obj.Expr2Map[key] - if !exists { - return false, fmt.Errorf("missing matched struct key of: `%s`", key) - } - t, exists := solved[e] - if !exists { - found = false - continue - } - if err := t1.Map[key].Cmp(t); err != nil { - return false, err // inconsistent! - } - } - - return found, nil // matched! -} - -// Possible returns an error if it is certain that it is NOT possible to get a -// solution with this invariant and the set of partials. In certain cases, it -// might not be able to determine that it's not possible, while simultaneously -// not being able to guarantee a possible solution either. In this situation, it -// should return nil, since this is used as a filtering mechanism, and the nil -// result of possible is preferred over eliminating a tricky, but possible one. -// This particular implementation is currently not implemented! -func (obj *EqualityWrapStructInvariant) Possible(partials []Invariant) error { - // XXX: not implemented - return nil // safer to return nil than error -} - -// EqualityWrapFuncInvariant expresses that a func in Expr1 must have args that -// match the type of the expressions listed in Expr2Map and a return value that -// matches the type of the expression in Expr2Out. -// TODO: should this be named EqualityWrapCallInvariant or not? -type EqualityWrapFuncInvariant struct { - Expr1 Expr - Expr2Map map[string]Expr - Expr2Ord []string - Expr2Out Expr -} - -// String returns a representation of this invariant. -func (obj *EqualityWrapFuncInvariant) String() string { - var s = make([]string, len(obj.Expr2Ord)) - for i, k := range obj.Expr2Ord { - t, ok := obj.Expr2Map[k] - if !ok { - panic("malformed func order") - } - if t == nil { - panic("malformed func field") - } - s[i] = fmt.Sprintf("%s %p", k, t) - } - return fmt.Sprintf("%p == func(%s) %p", obj.Expr1, strings.Join(s, "; "), obj.Expr2Out) -} - -// ExprList returns the list of valid expressions in this invariant. -func (obj *EqualityWrapFuncInvariant) ExprList() []Expr { - exprs := []Expr{obj.Expr1} - for _, x := range obj.Expr2Map { - exprs = append(exprs, x) - } - exprs = append(exprs, obj.Expr2Out) - return exprs -} - -// Matches returns whether an invariant matches the existing solution. If it is -// inconsistent, then it errors. -func (obj *EqualityWrapFuncInvariant) Matches(solved map[Expr]*types.Type) (bool, error) { - t1, exists1 := solved[obj.Expr1] // func type - if !exists1 { - return false, nil // not matched yet - } - if t1.Kind != types.KindFunc { - return false, fmt.Errorf("expected func kind") - } - - found := true // assume true - for _, key := range obj.Expr2Ord { - _, exists := t1.Map[key] - if !exists { - return false, fmt.Errorf("missing invariant struct key of: `%s`", key) - } - e, exists := obj.Expr2Map[key] - if !exists { - return false, fmt.Errorf("missing matched struct key of: `%s`", key) - } - t, exists := solved[e] - if !exists { - found = false - continue - } - if err := t1.Map[key].Cmp(t); err != nil { - return false, err // inconsistent! - } - } - - t, exists := solved[obj.Expr2Out] - if !exists { - return false, nil - } - if err := t1.Out.Cmp(t); err != nil { - return false, err // inconsistent! - } - - return found, nil // matched! -} - -// Possible returns an error if it is certain that it is NOT possible to get a -// solution with this invariant and the set of partials. In certain cases, it -// might not be able to determine that it's not possible, while simultaneously -// not being able to guarantee a possible solution either. In this situation, it -// should return nil, since this is used as a filtering mechanism, and the nil -// result of possible is preferred over eliminating a tricky, but possible one. -// This particular implementation is currently not implemented! -func (obj *EqualityWrapFuncInvariant) Possible(partials []Invariant) error { - // XXX: not implemented - return nil // safer to return nil than error -} - -// EqualityWrapCallInvariant expresses that a call result that happened in Expr1 -// must match the type of the function result listed in Expr2. In this case, -// Expr2 will be a function expression, and the returned expression should match -// with the Expr1 expression, when comparing types. -// TODO: should this be named EqualityWrapFuncInvariant or not? -// TODO: should Expr1 and Expr2 be reversed??? -type EqualityWrapCallInvariant struct { - Expr1 Expr - Expr2Func Expr -} - -// String returns a representation of this invariant. -func (obj *EqualityWrapCallInvariant) String() string { - return fmt.Sprintf("%p == call(%p)", obj.Expr1, obj.Expr2Func) -} - -// ExprList returns the list of valid expressions in this invariant. -func (obj *EqualityWrapCallInvariant) ExprList() []Expr { - return []Expr{obj.Expr1, obj.Expr2Func} -} - -// Matches returns whether an invariant matches the existing solution. If it is -// inconsistent, then it errors. -func (obj *EqualityWrapCallInvariant) Matches(solved map[Expr]*types.Type) (bool, error) { - t1, exists1 := solved[obj.Expr1] // call type - t2, exists2 := solved[obj.Expr2Func] - if !exists1 || !exists2 { - return false, nil // not matched yet - } - //if t1.Kind != types.KindFunc { - // return false, fmt.Errorf("expected func kind") - //} - - if t2.Kind != types.KindFunc { - return false, fmt.Errorf("expected func kind") - } - if err := t1.Cmp(t2.Out); err != nil { - return false, err // inconsistent! - } - return true, nil // matched! -} - -// Possible returns an error if it is certain that it is NOT possible to get a -// solution with this invariant and the set of partials. In certain cases, it -// might not be able to determine that it's not possible, while simultaneously -// not being able to guarantee a possible solution either. In this situation, it -// should return nil, since this is used as a filtering mechanism, and the nil -// result of possible is preferred over eliminating a tricky, but possible one. -// This particular implementation is currently not implemented! -func (obj *EqualityWrapCallInvariant) Possible(partials []Invariant) error { - // XXX: not implemented - return nil // safer to return nil than error -} - -// GeneratorInvariant is an experimental type of new invariant. The idea is that -// this is a special invariant that the solver knows how to use; the solver runs -// all the easy bits first, and then passes the current solution state into the -// function, and in response, it runs some user-defined code and builds some new -// invariants that are added to the solver! This is not without caveats... This -// should only be used sparingly, and with care. It can suffer from the -// confluence problem, if the generator code that was provided is incorrect. -// What this means is that it could generate different results (and a different -// final solution) depending on the order in which it is called. Since this is -// undesirable, you must only use it for straight-forward situations. As an -// extreme example, if it generated different invariants depending on the time -// of day, this would be very problematic, and evil. Alternatively, it could be -// a pure function, but that returns wildly different results depending on what -// invariants were passed in. Use it wisely. It was added to make the printf -// function (which can have an infinite number of signatures) possible to -// express in terms of "normal" invariants. Lastly, if you wanted to use this to -// add-in partial progress, you could have it generate a list of invariants and -// include a new generator invariant in this list. Be sure to only do this if -// you are making progress on each invocation, and make sure to avoid infinite -// looping which isn't something we can currently detect or prevent. One special -// bit about generators and returning a partial: you must always return the -// minimum set of expressions that need to be solved in the first Unify() call -// that also returns the very first generator. This is because you must not rely -// on the generator to tell the solver about new expressions that it *also* -// wants solved. This is because after the initial (pre-generator-running) -// collection of the invariants, we need to be able to build a list of all the -// expressions that need to be solved for us to consider the problem "done". If -// a new expression only appeared after we ran a generator, then this would -// require our solver be far more complicated than it needs to be and currently -// is. Besides, there's no reason (that I know of at the moment) that needs this -// sort of invariant that only appears after the solver is running. -// -// NOTE: We might *consider* an optimization where we return a different kind of -// error that represents a response of "impossible". This would mean that there -// is no way to reconcile the current world-view with what is know about things. -// However, it would be easier and better to just return your invariants and let -// the normal solver run its course, although future research might show that it -// could maybe help in some cases. -// XXX: solver question: Can our solver detect `expr1 == str` AND `expr1 == int` -// and fail the whole thing when we know of a case like this that is impossible? -type GeneratorInvariant struct { - // Func is a generator function that takes the state of the world, and - // returns new invariants that should be added to this world view. The - // state of the world includes both the currently unsolved invariants, - // as well as the known solution map that has been solved so far. If - // this returns nil, we add the invariants it returned and we remove it - // from the list. If we error, it's because we don't have any new - // information to provide at this time... - Func func(invariants []Invariant, solved map[Expr]*types.Type) ([]Invariant, error) - - // Inactive specifies that we tried to run this, but it didn't help us - // progress forwards. It can be reset if needed. It should only be set - // or read by the solver itself. - Inactive bool -} - -// String returns a representation of this invariant. -func (obj *GeneratorInvariant) String() string { - return fmt.Sprintf("gen(%p)", obj.Func) // TODO: improve this -} - -// ExprList returns the list of valid expressions in this invariant. -func (obj *GeneratorInvariant) ExprList() []Expr { - return []Expr{} -} - -// Matches returns whether an invariant matches the existing solution. If it is -// inconsistent, then it errors. -func (obj *GeneratorInvariant) Matches(solved map[Expr]*types.Type) (bool, error) { - // XXX: not implemented (don't panic though) - //return false, err // inconsistent! - //return false, nil // not matched yet - //return true, nil // matched! - return false, nil // not matched yet - - // If we error, it's because we don't have any new information to - // provide at this time... If it's nil, it's because the invariants - // could have worked with this solution. - //invariants, err := obj.Func(?, solved) - //if err != nil { - //} -} - -// Possible is currently not implemented! -func (obj *GeneratorInvariant) Possible(partials []Invariant) error { - // XXX: not implemented - return nil // safer to return nil than error -} - -// ConjunctionInvariant represents a list of invariants which must all be true -// together. In other words, it's a grouping construct for a set of invariants. -type ConjunctionInvariant struct { - Invariants []Invariant -} - -// String returns a representation of this invariant. -func (obj *ConjunctionInvariant) String() string { - var a []string - for _, x := range obj.Invariants { - s := x.String() - a = append(a, s) - } - return fmt.Sprintf("[%s]", strings.Join(a, ", ")) -} - -// ExprList returns the list of valid expressions in this invariant. -func (obj *ConjunctionInvariant) ExprList() []Expr { - exprs := []Expr{} - for _, x := range obj.Invariants { - exprs = append(exprs, x.ExprList()...) - } - return exprs -} - -// Matches returns whether an invariant matches the existing solution. If it is -// inconsistent, then it errors. -func (obj *ConjunctionInvariant) Matches(solved map[Expr]*types.Type) (bool, error) { - found := true // assume true - for _, invar := range obj.Invariants { - match, err := invar.Matches(solved) - if err != nil { - return false, nil - } - if !match { - found = false - } - } - return found, nil -} - -// Possible returns an error if it is certain that it is NOT possible to get a -// solution with this invariant and the set of partials. In certain cases, it -// might not be able to determine that it's not possible, while simultaneously -// not being able to guarantee a possible solution either. In this situation, it -// should return nil, since this is used as a filtering mechanism, and the nil -// result of possible is preferred over eliminating a tricky, but possible one. -// This particular implementation is currently not implemented! -func (obj *ConjunctionInvariant) Possible(partials []Invariant) error { - for _, invar := range obj.Invariants { - if err := invar.Possible(partials); err != nil { - // we found proof it's not possible - return errwrap.Wrapf(err, "not possible") - } - } - // XXX: unfortunately we didn't look for them all together with a solver - return nil -} - -// ExclusiveInvariant represents a list of invariants where one and *only* one -// should hold true. To combine multiple invariants in one of the list elements, -// you can group multiple invariants together using a ConjunctionInvariant. Do -// note that the solver might not verify that only one of the invariants in the -// list holds true, as it might choose to be lazy and pick the first solution -// found. -type ExclusiveInvariant struct { - Invariants []Invariant -} - -// String returns a representation of this invariant. -func (obj *ExclusiveInvariant) String() string { - var a []string - for _, x := range obj.Invariants { - s := x.String() - a = append(a, s) - } - return fmt.Sprintf("[%s]", strings.Join(a, ", ")) -} - -// ExprList returns the list of valid expressions in this invariant. -func (obj *ExclusiveInvariant) ExprList() []Expr { - // XXX: We should do this if we assume that exclusives don't have some - // sort of transient expr to satisfy that doesn't disappear depending on - // which choice in the exclusive is chosen... - //exprs := []Expr{} - //for _, x := range obj.Invariants { - // exprs = append(exprs, x.ExprList()...) - //} - //return exprs - // XXX: But if we ever specify an expr in this exclusive that isn't - // referenced anywhere else, then we'd need to use the above so that our - // type unification algorithm knows not to stop too early. - return []Expr{} // XXX: Do we want to the set instead? -} - -// Matches returns whether an invariant matches the existing solution. If it is -// inconsistent, then it errors. Because this partial invariant requires only -// one to be true, it will mask children errors, since it's normal for only one -// to be consistent. -func (obj *ExclusiveInvariant) Matches(solved map[Expr]*types.Type) (bool, error) { - found := false - reterr := fmt.Errorf("all exclusives errored") - var errs error - for _, invar := range obj.Invariants { - match, err := invar.Matches(solved) - if err != nil { - errs = errwrap.Append(errs, err) - continue - } - if !match { - // at least one was false, so we're not done here yet... - // we don't want to error yet, since we can't know there - // won't be a conflict once we get more data about this! - reterr = nil // clear the error - continue - } - if found { // we already found one - return false, fmt.Errorf("more than one exclusive solution") - } - found = true - } - - if found { // we got exactly one valid solution - return true, nil - } - - return false, errwrap.Wrapf(reterr, errwrap.String(errs)) -} - -// Possible returns an error if it is certain that it is NOT possible to get a -// solution with this invariant and the set of partials. In certain cases, it -// might not be able to determine that it's not possible, while simultaneously -// not being able to guarantee a possible solution either. In this situation, it -// should return nil, since this is used as a filtering mechanism, and the nil -// result of possible is preferred over eliminating a tricky, but possible one. -// This particular implementation is currently not implemented! -func (obj *ExclusiveInvariant) Possible(partials []Invariant) error { - var errs error - for _, invar := range obj.Invariants { - err := invar.Possible(partials) - if err == nil { - // we found proof it's possible - return nil - } - errs = errwrap.Append(errs, err) - } - - return errwrap.Wrapf(errs, "not possible") -} - -// Simplify attempts to reduce the exclusive invariant to eliminate any -// possibilities based on the list of known partials at this time. Hopefully, -// this will weed out some of the function polymorphism possibilities so that we -// can solve the problem without recursive, combinatorial permutation, which is -// very, very slow. -func (obj *ExclusiveInvariant) Simplify(partials []Invariant) ([]Invariant, error) { - if len(obj.Invariants) == 0 { // unexpected case - return []Invariant{}, nil // we don't need anything! - } - - possible := []Invariant{} - var reasons error - for _, invar := range obj.Invariants { // []Invariant - if err := invar.Possible(partials); err != nil { - reasons = errwrap.Append(reasons, err) - continue - } - possible = append(possible, invar) - } - - if len(possible) == 0 { // nothing was possible - return nil, errwrap.Wrapf(reasons, "no possible simplifications") - } - if len(possible) == 1 { // we flattened out the exclusive! - return possible, nil - } - - if len(possible) == len(obj.Invariants) { // nothing changed - return nil, fmt.Errorf("no possible simplifications, we're unchanged") - } - - invar := &ExclusiveInvariant{ - Invariants: possible, // hopefully a smaller exclusive! - } - return []Invariant{invar}, nil -} - -// AnyInvariant is an invariant that symbolizes that the expression can be any -// type. It is sometimes used to ensure that an expr actually gets a solution -// type so that it is not left unreferenced, and as a result, unsolved. -// TODO: is there a better name than AnyInvariant -type AnyInvariant struct { - Expr Expr -} - -// String returns a representation of this invariant. -func (obj *AnyInvariant) String() string { - return fmt.Sprintf("%p == *", obj.Expr) -} - -// ExprList returns the list of valid expressions in this invariant. -func (obj *AnyInvariant) ExprList() []Expr { - return []Expr{obj.Expr} -} - -// Matches returns whether an invariant matches the existing solution. If it is -// inconsistent, then it errors. -func (obj *AnyInvariant) Matches(solved map[Expr]*types.Type) (bool, error) { - _, exists := solved[obj.Expr] // we only care that it is found. - return exists, nil -} - -// Possible returns an error if it is certain that it is NOT possible to get a -// solution with this invariant and the set of partials. In certain cases, it -// might not be able to determine that it's not possible, while simultaneously -// not being able to guarantee a possible solution either. In this situation, it -// should return nil, since this is used as a filtering mechanism, and the nil -// result of possible is preferred over eliminating a tricky, but possible one. -// This particular implementation always returns nil. -func (obj *AnyInvariant) Possible([]Invariant) error { - // keep it simple, even though we don't technically check the inputs... - return nil -} - -// ValueInvariant is an invariant that stores the value associated with an expr -// if it happens to be known statically at unification/compile time. This must -// only be used for static/pure values. For example, in `$x = 42`, we know that -// $x is 42. It's useful here because for `printf("hello %d times", 42)` we can -// get both the format string, and the other args as these new invariants, and -// we'd store those separately into this invariant, where they can eventually be -// passed into the generator invariant, where it can parse the format string and -// we'd be able to produce a precise type for the printf function, since it's -// nearly impossible to do otherwise since the number of possibilities is -// infinite! One special note: these values are typically not consumed, by the -// solver, because they need to be around for the generator invariant to use, so -// make sure your solver implementation can still terminate with unused -// invariants! -type ValueInvariant struct { - Expr Expr - Value types.Value // pointer -} - -// String returns a representation of this invariant. -func (obj *ValueInvariant) String() string { - return fmt.Sprintf("%p == %s", obj.Expr, obj.Value) -} - -// ExprList returns the list of valid expressions in this invariant. -func (obj *ValueInvariant) ExprList() []Expr { - return []Expr{obj.Expr} -} - -// Matches returns whether an invariant matches the existing solution. If it is -// inconsistent, then it errors. -func (obj *ValueInvariant) Matches(solved map[Expr]*types.Type) (bool, error) { - typ, exists := solved[obj.Expr] - if !exists { - return false, nil - } - if err := typ.Cmp(obj.Value.Type()); err != nil { - return false, err - } - return true, nil -} - -// Possible returns an error if it is certain that it is NOT possible to get a -// solution with this invariant and the set of partials. In certain cases, it -// might not be able to determine that it's not possible, while simultaneously -// not being able to guarantee a possible solution either. In this situation, it -// should return nil, since this is used as a filtering mechanism, and the nil -// result of possible is preferred over eliminating a tricky, but possible one. -func (obj *ValueInvariant) Possible(partials []Invariant) error { - // XXX: Double check this is logical. It was modified from EqualsInvariant. - solved := map[Expr]*types.Type{ - obj.Expr: obj.Value.Type(), - } - for _, invar := range partials { // check each one - _, err := invar.Matches(solved) - if err != nil { // inconsistent, so it's not possible - return errwrap.Wrapf(err, "not possible") - } - } - - return nil -} - -// CallFuncArgsValueInvariant expresses that a func call is associated with a -// particular func, and that it is called with a specific list of args. Expr -// must match the function call expression, Func must match the actual function -// expression, and Args matches the args used in the call to run the func. -// TODO: should this be named FuncCallArgsValueInvariant or something different -// or not? -type CallFuncArgsValueInvariant struct { - // Expr represents the pointer to the ExprCall. - Expr Expr - - // Func represents the pointer to the ExprFunc that ExprCall is using. - Func Expr - - // Args represents the list of args that the ExprCall is using to call - // the ExprFunc. A solver might speculatively call Value() on each of - // these in the hopes of doing something useful if a value happens to be - // known statically at compile time. One such solver that might do this - // is the GeneratorInvariant inside of a difficult function like printf. - Args []Expr -} - -// String returns a representation of this invariant. -func (obj *CallFuncArgsValueInvariant) String() string { - return fmt.Sprintf("%p == callfuncargs(%p) %p", obj.Expr, obj.Func, obj.Args) // TODO: improve this -} - -// ExprList returns the list of valid expressions in this invariant. -func (obj *CallFuncArgsValueInvariant) ExprList() []Expr { - return []Expr{obj.Expr} // XXX: add obj.Func or each obj.Args ? -} - -// Matches returns whether an invariant matches the existing solution. If it is -// inconsistent, then it errors. -func (obj *CallFuncArgsValueInvariant) Matches(solved map[Expr]*types.Type) (bool, error) { - // XXX: not implemented (don't panic though) - //return false, err // inconsistent! - //return false, nil // not matched yet - //return true, nil // matched! - return false, nil // not matched yet -} - -// Possible returns an error if it is certain that it is NOT possible to get a -// solution with this invariant and the set of partials. In certain cases, it -// might not be able to determine that it's not possible, while simultaneously -// not being able to guarantee a possible solution either. In this situation, it -// should return nil, since this is used as a filtering mechanism, and the nil -// result of possible is preferred over eliminating a tricky, but possible one. -// This particular implementation is currently not implemented! -func (obj *CallFuncArgsValueInvariant) Possible(partials []Invariant) error { - // XXX: not implemented - return nil // safer to return nil than error -} - -// SkipInvariant expresses that a particular expression does must not be part of -// the final solution, and should be skipped. It can be part of the solving -// process though. -type SkipInvariant struct { - Expr Expr -} - -// String returns a representation of this invariant. -func (obj *SkipInvariant) String() string { - return fmt.Sprintf("skip(%p)", obj.Expr) -} - -// ExprList returns the list of valid expressions in this invariant. It is not -// used for this invariant. -func (obj *SkipInvariant) ExprList() []Expr { - // XXX: not used - return []Expr{obj.Expr} -} - -// Matches is not used for this invariant. -func (obj *SkipInvariant) Matches(solved map[Expr]*types.Type) (bool, error) { - // XXX: not used - panic("not used") -} - -// Possible is not used for this invariant. -func (obj *SkipInvariant) Possible(partials []Invariant) error { - // XXX: not used - panic("not used") -} diff --git a/lang/interpolate/interpolate.go b/lang/interpolate/interpolate.go index bfb6c3b0..b26e46cf 100644 --- a/lang/interpolate/interpolate.go +++ b/lang/interpolate/interpolate.go @@ -36,6 +36,7 @@ import ( "github.com/purpleidea/mgmt/lang/ast" "github.com/purpleidea/mgmt/lang/funcs" + "github.com/purpleidea/mgmt/lang/funcs/operators" "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/util/errwrap" @@ -312,7 +313,7 @@ func concatExprListIntoCall(exprs []interfaces.Expr) (interfaces.Expr, error) { // V: "", // empty str // } // return &ast.ExprCall{ - // Name: funcs.OperatorFuncName, // concatenate the two strings with + operator + // Name: operators.OperatorFuncName, // concatenate the two strings with + operator // Args: []interfaces.Expr{ // operator, // operator first // arg, // string arg @@ -344,7 +345,7 @@ func concatExprListIntoCall(exprs []interfaces.Expr) (interfaces.Expr, error) { return &ast.ExprCall{ // NOTE: if we don't set the data field we need Init() called on it! - Name: funcs.OperatorFuncName, // concatenate the two strings with + operator + Name: operators.OperatorFuncName, // concatenate the two strings with + operator Args: []interfaces.Expr{ operator, // operator first head, // string arg diff --git a/lang/interpret_test/TestAstFunc1/fail2.txtar b/lang/interpret_test/TestAstFunc1/fail2.txtar index 4fb30fee..bd774c0c 100644 --- a/lang/interpret_test/TestAstFunc1/fail2.txtar +++ b/lang/interpret_test/TestAstFunc1/fail2.txtar @@ -9,4 +9,4 @@ test "t1" { anotherstr => fmt.printf("hello %s", $x), } -- OUTPUT -- -# err: errUnify: can't unify, invariant illogicality with equality: base kind does not match (Str != Int) +# err: errUnify: type/value inconsistent at arg #1 for func `fmt.printf`: str != int diff --git a/lang/interpret_test/TestAstFunc1/polydoubleincludewithtype.txtar b/lang/interpret_test/TestAstFunc1/polydoubleincludewithtype.txtar index 0bd30f5b..e595301c 100644 --- a/lang/interpret_test/TestAstFunc1/polydoubleincludewithtype.txtar +++ b/lang/interpret_test/TestAstFunc1/polydoubleincludewithtype.txtar @@ -12,4 +12,4 @@ class c1($a, $b []str) { } } -- OUTPUT -- -# err: errUnify: can't unify, invariant illogicality with equals: base kind does not match (Str != List) +# err: errUnify: unify error with: str("hello"): type error: List != Str diff --git a/lang/interpret_test/TestAstFunc2/fortytwo.txtar b/lang/interpret_test/TestAstFunc2/fortytwo.txtar index 3cc62d0d..7dc845c0 100644 --- a/lang/interpret_test/TestAstFunc2/fortytwo.txtar +++ b/lang/interpret_test/TestAstFunc2/fortytwo.txtar @@ -3,6 +3,9 @@ import "fmt" import "math" # FIXME: floats don't print nicely: https://github.com/golang/go/issues/46118 # FIXME: This means that we see "42" for both, instead of 42.0 ... +# NOTE: It's important that we have both of these in the same test so that we +# can catch old unification bugs that saw ExprTopLevel as the same for both! +# This happened because we weren't copying the type signature being unified! test [fmt.printf("int: %d", math.fortytwo()),] {} test [fmt.printf("float: %f", math.fortytwo()),] {} -- OUTPUT -- diff --git a/lang/interpret_test/TestAstFunc2/fortytwoerror.txtar b/lang/interpret_test/TestAstFunc2/fortytwoerror.txtar new file mode 100644 index 00000000..87d99d32 --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/fortytwoerror.txtar @@ -0,0 +1,18 @@ +-- main.mcl -- +import "fmt" +import "math" + +# This should fail since it can't be polymorphic! +$myfortytwo = func() { + math.fortytwo() +} + +$a = $myfortytwo() + 4 +$b = $myfortytwo() + 3.0 + +test ["x",] { + int8 => $a, + float32 => $b, +} +-- OUTPUT -- +# err: errUnify: unify error with: topLevel(func() { }): type error: Int != Float diff --git a/lang/interpret_test/TestAstFunc2/fortytwowrapped.txtar b/lang/interpret_test/TestAstFunc2/fortytwowrapped.txtar new file mode 100644 index 00000000..c3bffa1c --- /dev/null +++ b/lang/interpret_test/TestAstFunc2/fortytwowrapped.txtar @@ -0,0 +1,15 @@ +-- main.mcl -- +import "fmt" +import "math" +# FIXME: floats don't print nicely: https://github.com/golang/go/issues/46118 +# FIXME: This means that we see "42" for both, instead of 42.0 ... + +func myfortytwo() { + math.fortytwo() +} + +test [fmt.printf("int: %d", myfortytwo()),] {} +test [fmt.printf("float: %f", myfortytwo()),] {} +-- OUTPUT -- +Vertex: test[float: 42] +Vertex: test[int: 42] diff --git a/lang/interpret_test/TestAstFunc2/invalid-function-call.txtar b/lang/interpret_test/TestAstFunc2/invalid-function-call.txtar index 4be4c38a..b3afb8fa 100644 --- a/lang/interpret_test/TestAstFunc2/invalid-function-call.txtar +++ b/lang/interpret_test/TestAstFunc2/invalid-function-call.txtar @@ -10,4 +10,4 @@ print "msg" { msg => fmt.printf("notfn: %d", $x), } -- OUTPUT -- -# err: errUnify: func arg count differs +# err: errUnify: unify error with: topLevel(singleton(int(42))): type error: Func != Int diff --git a/lang/interpret_test/TestAstFunc2/lookup3.txtar b/lang/interpret_test/TestAstFunc2/lookup3.txtar index 440fac43..2c342d29 100644 --- a/lang/interpret_test/TestAstFunc2/lookup3.txtar +++ b/lang/interpret_test/TestAstFunc2/lookup3.txtar @@ -12,4 +12,4 @@ $name = $st->missing + "fail" test "${name}" {} # this can't unify! -- OUTPUT -- -# err: errUnify: 2 unconsumed generators +# err: errUnify: error setting type: func() { }, error: field missing was not found in struct diff --git a/lang/interpret_test/TestAstFunc2/polymorphic-lambda.txtar b/lang/interpret_test/TestAstFunc2/polymorphic-lambda.txtar index 9ce06776..318f7f20 100644 --- a/lang/interpret_test/TestAstFunc2/polymorphic-lambda.txtar +++ b/lang/interpret_test/TestAstFunc2/polymorphic-lambda.txtar @@ -10,11 +10,11 @@ $add = func($x) { $num = 2 $out1 = $add($num) # 4 -test fmt.printf("%d + %d is %d", $num, $num, $out1) {} # simple math +test [fmt.printf("%d + %d is %d", $num, $num, $out1),] {} # simple math $val = "hello" $out2 = $add($val) # hellohello -test fmt.printf("%s + %s is %s", $val, $val, $out2) {} # simple concat +test [fmt.printf("%s + %s is %s", $val, $val, $out2),] {} # simple concat -- OUTPUT -- -# err: errUnify: can't unify, invariant illogicality with equality: base kind does not match (Int != Str) +# err: errUnify: unify error with: topLevel(singleton(func(x) { call:_operator(str("+"), var(x), var(x)) })): type error: Str != Int diff --git a/lang/interpret_test/TestAstFunc2/printfempty.txtar b/lang/interpret_test/TestAstFunc2/printfempty.txtar index a4dd3b08..2880e140 100644 --- a/lang/interpret_test/TestAstFunc2/printfempty.txtar +++ b/lang/interpret_test/TestAstFunc2/printfempty.txtar @@ -7,4 +7,4 @@ import "fmt" test fmt.printf() {} -- OUTPUT -- -# err: errUnify: 1 unconsumed generators +# err: errUnify: func `printf` infer error: must have at least one arg diff --git a/lang/interpret_test/TestAstFunc2/printfunificationerr0.txtar b/lang/interpret_test/TestAstFunc2/printfunificationerr0.txtar index 18b352bd..9b5e4717 100644 --- a/lang/interpret_test/TestAstFunc2/printfunificationerr0.txtar +++ b/lang/interpret_test/TestAstFunc2/printfunificationerr0.txtar @@ -2,4 +2,4 @@ import "fmt" test fmt.printf("%d%d", 42) {} # should not pass, missing second int -- OUTPUT -- -# err: errUnify: 1 unconsumed generators +# err: errUnify: unify error with: call:fmt.printf(str("%d%d"), int(42)): type error: Str != List diff --git a/lang/interpret_test/TestAstFunc2/res1.txtar b/lang/interpret_test/TestAstFunc2/res1.txtar index 6a2f0e61..2404f47d 100644 --- a/lang/interpret_test/TestAstFunc2/res1.txtar +++ b/lang/interpret_test/TestAstFunc2/res1.txtar @@ -3,4 +3,4 @@ test "t1" { stringptr => 42, # int, not str } -- OUTPUT -- -# err: errUnify: can't unify, invariant illogicality with equals: base kind does not match (Int != Str) +# err: errUnify: unify error with: int(42): type error: Str != Int diff --git a/lang/interpret_test/TestAstFunc2/test-monomorphic-bind.txtar b/lang/interpret_test/TestAstFunc2/test-monomorphic-bind.txtar index bceb61a7..67626649 100644 --- a/lang/interpret_test/TestAstFunc2/test-monomorphic-bind.txtar +++ b/lang/interpret_test/TestAstFunc2/test-monomorphic-bind.txtar @@ -10,4 +10,4 @@ test "test2" { anotherstr => $id("hello"), } -- OUTPUT -- -# err: errUnify: can't unify, invariant illogicality with equals: base kind does not match (Int != Str) +# err: errUnify: unify error with: topLevel(singleton(func(x) { var(x) })): type error: Str != Int diff --git a/lang/interpret_test/TestAstFunc2/test-monomorphic-class-arg.txtar b/lang/interpret_test/TestAstFunc2/test-monomorphic-class-arg.txtar index 79c21749..eaecfe92 100644 --- a/lang/interpret_test/TestAstFunc2/test-monomorphic-class-arg.txtar +++ b/lang/interpret_test/TestAstFunc2/test-monomorphic-class-arg.txtar @@ -12,4 +12,4 @@ class use_polymorphically($id) { } include use_polymorphically(func($x) {$x}) -- OUTPUT -- -# err: errUnify: can't unify, invariant illogicality with equals: base kind does not match (Int != Str) +# err: errUnify: unify error with: topLevel(singleton(func(x) { var(x) })): type error: Str != Int diff --git a/lang/interpret_test/TestAstFunc2/test-monomorphic-func-arg.txtar b/lang/interpret_test/TestAstFunc2/test-monomorphic-func-arg.txtar index fa2ef042..adf3c7c3 100644 --- a/lang/interpret_test/TestAstFunc2/test-monomorphic-func-arg.txtar +++ b/lang/interpret_test/TestAstFunc2/test-monomorphic-func-arg.txtar @@ -10,4 +10,4 @@ test "test1" { anotherstr => use_polymorphically(func($x) {$x}), } -- OUTPUT -- -# err: errUnify: can't unify, invariant illogicality with equals: base kind does not match (Int != Str) +# err: errUnify: unify error with: param(id): type error: Int != Str diff --git a/lang/interpret_test/TestAstFunc2/test-monomorphic-lambda-arg.txtar b/lang/interpret_test/TestAstFunc2/test-monomorphic-lambda-arg.txtar index 91359e19..30d3eebf 100644 --- a/lang/interpret_test/TestAstFunc2/test-monomorphic-lambda-arg.txtar +++ b/lang/interpret_test/TestAstFunc2/test-monomorphic-lambda-arg.txtar @@ -10,4 +10,4 @@ test "test1" { anotherstr => $use_polymorphically(func($x) {$x}), } -- OUTPUT -- -# err: errUnify: can't unify, invariant illogicality with equals: base kind does not match (Int != Str) +# err: errUnify: unify error with: param(id): type error: Int != Str diff --git a/lang/interpret_test/TestAstFunc2/unify-interpolate-edge1-fail.txtar b/lang/interpret_test/TestAstFunc2/unify-interpolate-edge1-fail.txtar index 757cca2a..e155ff61 100644 --- a/lang/interpret_test/TestAstFunc2/unify-interpolate-edge1-fail.txtar +++ b/lang/interpret_test/TestAstFunc2/unify-interpolate-edge1-fail.txtar @@ -7,4 +7,4 @@ test "test" {} Test["${name}"] -> Test["test"] # must fail -- OUTPUT -- -# err: errUnify: can't unify, invariant illogicality with equality: base kind does not match (Str != List) +# err: errUnify: unify error with: var(name): type error: Str != List diff --git a/lang/interpret_test/TestAstFunc2/unify-interpolate-edge2-fail.txtar b/lang/interpret_test/TestAstFunc2/unify-interpolate-edge2-fail.txtar index 521662c8..1bacda88 100644 --- a/lang/interpret_test/TestAstFunc2/unify-interpolate-edge2-fail.txtar +++ b/lang/interpret_test/TestAstFunc2/unify-interpolate-edge2-fail.txtar @@ -7,4 +7,4 @@ test "test" {} Test["test"] -> Test["${name}"] # must fail -- OUTPUT -- -# err: errUnify: can't unify, invariant illogicality with equality: base kind does not match (Str != List) +# err: errUnify: unify error with: var(name): type error: Str != List diff --git a/lang/interpret_test/TestAstFunc2/unify-interpolate-res-fail.txtar b/lang/interpret_test/TestAstFunc2/unify-interpolate-res-fail.txtar index 8f0a7e90..b49640f8 100644 --- a/lang/interpret_test/TestAstFunc2/unify-interpolate-res-fail.txtar +++ b/lang/interpret_test/TestAstFunc2/unify-interpolate-res-fail.txtar @@ -5,4 +5,4 @@ $name = ["a", "bb", "ccc",] test "${name}" {} # must fail -- OUTPUT -- -# err: errUnify: can't unify, invariant illogicality with equality: base kind does not match (Str != List) +# err: errUnify: unify error with: var(name): type error: Str != List diff --git a/lang/interpret_test/TestAstFunc2/very-complex-example.txtar b/lang/interpret_test/TestAstFunc2/very-complex-example.txtar index e63aeb75..4e8a1dd0 100644 --- a/lang/interpret_test/TestAstFunc2/very-complex-example.txtar +++ b/lang/interpret_test/TestAstFunc2/very-complex-example.txtar @@ -75,4 +75,4 @@ $foo = iter.map([$id1, $id2,], $generate) $name = $foo[0] || "fail" test "${name}" {} -- OUTPUT -- -# err: errUnify: can't unify, invariant illogicality with equals: base kind does not match (Str != Int) +# err: errUnify: unify error with: param(idn): type error: Str != Int diff --git a/lang/lang.go b/lang/lang.go index 7a537161..17333a45 100644 --- a/lang/lang.go +++ b/lang/lang.go @@ -243,10 +243,7 @@ func (obj *Lang) Init(ctx context.Context) error { // apply type unification logf := func(format string, v ...interface{}) { - // TODO: Remove the masked logger here when unification is clean! - if obj.Debug { // unification only has debug messages... - obj.Logf("unification: "+format, v...) - } + obj.Logf("unification: "+format, v...) } obj.Logf("running type unification...") diff --git a/lang/parser/lexparse_test.go b/lang/parser/lexparse_test.go index 6fb0a1e4..e93f1f6d 100644 --- a/lang/parser/lexparse_test.go +++ b/lang/parser/lexparse_test.go @@ -39,7 +39,7 @@ import ( "testing" "github.com/purpleidea/mgmt/lang/ast" - "github.com/purpleidea/mgmt/lang/funcs" + "github.com/purpleidea/mgmt/lang/funcs/operators" "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/types" langUtil "github.com/purpleidea/mgmt/lang/util" @@ -595,7 +595,7 @@ func TestLexParse0(t *testing.T) { &ast.StmtResField{ Field: "int64ptr", Value: &ast.ExprCall{ - Name: funcs.OperatorFuncName, + Name: operators.OperatorFuncName, Args: []interfaces.Expr{ &ast.ExprStr{ V: "+", @@ -636,13 +636,13 @@ func TestLexParse0(t *testing.T) { &ast.StmtResField{ Field: "float32", Value: &ast.ExprCall{ - Name: funcs.OperatorFuncName, + Name: operators.OperatorFuncName, Args: []interfaces.Expr{ &ast.ExprStr{ V: "+", }, &ast.ExprCall{ - Name: funcs.OperatorFuncName, + Name: operators.OperatorFuncName, Args: []interfaces.Expr{ &ast.ExprStr{ V: "+", @@ -688,7 +688,7 @@ func TestLexParse0(t *testing.T) { &ast.StmtResField{ Field: "int64ptr", Value: &ast.ExprCall{ - Name: funcs.OperatorFuncName, + Name: operators.OperatorFuncName, Args: []interfaces.Expr{ &ast.ExprStr{ V: "+", @@ -697,7 +697,7 @@ func TestLexParse0(t *testing.T) { V: 4, }, &ast.ExprCall{ - Name: funcs.OperatorFuncName, + Name: operators.OperatorFuncName, Args: []interfaces.Expr{ &ast.ExprStr{ V: "*", @@ -740,13 +740,13 @@ func TestLexParse0(t *testing.T) { &ast.StmtResField{ Field: "int64ptr", Value: &ast.ExprCall{ - Name: funcs.OperatorFuncName, + Name: operators.OperatorFuncName, Args: []interfaces.Expr{ &ast.ExprStr{ V: "+", }, &ast.ExprCall{ - Name: funcs.OperatorFuncName, + Name: operators.OperatorFuncName, Args: []interfaces.Expr{ &ast.ExprStr{ V: "*", @@ -792,7 +792,7 @@ func TestLexParse0(t *testing.T) { &ast.StmtResField{ Field: "int64ptr", Value: &ast.ExprCall{ - Name: funcs.OperatorFuncName, + Name: operators.OperatorFuncName, Args: []interfaces.Expr{ &ast.ExprStr{ V: "*", @@ -801,7 +801,7 @@ func TestLexParse0(t *testing.T) { V: 3, }, &ast.ExprCall{ - Name: funcs.OperatorFuncName, + Name: operators.OperatorFuncName, Args: []interfaces.Expr{ &ast.ExprStr{ V: "+", @@ -844,13 +844,13 @@ func TestLexParse0(t *testing.T) { &ast.StmtResField{ Field: "boolptr", Value: &ast.ExprCall{ - Name: funcs.OperatorFuncName, + Name: operators.OperatorFuncName, Args: []interfaces.Expr{ &ast.ExprStr{ V: ">", }, &ast.ExprCall{ - Name: funcs.OperatorFuncName, + Name: operators.OperatorFuncName, Args: []interfaces.Expr{ &ast.ExprStr{ V: "+", @@ -896,7 +896,7 @@ func TestLexParse0(t *testing.T) { &ast.StmtResField{ Field: "boolptr", Value: &ast.ExprCall{ - Name: funcs.OperatorFuncName, + Name: operators.OperatorFuncName, Args: []interfaces.Expr{ &ast.ExprStr{ V: ">", @@ -905,7 +905,7 @@ func TestLexParse0(t *testing.T) { V: 3, }, &ast.ExprCall{ - Name: funcs.OperatorFuncName, + Name: operators.OperatorFuncName, Args: []interfaces.Expr{ &ast.ExprStr{ V: "+", @@ -948,13 +948,13 @@ func TestLexParse0(t *testing.T) { &ast.StmtResField{ Field: "boolptr", Value: &ast.ExprCall{ - Name: funcs.OperatorFuncName, + Name: operators.OperatorFuncName, Args: []interfaces.Expr{ &ast.ExprStr{ V: ">", }, &ast.ExprCall{ - Name: funcs.OperatorFuncName, + Name: operators.OperatorFuncName, Args: []interfaces.Expr{ &ast.ExprStr{ V: "not", @@ -997,13 +997,13 @@ func TestLexParse0(t *testing.T) { &ast.StmtResField{ Field: "boolptr", Value: &ast.ExprCall{ - Name: funcs.OperatorFuncName, + Name: operators.OperatorFuncName, Args: []interfaces.Expr{ &ast.ExprStr{ V: "and", }, &ast.ExprCall{ - Name: funcs.OperatorFuncName, + Name: operators.OperatorFuncName, Args: []interfaces.Expr{ &ast.ExprStr{ V: "<", @@ -1766,7 +1766,7 @@ func TestLexParse0(t *testing.T) { Args: []*interfaces.Arg{}, Return: types.TypeInt, Body: &ast.ExprCall{ - Name: funcs.OperatorFuncName, + Name: operators.OperatorFuncName, Args: []interfaces.Expr{ &ast.ExprStr{ V: "+", @@ -1781,7 +1781,8 @@ func TestLexParse0(t *testing.T) { }, } // sometimes, the type can get set by the parser when it's known - if err := fn.SetType(types.NewType("func() int")); err != nil { + typ := types.NewType("func() int") + if err := fn.SetType(typ); err != nil { t.Fatal("could not build type") } exp := &ast.StmtProg{ @@ -1789,6 +1790,7 @@ func TestLexParse0(t *testing.T) { &ast.StmtFunc{ Name: "f2", Func: fn, + Type: typ, }, }, } @@ -1817,7 +1819,7 @@ func TestLexParse0(t *testing.T) { }, Return: types.TypeInt, Body: &ast.ExprCall{ - Name: funcs.OperatorFuncName, + Name: operators.OperatorFuncName, Args: []interfaces.Expr{ &ast.ExprStr{ V: "+", @@ -1864,7 +1866,7 @@ func TestLexParse0(t *testing.T) { }, Return: types.TypeStr, Body: &ast.ExprCall{ - Name: funcs.OperatorFuncName, + Name: operators.OperatorFuncName, Args: []interfaces.Expr{ &ast.ExprStr{ V: "+", @@ -1878,7 +1880,8 @@ func TestLexParse0(t *testing.T) { }, }, } - if err := fn.SetType(types.NewType("func(x str) str")); err != nil { + typ := types.NewType("func(x str) str") + if err := fn.SetType(typ); err != nil { t.Fatal("could not build type") } exp := &ast.StmtProg{ @@ -1886,6 +1889,7 @@ func TestLexParse0(t *testing.T) { &ast.StmtFunc{ Name: "f4", Func: fn, + Type: typ, }, }, } @@ -1938,7 +1942,7 @@ func TestLexParse0(t *testing.T) { }, Return: types.TypeStr, Body: &ast.ExprCall{ - Name: funcs.OperatorFuncName, + Name: operators.OperatorFuncName, Args: []interfaces.Expr{ &ast.ExprStr{ V: "+", @@ -1985,7 +1989,7 @@ func TestLexParse0(t *testing.T) { }, Return: types.TypeStr, Body: &ast.ExprCall{ - Name: funcs.OperatorFuncName, + Name: operators.OperatorFuncName, Args: []interfaces.Expr{ &ast.ExprStr{ V: "+", @@ -2095,7 +2099,7 @@ func TestLexParse0(t *testing.T) { }, //Return: types.TypeInt, Body: &ast.ExprCall{ - Name: funcs.OperatorFuncName, + Name: operators.OperatorFuncName, Args: []interfaces.Expr{ &ast.ExprStr{ V: "*", diff --git a/lang/parser/parser.y b/lang/parser/parser.y index 057d2a5f..f497b799 100644 --- a/lang/parser/parser.y +++ b/lang/parser/parser.y @@ -36,6 +36,7 @@ import ( "github.com/purpleidea/mgmt/lang/ast" "github.com/purpleidea/mgmt/lang/funcs" + "github.com/purpleidea/mgmt/lang/funcs/operators" "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/types" "github.com/purpleidea/mgmt/util" @@ -251,13 +252,15 @@ stmt: m[a.Name] = a.Type ord = append(ord, a.Name) } + var typ *types.Type if isFullyTyped { - typ := &types.Type{ + typ = &types.Type{ Kind: types.KindFunc, Map: m, Ord: ord, Out: $6.typ, } + // XXX: We might still need to do this for now... if err := fn.SetType(typ); err != nil { // this will ultimately cause a parser error to occur... yylex.Error(fmt.Sprintf("%s: %+v", ErrParseSetType, err)) @@ -266,6 +269,7 @@ stmt: $$.stmt = &ast.StmtFunc{ Name: $2.str, Func: fn, + Type: typ, // sam says add the type here instead... } } // `class name { }` @@ -566,7 +570,7 @@ call: { posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprCall{ - Name: funcs.OperatorFuncName, + Name: operators.OperatorFuncName, Args: []interfaces.Expr{ &ast.ExprStr{ // operator first V: $2.str, // for PLUS this is a `+` character @@ -580,7 +584,7 @@ call: { posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprCall{ - Name: funcs.OperatorFuncName, + Name: operators.OperatorFuncName, Args: []interfaces.Expr{ &ast.ExprStr{ // operator first V: $2.str, @@ -594,7 +598,7 @@ call: { posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprCall{ - Name: funcs.OperatorFuncName, + Name: operators.OperatorFuncName, Args: []interfaces.Expr{ &ast.ExprStr{ // operator first V: $2.str, @@ -608,7 +612,7 @@ call: { posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprCall{ - Name: funcs.OperatorFuncName, + Name: operators.OperatorFuncName, Args: []interfaces.Expr{ &ast.ExprStr{ // operator first V: $2.str, @@ -622,7 +626,7 @@ call: { posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprCall{ - Name: funcs.OperatorFuncName, + Name: operators.OperatorFuncName, Args: []interfaces.Expr{ &ast.ExprStr{ // operator first V: $2.str, @@ -636,7 +640,7 @@ call: { posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprCall{ - Name: funcs.OperatorFuncName, + Name: operators.OperatorFuncName, Args: []interfaces.Expr{ &ast.ExprStr{ // operator first V: $2.str, @@ -650,7 +654,7 @@ call: { posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprCall{ - Name: funcs.OperatorFuncName, + Name: operators.OperatorFuncName, Args: []interfaces.Expr{ &ast.ExprStr{ // operator first V: $2.str, @@ -664,7 +668,7 @@ call: { posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprCall{ - Name: funcs.OperatorFuncName, + Name: operators.OperatorFuncName, Args: []interfaces.Expr{ &ast.ExprStr{ // operator first V: $2.str, @@ -678,7 +682,7 @@ call: { posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprCall{ - Name: funcs.OperatorFuncName, + Name: operators.OperatorFuncName, Args: []interfaces.Expr{ &ast.ExprStr{ // operator first V: $2.str, @@ -692,7 +696,7 @@ call: { posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprCall{ - Name: funcs.OperatorFuncName, + Name: operators.OperatorFuncName, Args: []interfaces.Expr{ &ast.ExprStr{ // operator first V: $2.str, @@ -706,7 +710,7 @@ call: { posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprCall{ - Name: funcs.OperatorFuncName, + Name: operators.OperatorFuncName, Args: []interfaces.Expr{ &ast.ExprStr{ // operator first V: $2.str, @@ -720,7 +724,7 @@ call: { posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprCall{ - Name: funcs.OperatorFuncName, + Name: operators.OperatorFuncName, Args: []interfaces.Expr{ &ast.ExprStr{ // operator first V: $2.str, @@ -734,7 +738,7 @@ call: { posLast(yylex, yyDollar) // our pos $$.expr = &ast.ExprCall{ - Name: funcs.OperatorFuncName, + Name: operators.OperatorFuncName, Args: []interfaces.Expr{ &ast.ExprStr{ // operator first V: $1.str, @@ -947,6 +951,7 @@ bind: { posLast(yylex, yyDollar) // our pos var expr interfaces.Expr = $4.expr + // XXX: We still need to do this for now it seems... if err := expr.SetType($2.typ); err != nil { // this will ultimately cause a parser error to occur... yylex.Error(fmt.Sprintf("%s: %+v", ErrParseSetType, err)) @@ -954,6 +959,7 @@ bind: $$.stmt = &ast.StmtBind{ Ident: $1.str, Value: expr, + Type: $2.typ, // sam says add the type here instead... } } ; diff --git a/lang/unification/fastsolver/fastsolver.go b/lang/unification/fastsolver/fastsolver.go new file mode 100644 index 00000000..a2ebd0b8 --- /dev/null +++ b/lang/unification/fastsolver/fastsolver.go @@ -0,0 +1,212 @@ +// Mgmt +// Copyright (C) 2013-2024+ 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 . +// +// Additional permission under GNU GPL version 3 section 7 +// +// If you modify this program, or any covered work, by linking or combining it +// with embedded mcl code and modules (and that the embedded mcl code and +// modules which link with this program, contain a copy of their source code in +// the authoritative form) containing parts covered by the terms of any other +// license, the licensors of this program grant you additional permission to +// convey the resulting work. Furthermore, the licensors of this program grant +// the original author, James Shubin, additional permission to update this +// additional permission if he deems it necessary to achieve the goals of this +// additional permission. + +// Package fastsolver implements very fast type unification. +package fastsolver + +import ( + "context" + "fmt" + "sort" + "strconv" + "strings" + + "github.com/purpleidea/mgmt/lang/interfaces" + "github.com/purpleidea/mgmt/lang/types" + "github.com/purpleidea/mgmt/lang/unification" + unificationUtil "github.com/purpleidea/mgmt/lang/unification/util" + "github.com/purpleidea/mgmt/util/errwrap" +) + +const ( + // Name is the prefix for our solver log messages. + Name = "fast" + + // OptimizationNotImplemented is a placeholder magic flag we can use. + OptimizationNotImplemented = "not-implemented" +) + +func init() { + unification.Register(Name, func() unification.Solver { return &FastInvariantSolver{} }) + unification.Register("", func() unification.Solver { return &FastInvariantSolver{} }) // default +} + +// FastInvariantSolver is a fast invariant solver based on union find. It is +// intended to be computationally efficient. +type FastInvariantSolver struct { + // Strategy is a series of methodologies to heuristically improve the + // solver. + Strategy map[string]string + + // UnifiedState stores a common representation of our unification vars. + UnifiedState *types.UnifiedState + + Debug bool + Logf func(format string, v ...interface{}) + + // notImplemented tells the solver to behave differently somehow... + notImplemented bool +} + +// Init contains some handles that are used to initialize the solver. +func (obj *FastInvariantSolver) Init(init *unification.Init) error { + obj.Strategy = init.Strategy + obj.UnifiedState = init.UnifiedState + obj.Debug = init.Debug + obj.Logf = init.Logf + + optimizations, exists := init.Strategy[unification.StrategyOptimizationsKey] + if !exists { + return nil + } + // TODO: use a query string parser instead? + for _, x := range strings.Split(optimizations, ",") { + if x == OptimizationNotImplemented { + obj.notImplemented = true + continue + } + } + + return nil +} + +// Solve runs the invariant solver. It mutates the .Data field in the .Uni +// unification variables, so that each set contains a single type. If each of +// the sets contains a complete type that no longer contains any ?1 type fields, +// then we have succeeded to unify all of our invariants. If not, then our list +// of invariants must be ambiguous. This is O(N*K) where N is the number of +// invariants, and K is the size of the maximum type. Eg a list of list of map +// of int to str would be of size three. (TODO: or is it four?) +func (obj *FastInvariantSolver) Solve(ctx context.Context, data *unification.Data) (*unification.InvariantSolution, error) { + u := func(typ *types.Type) string { + return obj.UnifiedState.String(typ) + } + + // Build a "list" (map) of what we think we need to solve for exactly. + exprs := make(map[interfaces.Expr]struct{}) + + // TODO: Make better padding for debug output if we end up caring a lot! + pad := strconv.Itoa(len(strconv.Itoa(max(0, len(data.UnificationInvariants)-1)))) // hack + for i, x := range data.UnificationInvariants { // []*UnificationInvariant + // TODO: Is this a good break point for ctx? + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + // pass + } + + if x.Expr == nil { + return nil, fmt.Errorf("unexpected nil expr") + } + exprs[x.Expr] = struct{}{} // Add to the set of what I must solve! + + // TODO: Should we pass ctx into Unify? + if obj.Debug { + obj.Logf("#%"+pad+"d unify(%s): %s -- %s", i, x.Expr, u(x.Expect), u(x.Actual)) + } + if err := unificationUtil.Unify(x.Expect, x.Actual); err != nil { + // Storing the Expr with this invariant is so that we + // can generate this more helpful error message here. + // TODO: Improve this error message! + return nil, errwrap.Wrapf(err, "unify error with: %s", x.Expr) + } + if obj.Debug { + e1, e2 := unificationUtil.Extract(x.Expect), unificationUtil.Extract(x.Actual) + obj.Logf("#%"+pad+"d extract(%s): %s -- %s", i, x.Expr, u(e1), u(e2)) + } + } + count := len(exprs) // safety check + + // build final solution + solutions := []*unification.EqualsInvariant{} + for _, x := range data.UnificationInvariants { // []*UnificationInvariant + if x.Expect == nil || x.Actual == nil { + // programming error ? + return nil, fmt.Errorf("unexpected nil invariant") + } + + // zonk! + t1 := unificationUtil.Extract(x.Expect) + //t2 := unificationUtil.Extract(x.Actual) + + // TODO: collect all of these errors and return them together? + if t1.HasUni() { // || t2.HasUni() + return nil, fmt.Errorf("expr: %s is ambiguous: %s", x.Expr, u(t1)) + } + + //if err := t1.Cmp(t2); err != nil { // for development/debugging + // return nil, errwrap.Wrapf(err, "inconsistency between expect and actual") + //} + + if _, exists := exprs[x.Expr]; !exists { + // TODO: Do we need to check the consistency here? + continue // already solved + } + delete(exprs, x.Expr) // solved! + + invar := &unification.EqualsInvariant{ + Expr: x.Expr, + Type: t1, // || t2 + } + solutions = append(solutions, invar) + } + + // Determine that our solver produced a solution for every expr that + // we're interested in. If it didn't, and it didn't error, then it's a + // bug. We check for this because we care about safety, this ensures + // that our AST will get fully populated with the correct types! + if c := len(exprs); c > 0 { // if there's anything left, it's bad... + // programming error! + ptrs := []string{} + disp := make(map[string]string) // display hack + for i := range exprs { + s := fmt.Sprintf("%p", i) // pointer + ptrs = append(ptrs, s) + disp[s] = i.String() + } + sort.Strings(ptrs) + s := strings.Join(ptrs, ", ") + + obj.Logf("got %d unbound expr's: %s", c, s) + for i, s := range ptrs { + obj.Logf("(%d) %s => %s", i, s, disp[s]) + } + return nil, fmt.Errorf("got %d unbound expr's: %s", c, s) + } + + if l := len(solutions); l != count { // safety check + return nil, fmt.Errorf("got %d expressions and %d solutions", count, l) + } + + // Return a list instead of a map, to keep this ordering deterministic! + return &unification.InvariantSolution{ + Solutions: solutions, + }, nil +} diff --git a/lang/unification/interfaces.go b/lang/unification/interfaces.go index 42738ab4..76838efa 100644 --- a/lang/unification/interfaces.go +++ b/lang/unification/interfaces.go @@ -32,7 +32,6 @@ package unification import ( "context" "fmt" - "sort" "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/types" @@ -66,6 +65,12 @@ type Init struct { Logf func(format string, v ...interface{}) } +// Data contains the input data for the solver to process. +type Data struct { + // UnificationInvariants is an alternate data representation for Solve. + UnificationInvariants []*interfaces.UnificationInvariant +} + // Solver is the general interface that any solver needs to implement. type Solver interface { // Init initializes the solver struct before first use. @@ -73,7 +78,7 @@ type Solver interface { // Solve performs the actual solving. It must return as soon as possible // if the context is closed. - Solve(ctx context.Context, invariants []interfaces.Invariant, expected []interfaces.Expr) (*InvariantSolution, error) + Solve(context.Context, *Data) (*InvariantSolution, error) } // registeredSolvers is a global map of all possible unification solvers which @@ -121,126 +126,3 @@ func LookupDefault() (Solver, error) { return nil, fmt.Errorf("no registered default solver") } - -// DebugSolverState helps us in understanding the state of the type unification -// solver in a more mainstream format. -// Example: -// -// solver state: -// -// * str("foo") :: str -// * call:f(str("foo")) [0xc000ac9f10] :: ?1 -// * var(x) [0xc00088d840] :: ?2 -// * param(x) [0xc00000f950] :: ?3 -// * func(x) { var(x) } [0xc0000e9680] :: ?4 -// * ?2 = ?3 -// * ?4 = func(arg0 str) ?1 -// * ?4 = func(x str) ?2 -// * ?1 = ?2 -func DebugSolverState(solved map[interfaces.Expr]*types.Type, equalities []interfaces.Invariant) string { - s := "" - - // all the relevant Exprs - count := 0 - exprs := make(map[interfaces.Expr]int) - for _, equality := range equalities { - for _, expr := range equality.ExprList() { - count++ - exprs[expr] = count // for sorting - } - } - - // print the solved Exprs first - for expr, typ := range solved { - s += fmt.Sprintf("%v :: %v\n", expr, typ) - delete(exprs, expr) - } - - sortedExprs := []interfaces.Expr{} - for k := range exprs { - sortedExprs = append(sortedExprs, k) - } - sort.Slice(sortedExprs, func(i, j int) bool { return exprs[sortedExprs[i]] < exprs[sortedExprs[j]] }) - - // for each remaining expr, generate a shorter name than the full pointer - nextVar := 1 - shortNames := map[interfaces.Expr]string{} - for _, expr := range sortedExprs { - shortNames[expr] = fmt.Sprintf("?%d", nextVar) - nextVar++ - s += fmt.Sprintf("%p %v :: %s\n", expr, expr, shortNames[expr]) - } - - // print all the equalities using the short names - for _, equality := range equalities { - switch e := equality.(type) { - case *interfaces.EqualsInvariant: - _, ok := solved[e.Expr] - if !ok { - s += fmt.Sprintf("%s = %v\n", shortNames[e.Expr], e.Type) - } else { - // if solved, then this is redundant, don't print anything - } - - case *interfaces.EqualityInvariant: - type1, ok1 := solved[e.Expr1] - type2, ok2 := solved[e.Expr2] - if !ok1 && !ok2 { - s += fmt.Sprintf("%s = %s\n", shortNames[e.Expr1], shortNames[e.Expr2]) - } else if ok1 && !ok2 { - s += fmt.Sprintf("%s = %s\n", type1, shortNames[e.Expr2]) - } else if !ok1 && ok2 { - s += fmt.Sprintf("%s = %s\n", shortNames[e.Expr1], type2) - } else { - // if completely solved, then this is redundant, don't print anything - } - - case *interfaces.EqualityWrapFuncInvariant: - funcType, funcOk := solved[e.Expr1] - - args := "" - argsOk := true - for i, argName := range e.Expr2Ord { - if i > 0 { - args += ", " - } - argExpr := e.Expr2Map[argName] - argType, ok := solved[argExpr] - if !ok { - args += fmt.Sprintf("%s %s", argName, shortNames[argExpr]) - argsOk = false - } else { - args += fmt.Sprintf("%s %s", argName, argType) - } - } - - outType, outOk := solved[e.Expr2Out] - - if !funcOk || !argsOk || !outOk { - if !funcOk && !outOk { - s += fmt.Sprintf("%s = func(%s) %s\n", shortNames[e.Expr1], args, shortNames[e.Expr2Out]) - } else if !funcOk && outOk { - s += fmt.Sprintf("%s = func(%s) %s\n", shortNames[e.Expr1], args, outType) - } else if funcOk && !outOk { - s += fmt.Sprintf("%s = func(%s) %s\n", funcType, args, shortNames[e.Expr2Out]) - } else { - s += fmt.Sprintf("%s = func(%s) %s\n", funcType, args, outType) - } - } - - case *interfaces.CallFuncArgsValueInvariant: - // skip, not used in the examples I care about - - case *interfaces.AnyInvariant: - // skip, not used in the examples I care about - - case *interfaces.SkipInvariant: - // we don't care about this one - - default: - s += fmt.Sprintf("%v\n", equality) - } - } - - return s -} diff --git a/lang/unification/simplesolver/simplesolver.go b/lang/unification/simplesolver/simplesolver.go deleted file mode 100644 index 4dbd0119..00000000 --- a/lang/unification/simplesolver/simplesolver.go +++ /dev/null @@ -1,1354 +0,0 @@ -// Mgmt -// Copyright (C) 2013-2024+ 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 . -// -// Additional permission under GNU GPL version 3 section 7 -// -// If you modify this program, or any covered work, by linking or combining it -// with embedded mcl code and modules (and that the embedded mcl code and -// modules which link with this program, contain a copy of their source code in -// the authoritative form) containing parts covered by the terms of any other -// license, the licensors of this program grant you additional permission to -// convey the resulting work. Furthermore, the licensors of this program grant -// the original author, James Shubin, additional permission to update this -// additional permission if he deems it necessary to achieve the goals of this -// additional permission. - -package simplesolver - -import ( - "context" - "fmt" - "sort" - "strings" - - "github.com/purpleidea/mgmt/lang/interfaces" - "github.com/purpleidea/mgmt/lang/types" - "github.com/purpleidea/mgmt/lang/unification" - "github.com/purpleidea/mgmt/util/errwrap" -) - -const ( - // Name is the prefix for our solver log messages. - Name = "simple" - - // OptimizationSkipFuncCmp is the magic flag name to include the skip - // func cmp optimization which can speed up some simple programs. If - // this is specified, then OptimizationHeuristicalDrop is redundant. - OptimizationSkipFuncCmp = "skip-func-cmp" - - // OptimizationHeuristicalDrop is the magic flag name to include to tell - // the solver to drop some func compares. This is a less aggressive form - // of OptimizationSkipFuncCmp. This is redundant if - // OptimizationSkipFuncCmp is true. - OptimizationHeuristicalDrop = "heuristical-drop" - - // AllowRecursion specifies whether we're allowed to use the recursive - // solver or not. It uses an absurd amount of memory, and might hang - // your system if a simple solution doesn't exist. - AllowRecursion = false - - // RecursionDepthLimit specifies the max depth that is allowed. - // FIXME: RecursionDepthLimit is not currently implemented - RecursionDepthLimit = 5 // TODO: pick a better value ? - - // RecursionInvariantLimit specifies the max number of invariants we can - // recurse into. - RecursionInvariantLimit = 5 // TODO: pick a better value ? -) - -func init() { - unification.Register(Name, func() unification.Solver { return &SimpleInvariantSolver{} }) -} - -// SimpleInvariantSolver is an iterative invariant solver for AST expressions. -// It is intended to be very simple, even if it's computationally inefficient. -// TODO: Move some of the global solver constants into this struct as params. -type SimpleInvariantSolver struct { - // Strategy is a series of methodologies to heuristically improve the - // solver. - Strategy map[string]string - - Debug bool - Logf func(format string, v ...interface{}) - - // skipFuncCmp tells the solver to skip the slow loop entirely. This may - // prevent some correct programs from completing type unification. - skipFuncCmp bool - - // heuristicalDrop tells the solver to drop some func compares. This was - // determined heuristically and needs checking to see if it's even a - // sensible algorithmic approach. This is a less aggressive form of - // skipFuncCmp. This is redundant if skipFuncCmp is true. - heuristicalDrop bool - - // zTotal is a heuristic counter to measure the size of the slow loop. - zTotal int -} - -// Init contains some handles that are used to initialize the solver. -func (obj *SimpleInvariantSolver) Init(init *unification.Init) error { - obj.Strategy = init.Strategy - - obj.Debug = init.Debug - obj.Logf = init.Logf - - optimizations, exists := init.Strategy[unification.StrategyOptimizationsKey] - if !exists { - return nil - } - // TODO: use a query string parser instead? - for _, x := range strings.Split(optimizations, ",") { - if x == OptimizationSkipFuncCmp { - obj.skipFuncCmp = true - continue - } - if x == OptimizationHeuristicalDrop { - obj.heuristicalDrop = true - continue - } - } - - return nil -} - -// Solve is the actual solve implementation of the solver. -func (obj *SimpleInvariantSolver) Solve(ctx context.Context, invariants []interfaces.Invariant, expected []interfaces.Expr) (*unification.InvariantSolution, error) { - process := func(invariants []interfaces.Invariant) ([]interfaces.Invariant, []*interfaces.ExclusiveInvariant, error) { - equalities := []interfaces.Invariant{} - exclusives := []*interfaces.ExclusiveInvariant{} - generators := []interfaces.Invariant{} - - for ix := 0; len(invariants) > ix; ix++ { // while - x := invariants[ix] - switch invariant := x.(type) { - case *interfaces.EqualsInvariant: - equalities = append(equalities, invariant) - - case *interfaces.EqualityInvariant: - equalities = append(equalities, invariant) - - case *interfaces.EqualityInvariantList: - // de-construct this list variant into a series - // of equality variants so that our solver can - // be implemented more simply... - if len(invariant.Exprs) < 2 { - return nil, nil, fmt.Errorf("list invariant needs at least two elements") - } - for i := 0; i < len(invariant.Exprs)-1; i++ { - invar := &interfaces.EqualityInvariant{ - Expr1: invariant.Exprs[i], - Expr2: invariant.Exprs[i+1], - } - equalities = append(equalities, invar) - } - - case *interfaces.EqualityWrapListInvariant: - equalities = append(equalities, invariant) - - case *interfaces.EqualityWrapMapInvariant: - equalities = append(equalities, invariant) - - case *interfaces.EqualityWrapStructInvariant: - equalities = append(equalities, invariant) - - case *interfaces.EqualityWrapFuncInvariant: - equalities = append(equalities, invariant) - - case *interfaces.EqualityWrapCallInvariant: - equalities = append(equalities, invariant) - - case *interfaces.GeneratorInvariant: - // these are special, note the different list - generators = append(generators, invariant) - - // contains a list of invariants which this represents - case *interfaces.ConjunctionInvariant: - invariants = append(invariants, invariant.Invariants...) - - case *interfaces.ExclusiveInvariant: - // these are special, note the different list - if len(invariant.Invariants) > 0 { - exclusives = append(exclusives, invariant) - } - - case *interfaces.AnyInvariant: - equalities = append(equalities, invariant) - - case *interfaces.ValueInvariant: - equalities = append(equalities, invariant) - - case *interfaces.CallFuncArgsValueInvariant: - equalities = append(equalities, invariant) - - case *interfaces.SkipInvariant: - // drop it for now - //equalities = append(equalities, invariant) - - default: - return nil, nil, fmt.Errorf("unknown invariant type: %T", x) - } - } - - // optimization: if we have zero generator invariants, we can - // discard the value invariants! - // NOTE: if exclusives do *not* contain nested generators, then - // we don't need to check for exclusives here, and the logic is - // much faster and simpler and can possibly solve more cases... - if len(generators) == 0 && len(exclusives) == 0 { - used := []int{} - for i, x := range equalities { - _, ok1 := x.(*interfaces.ValueInvariant) - _, ok2 := x.(*interfaces.CallFuncArgsValueInvariant) - if !ok1 && !ok2 { - continue - } - used = append(used, i) // mark equality as used up - } - obj.Logf("%s: got %d equalities left after %d used up", Name, len(equalities)-len(used), len(used)) - // delete used equalities, in reverse order to preserve indexing! - for i := len(used) - 1; i >= 0; i-- { - ix := used[i] // delete index that was marked as used! - equalities = append(equalities[:ix], equalities[ix+1:]...) - } - } - - // append the generators at the end - // (they can go in any order, but it's more optimal this way) - equalities = append(equalities, generators...) - - return equalities, exclusives, nil - } - - obj.Logf("%s: invariants:", Name) - for i, x := range invariants { - obj.Logf("invariant(%d): %T: %s", i, x, x) - } - - solved := make(map[interfaces.Expr]*types.Type) - // iterate through all invariants, flattening and sorting the list... - equalities, exclusives, err := process(invariants) - if err != nil { - return nil, err - } - - //skipExprs := make(map[interfaces.Expr]struct{}) - - // XXX: if these partials all shared the same variable definition, would - // it all work??? Maybe we don't even need the first map prefix... - listPartials := make(map[interfaces.Expr]map[interfaces.Expr]*types.Type) - mapPartials := make(map[interfaces.Expr]map[interfaces.Expr]*types.Type) - structPartials := make(map[interfaces.Expr]map[interfaces.Expr]*types.Type) - funcPartials := make(map[interfaces.Expr]map[interfaces.Expr]*types.Type) - callPartials := make(map[interfaces.Expr]map[interfaces.Expr]*types.Type) - - isSolvedFn := func(solved map[interfaces.Expr]*types.Type) (map[interfaces.Expr]struct{}, bool) { - unsolved := make(map[interfaces.Expr]struct{}) - result := true - for _, x := range expected { - if typ, exists := solved[x]; !exists || typ == nil { - result = false - unsolved[x] = struct{}{} - } - } - return unsolved, result - } - - // list all the expr's connected to expr, use pairs as chains - listConnectedFn := func(expr interfaces.Expr, exprs []*interfaces.EqualityInvariant) []interfaces.Expr { - pairsType := unification.Pairs(exprs) - return pairsType.DFS(expr) - } - - // does the equality invariant already exist in the set? order of expr1 - // and expr2 doesn't matter - eqContains := func(eq *interfaces.EqualityInvariant, pairs []*interfaces.EqualityInvariant) bool { - for _, x := range pairs { - if eq.Expr1 == x.Expr1 && eq.Expr2 == x.Expr2 { - return true - } - if eq.Expr1 == x.Expr2 && eq.Expr2 == x.Expr1 { // reverse - return true - } - } - return false - } - - // build a static list that won't get consumed - eqInvariants := []*interfaces.EqualityInvariant{} - fnInvariants := []*interfaces.EqualityWrapFuncInvariant{} - for _, x := range equalities { - if eq, ok := x.(*interfaces.EqualityInvariant); ok { - eqInvariants = append(eqInvariants, eq) - } - - if eq, ok := x.(*interfaces.EqualityWrapFuncInvariant); ok { - fnInvariants = append(fnInvariants, eq) - } - } - - countGenerators := func() (int, int) { - active := 0 - total := 0 - for _, x := range equalities { - gen, ok := x.(*interfaces.GeneratorInvariant) - if !ok { - continue - } - total++ // total count - if gen.Inactive { - continue // skip inactive - } - active++ // active - } - return total, active - } - activeGenerators := func() int { - _, active := countGenerators() - return active - } - - obj.Logf("%s: starting loop with %d equalities", Name, len(equalities)) - // run until we're solved, stop consuming equalities, or type clash -Loop: - for { - select { - case <-ctx.Done(): - return nil, ctx.Err() - default: - // pass - } - // Once we're done solving everything else except the generators - // then we can exit, but we want to make sure the generators had - // a chance to "speak up" and make sure they were part of Unify. - // Every generator gets to run once, and if that does not change - // the result, then we mark it as inactive. - - obj.Logf("%s: iterate...", Name) - if len(equalities) == 0 && len(exclusives) == 0 && activeGenerators() == 0 { - break // we're done, nothing left - } - used := []int{} - for eqi := 0; eqi < len(equalities); eqi++ { - eqx := equalities[eqi] - obj.Logf("%s: match(%T): %+v", Name, eqx, eqx) - - // TODO: could each of these cases be implemented as a - // method on the Invariant type to simplify this code? - switch eq := eqx.(type) { - // trivials - case *interfaces.EqualsInvariant: - typ, exists := solved[eq.Expr] - if !exists { - solved[eq.Expr] = eq.Type // yay, we learned something! - used = append(used, eqi) // mark equality as used up - obj.Logf("%s: solved trivial equality", Name) - continue - } - // we already specified this, so check the repeat is consistent - if err := typ.Cmp(eq.Type); err != nil { - // this error shouldn't happen unless we purposefully - // try to trick the solver, or we're in a recursive try - return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with equals") - } - used = append(used, eqi) // mark equality as duplicate - obj.Logf("%s: duplicate trivial equality", Name) - continue - - // partials - case *interfaces.EqualityWrapListInvariant: - if _, exists := listPartials[eq.Expr1]; !exists { - listPartials[eq.Expr1] = make(map[interfaces.Expr]*types.Type) - } - - if typ, exists := solved[eq.Expr1]; exists { - // wow, now known, so tell the partials! - // TODO: this assumes typ is a list, is that guaranteed? - listPartials[eq.Expr1][eq.Expr2Val] = typ.Val - } - - // can we add to partials ? - for _, y := range []interfaces.Expr{eq.Expr2Val} { - typ, exists := solved[y] - if !exists { - continue - } - t, exists := listPartials[eq.Expr1][y] - if !exists { - listPartials[eq.Expr1][y] = typ // learn! - - // Even though this is only a partial learn, we should still add it to the solved information! - if newTyp, exists := solved[y]; !exists { - solved[y] = typ // yay, we learned something! - //used = append(used, i) // mark equality as used up when complete! - obj.Logf("%s: solved partial list val equality", Name) - } else if err := newTyp.Cmp(typ); err != nil { - return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with partial list val equality") - } - - continue - } - if err := t.Cmp(typ); err != nil { - return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with partial list val") - } - } - - // can we solve anything? - var ready = true // assume ready - typ := &types.Type{ - Kind: types.KindList, - } - valTyp, exists := listPartials[eq.Expr1][eq.Expr2Val] - if !exists { - ready = false // nope! - } else { - typ.Val = valTyp // build up typ - } - if ready { - if t, exists := solved[eq.Expr1]; exists { - if err := t.Cmp(typ); err != nil { - return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with list") - } - } - // sub checks - if t, exists := solved[eq.Expr2Val]; exists { - if err := t.Cmp(typ.Val); err != nil { - return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with list val") - } - } - - solved[eq.Expr1] = typ // yay, we learned something! - solved[eq.Expr2Val] = typ.Val // yay, we learned something! - used = append(used, eqi) // mark equality as used up - obj.Logf("%s: solved list wrap partial", Name) - continue - } - - case *interfaces.EqualityWrapMapInvariant: - if _, exists := mapPartials[eq.Expr1]; !exists { - mapPartials[eq.Expr1] = make(map[interfaces.Expr]*types.Type) - } - - if typ, exists := solved[eq.Expr1]; exists { - // wow, now known, so tell the partials! - // TODO: this assumes typ is a map, is that guaranteed? - mapPartials[eq.Expr1][eq.Expr2Key] = typ.Key - mapPartials[eq.Expr1][eq.Expr2Val] = typ.Val - } - - // can we add to partials ? - for _, y := range []interfaces.Expr{eq.Expr2Key, eq.Expr2Val} { - typ, exists := solved[y] - if !exists { - continue - } - t, exists := mapPartials[eq.Expr1][y] - if !exists { - mapPartials[eq.Expr1][y] = typ // learn! - - // Even though this is only a partial learn, we should still add it to the solved information! - if newTyp, exists := solved[y]; !exists { - solved[y] = typ // yay, we learned something! - //used = append(used, i) // mark equality as used up when complete! - obj.Logf("%s: solved partial map key/val equality", Name) - } else if err := newTyp.Cmp(typ); err != nil { - return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with partial map key/val equality") - } - - continue - } - if err := t.Cmp(typ); err != nil { - return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with partial map key/val") - } - } - - // can we solve anything? - var ready = true // assume ready - typ := &types.Type{ - Kind: types.KindMap, - } - keyTyp, exists := mapPartials[eq.Expr1][eq.Expr2Key] - if !exists { - ready = false // nope! - } else { - typ.Key = keyTyp // build up typ - } - valTyp, exists := mapPartials[eq.Expr1][eq.Expr2Val] - if !exists { - ready = false // nope! - } else { - typ.Val = valTyp // build up typ - } - if ready { - if t, exists := solved[eq.Expr1]; exists { - if err := t.Cmp(typ); err != nil { - return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with map") - } - } - // sub checks - if t, exists := solved[eq.Expr2Key]; exists { - if err := t.Cmp(typ.Key); err != nil { - return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with map key") - } - } - if t, exists := solved[eq.Expr2Val]; exists { - if err := t.Cmp(typ.Val); err != nil { - return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with map val") - } - } - - solved[eq.Expr1] = typ // yay, we learned something! - solved[eq.Expr2Key] = typ.Key // yay, we learned something! - solved[eq.Expr2Val] = typ.Val // yay, we learned something! - used = append(used, eqi) // mark equality as used up - obj.Logf("%s: solved map wrap partial", Name) - continue - } - - case *interfaces.EqualityWrapStructInvariant: - if _, exists := structPartials[eq.Expr1]; !exists { - structPartials[eq.Expr1] = make(map[interfaces.Expr]*types.Type) - } - - if typ, exists := solved[eq.Expr1]; exists { - // wow, now known, so tell the partials! - // TODO: this assumes typ is a struct, is that guaranteed? - if len(typ.Ord) != len(eq.Expr2Ord) { - return nil, fmt.Errorf("struct field count differs") - } - for i, name := range eq.Expr2Ord { - expr := eq.Expr2Map[name] // assume key exists - structPartials[eq.Expr1][expr] = typ.Map[typ.Ord[i]] // assume key exists - } - } - - // can we add to partials ? - for name, y := range eq.Expr2Map { - typ, exists := solved[y] - if !exists { - continue - } - t, exists := structPartials[eq.Expr1][y] - if !exists { - structPartials[eq.Expr1][y] = typ // learn! - - // Even though this is only a partial learn, we should still add it to the solved information! - if newTyp, exists := solved[y]; !exists { - solved[y] = typ // yay, we learned something! - //used = append(used, i) // mark equality as used up when complete! - obj.Logf("%s: solved partial struct field equality", Name) - } else if err := newTyp.Cmp(typ); err != nil { - return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with partial struct field equality") - } - - continue - } - if err := t.Cmp(typ); err != nil { - return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with partial struct field: %s", name) - } - } - - // can we solve anything? - var ready = true // assume ready - typ := &types.Type{ - Kind: types.KindStruct, - } - typ.Map = make(map[string]*types.Type) - for name, y := range eq.Expr2Map { - t, exists := structPartials[eq.Expr1][y] - if !exists { - ready = false // nope! - break - } - typ.Map[name] = t // build up typ - } - if ready { - typ.Ord = eq.Expr2Ord // known order - - if t, exists := solved[eq.Expr1]; exists { - if err := t.Cmp(typ); err != nil { - return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with struct") - } - } - // sub checks - for name, y := range eq.Expr2Map { - if t, exists := solved[y]; exists { - if err := t.Cmp(typ.Map[name]); err != nil { - return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with struct field: %s", name) - } - } - } - - solved[eq.Expr1] = typ // yay, we learned something! - // we should add the other expr's in too... - for name, y := range eq.Expr2Map { - solved[y] = typ.Map[name] // yay, we learned something! - } - used = append(used, eqi) // mark equality as used up - obj.Logf("%s: solved struct wrap partial", Name) - continue - } - - case *interfaces.EqualityWrapFuncInvariant: - if _, exists := funcPartials[eq.Expr1]; !exists { - funcPartials[eq.Expr1] = make(map[interfaces.Expr]*types.Type) - } - - if typ, exists := solved[eq.Expr1]; exists { - // wow, now known, so tell the partials! - // TODO: this assumes typ is a func, is that guaranteed? - if len(typ.Ord) != len(eq.Expr2Ord) { - return nil, fmt.Errorf("func arg count differs") - } - for i, name := range eq.Expr2Ord { - expr := eq.Expr2Map[name] // assume key exists - funcPartials[eq.Expr1][expr] = typ.Map[typ.Ord[i]] // assume key exists - } - funcPartials[eq.Expr1][eq.Expr2Out] = typ.Out - } - - // can we add to partials ? - for name, y := range eq.Expr2Map { - typ, exists := solved[y] - if !exists { - continue - } - t, exists := funcPartials[eq.Expr1][y] - if !exists { - funcPartials[eq.Expr1][y] = typ // learn! - - // Even though this is only a partial learn, we should still add it to the solved information! - if newTyp, exists := solved[y]; !exists { - solved[y] = typ // yay, we learned something! - //used = append(used, i) // mark equality as used up when complete! - obj.Logf("%s: solved partial func arg equality", Name) - } else if err := newTyp.Cmp(typ); err != nil { - return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with partial func arg equality") - } - - continue - } - if err := t.Cmp(typ); err != nil { - return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with partial func arg: %s", name) - } - } - for _, y := range []interfaces.Expr{eq.Expr2Out} { - typ, exists := solved[y] - if !exists { - continue - } - t, exists := funcPartials[eq.Expr1][y] - if !exists { - funcPartials[eq.Expr1][y] = typ // learn! - - // Even though this is only a partial learn, we should still add it to the solved information! - if newTyp, exists := solved[y]; !exists { - solved[y] = typ // yay, we learned something! - //used = append(used, i) // mark equality as used up when complete! - obj.Logf("%s: solved partial func return equality", Name) - } else if err := newTyp.Cmp(typ); err != nil { - return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with partial func return equality") - } - - continue - } - if err := t.Cmp(typ); err != nil { - return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with partial func arg") - } - } - - equivs := listConnectedFn(eq.Expr1, eqInvariants) // or equivalent! - if obj.Debug && len(equivs) > 0 { - obj.Logf("%s: equiv %d: %p %+v", Name, len(equivs), eq.Expr1, eq.Expr1) - for i, x := range equivs { - obj.Logf("%s: equiv(%d): %p %+v", Name, i, x, x) - } - } - // This determines if a pointer is equivalent to - // a pointer we're interested to match against. - inEquiv := func(needle interfaces.Expr) bool { - for _, x := range equivs { - if x == needle { - return true - } - } - return false - } - // is there another EqualityWrapFuncInvariant with the same Expr1 pointer? - fnDone := make(map[int]struct{}) - for z, fn := range fnInvariants { - if obj.skipFuncCmp { - break - } - obj.zTotal++ - // XXX: I think we're busy in this loop a lot. - select { - case <-ctx.Done(): - return nil, ctx.Err() - default: - // pass - } - // is this fn.Expr1 related by equivalency graph to eq.Expr1 ? - if (eq.Expr1 != fn.Expr1) && !inEquiv(fn.Expr1) { - if obj.Debug { - obj.Logf("%s: equiv skip: %p %+v", Name, fn.Expr1, fn.Expr1) - } - continue - } - if obj.Debug { - obj.Logf("%s: equiv used: %p %+v", Name, fn.Expr1, fn.Expr1) - } - //if eq.Expr1 != fn.Expr1 { // previously - // continue - //} - // wow they match or are equivalent - - if len(eq.Expr2Ord) != len(fn.Expr2Ord) { - return nil, fmt.Errorf("func arg count differs") - } - for i := range eq.Expr2Ord { - lhsName := eq.Expr2Ord[i] - lhsExpr := eq.Expr2Map[lhsName] // assume key exists - rhsName := fn.Expr2Ord[i] - rhsExpr := fn.Expr2Map[rhsName] // assume key exists - - lhsTyp, lhsExists := solved[lhsExpr] - rhsTyp, rhsExists := solved[rhsExpr] - - // add to eqInvariants if not already there! - // TODO: If this parent func invariant gets solved, - // will being unable to add this later be an issue? - newEq := &interfaces.EqualityInvariant{ - Expr1: lhsExpr, - Expr2: rhsExpr, - } - if !eqContains(newEq, eqInvariants) { - obj.Logf("%s: new equality: %p %+v <-> %p %+v", Name, newEq.Expr1, newEq.Expr1, newEq.Expr2, newEq.Expr2) - eqInvariants = append(eqInvariants, newEq) - // TODO: add it as a generator instead? - equalities = append(equalities, newEq) - fnDone[z] = struct{}{} // XXX: heuristical drop - } - - // both solved or both unsolved we skip - if lhsExists && !rhsExists { // teach rhs - typ, exists := funcPartials[eq.Expr1][rhsExpr] - if !exists { - funcPartials[eq.Expr1][rhsExpr] = lhsTyp // learn! - - // Even though this is only a partial learn, we should still add it to the solved information! - if newTyp, exists := solved[rhsExpr]; !exists { - solved[rhsExpr] = lhsTyp // yay, we learned something! - //used = append(used, i) // mark equality as used up when complete! - obj.Logf("%s: solved partial rhs func arg equality", Name) - fnDone[z] = struct{}{} // XXX: heuristical drop - } else if err := newTyp.Cmp(lhsTyp); err != nil { - return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with partial rhs func arg equality") - } - - continue - } - if err := typ.Cmp(lhsTyp); err != nil { - return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with partial func arg") - } - } - if rhsExists && !lhsExists { // teach lhs - typ, exists := funcPartials[eq.Expr1][lhsExpr] - if !exists { - funcPartials[eq.Expr1][lhsExpr] = rhsTyp // learn! - - // Even though this is only a partial learn, we should still add it to the solved information! - if newTyp, exists := solved[lhsExpr]; !exists { - solved[lhsExpr] = rhsTyp // yay, we learned something! - //used = append(used, i) // mark equality as used up when complete! - obj.Logf("%s: solved partial lhs func arg equality", Name) - fnDone[z] = struct{}{} // XXX: heuristical drop - } else if err := newTyp.Cmp(rhsTyp); err != nil { - return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with partial lhs func arg equality") - } - - continue - } - if err := typ.Cmp(rhsTyp); err != nil { - return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with partial func arg") - } - } - } - - lhsExpr := eq.Expr2Out - rhsExpr := fn.Expr2Out - - lhsTyp, lhsExists := solved[lhsExpr] - rhsTyp, rhsExists := solved[rhsExpr] - - // add to eqInvariants if not already there! - // TODO: If this parent func invariant gets solved, - // will being unable to add this later be an issue? - newEq := &interfaces.EqualityInvariant{ - Expr1: lhsExpr, - Expr2: rhsExpr, - } - if !eqContains(newEq, eqInvariants) { - obj.Logf("%s: new equality: %p %+v <-> %p %+v", Name, newEq.Expr1, newEq.Expr1, newEq.Expr2, newEq.Expr2) - eqInvariants = append(eqInvariants, newEq) - // TODO: add it as a generator instead? - equalities = append(equalities, newEq) - fnDone[z] = struct{}{} // XXX: heuristical drop - } - - // both solved or both unsolved we skip - if lhsExists && !rhsExists { // teach rhs - typ, exists := funcPartials[eq.Expr1][rhsExpr] - if !exists { - funcPartials[eq.Expr1][rhsExpr] = lhsTyp // learn! - - // Even though this is only a partial learn, we should still add it to the solved information! - if newTyp, exists := solved[rhsExpr]; !exists { - solved[rhsExpr] = lhsTyp // yay, we learned something! - //used = append(used, i) // mark equality as used up when complete! - obj.Logf("%s: solved partial rhs func return equality", Name) - fnDone[z] = struct{}{} // XXX: heuristical drop - } else if err := newTyp.Cmp(lhsTyp); err != nil { - return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with partial rhs func return equality") - } - - continue - } - if err := typ.Cmp(lhsTyp); err != nil { - return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with partial func arg") - } - } - if rhsExists && !lhsExists { // teach lhs - typ, exists := funcPartials[eq.Expr1][lhsExpr] - if !exists { - funcPartials[eq.Expr1][lhsExpr] = rhsTyp // learn! - - // Even though this is only a partial learn, we should still add it to the solved information! - if newTyp, exists := solved[lhsExpr]; !exists { - solved[lhsExpr] = rhsTyp // yay, we learned something! - //used = append(used, i) // mark equality as used up when complete! - obj.Logf("%s: solved partial lhs func return equality", Name) - fnDone[z] = struct{}{} // XXX: heuristical drop - } else if err := newTyp.Cmp(rhsTyp); err != nil { - return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with partial lhs func return equality") - } - - continue - } - if err := typ.Cmp(rhsTyp); err != nil { - return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with partial func arg") - } - } - - } // end big slow loop - if obj.heuristicalDrop { - fnDoneList := []int{} - for k := range fnDone { - fnDoneList = append(fnDoneList, k) - } - sort.Ints(fnDoneList) - - for z := len(fnDoneList) - 1; z >= 0; z-- { - zx := fnDoneList[z] // delete index that was marked as done! - fnInvariants = append(fnInvariants[:zx], fnInvariants[zx+1:]...) - if obj.Debug { - obj.Logf("zTotal: %d, had: %d, removing: %d", obj.zTotal, len(fnInvariants), len(fnDoneList)) - } - } - } - - // can we solve anything? - var ready = true // assume ready - typ := &types.Type{ - Kind: types.KindFunc, - } - typ.Map = make(map[string]*types.Type) - for name, y := range eq.Expr2Map { - t, exists := funcPartials[eq.Expr1][y] - if !exists { - ready = false // nope! - break - } - typ.Map[name] = t // build up typ - } - outTyp, exists := funcPartials[eq.Expr1][eq.Expr2Out] - if !exists { - ready = false // nope! - } else { - typ.Out = outTyp // build up typ - } - if ready { - typ.Ord = eq.Expr2Ord // known order - - if t, exists := solved[eq.Expr1]; exists { - if err := t.Cmp(typ); err != nil { - return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with func") - } - } - // sub checks - for name, y := range eq.Expr2Map { - if t, exists := solved[y]; exists { - if err := t.Cmp(typ.Map[name]); err != nil { - return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with func arg: %s", name) - } - } - } - if t, exists := solved[eq.Expr2Out]; exists { - if err := t.Cmp(typ.Out); err != nil { - return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with func out") - } - } - - solved[eq.Expr1] = typ // yay, we learned something! - // we should add the other expr's in too... - for name, y := range eq.Expr2Map { - solved[y] = typ.Map[name] // yay, we learned something! - } - solved[eq.Expr2Out] = typ.Out // yay, we learned something! - used = append(used, eqi) // mark equality as used up - obj.Logf("%s: solved func wrap partial", Name) - continue - } - - case *interfaces.EqualityWrapCallInvariant: - // the logic is slightly different here, because - // we can only go from the func type to the call - // type as we can't do the reverse determination - if _, exists := callPartials[eq.Expr2Func]; !exists { - callPartials[eq.Expr2Func] = make(map[interfaces.Expr]*types.Type) - } - - if typ, exists := solved[eq.Expr2Func]; exists { - // wow, now known, so tell the partials! - if typ.Kind != types.KindFunc { - return nil, fmt.Errorf("expected: %s, got: %s", types.KindFunc, typ.Kind) - } - callPartials[eq.Expr2Func][eq.Expr1] = typ.Out - } - - typ, ready := callPartials[eq.Expr2Func][eq.Expr1] - if ready { // ready to solve - if t, exists := solved[eq.Expr1]; exists { - if err := t.Cmp(typ); err != nil { - return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with call") - } - } - // sub checks - if t, exists := solved[eq.Expr2Func]; exists { - if err := t.Out.Cmp(typ); err != nil { - return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with call out") - } - } - - solved[eq.Expr1] = typ // yay, we learned something! - used = append(used, eqi) // mark equality as used up - obj.Logf("%s: solved call wrap partial", Name) - continue - } - - // regular matching - case *interfaces.EqualityInvariant: - typ1, exists1 := solved[eq.Expr1] - typ2, exists2 := solved[eq.Expr2] - - if !exists1 && !exists2 { // neither equality connects - // can't learn more from this equality yet - // nothing is known about either side of it - continue - } - if exists1 && exists2 { // both equalities already connect - // both sides are already known-- are they the same? - if err := typ1.Cmp(typ2); err != nil { - return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with equality") - } - used = append(used, eqi) // mark equality as used up - obj.Logf("%s: duplicate regular equality", Name) - continue - } - if exists1 && !exists2 { // first equality already connects - solved[eq.Expr2] = typ1 // yay, we learned something! - used = append(used, eqi) // mark equality as used up - obj.Logf("%s: solved regular equality", Name) - continue - } - if exists2 && !exists1 { // second equality already connects - solved[eq.Expr1] = typ2 // yay, we learned something! - used = append(used, eqi) // mark equality as used up - obj.Logf("%s: solved regular equality", Name) - continue - } - - panic("reached unexpected code") - - case *interfaces.GeneratorInvariant: - // this invariant can generate new ones - - // optimization: we want to run the generators - // last (but before the exclusives) because - // they take longer to run. So as long as we've - // made progress this time around, don't run - // this just yet, there's still time left... - if len(used) > 0 { - continue - } - - // skip if the inactive flag has been set, as it - // won't produce any new (novel) inequalities we - // can use to progress to a result at this time. - if eq.Inactive { - continue - } - - // If this returns nil, we add the invariants - // it returned and we remove it from the list. - // If we error, it's because we don't have any - // new information to provide at this time... - // XXX: should we pass in `invariants` instead? - gi, err := eq.Func(equalities, solved) - if err != nil { - // set the inactive flag of this generator - eq.Inactive = true - continue - } - - eqs, exs, err := process(gi) // process like at the top - if err != nil { - // programming error? - return nil, errwrap.Wrapf(err, "processing error") - } - equalities = append(equalities, eqs...) - exclusives = append(exclusives, exs...) - - used = append(used, eqi) // mark equality as used up - obj.Logf("%s: solved `generator` equality", Name) - // reset all other generator equality "inactive" flags - for _, x := range equalities { - gen, ok := x.(*interfaces.GeneratorInvariant) - if !ok { - continue - } - gen.Inactive = false - } - - continue - - // wtf matching - case *interfaces.AnyInvariant: - // this basically ensures that the expr gets solved - if _, exists := solved[eq.Expr]; exists { - used = append(used, eqi) // mark equality as used up - obj.Logf("%s: solved `any` equality", Name) - } - continue - - case *interfaces.ValueInvariant: - // don't consume these, they're stored in case - // a generator invariant wants to read them... - continue - - case *interfaces.CallFuncArgsValueInvariant: - // don't consume these, they're stored in case - // a generator invariant wants to read them... - continue - - case *interfaces.SkipInvariant: - //skipExprs[eq.Expr] = struct{}{} // save - used = append(used, eqi) // mark equality as used up - continue - - default: - return nil, fmt.Errorf("unknown invariant type: %T", eqx) - } - } // end inner for loop - if len(used) == 0 && activeGenerators() == 0 { - // Looks like we're now ambiguous, but if we have any - // exclusives, recurse into each possibility to see if - // one of them can help solve this! first one wins. Add - // in the exclusive to the current set of equalities! - - // To decrease the problem space, first check if we have - // enough solutions to solve everything. If so, then we - // don't need to solve any exclusives, and instead we - // only need to verify that they don't conflict with the - // found solution, which reduces the search space... - - // Another optimization that can be done before we run - // the combinatorial exclusive solver, is we can look at - // each exclusive, and remove the ones that already - // match, because they don't tell us any new information - // that we don't already know. We can also fail early - // if anything proves we're already inconsistent. - - // These two optimizations turn out to use the exact - // same algorithm and code, so they're combined here... - _, isSolved := isSolvedFn(solved) - if isSolved { - obj.Logf("%s: solved early with %d exclusives left!", Name, len(exclusives)) - } else { - obj.Logf("%s: unsolved with %d exclusives left!", Name, len(exclusives)) - if obj.Debug { - for i, x := range exclusives { - obj.Logf("%s: exclusive(%d) left: %s", Name, i, x) - } - } - } - - total, active := countGenerators() - // we still have work to do for consistency - if active > 0 { - continue Loop - } - - if total > 0 { - return nil, fmt.Errorf("%d unconsumed generators", total) - } - - // check for consistency against remaining invariants - obj.Logf("%s: checking for consistency against %d exclusives...", Name, len(exclusives)) - done := []int{} - for i, invar := range exclusives { - // test each one to see if at least one works - match, err := invar.Matches(solved) - if err != nil { - obj.Logf("exclusive invar failed: %+v", invar) - return nil, errwrap.Wrapf(err, "inconsistent exclusive") - } - if !match { - continue - } - done = append(done, i) - } - obj.Logf("%s: removed %d consistent exclusives...", Name, len(done)) - - // Remove exclusives that matched correctly. - for i := len(done) - 1; i >= 0; i-- { - ix := done[i] // delete index that was marked as done! - exclusives = append(exclusives[:ix], exclusives[ix+1:]...) - } - - // If we removed any exclusives, then we can start over. - if len(done) > 0 { - continue Loop - } - - // If we don't have any exclusives left, then we don't - // need the Value invariants... This logic is the same - // as in process() but it's duplicated here because we - // want it to happen at this stage as well. We can try - // and clean up the duplication and improve the logic. - // NOTE: We should probably check that there aren't any - // generators left in the equalities, but since we have - // already tried to use them up, it is probably safe to - // unblock the solver if it's only ValueInvatiant left. - if len(exclusives) == 0 || isSolved { // either is okay - used := []int{} - for i, x := range equalities { - _, ok1 := x.(*interfaces.ValueInvariant) - _, ok2 := x.(*interfaces.CallFuncArgsValueInvariant) - if !ok1 && !ok2 { - continue - } - used = append(used, i) // mark equality as used up - } - obj.Logf("%s: got %d equalities left after %d value invariants used up", Name, len(equalities)-len(used), len(used)) - // delete used equalities, in reverse order to preserve indexing! - for i := len(used) - 1; i >= 0; i-- { - ix := used[i] // delete index that was marked as used! - equalities = append(equalities[:ix], equalities[ix+1:]...) - } - - if len(used) > 0 { - continue Loop - } - } - - if len(exclusives) == 0 && isSolved { // old generators - used := []int{} - for i, x := range equalities { - _, ok := x.(*interfaces.GeneratorInvariant) - if !ok { - continue - } - used = append(used, i) // mark equality as used up - } - obj.Logf("%s: got %d equalities left after %d generators used up", Name, len(equalities)-len(used), len(used)) - // delete used equalities, in reverse order to preserve indexing! - for i := len(used) - 1; i >= 0; i-- { - ix := used[i] // delete index that was marked as used! - equalities = append(equalities[:ix], equalities[ix+1:]...) - } - - if len(used) > 0 { - continue Loop - } - } - - // what have we learned for sure so far? - partialSolutions := []interfaces.Invariant{} - obj.Logf("%s: %d solved, %d unsolved, and %d exclusives left", Name, len(solved), len(equalities), len(exclusives)) - if len(exclusives) > 0 { - // FIXME: can we do this loop in a deterministic, sorted way? - for expr, typ := range solved { - invar := &interfaces.EqualsInvariant{ - Expr: expr, - Type: typ, - } - partialSolutions = append(partialSolutions, invar) - obj.Logf("%s: solved: %+v", Name, invar) - } - - // also include anything that hasn't been solved yet - for _, x := range equalities { - partialSolutions = append(partialSolutions, x) - obj.Logf("%s: unsolved: %+v", Name, x) - } - } - obj.Logf("%s: solver state:\n%s", Name, unification.DebugSolverState(solved, equalities)) - - // Lastly, we could loop through each exclusive and see - // if it only has a single, easy solution. For example, - // if we know that an exclusive is A or B or C, and that - // B and C are inconsistent, then we can replace the - // exclusive with a single invariant and then run that - // through our solver. We can do this iteratively - // (recursively for accuracy, but in our case via the - // simplify method) so that if we're lucky, we rarely - // need to run the raw exclusive combinatorial solver, - // which is slow. - obj.Logf("%s: attempting to simplify %d exclusives...", Name, len(exclusives)) - - done = []int{} // clear for re-use - simplified := []interfaces.Invariant{} - for i, invar := range exclusives { - // The partialSolutions don't contain any other - // exclusives... We look at each individually. - s, err := invar.Simplify(partialSolutions) // XXX: pass in the solver? - if err != nil { - obj.Logf("exclusive simplification failed: %+v", invar) - continue - } - done = append(done, i) - simplified = append(simplified, s...) - } - obj.Logf("%s: simplified %d exclusives...", Name, len(done)) - - // Remove exclusives that matched correctly. - for i := len(done) - 1; i >= 0; i-- { - ix := done[i] // delete index that was marked as done! - exclusives = append(exclusives[:ix], exclusives[ix+1:]...) - } - - // Add new equalities and exclusives onto state globals. - eqs, exs, err := process(simplified) // process like at the top - if err != nil { - // programming error? - return nil, errwrap.Wrapf(err, "processing error") - } - equalities = append(equalities, eqs...) - exclusives = append(exclusives, exs...) - - // If we removed any exclusives, then we can start over. - if len(done) > 0 { - continue Loop - } - - // TODO: We could try and replace our combinatorial - // exclusive solver with a real SAT solver algorithm. - - if !AllowRecursion || len(exclusives) > RecursionInvariantLimit { - obj.Logf("%s: %d solved, %d unsolved, and %d exclusives left", Name, len(solved), len(equalities), len(exclusives)) - for i, eq := range equalities { - obj.Logf("%s: (%d) equality: %s", Name, i, eq) - } - for i, ex := range exclusives { - obj.Logf("%s: (%d) exclusive: %s", Name, i, ex) - } - - // these can be very slow, so try to avoid them - return nil, fmt.Errorf("only recursive solutions left") - } - - // let's try each combination, one at a time... - for i, ex := range unification.ExclusivesProduct(exclusives) { // [][]interfaces.Invariant - select { - case <-ctx.Done(): - return nil, ctx.Err() - default: - // pass - } - obj.Logf("%s: exclusive(%d):\n%+v", Name, i, ex) - // we could waste a lot of cpu, and start from - // the beginning, but instead we could use the - // list of known solutions found and continue! - // TODO: make sure none of these edit partialSolutions - recursiveInvariants := []interfaces.Invariant{} - recursiveInvariants = append(recursiveInvariants, partialSolutions...) - recursiveInvariants = append(recursiveInvariants, ex...) - // FIXME: implement RecursionDepthLimit - obj.Logf("%s: recursing...", Name) - solution, err := obj.Solve(ctx, recursiveInvariants, expected) - if err != nil { - obj.Logf("%s: recursive solution failed: %+v", Name, err) - continue // no solution found here... - } - // solution found! - obj.Logf("%s: recursive solution found!", Name) - return solution, nil - } - - // TODO: print ambiguity - obj.Logf("%s: ================ ambiguity ================", Name) - unsolved, isSolved := isSolvedFn(solved) - obj.Logf("%s: isSolved: %+v", Name, isSolved) - for _, x := range equalities { - obj.Logf("%s: unsolved equality: %+v", Name, x) - } - for x := range unsolved { - obj.Logf("%s: unsolved expected: (%p) %+v", Name, x, x) - } - for expr, typ := range solved { - obj.Logf("%s: solved: (%p) => %+v", Name, expr, typ) - } - return nil, unification.ErrAmbiguous - } - // delete used equalities, in reverse order to preserve indexing! - for i := len(used) - 1; i >= 0; i-- { - ix := used[i] // delete index that was marked as used! - equalities = append(equalities[:ix], equalities[ix+1:]...) - } - } // end giant for loop - - // build final solution - solutions := []*interfaces.EqualsInvariant{} - // FIXME: can we do this loop in a deterministic, sorted way? - for expr, typ := range solved { - // Don't do this here, or the current Unifier struct machinery - // will see it as a bug. Do it there until we change the API. - //if _, exists := skipExprs[expr]; exists { - // continue - //} - - invar := &interfaces.EqualsInvariant{ - Expr: expr, - Type: typ, - } - solutions = append(solutions, invar) - } - obj.Logf("zTotal: %d", obj.zTotal) - return &unification.InvariantSolution{ - Solutions: solutions, - }, nil -} diff --git a/lang/unification/solvers/simplesolver_test.go b/lang/unification/solvers/simplesolver_test.go deleted file mode 100644 index adbd585f..00000000 --- a/lang/unification/solvers/simplesolver_test.go +++ /dev/null @@ -1,352 +0,0 @@ -// Mgmt -// Copyright (C) 2013-2024+ 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 . -// -// Additional permission under GNU GPL version 3 section 7 -// -// If you modify this program, or any covered work, by linking or combining it -// with embedded mcl code and modules (and that the embedded mcl code and -// modules which link with this program, contain a copy of their source code in -// the authoritative form) containing parts covered by the terms of any other -// license, the licensors of this program grant you additional permission to -// convey the resulting work. Furthermore, the licensors of this program grant -// the original author, James Shubin, additional permission to update this -// additional permission if he deems it necessary to achieve the goals of this -// additional permission. - -//go:build !root - -package solvers - -import ( - "context" - "fmt" - "strings" - "testing" - - "github.com/purpleidea/mgmt/lang/ast" - "github.com/purpleidea/mgmt/lang/interfaces" - "github.com/purpleidea/mgmt/lang/types" - "github.com/purpleidea/mgmt/lang/unification" - "github.com/purpleidea/mgmt/util" -) - -func TestSimpleSolver1(t *testing.T) { - type test struct { // an individual test - name string - invariants []interfaces.Invariant - expected []interfaces.Expr - fail bool - expect map[interfaces.Expr]*types.Type - experr error // expected error if fail == true (nil ignores it) - experrstr string // expected error prefix - } - testCases := []test{} - - { - expr := &ast.ExprStr{V: "hello"} - - invariants := []interfaces.Invariant{ - &interfaces.EqualsInvariant{ - Expr: expr, - Type: types.NewType("str"), - }, - } - - invars, err := expr.Unify() - if err != nil { - panic("bad test") - } - invariants = append(invariants, invars...) - - testCases = append(testCases, test{ - name: "simple str", - invariants: invariants, - expected: []interfaces.Expr{ - expr, - }, - fail: false, - expect: map[interfaces.Expr]*types.Type{ - expr: types.TypeStr, - }, - }) - } - { - expr := &ast.ExprStr{V: "hello"} - - invariants := []interfaces.Invariant{ - &interfaces.EqualsInvariant{ - Expr: expr, - Type: types.NewType("int"), - }, - } - - invars, err := expr.Unify() - if err != nil { - panic("bad test") - } - invariants = append(invariants, invars...) - - testCases = append(testCases, test{ - name: "simple fail", - invariants: invariants, - expected: []interfaces.Expr{ - expr, - }, - fail: true, - //experr: ErrAmbiguous, - }) - } - { - // ?1 = func(x ?2) ?3 - // ?1 = func(arg0 str) ?4 - // ?3 = str # needed since we don't know what the func body is - expr1 := &interfaces.ExprAny{} // ?1 - expr2 := &interfaces.ExprAny{} // ?2 - expr3 := &interfaces.ExprAny{} // ?3 - expr4 := &interfaces.ExprAny{} // ?4 - - arg0 := &interfaces.ExprAny{} // arg0 - - invarA := &interfaces.EqualityWrapFuncInvariant{ - Expr1: expr1, // Expr - Expr2Map: map[string]interfaces.Expr{ // map[string]Expr - "x": expr2, - }, - Expr2Ord: []string{"x"}, // []string - Expr2Out: expr3, // Expr - } - - invarB := &interfaces.EqualityWrapFuncInvariant{ - Expr1: expr1, // Expr - Expr2Map: map[string]interfaces.Expr{ // map[string]Expr - "arg0": arg0, - }, - Expr2Ord: []string{"arg0"}, // []string - Expr2Out: expr4, // Expr - } - - invarC := &interfaces.EqualsInvariant{ - Expr: expr3, - Type: types.NewType("str"), - } - - invarD := &interfaces.EqualsInvariant{ - Expr: arg0, - Type: types.NewType("str"), - } - - testCases = append(testCases, test{ - name: "dual functions", - invariants: []interfaces.Invariant{ - invarA, - invarB, - invarC, - invarD, - }, - expected: []interfaces.Expr{ - expr1, - expr2, - expr3, - expr4, - arg0, - }, - fail: false, - expect: map[interfaces.Expr]*types.Type{ - expr1: types.NewType("func(str) str"), - expr2: types.NewType("str"), - expr3: types.NewType("str"), - expr4: types.NewType("str"), - arg0: types.NewType("str"), - }, - }) - } - { - // even though the arg names are different, it still unifies! - // ?1 = func(x str) ?2 - // ?1 = func(y str) ?3 - // ?3 = str # needed since we don't know what the func body is - expr1 := &interfaces.ExprAny{} // ?1 - expr2 := &interfaces.ExprAny{} // ?2 - expr3 := &interfaces.ExprAny{} // ?3 - - argx := &interfaces.ExprAny{} // argx - argy := &interfaces.ExprAny{} // argy - - invarA := &interfaces.EqualityWrapFuncInvariant{ - Expr1: expr1, // Expr - Expr2Map: map[string]interfaces.Expr{ // map[string]Expr - "x": argx, - }, - Expr2Ord: []string{"x"}, // []string - Expr2Out: expr2, // Expr - } - - invarB := &interfaces.EqualityWrapFuncInvariant{ - Expr1: expr1, // Expr - Expr2Map: map[string]interfaces.Expr{ // map[string]Expr - "y": argy, - }, - Expr2Ord: []string{"y"}, // []string - Expr2Out: expr3, // Expr - } - - invarC := &interfaces.EqualsInvariant{ - Expr: expr3, - Type: types.NewType("str"), - } - - invarD := &interfaces.EqualsInvariant{ - Expr: argx, - Type: types.NewType("str"), - } - - invarE := &interfaces.EqualsInvariant{ - Expr: argy, - Type: types.NewType("str"), - } - - testCases = append(testCases, test{ - name: "different func arg names", - invariants: []interfaces.Invariant{ - invarA, - invarB, - invarC, - invarD, - invarE, - }, - expected: []interfaces.Expr{ - expr1, - expr2, - expr3, - argx, - argy, - }, - fail: false, - expect: map[interfaces.Expr]*types.Type{ - expr1: types.NewType("func(str) str"), - expr2: types.NewType("str"), - expr3: types.NewType("str"), - argx: types.NewType("str"), - argy: types.NewType("str"), - }, - }) - } - - names := []string{} - for index, tc := range testCases { // run all the tests - if tc.name == "" { - t.Errorf("test #%d: not named", index) - continue - } - if util.StrInList(tc.name, names) { - t.Errorf("test #%d: duplicate sub test name of: %s", index, tc.name) - continue - } - names = append(names, tc.name) - t.Run(fmt.Sprintf("test #%d (%s)", index, tc.name), func(t *testing.T) { - invariants, expected, fail, expect, experr, experrstr := tc.invariants, tc.expected, tc.fail, tc.expect, tc.experr, tc.experrstr - - debug := testing.Verbose() - logf := func(format string, v ...interface{}) { - t.Logf(fmt.Sprintf("test #%d", index)+": "+format, v...) - } - - solver, err := unification.LookupDefault() - if err != nil { - t.Errorf("test #%d: FAIL", index) - t.Errorf("test #%d: solver lookup failed with: %+v", index, err) - return - } - init := &unification.Init{ - Debug: debug, - Logf: logf, - } - if err := solver.Init(init); err != nil { - t.Errorf("test #%d: FAIL", index) - t.Errorf("test #%d: solver init failed with: %+v", index, err) - return - } - solution, err := solver.Solve(context.TODO(), invariants, expected) - t.Logf("test #%d: solver completed with: %+v", index, err) - - if !fail && err != nil { - t.Errorf("test #%d: FAIL", index) - t.Errorf("test #%d: solver failed with: %+v", index, err) - return - } - if fail && err == nil { - t.Errorf("test #%d: FAIL", index) - t.Errorf("test #%d: solver passed, expected fail", index) - return - } - if fail && experr != nil && err != experr { // test for specific error! - t.Errorf("test #%d: FAIL", index) - t.Errorf("test #%d: expected fail, got wrong error", index) - t.Errorf("test #%d: got error: %+v", index, err) - t.Errorf("test #%d: exp error: %+v", index, experr) - return - } - - if fail && err != nil { - t.Logf("test #%d: err: %+v", index, err) - } - // test for specific error string! - if fail && experrstr != "" && !strings.HasPrefix(err.Error(), experrstr) { - t.Errorf("test #%d: FAIL", index) - t.Errorf("test #%d: expected fail, got wrong error", index) - t.Errorf("test #%d: got error: %s", index, err.Error()) - t.Errorf("test #%d: exp error: %s", index, experrstr) - return - } - - if expect == nil { // map[interfaces.Expr]*types.Type - return - } - - solutions := solution.Solutions - if debug { - t.Logf("\n\ntest #%d: solutions: %+v\n", index, solutions) - } - - solutionsMap := make(map[interfaces.Expr]*types.Type) - for _, invar := range solutions { - solutionsMap[invar.Expr] = invar.Type - } - - var failed bool - // TODO: do this in sorted order - for expr, exptyp := range expect { - typ, exists := solutionsMap[expr] - if !exists { - t.Errorf("test #%d: solution missing for: %+v", index, expr) - failed = true - break - } - if err := exptyp.Cmp(typ); err != nil { - t.Errorf("test #%d: solutions type cmp failed with: %+v", index, err) - t.Logf("test #%d: got: %+v", index, exptyp) - t.Logf("test #%d: exp: %+v", index, typ) - failed = true - break - } - } - if failed { - return - } - }) - } -} diff --git a/lang/unification/solvers/solvers.go b/lang/unification/solvers/solvers.go index c1c4d109..5036d585 100644 --- a/lang/unification/solvers/solvers.go +++ b/lang/unification/solvers/solvers.go @@ -33,5 +33,6 @@ package solvers import ( // import so the solver registers - _ "github.com/purpleidea/mgmt/lang/unification/simplesolver" + _ "github.com/purpleidea/mgmt/lang/unification/fastsolver" + //_ "github.com/purpleidea/mgmt/lang/unification/simplesolver" ) diff --git a/lang/unification/solvers/unification_test.go b/lang/unification/solvers/unification_test.go index 5da18aad..da1d334c 100644 --- a/lang/unification/solvers/unification_test.go +++ b/lang/unification/solvers/unification_test.go @@ -39,7 +39,7 @@ import ( _ "github.com/purpleidea/mgmt/engine/resources" // import so the resources register "github.com/purpleidea/mgmt/lang/ast" - "github.com/purpleidea/mgmt/lang/funcs" + "github.com/purpleidea/mgmt/lang/funcs/operators" "github.com/purpleidea/mgmt/lang/funcs/vars" "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/types" @@ -217,7 +217,7 @@ func TestUnification1(t *testing.T) { // int64ptr => 13 + 42, //} expr := &ast.ExprCall{ - Name: funcs.OperatorFuncName, + Name: operators.OperatorFuncName, Args: []interfaces.Expr{ &ast.ExprStr{ V: "+", @@ -261,7 +261,7 @@ func TestUnification1(t *testing.T) { // int64ptr => 13 + 42 - 4, //} innerFunc := &ast.ExprCall{ - Name: funcs.OperatorFuncName, + Name: operators.OperatorFuncName, Args: []interfaces.Expr{ &ast.ExprStr{ V: "-", @@ -275,7 +275,7 @@ func TestUnification1(t *testing.T) { }, } expr := &ast.ExprCall{ - Name: funcs.OperatorFuncName, + Name: operators.OperatorFuncName, Args: []interfaces.Expr{ &ast.ExprStr{ V: "+", @@ -317,7 +317,7 @@ func TestUnification1(t *testing.T) { // float32 => -25.38789 + 32.6 + 13.7, //} innerFunc := &ast.ExprCall{ - Name: funcs.OperatorFuncName, + Name: operators.OperatorFuncName, Args: []interfaces.Expr{ &ast.ExprStr{ V: "+", @@ -331,7 +331,7 @@ func TestUnification1(t *testing.T) { }, } expr := &ast.ExprCall{ - Name: funcs.OperatorFuncName, + Name: operators.OperatorFuncName, Args: []interfaces.Expr{ &ast.ExprStr{ V: "+", @@ -374,7 +374,7 @@ func TestUnification1(t *testing.T) { // int64 => $x, //} innerFunc := &ast.ExprCall{ - Name: funcs.OperatorFuncName, + Name: operators.OperatorFuncName, Args: []interfaces.Expr{ &ast.ExprStr{ V: "-", @@ -718,7 +718,7 @@ func TestUnification1(t *testing.T) { name: "typed if expr", ast: stmt, fail: true, - experrstr: "can't unify, invariant illogicality with equality: base kind does not match (Str != Int)", + experrstr: "type/value inconsistent at arg #1 for func `fmt.printf`: str != int", }) } { @@ -771,7 +771,60 @@ func TestUnification1(t *testing.T) { name: "typed var expr", ast: stmt, fail: true, - experrstr: "can't unify, invariant illogicality with equality: base kind does not match (Str != Bool)", + experrstr: "type/value inconsistent at arg #1 for func `fmt.printf`: str != bool", + }) + } + { + //import "fmt" + //$w = true + //$x str = $w # should fail unification + //test "t1" { + // stringptr => fmt.printf("hello %v", $x), + //} + wvar := &ast.ExprBool{V: true} + xvar := &ast.ExprVar{Name: "w"} + xvar.SetType(types.TypeStr) // should fail unification + expr := &ast.ExprCall{ + Name: "fmt.printf", + Args: []interfaces.Expr{ + &ast.ExprStr{ + V: "hello %s", + }, + &ast.ExprVar{ + Name: "x", // the var + }, + }, + } + stmt := &ast.StmtProg{ + Body: []interfaces.Stmt{ + &ast.StmtImport{ + Name: "fmt", + }, + &ast.StmtBind{ + Ident: "w", + Value: wvar, + }, + &ast.StmtBind{ + Ident: "x", // the var + Value: xvar, + }, + &ast.StmtRes{ + Kind: "test", + Name: &ast.ExprStr{V: "t1"}, + Contents: []ast.StmtResContents{ + &ast.StmtResField{ + Field: "anotherstr", + Value: expr, + }, + }, + }, + }, + } + testCases = append(testCases, test{ + name: "typed var expr again", + ast: stmt, + fail: true, + experrstr: "type/value inconsistent at arg #1 for func `fmt.printf`: str != bool", }) } diff --git a/lang/unification/unification.go b/lang/unification/unification.go index 1e68c05d..876603d7 100644 --- a/lang/unification/unification.go +++ b/lang/unification/unification.go @@ -34,8 +34,6 @@ package unification import ( "context" "fmt" - "sort" - "strings" "github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/types" @@ -99,77 +97,28 @@ func (obj *Unifier) Unify(ctx context.Context) error { if obj.Debug { obj.Logf("tree: %+v", obj.AST) } - invariants, err := obj.AST.Unify() + + // This used to take a map[string]*types.Type type context as in/output. + unificationInvariants, err := obj.AST.TypeCheck() // ([]*UnificationInvariant, error) if err != nil { return err } - // build a list of what we think we need to solve for to succeed - exprs := []interfaces.Expr{} - skips := make(map[interfaces.Expr]struct{}) - for _, x := range invariants { - if si, ok := x.(*interfaces.SkipInvariant); ok { - skips[si.Expr] = struct{}{} - continue - } - - exprs = append(exprs, x.ExprList()...) + data := &Data{ + UnificationInvariants: unificationInvariants, } - exprMap := ExprListToExprMap(exprs) // makes searching faster - exprList := ExprMapToExprList(exprMap) // makes it unique (no duplicates) - - solved, err := obj.Solver.Solve(ctx, invariants, exprList) + solved, err := obj.Solver.Solve(ctx, data) // often does union find if err != nil { return err } - // determine what expr's we need to solve for + obj.Logf("found a solution of length: %d", len(solved.Solutions)) if obj.Debug { - obj.Logf("expr count: %d", len(exprList)) - //for _, x := range exprList { - // obj.Logf("> %p (%+v)", x, x) - //} - } - - // XXX: why doesn't `len(exprList)` always == `len(solved.Solutions)` ? - // XXX: is it due to the extra ExprAny ??? I see an extra function sometimes... - - if obj.Debug { - obj.Logf("solutions count: %d", len(solved.Solutions)) - //for _, x := range solved.Solutions { - // obj.Logf("> %p (%+v) -- %s", x.Expr, x.Type, x.Expr.String()) - //} - } - - // Determine that our solver produced a solution for every expr that - // we're interested in. If it didn't, and it didn't error, then it's a - // bug. We check for this because we care about safety, this ensures - // that our AST will get fully populated with the correct types! - for _, x := range solved.Solutions { - delete(exprMap, x.Expr) // remove everything we know about - } - if c := len(exprMap); c > 0 { // if there's anything left, it's bad... - ptrs := []string{} - disp := make(map[string]string) // display hack - for i := range exprMap { - s := fmt.Sprintf("%p", i) // pointer - ptrs = append(ptrs, s) - disp[s] = i.String() + for _, x := range solved.Solutions { + obj.Logf("> %p %s -- %s", x.Expr, x.Type, x.Expr.String()) } - sort.Strings(ptrs) - // programming error! - s := strings.Join(ptrs, ", ") - - obj.Logf("got %d unbound expr's: %s", c, s) - for i, s := range ptrs { - obj.Logf("(%d) %s => %s", i, s, disp[s]) - } - return fmt.Errorf("got %d unbound expr's: %s", c, s) } - if obj.Debug { - obj.Logf("found a solution!") - } // solver has found a solution, apply it... // we're modifying the AST, so code can't error now... for _, x := range solved.Solutions { @@ -177,22 +126,17 @@ func (obj *Unifier) Unify(ctx context.Context) error { // programming error ? return fmt.Errorf("unexpected invalid solution at: %p", x) } - if _, exists := skips[x.Expr]; exists { - continue - } if obj.Debug { obj.Logf("solution: %p => %+v\t(%+v)", x.Expr, x.Type, x.Expr.String()) } // apply this to each AST node if err := x.Expr.SetType(x.Type); err != nil { - // programming error! - // If we error here, it's probably a bug. Likely we - // should have caught something during type unification, - // but it slipped through and the function Build API is - // catching it instead. Try and root cause it to avoid - // leaving any ghosts in the code. - return fmt.Errorf("error setting type: %+v, error: %+v", x.Expr, err) + // SetType calls the Build() API, which functions as a + // "check" step to add additional constraints that were + // not possible during type unification. + // TODO: Improve this error message! + return fmt.Errorf("error setting type: %+v, error: %s", x.Expr, err) } } return nil @@ -201,54 +145,12 @@ func (obj *Unifier) Unify(ctx context.Context) error { // InvariantSolution lists a trivial set of EqualsInvariant mappings so that you // can populate your AST with SetType calls in a simple loop. type InvariantSolution struct { - Solutions []*interfaces.EqualsInvariant // list of trivial solutions for each node + Solutions []*EqualsInvariant // list of trivial solutions for each node } -// ExprList returns the list of valid expressions. This struct is not part of -// the invariant interface, but it implements this anyways. -func (obj *InvariantSolution) ExprList() []interfaces.Expr { - exprs := []interfaces.Expr{} - for _, x := range obj.Solutions { - exprs = append(exprs, x.ExprList()...) - } - return exprs -} - -// ExclusivesProduct returns a list of different products produced from the -// combinatorial product of the list of exclusives. Each ExclusiveInvariant must -// contain between one and more Invariants. This takes every combination of -// Invariants (choosing one from each ExclusiveInvariant) and returns that list. -// In other words, if you have three exclusives, with invariants named (A1, B1), -// (A2), and (A3, B3, C3) you'll get: (A1, A2, A3), (A1, A2, B3), (A1, A2, C3), -// (B1, A2, A3), (B1, A2, B3), (B1, A2, C3) as results for this function call. -func ExclusivesProduct(exclusives []*interfaces.ExclusiveInvariant) [][]interfaces.Invariant { - if len(exclusives) == 0 { - return nil - } - - length := func(i int) int { return len(exclusives[i].Invariants) } - - // NextIx sets ix to the lexicographically next value, - // such that for each i > 0, 0 <= ix[i] < length(i). - NextIx := func(ix []int) { - for i := len(ix) - 1; i >= 0; i-- { - ix[i]++ - if i == 0 || ix[i] < length(i) { - return - } - ix[i] = 0 - } - } - - results := [][]interfaces.Invariant{} - - for ix := make([]int, len(exclusives)); ix[0] < length(0); NextIx(ix) { - x := []interfaces.Invariant{} - for j, k := range ix { - x = append(x, exclusives[j].Invariants[k]) - } - results = append(results, x) - } - - return results +// EqualsInvariant is an invariant that symbolizes that the expression has a +// known type. It is used for producing solutions. +type EqualsInvariant struct { + Expr interfaces.Expr + Type *types.Type } diff --git a/lang/unification/util.go b/lang/unification/util.go deleted file mode 100644 index 69e7a4bd..00000000 --- a/lang/unification/util.go +++ /dev/null @@ -1,136 +0,0 @@ -// Mgmt -// Copyright (C) 2013-2024+ 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 . -// -// Additional permission under GNU GPL version 3 section 7 -// -// If you modify this program, or any covered work, by linking or combining it -// with embedded mcl code and modules (and that the embedded mcl code and -// modules which link with this program, contain a copy of their source code in -// the authoritative form) containing parts covered by the terms of any other -// license, the licensors of this program grant you additional permission to -// convey the resulting work. Furthermore, the licensors of this program grant -// the original author, James Shubin, additional permission to update this -// additional permission if he deems it necessary to achieve the goals of this -// additional permission. - -package unification - -import ( - "github.com/purpleidea/mgmt/lang/interfaces" -) - -// ExprListToExprMap converts a list of expressions to a map that has the unique -// expr pointers as the keys. This is just an alternate representation of the -// same data structure. If you have any duplicate values in your list, they'll -// get removed when stored as a map. -func ExprListToExprMap(exprList []interfaces.Expr) map[interfaces.Expr]struct{} { - exprMap := make(map[interfaces.Expr]struct{}) - for _, x := range exprList { - exprMap[x] = struct{}{} - } - return exprMap -} - -// ExprMapToExprList converts a map of expressions to a list that has the unique -// expr pointers as the values. This is just an alternate representation of the -// same data structure. -func ExprMapToExprList(exprMap map[interfaces.Expr]struct{}) []interfaces.Expr { - exprList := []interfaces.Expr{} - // TODO: sort by pointer address for determinism ? - for x := range exprMap { - exprList = append(exprList, x) - } - return exprList -} - -// UniqueExprList returns a unique list of expressions with no duplicates. It -// does this my converting it to a map and then back. This isn't necessarily the -// most efficient way, and doesn't preserve list ordering. -func UniqueExprList(exprList []interfaces.Expr) []interfaces.Expr { - exprMap := ExprListToExprMap(exprList) - return ExprMapToExprList(exprMap) -} - -// ExprContains is an "in array" function to test for an expr in a slice of -// expressions. -func ExprContains(needle interfaces.Expr, haystack []interfaces.Expr) bool { - for _, v := range haystack { - if needle == v { - return true - } - } - return false -} - -// Pairs is a simple list of pairs of expressions which can be used as a simple -// undirected graph structure, or as a simple list of equalities. -type Pairs []*interfaces.EqualityInvariant - -// Vertices returns the list of vertices that the input expr is directly -// connected to. -func (obj Pairs) Vertices(expr interfaces.Expr) []interfaces.Expr { - m := make(map[interfaces.Expr]struct{}) - for _, x := range obj { - if x.Expr1 == x.Expr2 { // skip circular - continue - } - if x.Expr1 == expr { - m[x.Expr2] = struct{}{} - } - if x.Expr2 == expr { - m[x.Expr1] = struct{}{} - } - } - - out := []interfaces.Expr{} - // FIXME: can we do this loop in a deterministic, sorted way? - for k := range m { - out = append(out, k) - } - - return out -} - -// DFS returns a depth first search for the graph, starting at the input vertex. -func (obj Pairs) DFS(start interfaces.Expr) []interfaces.Expr { - var d []interfaces.Expr // discovered - var s []interfaces.Expr // stack - found := false - for _, x := range obj { // does the start exist? - if x.Expr1 == start || x.Expr2 == start { - found = true - break - } - } - if !found { - return nil // TODO: error - } - v := start - s = append(s, v) - for len(s) > 0 { - v, s = s[len(s)-1], s[:len(s)-1] // s.pop() - - if !ExprContains(v, d) { // if not discovered - d = append(d, v) // label as discovered - - for _, w := range obj.Vertices(v) { - s = append(s, w) - } - } - } - return d -} diff --git a/lang/unification/util/util.go b/lang/unification/util/util.go index f4d16477..8a873165 100644 --- a/lang/unification/util/util.go +++ b/lang/unification/util/util.go @@ -395,8 +395,8 @@ func OccursCheck(elem *types.Elem, typ *types.Type) error { // written based on the realization that something of this shape was needed // after looking at post-unification unification variables and realizing that // the data needed to be "bubbled upwards". It turns out the GHC Haskell has a -// similar function for this which is called "zonk". "zonk" is the name which -// it uses for this transformation, whimsically claiming that "zonk" is named +// similar function for this which is called "zonk". "zonk" is the name which it +// uses for this transformation, whimsically claiming that "zonk" is named // "after the sound it makes". (Thanks to Sam for the fun trivia!) // TODO: Untested alternate version that copies everything if anything changes. func Extract(typ *types.Type) *types.Type {