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

@ -0,0 +1,82 @@
#if !defined(_REPL_H_)
#define _REPL_H_ 1
#include "sim.h"
/*
* repl.h - interactive simulation REPL, execution-control slice (scheduler-
* core spec, task 12.1, Req 5 AC 22). New file per design.md's File
* Structure Plan ("repl.c -- NEW (Phase 3): interactive REPL command loop
* (Req 5 AC 22)"), Phase 3, sibling to sim.h/sim.c/sim_loop.c.
*
* This header declares repl.c's two driver-facing entry points, plus two
* `static inline` wrappers that fold the `sim->interactive` gate INTO the
* header itself. That inline-wrapper indirection is a deliberate, judgment-
* based deviation from this file family's usual "extern declared where
* consumed, no dedicated header" convention (sim_verify.c/sim_record.c/
* recording_session.c's entry points are all extern-declared directly
* inside sim_loop.c -- see that file's own header comment): sim_loop.c was
* already at 796/800 lines (this project's per-file cap, CLAUDE.md) before
* this task, leaving no room for even one bare `if (sim->interactive) { ...
* }` block at each of the two call sites design.md's REPL integration
* pseudocode requires (pre-replay pause, post-session pause), let alone two.
* Moving the gate into these inline wrappers keeps sim_loop.c's own diff to
* two single-line, brace-free call expressions -- this task's own text
* anticipates exactly this fallback ("move the hook logic into a tiny
* static inline helper in repl.h, or extract something unrelated out of
* sim_loop.c"). The two real functions (`xce_repl_start`/
* `xce_repl_post_session_check`) still carry the actual logic; a `static
* inline` here costs nothing at link time (each including TU gets its own
* copy, trivially small, no ODR risk -- sim_loop.c is the only production
* consumer).
*
* Boundary (Req 5 AC 17, "Simulation isolation", same as sim.h): this header
* only depends on sim.h (itself core.h + stdbool/stdint/stdio) -- no
* scheduler.h, no client.h, no capnp types. repl.c's own file header
* comment documents the same boundary for its implementation.
*/
/*
* xce_repl_start - the pre-replay pause (Req 5 AC 22's interaction model:
* "blocks on stdin at startup"). Prints the load banner ("[sim] loaded: ...
* snapshot @ ts=...", "[sim] ready. type 'help' for commands.") then enters
* the REPL command loop, blocking on stdin until the operator issues a
* command that hands control back (`step [N]`/`continue`) or terminates the
* process (`quit`, which never returns). Called once, by
* sched_sim_driver_run() (sim_loop.c), strictly before its event loop's
* `for(;;)` begins -- see xce_repl_pre_replay_pause() below for the actual
* production call site.
*/
extern void xce_repl_start(xce_sched_sim_driver_t* sim);
/*
* xce_repl_post_session_check - the per-session pause-decision point (Req 5
* AC 22's interaction model: "blocks on stdin ... at each breakpoint hit";
* task 12.1's own narrower scope: only a `step N` budget reaching zero, no
* breakpoints yet). Called once per completed ReplaySession, immediately
* after xce_core_end_session() (sim_loop.c), when sim->interactive is set.
* Internally decides whether to pause (a `step` budget hit zero; `continue`
* mode never pauses here in 12.1's scope) and, if so, re-enters the same
* blocking command loop xce_repl_start() uses. Task 12.2's own seam: it
* extends the INTERNAL pause decision (repl.c) to also check a breakpoint
* list -- this function's signature and sim_loop.c's own call site are not
* expected to change.
*/
extern void xce_repl_post_session_check(xce_sched_sim_driver_t* sim);
static inline void xce_repl_pre_replay_pause(xce_sched_sim_driver_t* sim)
{
if (sim->interactive) {
xce_repl_start(sim);
}
}
static inline void xce_repl_post_session_pause(xce_sched_sim_driver_t* sim)
{
if (sim->interactive) {
xce_repl_post_session_check(sim);
}
}
#endif

View file

@ -65,17 +65,34 @@
* write the output header/snapshot, so a re-derived value would drift from
* "the same SnapshotEntry that was loaded" (AC 19's own wording -- same ts,
* not merely a same-shaped one taken moments later).
*
* `repl_state` below is a SECOND field beyond design.md's literal sketch
* (scheduler-core spec, task 12.1, Req 5 AC 22) -- the interactive REPL's
* own execution-control state (current step-N/continue mode, remaining step
* budget; task 12.2/12.3 grow the same struct with breakpoints/param
* overrides). Declared here only as an opaque forward-declared pointer
* (xce_sched_repl_state_t, just below) -- the full struct body lives in
* repl.c, never in this header, matching plugin.h's own
* `xce_sched_core_t` forward-declare precedent for the identical "opaque
* pointer, defined in the consuming file" reason. NULL until
* xce_repl_start() lazily allocates it (repl.c) -- guaranteed NULL after a
* successful sched_sim_driver_start() the same way every other field here
* is, via that function's own `memset(sim, 0, sizeof(*sim))` (sim.c); no
* sim.c change was needed for this task.
*/
typedef struct xce_sched_repl_state_s xce_sched_repl_state_t;
typedef struct {
xce_sched_core_t core;
FILE* replay_file;
FILE* alt_events_file;
FILE* record_file;
uint64_t sim_speed_num;
uint64_t sim_speed_den;
bool verify;
bool interactive;
uint64_t snapshot_ts;
xce_sched_core_t core;
FILE* replay_file;
FILE* alt_events_file;
FILE* record_file;
uint64_t sim_speed_num;
uint64_t sim_speed_den;
bool verify;
bool interactive;
uint64_t snapshot_ts;
xce_sched_repl_state_t* repl_state;
} xce_sched_sim_driver_t;
/*