Source code for m2isar.metamodel.arch

# 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

"""This module contains classes for modeling the architectural part
of an M2-ISA-R model. The architectural part is anything but the functional
behavior of functions and instructions.
"""

import dataclasses
import itertools
from collections import defaultdict
from typing import TYPE_CHECKING, Union
from m2isar.frontends.coredsl2.expr_interpreter import ExprInterpreterVisitor
from m2isar.metamodel import type_info, attribute_info

from .. import M2TypeError
from .behav import BaseNode, IndexedReference, Operation, Literal

if TYPE_CHECKING:
	from .code_info import FunctionInfo
[docs] exprInterpretVisitor = ExprInterpreterVisitor()
[docs] def get_const_or_val(arg) -> int: if isinstance(arg, Parameter): return arg.value if isinstance(arg, Literal): arg = int(arg.value) if isinstance(arg, BaseNode): arg = exprInterpretVisitor.generate(arg, None) return arg
[docs] class Named: """A simple base class for a named object."""
[docs] name: str
"""The name of the object.""" def __init__(self, name: str): self.name = name
[docs] def __str__(self) -> str: return f'<{type(self).__name__} object>: name={self.name}'
[docs] def __repr__(self) -> str: return f'<{type(self).__name__} object>: name={self.name}'
[docs] ValOrConst = Union[int, "Parameter"]
[docs] class SizedRefOrConst(Named): """A simple base class for an object with a name and a size. Size can be either an int, a Parameter or a statically resolvable expression, expressed by a BaseNode. """
[docs] _size: Union[int, "Parameter", "BaseNode"]
"""The size of the object""" def __init__(self, name, size: ValOrConst): self._size = size super().__init__(name) @property
[docs] def size(self) -> int: """Returns the resolved size, by calling get_const_or_val on _size.""" ret = get_const_or_val(self._size) if ret is None: return None return int(ret)
[docs] def __str__(self) -> str: return f'{super().__str__()}, size={self.size}'
[docs] class Parameter(SizedRefOrConst): """An object holding a ("Parameter") Parameter value. Should have a value at some point, also holds attributes and signedness information. """
[docs] _value: Union[int, "Parameter", "BaseNode"]
"""The value this object holds. Can be an int, another constant or a statically resolvable BaseNode."""
[docs] attributes: "dict[attribute_info.ConstAttribute, list[BaseNode]]"
"""A dictionary of attributes, mapping attribute type to a list of attribute arguments."""
[docs] signed: bool
"""The signedness of this constant.""" def __init__(self, name, value: Union[int, "Parameter", "BaseNode"], attributes: "dict[attribute_info.ConstAttribute, list[BaseNode]]", size=None, signed=False): self._value = value self.attributes = attributes if attributes else {} self.signed = signed super().__init__(name, size) @property
[docs] def value(self): """Returns the resolved value this constant holds.""" return get_const_or_val(self._value)
@value.setter def value(self, value): self._value = value
[docs] def __str__(self) -> str: return f'{super().__str__()}, value={self.value}'
[docs] def __repr__(self) -> str: return f'{super().__repr__()}, value={self.value}'
[docs] class RangeSpec: """A class holding a range to denote a range of indices or width of a memory bank."""
[docs] _upper_base: Union[int, "Parameter", "BaseNode"]
"""The upper bound of the range. Can be an int, a constant or a statically resolvable BaseNode."""
[docs] _lower_base: Union[int, "Parameter", "BaseNode"]
"""The lower bound of the range. Can be an int, a constant or a statically resolvable BaseNode."""
[docs] _upper_power: Union[int, "Parameter", "BaseNode"]
"""Obsolete, do not use"""
[docs] _lower_power: Union[int, "Parameter", "BaseNode"]
"""Obsolete, do not use""" def __init__(self, upper_base: ValOrConst, lower_base: ValOrConst=None, upper_power: ValOrConst=1, lower_power: ValOrConst=1): self._upper_base = upper_base self._lower_base = lower_base self._upper_power = upper_power self._lower_power = lower_power @property
[docs] def upper_power(self): """Returns the resolved upper bound power.""" return get_const_or_val(self._upper_power)
@property
[docs] def lower_power(self): """Returns the resolved lower bound power.""" return get_const_or_val(self._lower_power)
@property
[docs] def upper_base(self): """Returns the resolved upper bound base.""" return get_const_or_val(self._upper_base)
@property
[docs] def lower_base(self): """Returns the resolved lower bound base.""" return get_const_or_val(self._lower_base)
@property
[docs] def upper(self) -> Union[int, None]: """Returns the resolved upper power.""" if self.upper_base is None or self.upper_power is None: return None ret = self.upper_base ** self.upper_power if self.lower_base is None or self.lower_power is None: return ret - 1 return ret
@property
[docs] def lower(self) -> int: """Returns the resolved lower power.""" if self.lower_base is None or self.lower_power is None: return 0 return self.lower_base ** self.lower_power
@property
[docs] def length(self): """Returns the length of the range using following algorithm: if self.upper is None: return None elif self.lower is None: return self.upper else return self.upper - self.lower + 1 """ if self.upper is None: return None if self.lower is None: return self.upper return self.upper - self.lower + 1
[docs] def __str__(self) -> str: return f'<RangeSpec object>, len {self.length}: {self.upper_base}:{self.lower_base}'
[docs] class FnParam(Named): """A function parameter."""
[docs] ty: type_info.PrimitiveType
[docs] _width: Union[int, "Parameter", "BaseNode"]
"""The array width of this parameter.""" def __init__(self, name, size, kind: type_info.TypeKind, width=1): self.ty = type_info.PrimitiveType(kind, size) self._width = width super().__init__(name) @property
[docs] def width(self): """Returns the resolved array width value.""" return get_const_or_val(self._width)
[docs] def __str__(self) -> str: return f'{super().__str__()}, type={self.ty}'
# =========================================================== # START # ARCHITECTURE used within the functional # behavior of instructions and functions. # ===========================================================
[docs] class Symbol(Named): """A simple base class for a symbol, which is a named object that can be used as an operand in an instruction or function.""" def __init__( self, name: str, ty: Union[type_info.PrimitiveType, type_info.FloatType, type_info.ArrayType, type_info.BitFieldType, type_info.PointerType], attributes : dict = {} ):
[docs] self.ty = ty
[docs] self.attributes = attributes
super().__init__(name)
[docs] class Variable(Symbol): """A variable is only defined in the functional behavior of a function, but not in the architectural part. Archtiectural Parts are depicted as Register Banks or Memories, but not as variables. However, we need to define variables for intermediate results, ...""" def __init__( self, name: str, ty: Union[type_info.PrimitiveType, type_info.FloatType, type_info.ArrayType], attributes: dict = {"static": attribute_info.AccessAttribute.RW}, value=None, # Compile Time information children=[] # Array support might require this ): assert isinstance(ty, (type_info.PrimitiveType, type_info.ArrayType)) if isinstance(ty, type_info.PrimitiveType): assert ty.kind.is_scalar elif isinstance(ty, type_info.ArrayType): # only allow 1D arrays for now assert ty.element_type.kind.is_scalar # optional: only for parameters/literals
[docs] self.value = value
[docs] self.children = children
assert (len(self.children) == 0 or isinstance(ty, type_info.ArrayType)) super().__init__(name, ty, attributes)
[docs] class Intrinsic(Symbol):
[docs] value: int
def __init__(self, name, size: ValOrConst, kind: type_info.TypeKind, value: int = None): self.value = value super().__init__(name, type_info.PrimitiveType(kind, get_const_or_val(size)))
[docs] class RegisterBank(Symbol): """A class representing a register bank. A register bank combines structured registers, which is used to represent registers in the architectural part of an M2-ISA-R model."""
[docs] children: "list[Memory]"
[docs] _initval: "dict[int, Union[int, Parameter, BaseNode]]"
def __init__(self, name, nr_ele: Union[int, Parameter], kind: type_info.TypeKind, size, attributes: "dict[attribute_info.MemoryAttribute, list[BaseNode]]"): self.children = [] self._initval = {} ty = type_info.ArrayType(type_info.PrimitiveType(kind, size), nr_ele) super().__init__(name, ty, attributes)
[docs] def initval(self, idx=None): """Return the initial value for the given index.""" return get_const_or_val(self._initval[idx])
@property
[docs] def is_main_reg(self): """Return true if this memory is tagged as being a general-purpose register.""" return self._is_specific_register(attribute_info.RegisterAttribute.IS_MAIN_REG, "X")
@property
[docs] def is_float_reg(self) -> bool: """Return true if this memory is tagged as being a float register array or named F.""" return self._is_specific_register(attribute_info.RegisterAttribute.IS_FLOAT_REG, "F")
@property
[docs] def is_vector_reg(self) -> bool: """Return true if this memory is tagged as being a vector register array or named V.""" return self._is_specific_register(attribute_info.RegisterAttribute.IS_VECTOR_REG, "V")
[docs] def _is_specific_register(self, register_type: attribute_info.RegisterAttribute, expected_name: str = "") -> bool: """ This is a helper function to ensure, that all checks are performed always the same. :param register_type: The register attribute qualifying for this check :param expected_name: The fixed name for this specific type of register :return: True if the register matches the constraints, False otherwise """ return register_type in self.attributes or expected_name.upper() == self.name.upper()
# be careful: This is only for single defined regs (No Alias or indexedReference)
[docs] class Register(Symbol): """A class representing a register. A register is a single defined Symbols. Dont mix it up bit alias that are IndexedReference of already declared Symbols. This class should simplify different handling to register bank. And is used to represent registers in the architectural part of an M2-ISA-R model."""
[docs] children: "list[Memory]"
[docs] _initval: "dict[Union[int, Parameter, BaseNode]]"
def __init__(self, name, kind: type_info.TypeKind, size, attributes: "dict[attribute_info.RegisterAttribute, list[BaseNode]]"): self.children = [] self._initval = {} ty = type_info.PrimitiveType(kind, size) super().__init__(name, ty, attributes)
[docs] def initval(self): """Return the initial value for the given index.""" return get_const_or_val(self._initval)
@property
[docs] def is_pc(self): """Return true if this memory is tagged as being the program counter.""" return self._is_specific_register(attribute_info.RegisterAttribute.IS_PC)
[docs] def _is_specific_register(self, register_type: attribute_info.RegisterAttribute, expected_name: str = "") -> bool: """ This is a helper function to ensure, that all checks are performed always the same. :param register_type: The register attribute qualifying for this check :param expected_name: The fixed name for this specific type of memory :return: True if the memory matches the constraints, False otherwise """ return register_type in self.attributes or expected_name.upper() == self.name.upper()
#Idea extern [const volatile]<- atleast store it #class Port -> raise ...
[docs] class Memory(Symbol): """A generic memory object. Can have children, which alias to specific indices of their parent memory. Has a variable array size, can therefore represent both scalar and array registers and/or memories. """
[docs] range: RangeSpec
[docs] children: "list[Memory]"
[docs] parent: "Union['Memory', None]"
[docs] _initval: "dict[int, Union[int, Parameter, BaseNode]]"
def __init__(self, name, kind : type_info.TypeKind, size, length, attributes: "dict[attribute_info.MemoryAttribute, list[BaseNode]]"): self.children = [] self._initval = {} self.parent = None # Just Legacy assert kind.is_numeric super().__init__(name, type_info.ArrayType(type_info.PrimitiveType(kind, size), length), attributes)
[docs] def initval(self, idx=None): """Return the initial value for the given index.""" return get_const_or_val(self._initval[idx])
@property
[docs] def data_range(self): """Returns a RangeSpec object with upper=range.upper-range.lower, lower=0.""" if self.range.upper is None or self.range.lower is None: return None return RangeSpec(self.range.upper - self.range.lower, 0)
@property
[docs] def is_csr_reg(self) -> bool: """Return true if this memory is tagged as being a csr register array or named CSR.""" return self._is_specific_memory(attribute_info.MemoryAttribute.IS_CSR_REG, "CSR")
@property
[docs] def is_main_mem(self): """Return true if this memory is tagged as being the main memory array.""" return self._is_specific_memory(attribute_info.MemoryAttribute.IS_MAIN_MEM)
[docs] def _is_specific_memory(self, memory_type: attribute_info.MemoryAttribute, expected_name: str = "") -> bool: """ This is a helper function to ensure, that all checks are performed always the same. :param memory_type: The memory attribute qualifying for this check :param expected_name: The fixed name for this specific type of memory :return: True if the memory matches the constraints, False otherwise """ return memory_type in self.attributes or expected_name.upper() == self.name.upper()
# TODO: decide later if you wanna keep lhs information : # unsigned<XLEN>& S0 = X[8]; vs # Intention: alias S0 <- X[8];
[docs] class Alias(Symbol): """A class representing an (potentially ranged) alias to a Register/Memory/RegisterBank entity, which refer to the architectural part of an M2-ISA-R model. This access might be ranged"""
[docs] parent: Union[Memory, RegisterBank]
[docs] _initval = 0
def __init__(self, name, parent: Union[Memory, RegisterBank], range: RangeSpec, type: type_info.PointerType, attributes: dict = {}): self.parent = parent
[docs] self.range = range
[docs] self.ty = type
assert isinstance(parent.ty, (type_info.ArrayType, type_info.PrimitiveType)) super().__init__(name, type, attributes) @property
[docs] def data_range(self): """Returns a RangeSpec object with upper=range.upper-range.lower, lower=0.""" if self.range.upper is None or self.range.lower is None: return None return RangeSpec(self.range.upper - self.range.lower, 0)
@property
[docs] def length(self): """Returns the length of the range using following algorithm: if self.upper is None: return None elif self.lower is None: return self.upper else return self.upper - self.lower + 1 """ if self.range.upper is None: return None if self.range.lower is None: return self.range.upper return self.range.upper - self.range.lower + 1
# ============================================================ # END # ARCHITECTURE used within the functional # behavior of instructions and functions. # ===========================================================
[docs] class BitField(Symbol): """A class representing an operand in an instruction encoding. Can be split into multiple parts, if the operand is split over two or more bit ranges. """
[docs] range: RangeSpec
[docs] kind: type_info.TypeKind
def __init__(self, name, _range: RangeSpec, kind: type_info.TypeKind): self.range = _range if not kind: self.ty = type_info.BitFieldType(type_info.TypeKind.UINT) super().__init__(name, type_info.BitFieldType(kind), attributes={})
[docs] def __str__(self) -> str: return f'{super().__repr__()}, range={self.range}, data_type={self.kind}'
[docs] def __repr__(self): return self.__str__()
@dataclasses.dataclass
[docs] class BitVal: """A class representing a fixed bit sequence in an instruction encoding. Modeled as length and integral value. """
[docs] length: int
[docs] value: int
[docs] class BitFieldDescr(Named): """A class representing a full instruction operand. Has no information about the actual bits it is composed of, for that use BitField. """ def __init__(self, name, size: ValOrConst, kind: type_info.TypeKind):
[docs] self.ty = type_info.PrimitiveType(kind, get_const_or_val(size))
super().__init__(name)
[docs] class Instruction(SizedRefOrConst): """A class representing an instruction."""
[docs] attributes: "dict[attribute_info.InstrAttribute, list[BaseNode]]"
[docs] encoding: "list[Union[BitField, BitVal]]"
[docs] mnemonic: str
[docs] assembly: str
[docs] operation: Operation
[docs] ext_name: str
[docs] fields: "dict[str, BitFieldDescr]"
[docs] vars: "dict[str, Symbol]"
[docs] throws: bool
[docs] mask: int
[docs] code: int
def __init__(self, name, attributes: "dict[attribute_info.InstrAttribute, list[BaseNode]]", encoding: "list[Union[BitField, BitVal]]", mnemonic: str, assembly: str, operation: Operation, function_info: "FunctionInfo"): self.ext_name = "" self.attributes = attributes if attributes else {} self.encoding = encoding self.fields: "dict[str, BitFieldDescr]" = {} self.vars = {} self.mnemonic = name.lower() if mnemonic is None else mnemonic self.assembly = assembly self.operation = operation if operation is not None else Operation([]) self.throws = False
[docs] self.function_info = function_info
self.mask = 0 self.code = 0 super().__init__(name, 0) for e in reversed(self.encoding): if isinstance(e, BitField): self._size += e.range.length if e.name in self.fields: f = self.fields[e.name] if f.ty.kind != e.ty.kind: raise M2TypeError(f'non-matching datatypes for BitField {e.name} in instruction {name}') if e.range.upper + 1 > f.ty.size: f.ty.size = e.range.upper + 1 else: f = BitFieldDescr(e.name, e.range.upper + 1, e.ty.kind) self.fields[e.name] = f else: self.mask |= (2**e.length - 1) << self._size self.code |= e.value << self._size self._size += e.length
[docs] def __str__(self) -> str: code_and_mask = f'code={self.code:#0{self.size+2}x}, mask={self.mask:#0{self.size+2}x}' return f'{super().__str__()}, ext_name={self.ext_name}, {code_and_mask}'
[docs] class Function(Named): """A class representing a function."""
[docs] attributes: "dict[attribute_info.FunctionAttribute, list[BaseNode]]"
[docs] ty: type_info.FunctionType
[docs] args: "list[FnParam]"
[docs] operation: "Operation"
[docs] extern: bool
[docs] ext_name: str
[docs] vars: "dict[str, Symbol]"
[docs] throws: bool
[docs] static: attribute_info.AccessAttribute
def __init__(self, name, attributes: "dict[attribute_info.FunctionAttribute, list[BaseNode]]", return_len, kind: type_info.TypeKind, args: "list[FnParam]", operation: "Operation", extern: bool=False, function_info: "FunctionInfo"=None): self.ext_name = "" self.attributes = attributes if attributes else {} self.ty = type_info.FunctionType(return_len, kind) self.vars = {} self.throws = False if args is None: args = [] self.args: "dict[str, FnParam]" = {}
[docs] self.function_info = function_info
for idx, arg in enumerate(args): if arg.name is None: arg_name = f"anon_{idx}" else: arg_name = arg.name self.args[arg_name] = arg self.operation = operation if operation is not None else Operation([]) self.static = attribute_info.AccessAttribute.NONE self.extern = extern super().__init__(name)
[docs] def __str__(self) -> str: return f'{super().__str__()}, type={self.ty}'
[docs] def extract_memory_alias(memories: "list[Memory]"): """Extract and separate parent and children memories from the given list of memory objects.""" parents = {} aliases = {} for m in memories: for c in m.children: aliases[c.name] = c p, a = extract_memory_alias(m.children) parents.update(p) aliases.update(a) if m.parent is None: parents[m.name] = m return parents, aliases
[docs] def extract_register_alias(register_banks: "list[RegisterBank]"): """Extract and separate parent and children register banks from the given list of register bank objects.""" parents = {} aliases = {} for m in register_banks: for c in m.children: aliases[c.name] = c parents[c.name] = m return parents, aliases
[docs] class AlwaysBlock(Named):
[docs] attributes: "dict[attribute_info.FunctionAttribute, list[BaseNode]]"
[docs] operation: "Operation"
def __init__(self, name: str, attributes, operation): self.attributes = attributes self.operation = operation super().__init__(name)
[docs] class InstructionSet(Named): """A class representing an InstructionSet collection. Bundles parameters, memories, functions and instructions under a common name. """ def __init__(self, name, extension: "list[str]", parameters: "dict[str, Parameter]", memories: "dict[str, Memory]", register_banks: "dict[str, RegisterBank]", functions: "dict[str, Function]", instructions: "dict[tuple[int, int], Instruction]"):
[docs] self.extension = extension
[docs] self.combines = []
[docs] self.parameters = parameters
self.memories, self.memory_aliases = extract_memory_alias(memories.values()) self.register_banks, self.register_aliases = extract_register_alias(register_banks.values())
[docs] self.functions = functions
[docs] self.instructions = instructions
super().__init__(name)
[docs] class InstructionSetGroup(InstructionSet): """A group of InstructionSet instances.""" def __init__(self, name, combines: "list[str]"): super().__init__(name, [], {}, {}, {}, {})
[docs] self.combines = combines
[docs] class CoreDef(Named): """A class representing an entire CPU core. Contains the collected attributes of multiple InstructionSets.""" def __init__(self, name, contributing_types: "list[str]", template: str, parameters: "dict[str, Parameter]", memories: "dict[str, Memory]", memory_aliases: "dict[str, Alias]", register_banks: "dict[str, Union[RegisterBank, Register]]", register_aliases: "dict[str, Alias]", functions: "dict[str, Function]", instructions: "dict[tuple[int, int], Instruction] | list[Instruction]", instr_classes: "set[int]", intrinsics: "dict[str, Intrinsic]"):
[docs] self.contributing_types = contributing_types
[docs] self.template = template
[docs] self.parameters = parameters
[docs] self.memories = memories
[docs] self.memory_aliases = memory_aliases
[docs] self.register_banks = register_banks
[docs] self.register_aliases = register_aliases
[docs] self.functions = functions
[docs] self.instructions = instructions
[docs] self.instr_classes = instr_classes
[docs] self.main_reg_file = None
[docs] self.float_reg_file = None
[docs] self.vector_reg_file = None
[docs] self.csr_reg_file = None
[docs] self.main_memory = None
[docs] self.pc_memory = None
[docs] self.global_irq_en_memory = None
[docs] self.global_irq_en_mask = None
[docs] self.procno_memory = None
[docs] self.irq_en_memory = None
[docs] self.irq_pending_memory = None
[docs] self.intrinsics = intrinsics
[docs] self._instructions_by_ext = None
[docs] self.functions_by_ext = defaultdict(dict)
[docs] self._instructions_by_class = None
for fn_name, fn_def in self.functions.items(): self.functions_by_ext[fn_def.ext_name][fn_name] = fn_def for mem in itertools.chain(self.memories.values(), self.memory_aliases.values()): if isinstance(mem, (Memory)): if mem.is_main_mem: self.main_memory = mem elif mem.is_csr_reg: self.csr_reg_file = mem elif attribute_info.MemoryAttribute.ETISS_IS_GLOBAL_IRQ_EN in mem.attributes: self.global_irq_en_memory = mem elif attribute_info.MemoryAttribute.ETISS_IS_PROCNO in mem.attributes: self.procno_memory = mem elif attribute_info.MemoryAttribute.ETISS_IS_IRQ_EN in mem.attributes: self.irq_en_memory = mem elif attribute_info.MemoryAttribute.ETISS_IS_IRQ_PENDING in mem.attributes: self.irq_pending_memory = mem elif isinstance(mem, (Alias)): if attribute_info.MemoryAttribute.ETISS_IS_GLOBAL_IRQ_EN in mem.attributes: self.global_irq_en_memory = mem elif attribute_info.MemoryAttribute.ETISS_IS_PROCNO in mem.attributes: self.procno_memory = mem elif attribute_info.MemoryAttribute.ETISS_IS_IRQ_EN in mem.attributes: self.irq_en_memory = mem elif attribute_info.MemoryAttribute.ETISS_IS_IRQ_PENDING in mem.attributes: self.irq_pending_memory = mem for regs in itertools.chain(self.register_banks.values(), self.register_aliases.values()): if isinstance(regs, RegisterBank): if regs.is_main_reg: self.main_reg_file = regs if regs.is_float_reg: self.float_reg_file = regs if regs.is_vector_reg: self.vector_reg_file = regs elif isinstance(regs, Register): if regs.is_pc: self.pc_memory = regs super().__init__(name) @property
[docs] def instructions_by_ext(self): if self._instructions_by_ext is not None: return self._instructions_by_ext assert isinstance(self.instructions, dict) self._instructions_by_ext = defaultdict(dict) for (code, mask), instr_def in self.instructions.items(): self._instructions_by_ext[instr_def.ext_name][(code, mask)] = instr_def return self._instructions_by_ext
@property
[docs] def instructions_by_class(self): if self._instructions_by_class is not None: return self._instructions_by_class assert isinstance(self.instructions, dict) self._instructions_by_class = defaultdict(dict) for (code, mask), instr_def in self.instructions.items(): self._instructions_by_class[instr_def.size][(code, mask)] = instr_def return self._instructions_by_class