diff --git a/.github/workflows/regress.yml b/.github/workflows/regress.yml index 1bd50a4096..fc11643df1 100644 --- a/.github/workflows/regress.yml +++ b/.github/workflows/regress.yml @@ -191,3 +191,31 @@ jobs: run: ./bin/build_container - name: Generate extension PDF run: ./do gen:profile[MockProfileRelease] + regress-gen-go: + runs-on: ubuntu-latest + env: + SINGULARITY: 1 + steps: + - name: Clone Github Repo Action + uses: actions/checkout@v4 + - name: Setup apptainer + uses: eWaterCycle/setup-apptainer@v2.0.0 + - name: Get container from cache + id: cache-sif + uses: actions/cache@v4 + with: + path: .singularity/image.sif + key: ${{ hashFiles('container.def', 'bin/.container-tag') }} + - name: Get gems and node files from cache + id: cache-bundle-npm + uses: actions/cache@v4 + with: + path: | + .home/.gems + node_modules + key: ${{ hashFiles('Gemfile.lock') }}-${{ hashFiles('package-lock.json') }} + - if: ${{ steps.cache-sif.outputs.cache-hit != 'true' }} + name: Build container + run: ./bin/build_container + - name: Generate Go code + run: ./do gen:go diff --git a/Rakefile b/Rakefile index 0d930c346c..ccda666883 100755 --- a/Rakefile +++ b/Rakefile @@ -435,6 +435,8 @@ namespace :test do Rake::Task["#{$root}/gen/certificate_doc/pdf/MockCertificateModel.pdf"].invoke Rake::Task["#{$root}/gen/profile_doc/pdf/MockProfileRelease.pdf"].invoke + Rake::Task["gen:go"].invoke + puts puts "Regression test PASSED" end diff --git a/backends/generators/Go/go_generator.py b/backends/generators/Go/go_generator.py new file mode 100644 index 0000000000..b02c8337ee --- /dev/null +++ b/backends/generators/Go/go_generator.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +import os +import sys +import argparse +import logging + +# Add parent directory to path to find generator.py +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from generator import load_instructions, load_csrs, parse_match, signed + +logging.basicConfig(level=logging.INFO, format="%(levelname)s:: %(message)s") + + +def make_go(instr_dict, csrs, output_file="inst.go"): + """ + Generate a Go source file with the instruction encodings followed by + a map of CSR names and addresses. + """ + args = " ".join(sys.argv) + prelude = f"// Code generated by {args}; DO NOT EDIT.\n" + prelude += """package riscv + +import "cmd/internal/obj" + +type inst struct { + opcode uint32 + funct3 uint32 + rs1 uint32 + rs2 uint32 + csr int64 + funct7 uint32 +} + +func encode(a obj.As) *inst { + switch a { +""" + + instr_str = "" + # Process instructions in sorted order (by name) + for name, info in sorted(instr_dict.items(), key=lambda x: x[0].upper()): + match_str = info["match"] + enc_match = parse_match(match_str) + opcode = (enc_match >> 0) & ((1 << 7) - 1) + funct3 = (enc_match >> 12) & ((1 << 3) - 1) + rs1 = (enc_match >> 15) & ((1 << 5) - 1) + rs2 = (enc_match >> 20) & ((1 << 5) - 1) + csr_val = (enc_match >> 20) & ((1 << 12) - 1) + funct7 = (enc_match >> 25) & ((1 << 7) - 1) + # Create the instruction case name. For example, "bclri" becomes "ABCLRI" + instr_case = f"A{name.upper().replace('.','')}" + instr_str += f""" case {instr_case}: + return &inst{{ {hex(opcode)}, {hex(funct3)}, {hex(rs1)}, {hex(rs2)}, {signed(csr_val,12)}, {hex(funct7)} }} +""" + instructions_end = """ } + return nil +} +""" + + # Build the CSR map block - now matching the second script's format + csrs_map_str = "var csrs = map[uint16]string {\n" + # Convert the dictionary to a list of tuples and sort by address + csr_items = [(int(addr), name.upper()) for addr, name in csrs.items()] + for addr, name in sorted(csr_items, key=lambda x: x[0]): + csrs_map_str += f'{hex(addr)} : "{name}",\n' + csrs_map_str += "}\n" + + go_code = prelude + instr_str + instructions_end + "\n" + csrs_map_str + + with open(output_file, "w", encoding="utf-8") as f: + f.write(go_code) + logging.info( + f"Generated {output_file} with {len(instr_dict)} instructions and {len(csrs)} CSRs" + ) + + +def parse_args(): + parser = argparse.ArgumentParser( + description="Generate Go code for RISC-V instructions and CSRs filtered by extensions" + ) + parser.add_argument( + "--inst-dir", + default="../../../arch/inst/", + help="Directory containing instruction YAML files", + ) + parser.add_argument( + "--csr-dir", + default="../../../arch/csr/", + help="Directory containing CSR YAML files", + ) + parser.add_argument("--output", default="inst.go", help="Output Go file name") + parser.add_argument( + "--extensions", + default="A,D,F,I,M,Q,Zba,Zbb,Zbs,S,System,V,Zicsr,Smpmp,Sm,H,U,Zicntr,Zihpm,Smhpm", + help="Comma-separated list of enabled extensions. Default includes standard extensions.", + ) + parser.add_argument( + "--arch", + default="RV64", + choices=["RV32", "RV64", "BOTH"], + help="Target architecture (RV32, RV64, or BOTH). Default is RV64.", + ) + parser.add_argument( + "--verbose", "-v", action="store_true", help="Enable verbose logging" + ) + parser.add_argument( + "--include-all", + "-a", + action="store_true", + help="Include all instructions, ignoring extension filtering", + ) + return parser.parse_args() + + +def main(): + args = parse_args() + + if args.verbose: + logging.getLogger().setLevel(logging.DEBUG) + + # Check if we should include all instructions + include_all = args.include_all or not args.extensions + + # Parse enabled extensions + if include_all: + enabled_extensions = [] + logging.info( + "Including all instructions and CSRs (extension filtering disabled)" + ) + else: + # Get extensions from the command line + enabled_extensions = [ + ext.strip() for ext in args.extensions.split(",") if ext.strip() + ] + logging.info(f"Enabled extensions: {', '.join(enabled_extensions)}") + + # Log target architecture + logging.info(f"Target architecture: {args.arch}") + + # Check if the directories exist + if not os.path.isdir(args.inst_dir): + logging.error(f"Instruction directory not found: {args.inst_dir}") + sys.exit(1) + if not os.path.isdir(args.csr_dir): + logging.warning(f"CSR directory not found: {args.csr_dir}") + + # Load instructions filtered by extensions or all instructions + instr_dict = load_instructions( + args.inst_dir, enabled_extensions, include_all, args.arch + ) + if not instr_dict: + logging.error("No instructions found or all were filtered out.") + logging.error( + "Try using --verbose to see more details about the filtering process." + ) + sys.exit(1) + logging.info(f"Loaded {len(instr_dict)} instructions") + + # Load CSRs filtered by extensions or all CSRs + csrs = load_csrs(args.csr_dir, enabled_extensions, include_all, args.arch) + if not csrs: + logging.warning("No CSRs found or all were filtered out.") + else: + logging.info(f"Loaded {len(csrs)} CSRs") + + # Generate the Go code + make_go(instr_dict, csrs, args.output) + + +if __name__ == "__main__": + main() diff --git a/backends/generators/generator.py b/backends/generators/generator.py new file mode 100644 index 0000000000..2ff7d0ea69 --- /dev/null +++ b/backends/generators/generator.py @@ -0,0 +1,422 @@ +#!/usr/bin/env python3 +import os +import yaml +import logging +import pprint + +pp = pprint.PrettyPrinter(indent=2) +logging.basicConfig(level=logging.INFO, format="%(levelname)s:: %(message)s") + + +def check_requirement(req, exts): + if isinstance(req, str): + return req in exts + elif isinstance(req, dict) and "name" in req: + # If it has a name field, just match the extension name and ignore version + return req["name"] in exts + return False + + +def parse_extension_requirements(extensions_spec): + """ + Parse the extension requirements from the definedBy field. + Extensions can be specified as a string or a dictionary with allOf/oneOf/anyOf fields. + Returns a function that checks if the given extensions satisfy the requirements. + """ + if extensions_spec is None: + # If definedBy is None, we should never match + logging.error(f"Missing 'definedBy' field") + return lambda exts: False + + if isinstance(extensions_spec, str): + # Simple case: a single extension + extension = extensions_spec + if extension.startswith("RV"): + # Extract the actual extension part from RV prefix + if extension.startswith("RV32") or extension.startswith("RV64"): + ext_parts = extension[4:] + else: + ext_parts = extension[2:] + # Check if any part matches enabled extensions + return lambda enabled_exts: any( + ext_part in enabled_exts for ext_part in ext_parts + ) + return lambda exts: extension in exts + + # Handle complex cases with allOf/oneOf/anyOf + if "allOf" in extensions_spec: + required = extensions_spec["allOf"] + if isinstance(required, str): + required = [required] + + # Process each requirement, which could be a string or a dict with name/version + return lambda exts: all(check_requirement(req, exts) for req in required) + + if "oneOf" in extensions_spec: + alternatives = extensions_spec["oneOf"] + if isinstance(alternatives, str): + alternatives = [alternatives] + + # Process each alternative, which could be a string or a dict with name/version + def check_alternative_one_of(alt, exts): + if isinstance(alt, str): + return alt in exts + elif isinstance(alt, dict) and "name" in alt: + return alt["name"] in exts + return False + + return lambda exts: any( + check_alternative_one_of(alt, exts) for alt in alternatives + ) + + # Handle anyOf case (most common in the error output) + if "anyOf" in extensions_spec: + alternatives = extensions_spec["anyOf"] + if isinstance(alternatives, str): + alternatives = [alternatives] + + # Process each alternative, which could be a string, dict with name/version, or nested allOf + def check_alternative(alt, exts): + if isinstance(alt, str): + return alt in exts + elif isinstance(alt, dict): + if "allOf" in alt: + reqs = alt["allOf"] + if isinstance(reqs, str): + reqs = [reqs] + return all(check_requirement(r, exts) for r in reqs) + elif "name" in alt: + return alt["name"] in exts + return False + + return lambda exts: any(check_alternative(alt, exts) for alt in alternatives) + + # Handle direct name/version specification + if "name" in extensions_spec and "version" in extensions_spec: + extension = extensions_spec["name"] + # We don't actually check the version, just the extension name + return lambda exts: extension in exts + + # Default case if we can't parse the requirements + logging.debug(f"Unrecognized extension specification format: {extensions_spec}") + # Let's be more permissive for now - we'll include instructions + # that have an unrecognized format rather than excluding them + return lambda exts: True + + +def load_instructions( + root_dir, enabled_extensions, include_all=False, target_arch="RV64" +): + """ + Recursively walk through root_dir, load YAML files that define an instruction, + filter by enabled extensions, and collect them into a dictionary keyed by the instruction name. + + If include_all is True, extension filtering is bypassed. + target_arch can be "RV32", "RV64", or "BOTH". + """ + instr_dict = {} + found_files = 0 + found_instructions = 0 + extension_filtered = 0 + encoding_filtered = 0 + + logging.info( + f"Searching for instruction files in {root_dir} for target architecture {target_arch}" + ) + + for dirpath, _, filenames in os.walk(root_dir): + for fname in filenames: + if not fname.endswith(".yaml"): + continue + found_files += 1 + path = os.path.join(dirpath, fname) + try: + with open(path, encoding="utf-8") as f: + data = yaml.safe_load(f) + except Exception as e: + logging.error(f"Error parsing {path}: {e}") + continue + + if data.get("kind") != "instruction": + continue + + found_instructions += 1 + name = data.get("name") + if not name: + logging.error(f"Missing 'name' field in {path}") + continue + + # If include_all is True, skip extension filtering + if not include_all: + # Check if this instruction is defined by an enabled extension + definedBy = data.get("definedBy") + if definedBy is None: + logging.error( + f"Missing 'definedBy' field in instruction {name} in {path}" + ) + extension_filtered += 1 + continue + + logging.debug(f"Instruction {name} definedBy: {definedBy}") + meets_extension_req = parse_extension_requirements(definedBy) + if not meets_extension_req(enabled_extensions): + msg = f"Skipping {name} because its extension is not enabled" + logging.debug(msg) + extension_filtered += 1 + continue + + # Check if this instruction is excluded by an enabled extension + excludedBy = data.get("excludedBy") + if excludedBy: + exclusion_check = parse_extension_requirements(excludedBy) + if exclusion_check(enabled_extensions): + msg = f"Skipping {name} because it's excluded by an enabled extension" + logging.debug(msg) + extension_filtered += 1 + continue + + encoding = data.get("encoding", {}) + if not encoding: + logging.error( + f"Missing 'encoding' field in instruction {name} in {path}" + ) + encoding_filtered += 1 + continue + + # Check if the instruction specifies a base architecture constraint + base = data.get("base") + if base is not None: + if (base == 32 and target_arch not in ["RV32", "BOTH"]) or ( + base == 64 and target_arch not in ["RV64", "BOTH"] + ): + msg = f"Skipping {name} because it requires base {base} which doesn't match target arch {target_arch}" + logging.debug(msg) + encoding_filtered += 1 + continue + + # Determine which encoding to use based on target architecture + if isinstance(encoding, dict): + if "RV64" in encoding and "RV32" in encoding: + # Instruction has both RV32 and RV64 encodings + if target_arch == "RV64": + encoding_to_use = encoding["RV64"] + instr_key = name + elif target_arch == "RV32": + encoding_to_use = encoding["RV32"] + instr_key = name + else: # BOTH + # For "BOTH", include both encodings with suitable naming + rv64_encoding = encoding["RV64"] + rv32_encoding = encoding["RV32"] + + # Process RV64 encoding + rv64_match = rv64_encoding.get("match") + if rv64_match: + instr_dict[name] = { + "match": rv64_match + } # RV64 gets the default name + + # Process RV32 encoding with a .rv32 suffix + rv32_match = rv32_encoding.get("match") + if rv32_match: + instr_dict[f"{name}.rv32"] = {"match": rv32_match} + + continue # Skip the rest of the loop as we've already added the encodings + elif "RV64" in encoding: + if target_arch in ["RV64", "BOTH"]: + encoding_to_use = encoding["RV64"] + instr_key = name + else: + msg = f"Skipping {name} because it has only RV64 encoding in {path}" + logging.debug(msg) + encoding_filtered += 1 + continue + elif "RV32" in encoding: + if target_arch in ["RV32", "BOTH"]: + encoding_to_use = encoding["RV32"] + instr_key = f"{name}.rv32" if target_arch == "BOTH" else name + else: + msg = f"Skipping {name} because it has only RV32 encoding in {path}" + logging.debug(msg) + encoding_filtered += 1 + continue + elif "match" in encoding: + # Generic encoding, no specific architecture + encoding_to_use = encoding + instr_key = name + else: + msg = f"Skipping {name} because its encoding in {path} has no recognized match field." + logging.warning(msg) + encoding_filtered += 1 + continue + else: + msg = f"Skipping {name} because its encoding in {path} is not a dictionary." + logging.warning(msg) + encoding_filtered += 1 + continue + + match_str = encoding_to_use.get("match") + if not match_str: + msg = f"Skipping {name} because 'match' field is missing in {path}" + logging.warning(msg) + encoding_filtered += 1 + continue + + instr_dict[instr_key] = {"match": match_str} + + if found_instructions > 0: + logging.info( + f"Found {found_instructions} instruction definitions in {found_files} files" + ) + if extension_filtered > 0: + logging.info(f"Filtered out {extension_filtered} instructions by extension") + if encoding_filtered > 0: + logging.info( + f"Filtered out {encoding_filtered} instructions due to encoding issues" + ) + logging.info(f"Added {len(instr_dict)} instruction encodings to the output") + else: + logging.warning(f"No instruction definitions found in {root_dir}") + + return instr_dict + + +def load_csrs(csr_root, enabled_extensions, include_all=False, target_arch="RV64"): + """ + Recursively walk through csr_root, load YAML files that define a CSR, + filter by enabled extensions, and collect them into a dictionary mapping + each address (as an integer) to the CSR name. + + If include_all is True, extension filtering is bypassed. + target_arch can be "RV32", "RV64", or "BOTH". + """ + csrs = {} + found_files = 0 + found_csrs = 0 + extension_filtered = 0 + arch_filtered = 0 + address_errors = 0 + + logging.info( + f"Searching for CSR files in {csr_root} for target architecture {target_arch}" + ) + + for dirpath, _, filenames in os.walk(csr_root): + for fname in filenames: + if not fname.endswith(".yaml"): + continue + found_files += 1 + path = os.path.join(dirpath, fname) + try: + with open(path, encoding="utf-8") as f: + data = yaml.safe_load(f) + except Exception as e: + logging.error(f"Error parsing CSR file {path}: {e}") + continue + + if data.get("kind") != "csr": + continue + + found_csrs += 1 + name = data.get("name") + if not name: + logging.error(f"Missing 'name' field in {path}") + continue + + address = data.get("address") + indirect_address = data.get("indirect_address") + + if not address and not indirect_address: + logging.error( + f"Missing 'address' or 'indirect_address' field in CSR {name} in {path}" + ) + address_errors += 1 + continue + + # Check if the CSR has a base constraint (32 or 64) + base = data.get("base") + if base: + if base == 32 and target_arch not in ["RV32", "BOTH"]: + logging.debug(f"Skipping CSR {name} because it requires RV32 base") + arch_filtered += 1 + continue + elif base == 64 and target_arch not in ["RV64", "BOTH"]: + logging.debug(f"Skipping CSR {name} because it requires RV64 base") + arch_filtered += 1 + continue + + # If include_all is True, skip extension filtering + if not include_all: + # Check if this CSR is defined by an enabled extension + definedBy = data.get("definedBy") + + # If definedBy is missing, log a warning but don't skip + # This is different from instructions where we're more strict + if definedBy is None: + logging.warning( + f"Missing 'definedBy' field in CSR {name} in {path}, including anyway" + ) + else: + logging.debug(f"CSR {name} definedBy: {definedBy}") + meets_extension_req = parse_extension_requirements(definedBy) + if not meets_extension_req(enabled_extensions): + msg = ( + f"Skipping CSR {name} because its extension is not enabled" + ) + logging.debug(msg) + extension_filtered += 1 + continue + + # If we're here, we've passed all checks + try: + # Use address if available, otherwise use indirect_address + addr_to_use = address if address is not None else indirect_address + if isinstance(addr_to_use, int): + addr_int = addr_to_use + else: + addr_int = int(addr_to_use, 0) + + # For BOTH architecture, add suffix to RV32-specific CSRs + if target_arch == "BOTH" and base == 32: + csrs[addr_int] = f"{name.upper()}.RV32" + else: + csrs[addr_int] = name.upper() + except Exception as e: + logging.error(f"Error parsing address {addr_to_use} in {path}: {e}") + address_errors += 1 + continue + + if found_csrs > 0: + logging.info(f"Found {found_csrs} CSR definitions in {found_files} files") + if extension_filtered > 0: + logging.info(f"Filtered out {extension_filtered} CSRs by extension") + if arch_filtered > 0: + logging.info( + f"Filtered out {arch_filtered} CSRs by architecture constraints" + ) + if address_errors > 0: + logging.info(f"Filtered out {address_errors} CSRs due to address issues") + logging.info(f"Added {len(csrs)} CSRs to the output") + else: + logging.warning(f"No CSR definitions found in {csr_root}") + + return csrs + + +def parse_match(match_str): + """ + Convert the bit pattern string to an integer. + Replace all '-' (variable bits) with '0' so that only constant bits are set. + """ + binary_str = "".join("0" if c == "-" else c for c in match_str) + return int(binary_str, 2) + + +# Returns signed interpretation of a value within a given width. +def signed(value: int, width: int) -> int: + return value if 0 <= value < (1 << (width - 1)) else value - (1 << width) + + +if __name__ == "__main__": + print("This module is not meant to be run directly.") + print("Please use go_generator.py instead.") diff --git a/backends/generators/tasks.rake b/backends/generators/tasks.rake new file mode 100644 index 0000000000..e158ea8917 --- /dev/null +++ b/backends/generators/tasks.rake @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +directory "#{$root}/gen/go" + +namespace :gen do + desc <<~DESC + Generate Go code from RISC-V instruction and CSR definitions + + Options: + * CONFIG - Configuration name (defaults to "_") + * OUTPUT_DIR - Output directory for generated Go code (defaults to "#{$root}/gen/go") + DESC + task :go => "#{$root}/gen/go" do + config_name = ENV["CONFIG"] || "_" + output_dir = ENV["OUTPUT_DIR"] || "#{$root}/gen/go/" + + # Ensure the output directory exists + FileUtils.mkdir_p output_dir + + # Make sure the architecture is resolved + Rake::Task["#{$root}/.stamps/resolve-#{config_name}.stamp"].invoke + + # Get the arch paths based on the config + inst_dir = $root / "arch" / "inst" + csr_dir = $root / "arch" / "csr" + + # If using a specific config other than the default, use the resolved arch + if config_name != "_" + inst_dir = $root / "gen" / "resolved_arch" / config_name / "inst" + csr_dir = $root / "gen" / "resolved_arch" / config_name / "csr" + end + + # Run the Go generator script using the same Python environment + # Note: The script uses --output not --output-dir + sh "#{$root}/.home/.venv/bin/python3 #{$root}/backends/generators/Go/go_generator.py --inst-dir=#{inst_dir} --csr-dir=#{csr_dir} --output=#{output_dir}inst.go" + end +end