User-defined types

User-defined type can be used to create complex circuit structures. A new user-defined type name is introduced by using defproc, defcell, defchan, or deftype statements. All user-defined types have the same basic structure:

  1. a type signature, that provides information about the interface to the type and the ports that are externally visible; and
  2. a body, contained in braces, that specifies the detailed definition of the user-defined type.

The type chosen for each port must be the most specific type used by that port in the body (see the implementation relation section).

User-defined types can also be parameterized, and this is covered in detail later.

Process, channel, and data types can include methods that provide mechanisms to manipulate the type or access parts of the type. There are a number of special built-in method names that can be specified for data types and channel types.

Instantiating user-defined types

User-defined type variables can be instantiated in much the same manner as ordinary type variables.

defproc test(bool N, n) { ... }
test x;
// x.N and x.n refer to the ports of ''x''

The ACT description above creates an instance of type test named x. Creating an instance of a type creates instances of all the ports listed as well as creating whatever is specified by the body of the type definition. The list of ports of a user-defined type can be accessed from the scope outside the type definition by using dot-notation. These externally visible ports are analogous to the fields of structures or record types in standard programming languages.

This analogy to records can be used to build complex data types, albeit with slightly different syntax compared to traditional programming languages.

Data types can be implemented using Booleans, where Boolean variables correspond to signals that can be accessed at the circuit level. In this case, conversions between the higher level description (e.g. an integer) and signals can be also described using set/get methods as illustrated below.

deftype mystruct <: int<16> (bool b[16])
{
  methods {
   set {
     (;i:16: [self{i} = 1 -> b[i]+ [] self{i} = 0 -> b[i]- ])
   }
   get {
     self := 0;
     (;i:16: [b[i] -> self := self | (1 << i) [] ~b[i] -> skip ])
   }
  }
}

Parameterized user-defined types

Processes, channels, and datatypes created using defproc, defchan, and deftype all support parameterization. Parameters are specified using the template keyword.

Since the syntax for all three is the same, we use a process definition to illustrate this. To create a parameterized type, the definition of the type is preceeded by a template specifier as shown below.

// A generic adder block
template<pint N> 
defproc adder (e1of2 a[N], b[N]; e1of2 s[N])
{
  ...
}

This example defines an adder that takes N as a parameter. Note that the value of N determines the size of the arrays in the port list for the process. Instances of this adder can be created in the following way:

adder<4> a1;  // a1 is a 4-bit adder
adder<16> a2; // a2 is a 16-bit adder

The value of a1.N is 4, while the value of a2.N is 16. To illustrate how one might define this adder block, assume we have processes fulladder, zerosource, and bitbucket already defined that implement a full-adder, a constant source of zeros, and a constant sink respectively. One possible definition of the adder would be:

template<pint N>
defproc adder (e1of2 a[N], b[N]; e1of2 s[N])
{
   fulladder fa[N];
   ( i : N-1 : fa[i].a = a[i]; fa[i].b = b[i]; fa[i].s = s[i];
                fa[i].co = fa[i+1].ci; )
   zerosource z;
   bitbucket w;
   fa[0].ci=z.x;
   fa[N-1].co = w.x;
}

This creates a parameterized ripple-carry adder. Notice the use of loops and arrays to connect the carry chain for the adder, and the inputs and outputs of the process to the fulladder ports.

As shown in the example above, the arguments in the template parameter list are specified by listing them next to the type name. Trailing arguments can be omitted from the parameter list attached to the type as shown in the example below.

template<pint N; preal w[N]>
defproc test (bool n[N]) { ... }
 
test<5> x;

Channels and data types can also be parameterized in the same way. For example, the following might be an N-bit dual rail definition.

template<pint N>
deftype d1of2 <: int<N> (bool d0[N], d1[N]) { ... }

Since the body of the type can use loops and selection statements in arbitrary ways, changing the parameters for the type can completely change the structure of the circuit. It can also change the ports for the type. Hence, when checking for type compatibility, the values of parameters are also taken into account. Hence, the full type for instance a2 above is in fact adder<5>, not just adder. Types such as fulladder that do not have parameters are more completely specified as fulladder<>, although the angle brackets can be omitted. Arrays can only correspond to instances of the same type—so an array cannot contain a three-bit adder and five-bit adder.

Default parameters

When defining complex user-defined types with many parameters, it can be useful to have default parameter values. ACT has syntax to support default parameter values for trailing parameters in a template definition.

template <pint N; pbool active_high = true>
defproc driver(bool? inp; bool! outp)
{
  bool sig;
  prs {
    inp => sig-
  }
  [active_high -> prs { sig => outp- }
  [] else -> sig = outp;
  ]
}

(Note: this is not a real signal driver, but the idea here is the you have a parameterized driver that can drive a fanout of N gates.) This definition has a default value for the active_high parameter as true. So an instance

driver<4> x;

will have four production rules:

 x.inp -> sig-
~x.inp -> sig+
 sig -> x.outp-
~sig -> x.outp+

However, this behavior can be changed by using:

driver<4,false> x;

In this case, sig will be connected to x.outp.

Note that ACT is very strict about type-checking; so, for example, driver<4> and driver<4,true> are not treated as the same type even though the default parameter value for the second template parameter is true.

Direction flags

Direction specifications can be used for built-in data and channel types, as well as user-defined types. Consider the e1of2 user-defined channel type that we saw earlier:

defchan e1of2 <: chan(bool) (bool d0,d1,e)
{
   spec {
    exclhi(d0,d1)
   }
   ...
}

When we use e1of2? or e1of2!, we need some mechanism to specify the access permissions for the port parameters of the user-defined type. The convention used is that there are five possible ways to specify any constraints on access to port parameters. For our example, we can use one of the following variations in the port parameter list:

bool d0; // No constraints; this port could be read or written
bool! d0; // Both e1of2? and e1of2! have bool! permissions
bool? d0; // Both e1of2? and e1of2! have bool? permissions
bool?! d0; // e1of2? has bool? permissions, and e1of2! has bool! permissions
bool!? d0; // e1of2? has bool! permissions, and e1of2! has bool? permissions

Hence, a better definition of an e1of2 channel would more completely specify the access permissions for the port parameters in the following way.

defchan e1of2 <: chan(bool) (bool?! d0,d1; bool!? e) { ... }

A careful examination of the type signature reveals that the sender and receiver have the appropriate permissions. There is a subtle interaction between connections and directional types in ACT, and this is detailed in the section on connections.

Macros and Functions within User-defined types

User-defined types support additional methods (beyond the special ones for channels and data types). These methods are of two types:

  • macros, which correspond to CHP fragments that are used for in-place substitution; and
  • functions which are supported by pure structures, which are similar to traditional functions.

Macros

Functions