diff options
author | Josh Rahm <joshuarahm@gmail.com> | 2024-10-16 00:03:09 -0600 |
---|---|---|
committer | Josh Rahm <joshuarahm@gmail.com> | 2024-10-16 00:03:09 -0600 |
commit | c31a34382d6fe1307a0c6fe1710c42f27fe833ca (patch) | |
tree | f74810d73aeda78e85f63f7c023769791c6afea2 | |
parent | 5924b745fbaf52000981c298ec8f18b3c0c4a1be (diff) | |
download | fiddle-c31a34382d6fe1307a0c6fe1710c42f27fe833ca.tar.gz fiddle-c31a34382d6fe1307a0c6fe1710c42f27fe833ca.tar.bz2 fiddle-c31a34382d6fe1307a0c6fe1710c42f27fe833ca.zip |
Add framework for more easily editing files.
This introduces the FilesM monad, which allows for monadic and
fragmented writing to files in a filesystem. This provides an
abstraction over writing to different "fragments" of files so
implementation, headers and declarations can all be written using just
one pass of the compiler.
-rw-r--r-- | src/Language/Fiddle/Compiler/Backend.hs | 2 | ||||
-rw-r--r-- | src/Language/Fiddle/Compiler/Backend/C.hs | 234 | ||||
-rw-r--r-- | src/Language/Fiddle/Compiler/Backend/Internal/FormattedWriter.hs | 113 | ||||
-rw-r--r-- | src/Language/Fiddle/Compiler/Backend/Internal/FragTree.hs | 159 | ||||
-rw-r--r-- | src/Language/Fiddle/Compiler/Backend/Internal/Writer.hs | 149 |
5 files changed, 490 insertions, 167 deletions
diff --git a/src/Language/Fiddle/Compiler/Backend.hs b/src/Language/Fiddle/Compiler/Backend.hs index eda3ede..dff8e47 100644 --- a/src/Language/Fiddle/Compiler/Backend.hs +++ b/src/Language/Fiddle/Compiler/Backend.hs @@ -62,7 +62,7 @@ backendToParserFunction processTranspileResult :: TranspileResult -> IO () processTranspileResult (TranspileResult mp) = - forM_ (Map.toList mp) $ \(file, text) -> + forM_ (Map.toList mp) $ \(file, text) -> do Data.Text.IO.writeFile file text nullBackend :: Backend diff --git a/src/Language/Fiddle/Compiler/Backend/C.hs b/src/Language/Fiddle/Compiler/Backend/C.hs index 9dbbec6..dd79bac 100644 --- a/src/Language/Fiddle/Compiler/Backend/C.hs +++ b/src/Language/Fiddle/Compiler/Backend/C.hs @@ -9,7 +9,7 @@ module Language.Fiddle.Compiler.Backend.C (cBackend) where import Control.Arrow import Control.Monad (unless) import Control.Monad.RWS -import Control.Monad.State (State) +import Control.Monad.State import Control.Monad.Trans.Writer (Writer, execWriter) import Data.Char (isSpace) import Data.Data (Typeable, cast) @@ -26,6 +26,9 @@ import Data.Text (Text) import qualified Data.Text as Text import Language.Fiddle.Ast import Language.Fiddle.Compiler.Backend +import qualified Language.Fiddle.Compiler.Backend.Internal.FragTree as FragTree +import Language.Fiddle.Compiler.Backend.Internal.FormattedWriter +import Language.Fiddle.Compiler.Backend.Internal.Writer import Language.Fiddle.Internal.UnitInterface import Language.Fiddle.Internal.UnitNumbers import Language.Fiddle.Types @@ -41,147 +44,47 @@ data CBackendFlags = CBackendFlags type StructName = Text -newtype Fragment = Fragment Int - deriving (Eq, Ord) - -- | Header fragment. The top. Starts which include guards and has include -- statements. -hF :: Fragment -hF = Fragment 0 +hF :: FileFragment +hF = ("HEADER", FragTree.above FragTree.center) -- | Structures fragment. The text fragment where structures are defined. -sF :: Fragment -sF = Fragment 25 +sF :: FileFragment +sF = ("HEADER", FragTree.below (snd hF)) -- | Implementation fragment. This is where function implementations go. -iF :: Fragment -iF = Fragment 75 +iF :: FileFragment +iF = ("HEADER", FragTree.above (snd fF)) -- | Assert fragment. This is where static asserts go. -aF :: Fragment -aF = Fragment 50 +aF :: FileFragment +aF = ("HEADER", FragTree.below (snd sF)) -- | Footer fragment. This is wehre the file include endif goes. -fF :: Fragment -fF = Fragment 100 - -tellLn :: (MonadWriter Text m) => Text -> m () -tellLn s = tell s >> tell "\n" +fF :: FileFragment +fF = ("HEADER", FragTree.below FragTree.center) type A = Commented SourceSpan type I = Identity -data St = St - --- | Current local state information while traversing the tree. -data Fmt = Fmt - { indentLevel :: Int, - pendingLine :: Text - } - -newtype M a = M {unM :: RWS () () (St, Files) a} - deriving newtype (Functor, Applicative, Monad, MonadState (St, Files)) - -newtype FormattedWriter a = FormattedWriter (RWS () Text Fmt a) - deriving newtype (Functor, Applicative, Monad, MonadState Fmt) - -indented :: FileM a -> FileM a -indented fn = do - textM (modify (\(Fmt id p) -> Fmt (id + 1) p)) - fn <* textM (modify (\(Fmt id p) -> Fmt (id - 1) p)) - -execFormattedWriter :: FormattedWriter a -> Text -execFormattedWriter ((>> flush) -> FormattedWriter rws) = snd $ execRWS rws () (Fmt 0 "") - -flush :: FormattedWriter () -flush = do - p <- gets pendingLine - modify $ \s -> s {pendingLine = ""} - tell p - -newtype FileFragments = FileFragments (Map Fragment (FormattedWriter ())) - -instance Semigroup FileFragments where - (FileFragments m1) <> (FileFragments m2) = FileFragments (Map.unionWith (>>) m1 m2) - -instance Monoid FileFragments where - mempty = FileFragments mempty +type M a = FilesM () FormattedWriter CFileState a newtype CFileState = CFileState { includedFiles :: Set String } -newtype FileM a = FileM {unFileM :: RWS Fragment FileFragments CFileState a} - deriving newtype (Functor, Applicative, Monad, MonadWriter FileFragments, MonadReader Fragment, MonadState CFileState) - -execFileM :: FileM a -> Text -execFileM fm = - let (_, FileFragments mp) = execRWS (unFileM fm) hF (CFileState mempty) - in ( execFormattedWriter - . sequence_ - . Map.elems - ) - mp - -requireInclude :: String -> FileM () +requireInclude :: String -> M () requireInclude file = do b <- (Set.member file) <$> gets includedFiles unless b $ do - under hF $ + checkout hF $ text $ Text.pack $ printf "#include <%s>\n" file modify $ \s -> s {includedFiles = Set.insert file (includedFiles s)} --- | Writes text to the current fragment context -text :: Text -> FileM () -text t = flip tellF_ t =<< ask - --- | Writes text to the current fragment context -textM :: FormattedWriter () -> FileM () -textM t = flip tellFM_ t =<< ask - --- | Executes a file monad within a different fragment. -under :: Fragment -> FileM () -> FileM () -under fr = local (const fr) - -tellF_ :: Fragment -> Text -> FileM () -tellF_ fp txt = tell $ FileFragments $ Map.singleton fp (tell txt) - -tellFM_ :: Fragment -> FormattedWriter () -> FileM () -tellFM_ fp txtM = tell $ FileFragments $ Map.singleton fp txtM - -newtype Files = Files - { filepaths :: Map FilePath (FileM ()) - } - -withFile :: FilePath -> FileM () -> M () -withFile fp fn = do - modify - ( second $ \(Files {filepaths = fps}) -> - Files - { filepaths = Map.alter (Just . (>> fn) . fromMaybe (return ())) fp fps - } - ) - -instance MonadWriter Text FormattedWriter where - tell txt = FormattedWriter $ do - indent <- (`Text.replicate` " ") <$> gets indentLevel - let lines = Text.splitOn "\n" txt - forM_ (init lines) $ \line -> do - pending <- gets pendingLine - modify $ \s -> s {pendingLine = ""} - tell indent - tell pending - tell line - tell "\n" - modify $ \s -> s {pendingLine = pendingLine s <> last lines} - - listen (FormattedWriter fn) = FormattedWriter $ listen fn - - pass (FormattedWriter fn) = FormattedWriter $ pass fn - cBackend :: Backend cBackend = Backend @@ -211,10 +114,6 @@ cBackend = backendTranspile = transpile } -toTranspileResult :: Files -> TranspileResult -toTranspileResult Files {filepaths = fps} = - TranspileResult $ fmap execFileM fps - transpile :: CBackendFlags -> () -> @@ -226,28 +125,37 @@ transpile cSourceOut = sourceFile } () - fiddleUnit = toTranspileResult . snd . fst $ execRWS (unM run) () (St, Files mempty) + fiddleUnit = toTranspileResult $ fst $ runFilesM execFormattedWriter () (CFileState mempty) hF run where - run :: M () + toTranspileResult :: Map FilePath Text -> TranspileResult + toTranspileResult mp = + TranspileResult $ + Map.mapKeys + ( \case + "SOURCE" | Right sourceFile' <- sourceFile -> sourceFile' + "HEADER" -> headerFile + k -> k + ) + mp + run = do - withFile headerFile $ do + checkout hF $ textM $ do tell $ "#ifndef " <> headerGuard <> "\n" tell $ "#define " <> headerGuard <> "\n\n" tell "#include <stdint.h>\n" - -- Pad out the implementation - under iF $ text "\n" + -- Pad out the implementation + checkout iF $ text "\n" walk (transpileWalk sourceFile headerFile) fiddleUnit () - withFile headerFile $ do - under hF $ - textM $ do - tell "\nstatic_assert(true); // https://github.com/clangd/clangd/issues/1167\n" + checkout hF $ + textM $ do + tell "\nstatic_assert(true); // https://github.com/clangd/clangd/issues/1167\n" - under fF $ - text headerFinal + checkout fF $ + text headerFinal headerFinal = "\n#endif /* " <> headerGuard <> " */\n" @@ -269,20 +177,10 @@ instance IsText Text where qualifiedPathToIdentifier :: (Foldable f, IsText t) => f t -> Text qualifiedPathToIdentifier = Text.intercalate "_" . map toText . toList -ensureNL :: FormattedWriter () -ensureNL = do - p <- gets pendingLine - if Text.null p - then return () - else do - modify $ \s -> s {pendingLine = ""} - tell p - tell "\n" - -pad :: FileM () -> FileM () +pad :: M () -> M () pad f = text "\n" *> f <* text "\n" -writeStaticAssert :: Text -> String -> N Bytes -> FileM () +writeStaticAssert :: Text -> String -> N Bytes -> M () writeStaticAssert structName regname off = do requireInclude "stddef.h" text $ @@ -309,7 +207,7 @@ selectByModifier mod (getter, setter) = (ModifierKeyword Wo _) -> [setter] (ModifierKeyword Pr _) -> [] -writeRegGet :: StructName -> QRegMetadata True -> FileM () +writeRegGet :: StructName -> QRegMetadata True -> M () writeRegGet structType ( QRegMetadata @@ -347,7 +245,7 @@ writeRegGet tell $ Text.pack $ printf " out[%d] = o->%s[%d];\n" i fieldName i tell "}\n\n" -writeRegSet :: StructName -> QRegMetadata True -> FileM () +writeRegSet :: StructName -> QRegMetadata True -> M () writeRegSet structType ( QRegMetadata @@ -401,10 +299,10 @@ pattern DefinedBitsP bitsName bitsFullPath offset <- } ) -writeRegisterBody :: StructName -> QRegMetadata True -> RegisterBody Checked I A -> FileM () +writeRegisterBody :: StructName -> QRegMetadata True -> RegisterBody Checked I A -> M () writeRegisterBody structName regmeta = walk_ registerWalk where - registerWalk :: forall t. (Walk t, Typeable t) => t I A -> FileM () + registerWalk :: forall t. (Walk t, Typeable t) => t I A -> M () registerWalk t = case () of () | (Just (DefinedBitsP bitsName fullPath offset)) <- castTS t -> @@ -433,7 +331,7 @@ writeImplementation :: QRegMetadata True -> Modifier f a -> Maybe (RegisterBody Checked I A) -> - FileM () + M () -- | Register is just padding, don't emit anything writeImplementation _ (regIsPadding -> True) _ _ = return () @@ -444,7 +342,7 @@ writeImplementation structName qMeta mod bod = do mapM_ (writeRegisterBody structName qMeta) bod -structBody :: StructName -> ObjTypeBody Checked I A -> FileM () +structBody :: StructName -> ObjTypeBody Checked I A -> M () structBody structName (ObjTypeBody _ decls _) = do forM_ decls $ \(Directed _ decl _) -> case decl of @@ -462,10 +360,10 @@ structBody structName (ObjTypeBody _ decls _) = do tell (sizeToField i sz) tell ";\n" - under aF $ + checkout aF $ writeStaticAssert structName i off - under iF $ + checkout iF $ writeImplementation structName regMetadata mod bod TypeSubStructure { subStructureBody = Identity bod, @@ -491,7 +389,7 @@ structBody structName (ObjTypeBody _ decls _) = do 8 -> "volatile uint64_t " <> f n -> "volatile uint8_t " <> f <> "[" <> Text.pack (show n) <> "]" -union :: Text -> FileM () -> FileM () +union :: Text -> M () -> M () union identifier fn = do text "#pragma pack(push, 1)\n" text $ "union " <> identifier <> " " @@ -499,7 +397,7 @@ union identifier fn = do text ";\n" text "#pragma pack(pop)\n" -struct :: Text -> FileM () -> FileM () +struct :: Text -> M () -> M () struct identifier fn = do text "#pragma pack(push, 1)\n" text $ "struct " <> identifier <> " " @@ -507,8 +405,11 @@ struct identifier fn = do text ";\n" text "#pragma pack(pop)\n" -body :: FileM a -> FileM a -body f = text "{\n" *> indented f <* textM (ensureNL >> tell "}") +body :: M a -> M a +body f = text "{\n" *> withIndent f <* textM (ensureNL >> tell "}") + +withIndent :: M a -> M a +withIndent = block incIndent decIndent identifierFor :: (ExportableDecl d) => d -> Text identifierFor = qualifiedPathToIdentifier . metadataFullyQualifiedPath . getMetadata @@ -536,7 +437,10 @@ emitDocComments (Commented comments _) = do then Text.tail t else t -transpileWalk :: Either ImplementationInHeader FilePath -> FilePath -> (forall t. (Walk t, Typeable t) => t I A -> () -> M (WalkContinuation ())) +transpileWalk :: + Either ImplementationInHeader FilePath -> + FilePath -> + (forall t. (Walk t, Typeable t) => t I A -> () -> M (WalkContinuation ())) transpileWalk _ headerFile t _ = case () of () | Just @@ -551,21 +455,19 @@ transpileWalk _ headerFile t _ = case () of Union {} -> union Struct {} -> struct - withFile headerFile $ do - under sF $ do - pad $ do - textM $ emitDocComments a - let structName = identifierFor (unwrap metadata) - structureType structName $ do - structBody structName objTypeBody + checkout sF $ do + pad $ do + textM $ emitDocComments a + let structName = identifierFor (unwrap metadata) + structureType structName $ do + structBody structName objTypeBody return Stop () | Just (getExportedObjectDecl -> Just e) <- castTS t -> do let qname = qualifiedPathToIdentifier (metadataFullyQualifiedPath (getMetadata e)) - withFile headerFile $ - under fF $ do - text "#define " - text qname - text $ Text.pack $ printf " ((%s*)0x%08x)\n" (toLiteralTypeName (exportedObjectDeclType e)) (exportedObjectDeclLocation e) + checkout fF $ do + text "#define " + text qname + text $ Text.pack $ printf " ((%s*)0x%08x)\n" (toLiteralTypeName (exportedObjectDeclType e)) (exportedObjectDeclLocation e) return Stop _ -> return (Continue ()) diff --git a/src/Language/Fiddle/Compiler/Backend/Internal/FormattedWriter.hs b/src/Language/Fiddle/Compiler/Backend/Internal/FormattedWriter.hs new file mode 100644 index 0000000..bde9ebe --- /dev/null +++ b/src/Language/Fiddle/Compiler/Backend/Internal/FormattedWriter.hs @@ -0,0 +1,113 @@ +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE OverloadedStrings #-} + +-- | This module provides the 'FormattedWriter' monad, which extends a basic +-- "Writer" over 'Text' by allowing formatting features like indentation, line +-- breaks, and ensuring the output is well-structured for readability. +-- +-- The 'FormattedWriter' is useful for generating pretty-printed output, such as +-- source code generation, where indentation and newline management are essential. +module Language.Fiddle.Compiler.Backend.Internal.FormattedWriter + ( -- * Types + FormattedWriter, -- | The main writer monad with formatting features. + -- * Core Operations + ensureNL, -- | Ensures that there is a newline at the end of the current line. + flush, -- | Flushes any pending text (writes the pending line). + incIndent, -- | Increases the indentation level. + decIndent, -- | Decreases the indentation level. + indented, -- | Performs an action with increased indentation. + execFormattedWriter, -- | Runs the 'FormattedWriter' and produces the final formatted 'Text'. + ) +where + +import Control.Monad (forM_) +import Control.Monad.RWS.Strict +import Data.Text (Text) +import qualified Data.Text as Text + +-- | Internal state for the 'FormattedWriter' monad. +-- +-- * 'indentLevel': The current level of indentation. +-- * 'pendingLine': The current line that is being built but not yet written. +data Fmt = Fmt + { indentLevel :: Int, -- ^ Current indentation level. + pendingLine :: Text -- ^ Text that has been added but not yet written to the output. + } + +-- | The 'FormattedWriter' is a monad that provides functionality for writing +-- formatted text, with control over indentation and newlines. +newtype FormattedWriter a = FormattedWriter (RWS () Text Fmt a) + deriving newtype (Functor, Applicative, Monad, MonadState Fmt) + +-- | 'MonadWriter' instance for 'FormattedWriter', which allows writing text +-- while maintaining proper indentation and ensuring that newlines are handled +-- correctly. Each line is prefixed with the correct indentation level. +instance MonadWriter Text FormattedWriter where + tell txt = FormattedWriter $ do + -- Get the current indentation level and prepare the indentation prefix. + indent <- (`Text.replicate` " ") <$> gets indentLevel + -- Split the text into lines. + let lines = Text.splitOn "\n" txt + -- For each line, write the indent, the pending line, and then the line itself. + forM_ (Prelude.init lines) $ \line -> do + pending <- gets pendingLine + modify $ \s -> s {pendingLine = ""} -- Reset pending line + tell indent -- Add indentation + tell pending -- Write any pending text + tell line -- Write the actual line + tell "\n" -- Add a newline after the line + -- The last fragment is kept as the new pending line, to be written later. + modify $ \s -> s {pendingLine = pendingLine s <> Prelude.last lines} + + -- Allow listening to the written text within the 'FormattedWriter' context. + listen (FormattedWriter fn) = FormattedWriter $ listen fn + + -- Allow transformation of the written text via a pass operation. + pass (FormattedWriter fn) = FormattedWriter $ pass fn + +-- | Ensures that a newline is written if there is any pending text. This is +-- useful for ensuring that the output ends on a clean line. +ensureNL :: FormattedWriter () +ensureNL = do + p <- gets pendingLine + if Text.null p + then return () + else do + modify $ \s -> s {pendingLine = ""} + tell p + tell "\n" + +-- | Flushes the pending line by writing it to the output and clearing the +-- pending state. This is useful when you want to force the current line to +-- be written immediately. +flush :: FormattedWriter () +flush = do + p <- gets pendingLine + modify $ \s -> s {pendingLine = ""} + tell p + +-- | Increases the indentation level by one. This will affect all subsequent +-- lines written within the 'FormattedWriter' monad. +incIndent :: FormattedWriter () +incIndent = modify (\(Fmt id p) -> Fmt (id + 1) p) + +-- | Decreases the indentation level by one. This will affect all subsequent +-- lines written within the 'FormattedWriter' monad. +decIndent :: FormattedWriter () +decIndent = modify (\(Fmt id p) -> Fmt (id - 1) p) + +-- | Runs the given 'FormattedWriter' action with an additional indentation level. +-- Once the action completes, the indentation level is decreased. +indented :: FormattedWriter () -> FormattedWriter () +indented fn = do + incIndent -- Increase indentation level + fn -- Run the action with the increased indentation + decIndent -- Restore the original indentation level + +-- | Runs a 'FormattedWriter' and returns the final formatted 'Text' output. +-- It ensures that any pending lines are flushed before returning the result. +execFormattedWriter :: FormattedWriter a -> Text +execFormattedWriter ((>> flush) -> FormattedWriter rws) = + snd $ execRWS rws () (Fmt 0 "") -- Execute the RWS monad, starting with no indentation and no pending line. + diff --git a/src/Language/Fiddle/Compiler/Backend/Internal/FragTree.hs b/src/Language/Fiddle/Compiler/Backend/Internal/FragTree.hs new file mode 100644 index 0000000..bc55c5a --- /dev/null +++ b/src/Language/Fiddle/Compiler/Backend/Internal/FragTree.hs @@ -0,0 +1,159 @@ +{-# LANGUAGE DeriveFoldable #-} +{-# LANGUAGE DeriveTraversable #-} + +-- | Module for a 'FragTree' data structure. This data structure represents +-- "fragments" of a file, which are logical points in a file or buffer. +-- +-- A fragment represents an indexed position in a file, but the position +-- is not necessarily discrete. Instead, fragments can be positioned +-- relative to each other (e.g., above or below other fragments), +-- allowing for arbitrary resolution within the file. +module Language.Fiddle.Compiler.Backend.Internal.FragTree + ( -- * Types + Fragment, + FragTree, + + -- * Fragment Constructors + center, + -- | A fragment at the center of the file. + above, + -- | A fragment above a given fragment. + below, + -- | A fragment below a given fragment. + -- * FragTree Operations + updateWithReturn, + -- | Update a fragment in the tree and return a custom value. + Language.Fiddle.Compiler.Backend.Internal.FragTree.lookup, + -- | Lookup a value by fragment. + update, + -- | Update or insert a value into the tree. + insertOrReplace, + -- | Insert a value at a fragment, replacing any existing value. + insertAppend, + -- | Insert a value by appending (using 'Monoid') if a value already exists. + insertWith, + -- | Insert a value, combining it with an existing value using a custom function. + delete, + -- | Delete a value from the tree at a specific fragment. + singleton, + -- | Create a tree with a single fragment and value. + ) +where + +-- | Direction of a fragment relative to another fragment. +data Loc = Above | Below + +-- | A 'Fragment' is a position in a file or buffer that can be relative +-- to other fragments (above or below). +newtype Fragment = Fragment [Loc] + +-- | Fragment that represents the center of the file. +center :: Fragment +center = Fragment [] + +-- | Create a fragment above the given fragment. +above :: Fragment -> Fragment +above (Fragment l) = Fragment $ Above : l + +-- | Create a fragment below the given fragment. +below :: Fragment -> Fragment +below (Fragment l) = Fragment $ Below : l + +-- | A 'FragTree' represents a binary tree where each node holds a fragment's value. +-- Each tree node has two children, representing the fragments above and below +-- the current fragment. Leaves represent empty nodes. +data FragTree a + = FragTree (FragTree a) (FragTree a) (Maybe a) + | FragLeaf + deriving (Functor) + +instance Foldable FragTree where + foldMap _ FragLeaf = mempty + foldMap fn (FragTree up down t) = + foldMap fn up <> foldMap fn t <> foldMap fn down + +instance Traversable FragTree where + traverse _ FragLeaf = pure FragLeaf + traverse fn (FragTree up down t) = do + (\a b c -> FragTree a c b) + <$> traverse fn up + <*> traverse fn t + <*> traverse fn down + +-- | 'Semigroup' instance for 'FragTree', merging two trees by combining values +-- from corresponding fragments, using the 'Semigroup' instance of the contained values. +instance (Semigroup a) => Semigroup (FragTree a) where + (<>) (FragTree ab1 be1 v1) (FragTree ab2 be2 v2) = + FragTree (ab1 <> ab2) (be1 <> be2) (v1 <> v2) + (<>) FragLeaf f = f + (<>) f FragLeaf = f + +-- | 'Monoid' instance for 'FragTree'. The empty tree is represented by 'FragLeaf'. +instance (Semigroup a) => Monoid (FragTree a) where + mempty = FragLeaf + +-- | 'updateWithReturn' updates a 'FragTree' at the specified 'Fragment', +-- returning both a custom value and the updated tree. +-- If the fragment doesn't exist, it is created. +updateWithReturn :: (Maybe a -> (b, Maybe a)) -> Fragment -> FragTree a -> (b, FragTree a) +updateWithReturn fn (Fragment (reverse -> loc)) = update' fn loc + where + -- Helper function for traversing and updating the tree. + update' fn [] FragLeaf = clean . FragTree FragLeaf FragLeaf <$> fn Nothing + update' fn [] (FragTree a b v) = clean . FragTree a b <$> fn v + update' fn (Above : ls) (FragTree a b v) = + fmap clean $ + (\a' -> FragTree a' b v) <$> update' fn ls a + update' fn (Below : ls) (FragTree a b v) = + fmap clean $ + (\b' -> FragTree a b' v) <$> update' fn ls b + update' fn (Above : ls) FragLeaf = + fmap clean $ + (\a' -> FragTree a' FragLeaf Nothing) <$> update' fn ls FragLeaf + update' fn (Below : ls) FragLeaf = + fmap clean $ + (\b' -> FragTree FragLeaf b' Nothing) <$> update' fn ls FragLeaf + +-- | Create a 'FragTree' with a single 'Fragment' and associated value. +singleton :: Fragment -> a -> FragTree a +singleton f a = insertOrReplace f a FragLeaf + +-- | Lookup a value in the 'FragTree' by its 'Fragment'. Returns 'Nothing' +-- if the fragment does not exist. +lookup :: Fragment -> FragTree a -> Maybe a +lookup l = fst . updateWithReturn (\a -> (a, a)) l + +-- | Update a 'FragTree' at the given 'Fragment'. If the fragment does not exist, +-- it is created. The function is applied to the existing value (or 'Nothing' if no value). +update :: (Maybe a -> Maybe a) -> Fragment -> FragTree a -> FragTree a +update fn fr = snd . updateWithReturn (\ma -> ((), fn ma)) fr + +-- | Insert a value into the 'FragTree', replacing any existing value at the given fragment. +insertOrReplace :: Fragment -> a -> FragTree a -> FragTree a +insertOrReplace l a = update (const $ Just a) l + +-- | Delete a fragment from the 'FragTree', setting its value to 'Nothing'. +delete :: Fragment -> FragTree a -> FragTree a +delete = update (const Nothing) + +-- | Insert a value by appending (using the 'Monoid' instance) to an existing value +-- at the given fragment. If no value exists, the provided value is inserted. +insertAppend :: (Monoid a) => Fragment -> a -> FragTree a -> FragTree a +insertAppend l a = update (<> Just a) l + +-- | Insert a value using a custom function to combine it with an existing value +-- at the given fragment. If no val +insertWith :: (a -> a -> a) -> Fragment -> a -> FragTree a -> FragTree a +insertWith fn fr v = + update + ( \case + Nothing -> Just v + Just a -> Just (fn a v) + ) + fr + +-- | Internal function for cleaning up a 'FragTree' by collapsing empty nodes +-- (i.e., 'FragTree' nodes with no values and no children) into 'FragLeaf'. +clean :: FragTree a -> FragTree a +clean (FragTree FragLeaf FragLeaf Nothing) = FragLeaf +clean x = x diff --git a/src/Language/Fiddle/Compiler/Backend/Internal/Writer.hs b/src/Language/Fiddle/Compiler/Backend/Internal/Writer.hs new file mode 100644 index 0000000..83f6e27 --- /dev/null +++ b/src/Language/Fiddle/Compiler/Backend/Internal/Writer.hs @@ -0,0 +1,149 @@ +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE OverloadedStrings #-} + +-- | This module contains the 'FilesM' monad, which allows for incremental, +-- fragmented, and monadic editing of "files" in a "filesystem" (represented as a +-- 'Map' from 'FilePath' to 'Text'). +-- +-- The 'FilesM' monad provides operations for "checking out" file fragments, writing +-- text to them, and managing file modifications in a monadic fashion. +-- The final result, produced by 'runFilesM', is a map of file paths to text, +-- representing the contents that should be written to each file. +module Language.Fiddle.Compiler.Backend.Internal.Writer + ( -- * Types + FileFragment, + + -- * FilesM Operations + + -- | Run the 'FilesM' monad and produce the final map of files and their contents. + runFilesM, + -- | A variant of 'runFilesM' that runs with a 'Writer'. + runFileMWriter, + -- | Write plain text to the current file fragment. + text, + -- | Write a monadic action that produces text to the current file fragment. + textM, + -- | The 'FilesM' monad for file-based operations. + FilesM, + -- | Checkout a specific file fragment for writing. + checkout, + -- | Write a line of text (with a newline) to the current file fragment. + tellLn, + -- | Surround a block of file operations with a start and end marker. + block, + ) +where + +import Control.Arrow +import Control.Monad.RWS.Strict +import Control.Monad.Trans.Writer (Writer, execWriter) +import Data.Map (Map) +import qualified Data.Map as Map +import Data.Monoid +import Data.Text (Text) +import Language.Fiddle.Compiler.Backend.Internal.FragTree (FragTree, Fragment) +import qualified Language.Fiddle.Compiler.Backend.Internal.FragTree as FragTree + +-- | 'FileMap' represents the mapping between 'FilePath' and file content. +-- The content of each file is stored as a 'FragTree', which can hold fragments +-- of text. +newtype FileMap a = FileMap (Map FilePath a) + deriving newtype (Functor, Show, Foldable) + +-- | 'Semigroup' instance for 'FileMap'. When merging two 'FileMap's, the content +-- of each file is combined using the 'Semigroup' instance of the file content type. +instance (Semigroup a) => Semigroup (FileMap a) where + (<>) (FileMap m1) (FileMap m2) = FileMap $ Map.unionWith (<>) m1 m2 + +-- | 'Monoid' instance for 'FileMap'. The empty file map is represented by an empty map. +instance (Semigroup a) => Monoid (FileMap a) where + mempty = FileMap mempty + +-- | The 'FilesM' monad provides a context for managing file fragments and writing +-- content to them in a monadic style. It is parameterized over: +-- +-- * @r@: The environment shared across file writes (e.g., metadata or global settings). +-- * @w@: The writer monad used to accumulate file content ('Text' or some other type). +-- * @s@: The state type for managing any local state during the file writes. +-- * @a@: The return type of the computation. +newtype FilesM r w s a + = FilesM + ( RWS + (r, FileFragment) -- Reader context containing global settings and current file fragment. + (FileMap (FragTree (Ap w ()))) -- Writer accumulating the file content as a 'FragTree'. + (s, w ()) -- State holding user-provided state 's' and the current writer accumulator. + a -- Result of the computation. + ) + deriving newtype (Functor, Applicative, Monad) + +-- | 'MonadState' instance for 'FilesM'. Allows manipulation of state within the 'FilesM' monad. +instance (Applicative w) => MonadState s (FilesM r w s) where + get = FilesM $ fst <$> get + put v = FilesM $ modify $ first (const v) + +-- | 'MonadReader' instance for 'FilesM'. Allows access to reader environment. +instance (Applicative w) => MonadReader r (FilesM r w s) where + ask = FilesM $ fst <$> ask + local fn (FilesM a) = FilesM $ local (first fn) a + +-- | A 'FileFragment' represents a specific section of a file. It is a tuple of a 'FilePath' +-- and a 'Fragment', which denotes a position in the file where modifications are made. +type FileFragment = (FilePath, Fragment) + +-- | Checkout a specific file fragment for writing. After checking out a fragment, +-- subsequent file operations will affect that fragment until another fragment is checked out. +checkout :: (Monad w) => FileFragment -> FilesM r w s a -> FilesM r w s a +checkout (fileName, newFrag) (FilesM fn) = + FilesM $ do + flush -- Flush any existing writes before switching fragments + -- Change the current fragment being worked on + local (\(r, _) -> (r, (fileName, newFrag))) $ fn <* flush + where + (FilesM flush) = flushFilesM + +-- | Internal helper function that flushes the current state of the 'FilesM' monad +-- by committing any pending writes to the current file fragment. +flushFilesM :: (Monad w) => FilesM r w s () +flushFilesM = FilesM $ do + (fileName, frag) <- asks snd -- Get the current file and fragment being written to + writer <- gets snd -- Get the current text to write + modify $ \(s, _) -> (s, return ()) -- Clear the writer + tell $ FileMap $ Map.singleton fileName $ FragTree.singleton frag (Ap writer) + +-- | Write plain text to the current file fragment. +text :: (MonadWriter t w) => t -> FilesM r w s () +text txt = FilesM $ modify (second (>> tell txt)) + +-- | Write a monadic action that produces text to the current file fragment. +textM :: (Monad w) => w () -> FilesM r w s () +textM fn = FilesM $ modify (second (>> fn)) + +-- | Run the 'FilesM' monad and produce the final file map along with any additional state. +-- The result is a 'Map' from 'FilePath' to some textual representation, as well as +-- the final state of the monad. +runFilesM :: + (Monad w) => + (forall x. w x -> t) -> -- Function to extract text from the writer monad + r -> -- Reader environment + s -> -- Initial state + FileFragment -> -- Initial file fragment to write to + FilesM r w s a -> -- FilesM monadic action to run + (Map FilePath t, (a, s)) -- Final map of files and their contents, and final result and state +runFilesM exec rr ss initFrag ((<* flushFilesM) -> FilesM rws) = + let (a, (s, _), FileMap filesMap) = runRWS rws (rr, initFrag) (ss, return ()) + in (fmap (exec . mapM_ getAp) filesMap, (a, s)) + +-- | A helper function to run 'FilesM' with a 'Writer' monad, where each file's contents +-- is represented as a monoid (such as 'Text'). +runFileMWriter :: (Monoid t) => r -> s -> FileFragment -> FilesM r (Writer t) s a -> Map FilePath t +runFileMWriter r s initFrag f = fst $ runFilesM execWriter r s initFrag f + +-- | Write a line of text (appends a newline) to the current file fragment. +tellLn :: (MonadWriter Text m) => Text -> m () +tellLn s = tell s >> tell "\n" + +-- | Surround a block of file operations with a start and end marker. +-- This is useful for inserting things like function or block delimiters. +block :: (Monad w) => w () -> w () -> FilesM r w s a -> FilesM r w s a +block s e f = textM s *> f <* textM e |