Skip to main content

logicaffeine_compile/codegen_sva/
rtl_extract.rs

1//! Verilog Declaration Parser
2//!
3//! Extracts module structure from Verilog/SystemVerilog source:
4//! module name, ports (direction, width), internal signals, parameters,
5//! and clock detection from `always @(posedge/negedge)` blocks.
6//!
7//! This is NOT a full Verilog parser — it handles declaration-level
8//! extraction for hardware verification KG construction.
9
10use std::collections::HashSet;
11
12/// Port direction.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum PortDirection {
15    Input,
16    Output,
17    Inout,
18}
19
20/// Signal type.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum SignalType {
23    Wire,
24    Reg,
25    Logic,
26}
27
28/// A port in the module declaration.
29#[derive(Debug, Clone)]
30pub struct RtlPort {
31    pub name: String,
32    pub direction: PortDirection,
33    pub width: u32,
34}
35
36/// An internal signal declaration.
37#[derive(Debug, Clone)]
38pub struct RtlSignal {
39    pub name: String,
40    pub signal_type: SignalType,
41    pub width: u32,
42}
43
44/// A parameter declaration.
45#[derive(Debug, Clone)]
46pub struct RtlParam {
47    pub name: String,
48    pub value: String,
49}
50
51/// Extracted module structure.
52#[derive(Debug, Clone)]
53pub struct RtlModule {
54    pub name: String,
55    pub ports: Vec<RtlPort>,
56    pub signals: Vec<RtlSignal>,
57    pub params: Vec<RtlParam>,
58    pub clocks: Vec<String>,
59}
60
61/// Parse error.
62#[derive(Debug)]
63pub struct RtlParseError {
64    pub message: String,
65}
66
67impl std::fmt::Display for RtlParseError {
68    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69        write!(f, "RTL parse error: {}", self.message)
70    }
71}
72
73/// Parse a Verilog module declaration and extract its structure.
74pub fn parse_verilog_module(src: &str) -> Result<RtlModule, RtlParseError> {
75    // Strip comments
76    let cleaned = strip_comments(src);
77
78    // Find module declaration (must be at word boundary — not inside another word)
79    let module_start = find_keyword(&cleaned, "module")
80        .ok_or_else(|| RtlParseError { message: "no 'module' keyword found".into() })?;
81
82    // Check for endmodule
83    if !cleaned.contains("endmodule") {
84        return Err(RtlParseError { message: "no 'endmodule' found".into() });
85    }
86
87    // Extract module name
88    let after_module = &cleaned[module_start + 7..];
89    let name_end = after_module.find(|c: char| c == '(' || c == ';' || c.is_whitespace())
90        .unwrap_or(after_module.len());
91    let module_name = after_module[..name_end].trim().to_string();
92
93    let mut module = RtlModule {
94        name: module_name,
95        ports: Vec::new(),
96        signals: Vec::new(),
97        params: Vec::new(),
98        clocks: Vec::new(),
99    };
100
101    // Parse ANSI-style port list: module name ( ... );
102    if let Some(paren_start) = after_module.find('(') {
103        if let Some(paren_end) = find_balanced_paren(&after_module[paren_start..]) {
104            let port_list = &after_module[paren_start + 1..paren_start + paren_end];
105            parse_port_list(port_list, &mut module.ports);
106        }
107    }
108
109    // Parse body: signals, parameters, always blocks
110    let body_start = cleaned.find(';').unwrap_or(0) + 1;
111    let body_end = cleaned.rfind("endmodule").unwrap_or(cleaned.len());
112    let body = &cleaned[body_start..body_end];
113
114    for line in body.lines() {
115        let trimmed = line.trim();
116        parse_body_line(trimmed, &mut module);
117    }
118
119    Ok(module)
120}
121
122/// Find a keyword at a word boundary (not inside another identifier).
123fn find_keyword(input: &str, keyword: &str) -> Option<usize> {
124    let klen = keyword.len();
125    for i in 0..input.len() {
126        if i + klen > input.len() { break; }
127        if &input[i..i + klen] == keyword {
128            let is_ident = |b: u8| b.is_ascii_alphanumeric() || b == b'_';
129            let before_ok = i == 0 || !is_ident(input.as_bytes()[i - 1]);
130            let after_ok = i + klen >= input.len() || !is_ident(input.as_bytes()[i + klen]);
131            if before_ok && after_ok {
132                return Some(i);
133            }
134        }
135    }
136    None
137}
138
139/// Strip single-line (//) and multi-line (/* */) comments.
140fn strip_comments(src: &str) -> String {
141    let mut result = String::with_capacity(src.len());
142    let bytes = src.as_bytes();
143    let mut i = 0;
144    while i < bytes.len() {
145        if i + 1 < bytes.len() && bytes[i] == b'/' && bytes[i + 1] == b'/' {
146            // Single-line comment: skip to end of line
147            while i < bytes.len() && bytes[i] != b'\n' {
148                i += 1;
149            }
150        } else if i + 1 < bytes.len() && bytes[i] == b'/' && bytes[i + 1] == b'*' {
151            // Multi-line comment: skip to */
152            i += 2;
153            while i + 1 < bytes.len() && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
154                i += 1;
155            }
156            i += 2; // skip */
157        } else {
158            result.push(bytes[i] as char);
159            i += 1;
160        }
161    }
162    result
163}
164
165/// Find the closing paren that balances the opening paren at position 0.
166fn find_balanced_paren(input: &str) -> Option<usize> {
167    let mut depth = 0i32;
168    for (i, c) in input.chars().enumerate() {
169        match c {
170            '(' => depth += 1,
171            ')' => {
172                depth -= 1;
173                if depth == 0 {
174                    return Some(i);
175                }
176            }
177            _ => {}
178        }
179    }
180    None
181}
182
183/// Parse a comma-separated port list.
184fn parse_port_list(port_list: &str, ports: &mut Vec<RtlPort>) {
185    for port_decl in port_list.split(',') {
186        let trimmed = port_decl.trim();
187        if trimmed.is_empty() { continue; }
188        if let Some(port) = parse_port_decl(trimmed) {
189            ports.push(port);
190        }
191    }
192}
193
194/// Parse a single port declaration like "input [7:0] data".
195fn parse_port_decl(decl: &str) -> Option<RtlPort> {
196    let tokens: Vec<&str> = decl.split_whitespace().collect();
197    if tokens.is_empty() { return None; }
198
199    let mut idx = 0;
200    let direction = match tokens.get(idx)?.to_lowercase().as_str() {
201        "input" => { idx += 1; PortDirection::Input }
202        "output" => { idx += 1; PortDirection::Output }
203        "inout" => { idx += 1; PortDirection::Inout }
204        _ => return None,
205    };
206
207    // Skip optional wire/reg/logic keyword
208    if let Some(tok) = tokens.get(idx) {
209        if matches!(tok.to_lowercase().as_str(), "wire" | "reg" | "logic") {
210            idx += 1;
211        }
212    }
213
214    // Check for width: [N:M]
215    let width = if let Some(tok) = tokens.get(idx) {
216        if tok.starts_with('[') {
217            idx += 1;
218            parse_width_spec(tok)
219        } else {
220            1
221        }
222    } else {
223        1
224    };
225
226    // Port name
227    let name = tokens.get(idx)?.trim_end_matches(|c: char| c == ',' || c == ')' || c == ';');
228    if name.is_empty() { return None; }
229
230    Some(RtlPort {
231        name: name.to_string(),
232        direction,
233        width,
234    })
235}
236
237/// Parse [N:M] width specification, returning the width (N - M + 1).
238fn parse_width_spec(spec: &str) -> u32 {
239    let inner = spec.trim_start_matches('[').trim_end_matches(']');
240    if let Some(colon) = inner.find(':') {
241        let high: i32 = inner[..colon].trim().parse().unwrap_or(0);
242        let low: i32 = inner[colon + 1..].trim().parse().unwrap_or(0);
243        ((high - low).abs() + 1) as u32
244    } else {
245        1
246    }
247}
248
249/// Parse a line from the module body.
250fn parse_body_line(line: &str, module: &mut RtlModule) {
251    let tokens: Vec<&str> = line.split_whitespace().collect();
252    if tokens.is_empty() { return; }
253
254    match tokens[0] {
255        "wire" | "reg" | "logic" => {
256            let sig_type = match tokens[0] {
257                "wire" => SignalType::Wire,
258                "reg" => SignalType::Reg,
259                "logic" => SignalType::Logic,
260                _ => return,
261            };
262            let mut idx = 1;
263            let width = if tokens.get(idx).map(|t| t.starts_with('[')).unwrap_or(false) {
264                let w = parse_width_spec(tokens[idx]);
265                idx += 1;
266                w
267            } else {
268                1
269            };
270            if let Some(name) = tokens.get(idx) {
271                let name = name.trim_end_matches(';');
272                if !name.is_empty() {
273                    module.signals.push(RtlSignal {
274                        name: name.to_string(),
275                        signal_type: sig_type,
276                        width,
277                    });
278                }
279            }
280        }
281        "parameter" | "localparam" => {
282            if tokens.len() >= 3 {
283                let name = tokens[1].to_string();
284                // Skip '=' and get value
285                let value = if tokens.len() >= 4 && tokens[2] == "=" {
286                    tokens[3].trim_end_matches(';').to_string()
287                } else if tokens.len() >= 3 {
288                    tokens[2].trim_start_matches('=').trim_end_matches(';').to_string()
289                } else {
290                    String::new()
291                };
292                module.params.push(RtlParam { name, value });
293            }
294        }
295        "always" => {
296            // Detect clock from always @(posedge/negedge clk)
297            let joined = line.to_string();
298            if let Some(edge_start) = joined.find("posedge").or_else(|| joined.find("negedge")) {
299                let after_edge = &joined[edge_start..];
300                let clock_start = after_edge.find(char::is_whitespace).unwrap_or(0) + 1;
301                let after_space = after_edge[clock_start..].trim();
302                let clock_end = after_space.find(|c: char| c == ')' || c == ',' || c.is_whitespace())
303                    .unwrap_or(after_space.len());
304                let clock_name = &after_space[..clock_end];
305                if !clock_name.is_empty() {
306                    if !module.clocks.contains(&clock_name.to_string()) {
307                        module.clocks.push(clock_name.to_string());
308                    }
309                }
310            }
311        }
312        _ => {}
313    }
314}