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. The rules 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.
  • 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:
    1. 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.
    2. 1+max(left,right)
      • addition +
      • subtraction -
    3. left+right
      • multiplication *
    4. left
      • division /
      • logical right shift »
      • arithmetic right shift »>
    5. right
      • mod %
    6. left + 2^right - 1
      • left shift «
  • For concatenation, the bit-width is the sum of all the components. For the 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.

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 <act/actsim_ext.h>
 
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 git repository in the simlib/ directory.

Operator Precedence

The operators have the following precedence, from the highest to lowest:

  1. ~, !
  2. *, /, %
  3. +, -
  4. «, », »>, <, >, , >=, =, !=
  5. &
  6. ^
  7. |
  8. ?