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 simple identifier segment without array marker.
241    pub fn ident(name: Identifier) -> Self {
242        Self {
243            key: SourceKey::Ident(name),
244            array: None,
245        }
246    }
247
248    /// Create an extension segment without array marker.
249    pub fn extension(name: Identifier) -> Self {
250        Self {
251            key: SourceKey::Extension(name),
252            array: None,
253        }
254    }
255
256    /// Create a segment with array push marker (`[]`).
257    pub fn with_array_push(mut self) -> Self {
258        self.array = Some(crate::path::ArrayIndexKind::Push);
259        self
260    }
261
262    /// Create a segment with array index marker (`[n]`).
263    pub fn with_array_index(mut self, index: usize) -> Self {
264        self.array = Some(crate::path::ArrayIndexKind::Specific(index));
265        self
266    }
267
268    /// Create a segment with current-index marker (`[^]`).
269    pub fn with_array_current(mut self) -> Self {
270        self.array = Some(crate::path::ArrayIndexKind::Current);
271        self
272    }
273
274    /// Create a quoted string segment without array marker.
275    pub fn quoted_string(s: impl Into<String>) -> Self {
276        Self {
277            key: SourceKey::quoted(s),
278            array: None,
279        }
280    }
281
282    /// Create a literal string segment (single-quoted) without array marker.
283    pub fn literal_string(s: impl Into<String>) -> Self {
284        Self {
285            key: SourceKey::literal(s),
286            array: None,
287        }
288    }
289}
290
291/// Syntax style for string keys (for round-trip formatting).
292///
293/// This preserves whether a string key was written with quotes, single quotes, or delimiters,
294/// similar to how `SyntaxHint` preserves code block formatting.
295#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
296pub enum StringStyle {
297    /// Quoted string: `"..."`
298    #[default]
299    Quoted,
300    /// Literal string (single-quoted): `'...'`
301    /// Content is taken literally, no escape processing
302    Literal,
303    /// Delimited literal string: `<'...'>`, `<<'...'>>`, `<<<'...'>>>`
304    /// The u8 indicates the delimiter level (1, 2, or 3)
305    /// Content is taken literally, no escape processing
306    DelimitedLitStr(u8),
307    /// Delimited code: `<`...`>`, `<<`...`>>`, `<<<`...`>>>`
308    /// The u8 indicates the delimiter level (1, 2, or 3)
309    DelimitedCode(u8),
310}
311
312/// A key in source representation.
313///
314/// This determines how the key should be rendered in the output.
315#[derive(Debug, Clone)]
316pub enum SourceKey {
317    /// Bare identifier: `foo`, `bar_baz`
318    Ident(Identifier),
319
320    /// Extension namespace: `$variant`, `$eure`
321    Extension(Identifier),
322
323    /// Hole key: `!` or `!label`
324    Hole(Option<Identifier>),
325
326    /// String key with syntax style hint.
327    /// - `StringStyle::Quoted`: `"hello world"`
328    /// - `StringStyle::Literal`: `'hello world'`
329    ///
330    /// Note: `PartialEq` ignores the style - only content matters for equality.
331    String(String, StringStyle),
332
333    /// Integer key: `123`
334    Integer(i64),
335
336    /// Tuple key: `(1, "a")`
337    Tuple(Vec<SourceKey>),
338
339    /// Tuple index: `#0`, `#1`
340    TupleIndex(u8),
341}
342
343impl PartialEq for SourceKey {
344    fn eq(&self, other: &Self) -> bool {
345        match (self, other) {
346            (Self::Ident(a), Self::Ident(b)) => a == b,
347            (Self::Extension(a), Self::Extension(b)) => a == b,
348            (Self::Hole(a), Self::Hole(b)) => a == b,
349            // String equality ignores style hint - only content matters
350            (Self::String(a, _), Self::String(b, _)) => a == b,
351            (Self::Integer(a), Self::Integer(b)) => a == b,
352            (Self::Tuple(a), Self::Tuple(b)) => a == b,
353            (Self::TupleIndex(a), Self::TupleIndex(b)) => a == b,
354            _ => false,
355        }
356    }
357}
358
359impl Eq for SourceKey {}
360
361impl SourceKey {
362    /// Create a hole key: `!` or `!label`.
363    pub fn hole(label: Option<Identifier>) -> Self {
364        SourceKey::Hole(label)
365    }
366
367    /// Create a quoted string key: `"..."`
368    pub fn quoted(s: impl Into<String>) -> Self {
369        SourceKey::String(s.into(), StringStyle::Quoted)
370    }
371
372    /// Create a literal string key (single-quoted): `'...'`
373    pub fn literal(s: impl Into<String>) -> Self {
374        SourceKey::String(s.into(), StringStyle::Literal)
375    }
376
377    /// Create a delimited literal string key: `<'...'>`, `<<'...'>>`, `<<<'...'>>>`
378    pub fn delimited_lit_str(s: impl Into<String>, level: u8) -> Self {
379        SourceKey::String(s.into(), StringStyle::DelimitedLitStr(level))
380    }
381
382    /// Create a delimited code key: `<`...`>`, `<<`...`>>`, `<<<`...`>>>`
383    pub fn delimited_code(s: impl Into<String>, level: u8) -> Self {
384        SourceKey::String(s.into(), StringStyle::DelimitedCode(level))
385    }
386}
387
388impl From<Identifier> for SourceKey {
389    fn from(id: Identifier) -> Self {
390        SourceKey::Ident(id)
391    }
392}
393
394impl From<i64> for SourceKey {
395    fn from(n: i64) -> Self {
396        SourceKey::Integer(n)
397    }
398}
399
400// ============================================================================
401// Comment and Trivia Types
402// ============================================================================
403
404/// A comment in the source.
405#[derive(Debug, Clone, PartialEq, Eq)]
406pub enum Comment {
407    /// Line comment: `// comment`
408    Line(String),
409    /// Block comment: `/* comment */`
410    Block(String),
411}
412
413impl Comment {
414    /// Create a line comment.
415    pub fn line(s: impl Into<String>) -> Self {
416        Comment::Line(s.into())
417    }
418
419    /// Create a block comment.
420    pub fn block(s: impl Into<String>) -> Self {
421        Comment::Block(s.into())
422    }
423
424    /// Get the comment text content.
425    pub fn text(&self) -> &str {
426        match self {
427            Comment::Line(s) | Comment::Block(s) => s,
428        }
429    }
430}
431
432/// Trivia: comments and blank lines that appear between statements.
433///
434/// Trivia is used to preserve whitespace and comments for round-trip formatting.
435#[derive(Debug, Clone, PartialEq, Eq)]
436pub enum Trivia {
437    /// A comment (line or block)
438    Comment(Comment),
439    /// A blank line (empty line separating statements)
440    BlankLine,
441}
442
443impl Trivia {
444    /// Create a line comment trivia.
445    pub fn line_comment(s: impl Into<String>) -> Self {
446        Trivia::Comment(Comment::Line(s.into()))
447    }
448
449    /// Create a block comment trivia.
450    pub fn block_comment(s: impl Into<String>) -> Self {
451        Trivia::Comment(Comment::Block(s.into()))
452    }
453
454    /// Create a blank line trivia.
455    pub fn blank_line() -> Self {
456        Trivia::BlankLine
457    }
458}
459
460impl From<Comment> for Trivia {
461    fn from(comment: Comment) -> Self {
462        Trivia::Comment(comment)
463    }
464}
465
466// ============================================================================
467// Builder Helpers
468// ============================================================================
469
470impl EureSource {
471    /// Create an empty EureSource.
472    pub fn new() -> Self {
473        Self::default()
474    }
475
476    /// Add a binding to this source.
477    pub fn push_binding(&mut self, binding: BindingSource) {
478        self.bindings.push(binding);
479    }
480
481    /// Add a section to this source.
482    pub fn push_section(&mut self, section: SectionSource) {
483        self.sections.push(section);
484    }
485}
486
487impl BindingSource {
488    /// Create a value binding: `path = value`
489    pub fn value(path: SourcePath, node: NodeId) -> Self {
490        Self {
491            trivia_before: Vec::new(),
492            path,
493            bind: BindSource::Value(node),
494            trailing_comment: None,
495        }
496    }
497
498    /// Create a block binding: `path { eure }`
499    pub fn block(path: SourcePath, source_id: SourceId) -> Self {
500        Self {
501            trivia_before: Vec::new(),
502            path,
503            bind: BindSource::Block(source_id),
504            trailing_comment: None,
505        }
506    }
507
508    /// Add a trailing comment.
509    pub fn with_trailing_comment(mut self, comment: Comment) -> Self {
510        self.trailing_comment = Some(comment);
511        self
512    }
513
514    /// Add trivia before this binding.
515    pub fn with_trivia(mut self, trivia: Vec<Trivia>) -> Self {
516        self.trivia_before = trivia;
517        self
518    }
519
520    /// Create an array binding with per-element layout: `path = [...]`
521    pub fn array(path: SourcePath, node: NodeId, elements: Vec<ArrayElementSource>) -> Self {
522        Self {
523            trivia_before: Vec::new(),
524            path,
525            bind: BindSource::Array { node, elements },
526            trailing_comment: None,
527        }
528    }
529}
530
531impl SectionSource {
532    /// Create a section with items body: `@ path` (items follow)
533    pub fn items(path: SourcePath, value: Option<NodeId>, bindings: Vec<BindingSource>) -> Self {
534        Self {
535            trivia_before: Vec::new(),
536            path,
537            body: SectionBody::Items { value, bindings },
538            trailing_comment: None,
539        }
540    }
541
542    /// Create a section with block body: `@ path { eure }`
543    pub fn block(path: SourcePath, source_id: SourceId) -> Self {
544        Self {
545            trivia_before: Vec::new(),
546            path,
547            body: SectionBody::Block(source_id),
548            trailing_comment: None,
549        }
550    }
551
552    /// Add a trailing comment.
553    pub fn with_trailing_comment(mut self, comment: Comment) -> Self {
554        self.trailing_comment = Some(comment);
555        self
556    }
557
558    /// Add trivia before this section.
559    pub fn with_trivia(mut self, trivia: Vec<Trivia>) -> Self {
560        self.trivia_before = trivia;
561        self
562    }
563}
564
565impl ArrayElementSource {
566    /// Create an array element source.
567    pub fn new(index: usize) -> Self {
568        Self {
569            trivia_before: Vec::new(),
570            index,
571            trailing_comment: None,
572        }
573    }
574
575    /// Add trivia before this element.
576    pub fn with_trivia(mut self, trivia: Vec<Trivia>) -> Self {
577        self.trivia_before = trivia;
578        self
579    }
580
581    /// Add a trailing comment.
582    pub fn with_trailing_comment(mut self, comment: Comment) -> Self {
583        self.trailing_comment = Some(comment);
584        self
585    }
586}
587
588#[cfg(test)]
589mod tests {
590    use super::*;
591
592    #[test]
593    fn test_source_path_segment_ident() {
594        let actual = SourcePathSegment::ident(Identifier::new_unchecked("foo"));
595        let expected = SourcePathSegment {
596            key: SourceKey::Ident(Identifier::new_unchecked("foo")),
597            array: None,
598        };
599        assert_eq!(actual, expected);
600    }
601
602    #[test]
603    fn test_source_path_segment_with_array_push() {
604        let actual = SourcePathSegment::ident(Identifier::new_unchecked("items")).with_array_push();
605        let expected = SourcePathSegment {
606            key: SourceKey::Ident(Identifier::new_unchecked("items")),
607            array: Some(crate::path::ArrayIndexKind::Push),
608        };
609        assert_eq!(actual, expected);
610    }
611
612    #[test]
613    fn test_source_path_segment_with_array_index() {
614        let actual =
615            SourcePathSegment::ident(Identifier::new_unchecked("items")).with_array_index(0);
616        let expected = SourcePathSegment {
617            key: SourceKey::Ident(Identifier::new_unchecked("items")),
618            array: Some(crate::path::ArrayIndexKind::Specific(0)),
619        };
620        assert_eq!(actual, expected);
621    }
622
623    #[test]
624    fn test_source_path_segment_with_array_current() {
625        let actual =
626            SourcePathSegment::ident(Identifier::new_unchecked("items")).with_array_current();
627        let expected = SourcePathSegment {
628            key: SourceKey::Ident(Identifier::new_unchecked("items")),
629            array: Some(crate::path::ArrayIndexKind::Current),
630        };
631        assert_eq!(actual, expected);
632    }
633
634    #[test]
635    fn test_binding_source_value() {
636        let path = vec![SourcePathSegment::ident(Identifier::new_unchecked("foo"))];
637        let binding = BindingSource::value(path.clone(), NodeId(1));
638        assert_eq!(binding.path, path);
639        assert!(matches!(binding.bind, BindSource::Value(NodeId(1))));
640        assert!(binding.trivia_before.is_empty());
641    }
642
643    #[test]
644    fn test_binding_source_block() {
645        let path = vec![SourcePathSegment::ident(Identifier::new_unchecked("user"))];
646        let binding = BindingSource::block(path.clone(), SourceId(1));
647        assert_eq!(binding.path, path);
648        assert!(matches!(binding.bind, BindSource::Block(SourceId(1))));
649        assert!(binding.trivia_before.is_empty());
650    }
651
652    #[test]
653    fn test_binding_with_trivia() {
654        let path = vec![SourcePathSegment::ident(Identifier::new_unchecked("foo"))];
655        let trivia = vec![Trivia::BlankLine, Trivia::line_comment("comment")];
656        let binding = BindingSource::value(path.clone(), NodeId(1)).with_trivia(trivia.clone());
657        assert_eq!(binding.trivia_before, trivia);
658    }
659
660    #[test]
661    fn test_section_source_items() {
662        let path = vec![SourcePathSegment::ident(Identifier::new_unchecked(
663            "server",
664        ))];
665        let section = SectionSource::items(path.clone(), None, vec![]);
666        assert_eq!(section.path, path);
667        assert!(matches!(
668            section.body,
669            SectionBody::Items {
670                value: None,
671                bindings
672            } if bindings.is_empty()
673        ));
674        assert!(section.trivia_before.is_empty());
675    }
676
677    #[test]
678    fn test_section_source_block() {
679        let path = vec![SourcePathSegment::ident(Identifier::new_unchecked(
680            "config",
681        ))];
682        let section = SectionSource::block(path.clone(), SourceId(2));
683        assert_eq!(section.path, path);
684        assert!(matches!(section.body, SectionBody::Block(SourceId(2))));
685        assert!(section.trivia_before.is_empty());
686    }
687
688    #[test]
689    fn test_section_with_trivia() {
690        let path = vec![SourcePathSegment::ident(Identifier::new_unchecked(
691            "server",
692        ))];
693        let trivia = vec![Trivia::BlankLine];
694        let section = SectionSource::items(path.clone(), None, vec![]).with_trivia(trivia.clone());
695        assert_eq!(section.trivia_before, trivia);
696    }
697
698    #[test]
699    fn test_source_document_empty() {
700        let doc = SourceDocument::empty();
701        assert_eq!(doc.sources.len(), 1);
702        assert_eq!(doc.root, SourceId(0));
703        assert!(doc.root_source().bindings.is_empty());
704        assert!(doc.root_source().sections.is_empty());
705    }
706}