This repository holds a computer-checked proof that simplifying a φ-calculus program gives the same result no matter in which order you apply the simplification rules.
The φ-calculus is the small formal language behind EO. A program in it is a term, and a fixed set of rules rewrites a term, one step at a time, into a simpler one. Often several rules apply at once, or one rule applies in several places, so you have a choice of what to rewrite next. The property proved here, called confluence (or Church–Rosser), says that the choice never matters: any two ways of rewriting the same term can always be continued until they meet at the same term.
The proof is written in Lean 4, a programming language that is also a proof assistant: a program that checks every step of a mathematical proof. If Lean accepts the proof, you do not need to trust the reasoning, only the statement of the theorem and the few definitions it uses.
The rules are the ones implemented by phino,
the reference tool for the φ-calculus.
The paper that defines the calculus
generates its table of rules (Fig. 4) from phino too,
so this proof, the paper, and the tool all talk about the same rules.
Lean reports that the main theorem, PhiConfluence.confluence,
depends only on the axioms propext and Quot.sound,
two standard parts of Lean's logic.
It uses no sorry (Lean's placeholder for a missing proof)
and no Classical.choice (the axiom of choice).
- Term — a φ-calculus expression.
It is one of six kinds:
a formation
⟦B⟧, an object with a listBof named attributes (its bindings); an applicatione(τ↦e'), which gives attributeτofethe valuee'; a dispatche.τ, which takes attributeτofe; the global objectΦ; the current objectξ; or⊥, the dead object that results from an error. - Attribute — the name of a binding.
It is a plain label such as
x, one of the special namesφandρ(ρis the object's parent), or a positional nameαᵢ, meaning "the attribute at positioni". A bindingx↦∅is void, because it has no value yet; a bindingx↦eis attached. The special bindingsλandΔhold native code and raw data; they are called assets. - Step —
e ⟶ e'means one rule rewriteseintoe', anywhere inside it.e ⟶∗ e'means zero or more steps. - Redex — a place inside a term where a rule can fire.
- Normal form — a term with no redex, so no rule can fire.
nf emeans "eis in normal form". - Well-formed — a term with no repeated attribute names in any formation
and no positional name used as a formation's attribute.
Lean calls this
WF.
The relation ⟶ uses the rules of phino version 0.0.139
(dd, dc, dca, null, over, stop, miss, stay, skip, alpha, overa, amiss, dot, copy, dl),
applied anywhere inside a term.
The sixteenth phino rule, dotg,
fires only on a whole program,
which phino rewrite never receives,
so the proof leaves it out.
The theorem, PhiConfluence.confluence, says:
For every well-formed term
e, ife ⟶∗ e₁ande ⟶∗ e₂, then some terme₃exists withe₁ ⟶∗ e₃ande₂ ⟶∗ e₃.
Some terms rewrite forever: ⟦x↦y,y↦x⟧.x never reaches a normal form.
So the usual shortcut, Newman's lemma,
which derives confluence only for systems where rewriting always stops,
does not apply.
The proof uses parallel reduction instead,
explained in Proof strategy.
The theorem is deliberately narrower than "the rules of Fig. 4 exactly as printed"; the differences from the paper are listed and explained below. The paper itself proves no confluence theorem. It assumes confluence when it defines two terms as equal if their normal forms are identical.
The Lean definition of a formation is deliberately looser
than the paper's grammar:
it allows repeated attribute names and positional names as attributes.
The well-formedness condition WF puts back the paper's own two restrictions.
They matter for different reasons.
- No positional name as a formation attribute (
legalKey) is required for confluence. Without it, the theorem is false. Take a malformed term⟦B₁, αᵢ↦e₁, B₂⟧(αᵢ↦e₂)whose void attribute sits at positioni. Thealpharule renames the argument and yields an object, while theoverrule yields⊥, and those two results can never meet (difference 8). The proof uses exactly this condition (lookup_alpha_absent_of_wf, and theoverandcopycases ofpar_triangle). It matchesphino, whose parser rejects such terms. - Unique attribute names (
Nodup, Def. Binding 4.8) keep the model faithful to the paper; no known example needs them for confluence. The proof carries this condition but never uses it. It stays for three reasons: the paper's Def. Binding requires it; it makes our attribute lookup, which takes the first match, agree withphino's lookup (difference 6); and every rewriting step preserves it.
curl -sSf https://elan.lean-lang.org/elan-init.sh | sh # one-time: Lean's toolchain manager
pip install -r .github/requirements.txt # one-time: Python deps of the generators
make # green ⇒ every theorem is kernel-checked and axiom-clean, as in CI
lake exe demo # the rules + example reductions, by the project's own reducer
make difftest # our reducer vs `phino rewrite --normalize` (needs phino on PATH)make needs GNU Make 4.3 or newer.
It downloads prebuilt parts of mathlib, Lean's standard mathematics library.
It generates the rule files from the pinned version of phino,
and regenerates them only when .phino-version or a generator changes.
It runs the generators' unit tests,
builds the proof with lake build,
and checks that no main theorem depends on a forbidden axiom.
The rule files Rules.lean and RuleData.lean are not kept in Git.
They are generated from the resources/normalize/*.yaml files
of the phino version named in .phino-version,
the same files the paper's Fig. 4 comes from,
so they cannot drift away from phino.
The project also contains a small program that simplifies terms,
the reducer.
The theorem reduce_sound proves every step the reducer takes
is a genuine ⟶ step.
make difftest runs the reducer and phino on the same programs
and checks they reach the same normal forms.
To see which axioms any result depends on,
run #print axioms <name> in Lean.
lake exe demo prints the rules in the paper's notation,
then prints the reducer's step-by-step simplification of example programs.
The paper's rule figure (operators.tex) is the primary source,
and phino's resources/normalize/*.yaml files
(shown by phino explain --normalize) are its executable version.
In the table below, C(e⊳ctx) is contextualization:
it resolves the current-object references ξ inside e against ctx
(paper, Fig. "Contextualization by induction").
ordinal(B, i) is the name of the attribute at position i of B,
counted as described in the note (†).
phino rule |
Lean Step constructor |
Pattern → result | Side condition |
|---|---|---|---|
dot |
Step.dot |
⟦B₁,τ↦e₁,B₂⟧.τ → C(e₁⊳⟦B₁,B₂⟧)(ρ↦⟦B₁,τ↦e₁,B₂⟧) |
nf e₁ ∧ not both λ∈B and Δ∈B |
dotg |
— (difference 10) | same, (ρ↦Φ) instead |
the formation is the whole program |
copy |
Step.copy |
⟦B₁,τ↦∅,B₂⟧(τ↦e₁) → ⟦B₁,τ↦e₁,B₂⟧ |
e₁ has no ξ ∧ nf e₁ (difference 7) |
alpha |
Step.alpha |
⟦B⟧(αᵢ↦e) → ⟦B⟧(τ↦e) |
τ = ordinal(B, i) is void (†) |
overa |
Step.overa |
⟦B⟧(αᵢ↦e) → ⊥ |
ordinal(B, i) is attached |
amiss |
Step.amiss |
⟦B⟧(αᵢ↦e) → ⊥ |
B has no position i (†) |
stay |
Step.stay |
⟦B₁,ρ↦e₁,B₂⟧(ρ↦e₂) → ⟦B₁,ρ↦e₁,B₂⟧ |
— |
skip |
Step.skip |
⟦B⟧(ρ↦e) → ⟦B⟧ |
ρ∉B |
over |
Step.over |
⟦B₁,τ↦e₁,B₂⟧(τ↦e₂) → ⊥ |
τ≠ρ (attached attribute) |
stop |
Step.stop |
⟦B⟧.τ → ⊥ |
τ∉B ∧ φ∉B ∧ λ∉B |
null |
Step.null |
⟦B₁,τ↦∅,B₂⟧.τ → ⊥ |
— (void attribute) |
miss |
Step.miss |
⟦B⟧(τ↦e) → ⊥ |
τ∉B ∧ τ≠ρ ∧ τ is not positional |
dl |
Step.dl |
⟦B⟧ → ⊥ |
λ∈B ∧ Δ∈B |
dd |
Step.dd |
⊥.τ → ⊥ |
— |
dc, dca |
Step.dc |
⊥(τ↦e) → ⊥, ⊥(αᵢ↦e) → ⊥ |
— (one Lean rule covers both) |
(†) Positions skip the assets and the parent.
When counting positions, phino skips the assets λ and Δ
and the parent ρ, and so does ordinal in Attributes.lean.
In ⟦λ⤍Fn, x↦∅, ρ↦∅⟧, x is at position 0,
and there is no position 1.
This follows the paper's Def. Ordinal and Def. Domain.
The three positional rules alpha, overa, and amiss
cover every possible position,
so one of them always applies to a positional argument of a formation.
Four more rules, Step.congDispatch, Step.congAppFn, Step.congAppArg,
and Step.congForm, let any rule fire inside a term:
in the object of a dispatch, in either side of an application,
or in the value of a formation's binding.
This matches the paper (operators.tex),
which says "rules may be applied in any order".
At the top of a term, at most one rule applies, with one exception:
- On a dispatch
⟦B⟧.τ, the rule depends on attributeτ:dotif it is attached,nullif it is void, andstopif it is missing andBholds neitherφnorλ. On⊥.τ,ddapplies. A dispatch of a missing attribute on a formation holdingφorλis already normal, because no rule covers it, and so are terms likeΦ.τandξ.τ. - On an application
⟦B⟧(τ↦e)with a namedτ, the rule depends on attributeτ:stayif it isρand attached,overif it is another attached name,copyif it is void andeis normal and has noξ,skipif it isρand missing, andmissif it is another missing name. With a positionalαᵢ, the rule isalpha,overa, oramiss, depending onordinal(B, i). On⊥(…),dcapplies. - The exception:
dlcompetes with every other rule on a formation holding bothλandΔ. Each such conflict still ends at⊥, except withdot, which would move the formation into aρbinding and leave a stuck…(ρ↦⊥). That is whyphinoforbidsdoton such a formation, andStep.dotdoes too.
The model follows the paper,
whose Fig. 4 is generated by phino (phino explain --normalize).
The items below record the choices that limit the theorem's scope
and the assumptions it makes.
None of them is a gap against the paper.
overneeds an attached attribute. Fig. 4 and ouroverboth requireτto have a value, as in⟦B₁,τ↦e₁,B₂⟧(τ↦e₂). Def. 4.9 (Formation) defines membershipτ∈bto include void attributes, so a careless reading would letoverfire on a void attribute too. There it would compete withcopy, and their results, an object and⊥, could never meet. We follow Fig. 4, where the two rules never compete.dotandcopywait for normal forms. They fire only when the value they move is already normal. This forces inner parts to simplify first, gives each rule a single way to fire, and removes the question of whetherdotorcopygoes first. It also means that whether a rule may fire can change as other parts of the term simplify, which is the main difficulty of the proof.copytakes one small step. The paper'scopy, like ours, fires once and needs a normal argument. It never simplifies its argument inside the rule, which would assume normal forms are unique, and a proof based on that would be circular, because unique normal forms are what confluence gives.- Assets
λandΔare not rewritten. Onlydllooks at them, turning a formation that holds both into⊥. The paper evaluates them separately, in its Morphing (fig:morphing, whereMlambdacalls native code) and Dataization (fig:dataization) functions. Those functions have state and side effects and depend on the host machine, so the question there is whether they are deterministic, not whether they are confluent. HereλandΔare inert values (Binding.lambdaandBinding.delta) that never fire. This boundary comes from the paper's own structure; it is not unfinished work. It is also why the paper's Appendix-A examples are checked withdifftest(against a mergedruntime.phi) instead of being rewritten in Lean. - Every rule
phino rewriteapplies is modelled. The ruledotputs the dispatched formation into the result'sρ, which is what lets some terms rewrite forever, and the model includes it. - Attribute names are unique (Def. 4.8).
With unique names, our lookup, which takes the first match,
agrees with
phino's lookup, which accepts a match at any position. TheNoduppart ofWFstates this assumption. copyrequires its argument to be normal and free ofξ.Step.copyfills a void attribute,⟦B₁,τ↦∅,B₂⟧(τ↦e₁) → ⟦B₁,τ↦e₁,B₂⟧, with no contextualization.phino's printed figure shows only the normal-form condition, because its printer hides theξcondition, butphino's engine enforces it. Whene₁has noξ, contextualization leaves it unchanged, and Lean proves this (contextualize_eq_selfinParallel.lean). So leaving contextualization out ofcopychanges nothing. Requiring noξalso (a) keepscopyindependent of the surrounding term, and (b) makesdifftestmeaningful: without the condition,⟦x↦∅⟧(x↦ξ)would give a different result. The single normal-form testnfincludes theξcondition for void applications, just asphino'sisNFdoes.- A formation's attributes are never positional.
The paper's grammar (
syntax.tex) uses positional namesαᵢonly in application arguments; a formation's attributes areφ,ρ, or labels. The LeanBindingaccepts any name, includingAttr.alpha, so it can represent a malformed formation with a positional attribute. ThelegalKeypart ofWFrules such formations out. - A formation has a parent attribute
ρonly when it declares one. This is modelled, not a difference; see The parent attribute. - The rule
dotgis not modelled.phinousesdotginstead ofdotwhen the dispatched formation is the whole program, and it putsΦinto the result'sρ. The model followsphino rewriteon a single expression, which is never the whole program, sodotalways fires anddotgnever does.
Two more loose spots in the model are intentional.
Step.alpha and Step.copy do not themselves require well-formedness;
the theorem's WF condition excludes malformed terms for all rules at once,
and phino does not check this per rule either.
Also, Nodup ignores the assets,
so a formation may hold two λ or two Δ bindings.
That is harmless while assets never fire,
and would need tightening only if they ever do.
The paper (foundations.tex, Def. Parent) gives an object a parent attribute ρ,
much like this in other languages.
A formation has one in phino only when it declares it,
as ρ↦∅ among its voids or as an attached ρ↦e,
the way EO declares ^.
Nothing adds a ρ behind the program's back,
so ⟦x↦Φ⟧ stays ⟦x↦Φ⟧,
and the model follows it with no extra machinery.
Two rules treat ρ apart from other names:
skipdrops aρapplied to a formation that declares none:⟦⟧(ρ↦Φ)becomes⟦⟧, not⊥.misssparesρfor that reason.dotstill hands every dispatched body its receiver as(ρ↦⟦B⟧);copyfills a declared voidρ,staykeeps an attached one, andskipdrops it otherwise.
Neither threatens confluence:
skip fires only where miss used to,
and it never competes with copy or stay,
since each of the three needs ρ in a different state.
difftest matches phino on both sides,
on cases such as ⟦⟧(ρ↦Φ) and ⟦ρ↦∅⟧(ρ↦Φ).
The paper (objectionary/calculus-paper) defines the calculus
in ordinary mathematical prose.
Its Fig. 4 rules and Appendix-A example reductions
are generated by phino
(\iexec{phino explain --normalize} and phino rewrite),
and its CI checks them.
phino is therefore the authoritative, executable definition,
in resources/normalize/*.yaml.
Because the paper's rules and examples come from phino,
matching the paper means matching phino,
and running both can test that.
| Decision | Reason |
|---|---|
Use phino and the paper's LaTeX source as the reference, never the arXiv PDF |
The LaTeX source regenerates its rules from phino, while a published PDF may lag behind. |
| Prove confluence through parallel reduction | Some terms rewrite forever, so Newman's lemma cannot be used; the parallel-reduction method (Tait, Martin-Löf, Takahashi) works either way. |
Build on mathlib's Relation library |
It already proves that the diamond property implies confluence, so less new code is needed. |
Keep attribute names (φ, ρ, αᵢ, labels) |
They match the paper and phino and make contextualization and positions natural; numbering variables instead (de Bruijn indices) would hide what the names mean. |
Define "normal form" directly, like phino's isNF, not as "no step possible" |
This avoids a circular dependency between Lean files; it depends only on the term itself, so it makes sense before confluence is known. One definition covers every rule, including copy's ξ condition. |
Keep a runnable reducer next to the rules, linked by reduce_sound |
The demo can run and print its steps, and a proof guarantees each printed step follows the rules — a closer link than phino has, since its Haskell engine is not proven against its YAML rules. |
Leave contextualization out of copy |
With no ξ in the argument, it changes nothing, and copy stays independent of the surrounding term (difference 7). |
| State the theorem for well-formed terms only | legalKey is needed for confluence; Nodup keeps the model faithful. Both are the paper's own rules (see Why only well-formed terms). |
The proof goes in six steps.
- Define one rewriting step
⟶(Step): thephinorules plus the four rules that let them fire inside a term. - Define parallel reduction
Par, which rewrites any number of redexes in one go, and the complete developmentdevel e, which rewrites all the redexesehas at once. - Prove that one step is a parallel step,
and a parallel step is a sequence of ordinary steps,
so many steps of either kind reach the same terms (
redMany_eq). - Prove the Takahashi triangle:
for a well-formed
e, ifereachesuin one parallel step, thenureachesdevel ein one parallel step (WF e → Par e u → Par u (devel e)). So any two parallel steps fromemeet again atdevel e; this is the diamond property. - A general theorem (
Abstract.Diamond.confluent, through mathlib'schurch_rosser) turns the diamond property into confluence, and step 3 carries it over to⟶. - Define equality of terms (
≡) as "the terms can be rewritten to a common term"; confluence makes it a proper equivalence on well-formed terms.
The hardest parts were proving that contextualization and rewriting
can happen in either order (par_contextualize_ctx),
handling rules whose permission to fire changes
as other parts simplify,
and handling dot's ρ, which lets terms rewrite forever.
- Well-formedness is a condition, not a type.
WFandWFB(WellFormed.lean, which depends only onSyntax) are a condition assumed by the diamond lemma and the main theorem. Building it into the type of bindings instead would force rewritingSyntax,Step, andAttributes. The proof that rewriting keeps terms well-formed (WF.stepandWF.parinPreservation.lean) rests on one fact: rewriting a binding's value never changes the attribute names (domain_append,domain_set). - Parallel reduction on binding lists is defined by hand.
ParandParBare defined together, andParBwalks a binding list one element at a time. Lean rejects the shortcut throughList.Forall₂, because it does not accept that kind of nested definition here. OnlyStep.congFormsplits a list into "before, this binding, after", andparB_sethandles that split once. Induction over both definitions usesPar.recwithmotive_2. The lemmaredMany_form_consis proved directly (byinduction … generalizingandform_step_inv), because the generalReflTransGen.liftis wrong here:stayturns an application into a formation. - Rule guards are checked on the already-rewritten part.
dotplaces the formation in two spots of its result, so a conflict betweendotand a rewrite of a neighbouring binding can take more than one step on each side to resolve. That rules out Huet's simpler method, which needs such conflicts to resolve in one step. A parallel step rewrites all copies at once, so it has no such problem. The parallel versions of the guarded rules test "is normal" on the rewritten valuee₁', not on the originale₁; for example,Par.dotreads it fromdevelB bsthroughlookup. That keepsdevelsimple and makes the triangle hold.
Limiting the diamond to well-formed terms.
The general theorem (church_rosser, Abstract.Diamond.confluent)
needs the diamond property for all terms,
but it fails for malformed ones,
where alpha and over conflict.
So the proof uses ParWF a b := WF a ∧ Par a b,
a parallel step from a well-formed term.
Since rewriting keeps terms well-formed (WF.par),
ParWF has the diamond property for all terms,
trivially so when the start is not well-formed.
The general theorem then gives confluence of ParWF,
and redMany_eq carries it back to ⟶ on well-formed terms.
Only the starting term must be well-formed,
so no new general lemma is needed (Diamond.lean).
The main risk to confluence is that rule guards, which change as a term simplifies, might break the diamond property. Three observations, which the proof makes rigorous, show they do not:
- Rules never compete at the top of a term.
In rewriting jargon, there are no critical pairs at the top.
On a dispatch
⟦B⟧.τ, the rulesdot,null,stop, andddexclude each other, depending on whetherτis attached, void, or missing, whetherBhasφ, and what the dispatched term is. On an application⟦B⟧(τ↦e), the rulescopy,alpha,overa,amiss,over,stay,skip,miss, anddcexclude each other, depending on the attribute's state, on the kind of name (positional names go toalpha,overa, oramiss, labels tocopy,over, ormiss, andρtocopy,stay, orskip), and on what the applied term is. The ruledlcompetes with others, but always ends at⊥, becausedotis barred there. This relies on the well-formedness conditions: no attribute name repeats, so each attribute has one state, and positional names never name a formation's attribute. - Conflicts deeper inside a term always resolve.
If
dotcopies a part that still has redexes, rewriting that part before or after the copy gives the same normal form. Ifover,null,stop,miss,dc, orddthrows a part away and yields⊥, whatever happened inside that part no longer matters. That is why these rules need no guard, and why confluence holds even though some terms rewrite forever. - Changing guards cause no harm.
Rewriting inside
e₁can make it normal and so allow adotorcopyon the outside, which breaks the most naive version of the triangle. But whilee₁is not normal, no outer rule competes with it, and rewriting a neighbour never disables a rule that could already fire. The third design choice above relies on exactly this.
Other methods do not fit:
Huet's one-step method, because dot copies terms;
orthogonality, because some rules mention the same τ twice;
Hindley–Rosen, because mathlib has no support for it;
and decreasing diagrams,
because Lean has no library for them and they are not needed.
Lean guarantees the proof is correct: the theorem follows from the definitions. It cannot guarantee the definitions are faithful, meaning they describe the φ-calculus the paper means, because the paper is written in prose. Several independent checks narrow that gap:
- A human needs to read only a little.
To trust the result, you need to read only
Syntax,Step, the definition of⟶∗, and the statement ofconfluence, a few dozen lines in total. - The printed rules come from
phino. The rule table (Rules.lean) is generated from the pinnedphinobefore every build, so it cannot drift fromphino. The rules the proof uses,Step, are written by hand, because the proof needs to look at them case by case, anddifftestchecks they behave likephino. - The results are compared with
phino, a technique called differential testing.Difftest.leanand.github/difftest.shsimplify each test program with bothphino rewrite --normalizeand our reducer and check the results are equal. All 26 programs match. Together they exercise every modelled rule, including how positions skipλ,Δ, andρ, on programs that end in⊥and on programs that end in real objects. The paper's Appendix A is generated with the samephino rewrite, so this also reproduces the paper's examples, except those that needλorΔto run. - CI checks the axioms.
The main results (
confluence,conv_equivalence,reduce_sound,par_triangle,parWF_diamond, andnf_iff) may depend only onpropextandQuot.sound, never onsorryAx,Classical.choice, ornative_decide.
CI or Lean checks every arrow in this chain:
paper ──(phino explain/rewrite, calculus-paper CI)──▶ phino rules + example reductions
▲ │
│ (CI diff, this project)
│ ▼
└────────────────────────────────── our printed rules + our reductions (the demo)
│
(Lean: reduce_sound, reduceStep ⊆ Step)
▼
relation Step
│
(Lean kernel: no sorry/axiom)
▼
confluence theorem
To trust the result, you must trust exactly three things,
its trusted computing base:
(a) Lean's kernel, the small core that checks proofs;
(b) the definitions and the theorem statement;
and (c), for the demo and CI only,
the code that prints and parses terms, and phino itself.
The proof adds nothing to this list.
An optional improvement would generate Step itself from phino's YAML files,
not just the printed table,
so the proof and the paper's figure would come from one source.
Today Step is written by hand and checked against phino by difftest.
Generating it would make the match structural instead of tested.
It would not change whether the proof is correct.
Main.lean / Difftest.lean demo + phino differential test
PhiConfluence/
Syntax · Attributes · WellFormed Term/Binding/Attr; lookup/fill/ordinal/erase; the WF predicate
Step the relation ⟶ — phino's rules + congruence closure
Nf · Normal structural normal form (the counterpart of phino's isNF)
Context contextualization C(e⊳ctx)
Parallel Par/ParB, complete development `devel`, the Takahashi triangle
Preservation · Diamond WF preserved under reduction; the WF-relativized diamond
Confluence the headline `confluence`
Equivalence `≡` (convertibility) as an Equivalence on well-formed terms
Reduce · Render · Rules executable reducer + reduce_sound; pretty-printer; rule table
RuleSchema · RuleData rule tags generated from phino by the fidelity lock
Abstract/Rewriting Diamond / Confluent vocabulary + the church_rosser bridge
.github/ regen-rules.sh · gen-rules.py · gen-rule-data.py · phino_render.py · difftest.sh
test_*.py (generator unit tests) · axioms.lean · requirements.txt · workflows/
Makefile `make` builds and checks everything CI checks, except difftest
CI runs separate GitHub Actions workflows
on every push to master and every pull request,
all against the pinned phino:
- build installs Lean, downloads mathlib (
lake exe cache get), and runsmake. That runs the generators' unit tests (.github/test_*.py), generatesRules.leanandRuleData.leanfrom the pinnedphino(.github/regen-rules.sh), builds the proof (lake build), rejects anysorry,admit, oraxiomin the source, and checks the main results' axioms (.github/axioms.lean).gen-rule-data.pyalso fails the build if the structure ofphino's rules changes. - difftest installs the pinned
phinobinary (named in.phino-versionand verified against.phino-sha256) and runsmake difftest, which fails if our reducer andphinodisagree. - phino-latest runs weekly
and fails when
.phino-versionfalls behind the newestphinorelease, so an outdated version is reported instead of quietly limitingdifftest.
The two Lean workflows share one setup action (.github/actions/setup-lean)
that caches ~/.elan and .lake,
so Lean and mathlib are not downloaded on every run.
The usual objectionary checks for style and licensing run alongside.
The proof uses Lean 4 (leanprover/lean4:v4.30.0)
and mathlib4 (pinned in lakefile.toml),
and it builds with Lake, Lean's build tool.
The general rewriting theory rests on mathlib's Relation library.
Fork the repository, make your changes,
and send us a pull request.
We review it and merge it into master
if it meets our quality standards.
To avoid frustration, run the full build before you send it:
makeIf you have phino installed, also compare our reducer with it:
make difftestYou need elan, which installs Lean,
Python 3 with the packages from .github/requirements.txt,
and GNU Make 4.3 or newer.