Repository navigation
Expand file tree
/
Copy pathform_map.py
More file actions
146 lines (129 loc) · 6.44 KB
/
Copy pathform_map.py
File metadata and controls
146 lines (129 loc) · 6.44 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
"""Form-selection map: which output FORMS can this function produce, and what
CONDITION selects each? (Mikey: "why don't we have a map for that mechanism?")
The mechanism, stated generally: a function that chooses among several output
representations based on a predicate over its input. Printers choosing radical
vs power form. Formatters choosing long vs short layout. Mode branches choosing
host vs subdomain. The bug class is always the same -- THE PREDICATE ADMITS AN
INPUT IT SHOULD NOT (or misses one it should), so a valid input gets rendered
in the wrong form. The output then looks plausible and is wrong, and reading
the format strings tells you nothing: the fault is in the `if`, not the
template.
We had the RULE twice over (layout KB: "a different form means the printer
chose another branch upstream"; pattern 40: modes must preserve invariants)
and no MAP. This is the map: for a function, every branch condition paired
with the form it selects, extracted from AST. Symbolic, no model, general.
FORM MAP for _print_Pow:
IF expr.exp.is_Rational and expr.exp.q != 1 ... -> root form (\\sqrt)
ELSE -> power form (^{})
With that in front of it, "pi**(1/E) renders as a radical" stops being a
mystery: the input's path through the conditions is checkable one predicate at
a time with check().
"""
import ast, os, sys
FORMY = ("return", "format string")
def _src(node, cap=100):
try:
s = ast.unparse(node)
except Exception:
return "?"
return " ".join(s.split())[:cap]
def _form_of(body):
"""A short description of what a branch produces: its return expression or
the format-ish strings it builds."""
for n in body:
for w in ast.walk(n):
if isinstance(w, ast.Return) and w.value is not None:
return "returns " + _src(w.value, 80)
# no return: look for string constants being assembled
frags = []
for n in body:
for w in ast.walk(n):
if isinstance(w, ast.Constant) and isinstance(w.value, str) and len(w.value) > 2:
frags.append(w.value.replace("\n", "\\n")[:40])
if len(frags) >= 2:
break
return ("builds " + " ... ".join(repr(f) for f in frags[:2])) if frags else "(no visible form)"
def form_map(path, func_name, limit=10):
"""Every branch condition in `func_name` paired with the form it selects."""
try:
tree = ast.parse(open(path, encoding="utf-8", errors="ignore").read())
except Exception as e:
return {"error": str(e)[:80]}
leaf = (func_name or "").split(".")[-1]
fn = None
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == leaf:
fn = node
break
if fn is None:
return {"error": "function %s not found" % leaf}
branches = []
def walk_ifs(node, depth=0):
for ch in getattr(node, "body", []) if not isinstance(node, ast.If) else []:
pass
for w in (node.body if hasattr(node, "body") else []):
if isinstance(w, ast.If):
branches.append({"condition": _src(w.test),
"selects": _form_of(w.body), "depth": depth})
walk_ifs_body(w.body, depth + 1)
# elif chains
orelse = w.orelse
while len(orelse) == 1 and isinstance(orelse[0], ast.If):
e = orelse[0]
branches.append({"condition": _src(e.test),
"selects": _form_of(e.body), "depth": depth})
walk_ifs_body(e.body, depth + 1)
orelse = e.orelse
if orelse:
branches.append({"condition": "(otherwise)",
"selects": _form_of(orelse), "depth": depth})
walk_ifs_body(orelse, depth + 1)
else:
walk_ifs(w, depth)
def walk_ifs_body(body, depth):
for w in body:
if isinstance(w, ast.If):
branches.append({"condition": _src(w.test),
"selects": _form_of(w.body), "depth": depth})
walk_ifs_body(w.body, depth + 1)
orelse = w.orelse
while len(orelse) == 1 and isinstance(orelse[0], ast.If):
e = orelse[0]
branches.append({"condition": _src(e.test),
"selects": _form_of(e.body), "depth": depth})
walk_ifs_body(e.body, depth + 1)
orelse = e.orelse
if orelse:
walk_ifs_body(orelse, depth + 1)
elif hasattr(w, "body"):
walk_ifs_body(w.body, depth)
walk_ifs_body(fn.body, 0)
if not branches:
return None
note = ("This function SELECTS among output forms. If the output is in the "
"WRONG FORM for some input, the fault is in one of these CONDITIONS "
"-- a predicate admitting an input it should not, or missing one it "
"should -- not in the form templates. Trace YOUR input through the "
"conditions with check(). AND: a predicate and the branch it "
"selects are ONE CONTRACT. Changing WHO gets a form changes what "
"must be handled in BOTH directions -- inputs you newly exclude "
"fall through to another branch (is their output there right?), "
"and inputs you newly admit reach this renderer (can it draw "
"them?). The reported input is one point in the space. After "
"editing a condition, enumerate the input classes on each side and "
"render ONE SPECIMEN of each through the real code with check(). "
"A selector fix that only tests the reported input is half a fix "
"-- sometimes the RENDERER must change too, so it can handle "
"everything the corrected predicate sends it.")
return {"function": leaf, "branches": branches[:limit], "note": note}
if __name__ == "__main__":
r = form_map(sys.argv[1], sys.argv[2])
if not r:
print("(no branches)")
elif r.get("error"):
print("error:", r["error"])
else:
print("FORM MAP for %s:" % r["function"])
for b in r["branches"]:
print(" %sIF %s" % (" " * b["depth"], b["condition"][:88]))
print(" %s -> %s" % (" " * b["depth"], b["selects"][:80]))