Skip to content

@tailrec and nested definitions #20105

Closed
@LucySMartin

Description

@LucySMartin

Compiler version

3.4.0
(also validated on near latest local build 3.5.0-RC1-bin-SNAPSHOT-nonbootstrapped-git-9a5b9b4

Minimized code

This compiles:

  @tailrec
  def foo(): Unit =
    def bar(): Unit =
      if (???)
        foo()
      else
        bar()
    bar()

This does not:

  @tailrec
  def foo(): Unit =
    def bar(): Unit =
      if (???)
        foo()
    bar()

Output

These should no compile, as there are no tail recursive calls within the outer @tailrec annotated block. It has erroneously detected that this is ok, due to the rewrite of the inner bar method. This should not compile, and if this were not annotated with @tailrec, the outer tailLabel should not be generated.

Looking into the one that does compile, it generates an unutilised label testLabel2[Unit]: , and fails to warn about having no tail
l recursion optimised calls on the to level method:

    @tailrec def foo(): Unit =
      {
        while <empty> do 
          tailLabel2[Unit]: 
            return
              {
                def bar(): Unit =
                  {
                    while <empty> do 
                      tailLabel1[Unit]: 
                        return
                          if ???() then Test.foo() else 
                            (return[tailLabel1] ()):Unit
                  }
                bar()
              }
      }

Expectation

For the behaviour between these two (weather this is an acceptable place to recur or not) to be consistent between these cases - or in the alternative - to support multi tiered tail recursion.

Notes

This compiles (and indeed rewrites to a loop) if the inner method is declared as inline. Potentially a rule that within annotated @tailrec methods, any inner methods that call the top level method are handled as inline for simple uses directly within this method?

I was having a play around, and while complex, it does look feasible that we could have some sort of rewrite that processes this as a single larger rewrite. What would people thing of optimiseing to something like this:

import scala.annotation.tailrec
import scala.util.boundary
import scala.util.boundary.break

object Test extends App {
    
  //println(a(0))
  println(a_tr(0))

  var count = 0

  def f(tNum: Int): Boolean = {
    count = count + 1
    if (count > 100000) {
      throw new Exception("Infinitely looping")
    }
    tNum == 4 || tNum == 1
  }

  @tailrec
  def a(i: Int): Int = {
    @tailrec
    def b(j: Int): Int = {
      if (f(1)) {
        a(j + 1)
      } else if (f(2)) {
        b(j + 1)
      } else {
        j
      }
    }

    if (f(3)) {
      a(i + 1)
    } else if (f(4)) {
      b(i + 1)
    } else {
      i
    }
  }

 //identical to a, but with tail recursion logic applied
  def a_tr(i: Int): Int = {
    class AExit
    class ATail

    var aRes: Int | Null = null

    boundary[AExit] {
      var aIState = i
      while (true) {
        boundary[ATail] {
          while (true) {
            def b(j: Int): Int = {
              var bJState = i
              while (true) {
                if (f(1)) {
                  aIState = bJState + 1
                  break(new ATail)
                } else if (f(2)) {
                  bJState = bJState + 1
                } else {
                  aRes = bJState
                  break(new AExit())
                }
              }
              ???
            }

            if (f(3)) {
              aIState = aIState + 1
            } else if (f(4)) {
              b(aIState + 1)
            } else {
              aRes = aIState
              break(new AExit)
            }
          }
          ???
        }
      }
      ???
    }

    aRes.nn
  }

}

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions