Skip to main content

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            // Binary connectives: walk both sides
471            LogicExpr::BinaryOp { left, right, op } => {
472                // TokenType::If = user-written conditional (provenance tag).
473                // TokenType::Implies = compiler-generated restriction/accessibility.
474                // Only user conditionals determine antecedent/consequent roles.
475                if matches!(op, crate::token::TokenType::If) {
476                    // User's explicit if...then — sets signal positions
477                    walk(left, interner, kg, seen, antecedent, consequent, pred_names,
478                         in_safety, in_liveness, Position::Antecedent, impl_depth);
479                    walk(right, interner, kg, seen, antecedent, consequent, pred_names,
480                         in_safety, in_liveness, Position::Consequent, impl_depth);
481
482                    // Triggers edge — use hw-aware extraction that prefers restriction
483                    // type names (Request, Grant) over verb predicates (Hold, Have)
484                    if let (Some(left_sig), Some(right_sig)) =
485                        (extract_hw_signal_name(left, interner), extract_hw_signal_name(right, interner))
486                    {
487                        if left_sig != right_sig {
488                            kg.add_edge(&left_sig, &right_sig, KgRelation::Triggers, None);
489
490                            // Sprint D: detect response pattern G(P → X(Q))
491                            let is_next = matches!(right,
492                                LogicExpr::Temporal { operator: crate::ast::logic::TemporalOperator::Next, .. }
493                            ) || is_kripke_next(right, interner);
494                            if is_next && in_safety {
495                                kg.add_entity(
496                                    format!("{}_responds_to_{}", right_sig, left_sig),
497                                    HwEntityType::ResponseProperty {
498                                        trigger: left_sig.clone(),
499                                        response: right_sig.clone(),
500                                        bound: Some(1),
501                                    },
502                                );
503                                kg.add_typed_edge(&left_sig, &right_sig, HwRelation::Triggers { delay: Some(1) });
504                            }
505
506                            // Sprint D: detect eventually-follows G(P → F(Q))
507                            let is_eventually = matches!(right,
508                                LogicExpr::Temporal { operator: crate::ast::logic::TemporalOperator::Eventually, .. }
509                            ) || is_kripke_eventually(right, interner);
510                            if is_eventually && in_safety {
511                                kg.add_typed_edge(&left_sig, &right_sig, HwRelation::EventuallyFollows);
512                            }
513                        }
514                    }
515                } else if matches!(op, crate::token::TokenType::Implies) {
516                    // Compiler-generated Implies — could be accessibility unwrap OR user conditional.
517                    // If left is an accessibility predicate, this is Kripke structure (skip edge extraction).
518                    // If left is a real predicate, this is a user conditional (extract edges).
519                    let left_is_accessibility = if let LogicExpr::Predicate { name, .. } = left {
520                        let pn = interner.resolve(*name).to_string();
521                        pn.contains("Accessible") || pn.contains("Reachable") || pn.contains("Next_Temporal")
522                    } else { false };
523
524                    if !left_is_accessibility {
525                        // User conditional lowered to Implies — extract edges but preserve
526                        // position as Neutral to avoid misclassifying restriction types
527                        // (Animal, Mammal) as antecedent/consequent signals.
528                        walk(left, interner, kg, seen, antecedent, consequent, pred_names,
529                             in_safety, in_liveness, position, impl_depth);
530                        walk(right, interner, kg, seen, antecedent, consequent, pred_names,
531                             in_safety, in_liveness, position, impl_depth);
532
533                        if let (Some(left_sig), Some(right_sig)) =
534                            (extract_hw_signal_name(left, interner), extract_hw_signal_name(right, interner))
535                        {
536                            if left_sig != right_sig {
537                                kg.add_edge(&left_sig, &right_sig, KgRelation::Triggers, None);
538
539                                // Detect response pattern G(P → X(Q))
540                                let is_next = matches!(right,
541                                    LogicExpr::Temporal { operator: crate::ast::logic::TemporalOperator::Next, .. }
542                                ) || is_kripke_next(right, interner);
543                                if is_next && in_safety {
544                                    kg.add_entity(
545                                        format!("{}_responds_to_{}", right_sig, left_sig),
546                                        HwEntityType::ResponseProperty {
547                                            trigger: left_sig.clone(),
548                                            response: right_sig.clone(),
549                                            bound: Some(1),
550                                        },
551                                    );
552                                    kg.add_typed_edge(&left_sig, &right_sig, HwRelation::Triggers { delay: Some(1) });
553                                }
554
555                                // Detect eventually-follows G(P → F(Q))
556                                let is_eventually = matches!(right,
557                                    LogicExpr::Temporal { operator: crate::ast::logic::TemporalOperator::Eventually, .. }
558                                ) || is_kripke_eventually(right, interner);
559                                if is_eventually && in_safety {
560                                    kg.add_typed_edge(&left_sig, &right_sig, HwRelation::EventuallyFollows);
561                                }
562                            }
563                        }
564                    } else {
565                        // Accessibility predicate — just walk both sides
566                        walk(left, interner, kg, seen, antecedent, consequent, pred_names,
567                             in_safety, in_liveness, position, impl_depth);
568                        walk(right, interner, kg, seen, antecedent, consequent, pred_names,
569                             in_safety, in_liveness, position, impl_depth);
570                    }
571                } else {
572                    // Other connectives — preserve position
573                    walk(left, interner, kg, seen, antecedent, consequent, pred_names,
574                         in_safety, in_liveness, position, impl_depth);
575                    walk(right, interner, kg, seen, antecedent, consequent, pred_names,
576                         in_safety, in_liveness, position, impl_depth);
577                }
578            }
579
580            // Negation
581            LogicExpr::UnaryOp { operand, .. } => {
582                walk(operand, interner, kg, seen, antecedent, consequent, pred_names,
583                     in_safety, in_liveness, position, impl_depth);
584
585                // Not(And(P, Q)) → Constrains edge + MutexProperty entity
586                if let LogicExpr::BinaryOp { left, right, op: crate::token::TokenType::And } = operand {
587                    if let (Some(left_sig), Some(right_sig)) =
588                        (extract_signal_name(left, interner), extract_signal_name(right, interner))
589                    {
590                        if left_sig != right_sig {
591                            kg.add_edge(&left_sig, &right_sig, KgRelation::Constrains, None);
592                            // Sprint D: add MutexProperty entity
593                            kg.add_entity(
594                                format!("mutex_{}_{}", left_sig, right_sig),
595                                HwEntityType::MutexProperty {
596                                    signals: vec![left_sig.clone(), right_sig.clone()],
597                                },
598                            );
599                        }
600                    }
601                }
602            }
603
604            // Temporal operators
605            LogicExpr::Temporal { body, .. } => {
606                walk(body, interner, kg, seen, antecedent, consequent, pred_names,
607                     in_safety, in_liveness, position, impl_depth);
608            }
609
610            LogicExpr::TemporalBinary { operator, left, right } => {
611                walk(left, interner, kg, seen, antecedent, consequent, pred_names,
612                     in_safety, in_liveness, position, impl_depth);
613                walk(right, interner, kg, seen, antecedent, consequent, pred_names,
614                     in_safety, in_liveness, position, impl_depth);
615
616                // Sprint D: Until → Precedes typed edge
617                if matches!(operator, crate::ast::logic::BinaryTemporalOp::Until) {
618                    if let (Some(left_sig), Some(right_sig)) =
619                        (extract_hw_signal_name(left, interner), extract_hw_signal_name(right, interner))
620                    {
621                        if left_sig != right_sig {
622                            kg.add_typed_edge(&left_sig, &right_sig, HwRelation::Precedes);
623                        }
624                    }
625                }
626            }
627
628            // Modal operators
629            LogicExpr::Modal { operand, .. } => {
630                walk(operand, interner, kg, seen, antecedent, consequent, pred_names,
631                     in_safety, in_liveness, position, impl_depth);
632            }
633
634            _ => {}
635        }
636    }
637
638    walk(expr, interner, &mut kg, &mut seen_signals,
639         &mut antecedent_signals, &mut consequent_signals, &mut predicate_names,
640         false, false, Position::Neutral, 0);
641
642    // Sprint D: detect handshake pairs from signal naming patterns
643    let signal_names: Vec<String> = seen_signals.iter().cloned().collect();
644    let handshake_pairs: Vec<(&str, &[&str])> = vec![
645        ("valid", &["ready", "rdy"]),
646        ("req", &["ack", "gnt", "grant"]),
647        ("request", &["acknowledge", "acknowledgment", "response", "grant"]),
648        ("cmd", &["resp", "response"]),
649        ("start", &["done", "complete"]),
650    ];
651    for (trigger_pattern, response_patterns) in &handshake_pairs {
652        let trigger_match = signal_names.iter().find(|s| s.to_lowercase().contains(trigger_pattern));
653        if let Some(trigger_sig) = trigger_match {
654            for resp_pattern in *response_patterns {
655                let resp_match = signal_names.iter().find(|s| {
656                    let lower = s.to_lowercase();
657                    lower.contains(resp_pattern) && *s != trigger_sig
658                });
659                if let Some(resp_sig) = resp_match {
660                    kg.add_entity(
661                        format!("handshake_{}_{}", trigger_sig, resp_sig),
662                        HwEntityType::Handshake {
663                            valid_signal: trigger_sig.clone(),
664                            ready_signal: resp_sig.clone(),
665                        },
666                    );
667                    kg.add_typed_edge(trigger_sig, resp_sig, HwRelation::HandshakesWith);
668                    break; // Only match the first response pattern
669                }
670            }
671        }
672    }
673
674    // Assign roles based on position inference + populate typed entities
675    for sig_name in &seen_signals {
676        let in_ante = antecedent_signals.contains(sig_name);
677        let in_cons = consequent_signals.contains(sig_name);
678        let name_lower = sig_name.to_lowercase();
679
680        let role = if name_lower.contains("clk") || name_lower.contains("clock") {
681            SignalRole::Clock
682        } else if in_ante && !in_cons {
683            SignalRole::Input
684        } else if in_cons && !in_ante {
685            SignalRole::Output
686        } else {
687            SignalRole::Internal
688        };
689
690        kg.add_signal(sig_name, 1, role.clone());
691
692        // Typed entity extraction: derive HwEntityType from signal role
693        if name_lower.contains("clk") || name_lower.contains("clock") {
694            kg.add_entity(sig_name, HwEntityType::Clock {
695                frequency: None,
696                domain: sig_name.clone(),
697            });
698        }
699    }
700
701    // Populate typed edges from legacy edges
702    let mut mutex_entities: Vec<(String, HwEntityType)> = Vec::new();
703    for edge in &kg.edges {
704        let typed_rel = match &edge.relation {
705            KgRelation::Triggers => HwRelation::Triggers { delay: None },
706            KgRelation::Constrains => HwRelation::Constrains,
707            KgRelation::Temporal => HwRelation::Triggers { delay: None },
708            KgRelation::TypeOf => HwRelation::Contains,
709        };
710        kg.typed_edges.push((edge.from.clone(), edge.to.clone(), typed_rel));
711
712        // Sprint D: Constrains edge → MutexProperty entity
713        if edge.relation == KgRelation::Constrains {
714            mutex_entities.push((
715                format!("mutex_{}_{}", edge.from, edge.to),
716                HwEntityType::MutexProperty {
717                    signals: vec![edge.from.clone(), edge.to.clone()],
718                },
719            ));
720        }
721    }
722    // Add mutex entities from Constrains edges (deferred to avoid borrow conflict)
723    let already_has_mutex = kg.entities.iter().any(|(_, e)| matches!(e, HwEntityType::MutexProperty { .. }));
724    if !already_has_mutex {
725        for (name, entity) in mutex_entities {
726            kg.add_entity(name, entity);
727        }
728    }
729
730    // Sprint D: detect mutex from signal naming patterns
731    // Signals with same base and different suffixes (e.g., grant_a, grant_b) → mutex
732    if !kg.entities.iter().any(|(_, e)| matches!(e, HwEntityType::MutexProperty { .. })) {
733        let sig_names: Vec<String> = kg.signals.iter().map(|s| s.name.clone()).collect();
734        let mut mutex_groups: std::collections::HashMap<String, Vec<String>> = std::collections::HashMap::new();
735        for name in &sig_names {
736            let lower = name.to_lowercase();
737            // Check for patterns like "grant_a" → base "grant"
738            if let Some(underscore_pos) = lower.rfind('_') {
739                let suffix = &lower[underscore_pos+1..];
740                if suffix.len() <= 2 { // Single char/digit suffix
741                    let base = lower[..underscore_pos].to_string();
742                    mutex_groups.entry(base).or_default().push(name.clone());
743                }
744            }
745        }
746        for (base, group) in &mutex_groups {
747            if group.len() >= 2 && (base.contains("grant") || base.contains("sel") || base.contains("enable")) {
748                kg.add_entity(
749                    format!("mutex_{}", base),
750                    HwEntityType::MutexProperty { signals: group.clone() },
751                );
752                // Add Constrains edges between pairs
753                for i in 0..group.len() {
754                    for j in (i+1)..group.len() {
755                        kg.add_edge(&group[i], &group[j], KgRelation::Constrains, None);
756                    }
757                }
758            }
759        }
760    }
761
762    // Determine property type and name from the top-level structure
763    // Use predicate names for descriptive property naming
764    let prop_name = predicate_names.iter()
765        .find(|n| {
766            let lower = n.to_lowercase();
767            !lower.contains("accessible") && !lower.contains("reachable")
768                && !lower.contains("next_temporal") && lower != "and" && lower != "or"
769        })
770        .cloned();
771
772    // Build a formula description from predicate names
773    let formula_desc = predicate_names.iter()
774        .filter(|n| {
775            let lower = n.to_lowercase();
776            !lower.contains("accessible") && !lower.contains("reachable")
777                && !lower.contains("next_temporal")
778        })
779        .cloned()
780        .collect::<Vec<_>>()
781        .join(", ");
782
783    match expr {
784        LogicExpr::Temporal { operator: crate::ast::logic::TemporalOperator::Always, .. } => {
785            let name = prop_name.unwrap_or_else(|| "Safety".to_string());
786            kg.add_property(name.clone(), "safety", "G(...)");
787            kg.add_entity(&name, HwEntityType::SafetyProperty {
788                formula: format!("G({})", formula_desc),
789            });
790        }
791        LogicExpr::Temporal { operator: crate::ast::logic::TemporalOperator::Eventually, .. } => {
792            let name = prop_name.unwrap_or_else(|| "Liveness".to_string());
793            kg.add_property(name.clone(), "liveness", "F(...)");
794            kg.add_entity(&name, HwEntityType::LivenessProperty {
795                formula: format!("F({})", formula_desc),
796            });
797        }
798        LogicExpr::Quantifier { kind: QuantifierKind::Universal, .. } => {
799            // Kripke-lowered G produces ∀w'(...)
800            let name = prop_name.unwrap_or_else(|| "Safety".to_string());
801            kg.add_property(name.clone(), "safety", "G(...)");
802            kg.add_entity(&name, HwEntityType::SafetyProperty {
803                formula: format!("G({})", formula_desc),
804            });
805        }
806        LogicExpr::Quantifier { kind: QuantifierKind::Existential, .. } => {
807            let name = prop_name.unwrap_or_else(|| "Liveness".to_string());
808            kg.add_property(name.clone(), "liveness", "F(...)");
809            kg.add_entity(&name, HwEntityType::LivenessProperty {
810                formula: format!("F({})", formula_desc),
811            });
812        }
813        _ => {}
814    }
815
816    kg
817}
818
819/// Check if an expression contains a Kripke-lowered Next: ∀w'(Next_Temporal(w,w') → P(w'))
820/// Recurses through restriction quantifiers (non-world variables) to find the temporal structure.
821fn is_kripke_next<'a>(
822    expr: &'a crate::ast::logic::LogicExpr<'a>,
823    interner: &crate::Interner,
824) -> bool {
825    use crate::ast::logic::{LogicExpr, QuantifierKind};
826    match expr {
827        LogicExpr::Quantifier { kind: QuantifierKind::Universal, body, variable, .. } => {
828            let var_name = interner.resolve(*variable).to_string();
829            if var_name.starts_with('w') {
830                // World quantifier — check for Next_Temporal in the body
831                if let LogicExpr::BinaryOp { left, op, .. } = body {
832                    if matches!(op, crate::token::TokenType::Implies | crate::token::TokenType::If) {
833                        if let LogicExpr::Predicate { name, .. } = left {
834                            let pred_name = interner.resolve(*name).to_string();
835                            if pred_name == "Next_Temporal" {
836                                return true;
837                            }
838                        }
839                    }
840                }
841            }
842            // Non-world quantifier (restriction) — recurse into body
843            is_kripke_next(body, interner)
844        }
845        LogicExpr::BinaryOp { right, op, .. } if matches!(op, crate::token::TokenType::Implies) => {
846            // Restriction: ∀x(Type(x) → Body) — recurse into right side
847            is_kripke_next(right, interner)
848        }
849        LogicExpr::Temporal { operator: crate::ast::logic::TemporalOperator::Next, .. } => true,
850        _ => false,
851    }
852}
853
854/// Check if an expression contains a Kripke-lowered Eventually: ∃w'(Reachable_Temporal(w,w') ∧ P(w'))
855/// Recurses through restriction quantifiers to find the temporal structure.
856fn is_kripke_eventually<'a>(
857    expr: &'a crate::ast::logic::LogicExpr<'a>,
858    interner: &crate::Interner,
859) -> bool {
860    use crate::ast::logic::{LogicExpr, QuantifierKind};
861    match expr {
862        LogicExpr::Quantifier { kind: QuantifierKind::Existential, body, variable, .. } => {
863            let var_name = interner.resolve(*variable).to_string();
864            if var_name.starts_with('w') {
865                if let LogicExpr::BinaryOp { left, op: crate::token::TokenType::And, .. } = body {
866                    if let LogicExpr::Predicate { name, .. } = left {
867                        let pred_name = interner.resolve(*name).to_string();
868                        if pred_name == "Reachable_Temporal" {
869                            return true;
870                        }
871                    }
872                }
873            }
874            // Recurse into body for restriction quantifiers
875            is_kripke_eventually(body, interner)
876        }
877        LogicExpr::Quantifier { kind: QuantifierKind::Universal, body, .. } => {
878            // Universal restriction quantifier — recurse
879            is_kripke_eventually(body, interner)
880        }
881        LogicExpr::BinaryOp { right, op, .. } if matches!(op, crate::token::TokenType::Implies) => {
882            is_kripke_eventually(right, interner)
883        }
884        LogicExpr::Temporal { operator: crate::ast::logic::TemporalOperator::Eventually, .. } => true,
885        _ => false,
886    }
887}
888
889/// Extract a hardware signal name from Kripke-lowered AST, preferring
890/// restriction predicate names (the TYPE, e.g., "Request") over verb predicates
891/// (e.g., "Hold/Have"). Used for edge extraction in the walk handler.
892fn extract_hw_signal_name<'a>(
893    expr: &'a crate::ast::logic::LogicExpr<'a>,
894    interner: &crate::Interner,
895) -> Option<String> {
896    use crate::ast::logic::LogicExpr;
897    match expr {
898        // Quantifier with restriction: ∀x(Request(x) → Hold(x,w')) → "Request"
899        LogicExpr::Quantifier { body, .. } => extract_hw_signal_name(body, interner),
900        LogicExpr::BinaryOp { left, right, op } => {
901            if matches!(op, crate::token::TokenType::Implies) {
902                // Restriction pattern: left is the type predicate, prefer it
903                let left_name = extract_signal_name(left, interner);
904                if left_name.is_some() {
905                    return left_name;
906                }
907                extract_hw_signal_name(right, interner)
908            } else {
909                extract_hw_signal_name(left, interner)
910                    .or_else(|| extract_hw_signal_name(right, interner))
911            }
912        }
913        // Fall through to regular extraction
914        _ => extract_signal_name(expr, interner),
915    }
916}
917
918/// Try to extract a signal name from an expression by recursing into
919/// quantifiers, binary ops, and event structures to find the first
920/// meaningful predicate argument.
921fn extract_signal_name<'a>(
922    expr: &'a crate::ast::logic::LogicExpr<'a>,
923    interner: &crate::Interner,
924) -> Option<String> {
925    use crate::ast::logic::LogicExpr;
926    match expr {
927        LogicExpr::Predicate { name, args, .. } => {
928            let pred_name = interner.resolve(*name).to_string();
929            // Skip accessibility/meta predicates
930            if pred_name.contains("Accessible") || pred_name.contains("Reachable")
931                || pred_name.contains("Next_Temporal")
932                || pred_name == "Agent" || pred_name == "Theme"
933            {
934                return None;
935            }
936            // Prefer non-world arguments (signal names) over predicate name (verb)
937            let arg_name = args.iter().find_map(|arg| match arg {
938                crate::ast::logic::Term::Constant(sym)
939                | crate::ast::logic::Term::Variable(sym) => {
940                    let aname = interner.resolve(*sym).to_string();
941                    if (!aname.starts_with('w') || aname.len() > 3) && aname.len() > 1 {
942                        Some(aname)
943                    } else {
944                        None
945                    }
946                }
947                _ => None,
948            });
949            if let Some(an) = arg_name {
950                return Some(an);
951            }
952            // Fall back to predicate name if no useful arguments
953            if pred_name.len() > 1 && pred_name.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) {
954                return Some(pred_name);
955            }
956            None
957        }
958        // Recurse into quantifiers to find the core predicate
959        LogicExpr::Quantifier { body, .. } => extract_signal_name(body, interner),
960        // Recurse into binary ops — for restrictions (Implies), look at the right side (body);
961        // for conjunctions, try both sides
962        LogicExpr::BinaryOp { left, right, op } => {
963            if matches!(op, crate::token::TokenType::Implies) {
964                // Restriction: ∀x(Type(x) → Body(x)) — signal is in the body
965                extract_signal_name(right, interner)
966                    .or_else(|| extract_signal_name(left, interner))
967            } else {
968                // Try left first, then right
969                extract_signal_name(left, interner)
970                    .or_else(|| extract_signal_name(right, interner))
971            }
972        }
973        // Recurse into existentials (event structures: ∃e(Run(e) ∧ Agent(e,x)))
974        LogicExpr::NeoEvent(data) => {
975            let verb_name = interner.resolve(data.verb).to_string();
976            Some(verb_name)
977        }
978        _ => None,
979    }
980}