Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b00af6926 | ||
|
|
ddf1be653e | ||
|
|
cede7e5ac0 | ||
|
|
964bd8ba61 | ||
|
|
a1db219fd2 | ||
|
|
241be1801b |
2
Makefile
2
Makefile
@@ -63,7 +63,7 @@ SRPM_BASE = $(PROGRAM)-$(VERSION)-$(RELEASE).src.rpm
|
|||||||
RPM = rpmbuild/RPMS/$(PROGRAM)-$(VERSION)-$(RELEASE).$(ARCH).rpm
|
RPM = rpmbuild/RPMS/$(PROGRAM)-$(VERSION)-$(RELEASE).$(ARCH).rpm
|
||||||
USERNAME := $(shell cat ~/.config/copr 2>/dev/null | grep username | awk -F '=' '{print $$2}' | tr -d ' ')
|
USERNAME := $(shell cat ~/.config/copr 2>/dev/null | grep username | awk -F '=' '{print $$2}' | tr -d ' ')
|
||||||
SERVER = 'dl.fedoraproject.org'
|
SERVER = 'dl.fedoraproject.org'
|
||||||
REMOTE_PATH = 'pub/alt/$(USERNAME)/$(PROGRAM)'
|
REMOTE_PATH = '/srv/pub/alt/$(USERNAME)/$(PROGRAM)'
|
||||||
ifneq ($(GOTAGS),)
|
ifneq ($(GOTAGS),)
|
||||||
BUILD_FLAGS = -tags '$(GOTAGS)'
|
BUILD_FLAGS = -tags '$(GOTAGS)'
|
||||||
endif
|
endif
|
||||||
|
|||||||
@@ -86,6 +86,8 @@ type LangArgs struct {
|
|||||||
|
|
||||||
OnlyUnify bool `arg:"--only-unify" help:"stop after type unification"`
|
OnlyUnify bool `arg:"--only-unify" help:"stop after type unification"`
|
||||||
SkipUnify bool `arg:"--skip-unify" help:"skip type unification"`
|
SkipUnify bool `arg:"--skip-unify" help:"skip type unification"`
|
||||||
|
UnifySolver *string `arg:"--unify-name" help:"pick a specific unification solver"`
|
||||||
|
UnifyOptimizations []string `arg:"--unify-optimizations" help:"list of unification optimizations to request (experts only)"`
|
||||||
|
|
||||||
Depth int `arg:"--depth" default:"-1" help:"max recursion depth limit (-1 is unlimited)"`
|
Depth int `arg:"--depth" default:"-1" help:"max recursion depth limit (-1 is unlimited)"`
|
||||||
|
|
||||||
|
|||||||
343
docs/release-notes/0.0.25
Normal file
343
docs/release-notes/0.0.25
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
I've just released version 0.0.25 of mgmt!
|
||||||
|
|
||||||
|
> 686 files changed, 28391 insertions(+), 6935 deletions(-)
|
||||||
|
|
||||||
|
This is the first release that I consider to be generally useful at
|
||||||
|
solving real-world problems, without needing to be an mgmt expert. It's
|
||||||
|
also the first release that includes a very real `mcl` codebase. An
|
||||||
|
accompanying blog post is also available:
|
||||||
|
https://purpleidea.com/blog/2024/03/27/a-new-provisioning-tool/
|
||||||
|
|
||||||
|
With that, here are a few highlights from the release:
|
||||||
|
|
||||||
|
* We have a new mgmt partner program. Please sign-up for early access
|
||||||
|
to these release notes, along with other special privileges. Details
|
||||||
|
at: https://bit.ly/mgmt-partner-program
|
||||||
|
|
||||||
|
* You can build self-contained mgmt binaries that contain a custom
|
||||||
|
application. An initial "provisioning tool" has been built in this way.
|
||||||
|
Please see the blog post for more details.
|
||||||
|
|
||||||
|
* Default lookup functions exist in the language, with syntactic sugar
|
||||||
|
(the || operator) , so you can get a default value if one doesn't
|
||||||
|
exist, eg: $some_struct->some_struct_field || "some_default_value".
|
||||||
|
|
||||||
|
* Resource fields can now accept interface{} (any) types.
|
||||||
|
|
||||||
|
* A panic feature now exists in the language.
|
||||||
|
|
||||||
|
* The exec resource has new `donecmd` and `creates` fields. Of note,
|
||||||
|
`creates` supports watches too!
|
||||||
|
|
||||||
|
* Send/recv now works for autogrouped resources!
|
||||||
|
|
||||||
|
* Added `include as` (for classes) to the language. Nested
|
||||||
|
(sugar/hierarchical) classes are now supported to make this more
|
||||||
|
powerful!
|
||||||
|
|
||||||
|
* Stats are printed if the function engine is waiting for too long.
|
||||||
|
|
||||||
|
* There's a new http:flag resource, and also an http:proxy resource so
|
||||||
|
that we can have caching http proxies!
|
||||||
|
|
||||||
|
* Added a firewalld resource for opening ports!
|
||||||
|
|
||||||
|
* Added a dhcp:range resource which is very powerful and has a fun API!
|
||||||
|
|
||||||
|
* Added the "embedded" and "entry" packages, for building standalone
|
||||||
|
tools. This goes perfectly with the new CLI library that we ported
|
||||||
|
everything to.
|
||||||
|
|
||||||
|
And much more...
|
||||||
|
|
||||||
|
|
||||||
|
DOWNLOAD
|
||||||
|
|
||||||
|
Prebuilt binaries are available here for this release:
|
||||||
|
https://github.com/purpleidea/mgmt/releases/tag/0.0.25
|
||||||
|
|
||||||
|
They can also be found on the Fedora mirror:
|
||||||
|
https://dl.fedoraproject.org/pub/alt/purpleidea/mgmt/releases/0.0.25/
|
||||||
|
|
||||||
|
|
||||||
|
NEWS
|
||||||
|
|
||||||
|
* We changed the logical operators in mcl to use well-known English
|
||||||
|
tokens: OR, AND, NOT. (but in lowercase of course)
|
||||||
|
|
||||||
|
* The history function has been temporarily removed from the syntactic
|
||||||
|
core. We'll add it back if we find it's useful to have sugar!
|
||||||
|
|
||||||
|
* A bunch of lexer/parser cleanups and improvements were made.
|
||||||
|
|
||||||
|
* Default lookup functions for lists, maps, and structs have been
|
||||||
|
added. These come with syntactic sugar as mentioned above. (We plan to
|
||||||
|
keep this syntax, but we're open to feedback and changes if they're
|
||||||
|
good.)
|
||||||
|
|
||||||
|
* Resources can accept the interface{} (any) type, although this should
|
||||||
|
be used sparingly.
|
||||||
|
|
||||||
|
* We added a new mcl test suite that checks resource output too!
|
||||||
|
|
||||||
|
* Added a new `value` resource. This is a special resource kind that
|
||||||
|
can be used for building some powerful state machines. Recommended for
|
||||||
|
experienced users only.
|
||||||
|
|
||||||
|
* Improved the golang function generation to allow functions that take
|
||||||
|
[]str, so now we have a bunch more functions (like join) in our stdlib
|
||||||
|
for free.
|
||||||
|
|
||||||
|
* Add some mac address formatting functions. (core/net)
|
||||||
|
|
||||||
|
* Added a panic resource and panic function into the core language.
|
||||||
|
This is useful for safely shutting down a running mcl program to
|
||||||
|
prevent something disastrous or unhandled.
|
||||||
|
|
||||||
|
* Added a `donecmd` field to the exec resource. This runs a command
|
||||||
|
after a successful CheckApply. This replaces the `&& echo foo > done`
|
||||||
|
pattern that you'd see in some code.
|
||||||
|
|
||||||
|
* Added a new internal `local` API that can be used for local machine
|
||||||
|
operations. So far, read/writing/watching values that are stored
|
||||||
|
locally.
|
||||||
|
|
||||||
|
* Added `value` functions which bridge the `value` resource via the
|
||||||
|
`local` API. To be used sparingly!
|
||||||
|
|
||||||
|
* Bumped to golang 1.20, and we'll probably move again before the next
|
||||||
|
release.
|
||||||
|
|
||||||
|
* Allow send/recv with autogrouped resources. This adds many
|
||||||
|
possibilities, in particular with the server style resources.
|
||||||
|
|
||||||
|
* Added a bunch of tests for sneaky corner cases. Some of these were
|
||||||
|
hard to write, but I think they're worth it.
|
||||||
|
|
||||||
|
* ExprBind is now monomorphic! This was a design mistake that we
|
||||||
|
introduced, but have since repaired. We now have far fewer copies
|
||||||
|
running in the function graph, and things are much more efficient. This
|
||||||
|
means lambdas can only have one type when used at two different call
|
||||||
|
sites, which is much more logical, safer, faster and memory efficient.
|
||||||
|
|
||||||
|
* Added an --only-unify option if you want to test your code but not
|
||||||
|
run it.
|
||||||
|
|
||||||
|
* Added a concat function for the common case of interpolation. This
|
||||||
|
makes type unification significantly faster.
|
||||||
|
|
||||||
|
* Eliminated some "benign" races. You might find this commit
|
||||||
|
interesting to read: bc63b7608e84f60bf9d568188814d411a0688738
|
||||||
|
|
||||||
|
* A pgraph bug was found and fixed. A test was added too! It's amazing
|
||||||
|
this was here for so long, it just shows how subtle graph
|
||||||
|
datastructures can be.
|
||||||
|
|
||||||
|
* Added `include as` (for classes) to the language which lets our
|
||||||
|
classes produce values which can then be used elsewhere. I decided this
|
||||||
|
feature would be necessary after writing a bunch of mcl. It does have
|
||||||
|
an extraneous scoping bug, but not anything that causes problems.
|
||||||
|
|
||||||
|
* Nested classes are now supported. This lets you write the equivalent
|
||||||
|
of nested classes, without actually having to nest them! This is not
|
||||||
|
inheritance, but rather a way of handling scope and passing it
|
||||||
|
downwards.
|
||||||
|
|
||||||
|
* Improved the Ordering compiler step to catch a bunch of unhandled
|
||||||
|
bugs. Sam is a genius and was able to figure out some of these using
|
||||||
|
wizardry.
|
||||||
|
|
||||||
|
* Added some convert functions to the mcl package.
|
||||||
|
|
||||||
|
* Allow edges with colons...
|
||||||
|
|
||||||
|
* ...Because we now support a new hierarchical autogrouping algorithm!
|
||||||
|
This let's us have some very powerful resources.
|
||||||
|
|
||||||
|
* ...Like http:*, dhcp:*, and so on, but we could even go deeper!
|
||||||
|
|
||||||
|
* Fixed a super sneaky bug with resource swapping. Due to how we Cmp,
|
||||||
|
this now preserves state more often, and in particular when we need it.
|
||||||
|
I'm fairly certain that some code in a WIP branch of mine was actually
|
||||||
|
blocked because of this issue. Pleased to run into it again, but now
|
||||||
|
with a fix in place!
|
||||||
|
|
||||||
|
* Added an http:flag resource. This let's a `wget` or similar call back
|
||||||
|
to the http:server to kick off an action.
|
||||||
|
|
||||||
|
* The http:flag resource supports directories now.
|
||||||
|
|
||||||
|
* Stats are printed if the function engine is waiting for too long.
|
||||||
|
This is mostly useful for developers who are building new functions and
|
||||||
|
have a bug in their midst!
|
||||||
|
|
||||||
|
* We added a --skip-unify option to prevent the double unification when
|
||||||
|
running locally. When using `mgmt run` to test locally, we type check,
|
||||||
|
and then deploy to ourselves, which then naturally type checks again.
|
||||||
|
This skips the first one, which would be unsafe generally, but is
|
||||||
|
perfectly safe when we're running a single instance.
|
||||||
|
|
||||||
|
* Added a new http:proxy resource, and then tweaked it's API, and then
|
||||||
|
added http streaming. This is an incredibly powerful resource that lets
|
||||||
|
us build a caching http proxy with a single resource. I can't wait to
|
||||||
|
see what else it gets used for. I'm using it for provisioning. It's not
|
||||||
|
performance optimized at the moment as it uses a single mutex for
|
||||||
|
everything, but this could be extended if we wanted to scale this out.
|
||||||
|
|
||||||
|
* Added a ton of measuring/timing of common operations. This confirmed
|
||||||
|
my belief that autoedges were slower than necessary. There are two ways
|
||||||
|
to improve this. We might end up doing either one or both. Autogrouping
|
||||||
|
is currently much faster than needed, so no improvements planned for
|
||||||
|
now!
|
||||||
|
|
||||||
|
* Started to clean up the internal FS API's. It would be really great
|
||||||
|
if the core golang team would add something so we could get rid of the
|
||||||
|
afero external interfaces.
|
||||||
|
|
||||||
|
* Added an "embedded" package to offer API's related to embedded mcl
|
||||||
|
programs! This lets us build standalone binaries which are powered by
|
||||||
|
mcl.
|
||||||
|
|
||||||
|
* Moved to a new CLI (go-arg) library. This has a few downsides, but
|
||||||
|
they are fixable upstream, and this vastly improved our code quality
|
||||||
|
and API's. This needed to happen, what with the mess that was
|
||||||
|
urfave/cli. Look at our diff's, they're really elegant! This let us
|
||||||
|
clean up our lib structs as well!
|
||||||
|
|
||||||
|
* Added an "entry" package to kick-off the embedded API stuff. This
|
||||||
|
uses the new CLI API's that we just built. The end-user now has a
|
||||||
|
really easy time building new tools.
|
||||||
|
|
||||||
|
* Added a bunch of util functions to aid in building certain standalone
|
||||||
|
tools. I'm willing to accept more contributions in this space if
|
||||||
|
they're sane, and related to our general mission. Please ask and then
|
||||||
|
send patches if you're unsure.
|
||||||
|
|
||||||
|
* Added a firewalld resource which makes opening up ports automatic
|
||||||
|
when we need them. Perfect for the standalone laptop use-case.
|
||||||
|
|
||||||
|
* Made type unification cancellable in case you get into a long-running
|
||||||
|
scenario and want to end early.
|
||||||
|
|
||||||
|
* Added a `creates` field to the exec resource. Very useful, and also
|
||||||
|
supports watches! This is very useful for the common uses of exec.
|
||||||
|
|
||||||
|
* Added a dhcp:range resource to offer any number of IP addresses to
|
||||||
|
devices that we don't know the mac addresses of in advance. This makes
|
||||||
|
building a provisioning tool even more ergonomic.
|
||||||
|
|
||||||
|
* Optimized the name invariants since we can usually avoid an exclusive
|
||||||
|
invariant in the common case. This roughly halved the type unification
|
||||||
|
time. More improvements coming too!
|
||||||
|
|
||||||
|
* Caught a sneaky list type that could get through type unification
|
||||||
|
when it was interpolated alone. This now enforces the string invariant
|
||||||
|
when we specify it, which is an important language design distinction.
|
||||||
|
We added tests for this of course too!
|
||||||
|
|
||||||
|
* The "log" package has been entirely refactored and is only visible in
|
||||||
|
one place at the top of the program. Nice! I have a design for a
|
||||||
|
"better logger / user interface" if we ever want to improve on this.
|
||||||
|
|
||||||
|
* Added release targets for standalone binary builds. I also improved
|
||||||
|
the Makefile release magic significantly.
|
||||||
|
|
||||||
|
* Made a lot of small "polish" improvements to various resources.
|
||||||
|
|
||||||
|
* Most interestingly, an embedded provisioner application has been
|
||||||
|
built and made available in full. Please test and share with others.
|
||||||
|
Hopefully this will encourage more interest in the project.
|
||||||
|
|
||||||
|
* We're looking for help writing Amazon, Google, DigitalOcean, Hetzner,
|
||||||
|
etc, resources if anyone is interested, reach out to us. Particularly
|
||||||
|
if there is support from those organizations as well.
|
||||||
|
|
||||||
|
* Many other bug fixes, changes, etc...
|
||||||
|
|
||||||
|
* See the git log for more NEWS, and for anything notable I left out!
|
||||||
|
|
||||||
|
|
||||||
|
BUGS/TODO
|
||||||
|
|
||||||
|
* Function values getting _passed_ to resources doesn't work yet, but
|
||||||
|
it's not a blocker, but it would definitely be useful. We're looking
|
||||||
|
into it.
|
||||||
|
|
||||||
|
* Function graphs are unnecessarily dynamic. We might make them more
|
||||||
|
static so that we don't need as many transactions. This is really a
|
||||||
|
compiler optimization and not a bug, but it's something important we'd
|
||||||
|
like to have.
|
||||||
|
|
||||||
|
* Running two Txn's during the same pause would be really helpful. I'm
|
||||||
|
not sure how much of a performance improvement we'd get from this, but
|
||||||
|
it would sure be interesting to build. If you want to build a fancy
|
||||||
|
synchronization primitive, then let us know! Again this is not a bug.
|
||||||
|
|
||||||
|
* Type unification performance can be improved drastically. I will have
|
||||||
|
to implement the fast algorithm so that we can scale to very large mcl
|
||||||
|
programs. Help is wanted if you are familiar with "unionfind" and/or
|
||||||
|
type unification.
|
||||||
|
|
||||||
|
|
||||||
|
TALKS
|
||||||
|
|
||||||
|
I don't have anything planned until CfgMgmtCamp 2025. If you'd like to
|
||||||
|
book me for a private event, or sponsor my travel for your conference,
|
||||||
|
please let me know.
|
||||||
|
|
||||||
|
I recently gave two talks: one at CfgMgmtCamp 2024, and one at FOSDEM
|
||||||
|
in the golang room. Both are available online and demonstrated an
|
||||||
|
earlier version of the provisioning tool which is fully available
|
||||||
|
today. The talks can be found here: https://purpleidea.com/talks/
|
||||||
|
|
||||||
|
|
||||||
|
PARTNER PROGRAM
|
||||||
|
|
||||||
|
We have a new mgmt partner program which gets you early access to
|
||||||
|
releases, bug fixes, support, and many other goodies. Please sign-up
|
||||||
|
today: https://bit.ly/mgmt-partner-program
|
||||||
|
|
||||||
|
|
||||||
|
MISC
|
||||||
|
|
||||||
|
Our mailing list host (Red Hat) is no longer letting non-Red Hat
|
||||||
|
employees use their infrastructure. We're looking for a new home. I've
|
||||||
|
opened a ticket with Freedesktop. If you have any sway with them or
|
||||||
|
other recommendations, please let me know:
|
||||||
|
https://gitlab.freedesktop.org/freedesktop/freedesktop/-/issues/1082
|
||||||
|
|
||||||
|
We're still looking for new contributors, and there are easy, medium
|
||||||
|
and hard issues available! You're also welcome to suggest your own!
|
||||||
|
Please join us in #mgmtconfig on Libera IRC or Matrix (preferred) and
|
||||||
|
ping us if you'd like help getting started! For details please see:
|
||||||
|
|
||||||
|
https://github.com/purpleidea/mgmt/blob/master/docs/faq.md#how-do-i-con
|
||||||
|
tribute-to-the-project-if-i-dont-know-golang
|
||||||
|
|
||||||
|
Many tagged #mgmtlove issues exist:
|
||||||
|
https://github.com/purpleidea/mgmt/issues?q=is%3Aissue+is%3Aopen+label%
|
||||||
|
3Amgmtlove
|
||||||
|
|
||||||
|
Although asking in IRC/matrix is the best way to find something to work
|
||||||
|
on.
|
||||||
|
|
||||||
|
|
||||||
|
MENTORING
|
||||||
|
|
||||||
|
We offer mentoring for new golang/mgmt hackers who want to get
|
||||||
|
involved. This is fun and friendly! You get to improve your skills,
|
||||||
|
and we get some patches in return. Ping me off-list for details.
|
||||||
|
|
||||||
|
|
||||||
|
THANKS
|
||||||
|
|
||||||
|
Thanks (alphabetically) to everyone who contributed to the latest
|
||||||
|
release:
|
||||||
|
Eng Zer Jun, James Shubin, Oliver Lowe, Samuel Gélineau
|
||||||
|
We had 4 unique committers since 0.0.24, and have had 90 overall.
|
||||||
|
run 'git log 0.0.24..0.0.25' to see what has changed since 0.0.24
|
||||||
|
|
||||||
|
|
||||||
|
Happy hacking,
|
||||||
|
James
|
||||||
|
@purpleidea
|
||||||
@@ -46,6 +46,7 @@ import (
|
|||||||
"github.com/purpleidea/mgmt/lang/embedded"
|
"github.com/purpleidea/mgmt/lang/embedded"
|
||||||
"github.com/purpleidea/mgmt/lang/funcs/simple"
|
"github.com/purpleidea/mgmt/lang/funcs/simple"
|
||||||
"github.com/purpleidea/mgmt/lang/types"
|
"github.com/purpleidea/mgmt/lang/types"
|
||||||
|
"github.com/purpleidea/mgmt/lang/unification/simplesolver" // TODO: remove me!
|
||||||
"github.com/purpleidea/mgmt/util"
|
"github.com/purpleidea/mgmt/util"
|
||||||
"github.com/purpleidea/mgmt/util/errwrap"
|
"github.com/purpleidea/mgmt/util/errwrap"
|
||||||
"github.com/purpleidea/mgmt/util/password"
|
"github.com/purpleidea/mgmt/util/password"
|
||||||
@@ -383,6 +384,12 @@ func (obj *provisioner) Customize(a interface{}) (*cli.RunArgs, error) {
|
|||||||
|
|
||||||
// Make any changes here that we want to...
|
// Make any changes here that we want to...
|
||||||
runArgs.RunLang.SkipUnify = true // speed things up for known good code
|
runArgs.RunLang.SkipUnify = true // speed things up for known good code
|
||||||
|
name := simplesolver.Name
|
||||||
|
// TODO: Remove these optimizations when the solver is faster overall.
|
||||||
|
runArgs.RunLang.UnifySolver = &name
|
||||||
|
runArgs.RunLang.UnifyOptimizations = []string{
|
||||||
|
simplesolver.OptimizationSkipFuncCmp,
|
||||||
|
}
|
||||||
libConfig.TmpPrefix = true
|
libConfig.TmpPrefix = true
|
||||||
libConfig.NoPgp = true
|
libConfig.NoPgp = true
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,9 @@ func init() {
|
|||||||
type GAPI struct {
|
type GAPI struct {
|
||||||
InputURI string // input URI of code file system to run
|
InputURI string // input URI of code file system to run
|
||||||
|
|
||||||
|
// Data is some additional data for the lang struct.
|
||||||
|
Data *lang.Data
|
||||||
|
|
||||||
lang *lang.Lang // lang struct
|
lang *lang.Lang // lang struct
|
||||||
wgRun *sync.WaitGroup
|
wgRun *sync.WaitGroup
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
@@ -261,6 +264,15 @@ func (obj *GAPI) Cli(info *gapi.Info) (*gapi.Deploy, error) {
|
|||||||
return nil, nil // success!
|
return nil, nil // success!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unificationStrategy := make(map[string]string)
|
||||||
|
if name := args.UnifySolver; name != nil && *name != "" {
|
||||||
|
unificationStrategy[unification.StrategyNameKey] = *name
|
||||||
|
}
|
||||||
|
if len(args.UnifyOptimizations) > 0 {
|
||||||
|
// TODO: use a query string parser instead?
|
||||||
|
unificationStrategy[unification.StrategyOptimizationsKey] = strings.Join(args.UnifyOptimizations, ",")
|
||||||
|
}
|
||||||
|
|
||||||
if !args.SkipUnify {
|
if !args.SkipUnify {
|
||||||
// apply type unification
|
// apply type unification
|
||||||
unificationLogf := func(format string, v ...interface{}) {
|
unificationLogf := func(format string, v ...interface{}) {
|
||||||
@@ -269,13 +281,19 @@ func (obj *GAPI) Cli(info *gapi.Info) (*gapi.Deploy, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
logf("running type unification...")
|
logf("running type unification...")
|
||||||
startTime := time.Now()
|
|
||||||
|
solver, err := unification.LookupDefault()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errwrap.Wrapf(err, "could not get default solver")
|
||||||
|
}
|
||||||
unifier := &unification.Unifier{
|
unifier := &unification.Unifier{
|
||||||
AST: iast,
|
AST: iast,
|
||||||
Solver: unification.SimpleInvariantSolverLogger(unificationLogf),
|
Solver: solver,
|
||||||
|
Strategy: unificationStrategy,
|
||||||
Debug: debug,
|
Debug: debug,
|
||||||
Logf: unificationLogf,
|
Logf: unificationLogf,
|
||||||
}
|
}
|
||||||
|
startTime := time.Now()
|
||||||
unifyErr := unifier.Unify(context.TODO())
|
unifyErr := unifier.Unify(context.TODO())
|
||||||
delta := time.Since(startTime)
|
delta := time.Since(startTime)
|
||||||
formatted := delta.String()
|
formatted := delta.String()
|
||||||
@@ -406,8 +424,11 @@ func (obj *GAPI) Cli(info *gapi.Info) (*gapi.Deploy, error) {
|
|||||||
Sema: info.Flags.Sema,
|
Sema: info.Flags.Sema,
|
||||||
GAPI: &GAPI{
|
GAPI: &GAPI{
|
||||||
InputURI: fs.URI(),
|
InputURI: fs.URI(),
|
||||||
|
Data: &lang.Data{
|
||||||
|
UnificationStrategy: unificationStrategy,
|
||||||
// TODO: add properties here...
|
// TODO: add properties here...
|
||||||
},
|
},
|
||||||
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,6 +467,7 @@ func (obj *GAPI) LangInit(ctx context.Context) error {
|
|||||||
Fs: fs,
|
Fs: fs,
|
||||||
FsURI: obj.InputURI,
|
FsURI: obj.InputURI,
|
||||||
Input: input,
|
Input: input,
|
||||||
|
Data: obj.Data,
|
||||||
|
|
||||||
Hostname: obj.data.Hostname,
|
Hostname: obj.data.Hostname,
|
||||||
Local: obj.data.Local,
|
Local: obj.data.Local,
|
||||||
|
|||||||
@@ -458,9 +458,15 @@ func TestAstFunc1(t *testing.T) {
|
|||||||
xlogf := func(format string, v ...interface{}) {
|
xlogf := func(format string, v ...interface{}) {
|
||||||
logf("unification: "+format, v...)
|
logf("unification: "+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
|
||||||
|
}
|
||||||
unifier := &unification.Unifier{
|
unifier := &unification.Unifier{
|
||||||
AST: iast,
|
AST: iast,
|
||||||
Solver: unification.SimpleInvariantSolverLogger(xlogf),
|
Solver: solver,
|
||||||
Debug: testing.Verbose(),
|
Debug: testing.Verbose(),
|
||||||
Logf: xlogf,
|
Logf: xlogf,
|
||||||
}
|
}
|
||||||
@@ -1028,9 +1034,15 @@ func TestAstFunc2(t *testing.T) {
|
|||||||
xlogf := func(format string, v ...interface{}) {
|
xlogf := func(format string, v ...interface{}) {
|
||||||
logf("unification: "+format, v...)
|
logf("unification: "+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
|
||||||
|
}
|
||||||
unifier := &unification.Unifier{
|
unifier := &unification.Unifier{
|
||||||
AST: iast,
|
AST: iast,
|
||||||
Solver: unification.SimpleInvariantSolverLogger(xlogf),
|
Solver: solver,
|
||||||
Debug: testing.Verbose(),
|
Debug: testing.Verbose(),
|
||||||
Logf: xlogf,
|
Logf: xlogf,
|
||||||
}
|
}
|
||||||
@@ -1830,9 +1842,15 @@ func TestAstFunc3(t *testing.T) {
|
|||||||
xlogf := func(format string, v ...interface{}) {
|
xlogf := func(format string, v ...interface{}) {
|
||||||
logf("unification: "+format, v...)
|
logf("unification: "+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
|
||||||
|
}
|
||||||
unifier := &unification.Unifier{
|
unifier := &unification.Unifier{
|
||||||
AST: iast,
|
AST: iast,
|
||||||
Solver: unification.SimpleInvariantSolverLogger(xlogf),
|
Solver: solver,
|
||||||
Debug: testing.Verbose(),
|
Debug: testing.Verbose(),
|
||||||
Logf: xlogf,
|
Logf: xlogf,
|
||||||
}
|
}
|
||||||
|
|||||||
37
lang/lang.go
37
lang/lang.go
@@ -50,6 +50,7 @@ import (
|
|||||||
"github.com/purpleidea/mgmt/lang/interpret"
|
"github.com/purpleidea/mgmt/lang/interpret"
|
||||||
"github.com/purpleidea/mgmt/lang/parser"
|
"github.com/purpleidea/mgmt/lang/parser"
|
||||||
"github.com/purpleidea/mgmt/lang/unification"
|
"github.com/purpleidea/mgmt/lang/unification"
|
||||||
|
_ "github.com/purpleidea/mgmt/lang/unification/solvers" // import so the solvers register
|
||||||
"github.com/purpleidea/mgmt/pgraph"
|
"github.com/purpleidea/mgmt/pgraph"
|
||||||
"github.com/purpleidea/mgmt/util"
|
"github.com/purpleidea/mgmt/util"
|
||||||
"github.com/purpleidea/mgmt/util/errwrap"
|
"github.com/purpleidea/mgmt/util/errwrap"
|
||||||
@@ -63,6 +64,18 @@ const (
|
|||||||
EngineStartupStatsTimeout = 10
|
EngineStartupStatsTimeout = 10
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Data is some data that is passed into the Lang struct. It is presented here
|
||||||
|
// as a single struct with room for multiple fields so that it can be changed or
|
||||||
|
// extended in the future without having to re-plumb through all the fields it
|
||||||
|
// contains
|
||||||
|
type Data struct {
|
||||||
|
// UnificationStrategy is a hack to tune unification performance until
|
||||||
|
// we have an overall cleaner unification algorithm in place.
|
||||||
|
UnificationStrategy map[string]string
|
||||||
|
|
||||||
|
// TODO: Add other fields here if necessary.
|
||||||
|
}
|
||||||
|
|
||||||
// Lang is the main language lexer/parser object.
|
// Lang is the main language lexer/parser object.
|
||||||
type Lang struct {
|
type Lang struct {
|
||||||
Fs engine.Fs // connected fs where input dir or metadata exists
|
Fs engine.Fs // connected fs where input dir or metadata exists
|
||||||
@@ -78,6 +91,9 @@ type Lang struct {
|
|||||||
// run the raw string as mcl code.
|
// run the raw string as mcl code.
|
||||||
Input string
|
Input string
|
||||||
|
|
||||||
|
// Data is some additional data for the lang struct.
|
||||||
|
Data *Data
|
||||||
|
|
||||||
Hostname string
|
Hostname string
|
||||||
Local *local.API
|
Local *local.API
|
||||||
World engine.World
|
World engine.World
|
||||||
@@ -100,6 +116,12 @@ type Lang struct {
|
|||||||
// watching them, *before* we pull their values, that way we'll know if they
|
// watching them, *before* we pull their values, that way we'll know if they
|
||||||
// changed from the values we wanted.
|
// changed from the values we wanted.
|
||||||
func (obj *Lang) Init(ctx context.Context) error {
|
func (obj *Lang) Init(ctx context.Context) error {
|
||||||
|
if obj.Data == nil {
|
||||||
|
return fmt.Errorf("lang struct was not built properly")
|
||||||
|
}
|
||||||
|
if obj.Data.UnificationStrategy == nil {
|
||||||
|
return fmt.Errorf("lang struct was not built properly")
|
||||||
|
}
|
||||||
if obj.Debug {
|
if obj.Debug {
|
||||||
obj.Logf("input: %s", obj.Input)
|
obj.Logf("input: %s", obj.Input)
|
||||||
tree, err := util.FsTree(obj.Fs, "/") // should look like gapi
|
tree, err := util.FsTree(obj.Fs, "/") // should look like gapi
|
||||||
@@ -220,18 +242,29 @@ func (obj *Lang) Init(ctx context.Context) error {
|
|||||||
|
|
||||||
// apply type unification
|
// apply type unification
|
||||||
logf := func(format string, v ...interface{}) {
|
logf := func(format string, v ...interface{}) {
|
||||||
|
// TODO: Remove the masked logger here when unification is clean!
|
||||||
if obj.Debug { // unification only has debug messages...
|
if obj.Debug { // unification only has debug messages...
|
||||||
obj.Logf("unification: "+format, v...)
|
obj.Logf("unification: "+format, v...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
obj.Logf("running type unification...")
|
obj.Logf("running type unification...")
|
||||||
timing = time.Now()
|
|
||||||
|
var solver unification.Solver
|
||||||
|
if name, exists := obj.Data.UnificationStrategy["solver"]; exists && name != "" {
|
||||||
|
if solver, err = unification.Lookup(name); err != nil {
|
||||||
|
return errwrap.Wrapf(err, "could not get solver: %s", name)
|
||||||
|
}
|
||||||
|
} else if solver, err = unification.LookupDefault(); err != nil {
|
||||||
|
return errwrap.Wrapf(err, "could not get default solver")
|
||||||
|
}
|
||||||
unifier := &unification.Unifier{
|
unifier := &unification.Unifier{
|
||||||
AST: obj.ast,
|
AST: obj.ast,
|
||||||
Solver: unification.SimpleInvariantSolverLogger(logf),
|
Solver: solver,
|
||||||
|
Strategy: obj.Data.UnificationStrategy,
|
||||||
Debug: obj.Debug,
|
Debug: obj.Debug,
|
||||||
Logf: logf,
|
Logf: logf,
|
||||||
}
|
}
|
||||||
|
timing = time.Now()
|
||||||
// NOTE: This is the "real" Unify that runs. (This is not for deploy.)
|
// NOTE: This is the "real" Unify that runs. (This is not for deploy.)
|
||||||
unifyErr := unifier.Unify(ctx)
|
unifyErr := unifier.Unify(ctx)
|
||||||
obj.Logf("type unification took: %s", time.Since(timing))
|
obj.Logf("type unification took: %s", time.Since(timing))
|
||||||
|
|||||||
@@ -137,6 +137,9 @@ func runInterpret(t *testing.T, code string) (_ *pgraph.Graph, reterr error) {
|
|||||||
lang := &Lang{
|
lang := &Lang{
|
||||||
Fs: fs,
|
Fs: fs,
|
||||||
Input: "/" + interfaces.MetadataFilename, // start path in fs
|
Input: "/" + interfaces.MetadataFilename, // start path in fs
|
||||||
|
Data: &Data{
|
||||||
|
UnificationStrategy: make(map[string]string), // empty
|
||||||
|
},
|
||||||
Debug: testing.Verbose(), // set via the -test.v flag to `go test`
|
Debug: testing.Verbose(), // set via the -test.v flag to `go test`
|
||||||
Logf: logf,
|
Logf: logf,
|
||||||
}
|
}
|
||||||
|
|||||||
243
lang/unification/interfaces.go
Normal file
243
lang/unification/interfaces.go
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2024+ James Shubin and the project contributors
|
||||||
|
// Written by James Shubin <james@shubin.ca> 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 <https://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
// 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 (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/purpleidea/mgmt/lang/interfaces"
|
||||||
|
"github.com/purpleidea/mgmt/lang/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ErrAmbiguous means we couldn't find a solution, but we weren't
|
||||||
|
// inconsistent.
|
||||||
|
ErrAmbiguous = interfaces.Error("can't unify, no equalities were consumed, we're ambiguous")
|
||||||
|
|
||||||
|
// StrategyNameKey is the string key used when choosing a solver name.
|
||||||
|
StrategyNameKey = "name"
|
||||||
|
|
||||||
|
// StrategyOptimizationsKey is the string key used to tell the solver
|
||||||
|
// about the specific optimizations you'd like to request. The format
|
||||||
|
// can be specific to each solver.
|
||||||
|
StrategyOptimizationsKey = "optimizations"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Init contains some handles that are used to initialize every solver. Each
|
||||||
|
// individual solver can choose to omit using some of the fields.
|
||||||
|
type Init struct {
|
||||||
|
// Strategy is a hack to tune unification performance until we have an
|
||||||
|
// overall cleaner unification algorithm in place.
|
||||||
|
Strategy map[string]string
|
||||||
|
|
||||||
|
Debug bool
|
||||||
|
Logf func(format string, v ...interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Solver is the general interface that any solver needs to implement.
|
||||||
|
type Solver interface {
|
||||||
|
// Init initializes the solver struct before first use.
|
||||||
|
Init(*Init) error
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// registeredSolvers is a global map of all possible unification solvers which
|
||||||
|
// can be used. You should never touch this map directly. Use methods like
|
||||||
|
// Register instead.
|
||||||
|
var registeredSolvers = make(map[string]func() Solver) // must initialize
|
||||||
|
|
||||||
|
// Register takes a solver and its name and makes it available for use. It is
|
||||||
|
// commonly called in the init() method of the solver at program startup. There
|
||||||
|
// is no matching Unregister function.
|
||||||
|
func Register(name string, solver func() Solver) {
|
||||||
|
if _, exists := registeredSolvers[name]; exists {
|
||||||
|
panic(fmt.Sprintf("a solver named %s is already registered", name))
|
||||||
|
}
|
||||||
|
|
||||||
|
//gob.Register(solver())
|
||||||
|
registeredSolvers[name] = solver
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup returns a pointer to the solver's struct.
|
||||||
|
func Lookup(name string) (Solver, error) {
|
||||||
|
solver, exists := registeredSolvers[name]
|
||||||
|
if !exists {
|
||||||
|
return nil, fmt.Errorf("not found")
|
||||||
|
}
|
||||||
|
return solver(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookupDefault attempts to return a "default" solver.
|
||||||
|
func LookupDefault() (Solver, error) {
|
||||||
|
if len(registeredSolvers) == 0 {
|
||||||
|
return nil, fmt.Errorf("no registered solvers")
|
||||||
|
}
|
||||||
|
if len(registeredSolvers) == 1 {
|
||||||
|
for _, solver := range registeredSolvers {
|
||||||
|
return solver(), nil // return the first and only one
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Should we remove this empty string feature?
|
||||||
|
// If one was registered with no name, then use that as the default.
|
||||||
|
if solver, exists := registeredSolvers[""]; exists { // empty name
|
||||||
|
return solver(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -27,25 +27,34 @@
|
|||||||
// additional permission if he deems it necessary to achieve the goals of this
|
// additional permission if he deems it necessary to achieve the goals of this
|
||||||
// additional permission.
|
// additional permission.
|
||||||
|
|
||||||
package unification // TODO: can we put this solver in a sub-package?
|
package simplesolver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/purpleidea/mgmt/lang/interfaces"
|
"github.com/purpleidea/mgmt/lang/interfaces"
|
||||||
"github.com/purpleidea/mgmt/lang/types"
|
"github.com/purpleidea/mgmt/lang/types"
|
||||||
|
"github.com/purpleidea/mgmt/lang/unification"
|
||||||
"github.com/purpleidea/mgmt/util/errwrap"
|
"github.com/purpleidea/mgmt/util/errwrap"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Name is the prefix for our solver log messages.
|
// Name is the prefix for our solver log messages.
|
||||||
Name = "solver: simple"
|
Name = "simple"
|
||||||
|
|
||||||
// ErrAmbiguous means we couldn't find a solution, but we weren't
|
// OptimizationSkipFuncCmp is the magic flag name to include the skip
|
||||||
// inconsistent.
|
// func cmp optimization which can speed up some simple programs. If
|
||||||
ErrAmbiguous = interfaces.Error("can't unify, no equalities were consumed, we're ambiguous")
|
// 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
|
// AllowRecursion specifies whether we're allowed to use the recursive
|
||||||
// solver or not. It uses an absurd amount of memory, and might hang
|
// solver or not. It uses an absurd amount of memory, and might hang
|
||||||
@@ -61,154 +70,63 @@ const (
|
|||||||
RecursionInvariantLimit = 5 // TODO: pick a better value ?
|
RecursionInvariantLimit = 5 // TODO: pick a better value ?
|
||||||
)
|
)
|
||||||
|
|
||||||
// SimpleInvariantSolverLogger is a wrapper which returns a
|
func init() {
|
||||||
// SimpleInvariantSolver with the log parameter of your choice specified. The
|
unification.Register(Name, func() unification.Solver { return &SimpleInvariantSolver{} })
|
||||||
// result satisfies the correct signature for the solver parameter of the
|
|
||||||
// Unification function.
|
|
||||||
// TODO: Get rid of this function and consider just using the struct directly.
|
|
||||||
func SimpleInvariantSolverLogger(logf func(format string, v ...interface{})) func(context.Context, []interfaces.Invariant, []interfaces.Expr) (*InvariantSolution, error) {
|
|
||||||
return func(ctx context.Context, invariants []interfaces.Invariant, expected []interfaces.Expr) (*InvariantSolution, error) {
|
|
||||||
sis := &SimpleInvariantSolver{
|
|
||||||
Debug: false, // TODO: consider plumbing this through
|
|
||||||
Logf: logf,
|
|
||||||
}
|
|
||||||
return sis.Solve(ctx, invariants, expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SimpleInvariantSolver is an iterative invariant solver for AST expressions.
|
// SimpleInvariantSolver is an iterative invariant solver for AST expressions.
|
||||||
// It is intended to be very simple, even if it's computationally inefficient.
|
// 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.
|
// TODO: Move some of the global solver constants into this struct as params.
|
||||||
type SimpleInvariantSolver struct {
|
type SimpleInvariantSolver struct {
|
||||||
|
// Strategy is a series of methodologies to heuristically improve the
|
||||||
|
// solver.
|
||||||
|
Strategy map[string]string
|
||||||
|
|
||||||
Debug bool
|
Debug bool
|
||||||
Logf func(format string, v ...interface{})
|
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.
|
// Solve is the actual solve implementation of the solver.
|
||||||
func (obj *SimpleInvariantSolver) Solve(ctx context.Context, invariants []interfaces.Invariant, expected []interfaces.Expr) (*InvariantSolution, error) {
|
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) {
|
process := func(invariants []interfaces.Invariant) ([]interfaces.Invariant, []*interfaces.ExclusiveInvariant, error) {
|
||||||
equalities := []interfaces.Invariant{}
|
equalities := []interfaces.Invariant{}
|
||||||
exclusives := []*interfaces.ExclusiveInvariant{}
|
exclusives := []*interfaces.ExclusiveInvariant{}
|
||||||
@@ -351,7 +269,7 @@ func (obj *SimpleInvariantSolver) Solve(ctx context.Context, invariants []interf
|
|||||||
|
|
||||||
// list all the expr's connected to expr, use pairs as chains
|
// list all the expr's connected to expr, use pairs as chains
|
||||||
listConnectedFn := func(expr interfaces.Expr, exprs []*interfaces.EqualityInvariant) []interfaces.Expr {
|
listConnectedFn := func(expr interfaces.Expr, exprs []*interfaces.EqualityInvariant) []interfaces.Expr {
|
||||||
pairsType := pairs(exprs)
|
pairsType := unification.Pairs(exprs)
|
||||||
return pairsType.DFS(expr)
|
return pairsType.DFS(expr)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -770,7 +688,12 @@ Loop:
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// is there another EqualityWrapFuncInvariant with the same Expr1 pointer?
|
// is there another EqualityWrapFuncInvariant with the same Expr1 pointer?
|
||||||
for _, fn := range fnInvariants {
|
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.
|
// XXX: I think we're busy in this loop a lot.
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
@@ -817,6 +740,7 @@ Loop:
|
|||||||
eqInvariants = append(eqInvariants, newEq)
|
eqInvariants = append(eqInvariants, newEq)
|
||||||
// TODO: add it as a generator instead?
|
// TODO: add it as a generator instead?
|
||||||
equalities = append(equalities, newEq)
|
equalities = append(equalities, newEq)
|
||||||
|
fnDone[z] = struct{}{} // XXX: heuristical drop
|
||||||
}
|
}
|
||||||
|
|
||||||
// both solved or both unsolved we skip
|
// both solved or both unsolved we skip
|
||||||
@@ -830,6 +754,7 @@ Loop:
|
|||||||
solved[rhsExpr] = lhsTyp // yay, we learned something!
|
solved[rhsExpr] = lhsTyp // yay, we learned something!
|
||||||
//used = append(used, i) // mark equality as used up when complete!
|
//used = append(used, i) // mark equality as used up when complete!
|
||||||
obj.Logf("%s: solved partial rhs func arg equality", Name)
|
obj.Logf("%s: solved partial rhs func arg equality", Name)
|
||||||
|
fnDone[z] = struct{}{} // XXX: heuristical drop
|
||||||
} else if err := newTyp.Cmp(lhsTyp); err != nil {
|
} else if err := newTyp.Cmp(lhsTyp); err != nil {
|
||||||
return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with partial rhs func arg equality")
|
return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with partial rhs func arg equality")
|
||||||
}
|
}
|
||||||
@@ -850,6 +775,7 @@ Loop:
|
|||||||
solved[lhsExpr] = rhsTyp // yay, we learned something!
|
solved[lhsExpr] = rhsTyp // yay, we learned something!
|
||||||
//used = append(used, i) // mark equality as used up when complete!
|
//used = append(used, i) // mark equality as used up when complete!
|
||||||
obj.Logf("%s: solved partial lhs func arg equality", Name)
|
obj.Logf("%s: solved partial lhs func arg equality", Name)
|
||||||
|
fnDone[z] = struct{}{} // XXX: heuristical drop
|
||||||
} else if err := newTyp.Cmp(rhsTyp); err != nil {
|
} else if err := newTyp.Cmp(rhsTyp); err != nil {
|
||||||
return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with partial lhs func arg equality")
|
return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with partial lhs func arg equality")
|
||||||
}
|
}
|
||||||
@@ -880,6 +806,7 @@ Loop:
|
|||||||
eqInvariants = append(eqInvariants, newEq)
|
eqInvariants = append(eqInvariants, newEq)
|
||||||
// TODO: add it as a generator instead?
|
// TODO: add it as a generator instead?
|
||||||
equalities = append(equalities, newEq)
|
equalities = append(equalities, newEq)
|
||||||
|
fnDone[z] = struct{}{} // XXX: heuristical drop
|
||||||
}
|
}
|
||||||
|
|
||||||
// both solved or both unsolved we skip
|
// both solved or both unsolved we skip
|
||||||
@@ -893,6 +820,7 @@ Loop:
|
|||||||
solved[rhsExpr] = lhsTyp // yay, we learned something!
|
solved[rhsExpr] = lhsTyp // yay, we learned something!
|
||||||
//used = append(used, i) // mark equality as used up when complete!
|
//used = append(used, i) // mark equality as used up when complete!
|
||||||
obj.Logf("%s: solved partial rhs func return equality", Name)
|
obj.Logf("%s: solved partial rhs func return equality", Name)
|
||||||
|
fnDone[z] = struct{}{} // XXX: heuristical drop
|
||||||
} else if err := newTyp.Cmp(lhsTyp); err != nil {
|
} else if err := newTyp.Cmp(lhsTyp); err != nil {
|
||||||
return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with partial rhs func return equality")
|
return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with partial rhs func return equality")
|
||||||
}
|
}
|
||||||
@@ -913,6 +841,7 @@ Loop:
|
|||||||
solved[lhsExpr] = rhsTyp // yay, we learned something!
|
solved[lhsExpr] = rhsTyp // yay, we learned something!
|
||||||
//used = append(used, i) // mark equality as used up when complete!
|
//used = append(used, i) // mark equality as used up when complete!
|
||||||
obj.Logf("%s: solved partial lhs func return equality", Name)
|
obj.Logf("%s: solved partial lhs func return equality", Name)
|
||||||
|
fnDone[z] = struct{}{} // XXX: heuristical drop
|
||||||
} else if err := newTyp.Cmp(rhsTyp); err != nil {
|
} else if err := newTyp.Cmp(rhsTyp); err != nil {
|
||||||
return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with partial lhs func return equality")
|
return nil, errwrap.Wrapf(err, "can't unify, invariant illogicality with partial lhs func return equality")
|
||||||
}
|
}
|
||||||
@@ -924,6 +853,21 @@ Loop:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // 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?
|
// can we solve anything?
|
||||||
@@ -1272,7 +1216,7 @@ Loop:
|
|||||||
obj.Logf("%s: unsolved: %+v", Name, x)
|
obj.Logf("%s: unsolved: %+v", Name, x)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
obj.Logf("%s: solver state:\n%s", Name, DebugSolverState(solved, equalities))
|
obj.Logf("%s: solver state:\n%s", Name, unification.DebugSolverState(solved, equalities))
|
||||||
|
|
||||||
// Lastly, we could loop through each exclusive and see
|
// Lastly, we could loop through each exclusive and see
|
||||||
// if it only has a single, easy solution. For example,
|
// if it only has a single, easy solution. For example,
|
||||||
@@ -1338,7 +1282,7 @@ Loop:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// let's try each combination, one at a time...
|
// let's try each combination, one at a time...
|
||||||
for i, ex := range exclusivesProduct(exclusives) { // [][]interfaces.Invariant
|
for i, ex := range unification.ExclusivesProduct(exclusives) { // [][]interfaces.Invariant
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return nil, ctx.Err()
|
return nil, ctx.Err()
|
||||||
@@ -1378,7 +1322,7 @@ Loop:
|
|||||||
for expr, typ := range solved {
|
for expr, typ := range solved {
|
||||||
obj.Logf("%s: solved: (%p) => %+v", Name, expr, typ)
|
obj.Logf("%s: solved: (%p) => %+v", Name, expr, typ)
|
||||||
}
|
}
|
||||||
return nil, ErrAmbiguous
|
return nil, unification.ErrAmbiguous
|
||||||
}
|
}
|
||||||
// delete used equalities, in reverse order to preserve indexing!
|
// delete used equalities, in reverse order to preserve indexing!
|
||||||
for i := len(used) - 1; i >= 0; i-- {
|
for i := len(used) - 1; i >= 0; i-- {
|
||||||
@@ -1403,7 +1347,8 @@ Loop:
|
|||||||
}
|
}
|
||||||
solutions = append(solutions, invar)
|
solutions = append(solutions, invar)
|
||||||
}
|
}
|
||||||
return &InvariantSolution{
|
obj.Logf("zTotal: %d", obj.zTotal)
|
||||||
|
return &unification.InvariantSolution{
|
||||||
Solutions: solutions,
|
Solutions: solutions,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
|
|
||||||
//go:build !root
|
//go:build !root
|
||||||
|
|
||||||
package unification
|
package solvers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -40,6 +40,7 @@ import (
|
|||||||
"github.com/purpleidea/mgmt/lang/ast"
|
"github.com/purpleidea/mgmt/lang/ast"
|
||||||
"github.com/purpleidea/mgmt/lang/interfaces"
|
"github.com/purpleidea/mgmt/lang/interfaces"
|
||||||
"github.com/purpleidea/mgmt/lang/types"
|
"github.com/purpleidea/mgmt/lang/types"
|
||||||
|
"github.com/purpleidea/mgmt/lang/unification"
|
||||||
"github.com/purpleidea/mgmt/util"
|
"github.com/purpleidea/mgmt/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -259,14 +260,27 @@ func TestSimpleSolver1(t *testing.T) {
|
|||||||
t.Run(fmt.Sprintf("test #%d (%s)", index, tc.name), func(t *testing.T) {
|
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
|
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{}) {
|
logf := func(format string, v ...interface{}) {
|
||||||
t.Logf(fmt.Sprintf("test #%d", index)+": "+format, v...)
|
t.Logf(fmt.Sprintf("test #%d", index)+": "+format, v...)
|
||||||
}
|
}
|
||||||
debug := testing.Verbose()
|
|
||||||
|
|
||||||
solver := SimpleInvariantSolverLogger(logf) // generates a solver with built-in logging
|
solver, err := unification.LookupDefault()
|
||||||
|
if err != nil {
|
||||||
solution, err := solver(context.TODO(), invariants, expected)
|
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)
|
t.Logf("test #%d: solver completed with: %+v", index, err)
|
||||||
|
|
||||||
if !fail && err != nil {
|
if !fail && err != nil {
|
||||||
37
lang/unification/solvers/solvers.go
Normal file
37
lang/unification/solvers/solvers.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2024+ James Shubin and the project contributors
|
||||||
|
// Written by James Shubin <james@shubin.ca> 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 <https://www.gnu.org/licenses/>.
|
||||||
|
//
|
||||||
|
// 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 solvers is used to have a central place to import all solvers from.
|
||||||
|
// It is also a good locus to run all of the unification tests from.
|
||||||
|
package solvers
|
||||||
|
|
||||||
|
import (
|
||||||
|
// import so the solver registers
|
||||||
|
_ "github.com/purpleidea/mgmt/lang/unification/simplesolver"
|
||||||
|
)
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
|
|
||||||
//go:build !root
|
//go:build !root
|
||||||
|
|
||||||
package lang // XXX: move this to the unification package
|
package solvers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -37,6 +37,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
_ "github.com/purpleidea/mgmt/engine/resources" // import so the resources register
|
||||||
"github.com/purpleidea/mgmt/lang/ast"
|
"github.com/purpleidea/mgmt/lang/ast"
|
||||||
"github.com/purpleidea/mgmt/lang/funcs"
|
"github.com/purpleidea/mgmt/lang/funcs"
|
||||||
"github.com/purpleidea/mgmt/lang/funcs/vars"
|
"github.com/purpleidea/mgmt/lang/funcs/vars"
|
||||||
@@ -848,13 +849,21 @@ func TestUnification1(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// apply type unification
|
// apply type unification
|
||||||
|
debug := testing.Verbose()
|
||||||
logf := func(format string, v ...interface{}) {
|
logf := func(format string, v ...interface{}) {
|
||||||
t.Logf(fmt.Sprintf("test #%d", index)+": unification: "+format, v...)
|
t.Logf(fmt.Sprintf("test #%d", index)+": unification: "+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
|
||||||
|
}
|
||||||
unifier := &unification.Unifier{
|
unifier := &unification.Unifier{
|
||||||
AST: xast,
|
AST: xast,
|
||||||
Solver: unification.SimpleInvariantSolverLogger(logf),
|
Solver: solver,
|
||||||
Debug: testing.Verbose(),
|
Debug: debug,
|
||||||
Logf: logf,
|
Logf: logf,
|
||||||
}
|
}
|
||||||
err = unifier.Unify(context.TODO())
|
err = unifier.Unify(context.TODO())
|
||||||
@@ -46,8 +46,11 @@ type Unifier struct {
|
|||||||
AST interfaces.Stmt
|
AST interfaces.Stmt
|
||||||
|
|
||||||
// Solver is the solver algorithm implementation to use.
|
// Solver is the solver algorithm implementation to use.
|
||||||
// XXX: Solver should be a solver interface, not a function signature.
|
Solver Solver
|
||||||
Solver func(context.Context, []interfaces.Invariant, []interfaces.Expr) (*InvariantSolution, error)
|
|
||||||
|
// Strategy is a hack to tune unification performance until we have an
|
||||||
|
// overall cleaner unification algorithm in place.
|
||||||
|
Strategy map[string]string
|
||||||
|
|
||||||
Debug bool
|
Debug bool
|
||||||
Logf func(format string, v ...interface{})
|
Logf func(format string, v ...interface{})
|
||||||
@@ -76,6 +79,15 @@ func (obj *Unifier) Unify(ctx context.Context) error {
|
|||||||
return fmt.Errorf("the Logf function is missing")
|
return fmt.Errorf("the Logf function is missing")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init := &Init{
|
||||||
|
Strategy: obj.Strategy,
|
||||||
|
Logf: obj.Logf,
|
||||||
|
Debug: obj.Debug,
|
||||||
|
}
|
||||||
|
if err := obj.Solver.Init(init); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if obj.Debug {
|
if obj.Debug {
|
||||||
obj.Logf("tree: %+v", obj.AST)
|
obj.Logf("tree: %+v", obj.AST)
|
||||||
}
|
}
|
||||||
@@ -98,7 +110,7 @@ func (obj *Unifier) Unify(ctx context.Context) error {
|
|||||||
exprMap := ExprListToExprMap(exprs) // makes searching faster
|
exprMap := ExprListToExprMap(exprs) // makes searching faster
|
||||||
exprList := ExprMapToExprList(exprMap) // makes it unique (no duplicates)
|
exprList := ExprMapToExprList(exprMap) // makes it unique (no duplicates)
|
||||||
|
|
||||||
solved, err := obj.Solver(ctx, invariants, exprList)
|
solved, err := obj.Solver.Solve(ctx, invariants, exprList)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -194,14 +206,14 @@ func (obj *InvariantSolution) ExprList() []interfaces.Expr {
|
|||||||
return exprs
|
return exprs
|
||||||
}
|
}
|
||||||
|
|
||||||
// exclusivesProduct returns a list of different products produced from the
|
// ExclusivesProduct returns a list of different products produced from the
|
||||||
// combinatorial product of the list of exclusives. Each ExclusiveInvariant must
|
// combinatorial product of the list of exclusives. Each ExclusiveInvariant must
|
||||||
// contain between one and more Invariants. This takes every combination of
|
// contain between one and more Invariants. This takes every combination of
|
||||||
// Invariants (choosing one from each ExclusiveInvariant) and returns that list.
|
// Invariants (choosing one from each ExclusiveInvariant) and returns that list.
|
||||||
// In other words, if you have three exclusives, with invariants named (A1, B1),
|
// 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),
|
// (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.
|
// (B1, A2, A3), (B1, A2, B3), (B1, A2, C3) as results for this function call.
|
||||||
func exclusivesProduct(exclusives []*interfaces.ExclusiveInvariant) [][]interfaces.Invariant {
|
func ExclusivesProduct(exclusives []*interfaces.ExclusiveInvariant) [][]interfaces.Invariant {
|
||||||
if len(exclusives) == 0 {
|
if len(exclusives) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,13 +76,13 @@ func ExprContains(needle interfaces.Expr, haystack []interfaces.Expr) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// pairs is a simple list of pairs of expressions which can be used as a simple
|
// 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.
|
// undirected graph structure, or as a simple list of equalities.
|
||||||
type pairs []*interfaces.EqualityInvariant
|
type Pairs []*interfaces.EqualityInvariant
|
||||||
|
|
||||||
// Vertices returns the list of vertices that the input expr is directly
|
// Vertices returns the list of vertices that the input expr is directly
|
||||||
// connected to.
|
// connected to.
|
||||||
func (obj pairs) Vertices(expr interfaces.Expr) []interfaces.Expr {
|
func (obj Pairs) Vertices(expr interfaces.Expr) []interfaces.Expr {
|
||||||
m := make(map[interfaces.Expr]struct{})
|
m := make(map[interfaces.Expr]struct{})
|
||||||
for _, x := range obj {
|
for _, x := range obj {
|
||||||
if x.Expr1 == x.Expr2 { // skip circular
|
if x.Expr1 == x.Expr2 { // skip circular
|
||||||
@@ -106,7 +106,7 @@ func (obj pairs) Vertices(expr interfaces.Expr) []interfaces.Expr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DFS returns a depth first search for the graph, starting at the input vertex.
|
// DFS returns a depth first search for the graph, starting at the input vertex.
|
||||||
func (obj pairs) DFS(start interfaces.Expr) []interfaces.Expr {
|
func (obj Pairs) DFS(start interfaces.Expr) []interfaces.Expr {
|
||||||
var d []interfaces.Expr // discovered
|
var d []interfaces.Expr // discovered
|
||||||
var s []interfaces.Expr // stack
|
var s []interfaces.Expr // stack
|
||||||
found := false
|
found := false
|
||||||
|
|||||||
@@ -126,8 +126,8 @@ func LatestFedoraVersion(ctx context.Context, arch string) (string, error) {
|
|||||||
m := 0
|
m := 0
|
||||||
for _, r := range data {
|
for _, r := range data {
|
||||||
version, err := strconv.Atoi(r.Version)
|
version, err := strconv.Atoi(r.Version)
|
||||||
if err != nil {
|
if err != nil { // skip strings like "40 Beta"
|
||||||
return "", err
|
continue
|
||||||
}
|
}
|
||||||
if arch != "" && arch != r.Arch { // skip others
|
if arch != "" && arch != r.Arch { // skip others
|
||||||
continue
|
continue
|
||||||
|
|||||||
Reference in New Issue
Block a user