From 21969a24a64af9cc6a5fba1a49a39d994be2b91e Mon Sep 17 00:00:00 2001 From: Hamza Remmal Date: Sun, 24 Aug 2025 13:09:31 +0200 Subject: [PATCH 1/4] chore: refactor `selectorOrMatch` --- compiler/src/dotty/tools/dotc/parsing/Parsers.scala | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index c4a77f17060c..3936015075c9 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -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()) } From 3dad81681abe011dae1c5f594d308e5968c17da6 Mon Sep 17 00:00:00 2001 From: Hamza Remmal Date: Sun, 24 Aug 2025 14:00:09 +0200 Subject: [PATCH 2/4] chore: add support for *unqualified selectors* --- compiler/src/dotty/tools/dotc/parsing/Parsers.scala | 5 +++++ compiler/src/dotty/tools/dotc/parsing/Tokens.scala | 2 +- .../src/dotty/tools/dotc/typer/Applications.scala | 13 +++++++++---- compiler/src/dotty/tools/dotc/typer/Typer.scala | 11 ++++++++--- tests/pos/unqualified-selector-enum.scala | 12 ++++++++++++ tests/pos/unqualified-selector-object.scala | 8 ++++++++ 6 files changed, 43 insertions(+), 8 deletions(-) create mode 100644 tests/pos/unqualified-selector-enum.scala create mode 100644 tests/pos/unqualified-selector-object.scala diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 3936015075c9..57098397bc1e 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -2368,6 +2368,7 @@ object Parsers { * | `try' Expr [`finally' Expr] * | `throw' Expr * | `return' [Expr] + * |  `.` id * | ForExpr * | [SimpleExpr `.'] id `=' Expr * | PrefixOperator SimpleExpr `=' Expr @@ -2756,6 +2757,7 @@ object Parsers { * SimpleExpr1 ::= literal * | xmlLiteral * | SimpleRef + * |  `.` id * | `(` [ExprsInParens] `)` * | SimpleExpr `.` id * | SimpleExpr `.` MatchClause @@ -2800,6 +2802,9 @@ object Parsers { case MACRO => val start = in.skipToken() MacroTree(simpleExpr(Location.ElseWhere)) + case DOT => + accept(DOT) + selector(EmptyTree) case _ => if isLiteral then literal() diff --git a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala index d47e6dab005f..e9eed07697d4 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala @@ -227,7 +227,7 @@ object Tokens extends TokensCommon { | BitSet(QUOTE, NEW) final val canStartExprTokens3: TokenSet = - canStartInfixExprTokens | BitSet(INDENT, IF, WHILE, FOR, TRY, THROW) + canStartInfixExprTokens | BitSet(INDENT, IF, WHILE, FOR, TRY, THROW, DOT) final val canStartExprTokens2: TokenSet = canStartExprTokens3 | BitSet(DO) diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 290e061772e4..28f0f30c80a1 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -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. diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index def6fac0556e..dcfcbe4ddf5b 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -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. @@ -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 diff --git a/tests/pos/unqualified-selector-enum.scala b/tests/pos/unqualified-selector-enum.scala new file mode 100644 index 000000000000..6805c9dc9d4f --- /dev/null +++ b/tests/pos/unqualified-selector-enum.scala @@ -0,0 +1,12 @@ +enum Opt[+T]: + case none extends Opt[Nothing] + case some[T](value: T) extends Opt[T] + +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) diff --git a/tests/pos/unqualified-selector-object.scala b/tests/pos/unqualified-selector-object.scala new file mode 100644 index 000000000000..bc29f3179533 --- /dev/null +++ b/tests/pos/unqualified-selector-object.scala @@ -0,0 +1,8 @@ +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 From ae05ac148776aa5142ddb59cc3f660c2a4d21a32 Mon Sep 17 00:00:00 2001 From: Hamza Remmal Date: Sun, 24 Aug 2025 17:47:04 +0200 Subject: [PATCH 3/4] chore: add support for *unqualified selectors* in patterns --- compiler/src/dotty/tools/dotc/parsing/Parsers.scala | 5 ++++- tests/pos/unqualified-selector-enum.scala | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 57098397bc1e..304d40f7f7b8 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -3291,7 +3291,7 @@ object Parsers { * | SimplePattern1 [TypeArgs] [ArgumentPatterns] * | ‘given’ RefinedType * SimplePattern1 ::= SimpleRef - * | SimplePattern1 `.' id + * | [SimplePattern1] `.' id * PatVar ::= id * | `_' */ @@ -3308,6 +3308,9 @@ object Parsers { simpleExpr(Location.InPattern) case XMLSTART => xmlLiteralPattern() + case DOT => + accept(DOT) + simplePatternRest(selector(EmptyTree)) case GIVEN => atSpan(in.offset) { val givenMod = atSpan(in.skipToken())(Mod.Given()) diff --git a/tests/pos/unqualified-selector-enum.scala b/tests/pos/unqualified-selector-enum.scala index 6805c9dc9d4f..755b3a0a7e4b 100644 --- a/tests/pos/unqualified-selector-enum.scala +++ b/tests/pos/unqualified-selector-enum.scala @@ -2,6 +2,11 @@ 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 From 4f478a7c4e91267ff3cd09302cc0005b408d58ba Mon Sep 17 00:00:00 2001 From: Hamza Remmal Date: Sun, 24 Aug 2025 22:12:22 +0200 Subject: [PATCH 4/4] chore: add `scala.language.experimental.unqualifiedSelectors` --- compiler/src/dotty/tools/dotc/config/Feature.scala | 2 ++ compiler/src/dotty/tools/dotc/parsing/Parsers.scala | 4 ++-- compiler/src/dotty/tools/dotc/parsing/Scanners.scala | 6 +++++- compiler/src/dotty/tools/dotc/parsing/Tokens.scala | 2 +- library/src/scala/language.scala | 5 +++++ library/src/scala/runtime/stdLibPatches/language.scala | 5 +++++ tests/pos/unqualified-selector-enum.scala | 2 ++ tests/pos/unqualified-selector-object.scala | 2 ++ 8 files changed, 24 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/config/Feature.scala b/compiler/src/dotty/tools/dotc/config/Feature.scala index 70a77c9560b2..7c949a7d38a6 100644 --- a/compiler/src/dotty/tools/dotc/config/Feature.scala +++ b/compiler/src/dotty/tools/dotc/config/Feature.scala @@ -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 @@ -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. diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 304d40f7f7b8..c55006308185 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -2802,7 +2802,7 @@ object Parsers { case MACRO => val start = in.skipToken() MacroTree(simpleExpr(Location.ElseWhere)) - case DOT => + case DOT if in.featureEnabled(Feature.unqualifiedSelectors) => accept(DOT) selector(EmptyTree) case _ => @@ -3308,7 +3308,7 @@ object Parsers { simpleExpr(Location.InPattern) case XMLSTART => xmlLiteralPattern() - case DOT => + case DOT if in.featureEnabled(Feature.unqualifiedSelectors) => accept(DOT) simplePatternRest(selector(EmptyTree)) case GIVEN => diff --git a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala index 52e03de60dea..3634ff48d339 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Scanners.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Scanners.scala @@ -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} @@ -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 ----------------------------------------------------------------- diff --git a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala index e9eed07697d4..d47e6dab005f 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Tokens.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Tokens.scala @@ -227,7 +227,7 @@ object Tokens extends TokensCommon { | BitSet(QUOTE, NEW) final val canStartExprTokens3: TokenSet = - canStartInfixExprTokens | BitSet(INDENT, IF, WHILE, FOR, TRY, THROW, DOT) + canStartInfixExprTokens | BitSet(INDENT, IF, WHILE, FOR, TRY, THROW) final val canStartExprTokens2: TokenSet = canStartExprTokens3 | BitSet(DO) diff --git a/library/src/scala/language.scala b/library/src/scala/language.scala index bacbb09ad615..4a6363fe1283 100644 --- a/library/src/scala/language.scala +++ b/library/src/scala/language.scala @@ -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. diff --git a/library/src/scala/runtime/stdLibPatches/language.scala b/library/src/scala/runtime/stdLibPatches/language.scala index 9d38ea4371ff..cae571657da5 100644 --- a/library/src/scala/runtime/stdLibPatches/language.scala +++ b/library/src/scala/runtime/stdLibPatches/language.scala @@ -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. diff --git a/tests/pos/unqualified-selector-enum.scala b/tests/pos/unqualified-selector-enum.scala index 755b3a0a7e4b..87c98f97928d 100644 --- a/tests/pos/unqualified-selector-enum.scala +++ b/tests/pos/unqualified-selector-enum.scala @@ -1,3 +1,5 @@ +import scala.language.experimental.unqualifiedSelectors + enum Opt[+T]: case none extends Opt[Nothing] case some[T](value: T) extends Opt[T] diff --git a/tests/pos/unqualified-selector-object.scala b/tests/pos/unqualified-selector-object.scala index bc29f3179533..e6a959876b4c 100644 --- a/tests/pos/unqualified-selector-object.scala +++ b/tests/pos/unqualified-selector-object.scala @@ -1,3 +1,5 @@ +import scala.language.experimental.unqualifiedSelectors + class Opt[+T](v: T) object Opt: