Skip to content
dgrnbrg edited this page Feb 10, 2013 · 5 revisions

piplin is composed of several files now, which correspond to the current organization of the system. I'll document them here, and over time they'll be somewhat refactored into the conceptual boundaries of the system's modules.

Type System

types.clj

ITyped

Objects representing values known to/usable by piplin must implement the protocol ITyped. typeof is explained in Types and Kinds. value and pipinst? are explained in Instances. The rules for types and objects are explained in the relevant sections.

Types and Kinds

typeof is used to find the concrete type of the object. This type must be a map and contain the key :kind, which determines the kind of a type. A kind is essentially a type without its parameters set. Some examples of type/kind pairs are uint32/unsigned_integer, struct[uint8 x, boolean y]/structure. The kind must be in the hierarchy piplin.types/types, which can be queried and modified with derive-type and isa-type?. Most of piplin's types are defined in other files, as features are intended to be written with all of their functionality together (e.g. Verilog, VHDL, and simulation code). defpiplintype is a convenience macro to simplify creating new types; it allows the type to be used as a function to create instances.

anontype is a function that returns a type with only a kind. This is convenient for defining parameterless types. It is primarily used for JVM types that need to extra implementation, like (anontype :j-int) and (anontype :boolean) (n.b. booleans are synthesizable, but java integers aren't).

Instances

Objects that represent values in piplin implement ITyped. Values can be either known "right now", or they can represent a deferred computation. To determine if the value is known "right now", use the predicate pipinst?. Values which satisfy the pipinst? predicate are commonly referred to as "immediates" in compiler texts.

value is used to get the underlying value of this object. This is needed to support Clojure's map and vector destructuring. Since many of the values are themselves maps, but without explicitly requesting the underlying values, (:key piplin-datatype) returns a new value representing the action of extracting that value from the map.

ASTNode is a type that simplifies creating a dynamic object that participates in the piplin type system and optionally supports forms of destructuring. Since working with this type is itself quite difficult, the macros mkast (make ast) and mkast-explicit-keys are used instead. mkast takes 4 arguments: a piplin type, an operation keyword (used by the simulator and synthesizer to dispatch the translation multimethods) (e.g. :+, :bit-slice), a list of arguments, and a simulation function. The list of arguments is actually captured from the surrounding scope, and applied passed to the simulation function in the order they're declared. In the future, the simulation function is going to be defined as a defmethod, as Verilog generation works today.

mkast-explicit-keys splits the arguments and argument order into 2 argument vectors. This allows for AST fragments that take a varying number of arguments depending on their usage. An example of this is bundle constructors, which take as many arguments as they have slots.

All values must participate in the promotion system. Promotion is a way to take an immediate value of one type and convert it to an immediate value of another type. For instance, true could be promoted to (bits 1), yielding #b1 (this syntax is for bits literals, explained later).

Promotion is handled by implementing the eponymous multimethod: promote. Usually, a promote function inspects the type of the input, and uses a cond or condp to convert it to the correct form. It feels like this should be a double-dispatch point, but I have not yet run into a case where that was necessary, since we want to be very strict about what types convert to which others--this is a huge source of errors in hardware, and with 15 minute to 6 hour compiles, we'd rather spend 30 seconds writing an explicit coercion than discover that the synthesized design fails to work correctly.

Many values also participate in the instance system. Instances are an easy way to create piplin values whose data is represented in some other data structure. For example, the data of an object of (bits 3) is a vector of length 3 containing 0s and 1s, and the data of an unsigned integer with modulo arithmetic is a long within the correct constraints. The instance system makes these takes easy. To participate, you must implement the methods check and constrain. check takes an instance and returns a truthy value if the instance is appropriately constrained. constrain takes an instance that would be correct except for the components which must be constrained, and returns the constrained form. For many types, constrain is a no-op. But for some types, like uintm (unsigned int, modulo on overflow), constrain actually does something (in this case, modulo).

Error handling

Much of piplin uses the (throw+ (error ...)) idiom. That is defined in types.clj. (error ...) stringifies and concatenates all of its arguments, and returns a CompilerError, which can be thrown using throw+ from the slingshot library.

Additional details

pprint-ast is used to pretty-print astfrags. It works better than clojure.pprint/pprint, but it could be improved.

uninst is used to make an immediate appear to the compiler as a runtime value. It stands for un-instance. This is a critical tool for debugging new operations.

assoc-dist-fn is used to add a distribution function to the AST node. Distribution functions are used to pass on type refinement information to the arguments of the runtime expression. See mux2-impl in math.clj for details.

type-unify and try-errors are questionably useful. See their docstrings and usage for details; they could be deprecated.

JVM integration types are implemented in a special way to support using them unmodified. That is an advanced topic and can be understood by reading how :j-{int,long,float,double,short,byte}, :boolean, :keyword, and :null are implemented in piplin.types.numbers.

casting types

cast is a function that can forcibly convert one expr to another. It uses promote under the hood at runtime.