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

Experiment: Less duplication, more aggressive let simplification #860

Open
wants to merge 2 commits into
base: scala-2
Choose a base branch
from

Conversation

gsps
Copy link
Contributor

@gsps gsps commented Nov 20, 2020

This PR adds let-bindings in favor of duplicating code in two more places (match desugaring and assertions for ADT field selections). Conversely, it also adds an additional let-simplifications step in InoxEncoder, as encoded functions would so far not benefit from such simplifications and end up in queries to solvers.

@gsps
Copy link
Contributor Author

gsps commented Nov 24, 2020

The change doesn't seem to have any tremendous impact on performance. Integration tests run in roughly the same amount of time, while non-integration tests might be marginally slower.

It seems the sbt test step (i.e. non-integration tests) previously consistently took around 2m30s, while on the two CI runs here it took ~30s longer, but this is obviously not a great benchmark. I manually looked at the newly-introduced verification.simplify timer for the examples run in sbt test and there are only two occasions on which it takes more than 50ms. So I'm not sure where these slowdowns -- if they're real -- even come from.

@samarion Did we ever have a proper CI for verification times?

@samarion
Copy link
Member

No, but I believe the main blocker is performance inconsistency within the SMT solvers. The solvers are typically fairly deterministic but rely on the input SMT-LIB to "seed" their heuristics. Since the Stainless transformation pipeline isn't 100% deterministic (because of non-deterministic ordering of identifier freshening), the performance can vary quite a bit for no good reason.

About these simplifications: I also worked on (and fiddled with) the simplifier infrastructure quite a bit and my main take-away was that making the input simpler for humans (as in less duplication and smaller ASTs) doesn't actually help the SMT solvers that much. IIRC, the only simplifications which really made a difference were

  1. Reducing the exponential growth of unfoldings. There were two main transformations which helped for this, namely
    a. merging function calls which appear in multiple branches (see mergeFunctions in Inox), and
    b. dropping function calls from assertions which aren't in the VC context (but this also somewhat weakens the solving procedure).
  2. Introducing domain-knowledge assertions, e.g. that function calls don't depend on ghost argument values (used in Stainless type encoding IIRC).

@gsps
Copy link
Contributor Author

gsps commented Nov 24, 2020

Thanks, that's very interesting! Viktor actually mentioned something like mergeFunctions the other day, so I shouldn't be surprised to discover that you actually foresaw this :-)

The changes in this PR remove some exponential growth, but at least the tweak for scrutinees probably doesn't apply to many of our (existing) test cases. I did stumble on the issue while working on imperative code, where a simple x.field match { ... } can grow quite dramatically due to Imperative + TypeEncoding + AssertionInjector. In that context it scraped ~8s off a 30s VC, so it seemed quite worthwhile.

Despite this PR not making a significant dent in our CI runtime, I'm still leaning toward merging it, if only to make everyone's life easier in reading the resulting TIP files.

There are, however, some reservations wrt. Streams, since let-binding intermediate expressions drops RecursiveTypes in some places (we drop them in computeType, @jad-hamza pointed out). This seems like a general source of brittleness in the Inox type-system, so I didn't intend to solve it on this PR.

@@ -199,7 +199,8 @@ trait TreeDeconstructor extends inox.ast.TreeDeconstructor {
case s.RecursiveType(id, tps, e) => (Seq(id), Seq(), Seq(e), tps, Seq(), (ids, _, es, ntps, _) => t.RecursiveType(ids(0), ntps, es(0)))
case s.ValueType(tpe) => (Seq(), Seq(), Seq(), Seq(tpe), Seq(), (_, _, _, tps, _) => t.ValueType(tps(0)))
case s.AnnotatedType(tpe, flags) =>
(Seq(), Seq(), Seq(), Seq(tpe), flags, (_, _, _, tps, flags) => t.AnnotatedType(tps(0), flags))
(Seq(), Seq(), Seq(), Seq(tpe), flags,
(_, _, _, tps, flags) => if (flags.nonEmpty) t.AnnotatedType(tps(0), flags) else tps(0))
Copy link
Member

Choose a reason for hiding this comment

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

We usually try not to simplify within the Deconstructor to make sure transformations are as predictable as possible. You could add the corresponding transformation within one of the various simplify procedures.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay, I'll see if I can find some simplification that's already applied anyways. I guess I'd still like to add an assertion in AnnotatedType that ensures we never end up with an empty list of annotations (which is impossible to spot in the pretty-printed tree!).

}

// NOTE(gsps): Keeping the old, code-duplicating behavior here, since index annotations on
// types are not propagated through `expr.getType`, breaking the Streams example.
Copy link
Member

Choose a reason for hiding this comment

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

I guess an alternative would be to replicate part of the type inference logic from the type checker. In the long run, it probably makes more sense to move all the assertion checks there anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You mean add the assertion check for ADTSelector in TypeChecker directly?

Copy link
Member

Choose a reason for hiding this comment

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

Well, all of the assertion checks injected in this class, really. It's not quite as good in terms of separation of concerns, but it makes sense to centralize all the checking.

Using let-bindings with type inference would also be reasonable and possibly not that difficult to implement. Actually, it would be great if we could add a simple locale type inference phrase to Stainless to compensate a bit for the lack of dependent types in Scala.

@vkuncak
Copy link
Collaborator

vkuncak commented Jun 1, 2022

Astonishingly, this is almost merge-able, but I guess it's not clear if we want it?

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.

3 participants