ponyc
ponyc copied to clipboard
Type unions: default arguments applied to the wrong function
When all member types in an union implement a function with the same number of arguments and the same return type, but one of them has a default argument, as follows:
class Foo
fun string(x: String = "foo"): String iso^ => x.clone()
class Bar
fun string(x: String): String iso^ => x.clone()
Then calling that function on an instance of that union will cause the default argument (here, "foo"
) to be passed to the function, no matter the underlying type of the variable. Consider the following code:
class Foo
fun string(x: String = "foo"): String iso^ =>
x.clone()
class Bar
fun string(x: String): String iso^ =>
x.clone()
actor Main
new create(env: Env) =>
let x: (Foo | Bar) = Bar
env.out.print(x.string())
The code above should fail to compile (as per the discussion in #4073), but instead compiles fine and prints "foo"
.
The issue seems to be influenced by the order in which the types are defined in the union type, as changing the type of x
from (Foo | Bar)
to (Bar | Foo)
will prevent the issue, with the code failing to compile:
~/dev/ponylang/ponyc/crash_examples/main.pony:16:27: not enough arguments
env.out.print(x.string())
^
Info:
~/dev/ponylang/ponyc/crash_examples/main.pony:9:14: definition is here
fun string(x: String): String iso^ =>
^
Also note that this is not about jumping to the wrong function implementation, as demonstrated by the following code:
use @printf[None](fmt: Pointer[U8] tag, ...)
class Foo
fun string(x: String = "foo"): String iso^ =>
@printf("Called on foo\n".cstring())
x.clone()
class Bar
fun string(x: String): String iso^ =>
@printf("Called on bar\n".cstring())
x.clone()
actor Main
new create(env: Env) =>
let x: (Foo | Bar) = Bar
env.out.print(x.string())
The above code will print
Called on bar
foo
So it appears that by the time we get to the check for "are there default args that make this match" we have already picked the function signature from the first member of the union and if that contains a default arg, it is used for all members. You can see this from:
class Foo
fun string(x: String = "foo"): String iso^ =>
x.clone()
class Bar
fun string(x: String = "bar"): String iso^ =>
x.clone()
actor Main
new create(env: Env) =>
let x: (Bar | Foo) = Foo
env.out.print(x.string())
I'm guessing this is happening during the flatten pass. It doesn't really matter though. As per #4073, this requires complete reworking of how default arguments work.