Skip to main content

saturnus_objects/
ram.rs

1//! A read-only view of a paused calculator's user memory: the HOME
2//! directory tree, the current directory, the data stack and the flags,
3//! read straight from RAM through the ROM's own system pointers. Nothing
4//! is written and the calculator does not change mode, so a host can show
5//! it at any time; the values are those the ROM left when it last went
6//! idle (in its outer loop, the stack showing), which is where a paused
7//! machine normally sits.
8//!
9//! Locations per model (48SX, 48GX, 49G) and the layouts: wiki:
10//! hardware/hp48-system-ram. The 38G, 39G and 40G keep aplets, not a HOME
11//! tree, and the 42S has no RPL user memory (its RAM holds HP-41-style
12//! programs and registers; wiki: hardware/hp42s); neither is covered.
13
14use saturnus_core::{Machine, Model};
15use serde::Serialize;
16
17use anyhow::{Context, Result, bail, ensure};
18
19use crate::decompile::Settings;
20use crate::names::NameTable;
21use crate::object::{Base, Memory, Object, Reader};
22use crate::prolog::{ObjectType, Record, records};
23
24/// Most stack levels read (the 49G's 256 KB of RAM holds about 50 000
25/// pointers; more means the pointers are not the ROM's).
26const MAX_LEVELS: usize = 100_000;
27/// Largest region read in one piece, in nibbles (the whole address space).
28const MAX_REGION: u32 = 1 << 20;
29
30impl Memory for Machine {
31    fn nibble(&self, addr: u32) -> Option<u8> {
32        (addr < MAX_REGION).then(|| self.peek(addr))
33    }
34}
35
36/// Where a model's ROM keeps its system pointers and flags in RAM (wiki:
37/// hardware/hp48-system-ram). Each `*_ptr` field is the address of a
38/// 5-nibble pointer.
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub struct Layout {
41    /// The HOME directory object.
42    pub home_ptr: u32,
43    /// The nibble after HOME.
44    pub home_end_ptr: u32,
45    /// The current (context) directory object.
46    pub context_ptr: u32,
47    /// The saved data stack pointer (D1): level 1's entry.
48    pub stack_ptr: u32,
49    /// The nibble after the data stack's 0 end marker.
50    pub stack_end_ptr: u32,
51    /// The system flags -1, -2, ... (bit 0 of the first nibble is -1).
52    pub system_flags: u32,
53    /// The user flags 1, 2, ...
54    pub user_flags: u32,
55    /// 64-flag words of each kind: 1 on the 48, 2 on the 49G.
56    pub flag_words: usize,
57    /// Pointer to the menu the soft keys show: its definition (48SX) or an
58    /// XLIB name of the menu library's entry for it (48GX, 49G); wiki:
59    /// hardware/hp48-system-ram, "Menu".
60    pub menu_ptr: u32,
61}
62
63impl Layout {
64    /// 48SX (ROM J; the same addresses in the 1991 entry list of ROM E).
65    pub const HP48SX: Layout = Layout {
66        home_ptr: 0x70592,
67        home_end_ptr: 0x70597,
68        context_ptr: 0x7059C,
69        stack_ptr: 0x70579,
70        stack_end_ptr: 0x7057E,
71        system_flags: 0x706C5,
72        user_flags: 0x706D5,
73        flag_words: 1,
74        menu_ptr: 0x7061E,
75    };
76    /// 48GX (ROM R).
77    pub const HP48GX: Layout = Layout {
78        home_ptr: 0x80711,
79        home_end_ptr: 0x80716,
80        context_ptr: 0x8071B,
81        stack_ptr: 0x806F8,
82        stack_end_ptr: 0x806FD,
83        system_flags: 0x80843,
84        user_flags: 0x80853,
85        flag_words: 1,
86        menu_ptr: 0x8079D,
87    };
88    /// 49G (ROM 2.10).
89    pub const HP49G: Layout = Layout {
90        home_ptr: 0x80711,
91        home_end_ptr: 0x80716,
92        context_ptr: 0x8071B,
93        stack_ptr: 0x806F8,
94        stack_end_ptr: 0x806FD,
95        system_flags: 0x80F02,
96        user_flags: 0x80F22,
97        flag_words: 2,
98        menu_ptr: 0x807ED,
99    };
100
101    /// The layout of `model`, or `None` for the aplet models, the Pioneers (42S, 27S,
102    /// 17BII, 17B) and the 28S.
103    pub fn of(model: Model) -> Option<Layout> {
104        match model {
105            Model::Hp48sx => Some(Self::HP48SX),
106            Model::Hp48gx => Some(Self::HP48GX),
107            Model::Hp49g => Some(Self::HP49G),
108            Model::Hp38g
109            | Model::Hp39g
110            | Model::Hp40g
111            | Model::Hp42s
112            | Model::Hp27s
113            | Model::Hp17bii
114            | Model::Hp17b
115            | Model::Hp28s => None,
116        }
117    }
118}
119
120/// A variable of a directory as the calculator lists it (`VARS` order,
121/// `BYTES` size and checksum).
122#[derive(Clone, Debug, PartialEq, Serialize)]
123pub struct Variable {
124    /// Name (HP characters as Unicode).
125    pub name: String,
126    /// The type name as `G D` prints it, e.g. `Real Number`, `Directory`.
127    #[serde(rename = "type")]
128    pub kind: String,
129    /// Size in bytes as `BYTES` reports it: the whole variable record
130    /// (name and object), so it can end in .5.
131    pub size: f64,
132    /// `BYTES`'s checksum: the CRC of the object's nibbles.
133    pub checksum: u16,
134    /// Address of the object in memory.
135    pub address: u32,
136    /// A directory's variables, newest first.
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub variables: Option<Vec<Variable>>,
139}
140
141/// The flags as 64-flag words, flag 1 (or -1) in bit 0 of the first.
142#[derive(Clone, Debug, PartialEq, Eq)]
143pub struct Flags {
144    /// System flags: -1..-64, then -65..-128 on the 49G.
145    pub system: Vec<u64>,
146    /// User flags: 1..64, then 65..128 on the 49G.
147    pub user: Vec<u64>,
148}
149
150impl Flags {
151    /// Whether flag `n` is set: negative for system flags, positive for
152    /// user flags; `None` for 0 or past the model's flags.
153    pub fn get(&self, n: i32) -> Option<bool> {
154        let (words, i) = match n {
155            0 => return None,
156            n if n < 0 => (&self.system, n.unsigned_abs() - 1),
157            n => (&self.user, n.unsigned_abs() - 1),
158        };
159        let word = words.get(usize::try_from(i / 64).ok()?)?;
160        Some(word >> (i % 64) & 1 == 1)
161    }
162
163    /// The binary integer display base (flags -11 and -12).
164    pub fn base(&self) -> Base {
165        match (self.get(-11), self.get(-12)) {
166            (Some(true), Some(true)) => Base::Hex,
167            (Some(true), _) => Base::Oct,
168            (_, Some(true)) => Base::Bin,
169            _ => Base::Dec,
170        }
171    }
172
173    /// The flags that are set, system flags first (-1, -2, ...), then user
174    /// flags (1, 2, ...).
175    pub fn set(&self) -> Vec<i32> {
176        let mut out = Vec::new();
177        for (words, sign) in [(&self.system, -1i32), (&self.user, 1)] {
178            for (w, word) in words.iter().enumerate() {
179                for bit in 0..64 {
180                    if word >> bit & 1 == 1 {
181                        let n = i32::try_from(w * 64 + bit + 1).unwrap_or(i32::MAX);
182                        out.push(sign * n);
183                    }
184                }
185            }
186        }
187        out
188    }
189}
190
191impl Serialize for Flags {
192    /// `{"system": ["0000000204010FF0"], "user": [...], "set": [-5, ...]}`:
193    /// words as 16 hex digits (a JSON number would lose bits), and the set
194    /// flags.
195    fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
196        use serde::ser::SerializeStruct;
197        let hex = |w: &Vec<u64>| w.iter().map(|v| format!("{v:016X}")).collect::<Vec<_>>();
198        let mut st = s.serialize_struct("Flags", 3)?;
199        st.serialize_field("system", &hex(&self.system))?;
200        st.serialize_field("user", &hex(&self.user))?;
201        st.serialize_field("set", &self.set())?;
202        st.end()
203    }
204}
205
206/// The calculator's CRC over `nibbles` (wiki: hardware/crc; `BYTES` zeroes
207/// the CRC register and reads the object).
208pub fn crc(nibbles: &[u8]) -> u16 {
209    let mut c: u32 = 0;
210    for &n in nibbles {
211        c = (c >> 4) ^ (((c ^ u32::from(n)) & 0xF) * 0x1081);
212    }
213    (c & 0xFFFF) as u16
214}
215
216/// User memory of a paused machine, read through `layout`.
217#[derive(Clone, Copy)]
218pub struct UserMemory<'a> {
219    mem: &'a dyn Memory,
220    layout: Layout,
221    /// The ROM's command names: with them, commands are named and
222    /// programs, algebraics and units get their text.
223    names: Option<&'a NameTable>,
224}
225
226impl std::fmt::Debug for UserMemory<'_> {
227    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
228        f.debug_struct("UserMemory")
229            .field("layout", &self.layout)
230            .field("names", &self.names.is_some())
231            .finish_non_exhaustive()
232    }
233}
234
235/// HOME read in one piece.
236struct Home {
237    /// Address of the HOME object.
238    addr: u32,
239    /// Its nibbles.
240    nibbles: Vec<u8>,
241    /// Nibble of its last-variable offset field.
242    offset_at: usize,
243}
244
245impl<'a> UserMemory<'a> {
246    /// `mem` with the system pointers at `layout`.
247    pub fn new(mem: &'a dyn Memory, layout: Layout) -> Self {
248        Self {
249            mem,
250            layout,
251            names: None,
252        }
253    }
254
255    /// The same view, naming commands from `names` (the table of the ROM
256    /// this memory runs) and writing programs, algebraics and units as
257    /// text in the display mode of the flags.
258    pub fn with_names(self, names: &'a NameTable) -> Self {
259        Self {
260            names: Some(names),
261            ..self
262        }
263    }
264
265    /// The user memory of `machine` (48SX, 48GX, 49G).
266    pub fn of(machine: &'a Machine) -> Result<Self> {
267        let model = machine.model();
268        let layout = Layout::of(model).with_context(|| {
269            let why = if model == Model::Hp28s {
270                "keeps its RPL memory in a layout this does not read"
271            } else if model.is_pioneer() {
272                "has no RPL user memory (no HOME directory, no RPL stack)"
273            } else {
274                "keeps aplets, not a HOME directory"
275            };
276            format!("the {} {why}: no memory view", model.name().to_uppercase())
277        })?;
278        Ok(Self::new(machine, layout))
279    }
280
281    /// The layout in use.
282    pub fn layout(&self) -> Layout {
283        self.layout
284    }
285
286    fn nibble(&self, addr: u32) -> Result<u8> {
287        self.mem
288            .nibble(addr)
289            .with_context(|| format!("#{addr:05X} is not readable"))
290    }
291
292    /// The 5-nibble field at `addr`.
293    fn pointer(&self, addr: u32) -> Result<u32> {
294        let mut v = 0;
295        for i in (0..5).rev() {
296            v = (v << 4) | u32::from(self.nibble(addr + i)?);
297        }
298        Ok(v)
299    }
300
301    /// The nibbles from `from` up to `to`.
302    fn region(&self, from: u32, to: u32) -> Result<Vec<u8>> {
303        ensure!(
304            from <= to && to - from <= MAX_REGION,
305            "region #{from:05X}-#{to:05X} is not plausible"
306        );
307        (from..to).map(|a| self.nibble(a)).collect()
308    }
309
310    fn home(&self) -> Result<Home> {
311        let addr = self.pointer(self.layout.home_ptr)?;
312        let end = self.pointer(self.layout.home_end_ptr)?;
313        let nibbles = self.region(addr, end).context("reading HOME")?;
314        let prolog = crate::prolog::read_field(&nibbles, 0, 5);
315        ensure!(
316            prolog == Some(ObjectType::Directory.prolog()),
317            "no directory at HOME (#{addr:05X}): the ROM has not set up memory yet"
318        );
319        // HOME attaches a list of libraries where an ordinary directory has
320        // one library id: a 3-nibble count, then 13 nibbles per library.
321        let count = crate::prolog::read_field(&nibbles, 5, 3).context("HOME truncated")?;
322        let offset_at = 8 + 13 * usize::try_from(count)?;
323        Ok(Home {
324            addr,
325            nibbles,
326            offset_at,
327        })
328    }
329
330    /// The variables of the directory body in `n` whose offset field is at
331    /// `offset_at`, newest first, sub-directories expanded; `base` is the
332    /// address of `n[0]`.
333    fn variables(
334        &self,
335        n: &[u8],
336        base: u32,
337        offset_at: usize,
338        depth: usize,
339    ) -> Result<Vec<Variable>> {
340        let rs = records(n, offset_at, depth)?;
341        let mut out = Vec::with_capacity(rs.len());
342        for r in rs.iter().rev() {
343            // The empty name is HOME's hidden directory (user keys, alarms).
344            if r.name_len == 0 {
345                continue;
346            }
347            out.push(self.variable(n, base, r, depth)?);
348        }
349        Ok(out)
350    }
351
352    fn variable(&self, n: &[u8], base: u32, r: &Record, depth: usize) -> Result<Variable> {
353        let name_bytes: Vec<u8> = (0..r.name_len)
354            .map(|i| {
355                crate::prolog::read_field(n, r.name_at + 2 + 2 * i, 2)
356                    .map(|b| (b & 0xFF) as u8)
357                    .context("name truncated")
358            })
359            .collect::<Result<_>>()?;
360        let object = &n[r.object..r.end()];
361        let address = base + u32::try_from(r.object)?;
362        let prolog = crate::prolog::read_field(object, 0, 5).context("object truncated")?;
363        let ty = ObjectType::from_prolog(prolog);
364        let (kind, variables) = match ty {
365            Some(ObjectType::Directory) => (
366                ty.map(ObjectType::name),
367                Some(self.variables(n, base, r.object + 8, depth + 1)?),
368            ),
369            Some(t) => (Some(t.name()), None),
370            // A 5-nibble pointer to a ROM object (inferred: the ROM's STO
371            // copies objects, so this is rare); the target's type.
372            None => (
373                self.pointer(prolog)
374                    .ok()
375                    .and_then(ObjectType::from_prolog)
376                    .map(ObjectType::name),
377                None,
378            ),
379        };
380        Ok(Variable {
381            name: crate::charset::decode(&name_bytes),
382            kind: kind.unwrap_or("ROM Pointer").to_string(),
383            size: (r.end() - r.start) as f64 / 2.0,
384            checksum: crc(object),
385            address,
386            variables,
387        })
388    }
389
390    /// HOME's variables, newest first (as `VARS`), with every
391    /// sub-directory's variables. Needs no decode budget: the walk sizes
392    /// objects without following ROM pointers, so its work is linear in
393    /// HOME's nibbles (at most the address space).
394    pub fn tree(&self) -> Result<Vec<Variable>> {
395        let home = self.home()?;
396        let vars = self.variables(&home.nibbles, home.addr, home.offset_at, 0)?;
397        // The records must fill HOME exactly, else the layout is wrong.
398        let end = records(&home.nibbles, home.offset_at, 0)?
399            .last()
400            .map_or(home.offset_at + 5, Record::end);
401        ensure!(
402            end == home.nibbles.len(),
403            "HOME's variables end at #{:05X}, HOME at #{:05X}",
404            home.addr + u32::try_from(end)?,
405            home.addr + u32::try_from(home.nibbles.len())?
406        );
407        Ok(vars)
408    }
409
410    /// The current directory as a path from HOME, e.g. `["HOME", "A"]`.
411    pub fn current_path(&self) -> Result<Vec<String>> {
412        let context = self.pointer(self.layout.context_ptr)?;
413        let home = self.pointer(self.layout.home_ptr)?;
414        let mut path = vec!["HOME".to_string()];
415        if context == home {
416            return Ok(path);
417        }
418        if find(&self.tree()?, context, &mut path) {
419            Ok(path)
420        } else {
421            bail!("the current directory (#{context:05X}) is not under HOME")
422        }
423    }
424
425    /// The addresses of the objects on the data stack, level 1 first.
426    pub fn stack_addresses(&self) -> Result<Vec<u32>> {
427        let top = self.pointer(self.layout.stack_ptr)?;
428        let end = self.pointer(self.layout.stack_end_ptr)?;
429        let span = end
430            .checked_sub(top)
431            .filter(|s| s % 5 == 0 && *s >= 5)
432            .with_context(|| format!("stack pointers #{top:05X}-#{end:05X} are not plausible"))?;
433        let depth = usize::try_from(span / 5 - 1)?;
434        ensure!(depth <= MAX_LEVELS, "stack deeper than {MAX_LEVELS} levels");
435        ensure!(
436            self.pointer(end - 5)? == 0,
437            "no end marker at #{:05X}: the ROM is not idle",
438            end - 5
439        );
440        (0..depth)
441            .map(|i| {
442                let at = top + 5 * u32::try_from(i)?;
443                let p = self.pointer(at)?;
444                ensure!(p != 0, "stack level {} is 0 at #{at:05X}", i + 1);
445                Ok(p)
446            })
447            .collect()
448    }
449
450    /// The data stack as typed objects, level 1 first; binary integers
451    /// carry the display base of flags -11 and -12.
452    pub fn stack(&self) -> Result<Vec<Object>> {
453        let flags = self.flags()?;
454        let base = flags.base();
455        let settings = self.settings(&flags);
456        // One budget for the whole stack: the result is one document a
457        // host must hold, however many levels share an object.
458        let reader = self.reader(&settings);
459        self.stack_addresses()?
460            .into_iter()
461            .enumerate()
462            .map(|(i, addr)| {
463                let mut obj = reader
464                    .decode_at(addr)
465                    .with_context(|| format!("stack level {}", i + 1))?;
466                obj.set_base(base);
467                Ok(obj)
468            })
469            .collect()
470    }
471
472    /// The display settings `flags` select on this layout's model (the
473    /// 48SX and 48GX show objects alike).
474    fn settings(&self, flags: &Flags) -> Settings {
475        let model = if self.layout == Layout::HP49G {
476            Model::Hp49g
477        } else {
478            Model::Hp48gx
479        };
480        Settings::from_flags(flags, model)
481    }
482
483    fn reader(&self, settings: &Settings) -> Reader<'a> {
484        match self.names {
485            Some(names) => Reader::with_names(self.mem, names, *settings),
486            None => Reader::new(self.mem),
487        }
488    }
489
490    /// The object at `addr` (a variable's address from [`UserMemory::tree`]),
491    /// binary integers in the display base, text in the display mode.
492    pub fn object_at(&self, addr: u32) -> Result<Object> {
493        let flags = self.flags()?;
494        let mut obj = self.reader(&self.settings(&flags)).decode_at(addr)?;
495        obj.set_base(flags.base());
496        Ok(obj)
497    }
498
499    /// [`UserMemory::stack`] as JSON with the calculator's text on every
500    /// object ([`crate::decompile::described`]), in the display mode the
501    /// flags select; the texts of all levels together are capped at
502    /// [`crate::decompile::MAX_DESCRIBED_TEXT`].
503    pub fn stack_described(&self) -> Result<Vec<serde_json::Value>> {
504        let settings = self.settings(&self.flags()?);
505        let mut left = crate::decompile::MAX_DESCRIBED_TEXT;
506        self.stack()?
507            .iter()
508            .map(|o| crate::decompile::described_within(o, &settings, &mut left))
509            .collect()
510    }
511
512    /// The stack's depth and level 1 as JSON with its text
513    /// ([`crate::decompile::described`]), or `None` on an empty stack:
514    /// what an Edit button needs, without decoding the other levels.
515    pub fn stack_top(&self) -> Result<(usize, Option<serde_json::Value>)> {
516        let addresses = self.stack_addresses()?;
517        let top = match addresses.first() {
518            Some(&addr) => Some(self.object_described(addr).context("stack level 1")?),
519            None => None,
520        };
521        Ok((addresses.len(), top))
522    }
523
524    /// [`UserMemory::object_at`] as JSON with the calculator's text on
525    /// every object ([`crate::decompile::described`]).
526    pub fn object_described(&self, addr: u32) -> Result<serde_json::Value> {
527        let settings = self.settings(&self.flags()?);
528        crate::decompile::described(&self.object_at(addr)?, &settings)
529    }
530
531    /// The text to edit of the object at `addr` (a variable's address or a
532    /// stack level's, [`UserMemory::stack_addresses`]): decoded and written
533    /// in the display mode made fit for compiling again
534    /// ([`crate::decompile::edit_text`]).
535    pub fn edit_text_at(&self, addr: u32) -> Result<String> {
536        let flags = self.flags()?;
537        let settings = self.settings(&flags).for_editing();
538        let mut obj = self.reader(&settings).decode_at(addr)?;
539        obj.set_base(flags.base());
540        crate::decompile::edit_text(&obj, &settings)
541    }
542
543    /// What identifies the object at `addr` whatever the display mode:
544    /// its size in nibbles and `BYTES`'s checksum of them (`storeText`'s
545    /// check that the object a save replaces is the one that was opened).
546    pub fn identity_at(&self, addr: u32) -> Result<(usize, u16)> {
547        let n: Vec<u8> = (addr..MAX_REGION)
548            .map(|a| self.nibble(a))
549            .collect::<Result<_>>()?;
550        let size = crate::prolog::object_size(&n, 0)?;
551        Ok((size, crc(&n[..size])))
552    }
553
554    fn words(&self, at: u32) -> Result<Vec<u64>> {
555        (0..self.layout.flag_words)
556            .map(|w| {
557                let start = at + 16 * u32::try_from(w)?;
558                let mut v = 0u64;
559                for i in (0..16).rev() {
560                    v = (v << 4) | u64::from(self.nibble(start + i)?);
561                }
562                Ok(v)
563            })
564            .collect()
565    }
566
567    /// Where the current menu's pointer points (see [`Layout::menu_ptr`];
568    /// `crate::menus::Menus::number` turns it into a menu number).
569    pub fn menu_pointer(&self) -> Result<u32> {
570        self.pointer(self.layout.menu_ptr)
571    }
572
573    /// The system and user flags.
574    pub fn flags(&self) -> Result<Flags> {
575        Ok(Flags {
576            system: self.words(self.layout.system_flags)?,
577            user: self.words(self.layout.user_flags)?,
578        })
579    }
580
581    /// A number that changes whenever HOME (any variable anywhere in the
582    /// tree), the current directory, the stack or the flags change: an
583    /// FNV-1a hash of HOME's nibbles, the system pointers, the stack's
584    /// entries and the flag words. A host polls it and re-reads only when
585    /// it moves. Reads HOME once (up to the whole RAM). It hashes the stack's
586    /// pointers, not the objects they point to: an object rewritten in place
587    /// at the same address would not move it.
588    pub fn change_counter(&self) -> Result<u64> {
589        let mut h = Fnv::default();
590        let l = self.layout;
591        for p in [
592            l.home_ptr,
593            l.home_end_ptr,
594            l.context_ptr,
595            l.stack_ptr,
596            l.stack_end_ptr,
597        ] {
598            h.word(u64::from(self.pointer(p)?));
599        }
600        let home = self.pointer(l.home_ptr)?;
601        let end = self.pointer(l.home_end_ptr)?;
602        for n in self.region(home, end)? {
603            h.byte(n);
604        }
605        // Stack entries; garbage pointers are hashed as they are.
606        let top = self.pointer(l.stack_ptr)?;
607        let stack_end = self
608            .pointer(l.stack_end_ptr)?
609            .min(top + 5 * MAX_LEVELS as u32);
610        for a in (top..stack_end).step_by(5) {
611            h.word(u64::from(self.pointer(a)?));
612        }
613        let flags = self.flags()?;
614        for w in flags.system.iter().chain(&flags.user) {
615            h.word(*w);
616        }
617        Ok(h.0)
618    }
619}
620
621/// Extend `path` to the directory at `addr` under `vars`.
622fn find(vars: &[Variable], addr: u32, path: &mut Vec<String>) -> bool {
623    for v in vars {
624        let Some(children) = &v.variables else {
625            continue;
626        };
627        path.push(v.name.clone());
628        if v.address == addr || find(children, addr, path) {
629            return true;
630        }
631        path.pop();
632    }
633    false
634}
635
636/// 64-bit FNV-1a.
637struct Fnv(u64);
638
639impl Default for Fnv {
640    fn default() -> Self {
641        Fnv(0xCBF2_9CE4_8422_2325)
642    }
643}
644
645impl Fnv {
646    fn byte(&mut self, b: u8) {
647        self.0 = (self.0 ^ u64::from(b)).wrapping_mul(0x0000_0100_0000_01B3);
648    }
649
650    fn word(&mut self, w: u64) {
651        for b in w.to_le_bytes() {
652            self.byte(b);
653        }
654    }
655}
656
657/// HOME's variables of `machine`, newest first, with sub-directories.
658pub fn memory_tree(machine: &Machine) -> Result<Vec<Variable>> {
659    UserMemory::of(machine)?.tree()
660}
661
662/// The current directory of `machine`, e.g. `["HOME", "A"]`.
663pub fn current_path(machine: &Machine) -> Result<Vec<String>> {
664    UserMemory::of(machine)?.current_path()
665}
666
667/// The data stack of `machine`, level 1 first.
668pub fn stack_objects(machine: &Machine) -> Result<Vec<Object>> {
669    UserMemory::of(machine)?.stack()
670}
671
672/// The flags of `machine`.
673pub fn flags(machine: &Machine) -> Result<Flags> {
674    UserMemory::of(machine)?.flags()
675}
676
677/// See [`UserMemory::change_counter`].
678pub fn change_counter(machine: &Machine) -> Result<u64> {
679    UserMemory::of(machine)?.change_counter()
680}
681
682#[cfg(test)]
683mod tests {
684    use super::*;
685
686    /// The aplet models and the 42S have no layout, each with its own
687    /// reason.
688    #[test]
689    fn models_without_a_memory_view() {
690        for model in [Model::Hp38g, Model::Hp39g, Model::Hp40g, Model::Hp42s] {
691            assert!(Layout::of(model).is_none(), "{}", model.name());
692        }
693        let m = Machine::new(Model::Hp42s, &vec![0u8; Model::Hp42s.rom_bytes()]).unwrap();
694        let e = UserMemory::of(&m).err().unwrap().to_string();
695        assert!(e.contains("the 42S has no RPL user memory"), "{e}");
696        let m = Machine::new(Model::Hp38g, &vec![0u8; Model::Hp38g.rom_bytes()]).unwrap();
697        let e = UserMemory::of(&m).err().unwrap().to_string();
698        assert!(e.contains("keeps aplets"), "{e}");
699    }
700    use crate::object::{MAX_DECODED_OBJECTS, Real, decode_at};
701
702    /// RAM #70000-#7FFFF of a 48SX.
703    struct Ram(Vec<u8>);
704
705    impl Memory for Ram {
706        fn nibble(&self, addr: u32) -> Option<u8> {
707            let i = usize::try_from(addr.checked_sub(0x70000)?).ok()?;
708            self.0.get(i).copied()
709        }
710    }
711
712    impl Ram {
713        fn put(&mut self, addr: u32, hex: &str) {
714            let at = (addr - 0x70000) as usize;
715            for (i, c) in hex.chars().enumerate() {
716                self.0[at + i] = c.to_digit(16).unwrap() as u8;
717            }
718        }
719
720        fn put_ptr(&mut self, addr: u32, value: u32) {
721            let hex: String = (0..5)
722                .map(|i| format!("{:X}", value >> (4 * i) & 0xF))
723                .collect();
724            self.put(addr, &hex);
725        }
726    }
727
728    /// HOME of a 48SX (ROM J in saturnus, 2026-10-05) after `42 'X' STO
729    /// "HI" 'YY' STO 'DIRA' CRDIR DIRA 5 'Z' STO`, with IOPAR from the
730    /// Kermit server and the hidden directory; current directory DIRA.
731    fn sx() -> Ram {
732        let mut r = Ram(vec![0; 0x10000]);
733        r.put(
734            0x7FE9B,
735            concat!(
736                "69A20200200746228D356007EFD2200000501000",
737                "00000069A20FF7C500000000C055375627B45697",
738                "37E2342534C0E4A20900006E57A2000805537562",
739                "7B45697378047A20B2130E10006014C61627D637",
740                "6047A20B2130080005094F40514255047A201932",
741                "24B2A24B2A24B2A23F2A29C2A2B2130630001085",
742                "10339201000000000000240B100020959520C2A2",
743                "09000084946100040449425144069A20FF7A0000",
744                "0000010A51033920000000000000005",
745            ),
746        );
747        let l = Layout::HP48SX;
748        r.put_ptr(l.home_ptr, 0x7FE9B);
749        r.put_ptr(l.home_end_ptr, 0x7FFFB);
750        r.put_ptr(l.context_ptr, 0x7FFCE);
751        // Stack: "AB" (in RAM) on a ROM-free real 7 and a binary integer.
752        r.put(0x71000, "C2A2090000142433920000000000000007");
753        r.put(0x71030, "E4A2051000A200000000000000");
754        r.put_ptr(0x7F000, 0x71000);
755        r.put_ptr(0x7F005, 0x7100E);
756        r.put_ptr(0x7F00A, 0x71030);
757        r.put_ptr(l.stack_ptr, 0x7F000);
758        r.put_ptr(l.stack_end_ptr, 0x7F014);
759        // Flags -11 and -12 (HEX), -40; user flag 7.
760        r.put(l.system_flags, "0FF0000008000000");
761        r.put(l.user_flags, "0400000000000000");
762        r
763    }
764
765    #[test]
766    fn tree_sizes_and_checksums_match_the_calculator() {
767        let ram = sx();
768        let m = UserMemory::new(&ram, Layout::HP48SX);
769        let tree = m.tree().unwrap();
770        // Name, type, size, checksum as the calculator's `G D` listed them.
771        let flat: Vec<(&str, &str, f64, u16)> = tree
772            .iter()
773            .map(|v| (v.name.as_str(), v.kind.as_str(), v.size, v.checksum))
774            .collect();
775        assert_eq!(
776            flat,
777            [
778                ("DIRA", "Directory", 31.0, 44093),
779                ("YY", "String", 13.5, 5889),
780                ("X", "Real Number", 16.0, 59472),
781                ("IOPAR", "List", 29.5, 8861),
782            ]
783        );
784        let dira = tree[0].variables.as_ref().unwrap();
785        assert_eq!(dira.len(), 1);
786        assert_eq!(
787            (dira[0].name.as_str(), dira[0].size, dira[0].checksum),
788            ("Z", 16.0, 23381)
789        );
790        assert_eq!(dira[0].address, 0x7FFE6);
791        assert_eq!(m.current_path().unwrap(), ["HOME", "DIRA"]);
792        assert_eq!(
793            decode_at(tree[2].address, &ram).unwrap(),
794            Object::Real {
795                value: Real::parse("42").unwrap()
796            }
797        );
798    }
799
800    #[test]
801    fn stack_and_flags() {
802        let ram = sx();
803        let m = UserMemory::new(&ram, Layout::HP48SX);
804        let flags = m.flags().unwrap();
805        assert_eq!(flags.get(-40), Some(true));
806        assert_eq!(flags.get(7), Some(true));
807        assert_eq!(flags.get(8), Some(false));
808        assert_eq!(flags.get(65), None);
809        assert_eq!(flags.base(), Base::Hex);
810        assert_eq!(flags.set(), [-5, -6, -7, -8, -9, -10, -11, -12, -40, 7]);
811        let json = serde_json::to_value(&flags).unwrap();
812        assert_eq!(json["system"][0], "0000008000000FF0");
813        assert_eq!(json["user"][0], "0000000000000040");
814        let stack = m.stack().unwrap();
815        assert_eq!(stack.len(), 3);
816        assert_eq!(stack[0], Object::String { value: "AB".into() });
817        assert_eq!(
818            stack[1],
819            Object::Real {
820                value: Real::parse("7").unwrap()
821            }
822        );
823        assert_eq!(
824            serde_json::to_value(&stack[2]).unwrap(),
825            serde_json::json!({"type": "binary", "value": 42, "base": "hex", "text": "# 2Ah"})
826        );
827    }
828
829    #[test]
830    fn change_counter_moves_with_the_tree_and_the_stack() {
831        let mut ram = sx();
832        let c0 = UserMemory::new(&ram, Layout::HP48SX)
833            .change_counter()
834            .unwrap();
835        assert_eq!(
836            UserMemory::new(&ram, Layout::HP48SX)
837                .change_counter()
838                .unwrap(),
839            c0
840        );
841        // X = 42 becomes 43.
842        ram.put(0x7FF39 + 16, "3");
843        let c1 = UserMemory::new(&ram, Layout::HP48SX)
844            .change_counter()
845            .unwrap();
846        assert_ne!(c1, c0);
847        ram.put_ptr(Layout::HP48SX.stack_ptr, 0x7F005);
848        assert_ne!(
849            UserMemory::new(&ram, Layout::HP48SX)
850                .change_counter()
851                .unwrap(),
852            c1
853        );
854    }
855
856    /// Lists L0..L39 at #71000, each holding two pointers to the next (the
857    /// last empty): 2^39 objects if every pointer were decoded anew.
858    fn shared_pointers(levels: u32) -> Ram {
859        let mut r = sx();
860        let base = 0x71000;
861        for i in 0..levels {
862            let a = base + 20 * i;
863            r.put(a, "47A20");
864            if i + 1 < levels {
865                r.put_ptr(a + 5, a + 20);
866                r.put_ptr(a + 10, a + 20);
867                r.put(a + 15, "B2130");
868            } else {
869                r.put(a + 5, "B2130");
870            }
871        }
872        r.put_ptr(0x7F000, base);
873        r.put_ptr(0x7F005, 0);
874        r.put_ptr(Layout::HP48SX.stack_ptr, 0x7F000);
875        r.put_ptr(Layout::HP48SX.stack_end_ptr, 0x7F00A);
876        r
877    }
878
879    #[test]
880    fn shared_pointers_hit_the_decode_budget() {
881        let ram = shared_pointers(40);
882        let m = UserMemory::new(&ram, Layout::HP48SX);
883        let calls: [&dyn Fn() -> Result<()>; 2] = [&|| decode_at(0x71000, &ram).map(drop), &|| {
884            m.stack().map(drop)
885        }];
886        for call in calls {
887            // The budget error is the bound on the work: no wall-clock
888            // assertion here, a loaded CI runner would make it flaky.
889            let e = call().unwrap_err();
890            assert!(format!("{e:#}").contains("(MAX_DECODED_"), "{e:#}");
891        }
892        // 2^12 - 1 lists: shared, but within the budget, and exact.
893        let ram = shared_pointers(12);
894        let obj = decode_at(0x71000, &ram).unwrap();
895        fn count(o: &Object) -> usize {
896            match o {
897                Object::List { items } => 1 + items.iter().map(count).sum::<usize>(),
898                _ => 1,
899            }
900        }
901        assert_eq!(count(&obj), (1 << 12) - 1);
902        assert!(count(&obj) < MAX_DECODED_OBJECTS);
903    }
904
905    /// A stack of `levels` pointers to the object `hex` at #71000.
906    fn repeated(levels: u32, hex: &str) -> Ram {
907        let mut r = sx();
908        r.put(0x71000, hex);
909        for i in 0..levels {
910            r.put_ptr(0x7F000 + 5 * i, 0x71000);
911        }
912        r.put_ptr(0x7F000 + 5 * levels, 0);
913        r.put_ptr(Layout::HP48SX.stack_ptr, 0x7F000);
914        r.put_ptr(Layout::HP48SX.stack_end_ptr, 0x7F005 + 5 * levels);
915        r
916    }
917
918    /// A string of `chars` A's.
919    fn string(chars: usize) -> String {
920        let mut len = String::new();
921        for i in 0..5 {
922            len.push_str(&format!("{:X}", (5 + 2 * chars) >> (4 * i) & 0xF));
923        }
924        format!("C2A20{len}{}", "14".repeat(chars))
925    }
926
927    #[test]
928    fn repeated_large_objects_hit_the_nibble_budget() {
929        // 500 levels of one 40 K-nibble string (DUP copies the pointer):
930        // 20 M nibbles of copies, more than MAX_CLONED_NIBBLES.
931        let ram = repeated(500, &string(20_000));
932        let m = UserMemory::new(&ram, Layout::HP48SX);
933        for e in [m.stack().unwrap_err(), m.stack_described().unwrap_err()] {
934            assert!(format!("{e:#}").contains("MAX_CLONED_NIBBLES"), "{e:#}");
935        }
936        // A stack a calculator really builds by DUP: 100 levels, 4 M
937        // nibbles of copies, twice MAX_DECODED_NIBBLES, still decodes.
938        let ram = repeated(100, &string(20_000));
939        let stack = UserMemory::new(&ram, Layout::HP48SX).stack().unwrap();
940        assert_eq!(stack.len(), 100);
941        assert_eq!(
942            stack[99],
943            Object::String {
944                value: "A".repeat(20_000)
945            }
946        );
947    }
948
949    #[test]
950    fn nested_texts_hit_the_described_budget() {
951        // A string of 2000 characters in lists nested 60 deep: each list
952        // carries the string's text again, 120 K characters a level.
953        let hex = format!(
954            "{}{}{}",
955            "47A20".repeat(60),
956            string(2000),
957            "B2130".repeat(60)
958        );
959        let ram = repeated(100, &hex);
960        let m = UserMemory::new(&ram, Layout::HP48SX);
961        assert_eq!(m.stack().unwrap().len(), 100);
962        let e = m.stack_described().unwrap_err();
963        assert!(format!("{e:#}").contains("MAX_DESCRIBED_TEXT"), "{e:#}");
964        // One level fits.
965        let ram = repeated(1, &hex);
966        let level = &UserMemory::new(&ram, Layout::HP48SX)
967            .stack_described()
968            .unwrap()[0];
969        assert_eq!(
970            level["items"][0]["text"].as_str().map(str::len),
971            Some(2000 + 59 * 4 + 2)
972        );
973    }
974
975    #[test]
976    fn broken_pointers_are_errors() {
977        let mut ram = sx();
978        ram.put_ptr(Layout::HP48SX.home_end_ptr, 0x7FFF0);
979        assert!(UserMemory::new(&ram, Layout::HP48SX).tree().is_err());
980        let mut ram = sx();
981        ram.put_ptr(Layout::HP48SX.stack_end_ptr, 0x7F013);
982        assert!(UserMemory::new(&ram, Layout::HP48SX).stack().is_err());
983        ram.put_ptr(Layout::HP48SX.stack_end_ptr, 0x7F00F);
984        assert!(UserMemory::new(&ram, Layout::HP48SX).stack().is_err());
985    }
986}