Zulip Chat Archive

Stream: lean4

Topic: (Interpreter, deBruijin style) Termination checker


McCoy R. Becker (Dec 27 2023 at 19:51):

Hi all! New to the community. I've been playing around with Lean 4 for a bit. I'm trying to write an applicative order evaluation interpreter for a lambda calculus with natural numbers, and binary primitives (addition, etc).

I wrote a deBrujin style eval for this, but I'm hitting the termination checker. I dumped my interpreter into a file: https://github.com/femtomc/logical_relations.lean/blob/main/IntroLogicalRelations.lean

Lean tells me that the issue is here: https://github.com/femtomc/logical_relations.lean/blob/c99b4604b9e1acde704db896efae0171ee25666f/IntroLogicalRelations.lean#L38 -- in LAbs, I shift the environment and extend it before evaluating the shadowed term t.

I think this extension upsets proving termination, but I'm not exactly sure how to remedy it.

Joachim Breitner (Dec 27 2023 at 20:23):

Hi and welcome!

Unrelated note: your github repository contains the .lake file; it is usually not added to the repository.

I had to add import Mathlib to the file to be able to load it.

McCoy R. Becker (Dec 27 2023 at 20:23):

thanks for these tips! Will adapt my code to this

Joachim Breitner (Dec 27 2023 at 20:28):

The error message mentions termination_by, and if you think your function is recursing on the size of the term, you can add

termination_by eval env t => t

after the function. But then the recursive call eval env (subst 0 v_eval t) complains.

At this point, maybe step back and ponder if eval really _is_ terminating, or not. It’s an evaluator for lambda terms, so probably not… in which case, def eval won’t work like this.

Maybe others have good domain-specific advise for what to do instead :-)

Mario Carneiro (Dec 27 2023 at 20:39):

Well the obvious solution is to use partial def eval instead

McCoy R. Becker (Dec 27 2023 at 20:42):

Interesting, thanks for the suggestion -- I'll explore it.

I'm trying to understand if (a) I've messed up the interpreter, or (b) it is written correctly, but I need to use well-founded recursion or (c) if partial def will work for my needs.

Mario Carneiro (Dec 27 2023 at 20:43):

An interpreter for lambda calculus is not terminating

Mario Carneiro (Dec 27 2023 at 20:43):

so yes this is written correctly and you should not expect to be able to prove termination

Mario Carneiro (Dec 27 2023 at 20:48):

As for whether partial def will work for your needs, that depends on what you want to do with this definition. If you just want to run it using #eval or compile it then yes this will work fine. If you want to prove properties about this function then no it won't work, because partial defs are opaque, you can't prove anything about them beyond their type. If you want to have both runnable code and something you can prove properties about it will be somewhat more complicated, you will need some predicate asserting that the input terminates, or alternatively you can use a fuel variable to make the definition "time out" after running for a while (and therefore make it terminating by fiat)

Joachim Breitner (Dec 27 2023 at 20:49):

partial def eval means you can run the interpreter, but not prove anything about it – depends on what… whatever Mario says :-)

McCoy R. Becker (Dec 27 2023 at 20:52):

I'm interesting in proving things. Fuel makes sense here.

McCoy R. Becker (Dec 27 2023 at 20:54):

Thanks for the ideas!

McCoy R. Becker (Dec 27 2023 at 22:22):

This _seems_ better, but still doesn't pass:

def eval :  -> Env  Term  Term
| 0, _, t@(_) => t
| fuel, env, Term.Nat n => Term.Nat n
| fuel, env, Term.Var n => (lookupEnv env n).getD (Term.Var n)
| fuel, env, Term.Abs t => Term.Abs (eval (fuel - 1) (Term.Var 0 :: env.map (shift 1)) t)
| fuel, env, Term.Prim f => Term.Prim f
| fuel, env, (Term.App (Term.Abs t) v) =>
  let v_eval := eval (fuel - 1) env v;
  eval (fuel - 1) env (subst 0 v_eval t)
| fuel, env, (Term.App t1 t2) =>
  let t1_eval := eval (fuel - 1) env t1;
  let t2_eval := eval (fuel - 1) env t2;
  eval (fuel - 1) env (Term.App t1_eval t2_eval)

termination_by eval fuel env t => fuel

Perhaps I'm not using the match guard / check correctly?

Mario Carneiro (Dec 27 2023 at 22:24):

use fuel+1 on the left rather than fuel-1 on the right

Mario Carneiro (Dec 27 2023 at 22:25):

def eval :  -> Env  Term  Term
| 0, _, t@(_) => t
| fuel+1, env, Term.Nat n => Term.Nat n
| fuel+1, env, Term.Var n => (lookupEnv env n).getD (Term.Var n)
| fuel+1, env, Term.Abs t => Term.Abs (eval fuel (Term.Var 0 :: env.map (shift 1)) t)
| fuel+1, env, Term.Prim f => Term.Prim f
| fuel+1, env, (Term.App (Term.Abs t) v) =>
  let v_eval := eval fuel env v;
  eval fuel env (subst 0 v_eval t)
| fuel+1, env, (Term.App t1 t2) =>
  let t1_eval := eval fuel env t1;
  let t2_eval := eval fuel env t2;
  eval fuel env (Term.App t1_eval t2_eval)

Mario Carneiro (Dec 27 2023 at 22:25):

no need for termination_by either

McCoy R. Becker (Dec 27 2023 at 22:25):

huh, that's interesting -- any intuition?

Mario Carneiro (Dec 27 2023 at 22:26):

This is a definition by structural recursion on Nat, so you want to use the constructors of Nat which are 0 and n+1

Mario Carneiro (Dec 27 2023 at 22:26):

besides this, fuel - 1 is not necessarily smaller than fuel because 0 - 1 = 0

McCoy R. Becker (Dec 27 2023 at 22:26):

Ah, great explanation, thank you

Mario Carneiro (Dec 27 2023 at 22:26):

You have a 0 case, but the typechecker doesn't know that


Last updated: May 02 2025 at 03:31 UTC