Skip to main content

qualia_core_db/sparql_library/parsers/
n3_star.rs

1//! N3-Star Parser for QualiaDB
2//!
3//! Implements RDF-Star (SPARQL 1.2) parsing for N3 syntax with embedded triples.
4//! N3-Star extends Turtle-Star with formulae, variables, and rules.
5
6use crate::lexicon::{generate_60bit_token, generate_embedded_triple_id};
7use crate::rdf_star::{RdfStarParseError, RdfStarParser};
8use crate::{q_hash, NQuin};
9
10/// N3-Star parser implementation
11pub struct N3StarParser {
12    /// Context hash for the current parsing session
13    context_hash: u64,
14    /// Variable bindings
15    variables: std::collections::HashMap<String, u64>,
16}
17
18impl N3StarParser {
19    /// Create a new N3-Star parser
20    pub fn new(context_hash: u64) -> Self {
21        Self {
22            context_hash,
23            variables: std::collections::HashMap::new(),
24        }
25    }
26
27    /// Bind a variable to a value
28    pub fn bind_variable(&mut self, var_name: &str, value: u64) {
29        self.variables.insert(var_name.to_string(), value);
30    }
31
32    /// Get a variable binding
33    pub fn get_variable(&self, var_name: &str) -> Option<u64> {
34        self.variables.get(var_name).copied()
35    }
36
37    /// Parse an N3 line
38    fn parse_line(&self, line: &str) -> Result<ParseResult, RdfStarParseError> {
39        let line = line.trim();
40        if line.is_empty() || line.starts_with('#') {
41            return Ok(ParseResult::Comment);
42        }
43
44        // Check for rule implications
45        if line.contains("=>") || line.contains("~>") || line.contains("^>") || line.contains("-o")
46        {
47            self.parse_rule(line)
48        } else if line.contains("{") {
49            self.parse_formula(line)
50        } else if line.contains("<<") {
51            self.parse_embedded_triple_line(line)
52        } else {
53            self.parse_triple_line(line)
54        }
55    }
56
57    /// Parse a rule with implication
58    fn parse_rule(&self, line: &str) -> Result<ParseResult, RdfStarParseError> {
59        // Format: { premise } => { conclusion }
60        // For now, we'll just recognize it and return a placeholder
61        let rule_type = if line.contains("~>") {
62            RuleType::Defeasible
63        } else if line.contains("^>") {
64            RuleType::Defeater
65        } else if line.contains("-o") {
66            RuleType::Linear
67        } else {
68            RuleType::Strict
69        };
70
71        Ok(ParseResult::Rule { rule_type })
72    }
73
74    /// Parse a formula (collection of triples)
75    fn parse_formula(&self, _line: &str) -> Result<ParseResult, RdfStarParseError> {
76        // Format: { s p o . s2 p2 o2 . }
77        // For now, just recognize it
78        Ok(ParseResult::Formula)
79    }
80
81    /// Parse a regular N3 triple
82    fn parse_triple_line(&self, line: &str) -> Result<ParseResult, RdfStarParseError> {
83        let parts: Vec<&str> = line.split_whitespace().collect();
84        if parts.len() < 3 {
85            return Err(RdfStarParseError::InvalidSyntax);
86        }
87
88        let subject = self.resolve_term(parts[0])?;
89        let predicate = self.resolve_term(parts[1])?;
90        let object = self.resolve_term(parts[2])?;
91
92        Ok(ParseResult::RegularTriple {
93            subject,
94            predicate,
95            object,
96            graph_hash: self.context_hash,
97        })
98    }
99
100    /// Parse an embedded triple line
101    fn parse_embedded_triple_line(&self, line: &str) -> Result<ParseResult, RdfStarParseError> {
102        let start = line
103            .find("<<")
104            .ok_or(RdfStarParseError::MalformedEmbeddedTriple)?;
105        let end = line
106            .find(">>")
107            .ok_or(RdfStarParseError::MalformedEmbeddedTriple)?;
108
109        let embedded_part = &line[start + 2..end];
110        let embedded_parts: Vec<&str> = embedded_part.split_whitespace().collect();
111        if embedded_parts.len() < 3 {
112            return Err(RdfStarParseError::MalformedEmbeddedTriple);
113        }
114
115        let subject = self.resolve_term(embedded_parts[0])?;
116        let predicate = self.resolve_term(embedded_parts[1])?;
117        let object = self.resolve_term(embedded_parts[2])?;
118
119        let virtual_id = generate_embedded_triple_id(subject, predicate, object);
120
121        let remaining = &line[end + 2..];
122        let outer_parts: Vec<&str> = remaining.split_whitespace().collect();
123        if outer_parts.len() < 2 {
124            return Err(RdfStarParseError::MalformedEmbeddedTriple);
125        }
126
127        let outer_predicate = self.resolve_term(outer_parts[0])?;
128        let outer_object = self.resolve_term(outer_parts[1])?;
129
130        Ok(ParseResult::EmbeddedTriple {
131            virtual_id,
132            components: [subject, predicate, object],
133            outer_predicate,
134            outer_object,
135            graph_hash: self.context_hash,
136        })
137    }
138
139    /// Resolve a term (IRI, blank node, or variable)
140    fn resolve_term(&self, term: &str) -> Result<u64, RdfStarParseError> {
141        // Check for variable
142        if term.starts_with('?') {
143            if let Some(value) = self.get_variable(term) {
144                return Ok(value);
145            }
146            // Generate a placeholder hash for unbound variables
147            return Ok(generate_60bit_token(term.as_bytes()));
148        }
149
150        // Strip angle brackets and quotes
151        let term = term
152            .trim_start_matches('<')
153            .trim_end_matches('>')
154            .trim_start_matches('"')
155            .trim_end_matches('"');
156
157        Ok(generate_60bit_token(term.as_bytes()))
158    }
159}
160
161impl RdfStarParser for N3StarParser {
162    fn parse_embedded_triple(
163        &mut self,
164        input: &[u8],
165    ) -> Result<(u64, [u64; 3]), RdfStarParseError> {
166        let line = std::str::from_utf8(input).map_err(|_| RdfStarParseError::InvalidUtf8)?;
167
168        match self.parse_line(line)? {
169            ParseResult::EmbeddedTriple {
170                virtual_id,
171                components,
172                ..
173            } => Ok((virtual_id, components)),
174            _ => Err(RdfStarParseError::MalformedEmbeddedTriple),
175        }
176    }
177
178    fn parse_triple(&mut self, input: &[u8]) -> Result<(u64, u64, u64), RdfStarParseError> {
179        let line = std::str::from_utf8(input).map_err(|_| RdfStarParseError::InvalidUtf8)?;
180
181        match self.parse_line(line)? {
182            ParseResult::RegularTriple {
183                subject,
184                predicate,
185                object,
186                ..
187            } => Ok((subject, predicate, object)),
188            _ => Err(RdfStarParseError::InvalidSyntax),
189        }
190    }
191
192    fn parse_quad(&mut self, input: &[u8]) -> Result<(u64, u64, u64, u64), RdfStarParseError> {
193        let line = std::str::from_utf8(input).map_err(|_| RdfStarParseError::InvalidUtf8)?;
194
195        match self.parse_line(line)? {
196            ParseResult::RegularTriple {
197                subject,
198                predicate,
199                object,
200                graph_hash,
201            } => Ok((subject, predicate, object, graph_hash)),
202            ParseResult::EmbeddedTriple {
203                outer_predicate,
204                outer_object,
205                graph_hash,
206                ..
207            } => Ok((0, outer_predicate, outer_object, graph_hash)),
208            _ => Err(RdfStarParseError::InvalidSyntax),
209        }
210    }
211
212    fn supports_quads(&self) -> bool {
213        true
214    }
215
216    fn supports_named_graphs(&self) -> bool {
217        false // N3 doesn't have named graphs like Trig
218    }
219
220    fn format_name(&self) -> &'static str {
221        "N3-Star"
222    }
223}
224
225/// Rule types in N3
226#[derive(Debug, Clone, Copy, PartialEq, Eq)]
227pub enum RuleType {
228    /// Classical modus ponens (=>)
229    Strict,
230    /// Defeasible (~>)
231    Defeasible,
232    /// Defeater (^>)
233    Defeater,
234    /// Linear logic (-o)
235    Linear,
236}
237
238/// Parse result for N3-Star
239enum ParseResult {
240    Comment,
241    Formula,
242    Rule {
243        #[allow(dead_code)]
244        rule_type: RuleType,
245    },
246    RegularTriple {
247        subject: u64,
248        predicate: u64,
249        object: u64,
250        graph_hash: u64,
251    },
252    EmbeddedTriple {
253        virtual_id: u64,
254        components: [u64; 3],
255        outer_predicate: u64,
256        outer_object: u64,
257        graph_hash: u64,
258    },
259}
260
261/// Stream all triples (and embedded-triple quins) from an N3/N3-Star reader
262/// into an [`ExternalSorter`]. Rules and formulae are skipped — only asserted
263/// triples and embedded-triple assertions are stored as NQuins.
264pub fn parse_n3_star_into<R: std::io::Read, S: crate::sparql_library::quin_sink::QuinSink>(
265    reader: R,
266    context_hash: u64,
267    sink: &mut S,
268) -> Result<u64, Box<dyn std::error::Error>> {
269    use std::io::BufRead;
270
271    let parser = N3StarParser::new(context_hash);
272    let mut count = 0u64;
273    let buf_reader = std::io::BufReader::new(reader);
274
275    for line in buf_reader.lines() {
276        let line = line?;
277        match parser.parse_line(&line)? {
278            ParseResult::Comment | ParseResult::Formula => continue,
279            ParseResult::Rule { rule_type } => {
280                let pred = q_hash("q42:n3RuleType");
281                let tag = rule_type as u64;
282                sink.push(NQuin {
283                    subject: context_hash,
284                    predicate: pred,
285                    object: tag,
286                    context: context_hash,
287                    metadata: 0,
288                    parity: context_hash ^ pred ^ tag ^ context_hash,
289                })?;
290                count += 1;
291            }
292            ParseResult::RegularTriple {
293                subject,
294                predicate,
295                object,
296                ..
297            } => {
298                sink.push(NQuin {
299                    subject,
300                    predicate,
301                    object,
302                    context: context_hash,
303                    metadata: 0,
304                    parity: subject ^ predicate ^ object ^ context_hash,
305                })?;
306                count += 1;
307            }
308            ParseResult::EmbeddedTriple {
309                virtual_id,
310                components,
311                outer_predicate,
312                outer_object,
313                ..
314            } => {
315                // Materialise the embedded (quoted) triple itself.
316                sink.push(NQuin {
317                    subject: virtual_id,
318                    predicate: components[1],
319                    object: components[2],
320                    context: context_hash,
321                    metadata: 0b10 << 61,
322                    parity: virtual_id ^ components[1] ^ components[2] ^ context_hash,
323                })?;
324                // Materialise the outer assertion about the quoted triple.
325                sink.push(NQuin {
326                    subject: components[0],
327                    predicate: outer_predicate,
328                    object: outer_object,
329                    context: context_hash,
330                    metadata: 0b10 << 61,
331                    parity: components[0] ^ outer_predicate ^ outer_object ^ context_hash,
332                })?;
333                count += 2;
334            }
335        }
336    }
337
338    Ok(count)
339}
340
341pub fn parse_n3_star_stream<R: std::io::Read>(
342    reader: R,
343    context_hash: u64,
344    sorter: &mut crate::external_sort::ExternalSorter,
345) -> Result<u64, Box<dyn std::error::Error>> {
346    parse_n3_star_into(reader, context_hash, sorter)
347}
348
349#[cfg(test)]
350mod tests {
351    use super::*;
352    use crate::rdf_star::RdfStarParser;
353
354    #[test]
355    fn test_n3_star_parser_creation() {
356        let parser = N3StarParser::new(0);
357        assert_eq!(parser.format_name(), "N3-Star");
358        assert!(parser.supports_quads());
359        assert!(!parser.supports_named_graphs());
360    }
361
362    #[test]
363    fn test_variable_binding() {
364        let mut parser = N3StarParser::new(0);
365        parser.bind_variable("?x", 123);
366        assert_eq!(parser.get_variable("?x"), Some(123));
367    }
368
369    #[test]
370    fn test_n3_star_parse_triple() {
371        let mut parser = N3StarParser::new(0);
372        let input = b"<http://example.org/s> <http://example.org/p> <http://example.org/o> .";
373        let result = parser.parse_triple(input);
374        assert!(result.is_ok());
375    }
376
377    #[test]
378    fn test_n3_star_parse_embedded() {
379        let mut parser = N3StarParser::new(0);
380        let input = b"<<http://example.org/Alice http://example.org/knows http://example.org/Bob>> http://example.org/saidBy http://example.org/Charlie .";
381        let result = parser.parse_embedded_triple(input);
382        assert!(result.is_ok());
383    }
384
385    #[test]
386    fn test_parse_regular_triple() {
387        let mut parser = N3StarParser::new(0);
388        let input =
389            b"<http://example.org/Alice> <http://example.org/knows> <http://example.org/Bob> .";
390        let result = parser.parse_triple(input);
391        assert!(result.is_ok());
392    }
393
394    #[test]
395    fn test_parse_rule() {
396        let parser = N3StarParser::new(0);
397        let input = b"{ ?x a :Person } => { ?x :hasName ?name } .";
398        let result = parser.parse_line(std::str::from_utf8(input).unwrap());
399        assert!(result.is_ok());
400    }
401}