Skip to content

Commit e26cb75

Browse files
mbovelValentin889Sporarum
committed
Runtime checks for qualified types
Co-Authored-By: Valentin Schneeberger <[email protected]> Co-Authored-By: Quentin Bernet <[email protected]>
1 parent 2d1bd7d commit e26cb75

File tree

14 files changed

+522
-9
lines changed

14 files changed

+522
-9
lines changed

compiler/src/dotty/tools/dotc/qualified_types/QualifiedTypes.scala

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,26 @@ import dotty.tools.dotc.ast.tpd.{
1919
Typed,
2020
given
2121
}
22+
import dotty.tools.dotc.config.Printers
2223
import dotty.tools.dotc.core.Atoms
2324
import dotty.tools.dotc.core.Constants.Constant
2425
import dotty.tools.dotc.core.Contexts.{ctx, Context}
25-
import dotty.tools.dotc.core.Decorators.{i, em, toTermName}
26+
import dotty.tools.dotc.core.Decorators.{em, i, toTermName}
2627
import dotty.tools.dotc.core.StdNames.nme
2728
import dotty.tools.dotc.core.Symbols.{defn, Symbol}
28-
import dotty.tools.dotc.core.Types.{AndType, ConstantType, SkolemType, ErrorType, MethodType, OrType, TermRef, Type, TypeProxy}
29-
29+
import dotty.tools.dotc.core.Types.{
30+
AndType,
31+
ConstantType,
32+
ErrorType,
33+
MethodType,
34+
OrType,
35+
SkolemType,
36+
TermRef,
37+
Type,
38+
TypeProxy
39+
}
3040
import dotty.tools.dotc.report
3141
import dotty.tools.dotc.reporting.trace
32-
import dotty.tools.dotc.config.Printers
3342

3443
object QualifiedTypes:
3544
/** Does the type `tp1` imply the qualifier `qualifier2`?
@@ -66,9 +75,22 @@ object QualifiedTypes:
6675
*/
6776
def adapt(tree: Tree, pt: Type)(using Context): Tree =
6877
trace(i"adapt $tree to $pt", Printers.qualifiedTypes):
69-
if containsQualifier(pt) && isSimple(tree) then
70-
val selfifiedTp = QualifiedType(tree.tpe, equalToPredicate(tree))
71-
if selfifiedTp <:< pt then tree.cast(selfifiedTp) else EmptyTree
78+
if containsQualifier(pt) then
79+
if tree.tpe.hasAnnotation(defn.RuntimeCheckedAnnot) then
80+
if checkContainsSkolem(pt) then
81+
tpd.evalOnce(tree): e =>
82+
If(
83+
e.isInstance(pt),
84+
e.asInstance(pt),
85+
Throw(New(defn.IllegalArgumentExceptionType, List()))
86+
)
87+
else
88+
tree.withType(ErrorType(em""))
89+
else if isSimple(tree) then
90+
val selfifiedTp = QualifiedType(tree.tpe, equalToPredicate(tree))
91+
if selfifiedTp <:< pt then tree.cast(selfifiedTp) else EmptyTree
92+
else
93+
EmptyTree
7294
else
7395
EmptyTree
7496

@@ -79,7 +101,7 @@ object QualifiedTypes:
79101
case SeqLiteral(elems, _) => elems.forall(isSimple)
80102
case Typed(expr, _) => isSimple(expr)
81103
case Block(Nil, expr) => isSimple(expr)
82-
case _ => tpd.isIdempotentExpr(tree)
104+
case _ => tpd.isIdempotentExpr(tree)
83105

84106
def containsQualifier(tp: Type)(using Context): Boolean =
85107
tp match
@@ -89,6 +111,18 @@ object QualifiedTypes:
89111
case OrType(tp1, tp2) => containsQualifier(tp1) || containsQualifier(tp2)
90112
case _ => false
91113

114+
def checkContainsSkolem(tp: Type)(using Context): Boolean =
115+
var res = true
116+
tp.foreachPart:
117+
case QualifiedType(_, qualifier) =>
118+
qualifier.foreachSubTree: subTree =>
119+
subTree.tpe.foreachPart:
120+
case tp: SkolemType =>
121+
report.error(em"The qualified type $qualifier cannot be checked at runtime", qualifier.srcPos)
122+
res = false
123+
case _ => ()
124+
case _ => ()
125+
res
92126

93127
private def equalToPredicate(tree: Tree)(using Context): Tree =
94128
Lambda(

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import config.Printers.{ transforms => debug }
1818
import patmat.Typ
1919
import dotty.tools.dotc.util.SrcPos
2020

21+
import qualified_types.QualifiedType
22+
2123
/** This transform normalizes type tests and type casts,
2224
* also replacing type tests with singleton argument type with reference equality check
2325
* Any remaining type tests
@@ -323,7 +325,7 @@ object TypeTestsCasts {
323325
* The transform happens before erasure of `testType`, thus cannot be merged
324326
* with `transformIsInstanceOf`, which depends on erased type of `testType`.
325327
*/
326-
def transformTypeTest(expr: Tree, testType: Type, flagUnrelated: Boolean): Tree = testType.dealias match {
328+
def transformTypeTest(expr: Tree, testType: Type, flagUnrelated: Boolean): Tree = testType.dealiasKeepRefiningAnnots match {
327329
case tref: TermRef if tref.symbol == defn.EmptyTupleModule =>
328330
ref(defn.RuntimeTuples_isInstanceOfEmptyTuple).appliedTo(expr)
329331
case _: SingletonType =>
@@ -352,6 +354,12 @@ object TypeTestsCasts {
352354
ref(defn.RuntimeTuples_isInstanceOfNonEmptyTuple).appliedTo(expr)
353355
case AppliedType(tref: TypeRef, _) if tref.symbol == defn.PairClass =>
354356
ref(defn.RuntimeTuples_isInstanceOfNonEmptyTuple).appliedTo(expr)
357+
case QualifiedType(parent, closureDef(qualifierDef)) =>
358+
evalOnce(expr): e =>
359+
// e.isInstanceOf[baseType] && qualifier(e.asInstanceOf[baseType])
360+
val arg = e.asInstance(parent)
361+
val qualifierTest = BetaReduce.reduceApplication(qualifierDef, List(List(arg))).get
362+
transformTypeTest(e, parent, flagUnrelated).and(qualifierTest)
355363
case _ =>
356364
val testWidened = testType.widen
357365
defn.untestableClasses.find(testWidened.isRef(_)) match

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ class CompilationTests {
172172
compileFilesInDir("tests/run", defaultOptions.and("-Wsafe-init")),
173173
compileFilesInDir("tests/run-deep-subtype", allowDeepSubtypes),
174174
compileFilesInDir("tests/run-custom-args/captures", allowDeepSubtypes.and("-language:experimental.captureChecking", "-source", "3.8")),
175+
compileFilesInDir("tests/run-custom-args/qualified-types", defaultOptions.and("-language:experimental.qualifiedTypes")),
175176
// Run tests for legacy lazy vals.
176177
compileFilesInDir("tests/run", defaultOptions.and("-Wsafe-init", "-Ylegacy-lazy-vals", "-Ycheck-constraint-deps"), FileFilter.include(TestSources.runLazyValsAllowlist)),
177178
).checkRuns()
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
def foo(x: Int, y: {v: Int with v > x}): y.type = y
2+
3+
def getInt(): Int =
4+
println("getInt called")
5+
42
6+
7+
@main def Test =
8+
val res = foo(getInt(), 2.runtimeChecked) // error
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Hello
2+
succeed
3+
65
4+
Hello
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
@main def Test: Unit =
2+
3+
({println ("Hello"); 4}).isInstanceOf[{ y : Int with y > 0}]
4+
5+
class Person(val name: String, var age: Int)
6+
def incAge(p: Person): Int =
7+
p.age += 1
8+
p.age
9+
10+
val p = new Person("Alice", 64)
11+
12+
if incAge(p).isInstanceOf[{v:Int with v == 65}] then println("succeed")
13+
println(p.age)
14+
15+
({println ("Hello"); 4}).isInstanceOf[Int & { y : Int with y > 0}]

0 commit comments

Comments
 (0)