Skip to main content

stack_compiler/
source_map.rs

1//! Rust source-map sidecar for post-compiler diagnostics.
2
3use crate::ast::{self, DiagramMember, GroupMember, LayoutStatement, NodeProperty};
4use crate::diagnostic::Span;
5use crate::lossless::{self, TokenKind};
6
7/// Whether a semantic value was authored or supplied by a language default.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum SourceOrigin {
10    /// The value was written in source at this end-exclusive span.
11    Authored(Span),
12    /// The value was omitted from source.
13    Omitted,
14}
15
16impl SourceOrigin {
17    /// Returns the authored span, or `None` for an omitted value.
18    pub const fn span(self) -> Option<Span> {
19        match self {
20            Self::Authored(span) => Some(span),
21            Self::Omitted => None,
22        }
23    }
24}
25
26/// Source origin for one node's semantic icon value.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct NodeIconSource {
29    /// Globally unique node identifier.
30    pub node_id: String,
31    /// Authored icon-string span or omitted default.
32    pub origin: SourceOrigin,
33}
34
35/// Semantic scope of a layout block.
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum LayoutScope {
38    /// The root diagram layout.
39    Diagram,
40    /// The layout of the group with this globally unique identifier.
41    Group(String),
42}
43
44/// Source origin for one scope's semantic order hint.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct LayoutOrderSource {
47    /// Diagram or group identity.
48    pub scope: LayoutScope,
49    /// Complete authored `order` statement span or omitted value.
50    pub origin: SourceOrigin,
51}
52
53/// Deterministic source locations associated with normalized semantic identities.
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct SourceMap {
56    theme: SourceOrigin,
57    node_icons: Vec<NodeIconSource>,
58    layout_orders: Vec<LayoutOrderSource>,
59}
60
61impl SourceMap {
62    pub(crate) fn from_document(document: &ast::Document, lossless: &lossless::Document) -> Self {
63        let theme = document
64            .diagram
65            .members
66            .iter()
67            .find_map(|member| match member {
68                DiagramMember::Theme(theme) => Some(SourceOrigin::Authored(theme.identifier.span)),
69                _ => None,
70            })
71            .unwrap_or(SourceOrigin::Omitted);
72
73        let mut node_icons = Vec::new();
74        let mut layout_orders = vec![LayoutOrderSource {
75            scope: LayoutScope::Diagram,
76            origin: layout_order_origin(
77                document
78                    .diagram
79                    .members
80                    .iter()
81                    .find_map(diagram_member_layout),
82                lossless,
83            ),
84        }];
85
86        for member in &document.diagram.members {
87            match member {
88                DiagramMember::Node(node) => node_icons.push(node_icon_source(node)),
89                DiagramMember::Group(group) => {
90                    append_group_sources(group, lossless, &mut node_icons, &mut layout_orders);
91                }
92                DiagramMember::Edge(_) | DiagramMember::Theme(_) | DiagramMember::Layout(_) => {}
93            }
94        }
95
96        Self {
97            theme,
98            node_icons,
99            layout_orders,
100        }
101    }
102
103    /// Returns the authored diagram theme identifier or omitted default.
104    pub const fn theme(&self) -> SourceOrigin {
105        self.theme
106    }
107
108    /// Returns node icon entries in depth-first declaration order.
109    pub fn node_icons(&self) -> &[NodeIconSource] {
110        &self.node_icons
111    }
112
113    /// Finds a node's authored icon string or omitted default by node identifier.
114    pub fn node_icon(&self, node_id: &str) -> Option<SourceOrigin> {
115        self.node_icons
116            .iter()
117            .find(|entry| entry.node_id == node_id)
118            .map(|entry| entry.origin)
119    }
120
121    /// Returns layout order entries with the diagram first, then groups in depth-first order.
122    pub fn layout_orders(&self) -> &[LayoutOrderSource] {
123        &self.layout_orders
124    }
125
126    /// Returns the diagram's authored order statement or omitted value.
127    pub fn diagram_order(&self) -> SourceOrigin {
128        self.layout_orders
129            .first()
130            .map_or(SourceOrigin::Omitted, |entry| entry.origin)
131    }
132
133    /// Finds a group's authored order statement or omitted value by group identifier.
134    pub fn group_order(&self, group_id: &str) -> Option<SourceOrigin> {
135        self.layout_orders
136            .iter()
137            .find_map(|entry| match &entry.scope {
138                LayoutScope::Group(identifier) if identifier == group_id => Some(entry.origin),
139                LayoutScope::Diagram | LayoutScope::Group(_) => None,
140            })
141    }
142}
143
144fn diagram_member_layout(member: &DiagramMember) -> Option<&ast::Layout> {
145    match member {
146        DiagramMember::Layout(layout) => Some(layout),
147        _ => None,
148    }
149}
150
151fn group_member_layout(member: &GroupMember) -> Option<&ast::Layout> {
152    match member {
153        GroupMember::Layout(layout) => Some(layout),
154        _ => None,
155    }
156}
157
158fn node_icon_source(node: &ast::Node) -> NodeIconSource {
159    let origin = node
160        .properties
161        .iter()
162        .find_map(|property| match property {
163            NodeProperty::Icon(icon) => Some(SourceOrigin::Authored(icon.span)),
164            NodeProperty::Kind(_) | NodeProperty::Detail(_) => None,
165        })
166        .unwrap_or(SourceOrigin::Omitted);
167
168    NodeIconSource {
169        node_id: node.identifier.value.clone(),
170        origin,
171    }
172}
173
174fn append_group_sources(
175    group: &ast::Group,
176    lossless: &lossless::Document,
177    node_icons: &mut Vec<NodeIconSource>,
178    layout_orders: &mut Vec<LayoutOrderSource>,
179) {
180    layout_orders.push(LayoutOrderSource {
181        scope: LayoutScope::Group(group.identifier.value.clone()),
182        origin: layout_order_origin(group.members.iter().find_map(group_member_layout), lossless),
183    });
184
185    for member in &group.members {
186        match member {
187            GroupMember::Node(node) => node_icons.push(node_icon_source(node)),
188            GroupMember::Group(child) => {
189                append_group_sources(child, lossless, node_icons, layout_orders);
190            }
191            GroupMember::Layout(_) => {}
192        }
193    }
194}
195
196fn layout_order_origin(
197    layout: Option<&ast::Layout>,
198    lossless: &lossless::Document,
199) -> SourceOrigin {
200    let Some(list) = layout.and_then(|layout| {
201        layout
202            .statements
203            .iter()
204            .find_map(|statement| match statement {
205                LayoutStatement::Order(list) => Some(list),
206                LayoutStatement::Direction(_) | LayoutStatement::RankSame(_) => None,
207            })
208    }) else {
209        return SourceOrigin::Omitted;
210    };
211
212    SourceOrigin::Authored(order_statement_span(list.span, lossless.tokens()))
213}
214
215fn order_statement_span(list_span: Span, tokens: &[lossless::Token]) -> Span {
216    let keyword = tokens
217        .iter()
218        .take_while(|token| token.span.end.byte_offset <= list_span.start.byte_offset)
219        .filter(|token| !matches!(token.kind, TokenKind::Whitespace | TokenKind::LineComment))
220        .last();
221
222    keyword.map_or(list_span, |keyword| Span::covering(keyword.span, list_span))
223}
224
225#[cfg(test)]
226mod tests {
227    use super::SourceOrigin;
228
229    #[test]
230    fn source_origin_returns_only_authored_spans() {
231        let span = crate::diagnostic::Span::point(crate::diagnostic::SourcePosition::start());
232        assert_eq!(SourceOrigin::Authored(span).span(), Some(span));
233        assert_eq!(SourceOrigin::Omitted.span(), None);
234    }
235}