openlang

Lazy record update forced unexpectedly with Strict enabled

Hey,

I can't tell whether I'm mistaken, this is a bug, or changing record-update desugaring produced a surprising consequence, but updating a lazy record field with Strict enabled seems to force it. Constructing the record doesn't force that field.

Here's a sample test:

{-# LANGUAGE Strict, OverloadedRecordDot #-}
import Prelude hiding (id)
import Debug.Trace

data Member = Member
  {id :: Int, years :: ~Int}

main = do
  let member = Member {id = 17, years = 39}
  let revised = member {years = trace "FORCED" 46}
  print revised.id

With GHC v9.12.4, “FORCED” appears. It doesn't with v9.2.8. Disabling Strict also prevents it, of course.

The approximate core looks like

case trace "FORCED" 46 of years
  __DEFAULT -> case member of
    Member id _ -> Member id years

and, as I understand it, the case-of around trace forces evaluation. I'm generally happy for Strict to make every case and let strict (sparing us countless ! bang patterns), but when the field being explicitly updated was declared lazy, this feels like a bug or an accidental design consequence.

How does that look to you?

The conversation continues 3

Follow the reasoning
  1. Permalink

    Apparently 9.6 is the first affected version, possibly related to “Full support has been added for updating records with GADTs and other existential data types (https://downloads.haskell.org/ghc/9.6.1/docs/users_guide/9.6.1-notes.html)”. Look at Note [Record Updates] in the type checker source (https://downloads.haskell.org/~ghc/9.6.7/docs/libraries/ghc-9.6.7/src/GHC.Tc.Gen.Expr.html). Its explanation is

    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'

    In other words, type checking pulls those strict lets out. My suspicion is that through 9.4 this was done while desugaring into Core, beyond Strict's reach, whereas doing it during type checking now exposes it to Strict.

    I'd call this a bug. For let revised = member { ... }, evaluating revised should reach its constructor without demanding the updated lazy fields themselves. Surprising that nobody caught this earlier.

  2. Permalink

    That seems buggy to me too. Could somebody here file a ticket? Many thanks!

  3. Permalink

    Thanks to you both for confirming! Tomorrow I'll head to the GHC issue tracker!

Other sides of the same question

  1. How can gcc produce Intel-style assembly instead of AT&T output?

    With gcc -S, the resulting assembly uses AT&T syntax, so can I request Intel syntax for the output files instead? Alternative…

    Replies 3Latest activity assembly