tui: fix nested source abandoning the calling script
Tui::Source kept the script position in single member fields (loading_lines/idx/...), so a sourced line that was itself `source inner` overwrote them: when the inner file finished, the outer script's remaining lines were silently dropped. The state is now a stack of SourceFrames — the stack is the call chain. A nested source just pushes a frame (the running driver, ticker thread or headless drain, picks it up next line) and the caller's frame resumes when it pops. Each frame still prints its own "source: <file> (N line(s))" summary; an interactive-line abort clears the whole chain; depth capped at 32 like the core script engine. The Computing modal shows the top frame. Regression-tested headless via BootDispatch (tests/tui/test_source.cpp): nested-then-continue, and self-recursion hitting the depth guard. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -281,20 +281,29 @@ void Tui::Source(const std::string &filename) {
|
|||||||
if (const char *home = std::getenv("HOME"))
|
if (const char *home = std::getenv("HOME"))
|
||||||
expanded = std::string(home) + expanded.substr(1);
|
expanded = std::string(home) + expanded.substr(1);
|
||||||
}
|
}
|
||||||
|
if (source_stack.size() >= 32) { // same depth guard as the core engine
|
||||||
|
Print("source: nesting too deep, skipping " + filename);
|
||||||
|
return;
|
||||||
|
}
|
||||||
std::ifstream f(expanded);
|
std::ifstream f(expanded);
|
||||||
if (!f) { Print("source failed: cannot open " + filename); return; }
|
if (!f) { Print("source failed: cannot open " + filename); return; }
|
||||||
|
|
||||||
// Slurp the whole file so we can drive line-by-line processing from the
|
// Slurp the whole file so we can drive line-by-line processing from the
|
||||||
// event loop (one line per posted task). This lets the screen redraw
|
// event loop (one line per posted task). This lets the screen redraw
|
||||||
// between lines and surface the "Computing…" modal.
|
// between lines and surface the "Computing…" modal.
|
||||||
loading_lines.clear();
|
SourceFrame fr;
|
||||||
|
fr.filename = filename;
|
||||||
std::string line;
|
std::string line;
|
||||||
while (std::getline(f, line)) loading_lines.push_back(line);
|
while (std::getline(f, line)) fr.lines.push_back(std::move(line));
|
||||||
|
|
||||||
|
// Nested source (a sourced line is itself `source …`): just stack the
|
||||||
|
// frame — the driver already running (ticker thread or headless drain)
|
||||||
|
// picks it up on the next ProcessNextSourceLine, and the caller's frame
|
||||||
|
// resumes when it finishes.
|
||||||
|
bool nested = !source_stack.empty();
|
||||||
|
source_stack.push_back(std::move(fr));
|
||||||
|
if (nested) return;
|
||||||
|
|
||||||
loading_filename = filename;
|
|
||||||
loading_idx = 0;
|
|
||||||
loading_executed = 0;
|
|
||||||
loading_lineno = 0;
|
|
||||||
loading_prev_in_source = in_source;
|
loading_prev_in_source = in_source;
|
||||||
source_origin_screen = screen_idx; // a sourced line that leaves this screen aborts
|
source_origin_screen = screen_idx; // a sourced line that leaves this screen aborts
|
||||||
in_source = true;
|
in_source = true;
|
||||||
@@ -332,9 +341,18 @@ void Tui::Source(const std::string &filename) {
|
|||||||
|
|
||||||
void Tui::ProcessNextSourceLine() {
|
void Tui::ProcessNextSourceLine() {
|
||||||
if (!loading.load()) return;
|
if (!loading.load()) return;
|
||||||
while (loading_idx < loading_lines.size()) {
|
while (!source_stack.empty()) {
|
||||||
const std::string &raw = loading_lines[loading_idx++];
|
if (source_stack.back().idx >= source_stack.back().lines.size()) {
|
||||||
++loading_lineno;
|
// Frame done: summarise it and resume the caller's frame.
|
||||||
|
const SourceFrame &done = source_stack.back();
|
||||||
|
Print("source: " + done.filename
|
||||||
|
+ " (" + std::to_string(done.executed) + " line(s))");
|
||||||
|
source_stack.pop_back();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
SourceFrame &fr = source_stack.back();
|
||||||
|
const std::string raw = fr.lines[fr.idx++];
|
||||||
|
++fr.lineno;
|
||||||
size_t start = raw.find_first_not_of(" \t");
|
size_t start = raw.find_first_not_of(" \t");
|
||||||
if (start == std::string::npos) continue;
|
if (start == std::string::npos) continue;
|
||||||
if (raw[start] == '#') continue;
|
if (raw[start] == '#') continue;
|
||||||
@@ -343,28 +361,26 @@ void Tui::ProcessNextSourceLine() {
|
|||||||
trimmed.pop_back();
|
trimmed.pop_back();
|
||||||
if (trimmed.empty()) continue;
|
if (trimmed.empty()) continue;
|
||||||
|
|
||||||
|
++fr.executed;
|
||||||
|
int lineno = fr.lineno; // copies: Submit can push a nested frame,
|
||||||
|
// which may reallocate and invalidate `fr`.
|
||||||
input = trimmed;
|
input = trimmed;
|
||||||
cursor_pos = (int)input.size();
|
cursor_pos = (int)input.size();
|
||||||
Submit();
|
Submit();
|
||||||
++loading_executed;
|
|
||||||
|
|
||||||
if (screen_idx != source_origin_screen) {
|
if (screen_idx != source_origin_screen) {
|
||||||
Print("source: line " + std::to_string(loading_lineno)
|
Print("source: line " + std::to_string(lineno)
|
||||||
+ " is interactive (would open a screen) — aborting.");
|
+ " is interactive (would open a screen) — aborting.");
|
||||||
screen_idx = source_origin_screen;
|
screen_idx = source_origin_screen;
|
||||||
loading.store(false);
|
source_stack.clear(); // an abort cancels the whole chain
|
||||||
computing_open = false;
|
break;
|
||||||
tick_in_flight.store(false);
|
|
||||||
in_source = loading_prev_in_source;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
// One effective line per tick — ack so the ticker can pace the next.
|
// One effective line per tick — ack so the ticker can pace the next.
|
||||||
tick_in_flight.store(false);
|
tick_in_flight.store(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Print("source: " + loading_filename
|
// Stack drained (or aborted): close up.
|
||||||
+ " (" + std::to_string(loading_executed) + " line(s))");
|
|
||||||
loading.store(false);
|
loading.store(false);
|
||||||
computing_open = false;
|
computing_open = false;
|
||||||
tick_in_flight.store(false);
|
tick_in_flight.store(false);
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ using namespace ftxui;
|
|||||||
Tui::Tui()
|
Tui::Tui()
|
||||||
: cursor_pos(0), history_idx(-1), scroll_offset(0), quit(false), in_source(false),
|
: cursor_pos(0), history_idx(-1), scroll_offset(0), quit(false), in_source(false),
|
||||||
loading(false), tick_in_flight(false),
|
loading(false), tick_in_flight(false),
|
||||||
loading_idx(0), loading_executed(0), loading_lineno(0),
|
|
||||||
loading_prev_in_source(false), screen_ptr(nullptr),
|
loading_prev_in_source(false), screen_ptr(nullptr),
|
||||||
screen_idx(4), // boot to the dashboard; console (screen 0) is now a sub-screen
|
screen_idx(4), // boot to the dashboard; console (screen 0) is now a sub-screen
|
||||||
connect_m1_idx(0), connect_m2_idx(0),
|
connect_m1_idx(0), connect_m2_idx(0),
|
||||||
@@ -64,12 +63,17 @@ void Tui::Run() {
|
|||||||
// script is opened from the dashboard. The Renderer re-reads the live
|
// script is opened from the dashboard. The Renderer re-reads the live
|
||||||
// progress every frame.
|
// progress every frame.
|
||||||
auto computing_modal = Renderer([this] {
|
auto computing_modal = Renderer([this] {
|
||||||
std::string progress = std::to_string(loading_executed) + " / "
|
std::string fname, progress;
|
||||||
+ std::to_string((int)loading_lines.size()) + " lines";
|
if (!source_stack.empty()) { // top frame = the file currently running
|
||||||
|
const SourceFrame &fr = source_stack.back();
|
||||||
|
fname = fr.filename;
|
||||||
|
progress = std::to_string(fr.executed) + " / "
|
||||||
|
+ std::to_string((int)fr.lines.size()) + " lines";
|
||||||
|
}
|
||||||
return vbox({
|
return vbox({
|
||||||
text(" Computing… ") | bold | center,
|
text(" Computing… ") | bold | center,
|
||||||
separator(),
|
separator(),
|
||||||
text(loading_filename) | center,
|
text(fname) | center,
|
||||||
text(progress) | center,
|
text(progress) | center,
|
||||||
}) | borderDouble | size(WIDTH, GREATER_THAN, 40);
|
}) | borderDouble | size(WIDTH, GREATER_THAN, 40);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -99,11 +99,16 @@ class Tui : public Frontend {
|
|||||||
// ---- Source-file loading (event-driven, one line per tick) ----
|
// ---- Source-file loading (event-driven, one line per tick) ----
|
||||||
std::atomic<bool> loading; ///< true while a script is being processed; read by tick thread.
|
std::atomic<bool> loading; ///< true while a script is being processed; read by tick thread.
|
||||||
std::atomic<bool> tick_in_flight; ///< main thread acks each tick by clearing this; ticker waits.
|
std::atomic<bool> tick_in_flight; ///< main thread acks each tick by clearing this; ticker waits.
|
||||||
std::string loading_filename;
|
// One script being processed. Nested `source` pushes a frame so the
|
||||||
std::vector<std::string> loading_lines;
|
// caller resumes where it left off — the stack IS the call chain.
|
||||||
size_t loading_idx;
|
struct SourceFrame {
|
||||||
int loading_executed;
|
std::string filename;
|
||||||
int loading_lineno;
|
std::vector<std::string> lines;
|
||||||
|
size_t idx = 0; ///< next line to process
|
||||||
|
int executed = 0; ///< effective (non-blank, non-comment) lines run
|
||||||
|
int lineno = 0; ///< current 1-based line, for messages
|
||||||
|
};
|
||||||
|
std::vector<SourceFrame> source_stack;
|
||||||
bool loading_prev_in_source;
|
bool loading_prev_in_source;
|
||||||
int source_origin_screen = 0; ///< screen a `source` started from; a sourced line that navigates away (opens an interactive screen) aborts it.
|
int source_origin_screen = 0; ///< screen a `source` started from; a sourced line that navigates away (opens an interactive screen) aborts it.
|
||||||
bool computing_open = false; ///< drives the global "Computing…" progress modal while a script loads.
|
bool computing_open = false; ///< drives the global "Computing…" progress modal while a script loads.
|
||||||
|
|||||||
71
tests/tui/test_source.cpp
Normal file
71
tests/tui/test_source.cpp
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
#include <doctest/doctest.h>
|
||||||
|
|
||||||
|
#include "frontends/tui/tui.hpp"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// Tui::Source nesting — regression for the bug where a nested `source`
|
||||||
|
// overwrote the single loading state, so the CALLING script's remaining
|
||||||
|
// lines never ran. Headless path (no screen): BootDispatch drains
|
||||||
|
// synchronously, exactly like `essim --batch --source`.
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::string run_boot(const std::string &cmd) {
|
||||||
|
Tui t;
|
||||||
|
t.BootDispatch(cmd);
|
||||||
|
std::ostringstream oss;
|
||||||
|
t.DumpOutput(oss);
|
||||||
|
return oss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("source: lines after a nested source still run") {
|
||||||
|
const char *inner = "test_src_inner.essim";
|
||||||
|
const char *outer = "test_src_outer.essim";
|
||||||
|
{
|
||||||
|
std::ofstream f(inner);
|
||||||
|
f << "new\n";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
std::ofstream f(outer);
|
||||||
|
f << "source " << inner << "\n"
|
||||||
|
"verify\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string out = run_boot(std::string("source ") + outer);
|
||||||
|
|
||||||
|
// The inner script ran and was summarised…
|
||||||
|
CHECK(out.find("system created.") != std::string::npos);
|
||||||
|
CHECK(out.find(std::string("source: ") + inner) != std::string::npos);
|
||||||
|
// …and the OUTER script kept going after it: verify executed…
|
||||||
|
CHECK(out.find("verify: 0 local mismatch(es)") != std::string::npos);
|
||||||
|
// …and the outer summary counts its 2 effective lines.
|
||||||
|
CHECK(out.find(std::string("source: ") + outer + " (2 line(s))")
|
||||||
|
!= std::string::npos);
|
||||||
|
|
||||||
|
std::remove(inner);
|
||||||
|
std::remove(outer);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("source: self-recursion stops at the depth guard") {
|
||||||
|
const char *loop = "test_src_loop.essim";
|
||||||
|
{
|
||||||
|
std::ofstream f(loop);
|
||||||
|
f << "source " << loop << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string out = run_boot(std::string("source ") + loop);
|
||||||
|
|
||||||
|
CHECK(out.find("source: nesting too deep, skipping")
|
||||||
|
!= std::string::npos);
|
||||||
|
// Every frame still closes with its own summary.
|
||||||
|
CHECK(out.find(std::string("source: ") + loop + " (1 line(s))")
|
||||||
|
!= std::string::npos);
|
||||||
|
|
||||||
|
std::remove(loop);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user