lemma_argument_impure: a lemma call whose argument does something
Every call of a @[lemma] is removed before the program runs, and its arguments are removed with
it. So each argument must be a term whose removal changes nothing:
- a variable or a literal;
- a constructor (
Cons(x, Nil)) or a tuple of such terms; - a call of a function the checker reflects (pure, total, terminating);
- a call of a builtin the logic knows, such as
+or<.
Anything else, such as a call that prints, could fail, or isn’t reflected, is an error. Erasing it would silently erase what it does.
A program that triggers it
mod Main do
needs IO.Console
fn cat(xs : List(Int), ys : List(Int)) : List(Int) do
match xs do
Nil -> ys
Cons(h, t) -> Cons(h, cat(t, ys))
end
end
@[lemma]
fn cat_nil(xs : List(Int)) : {Unit | cat(xs, Nil) == xs} do
match xs do
Nil -> ()
Cons(_, t) -> cat_nil(t)
end
end
fn noisy_list() : List(Int) do
println("building")
Cons(1, Nil)
end
fn main(_cap : Cap(IO.Console)) : Int do
cat_nil(noisy_list())
0
end
end
The fix
Bind the value to a name first, so the effect stays in the program and only the lemma call goes:
mod Main do
needs IO.Console
fn cat(xs : List(Int), ys : List(Int)) : List(Int) do
match xs do
Nil -> ys
Cons(h, t) -> Cons(h, cat(t, ys))
end
end
@[lemma]
fn cat_nil(xs : List(Int)) : {Unit | cat(xs, Nil) == xs} do
match xs do
Nil -> ()
Cons(_, t) -> cat_nil(t)
end
end
fn noisy_list() : List(Int) do
println("building")
Cons(1, Nil)
end
fn main(_cap : Cap(IO.Console)) : Int do
let xs = noisy_list()
cat_nil(xs)
0
end
end
Why the rule exists
Lemmas are proofs, and proofs must not change what a program does. A rule that left lemma calls in the program would cost time at run time. A rule that removed them along with their arguments’ effects would change behaviour. Requiring pure arguments avoids both.
See also: the language reference, the lemmas guide, invalid_lemma.