use anyhow::{Result, bail};
use saturnus::Model;
use crate::object::{ArrayItem, Base, Object, Real};
use crate::ram::Flags;
pub const MAX_TEXT: usize = 1 << 16;
pub const TRUNCATED: &str = "…";
pub const MAX_DESCRIBED_TEXT: usize = 1 << 22;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum NumberFormat {
Std,
Fix(u8),
Sci(u8),
Eng(u8),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Settings {
pub format: NumberFormat,
pub comma: bool,
pub base: Base,
pub word_size: u32,
pub all_parens: bool,
pub real_point: bool,
pub power_right: bool,
pub group_digits: bool,
pub tag_colon: bool,
}
impl Default for Settings {
fn default() -> Self {
Settings {
format: NumberFormat::Std,
comma: false,
base: Base::Dec,
word_size: 64,
all_parens: false,
real_point: false,
power_right: false,
group_digits: false,
tag_colon: true,
}
}
}
impl Settings {
pub fn standard(model: Model) -> Settings {
let hp49 = model == Model::Hp49g;
Settings {
real_point: hp49,
power_right: hp49,
tag_colon: !hp49,
..Settings::default()
}
}
pub fn from_flags(flags: &Flags, model: Model) -> Settings {
let set = |n: i32| flags.get(n).unwrap_or(false);
let value = |first: i32, bits: i32| {
(0..bits).fold(0u32, |v, k| v | (u32::from(set(-(first + k))) << k))
};
let digits = u8::try_from(value(45, 4)).unwrap_or(0);
let format = match (set(-49), set(-50)) {
(false, false) => NumberFormat::Std,
(true, false) => NumberFormat::Fix(digits),
(false, true) => NumberFormat::Sci(digits),
(true, true) => NumberFormat::Eng(digits),
};
let hp49 = model == Model::Hp49g;
Settings {
format,
comma: set(-51),
base: flags.base(),
word_size: value(5, 6) + 1,
all_parens: set(-53),
real_point: hp49,
power_right: hp49,
group_digits: hp49,
tag_colon: !hp49,
}
}
pub fn for_editing(self) -> Settings {
Settings {
format: NumberFormat::Std,
word_size: 64,
group_digits: false,
tag_colon: true,
..self
}
}
pub fn binary(&self, value: u64) -> String {
let mask = if self.word_size >= 64 {
u64::MAX
} else {
(1u64 << self.word_size) - 1
};
self.base.format(value & mask)
}
pub fn real(&self, r: &Real) -> String {
let s = match self.format {
NumberFormat::Std => std_text(r, self.real_point),
NumberFormat::Fix(d) if self.group_digits => {
group(&fix_text(r, usize::from(d)), self.comma)
}
NumberFormat::Fix(d) => fix_text(r, usize::from(d)),
NumberFormat::Sci(d) => sci_text(r, usize::from(d), 1),
NumberFormat::Eng(d) => sci_text(r, usize::from(d), 3),
};
if self.comma {
s.replace('.', ",").replace('\u{1}', ".")
} else {
s
}
}
pub fn unit_number(&self, r: &Real) -> String {
let integer = r.exponent >= 0
&& r.exponent < 12
&& r.digits[(r.exponent as usize + 1).min(12)..]
.iter()
.all(|&d| d == 0);
let format = if integer || r.digits.iter().all(|&d| d == 0) {
NumberFormat::Std
} else {
self.format
};
Settings {
format,
real_point: false,
group_digits: false,
..*self
}
.real(r)
}
fn inner(&self) -> Settings {
Settings {
group_digits: false,
..*self
}
}
pub fn complex(&self, re: &Real, im: &Real) -> String {
let sep = if self.comma { ';' } else { ',' };
let s = self.inner();
format!("({}{sep}{})", s.real(re), s.real(im))
}
}
fn group(text: &str, comma: bool) -> String {
let sep = if comma { '\u{1}' } else { ',' };
let (sign, rest) = match text.strip_prefix('-') {
Some(r) => ("-", r),
None => ("", text),
};
if rest.contains('E') {
return text.to_string();
}
let (int, frac) = rest.split_once('.').unwrap_or((rest, ""));
let mut out = String::new();
for (i, c) in int.chars().enumerate() {
if i > 0 && (int.len() - i) % 3 == 0 {
out.push(sep);
}
out.push(c);
}
format!("{sign}{out}.{frac}")
}
fn significant(r: &Real) -> &[u8] {
let last = r.digits.iter().rposition(|&d| d != 0).unwrap_or(0);
&r.digits[..=last]
}
fn digit(d: u8) -> char {
char::from(b'0' + d)
}
fn std_text(r: &Real, real_point: bool) -> String {
let sign = if r.negative { "-" } else { "" };
let sig = significant(r);
if r.digits.iter().all(|&d| d == 0) {
return if real_point { "0.".into() } else { "0".into() };
}
let e = r.exponent;
let k = sig.len() as i32;
if (0..12).contains(&e) {
let int: String = (0..=e)
.map(|i| digit(sig.get(i as usize).copied().unwrap_or(0)))
.collect();
let frac: String = sig
.iter()
.skip((e + 1) as usize)
.map(|&d| digit(d))
.collect();
if frac.is_empty() {
let point = if real_point { "." } else { "" };
return format!("{sign}{int}{point}");
}
return format!("{sign}{int}.{frac}");
}
if e < 0 && -e - 1 + k <= 12 {
let zeros = "0".repeat((-e - 1) as usize);
let frac: String = sig.iter().map(|&d| digit(d)).collect();
return format!("{sign}.{zeros}{frac}");
}
let rest: String = sig[1..].iter().map(|&d| digit(d)).collect();
format!("{sign}{}.{rest}E{e}", digit(sig[0]))
}
fn rounded(r: &Real, keep: usize) -> (Vec<u8>, i32) {
let mut d: Vec<u8> = r.digits.to_vec();
let mut e = r.exponent;
if keep >= 12 {
return (d, e);
}
let up = d[keep] >= 5;
d.truncate(keep);
if up {
let mut i = keep;
loop {
if i == 0 {
d.insert(0, 1);
d.truncate(keep.max(1));
e += 1;
break;
}
i -= 1;
if d[i] == 9 {
d[i] = 0;
} else {
d[i] += 1;
break;
}
}
}
(d, e)
}
fn fix_text(r: &Real, decimals: usize) -> String {
if r.digits.iter().all(|&d| d == 0) {
return format!("0.{}", "0".repeat(decimals));
}
let sign = if r.negative { "-" } else { "" };
let places = |e: i32| -> i32 {
if e >= 0 {
(decimals as i32).min(11 - e)
} else {
decimals as i32
}
};
let keep = r.exponent + 1 + places(r.exponent);
let (d, e) = match keep {
k if k >= 1 => rounded(r, k as usize),
0 if r.digits[0] >= 5 => (vec![1], r.exponent + 1),
_ => return sci_text(r, decimals.min(11), 1),
};
if e >= 12 {
return sci_text(r, decimals.min(11), 1);
}
let dec = places(e).max(0);
let at = |place: i32| -> u8 {
let i = e - place;
if i < 0 {
0
} else {
d.get(i as usize).copied().unwrap_or(0)
}
};
let int: String = if e >= 0 {
(0..=e).rev().map(|p| digit(at(p))).collect()
} else {
"0".into()
};
let frac: String = (1..=dec).map(|p| digit(at(-p))).collect();
format!("{sign}{int}.{frac}")
}
fn sci_text(r: &Real, decimals: usize, group: i32) -> String {
let sign = if r.negative { "-" } else { "" };
if r.digits.iter().all(|&d| d == 0) {
let zeros = "0".repeat(decimals);
return format!("0.{zeros}E0");
}
let sig = (decimals + 1).min(12);
let (mut d, e) = rounded(r, sig);
let shift = e.rem_euclid(group);
let exp = e - shift;
let int_digits = (shift + 1) as usize;
d.resize(sig.max(int_digits), 0);
let int: String = d[..int_digits].iter().map(|&x| digit(x)).collect();
let frac: String = d[int_digits..sig.max(int_digits)]
.iter()
.map(|&x| digit(x))
.collect();
format!("{sign}{int}.{frac}E{exp}")
}
#[derive(Debug, Default)]
pub(crate) struct Text {
s: String,
chars: usize,
full: bool,
}
impl Text {
pub(crate) fn push(&mut self, t: &str) {
if self.full {
return;
}
for c in t.chars() {
if self.chars == MAX_TEXT {
self.s.push_str(TRUNCATED);
self.full = true;
return;
}
self.s.push(c);
self.chars += 1;
}
}
pub(crate) fn full(&self) -> bool {
self.full
}
pub(crate) fn finish(self) -> String {
self.s
}
}
pub fn display(obj: &Object, s: &Settings) -> String {
let mut t = Text::default();
match obj {
Object::Real { value } => t.push(
&Settings {
group_digits: true,
..*s
}
.real(value),
),
Object::Tagged { tag, object } => {
t.push(tag);
t.push(": ");
write_object(&mut t, object, s);
}
_ => write_object(&mut t, obj, s),
}
t.finish()
}
pub fn has_text(obj: &Object) -> bool {
match obj {
Object::Program { source } | Object::Algebraic { source } => source.is_some(),
Object::Command {
name,
source,
address,
library,
command,
} => {
name.is_some()
|| source.is_some()
|| (address.is_none() && library.is_some() && command.is_some())
}
Object::Unit { unit, .. } => unit.is_some(),
Object::Unknown { source, .. } => source.is_some(),
Object::List { items } => items.iter().all(has_text),
Object::Tagged { object, .. } => has_text(object),
Object::Array { items, .. } => array_has_text(items),
_ => true,
}
}
fn array_has_text(items: &[ArrayItem]) -> bool {
items.iter().all(|i| match i {
ArrayItem::Item(o) => has_text(o),
ArrayItem::Row(row) => array_has_text(row),
})
}
pub fn text(obj: &Object, s: &Settings) -> Option<String> {
if !has_text(obj) {
return None;
}
Some(match obj {
Object::Name { value } | Object::LocalName { value } => format!("'{value}'"),
_ => display(obj, s),
})
}
pub fn edit_text(obj: &Object, s: &Settings) -> Result<String> {
if !has_text(obj) {
bail!("it has no text form (a graphic, library, backup, directory or code object)");
}
if holds_quote(obj) {
bail!("it holds a string with a \" in it, which no text can write");
}
let text = match obj {
Object::Tagged { .. } => element_text(obj, s).unwrap_or_default(),
_ => text(obj, s).unwrap_or_default(),
};
if text.chars().count() > MAX_TEXT {
bail!("its text is longer than {MAX_TEXT} characters");
}
Ok(text)
}
fn holds_quote(obj: &Object) -> bool {
match obj {
Object::String { value } => value.contains('"'),
Object::List { items } => items.iter().any(holds_quote),
Object::Tagged { object, .. } => holds_quote(object),
Object::Array { items, .. } => array_holds_quote(items),
_ => false,
}
}
fn array_holds_quote(items: &[ArrayItem]) -> bool {
items.iter().any(|i| match i {
ArrayItem::Item(o) => holds_quote(o),
ArrayItem::Row(row) => array_holds_quote(row),
})
}
fn element_text(obj: &Object, s: &Settings) -> Option<String> {
has_text(obj).then(|| {
let mut t = Text::default();
write_object(&mut t, obj, s);
t.finish()
})
}
fn cell_text(obj: &Object, s: &Settings) -> Option<String> {
match obj {
Object::Name { value } | Object::LocalName { value } => Some(format!("'{value}'")),
_ => element_text(obj, s),
}
}
pub fn described(obj: &Object, s: &Settings) -> Result<serde_json::Value> {
let mut left = MAX_DESCRIBED_TEXT;
described_within(obj, s, &mut left)
}
pub(crate) fn described_within(
obj: &Object,
s: &Settings,
left: &mut usize,
) -> Result<serde_json::Value> {
let mut v = serde_json::to_value(obj)?;
describe(&mut v, obj, s, text(obj, s), left)?;
Ok(v)
}
fn describe(
v: &mut serde_json::Value,
obj: &Object,
s: &Settings,
text: Option<String>,
left: &mut usize,
) -> Result<()> {
let Some(map) = v.as_object_mut() else {
return Ok(());
};
if let Some(text) = text {
let Some(rest) = left.checked_sub(text.chars().count()) else {
bail!(
"more than {MAX_DESCRIBED_TEXT} characters of text (MAX_DESCRIBED_TEXT): \
objects that repeat or nest deeply"
);
};
*left = rest;
map.insert("text".to_string(), serde_json::Value::String(text));
}
match obj {
Object::List { items } => {
if let Some(vs) = map.get_mut("items").and_then(|i| i.as_array_mut()) {
for (cv, c) in vs.iter_mut().zip(items) {
describe(cv, c, s, element_text(c, s), left)?;
}
}
}
Object::Tagged { object, .. } => {
if let Some(cv) = map.get_mut("object") {
describe(cv, object, s, element_text(object, s), left)?;
}
}
Object::Array { items, .. } => {
if let Some(vs) = map.get_mut("items").and_then(|i| i.as_array_mut()) {
describe_array(vs, items, s, left)?;
}
}
_ => {}
}
Ok(())
}
fn describe_array(
vs: &mut [serde_json::Value],
items: &[ArrayItem],
s: &Settings,
left: &mut usize,
) -> Result<()> {
for (cv, item) in vs.iter_mut().zip(items) {
match item {
ArrayItem::Item(o) => describe(cv, o, s, cell_text(o, s), left)?,
ArrayItem::Row(row) => {
if let Some(rv) = cv.as_array_mut() {
describe_array(rv, row, s, left)?;
}
}
}
}
Ok(())
}
pub(crate) fn write_object(t: &mut Text, obj: &Object, s: &Settings) {
if t.full() {
return;
}
match obj {
Object::Real { value } => t.push(&s.real(value)),
Object::Integer { value } => t.push(&value.to_source()),
Object::Complex { re, im } => t.push(&s.complex(re, im)),
Object::String { value } => {
t.push("\"");
t.push(value);
t.push("\"");
}
Object::Name { value } | Object::LocalName { value } => t.push(value),
Object::Character { value } => {
t.push("CHR ");
t.push(value);
}
Object::Binary { value, .. } => t.push(&s.binary(*value)),
Object::List { items } => {
t.push("{");
for o in items {
t.push(" ");
write_object(t, o, s);
}
t.push(" }");
}
Object::Tagged { tag, object } => {
if s.tag_colon {
t.push(":");
}
t.push(tag);
t.push(": ");
write_object(t, object, s);
}
Object::Unit { value, unit } => {
t.push(&s.unit_number(value));
t.push("_");
t.push(unit.as_deref().unwrap_or("?"));
}
Object::Array { items, .. } => write_array(t, items, s),
Object::Program { source } | Object::Algebraic { source } => {
t.push(source.as_deref().unwrap_or("?"));
}
Object::Command {
name,
source,
address,
library,
command,
} => match name.as_deref().or(source.as_deref()) {
Some(text) => t.push(text),
None => t.push(&command_text(None, *address, library.zip(*command))),
},
Object::Unknown { kind, source, .. } => match source {
Some(src) => t.push(src),
None => t.push(kind.as_deref().unwrap_or("External")),
},
}
}
fn write_array(t: &mut Text, items: &[ArrayItem], s: &Settings) {
t.push("[");
for (i, item) in items.iter().enumerate() {
match item {
ArrayItem::Item(o) => {
t.push(" ");
if let Object::Name { value } | Object::LocalName { value } = o.as_ref() {
t.push("'");
t.push(value);
t.push("'");
} else {
write_object(t, o, s);
}
}
ArrayItem::Row(row) => {
t.push(if i == 0 { "" } else { " " });
write_array(t, row, s);
}
}
}
if matches!(items.first(), Some(ArrayItem::Row(_))) {
t.push("]");
} else {
t.push(" ]");
}
}
fn command_text(name: Option<&str>, address: Option<u32>, xlib: Option<(u16, u16)>) -> String {
match (name, address, xlib) {
(Some(n), _, _) => n.to_string(),
(None, None, Some((l, c))) => format!("XLIB {l} {c}"),
_ => "External".to_string(),
}
}
#[derive(Clone, Debug)]
pub(crate) enum Element {
Object(Object),
Command {
name: Option<String>,
address: Option<u32>,
xlib: Option<(u16, u16)>,
arity: Option<u8>,
silent: bool,
},
Symbolic {
object: Object,
elements: Vec<Element>,
},
SystemBinary(u32),
Unit(UnitOp),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum UnitOp {
Times,
Divide,
Power,
Prefix,
End,
}
impl Element {
fn token(&self, t: &mut Text, s: &Settings) {
match self {
Element::Object(o) | Element::Symbolic { object: o, .. } => write_object(t, o, s),
Element::Command {
name,
address,
xlib,
..
} => t.push(&command_text(name.as_deref(), *address, *xlib)),
Element::SystemBinary(v) => t.push(&format!("<{v:X}h>")),
Element::Unit(_) => t.push("External"),
}
}
fn is_quote(&self) -> bool {
matches!(self, Element::Command { name: Some(n), .. } if n == "'")
}
}
pub(crate) fn program(elements: &[Element], s: &Settings) -> String {
let mut t = Text::default();
let mut first = true;
let mut open = false;
let mut glue = false;
for e in elements {
if t.full() {
break;
}
if matches!(e, Element::Command { silent: true, .. }) {
continue;
}
if e.is_quote() && open {
t.push("'");
open = false;
continue;
}
if !first && !glue {
t.push(" ");
}
first = false;
glue = false;
if e.is_quote() {
t.push("'");
open = true;
glue = true;
} else {
e.token(&mut t, s);
}
}
t.finish()
}
#[derive(Clone, Debug)]
enum Node {
Atom(String),
Negative(String),
Infix {
op: String,
prec: u8,
left: usize,
right: usize,
spaced: bool,
},
Prefix { op: String, prec: u8, arg: usize },
Postfix { op: String, arg: usize },
Call {
name: String,
args: Vec<usize>,
equation: bool,
},
Where {
expr: usize,
pairs: Vec<(usize, usize)>,
},
Derivative { var: usize, expr: usize },
Concat { left: usize, right: usize },
}
const ATOM: u8 = 20;
const PREC_ROOT: u8 = 13;
const PREC_POWER: u8 = 12;
const PREC_NEG: u8 = 11;
const PREC_PRODUCT: u8 = 10;
const PREC_SUM: u8 = 9;
const PREC_COMPARE: u8 = 7;
const PREC_NOT: u8 = 6;
const PREC_AND: u8 = 5;
const PREC_OR: u8 = 4;
const PREC_EQUATION: u8 = 3;
const PREC_WHERE: u8 = 2;
fn infix(name: &str) -> Option<(u8, bool)> {
Some(match name {
"^" => (PREC_POWER, false),
"*" | "/" => (PREC_PRODUCT, false),
"+" | "-" => (PREC_SUM, false),
"==" | "≠" | "<" | ">" | "≤" | "≥" => (PREC_COMPARE, false),
"AND" => (PREC_AND, true),
"OR" | "XOR" => (PREC_OR, true),
"=" => (PREC_EQUATION, false),
"|" => (PREC_WHERE, false),
_ => return None,
})
}
struct Tree {
nodes: Vec<Node>,
}
impl Tree {
fn add(&mut self, n: Node) -> usize {
self.nodes.push(n);
self.nodes.len() - 1
}
fn prec(&self, i: usize) -> u8 {
match &self.nodes[i] {
Node::Atom(_) | Node::Call { .. } | Node::Derivative { .. } | Node::Concat { .. } => {
ATOM
}
Node::Where { .. } => PREC_WHERE,
Node::Negative(_) => PREC_NEG,
Node::Infix { prec, .. } | Node::Prefix { prec, .. } => *prec,
Node::Postfix { .. } => ATOM - 1,
}
}
fn signed(&self, i: usize) -> bool {
matches!(self.nodes[i], Node::Prefix { .. } | Node::Negative(_))
}
fn render(&self, root: usize, t: &mut Text, all_parens: bool, power_right: bool) {
enum Work<'a> {
Node(usize),
Text(&'a str),
Owned(String),
}
let mut stack = vec![Work::Node(root)];
while let Some(w) = stack.pop() {
if t.full() {
return;
}
let i = match w {
Work::Text(s) => {
t.push(s);
continue;
}
Work::Owned(s) => {
t.push(&s);
continue;
}
Work::Node(i) => i,
};
let operand = |stack: &mut Vec<Work<'_>>, child: usize, parens: bool| {
let compound = !matches!(
self.nodes[child],
Node::Atom(_) | Node::Call { .. } | Node::Derivative { .. }
);
if parens || (all_parens && compound) {
stack.push(Work::Text(")"));
stack.push(Work::Node(child));
stack.push(Work::Text("("));
} else {
stack.push(Work::Node(child));
}
};
match &self.nodes[i] {
Node::Atom(s) | Node::Negative(s) => t.push(s),
Node::Infix {
op,
prec,
left,
right,
spaced,
} => {
let right_assoc = power_right && *prec == PREC_POWER;
let where_left = matches!(self.nodes[*left], Node::Where { .. });
let lp = !where_left
&& if right_assoc {
self.prec(*left) <= *prec
} else {
self.prec(*left) < *prec
};
let rp = if right_assoc {
self.prec(*right) < *prec
} else {
self.prec(*right) <= *prec
} && !self.signed(*right);
operand(&mut stack, *right, rp);
if *spaced {
stack.push(Work::Owned(format!(" {op} ")));
} else {
stack.push(Work::Owned(op.clone()));
}
operand(&mut stack, *left, lp);
}
Node::Prefix { op, prec, arg } => {
let p = self.prec(*arg) < *prec && !self.signed(*arg);
operand(&mut stack, *arg, p);
t.push(if p { op.trim_end() } else { op });
}
Node::Postfix { op, arg } => {
stack.push(Work::Owned(op.clone()));
let p = self.prec(*arg) < ATOM;
operand(&mut stack, *arg, p);
}
Node::Call {
name,
args,
equation,
} => {
stack.push(Work::Text(")"));
for (k, a) in args.iter().enumerate().rev() {
stack.push(Work::Node(*a));
if k == 1 && *equation {
stack.push(Work::Text("="));
} else if k > 0 {
stack.push(Work::Text(","));
}
}
t.push(name);
t.push("(");
}
Node::Where { expr, pairs } => {
stack.push(Work::Text(")"));
for (k, (n, v)) in pairs.iter().enumerate().rev() {
stack.push(Work::Node(*v));
stack.push(Work::Text("="));
stack.push(Work::Node(*n));
if k > 0 {
stack.push(Work::Text(","));
}
}
stack.push(Work::Text("|("));
let p = self.prec(*expr) <= PREC_WHERE;
operand(&mut stack, *expr, p);
}
Node::Derivative { var, expr } => {
stack.push(Work::Text(")"));
stack.push(Work::Node(*expr));
stack.push(Work::Text("("));
stack.push(Work::Node(*var));
t.push("∂");
}
Node::Concat { left, right } => {
stack.push(Work::Node(*right));
stack.push(Work::Node(*left));
}
}
}
}
}
#[derive(Clone, Copy, Debug)]
enum Slot {
Node(usize),
Count(u32),
}
pub(crate) fn algebraic(elements: &[Element], s: &Settings) -> Option<String> {
let s = &s.inner();
let mut tree = Tree { nodes: Vec::new() };
let root = expression(elements, s, &mut tree)?;
let mut t = Text::default();
t.push("'");
tree.render(root, &mut t, s.all_parens, s.power_right);
t.push("'");
Some(t.finish())
}
fn leaf(o: &Object, s: &Settings) -> Node {
let mut t = Text::default();
write_object(&mut t, o, s);
let text = t.finish();
let negative = match o {
Object::Real { value } => value.negative,
Object::Integer { value } => value.negative,
Object::Unit { value, .. } => value.negative,
_ => false,
};
if negative {
Node::Negative(text)
} else {
Node::Atom(text)
}
}
fn expression(elements: &[Element], s: &Settings, tree: &mut Tree) -> Option<usize> {
let mut stack: Vec<Slot> = Vec::new();
for e in elements {
let node = match e {
Element::Object(o) => tree.add(leaf(o, s)),
Element::Symbolic { elements, .. } => expression(elements, s, tree)?,
Element::SystemBinary(v) => {
stack.push(Slot::Count(*v));
continue;
}
Element::Unit(_) => return None,
Element::Command { name, arity, .. } => {
if let Some(Slot::Count(n)) = stack.last().copied() {
stack.pop();
counted(name.as_deref(), n, &mut stack, tree)?
} else {
apply(name.as_deref()?, *arity, &mut stack, tree)?
}
}
};
stack.push(Slot::Node(node));
}
match stack.as_slice() {
[Slot::Node(n)] => Some(*n),
_ => None,
}
}
fn counted(name: Option<&str>, n: u32, stack: &mut Vec<Slot>, tree: &mut Tree) -> Option<usize> {
let mut pop = || match stack.pop()? {
Slot::Node(n) => Some(n),
Slot::Count(_) => None,
};
match name {
Some("|") => {
let mut items = Vec::new();
for _ in 0..n {
items.push(pop()?);
}
items.reverse();
let (&expr, rest) = items.split_first()?;
if rest.len() % 2 != 0 {
return None;
}
let pairs = rest.chunks(2).map(|p| (p[0], p[1])).collect();
Some(tree.add(Node::Where { expr, pairs }))
}
None => {
let f = pop()?;
let mut args = Vec::new();
for _ in 0..n {
args.push(pop()?);
}
args.reverse();
let Node::Atom(name) = &tree.nodes[f] else {
return None;
};
let name = name.clone();
Some(tree.add(Node::Call {
name,
args,
equation: false,
}))
}
Some(_) => None,
}
}
fn apply(name: &str, arity: Option<u8>, stack: &mut Vec<Slot>, tree: &mut Tree) -> Option<usize> {
let mut pop = || match stack.pop()? {
Slot::Node(n) => Some(n),
Slot::Count(_) => None,
};
let arity = arity?;
if arity == 2
&& let Some((prec, spaced)) = infix(name)
{
let right = pop()?;
let left = pop()?;
return Some(tree.add(Node::Infix {
op: name.to_string(),
prec,
left,
right,
spaced,
}));
}
let unary = |op: &str, prec: u8, arg: usize, tree: &mut Tree| {
tree.add(Node::Prefix {
op: op.to_string(),
prec,
arg,
})
};
if arity == 1 {
match name {
"NEG" => return Some(unary("-", PREC_NEG, pop()?, tree)),
"NOT" => return Some(unary("NOT ", PREC_NOT, pop()?, tree)),
"√" => return Some(unary("√", PREC_ROOT, pop()?, tree)),
"!" => {
let arg = pop()?;
return Some(tree.add(Node::Postfix {
op: "!".into(),
arg,
}));
}
_ => {}
}
}
if name == "∂" && arity == 2 {
let expr = pop()?;
let var = pop()?;
return Some(tree.add(Node::Derivative { var, expr }));
}
let mut args = Vec::new();
for _ in 0..arity {
args.push(pop()?);
}
args.reverse();
if arity == 0 {
return Some(tree.add(Node::Atom(name.to_string())));
}
Some(tree.add(Node::Call {
name: name.to_string(),
args,
equation: name == "Σ" && arity == 4,
}))
}
pub(crate) fn unit(elements: &[Element], s: &Settings) -> Option<String> {
let mut tree = Tree { nodes: Vec::new() };
let mut stack: Vec<usize> = Vec::new();
let mut ended = false;
for e in elements {
if ended {
return None;
}
match e {
Element::Object(Object::String { value })
| Element::Object(Object::Character { value }) => {
stack.push(tree.add(Node::Atom(value.clone())));
}
Element::Object(Object::Real { value }) => {
stack.push(tree.add(Node::Atom(s.unit_number(value))));
}
Element::Object(o @ Object::Integer { .. }) => {
stack.push(tree.add(leaf(o, s)));
}
Element::Unit(op) => {
if *op == UnitOp::End {
ended = true;
continue;
}
let right = stack.pop()?;
let left = stack.pop()?;
let node = match op {
UnitOp::Prefix => Node::Concat { left, right },
UnitOp::Times | UnitOp::Divide | UnitOp::Power => {
let (op, prec) = match op {
UnitOp::Times => ("*", PREC_PRODUCT),
UnitOp::Divide => ("/", PREC_PRODUCT),
_ => ("^", PREC_POWER),
};
Node::Infix {
op: op.into(),
prec,
left,
right,
spaced: false,
}
}
UnitOp::End => return None,
};
stack.push(tree.add(node));
}
_ => return None,
}
}
let [root] = stack.as_slice() else {
return None;
};
let mut t = Text::default();
tree.render(*root, &mut t, false, false);
Some(t.finish())
}
#[cfg(test)]
mod tests {
use super::*;
fn real(s: &str) -> Real {
Real::parse(s).unwrap()
}
#[test]
fn edit_text_compiles_back() {
let fix = Settings {
format: NumberFormat::Fix(2),
base: Base::Hex,
word_size: 16,
tag_colon: false,
group_digits: true,
..Settings::standard(Model::Hp49g)
}
.for_editing();
let real = real_obj("1234.56789");
assert_eq!(edit_text(&real, &fix).unwrap(), "1234.56789");
let tagged = Object::Tagged {
tag: "T".into(),
object: Box::new(real.clone()),
};
assert_eq!(edit_text(&tagged, &fix).unwrap(), ":T: 1234.56789");
let bin = Object::Binary {
value: 0x12345,
base: None,
text: None,
};
assert_eq!(edit_text(&bin, &fix).unwrap(), "# 12345h");
let quoted = Object::List {
items: vec![Object::String {
value: "say \"hi\"".into(),
}],
};
assert!(
edit_text("ed, &fix)
.unwrap_err()
.to_string()
.contains('"')
);
let unknown = Object::Program { source: None };
assert!(edit_text(&unknown, &fix).is_err());
}
#[test]
fn described_carries_the_text_of_every_object() {
let name = |n: &str| Object::Name {
value: n.to_string(),
};
let hp49 = Settings::standard(Model::Hp49g);
let hp48 = Settings::standard(Model::Hp48gx);
let array = Object::Array {
dims: vec![1],
items: vec![ArrayItem::Item(Box::new(name("A")))],
};
let v = described(&array, &hp49).unwrap();
assert_eq!(v["text"], "[ 'A' ]");
assert_eq!(v["items"][0]["text"], "'A'");
let unit = Object::Unit {
value: real("2"),
unit: Some("m".to_string()),
};
assert_eq!(described(&unit, &hp49).unwrap()["text"], "2_m");
assert_eq!(text(&real_obj("2"), &hp49), Some("2.".to_string()));
assert_eq!(
described(&real_obj("1.23456789012E-5"), &hp48).unwrap()["text"],
"1.23456789012E-5"
);
let list = Object::List {
items: vec![
real_obj("1"),
name("QQ"),
Object::Tagged {
tag: "T".to_string(),
object: Box::new(real_obj("2")),
},
Object::Program { source: None },
],
};
let v = described(&list, &hp48).unwrap();
assert!(v.get("text").is_none(), "{v}");
assert_eq!(v["items"][1]["text"], "QQ");
assert_eq!(v["items"][2]["text"], ":T: 2");
assert_eq!(v["items"][2]["object"]["text"], "2");
assert!(v["items"][3].get("text").is_none());
assert_eq!(text(&name("QQ"), &hp48), Some("'QQ'".to_string()));
assert_eq!(text(&Object::Program { source: None }, &hp48), None);
}
fn real_obj(s: &str) -> Object {
Object::Real { value: real(s) }
}
#[test]
fn std_numbers_as_the_48_shows_them() {
let s = Settings::default();
for (v, text) in [
("1", "1"),
("-2", "-2"),
("1.5", "1.5"),
("1E-5", ".00001"),
(".001", ".001"),
("123456789012", "123456789012"),
("1E12", "1.E12"),
("1.5E300", "1.5E300"),
("-1.5E-300", "-1.5E-300"),
("1.23456", "1.23456"),
("0", "0"),
("100", "100"),
(".5", ".5"),
] {
assert_eq!(s.real(&real(v)), text, "{v}");
}
let p = Settings::standard(Model::Hp49g);
assert_eq!(p.real(&real("5")), "5.");
assert_eq!(p.real(&real("0")), "0.");
assert_eq!(p.real(&real("2.5")), "2.5");
}
#[test]
fn text_is_capped() {
let mut t = Text::default();
for _ in 0..MAX_TEXT {
t.push("ab");
}
let s = t.finish();
assert!(s.ends_with(TRUNCATED));
assert_eq!(s.chars().count(), MAX_TEXT + 1);
}
fn cmd(name: &str, arity: u8) -> Element {
Element::Command {
name: Some(name.into()),
address: None,
xlib: None,
arity: Some(arity),
silent: false,
}
}
fn name(n: &str) -> Element {
Element::Object(Object::Name { value: n.into() })
}
fn num(v: &str) -> Element {
Element::Object(Object::Real { value: real(v) })
}
#[test]
fn infix_with_precedence() {
let s = Settings::default();
let alg = |e: &[Element]| algebraic(e, &s).unwrap();
assert_eq!(alg(&[name("A"), num("1"), cmd("+", 2)]), "'A+1'");
assert_eq!(
alg(&[name("A"), name("B"), cmd("+", 2), name("C"), cmd("*", 2)]),
"'(A+B)*C'"
);
assert_eq!(
alg(&[name("A"), name("B"), name("C"), cmd("*", 2), cmd("+", 2)]),
"'A+B*C'"
);
assert_eq!(
alg(&[name("X"), cmd("SIN", 1), num("2"), cmd("^", 2)]),
"'SIN(X)^2'"
);
assert_eq!(alg(&[name("A"), cmd("NEG", 1)]), "'-A'");
assert_eq!(alg(&[name("A"), name("B"), cmd("MAX", 2)]), "'MAX(A,B)'");
assert_eq!(algebraic(&[name("A"), name("B")], &s), None);
assert_eq!(
algebraic(
&[
name("A"),
Element::Command {
name: None,
address: None,
xlib: None,
arity: None,
silent: false,
}
],
&s
),
None
);
}
#[test]
fn deep_expressions_render_without_recursion() {
let s = Settings::default();
let mut e = vec![name("A")];
for _ in 0..200_000 {
e.push(name("B"));
e.push(cmd("-", 2));
}
let text = algebraic(&e, &s).unwrap();
assert!(text.starts_with("'A-B-B"));
assert!(text.ends_with(TRUNCATED));
}
}