diff --git a/CHANGELOG.md b/CHANGELOG.md index 57f5ddd7b..25ad3e241 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). * Fixed a bug in `SBAs#toCFMPS` which would allow the returned view to reach a final node on a non-return symbol. * Fixed (another) inconsistency bug in `Incremental*DAGBuilder`s. * The `AUTParser` now correctly reads non-deterministic automata. +* The `TAFParsers` now correctly support wildcard transition definitions for DFAs. ## [0.11.0](https://github.com/LearnLib/automatalib/releases/tag/automatalib-0.11.0) - 2023-11-06 diff --git a/serialization/taf/src/main/java/net/automatalib/serialization/taf/parser/AbstractTAFBuilder.java b/serialization/taf/src/main/java/net/automatalib/serialization/taf/parser/AbstractTAFBuilder.java index a2a39c9db..0d7450750 100644 --- a/serialization/taf/src/main/java/net/automatalib/serialization/taf/parser/AbstractTAFBuilder.java +++ b/serialization/taf/src/main/java/net/automatalib/serialization/taf/parser/AbstractTAFBuilder.java @@ -57,12 +57,13 @@ public void init(Alphabet alphabet) { @Override public void declareState(String identifier, Set options) { if (!declaredStates.add(identifier)) { - error("State {0} declared twice", identifier); + error("State {0} declared twice, ignoring properties ...", identifier); + return; } boolean init = options.remove("initial") | options.remove("init"); if (init && automaton.getInitialState() != null) { - error("Duplicate initial state {0}", identifier); + error("Duplicate initial state {0}, ignoring property ...", identifier); init = false; } @@ -85,7 +86,9 @@ public void declareState(String identifier, Set options) { @Override public M finish() { - checkState(); + if (automaton == null) { + throw new IllegalStateException("Must call one of the parse methods first"); + } stateMap.clear(); declaredStates.clear(); @@ -95,12 +98,6 @@ public M finish() { return result; } - protected void checkState() { - if (automaton == null) { - throw new IllegalStateException(); - } - } - protected void error(String msgFmt, Object... args) { parser.error(msgFmt, args); } @@ -125,11 +122,12 @@ protected void doAddTransitions(String source, Collection symbols, Strin T exTrans = automaton.getTransition(src, input); if (exTrans != null) { if (!Objects.equals(tgt, automaton.getSuccessor(exTrans))) { - error("Duplicate transition from {0} on input {1} to differing target {2}" + - " would introduce non-determinism", source, StringUtil.enquoteIfNecessary(input, ID_PATTERN), tgt); + error("Duplicate transition from {0} on input {1} to differing target {2} would introduce non-determinism", + source, + StringUtil.enquoteIfNecessary(input, ID_PATTERN), + tgt); } else if (!Objects.equals(transProperty, automaton.getTransitionProperty(exTrans))) { - error("Duplicate transition from {0} on input {1} to {2} with " + - "differing property '{3}' would introduce non-determinism", + error("Duplicate transition from {0} on input {1} to {2} with differing property '{3}' would introduce non-determinism", source, StringUtil.enquoteIfNecessary(input, ID_PATTERN), tgt, diff --git a/serialization/taf/src/main/java/net/automatalib/serialization/taf/parser/TAFAnyParser.java b/serialization/taf/src/main/java/net/automatalib/serialization/taf/parser/TAFAnyParser.java index f4c4b5e42..85123b507 100644 --- a/serialization/taf/src/main/java/net/automatalib/serialization/taf/parser/TAFAnyParser.java +++ b/serialization/taf/src/main/java/net/automatalib/serialization/taf/parser/TAFAnyParser.java @@ -25,12 +25,13 @@ import net.automatalib.automaton.transducer.impl.CompactMealy; import net.automatalib.common.util.IOUtil; import net.automatalib.exception.FormatException; -import net.automatalib.serialization.ModelDeserializer; +import net.automatalib.serialization.InputModelData; +import net.automatalib.serialization.InputModelDeserializer; -final class TAFAnyParser implements ModelDeserializer> { +final class TAFAnyParser implements InputModelDeserializer> { @Override - public FiniteAlphabetAutomaton readModel(InputStream is) throws IOException, FormatException { + public InputModelData> readModel(InputStream is) throws IOException, FormatException { try (Reader r = IOUtil.asNonClosingUTF8Reader(is)) { final InternalTAFParser parser = new InternalTAFParser(r); @@ -42,13 +43,13 @@ final class TAFAnyParser implements ModelDeserializer, Integer> builder = new DefaultTAFBuilderDFA<>(parser, new CompactDFA.Creator<>()); parser.dfaBody(builder); - return builder.finish(); + return new InputModelData<>(builder.finish(), builder.getAlphabet()); } case MEALY: { final DefaultTAFBuilderMealy, Integer, CompactTransition> builder = new DefaultTAFBuilderMealy<>(parser, new CompactMealy.Creator<>()); parser.mealyBody(builder); - return builder.finish(); + return new InputModelData<>(builder.finish(), builder.getAlphabet()); } default: throw new IllegalStateException("Unknown type " + type); diff --git a/serialization/taf/src/main/java/net/automatalib/serialization/taf/parser/TAFDFAParser.java b/serialization/taf/src/main/java/net/automatalib/serialization/taf/parser/TAFDFAParser.java index 11b94fc15..f3a672056 100644 --- a/serialization/taf/src/main/java/net/automatalib/serialization/taf/parser/TAFDFAParser.java +++ b/serialization/taf/src/main/java/net/automatalib/serialization/taf/parser/TAFDFAParser.java @@ -19,6 +19,7 @@ import java.io.InputStream; import java.io.Reader; +import net.automatalib.alphabet.Alphabet; import net.automatalib.automaton.AutomatonCreator; import net.automatalib.automaton.fsa.MutableDFA; import net.automatalib.common.util.IOUtil; @@ -47,7 +48,8 @@ public InputModelData readModel(InputStream is) throws IOException, F throw new FormatException(ex); } - return new InputModelData<>(builder.finish(), builder.getAlphabet()); + final Alphabet alphabet = builder.getAlphabet(); // finish() will clear the variable + return new InputModelData<>(builder.finish(), alphabet); } } } diff --git a/serialization/taf/src/main/java/net/automatalib/serialization/taf/parser/TAFMealyParser.java b/serialization/taf/src/main/java/net/automatalib/serialization/taf/parser/TAFMealyParser.java index 90736bdc7..09b87e4e2 100644 --- a/serialization/taf/src/main/java/net/automatalib/serialization/taf/parser/TAFMealyParser.java +++ b/serialization/taf/src/main/java/net/automatalib/serialization/taf/parser/TAFMealyParser.java @@ -19,6 +19,7 @@ import java.io.InputStream; import java.io.Reader; +import net.automatalib.alphabet.Alphabet; import net.automatalib.automaton.AutomatonCreator; import net.automatalib.automaton.transducer.MutableMealyMachine; import net.automatalib.common.util.IOUtil; @@ -48,7 +49,8 @@ public InputModelData readModel(InputStream is) throws IOException, F throw new FormatException(ex); } - return new InputModelData<>(builder.finish(), builder.getAlphabet()); + final Alphabet alphabet = builder.getAlphabet(); // finish() will clear the variable + return new InputModelData<>(builder.finish(), alphabet); } } } diff --git a/serialization/taf/src/main/java/net/automatalib/serialization/taf/parser/TAFParsers.java b/serialization/taf/src/main/java/net/automatalib/serialization/taf/parser/TAFParsers.java index c53fda9a8..180fd9030 100644 --- a/serialization/taf/src/main/java/net/automatalib/serialization/taf/parser/TAFParsers.java +++ b/serialization/taf/src/main/java/net/automatalib/serialization/taf/parser/TAFParsers.java @@ -24,7 +24,6 @@ import net.automatalib.automaton.transducer.MutableMealyMachine; import net.automatalib.automaton.transducer.impl.CompactMealy; import net.automatalib.serialization.InputModelDeserializer; -import net.automatalib.serialization.ModelDeserializer; /** * Facade for TAF (textual automaton format) parsing. This class provides several static methods to access @@ -106,7 +105,7 @@ public static > InputM * @see #dfa() * @see #mealy() */ - public static ModelDeserializer> any() { + public static InputModelDeserializer> any() { return new TAFAnyParser(); } } diff --git a/serialization/taf/src/main/javacc/TAF.jj b/serialization/taf/src/main/javacc/TAF.jj index 258f8a405..9a04a3bda 100644 --- a/serialization/taf/src/main/javacc/TAF.jj +++ b/serialization/taf/src/main/javacc/TAF.jj @@ -216,7 +216,7 @@ Set stateOpts(): { [ - s=identifier() { result = new HashSet(); result.add(s); } + s=identifier() { result = new HashSet<>(); result.add(s); } ( s=identifier() { result.add(s); })* ] @@ -269,6 +269,7 @@ void transBlockDfa(TAFBuilderDFA builder, String source): { ( transDeclDfa(builder, source) )* + [ wildcardTransDeclDfa(builder, source) ] } diff --git a/serialization/taf/src/test/java/net/automatalib/serialization/taf/TAFSerializationTest.java b/serialization/taf/src/test/java/net/automatalib/serialization/taf/TAFSerializationTest.java index 690d12b69..b781ff83c 100644 --- a/serialization/taf/src/test/java/net/automatalib/serialization/taf/TAFSerializationTest.java +++ b/serialization/taf/src/test/java/net/automatalib/serialization/taf/TAFSerializationTest.java @@ -23,21 +23,25 @@ import net.automatalib.alphabet.Alphabet; import net.automatalib.alphabet.impl.Alphabets; +import net.automatalib.automaton.Automaton; +import net.automatalib.automaton.FiniteAlphabetAutomaton; import net.automatalib.automaton.MutableDeterministic; -import net.automatalib.automaton.UniversalAutomaton; import net.automatalib.automaton.fsa.DFA; import net.automatalib.automaton.fsa.impl.CompactDFA; +import net.automatalib.automaton.impl.CompactTransition; import net.automatalib.automaton.transducer.MealyMachine; import net.automatalib.automaton.transducer.impl.CompactMealy; import net.automatalib.common.util.io.UnclosableInputStream; import net.automatalib.common.util.io.UnclosableOutputStream; import net.automatalib.exception.FormatException; +import net.automatalib.serialization.InputModelData; import net.automatalib.serialization.InputModelDeserializer; import net.automatalib.serialization.InputModelSerializer; import net.automatalib.serialization.taf.parser.TAFParsers; import net.automatalib.serialization.taf.writer.TAFWriters; import net.automatalib.util.automaton.Automata; import net.automatalib.util.automaton.random.RandomAutomata; +import net.automatalib.word.Word; import org.testng.Assert; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; @@ -83,6 +87,81 @@ public void testMealySerialization() throws IOException, FormatException { Assert.assertTrue(Automata.testEquivalence(this.mealy, deserializedModel, INPUT_ALPHABET)); } + @Test + public void testAnySerializationDFA() throws IOException, FormatException { + final InputModelDeserializer> deserializer = TAFParsers.any(); + final InputModelSerializer> serializer = + TAFWriters.any(); + final FiniteAlphabetAutomaton deserializedModel = + writeAndReadModel(this.dfa, INPUT_ALPHABET, serializer, deserializer); + + Assert.assertNotNull(deserializedModel); + } + + @Test + public void testAnySerializationMealy() throws IOException, FormatException { + final InputModelDeserializer> deserializer = TAFParsers.any(); + final InputModelSerializer>> + serializer = TAFWriters.any(); + final FiniteAlphabetAutomaton deserializedModel = + writeAndReadModel(this.mealy, INPUT_ALPHABET, serializer, deserializer); + + Assert.assertNotNull(deserializedModel); + } + + @Test + public void testParseDFA() throws IOException, FormatException { + + final InputModelDeserializer> parser = TAFParsers.dfa(); + final Alphabet alphabet; + final CompactDFA dfa; + + try (InputStream is = TAFSerializationTest.class.getResourceAsStream("/dfa.taf")) { + final InputModelData> model = parser.readModel(is); + alphabet = model.alphabet; + dfa = model.model; + } + + Assert.assertNotNull(alphabet); + Assert.assertNotNull(dfa); + + Assert.assertEquals(alphabet, Alphabets.closedCharStringRange('a', 'd')); + Assert.assertTrue(dfa.accepts(Word.fromSymbols("a", "b", "c"))); + Assert.assertTrue(dfa.accepts(Word.fromSymbols("a", "b", "c", "a", "b", "c"))); + Assert.assertFalse(dfa.accepts(Word.fromSymbols("a", "a", "b"))); + } + + @Test + public void testParseMealy() throws IOException, FormatException { + + final InputModelDeserializer> parser = TAFParsers.mealy(); + final Alphabet alphabet; + final CompactMealy mealy; + + try (InputStream is = TAFSerializationTest.class.getResourceAsStream("/mealy.taf")) { + final InputModelData> model = parser.readModel(is); + alphabet = model.alphabet; + mealy = model.model; + } + + Assert.assertNotNull(alphabet); + Assert.assertNotNull(mealy); + + Assert.assertEquals(alphabet, Alphabets.fromArray("Hello", "?")); + Assert.assertEquals(mealy.computeOutput(Word.fromSymbols("Hello", "?")), Word.fromSymbols("World", "!")); + Assert.assertEquals(mealy.computeOutput(Word.fromSymbols("?", "?")), Word.fromSymbols("err", "err")); + Assert.assertNull(mealy.getState(Word.fromSymbols("Hello", "Hello"))); + } + + @Test + public void testParseError() throws IOException { + try (InputStream is = TAFSerializationTest.class.getResourceAsStream("/error.taf")) { + Assert.assertThrows(FormatException.class, () -> TAFParsers.dfa().readModel(is)); + Assert.assertThrows(FormatException.class, () -> TAFParsers.mealy().readModel(is)); + Assert.assertThrows(FormatException.class, () -> TAFParsers.any().readModel(is)); + } + } + @Test public void doNotCloseInputOutputStreamDFATest() throws IOException, FormatException { final InputModelDeserializer> deserializer = TAFParsers.dfa(); @@ -110,11 +189,11 @@ public void doNotCloseInputOutputStreamMealyTest() throws IOException, FormatExc } } - private , OUT extends UniversalAutomaton> OUT writeAndReadModel( - IN source, - Alphabet alphabet, - InputModelSerializer serializer, - InputModelDeserializer deserializer) throws IOException, FormatException { + private , OUT extends Automaton> OUT writeAndReadModel(IN source, + Alphabet alphabet, + InputModelSerializer serializer, + InputModelDeserializer deserializer) + throws IOException, FormatException { final ByteArrayOutputStream baos = new ByteArrayOutputStream(); serializer.writeModel(baos, source, alphabet); @@ -123,11 +202,11 @@ public void doNotCloseInputOutputStreamMealyTest() throws IOException, FormatExc return deserializer.readModel(is).model; } - private , OUT extends UniversalAutomaton> OUT writeAndReadUnclosableModel( - IN source, - Alphabet alphabet, - InputModelSerializer serializer, - InputModelDeserializer deserializer) throws IOException, FormatException { + private , OUT extends Automaton> OUT writeAndReadUnclosableModel(IN source, + Alphabet alphabet, + InputModelSerializer serializer, + InputModelDeserializer deserializer) + throws IOException, FormatException { final ByteArrayOutputStream baos = new ByteArrayOutputStream(); serializer.writeModel(new UnclosableOutputStream(baos), source, alphabet); diff --git a/serialization/taf/src/test/resources/dfa.taf b/serialization/taf/src/test/resources/dfa.taf new file mode 100644 index 000000000..e3b301d28 --- /dev/null +++ b/serialization/taf/src/test/resources/dfa.taf @@ -0,0 +1,15 @@ +dfa [a..d] { + init [initial, accepting] { + a -> s1 + * -> sink + } + s1 [initial] { // duplicate initial state, first definition should win + b -> acc + } + acc [accepting] { // duplicate state, first definition should win + * -> init + } + acc { + * -> s1 // duplicate transition, first definition should win + } +} \ No newline at end of file diff --git a/serialization/taf/src/test/resources/error.taf b/serialization/taf/src/test/resources/error.taf new file mode 100644 index 000000000..4fc919e6b --- /dev/null +++ b/serialization/taf/src/test/resources/error.taf @@ -0,0 +1,2 @@ +moore [a..b] { + init [initial] {} \ No newline at end of file diff --git a/serialization/taf/src/test/resources/mealy.taf b/serialization/taf/src/test/resources/mealy.taf new file mode 100644 index 000000000..21c1de516 --- /dev/null +++ b/serialization/taf/src/test/resources/mealy.taf @@ -0,0 +1,15 @@ +mealy {"Hello","?"} { + init [initial] { + "Hello" / "World" -> s1 + * / "err" -> sink + } + s1 { + "?" / "!" -> sink + "?" / "!" -> init // duplicate transition, first definition should win + "?" / "?" -> sink // duplicate output, first definition should win + "asd" / "?!?" -> sink // unknown input symbol + } + sink [sink] { // unknown property, should be ignored + * / "err" -> sink + } +} \ No newline at end of file