This is a Haskell reference implementation of the Metamath Zero specification. See mm0.md for the main specification.
To compile, install Haskell stack
, then go to the mm0-hs
directory and run stack setup
, then use stack build
to build the program and stack exec -- mm0-hs [args..]
to run it, or stack install
to move it to a global location in your PATH, so that mm0-hs [args..]
works from any directory (which in particular is what the vscode-mm0
extension expects, if you don't tell it otherwise).
To run the program you can use the following:
-
mm0-hs verify MM0-file [MMU-FILE]
:This verifies the given
.mm0
specification file, and checks the proof file if given. It printsspec checked
if the.mm0
file is well formed and typechecks, and if the.mmu
file is also provided then it printsverified
if the specification is proven, followed by all the result of alloutput
statements.
The MM0 spec leaves many parts of the language implementation-defined, and most of the specification is optional. This implementation has the following behavior:
- All the primary commands are supported:
sort, term, axiom, def, theorem, delimiter, notation, infixl/r, prefix, coercion, input, output
. - All sort modifiers are supported.
- Delimiters must be single characters. Other tokens may not contain delimiter characters.
- The following IO kinds are supported:
input string, output string
(see "string
IO" below)
- See below for the specification of the
.mmu
proof file format.
Both the input
and output
commands use the same grammar for reading strings from the logic, given by the following specification:
strict free sort hex;
term x0: hex; term x1: hex; term x2: hex; term x3: hex;
term x4: hex; term x5: hex; term x6: hex; term x7: hex;
term x8: hex; term x9: hex; term xa: hex; term xb: hex;
term xc: hex; term xd: hex; term xe: hex; term xf: hex;
strict free sort char;
term ch: hex > hex > char;
strict free sort string;
term s0: string;
term s1: char > string;
term sadd: string > string > string;
That is, for a specification to use the input string
and output string
directives, it must contain these sorts and terms with these exact names (and these sorts should not have any additional term constructors, although it is permissible to define term constructors taking these sorts as input). Both commands take exactly one definition identifier or expression, of type string
.
- The
hex
sort contains the numbers 0-15 with hexadecimal names. - The
char
sort forms an 8-bit character from twohex
digits. So for examplech x3 x0
is the character0x30
which represents ASCII character '0'. - The
string
sort contains lists of characters, formed by appending (usingsadd
) strings of length 0 and 1 (usings0
ands1
). The associativity of thesadd
's in a string does not matter.
With these terms and sorts, we can encode strings inside the logic, prove theorems about them and so on.
- The
output
command accepts a definition or expression of typestring
and completely unfolds the definition until only the above terms remain, and then interprets the result as a string and outputs it to standard out. - The
input
command "passes the contents of the.mm0
file to the logic" into the given definition or expression. More precisely, the proof file needs to already know what the input is, and the verifier will check that the proof file has provided a witness that matches the actual contents of the.mm0
file.
See hello.mm0 for an example of a specification file that can print Hello World!
on standard out (and also asserts that it does so in the specification) using output string
.
A (necessarily more complicated) example is string.mm0, which uses both input string
and output string
to read the specification file string.mm0
, assert that it equals a particular string
value defined in the logic, and then output it back to the console. (So it is similar in spirit to a quine, a program that prints its own source code, although the self reference is enabled explicitly by the input
command. A real quine would also need to print the contents of the .mmu
file.) It then also outputs hello world
for good measure. (Note for windows users: Make sure that git does not replace LF
with CRLF
in the specification file, or else the verification will fail.)
For demonstration purposes, this verifier accepts a text format for the proof file. (A production quality verifier should probably use a binary file format, but the information contained in the file should be about the same.) To simplify parsing, the whole file is written using lisp s-expression syntax:
- Whitespace is ignored except to separate tokens
- Line comments are written
--comment
and extend until the next\n
; line comments act like whitespace - An identifier token matches
[0-9a-zA-Z_:]+
- Characters
( )
are single character symbol tokens - Anything else is forbidden
The syntax is as follows:
mmu-file ::= (directive)*
directive ::= sort-stmt | term-stmt | def-stmt | axiom-stmt | thm-stmt | io-stmt
sort-stmt ::= '(' 'sort' sort-name ('pure')? ('strict')? ('provable')? ('free')? ')'
term-stmt ::= '(' 'term' term-name binders ret-type ')'
def-stmt ::= '(' ('local')? 'def' term-name binders ret-type dummies expr ')'
axiom-stmt ::= '(' 'axiom' assert-name binders axiom-hyps expr ')'
thm-stmt ::= '(' ('local')? 'theorem' assert-name binders thm-hyps expr dummies proof-expr ')'
io-stmt ::= '(' 'input' input-kind ')'
| '(' 'output' output-kind ')'
identifier ::= [0-9a-zA-Z_]+
sort-name ::= identifier
term-name ::= identifier
assert-name ::= identifier
var-name ::= identifier
hyp-name ::= identifier
input-kind ::= 'string'
output-kind ::= 'string'
binders ::= '(' (binder)* ')'
binder ::= '(' var-name sort-name ')'
| '(' var-name sort-name '(' (var-name)* ')' ')'
dummies ::= '(' (dummy-binder)* ')'
dummy-binder ::= '(' var-name sort-name ')'
ret-type ::= '(' sort-name '(' (var-name)* ')' ')'
axiom-hyps := '(' (expr)* ')'
thm-hyps ::= '(' (hyp-binder)* ')'
hyp-binder ::= '(' hyp-name expr ')'
expr ::= var-name | '(' term-name (expr)* ')'
proof-expr ::= hyp-name
| '(' assert-name '(' (expr)* ')' (proof-expr)* ')'
| '(' ':let' hyp-name proof-expr proof-expr ')'
| '(' ':conv' expr conv-expr proof-expr ')'
conv-expr ::= var-name
| '(' term-name (conv-expr)* ')'
| '(' ':sym' conv-expr ')'
| '(' ':unfold' term-name '(' (expr)* ')' '(' (var-name)* ')' conv-expr ')'
A proof file is a list of declarations, mirroring the declarations in the specification file. The verification of the proof proceeds in lock step with the declarations in the spec, but the proof file will potentially add additional definitions and theorems that are not present in the specification.
-
A
sort-stmt
declares a new sort. The syntax is the same as in.mm0
files except that the sort modifiers come after the wordsort
. -
A
term-stmt
declares a new term, anddef-stmt
declares a new definition. These are also the same as.mm0
files, except that bodies are required for definitions. -
An
axiom-stmt
declares a new axiom, andthm-stmt
declares a new theorem. The list of hypotheses is slightly different between axioms and theorems because axiom hypotheses don't need (or accept) names. Unlike the.mm0
format, theorems have proofs, and we also must pre-declare the list of dummy variables used in the proof. -
An
io-stmt
is an input or output statement, which formm0-hs
can only be(input string)
or(output string)
.
Binders have a simplified syntax:
- A bound variable binder like
{x: set}
is rendered(x set)
- A regular variable binder like
(ph: wff x)
is rendered(ph wff (x))
. Even if there are no dependencies, a regular variable will have the third argument:(ph: wff)
becomes(ph wff ())
.
Proof expressions are composed of theorem applications on hypotheses, but special rules have keywords starting with :
for them.
H
is the application of hypothesisH
, or a saved local proofH
.(thm (e1 e2) p1 p2)
is the application of theoremthm
, substitutinge1
ande2
for the variables in the theorem andp1
andp2
are subproofs proving the hypotheses to the theorem.(:let H p1 p2)
constructs a subproofp1
and binds it to a nameH
, which can be used in the proof ofp2
as if it were an additional hypothesis.(:conv rhs c p)
is a proof of|- rhs
ifc
is a conversion proof oflhs = rhs
andp
is a proof of|- lhs
.
Conversion expressions are "proofs of definitional equality":
x
is a proof thatx = x
(t c1 .. cn)
is a proof that(t l1 .. ln) = (t r1 .. rn)
ifci: li = ri
. Note that because of this rule and the previous one, any exprt
is a proof oft = t
when interpreted as a conversion.(:sym c)
is a proof ofr = l
ifc: l = r
.(:unfold t (e1 .. en) (d1 .. dm) c)
is a proof of(t e1 .. en) = rhs
ift
is a definition, and substitutinge1 .. en
for the arguments andd1 .. dm
for the dummy variables int
's definition expression yieldssub_lhs
, andc: sub_lhs = rhs
.