ashritaa-chinnari123's picture
Upload 8 files
38b27cd verified
Raw
History Blame Contribute Delete
11.3 kB
"""
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)