Skip to main content

qualia_core_db/sparql_library/parsers/
turtle_star.rs

1//! Turtle-Star Parser for QualiaDB
2//!
3//! Implements RDF-Star (SPARQL 1.2) parsing for Turtle syntax with embedded triples.
4//! Stack-based state machine for zero-allocation handling of deeply nested structures.
5//!
6//! Architecture:
7//! - Fixed-size stack array [StackFrame; 16] for nested embedded triples
8//! - No heap allocations in hot path
9//! - Virtual IDs minted via generate_embedded_triple_id()
10//! - Context stored separately in NQuin field (not in Virtual ID hash)
11
12use crate::lexicon::{generate_60bit_token, generate_embedded_triple_id};
13use crate::rdf_star::{RdfStarParseError, RdfStarParser};
14use crate::NQuin;
15use std::io::{BufRead, BufReader, Read};
16
17/// Maximum nesting depth for embedded triples
18const MAX_NESTING_DEPTH: usize = 16;
19
20/// Parsing state for a single frame
21#[repr(C)]
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23enum ParsingState {
24    ExpectSubject,
25    ExpectPredicate,
26    ExpectObject,
27    ExpectEmbeddedEnd,
28}
29
30/// Stack frame for tracking nested embedded triple parsing
31#[repr(C)]
32#[derive(Debug, Clone, Copy)]
33struct StackFrame {
34    subject: Option<u64>,
35    predicate: Option<u64>,
36    object: Option<u64>,
37    parsing_state: ParsingState,
38}
39
40impl StackFrame {
41    fn new() -> Self {
42        Self {
43            subject: None,
44            predicate: None,
45            object: None,
46            parsing_state: ParsingState::ExpectSubject,
47        }
48    }
49}
50
51/// Zero-allocation parser stack
52#[repr(C)]
53struct ParserStack {
54    frames: [StackFrame; MAX_NESTING_DEPTH],
55    depth: usize,
56}
57
58impl ParserStack {
59    fn new() -> Self {
60        Self {
61            frames: [StackFrame::new(); MAX_NESTING_DEPTH],
62            depth: 0,
63        }
64    }
65
66    fn push(&mut self, frame: StackFrame) -> Result<(), RdfStarParseError> {
67        if self.depth >= MAX_NESTING_DEPTH {
68            return Err(RdfStarParseError::BufferOverflow);
69        }
70        self.frames[self.depth] = frame;
71        self.depth += 1;
72        Ok(())
73    }
74
75    fn pop(&mut self) -> Option<StackFrame> {
76        if self.depth == 0 {
77            return None;
78        }
79        self.depth -= 1;
80        Some(self.frames[self.depth])
81    }
82
83    fn current(&mut self) -> &mut StackFrame {
84        &mut self.frames[self.depth - 1]
85    }
86
87    fn is_empty(&self) -> bool {
88        self.depth == 0
89    }
90
91    fn depth(&self) -> usize {
92        self.depth
93    }
94}
95
96/// Turtle-Star parser implementation
97pub struct TurtleStarParser {
98    /// Context hash for the current parsing session
99    context_hash: u64,
100    /// Stack for nested embedded triple parsing
101    stack: ParserStack,
102}
103
104impl TurtleStarParser {
105    /// Create a new Turtle-Star parser
106    pub fn new(context_hash: u64) -> Self {
107        Self {
108            context_hash,
109            stack: ParserStack::new(),
110        }
111    }
112
113    /// Session context hash stamped onto emitted quads.
114    pub fn session_context(&self) -> u64 {
115        self.context_hash
116    }
117
118    /// Whether the embedded-triple stack is empty (no in-flight `<<` frames).
119    pub fn stack_is_empty(&self) -> bool {
120        self.stack.is_empty()
121    }
122
123    fn expect_state(
124        frame: &mut StackFrame,
125        expected: ParsingState,
126        next: ParsingState,
127    ) -> Result<(), RdfStarParseError> {
128        if frame.parsing_state != expected {
129            return Err(RdfStarParseError::MalformedEmbeddedTriple);
130        }
131        frame.parsing_state = next;
132        Ok(())
133    }
134
135    /// Parse a Turtle-Star token (IRI, literal, or delimiter)
136    ///
137    /// Returns Ok(Some(hash)) for valid IRIs/literals, Ok(None) for delimiters
138    fn parse_token(&self, input: &[u8], pos: &mut usize) -> Result<Option<u64>, RdfStarParseError> {
139        let bytes = &input[*pos..];
140
141        // Skip whitespace
142        let mut start = 0;
143        while start < bytes.len() && bytes[start].is_ascii_whitespace() {
144            start += 1;
145        }
146
147        if start >= bytes.len() {
148            return Ok(None);
149        }
150
151        let ch = bytes[start];
152
153        // Check for embedded triple start
154        if ch == b'<' && start + 1 < bytes.len() && bytes[start + 1] == b'<' {
155            *pos += start + 2;
156            return Ok(None); // Signal embedded triple start
157        }
158
159        // Check for embedded triple end
160        if ch == b'>' && start + 1 < bytes.len() && bytes[start + 1] == b'>' {
161            *pos += start + 2;
162            return Ok(None); // Signal embedded triple end
163        }
164
165        // Check for statement terminator
166        if ch == b'.' {
167            *pos += start + 1;
168            return Ok(None);
169        }
170
171        // Check for semicolon (predicate separator in Turtle)
172        if ch == b';' {
173            *pos += start + 1;
174            return Ok(None);
175        }
176
177        // Parse IRI or literal (simplified - proper Turtle would have <> delimiters)
178        let mut end = start;
179        while end < bytes.len()
180            && !bytes[end].is_ascii_whitespace()
181            && bytes[end] != b'.'
182            && bytes[end] != b';'
183        {
184            end += 1;
185        }
186
187        if start == end {
188            return Ok(None);
189        }
190
191        *pos += end;
192
193        // Hash the token
194        let token =
195            std::str::from_utf8(&bytes[start..end]).map_err(|_| RdfStarParseError::InvalidUtf8)?;
196        let hash = generate_60bit_token(token.as_bytes());
197
198        Ok(Some(hash))
199    }
200
201    /// Parse an embedded triple using stack-based state machine
202    fn parse_embedded_triple_internal(
203        &mut self,
204        input: &[u8],
205        pos: &mut usize,
206    ) -> Result<(u64, [u64; 3]), RdfStarParseError> {
207        // Push new frame for embedded triple
208        let frame = StackFrame::new();
209        self.stack.push(frame)?;
210
211        let _start_depth = self.stack.depth();
212
213        // Parse subject
214        {
215            let frame = self.stack.current();
216            Self::expect_state(
217                frame,
218                ParsingState::ExpectSubject,
219                ParsingState::ExpectPredicate,
220            )?;
221        }
222        while *pos < input.len() && input[*pos].is_ascii_whitespace() {
223            *pos += 1;
224        }
225        if *pos + 1 < input.len() && input[*pos] == b'<' && input[*pos + 1] == b'<' {
226            *pos += 2; // skip <<
227            let (hash, _) = self.parse_embedded_triple_internal(input, pos)?;
228            self.stack.current().subject = Some(hash);
229        } else {
230            match self.parse_token(input, pos)? {
231                Some(hash) => self.stack.current().subject = Some(hash),
232                None => return Err(RdfStarParseError::MalformedEmbeddedTriple),
233            }
234        }
235
236        // Parse predicate
237        {
238            let frame = self.stack.current();
239            Self::expect_state(
240                frame,
241                ParsingState::ExpectPredicate,
242                ParsingState::ExpectObject,
243            )?;
244        }
245        match self.parse_token(input, pos)? {
246            Some(hash) => self.stack.current().predicate = Some(hash),
247            None => return Err(RdfStarParseError::MalformedEmbeddedTriple),
248        }
249
250        // Parse object (could be another embedded triple)
251        {
252            let frame = self.stack.current();
253            Self::expect_state(
254                frame,
255                ParsingState::ExpectObject,
256                ParsingState::ExpectEmbeddedEnd,
257            )?;
258        }
259        while *pos < input.len() && input[*pos].is_ascii_whitespace() {
260            *pos += 1;
261        }
262        if *pos + 1 < input.len() && input[*pos] == b'<' && input[*pos + 1] == b'<' {
263            *pos += 2; // skip <<
264            let (hash, _) = self.parse_embedded_triple_internal(input, pos)?;
265            self.stack.current().object = Some(hash);
266        } else {
267            match self.parse_token(input, pos)? {
268                Some(hash) => self.stack.current().object = Some(hash),
269                None => {
270                    return Err(RdfStarParseError::MalformedEmbeddedTriple);
271                }
272            }
273        }
274
275        // Expect >> terminator
276        {
277            let frame = self.stack.current();
278            Self::expect_state(
279                frame,
280                ParsingState::ExpectEmbeddedEnd,
281                ParsingState::ExpectSubject,
282            )?;
283        }
284        while *pos + 1 < input.len() && input[*pos].is_ascii_whitespace() {
285            *pos += 1;
286        }
287        if *pos + 1 >= input.len() || input[*pos] != b'>' || input[*pos + 1] != b'>' {
288            return Err(RdfStarParseError::MalformedEmbeddedTriple);
289        }
290        *pos += 2;
291
292        // Pop frame and get components
293        if !self.stack.is_empty() && self.stack.depth() > 1 {
294            // Nested frames remain for future recursive `<<` support.
295        }
296        let frame = self
297            .stack
298            .pop()
299            .ok_or(RdfStarParseError::MalformedEmbeddedTriple)?;
300
301        let subject = frame
302            .subject
303            .ok_or(RdfStarParseError::MalformedEmbeddedTriple)?;
304        let predicate = frame
305            .predicate
306            .ok_or(RdfStarParseError::MalformedEmbeddedTriple)?;
307        let object = frame
308            .object
309            .ok_or(RdfStarParseError::MalformedEmbeddedTriple)?;
310
311        // Generate Virtual ID (context-independent per architectural decision)
312        let virtual_id = generate_embedded_triple_id(subject, predicate, object);
313
314        Ok((virtual_id, [subject, predicate, object]))
315    }
316
317    /// Parse a full Turtle-Star annotation statement: `<< s p o >> annP annO [.]`.
318    ///
319    /// Returns `(virtual_id, [s,p,o], Option<(ann_predicate, ann_object)>)`. Unlike
320    /// `parse_embedded_triple` (which stops at `>>` and drops everything after),
321    /// this captures the outer predicate/object when present, so the caller can
322    /// emit the linking statement `virtual_id annP annO` — reaching parity with
323    /// the N-Quads-Star parser. A single parse pass handles both the annotated
324    /// and the bare-embedded-triple cases (the latter yields `None` for the
325    /// annotation), so there is no stateful re-parse of the same line.
326    fn parse_star_annotation(
327        &mut self,
328        input: &[u8],
329    ) -> Result<(u64, [u64; 3], Option<(u64, u64)>), RdfStarParseError> {
330        let mut pos = 0;
331        while pos < input.len() && input[pos].is_ascii_whitespace() {
332            pos += 1;
333        }
334        if pos + 1 < input.len() && input[pos] == b'<' && input[pos + 1] == b'<' {
335            pos += 2;
336        } else {
337            return Err(RdfStarParseError::MalformedEmbeddedTriple);
338        }
339        let (virtual_id, components) = self.parse_embedded_triple_internal(input, &mut pos)?;
340        // Outer predicate + object are optional: a line may be a bare `<< s p o >>`.
341        let annotation = match self.parse_token(input, &mut pos)? {
342            Some(ann_predicate) => self
343                .parse_token(input, &mut pos)?
344                .map(|ann_object| (ann_predicate, ann_object)),
345            None => None,
346        };
347        Ok((virtual_id, components, annotation))
348    }
349}
350
351impl RdfStarParser for TurtleStarParser {
352    fn parse_embedded_triple(
353        &mut self,
354        input: &[u8],
355    ) -> Result<(u64, [u64; 3]), RdfStarParseError> {
356        let mut pos = 0;
357        while pos < input.len() && input[pos].is_ascii_whitespace() {
358            pos += 1;
359        }
360        if pos + 1 < input.len() && input[pos] == b'<' && input[pos + 1] == b'<' {
361            pos += 2;
362        }
363        self.parse_embedded_triple_internal(input, &mut pos)
364    }
365
366    fn parse_triple(&mut self, input: &[u8]) -> Result<(u64, u64, u64), RdfStarParseError> {
367        let mut pos = 0;
368
369        let skip_ws = |p: &mut usize| {
370            while *p < input.len() && input[*p].is_ascii_whitespace() {
371                *p += 1;
372            }
373        };
374
375        skip_ws(&mut pos);
376        let subject = if pos + 1 < input.len() && input[pos] == b'<' && input[pos + 1] == b'<' {
377            pos += 2; // skip <<
378            let (hash, _) = self.parse_embedded_triple_internal(input, &mut pos)?;
379            // parse_token might have stopped at '>>'
380            // skip '>>'
381            skip_ws(&mut pos);
382            if pos + 1 < input.len() && input[pos] == b'>' && input[pos + 1] == b'>' {
383                pos += 2;
384            }
385            hash
386        } else {
387            match self.parse_token(input, &mut pos)? {
388                Some(h) => h,
389                None => return Err(RdfStarParseError::InvalidSyntax),
390            }
391        };
392
393        skip_ws(&mut pos);
394        let predicate = match self.parse_token(input, &mut pos)? {
395            Some(h) => h,
396            None => return Err(RdfStarParseError::InvalidSyntax),
397        };
398
399        skip_ws(&mut pos);
400        let object = if pos + 1 < input.len() && input[pos] == b'<' && input[pos + 1] == b'<' {
401            pos += 2; // skip <<
402            let (hash, _) = self.parse_embedded_triple_internal(input, &mut pos)?;
403            skip_ws(&mut pos);
404            if pos + 1 < input.len() && input[pos] == b'>' && input[pos + 1] == b'>' {
405                pos += 2;
406            }
407            hash
408        } else {
409            match self.parse_token(input, &mut pos)? {
410                Some(h) => h,
411                None => return Err(RdfStarParseError::InvalidSyntax),
412            }
413        };
414
415        // The object is the final token; this parser returns the triple, not the cursor,
416        // so `pos`'s final position is intentionally not propagated past here.
417        let _ = pos;
418        Ok((subject, predicate, object))
419    }
420
421    fn parse_quad(&mut self, _input: &[u8]) -> Result<(u64, u64, u64, u64), RdfStarParseError> {
422        // Turtle-Star doesn't support quads natively (use Trig-Star for that)
423        Err(RdfStarParseError::UnsupportedFeature)
424    }
425
426    fn supports_quads(&self) -> bool {
427        false
428    }
429
430    fn supports_named_graphs(&self) -> bool {
431        false
432    }
433
434    fn format_name(&self) -> &'static str {
435        "Turtle-Star"
436    }
437}
438
439/// Legacy function for backward compatibility with existing ingest pipeline
440///
441/// TODO: This should be refactored to use the RdfStarParser trait properly
442/// and integrate with the lexicon writing layer for 24-byte embedded triple storage.
443pub fn parse_turtle_star_into<R: Read, S: crate::sparql_library::quin_sink::QuinSink>(
444    reader: R,
445    context_hash: u64,
446    sink: &mut S,
447) -> Result<u64, Box<dyn std::error::Error>> {
448    let mut parser = TurtleStarParser::new(context_hash);
449    let mut count = 0;
450    let buf_reader = BufReader::new(reader);
451
452    for line in buf_reader.lines() {
453        let line = line?;
454        let l = line.trim();
455        if l.is_empty() || l.starts_with('#') || l.starts_with('@') {
456            continue;
457        }
458
459        // Convert to bytes for parser
460        let bytes = l.as_bytes();
461
462        // Check if line contains embedded triple marker
463        if l.contains("<<") {
464            // Parse the full annotation form `<< s p o >> annP annO` in one pass.
465            if let Ok((virtual_id, components, annotation)) = parser.parse_star_annotation(bytes) {
466                // Linking statement: the quoted triple's virtual id is the subject
467                // of the outer annotation (parity with the N-Quads-Star parser).
468                // This is what lets `lookup_embedded_triple` — and hence the
469                // result serializer — resolve `<<s p o>>` instead of falling back
470                // to a raw `<<{hex}>>` placeholder. Bare `<< s p o >>` lines with
471                // no outer predicate/object yield `None` and just emit the inner
472                // triple, as before.
473                if let Some((ann_predicate, ann_object)) = annotation {
474                    sink.push(NQuin {
475                        subject: virtual_id,
476                        predicate: ann_predicate,
477                        object: ann_object,
478                        context: context_hash,
479                        metadata: 0b10 << 61,
480                        parity: 0,
481                    })?;
482                    count += 1;
483                }
484
485                // Inner triple (its components), so the quoted triple is itself
486                // a real, queryable statement in the graph.
487                sink.push(NQuin {
488                    subject: components[0],
489                    predicate: components[1],
490                    object: components[2],
491                    context: context_hash, // Context in NQuin field, not Virtual ID
492                    metadata: 0b10 << 61,
493                    parity: 0,
494                })?;
495                count += 1;
496
497                // NOTE: the durable `virtual_id -> [s,p,o]` lexicon entry
498                // (`LexiconEntry::EmbeddedTriple`) is written by the volume
499                // builder (`q42/q42_volume.rs`), which now sees the virtual id via
500                // the linking statement above; the streaming sink itself has no
501                // embedded-entry channel yet (a future expansion).
502            }
503        } else {
504            // Parse regular triple
505            if let Ok((subject, predicate, object)) = parser.parse_triple(bytes) {
506                sink.push(NQuin {
507                    subject,
508                    predicate,
509                    object,
510                    context: context_hash,
511                    metadata: 0b10 << 61,
512                    parity: 0,
513                })?;
514                count += 1;
515            }
516        }
517    }
518
519    Ok(count)
520}
521
522pub fn parse_turtle_star_stream<R: Read>(
523    reader: R,
524    context_hash: u64,
525    sorter: &mut crate::external_sort::ExternalSorter,
526) -> Result<u64, Box<dyn std::error::Error>> {
527    parse_turtle_star_into(reader, context_hash, sorter)
528}
529
530#[cfg(test)]
531mod tests {
532    use super::*;
533    use crate::rdf_star::RdfStarParser;
534
535    #[test]
536    fn test_turtle_star_parser_creation() {
537        let parser = TurtleStarParser::new(0);
538        assert_eq!(parser.format_name(), "Turtle-Star");
539        assert!(!parser.supports_quads());
540        assert!(!parser.supports_named_graphs());
541        assert_eq!(parser.stack.depth(), 0);
542    }
543
544    #[test]
545    fn test_parser_stack_push_pop() {
546        let mut stack = ParserStack::new();
547        assert!(stack.is_empty());
548
549        let frame = StackFrame::new();
550        stack.push(frame).unwrap();
551        assert_eq!(stack.depth(), 1);
552        assert!(!stack.is_empty());
553
554        let popped = stack.pop();
555        assert!(popped.is_some());
556        assert!(stack.is_empty());
557    }
558
559    #[test]
560    fn test_parser_stack_overflow() {
561        let mut stack = ParserStack::new();
562        let frame = StackFrame::new();
563
564        // Fill to capacity
565        for _ in 0..MAX_NESTING_DEPTH {
566            stack.push(frame).unwrap();
567        }
568
569        // Should overflow
570        assert!(stack.push(frame).is_err());
571    }
572
573    #[test]
574    fn test_parse_simple_triple() {
575        let mut parser = TurtleStarParser::new(0);
576        let input = b"Alice knows Bob";
577        let result = parser.parse_triple(input);
578        assert!(result.is_ok());
579        let (s, p, o) = result.unwrap();
580        assert_ne!(s, 0);
581        assert_ne!(p, 0);
582        assert_ne!(o, 0);
583    }
584
585    #[test]
586    fn test_parse_embedded_triple() {
587        let mut parser = TurtleStarParser::new(0);
588        let input = b"<< Alice knows Bob >>"; // Simplified for testing
589        let result = parser.parse_embedded_triple(input);
590        assert!(result.is_ok());
591        let (virtual_id, components) = result.unwrap();
592        assert_ne!(virtual_id, 0);
593        assert_ne!(components[0], 0);
594        assert_ne!(components[1], 0);
595        assert_ne!(components[2], 0);
596
597        // Verify stack was properly managed
598        assert_eq!(parser.stack.depth(), 0);
599    }
600
601    struct VecSink(Vec<NQuin>);
602    impl crate::sparql_library::quin_sink::QuinSink for VecSink {
603        fn push(&mut self, q: NQuin) -> std::io::Result<()> {
604            self.0.push(q);
605            Ok(())
606        }
607    }
608
609    #[test]
610    fn parse_star_annotation_captures_outer_predicate_object() {
611        let mut parser = TurtleStarParser::new(0);
612        let (virtual_id, components, ann) = parser
613            .parse_star_annotation(b"<< Alice knows Bob >> certainty high")
614            .unwrap();
615        assert_eq!(
616            virtual_id,
617            generate_embedded_triple_id(components[0], components[1], components[2])
618        );
619        let (ann_p, ann_o) = ann.expect("outer annotation predicate/object");
620        assert_ne!(ann_p, 0);
621        assert_ne!(ann_o, 0);
622        assert_eq!(parser.stack.depth(), 0);
623    }
624
625    #[test]
626    fn parse_turtle_star_into_emits_linking_statement_and_inner_triple() {
627        let mut sink = VecSink(Vec::new());
628        let count =
629            parse_turtle_star_into(&b"<< Alice knows Bob >> certainty high\n"[..], 0, &mut sink)
630                .unwrap();
631        assert_eq!(count, 2, "expected outer linking statement + inner triple");
632        // sink[0] is the outer statement; its subject must be the virtual id of
633        // the inner triple (sink[1]) so lookup_embedded_triple can resolve it.
634        let inner = sink.0[1];
635        let vid = generate_embedded_triple_id(inner.subject, inner.predicate, inner.object);
636        assert_eq!(sink.0[0].subject, vid);
637    }
638
639    #[test]
640    fn parse_turtle_star_into_bare_embedded_triple_emits_only_inner() {
641        let mut sink = VecSink(Vec::new());
642        let count = parse_turtle_star_into(&b"<< Alice knows Bob >>\n"[..], 0, &mut sink).unwrap();
643        assert_eq!(count, 1, "bare embedded triple → just the inner triple");
644    }
645
646    #[test]
647    fn test_virtual_id_context_independence() {
648        use crate::lexicon::TAG_EMBEDDED;
649
650        // Same triple should generate same Virtual ID regardless of context
651        let context1 = 12345u64;
652        let context2 = 67890u64;
653
654        let mut parser1 = TurtleStarParser::new(context1);
655        let mut parser2 = TurtleStarParser::new(context2);
656
657        let input = b"<< Alice knows Bob >>";
658        let (vid1, _) = parser1.parse_embedded_triple(input).unwrap();
659        let (vid2, _) = parser2.parse_embedded_triple(input).unwrap();
660
661        assert_eq!(vid1, vid2, "Virtual ID should be context-independent");
662        assert_ne!(vid1 & TAG_EMBEDDED, 0, "TAG_EMBEDDED bit should be set");
663    }
664}
665
666/// Turtle-Star Serializer
667///
668/// Converts Virtual IDs and component hashes back to Turtle-Star syntax.
669// ---------------------------------------------------------------------------
670// Shared term-resolution helpers for the RDF-Star text serializers.
671//
672// Every text serializer below previously emitted the raw u64 term *hash* as a
673// decimal number (e.g. `<1> <2> <3> .`), which is not valid RDF and cannot be
674// consumed by any RDF tool — and the quoted-triple form used a non-standard,
675// non-round-tripping `<<<…>>>` (the parser reads `<<…>>`). These resolve each
676// hash to its real surface form through the same resolver used by the
677// N-Triples fast path: subjects/predicates as `<iri>` (or a `did:q42:ptr/…`
678// pointer), objects additionally distinguishing inline-typed literals
679// (`"42"^^<…#integer>`).
680// ---------------------------------------------------------------------------
681
682/// Resolve a subject/predicate term to its bracketed surface form.
683fn star_iri_term(val: u64) -> String {
684    let mut buf = Vec::new();
685    let _ = crate::query::resolver::write_iri_term(val, &mut buf);
686    String::from_utf8_lossy(&buf).into_owned()
687}
688
689/// Resolve an object term, distinguishing inline-typed literals from IRIs.
690fn star_object_term(val: u64) -> String {
691    let mut buf = Vec::new();
692    let _ = crate::query::resolver::write_object_term(val, &mut buf);
693    String::from_utf8_lossy(&buf).into_owned()
694}
695
696/// Resolve a term to a bare IRI string (no angle brackets), for JSON-LD.
697fn star_iri_bare(val: u64) -> String {
698    if let Some(bytes) = crate::resolver::resolve_hash(val) {
699        String::from_utf8_lossy(bytes).into_owned()
700    } else if (val & crate::resolver::MSB_FLAG) != 0 {
701        format!("did:q42:ptr/{:016x}", val & !crate::resolver::MSB_FLAG)
702    } else {
703        format!("quin:hash/{val:016x}")
704    }
705}
706
707/// Minimal JSON string escaping.
708fn star_json_escape(s: &str) -> String {
709    let mut out = String::with_capacity(s.len() + 2);
710    for c in s.chars() {
711        match c {
712            '"' => out.push_str("\\\""),
713            '\\' => out.push_str("\\\\"),
714            '\n' => out.push_str("\\n"),
715            '\r' => out.push_str("\\r"),
716            '\t' => out.push_str("\\t"),
717            c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
718            c => out.push(c),
719        }
720    }
721    out
722}
723
724/// Resolve an object term as a JSON-LD value node (`{"@id": …}` for IRIs,
725/// `{"@value": …, "@type": …}` for inline-typed literals). Lexicon-first, to
726/// match `write_object_term`.
727fn star_object_jsonld(val: u64) -> String {
728    if crate::resolver::resolve_hash(val).is_some() || (val & crate::resolver::MSB_FLAG) != 0 {
729        return format!(
730            r#"{{ "@id": "{}" }}"#,
731            star_json_escape(&star_iri_bare(val))
732        );
733    }
734    if let Some(lit) = crate::resolver::classify_inline_literal(val) {
735        return format!(
736            r#"{{ "@value": "{}", "@type": "{}" }}"#,
737            star_json_escape(&lit.to_string()),
738            star_json_escape(lit.datatype_iri())
739        );
740    }
741    format!(
742        r#"{{ "@id": "{}" }}"#,
743        star_json_escape(&star_iri_bare(val))
744    )
745}
746
747pub struct TurtleStarSerializer;
748
749impl TurtleStarSerializer {
750    pub fn new() -> Self {
751        Self
752    }
753}
754
755impl crate::rdf_star::RdfStarSerializer for TurtleStarSerializer {
756    fn serialize_embedded_triple(
757        &self,
758        _virtual_id: u64,
759        components: &[u64; 3],
760    ) -> Result<Vec<u8>, crate::rdf_star::RdfStarSerializeError> {
761        // Quoted triple: << <s> <p> o >> with resolved terms.
762        let output = format!(
763            "<< {} {} {} >>",
764            star_iri_term(components[0]),
765            star_iri_term(components[1]),
766            star_object_term(components[2])
767        );
768        Ok(output.into_bytes())
769    }
770
771    fn serialize_triple(
772        &self,
773        subject: u64,
774        predicate: u64,
775        object: u64,
776    ) -> Result<Vec<u8>, crate::rdf_star::RdfStarSerializeError> {
777        let output = format!(
778            "{} {} {} .",
779            star_iri_term(subject),
780            star_iri_term(predicate),
781            star_object_term(object)
782        );
783        Ok(output.into_bytes())
784    }
785
786    fn serialize_quad(
787        &self,
788        _subject: u64,
789        _predicate: u64,
790        _object: u64,
791        _graph: u64,
792    ) -> Result<Vec<u8>, crate::rdf_star::RdfStarSerializeError> {
793        // Turtle-Star doesn't support quads natively
794        Err(crate::rdf_star::RdfStarSerializeError::UnsupportedFeature)
795    }
796
797    fn supports_quads(&self) -> bool {
798        false
799    }
800
801    fn format_name(&self) -> &'static str {
802        "Turtle-Star"
803    }
804}
805
806/// CBOR-LD Serializer for SPARQL-Star
807///
808/// Implements CBOR-LD tags 103-106 for embedded triples per the RDF-Star CBOR-LD spec:
809/// - Tag 103: Triple (<<s p o>>)
810/// - Tag 104: Subject (s of <<s p o>>)
811/// - Tag 105: Predicate (p of <<s p o>>)
812/// - Tag 106: Object (o of <<s p o>>)
813pub struct CborLdStarSerializer;
814
815impl CborLdStarSerializer {
816    pub fn new() -> Self {
817        Self
818    }
819}
820
821impl crate::rdf_star::RdfStarSerializer for CborLdStarSerializer {
822    fn serialize_embedded_triple(
823        &self,
824        _virtual_id: u64,
825        components: &[u64; 3],
826    ) -> Result<Vec<u8>, crate::rdf_star::RdfStarSerializeError> {
827        // CBOR-LD Tag 103: Triple
828        // Format: 103(3-array of [subject, predicate, object])
829        use ciborium::ser;
830
831        let mut buffer = Vec::new();
832        let tagged = ciborium::tag::Required::<[u64; 3], 103>(*components);
833        ser::into_writer(&tagged, &mut buffer)
834            .map_err(|_| crate::rdf_star::RdfStarSerializeError::BufferTooSmall)?;
835
836        Ok(buffer)
837    }
838
839    fn serialize_triple(
840        &self,
841        subject: u64,
842        predicate: u64,
843        object: u64,
844    ) -> Result<Vec<u8>, crate::rdf_star::RdfStarSerializeError> {
845        use ciborium::ser;
846        let mut buffer = Vec::new();
847        ser::into_writer(&[subject, predicate, object], &mut buffer)
848            .map_err(|_| crate::rdf_star::RdfStarSerializeError::BufferTooSmall)?;
849        Ok(buffer)
850    }
851
852    fn serialize_quad(
853        &self,
854        subject: u64,
855        predicate: u64,
856        object: u64,
857        graph: u64,
858    ) -> Result<Vec<u8>, crate::rdf_star::RdfStarSerializeError> {
859        use ciborium::ser;
860        let mut buffer = Vec::new();
861        ser::into_writer(&[subject, predicate, object, graph], &mut buffer)
862            .map_err(|_| crate::rdf_star::RdfStarSerializeError::BufferTooSmall)?;
863        Ok(buffer)
864    }
865
866    fn supports_quads(&self) -> bool {
867        true
868    }
869
870    fn format_name(&self) -> &'static str {
871        "CBOR-LD-Star"
872    }
873}
874
875#[cfg(test)]
876mod cbor_serializer_tests {
877    use super::*;
878    use crate::rdf_star::RdfStarSerializer;
879
880    #[test]
881    fn test_cbor_serializer_creation() {
882        let serializer = CborLdStarSerializer::new();
883        assert_eq!(serializer.format_name(), "CBOR-LD-Star");
884        assert!(serializer.supports_quads());
885    }
886
887    #[test]
888    fn test_serialize_embedded_triple() {
889        let serializer = CborLdStarSerializer::new();
890        let components = [1u64, 2, 3];
891        let result = serializer.serialize_embedded_triple(0, &components);
892        assert!(result.is_ok());
893        let bytes = result.unwrap();
894        // CBOR tag 103 encodes as 0xd8 (major-type 6, one-byte follows) + 0x67 (value 103)
895        assert_eq!(bytes[0], 0xd8);
896        assert_eq!(bytes[1], 0x67);
897    }
898
899    #[test]
900    fn test_serialize_triple() {
901        let serializer = CborLdStarSerializer::new();
902        let result = serializer.serialize_triple(1, 2, 3);
903        assert!(result.is_ok());
904        let bytes = result.unwrap();
905        // Should be CBOR array of 3 integers
906        assert_eq!(bytes[0], 0x83); // Array of 3 in CBOR
907    }
908
909    #[test]
910    fn test_serialize_quad() {
911        let serializer = CborLdStarSerializer::new();
912        let result = serializer.serialize_quad(1, 2, 3, 4);
913        assert!(result.is_ok());
914        let bytes = result.unwrap();
915        // Should be CBOR array of 4 integers
916        assert_eq!(bytes[0], 0x84); // Array of 4 in CBOR
917    }
918}
919
920/// N-Triples-Star Serializer
921///
922/// Serializes to N-Triples-Star format: <<<s p o>>> p o .
923pub struct NTriplesStarSerializer;
924
925impl NTriplesStarSerializer {
926    pub fn new() -> Self {
927        Self
928    }
929}
930
931impl crate::rdf_star::RdfStarSerializer for NTriplesStarSerializer {
932    fn serialize_embedded_triple(
933        &self,
934        _virtual_id: u64,
935        components: &[u64; 3],
936    ) -> Result<Vec<u8>, crate::rdf_star::RdfStarSerializeError> {
937        // Quoted triple: << <s> <p> o >> with resolved terms (round-trips with
938        // the `<<`/`>>` parser).
939        let output = format!(
940            "<< {} {} {} >>",
941            star_iri_term(components[0]),
942            star_iri_term(components[1]),
943            star_object_term(components[2])
944        );
945        Ok(output.into_bytes())
946    }
947
948    fn serialize_triple(
949        &self,
950        subject: u64,
951        predicate: u64,
952        object: u64,
953    ) -> Result<Vec<u8>, crate::rdf_star::RdfStarSerializeError> {
954        // Format: <subject> <predicate> <object> . (terms self-bracketing).
955        let output = format!(
956            "{} {} {} .",
957            star_iri_term(subject),
958            star_iri_term(predicate),
959            star_object_term(object)
960        );
961        Ok(output.into_bytes())
962    }
963
964    fn serialize_quad(
965        &self,
966        _subject: u64,
967        _predicate: u64,
968        _object: u64,
969        _graph: u64,
970    ) -> Result<Vec<u8>, crate::rdf_star::RdfStarSerializeError> {
971        // N-Triples-Star doesn't support quads natively
972        Err(crate::rdf_star::RdfStarSerializeError::UnsupportedFeature)
973    }
974
975    fn supports_quads(&self) -> bool {
976        false
977    }
978
979    fn format_name(&self) -> &'static str {
980        "N-Triples-Star"
981    }
982}
983
984/// N-Quads-Star Serializer
985///
986/// Serializes to N-Quads-Star format: <<<s p o>>> p o <g> .
987pub struct NQuadsStarSerializer;
988
989impl NQuadsStarSerializer {
990    pub fn new() -> Self {
991        Self
992    }
993}
994
995impl crate::rdf_star::RdfStarSerializer for NQuadsStarSerializer {
996    fn serialize_embedded_triple(
997        &self,
998        _virtual_id: u64,
999        components: &[u64; 3],
1000    ) -> Result<Vec<u8>, crate::rdf_star::RdfStarSerializeError> {
1001        // Quoted triple: << <s> <p> o >> with resolved terms.
1002        let output = format!(
1003            "<< {} {} {} >>",
1004            star_iri_term(components[0]),
1005            star_iri_term(components[1]),
1006            star_object_term(components[2])
1007        );
1008        Ok(output.into_bytes())
1009    }
1010
1011    fn serialize_triple(
1012        &self,
1013        subject: u64,
1014        predicate: u64,
1015        object: u64,
1016    ) -> Result<Vec<u8>, crate::rdf_star::RdfStarSerializeError> {
1017        // Format: <subject> <predicate> <object> . (graph omitted → default).
1018        let output = format!(
1019            "{} {} {} .",
1020            star_iri_term(subject),
1021            star_iri_term(predicate),
1022            star_object_term(object)
1023        );
1024        Ok(output.into_bytes())
1025    }
1026
1027    fn serialize_quad(
1028        &self,
1029        subject: u64,
1030        predicate: u64,
1031        object: u64,
1032        graph: u64,
1033    ) -> Result<Vec<u8>, crate::rdf_star::RdfStarSerializeError> {
1034        // Format: <subject> <predicate> <object> <graph> .
1035        let output = format!(
1036            "{} {} {} {} .",
1037            star_iri_term(subject),
1038            star_iri_term(predicate),
1039            star_object_term(object),
1040            star_iri_term(graph)
1041        );
1042        Ok(output.into_bytes())
1043    }
1044
1045    fn supports_quads(&self) -> bool {
1046        true
1047    }
1048
1049    fn format_name(&self) -> &'static str {
1050        "N-Quads-Star"
1051    }
1052}
1053
1054/// JSON-LD Serializer for SPARQL-Star
1055///
1056/// Serializes to JSON-LD format with @annotation for embedded triples.
1057pub struct JsonLdStarSerializer;
1058
1059impl JsonLdStarSerializer {
1060    pub fn new() -> Self {
1061        Self
1062    }
1063}
1064
1065impl crate::rdf_star::RdfStarSerializer for JsonLdStarSerializer {
1066    fn serialize_embedded_triple(
1067        &self,
1068        _virtual_id: u64,
1069        components: &[u64; 3],
1070    ) -> Result<Vec<u8>, crate::rdf_star::RdfStarSerializeError> {
1071        // JSON-LD format with @annotation
1072        // {
1073        //   "@id": "_:b1",
1074        //   "@annotation": {
1075        //     "@id": "_:b2",
1076        //     "@type": "@id",
1077        //     "@value": { "subject": s, "predicate": p, "object": o }
1078        //   }
1079        // }
1080        // JSON-LD-Star annotation node with resolved terms (object may be a
1081        // typed-literal value node).
1082        let output = format!(
1083            "{{ \"@annotation\": {{ \"subject\": \"{}\", \"predicate\": \"{}\", \"object\": {} }} }}",
1084            star_json_escape(&star_iri_bare(components[0])),
1085            star_json_escape(&star_iri_bare(components[1])),
1086            star_object_jsonld(components[2])
1087        );
1088        Ok(output.into_bytes())
1089    }
1090
1091    fn serialize_triple(
1092        &self,
1093        subject: u64,
1094        predicate: u64,
1095        object: u64,
1096    ) -> Result<Vec<u8>, crate::rdf_star::RdfStarSerializeError> {
1097        // Expanded JSON-LD node object: subject @id, predicate → [ value ].
1098        let output = format!(
1099            "{{ \"@id\": \"{}\", \"{}\": [ {} ] }}",
1100            star_json_escape(&star_iri_bare(subject)),
1101            star_json_escape(&star_iri_bare(predicate)),
1102            star_object_jsonld(object)
1103        );
1104        Ok(output.into_bytes())
1105    }
1106
1107    fn serialize_quad(
1108        &self,
1109        subject: u64,
1110        predicate: u64,
1111        object: u64,
1112        graph: u64,
1113    ) -> Result<Vec<u8>, crate::rdf_star::RdfStarSerializeError> {
1114        // Node object carrying its named graph via @graph.
1115        let output = format!(
1116            "{{ \"@id\": \"{}\", \"{}\": [ {} ], \"@graph\": \"{}\" }}",
1117            star_json_escape(&star_iri_bare(subject)),
1118            star_json_escape(&star_iri_bare(predicate)),
1119            star_object_jsonld(object),
1120            star_json_escape(&star_iri_bare(graph))
1121        );
1122        Ok(output.into_bytes())
1123    }
1124
1125    fn supports_quads(&self) -> bool {
1126        true
1127    }
1128
1129    fn format_name(&self) -> &'static str {
1130        "JSON-LD-Star"
1131    }
1132}
1133
1134#[cfg(test)]
1135mod additional_serializer_tests {
1136    use super::*;
1137    use crate::rdf_star::RdfStarSerializer;
1138
1139    #[test]
1140    fn test_ntriples_serializer() {
1141        let serializer = NTriplesStarSerializer::new();
1142        assert_eq!(serializer.format_name(), "N-Triples-Star");
1143        assert!(!serializer.supports_quads());
1144
1145        let components = [1u64, 2, 3];
1146        let result = serializer.serialize_embedded_triple(0, &components);
1147        assert!(result.is_ok());
1148        let output = String::from_utf8(result.unwrap()).unwrap();
1149        // Standard RDF-Star quoted-triple delimiter (round-trips with the parser).
1150        assert!(output.starts_with("<< "), "got: {output}");
1151        // Terms are resolved, not raw decimals.
1152        assert!(output.contains("quin:hash/"), "resolved terms: {output}");
1153    }
1154
1155    #[test]
1156    fn test_nquads_serializer() {
1157        let serializer = NQuadsStarSerializer::new();
1158        assert_eq!(serializer.format_name(), "N-Quads-Star");
1159        assert!(serializer.supports_quads());
1160
1161        let result = serializer.serialize_quad(1, 2, 3, 4);
1162        assert!(result.is_ok());
1163        let output = String::from_utf8(result.unwrap()).unwrap();
1164        // Graph term 4 resolves to its IRI surface form, not the bare `<4>` hash.
1165        assert!(
1166            output.contains("quin:hash/0000000000000004"),
1167            "graph term must be resolved: {output}"
1168        );
1169    }
1170
1171    #[test]
1172    fn test_jsonld_serializer() {
1173        let serializer = JsonLdStarSerializer::new();
1174        assert_eq!(serializer.format_name(), "JSON-LD-Star");
1175        assert!(serializer.supports_quads());
1176
1177        let components = [1u64, 2, 3];
1178        let result = serializer.serialize_embedded_triple(0, &components);
1179        assert!(result.is_ok());
1180        let output = String::from_utf8(result.unwrap()).unwrap();
1181        assert!(output.contains("@annotation"));
1182    }
1183}
1184
1185/// Trig-Star Serializer
1186///
1187/// Serializes to Trig-Star format with named graphs.
1188pub struct TrigStarSerializer {
1189    current_graph: u64,
1190}
1191
1192impl TrigStarSerializer {
1193    pub fn new() -> Self {
1194        Self { current_graph: 0 }
1195    }
1196
1197    pub fn set_current_graph(&mut self, graph_hash: u64) {
1198        self.current_graph = graph_hash;
1199    }
1200}
1201
1202impl crate::rdf_star::RdfStarSerializer for TrigStarSerializer {
1203    fn serialize_embedded_triple(
1204        &self,
1205        _virtual_id: u64,
1206        components: &[u64; 3],
1207    ) -> Result<Vec<u8>, crate::rdf_star::RdfStarSerializeError> {
1208        // Quoted triple: << <s> <p> o >> with resolved terms.
1209        let output = format!(
1210            "<< {} {} {} >>",
1211            star_iri_term(components[0]),
1212            star_iri_term(components[1]),
1213            star_object_term(components[2])
1214        );
1215        Ok(output.into_bytes())
1216    }
1217
1218    fn serialize_triple(
1219        &self,
1220        subject: u64,
1221        predicate: u64,
1222        object: u64,
1223    ) -> Result<Vec<u8>, crate::rdf_star::RdfStarSerializeError> {
1224        let output = format!(
1225            "{} {} {} .",
1226            star_iri_term(subject),
1227            star_iri_term(predicate),
1228            star_object_term(object)
1229        );
1230        Ok(output.into_bytes())
1231    }
1232
1233    fn serialize_quad(
1234        &self,
1235        subject: u64,
1236        predicate: u64,
1237        object: u64,
1238        graph: u64,
1239    ) -> Result<Vec<u8>, crate::rdf_star::RdfStarSerializeError> {
1240        let triple = format!(
1241            "{} {} {} .",
1242            star_iri_term(subject),
1243            star_iri_term(predicate),
1244            star_object_term(object)
1245        );
1246        // Default graph → bare triple; named graph → GRAPH <g> { … } wrapper.
1247        let output = if graph == 0 {
1248            triple
1249        } else {
1250            format!("GRAPH {} {{ {} }}", star_iri_term(graph), triple)
1251        };
1252        Ok(output.into_bytes())
1253    }
1254
1255    fn supports_quads(&self) -> bool {
1256        true
1257    }
1258
1259    fn format_name(&self) -> &'static str {
1260        "Trig-Star"
1261    }
1262}
1263
1264#[cfg(test)]
1265mod trig_serializer_tests {
1266    use super::*;
1267    use crate::rdf_star::RdfStarSerializer;
1268
1269    #[test]
1270    fn test_trig_serializer() {
1271        let serializer = TrigStarSerializer::new();
1272        assert_eq!(serializer.format_name(), "Trig-Star");
1273        assert!(serializer.supports_quads());
1274
1275        let result = serializer.serialize_quad(1, 2, 3, 0);
1276        assert!(result.is_ok());
1277    }
1278}
1279
1280/// N3-Star Serializer
1281///
1282/// Serializes to N3-Star format with formulae and rules support.
1283pub struct N3StarSerializer {
1284    /// Current variable bindings
1285    variables: std::collections::HashMap<u64, String>,
1286}
1287
1288impl N3StarSerializer {
1289    pub fn new() -> Self {
1290        Self {
1291            variables: std::collections::HashMap::new(),
1292        }
1293    }
1294
1295    pub fn bind_variable(&mut self, hash: u64, name: String) {
1296        self.variables.insert(hash, name);
1297    }
1298}
1299
1300impl crate::rdf_star::RdfStarSerializer for N3StarSerializer {
1301    fn serialize_embedded_triple(
1302        &self,
1303        _virtual_id: u64,
1304        components: &[u64; 3],
1305    ) -> Result<Vec<u8>, crate::rdf_star::RdfStarSerializeError> {
1306        // Quoted triple: << <s> <p> o >> with resolved terms.
1307        let output = format!(
1308            "<< {} {} {} >>",
1309            star_iri_term(components[0]),
1310            star_iri_term(components[1]),
1311            star_object_term(components[2])
1312        );
1313        Ok(output.into_bytes())
1314    }
1315
1316    fn serialize_triple(
1317        &self,
1318        subject: u64,
1319        predicate: u64,
1320        object: u64,
1321    ) -> Result<Vec<u8>, crate::rdf_star::RdfStarSerializeError> {
1322        // A bound N3 variable renders as its `?name`; otherwise the term is
1323        // resolved to its IRI / typed-literal surface form (never a bare
1324        // `_:hash` blank node, which was the previous — invalid — fallback).
1325        let s = self
1326            .variables
1327            .get(&subject)
1328            .cloned()
1329            .unwrap_or_else(|| star_iri_term(subject));
1330        let p = self
1331            .variables
1332            .get(&predicate)
1333            .cloned()
1334            .unwrap_or_else(|| star_iri_term(predicate));
1335        let o = self
1336            .variables
1337            .get(&object)
1338            .cloned()
1339            .unwrap_or_else(|| star_object_term(object));
1340
1341        let output = format!("{} {} {} .", s, p, o);
1342        Ok(output.into_bytes())
1343    }
1344
1345    fn serialize_quad(
1346        &self,
1347        subject: u64,
1348        predicate: u64,
1349        object: u64,
1350        _graph: u64,
1351    ) -> Result<Vec<u8>, crate::rdf_star::RdfStarSerializeError> {
1352        // N3 doesn't have named graphs, so serialize as triple
1353        self.serialize_triple(subject, predicate, object)
1354    }
1355
1356    fn supports_quads(&self) -> bool {
1357        false
1358    }
1359
1360    fn format_name(&self) -> &'static str {
1361        "N3-Star"
1362    }
1363}
1364
1365#[cfg(test)]
1366mod n3_serializer_tests {
1367    use super::*;
1368    use crate::rdf_star::RdfStarSerializer;
1369
1370    #[test]
1371    fn test_n3_serializer() {
1372        let serializer = N3StarSerializer::new();
1373        assert_eq!(serializer.format_name(), "N3-Star");
1374        assert!(!serializer.supports_quads());
1375
1376        let result = serializer.serialize_triple(1, 2, 3);
1377        assert!(result.is_ok());
1378    }
1379
1380    #[test]
1381    fn test_n3_serializer_variables() {
1382        let mut serializer = N3StarSerializer::new();
1383        serializer.bind_variable(1, "x".to_string());
1384        serializer.bind_variable(2, "knows".to_string());
1385        serializer.bind_variable(3, "y".to_string());
1386
1387        let result = serializer.serialize_triple(1, 2, 3);
1388        assert!(result.is_ok());
1389        let output = String::from_utf8(result.unwrap()).unwrap();
1390        assert!(output.contains("x"));
1391    }
1392}