Skip to main content

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}