hermit-0.1.1.0: examples/fib-tuple/blog.txt
Using this file to keep track of progress/improvements coming out of this example.
Format: steps from Neil's worker/wrapper derivation with interspersed commentary.
Entire derivation is:
-- Normal fib definition
fib :: Nat -> Nat
fib = body
-- After WWSplit tactic
fib = wrap work
f :: (Nat -> Nat) -> Nat -> Nat
f = \fib -> body
wrap :: (Nat -> (Nat, Nat)) -> (Nat -> Nat)
unwrap :: (Nat -> Nat) -> (Nat -> (Nat, Nat))
work :: Nat -> (Nat, Nat)
-- Derivation
work = unwrap (f (wrap work))
{- extensionality -}
work n = unwrap (f (wrap work)) n
{- unfold 'unwrap -}
work n = (f (wrap work) n, f (wrap work) (n+1))
{- case 'n -}
work 0 = (f (wrap work) 0, f (wrap work) 1)
work (n+1) = (f (wrap work) (n+1), f (wrap work) (n+2))
{- unfold 'f -}
work 0 = (0, 1)
work (n+1) = (f (wrap work) (n+1), wrap work (n+1) + wrap work n)
{- unfold 'work -}
work 0 = (0, 1)
work (n+1) = (f (wrap work) (n+1), wrap (unwrap (f (wrap work))) (n+1) + wrap (unwrap (f (wrap work))) n)
{- wrap . unwrap = id (precondition) -}
work 0 = (0, 1)
work (n+1) = (f (wrap work) (n+1), f (wrap work) (n+1) + f (wrap work) n)
{- let-intro x2, let-float-tuple -}
work 0 = (0, 1)
work (n+1) = let (x,y) = (f (wrap work) n, f (wrap work) (n+1)) in (y,x+y)
{- fold 'unwrap -}
work 0 = (0, 1)
work (n+1) = let (x,y) = unwrap (f (wrap work)) n in (y,x+y)
{- fold 'work -}
work 0 = (0, 1)
work (n+1) = let (x,y) = work n in (y,x+y)
{- QED -}
Discussion:
###############################################################################
work = unwrap (f (wrap work))
To get to this point required creation of the WWSplit tactic. This
is slightly more complicated than how we did the split in the reverse
example in the paper, because we want to keep f abstract for most
of this derivation (we selectively unfold it once). I was able to
create a tactic which I believe is entirely general, allowing one
to rewrite:
letrec g = body
to:
let g = (let f = \g -> body
in (letrec work = unwrap (f (wrap work))
in wrap work))
There are a couple todo's regarding this split. It relies on
two GHC RULES pragmas (ww, and inline-fix). These should probably
be written as rewrites. Doing so would probably allow the whole
tactic to be expressed as a rewrite, rather than a hermit script.
###############################################################################
{- extensionality -}
work n = unwrap (f (wrap work)) n
This was done with "eta-expand 'n". Allowing core lint to run revealed
that eta-expand was not handling types correctly. This is fixed in commit
b49fcd2be2ff0592ab51a2e9b8a291dff48bbf4b by using splitFunTy_maybe, which
looks to be a really handy function. Must remember to exit Hermit with
resume, instead of abort, so core-lint runs.
###############################################################################
{- unfold 'unwrap -}
work n = (f (wrap work) n, f (wrap work) (n+1))
At the moment we have to be careful to call unfold at the right place. Otherwise
we end up with extra redexes that must be dealt with.
###############################################################################
{- case 'n -}
work 0 = (f (wrap work) 0, f (wrap work) 1)
work (n+1) = (f (wrap work) (n+1), f (wrap work) (n+2))
This step required two new capabilites:
* A case-split rewrite that accepts the name of a free variable of an
expression, finds all the data constructors belonging to the type,
and builds a case statement, cloning the expression for each case alt.
Getting the correct types of the pattern binders was tricky (or simple
once I found the correct GHC functions to instantiate the constructor
argument types).
* Modify the Hermit context to store two possible values for variables
that are case wildcard binders. These can refer to either the scrutinee
(previous behavior), or the constructor application of the pattern. This
is what allows the 'n above to be replaced by 0 and 1 when case splitting.
It is important to store the AltCon and binders in the context, and only
convert to a CoreExpr upon inlining, so that we can apply the constructor
at the correct types.
There is a case-split-inline rewrite that combines these steps. This step
also required rewriting fib to work over:
data Nat = Zero | Succ Nat
rather than Int, as the only Int constructor is the one which boxes Int#. We
probably need an case-literal rewrite, which accepts a literal and adds a
LitCon pattern. We could then go back to working on Int, and "case-literal 0"
rather than "case-split-inline 'n".
###############################################################################
{- unfold 'f -}
work 0 = (0, 1)
work (n+1) = (f (wrap work) (n+1), wrap work (n+1) + wrap work n)
This step is done by calling unfold and repeatedly applying case-reduce on
three of the four tupled expressions. Should we add case-reduce to the
unfold cleanup step? Is there a more general cleanup rewrite we can build
that isn't as invasive as bash, but handles this stuff for us? I can imagine
some rewrites (like case-reduce) that we simply always want to run, as they
are effectively dead code elimination.
###############################################################################
{- unfold 'work -}
work 0 = (0, 1)
work (n+1) = (f (wrap work) (n+1), wrap (unwrap (f (wrap work))) (n+1) + wrap (unwrap (f (wrap work))) n)
Our first run-in with equational reasoning issues. The problem is that we
want to unfold the definition of work we started with, not the one we currently
have. We are experimenting with two approaches to this:
1. Create a GHC rule on the fly, stash it in the ModGuts, and then
use unfold-rule. There are (currently) a few problems with this:
* Only top-level bindings consistently work as rules. This can probably
be solved by tracking down the occasional kernel panic. See my email
on July 12th about the issue.
* The add-rule rewrite works on ModGuts (by necessity), so you must be
at the top level to use it. This is problematic when inside scoping
braces, which prevent you from going up by design.
2. Stash the definition in the HermitMonad (currently) or alongside the
AST in the kernel. This is the method I'm currently using in the
derivation. It appears to work quite well, and gives us control
over how its unfolded. We can reject unfoldings that would result
in variables that are not defined (because a binding is no longer
present), for instance. TODO: prevent unintended capture!
The two rewrites added for #2 are:
stashDef :: String -> TranslateH CoreDef () -- stashes the RHS of binding, with a name
-- stash-defn
stashApply :: String -> RewriteH CoreExpr -- like inline, but looks up a stashed name
-- stash-apply
###############################################################################
{- wrap . unwrap = id (precondition) -}
work 0 = (0, 1)
work (n+1) = (f (wrap work) (n+1), f (wrap work) (n+1) + f (wrap work) n)
I'm currently using a GHC rule for this:
{-# RULES precondition forall w . wrap (unwrap w) = w #-}
###############################################################################
{- let-intro x2, let-float -}
work 0 = (0, 1)
work (n+1) = let (x,y) = (f (wrap work) n, f (wrap work) (n+1)) in (y,x+y)
{- fold 'unwrap -}
work 0 = (0, 1)
work (n+1) = let (x,y) = unwrap (f (wrap work)) n in (y,x+y)
{- fold 'work -}
work 0 = (0, 1)
work (n+1) = let (x,y) = work n in (y,x+y)
{- QED -}