Skip to main content

monist_core/
graph.rs

1use crate::ast::{Atomic, Formula, FormulaArena, Var};
2use crate::eval::ExecutionLimits;
3use crate::budget::ResourceBudget;
4use std::collections::{HashMap, HashSet};
5
6#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
7pub struct ScopedVar(pub Var, pub usize);
8
9#[derive(Debug, Clone, PartialEq, Eq, Hash)]
10pub struct Constraint {
11    pub v1: ScopedVar,
12    pub v2: ScopedVar,
13    pub weight: i32,
14    pub in_comp: bool,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq, Hash)]
18pub struct Edge {
19    pub source: ScopedVar,
20    pub target: ScopedVar,
21    pub weight: i32,
22    pub in_comp: bool,
23}
24
25impl From<Constraint> for Edge {
26    fn from(c: Constraint) -> Self {
27        Edge {
28            source: c.v1,
29            target: c.v2,
30            weight: c.weight,
31            in_comp: c.in_comp,
32        }
33    }
34}
35
36pub fn extract_constraints_aux(
37    arena: &FormulaArena,
38    formula_idx: usize,
39    depth: usize,
40    in_comp: bool,
41    budget: &ResourceBudget,
42    edge_count: &mut usize,
43) -> Vec<Constraint> {
44    if depth > budget.max_depth {
45        panic!("Graph Extraction Nesting Limit Exceeded");
46    }
47    let mut constraints = Vec::new();
48
49    let formula = match arena.get(formula_idx) {
50        Some(f) => f,
51        None => return constraints,
52    };
53
54    match formula {
55        Formula::Atom(atomic) => match atomic {
56            Atomic::Eq(x, y) => {
57                let sx = ScopedVar(x.clone(), depth);
58                let sy = ScopedVar(y.clone(), depth);
59                constraints.push(Constraint {
60                    v1: sx.clone(),
61                    v2: sy.clone(),
62                    weight: 0,
63                    in_comp,
64                });
65                constraints.push(Constraint {
66                    v1: sy,
67                    v2: sx,
68                    weight: 0,
69                    in_comp,
70                });
71                *edge_count += 2;
72            }
73            Atomic::Mem(x, y) => {
74                let sx = ScopedVar(x.clone(), depth);
75                let sy = ScopedVar(y.clone(), depth);
76                constraints.push(Constraint {
77                    v1: sx.clone(),
78                    v2: sy.clone(),
79                    weight: 1,
80                    in_comp,
81                });
82                constraints.push(Constraint {
83                    v1: sy,
84                    v2: sx,
85                    weight: -1,
86                    in_comp,
87                });
88                *edge_count += 2;
89            }
90            Atomic::Lt(x, y) => {
91                let sx = ScopedVar(x.clone(), depth);
92                let sy = ScopedVar(y.clone(), depth);
93                constraints.push(Constraint {
94                    v1: sy.clone(),
95                    v2: sx.clone(),
96                    weight: -1,
97                    in_comp,
98                });
99                *edge_count += 1;
100            }
101            _ => {}
102        },
103        Formula::Neg(f_idx) => {
104            constraints.extend(extract_constraints_aux(arena, *f_idx, depth, in_comp, budget, edge_count));
105        }
106        Formula::Conj(f1_idx, f2_idx)
107        | Formula::Disj(f1_idx, f2_idx)
108        | Formula::Impl(f1_idx, f2_idx) => {
109            constraints.extend(extract_constraints_aux(arena, *f1_idx, depth, in_comp, budget, edge_count));
110            constraints.extend(extract_constraints_aux(arena, *f2_idx, depth, in_comp, budget, edge_count));
111        }
112        Formula::Univ(_, _, f_idx) | Formula::Exist(_, _, f_idx) => {
113            constraints.extend(extract_constraints_aux(arena, *f_idx, depth + 1, in_comp, budget, edge_count));
114        }
115        Formula::Comp(_, _, f_idx) => {
116            constraints.extend(extract_constraints_aux(arena, *f_idx, depth + 1, true, budget, edge_count));
117        }
118    }
119    if *edge_count > budget.max_graph_edges {
120        panic!("Graph Edge Limit Exceeded");
121    }
122    constraints
123}
124
125/// The GraphArena represents the CPU Geometry Layer in the hybrid pipeline.
126/// It translates the semantic interactions (from the `FormulaArena`) into a weighted directed graph
127/// using De Bruijn indexing and lexical depths, enabling purely structural verification.
128#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
129pub struct GraphArena {
130    pub vars: Vec<ScopedVar>,
131    pub var_to_idx: HashMap<ScopedVar, usize>,
132    pub edges: Vec<(usize, usize, i32, bool)>, // Added in_comp
133}
134
135impl GraphArena {
136    pub fn new() -> Self {
137        Self {
138            vars: Vec::new(),
139            var_to_idx: HashMap::new(),
140            edges: Vec::new(),
141        }
142    }
143
144    pub fn add_var(&mut self, var: ScopedVar) -> usize {
145        if let Some(&idx) = self.var_to_idx.get(&var) {
146            idx
147        } else {
148            let idx = self.vars.len();
149            self.vars.push(var.clone());
150            self.var_to_idx.insert(var, idx);
151            idx
152        }
153    }
154
155    pub fn from_constraints(constraints: &[Constraint]) -> Self {
156        let mut arena = Self::new();
157        for c in constraints {
158            let u = arena.add_var(c.v1.clone());
159            let v = arena.add_var(c.v2.clone());
160            arena.edges.push((u, v, c.weight, c.in_comp));
161        }
162        arena
163    }
164
165    pub fn collapse_scc_0_weight(&mut self) {
166        // Obsolete: SCC flattening is now handled natively within evaluate_topology using kosaraju_scc.
167        // This is kept strictly for CLI compatibility to avoid refactoring the CLI arguments at this moment.
168    }
169
170    /// Returns Strongly Connected Components for 0-weight edges using Kosaraju's algorithm
171    pub fn kosaraju_scc(&self) -> Vec<Vec<usize>> {
172        let n = self.vars.len();
173        if n == 0 {
174            return Vec::new();
175        }
176
177        let mut adj = vec![Vec::new(); n];
178        let mut rev_adj = vec![Vec::new(); n];
179        for &(u, v, w, _) in &self.edges {
180            if w == 0 {
181                adj[u].push(v);
182                rev_adj[v].push(u);
183            }
184        }
185
186        let mut visited = vec![false; n];
187        let mut finish_order = Vec::new();
188
189        fn dfs1(u: usize, adj: &[Vec<usize>], visited: &mut [bool], finish_order: &mut Vec<usize>) {
190            visited[u] = true;
191            for &v in &adj[u] {
192                if !visited[v] {
193                    dfs1(v, adj, visited, finish_order);
194                }
195            }
196            finish_order.push(u);
197        }
198
199        for i in 0..n {
200            if !visited[i] {
201                dfs1(i, &adj, &mut visited, &mut finish_order);
202            }
203        }
204
205        visited.fill(false);
206        let mut sccs = Vec::new();
207
208        fn dfs2(u: usize, rev_adj: &[Vec<usize>], visited: &mut [bool], current_scc: &mut Vec<usize>) {
209            visited[u] = true;
210            current_scc.push(u);
211            for &v in &rev_adj[u] {
212                if !visited[v] {
213                    dfs2(v, rev_adj, visited, current_scc);
214                }
215            }
216        }
217
218        for &u in finish_order.iter().rev() {
219            if !visited[u] {
220                let mut current_scc = Vec::new();
221                dfs2(u, &rev_adj, &mut visited, &mut current_scc);
222                sccs.push(current_scc);
223            }
224        }
225
226        sccs
227    }
228
229    pub fn contract_graph(&self, sccs: &[Vec<usize>]) -> (Vec<usize>, Vec<(usize, usize, i32)>, Vec<usize>) {
230        let n = self.vars.len();
231        let mut rep = vec![0; n];
232        for scc in sccs {
233            let r = *scc.iter().min().unwrap_or(&0);
234            for &u in scc {
235                rep[u] = r;
236            }
237        }
238
239        let mut c_vars = Vec::new();
240        let mut c_vars_set = std::collections::HashSet::new();
241        for &r in &rep {
242            if c_vars_set.insert(r) {
243                c_vars.push(r);
244            }
245        }
246
247        let mut c_edges = std::collections::HashSet::new();
248        for &(u, v, w, _) in &self.edges {
249            let ru = rep[u];
250            let rv = rep[v];
251            if ru != rv || w != 0 {
252                c_edges.insert((ru, rv, w));
253            }
254        }
255
256        (c_vars, c_edges.into_iter().collect(), rep)
257    }
258
259    /// Continuous daemon that isolates Strongly Cantorian (ZFC-compliant) bedrock
260    /// by scanning for subgraphs satisfying the x = T(x) constraint (topological self-loops)
261    /// and severing their outgoing +1 offset edges to reduce computational load.
262    pub fn isolate_sc_bedrock(&mut self) -> Vec<String> {
263        let mut sc_nodes = HashSet::new();
264
265        // Detect x = T(x) constraints: nodes that have a +1 or -1 weight self-loop.
266        // We strictly enforce that Comprehension boundaries are respected:
267        // if the self-loop is part of a Comprehension (in_comp == true), it is an
268        // unstratifiable paradox and MUST NOT be isolated as Strongly Cantorian bedrock.
269        for &(u, v, w, in_comp) in &self.edges {
270            if u == v && w != 0 && !in_comp {
271                sc_nodes.insert(u);
272            }
273        }
274
275        if sc_nodes.is_empty() {
276            return Vec::new();
277        }
278
279        let mut actions = Vec::new();
280        let mut new_edges = HashSet::new();
281
282        for &(u, v, w, in_comp) in &self.edges {
283            if u == v && w != 0 && !in_comp {
284                actions.push(format!(
285                    "Neutralized SC defining self-loop on {}",
286                    self.var_name(u)
287                ));
288                continue;
289            }
290            // Only sever connections if they are NOT inside a Comprehension
291            if sc_nodes.contains(&u) && w == 1 && !in_comp {
292                actions.push(format!(
293                    "Severed outgoing +1 offset edge from SC bedrock node {} to {}",
294                    self.var_name(u),
295                    self.var_name(v)
296                ));
297                continue;
298            }
299            if sc_nodes.contains(&v) && w == -1 && !in_comp {
300                actions.push(format!(
301                    "Severed incoming -1 offset edge to SC bedrock node {} from {}",
302                    self.var_name(v),
303                    self.var_name(u)
304                ));
305                continue;
306            }
307            new_edges.insert((u, v, w, in_comp));
308        }
309        self.edges = new_edges.into_iter().collect();
310
311        // Remove duplicates and return
312        let mut unique_actions: Vec<String> = actions
313            .into_iter()
314            .collect::<HashSet<_>>()
315            .into_iter()
316            .collect();
317        unique_actions.sort();
318        unique_actions
319    }
320
321    fn var_name(&self, u: usize) -> String {
322        let var = &self.vars[u];
323        let name = match &var.0 {
324            crate::ast::Var::Free(n) => n.clone(),
325            crate::ast::Var::Bound(idx) => format!("b{}", idx),
326        };
327        format!("{}_{}", name, var.1)
328    }
329
330
331    pub fn topological_sort(&self) -> Option<Vec<usize>> {
332        let n = self.vars.len();
333        let mut in_degree = vec![0; n];
334        let mut adj = vec![Vec::new(); n];
335
336        for &(u, v, _, _) in &self.edges {
337            adj[u].push(v);
338            in_degree[v] += 1;
339        }
340
341        let mut queue = std::collections::VecDeque::new();
342        for i in 0..n {
343            if in_degree[i] == 0 {
344                queue.push_back(i);
345            }
346        }
347
348        let mut order = Vec::new();
349        while let Some(u) = queue.pop_front() {
350            order.push(u);
351            for &v in &adj[u] {
352                in_degree[v] -= 1;
353                if in_degree[v] == 0 {
354                    queue.push_back(v);
355                }
356            }
357        }
358
359        if order.len() == n {
360            Some(order)
361        } else {
362            None
363        }
364    }
365
366    pub fn classify_subsystems(&self, d: &[i32]) -> (bool, bool) {
367        let mut base_weight = i32::MIN;
368        for (i, var) in self.vars.iter().enumerate() {
369            if let crate::ast::Var::Free(_) = var.0 {
370                if d[i] > base_weight {
371                    base_weight = d[i];
372                }
373            }
374        }
375
376        if base_weight == i32::MIN {
377            for &w in d {
378                if w > base_weight {
379                    base_weight = w;
380                }
381            }
382            if base_weight == i32::MIN {
383                base_weight = 0;
384            }
385        }
386
387        let mut is_nfi = true;
388        let mut is_nfp = true;
389
390        for (i, var) in self.vars.iter().enumerate() {
391            let weight = d[i];
392            
393            if weight > base_weight + 1 {
394                is_nfi = false;
395            }
396
397            match var.0 {
398                crate::ast::Var::Free(_) => {
399                    if weight > base_weight + 1 {
400                        is_nfp = false;
401                    }
402                }
403                crate::ast::Var::Bound(_) => {
404                    if weight > base_weight {
405                        is_nfp = false;
406                    }
407                }
408            }
409        }
410
411        (is_nfp, is_nfi)
412    }
413
414    /// Evaluates the topological structure using a hybrid approach.
415    /// It attempts a fast O(V+E) DAG Shortest Path evaluation first. If the graph contains 
416    /// cycles, it falls back to the O(V*E) Bellman-Ford algorithm to detect negative-weight cycles
417    /// (Extensionality Collisions).
418    pub fn evaluate_topology(&mut self) -> Result<(Vec<i32>, Vec<String>, bool, bool), String> {
419        // Run the continuous daemon to dynamically sever outgoing +1 offset edges from SC bedrock
420        let sc_actions = self.isolate_sc_bedrock();
421
422        let n = self.vars.len();
423        if n == 0 {
424            return Ok((Vec::new(), sc_actions, true, true));
425        }
426
427        let sccs = self.kosaraju_scc();
428        let (c_vars, c_edges, reps) = self.contract_graph(&sccs);
429
430        let mut in_degree = HashMap::new();
431        for &u in &c_vars {
432            in_degree.insert(u, 0);
433        }
434        let mut adj = HashMap::new();
435        for &(u, v, w) in &c_edges {
436            adj.entry(u).or_insert_with(Vec::new).push((v, w));
437            *in_degree.entry(v).or_insert(0) += 1;
438        }
439
440        let mut queue = std::collections::VecDeque::new();
441        for (&u, &deg) in &in_degree {
442            if deg == 0 {
443                queue.push_back(u);
444            }
445        }
446
447        let mut order = Vec::new();
448        while let Some(u) = queue.pop_front() {
449            order.push(u);
450            if let Some(neighbors) = adj.get(&u) {
451                for &(v, _) in neighbors {
452                    if let Some(deg) = in_degree.get_mut(&v) {
453                        *deg -= 1;
454                        if *deg == 0 {
455                            queue.push_back(v);
456                        }
457                    }
458                }
459            }
460        }
461
462        // Fast-path: O(V+E) DAG Shortest Path on Contracted Graph
463        if order.len() == c_vars.len() {
464            let mut c_d = HashMap::new();
465            for &u in &c_vars {
466                c_d.insert(u, 0);
467            }
468            for &u in &order {
469                let du = *c_d.get(&u).unwrap();
470                if let Some(neighbors) = adj.get(&u) {
471                    for &(v, w) in neighbors {
472                        let dv = *c_d.get(&v).unwrap();
473                        if du + w < dv {
474                            c_d.insert(v, du + w);
475                        }
476                    }
477                }
478            }
479
480            let mut d = vec![0; n];
481            for i in 0..n {
482                d[i] = *c_d.get(&reps[i]).unwrap();
483            }
484
485            let (is_nfp, is_nfi) = self.classify_subsystems(&d);
486            return Ok((d, sc_actions, is_nfp, is_nfi));
487        }
488
489        // Fallback: O(V*E) Bellman-Ford
490        let mut d = vec![0; n];
491        let mut p: Vec<Option<(usize, i32)>> = vec![None; n];
492
493        // Relax edges n-1 times
494        for _ in 0..n {
495            let mut changed = false;
496            for &(u, v, w, _) in &self.edges {
497                if d[u] + w < d[v] {
498                    d[v] = d[u] + w;
499                    p[v] = Some((u, w));
500                    changed = true;
501                }
502            }
503            if !changed {
504                break;
505            }
506        }
507
508        // Final pass for negative weight cycles
509        let mut collision_vertex = None;
510        for &(u, v, w, _) in &self.edges {
511            if d[u] + w < d[v] {
512                collision_vertex = Some(v);
513                p[v] = Some((u, w));
514                break;
515            }
516        }
517
518        if let Some(mut curr) = collision_vertex {
519            let lambda_star = match ExecutionLimits::compute_for_graph(self) {
520                Some(limits) => limits.mcm,
521                None => f64::NEG_INFINITY,
522            };
523
524            for _ in 0..n {
525                curr = p[curr].unwrap().0;
526            }
527
528            let cycle_start = curr;
529            let mut cycle = Vec::new();
530
531            loop {
532                let (prev, w) = p[curr].unwrap();
533                cycle.push((prev, curr, w));
534                curr = prev;
535                if curr == cycle_start {
536                    break;
537                }
538            }
539
540            cycle.reverse();
541
542            let mut result = String::new();
543            result.push_str(&format!("Extensionality Collision: Negative-weight cycle detected (μ* = {:.4})!\n", lambda_star));
544            result.push_str("Engine halted safely (K_ITERATION_HALT)\n");
545            result.push_str("Topological Trace: ");
546
547            let mut sum_str = Vec::new();
548            let mut total_weight = 0;
549            for (u, v, w) in &cycle {
550                let u_var = &self.vars[*u];
551                let v_var = &self.vars[*v];
552
553                let u_name = match &u_var.0 {
554                    crate::ast::Var::Free(name) => name.clone(),
555                    crate::ast::Var::Bound(idx) => format!("b{}", idx),
556                };
557                let v_name = match &v_var.0 {
558                    crate::ast::Var::Free(name) => name.clone(),
559                    crate::ast::Var::Bound(idx) => format!("b{}", idx),
560                };
561
562                let u_str = format!("{}_{}", u_name, u_var.1);
563                let v_str = format!("{}_{}", v_name, v_var.1);
564
565                sum_str.push(format!("{} -> {} ({})", u_str, v_str, w));
566                total_weight += w;
567            }
568
569            result.push_str(&sum_str.join(" + "));
570            result.push_str(&format!(" = {}", total_weight));
571
572            return Err(result);
573        }
574
575        let (is_nfp, is_nfi) = self.classify_subsystems(&d);
576        Ok((d, sc_actions, is_nfp, is_nfi))
577    }
578
579    /// Extract Minimal Conflict Clauses for Vector Superposition (IDL Masking)
580    /// When Bellman-Ford flags a negative-weight cycle, this identifies the nodes
581    /// involved so the upper ingestion layer can translate them into a hyperdimensional 
582    /// destructive interference mask.
583    pub fn extract_conflict_clauses(&mut self) -> Vec<Vec<usize>> {
584        let n = self.vars.len();
585        let mut d = vec![0; n];
586        let mut p: Vec<Option<(usize, i32)>> = vec![None; n];
587        
588        // Relax edges
589        for _ in 0..n {
590            for &(u, v, w, _) in &self.edges {
591                if d[u] + w < d[v] {
592                    d[v] = d[u] + w;
593                    p[v] = Some((u, w));
594                }
595            }
596        }
597        
598        let mut conflict_clauses = Vec::new();
599        // Detect cycle
600        for &(u, v, w, _) in &self.edges {
601            if d[u] + w < d[v] {
602                // We found a node 'v' in a negative weight cycle
603                let mut curr = v;
604                for _ in 0..n {
605                    if let Some((prev, _)) = p[curr] {
606                        curr = prev;
607                    }
608                }
609                
610                let cycle_start = curr;
611                let mut cycle = Vec::new();
612                
613                loop {
614                    if let Some((prev, _)) = p[curr] {
615                        cycle.push(curr);
616                        curr = prev;
617                        if curr == cycle_start {
618                            break;
619                        }
620                    } else {
621                        break;
622                    }
623                }
624                cycle.reverse();
625                
626                // Only add if not already present
627                let mut sorted_cycle = cycle.clone();
628                sorted_cycle.sort();
629                
630                let is_duplicate = conflict_clauses.iter().any(|c: &Vec<usize>| {
631                    let mut sc = c.clone();
632                    sc.sort();
633                    sc == sorted_cycle
634                });
635                
636                if !is_duplicate {
637                    conflict_clauses.push(cycle);
638                }
639            }
640        }
641        
642        conflict_clauses
643    }
644}