Skip to main content

rs_luau/
compile.rs

1use std::{
2    ffi::{c_char, c_int, CString},
3    ptr::null,
4};
5
6use crate::{
7    cstdlib_free, luau_compile, LuauCompileOptions, LuauLibraryMemberConstantCallback,
8    LuauLibraryMemberTypeCallback,
9};
10
11#[derive(Debug, Clone)]
12pub struct CompilerLibraries {
13    libraries: Vec<String>,
14    member_type_callback: LuauLibraryMemberTypeCallback,
15    member_constant_callback: LuauLibraryMemberConstantCallback,
16}
17
18impl CompilerLibraries {
19    pub fn new(
20        libraries: Vec<String>,
21        member_type_callback: LuauLibraryMemberTypeCallback,
22        member_constant_callback: LuauLibraryMemberConstantCallback,
23    ) -> Self {
24        Self {
25            libraries,
26            member_type_callback,
27            member_constant_callback,
28        }
29    }
30}
31
32#[derive(Clone, Debug)]
33pub struct Compiler {
34    optimization_level: u8,
35    debug_level: u8,
36    type_info_level: u8,
37    coverage_level: u8,
38    vector_lib: Option<String>,
39    vector_ctor: Option<String>,
40    vector_type: Option<String>,
41    mutable_globals: Vec<String>,
42    userdata_types: Vec<String>,
43    disabled_builtins: Vec<String>,
44    libs: Option<CompilerLibraries>,
45}
46
47impl Compiler {
48    pub fn new() -> Self {
49        Self {
50            optimization_level: 1,
51            debug_level: 1,
52            type_info_level: 0,
53            coverage_level: 0,
54            vector_lib: None,
55            vector_ctor: None,
56            vector_type: None,
57            mutable_globals: Vec::new(),
58            userdata_types: Vec::new(),
59            disabled_builtins: Vec::new(),
60            libs: None,
61        }
62    }
63    /// Sets Luau compiler optimization level.
64    ///
65    /// Possible values:
66    /// * 0 - no optimization
67    /// * 1 - baseline optimization level that doesn't prevent debuggability (default)
68    /// * 2 - includes optimizations that harm debuggability such as inlining
69    #[must_use]
70    pub const fn set_optimization_level(mut self, level: u8) -> Self {
71        self.optimization_level = level;
72        self
73    }
74
75    /// Sets Luau compiler debug level.
76    ///
77    /// Possible values:
78    /// * 0 - no debugging support
79    /// * 1 - line info & function names only; sufficient for backtraces (default)
80    /// * 2 - full debug info with local & upvalue names; necessary for debugger
81    #[must_use]
82    pub const fn set_debug_level(mut self, level: u8) -> Self {
83        self.debug_level = level;
84        self
85    }
86
87    /// Sets Luau type information level used to guide native code generation decisions.
88    ///
89    /// Possible values:
90    /// * 0 - generate for native modules (default)
91    /// * 1 - generate for all modules
92    pub const fn set_type_info_level(mut self, level: u8) -> Self {
93        self.type_info_level = level;
94        self
95    }
96
97    /// Sets Luau compiler code coverage level.
98    ///
99    /// Possible values:
100    /// * 0 - no code coverage support (default)
101    /// * 1 - statement coverage
102    /// * 2 - statement and expression coverage (verbose)
103    #[must_use]
104    pub const fn set_coverage_level(mut self, level: u8) -> Self {
105        self.coverage_level = level;
106        self
107    }
108
109    #[must_use]
110    pub fn set_vector_lib(mut self, lib: impl Into<String>) -> Self {
111        self.vector_lib = Some(lib.into());
112        self
113    }
114
115    #[must_use]
116    pub fn set_vector_ctor(mut self, ctor: impl Into<String>) -> Self {
117        self.vector_ctor = Some(ctor.into());
118        self
119    }
120
121    #[must_use]
122    pub fn set_vector_type(mut self, r#type: impl Into<String>) -> Self {
123        self.vector_type = Some(r#type.into());
124        self
125    }
126
127    /// Sets a list of globals that are mutable.
128    ///
129    /// It disables the import optimization for fields accessed through these.
130    #[must_use]
131    pub fn set_mutable_globals(mut self, globals: Vec<String>) -> Self {
132        self.mutable_globals = globals;
133        self
134    }
135
136    /// Sets a list of userdata types that will be included in the type information.
137    #[must_use]
138    pub fn set_userdata_types(mut self, types: Vec<String>) -> Self {
139        self.userdata_types = types;
140        self
141    }
142
143    /// Sets a list of disabled builtin libs or functions like tonumber or math.abs
144    pub fn set_disabled_builtins(mut self, libs: Vec<String>) -> Self {
145        self.disabled_builtins = libs;
146        self
147    }
148
149    pub fn set_libraries(&mut self, libraries: CompilerLibraries) -> &mut Self {
150        let mut pointer_vec = Vec::with_capacity(libraries.libraries.len());
151
152        for v in &libraries.libraries {
153            pointer_vec.push(v.as_ptr());
154        }
155
156        self.libs = Some(libraries);
157
158        self
159    }
160
161    #[must_use]
162    pub fn compile(&self, source: impl AsRef<[u8]>) -> CompilerResult {
163        let vector_lib = self.vector_lib.clone();
164        let vector_lib = vector_lib.and_then(|lib| CString::new(lib).ok());
165        let vector_lib = vector_lib.as_ref();
166        let vector_ctor = self.vector_ctor.clone();
167        let vector_ctor = vector_ctor.and_then(|ctor| CString::new(ctor).ok());
168        let vector_ctor = vector_ctor.as_ref();
169        let vector_type = self.vector_type.clone();
170        let vector_type = vector_type.and_then(|t| CString::new(t).ok());
171        let vector_type = vector_type.as_ref();
172
173        macro_rules! vec2cstring_ptr {
174            ($name:ident, $name_ptr:ident) => {
175                let $name = self
176                    .$name
177                    .iter()
178                    .map(|name| CString::new(name.clone()).ok())
179                    .collect::<Option<Vec<_>>>()
180                    .unwrap_or_default();
181                let mut $name = $name.iter().map(|s| s.as_ptr()).collect::<Vec<_>>();
182                let mut $name_ptr = null();
183                if !$name.is_empty() {
184                    $name.push(null());
185                    $name_ptr = $name.as_ptr();
186                }
187            };
188        }
189
190        vec2cstring_ptr!(mutable_globals, mutable_globals_ptr);
191        vec2cstring_ptr!(userdata_types, userdata_types_ptr);
192        vec2cstring_ptr!(disabled_builtins, disabled_builtins_ptr);
193
194        let known_members_vec = self.libs.clone().map(|v| {
195            v.libraries
196                .into_iter()
197                .map(|s| CString::new(s).expect("Known members should not contain null byte"))
198                .collect::<Vec<_>>()
199        });
200
201        let mut known_members_vec_pointer = known_members_vec.map_or_else(
202            || vec![null()],
203            |v| v.iter().map(|c| c.as_ptr()).collect::<Vec<_>>(),
204        );
205
206        known_members_vec_pointer.push(null());
207            
208        unsafe {
209            let mut options = LuauCompileOptions {
210                optimizationLevel: self.optimization_level as c_int,
211                debugLevel: self.debug_level as c_int,
212                typeInfoLevel: self.type_info_level as c_int,
213                coverageLevel: self.coverage_level as c_int,
214                vectorLib: vector_lib.map_or(null(), |s| s.as_ptr()),
215                vectorCtor: vector_ctor.map_or(null(), |s| s.as_ptr()),
216                vectorType: vector_type.map_or(null(), |s| s.as_ptr()),
217                mutableGlobals: mutable_globals_ptr,
218                userdataTypes: userdata_types_ptr,
219                librariesWithKnownMembers: known_members_vec_pointer.as_ptr(),
220                libraryMemberTypeCallback: self.libs.clone().map(|v| v.member_type_callback),
221                libraryMemberConstantCallback: self
222                    .libs
223                    .clone()
224                    .map(|v| v.member_constant_callback),
225                disabledBuiltins: disabled_builtins_ptr,
226            };
227
228            let source = source.as_ref();
229            let mut len: usize = 0;
230
231            let bytecode = luau_compile(
232                source.as_ptr() as _,
233                source.len(),
234                &raw mut options,
235                &raw mut len,
236            );
237
238            CompilerResult { bytecode, len }
239        }
240    }
241}
242
243impl Default for Compiler {
244    fn default() -> Self {
245        Self::new()
246    }
247}
248
249pub struct CompilerResult {
250    bytecode: *const c_char,
251    len: usize,
252}
253
254impl CompilerResult {
255    // not technically unsafe
256    fn bytecode_unchecked(&self) -> &[u8] {
257        unsafe { std::slice::from_raw_parts(self.bytecode as _, self.len) }
258    }
259
260    pub fn bytecode(&self) -> Option<&[u8]> {
261        if self.is_err() {
262            None
263        } else {
264            Some(self.bytecode_unchecked())
265        }
266    }
267
268    pub fn error(&self) -> Option<&str> {
269        if self.is_ok() {
270            None
271        } else {
272            unsafe {
273                Some(
274                    std::str::from_utf8(std::slice::from_raw_parts(
275                        self.bytecode.add(1) as _,
276                        self.len - 1,
277                    ))
278                    .expect("Luau error was not valid UTF-8"),
279                )
280            }
281        }
282    }
283
284    /// Returns true if the compiler result is an error
285    pub fn is_err(&self) -> bool {
286        unsafe { !self.bytecode.is_null() && self.bytecode.read() == 0 }
287    }
288
289    /// Returns true if the compiler result is not an error
290    pub fn is_ok(&self) -> bool {
291        !self.is_err()
292    }
293}
294
295impl Drop for CompilerResult {
296    fn drop(&mut self) {
297        unsafe {
298            cstdlib_free(self.bytecode as _);
299        }
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use std::ffi::c_char;
306
307    use crate::{Luau, LuauBytecodeType, LuauCompilerConstant};
308
309    use super::{Compiler, CompilerLibraries};
310
311    unsafe extern "C-unwind" fn member_type_callback(
312        _: *const c_char,
313        _: *const c_char,
314    ) -> LuauBytecodeType {
315        LuauBytecodeType::LBC_TYPE_BOOLEAN
316    }
317
318    unsafe extern "C-unwind" fn member_constant_callback(
319        _: *const c_char,
320        _: *const c_char,
321        _: LuauCompilerConstant,
322    ) {
323    }
324
325    #[test]
326    fn compiler_success() {
327        let mut compiler = Compiler::new();
328
329        // has an effect so cant be optimized out entirely
330        let result = compiler
331            .set_optimization_level(2)
332            .set_coverage_level(1)
333            .set_mutable_globals(vec!["a".to_string()])
334            .compile("v()");
335
336        assert!(result.is_ok(), "Expected result to be a success");
337        assert!(
338            result.bytecode().is_some(),
339            "Expected resultant bytecode to be some"
340        );
341        assert!(
342            result.bytecode().is_some_and(|v| !v.is_empty()),
343            "Expected resultant bytecode to be non-empty"
344        );
345
346        let luau = Luau::default();
347
348        let load_result = luau.load(None, result.bytecode().unwrap(), 0);
349
350        assert_eq!(load_result, Ok(()));
351    }
352
353    #[test]
354    fn libs() {
355        let mut compiler = Compiler::new();
356
357        compiler.set_libraries(CompilerLibraries::new(
358            vec!["test".to_string()],
359            member_type_callback,
360            member_constant_callback,
361        ));
362
363        let compiler_result = compiler.compile("local a = test.test");
364
365        assert!(compiler_result.is_ok(), "Expected compiler to succeed");
366    }
367
368    #[test]
369    fn cloned_compiler() {
370        let mut compiler = {
371            let original_compiler = Compiler::new();
372            original_compiler.clone()
373        };
374
375        compiler.set_libraries(CompilerLibraries::new(
376            vec!["test".to_string()],
377            member_type_callback,
378            member_constant_callback,
379        ));
380
381        // has an effect so cant be optimized out entirely
382        let result = compiler.compile("v()");
383
384        assert!(result.is_ok(), "Expected result to be a success");
385        assert!(
386            result.bytecode().is_some(),
387            "Expected resultant bytecode to be some"
388        );
389        assert!(
390            result.bytecode().is_some_and(|v| !v.is_empty()),
391            "Expected resultant bytecode to be non-empty"
392        );
393    }
394
395    #[test]
396    fn compiler_error() {
397        let compiler = Compiler::new();
398
399        // will always be an error per an RFC
400        let result = compiler.compile("$");
401
402        assert!(
403            result.is_err(),
404            "Expected the compiler result to be an error"
405        );
406
407        assert!(
408            result.bytecode().is_none(),
409            "Expected the bytecode to be none"
410        );
411        assert!(
412            result.error().is_some(),
413            "Expected the compiler result output a string"
414        );
415    }
416}