File size: 11,336 Bytes
38b27cd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
"""
Line-by-line code explainer using Python's `ast` module — v2.
 
Improvement over v1: instead of just echoing back the code's own syntax
(e.g. "If n < 2, then: Returns False" — which is really just the code
with English scaffolding), this version translates expressions into
actual natural language (e.g. "Checks whether n is less than 2 — if so,
returns False, meaning n is not prime.").
 
Still fully deterministic (based on the real parsed structure, not a
guess), but reads like an explanation a person would give, not a
transliteration of the syntax.
"""
 
import ast
 
 
# ---------------------------------------------------------------------------
# Expression -> natural English translation
# ---------------------------------------------------------------------------
 
_CMP_WORDS = {
    ast.Lt: "is less than",
    ast.LtE: "is less than or equal to",
    ast.Gt: "is greater than",
    ast.GtE: "is greater than or equal to",
    ast.Eq: "is equal to",
    ast.NotEq: "is not equal to",
    ast.In: "is in",
    ast.NotIn: "is not in",
    ast.Is: "is",
    ast.IsNot: "is not",
}
 
_BINOP_WORDS = {
    ast.Add: "plus",
    ast.Sub: "minus",
    ast.Mult: "times",
    ast.Div: "divided by",
    ast.FloorDiv: "divided by (rounded down)",
    ast.Mod: "modulo",
    ast.Pow: "to the power of",
}
 
_BOOLOP_WORDS = {
    ast.And: "and",
    ast.Or: "or",
}
 
 
def expr_to_text(node) -> str:
    """Recursively turn an AST expression into a natural-English phrase."""
    if node is None:
        return "nothing"
 
    if isinstance(node, ast.Constant):
        return repr(node.value)
 
    if isinstance(node, ast.Name):
        return node.id
 
    if isinstance(node, ast.Attribute):
        return f"{expr_to_text(node.value)}.{node.attr}"
 
    if isinstance(node, ast.Subscript):
        return f"{expr_to_text(node.value)}[{expr_to_text(node.slice)}]"
 
    if isinstance(node, ast.Compare):
        left = expr_to_text(node.left)
        parts = [left]
        for op, comparator in zip(node.ops, node.comparators):
            word = _CMP_WORDS.get(type(op), "compares to")
            parts.append(f"{word} {expr_to_text(comparator)}")
        return " ".join(parts)
 
    if isinstance(node, ast.BoolOp):
        word = _BOOLOP_WORDS.get(type(node.op), "and")
        return f" {word} ".join(expr_to_text(v) for v in node.values)
 
    if isinstance(node, ast.UnaryOp):
        if isinstance(node.op, ast.Not):
            return f"not ({expr_to_text(node.operand)})"
        if isinstance(node.op, ast.USub):
            return f"negative {expr_to_text(node.operand)}"
        return expr_to_text(node.operand)
 
    if isinstance(node, ast.BinOp):
        word = _BINOP_WORDS.get(type(node.op), "combined with")
        return f"{expr_to_text(node.left)} {word} {expr_to_text(node.right)}"
 
    if isinstance(node, ast.Call):
        func_name = expr_to_text(node.func)
        args_text = [expr_to_text(a) for a in node.args]
 
        # Friendly phrasing for very common built-ins
        if func_name == "range":
            if len(args_text) == 1:
                return f"every whole number from 0 up to (not including) {args_text[0]}"
            elif len(args_text) == 2:
                return f"every whole number from {args_text[0]} up to (not including) {args_text[1]}"
            elif len(args_text) == 3:
                return f"every whole number from {args_text[0]} up to (not including) {args_text[1]}, stepping by {args_text[2]}"
        if func_name == "int":
            return f"the whole-number part of {args_text[0]}" if args_text else "an integer"
        if func_name == "len":
            return f"the length of {args_text[0]}" if args_text else "a length"
        if func_name == "str":
            return f"{args_text[0]} converted to text" if args_text else "a text value"
        if func_name in ("sum", "max", "min", "sorted", "reversed", "list", "set"):
            verb = {
                "sum": "the sum of",
                "max": "the largest value in",
                "min": "the smallest value in",
                "sorted": "a sorted version of",
                "reversed": "a reversed version of",
                "list": "a list built from",
                "set": "a set built from",
            }[func_name]
            return f"{verb} {args_text[0]}" if args_text else verb
 
        # Generic fallback: "the result of calling foo(a, b)"
        joined_args = ", ".join(args_text)
        return f"the result of calling {func_name}({joined_args})"
 
    if isinstance(node, ast.List):
        return "[" + ", ".join(expr_to_text(e) for e in node.elts) + "]"
 
    if isinstance(node, ast.Tuple):
        return "(" + ", ".join(expr_to_text(e) for e in node.elts) + ")"
 
    # Fallback: use ast.unparse if we don't have a specific rule for this node type
    try:
        return ast.unparse(node)
    except Exception:
        return "<expression>"
 
 
# ---------------------------------------------------------------------------
# Statement -> natural English translation
# ---------------------------------------------------------------------------
 
def _describe(node, indent=0, class_name=None):
    prefix = "  " * indent
    lines = []
 
    if isinstance(node, ast.FunctionDef):
        args = [a.arg for a in node.args.args]
        is_method = bool(args) and args[0] in ("self", "cls")
        display_args = args[1:] if is_method else args
        arg_text = " and ".join(display_args) if display_args else "no additional input"
 
        if node.name == "__init__":
            owner = class_name or "this"
            lines.append(f"{prefix}Line {node.lineno}: This is the constructor — it runs automatically whenever a new `{owner}` object is created, setting up {arg_text}.")
        elif is_method:
            lines.append(f"{prefix}Line {node.lineno}: Defines a method called `{node.name}` (called on instances of this class), which takes {arg_text} as input.")
        else:
            lines.append(f"{prefix}Line {node.lineno}: Defines a function called `{node.name}`, which takes {arg_text} as input.")
        for stmt in node.body:
            lines.extend(_describe(stmt, indent + 1, class_name=class_name))
 
    elif isinstance(node, ast.ClassDef):
        bases = [expr_to_text(b) for b in node.bases]
        if bases:
            lines.append(f"{prefix}Line {node.lineno}: Defines a class called `{node.name}`, which inherits from {', '.join(bases)}.")
        else:
            lines.append(f"{prefix}Line {node.lineno}: Defines a class called `{node.name}` — a blueprint for creating objects that bundle related data and behavior together.")
        for stmt in node.body:
            lines.extend(_describe(stmt, indent + 1, class_name=node.name))
 
    elif isinstance(node, ast.If):
        cond = expr_to_text(node.test)
        lines.append(f"{prefix}Line {node.lineno}: Checks whether {cond}. If true:")
        for stmt in node.body:
            lines.extend(_describe(stmt, indent + 1, class_name=class_name))
        if node.orelse:
            lines.append(f"{prefix}Otherwise (if that condition is false):")
            for stmt in node.orelse:
                lines.extend(_describe(stmt, indent + 1, class_name=class_name))
 
    elif isinstance(node, ast.For):
        target = expr_to_text(node.target)
        iterable = expr_to_text(node.iter)
        lines.append(f"{prefix}Line {node.lineno}: Repeats the following, setting {target} to {iterable}, one at a time:")
        for stmt in node.body:
            lines.extend(_describe(stmt, indent + 1, class_name=class_name))
 
    elif isinstance(node, ast.While):
        cond = expr_to_text(node.test)
        lines.append(f"{prefix}Line {node.lineno}: Keeps repeating the following as long as {cond}:")
        for stmt in node.body:
            lines.extend(_describe(stmt, indent + 1, class_name=class_name))
 
    elif isinstance(node, ast.Return):
        val = expr_to_text(node.value) if node.value is not None else "nothing"
        lines.append(f"{prefix}Line {node.lineno}: Stops the function here and gives back {val}.")
 
    elif isinstance(node, ast.Assign):
        targets = " and ".join(expr_to_text(t) for t in node.targets)
        val = expr_to_text(node.value)
        lines.append(f"{prefix}Line {node.lineno}: Stores {val} in {targets}.")
 
    elif isinstance(node, ast.AugAssign):
        target = expr_to_text(node.target)
        val = expr_to_text(node.value)
        op_word = _BINOP_WORDS.get(type(node.op), "combined with")
        lines.append(f"{prefix}Line {node.lineno}: Updates {target} by taking its current value {op_word} {val}.")
 
    elif isinstance(node, ast.Expr):
        if isinstance(node.value, ast.Constant) and isinstance(node.value.value, str):
            doc_text = node.value.value.strip().splitlines()[0]  # first line, in case it's multi-line
            lines.append(f'{prefix}Line {node.lineno}: Documentation string: "{doc_text}"')
        else:
            lines.append(f"{prefix}Line {node.lineno}: Runs {expr_to_text(node.value)}.")
 
    elif isinstance(node, ast.Raise):
        if node.exc is not None:
            if isinstance(node.exc, ast.Call):
                exc_name = expr_to_text(node.exc.func)
                exc_args = [expr_to_text(a) for a in node.exc.args]
                if exc_args:
                    lines.append(f"{prefix}Line {node.lineno}: Raises a {exc_name} error with the message {exc_args[0]}.")
                else:
                    lines.append(f"{prefix}Line {node.lineno}: Raises a {exc_name} error.")
            else:
                lines.append(f"{prefix}Line {node.lineno}: Raises an error — {expr_to_text(node.exc)}.")
        else:
            lines.append(f"{prefix}Line {node.lineno}: Re-raises the current error.")
 
    elif isinstance(node, ast.Import):
        names = ", ".join(a.name for a in node.names)
        lines.append(f"{prefix}Line {node.lineno}: Brings in the {names} module so its functions can be used.")
 
    elif isinstance(node, ast.ImportFrom):
        names = ", ".join(a.name for a in node.names)
        lines.append(f"{prefix}Line {node.lineno}: Brings in {names} from the {node.module} module.")
 
    else:
        try:
            lines.append(f"{prefix}Line {node.lineno}: {ast.unparse(node)}")
        except Exception:
            pass
 
    return lines
 
 
def explain_line_by_line(code: str):
    """
    Returns (success: bool, result)
    - If code parses: (True, list_of_explanation_strings)
    - If code has a syntax error: (False, error_message_string)
    """
    try:
        tree = ast.parse(code)
    except SyntaxError as e:
        return False, f"Can't generate a line-by-line breakdown: the code has a syntax error ({e.msg} at line {e.lineno})."
 
    explanation_lines = []
    for stmt in tree.body:
        explanation_lines.extend(_describe(stmt))
 
    if not explanation_lines:
        return True, ["(No statements found to explain.)"]
    return True, explanation_lines
 
 
if __name__ == "__main__":
    sample = """
def is_prime(n):
    if n < 2:
        return False
    for i in range(2, int(n ** 0.5) + 1):
        if n % i == 0:
            return False
    return True
"""
    ok, result = explain_line_by_line(sample)
    print("\n".join(result) if ok else result)