Skip to content

Union Type Gets Widen When Inside Tuple #23387

@WhiteZh

Description

@WhiteZh

Compiler version

Scala 3.7.1
The issue also exists on 3.3.5 and 3.6.4, which very likely implies that the issue exists on every version.
Notably, compiler with version less than 3.7 encounter additional bugs related on union type on top of the current bug.

JVM version is not related to the problem. Nor is the running environment.

Affected Component

The compiler itself.

Minimized code

enum E:
    case A
    case B
    case C

type NotC = E.A.type | E.B.type

def doSomething(): (Int, NotC) = (1, E.A)

def bee(nu: NotC): Unit =
    println("bee")

@main def main(): Unit =
    val nu = doSomething()
    val (x, nnu) = nu
    bee(nnu)

Output

Compiling project (Scala 3.7.1, JVM (24))
[error] ./main.scala:16:9
[error] Found:    (nnu : E)
[error] Required: NotC
[error]     bee(nnu)
[error]         ^^^
Error compiling project (Scala 3.7.1, JVM (24))
Compilation failed

Expectation

bee

Additioinal Note

Please see here: https://www.reddit.com/r/scala/comments/1l5cpg4/weird_behavior_of_union_type_widening_on_method/
for a full discussion regarding the issue.
I kept the bug report as minimal as possible as instructed, but the context might be helpful.

Activity

som-snytt

som-snytt commented on Jun 17, 2025

@som-snytt
Contributor

The trip through pattern matching:

        val $1$: (Int, E) =
          nu:(Int, NotC) @unchecked match
            {
              case Tuple2.unapply[Int, E](x @ _, nnu @ _) =>
                Tuple2.apply[Int, E](x, nnu)
            }
        val x: Int = $1$._1
        val nnu: E = $1$._2

but I wonder if #23373 will address it.

(I would try it now, but WSL just crashed and corrupted my repo.)

WhiteZh

WhiteZh commented on Jun 17, 2025

@WhiteZh
Author

Hi, I wonder how did you obtain the "trip though pattern matching"?

Additionally, after I've read the "trip through pattern matching", I think it might be worth mentioning that if I annotate the type of nnu like this (changed line 15):

enum E:
    case A
    case B
    case C

type NotC = E.A.type | E.B.type

def doSomething(): (Int, NotC) = (1, E.A)

def bee(nu: NotC): Unit =
    println("bee")

@main def main(): Unit =
    val nu = doSomething()
    val (x, nnu: NotC) = nu
    bee(nnu)

the code will compile but with a warning of unchecked type narrowing:

[warn] .\main.scala:15:26
[warn] pattern's type NotC is more specialized than the right hand side expression's type E
[warn]
[warn] If the narrowing is intentional, this can be communicated by adding `: @unchecked` after the expression,
[warn] which may result in a MatchError at runtime.
[warn] This patch can be rewritten automatically under -rewrite -source 3.2-migration.
[warn]     val (x, nnu: NotC) = nu
[warn]

Other type annotation such as annotating nu or (x, nnu) as a whole would still result in compilation errors.

noti0na1

noti0na1 commented on Jun 17, 2025

@noti0na1
Member

but I wonder if #23373 will address it.

It compiles fine with my PR:

        val nu: (Int, NotC) = doSomething()
        val $2$: (Int, NotC) =
          nu:(Int, NotC) @unchecked match 
            {
              case $1$ @ Tuple2.unapply[Int, E](_, _) => $1$:(Int, NotC)
            }
        val x: Int = $2$._1
        val nnu: NotC = $2$._2
        bee(nnu)

But I guess we should still try to fix the widening?

noti0na1

noti0na1 commented on Jun 17, 2025

@noti0na1
Member

I would suggest we update the code to following to illustrate the widening issue more clear:

@main def main(): Unit =
  doSomething() match
    case (_, nnu) => bee(nnu) // Found:    (nnu : E)
WhiteZh

WhiteZh commented on Jun 17, 2025

@WhiteZh
Author

Agree. That was similiar to how I wrote it initially. I later expanded it as I thought it would make each step clearer.

WhiteZh

WhiteZh commented on Jun 17, 2025

@WhiteZh
Author

Worth mentioning that the problem doesn't apply to all generic types. As this code works:

enum E:
    case A
    case B
    case C

type NotC = E.A.type | E.B.type

def doSomething(): Option[NotC] = Some(E.A)

def bee(nu: NotC): Unit =
    println("bee")

@main def main(): Unit =
  doSomething() match
    case Some(nnu) => bee(nnu)

    case _ => ()
som-snytt

som-snytt commented on Jun 18, 2025

@som-snytt
Contributor

@WhiteZh use -Vprint:all to see how code is expanded/translated, where all can be a "phase name".

If they accept #17563 someday, one could skip over explaining what a phase is, as a baby step.

added and removed
stat:needs triageEvery issue needs to have an "area" and "itype" label
on Jun 25, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @som-snytt@noti0na1@Gedochao@WhiteZh

        Issue actions

          Union Type Gets Widen When Inside Tuple · Issue #23387 · scala/scala3