Browse Source

Fine-tune parser rules. Split grammar file into 2.

Joeri Exelmans 5 years ago
parent
commit
7283c09aad

+ 10 - 4
src/sccd/model/parser.py

@@ -4,8 +4,14 @@ import sccd.schema
 from sccd.syntax.statement import *
 from sccd.model.context import *
 
-with open(os.path.join(os.path.dirname(sccd.schema.__file__),"grammar.g")) as file:
-  _grammar = file.read()
+_schema_dir = os.path.dirname(sccd.schema.__file__)
+
+with open(os.path.join(_schema_dir,"expr.g")) as file:
+  _expr_grammar = file.read()
+
+with open(os.path.join(_schema_dir,"state_ref.g")) as file:
+  _state_ref_grammar = file.read()
+
 
 # Lark transformer for parsetree-less parsing of expressions
 class _ExpressionTransformer(Transformer):
@@ -71,8 +77,8 @@ class _ExpressionTransformer(Transformer):
 # Global variables so we don't have to rebuild our parser every time
 # Obviously not thread-safe
 _transformer = _ExpressionTransformer()
-_expr_parser = Lark(_grammar, parser="lalr", start=["expr", "block"], transformer=_transformer)
-_state_ref_parser = Lark(_grammar, parser="lalr", start=["state_ref"])
+_expr_parser = Lark(_expr_grammar, parser="lalr", start=["expr", "block"], transformer=_transformer)
+_state_ref_parser = Lark(_state_ref_grammar, parser="lalr", start=["state_ref"])
 
 # Exported functions:
 

+ 30 - 17
src/sccd/model/xml_parser.py

@@ -74,12 +74,22 @@ class TreeHandler(ElementHandler):
     parent = self.top("state", default=None)
 
     short_name = el.get("id", "")
+    if parent is None:
+      if short_name:
+        raise XmlLoadError(el, "Root <state> must not have 'id' attribute.")
+    else:
+      if not short_name:
+        raise XmlLoadError(el, "Non-root <state> must have 'id' attribute.")
+
     state = constructor(short_name, parent)
 
     parent_children = self.top("state_children")
     already_there = parent_children.setdefault(short_name, state)
     if already_there is not state:
-      raise XmlLoadError(el, "Sibling state with the same id exists.")
+      if parent:
+        raise XmlLoadError(el, "Sibling state with the same id exists.")
+      else:
+        raise XmlLoadError(el, "Only 1 root <state> allowed.")
 
     self.push("state", state)
     self.push("state_children", {})
@@ -96,7 +106,7 @@ class TreeHandler(ElementHandler):
   def end_state(self, el):
     state, state_children = self._end_state()
 
-    initial = el.get("initial")
+    initial = el.get("initial", None)
     if initial is not None:
       state.default_state = state_children[initial]
     elif len(state.children) == 1:
@@ -161,8 +171,6 @@ class TreeHandler(ElementHandler):
     root_states = self.pop("state_children")
     if len(root_states) == 0:
       raise XmlLoadError(el, "Missing root <state> !")
-    if len(root_states) > 1:
-      raise XmlLoadError(el, "Only one root <state> allowed.")
     root = list(root_states.values())[0]
 
     transitions = self.pop("transitions")
@@ -174,19 +182,24 @@ class TreeHandler(ElementHandler):
       try:
         # Parse and find target state
         parse_tree = parse_state_ref(target_string)
-        def find_state(sequence) -> State:
-          if sequence.data == "relative_path":
-            state = source
-          elif sequence.data == "absolute_path":
-            state = root
-          for item in sequence.children:
-            if item.type == "PARENT_NODE":
-              state = state.parent
-            elif item.type == "CURRENT_NODE":
-              continue
-            elif item.type == "IDENTIFIER":
-              state = [x for x in state.children if x.short_name == item.value][0]
-          return state
+      except Exception as e:
+        raise XmlLoadError(t_el, "Parsing target '%s': %s" % (target_string, str(e)))
+
+      def find_state(sequence) -> State:
+        if sequence.data == "relative_path":
+          state = source
+        elif sequence.data == "absolute_path":
+          state = root
+        for item in sequence.children:
+          if item.type == "PARENT_NODE":
+            state = state.parent
+          elif item.type == "CURRENT_NODE":
+            continue
+          elif item.type == "IDENTIFIER":
+            state = [x for x in state.children if x.short_name == item.value][0]
+        return state
+
+      try:
         targets = [find_state(seq) for seq in parse_tree.children]
       except:
         raise XmlLoadError(t_el, "Could not find target '%s'." % (target_string))

+ 3 - 17
src/sccd/schema/grammar.g

@@ -1,27 +1,11 @@
 // Grammar file for Lark-parser
 
-// Parsing target of a transition as a sequence of nodes
+// Parsing expressions
 
 %import common.WS
 %ignore WS
-
 %import common.ESCAPED_STRING
 
-_PATH_SEP: "/" 
-PARENT_NODE: ".." 
-CURRENT_NODE: "." 
-IDENTIFIER: /[A-Za-z_][A-Za-z_0-9]*/ 
-
-// target of a transition
-state_ref: path | "(" path ("," path)+ ")" 
-
-?path: absolute_path | relative_path 
-absolute_path: _PATH_SEP _path_sequence
-relative_path: _path_sequence
-_path_sequence: (CURRENT_NODE | PARENT_NODE | IDENTIFIER) (_PATH_SEP _path_sequence)?
-
-
-// Parsing expressions
 
 // We use the same operators and operator precedence rules as Python
 
@@ -57,6 +41,8 @@ _path_sequence: (CURRENT_NODE | PARENT_NODE | IDENTIFIER) (_PATH_SEP _path_seque
      | func_call
      | array
 
+IDENTIFIER: /[A-Za-z_][A-Za-z_0-9]*/ 
+
 func_call: atom "(" param_list ")"
 param_list: ( expr ("," expr)* )?  -> params
 

+ 21 - 0
src/sccd/schema/state_ref.g

@@ -0,0 +1,21 @@
+// Grammar file for Lark-parser
+
+// Parsing target of a transition as a sequence of nodes
+
+%import common.WS
+%ignore WS
+%import common.ESCAPED_STRING
+
+
+_PATH_SEP: "/" 
+PARENT_NODE: ".." 
+CURRENT_NODE: "." 
+IDENTIFIER: /[A-Za-z_][A-Za-z_0-9]*/ 
+
+// target of a transition
+state_ref: path | "(" path ("," path)+ ")" 
+
+?path: absolute_path | relative_path 
+absolute_path: _PATH_SEP _path_sequence
+relative_path: _path_sequence
+_path_sequence: (CURRENT_NODE | PARENT_NODE | IDENTIFIER) (_PATH_SEP _path_sequence)?

+ 8 - 1
src/sccd/syntax/tree.py

@@ -185,7 +185,14 @@ class StateTree:
         def init_tree(state: State, parent_full_name: str, ancestors: List[State]):
             nonlocal next_id
 
-            full_name = parent_full_name + '/' + state.short_name
+            if state is root:
+                full_name = '/'
+            elif state.parent is root:
+                full_name = '/' + state.short_name
+            else:
+                full_name = parent_full_name + '/' + state.short_name
+
+            # full_name = parent_full_name + '/' + state.short_name
 
             state.gen = gen = StateGenerated(
                 state_id=next_id,

+ 26 - 0
test/test_files/features/datamodel/test_datamodel_expr.xml

@@ -0,0 +1,26 @@
+<?xml version="1.0" ?>
+<test>
+  <statechart>
+    <semantics/>
+    <datamodel>
+      <var id="x" expr="0"/>
+      <var id="y" expr="x"/><!-- this is allowed, value of x copied to y -->
+    </datamodel>
+    <tree>
+      <state initial="start">
+        <state id="start">
+          <transition target="/done" cond="y == 0">
+            <raise event="done" port="out"/>
+          </transition>
+        </state>
+        <state id="done"/>
+      </state>
+    </tree>
+  </statechart>
+
+  <output>
+    <big_step>
+      <event name="done" port="out"/>
+    </big_step>
+  </output>
+</test>

+ 2 - 2
test/test_files/syntax/fail_multiple_root.xml

@@ -6,8 +6,8 @@
 
     <tree>
       <!-- not allowed: more than one root state -->
-      <state id="a"/>
-      <state id="b"/>
+      <state/>
+      <state/>
     </tree>
   </statechart>
 

+ 20 - 0
test/test_files/syntax/fail_no_id.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" ?>
+<test>
+  <statechart>
+    <semantics/>
+    <datamodel/>
+
+    <tree>
+      <state>
+        <state><!-- non-root state must have 'id' -->
+        </state>
+      </state>
+    </tree>
+  </statechart>
+
+  <output>
+    <big_step>
+      <event name="ok" port="out" />
+    </big_step>
+  </output>
+</test>

+ 15 - 0
test/test_files/syntax/fail_root_id.xml

@@ -0,0 +1,15 @@
+<?xml version="1.0" ?>
+<test>
+  <statechart>
+    <semantics/>
+    <datamodel/>
+
+    <tree>
+      <!-- not allowed: root with id -->
+      <state id="root"/>
+    </tree>
+  </statechart>
+
+  <output>
+  </output>
+</test>

+ 0 - 30
test/test_files/syntax/test_no_id.xml

@@ -1,30 +0,0 @@
-<?xml version="1.0" ?>
-<test>
-  <statechart>
-    <semantics/>
-    <datamodel/>
-
-    <tree>
-      <!-- At this time, states without id implicitly have id="".
-           It seems to give no problems since every state in the hierarchy still
-           has a globally unique name, '/' for root, '//' for the id-less child of root, etc.
-           This may be changed in the future. -->
-      <state>
-        <state initial="">
-          <state>
-            <transition target="../b"/>
-          </state>
-          <state id="b">
-            <onentry><raise event="ok" port="out"/></onentry>
-          </state>
-        </state>
-      </state>
-    </tree>
-  </statechart>
-
-  <output>
-    <big_step>
-      <event name="ok" port="out" />
-    </big_step>
-  </output>
-</test>