summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorTom Smeding <tom@tomsmeding.com>2026-07-01 23:48:53 +0200
committerTom Smeding <tom@tomsmeding.com>2026-07-01 23:48:53 +0200
commit75ba6aeef88df2f051b73aa916fb66ce4c6b8a14 (patch)
treeb785610b666a0faa518d292f658fce60e2400327 /src
Initial
Diffstat (limited to 'src')
-rw-r--r--src/InitOnce.hs18
-rw-r--r--src/InitOnce/TheMap.hs32
-rw-r--r--src/InitOnce/Types.hs30
3 files changed, 80 insertions, 0 deletions
diff --git a/src/InitOnce.hs b/src/InitOnce.hs
new file mode 100644
index 0000000..60ea435
--- /dev/null
+++ b/src/InitOnce.hs
@@ -0,0 +1,18 @@
+{-# LANGUAGE TemplateHaskellQuotes #-}
+module InitOnce (once) where
+
+import Language.Haskell.TH (bindCode, CodeQ)
+import Type.Reflection (Typeable, typeOf)
+
+import InitOnce.TheMap (onceInsert)
+import InitOnce.Types (Location, location', TypedAny(..), getTypedAny)
+
+
+-- | The IO action is executed with asynchronous exceptions masked (using
+-- 'Control.Concurrent.MVar.modifyMVarMasked') to ensure that initialisation
+-- really happens once.
+once :: Typeable a => CodeQ (IO a) -> CodeQ a
+once code = bindCode location' $ \loc -> [|| typedOnceInsert loc $$(code) ||]
+
+typedOnceInsert :: Typeable a => Location -> IO a -> a
+typedOnceInsert loc act = getTypedAny (onceInsert loc ((\x -> TypedAny (typeOf x) x) <$> act))
diff --git a/src/InitOnce/TheMap.hs b/src/InitOnce/TheMap.hs
new file mode 100644
index 0000000..5c30794
--- /dev/null
+++ b/src/InitOnce/TheMap.hs
@@ -0,0 +1,32 @@
+-- | This is the only module that uses unsafePerformIO; to make sure it's safe,
+-- we disable all the known optimisations in GHC that can potentially mess
+-- things up: <https://discourse.haskell.org/t/can-noinline-fail-to-prevent-inlining/14324>
+-- Also, since there is nothing else in this module, there isn't really very
+-- much for GHC /to/ mess up, which is the point.
+{-# OPTIONS -fno-cse -fno-full-laziness -fno-state-hack #-}
+module InitOnce.TheMap (onceInsert) where
+
+import Control.Concurrent.MVar (modifyMVarMasked, newMVar, MVar)
+import Data.Map.Strict (Map)
+import qualified Data.Map.Strict as Map (empty, lookup, insert)
+import System.IO.Unsafe (unsafePerformIO)
+
+import InitOnce.Types (Location, TypedAny)
+
+
+{-# NOINLINE theMap #-}
+theMap :: MVar (Map Location TypedAny)
+theMap = unsafePerformIO (newMVar Map.empty)
+
+-- | The IO action is executed with asynchronous exceptions masked (using
+-- 'modifyMVarMasked').
+{-# NOINLINE onceInsert #-}
+onceInsert :: Location -> IO TypedAny -> TypedAny
+onceInsert key mkVal =
+ unsafePerformIO $
+ modifyMVarMasked theMap $ \mp ->
+ case Map.lookup key mp of
+ Nothing -> do
+ v <- mkVal
+ return (Map.insert key v mp, v)
+ Just v -> return (mp, v)
diff --git a/src/InitOnce/Types.hs b/src/InitOnce/Types.hs
new file mode 100644
index 0000000..05d5c9c
--- /dev/null
+++ b/src/InitOnce/Types.hs
@@ -0,0 +1,30 @@
+{-# LANGUAGE DeriveLift #-}
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE PatternSynonyms #-}
+{-# LANGUAGE TypeApplications #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+module InitOnce.Types (Location, location', TypedAny(TypedAny), getTypedAny) where
+
+import Language.Haskell.TH (Q, location, Loc(loc_start, loc_package, loc_module))
+import Language.Haskell.TH.Syntax (CharPos, Lift)
+import Type.Reflection (Typeable, TypeRep, (:~~:)(HRefl), pattern TypeRep, eqTypeRep)
+
+
+data Location = Location
+ String -- package
+ String -- module
+ CharPos -- position in source file (line)
+ deriving (Eq, Ord, Lift)
+
+location' :: Q Location
+location' = locationFromLoc <$> location
+ where locationFromLoc loc = Location (loc_package loc) (loc_module loc) (loc_start loc)
+
+data TypedAny = forall a. TypedAny (TypeRep a) a
+
+getTypedAny :: forall a. Typeable a => TypedAny -> a
+getTypedAny (TypedAny rep x) =
+ case eqTypeRep (TypeRep @a) rep of
+ Just HRefl -> x
+ Nothing ->
+ error $ "InitOnce: Type mismatch: requested " ++ show (TypeRep @a) ++ ", got " ++ show rep