Skip to content

Commit 0d950fb

Browse files
authored
Merge pull request #7727 from dotty-staging/change-byname-nullable
Handle nullability info in by-name arguments
2 parents 8101aeb + 643c616 commit 0d950fb

File tree

9 files changed

+218
-6
lines changed

9 files changed

+218
-6
lines changed

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

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

866881
object TreeInfo {

compiler/src/dotty/tools/dotc/config/Printers.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ object Printers {
3030
val lexical: Printer = noPrinter
3131
val inlining: Printer = noPrinter
3232
val interactiv: Printer = noPrinter
33+
val nullables: Printer = noPrinter
3334
val overload: Printer = noPrinter
3435
val patmatch: Printer = noPrinter
3536
val pickling: Printer = noPrinter

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import NameKinds.DefaultGetterName
2222
import ProtoTypes._
2323
import Inferencing._
2424
import transform.TypeUtils._
25-
import Nullables.given
25+
import Nullables.{postProcessByNameArgs, given}
2626

2727
import collection.mutable
2828
import config.Printers.{overload, typr, unapp}
@@ -794,7 +794,7 @@ trait Applications extends Compatibility {
794794
/** Subclass of Application for type checking an Apply node with untyped arguments. */
795795
class ApplyToUntyped(app: untpd.Apply, fun: Tree, methRef: TermRef, proto: FunProto, resultType: Type)(implicit ctx: Context)
796796
extends TypedApply(app, fun, methRef, proto.args, resultType) {
797-
def typedArg(arg: untpd.Tree, formal: Type): TypedArg = proto.typedArg(arg, formal.widenExpr)
797+
def typedArg(arg: untpd.Tree, formal: Type): TypedArg = proto.typedArg(arg, formal)
798798
def treeToArg(arg: Tree): untpd.Tree = untpd.TypedSplice(arg)
799799
def typeOfArg(arg: untpd.Tree): Type = proto.typeOfArg(arg)
800800
}
@@ -868,7 +868,8 @@ trait Applications extends Compatibility {
868868
else
869869
new ApplyToUntyped(tree, fun1, funRef, proto, pt)(
870870
given fun1.nullableInArgContext(given argCtx(tree)))
871-
convertNewGenericArray(app.result).computeNullable()
871+
convertNewGenericArray(
872+
postProcessByNameArgs(funRef, app.result).computeNullable())
872873
case _ =>
873874
handleUnexpectedFunType(tree, fun1)
874875
}
@@ -1030,7 +1031,7 @@ trait Applications extends Compatibility {
10301031
* It is performed during typer as creation of generic arrays needs a classTag.
10311032
* we rely on implicit search to find one.
10321033
*/
1033-
def convertNewGenericArray(tree: Tree)(implicit ctx: Context): Tree = tree match {
1034+
def convertNewGenericArray(tree: Tree)(implicit ctx: Context): Tree = tree match {
10341035
case Apply(TypeApply(tycon, targs@(targ :: Nil)), args) if tycon.symbol == defn.ArrayConstructor =>
10351036
fullyDefinedType(tree.tpe, "array", tree.span)
10361037

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

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import util.Spans.Span
1212
import Flags._
1313
import NullOpsDecorator._
1414
import collection.mutable
15+
import config.Printers.nullables
16+
import ast.{tpd, untpd}
1517

1618
/** Operations for implementing a flow analysis for nullability */
1719
object Nullables with
@@ -182,6 +184,13 @@ object Nullables with
182184
then infos
183185
else info :: infos
184186

187+
/** Retract all references to mutable variables */
188+
def retractMutables(given Context) =
189+
val mutables = infos.foldLeft(Set[TermRef]())((ms, info) =>
190+
ms.union(info.asserted.filter(_.symbol.is(Mutable))))
191+
infos.extendWith(NotNullInfo(Set(), mutables))
192+
end notNullInfoOps
193+
185194
given refOps: extension (ref: TermRef) with
186195

187196
/** Is the use of a mutable variable out of order
@@ -443,4 +452,70 @@ object Nullables with
443452
val retractedVars = curCtx.notNullInfos.flatMap(_.asserted.filter(isRetracted)).toSet
444453
curCtx.addNotNullInfo(NotNullInfo(Set(), retractedVars))
445454

455+
/** Post process all arguments to by-name parameters by removing any not-null
456+
* info that was used when typing them. Concretely:
457+
* If an argument corresponds to a call-by-name parameter, drop all
458+
* embedded not-null assertions of the form `x.$asInstanceOf[x.type & T]`
459+
* where `x` is a reference to a mutable variable. If the argument still typechecks
460+
* with the removed assertions and is still compatible with the formal parameter,
461+
* keep it. Otherwise issue an error that the call-by-name argument was typed using
462+
* flow assumptions about mutable variables and suggest that it is enclosed
463+
* in a `byName(...)` call instead.
464+
*/
465+
def postProcessByNameArgs(fn: TermRef, app: Tree)(given ctx: Context): Tree =
466+
fn.widen match
467+
case mt: MethodType if mt.paramInfos.exists(_.isInstanceOf[ExprType]) =>
468+
app match
469+
case Apply(fn, args) =>
470+
val dropNotNull = new TreeMap with
471+
override def transform(t: Tree)(given Context) = t match
472+
case AssertNotNull(t0) if t0.symbol.is(Mutable) =>
473+
nullables.println(i"dropping $t")
474+
transform(t0)
475+
case t: ValDef if !t.symbol.is(Lazy) => super.transform(t)
476+
case t: MemberDef =>
477+
// stop here since embedded references to mutable variables would be
478+
// out of order, so they would not asserted ot be not-null anyway.
479+
// @see Nullables.usedOutOfOrder
480+
t
481+
case _ => super.transform(t)
482+
483+
object retyper extends ReTyper with
484+
override def typedUnadapted(t: untpd.Tree, pt: Type, locked: TypeVars)(implicit ctx: Context): Tree = t match
485+
case t: ValDef if !t.symbol.is(Lazy) => super.typedUnadapted(t, pt, locked)
486+
case t: MemberDef => promote(t)
487+
case _ => super.typedUnadapted(t, pt, locked)
488+
489+
def postProcess(formal: Type, arg: Tree): Tree =
490+
val arg1 = dropNotNull.transform(arg)
491+
if arg1 eq arg then arg
492+
else
493+
val nestedCtx = ctx.fresh.setNewTyperState()
494+
val arg2 = retyper.typed(arg1, formal)(given nestedCtx)
495+
if nestedCtx.reporter.hasErrors || !(arg2.tpe <:< formal) then
496+
ctx.error(em"""This argument was typed using flow assumptions about mutable variables
497+
|but it is passed to a by-name parameter where such flow assumptions are unsound.
498+
|Wrapping the argument in `byName(...)` fixes the problem by disabling the flow assumptions.
499+
|
500+
|`byName` needs to be imported from the `scala.compiletime` package.""",
501+
arg.sourcePos)
502+
arg
503+
else
504+
nestedCtx.typerState.commit()
505+
arg2
506+
507+
def recur(formals: List[Type], args: List[Tree]): List[Tree] = (formals, args) match
508+
case (formal :: formalsRest, arg :: argsRest) =>
509+
val arg1 = postProcess(formal.widenExpr.repeatedToSingle, arg)
510+
val argsRest1 = recur(
511+
if formal.isRepeatedParam then formals else formalsRest,
512+
argsRest)
513+
if (arg1 eq arg) && (argsRest1 eq argsRest) then args
514+
else arg1 :: argsRest1
515+
case _ => args
516+
517+
tpd.cpy.Apply(app)(fn, recur(mt.paramInfos, args))
518+
case _ => app
519+
case _ => app
520+
end postProcessByNameArgs
446521
end Nullables

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

compiler/test/dotty/tools/dotc/CompilationTests.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ class CompilationTests extends ParallelTesting {
115115
compileFilesInDir("tests/neg-custom-args/deprecation", defaultOptions.and("-Xfatal-warnings", "-deprecation")),
116116
compileFilesInDir("tests/neg-custom-args/fatal-warnings", defaultOptions.and("-Xfatal-warnings")),
117117
compileFilesInDir("tests/neg-custom-args/allow-double-bindings", allowDoubleBindings),
118+
compileFilesInDir("tests/neg-custom-args/explicit-nulls", defaultOptions.and("-Yexplicit-nulls")),
118119
compileDir("tests/neg-custom-args/impl-conv", defaultOptions.and("-Xfatal-warnings", "-feature")),
119120
compileFile("tests/neg-custom-args/implicit-conversions.scala", defaultOptions.and("-Xfatal-warnings", "-feature")),
120121
compileFile("tests/neg-custom-args/implicit-conversions-old.scala", defaultOptions.and("-Xfatal-warnings", "-feature")),

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
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
-- [E007] Type Mismatch Error: tests/neg-custom-args/explicit-nulls/byname-nullables.scala:19:24 -----------------------
2+
19 | if x != null then f(x) // error: f is call-by-name
3+
| ^
4+
| Found: (x : String | Null)
5+
| Required: String
6+
-- Error: tests/neg-custom-args/explicit-nulls/byname-nullables.scala:43:32 --------------------------------------------
7+
43 | if x != null then f(identity(x), 1) // error: dropping not null check fails typing
8+
| ^^^^^^^^^^^
9+
| This argument was typed using flow assumptions about mutable variables
10+
| but it is passed to a by-name parameter where such flow assumptions are unsound.
11+
| Wrapping the argument in `byName(...)` fixes the problem by disabling the flow assumptions.
12+
|
13+
| `byName` needs to be imported from the `scala.compiletime` package.
14+
-- Error: tests/neg-custom-args/explicit-nulls/byname-nullables.scala:68:24 --------------------------------------------
15+
68 | if x != null then f(x, 1) // error: dropping not null check typechecks OK, but gives incompatible result type
16+
| ^
17+
| This argument was typed using flow assumptions about mutable variables
18+
| but it is passed to a by-name parameter where such flow assumptions are unsound.
19+
| Wrapping the argument in `byName(...)` fixes the problem by disabling the flow assumptions.
20+
|
21+
| `byName` needs to be imported from the `scala.compiletime` package.
22+
-- [E134] Type Mismatch Error: tests/neg-custom-args/explicit-nulls/byname-nullables.scala:81:22 -----------------------
23+
81 | if x != null then f(byName(x), 1) // error: none of the overloaded methods match argument types
24+
| ^
25+
| None of the overloaded alternatives of method f in object Test7 with types
26+
| (x: => String, y: Int): String
27+
| (x: String, y: String): String
28+
| match arguments (String | Null, (1 : Int))
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
object Test1 with
2+
3+
def f(x: String) =
4+
x ++ x
5+
6+
def g() =
7+
var x: String | Null = "abc"
8+
if x != null then f(x) // OK: f is call-by-value
9+
else x
10+
11+
12+
object Test2 with
13+
14+
def f(x: => String) =
15+
x ++ x
16+
17+
def g() =
18+
var x: String | Null = "abc"
19+
if x != null then f(x) // error: f is call-by-name
20+
else x
21+
22+
object Test3 with
23+
24+
def f(x: String, y: String) = x
25+
26+
def f(x: => String | Null, y: Int) =
27+
x
28+
29+
def g() =
30+
var x: String | Null = "abc"
31+
if x != null then f(x, 1) // OK: not-null check successfully dropped
32+
else x
33+
34+
object Test4 with
35+
36+
def f(x: String, y: String) = x
37+
38+
def f(x: => String | Null, y: Int) =
39+
x
40+
41+
def g() =
42+
var x: String | Null = "abc"
43+
if x != null then f(identity(x), 1) // error: dropping not null check fails typing
44+
else x
45+
46+
object Test5 with
47+
import compiletime.byName
48+
49+
def f(x: String, y: String) = x
50+
51+
def f(x: => String | Null, y: Int) =
52+
x
53+
54+
def g() =
55+
var x: String | Null = "abc"
56+
if x != null then f(byName(identity(x)), 1) // OK, byName avoids the flow typing
57+
else x
58+
59+
object Test6 with
60+
61+
def f(x: String, y: String) = x
62+
63+
def f(x: => String, y: Int) =
64+
x
65+
66+
def g() =
67+
var x: String | Null = "abc"
68+
if x != null then f(x, 1) // error: dropping not null check typechecks OK, but gives incompatible result type
69+
else x
70+
71+
object Test7 with
72+
import compiletime.byName
73+
74+
def f(x: String, y: String) = x
75+
76+
def f(x: => String, y: Int) =
77+
x
78+
79+
def g() =
80+
var x: String | Null = "abc"
81+
if x != null then f(byName(x), 1) // error: none of the overloaded methods match argument types
82+
else x

0 commit comments

Comments
 (0)