use pasta_dsl::parser::{Action, Span};
use pasta_lua::LineEnding;
use pasta_lua::code_gen::LuaCodeGenerator;
use pasta_lua::code_gen::source_map::SourceMapSink;
#[derive(Default)]
struct CapturingSink {
records: Vec<(u32, Span)>,
}
impl SourceMapSink for CapturingSink {
fn record_line(&mut self, lua_line: u32, pasta_line: u32) {
self.records
.push((lua_line, Span::new(pasta_line as usize, 0, pasta_line as usize, 0, 0, 0)));
}
fn record(&mut self, lua_line: u32, span: Span) {
self.records.push((lua_line, span));
}
}
fn sample_span() -> Span {
Span::new(7, 3, 7, 20, 42, 60)
}
#[test]
fn test_no_sink_is_byte_identical_and_records_nothing() {
let mut output = Vec::new();
let mut codegen = LuaCodeGenerator::with_line_ending(&mut output, LineEnding::Lf);
let action = Action::Talk {
text: "こんにちは".to_string(),
span: sample_span(),
};
codegen.generate_action(&action, "さくら").unwrap();
let with_seam = String::from_utf8(output).unwrap();
assert_eq!(with_seam, "act.さくら:talk(\"こんにちは\")\n");
}
#[test]
fn test_capturing_sink_records_line_to_span_for_action_path() {
let mut sink = CapturingSink::default();
let span = sample_span();
let mut output = Vec::new();
{
let mut codegen = LuaCodeGenerator::with_line_ending(&mut output, LineEnding::Lf);
codegen.set_source_map(&mut sink);
let action = Action::Talk {
text: "こんにちは".to_string(),
span,
};
codegen.generate_action(&action, "さくら").unwrap();
}
let lua = String::from_utf8(output).unwrap();
assert_eq!(lua, "act.さくら:talk(\"こんにちは\")\n");
assert_eq!(
sink.records,
vec![(1, span)],
"expected a single (lua_line=1 -> span) record for the talk action"
);
}
#[test]
fn test_capturing_sink_records_multiple_actions_with_increasing_lines() {
let mut sink = CapturingSink::default();
let span_a = Span::new(1, 1, 1, 10, 0, 9);
let span_b = Span::new(2, 1, 2, 10, 10, 19);
let mut output = Vec::new();
{
let mut codegen = LuaCodeGenerator::new(&mut output);
codegen.set_source_map(&mut sink);
codegen
.generate_action(
&Action::Talk {
text: "A".to_string(),
span: span_a,
},
"さくら",
)
.unwrap();
codegen
.generate_action(
&Action::Talk {
text: "B".to_string(),
span: span_b,
},
"さくら",
)
.unwrap();
}
assert_eq!(sink.records, vec![(1, span_a), (2, span_b)]);
}
#[test]
fn test_out_line_counter_matches_emitted_line_count() {
let mut output = Vec::new();
let mut codegen = LuaCodeGenerator::new(&mut output);
codegen.write_header().unwrap();
assert_eq!(codegen.out_line(), 3, "header should emit 3 output lines");
codegen
.generate_action(
&Action::Talk {
text: "x".to_string(),
span: sample_span(),
},
"さくら",
)
.unwrap();
assert_eq!(codegen.out_line(), 4);
let s = String::from_utf8(output).unwrap();
let emitted = s.matches('\n').count() as u32;
assert_eq!(emitted, 4, "actual emitted lines should match out_line");
}
mod zero_cost_sandbox_regression {
use pasta_dsl::parser::parse_str;
use pasta_lua::LuaTranspiler;
const FIXTURE: &str = "\
*メイン
さくら:「こんにちは」
うにゅう:「やあ」
";
fn transpile_production(source: &str) -> Vec<u8> {
let file = parse_str(source, "test.pasta").expect("parse ok");
let transpiler = LuaTranspiler::default();
let mut output = Vec::new();
transpiler
.transpile(&file, &mut output)
.expect("transpile ok");
output
}
#[test]
fn r5_2_production_transpile_is_deterministic_byte_for_byte() {
let run1 = transpile_production(FIXTURE);
let run2 = transpile_production(FIXTURE);
assert_eq!(
run1, run2,
"R5.2: production transpile (no sink) must be byte-for-byte deterministic"
);
assert!(!run1.is_empty(), "fixture must produce real output bytes");
}
#[test]
fn r4_6_none_sink_emits_zero_extra_bytes_golden() {
let produced = transpile_production(FIXTURE);
let produced = String::from_utf8(produced).expect("utf-8");
let produced_lf = produced.replace("\r\n", "\n");
let golden = "\
local PASTA = require \"pasta\"
local GLOBAL = require \"pasta.global\"
do
local SCENE = PASTA.create_scene(\"メイン\")
function SCENE.__start__(act, ...)
local args = { ... }
local save, var = act:init_scene(SCENE)
act.さくら:talk(\"「こんにちは」\")
act.うにゅう:talk(\"「やあ」\")
end
end
";
assert_eq!(
produced_lf, golden,
"R4.6: no-sink production transpile must byte-match the golden (seam adds zero bytes)"
);
}
}