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

No flatten for most backends #5528

Merged

Conversation

keyboardDrummer
Copy link
Member

@keyboardDrummer keyboardDrummer commented Jun 5, 2024

Description

Do not flatten matches for most backends. Such flattening can cause the generated code size to be the square of the input code size. Removing the flattening leads to a huge decrease in the size of the generated C# code inside the Dafny codebase. It goes from ~14K lines to ~7K.

The compilation of nested matches is done as follows:

datatype TX = X(x1arg: TY, x2arg: TY)
datatype TY = Y(yarg: int)
datatype TZ = Z(zarg: TW)
datatype TW = W(warg: int)

method M(a: TX) {
  match a
    case X(Y(b),Z(W(c)) => <body1>
    case r => <body2>
}

Is roughly compiled into

// Same datatypes

method M(a: TX) {
  var unmatched := true;
  if (unmatched && a is X) {
    var x1arg1 = ((X)a).1;
    if (x1 is Y) {
      var b = ((Y)x1arg1).1;
      var x2arg2 := ((X)a).2; 
      if (x2 is Z) {
        var zarg1 := ((Z)x2arg2).1;
        if (x4 is W) {
          var c := ((W)zarg1).1;
          unmatched := false;
          <body1>
        }
      } 
    }
  }
  if (unmatched) {
    var r := a;
    <body2>
  }
}

Caveats

Maintainability

To reduce the required work, Java and Dafny back-ends still compile using flattened matches

Ideally the transformation would be a Dafny-to-Dafny source transformation, instead of a customization of SinglePassCompiler. However, Dafny does not allow using statements in expression contexts, and this is needed for the transformation. I think it would be good to have an intermediate Dafny that does allow this, similar to what @cpitclaudel 's Dafny-in-Dafny compiler allowed, and then to define the rewrite that this PR introduces using a Dafny source translation.

Improvement

For C# we could generate much nicer code, since C# allows declaring new variables inside expressions using x is T xAsType expressions. We could get rid of the nested if statements and the unmatched variable. However, I'll leave this for future work.

How has this been tested?

  • Performance change. No additional tests added.

By submitting this pull request, I confirm that my contribution is made under the terms of the MIT license.

@keyboardDrummer keyboardDrummer changed the title No flatten for all backends No flatten for most backends Jun 5, 2024
@keyboardDrummer keyboardDrummer marked this pull request as ready for review June 5, 2024 11:19
@keyboardDrummer keyboardDrummer enabled auto-merge (squash) June 5, 2024 11:19
@robin-aws
Copy link
Member

Just for my own understanding, why did the original code generation lead to only a quadratic increase in code size? I would think that it would be a factor of the number of other data constructors, and be worse than quadratic for more nested matches, i.e. if the example above had lots of data constructors for each datatype, I'd expect the worst case to be something like O(|TX| * |TZ| * |TW|)

@keyboardDrummer
Copy link
Member Author

keyboardDrummer commented Jun 5, 2024

Just for my own understanding, why did the original code generation lead to only a quadratic increase in code size? I would think that it would be a factor of the number of other data constructors, and be worse than quadratic for more nested matches, i.e. if the example above had lots of data constructors for each datatype, I'd expect the worst case to be something like O(|TX| * |TZ| * |TW|)

I guess if your nested match occurs in a case that gets copied, like a default case, then it could be exponential yes.

But if your nested match occurs in a case that does not get copied, then the nesting has no particular effect.

However, regardless of nesting, for each match in the original program, the amount of flattened cases will be the number of constructors of the datatype, so the size will be #matches * #ofConstructors for a program where all matches match on the same datatype and all constructor are part of that datatype. The C# code that was generated for our Rust back-end was large simply because of this, not because of nested matches.

@@ -12,562 +19,658 @@
namespace Microsoft.Dafny.Compilers {
public abstract partial class SinglePassCodeGenerator {

public virtual void EmitExpr(Expression expr, bool inLetExprBody, ConcreteSyntaxTree wr, ConcreteSyntaxTree wStmts) {
Copy link
Member Author

@keyboardDrummer keyboardDrummer Jun 5, 2024

Choose a reason for hiding this comment

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

Applied a resharper refactoring to turn the if's into a switch statement. No actual changes to this method except for NestedMatchExpr, which now calls into a method.

@@ -11,8 +18,6 @@

namespace Microsoft.Dafny.Compilers {
public abstract partial class SinglePassCodeGenerator {


protected void TrStmt(Statement stmt, ConcreteSyntaxTree wr, ConcreteSyntaxTree wStmts = null) {
Copy link
Member Author

Choose a reason for hiding this comment

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

Applied a resharper refactoring to turn the if's into a switch statement. No actual changes to this method except for NestedMatchStmt, which now calls into a method.

@@ -460,5 +509,162 @@ public abstract partial class SinglePassCodeGenerator {
}
}
}

protected virtual void EmitNestedMatchStmt(NestedMatchStmt match, ConcreteSyntaxTree writer) {
Copy link
Member Author

Choose a reason for hiding this comment

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

This is where the changes in this file start.

// We end with applying the source expression to the delegate we just built
EmitExpr(e.Source, inLetExprBody, wArg, wStmts);
}

protected virtual void EmitNestedMatchExpr(NestedMatchExpr match, bool inLetExprBody, ConcreteSyntaxTree output, ConcreteSyntaxTree wStmts) {
Copy link
Member Author

Choose a reason for hiding this comment

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

This is where the changes in this file start.

Copy link
Member

@MikaelMayer MikaelMayer left a comment

Choose a reason for hiding this comment

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

This looks good to me.

@@ -67,7 +67,7 @@ jobs:

integration-tests:
needs: check-deep-tests
if: always() && (( github.event_name == 'pull_request' && (needs.check-deep-tests.result == 'success' || contains(github.event.pull_request.labels.*.name, 'run-deep-tests'))) || ( github.event_name == 'push' && ( github.ref_name == 'master' || vars.TEST_ON_FORK == 'true' )))
if: always() && (( github.event_name == 'pull_request' && (needs.check-deep-tests.result == 'success' || contains(github.event.pull_request.labels.*.name, 'run-deep-tests') || contains(github.event.pull_request.labels.*.name, 'run-integration-tests'))) || ( github.event_name == 'push' && ( github.ref_name == 'master' || vars.TEST_ON_FORK == 'true' )))
Copy link
Member

Choose a reason for hiding this comment

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

Go ahead there is no copyright on that one :-)

@@ -2668,5 +2669,18 @@ private class ExprLvalue : ILvalue {
AddUnsupported("<i>EmitHaltRecoveryStmt</i>");
}

protected override void EmitNestedMatchExpr(NestedMatchExpr match, bool inLetExprBody, ConcreteSyntaxTree output,
ConcreteSyntaxTree wStmts) {
EmitExpr(match.Flattened, inLetExprBody, output, wStmts);
Copy link
Member

Choose a reason for hiding this comment

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

That's the right way, thanks. I'll see later how we can actually replace that.

@@ -82,8 +82,8 @@ public class RustBackend : DafnyExecutableBackend {
stream.CopyTo(outFile);
});

using (var cargoToml = new FileStream(Path.Combine(targetDirectory, "Cargo.toml"), FileMode.Create, FileAccess.Write)) {
using var cargoTomlWriter = new StreamWriter(cargoToml);
await using (var cargoToml = new FileStream(Path.Combine(targetDirectory, "Cargo.toml"), FileMode.Create, FileAccess.Write)) {
Copy link
Member

Choose a reason for hiding this comment

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

NIce optimization.

@keyboardDrummer keyboardDrummer merged commit 5b048ff into dafny-lang:master Jun 5, 2024
21 checks passed
@keyboardDrummer keyboardDrummer deleted the noFlattenForAllBackends branch June 5, 2024 22:32
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.

None yet

3 participants