macroforge_ts 0.1.80

TypeScript macro expansion engine - write compile-time macros in Rust
Documentation
use napi_derive::napi;

use crate::api_types::{
    GeneratedRegionResult, MappingSegmentResult, SourceMappingResult, SpanResult,
};

// ============================================================================
// Position Mapper (Optimized with Binary Search)
// ============================================================================

/// Bidirectional position mapper for translating between original and expanded source positions.
///
/// This mapper enables IDE features like error reporting, go-to-definition, and hover
/// to work correctly with macro-expanded code by translating positions between the
/// original source (what the user wrote) and the expanded source (what the compiler sees).
///
/// # Performance
///
/// Position lookups use binary search for O(log n) complexity, where n is the number
/// of mapping segments. This is critical for responsive IDE interactions.
///
/// # Example
///
/// ```javascript
/// const mapper = new PositionMapper(sourceMapping);
///
/// // Convert original position to expanded
/// const expandedPos = mapper.original_to_expanded(42);
///
/// // Convert expanded position back to original (if not in generated code)
/// const originalPos = mapper.expanded_to_original(100);
///
/// // Check if a position is in macro-generated code
/// if (mapper.is_in_generated(pos)) {
///     const macro = mapper.generated_by(pos); // e.g., "Debug"
/// }
/// ```
#[napi(js_name = "PositionMapper")]
pub struct NativePositionMapper {
    /// Mapping segments sorted by position for binary search.
    segments: Vec<MappingSegmentResult>,
    /// Regions marking code generated by macros.
    generated_regions: Vec<GeneratedRegionResult>,
}

/// Wrapper around `NativePositionMapper` for NAPI compatibility.
///
/// This provides the same functionality as `NativePositionMapper` but with a
/// different JavaScript class name. Used internally by [`NativePlugin::get_mapper`].
#[napi(js_name = "NativeMapper")]
pub struct NativeMapper {
    /// The underlying position mapper implementation.
    pub(crate) inner: NativePositionMapper,
}

#[napi]
impl NativePositionMapper {
    /// Creates a new position mapper from source mapping data.
    ///
    /// # Arguments
    ///
    /// * `mapping` - The source mapping result from macro expansion
    ///
    /// # Returns
    ///
    /// A new `NativePositionMapper` ready for position translation.
    #[napi(constructor)]
    pub fn new(mapping: SourceMappingResult) -> Self {
        Self {
            segments: mapping.segments,
            generated_regions: mapping.generated_regions,
        }
    }

    /// Checks if this mapper has no mapping data.
    ///
    /// An empty mapper indicates no transformations occurred, so position
    /// translation is an identity operation.
    ///
    /// # Returns
    ///
    /// `true` if there are no segments and no generated regions.
    #[napi(js_name = "isEmpty")]
    pub fn is_empty(&self) -> bool {
        self.segments.is_empty() && self.generated_regions.is_empty()
    }

    /// Converts a position in the original source to the corresponding position in expanded source.
    ///
    /// Uses binary search for O(log n) lookup performance.
    ///
    /// # Arguments
    ///
    /// * `pos` - Byte offset in the original source
    ///
    /// # Returns
    ///
    /// The corresponding byte offset in the expanded source. If the position falls
    /// in a gap between segments, returns the position unchanged. If after the last
    /// segment, extrapolates based on the delta.
    ///
    /// # Algorithm
    ///
    /// 1. Binary search to find the segment containing or after `pos`
    /// 2. If inside a segment, compute offset within segment and translate
    /// 3. If after all segments, extrapolate from the last segment
    /// 4. Otherwise, return position unchanged (gap or before first segment)
    #[napi]
    pub fn original_to_expanded(&self, pos: u32) -> u32 {
        // Binary search to find the first segment where original_end > pos.
        // This gives us the segment that might contain pos, or the one after it.
        let idx = self.segments.partition_point(|seg| seg.original_end <= pos);

        if let Some(seg) = self.segments.get(idx) {
            // Check if pos is actually inside this segment (it might be in a gap)
            if pos >= seg.original_start && pos < seg.original_end {
                // Position is within this segment - calculate the offset and translate
                let offset = pos - seg.original_start;
                return seg.expanded_start + offset;
            }
        }

        // Handle case where position is after the last segment.
        // Extrapolate by adding the delta from the end of the last segment.
        if let Some(last) = self.segments.last()
            && pos >= last.original_end
        {
            let delta = pos - last.original_end;
            return last.expanded_end + delta;
        }

        // Fallback for positions before first segment or in gaps between segments.
        // Return unchanged as an identity mapping.
        pos
    }

    /// Converts a position in the expanded source back to the original source position.
    ///
    /// Returns `None` if the position is inside macro-generated code that has no
    /// corresponding location in the original source.
    ///
    /// # Arguments
    ///
    /// * `pos` - Byte offset in the expanded source
    ///
    /// # Returns
    ///
    /// `Some(original_pos)` if the position maps to original code,
    /// `None` if the position is in macro-generated code.
    #[napi]
    pub fn expanded_to_original(&self, pos: u32) -> Option<u32> {
        // First check if the position is in a generated region (no original mapping)
        if self.is_in_generated(pos) {
            return None;
        }

        // Binary search to find the segment containing or after this expanded position
        let idx = self.segments.partition_point(|seg| seg.expanded_end <= pos);

        if let Some(seg) = self.segments.get(idx)
            && pos >= seg.expanded_start
            && pos < seg.expanded_end
        {
            // Position is within this segment - translate back to original
            let offset = pos - seg.expanded_start;
            return Some(seg.original_start + offset);
        }

        // Handle extrapolation after the last segment
        if let Some(last) = self.segments.last()
            && pos >= last.expanded_end
        {
            let delta = pos - last.expanded_end;
            return Some(last.original_end + delta);
        }

        // Position doesn't map to any segment
        None
    }

    /// Returns the name of the macro that generated code at the given position.
    ///
    /// # Arguments
    ///
    /// * `pos` - Byte offset in the expanded source
    ///
    /// # Returns
    ///
    /// `Some(macro_name)` if the position is inside generated code (e.g., "Debug"),
    /// `None` if the position is in original (non-generated) code.
    #[napi]
    pub fn generated_by(&self, pos: u32) -> Option<String> {
        // Generated regions are typically small in number, so linear scan is acceptable.
        // If this becomes a bottleneck with many macros, could be optimized with binary search.
        self.generated_regions
            .iter()
            .find(|r| pos >= r.start && pos < r.end)
            .map(|r| r.source_macro.clone())
    }

    /// Maps a span (start + length) from expanded source to original source.
    ///
    /// # Arguments
    ///
    /// * `start` - Start byte offset in expanded source
    /// * `length` - Length of the span in bytes
    ///
    /// # Returns
    ///
    /// `Some(SpanResult)` with the mapped span in original source,
    /// `None` if either endpoint is in generated code.
    #[napi]
    pub fn map_span_to_original(&self, start: u32, length: u32) -> Option<SpanResult> {
        let end = start.saturating_add(length);
        // Both start and end must successfully map for the span to be valid
        let original_start = self.expanded_to_original(start)?;
        let original_end = self.expanded_to_original(end)?;

        Some(SpanResult {
            start: original_start,
            length: original_end.saturating_sub(original_start),
        })
    }

    /// Maps a span (start + length) from original source to expanded source.
    ///
    /// This always succeeds since every original position has an expanded equivalent.
    ///
    /// # Arguments
    ///
    /// * `start` - Start byte offset in original source
    /// * `length` - Length of the span in bytes
    ///
    /// # Returns
    ///
    /// A `SpanResult` with the mapped span in expanded source.
    #[napi]
    pub fn map_span_to_expanded(&self, start: u32, length: u32) -> SpanResult {
        let end = start.saturating_add(length);
        let expanded_start = self.original_to_expanded(start);
        let expanded_end = self.original_to_expanded(end);

        SpanResult {
            start: expanded_start,
            length: expanded_end.saturating_sub(expanded_start),
        }
    }

    /// Checks if a position is inside macro-generated code.
    ///
    /// # Arguments
    ///
    /// * `pos` - Byte offset in the expanded source
    ///
    /// # Returns
    ///
    /// `true` if the position is inside a generated region, `false` otherwise.
    #[napi]
    pub fn is_in_generated(&self, pos: u32) -> bool {
        self.generated_regions
            .iter()
            .any(|r| pos >= r.start && pos < r.end)
    }
}

#[napi]
impl NativeMapper {
    /// Creates a new mapper wrapping the given source mapping.
    ///
    /// # Arguments
    ///
    /// * `mapping` - The source mapping result from macro expansion
    #[napi(constructor)]
    pub fn new(mapping: SourceMappingResult) -> Self {
        Self {
            inner: NativePositionMapper::new(mapping),
        }
    }

    /// Checks if this mapper has no mapping data.
    #[napi(js_name = "isEmpty")]
    pub fn is_empty(&self) -> bool {
        self.inner.is_empty()
    }

    /// Converts a position in the original source to expanded source.
    /// See [`NativePositionMapper::original_to_expanded`] for details.
    #[napi]
    pub fn original_to_expanded(&self, pos: u32) -> u32 {
        self.inner.original_to_expanded(pos)
    }

    /// Converts a position in the expanded source back to original.
    /// See [`NativePositionMapper::expanded_to_original`] for details.
    #[napi]
    pub fn expanded_to_original(&self, pos: u32) -> Option<u32> {
        self.inner.expanded_to_original(pos)
    }

    /// Returns the name of the macro that generated code at the given position.
    /// See [`NativePositionMapper::generated_by`] for details.
    #[napi]
    pub fn generated_by(&self, pos: u32) -> Option<String> {
        self.inner.generated_by(pos)
    }

    /// Maps a span from expanded source to original source.
    /// See [`NativePositionMapper::map_span_to_original`] for details.
    #[napi]
    pub fn map_span_to_original(&self, start: u32, length: u32) -> Option<SpanResult> {
        self.inner.map_span_to_original(start, length)
    }

    /// Maps a span from original source to expanded source.
    /// See [`NativePositionMapper::map_span_to_expanded`] for details.
    #[napi]
    pub fn map_span_to_expanded(&self, start: u32, length: u32) -> SpanResult {
        self.inner.map_span_to_expanded(start, length)
    }

    /// Checks if a position is inside macro-generated code.
    /// See [`NativePositionMapper::is_in_generated`] for details.
    #[napi]
    pub fn is_in_generated(&self, pos: u32) -> bool {
        self.inner.is_in_generated(pos)
    }
}