Skip to main content

saturnus_objects/
names.rs

1//! Command names, read at run time from the ROM's own library tables.
2//!
3//! The ROM's decompiler names a command from the 6-nibble XLIB body
4//! (library, command) that precedes every built-in command object, and
5//! from the library's hash table; the link table maps command numbers to
6//! objects (RPLMAN p. 13; layouts observed in the 48SX, 48GX and 49G ROMs:
7//! wiki: protocols/rpl-libraries). Nothing ROM-derived is compiled in: a
8//! [`NameTable`] is built from the ROM image the user loaded, once per ROM.
9//!
10//! What the table holds:
11//!
12//! - every library whose header, hash table and link table check out
13//!   (found by scanning the image, so the 49G's flash banks need no map),
14//!   with its command names and, where the command starts with one of the
15//!   argument-checking dispatchers, its argument count;
16//! - the five unit operator markers (`*`, `/`, `^`, prefix, end).
17//!
18//! Build cost: one pass over the image (512 K nibbles on the 48SX, 4 M on
19//! the 49G) and a few thousand table reads; see the ROM-gated test.
20
21use std::collections::HashMap;
22
23use saturnus_core::Model;
24
25use crate::charset;
26use crate::object::Memory;
27use crate::prolog::{ObjectType, SEMI, read_field};
28
29/// Nibbles in a 49G flash bank: one window (#00000-#3FFFF or
30/// #40000-#7FFFF) shows one bank (wiki: hardware/hp49g).
31const BANK_NIBBLES: usize = 0x40000;
32/// Library header: number (3), then offsets to the hash, message and link
33/// tables and the configuration object (5 each).
34const HEADER_NIBBLES: usize = 23;
35/// An empty list, the unit operator markers: prolog #02A74 and SEMI.
36const EMPTY_LIST: [u8; 10] = [4, 7, 0xA, 2, 0, 0xB, 2, 1, 3, 0];
37/// Distance between the unit operator markers.
38const MARKER_STRIDE: u32 = 10;
39/// Most elements of a ROM unit object read when looking for the markers.
40const MAX_UNIT_ELEMENTS: usize = 40;
41/// Most common dispatch objects considered when looking for CK1-CK4.
42const DISPATCH_CANDIDATES: usize = 12;
43/// Most commands in one library: command numbers are 3 nibbles (the XLIB
44/// body). The largest real library has 1444 (49G 2.10, library 788).
45const MAX_LIBRARY_COMMANDS: usize = 0x1000;
46/// Most libraries in an image, copies counted: 52 on the 49G 2.10 ROM,
47/// 42 on the 48GX, 2 on the 48SX.
48const MAX_LIBRARIES: usize = 256;
49/// Most commands of all libraries together: 5940 on the 49G 2.10 ROM.
50/// Without it a crafted image whose headers all share one large link
51/// table makes gigabytes of tables.
52const MAX_COMMANDS: usize = 1 << 15;
53
54/// The 5-nibble field at `at`, or `None` past the end.
55fn f5(n: &[u8], at: usize) -> Option<usize> {
56    read_field(n, at, 5).map(|v| v as usize)
57}
58
59/// The target of the self-relative offset field at `at` (offsets wrap at
60/// #100000 like addresses).
61fn 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/// A hash table: where its number table starts, how many numbers it has,
72/// where it ends.
73#[derive(Clone, Copy, Debug)]
74struct Hash {
75    names_start: usize,
76    numbers: usize,
77    count: usize,
78}
79
80/// The hash table (hex string) at `t`, if its layout is valid.
81fn 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    // 16 offsets by name length, the number table's offset, one name.
89    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
109/// The name of command `c` in `h`, if it has one.
110fn 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/// One library found in the ROM.
131#[derive(Clone, Debug)]
132struct Library {
133    id: u16,
134    /// Name by command number (shorter than `link` when the last commands
135    /// have none).
136    names: Vec<Option<String>>,
137    /// Image index of each command's object (`None`: outside the image).
138    link: Vec<Option<usize>>,
139    /// Argument count by command number, where the command starts with a
140    /// dispatcher (filled after all libraries are found).
141    arity: Vec<Option<u8>>,
142}
143
144/// A command as the name table knows it.
145#[derive(Clone, Copy, Debug, PartialEq, Eq)]
146pub struct CommandInfo<'a> {
147    /// Library number.
148    pub library: u16,
149    /// Command number in the library.
150    pub number: u16,
151    /// Its name, if the library's hash table has one.
152    pub name: Option<&'a str>,
153    /// Its argument count, if it starts with an argument dispatcher
154    /// (CK0-CK5).
155    pub arity: Option<u8>,
156    /// An unnamed command of the structure library (the one holding `«`),
157    /// such as the word that quotes a program inside a program: the ROM's
158    /// decompiler shows nothing for it.
159    pub silent: bool,
160    /// What the command marks in the 49G's algebraic-mode syntax, for the
161    /// two unnamed structure words that carry it ([`Syntax`]).
162    pub syntax: Option<Syntax>,
163}
164
165/// The 49G's two unnamed structure words that embed an expression typed in
166/// algebraic-mode syntax in RPL (observed in RAM on ROM 2.10 and 1.19-6,
167/// commands 30 and 31 of the structure library, which the 48 has not:
168/// wiki: protocols/rpl-libraries "Algebraic-mode expressions").
169#[derive(Clone, Copy, Debug, PartialEq, Eq)]
170pub enum Syntax {
171    /// The next object is an expression in algebraic-mode syntax: the
172    /// ROM writes it between backquotes (`` `CF(-95)` ``). The 49G's
173    /// algebraic mode compiles every command line it evaluates so, and
174    /// keeps it as the history echo.
175    Algebraic,
176    /// Inside such an expression, the next object quoted (`'A'`).
177    Quote,
178}
179
180/// Command numbers of [`Syntax::Algebraic`] and [`Syntax::Quote`] in the
181/// structure library.
182const ALGEBRAIC_WORD: usize = 30;
183const QUOTE_WORD: usize = 31;
184
185/// The unit operators: ROM pointers to five empty lists (wiki:
186/// protocols/rpl-libraries "Unit operators").
187#[derive(Clone, Copy, Debug, PartialEq, Eq)]
188pub struct UnitMarkers {
189    /// `*`.
190    pub times: u32,
191    /// `/`.
192    pub divide: u32,
193    /// `^`.
194    pub power: u32,
195    /// A prefix (the character before the unit name).
196    pub prefix: u32,
197    /// The end of the unit expression.
198    pub end: u32,
199}
200
201/// What a built table holds, for reports and tests.
202#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
203pub struct NameStats {
204    /// Libraries found.
205    pub libraries: usize,
206    /// Command numbers with a name.
207    pub names: usize,
208    /// Commands with a known argument count.
209    pub arities: usize,
210}
211
212/// Command names and the other ROM facts the decompiler needs, built from
213/// one ROM image.
214#[derive(Clone, Default)]
215pub struct NameTable {
216    /// Libraries by number; a number can occur twice (a ROM can carry an
217    /// older copy), each is tried.
218    libraries: Vec<Library>,
219    index: HashMap<u16, Vec<usize>>,
220    /// Nibbles per bank when the image is banked (the 49G).
221    bank: Option<usize>,
222    units: Option<UnitMarkers>,
223    /// The library holding `«` and the other structure words.
224    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    /// The table of `model`'s ROM image `rom` (one nibble per element, as
238    /// [`saturnus_core::Machine::rom_nibbles`] gives it). A ROM without
239    /// libraries gives an empty table (every command stays unnamed). Only
240    /// the 48SX, 48GX (mask ROM, image index = address) and 49G (flash
241    /// banks of 256 K nibbles) are handled; for the 38G, 39G, 40G and the
242    /// Pioneers (42S, 27S, 17BII, 17B), the 28S and the 19BII,
243    /// whose ROM layout this does not know (banked differently, or no RPL
244    /// libraries at all on the Pioneers), the table is empty.
245    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    /// The table of `machine`'s ROM.
288    pub fn of(machine: &saturnus_core::Machine) -> NameTable {
289        Self::build(machine.model(), machine.rom_nibbles())
290    }
291
292    /// Counts for reports.
293    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    /// The heap memory the table holds, in bytes (approximate: vector
306    /// and string capacities).
307    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    /// Every named command: (library, number, name).
332    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    /// Nibbles per bank when the image is banked (the 49G).
342    pub fn bank(&self) -> Option<usize> {
343        self.bank
344    }
345
346    /// The library and command number of the command called `name` (the
347    /// first library holding it).
348    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    /// The commands of library `library` and how many of them have a name
356    /// (the first copy when the image holds it twice).
357    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    /// The image index of command `number` of `library`'s object, when
363    /// every copy of the library agrees on it.
364    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    /// The unit operator markers, if the ROM's unit table revealed them.
379    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    /// The one answer of several candidate libraries (copies of a library
401    /// in two places of the image): accepted only when they all agree.
402    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    /// The command XLIB `library` `number` (an XLIB name object). When
419    /// the image holds the library twice, the copies must agree.
420    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    /// The command whose object is at `addr` in `mem` (a ROM pointer), as
430    /// the ROM's decompiler finds it: the XLIB body in the six nibbles
431    /// before the object names a library and command whose link table
432    /// entry is this object. On a banked ROM the table does not know which
433    /// bank the CPU sees at `addr`: every copy of the library whose entry
434    /// lies at that offset of some bank must agree, else the command stays
435    /// unknown.
436    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                // The CPU sees a bank through one of two windows.
451                Some(b) => a < 2 * b && t % b == a % b,
452            };
453            same.then_some((lib, c))
454        });
455        self.agreed(candidates)
456    }
457}
458
459/// Every library header in `rom` whose hash and link tables check out;
460/// none when there are more than [`MAX_LIBRARIES`] or [`MAX_COMMANDS`]
461/// (not a ROM this knows).
462fn 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        // Cheap rejections first: both offsets set, the link table a hex
471        // string, the hash table a hex string or an indirection.
472        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            // An indirection directly followed by the link table.
496            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
526/// Image indexes that CPU address `a` can stand for: itself, or on a
527/// banked ROM the same offset in every bank.
528fn 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/// The argument dispatchers CK0-CK5 as ROM addresses.
536#[derive(Clone, Copy, Debug)]
537struct Dispatch {
538    ck0: Option<usize>,
539    /// CK1&Dispatch; CKn is `ck1 + (n - 1) * stride`.
540    ck1: usize,
541    stride: usize,
542}
543
544impl Dispatch {
545    /// The dispatchers, from the library that holds `+`: its commands'
546    /// first objects include four evenly spaced ones (CK1-CK4), `+`
547    /// starts with CK2, and the most common other one is CK0 (wiki:
548    /// protocols/rpl-libraries "Dispatchers").
549    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        // CK1 and CK3 around CK2, CK4 after: the smallest spacing that fits.
566        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    /// The argument count of the command whose object is at image index
578    /// `t`.
579    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
592/// The first element of the program at image index `t`, if it is one.
593fn first_object(rom: &[u8], t: usize) -> Option<usize> {
594    (f5(rom, t)? == ObjectType::Program.prolog() as usize).then(|| f5(rom, t + 5))?
595}
596
597/// The unit operator markers: the most common last element of the ROM's
598/// own unit objects is the end marker, the other four precede it.
599fn 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    // All five are empty lists, side by side, in one bank.
614    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
633/// The last element (a ROM pointer) of the unit body at `at`, if the body
634/// holds only reals, strings, characters and pointers.
635fn 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    /// A ROM of `size` nibbles with library `id` at #100: names `names`
680    /// (by command number), the commands' objects (programs starting with
681    /// the pointer in `first`) each preceded by their XLIB body.
682    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        // Hash table: 16 length offsets, number table offset, names, numbers.
688        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                // HP characters: « and » are bytes #AB and #BB.
696                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        // Link table right after the hash table.
727        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        // + is binary through "CK2" #2005; the others share "CK1" #2000,
755        // "CK3" #200A and "CK4" #200F appear once each.
756        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        // Not a command: no XLIB body before it, or one that lies.
791        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    /// The synthetic library's ROM with RAM objects at #3000.
800    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        // { 1 SIN }, « 1 SIN » with « and » as commands, 'A+1'.
833        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        // A stack level that is a command's object is that command.
866        assert_eq!(
867            json(0x1000),
868            serde_json::json!({"type": "command", "name": "SIN", "address": 4096, "library": 2, "command": 0})
869        );
870        // An unnamed command of the structure library would be silent; this
871        // one is in the same library as « so it is.
872        assert!(t.command_at(0x1100, &mem).unwrap().silent);
873        // Without names: the pointer is still a command, not a program.
874        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        // A list holding a pointer to itself: bounded by the nesting limit.
890        let cycle = format!("47A20{}B2130", ptr(0x3000));
891        // An algebraic whose elements do not form an expression, one whose
892        // operator lacks operands, a unit without its operators' operands.
893        let loose = "8BA2084E2010184E2010288E20B2130";
894        let short = format!("8BA20{}B2130", ptr(0x1040));
895        // A program pointing at itself: a ROM-style command, not followed.
896        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    /// `copy` (a synthetic library image) at the start of each bank of
923    /// a two-bank 49G-style image.
924    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        // The CPU sees bank 0's copy at #1000 (its first command).
937        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        // Where they agree (command 1), the answer stands.
945        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        // Command 1's link entry points past the end of the image.
952        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        // An address whose bank offset would equal the old sentinel's.
960        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    /// `headers` library headers every 20 nibbles, all sharing one empty
991    /// hash table and one link table of `commands` entries.
992    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        // The number table's offset: right at the end, no names.
998        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        // More libraries than any ROM (the 49G has 52).
1018        assert_eq!(libraries(MAX_LIBRARIES + 1, 100), 0);
1019        // More commands than a 3-nibble command number can name.
1020        assert_eq!(libraries(1, MAX_LIBRARY_COMMANDS + 1), 0);
1021        assert_eq!(libraries(1, MAX_LIBRARY_COMMANDS), 1);
1022        // More commands together than any ROM (the 49G has 5940).
1023        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        // Three units 1_m ending in the end marker, one ending elsewhere.
1035        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}