use rune_ast::{Noun, tag};
use tape::{bytes::Bytes, encode_nested, render, sigil, Chunk};
pub fn noun_to_bytes(n: &Noun) -> Vec<u8> {
match n {
Noun::Atom(0) => Vec::new(),
Noun::Atom(a) => {
let mut v = a.to_le_bytes().to_vec();
while v.last() == Some(&0) {
v.pop();
}
v
}
Noun::Cell(_, _) => {
let mut out = Vec::new();
let mut cur = n;
loop {
match cur {
Noun::Cell(h, t) => {
if let Noun::Atom(b) = h.as_ref() {
out.push(*b as u8);
}
cur = t.as_ref();
}
Noun::Atom(0) => break,
Noun::Atom(b) => {
out.push(*b as u8);
break;
}
}
}
out
}
}
}
fn text_of(n: &Noun) -> String {
String::from_utf8_lossy(&noun_to_bytes(n)).into_owned()
}
pub fn noun_to_chunk(n: &Noun) -> Option<Chunk> {
let Noun::Cell(head, payload) = n else { return None };
let Noun::Atom(t) = head.as_ref() else { return None };
let chunk = match *t {
tag::TEXT => Chunk::text(&text_of(payload)),
tag::ANNO => Chunk::annotation(&text_of(payload)),
tag::ERROR => Chunk::error(&text_of(payload)),
tag::LOG => Chunk::new(sigil::DOT, render::LOG, Bytes::from(noun_to_bytes(payload))),
tag::BUTTON => {
let Noun::Cell(label, target) = payload.as_ref() else { return None };
let label_c = Chunk::annotation(&text_of(label)); let target_c = Chunk::text(&text_of(target)); Chunk::new(sigil::ZAP, render::COMPONENT, encode_nested(&[label_c, target_c]))
}
_ => return None,
};
Some(chunk)
}
pub fn noun_to_chunks(n: &Noun) -> Vec<Chunk> {
if let Noun::Cell(head, tail) = n {
if matches!(head.as_ref(), Noun::Atom(t) if *t == tag::LIST) {
let mut out = Vec::new();
let mut cur = tail.as_ref();
while let Noun::Cell(elem, rest) = cur {
if let Some(c) = noun_to_chunk(elem) {
out.push(c);
}
cur = rest.as_ref();
}
return out;
}
}
noun_to_chunk(n).into_iter().collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn tape(s: &str) -> Noun {
let mut n = Noun::Atom(0);
for &b in s.as_bytes().iter().rev() {
n = Noun::cell(Noun::Atom(b as u64), n);
}
n
}
#[test]
fn tape_roundtrips_arbitrary_length() {
let s = "hello, cyber neuron";
assert_eq!(noun_to_bytes(&tape(s)), s.as_bytes());
}
#[test]
fn cord_atom_decodes_le() {
assert_eq!(noun_to_bytes(&Noun::Atom(0x6968)), b"hi");
assert_eq!(noun_to_bytes(&Noun::Atom(0)), b"");
}
#[test]
fn text_element_is_hax_text() {
let n = Noun::cell(Noun::Atom(tag::TEXT), tape("hi"));
let c = noun_to_chunk(&n).unwrap();
assert_eq!((c.sigil, c.render), (sigil::HAX, render::TEXT));
assert_eq!(&c.payload[..], b"hi");
}
#[test]
fn error_element_is_zap_error() {
let n = Noun::cell(Noun::Atom(tag::ERROR), tape("boom"));
let c = noun_to_chunk(&n).unwrap();
assert_eq!((c.sigil, c.render), (sigil::ZAP, render::ERROR));
assert!(std::str::from_utf8(&c.payload).unwrap().contains("boom"));
}
#[test]
fn button_element_is_zap_component_with_label_and_target() {
let n = Noun::cell(
Noun::Atom(tag::BUTTON),
Noun::cell(tape("ok"), tape("@master")),
);
let c = noun_to_chunk(&n).unwrap();
assert_eq!((c.sigil, c.render), (sigil::ZAP, render::COMPONENT));
let inner = tape::decode_nested(&c.payload);
assert_eq!(inner.len(), 2);
assert_eq!((inner[0].sigil, inner[0].render), (sigil::SIG, render::TEXT));
assert_eq!(&inner[0].payload[..], b"ok");
assert_eq!((inner[1].sigil, inner[1].render), (sigil::HAX, render::TEXT));
assert_eq!(&inner[1].payload[..], b"@master");
}
#[test]
fn list_yields_one_chunk_per_element() {
let elems = Noun::cell(
Noun::cell(Noun::Atom(tag::TEXT), tape("a")),
Noun::cell(
Noun::cell(Noun::Atom(tag::ERROR), tape("b")),
Noun::Atom(0),
),
);
let n = Noun::cell(Noun::Atom(tag::LIST), elems);
let chunks = noun_to_chunks(&n);
assert_eq!(chunks.len(), 2);
assert_eq!(chunks[0].render, render::TEXT);
assert_eq!(chunks[1].render, render::ERROR);
}
}