""" 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 "" # --------------------------------------------------------------------------- # 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)