Add concrete syntax for class diagrams + example (woods2.py)
This commit is contained in:
parent
c248fc9090
commit
52ded8af77
6 changed files with 311 additions and 58 deletions
|
|
@ -1,3 +1,5 @@
|
||||||
|
from lark import Transformer
|
||||||
|
|
||||||
def indent(multiline_string, how_much):
|
def indent(multiline_string, how_much):
|
||||||
lines = multiline_string.split('\n')
|
lines = multiline_string.split('\n')
|
||||||
return '\n'.join([' '*how_much+l for l in lines])
|
return '\n'.join([' '*how_much+l for l in lines])
|
||||||
|
|
@ -14,3 +16,42 @@ def display_value(val: any, type_name: str, indentation=0):
|
||||||
return str(val)
|
return str(val)
|
||||||
else:
|
else:
|
||||||
raise Exception("don't know how to display value" + type_name)
|
raise Exception("don't know how to display value" + type_name)
|
||||||
|
|
||||||
|
|
||||||
|
# internal use only
|
||||||
|
# just a dumb wrapper to distinguish between code and string
|
||||||
|
class _Code:
|
||||||
|
def __init__(self, code):
|
||||||
|
self.code = code
|
||||||
|
|
||||||
|
class TBase(Transformer):
|
||||||
|
|
||||||
|
def IDENTIFIER(self, token):
|
||||||
|
return str(token)
|
||||||
|
|
||||||
|
def INT(self, token):
|
||||||
|
return int(token)
|
||||||
|
|
||||||
|
def BOOL(self, token):
|
||||||
|
return token == "True"
|
||||||
|
|
||||||
|
def STR(self, token):
|
||||||
|
return str(token[1:-1]) # strip the "" or ''
|
||||||
|
|
||||||
|
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 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))
|
||||||
|
|
||||||
|
def literal(self, el):
|
||||||
|
return el[0]
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,142 @@
|
||||||
|
from lark import Lark, logger
|
||||||
|
from concrete_syntax.common import _Code, TBase
|
||||||
|
from uuid import UUID
|
||||||
|
from services.scd import SCD
|
||||||
|
from services.od import OD
|
||||||
|
|
||||||
grammar = r"""
|
grammar = r"""
|
||||||
%import common.WS
|
%import common.WS
|
||||||
%ignore WS
|
%ignore WS
|
||||||
%ignore COMMENT
|
%ignore COMMENT
|
||||||
|
|
||||||
?start: object*
|
?start: (class_ | association | global_constraint)*
|
||||||
|
|
||||||
IDENTIFIER: /[A-Za-z_][A-Za-z_0-9]*/
|
IDENTIFIER: /[A-Za-z_][A-Za-z_0-9]*/
|
||||||
COMMENT: /#[^\n]*\n/
|
|
||||||
|
|
||||||
# newline
|
COMMENT: /#[^\n]*\n/
|
||||||
_NL: /(\r?\n[\t ]*)+/
|
|
||||||
|
|
||||||
literal: INT
|
literal: INT
|
||||||
| STR
|
| STR
|
||||||
| BOOL
|
| BOOL
|
||||||
|
| CODE
|
||||||
|
| INDENTED_CODE
|
||||||
|
|
||||||
INT: /[0-9]+/
|
INT: /[0-9]+/
|
||||||
STR: /"[^"]*"/
|
STR: /"[^"]*"/
|
||||||
| /'[^']*'/
|
| /'[^']*'/
|
||||||
BOOL: "True" | "False"
|
BOOL: "True" | "False"
|
||||||
|
CODE: /`[^`]*`/
|
||||||
|
INDENTED_CODE: /```[^`]*```/
|
||||||
|
|
||||||
object: [IDENTIFIER] ":" IDENTIFIER [link] _NL [_INDENT slot+ _DEDENT]
|
INT_OR_INF: INT | "*"
|
||||||
link: "(" IDENTIFIER "->" IDENTIFIER ")"
|
|
||||||
slot: IDENTIFIER "=" literal _NL
|
multiplicity: "[" INT ".." INT_OR_INF "]"
|
||||||
|
|
||||||
|
ABSTRACT: "abstract"
|
||||||
|
|
||||||
|
superclasses: IDENTIFIER ("," IDENTIFIER)*
|
||||||
|
|
||||||
|
attrs: attr*
|
||||||
|
|
||||||
|
constraint: CODE | INDENTED_CODE
|
||||||
|
|
||||||
|
class_: [ABSTRACT] "class" IDENTIFIER [multiplicity] ["(" superclasses ")"] ["{" attrs [constraint] "}"]
|
||||||
|
|
||||||
|
association: "association" IDENTIFIER [multiplicity] IDENTIFIER "->" IDENTIFIER [multiplicity] ["{" [constraint] "}"]
|
||||||
|
|
||||||
|
OPTIONAL: "optional"
|
||||||
|
|
||||||
|
attr: [OPTIONAL] IDENTIFIER IDENTIFIER [constraint] ";"
|
||||||
|
|
||||||
|
global_constraint: "global" IDENTIFIER constraint
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
parser = Lark(grammar, parser='lalr')
|
||||||
|
|
||||||
|
def _handle_missing_multiplicity(multiplicity):
|
||||||
|
if multiplicity != None:
|
||||||
|
return multiplicity
|
||||||
|
else:
|
||||||
|
return (None, None)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_cd(state, m_text):
|
||||||
|
type_model_id = state.read_dict(state.read_root(), "SCD")
|
||||||
|
scd_mmm = UUID(state.read_value(type_model_id))
|
||||||
|
|
||||||
|
m = state.create_node()
|
||||||
|
cd = SCD(m, state)
|
||||||
|
od = OD(scd_mmm, m, state)
|
||||||
|
|
||||||
|
def _add_constraint_to_obj(obj_name, constraint):
|
||||||
|
c = od.create_actioncode_value(f"{obj_name}.constraint", constraint.code)
|
||||||
|
od.create_slot("constraint", obj_name, c)
|
||||||
|
|
||||||
|
primitive_types = {
|
||||||
|
type_name : UUID(state.read_value(state.read_dict(state.read_root(), type_name)))
|
||||||
|
for type_name in ["Integer", "String", "Boolean"]
|
||||||
|
}
|
||||||
|
|
||||||
|
class T(TBase):
|
||||||
|
def __init__(self, visit_tokens):
|
||||||
|
super().__init__(visit_tokens)
|
||||||
|
self.obj_counter = 0
|
||||||
|
|
||||||
|
def ABSTRACT(self, el):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def INT_OR_INF(self, el):
|
||||||
|
return float('inf') if el == "*" else int(el)
|
||||||
|
|
||||||
|
def multiplicity(self, el):
|
||||||
|
[lower, upper] = el
|
||||||
|
return (lower, upper)
|
||||||
|
|
||||||
|
def superclasses(self, el):
|
||||||
|
return list(el)
|
||||||
|
|
||||||
|
def attrs(self, el):
|
||||||
|
return list(el)
|
||||||
|
|
||||||
|
def constraint(self, el):
|
||||||
|
return el[0]
|
||||||
|
|
||||||
|
def attr(self, el):
|
||||||
|
[optional, attr_type, attr_name, constraint] = el
|
||||||
|
return (optional == "optional", attr_type, attr_name, constraint)
|
||||||
|
|
||||||
|
def global_constraint(self, el):
|
||||||
|
[name, constraint] = el
|
||||||
|
od.create_object(name, "GlobalConstraint")
|
||||||
|
_add_constraint_to_obj(name, constraint)
|
||||||
|
|
||||||
|
def class_(self, el):
|
||||||
|
[abstract, class_name, multiplicity, super_classes, attrs, constraint] = el
|
||||||
|
(lower, upper) = _handle_missing_multiplicity(multiplicity)
|
||||||
|
cd.create_class(class_name, abstract, lower, upper)
|
||||||
|
if super_classes != None:
|
||||||
|
for super_class in super_classes:
|
||||||
|
cd.create_inheritance(class_name, super_class)
|
||||||
|
if constraint != None:
|
||||||
|
_add_constraint_to_obj(class_name, constraint)
|
||||||
|
if attrs != None:
|
||||||
|
for attr in attrs:
|
||||||
|
(optional, attr_type, attr_name, constraint) = attr
|
||||||
|
# TODO: only create type ref if it doesn't exist yet
|
||||||
|
cd.create_model_ref(attr_type, primitive_types[attr_type])
|
||||||
|
cd.create_attribute_link(class_name, attr_type, attr_name, optional)
|
||||||
|
if constraint != None:
|
||||||
|
_add_constraint_to_obj(f"{class_name}_{attr_name}", constraint)
|
||||||
|
|
||||||
|
def association(self, el):
|
||||||
|
[assoc_name, src_multiplicity, src_name, tgt_name, tgt_multiplicity, constraint] = el
|
||||||
|
(src_lower, src_upper) = _handle_missing_multiplicity(src_multiplicity)
|
||||||
|
(tgt_lower, tgt_upper) = _handle_missing_multiplicity(tgt_multiplicity)
|
||||||
|
cd.create_association(assoc_name, src_name, tgt_name, src_lower, src_upper, tgt_lower, tgt_upper)
|
||||||
|
if constraint != None:
|
||||||
|
_add_constraint_to_obj(class_name, constraint)
|
||||||
|
|
||||||
|
tree = parser.parse(m_text)
|
||||||
|
t = T(visit_tokens=True).transform(tree)
|
||||||
|
|
||||||
|
return m
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
# Parser for Object Diagrams textual concrete syntax
|
# Parser for Object Diagrams textual concrete syntax
|
||||||
|
|
||||||
from lark import Lark, logger, Transformer
|
from lark import Lark, logger
|
||||||
from lark.indenter import Indenter
|
from lark.indenter import Indenter
|
||||||
from services.od import OD
|
from services.od import OD
|
||||||
from services.scd import SCD
|
from services.scd import SCD
|
||||||
|
from concrete_syntax.common import _Code, TBase
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
grammar = r"""
|
grammar = r"""
|
||||||
|
|
@ -39,12 +40,6 @@ slot: IDENTIFIER "=" literal ";"
|
||||||
|
|
||||||
parser = Lark(grammar, parser='lalr')
|
parser = Lark(grammar, parser='lalr')
|
||||||
|
|
||||||
# internal use only
|
|
||||||
# just a dumb wrapper to distinguish between code and string
|
|
||||||
class _Code:
|
|
||||||
def __init__(self, code):
|
|
||||||
self.code = code
|
|
||||||
|
|
||||||
# given a concrete syntax text string, and a meta-model, parses the CS
|
# given a concrete syntax text string, and a meta-model, parses the CS
|
||||||
def parse_od(state, m_text, mm):
|
def parse_od(state, m_text, mm):
|
||||||
tree = parser.parse(m_text)
|
tree = parser.parse(m_text)
|
||||||
|
|
@ -54,41 +49,11 @@ def parse_od(state, m_text, mm):
|
||||||
|
|
||||||
int_mm_id = UUID(state.read_value(state.read_dict(state.read_root(), "Integer")))
|
int_mm_id = UUID(state.read_value(state.read_dict(state.read_root(), "Integer")))
|
||||||
|
|
||||||
class T(Transformer):
|
class T(TBase):
|
||||||
def __init__(self, visit_tokens):
|
def __init__(self, visit_tokens):
|
||||||
super().__init__(visit_tokens)
|
super().__init__(visit_tokens)
|
||||||
self.obj_counter = 0
|
self.obj_counter = 0
|
||||||
|
|
||||||
def IDENTIFIER(self, token):
|
|
||||||
return str(token)
|
|
||||||
|
|
||||||
def INT(self, token):
|
|
||||||
return int(token)
|
|
||||||
|
|
||||||
def BOOL(self, token):
|
|
||||||
return token == "True"
|
|
||||||
|
|
||||||
def STR(self, token):
|
|
||||||
return str(token[1:-1]) # strip the "" or ''
|
|
||||||
|
|
||||||
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 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))
|
|
||||||
|
|
||||||
def literal(self, el):
|
|
||||||
return el[0]
|
|
||||||
|
|
||||||
def link_spec(self, el):
|
def link_spec(self, el):
|
||||||
[src, tgt] = el
|
[src, tgt] = el
|
||||||
return (src, tgt)
|
return (src, tgt)
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ woods_mm_cs = """
|
||||||
Man:Class {
|
Man:Class {
|
||||||
# We can define lower and upper cardinalities on Classes
|
# We can define lower and upper cardinalities on Classes
|
||||||
# (if unspecified, the lower-card is 0, and upper-card is infinity)
|
# (if unspecified, the lower-card is 0, and upper-card is infinity)
|
||||||
|
|
||||||
lower_cardinality = 1; # there must be at least one Man in every model
|
lower_cardinality = 1; # there must be at least one Man in every model
|
||||||
upper_cardinality = 2; # there must be at most two Men in every model
|
upper_cardinality = 2; # there must be at most two Men in every model
|
||||||
|
|
||||||
|
|
|
||||||
130
examples/conformance/woods2.py
Normal file
130
examples/conformance/woods2.py
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
from state.devstate import DevState
|
||||||
|
from bootstrap.scd import bootstrap_scd
|
||||||
|
from framework.conformance import Conformance, render_conformance_check_result
|
||||||
|
from concrete_syntax.textual_cd import parser as parser_cd
|
||||||
|
from concrete_syntax.textual_od import parser as parser_od
|
||||||
|
from concrete_syntax.textual_od import renderer as renderer_od
|
||||||
|
from concrete_syntax.common import indent
|
||||||
|
from concrete_syntax.plantuml import renderer as plantuml
|
||||||
|
from util.prompt import yes_no, pause
|
||||||
|
|
||||||
|
state = DevState()
|
||||||
|
|
||||||
|
print("Loading meta-meta-model...")
|
||||||
|
scd_mmm = bootstrap_scd(state)
|
||||||
|
print("OK")
|
||||||
|
|
||||||
|
print("Is our meta-meta-model a valid class diagram?")
|
||||||
|
conf = Conformance(state, scd_mmm, scd_mmm)
|
||||||
|
print(render_conformance_check_result(conf.check_nominal()))
|
||||||
|
|
||||||
|
# If you are curious, you can serialize the meta-meta-model:
|
||||||
|
# print("--------------")
|
||||||
|
# print(indent(
|
||||||
|
# renderer.render_od(state,
|
||||||
|
# m_id=scd_mmm,
|
||||||
|
# mm_id=scd_mmm),
|
||||||
|
# 4))
|
||||||
|
# print("--------------")
|
||||||
|
|
||||||
|
|
||||||
|
# Change this:
|
||||||
|
woods_mm_cs = """
|
||||||
|
abstract class Animal
|
||||||
|
|
||||||
|
class Bear (Animal)
|
||||||
|
|
||||||
|
class Man [1..2] (Animal) {
|
||||||
|
Integer weight `get_value(get_target(this)) > 20`; # <- constraint in context of attribute-link
|
||||||
|
|
||||||
|
`get_value(get_slot(this, "weight")) > 20` # <- constraint in context of Man-object
|
||||||
|
}
|
||||||
|
|
||||||
|
association afraidOf [0..6] Man -> Animal [1..2]
|
||||||
|
|
||||||
|
global total_weight_small_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 < 85
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("Parsing 'woods' meta-model...")
|
||||||
|
woods_mm = parser_cd.parse_cd(
|
||||||
|
state,
|
||||||
|
m_text=woods_mm_cs, # the string of text to parse
|
||||||
|
)
|
||||||
|
print("OK")
|
||||||
|
|
||||||
|
# As a double-check, you can serialize the parsed model:
|
||||||
|
print("--------------")
|
||||||
|
print(indent(
|
||||||
|
renderer_od.render_od(state,
|
||||||
|
m_id=woods_mm,
|
||||||
|
mm_id=scd_mmm),
|
||||||
|
4))
|
||||||
|
print("--------------")
|
||||||
|
|
||||||
|
print("Is our 'woods' meta-model a valid class diagram?")
|
||||||
|
conf = Conformance(state, woods_mm, scd_mmm)
|
||||||
|
print(render_conformance_check_result(conf.check_nominal()))
|
||||||
|
|
||||||
|
# Change this:
|
||||||
|
woods_m_cs = """
|
||||||
|
george:Man {
|
||||||
|
weight = 15;
|
||||||
|
}
|
||||||
|
billy:Man {
|
||||||
|
weight = 100;
|
||||||
|
}
|
||||||
|
bear1:Bear
|
||||||
|
bear2:Bear
|
||||||
|
:afraidOf (george -> bear1)
|
||||||
|
:afraidOf (george -> bear2)
|
||||||
|
"""
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("Parsing 'woods' model...")
|
||||||
|
woods_m = parser_od.parse_od(
|
||||||
|
state,
|
||||||
|
m_text=woods_m_cs,
|
||||||
|
mm=woods_mm, # this time, the meta-model is the previous model we parsed
|
||||||
|
)
|
||||||
|
print("OK")
|
||||||
|
|
||||||
|
# As a double-check, you can serialize the parsed model:
|
||||||
|
# print("--------------")
|
||||||
|
# print(indent(
|
||||||
|
# renderer.render_od(state,
|
||||||
|
# m_id=woods_m,
|
||||||
|
# mm_id=woods_mm),
|
||||||
|
# 4))
|
||||||
|
# print("--------------")
|
||||||
|
|
||||||
|
print("Is our model a valid woods-diagram?")
|
||||||
|
conf = Conformance(state, woods_m, woods_mm)
|
||||||
|
print(render_conformance_check_result(conf.check_nominal()))
|
||||||
|
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("==================================")
|
||||||
|
if yes_no("Print PlantUML?"):
|
||||||
|
print_mm = yes_no(" ▸ Print meta-model?")
|
||||||
|
print_m = yes_no(" ▸ Print model?")
|
||||||
|
print_conf = print_mm and print_m and yes_no(" ▸ Print conformance links?")
|
||||||
|
|
||||||
|
uml = ""
|
||||||
|
if print_mm:
|
||||||
|
uml += plantuml.render_package("Meta-model", plantuml.render_class_diagram(state, woods_mm))
|
||||||
|
if print_m:
|
||||||
|
uml += plantuml.render_package("Model", plantuml.render_object_diagram(state, woods_m, woods_mm))
|
||||||
|
if print_conf:
|
||||||
|
uml += plantuml.render_trace_conformance(state, woods_m, woods_mm)
|
||||||
|
|
||||||
|
print("==================================")
|
||||||
|
print(uml)
|
||||||
|
print("==================================")
|
||||||
|
print("Go to http://www.plantuml.com/plantuml/uml/")
|
||||||
|
print("and paste the above string.")
|
||||||
|
|
@ -132,21 +132,21 @@ class SCD:
|
||||||
set_cardinality("target_upper", tgt_max_c)
|
set_cardinality("target_upper", tgt_max_c)
|
||||||
return assoc_edge
|
return assoc_edge
|
||||||
|
|
||||||
def create_global_constraint(self, name: str):
|
# def create_global_constraint(self, name: str):
|
||||||
"""
|
# """
|
||||||
Defines a global constraint element.
|
# Defines a global constraint element.
|
||||||
|
|
||||||
Args:
|
# Args:
|
||||||
name: the name of the global constraint to be created
|
# name: the name of the global constraint to be created
|
||||||
|
|
||||||
Returns:
|
# Returns:
|
||||||
Nothing.
|
# Nothing.
|
||||||
"""
|
# """
|
||||||
# create element + morphism links
|
# # create element + morphism links
|
||||||
element_node = self.bottom.create_node() # create element node
|
# element_node = self.bottom.create_node() # create element node
|
||||||
self.bottom.create_edge(self.model, element_node, name) # attach to model
|
# self.bottom.create_edge(self.model, element_node, name) # attach to model
|
||||||
scd_node, = self.bottom.read_outgoing_elements(self.scd_model, "GlobalConstraint") # retrieve type
|
# scd_node, = self.bottom.read_outgoing_elements(self.scd_model, "GlobalConstraint") # retrieve type
|
||||||
self.bottom.create_edge(element_node, scd_node, "Morphism") # create morphism link
|
# self.bottom.create_edge(element_node, scd_node, "Morphism") # create morphism link
|
||||||
|
|
||||||
def create_attribute(self, name: str):
|
def create_attribute(self, name: str):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue