enso-org / enso

Hybrid visual and textual functional programming.
https://enso.org
Apache License 2.0
7.34k stars 321 forks source link

Suspended application of default arguments playing badly with warnings and causing weird behaviour #10605

Open radeusgd opened 1 month ago

radeusgd commented 1 month ago

Consider the following repro suspending-defaults.enso:

from Standard.Base import all

type My_Table
    Value x

    union self (tables : My_Table | Vector) arg1=1 arg2=2 arg3=3 =
        IO.println "Called ("+self.to_text+").union "+tables.to_text+" with "+arg1.to_text+" "+arg2.to_text+" "+arg3.to_text
        My_Table.Value [self.x, tables, arg1+arg2+arg3]

    columns self =
        "{Columns = "+self.x.to_text+"}"

call_union tables =
    first = tables.first
    rest = tables.drop 1
    first.union rest ...

main =
    check table =
        IO.println table.columns
    check <| call_union [My_Table.Value 1]
    tw = Warning.attach "foo" (My_Table.Value 2)
    check <| call_union [tw]
    check <| call_union [tw] arg2=1000

Actual behaviour

Running the above script yields:

Called ((My_Table.Value 1)).union [] with 1 2 3
{Columns = [1, [], 6]}
Called ((My_Table.Value 2)).union [] with 1 2 3
Called ((My_Table.Value 2)).union [] with 1 2 3
Called ((My_Table.Value 2)).union [] with 1 2 3
Execution finished with an error: Method `columns` of type My_Table could not be found.
        at <enso> suspending-defaults.main.check<arg-1>(suspending-defaults.enso:20:20-32)
        at <enso> suspending-defaults.main.check(suspending-defaults.enso:20:9-32)
        at <enso> suspending-defaults.main(suspending-defaults.enso:23:5-28)

We can see 2 anomalies here:

  1. The second call to union is executed 3 times (!) (as indicated by the prints) even though it should only happen once. We have a problem with re-running a side-effecting computation - that should never happen!
  2. Afterwards, the return value is somehow broken and we cannot call the columns method on it. Even though in the exactly same scenario above such call has worked all fine.

The difference between first and second calls is only that the second one contains warnings.

Expected behaviour

The program should print

Called ((My_Table.Value 1)).union [] with 1 2 3
{Columns = [1, [], 6]}
Called ((My_Table.Value 2)).union [] with 1 2 3
{Columns = [2, [], 6]}
Called ((My_Table.Value 2)).union [] with 1 1000 3
{Columns = [2, [], 1004]}
radeusgd commented 1 month ago

Of course if I remove the Warning.attach, we do get the expected behaviour. But it should also work the same with warnings attached.

Interestingly also, if I remove the ... then the first 2 calls now work fine. The third one will fail however because we no longer suspend the default arguments, so I could no longer override arg2:

Called ((My_Table.Value 1)).union [] with 1 2 3
{Columns = [1, [], 6]}
Called ((My_Table.Value 2)).union [] with 1 2 3
{Columns = [2, [], 6]}
Called ((My_Table.Value 2)).union [] with 1 2 3
Execution finished with an error: Type error: expected a function, but got My_Table.Value.
        at <enso> suspending-defaults.main<arg-1>(suspending-defaults.enso:24:14-38)
        at <enso> suspending-defaults.main(suspending-defaults.enso:24:5-38)

But this 'proves' that the problematic behaviour is caused by some interaction between both the suspending of default arguments using ... operator and warnings.

JaroslavTulach commented 1 month ago

The columns method is being searched for on (org.enso.interpreter.runtime.callable.function.Function) My_Table.union[e.enso:6-8] self=(My_Table.Value 2) tables=[]