1use anyhow::{Context, Result, bail};
11use saturnus_core::Model;
12
13use crate::charset;
14use crate::object::{ArrayItem, Graphic, 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
401pub fn graphic_file(g: &Graphic, family: Family) -> Result<Vec<u8>> {
404 let mut data = binary_header(family).to_vec();
405 data.extend(pack(&g.to_grob()?));
406 Ok(data)
407}
408
409fn prolog(out: &mut Vec<u8>, ty: ObjectType) {
410 put_field(out, u64::from(ty.prolog()), 5);
411}
412
413fn encode_into(obj: &Object, family: Family, out: &mut Vec<u8>) -> Result<bool> {
414 match obj {
415 Object::Real { value } => {
416 prolog(out, ObjectType::Real);
417 out.extend(value.to_body());
418 }
419 Object::Complex { re, im } => {
420 prolog(out, ObjectType::Complex);
421 out.extend(re.to_body());
422 out.extend(im.to_body());
423 }
424 Object::String { value } => {
425 prolog(out, ObjectType::String);
426 let mut body = Vec::new();
427 let n = put_chars(&mut body, value)?;
428 put_field(out, 5 + 2 * n as u64, 5);
429 out.extend(body);
430 }
431 Object::Name { value } | Object::LocalName { value } => {
432 let ty = if matches!(obj, Object::Name { .. }) {
433 ObjectType::GlobalName
434 } else {
435 ObjectType::LocalName
436 };
437 prolog(out, ty);
438 let mut body = Vec::new();
439 let n = put_chars(&mut body, value)?;
440 if n == 0 || n > 255 {
441 bail!("a name has 1 to 255 characters: {value:?}");
442 }
443 put_field(out, n as u64, 2);
444 out.extend(body);
445 }
446 Object::Character { value } => {
447 prolog(out, ObjectType::Character);
448 if put_chars(out, value)? != 1 {
449 bail!("a character object holds one character: {value:?}");
450 }
451 }
452 Object::Binary { value, .. } => {
453 prolog(out, ObjectType::BinaryInteger);
454 put_field(out, 21, 5);
455 put_field(out, *value, 16);
456 }
457 Object::Integer { value } => {
458 if family != Family::Hp49 {
459 bail!("exact integers exist on the 49G only; send a real instead");
460 }
461 prolog(out, ObjectType::Integer);
462 let n = value.to_nibbles();
463 put_field(out, 5 + n.len() as u64, 5);
464 out.extend(n);
465 }
466 Object::List { items } => {
467 prolog(out, ObjectType::List);
468 for item in items {
469 if !encode_into(item, family, out)? {
470 return Ok(false);
471 }
472 }
473 put_field(out, u64::from(SEMI), 5);
474 }
475 Object::Tagged { tag, object } => {
476 prolog(out, ObjectType::Tagged);
477 let mut body = Vec::new();
478 let n = put_chars(&mut body, tag)?;
479 if n > 255 {
480 bail!("tag longer than 255 characters");
481 }
482 put_field(out, n as u64, 2);
483 out.extend(body);
484 return encode_into(object, family, out);
485 }
486 Object::Unknown { hex, truncated, .. } => {
487 if *truncated {
488 return Ok(false);
489 }
490 for c in hex.chars() {
491 let d = c
492 .to_digit(16)
493 .with_context(|| format!("hex holds {c:?}, not a hex digit"))?;
494 out.push(d as u8);
495 }
496 }
497 Object::Command {
499 address: None,
500 library: Some(lib),
501 command: Some(cmd),
502 ..
503 } => {
504 prolog(out, ObjectType::XlibName);
505 put_field(out, u64::from(*lib), 3);
506 put_field(out, u64::from(*cmd), 3);
507 }
508 Object::Unit { .. }
509 | Object::Array { .. }
510 | Object::Program { .. }
511 | Object::Algebraic { .. }
512 | Object::Command { .. } => return Ok(false),
513 }
514 Ok(true)
515}
516
517pub fn to_source(obj: &Object, family: Family) -> Result<Option<String>> {
520 Ok(match obj {
521 Object::Real { value } => Some(value.to_source()),
522 Object::Integer { value } => {
523 if family != Family::Hp49 {
524 bail!("exact integers exist on the 49G only; send a real instead");
525 }
526 Some(value.to_source())
527 }
528 Object::Complex { re, im } => Some(format!("({},{})", re.to_source(), im.to_source())),
529 Object::String { value } => (!value.contains(['"', '\\'])).then(|| format!("\"{value}\"")),
530 Object::Name { value } => is_plain_name(value).then(|| format!("'{value}'")),
533 Object::Binary { value, .. } => Some(format!("#{value:X}h")),
534 Object::List { items } => {
535 let mut parts = Vec::new();
536 for item in items {
537 match to_source(item, family)? {
538 Some(s) => parts.push(s),
539 None => return Ok(None),
540 }
541 }
542 Some(format!("{{ {} }}", parts.join(" ")))
543 }
544 Object::Tagged { tag, object } => {
545 if !plain_token(tag, false) {
546 return Ok(None);
547 }
548 to_source(object, family)?.map(|s| format!(":{tag}:{s}"))
549 }
550 Object::Unit { value, unit } => {
551 let unit = unit.as_deref().context("a unit object needs its unit")?;
552 if !plain_token(unit, true) || unit.contains('_') {
553 bail!(
554 "not a unit expression (spaces and RPL delimiters are not allowed): {unit:?}"
555 );
556 }
557 Some(format!("{}_{unit}", value.to_source()))
558 }
559 Object::Array { items, .. } => array_source(items, family)?,
560 Object::Program { source } | Object::Algebraic { source } => {
561 let source = source.as_deref().context("this object needs its source")?;
562 Some(one_object_source(obj, source)?)
563 }
564 Object::Command {
567 name: None,
568 address: None,
569 library: Some(_),
570 command: Some(_),
571 ..
572 } => None,
573 Object::Command { name, source, .. } => {
574 let source = source
575 .as_deref()
576 .or(name.as_deref())
577 .context("this object needs its source")?;
578 Some(one_object_source(obj, source)?)
579 }
580 Object::LocalName { .. } | Object::Character { .. } | Object::Unknown { .. } => None,
581 })
582}
583
584fn plain_token(s: &str, parens: bool) -> bool {
588 !s.is_empty()
589 && s.chars().all(|c| {
590 !c.is_whitespace()
591 && !c.is_control()
592 && !matches!(
593 c,
594 '\'' | '"' | '«' | '»' | '{' | '}' | '[' | ']' | ':' | '#' | ',' | ';' | '\\'
595 )
596 && (parens || !matches!(c, '(' | ')'))
597 })
598}
599
600fn one_object_source(obj: &Object, source: &str) -> Result<String> {
605 let text = source.trim().replace("\\<<", "«").replace("\\>>", "»");
607 let mut scan = Scanner { s: &text, pos: 0 };
608 let ok = match obj {
609 Object::Program { .. } => scan.balanced('«', '»').is_some(),
610 Object::Algebraic { .. } => scan.quoted().is_some_and(|q| q.len() > 2),
611 _ => plain_token(&text, true) && scan.token().is_some(),
612 };
613 if !ok || !scan.rest().trim().is_empty() {
614 let what = match obj {
615 Object::Program { .. } => "a program's source must be one « ... » group",
616 Object::Algebraic { .. } => "an algebraic's source must be one '...' group",
617 _ => "a command's source must be one token",
618 };
619 bail!("{what} with nothing after it: {source:?}");
620 }
621 Ok(source.trim().to_string())
622}
623
624fn array_source(items: &[ArrayItem], family: Family) -> Result<Option<String>> {
625 let mut parts = Vec::new();
626 for item in items {
627 let s = match item {
628 ArrayItem::Row(row) => array_source(row, family)?,
629 ArrayItem::Item(obj) => {
630 if !matches!(**obj, Object::Real { .. } | Object::Complex { .. }) {
631 bail!("array elements must be reals or complex numbers");
632 }
633 to_source(obj, family)?
634 }
635 };
636 match s {
637 Some(s) => parts.push(s),
638 None => return Ok(None),
639 }
640 }
641 Ok(Some(format!("[ {} ]", parts.join(" "))))
642}
643
644#[derive(Clone, Debug, Default, PartialEq)]
647pub struct StackReply {
648 pub error: Option<String>,
650 pub levels: Vec<String>,
652}
653
654impl StackReply {
655 pub fn level(&self, n: usize) -> Option<&str> {
657 n.checked_sub(1)
658 .and_then(|i| self.levels.get(i))
659 .map(String::as_str)
660 }
661}
662
663fn level_prefix(line: &str) -> Option<(usize, &str)> {
665 let (num, rest) = line.split_once(':')?;
666 if num.is_empty() || !num.bytes().all(|b| b.is_ascii_digit()) {
667 return None;
668 }
669 let n = num.parse().ok()?;
670 (n >= 1).then_some((n, rest))
671}
672
673pub fn parse_stack(text: &str) -> StackReply {
678 let text = text.trim_end_matches(['\r', '\n']);
679 let mut reply = StackReply::default();
680 let mut values: Vec<String> = Vec::new();
682 let mut expected = 0usize;
684 for (i, line) in text.split('\n').enumerate() {
685 let line = line.strip_suffix('\r').unwrap_or(line);
686 if i == 0
687 && let Some(msg) = line.strip_prefix("Error:")
688 {
689 reply.error = Some(msg.trim().to_string());
690 continue;
691 }
692 if values.is_empty() {
693 if line.trim() == "Empty Stack" {
694 break;
695 }
696 if let Some((n, rest)) = level_prefix(line) {
697 values.push(rest.trim_start_matches(' ').to_string());
698 expected = n - 1;
699 }
700 continue;
701 }
702 if expected >= 1
703 && let Some(rest) = line.strip_prefix(format!("{expected}:").as_str())
704 {
705 values.push(rest.trim_start_matches(' ').to_string());
706 expected -= 1;
707 continue;
708 }
709 if let Some(last) = values.last_mut() {
710 last.push('\n');
711 last.push_str(line);
712 }
713 }
714 values.reverse();
715 reply.levels = values;
716 reply
717}
718
719#[cfg(test)]
720mod tests {
721 use super::*;
722
723 #[test]
724 fn a_picture_travels_as_a_binary_grob() {
725 let g = Graphic::from_packed(131, 64, &[0x5A; 17 * 64]).unwrap();
726 for (family, head) in [(Family::Hp48, b"HPHP48-X"), (Family::Hp49, b"HPHP49-X")] {
727 let file = graphic_file(&g, family).unwrap();
728 assert_eq!(&file[..HEADER_LEN], head);
729 assert_eq!(file.len(), HEADER_LEN + 2196 / 2);
730 assert!(is_binary_file(&file));
731 let Object::Unknown {
732 kind,
733 graphic: Some(back),
734 ..
735 } = decode_file(&file, &crate::NoMemory).unwrap()
736 else {
737 panic!("not a picture");
738 };
739 assert_eq!(kind.as_deref(), Some("Graphic"));
740 assert_eq!(back, g);
741 }
742 }
743
744 #[test]
745 fn stacks() {
746 let r = parse_stack("2: 42\r\n1: \"a\r\nb\"\r\n");
747 assert_eq!(r.levels, ["\"a\nb\"", "42"]);
748 assert_eq!(r.error, None);
749 assert_eq!(r.level(2), Some("42"));
750 assert_eq!(r.level(0), None);
751 let r = parse_stack("Error: Too Few Arguments\r\nEmpty Stack\r\n");
752 assert_eq!(r.error.as_deref(), Some("Too Few Arguments"));
753 assert!(r.levels.is_empty());
754 let r = parse_stack("1: { 1:2 }\n1: x");
756 assert_eq!(r.levels, ["{ 1:2 }\n1: x"]);
757 }
758 use crate::object::{Integer, NoMemory, Real};
759 use serde_json::json;
760
761 #[test]
762 fn headers_and_packing() {
763 assert_eq!(&binary_header(Family::Hp49), b"HPHP49-X");
764 assert!(is_binary_file(b"HPHP48-J\x01"));
765 assert!(!is_binary_file(b"HPHP47-J") && !is_binary_file(b"HPHP48-"));
766 assert_eq!(Family::of(Model::Hp49g), Family::Hp49);
767 assert_eq!(Family::of(Model::Hp48sx), Family::Hp48);
768 assert_eq!(pack(&[1, 2, 3]), [0x21, 0x03]);
769 assert_eq!(unpack(&[0x21, 0x03]), [1, 2, 3, 0]);
770 assert_eq!(ascii_body("%%HP: T(1)A(D)F(.);\r\n{ 1 }\r\n"), "{ 1 }\r\n");
771 assert_eq!(ascii_body("%%HP: T(3)A(R)F(,);\n« »"), "« »");
772 assert_eq!(ascii_body("{ 1 }"), "{ 1 }");
773 }
774
775 #[test]
776 fn plain_names() {
777 assert!(is_plain_name("X") && is_plain_name("ΣDAT") && is_plain_name("A.1"));
778 for bad in [
779 "",
780 "A\\GS",
781 "1A",
782 ".A",
783 "A B",
784 "X'",
785 "A+B",
786 "a:b",
787 "€",
788 &"A".repeat(128),
789 ] {
790 assert!(!is_plain_name(bad), "{bad:?}");
791 }
792 }
793
794 fn nib(s: &str) -> Vec<u8> {
796 s.chars().map(|c| c.to_digit(16).unwrap() as u8).collect()
797 }
798
799 fn dec(s: &str) -> Object {
800 decode(&nib(s), &NoMemory).unwrap()
801 }
802
803 fn real(s: &str) -> Real {
804 Real::parse(s).unwrap()
805 }
806
807 struct Rom;
809 impl Memory for Rom {
810 fn nibble(&self, addr: u32) -> Option<u8> {
811 let i = addr.checked_sub(0x2A2C9)? as usize;
812 nib("339200000000000000010").get(i).copied()
813 }
814 }
815
816 #[test]
820 fn unresolved_xlib_names_round_trip() {
821 let file = {
823 let mut d = binary_header(Family::Hp48).to_vec();
824 d.extend(pack(&nib("47A2029E202D4500B2130")));
825 d
826 };
827 let obj = decode_file(&file, &NoMemory).unwrap();
828 let Object::List { items } = &obj else {
829 panic!("{obj:?}")
830 };
831 assert_eq!(
832 serde_json::to_value(&items[0]).unwrap(),
833 json!({"type": "command", "library": 1234, "command": 5})
834 );
835 assert!(obj.needs_source());
836 assert_eq!(to_source(&obj, Family::Hp48).unwrap(), None);
837 assert_eq!(encode_file(&obj, Family::Hp48).unwrap().unwrap(), file);
838 let mut filled = obj.clone();
840 fill_sources(&mut filled, "{ XLIB 1234 5 }");
841 let Object::List { items } = &filled else {
842 panic!()
843 };
844 assert!(matches!(&items[0], Object::Command { source: Some(s), .. } if s == "XLIB 1234 5"));
845 assert_eq!(encode_file(&filled, Family::Hp48).unwrap().unwrap(), file);
846 }
847
848 #[test]
849 fn sources_come_from_the_ascii_text() {
850 let mut p = dec("D9D20E16329C2A2ED2A276BA193632B21300");
852 assert!(p.needs_source());
853 fill_sources(&mut p, "%%HP: T(1)A(D)F(.);\r\n« 1 2 +\r\n»\r\n");
854 assert_eq!(
855 p,
856 Object::Program {
857 source: Some("« 1 2 +\n»".into())
858 }
859 );
860 let mut a = dec("8BA2084E201085ED2A2D20B1B21300");
861 fill_sources(&mut a, "%%HP: T(1)A(D)F(.);\r\n'X^2'\r\n");
862 assert_eq!(
863 a,
864 Object::Algebraic {
865 source: Some("'X^2'".into())
866 }
867 );
868 let unit = "ADA209C2A2C2A2070000D6C2A207000037ED2A227B0186B0168B01B21300";
871 assert!(matches!(dec(unit), Object::Unknown { .. }));
872 let mut u = decode(&nib(unit), &Rom).unwrap();
873 assert_eq!(
874 u,
875 Object::Unit {
876 value: real("1"),
877 unit: None
878 }
879 );
880 fill_sources(&mut u, "%%HP: T(1)A(D)F(.);\r\n'1_m/s^2'\r\n");
881 assert_eq!(
882 u,
883 Object::Unit {
884 value: real("1"),
885 unit: Some("m/s^2".into())
886 }
887 );
888 }
889
890 #[test]
891 fn nested_sources_follow_the_tree() {
892 let mut list = Object::List {
893 items: vec![
894 Object::Real { value: real("1") },
895 Object::Program { source: None },
896 Object::String {
897 value: "a » b".into(),
898 },
899 Object::Tagged {
900 tag: "T".into(),
901 object: Box::new(Object::Command {
902 name: None,
903 address: None,
904 library: None,
905 command: None,
906 source: None,
907 }),
908 },
909 Object::Binary {
910 value: 42,
911 base: None,
912 text: None,
913 },
914 Object::List {
915 items: vec![Object::Algebraic { source: None }],
916 },
917 Object::Complex {
918 re: real("1"),
919 im: real("2"),
920 },
921 ],
922 };
923 fill_sources(
924 &mut list,
925 "{ 1 « 2 \"x»\" + » \"a » b\" :T: + # 42d { 'X+1' } (1,2) }",
926 );
927 let Object::List { items } = &list else {
928 panic!()
929 };
930 assert_eq!(
931 items[1],
932 Object::Program {
933 source: Some("« 2 \"x»\" + »".into())
934 }
935 );
936 assert_eq!(
937 items[3],
938 Object::Tagged {
939 tag: "T".into(),
940 object: Box::new(Object::Command {
941 name: None,
942 address: None,
943 library: None,
944 command: None,
945 source: Some("+".into())
946 })
947 }
948 );
949 assert_eq!(
950 items[5],
951 Object::List {
952 items: vec![Object::Algebraic {
953 source: Some("'X+1'".into())
954 }]
955 }
956 );
957 let mut l = Object::List {
959 items: vec![Object::Program { source: None }],
960 };
961 fill_sources(&mut l, "{ 1 2 }");
962 assert_eq!(
963 l,
964 Object::List {
965 items: vec![Object::Program { source: None }]
966 }
967 );
968 let mut p = Object::Program { source: None };
970 fill_sources(&mut p, "« garbled");
971 assert_eq!(
972 p,
973 Object::Program {
974 source: Some("« garbled".into())
975 }
976 );
977 }
978
979 #[test]
980 fn encode_round_trips_through_decode() {
981 let objs = [
982 Object::Real {
983 value: real("-4.79425538604E-1"),
984 },
985 Object::Complex {
986 re: real("1.5"),
987 im: real("-2"),
988 },
989 Object::String {
990 value: "Hi «x» \"q\"".into(),
991 },
992 Object::Name {
993 value: "ABC".into(),
994 },
995 Object::LocalName { value: "x".into() },
996 Object::Character { value: "A".into() },
997 Object::Binary {
998 value: u64::MAX,
999 base: None,
1000 text: None,
1001 },
1002 Object::List {
1003 items: vec![
1004 Object::Real { value: real("1") },
1005 Object::List { items: vec![] },
1006 Object::Tagged {
1007 tag: "T".into(),
1008 object: Box::new(Object::Real { value: real("5") }),
1009 },
1010 ],
1011 },
1012 ];
1013 for o in &objs {
1014 let file = encode_file(o, Family::Hp48).unwrap().unwrap();
1015 assert_eq!(&file[..8], b"HPHP48-X");
1016 assert_eq!(decode_file(&file, &NoMemory).unwrap(), *o, "{o:?}");
1017 let size = crate::prolog::object_size(&unpack(&file[8..]), 0).unwrap();
1019 assert_eq!(file.len(), 8 + size.div_ceil(2));
1020 }
1021 let file = encode_file(
1023 &Object::Real {
1024 value: real("8.72653549837E-3"),
1025 },
1026 Family::Hp48,
1027 )
1028 .unwrap()
1029 .unwrap();
1030 assert_eq!(unpack(&file[8..])[..21], nib("339207997389453562780")[..]);
1031 let i = Object::Integer {
1032 value: Integer::parse("-1234567890123456789").unwrap(),
1033 };
1034 let file = encode_file(&i, Family::Hp49).unwrap().unwrap();
1035 assert_eq!(
1036 unpack(&file[8..])[..30],
1037 nib("416209100098765432109876543219")[..]
1038 );
1039 assert!(encode_file(&i, Family::Hp48).is_err());
1040 assert_eq!(
1041 encode_file(
1042 &Object::Program {
1043 source: Some("« »".into())
1044 },
1045 Family::Hp48
1046 )
1047 .unwrap(),
1048 None
1049 );
1050 let u = dec("E1B20A0000FFFFF");
1052 assert!(matches!(u, Object::Unknown { .. }));
1053 assert_eq!(
1054 encode_file(&u, Family::Hp48).unwrap().unwrap()[8..],
1055 pack(&nib("E1B20A0000FFFFF"))[..]
1056 );
1057 }
1058
1059 #[test]
1060 fn sources_for_text_transfers() {
1061 let src = |o: Object| to_source(&o, Family::Hp48).unwrap();
1062 assert_eq!(src(Object::Real { value: real("0.5") }).unwrap(), "5.E-1");
1063 assert_eq!(
1064 src(Object::Complex {
1065 re: real("1"),
1066 im: real("-2")
1067 })
1068 .unwrap(),
1069 "(1.,-2.)"
1070 );
1071 assert_eq!(
1072 src(Object::String {
1073 value: "a b".into()
1074 })
1075 .unwrap(),
1076 "\"a b\""
1077 );
1078 assert_eq!(
1079 src(Object::String {
1080 value: "a\"b".into()
1081 }),
1082 None
1083 );
1084 assert_eq!(
1085 src(Object::Binary {
1086 value: 255,
1087 base: None,
1088 text: None
1089 })
1090 .unwrap(),
1091 "#FFh"
1092 );
1093 assert_eq!(
1094 src(Object::List {
1095 items: vec![
1096 Object::Name { value: "X".into() },
1097 Object::Unit {
1098 value: real("9.81"),
1099 unit: Some("m/s^2".into())
1100 }
1101 ]
1102 })
1103 .unwrap(),
1104 "{ 'X' 9.81_m/s^2 }"
1105 );
1106 assert_eq!(src(Object::LocalName { value: "x".into() }), None);
1107 assert!(to_source(&Object::Program { source: None }, Family::Hp48).is_err());
1108 }
1109
1110 #[test]
1111 fn text_cannot_break_out_of_its_quotes() {
1112 let src = |o: Object| to_source(&o, Family::Hp48);
1113 assert_eq!(
1115 src(Object::Name {
1116 value: "X' 11. 'QA' STO 'Y".into()
1117 })
1118 .unwrap(),
1119 None
1120 );
1121 let odd = Object::Tagged {
1122 tag: "a:b".into(),
1123 object: Box::new(Object::Real { value: Real::ZERO }),
1124 };
1125 assert_eq!(src(odd.clone()).unwrap(), None);
1126 assert!(encode_file(&odd, Family::Hp48).unwrap().is_some());
1127 let spaced = Object::Tagged {
1128 tag: "my tag".into(),
1129 object: Box::new(Object::Real { value: Real::ZERO }),
1130 };
1131 assert_eq!(src(spaced).unwrap(), None);
1132 let p = |s: &str| Object::Program {
1134 source: Some(s.into()),
1135 };
1136 assert!(src(p("« 1 » 22. 'QB' STO « 2 »")).is_err());
1137 assert!(src(p("1 2 +")).is_err());
1138 assert!(src(p("« 1 \"»\" »")).is_ok());
1139 assert!(src(p("\\<< 1 \\>>")).is_ok());
1140 assert!(src(p("« « 1 » »")).is_ok());
1141 let a = |s: &str| Object::Algebraic {
1142 source: Some(s.into()),
1143 };
1144 assert!(src(a("'X' 'Y' 44. 'QD' STO 'Z'")).is_err());
1145 assert!(src(a("X+1")).is_err());
1146 assert!(src(a("''")).is_err());
1147 assert_eq!(src(a("'X^2+1'")).unwrap().unwrap(), "'X^2+1'");
1148 let u = |s: &str| Object::Unit {
1149 value: Real::ZERO,
1150 unit: Some(s.into()),
1151 };
1152 assert!(src(u("m 33. 'QC' STO")).is_err());
1153 assert!(src(u("m'")).is_err());
1154 assert!(src(u("(m/s)^2")).is_ok());
1155 let c = |s: &str| Object::Command {
1156 name: None,
1157 address: None,
1158 library: None,
1159 command: None,
1160 source: Some(s.into()),
1161 };
1162 assert!(src(c("+ 1. 'QE' STO")).is_err());
1163 assert_eq!(src(c("\u{2192}LIST")).unwrap().unwrap(), "\u{2192}LIST");
1164 }
1165
1166 #[test]
1167 fn json_shapes_parse() {
1168 let o: Object = serde_json::from_value(json!({"type": "list", "items": [
1169 {"type": "real", "value": 0.5},
1170 {"type": "string", "value": "s"},
1171 {"type": "program", "source": "« 1 »"},
1172 {"type": "binary", "value": 42},
1173 {"type": "array", "dims": [2], "items": [{"type": "real", "value": 1}, {"type": "real", "value": 2}]}
1174 ]}))
1175 .unwrap();
1176 let Object::List { items } = o else { panic!() };
1177 assert_eq!(items.len(), 5);
1178 assert!(serde_json::from_value::<Object>(json!({"type": "real", "value": "x"})).is_err());
1179 assert!(serde_json::from_value::<Object>(json!({"type": "nope"})).is_err());
1180 }
1181}