Zulip Chat Archive

Stream: lean4

Topic: Inspect Tail Recursion


Scott Viteri (Apr 23 2021 at 14:41):

Is there a quick way to check if Lean has recognized that a function is tail recursive? (Besides running it on large examples and trying to crash Lean)

Mario Carneiro (Apr 23 2021 at 14:53):

I did this:

set_option trace.compiler.ir.result true

def tail_recursive : Nat  Nat
  | 0 => 0
  | (n+1) => tail_recursive n

def not_tail_recursive : Nat  Nat
  | 0 => 0
  | (n+1) => not_tail_recursive n + 1

The trace.compiler.ir.result printout is pretty close to the lowest level that is still lean IR, but it's still fairly functional so the tail recursion is not explicit. But if you compile to C using

lean --c="test.c" test.lean
lean_object* l_tail__recursive(lean_object* x_1) {
_start:
{
lean_object* x_2; uint8_t x_3;
x_2 = lean_unsigned_to_nat(0u);
x_3 = lean_nat_dec_eq(x_1, x_2);
if (x_3 == 0)
{
lean_object* x_4; lean_object* x_5;
x_4 = lean_unsigned_to_nat(1u);
x_5 = lean_nat_sub(x_1, x_4);
lean_dec(x_1);
x_1 = x_5;
goto _start;
}
else
{
lean_object* x_7;
lean_dec(x_1);
x_7 = lean_unsigned_to_nat(0u);
return x_7;
}
}
}

lean_object* l_not__tail__recursive(lean_object* x_1) {
_start:
{
lean_object* x_2; uint8_t x_3;
x_2 = lean_unsigned_to_nat(0u);
x_3 = lean_nat_dec_eq(x_1, x_2);
if (x_3 == 0)
{
lean_object* x_4; lean_object* x_5; lean_object* x_6; lean_object* x_7;
x_4 = lean_unsigned_to_nat(1u);
x_5 = lean_nat_sub(x_1, x_4);
x_6 = l_not__tail__recursive(x_5);
lean_dec(x_5);
x_7 = lean_nat_add(x_6, x_4);
lean_dec(x_6);
return x_7;
}
else
{
lean_object* x_8;
x_8 = lean_unsigned_to_nat(0u);
return x_8;
}
}
}

you can see that the l_tail__recursive function contains a goto _start while l_not__tail__recursive contains a recursive call

Scott Viteri (Apr 23 2021 at 14:56):

wow, this is great

Scott Viteri (Apr 23 2021 at 15:02):

What do I need to do to view the result of the ir option once I set it to true

Mario Carneiro (Apr 23 2021 at 15:03):

on vscode it pops up in the output panel; in the console it prints to stdout

Scott Viteri (Apr 23 2021 at 15:05):

I'm finding it on emacs lsp::stderr

Scott Viteri (Apr 23 2021 at 15:13):

[result]
def tail_recursive.match_1._rarg (x_1 : @& obj) (x_2 : obj) (x_3 : obj) : obj :=
  let x_4 : obj := 0;
  let x_5 : u8 := Nat.decEq x_1 x_4;
  case x_5 : u8 of
  Bool.false 
    dec x_2;
    let x_6 : obj := 1;
    let x_7 : obj := Nat.sub x_1 x_6;
    let x_8 : obj := app x_3 x_7;
    ret x_8
  Bool.true 
    dec x_3;
    let x_9 : obj := ctor_0[PUnit.unit];
    let x_10 : obj := app x_2 x_9;
    ret x_10
def tail_recursive.match_1 (x_1 : ) : obj :=
  let x_2 : obj := pap tail_recursive.match_1._rarg._boxed;
  ret x_2
def tail_recursive.match_1._rarg._boxed (x_1 : obj) (x_2 : obj) (x_3 : obj) : obj :=
  let x_4 : obj := tail_recursive.match_1._rarg x_1 x_2 x_3;
  dec x_1;
  ret x_4
[result]
def tail_recursive (x_1 : obj) : obj :=
  let x_2 : obj := 0;
  let x_3 : u8 := Nat.decEq x_1 x_2;
  case x_3 : u8 of
  Bool.false 
    let x_4 : obj := 1;
    let x_5 : obj := Nat.sub x_1 x_4;
    dec x_1;
    let x_6 : obj := tail_recursive x_5;
    ret x_6
  Bool.true 
    dec x_1;
    let x_7 : obj := 0;
    ret x_7

is the ir result

Scott Viteri (Apr 23 2021 at 15:15):

the "def tail_recursive" part makes sense to me though it does not show tail recursion. What is the purpose of the other "match" functions here?

Mario Carneiro (Apr 23 2021 at 15:26):

tail_recursive.match_1 was generated by the equation compiler, and the compiler just compiled that definition too

def tail_recursive.match_1.{u_1} : (motive : Nat  Sort u_1) 
  (x : Nat)  (Unit  motive 0)  ((n : Nat)  motive (Nat.succ n))  motive x :=
fun (motive : Nat  Sort u_1) (x : Nat) (h_1 : Unit  motive 0) (h_2 : (n : Nat)  motive (Nat.succ n)) =>
  Nat.casesOn x (h_1 Unit.unit) fun (n : Nat) => h_2 n

Mario Carneiro (Apr 23 2021 at 15:27):

the auxiliaries tail_recursive.match_1._rarg._boxed and tail_recursive.match_1._rarg were made by the compiler, but match_1 already existed

Jakob von Raumer (Apr 23 2021 at 15:49):

Is the compiled version of tail_recursive.match_1 optimized away or does it still exist in the fully compiled code?

Mario Carneiro (Apr 23 2021 at 15:52):

I believe the compiler will not use the definition of tail_recursive that lean sees but will directly compile an equation compiler definition to a general recursion

Mario Carneiro (Apr 23 2021 at 15:56):

In fact, right now the compiler doesn't know how to compile functions written directly by recursors:

def tail_recursive : Nat  Nat :=
  fun x => Nat.recOn (motive := λ _ => Nat) x 0 (fun _ ih => ih)
-- code generator does not support recursor 'Nat.recOn' yet, consider using 'match ... with' and/or structural recursion

So the equation compiler metadata is actually essential, at least for the present

Scott Viteri (Apr 23 2021 at 15:59):

#check tail_recursive.match_1 succeeds, but #check tail_recursive.match_1._rarg doesn't. So the generated functions are not one-to-one with the ir functions.

Scott Viteri (Apr 23 2021 at 16:01):

Oh that's what you mean by "match_1" already existed.

Scott Viteri (Apr 23 2021 at 16:04):

The Unit -> motive 0 part of match_1 is strange to me. Why is it not just motive 0 in the type signature?

Scott Viteri (Apr 23 2021 at 16:11):

Is the point of match_1 to create a lower level representation of your definition by removing match statements?

Mario Carneiro (Apr 23 2021 at 16:19):

There are definitions that are generated by the equation compiler, like tail_recursive.match_1, and definitions that are generated by the (lean-to-C) compiler, like tail_recursive.match_1._rarg.

  • The first class of definitions are produced by macros, tactics, and constructs like match. They appear in the environment, are checked by the kernel, and are in that sense "real" definitions.
  • The second class of definitions are created in the course of lowering code for generating machine code. Things like lifted lambda bodies will show up here as separate definitions because ultimately they will need to be separate functions in C. These are never touched by the kernel and are not added to the environment, although I don't think there is any fundamental reason why they couldn't be added; it's just not necessary. They are also not regular lean definitions - the IR syntax is not a term of type expr, even though it has a syntax that looks a lot like lean. (For example case x : u8 of ... is not a thing in lean, but it is in the IR.)

Mario Carneiro (Apr 23 2021 at 16:26):

Scott Viteri said:

The Unit -> motive 0 part of match_1 is strange to me. Why is it not just motive 0 in the type signature?

If I had to guess, I think this is to ensure that evaluating tail_recursive.match_1 (x+1) (fun _ => e1) (fun n => e2 n) does not require evaluating e1. This is handled by the evaluation semantics of Nat.casesOn directly, but for functions wrapping casesOn you get the default eager evaluation semantics, so it is important to introduce a thunk on any cases for constructors with no arguments, so that the case for that constructor is not unnecessarily evaluated.

Scott Viteri (Apr 23 2021 at 17:44):

This makes sense, thank you. Will match statements always turn into casesOn? Why not recOn?

Scott Viteri (Apr 23 2021 at 18:51):

hmm it also does support not rec directly

def tail_recursive'' : Nat  Nat :=
  fun x => Nat.rec (motive := λ _ => Nat) 0 (fun _ ih => ih) x

though casesOn uses rec under the hood.

Scott Viteri (Apr 23 2021 at 19:17):

Explained here: https://leanprover.zulipchat.com/#narrow/stream/270676-lean4/topic/type.20mismatch.20error


Last updated: Dec 20 2023 at 11:08 UTC