Skip to content

Implement inline #1492

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 77 commits into from
Oct 6, 2016
Merged

Implement inline #1492

merged 77 commits into from
Oct 6, 2016

Conversation

odersky
Copy link
Contributor

@odersky odersky commented Sep 1, 2016

  • Basic inliner
  • Keep original call alongside inlined code
  • Print inlining history in error messages
  • Support for separate compilation
  • Support for outer this references in inlined code
  • Create accessors for non-public members accessed from inlined code
  • Inline closures that come from an inline method
  • Use new inline keyword instead of annotation
  • Clarify relation to SIP

Review by @xeno-by

sym.getAnnotation(defn.InlineAnnot).get.tree
.attachment(InlinedBody).body

private class Typer extends ReTyper {
Copy link
Contributor

Choose a reason for hiding this comment

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

why is inliner a typer? Cant it be made it into a miniphase\part of post-typer?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  1. inlining needs to be done during typing, since macro expansion depends on it, and macro expansion needs to e.g. create implicit values.

  2. The inliner could possibly be formulated as another TreeMap instead of a full typer. I made it a type primarily for two reasons.

    2.1 That way we get constant folding for free (would have to be implemented explicitly in a treemap).
    2.2 That way we make sure the inlined code is type correct instead of failing during translation.

@odersky odersky force-pushed the add-inline branch 2 times, most recently from 1cf97bf to 5704089 Compare September 7, 2016 15:47
@xeno-by xeno-by mentioned this pull request Sep 9, 2016
18 tasks
@odersky
Copy link
Contributor Author

odersky commented Sep 9, 2016

/rebuild

@odersky odersky changed the title WIP: Implement inline Implement inline Sep 10, 2016
@odersky
Copy link
Contributor Author

odersky commented Sep 12, 2016

I think we are done for now. Over to you, @xeno-by!

@xeno-by
Copy link

xeno-by commented Sep 15, 2016

@odersky Congrats with finishing the initial version of this fun project! I'm on vacation together with Mano and Denys until the end of the week, but will do my best to review after I return.

@odersky
Copy link
Contributor Author

odersky commented Sep 15, 2016

/rebuild

@odersky
Copy link
Contributor Author

odersky commented Sep 15, 2016

Last two days commits

  • add inline function parameters
  • contain fixes for inline methods in inline arguments, which are needed for compiling nested fors
  • removes InlineInfo in favor of a simpler annotation-based scheme

case class Inlined[-T >: Untyped] private[ast] (call: tpd.Tree, bindings: List[MemberDef[T]], expansion: Tree[T])
extends Tree[T] {
type ThisTree[-T >: Untyped] = Inlined[T]
}
Copy link
Member

@smarter smarter Sep 19, 2016

Choose a reason for hiding this comment

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

} Indented two spaces too many



private def treatAsIdent() = {
testScala2Mode(i"$name is now a keyword, put in `...` to keep as an identifier")
Copy link
Member

@smarter smarter Sep 22, 2016

Choose a reason for hiding this comment

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

I would be more explicit in the error message proposed solution and say something like write $name instead of $name to keep using it as an identifier

@@ -530,7 +530,7 @@ object Flags {
final val MethodOrLazyOrDeferred = Method | Lazy | Deferred

/** Labeled `private` or `final` */
Copy link
Member

@smarter smarter Sep 22, 2016

Choose a reason for hiding this comment

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

Comment needs to be updated to include inline

def paramInfo(param: Symbol): Type = translateRepeated(param.info)
/** Add @inlineParam to inline call-by-value parameters */
def translateInline(tp: Type): Type = tp match {
case tp @ ExprType(tp1) => tp
Copy link
Member

Choose a reason for hiding this comment

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

write tp @ ExprType(_) since tp1 is not used

def typedTypedSplice(tree: untpd.TypedSplice)(implicit ctx: Context): Tree =
tree.tree match {
case tree1: TypeTree => tree1 // no change owner necessary here ...
case tree1: Ident => tree1 // ... or here
Copy link
Member

Choose a reason for hiding this comment

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

Why not?

object Test {

inline def swap[T](x: T, inline x_= : T => Unit, y: T, inline y_= : T => Unit) = {
val t = x
Copy link
Member

Choose a reason for hiding this comment

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

You don't actually need a temporary variable here, x_=(y); y_=(x) works fine.

}
}

/** If tree is a closure, it's body, otherwise tree itself */
Copy link
Member

Choose a reason for hiding this comment

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

Typo: it's -> its

*/
object closure {
def unapply(tree: Tree): Option[(List[Tree], Tree, Tree)] = tree match {
case Block(_, Closure(env, meth, tpt)) => Some(env, meth, tpt)
Copy link
Member

Choose a reason for hiding this comment

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

This is only correct for closures created by desugar.makeClosure and only for certain phases, we have similar pattern matches in other places that makes similar assumption and I don't like it either, I think ExpandSAMs#transformBlock is the only one which tries to be careful, maybe we could use that here:

case Block(stats @ (fn: DefDef) :: Nil, Closure(_, fnRef, tpt)) if fnRef.symbol == fn.symbol =>

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure about this one. The question is, how should closures in other blocks be categorized? Is that still a closure or something else? For the moment I'd leave as is.

@@ -2551,6 +2577,24 @@ object Types {
x => paramBounds mapConserve (_.subst(this, x).bounds),
x => resType.subst(this, x))

/** Merge nested polytypes into one polytype. nested polytypes are normally not suported
Copy link
Member

Choose a reason for hiding this comment

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

Typo: suported

val accessorRef = ref(accessor).appliedToTypeTrees(targs).appliedToArgss(argss)
(accessorDef, accessorRef)
} else {
// Hard case: Reference needs to go via a dyanmic prefix
Copy link
Member

Choose a reason for hiding this comment

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

Typo: dyanmic -> dynamic

def addAccessor(tree: Tree, refPart: Tree, targs: List[Tree], argss: List[List[Tree]],
accessedType: Type, rhs: (Tree, List[Type], List[List[Tree]]) => Tree)(implicit ctx: Context): Tree = {
val (accessorDef, accessorRef) =
if (refPart.symbol.isStatic) {
Copy link
Member

Choose a reason for hiding this comment

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

Couldn't this be extended to:

if (refPart.symbol.isStatic || refPart.symbol.isContainedIn(inlineMethod.owner) {
  ...
}

This way when we have:

    private val x: Int
    inline def get1 = x

we'll get:

def $inlineAccessor$get1$1: T = C.this.x

instead of:

def $inlineAccessor$get1$1(x$0: Test.C[T]): T = x$0.x

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, because the reference to the symbol might not be via this. So the right check is

    val qual = qualifier(refPart)
    def refIsLocal = qual match {
      case qual: This => qual.symbol == refPart.symbol.owner
      case _ => false
    }
    val (accessorDef, accessorRef) =
      if (refPart.symbol.isStatic || refIsLocal) { ...

// TODO: Handle references to non-public types.
// This is quite tricky, as such types can appear anywhere, including as parts
// of types of other things. For the moment we do nothing and complain
// at the implicit expansion site if there's a reference to an inaccessible type.
Copy link
Member

Choose a reason for hiding this comment

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

Can't we just disable access checks for type members in inlined blocks? We need accessors for term members because of the JVM but type members get erased anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not true for classes. They don't get erased.

Copy link
Member

Choose a reason for hiding this comment

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

Indeed, but does the JVM restrict access to classes in any way? As far as I know it only restricts access to the constructor of the class, but we take care of that by adding a term accessor for the constructor, not a type accessor.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I thought access to private classes is caught by the JVM

Copy link
Member

Choose a reason for hiding this comment

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

Doesn't seem so, here's some Java code:
Foo.java:

public class Foo {
  public class Inner {
    int get() { return 42; }
  }
  Inner inner = new Inner();
}

Bar.java:

public class Bar {
  public static void main(String[] args) {
    Foo.Inner inner = (new Foo()).inner;
    System.out.println(inner.get());
  }
}
$ javac Foo.java
$ javac Bar.java
$ java Bar
42
$ edit Foo.java, make Inner private
$ javac Foo.java
$ java Bar
42

Copy link
Contributor Author

@odersky odersky Oct 3, 2016

Choose a reason for hiding this comment

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

Inner classes indeed get the private modifier removed. But package inner classes are maintained in the JVM I think

Copy link
Member

Choose a reason for hiding this comment

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

Ah you're right indeed, the following breaks:

package a;

public class Foo {
  public class Inner {
    public int get() { return 42; }
  }
  public Inner inner = new Inner();
}
package b;

public class Bar {
  public static void main(String[] args) {
    a.Foo.Inner inner = (new a.Foo()).inner;
    System.out.println(inner.get());
  }
}
$ javac a/Foo.java
$ javac b/Bar.java
$ java b.Bar
$ Remove public from "public class Inner"
$ javac a/Foo.java
$ java b.Bar
Exception in thread "main" java.lang.IllegalAccessError: tried to access class a.Foo$Inner from class b.Bar
        at b.Bar.main(Bar.java:6)

I don't think adding a type alias is going to help us here since it's just going to get erased, I think we need to mark public in the bytecode all the package inner classes referenced from inline methods

Copy link
Member

@smarter smarter Oct 3, 2016

Choose a reason for hiding this comment

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

Interestingly, scalac 2.11 and 2.12 always emit private inner classes with ACC_PUBLIC:

package a;

class Foo {
  private class Inner {
    def get = 42
  }
}

But dotty does emit them without ACC_PUBLIC unless we remove private from private class Inner.

Both scalac and dotty will prevent you from leaking a reference to a private class:

package a;

class Foo {
  private class Inner {
    def get = 42
  }
  val inner = new Inner // error: non-private value inner refers to private class Inner
  type Duck = Inner // error: non-private type Duck refers to private class Inner
}

The easiest solution would be to always emit inner classes with ACC_PUBLIC as scalac does, @DarkDimius: do you think that this is reasonable ?

!ctx.owner.ownersIterator.exists(_.isInlineMethod) &&
!ctx.settings.YnoInline.value &&
!ctx.isAfterTyper)
adapt(Inliner.inlineCall(tree, pt), pt)
Copy link
Member

Choose a reason for hiding this comment

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

This is the only call to inlineCall and it's in a branch with tree.tpe <:< pt this means that when adaptToSubType is necessary we never inline, for example get isn't inlined in foo:

object Test {
  inline def get: Int = 1

  def foo: Unit = {
    get
  }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It turns out we cannot move this test earlier or we get inline errors. I fixed the problem by recursively calling adapt when converting to Unit. I think that's the only place where an inline was missing.

@@ -212,8 +213,22 @@ object Scanners {
/** A buffer for comments */
val commentBuf = new StringBuilder

private def handleMigration(keyword: Token): Token =
if (!isScala2Mode) keyword
else if (keyword == INLINE) treatAsIdent()
Copy link
Member

Choose a reason for hiding this comment

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

This means that the following doesn't work with -language:Scala2 because inline is treated as an identifier:

inline def x = ...

you have to use @inline instead. We can fix this by only treating INLINE specially if the previous token is AT.

- DRY
- Refactor out special path operations
To be done: outer accessors
To be done: error positions
To do this, factor out Key from Attachment into a new type, Property.Key.
... to tag inlined calls. Perform typings and transformations
of inlined calls in a context that refers to the INlined node
in its InlinedCall property.

The idea is that we can use this to issue better error
positions. This remains to be implemented.
Error messages now print the inlined positions as well
as the position of the inlined call, recursively.
Inline conditionals with constant conditions
odersky and others added 10 commits October 2, 2016 16:12
We got unbound symbols before because
a TreeTypeMap would copy a tree of an inline
DefDef but would not adapt the inline body
stored in the @inline annotation of the DefDef
to point to the updated tree.
Since fundamental operations such as treeCopy have to know
about inline bodies, it seems better to represent this
information directly in an annotation.
Now that we have BodyAnnot, InlineInfo can be eliminated.
Better comments and refactorings that move some things around
so that less modules depend on Inliner.
This special case was added two years ago, quoting from
5428549
"Inlined pure values are pure even if referenced from impure
prefixes (i.e. prefix need not be evaluated)"
This does not match the current semantics for inline where the prefix is
always evaluated.
This makes "publishLocal" much faster which is extremely useful when
working on the sbt bridge.
Since we have no nice way of hashing a typed tree we use the
pretty-printed tree of the method (as it would be printed by
-Xprint:posttyper -Xprint-types) as a hacky substitute for now.
@odersky
Copy link
Contributor Author

odersky commented Oct 2, 2016

Rebased to master

@@ -1859,7 +1859,7 @@ class Typer extends Namer with TypeAssigner with Applications with Implicits wit
if (folded ne tree) return adaptConstant(folded, folded.tpe.asInstanceOf[ConstantType])
// drop type if prototype is Unit
if (pt isRef defn.UnitClass)
return tpd.Block(tree :: Nil, Literal(Constant(())))
return adapt(tpd.Block(tree :: Nil, Literal(Constant(()))), pt)
Copy link
Member

Choose a reason for hiding this comment

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

This is not enough, the Block itself is not inlinable, so adapting the block doesn't help:

object X {
  inline def foo: Int = 1

  def test: Unit = {
    X.foo
  }
}

-Xprint:frontend:

package <empty> {
  final lazy module val X: X$ = new X$()
  final module class X$() extends Object() { this: X.type => 
    @1 @inline() inline def foo: Int = 1
    def test: Unit = 
      {
        {
          X.foo
          ()
        }
      }
  }
}

@odersky
Copy link
Contributor Author

odersky commented Oct 2, 2016

I have a tendency to merge this now. We already got fine-grained comments, and questions about more fundamental issues might just as well be expressed as separate issues. Since the PR is large it tends to get into conflicts with master quickly. It took me considerable time to rebase everything against current master today.

What do people think?

@smarter
Copy link
Member

smarter commented Oct 2, 2016

I think it would be good to get this code in soon, yeah

@odersky
Copy link
Contributor Author

odersky commented Oct 6, 2016

LGTM, as far as @smarter's commits are concerned.

@smarter
Copy link
Member

smarter commented Oct 6, 2016

LGTM too, with the following needing to be addressed as follow-ups:

@smarter smarter merged commit 87a7757 into scala:master Oct 6, 2016
smarter added a commit to smarter/dotty that referenced this pull request Oct 6, 2016
The one-parameter constructor of SourceFile was removed in scala#1494
smarter added a commit that referenced this pull request Oct 6, 2016
@allanrenucci allanrenucci deleted the add-inline branch December 14, 2017 16:57
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.

7 participants