diff --git a/fprime_python_model/fpp_ast/fpp_locations.py b/fprime_python_model/fpp_ast/fpp_locations.py index b332084..606f2a2 100644 --- a/fprime_python_model/fpp_ast/fpp_locations.py +++ b/fprime_python_model/fpp_ast/fpp_locations.py @@ -5,7 +5,7 @@ @dataclass class Location: - path: Path + file: Path pos: str including_loc: Optional["Location"] diff --git a/fprime_python_model/semantics/analysis.py b/fprime_python_model/semantics/analysis.py index 525fbbe..ef765e7 100644 --- a/fprime_python_model/semantics/analysis.py +++ b/fprime_python_model/semantics/analysis.py @@ -1,6 +1,6 @@ from dataclasses import dataclass, field from typing import Optional, Set, List, Dict, Tuple -from fprime_python_model.fpp_ast.fpp_ast_node import AstId +from fprime_python_model.fpp_ast.fpp_ast_node import AstId, AstNode from fprime_python_model.fpp_ast.fpp_ast import Ident, SpecLocKind, SpecLoc from fprime_python_model.semantics.symbol import ( Symbol, @@ -55,7 +55,7 @@ class Analysis: input_file_set: Set[Path] = field(default_factory=set) included_file_set: Set[Path] = field(default_factory=set) - location_specifier_map: Dict[Tuple[SpecLocKind, QualifiedName], SpecLoc] = field( + location_specifier_map: Dict[Tuple[SpecLocKind, QualifiedName], AstNode[SpecLoc]] = field( default_factory=dict ) parent_symbol_map: Dict[AstId, Symbol] = field(default_factory=dict) diff --git a/fprime_python_model/semantics/component.py b/fprime_python_model/semantics/component.py index 28e1d70..bee1c5c 100644 --- a/fprime_python_model/semantics/component.py +++ b/fprime_python_model/semantics/component.py @@ -57,12 +57,18 @@ class Component: :type port_interface: PortInterface :param command_map: The map from command opcodes to commands :type command_map: Dict[CommandOpcode, Command] + :param default_opcode: The next default opcode + :type default_opcode: int :param tlm_channel_map: The map from telemetry channel IDs to channels :type tlm_channel_map: Dict[TlmChannelId, TlmChannel] :param tlm_channel_name_map: The map from telemetry channel names to channels :type tlm_channel_name_map: Dict[UnqualifiedName, TlmChannel] + :param default_tlm_channel_id: The next default telemetry channel ID + :type default_tlm_channel_id: int :param event_map: The map from event IDs to events :type event_map: Dict[EventId, Event] + :param default_event_id: The next default event ID + :type default_event_id: int :param param_map: The map from parameter IDs to parameters :type param_map: Dict[ParamId, Param] :param spec_port_matching_list: The list of port matching specifiers @@ -71,10 +77,16 @@ class Component: :type state_machine_instance_map: Dict[UnqualifiedName, StateMachineInstance] :param port_matching_list: The list of port matching constraints :type port_matching_list: List[PortMatching] - :param constainer_map: The map from container IDs to containers + :param default_param_id: The next default parameter ID + :type default_param_id: int + :param container_map: The map from container IDs to containers :type container_map: Dict[ContainerId, Container] + :param default_container_id: The next default container ID + :type default_container_id: int :param record_map: The map from record IDs to records :type record_map: Dict[RecordId, Record] + :param default_record_id: The next default record ID + :type default_record_id: int """ a_node: fpp_ast.Annotated[AstNode[fpp_ast.DefComponent]] @@ -82,11 +94,14 @@ class Component: default_factory=lambda: PortInterface("component") ) command_map: Dict[CommandOpcode, Command] = field(default_factory=dict) + default_opcode: int = 0 tlm_channel_map: Dict[TlmChannelId, TlmChannel] = field(default_factory=dict) tlm_channel_name_map: Dict[UnqualifiedName, TlmChannel] = field( default_factory=dict ) + default_tlm_channel_id: int = 0 event_map: Dict[EventId, Event] = field(default_factory=dict) + default_event_id: int = 0 param_map: Dict[ParamId, Param] = field(default_factory=dict) spec_port_matching_list: List[ fpp_ast.Annotated[AstNode[fpp_ast.SpecPortMatching]] @@ -95,8 +110,11 @@ class Component: default_factory=dict ) port_matching_list: List[PortMatching] = field(default_factory=list) + default_param_id: int = 0 container_map: Dict[ContainerId, Container] = field(default_factory=dict) + default_container_id: int = 0 record_map: Dict[RecordId, Record] = field(default_factory=dict) + default_record_id: int = 0 @property def port_map(self) -> Dict[UnqualifiedName, PortInstance]: diff --git a/fprime_python_model/semantics/types_values.py b/fprime_python_model/semantics/types_values.py index 69223df..183142e 100644 --- a/fprime_python_model/semantics/types_values.py +++ b/fprime_python_model/semantics/types_values.py @@ -12,11 +12,12 @@ EnumSymbol, StructSymbol, ) -from dataclasses import dataclass +from dataclasses import dataclass, field from fprime_python_model.semantics.format import Format import math +@dataclass class Type(ABC): """An FPP Type""" @@ -99,6 +100,7 @@ class PrimitiveIntKind(Enum): U64 = "U64" +@dataclass class IntType(Type): """Integer types""" @@ -106,6 +108,7 @@ def is_int(self) -> bool: return True +@dataclass class PrimitiveType(Type): """Primitive types""" @@ -120,8 +123,7 @@ def bit_width(self) -> int: class PrimitiveIntType(PrimitiveType, IntType): """Primitive integer types""" - def __init__(self, kind: PrimitiveIntKind): - self.kind = kind + kind: PrimitiveIntKind def get_default_value(self) -> Optional["PrimitiveIntValue"]: return PrimitiveIntValue(0, self.kind) @@ -166,8 +168,7 @@ def __str__(self): @dataclass class FloatType(PrimitiveType): - def __init__(self, kind: FloatKind): - self.kind = kind + kind: FloatKind def get_default_value(self): return FloatValue(0, self.kind) @@ -210,8 +211,7 @@ def is_displayable(self): @dataclass class StringType(Type): - def __init__(self, size: Optional[AstNode[fpp_ast.Expr]] = None): - self.size = size + size: Optional[AstNode[fpp_ast.Expr]] = None def get_default_value(self): return StringValue("") @@ -237,8 +237,7 @@ def __str__(self): @dataclass class AbsType(Type): - def __init__(self, node: fpp_ast.Annotated[AstNode[fpp_ast.DefAbsType]]): - self.node = node + node: fpp_ast.Annotated[AstNode[fpp_ast.DefAbsType]] def get_default_value(self): return AbsTypeValue(self) @@ -252,13 +251,10 @@ def get_def_symbol(self): def __str__(self): return str(self.node[1].data.name) - +@dataclass class AliasType(Type): - def __init__( - self, node: fpp_ast.Annotated[AstNode[fpp_ast.DefAliasType]], alias_type: Type - ): - self.node = node - self.alias_type = alias_type + node: fpp_ast.Annotated[AstNode[fpp_ast.DefAliasType]] + alias_type: Type def get_default_value(self): return self.alias_type.get_default_value() @@ -282,18 +278,12 @@ def get_underlying_type(self): return self.alias_type.get_underlying_type() +@dataclass class ArrayType(Type): - def __init__( - self, - node: fpp_ast.Annotated[AstNode[fpp_ast.DefArray]], - anon_array: "AnonArrayType", - default: Optional["ArrayValue"] = None, - format: Optional[Format] = None, - ): - self.node = node - self.anon_array = anon_array - self.default = default - self.format = format + node: fpp_ast.Annotated[AstNode[fpp_ast.DefArray]] + anon_array: "AnonArrayType" + default: Optional["ArrayValue"] = None + format: Optional[Format] = None __match_args__ = ("node", "anon_array", "default", "format") @@ -319,16 +309,11 @@ def __str__(self) -> str: return f"array {self.node[1].data.name}" +@dataclass class EnumType(Type): - def __init__( - self, - node: fpp_ast.Annotated[AstNode[fpp_ast.DefEnum]], - rep_type: PrimitiveIntType, - default: Optional["EnumConstantValue"] = None, - ): - self.node = node - self.rep_type = rep_type - self.default = default + node: fpp_ast.Annotated[AstNode[fpp_ast.DefEnum]] + rep_type: PrimitiveIntType + default: Optional["EnumConstantValue"] = None def get_default_value(self) -> Optional["EnumConstantValue"]: return self.default @@ -355,20 +340,13 @@ def __str__(self) -> str: StructMembersType: TypeAlias = Dict[fpp_ast.Unqualified, Type] +@dataclass class StructType(Type): - def __init__( - self, - node: fpp_ast.Annotated[AstNode[fpp_ast.DefStruct]], - anon_struct: "AnonStructType", - default: Optional["StructValue"] = None, - sizes: Dict[fpp_ast.Unqualified, int] = dict(), - formats: Dict[fpp_ast.Unqualified, Format] = dict(), - ): - self.node = node - self.anon_struct = anon_struct - self.default = default - self.sizes = sizes if sizes is not None else {} - self.formats = formats if formats is not None else {} + node: fpp_ast.Annotated[AstNode[fpp_ast.DefStruct]] + anon_struct: "AnonStructType" + default: Optional["StructValue"] = None + sizes: Dict[fpp_ast.Unqualified, int] = field(default_factory=dict) + formats: Dict[fpp_ast.Unqualified, Format] = field(default_factory=dict) __match_args__ = ("node", "anon_struct", "default", "sizes", "formats") @@ -393,10 +371,10 @@ def __str__(self) -> str: return f"struct {self.node[1].data.name}" +@dataclass class AnonArrayType(Type): - def __init__(self, size: Optional[int], elt_type: Type): - self.size = size - self.elt_type = elt_type + size: Optional[int] + elt_type: Type __match_args__ = ("size", "elt_type") @@ -423,9 +401,9 @@ def __str__(self) -> str: return f"array of {self.elt_type}" +@dataclass class AnonStructType(Type): - def __init__(self, members: StructMembersType): - self.members: StructMembersType = members + members: StructMembersType __match_args__ = "members" @@ -451,6 +429,7 @@ def __str__(self) -> str: T = TypeVar("T") +@dataclass class Value(ABC): def __init__(self): pass @@ -496,10 +475,10 @@ def get_type(self) -> Type: pass +@dataclass class PrimitiveIntValue(Value): - def __init__(self, value: int, kind: PrimitiveIntKind): - self.value = value - self.kind = kind + value: int + kind: PrimitiveIntKind def get_type(self) -> "Type": return PrimitiveIntType(self.kind) @@ -514,9 +493,9 @@ def __neg__(self) -> "PrimitiveIntValue": return PrimitiveIntValue(-self.value, self.kind) +@dataclass class IntegerValue(Value): - def __init__(self, value: int): - self.value = value + value: int def fits_in_u64_width(self) -> bool: u64_bound = 1 << 64 @@ -535,11 +514,10 @@ def __neg__(self) -> Optional["IntegerValue"]: return IntegerValue(-self.value) +@dataclass class FloatValue(Value): - - def __init__(self, value: float, kind: FloatKind): - self.value = value - self.kind = kind + value: float + kind: FloatKind def is_zero(self) -> bool: return math.fabs(self.value) <= 0 @@ -554,9 +532,9 @@ def __neg__(self) -> "FloatValue": return FloatValue(-self.value, self.kind) +@dataclass class BooleanValue(Value): - def __init__(self, value: bool): - self.value = value + value: bool def get_type(self) -> "Type": return BooleanType() @@ -565,9 +543,9 @@ def __str__(self) -> str: return str(self.value) +@dataclass class StringValue(Value): - def __init__(self, value: str): - self.value = value + value: str def get_type(self) -> "Type": return StringType(None) @@ -576,9 +554,9 @@ def __str__(self) -> str: return f'"{self.value}"' +@dataclass class AnonArrayValue(Value): - def __init__(self, elements: List[Value]): - self.elements = elements + elements: List[Value] def get_type(self) -> "Type": size = len(self.elements) @@ -589,9 +567,9 @@ def __str__(self) -> str: return "[ " + ", ".join(str(e) for e in self.elements) + " ]" +@dataclass class AbsTypeValue(Value): - def __init__(self, t: AbsType): - self.t = t + t: AbsType def get_type(self) -> AbsType: return self.t @@ -600,10 +578,10 @@ def __str__(self) -> str: return f"value of type {self.t}" +@dataclass class ArrayValue(Value): - def __init__(self, anon_array: AnonArrayValue, t: ArrayType): - self.anon_array = anon_array - self.t = t + anon_array: AnonArrayValue + t: ArrayType def get_type(self) -> ArrayType: return self.t @@ -612,10 +590,10 @@ def __str__(self) -> str: return f"{self.anon_array}: {self.t.node[1].data.name}" +@dataclass class EnumConstantValue(Value): - def __init__(self, value: Tuple[fpp_ast.Unqualified, int], t: EnumType): - self.value = value - self.t = t + value: Tuple[fpp_ast.Unqualified, int] + t: EnumType def get_type(self) -> "EnumType": return self.t @@ -627,9 +605,9 @@ def __str__(self) -> str: StructMembersValue: TypeAlias = Dict[fpp_ast.Unqualified, Value] +@dataclass class AnonStructValue(Value): - def __init__(self, members: StructMembersValue): - self.members = members + members: StructMembersValue def get_type(self) -> AnonStructType: type_members: Dict[fpp_ast.Unqualified, Type] = dict() @@ -645,28 +623,19 @@ def __str__(self) -> str: return "{ " + ", ".join(member_strs) + " }" +@dataclass class StructValue(Value): - def __init__(self, anon_struct: AnonStructValue, t: StructType): - self.anon_struct = anon_struct - self.t = t + anon_struct: AnonStructValue + t: StructType def get_type(self) -> StructType: return self.t def __str__(self) -> str: - type_name = ( - getattr(self.t.node[1].data, "name", "unknown") - if hasattr(self.t, "node") - else "unknown" - ) - return f"{str(self.anon_struct)}: {type_name}" + return f"{str(self.anon_struct)}: {self.t.node[1].data.name}" +@dataclass class Binop(Generic[T]): - def __init__( - self, - int_op: Callable[[int, int], int], - double_op: Callable[[float, float], float], - ): - self.int_op = int_op - self.double_op = double_op + int_op: Callable[[int, int], int] + double_op: Callable[[float, float], float] diff --git a/fprime_python_model/translators/analysis_translator.py b/fprime_python_model/translators/analysis_translator.py index ea6924e..0bb8d32 100644 --- a/fprime_python_model/translators/analysis_translator.py +++ b/fprime_python_model/translators/analysis_translator.py @@ -228,8 +228,8 @@ def translate_file_set(self, l: List[str]) -> Set[Path]: def translate_location_specifier_map( self, l: List[Tuple[Tuple, Dict]] - ) -> Dict[Tuple[fpp_ast.SpecLocKind, QualifiedName], fpp_ast.SpecLoc]: - out_dict: Dict[Tuple[fpp_ast.SpecLocKind, QualifiedName], fpp_ast.SpecLoc] = ( + ) -> Dict[Tuple[fpp_ast.SpecLocKind, QualifiedName], AstNode[fpp_ast.SpecLoc]]: + out_dict: Dict[Tuple[fpp_ast.SpecLocKind, QualifiedName], AstNode[fpp_ast.SpecLoc]] = ( dict() ) for ls in l: @@ -238,7 +238,7 @@ def translate_location_specifier_map( spec_loc_kind = translate_spec_loc_kind(kind_json) spec_loc_qualified_name = self.translate_qualified_name(qual_name_json) spec_loc_a_node = self.get_annotated_ast_node_by_id(ls[1]["astNodeId"]) - out_dict[(spec_loc_kind, spec_loc_qualified_name)] = spec_loc_a_node[1].data + out_dict[(spec_loc_kind, spec_loc_qualified_name)] = spec_loc_a_node[1] return out_dict def translate_symbol(self, symbol_type: str, node_id: AstId) -> Symbol: @@ -998,11 +998,14 @@ def translate_component(self, d: Dict[str, dict]) -> Component: a_node=a_node, port_interface=self.translate_port_interface(d["portInterface"]), command_map=self.translate_command_map(d["commandMap"]), + default_opcode=int(d["defaultOpcode"]), tlm_channel_map=self.translate_tlm_channel_map(d["tlmChannelMap"]), tlm_channel_name_map=self.translate_tlm_channel_name_map( d["tlmChannelNameMap"] ), + default_tlm_channel_id=int(d["defaultTlmChannelId"]), event_map=self.translate_event_map(d["eventMap"]), + default_event_id=int(d["defaultEventId"]), param_map=self.translate_param_map(d["paramMap"]), spec_port_matching_list=self.translate_spec_port_matching_list( self.require_type(d["specPortMatchingList"], list) @@ -1013,8 +1016,11 @@ def translate_component(self, d: Dict[str, dict]) -> Component: port_matching_list=self.translate_port_matching_list( self.require_type(d["portMatchingList"], list) ), + default_param_id=int(d["defaultParamId"]), container_map=self.translate_container_map(d["containerMap"]), + default_container_id=int(d["defaultContainerId"]), record_map=self.translate_record_map(d["recordMap"]), + default_record_id=int(d["defaultRecordId"]) ) def translate_direct(self, d: Dict) -> Dict[AstId, Location]: diff --git a/tests/test_semantics_types.py b/tests/test_semantics_types.py new file mode 100644 index 0000000..3b57f31 --- /dev/null +++ b/tests/test_semantics_types.py @@ -0,0 +1,238 @@ +from fprime_python_model.model import FprimePythonModel +from fprime_python_model.semantics.format import ( + Format, + RationalField, + RationalFieldType, +) +from fprime_python_model.semantics.types_values import ( + ArrayType, + StringType, + IntegerType, + PrimitiveIntType, + PrimitiveType, + AliasType, + EnumType, + StructType, + AbsType, + Signedness, + StructValue, + AnonStructValue, + PrimitiveIntKind, + EnumConstantValue, + ArrayValue, + AnonArrayValue, + FloatValue, + FloatKind, + AnonArrayType, + FloatType, + AnonStructType, +) +from fprime_python_model.fpp_ast import fpp_ast +import os + + +def test_types(): + # Get location of test directory + test = "types" + test_dir = os.path.dirname(os.path.abspath(__file__)) + test_path = os.path.join(test_dir, test) + print(f"Test Case: {test}") + fpp_ref_file = os.path.join(test_path, f"{test}.fpp") + ast_file = os.path.join(test_path, f"{test}_ast.json") + analysis_file = os.path.join(test_path, f"{test}_analysis.json") + locations_file = os.path.join(test_path, f"{test}_locations.json") + files = [fpp_ref_file, ast_file, analysis_file, locations_file] + + # Make sure all test files exist + for f in files: + assert os.path.isfile(f) + + # Construct an FprimePythonModel using test inputs + model = FprimePythonModel(ast_file, locations_file, analysis_file) + + type_map = model.analysis.type_map + + # Integer + int_id = 128 + int_type = type_map[int_id] + assert isinstance(int_type, IntegerType) + assert int_type.is_numeric() + assert int_type.is_int() + assert not int_type.is_float() + assert not int_type.is_displayable() + + # Primitive Integer + primitive_int_id = 44 + primitive_int_type = type_map[primitive_int_id] + assert isinstance(primitive_int_type, PrimitiveIntType) + assert primitive_int_type.is_primitive() + assert primitive_int_type.is_int() + assert not primitive_int_type.is_float() + assert primitive_int_type.is_displayable() + + # Primitive (float) + primitive_float_id = 100 + primitive_float_type = type_map[primitive_float_id] + assert isinstance(primitive_float_type, PrimitiveType) + assert primitive_float_type.is_primitive() + assert primitive_float_type.is_float() + assert not primitive_float_type.is_int() + assert primitive_float_type.is_displayable() + + # String + string_id = 8 + string_type = type_map[string_id] + assert isinstance(string_type, StringType) + assert ( + string_type.size + and isinstance(string_type.size.data, fpp_ast.ExprLiteralInt) + and string_type.size.data.value == "40" + ) + assert string_type.is_displayable() + assert not string_type.is_numeric() + + # Alias types + alias_id = 121 + alias_type = type_map[alias_id] + alias_underlying_type = alias_type.get_underlying_type() + assert isinstance(alias_type, AliasType) + assert isinstance(alias_underlying_type, PrimitiveIntType) + assert alias_type.is_displayable() + assert alias_underlying_type.is_int() + assert alias_underlying_type.is_numeric() + assert not alias_underlying_type.is_float() + + # Arrays + array_id = 88 + array_type = type_map[array_id] + assert isinstance(array_type, ArrayType) + assert array_type.get_def_node_id() == 108 + assert array_type.get_array_size() == None + assert array_type.has_numeric_members() + assert array_type.is_displayable() + + array_id2 = 112 + array_type2 = type_map[array_id2] + assert isinstance(array_type2, ArrayType) + assert array_type2.get_array_size() == 4 + assert array_type2.has_numeric_members() + assert array_type2.format + first_field = array_type2.format.fields[0] + assert first_field[0].is_rational() and first_field[1] == "" + + # Enum + enum_id = 72 + enum_type = type_map[enum_id] + assert isinstance(enum_type, EnumType) + assert enum_type.is_displayable() + assert enum_type.default == None + assert ( + enum_type.rep_type.bit_width() == 32 + and enum_type.rep_type.signedness() == Signedness.SIGNED + ) + + # Struct + struct_id = 90 + struct_type = type_map[struct_id] + assert isinstance(struct_type, StructType) + assert struct_type.is_displayable() + anon_struct_member_type_map = { + "type": EnumType, + "history": ArrayType, + "pairHistory": ArrayType, + } + + pair_history_struct_type = StructType( + model.annotated_ast_id_map[103], + AnonStructType( + { + "time": FloatType(FloatKind.F32), + "value": FloatType(FloatKind.F32), + } + ), + ) + anon_struct_member_value_map = { + "type": EnumConstantValue( + ("TRIANGLE", 0), + EnumType( + model.annotated_ast_id_map[119], + PrimitiveIntType(PrimitiveIntKind.I32), + None, + ), + ), + "history": ArrayValue( + AnonArrayValue([FloatValue(0.0, FloatKind.F32)] * 4), + ArrayType( + model.annotated_ast_id_map[112], + AnonArrayType(4, FloatType(FloatKind.F32)), + ), + ), + "pairHistory": ArrayValue( + AnonArrayValue( + [ + StructValue( + AnonStructValue( + { + "time": FloatValue(0.0, FloatKind.F32), + "value": FloatValue(0.0, FloatKind.F32), + } + ), + pair_history_struct_type, + ) + ] + * 4 + ), + ArrayType( + model.annotated_ast_id_map[108], + AnonArrayType( + 4, + StructType( + model.annotated_ast_id_map[103], + AnonStructType( + { + "time": FloatType(FloatKind.F32), + "value": FloatType(FloatKind.F32), + } + ), + StructValue( + AnonStructValue( + { + "time": FloatValue(0.0, FloatKind.F32), + "value": FloatValue(0.0, FloatKind.F32), + } + ), + pair_history_struct_type, + ), + {}, + { + "time": Format( + "", [RationalField(None, RationalFieldType.FIXED)] + ), + "value": Format( + "", [RationalField(None, RationalFieldType.FIXED)] + ), + }, + ), + ), + ), + ), + } + + for k, v in struct_type.anon_struct.members.items(): + assert k in anon_struct_member_type_map + assert isinstance(v, anon_struct_member_type_map[k]) + + for ( + k, + v, + ) in struct_type.default.anon_struct.members.items(): + assert k in anon_struct_member_value_map + assert str(anon_struct_member_value_map[k]) == str(v) + + # Abstract type + abs_id = 64 + abs_type = type_map[abs_id] + assert isinstance(abs_type, AbsType) + assert not abs_type.is_displayable() + assert not abs_type.is_numeric() + assert not abs_type.is_primitive() diff --git a/tests/test_semantics_values.py b/tests/test_semantics_values.py new file mode 100644 index 0000000..d259d92 --- /dev/null +++ b/tests/test_semantics_values.py @@ -0,0 +1,96 @@ +from fprime_python_model.model import FprimePythonModel +from fprime_python_model.semantics.types_values import ( + StringValue, + IntegerValue, + BooleanValue, + FloatValue, + AnonArrayValue, + AnonArrayType, + AnonStructType, + AnonStructValue, + FloatKind, + FloatType, + BooleanType, + IntegerType, + StringType, +) +import os + + +def test_types(): + # Get location of test directory + test = "constants" + test_dir = os.path.dirname(os.path.abspath(__file__)) + test_path = os.path.join(test_dir, test) + print(f"Test Case: {test}") + fpp_ref_file = os.path.join(test_path, f"{test}.fpp") + ast_file = os.path.join(test_path, f"{test}_ast.json") + analysis_file = os.path.join(test_path, f"{test}_analysis.json") + locations_file = os.path.join(test_path, f"{test}_locations.json") + files = [fpp_ref_file, ast_file, analysis_file, locations_file] + + # Make sure all test files exist + for f in files: + assert os.path.isfile(f) + + # Construct an FprimePythonModel using test inputs + model = FprimePythonModel(ast_file, locations_file, analysis_file) + + value_map = model.analysis.value_map + + # Integer + int_id = 33 + int_value = value_map[int_id] + assert isinstance(int_value, IntegerValue) + assert int_value.value == 1 + assert isinstance(int_value.get_type(), IntegerType) + + # String + string_id = 5 + string_value = value_map[string_id] + assert isinstance(string_value, StringValue) + assert string_value.value == "This is a string." + assert isinstance(string_value.get_type(), StringType) + + # Boolean + bool_id = 73 + bool_value = value_map[bool_id] + assert isinstance(bool_value, BooleanValue) + assert bool_value.value == False + assert isinstance(bool_value.get_type(), BooleanType) + + # Float + float_id = 12 + float_value = value_map[float_id] + assert isinstance(float_value, FloatValue) + assert float_value.value == 2.0 + assert float_value.kind == FloatKind.F64 + assert isinstance(float_value.get_type(), FloatType) + + # Anon Struct + struct_value_map = { + "x": (IntegerValue, 1), + "y": (StringValue, "abc"), + "z": (BooleanValue, False), + } + + struct_id = 40 + struct_value = value_map[struct_id] + assert isinstance(struct_value, AnonStructValue) + assert isinstance(struct_value.get_type(), AnonStructType) + for unqual_name, value in struct_value.members.items(): + assert unqual_name in struct_value_map + assert isinstance(value, struct_value_map[unqual_name][0]) + assert value.value == struct_value_map[unqual_name][1] + + # Anon Array + array_values = [1, 2, 3] + array_id = 9 + array_value = value_map[array_id] + array_type = array_value.get_type() + assert isinstance(array_value, AnonArrayValue) + assert isinstance(array_type, AnonArrayType) + assert array_type.get_array_size() == 3 + assert isinstance(array_type.elt_type, IntegerType) + for index, value in enumerate(array_value.elements): + assert value.value == array_values[index] diff --git a/tests/to_json.py b/tests/to_json.py new file mode 100644 index 0000000..9351307 --- /dev/null +++ b/tests/to_json.py @@ -0,0 +1,282 @@ +from __future__ import annotations +import os +import json +from fprime_python_model.fpp_version import MIN_FPP_VERSION +from fprime_python_model.model import FprimePythonModel +from fprime_python_model.fpp_ast.fpp_ast_node import AstNode +from fprime_python_model.semantics.symbol import Symbol +from fprime_python_model.semantics.component_instance import ComponentInstance +from fprime_python_model.semantics.port_instance_identifier import ( + PortInstanceIdentifier, +) +from fprime_python_model.semantics.types_values import * +from fprime_python_model.semantics.analysis import Analysis +from dataclasses import is_dataclass, fields +from enum import Enum +from pathlib import Path +from typing import ( + Any, + Callable, + Dict, + Optional, + Type, + TypeVar, +) +from functools import lru_cache + +T = TypeVar("T") +SerializerFn = Callable[[Any], Any] + +SERIALIZERS: Dict[Type[Any], SerializerFn] = {} + + +def serializer(cls: Type[T]): + def wrapper(fn: Callable[[T], Any]): + SERIALIZERS[cls] = fn + return fn + + return wrapper + + +def find_serializer(obj: Any) -> Optional[SerializerFn]: + for cls, fn in SERIALIZERS.items(): + if isinstance(obj, cls): + return fn + return None + + +def snake_to_camel(s: str) -> str: + parts = s.split("_") + return parts[0] + "".join(p.capitalize() for p in parts[1:]) + + +@lru_cache(None) +def resolve_class_name(cls: type) -> str: + name = cls.__name__ + + suffix_to_strip = ("Value", "Type", "PortInstance", "Field") + prefix_to_strip = ("Command", "NonParamKind", "ParamKind") + + for suffix in suffix_to_strip: + if name.endswith(suffix) and name not in ("AliasType", "AbsType"): + name = name.removesuffix(suffix) + + for prefix in prefix_to_strip: + if name.startswith(prefix): + name = name[len(prefix) :] + break + + return name + + +def is_annotated_astnode_tuple(obj): + return isinstance(obj, tuple) and len(obj) == 3 and isinstance(obj[1], AstNode) + + +def serialize(obj): + fn = find_serializer(obj) + if fn: + return fn(obj) + + if obj is None: + return "None" + + # primitives + if isinstance(obj, (str, int, float, bool)): + return obj + + if isinstance(obj, Path): + return str(obj) + + # enums + if isinstance(obj, Enum): + return obj.name + + # AST node + if isinstance(obj, AstNode): + return {"astNodeId": obj.get_id()} + + # annotated AST tuple + if is_annotated_astnode_tuple(obj): + return serialize(obj[1]) + + # dataclasses + if is_dataclass(obj): + runtime = type(obj) + + data = { + snake_to_camel(f.name): serialize(getattr(obj, f.name)) for f in fields(obj) + } + + return {resolve_class_name(runtime): data} + + # dict + if isinstance(obj, dict): + return {str(k): serialize(v) for k, v in obj.items()} + + # list/tuple/set + if isinstance(obj, (list, tuple, set)): + return [serialize(x) for x in obj] + + if hasattr(obj, "__dict__"): + runtime = type(obj) + + data = { + snake_to_camel(k): serialize(v) + for k, v in obj.__dict__.items() + if not callable(v) and not k.startswith("_") + } + + return {resolve_class_name(runtime): data} + + return obj + + +# special cases + + +@serializer(AstNode) +def _ast_node(obj: AstNode): + return {"astNodeId": obj.get_id()} + + +@serializer(Symbol) +def _symbol(symbol: Symbol): + name = type(symbol).__name__.replace("Symbol", "") + return { + name: { + "nodeId": symbol.get_node_id(), + "unqualifiedName": symbol.get_unqualified_name(), + } + } + + +@serializer(ComponentInstance) +def _component_instance(obj: ComponentInstance): + data = serialize(obj.__dict__) + data["component"] = {"astNodeId": obj.component.a_node[1].get_id()} + return data + + +@serializer(PortInstanceIdentifier) +def _port_instance_identifier(obj: PortInstanceIdentifier): + data = serialize(obj.__dict__) + + if hasattr(obj.port_instance, "kind"): + data.setdefault("portInstance", {}) + data["portInstance"]["General"] = {"kind": obj.port_instance.kind.name} + + return data + + +@serializer(PrimitiveIntType) +def _primitive_int(obj: PrimitiveIntType): + return {"Int": {"PrimitiveInt": {"kind": {obj.kind.name: {}}}}} + + +@serializer(IntegerType) +def _integer(obj: IntegerType): + return {"Int": {"Integer": {}}} + + +@serializer(FloatType) +def _float(obj: FloatType): + return {"Primitive": {"Float": {"kind": {obj.kind.name: {}}}}} + + +def map_as_dict(m: Dict[Any, Any]) -> Dict[str, Any]: + return {str(k): serialize(v) for k, v in m.items()} + + +def analysis_to_json(analysis: Analysis) -> Dict[str, Any]: + return { + "fppVersion": str(MIN_FPP_VERSION), + "analysis": { + "componentInstanceMap": map_as_dict(analysis.component_instance_map), + "componentMap": map_as_dict(analysis.component_map), + "includedFileSet": serialize(analysis.included_file_set), + "inputFileSet": serialize(analysis.input_file_set), + "locationSpecifierMap": serialize(analysis.location_specifier_map), + "parentSymbolMap": serialize(analysis.parent_symbol_map), + "symbolScopeMap": serialize(analysis.symbol_scope_map), + "topologyMap": map_as_dict(analysis.topology_map), + "typeMap": map_as_dict(analysis.type_map), + "useDefMap": serialize(analysis.use_def_map), + "valueMap": map_as_dict(analysis.value_map), + }, + } + + +# for debugging +def json_diff(d1: Any, d2: Any, path="") -> list[str]: + diffs = [] + + if isinstance(d1, dict) and isinstance(d2, dict): + keys1 = set(d1.keys()) + keys2 = set(d2.keys()) + + # Keys only in d1 + for key in keys1 - keys2: + diffs.append(f"{path}{key} only in first JSON") + + # Keys only in d2 + for key in keys2 - keys1: + diffs.append(f"{path}{key} only in second JSON") + + # Keys in both + for key in keys1 & keys2: + diffs.extend(json_diff(d1[key], d2[key], path=f"{path}{key}.")) + + elif isinstance(d1, list) and isinstance(d2, list): + min_len = min(len(d1), len(d2)) + for i in range(min_len): + diffs.extend(json_diff(d1[i], d2[i], path=f"{path}{i}.")) + if len(d1) > len(d2): + for i in range(min_len, len(d1)): + diffs.append(f"{path}{i} only in first JSON") + elif len(d2) > len(d1): + for i in range(min_len, len(d2)): + diffs.append(f"{path}{i} only in second JSON") + else: + if d1 != d2: + diffs.append(f"{path[:-1]} differs: {d1} != {d2}") + + return diffs + + +TEST_DIR = Path(__file__).resolve().parent + +tests = [ + "types" +] # ["commands", "events", "telemetry", "parameters", "location_specifier"] +for test in tests: + print(f"Test case: {test}") + test_path = TEST_DIR / test + fpp_ref_file = test_path / f"{test}.fpp" + ast_file = test_path / f"{test}_ast.json" + analysis_file = test_path / f"{test}_analysis.json" + locations_file = test_path / f"{test}_locations.json" + + files = [fpp_ref_file, ast_file, analysis_file, locations_file] + + # Make sure all test files exist + for f in files: + assert os.path.isfile(f) + + # Construct an FprimePythonModel using test inputs + model = FprimePythonModel(ast_file, locations_file, analysis_file) + with open("analysis.json", "w") as f: + json.dump(analysis_to_json(model.analysis), f, indent=4) + + with open(analysis_file, "r") as f: + orig_analysis = json.load(f) + + with open("analysis.json", "r") as f: + test_analysis = json.load(f) + + res = json_diff(orig_analysis["analysis"], test_analysis["analysis"]) + if not res: + print("No diffs!") + for r in res: + print(r) + print()