"""gnucobol-std-dialects-ibm.py - read GnuCOBOL's dialect configuration files the way cobc 3.2 reads them, print what -std= changes, and work out what the lesson's probe program (gnucobol-std-dialects-ibm.cob) prints under each setting. Inputs : the config directory of a GnuCOBOL 3.2 installation or source tree (for example /usr/local/share/gnucobol/config, or gnucobol-3.2/config); `cobc --info` prints the directory your compiler uses (COB_CONFIG_DIR). Outputs: text on standard output. Standard library only (Python 3.8+). Run: python gnucobol-std-dialects-ibm.py --config DIR settings table python gnucobol-std-dialects-ibm.py --config DIR --expect ibm probe output python gnucobol-std-dialects-ibm.py --config DIR --expect ibm -fbinary-truncate python gnucobol-std-dialects-ibm.py --config DIR --words reserved-word lists python gnucobol-std-dialects-ibm.py --selftest [--testsuite DIR] How the files are read (cobc/config.c in the 3.2 release): - one "key: value" per line ("key value" and "key=value" also work), # starts a comment; - include "file" reads another file at that point; a later line overrides an earlier one; - a support value written with a leading + ("+obsolete") only RELAXES the current value: it is applied when the current value is stricter, and never over skip or ignore; - -std=NAME loads NAME.conf; -f= and -f[no-] on the command line are applied after the file, whatever their position on the command line. """ import argparse import re import sys from pathlib import Path SUPPORT = ["ok", "warning", "archaic", "obsolete", "skip", "ignore", "error", "unconformable"] LIST_KEYS = {"reserved", "not-reserved", "intrinsic-function", "not-intrinsic-function", "register", "not-register", "system-name", "not-system-name"} # the names cobc 3.2 --help lists after -std= (cobc/help.c) HELP_DIALECTS = ["default", "cobol2014", "cobol2002", "cobol85", "xopen", "ibm-strict", "ibm", "mvs-strict", "mvs", "mf-strict", "mf", "bs2000-strict", "bs2000", "acu-strict", "acu", "rm-strict", "rm", "gcos-strict", "gcos"] # settings that change how a compiled program behaves at run time or how data is laid out BEHAVIOUR = ["binary-size", "binary-truncate", "binary-byteorder", "pretty-display", "move-ibm", "perform-osvs", "arithmetic-osvs", "sticky-linkage", "hostsign", "defaultbyte", "init-justify", "odoslide", "assign-clause", "filename-mapping", "subscript-check", "format", "reserved-words", "word-length", "literal-length", "pic-length", "relax-syntax-checks", "complex-odo", "indirect-redefines", "larger-redefines"] TABLE_COLUMNS = ["default", "ibm-strict", "ibm", "mvs", "mf"] def load(config_dir, filename, state=None, stack=()): """Read one configuration file (and what it includes) into state, as cobc does.""" if state is None: state = {"@words": [], "@lists": {}} if filename in stack: raise ValueError(f"{filename}: recursive inclusion") text = (Path(config_dir) / filename).read_text(encoding="utf-8", errors="replace") for raw in text.splitlines(): line = raw.split("#", 1)[0].strip() if not line: continue m = re.match(r"([A-Za-z0-9-]+)[ \t:=]+(.*)$", line) if not m: continue key, value = m.group(1), m.group(2).strip() if key in ("include", "includeif"): inc = value.strip('"') if inc.endswith(".words"): state["@words"].append(inc) if key == "includeif" and not (Path(config_dir) / inc).exists(): continue load(config_dir, inc, state, stack + (filename,)) continue if key in LIST_KEYS: state["@lists"].setdefault(key, []).append(value) continue if value.startswith("+") and value[1:] in SUPPORT: new, cur = value[1:], state.get(key) if cur in SUPPORT and cur not in ("skip", "ignore") \ and SUPPORT.index(cur) > SUPPORT.index(new): state[key] = new continue state[key] = value.strip('"') if key == "name" else value return state def resolve(config_dir, dialect, flags=()): """Settings for `cobc -std= `; flags like -fbinary-truncate, -fno-pretty-display or -fbinary-size=2-4-8.""" st = load(config_dir, f"{dialect}.conf") for f in flags: m = re.match(r"-f(no-)?([a-z0-9-]+)(?:=(.*))?$", f) if not m: raise ValueError(f"not a -f flag: {f}") no, key, val = m.groups() if key not in st: raise ValueError(f"unknown configuration key: {key}") st[key] = val if val is not None else ("no" if no else "yes") return st # --- storage size of a USAGE BINARY (COMP) item: cobc/field.c compute_binary_size ---------- def binary_size(digits, signed, mode): if mode == "1-2-4-8": return 1 if digits <= 2 else 2 if digits <= 4 else 4 if digits <= 9 else 8 if mode == "2-4-8": return 2 if digits <= 4 else 4 if digits <= 9 else 8 if mode == "1--8": signed_top = [2, 4, 6, 9, 11, 14, 16, 18] # largest digit count for 1..8 bytes unsigned_top = [2, 4, 7, 9, 12, 14, 16, 18] for n, top in enumerate(signed_top if signed else unsigned_top, start=1): if digits <= top: return n raise ValueError(f"binary-size {mode} with {digits} digits") # --- DISPLAY of a USAGE BINARY item: libcob/termio.c display_common + numeric.c ------------- BIN_DIGITS = [1, 3, 5, 8, 10, 13, 15, 17, 20] # termio.c bin_digits[size] def display_binary(value, digits, signed, size, pretty): if pretty: # pretty_display_numeric: edit into +9(n) (signed) or 9(n) body = str(abs(value) % 10 ** digits).rjust(digits, "0") return (("-" if value < 0 else "+") + body) if signed else body width = BIN_DIGITS[size] # cob_print_realbin return ("%+*.*d" % (width, width, value)) if signed else ("%*.*d" % (width, width, value)) def store_binary(value, digits, signed, size, truncate): """Result of an arithmetic store into a COMP item without ON SIZE ERROR (cobc/typeck.c build_store_option; libcob/numeric.c cob_decimal_get_binary).""" if truncate: # COB_STORE_TRUNC_ON_OVERFLOW: keep the low-order PICTURE digits r = abs(value) % 10 ** digits return -r if (value < 0 and signed) else r bits = size * 8 # no truncation: the value only has to fit the bytes v = value % (1 << bits) if signed and v >= 1 << (bits - 1): v -= 1 << bits return v # --- the probe program -------------------------------------------------------------------- PROBE_FIELDS = [ # name, PICTURE text, digits, signed, VALUE ("WS-TINY", "9(2) ", 2, False, 12), ("WS-COUNT", "9(4) ", 4, False, 5000), ("WS-DELTA", "S9(4)", 4, True, -12), ("WS-TOTAL", "9(5) ", 5, False, 12345), ] def probe(settings): mode = settings["binary-size"] pretty = settings["pretty-display"] == "yes" trunc = settings["binary-truncate"] == "yes" out, sizes = [], {} for name, pic, digits, signed, value in PROBE_FIELDS: size = binary_size(digits, signed, mode) sizes[name] = size out.append(f"{name:<9} PIC {pic} COMP BYTES {size:02d} SHOWS " f"{display_binary(value, digits, signed, size, pretty)}") count = store_binary(5000 + 7000, 4, False, sizes["WS-COUNT"], trunc) out.append(f"ADD 7000 TO WS-COUNT SHOWS " f"{display_binary(count, 4, False, sizes['WS-COUNT'], pretty)}") out.append("WS-COUNT IS 12000" if count == 12000 else "WS-COUNT IS 2000" if count == 2000 else "WS-COUNT IS SOMETHING ELSE") return out def table(config_dir): cols = {d: resolve(config_dir, d) for d in TABLE_COLUMNS} w = max(len(k) for k in BEHAVIOUR) lines = [f"{'setting':<{w}} " + " ".join(f"{d:<11}" for d in TABLE_COLUMNS)] for k in BEHAVIOUR: lines.append(f"{k:<{w}} " + " ".join(f"{cols[d].get(k, '-'):<11}" for d in TABLE_COLUMNS)) return [l.rstrip() for l in lines] def words(config_dir): """Which reserved-word list each dialect compiles against. reserved-words: makes .words the complete list (cobc's built-in default list is not used); an include of a .words file adds its entries to the list in force.""" lines = [] for d in ["default", "ibm-strict", "ibm"]: st = resolve(config_dir, d) rw = st.get("reserved-words", "default") if rw.lower() in ("default", "off"): base = "built-in default list" else: n = sum(1 for l in (Path(config_dir) / f"{rw.lower()}.words").read_text( encoding="utf-8").splitlines() if l.startswith("reserved:")) base = f"{rw.lower()}.words only ({n} entries)" added = [(w, sum(1 for l in (Path(config_dir) / w).read_text(encoding="utf-8") .splitlines() if l.startswith("reserved:"))) for w in st["@words"]] extra = "".join(f" + {n} entries from {w}" for w, n in added) lines.append(f"-std={d:<11} {base}{extra}") return lines # --- self-test ------------------------------------------------------------------------------ TRUNC_CASE = { # tests/testsuite.src/run_misc.at "Numeric MOVE with/without -fbinary-truncate" "yes": ["x IS ZERO"] * 3, "no": ["x IS 30000"] * 3} VALUES = [int("123456789012345678"[:n]) for n in range(1, 19)] def selftest(testsuite=None): checks = [] def ok(cond, name): checks.append((bool(cond), name)) # binary-truncate case: MOVE / COMPUTE / ADD 30000 into PIC 9(4) COMP for trunc, want in TRUNC_CASE.items(): got = [] for _ in range(3): v = store_binary(30000, 4, False, 2, trunc == "yes") got.append("x IS 30000" if v == 30000 else "x IS ZERO" if v == 0 else "x >= 10000") ok(got == want, f"suite: binary-truncate {trunc}: {got[0]}") # sizes from the configuration-file comment table (default.conf) ok([binary_size(d, False, "2-4-8") for d in (1, 4, 5, 9, 10, 18)] == [2, 2, 4, 4, 8, 8], "conf comment: 2-4-8 sizes") ok([binary_size(d, False, "1-2-4-8") for d in (2, 3, 4, 5, 9, 10)] == [1, 2, 2, 4, 4, 8], "conf comment: 1-2-4-8 sizes") ok([binary_size(d, True, "1--8") for d in (2, 4, 6, 7, 9, 11, 14, 16, 18)] == [1, 2, 3, 4, 4, 5, 6, 7, 8], "conf comment: 1--8 signed sizes") ok([binary_size(d, False, "1--8") for d in (2, 4, 7, 9, 12, 14, 16, 18)] == [1, 2, 3, 4, 5, 6, 7, 8], "conf comment: 1--8 unsigned sizes") # display width rule ok(display_binary(1, 1, False, 1, False) == "001", "full-print: PIC 9 in 1 byte -> 001") ok(display_binary(-1, 1, True, 1, False) == "-001", "full-print: PIC S9 in 1 byte -> -001") ok(display_binary(-12, 4, True, 2, True) == "-0012", "pretty: S9(4) -12 -> -0012") if testsuite: ts = Path(testsuite) db = (ts / "data_binary.at").read_text(encoding="utf-8") def block(after, n=36): i = db.index(after) j = db.index("[0],\n[", i) + len("[0],\n[") return db[j:].split("\n")[:n] for mode in ("2-4-8", "1-2-4-8", "1--8"): want = block(f"AT_SETUP([BINARY: {mode} big-endian])") got = [] for signed in (False, True): for n, v in enumerate(VALUES, start=1): v = -v if signed else v size = binary_size(n, signed, mode) raw = (v % (1 << (8 * size))).to_bytes(size, "big") got.append(raw.hex() + "20" * (8 - size)) ok(got == want, f"suite: {mode} big-endian dumps (36 items)") want = block("AT_SETUP([BINARY: full-print])") got = [display_binary(v, n, False, binary_size(n, False, "1--8"), False) for n, v in enumerate(VALUES, start=1)] got += [display_binary(-v, n, True, binary_size(n, True, "1--8"), False) for n, v in enumerate(VALUES, start=1)] ok(got == want, "suite: full-print, 1--8 without pretty-display (36 items)") want = block("AT_SETUP([BINARY: 2-4-8 big-endian])\n", 200) k = db.index("-o prog2], [0], [], [])", db.index("AT_SETUP([BINARY: 2-4-8 big-endian])")) want = db[db.index("[0],\n[", k) + 6:].split("\n")[:36] got = [display_binary(v, n, False, binary_size(n, False, "2-4-8"), True) for n, v in enumerate(VALUES, start=1)] got += [display_binary(-v, n, True, binary_size(n, True, "2-4-8"), True) for n, v in enumerate(VALUES, start=1)] ok(got == want, "suite: 2-4-8 with pretty-display (36 items)") rm = (ts / "run_misc.at").read_text(encoding="utf-8") i = rm.index("AT_SETUP([Numeric MOVE with/without -fbinary-truncate])") seg = rm[i:rm.index("AT_CLEANUP", i)] ok("[x IS ZERO\nx IS ZERO\nx IS ZERO\n])" in seg and "[x IS 30000\nx IS 30000\nx IS 30000\n])" in seg, "suite: truncate case text") for passed, name in checks: print(("ok " if passed else "FAIL ") + name) good = sum(p for p, _ in checks) print(f"{good} of {len(checks)} checks passed") return good == len(checks) def main(): ap = argparse.ArgumentParser(description=__doc__.split("\n")[0]) ap.add_argument("--config", help="GnuCOBOL config directory") ap.add_argument("--expect", metavar="DIALECT", help="print the probe's expected output") ap.add_argument("--words", action="store_true", help="reserved-word settings") ap.add_argument("--selftest", action="store_true") ap.add_argument("--testsuite", help="gnucobol-3.2/tests/testsuite.src for --selftest") args, flags = ap.parse_known_args() if args.selftest: sys.exit(0 if selftest(args.testsuite) else 1) if not args.config: ap.error("--config DIR is required") if args.expect: for line in probe(resolve(args.config, args.expect, flags)): print(line) elif args.words: print("\n".join(words(args.config))) else: print("\n".join(table(args.config))) if __name__ == "__main__": main()