Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/lua-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ dhat-heap = ["dep:dhat"]
fast-alloc = ["dep:mimalloc"]

[dependencies]
lua-gc.workspace = true
lua-types.workspace = true
lua-vm.workspace = true
lua-stdlib.workspace = true
Expand Down
329 changes: 325 additions & 4 deletions crates/lua-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ use std::io::{self, BufReader, BufWriter, Read, Seek, SeekFrom, Write};
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::process::ExitCode;

use lua_types::closure::LuaLClosure;
use lua_types::closure::{LuaClosure, LuaLClosure};
use lua_types::error::{LuaError, LuaExit};
use lua_types::filehandle::LuaFileHandle;
use lua_types::gc::GcRef;
use lua_types::upval::UpVal;
use lua_types::value::LuaValue;
use lua_vm::state::{
new_state, DynLibId, DynamicSymbol, LuaState, OsExecuteReason, OsExecuteResult,
new_state, DynLibId, DynamicSymbol, GcKind, LuaState, OsExecuteReason, OsExecuteResult,
};

mod interp;
Expand Down Expand Up @@ -807,19 +807,340 @@ fn parser_hook(
}))
}

fn testc_push_string(state: &mut LuaState, bytes: &[u8]) -> Result<(), LuaError> {
let s = state.intern_str(bytes)?;
state.push(LuaValue::Str(s));
Ok(())
}

fn testc_gc_age(value: LuaValue) -> Option<lua_gc::GcAge> {
match value {
LuaValue::Str(v) => Some(v.0.age()),
LuaValue::Table(v) => Some(v.0.age()),
LuaValue::Function(LuaClosure::Lua(v)) => Some(v.0.age()),
LuaValue::Function(LuaClosure::C(v)) => Some(v.0.age()),
LuaValue::UserData(v) => Some(v.0.age()),
LuaValue::Thread(v) => Some(v.0.age()),
LuaValue::Nil
| LuaValue::Bool(_)
| LuaValue::Int(_)
| LuaValue::Float(_)
| LuaValue::LightUserData(_)
| LuaValue::Function(LuaClosure::LightC(_)) => None,
}
}

fn testc_gc_color(value: LuaValue) -> Option<lua_gc::Color> {
match value {
LuaValue::Str(v) => Some(v.0.color()),
LuaValue::Table(v) => Some(v.0.color()),
LuaValue::Function(LuaClosure::Lua(v)) => Some(v.0.color()),
LuaValue::Function(LuaClosure::C(v)) => Some(v.0.color()),
LuaValue::UserData(v) => Some(v.0.color()),
LuaValue::Thread(v) => Some(v.0.color()),
LuaValue::Nil
| LuaValue::Bool(_)
| LuaValue::Int(_)
| LuaValue::Float(_)
| LuaValue::LightUserData(_)
| LuaValue::Function(LuaClosure::LightC(_)) => None,
}
}

fn testc_gcage(state: &mut LuaState) -> Result<usize, LuaError> {
lua_vm::api::push_value(state, 1);
let value = state.pop();
let name = match testc_gc_age(value) {
Some(lua_gc::GcAge::New) => b"new".as_slice(),
Some(lua_gc::GcAge::Survival) => b"survival".as_slice(),
Some(lua_gc::GcAge::Old0) => b"old0".as_slice(),
Some(lua_gc::GcAge::Old1) => b"old1".as_slice(),
Some(lua_gc::GcAge::Old) => b"old".as_slice(),
Some(lua_gc::GcAge::Touched1) => b"touched1".as_slice(),
Some(lua_gc::GcAge::Touched2) => b"touched2".as_slice(),
None => b"no collectable".as_slice(),
};
testc_push_string(state, name)?;
Ok(1)
}

fn testc_gccolor(state: &mut LuaState) -> Result<usize, LuaError> {
lua_vm::api::push_value(state, 1);
let value = state.pop();
let name = match testc_gc_color(value) {
Some(color) if color.is_white() => b"white".as_slice(),
Some(lua_gc::Color::Gray) => b"gray".as_slice(),
Some(lua_gc::Color::Black) => b"black".as_slice(),
Some(_) => b"white".as_slice(),
None => b"no collectable".as_slice(),
};
testc_push_string(state, name)?;
Ok(1)
}

fn testc_gc_state_name(gc_state: lua_gc::GcState) -> &'static [u8] {
match gc_state {
lua_gc::GcState::Pause => b"pause",
lua_gc::GcState::Propagate => b"propagate",
lua_gc::GcState::Atomic => b"atomic",
lua_gc::GcState::Sweep => b"sweepallgc",
lua_gc::GcState::Finalize => b"callfin",
}
}

#[derive(Clone, Copy, PartialEq, Eq)]
enum TestcGcState {
Propagate,
Atomic,
Sweep,
Finalize,
Pause,
}

impl TestcGcState {
fn heap_state(self) -> lua_gc::GcState {
match self {
TestcGcState::Propagate => lua_gc::GcState::Propagate,
TestcGcState::Atomic => lua_gc::GcState::Atomic,
TestcGcState::Sweep => lua_gc::GcState::Sweep,
TestcGcState::Finalize => lua_gc::GcState::Finalize,
TestcGcState::Pause => lua_gc::GcState::Pause,
}
}
}

fn testc_gc_state_target(bytes: &[u8]) -> Option<TestcGcState> {
match bytes {
b"propagate" => Some(TestcGcState::Propagate),
b"atomic" | b"enteratomic" => Some(TestcGcState::Atomic),
b"sweepallgc" | b"sweepfinobj" | b"sweeptobefnz" | b"sweepend" => {
Some(TestcGcState::Sweep)
}
b"callfin" => Some(TestcGcState::Finalize),
b"pause" => Some(TestcGcState::Pause),
b"" => None,
_ => None,
}
}

fn testc_drive_gcstate(state: &mut LuaState, target: TestcGcState) -> Result<(), LuaError> {
if state.global().gckind == GcKind::Generational as u8 {
return Err(LuaError::runtime(format_args!(
"cannot change states in generational mode"
)));
}

if state.gc().run_until_gc_state_for_test(target.heap_state()) {
Ok(())
} else {
Err(LuaError::runtime(format_args!(
"could not reach requested GC state"
)))
}
}

fn testc_gcstate(state: &mut LuaState) -> Result<usize, LuaError> {
if lua_vm::api::get_top(state) == 0 {
let name = testc_gc_state_name(state.global().heap.gc_state());
testc_push_string(state, name)?;
return Ok(1);
}

let option = state.check_arg_string(1)?;
if option.is_empty() {
let name = testc_gc_state_name(state.global().heap.gc_state());
testc_push_string(state, name)?;
return Ok(1);
}

let Some(target) = testc_gc_state_target(&option) else {
return Err(LuaError::runtime(format_args!(
"unknown GC state '{}'",
String::from_utf8_lossy(&option)
)));
};
testc_drive_gcstate(state, target)?;
Ok(0)
}

fn testc_newuserdata(state: &mut LuaState) -> Result<usize, LuaError> {
let size = state.opt_arg_integer(1, 0)?;
let nuvalue = state.opt_arg_integer(2, 0)?;
if size < 0 {
return Err(LuaError::runtime(format_args!("userdata size must be non-negative")));
}
if nuvalue < 0 {
return Err(LuaError::runtime(format_args!("userdata value count must be non-negative")));
}
state.new_userdata_typed(b"testC", size as usize, nuvalue as i32)?;
Ok(1)
}

fn testc_type_count(state: &LuaState, name: &[u8]) -> Result<usize, LuaError> {
let count = match name {
b"string" => state.global().heap.type_name_count(|ty| ty.contains("LuaString")),
b"table" => state.global().heap.type_name_count(|ty| ty.contains("LuaTable")),
b"function" => state.global().heap.type_name_count(|ty| {
ty.contains("LuaLClosure") || ty.contains("LuaCClosure")
}),
b"userdata" => state.global().heap.type_name_count(|ty| ty.contains("LuaUserData")),
b"thread" => state.global().heap.type_name_count(|ty| ty.contains("LuaThread")),
_ => {
return Err(LuaError::runtime(format_args!(
"unknown type '{}'",
String::from_utf8_lossy(name)
)));
}
};
Ok(count)
}

fn testc_push_usize(state: &mut LuaState, value: usize) {
state.push(LuaValue::Int(value.min(i64::MAX as usize) as i64));
}

fn testc_totalmem(state: &mut LuaState) -> Result<usize, LuaError> {
if lua_vm::api::get_top(state) == 0 {
let total = state.global().total_bytes();
let blocks = state.global().heap.allgc_count();
let limit = state
.global()
.sandbox
.mem_limit
.get()
.unwrap_or(usize::MAX);
testc_push_usize(state, total);
testc_push_usize(state, blocks);
testc_push_usize(state, limit);
return Ok(3);
}

if let Some(limit) = lua_vm::api::to_integer_x(state, 1) {
let limit = if limit <= 0 {
None
} else {
Some(limit as usize)
};
state.global().sandbox.mem_limit.set(limit);
return Ok(0);
}

let name = state.check_arg_string(1)?;
let count = testc_type_count(state, &name)?;
testc_push_usize(state, count);
Ok(1)
}

fn testc_checkmemory(state: &mut LuaState) -> Result<usize, LuaError> {
let known = [
b"string".as_slice(),
b"table".as_slice(),
b"function".as_slice(),
b"userdata".as_slice(),
b"thread".as_slice(),
]
.into_iter()
.try_fold(0usize, |acc, name| testc_type_count(state, name).map(|n| acc + n))?;
let allgc = state.global().heap.allgc_count();
if known > allgc {
return Err(LuaError::runtime(format_args!(
"GC type telemetry exceeds allgc count"
)));
}
if state.global().heap.bytes_used() == 0 && allgc != 0 {
return Err(LuaError::runtime(format_args!(
"GC has live objects but zero tracked bytes"
)));
}
Ok(0)
}

fn testc_gcstats(state: &mut LuaState) -> Result<usize, LuaError> {
let (
mode,
gc_state,
bytes,
debt,
threshold,
allgc,
collections,
weak,
pendingfin,
tobefin,
) = {
let g = state.global();
(
if g.is_gen_mode() { "generational" } else { "incremental" },
String::from_utf8_lossy(testc_gc_state_name(g.heap.gc_state())).into_owned(),
g.total_bytes(),
g.gc_debt(),
g.heap.threshold_bytes(),
g.heap.allgc_count(),
g.heap.collections(),
g.weak_tables_registry.len(),
g.pending_finalizers.len(),
g.to_be_finalized.len(),
)
};
let tables = testc_type_count(state, b"table")?;
let functions = testc_type_count(state, b"function")?;
let threads = testc_type_count(state, b"thread")?;
let userdata = testc_type_count(state, b"userdata")?;
let strings = testc_type_count(state, b"string")?;
let stats = format!(
"mode={} state={} bytes={} debt={} threshold={} allgc={} collections={} weak={} pendingfin={} tobefin={} tables={} functions={} threads={} userdata={} strings={}",
mode,
gc_state,
bytes,
debt,
threshold,
allgc,
collections,
weak,
pendingfin,
tobefin,
tables,
functions,
threads,
userdata,
strings,
);
testc_push_string(state, stats.as_bytes())?;
Ok(1)
}

fn register_testc_table(state: &mut LuaState) -> Result<(), LuaError> {
state.enable_test_warning_handler()?;
let funcs: &[(&[u8], lua_vm::state::LuaCFunction)] = &[
(b"checkmemory", testc_checkmemory),
(b"gcage", testc_gcage),
(b"gccolor", testc_gccolor),
(b"gcstate", testc_gcstate),
(b"gcstats", testc_gcstats),
(b"newuserdata", testc_newuserdata),
(b"totalmem", testc_totalmem),
];
state.new_lib(funcs)?;
lua_vm::api::set_global(state, b"T")
}

/// Install Rust-native modules that ship with `lua-cli` into
/// `package.preload`. After `open_libs` has populated the `package` library,
/// each entry written to `package.preload[name]` becomes a loader that
/// `require(name)` will invoke through the preload searcher.
///
/// Phase G-1 ships a single preloaded module — `lfs`, the Rust-native
/// LuaFileSystem port from the `lua-rs-lfs` crate.
/// Phase G-1 ships `lfs`, the Rust-native LuaFileSystem port from the
/// `lua-rs-lfs` crate. `LUA_RS_TESTC=1` also installs a small internal `T`
/// table for official-test instrumentation.
fn register_preloaded_modules(state: &mut LuaState) -> Result<(), LuaError> {
lua_vm::api::get_global(state, b"package")?;
lua_vm::api::get_field(state, -1, b"preload")?;
lua_vm::api::push_cclosure(state, lua_rs_lfs::luaopen_lfs, 0)?;
lua_vm::api::set_field(state, -2, b"lfs")?;
state.pop_n(2);
if std::env::var_os("LUA_RS_TESTC").is_some() {
register_testc_table(state)?;
}
Ok(())
}

Expand Down
Loading
Loading