From e875821e70e53622c634197d36a98077a93e9c69 Mon Sep 17 00:00:00 2001 From: Joeri Exelmans Date: Mon, 7 Oct 2024 18:18:05 +0200 Subject: [PATCH] Concrete syntax no longer indentation-based (nightmare to parse). Add indented multi-line code terminals. --- concrete_syntax/common.py | 16 ++++++++ concrete_syntax/plantuml/renderer.py | 5 ++- concrete_syntax/textual_od/parser.py | 42 +++++++++---------- concrete_syntax/textual_od/renderer.py | 25 +++++------- experiments/exp_scd.py | 56 +++++++++++++++++--------- framework/conformance.py | 35 +++++++++++----- transformation/ramify.py | 6 ++- transformation/rewriter.py | 7 ++-- 8 files changed, 119 insertions(+), 73 deletions(-) create mode 100644 concrete_syntax/common.py diff --git a/concrete_syntax/common.py b/concrete_syntax/common.py new file mode 100644 index 0000000..d3ba5d0 --- /dev/null +++ b/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) diff --git a/concrete_syntax/plantuml/renderer.py b/concrete_syntax/plantuml/renderer.py index 56e4a54..0e18e86 100644 --- a/concrete_syntax/plantuml/renderer.py +++ b/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' diff --git a/concrete_syntax/textual_od/parser.py b/concrete_syntax/textual_od/parser.py index 0aeebe8..d7a8dee 100644 --- a/concrete_syntax/textual_od/parser.py +++ b/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] diff --git a/concrete_syntax/textual_od/renderer.py b/concrete_syntax/textual_od/renderer.py index 8968add..37c20b9 100644 --- a/concrete_syntax/textual_od/renderer.py +++ b/concrete_syntax/textual_od/renderer.py @@ -2,17 +2,8 @@ from services import od from services.bottom.V0 import Bottom -import json +from concrete_syntax.common import display_value -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) 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) diff --git a/experiments/exp_scd.py b/experiments/exp_scd.py index c3233eb..580dd5d 100644 --- a/experiments/exp_scd.py +++ b/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) diff --git a/framework/conformance.py b/framework/conformance.py index 94395fc..3d7d27f 100644 --- a/framework/conformance.py +++ b/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, '', mode='exec'), _globals, _locals) + return eval(compile(last, '', 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): diff --git a/transformation/ramify.py b/transformation/ramify.py index f3aa1ef..275f0e7 100644 --- a/transformation/ramify.py +++ b/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) diff --git a/transformation/rewriter.py b/transformation/rewriter.py index 5477a05..1436cc2 100644 --- a/transformation/rewriter.py +++ b/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: