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::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 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())
}
#[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::layout::{DocLayout, project_with_layout};
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 = project_with_layout(&doc, &DocLayout::new());
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 = project_with_layout(&doc, &DocLayout::new());
let formatted = format_source_document(&source);
let reparsed = parse_document(&formatted);
assert_eq!(reparsed, doc);
}
}