Skip to content

najaeda/naja-verilog

Repository files navigation

naja-verilog

build codecov License


Introduction

Naja-Verilog is a structural (gate-level) Verilog parser and can be used to read synthesis generated netlists.

This library provides a verilog interface to Naja SNL, however both projects are not tied and naja-verilog can be integrated in any project needing structural verilog support.

💁 If you have any questions, bug to report or request, please open an issue or send me a mail.

⭐ If you find Naja-Verilog interesting, and would like to stay up-to-date, consider starring this repo to help spread the word.

Acknowledgement

This project is supported and funded by NLNet through the NGI0 Entrust Fund.

Gate level netlist

This parser is dedicated to a very small part of verilog: the subset allowing to describe hierarchical gate level netlists. This is the format found at the output of synthesis tools such as Yosys.

To parse complete RTL level verilog or system verilog, please use projects such as: Verible. Apart the language support, the main difference with such RTL level parsing systems is that naja-verilog does not construct any AST but allows to construct the netlist on the fly while visiting the verilog source. Purpose is to reduce memory footprint and accelerate parsing time.

A comparable project can be found here: Parser-Verilog.


Compilation

Getting sources

# First clone the repository and go inside it
git clone https://github.com/najaeda/naja-verilog.git
cd naja-verilog
git submodule init
git submodule update

Dependencies

Mandatory dependencies:

  1. cmake: at least 3.22 version.
  2. Bison
  3. Flex

Embedded dependencies, through git sub module: google test.

On Ubuntu:

sudo apt-get install bison
sudo apt-get install flex

Using nix-shell:

nix-shell -p cmake bison flex

On macOS, using Homebrew:

brew install cmake bison flex

Ensure the versions of bison and flex installed via Homebrew take precedence over the macOS defaults by modifying your $PATH environment variable as follows:

export PATH="/opt/homebrew/opt/flex/bin:/opt/homebrew/opt/bison/bin:$PATH"

Building and Installing

#First define an env variable that points to the directory where you want naja-verilog to be installed:
export NAJA_INSTALL=<path_to_installation_dir>
# Create a build dir and go inside it
mkdir build
cd build
cmake <path_to_naja_sources_dir> -DCMAKE_INSTALL_PREFIX=$NAJA_INSTALL
#For instance: cmake ~/srcs/naja-verilog -DCMAKE_INSTALL_PREFIX=$NAJA_INSTALL
make
make test
make install

Create your own parser

Best starting point is to copy existing examples/implementations:

  • NajaVerilogSnippet: very simple snippet application verbosely printing visited objects.
  • VerilogConstructorTest: Example class used in Naja-verilog unit tests: visit verilog and collect in simple data structures.
  • Unit tests: covers trough unit testing most of the parser aspects.
  • SNLVRLConstructor: More concrete example showing Naja SNL (C++ gate netlist data structure) construction.

The principle of the parser is straightforward: inherit from VerilogConstructor and override callback methods launched while visiting verilog source.

Two passes Parsing

As ordering of verilog modules in single or across multiple files is not preknown and module interfaces need to be created before instances and connectivity are created, parsing can be done in a two pass way with:

  1. Parse modules, ports and parameters. Ignore instances and connectivity. Construct all interfaces.
  2. Reparse. Ignore ports and parameters. Parse instances and nets. Construct connectivity.

Callbacks

Stuctures (Identifier, Net, Port, Expression) details constructed by following callbacks can be found in VerilogType.h header.

An Identifier is a struct that holds the unescaped string. It also includes a boolean flag indicating whether the collected identifier was escaped or not.

Callbacks for Module

Starting a Module

void startModule(const naja::verilog::Identifier& module);

This callback is invoked when a module declaration begins. It receives the identifier of the module as an argument.

For instance, the callback is called with identifier:{name_="foo",escaped_=false} for the Verilog module declaration below:

module foo;

Ending a Module

void endModule();

This function is called upon reaching the end of a module declaration. It serves as a notifier for the end of the module's scope, allowing for any necessary cleanup.

The corresponding verilog end declaration is:

endmodule //foo

Callbacks for Ports

Simple Port Declaration in Module Interface

void moduleInterfaceSimplePort(const naja::verilog::Identifier& port)

Triggered when a module uses a simple declaration (only the port identifier) for ports. It will be called multiple times depending on the number of ports declared.

For example, it is called with identifier.name_="a", identifier.{name_="b@", escaped_=true}, and identifier.name_="c" for:

module foo(a, \b@ , c);

Port details in module implementation

Associated to previous method, following method collects port details: direction and size in module implementation section.

void moduleImplementationPort(const Port& port)

is called for each port listed in the implementation section of a module.

module foo(\a# , b, c);
input a;
output [3:0] b;
inout c;
//will invoke 3 times moduleImplementationPort with:
//Port identifier_={a#,true}, direction=Input, isBus()=true, range_.msb_=3, range_.lsb_=0
//Port identifier_={b,false}, direction=Output, isBus()=true, range_.msb_=0, range_.lsb_=3
//Port identifier_={c,false}, direction=InOut, isBus()=false

Port complete declaration in Module interface

void moduleInterfaceCompletePort(const Port& port)

Invoked for a complete interface port declaration, detailing port direction (input/output/inout) and in case of bus: msb and lsb.

module foo(input[3:0] a, output[0:3] b, inout c);
//will invoke 3 times moduleInterfaceCompletePort with:
//Port identifier_={a,false}, direction=Input, isBus()=true, range_.msb_=3, range_.lsb_=0
//Port identifier_={b,false}, direction=Output, isBus()=true, range_.msb_=0, range_.lsb_=3
//Port identifier_={c,false}, direction=InOut, isBus()=false

Callbacks for Nets

void addNet(const Net& net)

This callback is invoked for each net declaration within a module and will construct Net structure containing net details: msb, lsb (for busses) and type: Wire, Supply0 or Supply1.

wire net0, net1, net2;  // constructs 3 Net(s) named net0, net1, net2, type_=Wire, isBus()=false
wire [31:0] net3;       // construct 1 Net named net3, type_=Wire, isBus()=true, range_.msb_=31, range_.lsb_=0 
wire [-2:1] net4;       // construct 1 Net named net4, type_=Wire, isBus()=true, range_.msb_=-2, range_.lsb_=1 
supply0 constant0;      // construct 1 Net named constant0, type_=Supply0, isBus()=false
supply1 constant1;      // construct 1 Net named constant1, type_=Supply1, isBus()=false

Assign statements

void addAssign(const Identifiers& identifiers, const Expression& expression) 

is called for each assign statement, facilitating the capture of signal assignments.

Below are Verilog examples followed by pseudo C++ representations of the data structures that might be constructed by this callback.

assign n0 = n1;
//identifiers = { {identifier_={n0,false}, range_.valid_=false} }
//expressions = 
// {
//   { 
//     value_.index()=naja::verilog::Expression::Type::IDENTIFIER
//     with auto id=std::get<naja::verilog::Expression::Type::IDENTIFIER>(value_)
//     id.identifier_={"n1",false}, id.range_.valid_=false
//   } 
// }
assign n1 = 1'b0;
//identifiers = { {identifier_={n1,false}, range_.valid_=false} }
//expressions = 
// {
//   { 
//     value_.index()=naja::verilog::Expression::Type::NUMBER
//     with auto nb=std::get<naja::verilog::Expression::Type::NUMBER>(value_)
//     nb.base_=naja::verilog::BasedNumber::BINARY
//     nb.sign_=true, nb.signed_=false, nb.size_=1, nb.digits_="0"
//   } 
// }
assign { n2[3:2], n2[1:0] } = { n0, n1, 2'h2 };
//identifiers =
// {
//    { identifier_={n2,false}, range_.valid_=true, range_.msb_=3, range_.lsb=3 },
//    { identifier_={n2,false}, range_.valid_=true, range_.msb_=1, range_.lsb=0 },
// }
//expressions = 
// {
//   { 
//     value_.index()=naja::verilog::Expression::Type::CONCATENATION
//     with auto concat=std::get<naja::verilog::Expression::Type::CONCATENATION>(value_)
//     concat[0] is an Identifier identifier_={n0,false}, range_.valid_=false
//     concat[1] is an Identifier identifier_={n1,false}, range_.valid_=false
//     concat[2] is an NUMBER with:
//     nb.base_=naja::verilog::BasedNumber::HEX
//     nb.size_=2, nb.digits_="2"
//   } 
// }

Callback for instances

Starting Instantiation

void startInstantiation(const naja::verilog::Identifier& model)

allows to collect module (used as a model) name for one or multiple instanciations. This method will collect model={Model,false} for the two following declarations:

Model ins();
Model ins0(), ins1(), ins2();

Adding an Instance

void addInstance(const naja::verilog::Identifier& instance)

will be called 3 times with instance={ins1,false}, {ins2,false}, {ins3,false} for following declaration:

Model ins(), ins2(), ins3();

Ending Instantiation

void endInstantiation();

is called at the conclusion of an instance declaration, it signals that all instances have been processed and allows for post-processing cleanup.

Callbacks for Instance Connections

Named Port Connection

void addInstanceConnection(const naja::verilog::Identifier& port, const Expression& expression);

This function is called for each named port connection in an instance, capturing the relationship between the port and its connected net or expression.

mod1 inst2(.i0(net4[3:6]), .o0(net5));
//addInstanceConnection is called 2 times with:
//port=Identifier{"i0",false}
//expression_.value_.index() = naja::verilog::Expression::Type::IDENTIFIER,
//with auto id = std::get<naja::verilog::Expression::Type::IDENTIFIER>(expression.value_)
//id.identifier_={"net4",false}, id.isBus=true, id.range_.msb_=3, is.range_.lsb_=6
//and:
//port=Identifier{"o0",false}
//expression_.value_.index() = naja::verilog::Expression::Type::IDENTIFIER,
//with auto id = std::get<naja::verilog::Expression::Type::IDENTIFIER>(expression.value_)
//id.identifier_={"net5",false}, id.isBus=false

Ordered Port Connection

void addOrderedInstanceConnection(size_t portIndex, const Expression& expression);

is invoked for each port connection when ports are connected by order rather than by name.

mod1 inst4(net4[7:10], {net0, net1, net2, net3});
//addOrderedInstanceConnection is called 2 times with:
//portIndex=0
//expression_.value_.index() = naja::verilog::Expression::Type::IDENTIFIER,
//with auto id = std::get<naja::verilog::Expression::Type::IDENTIFIER>(expression.value_)
//id.identifier_={"net4",false}, id.isBus=true, id.range_.msb_=7, is.range_.lsb_=10
//and:
//portIndex=1
//expression_.value_.index() = naja::verilog::Expression::Type::CONCATENATION,
//with auto concat = std::get<naja::verilog::Expression::Type::CONCATENATION>(expression.value_)
//concat.size()=4 and all values are simple Identifiers

Callback for Parameter Assignment

void addParameterAssignment(const naja::verilog::Identifier& parameter, const Expression& expression);

This callback function is designed to handle parameter assignments within module instantiations.

module test();
  mod #(
    .PARAM0("VAL"),
  ) ins();
endmodule
//addParameterAssignment is called one time with:
//parameter={"PARAM0",false} and expression is a RangeIdentifier with name="VAL"