Repository: sybila/eBCSgen

Issue:
Check that rule labels used in regulations do actually exist
This should also cover #88.

Discussion:

Patch:
diff --git a/eBCSgen/Core/Formula.py b/eBCSgen/Core/Formula.py
index 853a415..a68e2a9 100644
--- a/eBCSgen/Core/Formula.py
+++ b/eBCSgen/Core/Formula.py
@@ -1,7 +1,7 @@
 from lark import Transformer, Tree
 
 from eBCSgen.Errors.ComplexOutOfScope import ComplexOutOfScope
-from eBCSgen.Core.Rate import tree_to_string
+from eBCSgen.utils import tree_to_string
 
 
 class Formula:
diff --git a/eBCSgen/Core/Rate.py b/eBCSgen/Core/Rate.py
index 648aeae..ba6acfd 100644
--- a/eBCSgen/Core/Rate.py
+++ b/eBCSgen/Core/Rate.py
@@ -4,6 +4,7 @@ from lark import Transformer, Tree, Token
 from sortedcontainers import SortedList
 
 from eBCSgen.TS.State import Vector
+from eBCSgen.utils import tree_to_string
 
 STATIC_MATH = """<kineticLaw><math xmlns="http://www.w3.org/1998/Math/MathML"><apply>{}</apply></math></kineticLaw>"""
 
@@ -252,15 +253,3 @@ class MathMLtransformer(Transformer):
         operator = self.operators[matches[1].type]
         return Tree(node, [matches[0], Token(matches[1].type, operator), matches[2]])
 
-
-def tree_to_string(tree):
-    """
-    Recursively constructs a list form given lark tree.
-
-    :param tree: given lark tree
-    :return: list of components
-    """
-    if type(tree) == Tree:
-        return sum(list(map(tree_to_string, tree.children)), [])
-    else:
-        return [str(tree)]
diff --git a/eBCSgen/Errors/RegulationParsingError.py b/eBCSgen/Errors/RegulationParsingError.py
new file mode 100644
index 0000000..554976b
--- /dev/null
+++ b/eBCSgen/Errors/RegulationParsingError.py
@@ -0,0 +1,6 @@
+class RegulationParsingError(Exception):
+    def __init__(self, error):
+        self.error = error
+
+    def __str__(self):
+        return "Error while parsing the regulation:\n\n{}".format(self.error)
diff --git a/eBCSgen/Parsing/ParseBCSL.py b/eBCSgen/Parsing/ParseBCSL.py
index 86c711f..3089ff2 100644
--- a/eBCSgen/Parsing/ParseBCSL.py
+++ b/eBCSgen/Parsing/ParseBCSL.py
@@ -3,7 +3,7 @@ import json
 import numpy as np
 from numpy import inf
 from copy import deepcopy
-from lark import Lark, Transformer, Tree
+from lark import Lark, Token, Transformer, Tree
 from lark import UnexpectedCharacters, UnexpectedToken, UnexpectedEOF
 from lark.load_grammar import _TERMINAL_NAMES
 import regex
@@ -26,6 +26,8 @@ from eBCSgen.Core.Side import Side
 from eBCSgen.Core.Model import Model
 from eBCSgen.Errors.ComplexParsingError import ComplexParsingError
 from eBCSgen.Errors.UnspecifiedParsingError import UnspecifiedParsingError
+from eBCSgen.Errors.RegulationParsingError import RegulationParsingError
+from eBCSgen.utils import tree_to_string
 
 
 def load_TS_from_json(json_file: str) -> TransitionSystem:
@@ -189,7 +191,7 @@ COMPLEX_GRAMMAR = """
 REGULATIONS_GRAMMAR = """
     regulation_def: "type" ( regular | programmed | ordered | concurrent_free | conditional ) 
 
-    !regular: "regular" _NL+ (DIGIT|LETTER| "+" | "*" | "(" | ")" | "[" | "]" | "_" | "|" | "&")+ _NL*
+    !regular: "regular" _NL+ expression _NL*
 
     programmed: "programmed" _NL+ (successors _NL+)* successors _NL*
     successors: CNAME ":" "{" CNAME ("," CNAME)* "}"
@@ -203,6 +205,40 @@ REGULATIONS_GRAMMAR = """
     context: CNAME ":" "{" rate_complex ("," rate_complex)* "}"
 """
 
+REGEX_GRAMMAR = r"""
+    !?expression: term ("|" term)*
+
+    ?term: factor+
+
+    ?factor: primary quantifier?
+
+    !quantifier: "??"
+            | "*?"
+            | "+?"
+            | "*+"
+            | "++"
+            | "?+"
+            | "*"
+            | "+"
+            | "?"
+            | "{NUMBER,NUMBER}"
+            | "{NUMBER}"
+
+    !primary: "(" expression ")"
+        | "[" REGEX_CHAR "-" REGEX_CHAR "]"
+        | "[" REGEX_CHAR* "]"
+        | SPECIAL_CHAR
+        | ESCAPED_CHAR
+        | "." 
+        | REGEX_CHAR
+
+    SPECIAL_CHAR: "^" | "$" | "&"
+
+    ESCAPED_CHAR: "\\" ("w"|"W"|"d"|"D"|"s"|"S"|"b"|"B"|"A"|"Z"|"G"|"."|"^"|"["|"]"|"("|")"|"{"|"}"|"?"|"*"|"+"|"|"|"\\")
+
+    REGEX_CHAR: /[^\\^$().*+?{}\[\]|]/
+"""
+
 
 class TransformRegulations(Transformer):
     def regulation(self, matches):
@@ -212,9 +248,11 @@ class TransformRegulations(Transformer):
         return matches[0]
 
     def regular(self, matches):
-        re = "".join(matches[1:])
-        # might raise exception
-        regex.compile(re)
+        re = "".join(tree_to_string(matches[1]))
+        try:
+            regex.compile(re)
+        except regex.error as e:
+            raise RegulationParsingError(f"Invalid regular expression: {re}. Error: {e}")
         return Regular(re)
 
     def programmed(self, matches):
@@ -527,6 +565,7 @@ class TreeToObjects(Transformer):
     def __init__(self):
         super(TreeToObjects, self).__init__()
         self.params = set()
+        self.labels = set()
 
     """
     A transformer which is called on a tree in a bottom-up manner and transforms all subtrees/tokens it encounters.
@@ -584,6 +623,8 @@ class TreeToObjects(Transformer):
                 lhs, arrow, rhs, rate1 = matches
         else:
             lhs, arrow, rhs = matches
+        if label:
+            self.labels.add(label)
         agents = tuple(lhs.seq + rhs.seq)
         mid = lhs.counter
         compartments = lhs.comp + rhs.comp
@@ -678,7 +719,9 @@ class TreeToObjects(Transformer):
             elif key == "regulation":
                 if regulation:
                     raise UnspecifiedParsingError("Multiple regulations")
-                regulation = value
+                # check if regulation is in label
+                if value.check_labels(self.labels):
+                    regulation = value
 
         params = self.params - set(definitions)
         return Model(rules, inits, definitions, params, regulation)
@@ -693,6 +736,7 @@ class Parser:
             + COMPLEX_GRAMMAR
             + EXTENDED_GRAMMAR
             + REGULATIONS_GRAMMAR
+            + REGEX_GRAMMAR
         )
         self.parser = Lark(
             grammar, parser="earley", propagate_positions=False, maybe_placeholders=False
diff --git a/eBCSgen/Regulations/ConcurrentFree.py b/eBCSgen/Regulations/ConcurrentFree.py
index ad5022e..282f48f 100644
--- a/eBCSgen/Regulations/ConcurrentFree.py
+++ b/eBCSgen/Regulations/ConcurrentFree.py
@@ -1,3 +1,4 @@
+from eBCSgen.Errors.RegulationParsingError import RegulationParsingError
 from eBCSgen.Regulations.Base import BaseRegulation
 
 
@@ -22,3 +23,10 @@ class ConcurrentFree(BaseRegulation):
             if p_rule and non_p_rule:
                 del candidates[non_p_rule.pop()]
         return candidates
+
+    def check_labels(self, model_labels):
+        for tuple in self.regulation:
+            for label in tuple:
+                if label not in model_labels:
+                    raise RegulationParsingError(f"Label {label} in concurrent-free regulation not present in model")
+        return True
\ No newline at end of file
diff --git a/eBCSgen/Regulations/Conditional.py b/eBCSgen/Regulations/Conditional.py
index a11998f..6f6914b 100644
--- a/eBCSgen/Regulations/Conditional.py
+++ b/eBCSgen/Regulations/Conditional.py
@@ -1,3 +1,4 @@
+from eBCSgen.Errors.RegulationParsingError import RegulationParsingError
 from eBCSgen.Regulations.Base import BaseRegulation
 
 
@@ -19,6 +20,12 @@ class Conditional(BaseRegulation):
     def filter(self, current_state, candidates):
         agents = set(current_state.content.value)
         return {rule: values for rule, values in candidates.items() if not self.check_intersection(rule.label, agents)}
+    
+    def check_labels(self, model_labels):
+        for label in self.regulation:
+            if label not in model_labels:
+                raise RegulationParsingError(f"Label {label} in conditional regulation not present in model")
+        return True
 
     def check_intersection(self, label, agents):
         if label not in self.regulation:
diff --git a/eBCSgen/Regulations/Ordered.py b/eBCSgen/Regulations/Ordered.py
index bb37018..5bae8e8 100644
--- a/eBCSgen/Regulations/Ordered.py
+++ b/eBCSgen/Regulations/Ordered.py
@@ -1,3 +1,4 @@
+from eBCSgen.Errors.RegulationParsingError import RegulationParsingError
 from eBCSgen.Regulations.Base import BaseRegulation
 
 
@@ -38,3 +39,10 @@ class Ordered(BaseRegulation):
             return candidates
         last_rule = current_state.memory.history[-1]
         return {rule: values for rule, values in candidates.items() if not (last_rule, rule.label) in self.regulation}
+    
+    def check_labels(self, model_labels):
+        for tuple in self.regulation:
+            for label in tuple:
+                if label not in model_labels:
+                    raise RegulationParsingError(f"Label {label} in programmed regulation not present in model")
+        return True
\ No newline at end of file
diff --git a/eBCSgen/Regulations/Programmed.py b/eBCSgen/Regulations/Programmed.py
index e5580e5..8492053 100644
--- a/eBCSgen/Regulations/Programmed.py
+++ b/eBCSgen/Regulations/Programmed.py
@@ -1,3 +1,4 @@
+from eBCSgen.Errors.RegulationParsingError import RegulationParsingError
 from eBCSgen.Regulations.Base import BaseRegulation
 
 
@@ -24,3 +25,12 @@ class Programmed(BaseRegulation):
         if last_rule in self.regulation:
             return {rule: values for rule, values in candidates.items() if rule.label in self.regulation[last_rule]}
         return candidates
+    
+    def check_labels(self, model_labels):
+        for rule_label, successors_labels in self.regulation.items():
+            if rule_label not in model_labels:
+                raise RegulationParsingError(f"Label {rule_label} in programmed regulation not present in model")
+            if not successors_labels.issubset(model_labels):
+                missing_labels = successors_labels - model_labels
+                raise RegulationParsingError(f"Label(s) {missing_labels} in programmed regulation not present in model")
+        return True
\ No newline at end of file
diff --git a/eBCSgen/Regulations/Regular.py b/eBCSgen/Regulations/Regular.py
index 2b8f859..7b7497b 100644
--- a/eBCSgen/Regulations/Regular.py
+++ b/eBCSgen/Regulations/Regular.py
@@ -1,4 +1,5 @@
 import regex
+from eBCSgen.Errors.RegulationParsingError import RegulationParsingError
 
 from eBCSgen.Regulations.Base import BaseRegulation
 
@@ -23,3 +24,18 @@ class Regular(BaseRegulation):
         path = "".join(current_state.memory.history)
         return {rule: values for rule, values in candidates.items()
                 if self.regulation.fullmatch(path + rule.label, partial=True) is not None}
+    
+    def check_labels(self, model_labels):
+        patterns = self.regulation.pattern.replace("(", "").replace(")", "").split("|")
+        subpaterns_set = set()
+        for pattern in patterns:
+            subpaterns_set = subpaterns_set.union(set(pattern.split(";")))
+
+        for subpattern in subpaterns_set:
+            subregex = regex.compile(subpattern)
+            if any(subregex.search(label) for label in model_labels):
+                continue
+            raise RegulationParsingError(
+                f"Label in programmed regulation not present in model"
+            )
+        return True
diff --git a/eBCSgen/utils.py b/eBCSgen/utils.py
new file mode 100644
index 0000000..11e6001
--- /dev/null
+++ b/eBCSgen/utils.py
@@ -0,0 +1,14 @@
+from lark import Tree
+
+
+def tree_to_string(tree):
+    """
+    Recursively constructs a list form given lark tree.
+
+    :param tree: given lark tree
+    :return: list of components
+    """
+    if type(tree) == Tree:
+        return sum(list(map(tree_to_string, tree.children)), [])
+    else:
+        return [str(tree)]


Test patch:
diff --git a/Testing/models/regulation5.txt b/Testing/models/regulation5.txt
index fbf34f7..ae05ba3 100644
--- a/Testing/models/regulation5.txt
+++ b/Testing/models/regulation5.txt
@@ -1,3 +1,3 @@
 #! regulation
 type regular
-(r1_Sr1_Tr2|r1_Tr1_Sr2)
+(r1_S;r1_T;r2|r1_T;r1_S;r2)