#!/usr/bin/env python3 # -*- coding: utf-8 -*- r"""extract_checks.py — smoke_all.gd 断言静态提取器(验证集清单生成与出口对账) 机器提取 game/smoke/smoke_all.gd 的全部静态 check( 调用点(每个调用点记录:所在函数、 行号、标签字符串字面量原文、cond 表达式原文),提供两种模式: 用法: python tools/extract_checks.py --extract [--src <源文件>] [--out <清单文件>] [--force] 生成验证集清单(已存在时拒写,除非 --force——冻结参照物防误覆) python tools/extract_checks.py --diff <清单文件> [--src <源文件>] 对当前源码再提取,与清单逐条比对——批 B 出口对账唯一工具 python tools/extract_checks.py --list-funcs [--src <源文件>] 只打印函数清单与各函数条数(编账本辅助,不写文件) 提取口径: - 静态 check( 调用点。与行级 grep(grep -c "check(")的差:grep 数含 func check 定义行 与一处注释行(「禁恒真 check(true)」),行级 grep=1067、真实调用点=1065(2026-10-22 基点实测)。 - 调用点可跨行:从 `check(` 起括号配平归集完整实参文本(() [] {} 三种括号都计深度); 字符串字面量内的括号/逗号/井号、行尾注释、转义序列均不参与结构判定(码位掩码单趟预扫)。 - cond 摘要=cond 原文跨行折叠为单行(连续空白并一空格);标签原文=第二个实参里的字符串 字面量逐字保真(含引号与转义序列原样,禁改写);标签实参若带格式化/拼接后缀 (`"..." % [...]`),后缀折叠后以「↩」接排在标签列。 - md 表格单元格内 `|` 转义为 `\|`、`\` 转义为 `\\`(源码串内无这两序列的歧义形态,读回可还原)。 --diff 对账规则:只比对「标签+cond+计数」三要素(合同 §五出口机检);行号随迁移漂移、 序号重排、白话释义/处置列由维护回填,均不参与比对。对不上的条目分三类报告: 消失(清单有现码无)、新增(现码有清单无)、成对字面变更(cond 同标签异=标签变更; 标签同 cond 异=cond 变更——一条「消失」与一条「新增」同因一字之改时按成对呈现)。 退出码:0=全对齐;1=有差异;2=用法错或文件不可读。 """ import argparse import bisect import re import subprocess import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent DEFAULT_SRC = ROOT / "game" / "smoke" / "smoke_all.gd" DEFAULT_OUT = ROOT / "game" / "tests" / "验证集清单.md" CALL_RE = re.compile(r"check\(") DEF_RE = re.compile(r"^\s*(static\s+)?func\s+check\s*\(") FUNC_RE = re.compile(r"^(static\s+)?func\s+([A-Za-z_][A-Za-z0-9_]*)") LITERAL_RE = re.compile(r'"((?:[^"\\]|\\.)*)"') OPENERS = "([{" CLOSERS = ")]}" CLOSER_OF = {")": "(", "]": "[", "}": "{"} def fold(text: str) -> str: """跨行折叠:连续空白(含换行/制表/回车)并为一空格。对账归一口径。""" return re.sub(r"\s+", " ", text).strip() def esc_cell(text: str) -> str: return text.replace("\\", "\\\\").replace("|", "\\|") def unesc_cell(text: str) -> str: """esc_cell 的逆。源码串内无 `\\|`/`\\\\` 歧义形态(提取口径节),还原无歧义。""" out = [] i = 0 while i < len(text): if text[i] == "\\" and i + 1 < len(text): out.append(text[i + 1]) i += 2 else: out.append(text[i]) i += 1 return "".join(out) def code_mask(joined: str) -> bytearray: """单趟预扫:1=码位(非字符串内部、非注释)。字符串与行尾注释位置置 0。""" mask = bytearray(len(joined)) in_str = False i = 0 n = len(joined) while i < n: ch = joined[i] if in_str: mask[i] = 0 if ch == "\\" and i + 1 < n: mask[i + 1] = 0 i += 2 continue if ch == '"': in_str = False i += 1 continue if ch == '"': in_str = True mask[i] = 0 i += 1 continue if ch == "#": nl = joined.find("\n", i) end = n if nl == -1 else nl for j in range(i, end): mask[j] = 0 i = end continue mask[i] = 1 i += 1 return mask def func_table(lines): """顶层函数表:[(name, start_line_1based)]。lambda 归宿主函数。""" funcs = [] for i, line in enumerate(lines, 1): m = FUNC_RE.match(line) if m: funcs.append((m.group(2), i)) return funcs def func_at(funcs, lineno): name = "" for fn, start in funcs: if start <= lineno: name = fn else: break return name def split_args(body): """按括号配平拆实参(顶层逗号分隔)。body=check( 与最外层 ) 之间的文本(换行已并空格)。""" args = [] depth = {"(": 0, "[": 0, "{": 0} cur = [] in_str = False i = 0 while i < len(body): ch = body[i] if in_str: if ch == "\\" and i + 1 < len(body): cur.append(body[i]) cur.append(body[i + 1]) # 转义序列原样入账 i += 2 continue if ch == '"': in_str = False cur.append(ch) elif ch == '"': in_str = True cur.append(ch) elif ch in depth: depth[ch] += 1 cur.append(ch) elif ch in CLOSERS: depth[CLOSER_OF[ch]] -= 1 cur.append(ch) elif ch == "," and depth["("] == 0 and depth["["] == 0 and depth["{"] == 0: args.append("".join(cur)) cur = [] else: cur.append(ch) i += 1 args.append("".join(cur)) return args def collect_call(joined, start): """从 check( 的 start 起括号配平归集调用。返回 (body, end_pos);不配平返回 None。""" i = start + len("check(") depth = 1 in_str = False parts = [] n = len(joined) while i < n: ch = joined[i] if in_str: if ch == "\\": parts.append(joined[i:i + 2]) i += 2 continue if ch == '"': in_str = False parts.append(ch) elif ch == '"': in_str = True parts.append(ch) elif ch in OPENERS: depth += 1 parts.append(ch) elif ch in CLOSERS: depth -= 1 if depth == 0: return "".join(parts), i parts.append(ch) elif ch == "#": nl = joined.find("\n", i) i = n if nl == -1 else nl # 行尾注释不进 body(并一空格) parts.append(" ") continue else: parts.append(ch if ch != "\n" else " ") i += 1 return None, i def extract(src_path: Path): """提取全部静态 check( 调用点。返回(条目列表, 函数表),条目保持源码顺序。""" src = src_path.read_text(encoding="utf-8") lines = src.split("\n") funcs = func_table(lines) offsets = [] total = 0 for line in lines: offsets.append(total) total += len(line) + 1 joined = "\n".join(lines) mask = code_mask(joined) def line_of(pos): i = bisect.bisect_right(offsets, pos) - 1 return i + 1 entries = [] for m in CALL_RE.finditer(joined): if not mask[m.start()]: continue # 字符串字面量内 / 注释里的 check( 字样 ln = line_of(m.start()) if DEF_RE.match(lines[ln - 1]): continue # func check 定义行 body, end_pos = collect_call(joined, m.start()) if body is None: raise SystemExit("[extract_checks] FAIL:行 %d 起的 check( 括号不配平到文件尾——先查解析缺口" % ln) args = split_args(body) if len(args) not in (2, 3): raise SystemExit( "[extract_checks] FAIL:行 %d 的 check( 拆出 %d 个实参(预期 2/3)——解析缺口,先查再跑" % (ln, len(args))) cond = args[0] label_expr = args[1] quiet = args[2] if len(args) == 3 else "" lm = LITERAL_RE.search(label_expr) if lm is None: raise SystemExit( "[extract_checks] FAIL:行 %d 的标签实参不含字符串字面量——解析缺口,先查再跑" % ln) entries.append({ "line": ln, "end_line": line_of(min(end_pos, len(joined) - 1)), "func": func_at(funcs, ln), "label_literal": lm.group(0), # 含引号,原文逐字 "label_suffix": label_expr[lm.end():], # 格式化/拼接后缀(通常为空或 % ...) "cond": cond, "quiet": quiet, }) return entries, funcs def label_cell(e): """标签列:字面量 ↩ 后缀(后缀空则只有字面量)。""" sfx = fold(e["label_suffix"]) return e["label_literal"] + (" ↩ " + sfx if sfx else "") def entry_key(e): """对账指纹:cond 折叠 + 标签字面量(逐字)+ 后缀折叠 + quiet。""" return (fold(e["cond"]), e["label_literal"], fold(e["label_suffix"]), fold(e["quiet"])) def pad3(n): return "S%02d" % n if n < 100 else "S%03d" % n def base_commit(): try: r = subprocess.run(["git", "rev-parse", "HEAD"], cwd=str(ROOT), capture_output=True, text=True, encoding="utf-8") if r.returncode == 0: return r.stdout.strip() except OSError: pass return "" def render_list(entries, funcs, src_rel: str) -> str: """生成验证集清单骨架。白话释义/处置两列留待回填(回填不动冻结列)。""" out = [] out.append("# 验证集清单(迁移参照物·冻结)") out.append("") out.append("> 生成日期:2026-10-22 · 基点 commit:`%s` · 源:`%s`" % (base_commit(), src_rel)) out.append("> 提取口径:静态 `check(` 调用点,括号配平归集(可跨行)。基点实测调用点 1065 处;") out.append("> 行级 `grep -c \"check(\"` = 1067 是另一口径——多出的 2 行为 `func check(` 定义行与") out.append("> 一处注释行(「禁恒真 check(true)」),非调用点。运行时计数大于静态计数=循环展开条目,") out.append("> 两口径各自浮动,禁互相换算(tests/README「断言计数双口径」)。") out.append("> 生成命令:`python tools/extract_checks.py --extract`(可复现;重新生成须 --force)。") out.append("> 冻结纪律:每条的「标签原文」「cond 摘要」「quiet」三列为机器提取逐字保真,禁任何改写;") out.append("> cond 摘要=原文跨行折叠单行(连续空白并一空格),单元格内 `|`→`\\|`、`\\`→`\\\\` 转义,读回还原。") out.append("> 「白话释义」「处置」两列由维护回填,不属冻结面。") out.append("> 出口对账:`python tools/extract_checks.py --diff game/tests/验证集清单.md`") out.append("> ——标签/cond/计数三对齐才算迁移完(合同 §五出口机检)。") out.append("> 维护义务:此后断言新增/删除/改判必须同步本清单(test-conventions §9.3 锚)。") out.append("") seq = 0 by_func = [] for e in entries: if by_func and by_func[-1][0] == e["func"]: by_func[-1][1].append(e) else: by_func.append((e["func"], [e])) for idx, (fn, es) in enumerate(by_func, 1): start, end = es[0]["line"], es[-1]["end_line"] out.append("## %s %s(行 %d–%d · %d 条 · 去向:待分类回填)" % (pad3(idx), fn, start, end, len(es))) out.append("") out.append("| 序号 | 行号 | 标签原文 | cond 摘要 | 白话释义 | 处置 | quiet |") out.append("|---|---|---|---|---|---|---|") for e in es: seq += 1 out.append("| %d | %d | %s | %s | (待填) | | %s |" % ( seq, e["line"], esc_cell(label_cell(e)), esc_cell(fold(e["cond"])), esc_cell(fold(e["quiet"])))) out.append("") out.append("---") out.append("全源共 %d 章、%d 条(静态调用点口径)。" % (len(by_func), seq)) return "\n".join(out) + "\n" CELL_SPLIT_RE = re.compile(r"(? int: cur, _funcs = extract(src_path) frozen = parse_list(list_path) cur_keys = {} for e in cur: cur_keys.setdefault(entry_key(e), []).append(e) frozen_keys = {} for e in frozen: frozen_keys.setdefault(entry_key(e), []).append(e) missing = [k for k in frozen_keys if k not in cur_keys] added = [k for k in cur_keys if k not in frozen_keys] # 成对归类:cond 同→标签变更;标签同→cond 变更 label_changes, cond_changes = [], [] used_added = set() rest_missing = [] for k in missing: cond, lit, sfx, _q = k pair = next((ka for ka in added if ka not in used_added and ka[0] == cond), None) if pair is not None: used_added.add(pair) label_changes.append((k, pair)) continue pair = next((ka for ka in added if ka not in used_added and ka[1] == lit and ka[2] == sfx), None) if pair is not None: used_added.add(pair) cond_changes.append((k, pair)) continue rest_missing.append(k) rest_added = [k for k in added if k not in used_added] ok = True print("[extract_checks] 对账:%s vs 现提取 %s" % (list_path.name, src_path.name)) print(" 清单条数=%d 现提取条数=%d %s" % ( len(frozen), len(cur), "(计数对齐)" if len(frozen) == len(cur) else "(计数不齐)")) def show(k, tag): cond, lit, sfx, q = k s = " %s cond=%s label=%s" % (tag, _short(cond), _short(lit)) if sfx: s += " suffix=%s" % _short(fold(sfx)) if q: s += " quiet=%s" % fold(q) print(s) if label_changes: ok = False print(" 标签字面变更 %d 对(cond 同、标签异):" % len(label_changes)) for old, new in label_changes: show(old, "-") show(new, "+") if cond_changes: ok = False print(" cond 字面变更 %d 对(标签同、cond 异):" % len(cond_changes)) for old, new in cond_changes: show(old, "-") show(new, "+") if rest_missing: ok = False print(" 消失 %d 条(清单有、现码无):" % len(rest_missing)) for k in rest_missing: print(" - [%s]" % frozen_keys[k][0].get("section", "?"), end="") show(k, "") if rest_added: ok = False print(" 新增 %d 条(现码有、清单无):" % len(rest_added)) for k in rest_added: es = cur_keys[k][0] print(" + [func %s @%d]" % (es["func"], es["line"]), end="") show(k, "") if ok: print("[extract_checks] PASS——标签/cond/计数三对齐,零差异") return 0 print("[extract_checks] FAIL——见上差异清单") return 1 def list_funcs(entries, funcs): by_func = [] for e in entries: if by_func and by_func[-1][0] == e["func"]: by_func[-1][1] = by_func[-1][1] + 1 else: by_func.append([e["func"], 1]) checked = {f for f, _ in by_func} for fn, n in by_func: print("%s\t%d" % (fn, n)) print("--- 无断言函数(helper,随套迁移)---") for fn, ln in funcs: if fn not in checked: print("%s\t@%d" % (fn, ln)) def main() -> int: if hasattr(sys.stdout, "reconfigure"): sys.stdout.reconfigure(encoding="utf-8", errors="replace") ap = argparse.ArgumentParser(description="smoke_all.gd 断言静态提取器(清单生成/对账)") ap.add_argument("--extract", action="store_true", help="生成验证集清单") ap.add_argument("--diff", metavar="清单文件", help="与清单逐条对账(批 B 出口机检)") ap.add_argument("--list-funcs", action="store_true", help="打印函数清单与条数(编账本辅助)") ap.add_argument("--src", default=str(DEFAULT_SRC), help="断言源文件") ap.add_argument("--out", default=str(DEFAULT_OUT), help="--extract 输出路径") ap.add_argument("--force", action="store_true", help="输出文件已存在时允许覆盖") args = ap.parse_args() if sum(bool(x) for x in (args.extract, args.diff, args.list_funcs)) != 1: print("[extract_checks] 用法:--extract / --diff <清单文件> / --list-funcs 三选一") return 2 src_path = Path(args.src) if not src_path.is_file(): print("[extract_checks] FAIL:源文件不可读:%s" % src_path) return 2 if args.extract: out = Path(args.out) if out.exists() and not args.force: print("[extract_checks] FAIL:清单已存在(%s)——冻结参照物防误覆,重生成须加 --force" % out) return 2 entries, _funcs = extract(src_path) out.write_text(render_list(entries, _funcs, src_path.name), encoding="utf-8", newline="\n") print("[extract_checks] 已生成 %s(%d 个调用点)" % (out, len(entries))) return 0 if args.diff: lp = Path(args.diff) if not lp.is_file(): print("[extract_checks] FAIL:清单不可读:%s" % lp) return 2 return diff_report(lp, src_path) entries, funcs = extract(src_path) list_funcs(entries, funcs) return 0 if __name__ == "__main__": sys.exit(main())