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