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