masm_formatter/
lib.rs

1use once_cell::sync::Lazy;
2use regex::Regex;
3use std::fs::File;
4use std::io::{self, BufRead, BufReader, Write};
5use std::path::Path;
6
7static SINGLE_LINE_EXPORT_REGEX: Lazy<Regex> =
8    Lazy::new(|| Regex::new(r"^export\..*(?:(?:::)|(?:->)).*$").unwrap());
9
10#[derive(Debug, PartialEq, Clone)]
11enum ConstructType {
12    Proc,
13    Export,
14    Begin,
15    End,
16    While,
17    Repeat,
18    If,
19    Else,
20}
21
22impl ConstructType {
23    fn from_str(s: &str) -> Option<Self> {
24        match s {
25            "proc" => Some(Self::Proc),
26            "export" => Some(Self::Export),
27            "begin" => Some(Self::Begin),
28            "end" => Some(Self::End),
29            "while" => Some(Self::While),
30            "repeat" => Some(Self::Repeat),
31            "if" => Some(Self::If),
32            "else" => Some(Self::Else),
33            _ => None,
34        }
35    }
36}
37
38const INDENT: &str = "    ";
39
40fn is_comment(line: &str) -> bool {
41    line.trim_start().starts_with('#')
42}
43
44fn is_stack_comment(line: &str) -> bool {
45    line.trim_start().starts_with("# =>")
46}
47
48fn is_single_export_line(line: &str) -> bool {
49    SINGLE_LINE_EXPORT_REGEX.is_match(line)
50}
51
52fn is_use_statement(line: &str) -> bool {
53    line.trim_start().starts_with("use.")
54}
55
56fn extract_and_sort_imports(lines: &[&str]) -> (Vec<String>, usize) {
57    let mut imports = Vec::new();
58    let mut import_end_index = 0;
59
60    for (i, line) in lines.iter().enumerate() {
61        let trimmed = line.trim();
62        if is_use_statement(trimmed) {
63            imports.push(trimmed.to_string());
64            import_end_index = i + 1;
65        } else if !trimmed.is_empty() {
66            // Stop collecting imports when we hit non-empty, non-import line
67            break;
68        } else if !imports.is_empty() {
69            // Include empty lines that come after imports but before other content
70            import_end_index = i + 1;
71        }
72    }
73
74    // Sort imports alphabetically
75    imports.sort();
76
77    (imports, import_end_index)
78}
79
80pub fn format_code(code: &str) -> String {
81    let lines: Vec<&str> = code.lines().collect();
82
83    // Extract and sort imports
84    let (sorted_imports, import_end_index) = extract_and_sort_imports(&lines);
85
86    let mut formatted_code = String::new();
87    let mut indentation_level = 0;
88    let mut construct_stack = Vec::new();
89    let mut last_line_was_empty = false;
90    let mut last_was_export_line = false;
91    let mut last_line_was_stack_comment = false;
92
93    // Add sorted imports first
94    for import in sorted_imports {
95        formatted_code.push_str(&import);
96        formatted_code.push('\n');
97    }
98
99    // Add empty line after imports if there were any and the next line isn't empty
100    if import_end_index > 0 && import_end_index < lines.len() {
101        if !lines[import_end_index].trim().is_empty() {
102            formatted_code.push('\n');
103        }
104    }
105
106    // Process remaining lines (skip the import section)
107    let remaining_lines = &lines[import_end_index..];
108
109    for line in remaining_lines {
110        let trimmed_line = line.trim();
111
112        if !trimmed_line.is_empty() {
113            if is_comment(trimmed_line) {
114                if is_stack_comment(trimmed_line) {
115                    last_line_was_stack_comment = true;
116                } else {
117                    last_line_was_stack_comment = false;
118                }
119
120                if last_was_export_line {
121                    formatted_code.push_str(trimmed_line);
122                } else {
123                    if let Some(prev_line) = formatted_code.lines().last() {
124                        let prev_indent_level =
125                            prev_line.chars().take_while(|&c| c == ' ').count() / 4;
126                        if prev_line.trim_start().starts_with("export") {
127                            formatted_code.push_str(&INDENT.repeat(prev_indent_level + 1));
128                        } else {
129                            formatted_code.push_str(&INDENT.repeat(indentation_level));
130                        }
131                    } else {
132                        formatted_code.push_str(&INDENT.repeat(indentation_level));
133                    }
134                    formatted_code.push_str(trimmed_line);
135                }
136                formatted_code.push('\n');
137                last_line_was_empty = false;
138                continue;
139            }
140
141            if is_single_export_line(trimmed_line) {
142                formatted_code.push_str(trimmed_line);
143                formatted_code.push('\n');
144                last_line_was_empty = false;
145                last_was_export_line = true;
146                continue;
147            }
148
149            last_was_export_line = false;
150
151            // Remove inline comment for keyword extraction.
152            let code_without_comment = trimmed_line.split('#').next().unwrap().trim();
153            let first_word = code_without_comment.split('.').next();
154
155            // Special handling for stack comment newline
156            if last_line_was_stack_comment && first_word.is_some() {
157                let word = first_word.unwrap();
158                if word != "end" && !last_line_was_empty {
159                    formatted_code.push('\n');
160                }
161                last_line_was_stack_comment = false;
162            }
163
164            if let Some(word) = first_word {
165                if let Some(construct) = ConstructType::from_str(word) {
166                    match construct {
167                        ConstructType::End => {
168                            if let Some(last_construct) = construct_stack.pop() {
169                                if last_construct != ConstructType::End && indentation_level > 0 {
170                                    indentation_level -= 1;
171                                }
172                            }
173                        }
174                        ConstructType::Else => {
175                            if let Some(last_construct) = construct_stack.last() {
176                                if *last_construct == ConstructType::If && indentation_level > 0 {
177                                    indentation_level -= 1;
178                                }
179                            }
180                        }
181                        _ => {
182                            construct_stack.push(construct.clone());
183                        }
184                    }
185
186                    formatted_code.push_str(&INDENT.repeat(indentation_level));
187                    formatted_code.push_str(trimmed_line);
188                    formatted_code.push('\n');
189                    last_line_was_empty = false;
190
191                    match construct {
192                        ConstructType::Begin
193                        | ConstructType::If
194                        | ConstructType::Proc
195                        | ConstructType::Export
196                        | ConstructType::Repeat
197                        | ConstructType::While
198                        | ConstructType::Else => {
199                            indentation_level += 1;
200                        }
201                        _ => {}
202                    }
203
204                    continue;
205                }
206            }
207
208            formatted_code.push_str(&INDENT.repeat(indentation_level));
209            formatted_code.push_str(trimmed_line);
210            formatted_code.push('\n');
211            last_line_was_empty = false;
212        } else if !last_line_was_empty {
213            formatted_code.push('\n');
214            last_line_was_empty = true;
215        }
216    }
217
218    // Ensure the output ends with exactly one newline.
219    while formatted_code.ends_with('\n') {
220        formatted_code.pop();
221    }
222    formatted_code.push('\n');
223
224    formatted_code
225}
226
227pub fn format_file(file_path: &Path) -> io::Result<()> {
228    let file = File::open(file_path)?;
229    let mut input_code = String::new();
230
231    let reader = BufReader::new(file);
232    for line in reader.lines() {
233        input_code.push_str(&line?);
234        input_code.push_str("\n");
235    }
236
237    let formatted_code = format_code(&input_code);
238
239    let mut file = File::create(file_path)?;
240    file.write_all(formatted_code.as_bytes())?;
241
242    Ok(())
243}