batch_assembler/
batch_assembler.rs1use std::path::Path;
3use uxn_tal::{Assembler, AssemblerError};
4
5fn 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
18fn 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 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
30fn 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
48fn 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
61const 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 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 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 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 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 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
232fn 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}