MimIR
MimIR is my Intermediate Representation
Loading...
Searching...
No Matches
Tutorial

A guided tour of Mim's syntax; the sections follow the Mim Language Reference.

Remarks
  • This page is lit/docs/tutorial.mim, rendered by mim --output-md.
  • Open it in the playground to run and edit the tour in the browser.
  • MimIR is a compiler intermediate representation. Mim is a lightweight textual interface for MimIR and not a full-featured programming language.
  • Every refly.struc.e below is a static assertion, checked while the graph is built. So this file compiling at all is what tests it.

Files, Imports, and Modules

import foo;, import "some/dir/foo.mim" as bar;, and pub import foo as *; bind a file the same way - only plugin additionally loads a shared object.

plugin core; // loads `libmim_core` and imports `core.mim` under the name `core`
plugin core as *; // … and once more, splicing its public members into this scope instead
plugin math;
plugin mem;
plugin refly;
use core.ops.u.w; // `+`, `<`, `<<`, … bound for unsigned `Idx s`, wrapping around

Lexical Structure

let dec = 42; // decimal
let bin = 0b101010; // binary
let oct = 0o52; // octal
let hex = 0x2A; // hexadecimal
let flt = 4.2e1:math.F64; // a floating-point literal needs its type
let chr = 'A'; // I8
let str = "Mim"; // «3; I8»
let neg = -42I32; // the sign belongs to the expression, not to the token
let nothing = ⊥; // `bot`; defaults to `*`
let anything = ⊤:Nat; // `top`, ascribed
let _ = refly.struc.e (⊥:[Nat, Bool], (⊥:Nat, ⊥:Bool)); // pushed into aggregates
let _ = refly.struc.e (dec, bin);
let _ = refly.struc.e (oct, hex);
let tru = 1₂; // an index literal spells its arity as a subscript …
let alt = 1_2; // … or after an `_`
let wid = 23I32; // or as a bit width - the very same as `23:I32`
let _ = refly.struc.e (tru, tt);
let _ = refly.struc.e (alt, tt);
let _ = refly.struc.e (wid, 23:I32);

Kinds and Builtin Types

let univ = Univ; // the universe of type levels
let lvl0 = Type (0:Univ); // the type at level 0 …
let star = *; // … which `*` abbreviates
let boxx = □; // and `Type (1:Univ)`, which `□` abbreviates
let nat = Nat;
let idx = Idx; // the builtin of type `Nat → *`
let rul = Rule Nat; // the type of rewrite rules over `Nat`
lam bool_is_idx (b: Bool): Idx 2 = b; // `Bool` abbreviates `Idx i1`; likewise I8 … I64

Patterns and Telescopes

(...) is a pattern and [...] a telescope: an unnamed element binds in the one and is a type in the other. Position decides which one you need.

lam ptrn (a: Nat, _: Bool): Nat = a;
let tele = [Nat, Bool];

A group distributes one annotated type over several names.

lam group (a b: Nat, c d: Bool): Nat = a + b where use core.ops.n; end;

An alias pattern binds the components and the whole value.

let (a, b, c) as abc = (23, 42, 66);
let _ = refly.struc.e (abc, (23, 42, 66));

Aggregates

let tup = (23, 42, 66); // a tuple
let pck = ‹3; 7›; // a pack - and a tuple of equal elements *is* one
let sig = [Nat, Bool]; // a sigma
let arr = «3; Nat»; // an array - and a sigma of equal components *is* one
let _ = refly.struc.e ((7, 7, 7), pck);
lam sigma_is_array (x: [Nat, Nat, Nat]): «3; Nat» = x;
let fst = tup#0_3; // extract
let upd = tup#1_3 ← 0; // insert: a *new* tuple, nothing is mutated
let _ = refly.struc.e (fst, 23);
let _ = refly.struc.e (upd, (23, 0, 66));

A nest of arrays fuses into one shape, and # follows suit.

lam fuse (m: «2; «3; Nat»»): «2, 3; Nat» = m;
lam fused (t: «2, 3; Nat», i: Idx 2, j: Idx 3): Nat =
refly.struc.e (t#i#j, t#(i, j));

Unless an extent depends on an earlier index - then the nest stays one array per axis.

let jagged = «i: 3, j: (1, 2, 3)#i; Nat»;

Dependent Tuple Types

let dyn = [n: Nat, «n; Nat»]; // a component mentions a name to its left: a *mutable* sigma

rec puts the declared name in scope inside the body - the only way for a type to mention itself - and keeps the sigma mutable even where nothing depends on it.

rec List = [head: I32, tail: mem.Ptr0 List];

A mutual block lets declarations refer to each other.

mutual
rec Tree = [val: I32, kids: mem.Ptr0 Forest];
rec Forest = [car: mem.Ptr0 Tree, cdr: mem.Ptr0 Forest];
end

Components carry field names, which beat lexical names of the same spelling - with a warning, as here.

let tail = 1₂;
lam by_field (l: List): mem.Ptr0 List = l#tail;
lam by_var (p: «2; Nat»): Nat = p#(tail); // parentheses force the variable
let Size = [w h: Nat]; // just `«2; Nat»`: the names belong to the shape
lam width (s: Size): Nat = s#w;

rec is also the only way to keep a genuine one-element aggregate.

rec One = [x: Nat];
lam real_extract (t: «2; One»): Nat = t#1₂#0₁; // a real Extract into the 1-tuple
lam axis_folds (t: «2; One»): One = t#(1₂, 0₁); // the size-1 axis folds away: just `t#1₂`

Functions and Continuations

Declaration Expression Type
lam λ / lm →
con cn Cn
fun fn Fn

These three declare one and the same function; @ff is the partial-evaluation filter.

lam pick_lam (T: *) ((x y: T), return: T → ⊥)@ff: ⊥ = return x;
con pick_con (T: *) ((x y: T), return: Cn T) = return x;
fun pick_fun (T: *) (x y: T): T = return x;

The expression forms are anonymous, so they cannot call themselves.

let anon_lam = λ (T: *) ((x y: T), return: T → ⊥)@ff: ⊥ = return x;
let anon_lm = lm (T: *) ((x y: T), return: T → ⊥)@ff: ⊥ = return x;
let anon_cn = cn (T: *) ((x y: T), return: Cn T) = return x;
let anon_fn = fn (T: *) (x y: T): T = return x;

And these three spell one and the same type.

lam fn_ty1 (f: [T: *] → [[T, T], T → ⊥] → ⊥): [T: *] → Cn [[T, T], Cn T] = f;
lam fn_ty2 (f: [T: *] → Cn [[T, T], Cn T]): [T: *] → Fn [T, T] → T = f;

Cn X has no codomain, so every name in its domain is erased.

lam erased (f: Cn [x y: I32]): Cn [I32, I32] = f;

An implicit domain is written {...}; @ passes one explicitly.

lam ident {T: *} (x: T): T = x;
let _ = refly.struc.e (ident 23, ident @Nat 23);

mutual also groups functions, and ret p = f $ a; body binds the result of a continuation-style call.

mutual
fun even (n: I32): Bool = core.select (n == 0I32, yes, no) () where
con yes () = return tt;
con no () = ret r = odd $ (n - 1I32); return r;
end;
fun odd (n: I32): Bool = core.select (n == 0I32, yes, no) () where
con yes () = return ff;
con no () = ret r = even $ (n - 1I32); return r;
end;
end

Unions

lam inject (A B: *) (x: A): A ∪ B = x inj A ∪ B;
lam elim (A B: *) (ab: A ∪ B): Nat =
match ab with
| _: A => 0
| _: B => 1;

Variants

A variant's cases are positional and never merged: these three [] cases stay three, where ∪ would collapse them.

rec Color = | Red | Green | Blue;
lam hue (c: Color): Nat =
match c with
| Red => 0
| Green => 1
| Blue => 2;
let _ = refly.struc.e (hue Color#Green, 1); // `T#C` selects a case by name ...
let _ = refly.struc.e (hue Color#2, 2); // ... or by index, from 0

With a payload, T#C is a function, and the arm binds the payload with a pattern; rec makes a variant recursive.

rec Nats = | Nil | Cons: [Nat, Nats];
lam first (l: Nats): Nat =
match l with
| Nil => 0
| Cons (h, _) => h;
let _ = refly.struc.e (first (Nats#Cons (23, Nats#Nil)), 23);

An arm selects its case by index just as well, exactly like T#n does.

lam first_by_index (l: Nats): Nat = match l with | 0 => 0 | 1 (h, _) => h;
let _ = refly.struc.e (first_by_index (Nats#Cons (23, Nats#Nil)), 23);

A variant is a type like any other, so it may just as well be anonymous.

lam get (x: | None | Some: Nat): Nat = match x with | None => 0 | Some n => n;
let _ = refly.struc.e (get ((| None | Some: Nat)#Some 42), 42);

Singletons

«e» is the singleton type whose sole inhabitant is e and sits one level above it.

lam ty (T: *) (x: T): * = «x»; // so `«23»` is a type …
lam kind (T: *) : □ = «T»; // … and `«Nat»` a kind

‹e› introduces such a value and the prefix # eliminates it again.

lam intro (T: *) (x: T): «x» = ‹x›;
let _ = refly.struc.e (#‹23›, 23);

# recovers the inhabitant from the type alone.

lam only (T: *) (x: «T»): * = #x;

The ; is what tells an array or pack from a singleton.

lam not_single (x: «2; Nat»): Nat = x#0₂;

# is a prefix only where an expression starts, so f #x extracts instead of applying.

lam extracts (t: «2; Nat», i: Idx 2): Nat = refly.struc.e (t #i, t#i);

Nominal Newtypes

nom wraps a type into a fresh one; two noms over the same type stay distinct.

nom Meter = I32;
nom Foot = I32;

inj wraps a value and the prefix # unwraps it again - both only in the mod (or file) that declared the nom. An importer therefore sees the type as abstract.

lam to_foot (m: Meter): Foot = #m inj Foot;
let _ = refly.struc.e (#(23I32 inj Meter), 23I32);

Like rec, a nom is in scope in its own body, and anx nom makes it an annex.

nom Chain = | End | Link: [I32, Chain];

Infix Operators

a op b is sugar for `op (a, b) - Mim gives the operators no meaning of their own.

let arith = (1 + 2, 5 - 3, 2 * 3, 7 / 2, 7 % 2) where use core.ops.n; end;
let compa = (1 == 1, 1 != 2, 1 < 2, 1 <= 2, 2 > 1) where use core.ops.n; end;
let shift = (1I32 << 2I32, 8I32 >> 1I32); // `core.ops.u.w`, in scope above

Rebind one and the arithmetic changes with it.

let surprise = 2 + 3 where let `+ = core.nat.mul; end;
let _ = refly.struc.e (surprise, 6);

Modules and Visibility

pub mod geo
pub let dim = 3;
priv let hidden = 0; // no path from outside may reach this
pub mod deep
pub let leaf = 7;
end
end
use geo.deep as *; // splices `leaf` into this scope - a plain `use` is sugar for this …
use geo.deep as dp; // … or binds another name for the module
let _ = refly.struc.e (leaf, dp.leaf);
let _ = refly.struc.e (leaf, geo.deep.leaf);

Annexes and Axioms

anx let answer = 42; // an annex is additionally registered by its qualified name
anx Answer = answer; // an alias for an annex
axm Set: * → *; // an axiom
axm set.(union = join, inter): {T: *} → [Set T, Set T] → Set T; // several at once, with an alias
axm Curried: [Nat, Nat] → *, 1; // … and a curry counter

Rules

mod rewrites
use core.ops.n;
pub rule dbl (a: Nat): (a + a) => (a * 2); // a rewrite rule, visible outside `rewrites`
norm zro (a: Nat): (a * 0) when (a == a) => 0; // its normalizing variant, `priv` by default
end
Todo
First-class rules are WIP and you can't really do anything useful with them yet.

LLVM

A bodyless extern forward-declares a function living in a native translation unit.

extern con println_i32 [mem.M 0, I32, Cn [mem.M 0]];

let rebinding is what keeps state-threading code readable. Don't name such a value mem: it would shadow the module, and mem.load would not resolve.

extern fun roundtrip (m: mem.M 0, v: I32): [mem.M 0, I32] =
let (m, ptr) = mem.alloc (I32, 0) m;
let m = mem.store (m, ptr, v);
let (m, val) = mem.load (m, ptr);
return (m, val);

Tick optimize in the playground to see this one as LLVM IR.

extern fun fact (n: I32): I32 =
loop (1I32, n)
where
con loop (acc i: I32) = core.select (i > 0I32, body, exit) ()
where
con body () = loop (acc * i, i - 1I32);
con exit () = return acc;
end;
use core.ops.s.w;
end;

In order to compile to LLVM you have to load the ll plugin (implies loading the opt plugin):

mim myprogram.mim -p ll

This will generate myprogram.ll that you can then pass to clang or other programs from the LLVM toolchain. You can also write to a different file or to stdout with -X ll:<arg>:

mim myprogram.mim -p ll -X ll:output=myoutput.ll # emits: myoutput.ll
mim myprogram.mim -p ll -X ll:output=- # emits to stdout
mim myprogram.mim -p ll -X ll:o=myoutput.ll # shortcut
mim myprogram.mim -p ll -X ll:o=-

Every plugin declares its own -X arguments; see Plugin Arguments.