#![doc(html_root_url = "https://docs.rs/pico8-to-lua/0.1.1")]
#![doc = include_str!("../README.md")]
use regex::{Regex, Replacer};
use std::{borrow::Cow, error::Error};
use find_matching_bracket::find_matching_paren;
use lazy_regex::regex;
fn replace_all_in_place<R: Replacer>(regex: &Regex, s: &mut Cow<'_, str>, replacer: R) {
let new = regex.replace_all(s, replacer);
if let Cow::Owned(o) = new {
*s = Cow::Owned(o);
} }
pub fn try_patch_includes<'h, E: Error>(
lua: impl Into<Cow<'h, str>>,
mut resolve: impl FnMut(&str) -> Result<String, E>,
) -> Result<Cow<'h, str>, E> {
let mut lua = lua.into();
let mut error = None;
replace_all_in_place(
regex!(r"(?m)^\s*#include\s+(\S+)"),
&mut lua,
|caps: ®ex::Captures| {
match resolve(&caps[1]) {
Ok(s) => s,
Err(e) => {
let result = format!("error(\"failed to include {:?}: {}\")", &caps[1], &e);
if error.is_none() {
error = Some(Err(e))
}
result
}
}
},
);
error.unwrap_or(Ok(lua))
}
#[allow(clippy::ptr_arg)]
pub fn was_patched(patch_output: &Cow<'_, str>) -> bool {
match patch_output {
Cow::Owned(_) => true,
Cow::Borrowed(_) => false,
}
}
pub fn patch_includes<'h>(
lua: impl Into<Cow<'h, str>>,
mut resolve: impl FnMut(&str) -> String,
) -> Cow<'h, str> {
let mut lua = lua.into();
replace_all_in_place(
regex!(r"(?m)^\s*#include\s+(\S+)"),
&mut lua,
|caps: ®ex::Captures| resolve(&caps[1]),
);
lua
}
pub fn find_includes(
lua: &str,
) -> impl Iterator<Item = String> {
regex!(r"(?m)^\s*#include\s+(\S+)").captures_iter(lua)
.map(|caps: regex::Captures| caps[1].to_string())
}
pub fn patch_lua<'h>(lua: impl Into<Cow<'h, str>>) -> Cow<'h, str> {
let mut lua = lua.into();
replace_all_in_place(regex!(r"!="), &mut lua, "~=");
replace_all_in_place(regex!(r"//"), &mut lua, "--");
replace_all_in_place(
regex!(r"(btnp?)\(\s*(\S+)\s*\)"),
&mut lua,
|caps: ®ex::Captures| {
let func = &caps[1];
let symbol = caps[2].trim_end_matches("\u{fe0f}");
let sub = match symbol {
"⬅" => "0",
"➡" => "1",
"⬆" => "2",
"⬇" => "3",
"🅾" => "4",
"❎" => "5",
x => x,
};
format!("{func}({sub})")
},
);
replace_all_in_place(
regex!(r"(?m)^(\s*)if\s*(\([^\n]*)$"),
&mut lua,
|caps: ®ex::Captures| {
let prefix = &caps[1];
let line = &caps[2];
if regex!(r"\bthen\b").is_match(line) {
return caps[0].to_string();
}
if let Some(index) = find_matching_paren(line, 0) {
let cond = &line[1..index];
let body = &line[index + 1..].trim_start();
let comment_start = body.find("--");
if let Some(cs) = comment_start {
let (code, comment) = body.split_at(cs);
format!(
"{}if {} then {} end {}",
prefix,
cond,
code.trim_end(),
comment
)
} else {
format!("{}if {} then {} end", prefix, cond, body)
}
} else {
caps[0].to_string()
}
},
);
replace_all_in_place(regex!(r"(?m)([^-\s]\S*)\s*([+\-*/%])=\s*([^\n\r]+?)(\s*(\breturn|\bend|\belse|;|--|$))"), &mut lua, "$1 = $1 $2 ($3)$4");
replace_all_in_place(regex!(r"(?m)^(\s*)\?([^\n\r]+)"), &mut lua, "${1}print($2)");
replace_all_in_place(
regex!(r"([^[:alnum:]_])0[bB]([01.]+)"),
&mut lua,
|caps: ®ex::Captures| {
let prefix = &caps[1];
let bin = &caps[2];
let mut parts = bin.split('.');
let p1 = parts.next().unwrap_or("");
let p2 = parts.next().unwrap_or("");
let int_val = u64::from_str_radix(p1, 2).ok();
let frac_val = if !p2.is_empty() {
let padded = format!("{:0<4}", p2);
u64::from_str_radix(&padded, 2).ok()
} else {
None
};
match (int_val, frac_val) {
(Some(i), Some(f)) => format!("{}0x{:x}.{:x}", prefix, i, f),
(Some(i), None) => format!("{}0x{:x}", prefix, i),
_ => caps[0].to_string(),
}
},
);
lua
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_not_equal_replacement() {
let lua = "if a != b then print(a) end";
let patched = patch_lua(lua);
assert!(patched.contains("a ~= b"));
}
#[test]
fn test_comment_replacement() {
let lua = "// this is a comment\nprint('hello')";
let patched = patch_lua(lua);
assert!(patched.contains("-- this is a comment"));
}
#[test]
fn test_shorthand_if_rewrite() {
let lua = "if (not b) i = 1\n";
let expected = "if not b then i = 1 end\n";
let patched = patch_lua(lua);
assert_eq!(patched, expected);
}
#[test]
fn test_shorthand_if_rewrite_comment() {
let lua = "if (not b) i = 1 // hi\n";
let expected = "if not b then i = 1 end -- hi\n";
let patched = patch_lua(lua);
assert_eq!(patched, expected);
}
#[test]
fn test_shorthand_if_rewrite_and() {
let lua = "if (not b and not c) i = 1\n";
let expected = "if not b and not c then i = 1 end\n";
let patched = patch_lua(lua);
assert_eq!(patched, expected);
}
#[test]
fn test_assignment_operator_rewrite() {
let lua = "x += 1";
let patched = patch_lua(lua);
assert_eq!(patched.trim(), "x = x + (1)");
}
#[test]
fn test_question_print_conversion0() {
let lua = "?x";
let patched = patch_lua(lua);
assert_eq!(patched.trim(), "print(x)");
}
#[test]
fn test_question_print_conversion() {
let lua = "?x + y";
let patched = patch_lua(lua);
assert_eq!(patched.trim(), "print(x + y)");
}
#[test]
fn test_binary_literal_conversion_integer() {
let lua = "a = 0b1010";
let patched = patch_lua(lua);
assert_eq!(patched.trim(), "a = 0xa");
}
#[test]
fn test_binary_literal_conversion_fractional() {
let lua = "a = 0b1010.1";
let patched = patch_lua(lua);
assert_eq!(patched.trim(), "a = 0xa.8");
}
#[test]
fn test_mixed_transforms() {
let lua = r#"
// comment
if (a != b) x += 1
?x
"#;
let patched = patch_lua(lua);
assert!(patched.contains("-- comment"), "{}", patched);
assert!(
patched.contains("if a ~= b then x = x + (1) end"),
"{}",
patched
);
assert!(patched.contains("print(x)"), "{}", patched);
}
#[test]
fn test_no_change_no_allocation() {
let lua = "x = 1";
let patched = patch_lua(lua);
assert!(match patched {
Cow::Owned(_) => false,
Cow::Borrowed(_) => true,
});
}
#[test]
fn test_change_requires_allocation() {
let lua = "x += 1";
let patched = patch_lua(lua);
assert!(match patched {
Cow::Owned(_) => true,
Cow::Borrowed(_) => false,
});
}
#[test]
fn test_includes() {
let lua = r#"
#include blah.p8
"#;
let patched = patch_includes(lua, |path| format!("-- INCLUDE {}", path));
assert!(patched.contains("-- INCLUDE blah.p8"), "{}", &patched);
}
#[test]
fn test_bad_comment() {
let lua = "--==configurations==--";
let patched = patch_lua(lua);
assert_eq!(patched.trim(), "--==configurations==--");
}
#[test]
fn test_bad_if() {
let lua =
"if (ord(tb.str[tb.i],tb.char)!=32) sfx(tb.voice) -- play the voice sound effect.";
let patched = patch_lua(lua);
assert_eq!(
patched.trim(),
"if ord(tb.str[tb.i],tb.char)~=32 then sfx(tb.voice) end -- play the voice sound effect."
);
}
#[test]
fn test_bad_incr() {
let lua = "tb.i+=1 -- increase the index, to display the next message on tb.str";
let patched = patch_lua(lua);
assert_eq!(
patched.trim(),
"tb.i = tb.i + (1) -- increase the index, to display the next message on tb.str"
);
}
#[test]
fn test_button() {
let lua = "if btnp(➡️) or btn(❎) then";
let patched = patch_lua(lua);
assert_eq!(patched.trim(), "if btnp(1) or btn(5) then");
}
#[test]
fn test_button2() {
let lua = "if btnp(❎) then";
let patched = patch_lua(lua);
assert_eq!(patched.trim(), "if btnp(5) then");
}
#[test]
fn test_button3() {
let lua = "if btnp(🅾) then";
let patched = patch_lua(lua);
assert_eq!(patched.trim(), "if btnp(4) then");
}
fn assert_patch(unpatched: &str, expected_patched: &str) {
let patched = patch_lua(unpatched);
assert_eq!(patched, expected_patched);
}
#[test]
fn test_cardboard_toad0() {
assert_patch(
"if (o.color) setmetatable(o.color, { __index = (message_instance or message).color })",
"if o.color then setmetatable(o.color, { __index = (message_instance or message).color }) end",
);
}
#[test]
fn test_cardboard_toad1() {
assert_patch(
r#"
if ((abs(x) < (a.w+a2.w)) and
(abs(y) < (a.h+a2.h)))
then "hi" end
"#,
r#"
if ((abs(x) < (a.w+a2.w)) and
(abs(y) < (a.h+a2.h)))
then "hi" end
"#,
);
}
#[test]
fn test_cardboard_toad2() {
assert_patch(
r#"
if (self.sprites ~= nil) then
self.sprite = self.sprites[self.sprites_index]
end
"#,
r#"
if (self.sprites ~= nil) then
self.sprite = self.sprites[self.sprites_index]
end
"#,
);
}
#[test]
fn test_cardboard_toad3() {
assert_patch("accum += f.delay or self.delay",
"accum = accum + (f.delay or self.delay)");
assert_patch("if true then accum += f.delay or self.delay end",
"if true then accum = accum + (f.delay or self.delay) end");
}
#[test]
fn test_celeste0() {
assert_patch("if freeze>0 then freeze-=1 return end",
"if freeze>0 then freeze = freeze - (1) return end");
}
#[test]
fn test_pooh_big_adventure0() {
assert_patch("if btnp(3) then self.choice += 1; result = true end",
"if btnp(3) then self.choice = self.choice + (1); result = true end");
assert_patch(" i += 1",
" i = i + (1)");
}
#[test]
fn test_plist0() {
let lua = r#"
i += 1
local key = keys[i]
"#;
let patched = patch_lua(lua);
assert!(patched.contains("i = i + (1)"));
}
#[test]
fn test_find_includes() {
let lua = r#"
#include a.p8
#include b.lua
"#;
assert_eq!(find_includes(lua).collect::<Vec<_>>(), vec!["a.p8", "b.lua"]);
}
#[test]
#[ignore = "need a real parser to fix this; see 'antlr' branch"]
fn test_not_so_well0() {
assert_eq!(patch_lua("pos += (delta - thresh):map(function(v) return mid(0, v, 4) end)"),
"pos = pos + ((delta - thresh):map(function(v) return mid(0, v, 4) end))");
}
}