Concrete syntax no longer indentation-based (nightmare to parse). Add indented multi-line code terminals.

This commit is contained in:
Joeri Exelmans 2024-10-07 18:18:05 +02:00
parent 59de61d0a3
commit e875821e70
8 changed files with 119 additions and 73 deletions

16
concrete_syntax/common.py Normal file
View file

@ -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)

View file

@ -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'

View file

@ -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]

View file

@ -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):
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" {attr_name} = {display_value(value, type_name)}\n"
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)

View file

@ -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)

View file

@ -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, '<string>', mode='exec'), _globals, _locals)
return eval(compile(last, '<string>', 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):

View file

@ -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)

View file

@ -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: