rtl-aid 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
rtl_aid/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .core import VerilogWikiParser
2
+
3
+ __version__ = "0.1.0"
4
+ __all__ = ["VerilogWikiParser"]
rtl_aid/cli.py ADDED
@@ -0,0 +1,94 @@
1
+ import argparse
2
+ from .core import VerilogWikiParser
3
+
4
+ def main():
5
+ parser = argparse.ArgumentParser()
6
+
7
+ group = parser.add_mutually_exclusive_group(required=True)
8
+
9
+ group.add_argument(
10
+ "-d", "--dir",
11
+ nargs="+",
12
+ metavar="DIR",
13
+ help="One or more directories to recursively scan for Verilog (.v) files"
14
+ )
15
+
16
+ group.add_argument(
17
+ "-f", "--file",
18
+ nargs="+",
19
+ metavar="FILE",
20
+ help="One or more specific Verilog files to parse (no directory traversal)"
21
+ )
22
+
23
+ parser.add_argument(
24
+ "-o", "--out",
25
+ default="temp/docs/modules",
26
+ metavar="OUT_DIR",
27
+ help="Output directory for generated markdown docs (default: temp/docs/modules)"
28
+ )
29
+
30
+ parser.add_argument(
31
+ "-v",
32
+ action="count",
33
+ default=0,
34
+ help="Verbose mode: -v shows modified files, -vv shows detailed section diffs"
35
+ )
36
+
37
+ parser.add_argument(
38
+ "--ci",
39
+ action="store_true",
40
+ help="Enable CI mode: fail (exit 1) on issues like missing docs, no IO, or invalid structures"
41
+ )
42
+
43
+ parser.add_argument(
44
+ "--print-errors",
45
+ action="store_true",
46
+ help="Print errors to stdout in addition to the error log file"
47
+ )
48
+
49
+ parser.add_argument(
50
+ "--json-graph",
51
+ action="store_true",
52
+ help="Generate dependency graph as JSON (graph.json) in output directory"
53
+ )
54
+
55
+ parser.add_argument(
56
+ "--exclude",
57
+ nargs="+",
58
+ metavar="EXCLUDE",
59
+ help="Directories or files to exclude from scanning"
60
+ )
61
+
62
+ parser.add_argument(
63
+ "--dry-run",
64
+ action="store_true",
65
+ help="Show what would be written without touching any files"
66
+ )
67
+
68
+ args = parser.parse_args()
69
+
70
+ paths = args.dir if args.dir else args.file
71
+
72
+ v = VerilogWikiParser(
73
+ paths,
74
+ verbose=args.v,
75
+ ci=args.ci,
76
+ json_graph=args.json_graph,
77
+ print_errors=args.print_errors,
78
+ exclude=args.exclude,
79
+ dry_run=args.dry_run,
80
+ )
81
+ v.scan()
82
+ v.generate_markdown(args.out)
83
+ v.write_json(args.out)
84
+ v.write_log()
85
+ v.run_ci_checks()
86
+
87
+ total = len(v.modules)
88
+ written = len(v.modified_files)
89
+ unchanged = total - written
90
+ action = "would be written" if args.dry_run else "written"
91
+ print(f"\n{total} module(s) processed — {written} doc(s) {action}, {unchanged} unchanged")
92
+
93
+ if __name__ == "__main__":
94
+ main()
rtl_aid/core.py ADDED
@@ -0,0 +1,364 @@
1
+ import re
2
+ import os
3
+ import tempfile
4
+ import json
5
+ import sys
6
+
7
+ class VerilogWikiParser(object):
8
+ def __init__(self, paths, verbose=0, ci=False, json_graph=False, print_errors=False, exclude=None, dry_run=False):
9
+ self.paths = paths
10
+ self.modules = {}
11
+ self.called_by = {}
12
+ self.modified_files = []
13
+ self.verbose = verbose
14
+ self.ci = ci
15
+ self.json_graph = json_graph
16
+ self.print_errors = print_errors
17
+ self.exclude = exclude or []
18
+ self.issues = []
19
+ self.dry_run = dry_run
20
+
21
+ # -------------------------
22
+ # CLEAN COMMENTS
23
+ # -------------------------
24
+ def clean(self, text):
25
+ text = re.sub(r"//[^\n]*", "\n", text)
26
+ text = re.sub(r"/\*.*?\*/", "", text, flags=re.S)
27
+ return text
28
+
29
+ # -------------------------
30
+ # PARSING
31
+ # -------------------------
32
+ def extract_module_and_ports(self, text):
33
+ pattern = r"module\s+(\w+)\s*(#\s*\((.*?)\))?\s*\((.*?)\)\s*;"
34
+ m = re.search(pattern, text, flags=re.S)
35
+ if not m:
36
+ return None
37
+
38
+ mod_name = m.group(1)
39
+ param_block = m.group(3) or ""
40
+ port_block = m.group(4)
41
+
42
+ inputs, outputs, inouts = [], [], []
43
+ parameters = []
44
+
45
+ for p in re.split(r",\s*\n|,\s*", param_block):
46
+ p = p.strip()
47
+ if p.startswith("parameter"):
48
+ param_str = re.sub(r"\bparameter\b", "", p).strip()
49
+ param_str = re.sub(r"^(integer|logic|reg|wire|bit|byte|shortint|int|longint|signed|unsigned)\s+", "", param_str)
50
+ parameters.append(param_str.strip())
51
+
52
+ ports = re.split(r",\s*\n|,\s*", port_block)
53
+
54
+ current_direction = None
55
+ for p in ports:
56
+ p = p.strip()
57
+ if not p:
58
+ continue
59
+
60
+ p = re.sub(r"`\w+\s+", "", p)
61
+ tokens = p.split()
62
+ if not tokens:
63
+ continue
64
+
65
+ name = tokens[-1]
66
+
67
+ if "input" in tokens:
68
+ current_direction = "input"
69
+ elif "output" in tokens:
70
+ current_direction = "output"
71
+ elif "inout" in tokens:
72
+ current_direction = "inout"
73
+
74
+ if current_direction == "input":
75
+ inputs.append(name)
76
+ elif current_direction == "output":
77
+ outputs.append(name)
78
+ elif current_direction == "inout":
79
+ inouts.append(name)
80
+
81
+ return {
82
+ "name": mod_name,
83
+ "inputs": inputs,
84
+ "outputs": outputs,
85
+ "inouts": inouts,
86
+ "parameters": parameters
87
+ }
88
+
89
+ def remove_module_header(self, text):
90
+ return re.sub(r"\bmodule\b[^;]*;", "", text)
91
+
92
+ def extract_calls(self, text, known_modules, current_module):
93
+ calls = set()
94
+ text = self.remove_module_header(text)
95
+
96
+ pattern = r"\b(\w+)\s*(?:#\s*\(.*?\))?\s+\w+\s*\("
97
+ for m in re.finditer(pattern, text, flags=re.S):
98
+ mod_name = m.group(1)
99
+ if mod_name in known_modules:
100
+ calls.add(mod_name)
101
+
102
+ return sorted(list(calls))
103
+
104
+ _TB_SUFFIXES = ("_tb.v", "_tb.sv", "_bench.v", "_bench.sv", "_testbench.v", "_testbench.sv")
105
+ _RTL_EXTENSIONS = (".v", ".sv")
106
+
107
+ def _is_rtl_file(self, filename):
108
+ return (
109
+ any(filename.endswith(ext) for ext in self._RTL_EXTENSIONS)
110
+ and not any(filename.endswith(s) for s in self._TB_SUFFIXES)
111
+ )
112
+
113
+ # -------------------------
114
+ # SCAN
115
+ # -------------------------
116
+ def scan(self):
117
+ file_texts = {}
118
+ all_files = []
119
+
120
+ def is_excluded(path):
121
+ for ex in self.exclude:
122
+ if ex in path:
123
+ return True
124
+ return False
125
+
126
+ for p in self.paths:
127
+ if os.path.isfile(p):
128
+ if not is_excluded(p):
129
+ all_files.append(p)
130
+ else:
131
+ for root, dirs, files in os.walk(p):
132
+ dirs[:] = [d for d in dirs if not is_excluded(os.path.join(root, d))]
133
+ if is_excluded(root):
134
+ continue
135
+ for f in files:
136
+ if self._is_rtl_file(f):
137
+ full_path = os.path.join(root, f)
138
+ if not is_excluded(full_path):
139
+ all_files.append(full_path)
140
+
141
+ for path in all_files:
142
+ with open(path, "r") as fh:
143
+ text = self.clean(fh.read())
144
+
145
+ file_texts[path] = text
146
+
147
+ mod = self.extract_module_and_ports(text)
148
+ if not mod:
149
+ continue
150
+
151
+ self.modules[mod["name"]] = {
152
+ "file": path,
153
+ "calls": [],
154
+ "inputs": mod["inputs"],
155
+ "outputs": mod["outputs"],
156
+ "inouts": mod["inouts"],
157
+ "parameters": mod["parameters"]
158
+ }
159
+
160
+ known_modules = set(self.modules.keys())
161
+
162
+ for path, text in file_texts.items():
163
+ mod = self.extract_module_and_ports(text)
164
+ if not mod:
165
+ continue
166
+
167
+ mod_name = mod["name"]
168
+ self.modules[mod_name]["calls"] = self.extract_calls(
169
+ text, known_modules, mod_name
170
+ )
171
+
172
+ self.build_called_by()
173
+
174
+ def build_called_by(self):
175
+ for mod in self.modules:
176
+ self.called_by[mod] = []
177
+
178
+ for caller, data in self.modules.items():
179
+ for callee in data["calls"]:
180
+ if callee in self.called_by:
181
+ self.called_by[callee].append(caller)
182
+
183
+ # -------------------------
184
+ # EXISTING MD PARSE
185
+ # -------------------------
186
+ def parse_existing_sections(self, path):
187
+ if not os.path.exists(path):
188
+ return {}
189
+
190
+ with open(path) as f:
191
+ content = f.read()
192
+
193
+ sections = {}
194
+ for sec in ["Description", "Parameters", "Inputs", "Outputs", "Inouts", "Calls", "Called By"]:
195
+ m = re.search(rf"## {sec}\n(.*?)(\n##|\Z)", content, re.S)
196
+ if m:
197
+ sections[sec] = m.group(1).strip()
198
+
199
+ return sections
200
+
201
+ # -------------------------
202
+ # DIFF
203
+ # -------------------------
204
+ def diff_lists(self, old, new):
205
+ old_set = set(old)
206
+ new_set = set(new)
207
+ return len(new_set - old_set), len(old_set - new_set)
208
+
209
+ # -------------------------
210
+ # MARKDOWN
211
+ # -------------------------
212
+ def generate_markdown(self, out_dir):
213
+ if not self.dry_run:
214
+ os.makedirs(out_dir, exist_ok=True)
215
+
216
+ managed_sections = ["Parameters", "Inputs", "Outputs", "Inouts", "Calls", "Called By"]
217
+
218
+ for mod, data in self.modules.items():
219
+ fname = os.path.join(out_dir, f"{mod}.md")
220
+
221
+ old_content = ""
222
+ if os.path.exists(fname):
223
+ with open(fname) as f:
224
+ old_content = f.read()
225
+
226
+ old_sections = self.parse_existing_sections(fname)
227
+ desc = old_sections.get("Description", "TODO: Add description")
228
+
229
+ if desc.startswith("TODO"):
230
+ self.issues.append(f"{mod}: missing description")
231
+
232
+ def format_list(lst):
233
+ return "\n".join([f"- {x}" for x in lst]) if lst else "- None"
234
+
235
+ new_sections = {
236
+ "Parameters": format_list(data["parameters"]),
237
+ "Inputs": format_list(data["inputs"]),
238
+ "Outputs": format_list(data["outputs"]),
239
+ "Inouts": format_list(data["inouts"]),
240
+ "Calls": format_list([f"[{c}]({c}.md)" for c in data["calls"]]),
241
+ "Called By": format_list([f"[{c}]({c}.md)" for c in self.called_by.get(mod, [])]),
242
+ }
243
+
244
+ # DIFF LOGIC
245
+ diffs = {}
246
+ for key in ["Parameters", "Inputs", "Outputs", "Inouts", "Calls"]:
247
+ old_list = re.findall(r"- (.+)", old_sections.get(key, ""))
248
+ new_list = re.findall(r"- (.+)", new_sections[key])
249
+ add, rem = self.diff_lists(old_list, new_list)
250
+ diffs[key] = (add, rem)
251
+
252
+ content = ""
253
+ if not old_content:
254
+ content = f"# {mod}\n\n## Description\n{desc}\n\n"
255
+ for k, v in new_sections.items():
256
+ content += f"## {k}\n{v}\n\n"
257
+ else:
258
+ content = old_content
259
+ for sec in managed_sections:
260
+ new_sec_text = f"## {sec}\n{new_sections[sec]}\n"
261
+ pattern = rf"## {sec}\n.*?(?=\n##|\Z)"
262
+ if re.search(pattern, content, flags=re.S):
263
+ content = re.sub(pattern, new_sec_text, content, flags=re.S)
264
+ else:
265
+ content += f"\n{new_sec_text}\n"
266
+
267
+ content = content.strip() + "\n"
268
+
269
+ if content != old_content:
270
+ if not self.dry_run:
271
+ with open(fname, "w") as f:
272
+ f.write(content)
273
+
274
+ diff_str = f"{mod}: " + ", ".join([f"{k} +{a}/-{r}" for k, (a, r) in diffs.items()])
275
+ self.modified_files.append((fname, diff_str))
276
+
277
+ # -------------------------
278
+ # LOGGING
279
+ # -------------------------
280
+ def write_log(self):
281
+ if self.modified_files:
282
+ if self.dry_run:
283
+ print("\n[DRY RUN] Would write:")
284
+ for fname, _ in self.modified_files:
285
+ print(f" {fname}")
286
+ if self.verbose >= 2:
287
+ print("\nDetailed section diffs:")
288
+ for _, diff_str in self.modified_files:
289
+ print(f" {diff_str}")
290
+ else:
291
+ tmp = tempfile.NamedTemporaryFile(delete=False, mode="w", suffix=".log")
292
+ tmp.write("Modified files:\n")
293
+ for fname, _ in self.modified_files:
294
+ tmp.write(fname + "\n")
295
+ tmp.write("\nDetailed section diffs:\n")
296
+ for _, diff_str in self.modified_files:
297
+ tmp.write(diff_str + "\n")
298
+ tmp.close()
299
+
300
+ if self.verbose >= 1:
301
+ print("\nModified files:")
302
+ for fname, _ in self.modified_files:
303
+ print(fname)
304
+
305
+ if self.verbose >= 2:
306
+ print("\nDetailed section diffs:")
307
+ for _, diff_str in self.modified_files:
308
+ print(diff_str)
309
+
310
+ print(f"\nLog file: {tmp.name}")
311
+
312
+ if not self.ci:
313
+ missing_docs = [mod for mod in self.modules if any(f"{mod}: missing description" in i for i in self.issues)]
314
+ if missing_docs:
315
+ print("\nMissing Docs Summary Report:")
316
+ for m in missing_docs:
317
+ print(f" - {m}")
318
+
319
+ # -------------------------
320
+ # JSON GRAPH
321
+ # -------------------------
322
+ def write_json(self, out_dir):
323
+ if not self.json_graph:
324
+ return
325
+
326
+ graph = {}
327
+ for m, d in self.modules.items():
328
+ graph[m] = {
329
+ "calls": d["calls"],
330
+ "called_by": self.called_by.get(m, [])
331
+ }
332
+
333
+ path = os.path.join(out_dir, "graph.json")
334
+ with open(path, "w") as f:
335
+ json.dump(graph, f, indent=2)
336
+
337
+ # -------------------------
338
+ # CI VALIDATION
339
+ # -------------------------
340
+ def run_ci_checks(self):
341
+ if not self.ci:
342
+ return
343
+
344
+ for mod, data in self.modules.items():
345
+ if not data["inputs"] and not data["outputs"]:
346
+ self.issues.append(f"{mod}: no IO")
347
+
348
+ if mod in data["calls"]:
349
+ self.issues.append(f"{mod}: self-instantiation")
350
+
351
+ if self.issues:
352
+ if not self.dry_run:
353
+ tmp = tempfile.NamedTemporaryFile(delete=False, mode="w", suffix="_errors.log")
354
+ tmp.write("CI FAIL:\n")
355
+ for i in self.issues:
356
+ tmp.write(i + "\n")
357
+ tmp.close()
358
+ print(f"\nError log file: {tmp.name}")
359
+
360
+ if self.print_errors or self.dry_run:
361
+ print("\nCI FAIL:")
362
+ for i in self.issues:
363
+ print(i)
364
+ sys.exit(1)
rtl_aid/lint.py ADDED
@@ -0,0 +1,184 @@
1
+ import re
2
+ import subprocess
3
+ import sys
4
+ import os
5
+ import argparse
6
+ import shutil
7
+
8
+
9
+ def _get_verilator_version():
10
+ try:
11
+ out = subprocess.check_output(["verilator", "--version"], text=True)
12
+ return out.strip().splitlines()[0]
13
+ except Exception:
14
+ return None
15
+
16
+
17
+ def _check_verilator():
18
+ if not shutil.which("verilator"):
19
+ print(
20
+ "Error: verilator is not installed or not in PATH.\n"
21
+ "\n"
22
+ "Install it:\n"
23
+ " Debian / Ubuntu: sudo apt install verilator\n"
24
+ " macOS: brew install verilator\n"
25
+ " From source: https://verilator.org/guide/latest/install.html\n"
26
+ "\n"
27
+ "Note: verilator is a system tool and cannot be installed via pip.",
28
+ file=sys.stderr,
29
+ )
30
+ sys.exit(1)
31
+
32
+
33
+ def _run_lint(filepath, include_dirs):
34
+ cmd = ["verilator", "--lint-only", "-Wall"]
35
+ for d in include_dirs:
36
+ cmd.extend(["-I", d])
37
+ cmd.append(filepath)
38
+ result = subprocess.run(cmd, capture_output=True, text=True)
39
+ return result.stdout + result.stderr, cmd
40
+
41
+
42
+ def parse_lint_output(output, filepath):
43
+ """Return {line_num: message} for warnings/errors in filepath only.
44
+
45
+ Only the first issue per line is kept — multiple warnings on the same
46
+ line would produce unreadable inline comments. Verilator continuation
47
+ lines (context arrows, notes) are skipped; only lines starting with
48
+ '%Warning' or '%Error' are matched.
49
+ """
50
+ issues = {}
51
+ target_base = os.path.basename(filepath)
52
+ target_abs = os.path.abspath(filepath)
53
+
54
+ # Format: %Warning-TYPE: path:line:col: message
55
+ # %Error: path:line:col: message
56
+ pattern = re.compile(r"^%(Warning[^:]*|Error[^:]*): (.+?):(\d+):\d+: (.+)$")
57
+ for line in output.splitlines():
58
+ m = pattern.match(line)
59
+ if not m:
60
+ continue
61
+ warn_file = m.group(2)
62
+ line_num = int(m.group(3))
63
+ message = m.group(4).strip()
64
+
65
+ if (os.path.basename(warn_file) == target_base
66
+ or os.path.abspath(warn_file) == target_abs):
67
+ if line_num not in issues:
68
+ issues[line_num] = message
69
+
70
+ return issues
71
+
72
+
73
+ def tag_file(filepath, issues, lint_cmd):
74
+ """Tag warned lines inline and add lint/tb-test header lines.
75
+
76
+ Idempotent: re-running replaces existing /* Check: */ tags and
77
+ skips header lines that are already present.
78
+ """
79
+ with open(filepath, "r") as f:
80
+ lines = f.readlines()
81
+
82
+ # Tag each warned line with a trailing /* Check: ... */ comment.
83
+ # Verilator line numbers are 1-based; Python list is 0-based.
84
+ # Guard against line numbers that fall outside the file — this can
85
+ # happen when a warning originates inside a macro expansion.
86
+ for line_num, message in sorted(issues.items()):
87
+ idx = line_num - 1
88
+ if idx < 0 or idx >= len(lines):
89
+ continue
90
+ line = lines[idx].rstrip("\n")
91
+ line = re.sub(r"\s*/\* Check:.*?\*/", "", line).rstrip()
92
+ lines[idx] = f"{line} /* Check: {message} */\n"
93
+
94
+ # Insert lint/tb-test headers after the leading comment block (copyright
95
+ # headers, file-level comments, blank lines at the top).
96
+ insert_idx = 0
97
+ for i, line in enumerate(lines):
98
+ s = line.strip()
99
+ if s.startswith("//") or s.startswith("/*") or s.startswith("*") or s == "":
100
+ insert_idx = i + 1
101
+ else:
102
+ break
103
+
104
+ lint_cmd_str = " ".join(lint_cmd)
105
+ full_content = "".join(lines)
106
+ new_headers = []
107
+ if "// lint-test:" not in full_content:
108
+ new_headers.append(f"// lint-test: {lint_cmd_str}\n")
109
+ if "// tb-test:" not in full_content:
110
+ new_headers.append("// tb-test: tba\n")
111
+
112
+ if new_headers:
113
+ lines = lines[:insert_idx] + new_headers + lines[insert_idx:]
114
+
115
+ with open(filepath, "w") as f:
116
+ f.writelines(lines)
117
+
118
+
119
+ def main():
120
+ parser = argparse.ArgumentParser(
121
+ prog="rtllint",
122
+ description="Run verilator lint on Verilog files and tag warnings inline"
123
+ )
124
+ parser.add_argument(
125
+ "file",
126
+ nargs="+",
127
+ metavar="FILE",
128
+ help="Verilog/SystemVerilog file(s) to lint"
129
+ )
130
+ parser.add_argument(
131
+ "-I", "--include",
132
+ dest="include_dirs",
133
+ action="append",
134
+ metavar="DIR",
135
+ default=[],
136
+ help="Add include directory (passed to verilator as -I, repeatable)"
137
+ )
138
+ parser.add_argument(
139
+ "--dry-run",
140
+ action="store_true",
141
+ help="Show issues without modifying any files"
142
+ )
143
+ parser.add_argument(
144
+ "-v",
145
+ action="store_true",
146
+ help="Print full verilator output"
147
+ )
148
+ args = parser.parse_args()
149
+
150
+ _check_verilator()
151
+
152
+ if args.v:
153
+ print(f"Using {_get_verilator_version()}")
154
+
155
+ any_issues = False
156
+ for filepath in args.file:
157
+ if not os.path.isfile(filepath):
158
+ print(f"Error: {filepath}: file not found", file=sys.stderr)
159
+ continue
160
+
161
+ output, cmd = _run_lint(filepath, args.include_dirs)
162
+ issues = parse_lint_output(output, filepath)
163
+
164
+ if args.v and output.strip():
165
+ print(output.strip())
166
+
167
+ if not issues:
168
+ print(f"{filepath}: clean")
169
+ continue
170
+
171
+ any_issues = True
172
+ if args.dry_run:
173
+ print(f"\n{filepath}: {len(issues)} issue(s) would be tagged:")
174
+ for ln, msg in sorted(issues.items()):
175
+ print(f" Line {ln}: {msg}")
176
+ else:
177
+ tag_file(filepath, issues, cmd)
178
+ print(f"{filepath}: tagged {len(issues)} issue(s)")
179
+
180
+ sys.exit(1 if any_issues else 0)
181
+
182
+
183
+ if __name__ == "__main__":
184
+ main()
@@ -0,0 +1,263 @@
1
+ Metadata-Version: 2.4
2
+ Name: rtl-aid
3
+ Version: 0.1.0
4
+ Summary: CI-native documentation layer for RTL projects
5
+ Author: vishwaksen-1
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/vishwaksen-1/rtl-aid
8
+ Project-URL: Issues, https://github.com/vishwaksen-1/rtl-aid/issues
9
+ Keywords: verilog,systemverilog,rtl,fpga,asic,eda,documentation,linting,ci,hardware,verilator,dependency-graph,code-analysis,agentic
10
+ Requires-Python: >=3.7
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Dynamic: license-file
14
+
15
+ # rtl-aid
16
+
17
+ CI-native documentation layer for RTL projects.
18
+
19
+ Parses Verilog/SystemVerilog source files, extracts module structure, and generates Markdown documentation that stays in sync with your RTL — automatically.
20
+
21
+ ---
22
+
23
+ ## Tools
24
+
25
+ | Command | Purpose |
26
+ |---------|---------|
27
+ | `rtldoc` | Generate and maintain module documentation |
28
+ | `rtllint` | Run verilator lint and tag warnings inline in source |
29
+
30
+ ---
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ pip install rtl-aid
36
+ ```
37
+
38
+ Or from source:
39
+
40
+ ```bash
41
+ git clone https://github.com/vishwaksen-1/rtl-aid
42
+ cd rtl-aid
43
+ pip install -e .
44
+ ```
45
+
46
+ **Requirements**
47
+
48
+ - Python 3.7+
49
+ - `rtllint` requires [verilator](https://verilator.org) — a system binary, not installable via pip:
50
+
51
+ ```bash
52
+ # Debian / Ubuntu
53
+ sudo apt install verilator
54
+
55
+ # macOS
56
+ brew install verilator
57
+ ```
58
+
59
+ `rtldoc` has no external dependencies. If verilator is missing, `rtllint` will exit with a clear error and install instructions.
60
+
61
+ ---
62
+
63
+ ## rtldoc
64
+
65
+ ### Quick start
66
+
67
+ ```bash
68
+ # Document all modules in a directory
69
+ rtldoc -d rtl/
70
+
71
+ # Document specific files
72
+ rtldoc -f rtl/core/alu.v rtl/core/decoder.v
73
+
74
+ # Custom output directory
75
+ rtldoc -d rtl/ -o docs/modules/
76
+ ```
77
+
78
+ ### What gets generated
79
+
80
+ For each module, a Markdown file is created or updated:
81
+
82
+ ```markdown
83
+ # alu
84
+
85
+ ## Description
86
+ TODO: Add description
87
+
88
+ ## Parameters
89
+ - DATA_WIDTH = 8
90
+ - OP_WIDTH = 4
91
+
92
+ ## Inputs
93
+ - clk
94
+ - rst
95
+ - op
96
+ - operand_a
97
+ - operand_b
98
+
99
+ ## Outputs
100
+ - result
101
+ - overflow
102
+
103
+ ## Calls
104
+ - [mux4](mux4.md)
105
+
106
+ ## Called By
107
+ - [cpu_core](cpu_core.md)
108
+ ```
109
+
110
+ The `Description` section is **never overwritten** — you write it once, rtldoc preserves it on every run. All other sections are auto-managed.
111
+
112
+ ### CLI reference
113
+
114
+ ```
115
+ rtldoc (-d DIR [DIR...] | -f FILE [FILE...]) [options]
116
+
117
+ Input:
118
+ -d, --dir DIR [DIR...] Recursively scan directories for .v / .sv files
119
+ -f, --file FILE [FILE...] Parse specific files (no directory traversal)
120
+
121
+ Output:
122
+ -o, --out OUT_DIR Output directory (default: temp/docs/modules)
123
+ --json-graph Write dependency graph to graph.json
124
+
125
+ Filtering:
126
+ --exclude PATTERN [...] Exclude paths containing any of these strings
127
+
128
+ Run modes:
129
+ --dry-run Show what would be written without touching files
130
+ --ci Fail (exit 1) on missing descriptions, no-IO modules,
131
+ or self-instantiations
132
+ --print-errors Print CI issues to stdout (in addition to the error log)
133
+
134
+ Verbosity:
135
+ -v Print modified file paths
136
+ -vv Print modified files + section-level diffs (+adds/-removals)
137
+ ```
138
+
139
+ ### CI integration
140
+
141
+ ```yaml
142
+ # .github/workflows/docs.yml
143
+ - name: Check RTL docs
144
+ run: rtldoc -d rtl/ -o docs/modules/ --ci --print-errors
145
+ ```
146
+
147
+ Exit codes: `0` = clean, `1` = CI check failed.
148
+
149
+ Testbench files (`_tb.v`, `_tb.sv`, `_bench.v`, `_testbench.v`, and `.sv` variants) are automatically excluded from scanning.
150
+
151
+ ### Dry run
152
+
153
+ Preview what would change before committing:
154
+
155
+ ```bash
156
+ rtldoc -d rtl/ --dry-run
157
+ ```
158
+
159
+ ```
160
+ [DRY RUN] Would write:
161
+ docs/modules/alu.md
162
+ docs/modules/decoder.md
163
+
164
+ 5 module(s) processed — 2 doc(s) would be written, 3 unchanged
165
+ ```
166
+
167
+ ### Dependency graph
168
+
169
+ ```bash
170
+ rtldoc -d rtl/ --json-graph -o docs/modules/
171
+ ```
172
+
173
+ Writes `docs/modules/graph.json`:
174
+
175
+ ```json
176
+ {
177
+ "cpu_core": {
178
+ "calls": ["alu", "decoder", "register_file"],
179
+ "called_by": []
180
+ },
181
+ "alu": {
182
+ "calls": ["mux4"],
183
+ "called_by": ["cpu_core"]
184
+ }
185
+ }
186
+ ```
187
+
188
+ ---
189
+
190
+ ## rtllint
191
+
192
+ Runs `verilator --lint-only -Wall` on a file and tags each warned line with an inline comment. Useful for tracking lint debt without blocking a build.
193
+
194
+ ### Usage
195
+
196
+ ```bash
197
+ rtllint rtl/core/alu.v
198
+
199
+ # With include directories
200
+ rtllint -I rtl/includes -I rtl/common rtl/core/alu.v
201
+
202
+ # Multiple files
203
+ rtllint rtl/core/*.v
204
+
205
+ # Preview without modifying files
206
+ rtllint --dry-run rtl/core/alu.v
207
+ ```
208
+
209
+ ### What gets written
210
+
211
+ Given a verilator warning on line 75:
212
+
213
+ ```
214
+ %Warning-WIDTHEXPAND: rtl/alu.v:75:12: Operator ADD generates 9 bits ...
215
+ ```
216
+
217
+ rtllint modifies `alu.v` in place:
218
+
219
+ ```verilog
220
+ // lint-test: verilator --lint-only -Wall rtl/alu.v
221
+ // tb-test: tba
222
+ ...
223
+ assign result = a + b; /* Check: Operator ADD generates 9 bits ... */
224
+ ```
225
+
226
+ Re-running rtllint replaces existing `/* Check: */` tags — it does not stack duplicates.
227
+
228
+ ### CLI reference
229
+
230
+ ```
231
+ rtllint FILE [FILE...] [options]
232
+
233
+ -I, --include DIR Add include directory (repeatable)
234
+ --dry-run Show issues without modifying files
235
+ -v Print full verilator output
236
+ ```
237
+
238
+ Exit codes: `0` = no issues found, `1` = one or more warnings/errors.
239
+
240
+ ---
241
+
242
+ ## Design principles
243
+
244
+ - **No heavy dependencies** — stdlib only, no parser frameworks
245
+ - **CI-first** — every flag is designed for scripted use
246
+ - **Safe for teams** — manual descriptions are never overwritten
247
+ - **Diff-aware** — files are only touched when content actually changes
248
+
249
+ ---
250
+
251
+ ## Supported syntax
252
+
253
+ | Feature | Status |
254
+ |---------|--------|
255
+ | Verilog-2001 (`.v`) | Supported |
256
+ | SystemVerilog (`.sv`) | Supported (port/param parsing) |
257
+ | ANSI port declarations | Supported |
258
+ | Comma-inherited port direction (`output reg a, b`) | Supported |
259
+ | Parameterised modules (`#(parameter ...)`) | Supported |
260
+ | Module instantiations with `#()` param override | Supported |
261
+ | Testbench auto-exclusion | Supported |
262
+ | Pre-2001 Verilog style | Not supported |
263
+ | Multi-module files | Not supported (first module only) |
@@ -0,0 +1,10 @@
1
+ rtl_aid/__init__.py,sha256=J3qdjHJISA6Cqd6sKZ2brPidpfxjy2a12O4Z4Wwu0TE,91
2
+ rtl_aid/cli.py,sha256=auXA5TCtGrpgxIf3qWL0ez9FA3BX3htst-YSuE6BY40,2480
3
+ rtl_aid/core.py,sha256=joZ_isBh4jFogCI-GRQ8NmICwqajKScc3QpDX4KajU8,12369
4
+ rtl_aid/lint.py,sha256=zlkb4GVUoDyz_mHqX62b5lMiIZMHAb4ZTnQC9WKAb9I,5707
5
+ rtl_aid-0.1.0.dist-info/licenses/LICENSE,sha256=ZRKXRB0OwPXwHVrgs_FKR0ccE7iVilonlm8hFTEq2II,1082
6
+ rtl_aid-0.1.0.dist-info/METADATA,sha256=DbTgxtm7trwoRNtmsJoVYFt8PkSq4q3IAz4fcYeUkPg,5922
7
+ rtl_aid-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
8
+ rtl_aid-0.1.0.dist-info/entry_points.txt,sha256=Ix76H3ujuKDefSIxfkE40k19yTwzRu1W2K0Z8aK0Ef0,72
9
+ rtl_aid-0.1.0.dist-info/top_level.txt,sha256=Mm_ImtgV6c0zD1ZqbVx4-P2uwxKTAqpCHiTO0qbQWM8,8
10
+ rtl_aid-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ rtldoc = rtl_aid.cli:main
3
+ rtllint = rtl_aid.lint:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vishwaksen Reddy Dhareddy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ rtl_aid