Skip to main content

styx_tree/
builder.rs

1//! Tree builder from parse events.
2
3use std::borrow::Cow;
4
5use styx_parse::{Event, ParseErrorKind, Span};
6
7use crate::value::{Entry, Object, Payload, Scalar, Sequence, Tag, Value};
8
9/// Error during tree building.
10#[derive(Debug, Clone, PartialEq)]
11pub enum BuildError {
12    /// Unexpected event during building.
13    UnexpectedEvent(String),
14    /// Unclosed structure.
15    UnclosedStructure,
16    /// Empty document.
17    EmptyDocument,
18    /// Parse error from the lexer/parser.
19    Parse(ParseErrorKind, Span),
20}
21
22impl std::fmt::Display for BuildError {
23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        match self {
25            BuildError::UnexpectedEvent(msg) => write!(f, "unexpected event: {}", msg),
26            BuildError::UnclosedStructure => write!(f, "unclosed structure"),
27            BuildError::EmptyDocument => write!(f, "empty document"),
28            BuildError::Parse(kind, span) => {
29                write!(f, "parse error at {}-{}: {}", span.start, span.end, kind)
30            }
31        }
32    }
33}
34
35impl std::error::Error for BuildError {}
36
37impl BuildError {
38    /// If this is a parse error, return it as a `ParseError` for diagnostic rendering.
39    pub fn as_parse_error(&self) -> Option<crate::diagnostic::ParseError> {
40        match self {
41            BuildError::Parse(kind, span) => {
42                Some(crate::diagnostic::ParseError::new(kind.clone(), *span))
43            }
44            _ => None,
45        }
46    }
47}
48
49/// Builder that constructs a tree from parse events.
50pub struct TreeBuilder {
51    stack: Vec<BuilderFrame>,
52    root_entries: Vec<Entry>,
53    pending_doc_comment: Option<String>,
54    errors: Vec<(ParseErrorKind, Span)>,
55}
56
57enum BuilderFrame {
58    Object {
59        entries: Vec<Entry>,
60        span: Span,
61        pending_doc_comment: Option<String>,
62    },
63    Sequence {
64        items: Vec<Value>,
65        span: Span,
66    },
67    Tag {
68        name: String,
69        span: Span,
70    },
71    Entry {
72        key: Option<Value>,
73        doc_comment: Option<String>,
74    },
75}
76
77impl TreeBuilder {
78    /// Create a new tree builder.
79    pub fn new() -> Self {
80        Self {
81            stack: Vec::new(),
82            root_entries: Vec::new(),
83            pending_doc_comment: None,
84            errors: Vec::new(),
85        }
86    }
87
88    /// Finish building and return the root value.
89    pub fn finish(self) -> Result<Value, BuildError> {
90        // Return the first error if any occurred during parsing
91        if let Some((kind, span)) = self.errors.into_iter().next() {
92            return Err(BuildError::Parse(kind, span));
93        }
94
95        if !self.stack.is_empty() {
96            return Err(BuildError::UnclosedStructure);
97        }
98
99        // Root is always an implicit object (no tag)
100        Ok(Value {
101            tag: None,
102            payload: Some(Payload::Object(Object {
103                entries: self.root_entries,
104                span: None,
105            })),
106            span: None,
107        })
108    }
109
110    /// Push a value to the current context.
111    fn push_value(&mut self, value: Value) {
112        // First, check if we're in a Tag frame - if so, the value becomes the tag's payload
113        if let Some(BuilderFrame::Tag { .. }) = self.stack.last() {
114            // Pop the tag frame
115            if let Some(BuilderFrame::Tag { name, span }) = self.stack.pop() {
116                // Create tagged value: the tag wraps the value's payload
117                let tagged = Value {
118                    tag: Some(Tag {
119                        name,
120                        span: Some(span),
121                    }),
122                    payload: value.payload,
123                    span: value.span,
124                };
125                // Recursively push the tagged value
126                self.push_value(tagged);
127            }
128            return;
129        }
130
131        // Check if we're in an Entry frame with a key - if so, this value completes the entry
132        if let Some(BuilderFrame::Entry { key: Some(_), .. }) = self.stack.last() {
133            // Pop the entry frame and add the complete entry to parent
134            if let Some(BuilderFrame::Entry { key, doc_comment }) = self.stack.pop() {
135                let key_val = key.unwrap();
136                match self.stack.last_mut() {
137                    Some(BuilderFrame::Object { entries, .. }) => {
138                        entries.push(Entry {
139                            key: key_val,
140                            value,
141                            doc_comment,
142                        });
143                    }
144                    _ => {
145                        self.root_entries.push(Entry {
146                            key: key_val,
147                            value,
148                            doc_comment,
149                        });
150                    }
151                }
152                // Re-push an empty entry frame for potential continuation
153                self.stack.push(BuilderFrame::Entry {
154                    key: None,
155                    doc_comment: None,
156                });
157            }
158            return;
159        }
160
161        match self.stack.last_mut() {
162            Some(BuilderFrame::Object {
163                entries,
164                pending_doc_comment,
165                ..
166            }) => {
167                // Value for an entry without explicit key - use unit key
168                entries.push(Entry {
169                    key: Value::unit(),
170                    value,
171                    doc_comment: pending_doc_comment.take(),
172                });
173            }
174            Some(BuilderFrame::Sequence { items, .. }) => {
175                items.push(value);
176            }
177            Some(BuilderFrame::Tag { .. }) => {
178                // Already handled above
179                unreachable!()
180            }
181            Some(BuilderFrame::Entry { key, .. }) => {
182                if key.is_none() {
183                    // This is the key
184                    *key = Some(value);
185                }
186            }
187            None => {
188                // Root level - treat as entry in implicit object
189                self.root_entries.push(Entry {
190                    key: Value::unit(),
191                    value,
192                    doc_comment: self.pending_doc_comment.take(),
193                });
194            }
195        }
196    }
197}
198
199impl Default for TreeBuilder {
200    fn default() -> Self {
201        Self::new()
202    }
203}
204
205impl TreeBuilder {
206    /// Process a parse event.
207    pub fn event(&mut self, event: Event<'_>) {
208        let span = event.span;
209        match event.kind {
210            styx_parse::EventKind::DocumentStart | styx_parse::EventKind::DocumentEnd => {
211                // No-op for tree building
212            }
213
214            styx_parse::EventKind::ObjectStart => {
215                self.stack.push(BuilderFrame::Object {
216                    entries: Vec::new(),
217                    span,
218                    pending_doc_comment: None,
219                });
220            }
221
222            styx_parse::EventKind::ObjectEnd => {
223                if let Some(BuilderFrame::Object {
224                    entries,
225                    span: start_span,
226                    ..
227                }) = self.stack.pop()
228                {
229                    // If stack is now empty, this is the root object
230                    if self.stack.is_empty() {
231                        self.root_entries = entries;
232                    } else {
233                        let obj = Value {
234                            tag: None,
235                            payload: Some(Payload::Object(Object {
236                                entries,
237                                span: Some(Span {
238                                    start: start_span.start,
239                                    end: span.end,
240                                }),
241                            })),
242                            span: Some(Span {
243                                start: start_span.start,
244                                end: span.end,
245                            }),
246                        };
247                        self.push_value(obj);
248                    }
249                }
250            }
251
252            styx_parse::EventKind::SequenceStart => {
253                self.stack.push(BuilderFrame::Sequence {
254                    items: Vec::new(),
255                    span,
256                });
257            }
258
259            styx_parse::EventKind::SequenceEnd => {
260                if let Some(BuilderFrame::Sequence {
261                    items,
262                    span: start_span,
263                }) = self.stack.pop()
264                {
265                    let seq = Value {
266                        tag: None,
267                        payload: Some(Payload::Sequence(Sequence {
268                            items,
269                            span: Some(Span {
270                                start: start_span.start,
271                                end: span.end,
272                            }),
273                        })),
274                        span: Some(Span {
275                            start: start_span.start,
276                            end: span.end,
277                        }),
278                    };
279                    self.push_value(seq);
280                }
281            }
282
283            styx_parse::EventKind::EntryStart => {
284                let doc_comment = match self.stack.last_mut() {
285                    Some(BuilderFrame::Object {
286                        pending_doc_comment,
287                        ..
288                    }) => pending_doc_comment.take(),
289                    _ => self.pending_doc_comment.take(),
290                };
291                self.stack.push(BuilderFrame::Entry {
292                    key: None,
293                    doc_comment,
294                });
295            }
296
297            styx_parse::EventKind::EntryEnd => {
298                if let Some(BuilderFrame::Entry { key, doc_comment }) = self.stack.pop()
299                    && let Some(key) = key
300                {
301                    // We have a key but might not have a value yet
302                    match self.stack.last_mut() {
303                        Some(BuilderFrame::Object { entries, .. }) => {
304                            // Check if last entry needs this key
305                            if let Some(last) = entries.last_mut()
306                                && last.key.is_unit()
307                                && last.doc_comment.is_none()
308                            {
309                                last.key = key;
310                                last.doc_comment = doc_comment;
311                                return;
312                            }
313                            // Otherwise add as unit-valued entry
314                            entries.push(Entry {
315                                key,
316                                value: Value::unit(),
317                                doc_comment,
318                            });
319                        }
320                        _ => {
321                            // Root level
322                            if let Some(last) = self.root_entries.last_mut()
323                                && last.key.is_unit()
324                                && last.doc_comment.is_none()
325                            {
326                                last.key = key;
327                                last.doc_comment = doc_comment;
328                                return;
329                            }
330                            self.root_entries.push(Entry {
331                                key,
332                                value: Value::unit(),
333                                doc_comment,
334                            });
335                        }
336                    }
337                }
338            }
339
340            styx_parse::EventKind::Key { tag, payload, kind } => {
341                let key_value = Value {
342                    tag: tag.map(|name| Tag {
343                        name: name.to_string(),
344                        span: Some(span),
345                    }),
346                    payload: payload.map(|text| {
347                        Payload::Scalar(Scalar {
348                            text: cow_to_string(text),
349                            kind,
350                            span: Some(span),
351                        })
352                    }),
353                    span: Some(span),
354                };
355                if let Some(BuilderFrame::Entry { key, .. }) = self.stack.last_mut() {
356                    *key = Some(key_value);
357                }
358            }
359
360            styx_parse::EventKind::Scalar { value, kind } => {
361                let scalar = Value {
362                    tag: None,
363                    payload: Some(Payload::Scalar(Scalar {
364                        text: cow_to_string(value),
365                        kind,
366                        span: Some(span),
367                    })),
368                    span: Some(span),
369                };
370
371                // Check if we're in an entry context with a key already set
372                if let Some(BuilderFrame::Entry { key, doc_comment }) = self.stack.last_mut()
373                    && key.is_some()
374                {
375                    // We have a key, this is the value
376                    let key_val = key.take().unwrap();
377                    let doc = doc_comment.take();
378
379                    // Pop the entry frame
380                    self.stack.pop();
381
382                    // Add to parent
383                    match self.stack.last_mut() {
384                        Some(BuilderFrame::Object { entries, .. }) => {
385                            entries.push(Entry {
386                                key: key_val,
387                                value: scalar,
388                                doc_comment: doc,
389                            });
390                        }
391                        _ => {
392                            self.root_entries.push(Entry {
393                                key: key_val,
394                                value: scalar,
395                                doc_comment: doc,
396                            });
397                        }
398                    }
399                    // Re-push entry frame for potential more processing
400                    self.stack.push(BuilderFrame::Entry {
401                        key: None,
402                        doc_comment: None,
403                    });
404                    return;
405                }
406
407                self.push_value(scalar);
408            }
409
410            styx_parse::EventKind::Unit => {
411                let unit = Value {
412                    tag: None,
413                    payload: None,
414                    span: Some(span),
415                };
416
417                // Similar logic to Scalar for entry handling
418                if let Some(BuilderFrame::Entry { key, doc_comment }) = self.stack.last_mut()
419                    && key.is_some()
420                {
421                    let key_val = key.take().unwrap();
422                    let doc = doc_comment.take();
423                    self.stack.pop();
424
425                    match self.stack.last_mut() {
426                        Some(BuilderFrame::Object { entries, .. }) => {
427                            entries.push(Entry {
428                                key: key_val,
429                                value: unit,
430                                doc_comment: doc,
431                            });
432                        }
433                        _ => {
434                            self.root_entries.push(Entry {
435                                key: key_val,
436                                value: unit,
437                                doc_comment: doc,
438                            });
439                        }
440                    }
441                    self.stack.push(BuilderFrame::Entry {
442                        key: None,
443                        doc_comment: None,
444                    });
445                    return;
446                }
447
448                self.push_value(unit);
449            }
450
451            styx_parse::EventKind::TagStart { name } => {
452                self.stack.push(BuilderFrame::Tag {
453                    name: name.to_string(),
454                    span,
455                });
456            }
457
458            styx_parse::EventKind::TagEnd => {
459                // Only pop if the top frame is a Tag - otherwise the tag was already
460                // consumed when its payload was processed
461                if !matches!(self.stack.last(), Some(BuilderFrame::Tag { .. })) {
462                    return;
463                }
464                if let Some(BuilderFrame::Tag { name, span }) = self.stack.pop() {
465                    // Tag with no payload - just the tag itself
466                    let tagged = Value {
467                        tag: Some(Tag {
468                            name,
469                            span: Some(span),
470                        }),
471                        payload: None,
472                        span: Some(span),
473                    };
474
475                    // Similar to scalar handling
476                    if let Some(BuilderFrame::Entry { key, doc_comment }) = self.stack.last_mut()
477                        && key.is_some()
478                    {
479                        let key_val = key.take().unwrap();
480                        let doc = doc_comment.take();
481                        self.stack.pop();
482
483                        match self.stack.last_mut() {
484                            Some(BuilderFrame::Object { entries, .. }) => {
485                                entries.push(Entry {
486                                    key: key_val,
487                                    value: tagged,
488                                    doc_comment: doc,
489                                });
490                            }
491                            _ => {
492                                self.root_entries.push(Entry {
493                                    key: key_val,
494                                    value: tagged,
495                                    doc_comment: doc,
496                                });
497                            }
498                        }
499                        self.stack.push(BuilderFrame::Entry {
500                            key: None,
501                            doc_comment: None,
502                        });
503                        return;
504                    }
505
506                    self.push_value(tagged);
507                }
508            }
509
510            styx_parse::EventKind::DocComment { lines } => {
511                // Lines are already stripped of `/// ` prefix by the parser
512                let comment = lines.join("\n");
513                match self.stack.last_mut() {
514                    Some(BuilderFrame::Object {
515                        pending_doc_comment,
516                        ..
517                    }) => {
518                        append_doc_comment(pending_doc_comment, comment);
519                    }
520                    _ => {
521                        append_doc_comment(&mut self.pending_doc_comment, comment);
522                    }
523                }
524            }
525
526            styx_parse::EventKind::Comment { .. } => {
527                // Ignore regular comments for tree building
528            }
529
530            styx_parse::EventKind::Error { kind } => {
531                self.errors.push((kind, span));
532            }
533        }
534    }
535}
536
537fn cow_to_string(cow: Cow<'_, str>) -> String {
538    cow.into_owned()
539}
540
541/// Append a doc comment line to an existing doc comment, joining with newline.
542fn append_doc_comment(target: &mut Option<String>, line: String) {
543    match target {
544        Some(existing) => {
545            existing.push('\n');
546            existing.push_str(&line);
547        }
548        None => {
549            *target = Some(line);
550        }
551    }
552}
553
554#[cfg(test)]
555mod tests {
556    use styx_parse::Parser;
557
558    use super::*;
559
560    fn parse(source: &str) -> Value {
561        let mut parser = Parser::new(source);
562        let mut builder = TreeBuilder::new();
563        while let Some(event) = parser.next_event() {
564            eprintln!("Event: {:?}", event);
565            builder.event(event);
566        }
567        builder.finish().unwrap()
568    }
569
570    fn try_parse(source: &str) -> Result<Value, BuildError> {
571        let mut parser = Parser::new(source);
572        let mut builder = TreeBuilder::new();
573        while let Some(event) = parser.next_event() {
574            builder.event(event);
575        }
576        builder.finish()
577    }
578
579    #[test]
580    fn test_empty_document() {
581        let value = parse("");
582        assert!(value.as_object().unwrap().is_empty());
583    }
584
585    #[test]
586    fn test_simple_entry() {
587        let value = parse("name Alice");
588        let obj = value.as_object().unwrap();
589        assert_eq!(obj.get("name").and_then(|v| v.as_str()), Some("Alice"));
590    }
591
592    #[test]
593    fn test_multiple_entries() {
594        let value = parse("name Alice\nage 30");
595        let obj = value.as_object().unwrap();
596        assert_eq!(obj.get("name").and_then(|v| v.as_str()), Some("Alice"));
597        assert_eq!(obj.get("age").and_then(|v| v.as_str()), Some("30"));
598    }
599
600    #[test]
601    fn test_path_access() {
602        let value = parse("name Alice\nage 30");
603        eprintln!("Built value: {value:#?}");
604        assert_eq!(value.get("name").and_then(|v| v.as_str()), Some("Alice"));
605        assert_eq!(value.get("age").and_then(|v| v.as_str()), Some("30"));
606    }
607
608    #[test]
609    fn test_unit_value() {
610        let value = parse("enabled @");
611        let obj = value.as_object().unwrap();
612        assert!(obj.get("enabled").unwrap().is_unit());
613    }
614
615    #[test]
616    fn test_unit_key() {
617        // @ followed by a value should create a unit key
618        let value = parse("@ server.schema.styx");
619        let obj = value.as_object().unwrap();
620        // The unit key entry
621        let unit_entry = obj.entries.iter().find(|e| e.key.is_unit());
622        assert!(
623            unit_entry.is_some(),
624            "should have unit key entry, got: {:?}",
625            obj.entries
626                .iter()
627                .map(|e| format!("key={:?}", e.key))
628                .collect::<Vec<_>>()
629        );
630        assert_eq!(
631            unit_entry.unwrap().value.as_str(),
632            Some("server.schema.styx")
633        );
634    }
635
636    #[test]
637    fn test_tag() {
638        let value = parse("type @user");
639        let obj = value.as_object().unwrap();
640        assert_eq!(obj.get("type").and_then(|v| v.tag_name()), Some("user"));
641    }
642
643    #[test]
644    fn test_tag_with_object_payload() {
645        let value = parse("result @err{message \"failed\"}");
646        let obj = value.as_object().unwrap();
647        let result = obj.get("result").unwrap();
648        assert_eq!(result.tag_name(), Some("err"));
649        // Check payload is an object with message field
650        let payload_obj = result.as_object().expect("payload should be object");
651        assert_eq!(
652            payload_obj.get("message").and_then(|v| v.as_str()),
653            Some("failed")
654        );
655    }
656
657    #[test]
658    fn test_tag_with_sequence_payload() {
659        let value = parse("color @rgb(255 128 0)");
660        let obj = value.as_object().unwrap();
661        let color = obj.get("color").unwrap();
662        assert_eq!(color.tag_name(), Some("rgb"));
663        // Check payload is a sequence
664        let payload_seq = color.as_sequence().expect("payload should be sequence");
665        assert_eq!(payload_seq.len(), 3);
666        assert_eq!(payload_seq.get(0).and_then(|v| v.as_str()), Some("255"));
667        assert_eq!(payload_seq.get(1).and_then(|v| v.as_str()), Some("128"));
668        assert_eq!(payload_seq.get(2).and_then(|v| v.as_str()), Some("0"));
669    }
670
671    #[test]
672    fn test_schema_structure_with_space_is_error() {
673        // @ @object { ... } with space before brace is now an error (3 atoms)
674        let source = r#"schema {
675  @ @object {
676    name @string
677  }
678}"#;
679
680        // This should produce a parse error (TooManyAtoms)
681        let result = try_parse(source);
682        assert!(
683            result.is_err(),
684            "@ @object {{ }} with space should be a parse error"
685        );
686        match result {
687            Err(BuildError::Parse(styx_parse::ParseErrorKind::TooManyAtoms, _)) => {}
688            Err(e) => panic!("expected TooManyAtoms error, got {:?}", e),
689            Ok(_) => panic!("expected error, got Ok"),
690        }
691    }
692
693    #[test]
694    fn test_schema_structure_no_space() {
695        // @ @object{ ... } without space before brace
696        let source = r#"schema {
697  @ @object{
698    name @string
699  }
700}"#;
701
702        // Debug: print all events
703        eprintln!("=== Events for no-space version ===");
704        let mut debug_parser = Parser::new(source);
705        while let Some(event) = debug_parser.next_event() {
706            eprintln!("Event: {:?}", event);
707        }
708
709        let value = parse(source);
710        let obj = value.as_object().unwrap();
711        eprintln!(
712            "Root entries: {:?}",
713            obj.entries
714                .iter()
715                .map(|e| e.key.as_str())
716                .collect::<Vec<_>>()
717        );
718        assert!(obj.get("schema").is_some(), "should have schema entry");
719        let schema = obj.get("schema").unwrap();
720        assert!(
721            schema.as_object().is_some(),
722            "schema should be an object, got tag={:?} payload={:?}",
723            schema.tag,
724            schema.payload.is_some()
725        );
726    }
727
728    #[test]
729    fn test_multiline_doc_comment() {
730        let source = r#"/// First line of doc
731/// Second line of doc
732name @string"#;
733        let value = parse(source);
734        let obj = value.as_object().unwrap();
735        let entry = obj.entries.iter().find(|e| e.key.as_str() == Some("name"));
736        assert!(entry.is_some(), "should have 'name' entry");
737        let entry = entry.unwrap();
738        assert_eq!(
739            entry.doc_comment,
740            Some("First line of doc\nSecond line of doc".to_string()),
741            "doc comment should contain both lines joined by newline"
742        );
743    }
744
745    #[test]
746    fn test_single_line_doc_comment() {
747        let source = r#"/// Just one line
748value 42"#;
749        let value = parse(source);
750        let obj = value.as_object().unwrap();
751        let entry = obj.entries.iter().find(|e| e.key.as_str() == Some("value"));
752        assert!(entry.is_some(), "should have 'value' entry");
753        let entry = entry.unwrap();
754        assert_eq!(entry.doc_comment, Some("Just one line".to_string()),);
755    }
756
757    #[test]
758    fn test_multiline_doc_comment_in_object() {
759        // Test doc comments inside a braced object
760        let source = r#"schema {
761    /// First line
762    /// Second line
763    /// Third line
764    field @string
765}"#;
766        let value = parse(source);
767        let obj = value.as_object().unwrap();
768        let schema = obj.get("schema").expect("should have schema");
769        let schema_obj = schema.as_object().expect("schema should be an object");
770        let entry = schema_obj
771            .entries
772            .iter()
773            .find(|e| e.key.as_str() == Some("field"));
774        assert!(entry.is_some(), "should have 'field' entry");
775        let entry = entry.unwrap();
776        assert_eq!(
777            entry.doc_comment,
778            Some("First line\nSecond line\nThird line".to_string()),
779            "doc comment should contain all lines joined by newline"
780        );
781    }
782}