avl-riscv-coverage 0.0.1__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.
@@ -0,0 +1,24 @@
1
+ from ._coverage import COVERPACKAGES, CoverPackage
2
+ from ._elf import EXTENSIONS, INSTRUCTIONS, parse_elf
3
+ from ._instr import Instruction
4
+ from ._isa import ISA, Encoding, extract_isa
5
+ from ._spike import parse_spike_log
6
+ from ._trace import TRACE, Trace
7
+
8
+ # Add version
9
+ __version__ : str = "0.0.1"
10
+
11
+ __all__ = [
12
+ "COVERPACKAGES",
13
+ "CoverPackage",
14
+ "EXTENSIONS",
15
+ "INSTRUCTIONS",
16
+ "parse_elf",
17
+ "Instruction",
18
+ "ISA",
19
+ "extract_isa",
20
+ "Encoding",
21
+ "parse_spike_log",
22
+ "TRACE",
23
+ "Trace",
24
+ ]
@@ -0,0 +1,185 @@
1
+ import argparse
2
+ import atexit
3
+ import importlib
4
+ import os
5
+ import subprocess
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ from avl import Coverage
10
+
11
+ from ._coverage import COVERPACKAGES
12
+ from ._elf import EXTENSIONS, parse_elf
13
+ from ._isa import ISA, extract_isa
14
+ from ._spike import parse_spike_log
15
+ from ._trace import TRACE
16
+
17
+ # Path of the current script (__main__.py) and reference json file
18
+ script_path = os.path.dirname(os.path.abspath(__file__))
19
+ ref_path = os.path.join(script_path, "instr_dict.json")
20
+ packages_path = os.path.join(script_path, "coverage_packages")
21
+ def main():
22
+
23
+ parser = argparse.ArgumentParser()
24
+ parser.add_argument("--ref", type=str, default=ref_path, help=f"Pointer to instr_dict.json (generated from https://github.com/riscv/riscv-opcodes) (default: {ref_path})")
25
+ parser.add_argument("--extensions", nargs="*", default=None, help="List of extensions")
26
+ parser.add_argument("--packages", nargs="*", default=[packages_path], help="List of python packages containing covergroups")
27
+ parser.add_argument("--elf", default=None, help="Elf file for testcase")
28
+ parser.add_argument("--trace", default=None, help="List of trace files")
29
+ parser.add_argument("--output", default="output", help="Output directory for report")
30
+ parser.add_argument("--html", action="store_true", help="Generate HTML report")
31
+ parser.add_argument("--verbose", action="store_true", help="Verbose messages")
32
+
33
+ args = parser.parse_args()
34
+
35
+ # Parse ISA
36
+ # Creates reference dict for all encodings supported
37
+ if args.verbose:
38
+ print(f"Parsing reference json file : {args.ref} ... ", end="")
39
+
40
+ if args.ref is None or not os.path.exists(args.ref):
41
+ print(f"ERROR : must supply a valid instruction reference json file (searched for: {args.ref})")
42
+ sys.exit(1)
43
+ else:
44
+ extract_isa(args.ref)
45
+
46
+ if args.verbose:
47
+ print("OK")
48
+
49
+ # Parse the elf file
50
+ # Constructs the dicts of instructions and extensions in the elf
51
+ if args.verbose:
52
+ print(f"Parsing elf file : {args.elf} ... ", end="")
53
+
54
+ if args.elf is None or not os.path.exists(args.elf):
55
+ print(f"ERROR : must supply a valid elf file (searched for {args.elf})")
56
+ sys.exit(1)
57
+ else:
58
+ parse_elf(args.elf)
59
+
60
+ if args.verbose:
61
+ print("OK")
62
+
63
+ # Prune un-supported extensions
64
+ if args.verbose:
65
+ print("Pruning un-supported encodings ... ", end="")
66
+
67
+ prune = []
68
+ for k, v in ISA.items():
69
+ # Remove instructions with different base
70
+ if v.base not in ["RV", EXTENSIONS["base"]]:
71
+ prune.append(k)
72
+ continue
73
+
74
+ # Remove instructions from unsupported extensions
75
+ matches = list(set(v.extensions) & set(EXTENSIONS.keys()))
76
+ if len(matches) == 0:
77
+ prune.append(k)
78
+
79
+ a = len(ISA)
80
+ for p in prune:
81
+ del ISA[p]
82
+ b = len(ISA)
83
+
84
+ if args.verbose:
85
+ print(f"{a-b} removed")
86
+
87
+ # Parse Tracefile
88
+ if args.verbose:
89
+ print(f"Parsing trace file : {args.trace} ... ", end="")
90
+
91
+ if args.trace is None or not os.path.exists(args.trace):
92
+ print(f"ERROR : must supply a valid trace (searched for: {args.trace})")
93
+ sys.exit(1)
94
+ else:
95
+ parse_spike_log(args.trace)
96
+
97
+ if args.verbose:
98
+ print("OK")
99
+
100
+
101
+ # Create output directory
102
+ if args.verbose:
103
+ print(f"Creating output directory : {args.output} ... ", end="")
104
+
105
+ try:
106
+ path = Path(args.output)
107
+ path.mkdir(parents=True, exist_ok=True)
108
+ except Exception:
109
+ print("Failed to create {args.output} {e}")
110
+ sys.exit(1)
111
+
112
+ if args.verbose:
113
+ print("OK")
114
+
115
+ # Create covergroups
116
+ if args.packages is not None:
117
+ if args.verbose:
118
+ print(f"Adding coverage packages : {','.join(args.packages)} ... ", end="")
119
+
120
+ for p in map(Path, args.packages):
121
+ assert p.is_dir(), f"Packages must be directories: {p}"
122
+
123
+ for f in sorted(p.rglob("*.py")):
124
+ package_name = os.path.basename(f).split(".",1)[0]
125
+ package_spec = importlib.util.spec_from_file_location(package_name, Path(f))
126
+ package_module = importlib.util.module_from_spec(package_spec)
127
+ package_spec.loader.exec_module(package_module)
128
+
129
+ if args.verbose:
130
+ print("OK")
131
+
132
+ # Process traces
133
+ if args.verbose:
134
+ print("Processing Trace ... ", end="")
135
+
136
+ instr = TRACE[0]
137
+ while True:
138
+ if instr is None:
139
+ break
140
+
141
+ # Sample all Coverpackages
142
+ for p in COVERPACKAGES:
143
+ p.sample(instr)
144
+
145
+ instr = instr.next
146
+
147
+ if args.verbose:
148
+ print("OK")
149
+
150
+ # Move coverage.json to output
151
+ if args.verbose:
152
+ print(f"Moving coverage.json to {args.output} ... ", end="")
153
+
154
+ try:
155
+ # Call the atexit to generate the report - then prevent pre-registered callback
156
+ Coverage().__at_exit__()
157
+ atexit.unregister(Coverage().__at_exit__)
158
+
159
+ Path("coverage.json").replace(Path(os.path.join(args.output, "coverage.json")))
160
+ except Exception as e:
161
+ print(f"Failed to relocate coverage.json to {args.output} : {e}")
162
+ sys.exit(1)
163
+
164
+ if args.verbose:
165
+ print("OK")
166
+
167
+ # Generate report
168
+ if args.html:
169
+ if args.verbose:
170
+ print(f"Generating report {args.output}/html/index.html ... ", end="")
171
+
172
+ try:
173
+ subprocess.run(
174
+ ['avl-coverage-analysis', '--path', args.output, "--output", os.path.join(args.output, "html"), "--stats"],
175
+ capture_output=True, text=True, check=True
176
+ )
177
+ except (subprocess.CalledProcessError, FileNotFoundError) as e:
178
+ print(f"Could not run avl-coverage-analysis: {e}")
179
+ sys.exit(1)
180
+
181
+ if args.verbose:
182
+ print("OK")
183
+
184
+ if __name__ == "__main__":
185
+ main()
@@ -0,0 +1,32 @@
1
+ from ._trace import Trace
2
+
3
+ COVERPACKAGES = []
4
+ """
5
+ List of all registered CoverPackage classes
6
+ """
7
+
8
+ class CoverPackage:
9
+
10
+ def __init__(self, name : str) -> None:
11
+ """
12
+ Constructor
13
+
14
+ :param name: Coverage Package name
15
+ :type name: str
16
+ """
17
+ self.name = name
18
+
19
+ def sample(self, trace : Trace) -> None:
20
+ """
21
+ Sample a given trace element
22
+
23
+ :param trace: Trace element
24
+ :type trace: Trace
25
+ """
26
+ raise ValueError(f"Unimplemented sample function: {self.name}")
27
+
28
+
29
+ __all__ = [
30
+ "COVERPACKAGES",
31
+ "CoverPackage",
32
+ ]
@@ -0,0 +1,154 @@
1
+ import re
2
+ import subprocess
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ from elftools.elf.elffile import ELFFile
7
+
8
+ from ._instr import Instruction
9
+
10
+ INSTRUCTIONS: dict[str, Instruction]= {}
11
+ """Dictionary of all instructions based on contents of elf file"""
12
+
13
+ EXTENSIONS: dict[str, str] = {}
14
+ """Dictionary of all available extensions and their version based on properties of elf file"""
15
+
16
+
17
+ def _parse_arch_string_(arch_string : str) -> dict[str, str]:
18
+ """
19
+ Parse RISC-V architecture string like 'rv64i2p1_m2p0_a2p1...'
20
+
21
+ :param arch_string: Architectural definition string extracted from elf
22
+ :type arch_string: str
23
+ :returns Dictionary of extensions and versions
24
+ :rtype : dict[str, str]
25
+ """
26
+
27
+ extensions = {}
28
+
29
+ if not arch_string.startswith('rv'):
30
+ return extensions
31
+
32
+ # Extract base (rv32/rv64)
33
+ if arch_string.startswith('rv64'):
34
+ base = 'RV64'
35
+ rest = arch_string[4:]
36
+ elif arch_string.startswith('rv32'):
37
+ base = 'RV32'
38
+ rest = arch_string[4:]
39
+ else:
40
+ return extensions
41
+
42
+ extensions['base'] = base
43
+
44
+ # Split by underscore
45
+ parts = rest.split('_')
46
+
47
+ # Parse each extension with version
48
+ # Format: extension_name + version (e.g., i2p1, m2p0, zicsr2p0)
49
+ for part in parts:
50
+ if not part:
51
+ continue
52
+
53
+ # Match pattern: letters followed by optional version (digit+p+digit)
54
+ match = re.match(r'^([a-z]+)(\d+p\d+)?$', part, re.IGNORECASE)
55
+ if match:
56
+ ext_name = match.group(1).upper()
57
+ version = match.group(2) if match.group(2) else None
58
+
59
+ # Handle special case: 'g' expands to imafd + zicsr + zifencei
60
+ if ext_name == 'G':
61
+ extensions['I'] = version
62
+ extensions['M'] = version
63
+ extensions['A'] = version
64
+ extensions['F'] = version
65
+ extensions['D'] = version
66
+ extensions['Zicsr'] = version
67
+ extensions['Zifencei'] = version
68
+ else:
69
+ extensions[ext_name] = version
70
+
71
+ return extensions
72
+
73
+ def _parse_riscv_attribes_(data : str) -> dict[str, str]:
74
+ """
75
+ Quick parser specifically for your data format
76
+
77
+ :param data: Attributes data extracted from elf file
78
+ :type data: str
79
+ :returns Dictionary of extensions and versions
80
+ :rtype : dict[str, str]
81
+ """
82
+
83
+ # Find the architecture string (starts with 'rv')
84
+ arch_start = data.find(b'rv')
85
+ if arch_start == -1:
86
+ return None
87
+
88
+ # Find the null terminator after the arch string
89
+ arch_end = data.find(b'\x00', arch_start)
90
+ if arch_end == -1:
91
+ arch_end = len(data)
92
+
93
+ arch_string = data[arch_start:arch_end].decode('ascii')
94
+
95
+ return _parse_arch_string_(arch_string)
96
+
97
+ def parse_elf(elfpath : Path) -> None:
98
+ """
99
+ Parse given elf file.
100
+ Extract class and instructions
101
+
102
+ :param elfpath: path to elf file
103
+ :type elfpath: path
104
+ """
105
+
106
+ with open(elfpath, "rb") as f:
107
+ elf = ELFFile(f)
108
+
109
+ # Check if it's RISC-V
110
+ if elf.header['e_machine'] != 'EM_RISCV':
111
+ raise ValueError (f"Not a RISC-V binary: {elf.header['e_machine']}")
112
+
113
+ # Extract Extensions
114
+ attrs_section = elf.get_section_by_name('.riscv.attributes')
115
+
116
+ if not attrs_section:
117
+ raise ValueError("No .riscv.attributes section found")
118
+ else:
119
+ EXTENSIONS.update(_parse_riscv_attribes_(attrs_section.data()))
120
+
121
+ # Discover mode
122
+ if EXTENSIONS["base"] not in ["RV64", "RV32"]:
123
+ raise ValueError(f"Unknown base architecture {EXTENSIONS['base']}")
124
+
125
+ # Run objdump to extract instructions
126
+ try:
127
+ result = subprocess.run(
128
+ ['riscv64-unknown-elf-objdump', '-d', elfpath],
129
+ capture_output=True, text=True, check=True
130
+ )
131
+
132
+ for line in result.stdout.split('\n'):
133
+ # Parse: " 2000000342: 0d0075d7 vsetvli a1,zero,e32,m1,ta,ma"
134
+ match = re.match(r'\s*([0-9a-f]+):\s+([0-9a-f]+)\s+(.+)', line)
135
+ if match:
136
+ addr = int(match.group(1), 16)
137
+ bytes_hex = int(match.group(2), 16)
138
+
139
+ INSTRUCTIONS[addr] = Instruction(addr, bytes_hex)
140
+
141
+ except (subprocess.CalledProcessError, FileNotFoundError) as e:
142
+ print(f"Could not run objdump: {e}")
143
+ sys.exit(1)
144
+
145
+ # Link instructions to prev / next
146
+ for instr in INSTRUCTIONS.values():
147
+ n = INSTRUCTIONS.get(instr.pc + instr.size, None)
148
+ instr.link(n)
149
+
150
+ __all__ = [
151
+ "INSTRUCTIONS",
152
+ "EXTENSIONS",
153
+ "parse_elf",
154
+ ]
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+
3
+ from ._isa import Encoding
4
+
5
+
6
+ class Instruction:
7
+ def __init__(self, pc : int, encoding_bytes : int) -> None:
8
+ """
9
+ Constructor
10
+
11
+ :param pc: Program counter
12
+ :type pc: int
13
+ :param encoding_bytes: Instruction encoding
14
+ :type encoding_bytes: int
15
+ """
16
+ self.pc = pc
17
+ self.encoding_bytes = encoding_bytes
18
+ self.encoding = None
19
+ self.operands = {}
20
+ self.prev = None
21
+ self.next = None
22
+
23
+ self.decode()
24
+
25
+ def __str__(self) -> str:
26
+ """
27
+ Return a string representation of the Instruction.
28
+
29
+ :return: String representation of the Instruction.
30
+ :rtype: str
31
+ """
32
+
33
+ s = f"{self.pc:016x} : "
34
+ s += f"{self.prev.encoding.mnemonic}" if self.prev is not None else "None"
35
+ s += " -> "
36
+ s += f"\033[31m{self.encoding.mnemonic}\033[0m"
37
+ s += " -> "
38
+ s += f"{self.next.encoding.mnemonic}" if self.next is not None else "None"
39
+
40
+ return s
41
+
42
+ def link(self, next : Instruction) -> None:
43
+ """
44
+ Create 2 way link to next instruction (if you can)
45
+
46
+ Next / Prev refers to location in program (i.e. pc +/- one instruction)
47
+ not location in trace
48
+
49
+ :param next: Next instruction
50
+ :type next: Instruction
51
+ """
52
+ if next is not None:
53
+ self.next = next
54
+ self.next.prev = self
55
+
56
+
57
+ def decode(self) -> None:
58
+ """
59
+ Decode instruction to get mnemonic and operands
60
+ """
61
+
62
+ # Determine size (compressed or standard)
63
+ if (self.encoding_bytes & 0b11) != 0b11:
64
+ self.size = 2
65
+ instr_code = self.encoding_bytes & 0xFFFF
66
+ else:
67
+ self.size = 4
68
+ instr_code = self.encoding_bytes & 0xFFFFFFFF
69
+
70
+ self.encoding = Encoding.get_encoding(instr_code)
71
+
72
+ # Extract Operand Values
73
+ for k in self.encoding.operands.keys():
74
+ self.operands[k] = Encoding.get_operand(instr_code, k)
75
+
76
+ __all__ = [
77
+ "Instruction"
78
+ ]