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