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
- - - - -
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-wh...`__
+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-wh...`__
-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.