Skip to content
Draft
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
2 changes: 2 additions & 0 deletions compiler/src/dotty/tools/dotc/config/Feature.scala
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ object Feature:
val quotedPatternsWithPolymorphicFunctions = experimental("quotedPatternsWithPolymorphicFunctions")
val packageObjectValues = experimental("packageObjectValues")
val subCases = experimental("subCases")
val unqualifiedSelectors = experimental("unqualifiedSelectors")

def experimentalAutoEnableFeatures(using Context): List[TermName] =
defn.languageExperimentalFeatures
Expand Down Expand Up @@ -66,6 +67,7 @@ object Feature:
(into, "Allow into modifier on parameter types"),
(modularity, "Enable experimental modularity features"),
(packageObjectValues, "Enable experimental package objects as values"),
(unqualifiedSelectors, "Enable unqualified selectors for expressions and patterns")
)

// legacy language features from Scala 2 that are no longer supported.
Expand Down
14 changes: 10 additions & 4 deletions compiler/src/dotty/tools/dotc/parsing/Parsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1269,9 +1269,7 @@ object Parsers {

/** Accept identifier or match clause acting as a selector on given tree `t` */
def selectorOrMatch(t: Tree): Tree =
atSpan(startOffset(t), in.offset) {
if in.token == MATCH then matchClause(t) else Select(t, ident())
}
if in.token == MATCH then matchClause(t) else selector(t)

def selector(t: Tree): Tree =
atSpan(startOffset(t), in.offset) { Select(t, ident()) }
Expand Down Expand Up @@ -2370,6 +2368,7 @@ object Parsers {
* | `try' Expr [`finally' Expr]
* | `throw' Expr
* | `return' [Expr]
* |  `.` id
* | ForExpr
* | [SimpleExpr `.'] id `=' Expr
* | PrefixOperator SimpleExpr `=' Expr
Expand Down Expand Up @@ -2758,6 +2757,7 @@ object Parsers {
* SimpleExpr1 ::= literal
* | xmlLiteral
* | SimpleRef
* |  `.` id
* | `(` [ExprsInParens] `)`
* | SimpleExpr `.` id
* | SimpleExpr `.` MatchClause
Expand Down Expand Up @@ -2802,6 +2802,9 @@ object Parsers {
case MACRO =>
val start = in.skipToken()
MacroTree(simpleExpr(Location.ElseWhere))
case DOT if in.featureEnabled(Feature.unqualifiedSelectors) =>
accept(DOT)
selector(EmptyTree)
case _ =>
if isLiteral then
literal()
Expand Down Expand Up @@ -3288,7 +3291,7 @@ object Parsers {
* | SimplePattern1 [TypeArgs] [ArgumentPatterns]
* | ‘given’ RefinedType
* SimplePattern1 ::= SimpleRef
* | SimplePattern1 `.' id
* | [SimplePattern1] `.' id
* PatVar ::= id
* | `_'
*/
Expand All @@ -3305,6 +3308,9 @@ object Parsers {
simpleExpr(Location.InPattern)
case XMLSTART =>
xmlLiteralPattern()
case DOT if in.featureEnabled(Feature.unqualifiedSelectors) =>
accept(DOT)
simplePatternRest(selector(EmptyTree))
case GIVEN =>
atSpan(in.offset) {
val givenMod = atSpan(in.skipToken())(Mod.Given())
Expand Down
6 changes: 5 additions & 1 deletion compiler/src/dotty/tools/dotc/parsing/Scanners.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import Tokens.*
import scala.annotation.{switch, tailrec}
import scala.collection.mutable
import scala.collection.immutable.SortedMap
import scala.collection.immutable.BitSet
import rewrites.Rewrites.patch
import config.Feature
import config.Feature.{migrateTo3, sourceVersion}
Expand Down Expand Up @@ -1241,7 +1242,10 @@ object Scanners {
if migrateTo3 then canStartStatTokens2 else canStartStatTokens3

def canStartExprTokens =
if migrateTo3 then canStartExprTokens2 else canStartExprTokens3
if migrateTo3 then
canStartExprTokens2
else
canStartExprTokens3 | (if featureEnabled(Feature.unqualifiedSelectors) then BitSet(DOT) else BitSet.empty)

// Literals -----------------------------------------------------------------

Expand Down
13 changes: 9 additions & 4 deletions compiler/src/dotty/tools/dotc/typer/Applications.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1096,10 +1096,15 @@ trait Applications extends Compatibility {
if tpt.isType && typedAheadType(tpt).tpe.typeSymbol.typeParams.isEmpty then
IgnoredProto(pt)
else
pt // Don't ignore expected value types of `new` expressions with parameterized type.
// If we have a `new C()` with expected type `C[T]` we want to use the type to
// instantiate `C` immediately. This is necessary since `C` might _also_ have using
// clauses that we want to instantiate with the best available type. See i15664.scala.
// Don't ignore expected value types of `new` expressions with parameterized type.
// If we have a `new C()` with expected type `C[T]` we want to use the type to
// instantiate `C` immediately. This is necessary since `C` might _also_ have using
// clauses that we want to instantiate with the best available type. See i15664.scala.
pt
// When typing the selector of an `Apply` node, and if the selector is `unqualifed` (e.g. `.some(10)`)
// We preserve the prototype `pt` and keep it in the prototype passed to type the selector.
// Unqualified selectors relies on the expected type to resolve the qualifier of the selection.
case Select(EmptyTree, name) => pt
case _ => IgnoredProto(pt)
// Do ignore other expected result types, since there might be an implicit conversion
// on the result. We could drop this if we disallow unrestricted implicit conversions.
Expand Down
11 changes: 8 additions & 3 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1037,7 +1037,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
def typedSelect(tree: untpd.Select, pt: Type)(using Context): Tree = {
record("typedSelect")

def typeSelectOnTerm(using Context): Tree =
def typeSelectOnTerm(tree: untpd.Select)(using Context): Tree =
if ctx.isJava then
// permitted selection depends on Java context (type or expression).
// we don't propagate (as a mode) whether a.b.m is a type name; OK since we only see type contexts.
Expand Down Expand Up @@ -1111,9 +1111,14 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
else if (ctx.isJava && tree.name.isTypeName)
// scala/bug#3120 Java uses the same syntax, A.B, to express selection from the
// value A and from the type A. We have to try both. (possibly exponential bc of qualifier retyping)
tryAlternatively(typeSelectOnTerm)(tryJavaSelectOnType)
tryAlternatively(typeSelectOnTerm(tree))(tryJavaSelectOnType)
else if tree.qualifier.isEmpty then
// TODO: Specify and develop the logic to resolve the qualifier here
val companion = pt.resultType.typeSymbol.companionModule
val qualified = cpy.Select(tree)(qualifier = untpd.ref(companion), name = tree.name)
typeSelectOnTerm(qualified)
else
typeSelectOnTerm
typeSelectOnTerm(tree)

warnUnnecessaryNN(tree1)
tree1
Expand Down
5 changes: 5 additions & 0 deletions library/src/scala/language.scala
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,11 @@ object language {
@compileTimeOnly("`subCases` can only be used at compile time in import statements")
object subCases

/** Experimental support for unqualified selectors
*/
@compileTimeOnly("`unqualifiedSelectors` can only be used at compile time in import statements")
object unqualifiedSelectors

}

/** The deprecated object contains features that are no longer officially suypported in Scala.
Expand Down
5 changes: 5 additions & 0 deletions library/src/scala/runtime/stdLibPatches/language.scala
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,11 @@ object language:
*/
@compileTimeOnly("`subCases` can only be used at compile time in import statements")
object subCases

/** Experimental support for unqualified selectors
*/
@compileTimeOnly("`unqualifiedSelectors` can only be used at compile time in import statements")
object unqualifiedSelectors
end experimental

/** The deprecated object contains features that are no longer officially suypported in Scala.
Expand Down
19 changes: 19 additions & 0 deletions tests/pos/unqualified-selector-enum.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import scala.language.experimental.unqualifiedSelectors

enum Opt[+T]:
case none extends Opt[Nothing]
case some[T](value: T) extends Opt[T]

def map[U](f: T => U): Opt[U] = this match
case .none => .none
case Opt.some(v) => .some(f(v)) // TODO: This should be `case .some(v) => .some(f(v))`
end map

object Proxy:
def opt(o: Opt[Int]): Opt[Int] = o

val _: Opt[Int] = .some(10)
val _: Opt[Any] = .none

val _ = Proxy.opt(.some(10))
val _ = Proxy.opt(.none)
10 changes: 10 additions & 0 deletions tests/pos/unqualified-selector-object.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import scala.language.experimental.unqualifiedSelectors

class Opt[+T](v: T)

object Opt:
def some[T](b: T): Opt[T] = Opt(b)
val none: Opt[Nothing] = ???

val _: Opt[Int] = .some(10)
val _: Opt[Int] = .none
Loading