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

Strange behavior of lambda equality #62

Closed
jad-hamza opened this issue Jul 24, 2017 · 26 comments
Closed

Strange behavior of lambda equality #62

jad-hamza opened this issue Jul 24, 2017 · 26 comments
Labels

Comments

@jad-hamza
Copy link
Contributor

f1 and f2 should be equal here. It looks like that the imperative cleanup transformation replaces plus(Zero(), Zero()) by u in f2. For Stainless, u and plus(Zero(), Zero()) don't have the same structure so the assertion that f1 is different than f2 succeeds.

Also, in general, don't we want (x: Nat) => plus(Zero(),Zero()) and (x: Nat) => u to be considered as the same function?

import stainless.lang._
import stainless.annotation._

object Equality {
  sealed abstract class Nat
  case class Zero() extends Nat 
  case class Succ(n: Nat) extends Nat

  def plus(a: Nat, b: Nat): Nat = a match {
    case Zero() => b
    case Succ(a2) => Succ(plus(a2,b))
  }

  def theorem(a: Nat) = {
    val f1 = (x: Nat) => plus(Zero(), Zero())
    val u = plus(Zero(), Zero())
    val f2 = (x: Nat) => plus(Zero(), Zero())
    assert(f1 != f2) // VALID
  }
}

Result after transformation by imperative.cleanup, probably due to the call to simplifyLets.

def theorem(a: Nat): Unit = {
  val f1: (Nat) => Nat = (x: Nat) => plus(Zero(), Zero())
  val u: Nat = plus(Zero(), Zero())
  val f2: (Nat) => Nat = (x: Nat) => u
  assert(f1  f2)
  ()
}
@samarion
Copy link
Member

I'll have to take a look at what imperative is doing here. I can also take a look at #30 while I'm at it.

About equality, we can't say that (x: Nat) => plus(Zero(),Zero()) and (x: Nat) => u are equal in general because of potential non-termination in plus. Namely, running

val u = plus(Zero(), Zero())
val f = (x: Nat) => u

may non-terminate whereas

val f = (x: Nat) => plus(Zero(), Zero())

will always terminate. However, if both lambdas were pure, some structural rewriting would occur and make them equal. It might be interesting for Inox to have a pure mode where all expressions are considered pure (this would make sense when it's a backend to Stainless).

This mode could maybe also support removing assertions for the let simplifications we had talked about. However, if I remember correctly I had looked into implementing such a mode and it was non-trivial due to how the Inox code is structured... I'll take a look again.

@jad-hamza
Copy link
Contributor Author

There are three variants that we can consider separately:

Variant 1 (the one you wrote)

((x: Nat) => plus(Zero(), Zero())) ==
{
  val u = plus(Zero(), Zero())
  (x: Nat) => u
}

Variant 2 (the one corresponding to defining u first, then checking equality (this one corresponds to the Stainless code above I think) :

val u = plus(Zero(), Zero())
((x: Nat) => plus(Zero(), Zero())) == ((x: Nat) => u)

Variant 3, where u is defined locally to the lambda that uses it

((x: Nat) => plus(Zero(), Zero())) == {
  (x: Nat) => {
    val u = plus(Zero(), Zero())
    u
  }
}

According to the semantics of the logic that Inox uses and to the definition of equality, are these formulas valid (semantically)?
I think it's ok for Inox to return UNKNOWN if proving that they are valid is too difficult, but I'm afraid that saying that the formulas are valid when we replace == by != might lead to inconsistencies.

Also, do we want the two following programs to be valid, or invalid (from a semantic/logic point of view)? The first one passes, the second one returns a counter-example. In the formal logic underlying Inox, how to define equality?

  def equalFunctions(a: Nat, b: Nat) = {
    require(a == b)

    ((x: Nat) => a) == ((x: Nat) => b)
  } holds
  def extensionality(f: Nat => Nat, g: Nat => Nat) = {
    require(forall((x: Nat) => f(x) == g(x)))

    f == g
  } holds

@samarion
Copy link
Member

samarion commented Jul 25, 2017 via email

@jad-hamza
Copy link
Contributor Author

If I understand correctly, all threes variants are invalid for the operational semantics of Inox (and valid if we replace == by !=)? About the inconsistency, I'm afraid specifically about the interaction with equalFunctions, which is valid.

@samarion
Copy link
Member

samarion commented Jul 25, 2017 via email

@jad-hamza
Copy link
Contributor Author

jad-hamza commented Jul 25, 2017

Hmm I see, that's funny

By the way, is this the code responsible for the strange behavior of the expression being replaced by the variable? epfl-lara/inox#31 I wonder if removing that case breaks anything else?

@samarion
Copy link
Member

samarion commented Jul 25, 2017 via email

@jad-hamza
Copy link
Contributor Author

jad-hamza commented Jul 25, 2017

From these examples, we can deduce that a formula F with a free variable x: Nat can be proven valid (or return true as a function), while the formula [t / x]F (where x is substituted by a term t: Nat) can be invalid (or return false as a function). In particular this may happen when t involve recursive functions, due to t not being "pure", is that right?

(In the examples above, x is u, and t is plus(Zero,Zero))

@samarion
Copy link
Member

samarion commented Jul 25, 2017 via email

@samarion
Copy link
Member

Fixed by epfl-lara/inox#31

@jad-hamza
Copy link
Contributor Author

To continue about equality, it looks like that it's not a congruence relation, is that ok?

And with this definition there are some consequences on Stainless. If a property is valid, it can become invalid after adding @inline annotations (or vice versa). Are there known examples where @inline break besides the ones using equality?

Also, are there other things that are known to break the substitution property besides this definition of equality?

By the way, in the Inox code, where can I see how equality is evaluated (in the operational semantics)? Is it the Equals case in the RecursiveEvaluator? Thanks

@samarion
Copy link
Member

samarion commented Jul 27, 2017 via email

@jad-hamza
Copy link
Contributor Author

For equality to be a congruence, don't we need it to respect the structure of the terms (lifting equality)? So u = Plus(Zero,Zero) would imply ((x: Nat) => u) = ((x: Nat) => Plus(Zero,Zero)).

For substitutions, I'm thinking that formally, there is a logical judgement that says: Given a context Gamma that assigns (unknown) variables to types, and a context Sigma that (known) variables to terms, a formula F is valid (for Inox), or Gamma, Sigma |- F valid.

If I understand correctly, for Inox, this judgement is defined in terms of the operations semantics in RecursiveEvaluator. More precisely, we have Gamma, Sigma |- F valid iff for all possible values v1,...,vn, the expression [v1,...,vn / Gamma] F evaluates to true under context Sigma.
Is the expression [v1,...,vn / Gamma] F allowed to diverge and not return anything (e.g. in presence of non-terminating terms)?

Evaluating a forall requires a similar definition right? The expression forall((x: T) => e) evaluates to true if
for any value v: T, [x/v] e evaluates to true.

@jad-hamza
Copy link
Contributor Author

For @inline, this is an example of a program that is verified valid with the annotation but there is a counter-example without the annotation. (Similarly, we can make something that is valid without the annotation but invalid with the annotation)

import stainless.lang._
import stainless.annotation._

object Equality {
  sealed abstract class Nat
  case class Zero() extends Nat 
  case class Succ(n: Nat) extends Nat

  def plus(a: Nat, b: Nat): Nat = a match {
    case Zero() => b
    case Succ(a2) => Succ(plus(a2,b))
  }

  @inline // the program is VALID when this is there, but INVALID when it's not
  def v() = plus(Zero(), Zero())

  def theorem(a: Nat) = {
    assert(((x: Nat) => plus(Zero(), Zero())) == ((x: Nat) => v))
  }
}

@samarion
Copy link
Member

samarion commented Jul 27, 2017 via email

@samarion
Copy link
Member

samarion commented Jul 27, 2017 via email

@jad-hamza
Copy link
Contributor Author

Can you expand a bit on the definition of congruence you have in mind?
Is it eval(e1 = e2) = true implies eval(e1) = eval(e2)?

So do we want [v1,...,vn / Gamma] F to always evaluate to true, or never evaluate to false? (do we allow non-termination for validity?)

I thought the Sigma would be populated when entering in a let binding? (Would that be the context rctx: RC in the RecursiveEvaluator?)

@jad-hamza
Copy link
Contributor Author

Equality is defined on "Expr", so for congruence I had in mind this:

e1 = e2 is valid implies Ctx[e1] = Ctx[e2] is valid, where Ctx is an Expr with a hole.

@jad-hamza
Copy link
Contributor Author

I agree about @inline, it should never make things less equal according to this definition of equality.
Do you know of other examples where @inline breaks validity vs invalidity that do not involve equality?

@jad-hamza
Copy link
Contributor Author

So do we want [v1,...,vn / Gamma] F to always evaluate to true, or never evaluate to false? (do we allow non-termination for validity?)

Nevermind, I misread your comment. So if the definition is that the term never evaluates to false, substituting by a non-terminating term shouldn't break validity right (ignoring this definition of equality)?

@samarion
Copy link
Member

samarion commented Jul 27, 2017 via email

@jad-hamza
Copy link
Contributor Author

For the congruence that means we're checking congruence only for the context: let x = HOLE in F and not other type of expression. For instance, not for the context, Lambda(x, HOLE).

@samarion
Copy link
Member

samarion commented Jul 27, 2017 via email

@jad-hamza
Copy link
Contributor Author

Yes, agreed! Though I think there are definitions of equality that do satisfy the more general requirement, even with call-by-value evaluation.

we would still have the same problems for functions that use the argument x.

Personally, I don't know which definition of equality we should use. It's just that this particular definition seems strange/arbitrary to me, because it is the only (?) operation that inspects the structure of the expression. Also, it is not consistent with function extensionality and general congruence/substitution. I think it's fine to have a definition of equality that does not always imply function extensionality (weaker), but a definition which contradicts it seems odd.

In general, I also think it's ok if we have a definition of equality that is not decidable/computable. The RecursiveEvaluator is already a partial function (e.g. due to recursion and quantifiers).

If the evaluator fails to check for equality, then it can hang or return UNKNOWN (like it does for non-terminating function and for some quantifiers). Of course the evaluator would never hang for equality on base types (if the evaluation terminates), because equality here is decidable. The evaluator could even use the current implementation, and return UNKNOWN if it's not able to prove equality using the current definition (nor disprove).

@samarion
Copy link
Member

samarion commented Jul 27, 2017 via email

@jad-hamza
Copy link
Contributor Author

Alright, thanks for all the explanations!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants