Przeglądaj źródła

Re-engineered state refs (used by transitions and INSTATE-macro). Implemented new strategy for INSTATE-macro in parser and interpreter.

Joeri Exelmans 4 lat temu
rodzic
commit
46abeb6ba5

+ 4 - 2
src/sccd/action_lang/cmd/prompt.py

@@ -21,12 +21,14 @@ if __name__ == "__main__":
   print("    apply(10, func(i: int) { return i+1; })")
   print()
 
+  parser = ActionLangParser()
+
   while True:
     try:
       line = input("> ")
       try:
         # Attempt to parse as a statement
-        stmt = parse_block(line) # may raise LarkError
+        stmt = parser.parse_stmt(line) # may raise LarkError
         stmt.init_stmt(scope)
 
         # Grow current stack frame if necessary
@@ -38,7 +40,7 @@ if __name__ == "__main__":
       except LarkError as e:
         try:
           # Attempt to parse as an expression
-          expr = parse_expression(line)
+          expr = parser.parse_expr(line)
           expr_type = expr.init_expr(scope)
           val = expr.eval(memory)
           print("%s: %s" % (str(val), str(expr_type)))

+ 33 - 17
src/sccd/action_lang/parser/text.py

@@ -1,15 +1,20 @@
-import os
-from lark import Lark, Transformer
+import lark
 from sccd.action_lang.static.statement import *
+from collections import defaultdict
 
-_grammar_dir = os.path.dirname(__file__)
+# Lark transformer for generating a parse tree of our own types.
+class Transformer(lark.Transformer):
 
-with open(os.path.join(_grammar_dir,"action_lang.g")) as file:
-  action_lang_grammar = file.read()
+  def __init__(self):
+    self.macros = defaultdict(list)
 
+  def set_macro(self, macro_id, constructor):
+    # print("registered macro", macro_id, constructor)
+    self.macros[macro_id].append(constructor)
 
-# Lark transformer for generating a parse tree of our own types.
-class ExpressionTransformer(Transformer):
+  def unset_macro(self, macro_id):
+    # print("unregistered macro", macro_id)
+    self.macros[macro_id].pop()
 
   array = Array
 
@@ -53,7 +58,15 @@ class ExpressionTransformer(Transformer):
     return FunctionCall(node[0], node[1].children)
 
   def macro_call(self, node):
-    return MacroCall(node[0], node[1].children)
+    macro_id = node[0]
+    params = node[1].children
+    try:
+      constructor = self.macros[macro_id][-1]
+    except IndexError as e:
+      print(self.macros)
+      raise Exception("Unknown macro: %s" % macro_id) from e
+
+    return constructor(params)
 
   def array_indexed(self, node):
     return ArrayIndexed(node[0], node[1])
@@ -119,16 +132,19 @@ class ExpressionTransformer(Transformer):
   def func_decl(self, node):
     return FunctionDeclaration(params_decl=node[0], body=node[1])
 
+import os
+grammar_dir = os.path.dirname(__file__)
+with open(os.path.join(grammar_dir,"action_lang.g")) as file:
+  grammar = file.read()
 
-# Global variables so we don't have to rebuild our parser every time
-# Obviously not thread-safe
-_transformer = ExpressionTransformer()
-_parser = Lark(action_lang_grammar, parser="lalr", start=["expr", "block"], transformer=_transformer)
+_default_parser = lark.Lark(grammar, parser="lalr", start=["expr", "stmt"], transformer=Transformer(), cache=True)
 
-# Exported functions:
+class TextParser:
+  def __init__(self, parser=_default_parser):
+    self.parser = parser
 
-def parse_expression(text: str) -> Expression:
-  return _parser.parse(text, start="expr")
+  def parse_expr(self, text: str) -> Expression:
+    return self.parser.parse(text, start="expr")
 
-def parse_block(text: str) -> Statement:
-  return _parser.parse(text, start="block")
+  def parse_stmt(self, text: str) -> Statement:
+    return self.parser.parse(text, start="block")

+ 4 - 7
src/sccd/statechart/parser/statechart.g

@@ -6,14 +6,11 @@
 %import common.ESCAPED_STRING
 
 
-// Parsing target of a transition as a sequence of nodes
-
-state_ref: path
-
+// A path to a state. XPath-like syntax.
 ?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)?
+absolute_path: _PATH_SEP path_sequence
+relative_path: path_sequence
+path_sequence: (CURRENT_NODE | PARENT_NODE | IDENTIFIER) (_PATH_SEP path_sequence)?
 
 _PATH_SEP: "/" 
 PARENT_NODE: ".." 

+ 50 - 37
src/sccd/statechart/parser/text.py

@@ -1,22 +1,38 @@
-import os
-from lark import Lark
+import lark
 from sccd.action_lang.parser import text as action_lang
 from sccd.statechart.static.tree import *
 from sccd.statechart.static.globals import *
-
-_grammar_dir = os.path.dirname(__file__)
-
-with open(os.path.join(_grammar_dir, "statechart.g")) as file:
-  _sc_grammar = action_lang.action_lang_grammar + file.read()
-
+from sccd.statechart.static.state_ref import *
 
 # Lark transformer for parsetree-less parsing of expressions
 # Extends action language's ExpressionTransformer
-class StatechartTransformer(action_lang.ExpressionTransformer):
-  def __init__(self):
+class Transformer(action_lang.Transformer):
+  def __init__(self, globals):
     super().__init__()
-    self.globals: Globals = None
-
+    self.globals = globals
+
+  def absolute_path(self, node):
+    # print("ABS", node[0])
+    return StatePath(is_absolute=True, sequence=node[0])
+
+  def relative_path(self, node):
+    # print("REL", node[0])
+    return StatePath(is_absolute=False, sequence=node[0])
+
+  def path_sequence(self, node):
+    # print("PATH_SEQ", node)
+    if node[0].type == "PARENT_NODE":
+      item = ParentNode()
+    elif node[0].type == "CURRENT_NODE":
+      item = CurrentNode()
+    elif node[0].type == "IDENTIFIER":
+      item = Identifier(value=node[0].value)
+
+    # Concatenate with rest of path
+    if len(node) == 2:
+      return [item] + node[1]
+    else:
+      return [item]
 
   # override: all durations must be added to 'globals'
   def duration_literal(self, node):
@@ -59,28 +75,25 @@ class StatechartTransformer(action_lang.ExpressionTransformer):
     event_id = self.globals.events.assign_id(event_name)
     return EventDecl(id=event_id, name=event_name, params_decl=node[1])
 
-
-# Global variables so we don't have to rebuild our parser every time
-# Obviously not thread-safe
-_transformer = StatechartTransformer()
-_parser = Lark(_sc_grammar, parser="lalr", start=["expr", "block", "event_decl_list", "state_ref", "semantic_choice"], transformer=_transformer)
-
-# Exported functions:
-
-def parse_expression(globals: Globals, text: str) -> Expression:
-  _transformer.globals = globals
-  return _parser.parse(text, start="expr")
-
-def parse_block(globals: Globals, text: str) -> Block:
-  _transformer.globals = globals
-  return _parser.parse(text, start="block")
-
-def parse_events_decl(globals: Globals, text: str) -> Tuple[List[EventDecl], List[EventDecl]]:
-  _transformer.globals = globals
-  return _parser.parse(text, start="event_decl_list")
-
-def parse_state_ref(text: str):
-  return _parser.parse(text, start="state_ref")
-
-def parse_semantic_choice(text: str):
-  return _parser.parse(text, start="semantic_choice")
+import os
+grammar_dir = os.path.dirname(__file__)
+with open(os.path.join(grammar_dir, "statechart.g")) as file:
+  # Concatenate Action Lang and SC grammars
+  grammar = action_lang.grammar + file.read()
+
+# Parses action language expressions and statements, and also event decls, state refs and semantic choices. 
+class TextParser(action_lang.TextParser):
+  def __init__(self, globals):
+    # Building the parser is actually the slowest step of parsing a statechart model.
+    # Doesn't have to happen every time, so should find a way to speed this up.
+    parser = lark.Lark(grammar, parser="lalr", start=["expr", "block", "event_decl_list", "path", "semantic_choice"], transformer=Transformer(globals), cache=True)
+    super().__init__(parser)
+
+  def parse_semantic_choice(self, text: str):
+    return self.parser.parse(text, start="semantic_choice")
+
+  def parse_events_decl(self, text: str) -> Tuple[List[EventDecl], List[EventDecl]]:
+    return self.parser.parse(text, start="event_decl_list")
+
+  def parse_path(self, text: str):
+    return self.parser.parse(text, start="path")

+ 38 - 39
src/sccd/statechart/parser/xml.py

@@ -7,6 +7,8 @@ from sccd.statechart.static.tree import *
 from sccd.statechart.dynamic.builtin_scope import *
 from sccd.util.xml_parser import *
 from sccd.statechart.parser.text import *
+from sccd.statechart.static.in_state import InState
+
 class SkipFile(Exception):
   pass
 
@@ -20,7 +22,8 @@ def check_duration_type(type):
     raise XmlError(msg)
 
 # path: filesystem path for finding external statecharts
-def statechart_parser_rules(globals, path, load_external = True, parse_f = parse_f) -> Rules:
+def statechart_parser_rules(globals, path, load_external = True, parse_f = parse_f, text_parser=TextParser(globals)) -> Rules:
+
   import os
   def parse_statechart(el):
     ext_file = el.get("src")
@@ -38,7 +41,7 @@ def statechart_parser_rules(globals, path, load_external = True, parse_f = parse
     else:
       if not load_external:
         raise SkipFile("Parser configured not to load statecharts from external files.")
-      statechart = parse_f(os.path.join(path, ext_file), [("statechart", statechart_parser_rules(globals, path, load_external=False, parse_f=parse_f))])
+      statechart = parse_f(os.path.join(path, ext_file), [("statechart", statechart_parser_rules(globals, path, load_external=False, parse_f=parse_f, text_parser=text_parser))])
 
     def parse_semantics(el):
       available_aspects = SemanticConfiguration.get_fields()
@@ -47,7 +50,7 @@ def statechart_parser_rules(globals, path, load_external = True, parse_f = parse
           aspect_type = available_aspects[aspect_name]
         except KeyError:
           raise XmlError("invalid semantic aspect: '%s'" % aspect_name)
-        result = parse_semantic_choice(text)
+        result = text_parser.parse_semantic_choice(text)
         if result.data == "wildcard":
           semantic_choice = list(aspect_type) # all options
         elif result.data == "list":
@@ -57,7 +60,7 @@ def statechart_parser_rules(globals, path, load_external = True, parse_f = parse
         setattr(statechart.semantics, aspect_name, semantic_choice)
 
     def parse_datamodel(el):
-      body = parse_block(globals, el.text)
+      body = text_parser.parse_stmt(el.text)
       body.init_stmt(statechart.scope)
       statechart.datamodel = body
 
@@ -87,6 +90,7 @@ def statechart_parser_rules(globals, path, load_external = True, parse_f = parse
       children_dict = {}
       transitions = [] # All of the statechart's transitions accumulate here, cause we still need to find their targets, which we can't do before the entire state tree has been built. We find their targets when encoutering the </root> closing tag.
       after_id = 0 # After triggers need unique IDs within the scope of the statechart model
+      refs_to_resolve = [] # Transition targets and INSTATE arguments. Resolved after constructing state tree.
 
       def get_default_state(el, state, children_dict):
         have_initial = False
@@ -113,6 +117,13 @@ def statechart_parser_rules(globals, path, load_external = True, parse_f = parse
 
       def state_child_rules(parent, sibling_dict: Dict[str, State]):
 
+        def macro_in_state(params):
+          refs = [StateRef(source=parent, path=text_parser.parse_path(p.string)) for p in params]
+          refs_to_resolve.extend(refs)
+          return InState(state_refs=refs)
+
+        text_parser.parser.options.transformer.set_macro("@in", macro_in_state)
+
         # A transition's guard expression and action statements can read the transition's event parameters, and also possibly the current state configuration. We therefore now wrap these into a function with a bunch of parameters for those values that we want to bring into scope.
         def wrap_transition_params(expr_or_stmt, trigger: Trigger):
           if isinstance(expr_or_stmt, Statement):
@@ -127,7 +138,7 @@ def statechart_parser_rules(globals, path, load_external = True, parse_f = parse
           wrapped = FunctionDeclaration(
             params_decl=
               # The param '@conf' (which, on purpose, is an illegal identifier in textual concrete syntax, to prevent naming collisions) will contain the statechart's configuration as a bitmap (SCCDInt). This parameter is currently only used in the expansion of the INSTATE-macro.
-              [ParamDecl(name="_conf", formal_type=SCCDStateConfiguration(state=parent))]
+              [ParamDecl(name="@conf", formal_type=SCCDStateConfiguration(state=parent))]
               # Plus all the parameters of the enabling events of the transition's trigger:
               + [param for event in trigger.enabling for param in event.params_decl],
             body=body)
@@ -139,7 +150,7 @@ def statechart_parser_rules(globals, path, load_external = True, parse_f = parse
             params = []
             def parse_param(el):
               expr_text = require_attribute(el, "expr")
-              expr = parse_expression(globals, expr_text)
+              expr = text_parser.parse_expr(expr_text)
               function = wrap_transition_params(expr, trigger=wrap_trigger)
               function.init_expr(scope)
               function.scope.name = "event_param"
@@ -166,7 +177,7 @@ def statechart_parser_rules(globals, path, load_external = True, parse_f = parse
 
           def parse_code(el):
             def finish_code():
-              block = parse_block(globals, el.text)
+              block = text_parser.parse_stmt(el.text)
               function = wrap_transition_params(block, trigger=wrap_trigger)
               function.init_expr(scope)
               function.scope.name = "code"
@@ -233,14 +244,21 @@ def statechart_parser_rules(globals, path, load_external = True, parse_f = parse
             raise XmlError("Root cannot be source of a transition.")
 
           target_string = require_attribute(el, "target")
-          transition = Transition(source=parent, target_string=target_string)
+
+          try:
+            path = text_parser.parse_path(target_string)
+          except Exception as e:
+            raise XmlErrorElement(t_el, "Parsing target '%s': %s" % (transition.target_string, str(e))) from e
+
+          transition = Transition(source=parent, path=path)
+          refs_to_resolve.append(transition)
 
           have_event_attr = False
           def parse_attr_event(event):
             nonlocal have_event_attr
             have_event_attr = True
 
-            positive_events, negative_events = parse_events_decl(globals, event)
+            positive_events, negative_events = text_parser.parse_events_decl(event)
 
             # Optimization: sort events by ID
             # Allows us to save time later.
@@ -257,7 +275,7 @@ def statechart_parser_rules(globals, path, load_external = True, parse_f = parse
             nonlocal after_id
             if have_event_attr:
               raise XmlError("Cannot specify 'after' and 'event' at the same time.")
-            after_expr = parse_expression(globals, after)
+            after_expr = text_parser.parse_expr(after)
             after_type = after_expr.init_expr(statechart.scope)
             check_duration_type(after_type)
             # After-events should only be generated by the runtime.
@@ -269,7 +287,7 @@ def statechart_parser_rules(globals, path, load_external = True, parse_f = parse
 
           def parse_attr_cond(cond):
             # Transition's guard expression
-            guard_expr = parse_expression(globals, cond)
+            guard_expr = text_parser.parse_expr(cond)
             guard_function = wrap_transition_params(guard_expr, transition.trigger)
             guard_type = guard_function.init_expr(statechart.scope)
             guard_function.scope.name = "guard"
@@ -290,41 +308,22 @@ def statechart_parser_rules(globals, path, load_external = True, parse_f = parse
 
           return (actions_rules(scope=statechart.scope, wrap_trigger=transition.trigger), finish_transition)
 
-        return {"state": parse_state, "parallel": parse_parallel, "history": parse_history, "onentry": parse_onentry, "onexit": parse_onexit, "transition": parse_transition}
+        def finish_state_child_rules():
+          text_parser.parser.options.transformer.unset_macro("@in")
+
+        return ({"state": parse_state, "parallel": parse_parallel, "history": parse_history, "onentry": parse_onentry, "onexit": parse_onexit, "transition": parse_transition}, finish_state_child_rules)
 
       def finish_root():
         root.type = OrState(state=root, default_state=get_default_state(el, root, children_dict))
 
-        for transition, t_el in transitions:
-          try:
-            parse_tree = parse_state_ref(transition.target_string)
-          except Exception as e:
-            raise XmlErrorElement(t_el, "Parsing target '%s': %s" % (transition.target_string, str(e))) from e
-
-          def find_target(sequence) -> State:
-            if sequence.data == "relative_path":
-              state = transition.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":
-                try:
-                  state = [x for x in state.children if x.short_name == item.value][0]
-                except IndexError:
-                  raise XmlError("%s has no child \"%s\"." % ("Root state" if state.parent is None else '"%s"'%state.short_name, item.value))
-            if state is root:
-              raise XmlError("Root cannot be target of a transition.")
-            return state
-
+        # State tree has been constructed, we can now resolve state refs:
+        for ref in refs_to_resolve:
           try:
-            transition.target = find_target(parse_tree.children[0])
-          except Exception as e:
+            ref.resolve(root=root)
+          except PathError as e:
             raise XmlErrorElement(t_el, "target=\"%s\": %s" % (transition.target_string, str(e))) from e
 
+        # Next, visit tree to statically calculate many properties of states and transitions:
         statechart.tree = StateTree(root)
 
       return (state_child_rules(root, sibling_dict=children_dict), finish_root)

+ 28 - 0
src/sccd/statechart/static/in_state.py

@@ -0,0 +1,28 @@
+from dataclasses import *
+from sccd.action_lang.static.expression import *
+from sccd.statechart.static.state_ref import StateRef
+
+# Macro expansion for @in
+@dataclass
+class InState(Expression):
+    state_refs: List[StateRef]
+
+    offset: Optional[int] = None
+
+    def init_expr(self, scope: Scope) -> SCCDType:
+        self.offset, _ = scope.get_rvalue("@conf")
+        return SCCDBool
+
+    def get_type(self) -> SCCDType:
+        return SCCDBool
+
+    def eval(self, memory: MemoryInterface):
+        state_configuration = memory.load(self.offset)
+        # print("state_configuration:", state_configuration)
+        # print("INSTATE ", [(r.target, r.target.state_id_bitmap) for r in self.state_refs], " ??")
+        result = reduce(lambda x,y: x and y, (bool(ref.target.state_id_bitmap & state_configuration) for ref in self.state_refs))
+        # print(result)
+        return result
+
+    def render(self):
+        return "@in(" + ",".join(ref.target.full_name for ref in self.state_refs)

+ 69 - 0
src/sccd/statechart/static/state_ref.py

@@ -0,0 +1,69 @@
+from abc import abstractmethod
+from dataclasses import dataclass
+from typing import *
+
+class PathError(Exception):
+    pass
+
+class PathItem:
+    @abstractmethod
+    def type(self):
+        pass
+
+class ParentNode(PathItem):
+    def type(self):
+        return "PARENT_NODE"
+
+    def __repr__(self):
+        return "ParentNode()"
+
+class CurrentNode(PathItem):
+    def type(self):
+        return "CURRENT_NODE"
+
+    def __repr__(self):
+        return "CurrentNode()"
+
+@dataclass
+class Identifier(PathItem):
+    value: str # state's short name
+
+    def type(self):
+        return "IDENTIFIER"
+
+    def __repr__(self):
+        return "Identifier(%s)" % self.value
+
+@dataclass
+class StatePath:
+    is_absolute: bool
+    sequence: List[PathItem]
+
+# Used by Transition and INSTATE-macro
+@dataclass(eq=False)
+class StateRef:
+    source: 'State'
+    path: StatePath
+
+    target: Optional['State'] = None
+
+    def resolve(self, root):
+        if self.path.is_absolute:
+            state = root
+        else:
+            state = self.source
+
+        for item in self.path.sequence:
+            item_type = item.type()
+            if item_type == "PARENT_NODE":
+                state = state.parent
+            elif item_type == "CURRENT_NODE":
+                continue
+            elif item_type == "IDENTIFIER":
+                try:
+                    state = [x for x in state.children if x.short_name == item.value][0]
+                except IndexError as e:
+                    raise PathError("%s has no child \"%s\"." % ("Root state" if state.parent is None else '"%s"'%state.short_name, item.value)) from e
+        if state.parent is None:
+            raise PathError("Root cannot be target of StateRef.")
+        self.target = state

+ 2 - 7
src/sccd/statechart/static/tree.py

@@ -2,6 +2,7 @@ import termcolor
 from typing import *
 import itertools
 from sccd.statechart.static.action import *
+from sccd.statechart.static.state_ref import StateRef
 from sccd.util.bitmap import *
 from sccd.util import timer
 from sccd.util.visit_tree import *
@@ -228,13 +229,7 @@ class AfterTrigger(Trigger):
 EMPTY_TRIGGER = Trigger(enabling=[])
 
 @dataclass(eq=False)
-class Transition:
-    source: State
-    target_string: Optional[str]
-    # scope: Scope
-
-    target: State = None
-
+class Transition(StateRef):
     guard: Optional[FunctionDeclaration] = None
     actions: List[Action] = field(default_factory=list)
     trigger: Trigger = EMPTY_TRIGGER

+ 5 - 4
src/sccd/test/parser/xml.py

@@ -5,6 +5,7 @@ _empty_scope = Scope("test", parent=None)
 
 def test_parser_rules(statechart_parser_rules):
   globals = Globals()
+  text_parser = TextParser(globals)
   input = []
   output = []
 
@@ -15,13 +16,13 @@ def test_parser_rules(statechart_parser_rules):
         params = []
         def parse_param(el):
           text = require_attribute(el, "expr")
-          expr = parse_expression(globals, text)
+          expr = text_parser.parse_expr(text)
           expr.init_expr(scope=_empty_scope)
           params.append(expr.eval(memory=None))
         return (params, parse_param)
 
       def parse_time(time: str) -> Expression:
-        expr = parse_expression(globals, time)
+        expr = text_parser.parse_expr(time)
         type = expr.init_expr(scope=_empty_scope)
         check_duration_type(type)
         return expr
@@ -70,7 +71,7 @@ def test_parser_rules(statechart_parser_rules):
 
           def parse_param(el):
             val_text = require_attribute(el, "val")
-            val_expr = parse_expression(globals, val_text)
+            val_expr = text_parser.parse_expr(val_text)
             val_expr.init_expr(scope=_empty_scope)
             val = val_expr.eval(memory=None)
             params.append(val)
@@ -99,7 +100,7 @@ def test_parser_rules(statechart_parser_rules):
         output=output)
       for i, variant in enumerate(variants)])
 
-    sc_rules = statechart_parser_rules(globals)
+    sc_rules = statechart_parser_rules(globals, text_parser=text_parser)
     return ([("statechart", sc_rules), ("input?", parse_input), ("output?", parse_output)], finish_test)
 
   return parse_test

+ 25 - 18
src/sccd/util/xml_parser.py

@@ -57,6 +57,9 @@ UnorderedElements = Dict[str, ParseElementF]
 Rules = Union[OrderedElements, UnorderedElements]
 RulesWDone = Union[Rules, Tuple[Rules,Callable]]
 
+# TODO: Refactor for readability :)
+# -> Introduce some actual domain-specific types instead of using lists, dicts and tuples
+
 # A very beefy parsing function on top of 'lxml' event-driven parsing, that takes parsing rules in a very powerful, schema-like format.
 # The 'rules' passed should be one of:
 #    1) A dictionary of XML tags mapped to a visit-calback, to denote that any of the tags in are allowed in any order and in any multiplicity.
@@ -102,14 +105,22 @@ def parse(src_file, rules: RulesWDone, ignore_unmatched = False, decorate_except
   rules_stack = [rules]
   results_stack = [[]]
 
+  def unpack_tuple(rules):
+    when_done = []
+    while isinstance(rules, tuple):
+      assert len(rules) == 2
+      when_done.append(rules[1])
+      rules = rules[0]
+    return (rules, when_done)
+
+  def pack_tuple(rules, when_done):
+    for cb in reversed(when_done):
+      rules = (rules, cb)
+    return rules
+
   for event, el in etree.iterparse(src_file, events=("start", "end")):
     try:
-      when_done = None
-      pair = rules_stack[-1]
-      if isinstance(pair, tuple):
-        rules, when_done = pair
-      else:
-        rules = pair
+      rules, when_done = unpack_tuple(rules_stack[-1])
       if event == "start":
         # print("start", el.tag)
 
@@ -133,14 +144,14 @@ def parse(src_file, rules: RulesWDone, ignore_unmatched = False, decorate_except
               if m & Multiplicity.AT_MOST_ONCE:
                 # We don't allow this element next time
                 rules = rules[1:]
-                rules_stack[-1] = (rules, when_done)
+                rules_stack[-1] = pack_tuple(rules, when_done)
 
               elif m & Multiplicity.AT_LEAST_ONCE:
                 # We don't require this element next time
                 m &= ~Multiplicity.AT_LEAST_ONCE
                 rules = list(rules) # copy list before editing
                 rules[0] = (m.unparse_suffix(tag), func) # edit rule
-                rules_stack[-1] = (rules, when_done)
+                rules_stack[-1] = pack_tuple(rules, when_done)
 
               parse_function = func
               break
@@ -151,7 +162,7 @@ def parse(src_file, rules: RulesWDone, ignore_unmatched = False, decorate_except
               else:
                 # Element is skipable
                 rules = rules[1:]
-                rules_stack[-1] = (rules, when_done)
+                rules_stack[-1] = pack_tuple(rules, when_done)
         else:
           print(rules)
           assert False # rule should always be a dict or list
@@ -181,15 +192,11 @@ def parse(src_file, rules: RulesWDone, ignore_unmatched = False, decorate_except
             raise XmlError("Missing required elements: %s " % ", ".join("<%s>" % t for t in missing_required))
         children_results = results_stack.pop()
         pair = rules_stack.pop()
-        if isinstance(pair, tuple):
-          _, when_done = pair
-          if when_done:
-            result = when_done(*children_results)
-            # print("end", el.tag, "with result=", result)
-            if result:
-              results_stack[-1].append(result)
-          # else:
-          #   print("end", el.tag)
+        for cb in when_done:
+          result = cb(*children_results)
+          # print("end", el.tag, "with result=", result)
+          if result:
+            results_stack[-1].append(result)
 
     except (XmlError, *decorate_exceptions) as e:
       # Assume exception occured while visiting current element 'el':

+ 43 - 0
test_files/features/instate/test_instate.xml

@@ -0,0 +1,43 @@
+<test>
+  <statechart>
+    <root>
+      <parallel id="p">
+        <state id="o0" initial="a">
+          <state id="a">
+            <transition port="in" event="try" target="../b" cond='@in("/p/o1/d")'>
+              <raise port="out" event="yes"/>
+            </transition>
+            <transition port="in" event="try" target="." cond='not @in("/p/o1/d")'>
+              <raise port="out" event="no"/>
+            </transition>
+          </state>
+          <state id="b">
+          </state>
+        </state>
+
+        <state id="o1" initial="c">
+          <state id="c">
+            <transition port="in" event="to_d" target="../d"/>
+          </state>
+          <state id="d">
+          </state>
+        </state>
+      </parallel>
+    </root>
+  </statechart>
+
+  <input>
+    <event port="in" name="try" time="0 d"/>
+    <event port="in" name="to_d" time="1 s"/>
+    <event port="in" name="try" time="2 s"/>
+  </input>
+
+  <output>
+    <big_step>
+      <event port="out" name="no"/>
+    </big_step>
+    <big_step>
+      <event port="out" name="yes"/>
+    </big_step>  
+  </output>
+</test>