muro-lang.dev / docs
Data
In brief: a data declaration is a book entry. Binders before : are parameters. The telescope after : before Type is indices. Constructors must be strictly positive. Nat, Unit, Empty, and ν stay primitive.
A declaration
data Maybe (A : Type) : Type where
nothing : Maybe A
just : A → Maybe A
(lib/muro/std/maybe.muro. examples/maybe.muro imports it.)
- The type former is spec.
- Constructors compute in run and may appear in evidence.
-
There is no kernel constructor named after your type.
Maybe,List,Vec,Finall use the samedty/ctor/mDatarepresentation.
Nat, Unit, Empty, and Stream stay primitive. Do not redeclare them.
Parameters and indices
data Name params : index-telescope Type where
ctor : telescope
Parameters are the binders before :. Indices are the telescope after : and before Type.
A nat field may carry a numeral bound before the arrow: (slot : Nat) {slot ≤ 1}. The spelling is A bounded field. The session split is A numeral bound.
data List (A : Type) : Type where -- one parameter, no indices
nil : List A
cons : A → List A → List A
data Fin : Nat → Type where -- no parameters, one index
fzero : Π (n : Nat) → Fin suc(n)
fsuc : Π (n : Nat) → Fin n → Fin suc(n)
Vec has both; see Indexed data.
Match
One named branch per constructor. Explicit motive over the scrutinee (and its indices, when there are indices).
def fromMaybe : run Π (-A : Type) → Π (d : A) → Π (m : Maybe A) → A :=
λ (-A : Type) → λ (d : A) → λ (m : Maybe A) →
match m motive (λ _ → A)
| nothing => d
| just a => a
A is erased. Emit drops that argument. fromMaybe Nat 0 (just (suc 0)) converts to suc 0; fromJust1 is refl.
A branch binds each field at the quantity the constructor declares. The default is affine: a in just a is used at most once in run and evidence. A constructor may declare a field + when its type is copyable (Data, a function, or a pair of copyable types), and then every branch binds that field reusable:
data Seed : Type where
at : Π (+ s : State) → Π (+ n : Nat) → Seed
A match on a Seed may use s and n as often as it needs. The + on a field is checked like the + on a binder: Π (+ s : Stream Nat) → … is refused, and a + field of function type scales the uses of the argument that fills it, so box (addn n) with box : Π (+ f : Nat → Nat) → Box needs a reusable n. A match on Nat is the one case that reads the scrutinee instead: the predecessor of a reusable variable is reusable (Terms).
Positivity
A constructor field is strictly positive in D after the checker unfolds it. D may be absent. D may be the head of a spine whose arguments do not contain D. D may occur to the right of a Π whose domain does not contain D. A product is positive on both sides. D inside an argument of D is refused, including under a spec name: Contra := Bad → Empty is the negative arrow, and List (Tree A) is nested. Another data type is held to the same test: its parameters are instantiated and every one of its fields must be strictly positive in D. Wrap that stores a Bad, placed in a domain (Wrap → Empty), is refused. Tree and Forest, each storing the other as a field, are accepted. A field that does not reduce in the fuel is refused.
This is rejected:
data Bad : Type where
mk : Π (n : Nat) → (Bad n → Nat) → Bad n
(Bad is not even well-formed that way — the point is the negative occurrence.)
Fields are small
Every constructor field (a binder after the parameters) must be a small type: a term of type Type. Parameters are (A : Type) and are exempt. A field of type Type is rejected:
data Box : Type where
box : Π (A : Type) → Box
With match … motive (λ _ → Type) such a Box would project a type back out of a term of type Type, and Type would be a retract of Box. Parametrise instead: data Box (A : Type) : Type.
Recursion
A call in run, evidence, or spec, to the definition being checked or to another definition that reaches it, must pass a constructor argument whose type is D … from a match on the argument the block descends on. The block shares one argument position. even and odd on Nat are one block (examples/even_odd.muro). For lists, that position is the tail. For trees, either child. Fields of a match on a computed value are not smaller.
def length : run Π (- A : Type) → Π (xs : List A) → Nat :=
λ (- A : Type) → λ (xs : List A) →
match xs motive (λ _ → Nat)
| nil => 0
| cons _ as => suc (length A as)
(lib/muro/std/list.muro. examples/list.muro imports it. [] is nil; :: is cons.)
def size : run Π (t : Tree) → Nat :=
λ (t : Tree) →
match t motive (λ _ → Nat)
| leaf => 0
| node l r => suc (plus (size l) (size r))
(examples/tree.muro. Both children are smaller.)
When is a data type Data?
+ is allowed only if the type WHNFs to a copyable type: Data, a function, or a pair of copyable types (Binders and quantities). For a user type to be Data, every parameter must be Data. Indices are not asked.
-
List Natis Data.+xs : List Natmay be reused. -
List (Nat → Nat)is not. -
Maybe Unitis Data. -
Vec A nis Data iffAis. The indexnis Nat; it does not disqualify the type.
Emit dialect
A 0-argument constructor is an atom: :nil, :nothing, :leaf.
Otherwise a tagged tuple: {:cons, a, as}, {:just, a}, {:node, l, r}.
ones2() is {:cons, {:suc, 0}, {:cons, {:suc, 0}, :nil}}.
{:ok, src} = Muro.emit_file("examples/list.muro", Muro.Lists)
Code.eval_string(src)
Muro.Lists.length(Muro.Lists.ones2())
# {:suc, {:suc, 0}}
A repr states the host shape of a declared type and emits decode and encode. Decode returns {:ok, value, table}. The type is still this inductive type. See Host representations.
Next: Indexed data.
Source on GitHub
· fetched from murolang/muro manual/