logicaffeine_language/semantics/
knowledge_graph.rs

1//! Knowledge Graph extraction for hardware verification.
2//!
3//! Extracts a structured Knowledge Graph from parsed hardware specifications,
4//! providing LLMs with formally grounded context for SVA generation.
5//!
6//! ## Ontology (Sprint 0E)
7//!
8//! The formal hardware ontology provides 28+ entity types and 24+ relation types,
9//! replacing AssertionForge's 35 LLM-prompt labels and 59 string labels with
10//! formally grounded, parameterized, serializable types.
11
12use serde::{Serialize, Deserialize};
13
14// ═══════════════════════════════════════════════════════════════════════════
15// HELPER ENUMS FOR ENTITY PARAMETERS
16// ═══════════════════════════════════════════════════════════════════════════
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19pub enum PortDirection { Input, Output, Inout }
20
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22pub enum SignalType { Wire, Reg, Logic }
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25pub enum ResetPolarity { ActiveHigh, ActiveLow }
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28pub enum CounterDirection { Up, Down, UpDown }
29
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31pub enum ArbitrationScheme { RoundRobin, Priority, WeightedRoundRobin }
32
33// ═══════════════════════════════════════════════════════════════════════════
34// FORMAL HARDWARE ONTOLOGY — 28+ ENTITY TYPES
35// ═══════════════════════════════════════════════════════════════════════════
36
37/// Formal hardware entity types with parameterized attributes.
38///
39/// 28 variants organized into 6 categories:
40/// - **Structural (8)**: Module, Port, Signal, Register, Memory, Fifo, Bus, Parameter
41/// - **Control (5)**: Fsm, Counter, Arbiter, Decoder, Mux
42/// - **Temporal (3)**: Clock, Reset, Interrupt
43/// - **Protocol (3)**: Handshake, Pipeline, Transaction
44/// - **Data (3)**: DataPath, Address, Configuration
45/// - **Property (6)**: SafetyProperty, LivenessProperty, FairnessProperty,
46///   ResponseProperty, MutexProperty, StabilityProperty
47#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
48pub enum HwEntityType {
49    // Structural (8)
50    Module { name: String, is_top: bool },
51    Port { direction: PortDirection, width: u32, domain: Option<String> },
52    Signal { width: u32, signal_type: SignalType, domain: Option<String> },
53    Register { width: u32, reset_value: Option<u64>, clock: Option<String> },
54    Memory { depth: u32, width: u32, ports: u8 },
55    Fifo { depth: u32, width: u32 },
56    Bus { width: u32, protocol: Option<String> },
57    Parameter { value: String },
58
59    // Control (5)
60    Fsm { states: Vec<String>, initial: Option<String> },
61    Counter { width: u32, direction: CounterDirection },
62    Arbiter { scheme: ArbitrationScheme, ports: u8 },
63    Decoder { input_width: u32, output_width: u32 },
64    Mux { inputs: u8, select_width: u32 },
65
66    // Temporal (3)
67    Clock { frequency: Option<String>, domain: String },
68    Reset { polarity: ResetPolarity, synchronous: bool },
69    Interrupt { priority: Option<u8>, edge_triggered: bool },
70
71    // Protocol (3)
72    Handshake { valid_signal: String, ready_signal: String },
73    Pipeline { stages: u32, stall_signal: Option<String> },
74    Transaction { request: String, response: String },
75
76    // Data (3)
77    DataPath { width: u32, signed: bool },
78    Address { width: u32, base: Option<u64>, range: Option<u64> },
79    Configuration { fields: Vec<String> },
80
81    // Property (6)
82    SafetyProperty { formula: String },
83    LivenessProperty { formula: String },
84    FairnessProperty { formula: String },
85    ResponseProperty { trigger: String, response: String, bound: Option<u32> },
86    MutexProperty { signals: Vec<String> },
87    StabilityProperty { signal: String, condition: String },
88}
89
90// ═══════════════════════════════════════════════════════════════════════════
91// FORMAL HARDWARE ONTOLOGY — 24+ RELATION TYPES
92// ═══════════════════════════════════════════════════════════════════════════
93
94/// Formal hardware relation types with parameterized attributes.
95///
96/// 24 variants organized into 6 categories:
97/// - **Data Flow (5)**: Drives, DrivesRegistered, DataFlow, Reads, Writes
98/// - **Control Flow (4)**: Controls, Selects, Enables, Resets
99/// - **Temporal (5)**: Triggers, Constrains, Follows, Precedes, Preserves
100/// - **Structural (4)**: Contains, Instantiates, ConnectsTo, BelongsToDomain
101/// - **Protocol (3)**: HandshakesWith, Acknowledges, Pipelines
102/// - **Specification (3)**: MutuallyExcludes, EventuallyFollows, AssumedBy
103#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
104pub enum HwRelation {
105    // Data Flow (5)
106    Drives,
107    DrivesRegistered { clock: String },
108    DataFlow,
109    Reads,
110    Writes,
111    // Control Flow (4)
112    Controls,
113    Selects,
114    Enables,
115    Resets,
116    // Temporal (5)
117    Triggers { delay: Option<u32> },
118    Constrains,
119    Follows { min: u32, max: u32 },
120    Precedes,
121    Preserves,
122    // Structural (4)
123    Contains,
124    Instantiates,
125    ConnectsTo,
126    BelongsToDomain { domain: String },
127    // Protocol (3)
128    HandshakesWith,
129    Acknowledges,
130    Pipelines { stages: u32 },
131    // Specification (3)
132    MutuallyExcludes,
133    EventuallyFollows,
134    AssumedBy,
135}
136
137// ═══════════════════════════════════════════════════════════════════════════
138// LEGACY TYPES (kept for backward compatibility with existing KG pipeline)
139// ═══════════════════════════════════════════════════════════════════════════
140
141/// Role of a hardware signal.
142#[derive(Debug, Clone, PartialEq, Eq)]
143pub enum SignalRole {
144    Input,
145    Output,
146    Internal,
147    Clock,
148}
149
150/// A signal node in the Knowledge Graph.
151#[derive(Debug, Clone)]
152pub struct KgSignal {
153    pub name: String,
154    pub width: u32,
155    pub role: SignalRole,
156}
157
158/// A temporal property node.
159#[derive(Debug, Clone)]
160pub struct KgProperty {
161    pub name: String,
162    pub property_type: String,
163    pub operator: String,
164}
165
166/// Relation type for KG edges.
167#[derive(Debug, Clone, PartialEq, Eq)]
168pub enum KgRelation {
169    Temporal,
170    Triggers,
171    Constrains,
172    TypeOf,
173}
174
175/// An edge in the Knowledge Graph.
176#[derive(Debug, Clone)]
177pub struct KgEdge {
178    pub from: String,
179    pub to: String,
180    pub relation: KgRelation,
181    pub property: Option<String>,
182}
183
184/// Hardware Knowledge Graph — extracted from Kripke-lowered FOL.
185#[derive(Debug, Clone)]
186pub struct HwKnowledgeGraph {
187    pub signals: Vec<KgSignal>,
188    pub properties: Vec<KgProperty>,
189    pub edges: Vec<KgEdge>,
190    /// Typed entities from the formal ontology (Sprint 0E).
191    pub entities: Vec<(String, HwEntityType)>,
192    /// Typed relations from the formal ontology (Sprint 0E).
193    pub typed_edges: Vec<(String, String, HwRelation)>,
194}
195
196impl HwKnowledgeGraph {
197    pub fn new() -> Self {
198        Self {
199            signals: Vec::new(),
200            properties: Vec::new(),
201            edges: Vec::new(),
202            entities: Vec::new(),
203            typed_edges: Vec::new(),
204        }
205    }
206
207    pub fn add_entity(&mut self, name: impl Into<String>, entity: HwEntityType) {
208        self.entities.push((name.into(), entity));
209    }
210
211    pub fn add_typed_edge(&mut self, from: impl Into<String>, to: impl Into<String>, relation: HwRelation) {
212        self.typed_edges.push((from.into(), to.into(), relation));
213    }
214
215    /// Serialize the KG to JSON for LLM consumption.
216    pub fn to_json(&self) -> String {
217        let mut out = String::from("{\n");
218
219        // Signals
220        out.push_str("  \"signals\": [\n");
221        for (i, sig) in self.signals.iter().enumerate() {
222            let role = match &sig.role {
223                SignalRole::Input => "input",
224                SignalRole::Output => "output",
225                SignalRole::Internal => "internal",
226                SignalRole::Clock => "clock",
227            };
228            out.push_str(&format!(
229                "    {{\"name\": \"{}\", \"width\": {}, \"role\": \"{}\"}}",
230                sig.name, sig.width, role
231            ));
232            if i < self.signals.len() - 1 {
233                out.push(',');
234            }
235            out.push('\n');
236        }
237        out.push_str("  ],\n");
238
239        // Properties
240        out.push_str("  \"properties\": [\n");
241        for (i, prop) in self.properties.iter().enumerate() {
242            out.push_str(&format!(
243                "    {{\"name\": \"{}\", \"type\": \"{}\", \"operator\": \"{}\"}}",
244                prop.name, prop.property_type, prop.operator
245            ));
246            if i < self.properties.len() - 1 {
247                out.push(',');
248            }
249            out.push('\n');
250        }
251        out.push_str("  ],\n");
252
253        // Edges
254        out.push_str("  \"edges\": [\n");
255        for (i, edge) in self.edges.iter().enumerate() {
256            let rel = match &edge.relation {
257                KgRelation::Temporal => "temporal",
258                KgRelation::Triggers => "triggers",
259                KgRelation::Constrains => "constrains",
260                KgRelation::TypeOf => "type_of",
261            };
262            let prop = edge
263                .property
264                .as_deref()
265                .map(|p| format!(", \"property\": \"{}\"", p))
266                .unwrap_or_default();
267            out.push_str(&format!(
268                "    {{\"from\": \"{}\", \"to\": \"{}\", \"relation\": \"{}\"{}}}",
269                edge.from, edge.to, rel, prop
270            ));
271            if i < self.edges.len() - 1 {
272                out.push(',');
273            }
274            out.push('\n');
275        }
276        out.push_str("  ],\n");
277
278        // Typed entities (Sprint 0E ontology)
279        out.push_str("  \"entities\": [\n");
280        for (i, (name, entity)) in self.entities.iter().enumerate() {
281            let entity_json = serde_json::to_string(entity).unwrap_or_else(|_| "{}".to_string());
282            out.push_str(&format!(
283                "    {{\"name\": \"{}\", \"entity_type\": {}}}",
284                name, entity_json
285            ));
286            if i < self.entities.len() - 1 {
287                out.push(',');
288            }
289            out.push('\n');
290        }
291        out.push_str("  ],\n");
292
293        // Typed edges (Sprint 0E ontology)
294        out.push_str("  \"typed_edges\": [\n");
295        for (i, (from, to, rel)) in self.typed_edges.iter().enumerate() {
296            let rel_json = serde_json::to_string(rel).unwrap_or_else(|_| "{}".to_string());
297            out.push_str(&format!(
298                "    {{\"from\": \"{}\", \"to\": \"{}\", \"relation\": {}}}",
299                from, to, rel_json
300            ));
301            if i < self.typed_edges.len() - 1 {
302                out.push(',');
303            }
304            out.push('\n');
305        }
306        out.push_str("  ]\n");
307
308        out.push('}');
309        out
310    }
311
312    pub fn add_signal(&mut self, name: impl Into<String>, width: u32, role: SignalRole) {
313        self.signals.push(KgSignal {
314            name: name.into(),
315            width,
316            role,
317        });
318    }
319
320    pub fn add_property(
321        &mut self,
322        name: impl Into<String>,
323        property_type: impl Into<String>,
324        operator: impl Into<String>,
325    ) {
326        self.properties.push(KgProperty {
327            name: name.into(),
328            property_type: property_type.into(),
329            operator: operator.into(),
330        });
331    }
332
333    pub fn add_edge(
334        &mut self,
335        from: impl Into<String>,
336        to: impl Into<String>,
337        relation: KgRelation,
338        property: Option<String>,
339    ) {
340        self.edges.push(KgEdge {
341            from: from.into(),
342            to: to.into(),
343            relation,
344            property,
345        });
346    }
347}
348
349impl Default for HwKnowledgeGraph {
350    fn default() -> Self {
351        Self::new()
352    }
353}
354
355/// Extract a Knowledge Graph from a Kripke-lowered LogicExpr AST.
356///
357/// Walks the AST to identify:
358/// - Signals: predicates that appear with world arguments
359/// - Properties: temporal patterns (∀w' Accessible_Temporal → ...) → safety, (∃w' Reachable → ...) → liveness
360/// - Edges: implication between predicates → Triggers, negated conjunction → Constrains
361///
362/// Signal roles are inferred from structural position:
363/// - Antecedent-only → Input
364/// - Consequent-only → Output
365/// - Both positions → Internal
366/// - Name contains "clk"/"clock" → Clock (overrides position)
367pub fn extract_from_kripke_ast<'a>(
368    expr: &'a crate::ast::logic::LogicExpr<'a>,
369    interner: &crate::Interner,
370) -> HwKnowledgeGraph {
371    use crate::ast::logic::{LogicExpr, QuantifierKind};
372    use std::collections::{HashSet, HashMap};
373
374    let mut kg = HwKnowledgeGraph::new();
375    let mut seen_signals: HashSet<String> = HashSet::new();
376    // Track signal positions for role inference
377    let mut antecedent_signals: HashSet<String> = HashSet::new();
378    let mut consequent_signals: HashSet<String> = HashSet::new();
379    // Track predicate names for property naming
380    let mut predicate_names: Vec<String> = Vec::new();
381
382    /// Position context for signal role inference.
383    #[derive(Clone, Copy, PartialEq)]
384    enum Position {
385        Neutral,
386        Antecedent,
387        Consequent,
388    }
389
390    fn walk<'a>(
391        expr: &'a LogicExpr<'a>,
392        interner: &crate::Interner,
393        kg: &mut HwKnowledgeGraph,
394        seen: &mut HashSet<String>,
395        antecedent: &mut HashSet<String>,
396        consequent: &mut HashSet<String>,
397        pred_names: &mut Vec<String>,
398        in_safety: bool,
399        in_liveness: bool,
400        position: Position,
401        impl_depth: u32,
402    ) {
403        match expr {
404            LogicExpr::Predicate { name, args, world } => {
405                let pred_name = interner.resolve(*name).to_string();
406
407                // Skip accessibility predicates themselves
408                if pred_name.contains("Accessible") || pred_name.contains("Reachable")
409                    || pred_name.contains("Next_Temporal")
410                {
411                    return;
412                }
413
414                // Collect predicate names for property naming
415                if !pred_name.starts_with('w') {
416                    pred_names.push(pred_name.clone());
417                }
418
419                // Predicates with world args are signals.
420                // Use both the predicate name and non-world arguments as signal names.
421                if world.is_some() {
422                    // The predicate name itself is a useful signal identifier
423                    let pred_lower = pred_name.to_lowercase();
424                    if !pred_lower.is_empty() {
425                        seen.insert(pred_name.clone());
426                        match position {
427                            Position::Antecedent => { antecedent.insert(pred_name.clone()); }
428                            Position::Consequent => { consequent.insert(pred_name.clone()); }
429                            Position::Neutral => {}
430                        }
431                    }
432
433                    for arg in args.iter() {
434                        if let crate::ast::logic::Term::Constant(sym)
435                            | crate::ast::logic::Term::Variable(sym) = arg
436                        {
437                            let arg_name = interner.resolve(*sym).to_string();
438                            // Skip world variables (w0, w1, ...) and single-letter bound vars
439                            if (!arg_name.starts_with('w') || arg_name.len() > 3)
440                                && arg_name.len() > 1
441                            {
442                                seen.insert(arg_name.clone());
443                                match position {
444                                    Position::Antecedent => { antecedent.insert(arg_name); }
445                                    Position::Consequent => { consequent.insert(arg_name); }
446                                    Position::Neutral => {}
447                                }
448                            }
449                        }
450                    }
451                }
452            }
453
454            // Universal quantifier with temporal accessibility → safety property
455            LogicExpr::Quantifier { kind: QuantifierKind::Universal, variable, body, .. } => {
456                let var_name = interner.resolve(*variable).to_string();
457                let is_temporal_world = var_name.starts_with('w');
458                walk(body, interner, kg, seen, antecedent, consequent, pred_names,
459                     in_safety || is_temporal_world, in_liveness, position, impl_depth);
460            }
461
462            // Existential quantifier with temporal reachability → liveness property
463            LogicExpr::Quantifier { kind: QuantifierKind::Existential, variable, body, .. } => {
464                let var_name = interner.resolve(*variable).to_string();
465                let is_temporal_world = var_name.starts_with('w');
466                walk(body, interner, kg, seen, antecedent, consequent, pred_names,
467                     in_safety, in_liveness || is_temporal_world, position, impl_depth);
468            }
469
470            // Counting quantifiers (AtMost, AtLeast, Cardinal, etc.) — recurse into body
471            LogicExpr::Quantifier { body, .. } => {
472                walk(body, interner, kg, seen, antecedent, consequent, pred_names,
473                     in_safety, in_liveness, position, impl_depth);
474            }
475
476            // Atom: bare symbol — treat as signal name
477            LogicExpr::Atom(sym) => {
478                let name = interner.resolve(*sym).to_string();
479                if !name.is_empty() && name.len() > 1 {
480                    seen.insert(name.clone());
481                    match position {
482                        Position::Antecedent => { antecedent.insert(name); }
483                        Position::Consequent => { consequent.insert(name); }
484                        Position::Neutral => {}
485                    }
486                }
487            }
488
489            // Binary connectives: walk both sides
490            LogicExpr::BinaryOp { left, right, op } => {
491                // TokenType::If = user-written conditional (provenance tag).
492                // TokenType::Implies = compiler-generated restriction/accessibility.
493                // Only user conditionals determine antecedent/consequent roles.
494                if matches!(op, crate::token::TokenType::If) {
495                    // User's explicit if...then — sets signal positions
496                    walk(left, interner, kg, seen, antecedent, consequent, pred_names,
497                         in_safety, in_liveness, Position::Antecedent, impl_depth);
498                    walk(right, interner, kg, seen, antecedent, consequent, pred_names,
499                         in_safety, in_liveness, Position::Consequent, impl_depth);
500
501                    // Triggers edge — use hw-aware extraction that prefers restriction
502                    // type names (Request, Grant) over verb predicates (Hold, Have)
503                    if let (Some(left_sig), Some(right_sig)) =
504                        (extract_hw_signal_name(left, interner), extract_hw_signal_name(right, interner))
505                    {
506                        if left_sig != right_sig {
507                            kg.add_edge(&left_sig, &right_sig, KgRelation::Triggers, None);
508
509                            // Sprint D: detect response pattern G(P → X(Q))
510                            let is_next = matches!(right,
511                                LogicExpr::Temporal { operator: crate::ast::logic::TemporalOperator::Next, .. }
512                            ) || is_kripke_next(right, interner);
513                            if is_next && in_safety {
514                                kg.add_entity(
515                                    format!("{}_responds_to_{}", right_sig, left_sig),
516                                    HwEntityType::ResponseProperty {
517                                        trigger: left_sig.clone(),
518                                        response: right_sig.clone(),
519                                        bound: Some(1),
520                                    },
521                                );
522                                kg.add_typed_edge(&left_sig, &right_sig, HwRelation::Triggers { delay: Some(1) });
523                            }
524
525                            // Sprint D: detect eventually-follows G(P → F(Q))
526                            let is_eventually = matches!(right,
527                                LogicExpr::Temporal { operator: crate::ast::logic::TemporalOperator::Eventually, .. }
528                            ) || is_kripke_eventually(right, interner);
529                            if is_eventually && in_safety {
530                                kg.add_typed_edge(&left_sig, &right_sig, HwRelation::EventuallyFollows);
531                            }
532                        }
533                    }
534                } else if matches!(op, crate::token::TokenType::Implies) {
535                    // Compiler-generated Implies — could be accessibility unwrap OR user conditional.
536                    // If left is an accessibility predicate, this is Kripke structure (skip edge extraction).
537                    // If left is a real predicate, this is a user conditional (extract edges).
538                    let left_is_accessibility = if let LogicExpr::Predicate { name, .. } = left {
539                        let pn = interner.resolve(*name).to_string();
540                        pn.contains("Accessible") || pn.contains("Reachable") || pn.contains("Next_Temporal")
541                    } else { false };
542
543                    if !left_is_accessibility {
544                        // User conditional lowered to Implies — extract edges but preserve
545                        // position as Neutral to avoid misclassifying restriction types
546                        // (Animal, Mammal) as antecedent/consequent signals.
547                        walk(left, interner, kg, seen, antecedent, consequent, pred_names,
548                             in_safety, in_liveness, position, impl_depth);
549                        walk(right, interner, kg, seen, antecedent, consequent, pred_names,
550                             in_safety, in_liveness, position, impl_depth);
551
552                        if let (Some(left_sig), Some(right_sig)) =
553                            (extract_hw_signal_name(left, interner), extract_hw_signal_name(right, interner))
554                        {
555                            if left_sig != right_sig {
556                                kg.add_edge(&left_sig, &right_sig, KgRelation::Triggers, None);
557
558                                // Detect response pattern G(P → X(Q))
559                                let is_next = matches!(right,
560                                    LogicExpr::Temporal { operator: crate::ast::logic::TemporalOperator::Next, .. }
561                                ) || is_kripke_next(right, interner);
562                                if is_next && in_safety {
563                                    kg.add_entity(
564                                        format!("{}_responds_to_{}", right_sig, left_sig),
565                                        HwEntityType::ResponseProperty {
566                                            trigger: left_sig.clone(),
567                                            response: right_sig.clone(),
568                                            bound: Some(1),
569                                        },
570                                    );
571                                    kg.add_typed_edge(&left_sig, &right_sig, HwRelation::Triggers { delay: Some(1) });
572                                }
573
574                                // Detect eventually-follows G(P → F(Q))
575                                let is_eventually = matches!(right,
576                                    LogicExpr::Temporal { operator: crate::ast::logic::TemporalOperator::Eventually, .. }
577                                ) || is_kripke_eventually(right, interner);
578                                if is_eventually && in_safety {
579                                    kg.add_typed_edge(&left_sig, &right_sig, HwRelation::EventuallyFollows);
580                                }
581                            }
582                        }
583                    } else {
584                        // Accessibility predicate — just walk both sides
585                        walk(left, interner, kg, seen, antecedent, consequent, pred_names,
586                             in_safety, in_liveness, position, impl_depth);
587                        walk(right, interner, kg, seen, antecedent, consequent, pred_names,
588                             in_safety, in_liveness, position, impl_depth);
589                    }
590                } else {
591                    // Other connectives — preserve position
592                    walk(left, interner, kg, seen, antecedent, consequent, pred_names,
593                         in_safety, in_liveness, position, impl_depth);
594                    walk(right, interner, kg, seen, antecedent, consequent, pred_names,
595                         in_safety, in_liveness, position, impl_depth);
596                }
597            }
598
599            // Negation
600            LogicExpr::UnaryOp { operand, .. } => {
601                walk(operand, interner, kg, seen, antecedent, consequent, pred_names,
602                     in_safety, in_liveness, position, impl_depth);
603
604                // Not(And(P, Q)) → Constrains edge + MutexProperty entity
605                if let LogicExpr::BinaryOp { left, right, op: crate::token::TokenType::And } = operand {
606                    if let (Some(left_sig), Some(right_sig)) =
607                        (extract_signal_name(left, interner), extract_signal_name(right, interner))
608                    {
609                        if left_sig != right_sig {
610                            kg.add_edge(&left_sig, &right_sig, KgRelation::Constrains, None);
611                            // Sprint D: add MutexProperty entity
612                            kg.add_entity(
613                                format!("mutex_{}_{}", left_sig, right_sig),
614                                HwEntityType::MutexProperty {
615                                    signals: vec![left_sig.clone(), right_sig.clone()],
616                                },
617                            );
618                        }
619                    }
620                }
621            }
622
623            // Temporal operators
624            LogicExpr::Temporal { body, .. } => {
625                walk(body, interner, kg, seen, antecedent, consequent, pred_names,
626                     in_safety, in_liveness, position, impl_depth);
627            }
628
629            LogicExpr::TemporalBinary { operator, left, right } => {
630                walk(left, interner, kg, seen, antecedent, consequent, pred_names,
631                     in_safety, in_liveness, position, impl_depth);
632                walk(right, interner, kg, seen, antecedent, consequent, pred_names,
633                     in_safety, in_liveness, position, impl_depth);
634
635                // Sprint D: Until → Precedes typed edge
636                if matches!(operator, crate::ast::logic::BinaryTemporalOp::Until) {
637                    if let (Some(left_sig), Some(right_sig)) =
638                        (extract_hw_signal_name(left, interner), extract_hw_signal_name(right, interner))
639                    {
640                        if left_sig != right_sig {
641                            kg.add_typed_edge(&left_sig, &right_sig, HwRelation::Precedes);
642                        }
643                    }
644                }
645            }
646
647            // Modal operators
648            LogicExpr::Modal { operand, .. } => {
649                walk(operand, interner, kg, seen, antecedent, consequent, pred_names,
650                     in_safety, in_liveness, position, impl_depth);
651            }
652
653            _ => {}
654        }
655    }
656
657    walk(expr, interner, &mut kg, &mut seen_signals,
658         &mut antecedent_signals, &mut consequent_signals, &mut predicate_names,
659         false, false, Position::Neutral, 0);
660
661    // Sprint D: detect handshake pairs from signal naming patterns
662    let signal_names: Vec<String> = seen_signals.iter().cloned().collect();
663    let handshake_pairs: Vec<(&str, &[&str])> = vec![
664        ("valid", &["ready", "rdy"]),
665        ("req", &["ack", "gnt", "grant"]),
666        ("request", &["acknowledge", "acknowledgment", "response", "grant"]),
667        ("cmd", &["resp", "response"]),
668        ("start", &["done", "complete"]),
669    ];
670    for (trigger_pattern, response_patterns) in &handshake_pairs {
671        let trigger_match = signal_names.iter().find(|s| s.to_lowercase().contains(trigger_pattern));
672        if let Some(trigger_sig) = trigger_match {
673            for resp_pattern in *response_patterns {
674                let resp_match = signal_names.iter().find(|s| {
675                    let lower = s.to_lowercase();
676                    lower.contains(resp_pattern) && *s != trigger_sig
677                });
678                if let Some(resp_sig) = resp_match {
679                    kg.add_entity(
680                        format!("handshake_{}_{}", trigger_sig, resp_sig),
681                        HwEntityType::Handshake {
682                            valid_signal: trigger_sig.clone(),
683                            ready_signal: resp_sig.clone(),
684                        },
685                    );
686                    kg.add_typed_edge(trigger_sig, resp_sig, HwRelation::HandshakesWith);
687                    break; // Only match the first response pattern
688                }
689            }
690        }
691    }
692
693    // Assign roles based on position inference + populate typed entities
694    for sig_name in &seen_signals {
695        let in_ante = antecedent_signals.contains(sig_name);
696        let in_cons = consequent_signals.contains(sig_name);
697        let name_lower = sig_name.to_lowercase();
698
699        let role = if name_lower.contains("clk") || name_lower.contains("clock") {
700            SignalRole::Clock
701        } else if in_ante && !in_cons {
702            SignalRole::Input
703        } else if in_cons && !in_ante {
704            SignalRole::Output
705        } else {
706            SignalRole::Internal
707        };
708
709        kg.add_signal(sig_name, 1, role.clone());
710
711        // Typed entity extraction: derive HwEntityType from signal role
712        if name_lower.contains("clk") || name_lower.contains("clock") {
713            kg.add_entity(sig_name, HwEntityType::Clock {
714                frequency: None,
715                domain: sig_name.clone(),
716            });
717        }
718    }
719
720    // Populate typed edges from legacy edges
721    let mut mutex_entities: Vec<(String, HwEntityType)> = Vec::new();
722    for edge in &kg.edges {
723        let typed_rel = match &edge.relation {
724            KgRelation::Triggers => HwRelation::Triggers { delay: None },
725            KgRelation::Constrains => HwRelation::Constrains,
726            KgRelation::Temporal => HwRelation::Triggers { delay: None },
727            KgRelation::TypeOf => HwRelation::Contains,
728        };
729        kg.typed_edges.push((edge.from.clone(), edge.to.clone(), typed_rel));
730
731        // Sprint D: Constrains edge → MutexProperty entity
732        if edge.relation == KgRelation::Constrains {
733            mutex_entities.push((
734                format!("mutex_{}_{}", edge.from, edge.to),
735                HwEntityType::MutexProperty {
736                    signals: vec![edge.from.clone(), edge.to.clone()],
737                },
738            ));
739        }
740    }
741    // Add mutex entities from Constrains edges (deferred to avoid borrow conflict)
742    let already_has_mutex = kg.entities.iter().any(|(_, e)| matches!(e, HwEntityType::MutexProperty { .. }));
743    if !already_has_mutex {
744        for (name, entity) in mutex_entities {
745            kg.add_entity(name, entity);
746        }
747    }
748
749    // Sprint D: detect mutex from signal naming patterns
750    // Signals with same base and different suffixes (e.g., grant_a, grant_b) → mutex
751    if !kg.entities.iter().any(|(_, e)| matches!(e, HwEntityType::MutexProperty { .. })) {
752        let sig_names: Vec<String> = kg.signals.iter().map(|s| s.name.clone()).collect();
753        let mut mutex_groups: std::collections::HashMap<String, Vec<String>> = std::collections::HashMap::new();
754        for name in &sig_names {
755            let lower = name.to_lowercase();
756            // Check for patterns like "grant_a" → base "grant"
757            if let Some(underscore_pos) = lower.rfind('_') {
758                let suffix = &lower[underscore_pos+1..];
759                if suffix.len() <= 2 { // Single char/digit suffix
760                    let base = lower[..underscore_pos].to_string();
761                    mutex_groups.entry(base).or_default().push(name.clone());
762                }
763            }
764        }
765        for (base, group) in &mutex_groups {
766            if group.len() >= 2 && (base.contains("grant") || base.contains("sel") || base.contains("enable")) {
767                kg.add_entity(
768                    format!("mutex_{}", base),
769                    HwEntityType::MutexProperty { signals: group.clone() },
770                );
771                // Add Constrains edges between pairs
772                for i in 0..group.len() {
773                    for j in (i+1)..group.len() {
774                        kg.add_edge(&group[i], &group[j], KgRelation::Constrains, None);
775                    }
776                }
777            }
778        }
779    }
780
781    // Determine property type and name from the top-level structure
782    // Use predicate names for descriptive property naming
783    let prop_name = predicate_names.iter()
784        .find(|n| {
785            let lower = n.to_lowercase();
786            !lower.contains("accessible") && !lower.contains("reachable")
787                && !lower.contains("next_temporal") && lower != "and" && lower != "or"
788        })
789        .cloned();
790
791    // Build a formula description from predicate names
792    let formula_desc = predicate_names.iter()
793        .filter(|n| {
794            let lower = n.to_lowercase();
795            !lower.contains("accessible") && !lower.contains("reachable")
796                && !lower.contains("next_temporal")
797        })
798        .cloned()
799        .collect::<Vec<_>>()
800        .join(", ");
801
802    match expr {
803        LogicExpr::Temporal { operator: crate::ast::logic::TemporalOperator::Always, .. } => {
804            let name = prop_name.unwrap_or_else(|| "Safety".to_string());
805            kg.add_property(name.clone(), "safety", "G(...)");
806            kg.add_entity(&name, HwEntityType::SafetyProperty {
807                formula: format!("G({})", formula_desc),
808            });
809        }
810        LogicExpr::Temporal { operator: crate::ast::logic::TemporalOperator::Eventually, .. } => {
811            let name = prop_name.unwrap_or_else(|| "Liveness".to_string());
812            kg.add_property(name.clone(), "liveness", "F(...)");
813            kg.add_entity(&name, HwEntityType::LivenessProperty {
814                formula: format!("F({})", formula_desc),
815            });
816        }
817        LogicExpr::Quantifier { kind: QuantifierKind::Universal, .. } => {
818            // Kripke-lowered G produces ∀w'(...)
819            let name = prop_name.unwrap_or_else(|| "Safety".to_string());
820            kg.add_property(name.clone(), "safety", "G(...)");
821            kg.add_entity(&name, HwEntityType::SafetyProperty {
822                formula: format!("G({})", formula_desc),
823            });
824        }
825        LogicExpr::Quantifier { kind: QuantifierKind::Existential, .. } => {
826            let name = prop_name.unwrap_or_else(|| "Liveness".to_string());
827            kg.add_property(name.clone(), "liveness", "F(...)");
828            kg.add_entity(&name, HwEntityType::LivenessProperty {
829                formula: format!("F({})", formula_desc),
830            });
831        }
832        _ => {}
833    }
834
835    kg
836}
837
838/// Check if an expression contains a Kripke-lowered Next: ∀w'(Next_Temporal(w,w') → P(w'))
839/// Recurses through restriction quantifiers (non-world variables) to find the temporal structure.
840fn is_kripke_next<'a>(
841    expr: &'a crate::ast::logic::LogicExpr<'a>,
842    interner: &crate::Interner,
843) -> bool {
844    use crate::ast::logic::{LogicExpr, QuantifierKind};
845    match expr {
846        LogicExpr::Quantifier { kind: QuantifierKind::Universal, body, variable, .. } => {
847            let var_name = interner.resolve(*variable).to_string();
848            if var_name.starts_with('w') {
849                // World quantifier — check for Next_Temporal in the body
850                if let LogicExpr::BinaryOp { left, op, .. } = body {
851                    if matches!(op, crate::token::TokenType::Implies | crate::token::TokenType::If) {
852                        if let LogicExpr::Predicate { name, .. } = left {
853                            let pred_name = interner.resolve(*name).to_string();
854                            if pred_name == "Next_Temporal" {
855                                return true;
856                            }
857                        }
858                    }
859                }
860            }
861            // Non-world quantifier (restriction) — recurse into body
862            is_kripke_next(body, interner)
863        }
864        LogicExpr::BinaryOp { right, op, .. } if matches!(op, crate::token::TokenType::Implies) => {
865            // Restriction: ∀x(Type(x) → Body) — recurse into right side
866            is_kripke_next(right, interner)
867        }
868        LogicExpr::Temporal { operator: crate::ast::logic::TemporalOperator::Next, .. } => true,
869        _ => false,
870    }
871}
872
873/// Check if an expression contains a Kripke-lowered Eventually: ∃w'(Reachable_Temporal(w,w') ∧ P(w'))
874/// Recurses through restriction quantifiers to find the temporal structure.
875fn is_kripke_eventually<'a>(
876    expr: &'a crate::ast::logic::LogicExpr<'a>,
877    interner: &crate::Interner,
878) -> bool {
879    use crate::ast::logic::{LogicExpr, QuantifierKind};
880    match expr {
881        LogicExpr::Quantifier { kind: QuantifierKind::Existential, body, variable, .. } => {
882            let var_name = interner.resolve(*variable).to_string();
883            if var_name.starts_with('w') {
884                if let LogicExpr::BinaryOp { left, op: crate::token::TokenType::And, .. } = body {
885                    if let LogicExpr::Predicate { name, .. } = left {
886                        let pred_name = interner.resolve(*name).to_string();
887                        if pred_name == "Reachable_Temporal" {
888                            return true;
889                        }
890                    }
891                }
892            }
893            // Recurse into body for restriction quantifiers
894            is_kripke_eventually(body, interner)
895        }
896        LogicExpr::Quantifier { kind: QuantifierKind::Universal, body, .. } => {
897            // Universal restriction quantifier — recurse
898            is_kripke_eventually(body, interner)
899        }
900        LogicExpr::BinaryOp { right, op, .. } if matches!(op, crate::token::TokenType::Implies) => {
901            is_kripke_eventually(right, interner)
902        }
903        LogicExpr::Temporal { operator: crate::ast::logic::TemporalOperator::Eventually, .. } => true,
904        _ => false,
905    }
906}
907
908/// Extract a hardware signal name from Kripke-lowered AST, preferring
909/// restriction predicate names (the TYPE, e.g., "Request") over verb predicates
910/// (e.g., "Hold/Have"). Used for edge extraction in the walk handler.
911fn extract_hw_signal_name<'a>(
912    expr: &'a crate::ast::logic::LogicExpr<'a>,
913    interner: &crate::Interner,
914) -> Option<String> {
915    use crate::ast::logic::LogicExpr;
916    match expr {
917        // Quantifier with restriction: ∀x(Request(x) → Hold(x,w')) → "Request"
918        LogicExpr::Quantifier { body, .. } => extract_hw_signal_name(body, interner),
919        LogicExpr::BinaryOp { left, right, op } => {
920            if matches!(op, crate::token::TokenType::Implies) {
921                // Restriction pattern: left is the type predicate, prefer it
922                let left_name = extract_signal_name(left, interner);
923                if left_name.is_some() {
924                    return left_name;
925                }
926                extract_hw_signal_name(right, interner)
927            } else {
928                extract_hw_signal_name(left, interner)
929                    .or_else(|| extract_hw_signal_name(right, interner))
930            }
931        }
932        // Fall through to regular extraction
933        _ => extract_signal_name(expr, interner),
934    }
935}
936
937/// Try to extract a signal name from an expression by recursing into
938/// quantifiers, binary ops, and event structures to find the first
939/// meaningful predicate argument.
940fn extract_signal_name<'a>(
941    expr: &'a crate::ast::logic::LogicExpr<'a>,
942    interner: &crate::Interner,
943) -> Option<String> {
944    use crate::ast::logic::LogicExpr;
945    match expr {
946        LogicExpr::Predicate { name, args, .. } => {
947            let pred_name = interner.resolve(*name).to_string();
948            // Skip accessibility/meta predicates
949            if pred_name.contains("Accessible") || pred_name.contains("Reachable")
950                || pred_name.contains("Next_Temporal")
951                || pred_name == "Agent" || pred_name == "Theme"
952            {
953                return None;
954            }
955            // Prefer non-world arguments (signal names) over predicate name (verb)
956            let arg_name = args.iter().find_map(|arg| match arg {
957                crate::ast::logic::Term::Constant(sym)
958                | crate::ast::logic::Term::Variable(sym) => {
959                    let aname = interner.resolve(*sym).to_string();
960                    if (!aname.starts_with('w') || aname.len() > 3) && aname.len() > 1 {
961                        Some(aname)
962                    } else {
963                        None
964                    }
965                }
966                _ => None,
967            });
968            if let Some(an) = arg_name {
969                return Some(an);
970            }
971            // Fall back to predicate name if no useful arguments
972            if pred_name.len() > 1 && pred_name.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) {
973                return Some(pred_name);
974            }
975            None
976        }
977        // Recurse into quantifiers to find the core predicate
978        LogicExpr::Quantifier { body, .. } => extract_signal_name(body, interner),
979        // Recurse into binary ops — for restrictions (Implies), look at the right side (body);
980        // for conjunctions, try both sides
981        LogicExpr::BinaryOp { left, right, op } => {
982            if matches!(op, crate::token::TokenType::Implies) {
983                // Restriction: ∀x(Type(x) → Body(x)) — signal is in the body
984                extract_signal_name(right, interner)
985                    .or_else(|| extract_signal_name(left, interner))
986            } else {
987                // Try left first, then right
988                extract_signal_name(left, interner)
989                    .or_else(|| extract_signal_name(right, interner))
990            }
991        }
992        // Recurse into existentials (event structures: ∃e(Run(e) ∧ Agent(e,x)))
993        LogicExpr::NeoEvent(data) => {
994            let verb_name = interner.resolve(data.verb).to_string();
995            Some(verb_name)
996        }
997        _ => None,
998    }
999}