fabula_dsl/lib.rs
1//! Text DSL parser for fabula patterns and graphs.
2//!
3//! Inspired by Kreminski et al. (2021) "Winnow: A Domain-Specific Language
4//! for Incremental Story Sifting" (AIIDE 2021). Extends Winnow with metric
5//! temporal constraints (Dechter/Meiri/Pearl 1991), strict variable scoping
6//! validation, and composition operators (Kreminski et al. 2025 FDG).
7//!
8//! Provides a human-readable syntax for defining temporal graph patterns
9//! and in-memory graphs, compiling them to fabula's core types.
10//!
11//! # Pattern syntax
12//!
13//! ```text
14//! pattern violation_of_hospitality {
15//! stage e1 {
16//! e1.eventType = "enterTown"
17//! e1.actor -> ?guest
18//! }
19//! stage e2 {
20//! e2.eventType = "showHospitality"
21//! e2.actor -> ?host
22//! e2.target -> ?guest
23//! }
24//! unless between e1 e2 {
25//! eMid.eventType = "leaveTown"
26//! eMid.actor -> ?guest
27//! }
28//! }
29//! ```
30//!
31//! # Graph syntax
32//!
33//! ```text
34//! graph {
35//! @1 ev1.eventType = "enterTown"
36//! @1 ev1.actor -> alice
37//! @2..5 ev2.eventType = "siege"
38//! now = 10
39//! }
40//! ```
41
42pub mod ast;
43pub mod compiler;
44pub mod error;
45pub mod lexer;
46pub mod parser;
47
48use ast::DocumentItem;
49use error::ParseError;
50use fabula::pattern::Pattern;
51use fabula_memory::{MemGraph, MemValue};
52use std::collections::HashMap;
53use std::fmt::Debug;
54
55pub use compiler::{compile_pattern_body, compile_pattern_body_with, MemMapper, TypeMapper};
56
57/// Result of parsing a document with patterns, graphs, and compose directives.
58///
59/// Generic over label and value types with defaults for backward compatibility.
60/// Use `ParsedDocument` (no params) for the default `Pattern<String, MemValue>`.
61pub struct ParsedDocument<L = String, V = MemValue> {
62 /// All compiled patterns (both directly declared and composed).
63 pub patterns: Vec<Pattern<L, V>>,
64 /// Parsed graphs (always `MemGraph` — graphs are test-only).
65 pub graphs: Vec<MemGraph>,
66}
67
68impl<L: Debug, V: Debug> std::fmt::Debug for ParsedDocument<L, V> {
69 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70 f.debug_struct("ParsedDocument")
71 .field("patterns", &self.patterns.len())
72 .field("graphs", &self.graphs.len())
73 .finish()
74 }
75}
76
77/// Parse a pattern DSL string into a compiled `Pattern<String, MemValue>`.
78pub fn parse_pattern(input: &str) -> Result<Pattern<String, MemValue>, ParseError> {
79 parse_pattern_with(input, &MemMapper)
80}
81
82/// Parse a pattern DSL string using a custom [`TypeMapper`].
83pub fn parse_pattern_with<M: TypeMapper>(
84 input: &str,
85 mapper: &M,
86) -> Result<Pattern<M::L, M::V>, ParseError> {
87 let tokens = lexer::Lexer::new(input).tokenize()?;
88 let ast = parser::Parser::new(tokens).parse_pattern_only()?;
89 compiler::compile_pattern_with(&ast, mapper)
90}
91
92/// Parse a graph DSL string into a `MemGraph`.
93pub fn parse_graph(input: &str) -> Result<MemGraph, ParseError> {
94 let tokens = lexer::Lexer::new(input).tokenize()?;
95 let ast = parser::Parser::new(tokens).parse_graph_only()?;
96 Ok(compiler::compile_graph(&ast))
97}
98
99/// Parse a document using the default `MemMapper`.
100///
101/// Items are processed in declaration order. Compose directives can reference
102/// any pattern or compose result defined before them (no forward references).
103pub fn parse_document(input: &str) -> Result<ParsedDocument, ParseError> {
104 parse_document_with(input, &MemMapper)
105}
106
107/// Parse a document using a custom [`TypeMapper`].
108///
109/// Patterns are compiled with the mapper; graphs are always `MemGraph`.
110pub fn parse_document_with<M: TypeMapper>(
111 input: &str,
112 mapper: &M,
113) -> Result<ParsedDocument<M::L, M::V>, ParseError> {
114 let tokens = lexer::Lexer::new(input).tokenize()?;
115 let doc = parser::Parser::new(tokens).parse_document()?;
116
117 let mut patterns = Vec::new();
118 let mut graphs = Vec::new();
119 let mut named: HashMap<String, Pattern<M::L, M::V>> = HashMap::new();
120
121 for item in &doc.items {
122 match item {
123 DocumentItem::Pattern(ast) => {
124 let pat = compiler::compile_pattern_with(ast, mapper)?;
125 named.insert(ast.name.clone(), pat.clone());
126 patterns.push(pat);
127 }
128 DocumentItem::Graph(ast) => {
129 graphs.push(compiler::compile_graph(ast));
130 }
131 DocumentItem::Compose(ast) => {
132 let composed = compiler::compile_compose_with(ast, &named, mapper)?;
133 for p in &composed {
134 named.insert(p.name.clone(), p.clone());
135 }
136 // For sequence/repeat (single result), the compose name
137 // is the pattern name. For choice (multiple results), also
138 // insert the group name pointing to the first alternative
139 // so chained composes can reference it.
140 if composed.len() > 1 && !named.contains_key(&ast.name) {
141 named.insert(ast.name.clone(), composed[0].clone());
142 }
143 patterns.extend(composed);
144 }
145 }
146 }
147
148 Ok(ParsedDocument { patterns, graphs })
149}