Skip to content

[WIP, don’t merge] IR pass: Simplify loop forms into a single, simple form#146

Closed
matthewhammer wants to merge 46 commits intomasterfrom
simploop
Closed

[WIP, don’t merge] IR pass: Simplify loop forms into a single, simple form#146
matthewhammer wants to merge 46 commits intomasterfrom
simploop

Conversation

@matthewhammer
Copy link
Contributor

@matthewhammer matthewhammer commented Feb 5, 2019

Initially, this pass is the identify function, and this description, to make sure I'm heading the right direction. The three transformation cases mentioned below should each be fairly straightforward, given this set up.

Loop transformations

To make the IR "narrower", this pass rewrites the following forms each to LoopE(e', None), for some appropriate e':

  • LoopE(_, Some _)
  • WhileE(_, _)
  • ForE(_,_,_)

To afford the control flow of these special forms, the new loop body e' introduces labels andBreakEs, as needed.

@matthewhammer matthewhammer requested a review from crusso February 5, 2019 23:28
@nomeata
Copy link
Contributor

nomeata commented Feb 5, 2019

If the rewrites are simple enough (for example, if they don’t need to do complex rewriting of subexpressions), they can just happen in desugar.ml, and we can remove the constructors from Ir altogether. Like we do for AndE.

@matthewhammer
Copy link
Contributor Author

I wondered about that. Why include them in the IR at all if the goal is to take them out at some point in the pipeline?

One reason could be that this makes the transformation optional, though that reason doesn't sound very compelling (why do this at all, then?).

@crusso ?

@matthewhammer
Copy link
Contributor Author

In any case, if my assumptions about what forms to rewrite, and how to rewrite them, are all correct, it will be easy to move this into desugar once I get it right.

@matthewhammer
Copy link
Contributor Author

matthewhammer commented Feb 6, 2019

Just reread #144

It seems like the intention is to actually do what @nomeata says above, i.e., remove these "extra" forms from the IR, and do these loop-simplification transformations in the existing desugar pass.

So, I'll plan on doing that approach, once I get this working.

@nomeata
Copy link
Contributor

nomeata commented Feb 6, 2019

I think the guideline of whether you do it in the desugarer (like AndE) or in a separate pass (like AwaitE) is:

  • Can you do the transformation by looking at the AST node (including type annotations), without knowing anything about the subterms or the context? Do it in the desugarer.
  • Do you need non-local information? Do it in a separate pass.

@nomeata nomeata changed the title IR pass: Simplify loop forms into a single, simple form [WIP, don’t merge] IR pass: Simplify loop forms into a single, simple form Feb 6, 2019
…ould be straightforward, given answers to the questions
@matthewhammer
Copy link
Contributor Author

I think the guideline of whether you do it in the desugarer (like AndE) or in a separate pass (like AwaitE) is:

* Can you do the transformation by looking at the AST node (including type annotations), without knowing anything about the subterms or the context? Do it in the desugarer.

* Do you need non-local information? Do it in a separate pass.

OK, thanks for this. I've put some questions, and a sketch of each planned transform, here:
c686ad0

Please have a look, especially for the questions there.

@matthewhammer
Copy link
Contributor Author

I'm going to continue working this today, following @crusso's tips and advice, just above.

The benefit is that many passes no longer need to keep a `con_env`
around, and many operations in `Type` are now always available, in
partiuclar `Type.as_*_sub`, `promote` etc.

The downside is that, because of the multi-pass approach in `typing.ml`, the
annotation needs to be a `kind ref`.

I tried using a `promise`, but it seems that certain things are run more
than once in `typing` (the same has caused issues with recoverable error
messages before). I am not sure if this is avoidable.

If maybe the initialization can be made a bit cleaner in `typing`, then
I think this will improve the life of those working on the `Ir`
noticably.
the `kind` is already in the `con`, so no need having it here.
so that the choice whether it is a `promise`, or a `ref`, or something
else, private to `type.ml`.

I tried to make it more promise-like, by trapping when the kind is set a
second time to something different, but it seems that `k = k'` can loop.
with a different value. A bit like a promise, but more compatible with
`typing.ml` running `infer_dec_typdecs` multiple times.

Not sure if it is worth the bother, though.
In `typing.ml`, we replace it with a `con_set`, so that we can still call
`avoid`.

In `check_ir.ml` we simply drop the field.
it is always the range of `typ_env` anyways.

This simplifies a few more functions.
nomeata added a commit that referenced this pull request Feb 15, 2019
and other improvements cherry-picked from #171 and #146, namely:
 * add an interface file (and remove code known to be dead)
 * make the `Check_ir.env` abstract
(* XXX: not sure how to get type info for `next` function...
- ...how to do I supply a non-empty con_env for `as_obj_sub` ?
*)
let _, tfs = Type.as_obj_sub "next" ty1 in
Copy link
Contributor

@crusso crusso Feb 15, 2019

Choose a reason for hiding this comment

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

I think there actually is a named constant for "next" (Construct.nextN), best to use it here and elsewhere

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually, couldn't you just construct the type of next as " () -> ?(E.typ pat)", (without needing an env) but maybe that's cheating.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@crusso hmm; I cannot use Construct.nextN here, since the code I want to call next (part of the Type module) expects a string, not a name.

Copy link
Contributor

Choose a reason for hiding this comment

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

You should be able to get the string from the name. Name is just a single constructor type (or at least it was) to prevent accidental confusion between alpha-convertible identifiers and rigid field names in the syntax. Andreas may have renamed Name to Lab(el) recently (but perhaps only in source). Type's probably just use string for field names...

let loopWhileE exp1 exp2 =
(* loop e1 while e2
~~> label l: loop {
let () = e1 ;
Copy link
Contributor

Choose a reason for hiding this comment

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

let () = e1 -> e1;

Copy link
Contributor Author

@matthewhammer matthewhammer Feb 15, 2019

Choose a reason for hiding this comment

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

Huh? I don't understand this comment; is something wrong in the (source) comment above?

src/construct.ml Outdated
blockE [
expD exp1 ;
letD id2 exp2 ;
expD (ifE id2
Copy link
Contributor

Choose a reason for hiding this comment

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

why bind to id2? (Perhaps Joachim spotted this one)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In general, I was using ANF to make things "easier" for me;
yesterday, @nomeata mentioned that this was suboptimal for the WASM output, so I'll avoid these intermediate bindings, and will fix this one.

Copy link
Contributor

Choose a reason for hiding this comment

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

K, but unfortunately, there's a cost, especially with recursion story (though I suspect Joachim optimizes this away now)

val labelE : Syntax.id -> typ -> exp -> exp
val loopE: exp -> exp option -> exp

val whileE' : exp -> exp -> exp'
Copy link
Contributor

Choose a reason for hiding this comment

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

Could these be whileE : ... -> exp etc, dropping the primes, in keeping with the rest of the API?

Copy link
Contributor

Choose a reason for hiding this comment

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

You'll need to correctly construct the type (of course) but also the effect too or this will mess up other transformations.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't see how to do this "cleanly"; how would you adjust these use cases? (see Simploop):


  (* Transformation from loop-while to loop: *)
  | I.LoopE (e1, Some e2) -> Construct.loopWhileE' (exp e1) (exp e2)

  (* Transformation from while to loop: *)
  | I.WhileE (e1, e2) -> Construct.whileE' (exp e1) (exp e2)

  (* Transformation from for to loop: *)
  | I.ForE (p, e1, e2) -> Construct.forE' p (exp e1) (exp e2)

Copy link
Contributor

Choose a reason for hiding this comment

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

True, but I guess here you could just do loopWhile (exp e1) (exp e2).it. I'm pretty sure I do that elsewhere in a few places...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, that's the pattern I was trying to avoid, but I can use it if you have a strong preference.

I don't have a preference myself, I just thought the version projecting out the it seemed slightly more "messy"; but again, I have no strong opinion; I will fix this.

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess you could provide both in this case... but, in general, the construct functions are there to make it easier to construct (nested) expressions, where its more convenient to have the exp return type for composing subexpressions.

Copy link
Contributor

@nomeata nomeata Feb 15, 2019

Choose a reason for hiding this comment

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

how would you adjust these use cases?

You would have to refactor the surrounding code to expect an exp, not an exp'. But that might be ugly, so maybe having both is fine.

- Continue isn't special IRCC, it's just a special named label ("continue-" or something like that) introduced by the parser.
(the loop(_,None) construct just loops forever, but you can still exit via break).

- I would indeed just do this desugaring in the parser
Copy link
Contributor

@crusso crusso Feb 15, 2019

Choose a reason for hiding this comment

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

Not sure about this comment - I think our intention is to eventually remove the complicated loops from the IR and desugar source loops to ir loops directly in the translator...

I guess once could imagine doing forE loops slight differently, optimizing for each collection type, so perhaps those would be best as a full blown transformation....

Desugaring is actually a bad name for the source to IR translation... desugaring usually means source 2 source...

Copy link
Contributor

@crusso crusso Feb 15, 2019

Choose a reason for hiding this comment

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

Even if we don't remove the constructs in the src->ir desugaring, it would be great if we could move the transform up the pipeline so awaitopt.ml and async.ml and tailcall.ml no longer need to consider all those looping constructs. At the moment only the compiler benefits.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I would indeed just do this desugaring in the parser

you (@crusso) wrote that, not me; I pasted it here from a GH conversation we had earlier :)
are you saying that you no longer believe this?

The rest of your comments make sense (e.g., regarding moving up this translation in the pipeline).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here's a link to the source of the aforementioned quoted text:
#146 (comment)

Copy link
Contributor

Choose a reason for hiding this comment

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

Ignore my comment - I think Andreas and Joachim didn't want this for better error reporting (and I guess to keep the AST more in line with syntax).

@nomeata nomeata changed the base branch from joachim/con-ann to master February 21, 2019 17:57
@nomeata
Copy link
Contributor

nomeata commented Feb 25, 2019

Is it time to un-WIP this and get it merged? Do you want to include it in desugar.ml?

@matthewhammer
Copy link
Contributor Author

Is it time to un-WIP this and get it merged? Do you want to include it in desugar.ml?

I think: Yes, and yes. I can work getting this done today.

@matthewhammer
Copy link
Contributor Author

@nomeata in preparation for doing this today, I moved the transforms into Desugar and removed Simploop from the pipeline. I also removed some uses of ANF in the transform's output. In the process, some other issues arose:

  • I have a question about one failed attempt to inline ANF; see my code comment within last commit; cc @crusso. Basically, array-gen diverges when I try to aggressively inline the projection of .next for the for-loop simplification. I don't understand why.
  • I'm getting (new?) errors on the dvm tests, after the first wave of tests all pass.
  • I tried (perhaps naively) to address the "conflicting files" issue above by rebasing with master; that didn't help, but maybe I did it wrong. I did git pull --rebase origin master, but git wanted me to fix each of those conflicts above by hand, and that seemed wrong; is there a way to avoid this?

@nomeata
Copy link
Contributor

nomeata commented Feb 26, 2019

I have a question about one failed attempt to inline ANF; see my code comment within last commit;

For those too lazy to search for this comment:
629b881#diff-b62a2a2b6b830d0e342967b88b07871eR472

If you inline letD nxt (dotE exp1 (nameN "next") tnxt) ; you might duplicate the evaluation of exp1. You still need to evaluate and bind exp1, once to not change the semantics. Might that be it?

I tried (perhaps naively) to address the "conflicting files" issue above by rebasing with master; that didn't help, but maybe I did it wrong. I did git pull --rebase origin master, but git wanted me to fix each of those conflicts above by hand, and that seemed wrong; is there a way to avoid this?

Well, if there are conflicts, you have to resolve them. Why do you think they are wrong?

Or is the problem that you have to do them repeatedly, as you rebase your patch series? You don’t really have a “clean, narrative” patch set anyways, so rebasing does not seem to be the right thing. Simply git pull origin master and do a merge.

I'm getting (new?) errors on the dvm tests, after the first wave of tests all pass.

Master is green, so these failures are introduced by your change. Looking at the output, it seem that the IR output has changed in cases where the compiler dumps IR (variable names have changed). If the changes are expected, then run make accept in test to make them part of this PR.

@nomeata
Copy link
Contributor

nomeata commented Feb 26, 2019

BTW, I just checked what happens if .next changes between loops, and wrote a test case:

let iter = new {
  private var i = 0;
  var next : (() -> ?()) = func () : ?() {
    print("old next: ");
    printInt(i);
    print("\n");
    i += 1;
    next := (func () : ?() {
      print("new next: ");
      printInt(i);
      print("\n");
      i += 1;
      return null;
    });
    return (?());
  }
};

for (x in iter) {
  print("Iter going\n");
};
print("Iter gone\n");

But actually the typing rule for for requires an immutable field next, so this is not an issue.

I don’t think anything is wrong with letD nxt (dotE exp1 (nameN "next") tnxt) btw. You need to bind the evaluation of exp1 anyways, and doing the lookup of next only once is fine.

@matthewhammer
Copy link
Contributor Author

Thanks for all of the feedback and clarifications @nomeata.

You still need to evaluate and bind exp1, once to not change the semantics. Might that be it?

Yep, I did that naively this morning. It was too early. I think that was the issue. Thanks for your investigation, in any case; I'll leave that transform for for alone now, since it seems okay with you.

Or is the problem that you have to do them repeatedly, as you rebase your patch series?

If I understand correctly, then "yes", each time I want to stay in sync with master now on this branch, I have conflicts like this. I was doing rebase because I thought it was no worse than merge, but I frankly don't appreciate the wider implications of either approach. The reason the conflicting files seemed "wrong" to me is that I never directly changed many of these files on my branch. Rather, my branch only affected a few existing modules (e.g., Construct, Pipeline and Desugar, perhaps one or two others), and the other modules (most from that list) were untouched. However, I (naively?) did a rebase earlier in the commit history that made these changes implicitly, I think; I guess that was a mistake, and I should have been doing something else to stay in sync with master, such as merge? Again, I'm confused by the "correct" way to do branches in git while master evolves concurrently. I take from your last message that I should always do merge and never rebase for branches such as this; but please correct me if I'm misinterpreting what you meant.

these failures are introduced by your change. Looking at the output, it seem that the IR output has changed in cases where the compiler dumps IR (variable names have changed)

Yes, I do see one instance of that. However, I also see things like this as well:

--- array-out-of-bounds.dvm-run (expected)
+++ array-out-of-bounds.dvm-run (actual)
@@ -1,2 +1,2 @@
-W, hypervisor: call failed with trap message: Uncaught RuntimeError: unreachable
+W, hypervisor: call failed with trap message: Uncaught RuntimeError: unreachable

This doesn't seem like a simple variable name difference. (am I missing something?). There are several errors like this, with the same unreachable trap message. Did you see those too?

In the meantime, I'm going to try to update DVM to see if that helps.

@matthewhammer
Copy link
Contributor Author

Updates:

  • doing update-dvm didn't seem to make these traps go away locally.
  • I did a fresh clone of master, and I get them there, too; so, it's something about my local build and test environment, it seems.

@matthewhammer
Copy link
Contributor Author

The commit history here is a mess, so I made a cleaner PR with a distillation of my changes. We can close this PR.

@nomeata
Copy link
Contributor

nomeata commented Feb 27, 2019

lots about git

If you feel like it, we can video-chat about the meaning and implications of rebase vs. merge. More efficient than writing about it here.

@nomeata nomeata deleted the simploop branch February 27, 2019 11:52
dfinity-bot added a commit that referenced this pull request Feb 28, 2023
## Changelog for ic-hs:
Branch: master
Commits: [dfinity/ic-hs@927d8152...6ddff2e4](dfinity/ic-hs@927d815...6ddff2e)

* [`54dcdd0c`](dfinity/ic-hs@54dcdd0) .github/workflows/release.yml: only sync github pages on linux ([dfinity/ic-hs⁠#143](https://github.com/dfinity/ic-hs/issues/143))
* [`81ac2e9c`](dfinity/ic-hs@81ac2e9) .github/workflows/release.yml: fix github-pages-deploy-action version ([dfinity/ic-hs⁠#144](https://github.com/dfinity/ic-hs/issues/144))
* [`09a7b445`](dfinity/ic-hs@09a7b44) fix broken link in README ([dfinity/ic-hs⁠#145](https://github.com/dfinity/ic-hs/issues/145))
* [`b393d8cb`](dfinity/ic-hs@b393d8c) .github/workflows/release.yml: skip coverage job on darwin ([dfinity/ic-hs⁠#146](https://github.com/dfinity/ic-hs/issues/146))
* [`55870466`](dfinity/ic-hs@5587046) split haskell modules to reduce memory usage ([dfinity/ic-hs⁠#142](https://github.com/dfinity/ic-hs/issues/142))
* [`6ddff2e4`](dfinity/ic-hs@6ddff2e) add httpbin implementation in Rust, bump nixpkgs, and sync universal_canister with ic monorepo ([dfinity/ic-hs⁠#138](https://github.com/dfinity/ic-hs/issues/138))
dfinity-bot added a commit that referenced this pull request Mar 1, 2023
## Changelog for ic-hs:
Branch: master
Commits: [dfinity/ic-hs@927d8152...808ba08b](dfinity/ic-hs@927d815...808ba08)

* [`54dcdd0c`](dfinity/ic-hs@54dcdd0) .github/workflows/release.yml: only sync github pages on linux ([dfinity/ic-hs⁠#143](https://github.com/dfinity/ic-hs/issues/143))
* [`81ac2e9c`](dfinity/ic-hs@81ac2e9) .github/workflows/release.yml: fix github-pages-deploy-action version ([dfinity/ic-hs⁠#144](https://github.com/dfinity/ic-hs/issues/144))
* [`09a7b445`](dfinity/ic-hs@09a7b44) fix broken link in README ([dfinity/ic-hs⁠#145](https://github.com/dfinity/ic-hs/issues/145))
* [`b393d8cb`](dfinity/ic-hs@b393d8c) .github/workflows/release.yml: skip coverage job on darwin ([dfinity/ic-hs⁠#146](https://github.com/dfinity/ic-hs/issues/146))
* [`55870466`](dfinity/ic-hs@5587046) split haskell modules to reduce memory usage ([dfinity/ic-hs⁠#142](https://github.com/dfinity/ic-hs/issues/142))
* [`6ddff2e4`](dfinity/ic-hs@6ddff2e) add httpbin implementation in Rust, bump nixpkgs, and sync universal_canister with ic monorepo ([dfinity/ic-hs⁠#138](https://github.com/dfinity/ic-hs/issues/138))
* [`808ba08b`](dfinity/ic-hs@808ba08) split Multiple controllers tests ([dfinity/ic-hs⁠#149](https://github.com/dfinity/ic-hs/issues/149))
dfinity-bot added a commit that referenced this pull request Mar 2, 2023
## Changelog for ic-hs:
Branch: master
Commits: [dfinity/ic-hs@927d8152...bdaaa321](dfinity/ic-hs@927d815...bdaaa32)

* [`54dcdd0c`](dfinity/ic-hs@54dcdd0) .github/workflows/release.yml: only sync github pages on linux ([dfinity/ic-hs⁠#143](https://github.com/dfinity/ic-hs/issues/143))
* [`81ac2e9c`](dfinity/ic-hs@81ac2e9) .github/workflows/release.yml: fix github-pages-deploy-action version ([dfinity/ic-hs⁠#144](https://github.com/dfinity/ic-hs/issues/144))
* [`09a7b445`](dfinity/ic-hs@09a7b44) fix broken link in README ([dfinity/ic-hs⁠#145](https://github.com/dfinity/ic-hs/issues/145))
* [`b393d8cb`](dfinity/ic-hs@b393d8c) .github/workflows/release.yml: skip coverage job on darwin ([dfinity/ic-hs⁠#146](https://github.com/dfinity/ic-hs/issues/146))
* [`55870466`](dfinity/ic-hs@5587046) split haskell modules to reduce memory usage ([dfinity/ic-hs⁠#142](https://github.com/dfinity/ic-hs/issues/142))
* [`6ddff2e4`](dfinity/ic-hs@6ddff2e) add httpbin implementation in Rust, bump nixpkgs, and sync universal_canister with ic monorepo ([dfinity/ic-hs⁠#138](https://github.com/dfinity/ic-hs/issues/138))
* [`808ba08b`](dfinity/ic-hs@808ba08) split Multiple controllers tests ([dfinity/ic-hs⁠#149](https://github.com/dfinity/ic-hs/issues/149))
* [`bdaaa321`](dfinity/ic-hs@bdaaa32) bump cachix/install-nix-action to v20 ([dfinity/ic-hs⁠#150](https://github.com/dfinity/ic-hs/issues/150))
mergify bot pushed a commit that referenced this pull request Mar 2, 2023
## Changelog for ic-hs:
Branch: master
Commits: [dfinity/ic-hs@927d8152...bdaaa321](dfinity/ic-hs@927d815...bdaaa32)

* [`54dcdd0c`](dfinity/ic-hs@54dcdd0) .github/workflows/release.yml: only sync github pages on linux ([dfinity/ic-hs⁠#143](https://github.com/dfinity/ic-hs/issues/143))
* [`81ac2e9c`](dfinity/ic-hs@81ac2e9) .github/workflows/release.yml: fix github-pages-deploy-action version ([dfinity/ic-hs⁠#144](https://github.com/dfinity/ic-hs/issues/144))
* [`09a7b445`](dfinity/ic-hs@09a7b44) fix broken link in README ([dfinity/ic-hs⁠#145](https://github.com/dfinity/ic-hs/issues/145))
* [`b393d8cb`](dfinity/ic-hs@b393d8c) .github/workflows/release.yml: skip coverage job on darwin ([dfinity/ic-hs⁠#146](https://github.com/dfinity/ic-hs/issues/146))
* [`55870466`](dfinity/ic-hs@5587046) split haskell modules to reduce memory usage ([dfinity/ic-hs⁠#142](https://github.com/dfinity/ic-hs/issues/142))
* [`6ddff2e4`](dfinity/ic-hs@6ddff2e) add httpbin implementation in Rust, bump nixpkgs, and sync universal_canister with ic monorepo ([dfinity/ic-hs⁠#138](https://github.com/dfinity/ic-hs/issues/138))
* [`808ba08b`](dfinity/ic-hs@808ba08) split Multiple controllers tests ([dfinity/ic-hs⁠#149](https://github.com/dfinity/ic-hs/issues/149))
* [`bdaaa321`](dfinity/ic-hs@bdaaa32) bump cachix/install-nix-action to v20 ([dfinity/ic-hs⁠#150](https://github.com/dfinity/ic-hs/issues/150))
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