use std::ops::ControlFlow;
use crate::datum::{Datum, DatumKind, Prefix};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Class {
Code,
Data,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Walk {
Descend,
Skip,
Stop,
}
#[derive(Debug, Clone, Copy)]
struct Ctx {
hard_quote: bool,
qq: u32,
}
impl Ctx {
const TOP: Ctx = Ctx {
hard_quote: false,
qq: 0,
};
const DATA: Ctx = Ctx {
hard_quote: true,
qq: 0,
};
fn class(self) -> Class {
if self.hard_quote || self.qq > 0 {
Class::Data
} else {
Class::Code
}
}
}
fn inner_ctx(prefix: Prefix, ctx: Ctx) -> Ctx {
match prefix {
Prefix::Quote => Ctx::DATA,
Prefix::Quasiquote => Ctx {
qq: ctx.qq + 1,
..ctx
},
Prefix::Unquote | Prefix::UnquoteSplicing => Ctx {
qq: ctx.qq.saturating_sub(1),
..ctx
},
Prefix::VarQuote
| Prefix::FunctionQuote
| Prefix::Deref
| Prefix::Splice
| Prefix::HashFn => ctx,
Prefix::ReadEval => Ctx::TOP,
Prefix::Meta
| Prefix::Mutable
| Prefix::FeatureConditional { .. }
| Prefix::ReaderConditional { .. } => ctx,
Prefix::Discard => Ctx::DATA,
}
}
fn node_class(datum: &Datum<'_>, ctx: Ctx) -> Class {
match &datum.kind {
DatumKind::HashLiteral { .. } | DatumKind::LabelRef { .. } => Class::Data,
_ => ctx.class(),
}
}
pub fn walk<'a, 't, F>(data: &'a [Datum<'t>], mut visit: F)
where
F: FnMut(&'a Datum<'t>, Class) -> Walk,
{
for datum in data {
if walk_datum(datum, Ctx::TOP, &mut visit).is_break() {
return;
}
}
}
fn walk_datum<'a, 't, F>(datum: &'a Datum<'t>, ctx: Ctx, visit: &mut F) -> ControlFlow<()>
where
F: FnMut(&'a Datum<'t>, Class) -> Walk,
{
match visit(datum, node_class(datum, ctx)) {
Walk::Skip => return ControlFlow::Continue(()),
Walk::Stop => return ControlFlow::Break(()),
Walk::Descend => {}
}
match &datum.kind {
DatumKind::List { items, tail, .. } => {
for item in items {
walk_datum(item, ctx, visit)?;
}
if let Some(tail) = tail {
walk_datum(tail, ctx, visit)?;
}
}
DatumKind::Prefixed {
prefix, inner, arg, ..
} => {
if let Some(arg) = arg {
walk_datum(arg, Ctx::DATA, visit)?;
}
walk_datum(inner, inner_ctx(*prefix, ctx), visit)?;
}
DatumKind::HashLiteral {
inner: Some(inner), ..
} => {
walk_datum(inner, Ctx::DATA, visit)?;
}
DatumKind::Label { inner, .. } => {
walk_datum(inner, ctx, visit)?;
}
_ => {}
}
ControlFlow::Continue(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::options::Options;
use crate::reader::parse;
fn classes<'a>(src: &'a str, opts: &Options) -> Vec<(&'a str, Class)> {
let parsed = parse(src, opts);
let mut out = Vec::new();
walk(&parsed.data, |d, c| {
out.push((d.span.text(src), c));
Walk::Descend
});
out
}
fn class_of(src: &str, opts: &Options, needle: &str) -> Class {
classes(src, opts)
.into_iter()
.find(|(t, _)| *t == needle)
.unwrap_or_else(|| panic!("{needle:?} not visited in {src:?}"))
.1
}
#[test]
fn top_level_and_list_items_are_code() {
let s = Options::scheme();
assert_eq!(class_of("(f x)", &s, "(f x)"), Class::Code);
assert_eq!(class_of("(f x)", &s, "f"), Class::Code);
assert_eq!(class_of("(f x)", &s, "x"), Class::Code);
}
#[test]
fn quote_makes_inner_data_deep() {
let s = Options::scheme();
assert_eq!(class_of("'(a b)", &s, "'(a b)"), Class::Code); assert_eq!(class_of("'(a b)", &s, "(a b)"), Class::Data);
assert_eq!(class_of("'(a b)", &s, "a"), Class::Data);
assert_eq!(class_of("'(a b)", &s, "b"), Class::Data);
}
#[test]
fn quasiquote_unquote_flips_back() {
let s = Options::scheme();
assert_eq!(class_of("`(a ,b)", &s, "a"), Class::Data);
assert_eq!(class_of("`(a ,b)", &s, "b"), Class::Code);
}
#[test]
fn double_unquote_under_double_quasiquote_is_code() {
let s = Options::scheme();
assert_eq!(class_of("``(,,c)", &s, "c"), Class::Code);
}
#[test]
fn unquote_cannot_escape_hard_quote() {
let s = Options::scheme();
assert_eq!(class_of("'(,b)", &s, "b"), Class::Data);
}
#[test]
fn hash_literal_is_data() {
let s = Options::scheme();
assert_eq!(class_of("#(1 2 3)", &s, "#(1 2 3)"), Class::Data);
}
#[test]
fn function_quote_is_code() {
let c = Options::common_lisp();
assert_eq!(class_of("#'foo", &c, "foo"), Class::Code);
}
#[test]
fn deref_is_code() {
let c = Options::clojure();
assert_eq!(class_of("@x", &c, "x"), Class::Code);
}
#[test]
fn deref_inside_quasiquote_stays_data() {
let c = Options::clojure();
assert_eq!(class_of("`(f @x)", &c, "x"), Class::Data);
assert_eq!(class_of("`(f ~@y)", &c, "y"), Class::Code);
}
#[test]
fn function_quote_inside_quote_stays_data() {
let c = Options::common_lisp();
assert_eq!(class_of("'(f #'a)", &c, "a"), Class::Data);
}
#[test]
fn read_eval_is_code_even_under_quote() {
let c = Options::common_lisp();
assert_eq!(class_of("'(a #.(f))", &c, "(f)"), Class::Code);
}
#[test]
fn stop_aborts_the_walk() {
let s = Options::scheme();
let src = "(a b) (c d)";
let parsed = parse(src, &s);
let mut visited = Vec::new();
walk(&parsed.data, |d, _| {
visited.push(d.span.text(src));
if d.span.text(src) == "b" {
Walk::Stop
} else {
Walk::Descend
}
});
assert!(visited.contains(&"b"));
assert!(!visited.contains(&"(c d)"));
assert!(!visited.contains(&"c"));
}
#[test]
fn skip_prunes_quoted_subtree() {
let s = Options::scheme();
let src = "(a '(big list) b)";
let parsed = parse(src, &s);
let mut visited = Vec::new();
walk(&parsed.data, |d, class| {
visited.push(d.span.text(src));
if class == Class::Data {
Walk::Skip
} else {
Walk::Descend
}
});
assert!(visited.contains(&"'(big list)"));
assert!(visited.contains(&"(big list)"));
assert!(!visited.contains(&"big"));
assert!(!visited.contains(&"list"));
assert!(visited.contains(&"b"));
}
}