1use anyhow::{Context, Result, bail};
11use saturnus::Model;
12
13use crate::charset;
14use crate::object::{ArrayItem, Memory, Object, decode};
15use crate::prolog::{ObjectType, SEMI};
16
17pub const HEADER_LEN: usize = 8;
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub enum Family {
23 Hp48,
25 Hp49,
27}
28
29impl Family {
30 pub fn of(model: Model) -> Family {
32 if model == Model::Hp49g {
33 Family::Hp49
34 } else {
35 Family::Hp48
36 }
37 }
38}
39
40pub fn binary_header(family: Family) -> [u8; HEADER_LEN] {
43 let digit = match family {
44 Family::Hp48 => b'8',
45 Family::Hp49 => b'9',
46 };
47 [b'H', b'P', b'H', b'P', b'4', digit, b'-', b'X']
48}
49
50pub fn is_binary_file(data: &[u8]) -> bool {
53 data.get(..HEADER_LEN).is_some_and(|h| {
54 (h.starts_with(b"HPHP48-") || h.starts_with(b"HPHP49-")) && h[7].is_ascii_graphic()
55 })
56}
57
58pub fn unpack(bytes: &[u8]) -> Vec<u8> {
60 bytes.iter().flat_map(|b| [b & 0x0F, b >> 4]).collect()
61}
62
63pub fn pack(nibbles: &[u8]) -> Vec<u8> {
66 nibbles
67 .chunks(2)
68 .map(|c| (c[0] & 0x0F) | (c.get(1).copied().unwrap_or(0) & 0x0F) << 4)
69 .collect()
70}
71
72pub fn ascii_body(text: &str) -> &str {
75 let Some(rest) = text.strip_prefix("%%HP:") else {
76 return text;
77 };
78 let Some(end) = rest.find(';') else {
79 return text;
80 };
81 let fields = rest[..end]
82 .chars()
83 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '(' | ')' | ' ' | '\t' | '.' | ','));
84 if !fields {
85 return text;
86 }
87 let rest = &rest[end + 1..];
88 rest.strip_prefix("\r\n")
89 .or_else(|| rest.strip_prefix('\n'))
90 .unwrap_or(rest)
91}
92
93pub fn is_plain_name(name: &str) -> bool {
99 let Some(first) = name.chars().next() else {
100 return false;
101 };
102 name.chars().count() <= 127
103 && !first.is_ascii_digit()
104 && first != '.'
105 && !name.chars().any(|c| {
106 c.is_whitespace()
107 || c.is_control()
108 || matches!(
109 c,
110 '\'' | '"'
111 | '«'
112 | '»'
113 | '{'
114 | '}'
115 | '['
116 | ']'
117 | '('
118 | ')'
119 | '#'
120 | ':'
121 | ','
122 | ';'
123 | '+'
124 | '-'
125 | '*'
126 | '/'
127 | '^'
128 | '='
129 | '<'
130 | '>'
131 | '\\'
132 )
133 })
134 && charset::encode(name).is_ok()
135}
136
137pub fn decode_file(data: &[u8], mem: &dyn Memory) -> Result<Object> {
139 if !is_binary_file(data) {
140 bail!("not an HP binary object (no HPHP48-x / HPHP49-x header)");
141 }
142 let nibbles = unpack(&data[HEADER_LEN..]);
143 decode(&nibbles, mem)
144}
145
146pub fn fill_sources(obj: &mut Object, text: &str) {
152 let body = ascii_body(text);
153 let body = body.replace("\r\n", "\n");
154 let mut filled = obj.clone();
155 let mut scan = Scanner { s: &body, pos: 0 };
156 let ok = scan.fill(&mut filled).is_some() && scan.rest().trim().is_empty();
157 if ok {
158 *obj = filled;
159 return;
160 }
161 let whole = Some(body.trim().to_string());
162 match obj {
163 Object::Program { source }
164 | Object::Algebraic { source }
165 | Object::Command { source, .. }
166 | Object::Unknown { source, .. } => *source = whole,
167 _ => {}
168 }
169}
170
171struct Scanner<'a> {
173 s: &'a str,
174 pos: usize,
175}
176
177impl<'a> Scanner<'a> {
178 fn rest(&self) -> &'a str {
179 &self.s[self.pos..]
180 }
181
182 fn skip_ws(&mut self) {
183 let r = self.rest();
184 self.pos += r.len() - r.trim_start().len();
185 }
186
187 fn peek(&self) -> Option<char> {
188 self.rest().chars().next()
189 }
190
191 fn expect(&mut self, lit: &str) -> Option<()> {
192 self.skip_ws();
193 if self.rest().starts_with(lit) {
194 self.pos += lit.len();
195 Some(())
196 } else {
197 None
198 }
199 }
200
201 fn token(&mut self) -> Option<&'a str> {
203 self.skip_ws();
204 let r = self.rest();
205 let end = r.find(char::is_whitespace).unwrap_or(r.len());
206 if end == 0 {
207 return None;
208 }
209 self.pos += end;
210 Some(&r[..end])
211 }
212
213 fn balanced(&mut self, open: char, close: char) -> Option<&'a str> {
215 self.skip_ws();
216 let r = self.rest();
217 if !r.starts_with(open) {
218 return None;
219 }
220 let mut depth = 0usize;
221 let mut in_string = false;
222 let mut escaped = false;
223 for (i, c) in r.char_indices() {
224 if in_string {
225 match c {
226 _ if escaped => escaped = false,
227 '\\' => escaped = true,
228 '"' => in_string = false,
229 _ => {}
230 }
231 continue;
232 }
233 if c == '"' {
234 in_string = true;
235 } else if c == open && (open != close || depth == 0) {
236 depth += 1;
237 } else if c == close {
238 depth -= 1;
239 if depth == 0 {
240 let end = i + c.len_utf8();
241 self.pos += end;
242 return Some(&r[..end]);
243 }
244 }
245 }
246 None
247 }
248
249 fn string(&mut self) -> Option<&'a str> {
251 self.skip_ws();
252 let r = self.rest();
253 if !r.starts_with('"') {
254 return None;
255 }
256 let mut escaped = false;
257 for (i, c) in r.char_indices().skip(1) {
258 match c {
259 _ if escaped => escaped = false,
260 '\\' => escaped = true,
261 '"' => {
262 self.pos += i + 1;
263 return Some(&r[..=i]);
264 }
265 _ => {}
266 }
267 }
268 None
269 }
270
271 fn quoted(&mut self) -> Option<&'a str> {
273 self.skip_ws();
274 let r = self.rest();
275 let inner = r.strip_prefix('\'')?;
276 let end = inner.find('\'')? + 2;
277 self.pos += end;
278 Some(&r[..end])
279 }
280
281 fn any(&mut self) -> Option<&'a str> {
283 self.skip_ws();
284 match self.peek()? {
285 '{' => self.balanced('{', '}'),
286 '[' => self.balanced('[', ']'),
287 '«' => self.balanced('«', '»'),
288 '(' => self.balanced('(', ')'),
289 '"' => self.string(),
290 '\'' => self.quoted(),
291 _ => self.token(),
292 }
293 }
294
295 fn fill(&mut self, obj: &mut Object) -> Option<()> {
297 self.skip_ws();
298 match obj {
299 Object::Real { .. } | Object::Integer { .. } => {
300 self.token()?;
301 }
302 Object::Name { .. } | Object::LocalName { .. } => {
303 if self.peek() == Some('\'') {
304 self.quoted()?;
305 } else {
306 self.token()?;
307 }
308 }
309 Object::Character { .. } => {
310 self.any()?;
311 }
312 Object::Binary { .. } => {
313 if self.token()? == "#" {
314 self.token()?;
315 }
316 }
317 Object::String { .. } => {
318 self.string()?;
319 }
320 Object::Complex { .. } => {
321 self.balanced('(', ')')?;
322 }
323 Object::Array { .. } => {
324 self.balanced('[', ']')?;
325 }
326 Object::List { items } => {
327 self.expect("{")?;
328 for item in items.iter_mut() {
329 self.fill(item)?;
330 }
331 self.expect("}")?;
332 }
333 Object::Tagged { tag, object } => {
334 self.expect(&format!(":{tag}:"))?;
335 self.fill(object)?;
336 }
337 Object::Program { source } => {
338 *source = Some(self.balanced('«', '»')?.to_string());
339 }
340 Object::Algebraic { source } => {
341 *source = Some(self.quoted()?.to_string());
342 }
343 Object::Unit { unit, .. } => {
344 let t = if self.peek() == Some('\'') {
345 let q = self.quoted()?;
346 &q[1..q.len() - 1]
347 } else {
348 self.token()?
349 };
350 *unit = Some(t.split_once('_')?.1.to_string());
351 }
352 Object::Command {
354 name: None,
355 address: None,
356 library: Some(_),
357 source,
358 ..
359 } if self.rest().trim_start().starts_with("XLIB ") => {
360 let start = self.pos;
361 for _ in 0..3 {
362 self.token()?;
363 }
364 *source = Some(self.s[start..self.pos].trim().to_string());
365 }
366 Object::Command { source, .. } | Object::Unknown { source, .. } => {
367 *source = Some(self.any()?.to_string());
368 }
369 }
370 Some(())
371 }
372}
373
374fn put_field(out: &mut Vec<u8>, value: u64, width: usize) {
376 out.extend((0..width).map(|i| ((value >> (4 * i)) & 0xF) as u8));
377}
378
379fn put_chars(out: &mut Vec<u8>, text: &str) -> Result<usize> {
380 let bytes = charset::encode(text)
381 .map_err(|c| anyhow::anyhow!("{c:?} is not in the HP character set"))?;
382 for b in &bytes {
383 put_field(out, u64::from(*b), 2);
384 }
385 Ok(bytes.len())
386}
387
388pub fn encode_file(obj: &Object, family: Family) -> Result<Option<Vec<u8>>> {
392 let mut nibbles = Vec::new();
393 if !encode_into(obj, family, &mut nibbles)? {
394 return Ok(None);
395 }
396 let mut data = binary_header(family).to_vec();
397 data.extend(pack(&nibbles));
398 Ok(Some(data))
399}
400
401fn prolog(out: &mut Vec<u8>, ty: ObjectType) {
402 put_field(out, u64::from(ty.prolog()), 5);
403}
404
405fn encode_into(obj: &Object, family: Family, out: &mut Vec<u8>) -> Result<bool> {
406 match obj {
407 Object::Real { value } => {
408 prolog(out, ObjectType::Real);
409 out.extend(value.to_body());
410 }
411 Object::Complex { re, im } => {
412 prolog(out, ObjectType::Complex);
413 out.extend(re.to_body());
414 out.extend(im.to_body());
415 }
416 Object::String { value } => {
417 prolog(out, ObjectType::String);
418 let mut body = Vec::new();
419 let n = put_chars(&mut body, value)?;
420 put_field(out, 5 + 2 * n as u64, 5);
421 out.extend(body);
422 }
423 Object::Name { value } | Object::LocalName { value } => {
424 let ty = if matches!(obj, Object::Name { .. }) {
425 ObjectType::GlobalName
426 } else {
427 ObjectType::LocalName
428 };
429 prolog(out, ty);
430 let mut body = Vec::new();
431 let n = put_chars(&mut body, value)?;
432 if n == 0 || n > 255 {
433 bail!("a name has 1 to 255 characters: {value:?}");
434 }
435 put_field(out, n as u64, 2);
436 out.extend(body);
437 }
438 Object::Character { value } => {
439 prolog(out, ObjectType::Character);
440 if put_chars(out, value)? != 1 {
441 bail!("a character object holds one character: {value:?}");
442 }
443 }
444 Object::Binary { value, .. } => {
445 prolog(out, ObjectType::BinaryInteger);
446 put_field(out, 21, 5);
447 put_field(out, *value, 16);
448 }
449 Object::Integer { value } => {
450 if family != Family::Hp49 {
451 bail!("exact integers exist on the 49G only; send a real instead");
452 }
453 prolog(out, ObjectType::Integer);
454 let n = value.to_nibbles();
455 put_field(out, 5 + n.len() as u64, 5);
456 out.extend(n);
457 }
458 Object::List { items } => {
459 prolog(out, ObjectType::List);
460 for item in items {
461 if !encode_into(item, family, out)? {
462 return Ok(false);
463 }
464 }
465 put_field(out, u64::from(SEMI), 5);
466 }
467 Object::Tagged { tag, object } => {
468 prolog(out, ObjectType::Tagged);
469 let mut body = Vec::new();
470 let n = put_chars(&mut body, tag)?;
471 if n > 255 {
472 bail!("tag longer than 255 characters");
473 }
474 put_field(out, n as u64, 2);
475 out.extend(body);
476 return encode_into(object, family, out);
477 }
478 Object::Unknown { hex, truncated, .. } => {
479 if *truncated {
480 return Ok(false);
481 }
482 for c in hex.chars() {
483 let d = c
484 .to_digit(16)
485 .with_context(|| format!("hex holds {c:?}, not a hex digit"))?;
486 out.push(d as u8);
487 }
488 }
489 Object::Command {
491 address: None,
492 library: Some(lib),
493 command: Some(cmd),
494 ..
495 } => {
496 prolog(out, ObjectType::XlibName);
497 put_field(out, u64::from(*lib), 3);
498 put_field(out, u64::from(*cmd), 3);
499 }
500 Object::Unit { .. }
501 | Object::Array { .. }
502 | Object::Program { .. }
503 | Object::Algebraic { .. }
504 | Object::Command { .. } => return Ok(false),
505 }
506 Ok(true)
507}
508
509pub fn to_source(obj: &Object, family: Family) -> Result<Option<String>> {
512 Ok(match obj {
513 Object::Real { value } => Some(value.to_source()),
514 Object::Integer { value } => {
515 if family != Family::Hp49 {
516 bail!("exact integers exist on the 49G only; send a real instead");
517 }
518 Some(value.to_source())
519 }
520 Object::Complex { re, im } => Some(format!("({},{})", re.to_source(), im.to_source())),
521 Object::String { value } => (!value.contains(['"', '\\'])).then(|| format!("\"{value}\"")),
522 Object::Name { value } => is_plain_name(value).then(|| format!("'{value}'")),
525 Object::Binary { value, .. } => Some(format!("#{value:X}h")),
526 Object::List { items } => {
527 let mut parts = Vec::new();
528 for item in items {
529 match to_source(item, family)? {
530 Some(s) => parts.push(s),
531 None => return Ok(None),
532 }
533 }
534 Some(format!("{{ {} }}", parts.join(" ")))
535 }
536 Object::Tagged { tag, object } => {
537 if !plain_token(tag, false) {
538 return Ok(None);
539 }
540 to_source(object, family)?.map(|s| format!(":{tag}:{s}"))
541 }
542 Object::Unit { value, unit } => {
543 let unit = unit.as_deref().context("a unit object needs its unit")?;
544 if !plain_token(unit, true) || unit.contains('_') {
545 bail!(
546 "not a unit expression (spaces and RPL delimiters are not allowed): {unit:?}"
547 );
548 }
549 Some(format!("{}_{unit}", value.to_source()))
550 }
551 Object::Array { items, .. } => array_source(items, family)?,
552 Object::Program { source } | Object::Algebraic { source } => {
553 let source = source.as_deref().context("this object needs its source")?;
554 Some(one_object_source(obj, source)?)
555 }
556 Object::Command {
559 name: None,
560 address: None,
561 library: Some(_),
562 command: Some(_),
563 ..
564 } => None,
565 Object::Command { name, source, .. } => {
566 let source = source
567 .as_deref()
568 .or(name.as_deref())
569 .context("this object needs its source")?;
570 Some(one_object_source(obj, source)?)
571 }
572 Object::LocalName { .. } | Object::Character { .. } | Object::Unknown { .. } => None,
573 })
574}
575
576fn plain_token(s: &str, parens: bool) -> bool {
580 !s.is_empty()
581 && s.chars().all(|c| {
582 !c.is_whitespace()
583 && !c.is_control()
584 && !matches!(
585 c,
586 '\'' | '"' | '«' | '»' | '{' | '}' | '[' | ']' | ':' | '#' | ',' | ';' | '\\'
587 )
588 && (parens || !matches!(c, '(' | ')'))
589 })
590}
591
592fn one_object_source(obj: &Object, source: &str) -> Result<String> {
597 let text = source.trim().replace("\\<<", "«").replace("\\>>", "»");
599 let mut scan = Scanner { s: &text, pos: 0 };
600 let ok = match obj {
601 Object::Program { .. } => scan.balanced('«', '»').is_some(),
602 Object::Algebraic { .. } => scan.quoted().is_some_and(|q| q.len() > 2),
603 _ => plain_token(&text, true) && scan.token().is_some(),
604 };
605 if !ok || !scan.rest().trim().is_empty() {
606 let what = match obj {
607 Object::Program { .. } => "a program's source must be one « ... » group",
608 Object::Algebraic { .. } => "an algebraic's source must be one '...' group",
609 _ => "a command's source must be one token",
610 };
611 bail!("{what} with nothing after it: {source:?}");
612 }
613 Ok(source.trim().to_string())
614}
615
616fn array_source(items: &[ArrayItem], family: Family) -> Result<Option<String>> {
617 let mut parts = Vec::new();
618 for item in items {
619 let s = match item {
620 ArrayItem::Row(row) => array_source(row, family)?,
621 ArrayItem::Item(obj) => {
622 if !matches!(**obj, Object::Real { .. } | Object::Complex { .. }) {
623 bail!("array elements must be reals or complex numbers");
624 }
625 to_source(obj, family)?
626 }
627 };
628 match s {
629 Some(s) => parts.push(s),
630 None => return Ok(None),
631 }
632 }
633 Ok(Some(format!("[ {} ]", parts.join(" "))))
634}
635
636#[derive(Clone, Debug, Default, PartialEq)]
639pub struct StackReply {
640 pub error: Option<String>,
642 pub levels: Vec<String>,
644}
645
646impl StackReply {
647 pub fn level(&self, n: usize) -> Option<&str> {
649 n.checked_sub(1)
650 .and_then(|i| self.levels.get(i))
651 .map(String::as_str)
652 }
653}
654
655fn level_prefix(line: &str) -> Option<(usize, &str)> {
657 let (num, rest) = line.split_once(':')?;
658 if num.is_empty() || !num.bytes().all(|b| b.is_ascii_digit()) {
659 return None;
660 }
661 let n = num.parse().ok()?;
662 (n >= 1).then_some((n, rest))
663}
664
665pub fn parse_stack(text: &str) -> StackReply {
670 let text = text.trim_end_matches(['\r', '\n']);
671 let mut reply = StackReply::default();
672 let mut values: Vec<String> = Vec::new();
674 let mut expected = 0usize;
676 for (i, line) in text.split('\n').enumerate() {
677 let line = line.strip_suffix('\r').unwrap_or(line);
678 if i == 0
679 && let Some(msg) = line.strip_prefix("Error:")
680 {
681 reply.error = Some(msg.trim().to_string());
682 continue;
683 }
684 if values.is_empty() {
685 if line.trim() == "Empty Stack" {
686 break;
687 }
688 if let Some((n, rest)) = level_prefix(line) {
689 values.push(rest.trim_start_matches(' ').to_string());
690 expected = n - 1;
691 }
692 continue;
693 }
694 if expected >= 1
695 && let Some(rest) = line.strip_prefix(format!("{expected}:").as_str())
696 {
697 values.push(rest.trim_start_matches(' ').to_string());
698 expected -= 1;
699 continue;
700 }
701 if let Some(last) = values.last_mut() {
702 last.push('\n');
703 last.push_str(line);
704 }
705 }
706 values.reverse();
707 reply.levels = values;
708 reply
709}
710
711#[cfg(test)]
712mod tests {
713 use super::*;
714
715 #[test]
716 fn stacks() {
717 let r = parse_stack("2: 42\r\n1: \"a\r\nb\"\r\n");
718 assert_eq!(r.levels, ["\"a\nb\"", "42"]);
719 assert_eq!(r.error, None);
720 assert_eq!(r.level(2), Some("42"));
721 assert_eq!(r.level(0), None);
722 let r = parse_stack("Error: Too Few Arguments\r\nEmpty Stack\r\n");
723 assert_eq!(r.error.as_deref(), Some("Too Few Arguments"));
724 assert!(r.levels.is_empty());
725 let r = parse_stack("1: { 1:2 }\n1: x");
727 assert_eq!(r.levels, ["{ 1:2 }\n1: x"]);
728 }
729 use crate::object::{Integer, NoMemory, Real};
730 use serde_json::json;
731
732 #[test]
733 fn headers_and_packing() {
734 assert_eq!(&binary_header(Family::Hp49), b"HPHP49-X");
735 assert!(is_binary_file(b"HPHP48-J\x01"));
736 assert!(!is_binary_file(b"HPHP47-J") && !is_binary_file(b"HPHP48-"));
737 assert_eq!(Family::of(Model::Hp49g), Family::Hp49);
738 assert_eq!(Family::of(Model::Hp48sx), Family::Hp48);
739 assert_eq!(pack(&[1, 2, 3]), [0x21, 0x03]);
740 assert_eq!(unpack(&[0x21, 0x03]), [1, 2, 3, 0]);
741 assert_eq!(ascii_body("%%HP: T(1)A(D)F(.);\r\n{ 1 }\r\n"), "{ 1 }\r\n");
742 assert_eq!(ascii_body("%%HP: T(3)A(R)F(,);\n« »"), "« »");
743 assert_eq!(ascii_body("{ 1 }"), "{ 1 }");
744 }
745
746 #[test]
747 fn plain_names() {
748 assert!(is_plain_name("X") && is_plain_name("ΣDAT") && is_plain_name("A.1"));
749 for bad in [
750 "",
751 "A\\GS",
752 "1A",
753 ".A",
754 "A B",
755 "X'",
756 "A+B",
757 "a:b",
758 "€",
759 &"A".repeat(128),
760 ] {
761 assert!(!is_plain_name(bad), "{bad:?}");
762 }
763 }
764
765 fn nib(s: &str) -> Vec<u8> {
767 s.chars().map(|c| c.to_digit(16).unwrap() as u8).collect()
768 }
769
770 fn dec(s: &str) -> Object {
771 decode(&nib(s), &NoMemory).unwrap()
772 }
773
774 fn real(s: &str) -> Real {
775 Real::parse(s).unwrap()
776 }
777
778 struct Rom;
780 impl Memory for Rom {
781 fn nibble(&self, addr: u32) -> Option<u8> {
782 let i = addr.checked_sub(0x2A2C9)? as usize;
783 nib("339200000000000000010").get(i).copied()
784 }
785 }
786
787 #[test]
791 fn unresolved_xlib_names_round_trip() {
792 let file = {
794 let mut d = binary_header(Family::Hp48).to_vec();
795 d.extend(pack(&nib("47A2029E202D4500B2130")));
796 d
797 };
798 let obj = decode_file(&file, &NoMemory).unwrap();
799 let Object::List { items } = &obj else {
800 panic!("{obj:?}")
801 };
802 assert_eq!(
803 serde_json::to_value(&items[0]).unwrap(),
804 json!({"type": "command", "library": 1234, "command": 5})
805 );
806 assert!(obj.needs_source());
807 assert_eq!(to_source(&obj, Family::Hp48).unwrap(), None);
808 assert_eq!(encode_file(&obj, Family::Hp48).unwrap().unwrap(), file);
809 let mut filled = obj.clone();
811 fill_sources(&mut filled, "{ XLIB 1234 5 }");
812 let Object::List { items } = &filled else {
813 panic!()
814 };
815 assert!(matches!(&items[0], Object::Command { source: Some(s), .. } if s == "XLIB 1234 5"));
816 assert_eq!(encode_file(&filled, Family::Hp48).unwrap().unwrap(), file);
817 }
818
819 #[test]
820 fn sources_come_from_the_ascii_text() {
821 let mut p = dec("D9D20E16329C2A2ED2A276BA193632B21300");
823 assert!(p.needs_source());
824 fill_sources(&mut p, "%%HP: T(1)A(D)F(.);\r\n« 1 2 +\r\n»\r\n");
825 assert_eq!(
826 p,
827 Object::Program {
828 source: Some("« 1 2 +\n»".into())
829 }
830 );
831 let mut a = dec("8BA2084E201085ED2A2D20B1B21300");
832 fill_sources(&mut a, "%%HP: T(1)A(D)F(.);\r\n'X^2'\r\n");
833 assert_eq!(
834 a,
835 Object::Algebraic {
836 source: Some("'X^2'".into())
837 }
838 );
839 let unit = "ADA209C2A2C2A2070000D6C2A207000037ED2A227B0186B0168B01B21300";
842 assert!(matches!(dec(unit), Object::Unknown { .. }));
843 let mut u = decode(&nib(unit), &Rom).unwrap();
844 assert_eq!(
845 u,
846 Object::Unit {
847 value: real("1"),
848 unit: None
849 }
850 );
851 fill_sources(&mut u, "%%HP: T(1)A(D)F(.);\r\n'1_m/s^2'\r\n");
852 assert_eq!(
853 u,
854 Object::Unit {
855 value: real("1"),
856 unit: Some("m/s^2".into())
857 }
858 );
859 }
860
861 #[test]
862 fn nested_sources_follow_the_tree() {
863 let mut list = Object::List {
864 items: vec![
865 Object::Real { value: real("1") },
866 Object::Program { source: None },
867 Object::String {
868 value: "a » b".into(),
869 },
870 Object::Tagged {
871 tag: "T".into(),
872 object: Box::new(Object::Command {
873 name: None,
874 address: None,
875 library: None,
876 command: None,
877 source: None,
878 }),
879 },
880 Object::Binary {
881 value: 42,
882 base: None,
883 text: None,
884 },
885 Object::List {
886 items: vec![Object::Algebraic { source: None }],
887 },
888 Object::Complex {
889 re: real("1"),
890 im: real("2"),
891 },
892 ],
893 };
894 fill_sources(
895 &mut list,
896 "{ 1 « 2 \"x»\" + » \"a » b\" :T: + # 42d { 'X+1' } (1,2) }",
897 );
898 let Object::List { items } = &list else {
899 panic!()
900 };
901 assert_eq!(
902 items[1],
903 Object::Program {
904 source: Some("« 2 \"x»\" + »".into())
905 }
906 );
907 assert_eq!(
908 items[3],
909 Object::Tagged {
910 tag: "T".into(),
911 object: Box::new(Object::Command {
912 name: None,
913 address: None,
914 library: None,
915 command: None,
916 source: Some("+".into())
917 })
918 }
919 );
920 assert_eq!(
921 items[5],
922 Object::List {
923 items: vec![Object::Algebraic {
924 source: Some("'X+1'".into())
925 }]
926 }
927 );
928 let mut l = Object::List {
930 items: vec![Object::Program { source: None }],
931 };
932 fill_sources(&mut l, "{ 1 2 }");
933 assert_eq!(
934 l,
935 Object::List {
936 items: vec![Object::Program { source: None }]
937 }
938 );
939 let mut p = Object::Program { source: None };
941 fill_sources(&mut p, "« garbled");
942 assert_eq!(
943 p,
944 Object::Program {
945 source: Some("« garbled".into())
946 }
947 );
948 }
949
950 #[test]
951 fn encode_round_trips_through_decode() {
952 let objs = [
953 Object::Real {
954 value: real("-4.79425538604E-1"),
955 },
956 Object::Complex {
957 re: real("1.5"),
958 im: real("-2"),
959 },
960 Object::String {
961 value: "Hi «x» \"q\"".into(),
962 },
963 Object::Name {
964 value: "ABC".into(),
965 },
966 Object::LocalName { value: "x".into() },
967 Object::Character { value: "A".into() },
968 Object::Binary {
969 value: u64::MAX,
970 base: None,
971 text: None,
972 },
973 Object::List {
974 items: vec![
975 Object::Real { value: real("1") },
976 Object::List { items: vec![] },
977 Object::Tagged {
978 tag: "T".into(),
979 object: Box::new(Object::Real { value: real("5") }),
980 },
981 ],
982 },
983 ];
984 for o in &objs {
985 let file = encode_file(o, Family::Hp48).unwrap().unwrap();
986 assert_eq!(&file[..8], b"HPHP48-X");
987 assert_eq!(decode_file(&file, &NoMemory).unwrap(), *o, "{o:?}");
988 let size = crate::prolog::object_size(&unpack(&file[8..]), 0).unwrap();
990 assert_eq!(file.len(), 8 + size.div_ceil(2));
991 }
992 let file = encode_file(
994 &Object::Real {
995 value: real("8.72653549837E-3"),
996 },
997 Family::Hp48,
998 )
999 .unwrap()
1000 .unwrap();
1001 assert_eq!(unpack(&file[8..])[..21], nib("339207997389453562780")[..]);
1002 let i = Object::Integer {
1003 value: Integer::parse("-1234567890123456789").unwrap(),
1004 };
1005 let file = encode_file(&i, Family::Hp49).unwrap().unwrap();
1006 assert_eq!(
1007 unpack(&file[8..])[..30],
1008 nib("416209100098765432109876543219")[..]
1009 );
1010 assert!(encode_file(&i, Family::Hp48).is_err());
1011 assert_eq!(
1012 encode_file(
1013 &Object::Program {
1014 source: Some("« »".into())
1015 },
1016 Family::Hp48
1017 )
1018 .unwrap(),
1019 None
1020 );
1021 let u = dec("E1B20A0000FFFFF");
1023 assert!(matches!(u, Object::Unknown { .. }));
1024 assert_eq!(
1025 encode_file(&u, Family::Hp48).unwrap().unwrap()[8..],
1026 pack(&nib("E1B20A0000FFFFF"))[..]
1027 );
1028 }
1029
1030 #[test]
1031 fn sources_for_text_transfers() {
1032 let src = |o: Object| to_source(&o, Family::Hp48).unwrap();
1033 assert_eq!(src(Object::Real { value: real("0.5") }).unwrap(), "5.E-1");
1034 assert_eq!(
1035 src(Object::Complex {
1036 re: real("1"),
1037 im: real("-2")
1038 })
1039 .unwrap(),
1040 "(1.,-2.)"
1041 );
1042 assert_eq!(
1043 src(Object::String {
1044 value: "a b".into()
1045 })
1046 .unwrap(),
1047 "\"a b\""
1048 );
1049 assert_eq!(
1050 src(Object::String {
1051 value: "a\"b".into()
1052 }),
1053 None
1054 );
1055 assert_eq!(
1056 src(Object::Binary {
1057 value: 255,
1058 base: None,
1059 text: None
1060 })
1061 .unwrap(),
1062 "#FFh"
1063 );
1064 assert_eq!(
1065 src(Object::List {
1066 items: vec![
1067 Object::Name { value: "X".into() },
1068 Object::Unit {
1069 value: real("9.81"),
1070 unit: Some("m/s^2".into())
1071 }
1072 ]
1073 })
1074 .unwrap(),
1075 "{ 'X' 9.81_m/s^2 }"
1076 );
1077 assert_eq!(src(Object::LocalName { value: "x".into() }), None);
1078 assert!(to_source(&Object::Program { source: None }, Family::Hp48).is_err());
1079 }
1080
1081 #[test]
1082 fn text_cannot_break_out_of_its_quotes() {
1083 let src = |o: Object| to_source(&o, Family::Hp48);
1084 assert_eq!(
1086 src(Object::Name {
1087 value: "X' 11. 'QA' STO 'Y".into()
1088 })
1089 .unwrap(),
1090 None
1091 );
1092 let odd = Object::Tagged {
1093 tag: "a:b".into(),
1094 object: Box::new(Object::Real { value: Real::ZERO }),
1095 };
1096 assert_eq!(src(odd.clone()).unwrap(), None);
1097 assert!(encode_file(&odd, Family::Hp48).unwrap().is_some());
1098 let spaced = Object::Tagged {
1099 tag: "my tag".into(),
1100 object: Box::new(Object::Real { value: Real::ZERO }),
1101 };
1102 assert_eq!(src(spaced).unwrap(), None);
1103 let p = |s: &str| Object::Program {
1105 source: Some(s.into()),
1106 };
1107 assert!(src(p("« 1 » 22. 'QB' STO « 2 »")).is_err());
1108 assert!(src(p("1 2 +")).is_err());
1109 assert!(src(p("« 1 \"»\" »")).is_ok());
1110 assert!(src(p("\\<< 1 \\>>")).is_ok());
1111 assert!(src(p("« « 1 » »")).is_ok());
1112 let a = |s: &str| Object::Algebraic {
1113 source: Some(s.into()),
1114 };
1115 assert!(src(a("'X' 'Y' 44. 'QD' STO 'Z'")).is_err());
1116 assert!(src(a("X+1")).is_err());
1117 assert!(src(a("''")).is_err());
1118 assert_eq!(src(a("'X^2+1'")).unwrap().unwrap(), "'X^2+1'");
1119 let u = |s: &str| Object::Unit {
1120 value: Real::ZERO,
1121 unit: Some(s.into()),
1122 };
1123 assert!(src(u("m 33. 'QC' STO")).is_err());
1124 assert!(src(u("m'")).is_err());
1125 assert!(src(u("(m/s)^2")).is_ok());
1126 let c = |s: &str| Object::Command {
1127 name: None,
1128 address: None,
1129 library: None,
1130 command: None,
1131 source: Some(s.into()),
1132 };
1133 assert!(src(c("+ 1. 'QE' STO")).is_err());
1134 assert_eq!(src(c("\u{2192}LIST")).unwrap().unwrap(), "\u{2192}LIST");
1135 }
1136
1137 #[test]
1138 fn json_shapes_parse() {
1139 let o: Object = serde_json::from_value(json!({"type": "list", "items": [
1140 {"type": "real", "value": 0.5},
1141 {"type": "string", "value": "s"},
1142 {"type": "program", "source": "« 1 »"},
1143 {"type": "binary", "value": 42},
1144 {"type": "array", "dims": [2], "items": [{"type": "real", "value": 1}, {"type": "real", "value": 2}]}
1145 ]}))
1146 .unwrap();
1147 let Object::List { items } = o else { panic!() };
1148 assert_eq!(items.len(), 5);
1149 assert!(serde_json::from_value::<Object>(json!({"type": "real", "value": "x"})).is_err());
1150 assert!(serde_json::from_value::<Object>(json!({"type": "nope"})).is_err());
1151 }
1152}