"""CRX Refined — Cluster-then-infer CRX with over-approximation detection. Standard CRX produces over-approximated grammars when the Hasse diagram is non-linear (branching). This module fixes that by: 1. Clustering similar sequences before inference 2. Inferring per-cluster (tighter grammars) 3. Reporting confidence metrics Key insight: code call sequences have branching patterns that CHAREs (linear chain expressions) can't represent. Clustering reduces branching within each group, making CRX's linear assumption more valid. Tradeoff: cluster granularity. - Too coarse → over-approximation (standard CRX) - Too fine → no generalization (one grammar per sequence) - Sweet spot → cluster by structural features """ from collections import defaultdict from .crx import CRX def _cluster_by_structure(sequences): """Cluster sequences by structural features. Uses (first_symbol, last_symbol, length_bucket) as the cluster key. This groups sequences that start and end the same way, which typically means they follow the same calling pattern. Args: sequences: list of token lists Returns: dict mapping cluster_key → list of sequences """ groups = defaultdict(list) for seq in sequences: if not seq: continue # Length bucket: short (1-3), medium (4-8), long (9+) length = len(seq) if length <= 3: length_bucket = 'short' elif length <= 8: length_bucket = 'med' else: length_bucket = 'long' key = (seq[0], seq[-1], length_bucket) groups[key].append(seq) return dict(groups) def crx_refined(sequences, min_cluster=2): """Cluster-then-infer CRX. Clusters sequences by (first, last, length), infers CRX per cluster, and returns the grammar from the largest cluster. Falls back to standard CRX if no cluster has enough sequences. Args: sequences: list of token lists min_cluster: minimum cluster size to infer from (default: 2) Returns: CHARE expression string """ S = [list(s) for s in sequences if s] if not S: return 'ε' clusters = _cluster_by_structure(S) # Find clusters large enough to infer from valid_clusters = {k: v for k, v in clusters.items() if len(v) >= min_cluster} if not valid_clusters: # No cluster large enough — fall back to standard CRX return CRX().infer(S) # Pick the largest cluster best_key = max(valid_clusters, key=lambda k: len(valid_clusters[k])) best_cluster = valid_clusters[best_key] return CRX().infer(best_cluster) def crx_with_confidence(sequences, min_cluster=2): """Cluster-then-infer CRX with confidence metrics. Returns: dict with keys: grammar: str — the refined grammar confidence: float — fraction of training pairs captured by grammar structure n_clusters: int — number of clusters largest_cluster: int — size of largest cluster n_sequences: int — total sequences """ S = [list(s) for s in sequences if s] if not S: return { 'grammar': 'ε', 'confidence': 1.0, 'n_clusters': 0, 'largest_cluster': 0, 'n_sequences': 0, } clusters = _cluster_by_structure(S) valid_clusters = {k: v for k, v in clusters.items() if len(v) >= min_cluster} if valid_clusters: best_key = max(valid_clusters, key=lambda k: len(valid_clusters[k])) best_cluster = valid_clusters[best_key] grammar = CRX().infer(best_cluster) else: best_cluster = S grammar = CRX().infer(S) # Compute confidence: what fraction of consecutive pairs in the training # data are "captured" by the grammar's structure all_pairs = set() for w in S: for i in range(len(w) - 1): all_pairs.add((w[i], w[i + 1])) # Pairs in the best cluster that appear in grammar structure cluster_pairs = set() for w in best_cluster: for i in range(len(w) - 1): cluster_pairs.add((w[i], w[i + 1])) # How many training pairs are cluster pairs? (coverage) captured = len(all_pairs & cluster_pairs) confidence = captured / len(all_pairs) if all_pairs else 1.0 return { 'grammar': grammar, 'confidence': confidence, 'n_clusters': len(clusters), 'largest_cluster': len(best_cluster), 'n_sequences': len(S), }