Skip to content

There is no way to create Selectors in macros #21225

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

Closed
joan38 opened this issue Jul 18, 2024 · 3 comments · Fixed by #22457
Closed

There is no way to create Selectors in macros #21225

joan38 opened this issue Jul 18, 2024 · 3 comments · Fixed by #22457
Labels
area:metaprogramming:other Issues tied to metaprogramming/macros not covered by the other labels. itype:language enhancement
Milestone

Comments

@joan38
Copy link
Contributor

joan38 commented Jul 18, 2024

Compiler version

3.4.2

Minimized code

Import(Ident(???), List(GivenSelector()))

The apply function on GivenSelector or SimpleSelector or RenameSelector or OmitSelector does not exist.

Workaround

val givenSelector = '{ import DummyImplicit.given; val _ = summon[DummyImplicit] }.asTerm match
  case Inlined(_, _, Block(List(Import(_, List(givenSelector)), _), _)) => givenSelector

Import(Ident(???), List(givenSelector))

Expectation

We should have a way to create Selectors

Thanks

@joan38 joan38 added itype:bug stat:needs triage Every issue needs to have an "area" and "itype" label labels Jul 18, 2024
@bishabosha
Copy link
Member

bishabosha commented Jul 18, 2024

there is no utility to creating an import, it would be a no-op - why are you trying to do this?

@joan38
Copy link
Contributor Author

joan38 commented Jul 18, 2024

So following #21199 I was looking for a workaround and tried #21224. After failing on both I was like "ok, let's write the tree myself" which looks like:

val givenSelector = '{ import DummyImplicit.given; val _ = summon[DummyImplicit] }.asTerm match
  case Inlined(_, _, Block(List(Import(_, List(givenSelector)), _), _)) => givenSelector

findAllTypesInPackage(typesPackage).map: typeRepr =>
  typeRepr.asType match
    case '[t] =>
      val companionObject = typeRepr.typeSymbol.owner.declaredFields.find(sym =>
        sym.name == typeRepr.typeSymbol.name && sym.flags.is(Flags.Module)
      )
      val importCompanionGivens =
        companionObject.map(companionObject => Import(Ident(companionObject.termRef), List(givenSelector)))

      val summonParser = TypeApply(
        Ident(Symbol.requiredMethod("scala.compiletime.summonInline").termRef),
        List(Inferred(TypeRepr.of[Parser].appliedTo(typeRepr)))
      )
      val parser = Block(importCompanionGivens.toList, summonParser).asExprOf[Parser[t]]

      '{$parser()}

because the following does not resolve givens in companion objects:

findAllTypesInPackage(typesPackage).map(_.asType).map:
  case '[t] => '{summonInline[t]()}

I'm not even sure how I could create an import of the companion object of a type (if it exists) in a quote blocks.

@Gedochao Gedochao added itype:language enhancement area:metaprogramming:other Issues tied to metaprogramming/macros not covered by the other labels. and removed itype:bug stat:needs triage Every issue needs to have an "area" and "itype" label labels Jul 22, 2024
@ghik
Copy link
Contributor

ghik commented Jan 19, 2025

I think I have a valid use case for creating Import trees manually, and therefore I need a way to create Selectors.

Let's assume we have a typeclass with a derived macro:

trait Codec[T]
object Codec {
  inline def derived[T]: Codec[T] = ???
}

Now, I want to create an enhanced version of the derived macro, which automatically injects some implicits/givens, so that they don't have to be imported manually.

So, instead of doing this:

import CustomCodecs.given
case class Data(int: Int, str: String) derives Codec

I would prefer this:

case class Data(int: Int, str: String)
object Data {
  given codec: Codec[T] = Codec.derivedWithDeps[T](CustomCodecs)
}

or perhaps even something like this:

case class Data(int: Int, str: String)
object Data extends HasCodecWithDeps[Data](CustomCodecs)

Why do I think this is better?

  • It's less "magic" and more reusable
  • It avoids IDE problems where an import needed by a macro is often detected as unused

Here's how I successfully managed to implement derivedWithDeps:

import scala.quoted.*

trait Codec[T]
object Codec {
  inline def derived[T]: Codec[T] = ???

  inline def derivedWithDeps[T](deps: Any): Codec[T] = ${derivedWithDepsImpl[T]}

  private def derivedWithDepsImpl[T](deps: Expr[Any])(using q: Quotes)(using Type[T]): Expr[Codec[T]] = {
    import q.reflect.*

    val givenSelector: Selector = {
      import dotty.tools.dotc.ast.untpd
      import dotty.tools.dotc.core.Contexts.Context
      import dotty.tools.dotc.core.StdNames
      import scala.quoted.runtime.impl.QuotesImpl

      given ctx: Context = q.asInstanceOf[QuotesImpl].ctx
      untpd.ImportSelector(untpd.Ident(StdNames.nme.EMPTY)).asInstanceOf[Selector]
    }
    
    val theImport = Import(deps.asTerm, List(givenSelector))
    Block(List(theImport), '{Codec.derived[T]}.asTerm).asExprOf[Codec[T]]
  }
}

It works as intended, but of course the missing piece is an API to create the Selector without tapping into the raw compiler API.

Note: trying to do the same with plain quote doesn't work:

    '{
      import ${deps}.given
      Codec.derived[T]
    }

This syntax isn't even recognized as valid, so it looks like splicing an Expr into an import statement is just plain impossible.

ghik added a commit to ghik/scala3 that referenced this issue Jan 26, 2025
jchyb added a commit that referenced this issue Feb 7, 2025
@WojciechMazur WojciechMazur added this to the 3.7.0 milestone Mar 11, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area:metaprogramming:other Issues tied to metaprogramming/macros not covered by the other labels. itype:language enhancement
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants