| import Pantograph.Frontend |
| import Test.Common |
|
|
| open Lean Pantograph Frontend |
|
|
| namespace Pantograph.Test.Frontend.Distil |
|
|
| abbrev TestM := TestT MetaM |
| abbrev Test := TestM Unit |
|
|
| private def collectSorrysFromSource (source: String) (options : Frontend.GoalCollectionOptions := {}) |
| : CoreM (List GoalState) := do |
| let (context, state) β do Frontend.createContextStateFromFile source (env? := β getEnv) |
| let m := show FrontendM _ from Frontend.mapCompilationSteps Ξ» step => do |
| return (step.before, β Frontend.collectSorrys step options) |
| let li β m.run {} |>.run context |>.run' state |
| let goalStates β li.filterMapM Ξ» (env, sorrys) => withEnv env do |
| if sorrys.isEmpty then |
| return .none |
| let { state, .. } β (Frontend.sorrysToGoalState sorrys).run' |
| return .some state |
| return goalStates |
|
|
| private def test_sorry_in_middle: Test := do |
| let sketch := " |
| example : β (n m: Nat), n + m = m + n := by |
| intros n m |
| sorry |
| " |
| let goalStates β collectSorrysFromSource sketch |
| let [goalState] := goalStates | panic! s!"Incorrect number of states: {goalStates.length}" |
| checkEq "goals" ((β goalState.serializeGoals (options := {})).map (Β·.devolatilize)) #[ |
| { |
| target := { pp? := "n + m = m + n" }, |
| vars := #[{ |
| userName := `n, |
| type? := .some { pp? := "Nat" }, |
| }, { |
| userName := `m, |
| type? := .some { pp? := "Nat" }, |
| } |
| ], |
| } |
| ] |
| let .success st _ β runTermElabMInMeta $ goalState.tryDraft .unfocus "have : 1 + 1 = 2 := by sorry\nsorry" |
| | fail "Draft tactic failed" |
| checkEq "goals" st.goals.length 2 |
|
|
| private def test_sorry_in_coupled: Test := do |
| let sketch := " |
| example : β (y: Nat), β (x: Nat), y + 1 = x := by |
| intro y |
| apply Exists.intro |
| case h => sorry |
| case w => sorry |
| " |
| let goalStates β collectSorrysFromSource sketch |
| let [goalState] := goalStates | panic! s!"Incorrect number of states: {goalStates.length}" |
| checkEq "goals" ((β goalState.serializeGoals (options := {})).map (Β·.devolatilize)) #[ |
| { |
| target := { pp? := "y + 1 = ?w" }, |
| vars := #[{ |
| userName := `y, |
| type? := .some { pp? := "Nat" }, |
| } |
| ], |
| }, |
| { |
| userName? := .some `w, |
| target := { pp? := "Nat" }, |
| vars := #[{ |
| userName := `y, |
| type? := .some { pp? := "Nat" }, |
| } |
| ], |
| } |
| ] |
|
|
| private def test_sorry_with_local_instance (tacticMode : Bool) : Test := do |
| let placeholder := if tacticMode then "by sorry" else "sorry" |
| let sketch := s!" |
| def test (Ξ± : Type) [s : Inhabited Ξ±] : Ξ± := @Inhabited.default Ξ± s |
| example (Ξ± : Type) [Inhabited Ξ±] : Ξ± := {placeholder} |
| " |
| let goalStates β collectSorrysFromSource sketch |
| let [goalState] := goalStates | panic! s!"Incorrect number of states: {goalStates.length}" |
| let result β runTermElabMInMeta $ goalState.tryTactic .unfocus "exact test Ξ±" |
| checkTrue "success" $ result matches .success .. |
| match result with |
| | .success .. => return () |
| | .failure messages => |
| let messages β messages.mapM (Β·.toString) |
| fail s!"Could not execute tactic {messages}" |
| | .parseError e => |
| fail s!"Parse error: {e}" |
| | .invalidAction e => |
| fail s!"Invalid action: {e}" |
|
|
| private def test_sorry_circular : Test := do |
| let sketch := " |
| theorem test (p q : Prop) (hp : p) (hq : q) : p β§ q β§ p := by sorry |
| " |
| let goalStates β collectSorrysFromSource sketch |
| let [goalState] := goalStates | panic! s!"Incorrect number of states: {goalStates.length}" |
| let result β runTermElabMInMeta $ goalState.tryTactic .unfocus "exact test" |
| checkTrue "failure" $ result matches .failure .. |
| match result with |
| | .success .. => |
| fail s!"This should not succeed" |
| | .failure .. => |
| return () |
| | .parseError e => |
| fail s!"Parse error: {e}" |
| | .invalidAction e => |
| fail s!"Invalid action: {e}" |
|
|
| private def test_environment_capture: Test := do |
| let sketch := " |
| def mystery (n: Nat) := n + 1 |
| |
| example (n: Nat) : mystery n + 1 = n + 2 := sorry |
| " |
| let goalStates β collectSorrysFromSource sketch |
| let [goalState] := goalStates | panic! s!"Incorrect number of states: {goalStates.length}" |
| checkEq "goals" ((β goalState.serializeGoals (options := {})).map (Β·.devolatilize)) #[ |
| { |
| target := { pp? := "mystery n + 1 = n + 2" }, |
| vars := #[{ |
| userName := `n, |
| type? := .some { pp? := "Nat" }, |
| }], |
| } |
| ] |
|
|
| private def test_capture_type_mismatch : Test := do |
| let input := " |
| def mystery (k: Nat) : Nat := true |
| " |
| let options := { collectTypeErrors := true } |
| let goalStates β collectSorrysFromSource input options |
| let [goalState] := goalStates | panic! s!"Incorrect number of states: {goalStates.length}" |
| checkEq "goals" ((β goalState.serializeGoals).map (Β·.devolatilize)) #[ |
| { |
| target := { pp? := "Nat" }, |
| vars := #[{ |
| userName := `k, |
| type? := .some { pp? := "Nat" }, |
| }], |
| } |
| ] |
|
|
| def test_capture_type_mismatch_in_binder : Test := do |
| let input := " |
| example (p: Prop) (h: (β (x: Prop), Nat) β p): p := h (Ξ» (y: Nat) => 5) |
| " |
| let options := { collectTypeErrors := true } |
| let goalStates β collectSorrysFromSource input options |
| let [goalState] := goalStates | panic! s!"Incorrect number of states: {goalStates.length}" |
| checkEq "goals" ((β goalState.serializeGoals (options := {})).map (Β·.devolatilize)) #[] |
|
|
| private def test_distil_simple : Test := do |
| let input := " |
| set_option pp.analyze true |
| theorem mystery : β (p q : Prop), p β¨ q β q β¨ p := sorry |
| " |
| let [_dst@{ goalState := state }] β distilSearchTargets (β getEnv) input |
| | fail "Incorrect number of search states" |
| let .success state _ β (state.tryTactic .unfocus "intro p q").run' (ctx := defaultElabContext) |
| | fail "`intro` failed" |
| checkEq "goals" state.goals.length 1 |
|
|
| private def test_distil_tail : Test := do |
| let input := " |
| theorem mystery : β (n m: Nat), n + m = m + n := by |
| intros n m |
| sorry |
| " |
| let [_dst@{ goalState := state }] β distilSearchTargets (β getEnv) input { ignoreValues := false } |
| | fail "Incorrect number of search states" |
| checkEq "start" ((β state.serializeGoals {}).map (Β·.devolatilize)) |
| #[{ |
| target := { pp? := "n + m = m + n" }, |
| vars := #[{ |
| userName := `n, |
| type? := .some { pp? := "Nat" }, |
| }, { |
| userName := `m, |
| type? := .some { pp? := "Nat" }, |
| } |
| ], |
| }] |
|
|
| private def test_distil_induction : Test := do |
| let input := " |
| theorem mystery : β (n m: Nat), n + m = m + n := by |
| intros n m |
| induction n with |
| | zero => |
| have h1 : 0 + m = m := sorry |
| sorry |
| | succ n ih => |
| have h2 : n + m = m := sorry |
| sorry |
| " |
| let [_dst@{ goalState := state }] β distilSearchTargets (β getEnv) input { ignoreValues := false } |
| | fail "Incorrect number of search states" |
| let n' := .mkSimple "nβ" |
| checkEq "start" ((β state.serializeGoals {}).map (Β·.devolatilize)) #[ |
| { |
| target := { pp? := "n + 1 + m = m + (n + 1)" }, |
| vars := #[ |
| { var n' "Nat" with isInaccessible := true }, |
| var `m "Nat", |
| var `n "Nat", |
| var `ih "n + m = m + n", |
| { var `h2 "n + m = m" with value? := .some { pp? := "?m.5" }}, |
| ], |
| }, |
| { |
| target := { pp? := "n + m = m" }, |
| vars := #[ |
| { var n' "Nat" with isInaccessible := true }, |
| var `m "Nat", |
| var `n "Nat", |
| var `ih "n + m = m + n", |
| ], |
| }, |
| { |
| target := { pp? := "0 + m = m + 0" }, |
| vars := #[ |
| var `n "Nat", |
| var `m "Nat", |
| { var `h1 "0 + m = m" with value? := .some { pp? := "?m.2" }}, |
| ], |
| }, |
| { |
| target := { pp? := "0 + m = m" }, |
| vars := #[ |
| var `n "Nat", |
| var `m "Nat", |
| ], |
| }, |
| ] |
| where |
| var (userName : Name) (type : String) : Protocol.Variable := { |
| userName, |
| type? := .some { pp? := type }, |
| } |
|
|
| private def test_distil_instance (tacticMode : Bool) : Test := do |
| let placeholder := if tacticMode then "by sorry" else "sorry" |
| let input := s!" |
| def test (Ξ± : Type) [s : Inhabited Ξ±] : Ξ± := @Inhabited.default Ξ± s |
| def mystery (Ξ± : Type) [Inhabited Ξ±] : Ξ± := {placeholder} |
| " |
| let [_dst@{ goalState := state }] β distilSearchTargets (β getEnv) input { ignoreValues := false } |
| | fail "Incorrect number of search states" |
| checkEq "start" ((β state.serializeGoals {}).map (Β·.devolatilize)) |
| #[{ |
| target := { pp? := .some "Ξ±" }, |
| vars := #[ |
| { |
| userName := `Ξ±, |
| type? := .some { pp? := .some "Type" } |
| }, |
| { |
| userName := .mkSimple "instβ", |
| isInaccessible := true, |
| type? := .some { pp? := .some "Inhabited Ξ±" } |
| }, |
| ] |
| }] |
| let state? β (state.tryTactic .unfocus "exact test Ξ±").run' (ctx := defaultElabContext) |
| match state? with |
| | .success state _ => |
| checkEq "goals" state.goals.length 0 |
| checkTrue "root" state.isSolved |
| | .failure messages => |
| let messages β messages.mapM (Β·.toString) |
| checkEq "messages" messages #[]; |
| fail "failed" |
| | .parseError e => |
| fail s!"Parse error: {e}" |
| | .invalidAction e => |
| fail s!"Invalid action: {e}" |
|
|
| private def test_distil_environment_capture : Test := do |
| let input := " |
| def mystery (n: Nat) := n + 1 |
| |
| theorem property (n: Nat) : mystery n + 1 = n + 2 := sorry |
| " |
| let [_dst@{ goalState := state }] β distilSearchTargets (β getEnv) input { ignoreValues := false } |
| | fail "Incorrect number of search states" |
| let goals := (β state.serializeGoals).map (Β·.devolatilize) |
| checkEq "goals" goals #[ |
| { |
| target := { pp? := "mystery n + 1 = n + 2" }, |
| vars := #[{ |
| userName := `n, |
| type? := .some { pp? := "Nat" }, |
| }], |
| } |
| ] |
| checkFalse "root" state.isSolved |
| let .success state _ β runTermElabMInMeta do state.tryTactic .unfocus "rfl" |
| | fail "Tactic block failed" |
| checkTrue "root" state.isSolved |
|
|
| private def test_distil_circular : Test := do |
| let input := " |
| theorem test (p q : Prop) (hp : p) (hq : q) : p β§ q β§ p := by sorry |
| " |
| let id := "test" |
| let [_dst@{ goalState := state }] β distilSearchTargets (β getEnv) input |
| | fail "Incorrect number of search states" |
| let result β (state.tryTactic .unfocus s!"exact {id} p q hp hq").run' (ctx := defaultElabContext) |
| match result with |
| | .success .. => |
| fail s!"This should not succeed" |
| | .failure messages => |
| let messages β messages.mapM (Β·.toString) |
| checkEq "failure" messages #[s!"{β getFileName}:0:0: error(lean.unknownIdentifier): Unknown identifier `{id}`\n"] |
| | .parseError e => |
| fail s!"Parse error: {e}" |
| | .invalidAction e => |
| fail s!"Invalid action: {e}" |
|
|
| private def test_distil_companion : Test := do |
| let input := " |
| def f : Nat β Nat := sorry |
| def g : Nat β Nat := Ξ» x => x + 1 |
| theorem mystery (n : Nat) : f n = g n := sorry |
| " |
| let [_dst@{ goalState := state }] β distilSearchTargets (β getEnv) input |
| | fail "Incorrect number of search states" |
| checkEq "start" ((β state.serializeGoals {}).map (Β·.devolatilize)) |
| #[{ |
| target := { pp? := .some "{ f // β (n : Nat), f n = g n }" }, |
| }] |
|
|
| private def test_distil_multiple_cond : Test := do |
| let input := " |
| def f : Nat β Nat := sorry |
| theorem mystery1 : f 1 = 1 := sorry |
| theorem mystery2 : f 2 = 2 := sorry |
| " |
| let [_dst@{ goalState := state }] β distilSearchTargets (β getEnv) input |
| | fail "Incorrect number of search states" |
| checkEq "start" ((β state.serializeGoals {}).map (Β·.devolatilize)) |
| #[{ |
| target := { pp? := .some "{ f // f 1 = 1 β§ f 2 = 2 }" }, |
| }] |
|
|
| private def test_distil_existing_value : Test := do |
| let input := " |
| def f : Nat β Nat := Ξ» x => x + sorry |
| theorem mystery1 : f 1 = 2 := sorry |
| theorem mystery2 : f 2 = 4 := sorry |
| " |
| let [_dst@{ goalState := state }] β distilSearchTargets (β getEnv) input { ignoreValues := false } |
| | fail "Incorrect number of search states" |
| checkEq "start" ((β state.serializeGoals {}).map (Β·.devolatilize)) |
| #[ |
| { |
| userName? := `f, |
| vars := #[{ userName := `x, type? := .some { pp? := .some "Nat" } }] |
| target := { pp? := .some "Nat" }, |
| }, |
| { |
| target := { pp? := .some "(fun x => x + ?f) 1 = 2" }, |
| }, |
| { |
| target := { pp? := .some "(fun x => x + ?f) 2 = 4" }, |
| }, |
| ] |
| checkFalse "root" state.isSolved |
| let .success state _ β runTermElabMInMeta do state.tryTactic .unfocus "exact x; rfl; rfl" |
| | fail "Tactic block failed" |
| checkEq "goals" state.goals.length 0 |
| checkTrue "root" state.isSolved |
|
|
| /-- Tests handling of newline chars -/ |
| private def test_distil_predicate : Test := do |
| let input := " |
| structure Command where |
| prog : String |
| args : List String |
| |
| deriving Repr, DecidableEq |
| |
| def p (s : String) : Prop := s = \"ls\" |
| |
| theorem mystery (s : String) : p s := sorry |
| " |
| let [_dst@{ goalState := state }] β distilSearchTargets (β getEnv) input |
| | fail "Incorrect number of search states" |
| checkTrue "has `p" <| (state.env.find? `p).isSome |
| let state? β (state.tryDraft .unfocus "by\n unfold p\n sorry").run' (ctx := defaultElabContext) |
| match state? with |
| | .success state _ => |
| checkEq "goals" state.goals.length 1 |
| | .failure messages => |
| let messages β messages.mapM (Β·.toString) |
| checkEq "messages" messages #[]; |
| fail "failed" |
| | .parseError e => |
| fail s!"Parse error: {e}" |
| | .invalidAction e => |
| fail s!"Invalid action: {e}" |
|
|
|
|
| def suite (env : Environment): List (String Γ IO LSpec.TestSeq) := |
| let tests := [ |
| ("sorry in middle", test_sorry_in_middle), |
| ("sorry in coupled", test_sorry_in_coupled), |
| ("sorry with local instances (term)", test_sorry_with_local_instance false), |
| ("sorry with local instances (tactic)", test_sorry_with_local_instance true), |
| ("sorry circular", test_sorry_circular), |
| ("environment_capture", test_environment_capture), |
| ("capture_type_mismatch", test_capture_type_mismatch), |
| --("capture_type_mismatch_in_binder", test_capture_type_mismatch_in_binder), |
| ("distil simple", test_distil_simple), |
| ("distil tail", test_distil_tail), |
| ("distil induction", test_distil_induction), |
| ("distil instance (term)", test_distil_instance false), |
| ("distil instance (true)", test_distil_instance true), |
| ("distil environment capture", test_distil_environment_capture), |
| ("distil circular", test_distil_circular), |
| ("distil companion", test_distil_companion), |
| ("distil multiple conditions", test_distil_multiple_cond), |
| ("distil existing value", test_distil_existing_value), |
| ("distil predicate", test_distil_predicate), |
| ] |
| tests.map (fun (name, test) => (name, runMetaMSeq env $ runTest test)) |
|
|