Lines
98.17 %
Functions
58.93 %
Branches
100 %
use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use super::value::{Pair, Value};
const BYTES_INLINE_THRESHOLD: usize = 32;
#[must_use]
pub fn format_value(value: &Value) -> String {
let mut out = String::new();
write_value(&mut out, value);
out
}
fn write_value(out: &mut String, value: &Value) {
match value {
Value::Nil => out.push_str("NIL"),
Value::Bool(true) => out.push_str("#t"),
Value::Bool(false) => out.push_str("#f"),
Value::Number(n) if *n.denom() == 1 => {
out.push_str(&n.numer().to_string());
Value::Number(n) => {
out.push_str(&format!("{}/{}", n.numer(), n.denom()));
Value::String(s) => write_string_literal(out, s),
Value::Symbol(s) => out.push_str(s),
Value::Bytes(b) => write_bytes_literal(out, b),
Value::Pair(pair) => write_pair_or_list(out, pair),
Value::Vector(items) => write_vector(out, items),
Value::Closure(c) => {
out.push_str(&format!("#<closure:{}>", c.code_id));
Value::Struct { name, fields } => write_struct(out, name, fields),
Value::Commodity {
amount,
commodity_id,
} => {
// Plist envelope keeps the wire shape uniform with native
// results: emacs `(read)` recovers `(:commodity <ratio> :id "<uuid>")`.
out.push_str("(:commodity ");
if *amount.denom() == 1 {
out.push_str(&amount.numer().to_string());
} else {
out.push_str(&format!("{}/{}", amount.numer(), amount.denom()));
out.push_str(" :id \"");
out.push_str(&commodity_id.to_string());
out.push_str("\")");
fn write_string_literal(out: &mut String, s: &str) {
out.push('"');
for ch in s.chars() {
match ch {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\t' => out.push_str("\\t"),
'\r' => out.push_str("\\r"),
other => out.push(other),
fn write_bytes_literal(out: &mut String, bytes: &[u8]) {
if bytes.len() <= BYTES_INLINE_THRESHOLD {
out.push_str("#u8(");
let mut first = true;
for byte in bytes {
if first {
first = false;
out.push(' ');
out.push_str(&byte.to_string());
out.push(')');
out.push_str("#\"");
out.push_str(&BASE64_STANDARD.encode(bytes));
fn write_pair_or_list(out: &mut String, pair: &Pair) {
out.push('(');
write_value(out, &pair.car);
let mut cdr = &pair.cdr;
loop {
match cdr {
Value::Nil => break,
Value::Pair(next) => {
write_value(out, &next.car);
cdr = &next.cdr;
other => {
out.push_str(" . ");
write_value(out, other);
break;
fn write_vector(out: &mut String, items: &[Value]) {
out.push_str("#(");
for item in items {
write_value(out, item);
fn write_struct(out: &mut String, name: &str, fields: &[Value]) {
out.push_str("#S(");
out.push_str(name);
for field in fields {
write_value(out, field);
#[cfg(test)]
mod tests {
use super::super::value::{Closure, Fraction, Pair};
use super::*;
use crate::ast::Expr;
use crate::reader::Reader;
#[test]
fn formats_nil() {
assert_eq!(format_value(&Value::Nil), "NIL");
fn formats_booleans() {
assert_eq!(format_value(&Value::Bool(true)), "#t");
assert_eq!(format_value(&Value::Bool(false)), "#f");
fn formats_integer_number() {
assert_eq!(
format_value(&Value::Number(Fraction::from_integer(42))),
"42"
);
fn formats_negative_integer() {
format_value(&Value::Number(Fraction::from_integer(-7))),
"-7"
fn formats_proper_fraction() {
assert_eq!(format_value(&Value::Number(Fraction::new(3, 4))), "3/4");
fn formats_string_with_escapes() {
let v = Value::String("a\nb\"c\\d".to_string());
assert_eq!(format_value(&v), "\"a\\nb\\\"c\\\\d\"");
fn formats_symbol_verbatim() {
assert_eq!(format_value(&Value::Symbol("FOO".into())), "FOO");
fn formats_short_bytes_as_u8_literal() {
let v = Value::Bytes(vec![0, 1, 255]);
assert_eq!(format_value(&v), "#u8(0 1 255)");
fn formats_empty_bytes_as_u8_literal() {
assert_eq!(format_value(&Value::Bytes(Vec::new())), "#u8()");
fn formats_bytes_at_inline_threshold() {
let bytes: Vec<u8> = (0..BYTES_INLINE_THRESHOLD as u8).collect();
let formatted = format_value(&Value::Bytes(bytes.clone()));
assert!(formatted.starts_with("#u8("));
assert!(formatted.ends_with(')'));
fn formats_long_bytes_as_base64_literal() {
let bytes: Vec<u8> = (0..=BYTES_INLINE_THRESHOLD as u8).collect();
assert!(formatted.starts_with("#\""));
assert!(formatted.ends_with('"'));
let payload = &formatted[2..formatted.len() - 1];
let decoded = BASE64_STANDARD.decode(payload).unwrap();
assert_eq!(decoded, bytes);
fn formats_proper_list() {
let list = Pair::cons(
Value::Number(Fraction::from_integer(1)),
Pair::cons(
Value::Number(Fraction::from_integer(2)),
Pair::cons(Value::Number(Fraction::from_integer(3)), Value::Nil),
),
assert_eq!(format_value(&list), "(1 2 3)");
fn formats_dotted_pair() {
let pair = Pair::cons(Value::Symbol("A".into()), Value::Symbol("B".into()));
assert_eq!(format_value(&pair), "(A . B)");
fn formats_improper_list() {
Value::Number(Fraction::from_integer(3)),
assert_eq!(format_value(&list), "(1 2 . 3)");
fn formats_vector() {
let v = Value::Vector(vec![
Value::Bool(true),
]);
assert_eq!(format_value(&v), "#(1 #t)");
fn formats_closure() {
let c = Value::Closure(Closure::new(7, vec![]));
assert_eq!(format_value(&c), "#<closure:7>");
fn formats_struct() {
let s = Value::Struct {
name: "ACCOUNT".into(),
fields: vec![
Value::Symbol("ID".into()),
Value::Number(Fraction::from_integer(42)),
],
};
assert_eq!(format_value(&s), "#S(ACCOUNT ID 42)");
fn round_trip_short_bytes_through_reader() {
let v = Value::Bytes(vec![0xCA, 0xFE, 0xBA, 0xBE]);
let formatted = format_value(&v);
let parsed = Reader::parse(&formatted).unwrap();
parsed.exprs,
vec![Expr::Bytes(vec![0xCA, 0xFE, 0xBA, 0xBE])]
fn round_trip_long_bytes_through_reader() {
let bytes: Vec<u8> = (0..=255).chain(0..=255).collect();
assert_eq!(parsed.exprs, vec![Expr::Bytes(bytes)]);
fn round_trip_full_byte_range_through_reader() {
let bytes: Vec<u8> = (0..=255).collect();
fn round_trip_rational_through_reader() {
for (num, den) in [(1i64, 2i64), (-3, 4), (1, 3), (7, 11)] {
let v = Value::Number(Fraction::new(num, den));
assert_eq!(parsed.exprs, vec![Expr::Number(Fraction::new(num, den))]);