Skip to main content

saturnus_objects/
menus.rs

1//! The built-in menus, read from the ROM image: which commands each menu
2//! number offers and which keys lead to submenus.
3//!
4//! Where the definitions are (wiki: protocols/rpl-libraries, "Built-in
5//! menus"): `MENU`'s own code names them, either as a list whose element
6//! n is menu n (48SX), or as a library whose command n is menu n (48GX and
7//! 49G: a library number in `MENU`'s code whose commands carry no names
8//! and are menu definitions). A definition is a list of keys, an array of
9//! XLIB names (one per key), or a program that builds the list. A key is
10//! a command (its name is the label), a `{ label action }` list whose
11//! action runs commands or switches to another menu, or a unit name.
12//!
13//! Nothing ROM-specific is compiled in: the reader finds the definitions
14//! from `MENU`'s code in the loaded image. Work is bounded: at most
15//! [`MAX_STEPS`] objects are visited, nesting at most [`MAX_NESTING`].
16
17use std::cell::Cell;
18use std::collections::{BTreeMap, HashMap, VecDeque};
19
20use crate::charset;
21use crate::names::NameTable;
22use crate::object::Memory;
23use crate::prolog::{ObjectType, SEMI, object_size, read_field};
24
25/// Most objects one read visits (the 49G's menu library holds about 440
26/// definitions of a few dozen keys each).
27pub const MAX_STEPS: usize = 1 << 18;
28/// Deepest nesting followed inside one definition.
29pub const MAX_NESTING: usize = 12;
30/// Elements read from one composite.
31const MAX_ELEMENTS: usize = 4096;
32/// How far `MENU`'s code is followed through other programs.
33const MAX_CODE_DEPTH: usize = 4;
34/// Fewest definitions a menu list or library has.
35const MIN_MENUS: usize = 20;
36
37/// What one menu offers.
38#[derive(Clone, Debug, Default, PartialEq, Eq)]
39pub struct MenuDef {
40    /// The commands its keys run, in key order, each once.
41    pub commands: Vec<String>,
42    /// Keys that switch to another menu: (label, menu number).
43    pub submenus: Vec<(String, u32)>,
44}
45
46/// The built-in menus of one ROM, by menu number.
47#[derive(Clone, Debug, Default, PartialEq, Eq)]
48pub struct Menus {
49    /// Definition of menu n at index n (`None`: no definition read).
50    menus: Vec<Option<MenuDef>>,
51    /// The menu library (48GX, 49G), when the menus are its commands.
52    library: Option<u16>,
53    /// CPU addresses of the list's definitions (48SX), by menu number.
54    addresses: HashMap<u32, u32>,
55    /// Whether the step budget ran out: later definitions may be missing
56    /// or short.
57    truncated: bool,
58}
59
60impl Menus {
61    /// The highest menu number plus one.
62    pub fn len(&self) -> usize {
63        self.menus.len()
64    }
65
66    /// Whether the read stopped at [`MAX_STEPS`] (garbage, or a ROM far
67    /// larger than any known): the result is incomplete.
68    pub fn truncated(&self) -> bool {
69        self.truncated
70    }
71
72    /// Whether no menu was found.
73    pub fn is_empty(&self) -> bool {
74        self.menus.is_empty()
75    }
76
77    /// Menu `n`'s definition.
78    pub fn get(&self, n: u32) -> Option<&MenuDef> {
79        self.menus.get(n as usize)?.as_ref()
80    }
81
82    /// The menu a current-menu pointer (`Layout::menu_ptr`) designates:
83    /// `pointer` is that pointer's value and `mem` the machine, which
84    /// holds either the definition itself (48SX) or an XLIB name of the
85    /// menu library's entry (48GX, 49G).
86    pub fn number(&self, pointer: u32, mem: &dyn Memory) -> Option<u32> {
87        if let Some(n) = self.addresses.get(&pointer) {
88            return Some(*n);
89        }
90        let lib = self.library?;
91        let field = |at: u32, w: u32| -> Option<u32> {
92            (0..w).rev().try_fold(0u32, |v, i| {
93                Some((v << 4) | u32::from(mem.nibble(at.checked_add(i)?)?))
94            })
95        };
96        if field(pointer, 5)? != ObjectType::XlibName.prolog() {
97            return None;
98        }
99        (field(pointer + 5, 3)? == u32::from(lib)).then(|| field(pointer + 8, 3))?
100    }
101
102    /// The menus with a definition: (number, definition).
103    pub fn iter(&self) -> impl Iterator<Item = (u32, &MenuDef)> {
104        self.menus
105            .iter()
106            .enumerate()
107            .filter_map(|(n, d)| Some((u32::try_from(n).ok()?, d.as_ref()?)))
108    }
109
110    /// Each menu's name: a root menu's from `roots` (the keyboard legend
111    /// of the key that opens it), a submenu's as its parent's name and the
112    /// label of the key that leads to it (`MTH HYP`), the shortest path
113    /// winning. A menu no key reaches is named as the calculator opens it,
114    /// `MENU n`, and its submenus after it (`MENU 62 FMT`).
115    pub fn paths(&self, roots: &[(u32, String)]) -> BTreeMap<u32, String> {
116        let mut out: BTreeMap<u32, String> = BTreeMap::new();
117        let mut queue: VecDeque<u32> = VecDeque::new();
118        for (n, name) in roots {
119            if self.get(*n).is_some() && !out.contains_key(n) {
120                out.insert(*n, name.clone());
121                queue.push_back(*n);
122            }
123        }
124        self.descend(&mut out, &mut queue);
125        let unnamed: Vec<u32> = self
126            .iter()
127            .map(|(n, _)| n)
128            .filter(|n| !out.contains_key(n))
129            .collect();
130        for n in unnamed {
131            if let std::collections::btree_map::Entry::Vacant(e) = out.entry(n) {
132                e.insert(format!("MENU {n}"));
133                queue.push_back(n);
134                self.descend(&mut out, &mut queue);
135            }
136        }
137        out
138    }
139
140    /// Name the submenus of the menus in `queue`, breadth first.
141    fn descend(&self, out: &mut BTreeMap<u32, String>, queue: &mut VecDeque<u32>) {
142        while let Some(n) = queue.pop_front() {
143            let (Some(def), Some(parent)) = (self.get(n), out.get(&n).cloned()) else {
144                continue;
145            };
146            for (label, sub) in &def.submenus {
147                if out.contains_key(sub) || self.get(*sub).is_none() {
148                    continue;
149                }
150                out.insert(*sub, format!("{parent} {label}"));
151                queue.push_back(*sub);
152            }
153        }
154    }
155
156    /// For each command, the names of the menus that offer it (in menu
157    /// number order), named by `paths`.
158    pub fn placement(&self, paths: &BTreeMap<u32, String>) -> BTreeMap<String, Vec<String>> {
159        let mut out: BTreeMap<String, Vec<String>> = BTreeMap::new();
160        for (n, def) in self.iter() {
161            let name = paths
162                .get(&n)
163                .cloned()
164                .unwrap_or_else(|| format!("MENU {n}"));
165            for c in &def.commands {
166                let list = out.entry(c.clone()).or_default();
167                if !list.contains(&name) {
168                    list.push(name.clone());
169                }
170            }
171        }
172        out
173    }
174}
175
176/// The 5-nibble field at `at`.
177fn f5(n: &[u8], at: usize) -> Option<u32> {
178    read_field(n, at, 5)
179}
180
181/// One element of a composite in the image.
182#[derive(Clone, Copy, Debug)]
183enum El {
184    /// An embedded object at this image index, with its prolog.
185    Embedded(usize, u32),
186    /// A 5-nibble pointer (a CPU address).
187    Pointer(u32),
188}
189
190/// Where the menu definitions are.
191#[derive(Clone, Debug)]
192enum Source {
193    /// Element n of the list at this image index is menu n.
194    List(Vec<El>),
195    /// Command n of this library is menu n.
196    Library(u16),
197}
198
199/// The ROM image as the CPU sees it while one object runs: on the 49G the
200/// low window shows the bank that holds `MENU` (the system's fixed bank)
201/// and the high window the bank of the object being read.
202#[derive(Clone, Copy)]
203struct View<'a> {
204    rom: &'a [u8],
205    bank: Option<usize>,
206    low: usize,
207    high: usize,
208}
209
210impl View<'_> {
211    /// The image index of CPU address `a`.
212    fn index(&self, a: u32) -> Option<usize> {
213        let a = a as usize;
214        let i = match self.bank {
215            None => a,
216            Some(b) if a < b => self.low * b + a,
217            Some(b) if a < 2 * b => self.high * b + (a - b),
218            Some(_) => return None,
219        };
220        (i < self.rom.len()).then_some(i)
221    }
222
223    /// The same view with the high window on the bank holding image index
224    /// `i`.
225    fn at(&self, i: usize) -> Self {
226        match self.bank {
227            Some(b) => View {
228                high: i / b,
229                ..*self
230            },
231            None => *self,
232        }
233    }
234}
235
236impl Memory for View<'_> {
237    fn nibble(&self, addr: u32) -> Option<u8> {
238        self.rom.get(self.index(addr)?).copied()
239    }
240}
241
242struct Reader<'a> {
243    rom: &'a [u8],
244    names: &'a NameTable,
245    view: View<'a>,
246    steps: Cell<usize>,
247    /// The budget of steps.
248    max_steps: usize,
249}
250
251impl<'a> Reader<'a> {
252    /// Charge one visited object; false once the budget is spent.
253    fn step(&self) -> bool {
254        let s = self.steps.get();
255        if s >= self.max_steps {
256            return false;
257        }
258        self.steps.set(s + 1);
259        true
260    }
261
262    fn prolog(&self, i: usize) -> Option<u32> {
263        f5(self.rom, i)
264    }
265
266    /// The elements of the list, program or algebraic at image index `i`.
267    fn elements(&self, i: usize) -> Vec<El> {
268        let mut out = Vec::new();
269        let mut at = i + 5;
270        while out.len() < MAX_ELEMENTS && self.step() {
271            let Some(p) = f5(self.rom, at) else { break };
272            if p == SEMI {
273                break;
274            }
275            if ObjectType::from_prolog(p).is_some() {
276                let Ok(size) = object_size(self.rom, at) else {
277                    break;
278                };
279                out.push(El::Embedded(at, p));
280                at += size;
281            } else if (0x02600..=0x02FFF).contains(&p) {
282                break;
283            } else {
284                out.push(El::Pointer(p));
285                at += 5;
286            }
287        }
288        out
289    }
290
291    /// The XLIB body (library, command) at image index `i`.
292    fn xlib_at(&self, i: usize) -> Option<(u16, u16)> {
293        Some((
294            read_field(self.rom, i, 3)? as u16,
295            read_field(self.rom, i + 3, 3)? as u16,
296        ))
297    }
298
299    /// The named command an element is, if any.
300    fn command(&self, e: El, view: View<'_>) -> Option<String> {
301        match e {
302            El::Embedded(i, p) if p == ObjectType::XlibName.prolog() => {
303                let (l, c) = self.xlib_at(i + 5)?;
304                self.names.xlib(l, c)?.name.map(str::to_string)
305            }
306            El::Pointer(a) => self.names.command_at(a, &view)?.name.map(str::to_string),
307            El::Embedded(..) => None,
308        }
309    }
310
311    /// The image index an element's object is at (an embedded object, a
312    /// pointer's target, an XLIB name's object), with the view to read it.
313    fn target(&self, e: El, view: View<'a>) -> Option<(usize, View<'a>)> {
314        match e {
315            El::Embedded(i, p) if p == ObjectType::XlibName.prolog() => {
316                let (l, c) = self.xlib_at(i + 5)?;
317                let t = self.names.object_index(l, c)?;
318                Some((t, view.at(t)))
319            }
320            El::Embedded(i, _) => Some((i, view)),
321            El::Pointer(a) => Some((view.index(a)?, view)),
322        }
323    }
324
325    /// The XLIB names of an array of them at image index `i`.
326    fn xlib_array(&self, i: usize) -> Option<Vec<(u16, u16)>> {
327        if self.prolog(i)? != ObjectType::Array.prolog()
328            || f5(self.rom, i + 10)? != ObjectType::XlibName.prolog()
329            || f5(self.rom, i + 15)? != 1
330        {
331            return None;
332        }
333        let count = f5(self.rom, i + 20)? as usize;
334        if count > MAX_ELEMENTS {
335            return None;
336        }
337        (0..count).map(|k| self.xlib_at(i + 25 + 6 * k)).collect()
338    }
339}
340
341/// Collects one definition.
342struct Collect<'r, 'a> {
343    r: &'r Reader<'a>,
344    source: &'r Source,
345    /// Image indexes of the list source's definitions, by menu number.
346    defs: HashMap<usize, u32>,
347    def: MenuDef,
348}
349
350impl<'a> Collect<'_, 'a> {
351    fn add_command(&mut self, name: String) {
352        if !self.def.commands.contains(&name) {
353            self.def.commands.push(name);
354        }
355    }
356
357    /// The menu an element switches to, if it is a reference to a menu
358    /// definition (an XLIB name of the menu library, or a pointer to one
359    /// of the list's definitions).
360    fn menu_ref(&self, e: El, view: View<'a>) -> Option<u32> {
361        match (e, self.source) {
362            (El::Embedded(i, p), Source::Library(lib)) if p == ObjectType::XlibName.prolog() => {
363                let (l, c) = self.r.xlib_at(i + 5)?;
364                (l == *lib).then_some(u32::from(c))
365            }
366            (El::Pointer(a), Source::List(_)) => self.defs.get(&view.index(a)?).copied(),
367            (El::Embedded(i, _), Source::List(_)) => self.defs.get(&i).copied(),
368            _ => None,
369        }
370    }
371
372    /// The first menu that the object at `i` switches to, looking into
373    /// its programs and lists.
374    fn find_menu(&self, i: usize, view: View<'a>, depth: usize) -> Option<u32> {
375        if depth > MAX_NESTING || !composite(self.r.prolog(i)?) {
376            return None;
377        }
378        for e in self.r.elements(i) {
379            if let Some(n) = self.menu_ref(e, view) {
380                return Some(n);
381            }
382            if let El::Embedded(j, p) = e
383                && composite(p)
384                && let Some(n) = self.find_menu(j, view, depth + 1)
385            {
386                return Some(n);
387            }
388        }
389        None
390    }
391
392    /// Every named command inside the object at `i` (not through pointers
393    /// to unnamed ROM code).
394    fn commands_in(&mut self, i: usize, view: View<'a>, depth: usize) {
395        if depth > MAX_NESTING {
396            return;
397        }
398        let Some(p) = self.r.prolog(i) else { return };
399        if p == ObjectType::XlibName.prolog() {
400            if let Some(name) = self.r.command(El::Embedded(i, p), view) {
401                self.add_command(name);
402            }
403            return;
404        }
405        if !composite(p) {
406            return;
407        }
408        for e in self.r.elements(i) {
409            if let Some(name) = self.r.command(e, view) {
410                self.add_command(name);
411            } else if let El::Embedded(j, q) = e
412                && composite(q)
413            {
414                self.commands_in(j, view, depth + 1);
415            }
416        }
417    }
418
419    /// The text of a key's label: a string, the first string inside a
420    /// program that draws it, or a command (shown by its name).
421    fn label(&self, e: El, view: View<'a>, depth: usize) -> Option<String> {
422        if let Some(name) = self.r.command(e, view) {
423            return Some(name);
424        }
425        let (i, view) = self.r.target(e, view)?;
426        let p = self.r.prolog(i)?;
427        if p == ObjectType::String.prolog() {
428            let len = (f5(self.r.rom, i + 5)? as usize).checked_sub(5)? / 2;
429            let bytes: Option<Vec<u8>> = (0..len)
430                .map(|k| read_field(self.r.rom, i + 10 + 2 * k, 2).map(|b| b as u8))
431                .collect();
432            // Without the control characters some labels carry (the
433            // 49G's folder mark).
434            let text: String = charset::decode(&bytes?)
435                .chars()
436                .filter(|c| !c.is_control())
437                .collect();
438            return Some(text.trim().to_string()).filter(|t| !t.is_empty());
439        }
440        if p == ObjectType::Program.prolog() && depth < 2 {
441            return self
442                .r
443                .elements(i)
444                .into_iter()
445                .find_map(|x| self.label(x, view, depth + 1));
446        }
447        None
448    }
449
450    /// One key.
451    fn key(&mut self, e: El, view: View<'a>, depth: usize) {
452        if depth > MAX_NESTING || !self.r.step() {
453            return;
454        }
455        if let Some(name) = self.r.command(e, view) {
456            self.add_command(name);
457            return;
458        }
459        let Some((i, view)) = self.r.target(e, view) else {
460            return;
461        };
462        let Some(p) = self.r.prolog(i) else { return };
463        if p == ObjectType::List.prolog() {
464            let els = self.r.elements(i);
465            // A key is `{ LabelObj Action }`, the action a list of the
466            // unshifted, left- and right-shifted actions where it has
467            // several; the label is only shown (RPLMAN 21.4.7, "Menu Key
468            // Assignments"). A list of one object is that object as the
469            // key: its label and what it runs.
470            if let [only] = els.as_slice() {
471                self.key(*only, view, depth + 1);
472                return;
473            }
474            let labelled = els
475                .split_first()
476                .and_then(|(&first, rest)| Some((self.label(first, view, 0)?, rest)));
477            if let Some((label, rest)) = labelled {
478                // A key that leads to a submenu offers that menu, not the
479                // commands its shifted variants type (`IF`'s `IF THEN
480                // END`): those are offered in the submenu itself.
481                let submenu = rest.iter().find_map(|&a| {
482                    self.menu_ref(a, view).or_else(|| {
483                        let (j, v) = self.r.target(a, view)?;
484                        self.find_menu(j, v, depth + 1)
485                    })
486                });
487                if let Some(n) = submenu {
488                    self.def.submenus.push((label, n));
489                    return;
490                }
491                for &a in rest {
492                    if let Some(name) = self.r.command(a, view) {
493                        self.add_command(name);
494                    } else if let Some((j, v)) = self.r.target(a, view) {
495                        self.commands_in(j, v, depth + 1);
496                    }
497                }
498                return;
499            }
500            self.commands_in(i, view, depth + 1);
501        } else if p == ObjectType::Program.prolog() {
502            self.commands_in(i, view, depth + 1);
503        }
504    }
505
506    /// The keys of a definition at image index `i`.
507    fn definition(&mut self, i: usize, view: View<'a>, depth: usize) {
508        if depth > 3 {
509            return;
510        }
511        let Some(p) = self.r.prolog(i) else { return };
512        if let Some(keys) = self.r.xlib_array(i) {
513            for (l, c) in keys {
514                // Built as an XLIB name element, read the same way.
515                match self.r.names.xlib(l, c).and_then(|x| x.name) {
516                    Some(name) => self.add_command(name.to_string()),
517                    None => {
518                        if let Some(t) = self.r.names.object_index(l, c) {
519                            let v = view.at(t);
520                            self.key_object(t, v, depth + 1);
521                        }
522                    }
523                }
524            }
525        } else if p == ObjectType::XlibName.prolog() {
526            if let Some((t, v)) = self.r.target(El::Embedded(i, p), view) {
527                self.definition(t, v, depth + 1);
528            }
529        } else if p == ObjectType::List.prolog() {
530            for e in self.r.elements(i) {
531                self.key(e, view, 1);
532            }
533        } else if p == ObjectType::Program.prolog() {
534            // A program that builds the menu: its list of keys.
535            if let Some(El::Embedded(j, _)) = self
536                .r
537                .elements(i)
538                .into_iter()
539                .find(|e| matches!(e, El::Embedded(_, q) if *q == ObjectType::List.prolog()))
540            {
541                self.definition(j, view, depth + 1);
542            }
543        }
544    }
545
546    /// A key stored as its own object (a library command that is a
547    /// `{ label action }` list or a command).
548    fn key_object(&mut self, t: usize, view: View<'a>, depth: usize) {
549        let p = self.r.prolog(t);
550        if p == Some(ObjectType::List.prolog()) || p == Some(ObjectType::Program.prolog()) {
551            self.key(El::Embedded(t, p.unwrap_or(0)), view, depth);
552        }
553    }
554}
555
556fn composite(p: u32) -> bool {
557    p == ObjectType::List.prolog() || p == ObjectType::Program.prolog()
558}
559
560/// The menus of the ROM image `rom` (one nibble per element), named
561/// through `names`, the table built from the same image for its model
562/// (which also knows whether the image is banked). `None` when `MENU` or
563/// its definitions cannot be found (also for models whose name table is
564/// empty: the 38G, 39G, 40G and 42S).
565pub fn read(rom: &[u8], names: &NameTable) -> Option<Menus> {
566    read_within(rom, names, MAX_STEPS)
567}
568
569/// [`read`] with a budget of `max_steps` objects.
570fn read_within(rom: &[u8], names: &NameTable, max_steps: usize) -> Option<Menus> {
571    let (lib, cmd) = names.find("MENU")?;
572    let menu = names.object_index(lib, cmd)?;
573    let bank = names.bank();
574    let low = bank.map_or(0, |b| menu / b);
575    let view = View {
576        rom,
577        bank,
578        low,
579        high: low,
580    };
581    let r = Reader {
582        rom,
583        names,
584        view,
585        steps: Cell::new(0),
586        max_steps,
587    };
588    let source = locate(&r, menu)?;
589    let count = match &source {
590        Source::List(els) => els.len() + 1,
591        Source::Library(l) => names.library_size(*l)?.0,
592    };
593    let mut c = Collect {
594        r: &r,
595        source: &source,
596        defs: HashMap::new(),
597        def: MenuDef::default(),
598    };
599    if let Source::List(els) = &source {
600        for (k, &e) in els.iter().enumerate() {
601            if let Some((i, _)) = r.target(e, view) {
602                c.defs.insert(i, u32::try_from(k + 1).ok()?);
603            }
604        }
605    }
606    let mut addresses = HashMap::new();
607    if let Source::List(els) = &source {
608        for (k, &e) in els.iter().enumerate() {
609            let a = match e {
610                El::Pointer(a) => Some(a),
611                // An embedded definition: its address is its image index
612                // (unbanked ROM).
613                El::Embedded(i, _) => u32::try_from(i).ok().filter(|_| bank.is_none()),
614            };
615            if let (Some(a), Ok(n)) = (a, u32::try_from(k + 1)) {
616                addresses.insert(a, n);
617            }
618        }
619    }
620    let library = match &source {
621        Source::Library(l) => Some(*l),
622        Source::List(_) => None,
623    };
624    let mut menus = vec![None; count];
625    for (n, slot) in menus.iter_mut().enumerate() {
626        let at = match &source {
627            // Menu 0 is the last menu: no definition of its own.
628            Source::List(els) => n.checked_sub(1).and_then(|k| els.get(k).copied()),
629            Source::Library(l) => names
630                .object_index(*l, u16::try_from(n).ok()?)
631                .and_then(|t| f5(rom, t).map(|p| El::Embedded(t, p))),
632        };
633        let Some(at) = at else { continue };
634        let Some((i, v)) = r.target(at, view) else {
635            continue;
636        };
637        c.def = MenuDef::default();
638        c.definition(i, v.at(i), 0);
639        *slot = Some(std::mem::take(&mut c.def));
640    }
641    Some(Menus {
642        menus,
643        library,
644        addresses,
645        truncated: r.steps.get() >= max_steps,
646    })
647}
648
649/// Where `MENU`'s code (the program at image index `menu`, followed
650/// through the unnamed programs it calls) finds the definitions: the
651/// largest list of at least [`MIN_MENUS`] composites, or a library named
652/// by a number in the code whose commands are unnamed menu definitions.
653fn locate(r: &Reader<'_>, menu: usize) -> Option<Source> {
654    let mut best: Option<(usize, Source)> = None;
655    let mut seen = std::collections::HashSet::new();
656    let mut queue: VecDeque<(usize, usize)> = VecDeque::from([(menu, 0)]);
657    let view = r.view;
658    while let Some((i, depth)) = queue.pop_front() {
659        if depth > MAX_CODE_DEPTH || !seen.insert(i) || !r.step() {
660            continue;
661        }
662        if r.prolog(i) != Some(ObjectType::Program.prolog()) {
663            continue;
664        }
665        for e in r.elements(i) {
666            let (t, embedded) = match e {
667                El::Embedded(j, _) => (j, true),
668                El::Pointer(a) => {
669                    if r.names.command_at(a, &view).is_some() {
670                        continue;
671                    }
672                    let Some(t) = view.index(a) else { continue };
673                    (t, false)
674                }
675            };
676            let Some(p) = r.prolog(t) else { continue };
677            if p == ObjectType::Program.prolog() {
678                queue.push_back((t, if embedded { depth } else { depth + 1 }));
679            } else if p == ObjectType::List.prolog() {
680                let els = r.elements(t);
681                let menus = els
682                    .iter()
683                    .filter(|&&x| {
684                        r.target(x, view)
685                            .and_then(|(j, _)| r.prolog(j))
686                            .is_some_and(composite)
687                    })
688                    .count();
689                if els.len() >= MIN_MENUS && menus * 5 >= els.len() * 4 {
690                    consider(&mut best, menus, Source::List(els));
691                }
692            } else if p == ObjectType::SystemBinary.prolog() {
693                let Some(v) = f5(r.rom, t + 5).and_then(|v| u16::try_from(v).ok()) else {
694                    continue;
695                };
696                let Some((len, named)) = r.names.library_size(v) else {
697                    continue;
698                };
699                if len < MIN_MENUS || named > 0 {
700                    continue;
701                }
702                let menus = (0..len)
703                    .filter(|&c| {
704                        u16::try_from(c)
705                            .ok()
706                            .and_then(|c| r.names.object_index(v, c))
707                            .is_some_and(|j| {
708                                r.xlib_array(j).is_some()
709                                    || r.prolog(j) == Some(ObjectType::List.prolog())
710                            })
711                    })
712                    .count();
713                if menus * 2 >= len {
714                    consider(&mut best, menus, Source::Library(v));
715                }
716            }
717        }
718    }
719    best.map(|(_, s)| s)
720}
721
722fn consider(best: &mut Option<(usize, Source)>, score: usize, s: Source) {
723    if best.as_ref().is_none_or(|(b, _)| score > *b) {
724        *best = Some((score, s));
725    }
726}
727
728#[cfg(test)]
729mod tests {
730    use super::*;
731    use saturnus::Model;
732
733    fn put(n: &mut [u8], at: usize, value: usize, width: usize) {
734        for i in 0..width {
735            n[at + i] = ((value >> (4 * i)) & 0xF) as u8;
736        }
737    }
738
739    fn put_rel(n: &mut [u8], at: usize, target: usize) {
740        put(n, at, (target + 0x100000 - at) & 0xFFFFF, 5);
741    }
742
743    fn put_hex(n: &mut [u8], at: usize, hex: &str) -> usize {
744        for (i, c) in hex.chars().enumerate() {
745            n[at + i] = c.to_digit(16).unwrap() as u8;
746        }
747        at + hex.len()
748    }
749
750    fn ptr(a: usize) -> String {
751        (0..5)
752            .map(|i| format!("{:X}", (a >> (4 * i)) & 0xF))
753            .collect()
754    }
755
756    fn xlib(lib: usize, cmd: usize) -> String {
757        format!("29E20{}", body(lib, cmd))
758    }
759
760    fn body(lib: usize, cmd: usize) -> String {
761        (0..3)
762            .map(|i| format!("{:X}", (lib >> (4 * i)) & 0xF))
763            .chain((0..3).map(|i| format!("{:X}", (cmd >> (4 * i)) & 0xF)))
764            .collect()
765    }
766
767    fn string(s: &str) -> String {
768        let mut h = format!("C2A20{}", ptr(5 + 2 * s.len()));
769        for b in s.bytes() {
770            h.push_str(&format!("{:X}{:X}", b & 0xF, b >> 4));
771        }
772        h
773    }
774
775    /// Library `id` at `base`: names by command number, and each
776    /// command's object (hex, placed from `base + 0x400` on, 0x100 apart,
777    /// each after its XLIB body). Returns the objects' addresses.
778    fn library(
779        n: &mut [u8],
780        base: usize,
781        id: usize,
782        names: &[Option<&str>],
783        objects: &[String],
784    ) -> Vec<usize> {
785        let (hash, link) = (base + 0x30, base + 0x300);
786        let body_at = hash + 10;
787        let mut at = body_at + 85;
788        let mut entries = vec![0; names.len()];
789        for len in 1..=16 {
790            for (c, name) in names.iter().enumerate() {
791                let Some(name) = name else { continue };
792                if name.len() != len {
793                    continue;
794                }
795                if read_field(n, body_at + 5 * (len - 1), 5) == Some(0) {
796                    put_rel(n, body_at + 5 * (len - 1), at);
797                }
798                entries[c] = at;
799                put(n, at, len, 2);
800                for (i, b) in name.bytes().enumerate() {
801                    put(n, at + 2 + 2 * i, usize::from(b), 2);
802                }
803                put(n, at + 2 + 2 * len, c, 3);
804                at += 2 + 2 * len + 3;
805            }
806        }
807        put_rel(n, body_at + 80, at);
808        for (c, &e) in entries.iter().enumerate() {
809            let f = at + 5 * c;
810            put(n, f, if e == 0 { 0 } else { f - e }, 5);
811        }
812        let end = at + 5 * names.len();
813        assert!(end < link);
814        put(n, hash, 0x02A4E, 5);
815        put(n, hash + 5, end - hash - 5, 5);
816        put(n, link, 0x02A4E, 5);
817        put(n, link + 5, 5 + 5 * names.len(), 5);
818        let mut addrs = Vec::new();
819        for (c, o) in objects.iter().enumerate() {
820            let obj = base + 0x400 + 0x100 * c;
821            put_hex(n, obj - 6, &body(id, c));
822            assert!(o.len() < 0xF0);
823            put_hex(n, obj, o);
824            put_rel(n, link + 10 + 5 * c, obj);
825            addrs.push(obj);
826        }
827        put(n, base, id, 3);
828        put_rel(n, base + 3, hash);
829        put_rel(n, base + 13, link);
830        addrs
831    }
832
833    /// Library 2 with MENU (a program around `menu_body`) and SIN, COS,
834    /// TAN (programs).
835    fn builtins(n: &mut [u8], menu_body: &str) -> Vec<usize> {
836        let prog = "D9D20B2130".to_string();
837        library(
838            n,
839            0x100,
840            2,
841            &[Some("MENU"), Some("SIN"), Some("COS"), Some("TAN")],
842            &[
843                format!("D9D20{menu_body}B2130"),
844                prog.clone(),
845                prog.clone(),
846                prog,
847            ],
848        )
849    }
850
851    #[test]
852    fn a_list_of_definitions_as_on_the_48sx() {
853        let mut n = vec![0u8; 0x8000];
854        let list = 0x4000;
855        let a = builtins(&mut n, &ptr(list));
856        let (sin, cos, tan) = (a[1], a[2], a[3]);
857        // Menu 1: SIN and a key { "TRIG" menu 2 }; menu 2: COS TAN and a
858        // labelled key { "T" TAN } again; menus 3-24: empty lists.
859        let mut at = put_hex(&mut n, list, "47A20");
860        let def1 = |m2: usize| {
861            format!(
862                "47A20{}47A20{}{}B2130B2130",
863                ptr(sin),
864                string("TRIG"),
865                ptr(m2)
866            )
867        };
868        let m2_at = at + def1(0).len();
869        at = put_hex(&mut n, at, &def1(m2_at));
870        at = put_hex(
871            &mut n,
872            at,
873            &format!(
874                "47A20{}{}47A20{}{}B2130B2130",
875                ptr(cos),
876                ptr(tan),
877                string("T"),
878                ptr(tan)
879            ),
880        );
881        for _ in 3..=24 {
882            at = put_hex(&mut n, at, "47A20B2130");
883        }
884        put_hex(&mut n, at, "B2130");
885        let names = NameTable::build(Model::Hp48sx, &n);
886        let m = read(&n, &names).unwrap();
887        assert_eq!(m.len(), 25);
888        assert_eq!(m.get(1).unwrap().commands, ["SIN"]);
889        assert_eq!(m.get(1).unwrap().submenus, [("TRIG".to_string(), 2)]);
890        assert_eq!(m.get(2).unwrap().commands, ["COS", "TAN"]);
891        let paths = m.paths(&[(1, "MATH".into())]);
892        assert_eq!(paths[&2], "MATH TRIG");
893        assert_eq!(paths[&3], "MENU 3");
894        let p = m.placement(&paths);
895        assert_eq!(p["TAN"], ["MATH TRIG"]);
896        assert_eq!(p["SIN"], ["MATH"]);
897        // The current-menu pointer on the 48SX is the definition itself.
898        struct Image<'a>(&'a [u8]);
899        impl Memory for Image<'_> {
900            fn nibble(&self, a: u32) -> Option<u8> {
901                self.0.get(a as usize).copied()
902            }
903        }
904        assert_eq!(m.number(m2_at as u32, &Image(&n)), Some(2));
905    }
906
907    #[test]
908    fn a_menu_library_as_on_the_48gx() {
909        let mut n = vec![0u8; 0x10000];
910        // MENU's code names library #A9 by a system binary.
911        builtins(&mut n, "119209A000");
912        // Library #A8: key objects. #A9: menu n = command n.
913        let keys = library(
914            &mut n,
915            0x8000,
916            0xA8,
917            &[None, None],
918            &[
919                format!("47A20{}D9D20{}B2130B2130", string("SUBM"), xlib(0xA9, 2)),
920                format!("47A20{}{}B2130", string("C"), xlib(2, 2)),
921            ],
922        );
923        let _ = keys;
924        let array = |items: &[String]| {
925            let body: String = items.iter().map(|x| x[5..].to_string()).collect();
926            format!(
927                "8E920{}29E2010000{}{}",
928                ptr(25 + 6 * items.len() - 5),
929                ptr(items.len()),
930                body
931            )
932        };
933        let mut defs = vec!["47A20B2130".to_string(); 22];
934        defs[1] = array(&[xlib(2, 1), xlib(0xA8, 0)]);
935        defs[2] = array(&[xlib(0xA8, 1), xlib(2, 3)]);
936        library(&mut n, 0xA000, 0xA9, &vec![None; 22], &defs);
937        let names = NameTable::build(Model::Hp48gx, &n);
938        let m = read(&n, &names).unwrap();
939        assert_eq!(m.len(), 22);
940        assert_eq!(m.get(1).unwrap().commands, ["SIN"]);
941        assert_eq!(m.get(1).unwrap().submenus, [("SUBM".to_string(), 2)]);
942        assert_eq!(m.get(2).unwrap().commands, ["COS", "TAN"]);
943        // The current-menu pointer designates an XLIB name of #A9.
944        let mut ram = n.clone();
945        put_hex(&mut ram, 0xF000, &xlib(0xA9, 2));
946        struct Image<'a>(&'a [u8]);
947        impl Memory for Image<'_> {
948            fn nibble(&self, a: u32) -> Option<u8> {
949                self.0.get(a as usize).copied()
950            }
951        }
952        assert_eq!(m.number(0xF000, &Image(&ram)), Some(2));
953    }
954
955    #[test]
956    fn single_object_keys_and_labels() {
957        let mut n = vec![0u8; 0x8000];
958        let list = 0x4000;
959        let a = builtins(&mut n, &ptr(list));
960        let (sin, cos, tan) = (a[1], a[2], a[3]);
961        // Menu 1: { SIN COS } shows SIN and runs COS; { TAN } is TAN.
962        let mut at = put_hex(&mut n, list, "47A20");
963        at = put_hex(
964            &mut n,
965            at,
966            &format!(
967                "47A2047A20{}{}B213047A20{}B2130B2130",
968                ptr(sin),
969                ptr(cos),
970                ptr(tan)
971            ),
972        );
973        for _ in 2..=24 {
974            at = put_hex(&mut n, at, "47A20B2130");
975        }
976        put_hex(&mut n, at, "B2130");
977        let names = NameTable::build(Model::Hp48sx, &n);
978        let m = read(&n, &names).unwrap();
979        assert_eq!(m.get(1).unwrap().commands, ["COS", "TAN"]);
980        assert!(!m.truncated());
981        // Out of steps: the result says so.
982        let short = read_within(&n, &names, 30).unwrap();
983        assert!(short.truncated());
984    }
985
986    #[test]
987    fn garbage_finds_no_menus() {
988        let mut n = vec![0u8; 0x8000];
989        for (i, x) in n.iter_mut().enumerate() {
990            *x = ((i * 7919) >> 3) as u8 & 0xF;
991        }
992        let names = NameTable::build(Model::Hp48sx, &n);
993        assert!(read(&n, &names).is_none());
994        // MENU whose code points at itself and at garbage: bounded, none.
995        let mut n = vec![0u8; 0x8000];
996        let a = builtins(&mut n, &format!("{}{}", ptr(0x500), ptr(0x7FF0)));
997        let _ = a;
998        let names = NameTable::build(Model::Hp48sx, &n);
999        assert!(read(&n, &names).is_none());
1000        assert!(read(&n, &NameTable::default()).is_none());
1001    }
1002}