1#![doc(html_root_url = "https://docs.rs/pico8-to-lua/0.1.1")]
2#![doc = include_str!("../README.md")]
3use regex::{Regex, Replacer};
15use std::{borrow::Cow, error::Error};
16use find_matching_bracket::find_matching_paren;
17use lazy_regex::regex;
18
19fn replace_all_in_place<R: Replacer>(regex: &Regex, s: &mut Cow<'_, str>, replacer: R) {
21 let new = regex.replace_all(s, replacer);
22 if let Cow::Owned(o) = new {
23 *s = Cow::Owned(o);
24 } }
26
27pub fn try_patch_includes<'h, E: Error>(
31 lua: impl Into<Cow<'h, str>>,
32 mut resolve: impl FnMut(&str) -> Result<String, E>,
33) -> Result<Cow<'h, str>, E> {
34 let mut lua = lua.into();
35 let mut error = None;
36
37 replace_all_in_place(
38 regex!(r"(?m)^\s*#include\s+(\S+)"),
39 &mut lua,
40 |caps: ®ex::Captures| {
41 match resolve(&caps[1]) {
42 Ok(s) => s,
43 Err(e) => {
44 let result = format!("error(\"failed to include {:?}: {}\")", &caps[1], &e);
48 if error.is_none() {
49 error = Some(Err(e))
50 }
51 result
52 }
53 }
54 },
55 );
56 error.unwrap_or(Ok(lua))
57}
58
59#[allow(clippy::ptr_arg)]
62pub fn was_patched(patch_output: &Cow<'_, str>) -> bool {
63 match patch_output {
64 Cow::Owned(_) => true,
65 Cow::Borrowed(_) => false,
66 }
67}
68
69pub fn patch_includes<'h>(
71 lua: impl Into<Cow<'h, str>>,
72 mut resolve: impl FnMut(&str) -> String,
73) -> Cow<'h, str> {
74 let mut lua = lua.into();
75 replace_all_in_place(
76 regex!(r"(?m)^\s*#include\s+(\S+)"),
77 &mut lua,
78 |caps: ®ex::Captures| resolve(&caps[1]),
79 );
80 lua
81}
82
83pub fn find_includes(
90 lua: &str,
91) -> impl Iterator<Item = String> {
92 regex!(r"(?m)^\s*#include\s+(\S+)").captures_iter(lua)
93 .map(|caps: regex::Captures| caps[1].to_string())
94}
95
96pub fn patch_lua<'h>(lua: impl Into<Cow<'h, str>>) -> Cow<'h, str> {
108 let mut lua = lua.into();
109 replace_all_in_place(regex!(r"!="), &mut lua, "~=");
111
112 replace_all_in_place(regex!(r"//"), &mut lua, "--");
114
115 replace_all_in_place(
117 regex!(r"(btnp?)\(\s*(\S+)\s*\)"),
118 &mut lua,
119 |caps: ®ex::Captures| {
120 let func = &caps[1];
121 let symbol = caps[2].trim_end_matches("\u{fe0f}");
122 let sub = match symbol {
123 "⬅" => "0",
124 "➡" => "1",
125 "⬆" => "2",
126 "⬇" => "3",
127 "🅾" => "4",
128 "❎" => "5",
129 x => x,
130 };
131 format!("{func}({sub})")
132 },
133 );
134
135 replace_all_in_place(
140 regex!(r"(?m)^(\s*)if\s*(\([^\n]*)$"),
141 &mut lua,
142 |caps: ®ex::Captures| {
143 let prefix = &caps[1];
144 let line = &caps[2];
145
146 if regex!(r"\bthen\b").is_match(line) {
147 return caps[0].to_string();
148 }
149 if let Some(index) = find_matching_paren(line, 0) {
150 let cond = &line[1..index];
151 let body = &line[index + 1..].trim_start();
152 let comment_start = body.find("--");
153 if let Some(cs) = comment_start {
154 let (code, comment) = body.split_at(cs);
155 format!(
156 "{}if {} then {} end {}",
157 prefix,
158 cond,
159 code.trim_end(),
160 comment
161 )
162 } else {
163 format!("{}if {} then {} end", prefix, cond, body)
164 }
165 } else {
166 caps[0].to_string()
167 }
168 },
169 );
170
171 replace_all_in_place(regex!(r"(?m)([^-\s]\S*)\s*([+\-*/%])=\s*([^\n\r]+?)(\s*(\breturn|\bend|\belse|;|--|$))"), &mut lua, "$1 = $1 $2 ($3)$4");
173
174 replace_all_in_place(regex!(r"(?m)^(\s*)\?([^\n\r]+)"), &mut lua, "${1}print($2)");
176
177 replace_all_in_place(
179 regex!(r"([^[:alnum:]_])0[bB]([01.]+)"),
180 &mut lua,
181 |caps: ®ex::Captures| {
182 let prefix = &caps[1];
183 let bin = &caps[2];
184 let mut parts = bin.split('.');
185
186 let p1 = parts.next().unwrap_or("");
187 let p2 = parts.next().unwrap_or("");
188
189 let int_val = u64::from_str_radix(p1, 2).ok();
190 let frac_val = if !p2.is_empty() {
191 let padded = format!("{:0<4}", p2);
192 u64::from_str_radix(&padded, 2).ok()
193 } else {
194 None
195 };
196
197 match (int_val, frac_val) {
198 (Some(i), Some(f)) => format!("{}0x{:x}.{:x}", prefix, i, f),
199 (Some(i), None) => format!("{}0x{:x}", prefix, i),
200 _ => caps[0].to_string(),
201 }
202 },
203 );
204 lua
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 #[test]
212 fn test_not_equal_replacement() {
213 let lua = "if a != b then print(a) end";
214 let patched = patch_lua(lua);
215 assert!(patched.contains("a ~= b"));
216 }
217
218 #[test]
219 fn test_comment_replacement() {
220 let lua = "// this is a comment\nprint('hello')";
221 let patched = patch_lua(lua);
222 assert!(patched.contains("-- this is a comment"));
223 }
224
225 #[test]
226 fn test_shorthand_if_rewrite() {
227 let lua = "if (not b) i = 1\n";
228 let expected = "if not b then i = 1 end\n";
229 let patched = patch_lua(lua);
230 assert_eq!(patched, expected);
231 }
232
233 #[test]
234 fn test_shorthand_if_rewrite_comment() {
235 let lua = "if (not b) i = 1 // hi\n";
236 let expected = "if not b then i = 1 end -- hi\n";
237 let patched = patch_lua(lua);
238 assert_eq!(patched, expected);
239 }
240
241 #[test]
242 fn test_shorthand_if_rewrite_and() {
243 let lua = "if (not b and not c) i = 1\n";
244 let expected = "if not b and not c then i = 1 end\n";
245 let patched = patch_lua(lua);
246 assert_eq!(patched, expected);
247 }
248
249 #[test]
250 fn test_assignment_operator_rewrite() {
251 let lua = "x += 1";
252 let patched = patch_lua(lua);
253 assert_eq!(patched.trim(), "x = x + (1)");
254 }
255
256 #[test]
257 fn test_question_print_conversion0() {
258 let lua = "?x";
259 let patched = patch_lua(lua);
260 assert_eq!(patched.trim(), "print(x)");
261 }
262
263 #[test]
264 fn test_question_print_conversion() {
265 let lua = "?x + y";
266 let patched = patch_lua(lua);
267 assert_eq!(patched.trim(), "print(x + y)");
268 }
269
270 #[test]
271 fn test_binary_literal_conversion_integer() {
272 let lua = "a = 0b1010";
273 let patched = patch_lua(lua);
274 assert_eq!(patched.trim(), "a = 0xa");
275 }
276
277 #[test]
278 fn test_binary_literal_conversion_fractional() {
279 let lua = "a = 0b1010.1";
280 let patched = patch_lua(lua);
281 assert_eq!(patched.trim(), "a = 0xa.8");
282 }
283
284 #[test]
285 fn test_mixed_transforms() {
286 let lua = r#"
287 // comment
288 if (a != b) x += 1
289 ?x
290 "#;
291 let patched = patch_lua(lua);
292 assert!(patched.contains("-- comment"), "{}", patched);
293 assert!(
294 patched.contains("if a ~= b then x = x + (1) end"),
295 "{}",
296 patched
297 );
298 assert!(patched.contains("print(x)"), "{}", patched);
299 }
300
301 #[test]
302 fn test_no_change_no_allocation() {
303 let lua = "x = 1";
304 let patched = patch_lua(lua);
305 assert!(match patched {
307 Cow::Owned(_) => false,
308 Cow::Borrowed(_) => true,
309 });
310 }
311
312 #[test]
313 fn test_change_requires_allocation() {
314 let lua = "x += 1";
315 let patched = patch_lua(lua);
316 assert!(match patched {
318 Cow::Owned(_) => true,
319 Cow::Borrowed(_) => false,
320 });
321 }
322
323 #[test]
324 fn test_includes() {
325 let lua = r#"
326 #include blah.p8
327 "#;
328 let patched = patch_includes(lua, |path| format!("-- INCLUDE {}", path));
329 assert!(patched.contains("-- INCLUDE blah.p8"), "{}", &patched);
330 }
331
332 #[test]
333 fn test_bad_comment() {
334 let lua = "--==configurations==--";
335 let patched = patch_lua(lua);
336 assert_eq!(patched.trim(), "--==configurations==--");
337 }
338
339 #[test]
340 fn test_bad_if() {
341 let lua =
342 "if (ord(tb.str[tb.i],tb.char)!=32) sfx(tb.voice) -- play the voice sound effect.";
343 let patched = patch_lua(lua);
344 assert_eq!(
345 patched.trim(),
346 "if ord(tb.str[tb.i],tb.char)~=32 then sfx(tb.voice) end -- play the voice sound effect."
347 );
348 }
349
350 #[test]
351 fn test_bad_incr() {
352 let lua = "tb.i+=1 -- increase the index, to display the next message on tb.str";
353 let patched = patch_lua(lua);
354 assert_eq!(
355 patched.trim(),
356 "tb.i = tb.i + (1) -- increase the index, to display the next message on tb.str"
357 );
358 }
359
360 #[test]
361 fn test_button() {
362 let lua = "if btnp(➡️) or btn(❎) then";
363 let patched = patch_lua(lua);
364 assert_eq!(patched.trim(), "if btnp(1) or btn(5) then");
365 }
366
367 #[test]
368 fn test_button2() {
369 let lua = "if btnp(❎) then";
370 let patched = patch_lua(lua);
371 assert_eq!(patched.trim(), "if btnp(5) then");
372 }
373
374 #[test]
375 fn test_button3() {
376 let lua = "if btnp(🅾) then";
377 let patched = patch_lua(lua);
378 assert_eq!(patched.trim(), "if btnp(4) then");
379 }
380
381 fn assert_patch(unpatched: &str, expected_patched: &str) {
382 let patched = patch_lua(unpatched);
383 assert_eq!(patched, expected_patched);
384 }
385
386 #[test]
387 fn test_cardboard_toad0() {
388 assert_patch(
389 "if (o.color) setmetatable(o.color, { __index = (message_instance or message).color })",
390 "if o.color then setmetatable(o.color, { __index = (message_instance or message).color }) end",
391 );
392 }
393
394 #[test]
395 fn test_cardboard_toad1() {
396 assert_patch(
397 r#"
398if ((abs(x) < (a.w+a2.w)) and
399 (abs(y) < (a.h+a2.h)))
400 then "hi" end
401"#,
402 r#"
403if ((abs(x) < (a.w+a2.w)) and
404 (abs(y) < (a.h+a2.h)))
405 then "hi" end
406"#,
407 );
408 }
409
410 #[test]
411 fn test_cardboard_toad2() {
412 assert_patch(
413 r#"
414 if (self.sprites ~= nil) then
415 self.sprite = self.sprites[self.sprites_index]
416 end
417"#,
418 r#"
419 if (self.sprites ~= nil) then
420 self.sprite = self.sprites[self.sprites_index]
421 end
422"#,
423 );
424 }
425
426 #[test]
427 fn test_cardboard_toad3() {
428 assert_patch("accum += f.delay or self.delay",
436 "accum = accum + (f.delay or self.delay)");
437
438 assert_patch("if true then accum += f.delay or self.delay end",
439 "if true then accum = accum + (f.delay or self.delay) end");
440 }
441
442 #[test]
443 fn test_celeste0() {
444 assert_patch("if freeze>0 then freeze-=1 return end",
445 "if freeze>0 then freeze = freeze - (1) return end");
446 }
447
448
449 #[test]
450 fn test_pooh_big_adventure0() {
451 assert_patch("if btnp(3) then self.choice += 1; result = true end",
452 "if btnp(3) then self.choice = self.choice + (1); result = true end");
453
454 assert_patch(" i += 1",
455 " i = i + (1)");
456 }
457
458 #[test]
459 fn test_plist0() {
460 let lua = r#"
461i += 1
462local key = keys[i]
463"#;
464 let patched = patch_lua(lua);
465 assert!(patched.contains("i = i + (1)"));
466 }
467
468 #[test]
469 fn test_find_includes() {
470
471 let lua = r#"
472#include a.p8
473#include b.lua
474"#;
475 assert_eq!(find_includes(lua).collect::<Vec<_>>(), vec!["a.p8", "b.lua"]);
476 }
477
478
479 #[test]
480 #[ignore = "need a real parser to fix this; see 'antlr' branch"]
481 fn test_not_so_well0() {
482 assert_eq!(patch_lua("pos += (delta - thresh):map(function(v) return mid(0, v, 4) end)"),
483 "pos = pos + ((delta - thresh):map(function(v) return mid(0, v, 4) end))");
484 }
485}