Note [Casting slot arguments]
Consider this function which selects between Int32# and Int64# from a unboxed sum.
foo :: (# Int32# | Int64# #) -> FD
foo x = case x of
(# x1 | #) -> F x1
(# | x2 #) -> D x2
Naturally we would expect x1 to have a PrimRep of Int32Rep and x2 of DoubleRep.
However we used to generate this (bogus) code after Unarise giving rise to #22208:
M.foo :: (# GHC.Prim.Int32# | GHC.Prim.Int64# #) -> M.FD
[GblId, Arity=1, Unf=OtherCon []] =
{} \r [sum_tag sum_field]
case sum_tag of tag_gsc {
__DEFAULT -> M.F [sum_field];
2# -> M.D [sum_field];
};
Where sum_field is used both as Int32# and Int64# depending on the branch
because they share the same SlotTy.
This usually works out since we put all int's in the same sort of register.
So even if the reps where wrong (x :: bits32) = (y :: bits64) would produce
correct code in the most cases.
However there are cases where this goes wrong, causing lint errors,in the case of #22208
compiler panics or in some cases incorrect results in the C backend.
For now our solution is to construct proper casts between the PrimRep of the slot and
the variables we want to store in, or read out of these slots.
This means when we have a sum (# Int32# | Int64# #) if we want to store a Int32
we convert it to a Int64 on construction of the tuple value, and convert it back
to a Int32 once when want to use the field. On most backends these coversions should
be no-ops at runtime so this seems reasonable.
Conversion for values coming out of a strict field happen in mapSumIdBinders. While
conversion during the construction of sums happen inside mkUbxSum.
A full example of casting during sum construction ----------------
To compile a constructor application of a unboxed sum of type (# Int32# | Int64# )
in an expression like `let sum = (# x | #)` we will call mkUbxSum to determine
which binders we have to replace sum with at use sites during unarise.
See also Note [Translating unboxed sums to unboxed tuples].
Int32# and Int64# in this case will share the same slot in the unboxed sum. This means
the sum after unarise will be represented by two binders. One for the tag and one for
the field. The later having Int64Rep.
However our input for the field is of Int32Rep. So in order to soundly construct
`(# x | #) :: (# Int32# | Int64# )` we must upcast `x` to Int64#.
To do this mkUbxSum will produce an expression with a hole for constructor application
to go into. That is the call to mkUbxSum and it's result will look something like:
>>> mkUbxSum (#|#) [Int32#, Int64#] (x::Int32#) us (x')
([1#::Int#, x'::Int64#], \rhs -> case int32ToInt# x of x' -> rhs )
We will use the returned arguments to construct an application to an unboxed tuple:
>>> mkTuple [tag::Int#, x'::Int64#]
(# tag, x' #)
Which we will then use as the rhs to pass into the casting wrapper to
construct an expression that casts `x` to the right type before constructing the
tuple
>>> (\rhs -> case int32ToInt# x of x' -> rhs ) (# tag, x' #)
case int32ToInt# x of x' -> (# #) 1# x'
Which results in the this definition for `sum` after all is said and done:
let sum = case int32ToInt# x of { x' -> (# #) 1# x' }
Not that the renaming is not optional. Cmm requires binders of different uniques
to have at least different types. See Note [CorePrep Overview]: 6. Clone all local Ids
A full example of casting during sum matching --------------------
When matching on an unboxed sum constructor we start out with
something like this the pre-unarise:
f :: (# Int32 | Int64# ) -> ...
f sum = case sum of
(# x |#) -> alt_rhs
...
We unarise the function arguments and get:
f sum_tag sum_slot1 = case sum_tag of
1# -> ???
Now we need to match up the original alternative binders with the sum slots passed
to the function. This is done by mapSumIdBinders which we we call for our
example alternative like this:
>>> mapSumIdBinders [x] [sum_slot1] alt_rhs env
(env', alt_rhs')
mapSumIdBinders first matches up the list of binders with the slots passed to
the function which is trivial in this case. Then we check if the slot and the
variable residing inside it agree on their Rep. If alternative binders and
the function arguments agree in their slot reps we we just extend the environment
with a mapping from `x` to `sum_slot1` and we return the rhs as is.
If the reps of the sum_slots do not agree with alternative binders they represent
then we need to wrap the whole RHS in nested cases which cast the sum_slot<n>
variables to the correct rep. Here `x` is of Int32Rep while `sum_slot1` will be
of Int64Rep. This means instead of retuning the original alt_rhs we will return:
>>> mapSumIdBinders [x] [sum_slot1] alt_rhs env
( env'[x=x']
, case int64ToInt32# (sum_slot1 :: Int64#) of
(x' :: Int32#) -> alt_rhs
)
We then run unarise on alt_rhs within that expression, which will replace the first occurrence
of `x` with sum_slot_arg_1 giving us post-unarise:
f sum_tag sum_slot1 =
case sum_tag of
1# -> case int64ToInt32# sum_slot1 of
x' -> ... x' ...
... References 2
- CorePrep Overview GHC.CoreToStg.Prep
- Translating unboxed sums to unboxed tuples GHC.Stg.Unarise
Referenced by 2
- GHC.Stg.Unarise call site
- GHC.Types.RepType call site