Skip to main content

uxn_tal/
lib.rs

1/// # UXN TAL Assembler
2///
3/// A Rust library for assembling TAL (Tal Assembly Language) files into UXN ROM files.
4///
5/// This library provides functionality to parse TAL source code and generate bytecode
6/// compatible with the UXN virtual machine.
7///
8/// ## Basic Assembly Example
9///
10/// ```rust
11/// use uxn_tal::{Assembler, AssemblerError};
12///
13/// fn main() -> Result<(), AssemblerError> {
14///     let tal_source = r#"
15///         |0100 @reset
16///             #48 #65 #6c #6c #6f #20 #57 #6f #72 #6c #64 #21 #0a
17///             #18 DEO
18///         BRK
19///     "#;
20///     
21///     let mut assembler = Assembler::new();
22///     let rom = assembler.assemble(tal_source, None)?;
23///     
24///     // Save the ROM to a file
25///     std::fs::write("hello.rom", rom)?;
26///     
27///     Ok(())
28/// }
29/// ```
30///
31/// ## Protocol URL Parsing with Git Support
32///
33/// The library provides enhanced URL parsing that automatically handles git repository URLs.
34/// You can use either the familiar `ProtocolParser::parse` or the explicit `parse_uxntal_url` function:
35///
36/// ```rust
37/// use uxn_tal::{ProtocolParser, parse_uxntal_url};
38///
39/// // Option 1: Standard API with automatic git support
40/// let result = ProtocolParser::parse("uxntal://git@github.com:user/repo/tree/main/file.tal");
41///
42/// // Option 2: Explicit enhanced parsing (same result)
43/// let result = parse_uxntal_url("uxntal://git@github.com:user/repo/tree/main/file.tal");
44///
45/// if let Some(repo_ref) = &result.repo_ref {
46///     println!("Repository: {}/{}", repo_ref.owner, repo_ref.repo);
47///     println!("Branch: {}", repo_ref.branch);
48///     println!("File: {}", repo_ref.path);
49/// }
50/// ```
51type AssembleDirectoryResult = (
52    std::path::PathBuf,
53    std::path::PathBuf,
54    Option<std::path::PathBuf>,
55    usize,
56);
57pub use util::{RealRomCache, RealRomEntryResolver};
58// Re-export get_or_write_cached_rom and hash_url for downstream crates
59pub use uxn_tal_common::{get_or_write_cached_rom, hash_url};
60pub mod assembler;
61pub mod bkend;
62pub mod bkend_buxn;
63pub mod bkend_drif;
64pub mod bkend_uxn;
65pub mod bkend_uxn38;
66pub mod chocolatal;
67pub mod debug;
68pub mod devicemap;
69pub mod dis_uxndis;
70pub mod error;
71pub mod hexrev;
72pub mod lexer;
73pub mod opcode_table;
74pub mod opcodes;
75pub mod parser;
76pub mod rom;
77pub mod runes;
78pub mod wsl;
79pub use assembler::Assembler;
80pub use error::AssemblerError;
81pub mod emulator_utils;
82pub mod fetch;
83pub mod mode_basic;
84pub mod mode_orca;
85pub mod paths;
86pub mod util;
87pub use fetch::parse_uxntal_url;
88pub use fetch::resolver::resolve_entry_from_url;
89pub mod probe_runtime;
90pub mod probe_tal;
91pub mod protocol_parser;
92
93pub use uxn_tal_defined::*;
94// Shadow the base ProtocolParser with our enhanced version that includes git support
95pub use protocol_parser::ProtocolParser;
96
97pub fn assemble(source: &str) -> Result<Vec<u8>, AssemblerError> {
98    let mut a = Assembler::new();
99    a.assemble(source, None)
100}
101
102pub fn assemble_with_path(source: &str, path: &str) -> Result<Vec<u8>, AssemblerError> {
103    let mut a = Assembler::new();
104    a.assemble(source, Some(path.to_string()))
105}
106
107/// Convenience function to assemble a TAL file directly from a file path
108pub fn assemble_file<P: AsRef<std::path::Path>>(input_path: P) -> Result<Vec<u8>, AssemblerError> {
109    let source = std::fs::read_to_string(&input_path)?;
110    let mut assembler = Assembler::new();
111    let path_str = input_path.as_ref().to_string_lossy().into_owned();
112    assembler.assemble(&source, Some(path_str))
113}
114
115/// Convenience function to assemble a TAL file and save the ROM to a file
116pub fn assemble_file_to_rom<P: AsRef<std::path::Path>, Q: AsRef<std::path::Path>>(
117    input_path: P,
118    output_path: Q,
119) -> Result<usize, AssemblerError> {
120    let rom = assemble_file(input_path)?;
121    std::fs::write(&output_path, &rom)?;
122    Ok(rom.len())
123}
124
125/// Convenience function to assemble a TAL file and save ROM with same name but .rom extension
126pub fn assemble_file_auto<P: AsRef<std::path::Path>>(
127    input_path: P,
128) -> Result<(std::path::PathBuf, usize), AssemblerError> {
129    let input_path = input_path.as_ref();
130    let output_path = input_path.with_extension("rom");
131    let size = assemble_file_to_rom(input_path, &output_path)?;
132    Ok((output_path, size))
133}
134
135/// Convenience function to assemble a TAL file and generate both ROM and symbol files
136pub fn assemble_file_with_symbols<P: AsRef<std::path::Path>>(
137    input_path: P,
138) -> Result<(std::path::PathBuf, std::path::PathBuf, usize), AssemblerError> {
139    let input_path = input_path.as_ref();
140    let source = std::fs::read_to_string(input_path)?;
141    let mut assembler = Assembler::new();
142    let path_str = input_path.to_string_lossy().into_owned();
143    let rom = assembler.assemble(&source, Some(path_str))?;
144
145    // Save ROM file
146    let rom_path = input_path.with_extension("rom");
147    std::fs::write(&rom_path, &rom)?;
148
149    // Save symbol file
150    let sym_path = input_path.with_extension("sym");
151    let symbols = assembler.generate_symbol_file();
152    std::fs::write(&sym_path, &symbols)?;
153
154    Ok((rom_path, sym_path, rom.len()))
155}
156
157/// Convenience function to batch process TAL files in a directory
158pub fn assemble_directory<P: AsRef<std::path::Path>>(
159    dir_path: P,
160    generate_symbols: bool,
161) -> Result<Vec<AssembleDirectoryResult>, AssemblerError> {
162    let dir_path = dir_path.as_ref();
163    let mut results = Vec::new();
164
165    for entry in std::fs::read_dir(dir_path)? {
166        let entry = entry?;
167        let path = entry.path();
168
169        if path.extension().and_then(|s| s.to_str()) == Some("tal") {
170            if generate_symbols {
171                let (rom_path, sym_path, size) = assemble_file_with_symbols(&path)?;
172                results.push((path, rom_path, Some(sym_path), size));
173            } else {
174                let (rom_path, size) = assemble_file_auto(&path)?;
175                results.push((path, rom_path, None, size));
176            }
177        }
178    }
179
180    Ok(results)
181}
182
183pub fn assemble_with_rust_interface_module(
184    source: &str,
185    module_name: &str,
186) -> Result<(Vec<u8>, String), AssemblerError> {
187    let mut a = Assembler::new();
188    let rom = a.assemble(source, None)?;
189    let module = generate_rust_interface_module(&a, module_name);
190    Ok((rom, module))
191}
192
193pub fn generate_rust_interface_module(
194    assembler: &crate::assembler::Assembler,
195    module_name: &str,
196) -> String {
197    let mut out = String::new();
198    out.push_str("#![allow(clippy::module_inception)]\n");
199    out.push_str(&format!("pub mod {} {{\n", module_name));
200    out.push_str("    #![allow(non_upper_case_globals)]\n");
201    out.push_str("    // Auto-generated: label address & size constants\n");
202    // Address and size constants
203    for name in &assembler.symbol_order {
204        if let Some(sym) = assembler.symbols.get(name) {
205            let id = {
206                let mut s: String = name
207                    .chars()
208                    .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
209                    .collect();
210                if s.chars()
211                    .next()
212                    .map(|c| c.is_ascii_digit())
213                    .unwrap_or(false)
214                {
215                    s.insert(0, '_');
216                }
217                s.to_ascii_uppercase()
218            };
219            out.push_str(&format!(
220                "    pub const _c{}: usize = 0x{:04X};\n",
221                id, sym.address
222            ));
223            // Compute size
224            let next_addr = assembler
225                .symbol_order
226                .iter()
227                .skip_while(|n| *n != name)
228                .skip(1)
229                .filter_map(|n| assembler.symbols.get(n))
230                .map(|s| s.address)
231                .find(|&a| a > sym.address)
232                .unwrap_or(assembler.effective_length as u16);
233            let size = next_addr.saturating_sub(sym.address);
234            out.push_str(&format!(
235                "    pub const _c{}_SIZE: usize = 0x{:04X};\n",
236                id, size
237            ));
238        }
239    }
240    // Helper function to get a slice for a label
241    out.push_str(
242        r#"
243    /// Returns a slice of RAM for a label by name (address, size)
244    pub fn get_slice<'a>(ram: &'a [u8], label: &str) -> Option<&'a [u8]> {
245        match label {
246"#,
247    );
248    for name in &assembler.symbol_order {
249        if let Some(_sym) = assembler.symbols.get(name) {
250            let id = {
251                let mut s: String = name
252                    .chars()
253                    .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
254                    .collect();
255                if s.chars()
256                    .next()
257                    .map(|c| c.is_ascii_digit())
258                    .unwrap_or(false)
259                {
260                    s.insert(0, '_');
261                }
262                s.to_ascii_uppercase()
263            };
264            out.push_str(&format!(
265                "            \"{name}\" => Some(&ram[_c{}.._c{}+_c{}_SIZE]),\n",
266                id, id, id
267            ));
268        }
269    }
270    out.push_str(
271        r#"            _ => None,
272        }
273    }
274"#,
275    );
276    out.push_str("}\n");
277    out
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283
284    #[test]
285    fn test_simple_assembly() {
286        let source = r#"
287            |0100
288            #42 #43 ADD BRK
289        "#;
290
291        let mut assembler = Assembler::new();
292        let rom = assembler.assemble(source, None).expect("Assembly failed");
293
294        // Should contain: LIT 0x42, LIT 0x43, ADD, BRK (6 bytes total)
295        // ROM trimming removes the 256-byte padding
296        assert_eq!(rom.len(), 6);
297        assert_eq!(rom[0], 0x80); // LIT
298        assert_eq!(rom[1], 0x42); // literal byte
299        assert_eq!(rom[2], 0x80); // LIT
300        assert_eq!(rom[3], 0x43); // literal byte
301        assert_eq!(rom[4], 0x18); // ADD opcode
302    }
303
304    #[test]
305    fn test_label_reference() {
306        let source = r#"
307            |0100 @start
308            ;data LDA2
309            BRK
310            @data #1234
311        "#;
312
313        let mut assembler = Assembler::new();
314        let rom = assembler
315            .assemble(source, Some("(test_label_reference)".to_string()))
316            .expect("Assembly failed");
317
318        // Should have label reference resolved to correct address
319        // ROM is trimmed, so no 256-byte padding
320        assert!(rom.len() > 4);
321        // ;data should generate LIT2 + 16-bit address
322        assert_eq!(rom[0], 0xa0); // LIT2
323                                  // Address of @data will be 0x100 + offset (where offset = 5 for the LIT2 + address + LDA2 + BRK)
324        let expected_addr = 0x0105_u16;
325        assert_eq!(rom[1], (expected_addr >> 8) as u8); // High byte
326        assert_eq!(rom[2], (expected_addr & 0xff) as u8); // Low byte
327    }
328
329    fn _test_instruction_modes() {
330        let source = r#"
331            |0100
332            ADD     ( base instruction )
333            ADD2    ( short mode )
334            ADDr    ( return mode )
335            ADDk    ( keep mode )
336            ADD2rk  ( all modes )
337            BRK
338        "#;
339
340        let mut assembler = Assembler::new();
341        // ROM is trimmed, so we check from index 0
342        let rom = assembler
343            .assemble(source, Some("(test_instruction_modes)".to_string()))
344            .expect("Assembly failed");
345        assert_eq!(rom[1], 0x18 | 0x20); // ADD2 (short mode)
346        assert_eq!(rom[2], 0x18 | 0x40); // ADDr (return mode)
347        assert_eq!(rom[3], 0x18 | 0x80); // ADDk (keep mode)
348        assert_eq!(rom[4], 0x18 | 0x20 | 0x40 | 0x80); // ADD2rk (all modes)
349        assert_eq!(rom[5], 0x00); // BRK
350    }
351
352    #[test]
353    fn test_hex_literals() {
354        let source = r#"
355            #12 #3456 #ab #cdef
356        "#;
357
358        let mut assembler = Assembler::new();
359        let rom = assembler
360            .assemble(source, Some("(test_hex_literals1)".to_string()))
361            .expect("Assembly failed");
362
363        // ROM is trimmed, literals become LIT + byte or LIT2 + short
364        // #12 -> LIT 0x12
365        assert_eq!(rom[0], 0x80); // LIT
366        assert_eq!(rom[1], 0x12); // byte
367        assert_eq!(rom[2], 0xa0); // LIT2
368        assert_eq!(rom[3], 0x34); // high byte
369        assert_eq!(rom[4], 0x56); // low byte
370                                  // #ab -> LIT 0xab
371        assert_eq!(rom[5], 0x80); // LIT
372        assert_eq!(rom[6], 0xab); // byte
373                                  // #cdef -> LIT2 0xcdef
374        assert_eq!(rom[7], 0xa0); // LIT2
375        assert_eq!(rom[8], 0xcd); // high byte
376        assert_eq!(rom[9], 0xef); // low byte
377    }
378
379    #[test]
380    fn test_character_literals() {
381        let source = r#"
382            |0100
383            'A 'B 'C
384        "#;
385
386        let mut assembler = Assembler::new();
387        let rom = assembler.assemble(source, None).expect("Assembly failed");
388
389        // Character literals become raw bytes (no LIT opcode)
390        assert_eq!(rom[0], b'A');
391        assert_eq!(rom[1], b'B');
392        assert_eq!(rom[2], b'C');
393    }
394
395    #[test]
396    fn test_raw_strings() {
397        let source = r#"
398            |0100
399            "Hello"
400        "#;
401
402        let mut assembler = Assembler::new();
403        let rom = assembler.assemble(source, None).expect("Assembly failed");
404
405        // Raw strings become raw bytes (ROM is trimmed)
406        assert_eq!(&rom[0..5], b"Hello");
407    }
408
409    #[test]
410    #[ignore = "reason: not sure why it fails, tbd"]
411    fn test_undefined_label_error() {
412        let source = r#"
413            |0100
414            ;undefined-label LDA2
415        "#;
416
417        let mut assembler = Assembler::new();
418        let result = assembler.assemble(source, None);
419
420        assert!(matches!(result, Err(AssemblerError::UndefinedLabel { .. })));
421    }
422
423    #[test]
424    #[ignore = "reason: not sure why it fails, tbd"]
425    fn test_duplicate_label_error() {
426        let source = r#"
427            |0100 @label
428            @label
429        "#;
430
431        let mut assembler = Assembler::new();
432        let result = assembler.assemble(source, Some("(test_duplicate_label_error)".to_owned()));
433
434        assert!(matches!(result, Err(AssemblerError::DuplicateLabel { .. })));
435    }
436
437    #[test]
438    #[ignore = "reason: not sure why it fails, tbd"]
439    fn test_unknown_opcode_error() {
440        let source = r#"
441            |0100
442            UNKNOWN
443        "#;
444
445        let mut assembler = Assembler::new();
446        let result = assembler.assemble(source, None);
447
448        assert!(matches!(result, Err(AssemblerError::UnknownOpcode { .. })));
449    }
450
451    #[test]
452    fn test_skip_directive() {
453        let source = r#"
454            |0100
455            #12
456            $04
457            #34
458        "#;
459
460        let mut assembler = Assembler::new();
461        let data = assembler
462            .assemble(source, Some("(test_skip_directive)".to_string()))
463            .unwrap();
464
465        // Should have: LIT 12, 4 zero bytes, LIT 34
466        // Starting at position 0 (after trimming padding)
467        assert_eq!(data[0], 0x80); // LIT
468        assert_eq!(data[1], 0x12); // Value
469        let _rom = assembler
470            .assemble(source, Some("(test_hex_literals)".to_string()))
471            .expect("Assembly failed");
472        assert_eq!(data[3], 0x00); // Skip byte 2
473        assert_eq!(data[4], 0x00); // Skip byte 3
474        assert_eq!(data[5], 0x00); // Skip byte 4
475        assert_eq!(data[6], 0x80); // LIT
476        assert_eq!(data[7], 0x34); // Value
477    }
478
479    #[test]
480    fn test_device_access() {
481        let source = r#"
482            |00 @System &r $2
483            |0100 @main
484                #ff .System/r DEO
485        "#;
486
487        let mut assembler = Assembler::new();
488        let data = assembler
489            .assemble(source, Some("(test_device_access)".to_string()))
490            .unwrap();
491
492        // Should generate: LIT ff, LIT 00 (System/r address), DEO
493        assert_eq!(data.len(), 5);
494        assert_eq!(data[0], 0x80); // LIT
495        assert_eq!(data[1], 0xff); // Value
496        assert_eq!(data[2], 0x80); // LIT (for device address)
497        assert_eq!(data[3], 0x00); // System/r address
498        assert_eq!(data[4], 0x17); // DEO opcode
499    }
500
501    #[test]
502    fn test_macros() {
503        let source = r#"
504            %DOUBLE { DUP ADD }
505            |0100 @main
506                #05 DOUBLE
507        "#;
508
509        let mut assembler = Assembler::new();
510        let data = assembler
511            .assemble(source, Some("(test_macros)".to_string()))
512            .unwrap();
513
514        // Should generate: LIT 05, DUP, ADD
515        assert_eq!(data.len(), 4);
516        assert_eq!(data[0], 0x80); // LIT
517        assert_eq!(data[1], 0x05); // Value
518        assert_eq!(data[2], 0x06); // DUP opcode
519        assert_eq!(data[3], 0x18); // ADD opcode
520    }
521
522    #[test]
523    fn test_inline_assembly() {
524        let source = r#"
525            |0100 @main
526                [ #05 DUP ADD ]
527        "#;
528
529        let mut assembler = Assembler::new();
530        let data = assembler
531            .assemble(source, Some("(test_inline_assembly)".to_string()))
532            .unwrap();
533
534        // Should generate: LIT 05, DUP, ADD
535        assert_eq!(data.len(), 4);
536        assert_eq!(data[0], 0x80); // LIT
537        assert_eq!(data[1], 0x05); // Value
538        assert_eq!(data[2], 0x06); // DUP opcode
539        assert_eq!(data[3], 0x18); // ADD opcode
540    }
541
542    #[test]
543    fn test_complete_tal_features() {
544        let source = r#"
545            |0100 @main
546                #41 #18 DEO
547                BRK
548        "#;
549
550        let mut assembler = Assembler::new();
551        let result = assembler.assemble(source, Some("(test_complete_tal_features)".to_string()));
552        if let Err(ref e) = result {
553            println!("Assembly error: {}", e);
554        }
555        assert!(result.is_ok(), "Complete TAL assembly should succeed");
556
557        let data = result.unwrap();
558        assert!(data.len() == 6, "Should generate some ROM data");
559
560        // Verify it starts with our expected instructions
561        assert_eq!(data[0], 0x80); // LIT
562        assert_eq!(data[1], 0x41); // Value 'A'
563        assert_eq!(data[2], 0x80); // LIT
564        assert_eq!(data[3], 0x18); // #18
565        assert_eq!(data[4], 0x17); // DEO
566    }
567}
568
569#[test]
570fn test_tal_strings_error() {
571    use crate::Assembler;
572    // TAL/UXN string equivalence to Python:
573    // guide = "TYPE \"HELP\" FOR INFO \x7f "
574    // bytes_free = " BYTES FREE\n"
575    let source = r#"&guide "TYPE 20 ""HELP" 20 "FOR 20 "INFO 20 7f 20 $1 &bytes-free 20 "BYTES 20 "FREE 0a $1"#;
576
577    // Assemble using Assembler struct to get label offsets
578    let mut assembler = Assembler::new();
579    let _ = assembler
580        .assemble(source, None)
581        .expect("Assembly should succeed");
582
583    // Get label offsets
584    let guide_offset = assembler.symbols["guide"].address as usize - 0x0100;
585    let bytes_free_offset = assembler.symbols["bytes-free"].address as usize - 0x0100;
586
587    // Get ROM bytes
588    let rom = assembler.rom.data();
589
590    // Expected bytes for guide and bytes_free
591    let expected_guide: &[u8] = b"TYPE \"HELP\" FOR INFO \x7f ";
592    let expected_bytes_free: &[u8] = b" BYTES FREE\n";
593
594    assert_eq!(
595        &rom[guide_offset..guide_offset + expected_guide.len()],
596        expected_guide
597    );
598    assert_eq!(
599        &rom[bytes_free_offset..bytes_free_offset + expected_bytes_free.len()],
600        expected_bytes_free
601    );
602}