diff --git a/util/design/sparse-fsm-encode.py b/util/design/sparse-fsm-encode.py index ccbfa1e9e15f8..b14c015f0486c 100755 --- a/util/design/sparse-fsm-encode.py +++ b/util/design/sparse-fsm-encode.py @@ -22,7 +22,10 @@ import argparse import logging as log import math +import os +import platform import random +import subprocess import sys from lib.common import get_hd, hd_histogram, wrapped_docstring @@ -55,6 +58,30 @@ """ +def get_python_version() -> str: + """Returns the current Python version as a string.""" + return platform.python_version() + + +def get_git_commit_hash(short: bool = False) -> str | None: + """Returns the current commit hash of the repo containing the CWD, if it exists.""" + cmd = ['git', 'rev-parse'] + if short: + cmd.append('--short') + cmd.append('HEAD') + try: + return subprocess.check_output( + cmd, + stderr=subprocess.DEVNULL, + text=True, + cwd=os.getcwd() + ).strip() + except subprocess.CalledProcessError as e: + # Not a git repo, or some other git error. + log.debug("Error getting git commit hash: %s", str(e)) + return None + + class EncodingGenerator: def __init__(self, min_hd: int, num_states: int, encoding_len: int, @@ -102,7 +129,7 @@ def generate(self) -> None: num_restarts = 0 rand = random.Random() - rand.seed(self.seed) + rand.seed(self.seed, version=2) rnd = rand.getrandbits(self.encoding_len) while len(self.encodings) < self.num_states: @@ -136,22 +163,38 @@ def generate(self) -> None: def _print_comment(self): if self.language == "c": comment = " *" - print(C_INSTRUCTIONS) + print(C_INSTRUCTIONS, file=sys.stderr) print("/*") elif self.language == "sv": comment = "//" - print(SV_INSTRUCTIONS) + print(SV_INSTRUCTIONS, file=sys.stderr) elif self.language == "rust": comment = "///" - print(RUST_INSTRUCTIONS) + print(RUST_INSTRUCTIONS, file=sys.stderr) print("///```text") else: raise ValueError(f"Unsupported language: {self.lanugage}") + # Retrieve the current git commit and Python version for reproducibility. + commit = get_git_commit_hash(short=True) + version = get_python_version() + + print(f"{comment} Encoding generated", end="") + if commit: + print(f" at commit {commit}", end="") + if version: + print(f" using Python {version}", end="") print( - f"{comment} Encoding generated with:\n" - f"{comment} $ ./util/design/sparse-fsm-encode.py -d {self.min_hd} -m {self.num_states} -n {self.encoding_len} \\\n" # noqa: E501 - f"{comment} -s {self.seed} --language={self.language}\n" + " with:\n" + f"{comment} $ ./util/design/sparse-fsm-encode.py" + f" --language={self.language}" + + (" --avoid-zero" if self.avoid_zero else "") + + f" \\\n" + f"{comment} " + f" --seed {self.seed}" + f" --distance {self.min_hd}" + f" --states {self.num_states}" + f" --bits {self.encoding_len}\n" f"{comment}\n" f"{comment} Hamming distance histogram:\n" f"{comment}") @@ -211,12 +254,12 @@ def _print_c(self): print("} my_state_t;") def _print_rust(self): - print("#[derive(Clone,Copy,Eq,PartialEq,Ord,ParitalOrd,Hash,Debug)]\n" + print("#[derive(Clone, Copy, Eq, PartialEq, Ord, ParitalOrd, Hash, Debug)]\n" "#[repr(transparent)]\n" f"struct MyState(u{self.encoding_len});\n" "\n" "impl MyState {") - fmt_str = " const MY_STATE{0:}: MyState {1:}= MyState(0x{3:0" + str( + fmt_str = " const MY_STATE{0:}: MyState {1:}= MyState(0x{3:0" + str( math.ceil(self.encoding_len / 4)) + "x});" for j, k in enumerate(self.encodings): pad = " " * (len(str(self.num_states)) - len(str(j))) @@ -241,26 +284,26 @@ def main(): description=wrapped_docstring(), formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument( - '-d', + '-d', '--distance', type=int, default=5, metavar='', help='Minimum Hamming distance between encoded states.') - parser.add_argument('-m', + parser.add_argument('-m', '--states', type=int, default=7, metavar='<#states>', help='Number of states to encode.') - parser.add_argument('-n', + parser.add_argument('-n', '--bits', type=int, default=10, metavar='<#nbits>', help='Encoding length [bit].') - parser.add_argument('-s', + parser.add_argument('-s', '--seed', type=int, metavar='', help='Custom seed for RNG.') - parser.add_argument('--language', + parser.add_argument('-l', '--language', choices=['sv', 'c', 'rust'], default='sv', help='Choose the language of the generated enum.') @@ -272,32 +315,32 @@ def main(): args = parser.parse_args() if args.language in ['c', 'rust']: - if args.n not in [8, 16, 32]: + if args.bits not in [8, 16, 32]: log.error("When using C or Rust, widths must be a power-of-two " - f"at least a byte (8 bits) wide. You chose {args.n}.") + f"at least a byte (8 bits) wide. You chose {args.bits}.") sys.exit(1) - if args.m < 2: + if args.states < 2: log.error('Number of states (m) must be at least 2.') sys.exit(1) - if args.m > 2**args.n: + if args.states > 2**args.bits: log.error( - f'Statespace 2^{args.n} not large enough to accommodate {args.m} states.' + f'Statespace 2^{args.bits} not large enough to accommodate {args.states} states.' ) sys.exit(1) - if (args.d >= args.n) and not (args.d == args.n and args.m == 2): + if (args.distance >= args.bits) and not (args.distance == args.bits and args.states == 2): log.error( - f'State is only {args.n} bits wide, which is not enough to fulfill a ' - f'minimum Hamming distance constraint of {args.d}.') + f'State is only {args.bits} bits wide, which is not enough to fulfill a ' + f'minimum Hamming distance constraint of {args.distance}.') sys.exit(1) - if args.d <= 0: + if args.distance <= 0: log.error('Hamming distance must be > 0.') sys.exit(1) - if args.d < 3: + if args.distance < 3: log.warning( 'A value of 4-5 is recommended for the minimum Hamming distance ' 'constraint. At a minimum, this should be set to 3.') @@ -305,14 +348,14 @@ def main(): # If no seed has been provided, we choose a seed and print it # into the generated output later on such that this run can be # reproduced. - if args.s is None: + if args.seed is None: random.seed() - args.s = random.getrandbits(32) + args.seed = random.getrandbits(32) - generator = EncodingGenerator(min_hd=args.d, - num_states=args.m, - encoding_len=args.n, - seed=args.s, + generator = EncodingGenerator(min_hd=args.distance, + num_states=args.states, + encoding_len=args.bits, + seed=args.seed, language=args.language, avoid_zero=args.avoid_zero) generator.generate()