Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .claude/rules/c-runtime-memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<heap>` where the short
form has `<json>`/`<text>` (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`
Expand Down
47 changes: 40 additions & 7 deletions src/eigenscript.c
Original file line number Diff line number Diff line change
Expand Up @@ -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. */

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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); }
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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;
}
}
}
Expand Down
6 changes: 5 additions & 1 deletion src/eigs_embed.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
13 changes: 12 additions & 1 deletion src/ext_store.c
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Comment on lines +130 to +135
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;
}
Expand Down
28 changes: 27 additions & 1 deletion src/lint.c
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
9 changes: 7 additions & 2 deletions src/trace.c
Original file line number Diff line number Diff line change
Expand Up @@ -1133,7 +1133,8 @@ static void write_value_ptr(Value *v) {
case VAL_BUFFER: tp_printf("<buffer:%d>", v->data.buffer.count); break;
case VAL_JSON_RAW: tp_puts("<json>"); break;
case VAL_TEXT_BUILDER: tp_puts("<text>"); break;
default: tp_puts("<heap>"); break;
/* No `default:` — -Werror=switch (Makefile CFLAGS) forces a new
* ValType to choose its tape rendering here. */
}
Comment on lines +1136 to 1138
}

Expand Down Expand Up @@ -1266,7 +1267,11 @@ static void write_value_ptr_full(Value *v, int *budget) {
}
case VAL_FN: wf_puts("<fn>", budget); break;
case VAL_BUILTIN: wf_puts("<builtin>", budget); break;
default: wf_puts("<heap>", 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("<heap>", budget); break;
case VAL_TEXT_BUILDER: wf_puts("<heap>", budget); break;
}
}

Expand Down
9 changes: 7 additions & 2 deletions src/vm.c
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,9 @@ static void obs_dump_value(EigsSlot s, char *buf, size_t nbuf) {
case VAL_TEXT_BUILDER:
snprintf(buf, nbuf, "<text-builder:%zu>", v->data.text_builder.len); break;
case VAL_JSON_RAW: snprintf(buf, nbuf, "<json-raw>"); break;
default: snprintf(buf, nbuf, "null"); break;
/* No `default:` — -Werror=switch (Makefile CFLAGS) forces a new
* ValType to choose its observer-dump rendering here. */
Comment on lines 110 to +114
case VAL_NULL: snprintf(buf, nbuf, "null"); break;
}
return;
}
Expand Down Expand Up @@ -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",
Comment on lines +4917 to 4921
val_type_name(target->type));
slot_decref(start_s); slot_decref(end_s); slot_decref(tgt_s);
Expand Down
Loading