1 \chapter[chap:description]{Hardware description}
2 This chapter will provide an overview of the hardware description language
3 that was created and the issues that have arisen in the process. It will
4 focus on the issues of the language, not the implementation.
6 When translating Haskell to hardware, we need to make choices in what kind
7 of hardware to generate for what Haskell constructs. When faced with
8 choices, we've tried to stick with the most obvious choice wherever
9 possible. In a lot of cases, when you look at a hardware description it is
10 comletely clear what hardware is described. We want our translator to
11 generate exactly that hardware whenever possible, to minimize the amount of
12 surprise for people working with it.
14 In this chapter we try to describe how we interpret a Haskell program from a
15 hardware perspective. We provide a description of each Haskell language
16 element that needs translation, to provide a clear picture of what is
19 \section{Function application}
20 The basic syntactic element of a functional program are functions and
21 function application. These have a single obvious VHDL translation: Each
22 function becomes a hardware component, where each argument is an input port
23 and the result value is the output port.
25 Each function application in turn becomes component instantiation. Here, the
26 result of each argument expression is assigned to a signal, which is mapped
27 to the corresponding input port. The output port of the function is also
28 mapped to a signal, which is used as the result of the application.
30 An example of a simple program using only function application would be:
33 -- | A simple function that returns the and of three bits
34 and3 :: Bit -> Bit -> Bit -> Bit
35 and3 a b c = and (and a b) c
38 This results in the following hardware:
42 \subsection{Partial application}
43 It should be obvious that we cannot generate hardware signals for all
44 expressions we can express in Haskell. The most obvious criterium for this
45 is the type of an expression. We will see more of this below, but for now it
46 should be obvious that any expression of a function type cannot be
47 represented as a signal or i/o port to a component.
49 From this, we can see that the above translation rules do not apply to a
50 partial application. Let's look at an example:
53 -- | Multiply the input word by four.
54 quadruple :: Word -> Word
55 quadruple n = mul (mul n)
60 It should be clear that the above code describes the following hardware:
64 Here, the definition of mul is a partial function application: It applies
65 \hs{2 :: Word} to the function \hs{(*) :: Word -> Word -> Word} resulting in
66 the expression \hs{(*) 2 :: Word -> Word}. Since this resulting expression
67 is again a function, we can't generate hardware for it directly. This is
68 because the hardware to generate for \hs{mul} depends completely on where
69 and how it is used. In this example, it is even used twice!
71 However, it is clear that the above hardware description actually describes
72 valid hardware. In general, we can see that any partial applied function
73 must eventually become completely applied, at which point we can generate
74 hardware for it using the rules for function application above. It might
75 mean that a partial application is passed around quite a bit (even beyond
76 function boundaries), but eventually, the partial application will become
80 An import concept in functional languages is recursion. In it's most basic
81 form, recursion is a function that is defined in terms of itself. This
82 usually requires multiple evaluations of this function, with changing
83 arguments, until eventually an evaluation of the function no longer requires
86 Recursion in a hardware description is a bit of a funny thing. Usually,
87 recursion is associated with a lot of nondeterminism, stack overflows, but
88 also flexibility and expressive power.
90 Given the notion that each function application will translate to a
91 component instantiation, we are presented with a problem. A recursive
92 function would translate to a component that contains itself. Or, more
93 precisely, that contains an instance of itself. This instance would again
94 contain an instance of itself, and again, into infinity. This is obviously a
95 problem for generating hardware.
97 This is expected for functions that describe infinite recursion. In that
98 case, we can't generate hardware that shows correct behaviour in a single
99 cycle (at best, we could generate hardware that needs an infinite number of
102 However, most recursive hardware descriptions will describe finite
103 recursion. This is because the recursive call is done conditionally. There
104 is usually a case statement where at least one alternative does not contain
105 the recursive call, which we call the "base case". If, for each call to the
106 recursive function, we would be able to detect which alternative applies,
107 we would be able to remove the case expression and leave only the base case
108 when it applies. This will ensure that expanding the recursive functions
109 will terminate after a bounded number of expansions.
111 This does imply the extra requirement that the base case is detectable at
112 compile time. In particular, this means that the decision between the base
113 case and the recursive case must not depend on runtime data.
115 \subsection{List recursion}
116 The most common deciding factor in recursion is the length of a list that is
117 passed in as an argument. Since we represent lists as vectors that encode
118 the length in the vector type, it seems easy to determine the base case. We
119 can simply look at the argument type for this. However, it turns out that
120 this is rather non-trivial to write down in Haskell in the first place. As
121 an example, we would like to write down something like this:
124 sum :: Vector n Word -> Word
125 sum xs = case null xs of
127 False -> head xs + sum (tail xs)
130 However, the typechecker will now use the following reasoning (element type
131 of the vector is left out):
134 \item tail has the type \hs{(n > 0) => Vector n -> Vector (n - 1)}
135 \item This means that xs must have the type \hs{(n > 0) => Vector n}
136 \item This means that sum must have the type \hs{(n > 0) => Vector n -> a}
137 \item sum is called with the result of tail as an argument, which has the
138 type \hs{Vector n} (since \hs{(n > 0) => n - 1 == m}).
139 \item This means that sum must have the type \hs{Vector n -> a}
140 \item This is a contradiction between the type deduced from the body of sum
141 (the input vector must be non-empty) and the use of sum (the input vector
142 could have any length).
145 As you can see, using a simple case at value level causes the type checker
146 to always typecheck both alternatives, which can't be done! This is a
147 fundamental problem, that would seem perfectly suited for a type class.
148 Considering that we need to switch between to implementations of the sum
149 function, based on the type of the argument, this sounds like the perfect
150 problem to solve with a type class. However, this approach has its own
151 problems (not the least of them that you need to define a new typeclass for
152 every recursive function you want to define).
154 Another approach tried involved using GADTs to be able to do pattern
155 matching on empty / non empty lists. While this worked partially, it also
156 created problems with more complex expressions.
158 TODO: How much detail should there be here? I can probably refer to
161 Evaluating all possible (and non-possible) ways to add recursion to our
162 descriptions, it seems better to leave out list recursion alltogether. This
163 allows us to focus on other interesting areas instead. By including
164 (builtin) support for a number of higher order functions like map and fold,
165 we can still express most of the things we would use list recursion for.
167 \subsection{General recursion}
168 Of course there are other forms of recursion, that do not depend on the
169 length (and thus type) of a list. For example, simple recursion using a
170 counter could be expressed, but only translated to hardware for a fixed
171 number of iterations. Also, this would require extensive support for compile
172 time simplification (constant propagation) and compile time evaluation
173 (evaluation constant comparisons), to ensure non-termination. Even then, it
174 is hard to really guarantee termination, since the user (or GHC desugarer)
175 might use some obscure notation that results in a corner case of the
176 simplifier that is not caught and thus non-termination.
178 Due to these complications, we leave other forms of recursion as