/* ************************************************************************ * Copyright (C) 2026 Advanced Micro Devices, Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * * ************************************************************************ */ /// stinkytofu-check: LLVM FileCheck-style test runner for stinkytofu-opt. /// /// Reads a .stir file containing: /// # RUN: %stinkytofu-opt --arch gfx1250 %s --SomePass --print-output /// # CHECK: expected_substring /// # CHECK-NEXT: must_be_on_next_line /// # CHECK-NOT: must_not_appear /// # XFAIL: the command is expected to exit non-zero; CHECK patterns still verified /// /// Executes the RUN command, captures stdout, and verifies CHECK directives. #include #include #include #ifdef _WIN32 #define popen _popen #define pclose _pclose #endif #include #include #include #include #include #include #include namespace { // Match a pattern against a line. Patterns can contain {{regex}} sections. // Plain text is matched literally, {{...}} sections are matched as regex. // Example: "ds_load_b128{{.*}}v[8:11]" matches " v[8:11] = ds_load_b128(v24)" bool matchPattern(const std::string& line, const std::string& pattern) { // If no {{ in pattern, use simple substring match. if (pattern.find("{{") == std::string::npos) return line.find(pattern) != std::string::npos; // Convert pattern to regex: literal parts are escaped, {{...}} becomes regex. std::string regexStr; size_t pos = 0; while (pos < pattern.size()) { size_t start = pattern.find("{{", pos); if (start == std::string::npos) { // Remaining is literal — escape regex special chars. for (size_t i = pos; i < pattern.size(); ++i) { if (std::string("[](){}.*+?^$\\|").find(pattern[i]) != std::string::npos) regexStr += '\\'; regexStr += pattern[i]; } break; } // Literal part before {{ for (size_t i = pos; i < start; ++i) { if (std::string("[](){}.*+?^$\\|").find(pattern[i]) != std::string::npos) regexStr += '\\'; regexStr += pattern[i]; } // Find closing }} size_t end = pattern.find("}}", start + 2); if (end == std::string::npos) break; // Malformed — treat rest as literal // Extract regex between {{ and }} regexStr += pattern.substr(start + 2, end - start - 2); pos = end + 2; } std::regex re(regexStr, std::regex::nosubs); return std::regex_search(line, re); } struct CheckDirective { enum Kind { CHECK, CHECK_NEXT, CHECK_SAME, CHECK_NOT, CHECK_DAG, CHECK_LABEL }; Kind kind; std::string pattern; int lineNum; }; // Parse # RUN:, # XFAIL, and # CHECK: directives from the test file. bool parseTestFile(const std::string& filename, std::string& runCmd, std::vector& checks, bool& xfail) { std::ifstream file(filename); if (!file.is_open()) { std::cerr << "Error: cannot open " << filename << "\n"; return false; } xfail = false; std::string line; int lineNum = 0; while (std::getline(file, line)) { ++lineNum; // Skip leading whitespace size_t pos = line.find_first_not_of(" \t"); if (pos == std::string::npos) continue; // Lines starting with # (comments in .stir) if (line[pos] != '#') continue; std::string rest = line.substr(pos + 1); pos = rest.find_first_not_of(" \t"); if (pos == std::string::npos) continue; rest = rest.substr(pos); if (rest.starts_with("XFAIL")) { xfail = true; } else if (rest.starts_with("RUN:")) { runCmd = rest.substr(4); // Trim leading whitespace pos = runCmd.find_first_not_of(" \t"); if (pos != std::string::npos) runCmd = runCmd.substr(pos); } else if (rest.starts_with("CHECK-LABEL:")) { std::string pat = rest.substr(12); pos = pat.find_first_not_of(" \t"); if (pos != std::string::npos) pat = pat.substr(pos); checks.push_back({CheckDirective::CHECK_LABEL, pat, lineNum}); } else if (rest.starts_with("CHECK-SAME:")) { std::string pat = rest.substr(11); pos = pat.find_first_not_of(" \t"); if (pos != std::string::npos) pat = pat.substr(pos); checks.push_back({CheckDirective::CHECK_SAME, pat, lineNum}); } else if (rest.starts_with("CHECK-NEXT:")) { std::string pat = rest.substr(11); pos = pat.find_first_not_of(" \t"); if (pos != std::string::npos) pat = pat.substr(pos); checks.push_back({CheckDirective::CHECK_NEXT, pat, lineNum}); } else if (rest.starts_with("CHECK-DAG:")) { std::string pat = rest.substr(10); pos = pat.find_first_not_of(" \t"); if (pos != std::string::npos) pat = pat.substr(pos); checks.push_back({CheckDirective::CHECK_DAG, pat, lineNum}); } else if (rest.starts_with("CHECK-NOT:")) { std::string pat = rest.substr(10); pos = pat.find_first_not_of(" \t"); if (pos != std::string::npos) pat = pat.substr(pos); checks.push_back({CheckDirective::CHECK_NOT, pat, lineNum}); } else if (rest.starts_with("CHECK:")) { std::string pat = rest.substr(6); pos = pat.find_first_not_of(" \t"); if (pos != std::string::npos) pat = pat.substr(pos); checks.push_back({CheckDirective::CHECK, pat, lineNum}); } } return !runCmd.empty(); } // Execute a command and capture stdout. Returns the exit status via out-param. std::string executeCommand(const std::string& cmd, int& exitStatus) { FILE* pipe = popen(cmd.c_str(), "r"); if (!pipe) { std::cerr << "Error: failed to execute: " << cmd << "\n"; exitStatus = -1; return {}; } std::string result; char buffer[4096]; while (fgets(buffer, sizeof(buffer), pipe)) result += buffer; exitStatus = pclose(pipe); return result; } // Substitute %s and %stinkytofu-opt in the RUN command. std::string substituteVars(const std::string& cmd, const std::string& testFile, const std::string& optBinary) { std::string result = cmd; // Replace %stinkytofu-opt first (longer match before %s) size_t pos; while ((pos = result.find("%stinkytofu-opt")) != std::string::npos) result.replace(pos, 15, optBinary); while ((pos = result.find("%s")) != std::string::npos) result.replace(pos, 2, testFile); return result; } // Verify CHECK directives against output lines. bool verifyChecks(const std::vector& outputLines, const std::vector& checks, const std::string& testFile) { size_t outputIdx = 0; bool ok = true; for (size_t i = 0; i < checks.size(); ++i) { const auto& chk = checks[i]; if (chk.kind == CheckDirective::CHECK_LABEL) { // Reset scan position — find pattern from the beginning. bool found = false; for (size_t j = 0; j < outputLines.size(); ++j) { if (matchPattern(outputLines[j], chk.pattern)) { outputIdx = j + 1; found = true; break; } } if (!found) { std::cerr << testFile << ":" << chk.lineNum << ": CHECK-LABEL failed: pattern not found\n"; std::cerr << " expected: " << chk.pattern << "\n"; ok = false; } continue; } if (chk.kind == CheckDirective::CHECK_SAME) { // Match on the same line as the previous CHECK (outputIdx-1). if (outputIdx == 0) { std::cerr << testFile << ":" << chk.lineNum << ": CHECK-SAME failed: no previous CHECK\n"; ok = false; continue; } size_t prevIdx = outputIdx - 1; if (!matchPattern(outputLines[prevIdx], chk.pattern)) { std::cerr << testFile << ":" << chk.lineNum << ": CHECK-SAME failed at output line " << (prevIdx + 1) << "\n"; std::cerr << " expected: " << chk.pattern << "\n"; std::cerr << " got: " << outputLines[prevIdx] << "\n"; ok = false; } continue; } if (chk.kind == CheckDirective::CHECK_NOT) { // Pattern must NOT appear before the next CHECK/CHECK-LABEL. // Find scan end: the output line matched by the next non-NOT directive. size_t scanEnd = outputLines.size(); for (size_t j = i + 1; j < checks.size(); ++j) { if (checks[j].kind == CheckDirective::CHECK_NOT) continue; // For CHECK-LABEL, find its match position to bound the scan. if (checks[j].kind == CheckDirective::CHECK_LABEL) { for (size_t k = 0; k < outputLines.size(); ++k) { if (matchPattern(outputLines[k], checks[j].pattern)) { scanEnd = k; break; } } } else { // For other CHECK directives, scan forward from current pos. for (size_t k = outputIdx; k < outputLines.size(); ++k) { if (matchPattern(outputLines[k], checks[j].pattern)) { scanEnd = k; break; } } } break; } for (size_t j = outputIdx; j < scanEnd; ++j) { if (matchPattern(outputLines[j], chk.pattern)) { std::cerr << testFile << ":" << chk.lineNum << ": CHECK-NOT failed: found '" << chk.pattern << "' at output line " << (j + 1) << "\n"; std::cerr << " output: " << outputLines[j] << "\n"; ok = false; break; } } continue; } if (chk.kind == CheckDirective::CHECK_NEXT) { if (outputIdx >= outputLines.size()) { std::cerr << testFile << ":" << chk.lineNum << ": CHECK-NEXT failed: no more output\n"; std::cerr << " expected: " << chk.pattern << "\n"; ok = false; continue; } if (!matchPattern(outputLines[outputIdx], chk.pattern)) { std::cerr << testFile << ":" << chk.lineNum << ": CHECK-NEXT failed at output line " << (outputIdx + 1) << "\n"; std::cerr << " expected: " << chk.pattern << "\n"; std::cerr << " got: " << outputLines[outputIdx] << "\n"; ok = false; } ++outputIdx; continue; } if (chk.kind == CheckDirective::CHECK_DAG) { // Collect consecutive CHECK-DAG directives into a group. std::vector dagGroup; dagGroup.push_back(&chk); while (i + 1 < checks.size() && checks[i + 1].kind == CheckDirective::CHECK_DAG) dagGroup.push_back(&checks[++i]); // All patterns in the group must match within the output range // [outputIdx, outputIdx + scanRange), in any order. // Each pattern consumes one output line. std::vector matched(dagGroup.size(), false); size_t startIdx = outputIdx; size_t endIdx = outputLines.size(); for (size_t g = 0; g < dagGroup.size(); ++g) { bool found = false; for (size_t j = startIdx; j < endIdx; ++j) { if (matchPattern(outputLines[j], dagGroup[g]->pattern)) { matched[g] = true; found = true; break; } } if (!found) { std::cerr << testFile << ":" << dagGroup[g]->lineNum << ": CHECK-DAG failed: pattern not found\n"; std::cerr << " expected: " << dagGroup[g]->pattern << "\n"; ok = false; } } // Advance outputIdx past all matched lines. // Find the furthest matched line. for (size_t j = startIdx; j < endIdx; ++j) { for (size_t g = 0; g < dagGroup.size(); ++g) { if (matched[g] && matchPattern(outputLines[j], dagGroup[g]->pattern)) { if (j + 1 > outputIdx) outputIdx = j + 1; } } } continue; } // CHECK: scan forward for the pattern. bool found = false; for (; outputIdx < outputLines.size(); ++outputIdx) { if (matchPattern(outputLines[outputIdx], chk.pattern)) { found = true; ++outputIdx; break; } } if (!found) { std::cerr << testFile << ":" << chk.lineNum << ": CHECK failed: pattern not found\n"; std::cerr << " expected: " << chk.pattern << "\n"; ok = false; } } return ok; } } // namespace int main(int argc, char** argv) { if (argc < 2) { std::cerr << "Usage: stinkytofu-check [--stinkytofu-opt ]\n"; return 1; } std::string testFile = argv[1]; std::string optBinary = "stinkytofu-opt"; // default for (int i = 2; i < argc; ++i) { if (std::string(argv[i]) == "--stinkytofu-opt" && i + 1 < argc) optBinary = argv[++i]; } // Parse test file std::string runCmd; std::vector checks; bool xfail = false; if (!parseTestFile(testFile, runCmd, checks, xfail)) { std::cerr << "Error: no RUN: directive found in " << testFile << "\n"; return 1; } if (checks.empty()) { std::cerr << "Error: no CHECK directives found in " << testFile << "\n"; return 1; } // Substitute variables and execute std::string cmd = substituteVars(runCmd, testFile, optBinary); int exitStatus = 0; std::string output = executeCommand(cmd, exitStatus); if (exitStatus != 0) { if (xfail) { std::cerr << testFile << ": note: command exited non-zero (expected by XFAIL)\n"; } else { std::cerr << "Warning: command exited with status " << exitStatus << "\n"; } } // Split output into lines std::vector outputLines; { std::istringstream stream(output); std::string line; while (std::getline(stream, line)) outputLines.push_back(line); } // Verify if (verifyChecks(outputLines, checks, testFile)) { std::cerr << testFile << ": PASSED\n"; return 0; } else { std::cerr << testFile << ": FAILED\n"; return 1; } }