elab 教程

本教程介绍如何将 stella 类型论的项写成 MoonBit 值、向内核询问它们的类型、对照你给出的类型检查它们,以及计算它们的范式。教程从恒等函数开始,以一个路径归纳证明结束。每一步背后的类型规则见设计说明。

快速开始

将模块添加到你的项目:

moon add Luna-Flow/stella@0.1.2

在 moon.pkg 中导入该包以及提供上下文和环境的 list 包:

import {
  "Luna-Flow/stella/elab",
  "moonbitlang/core/list",
}

最小的有用程序对单位类型上的恒等函数做类型检查并应用它:

using @elab {type TermChk, type TermInf}

test "quick start" {
  // (λx. x : 1 → 1)
  let id_unit = TermInf::Ann(Lam(Inf(Bound(0))), Inf(Pi(Inf(UnitType), Inf(UnitType))))
  let ty = @elab.type_inf_0(@list.empty(), id_unit)
  debug_inspect(@elab.quote(0, ty), content="Inf(Pi(Inf(UnitType), Inf(UnitType)))")
  let v = @elab.eval_inf(App(id_unit, UnitElement), @list.empty())
  debug_inspect(@elab.quote(0, v), content="UnitElement")
}

这里出现了三个要点。函数 Lam 没有定义域标注,因此内核无法推断其类型;Ann 提供了类型。type_inf_0 以值的形式返回类型,quote(0, _) 将值转回可打印的项。eval_inf 进行计算,应用归约为 UnitElement。

日常任务

读写德布鲁因索引

变量是数字:Bound(0) 是最近外层绑定子的变量,Bound(1) 是再外一层的,依此类推。绑定子是 Lam 以及 Pi、Sigma 和 W 的第二个参数。多态恒等函数 λA. λx. x:ΠA:U0Πx:AA\lambda A.\,\lambda x.\,x : \Pi_{A : \mathcal U_0} \Pi_{x : A} A 写法如下:

fn poly_id() -> TermInf {
  // Π(A : U0). Π(x : A). A  — inside the inner Π, A is Bound(1)
  let ty = TermChk::Inf(Pi(Inf(Universe(0)), Inf(Pi(Inf(Bound(0)), Inf(Bound(1))))))
  Ann(Lam(Lam(Inf(Bound(0)))), ty)
}

test "polymorphic identity" {
  let ty = @elab.type_inf_0(@list.empty(), poly_id())
  debug_inspect(
    @elab.quote(0, ty),
    content="Inf(Pi(Inf(Universe(0)), Inf(Pi(Inf(Bound(0)), Inf(Bound(1))))))",
  )
  // instantiate A := 1 and apply to ⋆
  let app = TermInf::App(App(poly_id(), Inf(UnitType)), UnitElement)
  debug_inspect(@elab.quote(0, @elab.type_inf_0(@list.empty(), app)), content="Inf(UnitType)")
  debug_inspect(@elab.quote(0, @elab.eval_inf(app, @list.empty())), content="UnitElement")
}

在类型中,内层 Π\Pi 的定义域是 Bound(0),即外层 Π\Pi 绑定的 AA;其陪域又多处于一个绑定子之下,因此同一个 AA 在那里是 Bound(1)。应用的类型通过代换计算:内核将陪域闭包应用于参数的值。

在上下文中声明常量

上下文列出名为 Global(...) 的自由变量的类型。用它来公设一个类型及其一个元素:

fn ctx_a() -> @elab.Context {
  // A : U0, a : A   (the head of the list is searched first)
  @list.List([(Global("a"), VNeutral(NFree(Global("A")))), (Global("A"), VUniverse(0))])
}

test "constants" {
  let ty = @elab.type_inf_0(ctx_a(), Free(Global("a")))
  debug_inspect(@elab.quote(0, ty), content="Inf(Free(Global(\"A\")))")
}

a 的类型是项 A 的值,即中性变量 VNeutral(NFree(Global("A")))。

使用对

对是对照 Σ\Sigma 类型检查的,投影则从中推断各自的类型:

test "pairs" {
  let ty_a = TermChk::Inf(Free(Global("A")))
  let a = TermChk::Inf(Free(Global("a")))
  // (a, ⋆) : Σ(x : A). 1
  let pair = TermInf::Ann(Pair(a, UnitElement), Inf(Sigma(ty_a, Inf(UnitType))))
  debug_inspect(@elab.quote(0, @elab.type_inf_0(ctx_a(), Fst(pair))), content="Inf(Free(Global(\"A\")))")
  debug_inspect(@elab.quote(0, @elab.type_inf_0(ctx_a(), Snd(pair))), content="Inf(UnitType)")
  debug_inspect(@elab.quote(0, @elab.eval_inf(Fst(pair), @list.empty())), content="Inf(Free(Global(\"a\")))")
}

处理类型错误

检查器抛出 TypeError,其消息指出失败的规则。像捕获任何 MoonBit 错误一样捕获它:

fn infer_or_message(ctx : @elab.Context, e : TermInf) -> String {
  try @elab.type_inf_0(ctx, e) catch {
    TypeError(msg) => msg
  } noraise {
    ty => "type: \{Repr(@elab.quote(0, ty))}"
  }
}

test "errors" {
  inspect(infer_or_message(ctx_a(), App(Free(Global("a")), UnitElement)), content="Illegal Application")
  inspect(infer_or_message(ctx_a(), Free(Global("b"))), content="Unknown Identifier: Global(\"b\")")
  inspect(infer_or_message(ctx_a(), Universe(0)), content="type: Inf(Universe(1))")
}

使用宇宙与累积性

Universe(i) 的类型是 Universe(i + 1),Ui\mathcal U_i 中的类型在每个更大的宇宙中也被接受:

test "universes" {
  debug_inspect(@elab.type_inf_0(@list.empty(), Universe(0)), content="VUniverse(1)")
  // 1 : U0, and therefore also 1 : U1
  @elab.type_chk(0, @list.empty(), @list.empty(), Inf(UnitType), VUniverse(1))
  // a function type lives in the larger universe of its parts
  debug_inspect(
    @elab.type_inf_0(@list.empty(), Pi(Inf(UnitType), Inf(Universe(0)))),
    content="VUniverse(1)",
  )
  assert_true(@elab.def_eq(0, VUniverse(0), VUniverse(1)))
  assert_false(@elab.def_eq(0, VUniverse(1), VUniverse(0)))
}

def_eq 是检查器所用的子类型测试,而不是对称的相等关系。

深入

用路径归纳证明命题

消去子 JElim(A, x, P, d, y, p) 将 P x (refl x) 的证明 d 和路径 p : Id(A, x, y) 转为 P y p 的证明。动机 P 必须是可推断的函数,因此要对其标注。这里动机是常函数 P=λy. λp. AP = \lambda y.\,\lambda p.\,A,JJ 沿 refl a 传输 a:

test "path induction" {
  let ty_a = TermChk::Inf(Free(Global("A")))
  let a = TermChk::Inf(Free(Global("a")))
  // P : Π(y : A). Id(A, a, y) → U0, P = λy. λp. A
  let motive_ty = TermChk::Inf(
    Pi(ty_a, Inf(Pi(Inf(Id(ty_a, a, Inf(Bound(0)))), Inf(Universe(0))))),
  )
  let motive = TermChk::Inf(Ann(Lam(Lam(ty_a)), motive_ty))
  let path = TermInf::Ann(Rfl(a), Inf(Id(ty_a, a, a)))
  let j = TermInf::JElim(ty_a, a, motive, a, a, path)
  debug_inspect(@elab.quote(0, @elab.type_inf_0(ctx_a(), j)), content="Inf(Free(Global(\"A\")))")
  // J computes on refl: J(A, a, P, d, a, refl a) = d
  debug_inspect(@elab.quote(0, @elab.eval_inf(j, @list.empty())), content="Inf(Free(Global(\"a\")))")
}

归一化项并比较

项的范式是其值的读回。归一化会在绑定子之下归约,因此它能判定项的 β\beta-相等:

fn normal_form(t : TermChk) -> TermChk {
  @elab.quote(0, @elab.eval_chk(t, @list.empty()))
}

test "normal forms" {
  let id_unit = TermInf::Ann(Lam(Inf(Bound(0))), Inf(Pi(Inf(UnitType), Inf(UnitType))))
  // λy. (λx. x) y  and  λy. y  have the same normal form
  let t1 = TermChk::Lam(Inf(App(id_unit, Inf(Bound(0)))))
  let t2 = TermChk::Lam(Inf(Bound(0)))
  assert_true(normal_form(t1) == normal_form(t2))
}

比较使用 TermChk 派生的 Eq,由于名字是索引,它就是 α\alpha-等价。

自己在绑定子之下检查

type_inf_0 处理闭项。要检查含有自由 Bound 变量的项,请以检查器在这些绑定子之下会具有的状态调用 type_inf 或 type_chk:层级,以及每个绑定子一个上下文条目 (Local(k), type) 和一个环境条目 val_var(Local(k)),最内层的在前。

test "under a binder" {
  // under x : 1, the term x has type 1
  let ctx : @elab.Context = @list.List([(Local(0), VUnitType)])
  let env : @elab.Env = @list.List([@elab.val_var(Local(0))])
  debug_inspect(@elab.type_inf(1, ctx, env, Bound(0)), content="VUnitType")
}

常见陷阱

  • 类型位置必须可推断。 类型是 TermChk,但形成规则会推断类型所在的宇宙,因此凡是需要类型的地方都写 Inf(UnitType),而不是裸的 Lam 或 Pair。
  • 函数需要标注才能被推断。 Lam(...) 只能被检查。用 Ann(lam, Inf(Pi(...))) 包裹它,才能在应用中或作为动机使用。
  • W 和 WRec 对族的写法不同。 在 W(A, B) 中,B 是绑定子之下的体;在 WRec(A, B, ...) 中,B 是类型为 A→UkA \to \mathcal U_k 的可推断函数项。
  • def_eq 有方向且不依赖上下文。 它测试的是子类型;并且由于不知道 f 的类型,它按读回结果比较 f x 这样的中性应用,不会对参数使用 η\eta。
  • 求值信任其输入。 eval_inf 和 val_ 函数在类型错误的项上会 panic;请先做类型检查。
  • 用 Debug 打印。 内核类型派生的是 Debug 而非 Show:请使用 debug_inspect、Repr(x) 或 @debug.to_string(x)。闭包会打印为 <function: ...>,因此打印值之前先对其 quote。

后续步骤

  • API 参考列出了每个构造子和函数。
  • 设计说明以推理规则记法给出类型规则,并解释求值归一化。
  • 从概览链接的论著从无类型 lambda 演算出发发展了该类型论。