Open prolativ opened 1 year ago
@prolativ Since you were already looking at the issue would you be up to try to diagnose and fix this?
@odersky how would we like to solve the problem then? Make the actual behaviour compliant with the specification of Selectable
? Or is there any chance to update the specification to make things more consistent?
I don't know, and won't have the cycles to get deep into it. We need a simple fix. Just explore whatever can be made to work.
I did some analysis and it seems like an attempt to bring the behaviours of Dynamic
and Selectable
closer to each other wouldn't be possible without introducing major breaking changes. The desugaring of applyDynamic
for subtypes of Selectable
takes into account the declared method signatures from refinements to determine how many argument lists should be consumed by the syntactic transformation (where the consumed lists are concatenated), while for Dynamic
s the number and structure of argument lists are preserved, e.g.
sel.foo(x1)(y1, y2)(...)
will become sel.applyDynamic("foo")(x1, y1, y2, ...)
dyn.foo(x1)(y1, y2)(...)
will become dyn.applyDynamic("foo")(x1)(y1, y2)(...)
In both cases the shape of the signatures of applyDynamic
don't really have to exactly match the application shape resulting from the desugaring, as long as some further syntactic adjustments can make the entire code compile. This makes
def applyDynamic(name: String)(args: Int*)(arg: Int): Int = args.sum + arg
(in case of Selectable
s) effectively equivalent to
def applyDynamic(name: String)(args: Int*): Int => Int = (arg: Int) => args.sum + arg
so it will work for a refinement method like
def foo(x: Int)(y: Int): Int => Int
which is sel2.foo
case from the initial snippet with a modified result type. I'm still wondering whether we could emit an error (or at least a warning) instead of throwing a ClassCastException
for sel2.foo
and similar cases. My idea for this moment would be:
applyDynamic
by turning trailing curried parameter lists into function parameters, e.g. for
def applyDynamic(name: String)(args: Int*)(x: Boolean)(y: String, z: Char): Int
the normalized type would be Boolean => (String, Char) => Int
by transforming to
def applyDynamic(name: String)(args: Int*): Boolean => (String, Char) => Int
applyDynamic
to the type declared in a structural refinement is ever possible, so for
class Sel2 extends Selectable {
def applyDynamic(name: String)(args: Int*)(arg: Int): Int = args.sum + arg
}
val sel2 = (new Sel2).asInstanceOf[Sel2 {
def foo(x: Int)(y: Int): Int
}]
that would cause a compilation warning/error because Int => Int
can never happen to be an Int
.
However implementing this check might be quite tricky (e.g. what about using
parameters placed in different positions in the signature of applyDynamic
?)
The problem of dangling parameter lists in applyDynamic
, however, seems to be independent of the problem of combining varargs with concatenated parameter lists.
E.g. when given
class Sel1 extends Selectable {
def applyDynamic(name: String)(args: Int*): Int = args.sum
}
val sel1 = (new Sel1).asInstanceOf[Sel1 {
def baz(xs: Int*)(ys: Int*): Int
}]
we could try to make
sel1.baz(1)(2)
compile by desugaring it to
sel1.applyDynamic("baz")(1, 2)
but what about something like
val ints1 = Seq(1)
sel1.baz(ints1*)(2)
?
sel1.applyDynamic("baz")(ints1*, 2)
is not something legal in general. On the other hand
val ints2 = Seq(2)
sel1.baz(1)(ints2*)
desugared as
sel1.applyDynamic("baz")(1, ints2*)
would make sense, although this seems quite inconsistent.
@odersky any opinions on how we should handle these 2 problems?
Compiler version
3.3.2-RC1-bin-20230615-916d4e7-NIGHTLY and before
Minimized code
Output
When one tries to compile and run the snippet from above after uncommenting only one commented line at a time, here's what happens in each case:
sel1.foo
: Compilation succeeds, prints3
. Relevant-Xprint:typer
output:sel1.bar
: Compilation errorRelevant
-Xprint:typer
output:sel2.foo
: Runtime exceptionRelevant
-Xprint:typer
output:sel2.bar
: Compilation errorRelevant
-Xprint:typer
output:Expectation
The specification says:
Given a value
v
of typeC { Rs }
, whereC
is a class reference andRs
are structural refinement declarations, and givenv.a
of typeU
[...]U
is a method type(T11, ..., T1n)...(TN1, ..., TNn): R
and it is not a dependent method type, we mapv.a(a11, ..., a1n)...(aN1, ..., aNn)
to:Thus, according to the spec, the 4 cases should get desugared as follows:
sel1.foo(1)(2)
->sel1.applyDynamic("foo")(1, 2).asInstanceOf[Int]
sel1.bar(1)(2)
->sel1.applyDynamic("bar")(1, 2).asInstanceOf[Int]
sel2.foo(1)(2)
->sel2.applyDynamic("foo")(1, 2).asInstanceOf[Int]
sel2.bar(1)(2)
->sel2.applyDynamic("bar")(1, 2).asInstanceOf[Int]
The first two cases should compile successfully and return3
then, while the latter two should either perform eta-expansion and fail at runtime with an exception on casting a function toInt
(we should try to avoid this situation) or complain about a missing parameter.On the other hand, as a user I would expect code using
Selectable
to behave analogously to code usingDynamic
instead. Thus, given the snippet belowand uncommenting a single line at a time, I get:
dyn1.foo
:dyn1.bar
:dyn2.foo
: Compiles, prints3
dyn2.bar
: Compiles, prints3
For
Dynamic
s the behaviour in scala 2 is basically the same, though the compilation errors fordyn1.foo
anddyn1.bar
are slightly different:Summing this up, the behaviour for
Dynamic
s in this case is the opposite to the specified behaviour forSelectable
, which in turn is different to the current actual (definitely buggy in some way) behaviour forSelectable
.