///|
/// High-level handle to an embedded Lua 5.4 state.
///
/// Each `Lua` value wraps a `lua_State*` pointer and owns the
/// lifetime of that state. When the `Lua` value is collected, the
/// underlying state is automatically closed.
type Lua
///|
pub impl Show for Lua with output(self : Lua, logger : &Logger) -> Unit {
@c.with_unsafe_borrowed(self, ptr => logger.write_string(
"",
))
}
///|
pub impl ToJson for Lua with to_json(self : Lua) -> Json {
@c.with_unsafe_borrowed(self, ptr => Json::string(
"",
))
}
///|
fn[R] Lua::with_state(self : Lua, f : (@lua.State) -> R raise?) -> R raise? {
let ptr = @c.unsafe_borrow(self)
let state = @lua.State::of_pointer(ptr)
let result = f(state)
let _ : Lua = @c.unsafe_return(ptr)
return result
}
///|
fn alloc(
ptr : @c.Pointer[Unit],
osize : UInt64,
nsize : UInt64,
) -> @c.Pointer[Unit] {
if ptr.is_null() && osize.to_int() is @lua.TypeThread {
@moonbit.make_external_object(
ptr => {
let state : @lua.State = @lua.State::of_pointer(ptr)
@lua.close(state)
},
nsize,
)
} else if nsize == 0 {
@memory.free(ptr)
return @c.Pointer::null()
} else {
return @memory.realloc(ptr, nsize)
}
}
///|
/// Creates a new `Lua` state with all standard libraries loaded.
///
/// This is the preferred entry point for embedding Lua. It
/// configures the allocator so that the state is closed
/// automatically when the returned `Lua` value is collected.
#as_free_fn
pub fn Lua::new() -> Lua raise Err {
guard @lua.new_state(alloc) is Some(lua) else {
raise MemoryError(String("failed to create new Lua state"))
}
@lib.open_libs(lua)
@c.unsafe_return(lua.to_pointer())
}
///|
/// High-level tagged representation of Lua values.
///
/// Values of this enum are used to move data between MoonBit code
/// and the underlying Lua state without exposing raw C pointers.
pub(all) enum Value {
Integer(Int64)
Number(Double)
String(String)
Boolean(Bool)
Nil
Thread(Lua)
Table(Table)
LightUserdata(LightUserdata)
Userdata(Userdata)
Function(Function)
}
///|
pub impl Show for Value with output(self : Value, logger : &Logger) -> Unit {
match self {
Integer(v) => logger.write_string(v.to_string())
Number(v) => logger.write_string(v.to_string())
String(v) => v.output(logger)
Boolean(v) => logger.write_string(v.to_string())
Nil => logger.write_string("nil")
Thread(lua) => lua.output(logger)
Table(table) => table.output(logger)
LightUserdata(ptr) => ptr.output(logger)
Userdata(userdata) => userdata.output(logger)
Function(function) => function.output(logger)
}
}
///|
pub impl ToJson for Value with to_json(self : Value) -> Json {
match self {
Integer(v) => Json::number(v.to_double(), repr=v.to_string())
Number(v) => Json::number(v)
String(v) => Json::string(v)
Boolean(v) => Json::boolean(v)
Nil => Json::null()
Thread(lua) => lua.to_json()
Table(table) => table.to_json()
LightUserdata(ptr) => ptr.to_json()
Userdata(userdata) => userdata.to_json()
Function(function) => function.to_json()
}
}
///|
/// Lightweight wrapper around a raw C pointer exposed to Lua.
///
/// This corresponds to Lua's `lightuserdata` type and does not
/// participate in garbage collection on the Lua side.
struct LightUserdata(@c.Pointer[Unit])
///|
pub impl Show for LightUserdata with output(
self : LightUserdata,
logger : &Logger,
) -> Unit {
logger.write_string(
"",
)
}
///|
pub impl ToJson for LightUserdata with to_json(self : LightUserdata) -> Json {
Json::string("")
}
///|
/// Opaque handle to full Lua userdata stored in the registry.
///
/// Instances of this type refer to heap-allocated blocks managed
/// by Lua and kept alive through a registry reference.
struct Userdata(Ref)
///|
pub impl Show for Userdata with output(self : Userdata, logger : &Logger) -> Unit {
logger.write_string("")
}
///|
pub impl ToJson for Userdata with to_json(self : Userdata) -> Json {
Json::string("")
}
///|
/// Opaque handle to a Lua function stored in the registry.
///
/// Values of this type can be obtained from the stack through
/// [`Value::Function`] and are represented by a registry reference.
struct Function(Ref)
///|
pub impl Show for Function with output(self : Function, logger : &Logger) -> Unit {
logger.write_string("")
}
///|
pub impl ToJson for Function with to_json(self : Function) -> Json {
Json::string("")
}
///|
fn state_get_value(lua : Lua, state : @lua.State, index : Int) -> Value? {
guard @lua.type_(state, index) is Some(type_) else { None }
match type_ {
Userdata => {
@lua.push_value(state, index)
Some(Userdata(lua.new_ref()))
}
Function => {
// Functions are treated as userdata in Lua, so we can return a reference
@lua.push_value(state, index)
Some(Function(lua.new_ref()))
}
Table => {
@lua.push_value(state, index)
Some(Table(lua.new_ref()))
}
String => {
let bytes = @lua.to_string(state, index).unwrap()
Some(String(@encoding/utf8.decode_lossy(bytes)))
}
Number =>
if @lua.is_integer(state, index) {
let v = @lua.to_integer(state, index)
Some(Integer(v))
} else {
let v = @lua.to_number(state, index)
Some(Number(v))
}
LightUserdata => {
let ptr = @lua.to_userdata(state, index)
Some(LightUserdata(ptr))
}
Boolean => {
let v = @lua.to_boolean(state, index)
Some(Boolean(v))
}
Nil => Some(Nil)
Thread => {
let thread = @lua.to_thread(state, index).unwrap()
Some(Thread(@c.unsafe_return(thread.to_pointer())))
}
}
}
///|
fn state_pop_top_value(lua : Lua, state : @lua.State) -> Value? {
guard @lua.type_(state, -1) is Some(type_) else { None }
match type_ {
Userdata => Some(Userdata(lua.new_ref()))
Function => Some(Function(lua.new_ref()))
Table => Some(Table(lua.new_ref()))
String => {
let bytes = @lua.to_string(state, -1).unwrap()
@lua.pop(state, 1)
Some(String(@encoding/utf8.decode_lossy(bytes)))
}
Number =>
if @lua.is_integer(state, -1) {
let v = @lua.to_integer(state, -1)
@lua.pop(state, 1)
Some(Integer(v))
} else {
let v = @lua.to_number(state, -1)
@lua.pop(state, 1)
Some(Number(v))
}
LightUserdata => {
let ptr = @lua.to_userdata(state, -1)
@lua.pop(state, 1)
Some(LightUserdata(ptr))
}
Boolean => {
let v = @lua.to_boolean(state, -1)
@lua.pop(state, 1)
Some(Boolean(v))
}
Nil => {
@lua.pop(state, 1)
Some(Nil)
}
Thread => {
let thread = @lua.to_thread(state, -1).unwrap()
@lua.pop(state, 1)
Some(Thread(@c.unsafe_return(thread.to_pointer())))
}
}
}
///|
/// Reads a value from the Lua stack at the given index.
///
/// Returns `None` if the index is invalid or the slot does not
/// contain a value that can be represented as [`Value`].
pub fn Lua::get(self : Lua, index : Int) -> Value? {
self.with_state(state => state_get_value(self, state, index))
}
///|
fn state_push_value(state : @lua.State, value : Value) -> Unit {
match value {
Integer(v) => @lua.push_integer(state, v)
Number(v) => @lua.push_number(state, v)
String(v) => {
let bytes = @encoding/utf8.encode(v)
@lua.push_string(state, bytes) |> ignore()
}
Boolean(v) => @lua.push_boolean(state, v)
Nil => @lua.push_nil(state)
Thread(v) => {
let thread_ptr = @c.unsafe_borrow(v)
let thread = @lua.State::of_pointer(thread_ptr)
@lua.push_thread(state, thread) |> ignore()
}
Table(table) =>
ignore(
@lua.raw_get_i(state, @lua.registry_index, table.0.int().to_int64()),
)
Userdata(userdata) =>
ignore(
@lua.raw_get_i(state, @lua.registry_index, userdata.0.int().to_int64()),
)
Function(function) =>
ignore(
@lua.raw_get_i(state, @lua.registry_index, function.0.int().to_int64()),
)
LightUserdata(ptr) => @lua.push_light_userdata(state, ptr.0)
}
}
///|
/// Pushes a high-level [`Value`] onto this state's Lua stack.
///
/// Tables, userdata, and functions are pushed by looking up their
/// internal registry reference; other variants are converted to the
/// corresponding Lua primitive type.
pub fn Lua::push(self : Lua, value : Value) -> Unit {
self.with_state(state => state_push_value(state, value))
}
///|
/// Error type used by the high-level `Lua` API.
///
/// Each variant corresponds to a particular Lua status code and
/// carries the underlying Lua error value as a [`Value`].
pub suberror Err {
/// a runtime error.
RuntimeError(Value)
/// memory allocation error. For such errors, Lua does not call the message handler.
MemoryError(Value)
/// error while running the message handler.
MessageHandlerError(Value)
/// syntax error during precompilation.
SyntaxError(Value)
/// a file-related error; e.g., it cannot open or read the file.
FileError(Value)
/// an unknown error.
UnknownError(Int, Value)
} derive(ToJson)
///|
fn Err::new(status : Int, value : Value) -> Err {
match status {
@lua.ErrRun => RuntimeError(value)
@lua.ErrMem => MemoryError(value)
@lua.ErrErr => MessageHandlerError(value)
@lua.ErrSyntax => SyntaxError(value)
@aux.ErrFile => FileError(value)
status => UnknownError(status, value)
}
}
///|
fn Err::value(self : Err) -> Value {
match self {
SyntaxError(value) => value
MessageHandlerError(value) => value
MemoryError(value) => value
RuntimeError(value) => value
FileError(value) => value
UnknownError(_, value) => value
}
}
///|
pub impl Show for Err with output(self : Err, logger : &Logger) -> Unit {
match self.value() {
String(string) => string.output(logger)
Number(number) => logger.write_string(number.to_string())
Integer(integer) => logger.write_string(integer.to_string())
Boolean(boolean) => logger.write_string(boolean.to_string())
Nil => logger.write_string("nil")
LightUserdata(light_userdata) => light_userdata.output(logger)
Thread(thread) => thread.output(logger)
Table(table) => table.output(logger)
Userdata(userdata) => userdata.output(logger)
Function(function) => function.output(logger)
}
}
///|
fn state_eval(
lua : Lua,
state : @lua.State,
code : String,
args : Array[Value],
) -> Array[Value] raise Err {
let base = @lua.get_top(state)
let status = @aux.load_bytes(state, @encoding/utf8.encode(code), "=(eval)")
guard status is @lua.Ok else {
raise Err::new(status, state_pop_top_value(lua, state).unwrap())
}
// Push arguments onto the stack
let n_args = args.length()
for arg in args {
state_push_value(state, arg)
}
let status = @lua.pcall(state, n_args, @lua.MultRet, 0)
guard status is @lua.Ok else {
raise Err::new(status, state_pop_top_value(lua, state).unwrap())
}
let results = []
while @lua.get_top(state) > base {
let value = state_pop_top_value(lua, state).unwrap()
results.push(value)
}
results.rev_in_place()
results
}
///|
/// Loads and runs a Lua chunk with the given arguments.
///
/// The `code` string is compiled as a Lua chunk and called with
/// the values from `args` as arguments. All return values are
/// collected from the stack and returned as an array of [`Value`](#Value).
///
/// Raises [`Err`](#Err) if compilation or execution fails.
pub fn Lua::eval(
self : Lua,
code : String,
args : Array[Value],
) -> Array[Value] raise Err {
self.with_state(state => state_eval(self, state, code, args))
}