I am implementing new instantiation rules and have run into  a
nasty bug.

What happens is this: when we do inlining we have to "clone"
the function to be inlined and all its descendants (nested functions,
local variables etc), specialising them, and reparenting them to
the caller.

So if f calls g, and g has a kid h, then after inlining g's body into f,
we have a clone of h which is now a child of f. We have to do  that
because g will probably be calling h (why else give g a child if you're
not going to call it?)

Now, if g was originally a child of f, after we inline it (in all locations
its called in f) if it isn't recursive, then g is not needed any more
so we can delete it (its not needed as a separate function because it's 
been inlined)

Deleting such unused children isn't optional, it has to be done to prevent
and exponential explosion, otherwise we may try to optimise g, when it
isn't used, and we could be repeatedly be cloning stuff which generates
more and more children. If I take the child removal out of Felix a program
that compiles in 60 seconds runs for over and hour without terminating.

Now, everything appeared to work before, until I added a new instantiation
rule. Previously this instance:

instance[T] X[T] { fun f: T -> T = ... }

would match 

class X[T] { fun f:T -> T = .. }

with a call like

fun g[T] (x:T) => f(x);

Due to a non-optimal algorithm, this is no longer the case.
the reason is this:

instance [U] X[U->U]

If we have that instance as well as the X[T] one we cannot match
against the X[T] instance because after T is replaced by double -> double
for example, the U->U instance would be more specialised.

So we defer the match against X[U] until we can be sure X[U->U] could
not be matched. This delay occurs whenever the are at least two
instances that cannot be excluded as matches in the future:
one (the X[T]) could match right now, but we still can't take it if the
other one X[U->U] could match in the future  and that is ALWAYS
the case if the caller is polymorphic in its variable (and  there is at
least one instance other than the fully general one).

The new algorithm isn't very smart about making the decision
I suspect, its (hopefully) conservative though. A proper match
can always be selected when the call is reduced down to a 
concrete type by substitution of type variables. So all of this should
work (actually it doesn't seem to work but that isn't the issue here :)

So the effect is that now we have dispatches that used to be taken
that are delayed, including a common one that is causing a heap
of breakages now but worked before:

        repr -> str

This is Python repr function, which by default is just str.
The problem is that this used to go through polymorphically
because that match can ALWAYS be taken because its the default.
But now it is never taken if there's a possible definition of repr
other than the default.

Ok that's the context. So here's the problem: normally only the parent
of a function, or its siblings (more generally any descendant of the
parent) can call a function. So we can delete that child if none
of the parents descendant family call the function.

Unfortunately that rule is only true for direct calls. It fails if you have
a closure of the the descendant hiding somewhere else. 
Any function which can see the child can make a closure
of it, and save it or return it. However, this is like a direct call
and can be treated the same way: the closure can only be created
if the function making it is itself called, so the transitive closure
of direct calls PLUS closure object constructions is still enough
to determine if a function can be called or not.

The PROBLEM is that there's another kind of indirect call
than through a closure (which is effectively an indirect dispatch
through a pointer). And that is of course:

        A call to instance via a virtual function.

Now, normally an instance isn't a child of anything let alone
the caller.  But consider now in general inlining a global function,
its children get cloned and become children of the caller.
So if you inline an instance, even one which is global, its children
become children of the caller.

Ok, so far so good, except for one thing: polymorphic recursion.
When you clone a function you specialise it with the call arguments:
this applied not only to the values passed to the function but also
to the types: when you inline a polymorphic function in T, with
a call using a double value, the cloned children of the called
function become children of the caller -- specialised with T replace
by double.

BUT there is a special exception. If the function with type parameter T
calls itself with a type other than T, that call is NOT recursive. Its call
to the original function with a DIFFERENT specialisation. This is
called "polymorphic recursion".

Polymorphic recursion is exactly what I'm trying to implement!
A simple example:

instance[T,U] Str[T * U] {
        fun str (x: T, y: U) => str x + ", " + str y;
}

That inner call to str dispatches through the same virtual function

        virtual str: T -> T

that call IT, but it doesn't usually won't recurse to the same
function. It might though, if you 

        str ( (1,2.4), "Hello")

then the dispatch to  T*U splits to a dispatch on T and on U,
and the dispatch on U does polymorphic recursion again,
and dispatches to str on int * double, in this case the same
polymorphic instance of the virtual function: we end up with three
dispatches to the concrete str functions for int, double, and string,
joined by two dispatches to the tuple specialisation.

So the problem I am having seems to be like this: a function
calls str which makes a str clone child specialised on some
type which in turn calls ITSELF, as a child, via another
function (such as repr calling str) and the virtual function
dispatch is ignored when forming the closure.

This really complicated! It shouldn't happen. It never happened
before. In principle, the compiler has to distinguish between
a dispatch to self via the same type arguments (actual recursion)
and dispatch through a distinct type, which is a call to the original
function, not the clone, and isn't recursive at all (if it is, you have
an infinite regress on instantiations which Felix can't handle)


Its very important this be exactly right because attempting to
inline a recursive function will lead to an infinite unrolling,
so we have to stop it. On the other hand dispatching to an instance
on a distinct type can only cause an infinite unrolling if we screwed
up (by definition!). A trivial example is dispatch

        A -> B dispatches to function on type B -> A
        .. .which dispatches to A->B again .. infinite expansion

Felix can't detect this oscillation (Haskell can). Its easy to write:

        fun str[A,B] (x:A, y:B) => str (y,x)

Here str calls itself with different type arguments so the call is not
recursive, but polymorphic recursive. After one expansion, the call
becomes recursive.

Anyhow .. if you're now lost .. me too!

--
john skaller
skal...@users.sourceforge.net
http://felix-lang.org




------------------------------------------------------------------------------
Live Security Virtual Conference
Exclusive live event will cover all the ways today's security and 
threat landscape has changed and how IT managers can respond. Discussions 
will include endpoint security, mobile security and the latest in malware 
threats. http://www.accelacomm.com/jaw/sfrnl04242012/114/50122263/
_______________________________________________
Felix-language mailing list
Felix-language@lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/felix-language

Reply via email to