Calculating Type Arguments [blog/lti-english/calc_args][edit]
Calculating Type Arguments [blog/lti-english/calc_args][edit]
Through the preceding constraint generation step, we have successfully transformed a non-constructive optimal solution search problem into a concrete, tangible outcome: a constraint set $C$. This constraint set encapsulates all boundary conditions that the undetermined type parameters $\overline{X}$ must satisfy for the entire polymorphic application to be type-correct. For each unknown variable $X_i$, we obtain a valid interval of the form $S_i \lt: X_i \lt: T_i$.
At this point, the first phase of the algorithm—“What do we know about the unknowns?”—has been successfully completed. However, our work is not yet finished. A constraint interval, such as $\mathbb{Z} \lt: X \lt: \mathbb{R}$, may itself permit multiple valid solutions (e.g., $\mathbb{Z}$ or $\mathbb{R}$). Ultimately, we must select a concrete type value for each $X_i$ to finalize the construction of the core language term.
This leads us to the final and crucial step of the algorithm: By what criteria should we make the ultimate choice from each variable’s valid interval? The answer must return to our original intent—the optimality requirement declared in the App-InfSpec specification: Our choices must yield a unique, minimal result type for the entire application expression. Therefore, the algorithm’s last step is to design a selection strategy that leverages the constraint set $C$ we have diligently collected, combines it with the function’s original return type $R$, and computes a set of concrete type arguments that ultimately minimize $R$. This is the core task of this section: “Calculating Type Arguments.”
To compute specific type parameters regarding $R$, there is another crucial operation: the change in polarity due to variable substitution. Currently, our consideration of polarity focuses primarily on function types, 1. Polarity [blog/lti-english/polarity][edit]
The polarity of a type variable $X$ in another type expression $R$ describes how the type of the entire expression changes accordingly when the type of that variable becomes larger or smaller.
and we only need to pay attention to the position of variables in function types (whether on the left or right of the arrow),
while also considering nested function structures. It is recommended that the reader pause here to think about the design of this algorithm.
Of course, you can also directly expand the code block below to view the specific implementation. 1.1. Variance Code [blog/lti-english/variance_code][edit]
pub fn Type::variance(self : Type, va : Var) -> Variance {
letrec go = (ty : Type, polarity : Bool) => {
match ty {
TyVar(v) if v == va => if polarity { Covariant } else { Contravariant }
TyVar(_) | TyTop | TyBot => Constant
TyFun(_, params, ret) => {
let param_variance = params
.map(p => go(p, !polarity))
.fold(init=Constant, combine_variance)
combine_variance(param_variance, go(ret, polarity))
}
}
}
and combine_variance = (v1 : Variance, v2 : Variance) => {
match (v1, v2) {
(Constant, v) | (v, Constant) => v
(Covariant, Covariant) | (Contravariant, Contravariant) => v1
(Covariant, Contravariant) | (Contravariant, Covariant) => Invariant
(Invariant, _) | (_, Invariant) => Invariant
}
}
go(self, true)
}
To minimize the return type $R$, our selection strategy becomes evident:
- If $X_i$ is covariant in $R$, we should choose the minimum value within its valid interval, i.e., its constraint’s lower bound.
- If $X_i$ is contravariant in $R$, we should choose the maximum value within its valid interval, i.e., its constraint’s upper bound.
- If $X_i$ is invariant in $R$, then to ensure result uniqueness and comparability, its valid interval must be a “single point,” i.e., its constraint’s upper and lower bounds must be equal.
The above strategy is formalized as an algorithm for computing the minimal substitution $\sigma_{CR}$. Given a satisfiable constraint set $C$ and return type $R$:
For each constraint $S \lt: X_i \lt: T$ in $C$:
- If $R$ is covariant or constant in $X_i$, then $\sigma_{CR}(X_i) = S$.
- If $R$ is contravariant in $X_i$, then $\sigma_{CR}(X_i) = T$.
- If $R$ is invariant in $X_i$ and $S=T$, then $\sigma_{CR}(X_i) = S$.
- In all other cases (especially when $S \neq T$ for invariant variables), $\sigma_{CR}$ is undefined.
When $\sigma_{CR}$ is undefined, the algorithm fails, precisely corresponding to the scenario in App-InfSpec where no unique optimal solution can be found.
2. Solving Constraints [blog/lti-english/solve_code][edit]
pub fn Constraints::solve(self : Constraints, ty : Type) -> Map[Var, Type]? {
let f = (vs : (Var, Subtype)) => {
let { l, r } = vs.1
let v = match ty.variance(vs.0) {
Constant | Covariant => Some(l)
Contravariant => Some(r)
Invariant => if l == r { Some(l) } else { None }
}
v.map(v => (vs.0, v))
}
let constraints = self.items()
let solved = constraints.iter().filter_map(f).to_array()
guard solved.length() == constraints.length() else { None }
Some(Map::from_array(solved))
}
At this point, we have fully defined a completely executable algorithm consisting of three steps: “Variable Elimination,” “Constraint Generation,” and “Parameter Calculation.” But how do we ensure that the behavior of this concrete algorithm is entirely consistent with the non-constructive, declarative The proof of their equivalence is distilled into a core proposition asserting: Proof Sketch: Part 1 (Correctness of the Algorithm):
To prove that $\sigma_{CR}$ is optimal, we take an arbitrary other valid substitution $\sigma'$ satisfying the constraints and need to prove $\sigma_{CR}(R) \lt: \sigma'(R)$.
Consider constructing a “chain of substitutions” from $\sigma_{CR}$ to $\sigma'$, where each step changes only one variable’s value. For example, $\sigma_0 = \sigma_{CR}$, $\sigma_1 = \sigma_0[X_1 \mapsto \sigma'(X_1)]$, …, $\sigma_n = \sigma'$.
Next, we prove that at each step along this path, the resulting type is monotonically non-decreasing, i.e., $\sigma_{i-1}(R) \lt: \sigma_i(R)$. The proof of this step relies directly on the aforementioned polarity definitions. For instance, if $R$ is covariant in $X_i$, our algorithm selected the lower bound $S$, and $\sigma'(X_i)$ must be greater than or equal to $S$. Therefore, by the definition of covariance, the resulting type must “increase.” Similarly, the assertion holds in other cases.
Ultimately, by transitivity, we conclude $\sigma_{CR}(R) \lt: \sigma'(R)$, proving the optimality of $\sigma_{CR}$. Part 2 (Completeness of Algorithm Failure):
When the algorithm fails, it must be because the constraint interval $[S, T]$ for some invariant variable $X_i$ has $S \neq T$.
We use proof by contradiction: assume that an optimal solution $\sigma$ still exists under these circumstances. Since $S \neq T$, we can always find another valid substitution $\sigma'$ such that $\sigma(X_i)$ differs from $\sigma'(X_i)$. However, because $R$ is invariant in $X_i$, $\sigma(R)$ and $\sigma'(R)$ become incomparable. This contradicts the assumption that “$\sigma$ is the optimal solution (i.e., it is smaller than all other solutions)”.
Therefore, in this case, the optimal solution necessarily does not exist. 3. Proof Sketch [blog/lti-english/proof_eq][edit]
App-InfSpec specification?
This core proposition proves the equivalence between our designed concrete algorithm and the App-InfSpec specification. It allows us to ultimately replace that non-executable specification with a fully algorithmic rule App-InfAlg:
$$ \frac{ \begin{array}{ccc} \Gamma \vdash f : \forall \overline{X}.\overline{T} \to R & \Gamma \vdash \overline{e} : \overline{S} & |\overline{X}| > 0 \\ \emptyset \vdash_X \overline{S} \lt: \overline{T} \Rightarrow \overline{D} & C = \bigwedge \overline{D} & \sigma = \sigma_{CR} \end{array} }{ \Gamma \vdash f(\overline{e}) : \sigma R } \quad (\text{App-InfAlg}) $$