Closed elfprince13 closed 3 years ago
Just curious, does this issue only arise when compiling with -source:3.0-migration
?
I'll double check on that in the morning, but the issue does not arise if you comment out:
implicit val a:A = new A{ def apply():Int = 6}
implicit val b:B = new B{ def apply():Int = 7}
TIL implicit nesting level is tied to -source
. My other idle question is where does evidence$5
come from?
The expectation is slightly off: an (a, b) ?=> op(a,b)
is constructed in main. It is passed to doOp
.
Also, doOp
could be spelled dooWop
.
Your code compiles in Scastie. Are you using a language option?
The calling behavior does seem correct BTW. By specification, context functions are always called whenever they are referred to. The problem here is that the context parameters given by doOp
are not prioritized over the outer implicits, which they normally should (but maybe not in Scala 2 compat mode).
Yes, implicit nesting level is not bumped under -source 3.0-migration
. Otherwise the code works.
By specification, context functions are always called whenever they are referred to.
So then when I comment out the implicit definitions in main why is there not an error that no implicit could be found?
And as @som-snytt says, where is evidence$5
coming from?
@elfprince13 see my previous comment and https://dotty.epfl.ch/docs/reference/contextual/context-functions.html
if the expected type of an expression E is a context function type (T_1, ..., T_n) ?=> U and E is not already an context function literal, E is converted to a context function literal... x_1, ..., x_n are available as givens in E.
I used -Xprint:typer
to see some expansion, but I assume evidence$5
is generated perhaps by typechecking, such as when rewrites are attempted. But I don't know how to view that. The print output only shows evidence$1
in op
, so I conjecture it is retrying the arg to doOp
and showing only the next fresh name, which is evidence$5
. Somewhere it says the names are arbitrary so we're not supposed to worry about them.
It would also be nice if Scala 3 had -Xlint
so it could tell us that certain variables are unused.
@elfprince13 In principle Console.println(doOp(op))
expands into something like Console.println(doOp(ev$0 ?=> op(summon)))
.
It still seems like there's a bug here. If I bump the source-level to 3.0, then I get 5
regardless of whether main
's set of implicit vals are defined. If I keep it on 3.0-migration, then I get 5
when they are not defined and a compilation error when they are. So obviously the implicit function is not evaluated when referred to in main
, so what's the point of the overhead in creating a wrapper function to "cast" an Op
to an Op
?
There is no bug at all. The old behavior when several implicits were in local scope was to raise an ambiguity error. That's why it doesn't compile. In the revised Scala 3 language, the compiler instead picks the innermost one (ev$0 in my example above).
Okay, so I think I understand both the migration and 3.0 behavior, and maybe this deserves a fresh issue, but .... expanding doOp(op)
to doOp(ev$0 ?=> op)
breaks one of the fundamental use cases for contextual abstractions and doesn't appear to have any benefits.
Consider the following code, emulating the behavior of a very simple STM that never commits:
import scala.annotation.tailrec
object Explode {
trait Txn {}
type AtomicOp[Z] = Txn ?=> Z
object AbortAndRetry extends scala.util.control.ControlThrowable("abort and retry this transaction") {}
def beginTxn:Txn = new Txn {}
def commit(using txn:Txn) = throw AbortAndRetry
@tailrec def atomic[Z](txn:AtomicOp[Z]):Z = {
try {
given Txn = beginTxn
val ret:Z = txn
commit
ret
} catch {
case AbortAndRetry => atomic(txn)
}
}
def main(args:Array[String]) = {
Console.println(atomic {
"hello world"
})
}
}
Compared to the equivalent, slightly uglier Scala 2 code, this version crashes due to a Stack Overflow in the wrapper functions generated here:
case AbortAndRetry => atomic(txn)
This definitely deserves a separate issue (and I think this issue can be closed).
expanding doOp(op) to doOp(ev$0 ?=> op) breaks one of the fundamental use cases for contextual abstractions and doesn't appear to have any benefits.
You're welcome to make an alternative proposal for how this feature works in this case, but beware that it currently behaves like this for a very good reason. So you should read this first: https://infoscience.epfl.ch/record/229878?ln=en
Minimized code
Output
Expectation
op
should not be invoked inmain
but should instead be passed todoOp
. Additionally, if the compiler is supposed to invoke it inmain
(which would be very surprising), the result should be a type error (foundInt
, requiredOp
). Furthera
should not be ambiguous with some anonymous evidence when I'm not even using any context bounds or anything else implicits / givens related that would be generating evidence.