Skip to main content

luaur_rt/
chunk.rs

1//! The [`Chunk`] builder returned by [`Lua::load`]. Mirrors `mlua::Chunk`.
2//!
3//! Compilation reuses the same machinery as the umbrella `luaur` crate's
4//! `compile`/`eval` helpers: source -> `luaur_compiler::compile` -> bytecode ->
5//! `luau_load` -> a Lua function on the stack -> `lua_pcall`.
6
7use crate::error::{Error, Result};
8use crate::function::Function;
9use crate::state::Lua;
10use crate::sys::*;
11use crate::traits::FromLuaMulti;
12
13use luaur_ast::records::parse_options::ParseOptions;
14use luaur_bytecode::records::bytecode_encoder::BytecodeEncoder;
15use luaur_compiler::functions::compile::compile as compiler_compile;
16use luaur_compiler::records::compile_options::CompileOptions;
17
18/// No-op bytecode encoder (same as the umbrella crate's `NoopEncoder`).
19struct NoopEncoder;
20impl BytecodeEncoder for NoopEncoder {
21    fn encode(&mut self, _data: &mut [u32]) {}
22}
23
24/// A not-yet-executed piece of Lua source.
25///
26/// Mirrors `mlua::Chunk`. Produced by [`Lua::load`]; finalized with
27/// [`Chunk::exec`], [`Chunk::eval`], or [`Chunk::into_function`].
28pub struct Chunk {
29    pub(crate) lua: Lua,
30    pub(crate) source: String,
31    pub(crate) name: String,
32    /// Optional environment table applied to the loaded function. Mirrors the
33    /// per-chunk environment set by `mlua::Chunk::set_environment`.
34    pub(crate) environment: Option<crate::table::Table>,
35    /// Optional per-chunk compiler. Mirrors `mlua::Chunk::set_compiler`. When
36    /// `None`, the VM-default compiler (`Lua::set_compiler`) is used, falling
37    /// back to luaur's default options.
38    pub(crate) compiler: Option<crate::compiler::Compiler>,
39}
40
41impl Chunk {
42    /// Override the chunk name shown in error messages / tracebacks.
43    ///
44    /// Mirrors `mlua::Chunk::set_name`.
45    pub fn set_name(mut self, name: impl Into<String>) -> Self {
46        self.name = name.into();
47        self
48    }
49
50    /// Set the environment (globals table) the loaded chunk runs in.
51    ///
52    /// Mirrors `mlua::Chunk::set_environment`. Applied to the function produced
53    /// by [`Chunk::into_function`] / [`Chunk::exec`] / [`Chunk::eval`].
54    pub fn set_environment(mut self, env: crate::table::Table) -> Self {
55        self.environment = Some(env);
56        self
57    }
58
59    /// Set the [`Compiler`](crate::Compiler) used to compile this chunk.
60    /// Mirrors `mlua::Chunk::set_compiler`.
61    pub fn set_compiler(mut self, compiler: crate::compiler::Compiler) -> Self {
62        self.compiler = Some(compiler);
63        self
64    }
65
66    /// Compile the chunk and call it with `args`, converting the result to `R`.
67    /// Mirrors `mlua::Chunk::call`.
68    pub fn call<R: FromLuaMulti>(self, args: impl crate::traits::IntoLuaMulti) -> Result<R> {
69        self.into_function()?.call::<R>(args)
70    }
71
72    /// The current chunk mode is always text here (luaur-rt loads source).
73    /// Mirrors `mlua::Chunk::set_mode` as a no-op accepting `mlua::ChunkMode`'s
74    /// role: luaur-rt auto-detects, so this is provided only for signature
75    /// parity and returns `self` unchanged.
76    pub fn set_mode(self, _mode: ChunkMode) -> Self {
77        self
78    }
79
80    /// The chunk name used for error messages / tracebacks.
81    ///
82    /// Mirrors `mlua::Chunk::name`.
83    pub fn name(&self) -> &str {
84        &self.name
85    }
86
87    /// Compile the source to bytecode (or return a [`Error::SyntaxError`]).
88    fn compile(&self) -> Result<Vec<u8>> {
89        // Pick the effective compiler: a per-chunk one wins over the VM-default
90        // one (`Lua::set_compiler`); otherwise use luaur's default options.
91        let effective = self.compiler.clone().or_else(|| self.lua.vm_compiler());
92        let mut scratch: Vec<*const core::ffi::c_char> = Vec::new();
93        let options = match &effective {
94            Some(c) => c.to_options(&mut scratch),
95            None => CompileOptions::default(),
96        };
97        let parse_options = ParseOptions::default();
98        let mut encoder = NoopEncoder;
99        let owned = self.source.clone();
100        let blob = compiler_compile(
101            &owned,
102            &options,
103            &parse_options,
104            &mut encoder as *mut dyn BytecodeEncoder,
105        );
106        let bytes = blob.into_bytes();
107        // A leading 0 byte is the compiler's error marker.
108        if bytes.first() == Some(&0u8) {
109            let message = String::from_utf8_lossy(&bytes[1..]).into_owned();
110            // Luau reports a syntax error caused by hitting end-of-input (an
111            // unterminated block/expression) with a "got <eof>" suffix; mlua
112            // surfaces that as `incomplete_input: true` so a REPL can keep
113            // reading. Detect it the same way.
114            let incomplete_input = message.contains("<eof>");
115            return Err(Error::SyntaxError {
116                message,
117                incomplete_input,
118            });
119        }
120        Ok(bytes)
121    }
122
123    /// Load the compiled chunk and leave the resulting function on top of the
124    /// stack, returning a [`Function`] handle.
125    ///
126    /// Mirrors `mlua::Chunk::into_function`.
127    pub fn into_function(self) -> Result<Function> {
128        let bytecode = self.compile()?;
129        let state = self.lua.state();
130        unsafe {
131            let chunkname = std::ffi::CString::new(format!("={}", self.name))
132                .unwrap_or_else(|_| std::ffi::CString::new("=chunk").unwrap());
133            let rc = luau_load(
134                state,
135                chunkname.as_ptr(),
136                bytecode.as_ptr() as *const c_char,
137                bytecode.len(),
138                0,
139            );
140            if rc != 0 {
141                // luau_load failure leaves an error message on the stack.
142                return Err(self.lua.pop_error(rc));
143            }
144            let func = Function::from_ref(self.lua.pop_ref());
145            if let Some(env) = &self.environment {
146                func.set_environment(env.clone())?;
147            }
148            Ok(func)
149        }
150    }
151
152    /// Statically type-check this chunk's source against the owning [`Lua`]'s
153    /// accumulated host definitions (the `typecheck` feature).
154    ///
155    /// Returns `Ok(())` when the source type-checks clean, or
156    /// [`Error::TypeError`](crate::Error::TypeError) carrying the structured
157    /// diagnostics otherwise. Because Luau is dynamically typed, the check is
158    /// advisory — it composes with `?` ahead of [`Chunk::exec`] / [`Chunk::eval`]
159    /// without changing what running the chunk does:
160    ///
161    /// ```
162    /// # #[cfg(feature = "typecheck")] {
163    /// # use luaur_rt::Lua;
164    /// let lua = Lua::new();
165    /// let c = lua.load("local x: number = 1\nreturn x");
166    /// c.check().unwrap();
167    /// # }
168    /// ```
169    #[cfg(feature = "typecheck")]
170    #[cfg_attr(docsrs, doc(cfg(feature = "typecheck")))]
171    pub fn check(&self) -> Result<()> {
172        self.lua.check(&self.source)
173    }
174
175    /// Run the chunk for its side effects, discarding return values.
176    ///
177    /// Mirrors `mlua::Chunk::exec`.
178    pub fn exec(self) -> Result<()> {
179        let f = self.into_function()?;
180        f.call::<()>(())
181    }
182
183    /// Run the chunk and convert its return value(s) to `R`.
184    ///
185    /// Mirrors `mlua::Chunk::eval`. Like the Lua REPL (and mlua), the source is
186    /// first tried as an *expression* (by prepending `return `); if that
187    /// compiles it is used, otherwise the chunk is run as a statement block.
188    /// This is what lets `lua.load("coroutine.create(f)").eval::<Thread>()` and
189    /// `lua.load("function() ... end").eval::<Function>()` work.
190    pub fn eval<R: FromLuaMulti>(self) -> Result<R> {
191        // Try the expression form first.
192        let expr = Chunk {
193            lua: self.lua.clone(),
194            source: format!("return {}", self.source),
195            name: self.name.clone(),
196            environment: self.environment.clone(),
197            compiler: self.compiler.clone(),
198        };
199        if let Ok(f) = expr.into_function() {
200            return f.call::<R>(());
201        }
202        // Fall back to statement-block mode.
203        let f = self.into_function()?;
204        f.call::<R>(())
205    }
206
207    /// Asynchronously load the chunk and call it with `args` (the `async`
208    /// feature). Mirrors `mlua::Chunk::call_async`.
209    #[cfg(feature = "async")]
210    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
211    pub async fn call_async<R>(self, args: impl crate::traits::IntoLuaMulti) -> Result<R>
212    where
213        R: FromLuaMulti,
214    {
215        self.into_function()?.call_async(args).await
216    }
217
218    /// Asynchronously run the chunk for its side effects (the `async` feature).
219    /// Mirrors `mlua::Chunk::exec_async`.
220    #[cfg(feature = "async")]
221    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
222    pub async fn exec_async(self) -> Result<()> {
223        self.call_async(()).await
224    }
225
226    /// Asynchronously evaluate the chunk as an expression (or block) and convert
227    /// the result to `R` (the `async` feature). Mirrors `mlua::Chunk::eval_async`.
228    ///
229    /// Like [`Chunk::eval`], the source is first tried as an expression (by
230    /// prepending `return `); if that compiles it is driven, otherwise the chunk
231    /// runs as a statement block.
232    #[cfg(feature = "async")]
233    #[cfg_attr(docsrs, doc(cfg(feature = "async")))]
234    pub async fn eval_async<R>(self) -> Result<R>
235    where
236        R: FromLuaMulti,
237    {
238        // Try the expression form first (mirrors `eval`).
239        let expr = Chunk {
240            lua: self.lua.clone(),
241            source: format!("return {}", self.source),
242            name: self.name.clone(),
243            environment: self.environment.clone(),
244            compiler: self.compiler.clone(),
245        };
246        if let Ok(f) = expr.into_function() {
247            return f.call_async::<R>(()).await;
248        }
249        let f = self.into_function()?;
250        f.call_async::<R>(()).await
251    }
252}
253
254/// How a chunk's bytes are interpreted. Mirrors `mlua::ChunkMode`. luaur-rt
255/// always loads text source, so this exists for signature parity with
256/// [`Chunk::set_mode`].
257#[derive(Debug, Clone, Copy, PartialEq, Eq)]
258pub enum ChunkMode {
259    /// Text source (the default and only supported mode here).
260    Text,
261    /// Precompiled bytecode (not supported by luaur-rt's high-level loader).
262    Binary,
263}