Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementing better error messages #83

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions src/as2fm/as2fm_common/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,13 @@ def _assemble_message(severity: Severity, element: "lxml.etree._Element", messag
"The element must have a filepath attribute. This is set by "
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have to admit that the implementation of this function slipped completely my attention! If I knew it existed, I would have started using it so long ago! :)

"`as2fm_common.logging.set_filepath_for_all_elements`."
)

severity_initial = severity.name[0]
path = element.attrib[INTERNAL_FILEPATH_ATTR]
return f"{severity_initial} ({path}:{element.sourceline}) {message}"


def error(element: "lxml.etree._Element", message: str) -> str:
def get_error_msg(element: "lxml.etree._Element", message: str) -> str:
"""
Log an error message.
Expand All @@ -92,7 +93,7 @@ def error(element: "lxml.etree._Element", message: str) -> str:
return _assemble_message(Severity.ERROR, element, message)


def warn(element: "lxml.etree._Element", message: str) -> str:
def get_warn_msg(element: "lxml.etree._Element", message: str) -> str:
"""
Log a warning message.
Expand All @@ -103,7 +104,7 @@ def warn(element: "lxml.etree._Element", message: str) -> str:
return _assemble_message(Severity.WARNING, element, message)


def info(element: "lxml.etree._Element", message: str) -> str:
def get_info_msg(element: "lxml.etree._Element", message: str) -> str:
"""
Log an info message.
Expand All @@ -112,3 +113,14 @@ def info(element: "lxml.etree._Element", message: str) -> str:
:return: The message with the line number
"""
return _assemble_message(Severity.INFO, element, message)


def log_error(element: "lxml.etree._Element", message: str) -> None:
"""
Log an error message and print it.
:param element: The element that caused the error
:param message: The message
"""
error_msg = get_error_msg(element, message)
print(error_msg)
34 changes: 20 additions & 14 deletions src/as2fm/jani_generator/scxml_helpers/scxml_expression.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
from typing import List, Optional, Type, Union

import esprima
import lxml.etree

from as2fm.as2fm_common.logging import get_error_msg
from as2fm.jani_generator.jani_entries.jani_convince_expression_expansion import (
CALLABLE_OPERATORS_MAP,
OPERATORS_TO_JANI_MAP,
Expand All @@ -44,7 +46,7 @@ class ArrayInfo:


def parse_ecmascript_to_jani_expression(
ecmascript: str, array_info: Optional[ArrayInfo] = None
ecmascript: str, elem: Optional["lxml.etree._Element"], array_info: Optional[ArrayInfo] = None
) -> JaniExpression:
"""
Parse ecmascript to jani expression.
Expand All @@ -56,20 +58,24 @@ def parse_ecmascript_to_jani_expression(
try:
ast = esprima.parseScript(ecmascript)
except esprima.error_handler.Error as e:
raise RuntimeError(f"Failed parsing ecmascript: {ecmascript}. Error: {e}.")
assert len(ast.body) == 1, "The ecmascript must contain exactly one expression."
raise RuntimeError(
get_error_msg(elem, f"Failed parsing ecmascript: {ecmascript}. Error: {e}.")
)
assert len(ast.body) == 1, get_error_msg(
elem, "The ecmascript must contain exactly one expression."
)
ast = ast.body[0]
try:
jani_expression = _parse_ecmascript_to_jani_expression(ast, array_info)
jani_expression = _parse_ecmascript_to_jani_expression(ast, elem, array_info)
except NotImplementedError:
raise RuntimeError(f"Unsupported ecmascript: {ecmascript}")
raise RuntimeError(get_error_msg(elem, f"Unsupported ecmascript: {ecmascript}"))
except AssertionError:
raise RuntimeError(f"Assertion from ecmascript: {ecmascript}")
raise RuntimeError(get_error_msg(elem, f"Assertion from ecmascript: {ecmascript}"))
return jani_expression


def _parse_ecmascript_to_jani_expression(
ast: esprima.nodes.Script, array_info: Optional[ArrayInfo] = None
ast: esprima.nodes.Script, elem: "lxml.etree._Element", array_info: Optional[ArrayInfo] = None
) -> JaniExpression:
"""
Parse ecmascript to jani expression.
Expand All @@ -79,7 +85,7 @@ def _parse_ecmascript_to_jani_expression(
:return: The jani expression.
"""
if ast.type == "ExpressionStatement":
return _parse_ecmascript_to_jani_expression(ast.expression, array_info)
return _parse_ecmascript_to_jani_expression(ast.expression, elem, array_info)
elif ast.type == "Literal":
return JaniExpression(JaniValue(ast.value))
elif ast.type == "Identifier":
Expand All @@ -95,7 +101,7 @@ def _parse_ecmascript_to_jani_expression(
{
"op": OPERATORS_TO_JANI_MAP[ast.operator],
"left": JaniValue(0),
"right": _parse_ecmascript_to_jani_expression(ast.argument, array_info),
"right": _parse_ecmascript_to_jani_expression(ast.argument, elem, array_info),
}
)
elif ast.type == "BinaryExpression" or ast.type == "LogicalExpression":
Expand All @@ -106,8 +112,8 @@ def _parse_ecmascript_to_jani_expression(
return JaniExpression(
{
"op": OPERATORS_TO_JANI_MAP[ast.operator],
"left": _parse_ecmascript_to_jani_expression(ast.left, array_info),
"right": _parse_ecmascript_to_jani_expression(ast.right, array_info),
"left": _parse_ecmascript_to_jani_expression(ast.left, elem, array_info),
"right": _parse_ecmascript_to_jani_expression(ast.right, elem, array_info),
}
)
elif ast.type == "ArrayExpression":
Expand All @@ -130,10 +136,10 @@ def _parse_ecmascript_to_jani_expression(
elements_list.extend([entry_type(0)] * elements_to_add)
return array_value_operator(elements_list)
elif ast.type == "MemberExpression":
object_expr = _parse_ecmascript_to_jani_expression(ast.object, array_info)
object_expr = _parse_ecmascript_to_jani_expression(ast.object, elem, array_info)
if ast.computed:
# This is an array access, like array[0]
array_index = _parse_ecmascript_to_jani_expression(ast.property, array_info)
array_index = _parse_ecmascript_to_jani_expression(ast.property, elem, array_info)
return array_access_operator(object_expr, array_index)
else:
# Access to the member of an object through dot notation
Expand Down Expand Up @@ -168,7 +174,7 @@ def _parse_ecmascript_to_jani_expression(
), f"Unsupported function call {function_name}."
expression_args: List[JaniExpression] = []
for arg in ast.arguments:
expression_args.append(_parse_ecmascript_to_jani_expression(arg, array_info))
expression_args.append(_parse_ecmascript_to_jani_expression(arg, elem, array_info))
return CALLABLE_OPERATORS_MAP[function_name](*expression_args)
else:
raise NotImplementedError(f"Unsupported ecmascript type: {ast.type}")
27 changes: 17 additions & 10 deletions src/as2fm/jani_generator/scxml_helpers/scxml_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,9 @@ def _interpret_scxml_assign(
:return: The action or expression to be executed.
"""
assert isinstance(elem, ScxmlAssign), f"Expected ScxmlAssign, got {type(elem)}"
assignment_target = parse_ecmascript_to_jani_expression(elem.get_location())
assignment_target = parse_ecmascript_to_jani_expression(
elem.get_location(), elem.get_xml_tree()
)
target_expr_type = assignment_target.get_expression_type()
is_target_array = target_expr_type == JaniExpressionType.IDENTIFIER and is_variable_array(
jani_automaton, assignment_target.as_identifier()
Expand All @@ -120,7 +122,7 @@ def _interpret_scxml_assign(
array_info = ArrayInfo(*var_info)
# Check if the target is an array, in case copy the length too
assignment_value = parse_ecmascript_to_jani_expression(
elem.get_expr(), array_info
elem.get_expr(), elem.get_xml_tree(), array_info
).replace_event(event_substitution)
assignments: List[JaniAssignment] = [
JaniAssignment({"ref": assignment_target, "value": assignment_value, "index": assign_index})
Expand Down Expand Up @@ -274,9 +276,9 @@ def _append_scxml_body_to_jani_edge(
array_info = None
if isinstance(res_eval_value, ArrayType):
array_info = ArrayInfo(get_args(res_eval_type)[0], max_array_size)
jani_expr = parse_ecmascript_to_jani_expression(expr, array_info).replace_event(
data_event
)
jani_expr = parse_ecmascript_to_jani_expression(
expr, param.get_xml_tree(), array_info
).replace_event(data_event)
new_edge_dest_assignments.append(
JaniAssignment({"ref": param_assign_name, "value": jani_expr})
)
Expand Down Expand Up @@ -328,7 +330,7 @@ def _append_scxml_body_to_jani_edge(
last_edge.destinations[-1]["location"] = interm_loc_before
previous_conditions: List[JaniExpression] = []
for if_idx, (cond_str, conditional_body) in enumerate(ec.get_conditional_executions()):
current_cond = parse_ecmascript_to_jani_expression(cond_str)
current_cond = parse_ecmascript_to_jani_expression(cond_str, ec.get_xml_tree())
jani_cond = _merge_conditions(previous_conditions, current_cond).replace_event(
data_event
)
Expand Down Expand Up @@ -534,7 +536,9 @@ def write_model(self):
max_array_size = self.max_array_size
expected_type = ArrayType
array_info = ArrayInfo(array_type, max_array_size)
init_value = parse_ecmascript_to_jani_expression(scxml_data.get_expr(), array_info)
init_value = parse_ecmascript_to_jani_expression(
scxml_data.get_expr(), scxml_data.get_xml_tree(), array_info
)
evaluated_expr = interpret_ecma_script_expr(scxml_data.get_expr(), read_vars)
assert check_value_type_compatible(evaluated_expr, expected_type), (
f"Invalid value for {scxml_data.get_name()}: "
Expand Down Expand Up @@ -656,7 +660,7 @@ def get_guard_exp_for_prev_conditions(self, event_name: str) -> Optional[JaniExp
<transition event="a" cond="_event.X <= 5" target="self" />
"""
previous_expressions = [
parse_ecmascript_to_jani_expression(cond)
parse_ecmascript_to_jani_expression(cond, None)
for cond in self._event_to_conditions.get(event_name, [])
]
if len(previous_expressions) > 0:
Expand Down Expand Up @@ -787,11 +791,14 @@ def write_model(self):
transition_targets: List[ScxmlTransitionTarget] = self.element.get_targets()
# Transition condition (guard) processing
previous_conditions_expr = [
parse_ecmascript_to_jani_expression(cond) for cond in self._previous_conditions
parse_ecmascript_to_jani_expression(cond, self.element.get_xml_tree())
for cond in self._previous_conditions
]
current_condition_expr = None
if current_condition is not None:
current_condition_expr = parse_ecmascript_to_jani_expression(current_condition)
current_condition_expr = parse_ecmascript_to_jani_expression(
current_condition, self.element.get_xml_tree()
)
if trigger_event is not None:
for single_cond_expr in previous_conditions_expr:
single_cond_expr.replace_event(trigger_event)
Expand Down
32 changes: 19 additions & 13 deletions src/as2fm/jani_generator/scxml_helpers/top_level_interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
import lxml.etree as ET

from as2fm.as2fm_common.common import remove_namespace, string_to_bool
from as2fm.as2fm_common.logging import error, set_filepath_for_all_elements
from as2fm.as2fm_common.logging import get_error_msg, set_filepath_for_all_elements
from as2fm.jani_generator.ros_helpers.ros_action_handler import RosActionHandler
from as2fm.jani_generator.ros_helpers.ros_communication_handler import (
RosCommunicationHandler,
Expand Down Expand Up @@ -89,7 +89,7 @@ def parse_main_xml(xml_path: str) -> FullModel:
with open(xml_path, "r", encoding="utf-8") as f:
xml = ET.parse(f, parser=parser_wo_comments)
set_filepath_for_all_elements(xml.getroot(), xml_path)
assert remove_namespace(xml.getroot().tag) == "convince_mc_tc", error(
assert remove_namespace(xml.getroot().tag) == "convince_mc_tc", get_error_msg(
xml.getroot(), "The top-level XML element must be convince_mc_tc."
)
model = FullModel()
Expand All @@ -108,9 +108,11 @@ def parse_main_xml(xml_path: str) -> FullModel:
model.bt_tick_when_not_running = string_to_bool(mc_parameter.attrib["value"])
else:
raise ValueError(
error(mc_parameter, f"Invalid mc_parameter tag: {mc_parameter.tag}")
get_error_msg(mc_parameter, f"Invalid mc_parameter tag: {mc_parameter.tag}")
)
assert model.max_time is not None, error(first_level, "`max_time` must be defined.")
assert model.max_time is not None, get_error_msg(
first_level, "`max_time` must be defined."
)
elif remove_namespace(first_level.tag) == "behavior_tree":
for child in first_level:
if remove_namespace(child.tag) == "input":
Expand All @@ -121,37 +123,41 @@ def parse_main_xml(xml_path: str) -> FullModel:
model.plugins.append(os.path.join(folder_of_xml, child.attrib["src"]))
else:
raise ValueError(
error(child, f"Invalid input type: {child.attrib['type']}")
get_error_msg(child, f"Invalid input type: {child.attrib['type']}")
)
else:
raise ValueError(
error(child, f"Invalid behavior_tree tag: {child.tag} != input")
get_error_msg(child, f"Invalid behavior_tree tag: {child.tag} != input")
)
assert model.bt is not None, error(first_level, "A Behavior Tree must be defined.")
assert model.bt is not None, get_error_msg(
first_level, "A Behavior Tree must be defined."
)
elif remove_namespace(first_level.tag) == "node_models":
for node_model in first_level:
assert remove_namespace(node_model.tag) == "input", error(
assert remove_namespace(node_model.tag) == "input", get_error_msg(
node_model, "Only input tags are supported."
)
assert node_model.attrib["type"] == "ros-scxml", error(
assert node_model.attrib["type"] == "ros-scxml", get_error_msg(
node_model, "Only ROS-SCXML node models are supported."
)
model.skills.append(os.path.join(folder_of_xml, node_model.attrib["src"]))
elif remove_namespace(first_level.tag) == "properties":
for jani_property in first_level:
assert remove_namespace(jani_property.tag) == "input", error(
assert remove_namespace(jani_property.tag) == "input", get_error_msg(
jani_property, "Only input tags are supported."
)
assert jani_property.attrib["type"] == "jani", error(
assert jani_property.attrib["type"] == "jani", get_error_msg(
jani_property,
"Only Jani properties are supported, not {jani_property.attrib['type']}.",
)
model.properties.append(os.path.join(folder_of_xml, jani_property.attrib["src"]))
assert len(model.properties) == 1, error(
assert len(model.properties) == 1, get_error_msg(
first_level, "Only exactly one Jani property is supported."
)
else:
raise ValueError(error(first_level, f"Invalid main point tag: {first_level.tag}"))
raise ValueError(
get_error_msg(first_level, f"Invalid main point tag: {first_level.tag}")
)
return model


Expand Down
26 changes: 23 additions & 3 deletions src/as2fm/scxml_converter/scxml_entries/scxml_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
Base SCXML class, defining the methods all SCXML entries shall implement.
"""

from typing import Optional

from lxml import etree as ET


Expand All @@ -28,11 +30,29 @@ def get_tag_name() -> str:
"""Get the tag name of the XML element."""
raise NotImplementedError

@staticmethod
def from_xml_tree(xml_tree: ET.Element) -> "ScxmlBase":
"""Create a ScxmlBase object from an XML tree."""
@classmethod
def from_xml_tree(cls, xml_tree: ET.Element) -> "ScxmlBase":
"""External interface to create a ScxmlBase object from an XML tree."""
instance = cls.from_xml_tree_impl(xml_tree)
instance.set_xml_tree(xml_tree)
return instance

@classmethod
def from_xml_tree_impl(cls, xml_tree: ET.Element) -> "ScxmlBase":
"""Child-specific implementation to create a ScxmlBase object from an XML tree."""
raise NotImplementedError

def set_xml_tree(self, xml_tree: ET.Element):
"""Set the xml_element this object was made from"""
self.xml_tree = xml_tree

def get_xml_tree(self) -> Optional[ET.Element]:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should call this get_xml_element, though it might not really be consistent with the from_xml_tree method.

"""Get the xml_element this object was made from."""
try:
return self.xml_tree
except AttributeError:
return None

def check_validity(self) -> bool:
"""Check if the object is valid."""
raise NotImplementedError
Expand Down
Loading