Skip to main content

debug_assemble/
debug_assemble.rs

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; // Set false to count differing comments as diffs
11
12fn 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    // 1. Internal assembler (non-fatal on error now)
27    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            // Emit symbol file from the same assembler instance.
42            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    // 2. External backends (always attempted)
56    println!("\nRunning external backends...");
57    let uxna = run_uxnasm(tal_path);
58    let drif = run_drifblim(tal_path); // optional
59    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    // 3. Summary
73    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        // Show last 3 non-empty lines of internal error (previously only last 1)
88        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    // 4. Detailed output (stdout/stderr trimmed)
119    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    // 5. Diffs (only if internal succeeded and external ok)
153    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        // Optional disassembly diff (needs uxndis.rom & uxncli)
172        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                // Print the internal ROM path for clarity
177                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                                // NOTE: This is where each external backend (including 'drifblim') is disassembled.
183                                // The next call to disassemble(rp) produces the B side of the diff.
184                                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    // 6. Explicit note if user expected relative $label support (internal failure hint)
218    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
229/* ---------------- Backend runners ---------------- */
230
231struct 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        // Convert Windows paths to WSL paths so uxnasm inside WSL can access them
250        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    // Optional: must exist drifblim.rom driver
298    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
362/* ---------------- Diff helpers ---------------- */
363
364struct 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                // differ only in trailing comment -> ignore
401                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
428/* ---------------- Disassembly ---------------- */
429fn 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            // Treat empty output as failure to avoid blank B-side in diff
448            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
471/* ---------------- System helpers ---------------- */
472
473fn 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    // Tiny delay to let filesystem flush outputs (mostly for WSL)
499    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
531/* ---------- Path translation (Windows host -> WSL) ---------- */
532fn wslize(p: &str) -> String {
533    // Fast path: already looks like a Unix path
534    if p.starts_with('/') {
535        return p.to_string();
536    }
537    // Drive letter?
538    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
549/* ---------------- Misc helpers ---------------- */
550
551fn 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
569// NEW: tail printer for uxnasm output
570fn 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}