# 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
import itertools
import logging
from typing import Union
from ... import (M2DuplicateError, M2NameError, M2TypeError, M2ValueError,
flatten)
from ...metamodel import arch, behav, intrinsics
from ...metamodel.code_info import FunctionInfoFactory
from .parser_gen import CoreDSL2Parser, CoreDSL2Visitor
from .utils import RADIX, SHORTHANDS, SIGNEDNESS
[docs]
logger = logging.getLogger("arch_builder")
[docs]
class ArchitectureModelBuilder(CoreDSL2Visitor):
"""ANTLR visitor to build an M2-ISA-R architecture model of a CoreDSL 2 specification."""
[docs]
_constants: "dict[str, arch.Constant]"
[docs]
_instructions: "dict[str, arch.Instruction]"
[docs]
_functions: "dict[str, arch.Function]"
[docs]
_always_blocks: "dict[str, arch.AlwaysBlock]"
[docs]
_instruction_sets: "dict[str, arch.InstructionSet]"
[docs]
_read_types: "dict[str, str]"
[docs]
_memories: "dict[str, arch.Memory]"
[docs]
_memory_aliases: "dict[str, arch.Memory]"
[docs]
_overwritten_instrs: "list[tuple[arch.Instruction, arch.Instruction]]"
[docs]
_instr_classes: "set[int]"
[docs]
_main_reg_file: Union[arch.Memory, None]
def __init__(self):
super().__init__()
self._constants = {}
self._instructions = {}
self._functions = {}
self._always_blocks = {}
self._instruction_sets = {}
self._read_types = {}
self._memories = {}
self._memory_aliases = {}
self._overwritten_instrs = []
self._instr_classes = set()
self._main_reg_file = None
[docs]
def visitBit_field(self, ctx: CoreDSL2Parser.Bit_fieldContext):
"""Generate a bit field (instruction parameter in encoding)."""
# generate lower and upper bounds
left = self.visit(ctx.left)
right = self.visit(ctx.right)
# instantiate M2-ISA-R objects
range_spec = arch.RangeSpec(left.value, right.value)
return arch.BitField(ctx.name.text, range_spec, arch.DataType.U)
[docs]
def visitBit_value(self, ctx: CoreDSL2Parser.Bit_valueContext):
"""Generate a fixed encoding part."""
val = self.visit(ctx.value)
return arch.BitVal(val.bit_size, val.value)
[docs]
def visitInstruction_set(self, ctx: CoreDSL2Parser.Instruction_setContext):
"""Generate a top-level instruction set object."""
# keep track of seen instruction set names
self._read_types[ctx.name.text] = None
name = ctx.name.text
extension = []
if ctx.extension:
extension = [obj.text for obj in ctx.extension]
# generate flat list of instruction set contents
contents = flatten([self.visit(obj) for obj in ctx.sections])
constants = {}
memories = {}
functions = {}
instructions = {}
# group contents by type
for item in contents:
if isinstance(item, arch.Constant):
constants[item.name] = item
elif isinstance(item, arch.Memory):
memories[item.name] = item
elif isinstance(item, arch.Function):
functions[item.name] = item
item.ext_name = name
elif isinstance(item, arch.Instruction):
instructions[(item.code, item.mask)] = item
item.ext_name = name
elif isinstance(item, arch.AlwaysBlock):
pass
else:
raise M2ValueError("unexpected item encountered")
# instantiate M2-ISA-R object
i = arch.InstructionSet(name, extension, constants, memories, functions, instructions)
if name in self._instruction_sets:
raise M2DuplicateError(f"instruction set \"{name}\" already defined")
# keep track of instruction set object
self._instruction_sets[name] = i
return i
[docs]
def visitSection_instructions(self, ctx: CoreDSL2Parser.Section_instructionsContext):
attributes = dict([self.visit(obj) for obj in ctx.attributes])
instructions: "list[arch.Instruction]" = [self.visit(obj) for obj in ctx.instructions]
for attr, val in attributes.items():
for instr in instructions:
if attr not in instr.attributes:
instr.attributes[attr] = val
return instructions
[docs]
def visitCore_def(self, ctx: CoreDSL2Parser.Core_defContext):
"""Generate a top-level CoreDef object."""
self.visitChildren(ctx)
name = ctx.name.text
c = arch.CoreDef(name, list(self._read_types.keys()), None,
self._constants, self._memories, self._memory_aliases,
self._functions, self._instructions, self._instr_classes,
intrinsics)
return c
[docs]
def visitSection_arch_state(self, ctx: CoreDSL2Parser.Section_arch_stateContext):
"""Generate "archictectural_state" section of CoreDSL file."""
decls = [self.visit(obj) for obj in ctx.declarations]
decls = list(itertools.chain.from_iterable(decls))
for obj in ctx.expressions:
self.visit(obj)
return decls
[docs]
def visitAlways_block(self, ctx: CoreDSL2Parser.Always_blockContext):
"""Generate always block"""
name = ctx.name.text
attributes = dict([self.visit(obj) for obj in ctx.attributes])
a = arch.AlwaysBlock(name, attributes, ctx.behavior)
self._always_blocks[name] = a
return a
[docs]
def visitInstruction(self, ctx: CoreDSL2Parser.InstructionContext):
"""Generate non-behavioral parts of an instruction."""
# read encoding, attributes and disassembly
encoding = [self.visit(obj) for obj in ctx.encoding]
attributes = dict([self.visit(obj) for obj in ctx.attributes])
disass = ctx.disass.text if ctx.disass is not None else None
i = arch.Instruction(ctx.name.text, attributes, encoding, disass, ctx.behavior, None)
self._instr_classes.add(i.size)
instr_id = (i.code, i.mask)
opcode_str = "{code:0{width}x}:{mask:0{width}x}".format(code=i.code, mask=i.mask, width=i.size//4)
i.function_info = FunctionInfoFactory.make(ctx.start.source[1].fileName, ctx.start.start, ctx.stop.stop, ctx.start.line, ctx.stop.line, f"instr_{i.name}_{opcode_str}")
# check for duplicate instructions
if instr_id in self._instructions:
self._overwritten_instrs.append((self._instructions[instr_id], i))
# keep track of instruction
self._instructions[instr_id] = i
return i
[docs]
def visitFunction_definition(self, ctx: CoreDSL2Parser.Function_definitionContext):
"""Generate non-behavioral parts of a function."""
# decode attributes
attributes = dict([self.visit(obj) for obj in ctx.attributes])
if arch.FunctionAttribute.ETISS_TRAP_ENTRY_FN in attributes:
attributes[arch.FunctionAttribute.ETISS_NEEDS_ARCH] = []
# decode return type and name
type_ = self.visit(ctx.type_)
name = ctx.name.text
# decode function arguments
params = []
if ctx.params:
params = self.visit(ctx.params)
if not isinstance(params, list):
params = [params]
return_size = None
data_type = arch.DataType.NONE
if isinstance(type_, arch.IntegerType):
return_size = type_._width
data_type = arch.DataType.S if type_.signed else arch.DataType.U
f = arch.Function(name, attributes, return_size, data_type, params, ctx.behavior, ctx.extern is not None)
if not f.extern:
f.function_info = FunctionInfoFactory.make(ctx.start.source[1].fileName, ctx.start.start, ctx.stop.stop, ctx.start.line, ctx.stop.line, "fn_" + f.name)
# error on duplicate function definition
# TODO: implement overwriting function prototypes?
f2 = self._functions.get(name, None)
if f2 is not None:
if len(f2.operation.statements) > 0:
raise M2DuplicateError(f"function \"{name}\" already defined")
self._functions.pop(name)
self._functions[name] = f
return f
[docs]
def visitParameter_declaration(self, ctx: CoreDSL2Parser.Parameter_declarationContext):
"""Generate function argument declaration."""
# type is required, name and array size optional
type_ = self.visit(ctx.type_)
name = None
size = None
if ctx.decl:
if ctx.decl.name:
name = ctx.decl.name.text
if ctx.decl.size:
size = [self.visit(obj) for obj in ctx.decl.size]
p = arch.FnParam(name, type_._width, arch.DataType.S if type_.signed else arch.DataType.U)
return p
[docs]
def visitInteger_constant(self, ctx: CoreDSL2Parser.Integer_constantContext):
"""Generate an integer literal."""
# extract raw text
text: str = ctx.value.text.lower()
# extract tick position for verilog-stlye literal
tick_pos = text.find("'")
# decode verilog-style literal
if tick_pos != -1:
width = int(text[:tick_pos])
radix = text[tick_pos+1]
value = int(text[tick_pos+2:], RADIX[radix])
# decode normal dec, hex, bin, oct literal
# TODO: remove width inference from text
else:
value = int(text, 0)
if text.startswith("0b"):
width = len(text) - 2
elif text.startswith("0x"):
width = (len(text) - 2) * 4
elif text.startswith("0") and len(text) > 1:
width = (len(text) - 1) * 3
else:
width = value.bit_length()
return behav.IntLiteral(value, width)
[docs]
def visitDeclaration(self, ctx: CoreDSL2Parser.DeclarationContext):
"""Generate a declaration."""
# extract storage type, qualifiers and attributes
storage = [self.visit(obj) for obj in ctx.storage]
qualifiers = [self.visit(obj) for obj in ctx.qualifiers]
attributes = dict([self.visit(obj) for obj in ctx.attributes])
# extract data type
type_ = self.visit(ctx.type_)
# extract list of contained declarations for the given type
decls: "list[CoreDSL2Parser.DeclaratorContext]" = ctx.declarations
ret_decls = []
# generate each declaration
for decl in decls:
name = decl.name.text
# generate a register alias
if type_.ptr == "&":
# error out on duplicate declaration
if name in self._memory_aliases:
raise M2DuplicateError(f"memory {name} already defined")
# assume default size
size = [1]
# alias needs to have a reference as initializer
init: behav.IndexedReference = self.visit(decl.init)
attributes = {}
# extract array size
if decl.size:
size = [self.visit(obj).value for obj in decl.size]
# extract referenced object and indices
left = init.index
right = init.right if init.right is not None else left
reference = init.reference
if decl.attributes:
attributes = dict([self.visit(obj) for obj in decl.attributes])
range_spec = arch.RangeSpec(left, right)
#if range.length != size[0]:
# raise ValueError(f"range mismatch for \"{name}\"")
# instantiate M2-ISA-R object, keep track of parent - child relations
m = arch.Memory(name, range_spec, type_._width, attributes)
m.parent = reference
m.parent.children.append(m)
# keep track of this declaration globally
self._memory_aliases[name] = m
# keep track of this declaration for this declaration statement
ret_decls.append(m)
# normal declaration
else:
# no storage specifier -> implementation parameter, "Constant" in M2-ISA-R
if len(storage) == 0:
if name in self._constants:
raise M2DuplicateError(f"constant {name} already defined")
# extract initializer if present
init = None
if decl.init is not None:
init = self.visit(decl.init)
c = arch.Constant(name, init, [], type_._width, type_.signed)
self._constants[name] = c
ret_decls.append(c)
# register and extern declaration: "Memory" object in M2-ISA-R
elif "register" in storage or "extern" in storage:
if name in self._memories:
raise M2DuplicateError(f"memory {name} already defined")
size = [1]
init = None
attributes = {}
if decl.size:
size = [self.visit(obj) for obj in decl.size]
if len(size) > 1:
raise NotImplementedError("arrays with more than one dimension are not supported")
if decl.init is not None:
init = self.visit(decl.init)
if decl.attributes:
attributes = dict([self.visit(obj) for obj in decl.attributes])
range_spec = arch.RangeSpec(size[0])
m = arch.Memory(name, range_spec, type_._width, attributes)
# attach init value to memory object
if init is not None:
m._initval[None] = init.generate(None)
if arch.MemoryAttribute.IS_MAIN_REG in attributes:
self._main_reg_file = m
self._memories[name] = m
ret_decls.append(m)
return ret_decls
[docs]
def visitType_specifier(self, ctx: CoreDSL2Parser.Type_specifierContext):
type_ = self.visit(ctx.type_)
if ctx.ptr:
type_.ptr = ctx.ptr.text
return type_
[docs]
def visitInteger_type(self, ctx: CoreDSL2Parser.Integer_typeContext):
"""Generate an integer type specification."""
# default signedness
signed = True
# minimal integer type is just a signedness without width
width = None
# extract sign
if ctx.signed is not None:
signed = self.visit(ctx.signed)
# extract size
if ctx.size is not None:
width = self.visit(ctx.size)
# extract and decode shorthand (int = signed<32>)
if ctx.shorthand is not None:
width = self.visit(ctx.shorthand)
# type check width
if isinstance(width, behav.IntLiteral):
width = width.value
elif isinstance(width, behav.NamedReference):
width = width.reference
else:
raise M2TypeError("width has wrong type")
return arch.IntegerType(width, signed, None)
[docs]
def visitVoid_type(self, ctx: CoreDSL2Parser.Void_typeContext):
"""Generate a void type."""
return arch.VoidType(None)
[docs]
def visitBool_type(self, ctx: CoreDSL2Parser.Bool_typeContext):
"""Generate a bool (alias for unsigned<1>)."""
return arch.IntegerType(1, False, None)
[docs]
def visitBinary_expression(self, ctx: CoreDSL2Parser.Binary_expressionContext):
"""Generate a binary expression."""
# visit LHS and RHS
left = self.visit(ctx.left)
right = self.visit(ctx.right)
op = behav.Operator(ctx.bop.text)
# return M2-ISA-R object
return behav.BinaryOperation(left, op, right)
[docs]
def visitSlice_expression(self, ctx: CoreDSL2Parser.Slice_expressionContext):
left = self.visit(ctx.left)
right = self.visit(ctx.right) if ctx.right is not None else None
expr = self.visit(ctx.expr).reference
op = behav.IndexedReference(expr, left, right)
return op
[docs]
def visitPrefix_expression(self, ctx: CoreDSL2Parser.Prefix_expressionContext):
prefix = behav.Operator(ctx.prefix.text)
expr = self.visit(ctx.right)
return behav.UnaryOperation(prefix, expr)
[docs]
def visitReference_expression(self, ctx: CoreDSL2Parser.Reference_expressionContext):
"""Generate a referencing expression."""
name = ctx.ref.text
# try to resolve the reference, error out if invalid
ref = self._constants.get(name) or self._memories.get(name) or self._memory_aliases.get(name)
if ref is None:
raise M2NameError(f"reference \"{name}\" could not be resolved")
return behav.NamedReference(ref)
[docs]
def visitStorage_class_specifier(self, ctx: CoreDSL2Parser.Storage_class_specifierContext):
return ctx.children[0].symbol.text
[docs]
def visitType_qualifier(self, ctx: CoreDSL2Parser.Type_qualifierContext):
return ctx.children[0].symbol.text
[docs]
def visitInteger_signedness(self, ctx: CoreDSL2Parser.Integer_signednessContext):
return SIGNEDNESS[ctx.children[0].symbol.text]
[docs]
def visitInteger_shorthand(self, ctx: CoreDSL2Parser.Integer_shorthandContext):
return behav.IntLiteral(SHORTHANDS[ctx.children[0].symbol.text])
[docs]
def visitAssignment_expression(self, ctx: CoreDSL2Parser.Assignment_expressionContext):
"""Generate an assignment. """
# extract LHS and RHS
left = self.visit(ctx.left)
right = self.visit(ctx.right)
# if LHS is a reference, assign RHS as its default value
if isinstance(left, behav.NamedReference):
if isinstance(left.reference, arch.Constant):
left.reference.value = right.generate(None)
elif isinstance(left.reference, arch.Memory):
left.reference._initval[None] = right.generate(None)
elif isinstance(left, behav.IndexedReference):
left.reference._initval[left.index.generate(None)] = right.generate(None)
[docs]
def visitAttribute(self, ctx: CoreDSL2Parser.AttributeContext):
"""Generate an attribute."""
name = ctx.name.text
# read attribute from enums
attr = arch.InstrAttribute._member_map_.get(name.upper()) or \
arch.MemoryAttribute._member_map_.get(name.upper()) or \
arch.FunctionAttribute._member_map_.get(name.upper())
# warn if attribute is unknown to M2-ISA-R
if attr is None:
logger.warning("unknown attribute \"%s\" encountered", name)
attr = name
return attr, ctx.params
[docs]
def visitChildren(self, node):
"""Helper method to return flatter results on tree visits."""
ret = super().visitChildren(node)
if isinstance(ret, list) and len(ret) == 1:
return ret[0]
return ret
[docs]
def aggregateResult(self, aggregate, nextResult):
"""Aggregate results from multiple children into a list."""
ret = aggregate
if nextResult is not None:
if ret is None:
ret = [nextResult]
else:
ret += [nextResult]
return ret