Skip to content

Commit a0e3525

Browse files
committed
Handle nullability info in by-name arguments
Implements the scheme described in my comment to #7546. - We type check an argument without or with flow info, depending on whether the function part of the application is known to take a call-by-name parameter. If we know nothing about the function part, we assume call-by-value. - At the end of type checking an application, if the argument is known to be by-name, remove all x.$asInstanceOf$[T] casts in the argument where x is a mutable variable, and run the type assigner (not the type checker!) again on the result. If this succeeds and gives a type that is still compatible with the formal parameter type, we are done. - Otherwise, issue an error saying that the argument cannot be treated as call-by-name since it contains flow-assumptions about mutable variables. As a remedy, suggest to wrap the argument in a `scala.compiletime.byName(...)` call. Here, `byName` is defined as follws: ``` inline def byName[T](x: => T): T ``` Wrapping an argument with byName means that we know statically that it is passed to a by-name parameter, so it will be typechecked without flow info.
1 parent 77ca516 commit a0e3525

File tree

5 files changed

+85
-5
lines changed

5 files changed

+85
-5
lines changed

compiler/src/dotty/tools/dotc/ast/TreeInfo.scala

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -861,6 +861,19 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] =>
861861
case _ => None
862862
}
863863
}
864+
865+
/** Extractor for not-null assertions */
866+
object AssertNotNull with
867+
def apply(tree: tpd.Tree, tpnn: Type)(given Context): tpd.Tree =
868+
tree.select(defn.Any_typeCast).appliedToType(AndType(tree.tpe, tpnn))
869+
870+
def unapply(tree: tpd.TypeApply)(given Context): Option[tpd.Tree] = tree match
871+
case TypeApply(Select(qual: RefTree, nme.asInstanceOfPM), arg :: Nil) =>
872+
arg.tpe match
873+
case AndType(ref, _) if qual.tpe eq ref => Some(qual)
874+
case _ => None
875+
case _ => None
876+
end AssertNotNull
864877
}
865878

866879
object TreeInfo {

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

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -793,7 +793,7 @@ trait Applications extends Compatibility {
793793
/** Subclass of Application for type checking an Apply node with untyped arguments. */
794794
class ApplyToUntyped(app: untpd.Apply, fun: Tree, methRef: TermRef, proto: FunProto, resultType: Type)(implicit ctx: Context)
795795
extends TypedApply(app, fun, methRef, proto.args, resultType) {
796-
def typedArg(arg: untpd.Tree, formal: Type): TypedArg = proto.typedArg(arg, formal.widenExpr)
796+
def typedArg(arg: untpd.Tree, formal: Type): TypedArg = proto.typedArg(arg, formal)
797797
def treeToArg(arg: Tree): untpd.Tree = untpd.TypedSplice(arg)
798798
def typeOfArg(arg: untpd.Tree): Type = proto.typeOfArg(arg)
799799
}
@@ -867,7 +867,8 @@ trait Applications extends Compatibility {
867867
else
868868
new ApplyToUntyped(tree, fun1, funRef, proto, pt)(
869869
given fun1.nullableInArgContext(given argCtx(tree)))
870-
convertNewGenericArray(app.result).computeNullable()
870+
convertNewGenericArray(
871+
postProcessByNameArgs(funRef, app.result).computeNullable())
871872
case _ =>
872873
handleUnexpectedFunType(tree, fun1)
873874
}
@@ -1029,7 +1030,7 @@ trait Applications extends Compatibility {
10291030
* It is performed during typer as creation of generic arrays needs a classTag.
10301031
* we rely on implicit search to find one.
10311032
*/
1032-
def convertNewGenericArray(tree: Tree)(implicit ctx: Context): Tree = tree match {
1033+
def convertNewGenericArray(tree: Tree)(implicit ctx: Context): Tree = tree match {
10331034
case Apply(TypeApply(tycon, targs@(targ :: Nil)), args) if tycon.symbol == defn.ArrayConstructor =>
10341035
fullyDefinedType(tree.tpe, "array", tree.span)
10351036

@@ -1045,6 +1046,56 @@ trait Applications extends Compatibility {
10451046
tree
10461047
}
10471048

1049+
/** Post process all arguments to by-name parameters by removing any not-null
1050+
* info that was used when typing them. Concretely:
1051+
* If an argument corresponds to a call-by-name parameter, drop all
1052+
* embedded not-null assertions of the form `x.$asInstanceOf[x.type & T]`
1053+
* where `x` is a reference to a mutable variable. If the argument still typechecks
1054+
* with the removed assertions and is still compatible with the formal parameter,
1055+
* keep it. Otherwise issue an error that the call-by-name argument was typed using
1056+
* flow assumptions about mutable variables and suggest that it is enclosed
1057+
* in a `byName(...)` call instead.
1058+
*/
1059+
private def postProcessByNameArgs(fn: TermRef, app: Tree)(given ctx: Context): Tree =
1060+
fn.widen match
1061+
case mt: MethodType if mt.paramInfos.exists.isInstanceOf[ExprType] =>
1062+
app match
1063+
case Apply(fn, args) =>
1064+
val dropNotNull = new TreeMap with
1065+
override def transform(t: Tree)(given Context) = t match
1066+
case AssertNotNull(t0) if t0.symbol.is(Mutable) => transform(t0)
1067+
case t: ValDef if !t.symbol.is(Lazy) => super.transform(t)
1068+
case t: MemberDef => t // stop here since embedded references are out of order anyway
1069+
case t => super.transform(t)
1070+
1071+
def postProcess(formal: Type, arg: Tree): Tree =
1072+
val arg1 = dropNotNull.transform(arg)
1073+
if (arg1 ne arg) && !(arg1.tpe <:< formal) then
1074+
ctx.error(em"""This argument was typed using flow assumptions about mutable variables
1075+
|but it is passed to a by-name parameter where such flow assumptions are unsound.
1076+
|Wrapping the argument in `byName(...)` fixes the problem by disabling the flow assumptions.
1077+
|
1078+
|`byName` needs to be imported from the `scala.compiletime` package.""",
1079+
arg.sourcePos)
1080+
arg
1081+
else
1082+
arg1
1083+
1084+
def recur(formals: List[Type], args: List[Tree]): List[Tree] = (formals, args) match
1085+
case (formal :: formalsRest, arg :: argsRest) =>
1086+
val arg1 = postProcess(formal.widenExpr.repeatedToSingle, arg)
1087+
val argsRest1 = recur(
1088+
if formal.isRepeatedParam then formals else formalsRest,
1089+
argsRest)
1090+
if (arg1 eq arg) && (argsRest1 eq argsRest) then args
1091+
else arg1 :: argsRest1
1092+
case _ => args
1093+
1094+
tpd.cpy.Apply(app)(fn, recur(mt.paramInfos, args))
1095+
case _ => app
1096+
case _ => app
1097+
end postProcessByNameArgs
1098+
10481099
def typedUnApply(tree: untpd.Apply, selType: Type)(implicit ctx: Context): Tree = {
10491100
record("typedUnApply")
10501101
val Apply(qual, args) = tree

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,13 @@ object Nullables with
161161
then infos
162162
else info :: infos
163163

164+
/** Retract all references to mutable variables */
165+
def retractMutables(given Context) =
166+
val mutables = infos.foldLeft(Set[TermRef]())((ms, info) =>
167+
ms.union(info.asserted.filter(_.symbol.is(Mutable))))
168+
infos.extendWith(NotNullInfo(Set(), mutables))
169+
end notNullInfoOps
170+
164171
given treeOps: extension (tree: Tree) with
165172

166173
/* The `tree` with added nullability attachment */

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -322,9 +322,15 @@ object ProtoTypes {
322322
* used to avoid repeated typings of trees when backtracking.
323323
*/
324324
def typedArg(arg: untpd.Tree, formal: Type)(implicit ctx: Context): Tree = {
325+
val wideFormal = formal.widenExpr
326+
val argCtx =
327+
if wideFormal eq formal then ctx
328+
else ctx.withNotNullInfos(ctx.notNullInfos.retractMutables)
325329
val locked = ctx.typerState.ownedVars
326-
val targ = cacheTypedArg(arg, typer.typedUnadapted(_, formal, locked), force = true)
327-
typer.adapt(targ, formal, locked)
330+
val targ = cacheTypedArg(arg,
331+
typer.typedUnadapted(_, wideFormal, locked)(given argCtx),
332+
force = true)
333+
typer.adapt(targ, wideFormal, locked)
328334
}
329335

330336
/** The type of the argument `arg`, or `NoType` if `arg` has not been typed before

library/src/scala/compiletime/package.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,7 @@ package object compiletime {
6363
* }
6464
*/
6565
type S[N <: Int] <: Int
66+
67+
/** Assertion that an argument is by-name. Used for nullability checking. */
68+
def byName[T](x: => T): T = x
6669
}

0 commit comments

Comments
 (0)