Pipeline checker utility for RISC-V architecture that validates processor
pipeline models. This tool analyzes machine description files to ensure all
instruction types are properly handled by pipeline scheduling models.

I write this tool since I am implment vector pipeline stuff for SiFive
core, but it's hard to find which instruction type is not handled by
pipeline scheduling models. This tool will help me to find out which
instruction type is not handled by pipeline scheduling models, so I can
fix them.

And I think it may be useful for other RISC-V core developers, so I
decided to upstream that :)

Usage:
```
./pipeline-checker <your-pipeline-model>
```
Example:
```
$ ./pipeline-checker sifive-7.md
Error: Some types are not consumed by the pipemodel
Missing types:
 {'vfclass', 'vimovxv', 'vmov', 'rdfrm', 'wrfrm', 'ghost', 'wrvxrm', 'crypto', 
'vwsll', 'vfmovfv', 'vimovvx', 'sf_vc', 'vfmovvf', 'sf_vc_se', 'rdvlenb', 
'vbrev', 'vrev8', 'sf_vqmacc', 'sf_vfnrclip', 'vsetvl_pre', 'rdvl', 'vsetvl'}
```

gcc/ChangeLog:

        * config/riscv/pipeline-checker: New file.
---
 gcc/config/riscv/pipeline-checker | 191 ++++++++++++++++++++++++++++++
 1 file changed, 191 insertions(+)
 create mode 100755 gcc/config/riscv/pipeline-checker

diff --git a/gcc/config/riscv/pipeline-checker 
b/gcc/config/riscv/pipeline-checker
new file mode 100755
index 00000000000..815698b0e20
--- /dev/null
+++ b/gcc/config/riscv/pipeline-checker
@@ -0,0 +1,191 @@
+#!/usr/bin/env python3
+
+# RISC-V pipeline model checker.
+# Copyright (C) 2025 Free Software Foundation, Inc.
+#
+# This file is part of GCC.
+#
+# GCC is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3, or (at your option)
+# any later version.
+#
+# GCC is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with GCC; see the file COPYING3.  If not see
+# <http://www.gnu.org/licenses/>.
+
+import re
+import sys
+import argparse
+from pathlib import Path
+from typing import List
+import pprint
+
+def remove_line_comments(text: str) -> str:
+    # Remove ';;' and everything after it on each line
+    cleaned_lines = []
+    for line in text.splitlines():
+        comment_index = line.find(';;')
+        if comment_index != -1:
+            line = line[:comment_index]
+        cleaned_lines.append(line)
+    return '\n'.join(cleaned_lines)
+
+
+def tokenize_sexpr(s: str) -> List[str]:
+    # Tokenize input string, including support for balanced {...} C blocks
+    tokens = []
+    i = 0
+    while i < len(s):
+        c = s[i]
+        if c.isspace():
+            i += 1
+        elif c == '(' or c == ')':
+            tokens.append(c)
+            i += 1
+        elif c == '"':
+            # Parse quoted string
+            j = i + 1
+            while j < len(s) and s[j] != '"':
+                if s[j] == '\\':
+                    j += 1  # Skip escape
+                j += 1
+            tokens.append(s[i:j+1])
+            i = j + 1
+        elif c == '{':
+            # Parse balanced C block
+            depth = 1
+            j = i + 1
+            while j < len(s) and depth > 0:
+                if s[j] == '{':
+                    depth += 1
+                elif s[j] == '}':
+                    depth -= 1
+                j += 1
+            tokens.append(s[i:j])  # Include enclosing braces
+            i = j
+        else:
+            # Parse atom
+            j = i
+            while j < len(s) and not s[j].isspace() and s[j] not in '()"{}':
+                j += 1
+            tokens.append(s[i:j])
+            i = j
+    return tokens
+
+
+def parse_sexpr(tokens: List[str]) -> any:
+    # Recursively parse tokenized S-expression
+    token = tokens.pop(0)
+    if token == '(':
+        lst = []
+        while tokens[0] != ')':
+            lst.append(parse_sexpr(tokens))
+        tokens.pop(0)  # Discard closing parenthesis
+        return lst
+    elif token.startswith('"') and token.endswith('"'):
+        return token[1:-1]  # Remove surrounding quotes
+    elif token.startswith('{') and token.endswith('}'):
+        return token  # Keep C code block as-is
+    else:
+        return token
+
+
+def find_define_attr_type(ast: any) -> List[List[str]]:
+    # Traverse AST to find all (define_attr "type" ...) entries
+    result = []
+    if isinstance(ast, list):
+        if ast and ast[0] == 'define_attr' and len(ast) >= 2 and ast[1] == 
'type':
+            result.append(ast)
+        for elem in ast:
+            result.extend(find_define_attr_type(elem))
+    return result
+
+
+def parse_md_file(path: Path):
+    # Read file, remove comments, and parse all top-level S-expressions
+    with open(path, encoding='utf-8') as f:
+        raw_content = f.read()
+    clean_content = remove_line_comments(raw_content)
+    tokens = tokenize_sexpr(clean_content)
+    items = []
+    while tokens:
+        items.append(parse_sexpr(tokens))
+    return items
+
+def parsing_str_set(s: str) -> set:
+    s = s.replace('\\','').split(',')
+    s = set(map(lambda x: x.strip(), s))
+    return s
+
+def get_avaliable_types(md_file_path: str):
+    # Main logic: parse input file and print define_attr "type" expressions
+    ast = parse_md_file(Path(md_file_path))
+
+    # Get all type from define_attr type
+    define_attr_types = find_define_attr_type(ast)
+    types = parsing_str_set (define_attr_types[0][2])
+    return types
+
+def get_consumed_type(entry: List[str]) -> set:
+    # Extract the consumed type from a define_insn_reservation entry
+    current_type = entry[0]
+    if current_type in ['and', 'or']:
+        return get_consumed_type(entry[1]) | get_consumed_type(entry[2])
+    elif current_type == 'eq_attr' and entry[1] == 'type':
+        return parsing_str_set(entry[2])
+    return set()
+
+def check_pipemodel(md_file_path: str):
+    # Load the RISCV MD file and check for pipemodel
+    ast = parse_md_file(Path(md_file_path))
+
+    consumed_type = set()
+
+    for entry in ast:
+        entry_type = entry[0]
+        if entry_type not in ["define_insn_reservation"]:
+            continue
+        consumed_type |= get_consumed_type(entry[3])
+    return consumed_type
+
+
+def main():
+    parser = argparse.ArgumentParser(description='Check GCC pipeline model for 
instruction type coverage')
+    parser.add_argument('pipeline_model', help='Pipeline model file to check')
+    parser.add_argument('--base-md',
+                        help='Base machine description file (default: riscv.md 
in script directory)',
+                        default=None)
+    parser.add_argument('-v', '--verbose',
+                        help='Show detailed type information',
+                        action='store_true')
+    args = parser.parse_args()
+
+    # Set default base-md path if not provided
+    if args.base_md is None:
+        script_dir = Path(__file__).parent
+        base_md_path = script_dir / "riscv.md"
+    else:
+        base_md_path = Path(args.base_md)
+    avaliable_types = get_avaliable_types(str(base_md_path))
+    consumed_type = check_pipemodel(args.pipeline_model)
+
+    if args.verbose:
+        print("Available types:\n", avaliable_types)
+        print("Consumed types:\n", consumed_type)
+
+    if not avaliable_types.issubset(consumed_type):
+        print("Error: Some types are not consumed by the pipemodel")
+        print("Missing types:\n", avaliable_types - consumed_type)
+        sys.exit(1)
+    else:
+        print("All available types are consumed by the pipemodel.")
+
+
+if __name__ == '__main__':
+    main()
-- 
2.34.1

Reply via email to