Add concrete syntax for class diagrams + example (woods2.py)

This commit is contained in:
Joeri Exelmans 2024-10-09 15:09:16 +02:00
parent c248fc9090
commit 52ded8af77
6 changed files with 311 additions and 58 deletions

View file

@ -1,3 +1,5 @@
from lark import Transformer
def indent(multiline_string, how_much):
lines = multiline_string.split('\n')
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)
else:
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]

View file

@ -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"""
%import common.WS
%ignore WS
%ignore COMMENT
?start: object*
?start: (class_ | association | global_constraint)*
IDENTIFIER: /[A-Za-z_][A-Za-z_0-9]*/
COMMENT: /#[^\n]*\n/
# 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] _NL [_INDENT slot+ _DEDENT]
link: "(" IDENTIFIER "->" IDENTIFIER ")"
slot: IDENTIFIER "=" literal _NL
INT_OR_INF: INT | "*"
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

View file

@ -1,9 +1,10 @@
# Parser for Object Diagrams textual concrete syntax
from lark import Lark, logger, Transformer
from lark import Lark, logger
from lark.indenter import Indenter
from services.od import OD
from services.scd import SCD
from concrete_syntax.common import _Code, TBase
from uuid import UUID
grammar = r"""
@ -39,12 +40,6 @@ slot: IDENTIFIER "=" literal ";"
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
def parse_od(state, m_text, mm):
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")))
class T(Transformer):
class T(TBase):
def __init__(self, visit_tokens):
super().__init__(visit_tokens)
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):
[src, tgt] = el
return (src, tgt)

View file

@ -43,6 +43,7 @@ woods_mm_cs = """
Man:Class {
# We can define lower and upper cardinalities on Classes
# (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
upper_cardinality = 2; # there must be at most two Men in every model

View 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.")

View file

@ -132,21 +132,21 @@ class SCD:
set_cardinality("target_upper", tgt_max_c)
return assoc_edge
def create_global_constraint(self, name: str):
"""
Defines a global constraint element.
# def create_global_constraint(self, name: str):
# """
# Defines a global constraint element.
Args:
name: the name of the global constraint to be created
# Args:
# name: the name of the global constraint to be created
Returns:
Nothing.
"""
# create element + morphism links
element_node = self.bottom.create_node() # create element node
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
self.bottom.create_edge(element_node, scd_node, "Morphism") # create morphism link
# Returns:
# Nothing.
# """
# # create element + morphism links
# element_node = self.bottom.create_node() # create element node
# 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
# self.bottom.create_edge(element_node, scd_node, "Morphism") # create morphism link
def create_attribute(self, name: str):
"""