Theorem Proving in a Makefile??


Tip

This demo is a variation on many others that are using inlined data and container-definitions and/or working with foreign languages. This is an instance of a custom containerized interpreter, and also demonstrates custom interpreter invocations.

Realistically most people are not sitting around and wondering whether they can prove Euclid1 or Gödel2 by using some wild matrioshka doll of a Makefile. But then again, people do get up to all kinds of weird things. 🤔

For the purposes of this demo, we'll use compose.mk to take the Lean4 prover for a test drive, and for starters just prove the law of the excluded middle.

It's fun, self-contained, and marginally practical! Partly because more academic tools like lean frequently do not have official containers =P, but mostly because this is part of a larger demo about notebooking.

Caveat

Note that this demo is big, and the lean/lake/mathlib toolchain here takes significant time to build, and the image on disk afterwards is ~10gb. If you're not interested in running the notebooking demo later then you may not want to run this one.

Lean Demo


The lean demo comes in 2 flavors. The first one is using compose.mk as a library, i.e. pure Makefile.

Summary
#!/usr/bin/env -S make -f
# Demonstrates an embedded container with a lean+mathlib container, 
# plus enough glue code for dispatch so that container usage is abstracted away.
# Included is a script and a theorem that we'll test with, but of course external files 
# are supported as well.
#
# Part of the `compose.mk` repo. This file runs as part of the test-suite.  
#
# USAGE: ./demos/lean.mk
# DOCS: http://robot-wranglers.github.io/compose.mk/demos/lean

include compose.mk

# Look, it's a minimal Dockerfile for running lean4 
# See also: https://leanprover-community.github.io/install/debian.html
define Dockerfile.Lean
FROM ${IMG_DEBIAN_BASE:-debian:bookworm-slim}
SHELL ["/bin/bash", "-x", "-c"]
RUN apt-get -qq update && apt-get install -qq -y git make curl sudo procps
RUN curl -sSf \
  https://raw.githubusercontent.com/leanprover/elan/master/elan-init.sh \
    > /usr/local/bin/elan-init.sh
RUN bash /usr/local/bin/elan-init.sh -y \
  --default-toolchain leanprover/lean4:v4.17.0
RUN cp /root/.elan/bin/lean /usr/local/bin 
RUN cp /root/.elan/bin/lake /usr/local/bin 
ENV PATH="$PATH:/root/.elan/bin/"
RUN lean --help
RUN lake new default
RUN cd default && \
  printf '[[require]]\nname = "mathlib"\nscope = "leanprover-community"' \
    >> lakefile.toml
RUN cd default && lake update && lake build
RUN elan --version; lean --version; leanc --version; lake --version; 
ENTRYPOINT ["lean"]
endef

# Look, it's hello-world in scriptwise lean-lang
# https://lean-lang.org/functional_programming_in_lean/hello-world/running-a-program.html
define my.script
def main : IO Unit := IO.println "Hello, world!"
endef

# Look, it's a theorem we might want to prove
# https://leanprover-community.github.io/logic_and_proof/classical_reasoning.html
# https://lean-lang.org/theorem_proving_in_lean4/propositions_and_proofs.html
define my.theorem 
section
open Classical
example : A ∨ ¬ A := by
  apply byContradiction
  intro (h1 : ¬ (A  ¬ A))
  have h2 : ¬ A := by
    intro (h3 : A)
    have h4 : A  ¬ A := Or.inl h3
    show False
    exact h1 h4
  have h5 : A  ¬ A := Or.inr h2
  show False
  exact h1 h5
end
endef

# Main entrypoint.  Ensures the container is ready, 
# then dispatches execution of both script and theorem
__main__: \
    Dockerfile.build/Lean \
    lean.run.script/my.script \
    lean.run.theorem/my.theorem

# Helpers that write embedded script/theorem to disk before use, 
# run them inside the container, and clean up afterwards.
lean.run.script/%:; lean_args="--run" ${make} lean.run.generic/${*}

lean.run.generic/% lean.run.theorem/%:
    ${io.mktemp} && ${make} mk.def.to.file/${*}/$${tmpf} \
    && img=Lean cmd="$${lean_args:-} $${tmpf}" \
        ${make} mk.docker

Lean Demo (CMK)


The 2nd version of the demo is mostly the same, but uses compose.mk as an interpreter, and is expressed in CMK-lang, which compiles to Makefile. This helps to clean up the syntax a bit and clarify intent.

Summary
#!/usr/bin/env -S ./compose.mk mk.interpret!
# CMK-lang translation of demos/lean.mk
#
# Part of the `compose.mk` repo. This file runs as part of the test-suite.  
#
# See also: http://robot-wranglers.github.io/compose.mk/demos/lean
#           http://robot-wranglers.github.io/compose.mk/demos/polyglots
#
# USAGE: ./demos/cmk/lean.cmk

# Look, a minimal Dockerfile for running lean4 
# See also: https://leanprover-community.github.io/install/debian.html
 Dockerfile.Lean
FROM ${IMG_DEBIAN_BASE:-debian:bookworm-slim}
SHELL ["/bin/bash", "-x", "-c"]
RUN apt-get -qq update && apt-get install -qq -y git make curl sudo procps
RUN curl -sSf \
  https://raw.githubusercontent.com/leanprover/elan/master/elan-init.sh \
  > /usr/local/bin/elan-init.sh
RUN bash /usr/local/bin/elan-init.sh -y \
  --default-toolchain leanprover/lean4:v4.17.0
RUN cp /root/.elan/bin/lean /usr/local/bin 
RUN cp /root/.elan/bin/lake /usr/local/bin 
ENV PATH="$PATH:/root/.elan/bin/"
RUN lean --help
RUN lake new default
RUN cd default \
  && printf '[[require]]\nname = "mathlib"\nscope = "leanprover-community"' >> lakefile.toml
RUN cd default && lake update && lake build
RUN elan --version; lean --version; leanc --version; lake --version; 
ENTRYPOINT ["lean"]


# Look, it's hello-world in scriptwise lean-lang
# https://lean-lang.org/functional_programming_in_lean/hello-world/running-a-program.html
 my.script
def main : IO Unit := IO.println "Hello, world!"
 with lean.run.script as target

# Look, it's a theorem we might want to prove
# https://leanprover-community.github.io/logic_and_proof/classical_reasoning.html
# https://lean-lang.org/theorem_proving_in_lean4/propositions_and_proofs.html
 my.theorem 
section
open Classical
example : A ∨ ¬ A := by
  apply byContradiction
  intro (h1 : ¬ (A  ¬ A))
  have h2 : ¬ A := by
    intro (h3 : A)
    have h4 : A  ¬ A := Or.inl h3
    show False
    exact h1 h4
  have h5 : A  ¬ A := Or.inr h2
  show False
  exact h1 h5
end
 with lean.run.theorem as target

# Main entrypoint.  Ensures the container is ready, 
# then dispatches execution of both script and theorem
__main__: Lean.build my.theorem.run my.script.run

# Top-level helpers. 
# These write embedded script/theorem to disk before use, 
# run them inside the container, and clean up afterwards.
lean.run.script/%:; lean_args="--run" ${make} lean.run.generic/${*}

lean.run.generic/% lean.run.theorem/%:
    img=Lean cmd="$${lean_args:-} ${*}" ${make} mk.docker

References