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

Round 2 of AntiAliasing changes #1252

Merged
merged 3 commits into from
Apr 28, 2022

Conversation

mario-bucev
Copy link
Collaborator

@mario-bucev mario-bucev commented Mar 21, 2022

(note: without mutation, we couldn't create a cycle with epfl-lara/bolts#44 (without resorting to lazy evaluation) :p)

@mario-bucev mario-bucev force-pushed the antialiasing-round2 branch from 9a84d38 to cda82ca Compare March 21, 2022 09:51
@mario-bucev mario-bucev linked an issue Mar 21, 2022 that may be closed by this pull request
@mario-bucev mario-bucev force-pushed the antialiasing-round2 branch from cda82ca to 2bcebc1 Compare March 21, 2022 10:28
@mario-bucev mario-bucev marked this pull request as draft March 21, 2022 12:37
@mario-bucev
Copy link
Collaborator Author

Converting this to a draft due to some inefficiency introduced by #1245.
For instance, given:

import stainless._
import stainless.lang._

object i1251b {

  case class Box(var length: Int)

  def mutate(b: Box): Unit = { b.length = 123; }

  def h2(b1: Box, b2: Box, cond: Boolean): Unit = {
    val x = b1.length
    val c = if (cond) b1 else b2
    val d = c
    mutate(d)
    assert(b1.length == x)
  }
}

we generate:

val x: Int = b1.length
var c: Box = (if (cond) {
  b1
} else {
  b2
})
var d: Box = (if (cond) {
  b1
} else if (!cond) {
  b2
} else {
  c
})
var res: (Unit, Box) = mutate(@DropVCs (if (cond) {
  b1
} else if (!cond && !cond) {
  b2
} else if (!cond && !!cond) {
  if (cond) {
    b1
  } else if (!cond) {
    b2
  } else {
    c
  }
} else if (cond) {
  b1
} else if (!cond && !cond) {
  b2
} else if (!cond && !!cond) {
  c
} else {
  d
}))
(if (cond) {
  b1 = @DropVCs Box(@DropVCs res._2.length).asInstanceOf[Box]
} else {
  ()
})
(if (!cond && (!cond && !cond)) {
  b2 = @DropVCs Box(@DropVCs res._2.length).asInstanceOf[Box]
} else {
  ()
})
(if (!cond && (!(!cond && !cond) && (!cond && !!cond))) {
  c = @DropVCs Box(@DropVCs res._2.length).asInstanceOf[Box]
} else {
  ()
})
(if (!cond && (!(!cond && !cond) && !(!cond && !!cond))) {
  d = @DropVCs Box(@DropVCs res._2.length).asInstanceOf[Box]
} else {
  ()
})
res._1
assert(b1.length == x)
()

due to duplicating every occurrence of c and d with if (cond) b1 else b2.
Even if we were a bit more careful in substituting, we would still end up with expression duplication.
So maybe we should rethink env.rewritings?

@vkuncak vkuncak requested a review from samarion March 21, 2022 18:12
Copy link
Member

@samarion samarion left a comment

Choose a reason for hiding this comment

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

About duplicating expressions, we're already doing this a lot in the imperative phase (e.g. during effect copying after function calls), so it's not necessarily a deal breaker.

You could try introducing some additional simplifications in the ImperativeCleanup phase to deal with these. Maybe using simplifyLets and/or simplifyExpr will give good results.

@@ -384,12 +390,12 @@ class AntiAliasing(override val s: Trees)(override val t: s.type)(using override
val updates1 =
targets1 map { target =>
val applied = updatedTarget(target, ArraySelect(array2, index2).setPos(swap))
transform(Assignment(target.receiver, applied).setPos(swap), env)
Assignment(target.receiver, applied).setPos(swap)
Copy link
Member

Choose a reason for hiding this comment

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

I think we still need to transform array1, index1, array2 and index2.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good point, I've totally missed that out! I'll also add tests for these cases.

val applied = updatedTarget(target, v)
transform(Assignment(target.receiver, applied).copiedFrom(up), env)
val applied = updatedTarget(target, recV)
Assignment(target.receiver, applied).copiedFrom(up)
Copy link
Member

Choose a reason for hiding this comment

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

I think this transformation (and a few others) might be incorrectly dropping stuff from the input array expression. If there was a Block expression in a, then I believe it would be silently dropped in the output here.

I'm not sure what the cleanest approach to fix this is, though. I guess some options are:

  1. adding a method which extracts the non-target part of an expression and join it into the output,
  2. we could always include the expression in the output and hope simplification cleans it up later on by having the result be something like
    Block(transform(a, env), Assignement(...))
  3. we could have a pre-transformation which makes sure to bring all blocks/targets into a normal form which is compatible with target assignment. This option would also make sure that mutating calls inside targets are handled correctly.

Copy link
Collaborator Author

@mario-bucev mario-bucev Mar 22, 2022

Choose a reason for hiding this comment

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

Option 3 seems to also benefit #1068 (as you pointed out)

Comment on lines 639 to 645
case au@ArrayUpdated(arr, i, arg) =>
val Seq(rewrArr, rewrI, rewrArg) = rewriteAndCheckArgs(au, Seq(arr, i, arg))
ArrayUpdated(transform(rewrArr, env), transform(rewrI, env), transform(rewrArg, env)).copiedFrom(au)

case mu@MapUpdated(map, k, v) =>
val Seq(rewrMap, rewrK, rewrV) = rewriteAndCheckArgs(mu, Seq(map, k, v))
MapUpdated(transform(rewrMap, env), transform(rewrK, env), transform(rewrV, env)).copiedFrom(mu)
Copy link
Member

Choose a reason for hiding this comment

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

I think these are only relevant when the array and map base types are mutable, no?

Comment on lines +578 to +594
case _ =>
throw MalformedStainlessCode(expr, s"Couldn't compute effect targets of array copy update ${au.asString}")
}
Copy link
Member

Choose a reason for hiding this comment

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

This (and the corresponding MapUpdated logic) seems to strict. This will also prevent users from using the snapshot(a).updated(i, v) construct which we were discussing.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That's true, for instance, the SortedArray benchmark needs to be updated to account for that restriction (I'm realizing that we can also return Seq.empty if the effect kind is replacement).
For ModfiyingKind though, I do not think there is a way to express the concerned targets.

@mario-bucev
Copy link
Collaborator Author

It remains to comment on parts of the code, I'll notify you once (if ever...) I'm done :)

@mario-bucev mario-bucev force-pushed the antialiasing-round2 branch from d315f3f to ece5e16 Compare April 20, 2022 13:04
@mario-bucev mario-bucev marked this pull request as ready for review April 20, 2022 14:40
@mario-bucev
Copy link
Collaborator Author

@samarion Ready for re-review! (when & if you have the time and desire to do so, of course :) )

Copy link
Member

@samarion samarion left a comment

Choose a reason for hiding this comment

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

I've read through most of the change and left a few comments. I quite like the move away from rewritings with explicit target tracking instead.

I didn't check the normalization logic yet, but I'm surprised by some of the benchmarks which were disabled due to it (for example, the MutateInsideN benchmarks). Which problems are the block and let hoisting steps solving?

if (be.receiver == vd.toVariable) getTargets(e, kind, be.path.path)
else Seq(be)
val targs0 = getTargets(b, kind, path)
// TODO: Explain why
Copy link
Member

Choose a reason for hiding this comment

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

Please do :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Oops, thank you for catching that!

getAllTargets(expr) flatMap { (target: Target) =>
inEnv(target.toEffect(ModifyingKind), env)
inEnv(target.toEffect(ModifyingKind), env).toSet
Copy link
Member

Choose a reason for hiding this comment

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

Nit: you don't need the call to toSet here, right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I have reverted the getTargets returning a Seq to return a Set (as it was before), but I forgot to review the parts in EffectsAnalyzer that depend on this Seq/Set return type (like for the commented snippet below!)

@@ -782,7 +821,7 @@ trait EffectsAnalyzer extends oo.CachingPhase {
.filter(p => effects contains p._2)
.flatMap(_._1)

case Assignment(v, value) => rec(value, env) ++ env.get(v)
case Assignment(v, value) => rec(value, env) ++ env.get(v).toSeq.flatten
Copy link
Member

Choose a reason for hiding this comment

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

Nit: why toSeq?

val castedValue =
if (adt.isInstanceOf[ADTType] && adt.asInstanceOf[ADTType].getSort.constructors.size == 1)
newValue
else
Annotated(AsInstanceOf(newValue, effect.receiver.getType), Seq(DropVCs)).copiedFrom(newValue)
Assignment(effect.receiver, castedValue).setPos(pos)
wrap(AsInstanceOf(newValue, target.receiver.getType))
Copy link
Member

Choose a reason for hiding this comment

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

Should we have AsInstanceOf(...).copiedFrom(newValue) to propagate positions?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah yes, good point!

}.filter(_._2.nonEmpty)
}

def aliasingValDefs2(updTargets: Set[Target], env: Env): Map[ValDef, Set[Target]] = {
Copy link
Member

Choose a reason for hiding this comment

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

Does overloading not work here? Adding the 2 suffix is more confusing IMO

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I had some issues with overloading IRC, but I can try adding a DummyImplicit to resolve the issue

Comment on lines 408 to 409
if (conds.isEmpty) None
else {
Copy link
Member

Choose a reason for hiding this comment

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

This case is redundant, calling orJoin on Seq() will return BooleanLiteral(true) and result in a None value for combinedCond.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fair point!

Copy link
Collaborator Author

@mario-bucev mario-bucev Apr 27, 2022

Choose a reason for hiding this comment

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

Actually, orJoin returns BooleanLiteral(false) on an empty sequence (but andJoin does return BooleanLiteral(true))

Copy link
Member

Choose a reason for hiding this comment

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

Oh right, good point.

Hmm, but then shouldn't we consider an empty condition as implying BooleanLiteral(true)? It seems fishy to me that we have special handling of the empty case. Maybe conds should rather be defined as

val conds = tgs.map(_.condition.getOrElse(BooleanLiteral(true))

(and then we can just use orJoin and avoid the special case for empty conditions).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That's a more sensical thing to do indeed

// But it also conditionally introduce a new target, namely Ref(123) when !cond1.
// In such case, we introduce `y` as a new target, with condition !cond1.
assert(targs.forall(_.condition.isDefined))
val negatedConds = not(orJoin(targs.map(_.condition.get).toSeq))
Copy link
Member

Choose a reason for hiding this comment

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

Does this really work in practice? IIRC, the not and orJoin simplifications are really trivial so you might not even catch the case where you have cond and !cond as conditions. Maybe you could introduce some dedicated logic since you have a strong expectation of what the conditions will look like.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

You are right, it doesn't work at all in practice (it ends up generating quite a lot of extra update statements, though it's not incorrect). I'll add something that at least catches things such as cond and !cond.

val ra = freshenLocals(replaceFromSymbols(env.rewritings, a))
val targets = getDirectTargets(ra, ArrayAccessor(i))
// Sanity check (the same as for Swap)
assert(Some(targets) == getDirectTargetsDealiased(transform(arr, env), ArrayAccessor(recI), env))
Copy link
Member

Choose a reason for hiding this comment

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

Isn't there a risk that target computation will fail on the transformed expression?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

getDirectTargetsDealiased returns a None in case of failure (which would fire the assertion...)

Copy link
Member

Choose a reason for hiding this comment

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

Yes, I meant: won't this generate spurious failures? It's not clear to me that we expect getDirectTargetsDealiased to always work on transformed trees.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah ok, in that sense! Yes you are right, I can remove these checks

@mario-bucev
Copy link
Collaborator Author

Thank you for reviewing! For the normalization, the two main motivations are to sequence effects of function calls (and addresses issue such as #1068) and introduce val bindings for indices and keys.
As for hoisting let within let, I think this is a "by-product" of the normalization and not necessarily sought after (though the comment suggests it is... I'll change that).

@samarion
Copy link
Member

Regarding block hoisting, could we maybe avoid flattening them? Blocks are somewhat useful in allowing local effects which is what the MutateInsideN benchmarks are testing.

@mario-bucev
Copy link
Collaborator Author

Will do!

@vkuncak vkuncak merged commit a041c97 into epfl-lara:main Apr 28, 2022
@mario-bucev mario-bucev deleted the antialiasing-round2 branch June 8, 2022 14:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants