// Mgmt // Copyright (C) 2013-2023+ James Shubin and the project contributors // Written by James Shubin and the project contributors // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . package interfaces import ( "context" "fmt" "strings" "github.com/purpleidea/mgmt/engine" "github.com/purpleidea/mgmt/lang/types" ) // FuncSig is the simple signature that is used throughout our implementations. type FuncSig = func([]types.Value) (types.Value, error) // 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 // might cause a panic. type Info struct { Pure bool // is the function pure? (can it be memoized?) Memo bool // should the function be memoized? (false if too much output) Slow bool // is the function slow? (avoid speculative execution) Sig *types.Type // the signature of the function, must be KindFunc Err error // is this a valid function, or was it created improperly? } // Init is the structure of values and references which is passed into all // functions on initialization. type Init struct { Hostname string // uuid for the host //Noop bool Input chan types.Value // Engine will close `input` chan Output chan types.Value // Stream must close `output` chan // TODO: should we pass in a *Scope here for functions like template() ? World engine.World Debug bool Logf func(format string, v ...interface{}) } // Func is the interface that any valid func must fulfill. It is very simple, // but still event driven. Funcs should attempt to only send values when they // have changed. // TODO: should we support a static version of this interface for funcs that // never change to avoid the overhead of the goroutine and channel listener? 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! // 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. Info() *Info // Init passes some important values and references to the function. Init(*Init) error // Stream is the mainloop of the function. It reads and writes from // channels to return the changing values that this func has over time. // It should shutdown and cleanup when the input context is cancelled. // It must not exit before any goroutines it spawned have terminated. // It must close the Output chan if it's done sending new values out. It // must send at least one value, or return an error. It may also return // an error at anytime if it can't continue. 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 // 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. // This is needed so that values can flow safely along the DAG that represents // 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 { 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 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.) Build(*types.Type) error // then, you can get argNames from Info() } // 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 { 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 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.) Build(*types.Type) error // then, you can get argNames from Info() } // 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. type NamedArgsFunc interface { Func // implement everything in Func but add the additional requirements // ArgGen implements the arg name generator function. By default, we use // the util.NumToAlpha function when this interface isn't implemented... ArgGen(int) (string, error) } // FuncData is some data that is passed into the function during compilation. It // helps provide some context about the AST and the deploy for functions that // might need it. // TODO: Consider combining this with the existing Data struct or more of it... // TODO: Do we want to add line/col/file values here, and generalize this? type FuncData struct { // Fs represents a handle to the filesystem that we're running on. This // is necessary for opening files if needed by import statements. The // file() paths used to get templates or other files from our deploys // come from here, this is *not* used to interact with the host file // system to manage file resources or other aspects. Fs engine.Fs // FsURI is the fs URI of the active filesystem. This is useful to pass // to the engine.World API for further consumption. FsURI string // Base directory (absolute path) that the running code is in. This is a // copy of the value from the Expr and Stmt Data struct for Init. Base string } // DataFunc is a function that accepts some context from the AST and deploy // before Init and runtime. If you don't wish to accept this data, then don't // implement this method and you won't get any. This is mostly useful for // special functions that are useful in core. // TODO: This could be replaced if a func ever needs a SetScope method... type DataFunc interface { Func // implement everything in Func but add the additional requirements // SetData is used by the language to pass our function some code-level // context. SetData(*FuncData) } // FuncEdge links an output vertex (value) to an input vertex with a named // argument. type FuncEdge struct { Args []string // list of named args that this edge sends to } // String displays the list of arguments this edge satisfies. It is a required // property to be a valid pgraph.Edge. func (obj *FuncEdge) String() string { return strings.Join(obj.Args, ", ") }