Logic, L2 — Worlds and Counting#
Level 2 · Practitioner
Source: src/MLambda.AI.Logic/Worlds.hs ·
Worlds.hp ·
Counting.hs ·
Counting.hp
Two samples, and one idea each. Worlds: a logic is a choice of which laws are open. Counting:
a claim can be decided rather than assumed.
Worlds — the frame conditions#
A world is a way things could be#
Modal logic is about necessity and possibility, and the machinery underneath is simpler than the
words suggest. There are worlds — ways things could be — and a relation between them, sees(w, u),
meaning from w, u is possible.
"Necessarily p" then means: p holds at every world this one sees. "Possibly p": p holds at some world it sees. Everything else is a question about the shape of that relation.
Four shapes, and the logic each gives you#
law refl = ∀ w, place(w) ⇒ sees(w, w) -- T
law trans = ∀ w u v, sees(w, u) ∧ sees(u, v) ⇒ sees(w, v) -- 4
law euclid = ∀ w u v, sees(w, u) ∧ sees(w, v) ⇒ sees(u, v) -- 5
law symm = ∀ w u, sees(w, u) ⇒ sees(u, w) -- B
Nothing there mentions a modality. Each condition is about sees alone, which is exactly what makes
them composable — a theory may take any subset without the rest:
| Logic | Conditions open |
|---|---|
| K | none |
| T | refl |
| S4 | refl, trans |
| B | refl, symm |
| K5 | euclid |
| S5 | refl, trans, euclid |
| KD | serial |
| KD45 | serial, trans, euclid |
And the choice is one line in the .hp file:
The question "did you mean K5 or S5?" is therefore never asked of the language. Both are writable,
both are checked by the same kernel, and the axioms [...] line of each .hp says which set its
theorems may lean on. A proof that reaches for a law outside the list is refused, so
"proved under T and 5" is a different statement from "proved", and the file keeps them apart.
The theorems worth having are the ones nobody gives you#
theorem b_from_t_and_5 : ∀ w u, place(w) ⇒ sees(w, u) ⇒ sees(u, w)
proof
intro w u declared a
have here : sees(w, w) by apply refl [declared]
apply euclid [a, here]
qed
Take euclid at (w, u, w): from sees(w,u) and sees(w,w) — which T gives for nothing — it
concludes sees(u,w). So a reflexive euclidean relation is symmetric, and S5 has axiom B without
anyone stating it. Axiom 4 comes the same way. That is why S5's accessibility is an equivalence
relation, and why S5 contains S4 and B without being handed either.
One step there is easy to get wrong. Turning sees(w,u) into sees(u,w) needs euclid at
(w, u, w), whose second premise is sees(w,w). Writing euclid [a, a] instead concludes
sees(u,u) — a true thing, and not the one you needed.
Two restrictions that are the language telling you something#
Reflexivity needed a premise. Every textbook writes T as every world sees itself:
That does not compile. ⊤ is not an atom, so w is bound by nothing and the rule's head would have
to invent a world. The fix is a generator relation — a place the host asserts — and the claim
weakens honestly: not every world sees itself but every world somebody declared sees itself. The
theorem says so, in its own text.
Seriality is the same shape, one law further on. Axiom D is ∀ w, ∃ u, sees(w, u) — every world
sees something — and a Horn rule cannot conclude that something exists (HS0022). So the
successor is named:
The consequence is worth stating plainly: axiom D holds of a frame exactly insofar as something seeded it. A world nobody seeds a successor for has none, and nothing complains. D is a precondition on the runtime, not an inference — and a reader who expects otherwise will be surprised by a frame that quietly fails to be serial.
Both restrictions are in diagnostics, with the shapes that fix them.
Run it#
The output is worth reading twice. Asked what a sees, the engine answers a, b, c — and a
itself is there even though nobody declared it a place, so reflexivity never touched it. Symmetry
turned a→b into b→a, and transitivity composed them. Open a different set of conditions and
that answer changes, which is the whole lesson in one line of output.
Counting — a claim that is decided, not assumed#
There is no engine in this sample#
Counting.hs declares a sort and a single predicate, and no laws at all:
A theory with no laws is a legitimate theory. It says what there is, and nothing about what follows — which is right here, because what follows is decided.
Two tactics, and what they hand the kernel#
ring normalises both sides to the same polynomial. If they are the same polynomial, they are
equal for every value of every variable — no hypotheses needed, which is why these claims have free
variables and no ∀.
theorem a_strict_step_survives : ∀ x y z, x < y ⇒ y <= z ⇒ x < z
proof
intro x y z first second
linarith
qed
linarith refutes the negation of the goal. Each premise says some polynomial is ≥ 0; so does
the negated goal. A non-negative combination of them is itself ≥ 0 — so if every variable cancels
and a negative constant is left, the premises cannot all have held, and the goal was true.
The kernel adds that combination up itself, with exact rational arithmetic, and looks at the sign. It never runs the search that found the multipliers. That is the part worth understanding: the search may be clever, buggy or lucky, and it does not matter, because what is checked is the arithmetic of the certificate.
This one needs the strictness bookkeeping to be real: the combination reaches exactly zero, and only
the strict premise x < y makes zero a contradiction.
axioms [] is the point#
Every theorem stands on its own certificate: ring normalises a polynomial and linarith finds a
Farkas combination, and neither consults a law.
Careful: axioms [] is not what makes that true. To the kernel an empty axiom list means every
law is allowed, and a list that names laws restricts a file to exactly those. What makes this corpus
assume nothing is that Counting.hs declares no laws at all, so there is nothing to lean on. There
is nothing in this corpus a reader has to take on trust, which is a stronger thing to be able to say
than "these theorems are proved".
A theory that proves but does not run#
Counting.hs quantifies over Double so the certificates have numbers to work with. The proof
checker is happy: it only parses the theory. The Shin lowering is not — Double is a built-in sort
and may not be a theory's own (HS0040).
It is right not to be, because there is no engine worth having here: none of these claims is a rule anything could run. One line in the project file says so:
which leaves the theory in the proof inputs and out of the engine inputs. Compare Worlds, which
does have an engine worth having, and where the right answer was to bind the variable instead.
The choice is about the theory, not about the error.
What the tests assert#
WorldsTests.cs ·
CountingTests.cs
- A declared world sees itself; an undeclared one sees nothing.
- Transitivity composes a chain, and symmetry runs it backwards.
- A seeded successor becomes accessible — and only a seeded one.
- B and 4 both come back
Proved, from T and 5. - Every
Countingtheorem isProvedby the kernel while the test runs, and a slack step keeps a strict inequality strict.
Try it yourself#
- Remove
symmfrom theaxioms [...]line inWorlds.hpand build. Everything still checks, because T and 5 give symmetry anyway: you are still in S5. Now removeeuclidtoo. You have moved to S4, and the theorems that fail are what negative introspection was buying. See the modal systems, explained. - Add a theorem to
Counting.hpthat is false —a + b = a— and watch the build fail. The certificate cannot be produced, andHilbertProofStrictwill not let it through. - Give
Worldsa second declared place and ask what it sees.