From 246d09147a34cf1c3f46348eb67eaa3263442c7c Mon Sep 17 00:00:00 2001 From: Aeledfyr Date: Sun, 30 Jun 2024 15:45:45 -0500 Subject: [PATCH 1/2] Add support for _G to access globals --- src/stdlib/base.rs | 2 ++ tests/scripts-wishlist/blank_test.lua | 1 + tests/scripts-wishlist/globals.lua | 4 ---- tests/scripts/globals.lua | 23 +++++++++++++++++++++++ 4 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 tests/scripts-wishlist/blank_test.lua delete mode 100644 tests/scripts-wishlist/globals.lua create mode 100644 tests/scripts/globals.lua diff --git a/src/stdlib/base.rs b/src/stdlib/base.rs index 5a18ca80..73c9ecf0 100644 --- a/src/stdlib/base.rs +++ b/src/stdlib/base.rs @@ -10,6 +10,8 @@ use crate::{ }; pub fn load_base<'gc>(ctx: Context<'gc>) { + ctx.set_global("_G", ctx.globals()); + ctx.set_global( "tonumber", Callback::from_fn(&ctx, |ctx, _, mut stack| { diff --git a/tests/scripts-wishlist/blank_test.lua b/tests/scripts-wishlist/blank_test.lua new file mode 100644 index 00000000..29a3826e --- /dev/null +++ b/tests/scripts-wishlist/blank_test.lua @@ -0,0 +1 @@ +-- Included to prevent git from deleting the scripts-wishlist directory diff --git a/tests/scripts-wishlist/globals.lua b/tests/scripts-wishlist/globals.lua deleted file mode 100644 index 63d0f0d1..00000000 --- a/tests/scripts-wishlist/globals.lua +++ /dev/null @@ -1,4 +0,0 @@ - --- Example wishlist test; _G isn't implemented yet, so this will fail. - -assert(type(_G) == "table") diff --git a/tests/scripts/globals.lua b/tests/scripts/globals.lua new file mode 100644 index 00000000..6bc30fd8 --- /dev/null +++ b/tests/scripts/globals.lua @@ -0,0 +1,23 @@ + +do + assert(type(_G) == "table") + assert(_G == _ENV) + assert(_G._G == _G) + + a = {} + assert(_G.a == a) + + number = 15 + assert(_G.number == 15) +end + +-- _G can be modified +do + old_g = _G + _G = nil + assert(_G == nil) + + b = {} + assert(_G == nil and old_g.b == b) + _G = old_g +end From 7b5ee021ba83954cc7230d1d9124900182688710 Mon Sep 17 00:00:00 2001 From: Aeledfyr Date: Sun, 30 Jun 2024 15:45:45 -0500 Subject: [PATCH 2/2] Implement load (for text chunks only) --- src/lua.rs | 13 ++- src/stack.rs | 4 + src/stdlib/load.rs | 217 ++++++++++++++++++++++++++++++++++++++ src/stdlib/mod.rs | 5 +- tests/scripts/globals.lua | 16 +++ tests/scripts/load.lua | 152 ++++++++++++++++++++++++++ 6 files changed, 404 insertions(+), 3 deletions(-) create mode 100644 src/stdlib/load.rs create mode 100644 tests/scripts/load.lua diff --git a/src/lua.rs b/src/lua.rs index 67f03007..8af680ab 100644 --- a/src/lua.rs +++ b/src/lua.rs @@ -9,7 +9,9 @@ use gc_arena::{ use crate::{ finalizers::Finalizers, stash::{Fetchable, Stashable}, - stdlib::{load_base, load_coroutine, load_io, load_math, load_string, load_table}, + stdlib::{ + load_base, load_coroutine, load_io, load_load_text, load_math, load_string, load_table, + }, string::InternedStringSet, thread::BadThreadMode, Error, ExternError, FromMultiValue, FromValue, Fuel, IntoValue, Registry, RuntimeError, @@ -158,6 +160,7 @@ impl Lua { pub fn full() -> Self { let mut lua = Lua::core(); lua.load_io(); + lua.load_load_text(); lua } @@ -186,6 +189,14 @@ impl Lua { }) } + /// Load the parts of the stdlib that allow loading new code at runtime + /// from text source code (not bytecode). + pub fn load_load_text(&mut self) { + self.enter(|ctx| { + load_load_text(ctx); + }) + } + /// Size of all memory used by this Lua context. /// /// This is equivalent to `self.gc_metrics().total_allocation()`. This counts all `Gc` allocated diff --git a/src/stack.rs b/src/stack.rs index f0f72e16..541057cb 100644 --- a/src/stack.rs +++ b/src/stack.rs @@ -49,6 +49,10 @@ impl<'gc, 'a> Stack<'gc, 'a> { .unwrap_or_default() } + pub fn get_mut(&mut self, i: usize) -> Option<&mut Value<'gc>> { + self.values.get_mut(self.bottom + i) + } + pub fn push_back(&mut self, value: Value<'gc>) { self.values.push(value); } diff --git a/src/stdlib/load.rs b/src/stdlib/load.rs new file mode 100644 index 00000000..d7e6b56e --- /dev/null +++ b/src/stdlib/load.rs @@ -0,0 +1,217 @@ +use std::pin::Pin; + +use gc_arena::{Collect, Gc}; + +use crate::fuel::count_fuel; +use crate::{ + BoxSequence, Callback, CallbackReturn, Closure, Context, Error, Execution, Function, IntoValue, + Sequence, SequencePoll, Stack, String, Table, TypeError, Value, +}; + +#[derive(Collect, Copy, Clone)] +#[collect(require_static)] +enum LoadMode { + Text, + Binary, + BinaryOrText, +} + +struct LoadInfo<'gc> { + chunk: String<'gc>, + name: Option>, + mode: Option, + env: Option>, +} + +const LOAD_BYTES_PER_FUEL: i32 = 32; + +pub fn load_load_text<'gc>(ctx: Context<'gc>) { + ctx.set_global( + "load", + load_wrapper(ctx, |ctx, info, mut exec| { + let mode = info.mode.unwrap_or(LoadMode::BinaryOrText); + let env = info.env.unwrap_or_else(|| ctx.globals()); + let name = match info.name { + Some(name) => format!("{}", name.display_lossy()), + None => "=(load)".into(), + }; + + if matches!(mode, LoadMode::Binary) { + return Err("loading binary chunks is not currently supported" + .into_value(ctx) + .into()); + } + + let source = info.chunk.as_bytes(); + exec.fuel() + .consume(count_fuel(LOAD_BYTES_PER_FUEL, source.len())); + + let closure = Closure::load_with_env(ctx, Some(&*name), source, env)?; + Ok(closure.into()) + }), + ); +} + +/// An implementation of the argument handling logic for `load` to simplify +/// custom load variants. +/// +/// This implements the argument handling required for a spec-compliant load +/// implementation, and then calls the provided function with the processed +/// arguments (`LoadInfo`). The callback should return either a `Function` or +/// an error, which this will convert to the format expected by `load`. +fn load_wrapper<'gc, F>(ctx: Context<'gc>, load_callback: F) -> Callback<'gc> +where + F: Fn(Context<'gc>, LoadInfo<'gc>, Execution<'gc, '_>) -> Result, Error<'gc>> + + 'static, +{ + let load_callback = Gc::new_static(&ctx, load_callback); + + Callback::from_fn_with(&ctx, load_callback, |&load_callback, ctx, _, mut stack| { + let (chunk, name, mode, env): (Value, Option, Option, Option) = + stack.consume(ctx)?; + + let mode = match mode.as_deref() { + Some(b"t") => Some(LoadMode::Text), + Some(b"b") => Some(LoadMode::Binary), + Some(b"bt") => Some(LoadMode::BinaryOrText), + Some(_m) => { + let error = "invalid load mode"; + stack.replace(ctx, (Value::Nil, error)); + return Ok(CallbackReturn::Return); + } + None => None, + }; + + let root = (name, mode, env, load_callback); + let inner = Callback::from_fn_with(&ctx, root, |&root, ctx, exec, mut stack| { + let (name, mode, env, load_callback) = root; + let chunk: String = stack.consume(ctx)?; + let info = LoadInfo { + chunk, + name, + mode, + env, + }; + match load_callback(ctx, info, exec) { + Ok(func) => stack.push_back(Value::Function(func)), + Err(e) => stack.replace(ctx, (Value::Nil, e.to_string())), + } + Ok(CallbackReturn::Return) + }); + let inner: Function = inner.into(); + + match chunk { + Value::String(_) => { + stack.push_back(chunk); + Ok(CallbackReturn::Call { + function: inner, + then: None, + }) + } + Value::Function(func) => { + // Should this support metamethod-callable values? + // PRLua only allows raw functions here. + Ok(CallbackReturn::Sequence(BoxSequence::new( + &ctx, + BuildLoadString { + step: 0, + total_len: 0, + func, + then: inner, + }, + ))) + } + _ => Err(TypeError { + expected: "string or function", + found: chunk.type_name(), + } + .into()), + } + }) +} + +#[derive(Collect)] +#[collect(no_drop)] +struct BuildLoadString<'gc> { + step: usize, + total_len: usize, + func: Function<'gc>, + then: Function<'gc>, +} + +impl BuildLoadString<'_> { + fn finalize<'gc>(&self, ctx: Context<'gc>, stack: &mut Stack<'gc, '_>) -> String<'gc> { + // There's no easy way to construct the string in-place with gc-arena, + // so we construct the string on the normal heap and copy then it to a + // new piccolo String allocation. + let mut bytes = Vec::with_capacity(self.total_len); + for value in stack.drain(..) { + let Value::String(s) = value else { + unreachable!() // guaranteed by the BuildLoadString sequence + }; + bytes.extend(s.as_bytes()); + } + // This isn't interned as it will only be used by the parser + String::from_slice(&ctx, &bytes) + } +} + +impl<'gc> Sequence<'gc> for BuildLoadString<'gc> { + fn poll( + mut self: Pin<&mut Self>, + ctx: Context<'gc>, + _exec: Execution<'gc, '_>, + mut stack: Stack<'gc, '_>, + ) -> Result, Error<'gc>> { + stack.resize(self.step); + + if self.step != 0 { + let done = match stack.get_mut(self.step - 1) { + None | Some(Value::Nil) => true, + Some(v) => { + // PRLua implicitly converts integer/number values to strings in load + let Some(s) = v.into_string(ctx) else { + let error = Error::from(TypeError { + expected: "string", + found: v.type_name(), + }); + stack.replace(ctx, (Value::Nil, error.to_value(ctx))); + return Ok(SequencePoll::Return); + }; + *v = Value::String(s); + self.total_len += s.len() as usize; + s.is_empty() + } + }; + if done { + // The last arg was nil or an empty string, so the load + // function is done. + stack.pop_back(); + let str = self.finalize(ctx, &mut stack); + stack.push_back(Value::String(str)); + return Ok(SequencePoll::TailCall(self.then)); + } + } + + let bottom = self.step; + self.step += 1; + Ok(SequencePoll::Call { + function: self.func, + bottom, + }) + } + + fn error( + self: Pin<&mut Self>, + ctx: Context<'gc>, + _exec: Execution<'gc, '_>, + error: Error<'gc>, + mut stack: Stack<'gc, '_>, + ) -> Result, Error<'gc>> { + // This catches errors thrown by the inner function; + // PUC-Rio's tests require it, but it's not documented. + let error = error.to_value(ctx); + stack.replace(ctx, (Value::Nil, error)); + Ok(SequencePoll::Return) + } +} diff --git a/src/stdlib/mod.rs b/src/stdlib/mod.rs index aa766153..1392b851 100644 --- a/src/stdlib/mod.rs +++ b/src/stdlib/mod.rs @@ -1,11 +1,12 @@ mod base; mod coroutine; mod io; +mod load; mod math; mod string; mod table; pub use self::{ - base::load_base, coroutine::load_coroutine, io::load_io, math::load_math, string::load_string, - table::load_table, + base::load_base, coroutine::load_coroutine, io::load_io, load::load_load_text, math::load_math, + string::load_string, table::load_table, }; diff --git a/tests/scripts/globals.lua b/tests/scripts/globals.lua index 6bc30fd8..307d6298 100644 --- a/tests/scripts/globals.lua +++ b/tests/scripts/globals.lua @@ -21,3 +21,19 @@ do assert(_G == nil and old_g.b == b) _G = old_g end + +-- Load uses the global context +do + local res = load("return _G")() + assert(res == _G) +end + +-- global context is used, even if _G is modified +-- (_G is not used internally) +do + old_G = _G + _G = nil + local res = load("return old_G")() + assert(res == old_G) + _G = old_G +end diff --git a/tests/scripts/load.lua b/tests/scripts/load.lua new file mode 100644 index 00000000..f4b9e8f3 --- /dev/null +++ b/tests/scripts/load.lua @@ -0,0 +1,152 @@ + +local function array_generator(arr) + local i = 0 + return function() + i = i + 1 + return arr[i] + end +end + +local primitives = { ["nil"] = 0, number = 0, string = 0, boolean = 0, ["function"] = 0, thread = 0 } +local function cmp_array_recurse(a, b) + local a_ty = type(a) + local b_ty = type(b) + if a_ty ~= b_ty then + return false + end + if primitives[a_ty] ~= nil then + return a == b + end + if rawlen(a) ~= rawlen(b) then + return false + end + for i = 1, rawlen(a) do + if not cmp_array_recurse(rawget(a, i), rawget(b, i)) then + return false + end + end + return true +end + +local log_arr = {} +function log(val) + table.insert(log_arr, val) +end + +do + log_arr = {} + + local read_func = array_generator({ "log(1)", "log(2)", "log(3)" }) + local f, err = load(read_func) + assert(f, err) + f() + + assert(cmp_array_recurse(log_arr, { 1, 2, 3 })) +end + +do + log_arr = {} + + local read_func = array_generator({ "log(", 1, ")", "log(", 1.5, ")" }) + local f, err = load(read_func) + assert(f, err) + f() + + assert(cmp_array_recurse(log_arr, { 1, 1.5 })) +end + +do + log_arr = {} + + local read_func = array_generator({ [[log("]], {}, [[")]] }) + local f, err = load(read_func) + assert(f == nil and err ~= nil, "PRLua does not implicitly convert tables to strings in load") +end + +do + -- PRLua does not support the call metamethod for load + local callable = setmetatable({}, { + __call = array_generator({ "log(3)" }) + }) + local res, err = pcall(function() + local f, err = load(callable) + assert(f == nil and err ~= nil) + end) + -- PRLua throws an actual error here (not caught by load itself), + -- but this seems underspecified + assert(not res) +end + +-- Environment Tests +-- By default, load uses the global context +do + local res = load("return _G")() + assert(res == _G) +end + +-- loaded code does not have access to the local scope of the caller +do + local a = 15 + local res = load("return a")() + assert(res == nil) +end + +-- The value of _G does not affect the load logic +do + local old_globals = _G + _G = {} + + local res = load("return _G")() + assert(res == _G) + + _G = old_globals +end + +-- Load defaults to the global context, even from places where the global +-- context is restricted +do + log_arr = {} + + local f, err = load([[ + local inner = load("log(32)") + inner() + log(16) + ]], "name", "t", { load = load }) + assert(f, err) + + local ok, res = pcall(function() f() end) + assert(not ok) + + assert(cmp_array_recurse(log_arr, { 32 })) +end + +do + local module = {} + local f, err = load([[ + a = 1 + b = 2 + c = { [1] = "a" } + ]], "name", "t", module) + assert(f, err) + + f() + + assert(module.a == 1 and module.b == 2 and module.c[1] == "a") +end + +-- Basic argument handling +do + local f, err = load([[ + local args = table.pack(...) + for i = 1, args.n do + log(args[i]) + end + return args[1] + ]]) + assert(f, err) + + log_arr = {} + local r = f("a", "b", "c") + assert(r == "a") + assert(cmp_array_recurse(log_arr, { "a", "b", "c" })) +end