feat(scheduler-core): implement REPL execution control (task 12.1)

Adds the interactive simulation REPL's execution-control slice (Req 5
AC 22): a single-threaded input loop (readline-based) supporting
step [N], continue, quit, and help, pausing before the first replay
event and blocking the driver loop while active.

Review round 1 raised two findings, both resolved here:

- GNU readline (GPL) was linked directly into xce_scheduler, a shipped
  production daemon. Switched the whole project to libedit (BSD) for
  line editing -- scheduler and controller (xce_test) both now use
  editline/readline.h, API-compatible (readline()/add_history()).

- Getting a clean regression run after the libedit swap required a
  full clean rebuild, which exposed a genuine pre-existing SIGSEGV
  (bug-1504): SnapshotData.internalState was a capnp Data field, and
  this codebase's capnpc-c backend silently no-ops Data-field
  encoding without zero-initializing the caller's stack struct,
  leaving an uninitialized capn_ptr written onto the wire. Fixed by
  switching internalState to List(UInt8) (with a new internalStateLen
  wire field), matching the working FetchFileReply.content/bytesRead
  precedent -- no hand-written accessor code needed.

A second, unrelated instance of the same bug class was found in the
controller's WAL-catchup path (WalCatchupEntry.payload) and
deliberately left unfixed as bug-1505, out of scheduler-core's
boundary; needs its own project-wide Data-field audit.

Scheduler suite: 34/34 clean. RED-phase reproduction recorded in
tasks.md (repl_should_pause() step-budget check disabled -> 2/4
tests fail as expected -> reverted -> 34/34 green).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JPPXeKSUXUqcqvzNXY1rzJ
This commit is contained in:
Rongsong Shen 2026-08-07 08:17:35 +00:00
parent de00ccff37
commit 02f7f48ff9
Signed by: rshen
SSH key fingerprint: SHA256:8Vf2pZ3h6rJa7EhGK+WaAQ9Mta/apZvteW6g9U/WKKk
17 changed files with 1491 additions and 81 deletions

View file

@ -4,21 +4,27 @@
* or structural check confirms no controller-facing symbol is reachable
* from the simulation driver's compiled object" -- plus the AC's own
* "confirmed by the struct definition itself, not runtime checks alone"
* clause. Scope: sim.h + sim.c + sim_loop.c + sim_record.c together (task
* 11.1 + task 11.2's sibling file + task 11.5's sibling file, the
* --sim-output writer) -- NOT a re-audit of core.c (task 4.1's own
* territory, test_core_isolation.c) and NOT plugin.c/builtin_fcfs.c (shared
* clause. Scope: sim.h + sim.c + sim_loop.c + sim_record.c + repl.c/repl.h
* together (task 11.1 + task 11.2's sibling file + task 11.5's sibling
* file, the --sim-output writer + task 12.1's sibling files, the
* interactive REPL) -- NOT a re-audit of core.c (task 4.1's own territory,
* test_core_isolation.c) and NOT plugin.c/builtin_fcfs.c (shared
* normal-mode infra, out of this task's boundary). sim_verify.c (task 11.4)
* is likewise out of scope here -- it is never compiled into
* sched_sim_isolation_lib below and carries no isolation-sensitive logic of
* its own (pure struct comparison, no I/O); extending this guard to it was
* not part of task 11.4's own text and is left as-is.
* not part of task 11.4's own text and is left as-is. repl.c/repl.h are
* added here per task 12.1's own text (extending this exact 3-file
* precedent to a 5-file one) -- the REPL reads/writes stdin/stdout and
* touches xce_sched_sim_driver_t directly, the same category of
* driver-level code this guard already covers.
*
* Three layers, extending test_core_isolation.c's two-layer pattern (same
* genre: source-text scans of the real, current on-disk files, not a frozen
* snapshot):
*
* 1. Source-text scan of sim.h/sim.c/sim_loop.c: no #include of
* 1. Source-text scan of sim.h/sim.c/sim_loop.c/sim_record.c/repl.c/
* repl.h: no #include of
* client.h/netio.h/xceapi.h/delay.h/idgen.h/xce/cluster.h; no call-site
* substring matching a symbol exported only by those libraries (the
* exact FORBIDDEN_SYMBOLS list test_core_isolation.c already
@ -83,9 +89,17 @@
#error \
"XCE_SIM_RECORD_C_PATH must be defined by meson.build to sim_record.c's absolute path"
#endif
#ifndef XCE_SIM_REPL_C_PATH
#error \
"XCE_SIM_REPL_C_PATH must be defined by meson.build to repl.c's absolute path"
#endif
#ifndef XCE_SIM_REPL_H_PATH
#error \
"XCE_SIM_REPL_H_PATH must be defined by meson.build to repl.h's absolute path"
#endif
#ifndef XCE_SIM_LIB_PATH
#error \
"XCE_SIM_LIB_PATH must be defined by meson.build to the absolute path of sim.c+sim_loop.c+sim_record.c's isolated static_library() archive"
"XCE_SIM_LIB_PATH must be defined by meson.build to the absolute path of sim.c+sim_loop.c+sim_record.c+repl.c's isolated static_library() archive"
#endif
/* Forbidden headers (design.md "Allowed Dependencies", same list
@ -359,16 +373,22 @@ static void test_sim_source_paths_are_readable(void)
char* c = read_whole_file(XCE_SIM_C_PATH);
char* loop = read_whole_file(XCE_SIM_LOOP_C_PATH);
char* record = read_whole_file(XCE_SIM_RECORD_C_PATH);
char* repl_c = read_whole_file(XCE_SIM_REPL_C_PATH);
char* repl_h = read_whole_file(XCE_SIM_REPL_H_PATH);
TEST_ASSERT_NOT_NULL_MESSAGE(h, XCE_SIM_H_PATH);
TEST_ASSERT_NOT_NULL_MESSAGE(c, XCE_SIM_C_PATH);
TEST_ASSERT_NOT_NULL_MESSAGE(loop, XCE_SIM_LOOP_C_PATH);
TEST_ASSERT_NOT_NULL_MESSAGE(record, XCE_SIM_RECORD_C_PATH);
TEST_ASSERT_NOT_NULL_MESSAGE(repl_c, XCE_SIM_REPL_C_PATH);
TEST_ASSERT_NOT_NULL_MESSAGE(repl_h, XCE_SIM_REPL_H_PATH);
free(h);
free(c);
free(loop);
free(record);
free(repl_c);
free(repl_h);
}
static void test_sim_never_includes_forbidden_headers(void)
@ -377,38 +397,54 @@ static void test_sim_never_includes_forbidden_headers(void)
char* c_raw = read_whole_file(XCE_SIM_C_PATH);
char* loop_raw = read_whole_file(XCE_SIM_LOOP_C_PATH);
char* record_raw = read_whole_file(XCE_SIM_RECORD_C_PATH);
char* repl_c_raw = read_whole_file(XCE_SIM_REPL_C_PATH);
char* repl_h_raw = read_whole_file(XCE_SIM_REPL_H_PATH);
char* h;
char* c;
char* loop;
char* record;
char* repl_c;
char* repl_h;
TEST_ASSERT_NOT_NULL(h_raw);
TEST_ASSERT_NOT_NULL(c_raw);
TEST_ASSERT_NOT_NULL(loop_raw);
TEST_ASSERT_NOT_NULL(record_raw);
TEST_ASSERT_NOT_NULL(repl_c_raw);
TEST_ASSERT_NOT_NULL(repl_h_raw);
h = strip_c_comments(h_raw);
c = strip_c_comments(c_raw);
loop = strip_c_comments(loop_raw);
record = strip_c_comments(record_raw);
repl_c = strip_c_comments(repl_c_raw);
repl_h = strip_c_comments(repl_h_raw);
TEST_ASSERT_NOT_NULL(h);
TEST_ASSERT_NOT_NULL(c);
TEST_ASSERT_NOT_NULL(loop);
TEST_ASSERT_NOT_NULL(record);
TEST_ASSERT_NOT_NULL(repl_c);
TEST_ASSERT_NOT_NULL(repl_h);
assert_no_forbidden_includes(h, "sim.h");
assert_no_forbidden_includes(c, "sim.c");
assert_no_forbidden_includes(loop, "sim_loop.c");
assert_no_forbidden_includes(record, "sim_record.c");
assert_no_forbidden_includes(repl_c, "repl.c");
assert_no_forbidden_includes(repl_h, "repl.h");
free(h_raw);
free(c_raw);
free(loop_raw);
free(record_raw);
free(repl_c_raw);
free(repl_h_raw);
free(h);
free(c);
free(loop);
free(record);
free(repl_c);
free(repl_h);
}
static void test_sim_never_calls_forbidden_symbols(void)
@ -417,38 +453,54 @@ static void test_sim_never_calls_forbidden_symbols(void)
char* c_raw = read_whole_file(XCE_SIM_C_PATH);
char* loop_raw = read_whole_file(XCE_SIM_LOOP_C_PATH);
char* record_raw = read_whole_file(XCE_SIM_RECORD_C_PATH);
char* repl_c_raw = read_whole_file(XCE_SIM_REPL_C_PATH);
char* repl_h_raw = read_whole_file(XCE_SIM_REPL_H_PATH);
char* h;
char* c;
char* loop;
char* record;
char* repl_c;
char* repl_h;
TEST_ASSERT_NOT_NULL(h_raw);
TEST_ASSERT_NOT_NULL(c_raw);
TEST_ASSERT_NOT_NULL(loop_raw);
TEST_ASSERT_NOT_NULL(record_raw);
TEST_ASSERT_NOT_NULL(repl_c_raw);
TEST_ASSERT_NOT_NULL(repl_h_raw);
h = strip_c_comments(h_raw);
c = strip_c_comments(c_raw);
loop = strip_c_comments(loop_raw);
record = strip_c_comments(record_raw);
repl_c = strip_c_comments(repl_c_raw);
repl_h = strip_c_comments(repl_h_raw);
TEST_ASSERT_NOT_NULL(h);
TEST_ASSERT_NOT_NULL(c);
TEST_ASSERT_NOT_NULL(loop);
TEST_ASSERT_NOT_NULL(record);
TEST_ASSERT_NOT_NULL(repl_c);
TEST_ASSERT_NOT_NULL(repl_h);
assert_no_forbidden_symbols(h, "sim.h");
assert_no_forbidden_symbols(c, "sim.c");
assert_no_forbidden_symbols(loop, "sim_loop.c");
assert_no_forbidden_symbols(record, "sim_record.c");
assert_no_forbidden_symbols(repl_c, "repl.c");
assert_no_forbidden_symbols(repl_h, "repl.h");
free(h_raw);
free(c_raw);
free(loop_raw);
free(record_raw);
free(repl_c_raw);
free(repl_h_raw);
free(h);
free(c);
free(loop);
free(record);
free(repl_c);
free(repl_h);
}
/*
@ -523,8 +575,8 @@ static void test_sim_object_has_no_forbidden_undefined_symbols(void)
/* posix format for an undefined symbol: "<name> U" (no
* value/size columns). Skip anything that doesn't parse as
* exactly that -- e.g. a blank line, or an archive member header
* some nm versions print (this archive has three member objects,
* sim.c.o, sim_loop.c.o, and sim_record.c.o). */
* some nm versions print (this archive has four member objects,
* sim.c.o, sim_loop.c.o, sim_record.c.o, and repl.c.o). */
if (sscanf(line, "%511s %c", symbol, &type) != 2) {
continue;
}