diff --git a/crates/lune-std-luau/src/lib.rs b/crates/lune-std-luau/src/lib.rs index 59b0934..e41eed5 100644 --- a/crates/lune-std-luau/src/lib.rs +++ b/crates/lune-std-luau/src/lib.rs @@ -4,6 +4,12 @@ use mlua::prelude::*; use lune_utils::TableBuilder; +mod options; + +use self::options::{LuauCompileOptions, LuauLoadOptions}; + +const BYTECODE_ERROR_BYTE: u8 = 0; + /** Creates the `luau` standard library module. @@ -12,5 +18,52 @@ use lune_utils::TableBuilder; Errors when out of memory. */ pub fn module(lua: &Lua) -> LuaResult { - TableBuilder::new(lua)?.build_readonly() + TableBuilder::new(lua)? + .with_function("compile", compile_source)? + .with_function("load", load_source)? + .build_readonly() +} + +fn compile_source<'lua>( + lua: &'lua Lua, + (source, options): (LuaString<'lua>, LuauCompileOptions), +) -> LuaResult> { + let bytecode = options.into_compiler().compile(source); + + match bytecode.first() { + Some(&BYTECODE_ERROR_BYTE) => Err(LuaError::RuntimeError( + String::from_utf8_lossy(&bytecode).into_owned(), + )), + Some(_) => lua.create_string(bytecode), + None => panic!("Compiling resulted in empty bytecode"), + } +} + +fn load_source<'lua>( + lua: &'lua Lua, + (source, options): (LuaString<'lua>, LuauLoadOptions), +) -> LuaResult> { + let mut chunk = lua.load(source.as_bytes()).set_name(options.debug_name); + + if let Some(environment) = options.environment { + let environment_with_globals = lua.create_table()?; + + if let Some(meta) = environment.get_metatable() { + environment_with_globals.set_metatable(Some(meta)); + } + + for pair in lua.globals().pairs() { + let (key, value): (LuaValue, LuaValue) = pair?; + environment_with_globals.set(key, value)?; + } + + for pair in environment.pairs() { + let (key, value): (LuaValue, LuaValue) = pair?; + environment_with_globals.set(key, value)?; + } + + chunk = chunk.set_environment(environment_with_globals); + } + + chunk.into_function() } diff --git a/crates/lune-std-luau/src/options.rs b/crates/lune-std-luau/src/options.rs new file mode 100644 index 0000000..a2040ec --- /dev/null +++ b/crates/lune-std-luau/src/options.rs @@ -0,0 +1,133 @@ +#![allow(clippy::struct_field_names)] + +use mlua::prelude::*; +use mlua::Compiler as LuaCompiler; + +const DEFAULT_DEBUG_NAME: &str = "luau.load(...)"; + +/** + Options for compiling Lua source code. +*/ +#[derive(Debug, Clone, Copy)] +pub struct LuauCompileOptions { + pub(crate) optimization_level: u8, + pub(crate) coverage_level: u8, + pub(crate) debug_level: u8, +} + +impl LuauCompileOptions { + pub fn into_compiler(self) -> LuaCompiler { + LuaCompiler::default() + .set_optimization_level(self.optimization_level) + .set_coverage_level(self.coverage_level) + .set_debug_level(self.debug_level) + } +} + +impl Default for LuauCompileOptions { + fn default() -> Self { + // NOTE: This is the same as LuaCompiler::default() values, but they are + // not accessible from outside of mlua so we need to recreate them here. + Self { + optimization_level: 1, + coverage_level: 0, + debug_level: 1, + } + } +} + +impl<'lua> FromLua<'lua> for LuauCompileOptions { + fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult { + Ok(match value { + LuaValue::Nil => Self::default(), + LuaValue::Table(t) => { + let mut options = Self::default(); + + let get_and_check = |name: &'static str| -> LuaResult> { + match t.get(name)? { + Some(n @ (0..=2)) => Ok(Some(n)), + Some(n) => Err(LuaError::runtime(format!( + "'{name}' must be one of: 0, 1, or 2 - got {n}" + ))), + None => Ok(None), + } + }; + + if let Some(optimization_level) = get_and_check("optimizationLevel")? { + options.optimization_level = optimization_level; + } + if let Some(coverage_level) = get_and_check("coverageLevel")? { + options.coverage_level = coverage_level; + } + if let Some(debug_level) = get_and_check("debugLevel")? { + options.debug_level = debug_level; + } + + options + } + _ => { + return Err(LuaError::FromLuaConversionError { + from: value.type_name(), + to: "CompileOptions", + message: Some(format!( + "Invalid compile options - expected table, got {}", + value.type_name() + )), + }) + } + }) + } +} + +/** + Options for loading Lua source code. +*/ +#[derive(Debug, Clone)] +pub struct LuauLoadOptions<'lua> { + pub(crate) debug_name: String, + pub(crate) environment: Option>, +} + +impl Default for LuauLoadOptions<'_> { + fn default() -> Self { + Self { + debug_name: DEFAULT_DEBUG_NAME.to_string(), + environment: None, + } + } +} + +impl<'lua> FromLua<'lua> for LuauLoadOptions<'lua> { + fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult { + Ok(match value { + LuaValue::Nil => Self::default(), + LuaValue::Table(t) => { + let mut options = Self::default(); + + if let Some(debug_name) = t.get("debugName")? { + options.debug_name = debug_name; + } + + if let Some(environment) = t.get("environment")? { + options.environment = Some(environment); + } + + options + } + LuaValue::String(s) => Self { + debug_name: s.to_string_lossy().to_string(), + environment: None, + }, + _ => { + return Err(LuaError::FromLuaConversionError { + from: value.type_name(), + to: "LoadOptions", + message: Some(format!( + "Invalid load options - expected string or table, got {}", + value.type_name() + )), + }) + } + }) + } +}