Skip to main content

outl_exec/runtimes/
lua.rs

1//! `lua` runtime — Lua 5.4 via [mlua](https://github.com/mlua-rs/mlua).
2//!
3//! mlua statically links the Lua C library through the `vendored`
4//! feature, so the host doesn't need Lua installed. We override
5//! `print` to funnel into a captured buffer.
6//!
7//! Gated behind the `lang-lua` feature.
8
9use std::sync::{Arc, Mutex};
10use std::time::Instant;
11
12use mlua::{Lua, MultiValue, Value, Variadic};
13
14use crate::runtime::{ExecContext, ExecError, ExecOutput, ExitStatus, OutputFormat, Runtime};
15
16/// mlua-backed Lua 5.4 runtime.
17pub struct LuaRuntime;
18
19impl Runtime for LuaRuntime {
20    fn language(&self) -> &'static str {
21        "lua"
22    }
23
24    fn execute(&self, source: &str, _ctx: &ExecContext) -> Result<ExecOutput, ExecError> {
25        let start = Instant::now();
26        let lua = Lua::new();
27        let buffer: Arc<Mutex<String>> = Arc::new(Mutex::new(String::new()));
28
29        // Override `print` to write to our buffer instead of stdout.
30        let sink = buffer.clone();
31        let print_fn = lua
32            .create_function(move |_, args: Variadic<Value>| {
33                let mut s = sink.lock().unwrap();
34                for (i, v) in args.iter().enumerate() {
35                    if i > 0 {
36                        s.push('\t');
37                    }
38                    s.push_str(&lua_value_tostring(v));
39                }
40                s.push('\n');
41                Ok(())
42            })
43            .map_err(|e| ExecError::Sandbox(format!("install print: {e}")))?;
44        lua.globals()
45            .set("print", print_fn)
46            .map_err(|e| ExecError::Sandbox(format!("set global print: {e}")))?;
47
48        match lua.load(source).eval::<MultiValue>() {
49            Ok(values) => {
50                let mut stdout = buffer.lock().unwrap().clone();
51                if stdout.is_empty() && !values.is_empty() {
52                    // Auto-display the last value of the chunk: `1 + 2`
53                    // returns 3 from `eval`, we show it.
54                    if let Some(last) = values.iter().last() {
55                        stdout.push_str(&lua_value_tostring(last));
56                    }
57                }
58                Ok(ExecOutput {
59                    stdout,
60                    stderr: String::new(),
61                    duration: start.elapsed(),
62                    exit: ExitStatus::Ok,
63                    format: OutputFormat::Text,
64                })
65            }
66            Err(e) => Ok(ExecOutput {
67                stdout: buffer.lock().unwrap().clone(),
68                stderr: e.to_string(),
69                duration: start.elapsed(),
70                exit: ExitStatus::Trap("lua-error".into()),
71                format: OutputFormat::Text,
72            }),
73        }
74    }
75}
76
77fn lua_value_tostring(v: &Value) -> String {
78    match v {
79        Value::Nil => "nil".into(),
80        Value::Boolean(b) => b.to_string(),
81        Value::Integer(i) => i.to_string(),
82        Value::Number(n) => n.to_string(),
83        Value::String(s) => s.to_str().map(|s| s.to_string()).unwrap_or_default(),
84        other => format!("{other:?}"),
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    fn run(src: &str) -> String {
93        LuaRuntime
94            .execute(src, &ExecContext::default())
95            .unwrap()
96            .stdout
97    }
98
99    #[test]
100    fn print_writes_stdout() {
101        assert_eq!(run("print(1 + 2)"), "3\n");
102    }
103
104    #[test]
105    fn return_value_auto_printed() {
106        assert_eq!(run("return 6 * 7"), "42");
107    }
108
109    #[test]
110    fn string_concat() {
111        assert_eq!(run("print('hello ' .. 'world')"), "hello world\n");
112    }
113
114    #[test]
115    fn tables_and_loops() {
116        let out = run("local s=0; for i=1,5 do s=s+i end; print(s)");
117        assert_eq!(out, "15\n");
118    }
119
120    #[test]
121    fn syntax_error_returns_trap() {
122        let out = LuaRuntime
123            .execute("function (", &ExecContext::default())
124            .unwrap();
125        assert!(matches!(out.exit, ExitStatus::Trap(_)));
126    }
127}