Files
egutierrez d782d463cb asegurate de que subimos todo
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 03:10:00 +02:00

575 lines
19 KiB
C++

#include "lua_engine.h"
extern "C" {
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}
#include <cctype>
#include <cstring>
#include <cstdio>
#include <string>
namespace lua_engine {
struct Engine {
lua_State* L = nullptr;
std::vector<RowCtx*> ctx_stack;
std::vector<int> visiting_derived;
};
namespace {
Engine* g_engine = nullptr;
Engine* engine_from_state(lua_State* L) {
return *static_cast<Engine**>(lua_getextraspace(L));
}
RowCtx* current_ctx(lua_State* L) {
Engine* e = engine_from_state(L);
if (!e || e->ctx_stack.empty()) return nullptr;
return e->ctx_stack.back();
}
// ---------------------------------------------------------------------------
// Push de cell respetando tipo declarado:
// Int/Float -> number (integer si exacto)
// Bool -> boolean (true/false/1/0); en otro caso push string
// Date/String/Json/Auto -> string
// Si types_orig == nullptr -> heuristica: parse_number; si parsea -> number.
// ---------------------------------------------------------------------------
void push_typed(lua_State* L, const char* v, data_table::ColumnType t) {
if (!v || !*v) { lua_pushnil(L); return; }
using data_table::ColumnType;
using data_table::parse_number;
if (t == ColumnType::Int) {
double d;
if (parse_number(v, d)) {
long long iv = (long long)d;
if ((double)iv == d) lua_pushinteger(L, (lua_Integer)iv);
else lua_pushnumber (L, (lua_Number)d);
} else lua_pushstring(L, v);
return;
}
if (t == ColumnType::Float) {
double d;
if (parse_number(v, d)) {
long long iv = (long long)d;
if ((double)iv == d) lua_pushinteger(L, (lua_Integer)iv);
else lua_pushnumber (L, (lua_Number)d);
} else lua_pushstring(L, v);
return;
}
if (t == ColumnType::Bool) {
if (std::strcmp(v, "true") == 0 || std::strcmp(v, "1") == 0) lua_pushboolean(L, 1);
else if (std::strcmp(v, "false") == 0 || std::strcmp(v, "0") == 0) lua_pushboolean(L, 0);
else lua_pushstring(L, v);
return;
}
if (t == ColumnType::Auto) {
// Sin tipo declarado: heuristica. parse_number -> number, else string.
double d;
if (parse_number(v, d)) {
long long iv = (long long)d;
if ((double)iv == d) lua_pushinteger(L, (lua_Integer)iv);
else lua_pushnumber (L, (lua_Number)d);
} else lua_pushstring(L, v);
return;
}
// String / Date / Json
lua_pushstring(L, v);
}
// Fwd: para recursion en row_index.
std::string eval_internal(Engine* e, int id, const RowCtx& ctx, std::string* err_out);
int row_index(lua_State* L) {
Engine* eng = engine_from_state(L);
RowCtx* ctx = current_ctx(L);
if (!ctx) { lua_pushnil(L); return 1; }
using data_table::ColumnType;
auto get_orig_type = [&](int c) -> ColumnType {
if (ctx->types_orig && c < ctx->n_types_orig) return ctx->types_orig[c];
return ColumnType::Auto;
};
if (lua_type(L, 2) == LUA_TSTRING) {
const char* key = lua_tostring(L, 2);
if (ctx->name_to_col) {
auto it = ctx->name_to_col->find(key);
if (it != ctx->name_to_col->end()) {
int col = it->second;
push_typed(L, ctx->cells[ctx->row * ctx->orig_cols + col], get_orig_type(col));
return 1;
}
}
if (ctx->derived_name_to_idx && ctx->derived) {
auto it = ctx->derived_name_to_idx->find(key);
if (it != ctx->derived_name_to_idx->end()) {
int didx = it->second;
if (didx < 0 || didx >= (int)ctx->derived->size()) {
lua_pushnil(L); return 1;
}
// cycle check
for (int v : eng->visiting_derived) {
if (v == didx) { lua_pushnil(L); return 1; }
}
const auto& d = (*ctx->derived)[didx];
if (d.formula.empty()) {
// retipo puro
if (d.source_col < 0 || d.source_col >= ctx->orig_cols) {
lua_pushnil(L); return 1;
}
push_typed(L, ctx->cells[ctx->row * ctx->orig_cols + d.source_col], d.type);
} else if (d.lua_id < 0) {
lua_pushnil(L);
} else {
eng->visiting_derived.push_back(didx);
std::string err;
std::string r = eval_internal(eng, d.lua_id, *ctx, &err);
eng->visiting_derived.pop_back();
push_typed(L, r.c_str(), d.type);
}
return 1;
}
}
lua_pushnil(L);
return 1;
}
if (lua_type(L, 2) == LUA_TNUMBER) {
int idx = (int)lua_tointeger(L, 2);
if (idx >= 1 && idx <= ctx->orig_cols) {
int col = idx - 1;
push_typed(L, ctx->cells[ctx->row * ctx->orig_cols + col], get_orig_type(col));
return 1;
}
}
lua_pushnil(L);
return 1;
}
// --- fn.* builtins ---
int b_upper(lua_State* L) {
const char* s = luaL_checkstring(L, 1);
std::string out(s);
for (char& c : out) if (c >= 'a' && c <= 'z') c -= 32;
lua_pushlstring(L, out.data(), out.size());
return 1;
}
int b_lower(lua_State* L) {
const char* s = luaL_checkstring(L, 1);
std::string out(s);
for (char& c : out) if (c >= 'A' && c <= 'Z') c += 32;
lua_pushlstring(L, out.data(), out.size());
return 1;
}
int b_length(lua_State* L) {
if (lua_isnil(L, 1)) { lua_pushinteger(L, 0); return 1; }
const char* s = luaL_checkstring(L, 1);
lua_pushinteger(L, (lua_Integer)std::strlen(s));
return 1;
}
int b_substring(lua_State* L) {
const char* s = luaL_checkstring(L, 1);
int start = (int)luaL_checkinteger(L, 2);
int len = (int)luaL_optinteger(L, 3, -1);
int slen = (int)std::strlen(s);
if (start < 1) start = 1;
if (start > slen) { lua_pushlstring(L, "", 0); return 1; }
int from = start - 1;
int take = (len < 0) ? slen - from : len;
if (from + take > slen) take = slen - from;
lua_pushlstring(L, s + from, take);
return 1;
}
int b_contains(lua_State* L) {
const char* h = luaL_checkstring(L, 1);
const char* n = luaL_checkstring(L, 2);
lua_pushboolean(L, std::strstr(h, n) != nullptr);
return 1;
}
int b_starts_with(lua_State* L) {
const char* h = luaL_checkstring(L, 1);
const char* n = luaL_checkstring(L, 2);
size_t ln = std::strlen(n);
lua_pushboolean(L, std::strncmp(h, n, ln) == 0);
return 1;
}
int b_ends_with(lua_State* L) {
const char* h = luaL_checkstring(L, 1);
const char* n = luaL_checkstring(L, 2);
size_t lh = std::strlen(h), ln = std::strlen(n);
lua_pushboolean(L, ln <= lh && std::strcmp(h + lh - ln, n) == 0);
return 1;
}
int b_replace(lua_State* L) {
const char* s = luaL_checkstring(L, 1);
const char* find = luaL_checkstring(L, 2);
const char* repl = luaL_checkstring(L, 3);
std::string out;
size_t flen = std::strlen(find);
if (flen == 0) { lua_pushstring(L, s); return 1; }
for (const char* p = s; *p; ) {
if (std::strncmp(p, find, flen) == 0) { out += repl; p += flen; }
else { out += *p++; }
}
lua_pushlstring(L, out.data(), out.size());
return 1;
}
int b_trim(lua_State* L) {
const char* s = luaL_checkstring(L, 1);
while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') ++s;
const char* e = s + std::strlen(s);
while (e > s && (e[-1] == ' ' || e[-1] == '\t' || e[-1] == '\n' || e[-1] == '\r')) --e;
lua_pushlstring(L, s, e - s);
return 1;
}
int b_concat(lua_State* L) {
int n = lua_gettop(L);
std::string out;
for (int i = 1; i <= n; ++i) {
size_t sl = 0;
const char* s = luaL_tolstring(L, i, &sl);
out.append(s, sl);
lua_pop(L, 1);
}
lua_pushlstring(L, out.data(), out.size());
return 1;
}
int b_to_number(lua_State* L) {
if (lua_isnumber(L, 1)) { lua_pushvalue(L, 1); return 1; }
const char* s = luaL_checkstring(L, 1);
char* end = nullptr;
double v = std::strtod(s, &end);
if (end == s) { lua_pushnil(L); return 1; }
lua_pushnumber(L, v);
return 1;
}
int b_to_string(lua_State* L) { luaL_tolstring(L, 1, nullptr); return 1; }
int b_to_bool(lua_State* L) {
if (lua_isboolean(L, 1)) { lua_pushvalue(L, 1); return 1; }
const char* s = luaL_optstring(L, 1, "");
lua_pushboolean(L, std::strcmp(s, "true") == 0 || std::strcmp(s, "1") == 0);
return 1;
}
int b_is_null(lua_State* L) { lua_pushboolean(L, lua_isnil(L, 1)); return 1; }
int b_is_empty(lua_State* L) {
if (lua_isnil(L, 1)) { lua_pushboolean(L, 1); return 1; }
const char* s = luaL_optstring(L, 1, "");
lua_pushboolean(L, *s == 0);
return 1;
}
int b_coalesce(lua_State* L) {
int n = lua_gettop(L);
for (int i = 1; i <= n; ++i) {
if (!lua_isnil(L, i)) { lua_pushvalue(L, i); return 1; }
}
lua_pushnil(L);
return 1;
}
int b_parse_date(lua_State* L) {
const char* s = luaL_checkstring(L, 1);
if (std::strlen(s) < 10) { lua_pushnil(L); return 1; }
int y, m, d;
if (std::sscanf(s, "%d-%d-%d", &y, &m, &d) != 3) { lua_pushnil(L); return 1; }
lua_createtable(L, 0, 3);
lua_pushinteger(L, y); lua_setfield(L, -2, "year");
lua_pushinteger(L, m); lua_setfield(L, -2, "month");
lua_pushinteger(L, d); lua_setfield(L, -2, "day");
return 1;
}
int b_year(lua_State* L) {
const char* s = luaL_checkstring(L, 1);
int y; if (std::sscanf(s, "%d", &y) != 1) { lua_pushnil(L); return 1; }
lua_pushinteger(L, y); return 1;
}
int b_month(lua_State* L) {
const char* s = luaL_checkstring(L, 1);
int y, m; if (std::sscanf(s, "%d-%d", &y, &m) != 2) { lua_pushnil(L); return 1; }
lua_pushinteger(L, m); return 1;
}
int b_day(lua_State* L) {
const char* s = luaL_checkstring(L, 1);
int y, m, d; if (std::sscanf(s, "%d-%d-%d", &y, &m, &d) != 3) { lua_pushnil(L); return 1; }
lua_pushinteger(L, d); return 1;
}
void apply_sandbox(lua_State* L) {
const char* nuke[] = { "io", "require", "loadfile", "dofile", "load",
"package", "debug", nullptr };
for (int i = 0; nuke[i]; ++i) {
lua_pushnil(L);
lua_setglobal(L, nuke[i]);
}
lua_getglobal(L, "os");
if (lua_istable(L, -1)) {
lua_createtable(L, 0, 4);
const char* keep[] = {"date", "time", "difftime", "clock", nullptr};
for (int i = 0; keep[i]; ++i) {
lua_getfield(L, -2, keep[i]);
lua_setfield(L, -2, keep[i]);
}
lua_setglobal(L, "os");
}
lua_pop(L, 1);
}
void register_builtins(lua_State* L) {
lua_createtable(L, 0, 24);
#define R(name, fn) lua_pushcfunction(L, fn); lua_setfield(L, -2, name);
R("upper", b_upper);
R("lower", b_lower);
R("length", b_length);
R("substring", b_substring);
R("contains", b_contains);
R("starts_with", b_starts_with);
R("ends_with", b_ends_with);
R("replace", b_replace);
R("trim", b_trim);
R("concat", b_concat);
R("to_number", b_to_number);
R("to_string", b_to_string);
R("to_bool", b_to_bool);
R("is_null", b_is_null);
R("is_empty", b_is_empty);
R("coalesce", b_coalesce);
R("parse_date", b_parse_date);
R("year", b_year);
R("month", b_month);
R("day", b_day);
#undef R
lua_setglobal(L, "fn");
}
void install_row_metatable(lua_State* L) {
luaL_newmetatable(L, "fn_row_meta");
lua_pushcfunction(L, row_index);
lua_setfield(L, -2, "__index");
lua_pop(L, 1);
}
// ---------------------------------------------------------------------------
// Preprocesador: [col] -> row["col"] respetando strings y comentarios.
// Auto-prepend `return` si la formula es expresion suelta.
// ---------------------------------------------------------------------------
bool ident_start(unsigned char c) {
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_' || c >= 0x80;
}
// Para nombres de cols dentro de [name]: permite espacios para "col with space"
// y '.' para futuro `alias.col` post-join (fase 9 — issue 0078).
bool ident_cont(unsigned char c) {
return ident_start(c) || (c >= '0' && c <= '9') || c == ' ' || c == '.';
}
// Para boundary de keywords Lua: NO permite espacio.
bool word_char(unsigned char c) {
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
(c >= '0' && c <= '9') || c == '_' || c >= 0x80;
}
bool kw_at(const std::string& s, size_t i, const char* kw) {
size_t k = std::strlen(kw);
if (i + k > s.size()) return false;
if (s.compare(i, k, kw) != 0) return false;
if (i + k == s.size()) return true;
unsigned char nc = (unsigned char)s[i + k];
return !word_char(nc);
}
bool needs_auto_return(const std::string& body) {
size_t i = 0;
while (i < body.size()) {
char c = body[i];
if (c == ' ' || c == '\t' || c == '\n' || c == '\r') { ++i; continue; }
// skip short comment
if (c == '-' && i + 1 < body.size() && body[i+1] == '-') {
// long comment?
if (i + 3 < body.size() && body[i+2] == '[' && body[i+3] == '[') {
size_t j = i + 4;
while (j + 1 < body.size() && !(body[j] == ']' && body[j+1] == ']')) ++j;
i = (j + 1 < body.size()) ? j + 2 : body.size();
continue;
}
while (i < body.size() && body[i] != '\n') ++i;
continue;
}
break;
}
if (i >= body.size()) return false;
const char* kws[] = {"return","if","for","while","do","local","repeat","function", nullptr};
for (int k = 0; kws[k]; ++k) if (kw_at(body, i, kws[k])) return false;
return true;
}
std::string brackets_pass(const std::string& src) {
std::string out;
out.reserve(src.size() + 16);
size_t i = 0;
while (i < src.size()) {
char c = src[i];
// strings
if (c == '"' || c == '\'') {
char q = c;
out += c; ++i;
while (i < src.size()) {
char d = src[i];
out += d; ++i;
if (d == '\\' && i < src.size()) { out += src[i++]; continue; }
if (d == q) break;
if (d == '\n') break;
}
continue;
}
// comentario corto / largo
if (c == '-' && i + 1 < src.size() && src[i+1] == '-') {
// long: --[[ ... ]]
if (i + 3 < src.size() && src[i+2] == '[' && src[i+3] == '[') {
out.append(src, i, 4); i += 4;
while (i + 1 < src.size() && !(src[i] == ']' && src[i+1] == ']')) {
out += src[i++];
}
if (i + 1 < src.size()) { out += src[i++]; out += src[i++]; }
continue;
}
// short
while (i < src.size() && src[i] != '\n') { out += src[i++]; }
continue;
}
// long string [[ ... ]]
if (c == '[' && i + 1 < src.size() && src[i+1] == '[') {
out.append(src, i, 2); i += 2;
while (i + 1 < src.size() && !(src[i] == ']' && src[i+1] == ']')) {
out += src[i++];
}
if (i + 1 < src.size()) { out += src[i++]; out += src[i++]; }
continue;
}
// bracket col-ref [name]
if (c == '[') {
// peek if next is valid ident_start
if (i + 1 < src.size() && ident_start((unsigned char)src[i+1])) {
size_t j = i + 1;
while (j < src.size() && src[j] != ']' && src[j] != '\n') {
if (!ident_cont((unsigned char)src[j])) { j = std::string::npos; break; }
++j;
}
if (j != std::string::npos && j < src.size() && src[j] == ']') {
std::string name(src, i + 1, j - i - 1);
// trim trailing space
while (!name.empty() && name.back() == ' ') name.pop_back();
out += "row[\"";
out += name;
out += "\"]";
i = j + 1;
continue;
}
}
}
out += c;
++i;
}
return out;
}
} // anon
std::string preprocess(const std::string& body) {
std::string pre = brackets_pass(body);
if (needs_auto_return(pre)) return "return " + pre;
return pre;
}
namespace {
std::string eval_internal(Engine* e, int id, const RowCtx& ctx, std::string* err_out) {
if (!e || !e->L || id < 0) {
if (err_out) *err_out = "invalid handle";
return "";
}
lua_State* L = e->L;
e->ctx_stack.push_back(const_cast<RowCtx*>(&ctx));
lua_rawgeti(L, LUA_REGISTRYINDEX, id);
lua_newuserdata(L, 1);
luaL_setmetatable(L, "fn_row_meta");
int rc = lua_pcall(L, 1, 1, 0);
e->ctx_stack.pop_back();
if (rc != LUA_OK) {
if (err_out) *err_out = lua_tostring(L, -1) ? lua_tostring(L, -1) : "runtime error";
lua_pop(L, 1);
return "";
}
std::string out;
if (lua_isnil(L, -1)) out = "";
else {
size_t n = 0;
const char* s = luaL_tolstring(L, -1, &n);
out.assign(s, n);
lua_pop(L, 1);
}
lua_pop(L, 1);
return out;
}
} // anon
Engine* get() {
if (g_engine) return g_engine;
g_engine = new Engine();
g_engine->L = luaL_newstate();
luaL_openlibs(g_engine->L);
*static_cast<Engine**>(lua_getextraspace(g_engine->L)) = g_engine;
apply_sandbox(g_engine->L);
register_builtins(g_engine->L);
install_row_metatable(g_engine->L);
return g_engine;
}
void shutdown() {
if (!g_engine) return;
lua_close(g_engine->L);
delete g_engine;
g_engine = nullptr;
}
int compile(Engine* e, const std::string& body, std::string* err_out) {
if (!e || !e->L) { if (err_out) *err_out = "engine null"; return -1; }
lua_State* L = e->L;
std::string final_body = preprocess(body);
std::string wrapped = "return function(row)\n" + final_body + "\nend";
if (luaL_loadbufferx(L, wrapped.data(), wrapped.size(), "formula", "t") != LUA_OK) {
if (err_out) *err_out = lua_tostring(L, -1) ? lua_tostring(L, -1) : "parse error";
lua_pop(L, 1);
return -1;
}
if (lua_pcall(L, 0, 1, 0) != LUA_OK) {
if (err_out) *err_out = lua_tostring(L, -1) ? lua_tostring(L, -1) : "compile error";
lua_pop(L, 1);
return -1;
}
if (!lua_isfunction(L, -1)) {
if (err_out) *err_out = "formula did not produce a function";
lua_pop(L, 1);
return -1;
}
int ref = luaL_ref(L, LUA_REGISTRYINDEX);
return ref;
}
void release(Engine* e, int id) {
if (!e || !e->L || id < 0) return;
luaL_unref(e->L, LUA_REGISTRYINDEX, id);
}
std::string eval(Engine* e, int id, const RowCtx& ctx, std::string* err_out) {
return eval_internal(e, id, ctx, err_out);
}
lua_State* raw_state() {
Engine* e = get();
return e ? e->L : nullptr;
}
} // namespace lua_engine