I’m not sure if it’s me, a bug or just an unexpected outcome when record updating desugaring was changed, but I’m seeing a lazy record field being forced when updating it under the Strict extension. It’s not forced when constructing the record.
For example, given this test:
{-# LANGUAGE Strict, OverloadedRecordDot #-}
import Prelude hiding (id)
import Debug.Trace
data Person = Person {id :: Int, age :: ~Int}
main = do
let person = Person {id = 13, age = 37}
let updated = person {age = trace "FORCED" 42}
print updated.id
GHC v9.12.4 prints out “FORCED”. v9.2.8 doesn’t. Naturally, without Strict it doesn’t either.
The core output is roughly
case trace "FORCED" 42 of age
__DEFAULT -> case person of
Person id _ -> Person id age
with the case-of of trace forcing it if I understand correctly. I’m fine with Strict in general causing all cases and lets to be strict (saving us from typing a million ! bang patterns), but if we’re explicitly updating a record field set to be lazy, it sounds like either a bug or an unintended design result.
To typecheck a record update, we desugar it first. Suppose we have
data T p q = T1 { x :: Int, y :: Bool, z :: Char }
| T2 { v :: Char }
| T3 { x :: Int }
| T4 { p :: Float, y :: Bool, x :: Int }
| T5
Then the record update `e { x=e1, y=e2 }` desugars as follows
e { x=e1, y=e2 }
===>
let { x' = e1; y' = e2 } in
case e of
T1 _ _ z -> T1 x' y' z
T4 p _ _ -> T4 p y' x'
That is, strict lets are pulled out during type checking. I believe what happened is that in 9.4 and earlier this transformation happened when desugaring to Core, where Strict couldn’t get at it, now it happens in type checking, where Strict can get at it.
I think this is a bug. In let updated = person { ... }, updated should be evaluated to its constructor, which doesn’t require evaluating the new lazy fields in the record update itself. I’m surprised this hasn’t been noticed before.