1use std::{
2 env, fs, io,
3 path::Path,
4 process::{Command, Stdio},
5 time::Duration,
6};
7
8use uxn_tal::{Assembler, AssemblerError};
9
10const DIS_IGNORE_COMMENT_DIFF: bool = true; fn main() -> Result<(), AssemblerError> {
13 let args: Vec<String> = env::args().collect();
14 if args.len() != 2 {
15 eprintln!("Usage: {} <file.tal>", args[0]);
16 std::process::exit(1);
17 }
18 let tal_path = &args[1];
19 let source = fs::read_to_string(tal_path).map_err(|e| serr(tal_path, &format!("read: {e}")))?;
20
21 println!("== Source: {} ==", tal_path);
22 for (i, line) in source.lines().enumerate() {
23 println!("{:4}: {}", i + 1, line);
24 }
25
26 let mut internal_rom: Option<Vec<u8>> = None;
28 let mut internal_err: Option<String> = None;
29 let internal_out_path = format!("{tal_path}.uxntal.rom");
30
31 let mut asm = Assembler::new();
32 match asm.assemble(&source, Some(tal_path.to_string())) {
33 Ok(bytes) => {
34 fs::write(&internal_out_path, &bytes).ok();
35 println!(
36 "\n[uxntal] OK -> {} ({} bytes)",
37 internal_out_path,
38 bytes.len()
39 );
40 internal_rom = Some(bytes);
41 let sym_path = format!("{}.sym", &internal_out_path);
43 let sym_bytes = asm.generate_symbol_file();
44 let _ = fs::write(&sym_path, &sym_bytes);
45 if have_uxncli() && Path::new("uxndis.rom").exists() {
46 dump_disassembly(&internal_out_path);
47 }
48 }
49 Err(e) => {
50 println!("\n[uxntal] FAIL: {e}");
51 internal_err = Some(format!("{e}"));
52 }
53 }
54
55 println!("\nRunning external backends...");
57 let uxna = run_uxnasm(tal_path);
58 let drif = run_drifblim(tal_path); if have_uxncli() && Path::new("uxndis.rom").exists() {
60 if internal_rom.is_some() {
61 dump_disassembly(&internal_out_path);
62 }
63 for r in [&uxna, &drif] {
64 if r.ok {
65 if let Some(ref rp) = r.rom_path {
66 dump_disassembly(rp);
67 }
68 }
69 }
70 }
71
72 println!("\n== Backend Summary ==");
74 println!(
75 " {:<9} {:<5} {:>8} output/summary",
76 "backend", "stat", "bytes"
77 );
78 if let Some(ref rom) = internal_rom {
79 println!(
80 " {:<9} {:<5} {:>8} {}",
81 "uxntal",
82 "OK",
83 rom.len(),
84 internal_out_path
85 );
86 } else {
87 let summary = internal_err
89 .as_ref()
90 .map(|e| last_n_lines(e, 3))
91 .unwrap_or_else(|| "-".into());
92 println!(" {:<9} {:<5} {:>8} {}", "uxntal", "FAIL", 0, summary);
93 }
94 for r in [&uxna, &drif] {
95 let summary = if r.ok {
96 r.rom_path.as_deref().unwrap_or("-").to_string()
97 } else {
98 let combined = if !r.stderr.trim().is_empty() {
99 r.stderr.trim().to_string()
100 } else {
101 r.stdout.trim().to_string()
102 };
103 if combined.is_empty() {
104 r.error.as_deref().unwrap_or("-").to_string()
105 } else {
106 last_n_lines(&combined, 3)
107 }
108 };
109 println!(
110 " {:<9} {:<5} {:>8} {}",
111 r.name,
112 if r.ok { "OK" } else { "FAIL" },
113 r.bytes.len(),
114 summary
115 );
116 }
117
118 for r in [&uxna, &drif] {
120 println!("\n== {} ==", r.name);
121 if r.ok {
122 println!(
123 "{}: OK, {} bytes{}",
124 r.name,
125 r.bytes.len(),
126 r.rom_path
127 .as_ref()
128 .map(|p| format!(", rom={}", p))
129 .unwrap_or_default()
130 );
131 } else {
132 println!("{}: FAIL: {}", r.name, r.error.as_deref().unwrap_or("-"));
133 }
134 if !r.stdout.trim().is_empty() {
135 println!("-- stdout --");
136 if r.name == "uxnasm" {
137 println!("{}", tail_block(&r.stdout, 4000));
138 } else {
139 println!("{}", trim_block(&r.stdout, 4000));
140 }
141 }
142 if !r.stderr.trim().is_empty() {
143 println!("-- stderr --");
144 if r.name == "uxnasm" {
145 println!("{}", tail_block(&r.stderr, 4000));
146 } else {
147 println!("{}", trim_block(&r.stderr, 4000));
148 }
149 }
150 }
151
152 if let Some(ref int_rom) = internal_rom {
154 println!("\n== Byte Diffs vs uxntal ==");
155 for r in [&uxna, &drif] {
156 if r.ok {
157 print!("uxntal vs {:<9}: ", r.name);
158 if let Some(d) = first_byte_diff(int_rom, &r.bytes) {
159 println!(
160 "first diff at 0x{:04X}: {:02X} != {:02X}",
161 d.index, d.a, d.b
162 );
163 } else {
164 println!("identical");
165 }
166 } else {
167 println!("uxntal vs {:<9}: skipped ({} failed)", r.name, r.name);
168 }
169 }
170
171 if have_uxncli() && Path::new("uxndis.rom").exists() {
173 println!("\n== Disassembly Diffs (first differing line) ==");
174 if dis_ok(&internal_out_path) {
175 let uxntal_dis = disassemble(&internal_out_path).unwrap_or_default();
176 println!("(disassemble) backend=uxntal rom={}", internal_out_path);
178 for r in [&uxna, &drif] {
179 if r.ok {
180 if let Some(ref rp) = r.rom_path {
181 if dis_ok(rp) {
182 println!("(disassemble) backend={} rom={}", r.name, rp);
185 match disassemble(rp) {
186 Some(other_dis) => {
187 match first_line_diff(&uxntal_dis, &other_dis) {
188 Some((ln, a, b)) => {
189 println!(
190 "uxntal vs {:<9} line {}:\n uxntal: {}\n {:<9}: {}",
191 r.name, ln, a, r.name, b
192 );
193 }
194 None => println!("uxntal vs {:<9} identical", r.name),
195 }
196 }
197 None => {
198 println!("uxntal vs {:<9} disassembly unavailable (empty output)", r.name);
199 }
200 }
201 } else {
202 println!("uxntal vs {:<9} disassembly unavailable", r.name);
203 }
204 }
205 }
206 }
207 } else {
208 println!("(uxntal disassembly unavailable, skipping)");
209 }
210 } else {
211 println!("\n(disassembly skipped: need uxndis.rom + uxncli)");
212 }
213 } else {
214 println!("\nSkipping diffs (internal uxntal failed).");
215 }
216
217 if internal_err
219 .as_ref()
220 .map(|e| e.contains("Skip directive requires hex value"))
221 .unwrap_or(false)
222 {
223 println!("\nNOTE: Internal assembler currently rejects $label (relative padding) which uxnasm accepts.");
224 }
225
226 Ok(())
227}
228
229struct BackendResult {
232 name: &'static str,
233 ok: bool,
234 rom_path: Option<String>,
235 bytes: Vec<u8>,
236 stdout: String,
237 stderr: String,
238 error: Option<String>,
239}
240
241fn run_uxnasm(tal: &str) -> BackendResult {
242 let out = format!("{tal}.uxnasm.rom");
243 let (cmd, mut args): (&str, Vec<String>) = if in_wsl() {
244 (
245 "uxnasm",
246 vec!["--verbose".to_string(), tal.to_string(), out.to_string()],
247 )
248 } else {
249 let wsl_tal = wslize(tal);
251 let wsl_out = wslize(&out);
252 (
253 "wsl",
254 vec![
255 "uxnasm".to_string(),
256 "--verbose".to_string(),
257 wsl_tal,
258 wsl_out,
259 ],
260 )
261 };
262 match spawn_capture(cmd, &mut args) {
263 Ok((status, so, se)) if status.success() && Path::new(&out).exists() => {
264 let out_clone = out.clone();
265 BackendResult {
266 name: "uxnasm",
267 ok: true,
268 rom_path: Some(out),
269 bytes: fs::read(&out_clone).unwrap_or_default(),
270 stdout: so,
271 stderr: se,
272 error: None,
273 }
274 }
275 Ok((_s, so, se)) => BackendResult {
276 name: "uxnasm",
277 ok: false,
278 rom_path: None,
279 bytes: vec![],
280 stdout: so,
281 stderr: se,
282 error: Some("uxnasm failed".into()),
283 },
284 Err(e) => BackendResult {
285 name: "uxnasm",
286 ok: false,
287 rom_path: None,
288 bytes: vec![],
289 stdout: String::new(),
290 stderr: String::new(),
291 error: Some(format!("spawn error: {e}")),
292 },
293 }
294}
295
296fn run_drifblim(tal: &str) -> BackendResult {
297 if !Path::new("drifblim.rom").exists() {
299 return BackendResult {
300 name: "drifblim",
301 ok: false,
302 rom_path: None,
303 bytes: vec![],
304 stdout: String::new(),
305 stderr: String::new(),
306 error: Some("drifblim.rom missing".into()),
307 };
308 }
309 let out = format!("{tal}.drifblim.rom");
310 let (cmd, mut args): (&str, Vec<String>) = if in_wsl() {
311 (
312 "uxncli",
313 vec!["drifblim.rom".to_string(), tal.to_string(), out.to_string()],
314 )
315 } else {
316 let wsl_tal = wslize(tal);
317 let wsl_out = wslize(&out);
318 (
319 "wsl",
320 vec![
321 "uxncli".to_string(),
322 "drifblim.rom".to_string(),
323 wsl_tal,
324 wsl_out,
325 ],
326 )
327 };
328 match spawn_capture(cmd, &mut args) {
329 Ok((status, so, se)) if status.success() && Path::new(&out).exists() => {
330 let out_clone = out.clone();
331 BackendResult {
332 name: "drifblim",
333 ok: true,
334 rom_path: Some(out),
335 bytes: fs::read(out_clone).unwrap_or_default(),
336 stdout: so,
337 stderr: se,
338 error: None,
339 }
340 }
341 Ok((_s, so, se)) => BackendResult {
342 name: "drifblim",
343 ok: false,
344 rom_path: None,
345 bytes: vec![],
346 stdout: so,
347 stderr: se,
348 error: Some("drifblim run failed".into()),
349 },
350 Err(e) => BackendResult {
351 name: "drifblim",
352 ok: false,
353 rom_path: None,
354 bytes: vec![],
355 stdout: String::new(),
356 stderr: String::new(),
357 error: Some(format!("spawn error: {e}")),
358 },
359 }
360}
361
362struct ByteDiff {
365 index: usize,
366 a: u8,
367 b: u8,
368}
369fn first_byte_diff(a: &[u8], b: &[u8]) -> Option<ByteDiff> {
370 let n = a.len().min(b.len());
371 for i in 0..n {
372 if a[i] != b[i] {
373 return Some(ByteDiff {
374 index: i,
375 a: a[i],
376 b: b[i],
377 });
378 }
379 }
380 if a.len() != b.len() {
381 Some(ByteDiff {
382 index: n,
383 a: a.get(n).copied().unwrap_or(0),
384 b: b.get(n).copied().unwrap_or(0),
385 })
386 } else {
387 None
388 }
389}
390
391fn first_line_diff(a: &str, b: &str) -> Option<(usize, String, String)> {
392 for (i, (la, lb)) in a.lines().zip(b.lines()).enumerate() {
393 if la == lb {
394 continue;
395 }
396 if DIS_IGNORE_COMMENT_DIFF {
397 let na = strip_dis_comment(la);
398 let nb = strip_dis_comment(lb);
399 if na == nb {
400 continue;
402 }
403 }
404 return Some((i + 1, la.to_string(), lb.to_string()));
405 }
406 let ac = a.lines().count();
407 let bc = b.lines().count();
408 if ac > bc {
409 a.lines()
410 .nth(bc)
411 .map(|extra| (bc + 1, extra.to_string(), String::new()))
412 } else if bc > ac {
413 b.lines()
414 .nth(ac)
415 .map(|extra| (ac + 1, String::new(), extra.to_string()))
416 } else {
417 None
418 }
419}
420
421fn strip_dis_comment(line: &str) -> &str {
422 match line.find('(') {
423 Some(idx) => line[..idx].trim_end(),
424 None => line.trim_end(),
425 }
426}
427
428fn disassemble(rom_path: &str) -> Option<String> {
430 if !have_uxncli() || !Path::new("uxndis.rom").exists() {
431 return None;
432 }
433 let (cmd, mut args): (&str, Vec<String>) = if in_wsl() {
434 (
435 "uxncli",
436 vec!["uxndis.rom".to_string(), rom_path.to_string()],
437 )
438 } else {
439 let wsl_rom_path = wslize(rom_path);
440 (
441 "wsl",
442 vec!["uxncli".to_string(), "uxndis.rom".to_string(), wsl_rom_path],
443 )
444 };
445 if let Ok((status, out, _)) = spawn_capture(cmd, &mut args) {
446 if status.success() {
447 if out.trim().is_empty() {
449 return None;
450 }
451 return Some(out);
452 }
453 }
454 None
455}
456
457fn dis_ok(rom_path: &str) -> bool {
458 have_uxncli() && Path::new("uxndis.rom").exists() && Path::new(rom_path).exists()
459}
460
461fn dump_disassembly(rom_path: &str) {
462 if !dis_ok(rom_path) {
463 return;
464 }
465 if let Some(text) = disassemble(rom_path) {
466 let path = format!("{rom_path}.dis.txt");
467 let _ = std::fs::write(&path, text);
468 }
469}
470
471fn spawn_capture(
474 cmd: &str,
475 args: &mut [impl AsRef<str>],
476) -> io::Result<(std::process::ExitStatus, String, String)> {
477 let mut c = Command::new(cmd);
478 for a in args {
479 c.arg(a.as_ref());
480 }
481 c.stdout(Stdio::piped()).stderr(Stdio::piped());
482 let mut child = c.spawn()?;
483 let stdout = {
484 let mut s = String::new();
485 if let Some(mut out) = child.stdout.take() {
486 let _ = io::Read::read_to_string(&mut out, &mut s);
487 }
488 s
489 };
490 let stderr = {
491 let mut s = String::new();
492 if let Some(mut er) = child.stderr.take() {
493 let _ = io::Read::read_to_string(&mut er, &mut s);
494 }
495 s
496 };
497 let status = child.wait()?;
498 std::thread::sleep(Duration::from_millis(5));
500 Ok((status, stdout, stderr))
501}
502
503fn in_wsl() -> bool {
504 std::env::var("WSL_DISTRO_NAME").is_ok()
505 || (Path::new("/proc/version").exists()
506 && fs::read_to_string("/proc/version")
507 .unwrap_or_default()
508 .to_lowercase()
509 .contains("microsoft"))
510}
511
512fn which(bin: &str) -> Option<String> {
513 let path = env::var_os("PATH")?;
514 for p in env::split_paths(&path) {
515 let cand = p.join(bin);
516 if cand.is_file() {
517 return cand.to_str().map(|s| s.to_string());
518 }
519 }
520 None
521}
522
523fn have_uxncli() -> bool {
524 if in_wsl() {
525 which("uxncli").is_some()
526 } else {
527 which("wsl").is_some()
528 }
529}
530
531fn wslize(p: &str) -> String {
533 if p.starts_with('/') {
535 return p.to_string();
536 }
537 if p.len() > 2 && p.as_bytes()[1] == b':' {
539 let drive = p.chars().next().unwrap().to_ascii_lowercase();
540 let rest = p[2..].replace('\\', "/");
541 if rest.is_empty() {
542 return format!("/mnt/{drive}");
543 }
544 return format!("/mnt/{drive}/{}", rest.trim_start_matches('/'));
545 }
546 p.replace('\\', "/")
547}
548
549fn serr(path: &str, msg: &str) -> AssemblerError {
552 AssemblerError::SyntaxError {
553 path: path.to_string(),
554 line: 0,
555 position: 0,
556 message: msg.to_string(),
557 source_line: String::new(),
558 }
559}
560
561fn trim_block(s: &str, max: usize) -> String {
562 if s.len() <= max {
563 s.to_string()
564 } else {
565 format!("{}...\n[truncated {} bytes]", &s[..max], s.len() - max)
566 }
567}
568
569fn tail_block(s: &str, max: usize) -> String {
571 if s.len() <= max {
572 s.to_string()
573 } else {
574 let start = s.len() - max;
575 format!("...\n{}\n[truncated first {} bytes]", &s[start..], start)
576 }
577}
578
579fn last_n_lines(s: &str, n: usize) -> String {
580 let mut lines: Vec<&str> = s.lines().filter(|l| !l.trim().is_empty()).collect();
581 if lines.is_empty() {
582 return String::new();
583 }
584 if lines.len() > n {
585 lines = lines.split_off(lines.len() - n);
586 }
587 lines.join(" | ")
588}