petspats/pyha

View on GitHub
pyha/conversion/redbaron_transforms.py

Summary

Maintainability
F
1 wk
Test Coverage
import logging
import textwrap
from contextlib import suppress

from parse import parse
from redbaron import Node, EndlNode, DefNode, AssignmentNode, TupleNode, CommentNode, FloatNode, \
    IntNode, UnitaryOperatorNode, GetitemNode, inspect, CallNode, AtomtrailersNode, CallArgumentNode, \
    BinaryOperatorNode, ComplexNode, AssociativeParenthesisNode
from redbaron.base_nodes import LineProxyList

import pyha
from pyha import Complex
from pyha.common.core import SKIP_FUNCTIONS, Hardware, PyhaFunc
from pyha.common.fixed_point import Sfix
from pyha.common.util import get_iterable, tabber, formatter, is_constant, const_filter
from pyha.conversion.type_transforms import escape_reserved_vhdl, VHDLModule, init_vhdl_type, VHDLEnum, VHDLList, \
    TypeAppendHack

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


def file_header():
    template = '-- generated by pyha {}'
    return template.format(pyha.__version__)


class NodeVHDL:
    def __init__(self, red_node, parent=None):
        self.red_node = red_node
        self.parent = parent
        self.target = None
        self.value = None
        self.first = None
        self.second = None
        self.test = None
        self.arguments = None
        self.name = None
        self.iterator = None

        for x in red_node._dict_keys:
            self.__dict__[x] = redbaron_node_to_vhdl_node(red_node.__dict__[x], caller=self)

        for x in red_node._list_keys:
            if 'format' not in x:
                self.__dict__[x] = []
                for xj in red_node.__dict__[x]:
                    # function is NOT simulated, dont convert it
                    if isinstance(xj, DefNode) and getattr(convert_obj, xj.name).calls == 0:
                        logger.warning(
                            f'Class "{red_node.name}" function "{xj.name}" was NOT called during simulation, not converting it!')
                        continue
                    self.__dict__[x].append(redbaron_node_to_vhdl_node(xj, caller=self))

        for x in red_node._str_keys:
            self.__dict__[x] = red_node.__dict__[x]

    def __str__(self):
        return str(self.red_node)


class NameNodeVHDL(NodeVHDL):
    def __str__(self):
        return escape_reserved_vhdl(self.red_node.value)


class AtomtrailersNodeVHDL(NodeVHDL):
    def is_function_call(self):
        return any(isinstance(x, CallNodeVHDL) for x in self.value)

    def __str__(self):
        ret = ''
        for i, x in enumerate(self.value):
            # add '.' infront if NameNode
            new = '.{}' if isinstance(x, NameNodeVHDL) and i != 0 else '{}'

            if x.value == 'len' and isinstance(x.red_node.next, CallNode):
                continue

            if isinstance(x.red_node, CallNode) and x.red_node.previous.value == 'len':
                new = f"{str(x)[1:-1]}'length"

            ret += new.format(x)

        return ret


class TupleNodeVHDL(NodeVHDL):
    def __iter__(self):
        return iter(self.value)

    def __str__(self):
        return ','.join(str(x) for x in self.value)


class AssignmentNodeVHDL(NodeVHDL):
    def __str__(self):
        return f'{self.target} := {self.value};'


class ReturnNodeVHDL(NodeVHDL):
    def __str__(self):
        ret = []
        for i, value in enumerate(get_iterable(self.value)):
            line = f'ret_{i} := {value}'
            if line[-1] != ';':
                line += ';'
            ret.append(line)

        ret += ['return;']
        return '\n'.join(ret)


class ComparisonNodeVHDL(NodeVHDL):
    def __str__(self):
        return f'{self.first} {self.value} {self.second}'


class BinaryOperatorNodeVHDL(ComparisonNodeVHDL):
    def __str__(self):

        # test if we are dealing with array appending ([a] + b)
        if self.value == '+':
            if isinstance(self.first, ListNodeVHDL) or isinstance(self.second, ListNodeVHDL):
                return f'{self.first} & {self.second}'
        elif self.value == '//':
            return f'integer({self.first} / {self.second})'
        elif self.value == '>>':
            self.value = 'sra'
        elif self.value == '<<':
            self.value = 'sla'
        elif self.value == '&':
            self.value = 'and'
        elif self.value == '|':
            self.value = 'or'
        elif self.value == '^':
            self.value = 'xor'
        elif self.value == '%':
            self.value = 'mod'

        return f'{self.first} {self.value} {self.second}'


class BooleanOperatorNodeVHDL(ComparisonNodeVHDL):
    pass


class AssociativeParenthesisNodeVHDL(NodeVHDL):
    def __str__(self):
        return f'({self.value})'


class ComparisonOperatorNodeVHDL(NodeVHDL):
    def __str__(self):
        if self.first == '==':
            return '='
        elif self.first == '!=':
            return '/='
        else:
            return super().__str__()


class IfelseblockNodeVHDL(NodeVHDL):
    def __str__(self):
        body = '\n'.join(str(x) for x in self.value)
        return body + '\nend if;'


class IfNodeVHDL(NodeVHDL):
    def __str__(self):
        body = '\n'.join(tabber(str(x)) for x in self.value)
        return f'if {self.test} then\n{body}'


class ElseNodeVHDL(NodeVHDL):
    def __str__(self):
        body = '\n'.join(tabber(str(x)) for x in self.value)
        return f'else\n{body}'


class ElifNodeVHDL(NodeVHDL):
    def __str__(self):
        body = '\n'.join(tabber(str(x)) for x in self.value)
        return f'elsif {self.test} then\n{body}'


class DefNodeVHDL(NodeVHDL):
    def __init__(self, red_node, parent=None):
        super().__init__(red_node, parent)

        # todo: remove me after refactorings
        try:
            self.data = getattr(convert_obj, self.name)
        except AttributeError:
            self.data = None

        self.name = escape_reserved_vhdl(self.name)

        # collect multiline comment
        self.multiline_comment = ''
        if isinstance(self.value[0], StringNodeVHDL):
            self.multiline_comment = str(self.value[0])
            del self.value[0]

        # remove last line,if it is \n
        if isinstance(self.value[-1], EndlNodeVHDL):
            del self.value[-1]

        self.arguments = self.build_arguments()
        self.variables = self.build_variables()

    def build_arguments(self):
        # function arguments
        argnames = inspect.getfullargspec(self.data.func).args[1:]  # skip the first 'self'
        argvals = list(self.data.get_arg_types())
        args = [init_vhdl_type(name, val, val) for name, val in zip(argnames, argvals)]
        args = ['self:in self_t; self_next:inout self_t; constant self_const: self_t_const'] + [
            f'{x._pyha_name()}: {x._pyha_type()}' for x in args]

        # function returns -> need to add as 'out' arguments in VHDL
        rets = []
        if self.data.get_output_types() is not None:
            if isinstance(self.data.get_output_types(), tuple):  # multiple returns
                rets = [init_vhdl_type(f'ret_{i}', val, val)
                        for i, val in enumerate(get_iterable(self.data.get_output_types()))]
            else:
                rets = [init_vhdl_type(f'ret_{0}', self.data.get_output_types(), self.data.get_output_types())]
            rets = [f'{x._pyha_name()}:out {x._pyha_type()}' for x in rets]

        return '; '.join(args + rets)

    def build_variables(self):
        argnames = inspect.getfullargspec(self.data.func).args
        variables = [init_vhdl_type(name, val, val)
                     for name, val in self.data.get_local_types().items()
                     if name not in argnames]

        variables = [f'variable {x._pyha_name()}: {x._pyha_type()};' for x in variables if x is not None]
        return '\n'.join(variables)

    def build_body(self):
        return '\n'.join(str(x) for x in self.value)

    def build_function(self, prototype_only=False):
        template = textwrap.dedent("""\
            procedure {NAME}{ARGUMENTS} is
            {MULTILINE_COMMENT}
            {VARIABLES}
            begin
            {BODY}
            end procedure;""")

        args = f'({self.arguments})' if len(self.arguments) > 2 else ''
        sockets = {'NAME': self.name,
                   'MULTILINE_COMMENT': self.multiline_comment,
                   'ARGUMENTS': args,
                   'VARIABLES': tabber(self.variables) if self.variables else '',
                   'BODY': tabber(self.build_body())}

        if prototype_only:
            return template.format(**sockets).splitlines()[0][:-3] + ';'
        return template.format(**sockets)

    def __str__(self):
        return self.build_function()


class DefArgumentNodeVHDL(NodeVHDL):
    # this node is not used. arguments are inferred from datamodel!
    pass


class PassNodeVHDL(NodeVHDL):
    def __str__(self):
        return ''


class CallNodeVHDL(NodeVHDL):
    def __str__(self):
        base = '(' + ', '.join(str(x) for x in self.value) + ')'

        # find if this call is part of assignment node or AssociativeParenthesisNode
        p = self.red_node.parent
        is_assign = False
        is_assoc = False
        while p is not None:
            if type(p) == AssignmentNode:
                is_assign = True
                break
            if type(p) == AssociativeParenthesisNode:
                is_assoc = True
                break
            p = p.parent

        if not is_assign and not is_assoc and isinstance(self.red_node.next_recursive, (EndlNode, CommentNode)):
            if not isinstance(self.red_node.parent.parent, CallArgumentNode):  # dont add ; for last argument
                base += ';'
        return base


class CallArgumentNodeVHDL(NodeVHDL):
    def __str__(self):
        # transform keyword arguments, = to =>
        if self.target is not None:
            return f'{self.target}=>{self.value}'

        return str(self.value)


class IntNodeVHDL(NodeVHDL):
    pass


class FloatNodeVHDL(NodeVHDL):
    pass


class UnitaryOperatorNodeVHDL(NodeVHDL):
    def __str__(self):
        if self.value == '-':
            return f'{self.value}{self.target}'  # eg. -1
        else:
            return f'{self.value} {self.target}'  # eg. not self.val


class AssertNodeVHDL(NodeVHDL):
    def __str__(self):
        return '--' + super().__str__()


class PrintNodeVHDL(NodeVHDL):
    def __str__(self):
        if isinstance(self.red_node.value[0], TupleNode):
            raise Exception(f'{self.red_node} -> print only supported with one Sfix argument!')
        return f"report to_string{self.value[0]};"


class ListNodeVHDL(NodeVHDL):
    def __str__(self):
        if len(self.value) == 1:
            return str(self.value[0])  # [a] -> a
        else:
            ret = f'({", ".join(str(x) for x in self.value)})'
            return ret


class EndlNodeVHDL(NodeVHDL):
    def __str__(self):
        if isinstance(self.red_node.previous_rendered, CommentNode):
            return '--' + str(self.red_node.previous_rendered)[1:]
        return ''


class HexaNodeVHDL(NodeVHDL):
    def __str__(self):
        return f'16#{self.value[2:]}#'


class CommentNodeVHDL(NodeVHDL):
    def __str__(self):
        return '--' + self.value[1:]


class StringNodeVHDL(NodeVHDL):
    """ Multiline comments come here """

    def __str__(self):
        if self.value[:3] == '"""' and self.value[-3:] == '"""':
            r = [x.strip() for x in self.value[3:-3].splitlines()]
            r = '\n-- '.join(x for x in r if x != '')
            return '-- ' + r

        return self.value[1:]


# this is mostly array indexing
class GetitemNodeVHDL(NodeVHDL):
    # turn python [] indexing to () indexing

    def get_index_target(self):
        ret = ''
        for x in self.parent.value:
            if x is self:
                break
            ret += '.' + str(x)
        return ret[1:]

    def is_negative_indexing(self, obj):
        try:
            return isinstance(obj, UnitaryOperatorNodeVHDL) and int(str(obj)) < 0
        except ValueError:
            return False

    def __str__(self):
        if self.is_negative_indexing(self.value):
            target = self.get_index_target()
            return f"({target}'length{self.value})"

        return f'({self.value})'


class SliceNodeVHDL(GetitemNodeVHDL):
    def get_index_target(self):
        return '.'.join(str(x) for x in self.parent.parent.value[:-1])

    # Example: [0:5] -> (0 to 4)
    # x[0:-1] -> x(0 to x'high-1)
    def __str__(self):
        if self.upper is None:
            upper = f"{self.get_index_target()}'high"
        else:
            # vhdl includes upper limit, subtract one to get same behaviour as in python
            upper = f'({self.upper})-1'

        if self.is_negative_indexing(self.upper):
            target = self.get_index_target()
            upper = f"{target}'high{self.upper}"

        lower = 0 if self.lower is None else self.lower
        return f'{lower} to {upper}'


class ForNodeVHDL(NodeVHDL):
    def __str__(self):
        template = textwrap.dedent("""\
                for {ITERATOR} in {RANGE} loop
                {BODY}
                end loop;""")

        sockets = {'ITERATOR': str(self.iterator)}
        sockets['RANGE'] = self.range_to_vhdl(str(self.target))
        sockets['BODY'] = '\n'.join(tabber(str(x)) for x in self.value)
        return template.format(**sockets)

    def range_to_vhdl(self, pyrange):
        # this for was transforemed by 'redbaron_pyfor_to_vhdl'
        if str(self.iterator) == '\\_i_\\':
            return f"{pyrange}'range"

        range_len_pattern = parse("\\range\\({}'length)", pyrange)
        if range_len_pattern is not None and ',' not in range_len_pattern[0]:
            return range_len_pattern[0] + "'range"
        else:
            range_pattern = parse('\\range\\({});', pyrange)
            if range_pattern is None:
                range_pattern = parse('\\range\\({})', pyrange)

            if range_pattern is not None:
                two_args = parse('{},{}', range_pattern[0])
                if two_args is not None:
                    # todo: handle many more cases
                    len = parse("{}'length", two_args[1].strip())
                    if len is not None:
                        return f"{two_args[0].strip()} to ({len[0]}'length) - 1"

                    len = parse("{}'length{}", two_args[1].strip())
                    if len is not None:
                        return f"{two_args[0].strip()} to ({len[0]}'length{len[1]}) - 1"

                    return f'{two_args[0].strip()} to ({two_args[1].strip()}) - 1'
                else:
                    len = parse("{}'length{}", range_pattern[0])
                    if len is not None:
                        return f"0 to ({len[0]}'length{len[1]}) - 1"
                    return f'0 to ({range_pattern[0]}) - 1'

        # at this point range was not:
        # range(len(x))
        # range(x)
        # range(x, y)
        # assume
        assert 0


class ClassNodeVHDL(NodeVHDL):
    def __init__(self, red_node, parent=None):
        super().__init__(red_node, parent)

        # todo: remove me after refactorings
        try:
            self.data = VHDLModule('-', convert_obj)
        except AttributeError:
            self.data = None
        # collect multiline comment
        self.multiline_comment = ''
        if len(self.value) and isinstance(self.value[0], StringNodeVHDL):
            self.multiline_comment = str(self.value[0])
            del self.value[0]

    def get_function(self, name):
        f = [x for x in self.value if str(x.name) == name]
        assert len(f)
        return f[0]

    def build_imports(self):
        template = textwrap.dedent("""\
            library ieee;
                use ieee.std_logic_1164.all;
                use ieee.numeric_std.all;
                use ieee.fixed_float_types.all;
                use ieee.fixed_pkg.all;
                use ieee.math_real.all;

            library work;
                use work.complex_pkg.all;
                use work.PyhaUtil.all;
                use work.Typedefs.all;
                use work.all;
            {IMPORTS}""")

        # add all converted classes to imports
        # look: https://github.com/tgingold/ghdl/issues/209
        from pyha.conversion.conversion import RecursiveConverter
        imports = [f'use work.{x}.all;' for x in RecursiveConverter.converted_modules.keys()]
        return template.format(IMPORTS=formatter(imports))

    def build_constructor(self, prototype_only=False):
        template = textwrap.dedent("""\
            function {NAME}{ARGS} return self_t is
                -- constructor
                variable self: self_t;
            begin
            {DATA}
                return self;
            end function;""")

        data = [x._pyha_constructor() for x in self.data.elems]
        args = '; '.join([x._pyha_constructor_arg() for x in self.data.elems if x._pyha_constructor_arg() != ''])
        if args != '':
            args = f'({args[:-2]})' if args[-2:] == '; ' else f'({args})'
        ret = template.format(NAME=escape_reserved_vhdl(self.name), ARGS=args, DATA=formatter(data))

        if prototype_only:
            return ret.splitlines()[0][:-3] + ';'
        return ret

    def build_data_structs(self):
        template = textwrap.dedent("""\
            type self_t is record
            {DATA}
            end record;""")

        data = [x._pyha_definition() for x in self.data.elems if not is_constant(x._name)]
        if not data:
            data = ['dummy: integer;']
        return template.format(DATA=formatter(data))

    def build_constants(self):
        template = textwrap.dedent("""\
            type self_t_const is record
            {DATA}
            end record;""")

        data = [x._pyha_definition() for x in self.data.elems if const_filter(x)]

        if not data:
            data = ['DUMMY: integer;']
        return template.format(DATA=formatter(data))

    def build_typedefs(self):
        # self typedefs
        typedefs = [x._pyha_typedef() for x in self.data.elems if x._pyha_typedef() is not None]

        # local vars
        for function in self.value:
            if not isinstance(function, DefNodeVHDL):
                continue
            variables = [init_vhdl_type(name, val, val) for name, val in function.data.get_local_types().items()]
            typedefs += [x._pyha_typedef() for x in variables if x is not None and x._pyha_typedef() is not None]
        typedefs = list(dict.fromkeys(typedefs))  # get rid of duplicates
        return typedefs

    def build_package_header(self):
        template = textwrap.dedent("""\
            {MULTILINE_COMMENT}
            package {NAME} is
            {SELF_T}
            {SELF_ARRAY_TYPEDEF}
            
            {CONST_SELF_T}
            {CONST_SELF_ARRAY_TYPEDEF}

            {FUNC_HEADERS}
            end package;""")

        sockets = {}
        sockets['MULTILINE_COMMENT'] = self.multiline_comment
        sockets['NAME'] = self.data._pyha_module_name()

        # data-structure without constants ie. registers
        sockets['SELF_T'] = tabber(self.build_data_structs())
        sockets[
            'SELF_ARRAY_TYPEDEF'] = f'    type {self.data._pyha_arr_type_name()} is array (natural range <>) of {self.data._pyha_type()};'

        # only constants
        with TypeAppendHack('_const'):
            sockets['CONST_SELF_T'] = tabber(self.build_constants())
            sockets[
                'CONST_SELF_ARRAY_TYPEDEF'] = f'    type {self.data._pyha_arr_type_name()} is array (natural range <>) of {self.data._pyha_type()};'

        proto = '\n'.join(x.build_function(prototype_only=True) for x in self.value if isinstance(x, DefNodeVHDL))
        proto += '\n' + self.build_constructor(prototype_only=True) + '\n'
        sockets['FUNC_HEADERS'] = tabber(proto)

        return template.format(**sockets)

    def build_package_body(self):
        template = textwrap.dedent("""\
            package body {NAME} is
            {USER_FUNCTIONS}
            
            {CONSTRUCTOR}
            end package body;""")

        sockets = {}
        sockets['NAME'] = self.data._pyha_module_name()

        sockets['CONSTRUCTOR'] = tabber(self.build_constructor())
        # sockets['INIT_SELF'] = tabber(self.build_init())
        # sockets['CONSTANT_SELF'] = tabber(self.build_reset_constants())
        # sockets['RESET_SELF'] = tabber(self.build_reset())
        # sockets['UPDATE_SELF'] = tabber(self.build_update_registers())
        sockets['USER_FUNCTIONS'] = '\n\n'.join(tabber(str(x)) for x in self.value if isinstance(x, DefNodeVHDL))

        return template.format(**sockets)

    def __str__(self):
        template = textwrap.dedent("""\
            {FILE_HEADER}
            {IMPORTS}

            {PACKAGE_HEADER}

            {PACKAGE_BODY}
            """)

        sockets = {}
        sockets['FILE_HEADER'] = file_header()
        sockets['IMPORTS'] = self.build_imports()
        sockets['PACKAGE_HEADER'] = self.build_package_header()
        sockets['PACKAGE_BODY'] = self.build_package_body()
        return template.format(**sockets)


def redbaron_node_to_vhdl_node(red: Node, caller):
    """ Convert RedBaron class to conversion class
    For example: red:NameNode returns NameNodeVHDL class
    """
    import sys

    red_type = red.__class__.__name__
    try:
        cls = getattr(sys.modules[__name__], red_type + 'VHDL')
    except AttributeError:
        if red_type == 'NoneType':
            return None
        raise

    return cls(red_node=red, parent=caller)


convert_obj = None


def set_convert_obj(obj):
    global convert_obj
    convert_obj = obj


def convert(red: Node, obj=None):
    set_convert_obj(obj)

    # delete all non convertable functions from redbaron AST
    # coding style is akward because of some redbaron bugs...
    while True:
        f = red.find('def', name=lambda x: x in SKIP_FUNCTIONS or x[:2] == '__' or x[:5] == '_pyha')
        if not f:
            break
        f.parent.remove(f)

    # delete functions that were not simulated
    # problem was that some transforms may still parse (e.g find all assignments in the design) these and run into trouble as no type info exists
    if obj is not None:
        for k, v in obj.__dict__.items():
            if isinstance(v, PyhaFunc):
                if not v.calls:
                    f = red.find('def', name=lambda x: x == k)
                    if not f:
                        continue
                    logger.warning(f'Not converting function {k}, was not called in simulation!')
                    f.parent.remove(f)


    # run RedBaron based conversions before parsing
    transform_preprocessor(red)
    transform_resize_arguments(red)
    transform_remove_copy(red)
    transform_for(red)
    if obj is not None:
        transform_dynamic_lists(red)
        transform_unroll_local_constructor(red)

    transform_call(red)
    transform_multiple_assignment(red)
    transform_lazy_operand(red)
    transform_int_cast(red)
    if obj is not None:
        transform_enum(red)
        transform_registers(red)
        transform_fixed_indexing_result_to_bool(red)
        transform_auto_resize(red)
    transform_complex_real_imag(red)
    transform_constants(red)

    conv = redbaron_node_to_vhdl_node(red, caller=None)  # converts all nodes

    return conv


#################### FUNCTIONS THAT MODIFY REDBARON AST #############
#####################################################################
#####################################################################
#####################################################################
#####################################################################
#####################################################################


def super_getattr(obj, attr, is_local=False):
    for part in attr.split('.'):
        if not is_local:
            if part == 'self' or part == 'self_next':
                continue

        if part.find('[') != -1:  # is array indexing
            try:
                index = int(part[part.find('[') + 1: part.find(']')])
            except ValueError:  # cant convert to int :(
                index = 0

            part = part[:part.find('[')]

            if isinstance(getattr(obj, part), Sfix):  # indexing of sfix bits..
                return 'FIXED_INDEXING_HACK'

            obj = getattr(obj, part)[index]  # just take first array element, because the index may be variable
        elif part.find(']') != -1:
            # this can happen if array index includes '.' so arr.split makes false split. example: self.a[self.b]
            continue
        else:
            try:
                obj = getattr(obj, part)
            except AttributeError:
                # obj might be dynamic array that has transformed indexing eg. a[0] -> a_0
                try:
                    index = int(part[part.find('_') + 1:])
                    name = part[:part.find('_')]
                    obj = getattr(obj, name)[index]
                except:
                    raise AttributeError

    return obj


def get_object(node):
    """ Parse rebaron AtomTrailers node into Python object (taken from ongoing conversion object)
     Works for object and local scope """

    if len(node) > 1 and (node[0].value == 'self' or node[0].value == 'self_next'):
        var_t = super_getattr(convert_obj, str(node))
    else:
        # get the SOURCE function (where call is going on) from datamodel
        def_parent = node.parent
        while not isinstance(def_parent, DefNode):
            def_parent = def_parent.parent

        source_func_name = f'self.{def_parent.name}'
        source_func_obj = super_getattr(convert_obj, str(source_func_name))

        func_locals = source_func_obj.get_local_types()

        class Struct:
            def __init__(self, **entries):
                self.__dict__.update(entries)

        struct = Struct(**func_locals)
        var_t = super_getattr(struct, str(node), is_local=True)

    return var_t


def transform_unroll_local_constructor(red_node):
    assigns = red_node.find_all('assign')
    for node in assigns:
        call = node.value.call
        if call is None:  # has no function call
            continue

        call_index = call.previous.index_on_parent
        if call_index != 0:  # input is something complicated..we only consider simple calls like 'Complex(..)'
            continue

        obj = get_object(node.target)
        if not isinstance(obj, Hardware):
            continue

        call_name = str(node.value[0])
        res_type = obj.__class__.__name__
        if res_type != call_name:  # not a call to constructor
            continue

        correct_indentation = node.indentation
        new = 'if True:\n'  # replace needs a BLOCK, so this is a dummy IF
        target = str(node.target)
        for i, (k, v) in enumerate(obj.__dict__.items()):
            if k.startswith('_pyha'):
                continue

            new += f'{correct_indentation}\t{target}.{k} = {call[i].value}\n'

        node.replace(new)


def transform_constants(red_node):
    nodes = red_node.find_all('atomtrailers')
    for node in nodes:
        const = any([is_constant(x.dumps()) for x in node])
        if const and node[0].dumps() == 'self':
            node[0].replace('self_const')


def transform_preprocessor(red_node):
    nodes = red_node.find_all('comment', value=lambda x: x == '# CONVERSION PREPROCESSOR replace next line with:')

    for x in nodes:
        new = str(x.parent[x.index_on_parent + 1].value)[2:]
        x.parent[x.index_on_parent + 2].replace(new)
        # x.replace(f'{x.target} = {x.target} {x.operator} {x.value}')


def transform_resize_arguments(red_node):
    """ Replace 'wrap' -> fixed_wrap
        'saturate' -> fixed_saturate
        'truncate' -> fixed_truncate
        'round' -> fixed_round

        Force default fixed_wrap and fixed_truncate.
    """
    nodes = red_node.find_all('call')

    for call in nodes:
        call_index = call.previous.index_on_parent
        if call_index != 0:  # not just resize().. maybe self.resize() etc...
            continue

        if call.previous.value not in ['resize', 'Sfix']:
            continue

        args = call.find_all('call_argument')
        overflow_kword_found = False
        round_kword_found = False
        for arg in args:
            if str(arg.value) == "'wrap'":
                arg.value = 'fixed_wrap'
                arg.target = 'overflow_style'
                overflow_kword_found = True

            if str(arg.value) == "'saturate'":
                arg.value = 'fixed_saturate'
                arg.target = 'overflow_style'
                overflow_kword_found = True

            if str(arg.value) == "'truncate'":
                arg.value = 'fixed_truncate'
                arg.target = 'round_style'
                round_kword_found = True

            if str(arg.value) == "'round'":
                arg.value = 'fixed_round'
                arg.target = 'round_style'

                round_kword_found = True

        # force defaults if not set
        if not overflow_kword_found:
            call.append(f'overflow_style=fixed_wrap')

        if not round_kword_found:
            call.append(f'round_style=fixed_truncate')


def transform_int_cast(red_node):
    """ Convert int() to to_integer(round_syle=fixed_truncate, overflow_style=fixed_wrap) for VHDL. """
    nodes = red_node.find_all('call')
    for call in nodes:
        call_index = call.previous.index_on_parent
        if call_index != 0:  # not just copy().. maybe self.copy() etc...
            continue

        if call.previous.value == 'int':
            call.previous.value = 'to_integer'
            call.append('round_style=fixed_truncate')
            call.append('overflow_style=fixed_wrap')


def transform_remove_copy(red_node):
    """ Remove copy() and deepcopy() calls, in VHDL everything is deepcopy by default
    """
    nodes = red_node.find_all('call')
    for call in nodes:
        call_index = call.previous.index_on_parent
        if call_index != 0:  # not just copy().. maybe self.copy() etc...
            continue

        if call.previous.value not in ['copy', 'deepcopy']:
            continue

        call.parent.replace(call.value.dumps())


def transform_complex_real_imag(red_node):
    nodes = red_node.find_all('atomtrailers')
    for node in nodes:
        if str(node[-1]) == 'real':
            del node[-1]
            node.replace(f'get_real({node.dumps()})')
        elif str(node[-1]) == 'imag':
            del node[-1]
            node.replace(f'get_imag({node.dumps()})')


def transform_multiple_assignment(red_node):
    """ Multi target assigns to single target:
    a, b, c = 1, 2, 3 ->
    a = 1
    b = 2
    c = 3
    """
    nodes = red_node.find_all('assign', target=lambda x: isinstance(x, TupleNode))

    for x in nodes:
        for i, (target, value) in enumerate(zip(x.target, x.value)):
            if i == len(x.target) - 1:
                x.replace(f'{target} = {value}')
            else:
                x.parent.insert(x.index_on_parent, f'{target} = {value}')


def transform_lazy_operand(red_node):
    """ *=, += .. etc:
    a *= b ->
    a = a * b
    """
    nodes = red_node.find_all('assign', operator=lambda x: x != '')
    for x in nodes:
        x.replace(f'{x.target} = {x.target} {x.operator} {x.value}')


def transform_auto_resize(red_node):
    """ Auto resize on Sfix assignments
     Examples (depend on initial Sfix type):
         self.sfix_reg = a        ->   self.sfix_reg = resize(a, 5, -29, fixed_wrap, fixed_round)
         self.sfix_list[0] = a    ->   self.sfix_list[0] = resize(a, 0, 0, fixed_saturate, fixed_round)
         """
    """ Resize stuff should happen on Sfix registers only, filter others out """
    """ Wrap all subjects to autosfix inside resize() according to initial type """
    """ When assignment target is sfix indexing ie. sfix[2], converts value to 'to_std_logic(value)' """

    nodes = red_node.find_all('assign')

    for node in nodes:

        var_t = get_object(node.target)

        if var_t == 'FIXED_INDEXING_HACK':
            node.value = f'to_std_logic({node.value})'
        elif isinstance(var_t, (Sfix)):
            if isinstance(node.value, (FloatNode, IntNode)) \
                    or (isinstance(node.value, UnitaryOperatorNode) and isinstance(node.value.target, (
                    FloatNode, IntNode))):  # second term to pass marked(-) nodes, like -1. -0.34 etc
                if var_t.signed:
                    node.value = f'Sfix({node.value}, {var_t.left}, {var_t.right})'
                else:
                    node.value = f'Ufix({node.value}, {var_t.left-1}, {var_t.right})'
            else:
                if var_t.signed:
                    node.value = f'resize({node.value}, {var_t.left}, {var_t.right}, fixed_{var_t.overflow_style}, fixed_{var_t.round_style})'
                else:
                    node.value = f'resize({node.value}, {var_t.left-1}, {var_t.right}, fixed_{var_t.overflow_style}, fixed_{var_t.round_style})'
        elif isinstance(var_t, (Complex)):
            if isinstance(node.value, (BinaryOperatorNode)) \
                    and (isinstance(node.value.second, BinaryOperatorNode) and isinstance(node.value.second.second, ComplexNode)):  # handle mul case: 1+1*1j
                node.value = f'Complex({node.value.first}, {node.value.value}{node.value.second.first}, {var_t.left}, {var_t.right})'
            elif isinstance(node.value, (BinaryOperatorNode)) and isinstance(node.value.second,
                                                                             ComplexNode):  # normal case: 1+1j
                node.value = f'Complex({node.value.first}, {node.value.value}{node.value.second.value[:-1]}, {var_t.left}, {var_t.right})'
            elif isinstance(node.value, (FloatNode)):
                node.value = f'Complex({node.value}, {var_t.left}, {var_t.right})'
            else:
                node.value = f'resize({node.value}, {var_t.left}, {var_t.right}, fixed_{var_t.overflow_style}, fixed_{var_t.round_style})'


def transform_fixed_indexing_result_to_bool(red_node):
    """ VHDL indexing of fixed point value returns 'std_logic' type, this casts such assignments to bool() """

    nodes = red_node.find_all('atomtrailers')

    for node in nodes:
        try:
            if node.parent.target == node:  # this node is assignment target ie. NODE = value
                continue
        except:
            pass
        try:
            var_t = get_object(node)
        except AttributeError:  # can happen when node is Sfix()
            continue

        if var_t == 'FIXED_INDEXING_HACK':
            node.value = f'bool({node})'


def transform_registers(red_node):
    def add_next(x):
        if len(x) > 1 and str(x[0].value) == 'self':
            x[0].replace('self_next')

    assigns = red_node.find_all('assign')
    for node in assigns:
        if isinstance(node.target, TupleNode):
            for mn in node.target:
                add_next(mn)
        else:
            add_next(node.target)


def transform_call(red_node):
    """
    Converts Python style function calls to VHDL style:
    self.d(a) -> d(self, a)

    If function owner is not exactly 'self' then 'type' is prepended.
    self.next.moving_average.main(x) -> type.main(self.next.moving_average, x)

    self.d(a) -> d(self, a)
    self.next.d(a) -> d(self.next, a)
    local.d() -> type.d(local)
    self.local.d() -> type.d(self.local)

    If return then:

    b = self.a(arg) ->

        variable pyha_ret_0: type;

        a(self, arg, pyha_ret_0);
        b := pyha_ret_0;

    Handling call inside call is limited to depth 1.

    """

    def find_line_node(red_obj):
        line_node = red_obj
        while True:
            if type(line_node.next) == EndlNode:
                break
            if hasattr(line_node.parent, 'value') and type(line_node.parent.value) == LineProxyList:
                if not (hasattr(line_node.parent, 'test') and (
                        line_node.parent.test == atom  # if WE are the if condition, skip
                        or line_node.parent.test == atom.parent)):  # if WE are the if condition (part of condition)
                    break

            line_node = line_node.parent
        return line_node

    is_hack = False

    # make sure each created variable is unique by appending this number and incrementing
    tmp_var_count = 0

    # loop over all atomtrailers, call is always a member of this
    atomtrailers = red_node.find_all('atomtrailers')
    for i, atom in enumerate(atomtrailers):
        if is_hack:  # when parsed out of order call
            atom = atomtrailers[i - 1]
            call = atom.call
            is_hack = False
        else:
            call = atom.call  # this actually points to the stuff between ()

        if call is None:  # this atomtrailer has no function call
            continue

        wat = call.call
        if wat is not None:  # one of the arguments is a call -> process it first (i expect it is next in the list)

            call_index = wat.previous.index_on_parent
            if call_index == 0:  # input is something like x() -> len(), Sfix() ....
                pass
            else:
                try:
                    atom = atomtrailers[i + 1]
                    call = atom.call
                    is_hack = True
                except:
                    continue # no idea what is going on here...

                if call is None:  # this atomtrailer has no function call
                    continue

        call_index = call.previous.index_on_parent
        if call_index == 0:  # input is something like x() -> len(), Sfix() ....
            continue

        # get the TARGET function object from datamodel
        target_func_name = atom.copy()
        del target_func_name[call_index + 1:]
        try:
            target_func_obj = super_getattr(convert_obj, str(target_func_name))
        except:  # happend for: (self.conjugate(complex_in) * complex_in).real
            continue

        if not target_func_obj.calls:
            # function is not simulated...
            line_node = find_line_node(atom)
            line_node.replace(f'# comment out because not called in simulation: {line_node.dumps()}')
            continue

        prefix = atom.copy()
        del prefix[call_index:]
        del atom[:call_index]

        tmp = prefix.copy()
        if isinstance(tmp[0], AtomtrailersNode):
            # this branch happens because of 'for transform'
            tmp[0][0] = 'self_const'
            call.insert(0, tmp)
        else:
            tmp[0] = 'self_const'
            call.insert(0, tmp)

        tmp = prefix.copy()
        if isinstance(tmp[0], AtomtrailersNode):
            tmp[0][0] = 'self_next'
            call.insert(0, tmp)
        else:
            tmp[0] = 'self_next'
            call.insert(0, tmp)

        tmp = prefix.copy()
        if isinstance(tmp[0], AtomtrailersNode):
            tmp[0][0] = 'self'
            call.insert(0, tmp)
        else:
            tmp[0] = 'self'
            call.insert(0, tmp)

        # get the SOURCE (where call is going on) function object from datamodel
        def_parent = atom
        while not isinstance(def_parent, DefNode):
            def_parent = def_parent.parent
        # def_parent = atom.parent_find('def')
        source_func_name = f'self.{def_parent.name}'
        source_func_obj = super_getattr(convert_obj, str(source_func_name))

        # if call is not to local class function
        # self.moving_average.main(x) -> MODULE_NAME.main(self.moving_average, x)
        if str(prefix) != 'self':
            var = super_getattr(convert_obj, str(prefix))
            var = init_vhdl_type('-', var, var)
            atom.insert(0, var._pyha_module_name())

        if target_func_obj.get_output_types() is None:
            continue  # function is not returning stuff -> this is simple
        else:

            # add return variables to function locals, so that they will be converted to VHDL variables
            ret_vars = []
            for x in get_iterable(target_func_obj.get_output_types()):
                name = f'pyha_ret_{tmp_var_count}'
                ret_vars.append(name)
                source_func_obj.add_local_type(name, x)
                tmp_var_count += 1

                # add return variable to arguments
                call.append(name)
                # call.value[-1].target = f'ret_{j}'

            # need to add new source line before the CURRENT line..search for the node with linenodes
            line_node = find_line_node(atom)

            # add function call BEFORE the CURRENT line
            if line_node != atom:  # equality means that value is not assigned to anything
                line_node.parent.insert(line_node.index_on_parent, atom.copy())
                atom.replace(','.join(ret_vars))


def transform_for(red_node):
    def modify_for(red_node):
        # if for range contains call to 'range' -> skip
        with suppress(Exception):
            if red_node.target('call')[0].previous.value == 'range':
                return

        ite = red_node.iterator
        red_node(ite.__class__.__name__, value=ite.value) \
            .map(lambda x: x.replace(f'{red_node.target}[_i_]'))

        red_node.iterator = '_i_'

    fors = red_node.find_all('for')
    for x in fors:
        modify_for(x)


def transform_enum(red_node):
    """
    Converts 'EnumType.ENUMVALUE' to integer value , see #154
    """
    data = VHDLModule('-', convert_obj)
    enums = [x for x in data.elems if isinstance(x, VHDLEnum)]
    for x in enums:
        type_name = x._pyha_type()
        red_names = red_node.find_all('atomtrailers', value=lambda x: x[0].value == type_name)
        for i, node in enumerate(red_names):
            enum_obj = type(x.current)[str(node[1])]
            red_names[i].replace(str(enum_obj.value))


def transform_dynamic_lists(red_node):
    data = VHDLModule('-', convert_obj)

    dynamic_lists = [x for x in data.elems if isinstance(x, VHDLList) and not x.elements_compatible_typed]
    for x in dynamic_lists:
        name = x._name
        red_names = red_node.find_all('atomtrailers')
        for node in red_names:
            for i, part in enumerate(node):
                if str(part) == name and isinstance(part.next, GetitemNode):
                    try:
                        index = int(str(part.next.value))
                        part.replace(f'{name}_{index}')
                        del node[i + 1]
                    except ValueError:

                        line_node = node
                        while True:
                            if type(line_node.next) == EndlNode:
                                break
                            if hasattr(line_node.parent, 'value') and type(line_node.parent.value) == LineProxyList:
                                if not (hasattr(line_node.parent, 'test') and (
                                        line_node.parent.test == node  # if WE are the if condition, skip
                                        or line_node.parent.test == node.parent)):  # if WE are the if condition (part of condition)
                                    break

                            line_node = line_node.parent

                        index = str(part.next.value)
                        node[i + 1].replace(' ')  # del node[i+1], crashes redbaron

                        correct_indentation = line_node.indentation
                        new = 'if True:\n'  # replace needs a BLOCK, so this is a dummy IF
                        for i in range(len(x.elems)):
                            part.replace(f'{name}_{i}')

                            head = 'if' if i == 0 else 'elif'
                            new += f'{correct_indentation}\t{head} {index} == {i}:\n{correct_indentation}\t\t{line_node}\n'

                        line_node.replace(new)