165 lines
5.7 KiB
Python
165 lines
5.7 KiB
Python
# 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
|
|
| INDENTED_CODE
|
|
|
|
INT: /[0-9]+/
|
|
STR: /"[^"]*"/
|
|
| /'[^']*'/
|
|
BOOL: "True" | "False"
|
|
CODE: /`[^`]*`/
|
|
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"]
|
|
}
|
|
|
|
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 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
|