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    /// Find the closest LOGOS span by searching nearby lines.
94    pub fn find_nearest_span(&self, rust_line: u32) -> Option<Span> {
95        // Try exact match first
96        if let Some(span) = self.line_to_span.get(&rust_line) {
97            return Some(*span);
98        }
99
100        // Search nearby lines (within 5 lines)
101        for offset in 1..=5 {
102            if rust_line > offset {
103                if let Some(span) = self.line_to_span.get(&(rust_line - offset)) {
104                    return Some(*span);
105                }
106            }
107            if let Some(span) = self.line_to_span.get(&(rust_line + offset)) {
108                return Some(*span);
109            }
110        }
111
112        None
113    }
114}
115
116/// Builder for constructing a SourceMap during code generation.
117#[derive(Debug)]
118pub struct SourceMapBuilder {
119    current_line: u32,
120    map: SourceMap,
121}
122
123impl SourceMapBuilder {
124    /// Create a new builder with the LOGOS source.
125    pub fn new(logos_source: &str) -> Self {
126        Self {
127            current_line: 1,
128            map: SourceMap::new(logos_source.to_string()),
129        }
130    }
131
132    /// Record a mapping from current Rust line to LOGOS span.
133    pub fn record_line(&mut self, logos_span: Span) {
134        self.map.line_to_span.insert(self.current_line, logos_span);
135    }
136
137    /// Record a variable origin.
138    pub fn record_var(&mut self, rust_name: &str, logos_name: Symbol, span: Span, role: OwnershipRole) {
139        self.map.var_origins.insert(
140            rust_name.to_string(),
141            VarOrigin {
142                logos_name,
143                span,
144                role,
145            },
146        );
147    }
148
149    /// Advance to the next line.
150    pub fn newline(&mut self) {
151        self.current_line += 1;
152    }
153
154    /// Add multiple newlines.
155    pub fn add_lines(&mut self, count: u32) {
156        self.current_line += count;
157    }
158
159    /// Get current line number.
160    pub fn current_line(&self) -> u32 {
161        self.current_line
162    }
163
164    /// Build the final source map.
165    pub fn build(self) -> SourceMap {
166        self.map
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    #[test]
175    fn source_map_stores_line_mappings() {
176        let mut map = SourceMap::new("Let x be 5.".to_string());
177        map.line_to_span.insert(1, Span::new(0, 11));
178
179        assert_eq!(map.get_span_for_line(1), Some(Span::new(0, 11)));
180        assert_eq!(map.get_span_for_line(2), None);
181    }
182
183    #[test]
184    fn source_map_builder_tracks_lines() {
185        let mut builder = SourceMapBuilder::new("test source");
186        assert_eq!(builder.current_line(), 1);
187
188        builder.newline();
189        assert_eq!(builder.current_line(), 2);
190
191        builder.add_lines(3);
192        assert_eq!(builder.current_line(), 5);
193    }
194
195    #[test]
196    fn source_map_builder_records_spans() {
197        let mut builder = SourceMapBuilder::new("Let x be 5.\nLet y be 10.");
198        builder.record_line(Span::new(0, 11));
199        builder.newline();
200        builder.record_line(Span::new(12, 24));
201
202        let map = builder.build();
203        assert_eq!(map.get_span_for_line(1), Some(Span::new(0, 11)));
204        assert_eq!(map.get_span_for_line(2), Some(Span::new(12, 24)));
205    }
206
207    #[test]
208    fn find_nearest_span_searches_nearby() {
209        let mut builder = SourceMapBuilder::new("source");
210        builder.record_line(Span::new(0, 10));
211        builder.add_lines(5);
212        // Line 1 has span, lines 2-6 don't
213
214        let map = builder.build();
215        // Line 3 should find line 1's span
216        assert_eq!(map.find_nearest_span(3), Some(Span::new(0, 10)));
217    }
218}