-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathverilator_runner.py
More file actions
1682 lines (1409 loc) · 76.3 KB
/
verilator_runner.py
File metadata and controls
1682 lines (1409 loc) · 76.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
Incremental Verilator runner - bottom-up dependency resolution.
Instead of starting with all files and removing problematic ones,
this approach starts with just the top module and incrementally adds
only the files that are actually needed based on Verilator's error messages.
Strategy:
1. Start with only the top module file
2. Run Verilator and parse errors for missing modules/packages/includes
3. Add only the files that provide those missing dependencies
4. Repeat until no more dependencies are needed or we hit a failure
This avoids the cascading exclusion problem by never adding files we don't need.
"""
from __future__ import annotations
from typing import List, Tuple, Set, Dict, Optional
import os
import re
import time
import subprocess
from core.log import print_green, print_yellow, print_red, print_blue
from core.file_manager import find_missing_modules, find_missing_module_files
def _detect_systemverilog_keyword_conflict(log_text: str) -> bool:
"""
Detect if errors are caused by SystemVerilog reserved keywords being used as identifiers.
Common keywords: dist, randomize, constraint, covergroup, inside, with, etc.
Returns True if we should retry with plain Verilog mode.
"""
# Pattern: syntax error, unexpected <KEYWORD>, expecting IDENTIFIER
# This indicates the code uses a SystemVerilog keyword as an identifier
sv_keywords = [
'dist', 'randomize', 'constraint', 'covergroup', 'inside', 'with',
'foreach', 'unique', 'priority', 'final', 'alias', 'matches',
'tagged', 'extern', 'pure', 'context', 'solve', 'before', 'after'
]
for keyword in sv_keywords:
# Pattern: "syntax error, unexpected <keyword>, expecting IDENTIFIER"
pattern = rf"syntax error, unexpected {keyword}, expecting IDENTIFIER"
if re.search(pattern, log_text, re.IGNORECASE):
print_yellow(f"[INCREMENTAL] Detected SystemVerilog keyword conflict: '{keyword}' used as identifier")
return True
return False
def _normalize_path(path: str, repo_root: str) -> str:
"""Normalize a file path to be relative to repo_root."""
try:
if os.path.isabs(path):
return os.path.relpath(path, repo_root).replace("\\", "/")
return path.replace("\\", "/")
except Exception:
return path.replace("\\", "/")
def _run(cmd: List[str], cwd: str, timeout: int = 300) -> Tuple[int, str]:
"""Run a command and capture output."""
try:
proc = subprocess.Popen(
cmd,
cwd=cwd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
)
output_lines = []
start = time.time()
while True:
if proc.poll() is not None:
# Process finished
remaining = proc.stdout.read()
if remaining:
output_lines.append(remaining)
break
line = proc.stdout.readline()
if line:
output_lines.append(line)
print(line, end='')
if time.time() - start > timeout:
proc.kill()
output_lines.append(f"\n[TIMEOUT] Process killed after {timeout}s\n")
break
return proc.returncode or 0, "".join(output_lines)
except Exception as e:
return 1, f"[EXCEPTION] {e}"
def _parse_missing_modules(log_text: str) -> List[str]:
"""Extract missing module names from Verilator errors."""
missing = set()
patterns = [
r"Cannot find file containing module: '([^']+)'",
r'Cannot find file containing module: "([^"]+)"',
r"Can't resolve module reference: '([^']+)'", # For "Can't resolve module reference: 'module_name'"
]
for pat in patterns:
for m in re.finditer(pat, log_text, flags=re.IGNORECASE):
missing.add(m.group(1))
return list(missing)
def _parse_missing_packages(log_text: str) -> List[str]:
"""Extract missing package names from Verilator errors."""
missing = set()
patterns = [
r"Package/class '([^']+)' not found",
r"%Error-PKGNODECL:\s*[^:]+:\d+:\d+:\s*Package/class '([^']+)' not found",
r"Importing from missing package '([^']+)'", # For "Importing from missing package 'pkg_name'"
r"Can't find typedef/interface:\s*'([^']+)'", # Typedefs often come from packages
r"Package/class for ':: reference' not found:\s*'([^']+)'", # For namespace reference errors
]
for pat in patterns:
for m in re.finditer(pat, log_text, flags=re.IGNORECASE):
missing.add(m.group(1))
return list(missing)
def _parse_package_scope_references(log_text: str) -> List[str]:
"""
Extract package names from scope resolution syntax errors (pkg::identifier).
When Verilator sees 'pkg_name::something' but pkg_name is not defined,
it reports "syntax error, unexpected ':'" - we can detect the package name
from the source line shown in the error.
"""
missing = set()
# Look for patterns like: "localparam X = pkg_name::Something"
# The error will show the line with the :: usage
pattern = r'\b([a-zA-Z_][a-zA-Z0-9_]*)::'
for m in re.finditer(pattern, log_text):
pkg_name = m.group(1)
# Filter out common non-package prefixes
if pkg_name not in ['std', 'this', 'super', 'local']:
missing.add(pkg_name)
return list(missing)
def _parse_missing_includes(log_text: str) -> List[str]:
"""Extract missing include file names from Verilator errors."""
missing = set()
patterns = [
r"Cannot find include file: ['\"]([^'\"]+)['\"]",
r"can't find file \"([^\"]+)\"",
]
for pat in patterns:
for m in re.finditer(pat, log_text, flags=re.IGNORECASE):
missing.add(m.group(1))
return list(missing)
def _parse_missing_interfaces(log_text: str) -> List[str]:
"""Extract missing interface names from Verilator errors."""
missing = set()
patterns = [
r"Cannot find file containing interface: '([^']+)'",
]
for pat in patterns:
for m in re.finditer(pat, log_text, flags=re.IGNORECASE):
missing.add(m.group(1))
return list(missing)
def _parse_included_files_with_errors(log_text: str, repo_root: str) -> List[str]:
"""
Extract files that have typedef/package errors and are included from other files.
These files (especially interfaces) often need to be compiled as standalone files.
Pattern:
%Error: path/to/file.sv:30:5: Can't find typedef/interface: 'TypeName'
30 | TypeName variable;
| ^~~~~~~~~
parent/file.sv:110:1: ... note: In file included from 'parent.sv'
Returns list of relative file paths that should be added to compilation.
"""
files = set()
# Pattern to match error in a file followed by "included from" note (with possible intervening lines)
# Capture the file with the error
pattern = r"%Error:\s*([a-zA-Z0-9_/.\\-]+\.sv[h]?):\d+:\d+:\s*Can't find (?:typedef/interface|package)[^\n]*(?:\n(?!%Error:)[^\n]*){0,5}?In file included from"
for m in re.finditer(pattern, log_text, flags=re.MULTILINE):
file_path = m.group(1)
# Normalize path relative to repo_root
rel_path = _normalize_path(file_path, repo_root)
files.add(rel_path)
return list(files)
def _parse_missing_interfaces(log_text: str) -> List[str]:
"""Extract missing interface names from Verilator errors."""
missing = set()
patterns = [
r"Cannot find file containing interface: '([^']+)'",
]
for pat in patterns:
for m in re.finditer(pat, log_text, flags=re.IGNORECASE):
missing.add(m.group(1))
return list(missing)
def _parse_forward_declaration_files(log_text: str, repo_root: str) -> List[str]:
"""
Extract file paths from 'Reference before declaration' errors.
These files contain type definitions that need to be compiled first.
Example error:
%Error: file.sv:23:14: Reference to 'bp_params_e' before declaration
file.sv:599:5: ... Location of original declaration
Returns list of relative file paths that need to be added to compilation.
"""
files = set()
# Pattern: ... Location of original declaration
# Followed by filename:line:col on next line
pattern = r"Location of original declaration\s*\n\s*(\d+)\s*\|\s*"
# Alternative pattern: look for lines with file paths after "Location of" messages
# Format: bp_common/src/include/file.svh:599:5: ... Location of original declaration
decl_pattern = r"^\s*([a-zA-Z0-9_/.\\-]+\.[sv]+h?):\d+:\d+:.*Location of original declaration"
for m in re.finditer(decl_pattern, log_text, flags=re.MULTILINE):
file_path = m.group(1)
# Normalize path relative to repo_root
rel_path = _normalize_path(file_path, repo_root)
files.add(rel_path)
return list(files)
def _parse_missing_import_packages(log_text: str) -> List[str]:
"""Extract package names from 'Import package not found' errors."""
missing = set()
pattern = r"Import package not found: '([^']+)'"
for m in re.finditer(pattern, log_text, flags=re.IGNORECASE):
missing.add(m.group(1))
return list(missing)
def _parse_missing_defines(log_text: str) -> List[str]:
"""
Extract undefined macro/define names from Verilator errors.
Example error:
%Error: rtl/nox.sv:20:38: Define or directive not defined: '`M_HART_ID'
Returns list of define names (without the backtick).
"""
missing = set()
# Pattern: Define or directive not defined: '`NAME'
pattern = r"Define or directive not defined:\s*'`([^']+)'"
for m in re.finditer(pattern, log_text, flags=re.IGNORECASE):
missing.add(m.group(1))
return list(missing)
def _find_header_with_define(repo_root: str, define_name: str, module_graph: Dict[str, Dict]) -> List[str]:
"""
Find header files (.vh, .svh) that define a specific macro.
Searches through all header files in the repository to find files containing
`define MACRO_NAME definitions.
Args:
repo_root: Root directory of the repository
define_name: Name of the define to search for (without backtick)
module_graph: Optional module graph with file information
Returns:
List of relative paths to header files that define this macro
"""
candidates = []
# Pattern to match: `define MACRO_NAME
pattern = re.compile(rf'^\s*`define\s+{re.escape(define_name)}\b', re.IGNORECASE | re.MULTILINE)
# Search through the repository for header files
for root, dirs, files in os.walk(repo_root):
# Skip common non-source directories
dirs[:] = [d for d in dirs if d not in {'.git', 'obj_dir', 'build', 'sim_build', '__pycache__'}]
for file in files:
if not (file.endswith('.vh') or file.endswith('.svh') or file.endswith('.v') or file.endswith('.sv')):
continue
full_path = os.path.join(root, file)
try:
with open(full_path, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
if pattern.search(content):
# Convert to relative path
rel_path = os.path.relpath(full_path, repo_root)
candidates.append(rel_path)
print_yellow(f"[INCREMENTAL] Found define '{define_name}' in: {rel_path}")
except Exception:
continue
return candidates
def _find_package_including_file(repo_root: str, include_file: str, module_graph: Dict[str, Dict]) -> List[str]:
"""
Find package (.sv) files that include a given .svh file.
When a .svh file contains typedefs used before declaration, we need to compile
the package file that includes it, not the .svh itself.
Args:
repo_root: Repository root
include_file: Relative path to .svh file (e.g., "bp_common/src/include/bp_common_aviary_pkgdef.svh")
module_graph: Module dependency graph
Returns:
List of package files (.sv) that include this file
"""
package_files: List[str] = []
include_basename = os.path.basename(include_file)
# Always use filesystem fallback as it's more reliable than module_graph for this use case
# Scan the repository filesystem for .sv files that include the .svh
for root, dirs, files in os.walk(repo_root):
dirs[:] = [d for d in dirs if d not in {'.git', 'obj_dir', 'build', 'out'}]
for fname in files:
if not fname.lower().endswith('.sv'):
continue
full_path = os.path.join(root, fname)
try:
with open(full_path, 'r', encoding='utf-8', errors='ignore') as fh:
content = fh.read(50000)
include_pattern = re.compile(rf'`include\s+["\']([^"\']*{re.escape(include_basename)})["\']', re.IGNORECASE)
if include_pattern.search(content) and re.search(r'^\s*package\s+\w+\s*;', content, re.MULTILINE | re.IGNORECASE):
rel = _normalize_path(os.path.relpath(full_path, repo_root), repo_root)
package_files.append(rel)
except Exception:
continue
return package_files
def _find_file_declaring_module(repo_root: str, module_name: str, module_graph: Dict[str, Dict]) -> List[str]:
"""
Find file(s) that declare a given module using the module graph.
Args:
repo_root: Repository root path
module_name: Name of the module to find
module_graph: Module dependency graph from config_generator_core
Returns:
List of relative paths to files declaring the module (may be empty or have multiple)
"""
if not module_graph:
# Fallback to file system search
return _find_file_by_search(repo_root, module_name, "module")
# Search in the module graph
found_files = []
for file_path, info in module_graph.items():
if 'modules' in info and module_name in info['modules']:
found_files.append(_normalize_path(file_path, repo_root))
if found_files:
return found_files
# Module not in graph, try file search fallback
return _find_file_by_search(repo_root, module_name, "module")
def _find_file_declaring_package(repo_root: str, package_name: str, module_graph: Dict[str, Dict]) -> List[str]:
"""Find the file(s) that declare a given package. Returns list of file paths (can be multiple for versioned packages)."""
if not module_graph:
return _find_file_by_search(repo_root, package_name, "package")
# Search in the module graph - collect ALL files that declare this package
found_files = []
for file_path, info in module_graph.items():
if 'packages' in info and package_name in info['packages']:
rel_path = _normalize_path(file_path, repo_root)
found_files.append(rel_path)
if found_files:
return found_files
# Package not in graph, try file search fallback
return _find_file_by_search(repo_root, package_name, "package")
def _find_file_declaring_interface(repo_root: str, interface_name: str, module_graph: Dict[str, Dict]) -> List[str]:
"""Find the file(s) that declare a given interface. Returns list of file paths."""
if not module_graph:
return _find_file_by_search(repo_root, interface_name, "interface")
# Search in the module graph
found_files = []
for file_path, info in module_graph.items():
if 'interfaces' in info and interface_name in info['interfaces']:
found_files.append(_normalize_path(file_path, repo_root))
if found_files:
return found_files
# Interface not in graph, try file search fallback
return _find_file_by_search(repo_root, interface_name, "interface")
def _find_file_by_search(repo_root: str, symbol_name: str, symbol_type: str) -> List[str]:
"""
Fallback: search the repository for files declaring the given symbol.
Args:
repo_root: Repository root
symbol_name: Name of the symbol (module/package/interface)
symbol_type: Type of symbol ("module", "package", or "interface")
Returns:
List of relative paths to files (may be empty or have multiple matches)
"""
# Build search patterns - for modules, also try interface since Verilator
# sometimes reports missing interfaces as missing modules
patterns = []
if symbol_type == "module":
# Try both module and interface patterns
patterns.append(re.compile(rf"^\s*module\s+{re.escape(symbol_name)}\b", re.IGNORECASE | re.MULTILINE))
patterns.append(re.compile(rf"^\s*interface\s+{re.escape(symbol_name)}\b", re.IGNORECASE | re.MULTILINE))
else:
# For package and interface, use exact type
patterns.append(re.compile(rf"^\s*{symbol_type}\s+{re.escape(symbol_name)}\b", re.IGNORECASE | re.MULTILINE))
found_files = []
for root, dirs, files in os.walk(repo_root):
# Skip common non-source directories
dirs[:] = [d for d in dirs if d not in {'.git', 'obj_dir', 'build', 'out'}]
for fname in files:
if not fname.endswith(('.sv', '.v', '.svh', '.vh')):
continue
full_path = os.path.join(root, fname)
try:
with open(full_path, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read() # Read entire file
# Check if any pattern matches
for pattern in patterns:
if pattern.search(content):
found_files.append(_normalize_path(full_path, repo_root))
break # Found in this file, no need to check other patterns
except Exception:
continue
return found_files
def _find_package_with_typedef(repo_root: str, typedef_name: str, module_graph: Dict[str, Dict]) -> List[str]:
"""
Find the package file(s) that contain a given typedef/struct.
When Verilator reports "Can't find typedef/interface: 'TypeName'", the type might be
defined inside a package. This function searches for files containing the typedef and
determines which package declares it.
Args:
repo_root: Repository root
typedef_name: Name of the typedef (e.g., 'BypassControll')
module_graph: Module dependency graph
Returns:
List of file paths declaring the package containing this typedef
"""
# Search for files containing this typedef
# Pattern matches:
# 1. } TypeName; (closing brace of struct/enum/union)
# 2. typedef struct/enum/union ... TypeName;
typedef_pattern = re.compile(
rf'\}}\s*{re.escape(typedef_name)}\s*;' # } TypeName;
rf'|typedef\s+(?:struct|enum|union)\s+.*\s+{re.escape(typedef_name)}\s*;' # typedef struct/enum/union ... TypeName;
,
re.MULTILINE | re.IGNORECASE
)
package_pattern = re.compile(r'^\s*package\s+(\w+)\s*;', re.MULTILINE)
found_packages = set()
files_searched = 0
matches_found = 0
for root, dirs, files in os.walk(repo_root):
dirs[:] = [d for d in dirs if d not in {'.git', 'obj_dir', 'build', 'out'}]
for fname in files:
if not fname.endswith(('.sv', '.svh')):
continue
files_searched += 1
full_path = os.path.join(root, fname)
try:
with open(full_path, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
# Check if this file contains the typedef
if typedef_pattern.search(content):
matches_found += 1
# Find which package this typedef is in
pkg_match = package_pattern.search(content)
if pkg_match:
package_name = pkg_match.group(1)
# Now find the file declaring this package
pkg_files = _find_file_declaring_package(repo_root, package_name, module_graph)
if pkg_files:
found_packages.update(pkg_files)
else:
print_yellow(f"[INCREMENTAL] Found typedef '{typedef_name}' in package '{package_name}', but package file not found")
else:
print_yellow(f"[INCREMENTAL] Found typedef '{typedef_name}' in {os.path.relpath(full_path, repo_root)}, but no package declaration")
except Exception as e:
continue
if not found_packages:
print_yellow(f"[INCREMENTAL] Searched {files_searched} files ({matches_found} matches), typedef '{typedef_name}' package not found")
return list(found_packages)
def _find_include_file(repo_root: str, include_name: str, current_includes: Set[str]) -> Optional[str]:
"""
Find an include file by searching include directories.
Returns the include directory containing the file (to be added to -I flags).
"""
# First check existing include directories
for inc_dir in current_includes:
full_inc_dir = os.path.join(repo_root, inc_dir) if not os.path.isabs(inc_dir) else inc_dir
potential_file = os.path.join(full_inc_dir, include_name)
if os.path.exists(potential_file):
return inc_dir
# Search the entire repository
for root, dirs, files in os.walk(repo_root):
dirs[:] = [d for d in dirs if d not in {'.git', 'obj_dir', 'build', 'out'}]
if include_name in files:
rel_dir = _normalize_path(root, repo_root)
return rel_dir
return None
def _is_pkg_file(path: str) -> bool:
"""Heuristic: treat files that define packages as package files.
Common convention is *_pkg.sv or *_pkg.svh. Many repos also use *_types.sv or *_config*.sv.
Also catch paths with '/pkg/' in them.
"""
p = path.lower()
base = os.path.basename(p)
return (
base.endswith("_pkg.sv")
or base.endswith("_pkg.svh")
or base.endswith("_types.sv")
or base.endswith("types.sv")
or base.endswith("_types.svh")
or base.endswith("types.svh")
or base.endswith("_config.sv")
or base.endswith("_config.svh")
or "config_and_types" in base
or "/pkg/" in p.replace("\\", "/")
)
def _order_sv_files(files: List[str], repo_root: str | None = None) -> List[str]:
"""Order SV files generically so that package providers compile before importers.
Generic algorithm:
- Detect which of the given files declare a SystemVerilog package.
- Parse "import <pkg>::*;" and similar forms in all files to build a dependency graph
between files via package usage.
- Detect explicit ordering constraints via `ifdef/`ifndef with `error directives.
- Topologically sort files so that package definitions appear before files that import them.
- Fall back to a heuristic (package-like files first) when content can't be read.
"""
if not repo_root:
# Fallback: simple, generic package-first ordering without hard-coded names
indexed = list(enumerate(files))
indexed.sort(key=lambda t: (0 if _is_pkg_file(t[1]) else 1, t[0]))
return [f for _i, f in indexed]
# Build mapping: package name -> file declaring it (within the provided file set)
pkg_decl_re = re.compile(r"^\s*package\s+(\w+)\s*;", re.IGNORECASE | re.MULTILINE)
import_re = re.compile(r"^\s*import\s+([a-zA-Z_]\w*)\s*::\s*\*\s*;", re.IGNORECASE | re.MULTILINE)
# Also catch inline/import lists: import a::*, b::*;
import_list_re = re.compile(r"^\s*import\s+([^;]+);", re.IGNORECASE | re.MULTILINE)
# Detect namespace references like package_name::type_name
namespace_ref_re = re.compile(r"\b([a-zA-Z_]\w+)::[a-zA-Z_]\w+", re.MULTILINE)
# Detect explicit ordering constraints: `ifdef DEFINE + `error means this file must come before files defining DEFINE
ifdef_error_re = re.compile(r"^\s*`ifdef\s+(\w+)\s*\n\s*`error", re.IGNORECASE | re.MULTILINE)
# Detect define declarations
define_re = re.compile(r"^\s*`define\s+(\w+)", re.IGNORECASE | re.MULTILINE)
# Detect typedef declarations (enum, struct, union) - these need to be compiled before use
typedef_re = re.compile(r"^\s*}\s*(\w+)\s*;", re.IGNORECASE | re.MULTILINE) # } type_name;
# Detect parameter type usage: parameter TYPE_NAME param_name = ...
param_type_re = re.compile(r"^\s*#?\s*\(\s*parameter\s+([a-zA-Z_]\w+)\s+", re.IGNORECASE | re.MULTILINE)
file_to_imports: Dict[str, Set[str]] = {f: set() for f in files}
pkg_to_file: Dict[str, str] = {}
# Map: define name -> file that defines it
define_to_file: Dict[str, str] = {}
# Map: file -> set of defines it requires to NOT be defined yet (must come before definer)
file_to_forbidden_defines: Dict[str, Set[str]] = {f: set() for f in files}
# Map: typedef name -> file that defines it
typedef_to_file: Dict[str, str] = {}
# Map: file -> set of typedefs it uses as parameter types
file_to_param_types: Dict[str, Set[str]] = {f: set() for f in files}
def _read(rel_path: str) -> str:
p = os.path.join(repo_root, rel_path)
try:
with open(p, "r", encoding="utf-8", errors="ignore") as fh:
return fh.read()
except Exception:
return ""
# Track any files that declare a package (provider files)
provider_files: Set[str] = set()
for f in files:
text = _read(f)
if not text:
continue
# Package declarations in this file
for m in pkg_decl_re.finditer(text):
pkg = m.group(1)
# only first package per file is considered for mapping
if pkg and pkg not in pkg_to_file:
pkg_to_file[pkg] = f
provider_files.add(f)
break
# Define declarations in this file
for m in define_re.finditer(text):
define = m.group(1)
if define and define not in define_to_file:
define_to_file[define] = f
# Typedef declarations in this file (enum, struct, union)
for m in typedef_re.finditer(text):
typedef = m.group(1)
if typedef and typedef not in typedef_to_file:
typedef_to_file[typedef] = f
# Explicit ordering constraints: ifdef + error means must come before definer
for m in ifdef_error_re.finditer(text):
define = m.group(1)
if define:
file_to_forbidden_defines[f].add(define)
# Parameter type usage: #(parameter TYPE_NAME param_name = ...)
for m in param_type_re.finditer(text):
param_type = m.group(1)
# Filter out built-in types and common parameter patterns
if param_type not in ['int', 'integer', 'logic', 'bit', 'reg', 'wire', 'real', 'string', 'signed', 'unsigned']:
file_to_param_types[f].add(param_type)
# Collect imports per file
for f in files:
text = _read(f)
if not text:
continue
# Fast path for common form
for m in import_re.finditer(text):
file_to_imports[f].add(m.group(1))
# Handle list imports (import a::*, b::c, d::*;)
for m in import_list_re.finditer(text):
part = m.group(1)
# Split by comma and extract package names before '::'
for seg in part.split(','):
seg = seg.strip()
if '::' in seg:
pkg = seg.split('::', 1)[0].strip()
if re.match(r"^[a-zA-Z_]\w*$", pkg):
file_to_imports[f].add(pkg)
# Detect namespace references (package_name::identifier)
# This catches cases where packages are used without explicit import
for m in namespace_ref_re.finditer(text):
pkg = m.group(1)
# Only consider it if the package exists in our file set
# and it's not a common false positive (like $unit::, etc.)
if pkg not in ['$unit', 'std', 'this', 'super', 'local'] and pkg in pkg_to_file:
file_to_imports[f].add(pkg)
# Build graph: edge from provider file -> importer file
nodes = list(files)
index_map = {f: i for i, f in enumerate(nodes)} # for stable tie-breaks
adj: Dict[str, Set[str]] = {f: set() for f in nodes}
indeg: Dict[str, int] = {f: 0 for f in nodes}
# Add edges for package imports
for f, imports in file_to_imports.items():
for pkg in imports:
provider = pkg_to_file.get(pkg)
if provider and provider != f:
if f not in adj[provider]:
adj[provider].add(f)
indeg[f] += 1
# Add edges for explicit ordering constraints (ifdef + error)
# If file A checks ifdef DEFINE and errors, and file B defines DEFINE,
# then A must come before B: add edge A -> B
for f, forbidden in file_to_forbidden_defines.items():
for define in forbidden:
definer = define_to_file.get(define)
if definer and definer != f:
if definer not in adj[f]:
adj[f].add(definer)
indeg[definer] += 1
# Add edges for typedef dependencies
# If file A uses a typedef as a parameter type, and file B defines that typedef,
# then B must come before A: add edge B -> A
for f, param_types in file_to_param_types.items():
for typedef in param_types:
definer = typedef_to_file.get(typedef)
if definer and definer != f:
if f not in adj[definer]:
adj[definer].add(f)
indeg[f] += 1
# Kahn's algorithm for topo sort, preserving original order among equals
zero_indeg = sorted([n for n in nodes if indeg[n] == 0], key=lambda x: index_map[x])
ordered: List[str] = []
while zero_indeg:
n = zero_indeg.pop(0)
ordered.append(n)
for m in sorted(adj[n], key=lambda x: index_map[x]):
indeg[m] -= 1
if indeg[m] == 0:
zero_indeg.append(m)
zero_indeg.sort(key=lambda x: index_map[x])
if len(ordered) != len(nodes):
# Cycle or unreadable content; append remaining by original order to avoid loss
remaining = [n for n in nodes if n not in ordered]
remaining.sort(key=lambda x: index_map[x])
ordered.extend(remaining)
# As a final gentle nudge, ensure actual package-declaring files (by content)
# appear as early as possible without violating the topo order: stable partition.
# If we failed to read contents (empty provider set), fallback to filename heuristics.
if not provider_files:
pkg_like = [f for f in ordered if _is_pkg_file(f)]
non_pkg = [f for f in ordered if not _is_pkg_file(f)]
return pkg_like + non_pkg
else:
pkg_like = [f for f in ordered if f in provider_files]
non_pkg = [f for f in ordered if f not in provider_files]
return pkg_like + non_pkg
def _has_sv_keyword_as_identifier(repo_root: str, files: List[str]) -> bool:
"""
Check if files use SystemVerilog keywords as identifiers (like block labels, variable names, etc.).
Common problematic keywords: dist, randomize, constraint, etc.
"""
import re
# SystemVerilog keywords that might be used as identifiers in Verilog code
# Note: 'bit' and 'logic' are removed since they're fundamental SV types and often used correctly
# Note: 'with' is removed since it matches comments too often
sv_keywords = ['dist', 'randomize', 'constraint', 'covergroup', 'coverpoint',
'bins', 'illegal_bins', 'ignore_bins', 'cross',
'matches', 'inside', 'tagged', 'priority', 'unique']
# Pattern to detect keyword used as identifier (e.g., "begin:dist", "wire dist", "reg [7:0] bit", etc.)
identifier_patterns = [
re.compile(r'\bbegin\s*:\s*(' + '|'.join(sv_keywords) + r')\b', re.I), # begin:keyword
re.compile(r'\b(wire|reg|logic|input|output|inout)(\s+\[[^\]]+\])?\s+(' + '|'.join(sv_keywords) + r')\b', re.I), # wire keyword or reg [N:0] keyword
re.compile(r'\bparameter\s+(' + '|'.join(sv_keywords) + r')\s*=', re.I), # parameter keyword =
]
print_blue(f"[KEYWORD CHECK] Checking {len(files)} files for SV keywords as identifiers")
for file_rel in files:
file_path = os.path.join(repo_root, file_rel) if not os.path.isabs(file_rel) else file_rel
try:
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
for pattern in identifier_patterns:
match = pattern.search(content)
if match:
print_yellow(f"[KEYWORD CHECK] FOUND SV keyword in {file_rel}: {match.group(0)}")
return True
except Exception:
continue
return False
def _detect_language_for_files(repo_root: str, files: List[str], module_graph: Dict[str, Dict] = None, include_dirs: Set[str] = None) -> str:
"""
Detect the appropriate language version for the given files.
This checks for SystemVerilog features (logic, interface, always_ff, etc.) in the actual files.
Also checks for SystemVerilog keywords used as identifiers, which requires Verilog mode.
Order matters: Check for SV features FIRST, then check for keyword conflicts.
This way, files with legitimate SV features get SV mode even if they use keywords as identifiers.
If module_graph is provided, also checks all dependency files (recursively).
If include_dirs is provided, also checks all .v/.sv files in those directories.
"""
import re
# Collect all files to check: direct files + dependencies from module graph + files in include dirs
files_to_check = set(files)
if module_graph:
# For each file in our compilation list, add all its dependencies
def collect_dependencies(file_path: str, visited: Set[str]):
if file_path in visited:
return
visited.add(file_path)
if file_path in module_graph:
info = module_graph[file_path]
# Add files from dependencies (modules, includes, etc.)
for dep_file in info.get('files', []):
if dep_file not in visited:
files_to_check.add(dep_file)
collect_dependencies(dep_file, visited)
visited = set()
for file_path in files:
collect_dependencies(file_path, visited)
# Add all .v/.sv files from include directories
if include_dirs:
for inc_dir in include_dirs:
full_inc_dir = os.path.join(repo_root, inc_dir) if not os.path.isabs(inc_dir) else inc_dir
if os.path.isdir(full_inc_dir):
try:
for file in os.listdir(full_inc_dir):
if file.endswith(('.v', '.sv', '.svh')):
# Use relative path from repo_root
rel_path = os.path.join(inc_dir, file) if not os.path.isabs(inc_dir) else os.path.join(full_inc_dir, file)
files_to_check.add(rel_path)
except Exception:
pass
# Also scan all .v/.sv files in the same directories as files we're compiling
# This catches cases where files in the same directory might use SV keywords but aren't in the initial list
scanned_dirs = set()
initial_file_count = len(files_to_check)
for file_rel in list(files_to_check):
file_dir = os.path.dirname(file_rel)
if file_dir and file_dir not in scanned_dirs:
scanned_dirs.add(file_dir)
full_dir = os.path.join(repo_root, file_dir) if not os.path.isabs(file_dir) else file_dir
if os.path.isdir(full_dir):
try:
dir_files = []
for file in os.listdir(full_dir):
if file.endswith(('.v', '.sv', '.svh')):
rel_path = os.path.join(file_dir, file) if file_dir else file
if rel_path not in files_to_check:
dir_files.append(file)
files_to_check.add(rel_path)
if dir_files:
print_blue(f"[LANG DETECT] Dir {file_dir}: added {dir_files}")
except Exception:
pass
# FIRST: Check for keyword conflicts (highest priority)
# Plain Verilog code using SV keywords as identifiers must use Verilog mode
has_keyword_conflict = _has_sv_keyword_as_identifier(repo_root, list(files_to_check))
if has_keyword_conflict:
return "1364-2005"
# SECOND: Check for SystemVerilog features
# If no keyword conflicts but has SV features, use SystemVerilog mode
has_sv_features = False
has_logic_keyword = False
has_interface = False
has_always_ff = False
print_blue(f"[LANG DETECT] No keyword conflicts, checking {len(files_to_check)} files for SV features...")
# Patterns for SystemVerilog features
sv_patterns = {
'logic': re.compile(r'\blogic\b', re.I),
'interface': re.compile(r'\binterface\b|\bmodport\b', re.I),
'always_ff': re.compile(r'\balways_ff\b|\balways_comb\b|\balways_latch\b', re.I),
'class': re.compile(r'\bclass\b', re.I),
'typedef_struct': re.compile(r'\btypedef\s+(enum|struct|union)', re.I),
}
for file_rel in files_to_check:
file_path = os.path.join(repo_root, file_rel) if not os.path.isabs(file_rel) else file_rel
try:
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
# Check for SystemVerilog features
if sv_patterns['logic'].search(content):
has_logic_keyword = True
has_sv_features = True
if sv_patterns['interface'].search(content):
has_interface = True
has_sv_features = True
if sv_patterns['always_ff'].search(content):
has_always_ff = True
has_sv_features = True
if sv_patterns['class'].search(content):
has_sv_features = True
if sv_patterns['typedef_struct'].search(content):
has_sv_features = True
except Exception as e:
continue
# If we found SV features, use SystemVerilog mode
if has_sv_features:
return "1800-2023"
print_blue(f"[LANG DETECT] No SV features found either")
# Check file extensions
has_sv_ext = any(f.endswith(('.sv', '.svh')) for f in files_to_check)
if has_sv_ext:
return "1800-2023"
return "1364-2005" # Default to Verilog-2005
def _build_verilator_cmd(
repo_root: str,
files: List[str],
include_dirs: Set[str],
top_module: str,
language_version: str = "1800-2023",
extra_flags: List[str] = None,
module_graph: Dict[str, Dict] = None
) -> List[str]:
"""Build the Verilator command line."""
cmd = [
"verilator",
"--lint-only",
"-Wall",
"--no-timing",
"-Wno-PROCASSWIRE",
"--relative-includes",
"-DEN_EXCEPT",
"-DEN_RVZICSR",
]
# Dynamically detect language for current file set AND their dependencies from module graph
# Also check files in include directories
detected_lang = _detect_language_for_files(repo_root, files, module_graph, include_dirs)
# Always set the language version
cmd.extend(["--language", detected_lang])
# Additionally, if SystemVerilog features detected, add --sv flag
if detected_lang.startswith("1800"):
cmd.append("--sv")
# Top module
if top_module:
cmd.extend(["--top-module", top_module])
# Include directories
for inc_dir in sorted(include_dirs):
cmd.append(f"-I{inc_dir}")
# Extra flags
if extra_flags:
cmd.extend(extra_flags)
# Files - filter out .h files (they should be in include dirs, not compiled directly)
# .h files in Verilog/SystemVerilog are typically config headers that need preprocessing
compilable_files = [f for f in files if not f.endswith('.h')]
# Use topological sorting to ensure packages come before importers
ordered_files = _order_sv_files(compilable_files, repo_root)
cmd.extend(ordered_files)
return cmd
def _try_package_candidates(
pkg_name: str,
pkg_files: List[str],
current_files: List[str],
current_includes: Set[str],
repo_root: str,
top_module: str,
language_version: str,
extra_flags: List[str],