Implement new luau built-in library. (#82)

This commit is contained in:
AsynchronousMatrix 2023-08-10 20:38:25 +01:00 committed by GitHub
parent e57b8c3b15
commit 0a8773dc04
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 305 additions and 0 deletions

134
src/lune/builtins/luau.rs Normal file
View file

@ -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<Self> {
Ok(match value {
LuaValue::Nil => Self {
optimization_level: 1,
coverage_level: 0,
debug_level: 1,
},
LuaValue::Table(t) => {
let optimization_level: Option<u8> = t.get("optimizationLevel")?;
let coverage_level: Option<u8> = t.get("coverageLevel")?;
let debug_level: Option<u8> = 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<Self> {
Ok(match value {
LuaValue::Nil => Self {
debug_name: DEFAULT_DEBUG_NAME.to_string(),
},
LuaValue::Table(t) => {
let debug_name: Option<String> = 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<LuaTable> {
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<LuaString<'lua>> {
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<LuaFunction<'a>> {
lua.load(source.to_str()?.trim_start())
.set_name(options.debug_name)
.into_function()
}

View file

@ -1,4 +1,5 @@
pub mod fs; pub mod fs;
pub mod luau;
pub mod net; pub mod net;
pub mod process; pub mod process;
pub mod serde; pub mod serde;

View file

@ -14,6 +14,7 @@ pub fn create(lua: &'static Lua, args: Vec<String>) -> LuaResult<()> {
("serde", builtins::serde::create(lua)?), ("serde", builtins::serde::create(lua)?),
("stdio", builtins::stdio::create(lua)?), ("stdio", builtins::stdio::create(lua)?),
("task", builtins::task::create(lua)?), ("task", builtins::task::create(lua)?),
("luau", builtins::luau::create(lua)?),
#[cfg(feature = "roblox")] #[cfg(feature = "roblox")]
("roblox", builtins::roblox::create(lua)?), ("roblox", builtins::roblox::create(lua)?),
]; ];

View file

@ -101,6 +101,9 @@ create_tests! {
task_delay: "task/delay", task_delay: "task/delay",
task_spawn: "task/spawn", task_spawn: "task/spawn",
task_wait: "task/wait", task_wait: "task/wait",
luau_compile: "luau/compile",
luau_load: "luau/load",
} }
#[cfg(feature = "roblox")] #[cfg(feature = "roblox")]

17
tests/luau/compile.luau Normal file
View file

@ -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.")

33
tests/luau/load.luau Normal file
View file

@ -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`")

116
types/Luau.luau Normal file
View file

@ -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