mirror of
https://github.com/Druthulu/BFM-decomp
synced 2026-09-26 13:33:34 -04:00
444 lines
21 KiB
Python
444 lines
21 KiB
Python
#!/usr/bin/env python3
|
|
"""gccmap_cites.py — tag every `file.c:NNN` citation in docs/gcc-2.7.2-map/*.md with the source tree it refers to (P33 E3).
|
|
|
|
tools/gccmap_cites.py # derive a tag for every cite from the reference trees and WRITE it in place (idempotent)
|
|
tools/gccmap_cites.py --dry-run # the same derivation and census, nothing written
|
|
tools/gccmap_cites.py --check # TEXTUAL: exit 0 iff every cite carries a tag and every override is still used
|
|
# (no reference tree needed — safe for CI and `make tools-health`)
|
|
tools/gccmap_cites.py --verify # re-derive every existing tag from the trees; exit 1 on any disagreement
|
|
tools/gccmap_cites.py --controls # the known-true cases (R39) — exit 0 iff every one derives as recorded
|
|
|
|
The problem this solves. The codegen map was written in Phase 23 against `tools/reference/gcc-papermario/`, which is
|
|
gcc 2.8.1 — NOT the pinned 2.7.2 — so its line numbers are correct for the wrong compiler (drift up to +611 lines, enough
|
|
to land inside a different function); the Phase-29 audit re-derived every claim against the vanilla 2.7.2 subset at
|
|
`tools/reference/gcc-2.7.2/` and found the MECHANISMS sound but left the numbers mixed. A reader following a citation
|
|
needs to know which tree it belongs to. Tags are written right after the cite (inside the same code span when the cite
|
|
is in one):
|
|
|
|
sched.c:2469 [2.7.2] the cited construct sits at that line in the vanilla 2.7.2 subset (checked, not assumed)
|
|
global.c:616 [2.8.1 pm] the cited construct sits at that line in gcc-papermario (2.8.1); the 2.7.2 line differs
|
|
engine_core.h:24926 [repo] a project file, not gcc
|
|
reload1.c:779 [?] undecided by the evidence AND no override — listed; `--check` fails on it
|
|
|
|
How a tag is derived (R33 — from the trees, never typed):
|
|
1. EVIDENCE: the identifiers in backticked/bold spans within ±160 characters of the cite (only C-looking ones — with an
|
|
underscore, as every real gcc macro/function cited has; prose words and bare RTL codes are dropped), nearest first. For each, the nearest occurrence to the cited line(s)
|
|
in each tree's file, within ±60 lines (a function name sits at the header, the cite often inside the body). The tree
|
|
whose occurrence is nearer wins; an equal distance is a TIE — the line is equally valid in both — resolved by an
|
|
override if one exists, else the author's cue, else 2.7.2. A line
|
|
past the end of a tree's file cannot be that tree. Files absent from the 2.7.2 subset (18 .c files + config/mips)
|
|
can only be `[2.8.1 pm]`.
|
|
2. CUES: the author's own words — `papermario` / `2.8.1` just before the cite, or `→2.7.2 :NNN` just after it, mean
|
|
2.8.1; `2.7.2` just before means 2.7.2. A cue that CONTRADICTS the evidence leaves the cite undecided (R34 — a
|
|
disagreement fails loudly, it is not averaged); a cue alone decides when there is no evidence.
|
|
3. OVERRIDES: docs/gcc-2.7.2-map/cite_overrides.tsv (`file:line<TAB>tag<TAB>reason`) — a human judgment, kept small;
|
|
`--check` refuses an override whose cite no longer exists (a stale override is a lie, R51).
|
|
A tag that already exists is never silently changed: a derivation that disagrees is reported and exits 1
|
|
(`--retag` accepts the derivation). Tagging, --verify and --controls need both trees and refuse loudly without them
|
|
(R43); --check needs neither.
|
|
"""
|
|
import argparse
|
|
import pathlib
|
|
import re
|
|
import sys
|
|
|
|
REPO = pathlib.Path(__file__).resolve().parent.parent
|
|
MAP_DIR = REPO / "docs" / "gcc-2.7.2-map"
|
|
OVERRIDES = MAP_DIR / "cite_overrides.tsv"
|
|
TREES = {"2.7.2": REPO / "tools" / "reference" / "gcc-2.7.2", "2.8.1 pm": REPO / "tools" / "reference" / "gcc-papermario"}
|
|
TAGS = ("2.7.2", "2.8.1 pm", "repo", "?")
|
|
# a cite, optionally already tagged: file.c:NNN[-MMM] [tag]
|
|
CITE = re.compile(r"\b([a-z0-9_-]+\.[ch]):(\d+)(?:-(\d+))?(?: \[(2\.7\.2|2\.8\.1 pm|repo|\?)\])?")
|
|
SPAN = re.compile(r"`([^`]+)`|\*\*([^*]+)\*\*")
|
|
IDENT = re.compile(r"[A-Za-z_][A-Za-z0-9_]{2,}")
|
|
DROP = {"A23", "PSX", "REG", "MEM", "SET", "USE", "RTL", "RTX", "CSE", "GTE", "LUID", "TRUE", "FALSE", "STEERABLE",
|
|
"INTRINSIC", "DETERMINED", "NOTHING", "SAME", "FILE", "BOTH", "ACROSS", "CALLS", "ORDER", "LEVER", "TEMP", "SLOT",
|
|
"VALUE", "DENOMINATOR", "FORWARD", "BIRTH", "WITHOUT", "FIRST", "CALL", "ALU", "EXPR", "CODE"}
|
|
NEAR, WIDE = 3, 60
|
|
SKIP = {"README.md"}
|
|
# the known-true cases the map's own text records (audited S23 / t7g); --controls derives them and compares
|
|
CONTROLS = [("sched.md", "sched.c:2469", "2.7.2"), ("regalloc.md", "global.c:616", "2.8.1 pm"),
|
|
("regalloc.md", "local-alloc.c:1745", "2.8.1 pm"), ("cse_expr.md", "cse.c:8621-8644", "2.8.1 pm"),
|
|
("loop.md", "loop.c:4819", "2.7.2"), ("t7g-giant-harvest.md", "engine_core.h:24926", "repo")]
|
|
|
|
|
|
def tree_file(tree: pathlib.Path, name: str):
|
|
for cand in (tree / name, tree / "config" / "mips" / name):
|
|
if cand.is_file():
|
|
return cand
|
|
return None
|
|
|
|
|
|
_cache = {}
|
|
|
|
|
|
def lines_of(path: pathlib.Path):
|
|
if path not in _cache:
|
|
_cache[path] = path.read_text(encoding="utf-8", errors="replace").split("\n")
|
|
return _cache[path]
|
|
|
|
|
|
_spans = {}
|
|
|
|
|
|
def spans_of(text: str):
|
|
"""Every backticked / bold span of the WHOLE document, paired once (pairing inside a window starting mid-span
|
|
inverts the pairs and silently drops the identifiers right next to a cite — an S88 defect)."""
|
|
key = id(text)
|
|
if key not in _spans:
|
|
# fenced code blocks would pair their backticks with the prose's and invert everything after them:
|
|
# blank them (same length, so every offset stays valid) before pairing
|
|
blanked = re.sub(r"```.*?```", lambda m: " " * len(m.group(0)), text, flags=re.S)
|
|
_spans[key] = [(m.start(), m.end(), m.group(1) or m.group(2)) for m in SPAN.finditer(blanked)]
|
|
return _spans[key]
|
|
|
|
|
|
def identifiers_near(text: str, start: int, end: int, own_file: str):
|
|
"""C-looking identifiers from backticked/bold spans within ±160 chars, nearest to the cite first."""
|
|
lo, hi = max(0, start - 160), min(len(text), end + 160)
|
|
found = []
|
|
for s, e, span_text in spans_of(text):
|
|
if e < lo or s > hi:
|
|
continue
|
|
dist = min(abs(s - start), abs(e - end))
|
|
for ident in IDENT.findall(span_text):
|
|
if ident in DROP or ident == own_file.split(".")[0]:
|
|
continue
|
|
if "_" not in ident: # every real gcc macro/function cited has one; bare ALL-CAPS words (NOT, AND, DEST) are prose
|
|
continue
|
|
found.append((dist, ident))
|
|
found.sort()
|
|
out, seen = [], set()
|
|
for _, ident in found:
|
|
if ident not in seen:
|
|
seen.add(ident); out.append(ident)
|
|
return out[:8]
|
|
|
|
|
|
def nearest(tree: pathlib.Path, name: str, a: int, b: int, ident: str):
|
|
"""Distance (in lines) from the cited range to the nearest occurrence of ident in this tree's file, within WIDE;
|
|
None = file absent; -1 = the cited line is past the end of the file; WIDE+1 = not found within the window."""
|
|
f = tree_file(tree, name)
|
|
if f is None:
|
|
return None
|
|
L = lines_of(f)
|
|
if a > len(L):
|
|
return -1
|
|
pat = re.compile(r"\b" + re.escape(ident) + (r"" if "_" in ident else r"\b"))
|
|
best = WIDE + 1
|
|
for i in range(max(0, a - 1 - WIDE), min(len(L), b + WIDE)):
|
|
if pat.search(L[i]):
|
|
d = 0 if a - 1 <= i <= b - 1 else (a - 1 - i if i < a - 1 else i - (b - 1))
|
|
best = min(best, d)
|
|
return best
|
|
|
|
|
|
def cue(text: str, start: int, end: int):
|
|
"""The author's own words: a prefix cue (tie-breaker) or the arrow form `→2.7.2 :NNN` (decisive — the author
|
|
states both numbers, so the cited one is the 2.8.1 line). Returns (tag, decisive)."""
|
|
strip = re.compile(r" \[(?:2\.7\.2|2\.8\.1 pm|repo|\?)\]") # never read the tags this tool itself wrote (R57)
|
|
before = strip.sub("", text[max(0, start - 40):start])[-24:]
|
|
after = strip.sub("", text[end:end + 30])[:14]
|
|
if re.search(r"`?→\**2\.7\.2 ?:", after):
|
|
return "2.8.1 pm", True
|
|
if "papermario" in before or "2.8.1" in before:
|
|
return "2.8.1 pm", False
|
|
if re.search(r"2\.7\.2 ?`?$", before) or before.endswith("2.7.2 "):
|
|
return "2.7.2", False
|
|
return None, False
|
|
|
|
|
|
DEF_HEADER = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*) \(") # gcc-2.x style: the function name at column 0, then " ("
|
|
_extents = {}
|
|
|
|
|
|
def enclosing(tree: pathlib.Path, name: str, line: int):
|
|
"""The function whose definition header is the last one at or before `line` in this tree's file (None = unknown)."""
|
|
f = tree_file(tree, name)
|
|
if f is None or not name.endswith(".c"):
|
|
return None
|
|
if f not in _extents:
|
|
_extents[f] = [(i + 1, m.group(1)) for i, ln in enumerate(lines_of(f)) if (m := DEF_HEADER.match(ln))]
|
|
if line > len(lines_of(f)):
|
|
return "<past EOF>"
|
|
last = None
|
|
for start, fn in _extents[f]:
|
|
if start > line:
|
|
break
|
|
last = fn
|
|
return last
|
|
|
|
|
|
def snippets_near(text: str, start: int, end: int):
|
|
"""Backticked spans within ±160 chars that look like quoted source (≥ 6 chars, not a lone identifier/cite)."""
|
|
lo, hi = max(0, start - 160), min(len(text), end + 160)
|
|
out = []
|
|
for s0, e0, span_text in spans_of(text):
|
|
if e0 < lo or s0 > hi or text[s0] != "`":
|
|
continue
|
|
s = " ".join(span_text.split())
|
|
if len(s) >= 6 and not CITE.search(s) and not re.fullmatch(r"[A-Za-z0-9_.$-]+", s):
|
|
out.append((min(abs(s0 - start), abs(e0 - end)), s))
|
|
return [s for _, s in sorted(out)][:6]
|
|
|
|
|
|
def snippet_distance(tree: pathlib.Path, name: str, a: int, b: int, snippet: str):
|
|
"""Distance from the cited range to the nearest line containing the whitespace-normalized snippet (see nearest())."""
|
|
f = tree_file(tree, name)
|
|
if f is None:
|
|
return None
|
|
L = lines_of(f)
|
|
if a > len(L):
|
|
return -1
|
|
best = WIDE + 1
|
|
for i in range(max(0, a - 1 - WIDE), min(len(L), b + WIDE)):
|
|
if snippet in " ".join(L[i].split()):
|
|
d = 0 if a - 1 <= i <= b - 1 else (a - 1 - i if i < a - 1 else i - (b - 1))
|
|
best = min(best, d)
|
|
return best
|
|
|
|
|
|
def evidence(name, a, b, idents, snippets=()):
|
|
"""The tree the identifiers point at, or None; plus a note. Quoted source snippets first (strongest), then function
|
|
extents (a cite inside a long body is far from the header), then the nearest-occurrence distance (macros, globals,
|
|
headers)."""
|
|
for s in snippets:
|
|
d27, d28 = snippet_distance(TREES["2.7.2"], name, a, b, s), snippet_distance(TREES["2.8.1 pm"], name, a, b, s)
|
|
ok27 = d27 is not None and 0 <= d27 <= WIDE
|
|
ok28 = d28 is not None and 0 <= d28 <= WIDE
|
|
if ok27 and (not ok28 or d27 <= d28):
|
|
return "2.7.2", f"quoted `{s}` at distance {d27} (pm {d28 if d28 is not None else 'n/a'})"
|
|
if ok28:
|
|
return "2.8.1 pm", f"quoted `{s}` at distance {d28} (2.7.2 {'n/a' if d27 is None else ('past EOF' if d27 == -1 else d27)})"
|
|
for ident in idents:
|
|
if "_" in ident and name.endswith(".c"):
|
|
e27, e28 = enclosing(TREES["2.7.2"], name, a), enclosing(TREES["2.8.1 pm"], name, a)
|
|
in27 = e27 is not None and (e27 == ident or e27.startswith(ident))
|
|
in28 = e28 is not None and (e28 == ident or e28.startswith(ident))
|
|
if in27 and in28:
|
|
return "tie", f"line {a} is inside `{ident}` in both trees"
|
|
if in27:
|
|
return "2.7.2", f"line {a} is inside `{e27}` (pm: `{e28}`)"
|
|
if in28:
|
|
return "2.8.1 pm", f"line {a} is inside `{e28}` (2.7.2: `{e27}`)"
|
|
for ident in idents:
|
|
d27, d28 = nearest(TREES["2.7.2"], name, a, b, ident), nearest(TREES["2.8.1 pm"], name, a, b, ident)
|
|
ok27 = d27 is not None and 0 <= d27 <= WIDE
|
|
ok28 = d28 is not None and 0 <= d28 <= WIDE
|
|
if ok27 and ok28 and d27 == d28:
|
|
return "tie", f"`{ident}` at the same distance {d27} in both trees"
|
|
if ok27 and (not ok28 or d27 < d28):
|
|
return "2.7.2", f"`{ident}` at distance {d27} (pm {d28 if d28 is not None else 'n/a'})"
|
|
if ok28:
|
|
note = f"`{ident}` at distance {d28} (2.7.2 {'n/a' if d27 is None else ('past EOF' if d27 == -1 else d27)})"
|
|
return "2.8.1 pm", note
|
|
return None, ("no C-looking identifier near the cite" if not idents else f"none of {idents[:4]} within ±{WIDE} lines in either tree")
|
|
|
|
|
|
def derive(text: str, m: re.Match, overrides):
|
|
name, a = m.group(1), int(m.group(2))
|
|
b = int(m.group(3)) if m.group(3) and int(m.group(3)) >= a else a
|
|
key = cite_key(m)
|
|
if (REPO / "src" / "shared" / name).exists():
|
|
return "repo", "project file"
|
|
c, decisive = cue(text, m.start(), m.end())
|
|
if decisive:
|
|
return c, "the text gives both numbers (→2.7.2 :NNN), so this is the 2.8.1 line"
|
|
f27 = tree_file(TREES["2.7.2"], name)
|
|
if f27 is not None and a > len(lines_of(f27)):
|
|
f28 = tree_file(TREES["2.8.1 pm"], name)
|
|
if f28 is not None and a <= len(lines_of(f28)):
|
|
return "2.8.1 pm", f"line {a} is past the end of the 2.7.2 file ({len(lines_of(f27))} lines)"
|
|
idents = identifiers_near(text, m.start(), m.end(), name)
|
|
ev, note = evidence(name, a, b, idents, snippets_near(text, m.start(), m.end()))
|
|
if ev == "tie": # equally valid in both: the human override may resolve it, else 2.7.2
|
|
if key in overrides:
|
|
return overrides[key][0], f"override resolves a tie ({note}): {overrides[key][1]}"
|
|
return c or "2.7.2", note + (f"; the text's cue [{c}] decides" if c else "; a tie is a valid 2.7.2 line")
|
|
if ev and c and ev != c:
|
|
return "?", f"evidence says [{ev}] ({note}) but the text's cue says [{c}] — resolve by hand (override)"
|
|
if ev:
|
|
return ev, note + (f"; cue agrees" if c else "")
|
|
if c:
|
|
return c, f"cue only ({note})"
|
|
if key in overrides:
|
|
tag, reason = overrides[key]
|
|
return tag, f"override: {reason}"
|
|
if tree_file(TREES["2.7.2"], name) is None:
|
|
note += " (file not in the 2.7.2 subset)"
|
|
else:
|
|
note += f" — line {a} is inside 2.7.2 `{enclosing(TREES['2.7.2'], name, a)}` / pm `{enclosing(TREES['2.8.1 pm'], name, a)}`"
|
|
return "?", note
|
|
|
|
|
|
def load_overrides():
|
|
out = {}
|
|
if OVERRIDES.exists():
|
|
for ln in OVERRIDES.read_text(encoding="utf-8").splitlines():
|
|
ln = ln.rstrip()
|
|
if not ln or ln.startswith("#"):
|
|
continue
|
|
key, tag, reason = (ln.split("\t") + ["", ""])[:3]
|
|
if tag not in TAGS or tag == "?":
|
|
sys.exit(f"gccmap_cites: bad override tag {tag!r} for {key} in {OVERRIDES}")
|
|
out[key.strip()] = (tag, reason.strip())
|
|
return out
|
|
|
|
|
|
def map_files():
|
|
return [p for p in sorted(MAP_DIR.glob("*.md")) if p.name not in SKIP]
|
|
|
|
|
|
def cite_key(m):
|
|
return f"{m.group(1)}:{m.group(2)}" + (f"-{m.group(3)}" if m.group(3) else "")
|
|
|
|
|
|
def require_trees():
|
|
for tag, path in TREES.items():
|
|
if not path.is_dir():
|
|
print(f"gccmap_cites: reference tree for [{tag}] missing at {path} — see docs/SETUP.md §5.6 (re-clone / fetch); "
|
|
f"--check needs no tree")
|
|
return False
|
|
return True
|
|
|
|
|
|
def run_check():
|
|
overrides = load_overrides()
|
|
used, untagged, n = set(), [], 0
|
|
for p in map_files():
|
|
text = p.read_text(encoding="utf-8")
|
|
for m in CITE.finditer(text):
|
|
n += 1
|
|
key = cite_key(m)
|
|
if key in overrides:
|
|
used.add(key)
|
|
if m.group(4) is None or m.group(4) == "?":
|
|
untagged.append((p.name, key, m.group(4)))
|
|
stale = sorted(set(overrides) - used)
|
|
print(f"gccmap_cites --check: {n} cites in {len(map_files())} files, {len(untagged)} untagged/undecided, "
|
|
f"{len(overrides)} overrides ({len(stale)} stale)")
|
|
for f, k, tag in untagged:
|
|
print(f" UNTAGGED {f}: {k}" + (" [?]" if tag == "?" else ""))
|
|
for k in stale:
|
|
print(f" STALE override {k}: no such cite any more — delete it from {OVERRIDES.name}")
|
|
if untagged or stale:
|
|
return 1
|
|
print("gccmap_cites --check: OK")
|
|
return 0
|
|
|
|
|
|
def run_controls():
|
|
if not require_trees():
|
|
return 2
|
|
overrides = {}
|
|
bad = 0
|
|
for fname, key, want in CONTROLS:
|
|
text = (MAP_DIR / fname).read_text(encoding="utf-8")
|
|
got = None
|
|
for m in CITE.finditer(text):
|
|
if cite_key(m) == key:
|
|
got, why = derive(text, m, overrides)
|
|
break
|
|
status = "ok " if got == want else "FAIL"
|
|
bad += got != want
|
|
print(f" {status} {fname} {key}: derived [{got}] (want [{want}]) — {why if got is not None else 'cite not found'}")
|
|
print(f"gccmap_cites --controls: {len(CONTROLS)} known-true cases, {bad} failed")
|
|
return 1 if bad else 0
|
|
|
|
|
|
def run_explain():
|
|
"""For every undecided cite: the map's own sentence and the cited lines in both trees — the input to an override."""
|
|
if not require_trees():
|
|
return 2
|
|
overrides = load_overrides()
|
|
n = 0
|
|
for p in map_files():
|
|
text = p.read_text(encoding="utf-8")
|
|
for m in CITE.finditer(text):
|
|
tag, why = derive(text, m, overrides)
|
|
if tag != "?":
|
|
continue
|
|
n += 1
|
|
name, a = m.group(1), int(m.group(2))
|
|
ctx = " ".join(text[max(0, m.start() - 110):m.end() + 70].split())
|
|
print(f"\n[{n}] {p.name} {cite_key(m)} — {why}\n text: …{ctx}…")
|
|
for tag_, tree in TREES.items():
|
|
f = tree_file(tree, name)
|
|
if f is None:
|
|
print(f" {tag_:9s}: (file absent)"); continue
|
|
L = lines_of(f)
|
|
show = [f"{i + 1}: {L[i].rstrip()[:110]}" for i in range(a - 1, min(len(L), a + 1)) if i >= 0]
|
|
print(f" {tag_:9s}: " + (" | ".join(show) if show else f"(past EOF, {len(L)} lines)"))
|
|
print(f"\ngccmap_cites --explain: {n} undecided cites shown")
|
|
return 0
|
|
|
|
|
|
def run_derive(write: bool, verify: bool, retag: bool):
|
|
if not require_trees():
|
|
return 2
|
|
overrides = load_overrides()
|
|
totals = {t: 0 for t in TAGS}; disagreements = []; undecided = []; changed_files = 0; n = 0; over_used = 0
|
|
over_keys_used, cite_keys = set(), set()
|
|
for p in map_files():
|
|
text = p.read_text(encoding="utf-8")
|
|
out, pos, file_changed = [], 0, False
|
|
for m in CITE.finditer(text):
|
|
n += 1
|
|
tag, why = derive(text, m, overrides)
|
|
cite_keys.add(cite_key(m))
|
|
if why.startswith("override"): # "override: …" and "override resolves a tie …"
|
|
over_used += 1; over_keys_used.add(cite_key(m))
|
|
existing = m.group(4)
|
|
totals[tag] += 1
|
|
if tag == "?":
|
|
undecided.append((p.name, cite_key(m), why))
|
|
if existing and existing != "?" and existing != tag:
|
|
disagreements.append((p.name, cite_key(m), existing, tag, why))
|
|
if not retag:
|
|
tag = existing # never silently change a tag
|
|
new = f"{m.group(1)}:{m.group(2)}" + (f"-{m.group(3)}" if m.group(3) else "") + f" [{tag}]"
|
|
if new != m.group(0):
|
|
file_changed = True
|
|
out.append(text[pos:m.start()]); out.append(new); pos = m.end()
|
|
out.append(text[pos:])
|
|
if file_changed:
|
|
changed_files += 1
|
|
if write:
|
|
p.write_text("".join(out), encoding="utf-8")
|
|
mode = "verify" if verify else ("write" if write else "dry-run")
|
|
print(f"gccmap_cites ({mode}): {n} cites in {len(map_files())} files — [2.7.2] {totals['2.7.2']} · [2.8.1 pm] "
|
|
f"{totals['2.8.1 pm']} · [repo] {totals['repo']} · undecided {totals['?']} · override hits {over_used} "
|
|
f"({len(overrides)} rows) · files {'written' if write else 'that would change'}: {changed_files}")
|
|
for f, k, why in undecided:
|
|
print(f" UNDECIDED {f}: {k} — {why}")
|
|
for k in sorted(set(overrides) & cite_keys - over_keys_used):
|
|
print(f" REDUNDANT override {k}: the evidence decides it — delete the row from {OVERRIDES.name}")
|
|
for f, k, old, new, why in disagreements:
|
|
print(f" DISAGREE {f}: {k} tagged [{old}], derived [{new}] ({why})" + ("" if retag else " — kept; --retag to accept"))
|
|
if verify:
|
|
return 1 if (disagreements or undecided) else 0
|
|
return 1 if undecided else 0
|
|
|
|
|
|
def main(argv):
|
|
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
g = ap.add_mutually_exclusive_group()
|
|
g.add_argument("--dry-run", action="store_true")
|
|
g.add_argument("--check", action="store_true")
|
|
g.add_argument("--verify", action="store_true")
|
|
g.add_argument("--controls", action="store_true")
|
|
g.add_argument("--explain", action="store_true", help="show the undecided cites with the map's sentence and both trees' lines")
|
|
ap.add_argument("--retag", action="store_true", help="accept a derivation that disagrees with an existing tag")
|
|
a = ap.parse_args(argv)
|
|
if a.check:
|
|
return run_check()
|
|
if a.controls:
|
|
return run_controls()
|
|
if a.explain:
|
|
return run_explain()
|
|
return run_derive(write=not (a.dry_run or a.verify), verify=a.verify, retag=a.retag)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv[1:]))
|