Introduction/Glossary

The goal of this guide is to help you understand how so-called “synthesizable” values returned from Piplin functions work.

  • pipinst: these represent the actual binary values flowing through the wires
  • AST: these represent deferred computations that need to know the values of registers or inputs before they can be computed
  • module: a container that organizes Piplin values and registers. These are not discussed much in this guide; see the modules tutorial for more information

pipinsts

Let’s look at some examples of pipinsts:

  • ((uintm 8) 3) is an unsigned int with modulo on overflow, 8 bits wide, with the value 3 (binary #b0000_0011)
  • (cast (sfxpts 7 3) 5.23) is a signed fixed-point number that satures on overflow (tops or bottoms out), with 7 bits for the integer part, and 3 bits for the fractional part, with the closest representable value to 5.23 (binary #b0000101_001)
  • true is a boolean (which is 1 bit wide) (binary #b1)

These cover some of the primitive types. Other numeric types include sints, which is a signed int with saturation on overflow, and complex, which takes another numeric type and adjoins a complex component, so that you can use +, -, and * on complex numbers in hardware.

For more parameterizable types, we usually use a def to make the type easier to reuse:

(def an-enum-type
  (enum #{:foo :bar :baz}))

(an-enum-type :foo)
;; => AST(PiplinEnum[...], :foo)
(cast an-enum-type :bar)
;; => AST(PiplinEnum[...], :bar)
(an-enum-type :baz)
;; => AST(PiplinEnum[...], :baz)
(an-enum-type :quux)
;; => Exception: "Cannot promote :quux to PiplinEnum[...]"

As we can see above, enums are defined by providing a set of values. Then, we can promote Clojure keywords to Piplin enums by invoking the enum like a function, or casting the keyword to the enum type.

(def a-bundle-type
  (bundle {:a (anontype :boolean)
           :b an-enum-type}))

(cast a-bundle-type {:a true :b :baz})
;; => AST(Bundle[...], {:a true, :b AST(PiplinEnum[...], :baz)})
(a-bundle-type {:a true :b :baz}
;; => Exception: "{:a true :b baz} does not match schema {:a AnonType[:boolean], :b PiplinEnum[...]}"

In the bundle’s definition, you may have noticed the (anontype :boolean). This is how to get the type of boolean values, i.e. true and false. Some of the Clojure types don’t have any type arguments. Since they are types with only a kind, they’re constructed with anontype. :boolean is the most common, but :keyword and :j-int can be used for Clojure keywords and JVM integers, respectively.

bundles are constructed by casting a map to the correct bundle type. Due to an issue with aggregate types, bundles must always be constructed via cast. This is explained below.

Warning about Aggregate types

All aggregate types (union, bundle, and array) have a bug where you must cast values, instead of being able to use the type as a constructor. This will be addressed in a future version.

Deferred Values

First, let’s see how adding uintms works:

(assert (= (+ ((uintm 8) 3)
              ((uintm 8) 4))
           ((uintm 8) 7)))

Uninst

Now, let’s see what happens if we add ((uintm 8) 3) to a register. As a debugging aid, there is a function piplin.types/uninst that takes a pipinst and returns a “deferred” version of that value. This will cause a typed fragment of AST to be constructed:

(+ ((uintm 8) 3)
   (piplin.types/uninst ((uintm 8) 4)))
;; => AST(type: UIntM[8]
;; =>     data:
;; =>     {:args
;; =>      {:rhs
;; =>       AST(type: UIntM[8]
;; =>           data:
;; =>           {:args {:expr AST(type: UIntM[8] data: 4 )}, :op :noop} ),
;; =>       :lhs AST(type: UIntM[8] data: 3 )},
;; =>      :op :+} )

We can see that we end up with an AST fragment whose topmost node has type UIntM[8]. data: marks the section that describes what sort of AST this is. In this case, it’s a deferred function evaluation, because it has the :op key, which determines the function, +. The :args key contains all the sub-fragments this one depends on. Arguments (in this case, :lhs and :rhs) have meaning determined by the :op.

The uninst’s AST representation has the :op set to :noop and one argument, :expr, which is ((uintm 8) 4). All non-terminal AST elements have an :op; however, some AST elements don’t actually perform any operation. For these, their :op wil be :noop, which indicates to the runtime that the AST node simple passes through its contained value, doing nothing to it.

Registers and Inputs

Here are examples of what module registers and inputs look like when printed:

;; a register named :foo
AST(UIntM[8], {:port-type :register, :port :foo, :args {}, :op :port})

;; an input named :foo
AST(UIntM[8], {:port-type :input, :port :foo, :args {}, :op :port})

Tying it all together

When you use a function like +, assoc, or nth, the value returned will vary depending on the arguments. If the arguments are Clojure values, the return values will be as usual in Clojure; however, if the arguments are Piplin values, the return value could be either a pipinst or a deferred AST fragment. If all of the arguments are pipinsts, then they will be immediately evaluated. If one or more of the arguments is a deferred AST fragment, then a new deferred AST fragment will be returned. This records what computation to do once the deferred values are available.

This design is meant to make it easier to write typed Piplin code using regular Clojure control flow. The ultimate goal is to make it easier to port algorithms that are initially designed and written as software, i.e. as plain Clojure, into FPGAs. As you develop your design, you will transition Clojure values into Piplin values. Then, instead of interacting with your functions on the REPL or in unit tests, you can wire them into modules, so that they can be further tested and simulated, then synthesized.

Another benefit of evaluating pipinsts immediately is that it reduces the amount of computation that is done at runtime, since everything that can be computed before simulation or synthesis will be precomputed.

Now you should know how Piplin is able to run at the REPL, in simulations, and in Verilog. Hopefully, this will help you better understand, interpret, and debug your designs.