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