Skip to content

Commit 130cc75

Browse files
committed
feat: Subsumption
1 parent 072c3ca commit 130cc75

3 files changed

Lines changed: 69 additions & 0 deletions

File tree

pantograph/expr.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,14 @@ def serial(self) -> dict:
158158
result["autoResume"] = self.auto_resume
159159
return result
160160

161+
class Subsumption(Enum):
162+
"""
163+
Subsumption result
164+
"""
165+
NONE = 1
166+
SUBSUMED = 2
167+
CYCLE = 3
168+
161169
@dataclass(frozen=True)
162170
class TacticHave:
163171
"""

pantograph/server.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
Goal,
1818
GoalState,
1919
Site,
20+
Subsumption,
2021
Tactic,
2122
TacticHave,
2223
TacticLet,
@@ -342,6 +343,50 @@ async def goal_resume_async(self, state: GoalState, goals: list[Goal]) -> GoalSt
342343
return GoalState.parse(result, [], self.to_remove_goal_states)
343344
goal_resume = to_sync(goal_resume_async)
344345

346+
async def goal_subsume_async(
347+
self,
348+
state: GoalState,
349+
goal: Goal,
350+
candidates: list[Goal],
351+
src_state: Optional[GoalState]=None
352+
) -> (Subsumption, Optional[GoalState], Optional[Goal]):
353+
"""
354+
Detect subsumption by candidate goals
355+
356+
The candidate goals must all exist in `src_state`. If `src_state` is not
357+
provided, they must exist in `state`. Returns a new goal state if a
358+
subsumption does not lead to a cycle, and the subsumptor if there is any
359+
subsumption happening.
360+
"""
361+
args = {
362+
"stateId": state.state_id,
363+
"goal": goal.id,
364+
"candidates": [g.id for g in candidates],
365+
}
366+
if src_state:
367+
args["srcStateId"] = src_state.state_id
368+
result = await self.run_async('goal.subsume', args)
369+
if "error" in result:
370+
raise ServerError(result)
371+
nextState = None
372+
if state_id := result.get("stateId"):
373+
nextState = GoalState(
374+
state_id=state_id,
375+
goals=[g for g in state.goals if g.id != goal.id],
376+
messages=[],
377+
_sentinel=self.to_remove_goal_states,
378+
)
379+
sub = Subsumption[result["result"].upper()]
380+
subsumptor = None
381+
if subsumptor := result.get("subsumptor"):
382+
gen = (g for g in candidates if g.id == subsumptor)
383+
subsumptor = next(gen)
384+
if subsumptor is None:
385+
raise ServerError("Subsumptor should not be none")
386+
return (sub, nextState, subsumptor)
387+
388+
goal_subsume = to_sync(goal_subsume_async)
389+
345390
async def env_add_async(
346391
self, name: str, levels: list[str],
347392
t: Expr, v: Expr, is_theorem: bool = True):

pantograph/test_server.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,22 @@ def test_dependent_mvars(self):
198198
self.assertEqual(state.goals[0].sibling_dep, {1})
199199
self.assertEqual(state.goals[1].sibling_dep, set())
200200

201+
def test_subsume(self):
202+
server = Server()
203+
state0 = server.goal_start("forall (p : Prop), p -> p")
204+
state1 = server.goal_tactic(state0, "intro p")
205+
state2 = server.goal_tactic(state1, "intro h")
206+
state3 = server.goal_tactic(state2, "revert h")
207+
src = state1.goals[0]
208+
(sub, state, subsumptor) = server.goal_subsume(
209+
state3,
210+
state3.goals[0],
211+
[state1.goals[0], state2.goals[0]],
212+
)
213+
self.assertEqual(sub, Subsumption.CYCLE)
214+
self.assertEqual(state, None)
215+
self.assertEqual(subsumptor, src)
216+
201217
def test_env_add_inspect(self):
202218
server = Server()
203219
server.env_add(

0 commit comments

Comments
 (0)