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;
pub struct LuaCodeGenerator<'a, W: Write> {
writer: &'a mut W,
indent_level: usize,
line_ending: LineEnding,
out_line: u32,
source_map: Option<&'a mut dyn SourceMapSink>,
}
impl<'a, W: Write> LuaCodeGenerator<'a, W> {
pub fn new(writer: &'a mut W) -> Self {
Self {
writer,
indent_level: 0,
line_ending: LineEnding::default(),
out_line: 0,
source_map: None,
}
}
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,
}
}
pub fn set_source_map(&mut self, sink: &'a mut dyn SourceMapSink) {
self.source_map = Some(sink);
}
pub fn out_line(&self) -> u32 {
self.out_line
}
fn record_span(&mut self, span: Span) {
if let Some(sink) = self.source_map.as_deref_mut()
&& span.is_valid()
{
sink.record(self.out_line, span);
}
}
fn record_block_line(&mut self, pasta_line: u32) {
if let Some(sink) = self.source_map.as_deref_mut()
&& pasta_line > 0
{
sink.record_line(self.out_line, pasta_line);
}
}
fn write_indent(&mut self) -> Result<(), TranspileError> {
let indent = " ".repeat(self.indent_level);
write!(self.writer, "{}", indent)?;
Ok(())
}
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(())
}
fn write_blank_line(&mut self) -> Result<(), TranspileError> {
write!(self.writer, "{}", self.line_ending.as_str())?;
self.out_line += 1;
Ok(())
}
fn write_raw(&mut self, s: &str) -> Result<(), TranspileError> {
write!(self.writer, "{}", s)?;
self.out_line += s.matches('\n').count() as u32;
Ok(())
}
fn write_line_terminator(&mut self) -> Result<(), TranspileError> {
writeln!(self.writer)?;
self.out_line += 1;
Ok(())
}
fn indent(&mut self) {
self.indent_level += 1;
}
fn dedent(&mut self) {
if self.indent_level > 0 {
self.indent_level -= 1;
}
}
fn end_block(&mut self) -> Result<(), TranspileError> {
self.dedent();
self.writeln("end")?;
self.write_blank_line()?;
Ok(())
}
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(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Default)]
struct CapturingSink {
records: Vec<(u32, u32)>,
}
impl SourceMapSink for CapturingSink {
fn record_line(&mut self, lua_line: u32, pasta_line: u32) {
self.records.push((lua_line, pasta_line));
}
}
#[test]
fn out_line_advances_per_choke_point_and_matches_bytes() {
let mut output = Vec::new();
{
let mut cg = LuaCodeGenerator::with_line_ending(&mut output, LineEnding::Lf);
assert_eq!(cg.out_line(), 0, "no line emitted yet");
cg.writeln("a").unwrap();
assert_eq!(cg.out_line(), 1, "writeln advances by 1");
cg.write_blank_line().unwrap();
assert_eq!(cg.out_line(), 2, "write_blank_line advances by 1");
cg.write_raw("frag").unwrap();
assert_eq!(cg.out_line(), 2, "write_raw without newline does not advance");
cg.write_raw("x\ny\nz").unwrap();
assert_eq!(cg.out_line(), 4, "write_raw advances per embedded newline");
cg.write_line_terminator().unwrap();
assert_eq!(cg.out_line(), 5, "write_line_terminator advances by 1");
}
let text = String::from_utf8(output).unwrap();
assert_eq!(text, "a\n\nfragx\ny\nz\n");
assert_eq!(
text.matches('\n').count(),
5,
"byte-level line count must equal final out_line"
);
}
#[test]
fn indent_dedent_levels_and_saturation_at_zero() {
let mut output = Vec::new();
{
let mut cg = LuaCodeGenerator::with_line_ending(&mut output, LineEnding::Lf);
cg.dedent(); cg.writeln("zero").unwrap();
cg.indent();
cg.indent();
cg.writeln("two").unwrap();
cg.dedent();
cg.writeln("one").unwrap();
cg.dedent();
cg.dedent(); cg.writeln("zero2").unwrap();
}
let text = String::from_utf8(output).unwrap();
assert_eq!(text, "zero\n two\n one\nzero2\n");
}
#[test]
fn end_block_dedents_then_writes_end_and_blank_line() {
let mut output = Vec::new();
{
let mut cg = LuaCodeGenerator::with_line_ending(&mut output, LineEnding::Lf);
cg.writeln("do").unwrap();
cg.indent();
cg.writeln("body").unwrap();
cg.end_block().unwrap();
}
let text = String::from_utf8(output).unwrap();
assert_eq!(text, "do\n body\nend\n\n");
}
#[test]
fn with_line_ending_crlf_emits_crlf_terminators() {
let mut output = Vec::new();
let final_out_line;
{
let mut cg = LuaCodeGenerator::with_line_ending(&mut output, LineEnding::CrLf);
cg.writeln("a").unwrap();
cg.write_blank_line().unwrap();
final_out_line = cg.out_line();
}
assert_eq!(String::from_utf8(output).unwrap(), "a\r\n\r\n");
assert_eq!(final_out_line, 2);
}
#[test]
fn write_header_emits_exact_require_lines() {
let mut output = Vec::new();
{
let mut cg = LuaCodeGenerator::with_line_ending(&mut output, LineEnding::Lf);
cg.write_header().unwrap();
}
assert_eq!(
String::from_utf8(output).unwrap(),
"local PASTA = require \"pasta\"\nlocal GLOBAL = require \"pasta.global\"\n\n"
);
}
#[test]
fn record_span_skips_invalid_span_and_records_valid_span() {
let mut sink = CapturingSink::default();
let mut output = Vec::new();
{
let mut cg = LuaCodeGenerator::with_line_ending(&mut output, LineEnding::Lf);
cg.set_source_map(&mut sink);
cg.writeln("line1").unwrap();
cg.record_span(Span::default()); cg.writeln("line2").unwrap();
cg.record_span(Span::new(7, 1, 7, 5, 10, 20)); }
assert_eq!(
sink.records,
vec![(2, 7)],
"only the valid span is recorded, mapped to out_line 2 -> pasta line 7"
);
}
#[test]
fn record_block_line_skips_zero_and_records_positive_line() {
let mut sink = CapturingSink::default();
let mut output = Vec::new();
{
let mut cg = LuaCodeGenerator::with_line_ending(&mut output, LineEnding::Lf);
cg.set_source_map(&mut sink);
cg.writeln("line1").unwrap();
cg.record_block_line(0); cg.record_block_line(9);
}
assert_eq!(sink.records, vec![(1, 9)]);
}
#[test]
fn recording_without_sink_is_inert() {
let mut output = Vec::new();
{
let mut cg = LuaCodeGenerator::with_line_ending(&mut output, LineEnding::Lf);
cg.writeln("a").unwrap();
cg.record_span(Span::new(1, 1, 1, 2, 0, 5));
cg.record_block_line(3);
}
assert_eq!(String::from_utf8(output).unwrap(), "a\n");
}
}