-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Implement inline #1492
Conversation
sym.getAnnotation(defn.InlineAnnot).get.tree | ||
.attachment(InlinedBody).body | ||
|
||
private class Typer extends ReTyper { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-
inlining needs to be done during typing, since macro expansion depends on it, and macro expansion needs to e.g. create implicit values.
-
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.
1cf97bf
to
5704089
Compare
/rebuild |
I think we are done for now. Over to you, @xeno-by! |
@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. |
/rebuild |
Last two days commits
|
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] | ||
} |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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` */ |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 */ |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 =>
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
}
}
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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
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.
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.
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) |
There was a problem hiding this comment.
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
()
}
}
}
}
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? |
I think it would be good to get this code in soon, yeah |
LGTM, as far as @smarter's commits are concerned. |
LGTM too, with the following needing to be addressed as follow-ups:
|
The one-parameter constructor of SourceFile was removed in scala#1494
Fix build failure after merging #1492
inline
keyword instead of annotationReview by @xeno-by