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 import scd, od
from services.bottom.V0 import Bottom from services.bottom.V0 import Bottom
from transformation import ramify from transformation import ramify
import json from concrete_syntax.common import display_value
from uuid import UUID from uuid import UUID
def render_class_diagram(state, model, prefix_ids=""): 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: for attr_name, attr_edge in attributes:
slot = m_od.get_slot(obj_node, attr_name) slot = m_od.get_slot(obj_node, attr_name)
if slot != None: 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}'
output += '\n' output += '\n'

View file

@ -7,46 +7,34 @@ from services.scd import SCD
from uuid import UUID from uuid import UUID
grammar = r""" grammar = r"""
%import common.WS_INLINE %import common.WS
%ignore WS_INLINE %ignore WS
%ignore COMMENT %ignore COMMENT
%declare _INDENT _DEDENT ?start: object*
?start: (_NL | object )*
IDENTIFIER: /[A-Za-z_][A-Za-z_0-9]*/ IDENTIFIER: /[A-Za-z_][A-Za-z_0-9]*/
COMMENT: /#.*/ COMMENT: /#[^\n]*\n/
# newline
_NL: /(\r?\n[\t ]*)+/
literal: INT literal: INT
| STR | STR
| BOOL | BOOL
| CODE | CODE
| INDENTED_CODE
INT: /[0-9]+/ INT: /[0-9]+/
STR: /"[^"]*"/ STR: /"[^"]*"/
| /'[^']*'/ | /'[^']*'/
BOOL: "True" | "False" BOOL: "True" | "False"
CODE: /`[^`]*`/ CODE: /`[^`]*`/
INDENTED_CODE: /```[^`]*```/
object: [IDENTIFIER] ":" IDENTIFIER [link_spec] _NL [_INDENT slot+ _DEDENT] object: [IDENTIFIER] ":" IDENTIFIER [link_spec] ["{" slot* "}"]
link_spec: "(" IDENTIFIER "->" IDENTIFIER ")" link_spec: "(" IDENTIFIER "->" IDENTIFIER ")"
slot: IDENTIFIER "=" literal _NL slot: IDENTIFIER "=" literal ";"
""" """
parser = Lark(grammar, parser='lalr')
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())
# internal use only # internal use only
# just a dumb wrapper to distinguish between code and string # 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): def CODE(self, token):
return _Code(str(token[1:-1])) # strip the `` 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): def literal(self, el):
return el[0] return el[0]

View file

@ -2,17 +2,8 @@
from services import od from services import od
from services.bottom.V0 import Bottom 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): def render_od(state, m_id, mm_id, hide_names=True):
bottom = Bottom(state) bottom = Bottom(state)
@ -26,19 +17,23 @@ def render_od(state, m_id, mm_id, hide_names=True):
def write_attributes(object_node): def write_attributes(object_node):
o = "" 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) 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 return o
for class_name, objects in m_od.get_all_objects().items(): for class_name, objects in m_od.get_all_objects().items():
for object_name, object_node in 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) output += write_attributes(object_node)
for assoc_name, links in m_od.get_all_links().items(): for assoc_name, links in m_od.get_all_links().items():
for link_name, (link_edge, src_name, tgt_name) in 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: # links can also have slots:
output += write_attributes(link_edge) output += write_attributes(link_edge)

View file

@ -62,22 +62,37 @@ def main():
dsl_mm_cs = """ dsl_mm_cs = """
# Integer:ModelRef # Integer:ModelRef
Bear:Class Bear:Class
Animal:Class Animal:Class {
abstract = True abstract = True;
Man:Class }
lower_cardinality = 1 Man:Class {
upper_cardinality = 2 lower_cardinality = 1;
# constraint = `get_value(get_slot(element, "weight")) < 100` upper_cardinality = 2;
Man_weight:AttributeLink (Man -> Integer) constraint = `get_value(get_slot(element, "weight")) < 20`;
name = "weight" }
optional = False Man_weight:AttributeLink (Man -> Integer) {
constraint = `get_value(get_target(element)) < 100` name = "weight";
afraidOf:Association (Man -> Animal) optional = False;
target_lower_cardinality = 1 constraint = ```
node = get_target(element)
get_value(node) < 20
```;
}
afraidOf:Association (Man -> Animal) {
target_lower_cardinality = 1;
}
Man_inh_Animal:Inheritance (Man -> Animal) Man_inh_Animal:Inheritance (Man -> Animal)
Bear_inh_Animal:Inheritance (Bear -> 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) dsl_mm_id = parser.parse_od(state, dsl_mm_cs, mm=scd_mm_id)
return dsl_mm_id return dsl_mm_id
@ -98,8 +113,9 @@ sum_of_weights:GlobalConstraint
def create_dsl_m_parser(): def create_dsl_m_parser():
# Create DSL M with parser # Create DSL M with parser
dsl_m_cs = """ dsl_m_cs = """
george :Man george :Man {
weight = 80 weight = 80;
}
bear1:Bear bear1:Bear
bear2:Bear bear2:Bear
:afraidOf (george -> bear1) :afraidOf (george -> bear1)
@ -139,7 +155,7 @@ bear2:Bear
lhs_id = state.create_node() lhs_id = state.create_node()
lhs_od = OD(ramified_mm_id, lhs_id, state) lhs_od = OD(ramified_mm_id, lhs_id, state)
lhs_od.create_object("man", prefix+"Man") 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_object("scaryAnimal", prefix+"Animal")
lhs_od.create_link("manAfraidOfAnimal", prefix+"afraidOf", "man", "scaryAnimal") lhs_od.create_link("manAfraidOfAnimal", prefix+"afraidOf", "man", "scaryAnimal")
@ -150,9 +166,9 @@ bear2:Bear
rhs_id = state.create_node() rhs_id = state.create_node()
rhs_od = OD(ramified_mm_id, rhs_id, state) rhs_od = OD(ramified_mm_id, rhs_id, state)
rhs_od.create_object("man", prefix+"Man") 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_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") rhs_od.create_link("billAfraidOfMan", prefix+"afraidOf", "bill", "man")
@ -229,7 +245,7 @@ bear2:Bear
# plantuml_str = render_rewrite() # plantuml_str = render_rewrite()
# print() # print()
# print("==============================================") print("==============================================")
# print(plantuml_str) # print(plantuml_str)

View file

@ -9,6 +9,17 @@ from pprint import pprint
import functools 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: class Conformance:
def __init__(self, state: State, model: UUID, type_model: UUID): def __init__(self, state: State, model: UUID, type_model: UUID):
self.state = state self.state = state
@ -368,12 +379,13 @@ class Conformance:
'get_all_instances': self.get_all_instances 'get_all_instances': self.get_all_instances
} }
# print("evaluating constraint ...", code) # print("evaluating constraint ...", code)
result = eval( loc = {**kwargs, **funcs}
result = exec_then_eval(
code, code,
{'__builtins__': {'isinstance': isinstance, 'print': print, {'__builtins__': {'isinstance': isinstance, 'print': print,
'int': int, 'float': float, 'bool': bool, 'str': str, 'tuple': tuple, 'len': len} 'int': int, 'float': float, 'bool': bool, 'str': str, 'tuple': tuple, 'len': len}
}, # globals }, # globals
{**kwargs, **funcs} # locals loc # locals
) )
# print('result =', result) # print('result =', result)
return result return result
@ -384,7 +396,8 @@ class Conformance:
for subtype_name in self.sub_types[type_name]: for subtype_name in self.sub_types[type_name]:
# print(subtype_name, 'is subtype of ') # print(subtype_name, 'is subtype of ')
result += [e_name for e_name, t_name in self.type_mapping.items() if t_name == subtype_name] 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): def check_constraints(self):
""" """
@ -399,12 +412,11 @@ class Conformance:
code = ActionCode(UUID(self.bottom.read_value(constraint)), self.bottom.state).read() code = ActionCode(UUID(self.bottom.read_value(constraint)), self.bottom.state).read()
return code return code
def check_result(result, local_or_global, tm_name, el_name=None): def check_result(result, description):
suffix = f"in '{el_name}'" if local_or_global == "Local" else ""
if not isinstance(result, bool): 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)}).") raise Exception(f"{description} evaluation result is not boolean! Instead got {result}")
elif not result: if not result:
errors.append(f"{local_or_global} constraint `{code}` of '{tm_name}'{suffix} not satisfied.") errors.append(f"{description} not satisfied.")
# local constraints # local constraints
for m_name, tm_name in self.type_mapping.items(): 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] morphisms = [m for m in morphisms if m in self.model_names]
for m_element in morphisms: for m_element in morphisms:
result = self.evaluate_constraint(code, element=m_element, type_name=tm_name) 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 # global constraints
glob_constraints = [] glob_constraints = []
@ -432,9 +445,9 @@ class Conformance:
for tm_name in glob_constraints: for tm_name in glob_constraints:
code = get_code(tm_name) code = get_code(tm_name)
if code != None: if code != None:
# print('glob constr:', code)
result = self.evaluate_constraint(code, model=self.model) 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 return errors
def precompute_structures(self): 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_id = state.read_dict(state.read_root(), "String")
string_type = UUID(state.read_value(string_type_id)) 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) m_scd = scd.SCD(model, state)
ramified = state.create_node() ramified = state.create_node()
ramified_scd = scd.SCD(ramified, state) ramified_scd = scd.SCD(ramified, state)
string_modelref = ramified_scd.create_model_ref("String", string_type) 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() classes = m_scd.get_classes()
for class_name, class_node in classes.items(): 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") # print(' creating attribute', attr_name, "with type String")
# Every attribute becomes 'string' type # Every attribute becomes 'string' type
# The string will be a Python expression # 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 # traceability link
bottom.create_edge(ramified_attr_link, attr_edge, RAMIFIES_LABEL) 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 transformation import ramify
from services import od from services import od
from services.primitives.string_type import String from services.primitives.string_type import String
from services.primitives.actioncode_type import ActionCode
from services.primitives.integer_type import Integer from services.primitives.integer_type import Integer
def process_rule(state, lhs: UUID, rhs: UUID): 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 # assume the type of the object is already the original type
# this is because primitive types (e.g., Integer) are not RAMified # this is because primitive types (e.g., Integer) are not RAMified
type_name = od.get_object_name(bottom, pattern_mm, rhs_type) 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 # 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, {}, {}) result = eval(python_expr, {}, {})
# Write the result into the host model. # 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. # 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) m_od.create_string_value(model_el_name_to_create, result)
name_mapping[pattern_name_to_create] = model_el_name_to_create name_mapping[pattern_name_to_create] = model_el_name_to_create
else: 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....") # 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: for pattern_name_to_create, rhs_el_to_create, original_type, original_type_name, model_el_name_to_create in edges_to_create: