Skip to content

[Scala3] Add Definition/Instance API#5277

Open
adkian-sifive wants to merge 2 commits intomainfrom
adkian-sifive/scala3-di
Open

[Scala3] Add Definition/Instance API#5277
adkian-sifive wants to merge 2 commits intomainfrom
adkian-sifive/scala3-di

Conversation

@adkian-sifive
Copy link
Copy Markdown
Contributor

@adkian-sifive adkian-sifive commented Apr 20, 2026

Tested locally with (a working subset of) InstanceSpec and DefinitionSpec - will move all tests once other unsupported APIs are ported

Contributor Checklist

  • Did you add Scaladoc to every public function/method?
  • Did you add at least one test demonstrating the PR?
  • Did you delete any extraneous printlns/debugging code?
  • Did you specify the type of improvement?
  • Did you add appropriate documentation in docs/src?
  • Did you request a desired merge strategy?
  • Did you add text to be included in the Release Notes for this change?

Type of Improvement

  • Feature (or new API)

Desired Merge Strategy

  • Squash: The PR will be squashed and merged (choose this if you have no preference).

Release Notes

Reviewer Checklist (only modified by reviewer)

  • Did you add the appropriate labels? (Select the most appropriate one based on the "Type of Improvement")
  • Did you mark the proper milestone (Bug fix: 3.6.x, 5.x, or 6.x depending on impact, API modification or big change: 7.0)?
  • Did you review?
  • Did you check whether all relevant Contributor checkboxes have been checked?
  • Did you do one of the following when ready to merge:
    • Squash: You/ the contributor Enable auto-merge (squash) and clean up the commit message.
    • Merge: Ensure that contributor has cleaned up their commit history, then merge with Create a merge commit.

@adkian-sifive adkian-sifive added the Scala 3 Changes related to upgrading to Scala 3 label Apr 20, 2026
@adkian-sifive adkian-sifive mentioned this pull request Apr 22, 2026
14 tasks
Comment on lines +18 to +37
// The transparent inline here ensures the precise return type
// propagates to the call site
transparent inline def selectDynamic(inline name: String): Any = {
${
HierarchyLookupMacro.selectDynamicImpl[A](
'{ this.asInstanceOf[Hierarchy[A]] },
'name
)
}
}

transparent inline def applyDynamic(inline name: String)(inline args: Any*): Any = {
${
HierarchyLookupMacro.applyDynamicImpl[A](
'{ this.asInstanceOf[Hierarchy[A]] },
'name,
'args
)
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The split between HierarchyProto and HierarchyIsA doesn't really make sense but the latter has a name that implies something very specified (support for .isA), so I think these methods should go on HierarchyProto (and we should ultimately meld HierarchyIsA into HierarchyProto).

val clzName = ct.runtimeClass.getCanonicalName
if (clzName == null) false
else inBaseClasses(modifyReplString(clzName))
}
Copy link
Copy Markdown
Contributor

@jackkoenig jackkoenig Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}
def isA[B](using ct: ClassTag[B]): Boolean = {
val clzName = ct.runtimeClass.getCanonicalName
if (clzName == null) false
else inBaseClasses(modifyReplString(clzName))

Don't use implicit in any Scala 3 code as it'll be annoying to switch to using later

Also consider using the syntactic sugar because I think it's nicer in API docs:

Suggested change
}
def isA[B : ClassTag]: Boolean = {
val clzName = summon[ClassTag[B]].runtimeClass.getCanonicalName
if (clzName == null) false
else inBaseClasses(modifyReplString(clzName))

/** Determine whether underlying proto is of type provided.
*
* @note IMPORTANT: this function requires summoning a TypeTag[B], which will fail if B is an inner class.
* @note IMPORTANT: this function requires summoning a ClassTag[B], which will fail if B is an inner class.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this statement still true? I asked Opus 4.7 to review and it said ClassTags can be summoned for inner classes, can we add a test for this and delete the note?

There's also a chance that we could capture the ClassTag for A in Scala 3 that might be able to make this whole thing work a lot better and just use <:<. We can save that for a follow on PR though.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another alternative is to use the new TypeTest, the following works:

case class Box[A](a: A) {
  def isA[B](using tt: TypeTest[A, B]): Boolean = tt.unapply(a).isDefined
}

Box(List(1, 2, 3)).isA[Seq[Int]] // true

That has same limitation about type parameterized types though. Maybe we should consider izumi-reflect?

import q.reflect.*
val argTerms = argsExpr match {
case Varargs(args) => args.toList.map(_.asTerm)
case _ => report.errorAndAbort("applyDynamic on an Instance requires explicit varargs")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this, can you add a comment explaining?

val fieldSym = typeSym.fieldMember(name) match {
case s if s != Symbol.noSymbol => s
case _ =>
typeSym.methodMember(name).find(_.paramSymss.flatten.isEmpty).getOrElse(Symbol.noSymbol)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment explaining that this looks for a method of the name with no arguments would be helpful.

I'm not completely sure why this is here though--we don't support @public on defs, maybe we hsould but this is not the place to change that. Now I will grant that it could be that the accessors are methods, but if that's the case we need to explain in comments.

import q.reflect.*
Lambda(
owner = Symbol.spliceOwner,
tpe = MethodType(List("a"))(_ => List(TypeRepr.of[A]), _ => TypeRepr.of[T]),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this mean? Like what meaning does "a" have in this context?

firstList.length == argTerms.length &&
firstList.lazyZip(argTerms).forall((p, a) => a.tpe <:< cTpe.memberType(p).widen)
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What apply method is this looking for? A comment explaining would be helpful.

Also it looks like it only finds the first one and only checks the first argument list. Do we know for sure that that is correct? If so, also needs to be explained in a comment.

Comment on lines +156 to +169
val finalTerm = applySym.paramSymss.zipWithIndex.foldLeft(initial) { case (acc, (_, idx)) =>
val args =
if (idx == 0) argTerms
else {
val paramTpes = acc.tpe.widen match {
case mt: MethodType => mt.paramTypes
case _ => Nil
}
paramTpes.map { pt =>
summonOrAbort(
pt,
explanation => s"could not summon implicit ${pt.show} when calling ${cTpe.show}.apply: $explanation"
)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appears to be assuming that only the first parameter list of whatever we're invoking is explicit and the rest are implicit, couldn't we have multiple parameter lists?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to this though, do we really need applyDynamic? When does that come up vs. select dynamic? A case that comes to mind is something like:

@instantiable
class MyModule extends RawModule {
  @public val xs = List(1, 2, 3)
}
// ...

val inst = Instantiate(new MyModule)
inst.xs(1) // Needs rewriting to inst._lookup(_.xs(using Lookupable.Aux[List[Int], List[Int]])).apply(1)

I can see this calling apply dyanmic so maybe that's what's going on. A comment explaining if that's the case would be good.

But my concern above still applies, what if you have a List[List[Int]]? There could be two applies in a row.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scala 3 Changes related to upgrading to Scala 3

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants