Repository: textX/textX (language: python)

Issue:
unordered group question
According to http://textx.github.io/textX/stable/grammar/#repetitions an unordered group must be applied to a sequence.

However, in https://github.com/textX/textX/blob/master/examples/pyFlies/pyflies.tx#L168 it is applied to an ordered choice.

Maybe we should clarify this in the docs...? Or did I misunderstood something?

Once we clarify this (how it should be) I can make a proposition for the docs. I could also elaborate on how optionals are handled in unordered groups...


Discussion / hints:

Fix patch:
diff --git a/docs/grammar.md b/docs/grammar.md
index b9678686e..d23adaf9c 100644
--- a/docs/grammar.md
+++ b/docs/grammar.md
@@ -47,11 +47,11 @@ depicted in the following figure:
 * `INT` rule: matches an integer number. This match will be converted to
   a Python `int` instance.
 * `FLOAT` rule: will match a floating point number. This match will be converted
-  to a Python `float` instance ('FLOAT' is a direct subtype of 'BASETYPE'; 
+  to a Python `float` instance ('FLOAT' is a direct subtype of 'BASETYPE';
   in order to distinguish floats and ints, 'STRICTFLOAT' was introduced).
-* `STRICTFLOAT` rule: will match a floating point number. This match will be 
-  converted to a Python `float` instance. A 'STRICTFLOAT' will not match an 'INT' 
-  (without "." or "e|E"). A 'NUMBER' is either a 'STRICTFLOAT' or an 'INT', 
+* `STRICTFLOAT` rule: will match a floating point number. This match will be
+  converted to a Python `float` instance. A 'STRICTFLOAT' will not match an 'INT'
+  (without "." or "e|E"). A 'NUMBER' is either a 'STRICTFLOAT' or an 'INT',
   and will, thus, be converted to a float or an int, respectively.
 * `BOOL` rule: matches the words `true` or `false`. This match
   will be converted to a Python `bool` instance.
@@ -262,8 +262,8 @@ Now, an ordered choice in the parentheses is optional.
 
 * **Unordered group** is a special kind of a sequence. Syntactically it is
   similar to a repetition. It is specified by the `#` operator and must be
-  applied to sequences. This operator will match each element of the sequence in
-  an arbitrary order:
+  applied to either sequences or ordered choices. This operator will match each
+  element of the sequence or the ordered choice in an arbitrary order:
 
         Colors:
           ("red" "green" "blue")#
@@ -280,10 +280,10 @@ Now, an ordered choice in the parentheses is optional.
 
         red blue red green
         blue green
-        
+
     Consider this example:
 
-        Modifier: 
+        Modifier:
             (static?='static' final?='final' visibility=Visibility)#
         ;
 
@@ -300,12 +300,38 @@ Now, an ordered choice in the parentheses is optional.
         public static
         final protected static
         ...
-        
+
+    You can combine unordered groups with parenthesized groups. Lets look at the
+    following example:
+
+        Unordered:
+          (('first' 'second')  'third')#
+        ;
+
+    This will match group with sequence `first second` and `third` in arbitrary
+    order but the sequence `first second` maintains the order. Thus, these
+    inputs will match:
+
+        first second third
+        third first second
+
+    But these won't:
+
+        third second first
+        second first third
+
+    Alternatively, you can use ordered choice instead of grouping. This will be
+    equivalent with the previous example:
+
+        Unordered:
+          ('first' 'second' | 'third')#
+        ;
+
     !!! note
         Unordered group may also have
         [repetition modifiers](#repetition-modifiers) defined.
 
-        
+
 ### Assignments
 
 Assignments are used as a part of the meta-model deduction process. Each
@@ -326,7 +352,7 @@ assignments are not given in this example.
 This example describes the rule and meta-class `Person`, that will parse and
 instantiate the `Person` objects with  these four attributes:
 
-* `name` - which will use the rule `Name` to match the input, it 
+* `name` - which will use the rule `Name` to match the input, it
   will be a reference to the instance of the `Name` class,
 * `surname` - will use `Surname` rule to match the input,
 * `age` - will use the built-in type `INT` to match a number from the
@@ -491,13 +517,13 @@ assignments. There are two types of rule references:
         ;
 
     Here, instead of `ID` a `WORD` rule is used to match the object's identifier.
-    
+
 
 !!! note
 
-    Attributes with `name` names are used for reference auto-resolving. By default, 
-    a dict lookup is used, thus they must be of a hashable type. See 
-    issues [#40](https://github.com/textX/textX/issues/40) and 
+    Attributes with `name` names are used for reference auto-resolving. By default,
+    a dict lookup is used, thus they must be of a hashable type. See
+    issues [#40](https://github.com/textX/textX/issues/40) and
     [#266](https://github.com/textX/textX/issues/266).
 
     A usual error is to match the name in this fashion:
@@ -505,7 +531,7 @@ assignments. There are two types of rule references:
         MyObj: name+=ID['.'];
 
     Here, `name` will be a list of strings that are separated by dot and that
-    will not work as the name must be hashable. The best way to implement this 
+    will not work as the name must be hashable. The best way to implement this
     and make `name` hashable is:
 
         MyObj: name=FQN;
@@ -566,7 +592,7 @@ There are two type of syntactic predicates:
         Element:
             AbeforeB | A | B
         ;
-        AbeforeB: 
+        AbeforeB:
             a='a' &'b'      // this succeeds only if 'b' follows 'a'
         ;
         A: a='a';
@@ -608,13 +634,13 @@ We also state in `QuotedID` that there are optional quotation marks around each
 ID, but we don't want those either `'"'?-`.
 
 Given this input:
-  
+
     first."second".third."fourth"
 
 `FullyQualifiedID` will return:
 
     first.second.third.fourth
-  
+
 
 ## Repetition modifiers
 
@@ -702,7 +728,7 @@ have attributes defined. For example:
     ;
 
 This rule has two defined attributes: `x` and `y`.
-    
+
 **Abstract rules** are rules that have no assignments and reference at least one
 abstract or common rule. They are usually given as an ordered choice of other
 rules and they are used to generalize other rules. For example:
@@ -748,7 +774,7 @@ A rule with a single reference to an abstract or common rule is also abstract:
     Value:
       OtherRule
     ;
-    
+
 Abstract rules can have multiple references in a single alternative with the
 following rules:
 
@@ -777,7 +803,7 @@ Another example:
     Model: (STRING | ID | '#' Rule1) Sufix;
     Rule1: a=INT; // common rule
     Sufix: '--';
-    
+
 This is also abstract rule as we are referencing `Rule1` which is a common rule
 and we have no assignments. Matching `# 42 --` as input will give an instance of
 `Rule1` with attribute `a` set to integer `42`.
@@ -801,7 +827,7 @@ In the following example we see what happens if we have multiple common rule ref
     Rule2: a=STRING; // common rule
     Prefix: '#';
     Sufix: '--';
-    
+
 For input `# 42 -- "some string"` the model will be an instance of `Rule1` with
 attribute `a` set to `42` as it is the first common rule reference in the last
 alternative (the one that succeeds) but `Rule2`, despite being discarded, must
@@ -840,7 +866,7 @@ specified in brackets (`[ ]`) at the beginning of the rule's definition after
 the rule's name. Currently, they are used to alter parser configuration for
 whitespace handling on the rule level.
 
-Rule modifiers act on the current rule and all rules referenced inside the rule 
+Rule modifiers act on the current rule and all rules referenced inside the rule
 (recursively): unless a refrenced rule has an explicit rule modifier, the currently
 active modifier state is propagated to referenced rules.
 
diff --git a/examples/pyFlies/pyflies.tx b/examples/pyFlies/pyflies.tx
index f3fb18dae..9ed016ef2 100644
--- a/examples/pyFlies/pyflies.tx
+++ b/examples/pyFlies/pyflies.tx
@@ -165,7 +165,7 @@ Block:
 ;
 
 TestInstance:
-  'test' type=[TestType] trials=INT (practice?="practice"|randomize?="randomize")#
+  'test' type=[TestType] trials=INT (practice?="practice" randomize?="randomize")#
 ;
 
 ScreenInstance:


Test patch:
diff --git a/tests/functional/regressions/test_issue358.py b/tests/functional/regressions/test_issue358.py
new file mode 100644
index 000000000..bb2ee09cd
--- /dev/null
+++ b/tests/functional/regressions/test_issue358.py
@@ -0,0 +1,107 @@
+"""
+Tests for issue: https://github.com/textX/textX/issues/358
+Usage of ordered choice in unordered groups.
+"""
+import pytest
+from textx import metamodel_from_str, TextXSyntaxError
+
+
+@pytest.mark.parametrize("grammar", [
+    r'''
+    Unordered: 'begin' (first?='first' second?='second')#;
+    ''',
+    r'''
+    Unordered: 'begin' (first?='first' | second?='second')#;
+    '''
+])
+def test_unordered_group_sequence_choice(grammar):
+    """
+    Test the equivalence of using sequence and ordered choice in optional matches in
+    unordered groups.
+    """
+    mm = metamodel_from_str(grammar)
+
+    model = mm.model_from_str('begin second')
+    assert model.second and not model.first
+    model = mm.model_from_str('begin first')
+    assert model.first and not model.second
+    model = mm.model_from_str('begin first second')
+    assert model.first and model.second
+    model = mm.model_from_str('begin second first')
+    assert model.first and model.second
+    model = mm.model_from_str('begin')
+    assert not model.first and not model.second
+
+    with pytest.raises(TextXSyntaxError):
+        mm.model_from_str('begin first second first')
+
+    with pytest.raises(TextXSyntaxError):
+        mm.model_from_str('begin first first')
+
+
+@pytest.mark.parametrize("grammar", [
+    r'''
+    Unordered: 'begin' (first='first' second='second')#;
+    ''',
+    r'''
+    Unordered: 'begin' (first='first' | second='second')#;
+    '''
+])
+def test_unordered_group_sequence_choice_nonoptional(grammar):
+    """
+    Test the equivalence of using sequence and ordered choice in unordered groups.
+    """
+    mm = metamodel_from_str(grammar)
+
+    model = mm.model_from_str('begin first second')
+    assert model.first == 'first' and model.second == 'second'
+    model = mm.model_from_str('begin second first')
+    assert model.first == 'first' and model.second == 'second'
+
+    with pytest.raises(TextXSyntaxError):
+        mm.model_from_str('begin first second first')
+
+    with pytest.raises(TextXSyntaxError):
+        mm.model_from_str('begin first first')
+
+
+@pytest.mark.parametrize("grammar", [
+    r'''
+    Unordered: 'begin' (first='first' second='second' | third='third')#;
+    ''',
+    r'''
+    Unordered: 'begin' ((first='first' second='second')  third='third')#;
+    '''
+])
+def test_unordered_group_choice_with_sequences(grammar):
+    """
+    Test the equivalence of using ordered choice and a sequence of parenthesed groups.
+    """
+    mm = metamodel_from_str(grammar)
+
+    model = mm.model_from_str('begin first second third')
+    assert model.first == 'first' and model.second == 'second' and model.third == 'third'
+    model = mm.model_from_str('begin third first second')
+    assert model.first == 'first' and model.second == 'second' and model.third == 'third'
+
+    with pytest.raises(TextXSyntaxError):
+        mm.model_from_str('begin second first third')
+
+    with pytest.raises(TextXSyntaxError):
+        mm.model_from_str('begin third second first')
+
+
+def test_unordered_group_with_optional_assignments():
+    """
+    Test for a question raised during the code review:
+    https://github.com/textX/textX/pull/359
+    """
+    grammar = r'''
+    Root: 'begin' a=Unordered;
+    Unordered: (first?='first' second?='second' | third?='third')#;
+    '''
+
+    mm = metamodel_from_str(grammar)
+    m = mm.model_from_str('begin')
+    # TODO: Maybe we shall have m.a not None and all its attributes to be None.
+    assert m.a is None