Skip to content

Commit 83ffe00

Browse files
Implement applied constructor types (#22543)
closes #22542 Introduce new syntax to make classes with `tracked` parameters easier to use. The new syntax is essentially the ability to use an application of a class constructor as a type, we call such types applied constructor types. With this new feature the following example compiles correctly and the type in the comment is the resulting type of the applied constructor types. ```scala import scala.language.experimental.modularity class C(tracked val v: Any) val c: C(42) /* C { val v: 42 } */ = C(42) ``` ### Syntax change ``` SimpleType ::= SimpleLiteral | ‘?’ TypeBounds --- | SimpleType1 +++ | SimpleType1 {ParArgumentExprs} ``` A `SimpleType` can now optionally be followed by `ParArgumentExprs`. --------- Co-authored-by: Matt Bovel <[email protected]>
1 parent 43118de commit 83ffe00

19 files changed

+206
-39
lines changed

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

+1-3
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,12 @@ object NamerOps:
3939
*/
4040
extension (tp: Type)
4141
def separateRefinements(cls: ClassSymbol, refinements: mutable.LinkedHashMap[Name, Type] | Null)(using Context): Type =
42-
val widenSkolemsMap = new TypeMap:
43-
def apply(tp: Type) = mapOver(tp.widenSkolem)
4442
tp match
4543
case RefinedType(tp1, rname, rinfo) =>
4644
try tp1.separateRefinements(cls, refinements)
4745
finally
4846
if refinements != null then
49-
val rinfo1 = widenSkolemsMap(rinfo)
47+
val rinfo1 = rinfo.widenSkolems
5048
refinements(rname) = refinements.get(rname) match
5149
case Some(tp) => tp & rinfo1
5250
case None => rinfo1

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ class TypeUtils:
5353
case ps => ps.reduceLeft(AndType(_, _))
5454
}
5555

56+
def widenSkolems(using Context): Type =
57+
val widenSkolemsMap = new TypeMap:
58+
def apply(tp: Type) = mapOver(tp.widenSkolem)
59+
widenSkolemsMap(self)
60+
5661
/** The element types of this tuple type, which can be made up of EmptyTuple, TupleX and `*:` pairs
5762
*/
5863
def tupleElementTypes(using Context): Option[List[Type]] =
@@ -134,7 +139,7 @@ class TypeUtils:
134139
case t => throw TypeError(em"Malformed NamedTuple: names must be string types, but $t was found.")
135140
val values = vals.tupleElementTypesUpTo(bound, normalize).getOrElse(Nil)
136141
names.zip(values)
137-
142+
138143
(if normalize then self.normalized else self).dealias match
139144
// for desugaring and printer, ignore derived types to avoid infinite recursion in NamedTuple.unapply
140145
case defn.NamedTupleDirect(nmes, vals) => extractNamesTypes(nmes, vals)

compiler/src/dotty/tools/dotc/parsing/Parsers.scala

+6-16
Original file line numberDiff line numberDiff line change
@@ -1331,16 +1331,6 @@ object Parsers {
13311331
*/
13321332
def qualId(): Tree = dotSelectors(termIdent())
13331333

1334-
/** Singleton ::= SimpleRef
1335-
* | SimpleLiteral
1336-
* | Singleton ‘.’ id
1337-
* -- not yet | Singleton ‘(’ Singletons ‘)’
1338-
* -- not yet | Singleton ‘[’ Types ‘]’
1339-
*/
1340-
def singleton(): Tree =
1341-
if isSimpleLiteral then simpleLiteral()
1342-
else dotSelectors(simpleRef())
1343-
13441334
/** SimpleLiteral ::= [‘-’] integerLiteral
13451335
* | [‘-’] floatingPointLiteral
13461336
* | booleanLiteral
@@ -2051,7 +2041,7 @@ object Parsers {
20512041
/** SimpleType ::= SimpleLiteral
20522042
* | ‘?’ TypeBounds
20532043
* | SimpleType1
2054-
* | SimpleType ‘(’ Singletons ‘)’ -- under language.experimental.dependent, checked in Typer
2044+
* | SimpleType ‘(’ Singletons ‘)’
20552045
* Singletons ::= Singleton {‘,’ Singleton}
20562046
*/
20572047
def simpleType(): Tree =
@@ -2083,11 +2073,11 @@ object Parsers {
20832073
val start = in.skipToken()
20842074
typeBounds().withSpan(Span(start, in.lastOffset, start))
20852075
else
2086-
def singletonArgs(t: Tree): Tree =
2087-
if in.token == LPAREN && in.featureEnabled(Feature.dependent)
2088-
then singletonArgs(AppliedTypeTree(t, inParensWithCommas(commaSeparated(singleton))))
2089-
else t
2090-
singletonArgs(simpleType1())
2076+
val tpt = simpleType1()
2077+
if in.featureEnabled(Feature.modularity) && in.token == LPAREN then
2078+
parArgumentExprss(wrapNew(tpt))
2079+
else
2080+
tpt
20912081

20922082
/** SimpleType1 ::= id
20932083
* | Singleton `.' id

compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala

+2
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,8 @@ enum ErrorMessageID(val isActive: Boolean = true) extends java.lang.Enum[ErrorMe
225225
case FormatInterpolationErrorID // errorNumber: 209
226226
case ValueClassCannotExtendAliasOfAnyValID // errorNumber: 210
227227
case MatchIsNotPartialFunctionID // errorNumber: 211
228+
case OnlyFullyDependentAppliedConstructorTypeID // errorNumber: 212
229+
case PointlessAppliedConstructorTypeID // errorNumber: 213
228230

229231
def errorNumber = ordinal - 1
230232

compiler/src/dotty/tools/dotc/reporting/messages.scala

+21
Original file line numberDiff line numberDiff line change
@@ -3487,3 +3487,24 @@ class MatchIsNotPartialFunction(using Context) extends SyntaxMsg(MatchIsNotParti
34873487
|
34883488
|Efficient operations will use `applyOrElse` to avoid computing the match twice,
34893489
|but the `apply` body would be executed "per element" in the example."""
3490+
3491+
final class PointlessAppliedConstructorType(tpt: untpd.Tree, args: List[untpd.Tree], tpe: Type)(using Context) extends TypeMsg(PointlessAppliedConstructorTypeID):
3492+
override protected def msg(using Context): String =
3493+
val act = i"$tpt(${args.map(_.show).mkString(", ")})"
3494+
i"""|Applied constructor type $act has no effect.
3495+
|The resulting type of $act is the same as its base type, namely: $tpe""".stripMargin
3496+
3497+
override protected def explain(using Context): String =
3498+
i"""|Applied constructor types are used to ascribe specialized types of constructor applications.
3499+
|To benefit from this feature, the constructor in question has to have a more specific type than the class itself.
3500+
|
3501+
|If you want to track a precise type of any of the class parameters, make sure to mark the parameter as `tracked`.
3502+
|Otherwise, you can safely remove the argument list from the type.
3503+
|"""
3504+
3505+
final class OnlyFullyDependentAppliedConstructorType()(using Context)
3506+
extends TypeMsg(OnlyFullyDependentAppliedConstructorTypeID):
3507+
override protected def msg(using Context): String =
3508+
i"Applied constructor type can only be used with classes where all parameters in the first parameter list are tracked"
3509+
3510+
override protected def explain(using Context): String = ""

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

+26
Original file line numberDiff line numberDiff line change
@@ -1282,6 +1282,10 @@ trait Applications extends Compatibility {
12821282
}
12831283
else {
12841284
val app = tree.fun match
1285+
case _ if ctx.mode.is(Mode.Type) && Feature.enabled(Feature.modularity) && !ctx.isAfterTyper =>
1286+
untpd.methPart(tree.fun) match
1287+
case Select(nw @ New(_), _) => typedAppliedConstructorType(nw, tree.args, tree)
1288+
case _ => realApply
12851289
case untpd.TypeApply(_: untpd.SplicePattern, _) if Feature.quotedPatternsWithPolymorphicFunctionsEnabled =>
12861290
typedAppliedSpliceWithTypes(tree, pt)
12871291
case _: untpd.SplicePattern => typedAppliedSplice(tree, pt)
@@ -1715,6 +1719,28 @@ trait Applications extends Compatibility {
17151719
def typedUnApply(tree: untpd.UnApply, selType: Type)(using Context): UnApply =
17161720
throw new UnsupportedOperationException("cannot type check an UnApply node")
17171721

1722+
/** Typecheck an applied constructor type – An Apply node in Type mode.
1723+
* This expands to the type this term would have if it were typed as an expression.
1724+
*
1725+
* e.g.
1726+
* ```scala
1727+
* // class C(tracked val v: Any)
1728+
* val c: C(42) = ???
1729+
* ```
1730+
*/
1731+
def typedAppliedConstructorType(nw: untpd.New, args: List[untpd.Tree], tree: untpd.Apply)(using Context) =
1732+
val tree1 = typedExpr(tree)
1733+
val preciseTp = tree1.tpe.widenSkolems
1734+
val classTp = typedType(nw.tpt).tpe
1735+
def classSymbolHasOnlyTrackedParameters =
1736+
!classTp.classSymbol.primaryConstructor.paramSymss.nestedExists: param =>
1737+
param.isTerm && !param.is(Tracked)
1738+
if !preciseTp.isError && !classSymbolHasOnlyTrackedParameters then
1739+
report.warning(OnlyFullyDependentAppliedConstructorType(), tree.srcPos)
1740+
if !preciseTp.isError && (preciseTp frozen_=:= classTp) then
1741+
report.warning(PointlessAppliedConstructorType(nw.tpt, args, classTp), tree.srcPos)
1742+
TypeTree(preciseTp)
1743+
17181744
/** Is given method reference applicable to argument trees `args`?
17191745
* @param resultType The expected result type of the application
17201746
*/

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ object ErrorReporting {
294294

295295
def dependentMsg =
296296
"""Term-dependent types are experimental,
297-
|they must be enabled with a `experimental.dependent` language import or setting""".stripMargin.toMessage
297+
|they must be enabled with a `experimental.modularity` language import or setting""".stripMargin.toMessage
298298

299299
def err(using Context): Errors = new Errors
300300
}

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

+3-11
Original file line numberDiff line numberDiff line change
@@ -2557,17 +2557,9 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
25572557
}
25582558

25592559
def typedAppliedTypeTree(tree: untpd.AppliedTypeTree)(using Context): Tree = {
2560-
tree.args match
2561-
case arg :: _ if arg.isTerm =>
2562-
if Feature.dependentEnabled then
2563-
return errorTree(tree, em"Not yet implemented: T(...)")
2564-
else
2565-
return errorTree(tree, dependentMsg)
2566-
case _ =>
2567-
2568-
val tpt1 = withoutMode(Mode.Pattern) {
2560+
val tpt1 = withoutMode(Mode.Pattern):
25692561
typed(tree.tpt, AnyTypeConstructorProto)
2570-
}
2562+
25712563
val tparams = tpt1.tpe.typeParams
25722564
if tpt1.tpe.isError then
25732565
val args1 = tree.args.mapconserve(typedType(_))
@@ -2691,7 +2683,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
26912683
typeIndexedLambdaTypeTree(tree, tparams, body)
26922684

26932685
def typedTermLambdaTypeTree(tree: untpd.TermLambdaTypeTree)(using Context): Tree =
2694-
if Feature.dependentEnabled then
2686+
if Feature.enabled(Feature.modularity) then
26952687
errorTree(tree, em"Not yet implemented: (...) =>> ...")
26962688
else
26972689
errorTree(tree, dependentMsg)

compiler/test/dotc/neg-best-effort-unpickling.excludelist

+3
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,6 @@ i18750.scala
1818

1919
# Crash on invalid prefix ([A] =>> Int)
2020
i22357a.scala
21+
22+
# `110 (of class java.lang.Integer)`
23+
context-function-syntax.scala

docs/_docs/internals/syntax.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ AnnotType1 ::= SimpleType1 {Annotation}
196196
197197
SimpleType ::= SimpleLiteral SingletonTypeTree(l)
198198
| ‘?’ TypeBounds
199-
| SimpleType1
199+
| SimpleType1 {ParArgumentExprs}
200200
SimpleType1 ::= id Ident(name)
201201
| Singleton ‘.’ id Select(t, name)
202202
| Singleton ‘.’ ‘type’ SingletonTypeTree(p)

docs/_docs/reference/experimental/modularity.md

+37
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,43 @@ LocalModifier ::= ‘tracked’
196196

197197
The (soft) `tracked` modifier is allowed as a local modifier.
198198

199+
## Applied constructor types
200+
201+
A new syntax is also introduced, to make classes with `tracked` parameters
202+
easier to use. The new syntax is essentially the ability to use an application
203+
of a class constructor as a type, we call such types applied constructor types.
204+
205+
With this new feature the following example compiles correctly and the type in
206+
the comment is the resulting type of the applied constructor types.
207+
208+
```scala
209+
import scala.language.experimental.modularity
210+
211+
class C(tracked val v: Any)
212+
213+
val c: C(42) /* C { val v: 42 } */ = C(42)
214+
```
215+
216+
### Syntax change
217+
218+
```
219+
SimpleType ::= SimpleLiteral
220+
| ‘?’ TypeBounds
221+
--- | SimpleType1
222+
+++ | SimpleType1 {ParArgumentExprs}
223+
```
224+
225+
A `SimpleType` can now optionally be followed by `ParArgumentExprs`.
226+
227+
The arguments are used to typecheck the whole type, as if it was a normal
228+
constructor application. For classes with `tracked` parameters this will mean
229+
that the resulting type will have a refinement for each `tracked` parameter.
230+
231+
For example, given the following class definition:
232+
```scala
233+
class Person(tracked val name: String, tracked val age: Int)
234+
```
235+
**Type** `Person("Kasia", 27)` will be translated to `Person { val name: "Kasia"; val age: 27 }`.
199236

200237
## Allow Class Parents to be Refined Types
201238

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
-- [E006] Not Found Error: tests/neg/applied_constructor_types.scala:8:10 ----------------------------------------------
2+
8 | val v1: f(1) = f(1) // error
3+
| ^
4+
| Not found: type f
5+
|
6+
| longer explanation available when compiling with `-explain`
7+
-- [E006] Not Found Error: tests/neg/applied_constructor_types.scala:9:10 ----------------------------------------------
8+
9 | val v2: id(1) = f(1) // error
9+
| ^^
10+
| Not found: type id - did you mean is?
11+
|
12+
| longer explanation available when compiling with `-explain`
13+
-- [E006] Not Found Error: tests/neg/applied_constructor_types.scala:10:10 ---------------------------------------------
14+
10 | val v3: idDependent(1) = f(1) // error
15+
| ^^^^^^^^^^^
16+
| Not found: type idDependent
17+
|
18+
| longer explanation available when compiling with `-explain`
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import scala.language.experimental.modularity
2+
3+
def f(x: Int): Int = x
4+
def id[T](x: T): T = x
5+
def idDependent(x: Any): x.type = x
6+
7+
def test =
8+
val v1: f(1) = f(1) // error
9+
val v2: id(1) = f(1) // error
10+
val v3: idDependent(1) = f(1) // error

tests/neg/context-function-syntax.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
val test =
2-
(using x: Int) => x // error // error
2+
(using x: Int) => x // error // error // error
33

44
val f = () ?=> 23 // error
55
val g: ContextFunction0[Int] = ??? // ok

tests/neg/deptypes.scala

+4-4
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22

33
type Vec[T] = (n: Int) =>> Array[T] // error: not yet implemented
44

5-
type Matrix[T](m: Int, n: Int) = Vec[Vec[T](n)](m) // error: not yet implemented
5+
type Matrix[T](m: Int, n: Int) = Vec[Vec[T](n)](m) // error // error: not yet implemented
66

7-
type Tensor2[T](m: Int)(n: Int) = Matrix[T](m, n) // error: not yet implemented
7+
type Tensor2[T](m: Int)(n: Int) = Matrix[T](m, n)
88

9-
val x: Vec[Int](10) = ??? // error: not yet implemented
9+
val x: Vec[Int](10) = ???
1010
val n = 10
11-
type T = Vec[String](n) // error: not yet implemented
11+
type T = Vec[String](n)

tests/neg/i7751.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
import language.`3.3`
2-
val a = Some(a=a,)=> // error // error // error // error
2+
val a = Some(a=a,)=> // error // error // error
33
val a = Some(x=y,)=>
+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import scala.language.experimental.modularity
2+
3+
class Box(tracked val v: Any)
4+
class C(tracked val x: Int)
5+
class NC(tracked val c: C)
6+
class NNC(tracked val c: NC)
7+
class F[A](tracked val a: Int)
8+
class G[A](tracked val a: A)
9+
class NF[A](tracked val f: F[A])
10+
11+
class Person(val name: String, tracked val age: Int)
12+
class PersonPrime(val name: String)(tracked val age: Int)
13+
class PersonBis(tracked val name: String)(val age: Int)
14+
15+
class Generic[A](val a: A)
16+
17+
object O:
18+
val m: Int = 27
19+
20+
class InnerClass(tracked val x: Int)
21+
22+
object Test extends App {
23+
val c: C(42) = C(42)
24+
val nc: NC(C(42)) = NC(C(42))
25+
val nc1: NC(c) = NC(c)
26+
val nnc: NNC(NC(C(42))) = NNC(NC(C(42)))
27+
val f: F[Int](42) = F[Int](42)
28+
val f2: F[Int](42) = F(42)
29+
val f3: F(42) = F(42)
30+
val g: G(42) = G(42)
31+
32+
val n: Int = 27
33+
val c2: C(n) = C(n)
34+
val c3: C(O.m) = C(O.m)
35+
36+
val box: Box(O.InnerClass(42)) = Box(O.InnerClass(42))
37+
val box2: Box(O.InnerClass(n)) = Box(O.InnerClass(n))
38+
val box3: Box(O.InnerClass(O.m)) = Box(O.InnerClass(O.m))
39+
40+
val person: Person("Kasia", 27) = Person("Kasia", 27) // warn
41+
val person1: Person("Kasia", n) = Person("Kasia", n) // warn
42+
val person2: Person("Kasia", O.m) = Person("Kasia", O.m) // warn
43+
44+
val personPrime: PersonPrime("Kasia")(27) = PersonPrime("Kasia")(27) // warn
45+
val personPrime1: PersonPrime("Kasia")(n) = PersonPrime("Kasia")(n) // warn
46+
val personPrime2: PersonPrime("Kasia")(O.m) = PersonPrime("Kasia")(O.m) // warn
47+
48+
val personBis: PersonBis("Kasia")(27) = PersonBis("Kasia")(27) // warn
49+
val personBis1: PersonBis("Kasia")(n) = PersonBis("Kasia")(n) // warn
50+
val personBis2: PersonBis("Kasia")(O.m) = PersonBis("Kasia")(O.m) // warn
51+
52+
val generic1: Generic(compiletime.erasedValue[Int]) = Generic(42) // warn
53+
val generic2: Generic(??? : Int) = Generic(42) // warn
54+
val generic3: Generic(43) = Generic(42) // warn
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-- [E212] Type Warning: tests/warn/applied_constructor_types.scala:6:10 ------------------------------------------------
2+
6 | val v1: UnspecificBox(4) = UnspecificBox(4) // warn
3+
| ^^^^^^^^^^^^^^^^
4+
|Applied constructor type can only be used with classes where all parameters in the first parameter list are tracked
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import scala.language.experimental.modularity
2+
3+
class UnspecificBox(val v: Any)
4+
5+
def test =
6+
val v1: UnspecificBox(4) = UnspecificBox(4) // warn

0 commit comments

Comments
 (0)