Skip to content

Commit

Permalink
Disallow some opaque type aliases in match type patterns
Browse files Browse the repository at this point in the history
Disallow opaque type aliases in match type patterns in scopes where the alias is visible.
  • Loading branch information
odersky committed Feb 16, 2025
1 parent 43f8cdb commit 0001a75
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 17 deletions.
14 changes: 13 additions & 1 deletion compiler/src/dotty/tools/dotc/core/Types.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5211,12 +5211,14 @@ object Types extends TypeUtils {

enum MatchTypeCaseError:
case Alias(sym: Symbol)
case OpaqueAlias(sym: Symbol)
case RefiningBounds(name: TypeName)
case StructuralType(name: TypeName)
case UnaccountedTypeParam(name: TypeName)

def explanation(using Context) = this match
case Alias(sym) => i"a type alias `${sym.name}`"
case OpaqueAlias(sym) => i"an opaque type alias `${sym.name}` in the scope where its alias is known"
case RefiningBounds(name) => i"an abstract type member `$name` with bounds that need verification"
case StructuralType(name) => i"an abstract type member `$name` that does not refine a member in its parent"
case UnaccountedTypeParam(name) => i"an unaccounted type parameter `$name`"
Expand Down Expand Up @@ -5321,7 +5323,17 @@ object Types extends TypeUtils {
else
tycon.info match
case _: RealTypeBounds =>
recAbstractTypeConstructor(pat)
val tsym = tycon.typeSymbol
if tsym.isOpaqueAlias
&& ctx.owner.isContainedIn(tsym.owner) // (1) opaque alias is known here, and
&& !ctx.owner.isAbstractOrAliasType // (2) we are not in an embedded type
// without (2), one cannot define a match type referring to an opaque type in the
// scope of that opaque type. But that should be OK, we should simply not be
// able to normalize such types in terms in the same scope.
then
MatchTypeCaseError.OpaqueAlias(tsym)
else
recAbstractTypeConstructor(pat)
case TypeAlias(tl @ HKTypeLambda(onlyParam :: Nil, resType: RefinedType)) =>
/* Unlike for eta-expanded classes, the typer does not automatically
* dealias poly type aliases to refined types. So we have to give them
Expand Down
7 changes: 7 additions & 0 deletions tests/neg/named-tuples-strawman-2.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- [E191] Type Error: tests/neg/named-tuples-strawman-2.scala:28:24 ----------------------------------------------------
28 | inline def toTuple: DropNames[NT] = x.asInstanceOf // error
| ^^^^^^^^^^^^^
| The match type contains an illegal case:
| case NamedTupleOps.NamedTuple[_, x] => x
| The pattern contains an opaque type alias `NamedTuple` in the scope where its alias is known.
| (this error can be ignored for now with `-source:3.3`)
29 changes: 29 additions & 0 deletions tests/neg/named-tuples-strawman-2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import compiletime.*
import compiletime.ops.int.*
import compiletime.ops.boolean.!

object NamedTupleDecomposition:
import NamedTupleOps.*

/** The names of the named tuple type `NT` */
type Names[NT <: AnyNamedTuple] <: Tuple = NT match
case NamedTuple[n, _] => n

/** The value types of the named tuple type `NT` */
type DropNames[NT <: AnyNamedTuple] <: Tuple = NT match
case NamedTuple[_, x] => x

object NamedTupleOps:

opaque type AnyNamedTuple = Any

opaque type NamedTuple[N <: Tuple, +X <: Tuple] >: X <: AnyNamedTuple = X

export NamedTupleDecomposition.*

object NamedTuple:
def apply[N <: Tuple, X <: Tuple](x: X): NamedTuple[N, X] = x

extension [NT <: AnyNamedTuple](x: NT)
inline def toTuple: DropNames[NT] = x.asInstanceOf // error
inline def names: Names[NT] = constValueTuple[Names[NT]]
15 changes: 15 additions & 0 deletions tests/neg/opaque-matches.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
-- [E191] Type Error: tests/neg/opaque-matches.scala:4:18 --------------------------------------------------------------
4 | type Fst[X] = X match // error
| ^
| The match type contains an illegal case:
| case NT.NT[a, b] => a
| The pattern contains a type alias `NT`.
| (this error can be ignored for now with `-source:3.3`)
5 | case NT[a, b] => a
-- [E191] Type Error: tests/neg/opaque-matches.scala:10:30 -------------------------------------------------------------
10 | inline def snd[NT](nt: NT): NTDecomposition.Snd[NT] = // error
| ^^^^^^^^^^^^^^^^^^^^^^^
| The match type contains an illegal case:
| case NT.NT[a, b] => b
| The pattern contains an opaque type alias `NT` in the scope where its alias is known.
| (this error can be ignored for now with `-source:3.3`)
19 changes: 19 additions & 0 deletions tests/neg/opaque-matches.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
object NT:
opaque type NT[A, B] = B

type Fst[X] = X match // error
case NT[a, b] => a

inline def fst[NT](nt: NT): Fst[NT] =
???

inline def snd[NT](nt: NT): NTDecomposition.Snd[NT] = // error
???

object NTDecomposition:

type Snd[X] = X match
case NT.NT[a, b] => b



41 changes: 25 additions & 16 deletions tests/run/named-tuples-strawman-2.scala
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,11 @@ object TupleOps:
for i <- toInclude.indices do
arr(i) = xs.productElement(toInclude(i).asInstanceOf[Int]).asInstanceOf[Object]
Tuple.fromArray(arr).asInstanceOf[Filter[X, P]]
end TupleOps

object NamedTupleDecomposition:
import NamedTupleOps.*
import TupleOps.ConcatDistinct

/** The names of the named tuple type `NT` */
type Names[NT <: AnyNamedTuple] <: Tuple = NT match
Expand All @@ -131,21 +133,38 @@ object NamedTupleDecomposition:
type DropNames[NT <: AnyNamedTuple] <: Tuple = NT match
case NamedTuple[_, x] => x

extension [N1 <: Tuple, V1 <: Tuple](nt1: NamedTuple[N1, V1])
inline def updateWith[N2 <: Tuple, V2 <: Tuple](nt2: NamedTuple[N2, V2])
: UpdateWith[NamedTuple[N1, V1], NamedTuple[N2, V2]] =
val names = constValueTuple[ConcatDistinct[N1, N2]].toArray
val names2 = constValueTuple[N2].toArray
val values1 = NamedTupleOps.toTuple(nt1)
val values2 = nt2.toTuple
val values = new Array[Object](names.length)
values1.toArray.copyToArray(values)
for i <- 0 until values2.size do
val idx = names.indexOf(names2(i))
values(idx) = values2.productElement(i).asInstanceOf[Object]
Tuple.fromArray(values).asInstanceOf[UpdateWith[NamedTuple[N1, V1], NamedTuple[N2, V2]]]

end NamedTupleDecomposition

object NamedTupleOps:
import TupleOps.*

opaque type AnyNamedTuple = Any

opaque type NamedTuple[N <: Tuple, +X <: Tuple] >: X <: AnyNamedTuple = X

export NamedTupleDecomposition.*
export NamedTupleDecomposition.{Names, DropNames}

object NamedTuple:
def apply[N <: Tuple, X <: Tuple](x: X): NamedTuple[N, X] = x

extension [NT <: AnyNamedTuple](x: NT)
inline def toTuple: DropNames[NT] = x.asInstanceOf
inline def names: Names[NT] = constValueTuple[Names[NT]]
extension [N <: Tuple, V <: Tuple](x: NamedTuple[N, V])
inline def toTuple: V = x.asInstanceOf

inline def names: N = constValueTuple[N]

/** Internal use only: Merge names and value components of two named tuple to
* impement `UpdateWith`.
Expand All @@ -169,18 +188,8 @@ object NamedTupleOps:
type UpdateWith[NT1 <: AnyNamedTuple, NT2 <: AnyNamedTuple] =
Merge[ConcatDistinct[Names[NT1], Names[NT2]], DropNames[NT1], Names[NT2], DropNames[NT2]]

extension [NT1 <: AnyNamedTuple](nt1: NT1)
inline def updateWith[NT2 <: AnyNamedTuple](nt2: NT2): UpdateWith[NT1, NT2] =
val names = constValueTuple[ConcatDistinct[Names[NT1], Names[NT2]]].toArray
val names2 = constValueTuple[Names[NT2]].toArray
val values1 = nt1.toTuple
val values2 = nt2.toTuple
val values = new Array[Object](names.length)
values1.toArray.copyToArray(values)
for i <- 0 until values2.size do
val idx = names.indexOf(names2(i))
values(idx) = values2.productElement(i).asInstanceOf[Object]
Tuple.fromArray(values).asInstanceOf[UpdateWith[NT1, NT2]]
export NamedTupleDecomposition.updateWith
end NamedTupleOps

@main def Test =
import TupleOps.*
Expand Down

0 comments on commit 0001a75

Please sign in to comment.