1use super::corelib::{string_key, unwrap_lua_value};
2use super::VM;
3use crate::bytecode::value::ValueKey;
4use crate::bytecode::{NativeCallResult, Value};
5use crate::config::LustConfig;
6use crate::lua_compat::register_lust_function;
7use crate::LustInt;
8use rand::rngs::StdRng;
9use rand::{Rng, SeedableRng};
10use regex::Regex;
11use std::fs;
12use std::io::{self, Read, Write};
13use std::rc::Rc;
14use std::sync::{Mutex, OnceLock};
15use std::thread;
16use std::time::{Duration, SystemTime, UNIX_EPOCH};
17
18static RNG: OnceLock<Mutex<StdRng>> = OnceLock::new();
19pub fn create_stdlib(config: &LustConfig, vm: &VM) -> Vec<(&'static str, Value)> {
20 let mut stdlib = vec![
21 ("print", create_print_fn()),
22 ("println", create_println_fn()),
23 ("type", create_type_fn()),
24 ("select", create_select_fn()),
25 ("random", create_math_random_fn()),
26 ("randomseed", create_math_randomseed_fn()),
27 ];
28 if config.is_module_enabled("io") {
29 stdlib.push(("io", create_io_module(vm)));
30 }
31
32 if config.is_module_enabled("string") {
33 stdlib.push(("string", create_string_module(vm)));
34 }
35
36 if config.is_module_enabled("os") {
37 stdlib.push(("os", create_os_module(vm)));
38 }
39
40 stdlib
41}
42
43fn create_print_fn() -> Value {
44 Value::NativeFunction(Rc::new(|args: &[Value]| {
45 for (i, arg) in args.iter().enumerate() {
46 if i > 0 {
47 print!("\t");
48 }
49
50 print!("{}", arg);
51 }
52
53 Ok(NativeCallResult::Return(Value::Nil))
54 }))
55}
56
57fn create_println_fn() -> Value {
58 Value::NativeFunction(Rc::new(|args: &[Value]| {
59 for (i, arg) in args.iter().enumerate() {
60 if i > 0 {
61 print!("\t");
62 }
63
64 print!("{}", arg);
65 }
66
67 println!();
68 Ok(NativeCallResult::Return(Value::Nil))
69 }))
70}
71
72fn create_type_fn() -> Value {
73 Value::NativeFunction(Rc::new(|args: &[Value]| {
74 if args.is_empty() {
75 return Err("type() requires at least one argument".to_string());
76 }
77
78 let value = &args[0];
79
80 if let Value::Enum { enum_name, variant, .. } = value {
82 if enum_name == "LuaValue" {
83 let lua_type = match variant.as_str() {
84 "Nil" => "nil",
85 "Bool" => "boolean",
86 "Int" | "Float" => "number",
87 "String" => "string",
88 "Table" => "table",
89 "Function" => "function",
90 "Userdata" | "LightUserdata" => "userdata",
91 "Thread" => "thread",
92 _ => "unknown",
93 };
94 return Ok(NativeCallResult::Return(Value::enum_variant(
95 "LuaValue",
96 "String",
97 vec![Value::string(lua_type)],
98 )));
99 }
100 }
101
102 let type_name = match value {
104 Value::Nil => "nil",
105 Value::Bool(_) => "bool",
106 Value::Int(_) => "int",
107 Value::Float(_) => "float",
108 Value::String(_) => "string",
109 Value::Array(_) => "array",
110 Value::Tuple(_) => "tuple",
111 Value::Map(_) => "map",
112 Value::Struct { .. } | Value::WeakStruct(_) => "struct",
113 Value::Enum { .. } => "enum",
114 Value::Function(_) => "function",
115 Value::NativeFunction(_) => "function",
116 Value::Closure { .. } => "function",
117 Value::Iterator(_) => "iterator",
118 Value::Task(_) => "task",
119 };
120 Ok(NativeCallResult::Return(Value::enum_variant(
121 "LuaValue",
122 "String",
123 vec![Value::string(type_name)],
124 )))
125 }))
126}
127
128pub(crate) fn create_select_fn() -> Value {
129 Value::NativeFunction(Rc::new(|args: &[Value]| {
130 if args.is_empty() {
131 return Err("select expects at least one argument".to_string());
132 }
133 let selector = unwrap_lua_value(args[0].clone());
134 let mut values: Vec<Value> = Vec::new();
135 for arg in args.iter().skip(1) {
136 let val = unwrap_lua_value(arg.clone());
137 if let Some(arr) = val.as_array() {
138 values.extend(arr.into_iter());
139 } else {
140 values.push(val);
141 }
142 }
143 if let Some(s) = selector.as_string() {
144 if s == "#" {
145 return Ok(NativeCallResult::Return(Value::Int(values.len() as LustInt)));
146 } else {
147 return Err("select expects '#' or an index as the first argument".to_string());
148 }
149 }
150 let raw_idx = if let Some(i) = selector.as_int() {
151 i
152 } else if let Some(f) = selector.as_float() {
153 f as LustInt
154 } else {
155 return Err("select expects '#' or an integer as the first argument".to_string());
156 };
157 let len = values.len() as isize;
158 let mut start = if raw_idx < 0 {
159 len + raw_idx as isize + 1
160 } else {
161 raw_idx as isize
162 };
163 if start < 1 {
164 start = 1;
165 }
166 let start_idx = (start - 1) as usize;
167 if start_idx >= values.len() {
168 return return_lua_values(Vec::new());
169 }
170 return_lua_values(values[start_idx..].to_vec())
171 }))
172}
173
174fn create_io_module(vm: &VM) -> Value {
175 let entries = [
176 (string_key("read_file"), create_io_read_file_fn()),
177 (
178 string_key("read_file_bytes"),
179 create_io_read_file_bytes_fn(),
180 ),
181 (string_key("write_file"), create_io_write_file_fn()),
182 (string_key("read_stdin"), create_io_read_stdin_fn()),
183 (string_key("read_line"), create_io_read_line_fn()),
184 (string_key("write_stdout"), create_io_write_stdout_fn()),
185 ];
186 vm.map_with_entries(entries)
187}
188
189fn create_io_read_file_fn() -> Value {
190 Value::NativeFunction(Rc::new(|args: &[Value]| {
191 if args.len() != 1 {
192 return Ok(NativeCallResult::Return(Value::err(Value::string(
193 "io.read_file(path) requires a single string path",
194 ))));
195 }
196
197 let path = match args[0].as_string() {
198 Some(p) => p,
199 None => {
200 return Ok(NativeCallResult::Return(Value::err(Value::string(
201 "io.read_file(path) requires a string path",
202 ))))
203 }
204 };
205 match fs::read_to_string(path) {
206 Ok(contents) => Ok(NativeCallResult::Return(Value::ok(Value::string(contents)))),
207 Err(err) => Ok(NativeCallResult::Return(Value::err(Value::string(
208 err.to_string(),
209 )))),
210 }
211 }))
212}
213
214fn create_io_read_file_bytes_fn() -> Value {
215 Value::NativeFunction(Rc::new(|args: &[Value]| {
216 if args.len() != 1 {
217 return Ok(NativeCallResult::Return(Value::err(Value::string(
218 "io.read_file_bytes(path) requires a single string path",
219 ))));
220 }
221
222 let path = match args[0].as_string() {
223 Some(p) => p,
224 None => {
225 return Ok(NativeCallResult::Return(Value::err(Value::string(
226 "io.read_file_bytes(path) requires a string path",
227 ))))
228 }
229 };
230
231 match fs::read(path) {
232 Ok(bytes) => {
233 let values: Vec<Value> = bytes
234 .into_iter()
235 .map(|b| Value::Int(b as LustInt))
236 .collect();
237 Ok(NativeCallResult::Return(Value::ok(Value::array(values))))
238 }
239
240 Err(err) => Ok(NativeCallResult::Return(Value::err(Value::string(
241 err.to_string(),
242 )))),
243 }
244 }))
245}
246
247fn create_io_write_file_fn() -> Value {
248 Value::NativeFunction(Rc::new(|args: &[Value]| {
249 if args.len() < 2 {
250 return Ok(NativeCallResult::Return(Value::err(Value::string(
251 "io.write_file(path, contents) requires a path and value",
252 ))));
253 }
254
255 let path = match args[0].as_string() {
256 Some(p) => p,
257 None => {
258 return Ok(NativeCallResult::Return(Value::err(Value::string(
259 "io.write_file(path, contents) requires a string path",
260 ))))
261 }
262 };
263 let contents = if let Some(s) = args[1].as_string() {
264 s.to_string()
265 } else {
266 format!("{}", args[1])
267 };
268 match fs::write(path, contents) {
269 Ok(_) => Ok(NativeCallResult::Return(Value::ok(Value::Nil))),
270 Err(err) => Ok(NativeCallResult::Return(Value::err(Value::string(
271 err.to_string(),
272 )))),
273 }
274 }))
275}
276
277fn create_io_read_stdin_fn() -> Value {
278 Value::NativeFunction(Rc::new(|args: &[Value]| {
279 if !args.is_empty() {
280 return Ok(NativeCallResult::Return(Value::err(Value::string(
281 "io.read_stdin() takes no arguments",
282 ))));
283 }
284
285 let mut buffer = String::new();
286 match io::stdin().read_to_string(&mut buffer) {
287 Ok(_) => Ok(NativeCallResult::Return(Value::ok(Value::string(buffer)))),
288 Err(err) => Ok(NativeCallResult::Return(Value::err(Value::string(
289 err.to_string(),
290 )))),
291 }
292 }))
293}
294
295fn create_io_read_line_fn() -> Value {
296 Value::NativeFunction(Rc::new(|args: &[Value]| {
297 if !args.is_empty() {
298 return Ok(NativeCallResult::Return(Value::err(Value::string(
299 "io.read_line() takes no arguments",
300 ))));
301 }
302
303 let mut line = String::new();
304 match io::stdin().read_line(&mut line) {
305 Ok(_) => Ok(NativeCallResult::Return(Value::ok(Value::string(line)))),
306 Err(err) => Ok(NativeCallResult::Return(Value::err(Value::string(
307 err.to_string(),
308 )))),
309 }
310 }))
311}
312
313fn create_io_write_stdout_fn() -> Value {
314 Value::NativeFunction(Rc::new(|args: &[Value]| {
315 let mut stdout = io::stdout();
316 for arg in args {
317 if let Err(err) = write!(stdout, "{}", arg) {
318 return Ok(NativeCallResult::Return(Value::err(Value::string(
319 err.to_string(),
320 ))));
321 }
322 }
323
324 if let Err(err) = stdout.flush() {
325 return Ok(NativeCallResult::Return(Value::err(Value::string(
326 err.to_string(),
327 ))));
328 }
329
330 Ok(NativeCallResult::Return(Value::ok(Value::Nil)))
331 }))
332}
333
334fn create_os_module(vm: &VM) -> Value {
335 let entries = [
336 (string_key("time"), create_os_time_fn()),
337 (string_key("sleep"), create_os_sleep_fn()),
338 (string_key("create_file"), create_os_create_file_fn()),
339 (string_key("create_dir"), create_os_create_dir_fn()),
340 (string_key("remove_file"), create_os_remove_file_fn()),
341 (string_key("remove_dir"), create_os_remove_dir_fn()),
342 (string_key("rename"), create_os_rename_fn()),
343 ];
344 vm.map_with_entries(entries)
345}
346
347fn create_os_time_fn() -> Value {
348 Value::NativeFunction(Rc::new(|args: &[Value]| {
349 if !args.is_empty() {
350 return Ok(NativeCallResult::Return(Value::err(Value::string(
351 "os.time() takes no arguments",
352 ))));
353 }
354
355 let now = SystemTime::now();
356 let seconds = match now.duration_since(UNIX_EPOCH) {
357 Ok(duration) => duration.as_secs_f64(),
358 Err(err) => -(err.duration().as_secs_f64()),
359 };
360
361 Ok(NativeCallResult::Return(Value::Float(seconds)))
362 }))
363}
364
365fn create_os_sleep_fn() -> Value {
366 Value::NativeFunction(Rc::new(|args: &[Value]| {
367 if args.len() != 1 {
368 return Ok(NativeCallResult::Return(Value::err(Value::string(
369 "os.sleep(seconds) requires a single float duration",
370 ))));
371 }
372
373 let seconds = match args[0].as_float() {
374 Some(value) => value,
375 None => {
376 return Ok(NativeCallResult::Return(Value::err(Value::string(
377 "os.sleep(seconds) requires a float duration",
378 ))))
379 }
380 };
381
382 if !seconds.is_finite() || seconds < 0.0 {
383 return Ok(NativeCallResult::Return(Value::err(Value::string(
384 "os.sleep(seconds) requires a finite, non-negative duration",
385 ))));
386 }
387
388 if seconds > (u64::MAX as f64) {
389 return Ok(NativeCallResult::Return(Value::err(Value::string(
390 "os.sleep(seconds) duration is too large",
391 ))));
392 }
393
394 thread::sleep(Duration::from_secs_f64(seconds));
395
396 Ok(NativeCallResult::Return(Value::ok(Value::Nil)))
397 }))
398}
399
400fn create_os_create_file_fn() -> Value {
401 Value::NativeFunction(Rc::new(|args: &[Value]| {
402 if args.len() != 1 {
403 return Ok(NativeCallResult::Return(Value::err(Value::string(
404 "os.create_file(path) requires a single string path",
405 ))));
406 }
407
408 let path = match args[0].as_string() {
409 Some(p) => p,
410 None => {
411 return Ok(NativeCallResult::Return(Value::err(Value::string(
412 "os.create_file(path) requires a string path",
413 ))))
414 }
415 };
416 match fs::OpenOptions::new().write(true).create(true).open(path) {
417 Ok(_) => Ok(NativeCallResult::Return(Value::ok(Value::Nil))),
418 Err(err) => Ok(NativeCallResult::Return(Value::err(Value::string(
419 err.to_string(),
420 )))),
421 }
422 }))
423}
424
425fn create_os_create_dir_fn() -> Value {
426 Value::NativeFunction(Rc::new(|args: &[Value]| {
427 if args.len() != 1 {
428 return Ok(NativeCallResult::Return(Value::err(Value::string(
429 "os.create_dir(path) requires a single string path",
430 ))));
431 }
432
433 let path = match args[0].as_string() {
434 Some(p) => p,
435 None => {
436 return Ok(NativeCallResult::Return(Value::err(Value::string(
437 "os.create_dir(path) requires a string path",
438 ))))
439 }
440 };
441 match fs::create_dir_all(path) {
442 Ok(_) => Ok(NativeCallResult::Return(Value::ok(Value::Nil))),
443 Err(err) => Ok(NativeCallResult::Return(Value::err(Value::string(
444 err.to_string(),
445 )))),
446 }
447 }))
448}
449
450fn create_os_remove_file_fn() -> Value {
451 Value::NativeFunction(Rc::new(|args: &[Value]| {
452 if args.len() != 1 {
453 return Ok(NativeCallResult::Return(Value::err(Value::string(
454 "os.remove_file(path) requires a single string path",
455 ))));
456 }
457
458 let path = match args[0].as_string() {
459 Some(p) => p,
460 None => {
461 return Ok(NativeCallResult::Return(Value::err(Value::string(
462 "os.remove_file(path) requires a string path",
463 ))))
464 }
465 };
466 match fs::remove_file(path) {
467 Ok(_) => Ok(NativeCallResult::Return(Value::ok(Value::Nil))),
468 Err(err) => Ok(NativeCallResult::Return(Value::err(Value::string(
469 err.to_string(),
470 )))),
471 }
472 }))
473}
474
475fn create_os_remove_dir_fn() -> Value {
476 Value::NativeFunction(Rc::new(|args: &[Value]| {
477 if args.len() != 1 {
478 return Ok(NativeCallResult::Return(Value::err(Value::string(
479 "os.remove_dir(path) requires a single string path",
480 ))));
481 }
482
483 let path = match args[0].as_string() {
484 Some(p) => p,
485 None => {
486 return Ok(NativeCallResult::Return(Value::err(Value::string(
487 "os.remove_dir(path) requires a string path",
488 ))))
489 }
490 };
491 match fs::remove_dir_all(path) {
492 Ok(_) => Ok(NativeCallResult::Return(Value::ok(Value::Nil))),
493 Err(err) => Ok(NativeCallResult::Return(Value::err(Value::string(
494 err.to_string(),
495 )))),
496 }
497 }))
498}
499
500fn create_os_rename_fn() -> Value {
501 Value::NativeFunction(Rc::new(|args: &[Value]| {
502 if args.len() != 2 {
503 return Ok(NativeCallResult::Return(Value::err(Value::string(
504 "os.rename(from, to) requires two string paths",
505 ))));
506 }
507
508 let from = match args[0].as_string() {
509 Some(f) => f,
510 None => {
511 return Ok(NativeCallResult::Return(Value::err(Value::string(
512 "os.rename(from, to) requires string paths",
513 ))))
514 }
515 };
516 let to = match args[1].as_string() {
517 Some(t) => t,
518 None => {
519 return Ok(NativeCallResult::Return(Value::err(Value::string(
520 "os.rename(from, to) requires string paths",
521 ))))
522 }
523 };
524 match fs::rename(from, to) {
525 Ok(_) => Ok(NativeCallResult::Return(Value::ok(Value::Nil))),
526 Err(err) => Ok(NativeCallResult::Return(Value::err(Value::string(
527 err.to_string(),
528 )))),
529 }
530 }))
531}
532
533fn create_string_module(vm: &VM) -> Value {
534 let entries = [
535 (string_key("len"), create_string_len_fn()),
536 (string_key("lower"), create_string_lower_fn()),
537 (string_key("upper"), create_string_upper_fn()),
538 (string_key("sub"), create_string_sub_fn()),
539 (string_key("byte"), create_string_byte_fn()),
540 (string_key("char"), create_string_char_fn()),
541 (string_key("find"), create_string_find_fn()),
542 (string_key("match"), create_string_match_fn()),
543 (string_key("gsub"), create_string_gsub_fn()),
544 (string_key("format"), create_string_format_fn()),
545 ];
546 vm.map_with_entries(entries)
547}
548
549fn create_string_len_fn() -> Value {
550 Value::NativeFunction(Rc::new(|args: &[Value]| {
551 let input = args.get(0).cloned().unwrap_or(Value::Nil);
552 let value = unwrap_lua_value(input);
553 match value {
554 Value::Nil => Ok(NativeCallResult::Return(Value::Int(0))), Value::String(s) => Ok(NativeCallResult::Return(Value::Int(s.len() as LustInt))),
556 other => Err(format!("string.len expects a string, got {:?}", other)),
557 }
558 }))
559}
560
561fn create_string_lower_fn() -> Value {
562 Value::NativeFunction(Rc::new(|args: &[Value]| {
563 let value = unwrap_lua_value(args.get(0).cloned().unwrap_or(Value::Nil));
564 let s = value
565 .as_string()
566 .ok_or_else(|| "string.lower expects a string".to_string())?;
567 Ok(NativeCallResult::Return(Value::string(&s.to_lowercase())))
568 }))
569}
570
571fn create_string_upper_fn() -> Value {
572 Value::NativeFunction(Rc::new(|args: &[Value]| {
573 let value = unwrap_lua_value(args.get(0).cloned().unwrap_or(Value::Nil));
574 let s = value
575 .as_string()
576 .ok_or_else(|| "string.upper expects a string".to_string())?;
577 Ok(NativeCallResult::Return(Value::string(&s.to_uppercase())))
578 }))
579}
580
581fn create_string_sub_fn() -> Value {
582 Value::NativeFunction(Rc::new(|args: &[Value]| {
583 let value = unwrap_lua_value(args.get(0).cloned().unwrap_or(Value::Nil));
584 let source = value
585 .as_string()
586 .ok_or_else(|| "string.sub expects a string".to_string())?;
587 let start = args
588 .get(1)
589 .map(|v| unwrap_lua_value(v.clone()).as_int().unwrap_or(1))
590 .unwrap_or(1);
591 let end = args
592 .get(2)
593 .map(|v| unwrap_lua_value(v.clone()).as_int().unwrap_or(source.len() as LustInt));
594 let (start_idx, end_idx) = normalize_range(start, end, source.len());
595 if start_idx >= source.len() || start_idx >= end_idx {
596 return Ok(NativeCallResult::Return(Value::string("")));
597 }
598 let slice = &source.as_bytes()[start_idx..end_idx.min(source.len())];
599 Ok(NativeCallResult::Return(Value::string(
600 String::from_utf8_lossy(slice),
601 )))
602 }))
603}
604
605fn create_string_byte_fn() -> Value {
606 Value::NativeFunction(Rc::new(|args: &[Value]| {
607 let value = unwrap_lua_value(args.get(0).cloned().unwrap_or(Value::Nil));
608 let source = value
609 .as_string()
610 .ok_or_else(|| "string.byte expects a string".to_string())?;
611 let start = args
612 .get(1)
613 .map(|v| unwrap_lua_value(v.clone()).as_int().unwrap_or(1))
614 .unwrap_or(1);
615 let end = args
616 .get(2)
617 .map(|v| unwrap_lua_value(v.clone()).as_int().unwrap_or(start));
618 let (start_idx, end_idx) = normalize_range(start, end, source.len());
619 let bytes = source.as_bytes();
620 if start_idx >= bytes.len() || start_idx >= end_idx {
621 return return_lua_values(vec![lua_nil()]);
622 }
623 let mut values = Vec::new();
624 for b in &bytes[start_idx..end_idx.min(bytes.len())] {
625 values.push(Value::Int(*b as LustInt));
626 }
627 return_lua_values(values)
628 }))
629}
630
631fn create_string_char_fn() -> Value {
632 Value::NativeFunction(Rc::new(|args: &[Value]| {
633 let mut output = String::new();
634 for arg in args {
635 let raw = unwrap_lua_value(arg.clone());
636 let code = raw
637 .as_int()
638 .or_else(|| raw.as_float().map(|f| f as LustInt))
639 .ok_or_else(|| "string.char expects numeric arguments".to_string())?;
640 if code < 0 || code > 255 {
641 return Err("string.char codepoints must be in [0,255]".to_string());
642 }
643 if let Some(ch) = char::from_u32(code as u32) {
644 output.push(ch);
645 }
646 }
647 Ok(NativeCallResult::Return(Value::string(output)))
648 }))
649}
650
651fn create_string_find_fn() -> Value {
652 Value::NativeFunction(Rc::new(|args: &[Value]| {
653 let subject = unwrap_lua_value(args.get(0).cloned().unwrap_or(Value::Nil));
654 let pattern_val = unwrap_lua_value(args.get(1).cloned().unwrap_or(Value::Nil));
655 let haystack = subject
656 .as_string()
657 .ok_or_else(|| "string.find expects a string subject".to_string())?;
658 let pattern = pattern_val
659 .as_string()
660 .ok_or_else(|| "string.find expects a pattern string".to_string())?;
661 let start = args
662 .get(2)
663 .map(|v| unwrap_lua_value(v.clone()).as_int().unwrap_or(1))
664 .unwrap_or(1);
665 let plain = args
666 .get(3)
667 .map(|v| matches!(unwrap_lua_value(v.clone()), Value::Bool(true)))
668 .unwrap_or(false);
669 let (offset, _) = normalize_range(start, None, haystack.len());
670 if offset > haystack.len() {
671 return return_lua_values(vec![lua_nil()]);
672 }
673 let slice = haystack.get(offset..).unwrap_or("");
674 if plain {
675 if let Some(pos) = slice.find(pattern) {
676 let begin = offset + pos;
677 let end = begin + pattern.len().saturating_sub(1);
678 return return_lua_values(vec![
679 Value::Int((begin as LustInt) + 1),
680 Value::Int((end as LustInt) + 1),
681 ]);
682 }
683 return return_lua_values(vec![lua_nil()]);
684 }
685 let regex = lua_pattern_to_regex(pattern)?;
686 if let Some(caps) = regex.captures(slice) {
687 if let Some(mat) = caps.get(0) {
688 let begin = offset + mat.start();
689 let end = offset + mat.end().saturating_sub(1);
690 let mut results: Vec<Value> = vec![
691 Value::Int((begin as LustInt) + 1),
692 Value::Int((end as LustInt) + 1),
693 ];
694 for idx in 1..caps.len() {
695 if let Some(c) = caps.get(idx) {
696 results.push(Value::string(c.as_str()));
697 } else {
698 results.push(lua_nil());
699 }
700 }
701 return return_lua_values(results);
702 }
703 }
704 return_lua_values(vec![lua_nil()])
705 }))
706}
707
708fn create_string_match_fn() -> Value {
709 Value::NativeFunction(Rc::new(|args: &[Value]| {
710 let subject = unwrap_lua_value(args.get(0).cloned().unwrap_or(Value::Nil));
711 let pattern_val = unwrap_lua_value(args.get(1).cloned().unwrap_or(Value::Nil));
712 let haystack = subject
713 .as_string()
714 .ok_or_else(|| "string.match expects a string subject".to_string())?;
715 let pattern = pattern_val
716 .as_string()
717 .ok_or_else(|| "string.match expects a pattern string".to_string())?;
718 let start = args
719 .get(2)
720 .map(|v| unwrap_lua_value(v.clone()).as_int().unwrap_or(1))
721 .unwrap_or(1);
722 let (offset, _) = normalize_range(start, None, haystack.len());
723 if offset > haystack.len() {
724 return Ok(NativeCallResult::Return(Value::Nil));
725 }
726 let slice = haystack.get(offset..).unwrap_or("");
727 let regex = lua_pattern_to_regex(pattern)?;
728 let Some(caps) = regex.captures(slice) else {
729 return Ok(NativeCallResult::Return(Value::Nil));
730 };
731 let Some(mat) = caps.get(0) else {
732 return Ok(NativeCallResult::Return(Value::Nil));
733 };
734
735 let capture_count = caps.len().saturating_sub(1);
736 if capture_count == 0 {
737 return Ok(NativeCallResult::Return(Value::string(mat.as_str())));
738 }
739 if capture_count == 1 {
740 if let Some(c) = caps.get(1) {
741 return Ok(NativeCallResult::Return(Value::string(c.as_str())));
742 }
743 return Ok(NativeCallResult::Return(Value::Nil));
744 }
745
746 let mut results = Vec::with_capacity(capture_count);
747 for idx in 1..caps.len() {
748 if let Some(c) = caps.get(idx) {
749 results.push(Value::string(c.as_str()));
750 } else {
751 results.push(Value::Nil);
752 }
753 }
754 return_lua_values(results)
755 }))
756}
757
758fn create_string_gsub_fn() -> Value {
759 Value::NativeFunction(Rc::new(|args: &[Value]| {
760 let subject = unwrap_lua_value(args.get(0).cloned().unwrap_or(Value::Nil));
761 let pattern_val = unwrap_lua_value(args.get(1).cloned().unwrap_or(Value::Nil));
762 let repl = args.get(2).cloned().unwrap_or(Value::Nil);
763 let limit = args
764 .get(3)
765 .map(|v| unwrap_lua_value(v.clone()).as_int().unwrap_or(-1))
766 .unwrap_or(-1);
767 let text = subject
768 .as_string()
769 .ok_or_else(|| "string.gsub expects a string subject".to_string())?;
770 let pattern = pattern_val
771 .as_string()
772 .ok_or_else(|| "string.gsub expects a pattern string".to_string())?;
773 let regex = lua_pattern_to_regex(pattern)?;
774 let max_repls = if limit < 0 { i64::MAX } else { limit };
775 enum Replacer {
776 String(String),
777 Func(Value),
778 Other(Value),
779 }
780 let replacer = match &repl {
781 Value::Enum { enum_name, variant, .. }
782 if enum_name == "LuaValue" && variant == "Function" =>
783 {
784 Replacer::Func(repl.clone())
785 }
786 Value::Function(_) | Value::NativeFunction(_) | Value::Closure { .. } => {
787 Replacer::Func(repl.clone())
788 }
789 _ => {
790 let unwrapped = unwrap_lua_value(repl.clone());
791 match unwrapped {
792 Value::String(s) => Replacer::String(s.to_string()),
793 other => Replacer::Other(other),
794 }
795 }
796 };
797 let mut last_end = 0;
798 let mut count: i64 = 0;
799 let mut output = String::new();
800 for caps in regex.captures_iter(text) {
801 if count >= max_repls {
802 break;
803 }
804 let mat = caps.get(0).unwrap();
805 output.push_str(&text[last_end..mat.start()]);
806 let replacement = match &replacer {
807 Replacer::String(template) => build_template_replacement(template.as_str(), &caps),
808 Replacer::Func(func_val) => {
809 VM::with_current(|vm| {
810 let mut call_args = Vec::new();
811 if caps.len() > 1 {
812 for idx in 1..caps.len() {
813 if let Some(c) = caps.get(idx) {
814 call_args.push(to_lua_value(vm, Value::string(c.as_str()))?);
815 } else {
816 call_args.push(lua_nil());
817 }
818 }
819 } else if let Some(m) = caps.get(0) {
820 call_args.push(to_lua_value(vm, Value::string(m.as_str()))?);
821 }
822 let result = vm
823 .call_value(func_val, call_args)
824 .map_err(|e| e.to_string())?;
825 let first = unwrap_first_return(result);
826 Ok(first.to_string())
827 })?
828 }
829 Replacer::Other(other) => other.to_string(),
830 };
831 output.push_str(&replacement);
832 last_end = mat.end();
833 count += 1;
834 }
835 output.push_str(&text[last_end..]);
836 return_lua_values(vec![Value::string(output), Value::Int(count)])
837 }))
838}
839
840fn create_string_format_fn() -> Value {
841 Value::NativeFunction(Rc::new(|args: &[Value]| {
842 if args.is_empty() {
843 return Err("string.format requires a format string".to_string());
844 }
845 let fmt_val = unwrap_lua_value(args[0].clone());
846 let fmt = fmt_val
847 .as_string()
848 .ok_or_else(|| "string.format expects a string format".to_string())?;
849 let rendered = render_format(fmt, &args[1..])?;
850 Ok(NativeCallResult::Return(Value::string(rendered)))
851 }))
852}
853
854#[derive(Clone)]
855enum TableData {
856 Array(Value),
857 Map(Value),
858}
859
860fn table_data(value: &Value) -> Option<TableData> {
861 if value.as_array().is_some() {
862 return Some(TableData::Array(value.clone()));
863 }
864
865 if value.as_map().is_some() {
866 return Some(TableData::Map(value.clone()));
867 }
868
869 if let Some(map) = value.struct_get_field("table") {
870 if map.as_map().is_some() {
871 return Some(TableData::Map(map));
872 }
873 }
874
875 None
876}
877
878fn read_sequence(data: &TableData) -> Vec<Value> {
879 match data {
880 TableData::Array(val) => val.as_array().unwrap_or_default(),
881 TableData::Map(val) => {
882 let map = val.as_map().unwrap_or_default();
883 let mut seq: Vec<Value> = Vec::new();
884 let mut idx: LustInt = 1;
885 loop {
886 let key = ValueKey::from_value(&Value::Int(idx));
887 if let Some(val) = map.get(&key) {
888 seq.push(val.clone());
889 idx += 1;
890 } else {
891 break;
892 }
893 }
894 seq
895 }
896 }
897}
898
899pub(crate) fn create_table_unpack_fn() -> Value {
900 Value::NativeFunction(Rc::new(|args: &[Value]| {
901 if args.is_empty() {
902 return Err("table.unpack expects a table/array".to_string());
903 }
904 let table_val = unwrap_lua_value(args[0].clone());
905 let Some(data) = table_data(&table_val) else {
906 return Err("table.unpack expects a table/array".to_string());
907 };
908 let seq = read_sequence(&data);
909 let start = args
910 .get(1)
911 .and_then(|v| unwrap_lua_value(v.clone()).as_int())
912 .unwrap_or(1);
913 let end = args
914 .get(2)
915 .and_then(|v| unwrap_lua_value(v.clone()).as_int())
916 .unwrap_or(seq.len() as LustInt);
917 let start_idx = (start - 1).max(0) as usize;
918 let end_idx = end.max(0) as usize;
919 let mut values: Vec<Value> = Vec::new();
920 for (i, val) in seq.iter().enumerate() {
921 if i < start_idx || i >= end_idx {
922 continue;
923 }
924 values.push(val.clone());
925 }
926 return_lua_values(values)
927 }))
928}
929
930fn create_math_random_fn() -> Value {
931 Value::NativeFunction(Rc::new(|args: &[Value]| {
932 let lower = args
933 .get(0)
934 .map(|v| unwrap_lua_value(v.clone()))
935 .and_then(|v| if matches!(v, Value::Nil) { None } else { Some(v) });
936 let upper = args
937 .get(1)
938 .map(|v| unwrap_lua_value(v.clone()))
939 .and_then(|v| if matches!(v, Value::Nil) { None } else { Some(v) });
940 let value = with_rng_mut(|rng| match (lower.as_ref(), upper.as_ref()) {
941 (None, _) => Value::Float(rng.gen::<f64>()),
942 (Some(max), None) => {
943 let hi = coerce_int(max).unwrap_or(1);
944 let upper_bound = if hi < 1 { 1 } else { hi };
945 Value::Int(rng.gen_range(1..=upper_bound))
946 }
947 (Some(min), Some(max)) => {
948 let lo = coerce_int(min).unwrap_or(1);
949 let hi = coerce_int(max).unwrap_or(lo);
950 let (start, end) = if lo <= hi { (lo, hi) } else { (hi, lo) };
951 Value::Int(rng.gen_range(start..=end))
952 }
953 })?;
954 Ok(NativeCallResult::Return(value))
955 }))
956}
957
958fn create_math_randomseed_fn() -> Value {
959 Value::NativeFunction(Rc::new(|args: &[Value]| {
960 let seed_val = args
961 .get(0)
962 .map(|v| unwrap_lua_value(v.clone()))
963 .unwrap_or(Value::Int(0));
964 let seed = coerce_int(&seed_val).unwrap_or(0) as u64;
965 let mutex = RNG.get_or_init(|| Mutex::new(StdRng::from_entropy()));
966 *mutex.lock().map_err(|e| e.to_string())? = StdRng::seed_from_u64(seed);
967 Ok(NativeCallResult::Return(Value::Nil))
968 }))
969}
970
971fn coerce_int(value: &Value) -> Option<LustInt> {
972 match value {
973 Value::Int(i) => Some(*i),
974 Value::Float(f) => Some(*f as LustInt),
975 Value::Bool(b) => Some(if *b { 1 } else { 0 }),
976 _ => None,
977 }
978}
979
980fn with_rng_mut<F, R>(f: F) -> Result<R, String>
981where
982 F: FnOnce(&mut StdRng) -> R,
983{
984 let mutex = RNG.get_or_init(|| Mutex::new(StdRng::from_entropy()));
985 mutex
986 .lock()
987 .map_err(|e| e.to_string())
988 .map(|mut guard| f(&mut *guard))
989}
990
991fn render_format(fmt: &str, args: &[Value]) -> Result<String, String> {
992 let mut out = String::new();
993 let mut arg_idx = 0;
994 let mut chars = fmt.chars().peekable();
995 while let Some(ch) = chars.next() {
996 if ch != '%' {
997 out.push(ch);
998 continue;
999 }
1000 if let Some('%') = chars.peek() {
1001 chars.next();
1002 out.push('%');
1003 continue;
1004 }
1005 let mut zero_pad = false;
1006 if let Some('0') = chars.peek() {
1007 zero_pad = true;
1008 chars.next();
1009 }
1010 let mut width_str = String::new();
1011 while let Some(next) = chars.peek() {
1012 if next.is_ascii_digit() {
1013 width_str.push(*next);
1014 chars.next();
1015 } else {
1016 break;
1017 }
1018 }
1019 let mut precision: Option<usize> = None;
1020 if let Some('.') = chars.peek() {
1021 chars.next();
1022 let mut prec = String::new();
1023 while let Some(next) = chars.peek() {
1024 if next.is_ascii_digit() {
1025 prec.push(*next);
1026 chars.next();
1027 } else {
1028 break;
1029 }
1030 }
1031 if !prec.is_empty() {
1032 precision = prec.parse().ok();
1033 }
1034 }
1035 let spec = chars
1036 .next()
1037 .ok_or_else(|| "incomplete format specifier".to_string())?;
1038 let width = if width_str.is_empty() {
1039 None
1040 } else {
1041 width_str.parse().ok()
1042 };
1043 let arg = args
1044 .get(arg_idx)
1045 .cloned()
1046 .unwrap_or(Value::Nil);
1047 arg_idx += 1;
1048 let raw = unwrap_lua_value(arg);
1049 let formatted = match spec {
1050 's' => raw.to_string(),
1051 'd' | 'i' | 'u' => {
1052 let num = raw
1053 .as_int()
1054 .or_else(|| raw.as_float().map(|f| f as LustInt))
1055 .unwrap_or(0);
1056 pad_value(format!("{}", num), width, zero_pad)
1057 }
1058 'x' => {
1059 let num = raw
1060 .as_int()
1061 .or_else(|| raw.as_float().map(|f| f as LustInt))
1062 .unwrap_or(0);
1063 pad_value(format!("{:x}", num), width, zero_pad)
1064 }
1065 'X' => {
1066 let num = raw
1067 .as_int()
1068 .or_else(|| raw.as_float().map(|f| f as LustInt))
1069 .unwrap_or(0);
1070 pad_value(format!("{:X}", num), width, zero_pad)
1071 }
1072 'c' => {
1073 let num = raw
1074 .as_int()
1075 .or_else(|| raw.as_float().map(|f| f as LustInt))
1076 .unwrap_or(0);
1077 if let Some(ch) = char::from_u32(num as u32) {
1078 ch.to_string()
1079 } else {
1080 "".to_string()
1081 }
1082 }
1083 'f' | 'g' => {
1084 let num = raw
1085 .as_float()
1086 .or_else(|| raw.as_int().map(|i| i as f64))
1087 .unwrap_or(0.0);
1088 if let Some(p) = precision {
1089 pad_value(format!("{:.*}", p, num), width, zero_pad)
1090 } else {
1091 pad_value(format!("{}", num), width, zero_pad)
1092 }
1093 }
1094 other => {
1095 pad_value(format!("{}", other), width, zero_pad)
1096 }
1097 };
1098 out.push_str(&formatted);
1099 }
1100 Ok(out)
1101}
1102
1103fn pad_value(value: String, width: Option<usize>, zero_pad: bool) -> String {
1104 if let Some(w) = width {
1105 if value.len() < w {
1106 let mut padded = String::new();
1107 let pad_char = if zero_pad { '0' } else { ' ' };
1108 for _ in 0..(w - value.len()) {
1109 padded.push(pad_char);
1110 }
1111 padded.push_str(&value);
1112 return padded;
1113 }
1114 }
1115 value
1116}
1117
1118fn normalize_range(start: LustInt, end: Option<LustInt>, len: usize) -> (usize, usize) {
1119 let len_i = len as LustInt;
1120 let mut s = if start < 0 { len_i + start + 1 } else { start };
1121 let mut e = end.unwrap_or(len_i);
1122 if e < 0 {
1123 e = len_i + e + 1;
1124 }
1125 if s < 1 {
1126 s = 1;
1127 }
1128 if e < 0 {
1129 e = 0;
1130 }
1131 if e > len_i {
1132 e = len_i;
1133 }
1134 if s > e {
1135 return (len, len);
1136 }
1137 (
1138 s.saturating_sub(1) as usize,
1139 e.max(0) as usize,
1140 )
1141}
1142
1143fn lua_pattern_to_regex(pattern: &str) -> Result<Regex, String> {
1144 fn is_regex_meta(ch: char) -> bool {
1145 matches!(ch, '.' | '+' | '*' | '?' | '^' | '$' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '\\')
1146 }
1147 let mut out = String::new();
1148 let mut chars = pattern.chars().peekable();
1149 let mut in_class = false;
1150 while let Some(ch) = chars.next() {
1151 match ch {
1152 '%' => {
1153 if let Some(next) = chars.next() {
1154 let translated = match next {
1155 'a' => Some(if in_class { "A-Za-z" } else { "[A-Za-z]" }),
1156 'c' => Some(if in_class { "\\p{Cc}" } else { "[\\p{Cc}]" }),
1157 'd' => Some(if in_class { "0-9" } else { "[0-9]" }),
1158 'l' => Some(if in_class { "a-z" } else { "[a-z]" }),
1159 'u' => Some(if in_class { "A-Z" } else { "[A-Z]" }),
1160 'w' => Some(if in_class { "A-Za-z0-9_" } else { "[A-Za-z0-9_]" }),
1161 'x' => Some(if in_class { "A-Fa-f0-9" } else { "[A-Fa-f0-9]" }),
1162 's' => Some(if in_class { "\\s" } else { "[\\s]" }),
1163 'p' => Some(if in_class { "\\p{P}" } else { "[\\p{P}]" }),
1164 'z' => Some(if in_class { "\\x00" } else { "[\\x00]" }),
1165 '%' => Some("%"),
1166 _ => None,
1167 };
1168 if let Some(rep) = translated {
1169 out.push_str(rep);
1170 } else {
1171 if is_regex_meta(next) {
1172 out.push('\\');
1173 }
1174 out.push(next);
1175 }
1176 } else {
1177 out.push('%');
1178 }
1179 }
1180 '[' => {
1181 in_class = true;
1182 out.push('[');
1183 }
1184 ']' => {
1185 in_class = false;
1186 out.push(']');
1187 }
1188 '.' | '+' | '*' | '?' => out.push(ch),
1189 '^' | '$' | '(' | ')' => out.push(ch),
1190 '{' | '}' | '|' | '\\' => {
1191 out.push('\\');
1192 out.push(ch);
1193 }
1194 '-' => {
1195 if in_class {
1196 out.push('-');
1197 } else {
1198 out.push_str("*?");
1199 }
1200 }
1201 other => {
1202 if !in_class && is_regex_meta(other) {
1203 out.push('\\');
1204 }
1205 out.push(other);
1206 }
1207 }
1208 }
1209 Regex::new(&out).map_err(|e| e.to_string())
1210}
1211
1212fn build_template_replacement(template: &str, caps: ®ex::Captures) -> String {
1213 let mut out = String::new();
1214 let mut chars = template.chars().peekable();
1215 while let Some(ch) = chars.next() {
1216 if ch == '%' {
1217 if let Some(next) = chars.next() {
1218 if next == '%' {
1219 out.push('%');
1220 continue;
1221 }
1222 if let Some(d) = next.to_digit(10) {
1223 let idx = d as usize;
1224 if idx == 0 {
1225 if let Some(m) = caps.get(0) {
1226 out.push_str(m.as_str());
1227 }
1228 } else if let Some(m) = caps.get(idx) {
1229 out.push_str(m.as_str());
1230 }
1231 continue;
1232 }
1233 out.push(next);
1234 } else {
1235 out.push('%');
1236 }
1237 } else {
1238 out.push(ch);
1239 }
1240 }
1241 out
1242}
1243
1244fn to_lua_value(vm: &VM, value: Value) -> Result<Value, String> {
1245 if let Value::Enum { enum_name, .. } = &value {
1246 if enum_name == "LuaValue" {
1247 return Ok(value);
1248 }
1249 }
1250 Ok(match value.clone() {
1251 Value::Nil => Value::enum_unit("LuaValue", "Nil"),
1252 Value::Bool(b) => Value::enum_variant("LuaValue", "Bool", vec![Value::Bool(b)]),
1253 Value::Int(i) => Value::enum_variant("LuaValue", "Int", vec![Value::Int(i)]),
1254 Value::Float(f) => Value::enum_variant("LuaValue", "Float", vec![Value::Float(f)]),
1255 Value::String(s) => Value::enum_variant("LuaValue", "String", vec![Value::String(s)]),
1256 Value::Map(map) => {
1257 let table = Value::Map(map.clone());
1258 let metamethods = vm.new_map_value();
1259 let lua_table = vm
1260 .instantiate_struct(
1261 "LuaTable",
1262 vec![
1263 (Rc::new("table".to_string()), table),
1264 (Rc::new("metamethods".to_string()), metamethods),
1265 ],
1266 )
1267 .map_err(|e| e.to_string())?;
1268 Value::enum_variant("LuaValue", "Table", vec![lua_table])
1269 }
1270 Value::Function(_) | Value::Closure { .. } | Value::NativeFunction(_) => {
1271 let handle = register_lust_function(value.clone());
1272 let lua_fn = vm
1273 .instantiate_struct(
1274 "LuaFunction",
1275 vec![(Rc::new("handle".to_string()), Value::Int(handle as LustInt))],
1276 )
1277 .map_err(|e| e.to_string())?;
1278 Value::enum_variant("LuaValue", "Function", vec![lua_fn])
1279 }
1280 other => Value::enum_variant(
1281 "LuaValue",
1282 "LightUserdata",
1283 vec![Value::Int(other.type_of() as LustInt)],
1284 ),
1285 })
1286}
1287
1288fn return_lua_values(values: Vec<Value>) -> Result<NativeCallResult, String> {
1289 VM::with_current(|vm| pack_lua_values(vm, values).map(NativeCallResult::Return))
1290}
1291
1292fn pack_lua_values(vm: &VM, values: Vec<Value>) -> Result<Value, String> {
1293 let mut packed = Vec::with_capacity(values.len());
1294 for value in values {
1295 packed.push(to_lua_value(vm, value)?);
1296 }
1297 Ok(Value::array(packed))
1298}
1299
1300fn unwrap_first_return(value: Value) -> Value {
1301 if let Value::Array(arr) = value {
1302 if let Some(first) = arr.borrow().get(0) {
1303 return unwrap_lua_value(first.clone());
1304 }
1305 return Value::Nil;
1306 }
1307 unwrap_lua_value(value)
1308}
1309
1310fn lua_nil() -> Value {
1311 Value::enum_unit("LuaValue", "Nil")
1312}
1313
1314#[cfg(test)]
1315mod tests {
1316 use super::*;
1317 #[test]
1318 fn stdlib_defaults_without_optional_modules() {
1319 let vm = VM::with_config(&LustConfig::default());
1320 let stdlib = create_stdlib(&LustConfig::default(), &vm);
1321 assert!(!stdlib.iter().any(|(name, _)| *name == "io"));
1322 assert!(!stdlib.iter().any(|(name, _)| *name == "os"));
1323 assert!(!stdlib.iter().any(|(name, _)| *name == "string"));
1324 }
1325
1326 #[test]
1327 fn stdlib_includes_optional_modules_when_configured() {
1328 let cfg = LustConfig::from_toml_str(
1329 r#"
1330 [settings]
1331 stdlib_modules = ["io", "os", "string"]
1332 "#,
1333 )
1334 .expect("parse");
1335 let vm = VM::with_config(&cfg);
1336 let stdlib = create_stdlib(&cfg, &vm);
1337 assert!(stdlib.iter().any(|(name, _)| *name == "io"));
1338 assert!(stdlib.iter().any(|(name, _)| *name == "os"));
1339 assert!(stdlib.iter().any(|(name, _)| *name == "string"));
1340 }
1341}