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