Skip to main content

logicaffeine_compile/
sourcemap.rs

1//! Source map for the diagnostic bridge.
2//!
3//! Maps generated Rust code back to LOGOS source positions, enabling
4//! friendly error messages for ownership/lifetime errors detected by rustc.
5//!
6//! # Architecture
7//!
8//! ```text
9//! LOGOS Source    CodeGen    Rust Source    rustc    Diagnostics
10//!     │              │            │           │           │
11//!     │ SourceMap    │            │           │           │
12//!     │◄─────────────┼────────────┼───────────┼───────────┤
13//!     │   line 5     │            │  line 12  │   E0382   │
14//!     │   span       │            │           │   line 12 │
15//!     └──────────────┴────────────┴───────────┴───────────┘
16//! ```
17//!
18//! # Usage
19//!
20//! The source map is built during code generation by calling builder methods
21//! at each statement. The diagnostic bridge then uses
22//! [`SourceMap::find_nearest_span`] to translate rustc line numbers.
23
24use crate::intern::Symbol;
25use crate::token::Span;
26use std::collections::HashMap;
27
28/// Semantic role of a variable in LOGOS ownership semantics.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum OwnershipRole {
31    /// The object being moved in "Give X to Y"
32    GiveObject,
33    /// The recipient in "Give X to Y"
34    GiveRecipient,
35    /// The object being borrowed in "Show X to Y"
36    ShowObject,
37    /// The recipient in "Show X to Y"
38    ShowRecipient,
39    /// A Let-bound variable
40    LetBinding,
41    /// Target of a Set statement
42    SetTarget,
43    /// Variable allocated inside a Zone
44    ZoneLocal,
45}
46
47/// Variable origin tracking for error translation.
48#[derive(Debug, Clone)]
49pub struct VarOrigin {
50    pub logos_name: Symbol,
51    pub span: Span,
52    pub role: OwnershipRole,
53}
54
55/// Maps generated Rust code back to LOGOS source.
56#[derive(Debug, Clone, Default)]
57pub struct SourceMap {
58    /// Maps line in generated Rust -> Span in LOGOS source
59    line_to_span: HashMap<u32, Span>,
60
61    /// Maps generated Rust variable names -> LOGOS origin info
62    var_origins: HashMap<String, VarOrigin>,
63
64    /// The original LOGOS source code (for error display)
65    logos_source: String,
66}
67
68impl SourceMap {
69    /// Create a new empty source map.
70    pub fn new(logos_source: String) -> Self {
71        Self {
72            line_to_span: HashMap::new(),
73            var_origins: HashMap::new(),
74            logos_source,
75        }
76    }
77
78    /// Get the LOGOS span for a given Rust line number.
79    pub fn get_span_for_line(&self, line: u32) -> Option<Span> {
80        self.line_to_span.get(&line).copied()
81    }
82
83    /// Get the origin info for a Rust variable name.
84    pub fn get_var_origin(&self, rust_var: &str) -> Option<&VarOrigin> {
85        self.var_origins.get(rust_var)
86    }
87
88    /// Get the original LOGOS source.
89    pub fn logos_source(&self) -> &str {
90        &self.logos_source
91    }
92
93    /// Every recorded (rust line, LOGOS span) pair — the wiring's test surface.
94    pub fn line_span_entries(&self) -> Vec<(u32, Span)> {
95        let mut entries: Vec<(u32, Span)> =
96            self.line_to_span.iter().map(|(l, s)| (*l, *s)).collect();
97        entries.sort_by_key(|(l, _)| *l);
98        entries
99    }
100
101    /// Find the closest LOGOS span by searching nearby lines.
102    pub fn find_nearest_span(&self, rust_line: u32) -> Option<Span> {
103        // Try exact match first
104        if let Some(span) = self.line_to_span.get(&rust_line) {
105            return Some(*span);
106        }
107
108        // Search nearby lines (within 5 lines)
109        for offset in 1..=5 {
110            if rust_line > offset {
111                if let Some(span) = self.line_to_span.get(&(rust_line - offset)) {
112                    return Some(*span);
113                }
114            }
115            if let Some(span) = self.line_to_span.get(&(rust_line + offset)) {
116                return Some(*span);
117            }
118        }
119
120        None
121    }
122}
123
124/// Builder for constructing a SourceMap during code generation.
125#[derive(Debug)]
126pub struct SourceMapBuilder {
127    current_line: u32,
128    map: SourceMap,
129}
130
131impl SourceMapBuilder {
132    /// Create a new builder with the LOGOS source.
133    pub fn new(logos_source: &str) -> Self {
134        Self {
135            current_line: 1,
136            map: SourceMap::new(logos_source.to_string()),
137        }
138    }
139
140    /// Record a mapping from current Rust line to LOGOS span.
141    pub fn record_line(&mut self, logos_span: Span) {
142        self.map.line_to_span.insert(self.current_line, logos_span);
143    }
144
145    /// Record a mapping for an explicit Rust line — the post-hoc recording
146    /// style used by mapped codegen, which counts emitted newlines instead of
147    /// threading a cursor through every emission point.
148    pub fn record_line_at(&mut self, line: u32, logos_span: Span) {
149        self.map.line_to_span.insert(line, logos_span);
150    }
151
152    /// Record a variable origin.
153    pub fn record_var(&mut self, rust_name: &str, logos_name: Symbol, span: Span, role: OwnershipRole) {
154        self.map.var_origins.insert(
155            rust_name.to_string(),
156            VarOrigin {
157                logos_name,
158                span,
159                role,
160            },
161        );
162    }
163
164    /// Advance to the next line.
165    pub fn newline(&mut self) {
166        self.current_line += 1;
167    }
168
169    /// Add multiple newlines.
170    pub fn add_lines(&mut self, count: u32) {
171        self.current_line += count;
172    }
173
174    /// Get current line number.
175    pub fn current_line(&self) -> u32 {
176        self.current_line
177    }
178
179    /// Build the final source map.
180    pub fn build(self) -> SourceMap {
181        self.map
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn source_map_stores_line_mappings() {
191        let mut map = SourceMap::new("Let x be 5.".to_string());
192        map.line_to_span.insert(1, Span::new(0, 11));
193
194        assert_eq!(map.get_span_for_line(1), Some(Span::new(0, 11)));
195        assert_eq!(map.get_span_for_line(2), None);
196    }
197
198    #[test]
199    fn source_map_builder_tracks_lines() {
200        let mut builder = SourceMapBuilder::new("test source");
201        assert_eq!(builder.current_line(), 1);
202
203        builder.newline();
204        assert_eq!(builder.current_line(), 2);
205
206        builder.add_lines(3);
207        assert_eq!(builder.current_line(), 5);
208    }
209
210    #[test]
211    fn source_map_builder_records_spans() {
212        let mut builder = SourceMapBuilder::new("Let x be 5.\nLet y be 10.");
213        builder.record_line(Span::new(0, 11));
214        builder.newline();
215        builder.record_line(Span::new(12, 24));
216
217        let map = builder.build();
218        assert_eq!(map.get_span_for_line(1), Some(Span::new(0, 11)));
219        assert_eq!(map.get_span_for_line(2), Some(Span::new(12, 24)));
220    }
221
222    #[test]
223    fn find_nearest_span_searches_nearby() {
224        let mut builder = SourceMapBuilder::new("source");
225        builder.record_line(Span::new(0, 10));
226        builder.add_lines(5);
227        // Line 1 has span, lines 2-6 don't
228
229        let map = builder.build();
230        // Line 3 should find line 1's span
231        assert_eq!(map.find_nearest_span(3), Some(Span::new(0, 10)));
232    }
233}