aboutsummaryrefslogtreecommitdiff
path: root/backend/tools/MIRTool
diff options
context:
space:
mode:
Diffstat (limited to 'backend/tools/MIRTool')
-rw-r--r--backend/tools/MIRTool/CMakeLists.txt90
-rw-r--r--backend/tools/MIRTool/LLVMRunTestExecutor.py682
-rw-r--r--backend/tools/MIRTool/MIRToolMain.cpp1147
-rw-r--r--backend/tools/MIRTool/MIRToolOperationsTest.cmake275
-rw-r--r--backend/tools/MIRTool/MIRToolOutputTest.cmake119
-rw-r--r--backend/tools/MIRTool/MIRToolStageTest.cmake200
6 files changed, 2513 insertions, 0 deletions
diff --git a/backend/tools/MIRTool/CMakeLists.txt b/backend/tools/MIRTool/CMakeLists.txt
new file mode 100644
index 0000000..4f84df7
--- /dev/null
+++ b/backend/tools/MIRTool/CMakeLists.txt
@@ -0,0 +1,90 @@
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+add_executable(RCCBackendMIRTool MIRToolMain.cpp)
+rcc_backend_configure_target(RCCBackendMIRTool)
+target_link_libraries(
+ RCCBackendMIRTool
+ PRIVATE
+ RCCBackend::Tooling
+ RCCBackend::RISCV
+)
+
+set_target_properties(
+ RCCBackendMIRTool
+ PROPERTIES
+ OUTPUT_NAME rcc-backend-mir
+)
+
+install(
+ TARGETS RCCBackendMIRTool
+ EXPORT RCCBackendTargets
+ RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}"
+)
+
+find_program(RCCBackendMIRToolLLVMMC NAMES llvm-mc-20 llvm-mc)
+find_program(RCCBackendMIRToolLLVMReadELF NAMES llvm-readelf-20 llvm-readelf)
+find_program(RCCBackendMIRToolLLVMObjdump NAMES llvm-objdump-20 llvm-objdump)
+
+set(RCCBackendMIRToolInputDirectory "${RCC_BACKEND_SOURCE_DIR}/unittests/Target/Inputs")
+set(RCCBackendMIRToolTestDirectory "${CMAKE_CURRENT_BINARY_DIR}/tests")
+
+add_test(
+ NAME RCCBackend.MIRTool.StagesAndPasses
+ COMMAND
+ "${CMAKE_COMMAND}"
+ "-DTOOL=$<TARGET_FILE:RCCBackendMIRTool>"
+ "-DINPUT=${RCCBackendMIRToolInputDirectory}/RISCVGenericPipeline.mir"
+ "-DWORK_DIRECTORY=${RCCBackendMIRToolTestDirectory}/stages"
+ -P "${CMAKE_CURRENT_SOURCE_DIR}/MIRToolStageTest.cmake"
+)
+
+foreach(
+ InputName
+ IN ITEMS
+ RISCVGenericPipeline
+ RISCVWideConstantPipeline
+ RISCVDataPipeline
+ RISCVStackFramePipeline
+)
+ add_test(
+ NAME RCCBackend.MIRTool.Assembly.${InputName}
+ COMMAND
+ "${CMAKE_COMMAND}"
+ "-DTOOL=$<TARGET_FILE:RCCBackendMIRTool>"
+ "-DINPUT=${RCCBackendMIRToolInputDirectory}/${InputName}.mir"
+ "-DOUTPUT=${RCCBackendMIRToolTestDirectory}/${InputName}.s"
+ -DOUTPUT_KIND=asm
+ "-DLLVM_MC=${RCCBackendMIRToolLLVMMC}"
+ -P "${CMAKE_CURRENT_SOURCE_DIR}/MIRToolOutputTest.cmake"
+ )
+ add_test(
+ NAME RCCBackend.MIRTool.Object.${InputName}
+ COMMAND
+ "${CMAKE_COMMAND}"
+ "-DTOOL=$<TARGET_FILE:RCCBackendMIRTool>"
+ "-DINPUT=${RCCBackendMIRToolInputDirectory}/${InputName}.mir"
+ "-DOUTPUT=${RCCBackendMIRToolTestDirectory}/${InputName}.o"
+ -DOUTPUT_KIND=obj
+ "-DLLVM_READELF=${RCCBackendMIRToolLLVMReadELF}"
+ "-DLLVM_OBJDUMP=${RCCBackendMIRToolLLVMObjdump}"
+ -P "${CMAKE_CURRENT_SOURCE_DIR}/MIRToolOutputTest.cmake"
+ )
+endforeach()
+
+add_test(
+ NAME RCCBackend.MIRTool.Operations
+ COMMAND
+ "${CMAKE_COMMAND}"
+ "-DTOOL=$<TARGET_FILE:RCCBackendMIRTool>"
+ "-DINPUT=${RCCBackendMIRToolInputDirectory}/RISCVGenericPipeline.mir"
+ "-DWORK_DIRECTORY=${RCCBackendMIRToolTestDirectory}/operations"
+ -P "${CMAKE_CURRENT_SOURCE_DIR}/MIRToolOperationsTest.cmake"
+)
+
+set_tests_properties(
+ RCCBackend.MIRTool.StagesAndPasses
+ RCCBackend.MIRTool.Operations
+ PROPERTIES TIMEOUT 180
+)
+
+rcc_backend_register_format_files("${CMAKE_CURRENT_SOURCE_DIR}/MIRToolMain.cpp")
diff --git a/backend/tools/MIRTool/LLVMRunTestExecutor.py b/backend/tools/MIRTool/LLVMRunTestExecutor.py
new file mode 100644
index 0000000..38ffd3c
--- /dev/null
+++ b/backend/tools/MIRTool/LLVMRunTestExecutor.py
@@ -0,0 +1,682 @@
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import annotations
+
+import argparse
+import csv
+import dataclasses
+import json
+import os
+import re
+import shlex
+import subprocess
+import sys
+import tempfile
+from collections import Counter
+from concurrent.futures import ThreadPoolExecutor
+from pathlib import Path
+from typing import Iterable
+
+
+IN_SCOPE_CATEGORIES = {"RV64GC_EXACT", "RV64GC_SEMANTIC", "RV64GC_INPUT"}
+OUT_OF_SCOPE_CATEGORIES = {
+ "OUT_RV32": "rv32",
+ "OUT_RVV": "rvv",
+ "OUT_EXTENSION": "extension",
+}
+RV64GC_FEATURES = {"i", "m", "a", "f", "d", "c", "zicsr", "zifencei"}
+NON_ISA_FEATURES = {
+ "disable-postmisched-load-clustering",
+ "disable-postmisched-store-clustering",
+ "fusion-add-load",
+ "fusion-add-mem",
+ "fusion-addi-load",
+ "fusion-auipc-addi",
+ "fusion-auipc-load",
+ "fusion-ld-add",
+ "fusion-logic-imm-reg",
+ "fusion-logic-reg-imm",
+ "fusion-logic-reg-reg",
+ "fusion-lui-addi",
+ "fusion-lui-load",
+ "fusion-mul-add",
+ "fusion-shift-bit-extract",
+ "fusion-shifted-zextw",
+ "fusion-zexth",
+ "fusion-zextw",
+ "prefer-w-inst",
+ "reserve-x6",
+ "reserve-x7",
+ "save-restore",
+ "tagged-globals",
+ "unaligned-scalar-mem",
+ "use-postra-scheduler",
+}
+RUN_PATTERN = re.compile(r"^\s*#\s*RUN:\s?(.*)$")
+FILECHECK_PATTERN = re.compile(r"(?<![\w./-])FileCheck(?=\s|$)")
+LLC_PATTERN = re.compile(r"(?<![\w./-])%?llc(?=\s|$)")
+NOT_PATTERN = re.compile(r"(?<![\w./-])not(?=\s|$)")
+SUBSTITUTION_PATTERN = re.compile(r"%([A-Za-z][A-Za-z0-9_/.:+-]*)")
+
+
+@dataclasses.dataclass(frozen=True)
+class RunDirective:
+ line: int
+ command: str
+
+
+@dataclasses.dataclass(frozen=True)
+class TestDescription:
+ relative_path: str
+ path: Path
+ category: str
+ reason: str
+ runs: tuple[RunDirective, ...]
+
+
+@dataclasses.dataclass
+class GateResult:
+ status: str
+ diagnostic: str = ""
+
+
+@dataclasses.dataclass
+class RunResult:
+ line: int
+ command: str
+ scope: str
+ scope_reason: str
+ production: str = "not-applicable"
+ filecheck: str = "not-applicable"
+ diagnostic: str = ""
+
+
+@dataclasses.dataclass
+class FileResult:
+ path: str
+ category: str
+ reason: str
+ input_gate: GateResult
+ runs: list[RunResult]
+
+
+def parse_arguments() -> argparse.Namespace:
+ parser = argparse.ArgumentParser(
+ description="Execute unmodified LLVM RISC-V MIR RUN directives with RCC"
+ )
+ parser.add_argument("--tool", type=Path, required=True)
+ parser.add_argument("--corpus", type=Path, required=True)
+ parser.add_argument("--manifest", type=Path, required=True)
+ parser.add_argument("--filecheck", type=Path, required=True)
+ parser.add_argument("--not-tool", type=Path)
+ parser.add_argument("--llvm-bin", type=Path)
+ parser.add_argument("--report", type=Path)
+ parser.add_argument("--jobs", type=int, default=max(1, min(8, os.cpu_count() or 1)))
+ parser.add_argument("--timeout", type=float, default=60.0)
+ parser.add_argument("--validate-only", action="store_true")
+ parser.add_argument("--strict", action="store_true")
+ parser.add_argument("--max-diagnostics", type=int, default=20)
+ return parser.parse_args()
+
+
+def read_manifest(path: Path) -> dict[str, tuple[str, str]]:
+ lines = [line for line in path.read_text(encoding="utf-8").splitlines() if not line.startswith("#")]
+ reader = csv.DictReader(lines, delimiter="\t")
+ if reader.fieldnames != ["path", "category", "reason"]:
+ raise ValueError("classification manifest has an invalid header")
+ result: dict[str, tuple[str, str]] = {}
+ for row in reader:
+ relative_path = row["path"]
+ category = row["category"]
+ reason = row["reason"]
+ if not relative_path or category not in IN_SCOPE_CATEGORIES | set(OUT_OF_SCOPE_CATEGORIES):
+ raise ValueError(f"classification manifest has an invalid row for '{relative_path}'")
+ if relative_path in result:
+ raise ValueError(f"classification manifest repeats '{relative_path}'")
+ result[relative_path] = (category, reason)
+ return result
+
+
+def read_run_directives(path: Path) -> tuple[RunDirective, ...]:
+ result: list[RunDirective] = []
+ command = ""
+ start_line = 0
+ for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
+ match = RUN_PATTERN.match(line)
+ if match is None:
+ if command:
+ raise ValueError(f"{path}:{start_line}: unterminated RUN continuation")
+ continue
+ fragment = match.group(1).rstrip()
+ if not command:
+ start_line = line_number
+ continued = fragment.endswith("\\")
+ if continued:
+ fragment = fragment[:-1].rstrip()
+ command = f"{command} {fragment}".strip()
+ if not continued:
+ if not command:
+ raise ValueError(f"{path}:{start_line}: empty RUN directive")
+ result.append(RunDirective(start_line, command))
+ command = ""
+ if command:
+ raise ValueError(f"{path}:{start_line}: unterminated RUN continuation")
+ return tuple(result)
+
+
+def load_tests(corpus: Path, manifest_path: Path) -> list[TestDescription]:
+ manifest = read_manifest(manifest_path)
+ corpus_paths = {
+ path.relative_to(corpus).as_posix(): path for path in corpus.rglob("*.mir") if path.is_file()
+ }
+ missing = sorted(set(manifest) - set(corpus_paths))
+ unclassified = sorted(set(corpus_paths) - set(manifest))
+ if missing or unclassified:
+ details = []
+ if missing:
+ details.append(f"missing files: {', '.join(missing[:5])}")
+ if unclassified:
+ details.append(f"unclassified files: {', '.join(unclassified[:5])}")
+ raise ValueError("classification and corpus differ: " + "; ".join(details))
+ tests = []
+ for relative_path in sorted(corpus_paths):
+ category, reason = manifest[relative_path]
+ runs = read_run_directives(corpus_paths[relative_path])
+ if not runs:
+ raise ValueError(f"{corpus_paths[relative_path]}: no RUN directives")
+ tests.append(
+ TestDescription(relative_path, corpus_paths[relative_path], category, reason, runs)
+ )
+ return tests
+
+
+def option_values(tokens: list[str], names: Iterable[str]) -> list[str]:
+ result: list[str] = []
+ name_set = set(names)
+ index = 0
+ while index < len(tokens):
+ token = tokens[index]
+ if token in name_set:
+ if index + 1 < len(tokens):
+ result.append(tokens[index + 1])
+ index += 2
+ continue
+ for name in name_set:
+ prefix = name + "="
+ if token.startswith(prefix):
+ result.append(token[len(prefix) :])
+ break
+ index += 1
+ return result
+
+
+def command_scope(test: TestDescription, command: str) -> tuple[str, str, bool]:
+ if test.category in OUT_OF_SCOPE_CATEGORIES:
+ scope = OUT_OF_SCOPE_CATEGORIES[test.category]
+ return scope, f"file classification {test.category}: {test.reason}", True
+ try:
+ tokens = shlex.split(command)
+ except ValueError as error:
+ return "invalid", f"cannot tokenize RUN directive: {error}", True
+
+ triples = option_values(tokens, ("-mtriple", "--mtriple"))
+ for triple in triples:
+ if triple.startswith("riscv32"):
+ return "rv32", f"RUN selects {triple}", True
+ if not triple.startswith("riscv64"):
+ return "target", f"RUN selects unsupported triple {triple}", True
+
+ abis = option_values(tokens, ("-target-abi", "--target-abi"))
+ for abi in abis:
+ if abi.startswith("ilp32"):
+ return "rv32", f"RUN selects {abi}", True
+ if not abi.startswith("lp64"):
+ return "target", f"RUN selects unsupported ABI {abi}", True
+
+ cpus = option_values(tokens, ("-mcpu", "--mcpu"))
+ for cpu in cpus:
+ if cpu not in {"generic-rv64", "generic-rv64gc"}:
+ return "cpu", f"RUN selects unsupported CPU {cpu}", True
+
+ attributes = option_values(tokens, ("-mattr", "--mattr"))
+ for attribute_list in attributes:
+ for attribute in attribute_list.split(","):
+ feature = attribute.strip().lstrip("+-")
+ if not feature:
+ continue
+ if feature == "v" or feature.startswith(("zv", "xsfv", "xsfmm")):
+ return "rvv", f"RUN selects vector feature {feature}", True
+ if feature not in RV64GC_FEATURES | NON_ISA_FEATURES:
+ return "extension", f"RUN selects non-RV64GC feature {feature}", True
+
+ has_target_context = bool(triples or abis or cpus or attributes or LLC_PATTERN.search(command))
+ return "selected", "RV64GC-compatible RUN", has_target_context
+
+
+def shell_quote(path: Path | str) -> str:
+ return shlex.quote(str(path))
+
+
+def validate_substitutions(command: str, path: Path, line: int) -> None:
+ supported = {"s", "t"}
+ for substitution in SUBSTITUTION_PATTERN.findall(command):
+ if substitution in supported or re.fullmatch(r"t[0-9]+", substitution):
+ continue
+ raise ValueError(f"{path}:{line}: unsupported lit substitution %{substitution}")
+
+
+def substitute_command(
+ command: str,
+ source: Path,
+ temporary_prefix: Path,
+ tool: Path,
+ filecheck: Path,
+ not_tool: Path | None,
+) -> str:
+ result = LLC_PATTERN.sub(shell_quote(tool), command)
+ result = FILECHECK_PATTERN.sub(shell_quote(filecheck), result)
+ if not_tool is not None:
+ result = NOT_PATTERN.sub(shell_quote(not_tool), result)
+ result = re.sub(
+ r"%t([0-9]+)", lambda match: shell_quote(f"{temporary_prefix}{match.group(1)}"), result
+ )
+ result = result.replace("%t", shell_quote(temporary_prefix))
+ return result.replace("%s", shell_quote(source))
+
+
+def filecheck_pipe(command: str) -> int | None:
+ match = FILECHECK_PATTERN.search(command)
+ if match is None:
+ return None
+ filecheck_position = match.start()
+ quote = ""
+ escaped = False
+ pipe_positions: list[int] = []
+ for index, character in enumerate(command[:filecheck_position]):
+ if escaped:
+ escaped = False
+ continue
+ if character == "\\" and quote != "'":
+ escaped = True
+ continue
+ if quote:
+ if character == quote:
+ quote = ""
+ continue
+ if character in {"'", '"'}:
+ quote = character
+ continue
+ if character == "|" and (index + 1 >= len(command) or command[index + 1] != "|"):
+ pipe_positions.append(index)
+ return pipe_positions[-1] if pipe_positions else None
+
+
+def run_shell(
+ command: str,
+ cwd: Path,
+ environment: dict[str, str],
+ timeout: float,
+ standard_input: bytes | None = None,
+) -> subprocess.CompletedProcess[bytes]:
+ return subprocess.run(
+ ["/bin/bash", "-o", "pipefail", "-c", command],
+ cwd=cwd,
+ env=environment,
+ input=standard_input,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ timeout=timeout,
+ check=False,
+ )
+
+
+def diagnostic_for(process: subprocess.CompletedProcess[bytes], command: str) -> str:
+ output = process.stderr.decode("utf-8", errors="replace")
+ if not output:
+ output = process.stdout.decode("utf-8", errors="replace")
+ return f"command: {command}\n{output}"[:8000].rstrip()
+
+
+def execute_run(
+ directive: RunDirective,
+ scope: str,
+ scope_reason: str,
+ test: TestDescription,
+ temporary_prefix: Path,
+ arguments: argparse.Namespace,
+ environment: dict[str, str],
+) -> RunResult:
+ result = RunResult(directive.line, directive.command, scope, scope_reason)
+ if scope != "selected":
+ return result
+ pipe = filecheck_pipe(directive.command)
+ try:
+ if pipe is not None:
+ production_command = substitute_command(
+ directive.command[:pipe].strip(),
+ test.path,
+ temporary_prefix,
+ arguments.tool,
+ arguments.filecheck,
+ arguments.not_tool,
+ )
+ filecheck_command = substitute_command(
+ directive.command[pipe + 1 :].strip(),
+ test.path,
+ temporary_prefix,
+ arguments.tool,
+ arguments.filecheck,
+ arguments.not_tool,
+ )
+ production = run_shell(
+ production_command, test.path.parent, environment, arguments.timeout
+ )
+ if production.returncode != 0:
+ result.production = "failed"
+ result.filecheck = "skipped"
+ result.diagnostic = diagnostic_for(production, production_command)
+ return result
+ result.production = "passed"
+ check = run_shell(
+ filecheck_command,
+ test.path.parent,
+ environment,
+ arguments.timeout,
+ production.stdout,
+ )
+ if check.returncode == 0:
+ result.filecheck = "passed"
+ else:
+ result.filecheck = "failed"
+ result.diagnostic = diagnostic_for(check, filecheck_command)
+ return result
+
+ command = substitute_command(
+ directive.command,
+ test.path,
+ temporary_prefix,
+ arguments.tool,
+ arguments.filecheck,
+ arguments.not_tool,
+ )
+ if FILECHECK_PATTERN.search(directive.command):
+ check = run_shell(command, test.path.parent, environment, arguments.timeout)
+ result.filecheck = "passed" if check.returncode == 0 else "failed"
+ if check.returncode != 0:
+ result.diagnostic = diagnostic_for(check, command)
+ return result
+
+ production = run_shell(command, test.path.parent, environment, arguments.timeout)
+ result.production = "passed" if production.returncode == 0 else "failed"
+ if production.returncode != 0:
+ result.diagnostic = diagnostic_for(production, command)
+ except subprocess.TimeoutExpired:
+ result.production = "failed"
+ result.diagnostic = f"command timed out after {arguments.timeout:g} seconds: {command}"
+ except OSError as error:
+ result.production = "failed"
+ result.diagnostic = f"cannot execute command: {error}: {command}"
+ return result
+
+
+def execute_input_gate(
+ test: TestDescription, arguments: argparse.Namespace
+) -> GateResult:
+ if test.category not in IN_SCOPE_CATEGORIES:
+ return GateResult("not-applicable")
+ command = [
+ str(arguments.tool),
+ "--verify",
+ "-mtriple=riscv64",
+ "-mattr=+m,+a,+f,+d,+c",
+ "-verify-machineinstrs",
+ str(test.path),
+ ]
+ try:
+ process = subprocess.run(
+ command,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ timeout=arguments.timeout,
+ check=False,
+ )
+ except subprocess.TimeoutExpired:
+ return GateResult("failed", f"input gate timed out after {arguments.timeout:g} seconds")
+ except OSError as error:
+ return GateResult("failed", f"cannot execute input gate: {error}")
+ if process.returncode == 0:
+ return GateResult("passed")
+ return GateResult("failed", diagnostic_for(process, shlex.join(command)))
+
+
+def execute_file(
+ test: TestDescription,
+ arguments: argparse.Namespace,
+ temporary_root: Path,
+ environment: dict[str, str],
+) -> FileResult:
+ gate = GateResult("not-run") if arguments.validate_only else execute_input_gate(test, arguments)
+ results: list[RunResult] = []
+ inherited_scope: tuple[str, str] | None = None
+ temporary_directory = temporary_root / re.sub(r"[^A-Za-z0-9_.-]", "_", test.relative_path)
+ temporary_directory.mkdir(parents=True, exist_ok=True)
+ temporary_prefix = temporary_directory / "tmp"
+ for directive in test.runs:
+ validate_substitutions(directive.command, test.path, directive.line)
+ scope, reason, has_target_context = command_scope(test, directive.command)
+ if not has_target_context and inherited_scope is not None:
+ scope, reason = inherited_scope
+ elif has_target_context:
+ inherited_scope = (scope, reason)
+ if arguments.validate_only:
+ results.append(RunResult(directive.line, directive.command, scope, reason))
+ else:
+ results.append(
+ execute_run(
+ directive,
+ scope,
+ reason,
+ test,
+ temporary_prefix,
+ arguments,
+ environment,
+ )
+ )
+ return FileResult(test.relative_path, test.category, test.reason, gate, results)
+
+
+def status_counts(values: Iterable[str]) -> dict[str, int]:
+ return dict(sorted(Counter(values).items()))
+
+
+def exact_file_status(result: FileResult) -> str:
+ if result.input_gate.status == "not-run":
+ return "not-run"
+ if result.input_gate.status == "failed":
+ return "failed"
+ selected = [run for run in result.runs if run.scope == "selected"]
+ if not selected:
+ return "no-compatible-run"
+ for run in selected:
+ if run.production == "failed" or run.filecheck in {"failed", "skipped"}:
+ return "failed"
+ if run.production == "not-applicable" and run.filecheck != "passed":
+ return "failed"
+ return "passed"
+
+
+def semantic_file_status(result: FileResult) -> str:
+ if result.input_gate.status == "not-run":
+ return "not-run"
+ if result.input_gate.status == "failed":
+ return "failed"
+ selected = [run for run in result.runs if run.scope == "selected"]
+ if not selected:
+ return "no-compatible-run"
+ return "failed" if any(run.production == "failed" for run in selected) else "passed"
+
+
+def build_summary(results: list[FileResult]) -> dict[str, object]:
+ runs = [run for result in results for run in result.runs]
+ exact = [result for result in results if result.category == "RV64GC_EXACT"]
+ semantic = [result for result in results if result.category == "RV64GC_SEMANTIC"]
+ input_only = [result for result in results if result.category == "RV64GC_INPUT"]
+ return {
+ "files": len(results),
+ "runs": len(runs),
+ "categories": status_counts(result.category for result in results),
+ "input_gate": status_counts(result.input_gate.status for result in results),
+ "scope": status_counts(run.scope for run in runs),
+ "production": status_counts(run.production for run in runs),
+ "filecheck": status_counts(run.filecheck for run in runs),
+ "exact_files": status_counts(exact_file_status(result) for result in exact),
+ "semantic_files": status_counts(semantic_file_status(result) for result in semantic),
+ "input_files": status_counts(result.input_gate.status for result in input_only),
+ }
+
+
+def count(summary: dict[str, object], section: str, status: str) -> int:
+ values = summary[section]
+ assert isinstance(values, dict)
+ value = values.get(status, 0)
+ assert isinstance(value, int)
+ return value
+
+
+def print_summary(summary: dict[str, object]) -> None:
+ selected = count(summary, "scope", "selected")
+ out_of_scope = int(summary["runs"]) - selected
+ print(
+ f"LLVM RISC-V MIR corpus: files={summary['files']} runs={summary['runs']} "
+ f"selected={selected} out-of-scope={out_of_scope}"
+ )
+ print(
+ "input gate: "
+ f"passed={count(summary, 'input_gate', 'passed')} "
+ f"failed={count(summary, 'input_gate', 'failed')} "
+ f"not-run={count(summary, 'input_gate', 'not-run')}"
+ )
+ print(
+ "production pass: "
+ f"passed={count(summary, 'production', 'passed')} "
+ f"failed={count(summary, 'production', 'failed')}"
+ )
+ print(
+ "FileCheck: "
+ f"passed={count(summary, 'filecheck', 'passed')} "
+ f"failed={count(summary, 'filecheck', 'failed')} "
+ f"skipped={count(summary, 'filecheck', 'skipped')}"
+ )
+ print(
+ "RV64GC_EXACT files: "
+ f"passed={count(summary, 'exact_files', 'passed')} "
+ f"failed={count(summary, 'exact_files', 'failed')} "
+ f"no-compatible-run={count(summary, 'exact_files', 'no-compatible-run')} "
+ f"not-run={count(summary, 'exact_files', 'not-run')}"
+ )
+ print("out-of-scope RUNs: " + json.dumps(summary["scope"], sort_keys=True))
+
+
+def print_diagnostics(results: list[FileResult], maximum: int) -> None:
+ emitted = 0
+ for result in results:
+ if result.input_gate.status == "failed" and emitted < maximum:
+ print(f"FAIL input {result.path}:\n{result.input_gate.diagnostic}", file=sys.stderr)
+ emitted += 1
+ for run in result.runs:
+ if not run.diagnostic or emitted >= maximum:
+ continue
+ print(f"FAIL {result.path}:{run.line}:\n{run.diagnostic}", file=sys.stderr)
+ emitted += 1
+ total = sum(
+ (1 if result.input_gate.status == "failed" else 0)
+ + sum(1 for run in result.runs if run.diagnostic)
+ for result in results
+ )
+ if total > emitted:
+ print(f"{total - emitted} additional failures omitted", file=sys.stderr)
+
+
+def strict_failure(summary: dict[str, object]) -> bool:
+ return (
+ count(summary, "input_gate", "failed") != 0
+ or count(summary, "exact_files", "failed") != 0
+ or count(summary, "exact_files", "no-compatible-run") != 0
+ or count(summary, "semantic_files", "failed") != 0
+ or count(summary, "semantic_files", "no-compatible-run") != 0
+ )
+
+
+def validate_paths(arguments: argparse.Namespace) -> None:
+ for description, path in (
+ ("RCC MIR tool", arguments.tool),
+ ("MIR corpus", arguments.corpus),
+ ("classification manifest", arguments.manifest),
+ ("FileCheck", arguments.filecheck),
+ ):
+ if not path.exists():
+ raise ValueError(f"{description} does not exist: {path}")
+ if arguments.not_tool is not None and not arguments.not_tool.exists():
+ raise ValueError(f"not tool does not exist: {arguments.not_tool}")
+ if arguments.jobs <= 0:
+ raise ValueError("--jobs must be positive")
+ if arguments.timeout <= 0:
+ raise ValueError("--timeout must be positive")
+
+
+def main() -> int:
+ arguments = parse_arguments()
+ try:
+ validate_paths(arguments)
+ arguments.tool = arguments.tool.resolve()
+ arguments.corpus = arguments.corpus.resolve()
+ arguments.manifest = arguments.manifest.resolve()
+ arguments.filecheck = arguments.filecheck.resolve()
+ if arguments.not_tool is not None:
+ arguments.not_tool = arguments.not_tool.resolve()
+ if arguments.llvm_bin is not None:
+ arguments.llvm_bin = arguments.llvm_bin.resolve()
+ tests = load_tests(arguments.corpus, arguments.manifest)
+ environment = os.environ.copy()
+ path_entries = []
+ if arguments.llvm_bin is not None:
+ path_entries.append(str(arguments.llvm_bin.resolve()))
+ path_entries.append(str(arguments.filecheck.resolve().parent))
+ if arguments.not_tool is not None:
+ path_entries.append(str(arguments.not_tool.resolve().parent))
+ environment["PATH"] = os.pathsep.join(path_entries + [environment.get("PATH", "")])
+ with tempfile.TemporaryDirectory(prefix="rcc-llvm-mir-") as temporary_directory:
+ temporary_root = Path(temporary_directory)
+ with ThreadPoolExecutor(max_workers=arguments.jobs) as executor:
+ results = list(
+ executor.map(
+ lambda test: execute_file(test, arguments, temporary_root, environment), tests
+ )
+ )
+ results.sort(key=lambda result: result.path)
+ summary = build_summary(results)
+ print_summary(summary)
+ if not arguments.validate_only:
+ print_diagnostics(results, arguments.max_diagnostics)
+ if arguments.report is not None:
+ arguments.report.parent.mkdir(parents=True, exist_ok=True)
+ arguments.report.write_text(
+ json.dumps(
+ {
+ "summary": summary,
+ "files": [dataclasses.asdict(result) for result in results],
+ },
+ indent=2,
+ sort_keys=True,
+ )
+ + "\n",
+ encoding="utf-8",
+ )
+ return 1 if arguments.strict and strict_failure(summary) else 0
+ except (OSError, ValueError) as error:
+ print(f"LLVMRunTestExecutor.py: {error}", file=sys.stderr)
+ return 2
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/backend/tools/MIRTool/MIRToolMain.cpp b/backend/tools/MIRTool/MIRToolMain.cpp
new file mode 100644
index 0000000..ae341f8
--- /dev/null
+++ b/backend/tools/MIRTool/MIRToolMain.cpp
@@ -0,0 +1,1147 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "rcc/CodeGen/CommandFlags.h"
+#include "rcc/CodeGen/MachineFunction.h"
+#include "rcc/MC/TargetRegistry.h"
+#include "rcc/Support/Error.h"
+#include "rcc/Support/TargetSelect.h"
+#include "rcc/Target/TargetMachine.h"
+#include "rcc/Tooling/ArtifactStore.h"
+#include "rcc/Tooling/BackendDriver.h"
+#include "rcc/Tooling/BackendInvocation.h"
+#include "rcc/Tooling/MIRFuzzer.h"
+#include "rcc/Tooling/MIRReducer.h"
+
+#include <algorithm>
+#include <charconv>
+#include <cstddef>
+#include <cstdint>
+#include <filesystem>
+#include <fstream>
+#include <iostream>
+#include <iterator>
+#include <mutex>
+#include <optional>
+#include <ranges>
+#include <span>
+#include <string>
+#include <string_view>
+#include <system_error>
+#include <utility>
+#include <vector>
+
+namespace {
+
+using rcc::Error;
+using rcc::ErrorCode;
+using rcc::Expected;
+using rcc::tooling::ArtifactKind;
+using rcc::tooling::BackendInvocation;
+using rcc::tooling::BackendRunResult;
+using rcc::tooling::CanonicalMIR;
+
+enum class Operation : std::uint8_t {
+ Run,
+ Verify,
+ Reproduce,
+ ReduceValidity,
+ ReduceFailure,
+ Fuzz,
+ ListPasses,
+};
+
+enum class CachePolicy : std::uint8_t {
+ Off,
+ Read,
+ Write,
+ ReadWrite,
+};
+
+struct CommandLine final {
+ Operation Action = Operation::Run;
+ CachePolicy Cache = CachePolicy::Off;
+ std::string InputPath;
+ std::string OutputPath = "-";
+ std::filesystem::path ReproducerPath;
+ std::filesystem::path CachePath;
+ ArtifactKind Output = ArtifactKind::FinalMIR;
+ BackendInvocation Invocation;
+ rcc::tooling::PipelineControls Controls;
+ rcc::tooling::MIRFuzzOptions FuzzOptions;
+ bool OutputWasSpecified = false;
+ bool DiscardOutput = false;
+ bool PrintActions = false;
+ bool ShowHelp = false;
+};
+
+void printUsage(std::ostream &Output) {
+ Output << "usage: rcc-backend-mir [operation] [options] [input.mir|-]\n"
+ "operations:\n"
+ " --verify Parse, canonicalize, and verify MIR\n"
+ " --reproduce <package> Strictly replay a reproducer package\n"
+ " --reduce <validity|failure> Structurally reduce MIR\n"
+ " --fuzz <layer,...|all> Run deterministic layered fuzzing\n"
+ " --list-passes List the canonical pipeline suffix\n"
+ "pipeline/output:\n"
+ " -run-pass <name> Run one named pass\n"
+ " -passes <name,...> Run an explicit pass sequence\n"
+ " -start-before <pass[,N]> Start before a pass instance\n"
+ " -start-after <pass[,N]> Start after a pass instance\n"
+ " -stop-before <pass[,N]> Stop before a pass instance\n"
+ " -stop-after <pass[,N]> Stop after a pass instance\n"
+ " -print-before <selector,...> Print snapshots before passes\n"
+ " -print-after <selector,...> Print snapshots after passes\n"
+ " -simplify-mir Omit default-valued MIR fields\n"
+ " -verify-machineinstrs Verify each machine pass result\n"
+ " --stage-snapshots Print every stage boundary\n"
+ " -filetype <asm|obj|null> Select the output artifact\n"
+ " -o <file|-> Select the output path\n"
+ "reproduction/cache/fuzz:\n"
+ " --crash-reproducer <path> Save input and manifest before execution\n"
+ " --cache-dir <path> Select the content-addressed cache root\n"
+ " --cache-policy <policy> off, read, write, or read-write\n"
+ " --fuzz-iterations <count> Cases per selected fuzz layer\n"
+ "execution identity:\n"
+ " --threads <count> Function worker count\n"
+ " --seed <value> Deterministic random seed\n"
+ " --fuel <value> Per-function Action fuel\n"
+ " --enable-action <name> Enable an Action ID component\n"
+ " --print-actions Print stable Action decisions\n"
+ " -O0|-O1|-O2|-O3|-Os|-Oz Optimization level identity\n"
+ " -relocation-model <static|pic>\n"
+ " -code-model <small|medium|large>\n"
+ " --layout <native-object|compiler-final-layout|external-relaxation>\n"
+ " -mtriple/-mcpu/-mtune/-mattr/-target-abi <value>\n"
+ " --data-layout <value> Override the MIR data layout\n"
+ " --target-option <name=value> Explicit target option\n"
+ " --assembler-option <name=value>\n"
+ " --assembler-flavor/--assembler-version <value>\n";
+}
+
+[[nodiscard]] Expected<std::optional<std::string_view>> takeOption(std::string_view Argument,
+ std::string_view Name,
+ int &Index, int ArgumentCount,
+ char *Arguments[]) {
+ const std::string LongName =
+ Name.starts_with('-') && !Name.starts_with("--") ? "-" + std::string(Name) : std::string();
+ if (Argument == Name || (!LongName.empty() && Argument == LongName)) {
+ if (Index + 1 >= ArgumentCount) {
+ return rcc::makeUnexpected(ErrorCode::InvalidInvocation,
+ "missing value for command-line option '" + std::string(Name) +
+ "'");
+ }
+ return std::string_view(Arguments[++Index]);
+ }
+ std::string Prefix(Name);
+ Prefix.push_back('=');
+ if (Argument.starts_with(Prefix))
+ return Argument.substr(Prefix.size());
+ if (!LongName.empty()) {
+ std::string LongPrefix(LongName);
+ LongPrefix.push_back('=');
+ if (Argument.starts_with(LongPrefix))
+ return Argument.substr(LongPrefix.size());
+ }
+ return std::nullopt;
+}
+
+[[nodiscard]] Expected<std::optional<bool>> parseBooleanOption(std::string_view Argument,
+ std::string_view Name) {
+ const std::string LongName =
+ Name.starts_with('-') && !Name.starts_with("--") ? "-" + std::string(Name) : std::string();
+ if (Argument == Name || (!LongName.empty() && Argument == LongName))
+ return true;
+ const auto ParseValue = [Name](std::string_view Value) -> Expected<std::optional<bool>> {
+ if (Value == "true" || Value == "1")
+ return std::optional<bool>{true};
+ if (Value == "false" || Value == "0")
+ return std::optional<bool>{false};
+ return rcc::makeUnexpected(ErrorCode::InvalidInvocation, "command-line option '" +
+ std::string(Name) +
+ "' requires true or false");
+ };
+ std::string Prefix(Name);
+ Prefix.push_back('=');
+ if (Argument.starts_with(Prefix))
+ return ParseValue(Argument.substr(Prefix.size()));
+ if (!LongName.empty()) {
+ std::string LongPrefix(LongName);
+ LongPrefix.push_back('=');
+ if (Argument.starts_with(LongPrefix))
+ return ParseValue(Argument.substr(LongPrefix.size()));
+ }
+ return std::nullopt;
+}
+
+template <typename IntegerT>
+[[nodiscard]] Expected<IntegerT> parseInteger(std::string_view Text, std::string_view Description) {
+ IntegerT Value = 0;
+ const auto Result = std::from_chars(Text.data(), Text.data() + Text.size(), Value);
+ if (Result.ec != std::errc{} || Result.ptr != Text.data() + Text.size()) {
+ return rcc::makeUnexpected(ErrorCode::InvalidInvocation,
+ std::string(Description) + " must be an unsigned decimal integer");
+ }
+ return Value;
+}
+
+[[nodiscard]] Expected<std::vector<std::string>> splitList(std::string_view Text,
+ std::string_view Description) {
+ std::vector<std::string> Result;
+ std::size_t Begin = 0;
+ while (Begin <= Text.size()) {
+ const std::size_t Comma = Text.find(',', Begin);
+ const std::size_t End = Comma == std::string_view::npos ? Text.size() : Comma;
+ if (End == Begin) {
+ return rcc::makeUnexpected(ErrorCode::InvalidInvocation,
+ std::string(Description) + " contains an empty element");
+ }
+ Result.emplace_back(Text.substr(Begin, End - Begin));
+ if (Comma == std::string_view::npos)
+ break;
+ Begin = Comma + 1U;
+ }
+ return Result;
+}
+
+void applyFeatureOverrides(std::vector<std::string> Overrides, std::vector<std::string> &Features) {
+ for (std::string &Override : Overrides) {
+ if (!Override.empty() && Override.front() != '+' && Override.front() != '-')
+ Override.insert(Override.begin(), '+');
+ const std::string_view Name = std::string_view(Override).substr(1);
+ std::erase_if(Features, [Name](const std::string &Feature) {
+ return Feature.size() > 1 && std::string_view(Feature).substr(1) == Name;
+ });
+ Features.push_back(std::move(Override));
+ }
+}
+
+[[nodiscard]] Expected<rcc::tooling::BackendOption> parseBackendOption(std::string_view Text,
+ std::string_view Category) {
+ const std::size_t Equal = Text.find('=');
+ if (Equal == std::string_view::npos || Equal == 0) {
+ return rcc::makeUnexpected(ErrorCode::InvalidInvocation,
+ std::string(Category) + " option must use name=value syntax");
+ }
+ return rcc::tooling::BackendOption{std::string(Text.substr(0, Equal)),
+ std::string(Text.substr(Equal + 1U))};
+}
+
+void setBackendOption(std::vector<rcc::tooling::BackendOption> &Options, std::string Name,
+ std::string Value) {
+ const auto Existing = std::ranges::find(Options, Name, &rcc::tooling::BackendOption::Name);
+ if (Existing == Options.end())
+ Options.push_back({std::move(Name), std::move(Value)});
+ else
+ Existing->Value = std::move(Value);
+}
+
+[[nodiscard]] Expected<void> setOperation(CommandLine &Command, Operation Action) {
+ if (Command.Action != Operation::Run && Command.Action != Action) {
+ return rcc::makeUnexpected(ErrorCode::InvalidInvocation,
+ "command-line operations are mutually exclusive");
+ }
+ Command.Action = Action;
+ return {};
+}
+
+[[nodiscard]] Expected<CachePolicy> parseCachePolicy(std::string_view Value) {
+ if (Value == "off")
+ return CachePolicy::Off;
+ if (Value == "read")
+ return CachePolicy::Read;
+ if (Value == "write")
+ return CachePolicy::Write;
+ if (Value == "read-write")
+ return CachePolicy::ReadWrite;
+ return rcc::makeUnexpected(ErrorCode::InvalidInvocation, "unknown cache policy");
+}
+
+[[nodiscard]] Expected<void> parseFuzzLayers(std::string_view Value, CommandLine &Command) {
+ auto OperationResult = setOperation(Command, Operation::Fuzz);
+ if (!OperationResult)
+ return OperationResult;
+ if (Value == "all") {
+ Command.FuzzOptions.Layers.clear();
+ return {};
+ }
+ auto Values = splitList(Value, "fuzz layer list");
+ if (!Values)
+ return std::unexpected(Values.error());
+ Command.FuzzOptions.Layers.clear();
+ for (const std::string &LayerName : *Values) {
+ auto Layer = rcc::tooling::parseMIRFuzzLayer(LayerName);
+ if (!Layer)
+ return std::unexpected(Layer.error());
+ Command.FuzzOptions.Layers.push_back(*Layer);
+ }
+ return {};
+}
+
+[[nodiscard]] Expected<CommandLine> parseCommandLine(int ArgumentCount, char *Arguments[],
+ BackendInvocation DefaultInvocation) {
+ CommandLine Command;
+ Command.Invocation = std::move(DefaultInvocation);
+ bool ExplicitPipeline = false;
+ bool UsedRunPass = false;
+ bool UsedPasses = false;
+ rcc::CodeGenCommandFlags CodeGenFlags;
+ std::vector<std::string_view> ArgumentViews;
+ ArgumentViews.reserve(static_cast<std::size_t>(ArgumentCount));
+ for (int Index = 0; Index < ArgumentCount; ++Index)
+ ArgumentViews.emplace_back(Arguments[Index]);
+ for (int Index = 1; Index < ArgumentCount; ++Index) {
+ const std::string_view Argument(Arguments[Index]);
+ if (Argument == "--help" || Argument == "-h") {
+ Command.ShowHelp = true;
+ continue;
+ }
+ if (Argument == "--verify") {
+ auto Set = setOperation(Command, Operation::Verify);
+ if (!Set)
+ return std::unexpected(Set.error());
+ continue;
+ }
+ if (Argument == "--list-passes") {
+ auto Set = setOperation(Command, Operation::ListPasses);
+ if (!Set)
+ return std::unexpected(Set.error());
+ continue;
+ }
+ if (Argument == "--stage-snapshots") {
+ Command.Controls.CaptureStageSnapshots = true;
+ continue;
+ }
+ if (Argument == "-simplify-mir" || Argument == "--simplify-mir") {
+ Command.Controls.SimplifyMIR = true;
+ continue;
+ }
+ if (Argument == "-verify-machineinstrs" || Argument == "--verify-machineinstrs") {
+ Command.Controls.VerifyEach = true;
+ continue;
+ }
+ if (Argument == "--no-verify-each") {
+ Command.Controls.VerifyEach = false;
+ continue;
+ }
+ if (Argument == "--no-change-detection") {
+ Command.Controls.DetectUnreportedChanges = false;
+ continue;
+ }
+ if (Argument == "-debugify-and-strip-all-safe" || Argument == "--debugify-and-strip-all-safe") {
+ Command.Controls.DebugifyAndStripAllSafe = true;
+ continue;
+ }
+ if (Argument == "--print-actions") {
+ Command.PrintActions = true;
+ continue;
+ }
+ std::size_t CodeGenIndex = static_cast<std::size_t>(Index);
+ auto CodeGenFlag = rcc::parseCodeGenCommandFlag(ArgumentViews, CodeGenIndex, CodeGenFlags);
+ if (!CodeGenFlag)
+ return std::unexpected(CodeGenFlag.error());
+ if (*CodeGenFlag) {
+ Index = static_cast<int>(CodeGenIndex);
+ continue;
+ }
+
+ bool ParsedCompatibilityOption = false;
+ for (const std::string_view Name : {
+ std::string_view{"-disable-gisel-legality-check"},
+ std::string_view{"-mcp-use-is-copy-instr"},
+ std::string_view{"-riscv-enable-cfi-instr-inserter"},
+ std::string_view{"-riscv-outliner-regsave"},
+ std::string_view{"-outliner-leaf-descendants"},
+ std::string_view{"-function-sections"},
+ }) {
+ auto Value = parseBooleanOption(Argument, Name);
+ if (!Value)
+ return std::unexpected(Value.error());
+ if (!*Value)
+ continue;
+ setBackendOption(Command.Invocation.TargetOptions, std::string(Name.substr(1)),
+ **Value ? "true" : "false");
+ ParsedCompatibilityOption = true;
+ break;
+ }
+ if (ParsedCompatibilityOption)
+ continue;
+
+ auto GlobalISelAbort =
+ takeOption(Argument, "-global-isel-abort", Index, ArgumentCount, Arguments);
+ if (!GlobalISelAbort)
+ return std::unexpected(GlobalISelAbort.error());
+ if (*GlobalISelAbort) {
+ auto Mode = rcc::parseGlobalISelAbortMode(**GlobalISelAbort);
+ if (!Mode)
+ return std::unexpected(Mode.error());
+ setBackendOption(Command.Invocation.TargetOptions, "global-isel-abort",
+ std::string(**GlobalISelAbort));
+ continue;
+ }
+ auto DebugOnly = takeOption(Argument, "-debug-only", Index, ArgumentCount, Arguments);
+ if (!DebugOnly)
+ return std::unexpected(DebugOnly.error());
+ if (*DebugOnly) {
+ if ((**DebugOnly).empty()) {
+ return rcc::makeUnexpected(ErrorCode::InvalidInvocation,
+ "-debug-only requires a nonempty debug type");
+ }
+ setBackendOption(Command.Invocation.TargetOptions, "debug-only", std::string(**DebugOnly));
+ continue;
+ }
+
+ auto Output = takeOption(Argument, "-o", Index, ArgumentCount, Arguments);
+ if (!Output)
+ return std::unexpected(Output.error());
+ if (*Output) {
+ Command.OutputPath = std::string(**Output);
+ Command.OutputWasSpecified = true;
+ continue;
+ }
+ auto RunPass = takeOption(Argument, "-run-pass", Index, ArgumentCount, Arguments);
+ if (!RunPass)
+ return std::unexpected(RunPass.error());
+ if (*RunPass) {
+ if (UsedPasses)
+ return rcc::makeUnexpected(ErrorCode::InvalidInvocation,
+ "-run-pass and -passes cannot be combined");
+ auto Names = splitList(**RunPass, "pass pipeline");
+ if (!Names)
+ return std::unexpected(Names.error());
+ if (!UsedRunPass)
+ Command.Invocation.PassPipeline.clear();
+ Command.Invocation.PassPipeline.insert(Command.Invocation.PassPipeline.end(),
+ std::make_move_iterator(Names->begin()),
+ std::make_move_iterator(Names->end()));
+ UsedRunPass = true;
+ ExplicitPipeline = true;
+ continue;
+ }
+ auto Passes = takeOption(Argument, "-passes", Index, ArgumentCount, Arguments);
+ if (!Passes)
+ return std::unexpected(Passes.error());
+ if (*Passes) {
+ if (UsedRunPass || UsedPasses)
+ return rcc::makeUnexpected(ErrorCode::InvalidInvocation,
+ "-run-pass and -passes cannot be combined");
+ auto Names = splitList(**Passes, "pass pipeline");
+ if (!Names)
+ return std::unexpected(Names.error());
+ Command.Invocation.PassPipeline = std::move(*Names);
+ UsedPasses = true;
+ ExplicitPipeline = true;
+ continue;
+ }
+
+ const auto ParseSelector = [&](std::string_view Name,
+ std::optional<std::string> &Destination) -> Expected<bool> {
+ auto Value = takeOption(Argument, Name, Index, ArgumentCount, Arguments);
+ if (!Value)
+ return std::unexpected(Value.error());
+ if (!*Value)
+ return false;
+ Destination = std::string(**Value);
+ return true;
+ };
+ auto StopBefore = ParseSelector("-stop-before", Command.Controls.StopBefore);
+ if (!StopBefore)
+ return std::unexpected(StopBefore.error());
+ if (*StopBefore)
+ continue;
+ auto StopAfter = ParseSelector("-stop-after", Command.Controls.StopAfter);
+ if (!StopAfter)
+ return std::unexpected(StopAfter.error());
+ if (*StopAfter)
+ continue;
+ auto StartBefore = ParseSelector("-start-before", Command.Controls.StartBefore);
+ if (!StartBefore)
+ return std::unexpected(StartBefore.error());
+ if (*StartBefore)
+ continue;
+ auto StartAfter = ParseSelector("-start-after", Command.Controls.StartAfter);
+ if (!StartAfter)
+ return std::unexpected(StartAfter.error());
+ if (*StartAfter)
+ continue;
+
+ auto InputLanguage = takeOption(Argument, "-x", Index, ArgumentCount, Arguments);
+ if (!InputLanguage)
+ return std::unexpected(InputLanguage.error());
+ if (*InputLanguage) {
+ if (**InputLanguage != "mir") {
+ return rcc::makeUnexpected(ErrorCode::InvalidInvocation, "input language must be 'mir'");
+ }
+ continue;
+ }
+
+ const auto ParseSelectorList = [&](std::string_view Name,
+ std::vector<std::string> &Destination) -> Expected<bool> {
+ auto Value = takeOption(Argument, Name, Index, ArgumentCount, Arguments);
+ if (!Value)
+ return std::unexpected(Value.error());
+ if (!*Value)
+ return false;
+ auto Selectors = splitList(**Value, Name);
+ if (!Selectors)
+ return std::unexpected(Selectors.error());
+ Destination.insert(Destination.end(), std::make_move_iterator(Selectors->begin()),
+ std::make_move_iterator(Selectors->end()));
+ return true;
+ };
+ auto PrintBefore = ParseSelectorList("-print-before", Command.Controls.PrintBefore);
+ if (!PrintBefore)
+ return std::unexpected(PrintBefore.error());
+ if (*PrintBefore)
+ continue;
+ auto PrintAfter = ParseSelectorList("-print-after", Command.Controls.PrintAfter);
+ if (!PrintAfter)
+ return std::unexpected(PrintAfter.error());
+ if (*PrintAfter)
+ continue;
+
+ auto Reproduce = takeOption(Argument, "--reproduce", Index, ArgumentCount, Arguments);
+ if (!Reproduce)
+ return std::unexpected(Reproduce.error());
+ if (*Reproduce) {
+ auto Set = setOperation(Command, Operation::Reproduce);
+ if (!Set)
+ return std::unexpected(Set.error());
+ Command.ReproducerPath = std::string(**Reproduce);
+ continue;
+ }
+ auto Reduce = takeOption(Argument, "--reduce", Index, ArgumentCount, Arguments);
+ if (!Reduce)
+ return std::unexpected(Reduce.error());
+ if (*Reduce) {
+ Operation Mode;
+ if (**Reduce == "validity")
+ Mode = Operation::ReduceValidity;
+ else if (**Reduce == "failure")
+ Mode = Operation::ReduceFailure;
+ else
+ return rcc::makeUnexpected(ErrorCode::InvalidInvocation, "unknown reducer mode");
+ auto Set = setOperation(Command, Mode);
+ if (!Set)
+ return std::unexpected(Set.error());
+ continue;
+ }
+ auto Fuzz = takeOption(Argument, "--fuzz", Index, ArgumentCount, Arguments);
+ if (!Fuzz)
+ return std::unexpected(Fuzz.error());
+ if (*Fuzz) {
+ auto Parsed = parseFuzzLayers(**Fuzz, Command);
+ if (!Parsed)
+ return std::unexpected(Parsed.error());
+ continue;
+ }
+ auto FuzzIterations =
+ takeOption(Argument, "--fuzz-iterations", Index, ArgumentCount, Arguments);
+ if (!FuzzIterations)
+ return std::unexpected(FuzzIterations.error());
+ if (*FuzzIterations) {
+ auto Count = parseInteger<std::size_t>(**FuzzIterations, "fuzz iteration count");
+ if (!Count)
+ return std::unexpected(Count.error());
+ Command.FuzzOptions.Iterations = *Count;
+ continue;
+ }
+ auto CrashReproducer =
+ takeOption(Argument, "--crash-reproducer", Index, ArgumentCount, Arguments);
+ if (!CrashReproducer)
+ return std::unexpected(CrashReproducer.error());
+ if (*CrashReproducer) {
+ Command.ReproducerPath = std::string(**CrashReproducer);
+ continue;
+ }
+ auto CacheDirectory = takeOption(Argument, "--cache-dir", Index, ArgumentCount, Arguments);
+ if (!CacheDirectory)
+ return std::unexpected(CacheDirectory.error());
+ if (*CacheDirectory) {
+ Command.CachePath = std::string(**CacheDirectory);
+ continue;
+ }
+ auto CacheMode = takeOption(Argument, "--cache-policy", Index, ArgumentCount, Arguments);
+ if (!CacheMode)
+ return std::unexpected(CacheMode.error());
+ if (*CacheMode) {
+ auto Policy = parseCachePolicy(**CacheMode);
+ if (!Policy)
+ return std::unexpected(Policy.error());
+ Command.Cache = *Policy;
+ continue;
+ }
+
+ const auto ParseUnsigned = [&](std::string_view Name, auto &Destination,
+ std::string_view Description) -> Expected<bool> {
+ auto Value = takeOption(Argument, Name, Index, ArgumentCount, Arguments);
+ if (!Value)
+ return std::unexpected(Value.error());
+ if (!*Value)
+ return false;
+ using IntegerT = std::remove_reference_t<decltype(Destination)>;
+ auto Parsed = parseInteger<IntegerT>(**Value, Description);
+ if (!Parsed)
+ return std::unexpected(Parsed.error());
+ Destination = *Parsed;
+ return true;
+ };
+ auto Threads =
+ ParseUnsigned("--threads", Command.Invocation.Determinism.ThreadCount, "thread count");
+ if (!Threads)
+ return std::unexpected(Threads.error());
+ if (*Threads)
+ continue;
+ auto Seed = ParseUnsigned("--seed", Command.Invocation.Determinism.RandomSeed, "random seed");
+ if (!Seed)
+ return std::unexpected(Seed.error());
+ if (*Seed) {
+ Command.FuzzOptions.Seed = Command.Invocation.Determinism.RandomSeed;
+ continue;
+ }
+ auto Fuel = takeOption(Argument, "--fuel", Index, ArgumentCount, Arguments);
+ if (!Fuel)
+ return std::unexpected(Fuel.error());
+ if (*Fuel) {
+ auto Value = parseInteger<std::uint64_t>(**Fuel, "Action fuel");
+ if (!Value)
+ return std::unexpected(Value.error());
+ Command.Invocation.ActionFuel = *Value;
+ continue;
+ }
+ auto EnabledAction = takeOption(Argument, "--enable-action", Index, ArgumentCount, Arguments);
+ if (!EnabledAction)
+ return std::unexpected(EnabledAction.error());
+ if (*EnabledAction) {
+ Command.Invocation.EnabledActions.emplace_back(**EnabledAction);
+ continue;
+ }
+
+ const auto ParseString = [&](std::string_view Name,
+ std::string &Destination) -> Expected<bool> {
+ auto Value = takeOption(Argument, Name, Index, ArgumentCount, Arguments);
+ if (!Value)
+ return std::unexpected(Value.error());
+ if (!*Value)
+ return false;
+ Destination = std::string(**Value);
+ return true;
+ };
+ auto DataLayout = ParseString("--data-layout", Command.Invocation.DataLayout);
+ if (!DataLayout)
+ return std::unexpected(DataLayout.error());
+ if (*DataLayout)
+ continue;
+ auto AssemblerFlavor = ParseString("--assembler-flavor", Command.Invocation.AssemblerFlavor);
+ if (!AssemblerFlavor)
+ return std::unexpected(AssemblerFlavor.error());
+ if (*AssemblerFlavor)
+ continue;
+ auto AssemblerVersion = ParseString("--assembler-version", Command.Invocation.AssemblerVersion);
+ if (!AssemblerVersion)
+ return std::unexpected(AssemblerVersion.error());
+ if (*AssemblerVersion)
+ continue;
+ auto Layout = takeOption(Argument, "--layout", Index, ArgumentCount, Arguments);
+ if (!Layout)
+ return std::unexpected(Layout.error());
+ if (*Layout) {
+ auto Value = rcc::tooling::parseLayoutOwnership(**Layout);
+ if (!Value)
+ return std::unexpected(Value.error());
+ Command.Invocation.Layout = *Value;
+ continue;
+ }
+ auto TargetOption = takeOption(Argument, "--target-option", Index, ArgumentCount, Arguments);
+ if (!TargetOption)
+ return std::unexpected(TargetOption.error());
+ if (*TargetOption) {
+ auto Value = parseBackendOption(**TargetOption, "target");
+ if (!Value)
+ return std::unexpected(Value.error());
+ const auto Existing = std::ranges::find(Command.Invocation.TargetOptions, Value->Name,
+ &rcc::tooling::BackendOption::Name);
+ if (Existing == Command.Invocation.TargetOptions.end())
+ Command.Invocation.TargetOptions.push_back(std::move(*Value));
+ else
+ Existing->Value = std::move(Value->Value);
+ continue;
+ }
+ auto AssemblerOption =
+ takeOption(Argument, "--assembler-option", Index, ArgumentCount, Arguments);
+ if (!AssemblerOption)
+ return std::unexpected(AssemblerOption.error());
+ if (*AssemblerOption) {
+ auto Value = parseBackendOption(**AssemblerOption, "assembler");
+ if (!Value)
+ return std::unexpected(Value.error());
+ const auto Existing = std::ranges::find(Command.Invocation.AssemblerOptions, Value->Name,
+ &rcc::tooling::BackendOption::Name);
+ if (Existing == Command.Invocation.AssemblerOptions.end())
+ Command.Invocation.AssemblerOptions.push_back(std::move(*Value));
+ else
+ Existing->Value = std::move(Value->Value);
+ continue;
+ }
+
+ if (Argument == "-" || !Argument.starts_with('-')) {
+ if (!Command.InputPath.empty())
+ return rcc::makeUnexpected(ErrorCode::InvalidInvocation, "input path is specified twice");
+ Command.InputPath = std::string(Argument);
+ continue;
+ }
+ return rcc::makeUnexpected(ErrorCode::InvalidInvocation,
+ "unknown command-line option '" + std::string(Argument) + "'");
+ }
+
+ if (Command.ShowHelp)
+ return Command;
+ if (CodeGenFlags.Triple) {
+ auto SelectedTarget = rcc::TargetRegistry::lookupTarget(*CodeGenFlags.Triple);
+ if (!SelectedTarget)
+ return std::unexpected(SelectedTarget.error());
+ Command.Invocation.Triple = (*SelectedTarget)->normalizeTriple(*CodeGenFlags.Triple);
+ Command.Invocation.Features.clear();
+ for (std::string_view Feature : (*SelectedTarget)->baselineFeatures())
+ Command.Invocation.Features.emplace_back(Feature);
+ }
+ if (CodeGenFlags.CPU)
+ Command.Invocation.CPU = *CodeGenFlags.CPU;
+ if (CodeGenFlags.TuneCPU)
+ Command.Invocation.TuneCPU = *CodeGenFlags.TuneCPU;
+ applyFeatureOverrides(std::move(CodeGenFlags.FeatureOverrides), Command.Invocation.Features);
+ if (CodeGenFlags.ABI) {
+ Command.Invocation.ABI = *CodeGenFlags.ABI;
+ } else {
+ auto SelectedTarget = rcc::TargetRegistry::lookupTarget(Command.Invocation.Triple);
+ if (!SelectedTarget)
+ return std::unexpected(SelectedTarget.error());
+ Command.Invocation.ABI = (*SelectedTarget)->defaultABI(Command.Invocation.Features);
+ }
+ if (CodeGenFlags.Relocation)
+ Command.Invocation.Relocation = *CodeGenFlags.Relocation;
+ if (CodeGenFlags.Code)
+ Command.Invocation.Code = *CodeGenFlags.Code;
+ if (CodeGenFlags.Optimization)
+ Command.Invocation.Optimization = *CodeGenFlags.Optimization;
+ const bool HasLimitedCodeGenPipeline = Command.Controls.StartBefore ||
+ Command.Controls.StartAfter ||
+ Command.Controls.StopBefore || Command.Controls.StopAfter;
+ if (UsedRunPass && HasLimitedCodeGenPipeline) {
+ return rcc::makeUnexpected(ErrorCode::InvalidInvocation,
+ "-run-pass cannot be used with a limited code-generation pipeline");
+ }
+ if (ExplicitPipeline || HasLimitedCodeGenPipeline) {
+ Command.Output = ArtifactKind::FinalMIR;
+ } else {
+ switch (CodeGenFlags.FileType) {
+ case rcc::CodeGenFileType::AssemblyFile:
+ Command.Output = ArtifactKind::Assembly;
+ break;
+ case rcc::CodeGenFileType::ObjectFile:
+ Command.Output = ArtifactKind::RelocatableObject;
+ break;
+ case rcc::CodeGenFileType::Null:
+ Command.Output = ArtifactKind::Assembly;
+ Command.DiscardOutput = true;
+ break;
+ }
+ }
+ Command.DiscardOutput =
+ CodeGenFlags.FileTypeWasSpecified && CodeGenFlags.FileType == rcc::CodeGenFileType::Null;
+ if (Command.Action == Operation::Reproduce) {
+ if (!Command.InputPath.empty())
+ return rcc::makeUnexpected(ErrorCode::InvalidInvocation,
+ "reproducer replay does not accept a separate MIR input");
+ } else if (Command.InputPath.empty()) {
+ Command.InputPath = "-";
+ }
+ if (Command.Cache != CachePolicy::Off && Command.CachePath.empty()) {
+ return rcc::makeUnexpected(ErrorCode::InvalidInvocation,
+ "a non-off cache policy requires --cache-dir");
+ }
+ if (Command.Action != Operation::Run && Command.Action != Operation::Reproduce &&
+ Command.Cache != CachePolicy::Off) {
+ return rcc::makeUnexpected(ErrorCode::InvalidInvocation,
+ "cache policies only apply to run and reproduce operations");
+ }
+ auto Canonical = Command.Invocation.canonicalizeAndVerify();
+ if (!Canonical)
+ return std::unexpected(Canonical.error());
+ return Command;
+}
+
+[[nodiscard]] Expected<std::string> readInput(std::string_view Path) {
+ if (Path == "-")
+ return std::string(std::istreambuf_iterator<char>(std::cin), std::istreambuf_iterator<char>());
+ std::ifstream Input(std::string(Path), std::ios::binary);
+ if (!Input)
+ return rcc::makeUnexpected(ErrorCode::IoError,
+ "cannot open input file '" + std::string(Path) + "'");
+ return std::string(std::istreambuf_iterator<char>(Input), std::istreambuf_iterator<char>());
+}
+
+[[nodiscard]] Expected<void> writeOutput(std::string_view Path,
+ std::span<const std::uint8_t> Bytes) {
+ if (Path == "-") {
+ if (!Bytes.empty()) {
+ std::cout.write(reinterpret_cast<const char *>(Bytes.data()),
+ static_cast<std::streamsize>(Bytes.size()));
+ }
+ if (!std::cout)
+ return rcc::makeUnexpected(ErrorCode::IoError, "cannot write standard output");
+ return {};
+ }
+ std::ofstream Output(std::string(Path), std::ios::binary | std::ios::trunc);
+ if (!Output)
+ return rcc::makeUnexpected(ErrorCode::IoError,
+ "cannot open output file '" + std::string(Path) + "'");
+ if (!Bytes.empty()) {
+ Output.write(reinterpret_cast<const char *>(Bytes.data()),
+ static_cast<std::streamsize>(Bytes.size()));
+ }
+ Output.close();
+ if (!Output)
+ return rcc::makeUnexpected(ErrorCode::IoError,
+ "cannot write output file '" + std::string(Path) + "'");
+ return {};
+}
+
+[[nodiscard]] Expected<void> writeText(std::string_view Path, std::string_view Text) {
+ const auto *Bytes = reinterpret_cast<const std::uint8_t *>(Text.data());
+ return writeOutput(Path, std::span<const std::uint8_t>(Bytes, Text.size()));
+}
+
+void printSnapshots(const BackendRunResult &Result) {
+ for (const rcc::tooling::PipelineSnapshot &Snapshot : Result.Pipeline.Snapshots) {
+ std::cerr << "*** " << rcc::tooling::spelling(Snapshot.Stage);
+ if (!Snapshot.Pass.empty())
+ std::cerr << ' ' << Snapshot.Pass << '#' << Snapshot.Occurrence;
+ if (!Snapshot.Function.empty())
+ std::cerr << " [" << Snapshot.Function << ']';
+ std::cerr << " ***\n" << Snapshot.MIR;
+ if (!Snapshot.MIR.ends_with('\n'))
+ std::cerr << '\n';
+ }
+}
+
+void printActions(const BackendRunResult &Result) {
+ for (const rcc::MachineActionEvent &Action : Result.Actions) {
+ std::cerr << (Action.Accepted ? "accepted " : "rejected ") << Action.ID.toString()
+ << " cost=" << Action.Cost << '\n';
+ }
+}
+
+[[nodiscard]] Expected<void> writePreExecutionReproducer(const CommandLine &Command,
+ const CanonicalMIR &Input,
+ const rcc::TargetDescription &Target) {
+ if (Command.ReproducerPath.empty())
+ return {};
+ auto Manifest =
+ rcc::tooling::createBackendManifest(rcc::tooling::ManifestKind::Reproducer, Command.Output,
+ Command.Invocation, Target, Input.Text);
+ if (!Manifest)
+ return std::unexpected(Manifest.error());
+ return rcc::tooling::writeReproducer(Command.ReproducerPath, *Manifest, Input.Text);
+}
+
+[[nodiscard]] Expected<BackendRunResult>
+runWithCache(const CommandLine &Command, CanonicalMIR Input, const rcc::TargetDescription &Target,
+ rcc::MachinePassInstrumentation *Instrumentation = nullptr) {
+ if (Command.Cache == CachePolicy::Off) {
+ return rcc::tooling::runBackend(std::move(Input), Command.Invocation, Command.Output,
+ Command.Controls, Instrumentation);
+ }
+ if (!rcc::tooling::isCacheableArtifact(Command.Output)) {
+ return rcc::makeUnexpected(ErrorCode::InvalidInvocation,
+ "the selected intermediate stage is not a cache boundary");
+ }
+ const ArtifactKind CacheKind = Command.Output;
+ if (Command.Cache == CachePolicy::Read || Command.Cache == CachePolicy::ReadWrite) {
+ auto Cached = rcc::tooling::loadCacheEntry(Command.CachePath, CacheKind, Command.Invocation,
+ Target, Input.Text);
+ if (Cached) {
+ return BackendRunResult{
+ .Kind = Command.Output,
+ .Output = std::move(Cached->Artifact),
+ .Pipeline = {},
+ .Actions = {},
+ };
+ }
+ if (Command.Cache == CachePolicy::Read || Cached.error().code() != ErrorCode::CacheMiss)
+ return std::unexpected(Cached.error());
+ }
+
+ const std::string CanonicalInput = Input.Text;
+ auto Run = rcc::tooling::runBackend(std::move(Input), Command.Invocation, Command.Output,
+ Command.Controls, Instrumentation);
+ if (!Run)
+ return std::unexpected(Run.error());
+ if (Command.Cache == CachePolicy::Write || Command.Cache == CachePolicy::ReadWrite) {
+ auto Manifest = rcc::tooling::createBackendManifest(
+ rcc::tooling::ManifestKind::Cache, CacheKind, Command.Invocation, Target, CanonicalInput,
+ std::span<const std::uint8_t>(Run->Output));
+ if (!Manifest)
+ return std::unexpected(Manifest.error());
+ auto Stored =
+ rcc::tooling::storeCacheEntry(Command.CachePath, *Manifest, CanonicalInput, Run->Output);
+ if (!Stored)
+ return std::unexpected(Stored.error());
+ }
+ return Run;
+}
+
+[[nodiscard]] Expected<void> runCommand(const CommandLine &OriginalCommand) {
+ CommandLine Command = OriginalCommand;
+ auto Machine = rcc::tooling::createTargetMachine(Command.Invocation);
+ if (!Machine)
+ return std::unexpected(Machine.error());
+ const rcc::TargetDescription &Target = (*Machine)->targetDescription();
+
+ if (Command.Action == Operation::Reproduce) {
+ auto Package = rcc::tooling::loadReproducer(Command.ReproducerPath, Target);
+ if (!Package)
+ return std::unexpected(Package.error());
+ Command.Output = Package->Manifest.Artifact;
+ Command.Invocation = Package->Manifest.Invocation;
+ Command.ReproducerPath.clear();
+ auto ReplayMachine = rcc::tooling::createTargetMachine(Command.Invocation);
+ if (!ReplayMachine)
+ return std::unexpected(ReplayMachine.error());
+ auto Run =
+ runWithCache(Command, std::move(Package->Input), (*ReplayMachine)->targetDescription());
+ if (!Run)
+ return std::unexpected(Run.error());
+ printSnapshots(*Run);
+ if (Command.PrintActions)
+ printActions(*Run);
+ return writeOutput(Command.OutputPath, Run->Output);
+ }
+
+ auto Source = readInput(Command.InputPath);
+ if (!Source)
+ return std::unexpected(Source.error());
+ const std::string SourceName = Command.InputPath == "-" ? "<stdin>" : Command.InputPath;
+ auto Input = rcc::tooling::parseAndCanonicalizeMIR(SourceName, std::move(*Source), Target,
+ Command.Invocation);
+ if (!Input)
+ return std::unexpected(Input.error());
+
+ if (Command.Action == Operation::Verify) {
+ if (Command.OutputWasSpecified)
+ return writeText(Command.OutputPath, Input->Text);
+ return {};
+ }
+ if (Command.Action == Operation::ListPasses) {
+ std::vector<std::string> Names;
+ for (std::size_t Index = 0; Index < Input->Document.unit().functionCount(); ++Index) {
+ const rcc::MachineFunction *Function = Input->Document.unit().functionAt(Index);
+ if (Function == nullptr)
+ return rcc::makeUnexpected(ErrorCode::InternalError, "MIR input contains a null function");
+ auto FunctionNames = (*Machine)->codeGenPassNames(*Function);
+ if (!FunctionNames)
+ return std::unexpected(FunctionNames.error());
+ for (std::string &Name : *FunctionNames) {
+ if (std::ranges::find(Names, Name) == Names.end())
+ Names.push_back(std::move(Name));
+ }
+ }
+ std::string Output;
+ for (const std::string &Name : Names) {
+ Output.append(Name);
+ Output.push_back('\n');
+ }
+ return writeText(Command.OutputPath, Output);
+ }
+ if (Command.Action == Operation::Fuzz) {
+ auto Result = rcc::tooling::fuzzMIR(Command.InputPath, Input->Text, Command.Invocation, Target,
+ Command.FuzzOptions);
+ if (!Result)
+ return std::unexpected(Result.error());
+ std::string Output = "seed=" + std::to_string(Result->Seed) +
+ " executed=" + std::to_string(Result->executed()) +
+ " accepted=" + std::to_string(Result->accepted()) +
+ " rejected=" + std::to_string(Result->rejected()) + "\n";
+ for (const rcc::tooling::MIRFuzzLayerResult &Layer : Result->Layers) {
+ Output.append(rcc::tooling::spelling(Layer.Layer));
+ Output.append(": executed=");
+ Output.append(std::to_string(Layer.Executed));
+ Output.append(" accepted=");
+ Output.append(std::to_string(Layer.Accepted));
+ Output.append(" rejected=");
+ Output.append(std::to_string(Layer.Rejected));
+ Output.push_back('\n');
+ }
+ return writeText(Command.OutputPath, Output);
+ }
+ if (Command.Action == Operation::ReduceValidity) {
+ rcc::tooling::MIRReducerOptions Options;
+ Options.Mode = rcc::tooling::ReductionMode::PreserveValidity;
+ auto Reduced = rcc::tooling::reduceMIR(Command.InputPath, Input->Text, Command.Invocation,
+ Target, {}, std::move(Options));
+ if (!Reduced)
+ return std::unexpected(Reduced.error());
+ return writeText(Command.OutputPath, Reduced->MIR);
+ }
+ if (Command.Action == Operation::ReduceFailure) {
+ auto InitialForRun = rcc::tooling::parseAndCanonicalizeMIR("reducer-initial.mir", Input->Text,
+ Target, Command.Invocation);
+ if (!InitialForRun)
+ return std::unexpected(InitialForRun.error());
+ auto InitialRun = rcc::tooling::runBackend(std::move(*InitialForRun), Command.Invocation,
+ Command.Output, Command.Controls);
+ if (InitialRun) {
+ return rcc::makeUnexpected(ErrorCode::ReductionFailed,
+ "failure-preserving reduction requires an initially failing run");
+ }
+ const ErrorCode FailureCode = InitialRun.error().code();
+ const auto Interesting = [&](std::string_view MIR,
+ const BackendInvocation &Invocation) -> Expected<bool> {
+ auto Candidate = rcc::tooling::parseAndCanonicalizeMIR("reducer-interesting.mir",
+ std::string(MIR), Target, Invocation);
+ if (!Candidate)
+ return std::unexpected(Candidate.error());
+ auto Run = rcc::tooling::runBackend(std::move(*Candidate), Invocation, Command.Output,
+ Command.Controls);
+ return !Run && Run.error().code() == FailureCode;
+ };
+ rcc::tooling::MIRReducerOptions Options;
+ Options.Mode = rcc::tooling::ReductionMode::PreserveFailure;
+ auto Reduced = rcc::tooling::reduceMIR(Command.InputPath, Input->Text, Command.Invocation,
+ Target, Interesting, std::move(Options));
+ if (!Reduced)
+ return std::unexpected(Reduced.error());
+ return writeText(Command.OutputPath, Reduced->MIR);
+ }
+
+ auto Reproducer = writePreExecutionReproducer(Command, *Input, Target);
+ if (!Reproducer)
+ return std::unexpected(Reproducer.error());
+
+ std::vector<rcc::tooling::StructuredDiagnostic> FailureDiagnostics;
+ std::mutex FailureDiagnosticsMutex;
+ rcc::MachinePassInstrumentation Instrumentation;
+ const auto RecordFunctionFailure = [&](std::string_view Rule, std::string_view Pass,
+ const rcc::MachineFunction &Function,
+ const Error &Failure) {
+ rcc::tooling::StructuredDiagnostic Diagnostic;
+ Diagnostic.Category = std::string(rcc::getErrorCodeName(Failure.code()));
+ Diagnostic.Stage =
+ std::string(rcc::tooling::spelling(rcc::tooling::stageForFunction(Function)));
+ Diagnostic.Pass = std::string(Pass);
+ Diagnostic.Function = std::string(Function.name());
+ Diagnostic.Rule = std::string(Rule);
+ Diagnostic.Message = std::string(Failure.message());
+ std::scoped_lock Lock(FailureDiagnosticsMutex);
+ FailureDiagnostics.push_back(std::move(Diagnostic));
+ };
+ Instrumentation.addFailureCallback(
+ [&](std::string_view Pass, const rcc::MachineFunction &Function, const Error &Failure) {
+ RecordFunctionFailure("machine-pass", Pass, Function, Failure);
+ });
+ Instrumentation.addAnalysisFailureCallback(
+ [&](std::string_view Analysis, const rcc::MachineFunction &Function, const Error &Failure) {
+ RecordFunctionFailure("machine-analysis", Analysis, Function, Failure);
+ });
+ Instrumentation.addUnitFailureCallback(
+ [&](std::string_view Pass, const rcc::CompilationUnit &, const Error &Failure) {
+ rcc::tooling::StructuredDiagnostic Diagnostic;
+ Diagnostic.Category = std::string(rcc::getErrorCodeName(Failure.code()));
+ Diagnostic.Stage = "compilation-unit";
+ Diagnostic.Pass = std::string(Pass);
+ Diagnostic.Rule = "compilation-unit-pass";
+ Diagnostic.Message = std::string(Failure.message());
+ std::scoped_lock Lock(FailureDiagnosticsMutex);
+ FailureDiagnostics.push_back(std::move(Diagnostic));
+ });
+ Instrumentation.addUnitAnalysisFailureCallback(
+ [&](std::string_view Analysis, const rcc::CompilationUnit &, const Error &Failure) {
+ rcc::tooling::StructuredDiagnostic Diagnostic;
+ Diagnostic.Category = std::string(rcc::getErrorCodeName(Failure.code()));
+ Diagnostic.Stage = "compilation-unit";
+ Diagnostic.Pass = std::string(Analysis);
+ Diagnostic.Rule = "compilation-unit-analysis";
+ Diagnostic.Message = std::string(Failure.message());
+ std::scoped_lock Lock(FailureDiagnosticsMutex);
+ FailureDiagnostics.push_back(std::move(Diagnostic));
+ });
+
+ auto Run = runWithCache(Command, std::move(*Input), Target,
+ Command.ReproducerPath.empty() ? nullptr : &Instrumentation);
+ if (!Run) {
+ if (!Command.ReproducerPath.empty()) {
+ if (FailureDiagnostics.empty()) {
+ rcc::tooling::StructuredDiagnostic Diagnostic;
+ Diagnostic.Category = std::string(rcc::getErrorCodeName(Run.error().code()));
+ Diagnostic.Stage = "driver";
+ Diagnostic.Rule = "backend-driver";
+ Diagnostic.Message = std::string(Run.error().message());
+ FailureDiagnostics.push_back(std::move(Diagnostic));
+ }
+ std::ranges::sort(FailureDiagnostics, [](const auto &Left, const auto &Right) {
+ if (Left.Function != Right.Function)
+ return Left.Function < Right.Function;
+ if (Left.Stage != Right.Stage)
+ return Left.Stage < Right.Stage;
+ if (Left.Pass != Right.Pass)
+ return Left.Pass < Right.Pass;
+ if (Left.Rule != Right.Rule)
+ return Left.Rule < Right.Rule;
+ if (Left.Category != Right.Category)
+ return Left.Category < Right.Category;
+ return Left.Message < Right.Message;
+ });
+ auto Updated = rcc::tooling::updateReproducerDiagnostics(Command.ReproducerPath,
+ std::move(FailureDiagnostics));
+ if (!Updated) {
+ return rcc::makeUnexpected(ErrorCode::ReproducerError,
+ "backend failed with '" + std::string(Run.error().message()) +
+ "' and reproducer diagnostic update failed with '" +
+ std::string(Updated.error().message()) + "'");
+ }
+ }
+ return std::unexpected(Run.error());
+ }
+ printSnapshots(*Run);
+ if (Command.PrintActions)
+ printActions(*Run);
+ if (Command.DiscardOutput)
+ return {};
+ return writeOutput(Command.OutputPath, Run->Output);
+}
+
+} // namespace
+
+int main(int ArgumentCount, char *Arguments[]) {
+ rcc::initializeAllTargets();
+ auto DefaultTarget = rcc::TargetRegistry::defaultTarget();
+ if (!DefaultTarget) {
+ std::cerr << "rcc-backend-mir: " << DefaultTarget.error().message() << '\n';
+ return 1;
+ }
+ auto DefaultInvocation = rcc::tooling::defaultBackendInvocation(**DefaultTarget);
+ if (!DefaultInvocation) {
+ std::cerr << "rcc-backend-mir: " << DefaultInvocation.error().message() << '\n';
+ return 1;
+ }
+ auto Command = parseCommandLine(ArgumentCount, Arguments, std::move(*DefaultInvocation));
+ if (!Command) {
+ std::cerr << "rcc-backend-mir: " << Command.error().message() << '\n';
+ printUsage(std::cerr);
+ return 1;
+ }
+ if (Command->ShowHelp) {
+ printUsage(std::cout);
+ return 0;
+ }
+ auto Result = runCommand(*Command);
+ if (!Result) {
+ if (Result.error().code() == ErrorCode::ParseError) {
+ std::cerr << Result.error().message() << '\n';
+ return 1;
+ }
+ std::cerr << "rcc-backend-mir: " << rcc::getErrorCodeName(Result.error().code()) << ": "
+ << Result.error().message() << '\n';
+ return 1;
+ }
+ return 0;
+}
diff --git a/backend/tools/MIRTool/MIRToolOperationsTest.cmake b/backend/tools/MIRTool/MIRToolOperationsTest.cmake
new file mode 100644
index 0000000..ff03619
--- /dev/null
+++ b/backend/tools/MIRTool/MIRToolOperationsTest.cmake
@@ -0,0 +1,275 @@
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+if(NOT DEFINED TOOL OR NOT DEFINED INPUT OR NOT DEFINED WORK_DIRECTORY)
+ message(FATAL_ERROR "MIR tool operations test is missing a required path")
+endif()
+
+file(REMOVE_RECURSE "${WORK_DIRECTORY}")
+file(MAKE_DIRECTORY "${WORK_DIRECTORY}")
+
+set(StandardInputMIR "${WORK_DIRECTORY}/stdin.mir")
+execute_process(
+ COMMAND "${TOOL}" --verify - -o "${StandardInputMIR}"
+ INPUT_FILE "${INPUT}"
+ RESULT_VARIABLE StandardInputResult
+ ERROR_VARIABLE StandardInputDiagnostics
+)
+if(NOT StandardInputResult EQUAL 0)
+ message(FATAL_ERROR "standard-input MIR parsing failed: ${StandardInputDiagnostics}")
+endif()
+file(SIZE "${StandardInputMIR}" StandardInputSize)
+if(StandardInputSize EQUAL 0)
+ message(FATAL_ERROR "standard-input MIR parsing produced no canonical output")
+endif()
+
+set(CacheDirectory "${WORK_DIRECTORY}/cache")
+set(CachedAssembly "${WORK_DIRECTORY}/cached.s")
+set(ReloadedAssembly "${WORK_DIRECTORY}/reloaded.s")
+execute_process(
+ COMMAND
+ "${TOOL}" "${INPUT}" -filetype=asm --cache-dir "${CacheDirectory}"
+ --cache-policy write -o "${CachedAssembly}"
+ RESULT_VARIABLE CacheWriteResult
+ ERROR_VARIABLE CacheWriteDiagnostics
+)
+if(NOT CacheWriteResult EQUAL 0)
+ message(FATAL_ERROR "cache write failed: ${CacheWriteDiagnostics}")
+endif()
+execute_process(
+ COMMAND
+ "${TOOL}" "${INPUT}" -filetype=asm --cache-dir "${CacheDirectory}"
+ --cache-policy read -o "${ReloadedAssembly}"
+ RESULT_VARIABLE CacheReadResult
+ ERROR_VARIABLE CacheReadDiagnostics
+)
+if(NOT CacheReadResult EQUAL 0)
+ message(FATAL_ERROR "cache hit reload failed: ${CacheReadDiagnostics}")
+endif()
+execute_process(
+ COMMAND "${CMAKE_COMMAND}" -E compare_files "${CachedAssembly}" "${ReloadedAssembly}"
+ RESULT_VARIABLE CacheComparisonResult
+)
+if(NOT CacheComparisonResult EQUAL 0)
+ message(FATAL_ERROR "cache hit changed the artifact bytes")
+endif()
+
+execute_process(
+ COMMAND
+ "${TOOL}" "${INPUT}" -filetype=asm --cache-dir
+ "${WORK_DIRECTORY}/empty-cache" --cache-policy read -o "${WORK_DIRECTORY}/miss.s"
+ RESULT_VARIABLE CacheMissResult
+ ERROR_VARIABLE CacheMissDiagnostics
+)
+if(CacheMissResult EQUAL 0 OR NOT CacheMissDiagnostics MATCHES "cache miss")
+ message(FATAL_ERROR "strict cache read did not report a cache miss")
+endif()
+
+file(GLOB_RECURSE CachedArtifacts "${CacheDirectory}/artifact.s")
+list(LENGTH CachedArtifacts CachedArtifactCount)
+if(NOT CachedArtifactCount EQUAL 1)
+ message(FATAL_ERROR "cache publication did not create exactly one assembly artifact")
+endif()
+list(GET CachedArtifacts 0 CachedArtifact)
+file(WRITE "${CachedArtifact}" "corrupt")
+execute_process(
+ COMMAND
+ "${TOOL}" "${INPUT}" -filetype=asm --cache-dir "${CacheDirectory}"
+ --cache-policy read -o "${WORK_DIRECTORY}/corrupt.s"
+ RESULT_VARIABLE CorruptCacheResult
+ ERROR_VARIABLE CorruptCacheDiagnostics
+)
+if(CorruptCacheResult EQUAL 0 OR NOT CorruptCacheDiagnostics MATCHES "cache miss")
+ message(FATAL_ERROR "strict cache reload accepted a corrupted artifact")
+endif()
+
+set(SuccessReproducer "${WORK_DIRECTORY}/success-reproducer")
+set(SuccessAssembly "${WORK_DIRECTORY}/success.s")
+set(ReplayedAssembly "${WORK_DIRECTORY}/success-replayed.s")
+execute_process(
+ COMMAND
+ "${TOOL}" "${INPUT}" -filetype=asm --crash-reproducer "${SuccessReproducer}"
+ -o "${SuccessAssembly}"
+ RESULT_VARIABLE ReproducerWriteResult
+ ERROR_VARIABLE ReproducerWriteDiagnostics
+)
+if(NOT ReproducerWriteResult EQUAL 0)
+ message(FATAL_ERROR "successful reproducer publication failed: ${ReproducerWriteDiagnostics}")
+endif()
+execute_process(
+ COMMAND "${TOOL}" --reproduce "${SuccessReproducer}" -o "${ReplayedAssembly}"
+ RESULT_VARIABLE ReproducerReplayResult
+ ERROR_VARIABLE ReproducerReplayDiagnostics
+)
+if(NOT ReproducerReplayResult EQUAL 0)
+ message(FATAL_ERROR "successful reproducer replay failed: ${ReproducerReplayDiagnostics}")
+endif()
+execute_process(
+ COMMAND "${CMAKE_COMMAND}" -E compare_files "${SuccessAssembly}" "${ReplayedAssembly}"
+ RESULT_VARIABLE ReproducerComparisonResult
+)
+if(NOT ReproducerComparisonResult EQUAL 0)
+ message(FATAL_ERROR "reproducer replay changed the artifact bytes")
+endif()
+
+set(RegBankInput "${WORK_DIRECTORY}/regbank.mir")
+execute_process(
+ COMMAND "${TOOL}" "${INPUT}" -stop-after=regbankselect -o "${RegBankInput}"
+ RESULT_VARIABLE RegBankResult
+ ERROR_VARIABLE RegBankDiagnostics
+)
+if(NOT RegBankResult EQUAL 0)
+ message(FATAL_ERROR "register-bank seed production failed: ${RegBankDiagnostics}")
+endif()
+
+set(FailureReproducer "${WORK_DIRECTORY}/failure-reproducer")
+execute_process(
+ COMMAND
+ "${TOOL}" "${RegBankInput}" -run-pass instruction-select
+ --fuel 0 --crash-reproducer "${FailureReproducer}" -o "${WORK_DIRECTORY}/failure.mir"
+ RESULT_VARIABLE FailureResult
+ ERROR_VARIABLE FailureDiagnostics
+)
+if(FailureResult EQUAL 0 OR
+ NOT FailureDiagnostics MATCHES "required selection action was rejected")
+ message(FATAL_ERROR "zero Action fuel did not produce the expected failure")
+endif()
+file(READ "${FailureReproducer}/manifest.yaml" FailureManifest)
+if(NOT FailureManifest MATCHES "instruction-select" OR
+ NOT FailureManifest MATCHES "required selection action was rejected")
+ message(FATAL_ERROR "failure reproducer did not record structured pass diagnostics")
+endif()
+execute_process(
+ COMMAND "${TOOL}" --reproduce "${FailureReproducer}" -o "${WORK_DIRECTORY}/replay.mir"
+ RESULT_VARIABLE FailureReplayResult
+ ERROR_VARIABLE FailureReplayDiagnostics
+)
+if(FailureReplayResult EQUAL 0 OR
+ NOT FailureReplayDiagnostics MATCHES "required selection action was rejected")
+ message(FATAL_ERROR "failure reproducer did not replay the original failure")
+endif()
+
+set(ActionOutput "${WORK_DIRECTORY}/selected.mir")
+execute_process(
+ COMMAND
+ "${TOOL}" "${RegBankInput}" -run-pass instruction-select
+ --print-actions -o "${ActionOutput}"
+ RESULT_VARIABLE ActionResult
+ ERROR_VARIABLE ActionDiagnostics
+)
+if(NOT ActionResult EQUAL 0 OR NOT ActionDiagnostics MATCHES "accepted instruction-select")
+ message(FATAL_ERROR "successful Action execution did not report stable decisions")
+endif()
+
+file(READ "${INPUT}" ReducerSeedText)
+string(REPLACE
+ " $x10 = COPY %2\n"
+ " %3:gprb(i64) = G_CONSTANT 99\n $x10 = COPY %2\n"
+ ReducerSeedText
+ "${ReducerSeedText}"
+)
+set(ReducerSeed "${WORK_DIRECTORY}/reducer-seed.mir")
+set(CanonicalReducerSeed "${WORK_DIRECTORY}/reducer-seed.canonical.mir")
+set(ReducedMIR "${WORK_DIRECTORY}/reduced.mir")
+file(WRITE "${ReducerSeed}" "${ReducerSeedText}")
+execute_process(
+ COMMAND "${TOOL}" --verify "${ReducerSeed}" -o "${CanonicalReducerSeed}"
+ RESULT_VARIABLE ReducerSeedResult
+ ERROR_VARIABLE ReducerSeedDiagnostics
+)
+if(NOT ReducerSeedResult EQUAL 0)
+ message(FATAL_ERROR "reducer seed failed canonicalization: ${ReducerSeedDiagnostics}")
+endif()
+execute_process(
+ COMMAND "${TOOL}" --reduce validity "${ReducerSeed}" -o "${ReducedMIR}"
+ RESULT_VARIABLE ValidityReducerResult
+ ERROR_VARIABLE ValidityReducerDiagnostics
+)
+if(NOT ValidityReducerResult EQUAL 0)
+ message(FATAL_ERROR "validity reduction failed: ${ValidityReducerDiagnostics}")
+endif()
+file(SIZE "${CanonicalReducerSeed}" CanonicalReducerSeedSize)
+file(SIZE "${ReducedMIR}" ReducedMIRSize)
+if(NOT ReducedMIRSize LESS CanonicalReducerSeedSize)
+ message(FATAL_ERROR "validity reducer did not make the canonical seed smaller")
+endif()
+execute_process(
+ COMMAND "${TOOL}" --verify "${ReducedMIR}"
+ RESULT_VARIABLE ReducedVerifyResult
+ ERROR_VARIABLE ReducedVerifyDiagnostics
+)
+if(NOT ReducedVerifyResult EQUAL 0)
+ message(FATAL_ERROR "validity reducer produced invalid MIR: ${ReducedVerifyDiagnostics}")
+endif()
+
+set(FailureReducedMIR "${WORK_DIRECTORY}/failure-reduced.mir")
+execute_process(
+ COMMAND
+ "${TOOL}" --reduce failure "${RegBankInput}" -run-pass instruction-select --fuel 0 -o
+ "${FailureReducedMIR}"
+ RESULT_VARIABLE FailureReducerResult
+ ERROR_VARIABLE FailureReducerDiagnostics
+)
+if(NOT FailureReducerResult EQUAL 0)
+ message(FATAL_ERROR "failure reduction failed: ${FailureReducerDiagnostics}")
+endif()
+execute_process(
+ COMMAND
+ "${TOOL}" "${FailureReducedMIR}" -run-pass instruction-select
+ --fuel 0 -o "${WORK_DIRECTORY}/failure-reduced-output.mir"
+ RESULT_VARIABLE ReducedFailureResult
+ ERROR_VARIABLE ReducedFailureDiagnostics
+)
+if(ReducedFailureResult EQUAL 0 OR
+ NOT ReducedFailureDiagnostics MATCHES "required selection action was rejected")
+ message(FATAL_ERROR "failure reducer did not preserve its interestingness condition")
+endif()
+
+set(FirstFuzz "${WORK_DIRECTORY}/fuzz-1.txt")
+set(SecondFuzz "${WORK_DIRECTORY}/fuzz-2.txt")
+execute_process(
+ COMMAND
+ "${TOOL}" --fuzz all --fuzz-iterations 2 --seed 123 "${INPUT}" -o
+ "${FirstFuzz}"
+ RESULT_VARIABLE FirstFuzzResult
+ ERROR_VARIABLE FirstFuzzDiagnostics
+)
+if(NOT FirstFuzzResult EQUAL 0)
+ message(FATAL_ERROR "layered fuzz execution failed: ${FirstFuzzDiagnostics}")
+endif()
+execute_process(
+ COMMAND
+ "${TOOL}" --fuzz all --fuzz-iterations 2 --seed 123 "${INPUT}" -o
+ "${SecondFuzz}"
+ RESULT_VARIABLE SecondFuzzResult
+ ERROR_VARIABLE SecondFuzzDiagnostics
+)
+if(NOT SecondFuzzResult EQUAL 0)
+ message(FATAL_ERROR "repeated layered fuzz execution failed: ${SecondFuzzDiagnostics}")
+endif()
+execute_process(
+ COMMAND "${CMAKE_COMMAND}" -E compare_files "${FirstFuzz}" "${SecondFuzz}"
+ RESULT_VARIABLE FuzzComparisonResult
+)
+if(NOT FuzzComparisonResult EQUAL 0)
+ message(FATAL_ERROR "layered fuzz accounting is not deterministic")
+endif()
+file(READ "${FirstFuzz}" FuzzText)
+if(NOT FuzzText MATCHES "seed=123 executed=11" OR
+ NOT FuzzText MATCHES "parser: executed=2" OR
+ NOT FuzzText MATCHES "builder: executed=2" OR
+ NOT FuzzText MATCHES "property-stage: executed=2" OR
+ NOT FuzzText MATCHES "mutation: executed=2" OR
+ NOT FuzzText MATCHES "pipeline: executed=2" OR
+ NOT FuzzText MATCHES "semantics: executed=1")
+ message(FATAL_ERROR "layered fuzz output does not account for all six layers")
+endif()
+execute_process(
+ COMMAND "${TOOL}" --fuzz parser,parser "${INPUT}"
+ RESULT_VARIABLE DuplicateFuzzResult
+ ERROR_VARIABLE DuplicateFuzzDiagnostics
+)
+if(DuplicateFuzzResult EQUAL 0 OR NOT DuplicateFuzzDiagnostics MATCHES "must be unique")
+ message(FATAL_ERROR "CLI fuzzing accepted duplicate layers")
+endif()
+
+file(REMOVE_RECURSE "${WORK_DIRECTORY}")
diff --git a/backend/tools/MIRTool/MIRToolOutputTest.cmake b/backend/tools/MIRTool/MIRToolOutputTest.cmake
new file mode 100644
index 0000000..00840bc
--- /dev/null
+++ b/backend/tools/MIRTool/MIRToolOutputTest.cmake
@@ -0,0 +1,119 @@
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+if(NOT DEFINED TOOL OR NOT DEFINED INPUT OR NOT DEFINED OUTPUT OR
+ NOT DEFINED OUTPUT_KIND)
+ message(FATAL_ERROR "MIR tool output test is missing a required argument")
+endif()
+
+get_filename_component(OutputDirectory "${OUTPUT}" DIRECTORY)
+file(MAKE_DIRECTORY "${OutputDirectory}")
+set(RepeatedOutput "${OUTPUT}.repeat")
+
+execute_process(
+ COMMAND "${TOOL}" "${INPUT}" -filetype "${OUTPUT_KIND}" -o "${OUTPUT}"
+ RESULT_VARIABLE FirstResult
+ ERROR_VARIABLE FirstDiagnostics
+)
+if(NOT FirstResult EQUAL 0)
+ message(FATAL_ERROR "${OUTPUT_KIND} production failed: ${FirstDiagnostics}")
+endif()
+execute_process(
+ COMMAND "${TOOL}" "${INPUT}" -filetype "${OUTPUT_KIND}" -o "${RepeatedOutput}"
+ RESULT_VARIABLE SecondResult
+ ERROR_VARIABLE SecondDiagnostics
+)
+if(NOT SecondResult EQUAL 0)
+ message(FATAL_ERROR "repeated ${OUTPUT_KIND} production failed: ${SecondDiagnostics}")
+endif()
+execute_process(
+ COMMAND "${CMAKE_COMMAND}" -E compare_files "${OUTPUT}" "${RepeatedOutput}"
+ RESULT_VARIABLE ComparisonResult
+)
+if(NOT ComparisonResult EQUAL 0)
+ message(FATAL_ERROR "${OUTPUT_KIND} output is not deterministic")
+endif()
+
+if(OUTPUT_KIND STREQUAL "asm")
+ file(READ "${OUTPUT}" Assembly)
+ if(NOT Assembly MATCHES "\\.attribute arch, \"rv64gc\"" OR
+ NOT Assembly MATCHES "jalr x0, x1, 0")
+ message(FATAL_ERROR "assembly output is missing required RV64GC content")
+ endif()
+ if(INPUT MATCHES "RISCVDataPipeline" AND
+ (NOT Assembly MATCHES "\\.LCPI[0-9]+_0:" OR
+ NOT Assembly MATCHES "\\.LJTI[0-9]+_0:"))
+ message(FATAL_ERROR "assembly output is missing constant-pool or jump-table data")
+ endif()
+ if(INPUT MATCHES "RISCVStackFramePipeline" AND
+ (NOT Assembly MATCHES "addi x2, x2, -32" OR
+ NOT Assembly MATCHES "addi x2, x2, 32"))
+ message(FATAL_ERROR "assembly output does not balance its stack frame")
+ endif()
+ if(DEFINED LLVM_MC AND NOT LLVM_MC STREQUAL "LLVM_MC-NOTFOUND" AND
+ NOT LLVM_MC STREQUAL "")
+ execute_process(
+ COMMAND "${LLVM_MC}" -triple=riscv64-unknown-linux-gnu -filetype=null "${OUTPUT}"
+ RESULT_VARIABLE AssemblerResult
+ ERROR_VARIABLE AssemblerDiagnostics
+ )
+ if(NOT AssemblerResult EQUAL 0)
+ message(FATAL_ERROR "LLVM MC rejected MIR tool assembly: ${AssemblerDiagnostics}")
+ endif()
+ endif()
+elseif(OUTPUT_KIND STREQUAL "obj")
+ file(READ "${OUTPUT}" ObjectMagic OFFSET 0 LIMIT 4 HEX)
+ if(NOT ObjectMagic STREQUAL "7f454c46")
+ message(FATAL_ERROR "object output is not an ELF file")
+ endif()
+ if(DEFINED LLVM_READELF AND NOT LLVM_READELF STREQUAL "LLVM_READELF-NOTFOUND" AND
+ NOT LLVM_READELF STREQUAL "")
+ execute_process(
+ COMMAND "${LLVM_READELF}" -h -S -s -r "${OUTPUT}"
+ RESULT_VARIABLE ReadELFResult
+ OUTPUT_VARIABLE ReadELFOutput
+ ERROR_VARIABLE ReadELFDiagnostics
+ )
+ if(NOT ReadELFResult EQUAL 0)
+ message(FATAL_ERROR "LLVM readelf rejected MIR tool object: ${ReadELFDiagnostics}")
+ endif()
+ if(NOT ReadELFOutput MATCHES "Type:[ \t]+REL" OR
+ NOT ReadELFOutput MATCHES "Machine:[ \t]+RISC-V" OR
+ NOT ReadELFOutput MATCHES "Flags:[ \t]+0x5" OR
+ NOT ReadELFOutput MATCHES "\\.text" OR
+ NOT ReadELFOutput MATCHES "\\.eh_frame" OR
+ NOT ReadELFOutput MATCHES "\\.riscv\\.attributes")
+ message(FATAL_ERROR "object output is missing required RV64GC ELF metadata")
+ endif()
+ if(INPUT MATCHES "RISCVDataPipeline" AND
+ (NOT ReadELFOutput MATCHES "\\.rodata" OR
+ NOT ReadELFOutput MATCHES "\\.rela\\.text" OR
+ NOT ReadELFOutput MATCHES "R_RISCV_64"))
+ message(FATAL_ERROR "data object is missing data or relocation sections")
+ endif()
+ endif()
+ if(DEFINED LLVM_OBJDUMP AND NOT LLVM_OBJDUMP STREQUAL "LLVM_OBJDUMP-NOTFOUND" AND
+ NOT LLVM_OBJDUMP STREQUAL "")
+ execute_process(
+ COMMAND "${LLVM_OBJDUMP}" -dr --no-show-raw-insn "${OUTPUT}"
+ RESULT_VARIABLE ObjdumpResult
+ OUTPUT_VARIABLE ObjdumpOutput
+ ERROR_VARIABLE ObjdumpDiagnostics
+ )
+ if(NOT ObjdumpResult EQUAL 0)
+ message(FATAL_ERROR "LLVM objdump rejected MIR tool object: ${ObjdumpDiagnostics}")
+ endif()
+ if(NOT ObjdumpOutput MATCHES "file format elf64-littleriscv" OR
+ NOT ObjdumpOutput MATCHES "ret")
+ message(FATAL_ERROR "object output does not contain RV64GC function code")
+ endif()
+ if(INPUT MATCHES "RISCVStackFramePipeline" AND
+ (NOT ObjdumpOutput MATCHES "addi[ \t]+sp, sp, -0x20" OR
+ NOT ObjdumpOutput MATCHES "addi[ \t]+sp, sp, 0x20"))
+ message(FATAL_ERROR "stack-frame object does not balance its stack pointer")
+ endif()
+ endif()
+else()
+ message(FATAL_ERROR "MIR tool output test received an unknown output kind")
+endif()
+
+file(REMOVE "${OUTPUT}" "${RepeatedOutput}")
diff --git a/backend/tools/MIRTool/MIRToolStageTest.cmake b/backend/tools/MIRTool/MIRToolStageTest.cmake
new file mode 100644
index 0000000..0d3ef2e
--- /dev/null
+++ b/backend/tools/MIRTool/MIRToolStageTest.cmake
@@ -0,0 +1,200 @@
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+if(NOT DEFINED TOOL OR NOT DEFINED INPUT OR NOT DEFINED WORK_DIRECTORY)
+ message(FATAL_ERROR "MIR tool stage test is missing a required path")
+endif()
+
+file(REMOVE_RECURSE "${WORK_DIRECTORY}")
+file(MAKE_DIRECTORY "${WORK_DIRECTORY}")
+
+set(
+ Stages
+ prelegalizer-mir
+ legalized-mir
+ regbank-mir
+ selected-mir
+ post-ssa-mir
+ post-ra-mir
+ final-mir
+)
+set(
+ StageOptions
+ --verify
+ -stop-after=legalizer
+ -stop-after=regbankselect
+ -stop-after=instruction-select
+ -stop-after=twoaddressinstruction
+ -stop-after=PBQPAllocator
+ -stop-after=final-encodability-verifier
+)
+set(StageIndex 0)
+foreach(Stage IN LISTS Stages)
+ list(GET StageOptions ${StageIndex} StageOption)
+ set(StageOutput "${WORK_DIRECTORY}/${Stage}.mir")
+ set(RepeatedOutput "${WORK_DIRECTORY}/${Stage}.repeat.mir")
+ set(CanonicalOutput "${WORK_DIRECTORY}/${Stage}.canonical.mir")
+ execute_process(
+ COMMAND "${TOOL}" "${INPUT}" "${StageOption}" -o "${StageOutput}"
+ RESULT_VARIABLE StageResult
+ ERROR_VARIABLE StageDiagnostics
+ )
+ if(NOT StageResult EQUAL 0)
+ message(FATAL_ERROR "${Stage} production failed: ${StageDiagnostics}")
+ endif()
+ execute_process(
+ COMMAND "${TOOL}" "${INPUT}" "${StageOption}" -o "${RepeatedOutput}"
+ RESULT_VARIABLE RepeatedResult
+ ERROR_VARIABLE RepeatedDiagnostics
+ )
+ if(NOT RepeatedResult EQUAL 0)
+ message(FATAL_ERROR "repeated ${Stage} production failed: ${RepeatedDiagnostics}")
+ endif()
+ execute_process(
+ COMMAND "${CMAKE_COMMAND}" -E compare_files "${StageOutput}" "${RepeatedOutput}"
+ RESULT_VARIABLE DeterminismResult
+ )
+ if(NOT DeterminismResult EQUAL 0)
+ message(FATAL_ERROR "${Stage} output is not deterministic")
+ endif()
+ execute_process(
+ COMMAND "${TOOL}" --verify "${StageOutput}" -o "${CanonicalOutput}"
+ RESULT_VARIABLE VerifyResult
+ ERROR_VARIABLE VerifyDiagnostics
+ )
+ if(NOT VerifyResult EQUAL 0)
+ message(FATAL_ERROR "${Stage} output failed strict MIR reload: ${VerifyDiagnostics}")
+ endif()
+ execute_process(
+ COMMAND "${CMAKE_COMMAND}" -E compare_files "${StageOutput}" "${CanonicalOutput}"
+ RESULT_VARIABLE CanonicalResult
+ )
+ if(NOT CanonicalResult EQUAL 0)
+ message(FATAL_ERROR "${Stage} output is not canonical after strict reload")
+ endif()
+ math(EXPR StageIndex "${StageIndex} + 1")
+endforeach()
+
+set(LLVMContractOutput "${WORK_DIRECTORY}/llvm-contract.mir")
+execute_process(
+ COMMAND
+ "${TOOL}" --verify "${INPUT}" -mtriple=riscv64
+ -mattr=+m,+a,+f,+d,+c -relocation-model=pic -code-model=small -simplify-mir
+ -verify-machineinstrs -o "${LLVMContractOutput}"
+ RESULT_VARIABLE LLVMContractResult
+ ERROR_VARIABLE LLVMContractDiagnostics
+)
+if(NOT LLVMContractResult EQUAL 0)
+ message(FATAL_ERROR "LLVM llc-compatible invocation failed: ${LLVMContractDiagnostics}")
+endif()
+file(SIZE "${LLVMContractOutput}" LLVMContractOutputSize)
+if(LLVMContractOutputSize EQUAL 0)
+ message(FATAL_ERROR "LLVM llc-compatible invocation produced no MIR")
+endif()
+
+foreach(
+ RemovedOption
+ IN ITEMS
+ --features=+m
+ --output=-
+)
+ execute_process(
+ COMMAND "${TOOL}" "${INPUT}" "${RemovedOption}"
+ RESULT_VARIABLE RemovedOptionResult
+ ERROR_VARIABLE RemovedOptionDiagnostics
+ )
+ if(RemovedOptionResult EQUAL 0 OR
+ NOT RemovedOptionDiagnostics MATCHES "unknown command-line option")
+ message(FATAL_ERROR "removed option ${RemovedOption} remains accepted")
+ endif()
+endforeach()
+
+set(PassList "${WORK_DIRECTORY}/passes.txt")
+execute_process(
+ COMMAND "${TOOL}" --list-passes "${INPUT}" -o "${PassList}"
+ RESULT_VARIABLE ListResult
+ ERROR_VARIABLE ListDiagnostics
+)
+if(NOT ListResult EQUAL 0)
+ message(FATAL_ERROR "pass listing failed: ${ListDiagnostics}")
+endif()
+file(READ "${PassList}" PassListText)
+foreach(RequiredPass IN ITEMS legalizer regbankselect instruction-select prologepilog)
+ if(NOT PassListText MATCHES "(^|\n)${RequiredPass}(\n|$)")
+ message(FATAL_ERROR "canonical pass list is missing ${RequiredPass}")
+ endif()
+endforeach()
+
+set(LegalizedOutput "${WORK_DIRECTORY}/run-pass.mir")
+execute_process(
+ COMMAND
+ "${TOOL}" "${INPUT}" -run-pass legalizer -o
+ "${LegalizedOutput}"
+ RESULT_VARIABLE RunPassResult
+ ERROR_VARIABLE RunPassDiagnostics
+)
+if(NOT RunPassResult EQUAL 0)
+ message(FATAL_ERROR "single-pass execution failed: ${RunPassDiagnostics}")
+endif()
+
+set(RegBankOutput "${WORK_DIRECTORY}/passes.mir")
+execute_process(
+ COMMAND
+ "${TOOL}" "${INPUT}" -passes legalizer,regbankselect -o
+ "${RegBankOutput}"
+ RESULT_VARIABLE PassesResult
+ ERROR_VARIABLE PassesDiagnostics
+)
+if(NOT PassesResult EQUAL 0)
+ message(FATAL_ERROR "explicit pass pipeline failed: ${PassesDiagnostics}")
+endif()
+
+set(StoppedOutput "${WORK_DIRECTORY}/stopped.mir")
+execute_process(
+ COMMAND
+ "${TOOL}" "${INPUT}" -stop-after legalizer -o
+ "${StoppedOutput}"
+ RESULT_VARIABLE StopResult
+ ERROR_VARIABLE StopDiagnostics
+)
+if(NOT StopResult EQUAL 0)
+ message(FATAL_ERROR "stop-after control failed: ${StopDiagnostics}")
+endif()
+execute_process(
+ COMMAND "${TOOL}" --verify "${StoppedOutput}"
+ RESULT_VARIABLE StopVerifyResult
+ ERROR_VARIABLE StopVerifyDiagnostics
+)
+if(NOT StopVerifyResult EQUAL 0)
+ message(FATAL_ERROR "stop-after output failed strict reload: ${StopVerifyDiagnostics}")
+endif()
+
+execute_process(
+ COMMAND
+ "${TOOL}" "${INPUT}" -filetype=null -print-before legalizer -print-after legalizer
+ RESULT_VARIABLE PrintResult
+ ERROR_VARIABLE PrintDiagnostics
+)
+if(NOT PrintResult EQUAL 0)
+ message(FATAL_ERROR "pass snapshot controls failed: ${PrintDiagnostics}")
+endif()
+if(NOT PrintDiagnostics MATCHES "[*][*][*] prelegalizer-mir legalizer#1" OR
+ NOT PrintDiagnostics MATCHES "[*][*][*] legalized-mir legalizer#1")
+ message(FATAL_ERROR "before/after pass snapshots are incomplete")
+endif()
+
+execute_process(
+ COMMAND
+ "${TOOL}" "${INPUT}" -filetype=null --stage-snapshots
+ RESULT_VARIABLE SnapshotResult
+ ERROR_VARIABLE SnapshotDiagnostics
+)
+if(NOT SnapshotResult EQUAL 0)
+ message(FATAL_ERROR "stage snapshot execution failed: ${SnapshotDiagnostics}")
+endif()
+foreach(Stage IN LISTS Stages)
+ if(NOT SnapshotDiagnostics MATCHES "[*][*][*] ${Stage}")
+ message(FATAL_ERROR "stage snapshots are missing ${Stage}")
+ endif()
+endforeach()
+
+file(REMOVE_RECURSE "${WORK_DIRECTORY}")