#include "lua_engine.h" extern "C" { #include "lua.h" #include "lualib.h" #include "lauxlib.h" } #include #include #include #include namespace lua_engine { struct Engine { lua_State* L = nullptr; std::vector ctx_stack; std::vector visiting_derived; }; namespace { Engine* g_engine = nullptr; Engine* engine_from_state(lua_State* L) { return *static_cast(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(&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(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