Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wrong staging level for quoted structural/refinement type with type member #22648

Open
TomasMikula opened this issue Feb 23, 2025 · 11 comments
Open
Labels
area:metaprogramming:quotes Issues related to quotes and splices itype:bug

Comments

@TomasMikula
Copy link
Contributor

Compiler version

3.6.3

Minimized code

import scala.quoted.*

transparent inline def foo =
  ${ fooImpl }

def fooImpl(using Quotes): Expr[Any] =
  '{
    new AnyRef {
      type T = Unit
      def make: T = ()
      def take(t: T): Unit = ()
    }: {
      type T
      def make: T
      def take(t: T): Unit
    }
  }

Output

% ~/Downloads/scala3-3.6.3-aarch64-apple-darwin/bin/scalac test.scala
-- Error: test.scala:14:16 -----------------------------------------------------
14 |      def make: T
   |                ^
   |                access to trait <refinement> from wrong staging level:
   |                 - the definition is at level 0,
   |                 - but the access is at level 1
-- Error: test.scala:15:22 -----------------------------------------------------
15 |      def take(t: T): Unit
   |                      ^^^^
   |                  access to trait <refinement> from wrong staging level:
   |                   - the definition is at level 0,
   |                   - but the access is at level 1
-- Error: test.scala:15:18 -----------------------------------------------------
15 |      def take(t: T): Unit
   |                  ^
   |                  access to trait <refinement> from wrong staging level:
   |                   - the definition is at level 0,
   |                   - but the access is at level 1
3 errors found

Expectation

Should compile, as there is no stage mismatch.

A workaround would be appreciated.

@TomasMikula TomasMikula added itype:bug stat:needs triage Every issue needs to have an "area" and "itype" label labels Feb 23, 2025
@Gedochao Gedochao added area:metaprogramming:quotes Issues related to quotes and splices and removed stat:needs triage Every issue needs to have an "area" and "itype" label labels Feb 24, 2025
@TomasMikula
Copy link
Contributor Author

In search of a workaround, how could I create a type like

Object {
  type T
  def make: T
}

manually, i.e. using the reflect API?

FWIW, here's a failed attempt:

RecursiveType(self =>
  val tp0 = TypeRepr.of[Object]
  val tp1 = Refinement(tp0, "T", TypeBounds.empty)
  val tp2 = Refinement(tp1, "make", MethodType(MethodTypeKind.Plain)(paramNames = Nil)(
    mt => Nil,
    mt => ???, // How to refer to type `T` in the return type of `make`?
               // Note that there's no TypeRef constructor to construct TypeRef(self.recThis, "T")
  ))
  tp2
)

Thanks for any suggestions.

@prolativ
Copy link
Contributor

What's your use case, actually?
In the simplest case you could define a trait and then instantiate it with an anonymous class in a macro, e.g.

import scala.quoted.*

trait MakerTaker:
  type T
  def make: T
  def take(t: T): Unit

transparent inline def foo =
  ${ fooImpl }

def fooImpl(using Quotes): Expr[MakerTaker] =
  '{
    new MakerTaker {
      type T = Unit
      def make: T = ()
      def take(t: T): Unit = ()
    }
  }

If you need foo to return a structural type with completely arbitrary members, you would need fooImpl to return an instance of Selectable with a refinement.

@TomasMikula
Copy link
Contributor Author

TomasMikula commented Feb 25, 2025

It's the latter, i.e. completely arbitrary members, incl. type members. I don't think Selectable is that much of a help, as

  1. I still need to synthesize the type ascription of the structurally typed value (which is where my quoting snippet actually fails).
  2. selectDynamic only accesses value members, not type members.

(I did realize quotes are going to be too limited for generating the code from dynamic data anyway, but still, it is a legit bug, and still, I don't have a way of defining the type ascription via reflect API either.)

I seem to be getting some mileage from

dotty.tools.dotc.core.Types.TypeRef(owner, name)
  .asInstanceOf[TypeRepr]

but I'm sure I'm shooting myself in the foot.

What's your use case, actually?

Read a schema specification file (like OpenAPI) and get a typed Scala API, without a separate codegen pass. The type definitions from the spec would be the type members of my structural type.

@prolativ
Copy link
Contributor

Actually, it seems like the original snippet almost compile

import scala.quoted.*

transparent inline def foo =
  ${ fooImpl }

def fooImpl(using Quotes): Expr[Any] =
  '{
    new AnyRef {
      type T = Unit
      def make: T = ()
      def take(t: T): Unit = ()
    }: {
      type T = Unit
      def make: T
      def take(t: T): Unit
    }
  }

Here I only added = Unit in the type ascription. Then foo preserves the refinement as expected. But trying to invoke foo.make by itself still wouldn't work by itself because that would some way of handling reflection (e.g. with Selectable, as mentioned before).
I guess the problem in the original snippet might be because the compiler treats type T without a following = ... as a type variable (if that's the right term for that) from the splice's point of view rather than an abstract type member.

@TomasMikula
Copy link
Contributor Author

That's an interesting observation.

In fact, I can run your snippet with an additional import:

import scala.reflect.Selectable.reflectiveSelectable

println(foo.make)

(prints (), as expected).

However, I indeed want to keep the type T abstract for client code.

@prolativ
Copy link
Contributor

You can still abstract over the actual value of T, e.g.

import scala.quoted.*

transparent inline def foo[A](inline value: A) =
  ${ fooImpl[A]('value) }

def fooImpl[A : Type](value: Expr[A])(using Quotes): Expr[Any] =
  value match
    case '{ $v: t } =>
      '{
        new AnyRef {
          type T = t
          def make: T = $v
          def take(t: T): Unit = ()
        }: {
          type T = t
          def make: T
          def take(t: T): Unit
        }
      }

Here, t doesn't have to be Unit

@TomasMikula
Copy link
Contributor Author

Though the user of the generated code would still see what T was. I want T to stay abstract from the user's point of view.

@prolativ
Copy link
Contributor

How about this workaround then?

import scala.quoted.*

transparent inline def foo =
  ${ fooImpl }

def fooImpl(using Quotes): Expr[Any] =
  type MakerTaker = {
    type T
    def make: T
    def take(t: T): Unit
  }

  '{
    new AnyRef {
      type T = Unit
      def make: T = ()
      def take(t: T): Unit = ()
    }: MakerTaker
  }

@TomasMikula
Copy link
Contributor Author

That compiles, but doesn't work - prints null instead of (). Probably a different bug?

workaround.scala :
[your snippet]

go.scala :

import scala.reflect.Selectable.reflectiveSelectable

println(foo.make)
% scala workaround.scala go.sc
Warning: setting /Users/tomas/tmp/wrong-staging-level as the project root directory for this run.
Compiling project (Scala 3.6.3, JVM (21))
Compiled project (Scala 3.6.3, JVM (21))
Compiling project (Scala 3.6.3, JVM (21))
Compiled project (Scala 3.6.3, JVM (21))
null

@prolativ
Copy link
Contributor

I guess that's because how the refinement with an abstract T is handled by type erasure in combination with how reflectiveSelectable works and in result () somehow gets transformed into null (maybe there's something about primitive boxing in the way?)
I tried this with List[Int] and Nil:

import scala.quoted.*

transparent inline def foo =
  ${ fooImpl }

def fooImpl(using Quotes): Expr[Any] =
  type MakerTaker = {
    type T
    def make: T
    def take(t: T): Unit
  }

  '{
    new AnyRef {
      type T = List[Int]
      def make: T = Nil
      def take(t: T): Unit = ()
    }: MakerTaker
  }

and in this case foo.make returns an empty list, as expected.

@TomasMikula
Copy link
Contributor Author

TomasMikula commented Feb 26, 2025

Hmm, so a special (and incorrect) handling of Unit? 🤔

Other than that, it does look like a working workaround.


Unfortunately though, through no fault of macros/quotes, it doesn't get me where I want to go, as even without macros

click for the no-macro version
import scala.reflect.Selectable.reflectiveSelectable

val Foo =
  new AnyRef {
    type T = List[Int]
    def make: T = Nil
    def take(t: T): Unit = ()
  }: {
    type T
    def make: T
    def take(t: T): Unit
  }

println(Foo.take(Foo.make))
Foo.take(Foo.make)

gives me

[error] Structural access not allowed on method take because it has a parameter type with an unstable erasure
[error] println(Foo.take(Foo.make))
[error]         ^^^^^^^^^^^^^^^^^^

🤷‍♂.
But that's for a different discussion.

UPDATE: Extra boxing does resolve that.

import scala.reflect.Selectable.reflectiveSelectable

case class Box[T](value: T)

val Foo =
  new AnyRef {
    type T = List[Int]
    def make: Box[T] = Box(Nil)
    def take(t: Box[T]): Unit = ()
  }: {
    type T
    def make: Box[T]
    def take(t: Box[T]): Unit
  }

println(Foo.take(Foo.make))
% scala nomacros.sc
Compiling project (Scala 3.6.3, JVM (21))
Compiled project (Scala 3.6.3, JVM (21))
()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area:metaprogramming:quotes Issues related to quotes and splices itype:bug
Projects
None yet
Development

No branches or pull requests

3 participants