Skip to content

Commit 573a0c5

Browse files
committed
Expose, tweak & use isElideableExpr in tuple specialisation
Handle the contrived example constructing a non-elideable construction of a tuple. Requires exposing isElideableExpr, tweaking its "isCaseClassApply" (to disallow block select qualifiers - while still allowing bare ident applies), also fixing the StableRealizable flag on tuple constructors so that isPureApply is true for those, so they can elideable too.
1 parent 434f7a7 commit 573a0c5

File tree

7 files changed

+83
-68
lines changed

7 files changed

+83
-68
lines changed

compiler/src/dotty/tools/dotc/core/SymDenotations.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@ object SymDenotations {
9292
if (myFlags.is(Trait)) NoInitsInterface & bodyFlags // no parents are initialized from a trait
9393
else NoInits & bodyFlags & parentFlags)
9494

95+
final def setStableConstructor()(using Context): Unit =
96+
val ctorStable = if myFlags.is(Trait) then myFlags.is(NoInits) else isNoInitsRealClass
97+
if ctorStable then primaryConstructor.setFlag(StableRealizable)
98+
9599
def isCurrent(fs: FlagSet)(using Context): Boolean =
96100
def knownFlags(info: Type): FlagSet = info match
97101
case _: SymbolLoader | _: ModuleCompleter => FromStartFlags

compiler/src/dotty/tools/dotc/core/unpickleScala2/Scala2Unpickler.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -617,7 +617,9 @@ class Scala2Unpickler(bytes: Array[Byte], classRoot: ClassDenotation, moduleClas
617617
// we need the checkNonCyclic call to insert LazyRefs for F-bounded cycles
618618
else if (!denot.is(Param)) tp1.translateFromRepeated(toArray = false)
619619
else tp1
620-
if (denot.isConstructor) addConstructorTypeParams(denot)
620+
if (denot.isConstructor)
621+
denot.owner.setStableConstructor()
622+
addConstructorTypeParams(denot)
621623
if (atEnd)
622624
assert(!denot.symbol.isSuperAccessor, denot)
623625
else {

compiler/src/dotty/tools/dotc/transform/SpecializeTuples.scala

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import ast.Trees.*, ast.tpd, core.*
66
import Contexts.*, Types.*, Decorators.*, Symbols.*, DenotTransformers.*
77
import SymDenotations.*, Scopes.*, StdNames.*, NameOps.*, Names.*
88
import MegaPhase.MiniPhase
9+
import typer.Inliner.isElideableExpr
910

1011
/** Specializes Tuples by replacing tuple construction and selection trees.
1112
*
@@ -23,10 +24,14 @@ class SpecializeTuples extends MiniPhase:
2324

2425
override def transformApply(tree: Apply)(using Context): Tree = tree match
2526
case Apply(TypeApply(fun: NameTree, targs), args)
26-
if fun.symbol.name == nme.apply && fun.symbol.exists && defn.isSpecializableTuple(fun.symbol.owner.companionClass, targs.map(_.tpe)) =>
27+
if fun.symbol.name == nme.apply && fun.symbol.exists && defn.isSpecializableTuple(fun.symbol.owner.companionClass, targs.map(_.tpe))
28+
&& isElideableExpr(tree)
29+
=>
2730
cpy.Apply(tree)(Select(New(defn.SpecialisedTuple(fun.symbol.owner.companionClass, targs.map(_.tpe)).typeRef), nme.CONSTRUCTOR), args).withType(tree.tpe)
2831
case Apply(TypeApply(fun: NameTree, targs), args)
29-
if fun.symbol.name == nme.CONSTRUCTOR && fun.symbol.exists && defn.isSpecializableTuple(fun.symbol.owner, targs.map(_.tpe)) =>
32+
if fun.symbol.name == nme.CONSTRUCTOR && fun.symbol.exists && defn.isSpecializableTuple(fun.symbol.owner, targs.map(_.tpe))
33+
&& isElideableExpr(tree)
34+
=>
3035
cpy.Apply(tree)(Select(New(defn.SpecialisedTuple(fun.symbol.owner, targs.map(_.tpe)).typeRef), nme.CONSTRUCTOR), args).withType(tree.tpe)
3136
case _ => tree
3237
end transformApply

compiler/src/dotty/tools/dotc/typer/Inliner.scala

Lines changed: 64 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,70 @@ object Inliner {
411411

412412
}
413413

414+
/** Very similar to TreeInfo.isPureExpr, but with the following inliner-only exceptions:
415+
* - synthetic case class apply methods, when the case class constructor is empty, are
416+
* elideable but not pure. Elsewhere, accessing the apply method might cause the initialization
417+
* of a containing object so they are merely idempotent.
418+
*/
419+
object isElideableExpr {
420+
def isStatElideable(tree: Tree)(using Context): Boolean = unsplice(tree) match {
421+
case EmptyTree
422+
| TypeDef(_, _)
423+
| Import(_, _)
424+
| DefDef(_, _, _, _) =>
425+
true
426+
case vdef @ ValDef(_, _, _) =>
427+
if (vdef.symbol.flags is Mutable) false else apply(vdef.rhs)
428+
case _ =>
429+
false
430+
}
431+
432+
def apply(tree: Tree)(using Context): Boolean = unsplice(tree) match {
433+
case EmptyTree
434+
| This(_)
435+
| Super(_, _)
436+
| Literal(_) =>
437+
true
438+
case Ident(_) =>
439+
isPureRef(tree) || tree.symbol.isAllOf(Inline | Param)
440+
case Select(qual, _) =>
441+
if (tree.symbol.is(Erased)) true
442+
else isPureRef(tree) && apply(qual)
443+
case New(_) | Closure(_, _, _) =>
444+
true
445+
case TypeApply(fn, _) =>
446+
if (fn.symbol.is(Erased) || fn.symbol == defn.QuotedTypeModule_of) true else apply(fn)
447+
case Apply(fn, args) =>
448+
val isCaseClassApply = {
449+
val cls = tree.tpe.classSymbol
450+
val meth = fn.symbol
451+
meth.name == nme.apply &&
452+
meth.flags.is(Synthetic) &&
453+
meth.owner.linkedClass.is(Case) &&
454+
cls.isNoInitsRealClass &&
455+
funPart(fn).match
456+
case Select(qual, _) => qual.symbol.is(Synthetic) // e.g: disallow `{ ..; Foo }.apply(..)`
457+
case meth @ Ident(_) => meth.symbol.is(Synthetic) // e.g: allow `import Foo.{ apply => foo }; foo(..)`
458+
case _ => false
459+
}
460+
if isPureApply(tree, fn) then
461+
apply(fn) && args.forall(apply)
462+
else if (isCaseClassApply)
463+
args.forall(apply)
464+
else if (fn.symbol.is(Erased)) true
465+
else false
466+
case Typed(expr, _) =>
467+
apply(expr)
468+
case Block(stats, expr) =>
469+
apply(expr) && stats.forall(isStatElideable)
470+
case Inlined(_, bindings, expr) =>
471+
apply(expr) && bindings.forall(isStatElideable)
472+
case NamedArg(_, expr) =>
473+
apply(expr)
474+
case _ =>
475+
false
476+
}
477+
}
414478
}
415479

416480
/** Produces an inlined version of `call` via its `inlined` method.
@@ -691,67 +755,6 @@ class Inliner(call: tpd.Tree, rhsToInline: tpd.Tree)(using Context) {
691755
|| tpe.cls.is(Package)
692756
|| tpe.cls.isStaticOwner && !(tpe.cls.seesOpaques && inlinedMethod.isContainedIn(tpe.cls))
693757

694-
/** Very similar to TreeInfo.isPureExpr, but with the following inliner-only exceptions:
695-
* - synthetic case class apply methods, when the case class constructor is empty, are
696-
* elideable but not pure. Elsewhere, accessing the apply method might cause the initialization
697-
* of a containing object so they are merely idempotent.
698-
*/
699-
object isElideableExpr {
700-
def isStatElideable(tree: Tree)(using Context): Boolean = unsplice(tree) match {
701-
case EmptyTree
702-
| TypeDef(_, _)
703-
| Import(_, _)
704-
| DefDef(_, _, _, _) =>
705-
true
706-
case vdef @ ValDef(_, _, _) =>
707-
if (vdef.symbol.flags is Mutable) false else apply(vdef.rhs)
708-
case _ =>
709-
false
710-
}
711-
712-
def apply(tree: Tree): Boolean = unsplice(tree) match {
713-
case EmptyTree
714-
| This(_)
715-
| Super(_, _)
716-
| Literal(_) =>
717-
true
718-
case Ident(_) =>
719-
isPureRef(tree) || tree.symbol.isAllOf(Inline | Param)
720-
case Select(qual, _) =>
721-
if (tree.symbol.is(Erased)) true
722-
else isPureRef(tree) && apply(qual)
723-
case New(_) | Closure(_, _, _) =>
724-
true
725-
case TypeApply(fn, _) =>
726-
if (fn.symbol.is(Erased) || fn.symbol == defn.QuotedTypeModule_of) true else apply(fn)
727-
case Apply(fn, args) =>
728-
val isCaseClassApply = {
729-
val cls = tree.tpe.classSymbol
730-
val meth = fn.symbol
731-
meth.name == nme.apply &&
732-
meth.flags.is(Synthetic) &&
733-
meth.owner.linkedClass.is(Case) &&
734-
cls.isNoInitsRealClass
735-
}
736-
if isPureApply(tree, fn) then
737-
apply(fn) && args.forall(apply)
738-
else if (isCaseClassApply)
739-
args.forall(apply)
740-
else if (fn.symbol.is(Erased)) true
741-
else false
742-
case Typed(expr, _) =>
743-
apply(expr)
744-
case Block(stats, expr) =>
745-
apply(expr) && stats.forall(isStatElideable)
746-
case Inlined(_, bindings, expr) =>
747-
apply(expr) && bindings.forall(isStatElideable)
748-
case NamedArg(_, expr) =>
749-
apply(expr)
750-
case _ =>
751-
false
752-
}
753-
}
754-
755758
/** Populate `thisProxy` and `paramProxy` as follows:
756759
*
757760
* 1a. If given type refers to a static this, thisProxy binds it to corresponding global reference,

compiler/src/dotty/tools/dotc/typer/Namer.scala

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1442,10 +1442,7 @@ class Namer { typer: Typer =>
14421442
cls.baseClasses.foreach(_.invalidateBaseTypeCache()) // we might have looked before and found nothing
14431443
cls.invalidateMemberCaches() // we might have checked for a member when parents were not known yet.
14441444
cls.setNoInitsFlags(parentsKind(parents), untpd.bodyKind(rest))
1445-
val ctorStable =
1446-
if cls.is(Trait) then cls.is(NoInits)
1447-
else cls.isNoInitsRealClass
1448-
if ctorStable then cls.primaryConstructor.setFlag(StableRealizable)
1445+
cls.setStableConstructor()
14491446
processExports(using localCtx)
14501447
defn.patchStdLibClass(cls)
14511448
addConstructorProxies(cls)

tests/run/i11114.check

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@ class scala.Tuple2$mcII$sp
33
class scala.Tuple2$mcII$sp
44
class scala.Tuple2$mcII$sp
55
class scala.Tuple2$mcII$sp
6+
initialise
7+
class scala.Tuple2

tests/run/i11114.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,5 @@
88

99
import Tuple2.{ apply => t2 }
1010
println(t2(1, 5).getClass)
11+
12+
println({ println("initialise"); Tuple2 }.apply(1, 6).getClass)

0 commit comments

Comments
 (0)