2024-04-22 13:00:06 -07:00
|
|
|
|
"""
|
|
|
|
|
Class which manages a Pantograph instance. All calls to the kernel uses this
|
|
|
|
|
interface.
|
|
|
|
|
"""
|
2024-09-09 19:04:56 -07:00
|
|
|
|
import json, pexpect, unittest, os
|
|
|
|
|
from typing import Union
|
|
|
|
|
from pathlib import Path
|
2024-05-17 19:58:16 -07:00
|
|
|
|
from pantograph.expr import parse_expr, Expr, Variable, Goal, GoalState, \
|
2024-04-22 22:38:20 -07:00
|
|
|
|
Tactic, TacticHave, TacticCalc
|
2024-05-31 17:09:12 -07:00
|
|
|
|
from pantograph.compiler import TacticInvocation
|
2024-04-19 16:47:35 -07:00
|
|
|
|
|
2024-04-22 13:00:06 -07:00
|
|
|
|
def _get_proc_cwd():
|
2024-09-09 19:04:56 -07:00
|
|
|
|
return Path(__file__).parent
|
2024-04-19 16:47:35 -07:00
|
|
|
|
def _get_proc_path():
|
2024-09-06 22:26:37 -07:00
|
|
|
|
return _get_proc_cwd() / "pantograph-repl"
|
2024-04-22 13:00:06 -07:00
|
|
|
|
|
2024-06-03 21:52:43 -07:00
|
|
|
|
class TacticFailure(Exception):
|
|
|
|
|
pass
|
2024-04-22 13:00:06 -07:00
|
|
|
|
class ServerError(Exception):
|
|
|
|
|
pass
|
2024-04-19 16:47:35 -07:00
|
|
|
|
|
|
|
|
|
class Server:
|
|
|
|
|
|
|
|
|
|
def __init__(self,
|
|
|
|
|
imports=["Init"],
|
2024-05-17 20:53:34 -07:00
|
|
|
|
project_path=None,
|
|
|
|
|
lean_path=None,
|
2024-09-06 22:26:37 -07:00
|
|
|
|
# Options for executing the REPL.
|
2024-09-09 18:49:35 -07:00
|
|
|
|
# Set `{ "automaticMode" : False }` to handle resumption by yourself.
|
2024-09-06 22:26:37 -07:00
|
|
|
|
options={},
|
|
|
|
|
core_options=[],
|
2024-04-19 16:47:35 -07:00
|
|
|
|
timeout=20,
|
|
|
|
|
maxread=1000000):
|
|
|
|
|
"""
|
|
|
|
|
timeout: Amount of time to wait for execution
|
|
|
|
|
maxread: Maximum number of characters to read (especially important for large proofs and catalogs)
|
|
|
|
|
"""
|
|
|
|
|
self.timeout = timeout
|
|
|
|
|
self.imports = imports
|
2024-05-17 20:53:34 -07:00
|
|
|
|
self.project_path = project_path if project_path else _get_proc_cwd()
|
|
|
|
|
self.lean_path = lean_path
|
2024-04-19 16:47:35 -07:00
|
|
|
|
self.maxread = maxread
|
|
|
|
|
self.proc_path = _get_proc_path()
|
|
|
|
|
|
|
|
|
|
self.options = options
|
2024-09-06 22:26:37 -07:00
|
|
|
|
self.core_options = core_options
|
|
|
|
|
self.args = " ".join(imports + [f'--{opt}' for opt in core_options])
|
2024-04-19 16:47:35 -07:00
|
|
|
|
self.proc = None
|
|
|
|
|
self.restart()
|
|
|
|
|
|
2024-05-17 19:52:00 -07:00
|
|
|
|
# List of goal states that should be garbage collected
|
|
|
|
|
self.to_remove_goal_states = []
|
|
|
|
|
|
2024-04-19 16:47:35 -07:00
|
|
|
|
def restart(self):
|
|
|
|
|
if self.proc is not None:
|
|
|
|
|
self.proc.close()
|
2024-05-17 20:53:34 -07:00
|
|
|
|
env = os.environ
|
|
|
|
|
if self.lean_path:
|
|
|
|
|
env = env | {'LEAN_PATH': self.lean_path}
|
|
|
|
|
|
2024-04-19 16:47:35 -07:00
|
|
|
|
self.proc = pexpect.spawn(
|
|
|
|
|
f"{self.proc_path} {self.args}",
|
|
|
|
|
encoding="utf-8",
|
2024-04-22 13:00:06 -07:00
|
|
|
|
maxread=self.maxread,
|
2024-09-06 22:26:37 -07:00
|
|
|
|
timeout=self.timeout,
|
2024-05-17 20:53:34 -07:00
|
|
|
|
cwd=self.project_path,
|
|
|
|
|
env=env,
|
2024-04-19 16:47:35 -07:00
|
|
|
|
)
|
2024-09-06 22:26:37 -07:00
|
|
|
|
self.proc.setecho(False) # Do not send any command before this.
|
|
|
|
|
ready = self.proc.readline() # Reads the "ready."
|
2024-09-09 19:04:56 -07:00
|
|
|
|
assert ready == "ready.\r\n", f"Server failed to emit ready signal: {ready}; Maybe the project needs to be rebuilt"
|
2024-09-06 22:26:37 -07:00
|
|
|
|
|
|
|
|
|
if self.options:
|
|
|
|
|
self.run("options.set", self.options)
|
2024-04-19 16:47:35 -07:00
|
|
|
|
|
2024-06-04 23:53:02 -07:00
|
|
|
|
self.run('options.set', {'printDependentMVars': True})
|
|
|
|
|
|
2024-04-19 16:47:35 -07:00
|
|
|
|
def run(self, cmd, payload):
|
2024-05-17 19:52:00 -07:00
|
|
|
|
"""
|
|
|
|
|
Runs a raw JSON command. Preferably use one of the commands below.
|
|
|
|
|
"""
|
2024-04-19 16:47:35 -07:00
|
|
|
|
s = json.dumps(payload)
|
|
|
|
|
self.proc.sendline(f"{cmd} {s}")
|
|
|
|
|
try:
|
2024-09-06 22:26:37 -07:00
|
|
|
|
line = self.proc.readline()
|
|
|
|
|
return json.loads(line)
|
2024-05-17 19:52:00 -07:00
|
|
|
|
except pexpect.exceptions.TIMEOUT as exc:
|
|
|
|
|
raise exc
|
|
|
|
|
|
|
|
|
|
def gc(self):
|
|
|
|
|
"""
|
|
|
|
|
Garbage collect deleted goal states.
|
|
|
|
|
|
|
|
|
|
Must be called periodically.
|
|
|
|
|
"""
|
2024-05-17 23:05:32 -07:00
|
|
|
|
if self.to_remove_goal_states:
|
|
|
|
|
self.run('goal.delete', {'stateIds': self.to_remove_goal_states})
|
|
|
|
|
self.to_remove_goal_states.clear()
|
2024-04-19 16:47:35 -07:00
|
|
|
|
|
2024-06-03 21:52:43 -07:00
|
|
|
|
def expr_type(self, expr: Expr) -> Expr:
|
2024-05-17 19:58:16 -07:00
|
|
|
|
"""
|
|
|
|
|
Evaluate the type of a given expression. This gives an error if the
|
|
|
|
|
input `expr` is ill-formed.
|
|
|
|
|
"""
|
|
|
|
|
result = self.run('expr.echo', {"expr": expr})
|
|
|
|
|
if "error" in result:
|
|
|
|
|
raise ServerError(result["desc"])
|
|
|
|
|
return parse_expr(result["type"])
|
|
|
|
|
|
2024-06-03 21:52:43 -07:00
|
|
|
|
def goal_start(self, expr: Expr) -> GoalState:
|
2024-04-22 13:00:06 -07:00
|
|
|
|
result = self.run('goal.start', {"expr": str(expr)})
|
|
|
|
|
if "error" in result:
|
2024-06-05 14:36:51 -07:00
|
|
|
|
print(f"Cannot start goal: {expr}")
|
2024-04-22 13:00:06 -07:00
|
|
|
|
raise ServerError(result["desc"])
|
2024-05-17 19:52:00 -07:00
|
|
|
|
return GoalState(state_id=result["stateId"], goals=[Goal.sentence(expr)], _sentinel=self.to_remove_goal_states)
|
2024-04-19 16:47:35 -07:00
|
|
|
|
|
2024-04-22 13:26:48 -07:00
|
|
|
|
def goal_tactic(self, state: GoalState, goal_id: int, tactic: Tactic) -> GoalState:
|
2024-04-22 22:38:20 -07:00
|
|
|
|
args = {"stateId": state.state_id, "goalId": goal_id}
|
|
|
|
|
if isinstance(tactic, str):
|
|
|
|
|
args["tactic"] = tactic
|
|
|
|
|
elif isinstance(tactic, TacticHave):
|
|
|
|
|
args["have"] = tactic.branch
|
|
|
|
|
elif isinstance(tactic, TacticCalc):
|
|
|
|
|
args["calc"] = tactic.step
|
2024-04-22 13:11:28 -07:00
|
|
|
|
else:
|
2024-04-22 22:38:20 -07:00
|
|
|
|
raise RuntimeError(f"Invalid tactic type: {tactic}")
|
2024-04-22 13:11:28 -07:00
|
|
|
|
result = self.run('goal.tactic', args)
|
2024-04-22 13:00:06 -07:00
|
|
|
|
if "error" in result:
|
|
|
|
|
raise ServerError(result["desc"])
|
|
|
|
|
if "tacticErrors" in result:
|
2024-06-03 21:52:43 -07:00
|
|
|
|
raise TacticFailure(result["tacticErrors"])
|
2024-04-22 13:00:06 -07:00
|
|
|
|
if "parseError" in result:
|
2024-06-03 21:52:43 -07:00
|
|
|
|
raise TacticFailure(result["parseError"])
|
2024-05-17 19:52:00 -07:00
|
|
|
|
return GoalState.parse(result, self.to_remove_goal_states)
|
2024-04-22 22:38:20 -07:00
|
|
|
|
|
|
|
|
|
def goal_conv_begin(self, state: GoalState, goal_id: int) -> GoalState:
|
|
|
|
|
result = self.run('goal.tactic', {"stateId": state.state_id, "goalId": goal_id, "conv": True})
|
|
|
|
|
if "error" in result:
|
|
|
|
|
raise ServerError(result["desc"])
|
|
|
|
|
if "tacticErrors" in result:
|
|
|
|
|
raise ServerError(result["tacticErrors"])
|
|
|
|
|
if "parseError" in result:
|
|
|
|
|
raise ServerError(result["parseError"])
|
2024-05-17 19:52:00 -07:00
|
|
|
|
return GoalState.parse(result, self.to_remove_goal_states)
|
2024-04-22 22:38:20 -07:00
|
|
|
|
|
|
|
|
|
def goal_conv_end(self, state: GoalState) -> GoalState:
|
|
|
|
|
result = self.run('goal.tactic', {"stateId": state.state_id, "goalId": 0, "conv": False})
|
|
|
|
|
if "error" in result:
|
|
|
|
|
raise ServerError(result["desc"])
|
|
|
|
|
if "tacticErrors" in result:
|
|
|
|
|
raise ServerError(result["tacticErrors"])
|
|
|
|
|
if "parseError" in result:
|
|
|
|
|
raise ServerError(result["parseError"])
|
2024-05-17 19:52:00 -07:00
|
|
|
|
return GoalState.parse(result, self.to_remove_goal_states)
|
2024-04-22 22:38:20 -07:00
|
|
|
|
|
2024-09-09 19:04:56 -07:00
|
|
|
|
def tactic_invocations(self, file_name: Union[str, Path]) -> tuple[list[str], list[TacticInvocation]]:
|
|
|
|
|
"""
|
|
|
|
|
Collect tactic invocation points in file, and return them.
|
|
|
|
|
"""
|
|
|
|
|
result = self.run('frontend.process', {
|
|
|
|
|
'fileName': str(file_name),
|
|
|
|
|
'invocations': True,
|
|
|
|
|
"sorrys": False,
|
2024-05-31 17:09:12 -07:00
|
|
|
|
})
|
2024-05-28 20:35:47 -07:00
|
|
|
|
if "error" in result:
|
|
|
|
|
raise ServerError(result["desc"])
|
2024-04-19 16:47:35 -07:00
|
|
|
|
|
2024-09-09 19:04:56 -07:00
|
|
|
|
with open(file_name, 'rb') as f:
|
2024-05-31 17:09:12 -07:00
|
|
|
|
content = f.read()
|
|
|
|
|
units = [content[begin:end].decode('utf-8') for begin,end in result['units']]
|
|
|
|
|
|
|
|
|
|
invocations = [TacticInvocation.parse(i) for i in result['invocations']]
|
|
|
|
|
return units, invocations
|
2024-05-28 20:35:47 -07:00
|
|
|
|
|
2024-09-09 19:28:35 -07:00
|
|
|
|
def load_sorry(self, command: str) -> list[GoalState]:
|
|
|
|
|
result = self.run('frontend.process', {
|
|
|
|
|
'file': command,
|
|
|
|
|
'invocations': False,
|
|
|
|
|
"sorrys": True,
|
|
|
|
|
})
|
2024-05-28 20:35:47 -07:00
|
|
|
|
if "error" in result:
|
|
|
|
|
raise ServerError(result["desc"])
|
2024-09-09 19:28:35 -07:00
|
|
|
|
states = [
|
|
|
|
|
GoalState.parse_inner(state_id, goals, self.to_remove_goal_states)
|
|
|
|
|
for (state_id, goals) in result['goalStates']
|
|
|
|
|
]
|
|
|
|
|
return states
|
2024-05-28 20:35:47 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2024-04-22 13:00:06 -07:00
|
|
|
|
def get_version():
|
2024-04-19 16:47:35 -07:00
|
|
|
|
import subprocess
|
2024-04-22 13:00:06 -07:00
|
|
|
|
with subprocess.Popen([_get_proc_path(), "--version"],
|
|
|
|
|
stdout=subprocess.PIPE,
|
|
|
|
|
cwd=_get_proc_cwd()) as p:
|
|
|
|
|
return p.communicate()[0].decode('utf-8').strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestServer(unittest.TestCase):
|
|
|
|
|
|
|
|
|
|
def test_version(self):
|
2024-09-09 18:49:35 -07:00
|
|
|
|
self.assertEqual(get_version(), "0.2.19")
|
2024-04-19 16:47:35 -07:00
|
|
|
|
|
2024-05-17 19:58:16 -07:00
|
|
|
|
def test_expr_type(self):
|
|
|
|
|
server = Server()
|
|
|
|
|
t = server.expr_type("forall (n m: Nat), n + m = m + n")
|
|
|
|
|
self.assertEqual(t, "Prop")
|
|
|
|
|
|
2024-04-22 13:00:06 -07:00
|
|
|
|
def test_goal_start(self):
|
|
|
|
|
server = Server()
|
|
|
|
|
state0 = server.goal_start("forall (p q: Prop), Or p q -> Or q p")
|
2024-05-17 19:52:00 -07:00
|
|
|
|
self.assertEqual(len(server.to_remove_goal_states), 0)
|
2024-04-22 13:00:06 -07:00
|
|
|
|
self.assertEqual(state0.state_id, 0)
|
2024-04-22 22:38:20 -07:00
|
|
|
|
state1 = server.goal_tactic(state0, goal_id=0, tactic="intro a")
|
2024-04-22 13:00:06 -07:00
|
|
|
|
self.assertEqual(state1.state_id, 1)
|
|
|
|
|
self.assertEqual(state1.goals, [Goal(
|
|
|
|
|
variables=[Variable(name="a", t="Prop")],
|
|
|
|
|
target="∀ (q : Prop), a ∨ q → q ∨ a",
|
|
|
|
|
name=None,
|
|
|
|
|
)])
|
|
|
|
|
self.assertEqual(str(state1.goals[0]),"a : Prop\n⊢ ∀ (q : Prop), a ∨ q → q ∨ a")
|
2024-04-19 16:47:35 -07:00
|
|
|
|
|
2024-05-17 19:52:00 -07:00
|
|
|
|
del state0
|
|
|
|
|
self.assertEqual(len(server.to_remove_goal_states), 1)
|
|
|
|
|
server.gc()
|
|
|
|
|
self.assertEqual(len(server.to_remove_goal_states), 0)
|
|
|
|
|
|
|
|
|
|
state0b = server.goal_start("forall (p: Prop), p -> p")
|
|
|
|
|
del state0b
|
|
|
|
|
self.assertEqual(len(server.to_remove_goal_states), 1)
|
|
|
|
|
server.gc()
|
|
|
|
|
self.assertEqual(len(server.to_remove_goal_states), 0)
|
|
|
|
|
|
2024-09-06 22:26:37 -07:00
|
|
|
|
def test_automatic_mode(self):
|
2024-04-22 22:38:20 -07:00
|
|
|
|
server = Server()
|
2024-09-06 22:26:37 -07:00
|
|
|
|
state0 = server.goal_start("forall (p q: Prop), Or p q -> Or q p")
|
|
|
|
|
self.assertEqual(len(server.to_remove_goal_states), 0)
|
|
|
|
|
self.assertEqual(state0.state_id, 0)
|
|
|
|
|
state1 = server.goal_tactic(state0, goal_id=0, tactic="intro a b h")
|
|
|
|
|
self.assertEqual(state1.state_id, 1)
|
|
|
|
|
self.assertEqual(state1.goals, [Goal(
|
|
|
|
|
variables=[
|
|
|
|
|
Variable(name="a", t="Prop"),
|
|
|
|
|
Variable(name="b", t="Prop"),
|
|
|
|
|
Variable(name="h", t="a ∨ b"),
|
|
|
|
|
],
|
|
|
|
|
target="b ∨ a",
|
|
|
|
|
name=None,
|
|
|
|
|
)])
|
|
|
|
|
state2 = server.goal_tactic(state1, goal_id=0, tactic="cases h")
|
|
|
|
|
self.assertEqual(state2.goals, [
|
|
|
|
|
Goal(
|
|
|
|
|
variables=[
|
|
|
|
|
Variable(name="a", t="Prop"),
|
|
|
|
|
Variable(name="b", t="Prop"),
|
|
|
|
|
Variable(name="h✝", t="a"),
|
|
|
|
|
],
|
|
|
|
|
target="b ∨ a",
|
|
|
|
|
name="inl",
|
|
|
|
|
),
|
|
|
|
|
Goal(
|
|
|
|
|
variables=[
|
|
|
|
|
Variable(name="a", t="Prop"),
|
|
|
|
|
Variable(name="b", t="Prop"),
|
|
|
|
|
Variable(name="h✝", t="b"),
|
|
|
|
|
],
|
|
|
|
|
target="b ∨ a",
|
|
|
|
|
name="inr",
|
|
|
|
|
),
|
|
|
|
|
])
|
|
|
|
|
state3 = server.goal_tactic(state2, goal_id=1, tactic="apply Or.inl")
|
|
|
|
|
state4 = server.goal_tactic(state3, goal_id=0, tactic="assumption")
|
|
|
|
|
self.assertEqual(state4.goals, [
|
|
|
|
|
Goal(
|
|
|
|
|
variables=[
|
|
|
|
|
Variable(name="a", t="Prop"),
|
|
|
|
|
Variable(name="b", t="Prop"),
|
|
|
|
|
Variable(name="h✝", t="a"),
|
|
|
|
|
],
|
|
|
|
|
target="b ∨ a",
|
|
|
|
|
name="inl",
|
|
|
|
|
)
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
2024-04-22 22:38:20 -07:00
|
|
|
|
def test_conv_calc(self):
|
2024-09-09 18:49:35 -07:00
|
|
|
|
server = Server(options={"automaticMode": False})
|
2024-04-22 22:38:20 -07:00
|
|
|
|
state0 = server.goal_start("∀ (a b: Nat), (b = 2) -> 1 + a + 1 = a + b")
|
|
|
|
|
|
|
|
|
|
variables = [
|
|
|
|
|
Variable(name="a", t="Nat"),
|
|
|
|
|
Variable(name="b", t="Nat"),
|
|
|
|
|
Variable(name="h", t="b = 2"),
|
|
|
|
|
]
|
|
|
|
|
state1 = server.goal_tactic(state0, goal_id=0, tactic="intro a b h")
|
|
|
|
|
state2 = server.goal_tactic(state1, goal_id=0, tactic=TacticCalc("1 + a + 1 = a + 1 + 1"))
|
|
|
|
|
self.assertEqual(state2.goals, [
|
|
|
|
|
Goal(
|
|
|
|
|
variables,
|
|
|
|
|
target="1 + a + 1 = a + 1 + 1",
|
|
|
|
|
name='calc',
|
|
|
|
|
),
|
|
|
|
|
Goal(
|
|
|
|
|
variables,
|
|
|
|
|
target="a + 1 + 1 = a + b",
|
|
|
|
|
),
|
|
|
|
|
])
|
|
|
|
|
state_c1 = server.goal_conv_begin(state2, goal_id=0)
|
|
|
|
|
state_c2 = server.goal_tactic(state_c1, goal_id=0, tactic="rhs")
|
|
|
|
|
state_c3 = server.goal_tactic(state_c2, goal_id=0, tactic="rw [Nat.add_comm]")
|
|
|
|
|
state_c4 = server.goal_conv_end(state_c3)
|
|
|
|
|
state_c5 = server.goal_tactic(state_c4, goal_id=0, tactic="rfl")
|
|
|
|
|
self.assertTrue(state_c5.is_solved)
|
|
|
|
|
|
|
|
|
|
state3 = server.goal_tactic(state2, goal_id=1, tactic=TacticCalc("_ = a + 2"))
|
|
|
|
|
state4 = server.goal_tactic(state3, goal_id=0, tactic="rw [Nat.add_assoc]")
|
|
|
|
|
self.assertTrue(state4.is_solved)
|
|
|
|
|
|
2024-09-09 19:28:35 -07:00
|
|
|
|
def test_load_sorry(self):
|
|
|
|
|
server = Server()
|
|
|
|
|
state0, = server.load_sorry("example (p: Prop): p → p := sorry")
|
|
|
|
|
self.assertEqual(state0.goals, [
|
|
|
|
|
Goal(
|
|
|
|
|
[Variable(name="p", t="Prop")],
|
|
|
|
|
target="p → p",
|
|
|
|
|
),
|
|
|
|
|
])
|
|
|
|
|
state1 = server.goal_tactic(state0, goal_id=0, tactic="intro h")
|
|
|
|
|
state2 = server.goal_tactic(state1, goal_id=0, tactic="exact h")
|
|
|
|
|
self.assertTrue(state2.is_solved)
|
|
|
|
|
|
2024-04-22 22:38:20 -07:00
|
|
|
|
|
2024-04-19 16:47:35 -07:00
|
|
|
|
if __name__ == '__main__':
|
2024-05-28 20:35:47 -07:00
|
|
|
|
unittest.main()
|