packages feed

haal 0.4.0.2 → 0.4.1.0

raw patch · 6 files changed

+460/−8 lines, 6 filesdep ~basenew-component:exe:haal-genPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependency ranges changed: base

API changes (from Hackage documentation)

- Haal.EquivalenceOracle.WMethod: data WMethodConfig
+ Haal.Dot: ParsedMealy :: String -> [String] -> [String] -> [String] -> [(String, String, String, String)] -> [String] -> ParsedMealy
+ Haal.Dot: [parsedInitState] :: ParsedMealy -> String
+ Haal.Dot: [parsedInputs] :: ParsedMealy -> [String]
+ Haal.Dot: [parsedOutputs] :: ParsedMealy -> [String]
+ Haal.Dot: [parsedStates] :: ParsedMealy -> [String]
+ Haal.Dot: [parsedTrans] :: ParsedMealy -> [(String, String, String, String)]
+ Haal.Dot: [parsedWarnings] :: ParsedMealy -> [String]
+ Haal.Dot: data ParsedMealy
+ Haal.Dot: generateModule :: String -> String -> ParsedMealy -> Either String String
+ Haal.Dot: instance GHC.Show.Show Haal.Dot.ParsedMealy
+ Haal.Dot: mealyToDot :: (FiniteOrd s, FiniteOrd i, Show s, Show i, Show o) => MealyAutomaton s i o -> Either String String
+ Haal.Dot: parseDot :: String -> Either String ParsedMealy
+ Haal.EquivalenceOracle.WMethod: newtype WMethodConfig

Files

CHANGELOG.md view
@@ -8,6 +8,16 @@  ## Unreleased +## 0.4.1.0 - 2026-03-21++### Added +- Serializer from Mealy Automaton to Dot format.+- haal-gen executable that accepts a .dot file and produces a haskell module+  that exports a function for the specified Mealy Automaton, with specific+  input and output types, rather than just using String for both.+- haal-models subpackage that exports learned models of tls, mqtt, tcp and dtls+  protocols.+ ## 0.4.0.2 - 2026-03-17  ### Changed 
+ app/HaalGen.hs view
@@ -0,0 +1,60 @@+-- | haal-gen: generate a Haskell module from a Mealy automaton in DOT format.+--+-- Usage:+--   haal-gen <dotfile> <ModuleName> <valueName>+--+-- The generated module is written to stdout.+module Main (main) where++import System.Environment (getArgs)+import System.Exit (exitFailure, exitSuccess)+import System.IO (hPutStrLn, stderr)++import Haal.Dot (parseDot, generateModule, parsedWarnings)++helpText :: String+helpText = unlines+    [ "haal-gen - generate a Haskell module from a Mealy automaton in DOT format"+    , ""+    , "Usage:"+    , "  haal-gen <dotfile> <ModuleName> <valueName>"+    , ""+    , "Arguments:"+    , "  <dotfile>     Path to a DOT file (AALpy or LearnLib format)"+    , "  <ModuleName>  Fully qualified Haskell module name (e.g. My.Module.Name)"+    , "  <valueName>   Name for the generated automaton value (e.g. myAutomaton)"+    , ""+    , "Output:"+    , "  Haskell source is written to stdout; warnings and errors to stderr."+    , "  The generated module contains typed input/output ADTs and a"+    , "  MealyAutomaton value, ready to import and use with haal."+    , ""+    , "Example:"+    , "  haal-gen model.dot Haal.Models.Foo fooModel > src/Haal/Models/Foo.hs"+    ]++main :: IO ()+main = do+    args <- getArgs+    case args of+        ["--help"] -> putStr helpText >> exitSuccess+        [dotFile, modName, valName] -> run dotFile modName valName+        _ -> do+            hPutStrLn stderr "Usage: haal-gen <dotfile> <ModuleName> <valueName>"+            hPutStrLn stderr "Run 'haal-gen --help' for more information."+            exitFailure++run :: FilePath -> String -> String -> IO ()+run dotFile modName valName = do+    src <- readFile dotFile+    case parseDot src of+        Left err -> do+            hPutStrLn stderr ("haal-gen: parse error: " ++ err)+            exitFailure+        Right pm -> do+            mapM_ (hPutStrLn stderr . ("haal-gen: warning: " ++)) (parsedWarnings pm)+            case generateModule modName valName pm of+                Left err  -> do+                    hPutStrLn stderr ("haal-gen: " ++ err)+                    exitFailure+                Right hsrc -> putStr hsrc
haal.cabal view
@@ -1,11 +1,11 @@ cabal-version: 2.2 --- This file has been generated from package.yaml by hpack version 0.38.1.+-- This file has been generated from package.yaml by hpack version 0.37.0. -- -- see: https://github.com/sol/hpack  name:           haal-version:        0.4.0.2+version:        0.4.1.0 synopsis:       A Haskell library for Active Automata Learning. description:    Please see the README on GitHub at <https://github.com/steve-anunknown/haal#readme> category:       Model Learning@@ -37,6 +37,7 @@       Haal.Automaton.MealyAutomaton       Haal.Automaton.MooreAutomaton       Haal.BlackBox+      Haal.Dot       Haal.EquivalenceOracle.CombinedOracle       Haal.EquivalenceOracle.RandomWalk       Haal.EquivalenceOracle.RandomWords@@ -91,6 +92,20 @@       base     , haal     , mtl+  default-language: Haskell2010++executable haal-gen+  main-is: HaalGen.hs+  other-modules:+      Paths_haal+  autogen-modules:+      Paths_haal+  hs-source-dirs:+      app+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints+  build-depends:+      base+    , haal   default-language: Haskell2010  executable io
+ src/Haal/Dot.hs view
@@ -0,0 +1,367 @@+{- | Serialization, parsing, and code generation for Mealy automata in DOT+format, following the convention used by AALpy and LearnLib.+-}+module Haal.Dot (+    mealyToDot,+    ParsedMealy (..),+    parseDot,+    generateModule,+) where++import Data.Char (isAlphaNum, isDigit, isLower, isSpace, toUpper)+import Data.List (intercalate, isInfixOf, isPrefixOf, nub)+import qualified Data.Map.Strict as Map+import Data.Maybe (mapMaybe)+import qualified Data.Set as Set++import Haal.Automaton.MealyAutomaton (MealyAutomaton, mealyTransitions)+import Haal.BlackBox (Automaton (..), FiniteOrd, initial)++-- ---------------------------------------------------------------------------+-- Serializer+-- ---------------------------------------------------------------------------++{- | Serialize a 'MealyAutomaton' to a DOT format string.++  The output follows the AALpy\/LearnLib convention:++  * States are rendered as @s0@, @s1@, … in ascending order, with their+    'show' representation as the node label.+  * Edge labels have the form @\"input\/output\"@.+  * The initial state is indicated by a @__start0@ dummy node.++  Returns @'Left' err@ if any input or output symbol's 'show' representation+  contains @\'\/\'@, as that would make the DOT file impossible to parse back+  correctly.+-}+mealyToDot ::+    (FiniteOrd s, FiniteOrd i, Show s, Show i, Show o) =>+    MealyAutomaton s i o ->+    Either String String+mealyToDot m = do+    let badInputs = filter ('/' `elem`) (map (show . snd) (Map.toList inputSet))+        badOutputs = filter ('/' `elem`) (map (show . snd . snd) (Map.toList trans))+    case (badInputs, badOutputs) of+        (i : _, _) -> Left ("Input label contains '/': " ++ i)+        (_, o : _) -> Left ("Output label contains '/': " ++ o)+        _ ->+            Right $+                unlines $+                    ["digraph haal {"]+                        ++ zipWith (curry nodeDecl) [0 :: Int ..] sortedStates+                        ++ map edgeDecl (Map.toList trans)+                        ++ [ "\t__start0 [label=\"\" shape=none];"+                           , "\t__start0 -> " ++ nodeId (initial m) ++ " [label=\"\"];"+                           , "}"+                           ]+  where+    sortedStates = Set.toAscList (states m)+    stateIndex = Map.fromList (zip sortedStates [0 :: Int ..])+    trans = mealyTransitions m+    inputSet = Map.fromList [((s, i), i) | (s, i) <- Map.keys trans]++    nodeId s = "s" ++ show (stateIndex Map.! s)++    nodeDecl (i, s) =+        "\ts" ++ show i ++ " [label=\"" ++ show s ++ "\"];"++    edgeDecl ((s, i), (s', o)) =+        "\t"+            ++ nodeId s+            ++ " -> "+            ++ nodeId s'+            ++ " [label=\""+            ++ show i+            ++ "/"+            ++ show o+            ++ "\"];"++-- ---------------------------------------------------------------------------+-- Parser+-- ---------------------------------------------------------------------------++{- | The raw representation of a Mealy automaton parsed from DOT format.+  State, input, and output names are kept as 'String's.+  States are ordered with the initial state first, then others in order of+  first appearance in the transition list.+-}+data ParsedMealy = ParsedMealy+    { parsedInitState :: String+    , parsedStates :: [String]+    , parsedInputs :: [String]+    , parsedOutputs :: [String]+    , parsedTrans :: [(String, String, String, String)]+    -- ^ @(srcState, input, dstState, output)@+    , parsedWarnings :: [String]+    }+    deriving (Show)++{- | Parse a DOT format string representing a Mealy automaton.++  Supports the output format of both AALpy and LearnLib:++  * Initial state indicated by @__start0 -> \<state\>@+  * Edge labels of the form @\"input\/output\"@ or @\"input \/ output\"@+    (whitespace around @\/@ is stripped)++  Returns @'Left' err@ with a descriptive message on failure.+-}+parseDot :: String -> Either String ParsedMealy+parseDot src = do+    let ls = lines src+    initSt <- findInit ls+    trans <- collectTrans ls+    let allStates = nub $ concatMap (\(s, _, d, _) -> [s, d]) trans+    if initSt `notElem` allStates+        then Left ("Initial state '" ++ initSt ++ "' does not appear in any transition")+        else do+            let otherStates = filter (/= initSt) allStates+                stateOrder = initSt : otherStates+                inputSyms = nub $ map (\(_, i, _, _) -> i) trans+                outputSyms = nub $ map (\(_, _, _, o) -> o) trans+                warnings = slashWarnings inputSyms outputSyms+            return+                ParsedMealy+                    { parsedInitState = initSt+                    , parsedStates = stateOrder+                    , parsedInputs = inputSyms+                    , parsedOutputs = outputSyms+                    , parsedTrans = trans+                    , parsedWarnings = warnings+                    }++-- ---------------------------------------------------------------------------+-- Code generator+-- ---------------------------------------------------------------------------++{- | Generate a Haskell module from a 'ParsedMealy'.++  @generateModule modName valName pm@ produces source for a module named+  @modName@ containing:++  * A @data \<modName\>Input@ type whose constructors are the sanitized input+    symbols, deriving @Show, Eq, Ord, Enum, Bounded@.+  * A @data \<modName\>Output@ type, similarly for output symbols.+  * A value @valName :: MealyAutomaton Int \<modName\>Input \<modName\>Output@.++  Returns @'Left' err@ if two distinct symbols sanitize to the same+  constructor name.+-}+generateModule :: String -> String -> ParsedMealy -> Either String String+generateModule modName valName pm = do+    inputCons <- sanitizeAll "In_" "input" (parsedInputs pm)+    outputCons <- sanitizeAll "Out_" "output" (parsedOutputs pm)+    let stateNames = parsedStates pm+        n = length stateNames+        stateIdx = Map.fromList (zip stateNames [0 :: Int ..])+        inputConMap = Map.fromList (zip (parsedInputs pm) inputCons)+        outputConMap = Map.fromList (zip (parsedOutputs pm) outputCons)+        modSuffix  = reverse . takeWhile (/= '.') . reverse $ modName+        inputType  = modSuffix ++ "Input"+        outputType = modSuffix ++ "Output"+        deltaLines = map (mkDeltaLine stateIdx inputConMap) (parsedTrans pm)+        lambdaLines = map (mkLambdaLine stateIdx inputConMap outputConMap) (parsedTrans pm)+    return $+        unlines $+            [ "-- Generated by haal-gen. Do not edit manually."+            , "module " ++ modName+            , "    ( " ++ inputType ++ " (..)"+            , "    , " ++ outputType ++ " (..)"+            , "    , " ++ valName+            , "    ) where"+            , ""+            , "import qualified Data.Set as Set"+            , "import Haal.Automaton.MealyAutomaton (MealyAutomaton, mkMealyAutomaton)"+            , ""+            , "data " ++ inputType+            ]+                ++ enumDecl inputCons+                ++ [ ""+                   , "data " ++ outputType+                   ]+                ++ enumDecl outputCons+                ++ [ ""+                   , valName ++ " :: MealyAutomaton Int " ++ inputType ++ " " ++ outputType+                   , valName+                        ++ " = mkMealyAutomaton delta lambda (Set.fromList [0.."+                        ++ show (n - 1)+                        ++ "]) 0"+                   , "  where"+                   ]+                ++ map ("    " ++) deltaLines+                ++ ["    delta _ _ = error \"haal-gen: undefined transition\""]+                ++ map ("    " ++) lambdaLines+                ++ ["    lambda _ _ = error \"haal-gen: undefined transition\""]++-- ---------------------------------------------------------------------------+-- Code generation helpers+-- ---------------------------------------------------------------------------++enumDecl :: [String] -> [String]+enumDecl [] = ["    deriving (Show, Eq, Ord, Enum, Bounded)"]+enumDecl (c : cs) =+    ["    = " ++ c]+        ++ map ("    | " ++) cs+        ++ ["    deriving (Show, Eq, Ord, Enum, Bounded)"]++mkDeltaLine ::+    Map.Map String Int ->+    Map.Map String String ->+    (String, String, String, String) ->+    String+mkDeltaLine stateIdx inputConMap (src, inp, dst, _) =+    "delta " ++ show si ++ " " ++ ic ++ " = " ++ show di+  where+    si = stateIdx Map.! src+    di = stateIdx Map.! dst+    ic = inputConMap Map.! inp++mkLambdaLine ::+    Map.Map String Int ->+    Map.Map String String ->+    Map.Map String String ->+    (String, String, String, String) ->+    String+mkLambdaLine stateIdx inputConMap outputConMap (src, inp, _, out) =+    "lambda " ++ show si ++ " " ++ ic ++ " = " ++ oc+  where+    si = stateIdx Map.! src+    ic = inputConMap Map.! inp+    oc = outputConMap Map.! out++{- | Sanitize a list of symbols to valid Haskell constructor names using the+  given prefix, failing if two distinct symbols would produce the same name.+-}+sanitizeAll :: String -> String -> [String] -> Either String [String]+sanitizeAll prefix kind syms =+    let sanitized = map (sanitizeName prefix) syms+        byConName = Map.fromListWith (++) (zip sanitized (map (: []) syms))+        collisions =+            [ (con, originals)+            | (con, originals) <- Map.toList byConName+            , length originals > 1+            ]+     in case collisions of+            [] -> Right sanitized+            cs ->+                Left $+                    "Colliding "+                        ++ kind+                        ++ " constructor names:\n"+                        ++ unlines+                            [ "  "+                                ++ intercalate ", " (map show originals)+                                ++ " → "+                                ++ con+                            | (con, originals) <- cs+                            ]++{- | Sanitize a symbol string to a valid Haskell constructor name:+  replace non-alphanumeric characters with @_@, strip leading\/trailing+  underscores, capitalise the first character, prefix with @N@ if it+  starts with a digit, and prepend the given prefix.+-}+sanitizeName :: String -> String -> String+sanitizeName prefix s = prefix ++ base+  where+    s1 = map (\c -> if isAlphaNum c then c else '_') s+    s2 = reverse (dropWhile (== '_') (reverse (dropWhile (== '_') s1)))+    s3 = if null s2 then "Unknown" else s2+    base = case s3 of+        (c : cs)+            | isLower c -> toUpper c : cs+            | isDigit c -> 'N' : s3+            | otherwise -> s3+        [] -> "Unknown"++-- ---------------------------------------------------------------------------+-- Parser helpers+-- ---------------------------------------------------------------------------++slashWarnings :: [String] -> [String] -> [String]+slashWarnings inputs outputs =+    [ "Input symbol contains '/': \"" ++ s ++ "\" — label may have been misparsed"+    | s <- inputs+    , '/' `elem` s+    ]+        ++ [ "Output symbol contains '/': \"" ++ s ++ "\" — label may have been misparsed"+           | s <- outputs+           , '/' `elem` s+           ]++findInit :: [String] -> Either String String+findInit ls =+    case mapMaybe extractInit ls of+        [] -> Left "No initial state marker found (expected '__start0 -> <state>')"+        (s : _) -> Right s+  where+    extractInit l+        | "__start0" `isInfixOf` l && "->" `isInfixOf` l =+            let afterArrow = trim $ drop 2 $ snd $ breakOn "->" l+                name = takeWhile isStateChar afterArrow+             in if null name then Nothing else Just name+        | otherwise = Nothing++collectTrans :: [String] -> Either String [(String, String, String, String)]+collectTrans ls = concat <$> mapM process relevantLines+  where+    relevantLines = filter isEdgeLine ls+    isEdgeLine l = "->" `isInfixOf` l && not ("__start0" `isInfixOf` l)+    process l =+        case extractLabel l of+            Nothing -> Right []+            Just "" -> Right []+            Just lbl -> case splitLabel lbl of+                Left err -> Left ("Malformed label \"" ++ lbl ++ "\": " ++ err)+                Right (inp, out) ->+                    case parseEndpoints l of+                        Nothing -> Left ("Could not parse endpoints in: " ++ trim l)+                        Just (src, dst) -> Right [(src, inp, dst, out)]++extractLabel :: String -> Maybe String+extractLabel l =+    case findSubstr "label=" l of+        Nothing -> Nothing+        Just after ->+            case dropWhile isSpace after of+                '"' : rest -> Just $ takeWhile (/= '"') rest+                rest -> Just $ takeWhile (\c -> c /= ',' && c /= ']' && not (isSpace c)) rest++parseEndpoints :: String -> Maybe (String, String)+parseEndpoints l =+    let (srcPart, rest) = breakOn "->" l+        src = trim srcPart+        afterArrow = trim (drop 2 rest)+        dst = trim $ takeWhile (\c -> c /= '[' && c /= ';') afterArrow+     in if null src || null dst then Nothing else Just (src, dst)++splitLabel :: String -> Either String (String, String)+splitLabel lbl =+    case breakOn "/" lbl of+        (_, []) -> Left "missing '/' separator"+        (inp, _ : out) ->+            let i = trim inp+                o = trim out+             in if null i+                    then Left "empty input"+                    else Right (i, o)++findSubstr :: String -> String -> Maybe String+findSubstr _ [] = Nothing+findSubstr needle haystack@(_ : xs)+    | needle `isPrefixOf` haystack = Just (drop (length needle) haystack)+    | otherwise = findSubstr needle xs++breakOn :: String -> String -> (String, String)+breakOn _ [] = ([], [])+breakOn needle haystack@(x : xs)+    | needle `isPrefixOf` haystack = ([], haystack)+    | otherwise =+        let (pre, rest) = breakOn needle xs+         in (x : pre, rest)++trim :: String -> String+trim = reverse . dropWhile isSpace . reverse . dropWhile isSpace++isStateChar :: Char -> Bool+isStateChar c = isAlphaNum c || c == '_'
src/Haal/EquivalenceOracle/WMethod.hs view
@@ -21,7 +21,7 @@ import System.Random (Random (randomR), StdGen)  -- | The 'WMethodConfig' type is used to configure the W-method equivalence oracle.-data WMethodConfig = WMethodConfig+newtype WMethodConfig = WMethodConfig     { wmDepth :: Int     -- ^ The number of extra states beyond the hypothesis to account for.     }
src/Haal/Learning/LMstar.hs view
@@ -54,7 +54,7 @@ {-@ data ObservationTable i o = ObservationTable     { prefixSetS  :: Set.Set [i]     , suffixSetE  :: Set.Set {v:[i] | len v > 0}-    , mappingT    :: Map.Map ([i], [i]) {v:[o] | len v > 0}+    , mappingT    :: Map.Map ([i], {v:[i] | len v > 0}) {v:[o] | len v > 0}     , prefixSetSI :: Set.Set {v:[i] | len v > 0}     } @-} data ObservationTable i o = ObservationTable@@ -412,9 +412,9 @@ {-@ insertStep       :: (Ord i, SUL sul m, Monad m)       => sul i o-      -> Map.Map ([i],[i]) {v:[o] | len v > 0}+      -> Map.Map ([i],{v:[i] | len v > 0}) {v:[o] | len v > 0}       -> ([i], {b:[i] | len b > 0})-      -> m (Map.Map ([i],[i]) {v:[o] | len v > 0}) @-}+      -> m (Map.Map ([i],{v:[i] | len v > 0}) {v:[o] | len v > 0}) @-} insertStep ::     (Ord i, SUL sul m, Monad m) =>     sul i o ->@@ -430,10 +430,10 @@  {-@ updateMap       :: (Ord i, SUL sul m, Monad m)-      => Map.Map ([i],[i]) {v:[o] | len v > 0}+      => Map.Map ([i],{v:[i] | len v > 0}) {v:[o] | len v > 0}       -> Set.Set ([i], {v:[i] | len v > 0})       -> sul i o-      -> m (Map.Map ([i],[i]) {v:[o] | len v > 0}) @-}+      -> m (Map.Map ([i],{v:[i] | len v > 0}) {v:[o] | len v > 0}) @-} updateMap ::     (Ord i, SUL sul m, Monad m) =>     Map.Map ([i], [i]) [o] ->