Constraint Generation [blog/lti/cg]

与其将类型参数推断视为一个在无限可能性中「寻找并验证最优解」的搜索问题, 不妨将其重构为一个类似于解代数方程的「求解未知数边界」的问题。 我们不再去猜测类型参数 $\overline{X}$ 可能是什么, 而是通过分析子类型关系本身,去推导出 $\overline{X}$ 必须满足的条件。

观察到我们的规则存在子类型约束,不妨考虑一般情况 $S \lt: T$, 这本身就隐含着对其构成部分(包括其中的未知变量)的约束。 若 $X$ 是 $T$ 中的一个未知变量,那么 $S$ 的结构就必然会限制 $X$ 可能的形态。 我们的算法,正是要将这种隐含的、结构上的限制,转化为一组显式的、关于 $X$ 上下界的断言。

然而,在系统性地提取这些约束之前,我们必须首先面对一个与变量作用域相关的、 虽属前期准备但至关重要的挑战。若不妥善处理,我们生成的约束本身就可能是非良构的。 这一挑战,催生了算法的第一个具体操作步骤:变量消去。

1. Variable Elimination [blog/lti/var_elim]

设想我们想为变量 $X$ 生成约束,以使得类型 $\forall Y. () \to (Y\to Y)$ 成为 $\forall Y. () \to X$ 的一个子类型。根据函数子类型的逆变/协变规则,这要求 $Y \to Y \lt: X$。然而,我们不能直接生成约束 $\{ Y \to Y \lt: X \lt: \top\}$,因为类型变量 $Y$ 在此约束中是自由的,但它本应被 $\forall Y$ 所绑定,这就造成了一种作用域错误。

正确的做法是,找到 $Y \to Y$ 的一个不含 $Y$ 的、且尽可能精确的超类型,并用它来约束 $X$。在本例中,即 $\bot \to \top$。变量消去机制,正是为了形式化地完成这一「寻找最精确边界」的任务。

  1. 提升 (Promotion), $S \Uparrow^V T$:$T$ 是 $S$ 的一个不含集合 $V$ 中任何自由变量的最小超类型
  2. 下降 (Demotion), $S \Downarrow^V T$:$T$ 是 $S$ 的一个不含集合 $V$ 中任何自由变量的最大子类型

这两套关系由一组递归的定则所定义,确保了对于任何类型 $S$ 和变量集 $V$,其结果 $T$ 都是唯一且总能被计算出来的(即它们是全函数)。 此处建议读者停下来思考一下,如何设计这两套关系的递归规则并自己使用代码实现它(提示:对于提升规则的情况,若 $X\in V$ 则将其提升到 $\top$,反之不变,其他情况是显然的)

1.1. Promotion and Demotion Rules [blog/lti/ve_rules]

  • 提升规则 ($S \Uparrow^V T$)

    • 对于 $\top$ 和 $\bot$: $$ \top \Uparrow^V \top \quad (\text{VU-Top}) $$ $$ \bot \Uparrow^V \bot \quad (\text{VU-Bot}) $$
    • 对于类型变量 $X$:
      • 若 $X$ 属于需要被消除的集合 $V$,则将其提升至 $\top$。 $$ \frac{X \in V}{X \Uparrow^V \top} \quad (\text{VU-Var-1}) $$
      • 若 $X$ 不在 $V$ 中,则保持不变。 $$ \frac{X \notin V}{X \Uparrow^V X} \quad (\text{VU-Var-2}) $$
    • 对于函数类型:
      • 递归地对其参数类型进行下降(因为参数位置是逆变的),并对其返回类型进行提升(因为返回位置是协变的)。 $$ \frac{\overline{S} \Downarrow^V \overline{U} \quad T \Uparrow^V R \quad \overline{X} \notin V}{\forall\overline{X}.\overline{S} \to T \Uparrow^V \forall\overline{X}.\overline{U} \to R} \quad (\text{VU-Fun}) $$
  • 下降规则 ($S \Downarrow^V T$)

    • 对于 $\top$ 和 $\bot$ 的处理和提升规则一致。
    • 对于类型变量 $X$:
      • 若 $X$ 属于 $V$,则将其下降至 $\bot$。 $$ \frac{X \in V}{X \Downarrow^V \bot} \quad (\text{VD-Var-1}) $$
      • 若 $X$ 不在 $V$ 中,则保持不变。
    • 对于函数类型:
      • 递归地对其参数类型进行提升,并对其返回类型进行下降。 $$ \frac{\overline{S} \Uparrow^V \overline{U} \quad T \Downarrow^V R \quad \overline{X} \notin V}{\forall\overline{X}.\overline{S} \to T \Downarrow^V \forall\overline{X}.\overline{U} \to R} \quad (\text{VD-Fun}) $$

这可以非常直接地在 MoonBit 中实现:

pub fn promotion(self : Type, v : Set[Var]) -> Type {
  match self {
    TyFun(xs, ss, t) => {
      let vP = v.iter().filter(x => !xs.contains(x)) |> Set::from_iter
      let us = ss.map(_.demotion(vP))
      let r = t.promotion(vP)
      guard xs.iter().all(x => !v.contains(x))
      TyFun(xs, us, r)
    }
    TyVar(vr) if v.contains(vr) => TyTop
    _ => self
  }
}

pub fn demotion(self : Type, v : Set[Var]) -> Type {
  match self {
    TyFun(xs, ss, t) => {
      let vP = v.iter().filter(x => !xs.contains(x)) |> Set::from_iter
      let us = ss.map(_.demotion(vP))
      let r = t.demotion(vP)
      TyFun(xs, us, r)
    }
    TyVar(vr) if v.contains(vr) => TyBot
    _ => self
  }
}

这套精心设计的规则,保证了变量消去操作的正确性与最优性,这由两个关键引理所保证:

  • 可靠性(Soundness):若 $S \Uparrow^V T$,那么可以证明 $S \lt: T$ 且 $T$ 中确实不含 $V$ 中的自由变量。对偶地,若 $S \Downarrow^V T$,则 $T \lt: S$ 且 $T$ 不含 $V$ 中的自由变量。
  • 完备性(Completeness):此操作找到了「最好」的边界。例如,对于提升操作,若存在另一个不含 $V$ 中变量的 $S$ 的超类型 $T'$,那么 $S \Uparrow^V T$ 所计算出的 $T$ 必然是 $T'$ 的子类型(即 $T \lt: T'$),这证明了 $T$ 是所有可能选项中最小的那一个。

约束生成是局部类型参数合成算法的核心引擎。 在通过变量消去确保了作用域安全之后, 此步骤的使命是将一个子类型判定问题——例如, $S \lt: T$,其中 $S$ 或 $T$ 中含有待定的类型参数 $\overline{X}$ 转化为一组对这些未知参数 $\overline{X}$ 的显式约束。 现在我们形式化地定义代码中的约束具体是什么结构:

  • 约束 (Constraint):在本系统中,每一个约束都具有形式 $S_i \lt: X_i \lt: T_i$,它为单个未知类型变量 $X_i$ 同时指定了一个下界 (lower bound) $S_i$ 和一个上界 (upper bound) $T_i$ 。

  • 约束集 (Constraint Set):一个约束集 $C$ 是关于一组未知变量 $\overline{X}$ 到其对应约束的有限映射(在代码中可以实现为一个 Hash Map)。约束集的一个关键不变量是,其中任何约束的上下界($S_i, T_i$)都不能含有任何待定的未知变量(即 $\overline{X}$ 中的变量)或任何需要被消去的局部变量(即 $V$ 中的变量)。空约束集 ($\emptyset$) 代表最无限制的约束,相当于为每一个 $X_i$ 指定了约束 $\bot \lt: X_i \lt: \top$ 。

  • 约束集的交 ($\wedge$) 定义为两个约束集 $C$ 和 $D$ 的交集,是通过将其对同一个变量的约束进行合并得到的。新的下界是原下界的最小上界(join, $\vee$),而新的上界是原上界的最大下界(meet, $\wedge$)

2. Constraint and Constraint Set Code [blog/lti/cg_def_code]

struct Constraints(Map[Var, Subtype])

pub fn Constraints::empty() -> Constraints {
  Constraints(Map::new())
}

pub fn meet(self : Constraints, other : Constraints) -> Constraints {
  union_with(self.inner(), other.inner(), (l, r) => {
    let { l: l1, r: r1 } = l
    let { l: l2, r: r2 } = r
    { l: l1 & l2, r: r1 | r2 }
  })
}

pub fn meets(c : Array[Constraints]) -> Constraints {
  c.iter().fold(init=Constraints::empty(), Constraints::meet)
}

约束生成过程被形式化为一个推导关系 $V \vdash_{\overline{X}} S \lt: T \Rightarrow C$,其意为:在需要回避的变量集为 $V$ 的条件下,为使 $S \lt: T$ 成立,关于未知变量 $\overline{X}$ 需满足的(最弱)约束集是 $C$,这个 $C$ 可以被视为是该推导关系的输出。

算法由一组递归规则定义,其中关键规则如下(注:我们始终假定 $\overline{X} \cap V = \emptyset$):

  • 平凡情况 (Trivial Cases):当子类型关系的上界是 $\top$ 或下界是 $\bot$ 时,该关系无条件成立,因此生成一个空约束集 $\emptyset$ 。
  • 上界约束 (Upper Bound Constraint):当需要判定 $Y \lt: S$(其中 $Y \in \overline{X}$ 是未知变量,而 $S$ 是已知类型)时,算法会为 $Y$ 生成一个上界约束。 $$ \frac{Y \in \overline{X} \quad S \Downarrow^V T \quad \text{FV}(S) \cap \overline{X} = \emptyset}{V \vdash_{\overline{X}} Y \lt: S \Rightarrow \{ \bot \lt: Y \lt: T \}} \quad (\text{CG-Upper}) $$ 注意,这里利用了前述的变量消去操作($S \Downarrow^V T$)来确保上界 $T$ 本身是良构的。
  • 下界约束 (Lower Bound Constraint):对偶地,当需要判定 $S \lt: Y$ 时,算法为 $Y$ 生成一个下界约束。 $$ \frac{Y \in \overline{X} \quad S \Uparrow^V T \quad \text{FV}(S) \cap \overline{X} = \emptyset}{V \vdash_{\overline{X}} S \lt: Y \Rightarrow \{ T \lt: Y \lt: \top \}} \quad (\text{CG-Lower}) $$
  • 函数类型 (Function Type):当比较两个函数类型时,算法递归地处理其参数和返回类型,并将生成的子约束集合并。 $$ \frac{V \cup \{\overline{Y}\} \vdash_{\overline{X}} \overline{T} \lt: \overline{R} \Rightarrow \overline{C} \quad V \cup \{\overline{Y}\} \vdash_{\overline{X}} S \lt: U \Rightarrow D}{V \vdash_{\overline{X}} \forall \overline{Y}.\overline{R} \to S \lt: \forall \overline{Y}.\overline{T} \to U \Rightarrow (\bigwedge \overline{C}) \wedge D} \quad (\text{CG-Fun}) $$ 这里,通过对子约束集取交集 ($\wedge$),实现了约束的累积。

3. Constraint Generation Code [blog/lti/cg_code]

/// Pre-condition: xs and v are disjoint sets of variables.
pub fn generate(
  self : Subtype,
  xs : Array[Var],
  v : Set[Var],
) -> Constraints raise {
  match (self.l, self.r) {
    (_, TyTop) | (TyBot, _) => Constraints::empty()
    (TyVar(y1), TyVar(y2)) if y1 == y2 && !xs.contains(y1) =>
      Constraints::empty()
    (TyVar(y), s) if xs.contains(y) => {
      let t = s.demotion(v)
      Map::from_array([(y, { l: TyBot, r: t })])
    }
    (s, TyVar(y)) if xs.contains(y) => {
      let t = s.promotion(v)
      Map::from_array([(y, { l: t, r: TyTop })])
    }
    (TyFun(ys1, r, s), TyFun(ys2, t, u)) if ys1 == ys2 => {
      let vy = v.union(Set::from_array(ys1))
      let cs = t.zip(r).map(tr => { l: tr.0, r: tr.1 }.generate(xs, vy))
        |> meets()
      cs.meet({ l: s, r: u }.generate(xs, vy))
    }
    _ => raise ConstraintGenerationError
  }
}

一个至关重要的观察是,在任何一次调用 $V \vdash_{\overline{X}} S \lt: T \Rightarrow C$ 时,未知变量 $\overline{X}$ 都只会出现在 $S$ 和 $T$ 的其中一边。这使得整个过程是一个匹配模子类型(matching-modulo-subtyping)问题,而非一个完全的合一(unification)问题,从而保证了算法的简洁性与确定性。