1"""This regression tests abstract rule which have an alternative with string
2matches and a rule reference.
3
4See https://github.com/textX/textX/pull/128
5
6"""
7from __future__ import unicode_literals
8import sys
9from textx import metamodel_from_str
10from textx import textx_isinstance
11
12if sys.version < '3':
13    text = unicode  # noqa
14else:
15    text = str
16
17# Global variable namespace
18namespace = {}
19
20
21def test_abstract_alternative_string_match():
22
23    grammar = r'''
24    Calc: assignments*=Assignment expression=Expression;
25    Assignment: variable=ID '=' expression=Expression ';';
26    Expression: operands=Term (operators=PlusOrMinus operands=Term)*;
27    PlusOrMinus: '+' | '-';
28    Term: operands=Factor (operators=MulOrDiv operands=Factor)*;
29    MulOrDiv: '*' | '/' ;
30    Factor: (sign=PlusOrMinus)?  op=Operand;
31    PrimitiveOperand: op_num=NUMBER | op_id=ID;
32    Operand: PrimitiveOperand | ('(' Expression ')');
33    '''
34
35    calc_mm = metamodel_from_str(grammar)
36
37    input_expr = '''
38        a = 10;
39        b = 2 * a + 17;
40        -(4-1)*a+(2+4.67)+b*5.89/(.2+7)
41    '''
42
43    model = calc_mm.model_from_str(input_expr)
44
45    def _is(x, rule):
46        return textx_isinstance(x, calc_mm[rule])
47
48    def assertIs(x, rule):
49        assert _is(x, rule), 'Unexpected object "{}" to rule "{}"'\
50            .format(x, rule)
51
52    def evaluate(x):
53
54        if isinstance(x, float):
55            return x
56
57        elif _is(x, 'Expression'):
58            ret = evaluate(x.operands[0])
59
60            for operator, operand in zip(x.operators,
61                                         x.operands[1:]):
62                if operator == '+':
63                    ret += evaluate(operand)
64                else:
65                    ret -= evaluate(operand)
66            return ret
67
68        elif _is(x, 'Term'):
69            ret = evaluate(x.operands[0])
70
71            for operator, operand in zip(x.operators,
72                                         x.operands[1:]):
73                if operator == '*':
74                    ret *= evaluate(operand)
75                else:
76                    ret /= evaluate(operand)
77            return ret
78
79        elif _is(x, 'Factor'):
80            value = evaluate(x.op)
81            return -value if x.sign == '-' else value
82
83        elif _is(x, 'Operand'):
84            if _is(x, 'PrimitiveOperand'):
85                if x.op_num is not None:
86                    return x.op_num
87                elif x.op_id:
88                    if x.op_id in namespace:
89                        return namespace[x.op_id]
90                    else:
91                        raise Exception('Unknown variable "{}" at position {}'
92                                        .format(x.op_id, x._tx_position))
93            else:
94                assertIs(x, 'CompoundOperand')
95                return evaluate(x.expression)
96
97        elif _is(x, 'Calc'):
98            for a in x.assignments:
99                namespace[a.variable] = evaluate(a.expression)
100
101            return evaluate(x.expression)
102
103        else:
104            assert False, 'Unexpected object "{}" of type "{}"'\
105                .format(x, type(x))
106
107    result = evaluate(model)
108
109    assert (result - 6.93805555) < 0.0001
110
111
112def test_abstract_alternative_multiple_rules_raises_exception():
113    """
114    Test that multiple rules references in a single alternative of abstract
115    rule is prohibited.
116    """
117
118    # In this grammar A is an abstract rule and referencing C D from second
119    # alternative would yield just the first common rule object (C in this
120    # case).
121    grammar = r'''
122    Model: a+=A;
123    A: B | '(' C D ')';
124    B: 'B' name=ID x=INT;
125    C: 'C' name=ID;
126    D: 'D' x=INT;
127    '''
128
129    meta = metamodel_from_str(grammar)
130    model = meta.model_from_str('B somename 23 ( C othername D 67 )')
131    assert len(model.a) == 2
132    assert model.a[0].name == 'somename'
133    assert model.a[0].x == 23
134    assert type(model.a[1]).__name__ == 'C'
135    assert model.a[1].name == 'othername'
136