riscfetch_core/
parsing.rs

1//! ISA string parsing functions
2
3use crate::extensions::{
4    STANDARD_EXTENSIONS, S_CATEGORY_NAMES, S_EXTENSIONS, Z_CATEGORY_NAMES, Z_EXTENSIONS,
5};
6
7/// Extension info with category and support status
8#[derive(Debug, Clone)]
9pub struct ExtensionInfo {
10    pub name: String,
11    pub description: String,
12    pub category: String,
13    pub supported: bool,
14}
15
16/// Strip rv32/rv64 prefix from ISA base part to get extension letters only
17#[must_use]
18pub fn strip_rv_prefix(base: &str) -> &str {
19    base.strip_prefix("rv64")
20        .or_else(|| base.strip_prefix("rv32"))
21        .unwrap_or(base)
22}
23
24/// Parse extensions from ISA string (pure function for testing)
25#[must_use]
26pub fn parse_extensions_compact(isa: &str) -> String {
27    let isa = isa.to_lowercase();
28    let mut exts = Vec::new();
29
30    // Get the base part before any underscore
31    let base = isa.split('_').next().unwrap_or(&isa);
32    let ext_part = strip_rv_prefix(base);
33
34    // G is shorthand for IMAFD (per RISC-V spec)
35    let has_g = ext_part.contains('g');
36
37    // Standard extensions in canonical order
38    // Note: E and I are mutually exclusive
39    let standard = [
40        ('i', "I", false), // (char, name, implied_by_g)
41        ('e', "E", false), // E = embedded (16 registers)
42        ('m', "M", true),
43        ('a', "A", true),
44        ('f', "F", true),
45        ('d', "D", true),
46        ('q', "Q", false),
47        ('c', "C", false),
48        ('b', "B", false),
49        ('v', "V", false),
50        ('h', "H", false),
51    ];
52
53    for (ch, name, implied_by_g) in standard {
54        if ext_part.contains(ch) || (has_g && implied_by_g) {
55            exts.push(name);
56        }
57    }
58
59    // If G is present but I wasn't explicitly added, add I (G implies IMAFD)
60    if has_g && !exts.contains(&"I") && !exts.contains(&"E") {
61        exts.insert(0, "I");
62    }
63
64    exts.join(" ")
65}
66
67/// Parse Z-extensions from ISA string (pure function for testing)
68#[must_use]
69pub fn parse_z_extensions(isa: &str) -> String {
70    let isa = isa.to_lowercase();
71    let mut z_exts = Vec::new();
72
73    // Check if G is present (G implies Zicsr_Zifencei per RISC-V spec)
74    let base = isa.split('_').next().unwrap_or(&isa);
75    let ext_part = strip_rv_prefix(base);
76    let has_g = ext_part.contains('g');
77
78    // Add implied Z-extensions from G
79    if has_g {
80        z_exts.push("zicsr".to_string());
81        z_exts.push("zifencei".to_string());
82    }
83
84    // Add explicit Z-extensions (z prefix only)
85    for part in isa.split('_') {
86        if part.starts_with('z') && !z_exts.contains(&part.to_string()) {
87            z_exts.push(part.to_string());
88        }
89    }
90
91    z_exts.join(" ")
92}
93
94/// Parse S-extensions from ISA string (pure function for testing)
95#[must_use]
96pub fn parse_s_extensions(isa: &str) -> String {
97    let isa = isa.to_lowercase();
98    let mut s_exts = Vec::new();
99
100    // Add explicit S-extensions (s prefix only)
101    for part in isa.split('_') {
102        if part.starts_with('s') && !s_exts.contains(&part.to_string()) {
103            s_exts.push(part.to_string());
104        }
105    }
106
107    s_exts.join(" ")
108}
109
110/// Parse extensions with explanations (pure function for testing)
111#[must_use]
112pub fn parse_extensions_explained(isa: &str) -> Vec<(String, String)> {
113    let isa = isa.to_lowercase();
114    let base = isa.split('_').next().unwrap_or(&isa);
115    let ext_part = strip_rv_prefix(base);
116    let mut exts = Vec::new();
117
118    for &(ch, name, desc) in STANDARD_EXTENSIONS {
119        if ext_part.contains(ch) {
120            exts.push((name.to_string(), desc.to_string()));
121        }
122    }
123
124    exts
125}
126
127/// Parse Z-extensions with explanations (pure function for testing)
128#[must_use]
129pub fn parse_z_extensions_explained(isa: &str) -> Vec<(String, String)> {
130    let isa = isa.to_lowercase();
131    let mut z_exts = Vec::new();
132
133    for &(pattern, name, desc, _category) in Z_EXTENSIONS {
134        if isa.contains(pattern) {
135            z_exts.push((name.to_string(), desc.to_string()));
136        }
137    }
138
139    z_exts
140}
141
142/// Parse S-extensions with explanations (pure function for testing)
143#[must_use]
144pub fn parse_s_extensions_explained(isa: &str) -> Vec<(String, String)> {
145    let isa = isa.to_lowercase();
146    let mut s_exts = Vec::new();
147
148    for &(pattern, name, desc, _category) in S_EXTENSIONS {
149        if isa.contains(pattern) {
150            s_exts.push((name.to_string(), desc.to_string()));
151        }
152    }
153
154    s_exts
155}
156
157/// Parse Z-extensions with category info
158#[must_use]
159pub fn parse_z_extensions_with_category(isa: &str) -> Vec<ExtensionInfo> {
160    let isa = isa.to_lowercase();
161    let mut z_exts = Vec::new();
162
163    // Check if G is present (G implies Zicsr_Zifencei per RISC-V spec)
164    let base = isa.split('_').next().unwrap_or(&isa);
165    let ext_part = strip_rv_prefix(base);
166    let has_g = ext_part.contains('g');
167
168    // Add implied Z-extensions from G
169    if has_g {
170        z_exts.push(ExtensionInfo {
171            name: "Zicsr".to_string(),
172            description: "CSR Instructions".to_string(),
173            category: "base".to_string(),
174            supported: true,
175        });
176        z_exts.push(ExtensionInfo {
177            name: "Zifencei".to_string(),
178            description: "Instruction-Fetch Fence".to_string(),
179            category: "base".to_string(),
180            supported: true,
181        });
182    }
183
184    for &(pattern, name, desc, category) in Z_EXTENSIONS {
185        if isa.contains(pattern) {
186            // Skip if already added (implied by G)
187            if !z_exts.iter().any(|e| e.name.eq_ignore_ascii_case(name)) {
188                z_exts.push(ExtensionInfo {
189                    name: name.to_string(),
190                    description: desc.to_string(),
191                    category: category.to_string(),
192                    supported: true,
193                });
194            }
195        }
196    }
197
198    z_exts
199}
200
201/// Parse S-extensions with category info
202#[must_use]
203pub fn parse_s_extensions_with_category(isa: &str) -> Vec<ExtensionInfo> {
204    let isa = isa.to_lowercase();
205    let mut s_exts = Vec::new();
206
207    for &(pattern, name, desc, category) in S_EXTENSIONS {
208        if isa.contains(pattern) {
209            s_exts.push(ExtensionInfo {
210                name: name.to_string(),
211                description: desc.to_string(),
212                category: category.to_string(),
213                supported: true,
214            });
215        }
216    }
217
218    s_exts
219}
220
221/// Get category display name for Z-extensions
222#[must_use]
223pub fn get_z_category_name(category: &str) -> &'static str {
224    Z_CATEGORY_NAMES
225        .iter()
226        .find(|(id, _)| *id == category)
227        .map_or("Other", |(_, name)| *name)
228}
229
230/// Get category display name for S-extensions
231#[must_use]
232pub fn get_s_category_name(category: &str) -> &'static str {
233    S_CATEGORY_NAMES
234        .iter()
235        .find(|(id, _)| *id == category)
236        .map_or("Other", |(_, name)| *name)
237}
238
239/// Group extensions by category
240#[must_use]
241pub fn group_by_category(extensions: &[ExtensionInfo]) -> Vec<(String, Vec<&ExtensionInfo>)> {
242    use std::collections::BTreeMap;
243    let mut groups: BTreeMap<String, Vec<&ExtensionInfo>> = BTreeMap::new();
244
245    for ext in extensions {
246        groups.entry(ext.category.clone()).or_default().push(ext);
247    }
248
249    groups.into_iter().collect()
250}
251
252/// Get ALL Z-extensions with support status based on ISA string
253#[must_use]
254pub fn get_all_z_extensions_with_status(isa: &str) -> Vec<ExtensionInfo> {
255    let isa = isa.to_lowercase();
256    let base = isa.split('_').next().unwrap_or(&isa);
257    let ext_part = strip_rv_prefix(base);
258    let has_g = ext_part.contains('g');
259
260    Z_EXTENSIONS
261        .iter()
262        .map(|&(pattern, name, desc, category)| {
263            let supported =
264                isa.contains(pattern) || (has_g && (pattern == "zicsr" || pattern == "zifencei"));
265            ExtensionInfo {
266                name: name.to_string(),
267                description: desc.to_string(),
268                category: category.to_string(),
269                supported,
270            }
271        })
272        .collect()
273}
274
275/// Get ALL S-extensions with support status based on ISA string
276#[must_use]
277pub fn get_all_s_extensions_with_status(isa: &str) -> Vec<ExtensionInfo> {
278    let isa = isa.to_lowercase();
279
280    S_EXTENSIONS
281        .iter()
282        .map(|&(pattern, name, desc, category)| {
283            let supported = isa.contains(pattern);
284            ExtensionInfo {
285                name: name.to_string(),
286                description: desc.to_string(),
287                category: category.to_string(),
288                supported,
289            }
290        })
291        .collect()
292}
293
294/// Get ALL standard extensions with support status
295#[must_use]
296pub fn get_all_standard_extensions_with_status(isa: &str) -> Vec<(String, String, bool)> {
297    let isa = isa.to_lowercase();
298    let base = isa.split('_').next().unwrap_or(&isa);
299    let ext_part = strip_rv_prefix(base);
300    let has_g = ext_part.contains('g');
301
302    STANDARD_EXTENSIONS
303        .iter()
304        .map(|&(char, name, desc)| {
305            let supported =
306                ext_part.contains(char) || (has_g && matches!(char, 'i' | 'm' | 'a' | 'f' | 'd'));
307            (name.to_string(), desc.to_string(), supported)
308        })
309        .collect()
310}
311
312/// Parse vector details from ISA string (pure function for testing)
313/// Returns None if no vector extension, Some(details) otherwise
314#[must_use]
315pub fn parse_vector_from_isa(isa: &str) -> Option<String> {
316    let isa = isa.to_lowercase();
317    let base = isa.split('_').next().unwrap_or(&isa);
318    let ext_part = strip_rv_prefix(base);
319
320    // Check for V extension in the extension part, or zve in Z-extensions
321    if !ext_part.contains('v') && !isa.contains("zve") {
322        return None;
323    }
324
325    let mut details = vec!["Enabled".to_string()];
326
327    // Detect VLEN from zvl* extensions (use largest value)
328    // If no zvl* specified, VLEN is implementation-defined (do not display)
329    if isa.contains("zvl65536b") {
330        details.push("VLEN>=65536".to_string());
331    } else if isa.contains("zvl32768b") {
332        details.push("VLEN>=32768".to_string());
333    } else if isa.contains("zvl16384b") {
334        details.push("VLEN>=16384".to_string());
335    } else if isa.contains("zvl8192b") {
336        details.push("VLEN>=8192".to_string());
337    } else if isa.contains("zvl4096b") {
338        details.push("VLEN>=4096".to_string());
339    } else if isa.contains("zvl2048b") {
340        details.push("VLEN>=2048".to_string());
341    } else if isa.contains("zvl1024b") {
342        details.push("VLEN>=1024".to_string());
343    } else if isa.contains("zvl512b") {
344        details.push("VLEN>=512".to_string());
345    } else if isa.contains("zvl256b") {
346        details.push("VLEN>=256".to_string());
347    } else if isa.contains("zvl128b") {
348        details.push("VLEN>=128".to_string());
349    } else if isa.contains("zvl64b") {
350        details.push("VLEN>=64".to_string());
351    } else if isa.contains("zvl32b") {
352        details.push("VLEN>=32".to_string());
353    }
354    // No default VLEN - it's implementation-defined per RISC-V spec
355
356    Some(details.join(", "))
357}
358
359#[cfg(test)]
360mod tests {
361    use super::*;
362
363    // Real ISA strings from actual RISC-V systems
364    const ISA_VISIONFIVE2: &str = "rv64imafdc_zicntr_zicsr_zifencei_zihpm_zba_zbb";
365    const ISA_SPACEMIT_K1: &str = "rv64imafdcv_zicbom_zicboz_zicntr_zicsr_zifencei_zihintpause_zihpm_zba_zbb_zbc_zbs_zkt_zvkt_zvl128b_zvl256b_zvl32b_zvl64b";
366    const ISA_MINIMAL: &str = "rv64imac";
367    const ISA_RV32: &str = "rv32imc";
368
369    // === parse_extensions_compact tests ===
370
371    #[test]
372    fn test_visionfive2() {
373        assert_eq!(parse_extensions_compact(ISA_VISIONFIVE2), "I M A F D C");
374    }
375
376    #[test]
377    fn test_spacemit() {
378        assert_eq!(parse_extensions_compact(ISA_SPACEMIT_K1), "I M A F D C V");
379    }
380
381    #[test]
382    fn test_minimal() {
383        assert_eq!(parse_extensions_compact(ISA_MINIMAL), "I M A C");
384    }
385
386    #[test]
387    fn test_rv32() {
388        assert_eq!(parse_extensions_compact(ISA_RV32), "I M C");
389    }
390
391    #[test]
392    fn test_unknown() {
393        assert_eq!(parse_extensions_compact("unknown"), "");
394    }
395
396    #[test]
397    fn test_case_insensitive() {
398        assert_eq!(
399            parse_extensions_compact("RV64IMAFDC"),
400            parse_extensions_compact("rv64imafdc")
401        );
402    }
403
404    #[test]
405    fn test_empty() {
406        assert_eq!(parse_extensions_compact(""), "");
407    }
408
409    // === Specification-based tests (from SPEC.md) ===
410
411    #[test]
412    fn spec_g_expansion() {
413        assert_eq!(parse_extensions_compact("rv64gc"), "I M A F D C");
414    }
415
416    #[test]
417    fn spec_g_expansion_uppercase() {
418        assert_eq!(parse_extensions_compact("RV64GC"), "I M A F D C");
419    }
420
421    #[test]
422    fn spec_e_extension() {
423        assert_eq!(parse_extensions_compact("rv32e"), "E");
424    }
425
426    #[test]
427    fn spec_e_with_c() {
428        assert_eq!(parse_extensions_compact("rv32ec"), "E C");
429    }
430
431    #[test]
432    fn spec_with_vector() {
433        assert_eq!(parse_extensions_compact("rv64imafdcv"), "I M A F D C V");
434    }
435
436    #[test]
437    fn spec_rv64_prefix_not_vector() {
438        let result = parse_extensions_compact("rv64imafdc");
439        assert!(!result.contains('V'));
440    }
441
442    #[test]
443    fn spec_z_extensions_ignored() {
444        assert_eq!(
445            parse_extensions_compact("rv64imafdc_zba_zbb"),
446            "I M A F D C"
447        );
448    }
449
450    #[test]
451    fn spec_rv64_only() {
452        assert_eq!(parse_extensions_compact("rv64"), "");
453    }
454
455    // === parse_z_extensions tests ===
456
457    #[test]
458    fn test_z_extensions_visionfive2() {
459        let result = parse_z_extensions(ISA_VISIONFIVE2);
460        assert!(result.contains("zicntr"));
461        assert!(result.contains("zicsr"));
462        assert!(result.contains("zifencei"));
463        assert!(result.contains("zba"));
464        assert!(result.contains("zbb"));
465    }
466
467    #[test]
468    fn test_z_extensions_spacemit() {
469        let result = parse_z_extensions(ISA_SPACEMIT_K1);
470        assert!(result.contains("zicbom"));
471        assert!(result.contains("zicboz"));
472        assert!(result.contains("zbc"));
473        assert!(result.contains("zbs"));
474        assert!(result.contains("zvl256b"));
475    }
476
477    #[test]
478    fn test_z_extensions_minimal() {
479        assert!(parse_z_extensions(ISA_MINIMAL).is_empty());
480    }
481
482    #[test]
483    fn spec_z_extensions_basic() {
484        assert_eq!(parse_z_extensions("rv64i_zicsr_zifencei"), "zicsr zifencei");
485    }
486
487    #[test]
488    fn spec_z_extensions_order() {
489        assert_eq!(parse_z_extensions("rv64i_zba_zbb_zbc"), "zba zbb zbc");
490    }
491
492    #[test]
493    fn spec_z_extensions_none() {
494        assert_eq!(parse_z_extensions("rv64imafdc"), "");
495    }
496
497    #[test]
498    fn spec_z_extensions_g_implies() {
499        assert_eq!(parse_z_extensions("rv64gc"), "zicsr zifencei");
500    }
501
502    #[test]
503    fn spec_z_extensions_case() {
504        assert_eq!(parse_z_extensions("rv64i_Zicsr"), "zicsr");
505    }
506
507    // === parse_s_extensions tests ===
508
509    #[test]
510    fn spec_s_extensions() {
511        let result = parse_s_extensions("rv64i_sstc");
512        assert!(result.contains("sstc"));
513    }
514
515    // === parse_extensions_explained tests ===
516
517    #[test]
518    fn test_explained_visionfive2() {
519        let result = parse_extensions_explained(ISA_VISIONFIVE2);
520        assert_eq!(result.len(), 6); // I M A F D C
521        assert!(result.iter().any(|(n, _)| n == "I"));
522        assert!(result.iter().any(|(n, _)| n == "M"));
523        assert!(result.iter().any(|(n, _)| n == "F"));
524        assert!(result.iter().any(|(n, _)| n == "D"));
525        assert!(result.iter().any(|(n, _)| n == "C"));
526    }
527
528    #[test]
529    fn test_z_explained_spacemit() {
530        let result = parse_z_extensions_explained(ISA_SPACEMIT_K1);
531        assert!(result
532            .iter()
533            .any(|(n, d)| n == "Zba" && d == "Address Generation"));
534        assert!(result
535            .iter()
536            .any(|(n, d)| n == "Zbb" && d == "Basic Bit Manipulation"));
537        assert!(result
538            .iter()
539            .any(|(n, d)| n == "Zbc" && d == "Carry-less Multiply"));
540    }
541
542    // === parse_vector_from_isa tests ===
543
544    #[test]
545    fn test_vector_no_vector() {
546        assert!(parse_vector_from_isa(ISA_VISIONFIVE2).is_none());
547    }
548
549    #[test]
550    fn test_vector_with_v() {
551        let result = parse_vector_from_isa(ISA_SPACEMIT_K1);
552        assert!(result.is_some());
553        let detail = result.unwrap();
554        assert!(detail.contains("Enabled"));
555        assert!(detail.contains("VLEN>=256"));
556    }
557
558    #[test]
559    fn test_vector_zve_only() {
560        let result = parse_vector_from_isa("rv64imac_zve32x");
561        assert!(result.is_some());
562        assert!(result.unwrap().contains("Enabled"));
563    }
564
565    #[test]
566    fn spec_vector_with_v() {
567        let result = parse_vector_from_isa("rv64imafdcv");
568        assert!(result.is_some());
569        assert!(result.unwrap().contains("Enabled"));
570    }
571
572    #[test]
573    fn spec_vector_none() {
574        assert!(parse_vector_from_isa("rv64imafdc").is_none());
575    }
576
577    #[test]
578    fn spec_vector_vlen_256() {
579        let result = parse_vector_from_isa("rv64imafdcv_zvl256b");
580        assert!(result.is_some());
581        assert!(result.unwrap().contains("VLEN>=256"));
582    }
583
584    #[test]
585    fn spec_vector_vlen_largest() {
586        let result = parse_vector_from_isa("rv64imafdcv_zvl128b_zvl256b");
587        assert!(result.is_some());
588        assert!(result.unwrap().contains("VLEN>=256"));
589    }
590
591    #[test]
592    fn spec_vector_no_default_vlen() {
593        let result = parse_vector_from_isa("rv64imafdcv");
594        assert!(result.is_some());
595        let detail = result.unwrap();
596        assert!(detail.contains("Enabled"));
597        assert!(!detail.contains("VLEN"));
598    }
599}