use std::fmt;
use std::sync::Arc;
use tabnas::Value;
use crate::shared::{Code, Fail};
pub const MAX_NESTING: usize = 256;
pub(crate) const TOO_DEEP: &str = "nesting deeper than 256 levels";
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SourceSpan {
pub file: Arc<str>,
pub start: usize,
pub end: usize,
}
impl SourceSpan {
pub fn new(file: &Arc<str>, start: usize, end: usize) -> SourceSpan {
SourceSpan {
file: Arc::clone(file),
start,
end,
}
}
pub fn position(&self, src: &str) -> (usize, usize) {
let start = self.start.min(src.len());
let before = &src[..floor_boundary(src, start)];
let row = before.matches('\n').count() + 1;
let line_start = before.rfind(['\n', '\r']).map_or(0, |index| index + 1);
let col = before[line_start..].chars().count() + 1;
(row, col)
}
}
#[derive(Clone, Debug)]
pub struct Sources {
files: Arc<[(Arc<str>, Arc<str>)]>,
}
impl Sources {
pub fn one(file: &str, text: &str) -> Sources {
Sources {
files: Arc::from(vec![(Arc::from(file), Arc::from(text))]),
}
}
pub(crate) fn several(files: Vec<(Arc<str>, Arc<str>)>) -> Sources {
debug_assert!(!files.is_empty(), "a program has at least one source");
Sources {
files: Arc::from(files),
}
}
pub fn first(&self) -> &Arc<str> {
&self.files[0].0
}
pub fn names_files(&self) -> bool {
self.files.len() > 1
}
fn find(&self, span: &SourceSpan) -> Option<&str> {
self.files
.iter()
.find(|(file, _)| **file == *span.file)
.map(|(_, text)| &**text)
}
pub fn text_of(&self, span: &SourceSpan) -> &str {
self.find(span).unwrap_or(&self.files[0].1)
}
pub fn position(&self, span: &SourceSpan) -> (usize, usize) {
span.position(self.text_of(span))
}
pub fn fail_at(&self, fail: Fail, span: &SourceSpan) -> Fail {
let (row, col) = self.position(span);
let fail = fail.at(row as u64, col as u64);
if self.names_files() && self.find(span).is_some() {
fail.in_file(&*span.file)
} else {
fail
}
}
}
fn floor_boundary(text: &str, index: usize) -> usize {
let mut index = index.min(text.len());
while !text.is_char_boundary(index) {
index -= 1;
}
index
}
impl fmt::Display for SourceSpan {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}..{}", self.file, self.start, self.end)
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum Expr {
Symbol { name: String, span: SourceSpan },
Keyword { name: String, span: SourceSpan },
Str { value: String, span: SourceSpan },
Num { lexeme: String, span: SourceSpan },
Bool { value: bool, span: SourceSpan },
Null { span: SourceSpan },
List { items: Vec<Expr>, span: SourceSpan },
Vector { items: Vec<Expr>, span: SourceSpan },
}
fn malformed(what: impl fmt::Display) -> Fail {
Fail::new(
Code::DslParseError,
format!("malformed reader output: {what}"),
)
}
fn field<'v>(fields: &'v indexmap::IndexMap<String, Value>, name: &str) -> Result<&'v Value, Fail> {
fields
.get(name)
.ok_or_else(|| malformed(format!("a node without {name:?}")))
}
fn text(fields: &indexmap::IndexMap<String, Value>, name: &str) -> Result<String, Fail> {
match field(fields, name)? {
Value::String(text) => Ok(text.clone()),
other => Err(malformed(format!("{name:?} is not a string: {other}"))),
}
}
fn offset(value: &Value) -> Result<usize, Fail> {
match value {
Value::Number(n) if n.is_finite() && *n >= 0.0 && n.fract() == 0.0 => Ok(*n as usize),
other => Err(malformed(format!(
"a span offset that is not a whole number: {other}"
))),
}
}
enum Node<'v> {
Atom(Expr),
Container {
list: bool,
span: SourceSpan,
items: &'v [Value],
},
}
struct Frame<'v> {
list: bool,
span: SourceSpan,
items: std::slice::Iter<'v, Value>,
built: Vec<Expr>,
}
impl Frame<'_> {
fn finish(self) -> Expr {
if self.list {
Expr::List {
items: self.built,
span: self.span,
}
} else {
Expr::Vector {
items: self.built,
span: self.span,
}
}
}
}
fn read_node<'v>(value: &'v Value, file: &Arc<str>) -> Result<Node<'v>, Fail> {
let Value::Object(fields) = value else {
return Err(malformed(format!("a node that is not an object: {value}")));
};
let span = match field(fields, "span")? {
Value::Array(pair) if pair.len() == 2 => {
SourceSpan::new(file, offset(&pair[0])?, offset(&pair[1])?)
}
other => return Err(malformed(format!("a span that is not a pair: {other}"))),
};
let tag = text(fields, "$")?;
Ok(match tag.as_str() {
"sym" => Node::Atom(Expr::Symbol {
name: text(fields, "name")?,
span,
}),
"kw" => Node::Atom(Expr::Keyword {
name: text(fields, "name")?,
span,
}),
"str" => Node::Atom(Expr::Str {
value: text(fields, "value")?,
span,
}),
"num" => Node::Atom(Expr::Num {
lexeme: text(fields, "lexeme")?,
span,
}),
"bool" => match field(fields, "value")? {
Value::Bool(value) => Node::Atom(Expr::Bool {
value: *value,
span,
}),
other => return Err(malformed(format!("a bool that is not a bool: {other}"))),
},
"null" => Node::Atom(Expr::Null { span }),
"list" | "vector" => match field(fields, "items")? {
Value::Array(items) => Node::Container {
list: tag == "list",
span,
items: items.as_slice(),
},
other => return Err(malformed(format!("items that are not an array: {other}"))),
},
other => return Err(malformed(format!("an unknown tag {other:?}"))),
})
}
fn too_deep(span: &SourceSpan) -> Fail {
Fail::new(
Code::DslParseError,
format!("too_deep: {TOO_DEEP} (at {span})"),
)
}
impl Expr {
pub fn from_value(value: &Value, file: &Arc<str>) -> Result<Expr, Fail> {
let mut stack: Vec<Frame<'_>> = Vec::new();
let mut pending = value;
loop {
let mut built = match read_node(pending, file)? {
Node::Atom(expr) => expr,
Node::Container { list, span, items } => {
if stack.len() >= MAX_NESTING {
return Err(too_deep(&span));
}
let mut frame = Frame {
list,
span,
items: items.iter(),
built: Vec::with_capacity(items.len()),
};
match frame.items.next() {
Some(first) => {
stack.push(frame);
pending = first;
continue;
}
None => frame.finish(),
}
}
};
loop {
let Some(mut frame) = stack.pop() else {
return Ok(built);
};
frame.built.push(built);
match frame.items.next() {
Some(item) => {
pending = item;
stack.push(frame);
break;
}
None => built = frame.finish(),
}
}
}
}
pub fn program_from_value(value: &Value, file: &Arc<str>) -> Result<Vec<Expr>, Fail> {
match value {
Value::Array(forms) => forms
.iter()
.map(|form| Expr::from_value(form, file))
.collect(),
other => Err(malformed(format!(
"a program that is not an array: {other}"
))),
}
}
pub fn span(&self) -> &SourceSpan {
match self {
Expr::Symbol { span, .. }
| Expr::Keyword { span, .. }
| Expr::Str { span, .. }
| Expr::Num { span, .. }
| Expr::Bool { span, .. }
| Expr::Null { span }
| Expr::List { span, .. }
| Expr::Vector { span, .. } => span,
}
}
pub fn symbol(&self) -> Option<&str> {
match self {
Expr::Symbol { name, .. } => Some(name),
_ => None,
}
}
pub fn is_atom(&self) -> bool {
!matches!(self, Expr::List { .. } | Expr::Vector { .. })
}
pub fn same_shape(&self, other: &Expr) -> bool {
match (self, other) {
(Expr::Symbol { name: a, .. }, Expr::Symbol { name: b, .. })
| (Expr::Keyword { name: a, .. }, Expr::Keyword { name: b, .. })
| (Expr::Str { value: a, .. }, Expr::Str { value: b, .. })
| (Expr::Num { lexeme: a, .. }, Expr::Num { lexeme: b, .. }) => a == b,
(Expr::Bool { value: a, .. }, Expr::Bool { value: b, .. }) => a == b,
(Expr::Null { .. }, Expr::Null { .. }) => true,
(Expr::List { items: a, .. }, Expr::List { items: b, .. })
| (Expr::Vector { items: a, .. }, Expr::Vector { items: b, .. }) => same_program(a, b),
_ => false,
}
}
}
pub fn same_program(a: &[Expr], b: &[Expr]) -> bool {
a.len() == b.len() && a.iter().zip(b).all(|(x, y)| x.same_shape(y))
}
pub fn canonical_form(expr: &Expr) -> String {
let mut out = String::new();
write_canonical(expr, &mut out);
out
}
fn write_canonical(expr: &Expr, out: &mut String) {
match expr {
Expr::Symbol { name, .. } => out.push_str(name),
Expr::Keyword { name, .. } => {
out.push(':');
out.push_str(name);
}
Expr::Str { value, .. } => out.push_str(&json_string(value)),
Expr::Num { lexeme, .. } => out.push_str(lexeme),
Expr::Bool { value, .. } => out.push_str(if *value { "true" } else { "false" }),
Expr::Null { .. } => out.push_str("null"),
Expr::List { items, .. } => write_items(items, '(', ')', out),
Expr::Vector { items, .. } => write_items(items, '[', ']', out),
}
}
fn write_items(items: &[Expr], open: char, close: char, out: &mut String) {
out.push(open);
for (index, item) in items.iter().enumerate() {
if index > 0 {
out.push(' ');
}
write_canonical(item, out);
}
out.push(close);
}
fn json_string(value: &str) -> String {
serde_json::to_string(value).unwrap_or_else(|_| {
String::from("\"\"")
})
}
pub fn canonical(program: &[Expr]) -> String {
program
.iter()
.map(canonical_form)
.collect::<Vec<_>>()
.join("\n")
}
pub fn format(program: &[Expr]) -> String {
let mut out = String::new();
for (index, form) in program.iter().enumerate() {
if index > 0 {
out.push('\n');
}
write_layout(form, 0, &mut out);
}
out
}
fn is_inline(expr: &Expr) -> bool {
!matches!(expr, Expr::List { .. })
}
fn write_layout(expr: &Expr, depth: usize, out: &mut String) {
let indent = " ".repeat(depth);
let Expr::List { items, .. } = expr else {
out.push_str(&indent);
out.push_str(&canonical_form(expr));
out.push('\n');
return;
};
let head = items.iter().take_while(|item| is_inline(item)).count();
if items.len() < 2 || head == 0 {
out.push_str(&indent);
out.push_str(&canonical_form(expr));
out.push('\n');
return;
}
out.push_str(&indent);
out.push_str(
&items[..head]
.iter()
.map(canonical_form)
.collect::<Vec<_>>()
.join(" "),
);
out.push('\n');
for child in &items[head..] {
write_layout(child, depth + 1, out);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parse;
fn sym(name: &str) -> Expr {
Expr::Symbol {
name: name.into(),
span: SourceSpan::new(&Arc::from("t"), 0, 0),
}
}
#[test]
fn sources_position_a_span_in_its_file_and_name_it_when_several() {
let a: Arc<str> = Arc::from("a.alc");
let b: Arc<str> = Arc::from("b.alc");
let one = Sources::one("a.alc", "x\ny");
let f = one.fail_at(
Fail::new(Code::InputInvalid, "m"),
&SourceSpan::new(&a, 2, 3),
);
assert_eq!(
(f.row, f.column, f.file.as_deref()),
(Some(2), Some(1), None)
);
assert_eq!(f.to_string(), "INPUT_INVALID: m (2:1)");
let two = Sources::several(vec![
(a.clone(), Arc::from("x\ny")),
(b.clone(), Arc::from("\n\nz")),
]);
let f = two.fail_at(
Fail::new(Code::InputInvalid, "m"),
&SourceSpan::new(&b, 2, 3),
);
assert_eq!(
(f.row, f.column, f.file.as_deref()),
(Some(3), Some(1), Some("b.alc"))
);
assert_eq!(f.to_string(), "INPUT_INVALID: m (b.alc:3:1)");
let f = two.fail_at(
Fail::new(Code::InputInvalid, "m"),
&SourceSpan::new(&a, 2, 3),
);
assert_eq!(
(f.row, f.column, f.file.as_deref()),
(Some(2), Some(1), Some("a.alc"))
);
let other: Arc<str> = Arc::from("stdlib/table.alc");
let f = two.fail_at(
Fail::new(Code::InputInvalid, "m"),
&SourceSpan::new(&other, 2, 3),
);
assert_eq!((f.row, f.column, f.file), (Some(2), Some(1), None));
}
#[test]
fn positions_are_one_based_rows_and_character_columns() {
let src = "ab\ncdé f\n";
let file = Arc::from("t");
assert_eq!(SourceSpan::new(&file, 0, 1).position(src), (1, 1));
assert_eq!(SourceSpan::new(&file, 3, 4).position(src), (2, 1));
assert_eq!(SourceSpan::new(&file, 8, 9).position(src), (2, 5));
assert_eq!(SourceSpan::new(&file, 99, 99).position(src), (3, 1));
assert_eq!(SourceSpan::new(&file, 3, 4).position("a\r b"), (1, 2));
assert_eq!(SourceSpan::new(&file, 3, 4).position("a\r\nb"), (2, 1));
}
#[test]
fn same_shape_ignores_spans_and_nothing_else() {
let a = parse("a (b 1) [\"x\"]").unwrap();
let b = parse("a\n b 1\n [\"x\"]").unwrap();
assert!(same_program(&a, &b));
assert!(!same_program(&a, &parse("a (b 2) [\"x\"]").unwrap()));
assert!(!sym("a").same_shape(&Expr::Keyword {
name: "a".into(),
span: sym("a").span().clone()
}));
}
#[test]
fn canonical_prints_every_atom_kind() {
let program = parse("s :k \"a\\\"b\\n\" -1.5e3 true false null [] ()").unwrap();
assert_eq!(
canonical(&program),
r#"(s :k "a\"b\n" -1.5e3 true false null [] ())"#
);
}
#[test]
fn layout_keeps_explicit_parens_only_where_layout_cannot_express_the_shape() {
let program = parse("(concat prefix (newline) suffix)\n((f x) y)\n(a (b c) d)").unwrap();
assert_eq!(
format(&program),
"concat prefix\n (newline)\n suffix\n\n((f x) y)\n\na\n b c\n d\n"
);
}
fn nested_tree(depth: usize) -> Value {
fn node(tag: &str, fields: Vec<(&str, Value)>) -> Value {
let mut object = indexmap::IndexMap::new();
object.insert("$".to_string(), Value::String(tag.into()));
for (name, value) in fields {
object.insert(name.to_string(), value);
}
object.insert(
"span".to_string(),
Value::array(vec![Value::Number(0.0), Value::Number(1.0)]),
);
Value::object(object)
}
let mut tree = node("sym", vec![("name", Value::String("x".into()))]);
for _ in 0..depth {
tree = node("list", vec![("items", Value::array(vec![tree]))]);
}
tree
}
#[test]
fn a_tagged_tree_at_the_bound_converts_and_one_past_it_is_too_deep() {
let file = Arc::from("t");
let at_limit = Expr::from_value(&nested_tree(MAX_NESTING), &file).expect("converts");
assert_eq!(
canonical_form(&at_limit),
format!("{}x{}", "(".repeat(MAX_NESTING), ")".repeat(MAX_NESTING))
);
let fail = Expr::from_value(&nested_tree(MAX_NESTING + 1), &file).expect_err("too deep");
assert_eq!(fail.code, Code::DslParseError);
assert!(fail.message.starts_with("too_deep: "), "{}", fail.message);
}
#[test]
fn a_far_deeper_tagged_tree_is_refused_without_recursing_into_it() {
let file = Arc::from("t");
let fail = Expr::from_value(&nested_tree(1_000), &file).expect_err("too deep");
assert!(fail.message.starts_with("too_deep: "), "{}", fail.message);
}
#[test]
fn malformed_reader_output_is_a_parse_error_not_a_panic() {
let file = Arc::from("t");
let bad = Value::String("nope".into());
let fail = Expr::from_value(&bad, &file).expect_err("not a node");
assert_eq!(fail.code, Code::DslParseError);
let mut fields = indexmap::IndexMap::new();
fields.insert("$".to_string(), Value::String("sym".into()));
fields.insert("span".to_string(), Value::array(vec![Value::Number(0.0)]));
assert!(Expr::from_value(&Value::object(fields), &file).is_err());
}
}