1#!/usr/bin/env python3 2# -------------------------------------------------------------- 3# miz_parser.py ―― Mizar 依存グラフ可視化 4# -------------------------------------------------------------- 5# theorem : yellow (Th…) 6# auto-theorem : lavender (Th_auto…) 7# definition / auto : teal (Def… / Def_auto…) 8# lemma : coral (Lm…) 9# external article : grey (environ 節に列挙された語) 10# -------------------------------------------------------------- 11from __future__ import annotations 12import re 13import sys 14from pathlib import Path 15from typing import Iterable 16 17import networkx as nx 18# from pyvis.network import Network 19 20 21# ─── 色定義 ────────────────────────────────────────── 22COL_TH = "#ffc107" 23COL_AUTO = "#b39ddb" 24COL_DEF = "#17a2b8" 25COL_LM = "#ff7f50" 26COL_EXT = "#9e9e9e" 27 28# ─── 基本正規表現 ────────────────────────────────── 29COMMENT_RE = re.compile(r"::.*?$", re.M) 30THEOREM_RE = re.compile(r"\btheorem\b(?:\s+([A-Za-z0-9_]+)\s*:)?", re.I) 31DEF_HDR_RE = re.compile(r"\bdefinition\b(?:\s+([A-Za-z0-9_]+)\s*:)?", re.I) 32 33# 行頭にある Def/Lm ラベル ( :Def1: や Def1: ) 34INLINE_HEAD_RE = re.compile(r"^[ \t]*:?\s*((?:Def|Lm)\d+)\s*:", re.I | re.M) 35 36# 行中に埋め込まれた :Def1: / :Lm3: など 37INLINE_INLINE_RE = re.compile(r":\s*((?:Def|Lm)\d+)\s*:", re.I) 38 39BY_RE = re.compile(r"\bby\s+([^;]+);", re.I | re.S) 40FROM_RE = re.compile(r"\bfrom\s+([^;]+);", re.I | re.S) 41 42# 〈記事:キーワード+番号〉も 1 トークン扱い 43LABEL_RE = re.compile( 44 r"[A-Z][A-Za-z0-9_]*:[A-Za-z0-9_]+\s*\d+" # TARSKI:def 1 など 45 r"|[A-Z][A-Za-z0-9_]*" # 単体記事名・内部ラベル 46) 47 48FOLD_RE = re.compile(r"^[A-Z](\d+)?$") # A, A1 …(吸収対象) 49INTERNAL_RE = re.compile(r"^(?:Th\d+|Th_auto\d+|Def\d+|Def_auto\d+|Lm\d+)$") 50EXT_ART_RE = re.compile(r"^[A-Z][A-Z0-9_]*$") 51 52 53# ─── environ から外部アーティクル抽出 ──────────────── 54def externals(txt: str) -> set[str]: 55 m = re.search(r"\benviron\b(.*?)(?:\bbegin\b)", txt, re.I | re.S) 56 return set() if not m else set(re.findall(r"[A-Z][A-Z0-9_]*", m[1])) 57 58 59# ─── 1 つの .miz を依存グラフに変換 ─────────────────── 60def parse_mizar(path: Path) -> nx.DiGraph: 61 src = COMMENT_RE.sub("", path.read_text(encoding="utf-8", errors="ignore")) 62 ext_art = externals(src) 63 64 #―― theorem:明示番号を把握して auto 番衝突を防ぐ ――――――――――― 65 explicit_nums, th_matches = set(), [] 66 for m in THEOREM_RE.finditer(src): 67 th_matches.append(m) 68 if m.group(1) and m.group(1).startswith("Th") and m.group(1)[2:].isdigit(): 69 explicit_nums.add(int(m.group(1)[2:])) 70 71 entries, inline_taken = [], set() 72 th_auto_i = def_auto_i = 1 73 74 #── theorem ブロック登録 ────────────────────────── 75 for m in th_matches: 76 if m.group(1): # 明示ラベル 77 lbl, kind = m.group(1), "th" 78 else: # auto 採番 79 while th_auto_i in explicit_nums: 80 th_auto_i += 1 81 lbl, kind = f"Th_auto{th_auto_i}", "th_auto" 82 th_auto_i += 1 83 entries.append(dict(start=m.start(), end=m.end(), label=lbl, kind=kind)) 84 85 #── definition ヘッダ処理 ───────────────────────── 86 for m in DEF_HDR_RE.finditer(src): 87 hdr_s, hdr_e = m.span() 88 # ブロック終端=次 the/def 手前 89 nxt_th = THEOREM_RE.search(src, hdr_e) 90 nxt_def = DEF_HDR_RE.search(src, hdr_e + 1) 91 blk_end = min(nxt_th.start() if nxt_th else len(src), 92 nxt_def.start() if nxt_def else len(src)) 93 94 # --- 行頭ラベル --- 95 for im in INLINE_HEAD_RE.finditer(src[hdr_e:blk_end]): 96 abs_pos = hdr_e + im.start() 97 lbl = im.group(1) 98 inline_taken.add(abs_pos) 99 kind = "lemma" if lbl.lower().startswith("lm") else "def" 100 entries.append(dict(start=abs_pos, end=abs_pos, label=lbl, kind=kind)) 101 102 # --- 行中ラベル --- 103 for jm in INLINE_INLINE_RE.finditer(src[hdr_e:blk_end]): 104 abs_pos = hdr_e + jm.start() 105 if abs_pos in inline_taken: # 行頭と重複しないように 106 continue 107 lbl = jm.group(1) 108 inline_taken.add(abs_pos) 109 kind = "lemma" if lbl.lower().startswith("lm") else "def" 110 entries.append(dict(start=abs_pos, end=abs_pos, label=lbl, kind=kind)) 111 112 # ヘッダに固有ラベルが無ければ auto 付与 113 if not any(hdr_s <= e["start"] < hdr_e for e in entries): 114 lbl = f"Def_auto{def_auto_i}" 115 def_auto_i += 1 116 entries.append(dict(start=hdr_s, end=hdr_e, label=lbl, kind="def")) 117 118 #── ファイル行頭の Def/Lm (未登録) ───────────────── 119 for m in INLINE_HEAD_RE.finditer(src): 120 if m.start() in inline_taken: 121 continue 122 lbl = m.group(1) 123 kind = "lemma" if lbl.lower().startswith("lm") else "def" 124 entries.append(dict(start=m.start(), end=m.end(), label=lbl, kind=kind)) 125 inline_taken.add(m.start()) 126 127 # 行中 :DefN: / :LmN: (未登録) 128 for m in INLINE_INLINE_RE.finditer(src): 129 if m.start() in inline_taken: 130 continue 131 lbl = m.group(1) 132 kind = "lemma" if lbl.lower().startswith("lm") else "def" 133 entries.append(dict(start=m.start(), end=m.end(), label=lbl, kind=kind)) 134 135 #―― 時系列順 & ラベル集合 ――――――――――――――――――――――― 136 entries.sort(key=lambda d: d["start"]) 137 internal_labels = {e["label"] for e in entries} 138 139 #―― グラフ (ノード) ――――――――――――――――――――――――――― 140 G = nx.DiGraph() 141 color_of = {"th": COL_TH, "th_auto": COL_AUTO, "def": COL_DEF, "lemma": COL_LM} 142 for e in entries: 143 G.add_node(e["label"], color=color_of[e["kind"]], label=e["label"]) 144 145 #―― 参照エッジ解析 ――――――――――――――――――――――――― 146 for idx, blk in enumerate(entries): 147 parent = blk["label"] 148 body = src[ 149 blk["end"]: 150 entries[idx + 1]["start"] if idx + 1 < len(entries) else len(src) 151 ] 152 153 refs: set[str] = set() 154 for seg in BY_RE.findall(body) + FROM_RE.findall(body): 155 for tok in LABEL_RE.findall(seg): 156 tok = tok.rstrip() 157 if FOLD_RE.fullmatch(tok): 158 continue 159 160 # ------------ 内部ラベル ------------ 161 if tok in internal_labels and INTERNAL_RE.fullmatch(tok): 162 refs.add(tok) 163 continue 164 165 # ------------ 外部参照 ------------ 166 art = tok.split(":")[0] # “XBOOLE_0:7” → “XBOOLE_0” 167 if art in ext_art: 168 refs.add(art) # 番号無視で記事名だけ追加 169 continue 170 171 refs.discard(parent) 172 for r in refs: 173 if r not in G: 174 col = COL_EXT if r in ext_art else COL_TH 175 G.add_node(r, color=col, label=r) 176 G.add_edge(r, parent) 177 178 return G 179 180 181# ─── 可視化 (HTML) ───────────────────────────────── 182def visualize(G: nx.DiGraph, outfile: Path) -> None: 183 net = Network(height="800px", width="100%", directed=True, notebook=False) 184 net.from_nx(G) 185 net.barnes_hut(gravity=-8000, central_gravity=0.5) 186 187 for n in net.nodes: 188 n.update({ 189 "value": max(nx.degree(G, n["id"]), 1), 190 "shape": "circle", 191 "font": {"vadjust": 0}, 192 }) 193 194 net.show(str(outfile)) 195 print(f"★ 生成完了: {outfile}") 196 197 198# ─── CLI ──────────────────────────────────────────── 199def main(files: Iterable[str]) -> None: 200 paths = [Path(p) for p in files] 201 if not paths: 202 print("usage: python miz_parser.py <file1.miz> [file2.miz ...]") 203 sys.exit(1) 204 205 for p in paths: 206 if not p.exists(): 207 print(f"※ file not found: {p}") 208 continue 209 graph = parse_mizar(p) 210 # visualize(graph, p.with_stem(p.stem + "_graph").with_suffix(".html")) 211 212 213if __name__ == "__main__": 214 main(sys.argv[1:]) 215