merge: issue/0045-cpp-extract-pure-logic — implementación paralela
This commit is contained in:
@@ -17,12 +17,14 @@ add_imgui_app(primitives_gallery
|
||||
${CMAKE_SOURCE_DIR}/functions/core/timeline.cpp
|
||||
demos_sql.cpp
|
||||
demos_scientific.cpp
|
||||
# text_editor + file_watcher (issue 0025)
|
||||
# text_editor + file_watcher (issue 0025) + file_poll_diff pure (issue 0045)
|
||||
${CMAKE_SOURCE_DIR}/functions/core/text_editor.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/core/file_watcher.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/core/file_poll_diff.cpp
|
||||
${CMAKE_SOURCE_DIR}/vendor/imgui_text_edit/TextEditor.cpp
|
||||
# sql_workbench (issue 0032)
|
||||
# sql_workbench (issue 0032) + sql_parse pure (issue 0045)
|
||||
${CMAKE_SOURCE_DIR}/functions/core/sql_workbench.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/core/sql_parse.cpp
|
||||
# Core primitives demoed (tokens vive en fn_framework)
|
||||
${CMAKE_SOURCE_DIR}/functions/core/fullscreen_window.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/core/page_header.cpp
|
||||
@@ -38,6 +40,7 @@ add_imgui_app(primitives_gallery
|
||||
${CMAKE_SOURCE_DIR}/functions/core/toast.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/core/tree_view.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/core/process_runner.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/core/process_state_machine.cpp
|
||||
# Viz primitives demoed
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/kpi_card.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/viz/bar_chart.cpp
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
add_imgui_app(shaders_lab
|
||||
main.cpp
|
||||
compiler.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/gfx/gl_loader.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/gfx/gl_shader.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/gfx/gl_framebuffer.cpp
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
#include "compiler.h"
|
||||
|
||||
#include "gfx/shader_canvas.h"
|
||||
#include "gfx/gl_shader.h"
|
||||
#include "gfx/uniform_parser.h"
|
||||
#include "gfx/uniform_panel.h"
|
||||
#include "gfx/dag_compile.h"
|
||||
#include "gfx/dag_uniforms.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// ── Globals declarados en main.cpp (single source of truth) ─────────────────
|
||||
extern fn::gfx::ShaderCanvas g_canvas_code;
|
||||
extern fn::gfx::ShaderCanvas g_canvas_dag;
|
||||
extern std::string g_source;
|
||||
extern std::string g_code_err;
|
||||
extern int g_code_err_line;
|
||||
extern std::chrono::steady_clock::time_point g_code_last_edit;
|
||||
extern bool g_code_dirty;
|
||||
extern std::vector<fn::gfx::UniformDescriptor> g_descs;
|
||||
extern fn::gfx::UniformStore g_store;
|
||||
extern std::vector<fn::gfx::DagStep> g_pipeline;
|
||||
extern std::string g_dag_glsl;
|
||||
extern std::string g_dag_err;
|
||||
extern int g_dag_err_line;
|
||||
|
||||
namespace shaders_lab {
|
||||
|
||||
void compile_code() {
|
||||
auto r = fn::gfx::compile_fragment(g_source);
|
||||
if (r.ok) {
|
||||
g_descs = fn::gfx::parse_uniforms(g_source);
|
||||
fn::gfx::uniforms_sync(g_store, g_descs);
|
||||
fn::gfx::canvas_set_program(g_canvas_code, r.program);
|
||||
g_code_err.clear();
|
||||
g_code_err_line = -1;
|
||||
} else {
|
||||
g_code_err = r.err_msg;
|
||||
g_code_err_line = r.err_line;
|
||||
}
|
||||
}
|
||||
|
||||
void compile_dag() {
|
||||
g_dag_glsl = fn::gfx::compile_dag_to_glsl(g_pipeline);
|
||||
auto r = fn::gfx::compile_fragment(g_dag_glsl);
|
||||
if (r.ok) {
|
||||
fn::gfx::canvas_set_program(g_canvas_dag, r.program);
|
||||
g_dag_err.clear();
|
||||
g_dag_err_line = -1;
|
||||
} else {
|
||||
g_dag_err = r.err_msg;
|
||||
g_dag_err_line = r.err_line;
|
||||
}
|
||||
}
|
||||
|
||||
void mark_code_dirty() {
|
||||
g_code_last_edit = std::chrono::steady_clock::now();
|
||||
g_code_dirty = true;
|
||||
}
|
||||
|
||||
} // namespace shaders_lab
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
// shaders_lab/compiler — extrae las rutinas impuras de compilacion del shader
|
||||
// (compile_code, compile_dag, mark_code_dirty) desde main.cpp para que el
|
||||
// archivo principal quede acotado a la composicion de paneles ImGui.
|
||||
//
|
||||
// Las globals (g_source, g_descs, g_store, g_pipeline, etc.) se declaran
|
||||
// extern y viven en main.cpp; aqui solo orquestamos compilacion.
|
||||
|
||||
namespace shaders_lab {
|
||||
|
||||
// Compila g_source -> programa OpenGL para g_canvas_code, refresca g_descs
|
||||
// y sincroniza g_store. Actualiza g_code_err / g_code_err_line.
|
||||
void compile_code();
|
||||
|
||||
// Compila g_pipeline -> g_dag_glsl -> programa OpenGL para g_canvas_dag.
|
||||
// Actualiza g_dag_err / g_dag_err_line.
|
||||
void compile_dag();
|
||||
|
||||
// Marca el shader Code como dirty y registra el timestamp del ultimo edit
|
||||
// (para debounce de 250ms en el render loop).
|
||||
void mark_code_dirty();
|
||||
|
||||
} // namespace shaders_lab
|
||||
@@ -19,6 +19,8 @@
|
||||
#include "core/app_menubar.h"
|
||||
#include "core/layout_storage.h"
|
||||
|
||||
#include "compiler.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
@@ -27,8 +29,9 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
static fn::gfx::ShaderCanvas g_canvas_code;
|
||||
static fn::gfx::ShaderCanvas g_canvas_dag;
|
||||
// Globals: linked extern desde compiler.cpp. NO `static` aqui.
|
||||
fn::gfx::ShaderCanvas g_canvas_code;
|
||||
fn::gfx::ShaderCanvas g_canvas_dag;
|
||||
|
||||
// Default placeholder so the Code panel does something useful on first launch
|
||||
// without committing to one specific look.
|
||||
@@ -47,19 +50,19 @@ void main() {
|
||||
}
|
||||
)glsl";
|
||||
|
||||
static std::string g_source = CODE_PLACEHOLDER;
|
||||
static std::string g_code_err;
|
||||
static int g_code_err_line = -1;
|
||||
static std::chrono::steady_clock::time_point g_code_last_edit;
|
||||
static bool g_code_dirty = true;
|
||||
static std::vector<fn::gfx::UniformDescriptor> g_descs;
|
||||
static fn::gfx::UniformStore g_store;
|
||||
std::string g_source = CODE_PLACEHOLDER;
|
||||
std::string g_code_err;
|
||||
int g_code_err_line = -1;
|
||||
std::chrono::steady_clock::time_point g_code_last_edit;
|
||||
bool g_code_dirty = true;
|
||||
std::vector<fn::gfx::UniformDescriptor> g_descs;
|
||||
fn::gfx::UniformStore g_store;
|
||||
|
||||
static std::vector<fn::gfx::DagStep> g_pipeline;
|
||||
static std::string g_dag_glsl;
|
||||
static std::string g_dag_err;
|
||||
static int g_dag_err_line = -1;
|
||||
static bool g_dag_dirty = true;
|
||||
std::vector<fn::gfx::DagStep> g_pipeline;
|
||||
std::string g_dag_glsl;
|
||||
std::string g_dag_err;
|
||||
int g_dag_err_line = -1;
|
||||
static bool g_dag_dirty = true; // solo lo usa main.cpp
|
||||
|
||||
// ── Panel visibility (toggled from View menu and panel close button) ──────
|
||||
static bool g_show_code = true;
|
||||
@@ -84,37 +87,10 @@ static char g_save_desc[256] = "";
|
||||
static char g_save_tags[128] = "shaders_lab,user";
|
||||
static std::string g_save_err;
|
||||
|
||||
static void compile_code() {
|
||||
auto r = fn::gfx::compile_fragment(g_source);
|
||||
if (r.ok) {
|
||||
g_descs = fn::gfx::parse_uniforms(g_source);
|
||||
fn::gfx::uniforms_sync(g_store, g_descs);
|
||||
fn::gfx::canvas_set_program(g_canvas_code, r.program);
|
||||
g_code_err.clear();
|
||||
g_code_err_line = -1;
|
||||
} else {
|
||||
g_code_err = r.err_msg;
|
||||
g_code_err_line = r.err_line;
|
||||
}
|
||||
}
|
||||
|
||||
static void compile_dag() {
|
||||
g_dag_glsl = fn::gfx::compile_dag_to_glsl(g_pipeline);
|
||||
auto r = fn::gfx::compile_fragment(g_dag_glsl);
|
||||
if (r.ok) {
|
||||
fn::gfx::canvas_set_program(g_canvas_dag, r.program);
|
||||
g_dag_err.clear();
|
||||
g_dag_err_line = -1;
|
||||
} else {
|
||||
g_dag_err = r.err_msg;
|
||||
g_dag_err_line = r.err_line;
|
||||
}
|
||||
}
|
||||
|
||||
static void mark_code_dirty() {
|
||||
g_code_last_edit = std::chrono::steady_clock::now();
|
||||
g_code_dirty = true;
|
||||
}
|
||||
// compile_code, compile_dag, mark_code_dirty viven en compiler.cpp
|
||||
using shaders_lab::compile_code;
|
||||
using shaders_lab::compile_dag;
|
||||
using shaders_lab::mark_code_dirty;
|
||||
|
||||
static void ensure_dag_default() {
|
||||
if (g_pipeline.empty()) {
|
||||
|
||||
@@ -7,6 +7,7 @@ add_imgui_app(text_editor_smoke
|
||||
main.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/core/text_editor.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/core/file_watcher.cpp
|
||||
${CMAKE_SOURCE_DIR}/functions/core/file_poll_diff.cpp
|
||||
${CMAKE_SOURCE_DIR}/vendor/imgui_text_edit/TextEditor.cpp
|
||||
)
|
||||
target_include_directories(text_editor_smoke PRIVATE
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
#include "core/file_poll_diff.h"
|
||||
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace fn_ui {
|
||||
|
||||
FileDiff file_poll_diff(const std::vector<FileEntry>& before,
|
||||
const std::vector<FileEntry>& after) {
|
||||
FileDiff out;
|
||||
|
||||
// Index before by path para lookup O(1).
|
||||
std::unordered_map<std::string, const FileEntry*> idx_before;
|
||||
idx_before.reserve(before.size());
|
||||
for (const auto& e : before) {
|
||||
idx_before[e.path] = &e;
|
||||
}
|
||||
|
||||
// Recorrer after: clasificar added/modified.
|
||||
std::unordered_set<std::string> seen_in_after;
|
||||
seen_in_after.reserve(after.size());
|
||||
for (const auto& e : after) {
|
||||
seen_in_after.insert(e.path);
|
||||
auto it = idx_before.find(e.path);
|
||||
if (it == idx_before.end()) {
|
||||
out.added.push_back(e.path);
|
||||
} else {
|
||||
const FileEntry* prev = it->second;
|
||||
if (prev->size != e.size || prev->mtime != e.mtime) {
|
||||
out.modified.push_back(e.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recorrer before: detectar removed (paths que no estan en after).
|
||||
for (const auto& e : before) {
|
||||
if (seen_in_after.find(e.path) == seen_in_after.end()) {
|
||||
out.removed.push_back(e.path);
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace fn_ui
|
||||
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
// file_poll_diff — diff puro entre dos snapshots de filesystem.
|
||||
//
|
||||
// Pareja natural de file_watcher: en plataformas sin inotify/RDCW (o cuando
|
||||
// se quiere fallback portable), el caller toma snapshots periodicos via
|
||||
// stat()/opendir() y los compara con esta funcion para emitir eventos
|
||||
// added/modified/removed. Sin I/O, sin estado.
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace fn_ui {
|
||||
|
||||
struct FileEntry {
|
||||
std::string path;
|
||||
uint64_t size = 0;
|
||||
int64_t mtime = 0; // unix seconds
|
||||
};
|
||||
|
||||
struct FileDiff {
|
||||
std::vector<std::string> added;
|
||||
std::vector<std::string> modified; // size o mtime distinto
|
||||
std::vector<std::string> removed;
|
||||
};
|
||||
|
||||
// Pura: calcula diff entre dos snapshots por path. Asume entries con paths
|
||||
// unicos en cada snapshot. Orden de los vectores: el orden en que aparecen
|
||||
// los paths en `after` (added/modified) y en `before` (removed).
|
||||
FileDiff file_poll_diff(const std::vector<FileEntry>& before,
|
||||
const std::vector<FileEntry>& after);
|
||||
|
||||
} // namespace fn_ui
|
||||
@@ -0,0 +1,77 @@
|
||||
---
|
||||
name: file_poll_diff
|
||||
kind: function
|
||||
lang: cpp
|
||||
domain: core
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "fn_ui::FileDiff fn_ui::file_poll_diff(const std::vector<fn_ui::FileEntry>& before, const std::vector<fn_ui::FileEntry>& after)"
|
||||
description: "Diff puro entre dos snapshots de filesystem (path/size/mtime). Devuelve vectores added/modified/removed. Sin I/O. Pareja del file_watcher para fallback portable basado en polling."
|
||||
tags: [filesystem, diff, poll, snapshot, pure]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: []
|
||||
tested: true
|
||||
tests: ["file_poll_diff detects added/modified/removed", "file_poll_diff: empty inputs", "file_poll_diff: identical snapshots"]
|
||||
test_file_path: "cpp/tests/test_file_poll_diff.cpp"
|
||||
file_path: "cpp/functions/core/file_poll_diff.cpp"
|
||||
params:
|
||||
- name: before
|
||||
desc: "Snapshot anterior. Vector de FileEntry {path, size, mtime}, paths unicos"
|
||||
- name: after
|
||||
desc: "Snapshot actual. Mismo formato. Paths unicos"
|
||||
output: "FileDiff con tres vectores de paths: added (en after y no en before), modified (path comun pero size o mtime distinto), removed (en before y no en after)."
|
||||
---
|
||||
|
||||
# file_poll_diff
|
||||
|
||||
Logica pura para calcular cambios de filesystem entre dos snapshots. Sin
|
||||
I/O, sin estado: el caller hace `stat()`/`opendir()` por su lado, construye
|
||||
los `FileEntry`, y esta funcion los compara.
|
||||
|
||||
## API
|
||||
|
||||
```cpp
|
||||
namespace fn_ui {
|
||||
|
||||
struct FileEntry {
|
||||
std::string path;
|
||||
uint64_t size = 0;
|
||||
int64_t mtime = 0; // unix seconds
|
||||
};
|
||||
|
||||
struct FileDiff {
|
||||
std::vector<std::string> added;
|
||||
std::vector<std::string> modified;
|
||||
std::vector<std::string> removed;
|
||||
};
|
||||
|
||||
FileDiff file_poll_diff(const std::vector<FileEntry>& before,
|
||||
const std::vector<FileEntry>& after);
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
## Reglas
|
||||
|
||||
- Comparacion por `path` exacto.
|
||||
- "Modified": al menos uno de `size` o `mtime` cambia.
|
||||
- Asume paths unicos por snapshot (la funcion no deduplica).
|
||||
- Complejidad: O(N+M) con un `unordered_map<string, const FileEntry*>` para
|
||||
indexar `before`.
|
||||
|
||||
## Cuando usar
|
||||
|
||||
- En plataformas donde `file_watcher` no tiene backend nativo (stub) y se
|
||||
necesita un fallback basado en polling.
|
||||
- Para consolidar bursts de eventos: tomar dos snapshots en el tiempo y
|
||||
reportar solo el cambio neto (sin los intermedios).
|
||||
- Tests del watcher: simular cambios de FS sin tocar disco.
|
||||
|
||||
## Por que pura
|
||||
|
||||
No abre archivos, no llama `stat`, no usa el reloj. Misma entrada produce la
|
||||
misma salida — testeable sin fixtures de FS.
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "file_watcher.h"
|
||||
|
||||
#include "core/file_poll_diff.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
@@ -280,4 +282,29 @@ const char* file_watcher_last_error(const FileWatcher* w) { return w ? w->last_
|
||||
|
||||
#endif
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Helper portable basado en file_poll_diff (puro). Comun a todas las plataformas.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
std::vector<FileEvent> file_watcher_events_from_diff(
|
||||
const std::vector<FileSnapshotEntry>& before,
|
||||
const std::vector<FileSnapshotEntry>& after) {
|
||||
|
||||
// Convertir a fn_ui::FileEntry para llamar a la funcion pura.
|
||||
std::vector<fn_ui::FileEntry> a, b;
|
||||
a.reserve(before.size());
|
||||
b.reserve(after.size());
|
||||
for (const auto& e : before) a.push_back({e.path, e.size, e.mtime});
|
||||
for (const auto& e : after) b.push_back({e.path, e.size, e.mtime});
|
||||
|
||||
fn_ui::FileDiff diff = fn_ui::file_poll_diff(a, b);
|
||||
|
||||
std::vector<FileEvent> out;
|
||||
out.reserve(diff.added.size() + diff.modified.size() + diff.removed.size());
|
||||
for (const auto& p : diff.added) out.push_back({p, FileEvent::Created});
|
||||
for (const auto& p : diff.modified) out.push_back({p, FileEvent::Modified});
|
||||
for (const auto& p : diff.removed) out.push_back({p, FileEvent::Deleted});
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace fn
|
||||
|
||||
@@ -7,7 +7,12 @@
|
||||
// Otros: stub (poll() devuelve siempre vacio)
|
||||
//
|
||||
// API no bloqueante: poll() drena los eventos disponibles desde la ultima llamada.
|
||||
//
|
||||
// Para plataformas sin backend nativo, el caller puede usar la helper
|
||||
// file_watcher_events_from_diff que delega en `file_poll_diff` (puro) para
|
||||
// derivar FileEvents a partir de dos snapshots tomados con stat()/opendir().
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -21,6 +26,14 @@ struct FileEvent {
|
||||
Kind kind;
|
||||
};
|
||||
|
||||
// Snapshot de filesystem (forma simple, identica al fn_ui::FileEntry de
|
||||
// file_poll_diff_cpp_core). Se replica aqui para no acoplar headers en apps.
|
||||
struct FileSnapshotEntry {
|
||||
std::string path;
|
||||
uint64_t size = 0;
|
||||
int64_t mtime = 0;
|
||||
};
|
||||
|
||||
// Crea un watcher vacio. El caller llama destroy.
|
||||
FileWatcher* file_watcher_create();
|
||||
|
||||
@@ -38,4 +51,14 @@ std::vector<FileEvent> file_watcher_poll(FileWatcher* w);
|
||||
// Devuelve el mensaje del ultimo error (vacio si no hay).
|
||||
const char* file_watcher_last_error(const FileWatcher* w);
|
||||
|
||||
// Helper portable: convierte dos snapshots de FS en FileEvents usando la
|
||||
// logica pura `file_poll_diff_cpp_core`. Util en plataformas sin inotify/
|
||||
// RDCW o como fallback de polling. El caller construye los snapshots con
|
||||
// stat()/opendir().
|
||||
//
|
||||
// Mapeo: added -> Created, modified -> Modified, removed -> Deleted.
|
||||
std::vector<FileEvent> file_watcher_events_from_diff(
|
||||
const std::vector<FileSnapshotEntry>& before,
|
||||
const std::vector<FileSnapshotEntry>& after);
|
||||
|
||||
} // namespace fn
|
||||
|
||||
@@ -8,7 +8,7 @@ purity: impure
|
||||
signature: "fn::FileWatcher* fn::file_watcher_create(); void fn::file_watcher_destroy(fn::FileWatcher*); bool fn::file_watcher_add(fn::FileWatcher*, const char* path); std::vector<fn::FileEvent> fn::file_watcher_poll(fn::FileWatcher*); const char* fn::file_watcher_last_error(const fn::FileWatcher*)"
|
||||
description: "Watcher de archivos/directorios cross-platform (Linux inotify, Windows ReadDirectoryChangesW). API no bloqueante: registra paths con add() y consulta eventos con poll(). Cada poll() drena todos los eventos pendientes desde la llamada anterior."
|
||||
tags: [filesystem, watcher, inotify, file_events, io]
|
||||
uses_functions: []
|
||||
uses_functions: ["file_poll_diff_cpp_core"]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
|
||||
@@ -1,10 +1,26 @@
|
||||
#include "core/process_runner.h"
|
||||
#include "core/process_state_machine.h"
|
||||
#include "core/tokens.h"
|
||||
#include <imgui.h>
|
||||
#include <cmath>
|
||||
|
||||
namespace fn_ui {
|
||||
|
||||
// Helpers para puentear el RunnerState (publico) con la SM pura.
|
||||
// RunnerState y ProcessState comparten orden y valores enteros (0..3).
|
||||
namespace {
|
||||
ProcessState to_ps(RunnerState s) { return static_cast<ProcessState>(static_cast<int>(s)); }
|
||||
RunnerState to_rs(ProcessState s) { return static_cast<RunnerState>(static_cast<int>(s)); }
|
||||
|
||||
// Aplica un evento al estado actual del runner via process_transition.
|
||||
void apply_event(ProcessRunner& r, std::atomic<int>& state, ProcessEvent ev) {
|
||||
(void)r;
|
||||
int cur = state.load();
|
||||
ProcessState ns = process_transition(to_ps(static_cast<RunnerState>(cur)), ev);
|
||||
state.store(static_cast<int>(to_rs(ns)));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ProcessRunner::ProcessRunner() = default;
|
||||
|
||||
ProcessRunner::~ProcessRunner() {
|
||||
@@ -26,7 +42,8 @@ std::string ProcessRunner::message() const {
|
||||
|
||||
void ProcessRunner::reset() {
|
||||
if (is_busy()) return;
|
||||
state_.store(static_cast<int>(RunnerState::Idle));
|
||||
// Reset event: Success/Error -> Idle (no-op si ya en Idle).
|
||||
apply_event(*this, state_, ProcessEvent::Reset);
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
message_.clear();
|
||||
}
|
||||
@@ -36,7 +53,9 @@ void runner_trigger(ProcessRunner& r,
|
||||
if (r.is_busy()) return;
|
||||
if (r.th_.joinable()) r.th_.join();
|
||||
|
||||
r.state_.store(static_cast<int>(RunnerState::Running));
|
||||
// Si venimos de Success/Error, primero Reset -> Idle, luego Spawned -> Running.
|
||||
apply_event(r, r.state_, ProcessEvent::Reset);
|
||||
apply_event(r, r.state_, ProcessEvent::Spawned);
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(r.mu_);
|
||||
r.message_.clear();
|
||||
@@ -55,7 +74,8 @@ void runner_trigger(ProcessRunner& r,
|
||||
std::lock_guard<std::mutex> lk(r.mu_);
|
||||
r.message_ = std::move(out);
|
||||
}
|
||||
r.state_.store(static_cast<int>(ok ? RunnerState::Success : RunnerState::Error));
|
||||
// Running -> Success/Error via SM pura.
|
||||
apply_event(r, r.state_, ok ? ProcessEvent::Finished : ProcessEvent::Failed);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ purity: impure
|
||||
signature: "class fn_ui::ProcessRunner { is_busy(); state(); message(); reset(); }; void runner_trigger(ProcessRunner&, std::function<bool(std::string&)>); void runner_status(const ProcessRunner&, const char* label)"
|
||||
description: "Ejecuta una tarea en std::thread en background y expone estado thread-safe (idle/running/success/error). Incluye widget inline con spinner + resultado."
|
||||
tags: [imgui, ui, async, thread, runner, tokens]
|
||||
uses_functions: ["tokens_cpp_core"]
|
||||
uses_functions: ["process_state_machine_cpp_core", "tokens_cpp_core"]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
#include "core/process_state_machine.h"
|
||||
|
||||
namespace fn_ui {
|
||||
|
||||
ProcessState process_transition(ProcessState s, ProcessEvent e) {
|
||||
using S = ProcessState;
|
||||
using E = ProcessEvent;
|
||||
switch (s) {
|
||||
case S::Idle:
|
||||
// Trigger es una solicitud, el cambio real ocurre con Spawned.
|
||||
if (e == E::Spawned) return S::Running;
|
||||
return s;
|
||||
case S::Running:
|
||||
if (e == E::Finished) return S::Success;
|
||||
if (e == E::Failed || e == E::Timeout) return S::Error;
|
||||
return s;
|
||||
case S::Success:
|
||||
case S::Error:
|
||||
if (e == E::Reset) return S::Idle;
|
||||
return s;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
const char* process_state_name(ProcessState s) {
|
||||
switch (s) {
|
||||
case ProcessState::Idle: return "Idle";
|
||||
case ProcessState::Running: return "Running";
|
||||
case ProcessState::Success: return "Success";
|
||||
case ProcessState::Error: return "Error";
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
const char* process_event_name(ProcessEvent e) {
|
||||
switch (e) {
|
||||
case ProcessEvent::Trigger: return "Trigger";
|
||||
case ProcessEvent::Spawned: return "Spawned";
|
||||
case ProcessEvent::Finished: return "Finished";
|
||||
case ProcessEvent::Failed: return "Failed";
|
||||
case ProcessEvent::Timeout: return "Timeout";
|
||||
case ProcessEvent::Reset: return "Reset";
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
} // namespace fn_ui
|
||||
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
// process_state_machine — logica pura de transiciones de un proceso async
|
||||
// (idle/running/success/error) a partir de eventos. Sin threads, sin I/O.
|
||||
//
|
||||
// Diseñada para que `process_runner` (impuro, threads + popen) delegue las
|
||||
// transiciones aqui y la UI/CLI puedan razonar sobre el estado sin depender
|
||||
// de la implementacion concreta del runner.
|
||||
|
||||
namespace fn_ui {
|
||||
|
||||
enum class ProcessState {
|
||||
Idle,
|
||||
Running,
|
||||
Success,
|
||||
Error,
|
||||
};
|
||||
|
||||
enum class ProcessEvent {
|
||||
Trigger, // se solicita lanzar (no cambia estado por si solo)
|
||||
Spawned, // el proceso/task realmente arranco
|
||||
Finished, // termino con exito
|
||||
Failed, // termino con error
|
||||
Timeout, // se cancelo por timeout (-> Error)
|
||||
Reset, // limpiar a Idle (solo desde Success/Error)
|
||||
};
|
||||
|
||||
// Pura: dado (state, event) devuelve el nuevo estado. Transiciones invalidas
|
||||
// devuelven el mismo estado sin mutar.
|
||||
ProcessState process_transition(ProcessState s, ProcessEvent e);
|
||||
|
||||
// Nombres legibles para logs/UI (sin alocar — punteros a literales staticos).
|
||||
const char* process_state_name(ProcessState s);
|
||||
const char* process_event_name(ProcessEvent e);
|
||||
|
||||
} // namespace fn_ui
|
||||
@@ -0,0 +1,71 @@
|
||||
---
|
||||
name: process_state_machine
|
||||
kind: function
|
||||
lang: cpp
|
||||
domain: core
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "fn_ui::ProcessState fn_ui::process_transition(fn_ui::ProcessState s, fn_ui::ProcessEvent e); const char* fn_ui::process_state_name(fn_ui::ProcessState); const char* fn_ui::process_event_name(fn_ui::ProcessEvent)"
|
||||
description: "State machine puro para procesos async. Estados: Idle, Running, Success, Error. Eventos: Trigger, Spawned, Finished, Failed, Timeout, Reset. Transiciones invalidas devuelven el mismo estado sin mutar."
|
||||
tags: [state_machine, process, async, runner, pure]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: []
|
||||
tested: true
|
||||
tests: ["process_transition: idle/spawned -> running", "process_transition: running -> success/error", "process_transition: reset from terminal", "process_transition: invalid events keep state", "process_state_name and process_event_name"]
|
||||
test_file_path: "cpp/tests/test_process_state_machine.cpp"
|
||||
file_path: "cpp/functions/core/process_state_machine.cpp"
|
||||
params:
|
||||
- name: s
|
||||
desc: "Estado actual (Idle, Running, Success, Error)"
|
||||
- name: e
|
||||
desc: "Evento a aplicar (Trigger, Spawned, Finished, Failed, Timeout, Reset)"
|
||||
output: "Nuevo ProcessState. Si la transicion (s, e) no esta definida, devuelve s sin cambios. process_state_name y process_event_name retornan punteros a literales staticos (no alocan)."
|
||||
---
|
||||
|
||||
# process_state_machine
|
||||
|
||||
Las apps del registry usan `process_runner` (impuro: threads, mutex, popen)
|
||||
para lanzar reindex, builds, deploys... Esta funcion extrae el contrato de
|
||||
estados a una tabla pura que se puede testear, simular y compartir con CLIs
|
||||
y bots sin depender del runner concreto.
|
||||
|
||||
## Tabla de transiciones
|
||||
|
||||
| Estado actual | Evento | Nuevo estado |
|
||||
|---------------|------------|--------------|
|
||||
| Idle | Spawned | Running |
|
||||
| Idle | Trigger | Idle (la solicitud no cambia el estado por si sola — el runner debe Spawnar) |
|
||||
| Running | Finished | Success |
|
||||
| Running | Failed | Error |
|
||||
| Running | Timeout | Error |
|
||||
| Success | Reset | Idle |
|
||||
| Error | Reset | Idle |
|
||||
| cualquier otra combinacion | | mismo estado |
|
||||
|
||||
`Trigger` se mantiene como evento explicito para que el runner pueda emitir
|
||||
"se pidio lanzar" antes de saber si el proceso arranca (util para logs y UI).
|
||||
|
||||
## API
|
||||
|
||||
```cpp
|
||||
namespace fn_ui {
|
||||
|
||||
enum class ProcessState { Idle, Running, Success, Error };
|
||||
enum class ProcessEvent { Trigger, Spawned, Finished, Failed, Timeout, Reset };
|
||||
|
||||
ProcessState process_transition(ProcessState s, ProcessEvent e);
|
||||
const char* process_state_name(ProcessState s);
|
||||
const char* process_event_name(ProcessEvent e);
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
## Uso desde process_runner
|
||||
|
||||
`process_runner` mantiene su `std::atomic<int>` con el estado, pero llama a
|
||||
`process_transition` para decidir el siguiente valor. Esto evita que la
|
||||
logica de transicion se duplique entre el thread productor y la UI consumer.
|
||||
@@ -0,0 +1,179 @@
|
||||
#include "core/sql_parse.h"
|
||||
|
||||
#include <cctype>
|
||||
#include <string>
|
||||
|
||||
namespace fn_ui {
|
||||
|
||||
namespace {
|
||||
|
||||
// Devuelve la version uppercase ASCII del primer token (delimitado por
|
||||
// whitespace) de `s`, asumiendo que `s` empieza ya en el token.
|
||||
std::string first_token_upper(const std::string& s, size_t start) {
|
||||
std::string out;
|
||||
while (start < s.size()) {
|
||||
unsigned char c = static_cast<unsigned char>(s[start]);
|
||||
if (std::isspace(c)) break;
|
||||
if (!std::isalpha(c)) break; // keywords son solo letras
|
||||
out.push_back(static_cast<char>(std::toupper(c)));
|
||||
++start;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Devuelve el indice del primer caracter "real" (no whitespace, no comentario)
|
||||
// a partir de `i`. Avanza saltando -- ... \n y /* ... */.
|
||||
size_t skip_ws_and_comments(const std::string& s, size_t i) {
|
||||
while (i < s.size()) {
|
||||
unsigned char c = static_cast<unsigned char>(s[i]);
|
||||
if (std::isspace(c)) { ++i; continue; }
|
||||
if (c == '-' && i + 1 < s.size() && s[i + 1] == '-') {
|
||||
// line comment hasta \n
|
||||
i += 2;
|
||||
while (i < s.size() && s[i] != '\n') ++i;
|
||||
continue;
|
||||
}
|
||||
if (c == '/' && i + 1 < s.size() && s[i + 1] == '*') {
|
||||
// block comment hasta */
|
||||
i += 2;
|
||||
while (i + 1 < s.size() && !(s[i] == '*' && s[i + 1] == '/')) ++i;
|
||||
if (i + 1 < s.size()) i += 2;
|
||||
else i = s.size();
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
std::string trim(const std::string& s) {
|
||||
size_t a = 0, b = s.size();
|
||||
while (a < b && std::isspace(static_cast<unsigned char>(s[a]))) ++a;
|
||||
while (b > a && std::isspace(static_cast<unsigned char>(s[b - 1]))) --b;
|
||||
return s.substr(a, b - a);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SqlStmtKind sql_classify(const std::string& stmt) {
|
||||
size_t i = skip_ws_and_comments(stmt, 0);
|
||||
if (i >= stmt.size()) return SqlStmtKind::Unknown;
|
||||
std::string head = first_token_upper(stmt, i);
|
||||
if (head == "SELECT" || head == "WITH") return SqlStmtKind::Select;
|
||||
if (head == "INSERT") return SqlStmtKind::Insert;
|
||||
if (head == "UPDATE") return SqlStmtKind::Update;
|
||||
if (head == "DELETE") return SqlStmtKind::Delete;
|
||||
if (head == "CREATE") return SqlStmtKind::Create;
|
||||
if (head == "DROP") return SqlStmtKind::Drop;
|
||||
if (head == "ALTER") return SqlStmtKind::Alter;
|
||||
if (head == "PRAGMA") return SqlStmtKind::Pragma;
|
||||
if (head == "EXPLAIN") return SqlStmtKind::Explain;
|
||||
return SqlStmtKind::Unknown;
|
||||
}
|
||||
|
||||
std::vector<SqlStatement> sql_parse(const std::string& input) {
|
||||
std::vector<SqlStatement> out;
|
||||
|
||||
enum class Mode { Normal, LineComment, BlockComment, SingleStr, DoubleStr, BackTick };
|
||||
Mode m = Mode::Normal;
|
||||
|
||||
size_t stmt_start = 0;
|
||||
int cur_line = 1;
|
||||
int stmt_line = 1;
|
||||
bool stmt_has_content = false;
|
||||
|
||||
auto flush = [&](size_t end) {
|
||||
std::string raw = input.substr(stmt_start, end - stmt_start);
|
||||
std::string t = trim(raw);
|
||||
if (!t.empty()) {
|
||||
SqlStatement s;
|
||||
s.text = t;
|
||||
s.kind = sql_classify(t);
|
||||
s.line = stmt_line;
|
||||
out.push_back(std::move(s));
|
||||
}
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < input.size(); ++i) {
|
||||
char c = input[i];
|
||||
char n = (i + 1 < input.size()) ? input[i + 1] : '\0';
|
||||
if (c == '\n') ++cur_line;
|
||||
|
||||
switch (m) {
|
||||
case Mode::Normal:
|
||||
if (c == '-' && n == '-') {
|
||||
m = Mode::LineComment;
|
||||
++i;
|
||||
} else if (c == '/' && n == '*') {
|
||||
m = Mode::BlockComment;
|
||||
++i;
|
||||
} else if (!stmt_has_content && !std::isspace(static_cast<unsigned char>(c))) {
|
||||
// marca inicio "real" de un statement (despues de skip ws/comments)
|
||||
stmt_line = cur_line;
|
||||
stmt_has_content = true;
|
||||
// procesar el caracter actual abajo (no es comentario ni ws)
|
||||
if (c == '\'') m = Mode::SingleStr;
|
||||
else if (c == '"') m = Mode::DoubleStr;
|
||||
else if (c == '`') m = Mode::BackTick;
|
||||
else if (c == ';') {
|
||||
flush(i);
|
||||
stmt_start = i + 1;
|
||||
stmt_has_content = false;
|
||||
}
|
||||
} else if (c == '\'') {
|
||||
m = Mode::SingleStr;
|
||||
} else if (c == '"') {
|
||||
m = Mode::DoubleStr;
|
||||
} else if (c == '`') {
|
||||
m = Mode::BackTick;
|
||||
} else if (c == ';') {
|
||||
flush(i);
|
||||
stmt_start = i + 1;
|
||||
stmt_has_content = false;
|
||||
}
|
||||
break;
|
||||
|
||||
case Mode::LineComment:
|
||||
if (c == '\n') m = Mode::Normal;
|
||||
break;
|
||||
|
||||
case Mode::BlockComment:
|
||||
if (c == '*' && n == '/') {
|
||||
m = Mode::Normal;
|
||||
++i;
|
||||
}
|
||||
break;
|
||||
|
||||
case Mode::SingleStr:
|
||||
if (c == '\'') {
|
||||
// SQL escapa '' como literal, sigue dentro de la cadena.
|
||||
if (n == '\'') { ++i; }
|
||||
else m = Mode::Normal;
|
||||
}
|
||||
break;
|
||||
|
||||
case Mode::DoubleStr:
|
||||
if (c == '"') {
|
||||
if (n == '"') { ++i; }
|
||||
else m = Mode::Normal;
|
||||
}
|
||||
break;
|
||||
|
||||
case Mode::BackTick:
|
||||
if (c == '`') {
|
||||
if (n == '`') { ++i; }
|
||||
else m = Mode::Normal;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ultimo statement sin ';' final
|
||||
if (stmt_start < input.size()) {
|
||||
flush(input.size());
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace fn_ui
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
// sql_parse — tokenizer y clasificador de statements SQL (logica pura).
|
||||
//
|
||||
// Separa una cadena multi-statement por ';' (fuera de strings y comentarios)
|
||||
// y clasifica cada statement por su keyword inicial. No ejecuta nada — esta
|
||||
// funcion es 100% pura: misma entrada, misma salida.
|
||||
//
|
||||
// Uso tipico:
|
||||
//
|
||||
// auto stmts = fn_ui::sql_parse("SELECT 1; INSERT INTO t VALUES (2);");
|
||||
// for (auto& s : stmts) {
|
||||
// switch (s.kind) { ... }
|
||||
// }
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace fn_ui {
|
||||
|
||||
enum class SqlStmtKind {
|
||||
Unknown,
|
||||
Select,
|
||||
Insert,
|
||||
Update,
|
||||
Delete,
|
||||
Create,
|
||||
Drop,
|
||||
Alter,
|
||||
Pragma,
|
||||
Explain,
|
||||
};
|
||||
|
||||
struct SqlStatement {
|
||||
SqlStmtKind kind = SqlStmtKind::Unknown;
|
||||
std::string text; // texto trimeado (sin ';' final)
|
||||
int line = 1; // linea de inicio en el input (1-based)
|
||||
};
|
||||
|
||||
// Tokeniza SQL multi-statement. Salta cadenas '...' "..." `...` y comentarios
|
||||
// -- linea y /* bloque */. Devuelve los statements no vacios.
|
||||
std::vector<SqlStatement> sql_parse(const std::string& input);
|
||||
|
||||
// Clasifica un statement individual por su keyword inicial (case-insensitive,
|
||||
// despues de saltar whitespace y comentarios iniciales).
|
||||
SqlStmtKind sql_classify(const std::string& stmt);
|
||||
|
||||
} // namespace fn_ui
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
name: sql_parse
|
||||
kind: function
|
||||
lang: cpp
|
||||
domain: core
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "std::vector<fn_ui::SqlStatement> fn_ui::sql_parse(const std::string& input); fn_ui::SqlStmtKind fn_ui::sql_classify(const std::string& stmt)"
|
||||
description: "Tokenizer y clasificador puro de SQL multi-statement. Separa por ';' fuera de strings ('...', \"...\", `...`) y comentarios (-- linea, /* bloque */), trimea, y clasifica cada statement por su keyword inicial (SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, PRAGMA, EXPLAIN, WITH→Select)."
|
||||
tags: [sql, parser, tokenizer, pure, sqlite]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: []
|
||||
tested: true
|
||||
tests: ["sql_parse classifies common statements", "sql_parse handles strings and comments", "sql_parse trims and ignores empty"]
|
||||
test_file_path: "cpp/tests/test_sql_parse.cpp"
|
||||
file_path: "cpp/functions/core/sql_parse.cpp"
|
||||
params:
|
||||
- name: input
|
||||
desc: "Texto SQL completo, posiblemente multi-statement, con ';' opcional al final"
|
||||
- name: stmt
|
||||
desc: "(sql_classify) Un solo statement ya separado, sin ';' final"
|
||||
output: "sql_parse: vector con un SqlStatement por statement no vacio (kind, text trimeado, line 1-based de inicio en el input). sql_classify: SqlStmtKind segun la primera keyword del statement."
|
||||
---
|
||||
|
||||
# sql_parse
|
||||
|
||||
Logica pura para entender un script SQL antes de pasarlo al motor: separar
|
||||
statements y clasificarlos. Sin estado, sin I/O, sin SQLite. Reutilizable
|
||||
desde `sql_workbench` y desde cualquier app/CLI que necesite distinguir
|
||||
SELECT de DDL para mostrar info al usuario o decidir como ejecutar.
|
||||
|
||||
## API
|
||||
|
||||
```cpp
|
||||
namespace fn_ui {
|
||||
|
||||
enum class SqlStmtKind {
|
||||
Unknown, Select, Insert, Update, Delete, Create, Drop, Alter, Pragma, Explain
|
||||
};
|
||||
|
||||
struct SqlStatement {
|
||||
SqlStmtKind kind;
|
||||
std::string text; // texto trimeado
|
||||
int line; // linea de inicio en el input (1-based)
|
||||
};
|
||||
|
||||
std::vector<SqlStatement> sql_parse(const std::string& input);
|
||||
SqlStmtKind sql_classify(const std::string& stmt);
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
## Reglas del tokenizer
|
||||
|
||||
- Strings: `'...'`, `"..."` y `` `...` `` se saltan enteras. Soporta el escape
|
||||
SQL estandar de doblar la quote (`'don''t'`, `"a""b"`).
|
||||
- Comentarios: `-- linea` hasta `\n` y `/* bloque */`.
|
||||
- El separador `;` solo divide cuando aparece en modo Normal (fuera de
|
||||
strings/comments).
|
||||
- Statements vacios (`;;`, `; ;`) se descartan tras trimear.
|
||||
- Si el ultimo statement no termina en `;`, se incluye igualmente.
|
||||
|
||||
## Clasificacion
|
||||
|
||||
`sql_classify` mira la primera palabra alfabetica despues de saltar whitespace
|
||||
y comentarios. `WITH` se clasifica como `Select` porque es la forma comun de
|
||||
iniciar CTEs que devuelven filas.
|
||||
|
||||
## Por que pura
|
||||
|
||||
No abre conexiones, no toca SQLite, no consulta el reloj. Misma entrada → misma
|
||||
salida. Esto permite testearla sin depender de un fixture de DB.
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "core/sql_workbench.h"
|
||||
|
||||
#include "core/sql_parse.h"
|
||||
#include "core/text_editor.h"
|
||||
#include "core/tokens.h"
|
||||
#include "core/button.h"
|
||||
@@ -25,24 +26,19 @@ namespace fn {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string ltrim(const std::string& s) {
|
||||
size_t i = 0;
|
||||
while (i < s.size() && std::isspace(static_cast<unsigned char>(s[i]))) ++i;
|
||||
return s.substr(i);
|
||||
}
|
||||
|
||||
std::string upper_first_token(const std::string& sql) {
|
||||
std::string t = ltrim(sql);
|
||||
size_t end = 0;
|
||||
while (end < t.size() && !std::isspace(static_cast<unsigned char>(t[end]))) ++end;
|
||||
std::string tok = t.substr(0, end);
|
||||
for (auto& c : tok) c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
|
||||
return tok;
|
||||
// Clasifica el primer statement con sql_parse pure. Si el input no contiene
|
||||
// statements (todo whitespace/comentarios), devuelve Unknown.
|
||||
fn_ui::SqlStmtKind first_stmt_kind(const std::string& sql) {
|
||||
auto stmts = fn_ui::sql_parse(sql);
|
||||
if (stmts.empty()) return fn_ui::SqlStmtKind::Unknown;
|
||||
return stmts.front().kind;
|
||||
}
|
||||
|
||||
bool is_readonly_stmt(const std::string& sql) {
|
||||
const std::string head = upper_first_token(sql);
|
||||
return head == "SELECT" || head == "PRAGMA" || head == "EXPLAIN" || head == "WITH";
|
||||
using K = fn_ui::SqlStmtKind;
|
||||
K k = first_stmt_kind(sql);
|
||||
// sql_parse clasifica WITH como Select; Pragma y Explain son readonly.
|
||||
return k == K::Select || k == K::Pragma || k == K::Explain;
|
||||
}
|
||||
|
||||
TextEditorState* editor_of(SqlWorkbenchState& state) {
|
||||
|
||||
@@ -8,7 +8,7 @@ purity: impure
|
||||
signature: "void fn::sql_workbench(const char* id, sqlite3* db, fn::SqlWorkbenchState& state, ImVec2 size); bool fn::sql_workbench_run_query(sqlite3*, const char*, fn::SqlWorkbenchState&); void fn::sql_workbench_load_schema(sqlite3*, fn::SqlWorkbenchState&); void fn::sql_workbench_destroy(fn::SqlWorkbenchState&)"
|
||||
description: "Workbench SQL embebido en ImGui: editor con highlighting (text_editor + CodeLang::SQL), tabla de resultados (table_view), sidebar de schema (sqlite_master) e historial. Ejecuta queries contra una sqlite3* del caller (no abre/cierra la DB)."
|
||||
tags: [imgui, sql, sqlite, editor, table, dashboard, registry, debug]
|
||||
uses_functions: ["button_cpp_core", "table_view_cpp_viz", "text_editor_cpp_core", "tokens_cpp_core"]
|
||||
uses_functions: ["button_cpp_core", "sql_parse_cpp_core", "table_view_cpp_viz", "text_editor_cpp_core", "tokens_cpp_core"]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
|
||||
@@ -31,6 +31,14 @@ add_fn_test(test_pie_chart_math test_pie_chart_math.cpp)
|
||||
add_fn_test(test_kpi_card_math test_kpi_card_math.cpp)
|
||||
add_fn_test(test_bar_chart_math test_bar_chart_math.cpp)
|
||||
|
||||
# Issue 0045 — tests de la logica pura extraida.
|
||||
add_fn_test(test_sql_parse test_sql_parse.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../functions/core/sql_parse.cpp)
|
||||
add_fn_test(test_process_state_machine test_process_state_machine.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../functions/core/process_state_machine.cpp)
|
||||
add_fn_test(test_file_poll_diff test_file_poll_diff.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../functions/core/file_poll_diff.cpp)
|
||||
|
||||
# --- Placeholders para primitivos UI (logica visual cubierta en 0048) ------
|
||||
add_fn_test(test_tokens test_tokens.cpp)
|
||||
add_fn_test(test_button test_button.cpp)
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
// Tests for fn_ui::file_poll_diff (cpp/functions/core/file_poll_diff).
|
||||
// Pura: compara dos snapshots de FS y devuelve added/modified/removed.
|
||||
|
||||
#define CATCH_CONFIG_MAIN
|
||||
#include "catch_amalgamated.hpp"
|
||||
|
||||
#include "core/file_poll_diff.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
using fn_ui::file_poll_diff;
|
||||
using fn_ui::FileEntry;
|
||||
|
||||
namespace {
|
||||
// Helper: ordena los vectores para comparaciones estables (el orden interno
|
||||
// depende del orden de iteracion de unordered_map, no es estable).
|
||||
void sort_diff(fn_ui::FileDiff& d) {
|
||||
std::sort(d.added.begin(), d.added.end());
|
||||
std::sort(d.modified.begin(), d.modified.end());
|
||||
std::sort(d.removed.begin(), d.removed.end());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("file_poll_diff detects added/modified/removed") {
|
||||
std::vector<FileEntry> before = {{"a", 10, 1}, {"b", 20, 2}, {"c", 30, 3}};
|
||||
std::vector<FileEntry> after = {{"a", 10, 1}, {"b", 25, 2}, {"d", 40, 4}};
|
||||
auto d = file_poll_diff(before, after);
|
||||
sort_diff(d);
|
||||
REQUIRE(d.added == std::vector<std::string>{"d"});
|
||||
REQUIRE(d.modified == std::vector<std::string>{"b"});
|
||||
REQUIRE(d.removed == std::vector<std::string>{"c"});
|
||||
}
|
||||
|
||||
TEST_CASE("file_poll_diff: empty inputs") {
|
||||
auto d = file_poll_diff({}, {});
|
||||
REQUIRE(d.added.empty());
|
||||
REQUIRE(d.modified.empty());
|
||||
REQUIRE(d.removed.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("file_poll_diff: identical snapshots") {
|
||||
std::vector<FileEntry> snap = {{"a", 1, 100}, {"b", 2, 200}};
|
||||
auto d = file_poll_diff(snap, snap);
|
||||
REQUIRE(d.added.empty());
|
||||
REQUIRE(d.modified.empty());
|
||||
REQUIRE(d.removed.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("file_poll_diff: all added (before vacio)") {
|
||||
std::vector<FileEntry> after = {{"a", 1, 1}, {"b", 2, 2}};
|
||||
auto d = file_poll_diff({}, after);
|
||||
sort_diff(d);
|
||||
REQUIRE(d.added == std::vector<std::string>{"a", "b"});
|
||||
REQUIRE(d.modified.empty());
|
||||
REQUIRE(d.removed.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("file_poll_diff: all removed (after vacio)") {
|
||||
std::vector<FileEntry> before = {{"a", 1, 1}, {"b", 2, 2}};
|
||||
auto d = file_poll_diff(before, {});
|
||||
sort_diff(d);
|
||||
REQUIRE(d.removed == std::vector<std::string>{"a", "b"});
|
||||
REQUIRE(d.modified.empty());
|
||||
REQUIRE(d.added.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("file_poll_diff: solo size cambia -> modified") {
|
||||
std::vector<FileEntry> before = {{"a", 100, 5}};
|
||||
std::vector<FileEntry> after = {{"a", 200, 5}};
|
||||
auto d = file_poll_diff(before, after);
|
||||
REQUIRE(d.modified == std::vector<std::string>{"a"});
|
||||
REQUIRE(d.added.empty());
|
||||
REQUIRE(d.removed.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("file_poll_diff: solo mtime cambia -> modified") {
|
||||
std::vector<FileEntry> before = {{"a", 100, 5}};
|
||||
std::vector<FileEntry> after = {{"a", 100, 9}};
|
||||
auto d = file_poll_diff(before, after);
|
||||
REQUIRE(d.modified == std::vector<std::string>{"a"});
|
||||
REQUIRE(d.added.empty());
|
||||
REQUIRE(d.removed.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("file_poll_diff: combinacion compleja") {
|
||||
std::vector<FileEntry> before = {
|
||||
{"keep", 10, 1},
|
||||
{"mod_size", 20, 2},
|
||||
{"mod_mtime", 30, 3},
|
||||
{"removed", 40, 4},
|
||||
};
|
||||
std::vector<FileEntry> after = {
|
||||
{"keep", 10, 1},
|
||||
{"mod_size", 21, 2},
|
||||
{"mod_mtime", 30, 9},
|
||||
{"new", 50, 5},
|
||||
};
|
||||
auto d = file_poll_diff(before, after);
|
||||
sort_diff(d);
|
||||
REQUIRE(d.added == std::vector<std::string>{"new"});
|
||||
REQUIRE(d.modified == std::vector<std::string>{"mod_mtime", "mod_size"});
|
||||
REQUIRE(d.removed == std::vector<std::string>{"removed"});
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// Tests for fn_ui::process_transition (cpp/functions/core/process_state_machine).
|
||||
// Tabla de transiciones definida en .md.
|
||||
|
||||
#define CATCH_CONFIG_MAIN
|
||||
#include "catch_amalgamated.hpp"
|
||||
|
||||
#include "core/process_state_machine.h"
|
||||
#include <cstring>
|
||||
|
||||
using fn_ui::process_transition;
|
||||
using fn_ui::process_state_name;
|
||||
using fn_ui::process_event_name;
|
||||
using fn_ui::ProcessState;
|
||||
using fn_ui::ProcessEvent;
|
||||
|
||||
TEST_CASE("process_transition: idle -> running on Spawned") {
|
||||
REQUIRE(process_transition(ProcessState::Idle, ProcessEvent::Spawned) == ProcessState::Running);
|
||||
}
|
||||
|
||||
TEST_CASE("process_transition: idle + Trigger sigue Idle (la solicitud no muta el estado)") {
|
||||
REQUIRE(process_transition(ProcessState::Idle, ProcessEvent::Trigger) == ProcessState::Idle);
|
||||
}
|
||||
|
||||
TEST_CASE("process_transition: running -> success on Finished") {
|
||||
REQUIRE(process_transition(ProcessState::Running, ProcessEvent::Finished) == ProcessState::Success);
|
||||
}
|
||||
|
||||
TEST_CASE("process_transition: running -> error on Failed/Timeout") {
|
||||
REQUIRE(process_transition(ProcessState::Running, ProcessEvent::Failed) == ProcessState::Error);
|
||||
REQUIRE(process_transition(ProcessState::Running, ProcessEvent::Timeout) == ProcessState::Error);
|
||||
}
|
||||
|
||||
TEST_CASE("process_transition: success/error -> idle on Reset") {
|
||||
REQUIRE(process_transition(ProcessState::Success, ProcessEvent::Reset) == ProcessState::Idle);
|
||||
REQUIRE(process_transition(ProcessState::Error, ProcessEvent::Reset) == ProcessState::Idle);
|
||||
}
|
||||
|
||||
TEST_CASE("process_transition: invalid events keep state") {
|
||||
// Idle no acepta Finished/Failed/Timeout.
|
||||
REQUIRE(process_transition(ProcessState::Idle, ProcessEvent::Finished) == ProcessState::Idle);
|
||||
REQUIRE(process_transition(ProcessState::Idle, ProcessEvent::Failed) == ProcessState::Idle);
|
||||
REQUIRE(process_transition(ProcessState::Idle, ProcessEvent::Timeout) == ProcessState::Idle);
|
||||
REQUIRE(process_transition(ProcessState::Idle, ProcessEvent::Reset) == ProcessState::Idle);
|
||||
|
||||
// Running ignora Trigger/Spawned/Reset.
|
||||
REQUIRE(process_transition(ProcessState::Running, ProcessEvent::Trigger) == ProcessState::Running);
|
||||
REQUIRE(process_transition(ProcessState::Running, ProcessEvent::Spawned) == ProcessState::Running);
|
||||
REQUIRE(process_transition(ProcessState::Running, ProcessEvent::Reset) == ProcessState::Running);
|
||||
|
||||
// Success ignora todo menos Reset.
|
||||
REQUIRE(process_transition(ProcessState::Success, ProcessEvent::Trigger) == ProcessState::Success);
|
||||
REQUIRE(process_transition(ProcessState::Success, ProcessEvent::Spawned) == ProcessState::Success);
|
||||
REQUIRE(process_transition(ProcessState::Success, ProcessEvent::Finished) == ProcessState::Success);
|
||||
REQUIRE(process_transition(ProcessState::Success, ProcessEvent::Failed) == ProcessState::Success);
|
||||
|
||||
// Error idem.
|
||||
REQUIRE(process_transition(ProcessState::Error, ProcessEvent::Finished) == ProcessState::Error);
|
||||
REQUIRE(process_transition(ProcessState::Error, ProcessEvent::Spawned) == ProcessState::Error);
|
||||
}
|
||||
|
||||
TEST_CASE("process_state_name and process_event_name") {
|
||||
REQUIRE(std::strcmp(process_state_name(ProcessState::Idle), "Idle") == 0);
|
||||
REQUIRE(std::strcmp(process_state_name(ProcessState::Running), "Running") == 0);
|
||||
REQUIRE(std::strcmp(process_state_name(ProcessState::Success), "Success") == 0);
|
||||
REQUIRE(std::strcmp(process_state_name(ProcessState::Error), "Error") == 0);
|
||||
|
||||
REQUIRE(std::strcmp(process_event_name(ProcessEvent::Trigger), "Trigger") == 0);
|
||||
REQUIRE(std::strcmp(process_event_name(ProcessEvent::Spawned), "Spawned") == 0);
|
||||
REQUIRE(std::strcmp(process_event_name(ProcessEvent::Finished), "Finished") == 0);
|
||||
REQUIRE(std::strcmp(process_event_name(ProcessEvent::Failed), "Failed") == 0);
|
||||
REQUIRE(std::strcmp(process_event_name(ProcessEvent::Timeout), "Timeout") == 0);
|
||||
REQUIRE(std::strcmp(process_event_name(ProcessEvent::Reset), "Reset") == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("process_transition: ciclo completo idle -> running -> success -> idle") {
|
||||
ProcessState s = ProcessState::Idle;
|
||||
s = process_transition(s, ProcessEvent::Spawned);
|
||||
REQUIRE(s == ProcessState::Running);
|
||||
s = process_transition(s, ProcessEvent::Finished);
|
||||
REQUIRE(s == ProcessState::Success);
|
||||
s = process_transition(s, ProcessEvent::Reset);
|
||||
REQUIRE(s == ProcessState::Idle);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
// Tests for fn_ui::sql_parse / sql_classify (cpp/functions/core/sql_parse).
|
||||
// Pura: tokeniza SQL multi-statement saltando strings y comentarios, y
|
||||
// clasifica por keyword inicial.
|
||||
|
||||
#define CATCH_CONFIG_MAIN
|
||||
#include "catch_amalgamated.hpp"
|
||||
|
||||
#include "core/sql_parse.h"
|
||||
|
||||
using fn_ui::sql_parse;
|
||||
using fn_ui::sql_classify;
|
||||
using fn_ui::SqlStmtKind;
|
||||
|
||||
TEST_CASE("sql_parse classifies common statements") {
|
||||
auto stmts = sql_parse("SELECT * FROM t; INSERT INTO t VALUES (1);");
|
||||
REQUIRE(stmts.size() == 2);
|
||||
REQUIRE(stmts[0].kind == SqlStmtKind::Select);
|
||||
REQUIRE(stmts[1].kind == SqlStmtKind::Insert);
|
||||
}
|
||||
|
||||
TEST_CASE("sql_parse handles strings and comments") {
|
||||
auto stmts = sql_parse("-- comment\nSELECT 'a;b' FROM t; /* x */ DELETE FROM t;");
|
||||
REQUIRE(stmts.size() == 2);
|
||||
REQUIRE(stmts[0].kind == SqlStmtKind::Select);
|
||||
REQUIRE(stmts[1].kind == SqlStmtKind::Delete);
|
||||
}
|
||||
|
||||
TEST_CASE("sql_parse trims and ignores empty") {
|
||||
auto stmts = sql_parse("; ; SELECT 1;;");
|
||||
REQUIRE(stmts.size() == 1);
|
||||
REQUIRE(stmts[0].kind == SqlStmtKind::Select);
|
||||
}
|
||||
|
||||
TEST_CASE("sql_parse: ddl and dcl keywords") {
|
||||
auto stmts = sql_parse("CREATE TABLE t(a); DROP TABLE t; ALTER TABLE t ADD b; UPDATE t SET a=1;");
|
||||
REQUIRE(stmts.size() == 4);
|
||||
REQUIRE(stmts[0].kind == SqlStmtKind::Create);
|
||||
REQUIRE(stmts[1].kind == SqlStmtKind::Drop);
|
||||
REQUIRE(stmts[2].kind == SqlStmtKind::Alter);
|
||||
REQUIRE(stmts[3].kind == SqlStmtKind::Update);
|
||||
}
|
||||
|
||||
TEST_CASE("sql_parse: pragma and explain") {
|
||||
auto stmts = sql_parse("PRAGMA foreign_keys = ON; EXPLAIN SELECT 1;");
|
||||
REQUIRE(stmts.size() == 2);
|
||||
REQUIRE(stmts[0].kind == SqlStmtKind::Pragma);
|
||||
REQUIRE(stmts[1].kind == SqlStmtKind::Explain);
|
||||
}
|
||||
|
||||
TEST_CASE("sql_parse: WITH clasifica como Select") {
|
||||
auto stmts = sql_parse("WITH x AS (SELECT 1) SELECT * FROM x;");
|
||||
REQUIRE(stmts.size() == 1);
|
||||
REQUIRE(stmts[0].kind == SqlStmtKind::Select);
|
||||
}
|
||||
|
||||
TEST_CASE("sql_parse: case-insensitive y trim") {
|
||||
auto stmts = sql_parse(" select 1;\n INSERT INTO t VALUES (2);");
|
||||
REQUIRE(stmts.size() == 2);
|
||||
REQUIRE(stmts[0].kind == SqlStmtKind::Select);
|
||||
REQUIRE(stmts[1].kind == SqlStmtKind::Insert);
|
||||
}
|
||||
|
||||
TEST_CASE("sql_parse: line tracking") {
|
||||
auto stmts = sql_parse("\n\nSELECT 1;\n-- skip\nDELETE FROM t;\n");
|
||||
REQUIRE(stmts.size() == 2);
|
||||
REQUIRE(stmts[0].line == 3);
|
||||
REQUIRE(stmts[1].line == 5);
|
||||
}
|
||||
|
||||
TEST_CASE("sql_parse: ultimo statement sin ;") {
|
||||
auto stmts = sql_parse("SELECT 1");
|
||||
REQUIRE(stmts.size() == 1);
|
||||
REQUIRE(stmts[0].kind == SqlStmtKind::Select);
|
||||
}
|
||||
|
||||
TEST_CASE("sql_classify: standalone") {
|
||||
REQUIRE(sql_classify("SELECT 1") == SqlStmtKind::Select);
|
||||
REQUIRE(sql_classify("garbage") == SqlStmtKind::Unknown);
|
||||
REQUIRE(sql_classify("") == SqlStmtKind::Unknown);
|
||||
REQUIRE(sql_classify("/* c */ DELETE FROM t") == SqlStmtKind::Delete);
|
||||
}
|
||||
|
||||
TEST_CASE("sql_parse: ; dentro de string no separa") {
|
||||
auto stmts = sql_parse("SELECT 'a;b;c' FROM t;");
|
||||
REQUIRE(stmts.size() == 1);
|
||||
REQUIRE(stmts[0].kind == SqlStmtKind::Select);
|
||||
}
|
||||
|
||||
TEST_CASE("sql_parse: backtick identifier") {
|
||||
auto stmts = sql_parse("SELECT * FROM `weird;name`;");
|
||||
REQUIRE(stmts.size() == 1);
|
||||
REQUIRE(stmts[0].kind == SqlStmtKind::Select);
|
||||
}
|
||||
Reference in New Issue
Block a user