mod interpreter;
use eros::Union as _;
pub use eure_document::data_model;
use eure_document::document::constructor::ScopeError;
pub use eure_document::document::node::NodeValue;
pub use eure_document::document::*;
pub use eure_document::identifier;
use eure_document::identifier::IdentifierError;
pub use eure_document::must_be;
pub use eure_document::parse;
pub use eure_document::path;
pub use eure_document::text;
use eure_document::text::TextParseError;
pub use eure_document::value;
pub use eure_document::write;
use eure_parol::EureParseError;
use crate::document::interpreter::CstInterpreter;
use eure_tree::prelude::*;
use eure_tree::tree::InputSpan;
use std::collections::HashMap;
use thiserror::Error;
use eure_document::path::PathSegment;
use eure_document::source::{BindSource, Comment, SectionBody, SourceDocument, SourceId, Trivia};
use eure_document::value::ObjectKey;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct OriginMap {
pub definition: HashMap<NodeId, CstNodeId>,
pub value: HashMap<NodeId, CstNodeId>,
pub key: HashMap<(NodeId, ObjectKey), CstNodeId>,
pub key_span: HashMap<(NodeId, ObjectKey), InputSpan>,
pub key_span_by_cst: HashMap<(CstNodeId, ObjectKey), InputSpan>,
}
impl OriginMap {
pub fn new() -> Self {
Self::default()
}
pub fn record_definition(&mut self, node_id: NodeId, cst_node_id: CstNodeId) {
self.definition.entry(node_id).or_insert(cst_node_id);
}
pub fn record_value(&mut self, node_id: NodeId, cst_node_id: CstNodeId) {
self.value.insert(node_id, cst_node_id);
}
pub fn record_key(&mut self, map_node_id: NodeId, key: ObjectKey, cst_node_id: CstNodeId) {
self.key.insert((map_node_id, key), cst_node_id);
}
pub fn record_key_span(&mut self, map_node_id: NodeId, key: ObjectKey, span: InputSpan) {
self.key_span.insert((map_node_id, key), span);
}
pub fn record_key_span_by_cst(
&mut self,
cst_node_id: CstNodeId,
key: ObjectKey,
span: InputSpan,
) {
self.key_span_by_cst.insert((cst_node_id, key), span);
}
pub fn get_value_span(&self, node_id: NodeId, cst: &Cst) -> Option<InputSpan> {
self.value
.get(&node_id)
.and_then(|&cst_node_id| cst.span(cst_node_id))
}
pub fn get_definition_span(&self, node_id: NodeId, cst: &Cst) -> Option<InputSpan> {
self.definition
.get(&node_id)
.and_then(|&cst_node_id| cst.span(cst_node_id))
}
pub fn get_key_span(
&self,
map_node_id: NodeId,
key: &ObjectKey,
cst: &Cst,
) -> Option<InputSpan> {
if let Some(&span) = self.key_span.get(&(map_node_id, key.clone())) {
return Some(span);
}
self.key
.get(&(map_node_id, key.clone()))
.and_then(|&cst_node_id| cst.span(cst_node_id))
}
}
#[derive(Debug, Error, PartialEq, Eq, Clone)]
pub enum InlineCodeError {
#[error("Does not match InlineCode1 pattern")]
InvalidInlineCode1Pattern,
#[error("Does not match DelimCodeStart pattern")]
InvalidDelimCodeStartPattern,
}
#[derive(Debug, Error, PartialEq, Eq, Clone)]
pub enum CodeBlockError {
#[error("Does not match CodeBlockStart pattern")]
InvalidCodeBlockStartPattern,
#[error(transparent)]
ContentError(#[from] TextParseError),
}
#[derive(Debug, Error, Clone)]
pub enum DocumentConstructionError {
#[error(transparent)]
CstError(#[from] CstConstructError),
#[error("Invalid identifier: {0}")]
InvalidIdentifier(#[from] IdentifierError),
#[error("Failed to parse integer: {0}")]
InvalidInteger(String),
#[error("Failed to parse float: {0}")]
InvalidFloat(String),
#[error("Document insert error: {error}")]
DocumentInsert {
error: InsertError,
node_id: CstNodeId,
parent_node_id: Option<NodeId>,
},
#[error("Dynamic token not found: {0:?}")]
DynamicTokenNotFound(DynamicTokenId),
#[error("Failed to parse big integer: {0}")]
InvalidBigInt(String),
#[error("Invalid inline code at node {node_id:?}: {error}")]
InvalidInlineCode {
node_id: CstNodeId,
error: InlineCodeError,
},
#[error("Invalid code block at node {node_id:?}: {error}")]
InvalidCodeBlock {
node_id: CstNodeId,
error: CodeBlockError,
},
#[error("Invalid string key at node {node_id:?}: {error}")]
InvalidStringKey {
node_id: CstNodeId,
error: TextParseError,
},
#[error("Invalid key type at node {node_id:?}")]
InvalidKeyType { node_id: CstNodeId },
#[error("Failed to end scope: {0}")]
EndScope(#[from] ScopeError),
#[error("Failed to parse tuple index: {value}")]
InvalidTupleIndex { node_id: CstNodeId, value: String },
#[error("Invalid literal string at node {node_id:?}")]
InvalidLiteralStr { node_id: CstNodeId },
#[error(
"Float keys are not supported. Found '{value}'. Use integer keys like 'a.3.1' only when the pattern is <int>.<int>"
)]
InvalidFloatKey { node_id: CstNodeId, value: String },
}
impl DocumentConstructionError {
pub fn span(&self, cst: &Cst) -> Option<InputSpan> {
match self {
DocumentConstructionError::CstError(cst_error) => {
let node_id = match cst_error {
CstConstructError::UnexpectedNode { node, .. } => Some(*node),
CstConstructError::UnexpectedExtraNode { node } => Some(*node),
CstConstructError::UnexpectedEndOfChildren { parent } => Some(*parent),
CstConstructError::UnexpectedEmptyChildren { node } => Some(*node),
CstConstructError::NodeIdNotFound { node } => Some(*node),
CstConstructError::Error(_) => None,
};
node_id.and_then(|id| cst.span(id))
}
DocumentConstructionError::DocumentInsert { node_id, .. } => cst.span(*node_id),
DocumentConstructionError::InvalidInlineCode { node_id, .. } => cst.span(*node_id),
DocumentConstructionError::InvalidCodeBlock { node_id, .. } => cst.span(*node_id),
DocumentConstructionError::InvalidStringKey { node_id, .. } => cst.span(*node_id),
DocumentConstructionError::InvalidKeyType { node_id } => cst.span(*node_id),
DocumentConstructionError::InvalidFloatKey { node_id, .. } => cst.span(*node_id),
_ => None,
}
}
pub fn span_with_origin_map(&self, cst: &Cst, origins: &OriginMap) -> Option<InputSpan> {
match self {
DocumentConstructionError::DocumentInsert {
error,
node_id,
parent_node_id,
} => {
if let (Some(parent_id), Some(key)) = (parent_node_id, self.extract_key(error))
&& let Some(span) = origins.get_key_span(*parent_id, &key, cst)
{
return Some(span);
}
if let Some(key) = self.extract_key(error)
&& let Some(&span) = origins.key_span_by_cst.get(&(*node_id, key.clone()))
{
return Some(span);
}
cst.span(*node_id)
}
_ => self.span(cst),
}
}
fn extract_key(&self, error: &InsertError) -> Option<ObjectKey> {
match &error.kind {
InsertErrorKind::AlreadyAssigned { key } => Some(key.clone()),
InsertErrorKind::BindingTargetHasValue
| InsertErrorKind::ExpectedMap
| InsertErrorKind::ExpectedArray => {
error.path.0.last().and_then(|segment| {
if let PathSegment::Value(key) = segment {
Some(key.clone())
} else {
None
}
})
}
_ => None,
}
}
}
pub fn parse_to_document(
input: &str,
) -> eros::UResult<EureDocument, (EureParseError, DocumentConstructionError)> {
let tree = eure_parol::parse(input).union()?;
let document = cst_to_document(input, &tree).union()?;
Ok(document)
}
pub fn parse_to_source_document(
input: &str,
) -> eros::UResult<eure_document::source::SourceDocument, (EureParseError, DocumentConstructionError)>
{
let tree = eure_parol::parse(input).union()?;
let source = cst_to_source_document(input, &tree).union()?;
Ok(source)
}
pub fn cst_to_document(input: &str, cst: &Cst) -> Result<EureDocument, DocumentConstructionError> {
let mut visitor = CstInterpreter::new(input);
visitor.visit_root_handle(cst.root_handle(), cst)?;
Ok(visitor.into_document())
}
pub fn cst_to_source_document(
input: &str,
cst: &Cst,
) -> Result<eure_document::source::SourceDocument, DocumentConstructionError> {
let mut visitor = CstInterpreter::new(input);
cst.visit_from_root(&mut visitor)?;
let mut source = visitor.into_source_document();
annotate_source_trivia_from_input(&mut source, input);
Ok(source)
}
fn annotate_source_trivia_from_input(source: &mut SourceDocument, input: &str) {
let (chunks, trailing) = collect_statement_trivia_chunks(input);
let mut chunks = chunks.into_iter();
attach_source_trivia(source, source.root, &mut chunks);
if source.source(source.root).trailing_trivia.is_empty() && !trailing.is_empty() {
source.source_mut(source.root).trailing_trivia = trailing;
}
}
fn collect_statement_trivia_chunks(input: &str) -> (Vec<Vec<Trivia>>, Vec<Trivia>) {
let mut chunks = Vec::new();
let mut pending = Vec::new();
for line in input.lines() {
let trimmed = line.trim();
if trimmed.is_empty() {
pending.push(Trivia::BlankLine);
continue;
}
if let Some(comment) = parse_line_comment(trimmed) {
pending.push(Trivia::Comment(Comment::Line(comment)));
continue;
}
if let Some(comment) = parse_block_comment(trimmed) {
pending.push(Trivia::Comment(Comment::Block(comment)));
continue;
}
if trimmed == "}" || trimmed == "]" {
continue;
}
chunks.push(std::mem::take(&mut pending));
}
(chunks, pending)
}
fn parse_line_comment(trimmed: &str) -> Option<String> {
let comment = trimmed.strip_prefix("//")?;
Some(comment.strip_prefix(' ').unwrap_or(comment).to_string())
}
fn parse_block_comment(trimmed: &str) -> Option<String> {
let content = trimmed.strip_prefix("/*")?.strip_suffix("*/")?;
Some(content.trim().to_string())
}
fn attach_source_trivia(
source: &mut SourceDocument,
source_id: SourceId,
chunks: &mut impl Iterator<Item = Vec<Trivia>>,
) {
let binding_len = source.source(source_id).bindings.len();
for index in 0..binding_len {
let nested = {
let binding = &mut source.source_mut(source_id).bindings[index];
if binding.trivia_before.is_empty() {
binding.trivia_before = chunks.next().unwrap_or_default();
}
match binding.bind {
BindSource::Block(nested) => Some(nested),
_ => None,
}
};
if let Some(nested) = nested {
attach_source_trivia(source, nested, chunks);
}
}
let section_len = source.source(source_id).sections.len();
for index in 0..section_len {
let block_nested = {
let section = &mut source.source_mut(source_id).sections[index];
if section.trivia_before.is_empty() {
section.trivia_before = chunks.next().unwrap_or_default();
}
match &mut section.body {
SectionBody::Items { bindings, .. } => {
for binding in bindings {
if binding.trivia_before.is_empty() {
binding.trivia_before = chunks.next().unwrap_or_default();
}
}
None
}
SectionBody::Block(nested) => Some(*nested),
}
};
if let Some(nested) = block_nested {
attach_source_trivia(source, nested, chunks);
}
}
}
#[derive(Debug, Clone)]
pub struct DocumentConstructionErrorWithOriginMap {
pub error: DocumentConstructionError,
pub partial_origins: OriginMap,
}
impl std::fmt::Display for DocumentConstructionErrorWithOriginMap {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.error)
}
}
impl std::error::Error for DocumentConstructionErrorWithOriginMap {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.error)
}
}
pub fn cst_to_document_and_origin_map(
input: &str,
cst: &Cst,
) -> Result<(EureDocument, OriginMap), Box<DocumentConstructionErrorWithOriginMap>> {
let mut visitor = CstInterpreter::new(input);
match visitor.visit_root_handle(cst.root_handle(), cst) {
Ok(()) => Ok(visitor.into_document_and_origin_map()),
Err(error) => {
let (_, partial_origins) = visitor.into_document_and_origin_map();
Err(Box::new(DocumentConstructionErrorWithOriginMap {
error,
partial_origins,
}))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use eure_document::plan::LayoutPlan;
use eure_document::value::ObjectKey;
use eure_fmt::format_source_document;
use eure_tree::tree::CstFacade;
fn parse_document(input: &str) -> EureDocument {
let cst = eure_parol::parse(input).unwrap();
cst_to_document(input, &cst).unwrap()
}
fn find_node_by_kind(cst: &Cst, start: CstNodeId, kind: NonTerminalKind) -> Option<CstNodeId> {
if let Some(CstNode::NonTerminal {
kind: node_kind, ..
}) = cst.node_data(start)
&& node_kind == kind
{
return Some(start);
}
for child in cst.children(start) {
if let Some(found) = find_node_by_kind(cst, child, kind) {
return Some(found);
}
}
None
}
#[test]
fn test_shrunk_span_excludes_leading_trailing_trivia() {
let input = "\n foo = 1";
let cst = eure_parol::parse(input).unwrap();
let root = cst.root();
let binding_node =
find_node_by_kind(&cst, root, NonTerminalKind::Binding).expect("Should find Binding");
let original_span = cst.concrete_span(binding_node).unwrap();
let shrunk_span = cst.span(binding_node).unwrap();
let original_text = original_span.as_str(input);
let shrunk_text = shrunk_span.as_str(input);
assert_eq!(original_text, "\n foo = 1");
assert_eq!(
shrunk_text, "foo = 1",
"Shrunk span should be 'foo = 1', got '{}'",
shrunk_text
);
}
#[test]
fn test_shrunk_span_with_trailing_comment() {
let input = "foo = 1 // comment\n";
let cst = eure_parol::parse(input).unwrap();
let root = cst.root();
let binding_node =
find_node_by_kind(&cst, root, NonTerminalKind::Binding).expect("Should find Binding");
let original_span = cst.concrete_span(binding_node).unwrap();
let shrunk_span = cst.span(binding_node).unwrap();
let original_text = original_span.as_str(input);
let shrunk_text = shrunk_span.as_str(input);
assert_eq!(original_text, "foo = 1");
assert_eq!(shrunk_text, "foo = 1");
}
#[test]
fn test_shrunk_span_for_eure_root() {
let input = " \n foo = 1 \n ";
let cst = eure_parol::parse(input).unwrap();
let root = cst.root();
let eure_node =
find_node_by_kind(&cst, root, NonTerminalKind::Eure).expect("Should find Eure");
let original_span = cst.concrete_span(eure_node).unwrap();
let shrunk_span = cst.span(eure_node).unwrap();
let original_text = original_span.as_str(input);
let shrunk_text = shrunk_span.as_str(input);
assert_eq!(original_text, " \n foo = 1");
assert_eq!(
shrunk_text.trim(),
"foo = 1",
"Shrunk span should be 'foo = 1'"
);
}
#[test]
fn test_terminal_span_always_returned() {
let input = " foo = 1";
let cst = eure_parol::parse(input).unwrap();
fn find_whitespace(cst: &Cst, node_id: CstNodeId) -> Option<CstNodeId> {
if let Some(CstNode::Terminal {
kind: TerminalKind::Whitespace,
..
}) = cst.node_data(node_id)
{
return Some(node_id);
}
for child in cst.children(node_id) {
if let Some(found) = find_whitespace(cst, child) {
return Some(found);
}
}
None
}
let ws_node = find_whitespace(&cst, cst.root()).expect("Should find whitespace");
let ws_span = cst.span(ws_node).unwrap();
assert_eq!(
ws_span.as_str(input),
" ",
"Terminal span should be returned even for trivia"
);
}
#[test]
fn test_hole_key_creates_partial_map() {
let doc = parse_document("!x = 1");
let root = doc.node(doc.get_root_id());
assert!(matches!(root.content, NodeValue::PartialMap(_)));
}
#[test]
fn test_anonymous_hole_key_creates_partial_map() {
let doc = parse_document("! = 1");
let root = doc.node(doc.get_root_id());
assert!(matches!(root.content, NodeValue::PartialMap(_)));
}
#[test]
fn test_hole_key_in_block_syntax() {
let doc = parse_document("!a {\n x = 1\n}");
let root = doc.node(doc.get_root_id());
assert!(matches!(root.content, NodeValue::PartialMap(_)));
}
#[test]
fn test_hole_key_in_nested_path() {
let doc = parse_document("a.!b = 1");
let root = doc.node(doc.get_root_id());
let a_node = match &root.content {
NodeValue::Map(map) => map
.get(&ObjectKey::String("a".into()))
.copied()
.map(|node_id| doc.node(node_id)),
_ => None,
};
assert!(matches!(a_node, Some(node) if matches!(node.content, NodeValue::PartialMap(_))));
}
#[test]
fn test_mixed_map_hole_and_resolved_keys() {
let doc = parse_document("normal = 1\n!hole = 2");
let root = doc.node(doc.get_root_id());
assert!(matches!(root.content, NodeValue::PartialMap(_)));
}
#[test]
fn test_hole_in_tuple_key_creates_partial_map() {
let doc = parse_document("(1, !) = 1");
let root = doc.node(doc.get_root_id());
assert!(matches!(root.content, NodeValue::PartialMap(_)));
}
#[test]
fn test_partial_map_structural_equality() {
let input = "!x = 1";
let doc1 = {
let cst = eure_parol::parse(input).unwrap();
cst_to_document(input, &cst).unwrap()
};
let doc2 = {
let cst = eure_parol::parse(input).unwrap();
cst_to_document(input, &cst).unwrap()
};
assert_eq!(doc1, doc2);
}
#[test]
fn test_partial_map_different_labels_not_equal() {
let input1 = "!x = 1";
let input2 = "!y = 1";
let doc1 = {
let cst = eure_parol::parse(input1).unwrap();
cst_to_document(input1, &cst).unwrap()
};
let doc2 = {
let cst = eure_parol::parse(input2).unwrap();
cst_to_document(input2, &cst).unwrap()
};
assert_ne!(doc1, doc2);
}
#[test]
fn test_partial_map_format_round_trip() {
let input = "!x = 1\nnormal = 2";
let doc = parse_document(input);
let source = LayoutPlan::auto(doc.clone()).expect("layout plan").emit();
let formatted = format_source_document(&source);
let reparsed = parse_document(&formatted);
assert_eq!(reparsed, doc);
}
#[test]
fn test_partial_tuple_key_format_round_trip() {
let input = "(1, !) = 1";
let doc = parse_document(input);
let source = LayoutPlan::auto(doc.clone()).expect("layout plan").emit();
let formatted = format_source_document(&source);
let reparsed = parse_document(&formatted);
assert_eq!(reparsed, doc);
}
#[test]
fn test_parse_to_source_document_preserves_leading_comment() {
let input = "// hello\nvalue = 1\n";
let source = parse_to_source_document(input).expect("source parse");
assert_eq!(format_source_document(&source), input);
assert_eq!(source.root_source().bindings.len(), 1);
assert!(matches!(
source.root_source().bindings[0].trivia_before.first(),
Some(eure_document::source::Trivia::Comment(
eure_document::source::Comment::Line(comment)
)) if comment == "hello"
));
}
#[test]
fn test_parse_to_source_document_preserves_inline_array_binding() {
let input = "items = [1, 2, 3]\n";
let source = parse_to_source_document(input).expect("source parse");
assert_eq!(format_source_document(&source), input);
}
fn try_parse_document(input: &str) -> Result<EureDocument, DocumentConstructionError> {
let cst = eure_parol::parse(input).unwrap();
cst_to_document(input, &cst)
}
#[test]
fn test_current_index_merges_with_last_push() {
let actual = parse_document("users[].x = 1\nusers[^].y = 2\n");
let expected = parse_document("users[0].x = 1\nusers[0].y = 2\n");
assert_eq!(actual, expected);
}
#[test]
fn test_current_index_in_nested_arrays() {
let actual = parse_document(
"orgs[].teams[].members[].name = \"Ada\"\n\
orgs[^].teams[^].members[].name = \"Linus\"\n",
);
let expected = parse_document(
"orgs[0].teams[0].members[0].name = \"Ada\"\n\
orgs[0].teams[0].members[1].name = \"Linus\"\n",
);
assert_eq!(actual, expected);
}
#[test]
fn test_current_index_without_prior_push_errors() {
let err = try_parse_document("users[^].name = \"Alice\"").unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("Expected array"),
"expected ExpectedArray, got: {msg}"
);
}
#[test]
fn test_current_index_after_specific_index_errors() {
let err = try_parse_document("items[0].x = 1\nitems[^].y = 2\n").unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("no prior push"),
"expected ArrayCurrentOutOfScope, got: {msg}"
);
}
}