diff --git a/.claude/rules/c-runtime-memory.md b/.claude/rules/c-runtime-memory.md index 6c8f0f70..61f6db11 100644 --- a/.claude/rules/c-runtime-memory.md +++ b/.claude/rules/c-runtime-memory.md @@ -27,7 +27,11 @@ iteration, or a collector that quietly stops working). model: a new owning edge out of Value/Env/Chunk goes into both, and only *counted* edges may be traversed (an uncounted edge trips the accounting abort and collection silently stops working). Conservative direction: - missing an edge leaks; inventing one frees live memory. + missing an edge leaks; inventing one frees live memory. A new `ValType` + or `ASTType` is a **build error** at every switch that must learn about + it (#737/#738: no `default:` arms on closed-enum switches — + `-Werror=switch` enforces exhaustiveness; don't add a `default:` back, + enumerate the no-op cases instead). - **Trace gating**: `g_trace_hist` (assignment history) and `g_trace_obs_hist` (observer snapshots) are compiler-set flags — recording is off unless the program contains a temporal query diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a580f6f..537c7923 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,31 @@ All notable changes to EigenScript are documented here. ### Fixed +- **The `ValType` switches are now exhaustive too, closing out the #738 + sweep — and the first build immediately caught real drift (#738).** + The ASTType half of #738 landed earlier (see below); this finishes the + issue: the remaining `default:` arms over closed enums are gone from + `val_type_name`, the Value destructor, `values_equal`, + `chan_clone_rec`, `GC_FOR_EACH_CHILD`, `gc_clear_node`, + `eigs_value_type` (embed API), `store_json_encode`, both trace-tape + value formatters, the observer dump, and `OP_SLICE`'s type check — a + new `ValType` is now a build error at 16 sites (verified by planting + an 11th enumerator), instead of a silent leak in the destructor plus a + wrong answer everywhere else. One `ASTType` straggler is also cleared: + W022's binding collector, added *after* the sweep, had reintroduced a + `default:` arm — the exact drift mode the issue predicted. The + enforcement's first compile found real drift: `store_json_encode` did + not handle `VAL_BUFFER`, so a buffer stored through ext_store silently + encodes as `null` (data loss — kept behavior-preserving here, tracked + as #805), and the trace tape's full nondet formatter renders + `VAL_JSON_RAW`/`VAL_TEXT_BUILDER` as opaque `` where the short + form has ``/`` (latent; noted in #805). All grouped arms + are otherwise strictly mechanical — suite output is byte-identical. + Legitimately open switches keep their `default:`: untrusted bytecode + bytes (the leaf-accessor scan, the non-GNU dispatch arm), guarded + subsets (JIT binop/cmparm emitters, compound-assign tokens), and + `TokType` (out of #738's scope). + - **The opcode operand-layout tables are now exhaustive switches on `OpCode`, closing a verifier drift (#737).** `op_name`, `op_verify_operands`, and `op_stack_effect` each switched on `uint8_t` diff --git a/src/eigenscript.c b/src/eigenscript.c index 22a458ab..6912e669 100644 --- a/src/eigenscript.c +++ b/src/eigenscript.c @@ -221,8 +221,10 @@ const char* val_type_name(ValType t) { case VAL_DICT: return "dict"; case VAL_BUFFER: return "buffer"; case VAL_TEXT_BUILDER: return "text_builder"; - default: return "?"; + /* No `default:` — -Werror=switch (Makefile CFLAGS) makes a new + * ValType a build error here instead of printing "?". */ } + return "?"; /* unreachable for valid ValType values */ } /* g_global_env is an EigsState field — see eigenscript.h bridge macros. */ @@ -981,7 +983,12 @@ void free_value(Value *v) { case VAL_TEXT_BUILDER: free(v->data.text_builder.data); break; - default: + /* No owned memory. Enumerated rather than covered by a `default:` so + * that -Werror=switch (Makefile CFLAGS) makes a new ValType a build + * error here instead of a silent leak. */ + case VAL_NUM: + case VAL_BUILTIN: + case VAL_NULL: break; } free(v); @@ -1338,10 +1345,19 @@ static Value *chan_clone_rec(Value *v, int depth) { out->data.dict.keys[i] = (char *)chan_intern_key(out->data.dict.keys[i]); return out; } - default: - val_incref(v); /* share fn/builtin/buffer/text_builder/json by refcount */ + /* Shared by refcount, not cloned. Enumerated rather than covered by a + * `default:` so that -Werror=switch (Makefile CFLAGS) forces a new + * ValType to choose clone-vs-share here. */ + case VAL_FN: + case VAL_BUILTIN: + case VAL_BUFFER: + case VAL_TEXT_BUILDER: + case VAL_JSON_RAW: + val_incref(v); return v; } + val_incref(v); /* unreachable for valid ValType values */ + return v; } /* Deep-copy a value for cross-thread channel transfer (see chan_clone_rec). @@ -1480,9 +1496,14 @@ static int values_equal_impl(Value *a, Value *b, int depth) { return a->data.text_builder.len == b->data.text_builder.len && memcmp(a->data.text_builder.data, b->data.text_builder.data, a->data.text_builder.len) == 0; - default: /* VAL_FN, VAL_BUILTIN, VAL_JSON_RAW — identity */ + /* Identity comparison. Enumerated rather than covered by a `default:` + * so -Werror=switch forces a new ValType to choose its equality. */ + case VAL_FN: + case VAL_BUILTIN: + case VAL_JSON_RAW: return a == b; } + return a == b; /* unreachable for valid ValType values */ } int values_equal(Value *a, Value *b) { return values_equal_impl(a, b, 0); } @@ -2514,7 +2535,13 @@ static int gc_env_is_node(Env *e) { } \ } \ break; \ - default: break; \ + /* Leaf types: no outgoing edges. Enumerated rather than \ + * covered by a `default:` so -Werror=switch makes a new \ + * ValType a build error at every expansion of this macro. */ \ + case VAL_NUM: case VAL_STR: case VAL_NULL: \ + case VAL_JSON_RAW: case VAL_BUILTIN: \ + case VAL_BUFFER: case VAL_TEXT_BUILDER: \ + break; \ } \ } \ } while (0) @@ -2569,7 +2596,13 @@ static void gc_clear_node(void *obj, int kind) { val_decref(v->data.dict.vals[i]); v->data.dict.count = 0; break; - default: break; + /* Leaf types: no outgoing edges to clear. Enumerated rather than + * covered by a `default:` so -Werror=switch keeps this in lockstep + * with GC_FOR_EACH_CHILD when a ValType is added. */ + case VAL_NUM: case VAL_STR: case VAL_NULL: + case VAL_JSON_RAW: case VAL_BUILTIN: + case VAL_BUFFER: case VAL_TEXT_BUILDER: + break; } } } diff --git a/src/eigs_embed.c b/src/eigs_embed.c index 93beb5e9..1d180c67 100644 --- a/src/eigs_embed.c +++ b/src/eigs_embed.c @@ -214,8 +214,12 @@ EigsValueType eigs_value_type(EigsValue *v) { case VAL_FN: case VAL_BUILTIN: return EIGS_TYPE_FN; case VAL_BUFFER: return EIGS_TYPE_BUFFER; - default: return EIGS_TYPE_OTHER; + /* No public embed-API mapping. Enumerated rather than covered by a + * `default:` so -Werror=switch forces a new ValType to choose one. */ + case VAL_JSON_RAW: + case VAL_TEXT_BUILDER: return EIGS_TYPE_OTHER; } + return EIGS_TYPE_OTHER; /* unreachable for valid ValType values */ } double eigs_value_as_num(EigsValue *v) { diff --git a/src/ext_store.c b/src/ext_store.c index 7516a2cf..523ce0d1 100644 --- a/src/ext_store.c +++ b/src/ext_store.c @@ -127,7 +127,18 @@ static void store_json_encode(Value *v, strbuf *out) { strbuf_append_char(out, '}'); break; } - default: + /* VAL_NULL/VAL_FN/VAL_BUILTIN are handled by the guard above; raw + * JSON, text builders and buffers have no store encoding (buffers + * silently store as null — a data-loss gap this enumeration made + * visible; tracked upstream). Enumerated rather than covered by a + * `default:` so -Werror=switch forces a new ValType to choose its + * store-JSON encoding here. */ + case VAL_NULL: + case VAL_FN: + case VAL_BUILTIN: + case VAL_JSON_RAW: + case VAL_TEXT_BUILDER: + case VAL_BUFFER: strbuf_append(out, "null"); break; } diff --git a/src/lint.c b/src/lint.c index 4d99c28f..0cee6f66 100644 --- a/src/lint.c +++ b/src/lint.c @@ -2571,7 +2571,33 @@ static void w022_collect_bindings(ASTNode *n, W022Table *t) { for (int c = 0; c < n->data.match.case_count; c++) w022_poison_idents(n->data.match.patterns[c], t); break; - default: + /* Nothing to do for these. Enumerated rather than covered by a `default:` + * so that -Werror=switch (Makefile CFLAGS) makes a new ASTType a build + * error here instead of a silent no-op. */ + case AST_NUM: + case AST_STR: + case AST_IDENT: + case AST_NULL: + case AST_BINOP: + case AST_UNARY: + case AST_RELATION: + case AST_IF: + case AST_LOOP: + case AST_RETURN: + case AST_BLOCK: + case AST_LIST: + case AST_INDEX: + case AST_PROGRAM: + case AST_INTERROGATE: + case AST_PREDICATE: + case AST_DICT: + case AST_DOT: + case AST_BREAK: + case AST_CONTINUE: + case AST_DOT_ASSIGN: + case AST_UNOBSERVED: + case AST_INDEX_ASSIGN: + case AST_SLICE: break; } LINT_FOR_EACH_CHILD(n, child, w022_collect_bindings(child, t)); diff --git a/src/trace.c b/src/trace.c index 8100a904..9a7fb3ae 100644 --- a/src/trace.c +++ b/src/trace.c @@ -1133,7 +1133,8 @@ static void write_value_ptr(Value *v) { case VAL_BUFFER: tp_printf("", v->data.buffer.count); break; case VAL_JSON_RAW: tp_puts(""); break; case VAL_TEXT_BUILDER: tp_puts(""); break; - default: tp_puts(""); break; + /* No `default:` — -Werror=switch (Makefile CFLAGS) forces a new + * ValType to choose its tape rendering here. */ } } @@ -1266,7 +1267,11 @@ static void write_value_ptr_full(Value *v, int *budget) { } case VAL_FN: wf_puts("", budget); break; case VAL_BUILTIN: wf_puts("", budget); break; - default: wf_puts("", budget); break; + /* Opaque placeholders (byte-identical to the old `default:` output; + * these have no replayable N-record encoding). Enumerated so that + * -Werror=switch forces a new ValType to choose its encoding here. */ + case VAL_JSON_RAW: wf_puts("", budget); break; + case VAL_TEXT_BUILDER: wf_puts("", budget); break; } } diff --git a/src/vm.c b/src/vm.c index edb8c1e5..c3e9ae90 100644 --- a/src/vm.c +++ b/src/vm.c @@ -110,7 +110,9 @@ static void obs_dump_value(EigsSlot s, char *buf, size_t nbuf) { case VAL_TEXT_BUILDER: snprintf(buf, nbuf, "", v->data.text_builder.len); break; case VAL_JSON_RAW: snprintf(buf, nbuf, ""); break; - default: snprintf(buf, nbuf, "null"); break; + /* No `default:` — -Werror=switch (Makefile CFLAGS) forces a new + * ValType to choose its observer-dump rendering here. */ + case VAL_NULL: snprintf(buf, nbuf, "null"); break; } return; } @@ -4912,7 +4914,10 @@ static Value *vm_run_ex(EigsChunk *chunk, Env *env, Task *resume) { case VAL_LIST: len = target->data.list.count; break; case VAL_STR: len = (int)strlen(target->data.str); break; case VAL_BUFFER: len = target->data.buffer.count; break; - default: + /* Not sliceable. Enumerated rather than covered by a `default:` + * so -Werror=switch forces a new ValType to choose here. */ + case VAL_NUM: case VAL_FN: case VAL_BUILTIN: case VAL_NULL: + case VAL_JSON_RAW: case VAL_DICT: case VAL_TEXT_BUILDER: rt_error(EK_TYPE, g_vm.current_line, "cannot slice %s", val_type_name(target->type)); slot_decref(start_s); slot_decref(end_s); slot_decref(tgt_s);