""" phi.ast_parse — Layer 3: Parse tree analysis → τ and δ distributions Converts equation strings to Python ASTs (after preprocessing math notation), then computes: τ(E) — node-type frequency histogram over Δ_{k-1} δ(E) — child-ordering frequency histogram over Δ_{2k²-1} Dependencies: Python ast, re (stdlib). Independent of phi.charclass. """ from __future__ import annotations import ast import re from typing import Dict, List, Optional, Tuple # ── Node types recognized by the parser ────────────────────────────────── NODE_TYPES = [ "bin_add", "bin_sub", "bin_mul", "bin_div", "bin_pow", "bin_eq", "un_neg", "un_not", "var", "const", "call", "subscript", "tuple", "list", "dict", "set", "lambda", "if_exp", ] # ── Math notation preprocessor ─────────────────────────────────────────── def preprocess_math(text: str) -> str: """Preprocess math notation into Python-parseable form. Handles: - Single = → == (but preserves <=, >=, !=, ==) - |x| → abs(x) - Parenthesized subscripts: p_(i+j) → p[i+j] - Implicit multiplication: (x)(y) → (x)*(y) - TeX markers: removed """ s = text.strip() if s.endswith(":"): return "" s = re.sub(r'\$|\$\$|\\\[|\\\]|\\\(|\\\)', '', s) s = re.sub(r'(?=!])=(?!=)', '==', s) s = re.sub(r'\|([^|=]+)\|', r'abs(\1)', s) s = re.sub(r'([a-zA-Z])_\(([^)]+)\)', r'\1[\2]', s) s = re.sub(r'\)\(', r')*(', s) return s def parse_to_ast(equation: str) -> Optional[ast.AST]: """Parse an equation string into a Python AST. Tries direct parse first, then wraps in parens for expressions where math notation drops outer parentheses. """ text = preprocess_math(equation) if not text: return None try: return ast.parse(text, mode="eval") except SyntaxError: pass try: return ast.parse(f"({text})", mode="eval") except SyntaxError: return None # ── AST node classification ────────────────────────────────────────────── def _classify_node(node: ast.AST) -> str: """Map an AST node to one of the NODE_TYPES.""" if isinstance(node, ast.BinOp): if isinstance(node.op, ast.Add): return "bin_add" if isinstance(node.op, ast.Sub): return "bin_sub" if isinstance(node.op, ast.Mult): return "bin_mul" if isinstance(node.op, ast.Div): return "bin_div" if isinstance(node.op, ast.Pow): return "bin_pow" return "bin_add" if isinstance(node, ast.UnaryOp): if isinstance(node.op, ast.USub): return "un_neg" if isinstance(node.op, ast.Not): return "un_not" return "un_neg" if isinstance(node, ast.Name): return "var" if isinstance(node, ast.Constant): return "const" if isinstance(node, ast.Call): return "call" if isinstance(node, ast.Subscript): return "subscript" if isinstance(node, ast.Tuple): return "tuple" return "const" # ── τ(E): node-type frequencies → Δ_{k-1} ─────────────────────────────── def compute_tau(equation: str) -> Optional[List[float]]: """Compute τ(E) — normalized node-type histogram. Returns 18 floats (one per NODE_TYPE) summing to 1.0, or None if the equation cannot be parsed. """ tree = parse_to_ast(equation) if tree is None: return None counts = {t: 0 for t in NODE_TYPES} for node in ast.walk(tree): t = _classify_node(node) counts[t] = counts.get(t, 0) + 1 total = sum(counts.values()) if total == 0: return [1.0 / len(NODE_TYPES)] * len(NODE_TYPES) return [counts[t] / total for t in NODE_TYPES] # ── δ(E): child-ordering frequencies → Δ_{2k²-1} ──────────────────────── # Which (parent, child) type combinations are tracked in δ DELTA_PARENT_TYPES = [ "bin_add", "bin_sub", "bin_mul", "bin_div", "bin_pow", "un_neg", "un_not", "call", "subscript", ] DELTA_CHILD_TYPES = [ "bin_add", "bin_sub", "bin_mul", "bin_div", "bin_pow", "un_neg", "var", "const", "call", ] MAX_CHILDREN = 3 def compute_delta(equation: str) -> Optional[List[float]]: """Compute δ(E) — child-ordering frequency histogram. For each (parent_type, child_type, child_index) triple, returns the fraction of all parent-child edges. Captures syntactic topology beyond raw node counts. Returns a flat list of length len(DELTA_PARENT_TYPES) × len(DELTA_CHILD_TYPES) × MAX_CHILDREN, or None if parse fails. """ tree = parse_to_ast(equation) if tree is None: return None edges: Dict[Tuple[str, str, int], int] = {} total_edges = 0 def walk(node: ast.AST, _child_idx: int = 0): nonlocal total_edges parent_t = _classify_node(node) for i, child in enumerate(ast.iter_child_nodes(node)): child_t = _classify_node(child) key = (parent_t, child_t, i) edges[key] = edges.get(key, 0) + 1 total_edges += 1 walk(child, i) walk(tree) if total_edges == 0: return None result = [] for pt in DELTA_PARENT_TYPES: for ct in DELTA_CHILD_TYPES: for i in range(MAX_CHILDREN): result.append(edges.get((pt, ct, i), 0) / total_edges) return result