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}