rilua 0.1.18

Lua 5.1.1 implemented in Rust, targeting the World of Warcraft addon variant.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
//! Coroutine library: create, resume, yield, wrap, status, running.
//!
//! Reference: coroutine functions in `lbaselib.c`, `lua_resume`/`lua_yield`
//! in `ldo.c`, PUC-Rio Lua 5.1.1.
//!
//! ## Architecture
//!
//! rilua uses a "swap model" for coroutines: `LuaState` always represents
//! the currently executing thread. When resuming a coroutine, the resumer's
//! per-thread state (stack, call_stack, etc.) is saved to a `LuaThread`
//! on the Rust stack, the coroutine's state is loaded from the GC arena
//! into `LuaState`, execution proceeds, and then the states are swapped
//! back.
//!
//! Yield is implemented via `LuaError::Yield(n_results)`. This error
//! propagates through the Rust call stack back to the resume handler,
//! which catches it and treats it as a successful yield.
//!
//! The `n_ccalls` counter (managed by `call_function`, the `luaD_call`
//! equivalent) determines the yield boundary: yield is only allowed when
//! `n_ccalls == 0`, preventing yield across C-call boundaries.

use crate::error::{LuaError, LuaResult, RuntimeError};
use crate::vm::callinfo::LUA_MULTRET;
use crate::vm::closure::{Closure, RustClosure, Upvalue};
use crate::vm::execute::{self, CallResult};
use crate::vm::gc::arena::GcRef;
use crate::vm::state::{LuaState, LuaThread, ThreadStatus};
use crate::vm::value::Val;

// ---------------------------------------------------------------------------
// Argument helpers (same pattern as other stdlib modules)
// ---------------------------------------------------------------------------

/// Returns the number of arguments passed to the current Rust function.
fn nargs(state: &LuaState) -> usize {
    let func = state.call_stack[state.ci].func;
    if state.top > func + 1 {
        state.top - func - 1
    } else {
        0
    }
}

/// Returns argument `n` (0-based) to the current Rust function.
fn arg(state: &LuaState, n: usize) -> Val {
    let func = state.call_stack[state.ci].func;
    state.stack_get(func + 1 + n)
}

/// Creates a simple runtime error.
fn simple_error(msg: String) -> LuaError {
    LuaError::Runtime(RuntimeError {
        message: msg,
        level: 0,
        traceback: vec![],
    })
}

// ---------------------------------------------------------------------------
// coroutine.create(f)
// ---------------------------------------------------------------------------

/// Creates a new coroutine with body `f`. `f` must be a Lua function.
///
/// Returns the new coroutine (as a thread value).
///
/// Reference: `luaB_cocreate` in `lbaselib.c`.
pub fn co_create(state: &mut LuaState) -> LuaResult<u32> {
    let func_val = arg(state, 0);

    // Validate: must be a Lua function (not a Rust function).
    match func_val {
        Val::Function(r) => {
            let cl = state
                .gc
                .closures
                .get(r)
                .ok_or_else(|| simple_error("invalid function reference".into()))?;
            if matches!(cl, Closure::Rust(_)) {
                return Err(simple_error(
                    "bad argument #1 to 'create' (Lua function expected)".into(),
                ));
            }
        }
        _ => {
            return Err(simple_error(
                "bad argument #1 to 'create' (Lua function expected)".into(),
            ));
        }
    }

    // Create a new thread with the function on its stack.
    let thread = LuaThread::new(func_val, state.global);
    let thread_ref = state.gc.alloc_thread(thread);

    state.push(Val::Thread(thread_ref));
    Ok(1)
}

// ---------------------------------------------------------------------------
// coroutine.resume(co, ...)
// ---------------------------------------------------------------------------

/// Resumes a coroutine. Returns `true, results...` on success or yield,
/// `false, error_message` on error.
///
/// Reference: `luaB_coresume` + `auxresume` in `lbaselib.c`,
/// `lua_resume` in `ldo.c`.
pub fn co_resume(state: &mut LuaState) -> LuaResult<u32> {
    let co_val = arg(state, 0);
    let Val::Thread(co_ref) = co_val else {
        return Err(simple_error(
            "bad argument #1 to 'resume' (coroutine expected)".into(),
        ));
    };

    // Collect arguments to pass to the coroutine.
    let n_resume_args = if nargs(state) > 1 {
        nargs(state) - 1
    } else {
        0
    };
    let mut resume_args = Vec::with_capacity(n_resume_args);
    for i in 0..n_resume_args {
        resume_args.push(arg(state, 1 + i));
    }

    // Run auxresume and translate the result.
    match auxresume(state, co_ref, &resume_args) {
        Ok(results) => {
            // Success or yield: push true + all results.
            let base = state.base;
            state.stack_set(base, Val::Bool(true));
            for (i, val) in results.iter().enumerate() {
                state.stack_set(base + 1 + i, *val);
            }
            state.top = base + 1 + results.len();
            Ok((1 + results.len()) as u32)
        }
        Err(error_val) => {
            // Error: push false + error value (preserved as-is).
            // PUC-Rio: lua_pushboolean(L, 0); lua_insert(L, -2); return 2;
            let base = state.base;
            state.stack_set(base, Val::Bool(false));
            state.stack_set(base + 1, error_val);
            state.top = base + 2;
            Ok(2)
        }
    }
}

/// Closes ALL open upvalues from the current thread and returns pairs
/// of `(upvalue_ref, stack_index)` for later reopening.
///
/// In rilua's swap model, the active thread's stack is about to be saved
/// and another thread's stack loaded. ANY open upvalue pointing into the
/// current stack would read from the wrong thread after the swap. We
/// close them all, capturing their current values into `Closed` state.
///
/// The returned pairs allow `load_thread_by_ref` to reopen the upvalues
/// when this thread becomes active again.
///
/// Must be called BEFORE `save_thread_state` while the thread's stack
/// is still active in `state.stack`.
fn close_thread_upvalues(state: &mut LuaState) -> Vec<(GcRef<Upvalue>, usize)> {
    let mut suspended = Vec::new();
    for &uv_ref in &state.open_upvalues {
        if let Some(uv) = state.gc.upvalues.get(uv_ref)
            && let Some(idx) = uv.stack_index()
        {
            suspended.push((uv_ref, idx));
        }
    }
    for &(uv_ref, _) in &suspended {
        if let Some(uv) = state.gc.upvalues.get_mut(uv_ref) {
            uv.close(&state.stack);
        }
    }
    suspended
}

/// Core resume logic shared by `coroutine.resume` and `coroutine.wrap`.
///
/// Returns `Ok(results)` on success/yield, `Err(error_val)` on error.
/// The error value is preserved as a `Val` to match PUC-Rio behavior
/// where non-string error objects (functions, tables, etc.) pass through
/// `resume`/`wrap` without stringification.
///
/// Reference: `auxresume` in `lbaselib.c`.
pub(crate) fn auxresume(
    state: &mut LuaState,
    co_ref: GcRef<LuaThread>,
    args: &[Val],
) -> Result<Vec<Val>, Val> {
    // Check coroutine status.
    let co_status = state
        .gc
        .threads
        .get(co_ref)
        .map_or(ThreadStatus::Dead, |t| t.status);

    match co_status {
        ThreadStatus::Dead => {
            let r = state.gc.intern_string(b"cannot resume dead coroutine");
            return Err(Val::Str(r));
        }
        ThreadStatus::Running | ThreadStatus::Normal => {
            let r = state
                .gc
                .intern_string(b"cannot resume non-suspended coroutine");
            return Err(Val::Str(r));
        }
        ThreadStatus::Initial | ThreadStatus::Suspended => {
            // OK to resume.
        }
    }

    // Close ALL open upvalues from the current (resumer) thread before the
    // stack swap. Any open upvalue pointing into this thread's stack would
    // read from the wrong data after the coroutine's stack is loaded.
    // The suspended pairs are stored on the resumer so they can be reopened
    // when control returns to this thread.
    let resumer_suspended = close_thread_upvalues(state);

    // Save the identity of the calling thread (for nested resume tracking).
    let saved_current_thread = state.current_thread;

    // Save the resumer's state onto saved_threads so the GC can
    // traverse it during coroutine execution. Without this, the
    // resumer's stack values (closures, strings, etc.) would be
    // invisible to the GC and could be incorrectly freed.
    let mut resumer = state.save_thread_state();
    resumer.suspended_upvals = resumer_suspended;
    state.saved_threads.push(resumer);

    // Load the coroutine's state into LuaState.
    state.load_thread_by_ref(co_ref, ThreadStatus::Running);

    // Track which thread is active.
    state.current_thread = Some(co_ref);

    // Transfer arguments to the coroutine's stack.
    if co_status == ThreadStatus::Initial {
        // First resume: push args after the function (they become function args).
        for &val in args {
            state.push(val);
        }
    } else {
        // Resuming from yield: the yield's CI is still on the call stack.
        // Push resume args as the "return values" of yield.
        // The yielded values area starts at state.base. Replace with resume args.
        for (i, &val) in args.iter().enumerate() {
            state.stack_set(state.base + i, val);
        }
        state.top = state.base + args.len();
    }

    // Execute the coroutine.
    let exec_result = if co_status == ThreadStatus::Initial {
        // First resume: call the function at stack[0].
        // Use precall+execute directly (not call_function) so n_ccalls stays 0.
        // This allows yield within the coroutine body.
        let func_idx = 0;
        (|| -> LuaResult<()> {
            match state.precall(func_idx, LUA_MULTRET)? {
                CallResult::Lua => execute::execute(state),
                CallResult::Rust => Ok(()),
            }
        })()
    } else if state.yielded_in_hook {
        // Resuming from a hook yield: the execute loop yielded directly
        // at a hook dispatch point (via yield_on_hook). No Rust/Lua hook
        // function was called, so there's no CI to pop with poscall.
        // Just restore base and continue execution from the saved PC.
        state.base = state.call_stack[state.ci].base;
        if state.top < state.base {
            state.top = state.base;
        }
        state.yielded_in_hook = false;

        (|| -> LuaResult<()> {
            while state.ci > 0 {
                execute::execute(state)?;
            }
            Ok(())
        })()
    } else {
        // Resuming from yield: the coroutine was suspended inside a Rust
        // function (yield). We need to complete the interrupted call.
        //
        // In PUC-Rio, resume() calls poscall for the interrupted C function,
        // then luaV_execute(L, ci - base_ci). The nexeccalls parameter makes
        // PUC-Rio's flat loop process ALL remaining CI levels.
        //
        // In rilua's recursive model, each execute() handles one function
        // level. When yield unwound the Rust call stack, the nested
        // execute() frames were lost. We must loop, calling execute() for
        // each CI level until the coroutine's base function completes
        // (ci == 0) or a new yield occurs.
        //
        // The resume args are at state.base..state.top. poscall reads from
        // first_result and places results at the caller's expected position.
        let first_result = state.base;
        if state.poscall(first_result) {
            state.top = state.call_stack[state.ci].top;
        }

        // Continue execution from where we left off.
        // Loop to handle all CI levels that were active when yield happened.
        // ci[0] is the sentinel base CI — execute only runs for ci > 0.
        // Each execute() handles one function level (OP_RETURN pops the CI).
        // When ci reaches 0, the coroutine's function has returned normally.
        (|| -> LuaResult<()> {
            while state.ci > 0 {
                execute::execute(state)?;
            }
            Ok(())
        })()
    };

    // Determine outcome and collect results.
    match exec_result {
        Ok(()) => {
            // Coroutine returned normally. Collect return values.
            // After execute returns from OP_RETURN at the top level,
            // results are at the coroutine's stack base area.
            let mut results = Vec::new();
            let ci_func = state.call_stack[state.ci].func;
            for i in ci_func..state.top {
                results.push(state.stack_get(i));
            }

            // Save coroutine state back (dead) and restore resumer.
            // Pop is safe: we push exactly once before execute, pop once after.
            let Some(resumer) = state.saved_threads.pop() else {
                let r = state
                    .gc
                    .intern_string(b"internal error: missing resumer state");
                return Err(Val::Str(r));
            };
            state.save_and_restore_by_ref(co_ref, ThreadStatus::Dead, resumer);
            state.current_thread = saved_current_thread;
            Ok(results)
        }
        Err(LuaError::Yield(n_results)) => {
            // Coroutine yielded. Collect yielded values.
            // The values are the top n_results on the stack.
            let mut results = Vec::new();
            let start = state.top.saturating_sub(n_results as usize);
            for i in start..state.top {
                results.push(state.stack_get(i));
            }

            // Save coroutine state as suspended and restore resumer.
            // Pop is safe: we push exactly once before execute, pop once after.
            let Some(resumer) = state.saved_threads.pop() else {
                let r = state
                    .gc
                    .intern_string(b"internal error: missing resumer state");
                return Err(Val::Str(r));
            };
            state.save_and_restore_by_ref(co_ref, ThreadStatus::Suspended, resumer);
            state.current_thread = saved_current_thread;
            Ok(results)
        }
        Err(err) => {
            // Coroutine errored. Preserve the error value as-is.
            // PUC-Rio: lua_xmove(co, L, 1) moves the error value directly.
            let error_val = state.error_object.take().unwrap_or_else(|| {
                let r = state.gc.intern_string(err.to_string().as_bytes());
                Val::Str(r)
            });

            // Save coroutine state as dead and restore resumer.
            // Pop is safe: we push exactly once before execute, pop once after.
            let Some(resumer) = state.saved_threads.pop() else {
                let r = state
                    .gc
                    .intern_string(b"internal error: missing resumer state");
                return Err(Val::Str(r));
            };
            state.save_and_restore_by_ref(co_ref, ThreadStatus::Dead, resumer);
            state.current_thread = saved_current_thread;
            Err(error_val)
        }
    }
}

// ---------------------------------------------------------------------------
// coroutine.yield(...)
// ---------------------------------------------------------------------------

/// Suspends the currently running coroutine.
///
/// All arguments become the results of `coroutine.resume()` in the
/// resumer. When the coroutine is resumed again, `yield` returns
/// the arguments passed to `resume`.
///
/// Reference: `luaB_yield` + `lua_yield` in PUC-Rio Lua 5.1.1.
pub fn co_yield(state: &mut LuaState) -> LuaResult<u32> {
    // Check yield boundary: can't yield from the main thread or across
    // C-call boundaries. PUC-Rio checks nCcalls > baseCcalls; here we
    // use current_thread == None as the main-thread indicator (the main
    // thread's initial call always increments nCcalls in PUC-Rio, making
    // yield impossible there).
    if state.current_thread.is_none() || state.n_ccalls > 0 {
        return Err(simple_error(
            "attempt to yield across metamethod/C-call boundary".into(),
        ));
    }

    // The yielded values are the arguments to yield (already on the stack).
    let n = nargs(state) as u32;

    // Signal yield by returning a special error.
    // The resume handler will catch this and collect the yielded values.
    Err(LuaError::Yield(n))
}

// ---------------------------------------------------------------------------
// coroutine.wrap(f)
// ---------------------------------------------------------------------------

/// Creates a coroutine and returns a function that resumes it each time
/// it is called. Arguments to the function go to `resume`; results of
/// `yield` become results of the function. Errors propagate.
///
/// Reference: `luaB_cowrap` + `luaB_auxwrap` in `lbaselib.c`.
pub fn co_wrap(state: &mut LuaState) -> LuaResult<u32> {
    // Create the coroutine (reuse co_create logic).
    let func_val = arg(state, 0);

    // Validate: must be a Lua function.
    match func_val {
        Val::Function(r) => {
            let cl = state
                .gc
                .closures
                .get(r)
                .ok_or_else(|| simple_error("invalid function reference".into()))?;
            if matches!(cl, Closure::Rust(_)) {
                return Err(simple_error(
                    "bad argument #1 to 'wrap' (Lua function expected)".into(),
                ));
            }
        }
        _ => {
            return Err(simple_error(
                "bad argument #1 to 'wrap' (Lua function expected)".into(),
            ));
        }
    }

    let thread = LuaThread::new(func_val, state.global);
    let thread_ref = state.gc.alloc_thread(thread);

    // Create a Rust closure with the thread as upvalue[0].
    let mut wrapper = RustClosure::new(wrap_aux, "wrap_aux");
    wrapper.upvalues.push(Val::Thread(thread_ref));
    let closure_ref = state.gc.alloc_closure(Closure::Rust(wrapper));

    state.push(Val::Function(closure_ref));
    Ok(1)
}

/// The wrapped coroutine resume function (upvalue[0] = thread).
///
/// Reference: `luaB_auxwrap` in `lbaselib.c`.
fn wrap_aux(state: &mut LuaState) -> LuaResult<u32> {
    // Get the thread from upvalue[0].
    let ci_func = state.call_stack[state.ci].func;
    let func_val = state.stack_get(ci_func);
    let Val::Function(closure_ref) = func_val else {
        return Err(simple_error("invalid wrap closure".into()));
    };

    let co_ref = {
        let cl = state
            .gc
            .closures
            .get(closure_ref)
            .ok_or_else(|| simple_error("invalid closure reference".into()))?;
        match cl {
            Closure::Rust(rc) => {
                if let Some(Val::Thread(r)) = rc.upvalues.first() {
                    *r
                } else {
                    return Err(simple_error("wrap: missing thread upvalue".into()));
                }
            }
            _ => return Err(simple_error("wrap: expected Rust closure".into())),
        }
    };

    // Collect arguments.
    let n = nargs(state);
    let mut args = Vec::with_capacity(n);
    for i in 0..n {
        args.push(arg(state, i));
    }

    // Resume the coroutine.
    match auxresume(state, co_ref, &args) {
        Ok(results) => {
            // Push results directly (no true/false prefix).
            let base = state.base;
            for (i, val) in results.iter().enumerate() {
                state.stack_set(base + i, *val);
            }
            state.top = base + results.len();
            Ok(results.len() as u32)
        }
        Err(error_val) => {
            // Propagate error (unlike resume which returns false+error_val).
            // PUC-Rio luaB_auxwrap: if error is a string, prepend location.
            // Then lua_error(L) re-raises with the error value.
            let final_val = if let Val::Str(r) = error_val {
                // String error: prepend source location (luaL_where pattern).
                let where_prefix = execute::get_where(state, 1);
                if where_prefix.is_empty() {
                    error_val
                } else {
                    let original = state
                        .gc
                        .string_arena
                        .get(r)
                        .map(|s| String::from_utf8_lossy(s.data()).to_string())
                        .unwrap_or_default();
                    let full = format!("{where_prefix}{original}");
                    Val::Str(state.gc.intern_string(full.as_bytes()))
                }
            } else {
                // Non-string error: re-raise as-is.
                error_val
            };
            state.error_object = Some(final_val);
            let display = match final_val {
                Val::Str(r) => state
                    .gc
                    .string_arena
                    .get(r)
                    .map(|s| String::from_utf8_lossy(s.data()).to_string())
                    .unwrap_or_default(),
                _ => format!("{final_val}"),
            };
            Err(LuaError::Runtime(RuntimeError {
                message: display,
                level: 0,
                traceback: vec![],
            }))
        }
    }
}

// ---------------------------------------------------------------------------
// coroutine.status(co)
// ---------------------------------------------------------------------------

/// Returns the status of a coroutine as a string:
/// `"running"`, `"suspended"`, `"normal"`, or `"dead"`.
///
/// Reference: `luaB_costatus` in `lbaselib.c`.
pub fn co_status(state: &mut LuaState) -> LuaResult<u32> {
    let co_val = arg(state, 0);
    let Val::Thread(co_ref) = co_val else {
        return Err(simple_error(
            "bad argument #1 to 'status' (coroutine expected)".into(),
        ));
    };

    // Check if this coroutine is the currently running one.
    if state.current_thread == Some(co_ref) {
        let s = state.gc.intern_string(b"running");
        state.push(Val::Str(s));
        return Ok(1);
    }

    let status_str = match state.gc.threads.get(co_ref).map(|t| t.status) {
        Some(ThreadStatus::Running) => "running",
        Some(ThreadStatus::Initial) | Some(ThreadStatus::Suspended) => "suspended",
        Some(ThreadStatus::Normal) => "normal",
        Some(ThreadStatus::Dead) | None => "dead",
    };

    let s = state.gc.intern_string(status_str.as_bytes());
    state.push(Val::Str(s));
    Ok(1)
}

// ---------------------------------------------------------------------------
// coroutine.running()
// ---------------------------------------------------------------------------

/// Returns the running coroutine, or nil if called from the main thread.
///
/// Reference: `luaB_corunning` in `lbaselib.c`.
pub fn co_running(state: &mut LuaState) -> LuaResult<u32> {
    match state.current_thread {
        Some(co_ref) => {
            state.push(Val::Thread(co_ref));
            Ok(1)
        }
        None => {
            // Main thread: return nothing (nil).
            Ok(0)
        }
    }
}