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

feat: abstractMVars: do not loop on bad input #4378

Closed
wants to merge 1 commit into from

Conversation

nomeata
Copy link
Collaborator

@nomeata nomeata commented Jun 6, 2024

previously abstractMVars would go into a loop on bad input, in
particular if an mvar occurs in its own type. While this should never
happen in practice and is thus not a problem for our users, it can be
quote annoying when one runs into this problem while developing meta
code, as the stack overflow means you don’t get your trace messages
etc.

By keeping track of the mvars whose type we are processing we can break
the loop. The function is pure, so no throwError, but even a panic!
is going to be somewhat helpful.

previously `abstractMVars` would go into a loop on bad input, in
particular if an mvar occurs in its own type. While this should never
happen in practice and is thus not a problem for our users, it can be
quote annoying when one runs into this problem while developing meta
code, as the stack overflow means you don’t get your `trace` messages
etc.

By keeping track of the mvars whose type we are processing we can break
the loop. The function is pure, so no `throwError`, but even a `panic!`
is going to be somewhat helpful.
@github-actions github-actions bot temporarily deployed to lean-lang.org/lean4/doc June 6, 2024 17:19 Inactive
@github-actions github-actions bot added the toolchain-available A toolchain is available for this PR, at leanprover/lean4-pr-releases:pr-release-NNNN label Jun 6, 2024
leanprover-community-mathlib4-bot added a commit to leanprover-community/batteries that referenced this pull request Jun 6, 2024
leanprover-community-mathlib4-bot added a commit to leanprover-community/mathlib4 that referenced this pull request Jun 6, 2024
@leanprover-community-mathlib4-bot leanprover-community-mathlib4-bot added the builds-mathlib CI has verified that Mathlib builds against this PR label Jun 6, 2024
@leanprover-community-mathlib4-bot
Copy link
Collaborator

Mathlib CI status (docs):

@leodemoura
Copy link
Member

@nomeata Let's discuss this PR in our next 1:1. There are many other places in the code that will loop if there are circular assignments at MetavarContext. The assign operation is a low-level one. We should improve its documentation instead, and make it clear that it is the caller responsibility to ensure a circular assignment is not created. We may also point users to higher level functions that perform occurs-check (e.g., isDefEq).

@nomeata
Copy link
Collaborator Author

nomeata commented Jun 7, 2024

Right, this is right now a draft so that I can find my bugs. We talked a bit at the tea time about this, and it seems that the isDefEq advice isn't fully correct, as it would not assign mvars of type Prop?

I would find it helpful if the API and it's docs could be improved. In particular a .checkedAssign is something that I often wanted, i.e. like assign (safe, returning Unit not Bool).

Also note that here it's not a circular assignment but a circular type (the mvar is unassigned, but occurs is in its own type) that cause this to fail baldy. Do the safe functions check for that?

@leodemoura
Copy link
Member

Right, this is right now a draft so that I can find my bugs. We talked a bit at the tea time about this, and it seems that the isDefEq advice isn't fully correct, as it would not assign mvars of type Prop?

Not true. If we call isDefEq on ?m and a term t, the assignment is performed before we try to use proof irrelevance. Here is an example.

import Lean

open Lean Meta
run_meta do
  let mvar ← mkFreshExprMVar (mkConst ``True)
  let term := mkConst ``True.intro
  IO.println (← ppExpr mvar). -- ?m
  IO.println (← isDefEq mvar term) -- true
  IO.println (← ppExpr mvar) -- True.intro

Here is a bad example using assign which creates a cycle

run_meta do
  let mvar1 ← mkFreshExprMVar (mkConst ``True)
  let mvar2 ← mkFreshExprMVar (mkConst ``True)
  mvar1.mvarId!.assign mvar2
  mvar2.mvarId!.assign mvar1 -- Bad, cycle created
  IO.println (← ppExpr mvar1)
  IO.println (← ppExpr mvar2)

Same example, using isDefEq to avoid the cycle

run_meta do
  let mvar1 ← mkFreshExprMVar (mkConst ``True)
  let mvar2 ← mkFreshExprMVar (mkConst ``True)
  mvar1.mvarId!.assign mvar2
  IO.println (← isDefEq mvar2 mvar1) -- true, and no cycle created
  IO.println (← ppExpr mvar1)
  IO.println (← ppExpr mvar2)

@leodemoura
Copy link
Member

I would find it helpful if the API and it's docs could be improved. In particular a .checkedAssign is something that I often wanted, i.e. like assign (safe, returning Unit not Bool).

We can implement .checkedAssign using isDefEq. It performs many checks before assigning ?m := e

  • Cyclic assignments
  • ?m and e types are definitionally equal.
  • Local context compatibility. That is, e does not contain a free variable that is not on ?m's local context.

@nomeata
Copy link
Collaborator Author

nomeata commented Jun 7, 2024

Thanks. Does it also check that the type of the mvar doesn't mention the mvar, possibly indirectly? (I am not 100% certain, but I think I ran into the infinite loop using just isDefEq, no assign.)

(Maybe I misunderstood what Sebastian said about proof irrelevance and mvars. Maybe it's mvars nested inside proofs.)

@nomeata
Copy link
Collaborator Author

nomeata commented Jun 8, 2024

Thanks. Does it also check that the type of the mvar doesn't mention the mvar, possibly indirectly? (I am not 100% certain, but I think I ran into the infinite loop using just isDefEq, no assign.)

Was able to condense it to a mwe at #4405.

@leodemoura
Copy link
Member

Thanks. Does it also check that the type of the mvar doesn't mention the mvar, possibly indirectly? (I am not 100% certain, but I think I ran into the infinite loop using just isDefEq, no assign.)

(Maybe I misunderstood what Sebastian said about proof irrelevance and mvars. Maybe it's mvars nested inside proofs.)

@nomeata This check is not performed. It is a different discussion whether it should be included in isDefEq or not. We need more evidence it is useful instead of just extra unnecessary work. The example in #4405 looks artificial to me, and was constructed programmatically. I don't recall ever seeing in real code a cyclic assignment issue similar to the issue described in #4405. What about the following:

  • You start using isDefEq or a wrapper (checkedAssign) on top of it.
  • If you still hit a loop, we investigate and understand what is going on. Then, we can decide whether it is needed.

@nomeata
Copy link
Collaborator Author

nomeata commented Jun 8, 2024

I was using isDefEq, and I was hitting this in practice, and because it manifested as a stack overflow (so no catching exceptions, no trace messages, although good old IO.eprintln helped to debug it) it took me some time to get to the bottom of it. Of course I was doing something in a way that nobody else did before, it seems, if I stumbled first over this, but at least I didn't feel particularly naughty doing it. It doesn't seem unreasonable to me to hope for a bit more robustness of the Meta API.

(I wonder if I can reproduce it using conv if that makes it more relevant.)

@nomeata
Copy link
Collaborator Author

nomeata commented Jun 8, 2024

Of course it's totally fair to say that what I am doing here is a bit too obscure (and now that I know what to look out for I am no longer blocked) and we wait to see if others hit the same issue before acting.

@leodemoura
Copy link
Member

I was using isDefEq, and I was hitting this in practice, and because it manifested as a stack overflow (so no catching exceptions, no trace messages, although good old IO.eprintln helped to debug it) it took me some time to get to the bottom of it. Of course I was doing something in a way that nobody else did before, it seems, if I stumbled first over this, but at least I didn't feel particularly naughty doing it. It doesn't seem unreasonable to me to hope for a bit more robustness of the Meta API.

(I wonder if I can reproduce it using conv if that makes it more relevant.)

Let's talk about it next time we meet. I would love to see the code and how it happened.
If this is a recurrent thing for you, we can add Context.extraDefEqChecks that when set to true will perform extra sanity checks.

@nomeata
Copy link
Collaborator Author

nomeata commented Jun 9, 2024

You were right to ask for better evidence. I didn’t necessarily expect that to be possible, but it is possible to trigger a stack overflows without using the programmatic interface. Added it to #4405 (comment).

@leodemoura
Copy link
Member

You were right to ask for better evidence. I didn’t necessarily expect that to be possible, but it is possible to trigger a stack overflows without using the programmatic interface. Added it to #4405 (comment).

Thanks. I will work on a fix this week for #4405.

@nomeata nomeata closed this Jun 10, 2024
@nomeata nomeata deleted the joachim/abstractMVars-loop branch June 10, 2024 14:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
builds-mathlib CI has verified that Mathlib builds against this PR toolchain-available A toolchain is available for this PR, at leanprover/lean4-pr-releases:pr-release-NNNN
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants