diff --git a/.github/scripts/README.md b/.github/scripts/README.md index 0d16ef0702..77bf3bed65 100644 --- a/.github/scripts/README.md +++ b/.github/scripts/README.md @@ -109,11 +109,11 @@ source .github/scripts/check_for_missing_readme_md.sh ### Prerequisites -- the script must be run while being in the root directory (/hdl) -- clean the repository to remove the files generated by Vivado -- it will be run only on Verilog files that do not contain "tb" in their path -- doesn't run on SystemVerilog files -- uses Python 3.x +* the script must be run while being in the root directory (/hdl) +* clean the repository to remove the files generated by Vivado +* runs on Verilog (.v) and SystemVerilog (.sv) files, except those containing +"tb" in their path/name (testbenches are ignored) +* uses Python 3.x ### Rules that are checked @@ -121,24 +121,57 @@ These rules can be found in the [HDL coding guidelines](https://analogdevicesinc #### 1. License header -It checks if the license header is up-to-date, containing the current year in -the year range. Exceptions are the JESD files and the ones specified in the -`avoid_list` string list. +It checks that the copyright years after Copyright (C) are up-to-date and properly +formatted. +Supported forms: a single year, a range (YYYY-YYYY), or a comma-separated list +combining singles and/or ranges. +Exceptions are the JESD files and the ones specified in the `avoid_list` string list. + +The following checks are performed (only the last year and, if present, the +penultimate year are considered): + * If the last year equals the current year: + * If a penultimate year exists and current - penultimate == 1, it must be a range (-). + * If it is written as a list (, current), the script warns and merges to ...-current. + * If a penultimate year exists and current - penultimate > 1, a list is correct; only formatting may be normalized. + * If there is no penultimate year, a single current year is OK. + * If the last year is not the current year: + * If current - last == 1, it extends the last token into a range ending at the current year. + * If current - last > 1, it appends , current. + +Examples: +``` +2013-2024 -> 2013-2025 +2013-2020 -> 2013-2020, 2025 +2024 -> 2024-2025 +2022 -> 2022, 2025 +2013, 2024 -> 2013, 2024-2025 +2013-2024, 2025 -> 2013-2025 (warn: wrong delimiter; merged) +2013-2020, 2025 -> 2013-2020, 2025 (ok as list; spacing normalized) +``` + If `-e` option is added, the script can update the year range. -#### 2. Two or more consecutive empty lines +#### 2. Empty lines -It checks in the whole file if there are two or more consecutive empty lines. -If `-e` option is added, the script can remove them and leave only one empty line. +It checks if the file contains empty line issues: +* Two or more consecutive empty lines. +* An empty line at the beginning of the file. +* An empty line at the end of the file. + +If `-e` option is added, the script removes redundant empty lines and ensures +that at most one empty line is kept, while also removing any leading or trailing +empty lines. #### 3. Trailing whitespace It checks if there are whitespace characters at the end of the lines. + If `-e` option is added, then they can be removed. -#### 4. Lines after `endmodule` tag +#### 4. Lines after end token + +It checks if there are lines after end token (`endmodule` or `endpackage`). -It checks if there are lines after it. If `-e` option is added, the script can remove them. #### 5. Parentheses around the module declaration @@ -147,66 +180,135 @@ It checks if the parentheses around the module declaration (meaning `) (` for the parameters' list) are on an empty line, right after the last parameter. It also checks for the closing parenthesis at the module declaration (meaning `);`) to be on an empty line, at the beginning, right after the last I/O port line. + If `-e` option is added, the script can put them in their proper place. #### 6. Indentation of code -It checks if all lines (except for the ones that are commented) have an indentation -of two or multiple of two spaces. -Other exceptions to this are the `module` line, the `endmodule` line, the `) (` -and the `);` from the module declaration. +It checks that indentation is done with spaces in multiples of 2 (minimum 2). +This applies to parameters, ports, and code inside modules or packages. #### 7. Position of the module instances It checks if the parameters' list (if that's the case) is in proper position, meaning that the position of `#` is checked, the parameters to be specified each -on its own line, the parentheses around the instance name and the closing parenthesis -of the module instance. +on its own line, the parentheses around the instance name and the closing +parenthesis of the module instance. _NOTE_: these rules are marked in the script with **GC** (stands for Guideline Check) in the comments. -### Changes done by the script to your files +#### 8. SystemVerilog packages + +It checks if SystemVerilog packages are defined correctly: +* `package ;` starts at column 0, with no extra spaces before or after ;. +* `endpackage` is alone on its line at column 0. +* Exactly one newline follows `endpackage`. +* Inside the package, typedefs and localparams follow the same indentation and alignment rules. + +If `-e` option is added, the script rewrites these to the guideline-compliant +format. + +#### 9. Localparams + +It checks if localparam declarations follow the rules: +* Indented by a multiple of 2 spaces (minimum 2). +* **List form:** each item is on a new line, `=` operator is aligned, and the last +item has no comma. +* **Concatenation form ({ … }):** + * The first element is on the same line as `{`. + * Following elements are indented +2 spaces. + * Inline comments are aligned at least 4 spaces after the longest element. + * The closing `};` is attached to the last element. + +If `-e` option is added, the script normalizes the block automatically. + +#### 10. Typedefs + +It checks if typedef blocks are formatted correctly: +* They are not written as one-liners, each item is on its own line. +* Base indentation is a multiple of 2 and at least 2. +* Inner items are indented +2. +* Closing `} type_name;` is aligned with the opening `typedef`. +* Inline comments after the closing are moved to a separate line. + +If `-e` option is added, the script rewrites the block to the guideline-compliant +format. + +#### 11. Project name vs. path -If one wants the script to make changes in files, they will be regarding: +It checks that in each system_project.tcl, the project name used in `adi_project` +matches the relative project path under `projects/`. -- license header, except for JESD files and the ones specified in `avoid_list` -- two or more consecutive empty lines -- trailing whitespaces -- lines after `endmodule` tag -- parentheses around the module declaration (meaning `) (` for the parameters' +Example: +``` +projects/ad9783_ebz/zcu102 ⇒ adi_project ad9783_ebz_zcu102 +``` + +If `-e` option is added, the script updates the project name automatically. + +### Changes done by the script to your files + +If edits are enabled (-e), the script may modify: +* license header, except for files specified in `avoid_list` +* empty lines (two or more consecutive, or at file start/end) +* trailing whitespaces +* lines after `endmodule`/`endpackage` tag +* parentheses around the module declaration (meaning `) (` for the parameters' list and `);` for when closing the declaration) +* typedefs and localparams — rewritten into a guideline-compliant format +* SystemVerilog packages — normalizes `package ;` / `endpackage` placement +and ensures exactly one newline after `endpackage` +* project name inside `system_project.tcl` (to match the relative project path) ### Ways to run the script +The script supports several modes of execution, depending on what files you want +to check and whether edits are allowed: + 1. With no arguments: `python3 check_guideline.py` -Checks all files with the properties specified above. -Does not modify the files. +Runs on all HDL files under `library/` and `projects/`, in check-only mode (does +not modify the files). 2. With arguments: - 1. `-e` with no file specified - Checks all files with the properties specified above. Additionally, - it modifies the module definition parentheses according to the guideline. - 2. `-m` - Checks files that are given as arguments (by their names including the - extension). - 3. `-me` - Checks files that are given as arguments (by their names including the - extension) and modifies the files where the guideline is not respected. - 4. `-p` - Checks files that are given as arguments (by their relative path) and - modifies the files where the guideline is not respected. - 5. `-pe` - Checks files that are given as arguments (by their relative path) and - modifies the files where the guideline is not respected. + 1. `-e` with no file specified: `python3 check_guideline.py -e` + Checks all files with the properties specified above and applies fixes + according to the guideline. + + 2. `-m` + Checks files that are given as arguments (by their names including the + extension). + + 3. `-me` + Checks files that are given as arguments (by their names including the + extension) and modifies the files where the guideline is not respected. + + 4. `-p` + Checks files that are given as arguments (by their relative path) and + modifies the files where the guideline is not respected. + + 5. `-pe` + Checks files that are given as arguments (by their relative path) and + modifies the files where the guideline is not respected. ### Examples of running ``` +# Check all files in the repo, no modifications python3 check_guideline.py >> warnings.txt + +# Check and edit every HDL file in the repo python3 check_guideline.py -e >> warnings.txt + +# Check a specific file given by name, no modifications python3 check_guideline.py -m axi_ad9783.v >> warnings.txt + +# Check and edit a specific files by name python3 check_guideline.py -me axi_ad9783.v axi_ad9783_if.v up_adc_common.v >> warnings.txt + +# Check specific files given by absolute/relative paths, no modifications python3 check_guideline.py -p ./library/axi_ad9783/axi_ad9783.v ./library/common/up_adc_common.v >> warnings.txt + +# Check and edit a specific file given by absolute/relative path python3 check_guideline.py -pe ./library/axi_ad9783/axi_ad9783_if.v >> warnings.txt ``` diff --git a/.github/scripts/check_guideline.py b/.github/scripts/check_guideline.py index 268c117626..8cfe3675a1 100755 --- a/.github/scripts/check_guideline.py +++ b/.github/scripts/check_guideline.py @@ -66,6 +66,9 @@ def is_multiline_comment (line): else: return False +def is_cmt(line): + return is_comment(line) or is_multiline_comment(line) + def is_paramdef (line): rparameter = re.compile(r'^\s*parameter\s.*') if (rparameter.match(line)): @@ -110,32 +113,335 @@ def string_in_list (module_path, modified_files): return False +# write lines to file +def rewrite_file(path, lines): + with open(path, "w") as f: + for line in lines: + f.write(line.rstrip() + "\n") + + +# get the indentation of a line +def indent_of(s: str) -> int: + return len(s) - len(s.lstrip()) + + +# return an even indentation width, at least base_min. +def even_indent(n: int, base_min: int = 2) -> int: + n2 = max(base_min, n) + return n2 if (n2 % 2 == 0) else (n2 + 1) + + +# split line into code and comment parts, return tuple (code, comment) +def split_code_comment_sl(raw: str): + code, sep, cmt = raw.rstrip("\n").partition("//") + return code.rstrip(), (sep + cmt).rstrip() if sep else "" + + +# append formatted warning for a specific line to the list +def emit_line_warning(lw, path, base_line_nb, start_idx, abs_idx, msg: str): + real_ln = base_line_nb + (abs_idx - start_idx) + lw.append(f"{path} : {real_ln} {msg}") + + +# compare two lists of lines and emit warnings for each differing line +def diff_line_warnings(lw, path, base_line_nb, start_idx, old_lines, new_lines, msg: str): + m = min(len(old_lines), len(new_lines)) + for k in range(m): + old = (old_lines[k].rstrip("\n")).rstrip() + neu = (new_lines[k].rstrip("\n")).rstrip() + if old != neu: + emit_line_warning(lw, path, base_line_nb, start_idx, start_idx + k, msg) + + +# find the line index where a statement ends (with ';'), starting from start_i +def stmt_end_line(lines, start_i: int) -> int: + depth = 0 + in_block = False + in_line = False + for j in range(start_i, len(lines)): + s = lines[j] + i = 0 + while i < len(s): + ch = s[i] + nxt = s[i+1] if i+1 < len(s) else "" + if in_line: + break + if in_block: + if ch == "*" and nxt == "/": + in_block = False + i += 2 + continue + i += 1 + continue + if ch == "/" and nxt == "/": + in_line = True + i += 2 + continue + if ch == "/" and nxt == "*": + in_block = True + i += 2 + continue + if ch in "([{": + depth += 1 + elif ch in ")]}": + depth = max(0, depth - 1) + elif ch == ";" and depth == 0: + return j + i += 1 + return -1 + + +# split a text by top-level commas (not inside (), [], {}, nor in comments) +def split_top_level_commas(text: str): + parts, buf = [], [] + depth = 0 + in_block = False + in_line = False + i = 0 + L = len(text) + while i < L: + ch = text[i] + nxt = text[i+1] if i+1 < L else "" + if in_line: + buf.append(ch) + if ch == "\n": in_line = False + i += 1 + continue + if in_block: + buf.append(ch) + if ch == "*" and nxt == "/": + buf.append("/") + i += 2 + continue + i += 1 + continue + if ch == "/" and nxt == "/": + in_line = True + buf.append("//") + i += 2 + continue + if ch == "/" and nxt == "*": + in_block = True + buf.append("/*") + i += 2 + continue + if ch in "([{": + depth += 1 + buf.append(ch) + i += 1 + continue + if ch in ")]}": + depth = max(0, depth - 1) + buf.append(ch) + i += 1 + continue + if ch == "," and depth == 0: + parts.append("".join(buf)) + buf = [] + i += 1 + continue + buf.append(ch) + i += 1 + parts.append("".join(buf)) + return parts + + +# find the position of the top-level assignment '=' (not '==', '!=', '<=', '>=') +def find_assign_eq_top(code: str) -> int: + depth = 0 + i = 0 + L = len(code) + while i < L: + ch = code[i] + nxt = code[i+1] if i+1 < L else "" + prv = code[i-1] if i-1 >= 0 else "" + if ch in "([{": + depth += 1 + i += 1 + continue + if ch in ")]}": + depth = max(0, depth - 1) + i += 1 + continue + if ch == "=" and depth == 0 and nxt != "=" and prv not in "<>!": + return i + i += 1 + return -1 + + +# strip trailing ';' or ',' and spaces +def strip_trailing_semicol(line: str) -> str: + return re.sub(r"[;,]\s*$", "", line.rstrip()) + + +# normalize string +def normalize_str(s: str) -> str: + # compress spaces, normalize '-' + s = re.sub(r"[ \t]+", " ", s.strip()) + s = re.sub(r"\s*-\s*", "-", s) + # remove ",," + s = re.sub(r",\s*,+", ",", s) + s = strip_trailing_semicol(s) + s = re.sub(r"\.\s*$", "", s) + # apply canonical ", " + s = re.sub(r",\s*", ", ", s) + return s + + +# normalize soft spacing around '=' in a code segment, keep comment if any +def soft_norm_eq_keep_cmt(seg: str) -> str: + code, sep, cmt = seg.partition("//") + s = code.rstrip() + eq = find_assign_eq_top(s) + if eq != -1: + if eq > 0 and s[eq-1] != " ": + s = s[:eq] + " " + s[eq:] + eq += 1 + if eq + 1 < len(s) and s[eq+1] != " ": + s = s[:eq+1] + " " + s[eq+1:] + out = s.rstrip() + if sep: + out += " //" + cmt.strip() + return out + + +# find the span of the top-level {...} block (not in comments) +def find_top_level_brace_span(lines, start_i: int, end_i: int): + depth = 0 + in_block = False + in_line = False + open_pos = None + for j in range(start_i, end_i + 1): + s = lines[j] + i = 0 + L = len(s) + while i < L: + ch = s[i] + nxt = s[i+1] if i+1 < L else "" + if in_line: + break + if in_block: + if ch == "*" and nxt == "/": + in_block = False + i += 2 + continue + i += 1 + continue + if ch == "/" and nxt == "/": + in_line = True + i += 2 + continue + if ch == "/" and nxt == "*": + in_block = True + i += 2 + continue + if ch == "{" and depth == 0: + open_pos = (j, i) + depth = 1 + i += 1 + continue + if ch == "{" and depth > 0: + depth += 1 + i += 1 + continue + if ch == "}": + depth = max(0, depth - 1) + if depth == 0 and open_pos is not None: + return open_pos + (j, i) + i += 1 + continue + if ch in "([<": + depth += 1 + elif ch in ")]>": + depth = max(0, depth - 1) + i += 1 + return None + + +# extract text between {...} from lines[open_l][open_c] to lines[close_l][close_c] +def extract_between_braces(lines, open_l, open_c, close_l, close_c) -> str: + # strictly between '{' and '}' (excluding '}'/';' or any text after) + if open_l == close_l: + return lines[open_l][open_c+1:close_c] + chunks = [lines[open_l][open_c+1:]] + + if close_l - open_l > 1: + chunks.extend(lines[open_l+1:close_l]) + chunks.append(lines[close_l][:close_c]) + + return "".join(chunks) + + +# replace lines[start_idx:end_idx+1] with new_block_lines, return original span +def replace_block_atomic(lines, start_idx, end_idx, new_block_lines): + orig_span = lines[start_idx:end_idx+1] + del lines[start_idx:end_idx+1] + for ln in reversed(new_block_lines): + lines.insert(start_idx, ln) + return orig_span + + +# find the index of the first non-space/tab character in code +def first_token_start(code: str) -> int: + for k, ch in enumerate(code): + if not ch.isspace(): + return k + return 0 + + +# align '=' across a list of 'segments' (strings without terminators). +# keeps the original trailing '//' comment (if present). +def _align_equals_segments(segments): + triples = [] + max_lhs = 0 + for seg in segments: + code, sep, cmt = seg.partition("//") + s = code.rstrip() + eq = find_assign_eq_top(s) + if eq == -1: + triples.append((None, s.rstrip(), (" //" + cmt.strip()) if sep else "")) + continue + lhs = s[:eq].rstrip() + rhs = s[eq+1:].lstrip() + max_lhs = max(max_lhs, len(lhs)) + triples.append((lhs, rhs, (" //" + cmt.strip()) if sep else "")) + + aligned = [] + for lhs, rhs, cmt in triples: + if lhs is None: + aligned.append((rhs + cmt).rstrip()) + else: + pad = " " * (max_lhs - len(lhs)) + aligned.append(f"{lhs}{pad} = {rhs}{cmt}".rstrip()) + + return aligned + + ############################################################################### # # Check if file has correct properties, meaning that the file extension has to -# be .v and it should not be some certain files (.sv, tb) +# be .v or .sv and it should not be some certain file (tb) # Returns true or false. ############################################################################### -def check_filename (filename): +def check_hdl_filename(filename): - if (filename.endswith('.v') == False): - return False - if (filename.endswith('.sv') == True): + if not (filename.endswith(".v") or filename.endswith(".sv")): return False if (filename.find("tb") != -1): return False - + return True ############################################################################### # -# Detect all modules present in the given directory in /library and /projects. +# Detect all HDL files (.v, .sv) present in the given directory in /library and +# /projects. # Return a list with the relative paths. ############################################################################### -def detect_all_modules (directory): +def detect_all_hdl_files (directory): - detected_modules_list = [] + detected_files = [] for folder, dirs, files in os.walk(directory): ## folder name must be either library or projects, ## and it must not contain a dot in the name (Vivado generated) @@ -144,11 +450,48 @@ def detect_all_modules (directory): for file in files: #filename_wout_ext = (os.path.splitext(file)[0]) - if (check_filename(file)): + if (check_hdl_filename(file)): fullpath = os.path.join(folder, file) - detected_modules_list.append(fullpath) + detected_files.append(fullpath) - return detected_modules_list + return detected_files + + +############################################################################### +# +# Detect the kind of file unit (package, module). +# Return the string containing the type of file unit, or None if not found. +############################################################################### +def detect_file_unit_(path: str): + + with open(path, "r") as f: + in_block = False + for raw in f: + line = raw.strip() + if in_block: + if "*/" in line: + line = line.split("*/", 1)[1].strip() + in_block = False + else: + continue + if not line: + continue + if line.startswith("/*"): + if "*/" in line: + line = line.split("*/", 1)[1].strip() + if not line: + continue + else: + in_block = True + continue + if line.startswith("//") or line.startswith("`"): + continue + if line.startswith("package "): + return "package" + if line.startswith("module "): + return "module" + + return None ############################################################################### @@ -156,40 +499,48 @@ def detect_all_modules (directory): # Determine the file name from the fullpath. # Return the string containing the file name without extension. ############################################################################### -def get_file_name (module_path): +def get_file_name (file_path): # split the path using the / and take the last group, which is the file.ext - split_path = module_path.split("/") - module_filename = split_path[len(split_path) - 1] + split_path = file_path.split("/") + filename = split_path[len(split_path) - 1] # take the module name from the filename with the extension - filename_wout_ext = module_filename.split(".")[0] + filename_wout_ext = filename.split(".")[0] return filename_wout_ext ############################################################################### # -# Check if there are lines after `endmodule and two consecutive empty lines, -# and if there are and edit_files is true, delete them. +# Check if there are lines after `endmodule or `endpackage and two consecutive +# empty lines, and if there are and edit_files is true, delete them. ############################################################################### -def check_extra_lines (module_path, list_of_lines, lw, edit_files): +def check_extra_lines (module_path, list_of_lines, lw, edit_files, end_token="endmodule"): - passed_endmodule = False + # Remove empty lines at the beggining of the file + if list_of_lines and list_of_lines[0].strip() == "": + lw.append(f"{module_path} : empty line at beginning of file") + if edit_files: + while list_of_lines and list_of_lines[0].strip() == "": + list_of_lines.pop(0) + + passed_end = False line_nb = 1 prev_line = "" remove_end_lines = False + add_end_line = False if (edit_files): remove_extra_lines = False for line in list_of_lines: - # GC: check for lines after endmodule - if (line.find("endmodule") != -1): - passed_endmodule = True + # GC: detect the closing token (end_token) in order to flag content after it + if (line.find(end_token) != -1): + passed_end = True - # if we passed the endmodule tag - if (passed_endmodule and (line.find("endmodule") == -1)): + # if we passed the closing token + if (passed_end and (line.find(end_token) == -1)): remove_end_lines = True # GC: check for empty lines @@ -207,25 +558,25 @@ def check_extra_lines (module_path, list_of_lines, lw, edit_files): if (remove_end_lines): if (edit_files): deleted_lines = False - passed_endmodule = False + passed_end = False line_nb = 1 while (line_nb <= len(list_of_lines)): line = list_of_lines[line_nb-1] - if (line.find("endmodule") != -1): - passed_endmodule = True - if (not (passed_endmodule and (line.find("endmodule") == -1))): + if (line.find(end_token) != -1): + passed_end = True + if (not (passed_end and (line.find(end_token) == -1))): line_nb += 1 else: deleted_lines = True list_of_lines.pop(line_nb-1) if (deleted_lines): - lw.append(module_path + " : deleted lines after endmodule") + lw.append(module_path + f" : deleted lines after {end_token}") else: - lw.append(module_path + " : couldn't delete lines after endmodule but must!") + lw.append(module_path + f" : couldn't delete lines after {end_token} but must!") else: - lw.append(module_path + " : extra lines after endmodule") + lw.append(module_path + f" : extra lines after {end_token}") if (edit_files and remove_extra_lines): line_nb = 1 @@ -244,6 +595,15 @@ def check_extra_lines (module_path, list_of_lines, lw, edit_files): if (line_nb >= 2): prev_line = line + # ensure the file ends with a newline + if list_of_lines and (not list_of_lines[-1].endswith('\n')): + lw.append(f"{module_path} : file does not end with a newline") + if edit_files: + list_of_lines[-1] += '\n' + add_end_line = True + + return add_end_line + ############################################################################### # Get the nth digit from a number. @@ -277,13 +637,16 @@ def header_check_allowed (module_path): ############################################################################### # # Check if the license header is written correctly, meaning: -# To have either a range of years from the first time it was committed and -# until the current year -# or just the current year, if this is the first commit. +# To have either: +# - a range of years from the first time it was committed up to the current year, +# - just the current year, if this is the first commit, +# - or a comma-separated list combining ranges and/or single years when the +# gap between the last recorded year and the current year is greater than 1. ############################################################################### -def check_copyright (list_of_lines, lw, edit_files): +def check_copyright(file_path: str, list_of_lines, lw, edit_files): currentYear = datetime.now().year + # license_header = """// *************************************************************************** #// *************************************************************************** #// Copyright (C) """ + str(currentYear) + """ Analog Devices, Inc. All rights reserved. @@ -318,9 +681,7 @@ def check_copyright (list_of_lines, lw, edit_files): #// #// *************************************************************************** #// ***************************************************************************""" - changed = False - template_matches = True header_status = -1 # for further development, if the entire license should be checked @@ -329,73 +690,649 @@ def check_copyright (list_of_lines, lw, edit_files): # if this is the line with the Copyright year line_nb = 2 - ## from [17-25] is the range of years - ## [17-20] is the beginning year - ## [21] is the dash [-] - ## [22-25] is the last year - aux = list(list_of_lines[line_nb]) - # match a year range - match = re.match(r'.*(Copyright\s\(C\)\s20[0-9]{2}[-]20[0-9]{2})', list_of_lines[line_nb]) - if (match is not None): - # only the last year must be updated (chars [22-25]) - c1 = str(get_digit(currentYear, 3)) - c2 = str(get_digit(currentYear, 2)) - c3 = str(get_digit(currentYear, 1)) - c4 = str(get_digit(currentYear, 0)) - - # if already set to current year, then no edits and no warnings - if (aux[22] == c1 and aux[23] == c2 and aux[24] == c3 and aux[25] == c4): - changed = False + if line_nb >= len(list_of_lines): + lw.append(file_path + " : copyright template doesn't match") + return 4 + + line = list_of_lines[line_nb] + + # capture the entire "years" sequence (single / range / comma-separated list) + # examples matched by (?P...): + # "2024" + # "2013-2024" + # "2013-2017, 2019, 2024" + m = re.search( + r'(Copyright\s*\(C\)\s*)(?P(?:20\d{2}(?:\s*-\s*20\d{2})?)(?:\s*,\s*(?:20\d{2}(?:\s*-\s*20\d{2})?))*)', + line + ) + if not m: + lw.append(file_path + " : copyright template doesn't match") + return 4 + + years_str = m.group('years') + # normalize the years string (spacing and commas) before parsing. + norm = normalize_str(years_str) + + # parse tokens as a list of dicts: {'t': 'single'|'range', 'a': int, 'b': int} + tokens_raw = [t.strip() for t in norm.split(",") if t.strip()] + parsed = [] + for tok in tokens_raw: + m1 = re.fullmatch(r"(20\d{2})-(20\d{2})", tok) + if m1: + a, b = int(m1.group(1)), int(m1.group(2)) + if a > b: + a, b = b, a + parsed.append({"t": "range", "a": a, "b": b}) + continue + m2 = re.fullmatch(r"(20\d{2})", tok) + if m2: + y = int(m2.group(1)) + parsed.append({"t": "single", "a": y, "b": y}) + continue + + header_status = 2 + return header_status + + if not parsed: + header_status = 2 + return header_status + + # last and penultimate visible years (right end of tokens) + last = parsed[-1] + last_num = last["b"] + prev_num = parsed[-2]["b"] if len(parsed) >= 2 else None + prev_idx = len(parsed) - 2 if len(parsed) >= 2 else None + + proposed = [dict(p) for p in parsed] + + + if last_num == currentYear: + # current year is already present at the end + if prev_num is not None: + dif = currentYear - prev_num + if dif == 1: + # if the last token is exactly 'current', it should be a range + if last["a"] == last["b"] == currentYear: + lw.append(file_path + " : wrong delimiter between previous year and current; merging into range") + if edit_files: + if proposed[prev_idx]["t"] == "single": + proposed[prev_idx] = { + "t": "range", + "a": proposed[prev_idx]["a"], + "b": currentYear + } + else: + proposed[prev_idx]["b"] = currentYear + proposed.pop() + # if dif > 1: list is correct; do nothing. + # example: "2013-2020, 2025" stays as list (spacing may be normalized). + # no penultimate and last == currentYear -> OK (no change). Example: "2025". + else: + # current year is missing at the end + # examples: + # "2013-2024" (in 2025) -> warning + "2013-2025" if editing + # "2013-2020" (in 2025) -> warning + "2013-2020, 2025" if editing + lw.append(file_path + " : current year missing; update needed") + dif = currentYear - last_num + if dif == 1: + # extend the last token into a range ending at currentYear + # examples: + # "2024" -> "2024-2025" + # "2013-2024" -> "2013-2025" + if edit_files: + if proposed[-1]["t"] == "single": + proposed[-1] = {"t": "range", "a": proposed[-1]["a"], "b": currentYear} + else: + proposed[-1]["b"] = currentYear + elif dif > 1: + # append ", currentYear" + # Example: "2013-2020" -> "2013-2020, 2025" + if edit_files: + proposed.append({"t": "single", "a": currentYear, "b": currentYear}) else: - aux[22] = c1 - aux[23] = c2 - aux[24] = c3 - aux[25] = c4 - #list_of_lines[line_nb] = "// Copyright (C) " + list_of_lines[line_nb][17] + list_of_lines[line_nb][18] + list_of_lines[line_nb][19] + list_of_lines[line_nb][20] + "-" + str(currentYear) + " Analog Devices, Inc. All rights reserved.\n" - changed = True + pass + + proposed_text = ", ".join( + (f"{t['a']}" if t["a"] == t["b"] else f"{t['a']}-{t['b']}") for t in proposed + ) + + start_i = m.start('years') + end_i = m.end('years') + head = line[:start_i].rstrip() + tail = line[end_i:] + + tail = re.sub(r'^\s*,\s*', '', tail) + tail = tail.lstrip() + tail = (' ' + tail) if tail else '' + new_line = head + ' ' + proposed_text + tail + + changed = (new_line != line) + + if edit_files and changed: + # files can be changed and header got updated + list_of_lines[line_nb] = new_line + lw.append(file_path + " : license header updated by the script") + header_status = 1 + + elif not edit_files and changed: + # files not to be edited and header is not up-to-date + lw.append(file_path + " : license header cannot be updated") + header_status = 3 else: - # match a single year - match = re.match(r'.*(Copyright\s\(C\)\s20[0-9]{2})', list_of_lines[line_nb]) - - if (match is not None): - ## if the year is different than the currentYear, - ## then must make a year range [17-25] - year = aux[17] + aux[18] + aux[19] + aux[20] - if (year != str(currentYear)): - aux.insert(21, '-') - aux.insert(22, str(get_digit(currentYear, 3))) - aux.insert(23, str(get_digit(currentYear, 2))) - aux.insert(24, str(get_digit(currentYear, 1))) - aux.insert(25, str(get_digit(currentYear, 0))) - #list_of_lines[line_nb] = "// Copyright (C) " + str(year) + "-" + str(currentYear) + " Analog Devices, Inc. All rights reserved.\n" - changed = True + # files can be changed and header got updated + header_status = 2 + + return header_status + + +############################################################################### +# +# Check/normalize a typedef (enum/struct/union) block. +# Rules: +# - base indent >= 2 +# - exactly one space before '{' +# - inner lines indent = base + 2 +# - items must be on separate lines (no one-liner) +# - last item without trailing comma +# - closing on the same line: '} TypeName;' +# - inline closing comment moved to next line (except one-liner case) +############################################################################### +def _check_typedef_block(idx, line_nb, list_of_lines, lw, edit_files, state): + + path = state.get("path", "") + if idx >= len(list_of_lines): + return idx, line_nb + + line = list_of_lines[idx] + + # detect typedef block start + if not re.match(r"^\s*typedef\s+(enum|struct|union)\b", line): + return idx, line_nb + + # base indentation (normalize to even >= 2 if editing) + base_indent = indent_of(line) + desired_indent = even_indent(base_indent, 2) + if base_indent != desired_indent: + emit_line_warning(lw, path, line_nb, idx, idx, + f"typedef indentation should be multiple of 2 and >= 2 (now {base_indent})") + if edit_files: + list_of_lines[idx] = (" " * desired_indent) + line.lstrip() + line = list_of_lines[idx] + base_indent = desired_indent + state["edited"] = True + + # find the statement end line (';' at top level) + end_idx = stmt_end_line(list_of_lines, idx) + if end_idx == -1: + return idx, line_nb # incomplete typedef + + # locate top-level brace span '{ ... }' + span = find_top_level_brace_span(list_of_lines, idx, end_idx) + if span is None: + # do not normalize if no brace initializer is found + return idx, line_nb + + open_l, open_c, close_l, close_c = span + is_one_line = (open_l == close_l) + + # split parts: header (up to '{'), inside text, and trailer after '}' + header_line = list_of_lines[open_l][:open_c] + inside_text = extract_between_braces(list_of_lines, open_l, open_c, close_l, close_c) + + # collect text after '}' up to end of statement (may span multiple lines) + trailer_after = list_of_lines[close_l][close_c+1: (len(list_of_lines[close_l]) if end_idx == close_l else None)] + if close_l < end_idx: + trailer_after = (trailer_after or "") + "".join(list_of_lines[close_l+1:end_idx+1]) + + # extract TypeName; and optional inline //comment appearing after '}' + m_t = re.search(r"\}\s*([A-Za-z_]\w*)\s*;\s*(//.*)?", "}" + trailer_after) + typename = None + inline_cmt = "" + next_line_had_typename = False + if m_t: + typename = m_t.group(1) + inline_cmt = (m_t.group(2) or "").strip() + if not typename and (close_l + 1) <= end_idx: + m_next = re.match(r"^\s*([A-Za-z_]\w*)\s*;\s*(//.*)?\n?$", list_of_lines[close_l + 1]) + if m_next: + typename = m_next.group(1) + next_line_had_typename = True + if not typename: + return idx, line_nb + + # exactly one space before '{' + if header_line.strip(): + trailing_spaces = len(header_line) - len(header_line.rstrip(" ")) + if trailing_spaces != 1: + emit_line_warning(lw, path, line_nb, idx, open_l, + "one space required before '{' in typedef") + + # no one-liner: items must be on separate lines + if is_one_line: + emit_line_warning(lw, path, line_nb, idx, open_l, + "typedef items must be on separate lines") + else: + # first item must not share the '{' line + after_open = list_of_lines[open_l][open_c+1:] + code_after, _ = split_code_comment_sl(after_open) + if code_after.strip(): + emit_line_warning(lw, path, line_nb, idx, open_l, + "first item in typedef must start on a new line") + + # inner lines: indent must be base + 2 (skip comments/empty/backticks) + if not is_one_line: + for j in range(open_l + 1, close_l): + s = list_of_lines[j] + if only_spaces_or_tabs(s) or is_cmt(s) or ("`" in s): + continue + if "{" in s or "}" in s: + continue + if indent_of(s) < (base_indent + 2): + emit_line_warning(lw, path, line_nb, idx, j, + "inner line in typedef must be indented by +2 spaces") + + # find last code line before '}' to check if it ends with ',' + j_last = None + last_code = "" + for j in range(close_l, open_l, -1): + s = list_of_lines[j] + seg = s[:close_c] if j == close_l else s + code, _ = split_code_comment_sl(seg) + code = code.rstrip() + if code: + j_last, last_code = j, code + break + + if j_last is not None and last_code.endswith(","): + emit_line_warning(lw, path, line_nb, idx, j_last, + "last entry in typedef must not end with ','") + + # '}' must align with the typedef line indentation + if indent_of(list_of_lines[close_l]) != base_indent: + emit_line_warning(lw, path, line_nb, idx, close_l, + "'}' in typedef must align with typedef line") + + # closing must be on the same line as '} TypeName; + if next_line_had_typename: + emit_line_warning(lw, path, line_nb, idx, close_l + 1, + "typedef closing must be on the same line as '} TypeName;'") + + # inline comment after the closing is moved to next line (for multi-line case) + if inline_cmt and not is_one_line: + emit_line_warning(lw, path, line_nb, idx, close_l, + "move inline comment to the next line") + + # header: enforce single space before '{'; keep any header //comment on its own line at +2 + h_code, h_cmt = split_code_comment_sl(header_line or "") + h_code = h_code.rstrip() + + canonical = [] + canonical.append((" " * base_indent) + h_code.lstrip() + " {\n") + + # header // comment moved to next line (indented by base + 2) + if h_cmt: + canonical.append((" " * (base_indent + 2)) + h_cmt.strip() + "\n") + + # split inner text by top-level commas; strip trailing ';'; keep ',' only for non-last items + raw_items = [it.strip() for it in split_top_level_commas(inside_text)] + items = [it for it in raw_items if it] + for k, it in enumerate(items): + comma = "," if k < len(items) - 1 else "" + canonical.append((" " * (base_indent + 2)) + strip_trailing_semicol(it) + comma + "\n") + + # closing and type on the same line + canonical.append((" " * base_indent) + f"}} {typename};\n") + + # move inline closing comment to a new line (multi-line only) + if inline_cmt and not is_one_line: + canonical.append((" " * base_indent) + inline_cmt + "\n") + + new_block = canonical + old_block = list_of_lines[idx:end_idx+1] + + # replace atomically if changes are needed + if edit_files and old_block != new_block: + replace_block_atomic(list_of_lines, idx, end_idx, new_block) + state["edited"] = True + + return idx, line_nb + + +############################################################################### +# +# Check/normalize a localparam block. +# Handles both list-style and brace-initializer style: +# - Base indent must be even and >= base_min_indent (default 2) +# - For brace style: +# * header and '{' on same line, with first item glued right after '{' +# * inner items at +2 indent, aligned by a column anchor +# * comments aligned after longest code + 4 spaces +# * last item ends with '};' (final comment, if any, is attached here) +# - For list style: +# * 'localparam' on its own line +# * each item on its own line at +2 indent +# * alignment of '=' across items +# If edit_files is True, the block is reformatted as needed. +############################################################################### +def _check_localparam_block(idx, line_nb, list_of_lines, lw, edit_files, state, + base_min_indent=2, align_equals=True, brace_comment_gap=4): + + path = state.get("path", "") + if idx >= len(list_of_lines): + return idx, line_nb + if not re.match(r"^\s*localparam\b", list_of_lines[idx]): + return idx, line_nb + + # compute statement span (from 'localparam' to terminating ';') + start_idx = idx + end_idx = stmt_end_line(list_of_lines, start_idx) + if end_idx == -1: + emit_line_warning(lw, path, line_nb, start_idx, start_idx, "unterminated localparam (missing ';')") + return idx, line_nb + + base_indent = indent_of(list_of_lines[start_idx]) + desired_base = even_indent(base_indent) + desired_inner = desired_base + 2 + + code0, _ = split_code_comment_sl(list_of_lines[start_idx]) + + # Case 1: one-liner without commas - only indent check + if ";" in code0 and "," not in code0 and ("\n" not in list_of_lines[start_idx]): + if base_indent != desired_base: + emit_line_warning(lw, path, line_nb, start_idx, start_idx, + f"indent for 'localparam' must be multiple of 2 and >= {base_min_indent}") + if edit_files: + list_of_lines[start_idx] = (" " * desired_base) + list_of_lines[start_idx].lstrip() + state["edited"] = True + return idx, line_nb + + # Case 2: brace-initializer form (= '{ ... };) + brace_span = find_top_level_brace_span(list_of_lines, start_idx, end_idx) + if brace_span is not None: + open_l, open_c, close_l, close_c = brace_span + + # collect header tokens before '{' + header_tokens = [] + if open_l > start_idx: + for h in range(start_idx, open_l): + code_h, _ = split_code_comment_sl(list_of_lines[h]) + t = code_h.strip() + if t: + header_tokens.append(t) + + # use the indent of the initial 'localparam' line as anchor + open_line = list_of_lines[open_l] + open_indent = indent_of(open_line) + new_open_indent = even_indent(base_indent) + if open_indent != new_open_indent: + emit_line_warning(lw, path, line_nb, start_idx, open_l, + f"indent for 'localparam' header must be multiple of 2 and >= {base_min_indent}") + + token_before_brace = open_line[open_indent:open_c].strip() + if token_before_brace: + header_tokens.append(token_before_brace) + + header_head = " ".join(header_tokens).rstrip() + + if header_head.endswith("= '"): + # case: = '{...}; + header_prefix = (" " * new_open_indent) + header_head + "{" + else: + # case: = {...}; + header_prefix = (" " * new_open_indent) + header_head + " {" + + header_prefix_len = len(header_prefix) + + desired_base = new_open_indent + desired_inner = desired_base + 2 + inner_indent = " " * desired_inner + inner_indent_len = len(inner_indent) + + inner_text = extract_between_braces(list_of_lines, open_l, open_c, close_l, close_c) + + # split inner text by top-level commas, keeping comments attached to items + items = [] + buf = [] + depth = 0 + i = 0 + L = len(inner_text) + trailing_cmt_for_prev = "" + at_item_start = True + + # helper: push current buffer as item (if any) + def flush_item(): + nonlocal trailing_cmt_for_prev + if trailing_cmt_for_prev and items: + c = items[-1]["cmt"] + items[-1]["cmt"] = (c + (" " if c and not c.endswith(" ") else "") + trailing_cmt_for_prev.strip()).strip() + trailing_cmt_for_prev = "" + code = "".join(buf).strip() + buf.clear() + if code: items.append({"code": code, "cmt": "", "apos": 0}) + + # scan inner text char by char + while i < L: + ch = inner_text[i] + nxt = inner_text[i+1] if i+1 < L else "" + + if ch == "/" and nxt == "/": + j = i + 2 + while j < L and inner_text[j] != "\n": j += 1 + cmt_text = "//" + inner_text[i+2:j] + if at_item_start: trailing_cmt_for_prev += " " + cmt_text + else: buf.append(cmt_text) + i = j + continue + + if ch == "/" and nxt == "*": + j = i + 2 + while j+1 < L and not (inner_text[j] == "*" and inner_text[j+1] == "/"): j += 1 + j2 = min(j+2, L) + cmt_text = inner_text[i:j2] + if at_item_start: trailing_cmt_for_prev += " " + cmt_text + else: buf.append(cmt_text) + i = j2 + continue + + if ch == "\n": + buf.append(ch) + i += 1 + continue + if ch in "([{": + depth += 1 + buf.append(ch) + at_item_start = False + i += 1 + continue + if ch in ")]}": + depth = max(0, depth - 1) + buf.append(ch) + at_item_start = False + i += 1 + continue + + if ch == "," and depth == 0: + flush_item() + at_item_start = True + i += 1 + continue + + if not ch.isspace(): + at_item_start = False + buf.append(ch) + i += 1 + + flush_item() + if not items: + return idx, line_nb + + for it in items: + code0 = strip_trailing_semicol(it["code"]) + + code_sl, sep_sl, cmt_sl = code0.partition("//") + if sep_sl: + code1 = code_sl.rstrip() + cmt_extra = "//" + cmt_sl.strip() else: - changed = False + m = re.search(r"(.*?)(/\*.*?\*/)\s*$", code0) + if m: + code1 = m.group(1).rstrip() + cmt_extra = m.group(2).strip() + else: + code1 = code0.rstrip() + cmt_extra = "" + + it["code"] = soft_norm_eq_keep_cmt(code1) + if cmt_extra: + it["cmt"] = (it["cmt"] + " " + cmt_extra).strip() if it["cmt"] else cmt_extra + it["apos"] = max(0, it["code"].find("'")) + + # determine alignment anchor column (apostrophe or first token) + braces_same_line = (open_l == close_l) + if braces_same_line: + anchor_abs = header_prefix_len + (items[0]["apos"] if items[0]["apos"] > 0 else first_token_start(items[0]["code"])) else: - # if none of the Copyright templates match - template_matches = False + tail = list_of_lines[open_l][open_c+1:] + p_apos = tail.find("'") + p_coma = tail.find(",") + if p_apos != -1 and (p_coma == -1 or p_apos < p_coma): + anchor_abs = header_prefix_len + p_apos + else: + anchor_abs = header_prefix_len + first_token_start(items[0]["code"]) - # files can be changed and header got updated - if (edit_files and changed and template_matches): - list_of_lines[line_nb] = "".join(aux) - lw.append(module_path + " : license header updated by the script") - header_status = 1 + # pre-render each line and measure longest code (for comment alignment) + prelim = [] + max_abs_pre_len = 0 - # header up-to-date already and matches a template - if (not changed and template_matches): - header_status = 2 + for k, it in enumerate(items): + base = (it["apos"] if it["apos"] > 0 else first_token_start(it["code"])) + if k == 0: + curr_base_abs = header_prefix_len + base + prefix = header_prefix # glue first item to '{' + else: + curr_base_abs = inner_indent_len + base + prefix = inner_indent + + # left padding to meet common anchor + need = max(0, anchor_abs - curr_base_abs) + pre_noprefix = (" " * need) + it["code"] + + # add ',' for non-last items, '};' for last item + suff = "," if k < len(items) - 1 else "};" + pre_noprefix += suff + + abs_pre_len = len(prefix) + len(pre_noprefix) + prelim.append((prefix, pre_noprefix, it["cmt"], abs_pre_len)) + if abs_pre_len > max_abs_pre_len: + max_abs_pre_len = abs_pre_len + + # final comment column = longest code + max(4, gap) + comment_abs_col = max_abs_pre_len + max(4, brace_comment_gap) + + # attach final comment (after ';') to last item + last_stmt_raw = list_of_lines[end_idx].rstrip("\n") + _, _, final_cmt_sl = last_stmt_raw.partition("//") + if final_cmt_sl.strip(): + final_cmt = "//" + final_cmt_sl.strip() + else: + mblk = re.search(r";\s*(/\*.*?\*/)\s*$", last_stmt_raw) + final_cmt = mblk.group(1) if mblk else "" + if final_cmt: + pfx, pre_np, cmt, abs_len = prelim[-1] + cmt = (cmt + " " + final_cmt).strip() if cmt else final_cmt + prelim[-1] = (pfx, pre_np, cmt, abs_len) + + # construct the new normalized block + new_block_lines = [] + for pfx, pre_np, cmt, abs_len in prelim: + line = pfx + pre_np + if cmt: + pad = " " * (comment_abs_col - abs_len) + line += pad + cmt + new_block_lines.append(line + "\n") + + # replace block atomically if edit_files and changes are needed + old_block = list_of_lines[start_idx:end_idx+1] + old_norm = [(ln.rstrip() + "\n") for ln in old_block] + new_norm = new_block_lines + if old_norm != new_norm: + diff_line_warnings( + lw, path, line_nb, start_idx, old_norm, new_norm, + "normalize localparam {..}: header with '{' and first item; even indent >=2; align items; comments at longest+4; '};' on last" + ) + if edit_files: + replace_block_atomic(list_of_lines, start_idx, end_idx, new_block_lines) + state["edited"] = True + delta = len(new_block_lines) - len(old_block) + return start_idx + len(new_block_lines) - 1, line_nb + delta + return idx, line_nb + + # Case 3: list form without braces + mkw = re.search(r"\blocalparam\b", code0) + raw_after_kw = code0[mkw.end():] if mkw else "" + joined = raw_after_kw + "".join(list_of_lines[start_idx+1:end_idx+1]) + cut = joined.rfind(";") + body = joined[:cut] if cut != -1 else joined + + # split by commas at top level + parts_raw = [p.strip() for p in split_top_level_commas(body)] + is_list = len(parts_raw) > 1 or (start_idx != end_idx) + if not is_list: return idx, line_nb + + had_issue = False + if base_indent != desired_base: + emit_line_warning(lw, path, line_nb, start_idx, start_idx, + f"indent for 'localparam' must be multiple of 2 and >= {base_min_indent}") + had_issue = True + if raw_after_kw.strip(): + emit_line_warning(lw, path, line_nb, start_idx, start_idx, + "localparam list must start on a new line"); had_issue = True + + for j in range(start_idx + 1, end_idx + 1): + s = list_of_lines[j] + code_j, cmt_j = split_code_comment_sl(s) + if code_j.strip() == "" and cmt_j: continue + if code_j.strip() == "": continue + if indent_of(s) != desired_inner: + emit_line_warning(lw, path, line_nb, start_idx, j, + "items in localparam list must be indented by +2 spaces (from 'localparam')") + had_issue = True + + parts_clean = [strip_trailing_semicol(p) for p in parts_raw if p] + parts_soft = [soft_norm_eq_keep_cmt(p) for p in parts_clean] + + last_stmt_raw = list_of_lines[end_idx].rstrip("\n") + _, _, final_cmt_sl = last_stmt_raw.partition("//") + if final_cmt_sl.strip(): + final_cmt = "//" + final_cmt_sl.strip() + else: + mblk = re.search(r";\s*(/\*.*?\*/)\s*$", last_stmt_raw) + final_cmt = mblk.group(1) if mblk else "" - # files not to be edited and header is not up-to-date - if (not edit_files and changed and template_matches): - lw.append(module_path + " : license header cannot be updated") - header_status = 3 + chosen_segments = _align_equals_segments(parts_soft) if align_equals else parts_soft - # template doesn't match - if (not template_matches): - lw.append(module_path + " : copyright template doesn't match") - header_status = 4 + # build normalized block inline (no inner helper) + new_block_aligned = [] + new_block_aligned.append((" " * desired_base) + "localparam\n") + for k, seg in enumerate(chosen_segments): + term = "," if k < len(chosen_segments) - 1 else ";" + cmt = (" " + final_cmt) if (k == len(chosen_segments) - 1 and final_cmt) else "" + new_block_aligned.append((" " * desired_inner) + seg + term + cmt + "\n") - return header_status + # replace if different or layout issues found + old_block = list_of_lines[start_idx:end_idx+1] + old_len = len(old_block) + old_norm = [(ln.rstrip() + "\n") for ln in old_block] + new_norm = new_block_aligned + + if old_norm != new_norm or had_issue: + if edit_files: + list_of_lines[start_idx:end_idx+1] = new_block_aligned + state["edited"] = True + delta = len(new_block_aligned) - old_len + return start_idx + len(new_block_aligned) - 1, line_nb + delta + + return idx, line_nb ############################################################################### @@ -418,7 +1355,7 @@ def get_and_check_module (module_path, lw, edit_files): ## do not check the license status for the files that must be avoided, ## since it doesn't apply if (header_check_allowed(module_path)): - header_status = check_copyright(list_of_lines, lw, edit_files) + header_status = check_copyright(module_path, list_of_lines, lw, edit_files) # GC: check if the license header is updated if (header_status == -1): edited = False @@ -440,7 +1377,15 @@ def get_and_check_module (module_path, lw, edit_files): changed_line1_sit = -1 extra_chars = False - for line in list_of_lines: + typedef_state = { + "open": False, + "base_indent": 0, + "open_idx": -1, + "path": module_path, + "edited": False + } + + for idx, line in enumerate(list_of_lines): pos_module = line.find("module") pos_endmodule = line.find("endmodule") pos_paranth1 = line.find("(") @@ -451,6 +1396,11 @@ def get_and_check_module (module_path, lw, edit_files): if (pos_endmodule != -1): passed_endmodule = True + + # typedef, locaparam inside of the module + if passed_module and (not passed_endmodule): + idx, line_nb = _check_typedef_block(idx, line_nb, list_of_lines, lw, edit_files, typedef_state) + idx, line_nb = _check_localparam_block(idx, line_nb, list_of_lines, lw, edit_files, typedef_state) # GC: check for spaces at the end of line if (re.search(" +$", line) != None): @@ -549,6 +1499,13 @@ def get_and_check_module (module_path, lw, edit_files): pos_closing = line.find(");") if (pos_closing >= 0): + if idx > 0: + prev_line = list_of_lines[idx - 1].rstrip("\n") + if prev_line.rstrip().endswith(","): + lw.append(f"{module_path} : {line_nb-1} last port in module must not end with ','") + if edit_files: + list_of_lines[idx - 1] = prev_line.rstrip().rstrip(",") + "\n" + end_line = line_nb if ((last_iodef_line + 1 != line_nb) or (pos_closing >= 0)): rest_of_line = line.strip().strip(";").strip().strip(")") @@ -573,7 +1530,7 @@ def get_and_check_module (module_path, lw, edit_files): ## if it's a regular line if ((pos_module == -1) and (pos_endmodule == -1) and (not only_spaces_or_tabs(line)) - and (not is_comment(line)) and (not is_multiline_comment(line)) + and (not is_cmt(line)) and passed_module and (not passed_endmodule) and (line.find("`") == -1)): indent_nb = len(line) - len(line.lstrip()) @@ -623,7 +1580,26 @@ def get_and_check_module (module_path, lw, edit_files): if (edit_files): if (changed_line1 != -1): if (changed_line1_sit == 2 or changed_line1_sit == 3): - list_of_lines.insert(changed_line1, ") (\n") + + insert_pos = changed_line1 + + cur_idx = changed_line1 - 1 + if 0 <= cur_idx < len(list_of_lines): + raw = list_of_lines[cur_idx] + p = raw.find("//") + if p != -1: + # if the comment starts with 'end...' -> insert BEFORE it so it gets pushed down + after = raw[p+2:].lstrip() + if after.lower().startswith("end"): + insert_pos = cur_idx + + # if the line is ONLY a comment (only whitespace before '//'), + # enforce 2 spaces at the start of the comment line + if raw[:p].strip() == "": + list_of_lines[cur_idx] = " " + raw[p:].strip() + "\n" + + # insert the new line with 2 leading spaces + list_of_lines.insert(insert_pos, " ) (\n") if (changed_line2 != -1): if (changed_line1 != -1 and changed_line1_sit > 1): @@ -637,24 +1613,23 @@ def get_and_check_module (module_path, lw, edit_files): # GC: check for lines after endmodule and empty lines # (and delete them, if desired) prev_length = len(list_of_lines) - check_extra_lines (module_path, list_of_lines, lw, edit_files) + changed_extra = check_extra_lines (module_path, list_of_lines, lw, edit_files) + if (edit_files): # if at least one of the things was edited if (changed_line1 != -1 or changed_line2 != -1 or extra_chars - or prev_length != len(list_of_lines) or (header_status == 1)): + or prev_length != len(list_of_lines) + or (header_status == 1) or typedef_state["edited"] + or changed_extra): - # then rewrite the file - with open(module_path, "w") as f: - for line in list_of_lines: + rewrite_file(module_path, list_of_lines) - # GC: check for whitespace at the end of the line w\o \n - aux_line = line[:-1] - aux_line = aux_line.rstrip() - - f.write(aux_line + "\n") - if (extra_chars): + if extra_chars: lw.append(module_path + " : removed extra spaces at the end of lines") + + if changed_extra: + lw.append(module_path + " : add new line after end token") if (not name_found): lw.append(module_path + " : module name couldn't be extracted\n") @@ -666,6 +1641,208 @@ def get_and_check_module (module_path, lw, edit_files): return module_name +############################################################################### +# +# Check for guideline rules applied to package definitions in SystemVerilog files. +# Similar to get_and_check_module but adapted for package-specific rules. +# This can modify the files if edit_files is true. +# Return the string containing the package name and print errors if guideline +# is not respected. +############################################################################### +def get_and_check_package(package_path, lw, edit_files): + + lw_initial_size = len(lw) + lw.append("\nAt package definition:") + + fp = open("%s" % (package_path), "r") + list_of_lines = fp.readlines() + fp.close() + + # Do not check license for files that must be avoided + if header_check_allowed(package_path): + header_status = check_copyright(package_path, list_of_lines, lw, edit_files) + if header_status == -1: + lw.append(f"{package_path} : copyright text doesn't match the pattern") + else: + header_status = -1 + + package_name = "" + name_found = False + passed_package = False + passed_endpackage = False + end_line = -1 + extra_chars = False + + changed_pkg_line = -1 + changed_endpkg_line = -1 + + # typedef normalization state + typedef_state = { + "open": False, + "base_indent": 0, + "open_idx": -1, + "path": package_path, + "edited": False, + } + + line_nb = 1 + for idx, line in enumerate(list_of_lines): + pos_package = line.find("package") + pos_endpackage = line.find("endpackage") + + # trailing spaces + if re.search(r" +$", line) is not None: + extra_chars = True + lw.append(package_path + " : " + str(line_nb) + " extra spaces at the end of line") + + # package start: canonical 'package ;' + if (pos_package == 0) and (not name_found) and (not is_cmt(line)): + passed_package = True + + m_can = re.match(r"^package\s+([A-Za-z_][A-Za-z0-9_$]*)\s*;\s*(//.*)?\n?$", line) + if m_can: + package_name = m_can.group(1) + name_found = True + + fixed = f"package {package_name};" + if m_can.group(2): + fixed += " " + m_can.group(2) + fixed += "\n" + + if fixed != line: + lw.append(package_path + " : " + str(line_nb) + " normalize 'package ;' (no space before and after ';')") + if edit_files: + list_of_lines[idx] = fixed + changed_pkg_line = line_nb + else: + # try to extract a valid name and rebuild canonical line + m_name = re.match(r"^package\s+([A-Za-z_][A-Za-z0-9_$]*)", line) + if m_name: + package_name = m_name.group(1) + name_found = True + lw.append(package_path + " : " + str(line_nb) + " package line must be 'package ;' at column 0") + if edit_files: + # preserve trailing // comment if any + comment = "" + tail = line[len(m_name.group(0)):] + if "//" in tail: + _, cmt = tail.split("//", 1) + comment = "//" + cmt.strip() + fixed = f"package {package_name};" + if comment: + fixed += " " + comment + fixed += "\n" + list_of_lines[idx] = fixed + changed_pkg_line = line_nb + else: + lw.append(package_path + " : " + str(line_nb) + " package name couldn't be extracted") + + # endpackage: must be alone on its line at column 0 + if (pos_endpackage == 0) and (not is_cmt(line)): + passed_endpackage = True + end_line = line_nb + if re.match(r"^endpackage\s*(//.*)?\n?$", line) is None: + lw.append(package_path + " : " + str(line_nb) + " 'endpackage' must be alone on its line at column 0") + if edit_files: + list_of_lines[idx] = "endpackage\n" + changed_endpkg_line = line_nb + + inside_pkg = passed_package and (not passed_endpackage) + if inside_pkg: + # typedef, localparam normalization + idx, line_nb = _check_typedef_block(idx, line_nb, list_of_lines, lw, edit_files, typedef_state) + idx, line_nb = _check_localparam_block(idx, line_nb, list_of_lines, lw, edit_files, typedef_state) + + # minimal indentation rule for other top-level lines (>= 2 spaces), + # excluding lines with directives/backticks or comments. + if (not only_spaces_or_tabs(line)) and (not is_cmt(line)) and (line.find("`") == -1): + if (pos_package == -1) and (pos_endpackage == -1) and (not typedef_state["open"]): + indent_nb = indent_of(line) + if indent_nb < 2: + lw.append(package_path + " : " + str(line_nb) + " no indentation found (need >= 2 spaces)") + + line_nb += 1 + + # remove extra lines after endpackage / empty lines + prev_length = len(list_of_lines) + changed_extra = check_extra_lines(package_path, list_of_lines, lw, edit_files, end_token="endpackage") + + # Rewrite file if needed + if edit_files: + if (changed_pkg_line != -1 or changed_endpkg_line != -1 + or extra_chars or (prev_length != len(list_of_lines)) + or (header_status == 1) or typedef_state["edited"] + or changed_extra): + + rewrite_file(package_path, list_of_lines) + + if extra_chars: + lw.append(package_path + " : removed extra spaces at the end of lines") + + if changed_extra: + lw.append(package_path + " : add new line after end token") + + if not name_found: + lw.append(package_path + " : package name couldn't be extracted") + + # remove the section title if nothing else was logged + if len(lw) == lw_initial_size + 1: + lw.pop() + + return package_name + + +# Global cache: projects that were already checked for system_project.tcl +PROJECTS_CHECKED = set() + +############################################################################### +# +# Check if the project name in the system_project.tcl file matches the +# expected name, which is the relative path from the projects folder, +############################################################################### +def check_project_name_vs_path(modified_files, lw, edit_files=False, checked_projects=None): + + if checked_projects is None: + checked_projects = PROJECTS_CHECKED + + projects_abs = os.path.abspath("projects") + + for f in modified_files: + folder = os.path.abspath(os.path.dirname(f)) + # Walk upward as long as we're inside /projects, but not at its root + while os.path.commonpath([folder, projects_abs]) == projects_abs and os.path.basename(folder) != "projects": + tcl_path = os.path.join(folder, "system_project.tcl") + + if os.path.exists(tcl_path) and folder not in checked_projects: + rel_path = os.path.relpath(folder, projects_abs) + expected_name = rel_path.replace(os.sep, "_") + + lines, found, changed = [], False, False + + with open(tcl_path, "r") as tclf: + for line in tclf: + m = re.match(r'\s*adi_project\s+(\S+)', line) + if m: + found = True + found_name = m.group(1) + if found_name != expected_name: + lw.append(f"{f} : adi_project '{found_name}' does not match expected '{expected_name}'") + if edit_files: + line = re.sub(r'(\s*adi_project\s+)\S+', r'\1' + expected_name, line) + changed = True + lines.append(line) + + if edit_files and found and changed: + with open(tcl_path, "w") as tclf: + tclf.writelines(lines) + lw.append(f"{tcl_path} : adi_project updated to '{expected_name}'") + + checked_projects.add(folder) + break + + folder = os.path.dirname(folder) + + ############################################################################### # # Find all occurrences of the given module (path) in all files from the given @@ -687,7 +1864,7 @@ def find_occurrences (directory, module_name, list_of_files): for file in files: fullpath = os.path.join(folder, file) - if (not check_filename(fullpath)): + if (not check_hdl_filename(fullpath)): continue search = False @@ -698,7 +1875,7 @@ def find_occurrences (directory, module_name, list_of_files): ## the file with the module definition is not accepted and ## neither the files that have to be avoided - if (search and file != (module_name + ".v")): + if search and file not in (module_name + ".v", module_name + ".sv"): with codecs.open(fullpath, 'r', encoding='utf-8', errors='ignore') as f: line_nb = 1 @@ -798,7 +1975,7 @@ def set_occurrence_lines (occurrence_item, list_of_lines): ############################################################################### # -# Check for the guideline rules applied to the module instaces and output +# Check for the guideline rules applied to the module instances and output # warnings for each line, if any. ############################################################################### def check_guideline_instances (occurrence_item, lw): @@ -963,7 +2140,7 @@ def check_guideline_instances (occurrence_item, lw): ############################################################################### # -# Check guideline for Verilog files in repository +# Check guideline for Verilog and SystemVerilog files in repository ############################################################################### ## all files given as parameters to the script (or all files from repo @@ -973,7 +2150,8 @@ def check_guideline_instances (occurrence_item, lw): edit_files = False guideline_ok = True # detect all modules from current directory (hdl) -all_modules = detect_all_modules("./") +all_hdl_files = detect_all_hdl_files("./") + xilinx_modules = [] xilinx_modules.append("ALT_IOBUF") @@ -986,6 +2164,7 @@ def check_guideline_instances (occurrence_item, lw): xilinx_modules.append("BUFGMUX") #xilinx_modules.append("BUFGMUX_1") xilinx_modules.append("BUFGMUX_CTRL") +xilinx_modules.append("BUFH") xilinx_modules.append("BUFIO") xilinx_modules.append("BUFR") xilinx_modules.append("GTHE3_CHANNEL") @@ -1022,10 +2201,10 @@ def check_guideline_instances (occurrence_item, lw): # look in the folder_path = current folder for folder, dirs, files in os.walk("./"): for name in files: - if((name == sys.argv[arg_nb]) and (check_filename(name))): + if((name == sys.argv[arg_nb]) and (check_hdl_filename(name))): #module_path = os.path.abspath(os.path.join(folder, sys.argv[arg_nb])) - module_path = os.path.join(folder, sys.argv[arg_nb]) - modified_files.append(module_path) + file_path = os.path.join(folder, sys.argv[arg_nb]) + modified_files.append(file_path) arg_nb += 1 # -p means a path/s will be specified @@ -1038,22 +2217,23 @@ def check_guideline_instances (occurrence_item, lw): arg_nb = 2 while (arg_nb < len(sys.argv)): if (os.path.exists(sys.argv[arg_nb])): - if (check_filename(sys.argv[arg_nb])): + if (check_hdl_filename(sys.argv[arg_nb])): modified_files.append(sys.argv[arg_nb]) else: + print(f"File {sys.argv[arg_nb]} doesn't exist!") error_files.append(sys.argv[arg_nb]) arg_nb += 1 # -e means it will be run on all files, making changes in them if (sys.argv[1] == "-e"): edit_files = True - modified_files = detect_all_modules("./") + modified_files = detect_all_hdl_files("./") else: ## if there is no argument then the script is run on all files, ## and without making changes in them edit_files = False - modified_files = detect_all_modules("./") + modified_files = detect_all_hdl_files("./") # no matter the number of arguments if (len(modified_files) <= 0): @@ -1061,46 +2241,55 @@ def check_guideline_instances (occurrence_item, lw): guideline_ok = True sys.exit(0) else: - for module_path in all_modules: - module_name = get_file_name(module_path) + for file_path in all_hdl_files: + + file_name = get_file_name(file_path) + pkg_module_name = file_name # list of warnings lw = [] # if the detected module is between the modified files - if (string_in_list(module_path, modified_files)): - module_name = get_and_check_module(module_path, lw, edit_files) - file_name = get_file_name(module_path) + if (string_in_list(file_path, modified_files)): + if detect_file_unit_(file_path) == "package": + # if the file is a package, then check the package definition + pkg_module_name = get_and_check_package(file_path, lw, edit_files=edit_files) + else: + # if the file is a module, then check the module definition + pkg_module_name = get_and_check_module(file_path, lw, edit_files=edit_files) - # file_name is without the known extension, which is .v - if (module_name != file_name): + # file_name is without the known extension, which is .v or .sv + if (pkg_module_name != file_name): + print(f"\n -> pkg_module_name {pkg_module_name} != file_name {file_name}") # applies only to the library folder - if (module_path.find("library") != -1): + if (file_path.find("library") != -1): guideline_ok = False - error_files.append(module_path) + error_files.append(file_path) + # check if the project name matches the path and add warnings to the same lw list + check_project_name_vs_path([file_path], lw, edit_files, checked_projects=PROJECTS_CHECKED) ## system_top modules won't be instantiated anywhere in other ## Verilog or SystemVerilog files - if (module_path.find("system_top") == -1): + if (file_path.find("system_top") == -1): # will search for instances only in the files given as arguments - occurrences_list = find_occurrences("./", module_name, modified_files) + occurrences_list = find_occurrences("./", pkg_module_name, modified_files) if (len(occurrences_list) > 0): for occurrence_item in occurrences_list: check_guideline_instances(occurrence_item, lw) if (len(lw) > 0): guideline_ok = False - print ("\n -> For %s in:" % module_path) + print ("\n -> For %s in:" % file_path) for message in lw: print(message) - for module_name in xilinx_modules: + for file_name in xilinx_modules: lw = [] - xilinx_occ_list = find_occurrences("./", module_name, modified_files) + xilinx_occ_list = find_occurrences("./", file_name, modified_files) if (len(xilinx_occ_list) > 0): for xilinx_occ_it in xilinx_occ_list: # if the xilinx module was found in the files that are of interest - for it in all_modules: + for it in all_hdl_files: if (xilinx_occ_it.path == it): # only then to check the guideline check_guideline_instances(xilinx_occ_it, lw) @@ -1111,7 +2300,7 @@ def check_guideline_instances (occurrence_item, lw): for message in lw: if (list_has_substring(modified_files, message)): if (not title_printed): - print ("\n -> For %s in:" % module_name) + print ("\n -> For %s in:" % file_name) title_printed = True guideline_ok = False print(message) diff --git a/.github/workflows/check_for_guideline_rules.yml b/.github/workflows/check_for_guideline_rules.yml index e36dfe9e9d..214c1a8f5f 100644 --- a/.github/workflows/check_for_guideline_rules.yml +++ b/.github/workflows/check_for_guideline_rules.yml @@ -25,7 +25,7 @@ jobs: # version range python-version: '3.10' - name: Checkout repository code - uses: actions/checkout@v4 + uses: actions/checkout@v6.0.0 # repository that retrieves all the changed files - name: Load get-changed-files repo uses: Ana06/get-changed-files@v2.3.0 @@ -35,11 +35,11 @@ jobs: - name: Print changed files run: | echo "::group::Click here to see the Changed files" - echo "${{ steps.changed_files.outputs.all }}" + echo "${{ steps.changed_files.outputs.added_modified_renamed }}" echo "::endgroup::" - name: Executing Py script for guideline check id: execution # -p flag means that files will be specified with their relative path run: | - python ./.github/scripts/check_guideline.py -p ${{ steps.changed_files.outputs.all }} + python ./.github/scripts/check_guideline.py -p ${{ steps.changed_files.outputs.added_modified_renamed }} shell: sh diff --git a/docs/user_guide/hdl_coding_guidelines.rst b/docs/user_guide/hdl_coding_guidelines.rst index 335e0d7c0f..e381d260df 100755 --- a/docs/user_guide/hdl_coding_guidelines.rst +++ b/docs/user_guide/hdl_coding_guidelines.rst @@ -36,18 +36,23 @@ A. Layout **A1** +HDL source files **must not** begin or end with empty lines, but **must** +terminate with exactly one newline character. + +**A2** + Spaces **must** be used instead of tabs. This allows the code to be properly visualized by any editor. **Do not** leave spaces at the end of a line. The following editor settings **must** be used: *Tab Size*: 2, *Indent Size*: 2. -**A2** +**A3** One white space **must** be inserted around operators, such as =, ==, &&, \|\|, &, \|, ^, etc. -.. _example-a2: +.. _example-a3: Incorrect: @@ -61,11 +66,11 @@ Correct: if ((my_signal == 1'b0) && (my_bus[3:0] == 4'd5)) -**A3** +**A4** The *always* block *should* have a space before \*\*@\*\* symbol. -.. _example-a3: +.. _example-a4: Incorrect: @@ -83,19 +88,19 @@ Correct: ... end -**A4** +**A5** The Verilog ``begin``/``end`` block **must** always be used, even if there is only one statement. This makes adding lines of code much easier and with fewer errors. -**A5** +**A6** Indentation levels **must** be used to show code nesting. Blank lines may be used as desired to improve code readability, but *not* in all cases. -.. _example-a5: +.. _example-a6: Incorrect: @@ -130,14 +135,14 @@ Correct: statement5; end -**A6** +**A7** In a ``case`` definition, indentation levels **must** be used to offset the statements that are encapsulated, but the use of blank lines can be used or omitted to best show the statement groupings (if really necessary). ``end`` should be indented as in the correct example. -.. _example-a6: +.. _example-a7: Incorrect: @@ -181,13 +186,13 @@ Correct: end endcase -**A7** +**A8** Alignment **should** be used in declarations, assignments, multi-line statements, and end of line comments. The code **must** be written in a tabular format. -.. _example-a7: +.. _example-a8: Incorrect: @@ -211,14 +216,14 @@ Correct: wire [ 2:0] my_select; // description -**A8** +**A9** Parentheses **must** be used around all boolean statements and in complex equations, in order to force the order of operations and avoid confusion. Complex boolean expressions *should* be expressed as multi-line aligned statements. -.. _example-a8: +.. _example-a9: Incorrect: @@ -243,12 +248,12 @@ Correct: my_delayed_signal1 = !your_signal; end -**A9** +**A10** A line **must** not contain more than one statement. **Do not** concatenate multiple statements on the same line. -.. _example-a9: +.. _example-a10: Incorrect: @@ -263,16 +268,16 @@ Correct: upper_en = (p5type && xadr1[0]); lower_en = (p5type && !xadr1[0]); -**A10** +**A11** In module instances: -**A10.1** +**A11.1** **All** parameters and ports, **must** be written on a separate line, even if there are few of them or their names are short. -.. _example-a10.1: +.. _example-a11.1: Incorrect: @@ -290,7 +295,7 @@ Correct: ) i_my_module ( .clk (clk)); -**A10.2** +**A11.2** When instantiating a module, the label of the module instance **must** be on a separate line, with the closing parenthesis of the @@ -298,7 +303,7 @@ parameters list (if that's the case) and the opening parenthesis of the ports list. The closing parenthesis of the ports list must be right next to the last parenthesis of the last port. -.. _example-a10.2: +.. _example-a11.2: .. code-block:: :linenos: @@ -314,21 +319,21 @@ to the last parenthesis of the last port. .en (en), .response_out (response_out)); -**A10.3** +**A11.3** Commented parts of code **must** not be added to the main branch (i.e if, case, module instances, etc). -**A11** +**A12** In module declarations: -**A11.1** +**A12.1** Verilog modules **must** use Verilog-2001 style parameter declarations. This increases legibility and consistency. -.. _example-a11.1: +.. _example-a12.1: .. code-block:: :linenos: @@ -355,61 +360,89 @@ declarations. This increases legibility and consistency. output interf2_data_out ); -**A11.2** +**A12.2** Comments are allowed inside a module declaration **only** for separating the interfaces by specifying the name and giving supplementary explanations. -**A11.3** +**A12.3** When declaring a module, the closing parenthesis of the parameters list **must** be on the same line with the last parameter and with the opening parenthesis of the ports list (as shown in the correct examples). -**A11.4** +**A12.4** After ``endmodule`` there **must** be only one newline, and nothing else after. -**A12** +**A13** + +SystemVerilog packages must follow these rules: + +**A13.1** + +The package name **must** be the same as the file name. + +**A13.2** + +The ``package ;`` declaration **must** start at the beginning of the line, +with no leading spaces and no extra spaces before or after ``;``. + +**A13.3** + +The ``endpackage`` statement **must** be on a separate line, with no leading +spaces and no extra spaces before or after ``;``. + +**A13.4** + +There **must** be only one newline after ``endpackage``, and nothing else after. + +**A13.5** + +In a package body, indentation levels must be used to offset the enclosed +statements. Blank lines may be used or omitted to improve readability, but only +where they help clarity. + +**A14** Ports **must** be indicated individually; that is, one port per line must be declared, using the direction indication and data type with each port. -**A13** +**A15** Signals and variables **must** be declared individually; that is, one signal/variable per line **must** be declared. -**A14** +**A16** All ports and signals **must** be grouped by interface. Group ports declaration by direction starting with input, inout and output ports. -**A15** +**A17** The clock and reset ports **must** be declared first. -**A16** +**A18** Verilog wires and registers declarations **must** be grouped in separate sections. **Firstly** register types and then wire types. -**A17** +**A19** The source files *should* have the format shown in Annex 1 for -Verilog code and Annex 2 for VHDL code. +Verilog code, Annex 2 for VHDL code and Annex 3 for SystemVerilog code. -**A18** +**A20** Local parameters **must** be declared first, before declaring wires or registers. -.. _example-a18: +.. _example-a20: .. code-block:: :linenos: @@ -426,6 +459,94 @@ or registers. wire [ 2:0] my_wire1; wire my_wire2; +**A21** + +Local parameter formatting: + +**A21.1** + +``localparam`` statements **must** be indented by a number of spaces that is a +multiple of 2, with a minimum of 2 spaces. +Items in a list (when present) must be indented by +2 spaces relative to the +``localparam`` line. + +**A21.2** + +**Multi-line list form** + +When declaring multiple local parameters as a list: + * the list must start on a new line after the ``localparam`` keyword; + * each intermediate item must end with a comma ``,``; the last item must not end with a comma; + * the ``=`` operator must be column-aligned across all items in the list. + +Correct: + +.. code-block:: + :linenos: + + localparam + PCORE_VERSION = 32'h00020062, + PCORE_MAGIC = 32'h5444444E; + +**A21.3** + +**Concatenation form ({ … })** + +When declaring a local parameter using concatenation: + * the first element may appear on the same line as ``{``; all subsequent elements must start on new lines, indented by +2 spaces; + * items must be aligned to a common anchor (apostrophes in literals or, otherwise, the first token) for visual alignment; + * inline comments for items should align to a common column, at least 4 spaces after the longest code element; + * the closing ``};`` must be attached to the last element on the same line; + * comments written after a comma belong to the preceding element and must be attached to that element’s line; + * any trailing comment after the final semicolon of the whole statement must be attached to the last element’s line. + +Correct: + +.. code-block:: + :linenos: + + localparam [31:0] CORE_VERSION = {16'h0002, /* MAJOR */ + 8'h01, /* MINOR */ + 8'h01}; /* PATCH */ + +**A22** + +In ``typedef`` declarations: + +**A22.1** + +The ``typedef`` line must be indented by a number of spaces that is a multiple +of 2, with a minimum of 2 spaces. + +**A22.2** + +All items must be on separate lines (no one-liners), indented by +2 spaces +relative to the typedef line. + +**A22.3** + +The closing ``}`` must be aligned with the ``typedef`` line indentation. +The type name and semicolon must be on the same line as ``}``. + +**A22.4** + +Inline comments after ``} type_name;`` must be moved to a separate line. + +Correct + +.. _example-a22: + +.. code-block:: + :linenos: + + typedef enum logic [1:0] { + IDLE = 2'b00, + ARMED = 2'b01, + WAITING = 2'b10, + RUNNING = 2'b11 + } type_name; + // comment describing the type + B. Naming Conventions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -598,7 +719,7 @@ D. General **D1** -A file **must** contain a single module. +A file **must** contain a single module or package. **D2** @@ -872,8 +993,120 @@ Annex 2 VHDL file format end Behavioral; -4. References -------------------------------------------------------------------------------- +Annex 3 SystemVerilog package file format +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: verilog + :linenos: + + // *************************************************************************** + // *************************************************************************** + // Copyright (C) year-year Analog Devices, Inc. All rights reserved. + // + // In this HDL repository, there are many different and unique modules, consisting + // of various HDL (Verilog or VHDL) components. The individual modules are + // developed independently, and may be accompanied by separate and unique license + // terms. + // + // The user should read each of these license terms, and understand the + // freedoms and responsibilities that he or she has by using this source/core. + // + // This core 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. + // + // Redistribution and use of source or resulting binaries, with or without modification + // of this file, are permitted under one of the following two license terms: + // + // 1. The GNU General Public License version 2 as published by the + // Free Software Foundation, which can be found in the top level directory + // of this repository (LICENSE_GPL2), and also online at: + // + // + // OR + // + // 2. An ADI specific BSD license, which can be found in the top level directory + // of this repository (LICENSE_ADIBSD), and also on-line at: + // https://github.com/analogdevicesinc/hdl/blob/main/LICENSE_ADIBSD + // This will allow to generate bit files and not release the source code, + // as long as it attaches to an ADI device. + // + // *************************************************************************** + // *************************************************************************** + + package axi_tdd_pkg; + + typedef enum logic [1:0] { + IDLE = 2'b00, + ARMED = 2'b01, + WAITING = 2'b10, + RUNNING = 2'b11 + } state_t; + // comment describing the type + + localparam + PCORE_VERSION = 32'h00020062, + PCORE_MAGIC = 32'h5444444E; // "TDDN", big endian + + // register address offset + localparam + ADDR_TDD_VERSION = 8'h00, + ADDR_TDD_ID = 8'h01, + ADDR_TDD_SCRATCH = 8'h02, + ADDR_TDD_IDENTIFICATION = 8'h03, + ADDR_TDD_INTERFACE = 8'h04, + ADDR_TDD_DEF_POLARITY = 8'h05, + ADDR_TDD_CONTROL = 8'h10, + ADDR_TDD_CH_ENABLE = 8'h11, + ADDR_TDD_CH_POLARITY = 8'h12, + ADDR_TDD_BURST_COUNT = 8'h13, + ADDR_TDD_STARTUP_DELAY = 8'h14, + ADDR_TDD_FRAME_LENGTH = 8'h15, + ADDR_TDD_SYNC_CNT_LOW = 8'h16, + ADDR_TDD_SYNC_CNT_HIGH = 8'h17, + ADDR_TDD_STATUS = 8'h18, + ADDR_TDD_CH_ON = 8'h20, + ADDR_TDD_CH_OFF = 8'h21; + + // channel offset values + localparam + CH0 = 0, + CH1 = 1, + CH2 = 2, + CH3 = 3, + CH4 = 4, + CH5 = 5, + CH6 = 6, + CH7 = 7, + CH8 = 8, + CH9 = 9, + CH10 = 10, + CH11 = 11, + CH12 = 12, + CH13 = 13, + CH14 = 14, + CH15 = 15, + CH16 = 16, + CH17 = 17, + CH18 = 18, + CH19 = 19, + CH20 = 20, + CH21 = 21, + CH22 = 22, + CH23 = 23, + CH24 = 24, + CH25 = 25, + CH26 = 26, + CH27 = 27, + CH28 = 28, + CH29 = 29, + CH30 = 30, + CH31 = 31; + + endpackage + + 4. References + ------------------------------------------------------------------------------- `[1] Philippe Garrault, Brian Philofsky, "HDL Coding Practices to Accelerate Design Performance", Xilinx, 2006