Files
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

691 lines
22 KiB
C++

/* ************************************************************************
* Copyright (C) 2025-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.
*
* ************************************************************************ */
/**
* @file PatternParserTest.cpp
* @brief Tests for the Pattern Parser
*
* This file tests the parsing of .pattern files including:
* - Valid pattern syntax
* - Error handling for invalid syntax
* - Edge cases (empty patterns, nested blocks, etc.)
*/
#include <gtest/gtest.h>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <string>
#include "stinkytofu/serialization/asm/PatternParser.hpp"
using namespace stinkytofu;
/**
* Test fixture for Pattern Parser tests
*/
class PatternParserTest : public ::testing::Test {
protected:
/**
* Helper to parse pattern from string
*/
std::vector<Pattern> parsePatternString(const std::string& input) {
// Create temporary file in platform temp directory
auto tmpPath = std::filesystem::temp_directory_path() / "test_pattern.pattern";
std::ofstream out(tmpPath);
out << input;
out.close();
// Parse it
auto patterns = parsePatternFile(tmpPath.string().c_str());
// Clean up
std::filesystem::remove(tmpPath);
return patterns;
}
/**
* Helper to parse pattern from string with diagnostics
*/
PatternParseResult parsePatternStringWithDiagnostics(const std::string& input) {
// Create temporary file in platform temp directory
auto tmpPath = std::filesystem::temp_directory_path() / "test_pattern_diag.pattern";
std::ofstream out(tmpPath);
out << input;
out.close();
// Parse it
auto result = parsePatternFileWithDiagnostics(tmpPath.string().c_str());
// Clean up
std::filesystem::remove(tmpPath);
return result;
}
};
// ============================================================================
// HAPPY PATH TESTS - Valid Patterns
// ============================================================================
TEST_F(PatternParserTest, ParsesSimplePeepholePattern) {
// Simplified test - just check pattern structure is recognized
const std::string input = R"(
peephole pattern AddZeroElimination {
match {
$add = v_add_f32 $dst, $src, 0.0
}
rewrite {
replace $add with $dst
}
}
)";
auto patterns = parsePatternString(input);
ASSERT_EQ(patterns.size(), 1);
EXPECT_EQ(patterns[0].type, PatternType::Peephole);
EXPECT_EQ(patterns[0].name, "AddZeroElimination");
// Match block should be parsed
EXPECT_FALSE(patterns[0].match.empty()) << "Match block should be parsed";
// NOTE: Current implementation may not fully parse rewrite blocks yet
// This test verifies the pattern structure is recognized without crashing
// TODO: When rewrite parsing is implemented, add:
// EXPECT_FALSE(patterns[0].rewrite.empty());
}
TEST_F(PatternParserTest, ParsesIntrinsicPattern) {
const std::string input = R"(
intrinsic ReluF32 {
arguments {
dest: vgpr
src: vgpr
}
body {
dest = v_max_f32(src, 0.0)
}
comment "ReLU activation"
python_binding true
}
)";
auto patterns = parsePatternString(input);
ASSERT_EQ(patterns.size(), 1);
EXPECT_EQ(patterns[0].type, PatternType::Intrinsic);
EXPECT_EQ(patterns[0].name, "ReluF32");
EXPECT_EQ(patterns[0].arguments.size(), 2);
EXPECT_EQ(patterns[0].comment, "ReLU activation");
EXPECT_TRUE(patterns[0].pythonBinding);
}
TEST_F(PatternParserTest, ParsesMultiplePatterns) {
const std::string input = R"(
peephole pattern Pattern1 {
match { $x = v_add_f32 $a, $b, $c }
rewrite { replace $x with v_add_f32 $a, $b, $c }
}
peephole pattern Pattern2 {
match { $y = v_mul_f32 $a, $b, $c }
rewrite { replace $y with v_mul_f32 $a, $b, $c }
}
)";
auto patterns = parsePatternString(input);
ASSERT_EQ(patterns.size(), 2);
EXPECT_EQ(patterns[0].name, "Pattern1");
EXPECT_EQ(patterns[1].name, "Pattern2");
}
TEST_F(PatternParserTest, ParsesPatternWithConstraints) {
const std::string input = R"(
peephole pattern ConstrainedPattern {
match {
$fma = v_fma_f32 $result, $a, $b, $c
$add = v_add_f32 $dst, $d, $result
}
constraints {
HasOneUse($result)
IsConstant($c)
}
rewrite {
replace [$fma, $add] with v_fma_f32 $dst, $a, $b, $c
}
}
)";
auto patterns = parsePatternString(input);
ASSERT_EQ(patterns.size(), 1);
EXPECT_FALSE(patterns[0].constraints.empty());
}
// ============================================================================
// ERROR PATH TESTS - Invalid Syntax
// ============================================================================
TEST_F(PatternParserTest, HandlesMissingBraces) {
const std::string input = R"(
peephole pattern BadPattern {
match {
$x = v_add_f32 $a, $b, $c
// Missing closing brace
}
)";
auto patterns = parsePatternString(input);
// Parser is lenient - accepts malformed input and continues
// This allows incremental development and better error recovery
// Validation happens at later stages
EXPECT_GE(patterns.size(), 0); // May succeed or fail
// If it succeeds, the pattern should have a name
if (!patterns.empty()) {
EXPECT_EQ(patterns[0].name, "BadPattern");
}
}
TEST_F(PatternParserTest, HandlesMissingPatternName) {
const std::string input = R"(
peephole pattern {
match { $x = v_add_f32 $a, $b, $c }
}
)";
auto patterns = parsePatternString(input);
// Parser is lenient - may accept patterns without explicit names
// Two acceptable behaviors:
// A) Return empty list (parsing failed due to missing name)
// B) Return pattern with empty/default name
if (patterns.empty()) {
// Parser rejected malformed pattern - acceptable
SUCCEED() << "Lenient parser rejected pattern without name";
} else {
// Parser accepted it with empty or default name - also acceptable
ASSERT_EQ(patterns.size(), 1);
// Name should be empty since none was provided
EXPECT_TRUE(patterns[0].name.empty() || patterns[0].name == "pattern")
<< "Pattern without explicit name should have empty or 'pattern' as name";
// Verify type was inferred correctly
EXPECT_EQ(patterns[0].type, PatternType::Peephole);
}
}
TEST_F(PatternParserTest, RejectsInvalidPatternType) {
const std::string input = R"(
invalid_type pattern MyPattern {
match { $x = v_add_f32 $a, $b, $c }
}
)";
auto patterns = parsePatternString(input);
EXPECT_TRUE(patterns.empty());
}
TEST_F(PatternParserTest, HandlesMissingMatchBlock) {
const std::string input = R"(
peephole pattern NoMatchBlock {
rewrite {
replace $x with v_add_f32 $a, $b, $c
}
}
)";
auto patterns = parsePatternString(input);
// Parser is lenient - may accept patterns without required blocks
// Validation of pattern completeness happens at usage time
if (patterns.empty()) {
// Parser rejected incomplete pattern - acceptable
SUCCEED() << "Parser rejected pattern without match block";
} else {
// Parser accepted incomplete pattern - also acceptable for lenient parsing
ASSERT_EQ(patterns.size(), 1);
EXPECT_EQ(patterns[0].name, "NoMatchBlock");
// Match block should be empty since it wasn't provided
EXPECT_TRUE(patterns[0].match.empty())
<< "Pattern without match block should have empty match field";
// NOTE: Rewrite block parsing may not be fully implemented
// For now, just verify the pattern structure was recognized
// TODO: When rewrite parsing is complete, add:
// EXPECT_FALSE(patterns[0].rewrite.empty());
}
}
TEST_F(PatternParserTest, HandlesDuplicatePatternNames) {
// Two patterns with the same name
const std::string input = R"(
peephole pattern DuplicateName {
match { $x = v_add_f32 $a, $b, $c }
rewrite { v_add_f32 $x, $a, $b }
}
peephole pattern DuplicateName {
match { $y = v_sub_f32 $d, $e, $f }
rewrite { v_sub_f32 $y, $d, $e }
}
)";
auto patterns = parsePatternString(input);
// Parser may accept both patterns (validation happens at registration time)
// or reject the duplicate
if (patterns.size() == 2) {
// Both patterns parsed - names may both be "DuplicateName"
EXPECT_EQ(patterns[0].name, "DuplicateName");
EXPECT_EQ(patterns[1].name, "DuplicateName");
// Semantic validation should catch duplicate names later
} else if (patterns.size() == 1) {
// Only first pattern parsed, second rejected
EXPECT_EQ(patterns[0].name, "DuplicateName");
}
// Key requirement: Doesn't crash on duplicate names
// Pattern registration phase should detect duplicates
}
TEST_F(PatternParserTest, HandlesInvalidIntrinsicSignature) {
// Intrinsic with malformed signature (missing closing paren)
const std::string input = R"(
intrinsic BadSignature {
comment: "Test"
signature: "v_add_f32("
cost: 4
}
)";
auto patterns = parsePatternString(input);
// Parser should handle malformed signature gracefully
if (patterns.empty()) {
SUCCEED() << "Parser rejected malformed signature";
} else {
// May parse with lenient rules
ASSERT_EQ(patterns.size(), 1);
EXPECT_EQ(patterns[0].type, PatternType::Intrinsic);
// Signature parsing/validation can happen later
}
// Key requirement: Doesn't crash on malformed signatures
}
TEST_F(PatternParserTest, HandlesMissingRequiredFields) {
// Intrinsic missing required fields (e.g., cost)
const std::string input = R"(
intrinsic IncompleteDef {
comment: "Missing cost field"
}
)";
auto patterns = parsePatternString(input);
// Parser may accept incomplete definitions (validation at usage)
if (patterns.empty()) {
SUCCEED() << "Parser rejected incomplete intrinsic";
} else {
// Lenient parser accepted it
ASSERT_EQ(patterns.size(), 1);
EXPECT_EQ(patterns[0].type, PatternType::Intrinsic);
// Required field validation happens at intrinsic registration
}
// Key requirement: Doesn't crash on missing fields
// Design decision: Accept incomplete patterns, validate at registration
}
TEST_F(PatternParserTest, HandlesInvalidKeywordNames) {
// Pattern with unknown/invalid keyword
const std::string input = R"(
peephole pattern TestPattern {
match { $x = v_add_f32 $a, $b, $c }
unknown_keyword { some stuff }
}
)";
auto patterns = parsePatternString(input);
// Parser should skip or error on unknown keywords
if (patterns.empty()) {
SUCCEED() << "Parser rejected unknown keyword";
} else {
// May parse and ignore unknown blocks
ASSERT_EQ(patterns.size(), 1);
EXPECT_EQ(patterns[0].name, "TestPattern");
EXPECT_FALSE(patterns[0].match.empty());
}
// Key requirement: Doesn't crash on unknown keywords
// Parser may skip unknown blocks or produce warnings
}
TEST_F(PatternParserTest, HandlesMismatchedBracesNesting) {
// Extra closing brace or missing opening brace
const std::string input = R"(
peephole pattern BraceError {
match { $x = v_add_f32 $a, $b }
}
}
)";
auto patterns = parsePatternString(input);
// Parser should detect brace mismatch
if (patterns.empty()) {
SUCCEED() << "Parser rejected mismatched braces";
} else {
// Lenient parser may have recovered
EXPECT_TRUE(true) << "Parser handled brace errors without crashing";
}
// Key requirement: Doesn't crash on brace mismatches
// Proper brace balancing is critical for parser stability
}
TEST_F(PatternParserTest, HandlesInvalidVariableNames) {
// Variable names with invalid syntax
const std::string input = R"(
peephole pattern BadVars {
match { $123invalid = v_add_f32 $-bad, $$double, $ok }
rewrite { v_mov_b32 $123invalid, $ok }
}
)";
auto patterns = parsePatternString(input);
// Parser may accept invalid variable names (semantic validation later)
if (patterns.empty()) {
SUCCEED() << "Parser rejected invalid variable names";
} else {
// Lenient parser accepted the pattern
ASSERT_EQ(patterns.size(), 1);
EXPECT_EQ(patterns[0].name, "BadVars");
// Variable name validation can happen during pattern matching
}
// Key requirement: Doesn't crash on invalid variable syntax
// Pattern matcher should validate variable names at match time
}
// ============================================================================
// EDGE CASE TESTS
// ============================================================================
TEST_F(PatternParserTest, HandlesEmptyFile) {
const std::string input = "";
auto patterns = parsePatternString(input);
EXPECT_TRUE(patterns.empty());
}
TEST_F(PatternParserTest, HandlesCommentsOnly) {
const std::string input = R"(
// This is a comment
/* This is a block comment */
# This might also be a comment depending on syntax
)";
auto patterns = parsePatternString(input);
EXPECT_TRUE(patterns.empty());
}
TEST_F(PatternParserTest, HandlesTrailingComma) {
const std::string input = R"(
intrinsic TestIntrinsic {
arguments {
a: vgpr,
b: vgpr, // Trailing comma
}
body {
a = v_add_f32(b, 0.0)
}
}
)";
auto patterns = parsePatternString(input);
// Should either accept or reject consistently
// TODO: Decide on trailing comma policy
ASSERT_FALSE(patterns.empty());
}
TEST_F(PatternParserTest, HandlesLongPatternNames) {
const std::string input = R"(
peephole pattern ThisIsAReallyLongPatternNameThatSomeoneDecidedToUseForSomeReason {
match { $x = v_add_f32 $a, $b, $c }
rewrite { replace $x with v_add_f32 $a, $b, $c }
}
)";
auto patterns = parsePatternString(input);
ASSERT_EQ(patterns.size(), 1);
EXPECT_GT(patterns[0].name.length(), 50);
}
TEST_F(PatternParserTest, HandlesUnicodeInComments) {
const std::string input = R"(
peephole pattern UnicodeTest {
match { $x = v_add_f32 $a, $b, $c }
rewrite {
// Comment with unicode: ??? ?? ??
replace $x with v_add_f32 $a, $b, $c
}
}
)";
auto patterns = parsePatternString(input);
// Should parse successfully, ignoring unicode in comments
ASSERT_EQ(patterns.size(), 1);
}
// ============================================================================
// PERFORMANCE TESTS
// ============================================================================
TEST_F(PatternParserTest, HandlesLargeFile) {
// Generate pattern file with 100 patterns
std::ostringstream input;
for (int i = 0; i < 100; ++i) {
input << "peephole pattern Pattern" << i << " {\n";
input << " match { $x = v_add_f32 $a, $b, $c }\n";
input << " rewrite { replace $x with v_mov_b32 $a, $b }\n";
input << "}\n\n";
}
auto patterns = parsePatternString(input.str());
EXPECT_EQ(patterns.size(), 100);
// Verify first and last to ensure all parsed
EXPECT_EQ(patterns[0].name, "Pattern0");
EXPECT_EQ(patterns[99].name, "Pattern99");
}
// ============================================================================
// CATEGORY 5: ERROR MESSAGE QUALITY TESTS
// ============================================================================
TEST_F(PatternParserTest, ErrorIncludesLineNumber) {
const std::string input = R"(
peephole pattern Test {
match {
$x = v_add_f32 $a $b $c
}
}
)";
auto result = parsePatternStringWithDiagnostics(input);
ASSERT_TRUE(result.hasErrors());
ASSERT_GT(result.diagnostics.size(), 0);
// Error should include line number (line 4 has the syntax error)
bool foundLineNumber = false;
for (const auto& diag : result.diagnostics) {
if (diag.getLine() > 0) {
foundLineNumber = true;
break;
}
}
EXPECT_TRUE(foundLineNumber) << "Diagnostics should include non-zero line numbers";
}
TEST_F(PatternParserTest, ErrorIncludesColumnNumber) {
const std::string input = R"(
peephole pattern Test {
match {
$x = v_add_f32 $a $b $c
}
}
)";
auto result = parsePatternStringWithDiagnostics(input);
ASSERT_TRUE(result.hasErrors());
ASSERT_GT(result.diagnostics.size(), 0);
// Error should include column number
bool foundColumnNumber = false;
for (const auto& diag : result.diagnostics) {
if (diag.getColumn() > 0) {
foundColumnNumber = true;
break;
}
}
EXPECT_TRUE(foundColumnNumber) << "Diagnostics should include non-zero column numbers";
}
TEST_F(PatternParserTest, ErrorIncludesDescriptiveMessage) {
const std::string input = R"(
peephole pattern Test {
match {
$x = v_add_f32 $a $b $c
}
}
)";
auto result = parsePatternStringWithDiagnostics(input);
ASSERT_TRUE(result.hasErrors());
ASSERT_GT(result.diagnostics.size(), 0);
// Error message should be descriptive (not empty)
for (const auto& diag : result.diagnostics) {
EXPECT_FALSE(diag.getMessage().empty()) << "Error messages should not be empty";
EXPECT_GT(diag.getMessage().length(), 5) << "Error messages should be descriptive";
}
}
TEST_F(PatternParserTest, CollectsMultipleErrors) {
const std::string input = R"(
peephole pattern Test1 {
match {
$x = v_add_f32 $a $b $c
}
}
peephole pattern Test2 {
match {
$y = v_mul_f32 $d $e $f
}
}
)";
auto result = parsePatternStringWithDiagnostics(input);
// Should collect errors from multiple patterns
EXPECT_TRUE(result.hasErrors());
// Lenient parser may collect multiple errors or stop after first pattern
EXPECT_GT(result.diagnostics.size(), 0);
}
TEST_F(PatternParserTest, ErrorCountMatchesActualErrors) {
const std::string input = R"(
peephole pattern Test {
match {
$x = v_add_f32 $a $b $c
}
}
)";
auto result = parsePatternStringWithDiagnostics(input);
ASSERT_TRUE(result.hasErrors());
// Count errors vs warnings
size_t errorCount = 0;
for (const auto& diag : result.diagnostics) {
if (diag.getLevel() == Diagnostic::Level::Error) {
errorCount++;
}
}
EXPECT_EQ(errorCount, result.errorCount()) << "errorCount() should match actual error count";
EXPECT_GT(errorCount, 0) << "Should have at least one error";
}
TEST_F(PatternParserTest, PartialParseWithErrors) {
const std::string input = R"(
peephole pattern ValidPattern {
match { $x = v_add_f32 $a, $b, $c }
rewrite { replace $x with v_mov_b32 $a, $b }
}
peephole pattern InvalidPattern {
match {
$y = v_mul_f32 $d $e $f
}
}
)";
auto result = parsePatternStringWithDiagnostics(input);
// Parser may accept first pattern and fail on second
// Or may be lenient and parse both with errors
// Key: should not crash and should provide diagnostics
EXPECT_GT(result.diagnostics.size(), 0) << "Should provide diagnostics for invalid input";
// If first pattern is valid, it should be parsed
// (lenient parser behavior - semantic validation happens later)
if (result.patterns.size() > 0) {
EXPECT_EQ(result.patterns[0].name, "ValidPattern");
}
}
// ============================================================================
// TODO: Add more tests as parser implementation evolves
// ============================================================================
// Test round-trip: parse -> serialize -> parse again
// Test nested patterns (if supported)
// Test all constraint types
// Test all instruction types
// Test error recovery (continue parsing after error)