Imports
/-
Copyright (c) 2026 Terence Rokop. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Terence Rokop
-/
module
public import Geb.Mathlib.Data.W.BasicConcrete syntaxes for the Geb AST (prototype)
The abstract syntax is the initial algebra of F X = Fin k + X × X.
A concrete syntax is a pair parse : C → Option D, print : D → C
satisfying the retraction law parse (print d) = some d; formatter
idempotence and injectivity of print are corollaries, proved once
here for every syntax.
This module carries the format-independent core (the abstract syntax, its annotated form, the rose presentation), one worked concrete syntax, the canonical S-expression form of [RFC9804], and the parser machinery more than one spelling shares: the decimal layer and the bounded loop over a rose node's children.
Every tree type here is a WType, so its recursion is carried by
WType.elim, WType.para and WType.rec.
Main definitions
Ast — the abstract syntax, the W-type on Ast.Shape.
Ast.toRose, Ast.ofRose — the two directions of the rose
presentation's bijection with Ast.
Tree — the abstract syntax with every node decorated by an A,
with Tree.map, Tree.extract, Tree.duplicate its functor and
comonad structure and Tree.erase the forgetful map to Ast.
Ann, Doc — the annotation vocabulary and the annotated document
type Tree k Ann, with Ast.trivialDoc the empty decoration.
Rose — the rose-tree presentation of the same fixed point, with
Rose.node and Rose.ofList its constructors and Rose.snoc
appending a child to one.
Rose.parseChildren — the bounded loop reading a rose node's
children up to the closing parenthesis, shared by every spelling of
the rose presentation that closes a child list with ')'.
Retraction, format — the law skeleton a concrete syntax proves.
Csexp.print, Csexp.parse — the [RFC9804] canonical S-expression
syntax.
Main statements
Ast.ind — the two-constructor induction principle on Ast.
Ast.ofRose_toRose, Ast.toRose_ofRose — the rose presentation is
a bijection.
Rose.ofList_ofFn — a rose node rebuilt by Rose.ofList from the
list of its children is that node. It is the equation justifying the
transport a variable-arity parser needs.
Csexp.parse_print — the retraction law for the S-expression syntax,
from Csexp.parseAst_printAst and Csexp.size_le_length_printAst.
Csexp.readVerbatim_append — a printed atom reads back whole,
whatever it contains.
Ast.erase_trivialDoc — the trivial decoration is erased away.
Csexp.format_idem, Csexp.print_injective — the generic corollaries
instantiated, which is what shows the law skeleton applies to a
syntax rather than only to a hypothetical one.
Geb.format_idem, Geb.print_injective — the corollaries of a
retraction, proved once for every syntax.
Tree.extract_duplicate, Tree.map_extract_duplicate,
Tree.duplicate_duplicate — the comonad laws on the annotated
syntax, with Tree.map_id and Tree.map_map the functor laws they
presuppose and Tree.extract_map, Tree.duplicate_map the
naturality of the two structure maps.
Implementation notes
Csexp.parseAst recurses on an explicit Nat bound rather than on its
input: the recursion descends only through the remainder each call
returns, which is not a form Lean's structural recursion accepts.
Csexp.parse supplies the input length, and Csexp.size_le_length_printAst
shows that bound admits every tree the printer emits.
The decimal layer is written here rather than reused, and the reason is
Classical.choice throughout. mathlib's Nat.digits depends on it.
Core supplies the whole layer — Nat.toDigits, which agrees with
Csexp.decOf pointwise on base 10, the decoder Nat.ofDigitChars, and
the same round trip in total form, Nat.ofDigitChars_ten_toDigits,
where Csexp.digitsVal_decOf is partial. It cannot be used. That round
trip depends on Classical.choice, and so does every lemma descending
from toDigits b n to toDigits b (n / b) — Nat.toDigits_eq_if and
Nat.toDigits_of_base_le — so it can be neither imported nor reproved.
The boundary is not depth but direction: the printer's descent lemmas
are choice-dependent, while the decoder's recursion equations and the
digit-character lemmas are not.
finEnumFin and finEnumEmpty are named because mathlib's FinEnum
instances for Fin n and Empty are Classical.choice-dependent.
Rose.Arity is an abbrev because a proof matching a child function's
type against a lemma stated at Fin n → _ needs it reducible;
Ast.ofRose_snoc and Ast.toRose_ofRose are the two here, and
Rose.parseAux_print and Rsexp.parseAux_print the third and fourth,
downstream. Its docstring names all
four and says what the reducibility costs.
Ast.Arity and Tree.Arity are plain defs, which keeps instance
search from reducing past them at a literal shape. No demand here
reaches that case; the defs guard against one arising later, which
would otherwise resolve through mathlib's Classical.choice-dependent
FinEnum.fin.
References
[RFC9804]
[UustaluVene2011]
Tags
abstract syntax, concrete syntax, W-type, retraction, S-expression
universe uA uB uC@[expose] public sectionnamespace GebChoice-free finite enumerations
Every Arity family below is finitely enumerable, which is what lets
WType.instDecidableEq decide equality of the corresponding tree type.
The #guards in the three GebTests syntax modules decide equality at
Ast k, at Tree k Ann and at Rose k. The two enumerations are named
because mathlib's FinEnum.fin and FinEnum.empty are built by
FinEnum.ofList, whose proof obligations depend on Classical.choice,
which CONTRIBUTING.md § Constructive-only
forbids. Naming them suffices for that consumer: WType.instDecidableEq
asks for a family at a general shape, where nothing reduces, so equality
is decided choice-free however the family is declared — Rose.Arity is
an abbrev and equality at Rose k is choice-free. What a reducible
family gives up is FinEnum demanded at a shape already a literal,
where search reduces past it to Empty or Fin n and selects mathlib's.
Ast.Arity and Tree.Arity are plain defs so that such a demand
would reach the named instance; nothing here makes one. Neither
construction is specific to syntax.
Fin n enumerated by the identity equivalence.
@[instance_reducible] def finEnumFin (n : Nat) : FinEnum (Fin n) := ⟨n, Equiv.refl (Fin n)⟩
Empty enumerated by the empty equivalence.
@[instance_reducible] def finEnumEmpty : FinEnum Empty :=
⟨0, ⟨Empty.elim, Fin.elim0, fun e ↦ e.elim, fun i ↦ i.elim0⟩⟩Abstract syntax
namespace Ast
The node shapes of the Geb abstract syntax: a leaf carrying a label
in Fin k, or a fork.
A leaf, labelled by an element of Fin k.
A fork, with two children.
The child index type of each shape: a leaf has none, a fork has two.
Fin 2 rather than Bool, so that every non-empty arity here is
enumerated by finEnumFin. A def rather than an abbrev: reducible,
instance search at a concrete shape would whnf past this family to
Empty or Fin 2 and select mathlib's Classical.choice-dependent
FinEnum, never reaching instFinEnumArity.
Every arity is finitely enumerable.
instance instFinEnumArity {k : Nat} (s : Shape k) : FinEnum (Arity s) :=
match s with
| .leaf _ => finEnumEmpty
| .fork => finEnumFin 2end Ast
The Geb abstract syntax: the initial algebra of F X = Fin k + X × X,
presented as the W-type on Ast.Shape k.
abbrev Ast (k : Nat) : Type := WType (Ast.Arity (k := k))namespace Ast
A leaf labelled i.
A fork with left child l and right child r.
def fork {k : Nat} (l r : Ast k) : Ast k :=
WType.mk .fork fun b : Fin 2 ↦ Fin.cases l (fun _ ↦ r) bThe number of nodes.
def size {k : Nat} : Ast k → Nat :=
WType.elim Nat fun x ↦
match x with
| ⟨.leaf _, _⟩ => 1
| ⟨.fork, ch⟩ => 1 + ch (0 : Fin 2) + ch (1 : Fin 2)
Induction on Ast in its two-constructor presentation, so that a
proof driven by it need not mention the underlying shape and arity.
theorem ind {k : Nat} {motive : Ast k → Prop}
(leaf : ∀ i, motive (leaf i))
(fork : ∀ l r, motive l → motive r → motive (fork l r)) :
∀ t, motive t :=
WType.rec (motive := motive) fun s f ih ↦
match s, f, ih with
k:ℕmotive:Ast k → Propleaf:∀ (i : Fin k), motive (Ast.leaf i)fork:∀ (l r : Ast k), motive l → motive r → motive (l.fork r)s:Shape kf✝:Arity s → WType Arityih:∀ (a : Arity s), motive (f✝ a)i:Fin kf:Arity (Shape.leaf i) → WType Arityx✝:∀ (a : Arity (Shape.leaf i)), motive (f a)⊢ motive (WType.mk (Shape.leaf i) f) k:ℕmotive:Ast k → Propleaf:∀ (i : Fin k), motive (Ast.leaf i)fork:∀ (l r : Ast k), motive l → motive r → motive (l.fork r)s:Shape kf✝:Arity s → WType Arityih:∀ (a : Arity s), motive (f✝ a)i:Fin kf:Arity (Shape.leaf i) → WType Arityx✝:∀ (a : Arity (Shape.leaf i)), motive (f a)⊢ motive (WType.mk (Shape.leaf i) f)
k:ℕmotive:Ast k → Propleaf:∀ (i : Fin k), motive (Ast.leaf i)fork:∀ (l r : Ast k), motive l → motive r → motive (l.fork r)s:Shape kf✝:Arity s → WType Arityih:∀ (a : Arity s), motive (f✝ a)i:Fin kf:Arity (Shape.leaf i) → WType Arityx✝:∀ (a : Arity (Shape.leaf i)), motive (f a)this:f = Empty.elim⊢ motive (WType.mk (Shape.leaf i) f)
k:ℕmotive:Ast k → Propleaf:∀ (i : Fin k), motive (Ast.leaf i)fork:∀ (l r : Ast k), motive l → motive r → motive (l.fork r)s:Shape kf:Arity s → WType Arityih:∀ (a : Arity s), motive (f✝ a)i:Fin kx✝:∀ (a : Arity (Shape.leaf i)), motive (Empty.elim a)⊢ motive (WType.mk (Shape.leaf i) Empty.elim); All goals completed! 🐙
k:ℕmotive:Ast k → Propleaf:∀ (i : Fin k), motive (Ast.leaf i)fork:∀ (l r : Ast k), motive l → motive r → motive (l.fork r)s:Shape kf✝:Arity s → WType Arityih✝:∀ (a : Arity s), motive (f✝ a)f:Arity Shape.fork → WType Arityih:∀ (a : Arity Shape.fork), motive (f a)⊢ motive (WType.mk Shape.fork f) k:ℕmotive:Ast k → Propleaf:∀ (i : Fin k), motive (Ast.leaf i)fork:∀ (l r : Ast k), motive l → motive r → motive (l.fork r)s:Shape kf✝:Arity s → WType Arityih✝:∀ (a : Arity s), motive (f✝ a)f:Arity Shape.fork → WType Arityih:∀ (a : Arity Shape.fork), motive (f a)⊢ motive (WType.mk Shape.fork f)
k:ℕmotive:Ast k → Propleaf:∀ (i : Fin k), motive (Ast.leaf i)fork:∀ (l r : Ast k), motive l → motive r → motive (l.fork r)s:Shape kf✝:Arity s → WType Arityih✝:∀ (a : Arity s), motive (f✝ a)f:Arity Shape.fork → WType Arityih:∀ (a : Arity Shape.fork), motive (f a)this:(fun b ↦ Fin.cases (f 0) (fun x ↦ f 1) b) = f⊢ motive (WType.mk Shape.fork f)
All goals completed! 🐙@[simp] theorem size_leaf {k : Nat} (i : Fin k) : (leaf i).size = 1 := rfl@[simp] theorem size_fork {k : Nat} (l r : Ast k) :
(fork l r).size = 1 + l.size + r.size := rflend AstAnnotated syntax
namespace Tree
The node shapes of the annotated syntax: an Ast.Shape paired with
the decoration carried at that node.
abbrev Shape (k : Nat) (A : Type uA) : Type uA := A × Ast.Shape k
The child index type of an annotated shape: that of the underlying
Ast.Shape, since decorating a node does not change its children. A
def rather than an abbrev, for the reason Ast.Arity gives.
Every annotated arity is finitely enumerable.
instance instFinEnumArity {k : Nat} {A : Type uA} (s : Shape k A) :
FinEnum (Arity s) :=
Ast.instFinEnumArity s.2end Tree
Ast k with every node decorated by an A: the initial algebra of
X ↦ A × F X. The initial algebra rather than the terminal coalgebra,
because syntax trees are finite; the cofree comonad on F admits
infinitely deep trees and is the home of execution traces, not syntax.
abbrev Tree (k : Nat) (A : Type uA) : Type uA :=
WType (Tree.Arity (k := k) (A := A))namespace Tree
Relabel every node along f.
def map {k : Nat} {A : Type uA} {B : Type uB} (f : A → B) :
Tree k A → Tree k B :=
WType.elim (Tree k B) fun x ↦ WType.mk (f x.1.1, x.1.2) x.2The comonad counit: the decoration at the root.
def extract {k : Nat} {A : Type uA} (t : Tree k A) : A :=
(WType.toSigma t).1.1The comonad comultiplication: relabel each node with the annotated subtree rooted at it, in the sense [UustaluVene2011] gives the comultiplication of the cofree recursive comonad. A paramorphism, since the new decoration at a node is that node's own subtree.
def duplicate {k : Nat} {A : Type uA} : Tree k A → Tree k (Tree k A) :=
WType.para (Tree k (Tree k A)) fun x ↦
WType.mk (WType.mk x.1 fun b ↦ (x.2 b).1, x.1.2) fun b ↦ (x.2 b).2
Forget every decoration, recovering the bare tree. This is the fold
induced by the second projection A × F X → F X, not the comonad
counit.
def erase {k : Nat} {A : Type uA} : Tree k A → Ast k :=
WType.elim (Ast k) fun x ↦ WType.mk x.1.2 x.2@[simp] theorem map_mk {k : Nat} {A : Type uA} {B : Type uB} (f : A → B)
(s : Shape k A) (ch : Arity s → Tree k A) :
map f (WType.mk s ch) = WType.mk (f s.1, s.2) fun b ↦ map f (ch b) :=
rfl@[simp] theorem extract_mk {k : Nat} {A : Type uA} (s : Shape k A)
(ch : Arity s → Tree k A) : extract (WType.mk s ch) = s.1 :=
rfl@[simp] theorem duplicate_mk {k : Nat} {A : Type uA} (s : Shape k A)
(ch : Arity s → Tree k A) :
duplicate (WType.mk s ch)
= WType.mk (WType.mk s ch, s.2) fun b ↦ duplicate (ch b) :=
WType.para_mk _ s chThe first functor law: relabelling along the identity is the identity.
theorem map_id {k : Nat} {A : Type uA} (t : Tree k A) : map id t = t :=
WType.rec (motive := fun t ↦ map id t = t)
(fun s ch ih ↦ k:ℕA:Type uAt:Tree k As:Shape k Ach:Arity s → WType Arityih:∀ (a : Arity s), map id (ch a) = ch a⊢ map id (WType.mk s ch) = WType.mk s ch k:ℕA:Type uAt:Tree k As:Shape k Ach:Arity s → WType Arityih:∀ (a : Arity s), map id (ch a) = ch a⊢ (WType.mk (s.1, s.2) fun b ↦ map id (ch b)) = WType.mk s ch; All goals completed! 🐙) tThe second functor law: relabelling twice is relabelling along the composite.
theorem map_map {k : Nat} {A : Type uA} {B : Type uB} {C : Type uC}
(f : A → B) (g : B → C) (t : Tree k A) :
map g (map f t) = map (g ∘ f) t :=
WType.rec (motive := fun t ↦ map g (map f t) = map (g ∘ f) t)
(fun s ch ih ↦ k:ℕA:Type uAB:Type uBC:Type uCf:A → Bg:B → Ct:Tree k As:Shape k Ach:Arity s → WType Arityih:∀ (a : Arity s), map g (map f (ch a)) = map (g ∘ f) (ch a)⊢ map g (map f (WType.mk s ch)) = map (g ∘ f) (WType.mk s ch) k:ℕA:Type uAB:Type uBC:Type uCf:A → Bg:B → Ct:Tree k As:Shape k Ach:Arity s → WType Arityih:∀ (a : Arity s), map g (map f (ch a)) = map (g ∘ f) (ch a)⊢ (WType.mk (g (f s.1), s.2) fun b ↦ map g (map f (ch b))) = WType.mk (g (f s.1), s.2) fun b ↦ map (g ∘ f) (ch b)
All goals completed! 🐙) tNaturality of the counit: reading the root decoration commutes with relabelling.
theorem extract_map {k : Nat} {A : Type uA} {B : Type uB} (f : A → B)
(t : Tree k A) : extract (map f t) = f (extract t) := k:ℕA:Type uAB:Type uBf:A → Bt:Tree k A⊢ (map f t).extract = f t.extract
cases t with
k:ℕA:Type uAB:Type uBf:A → Bs:Shape k Ach:Arity s → WType Arity⊢ (map f (WType.mk s ch)).extract = f (extract (WType.mk s ch)) All goals completed! 🐙Naturality of the comultiplication: redecorating commutes with relabelling, the relabelling acting on each subtree.
theorem duplicate_map {k : Nat} {A : Type uA} {B : Type uB} (f : A → B)
(t : Tree k A) :
duplicate (map f t) = map (map f) (duplicate t) :=
WType.rec (motive := fun t ↦ duplicate (map f t) = map (map f) (duplicate t))
(fun s ch ih ↦ k:ℕA:Type uAB:Type uBf:A → Bt:Tree k As:Shape k Ach:Arity s → WType Arityih:∀ (a : Arity s), (map f (ch a)).duplicate = map (map f) (duplicate (ch a))⊢ (map f (WType.mk s ch)).duplicate = map (map f) (duplicate (WType.mk s ch)) k:ℕA:Type uAB:Type uBf:A → Bt:Tree k As:Shape k Ach:Arity s → WType Arityih:∀ (a : Arity s), (map f (ch a)).duplicate = map (map f) (duplicate (ch a))⊢ (WType.mk (WType.mk (f s.1, s.2) fun b ↦ map f (ch b), s.2) fun b ↦ (map f (ch b)).duplicate) =
WType.mk (WType.mk (f s.1, s.2) fun b ↦ map f (ch b), s.2) fun b ↦ map (map f) (duplicate (ch b))
All goals completed! 🐙) tThe first comonad law: the subtree redecorating the root is the whole tree.
theorem extract_duplicate {k : Nat} {A : Type uA} (t : Tree k A) :
extract (duplicate t) = t := k:ℕA:Type uAt:Tree k A⊢ t.duplicate.extract = t
cases t with
k:ℕA:Type uAs:Shape k Ach:Arity s → WType Arity⊢ (duplicate (WType.mk s ch)).extract = WType.mk s ch All goals completed! 🐙The second comonad law: taking each node's subtree and then keeping only its root decoration recovers the original decoration.
theorem map_extract_duplicate {k : Nat} {A : Type uA} (t : Tree k A) :
map extract (duplicate t) = t :=
WType.rec (motive := fun t ↦ map extract (duplicate t) = t)
(fun s ch ih ↦ k:ℕA:Type uAt:Tree k As:Shape k Ach:Arity s → WType Arityih:∀ (a : Arity s), map extract (duplicate (ch a)) = ch a⊢ map extract (duplicate (WType.mk s ch)) = WType.mk s ch k:ℕA:Type uAt:Tree k As:Shape k Ach:Arity s → WType Arityih:∀ (a : Arity s), map extract (duplicate (ch a)) = ch a⊢ (WType.mk (s.1, s.2) fun b ↦ map extract (duplicate (ch b))) = WType.mk s ch
All goals completed! 🐙) tThe third comonad law, coassociativity of the redecoration map.
theorem duplicate_duplicate {k : Nat} {A : Type uA} (t : Tree k A) :
duplicate (duplicate t) = map duplicate (duplicate t) :=
WType.rec (motive := fun t ↦ duplicate (duplicate t) = map duplicate (duplicate t))
(fun s ch ih ↦ k:ℕA:Type uAt:Tree k As:Shape k Ach:Arity s → WType Arityih:∀ (a : Arity s), (duplicate (ch a)).duplicate = map duplicate (duplicate (ch a))⊢ (duplicate (WType.mk s ch)).duplicate = map duplicate (duplicate (WType.mk s ch)) k:ℕA:Type uAt:Tree k As:Shape k Ach:Arity s → WType Arityih:∀ (a : Arity s), (duplicate (ch a)).duplicate = map duplicate (duplicate (ch a))⊢ (WType.mk (WType.mk (WType.mk s ch, s.2) fun b ↦ duplicate (ch b), s.2) fun b ↦ (duplicate (ch b)).duplicate) =
WType.mk (duplicate (WType.mk s ch), s.2) fun b ↦ map duplicate (duplicate (ch b))
k:ℕA:Type uAt:Tree k As:Shape k Ach:Arity s → WType Arityih:∀ (a : Arity s), (duplicate (ch a)).duplicate = map duplicate (duplicate (ch a))⊢ (WType.mk (WType.mk (WType.mk s ch, s.2) fun b ↦ duplicate (ch b), s.2) fun b ↦ (duplicate (ch b)).duplicate) =
WType.mk (WType.mk (WType.mk s ch, s.2) fun b ↦ duplicate (ch b), s.2) fun b ↦ map duplicate (duplicate (ch b))
exact congrArg _ (funext ih) All goals completed! 🐙) tend TreeThe annotation vocabulary carried at a node. Durable metadata is an annotation value, never a lexical comment: generic parsers discard comments, and canonical forms differ over whether they are retained.
A name for the annotated occurrence.
Prose documentation of the annotated occurrence.
References to material bearing on the annotated occurrence.
@[ext] structure Ann where name : Option String := none doc : Option String := none links : List String := []
deriving Repr, DecidableEq, InhabitedAn annotated Geb document.
namespace AstDecorate every node with the empty annotation.
def trivialDoc {k : Nat} : Ast k → Doc k :=
WType.elim (Doc k) fun x ↦ WType.mk (({} : Ann), x.1) x.2Decorating every node with the empty annotation and then erasing is the identity.
theorem erase_trivialDoc {k : Nat} (a : Ast k) :
Tree.erase a.trivialDoc = a :=
WType.rec (motive := fun a ↦ Tree.erase (trivialDoc a) = a)
(fun s ch ih ↦ by k:ℕa:Ast ks:Shape kch:Arity s → WType Arityih:∀ (a : Arity s), Tree.erase (trivialDoc (ch a)) = ch a⊢ Tree.erase (trivialDoc (WType.mk s ch)) = WType.mk s ch simp only [trivialDoc, WType.elim_mk] k:ℕa:Ast ks:Shape kch:Arity s → WType Arityih:∀ (a : Arity s), Tree.erase (trivialDoc (ch a)) = ch a⊢ Tree.erase (WType.mk ({ }, s) fun b ↦ WType.elim (Doc k) (fun x ↦ WType.mk ({ }, x.fst) x.snd) (ch b)) = WType.mk s ch
exact congrArg _ (funext ih) All goals completed! 🐙) aend AstThe rose presentation and its bijection
namespace Rose
The node shapes of the rose presentation: a label in Fin k and a
number of children.
abbrev Shape (k : Nat) : Type := Fin k × Nat
The child index type of a rose shape. Reducible, and this is the one
family that has to be: simp and rw match at reducible transparency,
so with a plain def a child function whose type reads
Rose.Arity (i, n) → _ in a goal fails to unify with a lemma stated at
Fin n → _, and the proofs of Ast.ofRose_snoc and
Ast.toRose_ofRose fail here, Geb.Rose.parseAux_print's and
Geb.Rsexp.parseAux_print's downstream.
The cost is that instFinEnumArity below is unreachable at a concrete
shape: instance search reduces past this family to Fin n and selects
mathlib's Classical.choice-dependent FinEnum.fin. Deciding equality
at a Rose k does not incur that, WType.instDecidableEq asking for
the family at a general shape and so reaching the named instance; what
would pay it is a demand for FinEnum at a shape already a literal.
Ast.Arity and Tree.Arity are plain defs, at which search reaches
the named instance either way.
abbrev Arity {k : Nat} (s : Shape k) : Type := Fin s.2
Every rose arity is finitely enumerable. Named for the same reason
as the other two: WType.instDecidableEq asks for this family at a
general shape, so deciding equality at a Rose k goes through this
instance rather than mathlib's Classical.choice-dependent
FinEnum.fin. All three GebTests syntax modules decide it. A #guard
is not a declaration, so GebMeta.detectNonstandardAxiom would not
catch a leak there.
instance instFinEnumArity {k : Nat} (s : Shape k) : FinEnum (Arity s) :=
finEnumFin s.2end Rose
The rose-tree presentation: a label in Fin k and a finite sequence
of children, satisfying the same fixed-point equation as Ast k.
abbrev Rose (k : Nat) : Type := WType (Rose.Arity (k := k))namespace Rose
Append t to the children of r, keeping r's label.
def snoc {k : Nat} (r t : Rose k) : Rose k :=
match r with
| ⟨(i, n), f⟩ => WType.mk (i, n + 1) (Fin.snoc f t)
The node with label i and children f.
@[simp] theorem snoc_node {k : Nat} (i : Fin k) {n : Nat}
(f : Fin n → Rose k) (t : Rose k) :
snoc (node i f) t = node i (Fin.snoc f t) := rfl
The node with label i whose children are the entries of ts. The
arity is read off the list, so this is the constructor available to a
parser, which learns a node's children one at a time and its arity only
when the list ends.
def ofList {k : Nat} (i : Fin k) (ts : List (Rose k)) : Rose k :=
node i fun j : Fin ts.length ↦ ts.get j
ofList against a tuple presentation of the same children. The list
is a parameter rather than List.ofFn f, which is what makes the arity
equation substitutable: n = (List.ofFn f).length cannot be substituted,
n occurring on the right through the type of f.
theorem ofList_eq {k n : Nat} (i : Fin k) (ts : List (Rose k))
(f : Fin n → Rose k) (h : n = ts.length)
(hf : ∀ j : Fin n, ts.get (j.cast h) = f j) :
ofList i ts = node i f := by k:ℕn:ℕi:Fin kts:List (Rose k)f:Fin n → Rose kh:n = ts.lengthhf:∀ (j : Fin n), ts.get (Fin.cast h j) = f j⊢ ofList i ts = node i f
subst h k:ℕi:Fin kts:List (Rose k)f:Fin ts.length → Rose khf:∀ (j : Fin ts.length), ts.get (Fin.cast ⋯ j) = f j⊢ ofList i ts = node i f
exact congrArg (node i) (funext hf) All goals completed! 🐙
Rebuilding a node from the list of its children recovers it. This is
the equation justifying the transport a variable-arity parser needs and
a fixed-arity one does not: the loop that reads the children returns a
List, while the node takes a Fin n-indexed tuple.
theorem ofList_ofFn {k n : Nat} (i : Fin k) (f : Fin n → Rose k) :
ofList i (List.ofFn f) = node i f :=
ofList_eq i _ f List.length_ofFn.symm fun j ↦ by k:ℕn:ℕi:Fin kf:Fin n → Rose kj:Fin n⊢ (List.ofFn f).get (Fin.cast ⋯ j) = f j simp All goals completed! 🐙end Rosenamespace Rose
Read children until the closing parenthesis, delegating each to
childParse. The Nat argument bounds the loop: it recurses on the
remainder childParse returns, which is not a form Lean's structural
recursion accepts. The loop consumes one unit per child and one on the
closing parenthesis, so a node of n children needs n + 1.
Shared by every spelling that closes a child list with ')'.
def parseChildren {k : Nat}
(childParse : List Char → Option (Rose k × List Char)) :
Nat → List Char → Option (List (Rose k) × List Char) :=
Nat.rec (motive := fun _ ↦ List Char → Option (List (Rose k) × List Char))
(fun _ ↦ none)
fun _ ih cs ↦
match cs with
| [] => none
| c :: cs' =>
if c = ')' then some ([], cs')
else (childParse (c :: cs')).bind fun p ↦
(ih p.2).map fun q ↦ (p.1 :: q.1, q.2)@[simp] theorem parseChildren_succ_close {k : Nat}
(childParse : List Char → Option (Rose k × List Char)) (f : Nat)
(rest : List Char) :
parseChildren childParse (f + 1) (')' :: rest) = some ([], rest) := rfltheorem parseChildren_succ_cons {k : Nat}
(childParse : List Char → Option (Rose k × List Char)) (f : Nat)
(c : Char) (cs : List Char) (h : c ≠ ')') :
parseChildren childParse (f + 1) (c :: cs)
= (childParse (c :: cs)).bind fun p ↦
(parseChildren childParse f p.2).map fun q ↦ (p.1 :: q.1, q.2) :=
ite_eq_right hend Rosenamespace Ast
The binary-to-rose direction. A rose node is read as a curried
function: its label is the function and its children are the arguments
it is applied to, in order. A fork (l, r) is read as the application
of l to r. Application of a curried function is left-associative, so
l carries the label together with every argument but the last, and r
is the last argument alone — that is, the child sequence is consumed as
a snoclist. Reading application to the right instead gives a different
and equally valid bijection, so the choice has to be fixed.
def toRose {k : Nat} : Ast k → Rose k :=
WType.elim (Rose k) fun x ↦
match x with
| ⟨.leaf i, _⟩ => Rose.node i Fin.elim0
| ⟨.fork, ch⟩ => Rose.snoc (ch (0 : Fin 2)) (ch (1 : Fin 2))The rose-to-binary direction, folding a node's children into the left spine that carries the label at its head.
def ofRose {k : Nat} : Rose k → Ast k :=
WType.elim (Ast k) fun x ↦
Fin.foldl x.1.2 (fun acc j ↦ fork acc (x.2 j)) (leaf x.1.1)@[simp] theorem toRose_leaf {k : Nat} (i : Fin k) :
(leaf i).toRose = Rose.node i Fin.elim0 := rfl@[simp] theorem toRose_fork {k : Nat} (l r : Ast k) :
(fork l r).toRose = Rose.snoc l.toRose r.toRose := rfl@[simp] theorem ofRose_node {k : Nat} (i : Fin k) {n : Nat} (f : Fin n → Rose k) :
ofRose (Rose.node i f) =
Fin.foldl n (fun acc j ↦ fork acc (ofRose (f j))) (leaf i) :=
rfl
Appending a child to a rose node appends a fork on the binary side,
which is the step Ast.ofRose_toRose turns on.
theorem ofRose_snoc {k : Nat} (r t : Rose k) :
ofRose (Rose.snoc r t) = fork (ofRose r) (ofRose t) := by k:ℕr:Rose kt:Rose k⊢ ofRose (r.snoc t) = (ofRose r).fork (ofRose t)
obtain ⟨⟨i, n⟩, f⟩ := r k:ℕt:Rose ki:Fin kn:ℕf:Rose.Arity (i, n) → WType Rose.Arity⊢ ofRose (Rose.snoc (WType.mk (i, n) f) t) = (ofRose (WType.mk (i, n) f)).fork (ofRose t)
change ofRose (Rose.snoc (Rose.node i f) t)
= fork (ofRose (Rose.node i f)) (ofRose t) k:ℕt:Rose ki:Fin kn:ℕf:Rose.Arity (i, n) → WType Rose.Arity⊢ ofRose ((Rose.node i f).snoc t) = (ofRose (Rose.node i f)).fork (ofRose t)
simp only [Rose.snoc_node, ofRose_node, Fin.foldl_succ_last, Fin.snoc_castSucc,
Fin.snoc_last] All goals completed! 🐙
One half of the rose/binary bijection: converting to a rose tree and
back is the identity on Ast k.
theorem ofRose_toRose {k : Nat} (a : Ast k) : ofRose a.toRose = a :=
ind (motive := fun a ↦ ofRose a.toRose = a)
(fun i ↦ by k:ℕa:Ast ki:Fin k⊢ ofRose (leaf i).toRose = leaf i simp only [toRose_leaf, ofRose_node, Fin.foldl_zero] All goals completed! 🐙)
(fun l r ihl ihr ↦ by k:ℕa:Ast kl:Ast kr:Ast kihl:ofRose l.toRose = lihr:ofRose r.toRose = r⊢ ofRose (l.fork r).toRose = l.fork r rw [toRose_fork, k:ℕa:Ast kl:Ast kr:Ast kihl:ofRose l.toRose = lihr:ofRose r.toRose = r⊢ ofRose (l.toRose.snoc r.toRose) = l.fork r ofRose_snoc, k:ℕa:Ast kl:Ast kr:Ast kihl:ofRose l.toRose = lihr:ofRose r.toRose = r⊢ (ofRose l.toRose).fork (ofRose r.toRose) = l.fork r ihl, k:ℕa:Ast kl:Ast kr:Ast kihl:ofRose l.toRose = lihr:ofRose r.toRose = r⊢ l.fork (ofRose r.toRose) = l.fork r ihr k:ℕa:Ast kl:Ast kr:Ast kihl:ofRose l.toRose = lihr:ofRose r.toRose = r⊢ l.fork r = l.fork r] All goals completed! 🐙) a
The image of a left spine under toRose: the fold that ofRose
performs on a node's children is undone one child at a time, from the
last.
theorem toRose_foldl {k : Nat} (i : Fin k) :
∀ (n : Nat) (g : Fin n → Ast k),
(Fin.foldl n (fun acc j ↦ fork acc (g j)) (leaf i)).toRose
= Rose.node i fun j ↦ (g j).toRose :=
Nat.rec
(motive := fun n ↦ ∀ g : Fin n → Ast k,
(Fin.foldl n (fun acc j ↦ fork acc (g j)) (leaf i)).toRose
= Rose.node i fun j ↦ (g j).toRose)
(fun g ↦ by k:ℕi:Fin kg:Fin Nat.zero → Ast k⊢ (Fin.foldl Nat.zero (fun acc j ↦ acc.fork (g j)) (leaf i)).toRose = Rose.node i fun j ↦ (g j).toRose
simp only [Fin.foldl_zero, toRose_leaf] k:ℕi:Fin kg:Fin Nat.zero → Ast k⊢ Rose.node i Fin.elim0 = Rose.node i fun j ↦ (g j).toRose
exact congrArg _ (funext fun j ↦ j.elim0) All goals completed! 🐙)
(fun n ih g ↦ by k:ℕi:Fin kn:ℕih:∀ (g : Fin n → Ast k), (Fin.foldl n (fun acc j ↦ acc.fork (g j)) (leaf i)).toRose = Rose.node i fun j ↦ (g j).toRoseg:Fin n.succ → Ast k⊢ (Fin.foldl n.succ (fun acc j ↦ acc.fork (g j)) (leaf i)).toRose = Rose.node i fun j ↦ (g j).toRose
simp only [Fin.foldl_succ_last, toRose_fork,
ih (fun j ↦ g j.castSucc), Rose.snoc_node] k:ℕi:Fin kn:ℕih:∀ (g : Fin n → Ast k), (Fin.foldl n (fun acc j ↦ acc.fork (g j)) (leaf i)).toRose = Rose.node i fun j ↦ (g j).toRoseg:Fin n.succ → Ast k⊢ Rose.node i (Fin.snoc (fun j ↦ (g j.castSucc).toRose) (g (Fin.last n)).toRose) = Rose.node i fun j ↦ (g j).toRose
exact congrArg _ (Fin.snoc_init_self fun j ↦ (g j).toRose) All goals completed! 🐙)
The other half of the rose/binary bijection: converting from a rose
tree and back is the identity on Rose k.
theorem toRose_ofRose {k : Nat} (r : Rose k) : (ofRose r).toRose = r :=
WType.rec (motive := fun r ↦ (ofRose r).toRose = r)
(fun s f ih ↦ by k:ℕr:Rose ks:Rose.Shape kf:Rose.Arity s → WType Rose.Arityih:∀ (a : Rose.Arity s), (ofRose (f a)).toRose = f a⊢ (ofRose (WType.mk s f)).toRose = WType.mk s f
obtain ⟨i, n⟩ := s k:ℕr:Rose ki:Fin kn:ℕf:Rose.Arity (i, n) → WType Rose.Arityih:∀ (a : Rose.Arity (i, n)), (ofRose (f a)).toRose = f a⊢ (ofRose (WType.mk (i, n) f)).toRose = WType.mk (i, n) f
change (ofRose (Rose.node i f)).toRose = Rose.node i f k:ℕr:Rose ki:Fin kn:ℕf:Rose.Arity (i, n) → WType Rose.Arityih:∀ (a : Rose.Arity (i, n)), (ofRose (f a)).toRose = f a⊢ (ofRose (Rose.node i f)).toRose = Rose.node i f
rw [ofRose_node, k:ℕr:Rose ki:Fin kn:ℕf:Rose.Arity (i, n) → WType Rose.Arityih:∀ (a : Rose.Arity (i, n)), (ofRose (f a)).toRose = f a⊢ (Fin.foldl (i, n).2 (fun acc j ↦ acc.fork (ofRose (f j))) (leaf i)).toRose = Rose.node i f toRose_foldl k:ℕr:Rose ki:Fin kn:ℕf:Rose.Arity (i, n) → WType Rose.Arityih:∀ (a : Rose.Arity (i, n)), (ofRose (f a)).toRose = f a⊢ (Rose.node i fun j ↦ (ofRose (f j)).toRose) = Rose.node i f] k:ℕr:Rose ki:Fin kn:ℕf:Rose.Arity (i, n) → WType Rose.Arityih:∀ (a : Rose.Arity (i, n)), (ofRose (f a)).toRose = f a⊢ (Rose.node i fun j ↦ (ofRose (f j)).toRose) = Rose.node i f
exact congrArg _ (funext ih) All goals completed! 🐙) rend AstLaw skeletons for a concrete syntax
section Lawsvariable {D : Type uA} {C : Type uB} (parse : C → Option D) (print : D → C)The document-level retraction law: the only law a syntax must prove.
def Retraction : Prop := ∀ d : D, parse (print d) = some dThe formatter, defined where parsing succeeds.
def format (c : C) : Option C := (parse c).map printFormatter idempotence, the first corollary of a retraction: reformatting formatted input changes nothing.
theorem format_idem (hr : Retraction parse print) (c : C) :
(format parse print c).bind (format parse print)
= format parse print c := by D:Type uAC:Type uBparse:C → Option Dprint:D → Chr:Retraction parse printc:C⊢ (format parse print c).bind (format parse print) = format parse print c
unfold format D:Type uAC:Type uBparse:C → Option Dprint:D → Chr:Retraction parse printc:C⊢ ((Option.map print (parse c)).bind fun c ↦ Option.map print (parse c)) = Option.map print (parse c)
cases h : parse c with
| none => none D:Type uAC:Type uBparse:C → Option Dprint:D → Chr:Retraction parse printc:Ch:parse c = none⊢ ((Option.map print none).bind fun c ↦ Option.map print (parse c)) = Option.map print none simp All goals completed! 🐙
| some d => some D:Type uAC:Type uBparse:C → Option Dprint:D → Chr:Retraction parse printc:Cd:Dh:parse c = some d⊢ ((Option.map print (some d)).bind fun c ↦ Option.map print (parse c)) = Option.map print (some d) simp [hr d] All goals completed! 🐙Injectivity of the printer, the second corollary of a retraction: a syntax that can be parsed back cannot spell two documents alike.
theorem print_injective (hr : Retraction parse print) :
Function.Injective print := by D:Type uAC:Type uBparse:C → Option Dprint:D → Chr:Retraction parse print⊢ Function.Injective print
intro d1 d2 h D:Type uAC:Type uBparse:C → Option Dprint:D → Chr:Retraction parse printd1:Dd2:Dh:print d1 = print d2⊢ d1 = d2
have h1 := hr d1 D:Type uAC:Type uBparse:C → Option Dprint:D → Chr:Retraction parse printd1:Dd2:Dh:print d1 = print d2h1:parse (print d1) = some d1⊢ d1 = d2
rw [h, D:Type uAC:Type uBparse:C → Option Dprint:D → Chr:Retraction parse printd1:Dd2:Dh:print d1 = print d2h1:parse (print d2) = some d1⊢ d1 = d2 hr d2 D:Type uAC:Type uBparse:C → Option Dprint:D → Chr:Retraction parse printd1:Dd2:Dh:print d1 = print d2h1:some d2 = some d1⊢ d1 = d2] at h1 D:Type uAC:Type uBparse:C → Option Dprint:D → Chr:Retraction parse printd1:Dd2:Dh:print d1 = print d2h1:some d2 = some d1⊢ d1 = d2
exact (Option.some.inj h1).symm All goals completed! 🐙end LawsA concrete syntax: RFC 9804 canonical S-expressions
namespace CsexpDecimal digits
The ASCII character for a decimal digit; meaningful for d < 10.
def digitChar (d : Nat) : Char := Char.ofNat (48 + d)The decimal value of an ASCII digit character.
def charDigit (c : Char) : Option Nat :=
if 48 ≤ c.toNat && c.toNat ≤ 57 then some (c.toNat - 48) else nonetheorem charDigit_digitChar (d : Nat) (h : d < 10) :
charDigit (digitChar d) = some d := by d:ℕh:d < 10⊢ charDigit (digitChar d) = some d
revert h d:ℕ⊢ d < 10 → charDigit (digitChar d) = some d; revert d ⊢ ∀ d < 10, charDigit (digitChar d) = some d; decide All goals completed! 🐙theorem mapM_charDigit_digitChar : ∀ ds : List Nat, (∀ d ∈ ds, d < 10) →
(ds.map digitChar).mapM charDigit = some ds :=
List.rec (motive := fun ds ↦ (∀ d ∈ ds, d < 10) →
(ds.map digitChar).mapM charDigit = some ds)
(fun _ ↦ rfl)
(fun d ds ih h ↦ by d:ℕds:List ℕih:(∀ d ∈ ds, d < 10) → List.mapM charDigit (List.map digitChar ds) = some dsh:∀ d_1 ∈ d :: ds, d_1 < 10⊢ List.mapM charDigit (List.map digitChar (d :: ds)) = some (d :: ds)
have hd : d < 10 := h d (by d:ℕds:List ℕih:(∀ d ∈ ds, d < 10) → List.mapM charDigit (List.map digitChar ds) = some dsh:∀ d_1 ∈ d :: ds, d_1 < 10⊢ d ∈ d :: ds simp All goals completed! 🐙) d:ℕds:List ℕih:(∀ d ∈ ds, d < 10) → List.mapM charDigit (List.map digitChar ds) = some dsh:∀ d_1 ∈ d :: ds, d_1 < 10hd:d < 10⊢ List.mapM charDigit (List.map digitChar (d :: ds)) = some (d :: ds)
have ht : ∀ x ∈ ds, x < 10 := fun x hx ↦ h x (by d:ℕds:List ℕih:(∀ d ∈ ds, d < 10) → List.mapM charDigit (List.map digitChar ds) = some dsh:∀ d_1 ∈ d :: ds, d_1 < 10hd:d < 10x:ℕhx:x ∈ ds⊢ x ∈ d :: ds simp [hx] All goals completed! 🐙) d:ℕds:List ℕih:(∀ d ∈ ds, d < 10) → List.mapM charDigit (List.map digitChar ds) = some dsh:∀ d_1 ∈ d :: ds, d_1 < 10hd:d < 10ht:∀ x ∈ ds, x < 10⊢ List.mapM charDigit (List.map digitChar (d :: ds)) = some (d :: ds)
simp [List.mapM_cons, charDigit_digitChar d hd, ih ht] All goals completed! 🐙)The value of a little-endian decimal digit list.
def ofLE : List Nat → Nat := List.rec 0 fun d _ ih ↦ d + 10 * ih@[simp] theorem ofLE_nil : ofLE [] = 0 := rfl@[simp] theorem ofLE_cons (d : Nat) (ds : List Nat) :
ofLE (d :: ds) = d + 10 * ofLE ds := rfl
Little-endian decimal digits of n, on an explicit recursion bound.
Each step divides by ten, so n itself is always a sufficient bound.
def digitsLEAux : Nat → Nat → List Nat :=
Nat.rec (fun _ ↦ []) fun _ ih n ↦ if n = 0 then [] else n % 10 :: ih (n / 10)@[simp] theorem digitsLEAux_zero (n : Nat) : digitsLEAux 0 n = [] := rfltheorem digitsLEAux_succ (f n : Nat) :
digitsLEAux (f + 1) n =
if n = 0 then [] else n % 10 :: digitsLEAux f (n / 10) := rfl
Little-endian decimal digits. Hand-rolled because every route to a
decimal round trip in mathlib and in core depends on Classical.choice;
see the module docstring's implementation notes.
def digitsLE (n : Nat) : List Nat := digitsLEAux n ntheorem ofLE_digitsLEAux : ∀ f n : Nat, n ≤ f → ofLE (digitsLEAux f n) = n :=
Nat.rec (motive := fun f ↦ ∀ n : Nat, n ≤ f → ofLE (digitsLEAux f n) = n)
(fun n hn ↦ by n:ℕhn:n ≤ Nat.zero⊢ ofLE (digitsLEAux Nat.zero n) = n
simp only [digitsLEAux_zero, ofLE_nil] n:ℕhn:n ≤ Nat.zero⊢ 0 = n
exact (Nat.le_zero.mp hn).symm All goals completed! 🐙)
(fun f ih n hn ↦ by f:ℕih:∀ n ≤ f, ofLE (digitsLEAux f n) = nn:ℕhn:n ≤ f.succ⊢ ofLE (digitsLEAux f.succ n) = n
rw [digitsLEAux_succ f:ℕih:∀ n ≤ f, ofLE (digitsLEAux f n) = nn:ℕhn:n ≤ f.succ⊢ ofLE (if n = 0 then [] else n % 10 :: digitsLEAux f (n / 10)) = n] f:ℕih:∀ n ≤ f, ofLE (digitsLEAux f n) = nn:ℕhn:n ≤ f.succ⊢ ofLE (if n = 0 then [] else n % 10 :: digitsLEAux f (n / 10)) = n
split isTrue f:ℕih:∀ n ≤ f, ofLE (digitsLEAux f n) = nn:ℕhn:n ≤ f.succh✝:n = 0⊢ ofLE [] = nisFalse f:ℕih:∀ n ≤ f, ofLE (digitsLEAux f n) = nn:ℕhn:n ≤ f.succh✝:¬n = 0⊢ ofLE (n % 10 :: digitsLEAux f (n / 10)) = n
next h => f:ℕih:∀ n ≤ f, ofLE (digitsLEAux f n) = nn:ℕhn:n ≤ f.succh:n = 0⊢ ofLE [] = n simp [h] All goals completed! 🐙
next h => f:ℕih:∀ n ≤ f, ofLE (digitsLEAux f n) = nn:ℕhn:n ≤ f.succh:¬n = 0⊢ ofLE (n % 10 :: digitsLEAux f (n / 10)) = n rw [ofLE_cons, f:ℕih:∀ n ≤ f, ofLE (digitsLEAux f n) = nn:ℕhn:n ≤ f.succh:¬n = 0⊢ n % 10 + 10 * ofLE (digitsLEAux f (n / 10)) = n ih (n / 10) (by f:ℕih:∀ n ≤ f, ofLE (digitsLEAux f n) = nn:ℕhn:n ≤ f.succh:¬n = 0⊢ n / 10 ≤ f omega All goals completed! 🐙)] f:ℕih:∀ n ≤ f, ofLE (digitsLEAux f n) = nn:ℕhn:n ≤ f.succh:¬n = 0⊢ n % 10 + 10 * (n / 10) = n; omega All goals completed! 🐙)theorem ofLE_digitsLE (n : Nat) : ofLE (digitsLE n) = n :=
ofLE_digitsLEAux n n (Nat.le_refl n)theorem digitsLEAux_lt : ∀ f n : Nat, ∀ d ∈ digitsLEAux f n, d < 10 :=
Nat.rec (motive := fun f ↦ ∀ n : Nat, ∀ d ∈ digitsLEAux f n, d < 10)
(fun n d hd ↦ by n:ℕd:ℕhd:d ∈ digitsLEAux Nat.zero n⊢ d < 10 simp at hd All goals completed! 🐙)
(fun f ih n d hd ↦ by f:ℕih:∀ (n d : ℕ), d ∈ digitsLEAux f n → d < 10n:ℕd:ℕhd:d ∈ digitsLEAux f.succ n⊢ d < 10
rw [digitsLEAux_succ f:ℕih:∀ (n d : ℕ), d ∈ digitsLEAux f n → d < 10n:ℕd:ℕhd:d ∈ if n = 0 then [] else n % 10 :: digitsLEAux f (n / 10)⊢ d < 10] at hd f:ℕih:∀ (n d : ℕ), d ∈ digitsLEAux f n → d < 10n:ℕd:ℕhd:d ∈ if n = 0 then [] else n % 10 :: digitsLEAux f (n / 10)⊢ d < 10
split at hd isTrue f:ℕih:∀ (n d : ℕ), d ∈ digitsLEAux f n → d < 10n:ℕd:ℕh✝:n = 0hd:d ∈ []⊢ d < 10isFalse f:ℕih:∀ (n d : ℕ), d ∈ digitsLEAux f n → d < 10n:ℕd:ℕh✝:¬n = 0hd:d ∈ n % 10 :: digitsLEAux f (n / 10)⊢ d < 10
next => f:ℕih:∀ (n d : ℕ), d ∈ digitsLEAux f n → d < 10n:ℕd:ℕh✝:n = 0hd:d ∈ []⊢ d < 10 simp at hd All goals completed! 🐙
next => f:ℕih:∀ (n d : ℕ), d ∈ digitsLEAux f n → d < 10n:ℕd:ℕh✝:¬n = 0hd:d ∈ n % 10 :: digitsLEAux f (n / 10)⊢ d < 10
rcases List.mem_cons.mp hd with rfl | hd' inl f:ℕih:∀ (n d : ℕ), d ∈ digitsLEAux f n → d < 10n:ℕh✝:¬n = 0hd:n % 10 ∈ n % 10 :: digitsLEAux f (n / 10)⊢ n % 10 < 10inr f:ℕih:∀ (n d : ℕ), d ∈ digitsLEAux f n → d < 10n:ℕd:ℕh✝:¬n = 0hd:d ∈ n % 10 :: digitsLEAux f (n / 10)hd':d ∈ digitsLEAux f (n / 10)⊢ d < 10
· inl f:ℕih:∀ (n d : ℕ), d ∈ digitsLEAux f n → d < 10n:ℕh✝:¬n = 0hd:n % 10 ∈ n % 10 :: digitsLEAux f (n / 10)⊢ n % 10 < 10 omega All goals completed! 🐙
· inr f:ℕih:∀ (n d : ℕ), d ∈ digitsLEAux f n → d < 10n:ℕd:ℕh✝:¬n = 0hd:d ∈ n % 10 :: digitsLEAux f (n / 10)hd':d ∈ digitsLEAux f (n / 10)⊢ d < 10 exact ih (n / 10) d hd' All goals completed! 🐙)theorem digitsLE_lt (n : Nat) : ∀ d ∈ digitsLE n, d < 10 := digitsLEAux_lt n ntheorem digitsLE_ne_nil {n : Nat} (h : n ≠ 0) : digitsLE n ≠ [] := by n:ℕh:n ≠ 0⊢ digitsLE n ≠ []
obtain ⟨m, rfl⟩ : ∃ m, n = m + 1 := ⟨n - 1, by n:ℕh:n ≠ 0⊢ n = n - 1 + 1 omega All goals completed! 🐙⟩ m:ℕh:m + 1 ≠ 0⊢ digitsLE (m + 1) ≠ []
simp [digitsLE, digitsLEAux_succ] All goals completed! 🐙
Shortest-form decimal, big-endian, "0" for zero.
The value of a big-endian decimal digit string, or none if any
character is not a digit. Leading zeros are accepted, and so is the empty
string, whose value is 0: the parser may admit more than the printer
emits, since the retraction law constrains only the composite.
The decimal round trip: reading back a shortest-form spelling recovers the number. This is what the retraction law rests on at the label level.
theorem digitsVal_decOf (n : Nat) : digitsVal (decOf n) = some n := by n:ℕ⊢ digitsVal (decOf n) = some n
unfold decOf digitsVal n:ℕ⊢ Option.map (fun l ↦ ofLE l.reverse)
(List.mapM charDigit (if n = 0 then ['0'] else List.map digitChar (digitsLE n).reverse)) =
some n
by_cases h : n = 0 pos n:ℕh:n = 0⊢ Option.map (fun l ↦ ofLE l.reverse)
(List.mapM charDigit (if n = 0 then ['0'] else List.map digitChar (digitsLE n).reverse)) =
some nneg n:ℕh:¬n = 0⊢ Option.map (fun l ↦ ofLE l.reverse)
(List.mapM charDigit (if n = 0 then ['0'] else List.map digitChar (digitsLE n).reverse)) =
some n
· pos n:ℕh:n = 0⊢ Option.map (fun l ↦ ofLE l.reverse)
(List.mapM charDigit (if n = 0 then ['0'] else List.map digitChar (digitsLE n).reverse)) =
some n subst h pos ⊢ Option.map (fun l ↦ ofLE l.reverse)
(List.mapM charDigit (if 0 = 0 then ['0'] else List.map digitChar (digitsLE 0).reverse)) =
some 0; decide All goals completed! 🐙
· neg n:ℕh:¬n = 0⊢ Option.map (fun l ↦ ofLE l.reverse)
(List.mapM charDigit (if n = 0 then ['0'] else List.map digitChar (digitsLE n).reverse)) =
some n have hlt : ∀ d ∈ (digitsLE n).reverse, d < 10 := by n:ℕ⊢ digitsVal (decOf n) = some n
intro d hd n:ℕh:¬n = 0d:ℕhd:d ∈ (digitsLE n).reverse⊢ d < 10
exact digitsLE_lt n d (List.mem_reverse.mp hd) neg n:ℕh:¬n = 0hlt:∀ d ∈ (digitsLE n).reverse, d < 10⊢ Option.map (fun l ↦ ofLE l.reverse)
(List.mapM charDigit (if n = 0 then ['0'] else List.map digitChar (digitsLE n).reverse)) =
some n
rw [ite_eq_right h, neg n:ℕh:¬n = 0hlt:∀ d ∈ (digitsLE n).reverse, d < 10⊢ Option.map (fun l ↦ ofLE l.reverse) (List.mapM charDigit (List.map digitChar (digitsLE n).reverse)) = some n mapM_charDigit_digitChar _ hlt neg n:ℕh:¬n = 0hlt:∀ d ∈ (digitsLE n).reverse, d < 10⊢ Option.map (fun l ↦ ofLE l.reverse) (some (digitsLE n).reverse) = some n] neg n:ℕh:¬n = 0hlt:∀ d ∈ (digitsLE n).reverse, d < 10⊢ Option.map (fun l ↦ ofLE l.reverse) (some (digitsLE n).reverse) = some n
simp [ofLE_digitsLE] All goals completed! 🐙theorem decOf_all_digits (n : Nat) : ∀ c ∈ decOf n, (charDigit c).isSome := by n:ℕ⊢ ∀ c ∈ decOf n, (charDigit c).isSome = true
intro c hc n:ℕc:Charhc:c ∈ decOf n⊢ (charDigit c).isSome = true
unfold decOf at hc n:ℕc:Charhc:c ∈ if n = 0 then ['0'] else List.map digitChar (digitsLE n).reverse⊢ (charDigit c).isSome = true
by_cases h : n = 0 pos n:ℕc:Charhc:c ∈ if n = 0 then ['0'] else List.map digitChar (digitsLE n).reverseh:n = 0⊢ (charDigit c).isSome = trueneg n:ℕc:Charhc:c ∈ if n = 0 then ['0'] else List.map digitChar (digitsLE n).reverseh:¬n = 0⊢ (charDigit c).isSome = true
· pos n:ℕc:Charhc:c ∈ if n = 0 then ['0'] else List.map digitChar (digitsLE n).reverseh:n = 0⊢ (charDigit c).isSome = true subst h pos c:Charhc:c ∈ if 0 = 0 then ['0'] else List.map digitChar (digitsLE 0).reverse⊢ (charDigit c).isSome = true; rw [ite_eq_left rfl, pos c:Charhc:c ∈ ['0']⊢ (charDigit c).isSome = true List.mem_singleton pos c:Charhc:c = '0'⊢ (charDigit c).isSome = true] at hc pos c:Charhc:c = '0'⊢ (charDigit c).isSome = true; subst hc pos ⊢ (charDigit '0').isSome = true; decide All goals completed! 🐙
· neg n:ℕc:Charhc:c ∈ if n = 0 then ['0'] else List.map digitChar (digitsLE n).reverseh:¬n = 0⊢ (charDigit c).isSome = true rw [ite_eq_right h neg n:ℕc:Charhc:c ∈ List.map digitChar (digitsLE n).reverseh:¬n = 0⊢ (charDigit c).isSome = true] at hc neg n:ℕc:Charhc:c ∈ List.map digitChar (digitsLE n).reverseh:¬n = 0⊢ (charDigit c).isSome = true
obtain ⟨d, hd, rfl⟩ := List.mem_map.mp hc neg n:ℕh:¬n = 0d:ℕhd:d ∈ (digitsLE n).reversehc:digitChar d ∈ List.map digitChar (digitsLE n).reverse⊢ (charDigit (digitChar d)).isSome = true
have : d < 10 := digitsLE_lt n d (List.mem_reverse.mp hd) neg n:ℕh:¬n = 0d:ℕhd:d ∈ (digitsLE n).reversehc:digitChar d ∈ List.map digitChar (digitsLE n).reversethis:d < 10⊢ (charDigit (digitChar d)).isSome = true
simp [charDigit_digitChar d this] All goals completed! 🐙theorem decOf_ne_nil (n : Nat) : decOf n ≠ [] := by n:ℕ⊢ decOf n ≠ []
unfold decOf n:ℕ⊢ (if n = 0 then ['0'] else List.map digitChar (digitsLE n).reverse) ≠ []
by_cases h : n = 0 pos n:ℕh:n = 0⊢ (if n = 0 then ['0'] else List.map digitChar (digitsLE n).reverse) ≠ []neg n:ℕh:¬n = 0⊢ (if n = 0 then ['0'] else List.map digitChar (digitsLE n).reverse) ≠ []
· pos n:ℕh:n = 0⊢ (if n = 0 then ['0'] else List.map digitChar (digitsLE n).reverse) ≠ [] subst h pos ⊢ (if 0 = 0 then ['0'] else List.map digitChar (digitsLE 0).reverse) ≠ []; simp All goals completed! 🐙
· neg n:ℕh:¬n = 0⊢ (if n = 0 then ['0'] else List.map digitChar (digitsLE n).reverse) ≠ [] rw [ite_eq_right h neg n:ℕh:¬n = 0⊢ List.map digitChar (digitsLE n).reverse ≠ []] neg n:ℕh:¬n = 0⊢ List.map digitChar (digitsLE n).reverse ≠ []
simp [digitsLE_ne_nil h] All goals completed! 🐙Reading a decimal prefix
Split off the longest decimal prefix.
def readDigits : List Char → List Char × List Char :=
List.rec ([], []) fun c cs ih ↦
match charDigit c with
| some _ => (c :: ih.1, ih.2)
| none => ([], c :: cs)theorem readDigits_cons (c : Char) (cs : List Char) :
readDigits (c :: cs) =
match charDigit c with
| some _ => (c :: (readDigits cs).1, (readDigits cs).2)
| none => ([], c :: cs) := rfltheorem readDigits_append : ∀ ds rest : List Char,
(∀ c ∈ ds, (charDigit c).isSome) →
(∀ c cs, rest = c :: cs → charDigit c = none) →
readDigits (ds ++ rest) = (ds, rest) :=
List.rec (motive := fun ds ↦ ∀ rest : List Char,
(∀ c ∈ ds, (charDigit c).isSome) →
(∀ c cs, rest = c :: cs → charDigit c = none) →
readDigits (ds ++ rest) = (ds, rest))
(fun rest _ hr ↦ by rest:List Charx✝:∀ c ∈ [], (charDigit c).isSome = truehr:∀ (c : Char) (cs : List Char), rest = c :: cs → charDigit c = none⊢ readDigits ([] ++ rest) = ([], rest)
cases rest with
| nil => nil x✝:∀ c ∈ [], (charDigit c).isSome = truehr:∀ (c : Char) (cs : List Char), [] = c :: cs → charDigit c = none⊢ readDigits ([] ++ []) = ([], []) rfl All goals completed! 🐙
| cons c cs => cons x✝:∀ c ∈ [], (charDigit c).isSome = truec:Charcs:List Charhr:∀ (c_1 : Char) (cs_1 : List Char), c :: cs = c_1 :: cs_1 → charDigit c_1 = none⊢ readDigits ([] ++ c :: cs) = ([], c :: cs) simp [readDigits_cons, hr c cs rfl] All goals completed! 🐙)
(fun d ds ih rest hd hr ↦ by d:Chards:List Charih:∀ (rest : List Char),
(∀ c ∈ ds, (charDigit c).isSome = true) →
(∀ (c : Char) (cs : List Char), rest = c :: cs → charDigit c = none) → readDigits (ds ++ rest) = (ds, rest)rest:List Charhd:∀ c ∈ d :: ds, (charDigit c).isSome = truehr:∀ (c : Char) (cs : List Char), rest = c :: cs → charDigit c = none⊢ readDigits (d :: ds ++ rest) = (d :: ds, rest)
have h1 : (charDigit d).isSome := hd d (by d:Chards:List Charih:∀ (rest : List Char),
(∀ c ∈ ds, (charDigit c).isSome = true) →
(∀ (c : Char) (cs : List Char), rest = c :: cs → charDigit c = none) → readDigits (ds ++ rest) = (ds, rest)rest:List Charhd:∀ c ∈ d :: ds, (charDigit c).isSome = truehr:∀ (c : Char) (cs : List Char), rest = c :: cs → charDigit c = none⊢ d ∈ d :: ds simp All goals completed! 🐙) d:Chards:List Charih:∀ (rest : List Char),
(∀ c ∈ ds, (charDigit c).isSome = true) →
(∀ (c : Char) (cs : List Char), rest = c :: cs → charDigit c = none) → readDigits (ds ++ rest) = (ds, rest)rest:List Charhd:∀ c ∈ d :: ds, (charDigit c).isSome = truehr:∀ (c : Char) (cs : List Char), rest = c :: cs → charDigit c = noneh1:(charDigit d).isSome = true⊢ readDigits (d :: ds ++ rest) = (d :: ds, rest)
have h2 : ∀ c ∈ ds, (charDigit c).isSome := fun c hc ↦ hd c (by d:Chards:List Charih:∀ (rest : List Char),
(∀ c ∈ ds, (charDigit c).isSome = true) →
(∀ (c : Char) (cs : List Char), rest = c :: cs → charDigit c = none) → readDigits (ds ++ rest) = (ds, rest)rest:List Charhd:∀ c ∈ d :: ds, (charDigit c).isSome = truehr:∀ (c : Char) (cs : List Char), rest = c :: cs → charDigit c = noneh1:(charDigit d).isSome = truec:Charhc:c ∈ ds⊢ c ∈ d :: ds simp [hc] All goals completed! 🐙) d:Chards:List Charih:∀ (rest : List Char),
(∀ c ∈ ds, (charDigit c).isSome = true) →
(∀ (c : Char) (cs : List Char), rest = c :: cs → charDigit c = none) → readDigits (ds ++ rest) = (ds, rest)rest:List Charhd:∀ c ∈ d :: ds, (charDigit c).isSome = truehr:∀ (c : Char) (cs : List Char), rest = c :: cs → charDigit c = noneh1:(charDigit d).isSome = trueh2:∀ c ∈ ds, (charDigit c).isSome = true⊢ readDigits (d :: ds ++ rest) = (d :: ds, rest)
obtain ⟨v, hv⟩ := Option.isSome_iff_exists.mp h1 d:Chards:List Charih:∀ (rest : List Char),
(∀ c ∈ ds, (charDigit c).isSome = true) →
(∀ (c : Char) (cs : List Char), rest = c :: cs → charDigit c = none) → readDigits (ds ++ rest) = (ds, rest)rest:List Charhd:∀ c ∈ d :: ds, (charDigit c).isSome = truehr:∀ (c : Char) (cs : List Char), rest = c :: cs → charDigit c = noneh1:(charDigit d).isSome = trueh2:∀ c ∈ ds, (charDigit c).isSome = truev:ℕhv:charDigit d = some v⊢ readDigits (d :: ds ++ rest) = (d :: ds, rest)
simp [readDigits_cons, hv, ih rest h2 hr] All goals completed! 🐙)Read a non-empty decimal prefix and its value.
def readNat (cs : List Char) : Option (Nat × List Char) :=
let (ds, r) := readDigits cs
if ds.isEmpty then none else (digitsVal ds).map (·, r)Canonical verbatim atoms
The [RFC9804] verbatim encoding of an atom, length ":" content.
def printVerbatim (s : List Char) : List Char :=
decOf s.length ++ ':' :: s
Read one verbatim atom, returning it and the remaining input. The
n ≤ r.length guard rejects an atom declaring more content than
follows it, rather than truncating. parse cannot observe the guard —
a truncating read would consume the whole remaining input, and every
position that can follow one demands more — so what the guard buys is
that readVerbatim reads the [RFC9804] verbatim production correctly
on its own. GebTests.Prototypes.ConcreteSyntax asserts that directly.
def readVerbatim (cs : List Char) : Option (List Char × List Char) :=
match readNat cs with
| some (n, ':' :: r) => if n ≤ r.length then some (r.take n, r.drop n) else none
| _ => none
A printed atom reads back whole, and leaves exactly what followed
it. The length prefix is what delimits the content, so the content may
contain : and parentheses.
theorem readVerbatim_append (s rest : List Char) :
readVerbatim (printVerbatim s ++ rest) = some (s, rest) := by s:List Charrest:List Char⊢ readVerbatim (printVerbatim s ++ rest) = some (s, rest)
have hcolon : ∀ c cs, (':' :: (s ++ rest)) = c :: cs → charDigit c = none := by
intro c cs hc s:List Charrest:List Charc:Charcs:List Charhc:':' :: (s ++ rest) = c :: cs⊢ charDigit c = none; injection hc with h1 _ s:List Charrest:List Charc:Charcs:List Charh1:':' = ctail_eq✝:s ++ rest = cs⊢ charDigit c = none; subst h1 s:List Charrest:List Charcs:List Chartail_eq✝:s ++ rest = cs⊢ charDigit ':' = none; decide s:List Charrest:List Charhcolon:∀ (c : Char) (cs : List Char), ':' :: (s ++ rest) = c :: cs → charDigit c = none⊢ readVerbatim (printVerbatim s ++ rest) = some (s, rest)
have h : readNat (printVerbatim s ++ rest)
= some (s.length, ':' :: (s ++ rest)) := by
unfold readNat printVerbatim s:List Charrest:List Charhcolon:∀ (c : Char) (cs : List Char), ':' :: (s ++ rest) = c :: cs → charDigit c = none⊢ (match readDigits (decOf s.length ++ ':' :: s ++ rest) with
| (ds, r) => if ds.isEmpty = true then none else Option.map (fun x ↦ (x, r)) (digitsVal ds)) =
some (s.length, ':' :: (s ++ rest))
rw [List.append_assoc, s:List Charrest:List Charhcolon:∀ (c : Char) (cs : List Char), ':' :: (s ++ rest) = c :: cs → charDigit c = none⊢ (match readDigits (decOf s.length ++ (':' :: s ++ rest)) with
| (ds, r) => if ds.isEmpty = true then none else Option.map (fun x ↦ (x, r)) (digitsVal ds)) =
some (s.length, ':' :: (s ++ rest)) List.cons_append, s:List Charrest:List Charhcolon:∀ (c : Char) (cs : List Char), ':' :: (s ++ rest) = c :: cs → charDigit c = none⊢ (match readDigits (decOf s.length ++ ':' :: (s ++ rest)) with
| (ds, r) => if ds.isEmpty = true then none else Option.map (fun x ↦ (x, r)) (digitsVal ds)) =
some (s.length, ':' :: (s ++ rest))
readDigits_append _ _ (decOf_all_digits s.length) hcolon s:List Charrest:List Charhcolon:∀ (c : Char) (cs : List Char), ':' :: (s ++ rest) = c :: cs → charDigit c = none⊢ (match (decOf s.length, ':' :: (s ++ rest)) with
| (ds, r) => if ds.isEmpty = true then none else Option.map (fun x ↦ (x, r)) (digitsVal ds)) =
some (s.length, ':' :: (s ++ rest))] s:List Charrest:List Charhcolon:∀ (c : Char) (cs : List Char), ':' :: (s ++ rest) = c :: cs → charDigit c = none⊢ (match (decOf s.length, ':' :: (s ++ rest)) with
| (ds, r) => if ds.isEmpty = true then none else Option.map (fun x ↦ (x, r)) (digitsVal ds)) =
some (s.length, ':' :: (s ++ rest))
simp [decOf_ne_nil s.length, digitsVal_decOf, List.isEmpty_iff] s:List Charrest:List Charhcolon:∀ (c : Char) (cs : List Char), ':' :: (s ++ rest) = c :: cs → charDigit c = noneh:readNat (printVerbatim s ++ rest) = some (s.length, ':' :: (s ++ rest))⊢ readVerbatim (printVerbatim s ++ rest) = some (s, rest)
simp [readVerbatim, h] All goals completed! 🐙Printer
The head atom of a leaf s-expression.
def leafTok : List Char := ['l', 'e', 'a', 'f']The head atom of a fork s-expression.
def forkTok : List Char := ['f', 'o', 'r', 'k']Print a tree in [RFC9804] canonical form. A leaf label is its shortest-form decimal, written as a verbatim atom: canonical form admits no other atom encoding.
def printAst {k : Nat} : Ast k → List Char :=
WType.elim (List Char) fun x ↦
match x with
| ⟨.leaf i, _⟩ =>
'(' :: (printVerbatim leafTok ++ printVerbatim (decOf i.val) ++ [')'])
| ⟨.fork, ch⟩ =>
'(' :: (printVerbatim forkTok ++ ch (0 : Fin 2) ++ ch (1 : Fin 2) ++ [')'])@[simp] theorem printAst_leaf {k : Nat} (i : Fin k) :
printAst (Ast.leaf i)
= '(' :: (printVerbatim leafTok ++ printVerbatim (decOf i.val) ++ [')']) :=
rfl@[simp] theorem printAst_fork {k : Nat} (l r : Ast k) :
printAst (Ast.fork l r)
= '(' :: (printVerbatim forkTok ++ printAst l ++ printAst r ++ [')']) :=
rflParser
One layer of the recursive descent: read a single s-expression,
delegating each child to childParse.
def parseStep (k : Nat) (childParse : List Char → Option (Ast k × List Char)) :
List Char → Option (Ast k × List Char)
| [] => none
| c :: cs =>
if c = '(' then
match readVerbatim cs with
| some (tok, cs1) =>
if tok = leafTok then
match readVerbatim cs1 with
| some (ds, ')' :: cs2) =>
match digitsVal ds with
| some n => if h : n < k then some (Ast.leaf ⟨n, h⟩, cs2) else none
| none => none
| _ => none
else if tok = forkTok then
match childParse cs1 with
| some (l, cs2) =>
match childParse cs2 with
| some (r, ')' :: cs3) => some (Ast.fork l r, cs3)
| _ => none
| none => none
else none
| none => none
else none
Recursive descent over the canonical form, returning the tree and the
unconsumed input. The Nat argument bounds the recursion: the recursion
is on the input's structure only through the remainder each call returns,
which is not a form Lean's structural recursion accepts. parse supplies
the input length; size_le_length_printAst shows that this bound admits every
tree the printer emits. Whether it admits every input the grammar accepts
is not stated, and is not needed for the retraction law.
def parseAst (k : Nat) : Nat → List Char → Option (Ast k × List Char) :=
Nat.rec (fun _ ↦ none) fun _ ih ↦ parseStep k ihThe parser inverts the printer on printed input, given fuel at least the tree's node count, and returns the unconsumed remainder.
theorem parseAst_printAst {k : Nat} (a : Ast k) :
∀ (f : Nat) (rest : List Char), a.size ≤ f →
parseAst k f (printAst a ++ rest) = some (a, rest) :=
Ast.ind (motive := fun a ↦ ∀ (f : Nat) (rest : List Char), a.size ≤ f →
parseAst k f (printAst a ++ rest) = some (a, rest))
(fun i f rest hf ↦ by k:ℕa:Ast ki:Fin kf:ℕrest:List Charhf:(Ast.leaf i).size ≤ f⊢ parseAst k f (printAst (Ast.leaf i) ++ rest) = some (Ast.leaf i, rest)
cases f with
| zero => zero k:ℕa:Ast ki:Fin krest:List Charhf:(Ast.leaf i).size ≤ 0⊢ parseAst k 0 (printAst (Ast.leaf i) ++ rest) = some (Ast.leaf i, rest) simp at hf All goals completed! 🐙
| succ f => succ k:ℕa:Ast ki:Fin krest:List Charf:ℕhf:(Ast.leaf i).size ≤ f + 1⊢ parseAst k (f + 1) (printAst (Ast.leaf i) ++ rest) = some (Ast.leaf i, rest)
simp only [printAst_leaf, List.cons_append, parseAst_succ, parseStep,
List.append_assoc, List.nil_append] succ k:ℕa:Ast ki:Fin krest:List Charf:ℕhf:(Ast.leaf i).size ≤ f + 1⊢ (if True then
match readVerbatim (printVerbatim leafTok ++ (printVerbatim (decOf ↑i) ++ ')' :: rest)) with
| some (tok, cs1) =>
if tok = leafTok then
match readVerbatim cs1 with
| some (ds, ')' :: cs2) =>
match digitsVal ds with
| some n => if h : n < k then some (Ast.leaf ⟨n, h⟩, cs2) else none
| none => none
| x => none
else
if tok = forkTok then
match parseAst k f cs1 with
| some (l, cs2) =>
match parseAst k f cs2 with
| some (r, ')' :: cs3) => some (l.fork r, cs3)
| x => none
| none => none
else none
| none => none
else none) =
some (Ast.leaf i, rest)
rw [readVerbatim_append succ k:ℕa:Ast ki:Fin krest:List Charf:ℕhf:(Ast.leaf i).size ≤ f + 1⊢ (if True then
match some (leafTok, printVerbatim (decOf ↑i) ++ ')' :: rest) with
| some (tok, cs1) =>
if tok = leafTok then
match readVerbatim cs1 with
| some (ds, ')' :: cs2) =>
match digitsVal ds with
| some n => if h : n < k then some (Ast.leaf ⟨n, h⟩, cs2) else none
| none => none
| x => none
else
if tok = forkTok then
match parseAst k f cs1 with
| some (l, cs2) =>
match parseAst k f cs2 with
| some (r, ')' :: cs3) => some (l.fork r, cs3)
| x => none
| none => none
else none
| none => none
else none) =
some (Ast.leaf i, rest)] succ k:ℕa:Ast ki:Fin krest:List Charf:ℕhf:(Ast.leaf i).size ≤ f + 1⊢ (if True then
match some (leafTok, printVerbatim (decOf ↑i) ++ ')' :: rest) with
| some (tok, cs1) =>
if tok = leafTok then
match readVerbatim cs1 with
| some (ds, ')' :: cs2) =>
match digitsVal ds with
| some n => if h : n < k then some (Ast.leaf ⟨n, h⟩, cs2) else none
| none => none
| x => none
else
if tok = forkTok then
match parseAst k f cs1 with
| some (l, cs2) =>
match parseAst k f cs2 with
| some (r, ')' :: cs3) => some (l.fork r, cs3)
| x => none
| none => none
else none
| none => none
else none) =
some (Ast.leaf i, rest)
-- reduce the match on the `some` just produced, exposing the next atom
simp only [] succ k:ℕa:Ast ki:Fin krest:List Charf:ℕhf:(Ast.leaf i).size ≤ f + 1⊢ (if True then
if True then
match readVerbatim (printVerbatim (decOf ↑i) ++ ')' :: rest) with
| some (ds, ')' :: cs2) =>
match digitsVal ds with
| some n => if h : n < k then some (Ast.leaf ⟨n, h⟩, cs2) else none
| none => none
| x => none
else
if leafTok = forkTok then
match parseAst k f (printVerbatim (decOf ↑i) ++ ')' :: rest) with
| some (l, cs2) =>
match parseAst k f cs2 with
| some (r, ')' :: cs3) => some (l.fork r, cs3)
| x => none
| none => none
else none
else none) =
some (Ast.leaf i, rest)
rw [readVerbatim_append succ k:ℕa:Ast ki:Fin krest:List Charf:ℕhf:(Ast.leaf i).size ≤ f + 1⊢ (if True then
if True then
match some (decOf ↑i, ')' :: rest) with
| some (ds, ')' :: cs2) =>
match digitsVal ds with
| some n => if h : n < k then some (Ast.leaf ⟨n, h⟩, cs2) else none
| none => none
| x => none
else
if leafTok = forkTok then
match parseAst k f (printVerbatim (decOf ↑i) ++ ')' :: rest) with
| some (l, cs2) =>
match parseAst k f cs2 with
| some (r, ')' :: cs3) => some (l.fork r, cs3)
| x => none
| none => none
else none
else none) =
some (Ast.leaf i, rest)] succ k:ℕa:Ast ki:Fin krest:List Charf:ℕhf:(Ast.leaf i).size ≤ f + 1⊢ (if True then
if True then
match some (decOf ↑i, ')' :: rest) with
| some (ds, ')' :: cs2) =>
match digitsVal ds with
| some n => if h : n < k then some (Ast.leaf ⟨n, h⟩, cs2) else none
| none => none
| x => none
else
if leafTok = forkTok then
match parseAst k f (printVerbatim (decOf ↑i) ++ ')' :: rest) with
| some (l, cs2) =>
match parseAst k f cs2 with
| some (r, ')' :: cs3) => some (l.fork r, cs3)
| x => none
| none => none
else none
else none) =
some (Ast.leaf i, rest)
simp [digitsVal_decOf, i.isLt] All goals completed! 🐙)
(fun l r ihl ihr f rest hf ↦ by k:ℕa:Ast kl:Ast kr:Ast kihl:∀ (f : ℕ) (rest : List Char), l.size ≤ f → parseAst k f (printAst l ++ rest) = some (l, rest)ihr:∀ (f : ℕ) (rest : List Char), r.size ≤ f → parseAst k f (printAst r ++ rest) = some (r, rest)f:ℕrest:List Charhf:(l.fork r).size ≤ f⊢ parseAst k f (printAst (l.fork r) ++ rest) = some (l.fork r, rest)
cases f with
| zero => zero k:ℕa:Ast kl:Ast kr:Ast kihl:∀ (f : ℕ) (rest : List Char), l.size ≤ f → parseAst k f (printAst l ++ rest) = some (l, rest)ihr:∀ (f : ℕ) (rest : List Char), r.size ≤ f → parseAst k f (printAst r ++ rest) = some (r, rest)rest:List Charhf:(l.fork r).size ≤ 0⊢ parseAst k 0 (printAst (l.fork r) ++ rest) = some (l.fork r, rest) simp at hf All goals completed! 🐙
| succ f => succ k:ℕa:Ast kl:Ast kr:Ast kihl:∀ (f : ℕ) (rest : List Char), l.size ≤ f → parseAst k f (printAst l ++ rest) = some (l, rest)ihr:∀ (f : ℕ) (rest : List Char), r.size ≤ f → parseAst k f (printAst r ++ rest) = some (r, rest)rest:List Charf:ℕhf:(l.fork r).size ≤ f + 1⊢ parseAst k (f + 1) (printAst (l.fork r) ++ rest) = some (l.fork r, rest)
have hl : l.size ≤ f := by k:ℕa:Ast kl:Ast kr:Ast kihl:∀ (f : ℕ) (rest : List Char), l.size ≤ f → parseAst k f (printAst l ++ rest) = some (l, rest)ihr:∀ (f : ℕ) (rest : List Char), r.size ≤ f → parseAst k f (printAst r ++ rest) = some (r, rest)f:ℕrest:List Charhf:(l.fork r).size ≤ f⊢ parseAst k f (printAst (l.fork r) ++ rest) = some (l.fork r, rest) simp at hf k:ℕa:Ast kl:Ast kr:Ast kihl:∀ (f : ℕ) (rest : List Char), l.size ≤ f → parseAst k f (printAst l ++ rest) = some (l, rest)ihr:∀ (f : ℕ) (rest : List Char), r.size ≤ f → parseAst k f (printAst r ++ rest) = some (r, rest)rest:List Charf:ℕhf:1 + l.size + r.size ≤ f + 1⊢ l.size ≤ f; omega succ k:ℕa:Ast kl:Ast kr:Ast kihl:∀ (f : ℕ) (rest : List Char), l.size ≤ f → parseAst k f (printAst l ++ rest) = some (l, rest)ihr:∀ (f : ℕ) (rest : List Char), r.size ≤ f → parseAst k f (printAst r ++ rest) = some (r, rest)rest:List Charf:ℕhf:(l.fork r).size ≤ f + 1hl:l.size ≤ f⊢ parseAst k (f + 1) (printAst (l.fork r) ++ rest) = some (l.fork r, rest)
have hr : r.size ≤ f := by k:ℕa:Ast kl:Ast kr:Ast kihl:∀ (f : ℕ) (rest : List Char), l.size ≤ f → parseAst k f (printAst l ++ rest) = some (l, rest)ihr:∀ (f : ℕ) (rest : List Char), r.size ≤ f → parseAst k f (printAst r ++ rest) = some (r, rest)f:ℕrest:List Charhf:(l.fork r).size ≤ f⊢ parseAst k f (printAst (l.fork r) ++ rest) = some (l.fork r, rest) simp at hf k:ℕa:Ast kl:Ast kr:Ast kihl:∀ (f : ℕ) (rest : List Char), l.size ≤ f → parseAst k f (printAst l ++ rest) = some (l, rest)ihr:∀ (f : ℕ) (rest : List Char), r.size ≤ f → parseAst k f (printAst r ++ rest) = some (r, rest)rest:List Charf:ℕhf:1 + l.size + r.size ≤ f + 1hl:l.size ≤ f⊢ r.size ≤ f; omega succ k:ℕa:Ast kl:Ast kr:Ast kihl:∀ (f : ℕ) (rest : List Char), l.size ≤ f → parseAst k f (printAst l ++ rest) = some (l, rest)ihr:∀ (f : ℕ) (rest : List Char), r.size ≤ f → parseAst k f (printAst r ++ rest) = some (r, rest)rest:List Charf:ℕhf:(l.fork r).size ≤ f + 1hl:l.size ≤ fhr:r.size ≤ f⊢ parseAst k (f + 1) (printAst (l.fork r) ++ rest) = some (l.fork r, rest)
simp only [printAst_fork, List.cons_append, parseAst_succ, parseStep,
List.append_assoc, List.nil_append] succ k:ℕa:Ast kl:Ast kr:Ast kihl:∀ (f : ℕ) (rest : List Char), l.size ≤ f → parseAst k f (printAst l ++ rest) = some (l, rest)ihr:∀ (f : ℕ) (rest : List Char), r.size ≤ f → parseAst k f (printAst r ++ rest) = some (r, rest)rest:List Charf:ℕhf:(l.fork r).size ≤ f + 1hl:l.size ≤ fhr:r.size ≤ f⊢ (if True then
match readVerbatim (printVerbatim forkTok ++ (printAst l ++ (printAst r ++ ')' :: rest))) with
| some (tok, cs1) =>
if tok = leafTok then
match readVerbatim cs1 with
| some (ds, ')' :: cs2) =>
match digitsVal ds with
| some n => if h : n < k then some (Ast.leaf ⟨n, h⟩, cs2) else none
| none => none
| x => none
else
if tok = forkTok then
match parseAst k f cs1 with
| some (l, cs2) =>
match parseAst k f cs2 with
| some (r, ')' :: cs3) => some (l.fork r, cs3)
| x => none
| none => none
else none
| none => none
else none) =
some (l.fork r, rest)
rw [readVerbatim_append succ k:ℕa:Ast kl:Ast kr:Ast kihl:∀ (f : ℕ) (rest : List Char), l.size ≤ f → parseAst k f (printAst l ++ rest) = some (l, rest)ihr:∀ (f : ℕ) (rest : List Char), r.size ≤ f → parseAst k f (printAst r ++ rest) = some (r, rest)rest:List Charf:ℕhf:(l.fork r).size ≤ f + 1hl:l.size ≤ fhr:r.size ≤ f⊢ (if True then
match some (forkTok, printAst l ++ (printAst r ++ ')' :: rest)) with
| some (tok, cs1) =>
if tok = leafTok then
match readVerbatim cs1 with
| some (ds, ')' :: cs2) =>
match digitsVal ds with
| some n => if h : n < k then some (Ast.leaf ⟨n, h⟩, cs2) else none
| none => none
| x => none
else
if tok = forkTok then
match parseAst k f cs1 with
| some (l, cs2) =>
match parseAst k f cs2 with
| some (r, ')' :: cs3) => some (l.fork r, cs3)
| x => none
| none => none
else none
| none => none
else none) =
some (l.fork r, rest)] succ k:ℕa:Ast kl:Ast kr:Ast kihl:∀ (f : ℕ) (rest : List Char), l.size ≤ f → parseAst k f (printAst l ++ rest) = some (l, rest)ihr:∀ (f : ℕ) (rest : List Char), r.size ≤ f → parseAst k f (printAst r ++ rest) = some (r, rest)rest:List Charf:ℕhf:(l.fork r).size ≤ f + 1hl:l.size ≤ fhr:r.size ≤ f⊢ (if True then
match some (forkTok, printAst l ++ (printAst r ++ ')' :: rest)) with
| some (tok, cs1) =>
if tok = leafTok then
match readVerbatim cs1 with
| some (ds, ')' :: cs2) =>
match digitsVal ds with
| some n => if h : n < k then some (Ast.leaf ⟨n, h⟩, cs2) else none
| none => none
| x => none
else
if tok = forkTok then
match parseAst k f cs1 with
| some (l, cs2) =>
match parseAst k f cs2 with
| some (r, ')' :: cs3) => some (l.fork r, cs3)
| x => none
| none => none
else none
| none => none
else none) =
some (l.fork r, rest)
have hne : forkTok ≠ leafTok := by k:ℕa:Ast kl:Ast kr:Ast kihl:∀ (f : ℕ) (rest : List Char), l.size ≤ f → parseAst k f (printAst l ++ rest) = some (l, rest)ihr:∀ (f : ℕ) (rest : List Char), r.size ≤ f → parseAst k f (printAst r ++ rest) = some (r, rest)f:ℕrest:List Charhf:(l.fork r).size ≤ f⊢ parseAst k f (printAst (l.fork r) ++ rest) = some (l.fork r, rest) decide succ k:ℕa:Ast kl:Ast kr:Ast kihl:∀ (f : ℕ) (rest : List Char), l.size ≤ f → parseAst k f (printAst l ++ rest) = some (l, rest)ihr:∀ (f : ℕ) (rest : List Char), r.size ≤ f → parseAst k f (printAst r ++ rest) = some (r, rest)rest:List Charf:ℕhf:(l.fork r).size ≤ f + 1hl:l.size ≤ fhr:r.size ≤ fhne:forkTok ≠ leafTok⊢ (if True then
match some (forkTok, printAst l ++ (printAst r ++ ')' :: rest)) with
| some (tok, cs1) =>
if tok = leafTok then
match readVerbatim cs1 with
| some (ds, ')' :: cs2) =>
match digitsVal ds with
| some n => if h : n < k then some (Ast.leaf ⟨n, h⟩, cs2) else none
| none => none
| x => none
else
if tok = forkTok then
match parseAst k f cs1 with
| some (l, cs2) =>
match parseAst k f cs2 with
| some (r, ')' :: cs3) => some (l.fork r, cs3)
| x => none
| none => none
else none
| none => none
else none) =
some (l.fork r, rest)
simp only [ite_eq_right hne] succ k:ℕa:Ast kl:Ast kr:Ast kihl:∀ (f : ℕ) (rest : List Char), l.size ≤ f → parseAst k f (printAst l ++ rest) = some (l, rest)ihr:∀ (f : ℕ) (rest : List Char), r.size ≤ f → parseAst k f (printAst r ++ rest) = some (r, rest)rest:List Charf:ℕhf:(l.fork r).size ≤ f + 1hl:l.size ≤ fhr:r.size ≤ fhne:forkTok ≠ leafTok⊢ (if True then
if True then
match parseAst k f (printAst l ++ (printAst r ++ ')' :: rest)) with
| some (l, cs2) =>
match parseAst k f cs2 with
| some (r, ')' :: cs3) => some (l.fork r, cs3)
| x => none
| none => none
else none
else none) =
some (l.fork r, rest)
rw [ihl f _ hl succ k:ℕa:Ast kl:Ast kr:Ast kihl:∀ (f : ℕ) (rest : List Char), l.size ≤ f → parseAst k f (printAst l ++ rest) = some (l, rest)ihr:∀ (f : ℕ) (rest : List Char), r.size ≤ f → parseAst k f (printAst r ++ rest) = some (r, rest)rest:List Charf:ℕhf:(l.fork r).size ≤ f + 1hl:l.size ≤ fhr:r.size ≤ fhne:forkTok ≠ leafTok⊢ (if True then
if True then
match some (l, printAst r ++ ')' :: rest) with
| some (l, cs2) =>
match parseAst k f cs2 with
| some (r, ')' :: cs3) => some (l.fork r, cs3)
| x => none
| none => none
else none
else none) =
some (l.fork r, rest)] succ k:ℕa:Ast kl:Ast kr:Ast kihl:∀ (f : ℕ) (rest : List Char), l.size ≤ f → parseAst k f (printAst l ++ rest) = some (l, rest)ihr:∀ (f : ℕ) (rest : List Char), r.size ≤ f → parseAst k f (printAst r ++ rest) = some (r, rest)rest:List Charf:ℕhf:(l.fork r).size ≤ f + 1hl:l.size ≤ fhr:r.size ≤ fhne:forkTok ≠ leafTok⊢ (if True then
if True then
match some (l, printAst r ++ ')' :: rest) with
| some (l, cs2) =>
match parseAst k f cs2 with
| some (r, ')' :: cs3) => some (l.fork r, cs3)
| x => none
| none => none
else none
else none) =
some (l.fork r, rest)
-- reduce the match on the `some` just produced, exposing the second child
simp only [] succ k:ℕa:Ast kl:Ast kr:Ast kihl:∀ (f : ℕ) (rest : List Char), l.size ≤ f → parseAst k f (printAst l ++ rest) = some (l, rest)ihr:∀ (f : ℕ) (rest : List Char), r.size ≤ f → parseAst k f (printAst r ++ rest) = some (r, rest)rest:List Charf:ℕhf:(l.fork r).size ≤ f + 1hl:l.size ≤ fhr:r.size ≤ fhne:forkTok ≠ leafTok⊢ (if True then
if True then
match parseAst k f (printAst r ++ ')' :: rest) with
| some (r, ')' :: cs3) => some (l.fork r, cs3)
| x => none
else none
else none) =
some (l.fork r, rest)
rw [ihr f _ hr succ k:ℕa:Ast kl:Ast kr:Ast kihl:∀ (f : ℕ) (rest : List Char), l.size ≤ f → parseAst k f (printAst l ++ rest) = some (l, rest)ihr:∀ (f : ℕ) (rest : List Char), r.size ≤ f → parseAst k f (printAst r ++ rest) = some (r, rest)rest:List Charf:ℕhf:(l.fork r).size ≤ f + 1hl:l.size ≤ fhr:r.size ≤ fhne:forkTok ≠ leafTok⊢ (if True then
if True then
match some (r, ')' :: rest) with
| some (r, ')' :: cs3) => some (l.fork r, cs3)
| x => none
else none
else none) =
some (l.fork r, rest)] succ k:ℕa:Ast kl:Ast kr:Ast kihl:∀ (f : ℕ) (rest : List Char), l.size ≤ f → parseAst k f (printAst l ++ rest) = some (l, rest)ihr:∀ (f : ℕ) (rest : List Char), r.size ≤ f → parseAst k f (printAst r ++ rest) = some (r, rest)rest:List Charf:ℕhf:(l.fork r).size ≤ f + 1hl:l.size ≤ fhr:r.size ≤ fhne:forkTok ≠ leafTok⊢ (if True then
if True then
match some (r, ')' :: rest) with
| some (r, ')' :: cs3) => some (l.fork r, cs3)
| x => none
else none
else none) =
some (l.fork r, rest)
simp All goals completed! 🐙) a
A tree's node count bounds the length of its printed form, so the
input length is fuel enough for parseAst to read anything printAst
emits.
theorem size_le_length_printAst {k : Nat} (a : Ast k) : a.size ≤ (printAst a).length :=
Ast.ind (motive := fun a ↦ a.size ≤ (printAst a).length)
(fun i ↦ by k:ℕa:Ast ki:Fin k⊢ (Ast.leaf i).size ≤ (printAst (Ast.leaf i)).length simp [printAst_leaf] All goals completed! 🐙)
(fun l r ihl ihr ↦ by k:ℕa:Ast kl:Ast kr:Ast kihl:l.size ≤ (printAst l).lengthihr:r.size ≤ (printAst r).length⊢ (l.fork r).size ≤ (printAst (l.fork r)).length
simp only [Ast.size_fork, printAst_fork, List.length_cons,
List.length_append] k:ℕa:Ast kl:Ast kr:Ast kihl:l.size ≤ (printAst l).lengthihr:r.size ≤ (printAst r).length⊢ 1 + l.size + r.size ≤ (printVerbatim forkTok).length + (printAst l).length + (printAst r).length + ([].length + 1) + 1
omega All goals completed! 🐙) aThe two syntax maps and the retraction law
The printer of the canonical S-expression syntax. A concrete syntax needs a deterministic printer, not a normative canonical form; that this one also emits the format's canonical spelling is incidental rather than required.
The parser of the canonical S-expression syntax, rejecting trailing input.
def parse (k : Nat) (cs : List Char) : Option (Ast k) :=
match parseAst k cs.length cs with
| some (a, []) => some a
| _ => noneThe retraction law for the canonical S-expression syntax: printing a tree and parsing the result returns that tree.
theorem parse_print {k : Nat} (a : Ast k) : parse k (print a) = some a := by k:ℕa:Ast k⊢ parse k (print a) = some a
unfold parse print k:ℕa:Ast k⊢ (match parseAst k (printAst a).length (printAst a) with
| some (a, []) => some a
| x => none) =
some a
rw [show printAst a = printAst a ++ [] by k:ℕa:Ast k⊢ parse k (print a) = some a simp All goals completed! 🐙,
parseAst_printAst a _ [] (by k:ℕa:Ast k⊢ a.size ≤ (printAst a ++ []).length simpa using size_le_length_printAst a All goals completed! 🐙)] All goals completed! 🐙The generic corollaries, instantiated here
parse_print in the form the generic corollaries consume.
theorem retraction (k : Nat) : Retraction (parse k) (print (k := k)) :=
parse_print
Geb.format_idem at this syntax.
theorem format_idem (k : Nat) (c : List Char) :
(format (parse k) print c).bind (format (parse k) print)
= format (parse k) print c :=
Geb.format_idem _ _ (retraction k) c
Geb.print_injective at this syntax.
theorem print_injective (k : Nat) : Function.Injective (print (k := k)) :=
Geb.print_injective _ _ (retraction k)end Csexpend Geb