Basic
Since IML serves as both a programming language and a logic, function termination must be provable:
- Every
let recdefinition triggers a termination proving task. All recursive functions need to be admitted before they can be used. The admission includes proving termination. - For common recursion patterns, termination is proven automatically. When no custom measure is given, ImandraX's heuristics pick a single argument for the measure.
- When default measure is not sufficient, provide explicit termination measures using
[@@measure ...]or[@@adm ...].
Ordinals in Termination Proofs:
Termination measures in IML must return values of type Ordinal.t. This type represents ordinals up to ε₀ in Cantor Normal Form, providing a well-founded ordering essential for proving termination. When defining measures, you can use functions like Ordinal.of_int to convert integers to ordinals, or construct more complex ordinals for nested recursion.
Examples and Recipes
Quick Decision Tree:
Is your function rejected for termination?
├─ Does it use structural recursion on datatypes?
│ └─ Usually automatic, if the default measure picks the shrinking argument (see below).
├─ Does it have simple decreasing integer arguments?
│ └─ Usually automatic, if the default measure picks the decreasing argument (see below).
├─ Do arguments decrease lexicographically?
│ └─ Use `[@@adm arg1, arg2, ...]`
└─ Need custom measure?
└─ Use `[@@measure fn arg1 arg2 ...]`
Example: Rely on Default Measure
ImandraX handles these automatically:
Structural recursion on lists:
let rec sum_list = function
| [] -> 0
| x :: xs -> x + sum_list xs
(* Works automatically *)Simple decreasing integers:
let rec countdown x =
if x <= 0 then 0
else x + countdown (x - 1)
(* Works automatically *)How the default measure is chosen
With no [@@measure]/[@@adm], ImandraX measures a single argument: the first one appearing in a condition that leads to a recursive call (a match, an if, or a guard like x = s && f ...). No other argument is tried. So an earlier argument in a guard can take the place of a later one that actually shrinks:
(* Fails: measure is on `s`, not `tr`. Admits with `[@@adm tr]`. *)
let rec chain (s : int) (tr : int list) : bool =
match tr with
| [] -> true
| i :: rest -> i = s && chain (i + 1) restAn int argument is measured as max 0 x, so a base case of x <= 0 admits but x = 0 does not.
Advanced: When the default fails ("Goal is counter-satisfiable"), the chosen argument is visible in the subgoal's << conclusion, e.g. << (Int if s' >= 0 then s' else 0) (Int if s >= 0 then s else 0).
Example: Lexicographic Ordering with [@@adm]
Use when multiple arguments decrease in order:
let rec ackermann m n =
if m <= 0 then n + 1
else if n <= 0 then ackermann (m - 1) 1
else ackermann (m - 1) (ackermann m (n - 1))
[@@adm m, n][@@adm m, n]tells ImandraX: "m decreases first; when m stays the same, n decreases."- When to use:
- Multiple recursive calls with different decreasing patterns
- Nested recursion (like Ackermann)
- Arguments that decrease in a specific priority order
Example: Custom Measure with [@@measure]
(* First define a measure function returning `Ordinal.t` *)
let left_pad_measure n xs =
Ordinal.of_int (n - List.length xs)
let rec left_pad c n xs =
if List.length xs >= n then xs
else left_pad c n (c :: xs)
[@@measure left_pad_measure n xs]
(* Then attach the measure to the function *)Here n - List.length xs decreases with each recursive call and stays
non-negative; Ordinal.of_int maps it to an ordinal, giving ImandraX the
well-founded ordering it needs.
Gotchas
- Measure function must return
Ordinal.t [@@measure ...]works only on top-level functions (not those defined inside another function). In addition, it's generally discouraged to define nested recursive functions in IML.- Refer to [[reference/ordinal.md]] for more details on the
Ordinalmodule (constants, combinators, etc.).
Idioms
Recurring measure/termination patterns from expert proof corpora:
max 0 e guard. Ints can be negative; ordinals can't. Whenever the
decreasing quantity could dip below zero on bad inputs, guard it — this is the
standard idiom, used on nearly every int-recursion:
let rec real_pow (g : real) (n : int) : real =
if n <= 0 then 1.0 else g *. real_pow g (n - 1)
[@@measure Ordinal.of_int (max 0 n)]Upward recursion. Measure by distance to the bound:
[@@measure Ordinal.of_int (List.length l + 1 - k)] for a function recursing
on k + 1 up to List.length l.
Lexicographic measures. Ordinal.pair (see ordinal.md):
[@@measure Ordinal.pair (Ordinal.of_int (List.length p)) (Ordinal.of_int (List.length cands))].
Lemma-assisted admission ([@@by] on the definition). A [@@by] attached
to a let rec is the tactic for its termination proof obligation. When the
decrease isn't syntactic, prove supporting lemmas first and [%use] them in
the admission script:
lemma len_remove_lt x p = List.mem x p ==> List.length (remove x p) < List.length p
[@@by auto]
let rec perms_aux (candidates : int list) (p : int list) : int list list = ...
[@@measure Ordinal.pair (Ordinal.of_int (List.length p))
(Ordinal.of_int (List.length candidates))]
[@@by [%use len_remove_lt (List.hd candidates) p] @> simplify () @>>| auto]This is how functions with data-dependent decrease (e.g. recursing on n / d)
get admitted: the expert corpus admits prime_factors with a six-[%use]
termination script.
Defensive unreachable guards. When the decrease only holds under invariants the admission check can't see, add an early-return branch that makes termination trivial, and refute that branch later in the correctness proof:
let rec descent_loop (m : int) (q : sq4) : sq4 =
let mp = next_m m q in
if mp <= 0 || mp >= m then q (* unreachable under correct hypotheses *)
else descent_loop mp (descent_q m q)
[@@measure Ordinal.of_int (max 0 m)]The correctness theorem then proves mp is always in range, so the guard branch
never fires on valid inputs. This cleanly separates "it terminates" from "it's
correct". (Similarly: guard inputs, e.g. gcd returning 0 on negatives.)