elab 教程

本教程使用 elab 包将名称解析为内核项,并展示解析之后签名发生变化时会怎样。当你在内核之上构建自己的前端,或想理解解析器 (parser) 可能报告的错误 ScopeResolutionMismatch 时,可参阅本教程。

快速开始

在 moon.pkg 中导入内核和 elab:

import {
  "Luna-Flow/QED/kernel",
  "Luna-Flow/QED/elab",
}

解析等式 x=cx = c(其中 x 是局部变量,c 是已声明的常量),并将其降级为内核项:

test "quick start" {
  let a = @kernel.mk_tyvar("A")
  let st = @kernel.ks_add_const(@kernel.empty_kernel_state(), "c", a).unwrap()
  let ctx = @elab.elab_ctx_from_locals([("x", a)])
  let x = @elab.elab_resolve_name(st, ctx, "x").unwrap()
  let c = @elab.elab_resolve_name(st, ctx, "c").unwrap()
  let eq = @elab.elab_resolve_eq(st, ctx, x, c).unwrap()
  let t = @elab.elab_lower_to_term(eq).unwrap()
  inspect(
    @kernel.term_to_string(t),
    content="Comb(Comb(Const(= : fun(A, fun(A, bool))), Var(x : A)), Const(c#1 : A))",
  )
}

常量显示为 c#1:其标识在解析时就已固定。

日常任务

自底向上构建项

elab_resolve_app 和 elab_resolve_abs 在构建时检查类型:

test "bottom up" {
  let bool = @kernel.bool_ty()
  let st = @kernel.ks_add_const(@kernel.empty_kernel_state(), "neg", @kernel.fun_ty(bool, bool)).unwrap()
  let ctx = @elab.empty_elab_ctx()
  let neg = @elab.elab_resolve_name(st, ctx, "neg").unwrap()
  // λp. neg p  — the body is checked with p in scope
  let inner = @elab.elab_ctx_extend(ctx, "p", bool)
  let body = @elab.elab_resolve_app(st, inner, neg, @elab.elab_resolve_name(st, inner, "p").unwrap()).unwrap()
  let lam = @elab.elab_resolve_abs(st, ctx, "p", bool, body).unwrap()
  inspect(@elab.elab_check_core_type(st, ctx, lam, @kernel.fun_ty(bool, bool)), content="true")
  // neg applied to itself is rejected
  inspect(@elab.elab_resolve_app(st, ctx, neg, neg) is Err(@elab.CoreTypingFailure), content="true")
}

在某个实例上使用多态常量

用类型变量声明的常量可以在其模式 (schema) 的任意实例上解析:

test "instances" {
  let a = @kernel.mk_tyvar("A")
  let st = @kernel.ks_add_const(@kernel.empty_kernel_state(), "default", a).unwrap()
  let rc = @elab.elab_resolve_const(st, "default", @kernel.bool_ty()).unwrap()
  inspect(@kernel.hol_type_to_string(@elab.rconst_inst_ty(rc)), content="bool")
  inspect(@kernel.hol_type_to_string(@elab.rconst_schema_ty(rc)), content="A")
  // fun(A, A) is not an instance of the schema bool -> bool of `flip`
  let bool = @kernel.bool_ty()
  let st2 = @kernel.ks_add_const(st, "flip", @kernel.fun_ty(bool, bool)).unwrap()
  let bad = @elab.elab_resolve_const(st2, "flip", @kernel.fun_ty(a, a))
  inspect(bad is Err(@elab.InvalidConstInstance("flip")), content="true")
}

检测作用域漂移

已解析的项会记住每个常量指的是哪个声明。在出现遮蔽声明之后再次检查,会失败,而不会切换到新的常量:

test "scope drift" {
  let bool = @kernel.bool_ty()
  let st1 = @kernel.ks_add_const(@kernel.empty_kernel_state(), "flag", bool).unwrap()
  let ctx = @elab.empty_elab_ctx()
  let flag = @elab.elab_resolve_name(st1, ctx, "flag").unwrap()
  let st2 = @kernel.ks_add_const(@kernel.ks_push_scope(st1), "flag", bool).unwrap()
  inspect(@elab.rterm_well_formed(st2, ctx, flag), content="false")
  // resolving the name again in st2 gives the new constant
  let flag2 = @elab.elab_resolve_name(st2, ctx, "flag").unwrap()
  inspect(@elab.rterm_to_string(flag2), content="RConst(flag#2 : bool <= bool)")
  // back in the outer scope the old term is fine again
  let st3 = @kernel.ks_pop_scope(st2).unwrap()
  inspect(@elab.rterm_well_formed(st3, ctx, flag), content="true")
}

进一步使用

检查来自别处的项。 elab_roundtrip_term(state, ctx, t) 会重新解析内核项并检查是否有变化;在其他状态中构建的项进入你的代码的边界处使用它。

检查项。 rterm_collect_consts 列出每个常量出现及其标识,rterm_collect_free_vars 列出每个自由变量;两者都适用于诊断。

交给内核。 用 elab_lower_to_term 降级,并把结果传给内核规则。你解析出的标识随之传递,内核的可容许性检查会再次强制执行它们。

改用解析器。 对于文本输入,@parser.parse_term 和 @parser.parse_resolved_term 会替你运行此包;见 parser 教程。

常见陷阱

  • 期望类型推断。 绑定子类型从不推断;请把它们传给 elab_resolve_abs,或在上下文中连同类型声明局部变量。
  • 用 rterm_eq 比较。 它也比较绑定名,所以 α 等价的项可能不相等。请将两者都降级,再使用 @kernel.term_alpha_eq。
  • 扩展后复用上下文。 elab_ctx_extend 返回新上下文;旧的保持不变,离开绑定子时正需要这样。
  • 在状态中寻找 =。 等号是内置的,无需声明即可解析;你不能声明或遮蔽它。

后续步骤

  • elab API 列出了每个函数和错误。
  • elab 设计阐述了冻结性质及其成立的原因。
  • parser 教程展示了构建在此包之上的文本前端。