Просмотр исходного кода

Concrete syntax no longer indentation-based (nightmare to parse). Add indented multi-line code terminals.

Joeri Exelmans 1 год назад
Родитель
Сommit
e875821e70

+ 16 - 0
concrete_syntax/common.py

@@ -0,0 +1,16 @@
+def indent(multiline_string, how_much):
+    lines = multiline_string.split('\n')
+    return '\n'.join([' '*how_much+l for l in lines])
+
+def display_value(val: any, type_name: str, indentation=0):
+    if type_name == "ActionCode":
+        if '\n' in val:
+            return '```\n'+indent(val, indentation+4)+'\n'+' '*indentation+'```'
+        else:
+            return '`'+val+'`'
+    elif type_name == "String":
+        return '"'+val+'"'
+    elif type_name == "Integer" or type_name == "Boolean":
+        return str(val)
+    else:
+        raise Exception("don't know how to display value" + type_name)

+ 3 - 2
concrete_syntax/plantuml/renderer.py

@@ -3,7 +3,7 @@
 from services import scd, od
 from services.bottom.V0 import Bottom
 from transformation import ramify
-import json
+from concrete_syntax.common import display_value
 from uuid import UUID
 
 def render_class_diagram(state, model, prefix_ids=""):
@@ -97,7 +97,8 @@ def render_object_diagram(state, m, mm, render_attributes=True, prefix_ids=""):
                 for attr_name, attr_edge in attributes:
                     slot = m_od.get_slot(obj_node, attr_name)
                     if slot != None:
-                        output += f"\n{attr_name} => {json.dumps(od.read_primitive_value(bottom, slot, mm)[0])}"
+                        val, type_name = od.read_primitive_value(bottom, slot, mm)
+                        output += f"\n{attr_name} => {display_value(val, type_name)}"
             output += '\n}'
 
     output += '\n'

+ 21 - 21
concrete_syntax/textual_od/parser.py

@@ -7,46 +7,34 @@ from services.scd import SCD
 from uuid import UUID
 
 grammar = r"""
-%import common.WS_INLINE
-%ignore WS_INLINE
+%import common.WS
+%ignore WS
 %ignore COMMENT
 
-%declare _INDENT _DEDENT
-
-?start: (_NL | object )*
+?start: object*
 
 IDENTIFIER: /[A-Za-z_][A-Za-z_0-9]*/
-COMMENT: /#.*/
-
-# newline
-_NL: /(\r?\n[\t ]*)+/
+COMMENT: /#[^\n]*\n/
 
 literal: INT
        | STR
        | BOOL
        | CODE
+       | INDENTED_CODE
 
 INT: /[0-9]+/
 STR: /"[^"]*"/
    | /'[^']*'/
 BOOL: "True" | "False"
 CODE: /`[^`]*`/
+INDENTED_CODE: /```[^`]*```/
 
-object: [IDENTIFIER] ":" IDENTIFIER [link_spec] _NL [_INDENT slot+ _DEDENT]
+object: [IDENTIFIER] ":" IDENTIFIER [link_spec] ["{" slot* "}"]
 link_spec: "(" IDENTIFIER "->" IDENTIFIER ")"
-slot: IDENTIFIER "=" literal _NL
+slot: IDENTIFIER "=" literal ";"
 """
 
-
-class TreeIndenter(Indenter):
-    NL_type = '_NL'
-    OPEN_PAREN_types = []
-    CLOSE_PAREN_types = []
-    INDENT_type = '_INDENT'
-    DEDENT_type = '_DEDENT'
-    tab_len = 4
-
-parser = Lark(grammar, parser='lalr', postlex=TreeIndenter())
+parser = Lark(grammar, parser='lalr')
 
 # internal use only
 # just a dumb wrapper to distinguish between code and string
@@ -83,6 +71,18 @@ def parse_od(state, cs_text, mm):
         def CODE(self, token):
             return _Code(str(token[1:-1])) # strip the ``
 
+        def INDENTED_CODE(self, token):
+            skip = 4 # strip the ``` and the following newline character
+            space_count = 0
+            while token[skip+space_count] == " ":
+                space_count += 1
+            lines = token.split('\n')[1:-1]
+            for line in lines:
+                if line[0:space_count] != ' '*space_count:
+                    raise Exception("wrong indentation of INDENTED_CODE")
+            unindented_lines = [l[space_count:] for l in lines]
+            return _Code('\n'.join(unindented_lines))
+
         def literal(self, el):
             return el[0]
 

+ 11 - 16
concrete_syntax/textual_od/renderer.py

@@ -2,17 +2,8 @@
 
 from services import od
 from services.bottom.V0 import Bottom
-import json
-
-def display_value(val: any, type_name: str):
-    if type_name == "ActionCode":
-        return '`'+val+'`'
-    elif type_name == "String":
-        return '"'+val+'"'
-    elif type_name == "Integer" or type_name == "Boolean":
-        return str(val)
-    else:
-        raise Exception("don't know how to display value" + type_name)
+from concrete_syntax.common import display_value
+
 
 def render_od(state, m_id, mm_id, hide_names=True):
     bottom = Bottom(state)
@@ -26,19 +17,23 @@ def render_od(state, m_id, mm_id, hide_names=True):
 
     def write_attributes(object_node):
         o = ""
-        for attr_name, slot_node in m_od.get_slots(object_node):
-            value, type_name = m_od.read_slot(slot_node)
-            o += f"    {attr_name} = {display_value(value, type_name)}\n"
+        slots = m_od.get_slots(object_node)
+        if len(slots) > 0:
+            o += " {"
+            for attr_name, slot_node in slots:
+                value, type_name = m_od.read_slot(slot_node)
+                o += f"\n    {attr_name} = {display_value(value, type_name, indentation=4)};"
+            o += "\n}"
         return o
 
     for class_name, objects in m_od.get_all_objects().items():
         for object_name, object_node in objects.items():
-            output += f"{display_name(object_name)}:{class_name}\n"
+            output += f"\n{display_name(object_name)}:{class_name}"
             output += write_attributes(object_node)
 
     for assoc_name, links in m_od.get_all_links().items():
         for link_name, (link_edge, src_name, tgt_name) in links.items():
-            output += f"{display_name(link_name)}:{assoc_name} ({src_name} -> {tgt_name})\n"
+            output += f"\n{display_name(link_name)}:{assoc_name} ({src_name} -> {tgt_name})"
             # links can also have slots:
             output += write_attributes(link_edge)
 

+ 36 - 20
experiments/exp_scd.py

@@ -62,22 +62,37 @@ def main():
         dsl_mm_cs = """
 # Integer:ModelRef
 Bear:Class
-Animal:Class
-    abstract = True
-Man:Class
-    lower_cardinality = 1
-    upper_cardinality = 2
-#    constraint = `get_value(get_slot(element, "weight")) < 100`
-Man_weight:AttributeLink (Man -> Integer)
-    name = "weight"
-    optional = False
-    constraint = `get_value(get_target(element)) < 100`
-afraidOf:Association (Man -> Animal)
-    target_lower_cardinality = 1
+Animal:Class {
+    abstract = True;
+}
+Man:Class {
+    lower_cardinality = 1;
+    upper_cardinality = 2;
+    constraint = `get_value(get_slot(element, "weight")) < 20`;
+}
+Man_weight:AttributeLink (Man -> Integer) {
+    name = "weight";
+    optional = False;
+    constraint = ```
+        node = get_target(element)
+        get_value(node) < 20
+    ```;
+}
+afraidOf:Association (Man -> Animal) {
+    target_lower_cardinality = 1;
+}
 Man_inh_Animal:Inheritance (Man -> Animal)
 Bear_inh_Animal:Inheritance (Bear -> Animal)
-sum_of_weights:GlobalConstraint
-    constraint = `len(get_all_instances("afraidOf")) <= 1`
+
+not_too_fat:GlobalConstraint {
+    constraint = ```
+        # total weight of all men low enough
+        total_weight = 0
+        for man_name, man_id in get_all_instances("Man"):
+            total_weight += get_value(get_slot(man_id, "weight"))
+        total_weight < 50
+    ```;
+}
 """
         dsl_mm_id = parser.parse_od(state, dsl_mm_cs, mm=scd_mm_id)
         return dsl_mm_id
@@ -98,8 +113,9 @@ sum_of_weights:GlobalConstraint
     def create_dsl_m_parser():
         # Create DSL M with parser
         dsl_m_cs = """
-george :Man 
-    weight = 80
+george :Man {
+    weight = 80;
+}
 bear1:Bear
 bear2:Bear
 :afraidOf (george -> bear1)
@@ -139,7 +155,7 @@ bear2:Bear
     lhs_id = state.create_node()
     lhs_od = OD(ramified_mm_id, lhs_id, state)
     lhs_od.create_object("man", prefix+"Man")
-    lhs_od.create_slot(prefix+"weight", "man", lhs_od.create_string_value(f"man.{prefix}weight", 'v < 99'))
+    lhs_od.create_slot(prefix+"weight", "man", lhs_od.create_actioncode_value(f"man.{prefix}weight", 'v < 99'))
     lhs_od.create_object("scaryAnimal", prefix+"Animal")
     lhs_od.create_link("manAfraidOfAnimal", prefix+"afraidOf", "man", "scaryAnimal")
 
@@ -150,9 +166,9 @@ bear2:Bear
     rhs_id = state.create_node()
     rhs_od = OD(ramified_mm_id, rhs_id, state)
     rhs_od.create_object("man", prefix+"Man")
-    rhs_od.create_slot(prefix+"weight", "man", rhs_od.create_string_value(f"man.{prefix}weight", 'v + 5'))
+    rhs_od.create_slot(prefix+"weight", "man", rhs_od.create_actioncode_value(f"man.{prefix}weight", 'v + 5'))
     rhs_od.create_object("bill", prefix+"Man")
-    rhs_od.create_slot(prefix+"weight", "bill", rhs_od.create_string_value(f"bill.{prefix}weight", '100'))
+    rhs_od.create_slot(prefix+"weight", "bill", rhs_od.create_actioncode_value(f"bill.{prefix}weight", '100'))
 
     rhs_od.create_link("billAfraidOfMan", prefix+"afraidOf", "bill", "man")
 
@@ -229,7 +245,7 @@ bear2:Bear
     # plantuml_str = render_rewrite()
 
     # print()
-    # print("==============================================")
+    print("==============================================")
 
     # print(plantuml_str)
 

+ 24 - 11
framework/conformance.py

@@ -9,6 +9,17 @@ from pprint import pprint
 import functools
 
 
+# based on https://stackoverflow.com/a/39381428
+# Parses and executes a block of Python code, and returns the eval result of the last statement
+import ast
+def exec_then_eval(code, _globals, _locals):
+    block = ast.parse(code, mode='exec')
+    # assumes last node is an expression
+    last = ast.Expression(block.body.pop().value)
+    exec(compile(block, '<string>', mode='exec'), _globals, _locals)
+    return eval(compile(last, '<string>', mode='eval'), _globals, _locals)
+
+
 class Conformance:
     def __init__(self, state: State, model: UUID, type_model: UUID):
         self.state = state
@@ -368,12 +379,13 @@ class Conformance:
             'get_all_instances': self.get_all_instances
         }
         # print("evaluating constraint ...", code)
-        result = eval(
+        loc = {**kwargs, **funcs}
+        result = exec_then_eval(
             code,
             {'__builtins__': {'isinstance': isinstance, 'print': print,
                               'int': int, 'float': float, 'bool': bool, 'str': str, 'tuple': tuple, 'len': len}
              },  # globals
-            {**kwargs, **funcs}  # locals
+             loc # locals
         )
         # print('result =', result)
         return result
@@ -384,7 +396,8 @@ class Conformance:
             for subtype_name in self.sub_types[type_name]:
                 # print(subtype_name, 'is subtype of ')
                 result += [e_name for e_name, t_name in self.type_mapping.items() if t_name == subtype_name]
-        return result
+        result_with_ids = [ (e_name, self.bottom.read_outgoing_elements(self.model, e_name)[0]) for e_name in result]
+        return result_with_ids
 
     def check_constraints(self):
         """
@@ -399,12 +412,11 @@ class Conformance:
                 code = ActionCode(UUID(self.bottom.read_value(constraint)), self.bottom.state).read()
                 return code
 
-        def check_result(result, local_or_global, tm_name, el_name=None):
-            suffix = f"in '{el_name}'" if local_or_global == "Local" else ""
+        def check_result(result, description):
             if not isinstance(result, bool):
-                errors.append(f"{local_or_global} constraint `{code}` of '{tm_name}'{suffix} did not return boolean, instead got {type(result)} (value = {str(result)}).")
-            elif not result:
-                errors.append(f"{local_or_global} constraint `{code}` of '{tm_name}'{suffix} not satisfied.")
+                raise Exception(f"{description} evaluation result is not boolean! Instead got {result}")
+            if not result:
+                errors.append(f"{description} not satisfied.")
 
         # local constraints
         for m_name, tm_name in self.type_mapping.items():
@@ -415,7 +427,8 @@ class Conformance:
                 morphisms = [m for m in morphisms if m in self.model_names]
                 for m_element in morphisms:
                     result = self.evaluate_constraint(code, element=m_element, type_name=tm_name)
-                    check_result(result, "Local", tm_name, m_name)
+                    description = f"Local constraint of \"{tm_name}\" in \"{m_name}\""
+                    check_result(result, description)
 
         # global constraints
         glob_constraints = []
@@ -432,9 +445,9 @@ class Conformance:
         for tm_name in glob_constraints:
             code = get_code(tm_name)
             if code != None:
-                # print('glob constr:', code)
                 result = self.evaluate_constraint(code, model=self.model)
-                check_result(result, "Global", tm_name)
+                description = f"Global constraint \"{tm_name}\""
+                check_result(result, description)
         return errors
 
     def precompute_structures(self):

+ 5 - 1
transformation/ramify.py

@@ -15,12 +15,16 @@ def ramify(state: State, model: UUID, prefix = "RAM_") -> UUID:
     string_type_id = state.read_dict(state.read_root(), "String")
     string_type = UUID(state.read_value(string_type_id))
 
+    actioncode_type_id = state.read_dict(state.read_root(), "ActionCode")
+    actioncode_type = UUID(state.read_value(actioncode_type_id))
+
     m_scd = scd.SCD(model, state)
 
     ramified = state.create_node()
     ramified_scd = scd.SCD(ramified, state)
 
     string_modelref = ramified_scd.create_model_ref("String", string_type)
+    actioncode_modelref = ramified_scd.create_model_ref("ActionCode", actioncode_type)
 
     classes = m_scd.get_classes()
     for class_name, class_node in classes.items():
@@ -44,7 +48,7 @@ def ramify(state: State, model: UUID, prefix = "RAM_") -> UUID:
             # print('  creating attribute', attr_name, "with type String")
             # Every attribute becomes 'string' type
             # The string will be a Python expression
-            ramified_attr_link = ramified_scd._create_attribute_link(prefix+class_name, string_modelref, prefix+attr_name, optional=True)
+            ramified_attr_link = ramified_scd._create_attribute_link(prefix+class_name, actioncode_modelref, prefix+attr_name, optional=True)
             # traceability link
             bottom.create_edge(ramified_attr_link, attr_edge, RAMIFIES_LABEL)
 

+ 4 - 3
transformation/rewriter.py

@@ -8,6 +8,7 @@ from services.bottom.V0 import Bottom
 from transformation import ramify
 from services import od
 from services.primitives.string_type import String
+from services.primitives.actioncode_type import ActionCode
 from services.primitives.integer_type import Integer
 
 def process_rule(state, lhs: UUID, rhs: UUID):
@@ -90,9 +91,9 @@ def rewrite(state, lhs_m: UUID, rhs_m: UUID, pattern_mm: UUID, name_mapping: dic
             # assume the type of the object is already the original type
             # this is because primitive types (e.g., Integer) are not RAMified
             type_name = od.get_object_name(bottom, pattern_mm, rhs_type)
-            if type_name == "String":
+            if type_name == "ActionCode":
                 # Assume the string is a Python expression to evaluate
-                python_expr = String(UUID(bottom.read_value(rhs_el_to_create)), bottom.state).read()
+                python_expr = ActionCode(UUID(bottom.read_value(rhs_el_to_create)), bottom.state).read()
                 result = eval(python_expr, {}, {})
                 # Write the result into the host model.
                 # This will be the *value* of an attribute. The attribute-link (connecting an object to the attribute) will be created as an edge later.
@@ -102,7 +103,7 @@ def rewrite(state, lhs_m: UUID, rhs_m: UUID, pattern_mm: UUID, name_mapping: dic
                     m_od.create_string_value(model_el_name_to_create, result)
                 name_mapping[pattern_name_to_create] = model_el_name_to_create
             else:
-                raise Exception(f"RHS element '{pattern_name_to_create}' needs to be created in host, but has no un-RAMified type, and I don't know what to do with it. It's type is", type_name)
+                raise Exception(f"RHS element '{pattern_name_to_create}' needs to be created in host, but has no un-RAMified type, and I don't know what to do with it. It's type is '{type_name}'")
 
     # print("create edges....")
     for pattern_name_to_create, rhs_el_to_create, original_type, original_type_name, model_el_name_to_create in edges_to_create: