diff --git a/m2isar/backends/etiss/instruction_generator.py b/m2isar/backends/etiss/instruction_generator.py index 49acb45b..69a8a268 100644 --- a/m2isar/backends/etiss/instruction_generator.py +++ b/m2isar/backends/etiss/instruction_generator.py @@ -151,7 +151,7 @@ def generate_instruction_callback(core: arch.CoreDef, instr_def: arch.Instructio callback_template = Template(filename=str(template_dir/'etiss_instruction_callback.mako')) context = instruction_utils.TransformerContext(core.constants, core.memories, core.memory_aliases, instr_def.fields, instr_def.attributes, - core.functions, enc_idx, core_default_width, core_name, static_scalars, core.intrinsics, generate_coverage) + core.functions, enc_idx, core_default_width, core_name, static_scalars, core.intrinsics, generate_coverage, False) # force a block end if necessary if ((arch.InstrAttribute.NO_CONT in instr_def.attributes diff --git a/m2isar/backends/etiss/instruction_utils.py b/m2isar/backends/etiss/instruction_utils.py index d8424f3d..db57ec06 100644 --- a/m2isar/backends/etiss/instruction_utils.py +++ b/m2isar/backends/etiss/instruction_utils.py @@ -141,7 +141,7 @@ class TransformerContext: def __init__(self, constants: "dict[str, arch.Constant]", memories: "dict[str, arch.Memory]", memory_aliases: "dict[str, arch.Memory]", fields: "dict[str, arch.BitFieldDescr]", attributes: "list[arch.InstrAttribute]", functions: "dict[str, arch.Function]", - instr_size: int, native_size: int, arch_name: str, static_scalars: bool, intrinsics, generate_coverage: bool, ignore_static=False): + instr_size: int, native_size: int, arch_name: str, static_scalars: bool, intrinsics, generate_coverage: bool, ignore_static: bool = False): self.constants = constants self.memories = memories diff --git a/m2isar/backends/etiss/writer.py b/m2isar/backends/etiss/writer.py index bd1b41ff..05074c86 100755 --- a/m2isar/backends/etiss/writer.py +++ b/m2isar/backends/etiss/writer.py @@ -28,6 +28,7 @@ from .instruction_writer import write_functions, write_instructions +# TODO: not required anymore for Python >= v3.9 class BooleanOptionalAction(argparse.Action): """A boolean optional action for argparse, supports automatic generation of --no-x flags.""" diff --git a/m2isar/frontends/coredsl2/parser.py b/m2isar/frontends/coredsl2/parser.py index bcc88501..1052d525 100644 --- a/m2isar/frontends/coredsl2/parser.py +++ b/m2isar/frontends/coredsl2/parser.py @@ -23,6 +23,10 @@ from .importer import recursive_import from .load_order import LoadOrder from .utils import make_parser +from ...backends.etiss.writer import BooleanOptionalAction # TODO: refactor +from ...transforms.infer_types.transform import infer_types +from ...transforms.validate_behav.validate import validate_behav +from ...warnings import add_warnings_flags, KNOWN_WARNINGS def main(): @@ -30,6 +34,9 @@ def main(): parser.add_argument("top_level", help="The top-level CoreDSL file.") parser.add_argument("--log", default="info", choices=["critical", "error", "warning", "info", "debug"]) parser.add_argument("-I", dest="includes", action="append", default=[], help="Extra include directories") + parser.add_argument('--infer-types', action=BooleanOptionalAction, default=True, help="Run type inference after parsing.") + parser.add_argument('--validate', action=BooleanOptionalAction, default=False, help="Run validator after parsing.") + add_warnings_flags(parser, KNOWN_WARNINGS, KNOWN_WARNINGS) # only if --validate args = parser.parse_args() @@ -251,14 +258,23 @@ def main(): op.statements = always_block_statements + op.statements instr_def.operation = op + model_obj = M2Model( + M2_METAMODEL_VERSION, + models, + {}, + CodeInfoBase.database + ) + + if args.infer_types or args.validate: + logger.info("Running type inference") + model_obj = infer_types(model_obj) + if args.validate: + logger.info("Running validator") + warnings_info = args.warnings + validate_behav(model_obj, warnings_info) + logger.info("dumping model") with open(model_path / (abs_top_level.stem + '.m2isarmodel'), 'wb') as f: - model_obj = M2Model( - M2_METAMODEL_VERSION, - models, - {}, - CodeInfoBase.database - ) pickle.dump(model_obj, f) diff --git a/m2isar/metamodel/__init__.py b/m2isar/metamodel/__init__.py index e6268df2..0396add7 100644 --- a/m2isar/metamodel/__init__.py +++ b/m2isar/metamodel/__init__.py @@ -28,8 +28,11 @@ the hierarchy. """ +import pickle import inspect import logging +from typing import Union +from pathlib import Path from dataclasses import dataclass from . import arch, behav, code_info @@ -91,3 +94,36 @@ def __post_init__(self): self.line_infos[idx] = c elif isinstance(c, code_info.FunctionInfo): self.function_infos[idx] = c + + +def load_model( + model_path: Union[str, Path], allow_missmatch: bool = False +) -> M2Model: + logger = logging.getLogger("load_model") + logger.debug("loading model: %s", str(model_path)) + with open(model_path, "rb") as f: + model_obj: M2Model = pickle.load(f) + assert isinstance(model_obj, M2Model), "Expected M2Model" + required_version = M2_METAMODEL_VERSION + if model_obj.model_version != required_version: + err_handler = logger.warning if allow_missmatch else RuntimeError + err_handler("Loaded model version mismatch") + return model_obj + + +def dump_model( + model_obj: M2Model, out_path: Union[str, Path], ignore_suffix: bool = False +): + logger = logging.getLogger("dump_model") + if not ignore_suffix: + out_path = Path(out_path) + suffix = out_path.suffix + required_suffix = ".m2isarmodel" + if suffix not in [required_suffix]: + assert len(suffix) == 0, f"Invalid suffix: {suffix}" + out_path = out_path.parent / f"{out_path.stem}{required_suffix}" + else: + assert suffix == required_suffix, f"Invalid suffix: {suffix}, Expected: {required_suffix}" + logger.debug("dumping model: %s", out_path) + with open(out_path, "wb") as f: + pickle.dump(model_obj, f) diff --git a/m2isar/metamodel/arch.py b/m2isar/metamodel/arch.py index adcd7e9f..88b6ed8e 100644 --- a/m2isar/metamodel/arch.py +++ b/m2isar/metamodel/arch.py @@ -266,6 +266,13 @@ def width(self): return get_const_or_val(self._width) + def __str__(self) -> str: + return f'{super().__repr__()}, width={self.width}, signed={self.signed}' + + def __repr__(self): + return self.__str__() + + @property def actual_width(self): """Returns the resolved width value rounded to the nearest multiple of 8.""" diff --git a/m2isar/transforms/__init__.py b/m2isar/transforms/__init__.py new file mode 100644 index 00000000..044d6037 --- /dev/null +++ b/m2isar/transforms/__init__.py @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# This file is part of the M2-ISA-R project: https://github.com/tum-ei-eda/M2-ISA-R +# +# Copyright (C) 2024 +# Chair of Electrical Design Automation +# Technical University of Munich + +"""This module contains various transforms for M2-ISA-R models.""" diff --git a/m2isar/transforms/infer_types/__init__.py b/m2isar/transforms/infer_types/__init__.py new file mode 100644 index 00000000..9e0ff52b --- /dev/null +++ b/m2isar/transforms/infer_types/__init__.py @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# This file is part of the M2-ISA-R project: https://github.com/tum-ei-eda/M2-ISA-R +# +# Copyright (C) 2024 +# Chair of Electrical Design Automation +# Technical University of Munich + +"""This module contains a type inference pass for M2-ISA-R models.""" diff --git a/m2isar/transforms/infer_types/transform.py b/m2isar/transforms/infer_types/transform.py new file mode 100644 index 00000000..7707bb8c --- /dev/null +++ b/m2isar/transforms/infer_types/transform.py @@ -0,0 +1,70 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# This file is part of the M2-ISA-R project: https://github.com/tum-ei-eda/M2-ISA-R +# +# Copyright (C) 2022 +# Chair of Electrical Design Automation +# Technical University of Munich + +"""Type inference for M2-ISA-R metamodel.""" + +import sys +import argparse +import logging +import pathlib + +from m2isar.metamodel import patch_model, load_model, dump_model + +from . import visitor + + +def get_parser(): + # read command line args + parser = argparse.ArgumentParser() + parser.add_argument("top_level", help="A .m2isarmodel file.") + parser.add_argument("--log", default="info", choices=["critical", "error", "warning", "info", "debug"]) + parser.add_argument("--output", "-o", type=str, default=None) + return parser + + +def infer_types(model_obj): + logger = logging.getLogger("infer_types") + for _, core_def in model_obj.cores.items(): + logger.debug("inferring types for core %s", core_def.name) + patch_model(visitor) + for _, instr_def in core_def.instructions.items(): + logger.debug("inferring types for instr %s", instr_def.name) + instr_def.operation.generate(None) + for _, set_def in model_obj.sets.items(): + logger.debug("inferring types for set %s", set_def.name) + patch_model(visitor) + for _, instr_def in set_def.instructions.items(): + logger.debug("inferring types for instr %s", instr_def.name) + instr_def.operation.generate(None) + return model_obj + + +def run(args): + # initialize logging + logging.basicConfig(level=getattr(logging, args.log.upper())) + + # resolve model paths + top_level = pathlib.Path(args.top_level) + + out_path = (top_level.parent / top_level.stem) if args.output is None else args.output + print("out_path", out_path) + + model_obj = load_model(top_level) + model_obj = infer_types(model_obj) + + dump_model(model_obj, out_path) + + +def main(argv): + parser = get_parser() + args = parser.parse_args(argv) + run(args) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/m2isar/transforms/infer_types/visitor.py b/m2isar/transforms/infer_types/visitor.py new file mode 100644 index 00000000..948c380e --- /dev/null +++ b/m2isar/transforms/infer_types/visitor.py @@ -0,0 +1,367 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# This file is part of the M2-ISA-R project: https://github.com/tum-ei-eda/M2-ISA-R +# +# Copyright (C) 2022 +# Chair of Electrical Design Automation +# Technical University of Munich + +"""A transformation module for simplifying M2-ISA-R behavior expressions. The following +simplifications are done: + +* Resolvable :class:`m2isar.metamodel.arch.Constant` s are replaced by + `m2isar.metamodel.arch.IntLiteral` s representing their value +* Fully resolvable arithmetic operations are carried out and their results + represented as a matching :class:`m2isar.metamodel.arch.IntLiteral` +* Conditions and loops with fully resolvable conditions are either discarded entirely + or transformed into code blocks without any conditions +* Ternaries with fully resolvable conditions are transformed into only the matching part +* Type conversions of :class:`m2isar.metamodel.arch.IntLiteral` s apply the desired + type directly to the :class:`IntLiteral` and discard the type conversion +""" + +import logging +from copy import copy + +from m2isar.metamodel import arch, behav + +logger = logging.getLogger("infer_types") + +# pylint: disable=unused-argument + + +def operation(self: behav.Operation, context): + statements = [] + for stmt in self.statements: + temp = stmt.generate(context) + if isinstance(temp, list): + statements.extend(temp) + else: + statements.append(temp) + + self.statements = statements + return self + + +def binary_operation(self: behav.BinaryOperation, context): + self.left = self.left.generate(context) + self.right = self.right.generate(context) + + # see: https://github.com/Minres/CoreDSL/wiki/Expressions#arithmetic-type-rules + if self.op.value in ["+", "-", "*", "/", "%", "|", "&", "^", "<<", ">>"]: + if self.left.inferred_type is None or self.right.inferred_type is None: + logger.warning("Slice Operation needs inferred type. Skipping...") + self.inferred_type = None + return self + assert isinstance(self.left.inferred_type, arch.IntegerType) + assert isinstance(self.right.inferred_type, arch.IntegerType) + w1 = self.left.inferred_type._width + w2 = self.right.inferred_type._width + s1 = self.left.inferred_type.signed + s2 = self.right.inferred_type.signed + if self.op.value == "+": + if not s1 and not s2: + wr = max(w1, w2) + 1 + sr = False + elif s1 and s2: + wr = max(w1, w2) + 1 + sr = True + elif s1 and not s2: + wr = max(w1, w2 + 1) + 1 + sr = True + elif not s1 and s2: + wr = max(w1 + 1, w2) + 1 + sr = True + elif self.op.value == "-": + sr = True + if not s1 and not s2: + wr = max(w1 + 1, w2 + 1) + elif s1 and s2: + wr = max(w1 + 1, w2 + 1) + elif s1 and not s2: + wr = max(w1, w2 + 1) + 1 + elif not s1 and s2: + wr = max(w1 + 1, w2) + 1 + elif self.op.value == "*": + wr = w1 + w2 + sr = s1 or s2 + elif self.op.value == "/": + wr = w1 if not s2 else (w1 + 1) + sr = s1 or s2 + elif self.op.value == "%": + if not s1 and not s2: + wr = min(w1, w2) + sr = False + elif s1 and s2: + wr = min(w1, w2) + sr = True + elif s1 and not s2: + wr = min(w1, w2 + 1) + sr = True + elif not s1 and s2: + wr = min(w1, max(1, w2 - 1)) + sr = False + elif self.op.value in ["|", "&", "^"]: + wr = max(w1, w2) + sr = s1 or s2 + elif self.op.value in [">>", "<<"]: + wr = w1 + sr = s1 + self.inferred_type = arch.IntegerType(wr, sr, None) + else: + if self.op.value in ["||", "&&"]: + self.inferred_type = arch.IntegerType(1, False, None) # unsigned<1> / bool + elif self.op.value in ["<", ">", "==", "!=", ">=", "<="]: + self.inferred_type = arch.IntegerType(1, False, None) # unsigned<1> / bool + assert self.inferred_type is not None + + return self + + +def slice_operation(self: behav.SliceOperation, context): + self.expr = self.expr.generate(context) + self.left = self.left.generate(context) + self.right = self.right.generate(context) + + # type inference + if self.expr.inferred_type is None: + logger.warning("Slice Operation needs inferred type. Skipping...") + return self + assert isinstance(self.expr.inferred_type, arch.IntegerType) + ty = self.expr.inferred_type + # For non-static slices, we cann not infer the type! + if not isinstance(self.left, behav.IntLiteral): + logger.warning("Can not infer type of non-static slice operation. Skipping...") + return self + lval = self.left.value + if not isinstance(self.right, behav.IntLiteral): + logger.warning("Can not infer type of non-static slice operation. Skipping...") + return self + rval = self.right.value + width = lval - rval + 1 if lval > rval else rval - lval + 1 + ty_ = copy(ty) + ty_._width = width + self.inferred_type = ty_ + + return self + + +def concat_operation(self: behav.ConcatOperation, context): + self.left = self.left.generate(context) + self.right = self.right.generate(context) + if self.left.inferred_type is None: + logger.warning("Concat Operation needs inferred type. Skipping...") + return self + if self.right.inferred_type is None: + logger.warning("Concat Operation needs inferred type. Skipping...") + return self + width = self.left.inferred_type.width + self.right.inferred_type.width + ty = arch.IntegerType(width, False, None) + self.inferred_type = ty + + return self + + +def number_literal(self: behav.IntLiteral, context): + if isinstance(self, behav.IntLiteral): + bit_size = self.bit_size + signed = self.signed + + self.inferred_type = arch.IntegerType(bit_size, signed, None) + return self + + +def int_literal(self: behav.IntLiteral, context): + + # type inference + bit_size = self.bit_size + signed = self.signed + + self.inferred_type = arch.IntegerType(bit_size, signed, None) + + return self + + +def scalar_definition(self: behav.ScalarDefinition, context): + # type inference + signed = self.scalar.data_type == arch.DataType.S + width = self.scalar.size + self.inferred_type = arch.IntegerType(width, signed, None) + return self + + +def assignment(self: behav.Assignment, context): + self.target = self.target.generate(context) + self.expr = self.expr.generate(context) + + # if isinstance(self.expr, behav.IntLiteral) and isinstance(self.target, behav.ScalarDefinition): + # self.target.scalar.value = self.expr.value + + # type inference + self.inferred_type = None + + return self + + +def conditional(self: behav.Conditional, context): + self.conds = [x.generate(context) for x in self.conds] + # self.stmts = [[y.generate(context) for y in x] for x in self.stmts] + stmts = [] + for stmt in self.stmts: + if isinstance(stmt, list): # TODO: legacy? + new = [y.generate(context) for y in stmt] + else: + new = stmt.generate(context) + stmts.append(new) + self.stmts = stmts + + return self + + +def loop(self: behav.Loop, context): + self.cond = self.cond.generate(context) + self.stmts = [x.generate(context) for x in self.stmts] + + return self + + +def ternary(self: behav.Ternary, context): + + self.cond = self.cond.generate(context) + self.then_expr = self.then_expr.generate(context) + self.else_expr = self.else_expr.generate(context) + + # TODO + then_ty = self.then_expr.inferred_type + else_ty = self.else_expr.inferred_type + if then_ty and else_ty: + # assert then_ty.signed == else_ty.signed + wt = then_ty.width + we = else_ty.width + wr = max(wt, we) + self.inferred_type = arch.IntegerType(wr, True, None) + + return self + + +def return_(self: behav.Return, context): + if self.expr is not None: + self.expr = self.expr.generate(context) + + return self + + +def unary_operation(self: behav.UnaryOperation, context): + + self.right = self.right.generate(context) + + if self.right.inferred_type: + w1 = self.right.inferred_type.width + if self.op.value == "-": + inferred_type = arch.IntegerType(w1 + 1, True, None) + elif self.op.value == "~": + inferred_type = arch.IntegerType(w1, True, None) + elif self.op.value == "!": + inferred_type = arch.IntegerType(1, False, None) + else: + inferred_type = None + self.inferred_type = inferred_type + + return self + + +def named_reference(self: behav.NamedReference, context): + reference = self.reference + + # type inference + # self.infered_type = ? + if isinstance(reference, arch.BitFieldDescr): + assert self.reference.data_type in [arch.DataType.U, arch.DataType.S] + ty = arch.IntegerType(reference.size, reference.data_type == arch.DataType.S, None) + self.inferred_type = ty + + elif isinstance(reference, arch.Scalar): + dt = reference.data_type + sz = reference.size + assert dt in [arch.DataType.U, arch.DataType.S] + signed = dt == arch.DataType.S + ty = arch.IntegerType(sz, signed, None) + self.inferred_type = ty + elif isinstance(reference, arch.Memory): + self.inferred_type = arch.IntegerType(reference.size, False, None) + elif isinstance(reference, arch.Intrinsic): + assert self.reference.data_type in [arch.DataType.U, arch.DataType.S] + self.inferred_type = arch.IntegerType(reference.size, reference.data_type == arch.DataType.S, None) + elif isinstance(reference, arch.Constant): + self.inferred_type = arch.IntegerType(reference.size, reference.signed, None) + else: + assert False, "Unhandled reference" + + return self + + +def indexed_reference(self: behav.IndexedReference, context): + self.index = self.index.generate(context) + + # type inference + assert isinstance(self.reference, arch.Memory) + ty = arch.DataType.U # TODO: Memory class should keep track of dtype, not only size? + assert ty in [arch.DataType.U, arch.DataType.S] + size = self.reference.size + ty_ = arch.IntegerType(size, ty == arch.DataType.S, None) + + self.inferred_type = ty_ + + return self + + +def type_conv(self: behav.TypeConv, context): + self.expr = self.expr.generate(context) + + ty = self.expr.inferred_type + if ty is None: + logger.warning("Type conv needs inferred type. Skipping...") + return self + assert isinstance(ty, arch.IntegerType) + assert self.data_type in [arch.DataType.U, arch.DataType.S] + ty.signed = self.data_type == arch.DataType.S + if self.size is not None: + ty._width = self.size + + # type inference + self.inferred_type = ty + + return self + + +def callable_(self: behav.Callable, context): + if isinstance(self.ref_or_name, arch.Function): + assert self.ref_or_name.data_type in [arch.DataType.U, arch.DataType.S] + signed = self.ref_or_name.data_type == arch.DataType.S + width = self.ref_or_name.size + self.inferred_type = arch.IntegerType(width, signed, None) + self.args = [stmt.generate(context) for stmt in self.args] + + return self + + +def procedure_call(self: behav.ProcedureCall, context): + self.args = [stmt.generate(context) for stmt in self.args] + + return self + + +def group(self: behav.Group, context): + self.expr = self.expr.generate(context) + + if isinstance(self.expr, behav.IntLiteral): + return self.expr + + # type inference + self.inferred_type = self.expr.inferred_type + + return self + + +def break_(self: behav.Break, context): + return self diff --git a/m2isar/transforms/validate_behav/__init__.py b/m2isar/transforms/validate_behav/__init__.py new file mode 100644 index 00000000..71ee24de --- /dev/null +++ b/m2isar/transforms/validate_behav/__init__.py @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# This file is part of the M2-ISA-R project: https://github.com/tum-ei-eda/M2-ISA-R +# +# Copyright (C) 2024 +# Chair of Electrical Design Automation +# Technical University of Munich + +"""This module contains a behavior validator pass for M2-ISA-R models. +Type inference has to be run first! +""" diff --git a/m2isar/transforms/validate_behav/validate.py b/m2isar/transforms/validate_behav/validate.py new file mode 100644 index 00000000..c9a931d4 --- /dev/null +++ b/m2isar/transforms/validate_behav/validate.py @@ -0,0 +1,74 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# This file is part of the M2-ISA-R project: https://github.com/tum-ei-eda/M2-ISA-R +# +# Copyright (C) 2022 +# Chair of Electrical Design Automation +# Technical University of Munich + +"""validate behavior in M2-ISA-R metamodel.""" + +import sys +import argparse +import logging +import pathlib + +from ...metamodel import patch_model, load_model, dump_model +from ...warnings import WarningsManager, WarningsInfo, add_warnings_flags, KNOWN_WARNINGS + +from . import visitor + +class ValidatorContext(WarningsManager): + """Track miscellaneous information throughout the validation process.""" + def __init__(self, warnings_info: WarningsInfo = None): + super().__init__(warnings_info) + + +def get_parser(): + # read command line args + parser = argparse.ArgumentParser() + parser.add_argument("top_level", help="A .m2isarmodel file.") + parser.add_argument("--log", default="info", choices=["critical", "error", "warning", "info", "debug"]) + add_warnings_flags(parser, KNOWN_WARNINGS, KNOWN_WARNINGS) + return parser + + +def validate_behav(model_obj, warnings_info): + logger = logging.getLogger("validate_behav") + for _, core_def in model_obj.cores.items(): + logger.debug("validating behavior for core %s", core_def.name) + context = ValidatorContext(warnings_info) + patch_model(visitor) + for _, instr_def in core_def.instructions.items(): + logger.debug("validating behavior for instr %s", instr_def.name) + instr_def.operation.generate(context) + for _, set_def in model_obj.sets.items(): + logger.debug("validating behavior for set %s", set_def.name) + context = ValidatorContext(warnings_info) + patch_model(visitor) + for _, instr_def in set_def.instructions.items(): + logger.debug("validating behavior for instr %s", instr_def.name) + instr_def.operation.generate(context) + # return model_obj + + +def run(args): + # initialize logging + logging.basicConfig(level=getattr(logging, args.log.upper())) + + # resolve model paths + top_level = pathlib.Path(args.top_level) + + model_obj = load_model(top_level) + warnings_info = args.warnings + alidate_behav(model_obj, warnings_info) + + +def main(argv): + parser = get_parser() + args = parser.parse_args(argv) + run(args) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/m2isar/transforms/validate_behav/visitor.py b/m2isar/transforms/validate_behav/visitor.py new file mode 100644 index 00000000..8591d3fa --- /dev/null +++ b/m2isar/transforms/validate_behav/visitor.py @@ -0,0 +1,176 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# This file is part of the M2-ISA-R project: https://github.com/tum-ei-eda/M2-ISA-R +# +# Copyright (C) 2022 +# Chair of Electrical Design Automation +# Technical University of Munich + +"""A transformation module for simplifying M2-ISA-R behavior expressions. The following +simplifications are done: + +* Resolvable :class:`m2isar.metamodel.arch.Constant` s are replaced by + `m2isar.metamodel.arch.IntLiteral` s representing their value +* Fully resolvable arithmetic operations are carried out and their results + represented as a matching :class:`m2isar.metamodel.arch.IntLiteral` +* Conditions and loops with fully resolvable conditions are either discarded entirely + or transformed into code blocks without any conditions +* Ternaries with fully resolvable conditions are transformed into only the matching part +* Type conversions of :class:`m2isar.metamodel.arch.IntLiteral` s apply the desired + type directly to the :class:`IntLiteral` and discard the type conversion +""" + +import logging +from copy import copy + +from m2isar.metamodel import arch, behav + +logger = logging.getLogger("validate_behav") + +# pylint: disable=unused-argument + + +def operation(self: behav.Operation, context): + statements = [] + for stmt in self.statements: + temp = stmt.generate(context) + if isinstance(temp, list): + statements.extend(temp) + else: + statements.append(temp) + + self.statements = statements + return self + + +def binary_operation(self: behav.BinaryOperation, context): + self.left = self.left.generate(context) + op = self.op + self.right = self.right.generate(context) + + assert self.left.inferred_type is not None + assert self.right.inferred_type is not None + if op.value in ["|", "&", "^"] and self.left.inferred_type.width != self.right.inferred_type.width: + context.emit_warning(f"Bitwise operations with differently size operands are discouraged.", "bit-op-missmatch", logger=logger, line_info=self.line_info) + if op.value in ["<<", ">>", ">>>"] and self.right.inferred_type.signed: + context.emit_warning(f"Shift by signed amount", "shift-signed", logger=logger, line_info=self.line_info) + if op.value in ["<", "<=", ">", ">=", "==", "!="] and self.left.inferred_type.signed != self.right.inferred_type.signed: + if isinstance(self.left, behav.IntLiteral) and self.left.value == 0: + pass + if isinstance(self.right, behav.IntLiteral) and self.right.value == 0: + pass + else: + context.emit_warning(f"Signed vs. unsigned comparison", "sign-compare", logger=logger, line_info=self.line_info) + if op.value == "<<" and self.left.inferred_type.width <= self.right.inferred_type.width: + context.emit_warning(f"Shift count overflow for << operation ({self.left.inferred_type.width} vs. {self.right.inferred_type.width})", "shift-overflow", logger=logger, line_info=self.line_info) + return self + + +def slice_operation(self: behav.SliceOperation, context): + self.expr = self.expr.generate(context) + self.left = self.left.generate(context) + self.right = self.right.generate(context) + return self + + +def concat_operation(self: behav.ConcatOperation, context): + self.left = self.left.generate(context) + self.right = self.right.generate(context) + return self + + +def number_literal(self: behav.IntLiteral, context): + return self + + +def int_literal(self: behav.IntLiteral, context): + return self + + +def scalar_definition(self: behav.ScalarDefinition, context): + return self + + +def assignment(self: behav.Assignment, context): + self.target = self.target.generate(context) + self.expr = self.expr.generate(context) + assert self.target.inferred_type is not None + assert self.expr.inferred_type is not None + if self.target.inferred_type.width < self.expr.inferred_type.width: + context.emit_warning(f"Implicit truncation {self.expr.inferred_type.width} -> {self.target.inferred_type.width} found", "implicit-trunc", logger=logger, line_info=self.line_info) + if self.target.inferred_type.width > self.expr.inferred_type.width: + context.emit_warning(f"Implicit extend {self.expr.inferred_type.width} -> {self.target.inferred_type.width} found", "implicit-extend", logger=logger, line_info=self.line_info) + return self + + +def conditional(self: behav.Conditional, context): + self.conds = [x.generate(context) for x in self.conds] + stmts = [] + for stmt in self.stmts: + if isinstance(stmt, list): # TODO: legacy? + new = [y.generate(context) for y in stmt] + else: + new = stmt.generate(context) + stmts.append(new) + self.stmts = stmts + return self + + +def loop(self: behav.Loop, context): + self.cond = self.cond.generate(context) + self.stmts = [x.generate(context) for x in self.stmts] + return self + + +def ternary(self: behav.Ternary, context): + self.cond = self.cond.generate(context) + self.then_expr = self.then_expr.generate(context) + self.else_expr = self.else_expr.generate(context) + + return self + + +def return_(self: behav.Return, context): + if self.expr is not None: + self.expr = self.expr.generate(context) + return self + + +def unary_operation(self: behav.UnaryOperation, context): + self.right = self.right.generate(context) + return self + + +def named_reference(self: behav.NamedReference, context): + return self + + +def indexed_reference(self: behav.IndexedReference, context): + self.index = self.index.generate(context) + return self + + +def type_conv(self: behav.TypeConv, context): + self.expr = self.expr.generate(context) + return self + + +def callable_(self: behav.Callable, context): + self.args = [stmt.generate(context) for stmt in self.args] + return self + + +def procedure_call(self: behav.ProcedureCall, context): + self.args = [stmt.generate(context) for stmt in self.args] + return self + + +def group(self: behav.Group, context): + self.expr = self.expr.generate(context) + if isinstance(self.expr, behav.IntLiteral): + return self.expr + return self + + +def break_(self: behav.Break, context): + return self diff --git a/m2isar/warnings.py b/m2isar/warnings.py new file mode 100644 index 00000000..20df07fc --- /dev/null +++ b/m2isar/warnings.py @@ -0,0 +1,106 @@ +import argparse +from dataclasses import dataclass, field +from typing import Set + +KNOWN_WARNINGS = { + 'implicit-trunc', + 'shift-overflow', + 'shift-signed', + 'implicit-extend', + 'sign-compare', + 'unused-value', + 'bit-op-missmatch', +} + + +@dataclass +class WarningsInfo: + # known: Set[str] = field(default_factory=set) + known: Set[str] = field(default_factory=lambda: set(KNOWN_WARNINGS)) + # defaults: Set[str] = field(default_factory=set) + defaults: Set[str] = field(default_factory=lambda: set(KNOWN_WARNINGS)) + enabled: Set[str] = field(default_factory=set) + disabled: Set[str] = field(default_factory=set) + as_error: Set[str] = field(default_factory=set) + all_as_error: bool = False + + @property + def warnings(self): + return (self.defaults - self.disabled) | self.enabled + + @property + def errors(self): + return self.as_error if not self.all_as_error else self.warnings + + +class WarningFlagAction(argparse.Action): + def __call__(self, parser, namespace, values, option_string=None): + warnings_info = getattr(namespace, 'warnings_info', WarningsInfo()) + + for val in values: + if val == 'no-error': + warnings_info.all_as_error = False + elif val.startswith('no-'): + warn = val[3:] + assert warn in warnings_info.known, f"Unknown warning: {warn}" + warnings_info.disabled.add(warn) + elif val.startswith('error='): + warn = val[6:] + assert warn in warnings_info.known, f"Unknown warning: {warn}" + warnings_info.error_set.add(warn) + elif val == 'error': + warnings_info.all_as_error = True + elif val == 'all': + warnings_info.enabled.update(warnings_info.known) + else: + warn = val[3:] + assert warn in warnings_info.known, f"Unknown warning: {val}" + warnings_info.enabled.add(val) + # No need for -Wall as all warnings are enabled by default + + setattr(namespace, 'warnings', warnings_info) + +def add_warnings_flags(parser, known_warnings: Set[str], default_warnings: Set[str]): + parser.add_argument( + '-W', + dest='warnings', + metavar='warning', + action=WarningFlagAction, + nargs='+', + help=( + "Enable/disable warnings like -Wfoo or -Wno-foo; " + "Make fatal with -Werror or -Werror=foo" + ), + ) + + # Defaults + warnings_info = WarningsInfo(known=known_warnings, defaults=default_warnings) + parser.set_defaults(warnings=warnings_info) + + +class WarningsManager: + def __init__(self, warnings_info: WarningsInfo): + self.warnings_info = warnings_info + + # TODO: warnings as errors? + def emit_warning(self, msg, name=None, logger=None, line_info=None): + log_warn_f = logging.warning if logger is None else logger.warning + log_err_f = logging.error if logger is None else logger.error + if self.warnings_info is None: + return # ignore + assert name in self.warnings_info.known, f"Unknown warning: {name}" + is_err = name in self.warnings_info.errors + if name not in self.warnings_info.warnings: + # do nothing + return + log_f = log_err_f if is_err else log_warn_f + if name is not None: + msg += f" [-W{name}]" + if line_info is not None: + line_info_str = f"{line_info.file_path}:{line_info.start_line_no}" + msg += f" @ {line_info_str}" + log_f(msg) + if is_err: + raise RuntimeError(msg) + +