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

Make HOAS Quote pattern match with def method capture #17567

Merged
merged 1 commit into from Jun 21, 2023

Conversation

zeptometer
Copy link
Contributor

@zeptometer zeptometer commented May 24, 2023

This PR will fix #17105 by extracting symbols from eta-expanded identifiers.

This fix enables the use of patterns such as

case '{ def f(...): T = ...; $g(f): U } => 

where g will match any expression that may contain references to f.

@nicolasstucki nicolasstucki self-requested a review May 24, 2023 07:39
@nicolasstucki nicolasstucki self-assigned this May 24, 2023
@nicolasstucki nicolasstucki requested review from nicolasstucki and removed request for nicolasstucki May 24, 2023 07:39
compiler/src/scala/quoted/runtime/impl/QuoteMatcher.scala Outdated Show resolved Hide resolved
val capturedArgs = args.map(_.symbol)
val captureEnv = env.filter((k, v) => !capturedArgs.contains(v))
val capturedIds = args.map(getCapturedIdent)
val capturedSymbols = capturedIds.map(_.symbol)
Copy link
Contributor

Choose a reason for hiding this comment

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

getCapturedIdent could return the Symbol directly. This way, we can avoid this extra map.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here I intended to use capturedIds as a parameter to matchedOpen and we cannot omit it (we get compiler errors if we use args for matchedOpen instead).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

According to your other comment on i17105.check, it's likely I need to change this part anyway.

Copy link
Contributor

Choose a reason for hiding this comment

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

These outputs show that we need to adapt the type of g in the lambdas. For example g: (y: scala.Int)scala.Int should be g: scala.Int => scala.Int. We probably need to addapt the type here
https://github.com/lampepfl/dotty/blob/main/compiler/src/scala/quoted/runtime/impl/QuoteMatcher.scala#LL468C13-L468C13. We need to convert MethodTypes into funtion types. The defn.FunctionOf method might be useful here (defined in Definitions.scala).

I would expect this output to look like

((g: scala.Int => scala.Int, n: scala.Int) => g(n))
((g: (scala.Int, scala.Int) => scala.Int) => g(1, 2))
((g2: (scala.math.Ordered[scala.Int]) => scala.Boolean, ord: scala.math.Ordered[scala.Int]) => (g2(ord).||(2.<(3)): scala.Boolean))

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you, I'll try to fix this issue (and add appropriate test cases)

Copy link
Contributor

Choose a reason for hiding this comment

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

Int => Int could also be scala.Function1[scala.Int, scala.Int]. From Function0 up to Function22 we have concrete FnunctionN classes that implement the lambda type. T => R is equivalent to Function1[T, R] in the Scala type system. This only applies to normal function parameters, not if the argument is implicit as in def f(using T): R = ... or T ?=> R (similar for functions that contain erased parameters).

@zeptometer
Copy link
Contributor Author

zeptometer commented Jun 3, 2023

Hi @nicolasstucki, I'm trying to fix the issue that types of parameters in higher-order term holes being method types with the commit 209d763.

However, the current hotfix fails even with the simplest test case (I used this test case). I got the following error

-- Error: /home/yuito/code/dotty-workspace/issue-17105/test_2.scala:7:43 -------
7 |  testExpr { def f(x: Int) = "hello" * x; f(0) + "bye" }
  |                                          ^^^^
  |                                      parameter g does not take parameters

I found this message is caused at a method in QuoteMatcher.scala:l524, which generates a method body from a quote that matched against higher-order term pattern.

        def bodyFn(lambdaArgss: List[List[Tree]]): Tree = {
          val argsMap = args.view.map(_.symbol).zip(lambdaArgss.head).toMap
          val body = new TreeMap {
            override def transform(tree: Tree)(using Context): Tree =
              tree match
                case tree: Ident => env.get(tree.symbol).flatMap(argsMap.get).getOrElse(tree)
                case tree => super.transform(tree)
          }.transform(tree)
          TreeOps(body).changeNonLocalOwners(meth)
        }

In the test case, the matched body is f(0) + ("bye") and the current bodyFn turns it into g(0) + ("bye") (given f maps to g). However, here g has a function type Int => String whereas f has a method type (y: Int) => String: hence the method call g(0) fails (because only methods are allowed to apply at this stage... as far as I understand).

To avoid this issue, we want to refine bodyFn to convert f(0) to g.apply(0). I guess it will be something like this

              tree match
                case Apply(fnId: Ident, args) => {
                  val fnId2 = env.get(tree.symbol).flatMap(argsMap.get).getOrElse(tree)
                  ??? // want to return '{$fnId2.apply($args)} Not sure what is desirable way to construct Tree here...
                }
                case tree => super.transform(tree)

That's something I'd like your feedback. Does it sound valid to you? If so, what can I fill for ??? part?

@zeptometer
Copy link
Contributor Author

zeptometer commented Jun 4, 2023

I managed to fix a part of issue via c7a88fa and added a test case in 8d73b97👍 I'll try to generalize the fix to accommodate other patterns.

// TODO issue-17105: Is this pattern appropriate for methods?
case tp: MethodOrPoly => cpy.Apply(tree)(tfun, targs)
// TODO issue-17105: Appropriate pattern for function (appliable?) types?
case _ => ctx.typer.typed(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is this the right way to construct a typed tree?

val targs = transform(args)
tfun.tpe match
// TODO issue-17105: Is this pattern appropriate for methods?
case tp: MethodOrPoly => cpy.Apply(tree)(tfun, targs)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is this the right pattern to match against methods?

case Apply(methId: Ident, args) =>
env.get(tree.symbol).flatMap(argsMap.get)
.map(fnId => ctx.typer.typed(
untpd.Apply(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is this the right pattern to construct typed tree?

Copy link
Contributor

Choose a reason for hiding this comment

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

Typing an untyped tree is dangerous. Depending on the context where this is typed, we could end up with different result. It is also a bit inefficient.

The correct way to do this is to create the typed tree directly. In this case you can use the select and appliedToArgs helper methods to construct this tree. The code should be fnId.select(nme.apply).appliedToArgs(args)).

tests/run-macros/i17105/Macro_1.scala Outdated Show resolved Hide resolved
case Apply(methId: Ident, args) =>
env.get(tree.symbol).flatMap(argsMap.get)
.map(fnId => ctx.typer.typed(
untpd.Apply(
Copy link
Contributor

Choose a reason for hiding this comment

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

Typing an untyped tree is dangerous. Depending on the context where this is typed, we could end up with different result. It is also a bit inefficient.

The correct way to do this is to create the typed tree directly. In this case you can use the select and appliedToArgs helper methods to construct this tree. The code should be fnId.select(nme.apply).appliedToArgs(args)).

compiler/src/scala/quoted/runtime/impl/QuoteMatcher.scala Outdated Show resolved Hide resolved
compiler/src/scala/quoted/runtime/impl/QuoteMatcher.scala Outdated Show resolved Hide resolved
case Apply(fun, args) =>
val tfun = transform(fun)
val targs = transform(args)
(fun.tpe, tfun.tpe) match
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do we want to use fun.tpe.widenTermRefExpr in this case?

compiler/src/scala/quoted/runtime/impl/QuoteMatcher.scala Outdated Show resolved Hide resolved
val (pEnv, pmatch) = matchParamss(paramss1, paramss2)
val defEnv = pEnv + (scrutinee.symbol -> pattern.symbol)

ematch
Copy link
Contributor Author

Choose a reason for hiding this comment

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

ematch is always Seq.empty and pmatch should also return Seq.empty on match.
Should I remove the part ematch &&& pmatch &&& to simplify logic?

@zeptometer
Copy link
Contributor Author

zeptometer commented Jun 18, 2023

The latest test report says that the test case quote-nested-6.scala fails, but it passes in my environment https://github.com/lampepfl/dotty/actions/runs/5298658706/jobs/9591140406?pr=17567

Output from 'tests/run-staging/quote-nested-6.scala' did not match check file. Actual output:
{
  type T[X] = [A >: scala.Nothing <: scala.Any] =>> scala.collection.immutable.List[A][X]
  val x: java.lang.String = "foo"
  val z: [X >: scala.Nothing <: scala.Any] =>> scala.collection.immutable.List[X][java.lang.String] = scala.List.apply[java.lang.String](x)

  (x: java.lang.String)
}


Test output dumped in: tests/run-staging/quote-nested-6.check.out
  See diff of the checkfile (`brew install icdiff` for colored diff)
    > diff tests/run-staging/quote-nested-6.check tests/run-staging/quote-nested-6.check.out
  Replace checkfile with current output
    > mv tests/run-staging/quote-nested-6.check.out tests/run-staging/quote-nested-6.check
      
foo

The issue is that I cannot reproduce this failure in my environment. It might be due to different JDK (I'm using OpenJDK 11). I'll try switching OpenJDK 16.

[update] Hmm, tests keep passing with OpenJDK 16.

@nicolasstucki
Copy link
Contributor

The failure of quote-nested-6.scala was unrelated to this PR. This broke on that main branch due to a conflict between to other PRs, see #17993.

@zeptometer zeptometer marked this pull request as ready for review June 19, 2023 13:46
@zeptometer zeptometer changed the title WIP: Issue 17105 Make HOAS Quote pattern match with def method capture Jun 19, 2023
Copy link
Contributor

@nicolasstucki nicolasstucki left a comment

Choose a reason for hiding this comment

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

Otherwise LGTM

compiler/src/scala/quoted/runtime/impl/QuoteMatcher.scala Outdated Show resolved Hide resolved
case OpenTree(tree: Tree, patternTpe: Type, argIds: List[Tree], argTypes: List[Type], env: Env)

/** The Definitions object */
def defn(using Context): Definitions = ctx.definitions
Copy link
Contributor

Choose a reason for hiding this comment

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

defn should have been imported by import dotty.tools.dotc.core.Symbols.*. This definition should be removed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you, I've followed your advice.

@nicolasstucki nicolasstucki merged commit 0d8bdd6 into scala:main Jun 21, 2023
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

HOAS quote pattern fails to match with def caprure
2 participants