Skip to content
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

Implement applied constructor types #22543

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
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
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 @@ -1320,16 +1320,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 @@ -2018,7 +2008,7 @@ object Parsers {
/** SimpleType ::= SimpleLiteral
* | ‘?’ TypeBounds
* | SimpleType1
* | SimpleType ‘(’ Singletons ‘)’ -- under language.experimental.dependent, checked in Typer
* | SimpleType ‘(’ Singletons ‘)’ -- under language.experimental.modularity, checked in Typer
* Singletons ::= Singleton {‘,’ Singleton}
*/
def simpleType(): Tree =
Expand Down Expand Up @@ -2050,11 +2040,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 @@ -221,6 +221,8 @@ enum ErrorMessageID(val isActive: Boolean = true) extends java.lang.Enum[ErrorMe
case GivenSearchPriorityID // errorNumber: 205
case EnumMayNotBeValueClassesID // errorNumber: 206
case IllegalUnrollPlacementID // errorNumber: 207
case PointlessAppliedConstructorTypeID // errorNumber: 208
case OnlyFullyDependentAppliedConstructorTypeID // errorNumber: 209

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 @@ -3433,3 +3433,24 @@ extends DeclarationMsg(IllegalUnrollPlacementID):

def explain(using Context) = ""
end IllegalUnrollPlacement

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 that only have tracked parameters"

override protected def explain(using Context): String = ""
17 changes: 17 additions & 0 deletions compiler/src/dotty/tools/dotc/typer/Applications.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1694,6 +1694,23 @@ trait Applications extends Compatibility {
def typedUnApply(tree: untpd.UnApply, selType: Type)(using Context): UnApply =
throw new UnsupportedOperationException("cannot type check an UnApply node")

def typedAppliedConstructorType(tree: untpd.Apply)(using Context) = untpd.methPart(tree) match
case Select(New(tpt), _) =>
val tree1 = typedExpr(tree)
val widenSkolemsMap = new TypeMap:
def apply(tp: Type) = mapOver(tp.widenSkolem)
val preciseTp = widenSkolemsMap(tree1.tpe)
val classTp = typedType(tpt).tpe
def classSymbolHasOnlyTrackedParameters =
classTp.classSymbol.primaryConstructor.paramSymss.flatten.filter(_.isTerm).forall(_.is(Tracked))
if !preciseTp.isError && !classSymbolHasOnlyTrackedParameters then
report.warning(OnlyFullyDependentAppliedConstructorType(), tree.srcPos)
if !preciseTp.isError && (preciseTp frozen_=:= classTp) then
report.warning(PointlessAppliedConstructorType(tpt, tree.args, classTp), tree.srcPos)
TypeTree(preciseTp)
case _ =>
throw TypeError(em"Unexpected applied constructor type: $tree")

/** 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
}
20 changes: 7 additions & 13 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2515,17 +2515,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 @@ -2649,7 +2641,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 Expand Up @@ -3485,7 +3477,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer

/** Typecheck tree without adapting it, returning a typed tree.
* @param initTree the untyped tree
* @param pt the expected result type
* @param pt the expected result typ
* @param locked the set of type variables of the current typer state that cannot be interpolated
* at the present time
*/
Expand Down Expand Up @@ -3525,7 +3517,9 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer

def typedUnnamed(tree: untpd.Tree): Tree = tree match {
case tree: untpd.Apply =>
if (ctx.mode is Mode.Pattern) typedUnApply(tree, pt) else typedApply(tree, pt)
if (ctx.mode is Mode.Pattern) typedUnApply(tree, pt)
else if (Feature.enabled(modularity) && ctx.mode.is(Mode.Type) && !ctx.isAfterTyper) typedAppliedConstructorType(tree)
else typedApply(tree, pt)
case tree: untpd.This => typedThis(tree)
case tree: untpd.Number => typedNumber(tree, pt)
case tree: untpd.Literal => typedLiteral(tree)
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
27 changes: 27 additions & 0 deletions docs/_docs/reference/experimental/modularity.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,33 @@ 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`.

## 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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about constructors with mixed tracked and non-tracked parameters?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some tests for those, but the current approach is: you have to write all parameters, but the non-tracked params won't influence the resulting type.

Copy link
Member

@bishabosha bishabosha Feb 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that sounds strange, so a bit like an unused function parameter? maybe there should be a placeholder like erasedValue or ? for now?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't have to be unused, though one might argue that it makes it worse 😅 BUT, the following works:

class Generic[A](val a: A)
val _: Generic(compiletime.erasedValue[Int]) = Generic(42)
val _: Generic(??? : Int) = Generic(42)
val _: Generic(43) = Generic(42)

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 @@
-- [E209] 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 that only have tracked parameters
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
Loading