B-Lang-org / bsc

Bluespec Compiler (BSC)
Other
954 stars 146 forks source link

Preserving structure at synthesis boundaries #713

Open krame505 opened 4 months ago

krame505 commented 4 months ago

Currently, at synthesis boundaries all interface fields get turned into ports whose types are simply flattened bit vectors. This is rather annoying when trying to instantiate a Bluespec-synthesized module in hand-written Verilog. If an interface field contains complex structured data, the options are essentially to either

  1. Somehow dissect the bit vector in Verilog, which can be tedious and error-prone, or
  2. Make everything an interface so that wrapper generation creates a separate port for every sub-field, which can also lead to a tedious number of ports being created. This also may not be an option for data/tagged unions.

SystemVerilog has support for writing packed structs as port types. It would be very helpful if bsc could generate Verilog definitions of packed structs/unions corresponding to types used in interfaces, and use those types in synthesized module ports in place of bit vectors. (This would only affect the synthesized module interface - everything internal in the module would still be in terms of bit vectors.) There are two main challenges here - how to generate the definitions, and getting module synthesis to make use of them.

Note that the same types can be used in multiple interfaces and synthesized modules. It would be preferable to avoid generating multiple versions of the same type in a project. Thus I don't think this should be tied to synthesizing a particular module - it would be nice if bsc could spit out a separate .vh file containing the struct definitions. Users should also be able to customize the Verilog representation for types with a custom Bits instance.

I think for Verilog struct generation, an approach similar to the GenCRepr library I wrote would be preferable to making the compiler do this directly. This would be "just" a library using generics to generate the code at elaboration time. The Verilog representation for a type could be defined as a type class like

class VerilogRepr a where
  verilogRepr :: a -> (String, String)
  verilogDecl :: a -> Maybe String

The methods here take a proxy value to specify the type for the instance. verilogRepr is the left- and right-side portion of the type in verilog (e.g UInt 8 would be logic and [7:0].) Parametric types would be monomorphised, e.g. Maybe (Int 8) as Maybe_int8 or something like that. verilogDecl is the definition of the type, if it is a struct/data. This class could have a default instance for structs/data using generics, but users can override it if their type has a custom Bits instance.

Using generics tricks we could also have a utility to easilly write out the library containing the verilog representations for all the data types appearing in interfaces used by the project:

writeVerilogDecls :: (GenAllVerilogDecls tys) => String -> tys -> Module Empty
...

-- For example
{-# synthesize #-}
mkTop :: Module Top
mkTop = module
  writeVerilogDecls "lib_name" (_ :: (Ifc1, Ifc2, Ifc3))
  ...

Controlling when interface fields get flattened or use the struct representation is something I'm a bit less sure about. This should definitely be an opt-in feature, and probably something controlled per-type. Since there would be a default generic instance for VerilogRepr, we don't want to use the Verilog representation for any type with an instance. We could have another class

class (VerilogRepr a) => VerilogIfc a

such that users could write instance VerilogIfc MyType for any type appearing in an interface that should not be flattened. Another option, if we want finer-grained control, is to introduce a pragma on interface fields to specify the structured representation should be used; however specifying this on every interface would be tedious if one wishes to generally opt-in to this feature.

My current thinking is maybe have wrapper generation check for an instance of VerilogIfc for each field type, and if so add the pragma. This may require type representation to be computed as a type-level string, since this would happen significantly before elaboration time; i.e. something like

class (VerilogRepr :: * -> $ -> $ -> *) a lrepr repr where
  verilogDecl :: a -> Maybe String

This wouldn't be too much of an issue, except that we would need to actually add a type-level string append function like TAdd and friends.

The general idea originated in brainstorming with @mieszko, but I would like feedback from others before going too much further. We can discuss in the sync meeting on Friday - hopefully @quark17 is available? I would like to run this past @nanavati too but I think he is on vacation for the rest of the week. On a logistical note, I have about 2 months left in my internship this summer, so whatever I attempt I would like to have essentially wrapped up by the first week of September.

krame505 commented 4 months ago

To clarify, this would not affect anything to do with ready/enabled signals; the only change would be to the Verilog representation of (non-interface) fields of interfaces. Whether a value method should be split into separate ports would still be controlled solely by marking it as a struct or interface; many types that are currently interfaces solely to permit accessing the fields as separate ports could instead become structs. Trying to e.g. permit multiple interface action methods to share an enabled signal is not in scope.

Also note that we would still like to generate Verilog packed structs for interfaces that are used as action method inputs, since these are not split into separate ports by wrapper generation.

@mieszko pointed out that one might wish to specify the name of the generated Verilog struct for a type without overriding the generic implementation of its representation. There is no reason why these would need to be the same type class:

class VerilogRepr a where
  verilogRepr :: a -> (String, String)

class (VerilogRepr a) => VerilogImpl a where
  verilogImpl :: a -> Maybe String

These classes could have separate default generic implementations. Organizationally this split might also be helpful as VerilogRepr probably needs to be in the Prelude, if it is consulted during wrapper generation, but VerilogImpl (which would contain the bulk of the complexity) could live in a library, alongside utilities for writing out header files at elaboration time, etc.

krame505 commented 4 months ago

@nanavati pointed out that simply turning input/output ports into structs would not help much with debugging, since all the logic within a module would still be just in terms of bit vectors and slicing.

We also discussed a separate idea for using generics to improve wrapper generation, which is mostly orthogonal to this idea. I'll open a separate issue for that.