vize_l4 0.480.0

L4 - the Davinci emission level (codename Stesura): writer, expression rewriting, module assembly and targets
Documentation
//! Source Map v3 emission for generated render code.
//!
//! The codegen writes JavaScript into a single growing byte buffer. While that
//! buffer is being filled, [`SourceMapBuilder`] records *segments*: a generated
//! byte offset paired with the byte offset of the originating node in the
//! template source. Once emission finishes, [`SourceMapBuilder::finish`]
//! resolves both byte offsets to (line, column) — the generated side against the
//! final code buffer, the source side against the original template — and
//! serializes a standard [Source Map v3] document with base64 [VLQ]-encoded
//! `mappings`.
//!
//! Both sides are resolved from byte offsets: the AST stores no line/column
//! at all (Davinci P1-4 — the retired eager `Position` fields were never
//! line-accurate anyway, reporting everything on line 1 with a global-offset
//! column). The byte offset is exact, and computing line/column here from the
//! offsets is the conventional approach for source-map generators anyway.
//!
//! This is intentionally additive: recording a segment never writes to the code
//! buffer, so the generated `code` string is byte-identical whether or not the
//! `source_map` flag is enabled. The map is only assembled when the flag is on.
//!
//! Coverage spans the highest-value anchors — dynamic expressions (identifiers,
//! member expressions, event handlers, directive expressions), element tag
//! names, static attribute names and values, text/comment content, and
//! object-literal prop keys. Identifier-shaped anchors that carry a known source
//! symbol (a prop key, a static attribute name) additionally populate the v3
//! [`names`] array and the per-segment name index, so a consumer can recover the
//! original symbol. Mapping fidelity can still be widened incrementally; see the
//! crate-level codegen docs and the `#1533` tracking issue.
//!
//! [Source Map v3]: https://tc39.es/ecma426/
//! [`names`]: https://tc39.es/ecma426/#json-names
//! [VLQ]: https://en.wikipedia.org/wiki/Variable-length_quantity

use super::line_endings::line_break_len;
use alloc::vec::Vec;
use vize_l0::{FxHashMap, String, ToCompactString};

/// A recorded mapping anchor, as raw byte offsets.
///
/// `generated_offset` is a byte offset into the code buffer captured at the
/// moment the mapped token was about to be written; `source_offset` is the byte
/// offset of the originating node in the original template source. Both are
/// resolved to (line, column) at [`SourceMapBuilder::finish`]. `name` is an
/// index into the builder's `names` table when the anchor carries a known
/// source symbol (a prop key, a static attribute name), or `None` for anonymous
/// anchors; it becomes the optional 5th VLQ field of the segment.
#[derive(Debug, Clone, Copy)]
struct Segment {
    generated_offset: u32,
    source_offset: u32,
    name: Option<u32>,
}

/// Accumulates source-map segments during codegen and serializes a v3 map.
///
/// Lives in the codegen context as an `Option`; it is `Some` only when the
/// `source_map` codegen flag is enabled, so the no-map path pays nothing.
#[derive(Debug, Default)]
pub struct SourceMapBuilder {
    segments: Vec<Segment>,
    /// The v3 `names` array, in insertion order. A segment's `name` is an index
    /// into this vector.
    names: Vec<String>,
    /// Interns symbol names to their index in `names`, so repeated symbols
    /// (e.g. an `id` attribute on many elements) share one `names` entry.
    name_index: FxHashMap<String, u32>,
}

impl SourceMapBuilder {
    /// Create an empty builder.
    pub fn new() -> Self {
        Self {
            segments: Vec::new(),
            names: Vec::new(),
            name_index: FxHashMap::default(),
        }
    }

    /// Record a mapping from the current generated byte offset to a source byte
    /// offset.
    ///
    /// Call this *immediately before* writing the mapped token to the code
    /// buffer, passing the buffer's current length as `generated_offset` and the
    /// originating AST node's `loc.span.start` as `source_offset`.
    pub fn add_raw(&mut self, generated_offset: usize, source_offset: u32) {
        self.segments.push(Segment {
            generated_offset: generated_offset as u32,
            source_offset,
            name: None,
        });
    }

    /// Record a mapping that additionally carries a known source symbol `name`.
    ///
    /// The name is interned into the v3 `names` array (deduplicated across the
    /// whole map) and the segment references it by index, so a consumer can map
    /// the generated token back to the original identifier. Otherwise identical
    /// to [`add_raw`](Self::add_raw): call it immediately before writing the
    /// mapped token.
    pub fn add_named(&mut self, generated_offset: usize, source_offset: u32, name: &str) {
        let index = self.intern_name(name);
        self.segments.push(Segment {
            generated_offset: generated_offset as u32,
            source_offset,
            name: Some(index),
        });
    }

    /// Return the `names`-array index for `name`, inserting it if new.
    fn intern_name(&mut self, name: &str) -> u32 {
        if let Some(&index) = self.name_index.get(name) {
            return index;
        }
        let index = self.names.len() as u32;
        self.names.push(name.to_compact_string());
        self.name_index.insert(name.to_compact_string(), index);
        index
    }

    /// Serialize the accumulated segments into a Source Map v3 JSON string.
    ///
    /// `generated_code` is the final code buffer the segments index into;
    /// generated line/column for each segment is derived from it. `filename` is
    /// the original source path recorded in `sources`, and `source_content` is
    /// the original template text embedded as `sourcesContent` (so consumers can
    /// resolve mappings without a second fetch).
    ///
    /// Mappings are grouped by generated line and VLQ-encoded per the v3 spec.
    /// Each segment encodes 4 fields: generated column delta, source index
    /// delta, source line delta, source column delta (all relative to the
    /// previous segment, with source index/line/column relative across the whole
    /// document and generated column reset per line). Segments that carry a
    /// source symbol add a 5th field: the name-index delta (relative to the
    /// previous named segment across the whole document).
    pub fn finish(mut self, generated_code: &str, filename: &str, source_content: &str) -> String {
        // Resolve byte offsets to (line, col). Sort by generated offset first so
        // the generated-side scan cursor advances monotonically; the source side
        // is resolved with an independent binary search over precomputed line
        // starts, so it does not require the segments to be source-sorted.
        self.segments.sort_by_key(|s| s.generated_offset);

        let resolved = resolve_positions(generated_code, source_content, &self.segments);
        let mappings = encode_mappings(&resolved);

        // Use serde_json to build the document so `filename`/source content and
        // the `names` entries are correctly JSON-escaped (quotes, control chars,
        // unicode).
        let names: Vec<&str> = self.names.iter().map(String::as_str).collect();
        let doc = serde_json::json!({
            "version": 3,
            "file": filename,
            "sources": [filename],
            "sourcesContent": [source_content],
            "names": names,
            "mappings": mappings.as_str(),
        });

        // serde_json writes into std String; convert once to CompactString.
        serde_json::to_string(&doc)
            .unwrap_or_default()
            .to_compact_string()
    }
}

/// A segment whose endpoints have been resolved to 0-indexed line/column.
#[derive(Debug, Clone, Copy)]
struct ResolvedSegment {
    generated_line: u32,
    generated_column: u32,
    source_line: u32,
    source_column: u32,
    /// Index into the v3 `names` array when this anchor carries a known source
    /// symbol, carried through unchanged from the recorded segment.
    name: Option<u32>,
}

/// Resolve every segment's generated and source byte offsets to 0-indexed
/// (line, column) pairs.
///
/// The generated side uses a single monotonic forward scan (segments are sorted
/// by generated offset by the caller). The source side uses a precomputed table
/// of line-start byte offsets plus binary search, since segments are not sorted
/// by source offset.
///
/// Columns are counted in UTF-16 code units to match what source-map consumers
/// (browsers, the `source-map` npm package) expect. Both the generated render
/// code and template identifiers are overwhelmingly ASCII, so this equals the
/// byte/char column in the common case and only diverges on embedded non-ASCII.
fn resolve_positions(code: &str, source: &str, segments: &[Segment]) -> Vec<ResolvedSegment> {
    let code_bytes = code.as_bytes();
    let source_line_starts = line_start_table(source);

    let mut resolved = Vec::with_capacity(segments.len());

    let mut cursor = 0usize; // byte index into the generated code
    let mut gen_line = 0u32;
    let mut gen_line_start = 0usize;

    for seg in segments {
        // Generated side: advance the forward scan to this segment's offset.
        let target =
            floor_char_boundary(code, (seg.generated_offset as usize).min(code_bytes.len()));
        while cursor < target {
            let width = line_break_len(code, cursor);
            if width > 0 && cursor + width <= target {
                cursor += width;
                gen_line += 1;
                gen_line_start = cursor;
            } else {
                cursor += 1;
            }
        }
        let generated_column = utf16_len(code.get(gen_line_start..target).unwrap_or_default());

        // Source side: binary search the line-start table.
        let (source_line, source_column) =
            resolve_in_table(source, &source_line_starts, seg.source_offset as usize);

        resolved.push(ResolvedSegment {
            generated_line: gen_line,
            generated_column,
            source_line,
            source_column,
            name: seg.name,
        });
    }

    resolved
}

/// Byte offsets at which each line of `text` begins. Always starts with `0`;
/// each JavaScript line terminator adds the offset just past it.
fn line_start_table(text: &str) -> Vec<usize> {
    let mut starts = Vec::with_capacity(16);
    starts.push(0);
    let mut cursor = 0;
    while cursor < text.len() {
        let width = line_break_len(text, cursor);
        cursor += width.max(1);
        if width > 0 {
            starts.push(cursor);
        }
    }
    starts
}

/// Resolve a byte `offset` into `text` to a 0-indexed (line, column) using a
/// precomputed `line_starts` table. Column is in UTF-16 code units.
fn resolve_in_table(text: &str, line_starts: &[usize], offset: usize) -> (u32, u32) {
    let offset = floor_char_boundary(text, offset.min(text.len()));
    // The line is the index of the greatest line-start <= offset.
    let line = match line_starts.binary_search(&offset) {
        Ok(i) => i,
        Err(i) => i.saturating_sub(1), // i >= 1 because line_starts[0] == 0 <= offset
    };
    let line_start = line_starts.get(line).copied().unwrap_or(0);
    let column = utf16_len(text.get(line_start..offset).unwrap_or_default());
    (line as u32, column)
}

fn floor_char_boundary(text: &str, mut offset: usize) -> usize {
    while offset > 0 && !text.is_char_boundary(offset) {
        offset -= 1;
    }
    offset
}

/// UTF-16 code-unit length of `s` (chars above U+FFFF count as 2).
#[inline]
fn utf16_len(s: &str) -> u32 {
    let mut n = 0u32;
    for ch in s.chars() {
        n += ch.len_utf16() as u32;
    }
    n
}

/// Encode resolved segments as a v3 `mappings` string.
///
/// Lines are separated by `;`; segments within a line by `,`. Each segment is 4
/// VLQ-encoded deltas: generated column, source index, source line, source
/// column — plus an optional 5th, the name index, present only for segments
/// that carry a source symbol. Generated column is relative to the previous
/// segment on the same line (reset to absolute at each new line); the other
/// fields (source index/line/column and the name index) are relative to the
/// previous segment that emitted them, across the whole document.
fn encode_mappings(segments: &[ResolvedSegment]) -> String {
    let mut out = String::with_capacity(segments.len() * 6);

    let mut current_line = 0u32;
    let mut prev_generated_column = 0i64;
    let mut prev_source_index = 0i64;
    let mut prev_source_line = 0i64;
    let mut prev_source_column = 0i64;
    let mut prev_name_index = 0i64;
    let mut first_in_line = true;

    for seg in segments {
        // Emit `;` for every generated line we skip past, resetting the
        // per-line generated-column accumulator each time.
        while current_line < seg.generated_line {
            out.push(';');
            current_line += 1;
            prev_generated_column = 0;
            first_in_line = true;
        }

        if first_in_line {
            first_in_line = false;
        } else {
            out.push(',');
        }

        let gen_col = seg.generated_column as i64;
        let src_index = 0i64; // single source
        let src_line = seg.source_line as i64;
        let src_col = seg.source_column as i64;

        encode_vlq(&mut out, gen_col - prev_generated_column);
        encode_vlq(&mut out, src_index - prev_source_index);
        encode_vlq(&mut out, src_line - prev_source_line);
        encode_vlq(&mut out, src_col - prev_source_column);

        // The v3 name index is the optional 5th field. It is delta-encoded
        // against the previous *named* segment (the running accumulator only
        // advances when a name is present), matching how consumers decode it.
        if let Some(name) = seg.name {
            let name_index = name as i64;
            encode_vlq(&mut out, name_index - prev_name_index);
            prev_name_index = name_index;
        }

        prev_generated_column = gen_col;
        prev_source_index = src_index;
        prev_source_line = src_line;
        prev_source_column = src_col;
    }

    out
}

mod vlq;
use vlq::encode_vlq;

#[cfg(test)]
mod tests;