Skip to main content

batch_assembler/
batch_assembler.rs

1// use std::fs;
2use std::path::Path;
3use uxn_tal::{Assembler, AssemblerError};
4
5// Find all .tal files recursively in the workspace
6fn find_tal_files(root: &str) -> Result<Vec<String>, std::io::Error> {
7    let mut files = Vec::new();
8    for entry in walkdir::WalkDir::new(root) {
9        let entry = entry?;
10        let path = entry.path();
11        if path.extension().and_then(|s| s.to_str()) == Some("tal") {
12            files.push(path.to_string_lossy().to_string());
13        }
14    }
15    Ok(files)
16}
17
18// Assemble a single TAL file and return ROM size or error
19fn assemble_file(file_path: &str) -> Result<usize, AssemblerError> {
20    let source = std::fs::read_to_string(file_path)?;
21    let mut assembler = Assembler::new();
22    // Always use the canonical path for error context
23    let path = std::fs::canonicalize(file_path)
24        .map(|p| p.to_string_lossy().to_string())
25        .unwrap_or_else(|_| file_path.to_string());
26    let rom = assembler.assemble(&source, Some(path))?;
27    Ok(rom.len())
28}
29
30// Assemble a TAL file and write symbol file, returning ROM and symbol file paths and size
31fn assemble_file_with_symbols(
32    path: &Path,
33) -> Result<(std::path::PathBuf, std::path::PathBuf, usize), AssemblerError> {
34    let source = std::fs::read_to_string(path)?;
35    let mut assembler = Assembler::new();
36    let canonical = std::fs::canonicalize(path)
37        .map(|p| p.to_string_lossy().to_string())
38        .unwrap_or_else(|_| path.to_string_lossy().to_string());
39    let rom = assembler.assemble(&source, Some(canonical))?;
40    let rom_path = path.with_extension("rom");
41    let sym_path = path.with_extension("sym");
42    std::fs::write(&rom_path, &rom)?;
43    let sym_txt = assembler.generate_symbol_file();
44    std::fs::write(&sym_path, sym_txt)?;
45    Ok((rom_path, sym_path, rom.len()))
46}
47
48// Assemble a TAL file and write ROM, returning ROM path and size
49fn assemble_file_auto(path: &Path) -> Result<(std::path::PathBuf, usize), AssemblerError> {
50    let source = std::fs::read_to_string(path)?;
51    let mut assembler = Assembler::new();
52    let canonical = std::fs::canonicalize(path)
53        .map(|p| p.to_string_lossy().to_string())
54        .unwrap_or_else(|_| path.to_string_lossy().to_string());
55    let rom = assembler.assemble(&source, Some(canonical))?;
56    let rom_path = path.with_extension("rom");
57    std::fs::write(&rom_path, &rom)?;
58    Ok((rom_path, rom.len()))
59}
60
61// Directory containing demo TAL files (update as needed)
62const DEMOS_DIR: &str = "../tal";
63
64fn main() -> Result<(), Box<dyn std::error::Error>> {
65    println!("šŸ”Ø UXN TAL Batch Assembler Test");
66    println!("Testing all TAL files in the project");
67    println!("====================================");
68
69    let tal_files = find_tal_files(".")?;
70    let mut successful = 0;
71    let mut failed = 0;
72    let mut total_size = 0;
73    // NEW: collect failed builds
74    struct Failure {
75        file: String,
76        reason: String,
77        lines: Vec<String>,
78    }
79    let mut failures: Vec<Failure> = Vec::new();
80
81    for file_path in &tal_files {
82        print!("Assembling {}... ", file_path);
83
84        match assemble_file(file_path) {
85            Ok(size) => {
86                println!("āœ… Success ({} bytes)", size);
87                successful += 1;
88                total_size += size;
89            }
90            Err(e) => {
91                let msg = format!("{e}");
92                println!("āŒ Failed: {}", first_line(&msg));
93                failed += 1;
94                failures.push(Failure {
95                    file: file_path.to_string(),
96                    reason: first_line(&msg).to_string(),
97                    lines: last_n_nonempty_lines(&msg, 3),
98                });
99            }
100        }
101    }
102
103    // Check if --symbols flag is provided
104    let generate_symbols = std::env::args().any(|arg| arg == "--symbols" || arg == "-s");
105
106    if generate_symbols {
107        println!("šŸ” Symbol file generation enabled\n");
108    }
109
110    // Read directory and process all .tal files
111    if let Ok(entries) = std::fs::read_dir(DEMOS_DIR) {
112        for entry in entries {
113            let entry = entry?;
114            let path = entry.path();
115
116            // Skip if not a .tal file
117            if path.extension().and_then(|s| s.to_str()) != Some("tal") {
118                continue;
119            }
120
121            let filename = path.file_name().unwrap().to_string_lossy();
122            print!("šŸ“ Assembling {}... ", filename);
123
124            if generate_symbols {
125                match assemble_file_with_symbols(&path) {
126                    Ok((rom_path, sym_path, size)) => {
127                        println!(
128                            "āœ… {} bytes -> {} + {}",
129                            size,
130                            rom_path.file_name().unwrap().to_string_lossy(),
131                            sym_path.file_name().unwrap().to_string_lossy()
132                        );
133                        successful += 1;
134                        total_size += size;
135                    }
136                    Err(AssemblerError::Io(e)) => {
137                        let msg = format!("{e}");
138                        println!("āŒ IO error: {}", first_line(&msg));
139                        failed += 1;
140                        failures.push(Failure {
141                            file: path.display().to_string(),
142                            reason: "IO error".into(),
143                            lines: last_n_nonempty_lines(&msg, 3),
144                        });
145                    }
146                    Err(e) => {
147                        let msg = format!("{e}");
148                        println!("āŒ Assembly error: {}", first_line(&msg));
149                        failed += 1;
150                        failures.push(Failure {
151                            file: path.display().to_string(),
152                            reason: first_line(&msg).to_string(),
153                            lines: last_n_nonempty_lines(&msg, 3),
154                        });
155                    }
156                }
157            } else {
158                match assemble_file_auto(&path) {
159                    Ok((rom_path, size)) => {
160                        println!(
161                            "āœ… {} bytes -> {}",
162                            size,
163                            rom_path.file_name().unwrap().to_string_lossy()
164                        );
165                        successful += 1;
166                        total_size += size;
167                    }
168                    Err(AssemblerError::Io(e)) => {
169                        let msg = format!("{e}");
170                        println!("āŒ IO error: {}", first_line(&msg));
171                        failed += 1;
172                        failures.push(Failure {
173                            file: path.display().to_string(),
174                            reason: "IO error".into(),
175                            lines: last_n_nonempty_lines(&msg, 3),
176                        });
177                    }
178                    Err(e) => {
179                        let msg = format!("{e}");
180                        println!("āŒ Assembly error: {}", first_line(&msg));
181                        failed += 1;
182                        failures.push(Failure {
183                            file: path.display().to_string(),
184                            reason: first_line(&msg).to_string(),
185                            lines: last_n_nonempty_lines(&msg, 3),
186                        });
187                    }
188                }
189            }
190        }
191    } else {
192        println!(
193            "ā„¹ļø DEMOS_DIR '{}' does not exist, skipping demo assembly.",
194            DEMOS_DIR
195        );
196    }
197
198    println!("\nšŸ“Š Results:");
199    println!("  āœ… Successfully assembled: {} files", successful);
200    println!("  āŒ Failed: {} files", failed);
201    println!("  šŸ“¦ Total ROM size: {} bytes", total_size);
202
203    // NEW: failure table
204    if !failures.is_empty() {
205        println!("\nšŸ”Ž Failure Summary");
206        println!("{:<4} {:<60} Reason", "#", "File");
207        println!("{}", "-".repeat(100));
208        for (i, f) in failures.iter().enumerate() {
209            println!(
210                "{:<4} {:<60} {}",
211                i + 1,
212                truncate(&f.file, 60),
213                truncate(&f.reason, 30),
214            );
215            if f.lines.is_empty() {
216                println!("      -");
217            } else {
218                for line in &f.lines {
219                    println!("      {}", line);
220                }
221            }
222        }
223    }
224
225    if failed > 0 {
226        println!("\nšŸ’” Some files may use features not yet implemented in our assembler.");
227    }
228
229    Ok(())
230}
231
232// fn collect(dir: &Path, out: &mut Vec<PathBuf>) {
233//     if let Ok(rd) = fs::read_dir(dir) {
234//         for e in rd.flatten() {
235//             let p = e.path();
236//             if p.is_dir() {
237//                 collect(&p, out);
238//             } else if p.extension().map(|x| x == "tal").unwrap_or(false) {
239//                 out.push(p);
240//             }
241//         }
242//     }
243// }
244
245// NEW helpers
246fn first_line(s: &str) -> &str {
247    s.lines().next().unwrap_or(s)
248}
249fn last_n_nonempty_lines(s: &str, n: usize) -> Vec<String> {
250    let mut lines: Vec<String> = s
251        .lines()
252        .map(|l| l.trim_end().to_string())
253        .filter(|l| !l.is_empty())
254        .collect();
255    if lines.len() > n {
256        lines = lines.split_off(lines.len() - n);
257    }
258    lines
259}
260fn truncate(s: &str, max: usize) -> String {
261    if s.len() <= max {
262        s.to_string()
263    } else if max > 3 {
264        format!("{}...", &s[..max - 3])
265    } else {
266        s[..max].to_string()
267    }
268}