commit 9ff956a2e8556a872bec0633637982a51b4b88e7
parent 85f4d72210d79fa1a6841c7d0fab48343a28620e
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Thu, 16 Jul 2026 10:12:48 -0700
dbg: propagate explicit script failures
Diffstat:
| M | driver/cmd/dbg.c | | | 79 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------- |
| M | test/dbg/run.sh | | | 43 | +++++++++++++++++++++++++++++++++++++++++++ |
2 files changed, 97 insertions(+), 25 deletions(-)
diff --git a/driver/cmd/dbg.c b/driver/cmd/dbg.c
@@ -176,13 +176,9 @@ void driver_help_dbg(void) {
" --batch non-interactive: suppress banner, exit after\n"
" --script / --command sources drain; exit 1 on\n"
" any command error\n"
- " Note: multi-line jit{} / expr{} blocks in --script files must "
- "fit\n"
- " on a single line (continuation reads go to stdin, not the file).\n"
- "\n"
- " KNOWN LIMITATION: an unreadable --script file is diagnosed but\n"
- " currently returns status 0. Check stderr/file existence rather\n"
- " than relying on status alone until this is remediated.\n"
+ " Each script command must fit on one line. Missing, unreadable,\n"
+ " overlong, or malformed explicit scripts return status 1 without\n"
+ " falling through to an interactive prompt.\n"
"\n"
"SIGNALS\n"
" Ctrl-C is forwarded into the running session as an interrupt; at\n"
@@ -589,6 +585,7 @@ typedef struct DbgState {
uint32_t nscript_entries;
int batch_mode;
int error_count;
+ int script_failed;
} DbgState;
/* Like driver_errf but increments s->error_count so --batch can propagate
@@ -3245,47 +3242,74 @@ static void dbg_complete(void* user, const char* line, size_t cursor,
* Script source helpers
* ============================================================ */
-/* Execute a single command string as if typed at the REPL.
- * Returns 1 if the REPL should exit (quit command), 0 otherwise. */
-static int dbg_run_script_cmd(DbgState* s, const char* cmd) {
+typedef enum DbgScriptResult {
+ DBG_SCRIPT_CONTINUE = 0,
+ DBG_SCRIPT_QUIT,
+ DBG_SCRIPT_ERROR,
+} DbgScriptResult;
+
+/* Execute a single command string as if typed at the REPL. */
+static DbgScriptResult dbg_run_script_cmd(DbgState* s, const char* cmd) {
char line[LINE_CAP];
size_t n = driver_strlen(cmd);
- if (n >= LINE_CAP) n = LINE_CAP - 1;
+ int errors = s->error_count;
+ int quit;
+ if (n >= LINE_CAP) {
+ dbg_errf(s, "script command exceeds %u bytes", (unsigned)(LINE_CAP - 1u));
+ return DBG_SCRIPT_ERROR;
+ }
driver_memcpy(line, cmd, n);
line[n] = '\0';
- return dbg_dispatch(s, line);
+ quit = dbg_dispatch(s, line);
+ if (quit) return DBG_SCRIPT_QUIT;
+ return s->error_count != errors ? DBG_SCRIPT_ERROR : DBG_SCRIPT_CONTINUE;
}
-/* Execute debugger commands from a file, one per line.
- * Returns 1 if the REPL should exit (quit command), 0 otherwise. */
-static int dbg_run_script_file(DbgState* s, const char* path) {
+/* Execute debugger commands from a file, one per line. */
+static DbgScriptResult dbg_run_script_file(DbgState* s, const char* path) {
DriverLoad load = {0};
KitSlice bytes;
const char* p;
const char* end;
char line[LINE_CAP];
- if (driver_load_bytes(&s->env->file_io, DBG_TOOL, path, &load, &bytes) != 0)
- return 0; /* driver_load_bytes already emitted an error */
+ uint64_t line_no = 0;
+ if (driver_load_bytes(&s->env->file_io, DBG_TOOL, path, &load, &bytes) != 0) {
+ ++s->error_count; /* driver_load_bytes already emitted the diagnostic */
+ return DBG_SCRIPT_ERROR;
+ }
p = (const char*)bytes.data;
end = p + bytes.len;
while (p < end) {
const char* nl = p;
size_t len;
+ int errors = s->error_count;
+ int quit;
+ ++line_no;
while (nl < end && *nl != '\n') nl++;
len = (size_t)(nl - p);
/* Strip trailing carriage return. */
if (len > 0 && p[len - 1] == '\r') len--;
- if (len >= LINE_CAP) len = LINE_CAP - 1;
+ if (len >= LINE_CAP) {
+ dbg_errf(s, "%s:%llu: script line exceeds %u bytes", path,
+ (unsigned long long)line_no, (unsigned)(LINE_CAP - 1u));
+ driver_release_bytes(&s->env->file_io, &load);
+ return DBG_SCRIPT_ERROR;
+ }
driver_memcpy(line, p, len);
line[len] = '\0';
p = (nl < end) ? nl + 1 : end;
- if (dbg_dispatch(s, line)) {
+ quit = dbg_dispatch(s, line);
+ if (quit) {
driver_release_bytes(&s->env->file_io, &load);
- return 1;
+ return DBG_SCRIPT_QUIT;
+ }
+ if (s->error_count != errors) {
+ driver_release_bytes(&s->env->file_io, &load);
+ return DBG_SCRIPT_ERROR;
}
}
driver_release_bytes(&s->env->file_io, &load);
- return 0;
+ return DBG_SCRIPT_CONTINUE;
}
/* ============================================================
@@ -3301,9 +3325,14 @@ static void dbg_repl(DbgState* s) {
/* Phase 1: drain --script / --command sources in order. */
for (i = 0; i < s->nscript_entries; i++) {
DbgScriptEntry* e = &s->script_entries[i];
- int quit = (e->kind == DBG_ENTRY_FILE) ? dbg_run_script_file(s, e->value)
- : dbg_run_script_cmd(s, e->value);
- if (quit) return;
+ DbgScriptResult result =
+ (e->kind == DBG_ENTRY_FILE) ? dbg_run_script_file(s, e->value)
+ : dbg_run_script_cmd(s, e->value);
+ if (result == DBG_SCRIPT_QUIT) return;
+ if (result == DBG_SCRIPT_ERROR) {
+ s->script_failed = 1;
+ return;
+ }
}
/* Phase 2: in --batch mode, exit without falling through to stdin. */
@@ -3483,7 +3512,7 @@ int driver_dbg_ex(int argc, char** argv, const KitDriverExtension* ext) {
kit_target_free(target);
dbg_options_release(&o);
driver_env_fini(&env);
- return (st.batch_mode && st.error_count > 0) ? 1 : 0;
+ return (st.script_failed || (st.batch_mode && st.error_count > 0)) ? 1 : 0;
}
int driver_dbg(int argc, char** argv) { return driver_dbg_ex(argc, argv, NULL); }
diff --git a/test/dbg/run.sh b/test/dbg/run.sh
@@ -52,6 +52,49 @@ KIT_STRICT_XFAIL=${DBG_STRICT_XFAIL:-0}
kit_report_init
+# Explicit script sources have command-style status semantics independent of
+# transcript contents. Exercise them outside the golden matrix so failures can
+# never be hidden by prompt normalization or an interactive EOF fallback.
+dbg_expect_status() {
+ des_name=$1
+ des_want=$2
+ shift 2
+ "$@" </dev/null > "$KIT_WORK/$des_name.stdout" \
+ 2> "$KIT_WORK/$des_name.stderr"
+ des_got=$?
+ if [ "$des_got" -eq "$des_want" ]; then
+ kit_pass "$des_name"
+ else
+ kit_fail "$des_name" "status=$des_got expected=$des_want"
+ sed 's/^/ stdout| /' "$KIT_WORK/$des_name.stdout"
+ sed 's/^/ stderr| /' "$KIT_WORK/$des_name.stderr"
+ fi
+}
+
+printf 'set\n' > "$KIT_WORK/malformed.dbg"
+awk 'BEGIN { for (i = 0; i < 1100; ++i) printf "x"; printf "\n" }' \
+ > "$KIT_WORK/overlong.dbg"
+printf 'q\n' > "$KIT_WORK/quit.dbg"
+
+dbg_expect_status missing-script-batch 1 \
+ "$KIT" dbg --batch --script "$KIT_WORK/does-not-exist.dbg"
+dbg_expect_status missing-script-interactive 1 \
+ "$KIT" dbg --script "$KIT_WORK/does-not-exist.dbg"
+dbg_expect_status unreadable-script-batch 1 \
+ "$KIT" dbg --batch --script "$KIT_WORK"
+dbg_expect_status malformed-script-batch 1 \
+ "$KIT" dbg --batch --script "$KIT_WORK/malformed.dbg"
+dbg_expect_status malformed-script-interactive 1 \
+ "$KIT" dbg --script "$KIT_WORK/malformed.dbg"
+dbg_expect_status overlong-script-batch 1 \
+ "$KIT" dbg --batch --script "$KIT_WORK/overlong.dbg"
+dbg_expect_status overlong-script-interactive 1 \
+ "$KIT" dbg --script "$KIT_WORK/overlong.dbg"
+dbg_expect_status explicit-quit-batch 0 \
+ "$KIT" dbg --batch --script "$KIT_WORK/quit.dbg"
+dbg_expect_status explicit-quit-interactive 0 \
+ "$KIT" dbg --script "$KIT_WORK/quit.dbg"
+
# normalize_stdout RAW CASE_DIR : multi-stage stdout normalizer (lane-local).
# Strips the "(kit) " prompt and continuation markers, re-tokenizes CASE_DIR
# to @CASE@, masks run-to-run hex addresses to 0xADDR, trims trailing