diff options
Diffstat (limited to 'backend/tools')
| -rw-r--r-- | backend/tools/CMakeLists.txt | 3 | ||||
| -rw-r--r-- | backend/tools/MIRTool/CMakeLists.txt | 90 | ||||
| -rw-r--r-- | backend/tools/MIRTool/LLVMRunTestExecutor.py | 682 | ||||
| -rw-r--r-- | backend/tools/MIRTool/MIRToolMain.cpp | 1147 | ||||
| -rw-r--r-- | backend/tools/MIRTool/MIRToolOperationsTest.cmake | 275 | ||||
| -rw-r--r-- | backend/tools/MIRTool/MIRToolOutputTest.cmake | 119 | ||||
| -rw-r--r-- | backend/tools/MIRTool/MIRToolStageTest.cmake | 200 |
7 files changed, 2516 insertions, 0 deletions
diff --git a/backend/tools/CMakeLists.txt b/backend/tools/CMakeLists.txt new file mode 100644 index 0000000..da6b7f3 --- /dev/null +++ b/backend/tools/CMakeLists.txt @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: GPL-3.0-or-later + +add_subdirectory(MIRTool) 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}") |
