ponyc icon indicating copy to clipboard operation
ponyc copied to clipboard

Type unions: default arguments applied to the wrong function

Open ergl opened this issue 2 years ago • 1 comments

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

ergl avatar Apr 06 '22 12:04 ergl

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.

SeanTAllen avatar Oct 07 '23 20:10 SeanTAllen