ab15e9c933
A script error reached the `seterrorhandler` function once per frame, from a queue, after every other listener had run. In 1.12.1 the handler is the error function of the pcall each script runner makes (`0x704e4f`-`0x704e79`; the error function `0x703b40`). So it runs at the raise, inside the failing script: `this`, `event` and `arg1` are still the failing handler's, `debugstack()` shows the failing call, and the event's next listener has not run yet. Every benilla runner now passes that error function: handlers, OnLoad, file chunks, RunScript, key bindings and slash commands. It returns the handler's result, as `0x703b40` does. The queue is gone. A handler body that fails to compile reaches no handler (`0x704c70`). benilla's host log keeps each original fault, and `/errors` replays the ones raised before benilla's layer loaded. Tests cover the raise-time globals, the stack, the listener order, OnUpdate and chunks. Fixes #2656 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3771 lines
156 KiB
Rust
3771 lines
156 KiB
Rust
//! The virtual machine: one stack, one call-frame list, and the script host's 5.1 semantics for
|
|
//! calls, metamethods, coercions and error messages.
|
|
|
|
use std::rc::Rc;
|
|
|
|
use crate::heap::{FuncObj, Heap, LuaFunc, NativeFn, UpvalObj, UserObj};
|
|
use crate::number::str2d;
|
|
use crate::pools::Block;
|
|
use crate::proto::{Arith, Cmp, Op, Proto, Reg, Rk, RK_CONST};
|
|
use crate::value::{FuncRef, StrRef, TableRef, Value, V};
|
|
|
|
/// An error in flight: the error object, which `pcall` answers.
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub struct LuaError {
|
|
pub value: Value,
|
|
}
|
|
|
|
/// 5.1.5's `LUAI_MAXCCALLS`: nested host-level calls (a host call, a metamethod, a library
|
|
/// callback); past it by an eighth, an error handler's own calls are refused too.
|
|
const MAXCCALLS: u32 = 200;
|
|
/// The client's `LUA_MAXCALLS`, 5.0's 4096: `luaD_growCI` (`0x6f61c0`) refuses a call-info
|
|
/// array past it (`0x6f61c7`, `0x6f61e4`).
|
|
const MAXCALLS: usize = 4096;
|
|
/// 5.0's `BASIC_CI_SIZE`: the call-info array's first size (`stack_init`'s 0xc0 bytes,
|
|
/// `0x6f6be4`).
|
|
const BASIC_CI_SIZE: usize = 8;
|
|
/// 5.0's `EXTRA_STACK`: the slots past `stack_last` (`luaD_reallocstack`, `0x6f5e4d`), so a check
|
|
/// leaves this many and one more above what it asks for.
|
|
const EXTRA_STACK: usize = 5;
|
|
/// 5.0's first stack, `BASIC_STACK_SIZE + EXTRA_STACK` slots (`stack_init`'s 0x2d0 bytes,
|
|
/// `0x6f6bb8`).
|
|
const BASIC_STACK_SIZE: usize = 40 + EXTRA_STACK;
|
|
/// 5.0's `LUA_MINSTACK`: the room `luaD_precall` asks for a native (`0x6f6142`).
|
|
const LUA_MINSTACK: usize = 20;
|
|
/// Rust stack the nested host-level calls under one entry may use. C counts only levels, its
|
|
/// frames being small; a Rust level is larger, and much larger unoptimized, so the level count
|
|
/// alone could run a 2 MB thread out of stack. Past this the call raises "C stack overflow" as
|
|
/// at `LUAI_MAXCCALLS`, with the same eighth of margin for a handler. 200 levels of the deepest
|
|
/// library callback use under half of it in an optimized build, so there it never decides.
|
|
const MAX_RUST_STACK: usize = 1 << 20;
|
|
/// A stack slot in the collector's count: 5.0's 16-byte `TObject` (`stack_init`'s 0x2d0 bytes for
|
|
/// 45 slots, `0x6f6bb8`), counted as the stack grows.
|
|
const STACK_SLOT_BYTES: usize = 16;
|
|
/// A call record in the collector's count: 5.0's 24-byte `CallInfo` (`luaD_reallocCI`,
|
|
/// `0x6f5f1a`).
|
|
const CI_BYTES: usize = 24;
|
|
/// The state's own records in the collector's count, the block count a new state starts from
|
|
/// (`0x6f6e0b`): the 0x68-byte `lua_State` (`0x6f6b8d`) and the 0xc0-byte `global_State`
|
|
/// (`0x6f6d88`).
|
|
const STATE_BYTES: usize = 0x128;
|
|
/// The tables `luaV_gettable` (`0x6f7cf0`) reads along an `__index` chain entered from the API
|
|
/// (`lua_gettable`, `0x6f3ab3`): the loop count starts at 0 and the chain raises `loop in
|
|
/// gettable` once it passes 100 (`0x6f7cf7`), so the 101 tables at counts 0 to 100.
|
|
const GETTABLE_TABLES_API: u32 = 101;
|
|
/// The same chain entered from an instruction: the VM reads the first table itself and hands the
|
|
/// `__index` step a count of 0 (`0x6f8940`, `0x6f8a0a`, `0x6f8bf3`, `0x6f8c0d`), so the chain
|
|
/// reaches one table more.
|
|
const GETTABLE_TABLES_OP: u32 = 102;
|
|
/// The tables `luaV_settable` (`0x6f7f40`) writes along an `__newindex` chain: its count starts at
|
|
/// 0 and raises `loop in settable` once the table it just left makes it pass 100 (`0x6f7ffa`),
|
|
/// so the 101 tables, whichever way it is entered.
|
|
const SETTABLE_TABLES: u32 = 101;
|
|
|
|
#[derive(Clone, Copy)]
|
|
struct Frame {
|
|
func: FuncRef,
|
|
/// The first register; the called function sits just below, where its results go.
|
|
base: u32,
|
|
/// The next instruction, saved as C Lua saves `savedpc`: a pointer, so resuming the frame
|
|
/// reads one value to dispatch. Null for a native.
|
|
ip: *const Op,
|
|
/// Results the caller wants; -1 for all.
|
|
nresults: i32,
|
|
/// The code this frame runs, null for a native. Valid while the frame exists: its closure is
|
|
/// a collector root, and a hot reload retires the code it replaces instead of dropping it
|
|
/// (`Vm::retired`).
|
|
proto: *const Proto,
|
|
/// The closure's upvalue list and environment as the call found them, so entering the
|
|
/// frame reads no closure. The list is valid as `proto` is; the environment is reread
|
|
/// when `Vm::fenv_gen` moves.
|
|
upvals: *const u32,
|
|
env: TableRef,
|
|
/// `Vm::fenv_gen` when `env` was read: a `setfenv` since moves it, and the next global
|
|
/// access rereads the closure's environment.
|
|
env_gen: u32,
|
|
/// The constant table and register count, so resuming the frame reads no prototype.
|
|
consts: *const Value,
|
|
maxstack: u16,
|
|
/// The chunk this frame's code came from (`Heap::sources`).
|
|
source: u32,
|
|
/// Tail calls this frame replaced, which 5.0 counts as stack levels.
|
|
tailcalls: u32,
|
|
/// Returning from this frame ends the `execute` that entered it.
|
|
boundary: bool,
|
|
}
|
|
|
|
// A call writes one cache line of frame.
|
|
const _: () = assert!(std::mem::size_of::<Frame>() == 64);
|
|
|
|
impl Frame {
|
|
/// The index of the next instruction, for error positions.
|
|
fn pc(&self) -> usize {
|
|
if self.proto.is_null() {
|
|
return 0;
|
|
}
|
|
// SAFETY: a live Lua frame's `ip` points into its prototype's code.
|
|
unsafe { self.ip.offset_from((*self.proto).code.as_ptr()) as usize }
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn base(&self) -> usize {
|
|
self.base as usize
|
|
}
|
|
|
|
/// The stack slot of the called function, where its results go.
|
|
#[inline(always)]
|
|
fn func_idx(&self) -> usize {
|
|
self.base as usize - 1
|
|
}
|
|
}
|
|
|
|
/// Metamethod and library names, interned once.
|
|
#[derive(Clone)]
|
|
pub struct Names {
|
|
pub index: StrRef,
|
|
pub newindex: StrRef,
|
|
pub call: StrRef,
|
|
pub add: StrRef,
|
|
pub sub: StrRef,
|
|
pub mul: StrRef,
|
|
pub div: StrRef,
|
|
pub pow: StrRef,
|
|
pub unm: StrRef,
|
|
pub concat: StrRef,
|
|
pub eq: StrRef,
|
|
pub lt: StrRef,
|
|
pub le: StrRef,
|
|
pub mode: StrRef,
|
|
pub metatable: StrRef,
|
|
pub tostring: StrRef,
|
|
pub n: StrRef,
|
|
pub next: StrRef,
|
|
pub ipairs: StrRef,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct Vm {
|
|
pub heap: Heap,
|
|
/// The value stack. Crate-internal: frames address it through raw pointers, so only the VM
|
|
/// changes its length; the host reads and writes slots through `stack` and `stack_mut`.
|
|
pub(crate) stack: Vec<Value>,
|
|
pub(crate) top: usize,
|
|
frames: Vec<Frame>,
|
|
/// Open upvalues, by stack slot, ascending.
|
|
open: Vec<(usize, u32)>,
|
|
/// `concat`'s scratch buffer, kept between calls.
|
|
concat_buf: Vec<u8>,
|
|
/// Byte buffers the string library borrows for a call (`take_bufs`).
|
|
bufs: Vec<Vec<u8>>,
|
|
/// The base library's `next`: a generic `for` over it steps the table in place.
|
|
pub(crate) builtin_next: Value,
|
|
pub globals: TableRef,
|
|
pub registry: TableRef,
|
|
pub names: Names,
|
|
c_depth: u32,
|
|
/// `L->size_ci`: frames fit while `frames.len() + 1 < ci_size`. The array doubles as calls
|
|
/// nest and a doubling past [`MAXCALLS`] raises "stack overflow", so every overflow comes at
|
|
/// 4096 frames; the doubled array leaves the error's handler room to run.
|
|
ci_size: usize,
|
|
/// `ci_size - 1`: a call is made while `frames.len()` is below it, one compare on the call
|
|
/// path.
|
|
ci_limit: usize,
|
|
/// The stack below it may hold the frame of a running function whose code can read a
|
|
/// register it has not written (`Proto::clears`). A Lua frame entered below it starts with
|
|
/// every register past its arguments nil, as `luaD_precall` starts every frame, so such code
|
|
/// reads what C's would; above it no code can tell, and the clearing is skipped. Raised as
|
|
/// such a function is entered, reset at the host's outermost call.
|
|
clear_below: usize,
|
|
/// Error handlers running (`throw`): only they may use the margin past `MAX_RUST_STACK`.
|
|
in_handler: u32,
|
|
/// The Rust stack address where the outermost host-level call entered (`MAX_RUST_STACK`).
|
|
stack_base: usize,
|
|
/// The handler of each active protected call, innermost last; `None` for `pcall`.
|
|
handlers: Vec<Option<Value>>,
|
|
/// Collect at every safe point: a test switch that finds a value the roots miss.
|
|
pub gc_stress: bool,
|
|
/// Where the client's allocator keeps the stack and the call records, which
|
|
/// `luaD_reallocstack` and `luaD_reallocCI` reallocate (`0x6f5e45`, `0x6f5f1a`).
|
|
stack_block: Block,
|
|
ci_block: Block,
|
|
/// Code and upvalue lists a hot reload replaced, kept while a frame may still run them.
|
|
pub(crate) retired: Vec<(Rc<Proto>, crate::heap::Upvals)>,
|
|
/// Bumped by every `setfenv` of a Lua function, so a running frame's cached environment
|
|
/// knows to reread.
|
|
pub(crate) fenv_gen: u32,
|
|
/// Values a host holds outside the VM (a handle to a table or function), each a collector
|
|
/// root while its count is above zero: mlua's registry references.
|
|
roots: Roots,
|
|
/// The instruction-budget hook, and whether one is set (read where loops and calls pass).
|
|
hook: Option<Hook>,
|
|
/// A hook or a profile is on: the flag the dispatch loop tests.
|
|
hooked: bool,
|
|
/// The hook is running: 5.1's `luaD_hook` clears `allowhook` around the hook, so code the
|
|
/// hook runs does not count toward, or run, the hook again.
|
|
in_hook: bool,
|
|
/// The message of an abort unwinding to the host ([`Vm::abort`]).
|
|
abort: Option<Rc<[u8]>>,
|
|
/// Time charged to each chunk while profiling (`set_profiling`).
|
|
pub profile: Option<Box<Profile>>,
|
|
/// The embedding host's handle to its own state, which a host closure reads at each call
|
|
/// instead of capturing it, so a cloned VM's closures reach the clone's host. Weak, so the
|
|
/// host owning this VM makes no cycle.
|
|
pub host: Option<std::rc::Weak<dyn std::any::Any>>,
|
|
/// How the host runs its own code against this VM from inside it (`Vm::as_host`): it makes
|
|
/// the `&mut Vm` it is handed the one its API reaches, as for a callback.
|
|
pub reentry: Option<Reentry>,
|
|
/// `math.random`'s generator.
|
|
pub(crate) rand: crate::lib_math::CrtRand,
|
|
identity: crate::heap::Identity,
|
|
}
|
|
|
|
/// The host's way in: run the closure with the `&mut Vm` as the VM in effect.
|
|
pub type Reentry = Rc<dyn Fn(&mut Vm, &mut dyn FnMut())>;
|
|
|
|
/// Values a host holds outside the VM, each with a count of holders; mlua's registry
|
|
/// references. Counted through `&self`, so a holder can let go while the VM is borrowed.
|
|
#[derive(Debug, Default)]
|
|
pub struct RootTable {
|
|
/// Each root's value, holder count and serial (`ROOT_SERIAL`).
|
|
slots: std::cell::RefCell<Vec<(Value, u32, u64)>>,
|
|
free: std::cell::RefCell<Vec<u32>>,
|
|
}
|
|
|
|
/// Serials for roots, unique across every VM: a fork's copy of a root keeps its serial, and a
|
|
/// root made after the fork, in either, has one the other never had.
|
|
static ROOT_SERIAL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
|
|
|
|
impl RootTable {
|
|
fn root(&self, v: Value) -> u32 {
|
|
let serial = ROOT_SERIAL.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
|
let mut slots = self.slots.borrow_mut();
|
|
match self.free.borrow_mut().pop() {
|
|
Some(i) => {
|
|
slots[i as usize] = (v, 1, serial);
|
|
i
|
|
}
|
|
None => {
|
|
slots.push((v, 1, serial));
|
|
(slots.len() - 1) as u32
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Root `i`'s serial; 0 for an id that holds nothing.
|
|
pub fn serial(&self, i: u32) -> u64 {
|
|
self.slots
|
|
.borrow()
|
|
.get(i as usize)
|
|
.filter(|r| r.1 > 0)
|
|
.map_or(0, |r| r.2)
|
|
}
|
|
|
|
/// Root `i`'s value if it is the root with `serial`: a handle made in this VM, or in the
|
|
/// VM it was forked from before the fork.
|
|
pub fn get_as(&self, i: u32, serial: u64) -> Option<Value> {
|
|
self.slots
|
|
.borrow()
|
|
.get(i as usize)
|
|
.filter(|r| r.1 > 0 && r.2 == serial)
|
|
.map(|r| r.0)
|
|
}
|
|
|
|
/// Another holder of root `i`; an id this table never gave is ignored.
|
|
pub fn retain(&self, i: u32) {
|
|
if let Some(r) = self
|
|
.slots
|
|
.borrow_mut()
|
|
.get_mut(i as usize)
|
|
.filter(|r| r.1 > 0)
|
|
{
|
|
r.1 += 1;
|
|
}
|
|
}
|
|
|
|
/// One holder of root `i` lets go; an id that holds nothing is ignored.
|
|
pub fn unroot(&self, i: u32) {
|
|
let mut slots = self.slots.borrow_mut();
|
|
if let Some(r) = slots.get_mut(i as usize).filter(|r| r.1 > 0) {
|
|
r.1 -= 1;
|
|
if r.1 == 0 {
|
|
r.0 = Value::Nil;
|
|
self.free.borrow_mut().push(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Root `i`'s value; nil for an id that holds nothing.
|
|
pub fn get(&self, i: u32) -> Value {
|
|
self.slots
|
|
.borrow()
|
|
.get(i as usize)
|
|
.map_or(Value::Nil, |r| r.0)
|
|
}
|
|
|
|
fn live(&self) -> Vec<Value> {
|
|
let slots = self.slots.borrow();
|
|
slots.iter().filter(|r| r.1 > 0).map(|r| r.0).collect()
|
|
}
|
|
}
|
|
|
|
/// A VM's root table. A cloned VM gets its own copy, as it gets its own heap.
|
|
#[derive(Default)]
|
|
struct Roots(Rc<RootTable>);
|
|
|
|
impl Clone for Roots {
|
|
fn clone(&self) -> Self {
|
|
Roots(Rc::new(RootTable {
|
|
slots: self.0.slots.clone(),
|
|
free: self.0.free.clone(),
|
|
}))
|
|
}
|
|
}
|
|
|
|
/// The host's hook body; an error it answers is raised where the hook ran.
|
|
pub type HookFn = Rc<dyn Fn(&mut Vm) -> Result<(), LuaError>>;
|
|
|
|
/// A host hook run once for every `step` instructions Lua runs, counted as 5.1's
|
|
/// `LUA_MASKCOUNT` counts them: each instruction 5.1 dispatches, in every Lua frame (Lua a
|
|
/// library function or a metamethod calls included). It does not run at the instruction that
|
|
/// completes a step, as 5.1's does, but at the next loop back edge, call or return, once for each
|
|
/// step completed since. A non-terminating script passes those, so a budget still stops it, and
|
|
/// the dispatch loop pays nothing for the count until a hook is set.
|
|
#[derive(Clone)]
|
|
pub struct Hook {
|
|
pub step: u64,
|
|
pub f: HookFn,
|
|
counted: u64,
|
|
}
|
|
|
|
/// Wall time charged to each chunk (`Heap::sources`, by id). A switch between chunks (a call, a
|
|
/// return) and every loop back-edge closes the running segment and charges it to the chunk that
|
|
/// ran it; time in a host function goes to the chunk that called it; time outside Lua to none.
|
|
#[derive(Clone, Debug, Default)]
|
|
pub struct Profile {
|
|
pub ns: Vec<u64>,
|
|
last: Option<(std::time::Instant, u32)>,
|
|
}
|
|
|
|
impl Profile {
|
|
/// `source` runs from now: the running segment is charged to its chunk. A call or return
|
|
/// within one chunk reads no clock, so the profile costs what crossing chunks costs.
|
|
fn mark(&mut self, source: u32) {
|
|
if self.last.is_some_and(|(_, s)| s == source) {
|
|
return;
|
|
}
|
|
self.charge(std::time::Instant::now(), Some(source));
|
|
}
|
|
|
|
fn charge(&mut self, now: std::time::Instant, next: Option<u32>) {
|
|
if let Some((t, s)) = self.last {
|
|
let s = s as usize;
|
|
if self.ns.len() <= s {
|
|
self.ns.resize(s + 1, 0);
|
|
}
|
|
self.ns[s] += now.duration_since(t).as_nanos() as u64;
|
|
}
|
|
self.last = next.map(|n| (now, n));
|
|
}
|
|
|
|
/// Close the running segment: execution left Lua.
|
|
fn close(&mut self) {
|
|
self.charge(std::time::Instant::now(), None);
|
|
}
|
|
}
|
|
|
|
/// One stack level, as `lua_getinfo` describes it with `"Snl"`.
|
|
#[derive(Clone, Debug, Default)]
|
|
pub struct FrameInfo {
|
|
/// The chunk name, for a Lua function.
|
|
pub source: Option<Vec<u8>>,
|
|
/// `"Lua"`, `"main"`, `"C"` or `"tail"`.
|
|
pub what: &'static str,
|
|
pub current_line: Option<u32>,
|
|
pub line_defined: u32,
|
|
/// How the calling instruction named it: `("global", "foo")`.
|
|
pub name: Option<Vec<u8>>,
|
|
pub namewhat: &'static str,
|
|
/// The function running at the level (`lua_getinfo`'s `"f"`); nil for a tail call.
|
|
pub func: Value,
|
|
}
|
|
|
|
/// What a call produced, for the dispatch loop.
|
|
enum Called {
|
|
/// A Lua frame was pushed; run it.
|
|
Lua,
|
|
/// A native ran; its results are in place.
|
|
Done,
|
|
}
|
|
|
|
impl Default for Vm {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl Vm {
|
|
pub fn new() -> Vm {
|
|
let mut heap = Heap::default();
|
|
let mut s = |b: &[u8]| heap.intern(b);
|
|
let names = Names {
|
|
index: s(b"__index"),
|
|
newindex: s(b"__newindex"),
|
|
call: s(b"__call"),
|
|
add: s(b"__add"),
|
|
sub: s(b"__sub"),
|
|
mul: s(b"__mul"),
|
|
div: s(b"__div"),
|
|
pow: s(b"__pow"),
|
|
unm: s(b"__unm"),
|
|
concat: s(b"__concat"),
|
|
eq: s(b"__eq"),
|
|
lt: s(b"__lt"),
|
|
le: s(b"__le"),
|
|
mode: s(b"__mode"),
|
|
metatable: s(b"__metatable"),
|
|
tostring: s(b"__tostring"),
|
|
n: s(b"n"),
|
|
next: s(b"next"),
|
|
ipairs: s(b"ipairs"),
|
|
};
|
|
// `f_luaopen`: the globals by `luaH_new(L, 0, 4)` (`0x6f6e62`), the registry by
|
|
// `luaH_new(L, 4, 4)` (`0x6f6e87`).
|
|
let globals = heap.new_table(0, 4);
|
|
let registry = heap.new_table(4, 4);
|
|
let mut vm = Vm {
|
|
heap,
|
|
stack: vec![Value::Nil; BASIC_STACK_SIZE],
|
|
top: 0,
|
|
frames: Vec::new(),
|
|
open: Vec::new(),
|
|
concat_buf: Vec::new(),
|
|
bufs: Vec::new(),
|
|
builtin_next: Value::Nil,
|
|
globals,
|
|
registry,
|
|
names,
|
|
c_depth: 0,
|
|
ci_size: BASIC_CI_SIZE,
|
|
ci_limit: BASIC_CI_SIZE - 1,
|
|
clear_below: 0,
|
|
stack_base: 0,
|
|
in_handler: 0,
|
|
handlers: Vec::new(),
|
|
gc_stress: false,
|
|
stack_block: Block::None,
|
|
ci_block: Block::None,
|
|
retired: Vec::new(),
|
|
fenv_gen: 0,
|
|
roots: Roots::default(),
|
|
hook: None,
|
|
hooked: false,
|
|
in_hook: false,
|
|
abort: None,
|
|
profile: None,
|
|
host: None,
|
|
reentry: None,
|
|
rand: crate::lib_math::CrtRand::from_clock(),
|
|
identity: Default::default(),
|
|
};
|
|
vm.heap.charge(vm.state_bytes());
|
|
// `stack_init` (`0x6f6bb8`, `0x6f6be4`); the two records are the system's (`mallocstate`
|
|
// and `f_luaopen` allocate them with no state, `0x6f6b8d`, `0x6f6d88`).
|
|
let mut pools = vm.heap.pools();
|
|
let (stack_block, ci_block) = (
|
|
pools.alloc(STACK_SLOT_BYTES * BASIC_STACK_SIZE),
|
|
pools.alloc(CI_BYTES * BASIC_CI_SIZE),
|
|
);
|
|
drop(pools);
|
|
(vm.stack_block, vm.ci_block) = (stack_block, ci_block);
|
|
// `f_luaopen` ends with four times the count (`0x6f6ec8`).
|
|
let count = vm.heap.bytes();
|
|
vm.heap.set_threshold(4 * count);
|
|
vm
|
|
}
|
|
|
|
/// 5.0's `lua_setgcthreshold` (`0x6f4400`): the threshold `kb` kilobytes, past `0x3fffff`
|
|
/// (compared unsigned) the most there is, then a collection if the count has reached it.
|
|
pub fn set_gc_threshold(&mut self, kb: u32) {
|
|
let t = if kb > 0x3f_ffff {
|
|
u32::MAX as usize
|
|
} else {
|
|
(kb as usize) << 10
|
|
};
|
|
self.heap.set_threshold(t);
|
|
self.check_gc();
|
|
}
|
|
|
|
// ── strings and values ───────────────────────────────────────────────────────────────────
|
|
|
|
/// `N` empty byte buffers, reused from earlier calls where there are some.
|
|
pub(crate) fn take_bufs<const N: usize>(&mut self) -> [Vec<u8>; N] {
|
|
std::array::from_fn(|_| self.bufs.pop().unwrap_or_default())
|
|
}
|
|
|
|
/// Buffers back to the pool; a very large one is dropped, so one huge string does not stay.
|
|
pub(crate) fn give_bufs<const N: usize>(&mut self, bufs: [Vec<u8>; N]) {
|
|
for mut b in bufs {
|
|
if self.bufs.len() < 16 && b.capacity() <= 64 * 1024 {
|
|
b.clear();
|
|
self.bufs.push(b);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The value stack, up to its allocated length.
|
|
pub fn stack(&self) -> &[Value] {
|
|
&self.stack
|
|
}
|
|
|
|
/// The value stack's slots, to write values into (not to resize).
|
|
pub fn stack_mut(&mut self) -> &mut [Value] {
|
|
&mut self.stack
|
|
}
|
|
|
|
/// The first free stack slot.
|
|
pub fn top(&self) -> usize {
|
|
self.top
|
|
}
|
|
|
|
pub fn str(&mut self, s: &[u8]) -> Value {
|
|
self.heap.str_value(s)
|
|
}
|
|
|
|
pub fn bytes(&self, v: Value) -> Option<&[u8]> {
|
|
match v.unpack() {
|
|
V::Str(s) => Some(self.heap.str(s)),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// `luaV_tonumber`: a number, or a string `str2d` reads.
|
|
pub fn tonumber(&self, v: Value) -> Option<f64> {
|
|
match v.unpack() {
|
|
V::Num(n) => Some(n),
|
|
V::Str(s) => str2d(self.heap.str(s)),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// `luaV_tostring`'s conversion: a string, or a number as `%.14g`.
|
|
pub fn tostr(&mut self, v: Value) -> Option<StrRef> {
|
|
match v.unpack() {
|
|
V::Str(s) => Some(s),
|
|
V::Num(n) => Some(crate::number::with_g14(n, |b| self.heap.intern(b))),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub fn get_global(&mut self, name: &str) -> Value {
|
|
let k = self.str(name.as_bytes());
|
|
self.heap.table(self.globals).get(k)
|
|
}
|
|
|
|
pub fn set_global(&mut self, name: &str, v: Value) {
|
|
let k = self.str(name.as_bytes());
|
|
let g = self.globals;
|
|
self.heap.table_mut(g).set(k, v).unwrap();
|
|
}
|
|
|
|
pub fn new_native(&mut self, name: &'static str, f: NativeFn) -> Value {
|
|
let env = self.globals;
|
|
Value::Func(self.heap.new_func(FuncObj::Native {
|
|
f,
|
|
host: None,
|
|
name,
|
|
upvals: Box::new([]),
|
|
env,
|
|
}))
|
|
}
|
|
|
|
/// A native function over a host closure.
|
|
pub fn new_host(&mut self, name: &'static str, f: crate::heap::HostFn) -> Value {
|
|
fn unreachable_native(_: &mut Vm, _: usize) -> Result<usize, LuaError> {
|
|
unreachable!("a host function calls its closure")
|
|
}
|
|
let env = self.globals;
|
|
Value::Func(self.heap.new_func(FuncObj::Native {
|
|
f: unreachable_native,
|
|
host: Some(f),
|
|
name,
|
|
upvals: Box::new([]),
|
|
env,
|
|
}))
|
|
}
|
|
|
|
pub fn register(&mut self, name: &'static str, f: NativeFn) {
|
|
let v = self.new_native(name, f);
|
|
self.set_global(name, v);
|
|
}
|
|
|
|
pub fn new_user(&mut self, tag: u64, meta: Option<TableRef>) -> Value {
|
|
Value::User(self.heap.new_user(UserObj { meta, tag }))
|
|
}
|
|
|
|
// ── loading ──────────────────────────────────────────────────────────────────────────────
|
|
|
|
/// Compile a chunk into a function over the globals.
|
|
pub fn load(&mut self, src: &[u8], chunkname: &[u8]) -> Result<Value, crate::SyntaxError> {
|
|
let proto = crate::compile::compile(src, chunkname, &mut self.heap)?;
|
|
self.heap.charge_proto(&proto);
|
|
// A constant the compiler could not make: 5.1's `LUA_ERRMEM`.
|
|
if self.heap.oom {
|
|
self.heap.oom = false;
|
|
return Err(crate::SyntaxError(b"not enough memory".to_vec()));
|
|
}
|
|
let env = self.globals;
|
|
Ok(Value::Func(self.heap.new_func(FuncObj::Lua(LuaFunc::new(
|
|
proto,
|
|
crate::heap::Upvals::new(Vec::new()),
|
|
env,
|
|
)))))
|
|
}
|
|
|
|
/// Load and run a chunk protected, answering its results or its error value.
|
|
pub fn exec(&mut self, src: &[u8], chunkname: &[u8]) -> Result<Vec<Value>, Value> {
|
|
let f = match self.load(src, chunkname) {
|
|
Ok(f) => f,
|
|
Err(e) => return Err(self.str(&e.0)),
|
|
};
|
|
self.pcall_value(f, &[], -1)
|
|
}
|
|
|
|
/// Run `f`, which may raise (a metamethod, a host call), protected: on an error the stack,
|
|
/// frames and open upvalues above where it started unwind, as a `pcall` unwinds them, and
|
|
/// the error value is the answer.
|
|
pub fn protected<T>(
|
|
&mut self,
|
|
f: impl FnOnce(&mut Vm) -> Result<T, LuaError>,
|
|
) -> Result<T, Value> {
|
|
let (frames, c_depth, top, handlers) = (
|
|
self.frames.len(),
|
|
self.c_depth,
|
|
self.top,
|
|
self.handlers.len(),
|
|
);
|
|
// A protected call of its own, as mlua's `lua_pcall` with no handler: an error inside
|
|
// runs no enclosing `xpcall`'s handler.
|
|
self.handlers.push(None);
|
|
// A panic that passes leaves the state as at this call (`pcall_at`).
|
|
let r = self.unwind_guard(f);
|
|
self.handlers.truncate(handlers);
|
|
match r {
|
|
Ok(v) => Ok(v),
|
|
Err(e) => {
|
|
self.close_upvals(top);
|
|
self.frames.truncate(frames);
|
|
self.c_depth = c_depth;
|
|
self.top = top;
|
|
self.handlers.truncate(handlers);
|
|
self.restore_stack_limit();
|
|
Err(e.value)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Call `f` protected: its results, or its error value with the state unwound.
|
|
pub fn pcall_value(
|
|
&mut self,
|
|
f: Value,
|
|
args: &[Value],
|
|
nres: i32,
|
|
) -> Result<Vec<Value>, Value> {
|
|
self.pcall_value_with(f, args, nres, None)
|
|
}
|
|
|
|
/// [`Vm::pcall_value`] under an error function, `lua_pcall`'s `errfunc`: `handler` runs at
|
|
/// the raise, before the stack unwinds ([`Vm::throw`]), and its answer is the error.
|
|
pub fn pcall_value_with(
|
|
&mut self,
|
|
f: Value,
|
|
args: &[Value],
|
|
nres: i32,
|
|
handler: Option<Value>,
|
|
) -> Result<Vec<Value>, Value> {
|
|
let func_idx = self.top;
|
|
self.ensure(func_idx + args.len() + 1);
|
|
self.stack[func_idx] = f;
|
|
self.stack[func_idx + 1..func_idx + 1 + args.len()].copy_from_slice(args);
|
|
self.top = func_idx + 1 + args.len();
|
|
match self.pcall_at(func_idx, args.len(), nres, handler) {
|
|
Ok(()) => {
|
|
let out = self.stack[func_idx..self.top].to_vec();
|
|
self.top = func_idx;
|
|
Ok(out)
|
|
}
|
|
Err(e) => Err(e.value),
|
|
}
|
|
}
|
|
|
|
/// Call `f`, protected and with no arguments, while each global in `globals` holds the value
|
|
/// paired with it, read and written through the globals table's metamethods as
|
|
/// `lua_getglobal` and `lua_setglobal` do; each one's earlier value is put back after the
|
|
/// call, after an error too. The earlier values wait on the stack, where a collection inside
|
|
/// the call keeps them. It is the 1.12 script handler's convention (`this`, `event`,
|
|
/// `arg1..`) in one step, `handler` the call's error function, which runs at the raise with
|
|
/// the globals still set. Answers the call's error value, or the first one a read or write
|
|
/// of the globals raised. `f`, `handler` and the values must be rooted by the caller, as a
|
|
/// host's handles root them; the names wait on the stack with the earlier values.
|
|
pub fn call_with_globals(
|
|
&mut self,
|
|
f: Value,
|
|
globals: &[(StrRef, Value)],
|
|
handler: Option<Value>,
|
|
) -> Result<(), Value> {
|
|
let base = self.top;
|
|
let n = globals.len();
|
|
let g = Value::Table(self.globals);
|
|
for &(k, _) in globals {
|
|
self.push(Value::Str(k));
|
|
}
|
|
let read = self.protected(|vm| {
|
|
for &(k, _) in globals {
|
|
let old = vm.index(g, Value::Str(k), None)?;
|
|
vm.push(old);
|
|
}
|
|
Ok(())
|
|
});
|
|
if let Err(e) = read {
|
|
self.top = base;
|
|
return Err(e);
|
|
}
|
|
let set = self.protected(|vm| {
|
|
for &(k, v) in globals {
|
|
vm.setindex(g, Value::Str(k), v, None)?;
|
|
}
|
|
Ok(())
|
|
});
|
|
let called = set.and_then(|()| {
|
|
let func_idx = self.top;
|
|
self.push(f);
|
|
match self.pcall_at(func_idx, 0, 0, handler) {
|
|
Ok(()) => Ok(()),
|
|
Err(e) => Err(e.value),
|
|
}
|
|
});
|
|
let mut restored = Ok(());
|
|
for (i, &(k, _)) in globals.iter().enumerate() {
|
|
let old = self.stack[base + n + i];
|
|
if self.abort.is_some() {
|
|
// An abort raises again in any Lua `__newindex`: put the value back raw.
|
|
let _ = self.heap.table_mut(self.globals).set(Value::Str(k), old);
|
|
continue;
|
|
}
|
|
let r = self.protected(|vm| vm.setindex(g, Value::Str(k), old, None));
|
|
restored = restored.and(r);
|
|
}
|
|
self.top = base;
|
|
called.and(restored)
|
|
}
|
|
|
|
/// A value as a Rust string, for hosts and tests: strings and numbers as `tostring` writes
|
|
/// them, other values by type.
|
|
pub fn display(&mut self, v: Value) -> String {
|
|
match self.tostr(v) {
|
|
Some(s) => String::from_utf8_lossy(self.heap.str(s)).into_owned(),
|
|
None => match v.unpack() {
|
|
V::Nil => "nil".into(),
|
|
V::Bool(b) => b.to_string(),
|
|
_ => v.type_name().into(),
|
|
},
|
|
}
|
|
}
|
|
|
|
// ── errors ───────────────────────────────────────────────────────────────────────────────
|
|
|
|
/// Raise `v`: the innermost protected call's handler runs here, before the stack unwinds, as
|
|
/// 5.0's `luaD_throw` runs `errfunc` at the raise.
|
|
pub fn throw(&mut self, v: Value) -> LuaError {
|
|
if self.abort.is_some() {
|
|
return self.abort_error();
|
|
}
|
|
let Some(Some(h)) = self.handlers.last().copied() else {
|
|
return LuaError { value: v };
|
|
};
|
|
// 5.1's `luaG_errormsg`: a handler that is not a function is LUA_ERRERR, uncalled.
|
|
if !matches!(h.unpack(), V::Func(_)) {
|
|
return self.errerr();
|
|
}
|
|
// The handler stays in place while it runs, as `errfunc` does: an error inside it runs
|
|
// it again, for that error, until the host-call limit throws LUA_ERRERR past every
|
|
// handler. Its answer, or that error, is what the protected call gets.
|
|
self.in_handler += 1;
|
|
let r = self.call_value(h, &[v], 1);
|
|
self.in_handler -= 1;
|
|
match r {
|
|
Ok(vals) => LuaError {
|
|
value: vals.first().copied().unwrap_or(Value::Nil),
|
|
},
|
|
Err(e) => e,
|
|
}
|
|
}
|
|
|
|
/// `luaG_runerror`: the message with the current Lua function's position.
|
|
#[cold]
|
|
#[inline(never)]
|
|
pub fn rt_error(&mut self, msg: &str) -> LuaError {
|
|
self.rt_error_bytes(msg.as_bytes())
|
|
}
|
|
|
|
/// [`Vm::rt_error`] of bytes, as the client's C strings are: a name or text that is not
|
|
/// UTF-8 goes through as written.
|
|
#[cold]
|
|
#[inline(never)]
|
|
pub fn rt_error_bytes(&mut self, msg: &[u8]) -> LuaError {
|
|
let mut out = self.where_(0);
|
|
out.extend_from_slice(msg);
|
|
let v = self.str(&out);
|
|
self.throw(v)
|
|
}
|
|
|
|
/// `luaL_error`: the message with the position of the Lua function `level` frames up
|
|
/// (`luaL_where(L, 1)` from a native names its caller).
|
|
pub fn error_at(&mut self, level: usize, msg: &str) -> LuaError {
|
|
self.error_at_bytes(level, msg.as_bytes())
|
|
}
|
|
|
|
/// [`Vm::error_at`] of bytes, which need not be UTF-8.
|
|
pub fn error_at_bytes(&mut self, level: usize, msg: &[u8]) -> LuaError {
|
|
let mut out = self.where_(level);
|
|
out.extend_from_slice(msg);
|
|
let v = self.str(&out);
|
|
self.throw(v)
|
|
}
|
|
|
|
/// `luaL_where`: `chunk:line: ` of the frame `level` up, if it is a Lua function.
|
|
pub fn where_(&self, level: usize) -> Vec<u8> {
|
|
let Some(fi) = self.frame_at_level(level) else {
|
|
return Vec::new();
|
|
};
|
|
let f = &self.frames[fi];
|
|
// SAFETY: a live frame's code is alive (see `Frame::proto`).
|
|
match unsafe { f.proto.as_ref() } {
|
|
Some(p) => {
|
|
let pc = f.pc().saturating_sub(1);
|
|
let line = p.lines.get(pc).copied().unwrap_or(0);
|
|
if line == 0 {
|
|
return Vec::new();
|
|
}
|
|
let mut out = crate::chunkid_n(&p.source, 60);
|
|
out.extend_from_slice(format!(":{line}: ").as_bytes());
|
|
out
|
|
}
|
|
None => Vec::new(),
|
|
}
|
|
}
|
|
|
|
/// The function `level` frames up: `None` past the bottom, `Some(None)` for a level a tail
|
|
/// call replaced.
|
|
pub fn function_at_level(&self, level: usize) -> Option<Option<FuncRef>> {
|
|
let mut lv = level as i64;
|
|
let mut i = self.frames.len();
|
|
while i > 0 {
|
|
i -= 1;
|
|
if lv == 0 {
|
|
return Some(Some(self.frames[i].func));
|
|
}
|
|
lv -= 1;
|
|
if !self.frames[i].proto.is_null() {
|
|
lv -= self.frames[i].tailcalls as i64;
|
|
}
|
|
if lv < 0 {
|
|
return Some(None);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// A number naming this VM among every one made, a clone included.
|
|
pub fn vm_id(&self) -> u64 {
|
|
self.identity.0
|
|
}
|
|
|
|
/// No Lua or native runs on this VM: no call is in progress.
|
|
pub fn frames_empty(&self) -> bool {
|
|
self.frames.is_empty()
|
|
}
|
|
|
|
/// The lowest Rust stack address a native's own recursion may reach: what the nested calls
|
|
/// under the outermost host-level call may use (`MAX_RUST_STACK`), below where it entered.
|
|
/// The stack grows down on every platform benilla builds for.
|
|
pub(crate) fn rust_stack_limit(&self) -> usize {
|
|
self.stack_base.saturating_sub(MAX_RUST_STACK)
|
|
}
|
|
|
|
pub(crate) fn native_base(&self) -> usize {
|
|
self.frames.last().unwrap().base()
|
|
}
|
|
|
|
pub(crate) fn running(&self) -> FuncRef {
|
|
self.frames.last().unwrap().func
|
|
}
|
|
|
|
/// 5.0's `lua_getstack`: the frame `level` up, tail calls counting as levels.
|
|
fn frame_at_level(&self, level: usize) -> Option<usize> {
|
|
let mut level = level as i64;
|
|
let mut i = self.frames.len();
|
|
while i > 0 {
|
|
i -= 1;
|
|
if level == 0 {
|
|
return Some(i);
|
|
}
|
|
level -= 1;
|
|
if !self.frames[i].proto.is_null() {
|
|
level -= self.frames[i].tailcalls as i64;
|
|
}
|
|
if level < 0 {
|
|
return None;
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// 5.0's `getobjname` for register `reg` of the current Lua frame: `("local", name)` and so
|
|
/// on, from the locals and a symbolic run of the code up to the current instruction.
|
|
fn objname(&self, fi: usize, reg: usize) -> Option<(&'static str, Vec<u8>)> {
|
|
let f = &self.frames[fi];
|
|
// SAFETY: a live frame's code is alive (see `Frame::proto`).
|
|
let p = unsafe { f.proto.as_ref() }?;
|
|
let pc = f.pc().saturating_sub(1);
|
|
objname_in(p, &self.heap, pc, reg)
|
|
}
|
|
|
|
/// `luaG_typeerror`: `attempt to <op> <kind> `<name>' (a <type> value)`.
|
|
#[cold]
|
|
#[inline(never)]
|
|
fn type_error(&mut self, v: Value, reg: Option<usize>, op: &str) -> LuaError {
|
|
let t = v.type_name();
|
|
// A register names a value only in a running Lua frame; the host indexes with none.
|
|
let named = reg.and_then(|r| self.objname(self.frames.len().checked_sub(1)?, r));
|
|
let msg = match named {
|
|
Some((kind, name)) => [
|
|
format!("attempt to {op} {kind} `").as_bytes(),
|
|
&name,
|
|
format!("' (a {t} value)").as_bytes(),
|
|
]
|
|
.concat(),
|
|
None => format!("attempt to {op} a {t} value").into_bytes(),
|
|
};
|
|
self.rt_error_bytes(&msg)
|
|
}
|
|
|
|
#[cold]
|
|
#[inline(never)]
|
|
fn order_error(&mut self, a: Value, b: Value) -> LuaError {
|
|
let (t1, t2) = (a.type_name(), b.type_name());
|
|
// 5.0 compares the third character of the two type names.
|
|
let msg = if t1.as_bytes()[2] == t2.as_bytes()[2] {
|
|
format!("attempt to compare two {t1} values")
|
|
} else {
|
|
format!("attempt to compare {t1} with {t2}")
|
|
};
|
|
self.rt_error(&msg)
|
|
}
|
|
|
|
// ── stack ────────────────────────────────────────────────────────────────────────────────
|
|
|
|
#[inline(always)]
|
|
pub fn ensure(&mut self, n: usize) {
|
|
if self.stack.len() < n {
|
|
self.grow(n);
|
|
}
|
|
}
|
|
|
|
/// A need past the stack: grown by 5.0's rule ([`Vm::growstack`]) for what the need asks
|
|
/// above `top`, until it fits. Deviation: these are benilla's own needs, past what the client's
|
|
/// checks ([`Vm::checkstack`]) reserve: a frame's registers above `c_maxstack`
|
|
/// (`Proto::maxstack`), and the host's and natives' pushes, which ask for no margin of
|
|
/// `EXTRA_STACK` and one more. Where they pass what the client reserved, the stack grows
|
|
/// where the client's would not.
|
|
#[cold]
|
|
#[inline(never)]
|
|
fn grow(&mut self, n: usize) {
|
|
while self.stack.len() < n {
|
|
self.growstack(n.saturating_sub(self.top));
|
|
}
|
|
}
|
|
|
|
/// 5.0's `luaD_checkstack`: room for `n` slots above `top`, which leaves `EXTRA_STACK` and
|
|
/// one more past them (`stack_last - top <= n`, `0x6f60c1`, `0x6f6142`).
|
|
#[inline(always)]
|
|
fn checkstack(&mut self, top: usize, n: usize) {
|
|
if top + n + EXTRA_STACK + 1 >= self.stack.len() {
|
|
self.growstack(n);
|
|
}
|
|
}
|
|
|
|
/// 5.0's `luaD_growstack` (`0x6f5f60`): twice the size for a need it holds, else the size and
|
|
/// the need and `EXTRA_STACK`; `luaD_reallocstack` reallocates it (`0x6f5e45`).
|
|
#[cold]
|
|
#[inline(never)]
|
|
fn growstack(&mut self, n: usize) {
|
|
let size = self.stack.len();
|
|
let new = if n <= size {
|
|
2 * size
|
|
} else {
|
|
size + n + EXTRA_STACK
|
|
};
|
|
self.realloc_stack(new);
|
|
}
|
|
|
|
fn realloc_stack(&mut self, new: usize) {
|
|
let old = self.stack.len();
|
|
self.heap
|
|
.recharge(STACK_SLOT_BYTES * old, STACK_SLOT_BYTES * new);
|
|
let b = self
|
|
.heap
|
|
.pools()
|
|
.realloc(self.stack_block, STACK_SLOT_BYTES * new);
|
|
self.stack_block = b;
|
|
self.stack.resize(new, Value::Nil);
|
|
self.stack.shrink_to_fit();
|
|
}
|
|
|
|
#[inline]
|
|
pub fn push(&mut self, v: Value) {
|
|
self.ensure(self.top + 1);
|
|
self.stack[self.top] = v;
|
|
self.top += 1;
|
|
}
|
|
|
|
// ── upvalues ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
/// `luaF_findupval`: the open upvalue for stack slot `idx`, made if absent.
|
|
#[inline(never)]
|
|
fn find_upval(&mut self, idx: usize) -> u32 {
|
|
match self.open.binary_search_by_key(&idx, |&(i, _)| i) {
|
|
Ok(p) => self.open[p].1,
|
|
Err(p) => {
|
|
let u = self.heap.new_upval(UpvalObj::Open(idx as u32));
|
|
self.open.insert(p, (idx, u));
|
|
u
|
|
}
|
|
}
|
|
}
|
|
|
|
/// `luaF_close`: every open upvalue at or above `level` takes its value.
|
|
#[inline(never)]
|
|
fn close_upvals(&mut self, level: usize) {
|
|
while let Some(&(idx, u)) = self.open.last() {
|
|
if idx < level {
|
|
break;
|
|
}
|
|
*self.heap.upval_mut(u) = UpvalObj::Closed(self.stack[idx]);
|
|
self.open.pop();
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
fn upval_set(&mut self, u: u32, v: Value) {
|
|
match self.heap.upval(u) {
|
|
UpvalObj::Open(i) => self.stack[i as usize] = v,
|
|
UpvalObj::Closed(_) => *self.heap.upval_mut(u) = UpvalObj::Closed(v),
|
|
}
|
|
}
|
|
|
|
// ── metamethods ──────────────────────────────────────────────────────────────────────────
|
|
|
|
/// `luaT_gettmbyobj`: only tables and userdata carry metatables in 1.12.
|
|
fn tm(&self, v: Value, name: StrRef) -> Value {
|
|
match self.heap.metatable(v) {
|
|
Some(m) => self.heap.table(m).get(Value::Str(name)),
|
|
None => Value::Nil,
|
|
}
|
|
}
|
|
|
|
/// `callTMres` (`0x6f7dd0`): `f(a, b)`'s first result, called on the stack's top and read
|
|
/// from where it lands, with nothing allocated.
|
|
fn call_tm_res(&mut self, f: Value, a: Value, b: Value) -> Result<Value, LuaError> {
|
|
let func_idx = self.top;
|
|
self.ensure(func_idx + 4);
|
|
self.stack[func_idx] = f;
|
|
self.stack[func_idx + 1] = a;
|
|
self.stack[func_idx + 2] = b;
|
|
self.top = func_idx + 3;
|
|
self.call_at(func_idx, 2, 1)?;
|
|
let r = self.stack[func_idx];
|
|
self.top = func_idx;
|
|
Ok(r)
|
|
}
|
|
|
|
/// `call_binTM`: the first operand's metamethod, else the second's, called whatever it is
|
|
/// (a table with `__call` runs; a number raises "attempt to call").
|
|
#[cold]
|
|
#[inline(never)]
|
|
fn bin_tm(&mut self, a: Value, b: Value, name: StrRef) -> Result<Option<Value>, LuaError> {
|
|
let mut tm = self.tm(a, name);
|
|
if tm.is_nil() {
|
|
tm = self.tm(b, name);
|
|
}
|
|
if tm.is_nil() {
|
|
return Ok(None);
|
|
}
|
|
Ok(Some(self.call_tm_res(tm, a, b)?))
|
|
}
|
|
|
|
/// `luaV_gettable`'s common miss, without calls: `t[key]` was nil and `t` has a metatable;
|
|
/// follow `__index` while it is a table. `None` when a function, a non-table or a long
|
|
/// chain needs the full path.
|
|
#[inline(never)]
|
|
fn index_meta_fast(&self, mut tr: TableRef, key: &Value) -> Option<Value> {
|
|
for _ in 0..4 {
|
|
let Some(m) = self.heap.table(tr).meta else {
|
|
return Some(Value::Nil);
|
|
};
|
|
match (self.heap.table(m).get_ref(&Value::Str(self.names.index))).unpack() {
|
|
V::Nil => return Some(Value::Nil),
|
|
V::Table(next) => {
|
|
let v = self.heap.table(next).get_ref(key);
|
|
if !v.is_nil() {
|
|
return Some(v);
|
|
}
|
|
tr = next;
|
|
}
|
|
_ => return None,
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// `luaV_gettable` as the API enters it (`lua_gettable`), for the host and the natives. `reg`
|
|
/// names the indexed value for an error.
|
|
#[cold]
|
|
#[inline(never)]
|
|
pub fn index(&mut self, t: Value, key: Value, reg: Option<usize>) -> Result<Value, LuaError> {
|
|
self.index_chain(t, key, reg, GETTABLE_TABLES_API)
|
|
}
|
|
|
|
/// `luaV_gettable` as an instruction enters it: `GETGLOBAL`, `GETTABLE` and `SELF`.
|
|
#[cold]
|
|
#[inline(never)]
|
|
fn index_op(&mut self, t: Value, key: Value, reg: Option<usize>) -> Result<Value, LuaError> {
|
|
self.index_chain(t, key, reg, GETTABLE_TABLES_OP)
|
|
}
|
|
|
|
fn index_chain(
|
|
&mut self,
|
|
t: Value,
|
|
key: Value,
|
|
reg: Option<usize>,
|
|
tables: u32,
|
|
) -> Result<Value, LuaError> {
|
|
let mut t = t;
|
|
let mut reg = reg;
|
|
for _ in 0..tables {
|
|
let tm = match t.unpack() {
|
|
V::Table(tr) => {
|
|
let tab = self.heap.table(tr);
|
|
let v = tab.get(key);
|
|
if !v.is_nil() {
|
|
return Ok(v);
|
|
}
|
|
let Some(m) = tab.meta else {
|
|
return Ok(Value::Nil);
|
|
};
|
|
let tm = self.heap.table(m).get(Value::Str(self.names.index));
|
|
if tm.is_nil() {
|
|
return Ok(Value::Nil);
|
|
}
|
|
tm
|
|
}
|
|
_ => {
|
|
let tm = self.tm(t, self.names.index);
|
|
if tm.is_nil() {
|
|
return Err(self.type_error(t, reg, "index"));
|
|
}
|
|
tm
|
|
}
|
|
};
|
|
if let V::Func(_) = tm.unpack() {
|
|
return self.call_tm_res(tm, t, key);
|
|
}
|
|
t = tm;
|
|
reg = None;
|
|
}
|
|
Err(self.rt_error("loop in gettable"))
|
|
}
|
|
|
|
/// `luaV_settable`: 5.0 makes the key in a table before it consults `__newindex`.
|
|
#[cold]
|
|
#[inline(never)]
|
|
pub fn setindex(
|
|
&mut self,
|
|
t: Value,
|
|
key: Value,
|
|
val: Value,
|
|
reg: Option<usize>,
|
|
) -> Result<(), LuaError> {
|
|
let mut t = t;
|
|
let mut reg = reg;
|
|
for _ in 0..SETTABLE_TABLES {
|
|
let tm = match t.unpack() {
|
|
V::Table(tr) => {
|
|
let slot = match self.heap.table_mut(tr).slot_for(key) {
|
|
Ok(s) => s,
|
|
Err(e) => return Err(self.rt_error(e.message())),
|
|
};
|
|
let tab = self.heap.table(tr);
|
|
let tm = if tab.slot(slot).is_nil() {
|
|
match tab.meta {
|
|
Some(m) => self.heap.table(m).get(Value::Str(self.names.newindex)),
|
|
None => Value::Nil,
|
|
}
|
|
} else {
|
|
Value::Nil
|
|
};
|
|
if tm.is_nil() {
|
|
*self.heap.table_mut(tr).slot_mut(slot) = val;
|
|
return Ok(());
|
|
}
|
|
tm
|
|
}
|
|
_ => {
|
|
let tm = self.tm(t, self.names.newindex);
|
|
if tm.is_nil() {
|
|
return Err(self.type_error(t, reg, "index"));
|
|
}
|
|
tm
|
|
}
|
|
};
|
|
if let V::Func(_) = tm.unpack() {
|
|
self.call_value(tm, &[t, key, val], 0)?;
|
|
return Ok(());
|
|
}
|
|
t = tm;
|
|
reg = None;
|
|
}
|
|
Err(self.rt_error("loop in settable"))
|
|
}
|
|
|
|
/// The client's `Arith` (`0x6f9a80`). Numbers (and numeric strings) compute, `^` through the
|
|
/// global `__pow`; anything else tries the operands' metamethods, `__pow` for `^`.
|
|
#[cold]
|
|
#[inline(never)]
|
|
fn arith(
|
|
&mut self,
|
|
op: Arith,
|
|
a: Value,
|
|
b: Value,
|
|
regs: (Option<usize>, Option<usize>),
|
|
) -> Result<Value, LuaError> {
|
|
if let (Some(x), Some(y)) = (self.tonumber(a), self.tonumber(b)) {
|
|
return Ok(Value::Num(match op {
|
|
Arith::Add => crate::number::x87_op(x + y, x, y),
|
|
Arith::Sub => crate::number::x87_op(x - y, x, y),
|
|
Arith::Mul => crate::number::x87_op(x * y, x, y),
|
|
Arith::Div => crate::number::x87_op(x / y, x, y),
|
|
Arith::Pow => return self.pow_global(x, y),
|
|
}));
|
|
}
|
|
let name = match op {
|
|
Arith::Add => self.names.add,
|
|
Arith::Sub => self.names.sub,
|
|
Arith::Mul => self.names.mul,
|
|
Arith::Div => self.names.div,
|
|
Arith::Pow => self.names.pow,
|
|
};
|
|
match self.bin_tm(a, b, name)? {
|
|
Some(v) => Ok(v),
|
|
None => {
|
|
// `luaG_aritherror`: blame the first operand unless it converts.
|
|
let (bad, reg) = if self.tonumber(a).is_none() {
|
|
(a, regs.0)
|
|
} else {
|
|
(b, regs.1)
|
|
};
|
|
Err(self.type_error(bad, reg, "perform arithmetic on"))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The client's `OP_FORLOOP` checks, in its order, of the loop at `stack[ra..ra + 3]`: the
|
|
/// counter must be a number, not coerced (`0x6f9262`, `` `for' initial value must be a
|
|
/// number ``), and the limit and step are made numbers where they stand (`0x6f928d`,
|
|
/// `0x6f92b3`) or the loop raises.
|
|
#[cold]
|
|
#[inline(never)]
|
|
fn for_controls(&mut self, ra: usize) -> Result<(f64, f64, f64), LuaError> {
|
|
let Some(idx) = self.stack[ra].as_num() else {
|
|
return Err(self.rt_error("`for' initial value must be a number"));
|
|
};
|
|
let mut controls = [0.0; 2];
|
|
for (i, what) in ["limit", "step"].into_iter().enumerate() {
|
|
let Some(n) = self.tonumber(self.stack[ra + 1 + i]) else {
|
|
return Err(self.rt_error(&format!("`for' {what} must be a number")));
|
|
};
|
|
self.stack[ra + 1 + i] = Value::Num(n);
|
|
controls[i] = n;
|
|
}
|
|
Ok((idx, controls[0], controls[1]))
|
|
}
|
|
|
|
/// `x ^ y` on numbers, as the client's `Arith` computes it: the global `__pow`, read raw from
|
|
/// the globals (`0x6f9b5e`), must be a function (`0x6f9b72`) and is called with the two
|
|
/// numbers (`0x6f9b8c`); its first result is the answer.
|
|
fn pow_global(&mut self, x: f64, y: f64) -> Result<Value, LuaError> {
|
|
let f = self
|
|
.heap
|
|
.table(self.globals)
|
|
.get_str(Value::Str(self.names.pow));
|
|
if !matches!(f.unpack(), V::Func(_)) {
|
|
return Err(self.rt_error("`__pow' (`^' operator) is not a function"));
|
|
}
|
|
self.call_tm_res(f, Value::Num(x), Value::Num(y))
|
|
}
|
|
|
|
/// `luaV_equalObj`: tables and userdata consult a shared `__eq`.
|
|
#[cold]
|
|
#[inline(never)]
|
|
pub fn equals(&mut self, a: Value, b: Value) -> Result<bool, LuaError> {
|
|
match (a.unpack(), b.unpack()) {
|
|
(V::Table(x), V::Table(y)) if x != y => {}
|
|
(V::User(x), V::User(y)) if x != y => {}
|
|
_ => return Ok(a.raw_eq(b)),
|
|
}
|
|
let tm = self.comp_tm(a, b, self.names.eq);
|
|
match tm {
|
|
Some(f) => Ok(self.call_tm_res(f, a, b)?.truthy()),
|
|
None => Ok(false),
|
|
}
|
|
}
|
|
|
|
/// `get_compTM`: both operands' metatables must give the same metamethod.
|
|
fn comp_tm(&self, a: Value, b: Value, name: StrRef) -> Option<Value> {
|
|
let m1 = self.heap.metatable(a)?;
|
|
let tm1 = self.heap.table(m1).get(Value::Str(name));
|
|
if tm1.is_nil() {
|
|
return None;
|
|
}
|
|
let m2 = self.heap.metatable(b)?;
|
|
if m1 == m2 {
|
|
return Some(tm1);
|
|
}
|
|
let tm2 = self.heap.table(m2).get(Value::Str(name));
|
|
if tm1.raw_eq(tm2) {
|
|
Some(tm1)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// `call_orderTM`: `Some(result)`, or `None` with no shared metamethod.
|
|
fn order_tm(&mut self, a: Value, b: Value, name: StrRef) -> Result<Option<bool>, LuaError> {
|
|
let tm1 = self.tm(a, name);
|
|
if tm1.is_nil() {
|
|
return Ok(None);
|
|
}
|
|
let tm2 = self.tm(b, name);
|
|
if !tm1.raw_eq(tm2) {
|
|
return Ok(None);
|
|
}
|
|
Ok(Some(self.call_tm_res(tm1, a, b)?.truthy()))
|
|
}
|
|
|
|
/// `l_strcmp` under the "C" locale: bytewise, embedded zeros included.
|
|
fn str_lt(&self, a: StrRef, b: StrRef) -> std::cmp::Ordering {
|
|
self.heap.str(a).cmp(self.heap.str(b))
|
|
}
|
|
|
|
#[cold]
|
|
#[inline(never)]
|
|
pub fn less_than(&mut self, a: Value, b: Value) -> Result<bool, LuaError> {
|
|
match (a.unpack(), b.unpack()) {
|
|
(V::Num(x), V::Num(y)) => Ok(x < y),
|
|
(V::Str(x), V::Str(y)) => Ok(self.str_lt(x, y).is_lt()),
|
|
_ if a.type_name() == b.type_name() => match self.order_tm(a, b, self.names.lt)? {
|
|
Some(r) => Ok(r),
|
|
None => Err(self.order_error(a, b)),
|
|
},
|
|
_ => Err(self.order_error(a, b)),
|
|
}
|
|
}
|
|
|
|
#[cold]
|
|
#[inline(never)]
|
|
fn less_equal(&mut self, a: Value, b: Value) -> Result<bool, LuaError> {
|
|
match (a.unpack(), b.unpack()) {
|
|
(V::Num(x), V::Num(y)) => Ok(x <= y),
|
|
(V::Str(x), V::Str(y)) => Ok(self.str_lt(x, y).is_le()),
|
|
_ if a.type_name() == b.type_name() => {
|
|
if let Some(r) = self.order_tm(a, b, self.names.le)? {
|
|
return Ok(r);
|
|
}
|
|
// 5.0: `a <= b` as `not (b < a)`.
|
|
match self.order_tm(b, a, self.names.lt)? {
|
|
Some(r) => Ok(!r),
|
|
None => Err(self.order_error(a, b)),
|
|
}
|
|
}
|
|
_ => Err(self.order_error(a, b)),
|
|
}
|
|
}
|
|
|
|
/// 5.0's `luaV_concat` over `stack[first..=last]`, collapsing from the right; the result is
|
|
/// left in `stack[first]`.
|
|
#[cold]
|
|
#[inline(never)]
|
|
fn concat(&mut self, first: usize, last: usize) -> Result<(), LuaError> {
|
|
let mut total = last - first + 1;
|
|
let mut last = last;
|
|
while total > 1 {
|
|
let top = last + 1;
|
|
let mut n = 2;
|
|
let a = self.stack[top - 2];
|
|
let b = self.stack[top - 1];
|
|
let strnum = |v: Value| matches!(v.unpack(), V::Str(_) | V::Num(_));
|
|
match (strnum(a), strnum(b)) {
|
|
(true, true) => {
|
|
if matches!(b.unpack(), V::Str(s) if self.heap.str(s).is_empty()) {
|
|
// `a .. ""` is `a` as a string.
|
|
let sa = self.tostr(a).expect("a string or number");
|
|
self.stack[top - 2] = Value::Str(sa);
|
|
} else {
|
|
while n < total && strnum(self.stack[top - n - 1]) {
|
|
n += 1;
|
|
}
|
|
// A number is formatted straight into the result: 5.0 interns its text
|
|
// first, a string nothing else ever sees.
|
|
let mut buf = std::mem::take(&mut self.concat_buf);
|
|
buf.clear();
|
|
// Each piece grows the buffer fallibly, a number's text at most 24 bytes:
|
|
// past `MAX_STRING`, or room the allocator refuses, is `not enough
|
|
// memory` (C's `luaV_concat` allocates the sum and fails there).
|
|
let mut refused = false;
|
|
for i in (1..=n).rev() {
|
|
match self.stack[top - i].unpack() {
|
|
V::Str(s) => {
|
|
let b = self.heap.str(s);
|
|
if b.len() > buf.capacity() - buf.len()
|
|
&& !crate::api::grow_fallibly(&mut buf, b.len())
|
|
{
|
|
refused = true;
|
|
break;
|
|
}
|
|
buf.extend_from_slice(b);
|
|
}
|
|
V::Num(x) => {
|
|
if 24 > buf.capacity() - buf.len()
|
|
&& !crate::api::grow_fallibly(&mut buf, 24)
|
|
{
|
|
refused = true;
|
|
break;
|
|
}
|
|
crate::number::push_g14(x, &mut buf);
|
|
}
|
|
_ => unreachable!("checked above"),
|
|
}
|
|
}
|
|
if refused {
|
|
return Err(self.memory_error());
|
|
}
|
|
self.heap.concat_space(buf.len());
|
|
self.stack[top - n] = self.str(&buf);
|
|
// A large buffer is not kept: one long result would hold its memory.
|
|
if buf.capacity() <= 64 * 1024 {
|
|
self.concat_buf = buf;
|
|
}
|
|
}
|
|
}
|
|
_ => match self.bin_tm(a, b, self.names.concat)? {
|
|
Some(v) => self.stack[top - 2] = v,
|
|
None => {
|
|
// `luaG_concaterror`: blame the first operand unless it is a string.
|
|
let (bad, reg) = if matches!(a.unpack(), V::Str(_) | V::Num(_)) {
|
|
(b, top - 1)
|
|
} else {
|
|
(a, top - 2)
|
|
};
|
|
let base = self.frames.last().unwrap().base();
|
|
return Err(self.type_error(bad, Some(reg - base), "concatenate"));
|
|
}
|
|
},
|
|
}
|
|
total -= n - 1;
|
|
last -= n - 1;
|
|
}
|
|
if self.heap.oom {
|
|
return Err(self.memory_error());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
// ── calls ────────────────────────────────────────────────────────────────────────────────
|
|
|
|
/// Call `f` with `args` from the host or a native, answering `nres` results (-1: all).
|
|
pub fn call_value(
|
|
&mut self,
|
|
f: Value,
|
|
args: &[Value],
|
|
nres: i32,
|
|
) -> Result<Vec<Value>, LuaError> {
|
|
let func_idx = self.top;
|
|
self.ensure(func_idx + 1 + args.len() + 1);
|
|
self.stack[func_idx] = f;
|
|
self.stack[func_idx + 1..func_idx + 1 + args.len()].copy_from_slice(args);
|
|
self.top = func_idx + 1 + args.len();
|
|
self.call_at(func_idx, args.len(), nres)?;
|
|
let out = self.stack[func_idx..self.top].to_vec();
|
|
self.top = func_idx;
|
|
Ok(out)
|
|
}
|
|
|
|
/// Call the function at `func_idx` with the `nargs` values above it; its results replace
|
|
/// them, `top` just past the last. Runs a Lua function to completion.
|
|
pub fn call_at(&mut self, func_idx: usize, nargs: usize, nres: i32) -> Result<(), LuaError> {
|
|
// From the host into Lua: a profile charges what runs until it is back out.
|
|
if !self.frames.is_empty() {
|
|
return self.call_at_inner(func_idx, nargs, nres);
|
|
}
|
|
self.clear_below = 0;
|
|
if self.abort.take().is_some() {
|
|
// One a panic carried past the host's last call.
|
|
self.hooked = self.hook.is_some() || self.profile.is_some();
|
|
}
|
|
let mut r = self.unwind_guard(|vm| vm.call_at_inner(func_idx, nargs, nres));
|
|
if let Some(p) = self.profile.as_mut() {
|
|
p.close();
|
|
}
|
|
// An abort ends here, at the host, even where a native caught it.
|
|
if let Some(msg) = self.abort.take() {
|
|
self.hooked = self.hook.is_some() || self.profile.is_some();
|
|
let v = self.str(&msg);
|
|
r = Err(LuaError { value: v });
|
|
}
|
|
r
|
|
}
|
|
|
|
/// Run `f`, the host's entry into the VM: a Rust panic that unwinds out of it (a host
|
|
/// function's, or the VM's own) first puts the VM back as it was at entry, so the state
|
|
/// stays usable after the host catches the panic.
|
|
fn unwind_guard<T>(&mut self, f: impl FnOnce(&mut Vm) -> T) -> T {
|
|
let at = (
|
|
self.frames.len(),
|
|
self.c_depth,
|
|
self.top,
|
|
self.handlers.len(),
|
|
self.in_handler,
|
|
self.in_hook,
|
|
);
|
|
let id = self.identity();
|
|
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(self))) {
|
|
Ok(v) => v,
|
|
// Another VM in this one's place has its own state to keep.
|
|
Err(payload) if self.identity() != id => std::panic::resume_unwind(payload),
|
|
Err(payload) => {
|
|
let (frames, c_depth, top, handlers, in_handler, in_hook) = at;
|
|
self.close_upvals(top);
|
|
self.frames.truncate(frames);
|
|
self.c_depth = c_depth;
|
|
self.top = top;
|
|
self.handlers.truncate(handlers);
|
|
self.in_handler = in_handler;
|
|
self.in_hook = in_hook;
|
|
self.restore_stack_limit();
|
|
std::panic::resume_unwind(payload)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A call from the host or a native into Lua. The running chunk is the caller's again when
|
|
/// it returns, so the profile and the table census charge what the caller does next to it.
|
|
fn call_at_inner(&mut self, func_idx: usize, nargs: usize, nres: i32) -> Result<(), LuaError> {
|
|
let src = self.heap.cur_source;
|
|
let r = self.call_at_checked(func_idx, nargs, nres);
|
|
if self.heap.cur_source != src {
|
|
self.heap.cur_source = src;
|
|
if self.hooked {
|
|
self.prof_switch();
|
|
}
|
|
}
|
|
r
|
|
}
|
|
|
|
fn call_at_checked(
|
|
&mut self,
|
|
func_idx: usize,
|
|
nargs: usize,
|
|
nres: i32,
|
|
) -> Result<(), LuaError> {
|
|
self.c_depth += 1;
|
|
let marker = 0u8;
|
|
let here = std::ptr::addr_of!(marker) as usize;
|
|
if self.c_depth == 1 {
|
|
self.stack_base = here;
|
|
}
|
|
// Miri gives each local its own allocation, at no stack address: no measure there.
|
|
let used = if cfg!(miri) {
|
|
0
|
|
} else {
|
|
self.stack_base.abs_diff(here)
|
|
};
|
|
if self.c_depth >= MAXCCALLS || used > MAX_RUST_STACK {
|
|
// 5.1.5's `luaD_call`: the error at the limit, raised at the deeper level so its
|
|
// handler has room; a handler's calls past the margin raise without a handler.
|
|
let e = if self.c_depth >= MAXCCALLS + (MAXCCALLS >> 3)
|
|
|| used > MAX_RUST_STACK + (MAX_RUST_STACK >> 3)
|
|
{
|
|
self.errerr()
|
|
} else if self.c_depth == MAXCCALLS || (used > MAX_RUST_STACK && self.in_handler == 0) {
|
|
self.rt_error("C stack overflow")
|
|
} else {
|
|
return self.call_at_body(func_idx, nargs, nres);
|
|
};
|
|
self.c_depth -= 1;
|
|
return Err(e);
|
|
}
|
|
self.call_at_body(func_idx, nargs, nres)
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn call_at_body(&mut self, func_idx: usize, nargs: usize, nres: i32) -> Result<(), LuaError> {
|
|
let r = match self.precall(func_idx, nargs, nres) {
|
|
Ok(Called::Lua) => {
|
|
self.frames.last_mut().unwrap().boundary = true;
|
|
self.execute()
|
|
}
|
|
Ok(Called::Done) => Ok(()),
|
|
Err(e) => Err(e),
|
|
};
|
|
self.c_depth -= 1;
|
|
r
|
|
}
|
|
|
|
/// A protected call: on error the stack and frames unwind to here, open upvalues above
|
|
/// close, and the error is the result.
|
|
pub fn pcall_at(
|
|
&mut self,
|
|
func_idx: usize,
|
|
nargs: usize,
|
|
nres: i32,
|
|
handler: Option<Value>,
|
|
) -> Result<(), LuaError> {
|
|
let frames = self.frames.len();
|
|
let c_depth = self.c_depth;
|
|
let handlers = self.handlers.len();
|
|
self.handlers.push(handler);
|
|
// A panic that passes leaves this call's handler, frames and depth as they were, so a
|
|
// native that catches it finds the state as at this call.
|
|
let r = self.unwind_guard(|vm| vm.call_at(func_idx, nargs, nres));
|
|
self.handlers.truncate(handlers);
|
|
if let Err(e) = r {
|
|
self.close_upvals(func_idx);
|
|
self.frames.truncate(frames);
|
|
self.c_depth = c_depth;
|
|
self.top = func_idx;
|
|
self.restore_stack_limit();
|
|
return Err(e);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// 5.0's `luaD_precall`.
|
|
#[cold]
|
|
#[inline(never)]
|
|
fn precall(&mut self, func_idx: usize, nargs: usize, nres: i32) -> Result<Called, LuaError> {
|
|
self.precall_clearing(func_idx, nargs, nres, false)
|
|
}
|
|
|
|
/// [`Vm::precall`]; `clear` clears a Lua frame's registers wherever it starts.
|
|
#[cold]
|
|
#[inline(never)]
|
|
fn precall_clearing(
|
|
&mut self,
|
|
func_idx: usize,
|
|
nargs: usize,
|
|
nres: i32,
|
|
clear: bool,
|
|
) -> Result<Called, LuaError> {
|
|
let mut nargs = nargs;
|
|
let fref = match self.stack[func_idx].unpack() {
|
|
V::Func(f) => f,
|
|
_ => {
|
|
nargs = self.tryfunc(func_idx, nargs)?;
|
|
self.stack[func_idx]
|
|
.as_func()
|
|
.expect("`tryfunc` placed a function")
|
|
}
|
|
};
|
|
if self.frames.len() >= self.ci_limit {
|
|
self.grow_ci()?;
|
|
}
|
|
match self.heap.func(fref) {
|
|
FuncObj::Lua(LuaFunc {
|
|
proto, upvals, env, ..
|
|
}) => {
|
|
let pp = Rc::as_ptr(proto);
|
|
let (fup, fenv): (*const [u32], TableRef) = (&**upvals, *env);
|
|
// SAFETY: the closure being called holds this code, and the closure sits in the
|
|
// stack slot `func_idx` for the whole call.
|
|
let proto = unsafe { &*pp };
|
|
let base = func_idx + 1;
|
|
let np = proto.nparams as usize;
|
|
let frame_top = base + proto.maxstack as usize;
|
|
self.checkstack(precall_top50(base, nargs, proto), proto.c_maxstack as usize);
|
|
self.ensure(frame_top + 1);
|
|
let mut fill_from = base + nargs;
|
|
if proto.vararg {
|
|
// 5.0's `adjust_varargs`: the extras into `arg`, made by
|
|
// `luaH_new(L, actual, 1)` (`0x6f6266`), with `n`.
|
|
let extra = nargs.saturating_sub(np);
|
|
let t = self.heap.new_table(extra, 1);
|
|
for i in 0..extra {
|
|
let v = self.stack[base + np + i];
|
|
self.heap
|
|
.table_mut(t)
|
|
.set_int(i as i32 + 1, v)
|
|
.expect("the presized array part holds every extra");
|
|
}
|
|
let n = Value::Str(self.names.n);
|
|
self.heap
|
|
.table_mut(t)
|
|
.set(n, Value::Num(extra as f64))
|
|
.unwrap();
|
|
for i in nargs..np {
|
|
self.stack[base + i] = Value::Nil;
|
|
}
|
|
self.stack[base + np] = Value::Table(t);
|
|
fill_from = base + np + 1;
|
|
} else if nargs > np {
|
|
fill_from = base + np;
|
|
}
|
|
// Missing parameters are nil; below `clear_below`, every register past the
|
|
// parameters too, extra arguments included, as `luaD_precall` clears them.
|
|
if proto.clears {
|
|
self.clear_below = self.clear_below.max(frame_top);
|
|
}
|
|
let end = if clear || base < self.clear_below {
|
|
base + proto.c_maxstack as usize
|
|
} else {
|
|
base + np
|
|
};
|
|
if fill_from < end {
|
|
for v in &mut self.stack[fill_from..end] {
|
|
*v = Value::Nil;
|
|
}
|
|
}
|
|
self.top = frame_top;
|
|
self.frames.push(Frame {
|
|
func: fref,
|
|
base: base as u32,
|
|
ip: proto.code.as_ptr(),
|
|
nresults: nres,
|
|
consts: proto.consts.as_ptr(),
|
|
maxstack: proto.maxstack,
|
|
source: proto.source_id,
|
|
proto: pp,
|
|
upvals: fup.cast::<u32>(),
|
|
env: fenv,
|
|
env_gen: self.fenv_gen,
|
|
tailcalls: 0,
|
|
boundary: false,
|
|
});
|
|
// The client's `adjust_varargs` makes `arg` without checking the collector
|
|
// (`0x6f6200`..`0x6f6366`), where 5.1.5's checks it.
|
|
Ok(Called::Lua)
|
|
}
|
|
FuncObj::Native { .. } => {
|
|
self.call_native(fref, func_idx, nargs, nres)?;
|
|
Ok(Called::Done)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 5.0's `luaD_precall` for a native, the call depth already checked, then
|
|
/// `luaD_poscall`: the native `fref` at `func_idx` runs on the `nargs` values above it, and
|
|
/// its results replace them.
|
|
#[inline(never)]
|
|
fn call_native(
|
|
&mut self,
|
|
fref: FuncRef,
|
|
func_idx: usize,
|
|
nargs: usize,
|
|
nres: i32,
|
|
) -> Result<(), LuaError> {
|
|
let FuncObj::Native { f, host, .. } = self.heap.func(fref) else {
|
|
unreachable!("a native")
|
|
};
|
|
let f = *f;
|
|
let host = host.clone();
|
|
let base = func_idx + 1;
|
|
self.top = base + nargs;
|
|
self.checkstack(self.top, LUA_MINSTACK);
|
|
self.frames.push(Frame {
|
|
func: fref,
|
|
base: base as u32,
|
|
ip: std::ptr::null(),
|
|
nresults: nres,
|
|
consts: std::ptr::null(),
|
|
maxstack: 0,
|
|
source: self.heap.cur_source,
|
|
proto: std::ptr::null(),
|
|
upvals: std::ptr::null(),
|
|
env: self.globals,
|
|
env_gen: self.fenv_gen,
|
|
tailcalls: 0,
|
|
boundary: false,
|
|
});
|
|
let id = self.identity();
|
|
let r = match host {
|
|
Some(h) => h(self, nargs),
|
|
None => f(self, nargs),
|
|
};
|
|
self.check_identity(id);
|
|
let n = r?;
|
|
// A string the native made could not be: it raises here, in its caller.
|
|
if self.heap.oom {
|
|
return Err(self.memory_error());
|
|
}
|
|
self.frames.pop();
|
|
let first = self.top - n;
|
|
// One result wanted, at least one given: the common call in an expression.
|
|
if nres == 1 && n >= 1 {
|
|
self.stack[func_idx] = self.stack[first];
|
|
self.top = func_idx + 1;
|
|
} else if nres == 0 {
|
|
// None, a call statement.
|
|
self.top = func_idx;
|
|
} else {
|
|
self.poscall(func_idx, first, n, nres);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// 5.1's `tryfuncTM`: the value at `func_idx` is not a function, so its `__call` goes below
|
|
/// the arguments, the value first among them; answers the new argument count.
|
|
#[cold]
|
|
#[inline(never)]
|
|
fn tryfunc(&mut self, func_idx: usize, nargs: usize) -> Result<usize, LuaError> {
|
|
let other = self.stack[func_idx];
|
|
let tm = self.tm(other, self.names.call);
|
|
if !matches!(tm.unpack(), V::Func(_)) {
|
|
// A name only for a register of the running Lua function, as 5.1's `isinstack`: a
|
|
// metamethod is called from above them.
|
|
let reg = self
|
|
.frames
|
|
.last()
|
|
.filter(|f| !f.proto.is_null())
|
|
.map(|f| func_idx.wrapping_sub(f.base()))
|
|
.filter(|&r| r < self.frames.last().map_or(0, |f| f.maxstack as usize));
|
|
return Err(self.type_error(other, reg, "call"));
|
|
}
|
|
self.ensure(func_idx + nargs + 2);
|
|
self.stack
|
|
.copy_within(func_idx..func_idx + nargs + 1, func_idx + 1);
|
|
self.stack[func_idx] = tm;
|
|
self.top = func_idx + nargs + 2;
|
|
Ok(nargs + 1)
|
|
}
|
|
|
|
/// `luaD_growCI` (`0x6f61c0`): the call-info array doubles; past [`MAXCALLS`] the call is
|
|
/// refused with "stack overflow" (`0x6f61f2`), and a call while it is past the limit (the
|
|
/// overflow's handler recursing) with no handler at all (`0x6f61d3`).
|
|
#[cold]
|
|
#[inline(never)]
|
|
fn grow_ci(&mut self) -> Result<(), LuaError> {
|
|
if self.ci_size > MAXCALLS {
|
|
return Err(self.errerr());
|
|
}
|
|
self.set_ci_size(self.ci_size * 2);
|
|
if self.ci_size > MAXCALLS {
|
|
return Err(self.rt_error("stack overflow"));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn set_ci_size(&mut self, n: usize) {
|
|
self.heap.recharge(CI_BYTES * self.ci_size, CI_BYTES * n);
|
|
let b = self.heap.pools().realloc(self.ci_block, CI_BYTES * n);
|
|
self.ci_block = b;
|
|
self.ci_size = n;
|
|
self.ci_limit = n - 1;
|
|
}
|
|
|
|
/// 5.1.5's `restore_stack_limit`, after a protected call unwinds: an array an overflow grew
|
|
/// past the limit goes back to it, if the frames still in use leave room.
|
|
fn restore_stack_limit(&mut self) {
|
|
if self.ci_size > MAXCALLS && self.frames.len() + 1 < MAXCALLS {
|
|
self.set_ci_size(MAXCALLS);
|
|
}
|
|
}
|
|
|
|
/// 5.1.5's `LUA_ERRERR`: thrown as is, no handler run, its message the one
|
|
/// `luaD_seterrorobj` writes.
|
|
fn errerr(&mut self) -> LuaError {
|
|
LuaError {
|
|
value: self.str(b"error in error handling"),
|
|
}
|
|
}
|
|
|
|
/// 5.0's `luaD_poscall`: `n` results from `first` move to `dest`, adjusted to `wanted`.
|
|
#[cold]
|
|
#[inline(never)]
|
|
fn poscall(&mut self, dest: usize, first: usize, n: usize, wanted: i32) {
|
|
// One result wanted, at least one given: the common call in an expression.
|
|
if wanted == 1 && n >= 1 {
|
|
self.stack[dest] = self.stack[first];
|
|
self.top = dest + 1;
|
|
return;
|
|
}
|
|
if wanted < 0 {
|
|
self.stack.copy_within(first..first + n, dest);
|
|
self.top = dest + n;
|
|
} else {
|
|
let wanted = wanted as usize;
|
|
self.ensure(dest + wanted + 1);
|
|
let k = n.min(wanted);
|
|
self.stack.copy_within(first..first + k, dest);
|
|
for v in &mut self.stack[dest + k..dest + wanted] {
|
|
*v = Value::Nil;
|
|
}
|
|
self.top = dest + wanted;
|
|
}
|
|
}
|
|
|
|
// ── natives' view ────────────────────────────────────────────────────────────────────────
|
|
|
|
/// The `i`th argument (0-based) of the running native, or nil.
|
|
#[inline]
|
|
pub fn arg(&self, nargs: usize, i: usize) -> Value {
|
|
if i < nargs {
|
|
self.stack[self.frames.last().unwrap().base() + i]
|
|
} else {
|
|
Value::Nil
|
|
}
|
|
}
|
|
|
|
/// The running native's arguments.
|
|
pub fn args(&self, nargs: usize) -> &[Value] {
|
|
let b = self.frames.last().unwrap().base();
|
|
&self.stack[b..b + nargs]
|
|
}
|
|
|
|
/// The running function's environment table (`getfenv(0)`'s answer for a native).
|
|
pub fn current_env(&self) -> TableRef {
|
|
let f = self.frames.last().unwrap().func;
|
|
match self.heap.func(f) {
|
|
FuncObj::Lua(LuaFunc { env, .. }) | FuncObj::Native { env, .. } => *env,
|
|
}
|
|
}
|
|
|
|
/// `setfenv` of a Lua function: its environment becomes `env`, and every running frame
|
|
/// rereads its cached one. False for a native, whose environment does not change.
|
|
pub fn set_function_env(&mut self, f: FuncRef, env: TableRef) -> bool {
|
|
match self.heap.func_mut(f) {
|
|
FuncObj::Lua(l) => {
|
|
l.env = env;
|
|
self.fenv_gen = self.fenv_gen.wrapping_add(1);
|
|
true
|
|
}
|
|
FuncObj::Native { .. } => false,
|
|
}
|
|
}
|
|
|
|
/// The value of upvalue `u` (a handle from `LuaFunc::upvals`): its stack slot's while open.
|
|
pub fn read_upvalue(&self, u: u32) -> Value {
|
|
match self.heap.upval(u) {
|
|
UpvalObj::Open(i) => self.stack.get(i as usize).copied().unwrap_or(Value::Nil),
|
|
UpvalObj::Closed(v) => v,
|
|
}
|
|
}
|
|
|
|
/// Set upvalue `u`, as `SETUPVAL` does: its stack slot while open, else the upvalue.
|
|
pub fn write_upvalue(&mut self, u: u32, v: Value) {
|
|
match self.heap.upval(u) {
|
|
UpvalObj::Open(i) => {
|
|
if let Some(slot) = self.stack.get_mut(i as usize) {
|
|
*slot = v;
|
|
}
|
|
}
|
|
UpvalObj::Closed(_) => *self.heap.upval_mut(u) = UpvalObj::Closed(v),
|
|
}
|
|
}
|
|
|
|
// ── host roots ───────────────────────────────────────────────────────────────────────────
|
|
|
|
/// Hold `v` for the host: a collector root until [`Vm::unroot`] brings its count to zero.
|
|
pub fn root(&mut self, v: Value) -> u32 {
|
|
self.roots.0.root(v)
|
|
}
|
|
|
|
/// Another holder of root `i`.
|
|
pub fn retain(&mut self, i: u32) {
|
|
self.roots.0.retain(i);
|
|
}
|
|
|
|
/// One holder of root `i` lets go.
|
|
pub fn unroot(&mut self, i: u32) {
|
|
self.roots.0.unroot(i);
|
|
}
|
|
|
|
pub fn rooted(&self, i: u32) -> Value {
|
|
self.roots.0.get(i)
|
|
}
|
|
|
|
/// Root `i` if it is the one with `serial` ([`RootTable::get_as`]).
|
|
pub fn rooted_as(&self, i: u32, serial: u64) -> Option<Value> {
|
|
self.roots.0.get_as(i, serial)
|
|
}
|
|
|
|
/// The root table itself, which a host handle keeps to let go of its root without reaching
|
|
/// the VM: a handle dropped while the VM runs (a host closure the collector frees) or after
|
|
/// it is gone touches only the table.
|
|
pub fn root_table(&self) -> Rc<RootTable> {
|
|
self.roots.0.clone()
|
|
}
|
|
|
|
// ── the hook ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
/// Run `f` once for every `step` instructions, at the next back edge, call or return
|
|
/// ([`Hook`]).
|
|
pub fn set_hook(&mut self, step: u64, f: HookFn) {
|
|
self.hook = Some(Hook {
|
|
step: step.max(1),
|
|
f,
|
|
counted: 0,
|
|
});
|
|
self.hooked = true;
|
|
}
|
|
|
|
pub fn remove_hook(&mut self) {
|
|
self.hook = None;
|
|
self.hooked = self.profile.is_some() || self.abort.is_some();
|
|
}
|
|
|
|
/// Raise `msg` past every protected call, to the host's call into the VM: no error handler
|
|
/// runs, and until that call returns every raise is this one and every loop back edge, call
|
|
/// and return raises it again. A `pcall` that catches it lets its caller run only to the next
|
|
/// of those, so the unwinding ends. A host's guard against a script that never ends.
|
|
pub fn abort(&mut self, msg: &[u8]) -> LuaError {
|
|
self.abort = Some(msg.into());
|
|
self.hooked = true;
|
|
self.abort_error()
|
|
}
|
|
|
|
/// The abort in flight, raised again.
|
|
#[cold]
|
|
#[inline(never)]
|
|
fn abort_error(&mut self) -> LuaError {
|
|
let msg = self.abort.clone().expect("an abort");
|
|
let v = self.str(&msg);
|
|
LuaError { value: v }
|
|
}
|
|
|
|
/// Start or stop charging time to chunks; starting keeps what was charged so far.
|
|
pub fn set_profiling(&mut self, on: bool) {
|
|
if on {
|
|
self.profile.get_or_insert_with(Default::default);
|
|
} else {
|
|
self.profile = None;
|
|
}
|
|
self.hooked = self.hook.is_some() || self.profile.is_some() || self.abort.is_some();
|
|
}
|
|
|
|
/// The running chunk changed: close the segment and open one for it.
|
|
#[cold]
|
|
#[inline(never)]
|
|
fn prof_switch(&mut self) {
|
|
let src = self.heap.cur_source;
|
|
if let Some(p) = self.profile.as_mut() {
|
|
p.mark(src);
|
|
}
|
|
}
|
|
|
|
/// `n` more instructions ran, counted toward the hook's next run without running it.
|
|
#[cold]
|
|
#[inline(never)]
|
|
fn hook_add(&mut self, n: u64) {
|
|
if let Some(h) = self.hook.as_mut().filter(|_| !self.in_hook) {
|
|
h.counted += n;
|
|
}
|
|
}
|
|
|
|
/// `n` more instructions ran, as 5.1's `LUA_MASKCOUNT` counts them: the hook runs once for
|
|
/// each `step` they complete.
|
|
#[cold]
|
|
#[inline(never)]
|
|
fn hook_count(&mut self, n: u64) -> Result<(), LuaError> {
|
|
if self.abort.is_some() {
|
|
return Err(self.abort_error());
|
|
}
|
|
if self.in_hook {
|
|
return Ok(());
|
|
}
|
|
let Some(h) = self.hook.as_mut() else {
|
|
return Ok(());
|
|
};
|
|
h.counted += n;
|
|
while let Some(h) = self.hook.as_mut().filter(|h| h.counted >= h.step) {
|
|
h.counted -= h.step;
|
|
let f = h.f.clone();
|
|
self.in_hook = true;
|
|
let id = self.identity();
|
|
let r = f(self);
|
|
self.check_identity(id);
|
|
self.in_hook = false;
|
|
r?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// This VM's and its heap's identity ([`crate::heap::Identity`]).
|
|
#[inline(always)]
|
|
fn identity(&self) -> (u64, u64) {
|
|
(self.identity.0, self.heap.identity.0)
|
|
}
|
|
|
|
/// Stop if a native replaced the VM or its heap: the running frames' code and stack are
|
|
/// gone, and a Lua error would resume them.
|
|
#[inline(always)]
|
|
fn check_identity(&self, id: (u64, u64)) {
|
|
#[cold]
|
|
#[inline(never)]
|
|
fn replaced() -> ! {
|
|
panic!("a native replaced the running Lua VM or its heap");
|
|
}
|
|
if self.identity() != id {
|
|
replaced();
|
|
}
|
|
}
|
|
|
|
// ── stack inspection ─────────────────────────────────────────────────────────────────────
|
|
|
|
/// The stack level `level` up, as `lua_getstack` and `lua_getinfo(L, "Snl")` see it; a
|
|
/// level a tail call replaced is a `"tail"` frame.
|
|
pub fn frame_info(&self, level: usize) -> Option<FrameInfo> {
|
|
let mut lv = level as i64;
|
|
let mut i = self.frames.len();
|
|
while i > 0 {
|
|
i -= 1;
|
|
if lv == 0 {
|
|
return Some(self.describe(i));
|
|
}
|
|
lv -= 1;
|
|
if !self.frames[i].proto.is_null() {
|
|
lv -= self.frames[i].tailcalls as i64;
|
|
}
|
|
if lv < 0 {
|
|
return Some(FrameInfo {
|
|
what: "tail",
|
|
..Default::default()
|
|
});
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
fn describe(&self, i: usize) -> FrameInfo {
|
|
let f = &self.frames[i];
|
|
let named = self
|
|
.name_of(i)
|
|
.or_else(|| self.global_name_of(Value::Func(f.func)));
|
|
let (name, namewhat) = match named {
|
|
Some((kind, n)) => (Some(n), kind),
|
|
None => (None, ""),
|
|
};
|
|
// SAFETY: a live frame's code is alive (see `Frame::proto`).
|
|
match unsafe { f.proto.as_ref() } {
|
|
Some(p) => {
|
|
let pc = f.pc().saturating_sub(1);
|
|
FrameInfo {
|
|
source: Some(p.source.to_vec()),
|
|
what: if p.line_defined == 0 { "main" } else { "Lua" },
|
|
current_line: p.lines.get(pc).copied(),
|
|
line_defined: p.line_defined,
|
|
name,
|
|
namewhat,
|
|
func: Value::Func(f.func),
|
|
}
|
|
}
|
|
None => FrameInfo {
|
|
source: None,
|
|
what: "C",
|
|
current_line: None,
|
|
line_defined: 0,
|
|
name,
|
|
namewhat,
|
|
func: Value::Func(f.func),
|
|
},
|
|
}
|
|
}
|
|
|
|
/// 5.0's `getfuncname` for frame `i`: what the caller's calling instruction named it.
|
|
fn name_of(&self, i: usize) -> Option<(&'static str, Vec<u8>)> {
|
|
if i == 0 || self.frames[i].tailcalls > 0 {
|
|
return None;
|
|
}
|
|
let caller = &self.frames[i - 1];
|
|
// SAFETY: a live frame's code is alive (see `Frame::proto`).
|
|
let p = unsafe { caller.proto.as_ref() }?;
|
|
let pc = caller.pc().checked_sub(1)?;
|
|
match p.code.get(pc)? {
|
|
// The client's `getfuncname` names a function only an `OP_CALL` or `OP_TAILCALL`
|
|
// called (`0x6fc499`..`0x6fc4a1`): a generic `for`'s generator is named nothing.
|
|
Op::Call(a, ..) | Op::TailCall(a, ..) => objname_in(p, &self.heap, pc, *a as usize),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// 5.0's `travglobals` (`0x6fbf90`), which `lua_getinfo`'s `n` falls back on when the calling
|
|
/// instruction names nothing (`0x6fbe11`): the globals' hash part walked from its last node
|
|
/// down (`0x6fbfc6`), answering the first whose value is raw-equal to `f` (`0x6fbfcf`) and whose
|
|
/// key is a string (`0x6fbfd8`), as `("global", key)` (`0x6fbe22`).
|
|
fn global_name_of(&self, f: Value) -> Option<(&'static str, Vec<u8>)> {
|
|
let globals = self.heap.table(self.globals);
|
|
globals
|
|
.node
|
|
.iter()
|
|
.rev()
|
|
.find_map(|n| match n.key.unpack() {
|
|
V::Str(k) if n.val.raw_eq(f) => {
|
|
Some(("global", crate::lex::cstr(self.heap.str(k)).to_vec()))
|
|
}
|
|
_ => None,
|
|
})
|
|
}
|
|
|
|
/// The name a native was called by, as 5.0's `lua_getinfo` `n` finds it: from the calling
|
|
/// instruction (`getfuncname`), `("global", "strfind")`, `("method", "SetText")`, else by a
|
|
/// global holding it ([`Self::global_name_of`]).
|
|
pub fn called_name(&self) -> Option<(&'static str, Vec<u8>)> {
|
|
let me = self.frames.last()?;
|
|
self.calling_instruction_name()
|
|
.or_else(|| self.global_name_of(Value::Func(me.func)))
|
|
}
|
|
|
|
/// `getfuncname` for the running native: what its caller's calling instruction named it.
|
|
fn calling_instruction_name(&self) -> Option<(&'static str, Vec<u8>)> {
|
|
let n = self.frames.len();
|
|
if n < 2 {
|
|
return None;
|
|
}
|
|
let me = &self.frames[n - 1];
|
|
let caller = &self.frames[n - 2];
|
|
if me.tailcalls > 0 {
|
|
return None;
|
|
}
|
|
// SAFETY: a live frame's code is alive (see `Frame::proto`).
|
|
let p = unsafe { caller.proto.as_ref() }?;
|
|
let pc = caller.pc().checked_sub(1)?;
|
|
match p.code.get(pc)? {
|
|
// The client's `getfuncname` names a function only an `OP_CALL` or `OP_TAILCALL`
|
|
// called (`0x6fc499`..`0x6fc4a1`): a generic `for`'s generator is named nothing.
|
|
Op::Call(a, ..) | Op::TailCall(a, ..) => objname_in(p, &self.heap, pc, *a as usize),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// `luaL_argerror` (`0x6f4810`): `bad argument #n to `name' (msg)`, with a method's `self` not
|
|
/// counted, the name's bytes as the caller wrote them.
|
|
pub fn arg_error(&mut self, narg: usize, msg: &str) -> LuaError {
|
|
let msg = msg.as_bytes();
|
|
let named = self.called_name();
|
|
let mut narg = narg;
|
|
let name: Vec<u8> = match named {
|
|
Some((kind, n)) => {
|
|
if kind == "method" {
|
|
narg -= 1;
|
|
if narg == 0 {
|
|
let m = [b"calling `", &n[..], b"' on bad self (", msg, b")"].concat();
|
|
return self.error_at_bytes(1, &m);
|
|
}
|
|
}
|
|
n
|
|
}
|
|
None => b"?".to_vec(),
|
|
};
|
|
let head = format!("bad argument #{narg} to `");
|
|
let m = [head.as_bytes(), &name, b"' (", msg, b")"].concat();
|
|
self.error_at_bytes(1, &m)
|
|
}
|
|
|
|
/// `luaL_typerror`: `<expected> expected, got <type>`; an absent argument is "no value".
|
|
pub fn type_arg_error(&mut self, nargs: usize, narg: usize, expected: &str) -> LuaError {
|
|
let got = if narg > nargs {
|
|
"no value"
|
|
} else {
|
|
self.arg(nargs, narg - 1).type_name()
|
|
};
|
|
self.arg_error(narg, &format!("{expected} expected, got {got}"))
|
|
}
|
|
|
|
// ── collection ───────────────────────────────────────────────────────────────────────────
|
|
|
|
/// What the state holds outside its objects, in the collector's count: its records, its
|
|
/// stack and its call records.
|
|
/// 5.0's `checkstacksizes` (`0x6f7900`), which `traversestack` runs once a collection as it
|
|
/// marks the thread (`0x6f78ed`): call records under a quarter in use halve while more than
|
|
/// 16 (`0x6f7925`..`0x6f7936`), then a stack under a quarter in use up to the highest Lua
|
|
/// frame's `ci->top` halves while more than 90 slots (`0x6f7949`..`0x6f795e`).
|
|
fn check_stack_sizes(&mut self) {
|
|
if 4 * self.frames.len() < self.ci_size && self.ci_size > 2 * BASIC_CI_SIZE {
|
|
self.set_ci_size(self.ci_size / 2);
|
|
}
|
|
// `lim` (`0x6f7880`..`0x6f78a4`): the top, or the highest `ci->top` of a Lua frame, the
|
|
// registers the client's code reserves (`Proto::c_maxstack`). The innermost Lua frame's
|
|
// top is its `ci->top` in the client, where benilla's may stand above it.
|
|
let mut lim = match self.frames.last() {
|
|
Some(f) if !f.proto.is_null() => 0,
|
|
_ => self.top,
|
|
};
|
|
// What benilla's own frames use, which a halving must keep.
|
|
let mut used = self.top;
|
|
for f in &self.frames {
|
|
if !f.proto.is_null() {
|
|
// SAFETY: a frame's code lives as long as the frame (`Frame::proto`).
|
|
let cmax = unsafe { &*f.proto }.c_maxstack as usize;
|
|
lim = lim.max(f.base as usize + cmax);
|
|
used = used.max(f.base as usize + f.maxstack as usize + 1);
|
|
}
|
|
}
|
|
let size = self.stack.len();
|
|
if 4 * lim < size && size > 2 * BASIC_STACK_SIZE {
|
|
// Deviation: benilla's scratch registers above a frame's `c_maxstack`
|
|
// (`Proto::maxstack`) stay on the stack; the client has none, and halves.
|
|
if used <= size / 2 {
|
|
self.realloc_stack(size / 2);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn state_bytes(&self) -> usize {
|
|
STATE_BYTES + STACK_SLOT_BYTES * self.stack.len() + CI_BYTES * self.ci_size
|
|
}
|
|
|
|
/// 5.0's `luaC_checkGC`, at a safe point: every live value is on the stack or reachable.
|
|
#[inline]
|
|
pub fn check_gc(&mut self) {
|
|
if self.heap.bytes() >= self.heap.trigger || self.gc_stress {
|
|
self.full_gc();
|
|
}
|
|
}
|
|
|
|
/// 5.0's `luaC_collectgarbage` (`0x6f7340`): mark, which checks the stack's sizes as it
|
|
/// traverses it, sweep, `checkSizes`, and the client's threshold (`0x6fae00`).
|
|
#[cold]
|
|
#[inline(never)]
|
|
pub fn full_gc(&mut self) {
|
|
self.heap.collections += 1;
|
|
self.check_stack_sizes();
|
|
let mut roots: Vec<Value> = self.stack[..self.top].to_vec();
|
|
roots.push(Value::Table(self.globals));
|
|
roots.push(Value::Table(self.registry));
|
|
for f in &self.frames {
|
|
roots.push(Value::Func(f.func));
|
|
}
|
|
roots.extend(self.handlers.iter().flatten().copied());
|
|
roots.extend(self.roots.0.live());
|
|
// The generic `for` fast path compares against it by identity: a freed `next` whose id
|
|
// a new function took would step a table without calling that function.
|
|
roots.push(self.builtin_next);
|
|
let open: Vec<u32> = self.open.iter().map(|&(_, u)| u).collect();
|
|
// Names live forever.
|
|
let n = &self.names;
|
|
for s in [
|
|
n.index,
|
|
n.newindex,
|
|
n.call,
|
|
n.add,
|
|
n.sub,
|
|
n.mul,
|
|
n.div,
|
|
n.pow,
|
|
n.unm,
|
|
n.concat,
|
|
n.eq,
|
|
n.lt,
|
|
n.le,
|
|
n.mode,
|
|
n.metatable,
|
|
n.tostring,
|
|
n.n,
|
|
n.next,
|
|
] {
|
|
roots.push(Value::Str(s));
|
|
}
|
|
// A frame running code a hot reload replaced reads that code's constants and upvalues;
|
|
// with no frame left, nothing runs it.
|
|
if self.frames.is_empty() {
|
|
self.retired.clear();
|
|
}
|
|
let fixed = [self.stack_block, self.ci_block];
|
|
self.heap
|
|
.collect(&roots, &open, &self.retired, self.names.mode, &fixed);
|
|
self.heap.charge(self.state_bytes());
|
|
self.heap.pace();
|
|
// The host closures it freed drop last, with no arena borrowed, as host code: their
|
|
// captures' destructors may use the state.
|
|
if !self.heap.freed_hosts.is_empty() {
|
|
let freed = std::mem::take(&mut self.heap.freed_hosts);
|
|
// The host's `reentry` hands them this `&mut Vm`, while Lua may be running on it.
|
|
let id = self.identity();
|
|
self.as_host(move || drop(freed));
|
|
self.check_identity(id);
|
|
}
|
|
}
|
|
|
|
/// Run `f`, host code that may reach this VM through its host (a destructor that calls the
|
|
/// host's API), through `reentry` when the host set one, as a callback runs.
|
|
pub fn as_host(&mut self, f: impl FnOnce()) {
|
|
let mut f = Some(f);
|
|
let mut once = || {
|
|
if let Some(f) = f.take() {
|
|
f()
|
|
}
|
|
};
|
|
match self.reentry.clone() {
|
|
Some(r) => r(self, &mut once),
|
|
None => once(),
|
|
}
|
|
}
|
|
|
|
// ── the interpreter ──────────────────────────────────────────────────────────────────────
|
|
|
|
/// Run the top Lua frame until the frame that entered with `boundary` returns. The dispatch
|
|
/// loop has two instantiations: the fast one while no hook or profile is on and no running
|
|
/// function can read a register it has not written (`clear_below`), the slow one, which counts
|
|
/// for the hook and clears frames as `luaD_precall`, otherwise. A hook set while the fast
|
|
/// one runs moves it to the slow one at the next call or loop back edge, a function that
|
|
/// needs its frames cleared as it is entered.
|
|
fn execute(&mut self) -> Result<(), LuaError> {
|
|
loop {
|
|
let done = if self.hooked || self.clear_below != 0 {
|
|
self.run::<true>()?
|
|
} else {
|
|
self.run::<false>()?
|
|
};
|
|
if done {
|
|
return Ok(());
|
|
}
|
|
}
|
|
}
|
|
|
|
/// [`Vm::execute`]'s dispatch loop: `true` when the frame that entered with `boundary` has
|
|
/// returned, `false` to go on in the other instantiation from the top frame's saved place.
|
|
///
|
|
/// The register file is addressed through `regs`, a raw pointer to `stack[base]`, as C Lua
|
|
/// keeps `base` in a register. Anything that can grow the stack (a call, a metamethod, an
|
|
/// upvalue that touches the stack) is followed by `refresh!`, which recomputes it.
|
|
// `fr!` carries its own `unsafe`, nested in others at some uses.
|
|
#[allow(unused_unsafe)]
|
|
#[inline(never)]
|
|
fn run<const SLOW: bool>(&mut self) -> Result<bool, LuaError> {
|
|
// The running frame's state, kept in locals as C Lua's `luaV_execute` keeps it. A Lua
|
|
// call sets it straight from the callee; a return reloads it from the caller's frame.
|
|
// Only the three values every instruction touches live in locals; the rest of the
|
|
// running frame is read from it (`fr!`), so they keep their registers.
|
|
let mut ip: *const Op = std::ptr::null();
|
|
let mut k: *const Value;
|
|
let mut regs: *mut Value;
|
|
// While a hook is set, where the current straight run of instructions began, in
|
|
// `seg_code`'s code: the run is charged at a back edge, a forward jump, a call and a
|
|
// return (here, as the caller's frame is entered again), so each charge is the
|
|
// instructions executed.
|
|
let mut seg: *const Op = std::ptr::null();
|
|
let mut seg_code: *const Proto = std::ptr::null();
|
|
'frame: loop {
|
|
{
|
|
if SLOW && self.hooked && !seg.is_null() && seg != ip {
|
|
// The last run of the frame returned from; a call's run is charged already.
|
|
// SAFETY: the returned function's code is alive: its closure is unreachable
|
|
// at most since the return, and nothing has collected since.
|
|
let n = ran_since(seg, ip, unsafe { &*seg_code });
|
|
if n > 0 {
|
|
self.hook_count(n)?;
|
|
}
|
|
}
|
|
let frame = self.frames.last().expect("a Lua frame");
|
|
let base = frame.base();
|
|
ip = frame.ip;
|
|
k = frame.consts;
|
|
self.heap.cur_source = frame.source;
|
|
if SLOW {
|
|
if self.hooked {
|
|
seg = ip;
|
|
seg_code = frame.proto;
|
|
self.prof_switch();
|
|
} else {
|
|
// No run to keep: a hook set later starts from its first charge.
|
|
seg = std::ptr::null();
|
|
}
|
|
}
|
|
// SAFETY: `precall` grew the stack to cover `base + maxstack`, and only a
|
|
// collection shrinks it, never below a frame's registers, which `gc!` and `slow!`
|
|
// refresh after.
|
|
regs = unsafe { self.stack.as_mut_ptr().add(base) };
|
|
}
|
|
// The running frame, the last one. SAFETY: `execute` runs while its frame is on the
|
|
// list; the reference is used within one statement, before any push moves the list.
|
|
macro_rules! fr {
|
|
() => {
|
|
unsafe { self.frames.last().unwrap_unchecked() }
|
|
};
|
|
}
|
|
macro_rules! fr_mut {
|
|
() => {
|
|
unsafe { self.frames.last_mut().unwrap_unchecked() }
|
|
};
|
|
}
|
|
macro_rules! base {
|
|
() => {
|
|
(fr!().base as usize)
|
|
};
|
|
}
|
|
macro_rules! maxstack {
|
|
() => {
|
|
(fr!().maxstack as usize)
|
|
};
|
|
}
|
|
// SAFETY: the upvalue list lives as the code does (`Frame::upvals`), and
|
|
// `Proto::verify` checked every index against the function's upvalues.
|
|
macro_rules! upval {
|
|
($i:expr) => {
|
|
unsafe { *fr!().upvals.add($i as usize) }
|
|
};
|
|
}
|
|
// SAFETY: `Proto::verify` checked every constant index against the table.
|
|
macro_rules! kv {
|
|
($i:expr) => {{
|
|
let i = $i as usize;
|
|
debug_assert!(i < unsafe { (*fr!().proto).consts.len() });
|
|
unsafe { *k.add(i) }
|
|
}};
|
|
}
|
|
macro_rules! refresh {
|
|
() => {
|
|
// SAFETY: as above.
|
|
regs = unsafe { self.stack.as_mut_ptr().add(base!()) }
|
|
};
|
|
}
|
|
// SAFETY for `r!`: `regs` is current (see above), and `Proto::verify` checked that no
|
|
// instruction addresses a register at or past `maxstack`.
|
|
macro_rules! r {
|
|
($x:expr) => {
|
|
*{
|
|
let i = $x as usize;
|
|
debug_assert!(i < maxstack!() && base!() + i < self.stack.len());
|
|
unsafe { &mut *regs.add(i) }
|
|
}
|
|
};
|
|
}
|
|
macro_rules! rk {
|
|
($x:expr) => {{
|
|
let x: Rk = $x;
|
|
if x >= RK_CONST {
|
|
kv!(x - RK_CONST)
|
|
} else {
|
|
r!(x)
|
|
}
|
|
}};
|
|
}
|
|
// `rk!` by reference: a fast path reads the tag and payload in place instead of
|
|
// copying the value out.
|
|
macro_rules! rkref {
|
|
($x:expr) => {{
|
|
let x: Rk = $x;
|
|
if x >= RK_CONST {
|
|
let i = (x - RK_CONST) as usize;
|
|
debug_assert!(i < unsafe { (*fr!().proto).consts.len() });
|
|
// SAFETY: as `kv!`.
|
|
unsafe { &*k.add(i) }
|
|
} else {
|
|
let i = x as usize;
|
|
debug_assert!(i < maxstack!() && base!() + i < self.stack.len());
|
|
// SAFETY: as `r!`.
|
|
unsafe { &*regs.add(i) }
|
|
}
|
|
}};
|
|
}
|
|
macro_rules! rkreg {
|
|
($x:expr) => {{
|
|
let x: Rk = $x;
|
|
if x >= RK_CONST {
|
|
None
|
|
} else {
|
|
Some(x as usize)
|
|
}
|
|
}};
|
|
}
|
|
macro_rules! env {
|
|
() => {{
|
|
let fenv_gen = self.fenv_gen;
|
|
let f = fr_mut!();
|
|
if f.env_gen != fenv_gen {
|
|
f.env_gen = self.fenv_gen;
|
|
f.env = match self.heap.func(f.func) {
|
|
FuncObj::Lua(LuaFunc { env, .. }) => *env,
|
|
_ => unreachable!(),
|
|
};
|
|
}
|
|
f.env
|
|
}};
|
|
}
|
|
macro_rules! save {
|
|
() => {
|
|
fr_mut!().ip = ip
|
|
};
|
|
}
|
|
/// `check_gc` at a safe point: a collection drops the host closures it freed, whose
|
|
/// destructors may run Lua and move the stack, so the registers are refreshed after.
|
|
macro_rules! gc {
|
|
() => {
|
|
if self.heap.bytes() >= self.heap.trigger || self.gc_stress {
|
|
self.full_gc();
|
|
refresh!();
|
|
}
|
|
};
|
|
}
|
|
/// A call that may run Lua: the pc saved first, the registers refreshed after.
|
|
macro_rules! slow {
|
|
($e:expr) => {{
|
|
save!();
|
|
let v = $e;
|
|
refresh!();
|
|
v
|
|
}};
|
|
}
|
|
macro_rules! arith {
|
|
($a:expr, $b:expr, $c:expr, $op:tt, $kind:expr) => {{
|
|
let (x, y) = (r!($b), r!($c));
|
|
if let (Some(xn), Some(yn)) = (x.as_num(), y.as_num()) {
|
|
// Canonical numbers in, a canonical number out.
|
|
r!($a) = Value::num_raw(crate::number::x87_op(xn $op yn, xn, yn));
|
|
} else {
|
|
let v = slow!(self.arith($kind, x, y, (Some($b as usize), Some($c as usize))))?;
|
|
r!($a) = v;
|
|
}
|
|
}};
|
|
}
|
|
/// `K(b) op R(c)`, in source order for a metamethod and an error.
|
|
macro_rules! arithkr {
|
|
($a:expr, $kx:expr, $c:expr, $op:tt, $kind:expr) => {{
|
|
let (x, y) = (kv!($kx), r!($c));
|
|
if let (Some(xn), Some(yn)) = (x.as_num(), y.as_num()) {
|
|
r!($a) = Value::num_raw(crate::number::x87_op(xn $op yn, xn, yn));
|
|
} else {
|
|
let v = slow!(self.arith($kind, x, y, (None, Some($c as usize))))?;
|
|
r!($a) = v;
|
|
}
|
|
}};
|
|
}
|
|
/// The instructions run since `seg`, the one just fetched included, for the hook; one
|
|
/// when the hook was set during this frame's run.
|
|
macro_rules! ran {
|
|
() => {
|
|
if seg.is_null() {
|
|
1
|
|
} else {
|
|
// SAFETY: `seg_code` is the code of the frame that set `seg`: this one,
|
|
// or one that returned since with nothing collected in between; a frame
|
|
// entered with no hook set clears `seg`.
|
|
ran_since(seg, ip, unsafe { &*seg_code })
|
|
}
|
|
};
|
|
}
|
|
/// A run starts here, in the running frame's code.
|
|
macro_rules! mark {
|
|
() => {{
|
|
seg = ip;
|
|
seg_code = fr!().proto;
|
|
}};
|
|
}
|
|
/// A jump forward: the run so far is counted when a hook is set, so a run holds only
|
|
/// what ran ([`Proto::uncounted`] is found in it), and the hook waits for the next
|
|
/// back edge, call or return.
|
|
macro_rules! skip {
|
|
($off:expr) => {{
|
|
let off = $off as isize;
|
|
if SLOW && self.hooked {
|
|
let n = ran!();
|
|
// SAFETY: the compiler's jumps land inside the function.
|
|
ip = unsafe { ip.offset(off) };
|
|
mark!();
|
|
self.hook_add(n);
|
|
} else {
|
|
// SAFETY: as above.
|
|
ip = unsafe { ip.offset(off) };
|
|
}
|
|
}};
|
|
}
|
|
macro_rules! jump {
|
|
($off:expr) => {{
|
|
let off = $off as isize;
|
|
if off < 0 && self.hooked {
|
|
if !SLOW {
|
|
// A hook was set: the slow loop goes on from the loop's head.
|
|
// SAFETY: as `skip!`.
|
|
ip = unsafe { ip.offset(off) };
|
|
save!();
|
|
return Ok(false);
|
|
}
|
|
// A loop's back edge: the place a loop is charged.
|
|
let n = ran!();
|
|
// SAFETY: `Proto::verify` checked that every jump lands inside the code.
|
|
ip = unsafe { ip.offset(off) };
|
|
mark!();
|
|
slow!(self.hook_count(n))?;
|
|
} else {
|
|
skip!(off);
|
|
}
|
|
}};
|
|
}
|
|
/// 5.0's `OP_RETURN`: `n` values from `R(a)` to the caller's frame.
|
|
macro_rules! ret {
|
|
($a:expr, $n:expr) => {{
|
|
let a = $a;
|
|
let n: usize = $n;
|
|
let fi = self.frames.len() - 1;
|
|
let base = base!();
|
|
let first = base + a as usize;
|
|
if !self.open.is_empty() {
|
|
self.close_upvals(base);
|
|
}
|
|
// The returning frame's two fields that matter, read in place; its slot sits
|
|
// just below `regs`.
|
|
let (nresults, boundary) = {
|
|
let me = self.frames.last().unwrap();
|
|
(me.nresults, me.boundary)
|
|
};
|
|
let func_idx = base - 1;
|
|
self.frames.truncate(fi);
|
|
if nresults == 1 && n >= 1 {
|
|
// One result, the common call in an expression, into the callee's slot
|
|
// just below this frame's registers.
|
|
// SAFETY: `base` is at least 1 for a Lua frame; `regs` is still the
|
|
// returning frame's, and `a` one of its registers (`r!` would check it
|
|
// against the caller's frame, now on top).
|
|
unsafe { *regs.sub(1) = *regs.add(a as usize) };
|
|
} else if nresults == 0 {
|
|
// None, a call statement.
|
|
self.top = func_idx;
|
|
} else {
|
|
self.poscall(func_idx, first, n, nresults);
|
|
}
|
|
if boundary {
|
|
if nresults == 1 && n >= 1 {
|
|
self.top = func_idx + 1;
|
|
}
|
|
// The frame's last run; a return into Lua charges it as it re-enters.
|
|
if SLOW && self.hooked {
|
|
let n = ran!();
|
|
self.hook_count(n)?;
|
|
}
|
|
return Ok(true);
|
|
}
|
|
// Back in the calling Lua frame: a fixed result count restores its top.
|
|
let caller = self.frames.last().unwrap();
|
|
if nresults >= 0 {
|
|
self.top = caller.base() + caller.maxstack as usize;
|
|
}
|
|
continue 'frame;
|
|
}};
|
|
}
|
|
macro_rules! arithk {
|
|
($a:expr, $b:expr, $kx:expr, $op:tt, $kind:expr) => {{
|
|
let y = kv!($kx);
|
|
if let (Some(x), Some(yn)) = (r!($b).as_num(), y.as_num()) {
|
|
r!($a) = Value::num_raw(crate::number::x87_op(x $op yn, x, yn));
|
|
} else {
|
|
let x = r!($b);
|
|
let v = slow!(self.arith($kind, x, y, (Some($b as usize), None)))?;
|
|
r!($a) = v;
|
|
}
|
|
}};
|
|
}
|
|
/// A string key missing from table `tr`, through its metatable: the common one level
|
|
/// (`__index` a table holding the key) inline, a longer chain by `index_meta_fast`.
|
|
/// `None` when the full `index` must run.
|
|
macro_rules! meta_field {
|
|
($tr:expr, $key:expr) => {{
|
|
let (tr, key): (TableRef, Value) = ($tr, $key);
|
|
match self.heap.table(tr).meta {
|
|
None => Some(Value::Nil),
|
|
Some(m) => {
|
|
let idx = self.heap.table(m).get_str(Value::Str(self.names.index));
|
|
match idx.as_table() {
|
|
Some(t2) => {
|
|
let v = self.heap.table(t2).get_str(key);
|
|
if !v.is_nil() {
|
|
Some(v)
|
|
} else {
|
|
self.index_meta_fast(tr, &key)
|
|
}
|
|
}
|
|
None if idx.is_nil() => Some(Value::Nil),
|
|
None => None,
|
|
}
|
|
}
|
|
}
|
|
}};
|
|
}
|
|
/// A comparison with one constant side.
|
|
macro_rules! cmpk {
|
|
($cmp:expr, $cond:expr, $x:expr, $y:expr, $off:expr) => {{
|
|
let (x, y): (Value, Value) = ($x, $y);
|
|
let res = match ($cmp, x.as_num(), y.as_num()) {
|
|
(Cmp::Lt, Some(p), Some(q)) => p < q,
|
|
(Cmp::Le, Some(p), Some(q)) => p <= q,
|
|
// A constant is never a table or userdata, so `__eq` never applies.
|
|
(Cmp::Eq, _, _) => x.raw_eq(y),
|
|
(Cmp::Lt, _, _) => slow!(self.less_than(x, y))?,
|
|
(Cmp::Le, _, _) => slow!(self.less_equal(x, y))?,
|
|
};
|
|
if res == $cond {
|
|
jump!($off);
|
|
}
|
|
}};
|
|
}
|
|
loop {
|
|
debug_assert!(unsafe { (*fr!().proto).code.as_ptr_range().contains(&ip) });
|
|
// SAFETY: `Proto::verify` checked that every function ends in a return and
|
|
// every jump lands inside it.
|
|
let op = unsafe { *ip };
|
|
ip = unsafe { ip.add(1) };
|
|
match op {
|
|
Op::Move(a, b) => r!(a) = r!(b),
|
|
Op::LoadK(a, kx) => r!(a) = kv!(kx),
|
|
Op::LoadNil(a, b) => {
|
|
for i in a..=b {
|
|
r!(i) = Value::Nil;
|
|
}
|
|
}
|
|
Op::LoadBool(a, b) => r!(a) = Value::Bool(b),
|
|
Op::GetUpval(a, u) => {
|
|
let v = match self.heap.upval(upval!(u)) {
|
|
// An open upvalue is a stack slot, below this frame's registers or
|
|
// among them: addressed from `regs`, which is current.
|
|
UpvalObj::Open(i) => {
|
|
debug_assert!((i as usize) < self.stack.len());
|
|
// SAFETY: an open upvalue's slot is inside the stack.
|
|
unsafe { *regs.sub(base!()).add(i as usize) }
|
|
}
|
|
UpvalObj::Closed(v) => v,
|
|
};
|
|
r!(a) = v;
|
|
}
|
|
Op::SetUpval(a, u) => {
|
|
let v = r!(a);
|
|
let uv = upval!(u);
|
|
self.upval_set(uv, v);
|
|
refresh!();
|
|
}
|
|
Op::GetGlobal(a, kx) => {
|
|
let env = env!();
|
|
let key = kv!(kx);
|
|
let tab = self.heap.table(env);
|
|
let v = tab.get(key);
|
|
if !v.is_nil() || tab.meta.is_none() {
|
|
r!(a) = v;
|
|
} else {
|
|
let v = slow!(self.index_op(Value::Table(env), key, None))?;
|
|
r!(a) = v;
|
|
}
|
|
}
|
|
Op::SetGlobal(a, kx) => {
|
|
let env = env!();
|
|
let v = r!(a);
|
|
let key = kv!(kx);
|
|
let tab = self.heap.table_mut(env);
|
|
if tab.meta.is_none() && tab.set(key, v).is_ok() {
|
|
continue;
|
|
}
|
|
slow!(self.setindex(Value::Table(env), key, v, None))?;
|
|
}
|
|
Op::GetTable(a, b, c) => {
|
|
if let Some(tr) = r!(b).as_table() {
|
|
let tab = self.heap.table(tr);
|
|
let v = tab.get_ref(rkref!(c));
|
|
if !v.is_nil() || tab.meta.is_none() {
|
|
r!(a) = v;
|
|
continue;
|
|
}
|
|
if let Some(v) = self.index_meta_fast(tr, rkref!(c)) {
|
|
r!(a) = v;
|
|
continue;
|
|
}
|
|
}
|
|
let (t, key) = (r!(b), rk!(c));
|
|
let v = slow!(self.index_op(t, key, Some(b as usize)))?;
|
|
r!(a) = v;
|
|
}
|
|
Op::GetField(a, b, kx) => {
|
|
let key = kv!(kx);
|
|
if let Some(tr) = r!(b).as_table() {
|
|
let tab = self.heap.table(tr);
|
|
let v = tab.get_str(key);
|
|
if !v.is_nil() || tab.meta.is_none() {
|
|
r!(a) = v;
|
|
continue;
|
|
}
|
|
if let Some(v) = meta_field!(tr, key) {
|
|
r!(a) = v;
|
|
continue;
|
|
}
|
|
}
|
|
let t = r!(b);
|
|
let v = slow!(self.index_op(t, key, Some(b as usize)))?;
|
|
r!(a) = v;
|
|
}
|
|
Op::SelfField(a, b, kx) => {
|
|
let key = kv!(kx);
|
|
let obj = r!(b);
|
|
r!(a + 1) = obj;
|
|
if let Some(tr) = obj.as_table() {
|
|
let tab = self.heap.table(tr);
|
|
let v = tab.get_str(key);
|
|
if !v.is_nil() || tab.meta.is_none() {
|
|
r!(a) = v;
|
|
continue;
|
|
}
|
|
if let Some(v) = meta_field!(tr, key) {
|
|
r!(a) = v;
|
|
continue;
|
|
}
|
|
}
|
|
let v = slow!(self.index_op(obj, key, Some(b as usize)))?;
|
|
r!(a) = v;
|
|
}
|
|
Op::SetField(a, kx, c) => {
|
|
let key = kv!(kx);
|
|
let v = rk!(c);
|
|
if let Some(tr) = r!(a).as_table() {
|
|
let tab = self.heap.table_mut(tr);
|
|
if tab.meta.is_none() {
|
|
if tab.set(key, v).is_ok() {
|
|
continue;
|
|
}
|
|
} else if let Some(s) = tab.find(key) {
|
|
// As `SetTable`: an existing value is overwritten raw.
|
|
if !tab.slot(s).is_nil() {
|
|
*tab.slot_mut(s) = v;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
let t = r!(a);
|
|
slow!(self.setindex(t, key, v, Some(a as usize)))?;
|
|
}
|
|
Op::SetTable(a, b, c) => {
|
|
let key = rk!(b);
|
|
let v = rk!(c);
|
|
if let Some(tr) = r!(a).as_table() {
|
|
let tab = self.heap.table_mut(tr);
|
|
if tab.meta.is_none() {
|
|
if tab.set(key, v).is_ok() {
|
|
continue;
|
|
}
|
|
} else if let Some(s) = tab.find(key) {
|
|
// An existing value is overwritten raw: `__newindex` is only
|
|
// consulted for a nil one.
|
|
if !tab.slot(s).is_nil() {
|
|
*tab.slot_mut(s) = v;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
let t = r!(a);
|
|
slow!(self.setindex(t, key, v, Some(a as usize)))?;
|
|
}
|
|
Op::NewTable(a, b, c) => {
|
|
// `luaH_new`'s `setnodevector` (`0x6fa549`) raises past `MAXBITS`.
|
|
if let Err(e) = crate::table::check_lsize(c) {
|
|
save!();
|
|
return Err(self.rt_error(e.message()));
|
|
}
|
|
let t = self.heap.new_table(b as usize, c);
|
|
r!(a) = Value::Table(t);
|
|
save!();
|
|
self.top = base!() + maxstack!();
|
|
gc!();
|
|
}
|
|
Op::SelfOp(a, b, c) => {
|
|
let obj = r!(b);
|
|
r!(a + 1) = obj;
|
|
if let Some(tr) = r!(b).as_table() {
|
|
let tab = self.heap.table(tr);
|
|
let v = tab.get_ref(rkref!(c));
|
|
if !v.is_nil() || tab.meta.is_none() {
|
|
r!(a) = v;
|
|
continue;
|
|
}
|
|
if let Some(v) = self.index_meta_fast(tr, rkref!(c)) {
|
|
r!(a) = v;
|
|
continue;
|
|
}
|
|
}
|
|
let key = rk!(c);
|
|
let v = slow!(self.index_op(obj, key, Some(b as usize)))?;
|
|
r!(a) = v;
|
|
}
|
|
Op::Add(a, b, c) => arith!(a, b, c, +, Arith::Add),
|
|
Op::Sub(a, b, c) => arith!(a, b, c, -, Arith::Sub),
|
|
Op::Mul(a, b, c) => arith!(a, b, c, *, Arith::Mul),
|
|
Op::Div(a, b, c) => arith!(a, b, c, /, Arith::Div),
|
|
Op::AddK(a, b, kx) => arithk!(a, b, kx, +, Arith::Add),
|
|
Op::SubK(a, b, kx) => arithk!(a, b, kx, -, Arith::Sub),
|
|
Op::MulK(a, b, kx) => arithk!(a, b, kx, *, Arith::Mul),
|
|
Op::DivK(a, b, kx) => arithk!(a, b, kx, /, Arith::Div),
|
|
Op::AddKR(a, kx, c) => arithkr!(a, kx, c, +, Arith::Add),
|
|
Op::SubKR(a, kx, c) => arithkr!(a, kx, c, -, Arith::Sub),
|
|
Op::MulKR(a, kx, c) => arithkr!(a, kx, c, *, Arith::Mul),
|
|
Op::DivKR(a, kx, c) => arithkr!(a, kx, c, /, Arith::Div),
|
|
Op::JmpCmpK(cj, b, kx, off) => cmpk!(cj.cmp(), cj.cond(), r!(b), kv!(kx), off),
|
|
Op::JmpCmpKR(cj, kx, c, off) => cmpk!(cj.cmp(), cj.cond(), kv!(kx), r!(c), off),
|
|
Op::Pow(a, b, c) => {
|
|
// The client's `OP_POW` (`0x6f8e47`): always `Arith`, which calls
|
|
// the global `__pow` for numbers.
|
|
let (x, y) = (rk!(b), rk!(c));
|
|
let v = slow!(self.arith(Arith::Pow, x, y, (rkreg!(b), rkreg!(c))))?;
|
|
r!(a) = v;
|
|
}
|
|
Op::Unm(a, b) => {
|
|
let x = r!(b);
|
|
match self.tonumber(x) {
|
|
Some(n) => r!(a) = Value::Num(-n),
|
|
None => {
|
|
// The client's `OP_UNM` (`0x6f8ea1`): `call_binTM` with the operand
|
|
// and a nil temporary (`0x6f8edc`), so `__unm` gets (operand, nil).
|
|
let unm = self.names.unm;
|
|
match slow!(self.bin_tm(x, Value::Nil, unm))? {
|
|
Some(v) => r!(a) = v,
|
|
None => {
|
|
return Err(self.type_error(
|
|
x,
|
|
Some(b as usize),
|
|
"perform arithmetic on",
|
|
))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Op::Not(a, b) => r!(a) = Value::Bool(!r!(b).truthy()),
|
|
Op::Concat(a, b, c) => {
|
|
self.top = base!() + maxstack!();
|
|
let b0 = base!();
|
|
slow!(self.concat(b0 + b as usize, b0 + c as usize))?;
|
|
r!(a) = r!(b);
|
|
gc!();
|
|
}
|
|
Op::Jmp(off) => jump!(off),
|
|
Op::JmpCmp(cj, b, c, off) => {
|
|
let (cmp, cond) = (cj.cmp(), cj.cond());
|
|
let res = match (cmp, r!(b).as_num(), r!(c).as_num()) {
|
|
(Cmp::Lt, Some(p), Some(q)) => p < q,
|
|
(Cmp::Le, Some(p), Some(q)) => p <= q,
|
|
(Cmp::Eq, Some(p), Some(q)) => p == q,
|
|
_ => {
|
|
let (x, y) = (r!(b), r!(c));
|
|
match cmp {
|
|
Cmp::Eq => x.raw_eq(y) || slow!(self.equals(x, y))?,
|
|
Cmp::Lt => slow!(self.less_than(x, y))?,
|
|
Cmp::Le => slow!(self.less_equal(x, y))?,
|
|
}
|
|
}
|
|
};
|
|
if res == cond {
|
|
jump!(off);
|
|
}
|
|
}
|
|
Op::JmpIf(a, cond, off) => {
|
|
if r!(a).truthy() == cond {
|
|
jump!(off);
|
|
}
|
|
}
|
|
Op::TestSet(a, b, cond, off) => {
|
|
let v = r!(b);
|
|
if v.truthy() == cond {
|
|
r!(a) = v;
|
|
// A value's test jumps forward, never a back edge.
|
|
skip!(off);
|
|
}
|
|
}
|
|
Op::Call(a, b, c) => {
|
|
let func_idx = base!() + a as usize;
|
|
let nargs = if b == 0 {
|
|
self.top - func_idx - 1
|
|
} else {
|
|
b as usize - 1
|
|
};
|
|
save!();
|
|
if self.hooked {
|
|
if !SLOW {
|
|
// A hook was set: the slow loop makes this call.
|
|
fr_mut!().ip = unsafe { ip.sub(1) };
|
|
return Ok(false);
|
|
}
|
|
let n = ran!();
|
|
mark!();
|
|
slow!(self.hook_count(n))?;
|
|
}
|
|
// A Lua function without `...`: the frame is pushed here, as C Lua's
|
|
// `luaD_precall` does for it, without the general path.
|
|
if let Some(f) = r!(a).as_func() {
|
|
if let FuncObj::Lua(LuaFunc {
|
|
proto: p,
|
|
upvals: fup,
|
|
env: fenv,
|
|
shape,
|
|
}) = self.heap.func(f)
|
|
{
|
|
// `fi + 1` frames are live: the depth without reading the
|
|
// frame list.
|
|
if !shape.precall && self.frames.len() < self.ci_limit {
|
|
let np = shape.nparams as usize;
|
|
let consts = shape.consts;
|
|
let nbase = func_idx + 1;
|
|
let ftop = nbase + shape.maxstack as usize;
|
|
// `luaD_precall`'s check (`0x6f60c1`) and the frame's own
|
|
// registers, in one compare.
|
|
let need50 =
|
|
nbase + nargs + shape.c_maxstack as usize + EXTRA_STACK + 2;
|
|
let frame = Frame {
|
|
func: f,
|
|
base: nbase as u32,
|
|
ip: shape.code,
|
|
nresults: c as i32 - 1,
|
|
consts: shape.consts,
|
|
maxstack: shape.maxstack,
|
|
source: shape.source,
|
|
proto: Rc::as_ptr(p),
|
|
upvals: fup.as_ptr(),
|
|
env: *fenv,
|
|
env_gen: self.fenv_gen,
|
|
tailcalls: 0,
|
|
boundary: false,
|
|
};
|
|
if self.stack.len() < need50.max(ftop + 1) {
|
|
self.checkstack(nbase + nargs, shape.c_maxstack as usize);
|
|
self.ensure(ftop + 1);
|
|
}
|
|
if SLOW && nbase < self.clear_below {
|
|
// As `precall`: every register past the arguments.
|
|
// SAFETY: the closure being called holds its code.
|
|
let cmax = unsafe { &*frame.proto }.c_maxstack as usize;
|
|
for v in
|
|
&mut self.stack[nbase + nargs.min(np)..nbase + cmax]
|
|
{
|
|
*v = Value::Nil;
|
|
}
|
|
} else if nargs < np {
|
|
for v in &mut self.stack[nbase + nargs..nbase + np] {
|
|
*v = Value::Nil;
|
|
}
|
|
}
|
|
self.top = ftop;
|
|
self.frames.push(frame);
|
|
// Enter the callee from what is in hand, not from the
|
|
// frame just written.
|
|
ip = frame.ip;
|
|
k = consts;
|
|
self.heap.cur_source = frame.source;
|
|
if SLOW {
|
|
if self.hooked {
|
|
seg = ip;
|
|
seg_code = frame.proto;
|
|
self.prof_switch();
|
|
} else {
|
|
seg = std::ptr::null();
|
|
}
|
|
}
|
|
refresh!();
|
|
continue;
|
|
}
|
|
} else if self.frames.len() < self.ci_limit {
|
|
// A native: `precall`'s native half, without its dispatch.
|
|
self.call_native(f, func_idx, nargs, c as i32 - 1)?;
|
|
refresh!();
|
|
if c != 0 {
|
|
self.top = base!() + maxstack!();
|
|
}
|
|
gc!();
|
|
continue;
|
|
}
|
|
}
|
|
match self.precall(func_idx, nargs, c as i32 - 1)? {
|
|
// A function that needs its frames cleared runs in the slow loop.
|
|
Called::Lua if !SLOW && self.clear_below != 0 => return Ok(false),
|
|
Called::Lua => continue 'frame,
|
|
Called::Done => {
|
|
refresh!();
|
|
if c != 0 {
|
|
self.top = base!() + maxstack!();
|
|
}
|
|
gc!();
|
|
}
|
|
}
|
|
}
|
|
Op::TailCall(a, b) => {
|
|
let func_idx = base!() + a as usize;
|
|
let mut nargs = if b == 0 {
|
|
self.top - func_idx - 1
|
|
} else {
|
|
b as usize - 1
|
|
};
|
|
save!();
|
|
// A call like any other for the budget: a tail-recursive loop passes
|
|
// no back edge and no `Call`.
|
|
if self.hooked {
|
|
if !SLOW {
|
|
// A hook was set: the slow loop makes this call.
|
|
fr_mut!().ip = unsafe { ip.sub(1) };
|
|
return Ok(false);
|
|
}
|
|
let n = ran!();
|
|
mark!();
|
|
slow!(self.hook_count(n))?;
|
|
}
|
|
// 5.1's `OP_TAILCALL` resolves `__call` first, so a callable table's
|
|
// Lua `__call` replaces this frame too.
|
|
if r!(a).as_func().is_none() {
|
|
nargs = slow!(self.tryfunc(func_idx, nargs))?;
|
|
}
|
|
// A Lua callee replaces this frame; a native runs and its results
|
|
// return through the `Return` that follows.
|
|
let is_lua = matches!((r!(a)).unpack(),
|
|
V::Func(f) if matches!(self.heap.func(f), FuncObj::Lua(_))
|
|
);
|
|
if !is_lua {
|
|
match self.precall(func_idx, nargs, -1)? {
|
|
Called::Lua if !SLOW && self.clear_below != 0 => return Ok(false),
|
|
Called::Lua => continue 'frame,
|
|
Called::Done => {
|
|
refresh!();
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
// `luaD_precall`'s `inc_ci` counts the callee while this frame is still
|
|
// there, as C replaces it only after.
|
|
if self.frames.len() >= self.ci_limit {
|
|
// Saved above; on success the stack is as it was.
|
|
self.grow_ci()?;
|
|
}
|
|
let b0 = base!();
|
|
self.close_upvals(b0);
|
|
let dest = fr!().func_idx();
|
|
if SLOW && dest < self.clear_below {
|
|
// `OP_TAILCALL`'s order, where code may read what it leaves: the
|
|
// callee's frame is made where its function sits, registers cleared,
|
|
// then moved down whole over this one, and the place it was made
|
|
// keeps its copy.
|
|
let nres = fr!().nresults;
|
|
self.precall_clearing(func_idx, nargs, nres, true)?;
|
|
let mut new = self.frames.pop().unwrap();
|
|
let me = self.frames.pop().unwrap();
|
|
// C's frame, to `ci->top`; benilla's spare slots above stay as they
|
|
// were.
|
|
// SAFETY: the closure just called holds its code (`Frame::proto`).
|
|
let cmax = unsafe { &*new.proto }.c_maxstack as usize;
|
|
self.stack.copy_within(func_idx..func_idx + 1 + cmax, dest);
|
|
self.top -= func_idx - dest;
|
|
new.base = dest as u32 + 1;
|
|
// 5.1's `ci->tailcalls++` is a C int; saturated here so 2^32 tail
|
|
// calls cannot overflow, a count no traceback reaches.
|
|
new.tailcalls = me.tailcalls.saturating_add(1);
|
|
new.boundary = me.boundary;
|
|
self.frames.push(new);
|
|
continue 'frame;
|
|
}
|
|
// The client's `OP_TAILCALL` makes the call where the function sits
|
|
// (`0x6f922c`) and moves the frame down after (`0x6f9761`..`0x6f97ef`), so
|
|
// its stack check is made there.
|
|
if let V::Func(f) = r!(a).unpack() {
|
|
if let FuncObj::Lua(LuaFunc { proto, .. }) = self.heap.func(f) {
|
|
let (top, n) = (
|
|
precall_top50(func_idx + 1, nargs, proto),
|
|
proto.c_maxstack as usize,
|
|
);
|
|
self.checkstack(top, n);
|
|
}
|
|
}
|
|
let me = self.frames.pop().unwrap();
|
|
self.stack.copy_within(func_idx..func_idx + nargs + 1, dest);
|
|
self.top = dest + nargs + 1;
|
|
match self.precall(dest, nargs, me.nresults) {
|
|
Ok(Called::Lua) => {
|
|
let f = self.frames.last_mut().unwrap();
|
|
// 5.1's `ci->tailcalls++` is a C int; saturated here so 2^32
|
|
// tail calls cannot overflow, a count no traceback reaches.
|
|
f.tailcalls = me.tailcalls.saturating_add(1);
|
|
f.boundary = me.boundary;
|
|
if !SLOW && self.clear_below != 0 {
|
|
return Ok(false);
|
|
}
|
|
continue 'frame;
|
|
}
|
|
Ok(Called::Done) => unreachable!("a Lua callee"),
|
|
Err(e) => {
|
|
self.frames.push(me);
|
|
return Err(e);
|
|
}
|
|
}
|
|
}
|
|
Op::Return(a, b) => {
|
|
let n = if b == 0 {
|
|
self.top - (base!() + a as usize)
|
|
} else {
|
|
b as usize - 1
|
|
};
|
|
ret!(a, n);
|
|
}
|
|
Op::Return1(a) => ret!(a, 1),
|
|
// The client's `OP_FORLOOP` (`0x6f9262`): the loop's own variable is the
|
|
// counter, so the body may move it; each pass checks it is a number, makes
|
|
// the limit and step numbers in place, steps it and, while within the
|
|
// limit, writes it back and jumps. `ForLoopUp` knows the step is a positive
|
|
// constant.
|
|
Op::ForLoopUp(a, off) => {
|
|
let (idx, limit, _) = match (r!(a).as_num(), r!(a + 1).as_num()) {
|
|
(Some(i), Some(l)) => (i, l, 0.0),
|
|
_ => slow!(self.for_controls(base!() + a as usize))?,
|
|
};
|
|
let idx = idx + r!(a + 2).as_num().unwrap_or(f64::NAN);
|
|
if idx <= limit {
|
|
// Canonical numbers in, a canonical number out.
|
|
r!(a) = Value::num_raw(idx);
|
|
jump!(off);
|
|
}
|
|
}
|
|
Op::ForLoop(a, off) => {
|
|
let (idx, limit, step) =
|
|
match (r!(a).as_num(), r!(a + 1).as_num(), r!(a + 2).as_num()) {
|
|
(Some(i), Some(l), Some(s)) => (i, l, s),
|
|
_ => slow!(self.for_controls(base!() + a as usize))?,
|
|
};
|
|
let idx = idx + step;
|
|
if if step > 0.0 {
|
|
idx <= limit
|
|
} else {
|
|
limit <= idx
|
|
} {
|
|
// Canonical numbers in, a canonical number out.
|
|
r!(a) = Value::num_raw(idx);
|
|
jump!(off);
|
|
}
|
|
}
|
|
// The client's `TFORPREP` (`0x6f94a2`): a table generator becomes the raw
|
|
// global `next` over it once, at loop entry. The C build's patch swaps at
|
|
// every `TFORLOOP` instead, so where the global `next` is a callable table
|
|
// it raises `invalid key to 'next'` after one pass; the single swap here
|
|
// moves benilla-lua toward the reference.
|
|
Op::TForPrep(a, off) => {
|
|
if let V::Table(_) = (r!(a)).unpack() {
|
|
r!(a + 1) = r!(a);
|
|
let g = self.globals;
|
|
r!(a) = self.heap.table(g).get(Value::Str(self.names.next));
|
|
}
|
|
jump!(off);
|
|
}
|
|
// The client's `OP_TFORLOOP` (`0x6f933b`): the generator, the state and
|
|
// the first variable, the control, copied above the variables
|
|
// (`0x6f9344`..`0x6f93e0`), the call there for `nv` results (`0x6f940f`),
|
|
// the results moved down into the variables (`0x6f9445`..`0x6f9464`), and
|
|
// the loop ends when the first is nil (`0x6f9490`).
|
|
Op::TForLoop(a, nvar) => {
|
|
let nv = nvar as usize;
|
|
let ctl = a as usize + 2;
|
|
let cb = ctl + nv;
|
|
// The built-in `next` over a table, stepped without a call: what the
|
|
// call would do, minus the frame. An invalid key takes the call, which
|
|
// raises from inside `next` as before.
|
|
if r!(a) == self.builtin_next && !self.builtin_next.is_nil() {
|
|
let state = r!(a + 1);
|
|
if let V::Table(t) = state.unpack() {
|
|
if let Ok(step) = self.heap.table(t).next(r!(ctl)) {
|
|
// The registers from `cb` up as the call leaves them: the
|
|
// state and control copied to `cb + 1` and `cb + 2`, then
|
|
// `lua_next` writes the key over the control and the value
|
|
// above it (or `next` one nil there), and `nv` results move
|
|
// down to `cb`, nil past those given; those then copy into
|
|
// the variables. A body can read past its variables
|
|
// (`Proto::clears`).
|
|
let (k, v, n) = match step {
|
|
Some((k, v)) => (k, v, 2),
|
|
None => (Value::Nil, Value::Nil, 1),
|
|
};
|
|
let left = [k, state, k, v];
|
|
let res = [k, v];
|
|
for i in 0..nv.max(if n == 2 { 4 } else { 3 }) {
|
|
let x = if i < nv {
|
|
if i < n {
|
|
res[i]
|
|
} else {
|
|
Value::Nil
|
|
}
|
|
} else {
|
|
left[i]
|
|
};
|
|
if cb + i < maxstack!() {
|
|
r!(cb + i) = x;
|
|
}
|
|
if i < nv {
|
|
r!(ctl + i) = x;
|
|
}
|
|
}
|
|
if k.is_nil() {
|
|
skip!(1);
|
|
} else {
|
|
// SAFETY: a `TForLoop` is always followed by its `Jmp`.
|
|
let Op::Jmp(back) = (unsafe { *ip }) else {
|
|
unreachable!()
|
|
};
|
|
jump!(1 + back);
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
r!(cb + 2) = r!(ctl);
|
|
r!(cb + 1) = r!(a + 1);
|
|
r!(cb) = r!(a);
|
|
self.top = base!() + cb + 3;
|
|
let call_at = base!() + cb;
|
|
slow!(self.call_at(call_at, 2, nv as i32))?;
|
|
self.top = base!() + maxstack!();
|
|
for i in (0..nv).rev() {
|
|
r!(ctl + i) = r!(cb + i);
|
|
}
|
|
if r!(ctl).is_nil() {
|
|
skip!(1);
|
|
} else {
|
|
// SAFETY: a `TForLoop` is always followed by its `Jmp`.
|
|
let Op::Jmp(back) = (unsafe { *ip }) else {
|
|
unreachable!()
|
|
};
|
|
jump!(1 + back);
|
|
}
|
|
}
|
|
// 5.0's `OP_SETLIST` (`0x6f955a`): the items stored from the last down by
|
|
// `luaH_setnum`, into the array part as `OP_NEWTABLE` sized it or else as
|
|
// new keys, which may rehash.
|
|
Op::SetList(a, first, n) => {
|
|
let V::Table(t) = (r!(a)).unpack() else {
|
|
unreachable!()
|
|
};
|
|
for i in (1..=n as usize).rev() {
|
|
let v = r!(a as usize + i);
|
|
if let Err(e) = self
|
|
.heap
|
|
.table_mut(t)
|
|
.set_int((first as usize + i) as i32, v)
|
|
{
|
|
save!();
|
|
return Err(self.rt_error(e.message()));
|
|
}
|
|
}
|
|
}
|
|
Op::SetListTop(a, first) => {
|
|
let V::Table(t) = (r!(a)).unpack() else {
|
|
unreachable!()
|
|
};
|
|
let n = self.top - (base!() + a as usize) - 1;
|
|
for i in (1..=n).rev() {
|
|
let v = self.stack[base!() + a as usize + i];
|
|
if let Err(e) = self
|
|
.heap
|
|
.table_mut(t)
|
|
.set_int((first as usize + i) as i32, v)
|
|
{
|
|
save!();
|
|
return Err(self.rt_error(e.message()));
|
|
}
|
|
}
|
|
self.top = base!() + maxstack!();
|
|
}
|
|
Op::Close(a) => {
|
|
let b0 = base!();
|
|
self.close_upvals(b0 + a as usize);
|
|
refresh!();
|
|
}
|
|
Op::Closure(a, px) => {
|
|
// SAFETY: as `Frame::proto`.
|
|
let p = unsafe { &*fr!().proto }.protos[px as usize].clone();
|
|
let env = env!();
|
|
let b0 = base!();
|
|
let mut ups = Vec::with_capacity(p.upvals.len());
|
|
for &(in_stack, idx) in &p.upvals {
|
|
ups.push(if in_stack {
|
|
self.find_upval(b0 + idx as usize)
|
|
} else {
|
|
upval!(idx)
|
|
});
|
|
}
|
|
let f = self.heap.new_func(FuncObj::Lua(LuaFunc::new(
|
|
p,
|
|
crate::heap::Upvals::new(ups),
|
|
env,
|
|
)));
|
|
r!(a) = Value::Func(f);
|
|
save!();
|
|
self.top = base!() + maxstack!();
|
|
gc!();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The instructions 5.1 runs from `seg` to `ip` in `p`'s code, for the budget hook
|
|
/// ([`Proto::uncounted`] left out); 1 for a run that started before the hook was set, whose
|
|
/// start the VM did not keep.
|
|
#[cold]
|
|
fn ran_since(seg: *const Op, ip: *const Op, p: &Proto) -> u64 {
|
|
let code = p.code.as_ptr_range();
|
|
let (s, i) = (seg as usize, ip as usize);
|
|
if s < code.start as usize || s > i || i > code.end as usize {
|
|
return 1;
|
|
}
|
|
let size = std::mem::size_of::<Op>();
|
|
let (s, i) = (
|
|
(s - code.start as usize) / size,
|
|
(i - code.start as usize) / size,
|
|
);
|
|
let free = if p.uncounted.is_empty() {
|
|
0
|
|
} else {
|
|
let at = |pc: usize| p.uncounted.partition_point(|&u| (u as usize) < pc);
|
|
at(i) - at(s)
|
|
};
|
|
(i - s - free) as u64
|
|
}
|
|
|
|
/// `getobjname` (`0x6fc4c0`) over a prototype: a local's name, else what a symbolic run from the
|
|
/// start says last wrote `reg` before `pc`. Names are C strings there, so a constant's stops at a NUL.
|
|
fn objname_in(p: &Proto, heap: &Heap, pc: usize, reg: usize) -> Option<(&'static str, Vec<u8>)> {
|
|
if let Some(n) = p.local_name(reg as Reg, pc) {
|
|
return Some(("local", n.to_vec()));
|
|
}
|
|
let last = symbexec(p, pc, reg)?;
|
|
let cstr = |s: &[u8]| crate::lex::cstr(s).to_vec();
|
|
let kname = |c: Rk| -> Vec<u8> {
|
|
if c >= RK_CONST {
|
|
if let V::Str(s) = p.consts[(c - RK_CONST) as usize].unpack() {
|
|
return cstr(heap.str(s));
|
|
}
|
|
}
|
|
b"?".to_vec()
|
|
};
|
|
match p.code[last] {
|
|
Op::GetGlobal(_, kx) => match p.consts[kx as usize].unpack() {
|
|
V::Str(s) => Some(("global", cstr(heap.str(s)))),
|
|
_ => None,
|
|
},
|
|
Op::Move(a, b) if (b as usize) < (a as usize) => objname_in(p, heap, pc, b as usize),
|
|
// The client's `getobjname` has no `OP_GETUPVAL` arm (`0x6fc5e0`): an upvalue is unnamed.
|
|
Op::GetTable(_, _, c) => Some(("field", kname(c))),
|
|
Op::GetField(_, _, kx) => Some(("field", kname(kx + RK_CONST))),
|
|
Op::SelfOp(_, _, c) => Some(("method", kname(c))),
|
|
Op::SelfField(_, _, kx) => Some(("method", kname(kx + RK_CONST))),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// 5.1's `symbexec`: the instruction that last wrote `reg` before `lastpc`, running the code
|
|
/// from the start and taking each forward jump that lands at or before `lastpc`. As in 5.1, a
|
|
/// test counts as writing the register it tests, and a fused test takes its jump.
|
|
fn symbexec(p: &Proto, lastpc: usize, reg: usize) -> Option<usize> {
|
|
let mut last = None;
|
|
let mut pc = 0;
|
|
while pc < lastpc {
|
|
let op = p.code[pc];
|
|
let writes = |a: Reg| a as usize == reg;
|
|
let hit = match op {
|
|
Op::Move(a, _)
|
|
| Op::LoadK(a, _)
|
|
| Op::LoadBool(a, _)
|
|
| Op::GetUpval(a, _)
|
|
| Op::GetGlobal(a, _)
|
|
| Op::GetTable(a, ..)
|
|
| Op::GetField(a, ..)
|
|
| Op::NewTable(a, ..)
|
|
| Op::Add(a, ..)
|
|
| Op::Sub(a, ..)
|
|
| Op::Mul(a, ..)
|
|
| Op::Div(a, ..)
|
|
| Op::Pow(a, ..)
|
|
| Op::AddK(a, ..)
|
|
| Op::SubK(a, ..)
|
|
| Op::MulK(a, ..)
|
|
| Op::DivK(a, ..)
|
|
| Op::AddKR(a, ..)
|
|
| Op::SubKR(a, ..)
|
|
| Op::MulKR(a, ..)
|
|
| Op::DivKR(a, ..)
|
|
| Op::Unm(a, _)
|
|
| Op::Not(a, _)
|
|
| Op::Concat(a, ..)
|
|
| Op::JmpIf(a, ..)
|
|
| Op::TestSet(a, ..)
|
|
| Op::ForLoop(a, _)
|
|
| Op::ForLoopUp(a, _)
|
|
| Op::Closure(a, _) => writes(a),
|
|
Op::LoadNil(a, b) => (a as usize..=b as usize).contains(®),
|
|
Op::SelfOp(a, ..) | Op::SelfField(a, ..) => writes(a) || reg == a as usize + 1,
|
|
Op::Call(a, ..) | Op::TailCall(a, ..) => reg >= a as usize,
|
|
// The client's rule writes every register from `a` up (`0x6fc22c`).
|
|
Op::TForLoop(a, _) => reg >= a as usize,
|
|
_ => false,
|
|
};
|
|
if hit {
|
|
last = Some(pc);
|
|
}
|
|
let jump = match op {
|
|
Op::Jmp(off)
|
|
| Op::TForPrep(_, off)
|
|
| Op::ForLoop(_, off)
|
|
| Op::ForLoopUp(_, off)
|
|
| Op::JmpIf(_, _, off)
|
|
| Op::TestSet(_, _, _, off)
|
|
| Op::JmpCmp(_, _, _, off)
|
|
| Op::JmpCmpK(_, _, _, off)
|
|
| Op::JmpCmpKR(_, _, _, off) => Some(off),
|
|
_ => None,
|
|
};
|
|
if let Some(off) = jump {
|
|
let dest = (pc as i32 + 1 + off) as usize;
|
|
if pc < dest && dest <= lastpc {
|
|
pc = dest;
|
|
continue;
|
|
}
|
|
}
|
|
pc += 1;
|
|
}
|
|
last
|
|
}
|
|
|
|
/// The top `luaD_precall` checks the stack from for a Lua function whose first register is
|
|
/// `base` (`0x6f60ab`..`0x6f60c7`): above its arguments, or above `arg`, which `adjust_varargs`
|
|
/// leaves past the parameters.
|
|
fn precall_top50(base: usize, nargs: usize, p: &Proto) -> usize {
|
|
if p.vararg {
|
|
base + p.nparams as usize + 1
|
|
} else {
|
|
base + nargs
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
/// A deep recursion grows the stack and the call records by 5.0's rules; each collection
|
|
/// after it halves both once (`0x6f7936`, `0x6f795e`), and the count drops by what they shed.
|
|
#[test]
|
|
fn the_stack_and_the_call_records_halve_once_a_collection() {
|
|
let mut vm = crate::new_state();
|
|
vm.full_gc();
|
|
assert_eq!(vm.stack.len(), BASIC_STACK_SIZE);
|
|
assert_eq!(vm.ci_size, BASIC_CI_SIZE);
|
|
vm.exec(
|
|
b"local function f(n) if n > 0 then return 1 + f(n - 1) end return 0 end f(300)",
|
|
b"=t",
|
|
)
|
|
.unwrap();
|
|
// 45 doubled: the stack grows as `luaD_growstack` doubles it (`0x6f5f67`).
|
|
let (s0, k0) = (vm.stack.len(), vm.ci_size);
|
|
assert_eq!(s0 % BASIC_STACK_SIZE, 0);
|
|
assert!(
|
|
(s0 / BASIC_STACK_SIZE).is_power_of_two() && s0 >= 4 * 300 / 2,
|
|
"{s0}"
|
|
);
|
|
assert_eq!(k0, 512, "301 records: 8 doubled six times");
|
|
vm.full_gc();
|
|
assert_eq!(
|
|
(vm.stack.len(), vm.ci_size),
|
|
(s0 / 2, k0 / 2),
|
|
"halved once"
|
|
);
|
|
let mut prev = (vm.stack.len(), vm.ci_size, vm.heap.bytes());
|
|
loop {
|
|
vm.full_gc();
|
|
let now = (vm.stack.len(), vm.ci_size, vm.heap.bytes());
|
|
assert_eq!(
|
|
prev.2 - now.2,
|
|
STACK_SLOT_BYTES * (prev.0 - now.0) + CI_BYTES * (prev.1 - now.1),
|
|
"the count sheds what the stack and the records do"
|
|
);
|
|
if now == prev {
|
|
break;
|
|
}
|
|
assert!(now.0 == prev.0 / 2 || now.0 == prev.0);
|
|
assert!(now.1 == prev.1 / 2 || now.1 == prev.1);
|
|
prev = now;
|
|
}
|
|
// Never back to the first sizes: a stack halves only above 90 slots, the records above 16.
|
|
assert_eq!((vm.stack.len(), vm.ci_size), (2 * BASIC_STACK_SIZE, 16));
|
|
}
|
|
|
|
/// A tail call checks the stack where the called function sits, before its frame moves down
|
|
/// (`0x6f922c`, then `0x6f9761`..`0x6f97ef`). `f`'s frame starts at slot 4, under the main
|
|
/// chunk's three registers; with `n` locals it calls `g` from slot `4 + n`, its argument
|
|
/// above, and `g` reserves 11 registers: the check from `6 + n` asks `6 + n + 11 + 6 < 45`,
|
|
/// which 21 locals meet and 22 do not, where from `f`'s own slot it would ask only `5 + 17`.
|
|
#[test]
|
|
fn a_tail_call_checks_the_stack_where_the_call_is_made() {
|
|
for (n, size) in [(21, 45), (22, 90)] {
|
|
let mut vm = crate::new_state();
|
|
let locals: Vec<String> = (0..n).map(|i| format!("a{i}")).collect();
|
|
let src = format!(
|
|
"local function g(x) local p, q, r, s, t, u, v, w, y, z = 1 return x end \
|
|
local function f() local {} = 1 return g(1) end f()",
|
|
locals.join(", ")
|
|
);
|
|
vm.exec(src.as_bytes(), b"=t").unwrap();
|
|
assert_eq!(vm.stack.len(), size, "{n} locals");
|
|
}
|
|
}
|
|
|
|
/// The client's `adjust_varargs` makes the `arg` table without checking the collector
|
|
/// (`0x6f6200`..`0x6f6366`): a call to a vararg function whose body allocates nothing never
|
|
/// collects, with the count at the threshold.
|
|
#[test]
|
|
fn a_vararg_call_does_not_collect() {
|
|
let mut vm = crate::new_state();
|
|
vm.exec(b"function V(...) return 1 end", b"=t").unwrap();
|
|
let v = vm.get_global("V");
|
|
let count = vm.heap.bytes();
|
|
vm.heap.set_threshold(count);
|
|
let n = vm.heap.collections;
|
|
vm.call_value(v, &[Value::Num(1.0), Value::Num(2.0)], 0)
|
|
.unwrap();
|
|
assert_eq!(vm.heap.collections, n);
|
|
assert!(vm.heap.bytes() > count, "`arg` was made");
|
|
}
|
|
|
|
/// A collection the heap asks for (string ids running short) stays asked for through a new
|
|
/// threshold: `collectgarbage` with the most there is still collects at once.
|
|
#[test]
|
|
fn a_new_threshold_keeps_a_collection_the_heap_asked_for() {
|
|
let mut vm = crate::new_state();
|
|
vm.full_gc();
|
|
let used = vm.heap.counts()[0] as u32;
|
|
vm.heap.limit_string_ids(2 * used + 8);
|
|
let n = vm.heap.collections;
|
|
let mut i = 0;
|
|
while vm.heap.trigger != 0 {
|
|
vm.heap.intern(format!("s{i}").as_bytes());
|
|
i += 1;
|
|
}
|
|
assert_eq!(vm.heap.collections, n, "asked for, not run");
|
|
vm.set_gc_threshold(0x3f_ffff);
|
|
assert_eq!(vm.heap.collections, n + 1);
|
|
assert_eq!(vm.heap.threshold(), vm.heap.trigger, "the request is met");
|
|
}
|
|
|
|
/// A bare state has nothing of 16 bytes or less, so its smallest pool has taken no chunk and
|
|
/// has no free space: a collection leaves the count and a quarter of it (`0x6fae6f`).
|
|
#[test]
|
|
fn a_collection_with_a_pool_unused_leaves_a_quarter_more() {
|
|
let mut vm = Vm::new();
|
|
assert_eq!(
|
|
vm.heap.threshold(),
|
|
4 * vm.heap.bytes(),
|
|
"`f_luaopen` (`0x6f6ec8`)"
|
|
);
|
|
vm.full_gc();
|
|
let count = vm.heap.bytes();
|
|
assert_eq!(vm.heap.threshold(), count + count / 4);
|
|
}
|
|
}
|