From ea07aa1510b394b03dc1e22a3119d82b2545a805 Mon Sep 17 00:00:00 2001 From: InauguralPhysicist Date: Sat, 24 Oct 2026 14:23:19 -0500 Subject: [PATCH 1/2] runtime: one replay-refusal helper, and the taped-fs rule reads k_tape_kinds (#1943) See the PR for the evidence (each surviving implementation was compared with both copies on every input either copy handled). Based on a Codex Cloud draft. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KC99CmwatKssQYggkBCgwF --- .../1943-deduplicate-replay-and-tape-rules.md | 1 + src/builtins.c | 12 +++---- src/builtins_host.c | 10 ++---- src/builtins_internal.h | 2 +- src/ext_db.c | 16 +++------- src/ext_store.c | 31 +++++++------------ src/trace.c | 4 +++ src/trace.h | 2 ++ tools/tape_kinds_check.sh | 8 ++++- 9 files changed, 40 insertions(+), 46 deletions(-) create mode 100644 changes/internal/1943-deduplicate-replay-and-tape-rules.md diff --git a/changes/internal/1943-deduplicate-replay-and-tape-rules.md b/changes/internal/1943-deduplicate-replay-and-tape-rules.md new file mode 100644 index 000000000..e7e101707 --- /dev/null +++ b/changes/internal/1943-deduplicate-replay-and-tape-rules.md @@ -0,0 +1 @@ +- Deduplicate replay boundary refusals and derive filesystem error taping from the canonical tape-kind table. diff --git a/src/builtins.c b/src/builtins.c index f3431ac4e..ed7ecc97a 100644 --- a/src/builtins.c +++ b/src/builtins.c @@ -2460,11 +2460,11 @@ Value* builtin_env_get(Value *arg) { * Wrapping these with TRACE_NONDET_TAKE is not enough — the value the * tape carries does not pin down the host state — so the contract is * "fail loudly" at the boundary. Documented in docs/TRACE.md. */ -int replay_blocks(const char *fn) { +int replay_blocks(const char *fn, const char *boundary) { if (__builtin_expect(g_replay_enabled, 0)) { rt_error(EK_IO, 0, - "%s: not replayable under EIGS_REPLAY (subprocess/concurrency " - "boundary; see docs/TRACE.md)", fn); + "%s: not replayable under EIGS_REPLAY (%s boundary; " + "see docs/TRACE.md)", fn, boundary); return 1; } return 0; @@ -4763,7 +4763,7 @@ Value* builtin_recv(Value *arg) { * replay (test_builtin_errors BE07 under EIGS_REPLAY). */ Channel *ch = channel_arg(arg, "recv"); if (!ch) return make_null(); - if (replay_blocks("recv")) return make_null(); + if (replay_blocks("recv", "subprocess/concurrency")) return make_null(); pthread_mutex_lock(&ch->mutex); while (ch->count == 0 && !ch->closed && !eigs_state_exit_requested(eigs_current_state(), NULL)) @@ -4783,7 +4783,7 @@ Value* builtin_recv(Value *arg) { Value* builtin_try_recv(Value *arg) { Channel *ch = channel_arg(arg, "try_recv"); /* #1072: validate before the replay block */ if (!ch) return make_null(); - if (replay_blocks("try_recv")) return make_null(); + if (replay_blocks("try_recv", "subprocess/concurrency")) return make_null(); pthread_mutex_lock(&ch->mutex); Value *val = NULL; if (ch->count > 0) { @@ -4808,7 +4808,7 @@ Value* builtin_recv_timeout(Value *arg) { } Channel *ch = channel_arg(BUILTIN_LIST_VIEW(arg, 0), "recv_timeout"); if (!ch) return make_null(); - if (replay_blocks("recv_timeout")) return make_null(); /* #1072: after validation */ + if (replay_blocks("recv_timeout", "subprocess/concurrency")) return make_null(); /* #1072: after validation */ Value *ms_v = BUILTIN_LIST_VIEW(arg, 1); if (ms_v->type != VAL_NUM) { rt_error(EK_TYPE, 0, "recv_timeout: ms must be a number"); diff --git a/src/builtins_host.c b/src/builtins_host.c index 9f5157930..7d01a1eb0 100644 --- a/src/builtins_host.c +++ b/src/builtins_host.c @@ -1123,11 +1123,7 @@ Value* builtin_remove_file(Value *arg) { * one-string list for readers/mkdir, and [message, null] for ls (whose normal * result may itself be a one-string list). */ static Value *file_io_error_message(const char *fn, const char *message) { - int taped = strcmp(fn, "mkdir") == 0 || strcmp(fn, "ls") == 0 || - strcmp(fn, "write_text") == 0 || - strcmp(fn, "write_bytes") == 0 || - strcmp(fn, "remove_file") == 0 || - strcmp(fn, "rename") == 0 || strncmp(fn, "read_", 5) == 0; + int taped = trace_core_kind_declared(fn); if (taped && __builtin_expect(g_trace_enabled, 0)) { Value *record = make_list(strcmp(fn, "ls") == 0 ? 2 : 1); list_append_owned(record, make_str(message)); @@ -1615,7 +1611,7 @@ Value* builtin_exec_capture(Value *arg) { * layer and [-1, ""] is exec_capture's documented "did not run". #1072: * after the argument guard, so a wrong-typed call errs the same way in * both modes. */ - if (replay_blocks("exec_capture")) return exec_capture_result(-1, ""); + if (replay_blocks("exec_capture", "subprocess/concurrency")) return exec_capture_result(-1, ""); /* Detect timeout form: [["cmd", ...], timeout_num] */ double timeout_sec = -1; @@ -1766,7 +1762,7 @@ Value* builtin_exec_capture(Value *arg) { * NUL-truncation), null on EOF. Same 10 MB cap as proc_read. */ Value* builtin_proc_read_buf(Value *arg) { STRICT_LIST_MAX(arg, 2, "proc_read_buf"); - if (replay_blocks("proc_read_buf")) return make_null(); + if (replay_blocks("proc_read_buf", "subprocess/concurrency")) return make_null(); if (!arg || arg->type != VAL_LIST || arg->data.list.count != 2) return make_null(); EIGS_VIEW(fd_v_view); diff --git a/src/builtins_internal.h b/src/builtins_internal.h index b658fbe10..800481501 100644 --- a/src/builtins_internal.h +++ b/src/builtins_internal.h @@ -74,7 +74,7 @@ void register_host_builtins(Env *env); /* builtins.c — shared with builtins_host.c's subprocess builtins: the #148 * "fail loudly under EIGS_REPLAY" boundary check (channels use it too). */ -int replay_blocks(const char *fn); +int replay_blocks(const char *fn, const char *boundary); /* builtins.c — the "is this name the language's?" predicate shared with * builtins_host.c (build_corpus skips registered builtins). */ diff --git a/src/ext_db.c b/src/ext_db.c index c1628a420..509017983 100644 --- a/src/ext_db.c +++ b/src/ext_db.c @@ -4,6 +4,7 @@ */ #include "ext_db_internal.h" +#include "builtins_internal.h" #include "ext_names.h" #include "trace.h" #if EIGENSCRIPT_EXT_DB @@ -11,13 +12,6 @@ #define DB_MAX_PARAMS 16 -static int db_replay_blocks(const char *fn) { - if (!__builtin_expect(g_replay_enabled, 0)) return 0; - rt_error(EK_IO, 0, "%s: not replayable under EIGS_REPLAY " - "(database boundary; see docs/TRACE.md)", fn); - return 1; -} - /* Postgres's boolean type OID (pinned by the wire protocol; see the * DB_OID_* block below, which carries the same value). */ #define DB_PARAM_OID_BOOL 16 @@ -88,7 +82,7 @@ static PGresult* db_exec_from_arg(Value *arg) { Value* builtin_db_connect(Value *arg) { (void)arg; - if (db_replay_blocks("db_connect")) return make_null(); + if (replay_blocks("db_connect", "database")) return make_null(); const char *url = getenv("DATABASE_URL"); if (!url || !url[0]) { return make_str("{\"status\": \"no_database\", \"message\": \"DATABASE_URL not set\"}"); @@ -274,7 +268,7 @@ static int db_emit_json_value(strbuf *sb, unsigned int oid, int isnull, /* ---- DB query builtins ---- */ Value* builtin_db_query_value(Value *arg) { - if (db_replay_blocks("db_query_value")) return make_null(); + if (replay_blocks("db_query_value", "database")) return make_null(); /* Run a SQL query and return row 0 col 0, typed by its SQL type (#887). * Usage: db_query_value of "SELECT COUNT(*) FROM table" * or: db_query_value of ["SELECT name FROM t WHERE id=$1", id] @@ -328,7 +322,7 @@ Value* builtin_db_query_value(Value *arg) { } Value* builtin_db_execute(Value *arg) { - if (db_replay_blocks("db_execute")) return make_null(); + if (replay_blocks("db_execute", "database")) return make_null(); /* Run a SQL command with optional params. Returns "ok"; raises on failure. * Usage: db_execute of "INSERT INTO ..." (no params) * or: db_execute of ["INSERT INTO t (a) VALUES ($1)", param1] */ @@ -348,7 +342,7 @@ Value* builtin_db_execute(Value *arg) { } Value* builtin_db_query_json(Value *arg) { - if (db_replay_blocks("db_query_json")) return make_null(); + if (replay_blocks("db_query_json", "database")) return make_null(); /* Run SQL and return all rows as a JSON array of objects, each value * carrying its SQL type (#887). * Usage: db_query_json of "SELECT id, name FROM table" diff --git a/src/ext_store.c b/src/ext_store.c index 1d3e6e0ac..258e3742a 100644 --- a/src/ext_store.c +++ b/src/ext_store.c @@ -4,6 +4,7 @@ */ #include "ext_store_internal.h" +#include "builtins_internal.h" #include "trace.h" #if EIGENSCRIPT_FREESTANDING @@ -864,16 +865,6 @@ static int page_data_used(Page *page) { /* A store handle names live file state, and writes affect a world that the * trace tape does not reconstruct. Refuse the whole family rather than * recording a handle id or allowing any operation to consult live storage. */ -static int store_replay_blocks(const char *fn) { - if (__builtin_expect(g_replay_enabled, 0)) { - rt_error(EK_IO, 0, - "%s: not replayable under EIGS_REPLAY (store boundary; " - "see docs/TRACE.md)", fn); - return 1; - } - return 0; -} - /* Free a Store and its one owned Value (the parsed catalog dict). Every * free(store) must go through here — the catalog is a make_dict / decoded-JSON * Value that leaks otherwise (test_store/lab/handle_forge). */ @@ -902,7 +893,7 @@ static Value* builtin_store_open(Value *arg) { rt_error(EK_TYPE, 0, "store_open requires a string path\n"); return make_null(); } - if (store_replay_blocks("store_open")) return make_null(); + if (replay_blocks("store_open", "store")) return make_null(); const char *path = arg->data.str; Store *store = xcalloc(1, sizeof(Store)); @@ -974,7 +965,7 @@ static Value* builtin_store_open(Value *arg) { static Value* builtin_store_close(Value *arg) { Store *store = store_arg(arg, "store_close"); if (!store) return make_null(); - if (store_replay_blocks("store_close")) return make_null(); + if (replay_blocks("store_close", "store")) return make_null(); /* Release handle BEFORE freeing memory to prevent use-after-free * if another thread calls get_store() concurrently. */ Value *id_val = (arg && arg->type == VAL_DICT) ? store_dict_get_view(arg, "_store_id", &(EigsView){{0}}) : NULL; @@ -1003,7 +994,7 @@ static Value* builtin_store_put(Value *arg) { } Store *store = store_arg(list_get_view(arg, 0, &(EigsView){{0}}), "store_put"); if (!store) return make_null(); - if (store_replay_blocks("store_put")) return make_null(); + if (replay_blocks("store_put", "store")) return make_null(); Value *col_val = list_get_view(arg, 1, &(EigsView){{0}}); Value *record = list_get_view(arg, 2, &(EigsView){{0}}); if (!col_val || col_val->type != VAL_STR) { @@ -1168,7 +1159,7 @@ static Value* builtin_store_get(Value *arg) { } Store *store = store_arg(list_get_view(arg, 0, &(EigsView){{0}}), "store_get"); if (!store) return make_null(); - if (store_replay_blocks("store_get")) return make_null(); + if (replay_blocks("store_get", "store")) return make_null(); Value *col_val = list_get_view(arg, 1, &(EigsView){{0}}); Value *key_val = list_get_view(arg, 2, &(EigsView){{0}}); if (!col_val || col_val->type != VAL_STR) return make_null(); @@ -1331,7 +1322,7 @@ static Value* builtin_store_delete(Value *arg) { * than unwinding, so this return is the post-raise placeholder, not * "deleted nothing". Same shape as the arity guard above. */ if (!store) return make_bool(0); - if (store_replay_blocks("store_delete")) return make_bool(0); + if (replay_blocks("store_delete", "store")) return make_bool(0); Value *col_val = list_get_view(arg, 1, &(EigsView){{0}}); Value *key_val = list_get_view(arg, 2, &(EigsView){{0}}); ARG_GUARD(!col_val || col_val->type != VAL_STR, @@ -1380,7 +1371,7 @@ static Value* builtin_store_query(Value *arg) { } Store *store = store_arg(list_get_view(arg, 0, &(EigsView){{0}}), "store_query"); if (!store) return make_list(0); - if (store_replay_blocks("store_query")) return make_list(0); + if (replay_blocks("store_query", "store")) return make_list(0); Value *col_val = list_get_view(arg, 1, &(EigsView){{0}}); if (!col_val || col_val->type != VAL_STR) return make_list(0); @@ -1433,7 +1424,7 @@ static Value* builtin_store_count(Value *arg) { * count of 0. Distinct from the two documented-answer zeros below, which * are store_count's real result for an unknown collection. */ if (!store) return make_num(0); - if (store_replay_blocks("store_count")) return make_num(0); + if (replay_blocks("store_count", "store")) return make_num(0); Value *col_val = list_get_view(arg, 1, &(EigsView){{0}}); ARG_GUARD(!col_val || col_val->type != VAL_STR, "store_count", "[store handle, string collection]", make_num(0)); @@ -1488,7 +1479,7 @@ static Value* builtin_store_update(Value *arg) { * than unwinding, so this return is the post-raise placeholder, not * "no record updated". */ if (!store) return make_bool(0); - if (store_replay_blocks("store_update")) return make_bool(0); + if (replay_blocks("store_update", "store")) return make_bool(0); ARG_GUARD(!col_val || col_val->type != VAL_STR, "store_update", "[store handle, string collection, key, record]", make_bool(0)); @@ -1601,7 +1592,7 @@ static Value* builtin_store_update(Value *arg) { static Value* builtin_store_collections(Value *arg) { Store *store = store_arg(arg, "store_collections"); if (!store) return make_list(0); - if (store_replay_blocks("store_collections")) return make_list(0); + if (replay_blocks("store_collections", "store")) return make_list(0); Value *list = make_list(store->catalog->data.dict.count); for (int i = 0; i < store->catalog->data.dict.count; i++) { list_append_owned(list, make_str(store->catalog->data.dict.keys[i])); @@ -1626,7 +1617,7 @@ static Value* builtin_store_drop(Value *arg) { * store_drop's documented "no" (that is the documented-answer zero * below). */ if (!store) return make_bool(0); - if (store_replay_blocks("store_drop")) return make_bool(0); + if (replay_blocks("store_drop", "store")) return make_bool(0); Value *col_val = list_get_view(arg, 1, &(EigsView){{0}}); ARG_GUARD(!col_val || col_val->type != VAL_STR, "store_drop", "[store handle, string collection]", make_bool(0)); diff --git a/src/trace.c b/src/trace.c index 3f732c724..88e379433 100644 --- a/src/trace.c +++ b/src/trace.c @@ -1996,6 +1996,10 @@ static unsigned core_tape_kinds(const char *fn) { return 0; } +int trace_core_kind_declared(const char *name) { + return name && core_tape_kinds(name) != 0; +} + int trace_declare_kind(const char *name, unsigned kinds) { if (!name || !*name || !kinds || core_tape_kinds(name)) return 0; pthread_mutex_lock(&g_host_kinds_mu); diff --git a/src/trace.h b/src/trace.h index e7bf88ab5..e4fbe63f8 100644 --- a/src/trace.h +++ b/src/trace.h @@ -448,6 +448,8 @@ int trace_replay_refuse_off_owner(const char *fn); /* #1637: declare the return kinds (a ValType bit set, 1u << VAL_X) of a * host-recorded nondet name; replay refuses a record of another kind, and a * name declared nowhere. 0 = refused (empty name, no kinds, a core name). */ +/* True when a runtime builtin has a return-kind row in k_tape_kinds. */ +int trace_core_kind_declared(const char *name); int trace_declare_kind(const char *name, unsigned kinds); #define TRACE_NONDET_RET(name, expr) do { \ diff --git a/tools/tape_kinds_check.sh b/tools/tape_kinds_check.sh index af8a251b7..f31394c75 100644 --- a/tools/tape_kinds_check.sh +++ b/tools/tape_kinds_check.sh @@ -7,7 +7,8 @@ # replay, on a user's tape. This gate fails first, at build review: # taped = every name src/*.c records or takes: TRACE_NONDET_TAKE/RET/ # RECORD("x"), ARG_GUARD_TAPED/PRETAKE(..., "x", ...), -# trace_replay_take("x"), trace_nondet_value("x"), and +# trace_replay_take("x"), trace_nondet_value("x"), +# file_io_error("x", ...) (all filesystem errors are taped), and # trace_source_record/replay("x") (which also emits `x_chunk`) # declared = every {"x", ...} row of k_tape_kinds # and requires taped == declared, both non-empty. The smoke/embedding test @@ -30,6 +31,11 @@ for f in "$SRC_DIR"/*.c; do print m s = substr(s, RSTART + RLENGTH) } + while (match(s, /file_io_error[(]"[a-z_0-9]+"/)) { + m = substr(s, RSTART, RLENGTH); sub(/^[^"]*"/, "", m); sub(/"$/, "", m) + print m + s = substr(s, RSTART + RLENGTH) + } while (match(s, /trace_source_(record|replay)[(]"[a-z_0-9]+"/)) { m = substr(s, RSTART, RLENGTH); sub(/^[^"]*"/, "", m); sub(/"$/, "", m) print m; print m "_chunk" From e82419bb579afe928d804590101afb651ab18eec Mon Sep 17 00:00:00 2001 From: InauguralPhysicist Date: Sat, 24 Oct 2026 15:20:15 -0500 Subject: [PATCH 2/2] shape-contract: repin the four exempt builtins replay_blocks moved (#1943) The full lane failed in shape-contract (and so strict_differential's preflight): builtin_db_execute, builtin_db_query_json, builtin_db_query_value and builtin_exec_capture are exempt rows that pin their bodies' and helpers' SHA-256. Reviewed: in each, exactly one line changed, the replay refusal (db_replay_blocks("x") or replay_blocks("x") became replay_blocks("x", boundary)). Every helper in the pinned closures (db_build_query, db_exec_from_arg, ...) is byte-identical, so each row's arity reason still holds verbatim. Repinned with the tool's own hash functions: 8 hash lines. The gate reads 0 errors. Plant: one added line in builtin_db_execute is red, naming it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KC99CmwatKssQYggkBCgwF --- tools/strict_shape_contracts.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tools/strict_shape_contracts.json b/tools/strict_shape_contracts.json index 058729c7a..81bf47759 100644 --- a/tools/strict_shape_contracts.json +++ b/tools/strict_shape_contracts.json @@ -628,11 +628,11 @@ ], "disposition": "exempt", "body_sha256": [ - "2a0585cb8bc27e8913bc2a4934e967620b36e00f161615aaee7dd370b8e93a27" + "6b2fd24e37e2c8815cc09b9c56984e8b2393f6af707ff41170c97457f39a4f9f" ], "source_sha256": { "builtin_db_execute": [ - "2a0585cb8bc27e8913bc2a4934e967620b36e00f161615aaee7dd370b8e93a27" + "6b2fd24e37e2c8815cc09b9c56984e8b2393f6af707ff41170c97457f39a4f9f" ], "db_build_query": [ "43adc9e73c43cc8806b029b7b9abb68141f106ac27ef0627d768c65e5d407c21" @@ -654,11 +654,11 @@ ], "disposition": "exempt", "body_sha256": [ - "86243406977be526aae70eab442f2adcfc7f00f7a2547d0ed926f1e7613e6c5d" + "342be3ccd930d86dd44f9c06119e7a9fb7ea7abfc4626ef0bfd19aa97502a30a" ], "source_sha256": { "builtin_db_query_json": [ - "86243406977be526aae70eab442f2adcfc7f00f7a2547d0ed926f1e7613e6c5d" + "342be3ccd930d86dd44f9c06119e7a9fb7ea7abfc4626ef0bfd19aa97502a30a" ], "db_build_query": [ "43adc9e73c43cc8806b029b7b9abb68141f106ac27ef0627d768c65e5d407c21" @@ -680,11 +680,11 @@ ], "disposition": "exempt", "body_sha256": [ - "231665e1df1d4fd381dddeea01664035fd7f998fc4c54a8284c73f93ce789a13" + "294571ce18bb52316eda6dee7bd6da02e85882c53238c915a8b57dd6f88c0dd4" ], "source_sha256": { "builtin_db_query_value": [ - "231665e1df1d4fd381dddeea01664035fd7f998fc4c54a8284c73f93ce789a13" + "294571ce18bb52316eda6dee7bd6da02e85882c53238c915a8b57dd6f88c0dd4" ], "db_build_query": [ "43adc9e73c43cc8806b029b7b9abb68141f106ac27ef0627d768c65e5d407c21" @@ -790,11 +790,11 @@ ], "disposition": "exempt", "body_sha256": [ - "167f0efe94c146d6daa77ed91fd3ef73368fcfacfd373a2ee91ba6f5bccc9e0d" + "634c385bd84ab175fbb2c93000dfa9ffda1f899d6f4cb8242bcd164feb219985" ], "source_sha256": { "builtin_exec_capture": [ - "167f0efe94c146d6daa77ed91fd3ef73368fcfacfd373a2ee91ba6f5bccc9e0d" + "634c385bd84ab175fbb2c93000dfa9ffda1f899d6f4cb8242bcd164feb219985" ], "eigs_list_num": [ "25755c9b1f519fb0447e2d648dc0272419914db805f9249954cd2e146b48a3bc"