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