src/repl.lean (502 lines of code) (raw):
/-
Copyright (c) 2021 OpenAI. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Author(s): Stanislas Polu, Jesse Michael Han
REPL implementation to interact with Lean through stdio at a specific
declaration.
-/
import tactic
import data.string.basic
import all
import util.io
import util.tactic
import basic.table
import tools.shrink_proof
import tools.try_finish
section main
setup_tactic_parser
meta structure LeanREPLRequest : Type :=
(cmd : string)
(sid: string)
(tsid: string)
(tac: string)
(name: string)
(open_ns: string)
(term: string)
meta structure LeanREPLResponse : Type :=
(sid : option string)
(tsid : option string)
(tactic_state : option string)
(error: option string)
(proof_steps : list (string × string))
meta structure parent : Type :=
(tsid : string)
(action : string)
meta structure LeanREPLState : Type :=
(state : dict string (dict string (tactic_state × option parent)))
(next_sid : ℕ)
namespace LeanREPLState
meta def insert_ts (σ : LeanREPLState) (sid) (tsid) (ts) (parent : option parent): LeanREPLState :=
⟨dict.insert sid (dict.insert tsid (ts, parent) (σ.1.get_default (dict.empty) sid)) σ.1, σ.2⟩
meta def get_ts_parents (σ : LeanREPLState) (sid) (tsid) : option (tactic_state × option parent) :=
(σ.1.get_default (dict.empty) sid).get tsid
meta def get_ts (σ : LeanREPLState) (sid) (tsid) : option tactic_state :=
option.map prod.fst $ σ.get_ts_parents sid tsid
meta def get_next_tsid (σ : LeanREPLState) (sid) : string := (format! "{(σ.1.get_default (dict.empty) sid).size}").to_string
meta def erase_search (σ : LeanREPLState) (sid) : LeanREPLState := ⟨σ.1.erase sid, σ.2⟩
meta def get_next_sid (σ : LeanREPLState) : string := (format! "{σ.2}").to_string
meta def incr_next_sid (σ : LeanREPLState) : LeanREPLState := ⟨σ.1, σ.2+1⟩
end LeanREPLState
meta instance : has_from_json LeanREPLRequest := ⟨λ msg, match msg with
| (json.array [json.of_string cmd, json.array args]) := match cmd with
| "run_tac" := match json.array args with
| (json.array [json.of_string sid, json.of_string tsid, json.of_string tac]) := pure ⟨cmd, sid, tsid, tac, "", "", ""⟩
| exc := tactic.fail format!"request_parsing_error: cmd={cmd} data={exc}"
end
| "conjecture_set" := match json.array args with
| (json.array [json.of_string sid, json.of_string tsid, json.of_string term]) := pure ⟨cmd, sid, tsid, "", "", "", term⟩
| exc := tactic.fail format!"request_parsing_error: cmd={cmd} data={exc}"
end
| "conjecture_assume" := match json.array args with
| (json.array [json.of_string sid, json.of_string tsid, json.of_string term]) := pure ⟨cmd, sid, tsid, "", "", "", term⟩
| exc := tactic.fail format!"request_parsing_error: cmd={cmd} data={exc}"
end
| "init_search" := match json.array args with
| (json.array [json.of_string name, json.of_string open_ns]) := pure ⟨cmd, "", "", "", name, open_ns, ""⟩
| exc := tactic.fail format!"request_parsing_error: cmd={cmd} data={exc}"
end
| "clear_search" := match json.array args with
| (json.array [json.of_string sid]) := pure ⟨cmd, sid, "" , "", "", "", ""⟩
| exc := tactic.fail format!"request_parsing_error: cmd={cmd} data={exc}"
end
| "shrink_proof" := match json.array args with
| (json.array [json.of_string sid, json.of_string tsid]) := do
pure ⟨cmd, sid, tsid , "", "", "", ""⟩
| exc := tactic.fail format!"request_parsing_error: cmd={cmd} data={exc}"
end
| "try_finish" := match json.array args with
| (json.array [json.of_string sid, json.of_string tsid]) := do
pure ⟨cmd, sid, tsid , "", "", "", ""⟩
| exc := tactic.fail format!"request_parsing_error: cmd={cmd} data={exc}"
end
| exc := tactic.fail format!"request_parsing_error: data={exc}"
end
| exc := tactic.fail format!"request_parsing_error: data={exc}"
end
⟩
@[reducible]
meta def LeanREPL := state_t LeanREPLState io
meta def LeanREPL.forever (x : LeanREPL unit) : LeanREPL unit := do
σ₀ ← get,
state_t.lift $ io.iterate σ₀ $ λ σ, do {
(_, σ') ← x.run σ,
return (some σ')
},
state_t.lift $ io.fail' $ format! "[LeanREPL.forever] unreachable code"
meta def record_ts {m} [monad m] (sid: string) (ts : tactic_state) (parent : option parent) : (state_t LeanREPLState m) string := do {
σ ← get,
let tsid := σ.get_next_tsid sid,
modify $ λ σ, σ.insert_ts sid tsid ts parent,
pure tsid
}
meta def LeanREPLResponse.to_json: LeanREPLResponse → json
| ⟨sid, tsid, ts, err, steps⟩ :=
json.object [
⟨"search_id", match sid with
| none := json.null
| some sid := json.of_string sid
end⟩,
⟨"tactic_state_id", match tsid with
| none := json.null
| some tsid := json.of_string tsid
end⟩,
⟨"tactic_state", match ts with
| none := json.null
| some ts := json.of_string ts
end⟩,
⟨"error", match err with
| none := json.null
| some err := json.of_string err
end⟩,
⟨"proof_steps", json.array (steps.map $ λ ⟨ts_str, action⟩, json.array [json.of_string ts_str, json.of_string action])⟩
]
meta instance : has_to_format LeanREPLResponse :=
⟨has_to_format.to_format ∘ LeanREPLResponse.to_json⟩
meta def parse_theorem_name (nm: string) : tactic name :=
do lean.parser.run_with_input ident nm
meta def parse_open_namespace (open_ns: string) : tactic (list name) :=
do lean.parser.run_with_input (many ident) open_ns
meta def handle_init_search
(req : LeanREPLRequest)
: LeanREPL LeanREPLResponse := do {
σ ← get,
-- Parse declaration name.
decl_name ← state_t.lift $ io.run_tactic'' $ do {
parse_theorem_name req.name
},
-- Parse open namespaces.
decl_open_ns ← state_t.lift $ io.run_tactic'' $ do {
parse_open_namespace req.open_ns
},
-- Check that the declaration is a theorem.
is_theorem ← state_t.lift $ io.run_tactic'' $ do {
tactic.is_theorem decl_name
} <|> pure ff,
match is_theorem with
-- The declaration is not a theorem, return an error.
| ff := do {
let err := format! "not_a_theorem: name={req.name} open_ns={req.open_ns}",
pure ⟨none, none, none, some err.to_string, []⟩
}
-- The declaration is a theorem, set the env with open namespaces to it and
-- generate a new tactic state.
| tt := do {
ts ← state_t.lift $ io.run_tactic'' $ do {
env ← tactic.get_env,
decl ← env.get decl_name,
let g := decl.type,
tactic.set_goal_to g,
lean_file ← env.decl_olean decl_name,
tactic.set_env_core $ environment.for_decl_of_imported_module lean_file decl_name,
add_open_namespaces decl_open_ns,
tactic.read
},
let sid := σ.get_next_sid,
modify $ λ σ, σ.incr_next_sid,
tsid ← record_ts sid ts none,
ts_str ← (state_t.lift ∘ io.run_tactic'') $ postprocess_tactic_state ts,
pure $ ⟨sid, tsid, ts_str, none, []⟩
}
end
}
meta def handle_clear_search
(req : LeanREPLRequest)
: LeanREPL LeanREPLResponse := do {
-- Simply remove the table associated with the provided search id from the state.
modify $ λ σ, σ.erase_search req.sid,
pure $ ⟨req.sid, none, none, none, []⟩
}
meta def finalize_proof
(req : LeanREPLRequest)
(ts': tactic_state) : LeanREPL LeanREPLResponse := do {
σ ← get,
-- Retrieve the tactic state at index 0 to extract the top-level goal metavariable.
match σ.get_ts req.sid "0" with
| none := do {
let err := format! "unexpected_unknown_tsid_0: search_id={req.sid}",
pure ⟨none, none, none, some err.to_string, []⟩
}
| (some ts₀) := do {
result ← (state_t.lift ∘ io.run_tactic'') $ do {
-- Set to tactic state index 0 to retrieve the meta-variable for the top goal.
tactic.write ts₀,
[g] ← tactic.get_goals,
tgt ← tactic.infer_type g,
tactic.write ts',
pf ← tactic.get_assignment g >>= tactic.instantiate_mvars,
tactic.capture' (validate_proof tgt pf)
},
match result with
| (interaction_monad.result.success r s') := do {
tsid ← record_ts req.sid ts' (some ⟨req.tsid, req.tac⟩),
ts_str ← (state_t.lift ∘ io.run_tactic'') $ postprocess_tactic_state ts',
pure $ ⟨req.sid, tsid, ts_str, none, []⟩
}
| (interaction_monad.result.exception f p s') := do {
let msg := (f.get_or_else (λ _, format.of_string "n/a")) (),
let err := format! "proof_validation_failed: msg={msg}",
pure ⟨none, none, none, some err.to_string, []⟩
}
end
}
end
}
meta def handle_conjecture
(req : LeanREPLRequest)
: LeanREPL LeanREPLResponse := do {
σ ← get,
match (σ.get_ts req.sid req.tsid) with
| none := do {
let err := format! "unknown_id: search_id={req.sid} tactic_state_id={req.tsid}",
pure ⟨none, none, none, some err.to_string, []⟩
}
| (some ts) := do {
let conj_str := req.term,
-- Use `have` to introduce the new assumption
result_with_string ← state_t.lift $ io.run_tactic'' $ do {
tactic.write ts,
conj_name ← tactic.get_unused_name "h",
let tac_str := format! "have {conj_name} : {conj_str}",
get_tac_and_capture_result tac_str.to_string 5000 <|> do {
let msg : format := format!"parse_itactic failed on `{req.tac}`",
interaction_monad.mk_exception msg none <$> tactic.read
}
},
match result_with_string with
-- `have` was successful.
| interaction_monad.result.success _ ts' := do {
-- Narrow the tactic state to the assumption only
ts_narrowed ← (state_t.lift ∘ io.run_tactic'') $ do {
tactic.write ts',
g ← list.head <$> tactic.get_goals,
tactic.set_goals [g],
-- We need to revert all hypotheses, otherwise proof finalization will complain with
-- unknown variables.
tactic.revert_all,
tactic.read
},
-- Create a new search id, this is required so that the final check are only run on the
-- "narrowed" tactic state (tactic state of the conjecture only).
let sid := σ.get_next_sid,
modify $ λ σ, σ.incr_next_sid,
tsid ← record_ts sid ts_narrowed none,
ts_str ← (state_t.lift ∘ io.run_tactic'') $ postprocess_tactic_state ts_narrowed,
pure $ ⟨sid, tsid, ts_str, none, []⟩
}
| interaction_monad.result.exception fn pos ts' := do {
state_t.lift $ do {
let msg := (fn.get_or_else (λ _, format.of_string "n/a")) (),
let err := format! "conjecture_set_have_failed: pos={pos} msg={msg}",
pure ⟨none, none, none, some err.to_string, []⟩
}
}
end
}
end
}
meta def collect_proof_steps_aux (σ : LeanREPLState) (sid : string) : Π (tsid : string), io (list (tactic_state × string × tactic_state))
| tsid2 := do
match σ.get_ts_parents sid tsid2 with
| none := io.fail "collect_proof_steps: invalid tsid"
| (some ⟨ts2, parent⟩) :=
match parent with
| none := if tsid2 = "0" then pure [] else io.fail "no parent"
| (some ⟨tsid1, action⟩) :=
match σ.get_ts sid tsid1 with
| none := io.fail "parent doesn't exist"
| (some ts1) := do {
rest ← collect_proof_steps_aux tsid1,
pure (⟨ts1, action, ts2⟩ :: rest)
}
end
end
end
meta def collect_proof_steps (σ : LeanREPLState) (sid tsid : string) : io (list (tactic_state × string × tactic_state)) := do
rev_steps ← collect_proof_steps_aux σ sid tsid,
pure $ list.reverse rev_steps
meta def handle_shrink_proof
(req : LeanREPLRequest)
: LeanREPL LeanREPLResponse := do {
σ ← get,
match (σ.get_ts req.sid req.tsid) with
| none := do {
let err := format! "unknown_id: search_id={req.sid} tactic_state_id={req.tsid}",
pure ⟨none, none, none, some err.to_string, []⟩
}
| (some ts_final) := do {
ts_str ← state_t.lift $ io.run_tactic'' $ postprocess_tactic_state ts_final,
state_t.lift $ io.run_tac ts_final tactic.done,
steps ← state_t.lift $ collect_proof_steps σ req.sid req.tsid,
new_steps ← state_t.lift (shrink_proof steps),
new_steps ← state_t.lift $ new_steps.mmap $ λ ⟨ts1, action, _⟩, do {
ts1_str ← io.run_tactic'' $ postprocess_tactic_state ts1,
pure (ts1_str, action)
},
pure ⟨req.sid, req.tsid, ts_str, none, new_steps⟩
}
end
}
meta def handle_try_finish
(req : LeanREPLRequest)
: LeanREPL LeanREPLResponse := do {
σ ← get,
match (σ.get_ts req.sid req.tsid) with
| none := do {
let err := format! "unknown_id: search_id={req.sid} tactic_state_id={req.tsid}",
pure ⟨none, none, none, some err.to_string, []⟩
}
| (some ts) := do {
possible_action ← state_t.lift $ try_finish ts,
match possible_action with
| none := do {
let err := format! "try_finish_failed: search_id={req.sid} tactic_state_id={req.tsid}",
pure ⟨none, none, none, some err.to_string, []⟩
}
| some (action, ts') := do {
-- TODO: refactor so that finalizing a proof is a separate top-level call
goals ← state_t.lift $ io.run_tac ts' tactic.get_goals,
if goals.empty then do {
r ← finalize_proof { req with tac := action } ts',
match r.error with
| none := do {
ts_str ← state_t.lift $ io.run_tactic'' $ postprocess_tactic_state ts',
pure { r with proof_steps := [(action, ts_str)] }
}
| some err := pure r
end
} else do {
tsid ← record_ts req.sid ts' (some ⟨req.tsid, action⟩),
ts_str ← (state_t.lift ∘ io.run_tactic'') $ postprocess_tactic_state ts',
pure $ ⟨req.sid, tsid, ts_str, none, [(action, ts_str)]⟩
}
}
end
}
end
}
meta def handle_assume
(req : LeanREPLRequest)
: LeanREPL LeanREPLResponse := do {
σ ← get,
match (σ.get_ts req.sid req.tsid) with
| none := do {
let err := format! "unknown_id: search_id={req.sid} tactic_state_id={req.tsid}",
pure ⟨none, none, none, some err.to_string, []⟩
}
| (some ts) := do {
let conj_str := req.term,
-- Use `have` to introduce the new assumption
result_with_string ← state_t.lift $ io.run_tactic'' $ do {
tactic.write ts,
conj_name ← tactic.get_unused_name "h",
let tac_str := format! "have {conj_name} : {conj_str}",
get_tac_and_capture_result tac_str.to_string 5000 <|> do {
let msg : format := format!"parse_itactic failed on `{req.tac}`",
interaction_monad.mk_exception msg none <$> tactic.read
}
},
match result_with_string with
-- `have` was successful.
| interaction_monad.result.success _ ts' := do {
-- Narrow the tactic state to the initial goal with assumption.
ts_assumed ← (state_t.lift ∘ io.run_tactic'') $ do {
tactic.write ts',
(g1 :: gs) ← tactic.get_goals,
tactic.set_goals gs,
-- We need to revert all hypotheses, otherwise proof finalization will complain with
-- unknown variables.
tactic.revert_all,
tactic.read
},
-- Create a new search id, this is required so that the final check are only run on the
-- "assumed" tactic state (tactic state with additional assumption only).
let sid := σ.get_next_sid,
modify $ λ σ, σ.incr_next_sid,
tsid ← record_ts sid ts_assumed (some ⟨req.tsid, req.tac⟩),
ts_str ← (state_t.lift ∘ io.run_tactic'') $ postprocess_tactic_state ts_assumed,
pure $ ⟨sid, tsid, ts_str, none, []⟩
}
| interaction_monad.result.exception fn pos ts' := do {
state_t.lift $ do {
let msg := (fn.get_or_else (λ _, format.of_string "n/a")) (),
let err := format! "conjecture_assume_have_failed: pos={pos} msg={msg}",
pure ⟨none, none, none, some err.to_string, []⟩
}
}
end
}
end
}
meta def handle_parse_failed
(req : LeanREPLRequest)
: LeanREPL LeanREPLResponse := do {
-- A little hack that use `req` to pass error message
let err := format! "parse_failed: data={req.sid}",
pure ⟨none, none, none, some err.to_string, []⟩
}
meta def handle_run_tac
(req : LeanREPLRequest)
: LeanREPL LeanREPLResponse := do {
σ ← get,
match (σ.get_ts req.sid req.tsid) with
-- Received an unknown search id, return an error.
| none := do {
let err := format! "unknown_id: search_id={req.sid} tactic_state_id={req.tsid}",
pure ⟨none, none, none, some err.to_string, []⟩
}
-- The tactic state was retrieved from the state.
| (some ts) := do {
-- Set the tactic state and try to apply the tactic.
result_with_string ← state_t.lift $ io.run_tactic'' $ do {
tactic.write ts,
get_tac_and_capture_result req.tac 5000 <|> do {
let msg : format := format!"parse_itactic failed on `{req.tac}`",
interaction_monad.mk_exception msg none <$> tactic.read
}
},
match result_with_string with
-- The tactic application was successful.
| interaction_monad.result.success _ ts' := do {
n ← (state_t.lift ∘ io.run_tactic'') $ do {
tactic.write ts',
tactic.num_goals
},
-- monad_lift $ io.run_tactic'' $ tactic.trace format! "REMAINING SUBGOALS: {n}",
match n with
-- There is no more subgoals, check that the produced proof is valid.
| 0 := do {
finalize_proof req ts'
}
-- There are remaining subgoals, return the updated tactic state.
| n := do {
tsid ← record_ts req.sid ts' (some ⟨req.tsid, req.tac⟩),
ts_str ← (state_t.lift ∘ io.run_tactic'') $ postprocess_tactic_state ts',
pure $ ⟨req.sid, tsid, ts_str, none, []⟩
}
end
}
-- The tactic application failed, potentially return an error with the failure message.
| interaction_monad.result.exception fn pos ts' := do {
-- Some tactics such as linarith fail but result in a tactic state with no goals. Check if
-- that's the case and finalize the proof, otherwise error.
n ← (state_t.lift ∘ io.run_tactic'') $ do {
tactic.write ts',
tactic.num_goals
},
-- monad_lift $ io.run_tactic'' $ tactic.trace format! "REMAINING SUBGOALS: {n}",
match n with
-- There is no more subgoals, check that the produced proof is valid.
| 0 := do {
finalize_proof req ts'
}
-- There are remaining subgoals, return the error.
| _ := do {
state_t.lift $ do {
let msg := (fn.get_or_else (λ _, format.of_string "n/a")) (),
let err := format! "gen_tac_and_capture_res_failed: pos={pos} msg={msg}",
pure ⟨none, none, none, some err.to_string, []⟩
}
}
end
}
end
}
end
}
meta def handle_request (req : LeanREPLRequest) : LeanREPL LeanREPLResponse :=
match req.cmd with
| "run_tac" := handle_run_tac req
| "init_search" := handle_init_search req
| "clear_search" := handle_clear_search req
| "conjecture_set" := handle_conjecture req
| "conjecture_assume" := handle_assume req
| "shrink_proof" := handle_shrink_proof req
| "try_finish" := handle_try_finish req
| "parse_failed" := handle_parse_failed req
| exc := state_t.lift $ io.fail' format! "[fatal] unknown_command: cmd={exc}"
end
meta def parse_request (msg : string) : io LeanREPLRequest := do {
match json.parse msg with
| (some json_msg) := io.run_tactic'' $ has_from_json.from_json json_msg
| none := pure ⟨"parse_failed", msg, "", "", "", "", ""⟩
end
}
meta def loop : LeanREPL unit := do {
req ← (state_t.lift $ io.get_line >>= parse_request),
res ← handle_request req,
state_t.lift $ io.put_str_ln' $ format! "{(json.unparse ∘ LeanREPLResponse.to_json) res}"
}
meta def main : io unit := do {
state_t.run loop.forever ⟨dict.empty, 0⟩ $> ()
}
end main