17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-17 12:09:19 +02:00
Files
maszyna/vehicle/graph/graph_lua.cpp

593 lines
16 KiB
C++

/*
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
*/
#include "stdafx.h"
#include "vehicle/graph/graph_lua.h"
#include <algorithm>
#include <cmath>
#ifdef WITH_LUA
#include <lua.hpp>
#endif
namespace vgraph
{
namespace
{
log_sink &sink()
{
static log_sink instance;
return instance;
}
} // namespace
void set_log_sink(log_sink Sink)
{
sink() = std::move(Sink);
}
void log_message(std::string const &Message)
{
if (sink())
{
sink()(Message);
}
}
// block_random
block_random::block_random(std::uint64_t const Seed) : state(Seed != 0 ? Seed : 0x9E3779B97F4A7C15ull) {}
double block_random::next()
{
// xorshift64*: small, fast, and easy to reproduce exactly in the editor
state ^= state >> 12;
state ^= state << 25;
state ^= state >> 27;
return static_cast<double>((state * 0x2545F4914F6CDD1Dull) >> 11) * (1.0 / 9007199254740992.0);
}
std::uint64_t block_random::seed_from(std::string const &Text)
{
// fnv-1a
std::uint64_t hash{0xcbf29ce484222325ull};
for (auto const character : Text)
{
hash ^= static_cast<unsigned char>(character);
hash *= 0x100000001b3ull;
}
return hash;
}
#ifdef WITH_LUA
namespace
{
// keys of values kept out of reach of the scripts: light userdata can't be created from lua
char const block_definition_key{0};
char const block_name_key{0};
// instructions between checks of the budget, and the number of checks allowed in a single call
int constexpr hook_interval{1000};
int constexpr call_budget{2000}; // 2 million instructions; far more than any block needs in one step
thread_local int budget_remaining{0};
void budget_hook(lua_State *L, lua_Debug *)
{
if (--budget_remaining <= 0)
{
budget_remaining = call_budget; // leave room for the error handling
luaL_error(L, "instruction budget exceeded, endless loop?");
}
}
void open_library(lua_State *L, lua_CFunction Open, char const *Name)
{
lua_pushcfunction(L, Open);
lua_pushstring(L, Name);
lua_call(L, 1, 0);
}
// copies the fields of the table at Source into a new table pushed on the stack
void push_copy(lua_State *L, int const Source)
{
auto const source{Source < 0 ? lua_gettop(L) + Source + 1 : Source};
lua_newtable(L);
lua_pushnil(L);
while (lua_next(L, source) != 0)
{
lua_pushvalue(L, -2);
lua_insert(L, -2);
lua_rawset(L, -4);
}
}
// math.random and math.randomseed with the generator in the closure's upvalue, the same semantics as in lua 5.1
int script_random(lua_State *L)
{
auto *random{static_cast<block_random *>(lua_touserdata(L, lua_upvalueindex(1)))};
auto const r{random->next()};
switch (lua_gettop(L))
{
case 0:
{
lua_pushnumber(L, r);
return 1;
}
case 1:
{
auto const upper{luaL_checknumber(L, 1)};
luaL_argcheck(L, 1.0 <= upper, 1, "interval is empty");
lua_pushnumber(L, std::floor(r * std::floor(upper)) + 1.0);
return 1;
}
case 2:
{
auto const lower{std::floor(luaL_checknumber(L, 1))};
auto const upper{std::floor(luaL_checknumber(L, 2))};
luaL_argcheck(L, lower <= upper, 2, "interval is empty");
lua_pushnumber(L, std::floor(r * (upper - lower + 1.0)) + lower);
return 1;
}
default:
{
return luaL_error(L, "wrong number of arguments");
}
}
}
int script_randomseed(lua_State *L)
{
auto *random{static_cast<block_random *>(lua_touserdata(L, lua_upvalueindex(1)))};
// splitmix64 finalizer spreads small seeds over the whole state
auto x{static_cast<std::uint64_t>(static_cast<std::int64_t>(std::floor(luaL_checknumber(L, 1))))};
x += 0x9E3779B97F4A7C15ull;
x = (x ^ (x >> 30)) * 0xBF58476D1CE4E5B9ull;
x = (x ^ (x >> 27)) * 0x94D049BB133111EBull;
x ^= x >> 31;
*random = block_random{x};
return 0;
}
// print sends the message to the log, prefixed with the node id; limited, so a script printing in every step can't flood the log
int script_print(lua_State *L)
{
auto const count{static_cast<int>(lua_tointeger(L, lua_upvalueindex(2)))};
if (count >= 50)
{
return 0;
}
lua_pushinteger(L, count + 1);
lua_replace(L, lua_upvalueindex(2));
std::string message{"lua ["};
message += lua_tostring(L, lua_upvalueindex(1));
message += "]:";
auto const arguments{lua_gettop(L)};
lua_getglobal(L, "tostring");
for (int idx = 1; idx <= arguments; ++idx)
{
lua_pushvalue(L, -1);
lua_pushvalue(L, idx);
lua_call(L, 1, 1);
auto const *text{lua_tostring(L, -1)};
message += ' ';
message += (text != nullptr ? text : "?");
lua_pop(L, 1);
}
lua_pop(L, 1);
if (count + 1 == 50)
{
message += " (further messages of this block are suppressed)";
}
log_message(message);
return 0;
}
// block "name" returns a function, which takes the definition table and stores it in the environment
int script_block_definition(lua_State *L)
{
luaL_checktype(L, 1, LUA_TTABLE);
lua_pushvalue(L, lua_upvalueindex(1)); // environment
lua_pushlightuserdata(L, const_cast<char *>(&block_definition_key));
lua_pushvalue(L, 1);
lua_rawset(L, -3);
lua_pushlightuserdata(L, const_cast<char *>(&block_name_key));
lua_pushvalue(L, lua_upvalueindex(2));
lua_rawset(L, -3);
return 0;
}
int script_block(lua_State *L)
{
if (lua_type(L, 1) == LUA_TTABLE)
{
// block { ... } without a name
lua_pushstring(L, "");
lua_insert(L, 1);
}
luaL_checkstring(L, 1);
lua_pushvalue(L, lua_upvalueindex(1)); // environment
lua_pushvalue(L, 1); // name
lua_pushcclosure(L, script_block_definition, 2);
if (lua_gettop(L) >= 3 && lua_type(L, 2) == LUA_TTABLE)
{
// block("name", { ... })
lua_pushvalue(L, 2);
lua_call(L, 1, 0);
return 0;
}
return 1;
}
// libraries available to the scripts. everything else (io, os, package, debug, ffi, jit, loading code) stays out
char const *const base_functions[]{"assert", "error", "ipairs", "next", "pairs", "pcall", "select", "tonumber",
"tostring", "type", "unpack", "xpcall", "rawequal", "rawget", "rawset", "setmetatable",
"getmetatable", "_VERSION"};
char const *const libraries[]{"string", "table", "math", "bit", "coroutine"};
lua_State *create_state()
{
auto *L{luaL_newstate()};
if (L == nullptr)
{
return nullptr;
}
// interpreter only: instruction count hooks don't run inside compiled traces, and the budget has to stop endless loops
luaJIT_setmode(L, 0, LUAJIT_MODE_ENGINE | LUAJIT_MODE_OFF);
open_library(L, luaopen_base, "");
open_library(L, luaopen_table, LUA_TABLIBNAME);
open_library(L, luaopen_string, LUA_STRLIBNAME);
open_library(L, luaopen_math, LUA_MATHLIBNAME);
open_library(L, luaopen_bit, LUA_BITLIBNAME);
// string.dump would give access to bytecode
lua_getglobal(L, "string");
lua_pushnil(L);
lua_setfield(L, -2, "dump");
lua_pop(L, 1);
// the metatable of strings leads to the shared string library; hiding it keeps the blocks from changing it for others
lua_pushstring(L, "");
if (lua_getmetatable(L, -1) != 0)
{
lua_pushboolean(L, 0);
lua_setfield(L, -2, "__metatable");
lua_pop(L, 1);
}
lua_pop(L, 1);
lua_sethook(L, budget_hook, LUA_MASKCOUNT, hook_interval);
return L;
}
// pushes a new environment for the block with the specified node id
void push_environment(lua_State *L, std::string const &Nodeid)
{
lua_newtable(L);
auto const environment{lua_gettop(L)};
for (auto const *name : base_functions)
{
lua_getglobal(L, name);
lua_setfield(L, environment, name);
}
for (auto const *name : libraries)
{
lua_getglobal(L, name);
if (lua_istable(L, -1))
{
// copies, so changes made by one block don't leak to the others
push_copy(L, -1);
lua_setfield(L, environment, name);
}
lua_pop(L, 1);
}
// random generator of the block, seeded from the node id
lua_getfield(L, environment, "math");
auto *random{static_cast<block_random *>(lua_newuserdata(L, sizeof(block_random)))};
*random = block_random{block_random::seed_from(Nodeid)};
lua_pushvalue(L, -1);
lua_pushcclosure(L, script_random, 1);
lua_setfield(L, -3, "random");
lua_pushcclosure(L, script_randomseed, 1);
lua_setfield(L, -2, "randomseed");
lua_pop(L, 1);
// print to the log
lua_pushstring(L, Nodeid.c_str());
lua_pushinteger(L, 0);
lua_pushcclosure(L, script_print, 2);
lua_setfield(L, environment, "print");
lua_pushvalue(L, environment);
lua_setfield(L, environment, "_G");
// block definition
lua_pushvalue(L, environment);
lua_pushcclosure(L, script_block, 1);
lua_setfield(L, environment, "block");
}
// runs the chunk on top of the stack within the budget; returns false with the message on failure
bool protected_call(lua_State *L, int const Arguments, std::string &Error)
{
budget_remaining = call_budget;
if (lua_pcall(L, Arguments, 0, 0) != 0)
{
auto const *message{lua_tostring(L, -1)};
Error = (message != nullptr ? message : "unknown error");
lua_pop(L, 1);
return false;
}
return true;
}
// loads and runs the script in the environment on top of the stack; leaves the definition table on the stack, or returns false
bool run_script(lua_State *L, std::string const &Script, std::string const &Chunkname, std::string &Error)
{
auto const environment{lua_gettop(L)};
auto const chunkname{"=" + Chunkname};
if (luaL_loadbuffer(L, Script.data(), Script.size(), chunkname.c_str()) != 0)
{
Error = lua_tostring(L, -1);
lua_pop(L, 1);
return false;
}
lua_pushvalue(L, environment);
lua_setfenv(L, -2);
if (false == protected_call(L, 0, Error))
{
return false;
}
lua_pushlightuserdata(L, const_cast<char *>(&block_definition_key));
lua_rawget(L, environment);
if (false == lua_istable(L, -1))
{
lua_pop(L, 1);
Error = "the script doesn't call block \"name\" { ... }";
return false;
}
lua_getfield(L, -1, "update");
auto const hasupdate{lua_isfunction(L, -1)};
lua_pop(L, 1);
if (false == hasupdate)
{
lua_pop(L, 1);
Error = "the block has no update(self, i, o, dt) function";
return false;
}
return true;
}
bool is_identifier(std::string const &Text)
{
if (Text.empty() || (Text[0] >= '0' && Text[0] <= '9'))
{
return false;
}
return std::all_of(std::begin(Text), std::end(Text), [](char const Char) { return (Char >= 'a' && Char <= 'z') || (Char >= 'A' && Char <= 'Z') || (Char >= '0' && Char <= '9') || Char == '_'; });
}
// reads inputs or outputs of the definition table on top of the stack
bool read_ports(lua_State *L, char const *Field, port_direction const Direction, std::vector<port_def> &Ports, std::string &Error)
{
lua_getfield(L, -1, Field);
if (lua_isnil(L, -1))
{
lua_pop(L, 1);
return true;
}
if (false == lua_istable(L, -1))
{
lua_pop(L, 1);
Error = std::string{"\""} + Field + "\" has to be a table";
return false;
}
std::vector<port_def> ports;
lua_pushnil(L);
while (lua_next(L, -2) != 0)
{
if (lua_type(L, -2) != LUA_TSTRING || lua_type(L, -1) != LUA_TSTRING)
{
lua_pop(L, 3);
Error = std::string{"entries of \""} + Field + "\" have to look like name = \"bool\" | \"int\" | \"float\"";
return false;
}
std::string const key{lua_tostring(L, -2)};
std::string type{lua_tostring(L, -1)};
std::transform(std::begin(type), std::end(type), std::begin(type), [](unsigned char const Char) { return static_cast<char>(std::tolower(Char)); });
lua_pop(L, 1);
if (false == is_identifier(key))
{
lua_pop(L, 2);
Error = "port name \"" + key + "\" has to be made of letters, digits and '_'";
return false;
}
port_def port;
port.key = key;
port.direction = Direction;
port.dom = domain::signal;
port.optional = true;
if (type == "bool")
{
port.type = value_type::boolean;
}
else if (type == "int")
{
port.type = value_type::integer;
}
else if (type == "float")
{
port.type = value_type::real;
}
else
{
lua_pop(L, 2);
Error = "port \"" + key + "\" has unknown type \"" + type + "\", expected bool, int or float";
return false;
}
ports.push_back(std::move(port));
}
lua_pop(L, 1);
std::sort(std::begin(ports), std::end(ports), [](port_def const &Left, port_def const &Right) { return Left.key < Right.key; });
Ports.insert(std::end(Ports), std::begin(ports), std::end(ports));
return true;
}
} // namespace
bool lua_available()
{
return true;
}
bool parse_lua_block(std::string const &Script, std::string const &Chunkname, lua_block_definition &Definition, std::string &Error)
{
lua_host host;
auto *L{host.state()};
if (L == nullptr)
{
Error = "can't create lua state";
return false;
}
push_environment(L, Chunkname);
if (false == run_script(L, Script, Chunkname, Error))
{
return false;
}
// definition table on top of the stack, environment below
lua_pushlightuserdata(L, const_cast<char *>(&block_name_key));
lua_rawget(L, -3);
Definition.name = (lua_isstring(L, -1) ? lua_tostring(L, -1) : "");
lua_pop(L, 1);
if (false == read_ports(L, "inputs", port_direction::in, Definition.ports, Error) || false == read_ports(L, "outputs", port_direction::out, Definition.ports, Error))
{
return false;
}
for (std::size_t first = 0; first < Definition.ports.size(); ++first)
{
for (auto second = first + 1; second < Definition.ports.size(); ++second)
{
if (Definition.ports[first].key == Definition.ports[second].key)
{
Error = "\"" + Definition.ports[first].key + "\" is both an input and an output";
return false;
}
}
}
lua_getfield(L, -1, "params");
if (lua_istable(L, -1))
{
lua_pushnil(L);
while (lua_next(L, -2) != 0)
{
// numeric parameters can be set per vehicle in the graph; others stay as the script defines them
if (lua_type(L, -2) == LUA_TSTRING && lua_type(L, -1) == LUA_TNUMBER)
{
param_def param;
param.key = std::string{"lua."} + lua_tostring(L, -2);
param.type = param_type::number;
param.default_value = param_value::make_number(lua_tonumber(L, -1));
Definition.params.push_back(std::move(param));
}
lua_pop(L, 1);
}
}
lua_pop(L, 1);
std::sort(std::begin(Definition.params), std::end(Definition.params), [](param_def const &Left, param_def const &Right) { return Left.key < Right.key; });
lua_getfield(L, -1, "rate");
if (false == lua_isnil(L, -1))
{
if (lua_type(L, -1) != LUA_TNUMBER || lua_tonumber(L, -1) < 0.0)
{
Error = "\"rate\" has to be a number of updates per second, 0 or more";
return false;
}
Definition.rate = lua_tonumber(L, -1);
}
lua_pop(L, 1);
return true;
}
lua_host::lua_host() : m_state(create_state()) {}
lua_host::~lua_host()
{
if (m_state != nullptr)
{
lua_close(m_state);
}
}
int lua_host::load_block(std::string const &Script, std::string const &Nodeid, std::string &Error)
{
if (m_state == nullptr)
{
Error = "can't create lua state";
return LUA_NOREF;
}
auto *L{m_state};
auto const top{lua_gettop(L)};
push_environment(L, Nodeid);
if (false == run_script(L, Script, Nodeid, Error))
{
lua_settop(L, top);
return LUA_NOREF;
}
auto const reference{luaL_ref(L, LUA_REGISTRYINDEX)};
lua_settop(L, top);
return reference;
}
bool lua_host::call(int const Arguments, std::string &Error)
{
return protected_call(m_state, Arguments, Error);
}
void lua_host::release(int const Reference)
{
if (m_state != nullptr && Reference != LUA_NOREF && Reference != LUA_REFNIL)
{
luaL_unref(m_state, LUA_REGISTRYINDEX, Reference);
}
}
#else
bool lua_available()
{
return false;
}
bool parse_lua_block(std::string const &, std::string const &, lua_block_definition &, std::string &Error)
{
Error = "this build of the simulator has no lua support (WITH_LUA)";
return false;
}
lua_host::lua_host() = default;
lua_host::~lua_host() = default;
int lua_host::load_block(std::string const &, std::string const &, std::string &Error)
{
Error = "this build of the simulator has no lua support (WITH_LUA)";
return -1;
}
bool lua_host::call(int, std::string &Error)
{
Error = "this build of the simulator has no lua support (WITH_LUA)";
return false;
}
void lua_host::release(int) {}
#endif
} // namespace vgraph