Simon Jakobi pushed to branch wip/sjakobi/lazy-field-anns at Glasgow Haskell Compiler / GHC Commits: 8578bb2a by Simon Jakobi at 2026-06-24T17:34:30+02:00 Add -XLazyFieldAnnotations Unbundle the prefix `~` lazy field annotation syntax from StrictData. The new LazyFieldAnnotations extension controls only whether `~` is accepted on data and GADT constructor fields; StrictData (and hence Strict) implies it but continues to own the default strictness of unannotated fields. This lets hand-written and generated code write an explicit lazy annotation without flipping the module-wide default. The validity check in checkValidDataCon now keys off LazyFieldAnnotations instead of StrictData. Implements GHC proposal 752 (https://github.com/ghc-proposals/ghc-proposals/pull/752). Closes #24455. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> - - - - - 18 changed files: - + changelog.d/lazy-field-annotations - compiler/GHC/Driver/Flags.hs - compiler/GHC/Tc/Errors/Ppr.hs - compiler/GHC/Tc/Errors/Types.hs - compiler/GHC/Tc/TyCl.hs - docs/users_guide/exts/strict.rst - libraries/ghc-internal/src/GHC/Internal/LanguageExtensions.hs - + testsuite/tests/deSugar/should_run/LazyFieldAnnotationsSemantics.hs - + testsuite/tests/deSugar/should_run/LazyFieldAnnotationsSemantics.stdout - testsuite/tests/deSugar/should_run/all.T - testsuite/tests/driver/T4437.hs - testsuite/tests/interface-stability/template-haskell-exports.stdout - + testsuite/tests/typecheck/should_compile/LazyFieldAnnotations.hs - testsuite/tests/typecheck/should_compile/all.T - testsuite/tests/typecheck/should_fail/LazyFieldsDisabled.stderr - + testsuite/tests/typecheck/should_fail/LazyFieldsDisabledStrictData.hs - + testsuite/tests/typecheck/should_fail/LazyFieldsDisabledStrictData.stderr - testsuite/tests/typecheck/should_fail/all.T Changes: ===================================== changelog.d/lazy-field-annotations ===================================== @@ -0,0 +1,12 @@ +section: language +synopsis: Add the `LazyFieldAnnotations` extension +issues: #24455 +mrs: !16246 + +description: { + The new :extension:`LazyFieldAnnotations` extension permits the prefix + ``~`` lazy field annotation on data and GADT constructor fields without + enabling :extension:`StrictData`. :extension:`StrictData` (and therefore + :extension:`Strict`) now implies :extension:`LazyFieldAnnotations` and + continues to control the default strictness of unannotated fields. +} ===================================== compiler/GHC/Driver/Flags.hs ===================================== @@ -261,6 +261,7 @@ extensionName = \case LangExt.ExplicitLevelImports -> "ExplicitLevelImports" LangExt.ImplicitStagePersistence -> "ImplicitStagePersistence" LangExt.QualifiedStrings -> "QualifiedStrings" + LangExt.LazyFieldAnnotations -> "LazyFieldAnnotations" -- | Is this extension known by any other names? For example -- -XGeneralizedNewtypeDeriving is accepted @@ -343,6 +344,7 @@ impliedXFlags , (LangExt.TemplateHaskell, On LangExt.TemplateHaskellQuotes) , (LangExt.Strict, On LangExt.StrictData) + , (LangExt.StrictData, On LangExt.LazyFieldAnnotations) -- Historically only UnboxedTuples was required for unboxed sums to work. -- To avoid breaking code, we make UnboxedTuples imply UnboxedSums. ===================================== compiler/GHC/Tc/Errors/Ppr.hs ===================================== @@ -3231,7 +3231,7 @@ instance Diagnostic TcRnMessage where TcRnHasFieldResolvedIncomplete{} -> noHints TcRnBadFieldAnnotation _ _ LazyFieldsDisabled - -> [suggestExtension LangExt.StrictData] + -> [suggestExtension LangExt.LazyFieldAnnotations] TcRnBadFieldAnnotation{} -> noHints TcRnSuperclassCycle{} ===================================== compiler/GHC/Tc/Errors/Types.hs ===================================== @@ -6355,7 +6355,7 @@ data PatSynInvalidRhsReason data BadFieldAnnotationReason where {-| A lazy data type field annotation (~) was used without enabling the - extension StrictData. + extension LazyFieldAnnotations. Test cases: LazyFieldsDisabled ===================================== compiler/GHC/Tc/TyCl.hs ===================================== @@ -5186,7 +5186,7 @@ checkValidDataCon dflags existential_ok tc con ; let check_bang :: Type -> HsSrcBang -> HsImplBang -> Int -> TcM () check_bang orig_arg_ty bang rep_bang n | HsSrcBang _ _ SrcLazy <- bang - , not (bang_opt_strict_data bang_opts) + , not (xopt LangExt.LazyFieldAnnotations dflags) = addErrTc (bad_bang n LazyFieldsDisabled) -- Warn about UNPACK without "!" ===================================== docs/users_guide/exts/strict.rst ===================================== @@ -155,6 +155,44 @@ Note the following points: for more discussion and examples. +.. _lazy-field-annotations: + +Lazy field annotations +---------------------- + +.. extension:: LazyFieldAnnotations + :shortdesc: Allow the prefix ``~`` lazy field annotation on constructor fields. + + :since: 10.2.1 + + Allow the prefix ``~`` lazy field annotation on data and GADT constructor + fields. + +``LazyFieldAnnotations`` allows a prefix ``~`` lazy annotation in every +constructor-field position where a prefix ``!`` strict annotation is accepted: +Haskell-98 prefix and record fields, and GADT argument types and record +fields. :: + + {-# LANGUAGE LazyFieldAnnotations #-} + + data A = A ~Int Bool + data B = B { b1 :: ~Int, b2 :: !Bool } + + data C where + C1 :: ~Int -> C + C2 :: { c1 :: ~Int, c2 :: !Bool } -> C + +A field annotated with ``~`` is lazy: it is not forced when the constructor is +built, but held as a thunk — the same behaviour as an unannotated field. + +The ``~`` annotation must be written in prefix form:: + + data T = MkT ~Int -- valid + data T = MkT ~ Int -- invalid + +See `GHC Proposal #229 <https://github.com/ghc-proposals/ghc-proposals/blob/master/proposals/0229-whitespace-bang-patterns.rst>`__ +for the precise rules. + .. _strict-data: Strict-by-default data types @@ -163,6 +201,8 @@ Strict-by-default data types .. extension:: StrictData :shortdesc: Treat datatype fields as strict by default. + :implies: :extension:`LazyFieldAnnotations` + :since: 8.0.1 Make fields of data types defined in the current module strict by default. @@ -183,13 +223,8 @@ we interpret it as if they had written :: The extension only affects definitions in this module. -The ``~`` annotation must be written in prefix form:: - - data T = MkT ~Int -- valid - data T = MkT ~ Int -- invalid - -See `GHC Proposal #229 <https://github.com/ghc-proposals/ghc-proposals/blob/master/proposals/0229-whitespace-bang-patterns.rst>`__ -for the precise rules. +The ``~`` lazy field annotation is provided by :extension:`LazyFieldAnnotations` +(which ``StrictData`` implies); see :ref:`lazy-field-annotations`. .. _strict: ===================================== libraries/ghc-internal/src/GHC/Internal/LanguageExtensions.hs ===================================== @@ -169,6 +169,7 @@ data Extension | ExplicitLevelImports | ImplicitStagePersistence | QualifiedStrings + | LazyFieldAnnotations deriving (Eq, Enum, Show, Generic, Bounded) -- 'Ord' and 'Bounded' are provided for GHC API users (see discussions -- in https://gitlab.haskell.org/ghc/ghc/merge_requests/2707 and ===================================== testsuite/tests/deSugar/should_run/LazyFieldAnnotationsSemantics.hs ===================================== @@ -0,0 +1,24 @@ +{-# LANGUAGE LazyFieldAnnotations #-} + +-- | LazyFieldAnnotations does not change the default strictness of fields. +-- Without StrictData an unannotated field stays lazy and a ~ field is lazy +-- too; a ! field is still strict. +module Main where + +import Control.Exception (try, evaluate, SomeException) + +data T = Plain Int -- unannotated: lazy + | Lazy ~Int -- explicit ~: lazy + | Strict !Int -- explicit !: strict + +-- | Does building the constructor force the field? +forcesField :: T -> IO Bool +forcesField x = do + r <- try (evaluate x) :: IO (Either SomeException T) + pure (either (const True) (const False) r) + +main :: IO () +main = do + print =<< forcesField (Plain undefined) -- False: lazy + print =<< forcesField (Lazy undefined) -- False: lazy + print =<< forcesField (Strict undefined) -- True: strict ===================================== testsuite/tests/deSugar/should_run/LazyFieldAnnotationsSemantics.stdout ===================================== @@ -0,0 +1,3 @@ +False +False +True ===================================== testsuite/tests/deSugar/should_run/all.T ===================================== @@ -76,3 +76,5 @@ test('T19680A', normal, compile_and_run, ['']) test('T20024', exit_code(1), compile_and_run, ['']) test('Or5', exit_code(1), compile_and_run, ['']) + +test('LazyFieldAnnotationsSemantics', normal, compile_and_run, ['']) ===================================== testsuite/tests/driver/T4437.hs ===================================== @@ -36,7 +36,7 @@ check title expected got -- See Note [Adding a language extension] in compiler/GHC/Driver/Session.hs. expectedGhcOnlyExtensions :: [String] -expectedGhcOnlyExtensions = [ "QualifiedStrings", "Modifiers" ] +expectedGhcOnlyExtensions = [ "QualifiedStrings", "Modifiers", "LazyFieldAnnotations" ] expectedCabalOnlyExtensions :: [String] expectedCabalOnlyExtensions = ["Generics", ===================================== testsuite/tests/interface-stability/template-haskell-exports.stdout ===================================== @@ -269,6 +269,7 @@ module Language.Haskell.TH where | ExplicitLevelImports | ImplicitStagePersistence | QualifiedStrings + | LazyFieldAnnotations type FamilyResultSig :: * data FamilyResultSig = NoSig | KindSig Kind | TyVarSig (TyVarBndr ()) type FamilyResultSigQ :: * @@ -880,6 +881,7 @@ module Language.Haskell.TH.LanguageExtensions where | ExplicitLevelImports | ImplicitStagePersistence | QualifiedStrings + | LazyFieldAnnotations module Language.Haskell.TH.Lib where -- Safety: Safe @@ -1625,6 +1627,7 @@ module Language.Haskell.TH.Syntax where | ExplicitLevelImports | ImplicitStagePersistence | QualifiedStrings + | LazyFieldAnnotations type FamilyResultSig :: * data FamilyResultSig = NoSig | KindSig Kind | TyVarSig (TyVarBndr ()) type FieldExp :: * ===================================== testsuite/tests/typecheck/should_compile/LazyFieldAnnotations.hs ===================================== @@ -0,0 +1,15 @@ +{-# LANGUAGE LazyFieldAnnotations #-} +{-# LANGUAGE GADTs #-} + +-- | LazyFieldAnnotations alone accepts the prefix ~ annotation in every +-- constructor-field position, without enabling StrictData. +module LazyFieldAnnotations where + +-- Haskell-98 prefix and record fields +data A = A ~Int Bool +data B = B { b1 :: ~Int, b2 :: !Bool } + +-- GADT argument types and record fields +data C where + C1 :: ~Int -> C + C2 :: { c1 :: ~Int, c2 :: !Bool } -> C ===================================== testsuite/tests/typecheck/should_compile/all.T ===================================== @@ -967,4 +967,5 @@ test('T26805a', normal, compile, ['']) test('T24464', normal, compile, ['']) test('ExpansionQLIm', normal, compile, ['']) test('T23135', normal, compile, ['']) +test('LazyFieldAnnotations', normal, compile, ['']) ===================================== testsuite/tests/typecheck/should_fail/LazyFieldsDisabled.stderr ===================================== @@ -1,8 +1,8 @@ - LazyFieldsDisabled.hs:3:10: error: [GHC-81601] • Lazy field annotations (~) are disabled on the first argument of ‘A’ • In the definition of data constructor ‘A’ In the data type declaration for ‘A’ Suggested fix: - Perhaps you intended to use the ‘StrictData’ extension (implied by ‘Strict’) + Perhaps you intended to use the ‘LazyFieldAnnotations’ extension (implied by ‘StrictData’) + ===================================== testsuite/tests/typecheck/should_fail/LazyFieldsDisabledStrictData.hs ===================================== @@ -0,0 +1,8 @@ +{-# LANGUAGE StrictData #-} +{-# LANGUAGE NoLazyFieldAnnotations #-} + +-- | A later NoLazyFieldAnnotations overrides the implication from StrictData, +-- so ~ is rejected even though unannotated fields remain strict. +module LazyFieldsDisabledStrictData where + +data A = A ~Int ===================================== testsuite/tests/typecheck/should_fail/LazyFieldsDisabledStrictData.stderr ===================================== @@ -0,0 +1,8 @@ +LazyFieldsDisabledStrictData.hs:8:10: error: [GHC-81601] + • Lazy field annotations (~) are disabled + on the first argument of ‘A’ + • In the definition of data constructor ‘A’ + In the data type declaration for ‘A’ + Suggested fix: + Perhaps you intended to use the ‘LazyFieldAnnotations’ extension (implied by ‘StrictData’) + ===================================== testsuite/tests/typecheck/should_fail/all.T ===================================== @@ -690,6 +690,7 @@ test('T21444', normal, compile_fail, ['']) test('T23308', normal, compile_fail, ['']) test('MultiAssocDefaults', normal, compile_fail, ['']) test('LazyFieldsDisabled', normal, compile_fail, ['']) +test('LazyFieldsDisabledStrictData', normal, compile_fail, ['']) test('TyfamsDisabled', normal, compile_fail, ['']) test('CommonFieldResultTypeMismatch', normal, compile_fail, ['']) test('CommonFieldTypeMismatch', normal, compile_fail, ['']) View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/8578bb2a77130e48e6c43c29e2c829f5... -- View it on GitLab: https://gitlab.haskell.org/ghc/ghc/-/commit/8578bb2a77130e48e6c43c29e2c829f5... You're receiving this email because of your account on gitlab.haskell.org.