====== Expressions ====== Expression syntax in ACT is similar to expressions in the C language. * Integer expressions: operators that are supported include ''+'', ''-'', ''*'', ''/'', and ''%'' for integer arithmetic operations, and ''&'', ''|'', and ''~'' for bit-wise logical operations between integers. * Boolean expressions can be constructed from Boolean variables, the constants ''true'' and ''false'', and the Boolean operators ''&'', ''|'', and ''~'' denoting the and, or, and negation operations respectively. (This is a departure from C, where and/or operations between conditions use ''&&'' and ''||''.) * Numerical expressions can be compared using ''<'', ''< ='', ''>'', ''>='', ''='', and ''!='' for the operators less than, less than or equal to, greater than, greater than or equal to, equal to, and not equal to respectively. * The query expression ''(e ? e1 : e2)'' behaves like the C operator. The result is ''e1'' if ''e'' is true, and ''e2'' otherwise. * Logical shift operators are ''<<'' (left shift), ''>>'' (logical right shift), and the arithmetic right shift operator ''>>>''. * Bit operations: For a variable ''x'', the value ''x{b..a}'' extracts bits ''b'' through ''a'' (both included, with ''b'' at least ''a''). Both ''b'' and ''a'' must be constants (or computed by parameters, i.e. constants after expansion). The syntax ''x{a}'' is syntactic sugar for ''x{a..a}'', and extracts one bit from ''x''. Note that ''b'' must be at least ''a''; for example ''x{3..2}'' corresponds to two bits, but ''x{2..3}'' is not a valid reference. * Concatenation: Given a set of expressions, ''{e1,e2,e3,...,eN}'' concatenates the bits of the expressions to form a wide result. Expressions can also include function calls, with the usual function call syntax. More details on how functions are defined is below. Expressions can also include type-conversion operators: * ''int(x)'': ''x'' must be a Boolean expression. ''int(x)'' will be 0 if the expression is false, or 1 otherwise. * ''int(x,w)'': ''x'' must be an integer expression, and ''w'' must be evaluate to an integer constant after expansion (i.e. it can only be an integer expression that includes parameters). This changes the bit-width of the integer to be ''w''. If the width is reduced, the high order bits are truncated; if the bit-width is increased, the integer is zero extended. * ''bool(x)'': ''x'' must be an integer expression. This returns false if the integer is zero, and true otherwise. Syntactic replication is also supported for the operators ''&'', ''|'', ''^'', ''+'', and ''*''. This means the following expression is valid (+ i : 3 : p[i] + 2*i) and is equivalent to p[0] + 2*0 + p[1] + 2*1 + p[2] + 2*2 ===== Parameters and constant expressions ===== Parameter expressions are used to compute parameter values (''pint''/''pbool''/"preal") and are evaluated at expansion time. These expressions are signed integers, and use 64-bit integer arithmetic. All constants are simplified at expansion time using the same 64-bit integer arithmetic as parameters. In the context of expressions in the ACT language where the entire expression evaluates to a run-time constant, ''int(x)'' can also be specified when ''x'' is a real expression. In this case, the fractional part of the number is discarded. This means, for example, that pint y = 2; pint x = int(5.4/y) will succeed, while int x, y; chp { y := 2; x := int(5.4/y) } will report an error. ===== Expressions in CHP ===== The same expression syntax is also used in the ''chp'' and ''dataflow'' sub-languages. Once again, constant expressions are simplified as above. However, expressions can also include variables that are determined at run-time rather than expansion time (i.e. when the circuit is executing the specified computation.) In this case, we need rules to determine the bit-width of an expression. ==== Bit-width rules ==== The rules that are used to determine the bit-width of an expression involving circuit variables are as follows: * Each variable has the bit-width specified by its type. * A constant uses the minimum number of bits needed to represent it. Note that a negative constant is assumed to be a two's complement value, an its bit-width is determined in the same way. If you are unsure about what is going to happen and want to control the bit-width of a constant, ''int(const, width)'' can be used. Be careful about is negative constants, since the ''int(.)'' operation will zero-extend a value and not sign-extend it. * For unary operators, the bit-width of the result is the same as the bit-width of the argument * For binary operators and ternary operators where the result is an integer, let //left// be the bit-width of the left-hand side of the operator, and //right// be the bit-width of the right hand side. There are six categories of result bit-widths: - max(//left//,//right//) ; the smaller operand is zero-extended as needed. * bitwise AND ''&'' * bitwise OR ''|'' * bitwise XOR ''^'' * Query expressions where the result is an integer. In this case //left// and //right// are the bit-widths of the two options in the query expression, since the first part of the query expression is Boolean. - 1+max(//left//,//right//) * addition ''+'' * subtraction ''-'' - //left//+//right// * multiplication ''*'' - //left// * division ''/'' * logical right shift ''>>'' * arithmetic right shift ''>>>'' - //right// * mod ''%'' - //left// + 2^//right// - 1 * left shift ''<<'' * For concatenation, the bit-width is the sum of all the components. * For bitfield extraction, the bitwidth is determined by the number of bits extracted. While these bit-width rules are nice because you never lose bits, they can have some unexpected consequences. One of the not-so-nice effects of these rules is that, technically, addition is no longer associative in general! For example, consider the following two different assignment statements: int<2> a; int<3> b; int<4> c; ... chp { ... x := (a + b) + c; y := a + (b + c); ... } Applying the bit-width rules, the expression ''(a+b) + c'' has bit-width 5, whereas ''a + (b + c)'' has bit-width 6. While this does not have any consequences in this particular example, it could become problematic if the bitwise complement operator is used, or in the case where this expression needs to be negated (since subtraction is essentially taking the two's complement and then adding, which includes the bitwise complement operator). Another strange example is: ... chp { ... x := x - 1; y := y + (-1); ... } Now the right hand side of the first assignment takes ''x'' and ''1'' and does the subtraction that was expected. The second assignment, on the other hand, takes the bit-pattern for ''-1'' (this turns out to be ''1''), and adds it to ''y''! In other words, ''x-1'' and ''x+(-1)'' are not the same because of the way the bit-width rules operate. If you have any doubts, the ''int(...)'' operator can be used to specify the bit-width. ==== Conversion from/to integer to/from pure structures ==== A [[language:types2:data#pure_structure|pure structure]] in ACT is a user-defined type that only contains integers and boolean valued variables. This can be interpreted as a collection of bits, where the number of bits is the sum of all the components of the pure structure. The expression syntax allows conversions between pure structures and integers, and vice versa, as in the example below. mystructname s; int<8> x; ... chp { ... x := int(s); s := mystructname(x) ... } In this example, ''int(s)'' converts the pure structure into an integer with the appropriate bit-width. Note that the assignment may discard bits (if the bits needed exceeds 8) or zero-pad the result (if the bits needed is less than 8). The expression "mystructname(x)" does the opposite. When a structure has multiple fields, ACT packs them into an integer in left-to-right field order. So, for example, the structure deftype mystruct (int<4> a; bool b; int<3> c) { } would have the ''a'' field as the highest order bits, followed by ''b'' and then ''c''. Conversion to a structure from an integer follows the same unpacking convention. ===== Functions ===== ACT provides support for user-defined functions. These functions can be used to make your design more understandable. The syntax of a function is illustrated by the following example: function f (pint x) : pint { chp { self := x + 1 } } The ''function'' keyword is used to define a function. Here, function ''f'' is defined, and it takes one parameter (''x'' of type ''pint''), and has a return type ''pint'' (indicated by '': pint''). The CHP language is used to implement the body of a function. The special variable ''self'' can be used in the body of the CHP language, and its value on termination of the CHP program indicates the return value. Additional parameters that might be helpful as auxilliary variables can be defined within the body of the function in the usual way. function sumint (pint x) : pint { pint i; chp { i := 0; self := 0; *[ i < x -> self := self + i; i := i + 1 ] } } There are two flavors of functions: * Functions where all the arguments and return type are parameter types * Functions where all the arguments and return types are non-parameter data types The first kind of function is typically used when constructing the circuit, and its value can be statically computed during the circuit construction phase. The second kind of function is viewed as CHP, and can be implemented either by inlining the function or through some other approach (e.g. shared, partially shared, etc). For example, the following function that takes an ''int<8>'' argument can be called from the CHP sub-language: /* look at the MSB to determine if this is a negative 2's complement integer */ function isnegative (int<8> x) : bool { chp { self := bool(x{7}) } } ===== External Functions ===== ACT is a hardware description language. However, especially when simulating a design, it is useful to be able to have additional functionality that goes beyond the implemented circuit. For example, a designer may want to test a CHP program by providing it inputs from a test suite that is contained in a file. To do so would require providing an I/O library for ACT. Similarly there may be other additional features one might want for simulation purposes. Instead of providing specific solutions to a large number of potential use cases, ACT provides a single generic mechanism: the ability to call an //external// function. An external function is one that is not defined within the ACT language itself, but is rather defined in the C programming language. The C function must be compiled into a shared object library that is loaded by ACT at run-time. To define an external function, simply declare the function in ACT without providing a definition. // an external function with parameter arguments function myextfunc (pint a) : pint; In this example, function ''myextfunc'' is an external function that only involves parameters. Like all parameter-based expressions, the function will be evaluated during the expansion phase and replaced with a constant value. // an external function with circuit variable arguments function myextfunc2 (int<4> a) : int<5>; In this example, function ''myextfunc2'' is an external function that involves circuit arguments. While this can be used for simulation purposes, any synthesis/circuit generation operations will fail when confronted with such a function. Any CHP program that calls an external circuit function will be marked as //non-synthesizable// by the ACT library. Note that these functions still serve a useful purpose---in particular, they can be useful when modeling the environment of the circuit that is to be implemented, or could provide a reference implementation during hardware development. ==== External parameter functions ==== To implement the external parameter function, you must provide two things: a C implementation with a specific function prototype, and a ACT configuration file that maps the ACT function name to the C function name. Since ''pint'' types are 64-bit signed integers, the function declaration for an external function would look something like this: long myexternimpl (int nargs, long *args) { int i; /* nargs = # of arguments */ printf ("got %d args\n", nargs); for (i=0; i < nargs; i++) { printf (" arg[%d] = %ld\n", i, args[i]); } return 10; } In this example, the function simply prints out the number of arguments and the values passed in, returning a constant value. The second part is specifying the mapping from the ACT function name to the C function name. Since external functions can be organized in multiple libraries, the way this is specified is the following: begin act begin extern string_table libs "mylib" begin mylib string path "libpath.so" string myextfunc "myexternimpl" end end end The specification of the mapping is contained within the ''act'' and ''extern'' blocks. The list of libraries is specified as a string table called ''libs''. These library names are then used as the names of library specifier ''begin''..''end'' blocks, one per library. The library specifier block contains the path to the shared object library that contains the external function definitions via the ''path'' string. Finally, a list of mapping strings that map ACT function names to the corresponding C function name must also be included in the library block. Finally, the C file must be compiled as a shared object file, and assembled into a library. For example, on Linux, one might use the following commands to create ''libpath.so'' from a C definition in ''example.c''. $ gcc -shared -DPIC -fPIC -c example.c -o example.os $ gcc -shared -Wl,-x -o libpath.so example.os ==== External circuit functions ==== External circuit functions are implemented in a similar fashion. Currently, only the ACT simulator (''actsim'') uses this functionality, since the rest of the implementation flow simply views such external functions as non-synthesizable components. The C implementation of external functions uses a function prototype that includes bit-width specifiers for all the data types. (Currently there is a limit of 64 bits on the bit-width of arguments/results from external functions.) /* this includes the simple external interface datatype used */ #include struct expr_res myexternimpl2 (int nargs, struct expr_res *args) { struct expr_res r; int i; /* nargs = # of arguments */ printf ("got %d args\n", nargs); for (i=0; i < nargs; i++) { printf (" arg[%d] = %lu (width=%d)\n", i, args[i].v, args[i].width); } r.width = 5; r.v = 4; return r; } The specification of the mapping configuration file is similar to the case of external parameter functions. The only difference is that the outer most ''begin''/''end'' block is ''begin sim''..''end'' instead of ''begin act''..''end''. An example of file I/O implemented with external functions can be found in the ''actsim'' [[https://github.com/asyncvlsi/actsim|git repository]] in the ''simlib/'' directory. ===== Operator Precedence ===== The operators have the following precedence, from the highest to lowest: - ''~'', ''!'' - ''*'', ''/'', ''%'' - ''+'', ''-'' - ''<<'', ''>>'', ''>>>'', ''<'', ''>'', ''<='', ''>='', ''='', ''!='' - ''&'' - ''^'' - ''|'' - ''?''