-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Specialize Functions #1807
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
Specialize Functions #1807
Conversation
Perhaps it would be a good idea to move these phases to after erasure, since class Func1 extends Function1[Int, Int] {
def apply(i: Int) = i + 1
}
// this cast will be typed as `Int => Int` until erasure
(new Func1: Function1[Int, Int])(1) and the transformation doesn't catch it currently. |
new Splitter), // Expand selections involving union types into conditionals | ||
List(new VCInlineMethods, // Inlines calls to value class methods | ||
new Splitter, // Expand selections involving union types into conditionals | ||
new SpecializeFunction1), // Specialized Function1 by replacing super with specialized super |
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 do those two transformations have to be in different groups?
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 was unsure of what impact the interleaved phases would have here. DispatchToSpecializedApply
needs to know that the identifier type has a specialized apply before dispatching to it - so the addition to the decls (in SpecializeFunction1
) needs to happen before this phase's transformation is applied.
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 they be the same phase? If so, what restriction on the transformation API am I missing?
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.
DispatchToSpecializedApply needs to know that the identifier type has a specialized apply before dispatching to it
- the specialized apply already exists in
scala.Function1
, you don't need to wait for them to be added. - tree tranformer runs after denotTransformer, so the new definitions will be already present in types.
@@ -251,7 +251,7 @@ object NameOps { | |||
case nme.clone_ => nme.clone_ | |||
} | |||
|
|||
def specializedFor(classTargs: List[Types.Type], classTargsNames: List[Name], methodTargs: List[Types.Type], methodTarsNames: List[Name])(implicit ctx: Context): name.ThisName = { | |||
def specializedFor(classTargs: List[Types.Type], classTargsNames: List[Name], methodTargs: List[Types.Type] = scala.Nil, methodTarsNames: List[Name] = scala.Nil)(implicit ctx: Context): name.ThisName = { |
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'd prefer not to provide default arguments here, as it's easy to misuse this method.
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.
fair enough
tree match { | ||
case Apply(select @ Select(id, nme.apply), arg :: Nil) => | ||
val params = List(arg.tpe, tree.tpe) | ||
val specializedApply = nme.apply.specializedFor(params, params.map(_.typeSymbol.name)) |
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.
todo: this will pollute name table a lot. I would be nice to check if the name already exists before creating it, as if it doesn't, the specialization doesn't exist either.
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.
should this check be done in specializedFor
?
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.
yes) would be nice to have an extra boolean arg that checks if it should force creation.
import Denotations._, SymDenotations._, Scopes._, StdNames._, NameOps._, Names._ | ||
import ast.tpd | ||
|
||
class SpecializeFunction1 extends MiniPhaseTransform with DenotTransformer { |
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 only function1?
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.
we agreed to start there :)
But I could make it generic, albeit we only have specializations for JFunction0
, JFunction1
and JFunction2
|
||
blacklisted = Set( | ||
"scala.compat.java8.JFunction1", | ||
"scala.runtime.AbstractFunction1", |
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 do you need to blacklist those 3? As those functions don't know the actual type parameters of parent class, they shoundn't be specialized.
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.e. Aren't they already caught by https://github.com/lampepfl/dotty/pull/1807/files#diff-3a80aa3cb499ce4bfec8753eabcb1796R74?
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 was me being careful, I initially had some errors because of trying to apply the transformations to these, but the blacklist is never checked against in the current set of tests - as you pointed out they're caught by the earlier checks.
|
||
def transformDenot(cref: ClassDenotation, t1: Type, r: Type, func1: Type)(implicit ctx: Context): SingleDenotation = { | ||
val specializedFunction: TypeRef = | ||
ctx.requiredClassRef(functionPkg ++ specializedName(functionName, t1, r)) |
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.
requiredClassRef
would fail compilation if there's no such class. I don't think it should be used 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.
getClassIfDefined
instead?
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.
yes:-)
cref.copySymDenotation(info = newInfo) | ||
} | ||
|
||
private def transformTree(tmpl: Template, func1: Tree, t1: Type, r: Type)(implicit ctx: Context) = { |
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'd define it inline, as you use it once and the name is less informative then TransformTemplate.
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 wanted to keep things apart so that it would be easier to get a quick overview of the workings of SpecializeFunction1
, but perhaps it was taking it a bit far...
ctx.requiredClassRef(functionPkg ++ specializedName(functionName, t1, r)) | ||
|
||
def replaceFunction1(in: List[TypeRef]): List[TypeRef] = | ||
in.foldRight(List.empty[TypeRef]) { (tp, acc) => |
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 are recreating a list, removing all advantages of structural sharing.
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.
use mapConserve
(if (func1 eq t) specializedFunc1 else t) :: trees | ||
} | ||
|
||
val body = tmpl.body.foldRight(List.empty[Tree]) { |
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.
use mapConserve
.map(_.name) | ||
.toList | ||
|
||
assert( |
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 would be also nice to check that the actual implementation is in the right apply.
Ie, apply$mcII$sp
isn't a forwarder to generic apply
that unboxes.
I don't think so. After erasure the information is already lost, and I don't see why you cant translate this block now...
Where is a cast here? There shound be no cast needed here at all. |
The cast stops the optimization from applying, currently |
but why is cast there in the first place? There shouldn't be any casts needed. |
Without the cast (as should be in normal code) - the optimization applies and all is good. With the cast the optimization doesn't kick in - so the question arises: should it? |
So - you're totally right, there aren't any casts needed. But if there's a cast. The optimization doesn't apply. |
27afa90
to
44253d4
Compare
70cd9ba
to
43fed4c
Compare
95ad5ab
to
591bc8d
Compare
@DarkDimius - changes discussed today incorporated into the PR. |
7db2613
to
7a40ed1
Compare
private[this] var retTypes: Set[Symbol] = _ | ||
|
||
override def prepareForUnit(tree: Tree)(implicit ctx: Context) = { | ||
retTypes = Set(defn.UnitClass, |
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 take primitive value classes from ctx.definitons?
Same with argTypes.
.getOrElse(assert(false, "Could not find constructor for object `Test`")) | ||
} | ||
} | ||
} |
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 would be nice to have more run-tests that check if classes&lambdas inherit the right specialized functions using reflection.
|
||
// Transformations ----------------------------------------------------------- | ||
|
||
/** Transforms all classes extending `Function1[-T1, +R]` so that |
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.
Outdated comment?
|
||
// Extractors ---------------------------------------------------------------- | ||
private object ShouldTransformDenot { | ||
def unapply(cref: ClassDenotation)(implicit ctx: Context): Option[Seq[SpecializationTarget]] = |
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 think it's very simple implementation that doesn't warrant pattern-matching that allocates options.
Additionally, it's used only once. I'd inline it.
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.
If you want to keep it as a pattern, maybe consider using unapplySeq
functionPkg ++ specializedName(functionName ++ arity, args, ret) | ||
} | ||
|
||
val specializedApply: 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.
what if specializedParent doesn't exist?
case cref @ ShouldTransformDenot(targets) => { | ||
val specializedSymbols: Map[Symbol, (Symbol, Symbol)] = (for (SpecializationTarget(target, args, ret, original) <- targets) yield { | ||
val arity = args.length | ||
val specializedParent = ctx.getClassIfDefined { |
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.
shoulnd't this class always exist?
Wasn't it already extracted in targets
?
def replace(in: List[TypeRef]): List[TypeRef] = | ||
in.map { tref => | ||
val sym = tref.symbol | ||
specializedSymbols.get(sym).map { case (specializedParent, _) => |
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 this this is a very inefficient implementation.
Instead of building a Seq[(Symbol, (Symbol, Symbol)]
and then converting it and operating it,
you can use the fact that both specializedSymbols
and parents
maintain the same order and traverse them together.
This will allow for an implementation that doesn't allocate any tuples, doesn't convert collections and traverses collections only once.
|
||
val ClassInfo(prefix, cls, parents, decls, info) = cref.classInfo | ||
val newParents = replace(parents) | ||
val newInfo = ClassInfo(prefix, cls, newParents, specializeApplys(decls), info) |
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 think you don't need power of entire DenotTransformer
. InfoTransformer will do.
*/ | ||
override def transformTemplate(tree: Template)(implicit ctx: Context, info: TransformerInfo) = | ||
tree match { | ||
case tmpl @ ShouldTransformTree(targets) => { |
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.
same here.
override def transformTemplate(tree: Template)(implicit ctx: Context, info: TransformerInfo) = | ||
tree match { | ||
case tmpl @ ShouldTransformTree(targets) => { | ||
val symbolMap = (for ((tree, SpecializationTarget(target, args, ret, orig)) <- targets) yield { |
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.
same here, but even worse -> you flatten maps.
9c05e0b
to
615070e
Compare
|
||
def transformInfo(tp: Type, sym: Symbol)(implicit ctx: Context) = tp match { | ||
case tp: ClassInfo if !sym.is(Flags.Package) && (tp.decls ne EmptyScope) => { | ||
val newDecls = tp.decls.cloneScope |
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 clone scope when no changes are performed?
case Apply(select @ Select(id, nme.apply), arg :: Nil) => { | ||
val params = List(arg.tpe, tree.tpe) | ||
val specializedApply = specializedName(nme.apply, params) | ||
val hasOverridenSpecializedApply = id.tpe.decls.iterator.exists { |
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 iterate through all by hand instead of performing member lookup?
Why do you need to check if sym is override
?
sym => sym.is(Flags.Override) && (sym.name eq specializedApply) | ||
} | ||
|
||
if (hasOverridenSpecializedApply) tpd.Apply(tpd.Select(id, specializedApply), arg :: Nil) |
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.
what's wrong about dispatching to an abstract one?
9c6871d
to
3df75d5
Compare
) | ||
func2 = defn.FunctionClass(2) | ||
func2Applys = List( | ||
specApply(func2, defn.UnitType, List(defn.IntType, defn.IntType)), |
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.
Please use loops 😭
When a class directly extends a specialized function class, we need to replace the parent with the specialized interface. In other cases we don't replace it, even if the parent of a parent has a specialized apply - the symbols would propagate anyway.
b4f3c67
to
e17672b
Compare
Superseded by #3306 |
This PR is a bit of a work in progress, but I'd like feedback on the code from @DarkDimius and/or @odersky.
Todo:
PS: got stuck at Zurich airport, so haven't run the full test-suite yet. Letting our CI do the heavy lifting instead..