From 0a8773dc0453146cbf878436538c01cfdb3f0abb Mon Sep 17 00:00:00 2001 From: AsynchronousMatrix <38085006+4x8Matrix@users.noreply.github.com> Date: Thu, 10 Aug 2023 20:38:25 +0100 Subject: [PATCH] Implement new `luau` built-in library. (#82) --- src/lune/builtins/luau.rs | 134 ++++++++++++++++++++++++++++++++++++++ src/lune/builtins/mod.rs | 1 + src/lune/importer/mod.rs | 1 + src/tests.rs | 3 + tests/luau/compile.luau | 17 +++++ tests/luau/load.luau | 33 ++++++++++ types/Luau.luau | 116 +++++++++++++++++++++++++++++++++ 7 files changed, 305 insertions(+) create mode 100644 src/lune/builtins/luau.rs create mode 100644 tests/luau/compile.luau create mode 100644 tests/luau/load.luau create mode 100644 types/Luau.luau diff --git a/src/lune/builtins/luau.rs b/src/lune/builtins/luau.rs new file mode 100644 index 0000000..60ae267 --- /dev/null +++ b/src/lune/builtins/luau.rs @@ -0,0 +1,134 @@ +use mlua::prelude::*; +use mlua::Compiler as LuaCompiler; + +use crate::lune::lua::table::TableBuilder; + +const DEFAULT_DEBUG_NAME: &str = "luau.load(...)"; +const BYTECODE_ERROR_BYTE: u8 = 0; + +struct CompileOptions { + pub optimization_level: u8, + pub coverage_level: u8, + pub debug_level: u8, +} + +impl Default for CompileOptions { + fn default() -> Self { + Self { + optimization_level: 1, + coverage_level: 0, + debug_level: 1, + } + } +} + +impl<'lua> FromLua<'lua> for CompileOptions { + fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult { + Ok(match value { + LuaValue::Nil => Self { + optimization_level: 1, + coverage_level: 0, + debug_level: 1, + }, + LuaValue::Table(t) => { + let optimization_level: Option = t.get("optimizationLevel")?; + let coverage_level: Option = t.get("coverageLevel")?; + let debug_level: Option = t.get("debugLevel")?; + + Self { + optimization_level: optimization_level.unwrap_or(1), + coverage_level: coverage_level.unwrap_or(0), + debug_level: debug_level.unwrap_or(1), + } + } + _ => { + return Err(LuaError::FromLuaConversionError { + from: value.type_name(), + to: "CompileOptions", + message: Some(format!( + "Invalid compile options - expected table, got {}", + value.type_name() + )), + }) + } + }) + } +} + +struct LoadOptions { + pub debug_name: String, +} + +impl Default for LoadOptions { + fn default() -> Self { + Self { + debug_name: DEFAULT_DEBUG_NAME.to_string(), + } + } +} + +impl<'lua> FromLua<'lua> for LoadOptions { + fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult { + Ok(match value { + LuaValue::Nil => Self { + debug_name: DEFAULT_DEBUG_NAME.to_string(), + }, + LuaValue::Table(t) => { + let debug_name: Option = t.get("debugName")?; + + Self { + debug_name: debug_name.unwrap_or(DEFAULT_DEBUG_NAME.to_string()), + } + } + LuaValue::String(s) => Self { + debug_name: s.to_str()?.to_string(), + }, + _ => { + return Err(LuaError::FromLuaConversionError { + from: value.type_name(), + to: "LoadOptions", + message: Some(format!( + "Invalid load options - expected string or table, got {}", + value.type_name() + )), + }) + } + }) + } +} + +pub fn create(lua: &'static Lua) -> LuaResult { + 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>, CompileOptions), +) -> LuaResult> { + let source_bytecode_bytes = LuaCompiler::default() + .set_optimization_level(options.optimization_level) + .set_coverage_level(options.coverage_level) + .set_debug_level(options.debug_level) + .compile(source); + + let first_byte = source_bytecode_bytes.first().unwrap(); + + match *first_byte { + BYTECODE_ERROR_BYTE => Err(LuaError::RuntimeError( + String::from_utf8(source_bytecode_bytes).unwrap(), + )), + _ => lua.create_string(source_bytecode_bytes), + } +} + +fn load_source<'a>( + lua: &'static Lua, + (source, options): (LuaString<'a>, LoadOptions), +) -> LuaResult> { + lua.load(source.to_str()?.trim_start()) + .set_name(options.debug_name) + .into_function() +} diff --git a/src/lune/builtins/mod.rs b/src/lune/builtins/mod.rs index 20b13a4..46aefbe 100644 --- a/src/lune/builtins/mod.rs +++ b/src/lune/builtins/mod.rs @@ -1,4 +1,5 @@ pub mod fs; +pub mod luau; pub mod net; pub mod process; pub mod serde; diff --git a/src/lune/importer/mod.rs b/src/lune/importer/mod.rs index c0658d1..de3cdf6 100644 --- a/src/lune/importer/mod.rs +++ b/src/lune/importer/mod.rs @@ -14,6 +14,7 @@ pub fn create(lua: &'static Lua, args: Vec) -> LuaResult<()> { ("serde", builtins::serde::create(lua)?), ("stdio", builtins::stdio::create(lua)?), ("task", builtins::task::create(lua)?), + ("luau", builtins::luau::create(lua)?), #[cfg(feature = "roblox")] ("roblox", builtins::roblox::create(lua)?), ]; diff --git a/src/tests.rs b/src/tests.rs index 96a840c..270de16 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -101,6 +101,9 @@ create_tests! { task_delay: "task/delay", task_spawn: "task/spawn", task_wait: "task/wait", + + luau_compile: "luau/compile", + luau_load: "luau/load", } #[cfg(feature = "roblox")] diff --git a/tests/luau/compile.luau b/tests/luau/compile.luau new file mode 100644 index 0000000..a949d23 --- /dev/null +++ b/tests/luau/compile.luau @@ -0,0 +1,17 @@ +local luau = require("@lune/luau") + +local EMPTY_LUAU_CODE_BLOCK = "do end" +local BROKEN_LUAU_CODE_BLOCK = "do" + +assert(type(luau.compile) == "function", "expected `luau.compile` to be a function") + +assert( + type(luau.compile(EMPTY_LUAU_CODE_BLOCK)) == "string", + "expected `luau.compile` to return bytecode string" +) + +local success = pcall(function() + luau.compile(BROKEN_LUAU_CODE_BLOCK) +end) + +assert(success == false, "expected 'BROKEN_LUAU_CODE_BLOCK' to fail to compile into bytecode.") diff --git a/tests/luau/load.luau b/tests/luau/load.luau new file mode 100644 index 0000000..6a28b8e --- /dev/null +++ b/tests/luau/load.luau @@ -0,0 +1,33 @@ +local luau = require("@lune/luau") + +local RETURN_VALUE = 1 + +local EMPTY_LUAU_CODE_BLOCK = "do end" +local RETURN_LUAU_CODE_BLOCK = "return " .. tostring(RETURN_VALUE) + +local CUSTOM_SOURCE_BLOCK_NAME = "test" + +assert(type(luau.load) == "function", "expected `luau.compile` to be a function") + +assert( + type(luau.load(EMPTY_LUAU_CODE_BLOCK)) == "function", + "expected 'luau.load' to return a function" +) +assert( + luau.load(RETURN_LUAU_CODE_BLOCK)() == RETURN_VALUE, + "expected 'luau.load' to return a value" +) + +local sourceFunction = luau.load(EMPTY_LUAU_CODE_BLOCK, { debugName = CUSTOM_SOURCE_BLOCK_NAME }) +local sourceFunctionDebugName = debug.info(sourceFunction, "s") + +assert( + string.find(sourceFunctionDebugName, CUSTOM_SOURCE_BLOCK_NAME), + "expected source block name for 'luau.load' to return a custom debug name" +) + +local success = pcall(function() + luau.load(luau.compile(RETURN_LUAU_CODE_BLOCK)) +end) + +assert(success, "expected `luau.load` to be able to process the result of `luau.compile`") diff --git a/types/Luau.luau b/types/Luau.luau new file mode 100644 index 0000000..d57aee9 --- /dev/null +++ b/types/Luau.luau @@ -0,0 +1,116 @@ +--[=[ + @interface CompileOptions + @within Luau + + The Luau compiler options used in generating luau bytecode + + This is a dictionary that may contain one or more of the following values: + + * `optimizationLevel` - Sets the compiler option "optimizationLevel". Defaults to `1` + * `coverageLevel` - Sets the compiler option "coverageLevel". Defaults to `0` + * `debugLevel` - Sets the compiler option "debugLevel". Defaults to `1` + + Documentation regarding what these values represent can be found here; + * https://github.com/Roblox/luau/blob/bd229816c0a82a8590395416c81c333087f541fd/Compiler/include/luacode.h#L13 +]=] +export type CompileOptions = { + optimizationLevel: number, + coverageLevel: number, + debugLevel: number, +} + +--[=[ + @interface LoadOptions + @within Luau + + The Luau load options are used for generating a lua function from either bytecode or sourcecode + + This is a dictionary that may contain one or more of the following values: + + * `debugName` - The debug name of the closure. Defaults to `string ["..."]` +]=] +export type LoadOptions = { + debugName: string, +} + +--[=[ + @class Luau + + Built-in library for generating luau bytecode & functions. + + ### Example usage + + ```lua + local luau = require("@lune/luau") + + local bytecode = luau.compile("print('Hello, World!')") + local callableFn = luau.load(bytecode) + + -- Additionally, we can skip the bytecode generation and load a callable function directly from the code itself. + -- local callableFn = luau.load("print('Hello, World!')") + + callableFn() + ``` +]=] +local luau = {} + +--[=[ + @within Luau + + Compiles sourcecode into Luau bytecode + + An error will be thrown if the sourcecode given isn't valid Luau code. + + ### Example usage + + ```lua + local luau = require("@lune/luau") + + local bytecode = luau.compile("print('Hello, World!')", { + optimizationLevel: 1, + coverageLevel: 0, + debugLevel: 1, + }) + + ... + ``` + + @param source The string that'll be compiled into bytecode + @param CompileOptions The luau compiler options used when compiling the source string + + @return luau bytecode +]=] +function luau.compile(source: string, CompileOptions: CompileOptions): string + return nil :: any +end + +--[=[ + @within Luau + + Generates a function from either bytecode or sourcecode + + An error will be thrown if the sourcecode given isn't valid luau code. + + ### Example usage + + ```lua + local luau = require("@lune/luau") + + local bytecode = luau.compile("print('Hello, World!')") + local callableFn = luau.load(bytecode, { + debugName = "'Hello, World'" + }) + + callableFn() + ``` + + @param source Either bytecode or sourcecode + @param loadOptions The load options used when creating a callbable function + + @return luau function +]=] +function luau.load(source: string, loadOptions: LoadOptions): string + return nil :: any +end + +return luau