Skip to content

Implement applied constructor types #22543

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
May 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions compiler/src/dotty/tools/dotc/core/NamerOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,12 @@ object NamerOps:
*/
extension (tp: Type)
def separateRefinements(cls: ClassSymbol, refinements: mutable.LinkedHashMap[Name, Type] | Null)(using Context): Type =
val widenSkolemsMap = new TypeMap:
def apply(tp: Type) = mapOver(tp.widenSkolem)
tp match
case RefinedType(tp1, rname, rinfo) =>
try tp1.separateRefinements(cls, refinements)
finally
if refinements != null then
val rinfo1 = widenSkolemsMap(rinfo)
val rinfo1 = rinfo.widenSkolems
refinements(rname) = refinements.get(rname) match
case Some(tp) => tp & rinfo1
case None => rinfo1
Expand Down
7 changes: 6 additions & 1 deletion compiler/src/dotty/tools/dotc/core/TypeUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ class TypeUtils:
case ps => ps.reduceLeft(AndType(_, _))
}

def widenSkolems(using Context): Type =
val widenSkolemsMap = new TypeMap:
def apply(tp: Type) = mapOver(tp.widenSkolem)
widenSkolemsMap(self)

/** The element types of this tuple type, which can be made up of EmptyTuple, TupleX and `*:` pairs
*/
def tupleElementTypes(using Context): Option[List[Type]] =
Expand Down Expand Up @@ -134,7 +139,7 @@ class TypeUtils:
case t => throw TypeError(em"Malformed NamedTuple: names must be string types, but $t was found.")
val values = vals.tupleElementTypesUpTo(bound, normalize).getOrElse(Nil)
names.zip(values)

(if normalize then self.normalized else self).dealias match
// for desugaring and printer, ignore derived types to avoid infinite recursion in NamedTuple.unapply
case defn.NamedTupleDirect(nmes, vals) => extractNamesTypes(nmes, vals)
Expand Down
22 changes: 6 additions & 16 deletions compiler/src/dotty/tools/dotc/parsing/Parsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1331,16 +1331,6 @@ object Parsers {
*/
def qualId(): Tree = dotSelectors(termIdent())

/** Singleton ::= SimpleRef
* | SimpleLiteral
* | Singleton ‘.’ id
* -- not yet | Singleton ‘(’ Singletons ‘)’
* -- not yet | Singleton ‘[’ Types ‘]’
*/
def singleton(): Tree =
if isSimpleLiteral then simpleLiteral()
else dotSelectors(simpleRef())

/** SimpleLiteral ::= [‘-’] integerLiteral
* | [‘-’] floatingPointLiteral
* | booleanLiteral
Expand Down Expand Up @@ -2051,7 +2041,7 @@ object Parsers {
/** SimpleType ::= SimpleLiteral
* | ‘?’ TypeBounds
* | SimpleType1
* | SimpleType ‘(’ Singletons ‘)’ -- under language.experimental.dependent, checked in Typer
* | SimpleType ‘(’ Singletons ‘)’
* Singletons ::= Singleton {‘,’ Singleton}
*/
def simpleType(): Tree =
Expand Down Expand Up @@ -2083,11 +2073,11 @@ object Parsers {
val start = in.skipToken()
typeBounds().withSpan(Span(start, in.lastOffset, start))
else
def singletonArgs(t: Tree): Tree =
if in.token == LPAREN && in.featureEnabled(Feature.dependent)
then singletonArgs(AppliedTypeTree(t, inParensWithCommas(commaSeparated(singleton))))
else t
singletonArgs(simpleType1())
val tpt = simpleType1()
if in.featureEnabled(Feature.modularity) && in.token == LPAREN then
parArgumentExprss(wrapNew(tpt))
else
tpt

/** SimpleType1 ::= id
* | Singleton `.' id
Expand Down
2 changes: 2 additions & 0 deletions compiler/src/dotty/tools/dotc/reporting/ErrorMessageID.scala
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@ enum ErrorMessageID(val isActive: Boolean = true) extends java.lang.Enum[ErrorMe
case FormatInterpolationErrorID // errorNumber: 209
case ValueClassCannotExtendAliasOfAnyValID // errorNumber: 210
case MatchIsNotPartialFunctionID // errorNumber: 211
case OnlyFullyDependentAppliedConstructorTypeID // errorNumber: 212
case PointlessAppliedConstructorTypeID // errorNumber: 213

def errorNumber = ordinal - 1

Expand Down
21 changes: 21 additions & 0 deletions compiler/src/dotty/tools/dotc/reporting/messages.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3487,3 +3487,24 @@ class MatchIsNotPartialFunction(using Context) extends SyntaxMsg(MatchIsNotParti
|
|Efficient operations will use `applyOrElse` to avoid computing the match twice,
|but the `apply` body would be executed "per element" in the example."""

final class PointlessAppliedConstructorType(tpt: untpd.Tree, args: List[untpd.Tree], tpe: Type)(using Context) extends TypeMsg(PointlessAppliedConstructorTypeID):
override protected def msg(using Context): String =
val act = i"$tpt(${args.map(_.show).mkString(", ")})"
i"""|Applied constructor type $act has no effect.
|The resulting type of $act is the same as its base type, namely: $tpe""".stripMargin

override protected def explain(using Context): String =
i"""|Applied constructor types are used to ascribe specialized types of constructor applications.
|To benefit from this feature, the constructor in question has to have a more specific type than the class itself.
|
|If you want to track a precise type of any of the class parameters, make sure to mark the parameter as `tracked`.
|Otherwise, you can safely remove the argument list from the type.
|"""

final class OnlyFullyDependentAppliedConstructorType()(using Context)
extends TypeMsg(OnlyFullyDependentAppliedConstructorTypeID):
override protected def msg(using Context): String =
i"Applied constructor type can only be used with classes where all parameters in the first parameter list are tracked"

override protected def explain(using Context): String = ""
26 changes: 26 additions & 0 deletions compiler/src/dotty/tools/dotc/typer/Applications.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1282,6 +1282,10 @@ trait Applications extends Compatibility {
}
else {
val app = tree.fun match
case _ if ctx.mode.is(Mode.Type) && Feature.enabled(Feature.modularity) && !ctx.isAfterTyper =>
untpd.methPart(tree.fun) match
case Select(nw @ New(_), _) => typedAppliedConstructorType(nw, tree.args, tree)
case _ => realApply
case untpd.TypeApply(_: untpd.SplicePattern, _) if Feature.quotedPatternsWithPolymorphicFunctionsEnabled =>
typedAppliedSpliceWithTypes(tree, pt)
case _: untpd.SplicePattern => typedAppliedSplice(tree, pt)
Expand Down Expand Up @@ -1715,6 +1719,28 @@ trait Applications extends Compatibility {
def typedUnApply(tree: untpd.UnApply, selType: Type)(using Context): UnApply =
throw new UnsupportedOperationException("cannot type check an UnApply node")

/** Typecheck an applied constructor type – An Apply node in Type mode.
* This expands to the type this term would have if it were typed as an expression.
*
* e.g.
* ```scala
* // class C(tracked val v: Any)
* val c: C(42) = ???
* ```
*/
def typedAppliedConstructorType(nw: untpd.New, args: List[untpd.Tree], tree: untpd.Apply)(using Context) =
val tree1 = typedExpr(tree)
val preciseTp = tree1.tpe.widenSkolems
val classTp = typedType(nw.tpt).tpe
def classSymbolHasOnlyTrackedParameters =
!classTp.classSymbol.primaryConstructor.paramSymss.nestedExists: param =>
param.isTerm && !param.is(Tracked)
if !preciseTp.isError && !classSymbolHasOnlyTrackedParameters then
report.warning(OnlyFullyDependentAppliedConstructorType(), tree.srcPos)
if !preciseTp.isError && (preciseTp frozen_=:= classTp) then
report.warning(PointlessAppliedConstructorType(nw.tpt, args, classTp), tree.srcPos)
TypeTree(preciseTp)

/** Is given method reference applicable to argument trees `args`?
* @param resultType The expected result type of the application
*/
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/typer/ErrorReporting.scala
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ object ErrorReporting {

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

def err(using Context): Errors = new Errors
}
14 changes: 3 additions & 11 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2557,17 +2557,9 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
}

def typedAppliedTypeTree(tree: untpd.AppliedTypeTree)(using Context): Tree = {
tree.args match
case arg :: _ if arg.isTerm =>
if Feature.dependentEnabled then
return errorTree(tree, em"Not yet implemented: T(...)")
else
return errorTree(tree, dependentMsg)
case _ =>

val tpt1 = withoutMode(Mode.Pattern) {
val tpt1 = withoutMode(Mode.Pattern):
typed(tree.tpt, AnyTypeConstructorProto)
}

val tparams = tpt1.tpe.typeParams
if tpt1.tpe.isError then
val args1 = tree.args.mapconserve(typedType(_))
Expand Down Expand Up @@ -2691,7 +2683,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
typeIndexedLambdaTypeTree(tree, tparams, body)

def typedTermLambdaTypeTree(tree: untpd.TermLambdaTypeTree)(using Context): Tree =
if Feature.dependentEnabled then
if Feature.enabled(Feature.modularity) then
errorTree(tree, em"Not yet implemented: (...) =>> ...")
else
errorTree(tree, dependentMsg)
Expand Down
3 changes: 3 additions & 0 deletions compiler/test/dotc/neg-best-effort-unpickling.excludelist
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,6 @@ i18750.scala

# Crash on invalid prefix ([A] =>> Int)
i22357a.scala

# `110 (of class java.lang.Integer)`
context-function-syntax.scala
2 changes: 1 addition & 1 deletion docs/_docs/internals/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ AnnotType1 ::= SimpleType1 {Annotation}

SimpleType ::= SimpleLiteral SingletonTypeTree(l)
| ‘?’ TypeBounds
| SimpleType1
| SimpleType1 {ParArgumentExprs}
SimpleType1 ::= id Ident(name)
| Singleton ‘.’ id Select(t, name)
| Singleton ‘.’ ‘type’ SingletonTypeTree(p)
Expand Down
37 changes: 37 additions & 0 deletions docs/_docs/reference/experimental/modularity.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,43 @@ LocalModifier ::= ‘tracked’

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

## Applied constructor types

A new syntax is also introduced, 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`.

The arguments are used to typecheck the whole type, as if it was a normal
constructor application. For classes with `tracked` parameters this will mean
that the resulting type will have a refinement for each `tracked` parameter.

For example, given the following class definition:
```scala
class Person(tracked val name: String, tracked val age: Int)
```
**Type** `Person("Kasia", 27)` will be translated to `Person { val name: "Kasia"; val age: 27 }`.

## Allow Class Parents to be Refined Types

Expand Down
18 changes: 18 additions & 0 deletions tests/neg/applied_constructor_types.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
-- [E006] Not Found Error: tests/neg/applied_constructor_types.scala:8:10 ----------------------------------------------
8 | val v1: f(1) = f(1) // error
| ^
| Not found: type f
|
| longer explanation available when compiling with `-explain`
-- [E006] Not Found Error: tests/neg/applied_constructor_types.scala:9:10 ----------------------------------------------
9 | val v2: id(1) = f(1) // error
| ^^
| Not found: type id - did you mean is?
|
| longer explanation available when compiling with `-explain`
-- [E006] Not Found Error: tests/neg/applied_constructor_types.scala:10:10 ---------------------------------------------
10 | val v3: idDependent(1) = f(1) // error
| ^^^^^^^^^^^
| Not found: type idDependent
|
| longer explanation available when compiling with `-explain`
10 changes: 10 additions & 0 deletions tests/neg/applied_constructor_types.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import scala.language.experimental.modularity

def f(x: Int): Int = x
def id[T](x: T): T = x
def idDependent(x: Any): x.type = x

def test =
val v1: f(1) = f(1) // error
val v2: id(1) = f(1) // error
val v3: idDependent(1) = f(1) // error
2 changes: 1 addition & 1 deletion tests/neg/context-function-syntax.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
val test =
(using x: Int) => x // error // error
(using x: Int) => x // error // error // error

val f = () ?=> 23 // error
val g: ContextFunction0[Int] = ??? // ok
Expand Down
8 changes: 4 additions & 4 deletions tests/neg/deptypes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

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

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

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

val x: Vec[Int](10) = ??? // error: not yet implemented
val x: Vec[Int](10) = ???
val n = 10
type T = Vec[String](n) // error: not yet implemented
type T = Vec[String](n)
2 changes: 1 addition & 1 deletion tests/neg/i7751.scala
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import language.`3.3`
val a = Some(a=a,)=> // error // error // error // error
val a = Some(a=a,)=> // error // error // error
val a = Some(x=y,)=>
55 changes: 55 additions & 0 deletions tests/pos/applied_constructor_types.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import scala.language.experimental.modularity

class Box(tracked val v: Any)
class C(tracked val x: Int)
class NC(tracked val c: C)
class NNC(tracked val c: NC)
class F[A](tracked val a: Int)
class G[A](tracked val a: A)
class NF[A](tracked val f: F[A])

class Person(val name: String, tracked val age: Int)
class PersonPrime(val name: String)(tracked val age: Int)
class PersonBis(tracked val name: String)(val age: Int)

class Generic[A](val a: A)

object O:
val m: Int = 27

class InnerClass(tracked val x: Int)

object Test extends App {
val c: C(42) = C(42)
val nc: NC(C(42)) = NC(C(42))
val nc1: NC(c) = NC(c)
val nnc: NNC(NC(C(42))) = NNC(NC(C(42)))
val f: F[Int](42) = F[Int](42)
val f2: F[Int](42) = F(42)
val f3: F(42) = F(42)
val g: G(42) = G(42)

val n: Int = 27
val c2: C(n) = C(n)
val c3: C(O.m) = C(O.m)

val box: Box(O.InnerClass(42)) = Box(O.InnerClass(42))
val box2: Box(O.InnerClass(n)) = Box(O.InnerClass(n))
val box3: Box(O.InnerClass(O.m)) = Box(O.InnerClass(O.m))

val person: Person("Kasia", 27) = Person("Kasia", 27) // warn
val person1: Person("Kasia", n) = Person("Kasia", n) // warn
val person2: Person("Kasia", O.m) = Person("Kasia", O.m) // warn

val personPrime: PersonPrime("Kasia")(27) = PersonPrime("Kasia")(27) // warn
val personPrime1: PersonPrime("Kasia")(n) = PersonPrime("Kasia")(n) // warn
val personPrime2: PersonPrime("Kasia")(O.m) = PersonPrime("Kasia")(O.m) // warn

val personBis: PersonBis("Kasia")(27) = PersonBis("Kasia")(27) // warn
val personBis1: PersonBis("Kasia")(n) = PersonBis("Kasia")(n) // warn
val personBis2: PersonBis("Kasia")(O.m) = PersonBis("Kasia")(O.m) // warn

val generic1: Generic(compiletime.erasedValue[Int]) = Generic(42) // warn
val generic2: Generic(??? : Int) = Generic(42) // warn
val generic3: Generic(43) = Generic(42) // warn
}
4 changes: 4 additions & 0 deletions tests/warn/applied_constructor_types.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-- [E212] Type Warning: tests/warn/applied_constructor_types.scala:6:10 ------------------------------------------------
6 | val v1: UnspecificBox(4) = UnspecificBox(4) // warn
| ^^^^^^^^^^^^^^^^
|Applied constructor type can only be used with classes where all parameters in the first parameter list are tracked
6 changes: 6 additions & 0 deletions tests/warn/applied_constructor_types.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import scala.language.experimental.modularity

class UnspecificBox(val v: Any)

def test =
val v1: UnspecificBox(4) = UnspecificBox(4) // warn