Skip to main content

eure_document/
source.rs

1//! Source-level document representation for programmatic construction and formatting.
2//!
3//! This module provides types for representing Eure source structure as an AST,
4//! while actual values are referenced via [`NodeId`] into an [`EureDocument`].
5//!
6//! The structure directly mirrors the Eure grammar from `eure.par`:
7//!
8//! ```text
9//! Eure: [ ValueBinding ] { Binding } { Section } ;
10//! Binding: Keys BindingRhs ;
11//!   BindingRhs: ValueBinding | SectionBinding | TextBinding ;
12//! Section: At Keys SectionBody ;
13//!   SectionBody: [ ValueBinding ] { Binding } | Begin Eure End ;
14//! ```
15//!
16//! # Design
17//!
18//! ```text
19//! SourceDocument
20//! ├── EureDocument (semantic data)
21//! └── sources: Vec<EureSource> (arena)
22//!     └── EureSource
23//!         ├── leading_trivia: Vec<Trivia>
24//!         ├── value: Option<NodeId>
25//!         ├── bindings: Vec<BindingSource>
26//!         │   └── trivia_before: Vec<Trivia>
27//!         ├── sections: Vec<SectionSource>
28//!         │   └── trivia_before: Vec<Trivia>
29//!         └── trailing_trivia: Vec<Trivia>
30//! ```
31//!
32//! Trivia (comments and blank lines) is preserved for round-trip formatting.
33
34use std::collections::HashSet;
35
36use crate::document::{EureDocument, NodeId};
37use crate::prelude_internal::*;
38
39// ============================================================================
40// Core AST Types (mirrors grammar)
41// ============================================================================
42
43/// Index into the sources arena.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
45pub struct SourceId(pub usize);
46
47/// A source-level Eure document/block.
48///
49/// Mirrors grammar: `Eure: [ ValueBinding ] { Binding } { Section } ;`
50#[derive(Debug, Clone, Default)]
51pub struct EureSource {
52    /// Comments/blank lines before the first item (value, binding, or section)
53    pub leading_trivia: Vec<Trivia>,
54    /// Optional initial value binding: `[ ValueBinding ]`
55    pub value: Option<NodeId>,
56    /// Bindings in order: `{ Binding }`
57    pub bindings: Vec<BindingSource>,
58    /// Sections in order: `{ Section }`
59    pub sections: Vec<SectionSource>,
60    /// Comments/blank lines after the last item
61    pub trailing_trivia: Vec<Trivia>,
62}
63
64/// A binding statement: path followed by value or block.
65///
66/// Mirrors grammar: `Binding: Keys BindingRhs ;`
67#[derive(Debug, Clone)]
68pub struct BindingSource {
69    /// Comments/blank lines before this binding
70    pub trivia_before: Vec<Trivia>,
71    /// The path (Keys)
72    pub path: SourcePath,
73    /// The binding body (BindingRhs)
74    pub bind: BindSource,
75    /// Optional trailing comment (same line)
76    pub trailing_comment: Option<Comment>,
77}
78
79/// The right-hand side of a binding.
80///
81/// Mirrors grammar: `BindingRhs: ValueBinding | SectionBinding | TextBinding ;`
82#[derive(Debug, Clone)]
83pub enum BindSource {
84    /// Pattern #1: `path = value` (ValueBinding or TextBinding)
85    Value(NodeId),
86    /// Pattern #1b: `path = [array with element trivia]`
87    ///
88    /// Used when an array has comments between elements that need to be preserved.
89    Array {
90        /// Reference to the array node in EureDocument
91        node: NodeId,
92        /// Per-element layout information (comments before each element)
93        elements: Vec<ArrayElementSource>,
94    },
95    /// Pattern #2/#3: `path { eure }` (SectionBinding -> nested EureSource)
96    Block(SourceId),
97}
98
99/// Layout information for an array element.
100///
101/// Used to preserve comments that appear before array elements when converting
102/// from formats like TOML.
103#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct ArrayElementSource {
105    /// Trivia (comments/blank lines) before this element
106    pub trivia_before: Vec<Trivia>,
107    /// The index of this element in the NodeArray
108    pub index: usize,
109    /// Trailing comment on the same line as this element
110    pub trailing_comment: Option<Comment>,
111}
112
113/// A section statement: `@ path` followed by body.
114///
115/// Mirrors grammar: `Section: At Keys SectionBody ;`
116#[derive(Debug, Clone)]
117pub struct SectionSource {
118    /// Comments/blank lines before this section
119    pub trivia_before: Vec<Trivia>,
120    /// The path (Keys)
121    pub path: SourcePath,
122    /// The section body (SectionBody)
123    pub body: SectionBody,
124    /// Optional trailing comment (same line)
125    pub trailing_comment: Option<Comment>,
126}
127
128/// The body of a section.
129///
130/// Mirrors grammar: `SectionBody: [ ValueBinding ] { Binding } | Begin Eure End ;`
131#[derive(Debug, Clone)]
132pub enum SectionBody {
133    /// Pattern #4: `@ section` (items follow) - `[ ValueBinding ] { Binding }`
134    Items {
135        /// Optional initial value binding
136        value: Option<NodeId>,
137        /// Bindings in the section
138        bindings: Vec<BindingSource>,
139    },
140    /// Pattern #5/#6: `@ section { eure }` - `Begin Eure End`
141    Block(SourceId),
142}
143
144// ============================================================================
145// Source Document
146// ============================================================================
147
148/// A document with source structure metadata.
149///
150/// Combines semantic data ([`EureDocument`]) with source AST information
151/// for round-trip conversions, preserving the exact source structure.
152#[derive(Debug, Clone)]
153pub struct SourceDocument {
154    /// The semantic data (values, structure)
155    pub document: EureDocument,
156    /// Arena of all EureSource blocks
157    pub sources: Vec<EureSource>,
158    /// Root source index (always 0)
159    pub root: SourceId,
160    /// Array nodes that should be formatted multi-line (even without trivia)
161    pub multiline_arrays: HashSet<NodeId>,
162}
163
164impl SourceDocument {
165    /// Create a new source document with the given document and sources.
166    #[must_use]
167    pub fn new(document: EureDocument, sources: Vec<EureSource>) -> Self {
168        Self {
169            document,
170            sources,
171            root: SourceId(0),
172            multiline_arrays: HashSet::new(),
173        }
174    }
175
176    /// Create an empty source document.
177    pub fn empty() -> Self {
178        Self {
179            document: EureDocument::new_empty(),
180            sources: vec![EureSource::default()],
181            root: SourceId(0),
182            multiline_arrays: HashSet::new(),
183        }
184    }
185
186    /// Mark an array node as needing multi-line formatting.
187    pub fn mark_multiline_array(&mut self, node_id: NodeId) {
188        self.multiline_arrays.insert(node_id);
189    }
190
191    /// Check if an array node should be formatted multi-line.
192    pub fn is_multiline_array(&self, node_id: NodeId) -> bool {
193        self.multiline_arrays.contains(&node_id)
194    }
195
196    /// Get a reference to the document.
197    pub fn document(&self) -> &EureDocument {
198        &self.document
199    }
200
201    /// Get a mutable reference to the document.
202    pub fn document_mut(&mut self) -> &mut EureDocument {
203        &mut self.document
204    }
205
206    /// Get the root EureSource.
207    pub fn root_source(&self) -> &EureSource {
208        &self.sources[self.root.0]
209    }
210
211    /// Get a reference to an EureSource by ID.
212    pub fn source(&self, id: SourceId) -> &EureSource {
213        &self.sources[id.0]
214    }
215
216    /// Get a mutable reference to an EureSource by ID.
217    pub fn source_mut(&mut self, id: SourceId) -> &mut EureSource {
218        &mut self.sources[id.0]
219    }
220}
221
222// ============================================================================
223// Path Types
224// ============================================================================
225
226/// A path in source representation.
227pub type SourcePath = Vec<SourcePathSegment>;
228
229/// A segment in a source path.
230#[derive(Debug, Clone, PartialEq, Eq)]
231pub struct SourcePathSegment {
232    /// The key part of the segment
233    pub key: SourceKey,
234    /// Optional array marker describing the kind of array index that followed
235    /// the key (`[]`, `[n]`, or `[^]`), or `None` if no array marker was present.
236    pub array: Option<crate::path::ArrayIndexKind>,
237}
238
239impl SourcePathSegment {
240    /// Create a root-relative segment used when a path starts with an array
241    /// marker, e.g. `@[]` or `[]` inside a section body.
242    pub fn root_array(array: crate::path::ArrayIndexKind) -> Self {
243        Self {
244            key: SourceKey::Root,
245            array: Some(array),
246        }
247    }
248
249    /// Create a simple identifier segment without array marker.
250    pub fn ident(name: Identifier) -> Self {
251        Self {
252            key: SourceKey::Ident(name),
253            array: None,
254        }
255    }
256
257    /// Create an extension segment without array marker.
258    pub fn extension(name: Identifier) -> Self {
259        Self {
260            key: SourceKey::Extension(name),
261            array: None,
262        }
263    }
264
265    /// Create a segment with array push marker (`[]`).
266    pub fn with_array_push(mut self) -> Self {
267        self.array = Some(crate::path::ArrayIndexKind::Push);
268        self
269    }
270
271    /// Create a segment with array index marker (`[n]`).
272    pub fn with_array_index(mut self, index: usize) -> Self {
273        self.array = Some(crate::path::ArrayIndexKind::Specific(index));
274        self
275    }
276
277    /// Create a segment with current-index marker (`[^]`).
278    pub fn with_array_current(mut self) -> Self {
279        self.array = Some(crate::path::ArrayIndexKind::Current);
280        self
281    }
282
283    /// Create a quoted string segment without array marker.
284    pub fn quoted_string(s: impl Into<String>) -> Self {
285        Self {
286            key: SourceKey::quoted(s),
287            array: None,
288        }
289    }
290
291    /// Create a literal string segment (single-quoted) without array marker.
292    pub fn literal_string(s: impl Into<String>) -> Self {
293        Self {
294            key: SourceKey::literal(s),
295            array: None,
296        }
297    }
298}
299
300/// Syntax style for string keys (for round-trip formatting).
301///
302/// This preserves whether a string key was written with quotes, single quotes, or delimiters,
303/// similar to how `SyntaxHint` preserves code block formatting.
304#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
305pub enum StringStyle {
306    /// Quoted string: `"..."`
307    #[default]
308    Quoted,
309    /// Literal string (single-quoted): `'...'`
310    /// Content is taken literally, no escape processing
311    Literal,
312    /// Delimited literal string: `<'...'>`, `<<'...'>>`, `<<<'...'>>>`
313    /// The u8 indicates the delimiter level (1, 2, or 3)
314    /// Content is taken literally, no escape processing
315    DelimitedLitStr(u8),
316    /// Delimited code: `<`...`>`, `<<`...`>>`, `<<<`...`>>>`
317    /// The u8 indicates the delimiter level (1, 2, or 3)
318    DelimitedCode(u8),
319}
320
321/// A key in source representation.
322///
323/// This determines how the key should be rendered in the output.
324#[derive(Debug, Clone)]
325pub enum SourceKey {
326    /// Current/root node. Used only as a carrier for a leading array marker.
327    Root,
328
329    /// Bare identifier: `foo`, `bar_baz`
330    Ident(Identifier),
331
332    /// Extension namespace: `$variant`, `$eure`
333    Extension(Identifier),
334
335    /// Hole key: `!` or `!label`
336    Hole(Option<Identifier>),
337
338    /// String key with syntax style hint.
339    /// - `StringStyle::Quoted`: `"hello world"`
340    /// - `StringStyle::Literal`: `'hello world'`
341    ///
342    /// Note: `PartialEq` ignores the style - only content matters for equality.
343    String(String, StringStyle),
344
345    /// Integer key: `123`
346    Integer(i64),
347
348    /// Tuple key: `(1, "a")`
349    Tuple(Vec<SourceKey>),
350
351    /// Tuple index: `#0`, `#1`
352    TupleIndex(u8),
353}
354
355impl PartialEq for SourceKey {
356    fn eq(&self, other: &Self) -> bool {
357        match (self, other) {
358            (Self::Root, Self::Root) => true,
359            (Self::Ident(a), Self::Ident(b)) => a == b,
360            (Self::Extension(a), Self::Extension(b)) => a == b,
361            (Self::Hole(a), Self::Hole(b)) => a == b,
362            // String equality ignores style hint - only content matters
363            (Self::String(a, _), Self::String(b, _)) => a == b,
364            (Self::Integer(a), Self::Integer(b)) => a == b,
365            (Self::Tuple(a), Self::Tuple(b)) => a == b,
366            (Self::TupleIndex(a), Self::TupleIndex(b)) => a == b,
367            _ => false,
368        }
369    }
370}
371
372impl Eq for SourceKey {}
373
374impl SourceKey {
375    /// Create a hole key: `!` or `!label`.
376    pub fn hole(label: Option<Identifier>) -> Self {
377        SourceKey::Hole(label)
378    }
379
380    /// Create a quoted string key: `"..."`
381    pub fn quoted(s: impl Into<String>) -> Self {
382        SourceKey::String(s.into(), StringStyle::Quoted)
383    }
384
385    /// Create a literal string key (single-quoted): `'...'`
386    pub fn literal(s: impl Into<String>) -> Self {
387        SourceKey::String(s.into(), StringStyle::Literal)
388    }
389
390    /// Create a delimited literal string key: `<'...'>`, `<<'...'>>`, `<<<'...'>>>`
391    pub fn delimited_lit_str(s: impl Into<String>, level: u8) -> Self {
392        SourceKey::String(s.into(), StringStyle::DelimitedLitStr(level))
393    }
394
395    /// Create a delimited code key: `<`...`>`, `<<`...`>>`, `<<<`...`>>>`
396    pub fn delimited_code(s: impl Into<String>, level: u8) -> Self {
397        SourceKey::String(s.into(), StringStyle::DelimitedCode(level))
398    }
399}
400
401impl From<Identifier> for SourceKey {
402    fn from(id: Identifier) -> Self {
403        SourceKey::Ident(id)
404    }
405}
406
407impl From<i64> for SourceKey {
408    fn from(n: i64) -> Self {
409        SourceKey::Integer(n)
410    }
411}
412
413// ============================================================================
414// Comment and Trivia Types
415// ============================================================================
416
417/// A comment in the source.
418#[derive(Debug, Clone, PartialEq, Eq)]
419pub enum Comment {
420    /// Line comment: `// comment`
421    Line(String),
422    /// Block comment: `/* comment */`
423    Block(String),
424}
425
426impl Comment {
427    /// Create a line comment.
428    pub fn line(s: impl Into<String>) -> Self {
429        Comment::Line(s.into())
430    }
431
432    /// Create a block comment.
433    pub fn block(s: impl Into<String>) -> Self {
434        Comment::Block(s.into())
435    }
436
437    /// Get the comment text content.
438    pub fn text(&self) -> &str {
439        match self {
440            Comment::Line(s) | Comment::Block(s) => s,
441        }
442    }
443}
444
445/// Trivia: comments and blank lines that appear between statements.
446///
447/// Trivia is used to preserve whitespace and comments for round-trip formatting.
448#[derive(Debug, Clone, PartialEq, Eq)]
449pub enum Trivia {
450    /// A comment (line or block)
451    Comment(Comment),
452    /// A blank line (empty line separating statements)
453    BlankLine,
454}
455
456impl Trivia {
457    /// Create a line comment trivia.
458    pub fn line_comment(s: impl Into<String>) -> Self {
459        Trivia::Comment(Comment::Line(s.into()))
460    }
461
462    /// Create a block comment trivia.
463    pub fn block_comment(s: impl Into<String>) -> Self {
464        Trivia::Comment(Comment::Block(s.into()))
465    }
466
467    /// Create a blank line trivia.
468    pub fn blank_line() -> Self {
469        Trivia::BlankLine
470    }
471}
472
473impl From<Comment> for Trivia {
474    fn from(comment: Comment) -> Self {
475        Trivia::Comment(comment)
476    }
477}
478
479// ============================================================================
480// Builder Helpers
481// ============================================================================
482
483impl EureSource {
484    /// Create an empty EureSource.
485    pub fn new() -> Self {
486        Self::default()
487    }
488
489    /// Add a binding to this source.
490    pub fn push_binding(&mut self, binding: BindingSource) {
491        self.bindings.push(binding);
492    }
493
494    /// Add a section to this source.
495    pub fn push_section(&mut self, section: SectionSource) {
496        self.sections.push(section);
497    }
498}
499
500impl BindingSource {
501    /// Create a value binding: `path = value`
502    pub fn value(path: SourcePath, node: NodeId) -> Self {
503        Self {
504            trivia_before: Vec::new(),
505            path,
506            bind: BindSource::Value(node),
507            trailing_comment: None,
508        }
509    }
510
511    /// Create a block binding: `path { eure }`
512    pub fn block(path: SourcePath, source_id: SourceId) -> Self {
513        Self {
514            trivia_before: Vec::new(),
515            path,
516            bind: BindSource::Block(source_id),
517            trailing_comment: None,
518        }
519    }
520
521    /// Add a trailing comment.
522    pub fn with_trailing_comment(mut self, comment: Comment) -> Self {
523        self.trailing_comment = Some(comment);
524        self
525    }
526
527    /// Add trivia before this binding.
528    pub fn with_trivia(mut self, trivia: Vec<Trivia>) -> Self {
529        self.trivia_before = trivia;
530        self
531    }
532
533    /// Create an array binding with per-element layout: `path = [...]`
534    pub fn array(path: SourcePath, node: NodeId, elements: Vec<ArrayElementSource>) -> Self {
535        Self {
536            trivia_before: Vec::new(),
537            path,
538            bind: BindSource::Array { node, elements },
539            trailing_comment: None,
540        }
541    }
542}
543
544impl SectionSource {
545    /// Create a section with items body: `@ path` (items follow)
546    pub fn items(path: SourcePath, value: Option<NodeId>, bindings: Vec<BindingSource>) -> Self {
547        Self {
548            trivia_before: Vec::new(),
549            path,
550            body: SectionBody::Items { value, bindings },
551            trailing_comment: None,
552        }
553    }
554
555    /// Create a section with block body: `@ path { eure }`
556    pub fn block(path: SourcePath, source_id: SourceId) -> Self {
557        Self {
558            trivia_before: Vec::new(),
559            path,
560            body: SectionBody::Block(source_id),
561            trailing_comment: None,
562        }
563    }
564
565    /// Add a trailing comment.
566    pub fn with_trailing_comment(mut self, comment: Comment) -> Self {
567        self.trailing_comment = Some(comment);
568        self
569    }
570
571    /// Add trivia before this section.
572    pub fn with_trivia(mut self, trivia: Vec<Trivia>) -> Self {
573        self.trivia_before = trivia;
574        self
575    }
576}
577
578impl ArrayElementSource {
579    /// Create an array element source.
580    pub fn new(index: usize) -> Self {
581        Self {
582            trivia_before: Vec::new(),
583            index,
584            trailing_comment: None,
585        }
586    }
587
588    /// Add trivia before this element.
589    pub fn with_trivia(mut self, trivia: Vec<Trivia>) -> Self {
590        self.trivia_before = trivia;
591        self
592    }
593
594    /// Add a trailing comment.
595    pub fn with_trailing_comment(mut self, comment: Comment) -> Self {
596        self.trailing_comment = Some(comment);
597        self
598    }
599}
600
601#[cfg(test)]
602mod tests {
603    use super::*;
604
605    #[test]
606    fn test_source_path_segment_ident() {
607        let actual = SourcePathSegment::ident(Identifier::new_unchecked("foo"));
608        let expected = SourcePathSegment {
609            key: SourceKey::Ident(Identifier::new_unchecked("foo")),
610            array: None,
611        };
612        assert_eq!(actual, expected);
613    }
614
615    #[test]
616    fn test_source_path_segment_with_array_push() {
617        let actual = SourcePathSegment::ident(Identifier::new_unchecked("items")).with_array_push();
618        let expected = SourcePathSegment {
619            key: SourceKey::Ident(Identifier::new_unchecked("items")),
620            array: Some(crate::path::ArrayIndexKind::Push),
621        };
622        assert_eq!(actual, expected);
623    }
624
625    #[test]
626    fn test_source_path_segment_with_array_index() {
627        let actual =
628            SourcePathSegment::ident(Identifier::new_unchecked("items")).with_array_index(0);
629        let expected = SourcePathSegment {
630            key: SourceKey::Ident(Identifier::new_unchecked("items")),
631            array: Some(crate::path::ArrayIndexKind::Specific(0)),
632        };
633        assert_eq!(actual, expected);
634    }
635
636    #[test]
637    fn test_source_path_segment_with_array_current() {
638        let actual =
639            SourcePathSegment::ident(Identifier::new_unchecked("items")).with_array_current();
640        let expected = SourcePathSegment {
641            key: SourceKey::Ident(Identifier::new_unchecked("items")),
642            array: Some(crate::path::ArrayIndexKind::Current),
643        };
644        assert_eq!(actual, expected);
645    }
646
647    #[test]
648    fn test_binding_source_value() {
649        let path = vec![SourcePathSegment::ident(Identifier::new_unchecked("foo"))];
650        let binding = BindingSource::value(path.clone(), NodeId(1));
651        assert_eq!(binding.path, path);
652        assert!(matches!(binding.bind, BindSource::Value(NodeId(1))));
653        assert!(binding.trivia_before.is_empty());
654    }
655
656    #[test]
657    fn test_binding_source_block() {
658        let path = vec![SourcePathSegment::ident(Identifier::new_unchecked("user"))];
659        let binding = BindingSource::block(path.clone(), SourceId(1));
660        assert_eq!(binding.path, path);
661        assert!(matches!(binding.bind, BindSource::Block(SourceId(1))));
662        assert!(binding.trivia_before.is_empty());
663    }
664
665    #[test]
666    fn test_binding_with_trivia() {
667        let path = vec![SourcePathSegment::ident(Identifier::new_unchecked("foo"))];
668        let trivia = vec![Trivia::BlankLine, Trivia::line_comment("comment")];
669        let binding = BindingSource::value(path.clone(), NodeId(1)).with_trivia(trivia.clone());
670        assert_eq!(binding.trivia_before, trivia);
671    }
672
673    #[test]
674    fn test_section_source_items() {
675        let path = vec![SourcePathSegment::ident(Identifier::new_unchecked(
676            "server",
677        ))];
678        let section = SectionSource::items(path.clone(), None, vec![]);
679        assert_eq!(section.path, path);
680        assert!(matches!(
681            section.body,
682            SectionBody::Items {
683                value: None,
684                bindings
685            } if bindings.is_empty()
686        ));
687        assert!(section.trivia_before.is_empty());
688    }
689
690    #[test]
691    fn test_section_source_block() {
692        let path = vec![SourcePathSegment::ident(Identifier::new_unchecked(
693            "config",
694        ))];
695        let section = SectionSource::block(path.clone(), SourceId(2));
696        assert_eq!(section.path, path);
697        assert!(matches!(section.body, SectionBody::Block(SourceId(2))));
698        assert!(section.trivia_before.is_empty());
699    }
700
701    #[test]
702    fn test_section_with_trivia() {
703        let path = vec![SourcePathSegment::ident(Identifier::new_unchecked(
704            "server",
705        ))];
706        let trivia = vec![Trivia::BlankLine];
707        let section = SectionSource::items(path.clone(), None, vec![]).with_trivia(trivia.clone());
708        assert_eq!(section.trivia_before, trivia);
709    }
710
711    #[test]
712    fn test_source_document_empty() {
713        let doc = SourceDocument::empty();
714        assert_eq!(doc.sources.len(), 1);
715        assert_eq!(doc.root, SourceId(0));
716        assert!(doc.root_source().bindings.is_empty());
717        assert!(doc.root_source().sections.is_empty());
718    }
719}