From f8c7b8d76689b4ef989af2d32d0054f8bb5138d7 Mon Sep 17 00:00:00 2001 From: Joshua Moerman Date: Fri, 18 Oct 2024 16:36:36 +0200 Subject: [PATCH] A little bit more on Mealy machines and updates the README --- README.md | 41 +++++++++++----------- test/Spec.hs | 99 ++++++++++++++++++++++++++++++++++++---------------- 2 files changed, 89 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 79139cb..d32abbe 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,13 @@ Learning Nominal Automata ========================= -*NOTE*: Please download the archive `popl-artifact.zip`. This contains the -specific versions (of this and nlambda) used for the POPL submission. -This archive should contain a similar README, with simpler instructions. -If you want the newest version of the software, then don't use that -archive, but use the code in this repository. - - # Dependencies This artifact was tested on a Debian system. During development both Mac and Windows have been used, so it should work on these operating systems too. Note that you will need the Z3 solver (as executable). The algorithms are implemented in Haskell and you will need a recent GHC (at least 7.10). +Currently, GHC 9.10 is used in the development. We use the library [nlambda](https://github.com/szynwelski/nlambda). It is recommended to use the most recent version. Just grab the source and @@ -26,24 +20,28 @@ Follow the build guide on their website. # Building -You can use the stack tool. Make sure to include nlambda as a package. -It should be a matter of `stack build`, if not, stack will probably -tell you what to do. (If you need any help, send me a message.) +The `cabal` build tool should suffice. Please put both this repository and +the nlambda repository somehwere, and add a `cabal.project` file with the +following contents: +``` +packages: nominal-lstar nlambda +``` +Then a simple `cabal build all` should do it. To test whether everything +works, run `cabal test nominal-lstar`. (Please get in touch if you have +trouble setting it up.) # Running -Stack will produce a binary in the `.stack-works` directory, which can -be invoked directly. Alternatively one can run `stack exec nominal-lstar`. -There is two modes of operation: Running the examples, or running it -interactively. +Run `cabal run nominal-lstar` within the `nominal-lstar` directory. ## Examples -The executable expects three arguments: +The executable expects one or three arguments: ``` -stack exec NominalAngluin -- +cabal run nominal-lstar +cabal run nominal-lstar ``` There are three learners: @@ -75,7 +73,7 @@ stack data structure): For example: ``` -stack exec nominal-lstar -- NomLStar EqDFA "Fifo 2" +cabal run nominal-lstar NomLStar EqDFA "Fifo 2" ``` The program will output all the intermediate hypotheses. And will terminate @@ -95,7 +93,7 @@ We proved by hand that the learnt model did indeed accept the language. Run the tool like so: ``` -stack exec nominal-lstar -- +cabal run nominal-lstar ``` (So similar to the above case, but without specifying the equivalence checker and example.) The tool will ask you membership queries and @@ -122,7 +120,7 @@ A: True Q: [0] A: True Automaton {states = {{([],True)}}, alphabet = {a₁ : for a₁ ∊ 𝔸}, delta = {({([],True)},a₁,{([],True)}) : for a₁ ∊ 𝔸}, initialStates = {{([],True)}}, finalStates = {{([],True)}}} -3. Equivalent? +3. Equivalent? # Is the following automaton correct? # Automaton {states = {{([],True)}}, alphabet = {a₁ : for a₁ ∊ 𝔸}, delta = {({([],True)},a₁,{([],True)}) : for a₁ ∊ 𝔸}, initialStates = {{([],True)}}, finalStates = {{([],True)}}} @@ -142,11 +140,14 @@ A: True Q: [1,0] A: False Q: [1,0,1] -A: +A: ``` # Changes since first release +The original version of the tool, presented at POPL, is commit e1b00e1 (from +2016). Since then, some new features are implemented: + * Better support for interactive communication. * Optimisation: add only one row/column to fix closedness/consistency * Simpler observation table diff --git a/test/Spec.hs b/test/Spec.hs index 6121ad8..3940adb 100644 --- a/test/Spec.hs +++ b/test/Spec.hs @@ -14,25 +14,38 @@ import Test.Tasty import Test.Tasty.HUnit main :: IO () -main = defaultMain (testGroup "Unit tests" unitTests) +main = defaultMain (testGroup "Unit tests" [dfaLearning, mealyBasics]) where - unitTests = - [ testCase "Learning DFA DW1" $ learnAndCompare (runningExample atoms 1) @?= True - , testCase "Learning DFA DW2" $ learnAndCompare (runningExample atoms 2) @?= True - , testCase "Mealy deterministic echo" $ isTrue (mealyIsDeterministic echoMachine) @?= True - , testCase "Mealy deterministic memory True" $ isTrue (mealyIsDeterministic (memoryMachine True)) @?= True - , testCase "Mealy deterministic memory False" $ isTrue (mealyIsDeterministic (memoryMachine False)) @?= True - , testCase "Mealy enabled echo" $ isTrue (mealyIsEnabled echoMachine) @?= True - , testCase "Mealy enabled memory True" $ isTrue (mealyIsEnabled (memoryMachine True)) @?= True - , testCase "Mealy enabled memory False" $ isTrue (mealyIsEnabled (memoryMachine False)) @?= True - ] + dfaLearning = + testGroup + "DFA Learning" + [ testCase "Learning DFA DW1" $ learnDFAAndCompare (runningExample atoms 1) @?= True + , testCase "Learning DFA DW2" $ learnDFAAndCompare (runningExample atoms 2) @?= True + ] + mealyBasics = + testGroup + "Mealy Basics" + [ testCase "Mealy deterministic echo" $ isTrue (mealyIsDeterministic echoMachine) @?= True + , testCase "Mealy deterministic memory True" $ isTrue (mealyIsDeterministic (memoryMachine True)) @?= True + , testCase "Mealy deterministic memory False" $ isTrue (mealyIsDeterministic (memoryMachine False)) @?= True + , testCase "Mealy enabled echo" $ isTrue (mealyIsEnabled echoMachine) @?= True + , testCase "Mealy enabled memory True" $ isTrue (mealyIsEnabled (memoryMachine True)) @?= True + , testCase "Mealy enabled memory False" $ isTrue (mealyIsEnabled (memoryMachine False)) @?= True + , testCase "Mealy reachable echo" $ isTrue (mealyReachable echoMachine `eq` singleton ()) @?= True + , testCase "Mealy reachable memory True" $ isTrue (mealyReachable (memoryMachine True) `eq` (singleton Nothing `union` NLambda.map Just atoms)) @?= True + , testCase "Mealy reachable memory False" $ isTrue (mealyReachable (memoryMachine False) `eq` (singleton Nothing `union` NLambda.map Just atoms)) @?= True + ] -learnAndCompare :: (Contextual i, Show i, Nominal i, Nominal q1) => Automaton q1 i -> Bool -learnAndCompare target = equivalenceCheck target learnedModel +learnDFAAndCompare :: (Contextual i, Show i, Nominal i, Nominal q1) => Automaton q1 i -> Bool +learnDFAAndCompare target = equivalenceCheck target learnedModel where learnedModel = learnAngluin (teacherWithTarget target) equivalenceCheck m1 m2 = isTrue . isEmpty $ bisim m1 m2 +{- ******************************************************************* + Basic data structure for Nominal Mealy machines and basic functions to + check whether they are deterministic and input-enabled. +-} data MealyMachine s i o = MealyMachine { mealyInitialState :: s , mealyStates :: Set s @@ -47,27 +60,51 @@ mealyIsDeterministic m = forAll (\s -> forAll (\i -> let ot = mapFilter ((\(s2, mealyIsEnabled :: _ => MealyMachine s i o -> Formula mealyIsEnabled m = forAll (\s -> forAll (\i -> let ot = mapFilter ((\(s2, i2, o, t) -> maybeIf ((s, i) `eq` (s2, i2)) (o, t))) (mealyTransitions m) in isNotEmpty ot) (mealyInputs m)) (mealyStates m) -echoMachine :: MealyMachine () Atom Atom -echoMachine = MealyMachine - { mealyInitialState = () - , mealyStates = singleton () - , mealyInputs = atoms - , mealyTransitions = NLambda.map (\a -> ((), a, a, ())) atoms - } +-- Computes all reachable states +mealyReachable :: _ => MealyMachine s i o -> Set s +mealyReachable m = go (singleton (mealyInitialState m)) empty + where + go todo visited = + ite (isEmpty todo) + {- then -} visited + {- else -} (let v = visited `union` todo + s = successors todo + in go (s \\ v) v) + successors todo = NLambda.sum (pairsWith (\s i -> mapFilter ((\(s2, i2, _, t) -> maybeIf ((s, i) `eq` (s2, i2)) t)) (mealyTransitions m)) todo (mealyInputs m)) +{- ******************************************************************* + EXAMPLE MEALY MACHINES +-} + +-- Immediately echoes the input (of type Atom) +echoMachine :: MealyMachine () Atom Atom +echoMachine = + MealyMachine + { mealyInitialState = () + , mealyStates = singleton () + , mealyInputs = atoms + , mealyTransitions = NLambda.map (\a -> ((), a, a, ())) atoms + } + +-- The next example is a memory cell to store a single atom. You can Put +-- or Get the values. Depending on the mode, the machine can forget its +-- value after a Get operation. data MIn a = MPut a | MGet - deriving (Eq, Ord, Show, Generic, Nominal, Contextual) + deriving (Eq, Ord, Show, Generic, Nominal, Contextual) data MOut a = MNok | MOk | MVal a - deriving (Eq, Ord, Show, Generic, Nominal, Contextual) + deriving (Eq, Ord, Show, Generic, Nominal, Contextual) memoryMachine :: Bool -> MealyMachine (Maybe Atom) (MIn Atom) (MOut Atom) -memoryMachine forget = MealyMachine - { mealyInitialState = Nothing - , mealyStates = allStates - , mealyInputs = singleton MGet `union` NLambda.map MPut atoms - , mealyTransitions = NLambda.pairsWith (\q a -> (q, MPut a, MOk, Just a)) allStates atoms `union` - NLambda.map (\a -> (Just a, MGet, MVal a, if forget then Nothing else Just a)) atoms `union` - NLambda.singleton (Nothing, MGet, MNok, Nothing) - } - where allStates = singleton Nothing `union` (NLambda.map Just atoms) +memoryMachine forget = + MealyMachine + { mealyInitialState = Nothing + , mealyStates = allStates + , mealyInputs = singleton MGet `union` NLambda.map MPut atoms + , mealyTransitions = + NLambda.pairsWith (\q a -> (q, MPut a, MOk, Just a)) allStates atoms + `union` NLambda.map (\a -> (Just a, MGet, MVal a, if forget then Nothing else Just a)) atoms + `union` NLambda.singleton (Nothing, MGet, MNok, Nothing) + } + where + allStates = singleton Nothing `union` (NLambda.map Just atoms)