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

Pattern matcher: extractors become name-based. #2835

Closed
wants to merge 17 commits into from
Closed

Pattern matcher: extractors become name-based. #2835

wants to merge 17 commits into from

Conversation

paulp
Copy link
Contributor

@paulp paulp commented Aug 15, 2013

An extractor is no longer required to return Option[T], and
can instead return anything which directly contains methods
with these signatures:

  def isEmpty: Boolean
  def get: T

If the type of get contains methods with the names of
product selectors (_1, _2, etc.) then the type and arity
of the extraction is inferred from the type of get. If
it does not contain _1, then it is a single value
extractor analogous like Option[T].

This has significant benefits and opens new territory:

  • an AnyVal based Option-like class can be used which
    leverages null as None, and no allocations are necessary
  • for primitive types the benefit is squared (see below)
  • the performance difference between case classes and
    extractors should now be largely eliminated
  • this in turn allows us to recapture great swaths of
    memory which are currently squandered (e.g. every
    TypeRef has fields for pre and args, even though these
    are more than half the time NoPrefix and Nil)

Here is a primitive example:

  final class OptInt(val x: Int) extends AnyVal {
    def get: Int = x
    def isEmpty = x == Int.MinValue // or whatever is appropriate
  }
  // This boxes TWICE: Int => Integer => Some(Integer)
  def unapply(x: Int): Option[Int]
  // This boxes NONCE
  def unapply(x: Int): OptInt

As a multi-value example, after I contribute some methods to TypeRef:

  def isEmpty = false
  def get     = this
  def _1      = pre
  def _2      = sym
  def _3      = args

Then its extractor becomes

  def unapply(x: TypeRef) = x

Which, it need hardly be said, involves no allocations.

@paulp
Copy link
Contributor Author

paulp commented Aug 15, 2013

There's still probably plenty to quibble with, but this at least presents a basis for discussion.

@paulp
Copy link
Contributor Author

paulp commented Aug 15, 2013

I am assuming the failure in pr-scala-distpack is not on my conscience.

@adriaanm
Copy link
Contributor

I'm a fan of the refactoring in d37e629, but could you please make a rough summary of behavior changes in that commit. I went through the codegen bit, but the behavior signal in match translation and typers is hard to separate from the refactoring "noise". Based on initial skimming, I expect a swift path to LGTM.

@paulp
Copy link
Contributor Author

paulp commented Aug 16, 2013

I will jump to the conclusion that the failures in pr-scala-integrate-partest aren't my doing either.

[warn]  ::::::::::::::::::::::::::::::::::::::::::::::
[warn]  ::          UNRESOLVED DEPENDENCIES         ::
[warn]  ::::::::::::::::::::::::::::::::::::::::::::::
[warn]  :: org.scala-lang.modules#scala-xml_2.11.0-M4;1.0-RC2: not found
[warn]  ::::::::::::::::::::::::::::::::::::::::::::::
sbt.ResolveException: unresolved dependency: org.scala-lang.modules#scala-xml_2.11.0-M4;1.0-RC2: not found
    at sbt.IvyActions$.sbt$IvyActions$$resolve(IvyActions.scala:213)
    at sbt.IvyActions$$anonfun$update$1.apply(IvyActions.scala:122)

@paulp
Copy link
Contributor Author

paulp commented Aug 16, 2013

That last push accounts for 7214. I'm not going to bother with any please-rebuilds because I assume it will just fail again.

It's a source of constant peril that sym.tpe on NoSymbol
is fine (it's NoType) but tpe memberType sym on NoSymbol throws
a NSDNHO. The last thing we should be doing is discouraging
people from using memberType in favor of sym.tpe, the latter
being almost always the wrong thing.
Motivated by pattern matcher work, also useful elsewhere.
The new method is the same as sameMethodAndFieldSignatures, but ignores
generic signatures. This allows for testing methods which receive the same
descriptor but differing generic signatures. In particular, this happens
with value classes, which get a generic signature where a method written
in terms of the underlying values does not.
We should do a lot more of this - it's ridiculously difficult
and error prone to generate code of this kind involving implicits,
type inference, etc. where the same goal is trivially accomplished
by generating a method call and letting the typer work out the
details.
Because who doesn't want a little positioning in their life.
All parents of an intersection type must be checkable for the
type to be checkable.
This exploits the infrastructure developed for checking the
checkability of type patterns to improve pattern type inference,
which suffered from a similar deficit. There was a hack for
SI-2486, the best I could manage at the time I wrote it; that
is replaced with the principled approach.
This makes it a lot less error prone and redundant to find the
part you need when unwrapping an UnApply tree.
An extractor is no longer required to return Option[T], and
can instead return anything which directly contains methods
with these signatures:

  def isEmpty: Boolean
  def get: T

If the type of get contains methods with the names of
product selectors (_1, _2, etc.) then the type and arity
of the extraction is inferred from the type of get. If
it does not contain _1, then it is a single value
extractor analogous like Option[T].

This has significant benefits and opens new territory:

  - an AnyVal based Option-like class can be used which
    leverages null as None, and no allocations are necessary
  - for primitive types the benefit is squared (see below)
  - the performance difference between case classes and
    extractors should now be largely eliminated
  - this in turn allows us to recapture great swaths of
    memory which are currently squandered (e.g. every
    TypeRef has fields for pre and args, even though these
    are more than half the time NoPrefix and Nil)

Here is a primitive example:

  final class OptInt(val x: Int) extends AnyVal {
    def get: Int = x
    def isEmpty = x == Int.MinValue // or whatever is appropriate
  }
  // This boxes TWICE: Int => Integer => Some(Integer)
  def unapply(x: Int): Option[Int]
  // This boxes NONCE
  def unapply(x: Int): OptInt

As a multi-value example, after I contribute some methods to TypeRef:

  def isEmpty = false
  def get     = this
  def _1      = pre
  def _2      = sym
  def _3      = args

Then it's extractor becomes

  def unapply(x: TypeRef) = x

Which, it need hardly be said, involves no allocations.
That was a lot harder than it looks. The dueling test cases
for various corners make it sensitive to the slightest change
in the logic. And of course it never helps that correctness
depends on ad hoc in-place overloading resolution, which is
now performed in a method called inPlaceAdHocOverloadingResolution
so it's a little more obvious.
@adriaanm
Copy link
Contributor

I went through the whole diff. Excellent work. Good to merge on Monday, as far as I'm concerned.

Your refactoring is a great improvement, but, for my understanding, is there anything in MatchTranslation that is crucial for the new translation?

Here are my Cliff's notes for understanding the new implementation of the new behavior:

Anything I missed?

def resultType = tpe.finalResultType
def method = unapplyMember(tpe)
def paramType = firstParamType(unapplyType)
def rawGet = if (isBool) UnitTpe else resultOfMatchingMethod(resultType, "get")()
Copy link
Member

Choose a reason for hiding this comment

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

How about nme.get here?

@xeno-by
Copy link
Member

xeno-by commented Aug 17, 2013

Gone are the days of oppression. Unapply macros are finally making it into trunk!

import scala.reflect.macros.Context
import language.experimental.macros

trait Tree
case object SomeTree extends Tree

object NewQuasiquotes {
  implicit class QuasiquoteInterpolation(c: StringContext) {
    object nq {
      def unapply(t: Tree) = macro QuasiquoteMacros.unapplyImpl
    }
  }
}

object QuasiquoteMacros {
  def unapplyImpl(c: Context)(t: c.Tree) = {
    import c.universe._
    q"""
      new {
        def isEmpty = false
        def get = this
        def _1 = SomeTree
        def _2 = SomeTree
        def unapply(t: Tree) = this
      }.unapply($t)
    """
  }
}

object Test extends App {
  import NewQuasiquotes._
  SomeTree match {
    case nq"$x + $y" => println((x, y))
  }
}

09:10 ~/Projects/patmat/sandbox (topic/patmat)$ ssr
(SomeTree,SomeTree)

As brought up by Denys yesterday, this pull request will finally allow us to turn unapplications into macros without resorting to insane hacks like manual assembly of UnApply nodes. Oh yeah!

@retronym
Copy link
Member

Could you submit a follow up pr with a few test cases for unapply macros? Would be great to nail them down.

IntelliJ will also need to be updated with the relaxed rules for extractors. SIP worthy? /cc @Alefas

@xeno-by
Copy link
Member

xeno-by commented Aug 17, 2013

@retronym Yes, that's what I'm doing right now.

@xeno-by
Copy link
Member

xeno-by commented Aug 17, 2013

@retronym and all: https://github.com/paulp/scala/pull/8 establishes a pattern that can be used to implement extractor macros, explains how to implement it and provides examples in tests.

@xeno-by
Copy link
Member

xeno-by commented Aug 17, 2013

@paulp Btw when playing with extractor macros, I noticed a gotcha:

object Matcher {
  def isEmpty = false
  def get = this
  def _1 = 2
  def unapply(x: Int) = this
  override def toString = "oops"
}

2 match {
  case Matcher(x) => println(x) // will print oops!
}

Is this intended?

@paulp
Copy link
Contributor Author

paulp commented Aug 17, 2013

@xeno-by this has been bugging me and I'm not sure what to do about it. I was going to bring it up today. It's only an issue with Product1 (or something else which defines _1 and not _2 for whatever reason) but in such cases I don't know what the right thing is to do. I'm surprised it prints oops, I thought the logic would have it printing 2, but I'm not sure 2 isn't a problem also.

@paulp
Copy link
Contributor Author

paulp commented Aug 17, 2013

@adriaanm I got stuck in some other stuff yesterday, I will elaborate/comment today.

@dragos
Copy link
Contributor

dragos commented Aug 17, 2013

It seems the IDE validation fails with a compilation error: (the Jenkins job looks blue, though):

[ERROR] /localhome/jenkins/c/workspace/pr-scala-integrate-ide/scala-ide/org.scala-ide.sdt.core/src/scala/tools/eclipse/formatter/ScalaFormatterPreferenceInitializer.scala:20: error: overloaded method value setDefault with alternatives:
[ERROR]   (x$1: String,x$2: Boolean)Unit <and>
[ERROR]   (x$1: String,x$2: String)Unit <and>
[ERROR]   (x$1: String,x$2: Long)Unit <and>
[ERROR]   (x$1: String,x$2: Int)Unit <and>
[ERROR]   (x$1: String,x$2: Float)Unit <and>
[ERROR]   (x$1: String,x$2: Double)Unit
[ERROR]  cannot be applied to (String, _66)
[ERROR]           preferenceStore.setDefault(preference.eclipseKey, pd.defaultValue)

@adriaanm
Copy link
Contributor

We had to make the jenkins job always succeed because the build flow plugin's ignore combinator is broken.

@paulp
Copy link
Contributor Author

paulp commented Aug 17, 2013

@dragos thanks for pointing it out, that looks like a error which needs addressing here.

@dragos
Copy link
Contributor

dragos commented Aug 17, 2013

@paulp for the record, the overloaded method is defined in a trait hierarchy in PreferenceDescriptor.scala

@paulp
Copy link
Contributor Author

paulp commented Aug 17, 2013

I'm sure I'll be able to fix it. This is one of those brutal things where some arbitrary overloading resolution is done in-place on the tree (directly modifying the symbol and type in the midst of typing) so of course no matter how one touches it something somewhere will break.

@adriaanm
Copy link
Contributor

Here are some other failures (failing to compile scalariform):

@dragos, while looking at the IDE logs I noticed some new failures: org.scala-ide.sbt.full.library, org.scala-ide.sdt.core, org.scala-ide.sdt.core.tests, org.scala-ide.sdt.debug.tests, org.scala-refactoring.library, scalariform (the list above), Sbinary build failures on 2.10 (it used to work, but now complains about xml) -- I haven't looked into them.

@dragos
Copy link
Contributor

dragos commented Aug 17, 2013

@adriaanm I'm going through the most recent.. Do you have a tip on how to find only failures? I'm clicking my way through them, but it's very time consuming.

@paulp
Copy link
Contributor Author

paulp commented Aug 17, 2013

See my comment in #2848 ; there was definitely a bug on my end which was shown in scalariform, but I don't know to what extent that will explain the current crop of failures.

@paulp
Copy link
Contributor Author

paulp commented Aug 17, 2013

Anywhere you see this:

[ERROR] error: symbol value key does not exist in scalariform.formatter.preferences.PreferencesImporterExporter.getPreferences
[ERROR] error: scala.reflect.internal.FatalError: symbol value key does not exist in scalariform.formatter.preferences.PreferencesImporterExporter.getPreferences

That's my bug.

@adriaanm
Copy link
Contributor

@dragos: The script does the grepping for you, should be extensible to find other failures. To clarify: the script I attached in that thread. I've gisted it: https://gist.github.com/adriaanm/6258066

@paulp
Copy link
Contributor Author

paulp commented Aug 17, 2013

Superseded by #2848.

@paulp paulp closed this Aug 17, 2013
@paulp
Copy link
Contributor Author

paulp commented Aug 18, 2013

@dragos I've reproduced the overload issue, hopefully that will be the hard part. I'm sort of impressed we have no test which exercises this. It doesn't even have to be overloaded.

trait Bip[T] { def h: T }
trait BoolBip extends Bip[Boolean]

class A {
  def g(x: Boolean): Unit = ()
  def f(xs: List[Bip[_]]) = xs foreach { case x: BoolBip => g(x.h) }
}
b.scala:6: error: type mismatch;
 found   : _1
 required: Boolean
  def f(xs: List[Bip[_]]) = xs foreach { case x: BoolBip => g(x.h) }
                                                                ^
one error found

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
5 participants