summaryrefslogtreecommitdiff
path: root/src/Language/Fiddle/Compiler/ConsistencyCheck.hs
diff options
context:
space:
mode:
authorJosh Rahm <joshuarahm@gmail.com>2024-09-26 00:28:41 -0600
committerJosh Rahm <joshuarahm@gmail.com>2024-09-26 00:28:41 -0600
commita4cffc1eeb547f780068875a703251db6aa41d6c (patch)
tree77d44ebaa73a909923b958c11daf1acd5a735977 /src/Language/Fiddle/Compiler/ConsistencyCheck.hs
parent3a59cfb59b3339e13bdc9dfd1696ae2c554fcd9a (diff)
downloadfiddle-a4cffc1eeb547f780068875a703251db6aa41d6c.tar.gz
fiddle-a4cffc1eeb547f780068875a703251db6aa41d6c.tar.bz2
fiddle-a4cffc1eeb547f780068875a703251db6aa41d6c.zip
Rename some of the stages.
Stage1 -> Parsed Stage2 -> Expanded Stage3 -> Checked
Diffstat (limited to 'src/Language/Fiddle/Compiler/ConsistencyCheck.hs')
-rw-r--r--src/Language/Fiddle/Compiler/ConsistencyCheck.hs523
1 files changed, 523 insertions, 0 deletions
diff --git a/src/Language/Fiddle/Compiler/ConsistencyCheck.hs b/src/Language/Fiddle/Compiler/ConsistencyCheck.hs
new file mode 100644
index 0000000..90f4aa4
--- /dev/null
+++ b/src/Language/Fiddle/Compiler/ConsistencyCheck.hs
@@ -0,0 +1,523 @@
+{-# LANGUAGE ConstraintKinds #-}
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE IncoherentInstances #-}
+{-# LANGUAGE TypeOperators #-}
+{-# LANGUAGE UndecidableInstances #-}
+
+module Language.Fiddle.Compiler.ConsistencyCheck (checkConsistency) where
+
+import Control.Monad (forM, forM_, unless, when)
+import Control.Monad.Identity (Identity (Identity))
+import Control.Monad.RWS (MonadState (get, put), MonadWriter (tell), gets, modify')
+import Data.Foldable (Foldable (toList), foldlM)
+import Data.Functor.Identity
+import qualified Data.IntMap as IntMap
+import Data.Kind (Type)
+import Data.List (inits, intercalate)
+import Data.List.NonEmpty (NonEmpty (..))
+import qualified Data.List.NonEmpty as NonEmpty
+import Data.Map (Map)
+import qualified Data.Map as Map
+import Data.Maybe (fromMaybe)
+import qualified Data.Set as Set
+import qualified Data.Text as Text
+import Data.Word (Word32)
+import Language.Fiddle.Ast
+import Language.Fiddle.Compiler
+import Language.Fiddle.Internal.Scopes
+import Language.Fiddle.Types (Commented (unCommented), SourceSpan)
+import Text.Printf (printf)
+import Prelude hiding (unzip)
+
+newtype GlobalState = GlobalState
+ { globalScope :: Scope String (Either SizeBits SizeBytes)
+ }
+
+newtype LocalState = LocalState (ScopePath String)
+
+type I = Identity
+
+type Annot = Commented SourceSpan
+
+type SizeBits = Word32
+
+type SizeBytes = Word32
+
+checkConsistency ::
+ FiddleUnit Expanded I Annot ->
+ Compile () (FiddleUnit Checked I Annot)
+checkConsistency =
+ fmap snd
+ . subCompile (GlobalState mempty)
+ . advanceStage (LocalState mempty)
+
+instance CompilationStage Expanded where
+ type StageAfter Expanded = Checked
+ type StageMonad Expanded = Compile GlobalState
+ type StageState Expanded = LocalState
+ type StageFunctor Expanded = Identity
+ type StageAnnotation Expanded = Commented SourceSpan
+
+deriving instance AdvanceStage Expanded FiddleUnit
+
+deriving instance AdvanceStage Expanded Expression
+
+deriving instance AdvanceStage Expanded ObjType
+
+deriving instance AdvanceStage Expanded DeferredRegisterBody
+
+deriving instance AdvanceStage Expanded RegisterBitsDecl
+
+deriving instance AdvanceStage Expanded RegisterBitsTypeRef
+
+deriving instance AdvanceStage Expanded AnonymousBitsType
+
+deriving instance AdvanceStage Expanded BitType
+
+deriving instance AdvanceStage Expanded EnumBody
+
+deriving instance AdvanceStage Expanded EnumConstantDecl
+
+deriving instance AdvanceStage Expanded PackageBody
+
+deriving instance (AdvanceStage Expanded t) => AdvanceStage Expanded (Directed t)
+
+instance AdvanceStage Expanded RegisterBody where
+ advanceStage s body = fst <$> registerBodyToStage3 s body
+
+instance AdvanceStage Expanded ObjTypeBody where
+ advanceStage s body = fst <$> objTypeBodyToStage3 s body 0
+
+instance AdvanceStage Expanded FiddleDecl where
+ modifyState t s = case t of
+ (BitsDecl id typ a) -> do
+ typeSize <- getTypeSize typ
+ insertTypeSize s id typeSize
+ return s
+ (PackageDecl n _ _) -> do
+ let strs = nameToList n
+ let (LocalState scopePath) = s
+
+ return $
+ LocalState $
+ scopePath {currentScope = strs ++ currentScope scopePath}
+ (UsingDecl n _) ->
+ let (LocalState scopePath) = s
+ in return $
+ LocalState $
+ scopePath
+ { usingPaths = nameToList n : usingPaths scopePath
+ }
+ _ -> return s
+
+nameToList :: Name f a -> [String]
+nameToList (Name idents _) = map (\(Identifier (Text.unpack -> s) _) -> s) (toList idents)
+
+objTypeBodyToStage3 ::
+ LocalState ->
+ ObjTypeBody Expanded I Annot ->
+ Word32 ->
+ Compile GlobalState (ObjTypeBody Checked I Annot, Word32)
+objTypeBodyToStage3 st (ObjTypeBody bodyType decls a) startOff = do
+ let isUnion = case bodyType of
+ Union {} -> True
+ _ -> False
+ (cur, returned) <-
+ foldlM
+ ( \(cursor, returned) decl ->
+ case undirected decl of
+ RegisterDecl mMod mIdent expr mBody a -> do
+ (s3RegisterBody, mCalculatedSize) <-
+ fUnzip <$> mapM (registerBodyToStage3 st) mBody
+
+ nExpr <- advanceStage st expr
+
+ let s3 =
+ mapDirected
+ ( const $
+ RegisterDecl
+ mMod
+ mIdent
+ nExpr
+ s3RegisterBody
+ a
+ )
+ decl
+
+ declaredSizeBits <- fromIntegral <$> exprToSize expr
+
+ when ((declaredSizeBits `mod` 8) /= 0) $
+ tell
+ [ Diagnostic
+ Error
+ "Register size is not a multiple of 8. Please pad register size to align with 8. "
+ (unCommented a)
+ ]
+
+ forM_ mCalculatedSize $ \(fromIntegral -> calculatedSize) ->
+ unless (calculatedSize == declaredSizeBits) $
+ let helpful =
+ if calculatedSize < declaredSizeBits
+ then
+ printf
+ "\nPerhaps you should add 'reserved(%d)' to the end of your register declaration?"
+ (declaredSizeBits - calculatedSize)
+ else ""
+ in tell
+ [ Diagnostic
+ Error
+ ( printf
+ "Calculated size %d does not match declared size %d.%s"
+ calculatedSize
+ declaredSizeBits
+ helpful
+ )
+ (unCommented a)
+ ]
+
+ if isUnion
+ then
+ checkUnion cursor (declaredSizeBits `div` 8) (s3 : returned) a
+ else
+ return (cursor + declaredSizeBits `div` 8, s3 : returned)
+ TypeSubStructure (Identity subBody) maybeIdent annot -> do
+ (newBody, size) <-
+ objTypeBodyToStage3
+ st
+ subBody
+ ( if isUnion then startOff else cursor
+ )
+ let s3 = mapDirected (const $ TypeSubStructure (Identity newBody) maybeIdent annot) decl
+
+ checkTypesSubStructure subBody maybeIdent annot
+ if isUnion
+ then
+ checkUnion cursor size (s3 : returned) a
+ else
+ return (cursor + size, s3 : returned)
+ ReservedDecl expr annot -> do
+ size' <- fromIntegral <$> exprToSize expr
+ when ((size' `mod` 8) /= 0) $
+ tell
+ [ Diagnostic
+ Error
+ "Can only reserve a multiple of 8 bits in this context."
+ (unCommented a)
+ ]
+
+ expr' <- advanceStage st expr
+ let size = size' `div` 8
+ let s3 = mapDirected (const $ ReservedDecl expr' annot) decl
+ if isUnion
+ then
+ checkUnion cursor size (s3 : returned) a
+ else
+ return (cursor + size, s3 : returned)
+ AssertPosStatement _ expr a -> do
+ declaredPos <- fromIntegral <$> exprToSize expr
+
+ let expectedPos = if isUnion then startOff else cursor + startOff
+
+ when (expectedPos /= declaredPos) $ do
+ tell
+ [ Diagnostic
+ Error
+ ( printf
+ "Position assertion failed. Asserted 0x%x, calculated 0x%x"
+ declaredPos
+ expectedPos
+ )
+ (unCommented a)
+ ]
+ return (cursor, returned)
+ )
+ (0, [])
+ decls
+
+ return (ObjTypeBody bodyType (reverse returned) a, cur)
+ where
+ checkTypesSubStructure
+ (ObjTypeBody bodyType decls _)
+ maybeIdent
+ annot =
+ let emitWarning s = tell [Diagnostic Warning s (unCommented annot)]
+ in case () of
+ ()
+ | [_] <- decls,
+ (Union {}) <- bodyType ->
+ emitWarning "Union with a single field. Should this be a struct?"
+ ()
+ | [_] <- decls,
+ (Struct {}) <- bodyType,
+ Nothing <- maybeIdent ->
+ emitWarning "Anonymous sub-struct with single field is superfluous."
+ ()
+ | [] <- decls ->
+ emitWarning
+ ( printf
+ "Empty sub-%s is superfluous."
+ ( case bodyType of
+ Union {} -> "union"
+ Struct {} -> "struct"
+ )
+ )
+ _ -> return ()
+ fUnzip xs = (fst <$> xs, snd <$> xs)
+ pushApply :: Maybe (a, b) -> (Maybe a, Maybe b)
+ pushApply (Just (a, b)) = (Just a, Just b)
+ pushApply Nothing = (Nothing, Nothing)
+
+registerBodyToStage3 ::
+ LocalState ->
+ RegisterBody Expanded I Annot ->
+ Compile GlobalState (RegisterBody Checked I Annot, Word32)
+registerBodyToStage3
+ st
+ (RegisterBody bodyType (Identity deferredRegisterBody) a') = do
+ let isUnion = case bodyType of
+ Union {} -> True
+ _ -> False
+
+ case deferredRegisterBody of
+ DeferredRegisterBody decls a -> do
+ (cur, returned) <-
+ foldlM
+ ( \(cursor, returned) decl ->
+ case undirected decl of
+ ReservedBits expr a -> do
+ size <- fromIntegral <$> exprToSize expr
+ expr' <- advanceStage st expr
+ let s3 =
+ mapDirected
+ (const $ ReservedBits expr' a)
+ decl
+ if isUnion
+ then checkUnion cursor size (s3 : returned) a
+ else
+ return (cursor + size, s3 : returned)
+ BitsSubStructure registerBody maybeIdent annot -> do
+ checkBitsSubStructure registerBody maybeIdent annot
+
+ (newBody, subsize) <- registerBodyToStage3 st registerBody
+ let s3 =
+ mapDirected
+ (const $ BitsSubStructure newBody maybeIdent annot)
+ decl
+
+ if isUnion
+ then checkUnion cursor subsize (s3 : returned) a
+ else
+ return (cursor + subsize, s3 : returned)
+ DefinedBits modifier identifier typeref a -> do
+ (s3TypeRef, size) <- registerBitsTypeRefToStage3 st typeref
+ let s3 =
+ mapDirected
+ (const $ DefinedBits modifier identifier s3TypeRef a)
+ decl
+
+ if isUnion
+ then checkUnion cursor size (s3 : returned) a
+ else
+ return (cursor + size, s3 : returned)
+ )
+ (0, [])
+ decls
+
+ return
+ ( RegisterBody
+ bodyType
+ (Identity (DeferredRegisterBody (reverse returned) a))
+ a',
+ cur
+ )
+ where
+ checkBitsSubStructure
+ (RegisterBody bodyType (Identity (DeferredRegisterBody decls _)) _)
+ maybeIdent
+ annot =
+ let emitWarning s = tell [Diagnostic Warning s (unCommented annot)]
+ in case () of
+ ()
+ | [_] <- decls,
+ (Union {}) <- bodyType ->
+ emitWarning "Union with a single field. Should this be a struct?"
+ ()
+ | [_] <- decls,
+ (Struct {}) <- bodyType,
+ Nothing <- maybeIdent ->
+ emitWarning "Anonymous sub-struct with single field is superfluous."
+ ()
+ | [] <- decls ->
+ emitWarning
+ ( printf
+ "Empty sub-%s is superfluous."
+ ( case bodyType of
+ Union {} -> "union"
+ Struct {} -> "struct"
+ )
+ )
+ _ -> return ()
+
+registerBitsTypeRefToStage3 ::
+ LocalState ->
+ RegisterBitsTypeRef Expanded I Annot ->
+ Compile GlobalState (RegisterBitsTypeRef Checked I Annot, Word32)
+registerBitsTypeRefToStage3 localState = \case
+ RegisterBitsArray ref expr a -> do
+ (ref', size) <- registerBitsTypeRefToStage3 localState ref
+ multiplier <- exprToSize expr
+ expr' <- advanceStage localState expr
+ return
+ ( RegisterBitsArray ref' expr' a,
+ size * fromIntegral multiplier
+ )
+ RegisterBitsReference name a ->
+ (RegisterBitsReference name a,) <$> lookupTypeSize localState name
+ RegisterBitsJustBits expr a -> do
+ expr' <- advanceStage localState expr
+ (RegisterBitsJustBits expr' a,)
+ . fromIntegral
+ <$> exprToSize expr
+
+checkUnion :: Word32 -> Word32 -> b -> Commented SourceSpan -> Compile s (Word32, b)
+checkUnion cursor subsize ret a = do
+ when (cursor /= 0 && subsize /= cursor) $ do
+ tell
+ [ Diagnostic
+ Warning
+ ( printf
+ "Jagged union found. Found size %d, expected %d.\n \
+ \ Please wrap smaller fields in a struct with padding so all \
+ \ fields are the same size?"
+ subsize
+ cursor
+ )
+ (unCommented a)
+ ]
+ return (max cursor subsize, ret)
+
+exprToSize ::
+ (NumberType stage ~ Integer) =>
+ Expression stage I Annot ->
+ Compile s Integer
+exprToSize (LitNum num _) = return num
+exprToSize e = do
+ tell [Diagnostic Error "Variables not allowed" (unCommented $ annot e)]
+ compilationFailure
+
+lookupTypeSize :: LocalState -> Name I Annot -> Compile GlobalState SizeBits
+lookupTypeSize (LocalState scopePath) (Name idents a) = do
+ -- Convert the list of identifiers to a string path
+ let path = fmap (\(Identifier s _) -> Text.unpack s) idents
+
+ -- Get the current scope and perform the lookup
+ results <- gets $ lookupScopeWithPath scopePath path . globalScope
+
+ case results of
+ -- Successfully resolved to a unique size
+ [(_, Right sz)] -> return sz
+ -- Multiple ambiguous results found
+ matches@(_ : _) -> do
+ -- Generate a list of ambiguous paths for error reporting
+ let ambiguousPaths =
+ map
+ ( \(resolvedPath, _) ->
+ intercalate "." (NonEmpty.toList resolvedPath)
+ )
+ matches
+ tell
+ [ Diagnostic
+ Error
+ ( printf
+ "Ambiguous occurrence of '%s'. Multiple matches found:\n%s"
+ (intercalate "." $ NonEmpty.toList path)
+ (unlines ambiguousPaths) -- List all ambiguous paths
+ )
+ (unCommented a)
+ ]
+ compilationFailure
+
+ -- No matches found
+ _ -> do
+ tell
+ [ Diagnostic
+ Error
+ ( printf
+ "Cannot resolve '%s'. No matching symbols found."
+ (intercalate "." $ NonEmpty.toList path)
+ )
+ (unCommented a)
+ ]
+ compilationFailure
+
+getTypeSize :: BitType Expanded I Annot -> Compile s SizeBits
+getTypeSize (RawBits expr _) = fromIntegral <$> exprToSize expr
+getTypeSize (EnumBitType expr (Identity (EnumBody constants _)) ann) = do
+ declaredSize <- fromIntegral <$> exprToSize expr
+
+ -- If the declared size is less than or equal to 4, we'll enforce that the
+ -- enum is packed. This is to make sure the user has covered all bases.
+ when (declaredSize <= 4) $ do
+ imap <-
+ foldlM
+ ( \imap (undirected -> enumConst) -> do
+ number <- case enumConst of
+ EnumConstantDecl _ expr _ -> exprToSize expr
+ EnumConstantReserved expr _ -> exprToSize expr
+
+ when (number >= 2 ^ declaredSize) $
+ tell
+ [ Diagnostic
+ Error
+ ( printf
+ "Enum constant too large. Max allowed %d\n"
+ ((2 :: Int) ^ declaredSize)
+ )
+ (unCommented (annot enumConst))
+ ]
+
+ return $ IntMap.insert (fromIntegral number) True imap
+ )
+ IntMap.empty
+ constants
+ let missing =
+ filter (not . (`IntMap.member` imap)) [0 .. 2 ^ declaredSize - 1]
+ unless (null missing) $
+ tell
+ [ Diagnostic
+ Warning
+ ( printf
+ "Missing enum constants %s. Small enums should be fully \
+ \ populated. Use 'reserved' if needed."
+ (intercalate ", " (map show missing))
+ )
+ (unCommented ann)
+ ]
+
+ return declaredSize
+
+diagnosticError :: String -> Annot -> Compile a ()
+diagnosticError str a = tell [Diagnostic Error str (unCommented a)]
+
+insertTypeSize ::
+ LocalState ->
+ Identifier f Annot ->
+ SizeBits ->
+ Compile GlobalState ()
+insertTypeSize (LocalState scopePath) (Identifier s annot) size = do
+ modifyM $
+ \(GlobalState globalScope) ->
+ let fullName =
+ NonEmpty.prependList
+ (currentScope scopePath)
+ (NonEmpty.singleton (Text.unpack s))
+ in case upsertScope fullName (Right size) globalScope of
+ (Just _, _) -> do
+ diagnosticError (printf "Duplicate type %s" s) annot
+ compilationFailure
+ (Nothing, n) -> return $ GlobalState n
+ where
+ modifyM fn = do
+ s <- get
+ put =<< fn s