max / quasi
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
- Claude-Session
- https://claude.ai/code/session_01MptwXZ8k65v19rFmdGAyki
1 file changed,
+163 insertions,
-0 deletions
| @@ -1,0 +1,163 @@ | |||
| 1 | + | #!/usr/bin/env python3 | |
| 2 | + | """Re-run the zero-site census behind section 3's deletion candidates. | |
| 3 | + | ||
| 4 | + | Section 8 of wiki `quasi-declare-form` names "a dead-list member with a live | |
| 5 | + | construction site" as one of four things that reopen the closure claim, and | |
| 6 | + | warns that a fifth one means the census needs re-running with a different | |
| 7 | + | predicate rather than patching. Four have already been found: `Row::meter`, | |
| 8 | + | the `menu` field on Row and Cells, `Field::multiple` and `Field::keeps_value`. | |
| 9 | + | ||
| 10 | + | WHY THE ORIGINAL CENSUS MISSED THEM. It looked for `Type::method(`. Three of the | |
| 11 | + | four are reached another way, and the vocabulary is built by chaining, so that | |
| 12 | + | predicate cannot see most of its own subject: | |
| 13 | + | ||
| 14 | + | Row::meter(..) an associated call <- the only shape it saw | |
| 15 | + | row.meter(..) a chained builder | |
| 16 | + | row.meter = Some(..) a field assignment | |
| 17 | + | ||
| 18 | + | `.current` is the documented case: 10 live sites, every one a field assignment. | |
| 19 | + | So this script counts all three reach-paths, per name, over the three shape | |
| 20 | + | directories, with comments, strings, raw strings and char literals blanked | |
| 21 | + | first, tests and `parity.rs` excluded, by reusing `population.py`. | |
| 22 | + | ||
| 23 | + | A hit is a CANDIDATE, not a verdict. `.value(` matches any type's `value` | |
| 24 | + | builder, so a name that comes back live still has to be read at its sites to | |
| 25 | + | say whether the receiver is the vocabulary type. The script prints the sites so | |
| 26 | + | that reading is cheap. What it is for is the other direction: a name that comes | |
| 27 | + | back at zero under all three paths is safely dead, and that is a claim the old | |
| 28 | + | predicate could not make. | |
| 29 | + | ||
| 30 | + | python3 scripts/dead-list-census.py # the census | |
| 31 | + | python3 scripts/dead-list-census.py --sites # every hit with its line | |
| 32 | + | """ | |
| 33 | + | ||
| 34 | + | from __future__ import annotations | |
| 35 | + | ||
| 36 | + | import argparse | |
| 37 | + | import importlib.util | |
| 38 | + | import re | |
| 39 | + | from pathlib import Path | |
| 40 | + | ||
| 41 | + | ROOT = Path.home() / "Code" | |
| 42 | + | spec = importlib.util.spec_from_file_location("population", Path(__file__).resolve().parent / "population.py") | |
| 43 | + | pop = importlib.util.module_from_spec(spec) | |
| 44 | + | spec.loader.exec_module(pop) | |
| 45 | + | ||
| 46 | + | # Section 3's deletion candidates, by owning type. `Node`'s two variants and the | |
| 47 | + | # two enums are matched as paths rather than as members. | |
| 48 | + | CANDIDATES = { | |
| 49 | + | "Row": ["open", "chosen", "value", "selected", "toggle", "worth", "role", | |
| 50 | + | "primary", "identified", "choosing"], | |
| 51 | + | "Cells": ["values"], | |
| 52 | + | "Field": ["curve", "as_asked"], | |
| 53 | + | "Screen": ["saying", "holds", "chooses", "anchors", "feeds", "is_live", "clocks"], | |
| 54 | + | "Node": ["clock", "and_more", "meter", "holds", "names", "about"], | |
| 55 | + | } | |
| 56 | + | VARIANTS = ["Node::Age", "Node::Prose", "Loading::", "Clock::"] | |
| 57 | + | ||
| 58 | + | # The six near-dead members the record KEEPS, each with one consumer. Counted | |
| 59 | + | # as a control: if the script cannot see these, its predicate is broken and its | |
| 60 | + | # zeroes mean nothing. | |
| 61 | + | CONTROL = { | |
| 62 | + | # The six near-dead members the record keeps, one consumer each. | |
| 63 | + | "Row::disclosing": 1, "Row::relaxed": 1, "Row::depth": 1, | |
| 64 | + | "Slot::with_ranked": 1, "Slot::revealed_by": 1, "Choice::plain": 1, | |
| 65 | + | # The four the old census condemned wrongly. These are the real control: | |
| 66 | + | # each is reached by a chained builder or a field assignment, which is the | |
| 67 | + | # exact defect, and a predicate that cannot see them is the old one. | |
| 68 | + | "Row::meter": 5, "Row::menu": 2, "Cells::menu": 1, | |
| 69 | + | "Field::many": 1, "Field::keeping_value": 1, | |
| 70 | + | } | |
| 71 | + | ||
| 72 | + | ||
| 73 | + | def reach_paths(ty: str, name: str) -> dict[str, re.Pattern]: | |
| 74 | + | """The three ways a member is reached, as named patterns.""" | |
| 75 | + | return { | |
| 76 | + | "assoc": re.compile(rf"\b{ty}::{name}\s*\("), | |
| 77 | + | "chained": re.compile(rf"\.{name}\s*\("), | |
| 78 | + | "assigned": re.compile(rf"\.{name}\s*=(?!=)"), | |
| 79 | + | } | |
| 80 | + | ||
| 81 | + | ||
| 82 | + | def scan(): | |
| 83 | + | files = [] | |
| 84 | + | for rel in pop.SHAPE_DIRS: | |
| 85 | + | d = ROOT / rel | |
| 86 | + | for f in sorted(d.rglob("*.rs")): | |
| 87 | + | parts = f.relative_to(ROOT).parts | |
| 88 | + | if f.name in ("tests.rs", "parity.rs") or "tests" in parts: | |
| 89 | + | continue | |
| 90 | + | raw = f.read_text(encoding="utf-8", errors="replace") | |
| 91 | + | files.append((str(f.relative_to(ROOT)), pop.strip_cfg_test(pop.blank_noncode(raw)), raw)) | |
| 92 | + | return files | |
| 93 | + | ||
| 94 | + | ||
| 95 | + | def hits_for(files, pattern) -> list[str]: | |
| 96 | + | out = [] | |
| 97 | + | for rel, blanked, raw in files: | |
| 98 | + | for m in pattern.finditer(blanked): | |
| 99 | + | line = blanked.count("\n", 0, m.start()) + 1 | |
| 100 | + | lines = raw.splitlines() | |
| 101 | + | text = lines[line - 1].strip() if 0 <= line - 1 < len(lines) else "" | |
| 102 | + | out.append(f"{rel}:{line} {text[:88]}") | |
| 103 | + | return out | |
| 104 | + | ||
| 105 | + | ||
| 106 | + | def main() -> int: | |
| 107 | + | ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) | |
| 108 | + | ap.add_argument("--sites", action="store_true", help="print every hit with its line") | |
| 109 | + | args = ap.parse_args() | |
| 110 | + | ||
| 111 | + | files = scan() | |
| 112 | + | ||
| 113 | + | print("CONTROL -- the six near-dead members the record keeps at one site each.") | |
| 114 | + | print("If any reads 0 here, this predicate is broken and its zeroes mean nothing.\n") | |
| 115 | + | broken = False | |
| 116 | + | for full, expected in CONTROL.items(): | |
| 117 | + | ty, name = full.split("::") | |
| 118 | + | total = sum(len(hits_for(files, p)) for p in reach_paths(ty, name).values()) | |
| 119 | + | flag = "" if total >= expected else " <-- PREDICATE BROKEN" | |
| 120 | + | if total < expected: | |
| 121 | + | broken = True | |
| 122 | + | print(f" {full:24} {total:3} (record says {expected}){flag}") | |
| 123 | + | ||
| 124 | + | print("\nDELETION CANDIDATES, all three reach-paths.\n") | |
| 125 | + | live = [] | |
| 126 | + | for ty, names in CANDIDATES.items(): | |
| 127 | + | for name in names: | |
| 128 | + | paths = reach_paths(ty, name) | |
| 129 | + | found = {k: hits_for(files, p) for k, p in paths.items()} | |
| 130 | + | total = sum(len(v) for v in found.values()) | |
| 131 | + | mark = " LIVE" if total else "" | |
| 132 | + | detail = " ".join(f"{k}={len(v)}" for k, v in found.items() if v) | |
| 133 | + | print(f" {ty + '::' + name:24} {total:3} {detail}{mark}") | |
| 134 | + | if total: | |
| 135 | + | live.append((f"{ty}::{name}", found)) | |
| 136 | + | ||
| 137 | + | print("\nVARIANTS AND ENUMS\n") | |
| 138 | + | for v in VARIANTS: | |
| 139 | + | p = re.compile(re.escape(v)) | |
| 140 | + | h = hits_for(files, p) | |
| 141 | + | print(f" {v:24} {len(h):3}{' LIVE' if h else ''}") | |
| 142 | + | if h: | |
| 143 | + | live.append((v, {"path": h})) | |
| 144 | + | ||
| 145 | + | print(f"\n{len(live)} of {sum(len(v) for v in CANDIDATES.values()) + len(VARIANTS)} " | |
| 146 | + | f"deletion candidates have at least one candidate site.") | |
| 147 | + | print("A candidate site is not a live construction: read it before acting. " | |
| 148 | + | "A name at zero under all three paths is safely dead.") | |
| 149 | + | if broken: | |
| 150 | + | print("\nTHE CONTROL FAILED. Do not quote any zero above.") | |
| 151 | + | ||
| 152 | + | if args.sites: | |
| 153 | + | print("\n--- SITES ---") | |
| 154 | + | for name, found in live: | |
| 155 | + | print(f"\n{name}") | |
| 156 | + | for kind, hs in found.items(): | |
| 157 | + | for h in hs: | |
| 158 | + | print(f" [{kind}] {h}") | |
| 159 | + | return 0 | |
| 160 | + | ||
| 161 | + | ||
| 162 | + | if __name__ == "__main__": | |
| 163 | + | raise SystemExit(main()) |