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