-
Notifications
You must be signed in to change notification settings - Fork 90
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add a type parameter to TypeF for contracts #2078
Conversation
The TypeF recursion scheme is parametrized by a number of types: the type of Nickel types, the type of enum rows and the type of record rows. There is one more Rust type that can be stored in a TypeF: a contract. It's currently hardcoded to be a `RichTerm`, which is fine because we never want to use another Rust type to store something else With the development of the experimental bytecode interpreter, we are changing the representation of terms. Because we want to keep mailine Nickel operational during the transition, we have to copy paste all the `typ` module somewhere else, replacing `RichTerm` with the type of the new AST. There is an alternative: as `TypeF` is already parametrized by a bunch of things, why not also parametrize the type of what's stored inside `TypeF::Contract`? This adds a bit more complexity, and requires an additional closure when mapping on or recursing into a `Type`. But the price doesn't seem too big. There is an unexpected benefit: in fact, we do store something else than a `RichTerm`! During typechecking, we need to store an additional environment alongside a contract in order to properly compute contract equality. This led to having an additional constructor `GenericUnifType::Contract`, that must be handled separately than concrete types. When converting from a `Type` to a `UnifType`, the node `TypeF::Flat` is converted to`GenericUnifType::Contract`, and all typechecking functions then assume the invariant that they will never encounter a `TypeF::Flat`. But the Rust type system can't guarantee it. There's also a bespoke runtime error for that (although we're pretty sure it can't happen), a bunch of `debug_assert`, etc. We can in fact kill two birds with one stone: by parametrizing `TypeF`, we can now instantiate it with `(RichTerm, Environment)` for unification types. No need for runtime invariants or ad-hoc treatment anymore. Doing so, we also rename `TypeF::Flat` to `TypeF::Contract`, the latter being a historical but bad name.
Bencher Report
Click to view all benchmark results
|
@@ -1678,15 +1657,16 @@ fn walk_type<V: TypecheckVisitor>( | |||
// Currently, the parser can't generate unbound type variables by construction. Thus we | |||
// don't check here for unbound type variables again. | |||
| TypeF::Var(_) | |||
// An enum type can't contain a flat type inside. | |||
// An enum type can't contain a contract. | |||
// TODO: the assertion above isn't true anymore (ADTs). Need fixing? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👀
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
$ nickel typecheck <<< "[| 'Foo (5 : String) |]"
$ # OK
Yup, needs fixing 🙃
The TypeF recursion scheme is parametrized by a number of (Rust) types: the type of Nickel types, the type of enum rows and the type of record rows. There is one more Rust type that can be stored in a TypeF: a contract. It's currently hardcoded to be a
RichTerm
, which is fine because we never want to use another Rust type to store something elseWith the development of the experimental bytecode interpreter, we are changing the representation of terms. Because we want to keep mailine Nickel operational during the transition, we have to copy paste all the
typ
module somewhere else, replacingRichTerm
with the type of the new AST.There is an alternative: as
TypeF
is already parametrized by a bunch of things, why not also parametrize the type of what's stored insideTypeF::Contract
? This adds a bit more complexity, and requires an additional closure when mapping on or recursing into aType
. But the price doesn't seem too high, and we can reuse the same implementation for both mainline Nickel and bytecode Nickel.There is an unexpected benefit: in fact, we do store something else than a
RichTerm
! During typechecking, we need to store an additional environment alongside a contract in order to properly compute contract equality. This led to having an additional constructorGenericUnifType::Contract
, that must be handled separately from concrete types. When converting from aType
to aUnifType
, the nodeTypeF::Flat
is converted toGenericUnifType::Contract
, and all typechecking functions then assume the invariant that they will never encounter aTypeF::Flat
. But the Rust type system can't guarantee it. There's also a bespoke runtime error for that (although we're pretty sure it can't happen), a bunch ofdebug_assert
, etc.We can thus kill two birds with one stone: by parametrizing
TypeF
, we can now instantiate it with(RichTerm, Environment)
for unification types. No need for runtime invariants or ad-hoc treatment anymore.Doing so, we also rename
TypeF::Flat
toTypeF::Contract
, the former being a historical but bad name.