Skip to content

Commit 506a6f3

Browse files
Fix missing local context + valid proofs wrongfully rejected in tactic mode (#82)
* Fix missing local context + fvar when validating proofs * Solve `exact?` self-application issue * Fix remaining cases where access to future environment in initial commands was possible through ProofSnapshot
1 parent 4a7e8af commit 506a6f3

13 files changed

+334
-26
lines changed

REPL/Frontend.lean

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,20 +28,30 @@ If there is no existing environment, we parse the input for headers (e.g. import
2828
and create a new environment.
2929
Otherwise, we add to the existing environment.
3030
31-
Returns the resulting command state, along with a list of messages and info trees.
31+
Returns:
32+
1. The header-only command state (only useful when cmdState? is none)
33+
2. The resulting command state after processing the entire input
34+
3. List of messages
35+
4. List of info trees
3236
-/
3337
def processInput (input : String) (cmdState? : Option Command.State)
3438
(opts : Options := {}) (fileName : Option String := none) :
35-
IO (Command.State × List Message × List InfoTree) := unsafe do
39+
IO (Command.State × Command.State × List Message × List InfoTree) := unsafe do
3640
Lean.initSearchPath (← Lean.findSysroot)
3741
enableInitializersExecution
3842
let fileName := fileName.getD "<input>"
3943
let inputCtx := Parser.mkInputContext input fileName
40-
let (parserState, commandState) ← match cmdState? with
44+
45+
match cmdState? with
4146
| none => do
47+
-- Split the processing into two phases to prevent self-reference in proofs in tactic mode
4248
let (header, parserState, messages) ← Parser.parseHeader inputCtx
4349
let (env, messages) ← processHeader header opts messages inputCtx
44-
pure (parserState, (Command.mkState env messages opts))
45-
| some cmdState => do
46-
pure ({ : Parser.ModuleParserState }, cmdState)
47-
processCommandsWithInfoTrees inputCtx parserState commandState
50+
let headerOnlyState := Command.mkState env messages opts
51+
let (cmdState, messages, trees) ← processCommandsWithInfoTrees inputCtx parserState headerOnlyState
52+
return (headerOnlyState, cmdState, messages, trees)
53+
54+
| some cmdStateBefore => do
55+
let parserState : Parser.ModuleParserState := {}
56+
let (cmdStateAfter, messages, trees) ← processCommandsWithInfoTrees inputCtx parserState cmdStateBefore
57+
return (cmdStateBefore, cmdStateAfter, messages, trees)

REPL/Main.lean

Lines changed: 67 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,15 @@ def sorries (trees : List InfoTree) (env? : Option Environment) (rootGoals? : Op
102102
fun ⟨ctx, g, pos, endPos⟩ => do
103103
let (goal, proofState) ← match g with
104104
| .tactic g => do
105-
let s ← ProofSnapshot.create ctx none env? [g] rootGoals?
106-
pure ("\n".intercalate <| (← s.ppGoals).map fun s => s!"{s}", some s)
105+
let lctx ← ctx.runMetaM {} do
106+
match ctx.mctx.findDecl? g with
107+
| some decl => return decl.lctx
108+
| none => throwError "unknown metavariable '{g}'"
109+
let s ← ProofSnapshot.create ctx lctx env? [g] rootGoals?
110+
pure ("\n".intercalate <| (← s.ppGoals).map fun s => s!"{s}", some s)
107111
| .term lctx (some t) => do
108-
let s ← ProofSnapshot.create ctx lctx env? [] rootGoals? [t]
109-
pure ("\n".intercalate <| (← s.ppGoals).map fun s => s!"{s}", some s)
112+
let s ← ProofSnapshot.create ctx lctx env? [] rootGoals? [t]
113+
pure ("\n".intercalate <| (← s.ppGoals).map fun s => s!"{s}", some s)
110114
| .term _ none => unreachable!
111115
let proofStateId ← proofState.mapM recordProofSnapshot
112116
return Sorry.of goal pos endPos proofStateId
@@ -117,23 +121,55 @@ def ppTactic (ctx : ContextInfo) (stx : Syntax) : IO Format :=
117121
catch _ =>
118122
pure "<failed to pretty print>"
119123

120-
def tactics (trees : List InfoTree) : M m (List Tactic) :=
124+
def tactics (trees : List InfoTree) (env? : Option Environment) : M m (List Tactic) :=
121125
trees.flatMap InfoTree.tactics |>.mapM
122126
fun ⟨ctx, stx, rootGoals, goals, pos, endPos, ns⟩ => do
123-
let proofState := some (← ProofSnapshot.create ctx none none goals rootGoals)
127+
let proofState := some (← ProofSnapshot.create ctx none env? goals rootGoals)
124128
let goals := s!"{(← ctx.ppGoals goals)}".trim
125129
let tactic := Format.pretty (← ppTactic ctx stx)
126130
let proofStateId ← proofState.mapM recordProofSnapshot
127131
return Tactic.of goals tactic pos endPos proofStateId ns
128132

129-
def collectRootGoalsAsSorries (trees : List InfoTree) : M m (List Sorry) := do
133+
def collectRootGoalsAsSorries (trees : List InfoTree) (env? : Option Environment) : M m (List Sorry) := do
130134
trees.flatMap InfoTree.rootGoals |>.mapM
131135
fun ⟨ctx, goals, pos⟩ => do
132-
let proofState := some (← ProofSnapshot.create ctx none none goals goals)
136+
let proofState := some (← ProofSnapshot.create ctx none env? goals goals)
133137
let goals := s!"{(← ctx.ppGoals goals)}".trim
134138
let proofStateId ← proofState.mapM recordProofSnapshot
135139
return Sorry.of goals pos pos proofStateId
136140

141+
142+
private def collectFVarsAux : Expr → NameSet
143+
| .fvar fvarId => NameSet.empty.insert fvarId.name
144+
| .app fm arg => (collectFVarsAux fm).union $ collectFVarsAux arg
145+
| .lam _ binderType body _ => (collectFVarsAux binderType).union $ collectFVarsAux body
146+
| .forallE _ binderType body _ => (collectFVarsAux binderType).union $ collectFVarsAux body
147+
| .letE _ type value body _ => ((collectFVarsAux type).union $ collectFVarsAux value).union $ collectFVarsAux body
148+
| .mdata _ expr => collectFVarsAux expr
149+
| .proj _ _ struct => collectFVarsAux struct
150+
| _ => NameSet.empty
151+
152+
/-- Collect all fvars in the expression, and return their names. -/
153+
private def collectFVars (e : Expr) : MetaM (Array Expr) := do
154+
let names := collectFVarsAux e
155+
let mut fvars := #[]
156+
for ldecl in ← getLCtx do
157+
if ldecl.isImplementationDetail then
158+
continue
159+
if names.contains ldecl.fvarId.name then
160+
fvars := fvars.push $ .fvar ldecl.fvarId
161+
return fvars
162+
163+
164+
private def abstractAllLambdaFVars (e : Expr) : MetaM Expr := do
165+
let mut e' := e
166+
while e'.hasFVar do
167+
let fvars ← collectFVars e'
168+
if fvars.isEmpty then
169+
break
170+
e' ← Meta.mkLambdaFVars fvars e'
171+
return e'
172+
137173
/--
138174
Evaluates the current status of a proof, returning a string description.
139175
Main states include:
@@ -153,25 +189,39 @@ def getProofStatus (proofState : ProofSnapshot) : M m String := do
153189
| none => return "Error: Goal not assigned"
154190
| some pf => do
155191
let pf ← instantiateMVars pf
192+
193+
-- First check that the proof has the expected type
156194
let pft ← Meta.inferType pf >>= instantiateMVars
157-
if pf.hasSorry then
158-
return "Incomplete: contains sorry"
195+
let expectedType ← Meta.inferType (mkMVar goalId) >>= instantiateMVars
196+
unless (← Meta.isDefEq pft expectedType) do
197+
return s!"Error: proof has type {pft} but root goal has type {expectedType}"
198+
199+
let pf ← goalId.withContext $ abstractAllLambdaFVars pf
200+
let pft ← Meta.inferType pf >>= instantiateMVars
201+
159202
if pf.hasExprMVar then
160203
return "Incomplete: contains metavariable(s)"
161204

162-
let decl := Declaration.defnDecl ({
205+
-- Find all level parameters
206+
let usedLevels := collectLevelParams {} pft
207+
let usedLevels := collectLevelParams usedLevels pf
208+
209+
let decl := Declaration.defnDecl {
163210
name := Name.anonymous,
164211
type := pft,
165212
value := pf,
166-
levelParams := (collectLevelParams {} pft).params.toList,
213+
levelParams := usedLevels.params.toList,
167214
hints := ReducibilityHints.opaque,
168215
safety := DefinitionSafety.safe
169-
})
216+
}
170217

171218
try
172219
let _ ← addDecl decl
173220
catch ex =>
174221
return s!"Error: kernel type check failed: {← ex.toMessageData.toString}"
222+
223+
if pf.hasSorry then
224+
return "Incomplete: contains sorry"
175225
return "Completed"
176226

177227
| _ => return "Not verified: more than one initial goal"
@@ -251,19 +301,19 @@ def runCommand (s : Command) : M IO (CommandResponse ⊕ Error) := do
251301
if notFound then
252302
return .inr ⟨"Unknown environment."
253303
let initialCmdState? := cmdSnapshot?.map fun c => c.cmdState
254-
let (cmdState, messages, trees) ← try
304+
let (initialCmdState, cmdState, messages, trees) ← try
255305
IO.processInput s.cmd initialCmdState?
256306
catch ex =>
257307
return .inr ⟨ex.toString⟩
258308
let messages ← messages.mapM fun m => Message.of m
259309
-- For debugging purposes, sometimes we print out the trees here:
260310
-- trees.forM fun t => do IO.println (← t.format)
261-
let sorries ← sorries trees (initialCmdState?.map (·.env)) none
311+
let sorries ← sorries trees initialCmdState.env none
262312
let sorries ← match s.rootGoals with
263-
| some true => pure (sorries ++ (← collectRootGoalsAsSorries trees))
313+
| some true => pure (sorries ++ (← collectRootGoalsAsSorries trees initialCmdState.env))
264314
| _ => pure sorries
265315
let tactics ← match s.allTactics with
266-
| some true => tactics trees
316+
| some true => tactics trees initialCmdState.env
267317
| _ => pure []
268318
let cmdSnapshot :=
269319
{ cmdState

test/assumption_proof.expected.out

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{"sorries":
2+
[{"proofState": 0,
3+
"pos": {"line": 1, "column": 49},
4+
"goal": "x : Nat\nh1 : x = 2\n⊢ x = 2",
5+
"endPos": {"line": 1, "column": 54}}],
6+
"messages":
7+
[{"severity": "warning",
8+
"pos": {"line": 1, "column": 8},
9+
"endPos": {"line": 1, "column": 10},
10+
"data": "declaration uses 'sorry'"}],
11+
"env": 0}
12+
13+
{"proofStatus": "Completed", "proofState": 1, "goals": []}
14+

test/assumption_proof.in

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{"cmd": "theorem aa (x : Nat) (h1 : x = 2) : x = 2 := by sorry"}
2+
3+
{"tactic": "assumption", "proofState": 0}

test/name_generator.expected.out

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
"proofState": 7,
4444
"goals": []}
4545

46-
{"proofStatus": "Incomplete: contains sorry",
46+
{"proofStatus": "Incomplete: contains metavariable(s)",
4747
"proofState": 8,
4848
"messages":
4949
[{"severity": "error",
@@ -52,7 +52,7 @@
5252
"data": "unsolved goals\nx : Int\nh0 : x > 0\n⊢ x > 0"}],
5353
"goals": []}
5454

55-
{"proofStatus": "Incomplete: contains sorry",
55+
{"proofStatus": "Incomplete: contains metavariable(s)",
5656
"proofState": 9,
5757
"messages":
5858
[{"severity": "error",

test/proof_branching.expected.out

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{"sorries":
2+
[{"proofState": 0,
3+
"pos": {"line": 1, "column": 75},
4+
"goal": "p q r : Prop\nh1 : p ∧ q\nh2 : q → r\n⊢ p ∧ r",
5+
"endPos": {"line": 1, "column": 80}}],
6+
"messages":
7+
[{"severity": "warning",
8+
"pos": {"line": 1, "column": 8},
9+
"endPos": {"line": 1, "column": 19},
10+
"data": "declaration uses 'sorry'"}],
11+
"env": 0}
12+
13+
{"proofStatus": "Incomplete: open goals remain",
14+
"proofState": 1,
15+
"goals":
16+
["case left\np q r : Prop\nh1 : p ∧ q\nh2 : q → r\n⊢ p",
17+
"case right\np q r : Prop\nh1 : p ∧ q\nh2 : q → r\n⊢ r"]}
18+
19+
{"proofStatus": "Incomplete: open goals remain",
20+
"proofState": 2,
21+
"goals": ["case right\np q r : Prop\nh1 : p ∧ q\nh2 : q → r\n⊢ r"]}
22+
23+
{"proofStatus": "Incomplete: open goals remain",
24+
"proofState": 3,
25+
"goals": ["case right\np q r : Prop\nh1 : p ∧ q\nh2 : q → r\n⊢ q"]}
26+
27+
{"proofStatus": "Completed", "proofState": 4, "goals": []}
28+

test/proof_branching.in

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{"cmd": "theorem complex_and (p q r : Prop) (h1 : p ∧ q) (h2 : q → r) : p ∧ r := by sorry"}
2+
3+
{"tactic": "apply And.intro", "proofState": 0}
4+
5+
{"tactic": "exact h1.left", "proofState": 1}
6+
7+
{"tactic": "apply h2", "proofState": 2}
8+
9+
{"tactic": "exact h1.right", "proofState": 3}
10+

test/proof_branching2.expected.out

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{"sorries":
2+
[{"proofState": 0,
3+
"pos": {"line": 1, "column": 43},
4+
"goal": "p q : Prop\n⊢ p ∧ q → q ∧ p",
5+
"endPos": {"line": 1, "column": 48}}],
6+
"messages":
7+
[{"severity": "warning",
8+
"pos": {"line": 1, "column": 0},
9+
"endPos": {"line": 1, "column": 7},
10+
"data": "declaration uses 'sorry'"}],
11+
"env": 0}
12+
13+
{"proofStatus": "Incomplete: open goals remain",
14+
"proofState": 1,
15+
"goals": ["p q : Prop\nh : p ∧ q\n⊢ q ∧ p"]}
16+
17+
{"proofStatus": "Incomplete: open goals remain",
18+
"proofState": 2,
19+
"goals": ["p q : Prop\nh : p ∧ q\nhp : p\n⊢ q ∧ p"]}
20+
21+
{"proofStatus": "Incomplete: open goals remain",
22+
"proofState": 3,
23+
"goals": ["p q : Prop\nh : p ∧ q\nhp : p\nhq : q\n⊢ q ∧ p"]}
24+
25+
{"proofStatus": "Incomplete: open goals remain",
26+
"proofState": 4,
27+
"goals":
28+
["case left\np q : Prop\nh : p ∧ q\nhp : p\nhq : q\n⊢ q",
29+
"case right\np q : Prop\nh : p ∧ q\nhp : p\nhq : q\n⊢ p"]}
30+
31+
{"proofStatus": "Incomplete: open goals remain",
32+
"proofState": 5,
33+
"goals": ["case right\np q : Prop\nh : p ∧ q\nhp : p\nhq : q\n⊢ p"]}
34+
35+
{"proofStatus": "Completed", "proofState": 6, "goals": []}
36+

test/proof_branching2.in

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{"cmd": "example (p q : Prop) : p ∧ q → q ∧ p := by sorry"}
2+
3+
{"tactic": "intro h", "proofState": 0}
4+
5+
{"tactic": "have hp : p := h.left", "proofState": 1}
6+
7+
{"tactic": "have hq : q := h.right", "proofState": 2}
8+
9+
{"tactic": "apply And.intro", "proofState": 3}
10+
11+
{"tactic": "exact hq", "proofState": 4}
12+
13+
{"tactic": "exact hp", "proofState": 5}
14+
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{"sorries":
2+
[{"proofState": 0,
3+
"pos": {"line": 1, "column": 62},
4+
"goal": "x y z : Nat\nh1 : x = y\nh2 : y = z\n⊢ x = z",
5+
"endPos": {"line": 1, "column": 67}}],
6+
"messages":
7+
[{"severity": "warning",
8+
"pos": {"line": 1, "column": 0},
9+
"endPos": {"line": 1, "column": 7},
10+
"data": "declaration uses 'sorry'"}],
11+
"env": 0}
12+
13+
{"proofStatus": "Completed", "proofState": 1, "goals": []}
14+
15+
{"sorries":
16+
[{"proofState": 2,
17+
"pos": {"line": 1, "column": 64},
18+
"goal": "f : Nat → Nat\nn : Nat\nh : n = 3\n⊢ f n = f 3",
19+
"endPos": {"line": 1, "column": 69}}],
20+
"messages":
21+
[{"severity": "warning",
22+
"pos": {"line": 1, "column": 0},
23+
"endPos": {"line": 1, "column": 7},
24+
"data": "declaration uses 'sorry'"}],
25+
"env": 1}
26+
27+
{"proofStatus": "Completed", "proofState": 3, "goals": []}
28+

0 commit comments

Comments
 (0)