use crate::ast::{Expr, SourceSpan, MAX_NESTING, TOO_DEEP};
use crate::shared::{Code, Fail};
pub(crate) const MESSAGES: [(&str, &str); 6] = [
(
"empty_step",
"a pipe step must be a symbol or a non-empty list",
),
(
"bad_def",
"def takes a name and a value, or a name, [params] and a body",
),
("bad_let", "let takes one binding [name value] and one body"),
("bad_if", "if takes a condition and exactly two branches"),
(
"bad_match",
"match takes a value and (case pattern body) clauses",
),
("too_deep", TOO_DEEP),
];
fn message(code: &str) -> &'static str {
MESSAGES
.iter()
.find(|(known, _)| *known == code)
.map_or("invalid form", |(_, message)| message)
}
fn fail(code: &str, span: &SourceSpan, src: &str) -> Fail {
let (row, col) = span.position(src);
Fail::new(Code::DslParseError, format!("{code}: {}", message(code))).at(row as u64, col as u64)
}
pub fn program(forms: Vec<Expr>, src: &str) -> Result<Vec<Expr>, Fail> {
forms.into_iter().map(|form| expr(form, src)).collect()
}
pub fn expr(form: Expr, src: &str) -> Result<Expr, Fail> {
deep(form, src).map(|form| form.expr)
}
struct Deep {
expr: Expr,
depth: usize,
}
fn deep(form: Expr, src: &str) -> Result<Deep, Fail> {
match form {
Expr::List { items, span } => rewrite(each(items, src)?, span, src),
Expr::Vector { items, span } => container(each(items, src)?, span, src, |items, span| {
Expr::Vector { items, span }
}),
atom => Ok(Deep {
expr: atom,
depth: 0,
}),
}
}
fn each(items: Vec<Expr>, src: &str) -> Result<Vec<Deep>, Fail> {
items.into_iter().map(|item| deep(item, src)).collect()
}
fn bounded(deep: Deep, span: &SourceSpan, src: &str) -> Result<Deep, Fail> {
if deep.depth > MAX_NESTING {
Err(fail("too_deep", span, src))
} else {
Ok(deep)
}
}
fn container(
items: Vec<Deep>,
span: SourceSpan,
src: &str,
make: impl FnOnce(Vec<Expr>, SourceSpan) -> Expr,
) -> Result<Deep, Fail> {
let depth = 1 + items.iter().map(|item| item.depth).max().unwrap_or(0);
let items = items.into_iter().map(|item| item.expr).collect();
bounded(
Deep {
expr: make(items, span.clone()),
depth,
},
&span,
src,
)
}
fn list(items: Vec<Deep>, span: SourceSpan, src: &str) -> Result<Deep, Fail> {
container(items, span, src, |items, span| Expr::List { items, span })
}
fn rewrite(items: Vec<Deep>, span: SourceSpan, src: &str) -> Result<Deep, Fail> {
match items.first().and_then(|head| head.expr.symbol()) {
Some("def") => def(items, span, src),
Some("pipe") => pipe(items, span, src),
Some("let") => shape(items, span, src, "bad_let", is_let),
Some("if") => shape(items, span, src, "bad_if", |items| items.len() == 4),
Some("match") => shape(items, span, src, "bad_match", is_match),
_ => list(items, span, src),
}
}
fn def(mut items: Vec<Deep>, span: SourceSpan, src: &str) -> Result<Deep, Fail> {
let named = items
.get(1)
.is_some_and(|name| name.expr.symbol().is_some());
match items.len() {
3 if named => list(items, span, src),
4 if named && matches!(items[2].expr, Expr::Vector { .. }) => {
let body = items.pop().expect("four items were counted");
let params = items.pop().expect("four items were counted");
let function = Deep {
depth: 1 + params.depth.max(body.depth),
expr: Expr::List {
items: vec![
Expr::Symbol {
name: "fn".into(),
span: span.clone(),
},
params.expr,
body.expr,
],
span: span.clone(),
},
};
items.push(function);
list(items, span, src)
}
_ => Err(fail("bad_def", &span, src)),
}
}
fn pipe(items: Vec<Deep>, span: SourceSpan, src: &str) -> Result<Deep, Fail> {
let mut steps = items.into_iter();
let head = steps.next().expect("a rewrite has a head");
let Some(mut acc) = steps.next() else {
return list(vec![head], span, src);
};
for step in steps {
acc = match step.expr {
Expr::Symbol { name, span: at } => Deep {
depth: acc.depth + 1,
expr: Expr::List {
items: vec![
Expr::Symbol {
name,
span: at.clone(),
},
acc.expr,
],
span: at,
},
},
Expr::List {
mut items,
span: at,
} if !items.is_empty() => {
let depth = step.depth.max(acc.depth + 1);
items.push(acc.expr);
Deep {
depth,
expr: Expr::List { items, span: at },
}
}
other => return Err(fail("empty_step", other.span(), src)),
};
acc = bounded(acc, &span, src)?;
}
Ok(acc)
}
fn shape(
items: Vec<Deep>,
span: SourceSpan,
src: &str,
code: &str,
ok: impl Fn(&[Deep]) -> bool,
) -> Result<Deep, Fail> {
if ok(&items) {
list(items, span, src)
} else {
Err(fail(code, &span, src))
}
}
fn is_let(items: &[Deep]) -> bool {
items.len() == 3
&& matches!(
&items[1].expr,
Expr::Vector { items: binding, .. }
if binding.len() == 2 && binding[0].symbol().is_some()
)
}
fn is_match(items: &[Deep]) -> bool {
items.len() >= 2
&& items[2..].iter().all(|clause| {
matches!(
&clause.expr,
Expr::List { items: parts, .. }
if parts.len() == 3 && parts[0].symbol() == Some("case")
)
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{canonical, parse};
fn core(src: &str) -> String {
let forms = parse(src).unwrap_or_else(|fail| panic!("{src:?}: {fail}"));
let core = program(forms, src).unwrap_or_else(|fail| panic!("{src:?}: {fail}"));
canonical(&core)
}
fn failure(src: &str) -> Fail {
let forms = parse(src).unwrap_or_else(|fail| panic!("{src:?}: {fail}"));
program(forms, src).expect_err("the program should not desugar")
}
#[test]
fn a_def_with_parameters_becomes_a_def_of_a_fn() {
assert_eq!(
core("def csv-field [value]\n scalar-text value"),
"(def csv-field (fn [value] (scalar-text value)))"
);
assert_eq!(core("def sep \",\""), "(def sep \",\")");
}
#[test]
fn the_generated_fn_carries_the_def_span() {
let src = "def f [x]\n x";
let core = program(parse(src).unwrap(), src).unwrap();
let Expr::List { items, span } = &core[0] else {
panic!("a def list");
};
let Expr::List {
items: function,
span: fn_span,
} = &items[2]
else {
panic!("a fn list");
};
assert_eq!(fn_span, span);
assert_eq!(function[0].span(), span);
assert_eq!((function[1].span().start, function[1].span().end), (6, 9));
assert_eq!((function[2].span().start, function[2].span().end), (12, 13));
}
#[test]
fn pipe_threads_the_value_data_last() {
assert_eq!(
core("pipe input (select (path \"payload\" \"records\" each-index)) (map normalize) (filter active?)"),
"(filter active? (map normalize (select (path \"payload\" \"records\" each-index) input)))"
);
assert_eq!(core("pipe x f g"), "(g (f x))");
assert_eq!(core("pipe x"), "x");
assert_eq!(core("(pipe)"), "(pipe)");
}
#[test]
fn a_pipe_inside_a_def_desugars_both() {
assert_eq!(
core("def export [input]\n pipe input\n table-from-json api-binding\n csv csv-options"),
"(def export (fn [input] (csv csv-options (table-from-json api-binding input))))"
);
}
#[test]
fn a_step_that_is_not_applicable_is_an_empty_step_error() {
for (src, col) in [
("pipe x ()", 8),
("pipe x 1", 8),
("pipe x \"s\"", 8),
("pipe x [f]", 8),
("pipe x :k", 8),
("pipe x\n f\n []", 3),
("pipe x\r ()", 2),
] {
let fail = failure(src);
assert_eq!(fail.code, Code::DslParseError, "{src:?}");
assert!(
fail.message.starts_with("empty_step: "),
"{src:?}: {}",
fail.message
);
assert_eq!(fail.column, Some(col), "{src:?}");
}
assert_eq!(failure("pipe x\n f\n []").row, Some(3));
}
#[test]
fn the_core_form_shapes_are_checked() {
assert_eq!(core("let [x 1] x"), "(let [x 1] x)");
assert_eq!(core("if a b c"), "(if a b c)");
assert_eq!(
core("match v\n case 1 \"one\"\n case _ \"other\""),
"(match v (case 1 \"one\") (case _ \"other\"))"
);
assert_eq!(core("match v"), "(match v)");
for (src, code) in [
("let [x] x", "bad_let"),
("let [x 1 2] x", "bad_let"),
("let [1 x] x", "bad_let"),
("let [x 1]", "bad_let"),
("if a b", "bad_if"),
("if a b c d", "bad_if"),
("(match)", "bad_match"),
("match v (case 1)", "bad_match"),
("match v (when 1 2)", "bad_match"),
("match v 1", "bad_match"),
("(def)", "bad_def"),
("def x", "bad_def"),
("def 1 2", "bad_def"),
("def x y z", "bad_def"),
("def x [a] b c", "bad_def"),
] {
let fail = failure(src);
assert!(
fail.message.starts_with(&format!("{code}: ")),
"{src:?}: {}",
fail.message
);
assert_eq!((fail.row, fail.column), (Some(1), Some(1)), "{src:?}");
}
}
#[test]
fn a_rewrite_that_nests_past_the_bound_is_too_deep_at_the_form() {
let at_limit = format!("pipe x{}", " f".repeat(MAX_NESTING));
assert_eq!(
core(&at_limit),
format!("{}x{}", "(f ".repeat(MAX_NESTING), ")".repeat(MAX_NESTING))
);
let fail = failure(&format!("pipe x{}", " f".repeat(MAX_NESTING + 1)));
assert!(fail.message.starts_with("too_deep: "), "{}", fail.message);
assert_eq!((fail.row, fail.column), (Some(1), Some(1)));
assert!(failure(&format!("pipe x{}", " f".repeat(100_000)))
.message
.starts_with("too_deep: "));
let body = |depth: usize| format!("{}x{}", "(".repeat(depth), ")".repeat(depth));
let fits = format!("def f [x] {}", body(MAX_NESTING - 2));
assert!(core(&fits).starts_with("(def f (fn [x] "));
let fail = failure(&format!("def f [x] {}", body(MAX_NESTING - 1)));
assert!(fail.message.starts_with("too_deep: "), "{}", fail.message);
}
#[test]
fn other_forms_pass_through_unchanged() {
assert_eq!(
core("fn [x] (get :label x)\n(case 1 2)\ndef x (pipe y f)"),
"(fn [x] (get :label x))\n(case 1 2)\n(def x (f y))"
);
}
}