Skip to content

Quote pattern matching runtime spec #8687

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

Merged
merged 2 commits into from
Apr 15, 2020

Conversation

nicolasstucki
Copy link
Contributor

A first draft of the evaluation semantics of quote pattern matching.

@nicolasstucki nicolasstucki requested a review from liufengyun April 7, 2020 15:30
@nicolasstucki nicolasstucki marked this pull request as ready for review April 8, 2020 06:54
Copy link
Contributor

@liufengyun liufengyun left a comment

Choose a reason for hiding this comment

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

I left some open discussion points:

  • about the semantics of wildcard pattern
  • about the semantics of local definitions
  • about whether we should allow pattern matching inside quoted code patterns

/cc : @biboudis @OlivierBlanvillain @anatoliykmetyuk


## Evaluation sematics

Bellow we provide an informal description of the pattern evailatuion.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
Bellow we provide an informal description of the pattern evailatuion.

Copy link
Contributor

Choose a reason for hiding this comment

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

Some short summary is probably a good idea, and apart from typos this seems good?

* `&&&` matches if both sides match. Concatenates the extracted expressions of both sides.

```scala
'{ e } =?= '{ $x } && typeOf('{e}) <:< typeOf('{$x}) && isColosedUnder()('{e}) ===> matched('{e})
Copy link
Contributor

Choose a reason for hiding this comment

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

This looks like to be a wildcard pattern, the type checking looks redundant.

The condition isColosedUnder()('{e}) is a little controversial. I see the motivation is for safety. The question is:

  • whether it is indeed safe
  • potentially high usability penalty here, which may out-weigh the concern of safety

Copy link
Contributor

Choose a reason for hiding this comment

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

matched('{e}) probably also means "match x to e", right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually $ should not be part of this part of the specification, here we deal with hole[T] which is the thing that is generated after typing the quote pattern.


```scala
'{ e } =?= '{ $x } && typeOf('{e}) <:< typeOf('{$x}) && isColosedUnder()('{e}) ===> matched('{e})
'{ e } =?= '{ $y(x1, ..., xn) } && isColosedUnder(x1, ... xn)('{e}) ===> matched(lift(x1, ..., xn)('{e}))
Copy link
Contributor

Choose a reason for hiding this comment

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

This quoted pattern is a little counter-intuitive, both syntactically and semantically. I'm not sure how useful and usable it is in practice.

Copy link
Contributor

Choose a reason for hiding this comment

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

It's a higher-order pattern variable (see https://dl.acm.org/doi/10.1145/3136040.3136043, section 5.7). I think it's quite intuitive in actual examples. But it's a pretty limited pattern, and as was discussed before in the group meeting, it could be better to go with a scope-tracking type system instead.

'{ val x: T = e1; e2 } =?= '{ val y: P = p1; p2 } ===> withEnv(x -> y)('[T] =?= '[P] &&& '{e1} =?= '{p1} &&& '{e2} =?= '{p2})
'{ def x0(x1: T1, ..., xn: Tn): T0 = e1; e2 } =?= '{ def y0(y1: P1, ..., yn: Pn): P0 = p1; p2 } ===> withEnv(x0 -> y0, ..., xn -> yn)('[T0] =?= '[P0] &&& ... &&& '[Tn] =?= '[Pn] &&& '{e1} =?= '{p1} &&& '{e2} =?= '{p2})

'{ e1; e2 } =?= '{ p1; p2 } ===> '{e1} =?= '{p1} &&& '{e2} =?= '{p2}
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems it can be removed, it's a special case of the block.


'{ {e0; e1; ...; en}; em } =?= '{ {p0; p1; ...; pm}; em } ===> '{ e0; {e1; ...; en; em} } =?= '{ p0; {p1; ...; pm; em} }
'{ val x: T = e1; e2 } =?= '{ val y: P = p1; p2 } ===> withEnv(x -> y)('[T] =?= '[P] &&& '{e1} =?= '{p1} &&& '{e2} =?= '{p2})
'{ def x0(x1: T1, ..., xn: Tn): T0 = e1; e2 } =?= '{ def y0(y1: P1, ..., yn: Pn): P0 = p1; p2 } ===> withEnv(x0 -> y0, ..., xn -> yn)('[T0] =?= '[P0] &&& ... &&& '[Tn] =?= '[Pn] &&& '{e1} =?= '{p1} &&& '{e2} =?= '{p2})
Copy link
Contributor

Choose a reason for hiding this comment

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

The semantics for local definitions is surprising. I see the intention is to avoid scope extrusion, but it seems to be a big usability penalty here.

Copy link
Contributor

Choose a reason for hiding this comment

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

@liufengyun What alternative semantics are you thinking about?

Copy link
Contributor

Choose a reason for hiding this comment

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

The original comment is actually about the following rule:

'{ e } =?= '{ $x } && typeOf('{e}) <:< typeOf('{$x}) && isColosedUnder()('{e}) ===>
matched('{e})

From my limited meta-programming experience, I like quoted pattern matching on terms and types a lot, it's a big productivity booster. In particular, it saves a lot of boilerplate when pattern matching on known methods and types. This makes developing macro-based DSLs a joyful experience. Previously, they have to be performed via low-level reflect API in a tedious way or doing it in an unsafe way via string comparison. @nicolasstucki has done a great job here for Scala meta-programmers👍

However, I'm not sure that making it safer and feature-complete will help much in practice, in particular when it comes at the price of usability. By usability, I mean simple API, natural and precise semantics, stability, debuggability, a clear-cut boundary of what is supported (accountable & trustable) and what is not.

For example, maybe it is safe drop the support for quoted code patterns of pattern matches and definitions, and leave them to reflect API?

'{ e: T } =?= '{ p: P } ===> '{e} =?= '{p} &&& '[T] =?= '[P]
'{ e } =?= '{ p: P } ===> '{e} =?= '{p}
'{ e.x } =?= '{ p.x } ===> '{e} =?= '{p}
'{ x } =?= '{ x } ===> matched
Copy link
Contributor

@Blaisorblade Blaisorblade Apr 8, 2020

Choose a reason for hiding this comment

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

Should this be somewhere next with '{{ x}} =?= '{{y }} , and maybe merged together?
NOPE, that's a pattern.

Copy link
Contributor

Choose a reason for hiding this comment

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

I now suspect I agree with this rule, but it took me a while to see why '{ x } =?= '{ y } is illegal (because alpha-equivalence is handled when checking local binders). Some explanation would help. (Who writes it is another question).

BTW, I'd hope Selects encoded as Idents (per desugarIdent) show up as Select here. I see they do not in Tasty reflection... does at least -Ycheck ensure that they're represented consistently so it's well-defined what you have to write to match Int (as opposed to needing Int, scala.Int or _root_.scala.Int depending on, say, what the user wrote)?

@Blaisorblade
Copy link
Contributor

Blaisorblade commented Apr 8, 2020

Hi! I hope it's okay, I took the liberty of leaving a few review comments, also because this relates to #8600 (and I hope I can contribute a bit here).
And to be clear, it's very cool you're sketching this!


'[T] =?= '[P] && T <:< P ===> matched
'[ T0[T1, ..., Tn] ] =?= '[ P0[P1, ..., Pn] ] ===> '[T0] =?= '[P0] &&& ... &&& '[Tn] =?= '[Pn]
'[T @annot] =?= '[P] ===> '[T] =?= '[P]
Copy link
Member

Choose a reason for hiding this comment

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

Annotations should be handled by subtyping rules already (and some annotations can create proper subtype, see https://github.com/lampepfl/dotty/blob/4253708ee22265959b6768547169c330244bf9cb/compiler/src/dotty/tools/dotc/core/Types.scala#L4589)

'{ e.super[T] } =?= '{ p.super[T] } ===> '{e} =?= '{p}
'{ Seq(e0, ..., en): _* } =?= '{ Seq(p0, ..., pn): _* } ===> '{e0} =?= '{p0} &&& ... &&& '{en} =?= '{pn}

'[T] =?= '[P] && T <:< P ===> matched
Copy link
Member

@smarter smarter Apr 8, 2020

Choose a reason for hiding this comment

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

This seems to contradict the fact that scala.quoted.Type is invariant in its type parameter. I guess this is needed for #8695 to work, but as I noted there, I tihnk the proper thing to do is to create quoted type variable instead of doing type inference -> picking "Any" -> relying on this rule to get other types to match.

Copy link
Contributor

@LPTK LPTK left a comment

Choose a reason for hiding this comment

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

I understand that this is an informal spec, but it is still missing the whole aspect about checking subtyping while extracting pattern type variables. The tricky part is that you'd need to specify a big part of Dotty's subtyping algorithm. But this is important and significant: there are tough questions to answer, especially when dealing with matching GADT types.

'{ Seq(e0, ..., en): _* } =?= '{ Seq(p0, ..., pn): _* } ===> '{e0} =?= '{p0} &&& ... &&& '{en} =?= '{pn}

'[T] =?= '[P] && T <:< P ===> matched
'[ T0[T1, ..., Tn] ] =?= '[ P0[P1, ..., Pn] ] ===> '[T0] =?= '[P0] &&& ... &&& '[Tn] =?= '[Pn]
Copy link
Contributor

@LPTK LPTK Apr 14, 2020

Choose a reason for hiding this comment

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

Does Dotty do higher-order subtyping (cc @smarter)? As in, will it successfully check List <: Seq, i.e., [+X] => List[X] <: [+Y] => Seq[Y]? What happens if the variances differ?
Moreover, it should be possible to match types which do not align in their parameters. For example, type pattern Iterable[$t] should be able to match type Map[K,V].

Copy link
Member

Choose a reason for hiding this comment

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

As in, will it successfully check List <: Seq

Yes.

What happens if the variances differ?

We take variance into account: https://github.com/lampepfl/dotty/blob/63ee630044b5555df691de02e59fb6d43d121a6d/compiler/src/dotty/tools/dotc/core/TypeComparer.scala#L594-L595 (but note that type lambdas themselves have inferred variance now: #8082)

@LPTK
Copy link
Contributor

LPTK commented Apr 14, 2020

On the same topic, since it's related: I stumbled upon the flops-rewrite tests in tests/run-macros/ and noticed that their encoding of safe term transformations, based on quote-matching, is unsound.

For instance, in tests/run-macros/flops-rewrite-3/Macro_1.scala, if we try:

  val rewriter = Rewriter().withFixPoint.withPost(
    Transformation.safe[Any] {
      case _ => '{"oops"}
    }

We get a match error in the inner match of:

  def apply[T: Type](e: Expr[T])(using QuoteContext): Expr[T] = {
    e match {
      case '{ $e: U } => transform.applyOrElse(e, identity) match { case '{ $e2: T } => e2 }
      case e => e
    }

Indeed, the transformation may rewrite to something that's larger than the expected type, in which case it should not be deemed applicable. There is a U <: T check missing.

This is an important gotcha to watch out for, if a rewrite primitive is ever added to the reflection API.

@nicolasstucki nicolasstucki force-pushed the add-quote-matching-spec branch from 52b16dd to b7f0b51 Compare April 15, 2020 09:49
@nicolasstucki nicolasstucki force-pushed the add-quote-matching-spec branch from b7f0b51 to b48ca44 Compare April 15, 2020 09:51
@nicolasstucki
Copy link
Contributor Author

I've moved the spec to the Matcher class to make it easier to maintain in sync with the implementation. I also named the rules and added references to the rules in the implementation.

I improved the readability of the rules based on the feedback (still to be improved) as well as fixing some rules that were not in line with the implementation. The current state represented the current implementation.

For actual semantic issues, I did not fix the rules but added some TODOs in the code. Once we have this spec in we can fix those while updating the spec at the same time.

I suggest merging this and have any discussion on semantics on new issues that reference the spec.

Thank you for all the feedback.

@nicolasstucki
Copy link
Contributor Author

@liufengyun, could you re-review this. We should just make sure that the current spec is inline with the current implementation for this PR.

Copy link
Contributor

@liufengyun liufengyun left a comment

Choose a reason for hiding this comment

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

LGTM

@nicolasstucki nicolasstucki merged commit c811957 into scala:master Apr 15, 2020
@nicolasstucki nicolasstucki deleted the add-quote-matching-spec branch April 15, 2020 11:09
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.

5 participants