[RFC][SV] Adding Interface Operations to SV Dialect

@jdd @clattner moving this over from the old forum.

Introduction

After some discussion with @jdd, it seems useful to add SystemVerilog interfaces to the SV dialect. The goal is to add some “AST-like” operations that can represent SystemVerilog interfaces in MLIR and ultimately be emitted as valid SystemVerilog.

To support this, I propose to add 3 new operations to the SV dialect: interface, interface signal, and interface modport. I’ll put snippets of ODS to illustrate them.

Interface

def InterfaceOp : SVOp<"interface",
    [Symbol, SymbolTable, ...]>, ... {
  ...
  let regions = (region SizedRegion<1>:$body);
}

An interface serves to give a name to a group of signals and modports.

  • It is a Symbol, so it can be defined inside a larger SymbolTable, and looked up by name
  • It is also a SymbolTable itself, to look up the definitions nested inside it (see below)
  • It contains a region that exists to hold its child definitions (see below)

Interface Signal

def SignalTypeAttr : TypeAttrBase<"IntegerType", "Integer type attribute">;

def InterfaceSignalOp : SVOp<"interface.signal", [Symbol, HasParent<"InterfaceOp">]>,
    ... {
  let arguments = (ins
    StrAttr:$sym_name,
    SignalTypeAttr:$type
  );
}

The interface signal operation gives a name and a type to a signal.

  • It is defined inside an interface’s region
  • It is a Symbol, so it can looked up inside its parent
  • It contains an attribute to name the signal
  • It contains a type to indicate the signal’s size

We discussed what SystemVerilog construct this operation should represent. For now, to emit this as SystemVerilog, we can emit logic, with the bitwidth determined by the type. Down the road, if there were hypothetical sv.logic, sv.wire, etc. types, we could use that to guide the emitter.

For the initial proposal, we discussed simply supporting an IntegerType from the standard MLIR types. This should suffice to emit the bitwidth and signedness of a signal for now. As more of the SV dialect is fleshed out, it should be relatively smooth to support new types.

Interface Modport

def ModPortDirectionInput : StrEnumAttrCase<"input">;
def ModPortDirectionOutput : StrEnumAttrCase<"output">;
def ModPortDirectionInout : StrEnumAttrCase<"inout">;

def ModPortDirectionAttr : StrEnumAttr<"ModPortDirectionAttr",
  "Defines direction in a modport",
  [ModPortDirectionInput, ModPortDirectionOutput, ModPortDirectionInout]>;

def ModPortDirectionField : StructFieldAttr<"direction", ModPortDirectionAttr>;

def ModPortSignalField : StructFieldAttr<"signal", FlatSymbolRefAttr>;

def ModPortStructAttr : StructAttr<"ModPortStructAttr", SVDialect,
  [ModPortDirectionField, ModPortSignalField]>;

def ModPortStructArrayAttr : TypedArrayAttrBase<ModPortStructAttr,
  "array of modport structs">;

def InterfaceModPortOp : SVOp<"interface.modport", [Symbol, HasParent<"InterfaceOp">]>,
    ... {
  ...
  let arguments = (ins
    StrAttr:$sym_name,
    ModPortStructArrayAttr:$ports
  );
}

The interface modport operation names a modport and gives the directions to the signals.

  • It is defined inside an interface’s region
  • It is a Symbol, so it can looked up inside its parent
  • It contains an attribute to name the modport
  • It contains an array of FlatSymbolRefAttr to list the signals from the interface
  • It contains an array of StrEnumAttr to list the directions of the signals in the modport

There are different ways we can store this information. This proposal is using an array of structs, where each struct contains a symbol reference and an enum to indicate its direction. I used a string enum since it made a nice example but it could be a different implementation.

Use Cases

Given the above operations, and some appropriate helper methods, we can answer questions like the following:

  • Look up an interface by name
  • Look up a signal in an interface by name and get its type/bitwidth
  • Look up a modport in an interface by name
  • Look up a signal’s direction in a modport

Are there other necessary/sufficient conditions I am missing? I’m hoping this minimal proposal will be enough to be useful in multiple places.

For example, I’m imagining we could build interfaces for FIRRTL bundle types, and come up with a way to reference such interfaces in the module’s ports. That was the motivating example the got me interested in this proposal, but it is just one potential use case.

I’m imagining at the very least it would be useful to be able to emit these operations as SystemVerilog.

Please chime in with other concrete use cases.

Example

Putting it all together, you would be able to define the proposed operation something like this:

  sv.interface @handshake_example {
    sv.interface.signal @data : i32
    sv.interface.signal @valid : i1
    sv.interface.signal @ready : i1
    sv.interface.modport @dataflow_in (@data : "input", @valid : "input", @ready : "output")
    sv.interface.modport @dataflow_out (@data : "output", @valid : "output", @ready : "input")
  }                                                                                                                                                                                                           

Beyond that, I imagine we could add builders, helper methods, EmitVerilog support, etc.

Pull Request

I put together a pull request containing the ideas outlined in this proposal here: https://github.com/llvm/circt/pull/73.

I’m open to any and all feedback.