scala / scala3

The Scala 3 compiler, also known as Dotty.
https://dotty.epfl.ch
Apache License 2.0
5.84k stars 1.05k forks source link

Undefined reference when typing dependent annotation #21595

Open mbovel opened 4 weeks ago

mbovel commented 4 weeks ago

The following still fails:

class dummy(b: Boolean) extends annotation.StaticAnnotation

object Test:
  def foo(elem: Int, bla: Int @dummy(elem == 0)) = bla
-- Error: try/annn2.scala:9:37 -------------------------------------------------
  9 |  def foo(elem: Int, bla: Int @dummy(elem == 0)) = bla
    |                                     ^^^^^^^^^
    |          undefined: elem.== # -1: TermRef(TermParamRef(elem),==) at typer

The initial typing of the arguments work, but when we want to create the MethodType using fromSymbols we perform a substitution. We correctly substitute TermRef(NoPrefix, elem) by a TermParamRef representing the first parameter of the MethodType, the TypeMap goes back up one level, and creates new elem.== which can be done without forcing the underlying type of elem, but when we go back up one more level and create a new elem.==(0), the TypeAssigner for Apply forces the computation of the underlying type of elem.==, which in turn requires computing the underlying type of elem, where we get NoType because we're in the middle of computing paramInfos:

https://github.com/scala/scala3/blob/d490d13e0ccd4279b7586ab0236dd673f884d63f/compiler/src/dotty/tools/dotc/core/Types.scala#L4697-L4700

https://github.com/scala/scala3/blob/d490d13e0ccd4279b7586ab0236dd673f884d63f/compiler/src/dotty/tools/dotc/core/Types.scala#L4091

To avoid this issue it seems we'd need paramInfos to be filled progressively as we compute each parameter, so the second parameter can at least safely refer to the first one.

Originally posted by @smarter in https://github.com/scala/scala3/issues/19957#issuecomment-2305262773

mbovel commented 4 weeks ago

I tried to add a notion of "temporary parameter types", that would be used in MethodType before paramInfosare properly initialized: https://github.com/mbovel/dotty/tree/mb/21595. That seems hacky, but that's the smallest changset I can think of. What else could we do? Refactor MethodType more deeply so that it computes parameters one by one? Would you use a mutable sequence for that?

mbovel commented 4 weeks ago

@EugeneFlesselle and @natsukagami noted that we have a similar limitation with default parameters:

-- [E006] Not Found Error: 21595.scala:3:32 ------------------------------------
3 |  def foo(elem: Int, bla: Int = elem + 1) = bla
  |                                ^^^^
  |                                Not found: elem
  |
  | longer explanation available when compiling with `-explain`

A workaround for both is to use multiple parameter lists:

object Test:
  def foo(elem: Int)(bla: Int = elem + 1) = bla
class dummy(b: Boolean) extends annotation.StaticAnnotation

object Test:
  def foo(elem: Int)(bla: Int @dummy(elem == 0)) = bla
mbovel commented 4 weeks ago

(Note also that I don't have this problem on my qualified types experiments, because I have a custom annotation with a custom representation and its own mapWith. With these, the method resolution is never re-triggered. I wondering if this could be problematic in some situations.)

odersky commented 3 weeks ago

I am more and more convinced that the whole approach of letting annotations take arbitrary trees is doomed. We'll never get it to work properly in all cases. We should seriously consider switching to annotations taking types (including constant and singleton types), not trees.

noti0na1 commented 3 weeks ago

I had a similar issue with integrateRT in cc.Setup (for a different reason)!