import logos
import logos.tactics as t
from logos.goal import Goal
from logos.runner import run, TacticFailedTactic Combinators
Combinators compose tactics into more powerful ones. They let you sequence, branch, and repeat tactics without writing a new function.
then(*tactics)
Sequences tactics as a single compound tactic. Equivalent to listing them separately, but usable anywhere a single tactic is expected.
x, y = logos.vars("x y", int)
# These are equivalent:
logos.check(logos.prove(
logos.forall(int, int, lambda x, y: x + y == y + x),
[t.then(t.intro("x", "y"), t.ring())], # single compound tactic
))
logos.check(logos.prove(
logos.forall(int, int, lambda x, y: x + y == y + x),
[t.intro("x", "y"), t.ring()], # two separate tactics
))
print("Both work")Both work
then is particularly useful when you need a single tactic for a subgoal:
@logos.define
def double(x: int):
return x + x
x, y = logos.vars("x y", int)
logos.check(logos.prove(
logos.forall(int, int, lambda x, y:
(x > logos.lit(0)) >> ((y > logos.lit(0)) >> ((double(x) > logos.lit(0)) & (double(y) > logos.lit(0))))
),
[
t.intro("x", "y", "hx", "hy"),
t.split(),
[t.then(t.unfold(double), t.linarith())], # subgoal 1 as a compound tactic
[t.then(t.unfold(double), t.linarith())], # subgoal 2
],
))
print("Proved compound goal")Proved compound goal
first(*tactics)
Tries each tactic in order and succeeds with the first one that doesn’t raise TacticFailed. Useful for automation when multiple tactics might apply.
x = logos.var("x", int)
# refl works for x == x, ring works for x + 0 == x
# first tries refl, then ring
logos.check(logos.prove(logos.forall(int, lambda x: x == x),
[t.intro("x"), t.first(t.refl(), t.ring())]))
print("x = x via first(refl, ring)")
logos.check(logos.prove(logos.forall(int, lambda x: x + logos.lit(0) == x),
[t.intro("x"), t.first(t.refl(), t.ring())]))
print("x + 0 = x via first(refl, ring)")x = x via first(refl, ring)
x + 0 = x via first(refl, ring)
This pattern is useful in generic automation:
# A "cheap auto" tactic that tries simple things first
cheap_auto = t.first(t.refl(), t.assumption(), t.decide(), t.ring())
logos.check(logos.prove(logos.forall(int, lambda x: x == x),
[t.intro("x"), cheap_auto]))
logos.check(logos.prove(logos.lit(2) + logos.lit(2) == logos.lit(4),
[cheap_auto]))
print("cheap_auto handles both")cheap_auto handles both
try_(tactic)
Applies the tactic; if it fails, silently succeeds (the goal remains open for the next tactic). Never raises TacticFailed.
x = logos.var("x", int)
hyp = x > logos.lit(0)
# try_(ring()) fails silently on a non-ring goal, then assumption() closes it
logos.check(logos.prove(
logos.forall(int, lambda x: (x > logos.lit(0)) >> (x > logos.lit(0))),
[
t.intro("x", "h"),
t.try_(t.ring()), # fails (not a ring goal), but doesn't raise
t.assumption(), # closes the goal
],
))
print("try_ worked correctly")try_ worked correctly
repeat(tactic)
Applies the tactic repeatedly until it fails. Useful for stripping multiple Forall binders one at a time or simplifying iteratively.
x, y, z = logos.vars("x y z", int)
# Strip all three Foralls one at a time, then ring
logos.check(logos.prove(
logos.forall(int, int, int, lambda x, y, z: x + y + z == z + y + x),
[
t.repeat(t.intro("_")), # strips x, y, z one at a time
t.ring(),
],
))
print("repeat(intro) stripped all foralls")repeat(intro) stripped all foralls
all_goals(tactic)
Applies the tactic to the current goal (acts as the tactic itself). In a proof list after a splitting tactic, repeat it for each subgoal using separate nested lists, or use t.first to handle multiple cases:
x, y = logos.vars("x y", int)
# Use the same sub-tactic for each subgoal
close_with_assumption = t.assumption()
logos.check(logos.prove(
logos.forall(int, int, lambda x, y:
(x > logos.lit(0)) >> ((y > logos.lit(0)) >> ((x > logos.lit(0)) & (y > logos.lit(0))))
),
[
t.intro("x", "y", "hx", "hy"),
t.split(),
[close_with_assumption],
[close_with_assumption],
],
))
print("Both subgoals closed by same tactic")Both subgoals closed by same tactic
Building a Mini Automation
Combinators let you compose a simple automation tactic without writing Python:
# A mini "auto" that tries a battery of cheap tactics
auto = t.first(
t.refl(),
t.assumption(),
t.decide(),
t.ring(),
t.linarith(),
)
# Now use it for simple goals without specifying which tactic applies
x, y = logos.vars("x y", int)
for stmt, by in [
(logos.forall(int, lambda x: x == x), [t.intro("x"), auto]),
(logos.lit(3) > logos.lit(2), [auto]),
(logos.forall(int, int, lambda x, y: x + y == y + x), [t.intro("x", "y"), auto]),
]:
logos.check(logos.prove(stmt, by))
print("mini-auto handled all three goals")mini-auto handled all three goals