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}