# Parser for Object Diagrams textual concrete syntax from lark import Lark, logger, Transformer from lark.indenter import Indenter from api.od import ODAPI from services.scd import SCD from concrete_syntax.common import _Code from uuid import UUID grammar = r""" %import common.WS %ignore WS %ignore COMMENT ?start: object* IDENTIFIER: /[A-Za-z_][A-Za-z_0-9]*/ COMMENT: /#[^\n]*/ literal: INT | STR | BOOL | CODE | BYTES | INDENTED_CODE INT: /[0-9]+/ STR: /"[^"]*"/ | /'[^']*'/ BOOL: "True" | "False" CODE: /`[^`]*`/ BYTES: /b"[^"]*"/ | /b'[^']*'/ INDENTED_CODE: /```[^`]*```/ type_name: IDENTIFIER # name (optional) type object: [IDENTIFIER] ":" type_name [link_spec | rev_link_spec] ["{" slot* "}"] link_spec: "(" IDENTIFIER "->" IDENTIFIER ")" rev_link_spec: "(" IDENTIFIER "<-" IDENTIFIER ")" slot: IDENTIFIER "=" literal ";" """ parser = Lark(grammar, parser='lalr', propagate_positions=True) class DefaultNameGenerator: def __init__(self): self.counter = 0 def __call__(self, type_name): name = f"__{type_name}_{self.counter}" self.counter += 1 return name # given a concrete syntax text string, and a meta-model, parses the CS # Parameter 'type_transform' is useful for adding prefixes to the type names, when parsing a model and pretending it is an instance of a prefixed meta-model. def parse_od(state, m_text, # text to parse mm, # meta-model of model that will be parsed. The meta-model must already have been parsed. type_transform=lambda type_name: type_name, name_generator=DefaultNameGenerator(), # exception to raise if anonymous (nameless) object/link occurs in the model. Main reason for this is to forbid them in LHS of transformation rules. ): tree = parser.parse(m_text) m = state.create_node() od = ODAPI(state, m, mm) primitive_types = { type_name : UUID(state.read_value(state.read_dict(state.read_root(), type_name))) for type_name in ["Integer", "String", "Boolean", "ActionCode", "Bytes"] } class T(Transformer): def __init__(self, visit_tokens): super().__init__(visit_tokens) def IDENTIFIER(self, token): return (str(token), token.line) def INT(self, token): return (int(token), token.line) def BOOL(self, token): return (token == "True", token.line) def STR(self, token): return (str(token[1:-1]), token.line) # strip the "" or '' def CODE(self, token): return (_Code(str(token[1:-1])), token.line) # strip the `` def BYTES(self, token): # Strip b"" or b'', and make \\ back to \ (happens when reading the file as a string) return (token[2:-1].encode().decode('unicode_escape').encode('raw_unicode_escape'), token.line) # Strip b"" or b'' 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 len(line) >= space_count and 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)), token.line) def literal(self, el): return el[0] def link_spec(self, el): [(src, src_line), (tgt, _)] = el return (src, tgt, src_line) def rev_link_spec(self, el): [(tgt, tgt_line), (src, _)] = el # <-- reversed :) return (src, tgt, tgt_line) def type_name(self, el): type_name, line = el[0] if type_name in primitive_types: return (type_name, line) else: return (type_transform(type_name), line) def slot(self, el): [(attr_name, line), (value, _)] = el return (attr_name, value, line) def object(self, el): [obj, (type_name, line), link] = el[0:3] slots = el[3:] try: if obj != None: (obj_name, _) = obj else: # anonymous object - auto-generate a name obj_name = name_generator(type_name) if state.read_dict(m, obj_name) != None: msg = f"Element '{obj_name}:{type_name}': name '{obj_name}' already in use." raise Exception(msg + " Names must be unique") # print(msg + " Ignoring.") return if link == None: obj_node = od.create_object(obj_name, type_name) else: (src, tgt, _) = link if tgt in primitive_types: if state.read_dict(m, tgt) == None: scd = SCD(m, state) scd.create_model_ref(tgt, primitive_types[tgt]) src_obj = od.get(src) tgt_obj = od.get(tgt) obj_node = od.create_link(obj_name, type_name, src_obj, tgt_obj) # Create slots for attr_name, value, line in slots: if isinstance(value, _Code): od.set_slot_value(obj_node, attr_name, value.code, is_code=True) else: od.set_slot_value(obj_node, attr_name, value) return obj_name except Exception as e: # raising a *new* exception (instead of adding a note to the existing exception) because Lark will also raise a new exception, and ignore our note: raise Exception(f"at line {line}:\n " + m_text.split('\n')[line-1] + "\n"+ str(e)) from e t = T(visit_tokens=True).transform(tree) return m