Skip to content

Typer regression in getkyo/kyo #22974

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

Open
WojciechMazur opened this issue Apr 11, 2025 · 7 comments · May be fixed by #23030
Open

Typer regression in getkyo/kyo #22974

WojciechMazur opened this issue Apr 11, 2025 · 7 comments · May be fixed by #23030
Assignees
Labels
area:typer itype:bug regression This worked in a previous version but doesn't anymore

Comments

@WojciechMazur
Copy link
Contributor

Based on OpenCB failure in getkyo/kyo - build logs
We failed to spot the problem earlier, becouse it previously failed due to different errors since 3.6.4-RC1-bin-20241121-5d1d274-NIGHTLY (when using different project revision)

Compiler version

3.7.0-RC2
Last good release: 3.7.0-RC1-bin-20250119-bd699fc-NIGHTLY
First bad release: 3.7.0-RC1-bin-20250120-db23c08-NIGHTLY

Bisect points to fe2e6e9 but some revisions fail with different errors or StackOverflowError

Minimized code

trait Kyo[+A, -S]
opaque type <[+A, -S] = A | Kyo[A, S]

trait Abort[-E]
opaque type IO <: Abort[Nothing] = Abort[Nothing]
trait Error[+E]
opaque type Success[+A] = A
opaque type Result[+E, +A] >: Success[A] | Error[E] = Success[A] | Error[E]

object IO:
  object Unsafe:
    inline def apply[A, S](inline f: A < S): A < (IO & S) = ???

opaque type Async <: IO = IO

opaque type Queue[A] = Queue.Unsafe[A]
object Queue:
  abstract class Unsafe[A]
  opaque type Unbounded[A] <: Queue[A] = Queue[A]
  object Unbounded:
    inline def initWith[A]()[B, S](inline f: Unbounded[A] => B < S): B < (IO & S) =
        IO.Unsafe(f(Unsafe.init()))

    opaque type Unsafe[A] <: Queue.Unsafe[A] = Queue[A]
    object Unsafe:
      def init[A](): Unsafe[A] = ???

sealed trait Resource
object Resource:
  def run[A, S](v: A < (Resource & S)): A < (Async & S) =
    Queue.Unbounded.initWith[Unit < (Async & Abort[Throwable])]() { q =>
      ???
    }

Output

Compiling project (Scala 3.7.1-RC1-bin-20250411-98c84c3-NIGHTLY, JVM (21))
[error] ./defs.scala:31:5
[error] Found:    Unbounded$_this.Unsafe[Unit < (Async & Abort[Throwable])]
[error] Required: Queue.Unbounded[Unit < (Async & Abort[Throwable])]
Error compiling project (Scala 3.7.1-RC1-bin-20250411-98c84c3-NIGHTLY, JVM (21))

Different error 3.7.0-RC1-bin-20250120-db23c08-NIGHTLY

[error] ./defs.scala:30:3
[error] Recursion limit exceeded.
[error] Maybe there is an illegal cyclic reference?
[error] If that's not the case, you could also try to increase the stacksize using the -Xss JVM option.
[error] For the unprocessed stack trace, compile with -Xno-enrich-error-messages.
[error] A recurring operation is (inner to outer):
[error] 
[error]   find-member defs$package.Success
Error compiling project (Scala 3.7.0-RC1-bin-202501

Expectation

@WojciechMazur WojciechMazur added area:typer itype:bug regression This worked in a previous version but doesn't anymore labels Apr 11, 2025
@WojciechMazur
Copy link
Contributor Author

Another kyo issue refering to the same bisect, but not minimized:

//> using dep "io.getkyo::kyo-core:0.17.0"
//> using dep "org.scalatest::scalatest-freespec:3.2.19"

package kyo
import org.scalatest.freespec.AsyncFreeSpec
import org.scalatest.Assertion

class Test extends AsyncFreeSpec {
  def run(v: Assertion < (Abort[Any] & Async & Resource)): Assertion = ???
  given Frame = ???

  "test" in run {
    (for
      size <- Choice.get(Seq(1))
      channel <- Channel.init[Int](size)
      latch <- Latch.init(1)
      pollFiber <- Async.run(
        latch.await.andThen(Async.fill(100, 100)(Abort.run(channel.poll)))
      )
      polled <- pollFiber.get
    yield assert(0 == polled.count(_.toMaybe.flatten.isDefined)))
      .pipe(Choice.run, _.unit, Loop.repeat(1))
      .andThen(succeed)
  }
}
[error] ./defs.scala:21:36
[error] Found:    $proxy38.Absent | $proxy38.Present[Int]
[error] Required: (kyo.Maybe.Absent | kyo.Maybe.Present[Int]) & ($proxy38.Absent | $proxy38.Present[Int])
[error]     yield assert(0 == polled.count(_.toMaybe.flatten.isDefined)))
[error]          

@Gedochao
Copy link
Contributor

cc @jchyb

@jchyb jchyb self-assigned this Apr 14, 2025
@SethTisue
Copy link
Member

fyi @fwbrasil

@jchyb
Copy link
Contributor

jchyb commented Apr 15, 2025

Minimised further (same error, the issue seems to be that somewhere when retyping the inlined f function and it's application, we lose the data about proxies - f$proxy.apply is typed as () => Queue.Unbounded even though it itself is (Queue.Unbounded => Unit) & ($proxy1.Unbounded => Unit), at least as shown by the logs):

opaque type Queue = Queue.Unsafe
object Queue:
  abstract class Unsafe
  opaque type Unbounded = Queue
  object Unbounded:
    inline def initWith()(f: Unbounded => Unit): Unit =
      f(Unsafe.init())

    opaque type Unsafe <: Queue.Unsafe = Queue
    object Unsafe:
      def init[A](): Unsafe = ???

sealed trait Resource
object Resource:
  def run: Unit =
    Queue.Unbounded.initWith() { q =>
      ???
    }

@jchyb
Copy link
Contributor

jchyb commented Apr 18, 2025

Minimisation without inlines:

object outer:
  opaque type Queue = Queue.Unsafe
  object Queue:
    abstract class Unsafe
    opaque type Unbounded = Queue
    object Unbounded:
      opaque type Unsafe <: Queue.Unsafe = Queue
      object Unsafe:
        def init[A](): Unsafe = ???

object Resource:
  def run: Unit =
    // we would generate these as part of inlining
    val $proxy2: outer.type{type Queue = outer.Queue.Unsafe} =
      outer.asInstanceOf[outer.type{type Queue = outer.Queue.Unsafe}]
    val $proxy1: $proxy2.Queue.type{type Unbounded = $proxy2.Queue} =
      $proxy2.Queue.asInstanceOf[$proxy2.Queue.type{type Unbounded = $proxy2.Queue}]
    val $proxy3: $proxy1.Unbounded.type{type Unsafe = $proxy2.Queue} =
      $proxy1.Unbounded.asInstanceOf[$proxy1.Unbounded.type{type Unsafe = $proxy2.Queue}]
    val Unbounded$_this:
      $proxy1.Unbounded.type{type Unsafe = $proxy2.Queue} = $proxy3
    val f$proxy1: (outer.Queue.Unbounded => Unit) & ($proxy1.Unbounded => Unit) =
      ((q: outer.Queue.Unbounded) => ???).asInstanceOf[(outer.Queue.Unbounded => Unit) & ($proxy1.Unbounded => Unit)]

    val app: Unbounded$_this.Unsafe = Unbounded$_this.Unsafe.init()
    f$proxy1(app)

The underlying issue seems to be with the type comparer. The above should compile, yet it does not, as the apply method of (outer.Queue.Unbounded => Unit) & ($proxy1.Unbounded => Unit) is typed as outer.Queue.Unbounded => Unit. We can prove that this is incorrect, by removing (outer.Queue.Unbounded => Unit) & and having the code compile successfully. What seems to happen is that when merging outer.Queue.Unbounded and $proxy1.Unbounded Function1 arguments, we test if one element is the subtype of another, and remove that subtype (here, while the prefixing refinements are subtypes of one another, the actual aliased types are not).

@jchyb
Copy link
Contributor

jchyb commented Apr 23, 2025

Minimisation derived from the second issue listed here (although with a different error);

object other:
  sealed abstract class Absent
  case object Absent extends Absent
  case class PresentAbsent(val depth: Int)
  opaque type Present[+A] = A | PresentAbsent
  opaque type Maybe[+A] >: (Absent | Present[A]) = Absent | Present[A]

  extension [A](self: Maybe[A]) {
    inline def flatten[B]: Maybe[B] = if isEmpty then Absent else ???
    inline def isDefined: Boolean = !isEmpty
    def isEmpty: Boolean = self.isInstanceOf[Absent]
  }

class Test {
  def main(): Unit =
    import other.Maybe
    val res: Maybe[Maybe[Int]] = ???
    res.flatten.isDefined
}

error:

20 |    res.flatten.isDefined
   |    ^^^
   |    Found:    (self$proxy1 : (res : other.Maybe[other.Maybe[Int]]) &
   |      $proxy1.Maybe[$proxy1.Maybe[Int]])
   |    Required: other$_this.Maybe[other.Maybe[Int]]

@jchyb
Copy link
Contributor

jchyb commented Apr 24, 2025

Minimisation of the second issue (different error, and a different bug from the above) - needs two modules/compilation steps.
macro.scala:

import scala.quoted._

inline def passThorugh(inline condition: Boolean): Any =
  ${ passThorughImpl('{condition}) }

def passThorughImpl(condition: Expr[Boolean])(using Quotes): Expr[Any] = condition

Maybe.scala:

package pack
import Maybe._
opaque type Maybe[+A] >: (Absent | Present[A]) = Absent | Present[A]
object Maybe:
  sealed abstract class Absent
  case object Absent extends Absent
  object internal:
    case class PresentAbsent(val depth: Int)
  opaque type Present[+A] = A | internal.PresentAbsent

  extension [A](self: Maybe[A]) {
    inline def flatten[B]: Maybe[B] = ???
    inline def isDefined: Boolean = ???
  }

main.test.scala:

object Test {
  def main(): Unit =
    import pack.Maybe
    val res: Maybe[Maybe[Int]] = ???
    passThorugh(res.flatten.isDefined)
}

scala-cli compile --test --server=false Maybe.scala main.test.scala macro.scala

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area:typer itype:bug regression This worked in a previous version but doesn't anymore
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants