1use std::collections::HashMap;
22
23use saturnus_core::Model;
24
25use crate::charset;
26use crate::object::Memory;
27use crate::prolog::{ObjectType, SEMI, read_field};
28
29const BANK_NIBBLES: usize = 0x40000;
32const HEADER_NIBBLES: usize = 23;
35const EMPTY_LIST: [u8; 10] = [4, 7, 0xA, 2, 0, 0xB, 2, 1, 3, 0];
37const MARKER_STRIDE: u32 = 10;
39const MAX_UNIT_ELEMENTS: usize = 40;
41const DISPATCH_CANDIDATES: usize = 12;
43const MAX_LIBRARY_COMMANDS: usize = 0x1000;
46const MAX_LIBRARIES: usize = 256;
49const MAX_COMMANDS: usize = 1 << 15;
53
54fn f5(n: &[u8], at: usize) -> Option<usize> {
56 read_field(n, at, 5).map(|v| v as usize)
57}
58
59fn rel(n: &[u8], at: usize) -> Option<usize> {
62 let off = f5(n, at)?;
63 let target = if off >= 0x80000 {
64 at.checked_sub(0x100000 - off)?
65 } else {
66 at.checked_add(off)?
67 };
68 (target < n.len()).then_some(target)
69}
70
71#[derive(Clone, Copy, Debug)]
74struct Hash {
75 names_start: usize,
76 numbers: usize,
77 count: usize,
78}
79
80fn hash_at(n: &[u8], t: usize) -> Option<Hash> {
82 if f5(n, t)? != ObjectType::BinaryInteger.prolog() as usize {
83 return None;
84 }
85 let len = f5(n, t + 5)?;
86 let end = t.checked_add(5)?.checked_add(len)?;
87 let body = t + 10;
88 if len < 5 + 85 || end > n.len() {
90 return None;
91 }
92 for i in 0..16 {
93 let at = body + 5 * i;
94 if f5(n, at)? != 0 && rel(n, at)? >= end {
95 return None;
96 }
97 }
98 let numbers = rel(n, body + 80)?;
99 if numbers < body + 85 || numbers > end || (end - numbers) % 5 != 0 {
100 return None;
101 }
102 Some(Hash {
103 names_start: body + 85,
104 numbers,
105 count: (end - numbers) / 5,
106 })
107}
108
109fn name_in(n: &[u8], h: &Hash, c: usize) -> Option<String> {
111 let at = h.numbers + 5 * c;
112 let back = f5(n, at)?;
113 if back == 0 {
114 return None;
115 }
116 let entry = at.checked_sub(back)?;
117 if entry < h.names_start {
118 return None;
119 }
120 let len = read_field(n, entry, 2)? as usize;
121 if len == 0 || entry + 2 + 2 * len + 3 > h.numbers {
122 return None;
123 }
124 let bytes: Option<Vec<u8>> = (0..len)
125 .map(|i| read_field(n, entry + 2 + 2 * i, 2).map(|b| b as u8))
126 .collect();
127 Some(charset::decode(&bytes?))
128}
129
130#[derive(Clone, Debug)]
132struct Library {
133 id: u16,
134 names: Vec<Option<String>>,
137 link: Vec<Option<usize>>,
139 arity: Vec<Option<u8>>,
142}
143
144#[derive(Clone, Copy, Debug, PartialEq, Eq)]
146pub struct CommandInfo<'a> {
147 pub library: u16,
149 pub number: u16,
151 pub name: Option<&'a str>,
153 pub arity: Option<u8>,
156 pub silent: bool,
160 pub syntax: Option<Syntax>,
163}
164
165#[derive(Clone, Copy, Debug, PartialEq, Eq)]
170pub enum Syntax {
171 Algebraic,
176 Quote,
178}
179
180const ALGEBRAIC_WORD: usize = 30;
183const QUOTE_WORD: usize = 31;
184
185#[derive(Clone, Copy, Debug, PartialEq, Eq)]
188pub struct UnitMarkers {
189 pub times: u32,
191 pub divide: u32,
193 pub power: u32,
195 pub prefix: u32,
197 pub end: u32,
199}
200
201#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
203pub struct NameStats {
204 pub libraries: usize,
206 pub names: usize,
208 pub arities: usize,
210}
211
212#[derive(Clone, Default)]
215pub struct NameTable {
216 libraries: Vec<Library>,
219 index: HashMap<u16, Vec<usize>>,
220 bank: Option<usize>,
222 units: Option<UnitMarkers>,
223 structure: Option<u16>,
225}
226
227impl std::fmt::Debug for NameTable {
228 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
229 f.debug_struct("NameTable")
230 .field("stats", &self.stats())
231 .field("units", &self.units)
232 .finish_non_exhaustive()
233 }
234}
235
236impl NameTable {
237 pub fn build(model: Model, rom: &[u8]) -> NameTable {
246 let bank = match model {
247 Model::Hp48sx | Model::Hp48gx => None,
248 Model::Hp49g => Some(BANK_NIBBLES),
249 Model::Hp38g
250 | Model::Hp39g
251 | Model::Hp40g
252 | Model::Hp42s
253 | Model::Hp27s
254 | Model::Hp17bii
255 | Model::Hp17b
256 | Model::Hp28s
257 | Model::Hp19bii => {
258 return NameTable::default();
259 }
260 };
261 let mut libraries = scan(rom, bank);
262 let dispatch = Dispatch::find(rom, &libraries);
263 for lib in &mut libraries {
264 lib.arity = lib
265 .link
266 .iter()
267 .map(|t| dispatch.as_ref().zip(*t).and_then(|(d, t)| d.arity(rom, t)))
268 .collect();
269 }
270 let mut index: HashMap<u16, Vec<usize>> = HashMap::new();
271 for (i, lib) in libraries.iter().enumerate() {
272 index.entry(lib.id).or_default().push(i);
273 }
274 let structure = libraries
275 .iter()
276 .find(|l| l.names.iter().any(|n| n.as_deref() == Some("«")))
277 .map(|l| l.id);
278 NameTable {
279 libraries,
280 index,
281 bank,
282 units: unit_markers(rom, bank),
283 structure,
284 }
285 }
286
287 pub fn of(machine: &saturnus_core::Machine) -> NameTable {
289 Self::build(machine.model(), machine.rom_nibbles())
290 }
291
292 pub fn stats(&self) -> NameStats {
294 let mut s = NameStats {
295 libraries: self.libraries.len(),
296 ..NameStats::default()
297 };
298 for lib in &self.libraries {
299 s.names += lib.names.iter().flatten().count();
300 s.arities += lib.arity.iter().flatten().count();
301 }
302 s
303 }
304
305 pub fn heap_bytes(&self) -> usize {
308 use std::mem::size_of;
309 let libs: usize = self
310 .libraries
311 .iter()
312 .map(|l| {
313 l.names.capacity() * size_of::<Option<String>>()
314 + l.names
315 .iter()
316 .flatten()
317 .map(String::capacity)
318 .sum::<usize>()
319 + l.link.capacity() * size_of::<Option<usize>>()
320 + l.arity.capacity() * size_of::<Option<u8>>()
321 })
322 .sum();
323 let index: usize = self
324 .index
325 .values()
326 .map(|v| size_of::<(u16, Vec<usize>)>() + v.capacity() * size_of::<usize>())
327 .sum();
328 libs + self.libraries.capacity() * size_of::<Library>() + index
329 }
330
331 pub fn names(&self) -> impl Iterator<Item = (u16, u16, &str)> + '_ {
333 self.libraries.iter().flat_map(|lib| {
334 lib.names
335 .iter()
336 .enumerate()
337 .filter_map(move |(c, n)| Some((lib.id, u16::try_from(c).ok()?, n.as_deref()?)))
338 })
339 }
340
341 pub fn bank(&self) -> Option<usize> {
343 self.bank
344 }
345
346 pub fn find(&self, name: &str) -> Option<(u16, u16)> {
349 self.libraries.iter().find_map(|lib| {
350 let c = lib.names.iter().position(|n| n.as_deref() == Some(name))?;
351 Some((lib.id, u16::try_from(c).ok()?))
352 })
353 }
354
355 pub fn library_size(&self, library: u16) -> Option<(usize, usize)> {
358 let lib = &self.libraries[*self.index.get(&library)?.first()?];
359 Some((lib.link.len(), lib.names.iter().flatten().count()))
360 }
361
362 pub fn object_index(&self, library: u16, number: u16) -> Option<usize> {
365 let c = usize::from(number);
366 let mut found = None;
367 for &i in self.index.get(&library)? {
368 let t = (*self.libraries[i].link.get(c)?)?;
369 match found {
370 None => found = Some(t),
371 Some(f) if f == t => {}
372 Some(_) => return None,
373 }
374 }
375 found
376 }
377
378 pub fn unit_markers(&self) -> Option<UnitMarkers> {
380 self.units
381 }
382
383 fn info<'s>(&'s self, lib: &'s Library, c: usize) -> Option<CommandInfo<'s>> {
384 let name = lib.names.get(c).and_then(|n| n.as_deref());
385 let silent = name.is_none() && Some(lib.id) == self.structure;
386 Some(CommandInfo {
387 library: lib.id,
388 number: u16::try_from(c).ok()?,
389 name,
390 arity: lib.arity.get(c).copied().flatten(),
391 silent,
392 syntax: match c {
393 ALGEBRAIC_WORD if silent => Some(Syntax::Algebraic),
394 QUOTE_WORD if silent => Some(Syntax::Quote),
395 _ => None,
396 },
397 })
398 }
399
400 fn agreed<'s>(
403 &'s self,
404 candidates: impl Iterator<Item = (&'s Library, usize)>,
405 ) -> Option<CommandInfo<'s>> {
406 let mut found: Option<CommandInfo<'s>> = None;
407 for (lib, c) in candidates {
408 let info = self.info(lib, c)?;
409 match found {
410 None => found = Some(info),
411 Some(f) if f == info => {}
412 Some(_) => return None,
413 }
414 }
415 found
416 }
417
418 pub fn xlib(&self, library: u16, number: u16) -> Option<CommandInfo<'_>> {
421 let c = usize::from(number);
422 let candidates = self.index.get(&library)?.iter().filter_map(move |&i| {
423 let lib = &self.libraries[i];
424 (c < lib.link.len()).then_some((lib, c))
425 });
426 self.agreed(candidates)
427 }
428
429 pub fn command_at(&self, addr: u32, mem: &dyn Memory) -> Option<CommandInfo<'_>> {
437 let start = addr.checked_sub(6)?;
438 let mut prefix = [0u8; 6];
439 for (i, p) in prefix.iter_mut().enumerate() {
440 *p = mem.nibble(start + i as u32)?;
441 }
442 let library = read_field(&prefix, 0, 3)? as u16;
443 let c = read_field(&prefix, 3, 3)? as usize;
444 let a = addr as usize;
445 let candidates = self.index.get(&library)?.iter().filter_map(move |&i| {
446 let lib = &self.libraries[i];
447 let t = (*lib.link.get(c)?)?;
448 let same = match self.bank {
449 None => t == a,
450 Some(b) => a < 2 * b && t % b == a % b,
452 };
453 same.then_some((lib, c))
454 });
455 self.agreed(candidates)
456 }
457}
458
459fn scan(rom: &[u8], bank: Option<usize>) -> Vec<Library> {
463 let hex = ObjectType::BinaryInteger.prolog() as usize;
464 let sysbin = ObjectType::SystemBinary.prolog() as usize;
465 let ext = ObjectType::ExtendedPointer.prolog() as usize;
466 let mut out = Vec::new();
467 let mut commands = 0;
468 let last = rom.len().saturating_sub(HEADER_NIBBLES);
469 for p in 0..last {
470 if f5(rom, p + 3) == Some(0) || f5(rom, p + 13) == Some(0) {
473 continue;
474 }
475 let Some(tl) = rel(rom, p + 13) else { continue };
476 if f5(rom, tl) != Some(hex) {
477 continue;
478 }
479 let Some(th) = rel(rom, p + 3) else { continue };
480 let Some(ph) = f5(rom, th) else { continue };
481 if ph != hex && ph != sysbin && ph != ext {
482 continue;
483 }
484 let Some(len) = f5(rom, tl + 5) else { continue };
485 if len < 5 || (len - 5) % 5 != 0 || tl + 5 + len > rom.len() {
486 continue;
487 }
488 let count = (len - 5) / 5;
489 if count > MAX_LIBRARY_COMMANDS {
490 continue;
491 }
492 let hash = if ph == hex {
493 hash_at(rom, th)
494 } else {
495 let size = if ph == sysbin { 10 } else { 15 };
497 if tl != th + size {
498 continue;
499 }
500 let Some(a) = f5(rom, th + 5) else { continue };
501 let mut found = candidates(a, bank, rom.len()).filter_map(|t| hash_at(rom, t));
502 match (found.next(), found.next()) {
503 (Some(h), None) => Some(h),
504 _ => None,
505 }
506 };
507 let Some(hash) = hash.filter(|h| h.count <= count) else {
508 continue;
509 };
510 commands += count;
511 if out.len() == MAX_LIBRARIES || commands > MAX_COMMANDS {
512 return Vec::new();
513 }
514 let link = (0..count).map(|c| rel(rom, tl + 10 + 5 * c)).collect();
515 let names = (0..hash.count).map(|c| name_in(rom, &hash, c)).collect();
516 out.push(Library {
517 id: read_field(rom, p, 3).unwrap_or(0) as u16,
518 names,
519 link,
520 arity: Vec::new(),
521 });
522 }
523 out
524}
525
526fn candidates(a: usize, bank: Option<usize>, len: usize) -> Box<dyn Iterator<Item = usize>> {
529 match bank {
530 None => Box::new((a < len).then_some(a).into_iter()),
531 Some(b) => Box::new((0..len / b).map(move |k| k * b + a % b)),
532 }
533}
534
535#[derive(Clone, Copy, Debug)]
537struct Dispatch {
538 ck0: Option<usize>,
539 ck1: usize,
541 stride: usize,
542}
543
544impl Dispatch {
545 fn find(rom: &[u8], libraries: &[Library]) -> Option<Dispatch> {
550 let (lib, plus) = libraries.iter().find_map(|l| {
551 let c = l.names.iter().position(|n| n.as_deref() == Some("+"))?;
552 Some((l, c))
553 })?;
554 let mut counts: HashMap<usize, usize> = HashMap::new();
555 for &t in lib.link.iter().flatten() {
556 if let Some(p) = first_object(rom, t) {
557 *counts.entry(p).or_default() += 1;
558 }
559 }
560 let ck2 = first_object(rom, (*lib.link.get(plus)?)?)?;
561 let mut common: Vec<(usize, usize)> = counts.into_iter().collect();
562 common.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
563 common.truncate(DISPATCH_CANDIDATES);
564 let has = |v: usize| common.iter().any(|&(p, _)| p == v);
565 let stride = common
567 .iter()
568 .filter_map(|&(p, _)| p.checked_sub(ck2).filter(|s| *s > 0))
569 .filter(|&s| ck2 >= s && has(ck2 - s) && has(ck2 + 2 * s))
570 .min()?;
571 let ck1 = ck2 - stride;
572 let cks: Vec<usize> = (0..5).map(|k| ck1 + k * stride).collect();
573 let ck0 = common.iter().map(|&(p, _)| p).find(|p| !cks.contains(p));
574 Some(Dispatch { ck0, ck1, stride })
575 }
576
577 fn arity(&self, rom: &[u8], t: usize) -> Option<u8> {
580 let p = first_object(rom, t)?;
581 if Some(p) == self.ck0 {
582 return Some(0);
583 }
584 let k = p.checked_sub(self.ck1)?;
585 if k % self.stride != 0 || k / self.stride > 4 {
586 return None;
587 }
588 u8::try_from(k / self.stride + 1).ok()
589 }
590}
591
592fn first_object(rom: &[u8], t: usize) -> Option<usize> {
594 (f5(rom, t)? == ObjectType::Program.prolog() as usize).then(|| f5(rom, t + 5))?
595}
596
597fn unit_markers(rom: &[u8], bank: Option<usize>) -> Option<UnitMarkers> {
600 let unit = ObjectType::Unit.prolog() as usize;
601 let mut ends: HashMap<usize, usize> = HashMap::new();
602 for p in 0..rom.len().saturating_sub(5) {
603 if f5(rom, p) != Some(unit) {
604 continue;
605 }
606 if let Some(e) = unit_end(rom, p + 5) {
607 *ends.entry(e).or_default() += 1;
608 }
609 }
610 let (&end, _) = ends.iter().max_by(|a, b| a.1.cmp(b.1).then(b.0.cmp(a.0)))?;
611 let end = u32::try_from(end).ok()?;
612 let first = end.checked_sub(4 * MARKER_STRIDE)? as usize;
613 let is_block = |at: usize| {
615 (0..5).all(|k| {
616 let s = at + k * MARKER_STRIDE as usize;
617 rom.get(s..s + 10) == Some(&EMPTY_LIST[..])
618 })
619 };
620 if !candidates(first, bank, rom.len()).any(is_block) {
621 return None;
622 }
623 let m = |k: u32| end - (4 - k) * MARKER_STRIDE;
624 Some(UnitMarkers {
625 times: m(0),
626 divide: m(1),
627 power: m(2),
628 prefix: m(3),
629 end,
630 })
631}
632
633fn unit_end(rom: &[u8], mut at: usize) -> Option<usize> {
636 let real = ObjectType::Real.prolog() as usize;
637 let string = ObjectType::String.prolog() as usize;
638 let chr = ObjectType::Character.prolog() as usize;
639 let mut last = None;
640 for _ in 0..MAX_UNIT_ELEMENTS {
641 let p = f5(rom, at)?;
642 if p == SEMI as usize {
643 return last;
644 }
645 if p == real {
646 at += 21;
647 } else if p == string {
648 at += 5 + f5(rom, at + 5)?;
649 } else if p == chr {
650 at += 7;
651 } else if (0x02600..=0x02FFF).contains(&p) {
652 return None;
653 } else {
654 last = Some(p);
655 at += 5;
656 }
657 }
658 None
659}
660
661#[cfg(test)]
662mod tests {
663 use super::*;
664
665 fn put(out: &mut Vec<u8>, at: usize, value: usize, width: usize) {
666 if out.len() < at + width {
667 out.resize(at + width, 0);
668 }
669 for i in 0..width {
670 out[at + i] = ((value >> (4 * i)) & 0xF) as u8;
671 }
672 }
673
674 fn put_rel(out: &mut Vec<u8>, at: usize, target: usize) {
675 let off = (target + 0x100000 - at) & 0xFFFFF;
676 put(out, at, off, 5);
677 }
678
679 fn synthetic(id: usize, names: &[Option<&str>], first: &[usize]) -> Vec<u8> {
683 let mut r = vec![0u8; 0x4000];
684 let header = 0x100;
685 let hash = 0x200;
686 let objects = 0x1000;
687 let body = hash + 10;
689 let mut at = body + 85;
690 let mut entries = vec![0usize; names.len()];
691 let mut first_of_len = [0usize; 16];
692 for len in 1..=16 {
693 for (c, n) in names.iter().enumerate() {
694 let Some(n) = n else { continue };
695 let bytes: Vec<u8> = n.chars().map(|c| c as u32 as u8).collect();
697 if bytes.len() != len {
698 continue;
699 }
700 if first_of_len[len - 1] == 0 {
701 first_of_len[len - 1] = at;
702 }
703 entries[c] = at;
704 put(&mut r, at, len, 2);
705 for (i, b) in bytes.iter().enumerate() {
706 put(&mut r, at + 2 + 2 * i, usize::from(*b), 2);
707 }
708 put(&mut r, at + 2 + 2 * len, c, 3);
709 at += 2 + 2 * len + 3;
710 }
711 }
712 for (i, &e) in first_of_len.iter().enumerate() {
713 if e != 0 {
714 put_rel(&mut r, body + 5 * i, e);
715 }
716 }
717 let numbers = at;
718 put_rel(&mut r, body + 80, numbers);
719 for (c, &e) in entries.iter().enumerate() {
720 let field = numbers + 5 * c;
721 put(&mut r, field, if e == 0 { 0 } else { field - e }, 5);
722 }
723 let end = numbers + 5 * names.len();
724 put(&mut r, hash, 0x02A4E, 5);
725 put(&mut r, hash + 5, end - hash - 5, 5);
726 let link = end;
728 put(&mut r, link, 0x02A4E, 5);
729 put(&mut r, link + 5, 5 + 5 * names.len(), 5);
730 for c in 0..names.len() {
731 let obj = objects + 0x40 * c;
732 put(&mut r, obj - 6, id, 3);
733 put(&mut r, obj - 3, c, 3);
734 put(&mut r, obj, 0x02D9D, 5);
735 put(&mut r, obj + 5, first.get(c).copied().unwrap_or(0x3000), 5);
736 put(&mut r, obj + 10, SEMI as usize, 5);
737 put_rel(&mut r, link + 10 + 5 * c, obj);
738 }
739 put(&mut r, header, id, 3);
740 put_rel(&mut r, header + 3, hash);
741 put_rel(&mut r, header + 13, link);
742 r
743 }
744
745 struct Image<'a>(&'a [u8]);
746 impl Memory for Image<'_> {
747 fn nibble(&self, addr: u32) -> Option<u8> {
748 self.0.get(addr as usize).copied()
749 }
750 }
751
752 #[test]
753 fn names_resolve_by_number_and_by_address() {
754 let names = [
757 Some("SIN"),
758 Some("+"),
759 None,
760 Some("+"),
761 Some("IFTE"),
762 Some("INTG"),
763 Some("COS"),
764 ];
765 let first = [0x2000, 0x2005, 0x2000, 0x2005, 0x200A, 0x200F, 0x2000];
766 let rom = synthetic(0x2, &names, &first);
767 let t = NameTable::build(Model::Hp48sx, &rom);
768 assert_eq!(
769 t.stats(),
770 NameStats {
771 libraries: 1,
772 names: 6,
773 arities: 7
774 }
775 );
776 let sin = t.xlib(2, 0).unwrap();
777 assert_eq!((sin.name, sin.arity), (Some("SIN"), Some(1)));
778 assert_eq!(t.xlib(2, 3).unwrap().name, Some("+"));
779 assert_eq!(t.xlib(2, 2).unwrap().name, None);
780 assert_eq!(t.xlib(2, 4).unwrap().arity, Some(3));
781 assert_eq!(t.xlib(2, 5).unwrap().arity, Some(4));
782 assert_eq!(t.xlib(2, 7), None);
783 assert_eq!(t.xlib(3, 0), None);
784 let mem = Image(&rom);
785 let plus = t.command_at(0x1040, &mem).unwrap();
786 assert_eq!(
787 (plus.number, plus.name, plus.arity),
788 (1, Some("+"), Some(2))
789 );
790 assert_eq!(t.command_at(0x1041, &mem), None);
792 let mut liar = rom.clone();
793 put(&mut liar, 0x1040 - 3, 2, 3);
794 assert_eq!(t.command_at(0x1040, &Image(&liar)), None);
795 let all: Vec<&str> = t.names().map(|(_, _, n)| n).collect();
796 assert_eq!(all, ["SIN", "+", "+", "IFTE", "INTG", "COS"]);
797 }
798
799 fn with_ram(objects: &[(usize, &str)]) -> (Vec<u8>, NameTable) {
801 let names = [
802 Some("SIN"),
803 Some("+"),
804 Some("«"),
805 Some("»"),
806 None,
807 Some("IFTE"),
808 Some("INTG"),
809 ];
810 let first = [0x2000, 0x2005, 0x2000, 0x2000, 0x2000, 0x200A, 0x200F];
811 let mut rom = synthetic(0x2, &names, &first);
812 let t = NameTable::build(Model::Hp48sx, &rom);
813 for (at, hex) in objects {
814 for (i, c) in hex.chars().enumerate() {
815 let v = c.to_digit(16).unwrap() as usize;
816 put(&mut rom, at + i, v, 1);
817 }
818 }
819 (rom, t)
820 }
821
822 fn ptr(a: usize) -> String {
823 (0..5)
824 .map(|i| format!("{:X}", (a >> (4 * i)) & 0xF))
825 .collect()
826 }
827
828 #[test]
829 fn decoding_with_names() {
830 use crate::decompile::Settings;
831 use crate::object::{Object, Reader};
832 let one = "339200000000000000010";
834 let list = format!("47A20{one}{}B2130", ptr(0x1000));
835 let prog = format!(
836 "D9D20{}{one}{}{}B2130",
837 ptr(0x1080),
838 ptr(0x1000),
839 ptr(0x10C0)
840 );
841 let alg = format!("8BA2084E201014{one}{}B2130", ptr(0x1040));
842 let (rom, t) = with_ram(&[(0x3000, &list), (0x3100, &prog), (0x3200, &alg)]);
843 let mem = Image(&rom);
844 let r = Reader::with_names(&mem, &t, Settings::default());
845 let json = |a: u32| serde_json::to_value(r.decode_at(a).unwrap()).unwrap();
846 assert_eq!(
847 json(0x3000),
848 serde_json::json!({"type": "list", "items": [
849 {"type": "real", "value": 1.0},
850 {"type": "command", "name": "SIN", "address": 4096,
851 "library": 2, "command": 0}]})
852 );
853 assert_eq!(
854 r.decode_at(0x3100).unwrap(),
855 Object::Program {
856 source: Some("« 1 SIN »".into())
857 }
858 );
859 assert_eq!(
860 r.decode_at(0x3200).unwrap(),
861 Object::Algebraic {
862 source: Some("'A+1'".into())
863 }
864 );
865 assert_eq!(
867 json(0x1000),
868 serde_json::json!({"type": "command", "name": "SIN", "address": 4096, "library": 2, "command": 0})
869 );
870 assert!(t.command_at(0x1100, &mem).unwrap().silent);
873 let plain = Reader::new(&mem);
875 assert_eq!(
876 serde_json::to_value(plain.decode_at(0x3000).unwrap()).unwrap()["items"][1],
877 serde_json::json!({"type": "command", "address": 4096})
878 );
879 assert_eq!(
880 plain.decode_at(0x3100).unwrap(),
881 Object::Program { source: None }
882 );
883 }
884
885 #[test]
886 fn garbage_ram_gives_errors_or_unknowns() {
887 use crate::decompile::Settings;
888 use crate::object::{Object, Reader};
889 let cycle = format!("47A20{}B2130", ptr(0x3000));
891 let loose = "8BA2084E2010184E2010288E20B2130";
894 let short = format!("8BA20{}B2130", ptr(0x1040));
895 let selfp = format!("D9D20{}B2130", ptr(0x3400));
897 let (rom, t) = with_ram(&[
898 (0x3000, &cycle),
899 (0x3100, loose),
900 (0x3200, &short),
901 (0x3400, &selfp),
902 ]);
903 let mem = Image(&rom);
904 let r = Reader::with_names(&mem, &t, Settings::default());
905 assert!(r.decode_at(0x3000).is_err());
906 assert!(matches!(
907 r.decode_at(0x3100),
908 Ok(Object::Algebraic { source: None }) | Err(_)
909 ));
910 assert_eq!(
911 r.decode_at(0x3200).unwrap(),
912 Object::Algebraic { source: None }
913 );
914 assert_eq!(
915 r.decode_at(0x3400).unwrap(),
916 Object::Program {
917 source: Some("External".into())
918 }
919 );
920 }
921
922 fn two_banks(a: &[u8], b: &[u8]) -> Vec<u8> {
925 let mut r = vec![0u8; 2 * BANK_NIBBLES];
926 r[..a.len()].copy_from_slice(a);
927 r[BANK_NIBBLES..BANK_NIBBLES + b.len()].copy_from_slice(b);
928 r
929 }
930
931 #[test]
932 fn banked_copies_must_agree() {
933 let first = [0x2000, 0x2005, 0x2000];
934 let sin = synthetic(0x2, &[Some("SIN"), Some("+"), Some("X")], &first);
935 let cos = synthetic(0x2, &[Some("COS"), Some("+"), Some("X")], &first);
936 let mem = Image(&sin);
938 let same = NameTable::build(Model::Hp49g, &two_banks(&sin, &sin));
939 assert_eq!(same.command_at(0x1000, &mem).unwrap().name, Some("SIN"));
940 assert_eq!(same.xlib(2, 0).unwrap().name, Some("SIN"));
941 let differ = NameTable::build(Model::Hp49g, &two_banks(&sin, &cos));
942 assert_eq!(differ.command_at(0x1000, &mem), None);
943 assert_eq!(differ.xlib(2, 0), None);
944 assert_eq!(differ.command_at(0x1040, &mem).unwrap().name, Some("+"));
946 }
947
948 #[test]
949 fn link_entries_outside_the_image_match_nothing() {
950 let mut lib = synthetic(0x2, &[Some("SIN"), Some("COS")], &[0x2000, 0x2000]);
951 let link = (0..lib.len() - 5)
953 .find(|&i| read_field(&lib, i, 5) == Some(0x02A4E) && i > 0x210)
954 .unwrap();
955 put(&mut lib, link + 15, 0x7FFFF, 5);
956 let image = two_banks(&lib, &[]);
957 let t = NameTable::build(Model::Hp49g, &image);
958 assert_eq!(t.xlib(2, 1).unwrap().name, Some("COS"));
959 let mut ram = vec![0u8; 0x40000];
961 put(&mut ram, 0x3FFF9, 0x2, 3);
962 put(&mut ram, 0x3FFFC, 1, 3);
963 assert_eq!(t.command_at(0x3FFFF, &Image(&ram)), None);
964 }
965
966 #[test]
967 fn other_models_get_an_empty_table() {
968 let rom = synthetic(0x2, &[Some("SIN")], &[0x2000]);
969 assert_eq!(NameTable::build(Model::Hp48gx, &rom).stats().names, 1);
970 for model in [Model::Hp38g, Model::Hp39g, Model::Hp40g, Model::Hp42s] {
971 let t = NameTable::build(model, &rom);
972 assert_eq!(t.stats(), NameStats::default(), "{}", model.name());
973 assert_eq!(t.unit_markers(), None);
974 }
975 }
976
977 #[test]
978 fn garbage_gives_an_empty_table() {
979 let mut rom = vec![0u8; 0x10000];
980 for (i, n) in rom.iter_mut().enumerate() {
981 *n = ((i * 7919) >> 3) as u8 & 0xF;
982 }
983 let t = NameTable::build(Model::Hp49g, &rom);
984 assert_eq!(t.stats().names, 0);
985 assert_eq!(t.unit_markers(), None);
986 assert_eq!(t.command_at(5, &Image(&rom)), None);
987 assert_eq!(NameTable::build(Model::Hp48sx, &[]).stats().libraries, 0);
988 }
989
990 fn shared_tables(headers: usize, commands: usize) -> Vec<u8> {
993 let mut r = vec![0u8; 0x10000];
994 let (th, tl) = (0x2000, 0x3000);
995 put(&mut r, th, 0x02A4E, 5);
996 put(&mut r, th + 5, 90, 5);
997 put(&mut r, th + 90, 5, 5);
999 put(&mut r, tl, 0x02A4E, 5);
1000 put(&mut r, tl + 5, 5 + 5 * commands, 5);
1001 for k in 0..headers {
1002 put(&mut r, 20 * k, 0x700 + (k & 0xFF), 3);
1003 put_rel(&mut r, 20 * k + 3, th);
1004 put_rel(&mut r, 20 * k + 13, tl);
1005 }
1006 r
1007 }
1008
1009 #[test]
1010 fn crafted_link_tables_are_capped() {
1011 let libraries = |h, c| {
1012 NameTable::build(Model::Hp48gx, &shared_tables(h, c))
1013 .stats()
1014 .libraries
1015 };
1016 assert_eq!(libraries(10, 100), 10);
1017 assert_eq!(libraries(MAX_LIBRARIES + 1, 100), 0);
1019 assert_eq!(libraries(1, MAX_LIBRARY_COMMANDS + 1), 0);
1021 assert_eq!(libraries(1, MAX_LIBRARY_COMMANDS), 1);
1022 assert_eq!(libraries(MAX_COMMANDS / 2000 + 1, 2000), 0);
1024 }
1025
1026 #[test]
1027 fn unit_markers_from_the_rom_units() {
1028 let mut rom = vec![0u8; 0x2000];
1029 let block = 0x400;
1030 for k in 0..5 {
1031 rom[block + 10 * k..block + 10 * k + 10].copy_from_slice(&EMPTY_LIST);
1032 }
1033 let end = block + 40;
1034 for (u, last) in [(0x800, end), (0x900, end), (0xA00, end), (0xB00, 0x1234)] {
1036 put(&mut rom, u, 0x02ADA, 5);
1037 put(&mut rom, u + 5, 0x2A2C9, 5);
1038 put(&mut rom, u + 10, 0x02A2C, 5);
1039 put(&mut rom, u + 15, 7, 5);
1040 put(&mut rom, u + 20, 0x6D, 2);
1041 put(&mut rom, u + 22, last, 5);
1042 put(&mut rom, u + 27, SEMI as usize, 5);
1043 }
1044 let m = NameTable::build(Model::Hp48sx, &rom)
1045 .unit_markers()
1046 .unwrap();
1047 assert_eq!(
1048 (m.times, m.divide, m.power, m.prefix, m.end),
1049 (0x400, 0x40A, 0x414, 0x41E, 0x428)
1050 );
1051 }
1052}