aboutsummaryrefslogtreecommitdiff
path: root/rv64cpu
diff options
context:
space:
mode:
Diffstat (limited to 'rv64cpu')
-rwxr-xr-xrv64cpu/scripts/run_compliance.py375
-rw-r--r--rv64cpu/tb/CMakeLists.txt4
-rw-r--r--rv64cpu/tb/compliance/CMakeLists.txt41
-rw-r--r--rv64cpu/tb/compliance/compliance_runner.cc408
4 files changed, 756 insertions, 72 deletions
diff --git a/rv64cpu/scripts/run_compliance.py b/rv64cpu/scripts/run_compliance.py
index 92456ec..6932a15 100755
--- a/rv64cpu/scripts/run_compliance.py
+++ b/rv64cpu/scripts/run_compliance.py
@@ -1,94 +1,299 @@
#!/usr/bin/env python3
-"""
-run_compliance.py — riscv-arch-test 自动化运行脚本
+"""riscv-arch-test compliance orchestration.
-Usage:
- python3 scripts/run_compliance.py --suite rv64i
- python3 scripts/run_compliance.py --suite rv64im --build-dir build
- python3 scripts/run_compliance.py --list
+// Richard CPU
+// Copyright (C) 2026 Richard Qin
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
-Requirements:
- - Verilator simulation binary built (tb_soc or compliance_runner)
- - riscv64-unknown-elf-gcc toolchain in PATH
- - sw/riscv-arch-test submodule initialized
+This script does three things:
+1. Generates compliance ELFs through riscv-arch-test.
+2. Runs the local compliance runner binary for each generated ELF.
+3. Optionally compares produced signatures against discovered references.
"""
import argparse
+import os
+import shutil
import subprocess
import sys
-import os
from pathlib import Path
+from typing import Dict, List, Optional
PROJECT_ROOT = Path(__file__).resolve().parent.parent
ARCH_TEST_DIR = PROJECT_ROOT / "sw" / "riscv-arch-test"
BUILD_DIR = PROJECT_ROOT / "build"
SUITES = {
- "rv64i": "riscv-test-suite/rv64i_m/I",
- "rv64im": "riscv-test-suite/rv64i_m/M",
- "rv64ia": "riscv-test-suite/rv64i_m/A",
- "rv64if": "riscv-test-suite/rv64i_m/F",
- "rv64id": "riscv-test-suite/rv64i_m/D",
- "rv64ic": "riscv-test-suite/rv64i_m/C",
- "rv64priv": "riscv-test-suite/rv64i_m/privilege",
+ "rv64i": "I",
+ "rv64im": "I,M",
+ "rv64ia": "I,A",
+ "rv64if": "I,F",
+ "rv64id": "I,D",
+ "rv64ic": "I,C",
+ "rv64priv": "S,Sm",
+ "rv64g": "I,M,A,F,D,C",
+ "rv64full": "I,M,A,F,D,C,S,Sm",
}
def list_suites():
- print("Available test suites:")
- for name, path in SUITES.items():
- exists = "✓" if (ARCH_TEST_DIR / path).exists() else "✗ (not found)"
- print(f" {name:12s} → {path} {exists}")
+ print("Available suites and ACT extension filters:")
+ for name, extensions in SUITES.items():
+ print(f" {name:12s} -> EXTENSIONS={extensions}")
+
+
+def run_cmd(cmd: List[str], cwd: Optional[Path] = None, verbose: bool = False) -> subprocess.CompletedProcess:
+ if verbose:
+ print("+", " ".join(cmd))
+ return subprocess.run(cmd, cwd=cwd, text=True, capture_output=not verbose, check=False)
+
+
+def parse_config_name(config_path: Path) -> str:
+ with config_path.open("r", encoding="utf-8") as f:
+ for raw_line in f:
+ line = raw_line.strip()
+ if line.startswith("name:"):
+ return line.split(":", 1)[1].strip()
+ raise RuntimeError(f"Missing 'name' in config: {config_path}")
+
+
+def discover_elfs(work_dir: Path, config_name: str) -> List[Path]:
+ elf_root = work_dir / config_name / "elfs"
+ if not elf_root.exists():
+ return []
+ return sorted(elf_root.rglob("*.elf"))
+
+
+def find_reference_signature(elf_path: Path, work_dir: Path) -> Optional[Path]:
+ candidates = [
+ elf_path.with_suffix(".signature"),
+ elf_path.with_suffix(".reference_output"),
+ elf_path.with_suffix(".ref"),
+ elf_path.parent / "signatures" / f"{elf_path.stem}.signature",
+ ]
+ for candidate in candidates:
+ if candidate.exists():
+ return candidate
+
+ # ACT4 places reference signatures under work/<config>/build or work/common/*/build.
+ # Prefer raw .sig files for byte-for-byte comparison with DUT output.
+ sig_matches = sorted(work_dir.rglob(f"{elf_path.stem}.sig"))
+ if sig_matches:
+ return sig_matches[0]
+
+ # Fallback when only processed result files are available.
+ result_matches = sorted(work_dir.rglob(f"{elf_path.stem}.results"))
+ if result_matches:
+ return result_matches[0]
+
+ return None
+
+
+def compare_text_files(a: Path, b: Path) -> bool:
+ return a.read_text(encoding="utf-8", errors="ignore") == b.read_text(encoding="utf-8", errors="ignore")
+
+
+def ensure_submodule_ready(arch_test_dir: Path):
+ if not arch_test_dir.exists():
+ raise RuntimeError(f"Missing arch-test directory: {arch_test_dir}")
+ if not (arch_test_dir / "Makefile").exists():
+ raise RuntimeError(
+ f"Invalid arch-test checkout (Makefile missing): {arch_test_dir}. "
+ "Run: git submodule update --init --recursive"
+ )
+
+
+def ensure_binary(sim_bin: Path, build_dir: Path, verbose: bool):
+ if sim_bin.exists():
+ return sim_bin
+ print(f"Simulation binary not found: {sim_bin}")
+ print("Attempting to build compliance_runner target...")
+ result = run_cmd(["cmake", "--build", str(build_dir), "--target", "compliance_runner"], verbose=verbose)
+ if result.returncode != 0:
+ if not verbose:
+ print(result.stdout)
+ print(result.stderr)
+ raise RuntimeError("Failed to build compliance_runner")
+
+ if sim_bin.exists():
+ return sim_bin
+
+ matches = [p for p in build_dir.rglob(sim_bin.name) if p.is_file() and os.access(p, os.X_OK)]
+ if len(matches) == 1:
+ print(f"Using discovered simulation binary: {matches[0]}")
+ return matches[0]
+ if len(matches) > 1:
+ matches.sort(key=lambda p: len(str(p)))
+ print(f"Using discovered simulation binary: {matches[0]}")
+ return matches[0]
+ raise RuntimeError(f"Build completed but binary still missing: {sim_bin}")
-def run_suite(suite_name: str, build_dir: Path, sim_binary: str, verbose: bool):
+
+def generate_elfs(
+ arch_test_dir: Path,
+ config_file: Path,
+ work_dir: Path,
+ extensions: str,
+ jobs: int,
+ verbose: bool,
+):
+ cmd = [
+ "make",
+ "elfs",
+ f"CONFIG_FILES={config_file}",
+ f"WORKDIR={work_dir}",
+ f"EXTENSIONS={extensions}",
+ "EXCLUDE_EXTENSIONS=",
+ f"JOBS={jobs}",
+ "FAST=True",
+ ]
+ result = run_cmd(cmd, cwd=arch_test_dir, verbose=verbose)
+ if result.returncode != 0:
+ merged_output = ""
+ if result.stdout:
+ merged_output += result.stdout + "\n"
+ if result.stderr:
+ merged_output += result.stderr
+
+ missing_tooling_hints = [
+ "uv command not found",
+ "covergroupgen: Permission denied",
+ "command not found",
+ ]
+ if any(hint.lower() in merged_output.lower() for hint in missing_tooling_hints):
+ raise RuntimeError(
+ "Failed to generate ELFs because ACT tooling is unavailable in this environment. "
+ "Install uv and project python deps in sw/riscv-arch-test, or run with --skip-generate "
+ "to reuse an existing work directory with prebuilt ELFs."
+ )
+
+ if not verbose:
+ print(result.stdout)
+ print(result.stderr)
+ raise RuntimeError("Failed to generate compliance ELFs via riscv-arch-test")
+
+
+def run_suite(
+ suite_name: str,
+ build_dir: Path,
+ sim_binary: str,
+ config_file: Path,
+ work_dir: Path,
+ max_cycles: int,
+ max_tests: int,
+ jobs: int,
+ require_reference: bool,
+ keep_work: bool,
+ skip_generate: bool,
+ verbose: bool,
+):
if suite_name not in SUITES:
print(f"Unknown suite: {suite_name}")
print(f"Available: {', '.join(SUITES.keys())}")
- sys.exit(1)
+ return False
- suite_path = ARCH_TEST_DIR / SUITES[suite_name]
- if not suite_path.exists():
- print(f"Suite path not found: {suite_path}")
- print("Did you initialize the submodule?")
- print(" git submodule update --init --recursive")
- sys.exit(1)
+ ensure_submodule_ready(ARCH_TEST_DIR)
- sim_bin = build_dir / sim_binary
- if not sim_bin.exists():
- print(f"Simulation binary not found: {sim_bin}")
- print("Build it first: cmake --build build --target compliance_runner")
- sys.exit(1)
+ if not config_file.exists():
+ raise RuntimeError(f"Config file not found: {config_file}")
- test_files = sorted(suite_path.glob("src/*.S"))
- if not test_files:
- print(f"No test sources found in {suite_path / 'src'}")
- sys.exit(1)
+ config_name = parse_config_name(config_file)
+ sim_bin = ensure_binary(build_dir / sim_binary, build_dir, verbose)
+
+ if not skip_generate:
+ if work_dir.exists() and not keep_work:
+ shutil.rmtree(work_dir)
+ work_dir.mkdir(parents=True, exist_ok=True)
+
+ extensions = SUITES[suite_name]
+ if skip_generate:
+ print(f"Skipping ELF generation for suite={suite_name}, extensions={extensions}")
+ else:
+ print(f"Generating ELFs for suite={suite_name}, extensions={extensions}")
+ generate_elfs(ARCH_TEST_DIR, config_file, work_dir, extensions, jobs, verbose)
- print(f"\n{'='*60}")
- print(f"Running {suite_name} compliance tests ({len(test_files)} tests)")
- print(f"{'='*60}\n")
+ elf_files = discover_elfs(work_dir, config_name)
+ if max_tests > 0:
+ elf_files = elf_files[:max_tests]
+
+ if not elf_files:
+ print("No generated ELF files found.")
+ return False
+
+ signature_root = build_dir / "compliance_signatures" / suite_name
+ signature_root.mkdir(parents=True, exist_ok=True)
+
+ print(f"Running compliance runner on {len(elf_files)} ELF files")
passed = 0
failed = 0
- errors = []
+ no_reference = 0
- for test_file in test_files:
- test_name = test_file.stem
- if verbose:
- print(f" [{test_name}] ", end="", flush=True)
+ for idx, elf in enumerate(elf_files, start=1):
+ rel = elf.relative_to(work_dir / config_name / "elfs")
+ sig_path = signature_root / rel.with_suffix(".signature")
+ sig_path.parent.mkdir(parents=True, exist_ok=True)
- # TODO: Implement actual test compilation and simulation
- # This is a skeleton — fill in with your specific flow:
- # 1. Compile test_file with riscv64-unknown-elf-gcc
- # 2. Run the sim binary with the compiled ELF
- # 3. Compare signature output against reference
if verbose:
- print("⏭ SKIP (runner not yet implemented)")
+ print(f"[{idx}/{len(elf_files)}] {rel}")
+
+ run_result = run_cmd(
+ [
+ str(sim_bin),
+ "--elf",
+ str(elf),
+ "--max-cycles",
+ str(max_cycles),
+ "--signature",
+ str(sig_path),
+ ],
+ verbose=verbose,
+ )
+ if not sig_path.exists():
+ failed += 1
+ print(f"FAIL {rel} (signature not generated)")
+ continue
+
+ if run_result.returncode not in (0, 3):
+ failed += 1
+ if not verbose:
+ print(f"FAIL {rel} (runner exit={run_result.returncode})")
+ continue
- print(f"\n{'='*60}")
- print(f"Results: {passed} passed, {failed} failed, {len(test_files) - passed - failed} skipped")
- print(f"{'='*60}\n")
+ ref_sig = find_reference_signature(elf, work_dir)
+ if ref_sig is None:
+ no_reference += 1
+ if require_reference:
+ failed += 1
+ print(f"FAIL {rel} (no reference signature found)")
+ else:
+ passed += 1
+ continue
+
+ if compare_text_files(sig_path, ref_sig):
+ passed += 1
+ else:
+ failed += 1
+ print(f"FAIL {rel} (signature mismatch against {ref_sig})")
+
+ print("=" * 60)
+ print(f"Suite: {suite_name}")
+ print(f"Total: {len(elf_files)} Passed: {passed} Failed: {failed} NoRef: {no_reference}")
+ print(f"Signatures: {signature_root}")
+ print("=" * 60)
return failed == 0
@@ -98,6 +303,36 @@ def main():
parser.add_argument("--suite", type=str, help="Test suite to run")
parser.add_argument("--build-dir", type=str, default="build", help="Build directory")
parser.add_argument("--sim", type=str, default="compliance_runner", help="Sim binary name")
+ parser.add_argument(
+ "--config",
+ type=str,
+ default="config/spike/spike-rv64-max/test_config.yaml",
+ help="Config file path (relative to sw/riscv-arch-test or absolute)",
+ )
+ parser.add_argument(
+ "--work-dir",
+ type=str,
+ default="build/compliance_work",
+ help="Working directory for generated ELFs",
+ )
+ parser.add_argument("--max-cycles", type=int, default=2000, help="Cycles per test for compliance runner")
+ parser.add_argument("--max-tests", type=int, default=0, help="Limit number of ELFs to run (0 = all)")
+ parser.add_argument("--jobs", type=int, default=0, help="Parallel jobs for ACT test generation")
+ parser.add_argument(
+ "--require-reference",
+ action="store_true",
+ help="Fail tests when reference signature is missing",
+ )
+ parser.add_argument(
+ "--keep-work",
+ action="store_true",
+ help="Keep existing work directory instead of cleaning it first",
+ )
+ parser.add_argument(
+ "--skip-generate",
+ action="store_true",
+ help="Skip ACT generation and reuse existing ELFs under --work-dir",
+ )
parser.add_argument("--list", action="store_true", help="List available suites")
parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
@@ -111,8 +346,32 @@ def main():
parser.print_help()
sys.exit(1)
- build_dir = Path(args.build_dir)
- success = run_suite(args.suite, build_dir, args.sim, args.verbose)
+ build_dir = Path(args.build_dir).resolve()
+ work_dir = Path(args.work_dir).resolve()
+
+ config_file = Path(args.config)
+ if not config_file.is_absolute():
+ config_file = ARCH_TEST_DIR / config_file
+
+ try:
+ success = run_suite(
+ suite_name=args.suite,
+ build_dir=build_dir,
+ sim_binary=args.sim,
+ config_file=config_file,
+ work_dir=work_dir,
+ max_cycles=args.max_cycles,
+ max_tests=args.max_tests,
+ jobs=args.jobs,
+ require_reference=args.require_reference,
+ keep_work=args.keep_work,
+ skip_generate=args.skip_generate,
+ verbose=args.verbose,
+ )
+ except RuntimeError as e:
+ print(f"ERROR: {e}")
+ success = False
+
sys.exit(0 if success else 1)
diff --git a/rv64cpu/tb/CMakeLists.txt b/rv64cpu/tb/CMakeLists.txt
index 361a6e7..ebcd9dc 100644
--- a/rv64cpu/tb/CMakeLists.txt
+++ b/rv64cpu/tb/CMakeLists.txt
@@ -233,5 +233,5 @@ add_test(NAME tb_core_mie_masking COMMAND $<TARGET_FILE:tb_core> --case mie_mask
# ═══════════════════════════════════════════════════════════════════════════════════
# Compliance Tests
# ═══════════════════════════════════════════════════════════════════════════════════
-#
-# add_subdirectory(compliance)
+
+add_subdirectory(compliance)
diff --git a/rv64cpu/tb/compliance/CMakeLists.txt b/rv64cpu/tb/compliance/CMakeLists.txt
index b832f01..0139f54 100644
--- a/rv64cpu/tb/compliance/CMakeLists.txt
+++ b/rv64cpu/tb/compliance/CMakeLists.txt
@@ -4,18 +4,35 @@
#
# ─────────────────────────────────────────────────────────────────────────────────
-# The compliance runner needs the SoC sim binary
-# Uncomment when both the SoC and compliance_runner are implemented:
-#
-# add_verilator_target(
-# NAME compliance_runner
-# TOP_MODULE richard_soc
-# SOURCES ${ALL_RTL_SOURCES}
-# INCLUDE_DIRS ${RTL_COMMON_INCLUDES}
-# TB_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/compliance_runner.cpp
-# EXTRA_FLAGS --threads 2
-# )
-#
+# Minimal compliance runner target (core-only for bootstrap stage).
+# NOTE: This uses richard_core until SoC top and memory/peripheral harness are ready.
+add_verilator_target(
+ NAME compliance_runner
+ TOP_MODULE richard_core
+ SOURCES ${RTL_CORE_DIR}/include/rv64_pkg.sv
+ ${RTL_CORE_DIR}/alu.sv
+ ${RTL_CORE_DIR}/branch_unit.sv
+ ${RTL_CORE_DIR}/decoder.sv
+ ${RTL_CORE_DIR}/regfile.sv
+ ${RTL_CORE_DIR}/if_stage.sv
+ ${RTL_CORE_DIR}/if_id_reg.sv
+ ${RTL_CORE_DIR}/id_stage.sv
+ ${RTL_CORE_DIR}/id_ex_reg.sv
+ ${RTL_CORE_DIR}/ex_stage.sv
+ ${RTL_CORE_DIR}/ex_mem_reg.sv
+ ${RTL_CORE_DIR}/mem_stage.sv
+ ${RTL_CORE_DIR}/mem_wb_reg.sv
+ ${RTL_CORE_DIR}/wb_stage.sv
+ ${RTL_CORE_DIR}/hazard_unit.sv
+ ${RTL_CORE_DIR}/include/csr_pkg.sv
+ ${RTL_CORE_DIR}/csr_unit.sv
+ ${RTL_CORE_DIR}/trap_ctrl.sv
+ ${RTL_CORE_DIR}/richard_core.sv
+ INCLUDE_DIRS ${RTL_COMMON_INCLUDES}
+ TB_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/compliance_runner.cc
+ EXTRA_FLAGS -Wno-PINCONNECTEMPTY
+)
+
# ─── Register individual compliance tests via CTest ──────────────────────────────
# This is done dynamically by the compliance_runner binary and run_compliance.py.
# You can also manually register tests like:
diff --git a/rv64cpu/tb/compliance/compliance_runner.cc b/rv64cpu/tb/compliance/compliance_runner.cc
new file mode 100644
index 0000000..5cc4709
--- /dev/null
+++ b/rv64cpu/tb/compliance/compliance_runner.cc
@@ -0,0 +1,408 @@
+// Richard CPU
+// Copyright (C) 2026 Richard Qin
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+//
+// File path: tb/compliance/compliance_runner.cc
+// Minimal compliance runner bootstrap.
+// This runner intentionally keeps behavior simple so the toolchain flow can be
+// integrated first, then replaced with full ELF/signature handling.
+
+#include <cinttypes>
+#include <cstdint>
+#include <cstring>
+#include <fstream>
+#include <iomanip>
+#include <iostream>
+#include <map>
+#include <string>
+#include <vector>
+#include <verilated.h>
+
+#include "Vrichard_core.h"
+
+namespace {
+
+struct Options {
+ uint64_t max_cycles = 2000;
+ std::string elf_path;
+ std::string signature_path;
+ bool show_help = false;
+};
+
+struct Symbols {
+ uint64_t begin_signature = 0;
+ uint64_t end_signature = 0;
+ uint64_t tohost = 0;
+};
+
+class ByteMemory {
+public:
+ void write_byte(uint64_t addr, uint8_t value) {
+ mem_[addr] = value;
+ }
+
+ uint8_t read_byte(uint64_t addr) const {
+ auto it = mem_.find(addr);
+ if (it == mem_.end()) {
+ return 0;
+ }
+ return it->second;
+ }
+
+ uint32_t read_u32(uint64_t addr) const {
+ uint32_t value = 0;
+ for (int i = 0; i < 4; ++i) {
+ value |= static_cast<uint32_t>(read_byte(addr + i)) << (8 * i);
+ }
+ return value;
+ }
+
+ uint64_t read_u64(uint64_t addr) const {
+ uint64_t value = 0;
+ for (int i = 0; i < 8; ++i) {
+ value |= static_cast<uint64_t>(read_byte(addr + i)) << (8 * i);
+ }
+ return value;
+ }
+
+ void write_store(uint64_t addr, uint64_t wdata, uint8_t size_code) {
+ uint64_t bytes = 0;
+ switch (size_code) {
+ case 0b000:
+ case 0b100:
+ bytes = 1;
+ break;
+ case 0b001:
+ case 0b101:
+ bytes = 2;
+ break;
+ case 0b010:
+ case 0b110:
+ bytes = 4;
+ break;
+ case 0b011:
+ default:
+ bytes = 8;
+ break;
+ }
+
+ const uint64_t offset = addr & 0x7ULL;
+ const uint64_t aligned = addr & ~0x7ULL;
+ for (uint64_t i = 0; i < bytes; ++i) {
+ const uint64_t lane = offset + i;
+ const uint8_t byte_value = static_cast<uint8_t>((wdata >> (lane * 8)) & 0xFFULL);
+ write_byte(aligned + lane, byte_value);
+ }
+ }
+
+private:
+ std::map<uint64_t, uint8_t> mem_;
+};
+
+#pragma pack(push, 1)
+struct Elf64Ehdr {
+ unsigned char e_ident[16];
+ uint16_t e_type;
+ uint16_t e_machine;
+ uint32_t e_version;
+ uint64_t e_entry;
+ uint64_t e_phoff;
+ uint64_t e_shoff;
+ uint32_t e_flags;
+ uint16_t e_ehsize;
+ uint16_t e_phentsize;
+ uint16_t e_phnum;
+ uint16_t e_shentsize;
+ uint16_t e_shnum;
+ uint16_t e_shstrndx;
+};
+
+struct Elf64Phdr {
+ uint32_t p_type;
+ uint32_t p_flags;
+ uint64_t p_offset;
+ uint64_t p_vaddr;
+ uint64_t p_paddr;
+ uint64_t p_filesz;
+ uint64_t p_memsz;
+ uint64_t p_align;
+};
+
+struct Elf64Shdr {
+ uint32_t sh_name;
+ uint32_t sh_type;
+ uint64_t sh_flags;
+ uint64_t sh_addr;
+ uint64_t sh_offset;
+ uint64_t sh_size;
+ uint32_t sh_link;
+ uint32_t sh_info;
+ uint64_t sh_addralign;
+ uint64_t sh_entsize;
+};
+
+struct Elf64Sym {
+ uint32_t st_name;
+ unsigned char st_info;
+ unsigned char st_other;
+ uint16_t st_shndx;
+ uint64_t st_value;
+ uint64_t st_size;
+};
+#pragma pack(pop)
+
+constexpr uint32_t PT_LOAD = 1;
+constexpr uint32_t SHT_SYMTAB = 2;
+
+Options parse_args(int argc, char** argv) {
+ Options opts;
+ for (int i = 1; i < argc; ++i) {
+ const std::string arg = argv[i];
+ if (arg == "--help" || arg == "-h") {
+ opts.show_help = true;
+ continue;
+ }
+ if (arg == "--max-cycles" && (i + 1) < argc) {
+ opts.max_cycles = static_cast<uint64_t>(std::stoull(argv[++i]));
+ continue;
+ }
+ if (arg == "--elf" && (i + 1) < argc) {
+ opts.elf_path = argv[++i];
+ continue;
+ }
+ if (arg == "--signature" && (i + 1) < argc) {
+ opts.signature_path = argv[++i];
+ continue;
+ }
+ }
+ return opts;
+}
+
+void print_help() {
+ std::cout
+ << "compliance_runner (bootstrap)\n"
+ << "Usage:\n"
+ << " compliance_runner --elf PATH [--max-cycles N] [--signature PATH]\n";
+}
+
+bool load_elf64(const std::string& path, ByteMemory* memory, Symbols* symbols) {
+ std::ifstream in(path, std::ios::binary);
+ if (!in) {
+ std::cerr << "Failed to open ELF: " << path << "\n";
+ return false;
+ }
+
+ in.seekg(0, std::ios::end);
+ const std::streamsize size = in.tellg();
+ in.seekg(0, std::ios::beg);
+ if (size <= 0) {
+ std::cerr << "Empty ELF file: " << path << "\n";
+ return false;
+ }
+
+ std::vector<uint8_t> data(static_cast<size_t>(size));
+ in.read(reinterpret_cast<char*>(data.data()), size);
+ if (!in) {
+ std::cerr << "Failed to read ELF contents: " << path << "\n";
+ return false;
+ }
+
+ if (data.size() < sizeof(Elf64Ehdr)) {
+ std::cerr << "ELF too small: " << path << "\n";
+ return false;
+ }
+
+ const auto* ehdr = reinterpret_cast<const Elf64Ehdr*>(data.data());
+ if (!(ehdr->e_ident[0] == 0x7F && ehdr->e_ident[1] == 'E' && ehdr->e_ident[2] == 'L' && ehdr->e_ident[3] == 'F')) {
+ std::cerr << "Invalid ELF magic: " << path << "\n";
+ return false;
+ }
+ if (ehdr->e_ident[4] != 2 || ehdr->e_ident[5] != 1) {
+ std::cerr << "Only ELF64 little-endian is supported: " << path << "\n";
+ return false;
+ }
+
+ const uint64_t ph_end = ehdr->e_phoff + static_cast<uint64_t>(ehdr->e_phnum) * ehdr->e_phentsize;
+ if (ph_end > data.size()) {
+ std::cerr << "Program header table out of range\n";
+ return false;
+ }
+
+ for (uint16_t i = 0; i < ehdr->e_phnum; ++i) {
+ const uint64_t off = ehdr->e_phoff + static_cast<uint64_t>(i) * ehdr->e_phentsize;
+ const auto* phdr = reinterpret_cast<const Elf64Phdr*>(data.data() + off);
+ if (phdr->p_type != PT_LOAD) {
+ continue;
+ }
+ if (phdr->p_offset + phdr->p_filesz > data.size()) {
+ std::cerr << "LOAD segment out of range\n";
+ return false;
+ }
+
+ const uint64_t base = (phdr->p_paddr != 0) ? phdr->p_paddr : phdr->p_vaddr;
+ for (uint64_t j = 0; j < phdr->p_filesz; ++j) {
+ memory->write_byte(base + j, data[phdr->p_offset + j]);
+ }
+ for (uint64_t j = phdr->p_filesz; j < phdr->p_memsz; ++j) {
+ memory->write_byte(base + j, 0);
+ }
+ }
+
+ const uint64_t sh_end = ehdr->e_shoff + static_cast<uint64_t>(ehdr->e_shnum) * ehdr->e_shentsize;
+ if (ehdr->e_shoff == 0 || ehdr->e_shnum == 0 || sh_end > data.size()) {
+ return true;
+ }
+
+ for (uint16_t i = 0; i < ehdr->e_shnum; ++i) {
+ const uint64_t off = ehdr->e_shoff + static_cast<uint64_t>(i) * ehdr->e_shentsize;
+ const auto* shdr = reinterpret_cast<const Elf64Shdr*>(data.data() + off);
+ if (shdr->sh_type != SHT_SYMTAB || shdr->sh_entsize == 0) {
+ continue;
+ }
+ if (shdr->sh_offset + shdr->sh_size > data.size() || shdr->sh_link >= ehdr->e_shnum) {
+ continue;
+ }
+
+ const uint64_t str_off = ehdr->e_shoff + static_cast<uint64_t>(shdr->sh_link) * ehdr->e_shentsize;
+ const auto* str_shdr = reinterpret_cast<const Elf64Shdr*>(data.data() + str_off);
+ if (str_shdr->sh_offset + str_shdr->sh_size > data.size()) {
+ continue;
+ }
+
+ const char* strtab = reinterpret_cast<const char*>(data.data() + str_shdr->sh_offset);
+ const uint64_t sym_count = shdr->sh_size / shdr->sh_entsize;
+ for (uint64_t s = 0; s < sym_count; ++s) {
+ const uint64_t sym_off = shdr->sh_offset + s * shdr->sh_entsize;
+ const auto* sym = reinterpret_cast<const Elf64Sym*>(data.data() + sym_off);
+ if (sym->st_name >= str_shdr->sh_size) {
+ continue;
+ }
+ const std::string name(strtab + sym->st_name);
+ if (name == "begin_signature") {
+ symbols->begin_signature = sym->st_value;
+ } else if (name == "end_signature") {
+ symbols->end_signature = sym->st_value;
+ } else if (name == "tohost") {
+ symbols->tohost = sym->st_value;
+ }
+ }
+ }
+
+ return true;
+}
+
+void tick(Vrichard_core* top, ByteMemory* memory) {
+ top->imem_rdata = memory->read_u32(top->imem_addr);
+ top->dmem_rdata = top->dmem_read_en ? memory->read_u64(top->dmem_addr & ~0x7ULL) : 0;
+ top->ext_timer_int = 0;
+ top->ext_software_int = 0;
+ top->ext_external_int = 0;
+
+ top->clk = 0;
+ top->eval();
+ top->clk = 1;
+ top->eval();
+
+ if (top->dmem_write_en) {
+ memory->write_store(top->dmem_addr, top->dmem_wdata, top->dmem_size);
+ }
+}
+
+void dump_signature(const ByteMemory& memory, const Symbols& symbols, const std::string& signature_path) {
+ if (signature_path.empty()) {
+ return;
+ }
+
+ std::ofstream sig(signature_path);
+ if (!sig) {
+ std::cerr << "Failed to open signature file for writing: " << signature_path << "\n";
+ return;
+ }
+
+ if (symbols.begin_signature == 0 || symbols.end_signature <= symbols.begin_signature) {
+ sig << "00000000\n";
+ return;
+ }
+
+ const uint64_t bytes = symbols.end_signature - symbols.begin_signature;
+ for (uint64_t off = 0; off + 8 <= bytes; off += 8) {
+ const uint64_t value = memory.read_u64(symbols.begin_signature + off);
+ sig << std::hex << std::setfill('0') << std::setw(16) << value << "\n";
+ }
+}
+
+} // namespace
+
+int main(int argc, char** argv) {
+ Verilated::commandArgs(argc, argv);
+ const Options opts = parse_args(argc, argv);
+
+ if (opts.show_help) {
+ print_help();
+ return 0;
+ }
+ if (opts.elf_path.empty()) {
+ // Keep compatibility with CTest smoke invocation that runs the binary
+ // without arguments to validate toolchain/linking only.
+ std::cout << "compliance_runner: no --elf provided, smoke mode pass\n";
+ return 0;
+ }
+
+ auto* top = new Vrichard_core;
+ ByteMemory memory;
+ Symbols symbols;
+
+ if (!load_elf64(opts.elf_path, &memory, &symbols)) {
+ delete top;
+ return 2;
+ }
+
+ top->rst = 1;
+ for (int i = 0; i < 5; ++i) {
+ tick(top, &memory);
+ }
+ top->rst = 0;
+
+ bool saw_tohost = false;
+ bool test_pass = false;
+ for (uint64_t i = 0; i < opts.max_cycles; ++i) {
+ tick(top, &memory);
+ if (top->dmem_write_en && symbols.tohost != 0) {
+ const uint64_t addr = top->dmem_addr;
+ const bool overlaps_tohost = (addr <= symbols.tohost + 3ULL) && (addr + 7ULL >= symbols.tohost);
+ if (overlaps_tohost) {
+ const uint32_t status = static_cast<uint32_t>(memory.read_u64(symbols.tohost) & 0xFFFFFFFFULL);
+ if (status == 1U) {
+ saw_tohost = true;
+ test_pass = true;
+ break;
+ }
+ if (status == 3U) {
+ saw_tohost = true;
+ test_pass = false;
+ break;
+ }
+ }
+ }
+ }
+
+ dump_signature(memory, symbols, opts.signature_path);
+
+ delete top;
+ if (!saw_tohost) {
+ return 3;
+ }
+ return test_pass ? 0 : 1;
+}