pasta_lua 0.2.2

Pasta Lua - Lua integration for Pasta DSL
Documentation
//! Lua code generator for Pasta DSL.
//!
//! This module generates Lua source code from Pasta AST nodes.
//! Implements Requirements 1, 3a-3g for Lua code generation.

mod element_gen;
mod scope_gen;
pub mod source_map;

use crate::config::LineEnding;
use crate::error::TranspileError;

use pasta_dsl::parser::Span;
use source_map::SourceMapSink;

use std::io::Write;

/// Lua code generator.
///
/// Generates Lua source code from Pasta AST nodes.
///
/// # Source-map seam (R4: feasibility)
///
/// The generator tracks `out_line`, the 1-based line number of the most recently
/// emitted output line, and carries an optional [`SourceMapSink`]. When a sink is
/// attached (via [`set_source_map`](LuaCodeGenerator::set_source_map)) and a span is
/// known at the emit point, the generator records `(out_line -> span)` so the
/// `.pasta` <-> `.lua` line correspondence is resolvable. When no sink is attached
/// (the default for ALL production transpile callers) the seam is inert and the
/// generated bytes are byte-identical to before this seam existed (Requirement 4.6).
pub struct LuaCodeGenerator<'a, W: Write> {
    /// Output writer
    writer: &'a mut W,
    /// Current indentation level
    indent_level: usize,
    /// Line ending style
    line_ending: LineEnding,
    /// 1-based line number of the most recently emitted output line.
    ///
    /// Starts at 0 (nothing emitted yet) and is incremented by exactly 1 for each
    /// line terminator written through the emit choke points (`writeln`,
    /// `write_blank_line`, the raw expression-statement terminator, and per embedded
    /// `\n` in `write_raw`). Internal only: never alters emitted bytes.
    out_line: u32,
    /// Optional source-map sink (producer side of the `debug -> code_gen` seam).
    ///
    /// `None` for production transpile (byte-identical, zero-cost). When `Some`, the
    /// generator calls `record(out_line, span)` for span-bearing constructs.
    source_map: Option<&'a mut dyn SourceMapSink>,
}

impl<'a, W: Write> LuaCodeGenerator<'a, W> {
    /// Create a new Lua code generator.
    pub fn new(writer: &'a mut W) -> Self {
        Self {
            writer,
            indent_level: 0,
            line_ending: LineEnding::default(),
            out_line: 0,
            source_map: None,
        }
    }

    /// Create a new Lua code generator with specified line ending.
    pub fn with_line_ending(writer: &'a mut W, line_ending: LineEnding) -> Self {
        Self {
            writer,
            indent_level: 0,
            line_ending,
            out_line: 0,
            source_map: None,
        }
    }

    /// Attach a source-map sink (producer side of the source-map seam).
    ///
    /// While a sink is attached, the generator records `(out_line -> span)` for
    /// span-bearing constructs it emits. Attaching a sink NEVER changes the emitted
    /// bytes — it only observes line numbers. The default (no sink) path is
    /// byte-identical and zero-cost (Requirement 4.6).
    pub fn set_source_map(&mut self, sink: &'a mut dyn SourceMapSink) {
        self.source_map = Some(sink);
    }

    /// Current value of the output-line counter (1-based line most recently emitted).
    ///
    /// Returns 0 before any line has been emitted. Exposed for accuracy assertions
    /// and for callers that need the line a construct will land on.
    pub fn out_line(&self) -> u32 {
        self.out_line
    }

    /// Record `(out_line -> span)` if a sink is attached and the span is valid.
    ///
    /// Inert (no-op, zero-cost) when no sink is attached — the production default.
    /// Invalid/default spans (`end_byte == 0`) are skipped so synthetic/headerless
    /// output does not pollute the map.
    ///
    /// # Coverage note (future `pasta-source-map` spec)
    ///
    /// This seam is wired for the representative action-emitting path
    /// (`generate_action`). Full coverage — recording every `generate_*` construct
    /// (var sets, calls, choices, word definitions, scene/function headers, code
    /// blocks) and handling the `currentline` edge cases plus the post-transpile
    /// `normalize_output` line-shift — is OUT OF BOUNDARY here and is owned by the
    /// downstream `pasta-source-map` spec. To extend coverage, thread the relevant
    /// node's `span` to its emit point and call `record_span(span)` immediately
    /// before the corresponding `writeln`.
    fn record_span(&mut self, span: Span) {
        if let Some(sink) = self.source_map.as_deref_mut() {
            if span.is_valid() {
                sink.record(self.out_line, span);
            }
        }
    }

    /// Record `(out_line -> pasta_line)` directly if a sink is attached.
    ///
    /// Inert (no-op, zero-cost) when no sink is attached — the production default.
    /// Unlike [`record_span`](Self::record_span), this maps the most recently
    /// emitted output line to an explicit 1-based `.pasta` line rather than a span's
    /// `start_line`. It is the per-line mapping primitive used by multi-line elements
    /// whose output lines correspond 1:1 to distinct `.pasta` source lines — chiefly
    /// code blocks, where in-block breakpoint precision (Requirement 1.3) requires
    /// each generated `.lua` line to resolve to its own originating `.pasta` line.
    ///
    /// `pasta_line` must be 1-based. Callers compute it as
    /// `block.span.start_line + offset` (see `generate_code_block`). A `0` value is
    /// treated as invalid and skipped, mirroring the `span.is_valid()` guard in
    /// `record_span` (the default/uninitialized line is not recorded).
    fn record_block_line(&mut self, pasta_line: u32) {
        if let Some(sink) = self.source_map.as_deref_mut() {
            if pasta_line > 0 {
                sink.record_line(self.out_line, pasta_line);
            }
        }
    }

    /// Write indentation at current level.
    fn write_indent(&mut self) -> Result<(), TranspileError> {
        let indent = "    ".repeat(self.indent_level);
        write!(self.writer, "{}", indent)?;
        Ok(())
    }

    /// Write a line with current indentation.
    ///
    /// Single primary line-emitting choke point: emits exactly one line terminator,
    /// so it advances `out_line` by 1.
    fn writeln(&mut self, s: &str) -> Result<(), TranspileError> {
        self.write_indent()?;
        write!(self.writer, "{}{}", s, self.line_ending.as_str())?;
        self.out_line += 1;
        Ok(())
    }

    /// Write a blank line without indentation.
    ///
    /// Emits exactly one line terminator, so it advances `out_line` by 1.
    fn write_blank_line(&mut self) -> Result<(), TranspileError> {
        write!(self.writer, "{}", self.line_ending.as_str())?;
        self.out_line += 1;
        Ok(())
    }

    /// Write without indentation.
    ///
    /// `s` may contain embedded newlines; `out_line` is advanced by the number of
    /// `\n` characters in `s` so the counter stays accurate regardless of content.
    /// (Current call sites pass fragments without a trailing terminator — the
    /// terminator is emitted separately via [`write_line_terminator`].)
    fn write_raw(&mut self, s: &str) -> Result<(), TranspileError> {
        write!(self.writer, "{}", s)?;
        self.out_line += s.matches('\n').count() as u32;
        Ok(())
    }

    /// Emit a bare line terminator for the open line built via `write_raw` fragments.
    ///
    /// Writes the same byte the previous inline `writeln!(self.writer)` calls wrote
    /// (a single `\n`, later normalized) and advances `out_line` by 1. Keeps the
    /// out-line accounting centralized so the counter cannot drift from the bytes.
    fn write_line_terminator(&mut self) -> Result<(), TranspileError> {
        writeln!(self.writer)?;
        self.out_line += 1;
        Ok(())
    }

    /// Increase indentation level.
    fn indent(&mut self) {
        self.indent_level += 1;
    }

    /// Decrease indentation level.
    fn dedent(&mut self) {
        if self.indent_level > 0 {
            self.indent_level -= 1;
        }
    }

    /// Close a block: dedent, write `end`, and add a blank line.
    ///
    /// Common pattern used by actor, global scene, and local scene generators.
    fn end_block(&mut self) -> Result<(), TranspileError> {
        self.dedent();
        self.writeln("end")?;
        self.write_blank_line()?;
        Ok(())
    }

    /// Write the Lua header (require statement).
    pub fn write_header(&mut self) -> Result<(), TranspileError> {
        self.writeln("local PASTA = require \"pasta\"")?;
        self.writeln("local GLOBAL = require \"pasta.global\"")?;
        self.write_blank_line()?;
        Ok(())
    }
}