Skip to content
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

Merged
merged 1 commit into from
Oct 24, 2024

Conversation

yannham
Copy link
Member

@yannham yannham commented Oct 23, 2024

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 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 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 constructor GenericUnifType::Contract, that must be handled separately from concrete types. When converting from a Type to a UnifType, the node TypeF::Flat is converted toGenericUnifType::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 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 to TypeF::Contract, the former being a historical but bad name.

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.
@yannham yannham requested a review from jneem October 23, 2024 17:27
Copy link
Contributor

github-actions bot commented Oct 23, 2024

🐰 Bencher Report

Branch2078/merge
Testbedubuntu-latest

⚠️ WARNING: The following Measure does not have a Threshold. Without a Threshold, no Alerts will ever be generated!

Click here to create a new Threshold
For more information, see the Threshold documentation.
To only post results if a Threshold exists, set the --ci-only-thresholds CLI flag.

Click to view all benchmark results
BenchmarkLatencynanoseconds (ns)
fibonacci 10📈 view plot
⚠️ NO THRESHOLD
486,400.00
foldl arrays 50📈 view plot
⚠️ NO THRESHOLD
1,697,700.00
foldl arrays 500📈 view plot
⚠️ NO THRESHOLD
6,655,300.00
foldr strings 50📈 view plot
⚠️ NO THRESHOLD
7,044,900.00
foldr strings 500📈 view plot
⚠️ NO THRESHOLD
62,522,000.00
generate normal 250📈 view plot
⚠️ NO THRESHOLD
44,732,000.00
generate normal 50📈 view plot
⚠️ NO THRESHOLD
2,083,700.00
generate normal unchecked 1000📈 view plot
⚠️ NO THRESHOLD
3,367,300.00
generate normal unchecked 200📈 view plot
⚠️ NO THRESHOLD
756,840.00
pidigits 100📈 view plot
⚠️ NO THRESHOLD
3,217,500.00
pipe normal 20📈 view plot
⚠️ NO THRESHOLD
1,509,600.00
pipe normal 200📈 view plot
⚠️ NO THRESHOLD
10,409,000.00
product 30📈 view plot
⚠️ NO THRESHOLD
825,310.00
scalar 10📈 view plot
⚠️ NO THRESHOLD
1,519,200.00
sum 30📈 view plot
⚠️ NO THRESHOLD
823,440.00
🐰 View full continuous benchmarking report in Bencher

@@ -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?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👀

Copy link
Member Author

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 🙃

@yannham yannham added this pull request to the merge queue Oct 24, 2024
Merged via the queue into master with commit 6fc81e8 Oct 24, 2024
10 checks passed
@yannham yannham deleted the task/typ-parametrize-by-te branch October 24, 2024 11:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants