Skip to content

defer blocks discarding unit effects #903

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

Closed
johnhungerford opened this issue Dec 5, 2024 · 13 comments
Closed

defer blocks discarding unit effects #903

johnhungerford opened this issue Dec 5, 2024 · 13 comments
Labels
bug Something isn't working

Comments

@johnhungerford
Copy link
Contributor

Minimized example

import kyo.*

def runInner: Unit < Async = defer:
  await(Console.println("test"))

@main def main(): Unit =
  import AllowUnsafe.embrace.danger
  val _ =  KyoApp.Unsafe.runAndBlock(Duration.Infinity)(runInner)

scalac options in build.sbt:

scalacOptions ++= Seq(
  "-Wvalue-discard", 
  "-Wnonunit-statement", 
  "-Wconf:msg=(discarded.*value|pure.*statement):error",
),

Expected result

Should cause compiler error: discarded non-Unit value of type Unit < (kyo.IO & kyo.Abort[java.io.IOException]) (If the defer block in runInner is replaced with a direct call to Console.println, this is what happens)

Actual result

No compiler error or warnings, and the application runs without displaying anything message

@johnhungerford
Copy link
Contributor Author

johnhungerford commented Dec 5, 2024

Workaround: annotating runInner as Any < ... will cause the compiler to catch the error. It seems to be a problem only for unit values. Anyone working with kyo-direct might consider using Any in place of Unit until some solution is found.

@hearnadam hearnadam added the bug Something isn't working label Jan 4, 2025
@fwbrasil
Copy link
Collaborator

/bounty 100

@fwbrasil
Copy link
Collaborator

I took a look at this and couldn't find a way to fix it. It seems the compiler just bypasses the check. cc/ @rssh

@algora-pbc algora-pbc bot removed the 💎 Bounty label Mar 28, 2025
@ahoy-jon
Copy link
Contributor

I may have found something similar

//> using scala 3.7.0-RC3

//> using option -Wvalue-discard
//> using option -Wnonunit-statement
//> using option -Wconf:msg=(unused.*value|discarded.*value|pure.*statement):error
//> using option -language:strictEquality

//> using dep io.getkyo::kyo-prelude::0.18.0
//> using dep io.getkyo::kyo-core::0.18.0
//> using dep io.getkyo::kyo-direct::0.18.0
//> using dep io.getkyo::kyo-combinators::0.18.0

import kyo.*

object KyoAppTest extends KyoApp {
  val print: Unit < IO = Console.printLine("YO")
  val fail: Unit < Abort[Exception] = Abort.fail(new Exception("fail"))

  val prg = print *> fail

  val prgWithDefer = defer {
    print.now
    fail.now
  }

  val oups: Unit < (IO & Abort[Exception]) = defer {
    Console.printLine("YO").now
    Abort.fail(new Exception("fail")).now
  }

  val fixOups: Unit < (IO & Abort[Exception]) = defer {
    Console.printLine("YO").now
    Abort.fail(new Exception("fail")).now
    {}
  }

  val prgs=  Chunk(prg, prgWithDefer, oups, fixOups).zipWithIndex

  val all = Kyo.foreach(prgs)((prg, i) => Console.printLine(i) *> Abort.run(prg))

  run(all.unit)
}
0
YO
1
YO
2
3
YO

so it doesn't execute properly "oups"

  val oups: Unit < (IO & Abort[Exception]) = defer {
    Console.printLine("YO").now
    Abort.fail(new Exception("fail")).now
  }

but it works for

  val fixOups: Unit < (IO & Abort[Exception]) = defer {
    Console.printLine("YO").now
    Abort.fail(new Exception("fail")).now
    {}
  }

and other versions, such as

  val oups: Unit < (IO & Abort[Exception]) = defer {
    Console.printLine("YO").now
    Abort.fail(new Exception("fail")).unit.now
  }

There is a discard of the block for Nothing < S in tail position

@ahoy-jon
Copy link
Contributor

ahoy-jon commented Apr 19, 2025

I am not able to reproduce the original bug with io.getkyo::kyo-direct::0.18.0

def runInner: Unit < Async = defer:
  Console.printLine("test").now

object Kyo903 extends KyoApp {
  run(runInner)
}

produce

test

def runInner: Unit < Any = defer:
  Console.printLine("test").now

object Kyo903 extends KyoApp {
  run(runInner)
}

don't produce a compile error, and don't produce a result

@ahoy-jon
Copy link
Contributor

So, what is happening. With this code

  def runInnerOK: Unit < IO = defer:
    Console.printLine("test").now

  def runInnerNOK: Unit < Any = defer:
    Console.printLine("test").now

we have (decompiled)

    public Object runInnerOK() {
        LazyRef var1 = new LazyRef();
        Async.InferAsyncArg InferAsyncArg_this = new Async.InferAsyncArg(this.given_KyoCpsMonad_s$5(var1));
        return InferAsyncArg_this.am().apply(RunInner$::runInnerOK$$anonfun$1);
    }

    public Object runInnerNOK() {
        Pending.package var1 = .MODULE$;
        LazyRef var4 = new LazyRef();
        Async.InferAsyncArg InferAsyncArg_this = new Async.InferAsyncArg(this.given_KyoCpsMonad_s$6(var4));
        InferAsyncArg_this.am().apply(RunInner$::$anonfun$1);
        BoxedUnit v$proxy7 = BoxedUnit.UNIT;
        return v$proxy7;
    }

Which show that the compiler is inserting a BoxedUnit.UNIT, and returns it, and doesn't warn on value-discard as other similar situations like:

def anyCallToUnit: Unit < Any =
    runInnerOK
discarded non-Unit value of type Unit < kyo.IO. Add `: Unit` to discard silently

The problem can be reproduced with

  transparent inline def toto: Unit < IO = Console.printLine("test")

  def tata: Unit < Any =
    toto
    public Object tata() {
        Pending.package var1 = .MODULE$;
        Frame.package.Frame var4 = kyo.Frame.package.Frame..MODULE$;
        Frame.package var5 = kyo.Frame.package..MODULE$;
        kyo.Console..MODULE$.printLine("test", "0dependent.scala:111:5|dependant.RunInner$|tata|def tata: Unit < Any =\n  toto\ud83d\udccd");
        BoxedUnit v$proxy8 = BoxedUnit.UNIT;
        return v$proxy8;
    }

It is an issue with transparent inline and value-discard warnings.

@ahoy-jon
Copy link
Contributor

ahoy-jon commented Apr 19, 2025

Done, it's here : scala/scala3#23018

Maybe it would be nice to have some Scalafix rules to detect those cases, or use a custom Unit type ?

opaque type Unit <: scala.Unit = scala.Unit

solutions for now

val x = defer { ... } .unit
val x:Any < ...  = defer {...}

@ahoy-jon
Copy link
Contributor

created a Scalafix rule to help with it: https://github.com/ahoy-jon/kyo-scalafix-rules

@ahoy-jon
Copy link
Contributor

⚠️ There is the same issue with Env.runLayer, provide:

  val runLayer: Unit < Any = {
    Env.runLayer(Layer.empty)(Console.printLine("test"))
  }
  val provide: Unit < Any = {
    Console.printLine("test").provide(Layer.empty)
  }

and probably runLiftTest.

@ahoy-jon
Copy link
Contributor

#1202 solves the issue, by making the compiler report errors for those cases.

@hearnadam
Copy link
Collaborator

Nice work!

@fwbrasil
Copy link
Collaborator

Pretty cool! Thank you @ahoy-jon

@ahoy-jon
Copy link
Contributor

ahoy-jon commented May 24, 2025

Thank you! To make it clear, it fixes problems with:

  • defer
  • .provide
  • Env.runLayer
  • any transparent inline that would produce a Unit < S, and could be silently dropped by the compiler over a lift(()) like:
transparent inline def toto: Unit < IO = Console.printLine("test")

def tata: Unit < Any =
  toto

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

4 participants