Open gusty opened 3 years ago
Here's a smaller repro:
open System.Collections.Generic
type [<Struct>]MemoizationKeyWrapper<'a> = MemoizationKeyWrapper of 'a
type MemoizeN =
static member getOrAdd (cd: Dictionary<MemoizationKeyWrapper<'a>,'b>) (f: 'a -> 'b) k =
match cd.TryGetValue (MemoizationKeyWrapper k) with
| (true , v) -> v
| (false, _) ->
let v = f k
cd.Add (MemoizationKeyWrapper k, v)
v
let inline memoizeN (f:'``(T1 -> T2 -> ... -> Tn)``): '``(T1 -> T2 -> ... -> Tn)`` =
let inline call_2 (a: ^MemoizeN, b: ^b) = ((^MemoizeN or ^b) : (static member MemoizeN : ^MemoizeN * 'b -> _ ) (a, b))
call_2 (Unchecked.defaultof<MemoizeN>, Unchecked.defaultof<'``(T1 -> T2 -> ... -> Tn)``>) f
type MemoizeN with
static member MemoizeN (_: obj , _: 'a -> 'b) = MemoizeN.getOrAdd (Dictionary ())
static member inline MemoizeN (_: MemoizeN, _:'t -> 'a -> 'b) = MemoizeN.getOrAdd (Dictionary ()) << (<<) memoizeN
and some tests
let effs = ResizeArray ()
let f x = effs.Add "f"; string x
let g x (y:string) z : uint32 = effs.Add "g"; uint32 (x * int y + int z)
let h x y z = effs.Add "h"; new System.DateTime (x, y, z)
let sum2 (a:int) = effs.Add "sum2"; (+) a
let sum3 a (b:int) c = effs.Add "sum3"; a + b + c
let sum4 a b c d : int = effs.Add "sum4"; a + b + c + d
// memoize them
let msum2 = memoizeN sum2
let msum3 = memoizeN sum3
let msum4 = memoizeN sum4
let mf = memoizeN f
let mg = memoizeN g
let mh = memoizeN h
// check memoization really happens
let _v1 = msum2 1 1
let _v2 = msum2 1 1
let _v3 = msum2 2 1
let _v4 = msum3 1 2 3
let _v5 = msum3 1 2 3
let _v6 = msum4 3 1 2 3
let _v7 = msum4 3 1 2 3
let _v8 = msum4 3 5 2 3
let _v9 = mf 3M
let _v10 = mf 3M
let _v11 = mg 4 "2" 3M
let _v12 = mg 4 "2" 3M
let _v13 = mh 2010 1 1
let _v14 = mh 2010 1 1
Assert.AreEqual ([|"sum2"; "sum2"; "sum3"; "sum4"; "sum4"; "f"; "g"; "h"|], effs.ToArray ()))
Thanks for isolating the issue @gusty. I debugged the code and noticed witnesses are not helpful here so it comes down to Fable's own trait-call resolution. If I understand the sample correctly memoizeN
will be calling MemoizeN.MemoizeN(_: MemoizeN, _:'t -> 'a -> 'b)
until the arity of the second argument is reduced to 1 in which case it will call the first overload and recursivity will stop. The (dead simple) resolution mechanism of Fable to find the correct member is here: https://github.com/fable-compiler/Fable/blob/d94e94cc1eb07dcf73be43f4e883321453377f71/src/Fable.Transforms/FSharp2Fable.Util.fs#L924-L942
The interesting part is the typeEquals
function which is called in non-strict mode and whose implementation is this: https://github.com/fable-compiler/Fable/blob/d94e94cc1eb07dcf73be43f4e883321453377f71/src/Fable.Transforms/Transforms.Util.fs#L542-L570
If I change the implementation as in the diff below now the code compiles but the test doesn't pass because the first overload is always being picked.
So I'm assuming when there are multiple candidates we somehow must pick the one with the arity closer to the expected one. Do you know what's the exact algorithm for this resolution? Is there any scoring mechanism to match the argument types?
Is there any scoring mechanism to match the argument types?
F# compiler has a tie breaker for deciding over multiple candidates.
But there are many problems to that approach. The tie breaker, in case of trait calls, does some incrementing constraint solving recursively, which is very complicated and far from perfect.
I think here we can make Fable smarter.
We can certainly apply some rules like the arity, and stuff like pick the less generic one, but it won't be the same as the F# compiler.
I did propose in an F# suggestion an attribute based priority resolution. If Fable can interpret that attribute we can decorate our overloads with it and hint Fable about priorities.
So I'm assuming when there are multiple candidates we somehow must pick the one with the arity closer to the expected one.
Actually, in this (and many other of my repros) case, what tie breaks is the first dummy parameter.
The second overload has type MemoizeN
which is exactly what is being sent while the first overload has obj
which matches MemoizeN
as it is a super-class, so when both matches the closest one in the type hierarchy is preferred, which in this code is the second one.
I've checked the F# of your example and although there seems to be an issue with the witness passing in Fable because when debugging I don't see any witness in the context at the time of resolving the trait call, unfortunately the witness won't help either because F# only passes it once in the top call of msum4
for example, but here we need to have different resolutions as memoizeN
is being called recursively.
Description
It seems a stack overflow happens internally in presence of recursive (but finite) trait calls.
Repro code
Expected and actual results
Compile and work but it fails compilation with an error stating "Maximum call stack exceeded"
Related information
Note: this code also depends on fixing #2472 but still can be tested by removing the tests for
Tuple<_>
.Also consider as alternative as incorporating it as a fable test, to test F#+ tests, where this it is already included (but disabled of course).