#1171: GHC doesn't respect the imprecise exceptions semantics
----------------------+-----------------------------------------------------
Reporter: neil | Owner:
Type: bug | Status: reopened
Priority: low | Milestone: _|_
Component: Compiler | Version: 6.6
Severity: normal | Resolution:
Keywords: | Difficulty: Unknown
Testcase: cg059 | Architecture: Multiple
Os: Multiple |
----------------------+-----------------------------------------------------
Comment (by igloo):
The below is from e-mail and IRC conversations that happened in parallel
with this bug report.
I've editted things slightly to make them flow better, but hopefully
haven't changed any important meanings.
----
__I said:``__
In http://research.microsoft.com/~simonpj/Papers/imprecise-exn.htm
I think you claim in section 4.3, in the rationale for the Bad case,
that if we have
{{{
f x = case x of
True -> error "A"
False -> error "B"
}}}
then the call
{{{
f (error "X")
}}}
is allowed to raise B, as this permits transformations like
{{{
f' x = let b = error "B"
in b `seq` case x of
True -> error "A"
False -> b
}}}
which in turn is justified because the case expression is strict in
{{{
error "B"
}}}
(as well as every other expression).
However, the Ok case tells me that if I call
{{{
f True
}}}
then I can get the errors raised by the
{{{
True -> error "A"
}}}
branch only. Thus it must raise A. But with the above transformation f'
raises B.
I also think that this behaviour is very confusing to users; it makes
sense that
{{{
error "A" + error "B"
}}}
needs to evaluate both values, so throwing either exception is
reasonable, but in
{{{
f True
}}}
it is "obvious" that A is raised and B is not!
Traditionally, we would say that e is strict in x if
{{{
x = _|_ => e = _|_
}}}
However, with the set-based imprecise exceptions, in which we
distinguish between different bottoms, it seems to me that a better
definition would be that e is strict in x if
{{{
x = Bad xs => e = Bad ys and xs \subseteq ys
}}}
Thus, for example, a case can throw an exception if either the scrutinee
can or /all/ the branches can, i.e. in the Bad case in 4.3 we take the
big intersection rather than big union.
So we wouldn't be allowed to pull
{{{
error "B"
}}}
out of the above case, but we would still be able to translate
{{{
case x of
True -> y
False -> y
}}}
into
{{{
y `seq` case x of
True -> y
False -> y
}}}
I am also unconvinced by a non-terminating program being allowed to
throw anything it likes. It seems much nicer to permit it only to either
not terminate or to throw a special exception, here-on written N.
I haven't written a denotational semantics or anything, so perhaps this
would all unravel if I tried, but here are some example definitions
followed by what exceptions I think various expressions ought to be able
to throw; are there any obvious nasty corners I have left unexplored?:
{{{
f x = case x of
True -> error "A"
False -> error "B"
g x = case x of
True -> error "C"
False -> error "C"
h () () = ()
i = i
j = error "D" + j
-----
f True A
f (error "E") E
g True C
g (error "F") C or F
h (error "G") (error "H") G or H
i N or non-termination
j D, N or non-termination
}}}
I also haven't looked into the performance implications, although
personally I'd prefer a semantics that is more intuitive along with a
few more bang patterns sprinkled around.
----
__Simon PJ replied:``__
I think you are basically right here. Another way to say it is this. In
4.5 we
claim that GHC's transformations can reduce the set of possible exceptions
from
a term, but not increase it. But the (current) strictness analysis
transformation increases it. I agree that is undesirable.
As I say on the Trac, we could change this at the cost of making fewer
functions
strict. I can tell anyone how to do this, if you want. It would be good
to
measure the performance impact of doing so.
----
__Simon M__ has a memory that there is some problem, possibly related to
monotonicity, with only allowing non-terminating programs to either not
terminate or throw a special non-termination error, rather than allowing
them to behave like any bottom they wish as the imprecise exceptions paper
allows them to. However, he can't remember what the problem actually is;
if anyone can then it would be good to have it documented.
----
Regarding __Simon PJ__``'s earlier comment
{{{
It's easily stopped, too, by stopping GHC treating error like bottom;
but that would make many functions less strict. It'd be interesting to
measure the performance impact of this.
}}}
__Simon M replied:``__
It would be a shame if error wasn't treated as _|_, because that would
lose the opportunity to transform
{{{
Case error "foo" of alts ===> error "foo"
}}}
wouldn't it? This is only dead code elimination of course.
and __Simon PJ followed up with:``__
You are right that the strictness of an Id would have to distinguish:
* normal
* diverges (bottom)
* crashes (calls error)
Currently it does not. Changing the strictness analyser to make 'error'
look like 'normal' rather than 'diverges' would indeed have the effect of
making GHC not realise that (error "x") was a case scrutinee that could
be simplified.
So yes, there is a bit more to testing the effect of making 'error' less
strict than I was suggesting. Quite doable, but more than a moment's
work.
----
Regarding my strictness test
{{{
x = Bad xs => e = Bad ys and xs \subseteq ys
}}}
__Simon M__ suggested that an implementation might:
invent a magic exception X only used by the strictness analyser, and treat
this as
{{{
x = Bad X => e = Bad ys and X `elem` ys
}}}
All other exceptions (and _|_) can be mapped to the same thing; all that
matters is
whether x's exception was raised or not. I imagine you'd need to fiddle
around
with the strictness analyser's domain.
----
The sort of case where Simon PJ is worried we will lose performance is
{{{
f x [] = error "Can't happen"
f x (y:ys) = ... strict in x ...
}}}
where we would no longer be allowed to claim to be strict in x, as
{{{
f (error "X") []
}}}
should only throw "Can't happen". Debatably it's better to tell the
compiler explicitly that you want it to be strict in x with
{{{
f !x [] = error "Can't happen"
f !x (y:ys) = ... strict in x ...
}}}
anyway, rather than relying on the strictness analyser to figure it out.
----
Finally, I am not entirely convinced by the semantics of imprecise
exceptions as given in the paper; they tend allow too many exceptions to
be thrown, possibly under the assumption no compiler would actually
transform a program in such a way that
the unexpected exceptions actually would be thrown.
For example
{{{
(error "A") (error "B")
}}}
is permitted to throw B, even though
{{{
error "B"
}}}
would never be evaluated under non-strict evaluation.
Similarly,
{{{
case error "A" of
True -> error "B"
False -> 'x'
}}}
is allowed to throw B.
Incidentally, I do think it is reasonable for
{{{
case error "A" of
True -> error "B"
False -> error "B"
}}}
to throw B.
--
Ticket URL: <http://hackage.haskell.org/trac/ghc/ticket/1171>
GHC <http://www.haskell.org/ghc/>
The Glasgow Haskell Compiler_______________________________________________
Glasgow-haskell-bugs mailing list
[email protected]
http://www.haskell.org/mailman/listinfo/glasgow-haskell-bugs