check_macroassembler_style.py (11926B)
1 # vim: set ts=8 sts=4 et sw=4 tw=99: 2 # This Source Code Form is subject to the terms of the Mozilla Public 3 # License, v. 2.0. If a copy of the MPL was not distributed with this 4 # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 6 # ---------------------------------------------------------------------------- 7 # This script checks that SpiderMonkey MacroAssembler methods are properly 8 # annotated. 9 # 10 # The MacroAssembler has one interface for all platforms, but it might have one 11 # definition per platform. The code of the MacroAssembler use a macro to 12 # annotate the method declarations, in order to delete the function if it is not 13 # present on the current platform, and also to locate the files in which the 14 # methods are defined. 15 # 16 # This script scans the MacroAssembler.h header, for method declarations. 17 # It also scans MacroAssembler-/arch/.cpp, MacroAssembler-/arch/-inl.h, and 18 # MacroAssembler-inl.h for method definitions. The result of both scans are 19 # uniformized, and compared, to determine if the MacroAssembler.h header as 20 # proper methods annotations. 21 # ---------------------------------------------------------------------------- 22 23 import difflib 24 import os 25 import re 26 import sys 27 28 architecture_independent = set(["generic"]) 29 all_unsupported_architectures_names = set(["mips64", "mips_shared"]) 30 all_architecture_names = set([ 31 "x86", 32 "x64", 33 "arm", 34 "arm64", 35 "loong64", 36 "riscv64", 37 "wasm32", 38 ]) 39 all_shared_architecture_names = set([ 40 "x86_shared", 41 "arm", 42 "arm64", 43 "loong64", 44 "riscv64", 45 "wasm32", 46 ]) 47 48 reBeforeArg = r"(?<=[(,\s])" 49 reArgType = r"(?P<type>[\w\s:*&<>]+)" 50 reArgName = r"(?P<name>\s\w+)" 51 reArgDefault = r"(?P<default>(?:\s=(?:(?:\s[\w:]+\(\))|[^,)]+))?)" 52 reAfterArg = "(?=[,)])" 53 reMatchArg = re.compile(reBeforeArg + reArgType + reArgName + reArgDefault + reAfterArg) 54 55 56 def get_normalized_signatures(signature, fileAnnot=None): 57 # Remove static 58 signature = signature.replace("static", "") 59 # Remove semicolon. 60 signature = signature.replace(";", " ") 61 # Normalize spaces. 62 signature = re.sub(r"\s+", " ", signature).strip() 63 # Remove new-line induced spaces after opening braces. 64 signature = re.sub(r"\(\s+", "(", signature).strip() 65 # Match arguments, and keep only the type. 66 signature = reMatchArg.sub(r"\g<type>", signature) 67 # Remove class name 68 signature = signature.replace("MacroAssembler::", "") 69 70 # Extract list of architectures 71 archs = ["generic"] 72 if fileAnnot: 73 archs = [fileAnnot["arch"]] 74 75 if "DEFINED_ON(" in signature: 76 archs = re.sub( 77 r".*DEFINED_ON\((?P<archs>[^()]*)\).*", r"\g<archs>", signature 78 ).split(",") 79 archs = [a.strip() for a in archs] 80 signature = re.sub(r"\s+DEFINED_ON\([^()]*\)", "", signature) 81 82 elif "PER_ARCH" in signature: 83 archs = all_architecture_names 84 signature = re.sub(r"\s+PER_ARCH", "", signature) 85 86 elif "PER_SHARED_ARCH" in signature: 87 archs = all_shared_architecture_names 88 signature = re.sub(r"\s+PER_SHARED_ARCH", "", signature) 89 90 elif "OOL_IN_HEADER" in signature: 91 assert archs == ["generic"] 92 signature = re.sub(r"\s+OOL_IN_HEADER", "", signature) 93 94 else: 95 # No signature annotation, the list of architectures remains unchanged. 96 pass 97 98 # Extract inline annotation 99 inline = False 100 if fileAnnot: 101 inline = fileAnnot["inline"] 102 103 if "inline " in signature: 104 signature = re.sub(r"inline\s+", "", signature) 105 inline = True 106 107 inlinePrefx = "" 108 if inline: 109 inlinePrefx = "inline " 110 signatures = [{"arch": a, "sig": inlinePrefx + signature} for a in archs] 111 112 return signatures 113 114 115 file_suffixes = set([ 116 a.replace("_", "-") 117 for a in all_architecture_names.union(all_shared_architecture_names).union( 118 all_unsupported_architectures_names 119 ) 120 ]) 121 122 123 def get_file_annotation(filename): 124 origFilename = filename 125 filename = filename.split("/")[-1] 126 127 inline = False 128 if filename.endswith(".cpp"): 129 filename = filename[: -len(".cpp")] 130 elif filename.endswith("-inl.h"): 131 inline = True 132 filename = filename[: -len("-inl.h")] 133 elif filename.endswith(".h"): 134 # This allows the definitions block in MacroAssembler.h to be 135 # style-checked. 136 inline = True 137 filename = filename[: -len(".h")] 138 else: 139 raise Exception("unknown file name", origFilename) 140 141 arch = "generic" 142 for suffix in file_suffixes: 143 if filename == "MacroAssembler-" + suffix: 144 arch = suffix 145 break 146 147 return {"inline": inline, "arch": arch.replace("-", "_")} 148 149 150 def get_macroassembler_definitions(filename): 151 try: 152 fileAnnot = get_file_annotation(filename) 153 except Exception: 154 return [] 155 156 style_section = False 157 lines = "" 158 signatures = [] 159 with open(filename, encoding="utf-8") as f: 160 for line in f: 161 if "//{{{ check_macroassembler_style" in line: 162 if style_section: 163 raise "check_macroassembler_style section already opened." 164 style_section = True 165 braces_depth = 0 166 elif "//}}} check_macroassembler_style" in line: 167 style_section = False 168 if not style_section: 169 continue 170 171 # Ignore preprocessor directives. 172 if line.startswith("#"): 173 continue 174 175 # Remove comments from the processed line. 176 line = re.sub(r"//.*", "", line) 177 178 # Locate and count curly braces. 179 open_curly_brace = line.find("{") 180 was_braces_depth = braces_depth 181 braces_depth = braces_depth + line.count("{") - line.count("}") 182 183 # Raise an error if the check_macroassembler_style macro is used 184 # across namespaces / classes scopes. 185 if braces_depth < 0: 186 raise "check_macroassembler_style annotations are not well scoped." 187 188 # If the current line contains an opening curly brace, check if 189 # this line combines with the previous one can be identified as a 190 # MacroAssembler function signature. 191 if open_curly_brace != -1 and was_braces_depth == 0: 192 lines = lines + line[:open_curly_brace] 193 if "MacroAssembler::" in lines: 194 signatures.extend(get_normalized_signatures(lines, fileAnnot)) 195 lines = "" 196 continue 197 198 # We do not aggregate any lines if we are scanning lines which are 199 # in-between a set of curly braces. 200 if braces_depth > 0: 201 continue 202 if was_braces_depth != 0: 203 line = line[line.rfind("}") + 1 :] 204 205 # This logic is used to remove template instantiation, static 206 # variable definitions and function declaration from the next 207 # function definition. 208 last_semi_colon = line.rfind(";") 209 if last_semi_colon != -1: 210 lines = "" 211 line = line[last_semi_colon + 1 :] 212 213 # Aggregate lines of non-braced text, which corresponds to the space 214 # where we are expecting to find function definitions. 215 lines = lines + line 216 217 return signatures 218 219 220 def get_macroassembler_declaration(filename): 221 style_section = False 222 lines = "" 223 signatures = [] 224 with open(filename, encoding="utf-8") as f: 225 for line in f: 226 if "//{{{ check_macroassembler_decl_style" in line: 227 style_section = True 228 elif "//}}} check_macroassembler_decl_style" in line: 229 style_section = False 230 if not style_section: 231 continue 232 233 # Ignore preprocessor directives. 234 if line.startswith("#"): 235 continue 236 237 line = re.sub(r"//.*", "", line) 238 if len(line.strip()) == 0 or "public:" in line or "private:" in line: 239 lines = "" 240 continue 241 242 lines = lines + line 243 244 # Continue until we have a complete declaration 245 if ";" not in lines: 246 continue 247 248 # Skip member declarations: which are lines ending with a 249 # semi-colon without any list of arguments. 250 if ")" not in lines: 251 lines = "" 252 continue 253 254 signatures.extend(get_normalized_signatures(lines)) 255 lines = "" 256 257 return signatures 258 259 260 def append_signatures(d, sigs): 261 for s in sigs: 262 if s["sig"] not in d: 263 d[s["sig"]] = [] 264 d[s["sig"]].append(s["arch"]) 265 return d 266 267 268 def generate_file_content(signatures): 269 output = [] 270 for s in sorted(signatures.keys()): 271 archs = set(sorted(signatures[s])) 272 archs -= all_unsupported_architectures_names 273 if len(archs.symmetric_difference(architecture_independent)) == 0: 274 output.append(s + ";\n") 275 if s.startswith("inline"): 276 # TODO, bug 1432600: This is mistaken for OOL_IN_HEADER 277 # functions. (Such annotation is already removed by the time 278 # this function sees the signature here.) 279 output.append(" is defined in MacroAssembler-inl.h\n") 280 else: 281 output.append(" is defined in MacroAssembler.cpp\n") 282 else: 283 if len(archs.symmetric_difference(all_architecture_names)) == 0: 284 output.append(s + " PER_ARCH;\n") 285 elif len(archs.symmetric_difference(all_shared_architecture_names)) == 0: 286 output.append(s + " PER_SHARED_ARCH;\n") 287 else: 288 output.append(s + " DEFINED_ON(" + ", ".join(sorted(archs)) + ");\n") 289 for a in sorted(archs): 290 a = a.replace("_", "-") 291 masm = "%s/MacroAssembler-%s" % (a, a) 292 if s.startswith("inline"): 293 output.append(" is defined in %s-inl.h\n" % masm) 294 else: 295 output.append(" is defined in %s.cpp\n" % masm) 296 return output 297 298 299 def check_style(): 300 # We read from the header file the signature of each function. 301 decls = dict() # type: dict(signature => ['x86', 'x64']) 302 303 # We infer from each file the signature of each MacroAssembler function. 304 defs = dict() # type: dict(signature => ['x86', 'x64']) 305 306 root_dir = os.path.join("js", "src", "jit") 307 for dirpath, dirnames, filenames in os.walk(root_dir): 308 for filename in filenames: 309 if "MacroAssembler" not in filename: 310 continue 311 312 filepath = os.path.join(dirpath, filename).replace("\\", "/") 313 314 if filepath.endswith("MacroAssembler.h"): 315 decls = append_signatures( 316 decls, get_macroassembler_declaration(filepath) 317 ) 318 defs = append_signatures(defs, get_macroassembler_definitions(filepath)) 319 320 if not decls or not defs: 321 raise Exception("Did not find any definitions or declarations") 322 323 # Compare declarations and definitions output. 324 difflines = difflib.unified_diff( 325 generate_file_content(decls), 326 generate_file_content(defs), 327 fromfile="check_macroassembler_style.py declared syntax", 328 tofile="check_macroassembler_style.py found definitions", 329 ) 330 ok = True 331 for diffline in difflines: 332 ok = False 333 print(diffline, end="") 334 return ok 335 336 337 def main(): 338 ok = check_style() 339 340 if ok: 341 print("TEST-PASS | check_macroassembler_style.py | ok") 342 else: 343 print( 344 "TEST-UNEXPECTED-FAIL | check_macroassembler_style.py | actual output does not match expected output; diff is above" # noqa: E501 345 ) 346 347 sys.exit(0 if ok else 1) 348 349 350 if __name__ == "__main__": 351 main()