Jordan Dehmel, 2025-2026
This is not necessarily decidable! If you choose your rules wisely, it might be, but it almost certainly will not be if you aren't smart about it. A poor choice of rules can make proving "2 is a natural number" take seconds, or not verify at all! Tread with care!
Verily is a theorem prover based on AST-rewriting-rules. It is not based on the Curry-Howard isomorphism. An axiom is a syntax tree which is taken to be a theorem without proof, and a theorem is a syntax tree which is derived from known theorems via known rules. A tree is said to be proven if it is a theorem. Verily finds theorems given axioms and rules, and also allows you to apply rules yourself.
You can do a few things.
- Add things known to be true via
axioms - Introduce ways of deriving new truths from known truths via
rules - Query whether we can deduce something to be true via
theorems - Manually derive new truths from existing ones via
apply
Everything is uninterpreted! Though operators like +, *,
etc are built-in and have the expected precedence,
they don't do anything! They are just ASTs!
This is a string-rewriting-rule-based theorem prover, not a Curry-Howard-isomorphism-based one!
This repo contains some examples, a verily CLI, some tests, and
a verily syntax highlighting extension for vscode.
To build the CLI, just run make from the root of this repo.
This will create ./verily.out, which is the executable. Run
./verily.out --help for help. View the examples to see
verily's syntax.
To install the vscode syntax highlighting extension, change
directories to verily-highlighting. Then, run
npx @vscode/vsce package to build the extension. This will
produce a local file ending in .vsix. If this file were named
NAME_GOES_HERE.vsix, you would then run
code --install-extension NAME_GOES_HERE.vsix. You must then
reload your vscode window (ctrl+shift+p
'Developer: Reload Window') for the extension to start
syntax-highlighting any open files with the extension .verily.
Given the file:
# I am a comment
// I am also a comment
-- I, too, am a comment
axiom zero_in_nat: 0 in Nat;
axiom successor_fn: S in Nat to Nat;
rule fn_app_typing:
over f, x, A, B
given f in A to B, x in A
deduce f(x) in B
;
# These two applications manually prove the theorem
# apply fn_app_typing
# to successor_fn,
# zero_in_nat
# as one_in_nat
# ;
# apply fn_app_typing
# to successor_fn,
# one_in_nat
# as two_in_nat
# ;
# Automatically prove theorem
prove_forward: S(S(0)) in Nat;
We could run the theorem prover:
./verily.out FILENAME_GOES_HERE.verilyAnd receive a proof:
(theorem
(in (@ S (@ S 0)) Nat)
(rule_application
(rule fn_app_typing)
(premises
(axiom (in S (to Nat Nat)))
(theorem
(in (@ S 0) Nat)
(rule_application
(rule fn_app_typing)
(premises
(axiom (in S (to Nat Nat)))
(axiom (in 0 Nat))
)
)
)
)
)
)
This proof shows the series of rule applications by which you can derive the theorem from axioms. If no proof can be found, it will report an error. Be careful: Just because a statement does not follow from your rules does not mean it is false! And just because verily fails to find entailment does not mean that a derivation doesn't exist!
The following are operators which will be parsed. Note that, since the entire language is uninterpreted, straying from these will not cause any errors: It just might not parse the way you expect.
Operators (in precedent order):
'("prime", suffix unary)^*/%+-<><=>===crosstoinnot(prefix unary)orandiffimpliesderivesmodels
Unless otherwise specified, the above operators are infix binary
(for operator x and terms A, B, it would look like A x B
and parse to S-EXPR (x A A)).
Parentheses and python-syntax function calls (and tuples) are
also built-in. Symbols do not need to be defined before use. All
statements should end in semicolons.
Since a and b or c is ambiguous in most languages, it is
considered bad form to write it: Instead, write either
(a and b) or c or a and (b or c), depending on what you
actually want.
Any single token can be used as a quantifier. Technically, the
parser recognizes any expression of the form
TOKEN EXPR . EXPR as a quantified expression. The basic ones
are forall, exists, and lambda, although
none of these are interpreted. If you want them to perform
their intuitive functions, you need to add appropriate rules.
Calls are written f(x). Only unary calls are built-in. If you
wanted to provide multiple arguments (interpretation-specific),
you could say f(x)(y) (Currying) or f((x, y)) (providing a
single 2-tuple).
The deduction theorem goes something like this: "If we can
derive --meta_prove CLI flag or the setting "meta_prove=true";
verily statement, this will be a valid rule.
You can use the include "local_filepath"; command. This takes
a path relative to the current file (the CWD in CLI mode).
Axioms are things that are assumed to be true. They are declared
using the axiom keyword.
axiom: is_even(0);
Theorems are not assumed to be true: They must be proven via
the known induction rules and axioms. They use the same syntax,
but use the theorem keyword.
theorem: is_even(S(S(0)));
Theorems are interchangeably referred to as "annotations" throughout. If they cannot be proven, an error will be raised. Once they are proven, they act the same as axioms: Another proof can use them without re-proving them.
Theorems are derived from axioms via inference rules. An inference rule takes the form
rule:
over x, y, z
given fe, fi, fo
deduce fum
;
Inference rules are operations on uninterpreted ASTs. Given that some AST is a theorem, we can deduce that some other AST is as well. The over clause lists free variables. Note that, in verily, we start from the AST to be proven and work backwards to axioms: If a proof exists within the system, it will be found by this procedure (G{"o}del's completeness theorem). The over clause of a rule declares the free variables. Because of the way the deduction system works, all free variables must occur in the "deduce" section of the rule.
An inference rule "rule: over x given A, B deduce C" can be read
"for all
The "over" and "given" sections of a rule are optional, but the "deduce" section is required.
Despite repeated claims to the contrary, there are actually a few words which mean something specific in verily. Each of them starts a statement. The following are those keywords, with their form and purpose.
rule introduces a new rule. See previous sections for the
form of these statements.
axiom: EXPR; introduces an axiom (statement which can be
taken to be true without proof).
theorem: EXPR; tells the solver to try to find a proof for
the given expression before moving on to the next statement.
prove_forward: EXPR; works the same as theorem, but starts
in forward derivation mode instead of the default backwards.
setting "SOME_STR"; depending on the string, sets some
internal setting.
include "SOME_STR"; runs the given (local-path, like EG C++)
file, executing each statement in that file before moving onto
the next statement in the current file.
There are many synonymous keywords. Some of them are listed below.
Synonymous with include:
import(ease of use)
Synonymous with setting:
option(ease of use)
Synonymous with theorem:
lemma(for organization)deduce(because it's used for that within rules)prove(for compatibility)prove_backward(to match the form ofprove_forward)assert(for compatibility)
Synonymous with axiom:
assume(for compatibility)
We can manually add pending theorems via wts (Want To Show).
We can manually apply rules via the apply keyword.
wts S(S(S(0))) in Nat;
apply application_typing to succ, zero_nat as one_nat;
apply application_typing to succ, one_nat as two_nat;
apply application_typing to succ, two_nat as three_nat;
All of the following are valid formulations.
apply X;(applies ruleXto every viable existing case)apply X to Y;apply X to Y, Z, A;apply X to A as Y;apply all;(applies all rules to every viable existing case exactly once)
We want to work backwards: Given some theorem to prove, find the theorems which prove it, and the theorems to prove those, etc. This is not necessarily decidable or more efficient than forward search, but in practice usually is. We do this by pattern matching on the consequence of generic deduction rules. Deduction rules have some number of free variables, some number of antecedents / requirements, and exactly one consequence. They are meta-statements that say "for any instances of the free values under which all the antecedents are theorems, we can deduce that the consequence (under those same values) is too".
For instance, the
Forward search: If we knew the statements
Backward search: We want to show
| FVs in antecedents | FVs in consequence | Notes |
|---|---|---|
| Not all | Not all | Neither method works |
| Not all | All | Backward only |
| All | Not all | Forward only |
| All | All | Both methods work |
The solver's solution here is alternation: When a theorem
statement is found, it will try backwards deduction until that
can go no further. Then, it will try forward deduction until
that can go no further. This will continue until the theorem is
proven or the number of allotted deduction passes is exhausted.
Think of the known truths as one point
In alternating mode, circles are emitted from both
Now imagine we put a third point
Note that the solver could give up due to resource constraints without a lemma but quickly find a proof with a lemma. However, a bad lemma could lie completely off the path between the known truth and the would-be theorem, in which case it will only waste solver time. In general, designing good lemmas is about as hard as finding a proof. However, if you already have a proof in mind, a good lemma can help the solver connect the dots.
Technically speaking, backwards proof search is more like bolts of lightning shooting off the point in random directions, hoping to touch the known truth. This is because forwards proof search is breadth-first while backwards proof search is depth-first.