Skip to main content

uxn_tal/
debug.rs

1use crate::bkend::{AssemblerBackend, AssemblyOutput};
2use crate::dis_uxndis::run_dis_file;
3use crate::{Assembler, AssemblerError};
4use std::fs;
5use std::path::Path;
6use std::process::Command;
7
8pub struct DebugAssembleResult {
9    pub uxntal_rom_path: String,
10    pub uxnasm_rom_path: String,
11    pub drifblim_rom_path: String,
12    pub uxntal_rom_bytes: Vec<u8>,
13    pub uxnasm_rom_bytes: Vec<u8>,
14    pub drifblim_rom_bytes: Vec<u8>,
15    pub uxntal_output: String,
16    pub uxnasm_output: String,
17    pub drifblim_output: String,
18    pub uxntal_dis_output: String,
19    pub uxnasm_dis_output: String,
20    pub drifblim_dis_output: String,
21    pub diff_uxntal_uxnasm: Option<(usize, String, String)>,
22    pub diff_uxntal_drifblim: Option<(usize, String, String)>,
23    pub diff_uxnasm_drifblim: Option<(usize, String, String)>,
24    pub backend_errors: Vec<(String, String)>,
25}
26
27#[derive(Default)]
28pub struct UxntalBackend {
29    pub drif_mode: bool,
30}
31
32impl UxntalBackend {
33    pub fn with_drif_mode(drif_mode: bool) -> Self {
34        Self { drif_mode }
35    }
36}
37
38impl AssemblerBackend for UxntalBackend {
39    fn name(&self) -> &'static str {
40        "uxntal"
41    }
42    fn assemble(&self, tal_file: &str, tal_source: &str) -> Result<AssemblyOutput, AssemblerError> {
43        let mut assembler = if self.drif_mode {
44            Assembler::with_drif_mode(true)
45        } else {
46            Assembler::new()
47        };
48        let rom = assembler.assemble(tal_source, Some(tal_file.to_string()))?;
49        let rom_path = format!("{}_{}.rom", tal_file, self.name());
50        fs::write(&rom_path, &rom).map_err(|e| io_err(&rom_path, e))?;
51        Ok(AssemblyOutput {
52            rom_path: rom_path.clone(),
53            rom_bytes: rom.clone(),
54            stdout: run_vm_last(&rom)?,
55            disassembly: run_dis_file(&rom_path)?,
56        })
57    }
58}
59
60pub struct UxnasmBackend;
61impl AssemblerBackend for UxnasmBackend {
62    fn name(&self) -> &'static str {
63        "uxnasm"
64    }
65    fn assemble(&self, tal_file: &str, _src: &str) -> Result<AssemblyOutput, AssemblerError> {
66        // let input = tal_file.replace('\\', "/");
67        // let input = input.replace("//?/C:", "/mnt/c"); // Handle Windows long path prefix
68        let input = if cfg!(windows) {
69            wslpath::windows_to_wsl(tal_file).map_err(|e| {
70                syntax_err(tal_file, &format!("Could not convert TAL path to WSL: {e}"))
71            })?
72        } else {
73            tal_file.to_string()
74        };
75        //let input = tal_file;//.replace("\\\\?\\", ""); // Handle Windows long path prefix
76        // let input = match Path::new(&input).canonicalize() {
77        //     Ok(abs_path) => {
78        //         let cwd = std::env::current_dir().unwrap_or_else(|_| Path::new(".").to_path_buf());
79        //         match abs_path.strip_prefix(&cwd) {
80        //             Ok(rel) => rel.to_string_lossy().replace('\\', "/"),
81        //             Err(_) => abs_path.to_string_lossy().replace('\\', "/"),
82        //         }
83        //     }
84        //     Err(_) => input,
85        // };
86        let rom_path = format!("{}_{}.rom", input, self.name());
87        let mut cmd = spawn_cmd("uxnasm", &["--verbose", &input, &rom_path]);
88        let output = cmd
89            .output()
90            .map_err(|e| syntax_err(&input, &format!("Failed to run uxnasm: {e}")))?;
91        if !output.status.success() {
92            return Err(syntax_err(
93                &input,
94                &format!(
95                    "uxnasm failed {:?} {:?} stderr: {} stdout: {}",
96                    output.status,
97                    std::env::current_dir(),
98                    String::from_utf8_lossy(&output.stderr),
99                    String::from_utf8_lossy(&output.stdout)
100                        .lines()
101                        .rev()
102                        .take(3)
103                        .collect::<Vec<_>>()
104                        .into_iter()
105                        .rev()
106                        .collect::<Vec<_>>()
107                        .join("\n")
108                ),
109            ));
110        }
111        // let rom_path = rom_path.replace("/mnt/c/", "C:/"); // Handle Windows long path prefix
112        let rom_path = if cfg!(windows) {
113            wslpath::wsl_to_windows(&rom_path).map_err(|e| {
114                syntax_err(
115                    &rom_path,
116                    &format!("Could not convert ROM path to Windows: {e}"),
117                )
118            })?
119        } else {
120            rom_path
121        };
122        let bytes = fs::read(&rom_path).unwrap_or_default();
123        Ok(AssemblyOutput {
124            rom_path: rom_path.clone(),
125            rom_bytes: bytes.clone(),
126            stdout: run_uxncli_get_stdout(&rom_path)?,
127            disassembly: run_dis_file(&rom_path)?,
128        })
129    }
130}
131
132pub struct DrifblimBackend;
133impl AssemblerBackend for DrifblimBackend {
134    fn name(&self) -> &'static str {
135        "drifblim"
136    }
137    fn assemble(&self, tal_file: &str, _src: &str) -> Result<AssemblyOutput, AssemblerError> {
138        // let mut temp_file = tempfile::NamedTempFile::new().map_err(|e| syntax_err(tal_file, &format!("tempfile error: {e}")))?;
139        // temp_file.write_all(_src.as_bytes()).map_err(|e| syntax_err(tal_file, &format!("tempfile write error: {e}")))?;
140        // let tal_path = temp_file.path().to_string_lossy().to_string();
141        // let tal_path = tal_path.replace('\\', "/");
142        // let rom_path = format!("{}_{}.rom", tal_path, self.name());
143        // println!("rom_path: {}", rom_path);
144
145        let input = tal_file.replace('\\', "/");
146        let input = input.replace("//?/C:", "/mnt/c"); // Handle Windows long path prefix
147                                                       //let input = tal_file;//.replace("\\\\?\\", ""); // Handle Windows long path prefix
148        let rom_path = format!("{}_{}.rom", input, self.name());
149        println!("rom_path: {}", rom_path);
150        println!("tal_file: {}", tal_file);
151        println!("input: {}", input);
152        let stdout = Self::run_drif(&input, &input, &rom_path)?;
153        let rom_path = rom_path.replace("/mnt/c/", "C:/"); // Handle Windows long path prefix
154        let rom_path = rom_path.replace('/', "\\");
155        let bytes = fs::read(&rom_path).unwrap_or_default();
156        if bytes.is_empty() {
157            return Err(syntax_err(&rom_path, "drifblim produced empty ROM"));
158        }
159        Ok(AssemblyOutput {
160            rom_path: rom_path.clone(),
161            rom_bytes: bytes,
162            stdout,
163            disassembly: run_dis_file(&rom_path)?,
164        })
165    }
166}
167impl DrifblimBackend {
168    fn run_drif(tal_path: &str, _tal_file: &str, rom_path: &str) -> Result<String, AssemblerError> {
169        println!("tal_path: {}", tal_path);
170        println!("rom_path: {}", rom_path);
171        let drifblim_rom = crate::bkend_drif::drifblim_repo_get_drifblim();
172        let drifblim_rom = if cfg!(windows) {
173            wslpath::windows_to_wsl(&drifblim_rom.to_string_lossy())
174                .map_err(|_| syntax_err(rom_path, "Could not convert drifblim path to WSL"))?
175        } else {
176            drifblim_rom.to_string_lossy().to_string()
177        };
178        let output = if cfg!(windows) {
179            let in_wsl = detect_wsl();
180            if in_wsl {
181                Command::new("uxncli")
182                    .arg(drifblim_rom)
183                    .arg(tal_path)
184                    .arg(rom_path)
185                    .output()
186            } else {
187                Command::new("wsl")
188                    .arg("uxncli")
189                    .arg(drifblim_rom)
190                    .arg(tal_path)
191                    .arg(rom_path)
192                    .output()
193            }
194            .map_err(|e| syntax_err(rom_path, &format!("drifblim failed: {e}")))?
195        } else {
196            Command::new("uxncli")
197                .arg(drifblim_rom)
198                .arg(tal_path)
199                .arg(rom_path)
200                .output()
201                .map_err(|e| syntax_err(rom_path, &format!("drifblim failed: {e}")))?
202        };
203        println!("drifblim stdout: {:?}", output);
204        if !output.stderr.is_empty() {
205            let stderr_str = String::from_utf8_lossy(&output.stderr);
206            if stderr_str.contains("Assembled ") {
207                // Write tal_file to a temp file with a short name
208                println!("Path exceeded error from drifblim, retrying with short path");
209                // let tal_path_rel = Path::new(tal_path)
210                //     .file_name()
211                //     .map(|f| f.to_string_lossy().to_string())
212                //     .unwrap_or_else(|| "temp.tal".to_string());
213                // let temp_path = format!("./{}", tal_path_rel);
214                // let rom_path = format!("{}_{}.rom", temp_path, "drifblim");
215                // let tal_path = wslpath::wsl_to_windows(tal_path)
216                //     .or_else(|_| Err(syntax_err(&rom_path, "Could not convert TAL path to WSL")))?;
217                // fs::write(&temp_path, tal_file.as_bytes()).map_err(|e| syntax_err(&temp_path, &format!("Failed to write to temp path: {e}")))?;
218                // return Self::run_drif(&temp_path, &tal_file, &rom_path);
219            } else {
220                return Err(syntax_err(
221                    rom_path,
222                    &format!(
223                        "drifblim stderr: {}",
224                        String::from_utf8_lossy(&output.stderr)
225                    ),
226                ));
227            }
228        }
229        Ok(String::from_utf8_lossy(&output.stdout).to_string())
230    }
231}
232
233pub struct DrifblimSeedBackend;
234impl AssemblerBackend for DrifblimSeedBackend {
235    fn name(&self) -> &'static str {
236        "drifseed"
237    }
238    fn assemble(&self, tal_file: &str, _src: &str) -> Result<AssemblyOutput, AssemblerError> {
239        // let mut temp_file = tempfile::NamedTempFile::new().map_err(|e| syntax_err(tal_file, &format!("tempfile error: {e}")))?;
240        // temp_file.write_all(_src.as_bytes()).map_err(|e| syntax_err(tal_file, &format!("tempfile write error: {e}")))?;
241        // let tal_path = temp_file.path().to_string_lossy().to_string();
242        // let tal_path = tal_path.replace('\\', "/");
243        // let rom_path = format!("{}_{}.rom", tal_path, self.name());
244        // println!("rom_path: {}", rom_path);
245        let tal_file = Path::new(tal_file)
246            .canonicalize()
247            .ok()
248            .and_then(|abs| {
249                std::env::current_dir()
250                    .ok()
251                    .and_then(|cwd| abs.strip_prefix(&cwd).ok().map(|rel| rel.to_path_buf()))
252                    .or(Some(abs))
253            })
254            .unwrap_or_else(|| Path::new(tal_file).to_path_buf());
255        let cwd_wsl = std::env::current_dir()
256            .ok()
257            .and_then(|cwd| wslpath::windows_to_wsl(&cwd.display().to_string()).ok())
258            .unwrap_or_else(|| ".".to_string());
259        let input = if cfg!(windows) {
260            let input = wslpath::windows_to_wsl(&tal_file.display().to_string()).map_err(|e| {
261                syntax_err(
262                    &tal_file.display().to_string(),
263                    &format!("Could not convert TAL path to WSL: {e}"),
264                )
265            })?;
266            if input.starts_with(&cwd_wsl) {
267                input[cwd_wsl.len()..].trim_start_matches('/').to_string()
268            } else {
269                input
270            }
271        } else {
272            tal_file.display().to_string()
273        };
274        //let input = tal_file;//.replace("\\\\?\\", ""); // Handle Windows long path prefix
275        let rom_path = format!("{}_{}.rom", input, self.name());
276        println!("cwd_wsl: {}", cwd_wsl);
277        println!("rom_path: {}", rom_path);
278        println!("tal_file: {}", tal_file.display());
279        println!("input: {}", input);
280        // std::process::exit(0);
281        let stdout = Self::run_drif(&input, &input, &rom_path)?;
282        let rom_path = if cfg!(windows) {
283            wslpath::wsl_to_windows(&rom_path).map_err(|e| {
284                syntax_err(
285                    &rom_path,
286                    &format!("Could not convert ROM path to Windows: {e}"),
287                )
288            })?
289        } else {
290            rom_path.to_string()
291        };
292        let bytes = fs::read(&rom_path).unwrap_or_default();
293        println!("rom_path: {}", rom_path);
294
295        if bytes.is_empty() {
296            return Err(syntax_err(&rom_path, "drifblim produced empty ROM"));
297        }
298
299        Ok(AssemblyOutput {
300            rom_path: rom_path.clone(),
301            rom_bytes: bytes,
302            stdout,
303            disassembly: run_dis_file(&rom_path)?,
304        })
305    }
306}
307impl DrifblimSeedBackend {
308    fn run_drif(tal_path: &str, _tal_file: &str, rom_path: &str) -> Result<String, AssemblerError> {
309        println!("tal_path: {}", tal_path);
310        println!("rom_path: {}", rom_path);
311        let drifblim_rom = crate::bkend_drif::drifblim_repo_get_drifblim_seed();
312        let drifblim_rom = if cfg!(windows) {
313            wslpath::windows_to_wsl(&drifblim_rom.to_string_lossy())
314                .map_err(|_| syntax_err(rom_path, "Could not convert drifblim path to WSL"))?
315        } else {
316            drifblim_rom.to_string_lossy().to_string()
317        };
318        let in_wsl = detect_wsl();
319        let output = if in_wsl || !cfg!(windows) {
320            Command::new("uxncli")
321                .arg(drifblim_rom)
322                .arg(tal_path)
323                .arg(rom_path)
324                .output()
325        } else {
326            Command::new("wsl")
327                .arg("uxncli")
328                .arg(drifblim_rom)
329                .arg(tal_path)
330                .arg(rom_path)
331                .output()
332        }
333        .map_err(|e| syntax_err(rom_path, &format!("drifblim failed: {e}")))?;
334        println!("drifblim stdout: {:?}", output);
335        if !output.stderr.is_empty() {
336            let stderr_str = String::from_utf8_lossy(&output.stderr);
337            if stderr_str.contains("Assembled ") {
338                // Write tal_file to a temp file with a short name
339                // println!("Path exceeded error from drifblim, retrying with short path");
340                // let tal_path_rel = Path::new(tal_path)
341                //     .file_name()
342                //     .map(|f| f.to_string_lossy().to_string())
343                //     .unwrap_or_else(|| "temp.tal".to_string());
344                // let temp_path = format!("./{}", tal_path_rel);
345                // let rom_path = format!("{}_{}.rom", temp_path, "drifblim");
346                // let tal_path = wslpath::wsl_to_windows(tal_path)
347                //     .or_else(|_| Err(syntax_err(&rom_path, "Could not convert TAL path to WSL")))?;
348                // fs::write(&temp_path, tal_file.as_bytes()).map_err(|e| syntax_err(&temp_path, &format!("Failed to write to temp path: {e}")))?;
349                // return Self::run_drif(&temp_path, &tal_file, &rom_path);
350            } else {
351                return Err(syntax_err(
352                    rom_path,
353                    &format!(
354                        "drifblim stderr: {}",
355                        String::from_utf8_lossy(&output.stderr)
356                    ),
357                ));
358            }
359        }
360        Ok(String::from_utf8_lossy(&output.stdout).to_string())
361    }
362}
363
364pub struct DrifloonBackend;
365impl AssemblerBackend for DrifloonBackend {
366    fn name(&self) -> &'static str {
367        "drifloon"
368    }
369    fn assemble(&self, tal_file: &str, _src: &str) -> Result<AssemblyOutput, AssemblerError> {
370        // let mut temp_file = tempfile::NamedTempFile::new().map_err(|e| syntax_err(tal_file, &format!("tempfile error: {e}")))?;
371        // temp_file.write_all(_src.as_bytes()).map_err(|e| syntax_err(tal_file, &format!("tempfile write error: {e}")))?;
372        // let tal_path = temp_file.path().to_string_lossy().to_string();
373        // let tal_path = tal_path.replace('\\', "/");
374        // let rom_path = format!("{}_{}.rom", tal_path, self.name());
375        // println!("rom_path: {}", rom_path);
376
377        let input = tal_file.replace('\\', "/");
378        let input = input.replace("//?/C:", "/mnt/c"); // Handle Windows long path prefix
379                                                       //let input = tal_file;//.replace("\\\\?\\", ""); // Handle Windows long path prefix
380        let rom_path = format!("{}_{}.rom", input, self.name());
381        println!("rom_path: {}", rom_path);
382        println!("tal_file: {}", tal_file);
383        println!("input: {}", input);
384        let stdout = Self::run_drifloon(&input, _src, &rom_path)?;
385        let rom_path = rom_path.replace("/mnt/c/", "C:/"); // Handle Windows long path prefix
386        let rom_path = rom_path.replace('/', "\\");
387        let bytes = fs::read(&rom_path).unwrap_or_default();
388        if bytes.is_empty() {
389            return Err(syntax_err(&rom_path, "drifloon produced empty ROM"));
390        }
391        Ok(AssemblyOutput {
392            rom_path: rom_path.clone(),
393            rom_bytes: bytes,
394            stdout,
395            disassembly: run_dis_file(&rom_path)?,
396        })
397    }
398}
399impl DrifloonBackend {
400    fn run_drifloon(
401        tal_path: &str,
402        tal_file: &str,
403        rom_path: &str,
404    ) -> Result<String, AssemblerError> {
405        println!("tal_path: {}", tal_path);
406        println!("rom_path: {}", rom_path);
407        let home_dir = dirs::home_dir()
408            .ok_or_else(|| syntax_err(rom_path, "Could not determine home directory"))?;
409        let uxntal_path = home_dir.join(".uxntal");
410        let drifblim_path = uxntal_path.join(".drifblim");
411        let drifloon_path = drifblim_path.join("src").join("drifloon.rom");
412        let drifloon_path = if cfg!(target_os = "windows") {
413            wslpath::windows_to_wsl(&drifloon_path.to_string_lossy())
414                .map_err(|_| syntax_err(rom_path, "Could not convert drifloon path to WSL"))?
415        } else {
416            drifloon_path.to_string_lossy().to_string()
417        };
418        let output = if cfg!(target_os = "windows") {
419            let in_wsl = detect_wsl();
420            if in_wsl {
421                let mut child = Command::new("uxncli")
422                    .arg(&drifloon_path)
423                    .stdin(std::process::Stdio::piped())
424                    .stdout(std::process::Stdio::piped())
425                    .stderr(std::process::Stdio::piped())
426                    .spawn()
427                    .map_err(|e| syntax_err(rom_path, &format!("drifloon failed to spawn: {e}")))?;
428
429                if let Some(mut stdin) = child.stdin.take() {
430                    stdin.write_all(tal_file.as_bytes()).map_err(|e| {
431                        syntax_err(rom_path, &format!("drifloon failed to write to stdin: {e}"))
432                    })?;
433                }
434                child.wait_with_output()
435            } else {
436                let mut child = Command::new("wsl")
437                    .arg("uxncli")
438                    .arg(&drifloon_path)
439                    .stdin(std::process::Stdio::piped())
440                    .stdout(std::process::Stdio::piped())
441                    .stderr(std::process::Stdio::piped())
442                    .spawn()
443                    .map_err(|e| syntax_err(rom_path, &format!("drifloon failed to spawn: {e}")))?;
444                if let Some(mut stdin) = child.stdin.take() {
445                    stdin.write_all(tal_file.as_bytes()).map_err(|e| {
446                        syntax_err(rom_path, &format!("drifloon failed to write to stdin: {e}"))
447                    })?;
448                }
449                child.wait_with_output()
450            }
451            .map_err(|e| syntax_err(rom_path, &format!("drifloon failed: {e}")))?
452        } else {
453            let mut child = Command::new("uxncli")
454                .arg(&drifloon_path)
455                .stdin(std::process::Stdio::piped())
456                .stdout(std::process::Stdio::piped())
457                .stderr(std::process::Stdio::piped())
458                .spawn()
459                .map_err(|e| syntax_err(rom_path, &format!("drifloon failed to spawn: {e}")))?;
460            if let Some(mut stdin) = child.stdin.take() {
461                stdin.write_all(tal_file.as_bytes()).map_err(|e| {
462                    syntax_err(rom_path, &format!("drifloon failed to write to stdin: {e}"))
463                })?;
464            }
465            child
466                .wait_with_output()
467                .map_err(|e| syntax_err(rom_path, &format!("drifloon failed: {e}")))?
468        };
469        println!("drifloon path: {}", &drifloon_path);
470        println!("drifloon stdout: {:?}", output);
471        println!("drifloon stderr: {:?}", output.stderr);
472        println!("rom_path: {}", rom_path);
473        // Write output to rom_path
474        if !output.stdout.is_empty() {
475            let rom_path = if cfg!(windows) {
476                wslpath::wsl_to_windows(rom_path).map_err(|e| {
477                    syntax_err(
478                        rom_path,
479                        &format!("Could not convert ROM path to Windows: {e}"),
480                    )
481                })?
482            } else {
483                rom_path.to_string()
484            };
485            fs::write(&rom_path, &output.stdout)
486                .map_err(|e| syntax_err(&rom_path, &format!("Failed to write ROM: {e}")))?;
487        }
488
489        // println!("{:?}", tal_file);
490        if !output.stderr.is_empty() && output.stdout.is_empty() {
491            return Err(syntax_err(
492                rom_path,
493                &format!(
494                    "drifloon stderr: {}",
495                    String::from_utf8_lossy(&output.stderr)
496                ),
497            ));
498        }
499        Ok(String::from_utf8_lossy(&output.stdout).to_string())
500    }
501}
502
503fn detect_wsl() -> bool {
504    std::env::var("WSL_DISTRO_NAME").is_ok()
505        || std::env::var("WSLENV").is_ok()
506        || (Path::new("/proc/version").exists()
507            && fs::read_to_string("/proc/version")
508                .unwrap_or_default()
509                .to_lowercase()
510                .contains("microsoft"))
511}
512
513fn spawn_cmd(cmd: &str, args: &[&str]) -> Command {
514    if cfg!(not(windows)) {
515        let mut command = Command::new(cmd);
516        command.args(args);
517        return command;
518    }
519    let in_wsl = detect_wsl();
520    if in_wsl {
521        let mut command = Command::new(cmd);
522        command.args(args);
523        command
524    } else {
525        let mut all = vec![cmd];
526        all.extend(args);
527        let mut command = Command::new("wsl");
528        command.args(all);
529        command
530    }
531}
532
533fn emu_err(path: &str, msg: &str) -> AssemblerError {
534    AssemblerError::Backend {
535        message: format!("{path}: {msg}"),
536    }
537}
538
539fn run_vm_last(bytes: &[u8]) -> Result<String, AssemblerError> {
540    let tmp = ".__temp_uxntal_exec.rom";
541    fs::write(tmp, bytes).map_err(|e| io_err(tmp, e))?;
542    let out = run_uxncli_get_stdout(tmp)?;
543    let _ = fs::remove_file(tmp);
544    Ok(out)
545}
546
547pub fn run_uxncli_get_stdout(rom_path: &str) -> Result<String, AssemblerError> {
548    if cfg!(windows) {
549        let in_wsl = crate::wsl::detect_wsl();
550        let output = if in_wsl {
551            Command::new("uxncli").arg(rom_path).output()
552        } else {
553            Command::new("wsl").arg("uxncli").arg(rom_path).output()
554        }
555        .map_err(|e| emu_err(rom_path, &format!("uxncli failed: {e}")))?;
556        Ok(String::from_utf8_lossy(&output.stdout).to_string())
557    } else {
558        let output = Command::new("uxncli")
559            .arg(rom_path)
560            .output()
561            .map_err(|e| emu_err(rom_path, &format!("uxncli failed: {e}")))?;
562        Ok(String::from_utf8_lossy(&output.stdout).to_string())
563    }
564}
565
566fn syntax_err(path: &str, msg: &str) -> AssemblerError {
567    AssemblerError::SyntaxError {
568        path: path.to_string(),
569        line: 0,
570        position: 0,
571        message: msg.to_string(),
572        source_line: String::new(),
573    }
574}
575
576fn io_err(path: &str, e: std::io::Error) -> AssemblerError {
577    syntax_err(path, &format!("IO error: {e}"))
578}
579
580#[derive(Default)]
581pub struct DebugAssembler {
582    pub drif_mode: bool,
583}
584
585impl DebugAssembler {
586    pub fn new() -> Self {
587        Self { drif_mode: false }
588    }
589
590    pub fn with_drif_mode(drif_mode: bool) -> Self {
591        Self { drif_mode }
592    }
593
594    pub fn assemble_and_compare(
595        &self,
596        tal_file: &str,
597        tal_source: &str,
598        verbose: bool,
599    ) -> Result<DebugAssembleResult, AssemblerError> {
600        let backends: Vec<Box<dyn AssemblerBackend>> = vec![
601            Box::new(UxntalBackend::with_drif_mode(self.drif_mode)),
602            Box::new(DrifloonBackend),
603            Box::new(UxnasmBackend),
604            Box::new(DrifblimBackend),
605            Box::new(DrifblimSeedBackend),
606            Box::new(crate::bkend_buxn::UxnBuxnBackend),
607            Box::new(crate::bkend_uxn::UxnDUxnAsmBackend),
608        ];
609        let mut uxntal = None;
610        let mut uxnasm = None;
611        let mut drif = None;
612        let mut drifloon = None;
613        let mut drifblim_seed = None;
614        let mut buxn = None;
615        let mut uxn38 = None;
616        let mut duxnasm = None;
617        let mut backend_errors = Vec::new();
618
619        for b in backends {
620            match b.assemble(tal_file, tal_source) {
621                Ok(out) => {
622                    // Always write .sym file for each backend's ROM if possible
623                    let sym_path = format!("{}.sym", out.rom_path);
624                    if !out.rom_bytes.is_empty() {
625                        // Use the internal assembler for symbol generation if possible
626                        if b.name() == "uxntal" {
627                            let mut assembler = if self.drif_mode {
628                                Assembler::with_drif_mode(true)
629                            } else {
630                                Assembler::new()
631                            };
632                            if assembler
633                                .assemble(tal_source, Some(tal_file.to_string()))
634                                .is_ok()
635                            {
636                                let sym_bytes = assembler.generate_symbol_file();
637                                let _ = fs::write(&sym_path, &sym_bytes);
638                            }
639                        }
640                    }
641                    match b.name() {
642                        "uxntal" => uxntal = Some(out),
643                        "uxnasm" => uxnasm = Some(out),
644                        "drifblim" => drif = Some(out),
645                        "drifloon" => drifloon = Some(out),
646                        "drifseed" => drifblim_seed = Some(out),
647                        "buxn" => buxn = Some(out),
648                        "uxn38" => uxn38 = Some(out),
649                        "duxnasm" => duxnasm = Some(out),
650                        _ => {}
651                    }
652                }
653                Err(e) => backend_errors.push((b.name().to_string(), e.to_string())),
654            }
655        }
656
657        // Print summary and diffs for all three backends, even if one fails
658        if verbose {
659            println!("\n== Backend Summary ==");
660            println!(
661                "  {:<9} {:<5} {:>8}  output/summary",
662                "backend", "stat", "bytes"
663            );
664            for (name, out) in [
665                ("uxntal", &uxntal),
666                ("uxnasm", &uxnasm),
667                ("drifblim", &drif),
668                ("drifloon", &drifloon),
669                ("drifseed", &drifblim_seed),
670                ("buxn", &buxn),
671                ("uxn38", &uxn38),
672                ("duxnasm", &duxnasm),
673            ] {
674                if let Some(ref o) = out {
675                    println!(
676                        "  {:<9} {:<5} {:>8}  {}",
677                        name,
678                        "OK",
679                        o.rom_bytes.len(),
680                        o.rom_path
681                    );
682                } else {
683                    let summary = backend_errors
684                        .iter()
685                        .find(|(n, _)| n == name)
686                        .map(|(_, e)| e.as_str())
687                        .unwrap_or("-");
688                    println!("  {:<9} {:<5} {:>8}  {}", name, "FAIL", 0, summary);
689                }
690            }
691
692            // Print byte diffs for all pairs
693            println!("\n== Byte Diffs ==");
694            let pairs = [
695                ("uxntal", &uxntal, "uxnasm", &uxnasm),
696                ("uxntal", &uxntal, "drifblim", &drif),
697                ("uxnasm", &uxnasm, "drifblim", &drif),
698                ("uxntal", &uxntal, "drifloon", &drifloon),
699                ("uxnasm", &uxnasm, "drifloon", &drifloon),
700                ("drifblim", &drif, "drifloon", &drifloon),
701                ("drifblim", &drif, "drifseed", &drifblim_seed),
702                ("uxntal", &uxntal, "drifseed", &drifblim_seed),
703                ("uxnasm", &uxnasm, "drifseed", &drifblim_seed),
704                ("drifloon", &drifloon, "drifseed", &drifblim_seed),
705                ("buxn", &buxn, "uxntal", &uxntal),
706                ("buxn", &buxn, "uxnasm", &uxnasm),
707                ("buxn", &buxn, "drifblim", &drif),
708                ("buxn", &buxn, "drifloon", &drifloon),
709                ("buxn", &buxn, "drifseed", &drifblim_seed),
710                ("uxn38", &uxn38, "uxntal", &uxntal),
711                ("uxn38", &uxn38, "uxnasm", &uxnasm),
712                ("uxn38", &uxn38, "drifblim", &drif),
713                ("uxn38", &uxn38, "drifloon", &drifloon),
714                ("uxn38", &uxn38, "drifseed", &drifblim_seed),
715                ("uxn38", &uxn38, "buxn", &buxn),
716                ("duxnasm", &duxnasm, "uxntal", &uxntal),
717                ("duxnasm", &duxnasm, "uxnasm", &uxnasm),
718                ("duxnasm", &duxnasm, "drifblim", &drif),
719                ("duxnasm", &duxnasm, "drifloon", &drifloon),
720                ("duxnasm", &duxnasm, "drifseed", &drifblim_seed),
721                ("duxnasm", &duxnasm, "buxn", &buxn),
722                ("duxnasm", &duxnasm, "uxn38", &uxn38),
723            ];
724            for (a_name, a, b_name, b) in pairs {
725                print!("{:<8} vs {:<9}: ", a_name, b_name);
726                if let (Some(ref a), Some(ref b)) = (a, b) {
727                    if let Some(d) = first_byte_diff(&a.rom_bytes, &b.rom_bytes) {
728                        println!(
729                            "first diff at 0x{:04X}: {:02X} != {:02X}",
730                            d.index, d.a, d.b
731                        );
732                    } else {
733                        println!("identical");
734                    }
735                } else {
736                    println!("skipped (missing backend)");
737                }
738            }
739
740            // Print disassembly diffs for all pairs
741            println!("\n== Disassembly Diffs (first differing line) ==");
742            let dis_pairs = [
743                ("uxntal", &uxntal, "uxnasm", &uxnasm),
744                ("uxntal", &uxntal, "drifblim", &drif),
745                ("uxnasm", &uxnasm, "drifblim", &drif),
746                ("uxntal", &uxntal, "drifloon", &drifloon),
747                ("uxnasm", &uxnasm, "drifloon", &drifloon),
748                ("drifblim", &drif, "drifloon", &drifloon),
749                ("drifblim", &drif, "drifseed", &drifblim_seed),
750                ("uxntal", &uxntal, "drifseed", &drifblim_seed),
751                ("uxnasm", &uxnasm, "drifseed", &drifblim_seed),
752                ("drifloon", &drifloon, "drifseed", &drifblim_seed),
753                ("buxn", &buxn, "uxntal", &uxntal),
754                ("buxn", &buxn, "uxnasm", &uxnasm),
755                ("buxn", &buxn, "drifblim", &drif),
756                ("buxn", &buxn, "drifloon", &drifloon),
757                ("buxn", &buxn, "drifseed", &drifblim_seed),
758                ("uxn38", &uxn38, "uxntal", &uxntal),
759                ("uxn38", &uxn38, "uxnasm", &uxnasm),
760                ("uxn38", &uxn38, "drifblim", &drif),
761                ("uxn38", &uxn38, "drifloon", &drifloon),
762                ("uxn38", &uxn38, "drifseed", &drifblim_seed),
763                ("uxn38", &uxn38, "buxn", &buxn),
764                ("duxnasm", &duxnasm, "uxntal", &uxntal),
765                ("duxnasm", &duxnasm, "uxnasm", &uxnasm),
766                ("duxnasm", &duxnasm, "drifblim", &drif),
767                ("duxnasm", &duxnasm, "drifloon", &drifloon),
768                ("duxnasm", &duxnasm, "drifseed", &drifblim_seed),
769                ("duxnasm", &duxnasm, "buxn", &buxn),
770                ("duxnasm", &duxnasm, "uxn38", &uxn38),
771            ];
772            for (a_name, a, b_name, b) in dis_pairs {
773                print!("{:<8} vs {:<9}: ", a_name, b_name);
774                if let (Some(ref a), Some(ref b)) = (a, b) {
775                    if a.disassembly.is_empty() {
776                        println!("skipped (empty disassembly from {})", a_name);
777                        continue;
778                    }
779                    if b.disassembly.is_empty() {
780                        println!("skipped (empty disassembly from {})", b_name);
781                        continue;
782                    }
783                    match diff_first(&a.disassembly, &b.disassembly) {
784                        Some((ln, la, lb)) => {
785                            println!("line {}:\n  {}: {}\n  {}: {}", ln, a_name, la, b_name, lb);
786                        }
787                        None => {
788                            let lines: Vec<_> = a.disassembly.lines().take(3).collect();
789                            if lines.is_empty() {
790                                println!("identical (no disassembly output)");
791                            } else {
792                                println!("identical");
793                                // for l in lines {
794                                //     println!("    {}", l);
795                                // }
796                                // if a.disassembly.lines().count() > 3 {
797                                //     println!("    ...");
798                                // }
799                                // println!(
800                                //     "identical (first lines):\n    {}: {}\n    {}: {}",
801                                //     a_name, a.rom_path, b_name, b.rom_path
802                                // );
803                            }
804                        }
805                    }
806                } else {
807                    println!("skipped (missing backend)");
808                }
809            }
810
811            // Print backend errors if any
812            if !backend_errors.is_empty() {
813                println!("\n== Backend Errors ==");
814                for (name, err) in &backend_errors {
815                    println!("{}: {}", name, err);
816                }
817            }
818        }
819
820        // Prepare summary fields
821        let uxntal_rom_path = uxntal
822            .as_ref()
823            .map(|x| x.rom_path.clone())
824            .unwrap_or_default();
825        let uxnasm_rom_path = uxnasm
826            .as_ref()
827            .map(|x| x.rom_path.clone())
828            .unwrap_or_default();
829        let drifblim_rom_path = drif
830            .as_ref()
831            .map(|x| x.rom_path.clone())
832            .unwrap_or_default();
833        let uxntal_rom_bytes = uxntal
834            .as_ref()
835            .map(|x| x.rom_bytes.clone())
836            .unwrap_or_default();
837        let uxnasm_rom_bytes = uxnasm
838            .as_ref()
839            .map(|x| x.rom_bytes.clone())
840            .unwrap_or_default();
841        let drifblim_rom_bytes = drif
842            .as_ref()
843            .map(|x| x.rom_bytes.clone())
844            .unwrap_or_default();
845        let uxntal_output = uxntal
846            .as_ref()
847            .map(|x| x.stdout.clone())
848            .unwrap_or_default();
849        let uxnasm_output = uxnasm
850            .as_ref()
851            .map(|x| x.stdout.clone())
852            .unwrap_or_default();
853        let drifblim_output = drif.as_ref().map(|x| x.stdout.clone()).unwrap_or_default();
854        let uxntal_dis_output = uxntal
855            .as_ref()
856            .map(|x| x.disassembly.clone())
857            .unwrap_or_default();
858        let uxnasm_dis_output = uxnasm
859            .as_ref()
860            .map(|x| x.disassembly.clone())
861            .unwrap_or_default();
862        let drifblim_dis_output = drif
863            .as_ref()
864            .map(|x| x.disassembly.clone())
865            .unwrap_or_default();
866
867        // Disassembly diffs for all pairs
868        let diff_uxntal_uxnasm = diff_first(&uxntal_dis_output, &uxnasm_dis_output);
869        let diff_uxntal_drifblim = diff_first(&uxntal_dis_output, &drifblim_dis_output);
870        let diff_uxnasm_drifblim = diff_first(&uxnasm_dis_output, &drifblim_dis_output);
871
872        Ok(DebugAssembleResult {
873            uxntal_rom_path,
874            uxnasm_rom_path,
875            drifblim_rom_path,
876            uxntal_rom_bytes,
877            uxnasm_rom_bytes,
878            drifblim_rom_bytes,
879            uxntal_output,
880            uxnasm_output,
881            drifblim_output,
882            uxntal_dis_output,
883            uxnasm_dis_output,
884            drifblim_dis_output,
885            diff_uxntal_uxnasm,
886            diff_uxntal_drifblim,
887            diff_uxnasm_drifblim,
888            backend_errors,
889        })
890    }
891}
892
893fn diff_first(a: &str, b: &str) -> Option<(usize, String, String)> {
894    for (i, (la, lb)) in a.lines().zip(b.lines()).enumerate() {
895        if la != lb {
896            // Check if the difference is only in symbol comments (parts in parentheses)
897            if let (Some(a_without_comment), Some(b_without_comment)) =
898                (strip_symbol_comment(la), strip_symbol_comment(lb))
899            {
900                if a_without_comment == b_without_comment {
901                    // Only symbol comment differs, skip this difference
902                    continue;
903                }
904            }
905            return Some((i + 1, la.to_string(), lb.to_string()));
906        }
907    }
908    let ac = a.lines().count();
909    let bc = b.lines().count();
910    if ac > bc {
911        a.lines()
912            .nth(bc)
913            .map(|extra| (bc + 1, extra.to_string(), String::new()))
914    } else if bc > ac {
915        b.lines()
916            .nth(ac)
917            .map(|extra| (ac + 1, String::new(), extra.to_string()))
918    } else {
919        None
920    }
921}
922
923/// Strip symbol comments from disassembly lines to compare only the actual bytecode
924/// Returns the line without the comment part if it has one, otherwise returns the original line
925fn strip_symbol_comment(line: &str) -> Option<&str> {
926    if let Some(comment_start) = line.find('(') {
927        if let Some(comment_end) = line.rfind(')') {
928            if comment_start < comment_end {
929                // Extract the part before the comment and trim whitespace
930                return Some(line[..comment_start].trim_end());
931            }
932        }
933    }
934    Some(line)
935}
936
937fn first_byte_diff(a: &[u8], b: &[u8]) -> Option<ByteDiff> {
938    let len = std::cmp::min(a.len(), b.len());
939    for i in 0..len {
940        if a[i] != b[i] {
941            let rom_address = i + 0x100; // Convert file offset to ROM address
942                                         // Add debug for the specific problematic offset
943            if rom_address == 0x0B48 {
944                eprintln!("DEBUG: [Byte comparison] At ROM address 0x{:04X} (file offset 0x{:04X}): a[{}]=0x{:02X}, b[{}]=0x{:02X}", rom_address, i, i, a[i], i, b[i]);
945                // Show context around the problematic byte
946                let start = (i as i32 - 8).max(0) as usize;
947                let end = (i + 8).min(len);
948                eprint!("DEBUG: [Context a]: ");
949                for (j, val) in a.iter().enumerate().take(end).skip(start) {
950                    if j == i {
951                        eprint!("[{:02X}] ", val);
952                    } else {
953                        eprint!("{:02X} ", val);
954                    }
955                }
956                eprintln!();
957                eprint!("DEBUG: [Context b]: ");
958                for (j, val) in b.iter().enumerate().take(end).skip(start) {
959                    if j == i {
960                        eprint!("[{:02X}] ", val);
961                    } else {
962                        eprint!("{:02X} ", val);
963                    }
964                }
965                eprintln!();
966            }
967            return Some(ByteDiff {
968                index: rom_address, // Report ROM address, not file offset
969                a: a[i],
970                b: b[i],
971            });
972        }
973    }
974    if a.len() != b.len() {
975        Some(ByteDiff {
976            index: len,
977            a: if a.len() > len { a[len] } else { 0 },
978            b: if b.len() > len { b[len] } else { 0 },
979        })
980    } else {
981        None
982    }
983}
984
985struct ByteDiff {
986    index: usize,
987    a: u8,
988    b: u8,
989}
990
991// debug_preprocess.rs: Compare chocolatal (Rust) vs deluge (Docker) preprocessors
992//
993// This module provides a CLI and helpers to run both preprocessors on the same input,
994// diff the results, and print diagnostics for debugging and regression testing.
995
996use crate::chocolatal::{deluge_preprocess, preprocess};
997use std::io::{self, Write};
998use std::path::PathBuf;
999
1000/// Run both preprocessors on the given file and print a diff.
1001pub fn compare_preprocessors(input_path: &str, root_dir: &PathBuf) -> io::Result<()> {
1002    // Read input file
1003    let input = fs::read_to_string(input_path)?;
1004    // Run chocolatal (Rust)
1005    let rust_out = match preprocess(&input, input_path, root_dir) {
1006        Ok(s) => s,
1007        Err(e) => format!("[chocolatal error: {:?}]\n", e),
1008    };
1009    let rust_path = "chocolatal.tal";
1010    let mut file = fs::File::create(rust_path)?;
1011    file.write_all(rust_out.as_bytes())?;
1012    // Run deluge (Docker)
1013    let deluge_out = deluge_preprocess(input_path)?;
1014    let deluge_pp_path = "deluge_pp_cmp.tal";
1015    let mut file = fs::File::create(deluge_pp_path)?;
1016    file.write_all(deluge_out.as_bytes())?;
1017    // Diff the outputs (simple line-by-line)
1018    let rust_lines: Vec<_> = rust_out.lines().collect();
1019    let deluge_lines: Vec<_> = deluge_out.lines().collect();
1020    let max = rust_lines.len().max(deluge_lines.len());
1021    let mut found = false;
1022    for i in 0..max {
1023        let r = rust_lines.get(i).map_or("", |v| v);
1024        let d = deluge_lines.get(i).map_or("", |v| v);
1025        if r != d {
1026            println!(
1027                "First difference at line {}:{}:\n  chocolatal : {}\n  deluge_pp  : {}\n{}:{}:\n",
1028                rust_path,
1029                i + 1,
1030                r,
1031                d,
1032                deluge_pp_path,
1033                i + 1
1034            );
1035            found = true;
1036            break;
1037        }
1038    }
1039    if !found {
1040        println!("No differences found.");
1041    }
1042    Ok(())
1043}