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 Region {
Code,
SealedData,
PorousData,
}
impl Region {
pub fn class(self) -> Class {
match self {
Region::Code => Class::Code,
Region::SealedData | Region::PorousData => Class::Data,
}
}
pub fn is_prunable(self) -> bool {
matches!(self, Region::SealedData)
}
}
#[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 region(self) -> Region {
if self.hard_quote {
Region::SealedData
} else if self.qq > 0 {
Region::PorousData
} else {
Region::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_region(datum: &Datum<'_>, ctx: Ctx) -> Region {
match &datum.kind {
DatumKind::HashLiteral { .. } | DatumKind::LabelRef { .. } => Region::SealedData,
_ => ctx.region(),
}
}
pub fn walk<'a, 't, F>(data: &'a [Datum<'t>], mut visit: F)
where
F: FnMut(&'a Datum<'t>, Class) -> Walk,
{
walk_regions(data, |datum, region| visit(datum, region.class()));
}
pub fn walk_regions<'a, 't, F>(data: &'a [Datum<'t>], mut visit: F)
where
F: FnMut(&'a Datum<'t>, Region) -> 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>, Region) -> Walk,
{
match visit(datum, node_region(datum, ctx)) {
Walk::Skip => return ControlFlow::Continue(()),
Walk::Stop => return ControlFlow::Break(()),
Walk::Descend => {}
}
let mut flow = ControlFlow::Continue(());
for_each_child(datum, ctx, |child, cctx| {
if flow.is_continue() {
flow = walk_datum(child, cctx, visit);
}
});
flow
}
fn for_each_child<'a, 't>(datum: &'a Datum<'t>, ctx: Ctx, mut f: impl FnMut(&'a Datum<'t>, Ctx)) {
match &datum.kind {
DatumKind::List { items, tail, .. } => {
for item in items {
f(item, ctx);
}
if let Some(tail) = tail {
f(tail, ctx);
}
}
DatumKind::Prefixed {
prefix, inner, arg, ..
} => {
if let Some(arg) = arg {
f(arg, Ctx::DATA);
}
f(inner, inner_ctx(*prefix, ctx));
}
DatumKind::HashLiteral {
inner: Some(inner), ..
} => f(inner, Ctx::DATA),
DatumKind::Label { inner, .. } => f(inner, ctx),
_ => {}
}
}
pub struct CodeNodes<'a, 't> {
stack: Vec<(&'a Datum<'t>, Ctx)>,
}
impl<'a, 't> Iterator for CodeNodes<'a, 't> {
type Item = &'a Datum<'t>;
fn next(&mut self) -> Option<Self::Item> {
while let Some((datum, ctx)) = self.stack.pop() {
let region = node_region(datum, ctx);
if region == Region::SealedData {
continue;
}
let start = self.stack.len();
for_each_child(datum, ctx, |child, cctx| self.stack.push((child, cctx)));
self.stack[start..].reverse();
if region == Region::Code {
return Some(datum);
}
}
None
}
}
impl std::iter::FusedIterator for CodeNodes<'_, '_> {}
pub fn code_nodes<'a, 't>(data: &'a [Datum<'t>]) -> CodeNodes<'a, 't> {
CodeNodes {
stack: data.iter().rev().map(|d| (d, Ctx::TOP)).collect(),
}
}
#[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"));
}
fn regions<'a>(src: &'a str, opts: &Options) -> Vec<(&'a str, Region)> {
let parsed = parse(src, opts);
let mut out = Vec::new();
walk_regions(&parsed.data, |d, r| {
out.push((d.span.text(src), r));
Walk::Descend
});
out
}
fn region_of(src: &str, opts: &Options, needle: &str) -> Region {
regions(src, opts)
.into_iter()
.find(|(t, _)| *t == needle)
.unwrap_or_else(|| panic!("{needle:?} not visited in {src:?}"))
.1
}
#[test]
fn hard_quote_is_sealed_quasiquote_template_is_porous() {
let s = Options::scheme();
assert_eq!(region_of("'(a b)", &s, "(a b)"), Region::SealedData);
assert_eq!(region_of("`(a ,b)", &s, "(a ,b)"), Region::PorousData);
assert_eq!(region_of("`(a ,b)", &s, "b"), Region::Code);
assert_eq!(region_of("#(1 2 3)", &s, "#(1 2 3)"), Region::SealedData);
}
#[test]
fn only_sealed_data_is_prunable() {
assert!(Region::SealedData.is_prunable());
assert!(!Region::PorousData.is_prunable());
assert!(!Region::Code.is_prunable());
assert_eq!(Region::SealedData.class(), Class::Data);
assert_eq!(Region::PorousData.class(), Class::Data);
assert_eq!(Region::Code.class(), Class::Code);
}
#[test]
fn walk_class_matches_walk_regions_class_for_every_node() {
let cases = [
(Options::scheme(), "(f x)"),
(Options::scheme(), "'(a b)"),
(Options::scheme(), "`(a ,b)"),
(Options::scheme(), "``(,,c)"),
(Options::scheme(), "'(a ,b)"),
(Options::scheme(), "#(1 2 3)"),
(Options::scheme(), "(let ((x 1)) `(v ,x '(w)))"),
(Options::common_lisp(), "'(f #'a #.(g))"),
(Options::common_lisp(), "#+sbcl (defun only () 1)"),
];
for (opts, src) in &cases {
let via_class = classes(src, opts);
let via_region: Vec<(&str, Class)> = regions(src, opts)
.into_iter()
.map(|(t, r)| (t, r.class()))
.collect();
assert_eq!(via_class, via_region, "mismatch for {src:?}");
}
}
#[test]
fn blanket_skip_on_binary_data_drops_quasiquoted_code() {
let s = Options::scheme();
let src = "`(a ,(f y))";
let parsed = parse(src, &s);
let mut code_lists = Vec::new();
walk(&parsed.data, |d, class| {
if class == Class::Data {
return Walk::Skip;
}
if matches!(d.kind, DatumKind::List { .. }) {
code_lists.push(d.span.text(src));
}
Walk::Descend
});
assert!(!code_lists.contains(&"(f y)"));
}
#[test]
fn region_pruning_keeps_quasiquoted_code_but_prunes_sealed() {
let s = Options::scheme();
let src = "`(a ,(f y) '(b c))";
let parsed = parse(src, &s);
let mut code_lists = Vec::new();
let mut visited = Vec::new();
walk_regions(&parsed.data, |d, region| {
visited.push(d.span.text(src));
if region.is_prunable() {
return Walk::Skip;
}
if region == Region::Code && matches!(d.kind, DatumKind::List { .. }) {
code_lists.push(d.span.text(src));
}
Walk::Descend
});
assert!(code_lists.contains(&"(f y)"));
assert!(visited.contains(&"(b c)"));
assert!(!visited.contains(&"b"));
assert!(!visited.contains(&"c"));
}
fn code_texts<'a>(src: &'a str, opts: &Options) -> Vec<&'a str> {
let parsed = parse(src, opts);
code_nodes(&parsed.data).map(|d| d.span.text(src)).collect()
}
#[test]
fn code_nodes_yields_code_preorder_and_prunes_sealed() {
let s = Options::scheme();
assert_eq!(
code_texts("(f '(a b) x)", &s),
vec!["(f '(a b) x)", "f", "'(a b)", "x"],
);
}
#[test]
fn code_nodes_descends_porous_to_reach_unquoted_code() {
let s = Options::scheme();
let got = code_texts("`(a ,(f y))", &s);
assert!(got.contains(&"(f y)"));
assert!(got.contains(&"y"));
assert!(!got.contains(&"a"));
}
#[test]
fn code_nodes_is_fused_after_exhaustion() {
let s = Options::scheme();
let parsed = parse("(f x)", &s);
let mut it = code_nodes(&parsed.data);
while it.next().is_some() {}
assert!(it.next().is_none());
assert!(it.next().is_none());
}
#[test]
fn code_nodes_is_lazy_and_short_circuits() {
let s = Options::scheme();
let parsed = parse("(a (b (c (d e))))", &s);
let first_b = code_nodes(&parsed.data).find(|d| d.span.text("(a (b (c (d e))))") == "b");
assert!(first_b.is_some());
}
#[test]
fn code_nodes_matches_walk_regions_code_classification() {
let s = Options::scheme();
for src in [
"(f x)",
"`(a ,(f y) '(b c))",
"(a '(b c) d)",
"``(,,c)",
"'(a ,b)",
"(let ((x 1)) `(v ,x))",
] {
let parsed = parse(src, &s);
let via_iter: Vec<&str> = code_nodes(&parsed.data).map(|d| d.span.text(src)).collect();
let mut via_visitor = Vec::new();
walk_regions(&parsed.data, |d, r| {
if r == Region::Code {
via_visitor.push(d.span.text(src));
}
Walk::Descend
});
assert_eq!(via_iter, via_visitor, "mismatch for {src:?}");
}
}
#[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"));
}
}