logicaffeine_compile/codegen_sva/
rtl_extract.rs1use std::collections::HashSet;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum PortDirection {
15 Input,
16 Output,
17 Inout,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum SignalType {
23 Wire,
24 Reg,
25 Logic,
26}
27
28#[derive(Debug, Clone)]
30pub struct RtlPort {
31 pub name: String,
32 pub direction: PortDirection,
33 pub width: u32,
34}
35
36#[derive(Debug, Clone)]
38pub struct RtlSignal {
39 pub name: String,
40 pub signal_type: SignalType,
41 pub width: u32,
42}
43
44#[derive(Debug, Clone)]
46pub struct RtlParam {
47 pub name: String,
48 pub value: String,
49}
50
51#[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#[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
73pub fn parse_verilog_module(src: &str) -> Result<RtlModule, RtlParseError> {
75 let cleaned = strip_comments(src);
77
78 let module_start = find_keyword(&cleaned, "module")
80 .ok_or_else(|| RtlParseError { message: "no 'module' keyword found".into() })?;
81
82 if !cleaned.contains("endmodule") {
84 return Err(RtlParseError { message: "no 'endmodule' found".into() });
85 }
86
87 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 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 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
122fn 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
139fn 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 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 i += 2;
153 while i + 1 < bytes.len() && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
154 i += 1;
155 }
156 i += 2; } else {
158 result.push(bytes[i] as char);
159 i += 1;
160 }
161 }
162 result
163}
164
165fn 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
183fn 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
194fn 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 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 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 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
237fn 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
249fn 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 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 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}