OpenVADL (9e732b53)
open-vadl
|
Listing riscv_isa shows a complete ISA specification of all RISC-V instructions with immediate operands and all branch instructions. It is a good example to show the most important VADL ISA features.
Line 1 defines an instruction set architecture
with the name RV32I
. The ISA section specifies basic architecture elements like registers, program counter and memory model and the instructions with their behavior, encoding and assembly representation. In line 3 a constant with the decimal value 32
for the register size is defined.
Lines 5 to 9 declare user defined types. VADL supports bit vector types (for details see section langref_type_system). The basic type is Bits
. There exist two subtypes representing signed (SInt
) and unsigned (UInt
) two's complement integers. The size of the bit vector is specified in angle brackets (<N>
).
Line 12 demonstrates the definition of a register file. Here we have a mapping of a 5 bit register index to the register type Regs
, a bit vector type with a bit width of 32
. Annotations can be used to detail a definition and are available for most of the ISA elements. Here the annotation is used to declare that the register X(0)
is a zero register, a register where a write has no effect and when read always returns zero (see line 11).
The memory model is defined as a mapping from the 32
address Addr
to a byte of eight bits. Additionally to the memory model a memory consistency model can be dedeclared with an annotation. One of the possible predefined consistency models is the weak memory consistency model of the RISC-V ( rvWeakMemoryOrdering
)
The declaration of a program counter is required in every ISA specification (line 15). The program counter is implicitely incremented by the size of an instruction if it is not modified in the instruction. If not changed by an annotation the program counter points to the start of the defined instruction.
A format definition is used to specify bitfields with named and typed member fields. There are two different variants for format specification. The first one defines bitfields with a name followed by a colon ":"
and a type (line 17 to 24). The second one defines bitfields with a name and a list of subranges in square brackets (line 26 to 33). It is possible to define accsess functions to format fields (line 23). The infix operator as
casts the left value to the type on the right side. The effects of truncation and extension effects are detailed in section langref_type_system.
Usually, many instruction definitions are quite similar. VADL supports type safe syntactic macro templates to avoid copying and modifying specifications. A macro definition starts with the keyword model
followed by the typed arguments and the result type of the macro ( line 36). There exist syntactic types like Id
(identifier), BinOp
(binary operator), Bin
(binary constant) or IsaDefs
( ISA definitions). An instantiation of a macro or the substitution of a macro argument are indicated by the dollar sign.
An instruction
defines the behavior of an instruction (line 37). After the equality symbol "="
the behavior is defined by a single statement or a list of statements in parantheses. Assignment statements use the symbol ":="
to separate the target on the left hand side from the expression on the right hand side. The precedence of all operators is listed in a table in section expr_precedence. A conditional statement is shown in line 45.
The encoding
sets the fields in an instruction word which are constant for the given instruction (line 39). The assembly
specifies the assembly language syntax for the instruction with a string expression (line 40).
By packing these three definitions into a macro, an instruction with behavior, encoding and assembly can be specified in a single line. This macro is invoked six times for all RISC-V instructions with immediate operands (lines 51 to 56).
Listing pdl_overview shows the main elements of a VADL processor specification. Usually, a VADL processor specification has some common definitions in the beginning, followed by the main sections which describe the ISA or the MiA. On line 1, a constant MLen
with the value 64 is defined. Type aliases can be defined with the keyword using
as shown on lines 3 to 5. On line 7, a function is defined that compares two values of type SIntM
and returns the result of the comparison as a value of type Bool
. import
allows the import of VADL specification parts from separate files. On line 9, a specification named RV3264IM
is imported from a file called rv3264im.vadl
by setting the model ArchSize
to Arch64
. In this example, RV3264IM
refers to another ISA specification.
An instruction set architecture
specification can extend another ISA specification and optionally pass macros ( line 11). Section tut_isa_definition contains a detailed description of the ISA specification. Lines 13 to 19 demonstrate the definition of the application binary interface
(see Section tut_abi_definition), the assembly description
(see Section tut_asm_definition), the processor
specification (see Section tut_prc_definition). On line 17 a MiA named FiveStage
is defined for the ISA RV64IM
(see Section tut_mia_definition).
Constant definitions start with the keyword constant
followed by the name of the constant, an optional type after the colon symbol ":"
, the equal symbol "="
and an expression which has to be evaluated at parse time (see Listing constants_literals). The evaluation of the expression is done with a signed integer type with unlimited size (internally the parser uses a Java BigInteger
type for the evaluation). Therefore, the constant twice
as expected has the value 64
. The constants min1one
and min1two
are of the fixed size type SInt<64>
. They cannot be used in expressions with unlimited size any more, the expression evaluaton is done on type SInt<64>
and the operands must have the type SInt<64>
.
Literals can also be specified as binary or hexadecimal numbers. A single quote symbol can be inserted into numbers to make them more readable. The constant binsize
represents the same value 32
as the constant size
. Equally to the constant min1two
the constant min1one
has the value minus one (-1
) as the very large hexadecimal constant with a positive integer value is casted to a signed integer of the same size resulting in the negative value.
Constant expressions can be quite complex, they can contain function calls, let
, if
and match
expressions. The definition of addEx2
in Listing constants_expressions shows the call of the function add
from the VADL
builtin namespace. This is equivalent to the usage of the "+"
operator in the definition of addEx1
. A let
expression defines the binding of an expression to a name. The name then can be used in the expression after the keyword in
. In the constant definition of letEx
the value 16
is bound to the name size
which is used in the addition size + 16
after in
. VADL has nested scoping of name spaces. A let
expression starts a new scope. Therefore, the definition of size
in the let
expression hides the definition of the constant size
in line 1 of Listing constants_expressions. An if
expression can be used in a constant definition if the condition can be evaluated at parse time. An if
expression always needs an else
part. As letEx
has the value 32
the value of ifEx
is 5
. A match
expression can be used to specify a multi way selection. The expression after the keyword match
is checked for equality with a list of expressions after the keyword with
included in braces and separated by a comma symbol ","
. The expressions in the list are evaluated sequentially, the expression which matches first is selected and the result of the evaluated expression after the arrow symbol "=>"
gives the result of the whole match
expression. A match
expression must contain a catch all expression (denoted by the underscore symbol "_"
) as last entry. In if
and match
expressions the types of the different alternatives must be identical.
Enumerations are used to assign names to expressions in an own name space. An enumeration is defined by the keyword enumeration
followed by the name of the enumeration, an optional type after the colon symbol ":"
and an equality symbol "="
. Then follows a list of names enclosed in braces and separated by a comma symbol ","
. The first name has the value 0
, every further name has the value of its predecessor incremented by one. Optionally a constant expression can be assigned to the name after the equality symbol "="
. Line 7 of Listing enumerations shows the use of an enumeration element with the added name space in front separated by "::"
to the name.
The type system is explained in detail in the reference manual (see Section langref_type_system). In VADL it is possible to declare bit vectors of arbitrary length. The basic types are Bits, SInt and UInt which can be used to form vectors. Type aliases are defined by the keyword using
followed by the alias name of the type, the equality symbol "="
and the type literal. The type literal is comprised of the name of a basic type, a type alias or a format optionally followed by a number of vector sizes in angle brackets. Listing using shows some type declarations and their meaning in the comments.
Functions in VADL are pure if they do not read registers or memory. Functions cannot write registers or memory. As long as functions do not read registers which have an effect when read, they do not have side effects. As VADL specifications have to be translated to specifications in a hardware description language or to patterns for the instruction selector of a compiler, neither recursive calls nor higher order functions are allowed. A function is defined by the keyword function
followed by the function's name, optionally a parameter list in parentheses, the arrow symbol "->"
, the return type of the function, the equality symbol "="
and an expression.
In line 1 of Listing functions shows the definition of the parameter less function size
which always will return the value 32
. In line 17 a function with one argument of type Bits<12>
is defined which maps two different enumerations to each other.
A format
definition names bit fields of a bit vector and is used to describe instruction formats or system registers. It starts with the keyword format
followed by the name of the format, the colon symbol ":"
, a type literal, the equal symbol "="
and a list of format fields enclosed in braces and separated by the comma symbol ","
. There exist two variants to define a bit field.
The first one, demonstrated with the definition of the format Itype
in Listing formats, defines a bit field with its name followed by the colon symbol ":"
and a type literal. Examples are the fields rs1
and funct3
of Itype
. Format definitions start with the most significant bits. Therefore, the field imm
occupies the bits from position 31
to 20
.
The second one uses a bit slice notation (see the format Btype
in Listing formats). A slice is defined as a concatenation of single bits and bit ranges in any order. A bit range starts with index of the highest bit of the range, then follows the range symbol ".."
and it ends with the index of the lowest bit of the range. Additionally it is possible to add a type literal to a slice separated by the colon symbol ":"
.
Bit fields are not allowed to overlap. Every bit inside a format has to be covered by a field definition. It is possible to use nested format definitions.
It is possible to define access functions to bit fields. They are defined by the name of the access function followed by the equality symbol "="
and an expression which can use any field name within the format definition. Every format has its own name space.
VADL exhibits a syntactical macro system. The advantage of a syntactical macro system compared to a lexical macro system is the type safety. There exists a set of syntax types which cover syntactical elements like an expression or an identifier. The syntax types are designed to have a one-to-one relation to parser rules. This already provides a partially ordered subtype relation. The following table lists all core syntax types with a description and examples:
Type | Description | Examples |
---|---|---|
Ex | Generic VADL Expression | X(rs1) + X(rs2) * 2 |
Lit | Generic VADL Literal | 1, "ADDI" |
Val | Generic VADL Value Literal | true, 1, 0b001, 0x00ff |
Bool | Boolean Literal | true, false |
Int | Integer Literal | 1, 2, 3 |
Bin | Binary or Hexadecimal Literal | 0b0111, 0xff |
Str | String Literal | "ADDI" |
CallEx | Arbitrary Call Expression | MEM<2>(rs1),PC.next,abs(X(rs1)),Z(0)(1) |
SymEx | Symbol Expression | rs1, MEM<2>, VADL::add |
Id | Identifier | rs1, ADDI, X |
BinOp | Binary Operator | +, -, *, &&, + |, <>>, !in |
UnOp | Unary Operator | -, ~, ! |
Stat | Generic VADL Statement | X(rd) := X(rs) |
Stats | List of VADL Statements | X(rd) := X(rs) ... |
Defs | List of common VADL Definitions | constant b = 8 * 4 ... |
IsaDefs | List of VADL ISA Definition | instruction ORI : Itype = { ... } ... |
Encs | Element(s) of an Encoding Definition | opcode = 0b110'0011, none, ... |
Call expressions represent function or method calls, memory accesses or indexed registers accesses with slicing and field accesses. The left hand side expression of an assignment statement also is a call expression. Additional examples are X(rs1)(15..0)
, IntQueue.consume(@BranchIntBase)
, VADL::add(X(5), X(6) * 2)
and a(11..8,3..0)
. A symbol expression consists of an identifier path optionally followed by a vector specification ( <VectorSizeExpression>
). Stats
, Defs
, IsaDefs
and Encs
require at least one element of the specified type.
Figure syntax_type_hierarchy displays the subtype relation between the presented core types. The macro type system provides an implicit up-casting of the value types. For example, if a model expects a value of type Val
, any subtype, i.e. Bool
, Int
or Bin
will be accepted as argument.
A macro is defined through the keyword model
followed by the name of the macro, a list of typed arguments in parentheses separated by the comma symbol ","
, the type of the macro after a colon symbol ":"
and after the equal symbol "="
the body of the macro enclosed in braces. The usage of the model arguments inside the model body is indicated by the dollar symbol "$"
. When a model is invoked, the model arguments in the body are substituted by the values passed in the arguments. Similar to arguments the invocation of a model is indicated by the dollar symbol "$"
. The arguments in a model invocation are separated by the semicolon symbol ";"
. The result of the model invocation in line 8 of Listing macro_model_definition is shown in Listing macro_model_invocation.
VADL provides an explicitly typed match
-macro to support the conditional application of macros. It will conditionally insert the match result into the syntax tree. It can be used inside a model
definition as well as in any location in a specification. A match macro is started by the keyword match
followed by the colon symbol ":"
and the syntax type of the macro. Enclosed by parentheses is a list of match
elements separated by a semicolon ";"
. A match
element contains a condition followed by the result of the macro after the double arrow symbol "=>"
. For the conditions only comparisons for equality ("="
) or inequality ("!="
) between two syntax elements are allowed. For every match
-macro a default case has to be provided at the last position, indicated by the underline symbol "_"
. When used outside of a model definition only macro invocations can be used in the comparison. In the example in Listing match_macro, a user can switch between a 32 and 64 bit address width by setting the appropriate model Arch
to the identifier Arch64
.
Listing divide_by_null shows a model
that optionally wraps an operation into a zero check. It demonstrates the usage of multiple conditions separated by the comma symbol ","
for the same match
-result. In this example multiple conditions are applied to two operators ("/"
and "%"
). The match
-macro is used inside a model definition and uses the model parameters in the conditions.
In real world processor specifications the number of model arguments can become quite large. Model types can be grouped together in a record
to reduce the number of arguments. Listing record_definition shows a record
definition used for type composition. In this particular case the record definition composes an Id
and BinOp
type to the new type BinInstRec
. The body of a record definition consists of a parameter list providing typed fields. Listing record_application shows how the record is initialized and the fields name
and op
are accessed. Passing a record type argument can be either done by reference or by creating a syntax tuple. A syntax tuple is specified the same way a model argument list is provided, i.e. syntax elements are separated by ";"
and enclosed inside parentheses. Accessing the passed elements is done using the record's name followed by a "."
and the desired field. Accesses of sub-records can be arbitrary chained together. Furthermore, it is important to note that records are treated as type tuples. Their field names do not affect the type and are only used to access the internal elements.
While most of the needs are covered by syntactical macros, string and identifier manipulation is best done using lexical macros. A lexical macro acts on the abstraction level of token streams in contrast to an already parsed AST. Two use-cases are supported using special syntax type converting functions. Firstly, templates generating instruction behavior and assembly often need the instruction name once in form of an identifier (Id
) and again in form of a string (Str
). This use case is covered by the IdToStr
function (will be renamed to AsStr
). This function takes an Id
typed syntax element and converts it to a Str
typed syntax element. Secondly, the ExtendId
function allows safe identifier manipulation (will be renamed to AsId
). This function takes an arbitrary number of Id
or Str
typed syntax elements, converts Id
typed elements to Str
, concatenates them and returns a single Id
typed syntax element. Listing lexical_macros shows a small example of both functions with their typed result as comment. It is important to note that the context of identifiers generated by lexical macros is strictly separated from the context of the syntactical macros. Therefore, it is not possible to define or refer to a model name or parameter using a generated identifier.
Higher order macros are macros which generate macros or which take macros as arguments. In the macro expansion system of OpenVADL, model instances are expanded immediately at the site they are declared. This allows the usage of models that produce models.
Listing model_producing_model shows the model BinExFactory
which in turn produces a model. Because the $BinExFactory
instance is evaluated immediately after it is parsed, the produced model Addition
is known to the parser and can be used in the definition of the ADD
instruction.
When using a macro as an argument of a macro, it is necessary to specify the signature of the passed macro in the argument type declaration (e.g. (Ex, Ex) -> Ex
in Listing higher_order_model_definition). As an alternative with better readability the signature can be declared in a separate type definition with the keyword model-type
followed by the signature after the equal symbol =
.
The model BinExStat
takes a macro of type BinExType
as an argument and returns a statement. When the model BinExStat
is invoked with the model AddExp
as an argument, an assignment statement with an addition on the right hand side is generated.
If a macro is passed as an argument to a model and assuming that the type for this argument is declared by a model-type
, then OpenVADL allows the model parameters of the passed macro to be supertypes of the model-type
parameters and the result type to be a subtype of the model-type
result. Listing model_type_parameters shows a reference to model Constants
being used as an IsaDefsFactory
. The reference is of a valid type because the result type Defs
is a subtype of IsaDefs
and the type Ex
of parameter size
is a supertype of Id
(see Listing syntax_type_hierarchy).
The ARM architecture AArch32 has a register file called R
consisting of 16 registers which are 32 bits wide (see Listing higher_order_macro). Conditions are specified by boolean expressions on flags of the status register APSR
, e.g. the zero flag Z
. Every instruction can be executed conditionally. There are 15 different conditions which are described by an enumeration in the specification and encoded by the cc
field in an instruction word which is 32 bits wide. Arithmetic/logic instructions, which have an immediate value as second source operand, share a common instruction encoding specified in the ArLoImm
instruction format.
As in the AArch32 architecture every instruction can be executed conditionally, a basic instruction exists in 15 variants for 15 different conditions. This problem can be solved smartly by an extension macro design pattern using higher-order macros as demonstrated in Listing higher_order_macro.
To reduce the number of macro arguments record types are defined for an instruction and a condition. The Inst
record type definition groups the four arguments describing an instruction together. The Cond
record type definition consists of a string representing the extension of the assembly name, the identifier of the enumeration of the condition encoding and a boolean expression for condition evaluation.
Now 15 different instructions with a unique identifier have to be created. This can be handled with the lexical macro function ExtendId
by appending the extension string of the condition to the identifier.
The final problem is that there is a set of models which describe different kinds of conditional instructions and all these models should be called 15 times for the 15 different conditions. This can be solved by the higher-order model CondInstr
, which takes an instruction model (e.g. ALImmCondInstr
) as first argument. The instruction model is then called 15 times with an argument list which has been extended by the conditions. In the above example the 4 macro calls expand to 60 different instructions. The AArch32 architecture has instructions with a lot of additional variants like setting the status register, shifted operands or complex addressing modes. This leads to a specification with multiple higher-order macro arguments.
VADL provides the possibility of passing configuration information to the macro system using the command line. Currently, this mechanism is kept very simple and is restricted to elements of type Id
. To prepare a configurable macro variable a default model of type Id
has to be defined. Listing macro_configuration shows such a variable of name Size
, with the default setting Arch32
. Without any passed configurations the instantiation of Size
results in the identifier Arch32
. If VADL receives the command line option -m
or --model
followed by the string "Size=Arch64"
, the value of Arch
is overridden. If Arch
is instantiated given the previous command line option, it would result in Arch64
. In combination with conditional expansion, see Section macro_match and Listing match_macro, this simple mechanism already provides powerful configuration capabilities.
Similarly to model passing in the command line it is possible to pass models as an argument to import declarations as demonstrated in Listing macro_import.
An instruction set architecture definition is the main part of a processor specification. It starts with the three keywords instruction set architecture
followed by the name of the ISA, the equality symbol "="
and the definition of the instruction set enclosed in braces (see line 1 of Listing lst_isa_definition). Optionally it is possible to extend an existing ISA with the keyword extending
followed by the name of the ISA to extend (shown in line 3). Currently it is only possible to extend one ISA. Discussions are ongoing how to support extending multiple ISAs.
At the beginning of an ISA section usually there is a set of common definitions (constant
, enumeration
, using
, function
and format
). These are followed by the definition of ISA elements like registers or instructions, which are usually generated by macros.
Declaring a program counter (PC) is required to define branch instructions or relative addressing (see a declaration in line 7 of Listing lst_program_counter). If an instruction does not explicitly modify the PC, it is implicitly incremented by the instruction size in each execution cycle.
In most architectures, the PC points to the start of the current instruction when used to compute a relative branch address. Therefore, this also is the default behavior in a VADL processor specification when no annotation is added to the PC definition. This behavior can be changed by adding the annotation [next]
, which lets the PC point to the end of the current instruction when the PC is read. When the PC is written, it always points to the begin of an instruction. The ARM AArch32 architecture has the peculiar behavior that the PC points to the end of the following instruction when used to compute the branch target address. This behavior can be specified by the annotation [next next]
. It is required that the following instruction has the same size as the current instruction.
If an instruction does not explicitly modify the PC, it is implicitly incremented by the instruction size in each execution cycle.
The read value of the PC as defined by the annotation can be overruled by using one of the builtin member methods for the program counter: current
, next
, and nextnext
(see lines 10 and 11 of Listing lst_program_counter). Independent of any PC annotation the method current
always returns the start of the current instruction, the method next
always returns the end of the current instruction, and the method nextnext
the end of the following instruction.
Listing register_declaration demonstrates two ways to declare a register file or a multidimensional register. In line 6 a register file named S
is declared by a relation which maps a five bit sized Index
to a bit vector of type Word
using the relation symbol "->"
. In line 13 the register Y
with the same layout as S
is declared as a 32 element vector of type Word
. The first way of declaration allows only register files where the number of registers is a power of two, the second way allows an arbitrary number of registers. Both ways allow the declaration of multidimensional registers.
In RISC architectures it is common to use a certain register as a zero register, a register that ignores values assigned to it and when read, always returns zero. In VADL such registers are described by an annotation enclosed in square brackets. The more generic version in line 7 allows to bind any constant with a certain register using a const
annotation and specifying the constant after the equality symbol "="
. The specific version in line 8 only allows the constant 0
with the zero
annotation.
It is possible to declare an alias of a register. The alias name follows the two keywords alias
and register
followed by an optional type literal after the colon symbol ":"
and the register which is aliased after the equality symbol "="
. The alias can be to single register, a certain register of a register file or a complete register file. The only requirement is that both registers have the same number of bits. It is allowed that the alias register has other annotations than the aliased register as demonstrated with the registers X
and S
. The alias register inherits all attributes from the aliased register. It is possible to make the PC an alias of a register using the keywords alias program counter
.
Listing partial_register_access shows the declaration of a status register with some bit fields. It is possible that these bit fields can be accessed directly (partial read and write) or the register can be accessed only as a whole (full read and write). Partial read and write is the default behavior. Then the fields can be directly accessed, e.g. flags.N
. When the register can only be accessed as a whole, then indexing or slicing is necessary to extract fields and concatenation is necessary to write the register. The behavior of the register is controlled by annotations. Additionally, it can be specified, that a register read or write has a side effect.
The characteristics of different memories are declared with the keyword memory
followed by the name of the memory, the colon symbol ":"
, and a relation from the address type to the memory cell type. The memory relation is specified by the type literal for the address space followed by the relation operator "->"
and the type literal for a memory cell (see the declaration of a memory named Mem
in Listing memory_declaration). The memory declaration only describes the mapping of an address space to a memory cell, it does not specify the pyhiscal memory available in a processor.
The memory characteristics can be detailed with different annotations. If no annotation is defined, the memory serves both as data and instruction memory, the memory access is carried out in little endian mode, there is no address translation and the memory consistency model is sequential consistency. With the annotation [data]
a memory is only used for data. With the annotation [instruction]
a memory is only used for instructions. With the annotation [bigEndian]
a memory is only accessed in big endian mode. If a processor supports bi-endianess, the endianess is selected by the condition evaluated to true, e.g. dependent on a system register. If the condition is evaluated to false, the opposite endianess is selected. Exceptions like alignment errors could be specified in every memory accessing instruction directly. But this violates the principle of separation of concerns. With the raise
annotation the throwing of an exception is declared together with the memory. The translate
annotation connects the specified address translation process with a memory. There exist different memory consistency models which are specified with the ordering
annotation.
Listing instruction_definition presents an instruction definition in line 11. An instruction definition starts with the keyword instruction
followed by the unique name of the instruction, a type literal (usually the name of a format specification) after the colon symbol ":"
and a statement after the equality symbol "="
that defines the behavior of the instruction. Every instruction definition needs a corresponding encoding and assembly definition. All field and access function names of the instruction's format are visible inside the instruction and inside encoding and assembly definitions.
There are restrictions on the execution order of the statements. The statements have a sequential semantics, but the OpenVADL compiler must be able to reorder the operations to comply with the restrictions. All register and memory reads are done in parallel at the beginning of the instruction's execution cycle. All register and memory writes are done in parallel after all reads at the end of the execution cycle. It is forbidden that a certain register or memory cell is written twice. There is no order on the execution of writes and the result would be undefined if the same register is written twice.
Therefore, there exist annotations which specify restrictions on used resources. With the require
annotation it is possible to specify constraints which will be checked by OpenVADL's generators and the decoder. In the example in Listing instruction_definition a constraint is specified which requires that the indices of the base and the target register are different. This constraint is additionally checked by the decoder, after the decoding necessary to determine the correct instruction is completed. There are no restrictions on the used relational operators for the require
annotation.
Instructions can be grouped into multiple sets used for specifying characteristics of VLIW
instructions or MiA elements. A set of instructions is named operation
and can be defined by an annotation as shown in Listing instruction_definition or in an operation
definition (see Section tut_operation_definition)
A let
statement is used to define the instruction LDUP
in Listing instruction_definition. An identifier follows the keyword let
at the start of the statement. The let
statement binds the expression after the equality symbol "="
with the identifier which enables the use of the result of the expression in the statement after the keyword in
. This identifier is only visible in the scope defined by the statement after in
. It is common that the statement after in
is a block statement which is also the case in the current example.
Some VADL builtin functions return both the result of the computation and a status of the computation. The status consists of the four subfields .negative
, .zero
, .carry
and .overflow
. Such functions only can be called with a special let
statement which allows two names separated by the comma symbol ","
as demonstrated in the definition of the instruction ADDIS
in Listing instruction_definition. The VADL builtin functions have a separated name space which is designated with the path specifyer VADL::
. The first name in the special let denotes the result of the computation, the second name denotes the status of the computation. In the ADDIS
instruction the status elements are selected by their subfield names, concatenated to a bit string of the type Bits<4>
and assigned to the register named status
which also is of type Bits<4>
.
A block statement groups multiple statements together by enclosing them in braces giving a single statement. An empty block statement is a valid statement.
The instruction definitions LDUP
and ADDIS
in Listing instruction_definition show some assignment statements which allow the assignment of a value to a register or to memory. On the left hand side of the assignment operator ":="
a call expression restricted to a register or memory access is expected, on the right hand side any expression is allowed. A register file access usually is indexed. A memory access is always indexed and sometimes is a vector access denoted by the vector size in angle brackets. On the left hand side the register or memory is always written, on the right hand side it is always read.
Listing if_match_raise_statement shows the usage of an if
statement in lines 33 to 36. In contrast to an if
expression the else
part is optional. When if
statements are nested, an else
part belongs to the closest then
part. After the keywords then
and else
a single statement is allowed, which also could be a potentially empty block statement.
A match statement selects a statement based on a pattern as demonstrated in lines 43 to 48 of Listing if_match_raise_statement. It starts with the match
keyword, the selection expression and the with
keyword followed by a list of entries separated by the comma symbol ","
. Each entry consists of possible candidate expression on the left hand side and an arbitrary statement on the right hand side of the double arrow symbol =>
. At least one entry must contain the wildcard _
as candidate, which always matches and prevents the match from being not evaluated. If a candidate expression is equal to the selection expression, the match statement evaluates to the right hand side of the matched entry. It is also possible to use a list of candidate expressions in curly braces separated by the comma symbol ","
.
VADL has special notations to mark exceptional behavior. Technically, these notations are not necessary, as every exceptional behavior can be described with the base ISA language constructs. However, neither a human reader nor the compiler generator can distinguish normal behavior from exceptional behavior. Therefore, it is required that exceptional behavior is marked by the keyword raise
as shown in Listing if_match_raise_statement at line 34. The code after the keyword raise
can be any (block) statement or the call of an exception defined elsewhere. After the execution of the exception code the whole instruction is exited, no other statements are executed anymore. Therefore, in the example the else
in line 35 could be deleted without changing the behavior of this instruction.
Exception raising code is often quite similar. Exceptions can be specified similarly to functions to enable code reuse. An exception is defined by the keyword exception
followed by the exception's name, optionally a parameter list in parentheses, the equality symbol "="
and a statement which is usually a block statement. In contrast to functions, exceptions do not return a result, but have side effects caused by assignment statements (see lines 26 to 29). Nevertheless, it must be guaranteed that reads to a register or memory location precede all writes.
To specify exceptional behavior like overflow, the basic VADL built-in functions exist in a version which returns a status like the occurence of an overflow during the computation. These built-in functions are used to specify instructions that handle operations with overflow as demonstrated in the example in Listing if_match_raise_statement.
Tensors are multi-dimensional arrays with a uniform type. In VADL tensors are specified by vectors of vectors with a bit vector for the innermost dimension. When defining tensors the size of every dimension has to be enclosed separately in angle brackets. When indexing tensors the index of every dimension has to be enclosed separately in parentheses. The outermost index is the first one, the innermost index is the last one (left to right). Listing tensor_forall gives some examples for the definition of multidimensional registers (lines 3 to 6). Note that register A
and Z
have the same layout, they are just defined with two different notations, A
with a multidimensional notation, Z
with a relational notation which is more restricted in the size of the dimension (power of two).
VADL provides the forall
keyword which can be used as a statement or as an expression to avoid elaborate specifications of tensor operations. The VADL forall
looks similar to known forall
or for
constructs in other programming or specification languages. But the semantics has some subtle differences. The body of a VADL forall
statement or expression is executed in parallel, there is no sequential execution and update of registers. It is forbidden to write to the same resource twice (register or memory). A forall starts with the keyword forall
and is followed by a list of comma separated triples consisting of an index identfier, the keyword in
and a range, the keyword do
and the statement which should executed in parallel using different indices. The ranges can be ascending or descending. The symbol binds each value in its range and can be used inside the body similar to the variables in a let
statement. The instruction definitions for Init4X
, Init4Z
and AddElementsStat
in Listing tensor_forall demonstrate the usage of a forall
statement.
In VADL a forall
expression with the keywords tensor
or fold
can be used to describe concise tensor expressions. The forall
expression with the keywords tensor
or fold
has the same structure and variable binding behavior as the forall
statement described above. Instead of the do
keyword, the expression either has the tensor
keyword followed by an arbitrary expression, or the fold
keyword followed by one of the binary operators +
, -
, *
, &
, |
and ^
, then the keyword with
and an arbitrary expression. The fold
expression chains each expression together with the provided operator. Therefore, the result type is the same as the type of the expression after with
. Listing tensor_forall shows both forall
expressions. The instruction AddElementsExpr
does the same as the instruction AddElementsStat
but uses a tensor
expression instead of a forall
statement. The instruction Dot
computes the scalar product using a multiplication with a double sized result and stores the computed sum in a register which is double sized.
The encoding definition enables the assignment of fixed values like an operation code or a condition code to the format fields of an instruction. It starts with the keyword encoding
and is followed by the instruction's name, the equality symbol "="
and a list of format field assignments enclosed in braces and separated by the comma symbol ","
. The allowed format field names are defined by the format declared in the instruction definition. A format field assignment starts with the field name, is followed by the equality symbol "="
and an expression which can be evaluated at parse time. Listing encoding_assembly shows two encoding definitions in line 29 and line 34. Often the encoding definitions are generated by a macro and sometimes no format field assignment is required. For these cases the keyword none
represents an empty assignment (see line 34).
An assembly definition specifies how an instruction is emitted by the compiler in assembly source code or how a disassembler displays an instruction. In the future the assembler generator will be able to derive the parser from the assembly definition employing program inversion. For now the assembler parser has to be specified in the assembly description definition (see section tut_asm_definition).
An assembly definition starts with the keyword assembly
followed by a single name of an instruction or multiple names of instructions separated by a comma symbol ","
, the equality symbol "="
and a string expression enclosed in parentheses specifying the appearance of the instruction (see Listing encoding_assembly lines 30 and 35). Both the comma ","
and the plus "+"
operator are interpreted as concatenation. Whitespaces have to be defined explicitly, strings are concatenated directly without space. Pure user defined or builtin string functions can be used to format the elements in the assembly definition. The builtin mnemonic
converts the instruction name into a string. The argument of the builtin register
is searched for a use in register indexing and the name of the indexed register is concatenated with the argument converted to a decimal number. If an alias for the indexed register element is defined, the alias name of the register is used instead.
The builtins decimal
and hex
convert their argument into a string in decimal respective hexadecimal representation. As arguments both format fields (see line 30) and access functions (see line 35 with immX) can be used. In the second case the access function is applied before the string conversion. An explicit use of the decode function is allowed too (decimal(decodeX(imm13))
). The definition of a user defined string function is shown in line 24 to 25 with the definition of WReg
. This function converts the argument to the string "wzr"
or concatenates "w"
with the decimal representation of the argument.
A pseudo instruction is an assembly instruction which is not a real machine instruction, but which can be mapped to one or more real machine instructions. The semantics of a pseudo instruction is specified by one or multiple calls of machine instructions. A pseudo instruction starts with the keyword pseudo instruction
followed by the name of the pseudo instruction, formal arguments enclosed in parentheses separated by the comma symbol ","
, the equality symbol "="
and one or more instruction calls enclosed in curly braces. Each instruction call encloses its arguments in curly braces and the arguments are triples of a name of a format field, the equality symbol "="
and a parse time expression or an argument of the pseudo instruction or an relocation. Listing pseudo_instruction shows two examples of pseudo instruction definitions. Every pseudo instruction requires a corresponding assembly definition.
Symbol resolution of machine code and data sometimes cannot be performed at compile or assembly time. Linkers and loaders have to adjust code and data to reflect the assigned load addresses for position-dependent code and data. This process is called relocation.
Relocations in object files identify parts of instructions or data that have to be resolved at link or load time. VADL has the relocation
keyword to define such relocations which initially are designed to support the ELF object file format. Listing relocation_definition shows some relocation definitions and common annotations.
The relocation definition is similar to a function definition, only the keyword function
is replaced by relocation
. Currently for relocations only a single argument is supported. With annotations different kinds of relocations are selected. For now the annotations [absolute]
, [relative]
and [globalOffset]
are suported. An [absolute]
relocation is used for a symbol which represents an absolute address and is the default if no annotation is given. For position independent code the [relative]
relocation represents a program counter relative symbol and a [globalOffset]
relocation relies on a global offset table (GOT) which adds an indirection to achieve position independent code. Both [relative]
and [globalOffset]
do not require to reference PC
or GOT
since the annotation indicates how the value has to change. For relative relocations, the compiler generator will subtract the program counter for the returned value. For global offset relocations, the offset of the global offset table and the offset of the symbol are added and the program counter is subtracted.
An ABI ensures consistent and well-defined interoperation between different units of object code. A VADL ABI definition provides the necessary information to the compiler generator to generate an ABI compliant compiler. Additionally information is supplied to support the compiler generator to generate an efficient compiler. The ABI specification section in VADL supports the definition of
Listing application_binary_interface shows all elements of the ABI section.
An ABI section starts with the keyword application binary interface
followed by a unique identifier. Since most elements inside the ABI section rely on previously defined ISA elements, it is required to reference an ISA section using the for
keyword after the identifier. Definitions from the referenced ISA are available inside the ABI section. In the example in Listing application_binary_interface the ABI uses the register file X
and the instructions ADDI
and LUI
from the ISA RV32I
.
In order to reference registers with additional names, the ABI section provides the alias register
keyword. The keywords alias register
is followed by the new identifier and after the equality symbol "="
a reference to the original register. If multiple names are available for a specific register, the annotation [preferred alias]
emits the preferred name in generated code (lines 2 to 5).
In an ABI some registers fulfill a certain purpose like being used to keep the return address
or are used as a stack pointer
. With the annotation [alignment : ByteCount]
the stack pointer is aligned to ByteCount
memory elements. These special purposes are declared with the self explaining keywords. The global pointer
is the register used to access data in a global memory area, the frame pointer
register gives the access to the stack frame (activation record) of a function or method and the thread pointer
register is used to acces thread local storage.
Calling conventions describe rules which have to be obeyed during a function call. The specification contains information on which registers are used to pass arguments (function argument
) or return values (return value
), or which registers are managed by the caller or callee (caller saved
, callee saved
). Each definition has the same structure, i.e., a descriptive keyword that declares which register or register group will be specified, followed by the equality symbol "="
and one or more references to the actual registers. To be more concise, VADL provides a special syntax to address multiple registers with similar names. In the example, the compact expression a{0..7}
evaluates to [a0, a1, a2, a3, a4, a5, a6, a7]
. Note that the callee saved sequence contains the return address ra
on purpose in the example even though the official RISC-V ABI documentation states it as caller saved. This is required because a function call changes the register whereas the return won't restore the old value.
The compiler generator cannot automatically deduce all necessary code sequences for its functionality. There exist two mechanisms to select such code sequences, referencing pseudo instructions and defining special sequences.
The reference to a pseudo instruction starts with the keyword pseudo
followed by some keywords describing the functionality, the keyword instruction
, the equality symbol "="
and the name of the referenced pseudo instruction. Five pseudo instruction references are available. call
is a pseudo instruction implementing a function call. return
is a pseudo instruction implementing a function return. absolute address load
is a pseudo instruction implementing the loading of an absolute address. local address load
is a pseudo instruction implementing the loading of a local program counter relative address. global address load
is a pseudo instruction implementing the loading of an address using a global offset table.
The definition of compiler sequences uses a syntax similarly to the definition of pseudo instructions. Instead of the keyword pseudo instruction
they use constant sequence
and register adjustment sequence
. The constant sequences have two arguments, a register index and an immediate value. They define efficient code sequences to load immediate values of different types in different sizes. The register adjustment sequences have three arguments, a destination register index, a source register index and an immediate value. They define efficient code sequences to add immediate values of different types in different sizes to the source register and store the result in the destination register and are used for the stack frame creation and unwinding. If an immediate does not fit into the immediate of a register adjustment sequence, then a constant sequence will be used. This requires an additional register which can be more costly.
Assembly languages have no standardized structure like object files. However, many languages are alike. This similarity allows VADL to make some assumptions about the structure of the assembly files to reduce specification effort. Firstly, labels have a predefined syntax: the name followed by a colon (e.g., loop:
). Secondly, each statement must correspond either to an assembly directive or to a (pseudo) instruction of the processor's architecture. Lastly, the overall structure of the source file is a sequence of labels and statements. As the assembly directives for different architectures are the same, a VADL specification thus can focus on defining the syntax of the assembly instructions.
Listing assembly_description presents the structure of an assembly description definition, including its three subsections. An assembly description has to refer to an ABI. By extension, the assembly description also depends on the ISA linked to the ABI. The commitment to a particular ABI instead of an ISA is necessary to provide additional information about the usage of some registers. For example, a generated linker could use the defined global pointer to optimize access to certain variables. As with any top-level element, annotations can provide additional information to the generators.
The directives
definition is the first subsection in the example. It starts with the keyword directives
followed by the equality symbol "="
and a list of directive mappings enclosed in curly braces separated by the comma symbol ","
. A directive mapping maps the string representation of a directive with the relation symbol "->"
to an identfier representing a builtin directive. A list of all available builtin directives is contained in the reference manual table_assembly_directives.
The modifiers
definition is the second subsection in the example. It starts with the keyword modifiers
followed by the equality symbol "="
and a list of modifier mappings enclosed in curly braces separated by the comma symbol ","
. A modifier mapping maps the assembly string representation of a modifier with the relation symbol "->"
to an identfier representing a relocation defined in the ISA.
The most crucial element of the assembly description is the grammar definition. It defines the structure of assembly instructions as a formal language grammar augmented with semantic information. For example, users can annotate sub-elements of an instruction with type information, thus capturing the role of an element (e.g., refers to a register). This tutorial will abstain from discussing all intricacies of the grammar element. However, the example in Listing assembly_description gives readers a good intuition of how the grammar element captures relevant information for the assembler generation. The example shows the definition of a rule that describes all RISC-V instructions with two registers as source operand and one register as destination operand. A second rule describes all compare against zero and branch pseudo instructions which have a register and an immediate value as source operands. Register
and ImmediateOperand
are non-terminals that have a default definition in the language. Register
returns the parsed register if there was no error during parsing for a register. ImmediateOperand
returns the value resulting from parsing and evaluating an expression if there was no error during parsing for an expression. Users can override these defaults by providing a rule with the corresponding name. Table assembly_nonterminals in the reference manual lists all available rules.
The power of the grammar system is rooted in the type system of the language as it also models the semantic information. Usually, when parsing an assembly file, the algorithm receives tokens with primitive types from the lexical analysis. These tokens do not capture any semantic information. However, an assembler must check whether the tokens satisfy context-dependent criteria. For example, when the assembler encounters an ADD
instruction, the first operand has to be a valid register. VADL uses its type system to capture this information. By annotating elements of the grammar with a semantic type, the user instructs the parser generator to insert a conversion routine for the value of the given element. This routine depends on the input and output types and may include validation and transformation of the input value. For example, the conversion routine from the primitive string type to the register type checks whether a register has a matching name. The procedure's successful completion asserts that the value refers to a valid register. VADL's type system conveys this information to other parts of the grammar. A parser can generate a meaningful error message if the validation fails.
Readers may wonder why VADL requires a separate grammar for the assembly syntax even though the ISA section describes assembly formatting functions. The idea is that the language could also define the grammar solely by the inversion of the formatting function. We decided against such an approach for two reasons. Firstly, VADL does not always require grammar specifications for each instruction. By defining conventions for grammar rule names, generators may support users by synthesizing rules from the formatting functions. This approach allows for a graceful degradation of the required amount of specification as users may provide rules on a per-instruction basis. For example, a generator may create the grammar rule from Figure lst:lui_grammar} from the associated formatting function. Secondly, if the language relies solely on function inversion, generators must have sophisticated inversion routines, as the system has to support every possible formatting function. By defining the grammar separately, VADL provides an escape hatch if the rule generation capabilities of a generator are not general enough. Lastly, a single assembly instruction may map to multiple valid text representations (e.g., multiple spaces instead of one).
The microarchitecture section aims to specify the processor implementation at a high level of abstraction. These abstractions enable a concise and understandable specification, as the generators handle many implementation details (e.g., hazard detection or pipeline registers). Users would have more flexibility and control with low level microarchitecture specifications (e.g., in a HDL), but it would be impossible for generators to determine the purpose or the correctness of such specifications. Therefore, only predefined elements configurable by annotations are supported. If these predefined elements are not sufficient, further elements have to be added to the language and the affected generators have to be extended.
Firstly, this section will present the core concepts of the MiA definition - pipeline stages and instructions. Then, these concepts are illustrated with the example of a 5-stage implementation of the RISC-V architecture. Finally, logic elements that model components outside of the stages (e.g., caches, control logic) are discussed.
Pipeline stages allow users to define the hardware structure of the processor. Each stage defines cyclic behavior, which the processor executes. For example, one processor stage might fetch instructions from memory while another computes arithmetic results. Users can specify the exact behavior using syntax similar to that used to express the instruction behavior in Section tut_isa_definition. It is easy to define a concise microarchitecture using powerful language built-ins. Section tut_example_mia provides some examples of pipeline stages.
In addition to the provided examples, annotations can specify a stage's restart interval and latency period. The restart interval governs the frequency at which new inputs are allowed to enter the stage. In contrast, the latency period controls the number of machine cycles required to complete a single execution. Additionally, users can assign a range to the latency, thus providing pipeline stages of varying lengths.
The instruction abstraction is a central concept of the MiA. Users can leverage this concept with Instruction
typed variables. These variables abstract away two dimensions – the kind of instruction and the progress of the instruction execution. The first aspect implies that the MiA specification is not aware of the instructions present in the ISA. Such variables may even represent VLIW bundles. The second aspect implies that the MiA specification is not aware of the execution state. That is, it is not aware of which parts of the instruction semantics have already been computed at any point in the pipeline. The generator resolves these abstractions automatically during the microarchitecture synthesis. If the generator cannot entirely resolve the abstractions, it will raise an error.
Because the MiA is blissfully unaware of the complexity behind the Instruction
variable, it can solely interact with the instruction using abstract operations on the variable. For example, it can specify that the instruction should make arithmetic computations using instr.compute
. VADL provides a set of such operations. We will refer to them as instruction mappings, or simply mappings. Some mappings are very general (e.g., read any register), while others are more specific (e.g., read register file X
). This enables users to trade off between precise control and compatibility with other ISAs.
This Section describes the FiveStage
microarchitecture depicted in Listing mia_definition. A microarchitecture must implement an ISA, such as the RV32IM
architecture (line 2) in our example. The pipeline consists of five stages. The specifications of each stage will be discussed in the following paragraphs. The dataBusWidth
annotation determines the width of the memory interface. In this example, reading from and writing to memory is done in 32-bit blocks.
Listing mia_definition depicts the FETCH
and DECODE
stages of the pipeline. All stages but the final stage have to specify the result of the stage. The order of stages is defined by accessing the result of a previous stage. The FETCH
stage makes use of the fetchNext
built-in. The result type of this operation (FetchResult
) abstracts the fetch size while the built-in automatically determines the next program counter. The generator determines the fetch size by analyzing the instructions in the ISA. In the future, VADL users may provide additional options for the fetch operation (e.g., buffers, multiple instructions). To understand the MiA specification, it is sufficient to know that the fetchNext
built-in loads enough bytes from the correct memory position to represent a single instruction.
The DECODE
stage makes use of the decode
built-in. The primary goal of this built-in is to represent a decoder for the implemented ISA. The generator will synthesize a decoder automatically. It takes a FetchResult
as input and produces an Instruction
as output. This is the origin of the instruction abstraction, which was discussed in Section tut_instruction_abstraction. The FetchResult
input is obtained from the preceding FETCH
stage. Note that the generator can resolve the instruction abstraction because it has access to the ISA. The decoded instruction then reads the source operands from the X
register file.
Listing mia_definition shows the specification for the EXECUTE
stage. It is responsible for computing arithmetic operations and executing branches. Firstly, the stage obtains the current instruction from the DECODE
stage (line 17). Then, the specification checks whether the instruction is valid (line 18). If not, the stage raises an invalid instruction exception, thus redirecting the control flow to the exception handler (line 19). If the instruction is valid, the stage computes arithmetic operations (line 20) and writes the new program counter (line 22). In addition, the stage verifies whether the instruction is on the correct program execution path (line 21). If this is not the case (branch misprediction), the control logic flushes the EXECUTE
stage and all its predecessors. The MEMORY
and WRITE_BACK
stages in Listing mia_definition complete the 5-stage pipeline. The displayed definitions define a valid VADL MiA specification.
VADL uses the concept of a logic element to model microarchitectural concepts besides stages. The complexity of logic elements varies greatly depending on its semantics. An annotation determines a logic element's type and, thus, its semantics. For example, Listing forwarding displays a logic element that allows users to define forwarding paths between stages. The generator must be aware of the logic element's semantics as it must derive the implementation in the microarchitecture synthesis.
Connecting logic elements with the instruction abstraction realizes their full potential. Listing forwarding also shows how instructions may read and write values to the previously mentioned forwarding logic. As the generator is aware of the semantics, it can synthesize the logic of the forwarding network. Furthermore, it can also integrate this knowledge into the hazard detection logic element. After all, the control unit should not stall the pipeline if a forward can resolve the hazard.
Readers familiar with microarchitecture design may have noticed that the specification does not contain elements for the necessary control logic and hazard detection. If the generator does not find a logic element that handles these circumstances, it inserts a default hazard detection and control element into the MiA. Later, the microarchitecture synthesis determines the necessary control logic for the processor.
To represent a memory sub-system, VADL provides a cache
definition to describe caches. The definition can be parameterized through annotations. Listing cache_definition defines a cache named L1
with 1024 entries (cache lines).
A single cache line has 4 blocks where a single block corresponds to one addressable unit. For instance, this would be eight bits on a byte-addressable architecture. Our cache is defined to be 2-way associative (n_set
). Since the cache has 1024 entries and each set contains two entries, the cache has a total of 512 sets. Observe that setting (n_set
) to 1 is equivalent to a direct mapped cache, while n_set = entries
makes the cache fully associative. Most importantly, the attached_to
annotation defines where the cache can fallback to in case of a miss. The fallback storage can be another cache (e.g., level 2), memory or a process. The latter can be used to translate a virtual address to a physical one before accessing main memory for instance. In addition, several behavioral aspects of the cache can be specified, such as write and eviction policy.