Skip to main content

saturnus_objects/
decompile.rs

1//! The calculator's text for objects: numbers in the display mode, data
2//! objects as the stack shows them, programs, algebraics (infix) and unit
3//! expressions rebuilt from their elements.
4//!
5//! The rules (number formats, spacing, the operators' precedence and
6//! parentheses) are those the ROM's own decompiler shows, checked against
7//! it by the ROM-gated oracle test (`saturnus-kermit` e2e,
8//! `decompiler_matches_the_rom`); wiki: protocols/rpl-libraries for how
9//! commands are named.
10//!
11//! Text is capped at [`MAX_TEXT`] characters; longer text ends in
12//! [`TRUNCATED`]. The texts [`described`] adds are capped together at
13//! [`MAX_DESCRIBED_TEXT`].
14
15use anyhow::{Result, bail};
16use saturnus_core::Model;
17
18use crate::object::{ArrayItem, Base, Object, Real};
19use crate::ram::Flags;
20
21/// Longest text produced for one object, in characters.
22pub const MAX_TEXT: usize = 1 << 16;
23/// Appended to text cut at [`MAX_TEXT`].
24pub const TRUNCATED: &str = "…";
25/// Most characters of `text` fields one [`described`] call (one stack
26/// read) adds. Every object inside another carries its own text, so a
27/// string nested 64 levels deep is written 64 times: the decode budget
28/// bounds the objects, this bounds the copies of their text. 4 M
29/// characters, a few MB of JSON; the 49G's whole RAM (256 KB) holds at
30/// most 256 K characters of strings.
31pub const MAX_DESCRIBED_TEXT: usize = 1 << 22;
32
33/// How reals are shown (flags -49 and -50, digits in -45 to -48).
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub enum NumberFormat {
36    /// Standard: up to 12 digits, no trailing zeros.
37    Std,
38    /// Fixed, with this many decimals.
39    Fix(u8),
40    /// Scientific, with this many decimals in the mantissa.
41    Sci(u8),
42    /// Engineering, with this many digits after the first.
43    Eng(u8),
44}
45
46/// The display settings the text depends on.
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48pub struct Settings {
49    /// Real number format.
50    pub format: NumberFormat,
51    /// Comma as the fraction mark (flag -51); the separator in complex
52    /// numbers becomes `;`.
53    pub comma: bool,
54    /// Binary integer base (flags -11, -12).
55    pub base: Base,
56    /// Binary integer word size in bits, 1-64 (flags -5 to -10).
57    pub word_size: u32,
58    /// Show every parenthesis in algebraics (flag -53).
59    pub all_parens: bool,
60    /// Reals with an integer value keep their point (`5.`), as on the 49G,
61    /// whose exact integers show without one (not inside unit objects).
62    pub real_point: bool,
63    /// `^` groups to the right (`A^B^C` is `A^(B^C)`), as on the 49G; on
64    /// the 48 it groups to the left.
65    pub power_right: bool,
66    /// FIX groups the integer digits by three (`1,234.500`; `1.234,500`
67    /// with a comma fraction mark) inside lists, programs, arrays and
68    /// tagged objects too, as the 49G does; both models group a real that
69    /// is a whole stack level. Never inside complex numbers, algebraics or
70    /// units.
71    pub group_digits: bool,
72    /// A tagged object inside a composite shows its tag between colons
73    /// (`:T: 5`, the 48) or with a trailing one only (`T: 5`, the 49G, and
74    /// both models for a whole stack level).
75    pub tag_colon: bool,
76}
77
78impl Default for Settings {
79    /// The calculator's defaults: STD, period, decimal binaries, 64 bits.
80    fn default() -> Self {
81        Settings {
82            format: NumberFormat::Std,
83            comma: false,
84            base: Base::Dec,
85            word_size: 64,
86            all_parens: false,
87            real_point: false,
88            power_right: false,
89            group_digits: false,
90            tag_colon: true,
91        }
92    }
93}
94
95impl Settings {
96    /// The defaults of `model`.
97    pub fn standard(model: Model) -> Settings {
98        let hp49 = model == Model::Hp49g;
99        Settings {
100            real_point: hp49,
101            power_right: hp49,
102            tag_colon: !hp49,
103            ..Settings::default()
104        }
105    }
106
107    /// The settings `flags` select on `model`.
108    pub fn from_flags(flags: &Flags, model: Model) -> Settings {
109        let set = |n: i32| flags.get(n).unwrap_or(false);
110        // Bit k of a multi-flag value is flag -(first + k).
111        let value = |first: i32, bits: i32| {
112            (0..bits).fold(0u32, |v, k| v | (u32::from(set(-(first + k))) << k))
113        };
114        let digits = u8::try_from(value(45, 4)).unwrap_or(0);
115        let format = match (set(-49), set(-50)) {
116            (false, false) => NumberFormat::Std,
117            (true, false) => NumberFormat::Fix(digits),
118            (false, true) => NumberFormat::Sci(digits),
119            (true, true) => NumberFormat::Eng(digits),
120        };
121        let hp49 = model == Model::Hp49g;
122        Settings {
123            format,
124            comma: set(-51),
125            base: flags.base(),
126            word_size: value(5, 6) + 1,
127            all_parens: set(-53),
128            real_point: hp49,
129            power_right: hp49,
130            group_digits: hp49,
131            tag_colon: !hp49,
132        }
133    }
134
135    /// These settings for text that is edited and compiled again: every
136    /// digit of a real (STD), binary integers at 64 bits, no digit
137    /// grouping and tags between colons, so that the text compiles back
138    /// to the same object; the fraction mark, base and parentheses stay.
139    pub fn for_editing(self) -> Settings {
140        Settings {
141            format: NumberFormat::Std,
142            word_size: 64,
143            group_digits: false,
144            tag_colon: true,
145            ..self
146        }
147    }
148
149    /// `value` as a binary integer in the base and word size.
150    pub fn binary(&self, value: u64) -> String {
151        let mask = if self.word_size >= 64 {
152            u64::MAX
153        } else {
154            (1u64 << self.word_size) - 1
155        };
156        self.base.format(value & mask)
157    }
158
159    /// `r` as the calculator shows it.
160    pub fn real(&self, r: &Real) -> String {
161        let s = match self.format {
162            NumberFormat::Std => std_text(r, self.real_point),
163            NumberFormat::Fix(d) if self.group_digits => {
164                group(&fix_text(r, usize::from(d)), self.comma)
165            }
166            NumberFormat::Fix(d) => fix_text(r, usize::from(d)),
167            NumberFormat::Sci(d) => sci_text(r, usize::from(d), 1),
168            NumberFormat::Eng(d) => sci_text(r, usize::from(d), 3),
169        };
170        if self.comma {
171            s.replace('.', ",").replace('\u{1}', ".")
172        } else {
173            s
174        }
175    }
176
177    /// A unit object's number or power: an integer value as an integer
178    /// (`2_m^2` even in FIX), anything else in the display mode; never a
179    /// trailing point or digit groups (oracle-checked: `1.50E0_m^2.50E0`
180    /// in 2 SCI).
181    pub fn unit_number(&self, r: &Real) -> String {
182        let integer = r.exponent >= 0
183            && r.exponent < 12
184            && r.digits[(r.exponent as usize + 1).min(12)..]
185                .iter()
186                .all(|&d| d == 0);
187        let format = if integer || r.digits.iter().all(|&d| d == 0) {
188            NumberFormat::Std
189        } else {
190            self.format
191        };
192        Settings {
193            format,
194            real_point: false,
195            group_digits: false,
196            ..*self
197        }
198        .real(r)
199    }
200
201    /// The settings inside a complex number or an algebraic: no digit
202    /// groups.
203    fn inner(&self) -> Settings {
204        Settings {
205            group_digits: false,
206            ..*self
207        }
208    }
209
210    /// A complex number `(re,im)`.
211    pub fn complex(&self, re: &Real, im: &Real) -> String {
212        let sep = if self.comma { ';' } else { ',' };
213        let s = self.inner();
214        format!("({}{sep}{})", s.real(re), s.real(im))
215    }
216}
217
218/// `text` (a FIX number, period as the mark) with its integer digits
219/// grouped by three; with `comma`, the groups are separated by periods
220/// (the caller swaps the mark afterwards).
221fn group(text: &str, comma: bool) -> String {
222    let sep = if comma { '\u{1}' } else { ',' };
223    let (sign, rest) = match text.strip_prefix('-') {
224        Some(r) => ("-", r),
225        None => ("", text),
226    };
227    if rest.contains('E') {
228        return text.to_string();
229    }
230    let (int, frac) = rest.split_once('.').unwrap_or((rest, ""));
231    let mut out = String::new();
232    for (i, c) in int.chars().enumerate() {
233        if i > 0 && (int.len() - i) % 3 == 0 {
234            out.push(sep);
235        }
236        out.push(c);
237    }
238    format!("{sign}{out}.{frac}")
239}
240
241/// Mantissa digits without trailing zeros (at least one).
242fn significant(r: &Real) -> &[u8] {
243    let last = r.digits.iter().rposition(|&d| d != 0).unwrap_or(0);
244    &r.digits[..=last]
245}
246
247fn digit(d: u8) -> char {
248    char::from(b'0' + d)
249}
250
251/// STD: fixed notation when the digits fit 12 places, else scientific.
252fn std_text(r: &Real, real_point: bool) -> String {
253    let sign = if r.negative { "-" } else { "" };
254    let sig = significant(r);
255    if r.digits.iter().all(|&d| d == 0) {
256        return if real_point { "0.".into() } else { "0".into() };
257    }
258    let e = r.exponent;
259    let k = sig.len() as i32;
260    if (0..12).contains(&e) {
261        // Integer part: the first e + 1 digits (zero-padded).
262        let int: String = (0..=e)
263            .map(|i| digit(sig.get(i as usize).copied().unwrap_or(0)))
264            .collect();
265        let frac: String = sig
266            .iter()
267            .skip((e + 1) as usize)
268            .map(|&d| digit(d))
269            .collect();
270        if frac.is_empty() {
271            let point = if real_point { "." } else { "" };
272            return format!("{sign}{int}{point}");
273        }
274        return format!("{sign}{int}.{frac}");
275    }
276    if e < 0 && -e - 1 + k <= 12 {
277        let zeros = "0".repeat((-e - 1) as usize);
278        let frac: String = sig.iter().map(|&d| digit(d)).collect();
279        return format!("{sign}.{zeros}{frac}");
280    }
281    let rest: String = sig[1..].iter().map(|&d| digit(d)).collect();
282    format!("{sign}{}.{rest}E{e}", digit(sig[0]))
283}
284
285/// Round the 12-digit mantissa of `r` to `keep` digits (half up); the
286/// digits and the exponent after a carry.
287fn rounded(r: &Real, keep: usize) -> (Vec<u8>, i32) {
288    let mut d: Vec<u8> = r.digits.to_vec();
289    let mut e = r.exponent;
290    if keep >= 12 {
291        return (d, e);
292    }
293    let up = d[keep] >= 5;
294    d.truncate(keep);
295    if up {
296        let mut i = keep;
297        loop {
298            if i == 0 {
299                d.insert(0, 1);
300                d.truncate(keep.max(1));
301                e += 1;
302                break;
303            }
304            i -= 1;
305            if d[i] == 9 {
306                d[i] = 0;
307            } else {
308                d[i] += 1;
309                break;
310            }
311        }
312    }
313    (d, e)
314}
315
316/// FIX: `decimals` places, at most 12 digits in all (fewer decimals for
317/// large numbers); scientific when the integer part needs more than 12
318/// digits or a nonzero number rounds to zero.
319fn fix_text(r: &Real, decimals: usize) -> String {
320    if r.digits.iter().all(|&d| d == 0) {
321        return format!("0.{}", "0".repeat(decimals));
322    }
323    let sign = if r.negative { "-" } else { "" };
324    let places = |e: i32| -> i32 {
325        if e >= 0 {
326            (decimals as i32).min(11 - e)
327        } else {
328            decimals as i32
329        }
330    };
331    // Digits kept from the first: the integer part and the decimals.
332    let keep = r.exponent + 1 + places(r.exponent);
333    let (d, e) = match keep {
334        k if k >= 1 => rounded(r, k as usize),
335        // Below the last place: rounds to one unit of it, or to zero.
336        0 if r.digits[0] >= 5 => (vec![1], r.exponent + 1),
337        _ => return sci_text(r, decimals.min(11), 1),
338    };
339    if e >= 12 {
340        return sci_text(r, decimals.min(11), 1);
341    }
342    let dec = places(e).max(0);
343    let at = |place: i32| -> u8 {
344        let i = e - place;
345        if i < 0 {
346            0
347        } else {
348            d.get(i as usize).copied().unwrap_or(0)
349        }
350    };
351    let int: String = if e >= 0 {
352        (0..=e).rev().map(|p| digit(at(p))).collect()
353    } else {
354        "0".into()
355    };
356    let frac: String = (1..=dec).map(|p| digit(at(-p))).collect();
357    format!("{sign}{int}.{frac}")
358}
359
360/// SCI (`group` 1) or ENG (`group` 3) with `decimals` + 1 significant
361/// digits; ENG pads the integer part to put the exponent on a multiple of
362/// three.
363fn sci_text(r: &Real, decimals: usize, group: i32) -> String {
364    let sign = if r.negative { "-" } else { "" };
365    if r.digits.iter().all(|&d| d == 0) {
366        let zeros = "0".repeat(decimals);
367        return format!("0.{zeros}E0");
368    }
369    let sig = (decimals + 1).min(12);
370    let (mut d, e) = rounded(r, sig);
371    let shift = e.rem_euclid(group);
372    let exp = e - shift;
373    let int_digits = (shift + 1) as usize;
374    d.resize(sig.max(int_digits), 0);
375    let int: String = d[..int_digits].iter().map(|&x| digit(x)).collect();
376    let frac: String = d[int_digits..sig.max(int_digits)]
377        .iter()
378        .map(|&x| digit(x))
379        .collect();
380    format!("{sign}{int}.{frac}E{exp}")
381}
382
383/// A string capped at [`MAX_TEXT`] characters.
384#[derive(Debug, Default)]
385pub(crate) struct Text {
386    s: String,
387    chars: usize,
388    full: bool,
389}
390
391impl Text {
392    pub(crate) fn push(&mut self, t: &str) {
393        if self.full {
394            return;
395        }
396        for c in t.chars() {
397            if self.chars == MAX_TEXT {
398                self.s.push_str(TRUNCATED);
399                self.full = true;
400                return;
401            }
402            self.s.push(c);
403            self.chars += 1;
404        }
405    }
406
407    pub(crate) fn full(&self) -> bool {
408        self.full
409    }
410
411    pub(crate) fn finish(self) -> String {
412        self.s
413    }
414}
415
416/// `obj` as the stack shows it as a whole level.
417pub fn display(obj: &Object, s: &Settings) -> String {
418    let mut t = Text::default();
419    match obj {
420        Object::Real { value } => t.push(
421            &Settings {
422                group_digits: true,
423                ..*s
424            }
425            .real(value),
426        ),
427        Object::Tagged { tag, object } => {
428            t.push(tag);
429            t.push(": ");
430            write_object(&mut t, object, s);
431        }
432        _ => write_object(&mut t, obj, s),
433    }
434    t.finish()
435}
436
437/// Whether the calculator's text of `obj` is known in full: every
438/// program, algebraic and unit inside it was decompiled and every command
439/// has a name (or is an XLIB name, shown by its numbers).
440pub fn has_text(obj: &Object) -> bool {
441    match obj {
442        Object::Program { source } | Object::Algebraic { source } => source.is_some(),
443        Object::Command {
444            name,
445            source,
446            address,
447            library,
448            command,
449        } => {
450            name.is_some()
451                || source.is_some()
452                || (address.is_none() && library.is_some() && command.is_some())
453        }
454        Object::Unit { unit, .. } => unit.is_some(),
455        Object::Unknown { source, .. } => source.is_some(),
456        Object::List { items } => items.iter().all(has_text),
457        Object::Tagged { object, .. } => has_text(object),
458        Object::Array { items, .. } => array_has_text(items),
459        _ => true,
460    }
461}
462
463fn array_has_text(items: &[ArrayItem]) -> bool {
464    items.iter().all(|i| match i {
465        ArrayItem::Item(o) => has_text(o),
466        ArrayItem::Row(row) => array_has_text(row),
467    })
468}
469
470/// The calculator's text of `obj` as a whole stack level or a variable's
471/// value ([`display`]; a name with its quotes, as the stack shows one), or
472/// `None` if a part of it has none ([`has_text`]).
473pub fn text(obj: &Object, s: &Settings) -> Option<String> {
474    if !has_text(obj) {
475        return None;
476    }
477    Some(match obj {
478        Object::Name { value } | Object::LocalName { value } => format!("'{value}'"),
479        _ => display(obj, s),
480    })
481}
482
483/// Text of `obj` to edit and compile again (the palette's editor), in
484/// settings made by [`Settings::for_editing`]: [`text`], but a tagged
485/// object as `:tag:object`, which compiles. An error for an object with
486/// no such text: a part without text ([`has_text`]), a string holding `"`
487/// (the calculator has no way to write one), or text cut at [`MAX_TEXT`].
488pub fn edit_text(obj: &Object, s: &Settings) -> Result<String> {
489    if !has_text(obj) {
490        bail!("it has no text form (a graphic, library, backup, directory or code object)");
491    }
492    if holds_quote(obj) {
493        bail!("it holds a string with a \" in it, which no text can write");
494    }
495    let text = match obj {
496        Object::Tagged { .. } => element_text(obj, s).unwrap_or_default(),
497        _ => text(obj, s).unwrap_or_default(),
498    };
499    if text.chars().count() > MAX_TEXT {
500        bail!("its text is longer than {MAX_TEXT} characters");
501    }
502    Ok(text)
503}
504
505/// Whether a string inside `obj` holds `"`.
506fn holds_quote(obj: &Object) -> bool {
507    match obj {
508        Object::String { value } => value.contains('"'),
509        Object::List { items } => items.iter().any(holds_quote),
510        Object::Tagged { object, .. } => holds_quote(object),
511        Object::Array { items, .. } => array_holds_quote(items),
512        _ => false,
513    }
514}
515
516fn array_holds_quote(items: &[ArrayItem]) -> bool {
517    items.iter().any(|i| match i {
518        ArrayItem::Item(o) => holds_quote(o),
519        ArrayItem::Row(row) => array_holds_quote(row),
520    })
521}
522
523/// The text of `obj` as an element of a list or a tagged object.
524fn element_text(obj: &Object, s: &Settings) -> Option<String> {
525    has_text(obj).then(|| {
526        let mut t = Text::default();
527        write_object(&mut t, obj, s);
528        t.finish()
529    })
530}
531
532/// The text of `obj` as an element of an array (a name quoted).
533fn cell_text(obj: &Object, s: &Settings) -> Option<String> {
534    match obj {
535        Object::Name { value } | Object::LocalName { value } => Some(format!("'{value}'")),
536        _ => element_text(obj, s),
537    }
538}
539
540/// `obj` as JSON (its serde shape) with the calculator's own text in a
541/// `text` field on the object and on every object inside it (a list's
542/// items, a tagged object's object, an array's elements), each as it is
543/// written in that place; no `text` where there is none ([`has_text`]).
544/// A front end shows and copies these texts and formats nothing itself.
545/// An error past [`MAX_DESCRIBED_TEXT`] characters of text.
546pub fn described(obj: &Object, s: &Settings) -> Result<serde_json::Value> {
547    let mut left = MAX_DESCRIBED_TEXT;
548    described_within(obj, s, &mut left)
549}
550
551/// [`described`] with `left` characters of text, shared by the caller's
552/// other calls (the levels of one stack).
553pub(crate) fn described_within(
554    obj: &Object,
555    s: &Settings,
556    left: &mut usize,
557) -> Result<serde_json::Value> {
558    let mut v = serde_json::to_value(obj)?;
559    describe(&mut v, obj, s, text(obj, s), left)?;
560    Ok(v)
561}
562
563fn describe(
564    v: &mut serde_json::Value,
565    obj: &Object,
566    s: &Settings,
567    text: Option<String>,
568    left: &mut usize,
569) -> Result<()> {
570    let Some(map) = v.as_object_mut() else {
571        return Ok(());
572    };
573    if let Some(text) = text {
574        let Some(rest) = left.checked_sub(text.chars().count()) else {
575            bail!(
576                "more than {MAX_DESCRIBED_TEXT} characters of text (MAX_DESCRIBED_TEXT): \
577                 objects that repeat or nest deeply"
578            );
579        };
580        *left = rest;
581        map.insert("text".to_string(), serde_json::Value::String(text));
582    }
583    match obj {
584        Object::List { items } => {
585            if let Some(vs) = map.get_mut("items").and_then(|i| i.as_array_mut()) {
586                for (cv, c) in vs.iter_mut().zip(items) {
587                    describe(cv, c, s, element_text(c, s), left)?;
588                }
589            }
590        }
591        Object::Tagged { object, .. } => {
592            if let Some(cv) = map.get_mut("object") {
593                describe(cv, object, s, element_text(object, s), left)?;
594            }
595        }
596        Object::Array { items, .. } => {
597            if let Some(vs) = map.get_mut("items").and_then(|i| i.as_array_mut()) {
598                describe_array(vs, items, s, left)?;
599            }
600        }
601        _ => {}
602    }
603    Ok(())
604}
605
606fn describe_array(
607    vs: &mut [serde_json::Value],
608    items: &[ArrayItem],
609    s: &Settings,
610    left: &mut usize,
611) -> Result<()> {
612    for (cv, item) in vs.iter_mut().zip(items) {
613        match item {
614            ArrayItem::Item(o) => describe(cv, o, s, cell_text(o, s), left)?,
615            ArrayItem::Row(row) => {
616                if let Some(rv) = cv.as_array_mut() {
617                    describe_array(rv, row, s, left)?;
618                }
619            }
620        }
621    }
622    Ok(())
623}
624
625pub(crate) fn write_object(t: &mut Text, obj: &Object, s: &Settings) {
626    if t.full() {
627        return;
628    }
629    match obj {
630        Object::Real { value } => t.push(&s.real(value)),
631        Object::Integer { value } => t.push(&value.to_source()),
632        Object::Complex { re, im } => t.push(&s.complex(re, im)),
633        Object::String { value } => {
634            t.push("\"");
635            t.push(value);
636            t.push("\"");
637        }
638        Object::Name { value } | Object::LocalName { value } => t.push(value),
639        Object::Character { value } => {
640            t.push("CHR ");
641            t.push(value);
642        }
643        Object::Binary { value, .. } => t.push(&s.binary(*value)),
644        Object::List { items } => {
645            t.push("{");
646            for o in items {
647                t.push(" ");
648                write_object(t, o, s);
649            }
650            t.push(" }");
651        }
652        Object::Tagged { tag, object } => {
653            if s.tag_colon {
654                t.push(":");
655            }
656            t.push(tag);
657            t.push(": ");
658            write_object(t, object, s);
659        }
660        Object::Unit { value, unit } => {
661            t.push(&s.unit_number(value));
662            t.push("_");
663            t.push(unit.as_deref().unwrap_or("?"));
664        }
665        Object::Array { items, .. } => write_array(t, items, s),
666        Object::Program { source } | Object::Algebraic { source } => {
667            t.push(source.as_deref().unwrap_or("?"));
668        }
669        Object::Command {
670            name,
671            source,
672            address,
673            library,
674            command,
675        } => match name.as_deref().or(source.as_deref()) {
676            Some(text) => t.push(text),
677            None => t.push(&command_text(None, *address, library.zip(*command))),
678        },
679        Object::Unknown { kind, source, .. } => match source {
680            Some(src) => t.push(src),
681            None => t.push(kind.as_deref().unwrap_or("External")),
682        },
683    }
684}
685
686fn write_array(t: &mut Text, items: &[ArrayItem], s: &Settings) {
687    t.push("[");
688    for (i, item) in items.iter().enumerate() {
689        match item {
690            ArrayItem::Item(o) => {
691                t.push(" ");
692                // A name in a (49G symbolic) array is shown quoted.
693                if let Object::Name { value } | Object::LocalName { value } = o.as_ref() {
694                    t.push("'");
695                    t.push(value);
696                    t.push("'");
697                } else {
698                    write_object(t, o, s);
699                }
700            }
701            ArrayItem::Row(row) => {
702                t.push(if i == 0 { "" } else { " " });
703                write_array(t, row, s);
704            }
705        }
706    }
707    if matches!(items.first(), Some(ArrayItem::Row(_))) {
708        t.push("]");
709    } else {
710        t.push(" ]");
711    }
712}
713
714/// A command's text: its name; an XLIB name without one as the
715/// calculator shows it (`XLIB 1234 5`); an unnamed ROM object `External`.
716fn command_text(name: Option<&str>, address: Option<u32>, xlib: Option<(u16, u16)>) -> String {
717    match (name, address, xlib) {
718        (Some(n), _, _) => n.to_string(),
719        (None, None, Some((l, c))) => format!("XLIB {l} {c}"),
720        _ => "External".to_string(),
721    }
722}
723
724/// One element of a program, algebraic or unit body.
725#[derive(Clone, Debug)]
726pub(crate) enum Element {
727    /// A data object (embedded, or a ROM constant).
728    Object(Object),
729    /// A command: its name (`None`: no name in the ROM's tables), its ROM
730    /// address (not for XLIB names), its argument count when known.
731    Command {
732        name: Option<String>,
733        address: Option<u32>,
734        /// Library and command number, when known.
735        xlib: Option<(u16, u16)>,
736        arity: Option<u8>,
737        /// Shown as nothing (see `CommandInfo::silent`).
738        silent: bool,
739    },
740    /// An algebraic inside a composite, with its own elements (an
741    /// operand of `∂`, `Σ`, `|`, a user function's name).
742    Symbolic {
743        object: Object,
744        elements: Vec<Element>,
745    },
746    /// A system binary (in an algebraic, the operand count of a user
747    /// function or of `|`).
748    SystemBinary(u32),
749    /// A unit operator.
750    Unit(UnitOp),
751}
752
753/// Unit operators (wiki: protocols/rpl-libraries "Unit operators").
754#[derive(Clone, Copy, Debug, PartialEq, Eq)]
755pub(crate) enum UnitOp {
756    Times,
757    Divide,
758    Power,
759    Prefix,
760    End,
761}
762
763impl Element {
764    fn token(&self, t: &mut Text, s: &Settings) {
765        match self {
766            Element::Object(o) | Element::Symbolic { object: o, .. } => write_object(t, o, s),
767            Element::Command {
768                name,
769                address,
770                xlib,
771                ..
772            } => t.push(&command_text(name.as_deref(), *address, *xlib)),
773            Element::SystemBinary(v) => t.push(&format!("<{v:X}h>")),
774            Element::Unit(_) => t.push("External"),
775        }
776    }
777
778    fn is_quote(&self) -> bool {
779        matches!(self, Element::Command { name: Some(n), .. } if n == "'")
780    }
781}
782
783/// A program's text: its elements separated by spaces. A user program's
784/// body starts with the command `«` and ends with `»`; a program inside a
785/// structure (a CASE clause) has neither and shows its elements only. A
786/// quoted object `' X '` closes up to `'X'`.
787pub(crate) fn program(elements: &[Element], s: &Settings) -> String {
788    let mut t = Text::default();
789    let mut first = true;
790    let mut open = false;
791    let mut glue = false;
792    for e in elements {
793        if t.full() {
794            break;
795        }
796        if matches!(e, Element::Command { silent: true, .. }) {
797            continue;
798        }
799        if e.is_quote() && open {
800            // Closing quote: no space before it.
801            t.push("'");
802            open = false;
803            continue;
804        }
805        if !first && !glue {
806            t.push(" ");
807        }
808        first = false;
809        glue = false;
810        if e.is_quote() {
811            // Opening quote: no space after it.
812            t.push("'");
813            open = true;
814            glue = true;
815        } else {
816            e.token(&mut t, s);
817        }
818    }
819    t.finish()
820}
821
822/// An expression node of an algebraic or unit.
823#[derive(Clone, Debug)]
824enum Node {
825    /// Text that binds tightest (a name, a number, a call).
826    Atom(String),
827    /// A number, which needs parentheses where its sign could bind
828    /// differently.
829    Negative(String),
830    /// A binary infix operator.
831    Infix {
832        op: String,
833        prec: u8,
834        left: usize,
835        right: usize,
836        spaced: bool,
837    },
838    /// A prefix operator (`-`, `NOT `, `√`).
839    Prefix { op: String, prec: u8, arg: usize },
840    /// A postfix operator (`!`).
841    Postfix { op: String, arg: usize },
842    /// A function call `F(a,b)`; with `equation`, the first two
843    /// arguments are written `a=b` (`Σ(K=0,M,K)`).
844    Call {
845        name: String,
846        args: Vec<usize>,
847        equation: bool,
848    },
849    /// `expr|(name=value,...)`.
850    Where {
851        expr: usize,
852        pairs: Vec<(usize, usize)>,
853    },
854    /// The derivative `∂X(f)`.
855    Derivative { var: usize, expr: usize },
856    /// Juxtaposition without operator (a unit prefix `k` + `m`).
857    Concat { left: usize, right: usize },
858}
859
860/// Binding strength of the operators (higher binds tighter).
861const ATOM: u8 = 20;
862const PREC_ROOT: u8 = 13;
863const PREC_POWER: u8 = 12;
864const PREC_NEG: u8 = 11;
865const PREC_PRODUCT: u8 = 10;
866const PREC_SUM: u8 = 9;
867const PREC_COMPARE: u8 = 7;
868const PREC_NOT: u8 = 6;
869const PREC_AND: u8 = 5;
870const PREC_OR: u8 = 4;
871const PREC_EQUATION: u8 = 3;
872const PREC_WHERE: u8 = 2;
873
874/// The infix operators by name: precedence, whether written with spaces.
875fn infix(name: &str) -> Option<(u8, bool)> {
876    Some(match name {
877        "^" => (PREC_POWER, false),
878        "*" | "/" => (PREC_PRODUCT, false),
879        "+" | "-" => (PREC_SUM, false),
880        "==" | "≠" | "<" | ">" | "≤" | "≥" => (PREC_COMPARE, false),
881        "AND" => (PREC_AND, true),
882        "OR" | "XOR" => (PREC_OR, true),
883        "=" => (PREC_EQUATION, false),
884        "|" => (PREC_WHERE, false),
885        _ => return None,
886    })
887}
888
889struct Tree {
890    nodes: Vec<Node>,
891}
892
893impl Tree {
894    fn add(&mut self, n: Node) -> usize {
895        self.nodes.push(n);
896        self.nodes.len() - 1
897    }
898
899    fn prec(&self, i: usize) -> u8 {
900        match &self.nodes[i] {
901            Node::Atom(_) | Node::Call { .. } | Node::Derivative { .. } | Node::Concat { .. } => {
902                ATOM
903            }
904            Node::Where { .. } => PREC_WHERE,
905            Node::Negative(_) => PREC_NEG,
906            Node::Infix { prec, .. } | Node::Prefix { prec, .. } => *prec,
907            Node::Postfix { .. } => ATOM - 1,
908        }
909    }
910
911    /// Render node `root` into `t` without recursion (garbage can nest
912    /// arbitrarily deep).
913    /// Whether node `i` starts with a sign or prefix operator.
914    fn signed(&self, i: usize) -> bool {
915        matches!(self.nodes[i], Node::Prefix { .. } | Node::Negative(_))
916    }
917
918    fn render(&self, root: usize, t: &mut Text, all_parens: bool, power_right: bool) {
919        enum Work<'a> {
920            Node(usize),
921            Text(&'a str),
922            Owned(String),
923        }
924        let mut stack = vec![Work::Node(root)];
925        while let Some(w) = stack.pop() {
926            if t.full() {
927                return;
928            }
929            let i = match w {
930                Work::Text(s) => {
931                    t.push(s);
932                    continue;
933                }
934                Work::Owned(s) => {
935                    t.push(&s);
936                    continue;
937                }
938                Work::Node(i) => i,
939            };
940            // Children are pushed in reverse order of output.
941            let operand = |stack: &mut Vec<Work<'_>>, child: usize, parens: bool| {
942                let compound = !matches!(
943                    self.nodes[child],
944                    Node::Atom(_) | Node::Call { .. } | Node::Derivative { .. }
945                );
946                if parens || (all_parens && compound) {
947                    stack.push(Work::Text(")"));
948                    stack.push(Work::Node(child));
949                    stack.push(Work::Text("("));
950                } else {
951                    stack.push(Work::Node(child));
952                }
953            };
954            match &self.nodes[i] {
955                Node::Atom(s) | Node::Negative(s) => t.push(s),
956                Node::Infix {
957                    op,
958                    prec,
959                    left,
960                    right,
961                    spaced,
962                } => {
963                    // Operators group to the left, `^` to the right where
964                    // `power_right`; a sign or prefix on the right reads
965                    // unambiguously (`A^-B`).
966                    let right_assoc = power_right && *prec == PREC_POWER;
967                    // A where-expression on the left is written bare
968                    // (`X|(X=2)+1`, `X|(X=2)^2`), on the right and under
969                    // a prefix it is parenthesised (`1+(X|(X=2))`,
970                    // `-(X|(X=2))`): oracle-checked on all three ROMs.
971                    let where_left = matches!(self.nodes[*left], Node::Where { .. });
972                    let lp = !where_left
973                        && if right_assoc {
974                            self.prec(*left) <= *prec
975                        } else {
976                            self.prec(*left) < *prec
977                        };
978                    let rp = if right_assoc {
979                        self.prec(*right) < *prec
980                    } else {
981                        self.prec(*right) <= *prec
982                    } && !self.signed(*right);
983                    operand(&mut stack, *right, rp);
984                    if *spaced {
985                        stack.push(Work::Owned(format!(" {op} ")));
986                    } else {
987                        stack.push(Work::Owned(op.clone()));
988                    }
989                    operand(&mut stack, *left, lp);
990                }
991                Node::Prefix { op, prec, arg } => {
992                    let p = self.prec(*arg) < *prec && !self.signed(*arg);
993                    operand(&mut stack, *arg, p);
994                    // `NOT A` but `NOT(A AND B)`.
995                    t.push(if p { op.trim_end() } else { op });
996                }
997                Node::Postfix { op, arg } => {
998                    stack.push(Work::Owned(op.clone()));
999                    let p = self.prec(*arg) < ATOM;
1000                    operand(&mut stack, *arg, p);
1001                }
1002                Node::Call {
1003                    name,
1004                    args,
1005                    equation,
1006                } => {
1007                    stack.push(Work::Text(")"));
1008                    for (k, a) in args.iter().enumerate().rev() {
1009                        stack.push(Work::Node(*a));
1010                        if k == 1 && *equation {
1011                            stack.push(Work::Text("="));
1012                        } else if k > 0 {
1013                            stack.push(Work::Text(","));
1014                        }
1015                    }
1016                    t.push(name);
1017                    t.push("(");
1018                }
1019                Node::Where { expr, pairs } => {
1020                    stack.push(Work::Text(")"));
1021                    for (k, (n, v)) in pairs.iter().enumerate().rev() {
1022                        stack.push(Work::Node(*v));
1023                        stack.push(Work::Text("="));
1024                        stack.push(Work::Node(*n));
1025                        if k > 0 {
1026                            stack.push(Work::Text(","));
1027                        }
1028                    }
1029                    stack.push(Work::Text("|("));
1030                    let p = self.prec(*expr) <= PREC_WHERE;
1031                    operand(&mut stack, *expr, p);
1032                }
1033                Node::Derivative { var, expr } => {
1034                    stack.push(Work::Text(")"));
1035                    stack.push(Work::Node(*expr));
1036                    stack.push(Work::Text("("));
1037                    stack.push(Work::Node(*var));
1038                    t.push("∂");
1039                }
1040                Node::Concat { left, right } => {
1041                    stack.push(Work::Node(*right));
1042                    stack.push(Work::Node(*left));
1043                }
1044            }
1045        }
1046    }
1047}
1048
1049/// An operand on the evaluation stack: a node, or a user function's
1050/// argument count.
1051#[derive(Clone, Copy, Debug)]
1052enum Slot {
1053    Node(usize),
1054    Count(u32),
1055}
1056
1057/// An algebraic's text with its quotes, or `None` when its elements do
1058/// not form one expression (a command without a known argument count, a
1059/// malformed body).
1060pub(crate) fn algebraic(elements: &[Element], s: &Settings) -> Option<String> {
1061    let s = &s.inner();
1062    let mut tree = Tree { nodes: Vec::new() };
1063    let root = expression(elements, s, &mut tree)?;
1064    let mut t = Text::default();
1065    t.push("'");
1066    tree.render(root, &mut t, s.all_parens, s.power_right);
1067    t.push("'");
1068    Some(t.finish())
1069}
1070
1071fn leaf(o: &Object, s: &Settings) -> Node {
1072    let mut t = Text::default();
1073    write_object(&mut t, o, s);
1074    let text = t.finish();
1075    let negative = match o {
1076        Object::Real { value } => value.negative,
1077        Object::Integer { value } => value.negative,
1078        Object::Unit { value, .. } => value.negative,
1079        _ => false,
1080    };
1081    if negative {
1082        Node::Negative(text)
1083    } else {
1084        Node::Atom(text)
1085    }
1086}
1087
1088fn expression(elements: &[Element], s: &Settings, tree: &mut Tree) -> Option<usize> {
1089    let mut stack: Vec<Slot> = Vec::new();
1090    for e in elements {
1091        let node = match e {
1092            Element::Object(o) => tree.add(leaf(o, s)),
1093            Element::Symbolic { elements, .. } => expression(elements, s, tree)?,
1094            Element::SystemBinary(v) => {
1095                stack.push(Slot::Count(*v));
1096                continue;
1097            }
1098            Element::Unit(_) => return None,
1099            Element::Command { name, arity, .. } => {
1100                if let Some(Slot::Count(n)) = stack.last().copied() {
1101                    stack.pop();
1102                    counted(name.as_deref(), n, &mut stack, tree)?
1103                } else {
1104                    apply(name.as_deref()?, *arity, &mut stack, tree)?
1105                }
1106            }
1107        };
1108        stack.push(Slot::Node(node));
1109    }
1110    match stack.as_slice() {
1111        [Slot::Node(n)] => Some(*n),
1112        _ => None,
1113    }
1114}
1115
1116/// A command that takes its operand count from the stack: `|` (the
1117/// expression, then name and value pairs; the count covers all of them),
1118/// or a user function (the arguments and its name; the count covers the
1119/// arguments; the command applying it has no name).
1120fn counted(name: Option<&str>, n: u32, stack: &mut Vec<Slot>, tree: &mut Tree) -> Option<usize> {
1121    let mut pop = || match stack.pop()? {
1122        Slot::Node(n) => Some(n),
1123        Slot::Count(_) => None,
1124    };
1125    match name {
1126        Some("|") => {
1127            let mut items = Vec::new();
1128            for _ in 0..n {
1129                items.push(pop()?);
1130            }
1131            items.reverse();
1132            let (&expr, rest) = items.split_first()?;
1133            if rest.len() % 2 != 0 {
1134                return None;
1135            }
1136            let pairs = rest.chunks(2).map(|p| (p[0], p[1])).collect();
1137            Some(tree.add(Node::Where { expr, pairs }))
1138        }
1139        None => {
1140            let f = pop()?;
1141            let mut args = Vec::new();
1142            for _ in 0..n {
1143                args.push(pop()?);
1144            }
1145            args.reverse();
1146            let Node::Atom(name) = &tree.nodes[f] else {
1147                return None;
1148            };
1149            let name = name.clone();
1150            Some(tree.add(Node::Call {
1151                name,
1152                args,
1153                equation: false,
1154            }))
1155        }
1156        Some(_) => None,
1157    }
1158}
1159
1160/// Apply command `name` to operands on `stack`.
1161fn apply(name: &str, arity: Option<u8>, stack: &mut Vec<Slot>, tree: &mut Tree) -> Option<usize> {
1162    let mut pop = || match stack.pop()? {
1163        Slot::Node(n) => Some(n),
1164        Slot::Count(_) => None,
1165    };
1166    // Without a known argument count there is no telling how many
1167    // operands it takes: no text (the caller leaves `source` unset).
1168    let arity = arity?;
1169    if arity == 2
1170        && let Some((prec, spaced)) = infix(name)
1171    {
1172        let right = pop()?;
1173        let left = pop()?;
1174        return Some(tree.add(Node::Infix {
1175            op: name.to_string(),
1176            prec,
1177            left,
1178            right,
1179            spaced,
1180        }));
1181    }
1182    let unary = |op: &str, prec: u8, arg: usize, tree: &mut Tree| {
1183        tree.add(Node::Prefix {
1184            op: op.to_string(),
1185            prec,
1186            arg,
1187        })
1188    };
1189    if arity == 1 {
1190        match name {
1191            "NEG" => return Some(unary("-", PREC_NEG, pop()?, tree)),
1192            "NOT" => return Some(unary("NOT ", PREC_NOT, pop()?, tree)),
1193            "√" => return Some(unary("√", PREC_ROOT, pop()?, tree)),
1194            "!" => {
1195                let arg = pop()?;
1196                return Some(tree.add(Node::Postfix {
1197                    op: "!".into(),
1198                    arg,
1199                }));
1200            }
1201            _ => {}
1202        }
1203    }
1204    if name == "∂" && arity == 2 {
1205        let expr = pop()?;
1206        let var = pop()?;
1207        return Some(tree.add(Node::Derivative { var, expr }));
1208    }
1209    let mut args = Vec::new();
1210    for _ in 0..arity {
1211        args.push(pop()?);
1212    }
1213    args.reverse();
1214    if arity == 0 {
1215        return Some(tree.add(Node::Atom(name.to_string())));
1216    }
1217    Some(tree.add(Node::Call {
1218        name: name.to_string(),
1219        args,
1220        equation: name == "Σ" && arity == 4,
1221    }))
1222}
1223
1224/// A unit expression's text (`kg*m^2/s^2`) from the elements after the
1225/// number, or `None` if they do not form one.
1226pub(crate) fn unit(elements: &[Element], s: &Settings) -> Option<String> {
1227    let mut tree = Tree { nodes: Vec::new() };
1228    let mut stack: Vec<usize> = Vec::new();
1229    let mut ended = false;
1230    for e in elements {
1231        if ended {
1232            return None;
1233        }
1234        match e {
1235            Element::Object(Object::String { value })
1236            | Element::Object(Object::Character { value }) => {
1237                stack.push(tree.add(Node::Atom(value.clone())));
1238            }
1239            Element::Object(Object::Real { value }) => {
1240                stack.push(tree.add(Node::Atom(s.unit_number(value))));
1241            }
1242            Element::Object(o @ Object::Integer { .. }) => {
1243                stack.push(tree.add(leaf(o, s)));
1244            }
1245            Element::Unit(op) => {
1246                if *op == UnitOp::End {
1247                    ended = true;
1248                    continue;
1249                }
1250                let right = stack.pop()?;
1251                let left = stack.pop()?;
1252                let node = match op {
1253                    UnitOp::Prefix => Node::Concat { left, right },
1254                    UnitOp::Times | UnitOp::Divide | UnitOp::Power => {
1255                        let (op, prec) = match op {
1256                            UnitOp::Times => ("*", PREC_PRODUCT),
1257                            UnitOp::Divide => ("/", PREC_PRODUCT),
1258                            _ => ("^", PREC_POWER),
1259                        };
1260                        Node::Infix {
1261                            op: op.into(),
1262                            prec,
1263                            left,
1264                            right,
1265                            spaced: false,
1266                        }
1267                    }
1268                    UnitOp::End => return None,
1269                };
1270                stack.push(tree.add(node));
1271            }
1272            _ => return None,
1273        }
1274    }
1275    let [root] = stack.as_slice() else {
1276        return None;
1277    };
1278    let mut t = Text::default();
1279    tree.render(*root, &mut t, false, false);
1280    Some(t.finish())
1281}
1282
1283#[cfg(test)]
1284mod tests {
1285    use super::*;
1286
1287    fn real(s: &str) -> Real {
1288        Real::parse(s).unwrap()
1289    }
1290
1291    /// The text to edit keeps every digit and writes a tagged object so
1292    /// that it compiles; a string holding `"` has none.
1293    #[test]
1294    fn edit_text_compiles_back() {
1295        let fix = Settings {
1296            format: NumberFormat::Fix(2),
1297            base: Base::Hex,
1298            word_size: 16,
1299            tag_colon: false,
1300            group_digits: true,
1301            ..Settings::standard(Model::Hp49g)
1302        }
1303        .for_editing();
1304        let real = real_obj("1234.56789");
1305        assert_eq!(edit_text(&real, &fix).unwrap(), "1234.56789");
1306        let tagged = Object::Tagged {
1307            tag: "T".into(),
1308            object: Box::new(real.clone()),
1309        };
1310        assert_eq!(edit_text(&tagged, &fix).unwrap(), ":T: 1234.56789");
1311        let bin = Object::Binary {
1312            value: 0x12345,
1313            base: None,
1314            text: None,
1315        };
1316        assert_eq!(edit_text(&bin, &fix).unwrap(), "# 12345h");
1317        let quoted = Object::List {
1318            items: vec![Object::String {
1319                value: "say \"hi\"".into(),
1320            }],
1321        };
1322        assert!(
1323            edit_text(&quoted, &fix)
1324                .unwrap_err()
1325                .to_string()
1326                .contains('"')
1327        );
1328        let unknown = Object::Program { source: None };
1329        assert!(edit_text(&unknown, &fix).is_err());
1330    }
1331
1332    /// The texts a front end shows: the cases a formatter in the page got
1333    /// wrong (a name in a 49G array, a whole-number unit on the 49G, a
1334    /// small real), each on the object and on the objects inside.
1335    #[test]
1336    fn described_carries_the_text_of_every_object() {
1337        let name = |n: &str| Object::Name {
1338            value: n.to_string(),
1339        };
1340        let hp49 = Settings::standard(Model::Hp49g);
1341        let hp48 = Settings::standard(Model::Hp48gx);
1342        let array = Object::Array {
1343            dims: vec![1],
1344            items: vec![ArrayItem::Item(Box::new(name("A")))],
1345        };
1346        let v = described(&array, &hp49).unwrap();
1347        assert_eq!(v["text"], "[ 'A' ]");
1348        assert_eq!(v["items"][0]["text"], "'A'");
1349        let unit = Object::Unit {
1350            value: real("2"),
1351            unit: Some("m".to_string()),
1352        };
1353        assert_eq!(described(&unit, &hp49).unwrap()["text"], "2_m");
1354        assert_eq!(text(&real_obj("2"), &hp49), Some("2.".to_string()));
1355        assert_eq!(
1356            described(&real_obj("1.23456789012E-5"), &hp48).unwrap()["text"],
1357            "1.23456789012E-5"
1358        );
1359        // A list: its own text, and each element's as written inside it;
1360        // a name alone is quoted, inside a list it is not.
1361        let list = Object::List {
1362            items: vec![
1363                real_obj("1"),
1364                name("QQ"),
1365                Object::Tagged {
1366                    tag: "T".to_string(),
1367                    object: Box::new(real_obj("2")),
1368                },
1369                Object::Program { source: None },
1370            ],
1371        };
1372        let v = described(&list, &hp48).unwrap();
1373        assert!(v.get("text").is_none(), "{v}");
1374        assert_eq!(v["items"][1]["text"], "QQ");
1375        assert_eq!(v["items"][2]["text"], ":T: 2");
1376        assert_eq!(v["items"][2]["object"]["text"], "2");
1377        assert!(v["items"][3].get("text").is_none());
1378        assert_eq!(text(&name("QQ"), &hp48), Some("'QQ'".to_string()));
1379        assert_eq!(text(&Object::Program { source: None }, &hp48), None);
1380    }
1381
1382    fn real_obj(s: &str) -> Object {
1383        Object::Real { value: real(s) }
1384    }
1385
1386    #[test]
1387    fn std_numbers_as_the_48_shows_them() {
1388        let s = Settings::default();
1389        for (v, text) in [
1390            ("1", "1"),
1391            ("-2", "-2"),
1392            ("1.5", "1.5"),
1393            ("1E-5", ".00001"),
1394            (".001", ".001"),
1395            ("123456789012", "123456789012"),
1396            ("1E12", "1.E12"),
1397            ("1.5E300", "1.5E300"),
1398            ("-1.5E-300", "-1.5E-300"),
1399            ("1.23456", "1.23456"),
1400            ("0", "0"),
1401            ("100", "100"),
1402            (".5", ".5"),
1403        ] {
1404            assert_eq!(s.real(&real(v)), text, "{v}");
1405        }
1406        let p = Settings::standard(Model::Hp49g);
1407        assert_eq!(p.real(&real("5")), "5.");
1408        assert_eq!(p.real(&real("0")), "0.");
1409        assert_eq!(p.real(&real("2.5")), "2.5");
1410    }
1411
1412    #[test]
1413    fn text_is_capped() {
1414        let mut t = Text::default();
1415        for _ in 0..MAX_TEXT {
1416            t.push("ab");
1417        }
1418        let s = t.finish();
1419        assert!(s.ends_with(TRUNCATED));
1420        assert_eq!(s.chars().count(), MAX_TEXT + 1);
1421    }
1422
1423    fn cmd(name: &str, arity: u8) -> Element {
1424        Element::Command {
1425            name: Some(name.into()),
1426            address: None,
1427            xlib: None,
1428            arity: Some(arity),
1429            silent: false,
1430        }
1431    }
1432
1433    fn name(n: &str) -> Element {
1434        Element::Object(Object::Name { value: n.into() })
1435    }
1436
1437    fn num(v: &str) -> Element {
1438        Element::Object(Object::Real { value: real(v) })
1439    }
1440
1441    #[test]
1442    fn infix_with_precedence() {
1443        let s = Settings::default();
1444        let alg = |e: &[Element]| algebraic(e, &s).unwrap();
1445        assert_eq!(alg(&[name("A"), num("1"), cmd("+", 2)]), "'A+1'");
1446        // (A+B)*C and A+B*C.
1447        assert_eq!(
1448            alg(&[name("A"), name("B"), cmd("+", 2), name("C"), cmd("*", 2)]),
1449            "'(A+B)*C'"
1450        );
1451        assert_eq!(
1452            alg(&[name("A"), name("B"), name("C"), cmd("*", 2), cmd("+", 2)]),
1453            "'A+B*C'"
1454        );
1455        assert_eq!(
1456            alg(&[name("X"), cmd("SIN", 1), num("2"), cmd("^", 2)]),
1457            "'SIN(X)^2'"
1458        );
1459        assert_eq!(alg(&[name("A"), cmd("NEG", 1)]), "'-A'");
1460        assert_eq!(alg(&[name("A"), name("B"), cmd("MAX", 2)]), "'MAX(A,B)'");
1461        // Unknown argument count, or leftovers: no text.
1462        assert_eq!(algebraic(&[name("A"), name("B")], &s), None);
1463        assert_eq!(
1464            algebraic(
1465                &[
1466                    name("A"),
1467                    Element::Command {
1468                        name: None,
1469                        address: None,
1470                        xlib: None,
1471                        arity: None,
1472                        silent: false,
1473                    }
1474                ],
1475                &s
1476            ),
1477            None
1478        );
1479    }
1480
1481    #[test]
1482    fn deep_expressions_render_without_recursion() {
1483        let s = Settings::default();
1484        let mut e = vec![name("A")];
1485        for _ in 0..200_000 {
1486            e.push(name("B"));
1487            e.push(cmd("-", 2));
1488        }
1489        let text = algebraic(&e, &s).unwrap();
1490        assert!(text.starts_with("'A-B-B"));
1491        assert!(text.ends_with(TRUNCATED));
1492    }
1493}