Commenting an Existing Project
This tutorial introduces the NEX Commentator agent, a tool designed to automatically generate high-quality, context-aware comments for RTL code. Using a real-world SystemVerilog project as an example, you'll learn how to run the commentator from the command line, customize its behavior, consume its structured output in scripts and CI pipelines, and integrate it directly into your editor for a smoother developer workflow.
Note
Before you begin, ensure you have correctly installed the NEX CLI application and Git on your machine.
Prerequisites
For this demonstration, we will use the VeriGPU open-source project available at this link.
Navigate to a directory on your local machine where you have read/write permissions. For example, ~/projects/. Then run the following commands:
This directory contains all the source files for the OpenSource GPU.
Adding Comments
Open the gpu_die.sv file. This file contains the top-level module definition of the CPU-to-GPU bridge.
Now, from the terminal, run the following command:
You should see output similar to this:
â
File {self.file} read successfully!
Files in directory: ~/projects/VeriGPU/src
- ~/projects/VeriGPU/src/gpu_card.sv
- ~/projects/VeriGPU/src/mem_16mb.sv
- ~/projects/VeriGPU/src/core.sv
...
Reading file: ~/projects/VeriGPU/src/core.sv
Reading file: ~/projects/VeriGPU/src/gpu_controller.sv
Reading file: ~/projects/VeriGPU/src/global_mem_controller.sv
...
â
Comments generated successfully!
â
Code integrity verified!
đ Commented Source Code:
//==============================================================================
// GPU Die Top-Level Module
//==============================================================================
// This module represents the contents of the GPU die, containing:
// - GPU cores (currently single core implementation)
// - Global memory controller (includes memory with simulated access delays)
// - GPU controller (interfaces with host CPU)
//
// Architecture:
// Host CPU <-> GPU Controller <-> Global Memory Controller <-> Memory
// ^
// |
// GPU Core <---+
//
// Note: For now, the global memory controller contains both the controller
// logic and the actual memory. These may be split into separate modules later.
//==============================================================================
`default_nettype none
module gpu_die (
input clk, // System clock
input rst, // Asynchronous reset, active low
...
What the NEX Commentator Agent Does
- First, it analyzes the
gpu_die.svfile to understand its code structure. - Then, it examines other relevant files connected to the source code, such as imported module definitions or other conceptually related entities, to understand their roles and how the overall system functions.
- Finally, the commentator adds detailed comments to the source file and verifies that the commented version is structurally equivalent to the original code.
If the verification passes, the agent returns the fully commented source code, which is the final output you are interested in.
By reviewing the commented code in gpu_die-default.sv, you will notice that the comments cover not only the content of the current file but also the overall functionality of the gpu_die design. For example, the preamble at lines 9-16 includes an architectural sketch and notes on current implementation limitations:
...
// Architecture:
// Host CPU <-> GPU Controller <-> Global Memory Controller <-> Memory
// ^
// |
// GPU Core <---+
//
// Note: For now, the global memory controller contains both the controller
// logic and the actual memory. These may be split into separate modules later.
...
This comprehensive commenting helps clarify the design and its components beyond the immediate code in the file.
Personalizing Comments
You can customize how NEX generates comments in your source code by creating a special configuration file named NEX.md in the root directory of your source code project. This file serves as a repo-level configuration resource that NEX uses to tailor its behavior according to your preferences, allowing you to specify the style and format of the comments it produces.
For this tutorial, create a new NEX.md file into your project root directory with the following content:
# Comments Directives
The Commentator agent should analyze the RTL code and generate comments based on the following directives:
### 1. Module-Level Intent
- At the top of each module, add a block comment explaining its high-level purpose.
- **Example**: Identify if a module is an ALU, a MUX, a FIFO, or a controller and describe its function.
- Also, list and describe the main interface protocols used (e.g., AXI-Stream, APB, I2C).
### 2. Finite State Machine (FSM) Documentation
- Automatically detect any Finite State Machines within `always` blocks.
- For each state, add an inline comment describing its purpose.
- Add comments explaining the conditions for transitioning between states.
- **Example**:
```verilog
// State: IDLE - Waiting for the 'start' signal.
// State: PROCESSING - Performing data transformation.
// State: DONE - Operation complete, asserting 'done' signal.
```
### 3. Pipeline Stage Identification
- In pipelined architectures, identify the registers that separate stages.
- Add a prominent comment to delineate the logic belonging to each pipeline stage.
- **Example**:
```verilog
//=================================================
//== Pipeline Stage 1: Instruction Fetch (IF)
//=================================================
...
//=================================================
//== Pipeline Stage 2: Decode & Register Read (ID)
//=================================================
```
### 4. Signal and Port Explanations
- For every port in the module's interface, add a comment explaining its role (e.g., data, address, valid, ready).
- For critical internal signals (like state registers, counters, or important data paths), add a comment explaining their purpose.
### 5. Clocking and Reset Strategy
- Identify the clock and reset signals for each sequential block.
- Add a comment specifying if the reset is synchronous or asynchronous.
- If Clock Domain Crossing (CDC) logic is detected, flag it with a comment.
- **Example**: `// CDC: Signal 'data_in' crosses from 'clk_a' to 'clk_b' domain.`
Once the NEX.md file is in place, simply rerun the same command used previously to generate comments:
No additional flags or options are necessary; NEX will automatically detect and apply the directives specified in NEX.md.
At the end of the process, you should obtain something like this:
//=============================================================================
// Module: gpu_die
//=============================================================================
// Description:
// Top-level GPU die integration module that represents the complete GPU
// silicon die contents. This module integrates and interconnects:
// - GPU core(s): Currently instantiates a single RISC-V based GPU core
// - Global memory controller: Manages access to global GPU memory
// - GPU controller: Handles communication with the host CPU and orchestrates
// GPU operations (memory transfers, kernel launches, etc.)
//
// Key Features:
// - Supports CPU-GPU communication via instruction/data interface
// - Provides memory-mapped I/O for data transfers
// - Manages GPU core lifecycle (enable, clear, halt, PC control)
// - Currently single-core design (future: multi-core support)
//
// Interface Protocol:
// - Custom CPU-GPU command interface (non-standard):
// * cpu_recv_instr: 32-bit instruction from CPU
// * cpu_in_data/cpu_out_data: 32-bit bidirectional data path
// * cpu_out_ack: Handshake signal for CPU read operations
//
// Notes:
// - Global memory is currently embedded in global_mem_controller but will
// be separated in future revisions
// - Parameters (addr_width, data_width, memory_size, mem_simulated_delay)
// are defined in external const.sv file
//=============================================================================
`default_nettype none
module gpu_die (
//=========================================================================
// Clock and Reset
//=========================================================================
input clk, // System clock
input rst, // Asynchronous active-low reset
//=========================================================================
// CPU Communication Interface
//=========================================================================
// Command and data interface with mainboard CPU
// Note: Using "in/out" naming convention (relative to this module)
// instead of "rd/wr" to avoid point-of-view ambiguity
input [31:0] cpu_recv_instr, // Instruction word from CPU (e.g., COPY_TO_GPU, KERNEL_LAUNCH)
input [31:0] cpu_in_data, // Data input from CPU (for memory writes, parameters)
output reg [31:0] cpu_out_data, // Data output to CPU (for memory reads)
output reg cpu_out_ack, // Acknowledge signal for CPU read operations (active high)
//=========================================================================
// GPU Status and Debug Outputs
//=========================================================================
output reg halt, // GPU core halt indicator (active high)
output reg outflen, // Float output enable (for debugging)
output reg outen, // Integer output enable (for debugging)
output reg [data_width - 1:0] out // Debug output data (integer or float)
);
//=========================================================================
// Internal Signal Declarations
//=========================================================================
//-------------------------------------------------------------------------
// Core 1 <-> Global Memory Controller Interface
//-------------------------------------------------------------------------
// Memory request and response signals for the GPU core
wire core1_mem_rd_req; // Core read request (active high)
wire core1_mem_wr_req; // Core write request (active high)
wire [addr_width - 1:0] core1_mem_addr; // Memory address for read/write operations
wire [data_width - 1:0] core1_mem_rd_data; // Data read from memory
wire [data_width - 1:0] core1_mem_wr_data; // Data to write to memory
wire core1_mem_busy; // Memory controller busy signal (active high)
wire core1_mem_ack; // Memory operation acknowledge (active high, pulsed)
//-------------------------------------------------------------------------
// GPU Controller <-> Global Memory Controller Interface
//-------------------------------------------------------------------------
// Direct memory access by controller for data transfers and setup
wire contr_mem_wr_en; // Controller write enable (active high)
wire contr_mem_rd_en; // Controller read enable (active high)
wire [addr_width - 1:0] contr_mem_wr_addr; // Controller write address
wire [data_width - 1:0] contr_mem_wr_data; // Controller write data
wire [addr_width - 1:0] contr_mem_rd_addr; // Controller read address
wire [data_width - 1:0] contr_mem_rd_data; // Controller read data
wire contr_mem_rd_ack; // Controller read acknowledge (active high, pulsed)
//-------------------------------------------------------------------------
// GPU Controller <-> Core Control Interface
//-------------------------------------------------------------------------
// Signals for GPU controller to manage core execution
wire contr_core1_ena; // Enable core execution (active high)
wire contr_core1_clr; // Synchronous clear/reset core (active high)
wire contr_core1_set_pc_req; // Request to set program counter (active high)
wire [data_width - 1:0] contr_core1_set_pc_addr; // New program counter address
wire contr_core1_halt; // Core halt status from core (active high)
//-------------------------------------------------------------------------
// Local Core Status Registers
//-------------------------------------------------------------------------
reg core1_halt; // Registered halt status for core 1
//=========================================================================
// Submodule Instantiations
//=========================================================================
//-------------------------------------------------------------------------
// Global Memory Controller
//-------------------------------------------------------------------------
// Manages access to global GPU memory with simulated latency.
// Arbitrates between core memory requests and controller direct access.
// Currently includes embedded memory array (future: separate memory module)
//-------------------------------------------------------------------------
global_mem_controller global_mem_controller_ (
.clk(clk),
.rst(rst),
// Core 1 memory interface
.core1_addr(core1_mem_addr),
.core1_wr_req(core1_mem_wr_req),
.core1_rd_req(core1_mem_rd_req),
.core1_rd_data(core1_mem_rd_data),
.core1_wr_data(core1_mem_wr_data),
.core1_busy(core1_mem_busy),
.core1_ack(core1_mem_ack),
// GPU Controller direct memory access interface
.contr_wr_en (contr_mem_wr_en),
.contr_rd_en (contr_mem_rd_en),
.contr_wr_addr(contr_mem_wr_addr),
.contr_wr_data(contr_mem_wr_data),
.contr_rd_addr(contr_mem_rd_addr),
.contr_rd_data(contr_mem_rd_data),
.contr_rd_ack (contr_mem_rd_ack)
);
//-------------------------------------------------------------------------
// GPU Core (Core 1)
//-------------------------------------------------------------------------
// Single RISC-V based GPU execution core with integer and floating-point
// support. Executes GPU kernels loaded into global memory.
// Supports RV32I base instruction set plus RV32M (multiply/divide) and
// RV32F (single-precision floating-point) extensions.
//-------------------------------------------------------------------------
core core1 (
.rst (rst),
.clk (clk),
.clr (contr_core1_clr), // Synchronous reset from controller
.ena (contr_core1_ena), // Enable signal from controller
.set_pc_req (contr_core1_set_pc_req), // PC set request from controller
.set_pc_addr(contr_core1_set_pc_addr), // New PC address from controller
// Debug outputs
.outflen(outflen), // Float value output enable
.out (out), // Debug output data
.outen (outen), // Integer value output enable
// Halt status
.halt(contr_core1_halt), // Core halt signal to controller
// Memory interface
.mem_addr (core1_mem_addr), // Memory address
.mem_rd_data(core1_mem_rd_data), // Data from memory
.mem_wr_data(core1_mem_wr_data), // Data to memory
.mem_ack (core1_mem_ack), // Memory operation complete
.mem_busy (core1_mem_busy), // Memory controller busy
.mem_rd_req (core1_mem_rd_req), // Read request
.mem_wr_req (core1_mem_wr_req) // Write request
);
//-------------------------------------------------------------------------
// GPU Controller
//-------------------------------------------------------------------------
// Handles CPU-GPU communication and orchestrates GPU operations including:
// - Memory allocation and data transfers (CPU <-> GPU)
// - Kernel loading and launch
// - Core lifecycle management (reset, enable, PC control)
// Implements a simple state machine to process variable-length CPU commands
//-------------------------------------------------------------------------
gpu_controller gpu_controller_ (
.rst(rst),
.clk(clk),
// CPU interface
.cpu_recv_instr(cpu_recv_instr), // Instruction from CPU
.cpu_in_data (cpu_in_data), // Data from CPU
.cpu_out_data (cpu_out_data), // Data to CPU
.cpu_out_ack (cpu_out_ack), // Data valid signal to CPU
// Global memory access interface
.mem_wr_en (contr_mem_wr_en), // Memory write enable
.mem_rd_en (contr_mem_rd_en), // Memory read enable
.mem_wr_addr(contr_mem_wr_addr), // Write address
.mem_wr_data(contr_mem_wr_data), // Write data
.mem_rd_addr(contr_mem_rd_addr), // Read address
.mem_rd_data(contr_mem_rd_data), // Read data
.mem_rd_ack (contr_mem_rd_ack), // Read acknowledge
// Core control interface
.core_ena (contr_core1_ena), // Core enable
.core_clr (contr_core1_clr), // Core synchronous clear
.core_halt (contr_core1_halt), // Core halt status
.core_set_pc_req (contr_core1_set_pc_req), // PC set request
.core_set_pc_addr(contr_core1_set_pc_addr) // New PC address
);
//=========================================================================
// Clocking and Reset Strategy
//=========================================================================
// - Clock Domain: Single clock domain (clk) for entire GPU die
// - Reset: Asynchronous active-low reset (rst)
// * global_mem_controller: Uses async reset for memory state
// * core: Uses async reset + synchronous clear (clr) for execution state
// * gpu_controller: Uses async reset for FSM state
// - No Clock Domain Crossing (CDC) present in this module
//=========================================================================
endmodule
This demonstrates how the configuration file influences the style and detail of the generated comments.
Structured Output and Pipelining
Integrating NEX into your own scripts or pipeline is simple and straightforward. Resorting to the original gpu_die.sv file, we can call NEX with --json-output to force the response to be a structured JSON object:
This will produce something like:
{
"status": 200,
"language": "verilog",
"commented_code": "//--------------------------------------------------------------------------------------------------\n// Module: gpu_die\n//\n// Description:\n// This module represents the top-level entity of the GPU die. It integrates the primary\n// components of the GPU, including one or more GPU cores, a global memory controller, and a\n// master GPU controller. It manages the interactions between these components and provides an\n// interface for communication with an external main CPU.\n//\n// Submodules:\n// - global_mem_controller: Manages access to the GPU's global memory.\n//
...
}
Example: Parse Output JSON and Feed a Downstream Task
What follows is a tiny Bash pipeline that:
1. Runs the commentator agent with --json-output;
2. Parses the JSON response to extract the commented_code field;
3. Writes it to a new file;
4. Runs a downstream step (here: verilator --lint-only) to demonstrate how you can plug NEX into your existing CI/tooling.
Tip
Required dependencies: jq (for JSON parsing) and optionally verilator (for linting).
Create a file called comment_and_lint.sh:
#!/usr/bin/env bash
set -euo pipefail
INPUT_FILE="${1:-gpu_die.sv}"
OUT_FILE="${2:-gpu_die-commented.sv}"
echo "Running NEX on input file: $INPUT_FILE"
RAW_OUT=$(nex --agent=commentator --input-file="$INPUT_FILE" --json-output)
# Extract the JSON object by finding the first '{' and the last '}'.
JSON_PAYLOAD=$(printf '%s\n' "$RAW_OUT" | awk '
{
lines[NR] = $0
}
END {
# Combine lines into one string
for (i = 1; i <= NR; i++) {
all = (i == 1 ? lines[i] : all "\n" lines[i])
}
start = index(all, "{")
if (start == 0) exit 1
# Find the last "}"
for (i = length(all); i >= start; i--) {
if (substr(all, i, 1) == "}") {
print substr(all, start, i - start + 1)
exit 0
}
}
exit 1
}')
echo "Raw output (may include logs):"
printf '%s\n' "$RAW_OUT"
echo "Validating NEX output..."
if ! echo "$JSON_PAYLOAD" | jq -e . >/dev/null 2>&1; then
echo "Failed to parse JSON. Raw output was:" >&2
printf '%s\n' "$RAW_OUT" >&2
exit 1
fi
# The structure is {"agent_response": {"status": ..., "language": ..., "commented_code": ...}}
STATUS=$(printf '%s' "$JSON_PAYLOAD" | jq -r '.agent_response.status // empty')
if [[ "$STATUS" != "200" ]]; then
echo "NEX did not return status=200. Full output:" >&2
printf '%s\n' "$RAW_OUT" >&2
exit 1
fi
LANG=$(printf '%s' "$JSON_PAYLOAD" | jq -r '.agent_response.language // "unknown"')
CODE=$(printf '%s' "$JSON_PAYLOAD" | jq -r '.agent_response.commented_code')
echo "NEX output validated. Detected language: $LANG"
echo "Writing commented code to: $OUT_FILE"
printf '%s\n' "$CODE" > "$OUT_FILE"
echo "â
Wrote commented code to: $OUT_FILE (language=$LANG)"
echo "Running linter on commented code..."
if command -v verilator >/dev/null 2>&1; then
echo "đ Running: verilator --lint-only $OUT_FILE"
verilator --lint-only "$OUT_FILE"
echo "â
Verilator lint passed"
else
echo "âšī¸ verilator not found; skipping lint step"
fi
# Bonus: print a quick summary
LINES=$(wc -l < "$OUT_FILE" | tr -d ' ')
BYTES=$(wc -c < "$OUT_FILE" | tr -d ' ')
echo "đ Summary: $LINES lines, $BYTES bytes"
Make it executable and run it:
This pattern (structured JSON â extract fields â downstream steps) is a simple way to integrate NEX into build scripts, CI pipelines, or custom developer tooling.
Tip
Writing a custom parser can be useful for educational purposes, but the Commentator Agent can also write comments directly back to the input file using the --inplace flag:
Takeaways
In this tutorial, we learned how to use the NEX Commentator Agent to generate context-aware comments for an existing SystemVerilog project. We also saw how to customize comment style with a repo-level NEX.md file, consume structured JSON output in scripts and CI pipelines, and write generated comments directly back to the source file with --inplace.
The Commentator is especially useful when documenting unfamiliar RTL, standardizing comment style across a project, or adding automated documentation steps to an existing development workflow.