If you had ambiguous code, and specified an invalid type, this could
sneak through and become a runtime error, instead of a compile-time
error. We fix this and add a test.
We forgot to omit looking deeper into private struct fields. I don't
know why we didn't catch this earlier, I can only assume some subtlety
changed, since we've previously used many of the resources this would
fail on. Maybe golang broke some API that they didn't consider stable?
This also adds a new test for this, and ensures each resource can be
inspected too!
This adds a modern type unification algorithm, which drastically
improves performance, particularly for bigger programs.
This required a change to the AST to add TypeCheck methods (for Stmt)
and Infer/Check methods (for Expr). This also changed how the functions
express their invariants, and as a result this was changed as well.
This greatly improves the way we express these invariants, and as a
result it makes adding new polymorphic functions significantly easier.
This also makes error output for the user a lot better in pretty much
all scenarios.
The one downside of this patch is that a good chunk of it is merged in
this giant single commit since it was hard to do it step-wise. That's
not the end of the world.
This couldn't be done without the guidance of Sam who helped me in
explaining, debugging, and writing all the sneaky algorithmic parts and
much more. Thanks again Sam!
Co-authored-by: Samuel Gélineau <gelisam@gmail.com>
This adds the core unification helper functions that do the core work of
solving the invariants. This includes the actual Unify, OccursCheck, and
Extract which is sometimes known as "zonk".
A few other small functions are also included.
Co-authored-by: Samuel Gélineau <gelisam@gmail.com>
When we look at unification variables from two different places, the
default printer will always start numbering them from ?1 and therefore
if we look at two unrelated systems, they might both print as ?1 when
they are in fact different pointers.
We don't collect them all by default since it's usually not necessary
except for debugging, but in those situations, we want a consistent
unification store which we can pass around to get sensible debug output.
This is used for representing a unification variable in our type during
type unification. For example, this allows us to have a [?1] or a
map{?1:[?2]} and so on...
Plumb through the standard context.Context so that a function can be
cancelled if someone requests this. It makes it less awkward to write
simple functions that might depend on io or network access.
Originally, I considered having more than one way to express the meta
param. After thinking about it for longer, it probably makes sense to
have a second meta param if necessary, and to avoid the exclusive.
This removes the exclusive from the res names and edge names. We now
require that the names should be lists of strings, however they can
still be single strings if that can be determined statically.
Programmers should explicitly wrap their variables in a string by
interpolation to force this, or in square brackets to force a list. The
former is generally preferable because it generates a small function
graph since it doesn't need to build a list.
This adds a unification optimizations API, and uses it to optimize the
embedded provisioner. With these turned on, type unification drops from
around 1m45s to 2.5s which is a 40x speedup.
This commit adds the ability to build a standalone provisioning tool.
This is the first useful public mcl code base as well. It is not
perfect, but does serve as a rough starting point to show what is
possible. In the future as the language and the engine evolve, this will
likely get more elegant, and also grow new features.
To build this, run `make clean && GOTAGS='embedded_provisioner' make`.
To run this, run `mgmt provisioner`.
This is a bit of a hack until we improve the GAPI a bit, but will let us
shut down type unification a bit faster if we want to interrupt a long
running operation. Hopefully our future algorthmic performance
improvements will obliviate the need for this to be a common issue.
If we had a single list wrapped in an interpolated string, it could
sneak through type unification, which is not correct. Wrapping a
variable by interpolation in a string, must force it to be a string.
This optimizes the list of type unification invariants that we generate
for the common case where a resource or edge name is known statically.
For one code base this halved the type unification time in half. More
work can be done though!
I had previous good success using this kind of pattern for discussing
distro UID's. Let's put this in as a reminded to see if it's worth
pursuing longer term.
This is a helper function that can generate a bunch of functions from a
struct type. This is most useful when using a CLI args struct for
command line parsing and then storing the values as functions.
An alternative version of this might choose to return all of the values
as a single giant struct.
If we want to use special struct types from our CLI parser, we also need
to be able to both identify, and convert them to our language type and
value representations.
For as long as we don't have fancier types in our language, these should
both be strings. Tests and extensions to these additions are welcome!
If we're importing an embedded module, we need to also include any
possible system scope imports (like functions) that might be using the
same namespace. These might be used by a custom CLI frontend to extend
the code with values from the CLI parsing, for example.