Skip to main content

zerodds_ast/facet/
source.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Source provenance. Spec §4.7.
4
5use alloc::string::{String, ToString};
6use alloc::vec::Vec;
7
8/// A byte range in a source file.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub struct ByteSpan {
11    /// Inclusive start offset.
12    pub start: u32,
13    /// Exclusive end offset.
14    pub end: u32,
15}
16
17/// One step of preprocessor expansion.
18///
19/// The chain lets a diagnostic point at the macro that produced a
20/// construct as well as at the place the construct textually appears.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct MacroExpansion {
23    /// Macro name.
24    pub name: String,
25    /// Where the macro was invoked, as a range of the file in `path`.
26    pub invoked_at: Option<ByteSpan>,
27    /// The file the invocation is written in, as the frontend saw it: an
28    /// inner expansion of a chain sits in the body of the macro around
29    /// it, which may come from another file than the node.
30    pub path: Option<String>,
31}
32
33/// Where a node came from.
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub enum SourceOrigin {
36    /// Parsed from a file.
37    File {
38        /// Path as the frontend saw it.
39        path: String,
40    },
41    /// Created programmatically, with no source text.
42    Synthetic {
43        /// Why it was synthesised.
44        ///
45        /// Owned rather than `&'static str`: a snapshot that replaced
46        /// every reason with one fixed sentence would answer "where did
47        /// this come from" with the loader's answer, not the model's.
48        reason: String,
49    },
50    /// Part of the built-in set the model always carries.
51    Builtin,
52}
53
54/// Provenance, always present on every node.
55///
56/// A node created programmatically records a synthetic origin rather
57/// than none, so "where did this come from" always has an answer.
58/// Spec §4.7.
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct SourceFacet {
61    /// Where the node came from.
62    pub origin: SourceOrigin,
63    /// Byte range in the source file, when there is one.
64    pub span: Option<ByteSpan>,
65    /// Preprocessor expansions that produced this construct, outermost
66    /// first.
67    pub macro_chain: Vec<MacroExpansion>,
68    /// Documentation comments attached to the declaration.
69    pub doc_comments: Vec<String>,
70}
71
72impl SourceFacet {
73    /// Provenance for a programmatically created node.
74    #[must_use]
75    pub fn synthetic(reason: &str) -> Self {
76        Self {
77            origin: SourceOrigin::Synthetic {
78                reason: reason.to_string(),
79            },
80            span: None,
81            macro_chain: Vec::new(),
82            doc_comments: Vec::new(),
83        }
84    }
85
86    /// Provenance for a built-in declaration.
87    #[must_use]
88    pub const fn builtin() -> Self {
89        Self {
90            origin: SourceOrigin::Builtin,
91            span: None,
92            macro_chain: Vec::new(),
93            doc_comments: Vec::new(),
94        }
95    }
96
97    /// Provenance for a node parsed from a file.
98    #[must_use]
99    pub fn file(path: String, span: Option<ByteSpan>) -> Self {
100        Self {
101            origin: SourceOrigin::File { path },
102            span,
103            macro_chain: Vec::new(),
104            doc_comments: Vec::new(),
105        }
106    }
107
108    /// Whether the node came from parsed source text.
109    #[must_use]
110    pub const fn is_from_file(&self) -> bool {
111        matches!(self.origin, SourceOrigin::File { .. })
112    }
113}
114
115impl Default for SourceFacet {
116    fn default() -> Self {
117        Self::synthetic("created through the model API")
118    }
119}