Init Commit: Moved bproto to seperate repo

This commit is contained in:
AlexanderHD27
2025-04-14 14:43:03 +02:00
commit 45bfc724fc
125 changed files with 10822 additions and 0 deletions

15
src/backend/__init__.py Normal file
View File

@@ -0,0 +1,15 @@
from abc import ABC, abstractmethod
from jinja2 import Environment
from protocol_components.protocol import ProtocolDefinitions
from backend.fsOutput import BackendFileSystemOutput
class BackendRenderer(ABC):
@abstractmethod
def __init__(self, output_folder: str):
raise NotImplementedError()
@abstractmethod
def render(self, protocol_definition: ProtocolDefinitions, jinja_env: Environment) -> BackendFileSystemOutput:
raise NotImplementedError()

237
src/backend/fsOutput.py Normal file
View File

@@ -0,0 +1,237 @@
from abc import ABC, abstractmethod
from typing import Literal
import os
import re
import shutil
import jinja2
FILE_TREE_STRING_POSITION_t = Literal["last", "first", "mid"]
def map_link_symbol(position: FILE_TREE_STRING_POSITION_t) -> str:
"""For displaying the file tree in the console, this function maps the position of the file in the tree to the correct link symbol.
Args:
position (FILE_TREE_STRING_POSITION_t): The position of the file in the tree.
Returns:
str: The correct link symbol for the position.
"""
match position:
case "last":
return ""
case "first":
return ""
case "mid":
return ""
case _: # This should not happend
return ""
class BackendFileSystemOutput(ABC):
"""Represents a node in the filesystem output tree.
This class is abstract and should be inherited by classes that represent files or folders in the filesystem output tree.
"""
INDENT_FILE_TREE_STRING = " " * 4
@abstractmethod
def saveToDisk(self, parent_path: str):
"""Saves the file or folder to the disk.
Calles their potential subfiles and subfolders to save themselves.
Args:
parent_path (str): Where to save the file or folder.
"""
pass
@abstractmethod
def assemble_file_tree_string(self, indent: str = "", position: FILE_TREE_STRING_POSITION_t = "last") -> str:
"""Assembles a string representation of the file tree.
This is ment to display to the use what the output will look like.
Args:
layer (int, optional): Indentation layer. Defaults to "". Should be left untouched for the intial call.
position (FILE_TREE_STRING_POSITION_t, optional): For multiple children nodes, what position in the parent node. Defaults to "first". Should be left untouched for the intial call.
Returns:
str: The string representation of the file tree (multiline).
"""
pass
@abstractmethod
def toString(self) -> dict[str, str]:
"""Renders out FilesystemOutput as a dictionary of strings.
Each key is a filename and the value is the content of the file.
Folders are include in the filename list with a trailing slash, but not as a separate key.
Returns:
dict[str, str]: _description_
"""
pass
class BackendFSOutFile(BackendFileSystemOutput):
"""Represents a file with string conntent in the filesystem output tree.
Inherited from BackendFileSystemOutput.
"""
def __init__(self, filename: str, conntent: str):
"""Initializes the file with conntent and a filename.
Args:
conntent (str): File conntent as string.
filename (str): Filename.
"""
self.conntent = conntent
self.filename = filename
def saveToDisk(self, parent_path: str):
p = os.path.normpath(os.path.join(parent_path, self.filename))
with open(p, "w") as f:
f.write(self.conntent)
def toString(self) -> dict[str, str]:
return {self.filename: self.conntent}
def assemble_file_tree_string(self, indent: str = "", position: FILE_TREE_STRING_POSITION_t = "last") -> str:
return indent + f"{map_link_symbol(position)}── [{self.filename}]\n"
class BackendFSOutFolder(BackendFileSystemOutput):
"""Represents a folder in the filesystem output tree.
This Node has multiple subnodes, can be files or folders.
Inherited from BackendFileSystemOutput.
"""
def __init__(self, name: str, sub_content: list[BackendFileSystemOutput] = []):
"""Initializes the folder with a name and subnodes.
Args:
name (str): Name of the folder.
sub_content (list[BackendFileSystemOutput]): Subnodes of the folder.
"""
self.name = name
self.sub_content: list[BackendFileSystemOutput] = sub_content
def saveToDisk(self, parent_path: str):
p = os.path.normpath(os.path.join(parent_path, self.name))
# This cause an **incident**
# if os.path.exists(p):
# shutil.rmtree(p)
os.makedirs(p, exist_ok=True)
for sub in self.sub_content:
sub.saveToDisk(p)
def toString(self) -> dict[str, str]:
res = {}
for sub in self.sub_content:
res.update({
os.path.normpath(os.path.join(self.name, k)): v
for k, v in sub.toString().items()
})
return res
def assemble_file_tree_string(self, indent: str = "", position: FILE_TREE_STRING_POSITION_t = "last") -> str:
if position == "last":
indent_string = indent + BackendFileSystemOutput.INDENT_FILE_TREE_STRING
else:
indent_string = indent + ("" + BackendFileSystemOutput.INDENT_FILE_TREE_STRING[1:])
s = indent + f"{map_link_symbol(position)}── {self.name}\n"
for i, sub in enumerate(self.sub_content):
if i == len(self.sub_content) - 1:
s += sub.assemble_file_tree_string(indent_string, "last")
else:
s += sub.assemble_file_tree_string(indent_string, "mid")
return s
class BackendFSOutStaticConent(BackendFileSystemOutput):
"""Represents a static file or folder in the filesystem output tree.
The content is copied from the source to the output.
Inherited from BackendFileSystemOutput.
"""
def __init__(self, src_path: str, dest_path: str | None = None):
"""Initializes the static content with a source path and a destination path.
If dest_path is None, the top level folder (basename) of the source path is used.
Args:
src_path (str): _description_
dest_path (str | None, optional): _description_. Defaults to None.
"""
self.src_path = src_path
self.dest_path = dest_path or os.path.basename(src_path)
def saveToDisk(self, parent_path: str):
dest = os.path.normpath(os.path.join(parent_path, self.dest_path))
# if os.path.exists(dest):
# shutil.rmtree(dest)
shutil.copy(self.src_path, dest)
def toString(self) -> dict[str, str]:
res = {}
if os.path.isfile(self.src_path):
with open(self.src_path, "r") as f:
res.update({
os.path.normpath(os.path.join(self.dest_path, os.path.basename(self.src_path))): f.read()
})
else:
for root, _, files in os.walk(self.src_path):
for file in files:
with open(os.path.normpath(os.path.join(root, file)), "r") as f:
res.update({
os.path.normpath(os.path.join(self.dest_path, file)): f.read()
})
return res
def assemble_file_tree_string(self, indent: str = "", position: FILE_TREE_STRING_POSITION_t = "last") -> str:
return indent + f"{map_link_symbol(position)}── Static [{self.dest_path}]\n"
class BackendFSOutputJinjaFile(BackendFSOutFile):
"""Represents a file that is rendered with jinja2.
Inherited from BackendFSOutFile.
"""
def __init__(self, jinja_env: jinja2.Environment, template_name: str, output_name: str | None, context: dict):
"""Initializes the file with a jinja2 environment, a template name, an output name and a context.
Args:
jinja_env (jinja2.Environment): Jinja2 environment to render the template.
template_name (str): Jinja2 template name.
output_name (str): Filename of the output; If left None, template_name is used and the .jinja2 extension is removed.
context (dict): Context for the template, needed for rendering.
"""
self.jinja_env = jinja_env
self.template_name = template_name
self.output_name = output_name
self.context = context
if self.output_name is None:
self.output_name = re.sub(r"(?i)\b\.jinja\d?\b", "", self.template_name)
def saveToDisk(self, parent_path: str):
template = self.jinja_env.get_template(self.template_name)
output = template.render(self.context)
p = os.path.normpath(os.path.join(parent_path, self.output_name))
with open(p, "w") as f:
f.write(output)
def toString(self) -> dict[str, str]:
template = self.jinja_env.get_template(self.template_name)
output = template.render(self.context)
return {self.output_name: output}
def assemble_file_tree_string(self, indent: str = "", position: FILE_TREE_STRING_POSITION_t = "last") -> str:
return indent + f"{map_link_symbol(position)}── Template [{self.output_name}]\n"

View File

@@ -0,0 +1,270 @@
from backend import BackendRenderer
from protocol_components.dtypes import BprotoFieldBaseType
from protocol_components.protocol import ProtocolDefinitions
from protocol_components.bitfields import Bitfield
from protocol_components.field import Field, FieldBitfield, FieldEnum
from protocol_components.crc import CRC_SIZE
from backend.fsOutput import BackendFileSystemOutput, BackendFSOutputJinjaFile, BackendFSOutFolder, BackendFSOutStaticConent
from nameHandling.style.cNameStyle import NameStyleC
from nameHandling.base import ComponentName
from jinja2 import Environment
from copy import deepcopy
C_DATA_TYPE_MAP = {
BprotoFieldBaseType.BOOL: ("bool", False),
BprotoFieldBaseType.UINT8: ("uint8_t", 0),
BprotoFieldBaseType.UINT16: ("uint16_t", 0),
BprotoFieldBaseType.UINT32: ("uint32_t", 0),
BprotoFieldBaseType.UINT64: ("uint64_t", 0),
BprotoFieldBaseType.INT8: ("int8_t", 0),
BprotoFieldBaseType.INT16: ("int16_t", 0),
BprotoFieldBaseType.INT32: ("int32_t", 0),
BprotoFieldBaseType.INT64: ("int64_t", 0),
BprotoFieldBaseType.FLOAT32: ("float", 0.0),
BprotoFieldBaseType.FLOAT64: ("double", 0.0),
BprotoFieldBaseType.CHAR: ("char", "''"),
BprotoFieldBaseType.STRING: ("char", "\"\""),
BprotoFieldBaseType.BITFIELD: ("", ""),
BprotoFieldBaseType.ENUM: ("", ""),
}
FOR_LOOP_THRESHOLD = 8
def map_data_type(field: Field) -> str:
if isinstance(field, FieldBitfield):
return {
"name": NameStyleC.toStr(field.bitfield.name, "bitfield_name"),
"aux": None,
"array": ""
}
elif isinstance(field, FieldEnum):
return {
"name": NameStyleC.toStr(field.enum.name, "enum_name"),
"aux": C_DATA_TYPE_MAP[field.enum.map_auxiliary_datatypes()][0],
"array": ""
}
elif field.type in BprotoFieldBaseType:
if field.array_size > 1 or field.type == BprotoFieldBaseType.STRING:
return {
"name": C_DATA_TYPE_MAP[field.type][0],
"aux": None,
"array": f"[{field.array_size}]"
}
else:
return {
"name": C_DATA_TYPE_MAP[field.type][0],
"aux": None,
"array": ""
}
else:
raise ValueError(f"Type {field.type} is not implement in map_Data_type()")
def convert_bitfield_to_c_style_bit_list(bitfield: Bitfield) -> list[tuple[str, int]]:
last_pos = 0
res = []
for bit_name, pos in bitfield.bits.items():
if pos - (last_pos + 1) > 1:
# We skiped bits
res.append({
"name": "",
"length": pos - (last_pos + 1) - 1,
"spacer": True,
})
last_pos = pos
res.append({
"name": NameStyleC.toStr(bit_name, "bitfield_member"),
"length": 1,
"spacer": False,
"bit_pos": pos % 8,
"byte_pos": pos // 8
})
return res
def generate_max_message_struct_expr(p: ProtocolDefinitions) -> str:
res = "{}"
for i in p.messages.values():
res = res.format("MAX({}, sizeof(" + NameStyleC.toStr(i.name, "struct_name") + "))")
return res.format(0)
def convert_field_to_toBytes_overwrite(field: Field):
if isinstance(field, FieldBitfield):
return f"toBytes_{NameStyleC.toStr(field.bitfield.name, 'bitfield_name')}(data, &msg->{NameStyleC.toStr(field.name, 'struct_member')})"
else:
return None
def convert_field_to_fromBytes_overwrite(field: Field):
if isinstance(field, FieldBitfield):
return f"fromBytes_{NameStyleC.toStr(field.bitfield.name, 'bitfield_name')}(data, &msg->{NameStyleC.toStr(field.name, 'struct_member')})"
else:
return None
class CBackendRenderer(BackendRenderer):
def __init__(self, output_folder):
self.output_folder = output_folder
self.protocol: ProtocolDefinitions = None
self.jinja_env: Environment = None
self.jinja_context: dict = None
def render(self,
protocol_definition: ProtocolDefinitions,
jinja_env: Environment) -> BackendFileSystemOutput:
if protocol_definition.protocol_hash_inital is None:
raise ValueError("Protocol hash inital for crc16 should no be None")
self.protocol = NameStyleC.preprocess(deepcopy(protocol_definition))
self.jinja_env = jinja_env
self.jinja_context = {
"protocol": {
"name": NameStyleC.toStr(self.protocol.name, "enum_item"),
"version": self.protocol.version,
"crc_size": CRC_SIZE,
"crc_initial": hex(self.protocol.protocol_hash_inital),
"max_struct_size_expr": generate_max_message_struct_expr(self.protocol),
"max_data_size": max([i.get_size_bytes() for i in self.protocol.messages.values()]) + 1 + CRC_SIZE
},
"import_name": NameStyleC.toStr(self.protocol.name, 'enum_item'),
"enums": [
{
"name": NameStyleC.toStr(name, "enum_name"),
"consts": [
(NameStyleC.toStr(k, "enum_item"), v)
for k, v in e.values.items()
]
}
for name, e in self.protocol.enums.items()
],
"bitfields": [
{
"name": NameStyleC.toStr(b.name, "bitfield_name"),
"size": b.get_size_bytes(),
"size_name": NameStyleC.toStr(b.name, "enum_item").upper() + "_SIZE",
"bits": convert_bitfield_to_c_style_bit_list(b),
}
for name, b in self.protocol.bitfields.items()
],
"messages": [
{
"name": NameStyleC.toStr(name, "struct_name"),
"id": m.get_identifier(),
"id_name": NameStyleC.toStr(name, "enum_item") + "_ID",
"size": m.get_size_bytes() + 1 + CRC_SIZE,
"size_name": NameStyleC.toStr(name, "enum_item") + "_SIZE",
"fields": [
{
"name": NameStyleC.toStr(k, "struct_member"),
"size": v.get_size_bytes(),
"base_size": v.get_base_size_bytes(),
"array_size": v.array_size,
"decompose_mode": (
"for"
if v.array_size > FOR_LOOP_THRESHOLD or v.type == BprotoFieldBaseType.STRING
else (
"inline_for" if v.array_size > 1 else "normal"
)
),
"type": map_data_type(v),
"pre_init_value": 0 if isinstance(v, FieldEnum) else None,
"toBytes_overwride": convert_field_to_toBytes_overwrite(v),
"fromBytes_overwride": convert_field_to_fromBytes_overwrite(v)
}
for k, v in m.fields.items()
]
}
for name, m, in self.protocol.messages.items()
]
}
return BackendFSOutFolder(self.output_folder, [
BackendFSOutputJinjaFile( # CMake
self.jinja_env,
"c/template/bproto.cmake.jinja2",
f"{NameStyleC.toStr(self.protocol.name, 'enum_item')}.cmake",
self.jinja_context
),
BackendFSOutFolder("include", [
BackendFSOutputJinjaFile( # Enum
self.jinja_env,
"c/template/include/enums.h.jinja2",
f"{NameStyleC.toStr(self.protocol.name, 'enum_item')}_enum.h",
self.jinja_context
),
BackendFSOutputJinjaFile( # Message
self.jinja_env,
"c/template/include/message/message.h.jinja2",
f"{NameStyleC.toStr(self.protocol.name, 'enum_item')}_message.h",
self.jinja_context
),
BackendFSOutputJinjaFile( # Bitfields
self.jinja_env,
"c/template/include/bitfield/bitfield.h.jinja2",
f"{NameStyleC.toStr(self.protocol.name, 'enum_item')}_bitfield.h",
self.jinja_context
),
BackendFSOutputJinjaFile( # Bitfields
self.jinja_env,
"c/template/include/crc.h.jinja2",
f"{NameStyleC.toStr(self.protocol.name, 'enum_item')}_crc.h",
self.jinja_context
),
]),
BackendFSOutFolder("src", [
BackendFSOutputJinjaFile( # Bitfields
self.jinja_env,
"c/template/src/bitfield.c.jinja2",
f"{NameStyleC.toStr(self.protocol.name, 'enum_item')}_bitfield.c",
self.jinja_context
),
BackendFSOutputJinjaFile( # Message fromBytes
self.jinja_env,
"c/template/src/message/fromBytes.c.jinja2",
f"{NameStyleC.toStr(self.protocol.name, 'enum_item')}_message_fromBytes.c",
self.jinja_context
),
BackendFSOutputJinjaFile( # Message toBytes
self.jinja_env,
"c/template/src/message/toBytes.c.jinja2",
f"{NameStyleC.toStr(self.protocol.name, 'enum_item')}_message_toBytes.c",
self.jinja_context
),
BackendFSOutputJinjaFile( # CRC
self.jinja_env,
"c/template/src/crc.c.jinja2",
f"{NameStyleC.toStr(self.protocol.name, 'enum_item')}_crc.c",
self.jinja_context
),
BackendFSOutputJinjaFile( # Map Message Size/ID
self.jinja_env,
"c/template/src/message/mapping.c.jinja2",
f"{NameStyleC.toStr(self.protocol.name, 'enum_item')}_mapping.c",
self.jinja_context
),
]),
])

View File

@@ -0,0 +1,244 @@
from backend import BackendRenderer
from protocol_components.dtypes import BprotoFieldBaseType
from protocol_components.protocol import ProtocolDefinitions
from protocol_components.field import Field, FieldBitfield, FieldEnum
from protocol_components.crc import CRC_SIZE
from backend.fsOutput import BackendFileSystemOutput, BackendFSOutputJinjaFile, BackendFSOutFolder, BackendFSOutStaticConent
from nameHandling.style.pythonNameStyle import NameStylePython
from nameHandling.base import ComponentName
from jinja2 import Environment
from copy import deepcopy
ENDIANDNESS = "<"
PYTHON_DATA_TYPE_MAP = {
BprotoFieldBaseType.BOOL: ("bool", False),
BprotoFieldBaseType.UINT8: ("int", 0),
BprotoFieldBaseType.UINT16: ("int", 0),
BprotoFieldBaseType.UINT32: ("int", 0),
BprotoFieldBaseType.UINT64: ("int", 0),
BprotoFieldBaseType.INT8: ("int", 0),
BprotoFieldBaseType.INT16: ("int", 0),
BprotoFieldBaseType.INT32: ("int", 0),
BprotoFieldBaseType.INT64: ("int", 0),
BprotoFieldBaseType.FLOAT32: ("float", 0.0),
BprotoFieldBaseType.FLOAT64: ("float", 0.0),
BprotoFieldBaseType.CHAR: ("str", "''"),
BprotoFieldBaseType.STRING: ("str", "''"),
BprotoFieldBaseType.BITFIELD: ("", ""),
BprotoFieldBaseType.ENUM: ("", ""),
}
PYTHON_DATA_TYPE_STRUCT_FORMAT = {
BprotoFieldBaseType.BOOL: "B",
BprotoFieldBaseType.UINT8: "B",
BprotoFieldBaseType.UINT16: "H",
BprotoFieldBaseType.UINT32: "I",
BprotoFieldBaseType.UINT64: "Q",
BprotoFieldBaseType.INT8: "b",
BprotoFieldBaseType.INT16: "h",
BprotoFieldBaseType.INT32: "i",
BprotoFieldBaseType.INT64: "q",
BprotoFieldBaseType.FLOAT32: "f",
BprotoFieldBaseType.FLOAT64: "d",
}
def map_to_bytes_conversion(field: Field, endiend: str) -> str:
name = NameStylePython.toStr(field.name, "class_member")
if isinstance(field, FieldEnum):
return f"struct.pack('{endiend}B', self.{name}.value)"
elif isinstance(field, FieldBitfield):
return f"self.{name}.to_bytes()"
elif field.type in PYTHON_DATA_TYPE_STRUCT_FORMAT:
if field.array_size > 1:
argument = ", ".join([
f"self.{name}[{i}]"
for i in range(field.array_size)
])
return f"struct.pack('{endiend}{PYTHON_DATA_TYPE_STRUCT_FORMAT[field.type] * field.array_size}', {argument})"
else:
return f"struct.pack('{endiend}{PYTHON_DATA_TYPE_STRUCT_FORMAT[field.type]}', self.{name})"
elif field.type == BprotoFieldBaseType.STRING or field.type == BprotoFieldBaseType.CHAR:
return f"self.{name}.encode('ascii')[:{field.array_size}].ljust({field.array_size}, b'\\x00')"
else:
raise TypeError(f"Cannot convert field {field} to python to bytes conversion string")
def map_from_bytes_conversion(field: Field, endiend: str) -> str:
if isinstance(field, FieldEnum):
return f"{NameStylePython.toStr(field.enum.name, "enum_name")}(struct.unpack('{endiend}B', data[:{field.get_size_bytes()}])[0])"
elif isinstance(field, FieldBitfield):
return f"{NameStylePython.toStr(field.bitfield.name, "class_name")}().from_bytes(data[:{field.get_size_bytes()}])"
elif field.type == BprotoFieldBaseType.STRING or field.type == BprotoFieldBaseType.CHAR:
return f"data[:{field.get_size_bytes()}].decode('ascii').strip('\\x00')"
elif field.type == BprotoFieldBaseType.BOOL:
if field.array_size > 1:
return f"[bool(i) for i in struct.unpack('{endiend}{PYTHON_DATA_TYPE_STRUCT_FORMAT[field.type] * field.array_size}', data[:{field.get_size_bytes()}])]"
else:
return f"bool(struct.unpack('>{PYTHON_DATA_TYPE_STRUCT_FORMAT[field.type]}', data[:{field.get_size_bytes()}])[0])"
elif field.type in PYTHON_DATA_TYPE_STRUCT_FORMAT:
if field.array_size > 1:
return f"list(struct.unpack('{endiend}{PYTHON_DATA_TYPE_STRUCT_FORMAT[field.type] * field.array_size}', data[:{field.get_size_bytes()}]))"
else:
return f"struct.unpack('{endiend}{PYTHON_DATA_TYPE_STRUCT_FORMAT[field.type]}', data[:{field.get_size_bytes()}])[0]"
else:
raise TypeError(f"Cannot convert field {field} to python to bytes conversion string")
def map_data_type_anotation(field: Field) -> str:
if isinstance(field, FieldEnum):
return f"{NameStylePython.toStr(field.enum.name, "enum_name")}"
elif isinstance(field, FieldBitfield):
return f"{NameStylePython.toStr(field.bitfield.name, "class_name")}"
elif field.type in PYTHON_DATA_TYPE_STRUCT_FORMAT:
if field.array_size > 1:
return f"Annotated[List[{PYTHON_DATA_TYPE_MAP[field.type][0]}], {field.array_size}]"
else:
return f"{PYTHON_DATA_TYPE_MAP[field.type][0]}"
elif field.type == BprotoFieldBaseType.STRING or field.type == BprotoFieldBaseType.CHAR:
return "str"
else:
raise TypeError(f"Cannot convert field {field} to python to bytes conversion string")
class PythonBackendRenderer(BackendRenderer):
def __init__(self, output_folder):
self.output_folder = output_folder
self.protocol: ProtocolDefinitions = None
self.jinja_env: Environment = None
self.jinja_context: dict = None
def render(self,
protocol_definition: ProtocolDefinitions,
jinja_env: Environment) -> BackendFileSystemOutput:
if protocol_definition.protocol_hash_inital is None:
raise ValueError("Protocol hash inital for crc16 should no be None")
self.protocol = NameStylePython.preprocess(deepcopy(protocol_definition))
self.jinja_env = jinja_env
self.jinja_context = {
"protocol": {
"name": NameStylePython.toStr(self.protocol.name, "enum_item"),
"version": self.protocol.version,
"crc_size": CRC_SIZE,
"crc_initial": hex(self.protocol.protocol_hash_inital),
"endian_format": ENDIANDNESS,
"endian_str": "little" if ENDIANDNESS == "<" else "big"
},
"import_name": NameStylePython.toStr(self.protocol.name, 'enum_item'),
"enums": [
{
"name": NameStylePython.toStr(name, "enum_name"),
"consts": [
(NameStylePython.toStr(k, "enum_item"), v)
for k, v in e.values.items()
]
}
for name, e in self.protocol.enums.items()
],
"bitfields": [
{
"name": NameStylePython.toStr(b.name, "class_name"),
"size": b.get_size_bytes(),
"bits": [
{
"name": NameStylePython.toStr(k, "class_member"),
"pos": v
}
for k, v in b.bits.items()
]
}
for name, b in self.protocol.bitfields.items()
],
"messages": [
{
"name": NameStylePython.toStr(name, "class_name"),
"id": m.get_identifier(),
"id_name": NameStylePython.toStr(ComponentName(["MSG", "ID"]) + name, "enum_item"),
"size": m.get_size_bytes() + 1 + CRC_SIZE,
"fields": [
{
"name": NameStylePython.toStr(k, "class_member"),
"default_value": NameStylePython.toStr(v.bitfield.name, "class_name") + "()"
if v.type == BprotoFieldBaseType.BITFIELD
else (
NameStylePython.toStr(v.enum.name, "enum_name") + "." + NameStylePython.toStr(list(v.enum.values.keys())[0], "enum_item")
if v.type == BprotoFieldBaseType.ENUM
else PYTHON_DATA_TYPE_MAP[v.type][1]
),
"size": v.get_size_bytes(),
"array_size": v.array_size,
"type": map_data_type_anotation(v),
"to_bytes_conversion": map_to_bytes_conversion(v, ENDIANDNESS),
"from_bytes_conversion": map_from_bytes_conversion(v, ENDIANDNESS)
}
for k, v in m.fields.items()
]
}
for name, m, in self.protocol.messages.items()
]
}
return BackendFSOutFolder(self.output_folder, [
BackendFSOutputJinjaFile( # Enum
self.jinja_env,
"python/template/enum.py.jinja2",
f"{NameStylePython.toStr(self.protocol.name, 'enum_item')}_protocol_enum.py",
self.jinja_context
),
BackendFSOutputJinjaFile( # Bitfields
self.jinja_env,
"python/template/bproto_protocol_bitfield.py.jinja2",
f"{NameStylePython.toStr(self.protocol.name, 'enum_item')}_protocol_bitfield.py",
self.jinja_context
),
BackendFSOutputJinjaFile( # Message IDs
self.jinja_env,
"python/template/bproto_protocol_packets_ids.py.jinja2",
f"{NameStylePython.toStr(self.protocol.name, 'enum_item')}_protocol_message_ids.py",
self.jinja_context
),
BackendFSOutputJinjaFile( # Message
self.jinja_env,
"python/template/bproto_protocol_packets.py.jinja2",
f"{NameStylePython.toStr(self.protocol.name, 'enum_item')}_protocol_packets.py",
self.jinja_context
),
BackendFSOutStaticConent(
"template/python/static/bproto_base.py",
"bproto_base.py"
),
BackendFSOutStaticConent(
"template/python/static/bproto_error.py",
"bproto_error.py"
)
])

View File

@@ -0,0 +1,71 @@
from backend import BackendRenderer
from jinja2 import Environment
from protocol_components.dtypes import BprotoFieldBaseType
from protocol_components.protocol import ProtocolDefinitions
from backend.fsOutput import BackendFileSystemOutput, BackendFSOutputJinjaFile, BackendFSOutFolder
from nameHandling.base import NameStyleBproto
class TxtBackendRenderer(BackendRenderer):
def __init__(self, output_folder):
self.output_folder = output_folder
def render(self,
protocol_definition: ProtocolDefinitions,
jinja_env: Environment) -> BackendFileSystemOutput:
return BackendFSOutFolder(self.output_folder, [
BackendFSOutputJinjaFile(
jinja_env,
"txt/protocolSummary.txt.jinja2",
"protocolSummary.txt", context={
"name": NameStyleBproto.toStr(protocol_definition.name),
"version": protocol_definition.version,
"enums": [
{
"name": NameStyleBproto.toStr(name),
"consts": [
(NameStyleBproto.toStr(k, "enum"), v)
for k, v in e.values.items()
]
}
for name, e in protocol_definition.enums.items()
],
"bitfields": [
{
"name": NameStyleBproto.toStr(name),
"fields": [
(NameStyleBproto.toStr(k, "member"), v)
for k, v in b.bits.items()
]
}
for name, b in protocol_definition.bitfields.items()
],
"messages": [
{
"name": NameStyleBproto.toStr(name),
"id": m.get_identifier(),
"size": m.get_size_bytes(),
"fields": [
{
"name": NameStyleBproto.toStr(k, "member"),
"type": v.type.value,
"size": v.get_size_bytes(),
"array_size": v.array_size,
"ref": NameStyleBproto.toStr(v.bitfield.name)
if v.type == BprotoFieldBaseType.BITFIELD
else (
NameStyleBproto.toStr(v.enum.name)
if v.type == BprotoFieldBaseType.ENUM
else None
)
}
for k, v in m.fields.items()
]
}
for name, m, in protocol_definition.messages.items()
]
}
)]
)

54
src/compiler.py Normal file
View File

@@ -0,0 +1,54 @@
from backend import BackendRenderer
from backend.rendering.txtBackend import TxtBackendRenderer
from backend.fsOutput import BackendFileSystemOutput, BackendFSOutFolder
from protocol_components.crc import crc16_calc_inital
from protocol_components.protocol import ProtocolDefinitions
from parser.parser import parse_ast_string, bproto_ErrorListener
from parser.ast_visitor import BprotoASTVisitor
from jinja2 import FileSystemLoader, Environment
import sys
class BprotoCompiler:
def __init__(self, backends: list[BackendRenderer, str], template_directory: str):
self.backends = backends
self.template_dir = template_directory
def compile(self, source_text: str, main_output_folder: str) -> BackendFSOutFolder:
# Stage 1 - Parse & Create AST
ast = parse_ast_string(source_text)
if type(ast) is bproto_ErrorListener:
print(
ast.get_syntax_error_message(source_text),
file=sys.stderr
)
sys.exit(1)
# Stage 2 - Interpret AST **Magic** and build ProtocolDefinition Object
# Also this does the forward and backward resolving
vinterp = BprotoASTVisitor()
protocol_definition: ProtocolDefinitions = vinterp.visit(ast)
# Stage 3.1 - Render Txt for Hash
jinja_env = Environment(
loader=FileSystemLoader(self.template_dir)
)
txt_backend = TxtBackendRenderer(".")
txt_res = txt_backend.render(protocol_definition, jinja_env).toString().get("protocolSummary.txt")
protocol_definition.protocol_hash_inital = crc16_calc_inital(txt_res.encode("ascii"))
# Stage 3.2 - Backend Rendering
backend_outputs: list[BackendFileSystemOutput] = []
for backend in self.backends:
backend_outputs.append(
backend.render(protocol_definition, jinja_env)
)
return BackendFSOutFolder(main_output_folder, backend_outputs)

113
src/errors.py Normal file
View File

@@ -0,0 +1,113 @@
from protocol_components import AbstractProtocolComponent
class BprotoCompilerError(Exception):
"""A General error that is raised during the compilation stage.
Inherited from Exception.
"""
def __init__(self, message: str):
super().__init__(message)
class BprotoFrontendError(BprotoCompilerError):
"""Error raised during the frontend compilation stage.
Inherited from BprotoCompilerError.
"""
def __init__(self, message):
super().__init__(message)
class BprotoAlreadyDefinedError(BprotoFrontendError):
"""Error that indicates that a protocol component in the same category/scope with the same name is already defined.
This error should be use during the frontend compilation state.
Inherited from BprotoFrontendError.
"""
def __init__(self, entity: AbstractProtocolComponent):
self.entity = entity
super().__init__(f"{self.entity.get_type_name()} {self.entity.get_name()} already defined!")
class BprotoDuplicateNameError(BprotoFrontendError):
"""Error that an item with an a entity with the same name is already defined in the same scope.
This error should be use during the frontend compilation state.
Its very similar to BprotoAlreadyDefinedError, but this error is used for items that are not protocol components like fields or enum values
Inherited from BprotoFrontendError.
"""
def __init__(self, item: str, parent_entity: AbstractProtocolComponent):
self.item = item
self.parent = parent_entity
super().__init__(f"{item} already defined in {parent_entity.get_type_name()} {parent_entity.get_name()}!")
class BprotoDuplicateEnumValueError(BprotoFrontendError):
"""Error raise when building an enumeration and a value is used more than once.
This error should be use during the frontend compilation state.
Incomparision to BprotoDuplicateNameError, this error is used for enum values, not the name.
Its specific to the enum component.
Inherited from BprotoFrontendError.
"""
def __init__(self, value: int, enum: AbstractProtocolComponent):
self.value = value
self.enum_name = enum
super().__init__(f"Value {value} already used in enum {enum.get_name()}!")
class BprotoDuplicateBitfieldPositionError(BprotoFrontendError):
"""Error raise when building a bitfield and a position is used more than once.
This error should be use during the frontend compilation
Incomparision to BprotoDuplicateNameError, this error is used for bitfield positions, not the name.
Its specific to the bitfield component.
Inherited from BprotoFrontendError.
"""
def __init__(self, position: int, bitfield: AbstractProtocolComponent):
self.position = position
self.bitfield_name = bitfield
super().__init__(f"Position {position} already used in bitfield {bitfield.get_name()}!")
class BprotoEnumBitsizeTooLargeError(BprotoFrontendError):
"""Error raise when building an enumeration and the size of the enum exceeds the size of 8 bytes.
This error should be use during the frontend compilation state.
This is cause by the fact that bproto enums are just unsigned integer and the largest unsigned integer is 8 bytes long.
Inherited from BprotoFrontendError.
"""
def __init__(self, enum: AbstractProtocolComponent):
self.enum = enum
super().__init__(f"Enum {enum.get_name()} exceed size of 8 bytes (= uint64) with {enum.get_size_bytes()} bytes!")
class BprotoMessageIDAlreadyUsed(BprotoFrontendError):
"""Error raise when building a message and the message id is already used.
This error should be use during the frontend compilation state.
Inherited from BprotoFrontendError.
"""
def __init__(self, message_id: int, message: AbstractProtocolComponent):
self.message_id = message_id
self.message = message
super().__init__(f"Message id {message_id} already used in {message.get_type_name()} {message.get_name()}!")
class BprotoUnresolvedReferenceError(BprotoFrontendError):
"""Error raise when a reference to a protocol component could not be resolved.
This error should be use during the frontend compilation state.
Inherited from BprotoFrontendError.
"""
def __init__(self, reference: str, parent: AbstractProtocolComponent, grand_parent: AbstractProtocolComponent):
self.reference = reference
self.parent = parent
super().__init__(f"Reference {reference} in {parent.get_type_name()} {parent.get_name()} (in {grand_parent.get_type_name()} {grand_parent.get_name()}) could not be resolved!")

62
src/gen/bprotoV1.interp Normal file
View File

@@ -0,0 +1,62 @@
token literal names:
null
'protocol'
'version'
'message'
'{'
','
'}'
'['
']'
'enum'
'bits'
':'
'='
null
null
null
null
null
token symbolic names:
null
null
null
null
null
null
null
null
null
null
null
null
null
INT
IDENTIFIER
WS
COMMENT
COMMENT_MULTILINE
rule names:
protocol_defintion
protocol_header
message_def
message_id
enum_def
bit_field_def
field
field_pos
dtype
type_standard
type_enum
type_bitfield
array_extension
enum_body
enum_field
bitfield_body
bitfield_field
atn:
[4, 1, 17, 156, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 1, 0, 1, 0, 1, 0, 1, 0, 4, 0, 39, 8, 0, 11, 0, 12, 0, 40, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 5, 2, 55, 8, 2, 10, 2, 12, 2, 58, 9, 2, 1, 2, 1, 2, 3, 2, 62, 8, 2, 3, 2, 64, 8, 2, 1, 2, 1, 2, 1, 3, 1, 3, 1, 3, 1, 3, 1, 4, 1, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 8, 1, 8, 1, 8, 3, 8, 92, 8, 8, 1, 9, 1, 9, 3, 9, 96, 8, 9, 1, 10, 1, 10, 1, 10, 3, 10, 101, 8, 10, 1, 11, 1, 11, 1, 11, 3, 11, 106, 8, 11, 1, 12, 1, 12, 1, 12, 1, 12, 1, 13, 1, 13, 1, 13, 1, 13, 5, 13, 116, 8, 13, 10, 13, 12, 13, 119, 9, 13, 1, 13, 1, 13, 3, 13, 123, 8, 13, 3, 13, 125, 8, 13, 1, 13, 1, 13, 1, 14, 1, 14, 1, 14, 3, 14, 132, 8, 14, 1, 15, 1, 15, 1, 15, 1, 15, 5, 15, 138, 8, 15, 10, 15, 12, 15, 141, 9, 15, 1, 15, 1, 15, 3, 15, 145, 8, 15, 3, 15, 147, 8, 15, 1, 15, 1, 15, 1, 16, 1, 16, 1, 16, 3, 16, 154, 8, 16, 1, 16, 0, 0, 17, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 0, 0, 157, 0, 34, 1, 0, 0, 0, 2, 42, 1, 0, 0, 0, 4, 47, 1, 0, 0, 0, 6, 67, 1, 0, 0, 0, 8, 71, 1, 0, 0, 0, 10, 75, 1, 0, 0, 0, 12, 79, 1, 0, 0, 0, 14, 84, 1, 0, 0, 0, 16, 91, 1, 0, 0, 0, 18, 93, 1, 0, 0, 0, 20, 97, 1, 0, 0, 0, 22, 102, 1, 0, 0, 0, 24, 107, 1, 0, 0, 0, 26, 111, 1, 0, 0, 0, 28, 128, 1, 0, 0, 0, 30, 133, 1, 0, 0, 0, 32, 150, 1, 0, 0, 0, 34, 38, 3, 2, 1, 0, 35, 39, 3, 4, 2, 0, 36, 39, 3, 8, 4, 0, 37, 39, 3, 10, 5, 0, 38, 35, 1, 0, 0, 0, 38, 36, 1, 0, 0, 0, 38, 37, 1, 0, 0, 0, 39, 40, 1, 0, 0, 0, 40, 38, 1, 0, 0, 0, 40, 41, 1, 0, 0, 0, 41, 1, 1, 0, 0, 0, 42, 43, 5, 1, 0, 0, 43, 44, 5, 14, 0, 0, 44, 45, 5, 2, 0, 0, 45, 46, 5, 13, 0, 0, 46, 3, 1, 0, 0, 0, 47, 48, 5, 3, 0, 0, 48, 49, 3, 6, 3, 0, 49, 50, 5, 14, 0, 0, 50, 56, 5, 4, 0, 0, 51, 52, 3, 12, 6, 0, 52, 53, 5, 5, 0, 0, 53, 55, 1, 0, 0, 0, 54, 51, 1, 0, 0, 0, 55, 58, 1, 0, 0, 0, 56, 54, 1, 0, 0, 0, 56, 57, 1, 0, 0, 0, 57, 63, 1, 0, 0, 0, 58, 56, 1, 0, 0, 0, 59, 61, 3, 12, 6, 0, 60, 62, 5, 5, 0, 0, 61, 60, 1, 0, 0, 0, 61, 62, 1, 0, 0, 0, 62, 64, 1, 0, 0, 0, 63, 59, 1, 0, 0, 0, 63, 64, 1, 0, 0, 0, 64, 65, 1, 0, 0, 0, 65, 66, 5, 6, 0, 0, 66, 5, 1, 0, 0, 0, 67, 68, 5, 7, 0, 0, 68, 69, 5, 13, 0, 0, 69, 70, 5, 8, 0, 0, 70, 7, 1, 0, 0, 0, 71, 72, 5, 9, 0, 0, 72, 73, 5, 14, 0, 0, 73, 74, 3, 26, 13, 0, 74, 9, 1, 0, 0, 0, 75, 76, 5, 10, 0, 0, 76, 77, 5, 14, 0, 0, 77, 78, 3, 30, 15, 0, 78, 11, 1, 0, 0, 0, 79, 80, 3, 14, 7, 0, 80, 81, 5, 14, 0, 0, 81, 82, 5, 11, 0, 0, 82, 83, 3, 16, 8, 0, 83, 13, 1, 0, 0, 0, 84, 85, 5, 7, 0, 0, 85, 86, 5, 13, 0, 0, 86, 87, 5, 8, 0, 0, 87, 15, 1, 0, 0, 0, 88, 92, 3, 18, 9, 0, 89, 92, 3, 20, 10, 0, 90, 92, 3, 22, 11, 0, 91, 88, 1, 0, 0, 0, 91, 89, 1, 0, 0, 0, 91, 90, 1, 0, 0, 0, 92, 17, 1, 0, 0, 0, 93, 95, 5, 14, 0, 0, 94, 96, 3, 24, 12, 0, 95, 94, 1, 0, 0, 0, 95, 96, 1, 0, 0, 0, 96, 19, 1, 0, 0, 0, 97, 100, 5, 9, 0, 0, 98, 101, 3, 26, 13, 0, 99, 101, 5, 14, 0, 0, 100, 98, 1, 0, 0, 0, 100, 99, 1, 0, 0, 0, 101, 21, 1, 0, 0, 0, 102, 105, 5, 10, 0, 0, 103, 106, 3, 30, 15, 0, 104, 106, 5, 14, 0, 0, 105, 103, 1, 0, 0, 0, 105, 104, 1, 0, 0, 0, 106, 23, 1, 0, 0, 0, 107, 108, 5, 7, 0, 0, 108, 109, 5, 13, 0, 0, 109, 110, 5, 8, 0, 0, 110, 25, 1, 0, 0, 0, 111, 117, 5, 4, 0, 0, 112, 113, 3, 28, 14, 0, 113, 114, 5, 5, 0, 0, 114, 116, 1, 0, 0, 0, 115, 112, 1, 0, 0, 0, 116, 119, 1, 0, 0, 0, 117, 115, 1, 0, 0, 0, 117, 118, 1, 0, 0, 0, 118, 124, 1, 0, 0, 0, 119, 117, 1, 0, 0, 0, 120, 122, 3, 28, 14, 0, 121, 123, 5, 5, 0, 0, 122, 121, 1, 0, 0, 0, 122, 123, 1, 0, 0, 0, 123, 125, 1, 0, 0, 0, 124, 120, 1, 0, 0, 0, 124, 125, 1, 0, 0, 0, 125, 126, 1, 0, 0, 0, 126, 127, 5, 6, 0, 0, 127, 27, 1, 0, 0, 0, 128, 131, 5, 14, 0, 0, 129, 130, 5, 12, 0, 0, 130, 132, 5, 13, 0, 0, 131, 129, 1, 0, 0, 0, 131, 132, 1, 0, 0, 0, 132, 29, 1, 0, 0, 0, 133, 139, 5, 4, 0, 0, 134, 135, 3, 32, 16, 0, 135, 136, 5, 5, 0, 0, 136, 138, 1, 0, 0, 0, 137, 134, 1, 0, 0, 0, 138, 141, 1, 0, 0, 0, 139, 137, 1, 0, 0, 0, 139, 140, 1, 0, 0, 0, 140, 146, 1, 0, 0, 0, 141, 139, 1, 0, 0, 0, 142, 144, 3, 32, 16, 0, 143, 145, 5, 5, 0, 0, 144, 143, 1, 0, 0, 0, 144, 145, 1, 0, 0, 0, 145, 147, 1, 0, 0, 0, 146, 142, 1, 0, 0, 0, 146, 147, 1, 0, 0, 0, 147, 148, 1, 0, 0, 0, 148, 149, 5, 6, 0, 0, 149, 31, 1, 0, 0, 0, 150, 153, 5, 14, 0, 0, 151, 152, 5, 11, 0, 0, 152, 154, 5, 13, 0, 0, 153, 151, 1, 0, 0, 0, 153, 154, 1, 0, 0, 0, 154, 33, 1, 0, 0, 0, 17, 38, 40, 56, 61, 63, 91, 95, 100, 105, 117, 122, 124, 131, 139, 144, 146, 153]

29
src/gen/bprotoV1.tokens Normal file
View File

@@ -0,0 +1,29 @@
T__0=1
T__1=2
T__2=3
T__3=4
T__4=5
T__5=6
T__6=7
T__7=8
T__8=9
T__9=10
T__10=11
T__11=12
INT=13
IDENTIFIER=14
WS=15
COMMENT=16
COMMENT_MULTILINE=17
'protocol'=1
'version'=2
'message'=3
'{'=4
','=5
'}'=6
'['=7
']'=8
'enum'=9
'bits'=10
':'=11
'='=12

View File

@@ -0,0 +1,68 @@
token literal names:
null
'protocol'
'version'
'message'
'{'
','
'}'
'['
']'
'enum'
'bits'
':'
'='
null
null
null
null
null
token symbolic names:
null
null
null
null
null
null
null
null
null
null
null
null
null
INT
IDENTIFIER
WS
COMMENT
COMMENT_MULTILINE
rule names:
T__0
T__1
T__2
T__3
T__4
T__5
T__6
T__7
T__8
T__9
T__10
T__11
INT
IDENTIFIER
WS
COMMENT
COMMENT_MULTILINE
channel names:
DEFAULT_TOKEN_CHANNEL
HIDDEN
mode names:
DEFAULT_MODE
atn:
[4, 0, 17, 126, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 3, 1, 3, 1, 4, 1, 4, 1, 5, 1, 5, 1, 6, 1, 6, 1, 7, 1, 7, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 11, 1, 11, 1, 12, 4, 12, 86, 8, 12, 11, 12, 12, 12, 87, 1, 13, 4, 13, 91, 8, 13, 11, 13, 12, 13, 92, 1, 14, 4, 14, 96, 8, 14, 11, 14, 12, 14, 97, 1, 14, 1, 14, 1, 15, 1, 15, 1, 15, 1, 15, 5, 15, 106, 8, 15, 10, 15, 12, 15, 109, 9, 15, 1, 15, 1, 15, 1, 16, 1, 16, 1, 16, 1, 16, 5, 16, 117, 8, 16, 10, 16, 12, 16, 120, 9, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 118, 0, 17, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, 9, 19, 10, 21, 11, 23, 12, 25, 13, 27, 14, 29, 15, 31, 16, 33, 17, 1, 0, 4, 1, 0, 48, 57, 4, 0, 48, 57, 65, 90, 95, 95, 97, 122, 3, 0, 9, 10, 13, 13, 32, 32, 2, 0, 10, 10, 13, 13, 130, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0, 0, 0, 0, 25, 1, 0, 0, 0, 0, 27, 1, 0, 0, 0, 0, 29, 1, 0, 0, 0, 0, 31, 1, 0, 0, 0, 0, 33, 1, 0, 0, 0, 1, 35, 1, 0, 0, 0, 3, 44, 1, 0, 0, 0, 5, 52, 1, 0, 0, 0, 7, 60, 1, 0, 0, 0, 9, 62, 1, 0, 0, 0, 11, 64, 1, 0, 0, 0, 13, 66, 1, 0, 0, 0, 15, 68, 1, 0, 0, 0, 17, 70, 1, 0, 0, 0, 19, 75, 1, 0, 0, 0, 21, 80, 1, 0, 0, 0, 23, 82, 1, 0, 0, 0, 25, 85, 1, 0, 0, 0, 27, 90, 1, 0, 0, 0, 29, 95, 1, 0, 0, 0, 31, 101, 1, 0, 0, 0, 33, 112, 1, 0, 0, 0, 35, 36, 5, 112, 0, 0, 36, 37, 5, 114, 0, 0, 37, 38, 5, 111, 0, 0, 38, 39, 5, 116, 0, 0, 39, 40, 5, 111, 0, 0, 40, 41, 5, 99, 0, 0, 41, 42, 5, 111, 0, 0, 42, 43, 5, 108, 0, 0, 43, 2, 1, 0, 0, 0, 44, 45, 5, 118, 0, 0, 45, 46, 5, 101, 0, 0, 46, 47, 5, 114, 0, 0, 47, 48, 5, 115, 0, 0, 48, 49, 5, 105, 0, 0, 49, 50, 5, 111, 0, 0, 50, 51, 5, 110, 0, 0, 51, 4, 1, 0, 0, 0, 52, 53, 5, 109, 0, 0, 53, 54, 5, 101, 0, 0, 54, 55, 5, 115, 0, 0, 55, 56, 5, 115, 0, 0, 56, 57, 5, 97, 0, 0, 57, 58, 5, 103, 0, 0, 58, 59, 5, 101, 0, 0, 59, 6, 1, 0, 0, 0, 60, 61, 5, 123, 0, 0, 61, 8, 1, 0, 0, 0, 62, 63, 5, 44, 0, 0, 63, 10, 1, 0, 0, 0, 64, 65, 5, 125, 0, 0, 65, 12, 1, 0, 0, 0, 66, 67, 5, 91, 0, 0, 67, 14, 1, 0, 0, 0, 68, 69, 5, 93, 0, 0, 69, 16, 1, 0, 0, 0, 70, 71, 5, 101, 0, 0, 71, 72, 5, 110, 0, 0, 72, 73, 5, 117, 0, 0, 73, 74, 5, 109, 0, 0, 74, 18, 1, 0, 0, 0, 75, 76, 5, 98, 0, 0, 76, 77, 5, 105, 0, 0, 77, 78, 5, 116, 0, 0, 78, 79, 5, 115, 0, 0, 79, 20, 1, 0, 0, 0, 80, 81, 5, 58, 0, 0, 81, 22, 1, 0, 0, 0, 82, 83, 5, 61, 0, 0, 83, 24, 1, 0, 0, 0, 84, 86, 7, 0, 0, 0, 85, 84, 1, 0, 0, 0, 86, 87, 1, 0, 0, 0, 87, 85, 1, 0, 0, 0, 87, 88, 1, 0, 0, 0, 88, 26, 1, 0, 0, 0, 89, 91, 7, 1, 0, 0, 90, 89, 1, 0, 0, 0, 91, 92, 1, 0, 0, 0, 92, 90, 1, 0, 0, 0, 92, 93, 1, 0, 0, 0, 93, 28, 1, 0, 0, 0, 94, 96, 7, 2, 0, 0, 95, 94, 1, 0, 0, 0, 96, 97, 1, 0, 0, 0, 97, 95, 1, 0, 0, 0, 97, 98, 1, 0, 0, 0, 98, 99, 1, 0, 0, 0, 99, 100, 6, 14, 0, 0, 100, 30, 1, 0, 0, 0, 101, 102, 5, 47, 0, 0, 102, 103, 5, 47, 0, 0, 103, 107, 1, 0, 0, 0, 104, 106, 8, 3, 0, 0, 105, 104, 1, 0, 0, 0, 106, 109, 1, 0, 0, 0, 107, 105, 1, 0, 0, 0, 107, 108, 1, 0, 0, 0, 108, 110, 1, 0, 0, 0, 109, 107, 1, 0, 0, 0, 110, 111, 6, 15, 0, 0, 111, 32, 1, 0, 0, 0, 112, 113, 5, 47, 0, 0, 113, 114, 5, 42, 0, 0, 114, 118, 1, 0, 0, 0, 115, 117, 9, 0, 0, 0, 116, 115, 1, 0, 0, 0, 117, 120, 1, 0, 0, 0, 118, 119, 1, 0, 0, 0, 118, 116, 1, 0, 0, 0, 119, 121, 1, 0, 0, 0, 120, 118, 1, 0, 0, 0, 121, 122, 5, 42, 0, 0, 122, 123, 5, 47, 0, 0, 123, 124, 1, 0, 0, 0, 124, 125, 6, 16, 0, 0, 125, 34, 1, 0, 0, 0, 6, 0, 87, 92, 97, 107, 118, 1, 6, 0, 0]

106
src/gen/bprotoV1Lexer.py Normal file
View File

@@ -0,0 +1,106 @@
# Generated from bprotoV1.g4 by ANTLR 4.13.2
from antlr4 import *
from io import StringIO
import sys
if sys.version_info[1] > 5:
from typing import TextIO
else:
from typing.io import TextIO
def serializedATN():
return [
4,0,17,126,6,-1,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,
2,6,7,6,2,7,7,7,2,8,7,8,2,9,7,9,2,10,7,10,2,11,7,11,2,12,7,12,2,
13,7,13,2,14,7,14,2,15,7,15,2,16,7,16,1,0,1,0,1,0,1,0,1,0,1,0,1,
0,1,0,1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,2,1,2,1,2,1,2,1,
2,1,2,1,2,1,3,1,3,1,4,1,4,1,5,1,5,1,6,1,6,1,7,1,7,1,8,1,8,1,8,1,
8,1,8,1,9,1,9,1,9,1,9,1,9,1,10,1,10,1,11,1,11,1,12,4,12,86,8,12,
11,12,12,12,87,1,13,4,13,91,8,13,11,13,12,13,92,1,14,4,14,96,8,14,
11,14,12,14,97,1,14,1,14,1,15,1,15,1,15,1,15,5,15,106,8,15,10,15,
12,15,109,9,15,1,15,1,15,1,16,1,16,1,16,1,16,5,16,117,8,16,10,16,
12,16,120,9,16,1,16,1,16,1,16,1,16,1,16,1,118,0,17,1,1,3,2,5,3,7,
4,9,5,11,6,13,7,15,8,17,9,19,10,21,11,23,12,25,13,27,14,29,15,31,
16,33,17,1,0,4,1,0,48,57,4,0,48,57,65,90,95,95,97,122,3,0,9,10,13,
13,32,32,2,0,10,10,13,13,130,0,1,1,0,0,0,0,3,1,0,0,0,0,5,1,0,0,0,
0,7,1,0,0,0,0,9,1,0,0,0,0,11,1,0,0,0,0,13,1,0,0,0,0,15,1,0,0,0,0,
17,1,0,0,0,0,19,1,0,0,0,0,21,1,0,0,0,0,23,1,0,0,0,0,25,1,0,0,0,0,
27,1,0,0,0,0,29,1,0,0,0,0,31,1,0,0,0,0,33,1,0,0,0,1,35,1,0,0,0,3,
44,1,0,0,0,5,52,1,0,0,0,7,60,1,0,0,0,9,62,1,0,0,0,11,64,1,0,0,0,
13,66,1,0,0,0,15,68,1,0,0,0,17,70,1,0,0,0,19,75,1,0,0,0,21,80,1,
0,0,0,23,82,1,0,0,0,25,85,1,0,0,0,27,90,1,0,0,0,29,95,1,0,0,0,31,
101,1,0,0,0,33,112,1,0,0,0,35,36,5,112,0,0,36,37,5,114,0,0,37,38,
5,111,0,0,38,39,5,116,0,0,39,40,5,111,0,0,40,41,5,99,0,0,41,42,5,
111,0,0,42,43,5,108,0,0,43,2,1,0,0,0,44,45,5,118,0,0,45,46,5,101,
0,0,46,47,5,114,0,0,47,48,5,115,0,0,48,49,5,105,0,0,49,50,5,111,
0,0,50,51,5,110,0,0,51,4,1,0,0,0,52,53,5,109,0,0,53,54,5,101,0,0,
54,55,5,115,0,0,55,56,5,115,0,0,56,57,5,97,0,0,57,58,5,103,0,0,58,
59,5,101,0,0,59,6,1,0,0,0,60,61,5,123,0,0,61,8,1,0,0,0,62,63,5,44,
0,0,63,10,1,0,0,0,64,65,5,125,0,0,65,12,1,0,0,0,66,67,5,91,0,0,67,
14,1,0,0,0,68,69,5,93,0,0,69,16,1,0,0,0,70,71,5,101,0,0,71,72,5,
110,0,0,72,73,5,117,0,0,73,74,5,109,0,0,74,18,1,0,0,0,75,76,5,98,
0,0,76,77,5,105,0,0,77,78,5,116,0,0,78,79,5,115,0,0,79,20,1,0,0,
0,80,81,5,58,0,0,81,22,1,0,0,0,82,83,5,61,0,0,83,24,1,0,0,0,84,86,
7,0,0,0,85,84,1,0,0,0,86,87,1,0,0,0,87,85,1,0,0,0,87,88,1,0,0,0,
88,26,1,0,0,0,89,91,7,1,0,0,90,89,1,0,0,0,91,92,1,0,0,0,92,90,1,
0,0,0,92,93,1,0,0,0,93,28,1,0,0,0,94,96,7,2,0,0,95,94,1,0,0,0,96,
97,1,0,0,0,97,95,1,0,0,0,97,98,1,0,0,0,98,99,1,0,0,0,99,100,6,14,
0,0,100,30,1,0,0,0,101,102,5,47,0,0,102,103,5,47,0,0,103,107,1,0,
0,0,104,106,8,3,0,0,105,104,1,0,0,0,106,109,1,0,0,0,107,105,1,0,
0,0,107,108,1,0,0,0,108,110,1,0,0,0,109,107,1,0,0,0,110,111,6,15,
0,0,111,32,1,0,0,0,112,113,5,47,0,0,113,114,5,42,0,0,114,118,1,0,
0,0,115,117,9,0,0,0,116,115,1,0,0,0,117,120,1,0,0,0,118,119,1,0,
0,0,118,116,1,0,0,0,119,121,1,0,0,0,120,118,1,0,0,0,121,122,5,42,
0,0,122,123,5,47,0,0,123,124,1,0,0,0,124,125,6,16,0,0,125,34,1,0,
0,0,6,0,87,92,97,107,118,1,6,0,0
]
class bprotoV1Lexer(Lexer):
atn = ATNDeserializer().deserialize(serializedATN())
decisionsToDFA = [ DFA(ds, i) for i, ds in enumerate(atn.decisionToState) ]
T__0 = 1
T__1 = 2
T__2 = 3
T__3 = 4
T__4 = 5
T__5 = 6
T__6 = 7
T__7 = 8
T__8 = 9
T__9 = 10
T__10 = 11
T__11 = 12
INT = 13
IDENTIFIER = 14
WS = 15
COMMENT = 16
COMMENT_MULTILINE = 17
channelNames = [ u"DEFAULT_TOKEN_CHANNEL", u"HIDDEN" ]
modeNames = [ "DEFAULT_MODE" ]
literalNames = [ "<INVALID>",
"'protocol'", "'version'", "'message'", "'{'", "','", "'}'",
"'['", "']'", "'enum'", "'bits'", "':'", "'='" ]
symbolicNames = [ "<INVALID>",
"INT", "IDENTIFIER", "WS", "COMMENT", "COMMENT_MULTILINE" ]
ruleNames = [ "T__0", "T__1", "T__2", "T__3", "T__4", "T__5", "T__6",
"T__7", "T__8", "T__9", "T__10", "T__11", "INT", "IDENTIFIER",
"WS", "COMMENT", "COMMENT_MULTILINE" ]
grammarFileName = "bprotoV1.g4"
def __init__(self, input=None, output:TextIO = sys.stdout):
super().__init__(input, output)
self.checkVersion("4.13.2")
self._interp = LexerATNSimulator(self, self.atn, self.decisionsToDFA, PredictionContextCache())
self._actions = None
self._predicates = None

View File

@@ -0,0 +1,29 @@
T__0=1
T__1=2
T__2=3
T__3=4
T__4=5
T__5=6
T__6=7
T__7=8
T__8=9
T__9=10
T__10=11
T__11=12
INT=13
IDENTIFIER=14
WS=15
COMMENT=16
COMMENT_MULTILINE=17
'protocol'=1
'version'=2
'message'=3
'{'=4
','=5
'}'=6
'['=7
']'=8
'enum'=9
'bits'=10
':'=11
'='=12

165
src/gen/bprotoV1Listener.py Normal file
View File

@@ -0,0 +1,165 @@
# Generated from bprotoV1.g4 by ANTLR 4.13.2
from antlr4 import *
if "." in __name__:
from .bprotoV1Parser import bprotoV1Parser
else:
from bprotoV1Parser import bprotoV1Parser
# This class defines a complete listener for a parse tree produced by bprotoV1Parser.
class bprotoV1Listener(ParseTreeListener):
# Enter a parse tree produced by bprotoV1Parser#protocol_defintion.
def enterProtocol_defintion(self, ctx:bprotoV1Parser.Protocol_defintionContext):
pass
# Exit a parse tree produced by bprotoV1Parser#protocol_defintion.
def exitProtocol_defintion(self, ctx:bprotoV1Parser.Protocol_defintionContext):
pass
# Enter a parse tree produced by bprotoV1Parser#protocol_header.
def enterProtocol_header(self, ctx:bprotoV1Parser.Protocol_headerContext):
pass
# Exit a parse tree produced by bprotoV1Parser#protocol_header.
def exitProtocol_header(self, ctx:bprotoV1Parser.Protocol_headerContext):
pass
# Enter a parse tree produced by bprotoV1Parser#message_def.
def enterMessage_def(self, ctx:bprotoV1Parser.Message_defContext):
pass
# Exit a parse tree produced by bprotoV1Parser#message_def.
def exitMessage_def(self, ctx:bprotoV1Parser.Message_defContext):
pass
# Enter a parse tree produced by bprotoV1Parser#message_id.
def enterMessage_id(self, ctx:bprotoV1Parser.Message_idContext):
pass
# Exit a parse tree produced by bprotoV1Parser#message_id.
def exitMessage_id(self, ctx:bprotoV1Parser.Message_idContext):
pass
# Enter a parse tree produced by bprotoV1Parser#enum_def.
def enterEnum_def(self, ctx:bprotoV1Parser.Enum_defContext):
pass
# Exit a parse tree produced by bprotoV1Parser#enum_def.
def exitEnum_def(self, ctx:bprotoV1Parser.Enum_defContext):
pass
# Enter a parse tree produced by bprotoV1Parser#bit_field_def.
def enterBit_field_def(self, ctx:bprotoV1Parser.Bit_field_defContext):
pass
# Exit a parse tree produced by bprotoV1Parser#bit_field_def.
def exitBit_field_def(self, ctx:bprotoV1Parser.Bit_field_defContext):
pass
# Enter a parse tree produced by bprotoV1Parser#field.
def enterField(self, ctx:bprotoV1Parser.FieldContext):
pass
# Exit a parse tree produced by bprotoV1Parser#field.
def exitField(self, ctx:bprotoV1Parser.FieldContext):
pass
# Enter a parse tree produced by bprotoV1Parser#field_pos.
def enterField_pos(self, ctx:bprotoV1Parser.Field_posContext):
pass
# Exit a parse tree produced by bprotoV1Parser#field_pos.
def exitField_pos(self, ctx:bprotoV1Parser.Field_posContext):
pass
# Enter a parse tree produced by bprotoV1Parser#dtype.
def enterDtype(self, ctx:bprotoV1Parser.DtypeContext):
pass
# Exit a parse tree produced by bprotoV1Parser#dtype.
def exitDtype(self, ctx:bprotoV1Parser.DtypeContext):
pass
# Enter a parse tree produced by bprotoV1Parser#type_standard.
def enterType_standard(self, ctx:bprotoV1Parser.Type_standardContext):
pass
# Exit a parse tree produced by bprotoV1Parser#type_standard.
def exitType_standard(self, ctx:bprotoV1Parser.Type_standardContext):
pass
# Enter a parse tree produced by bprotoV1Parser#type_enum.
def enterType_enum(self, ctx:bprotoV1Parser.Type_enumContext):
pass
# Exit a parse tree produced by bprotoV1Parser#type_enum.
def exitType_enum(self, ctx:bprotoV1Parser.Type_enumContext):
pass
# Enter a parse tree produced by bprotoV1Parser#type_bitfield.
def enterType_bitfield(self, ctx:bprotoV1Parser.Type_bitfieldContext):
pass
# Exit a parse tree produced by bprotoV1Parser#type_bitfield.
def exitType_bitfield(self, ctx:bprotoV1Parser.Type_bitfieldContext):
pass
# Enter a parse tree produced by bprotoV1Parser#array_extension.
def enterArray_extension(self, ctx:bprotoV1Parser.Array_extensionContext):
pass
# Exit a parse tree produced by bprotoV1Parser#array_extension.
def exitArray_extension(self, ctx:bprotoV1Parser.Array_extensionContext):
pass
# Enter a parse tree produced by bprotoV1Parser#enum_body.
def enterEnum_body(self, ctx:bprotoV1Parser.Enum_bodyContext):
pass
# Exit a parse tree produced by bprotoV1Parser#enum_body.
def exitEnum_body(self, ctx:bprotoV1Parser.Enum_bodyContext):
pass
# Enter a parse tree produced by bprotoV1Parser#enum_field.
def enterEnum_field(self, ctx:bprotoV1Parser.Enum_fieldContext):
pass
# Exit a parse tree produced by bprotoV1Parser#enum_field.
def exitEnum_field(self, ctx:bprotoV1Parser.Enum_fieldContext):
pass
# Enter a parse tree produced by bprotoV1Parser#bitfield_body.
def enterBitfield_body(self, ctx:bprotoV1Parser.Bitfield_bodyContext):
pass
# Exit a parse tree produced by bprotoV1Parser#bitfield_body.
def exitBitfield_body(self, ctx:bprotoV1Parser.Bitfield_bodyContext):
pass
# Enter a parse tree produced by bprotoV1Parser#bitfield_field.
def enterBitfield_field(self, ctx:bprotoV1Parser.Bitfield_fieldContext):
pass
# Exit a parse tree produced by bprotoV1Parser#bitfield_field.
def exitBitfield_field(self, ctx:bprotoV1Parser.Bitfield_fieldContext):
pass
del bprotoV1Parser

1266
src/gen/bprotoV1Parser.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,98 @@
# Generated from bprotoV1.g4 by ANTLR 4.13.2
from antlr4 import *
if "." in __name__:
from .bprotoV1Parser import bprotoV1Parser
else:
from bprotoV1Parser import bprotoV1Parser
# This class defines a complete generic visitor for a parse tree produced by bprotoV1Parser.
class bprotoV1Visitor(ParseTreeVisitor):
# Visit a parse tree produced by bprotoV1Parser#protocol_defintion.
def visitProtocol_defintion(self, ctx:bprotoV1Parser.Protocol_defintionContext):
return self.visitChildren(ctx)
# Visit a parse tree produced by bprotoV1Parser#protocol_header.
def visitProtocol_header(self, ctx:bprotoV1Parser.Protocol_headerContext):
return self.visitChildren(ctx)
# Visit a parse tree produced by bprotoV1Parser#message_def.
def visitMessage_def(self, ctx:bprotoV1Parser.Message_defContext):
return self.visitChildren(ctx)
# Visit a parse tree produced by bprotoV1Parser#message_id.
def visitMessage_id(self, ctx:bprotoV1Parser.Message_idContext):
return self.visitChildren(ctx)
# Visit a parse tree produced by bprotoV1Parser#enum_def.
def visitEnum_def(self, ctx:bprotoV1Parser.Enum_defContext):
return self.visitChildren(ctx)
# Visit a parse tree produced by bprotoV1Parser#bit_field_def.
def visitBit_field_def(self, ctx:bprotoV1Parser.Bit_field_defContext):
return self.visitChildren(ctx)
# Visit a parse tree produced by bprotoV1Parser#field.
def visitField(self, ctx:bprotoV1Parser.FieldContext):
return self.visitChildren(ctx)
# Visit a parse tree produced by bprotoV1Parser#field_pos.
def visitField_pos(self, ctx:bprotoV1Parser.Field_posContext):
return self.visitChildren(ctx)
# Visit a parse tree produced by bprotoV1Parser#dtype.
def visitDtype(self, ctx:bprotoV1Parser.DtypeContext):
return self.visitChildren(ctx)
# Visit a parse tree produced by bprotoV1Parser#type_standard.
def visitType_standard(self, ctx:bprotoV1Parser.Type_standardContext):
return self.visitChildren(ctx)
# Visit a parse tree produced by bprotoV1Parser#type_enum.
def visitType_enum(self, ctx:bprotoV1Parser.Type_enumContext):
return self.visitChildren(ctx)
# Visit a parse tree produced by bprotoV1Parser#type_bitfield.
def visitType_bitfield(self, ctx:bprotoV1Parser.Type_bitfieldContext):
return self.visitChildren(ctx)
# Visit a parse tree produced by bprotoV1Parser#array_extension.
def visitArray_extension(self, ctx:bprotoV1Parser.Array_extensionContext):
return self.visitChildren(ctx)
# Visit a parse tree produced by bprotoV1Parser#enum_body.
def visitEnum_body(self, ctx:bprotoV1Parser.Enum_bodyContext):
return self.visitChildren(ctx)
# Visit a parse tree produced by bprotoV1Parser#enum_field.
def visitEnum_field(self, ctx:bprotoV1Parser.Enum_fieldContext):
return self.visitChildren(ctx)
# Visit a parse tree produced by bprotoV1Parser#bitfield_body.
def visitBitfield_body(self, ctx:bprotoV1Parser.Bitfield_bodyContext):
return self.visitChildren(ctx)
# Visit a parse tree produced by bprotoV1Parser#bitfield_field.
def visitBitfield_field(self, ctx:bprotoV1Parser.Bitfield_fieldContext):
return self.visitChildren(ctx)
del bprotoV1Parser

62
src/main.py Normal file
View File

@@ -0,0 +1,62 @@
import argparse
from backend.rendering.pythonBackend import PythonBackendRenderer
from backend.rendering.cBackend import CBackendRenderer
from backend.rendering.txtBackend import TxtBackendRenderer
from compiler import BprotoCompiler
INPUT_FILE = "examples/test_protocol.bproto"
def main():
cli_args_parser = argparse.ArgumentParser(description="Bproto Compiler")
cli_args_parser.add_argument(
"-i", "--input",
help="Input file to compile",
required=True
)
cli_args_parser.add_argument(
"-o", "--output",
help="Output file to write compiled code to",
required=True
)
cli_args_parser.add_argument(
"-t", "--target",
help="Target language to compile to",
required=True,
choices=["python", "c", "txt"],
action="append"
)
args = cli_args_parser.parse_args()
with open(args.input, "r") as f:
source_text = f.read()
# Backend configuration
backends = []
if "python" in args.target:
print("Compiling to Python")
backends.append(PythonBackendRenderer("python"))
if "c" in args.target:
print("Compiling to C")
backends.append(CBackendRenderer("c"))
if "txt" in args.target:
print("Compiling to Txt")
backends.append(TxtBackendRenderer("txt"))
compiler = BprotoCompiler(backends, "template")
output = compiler.compile(source_text, ".")
output.saveToDisk(args.output)
string_outputs = output.toString()
print("Output Files:", list(string_outputs.keys()))
print(string_outputs.get("txt/protocolSummary.txt"))
if __name__ == '__main__':
main()

169
src/nameHandling/base.py Normal file
View File

@@ -0,0 +1,169 @@
from typing import Literal
from abc import ABC, abstractmethod
import re
from copy import deepcopy
class ComponentName:
"""Class to represent a component name. A portable container for component names.
Should be used instead of strings to represent component like bitfields, enums, messages, etc.
Conversions to diffrent target styles are implemented separately via ComponentNameStyle classes.
"""
def __init__(self, name_parts: list = [], source_style: object | None = None, source_string: str = ""):
"""Initialize a ComponentName object.
Args:
name_parts (list, optional): list of lowercase name components as string . Defaults to [].
source_style (object | None, optional): ComponentNameStyle use to create this ComponentName. This is used for compare other strings agains. Defaults to None.
source_string (str, optional): String representation of the name. Defaults to "". This is used for clearification in error messages.
"""
self.name_parts: list[str] = name_parts
self.source_style = source_style
self.source_string = source_string
def __repr__(self):
return f"ComponentName({'-'.join(map(lambda s: s[0].upper() + s[1:].lower(), self.name_parts))})"
def __eq__(self, value: object | str) -> bool:
if isinstance(value, ComponentName):
return self.name_parts == value.name_parts
elif isinstance(value, str) and self.source_style is not None:
return self.source_style.fromStr(value) == self
else:
return False
def __add__(self, other: str):
if isinstance(other, type(self)):
return ComponentName(
self.name_parts + other.name_parts,
self.source_style
)
elif isinstance(other, str):
return ComponentName(
self.name_parts + self.source_style.fromStr(other).name_parts,
self.source_style
)
else:
raise TypeError(f"Cannot combinde {type(other)} with ComponentName")
def __hash__(self):
return hash(tuple(self.name_parts))
def __deepcopy__(self, memo={}):
if id(self) in memo:
return memo[id(self)]
res = ComponentName(
[],
self.source_style,
self.source_string
)
res.name_parts = deepcopy(self.name_parts, memo)
res.source_style = deepcopy(self.source_style, memo)
res.source_string = deepcopy(self.source_string, memo)
memo[id(self)] = res
memo[id(res)] = res
return res
class NameStyle(ABC):
"""Abstract class to represent a style of component names.
This converts ComponentNames to strings and vice versa.
"""
def __init__(self):
pass
@staticmethod
@abstractmethod
def toStr(component_name: ComponentName, context: str = "") -> str:
"""Given a ComponentName object, convert it to a string in the target style.
Args:
component_name (NameStyle): The ComponentName object to convert.
context (str): Additional context to use when converting the name.
Returns:
str: String representation of the ComponentName object in a target style.
"""
pass
@staticmethod
@abstractmethod
def fromStr(name: str, context: str = "") -> ComponentName:
"""Given a string in the target style, convert it to a ComponentName object.
Args:
name (str): The string to convert.
context (str): Additional context to use when converting the name.
Returns:
ComponentName: ComponentName object representing a name
"""
pass
class NameStyleBproto(NameStyle):
"""Converter class for bproto style component names.
Inherited from NameStyle.
"""
@staticmethod
def toStr(component_name: ComponentName, context: str = Literal["", "enum", "member"]) -> str:
if context == "enum":
return '_'.join(map(lambda s: s.upper(), component_name.name_parts))
elif context == "member":
res = ''.join(map(lambda s: s[0].upper() + s[1:].lower(), component_name.name_parts))
return res[0].lower() + res[1:]
else:
return ''.join(map(lambda s: s[0].upper() + s[1:].lower(), component_name.name_parts))
@staticmethod
def fromStr(bproto_style_name: str | ComponentName, context: Literal["", "enum", "protocol_name"] = "") -> ComponentName:
if isinstance(bproto_style_name, ComponentName):
bproto_style_name.source_style = NameStyleBproto
return bproto_style_name
first_split = re.split(r"-|_", bproto_style_name)
second_split: list[str] = []
if context == "enum" or context == "protocol_name":
second_split.extend(list(map(lambda s: s.lower(), first_split)))
else:
for part in first_split:
second_split.extend(
list(map(lambda s: s.lower(),
re.split(r'(?=[A-Z])', part)
))
)
while "" in second_split:
second_split.remove("")
return ComponentName(second_split, NameStyleBproto, bproto_style_name)
def to_cap_case(component_name: ComponentName) -> str:
return "".join([i[0].upper() + i[1:].lower() for i in component_name.name_parts])
def to_camel_case(component_name: ComponentName) -> str:
res = "".join([i[0].upper() + i[1:].lower() for i in component_name.name_parts])
return res[0].lower() + res[1:]
def to_snake_case(component_name: ComponentName) -> str:
return "_".join([i.lower() for i in component_name.name_parts])
def to_screaming_case(component_name: ComponentName) -> str:
return "_".join([i.upper() for i in component_name.name_parts])

View File

@@ -0,0 +1,53 @@
from abc import ABC, abstractmethod
from nameHandling.base import ComponentName, NameStyleBproto
from protocol_components.message import Message
class ComponentNameModifier(ABC):
"""A generic statefull 'maschine' that modifies (apply) a change to a ComponentName.
This is an abstract class
"""
@abstractmethod
def apply(self, name: ComponentName) -> ComponentName:
"""Apply the change to the ComponentName.
This changes the component name in place and returns it.
Args:
name (ComponentName): The ComponentName to modify.
Returns:
ComponentName: Modified ComponentName
"""
pass
class BrandApplier(ComponentNameModifier):
"""Applies a protocol brand name to a ComponentName.
Just prepends the brand name to the ComponentName.
Inherited from ComponentNameModifier.
"""
def __init__(self, brand_name: str | ComponentName):
"""Initialize the BrandApplier.
Args:
brand_name (str|ComponentName): Brand name to apply.
"""
if isinstance(brand_name, ComponentName):
self.brand_name: ComponentName = brand_name
elif isinstance(brand_name, str):
self.brand_name: ComponentName = NameStyleBproto.fromStr(brand_name)
else:
raise TypeError("Invalid Type for Brand Name")
def apply(self, obj: ComponentName | Message) -> ComponentName:
if isinstance(obj, ComponentName):
return self.brand_name + obj
elif isinstance(obj, Message):
obj.name = self.apply(obj.name)
return obj
else:
raise TypeError(f"Cannot apply naming to type {type(obj)}")

View File

@@ -0,0 +1,95 @@
from protocol_components.field import Field, FieldEnum, FieldBitfield, FieldEnumRef, FieldBitfieldRef, FactoryField
from protocol_components.dtypes import BprotoFieldBaseType
from protocol_components.message import Message
from nameHandling.brand_applier import BrandApplier
import protocol_components.protocol
from errors import BprotoUnresolvedReferenceError
class ProtocolReferenceResolver:
"""Resolves refences in fields in messages in protocol definitiown
This is a one-time use class, once the resolve_refrence_protocol is called, the object should not be used anymore
Usally used after the ProtocolDefinitions object is created by the FactoryProtocolDefition
"""
def __init__(self, protocol: protocol_components.protocol.ProtocolDefinitions):
"""Initializes the ProtocolReferenceResolver class
Args:
protocol (ProtocolDefinitions): Protocol to resolve refrences in, must not be fully assembled by the ProtocolFactory
"""
self.protocol = protocol
def resolve_refrence_protocol(self) -> protocol_components.protocol.ProtocolDefinitions:
"""Resolves refrences in the protocol definitiown
This modifies the protocol definitiown in place and returns it
Returns:
ProtocolDefinitions: Protocol definitiown with refrences resolved
"""
for name, message in self.protocol.messages.items():
self.protocol.messages[name] = self._resolve_refrence_fields_in_message(message)
return self.protocol
def _resolve_refrence_fields_in_message(self, message: Message) -> Message:
for name, field in message.fields.items():
message.fields[name] = self._resolve_refrence_fields(field, message)
return message
def _resolve_refrence_fields(self, field: Field, parent_message: Message) -> Field:
if isinstance(field, FieldBitfieldRef):
return self._resolve_refrence_bitfield(field, parent_message)
elif isinstance(field, FieldEnumRef):
return self._resolve_refrence_enum(field, parent_message)
else:
return field
def _resolve_refrence_bitfield(self, field: FieldBitfieldRef, parent_message: Message) -> FieldBitfield:
if field.ref not in self.protocol.bitfields:
raise BprotoUnresolvedReferenceError(field.ref, field, parent_message)
factory = FactoryField()
return factory.assemble(
field.name,
field.pos,
BprotoFieldBaseType.BITFIELD,
field.array_size,
self.protocol.bitfields[field.ref]
)
def _resolve_refrence_enum(self, field: FieldEnumRef, parent_message: Message) -> FieldEnum:
if field.ref not in self.protocol.enums:
raise BprotoUnresolvedReferenceError(field.ref, field, parent_message)
factory = FactoryField()
return factory.assemble(
field.name,
field.pos,
BprotoFieldBaseType.ENUM,
field.array_size,
self.protocol.enums[field.ref]
)
def back_resolve_inlines(self) -> protocol_components.protocol.ProtocolDefinitions:
inline_brand_applyer = BrandApplier("inline")
for msg_name, msg in self.protocol.messages.items():
for field_name, field in msg.fields.items():
if isinstance(field, FieldBitfield) and field.bitfield.inline:
field.bitfield.name = inline_brand_applyer.apply(
msg.name + field.name
)
self.protocol.bitfields.update({
field.bitfield.name: field.bitfield
})
elif isinstance(field, FieldEnum) and field.enum.inline:
field.enum.name = inline_brand_applyer.apply(
msg.name + field.name
)
self.protocol.enums.update({
field.enum.name: field.enum
})
return self.protocol

View File

@@ -0,0 +1,75 @@
from nameHandling.base import ComponentName, NameStyle, to_cap_case, to_camel_case, to_screaming_case
from protocol_components.protocol import ProtocolDefinitions
import re
from typing import Literal
class NameStyleC(NameStyle):
@staticmethod
def toStr(bproto_style_name: str | ComponentName, context:
Literal[
"", "enum_name", "enum_item",
"struct_name", "struct_member",
"bitfield_name", "bitfield_member",
] = "") -> str:
match context:
case "struct_name":
res = to_cap_case(bproto_style_name)
case "struct_member":
res = to_camel_case(bproto_style_name)
case "enum_name":
res = to_cap_case(bproto_style_name)
case "enum_item":
res = to_screaming_case(bproto_style_name)
case "bitfield_name":
res = to_cap_case(bproto_style_name)
case "bitfield_member":
res = to_camel_case(bproto_style_name)
case _:
res = to_camel_case(bproto_style_name)
res = re.sub(r"_+", "_", res)
return res
@staticmethod
def fromStr(bproto_style_name: str | ComponentName, context: str = "") -> ComponentName:
raise NotImplementedError("This mode is not avaialbe for NameStyleC")
@staticmethod
def preprocess(protocol: ProtocolDefinitions) -> ProtocolDefinitions:
# Enum Mod
protocol.enums = {
protocol.name + ComponentName(["_enum_"]) + v.name: v
for _, v in protocol.enums.items()
}
for enum_name, e in protocol.enums.items():
e.name = enum_name
e.values = {
enum_name + "value" + k: v
for k, v in e.values.items()
}
# Bitfield mod
protocol.bitfields = {
protocol.name + ComponentName(["_bitfield_"]) + v.name: v
for _, v in protocol.bitfields.items()
}
for name, b in protocol.bitfields.items():
b.name = name
# Message mod
protocol.messages = {
protocol.name + ComponentName(["_message_"]) + v.name: v
for _, v in protocol.messages.items()
}
for name, m in protocol.messages.items():
m.name = name
return protocol

View File

@@ -0,0 +1,57 @@
from typing import Literal
from nameHandling.base import ComponentName, NameStyle, to_cap_case, to_snake_case, to_screaming_case
from protocol_components.protocol import ProtocolDefinitions
class NameStylePython(NameStyle):
@staticmethod
def toStr(bproto_style_name: str | ComponentName, context:
Literal[
"", "enum_name", "enum_item",
"class_name", "class_member",
] = "") -> str:
match context:
case "class_name":
return to_cap_case(bproto_style_name)
case "class_member":
return to_snake_case(bproto_style_name)
case "enum_name":
return to_cap_case(bproto_style_name)
case "enum_item":
return to_screaming_case(bproto_style_name)
case _:
return to_snake_case(bproto_style_name)
@staticmethod
def fromStr(bproto_style_name: str | ComponentName, context: str = "") -> ComponentName:
raise NotImplementedError("This mode is not avaialbe for NameStylePython")
@staticmethod
def preprocess(protocol: ProtocolDefinitions) -> ProtocolDefinitions:
protocol.enums = {
protocol.name + "Enum" + v.name: v
for _, v in protocol.enums.items()
}
for name, e in protocol.enums.items():
e.name = name
protocol.bitfields = {
protocol.name + "Bitfield" + v.name: v
for _, v in protocol.bitfields.items()
}
for name, b in protocol.bitfields.items():
b.name = name
protocol.messages = {
protocol.name + "Message" + v.name: v
for _, v in protocol.messages.items()
}
for name, m in protocol.messages.items():
m.name = name
return protocol

157
src/parser/ast_visitor.py Normal file
View File

@@ -0,0 +1,157 @@
from protocol_components import AbstractProtocolComponent
from protocol_components.dtypes import BprotoFieldBaseType
from protocol_components.field import Field, FactoryField
from protocol_components.enumeration import FactoryEnumeration
from protocol_components.bitfields import FactoryBitfield
from protocol_components.message import Message, FactoryMessage
from protocol_components.protocol import ProtocolDefinitions
from protocol_components.protocolFactory import FactoryProtocolDefition
from gen.bprotoV1Visitor import bprotoV1Visitor as BprotoVisitor
from antlr4 import TerminalNode
class BprotoASTVisitor(BprotoVisitor):
# Defintion parsing
def visitProtocol_defintion(self, ctx) -> ProtocolDefinitions:
factory = FactoryProtocolDefition()
name, version = self.visit(ctx.protocol_header())
for message in ctx.message_def():
message_object = self.visit(message)
factory.add_message(message_object)
for enum in ctx.enum_def():
enum_object = self.visit(enum)
factory.add_enum(enum_object)
for bitfield in ctx.bit_field_def():
bitfield_object = self.visit(bitfield)
factory.add_bitfield(bitfield_object)
return factory.assemble(name, version)
def visitProtocol_header(self, ctx):
return ctx.IDENTIFIER().getText(), int(ctx.INT().getText())
def visitBit_field_def(self, ctx):
factory: FactoryBitfield = self.visit(ctx.bitfield_body())
name = ctx.IDENTIFIER().getText()
return factory.assemble(name)
def visitEnum_def(self, ctx):
factory: FactoryEnumeration = self.visit(ctx.enum_body())
name = ctx.IDENTIFIER().getText()
return factory.assemble(name)
# Message definition parsing
def visitMessage_def(self, ctx) -> Message:
identifier = ctx.IDENTIFIER().getText()
index_number = self.visit(ctx.message_id())
factory = FactoryMessage()
fields = ctx.field()
for field in fields:
factory.add_field(self.visit(field))
return factory.assemble(identifier, index_number)
def visitMessage_id(self, ctx) -> int:
return int(ctx.INT().getText())
def visitField(self, ctx) -> Field:
identfier = ctx.IDENTIFIER().getText()
pos = self.visit(ctx.field_pos())
field_type, array_size, refrence = self.visit(ctx.dtype())
factory = FactoryField()
return factory.assemble(
identfier,
pos,
field_type,
array_size,
refrence
)
def visitField_pos(self, ctx) -> int:
return int(ctx.INT().getText())
# Field Type parsing
def visitDtype(self, ctx) -> tuple[BprotoFieldBaseType, int, None | str | AbstractProtocolComponent]:
if ctx.type_enum():
return self.visit(ctx.type_enum())
elif ctx.type_bitfield():
return self.visit(ctx.type_bitfield())
elif ctx.type_standard():
return self.visit(ctx.type_standard())
def visitType_standard(self, ctx) -> tuple[BprotoFieldBaseType, int, None]:
return (
ctx.IDENTIFIER().getText(),
self.visit(ctx.array_extension())
if ctx.array_extension() else 1,
None,
)
def visitArray_extension(self, ctx):
return int(ctx.INT().getText())
def visitType_enum(self, ctx) -> tuple[BprotoFieldBaseType, int, AbstractProtocolComponent | str]:
if isinstance(ctx.getChild(1), TerminalNode):
ref_name = ctx.IDENTIFIER().getText()
return BprotoFieldBaseType.ENUM, 1, ref_name
else:
enum_factory: FactoryEnumeration = self.visit(ctx.enum_body())
return BprotoFieldBaseType.ENUM, 1, enum_factory.assemble("", inline=True)
def visitType_bitfield(self, ctx):
if isinstance(ctx.getChild(1), TerminalNode):
ref_name = ctx.IDENTIFIER().getText()
return BprotoFieldBaseType.BITFIELD, 1, ref_name
else:
factory: FactoryBitfield = self.visit(ctx.bitfield_body())
return BprotoFieldBaseType.BITFIELD, 1, factory.assemble("", inline=True)
# Enum parsing
def visitEnum_body(self, ctx) -> FactoryEnumeration:
enumeration_factory = FactoryEnumeration()
for field in ctx.enum_field():
name, value = self.visit(field)
enumeration_factory.add_value(name, value)
return enumeration_factory
def visitEnum_field(self, ctx):
name = ctx.IDENTIFIER().getText()
if ctx.getChildCount() > 1:
value = int(ctx.INT().getText())
else:
value = None
return name, value
# Bit Field parsing
def visitBitfield_body(self, ctx):
factory = FactoryBitfield()
for field in ctx.bitfield_field():
name, pos = self.visit(field)
factory.add_bit(name, pos)
return factory
def visitBitfield_field(self, ctx):
name = ctx.IDENTIFIER().getText()
if ctx.getChildCount() > 1:
position = int(ctx.INT().getText())
else:
position = None
return name, position

120
src/parser/parser.py Normal file
View File

@@ -0,0 +1,120 @@
from antlr4 import InputStream, CommonTokenStream, ParserRuleContext, DFA
from antlr4.error.ErrorListener import ErrorListener
from gen.bprotoV1Lexer import bprotoV1Lexer as BprotoLexer
from gen.bprotoV1Parser import bprotoV1Parser as BprotoParser
class bproto_ErrorListener(ErrorListener):
"""
Error listener for the ANTLR parser.
This handles/collects syntax errors and formats them for the user
"""
def __init__(self):
super().__init__()
self.syntax_error_list: list[tuple[int, int, str, Exception]] = []
self.previous_line = 5
def syntaxError(self, recognizer, offendingSymbol, line, column, msg, e):
self.syntax_error_list.append((line, column, msg, e))
def reportAmbiguity(self, recognizer, dfa: DFA, startIndex,
stopIndex, exact, ambigAlts, configs):
pass
def reportAttemptingFullContext(self, recognizer, dfa, startIndex,
stopIndex, conflictingAlts, configs):
pass
def reportContextSensitivity(self, recognizer, dfa, startIndex,
stopIndex, prediction, configs):
pass
@staticmethod
def make_code_preview(
source_text: list[str], line: int, column: int) -> str:
code_window = ""
for i in range(max(0, line - 5), line):
code_window += f"{i + 1:3d} | {source_text[i]}\n"
code_window += " | " + " " * (column) + "^\n"
return code_window
def get_syntax_error_message(self, source_text: str) -> str:
"""generates a humman readable error message from the syntax errors
Args:
source_text (str): The source text that was parsed
Returns:
str: The human readable error message (multi-line)
"""
source_text: list[str] = source_text.split("\n")
result_error_message = ""
for line, column, msg, e in self.syntax_error_list:
result_error_message += bproto_ErrorListener.make_code_preview(
source_text, line, column
)
result_error_message += f"line {line} column {column}: {msg}\n\n"
result_error_message += f"Found {len(self.syntax_error_list)} " \
+ "syntax errors"
return result_error_message
def create_ast_parser(protocol_defintion_text: str, error_message_listner: bproto_ErrorListener) -> BprotoParser:
"""Creates an ANTLR parser for the given protocol definition text
This function just creates and configures the parser, it does not parse the text
Args:
protocol_defintion_text (str): The protocol definition text from the source files
error_message_listner (bproto_ErrorListener): The error listener to collect syntax errors
Returns:
BprotoParser: The configured ANTLR parser, ready to walk the tree
"""
input_stream = InputStream(protocol_defintion_text)
antlr_lexer = BprotoLexer(input_stream)
antlr_lexer.removeErrorListeners()
antlr_lexer.addErrorListener(error_message_listner)
antlr_token_stream = CommonTokenStream(antlr_lexer)
antlr_parser = BprotoParser(antlr_token_stream)
antlr_parser.removeErrorListeners()
antlr_parser.addErrorListener(error_message_listner)
return antlr_parser
def parse_ast_string(
protocol_defintion_text: str
) -> ParserRuleContext | bproto_ErrorListener:
"""
Parse the protocol definition text into an AST.
If there are syntax errors, return an error listener with the syntax errors
Args:
protocol_defintion_text (str): The protocol definition text from the
source files
Returns:
ParserRuleContext | bproto_ErrorListener: The parsed AST if successful,
otherwise an error listener with syntax errors for reporting
"""
error_message_listner = bproto_ErrorListener()
antlr_parser = create_ast_parser(
protocol_defintion_text,
error_message_listner
)
ast = antlr_parser.protocol_defintion()
if antlr_parser.getNumberOfSyntaxErrors() > 0:
return error_message_listner
return ast

View File

@@ -0,0 +1,91 @@
from abc import ABC, abstractmethod
from collections import OrderedDict
from nameHandling.base import ComponentName
class AbstractProtocolComponent(ABC):
@abstractmethod
def get_identifier(self) -> int | ComponentName:
"""Get the identifier of the protocol component.
This identifier should be unique and is ment for sorting and indexing.
Returns:
int | str: Identifier of the protocol component
"""
pass
@abstractmethod
def get_name(self) -> ComponentName:
"""Get the display name of the protocol component.
This is ment to display error messages, render into code, etc.
Should be human readable and unique.
Returns:
str: Display name of the protocol component
"""
pass
@abstractmethod
def get_type_name(self) -> str:
"""Get the type name of the protocol component.
This is ment to display error messages.
Should be human readable and describe the type of the component.
Returns:
str: Type name of the protocol component
"""
pass
@abstractmethod
def get_size_bytes(self) -> int:
"""Get the size of the protocol component in bytes.
Should be the same as ceil(get_bitsize() / 8).
Returns:
int: Size of the protocol component in bytes
"""
pass
@abstractmethod
def get_size_bits(self) -> int:
"""Get the size of the protocol component in bits.
Should be the same as get_byte_size() * 8.
Returns:
int: Size of the protocol component in bits
"""
pass
def sort_dict_by_value(d: dict[str, int]) -> dict[str, int]:
"""Sort a dictionary by its values.
Values should be something sortable.
Args:
d (dict[str, int]): dictionary to be sorted
Returns:
dict[str,int]: Sorted dictionary
"""
return OrderedDict(sorted(d.items(), key=lambda x: x[1]))
def find_smallset_next_value(s: set[int], inital=0) -> int:
"""Find the next smallest value that is not in the set.
Used to fill in missing values in an enumeration or bitfield.
Args:
s (set[int]): Set of values
inital (int, optional): Start value to search from. Defaults to 0. For optimization purposes.
Returns:
int: Next smallest value not in the set
"""
i = inital
while i in s:
i += 1
return i

View File

@@ -0,0 +1,167 @@
from . import AbstractProtocolComponent, find_smallset_next_value, sort_dict_by_value
from errors import BprotoDuplicateNameError, BprotoDuplicateBitfieldPositionError
from nameHandling.base import ComponentName, NameStyleBproto
import math
from copy import deepcopy
class Bitfield(AbstractProtocolComponent):
"""Represents a bitfield in a protocol.
This should not be instantiated directly, but rather through the FactoryBitfield class.
Inherit from AbstractProtocolComponent.
"""
def __init__(self, name: ComponentName, inline: bool = False):
"""Initialize a Bitfield object; should not be used directly.
This methode should only be called by the FactoryBitfield class.
Args:
name (ComponentName): Name of the bitfield, should be unique
length (int): Length of the bitfield in bits.
inline: If True mark this bitfield as a inline definition
"""
self.name: ComponentName = name
self.length: int = 0
self.bits: dict[ComponentName, int] = {}
self.inline: bool = inline
# Inherited from AbstractProtocolComponent
def get_identifier(self):
return self.name
def get_name(self):
return self.name
def get_type_name(self):
return "bitfield"
def get_size_bits(self):
return self.length
def get_size_bytes(self):
return max(math.ceil(self.get_size_bits() / 8), 1)
# Methodes specific to this class
def apply_naming(self, naming: str):
"""Apply a naming scheme to the bitfield.
TODO: Is this method needed? REFACTOR!!!
"""
self.name = f"{naming}_{self.name}"
def __repr__(self):
field_names = ", ".join([f"{name}:{pos}" for name, pos in self.bits.items()])
return f"Bitfield({self.name}, {field_names})"
def __deepcopy__(self, memo={}):
if id(self) in memo:
return memo[id(self)]
new_bitfield = Bitfield(
deepcopy(self.name, memo),
deepcopy(self.inline, memo)
)
new_bitfield.length = deepcopy(self.length, memo)
new_bitfield.bits = {
deepcopy(k, memo): deepcopy(v, memo)
for k, v in self.bits.items()
}
memo[id(self)] = new_bitfield
memo[id(new_bitfield)] = new_bitfield
return new_bitfield
class FactoryBitfield:
"""Factory class to create Bitfield objects.
Used to assemble a Bitfield object from its parts.
This class should be used to create Bitfield objects.
Its a one-time use class, so create a new instance for each Bitfield object.
This Factory is ment to be used during traversal of the abstract syntax tree.
Simular to the FactoryEnumeration class.
"""
def __init__(self):
self.bits_list: list[tuple[str, int | None]] = []
def add_bit(self, name: str, position: int | None):
"""Add a bit to the bitfield.
Used to build the Bitfield object.
Duplication checks are not done here, but in the assemble
Args:
name (str): Name of the bit
position (int | None): Position of the bit in the bitfield. If None, the position will be filled in later.
"""
self.bits_list.append((NameStyleBproto.fromStr(name), position))
@staticmethod
def calculate_bitsize(bits: dict[int, str]):
"""Given a dictionary of bit names and their positions, calculate the bitsize of the bitfield.
Its just a wrapper around the max methode to be honest.
Args:
bits (dict[int, str]): Dictionary of bit names and their positions
Returns:
int: Number of bits needed to represent the bit
"""
return max(bits.values(), default=0) + 1
def assemble(self, name: str, inline: bool = False):
"""Finalize the Bitfield object and return it.
This method should be called after adding all bits to the bitfield.
This will:
1. Check for duplicate names and positions
2. Fill in missing positions
3. Calculate the bitsize of the bitfield
4. Sort the dictionary by value
(Very simular to the FactoryEnumeration.assemble method)
Args:
name (str): Name of the bitfield, should be unique
inline (bool): If true marks the resulting bitfield as inline
Raises:
BprotoDuplicateBitfieldPositionError: Thrown when a bitfield position is duplicated
BprotoDuplicateNameError: Thrown when a bitfield name is duplicated
Returns:
_type_: The finalized Bitfield object
"""
resulting_bitfield = Bitfield(NameStyleBproto.fromStr(name), inline=inline)
bits: dict[ComponentName, int | None] = {}
bit_names: set[ComponentName] = set()
bit_positions = set()
# Check for duplicates in name or value
for name, pos in self.bits_list:
if pos is not None and pos in bit_positions:
raise BprotoDuplicateBitfieldPositionError(pos, resulting_bitfield)
if name in bit_names:
raise BprotoDuplicateNameError(name, resulting_bitfield)
if pos is not None:
bit_positions.add(pos)
bits[name] = pos
bit_names.add(name)
# Fill in missing values
for name, pos in self.bits_list:
if pos is None:
pos = find_smallset_next_value(bit_positions)
bit_positions.add(pos)
bits[name] = pos
# Sort the dictionary by value
resulting_bitfield.length = FactoryBitfield.calculate_bitsize(bits)
resulting_bitfield.bits = sort_dict_by_value(bits)
return resulting_bitfield

View File

@@ -0,0 +1,22 @@
CRC_SIZE = 2
def crc16_calc_inital(data: bytearray) -> int:
"""CRC-16/CITT-FALSE
from https://stackoverflow.com/questions/35205702/calculating-crc16-in-python
Args:
data (bytearray): protocol hash
Returns:
bytes: initals for crc16_calc_with_initial
"""
crc = 0xFFFF
for i in range(0, len(data)):
crc ^= data[i] << 8
for j in range(0, 8):
if (crc & 0x8000) > 0:
crc = (crc << 1) ^ 0x1021
else:
crc = crc << 1
return crc & 0xFFFF

View File

@@ -0,0 +1,153 @@
import enum
from . import AbstractProtocolComponent
from errors import BprotoFrontendError
class BprotoFieldBaseType(enum.Enum):
BOOL = "bool"
CHAR = "char"
STRING = "string"
FLOAT32 = "float32"
FLOAT64 = "float64"
INT8 = "int8"
INT16 = "int16"
INT32 = "int32"
INT64 = "int64"
UINT8 = "uint8"
UINT16 = "uint16"
UINT32 = "uint32"
UINT64 = "uint64"
ENUM = "enum"
BITFIELD = "bitfield"
class ArrayPolicy(enum.Enum):
"""Enum respresenting the options for policy, regarding if an datatype is an array or not.
Options: DISALLOW, ALLOW, FORCE
"""
DISALLOW = 0
ALLOW = 1
FORCE = 2
DTYPE_SIZE_MAP = {
BprotoFieldBaseType.UINT8: 1,
BprotoFieldBaseType.UINT16: 2,
BprotoFieldBaseType.UINT32: 4,
BprotoFieldBaseType.UINT64: 8,
BprotoFieldBaseType.INT8: 1,
BprotoFieldBaseType.INT16: 2,
BprotoFieldBaseType.INT32: 4,
BprotoFieldBaseType.INT64: 8,
BprotoFieldBaseType.FLOAT32: 4,
BprotoFieldBaseType.FLOAT64: 8,
BprotoFieldBaseType.BOOL: 1,
BprotoFieldBaseType.CHAR: 1,
BprotoFieldBaseType.STRING: 1,
}
DTYPE_ARRAY_POLICY_MAP = {
BprotoFieldBaseType.UINT8: ArrayPolicy.ALLOW,
BprotoFieldBaseType.UINT16: ArrayPolicy.ALLOW,
BprotoFieldBaseType.UINT32: ArrayPolicy.ALLOW,
BprotoFieldBaseType.UINT64: ArrayPolicy.ALLOW,
BprotoFieldBaseType.INT8: ArrayPolicy.ALLOW,
BprotoFieldBaseType.INT16: ArrayPolicy.ALLOW,
BprotoFieldBaseType.INT32: ArrayPolicy.ALLOW,
BprotoFieldBaseType.INT64: ArrayPolicy.ALLOW,
BprotoFieldBaseType.FLOAT32: ArrayPolicy.ALLOW,
BprotoFieldBaseType.FLOAT64: ArrayPolicy.ALLOW,
BprotoFieldBaseType.BOOL: ArrayPolicy.ALLOW,
BprotoFieldBaseType.CHAR: ArrayPolicy.ALLOW,
BprotoFieldBaseType.STRING: ArrayPolicy.FORCE,
BprotoFieldBaseType.ENUM: ArrayPolicy.DISALLOW,
BprotoFieldBaseType.BITFIELD: ArrayPolicy.DISALLOW,
}
def validate_datatype(dtype: str, array_size: int, field: AbstractProtocolComponent) -> tuple[BprotoFieldBaseType, int]:
"""Given a string and array size, validate the datatype and array size.
Valid datatypes are defined in BprotoFieldBaseType enum.
Array size is validated based on the datatype and the array policy.
Args:
dtype (str): string representation of the datatype
array_size (int): size of the array
field (AbstractProtocolComponent): Datatypes parent field, for error reporting
Raises:
BprotoUnknownDataTypeError: Throws when the datatype is unknown
ValueError: Thrown when something went wrong, this should not happen, like an unimplemented array policy. This is not user error, but a developer error.
BprotoArrayPolicyViolationError: When array policy is violated: E.g. an Datatype is declared as an array, but the policy is disallowing it.
Returns:
tuple[BprotoFieldBaseType,int]: (datatype as enum, array size)
"""
if dtype not in BprotoFieldBaseType:
raise BprotoUnknownDataTypeError(dtype)
dtype: BprotoFieldBaseType = BprotoFieldBaseType(dtype)
array_policy = DTYPE_ARRAY_POLICY_MAP.get(dtype, None)
if array_policy is None:
raise ValueError("Internal Error: Array policy for this type was not found, fix this!")
match(array_policy):
case ArrayPolicy.ALLOW:
pass
case ArrayPolicy.DISALLOW:
if array_size > 1:
raise BprotoArrayPolicyViolationError(array_policy, dtype, field)
case ArrayPolicy.FORCE:
# This is fine, an array size of 1 is allowed
pass
case _:
raise ValueError("Internal Error: Array policy not implemented!")
return dtype, array_size
# Expections
class BprotoUnknownDataTypeError(BprotoFrontendError):
"""Error raise when building a field and the datatype is unknown.
This error should be use during the frontend compilation state.
Inherited from BprotoFrontendError.
"""
def __init__(self, data_type: str):
self.data_type = data_type
super().__init__(f"Unknown data type {data_type}!")
class BprotoArrayPolicyViolationError(BprotoFrontendError):
"""Error raise when building a field and the array policy is violated.
This error should be use during the frontend compilation state.
Inherited from BprotoFrontendError.
"""
def __init__(self, policy: ArrayPolicy, dtype: BprotoFieldBaseType, field: AbstractProtocolComponent):
self.policy = policy
self.field = field
match(policy):
case ArrayPolicy.ALLOW:
raise ValueError("Internal Error: Array policy ALLOW should not be violated!")
case ArrayPolicy.FORCE:
super().__init__(f"{field.get_name()} of type {dtype} must be an array!")
case ArrayPolicy.DISALLOW:
super().__init__(f"{field.get_name()} of type {dtype} must not be an array!")
case _:
raise ValueError("Internal Error: Array policy not implemented!")

View File

@@ -0,0 +1,232 @@
from . import AbstractProtocolComponent, find_smallset_next_value, sort_dict_by_value
from .dtypes import DTYPE_SIZE_MAP, BprotoFieldBaseType
from errors import BprotoDuplicateNameError, BprotoDuplicateEnumValueError, BprotoEnumBitsizeTooLargeError
from nameHandling.base import ComponentName, NameStyleBproto
import math
from copy import deepcopy
class Enumeration(AbstractProtocolComponent):
"""Represents an enumeration in a protocol.
This should not be instantiated directly, but rather through the FactoryEnumeration class.
Inherit from AbstractProtocolComponent.
"""
def __init__(self, name: ComponentName, bitsize: int = 0, inline: bool = False):
"""Initialize an Enumeration object; should not be used directly.
This methode should only be called by the FactoryEnumeration class.
Args:
name (ComponentName): Name of the enumeration, should be unique
bitsize (int, optional): How many bits are need to represent the largest value? Defaults to 0.
inline (bool): If true marks this enum as inline defintion for later renaming
"""
self.name: ComponentName = name
self.values: dict[ComponentName, int] = {}
self.bitsize: int = bitsize
self.inline: bool = inline
def __deepcopy__(self, memo={}):
if id(self) in memo:
return memo[id(self)]
new_enum = Enumeration(
self.name,
self.bitsize,
self.inline
)
new_enum.name = deepcopy(self.name, memo)
new_enum.bitsize = deepcopy(self.bitsize, memo)
new_enum.inline = deepcopy(self.inline, memo)
new_enum.values = deepcopy(self.values, memo)
memo[id(self)] = new_enum
return new_enum
# Inherited from AbstractProtocolComponent
def get_identifier(self):
return self.name
def get_name(self):
return self.name
def get_type_name(self):
return "enum"
def get_size_bits(self):
"""Returns the bits used to represent the enumeration.
Returns:
int: Number of bits used to represent the enumeration
"""
return self.bitsize
def get_size_bytes(self):
"""Return the number of bytes needed to represent the enumeration.
Simular to get_bitsize, but returns the number of bytes.
Formula: ceil(bitsize / 8)
Returns:
int: Number of bytes needed to represent the enumeration
"""
return math.ceil(self.bitsize / 8)
# Methodes specific to this class
def apply_naming(self, naming: str):
"""TODO: Is this method needed? REFACTOR!!!
Args:
naming (str): _description_
"""
self.name = f"{naming}_{self.name}"
def map_auxiliary_datatypes(self) -> BprotoFieldBaseType:
"""Return the bproto datatype that best represents the enumeration.
There is a theoretical limit of 64 bits for an enumeration.
TODO: Do we need to handle this case? Probably...
Returns:
BprotoFieldBaseType: Datatype to represent the enumeration
"""
bytes_size = self.get_size_bytes()
if bytes_size <= 1:
return BprotoFieldBaseType.UINT8
elif bytes_size <= 2:
return BprotoFieldBaseType.UINT16
elif bytes_size <= 4:
return BprotoFieldBaseType.UINT32
elif bytes_size <= 8:
return BprotoFieldBaseType.UINT64
else:
raise BprotoEnumBitsizeTooLargeError(self)
def __repr__(self):
value_names = ", ".join([
f"{name}={val}"
for name, val in self.values.items()
])
return f"Enum({self.name}, {value_names})"
class FactoryEnumeration:
"""Factory class for creating Enumeration objects.
Used to assemble Enumerations from key-value pairs.
This is a one-time use factory. After calling assemble, the object should not be used anymore.
This Factory is ment to be used during traversale of the abstract syntax tree.
Simular to the FactoryBitfield class.
"""
def __init__(self):
self.key_value_pairs: list[tuple[NameStyleBproto, int]] = []
def add_value(self, name: str, value: int | None):
"""Add a key-value pair to the enumeration.
Used to build up the enumeration.
Duplication checks are not done here, but in the assemble
Args:
name (str): Name of the value
value (int | None): Value of the enumeration. If None, the value will be filled in later.
"""
self.key_value_pairs.append((NameStyleBproto.fromStr(name, "enum"), value))
@staticmethod
def calculate_bitsize(values: list[int]) -> int:
"""Given a list of unique values, calculate the bitsize needed to represent the largest value/ amount of values.
For empty lists, it will return 1.
Args:
values (list[int]): List of UNIQUE values, usually a list values of an enumeration
Returns:
int: Number of bits needed to represent the values
"""
if len(values) == 0:
return 1
max_value = max(values)
if max_value <= 8589934592:
return math.ceil(math.log2(max_value + 1))
else:
# This is a bit of a hack, but it works for now
# The problem is that the log2 function is not accurate enough for large numbers
# log2(2**64) = log2(2**64 - 1) = log2(2**64 + 1) = 64
# And this makes in this case a difference of 1 bit
if max_value <= 18446744073709551615:
return 64
return 128
def assemble(self, name: ComponentName, inline: bool = False) -> Enumeration:
"""Finalize the enumeration and create an Enumeration object.
It uses the key-value pairs added with add_values earlier.
This will:
1. Check for duplicates in name or value
2. Fill in missing values
3. Sort the dictionary by value
4. Calculate the bitsize of the enumeration and ensure it is within the protocol specs
After calling this method, the FactoryEnumeration object should not be used anymore.
Args:
name (ComponentName): Name of the enumeration, should be unique
inline (bool): if True marks this enum as aline
Raises:
BprotoDuplicateNameError: Error when a name of value is duplicated
BprotoDuplicateEnumValueError: Error when a value is duplicated
BprotoEnumBitsizeTooLargeError: Error when the bitsize of the enumeration is too large for the protocol specs
Returns:
Enumeration: Finished enumeration object to be used in the backend processing step
"""
enum_dict = {}
used_values = set()
resulting_enum = Enumeration(NameStyleBproto.fromStr(name), 0, inline=inline)
# Check for duplicates in name or value
for key, value in self.key_value_pairs:
if key in enum_dict:
raise BprotoDuplicateNameError(key, resulting_enum)
if value in used_values and value is not None:
raise BprotoDuplicateEnumValueError(value, resulting_enum)
if value is not None:
used_values.add(value)
enum_dict.update({key: value})
# Fill in missing values
search_start = 0
for key, value in enum_dict.items():
if value is None:
value = find_smallset_next_value(used_values, search_start)
search_start = value + 1
used_values.add(value)
enum_dict[key] = value
# Setting a few last properties
resulting_enum.values = sort_dict_by_value(enum_dict)
resulting_enum.bitsize = FactoryEnumeration.calculate_bitsize(list(used_values))
# Getting the best datatype to represent the enumeration
# This will raise an error if the enumeration is too large
dtype = resulting_enum.map_auxiliary_datatypes()
# Mapping datatype to its bitsize
resulting_enum.bitsize = DTYPE_SIZE_MAP[dtype] * 8
return resulting_enum

View File

@@ -0,0 +1,337 @@
from .dtypes import BprotoFieldBaseType, DTYPE_SIZE_MAP, validate_datatype
from .bitfields import Bitfield
from .enumeration import Enumeration
from . import AbstractProtocolComponent
from nameHandling.base import ComponentName, NameStyleBproto
from copy import deepcopy
class Field(AbstractProtocolComponent):
"""Represents a field in a protocol message.
This object should not be instantiated directly, but rather through the FactoryField class.
This is done during the frontend parsing of the protocol definition.
Extra classes exist for bitfields and enumerations / their references.
Inherit from AbstractProtocolComponent.
"""
def __init__(self,
name: ComponentName,
pos: int,
dtype: BprotoFieldBaseType,
array_size: int
):
"""Initialize a Field object; should not be used directly. Use the FactoryField class.
Args:
name (ComponentName): Name of the field, should be unique
pos (int): Position of the field in the message
dtype (BprotoFieldBaseType): Datatype of the field, this should already be validated
array_size (int): Size of the array, 1 for scalar fields
"""
self.name: ComponentName = name
self.pos = pos
self.type: BprotoFieldBaseType = dtype
self.array_size: int = array_size
self.ref = None
def resolve_reference(self,
enum_dict: dict[ComponentName, Enumeration],
bitfield_dict: dict[ComponentName, Bitfield]
) -> AbstractProtocolComponent:
"""_summary_
Args:
enum_dict (dict[ComponentName, Enumeration]): Dictoray
bitfield_dict (dict[ComponentName, Bitfield]): _description_
Returns:
AbstractProtocolComponent: Return the resolved reference object
"""
return self
# Inherited from AbstractProtocolComponent
def get_identifier(self) -> ComponentName:
return self.name
def get_name(self) -> ComponentName:
return self.name
def get_type_name(self) -> str:
return "field"
def get_size_bytes(self) -> int:
return DTYPE_SIZE_MAP[self.type] * self.array_size
def get_size_bits(self) -> int:
return DTYPE_SIZE_MAP[self.type] * self.array_size * 8
def get_base_size_bytes(self) -> int:
return DTYPE_SIZE_MAP[self.type]
# Dunder methods
def __repr__(self):
return f"{self.__class__.__name__}[{self.pos}]({self.name}, {self.type}, {self.array_size})" + (f" ref:{self.ref}" if self.ref is not None else "")
def __deepcopy__(self, memo):
if id(self) in memo:
return memo[id(self)]
new_field = Field(
deepcopy(self.name, memo),
deepcopy(self.pos, memo),
deepcopy(self.type, memo),
deepcopy(self.array_size, memo)
)
new_field.ref = deepcopy(self.ref, memo)
memo[id(self)] = new_field
return new_field
# Bitfield Fields
class FieldBitfield(Field):
"""Field object specifically for inline bitfields.
This object should not be instantiated directly, but rather through the FactoryField class.
Similar to FieldEnum, but for bitfields.
Inherit from Field and behaves like a Field object.
"""
def __init__(self, name: ComponentName, pos: int, bitfield: Bitfield):
"""Should not be used directly. Use the FactoryField class.
Args:
name (ComponentName): Name of the field, should be unique
pos (int): Position of the field in the message
bitfield (Bitfield): Bitfield object that this field represents
"""
super().__init__(name, pos, BprotoFieldBaseType.BITFIELD, 1)
self.bitfield: Bitfield = bitfield
def __deepcopy__(self, memo):
if id(self) in memo:
return memo[id(self)]
new_field = FieldBitfield(
deepcopy(self.name, memo),
deepcopy(self.pos, memo),
deepcopy(self.bitfield, memo)
)
new_field.type = deepcopy(self.type, memo)
new_field.array_size = deepcopy(self.array_size, memo)
memo[id(self)] = new_field
return new_field
def get_size_bytes(self) -> int:
return self.bitfield.get_size_bytes()
def get_size_bits(self):
return self.bitfield.get_bitsize()
def get_base_size_bytes(self) -> int:
return self.bitfield.get_size_bytes()
class FieldBitfieldRef(Field):
"""Field object specifically for bitfield references.
This object should not be instantiated directly, but rather through the FactoryField class.
The reference is the name of the bitfield that this field represents.
This refrence is not validated.
An object of this class should be eventually resolve to a FieldBitfield object.
Similar to FieldRefEnum, but for bitfield references.
Inherit from Field and behaves like a Field object.
"""
def __init__(self, name: ComponentName, pos: int, bitfield: ComponentName):
"""Should not be used directly. Use the FactoryField class.
Args:
name (ComponentName): Name of the field, should be unique
pos (int): Position of the field in the message
bitfield (ComponentName): Name of the bitfield that this field references
"""
super().__init__(name, pos, BprotoFieldBaseType.BITFIELD, 1)
self.ref: ComponentName = bitfield
def __deepcopy__(self, memo):
return super().__deepcopy__(memo)
def get_size_bytes(self):
return 0
def get_size_bits(self):
return 0
def get_base_size_bytes(self) -> int:
return 0
# Enum Fields
class FieldEnum(Field):
"""Field object specifically for inline enumerations.
This object should not be instantiated directly, but rather through the FactoryField class.
Similar to FieldBitfield, but for enumerations.
Inherit from Field and behaves like a Field object.
"""
def __init__(self, name: ComponentName, pos: int, enum: Enumeration):
"""Should not be used directly. Use the FactoryField class.
Args:
name (ComponentName): Name of the field, should be unique
pos (int): Position of the field in the message
enum (Enumeration): Enumeration object that this field represents
"""
super().__init__(name, pos, BprotoFieldBaseType.ENUM, 1)
self.enum: Enumeration = enum
def __deepcopy__(self, memo={}):
if id(self) in memo:
return memo[id(self)]
new_field = FieldEnum(
deepcopy(self.name, memo),
deepcopy(self.pos, memo),
deepcopy(self.enum, memo)
)
new_field.type = deepcopy(self.type, memo)
new_field.array_size = deepcopy(self.array_size, memo)
memo[id(self)] = new_field
return new_field
def get_size_bytes(self) -> int:
return self.enum.get_size_bytes()
def get_size_bits(self) -> int:
return self.enum.get_bitsize()
def get_base_size_bytes(self) -> int:
return self.enum.get_size_bytes()
class FieldEnumRef(Field):
"""Field object specifically for enumeration references.
This object should not be instantiated directly, but rather through the FactoryField class.
The reference is the name of the enumeration that this field represents.
This refrence is not validated.
An object of this class should be eventually resolve to a BprotoFieldEnum object.
Similar to FieldRefBitfield, but for enumerations.
Inherit from Field and behaves like a Field object.
"""
def __init__(self, name: ComponentName, pos: int, enum: ComponentName):
"""Should not be used directly. Use the FactoryField class.
Args:
name (ComponentName): Name of the field, should be unique
pos (int): Position of the field in the message
enum (ComponentName): Name of the enumeration that this field references
"""
super().__init__(name, pos, BprotoFieldBaseType.ENUM, 1)
self.ref: ComponentName = enum
def __deepcopy__(self, memo):
return super().__deepcopy__(memo)
def get_size_bytes(self):
return 0
def get_size_bits(self):
return 0
def get_base_size_bytes(self) -> int:
return 0
# Factory class
class FactoryField():
"""Factory class for building Field objects representing a bproto field.
This is ment to be used during the frontend compilation stage, during the traversal of the abstract syntax tree.
Its a one-time use class, so create a new instance for each Field object.
This class is compleatly statless (yet)
"""
def __init__(self):
"""Initialize the FactoryField object. Nothing to do here yet.
"""
pass
def assemble(self,
name: str,
pos: int,
dtype: str,
array_size: int,
ref: None | str | Bitfield | Enumeration) -> Field:
"""Finalize the field object and return it. Builds Enum and Bitfield Field objects if needed.
If the datatype is a bitfield or enumeration, the reference object should be provided.
The reference object should be a Bitfield or Enumeration object, or the name of the object.
Acorrding to the reference (and datatype), the correct specialized Field object is created.
Otherwise, the reference should be None.
If the datatype is a standard type, a Field object is created.
This methode will:
1. Validate the datatype and array size (array policy)
2. Create the correct Field object based on the datatype and reference
Args:
name (str): Name of the field, should be unique
pos (int): Position of the field in the message
dtype (str): Datatype of the field in string form
array_size (int): Size of the array, 1 for scalar fields
ref (None | str | Bitfield | Enumeration): For bitfields and enumerations, the reference object / reference name
Raises:
ValueError: Thrown when a reference type is provided that is neither a string nor a Bitfield or Enumeration object, or when an unimplemented array policy is encountered.
BprotoUnknownDataTypeError: Throws when the datatype is unknown
BprotoArrayPolicyViolationError: When array policy is violated: E.g. an Datatype is declared as an array, but the policy is disallowing it.
Returns:
Field: The finalized Field object, can also be a specialized Field object like FieldBitfield or BprotoFieldEnum
"""
converted_name = NameStyleBproto.fromStr(name)
converted_ref = NameStyleBproto.fromStr(ref) if isinstance(ref, str) else ref
resulting_field = Field(
converted_name,
pos,
dtype,
array_size
)
dtype, array_size = validate_datatype(dtype, array_size, resulting_field)
if dtype == BprotoFieldBaseType.BITFIELD:
if isinstance(ref, Bitfield):
resulting_field = FieldBitfield(converted_name, pos, ref)
elif isinstance(ref, str):
resulting_field = FieldBitfieldRef(converted_name, pos, converted_ref)
else:
raise ValueError("Internal Error: Invalid reference type for bitfield")
elif dtype == BprotoFieldBaseType.ENUM:
if isinstance(ref, Enumeration):
resulting_field = FieldEnum(converted_name, pos, ref)
elif isinstance(ref, str):
resulting_field = FieldEnumRef(converted_name, pos, converted_ref)
else:
raise ValueError("Internal Error: Invalid reference type for enum")
else:
resulting_field = Field(converted_name, pos, dtype, array_size)
return resulting_field

View File

@@ -0,0 +1,144 @@
from .field import Field
from . import AbstractProtocolComponent
from nameHandling.base import ComponentName, NameStyleBproto
from errors import BprotoDuplicateNameError, BprotoMessageIDAlreadyUsed
from copy import deepcopy
from collections import OrderedDict
class Message(AbstractProtocolComponent):
"""Representation of a bproto message.
Should not be instantiated directly, but rather through the FactoryMessage class.
Contains a dict of fields, where the key is the field name and the value is the Field object.
Inherit from AbstractProtocolComponent
"""
def __init__(self, name: ComponentName, index_number: int):
"""Should not be used directly. Use the FactoryMessage class.
Args:
name (str): Name of the message, should be unique
index_number (int): Index number of message, used for ordering, should be unique
"""
# field: {name: (type, array_size)}
self.name: ComponentName = name
self.message_index_number = index_number
self.fields: dict[ComponentName, Field] = {}
def __deepcopy__(self, memo={}):
if id(self) in memo:
return memo[id(self)]
new_message = Message(
deepcopy(self.name, memo),
deepcopy(self.message_index_number, memo)
)
new_message.fields = {
deepcopy(k, memo): deepcopy(v, memo)
for k, v in self.fields.items()
}
memo[id(self)] = new_message
return new_message
# Inherited from AbstractProtocolComponent
def get_identifier(self):
return self.message_index_number
def get_name(self):
return self.name
def get_type_name(self):
return "message"
def get_size_bytes(self) -> int:
return sum([field.get_size_bytes() for field in self.fields.values()])
def get_size_bits(self) -> int:
return sum([field.get_size_bits() for field in self.fields.values()])
# Methodes specific to this class
def apply_naming(self, naming: str):
"""Refactor this, should not be used.
Args:
naming (str): _description_
"""
self.name = f"{naming}_{self.name}"
class FactoryMessage():
"""Factory class for build Message objects representing a bproto message.
This is ment to be used during the frontend compilation stage, during the traversal of the abstract syntax tree.
Fields are added to the message using the add_field method.
The message is finalized using the assemble method.
After that the Factory should not be used anymore.
Its a one-time use class, so create a new instance for each Message object.
This class should be used to create Message objects.
"""
def __init__(self):
self.fields: list[Field] = []
def add_field(self, field: Field):
"""Adds a finished field to the message.
This does not check for duplications, this is done in the assemble method.
Args:
field (BprotoField): The field to add to the message.
"""
self.fields.append(field)
@staticmethod
def sort_fields_dict(fields: dict[ComponentName, Field]) -> dict[ComponentName, Field]:
"""Static methode for sorting a dictionary of bproto fields by their position.
Args:
fields (dict[ComponentName, BprotoField]): The fields to sort.
Returns:
dict[ComponentName, BprotoField]: The sorted fields.
"""
return OrderedDict(sorted(fields.items(), key=lambda x: x[1].pos))
def assemble(self, name: ComponentName, index_number: int) -> Message:
"""Finalize the message and create the Message object, returning it.
After this method is called, the Factory should not be used anymore.
Args:
name (ComponentName): Name of the message, should be unique. (No uniqueness checks are done here)
index_number (int): Index number of message, used for ordering, should be unique. (No uniqueness checks are done here)
Raises:
BprotoDuplicateNameError: Raise if a field name is used more than once.
Returns:
BprotoMessage: The finished message object.
"""
resulting_message = Message(NameStyleBproto.fromStr(name), index_number)
field_names: set[ComponentName] = set()
field_positions: set[int] = set()
fields_dict: dict[ComponentName, Field] = {}
for i in self.fields:
if i.name in field_names:
raise BprotoDuplicateNameError(i.name, resulting_message)
if i.pos in field_positions:
raise BprotoMessageIDAlreadyUsed(i.pos, resulting_message)
field_positions.add(i.pos)
field_names.add(i.name)
fields_dict[i.name] = i
fields_dict = FactoryMessage.sort_fields_dict(fields_dict)
resulting_message.fields = fields_dict
return resulting_message

View File

@@ -0,0 +1,57 @@
from protocol_components.message import Message
from protocol_components.bitfields import Bitfield
from protocol_components.enumeration import Enumeration
from nameHandling.base import ComponentName
from copy import deepcopy
class ProtocolDefinitions():
"""Incapuslates all the components of a protocol definition
"""
def __init__(self, name: ComponentName, version: int,
enums: dict[ComponentName, Enumeration],
bitfields: dict[ComponentName, Bitfield],
messages: dict[ComponentName, Message]
):
"""Initializes the ProtocolDefinitions class; Should not be called directly
Use FactoryProtocolDefition.assemble instead
Args:
name (ComponentName): Name of the protocol
version (int): Version of the protocol
enums (dict[ComponentName, Enumeration]): Enums of the protocol
bitfields (dict[ComponentName, Bitfield]): Bitfields of the protocol
messages (dict[ComponentName, Message]): Messages of the protocol
"""
self.name: ComponentName = name
self.version = version
self.enums = enums
self.bitfields = bitfields
self.messages = messages
self.protocol_hash_inital: None | int = None
def __deepcopy__(self, memo={}):
if id(self) in memo:
return memo[id(self)]
res = ProtocolDefinitions(
deepcopy(self.name, memo),
deepcopy(self.version, memo),
enums={
deepcopy(k, memo): deepcopy(v, memo)
for k, v in self.enums.items()
},
bitfields={
deepcopy(k, memo): deepcopy(v, memo)
for k, v in self.bitfields.items()
},
messages={
deepcopy(k, memo): deepcopy(v, memo)
for k, v in self.messages.items()
}
)
res.protocol_hash_inital = self.protocol_hash_inital
memo[id(self)] = res
return res

View File

@@ -0,0 +1,107 @@
from protocol_components.message import Message
from protocol_components.bitfields import Bitfield
from protocol_components.enumeration import Enumeration
from protocol_components.protocol import ProtocolDefinitions
from protocol_components import AbstractProtocolComponent
from nameHandling.resolver import ProtocolReferenceResolver
from errors import BprotoAlreadyDefinedError
from nameHandling.base import ComponentName, NameStyleBproto
from collections import OrderedDict
class FactoryProtocolDefition:
"""Factory class for creating ProtocolDefinitions
This a one-time use class, once the ProtocolDefinitions object is created, the factory should not be used anymore
"""
def __init__(self):
"""Initializes the FactoryProtocolDefition class; Nothing special here
"""
self.bitfields: dict[ComponentName, Bitfield] = {}
self.enums: dict[ComponentName, Enumeration] = {}
self.messages: dict[ComponentName, Message] = {}
def add_message(self, message: Message):
"""Add a message to the factory, name must be unique
Args:
message (Message): message to add
Raises:
BprotoAlreadyDefinedError: Thrown if message name was already defined
"""
if message.name in self.messages:
raise BprotoAlreadyDefinedError(message)
self.messages.update({message.name: message})
def add_bitfield(self, bitfield: Bitfield):
"""Add a bitfield to the factory, name must be unique
Args:
bitfield (Bitfield): Bitfield to add with unique name
Raises:
BprotoAlreadyDefinedError: Thrown if bitfield name was already defined
"""
if bitfield.name in self.bitfields:
raise BprotoAlreadyDefinedError(bitfield)
self.bitfields.update({bitfield.name: bitfield})
def add_enum(self, enum: Enumeration):
"""Add an enumeration to the factory, name must be unique
Args:
enum (Enumeration): Enumeration to add with unique name
Raises:
BprotoAlreadyDefinedError: Thrown if enum name was already defined
"""
if enum.name in self.enums:
raise BprotoAlreadyDefinedError(enum)
self.enums.update({enum.name: enum})
@staticmethod
def sort_components(components: dict[str, AbstractProtocolComponent]) -> dict[str, AbstractProtocolComponent]:
"""Sorts the components of a protocol definitiown by their identifier (can be integer or string)
Args:
components (dict[str, AbstractProtocolComponent]): Components, dictionary of messages
Returns:
dict[str,AbstractProtocolComponent]: Sorted dictionary of components, is a OrderedDict
"""
return OrderedDict(sorted(components.items(), key=lambda x: x[1].get_identifier()))
def assemble(self, name: str, version: int) -> ProtocolDefinitions:
"""Finalizes the ProtocolDefinitions object and returns it
After calling this method, the factory should not be used anymore
This:
1. Resolves refrence fields
2. Sorts the messages
3. Creates the ProtocolDefinitions object
Args:
name (str): Name of the protocol, gets converted to ComponentName
version (int): Version of the protocol
Returns:
ProtocolDefinitions: final ProtocolDefinitions object
"""
self.messages = self.sort_components(self.messages)
resulting_protocol_defition = ProtocolDefinitions(
NameStyleBproto.fromStr(name, "protocol_name"),
version,
self.enums,
self.bitfields,
FactoryProtocolDefition.sort_components(self.messages)
)
resolver = ProtocolReferenceResolver(resulting_protocol_defition)
resolver.resolve_refrence_protocol()
resolver.back_resolve_inlines()
return resulting_protocol_defition