Limitations of the compiler
Here is a list of Haskell features for which the Clash compiler has only limited support (for now):
-
Recursively defined functions
At first hand, it seems rather bad that a compiler for a functional language cannot synthesize recursively defined functions to circuits. However, when viewing your functions as a structural specification of a circuit, this feature of the Clash compiler makes sense. Also, only certain types of recursion are considered non-synthesizable; recursively defined values are for example synthesizable: they are (often) synthesized to feedback loops.
Let us distinguish between three variants of recursion:
-
Dynamic data-dependent recursion
As demonstrated in this definition of a function that calculates the n'th Fibbonacci number:
fibR 0 = 0 fibR 1 = 1 fibR n = fibR (n-1) + fibR (n-2)To get the first 10 numbers, we do the following:
>>> import qualified Data.List as L >>> L.map fibR [0..9] [0,1,1,2,3,5,8,13,21,34]The
fibRfunction is not synthesizable by the Clash compiler, because, when we take a structural view,fibRdescribes an infinitely deep structure.In principle, descriptions like the above could be synthesized to a circuit, but it would have to be a sequential circuit. Where the most general synthesis would then require a stack. Such a synthesis approach is also known as behavioral synthesis, something which the Clash compiler simply does not do. One reason that Clash does not do this is because it does not fit the paradigm that only functions working on values of type
Signalresult in sequential circuits, and all other (non higher-order) functions result in combinational circuits. This paradigm gives the designer the most straightforward mapping from the original Haskell description to generated circuit, and thus the greatest control over the eventual size of the circuit and longest propagation delay. -
Value-recursion
As demonstrated in this definition of a function that calculates the n'th Fibbonaci number on the n'th clock cycle:
fibS :: SystemClockResetEnable => Signal System (Unsigned 64) fibS = r where r = register 0 r + register 0 (register 1 r)To get the first 10 numbers, we do the following:
>>> sampleN @System 11 fibS [0,0,1,1,2,3,5,8,13,21,34]Unlike the
fibRfunction, the abovefibSfunction is synthesizable by the Clash compiler. Where the recursively defined (non-function) value r is synthesized to a feedback loop containing three registers and one adder.Note that not all recursively defined values result in a feedback loop. An example that uses recursively defined values which does not result in a feedback loop is the following function that performs one iteration of bubble sort:
sortV xs = map fst sorted :< (snd (last sorted)) where lefts = head xs :> map snd (init sorted) rights = tail xs sorted = zipWith compareAndSwap (lazyV lefts) rights compareAndSwap a b = if a < b then (a,b) else (b,a)Where we can clearly see that
leftsandsortedare defined in terms of each other. Also the abovesortVfunction is synthesizable. -
Static/Structure-dependent recursion
Static, or, structure-dependent recursion is a rather vague concept. What we mean by this concept are recursive definitions where a user can sensibly imagine that the recursive definition can be completely unfolded (all recursion is eliminated) at compile-time in a finite amount of time.
Such definitions would e.g. be:
mapV :: (a -> b) -> Vec n a -> Vec n b mapV _ Nil = Nil mapV f (Cons x xs) = Cons (f x) (mapV f xs) topEntity :: Vec 4 Int -> Vec 4 Int topEntity = mapV (+1)Where one can imagine that a compiler can unroll the definition of
mapVfour times, knowing that thetopEntityfunction appliesmapVto aVecof length 4. Sadly, the compile-time evaluation mechanisms in the Clash compiler are very poor, and a user-defined function such as themapVfunction defined above, is currently not synthesizable. We do plan to add support for this in the future. In the mean time, this poor support for user-defined recursive functions is amortized by the fact that the Clash compiler has built-in support for the higher-order functions defined inClash.Sized.Vector. Most regular design patterns often encountered in circuit design are captured by the higher-order functions inClash.Sized.Vector.
-
-
Recursive datatypes
The Clash compiler needs to be able to determine a bit-size for any value that will be represented in the eventual circuit. More specifically, we need to know the maximum number of bits needed to represent a value. While this is trivial for values of the elementary types, sum types, and product types, putting a fixed upper bound on recursive types is not (always) feasible. This means that the ubiquitous list type is unsupported! The only recursive types that are currently supported by the Clash compiler is the
Vector andRTreetypes, for which the compiler has hard-coded knowledge.For "easy"
Vector literals you should use Template Haskell splices and thelistToVecTHmeta-function. -
GADTs
Clash has experimental support for GADTs. Similar to recursive types, Clash cannot determine bit-sizes of GADTs. Notable exceptions to this rule are
VecandRTree. You can still use your own GADTs, as long as they can be removed through static analysis. For example, the following case will be optimized away and is therefore fine to use:x = case resetKind @System of SAsynchronous -> 'a' SSynchronous -> 'b' -
Floating point types
There is no support for the
FloatandDoubletypes, if you need numbers with a fractional part you can use theFixedpoint type.As to why there is no support for these floating point types:
-
In order to achieve reasonable operating frequencies, arithmetic circuits for floating point data types must be pipelined.
-
Haskell's primitive arithmetic operators on floating point data types, such as
plusFloat#plusFloat# :: Float# -> Float# -> Float#which underlie
Float'sNuminstance, must be implemented as purely combinational circuits according to their type. Remember, sequential circuits operate on values of typeSignal dom a.
Although it is possible to implement purely combinational (not pipelined) arithmetic circuits for floating point data types, the circuit would be unreasonable slow. So, without synthesis possibilities for the basic arithmetic operations, there is no point in supporting the floating point data types.
-
-
Haskell primitive types
Only the following primitive Haskell types are supported:
IntegerIntInt8Int16Int32Int64(not available when compiling with-fclash-intwidth=32on a 64-bit machine)WordWord8Word16Word32Word64(not available when compiling with-fclash-intwidth=32on a 64-bit machine)Char
There are several aspects of which you should take note:
-
IntandWordare represented by the same number of bits as is native for the architecture of the computer on which the Clash compiler is executed. This means that if you are working on a 64-bit machine,IntandWordwill be 64-bit. This might be problematic when you are working in a team, and one designer has a 32-bit machine, and the other has a 64-bit machine. In general, you should be avoidingIntin such cases, but as a band-aid solution, you can force the Clash compiler to use a specific bit-width forIntandWordusing the-fclash-intwidth=Nflag, where N must either be 32 or 64. -
When you use the
-fclash-intwidth=32flag on a 64-bit machine, theWord64andInt64types cannot be translated. This restriction does not apply to the other three combinations of-fclash-intwidthflag and machine type. -
The translation of
Integeris not meaning-preserving.Integerin Haskell is an arbitrary precision integer, something that cannot be represented in a statically known number of bits. In the Clash compiler, we chose to representIntegerby the same number of bits as we do forIntandWord. As you have read in a previous bullet point, this number of bits is either 32 or 64, depending on the architecture of the machine the Clash compiler is running on, or the setting of the-fclash-intwidthflag.Consequently, you should use
Integerwith due diligence; be especially careful when usingfromIntegralas it does a conversion viaInteger. For example:signedToUnsigned :: Signed 128 -> Unsigned 128 signedToUnsigned = fromIntegralcan either lose the top 64 or 96 bits depending on whether
Integeris represented by 64 or 32 bits. Instead, when doing such conversions, you should usebitCoerce:signedToUnsigned :: Signed 128 -> Unsigned 128 signedToUnsigned = bitCoerce
-
Side-effects:
IO,ST, etc.There is no support for side-effecting computations such as those in the
IOorSTmonad. There is also no support for Haskell's FFI.