[Haskell-cafe] Debugging partial functions by the rules
Donald Bruce Stewart
dons at cse.unsw.edu.au
Tue Nov 14 23:54:31 EST 2006
So all this talk of locating head [] and fromJust failures got me
thinking:
Couldn't we just use rewrite rules to rewrite *transparently*
all uses of fromJust to safeFromJust, tagging the call site
with a location?
To work this requires a few things to go right:
* a rewrite rule
* assertions
* and rewrite rules firing before assertions are expanded
Let's try this. Consider the program:
1 import qualified Data.Map as M
2 import Data.Maybe
3
4 main = print f
5
6 f = let m = M.fromList
7 [(1,"1")
8 ,(2,"2")
9 ,(3,"3")]
10 s = M.lookup 4 m
11 in fromJust s
When we run it we get the not so useful error:
$ ./A
A: Maybe.fromJust: Nothing
Ok, so we have a few tricks for locating this, using LocH
(http://www.cse.unsw.edu.au/~dons/loch.html), we can catch an
assertion failure, but we have to insert the assertion by hand:
1 import Debug.Trace.Location
2 import qualified Data.Map as M
3 import Data.Maybe
4
5 main = do print f
6
7 f = let m = M.fromList
8 [(1,"1")
9 ,(2,"2")
10 ,(3,"3")]
11 s = M.lookup 4 m
12 in safeFromJust assert s
13
14 safeFromJust a = check a . fromJust
Which correctly identifies the call site:
$ ./A
A: A.hs:12:20-25: Maybe.fromJust: Nothing
Now, this approach is a little fragile. 'assert' is only respected by GHC if -O
is *not* on, so if we happened to try this trick with -O, we'd get:
$ ./A
A: Debug.Trace.Location.failure
So lesson one: you have to do the bug hunting with -Onot.
Currently there's -fignore-asserts for turning off assertions, but no flag for
turning them on with -O, Simon, could this be fixed? Could we get a
-frespect-asserts that works even with -O ?
Ok, assuming this assert trick is used, can we get the compiler to insert the
asserts for us? If so, this would be a great advantage, you'd just be able to
switch on a flag, or import a debugging module, and your fromJusts would be
transparently rewritten. With rewrite rules we do just this!
So, to our initial unsafe use of fromJust, we add a rewrite rule:
--
-- rewrite fromJust to a located version, and hope that GHC expands
-- 'assert' after the rule fires..
--
{-# RULES
"located fromJust" fromJust = check assert . myFromJust
#-}
This just tells the compiler to replace every occurence of fromJust with a
assertion-throwing fromJust, should it fail. We have to use myFromJust here, to
avoid rule recursion.
--
-- Inlined to avoid recursion in the rule:
--
myFromJust :: Maybe a -> a
myFromJust Nothing = error "Maybe.fromJust: Nothing" -- yuck
myFromJust (Just x) = x
Ok, so can we get ghc to rewrite fromJust to the safe fromJust magicaly?
$ ghc --make -Onot A.hs -fglasgow-exts -ddump-simpl-stats
[1 of 1] Compiling Main ( A.hs, A.o )
1 RuleFired
1 located fromJust
Linking A ...
Yes, the rule fired! GHC *did* rewrite our fromJust to a more useful fromJust.
Running it:
$ ./A
A: A.hs:19:36-41: Maybe.fromJust: Nothing
Looks good! But that is deceiving: the assert was expanded before the rule
fired, and refers to the rewrite rule source line (line 19), not the fromJust
call site (line 12). Now if we could just have the 'assert' token inserted
into the AST before it was expanded, we'd be home and dry. Could this be done
with TH? Or could we arrange for asserts in rewrite rules not to be expanded
till later?
Note that this is still a useful technique, we can rewrite head/fromJust/... to
some other possibly more useful message. And if we can constrain the rule to fire
in only particular modules, we may be able to narrow down the bug, just by
turning on a rule. For example, adding:
{-# RULES
"located fromJust" fromJust = safeFromJust
#-}
safeFromJust s = case s of
Nothing -> "safeFromJust: failed with Nothing. Ouch"
Just x -> x
will produce:
$ ./A
"safeFromJust: failed with Nothing. Ouch"
So rewrite rules can be used to transparently alter uses of partial functions
like head and fromJust.
So, further work:
* have 'assert' respected when -O is on
* think up a technique for splicing in 'assert' via rewrite rules (or TH
...) such that the src locations are expanded after the rewrite, and
correctly reflect the location of the splice point.
Any ideas?
-- Don
More information about the Haskell-Cafe
mailing list