From 4d982fdcfbab582e14c4cdacbf48038ed0de8e60 Mon Sep 17 00:00:00 2001 From: InauguralPhysicist Date: Thu, 9 Jul 2026 00:17:08 -0500 Subject: [PATCH] change: raise on invalid input in numeric/data builtins (silent-tolerance batch-2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second batch of the silent-tolerance audit (#490–#512). Builtins that folded bad input to a null/0/""/wrong-order sentinel — the hardest class of bug to spot in a data or numeric pipeline — now raise a catchable error from the closed error-kind set: - matmul (#512): incompatible shapes -> value, non-matrix -> type_mismatch, oversized result -> limit. - sha256/md5/sha256_file/md5_file/hmac_sha256 (#511): non-string -> type_mismatch (was the "" sentinel = a silent wrong digest); _file variants raise io on open failure. - range (#497/#498): non-numeric arg -> type_mismatch; past the 1M cap -> limit instead of silently building an unbounded list (the cap sized only the prealloc while the loop ran to the original bound — an OOM). - sort_by (#501): non-numeric key -> type_mismatch (was coerced to 0.0); mirrors sort (#368). - get_at/set_at + buf_get/buf_set (#499/#502): out-of-range -> index_range, non-list/non-buffer target or non-integer index -> type_mismatch, matching the xs[i] operator. Load-bearing check: no stdlib (lib/) reliance; the only reliance was three tests asserting the old silent behavior (range-of-string, get_at/buf_get OOB, sha of null, get_at/set_at past-the-start, matmul overflow) — updated to assert the raise. The get_at/set_at -4 case was asserting an inconsistency with [] (which already raises on -4); the change makes them consistent, which is exactly #312's stated goal. New regression: suite section [116] (test_raise_on_bad_input.eigs, 21 checks). Release + ASan/leak suites green (2646/2646, leak tally 0). Closes #497 Closes #498 Closes #499 Closes #501 Closes #502 Closes #511 Closes #512 Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 24 ++++ src/builtins.c | 200 +++++++++++++++++++++-------- src/builtins_tensor.c | 39 +++++- src/hash.c | 40 ++++-- tests/run_all_tests.sh | 11 ++ tests/test_builtin_contracts.eigs | 21 ++- tests/test_builtin_indirect.eigs | 18 ++- tests/test_builtin_overflow.sh | 12 +- tests/test_coverage_v2.eigs | 10 +- tests/test_hash.eigs | 30 ++++- tests/test_raise_on_bad_input.eigs | 153 ++++++++++++++++++++++ 11 files changed, 477 insertions(+), 81 deletions(-) create mode 100644 tests/test_raise_on_bad_input.eigs diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c5578c..00f7a27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,30 @@ All notable changes to EigenScript are documented here. atomic. Still open under #488: a debug-mode *assertion* that a delimited region issued no scheduler yield (needs VM support). +### Changed +- **Silent-tolerance audit, batch 2 — invalid input raises instead of + returning a silent wrong value** (#497, #498, #499, #501, #502, #511, + #512). Builtins that used to fold bad input to a `null`/`0`/`""`/ + wrong-order sentinel — the hardest class of bug to spot in a data or + numeric pipeline — now raise a catchable error from the closed error-kind + set: + - `matmul` — incompatible shapes raise `value`, non-matrix operands raise + `type_mismatch`, an oversized result raises `limit` (#512). + - `sha256` / `md5` / `sha256_file` / `md5_file` / `hmac_sha256` — a + non-string argument raises `type_mismatch` (was the empty-string + sentinel, i.e. a silent wrong digest); the `_file` variants also raise + `io` when the file can't be opened (#511). + - `range` — a non-numeric argument raises `type_mismatch` (#497); a + request past the 1M cap raises `limit` instead of silently building an + unbounded list (the cap used to size only the prealloc while the loop + ran to the original bound — an OOM, #498). + - `sort_by` — a key function returning a non-number raises `type_mismatch` + (was coerced to `0.0`, a silent wrong order); mirrors `sort` (#368) (#501). + - `get_at` / `set_at` and `buf_get` / `buf_set` — an out-of-range index + raises `index_range` and a non-list/non-buffer target or non-integer + index raises `type_mismatch`, matching the `xs[i]` operator (was a silent + fold-to-0 / no-op) (#499, #502). + ### Fixed - **Circular `import` / `load_file` no longer crashes (#496).** A mutual or self-referential `import` (a→b→a) or `load_file` used to recurse through diff --git a/src/builtins.c b/src/builtins.c index b19eca8..0bdba12 100644 --- a/src/builtins.c +++ b/src/builtins.c @@ -3890,13 +3890,24 @@ Value* builtin_range(Value *arg) { end = (int)arg->data.num; } else if (arg->type == VAL_LIST) { int argc = arg->data.list.count; - if (argc >= 1 && arg->data.list.items[0]->type == VAL_NUM) + /* #497: every provided bound must be numeric. A non-number used to + * be silently treated as 0 (or fold the whole call to []) — a + * silent-wrong loop count. */ + for (int i = 0; i < argc && i < 3; i++) { + if (arg->data.list.items[i]->type != VAL_NUM) { + rt_error(EK_TYPE, 0, + "range: argument %d must be a number, got %s", i, + val_type_name(arg->data.list.items[i]->type)); + return make_list(0); + } + } + if (argc >= 1) start = (int)arg->data.list.items[0]->data.num; - if (argc >= 2 && arg->data.list.items[1]->type == VAL_NUM) + if (argc >= 2) end = (int)arg->data.list.items[1]->data.num; else { end = start; start = 0; } /* single-element list: treat as range of n */ - if (argc >= 3 && arg->data.list.items[2]->type == VAL_NUM) { + if (argc >= 3) { step = (int)arg->data.list.items[2]->data.num; /* The Euler-like update that feeds step can never produce * exactly zero — it trades the zero singularity for infinity. @@ -3906,6 +3917,11 @@ Value* builtin_range(Value *arg) { if (step == 0) step = 1; /* cppcheck-suppress zerodivcond */ } } else { + /* #497: a non-num, non-list argument (e.g. a string) is a type + * error, not a silent empty range. */ + rt_error(EK_TYPE, 0, + "range requires a number or a list of numbers, got %s", + val_type_name(arg->type)); return make_list(0); } @@ -3918,7 +3934,14 @@ Value* builtin_range(Value *arg) { count = (start - end - step - 1) / (-step); // cppcheck-suppress zerodivcond } if (count < 0) count = 0; - if (count > 1000000) count = 1000000; + /* #498: the 1M cap used to size only the prealloc — the loop below still + * ran to the original `end`, growing the list past the cap (unbounded + * memory, OOM). Raise loudly instead of silently building a giant list. */ + if (count > 1000000) { + rt_error(EK_LIMIT, 0, + "range: too many elements (%d, max 1000000)", count); + return make_list(0); + } /* #292: range builds `count` fresh number Values — charge the sandbox budget * so a range-in-a-loop can't aggregate past it. */ if (!sandbox_charge((size_t)count * (sizeof(Value) + sizeof(Value *)))) return make_list(0); @@ -3963,75 +3986,119 @@ Value* builtin_fill(Value *arg) { /* ==== BUILTIN: set_at — mutate a list element in place ==== */ /* set_at of [list, index, value] — sets list[index] = value, returns list */ /* set_at of [list, row, col, value] — sets list[row][col] = value for 2D */ +/* #499: out-of-range / non-list / non-integer index used to be a silent + * no-op (set_at) or a fold-to-0 (get_at) — inconsistent with the `xs[i]` + * operator, which raises index_range. These helpers raise the same kinds. */ +static int at_index(Value *idx_val, int count, const char *what, + int *out) { + if (!idx_val || idx_val->type != VAL_NUM) { + rt_error(EK_VALUE, 0, "%s index must be an integer", what); + return 0; + } + int idx = (int)idx_val->data.num; + if (idx < 0) idx += count; /* #312: negative counts from end */ + if (idx < 0 || idx >= count) { + rt_error(EK_INDEX, 0, "index %d out of range (list length %d)", + (int)idx_val->data.num, count); + return 0; + } + *out = idx; + return 1; +} + Value* builtin_set_at(Value *arg) { - if (!arg || arg->type != VAL_LIST) return make_null(); + if (!arg || arg->type != VAL_LIST) { + rt_error(EK_TYPE, 0, "set_at requires [list, index, value] or " + "[list, row, col, value]"); + return make_null(); + } int argc = arg->data.list.count; if (argc == 3) { /* 1D: set_at of [list, index, value] */ Value *list = arg->data.list.items[0]; - int idx = (arg->data.list.items[1]->type == VAL_NUM) ? (int)arg->data.list.items[1]->data.num : 0; Value *val = arg->data.list.items[2]; - /* #312: negative indices count from the end, matching []. */ - if (list->type == VAL_LIST && idx < 0) idx += list->data.list.count; - if (list->type == VAL_LIST && idx >= 0 && idx < list->data.list.count) { - val_incref(val); - val_decref(list->data.list.items[idx]); - list->data.list.items[idx] = val; + if (!list || list->type != VAL_LIST) { + rt_error(EK_TYPE, 0, "set_at: first argument must be a list"); + return make_null(); } + int idx; + if (!at_index(arg->data.list.items[1], list->data.list.count, "set_at", &idx)) + return make_null(); + val_incref(val); + val_decref(list->data.list.items[idx]); + list->data.list.items[idx] = val; return list; } if (argc == 4) { /* 2D: set_at of [list, row, col, value] */ Value *list = arg->data.list.items[0]; - int row = (arg->data.list.items[1]->type == VAL_NUM) ? (int)arg->data.list.items[1]->data.num : 0; - int col = (arg->data.list.items[2]->type == VAL_NUM) ? (int)arg->data.list.items[2]->data.num : 0; Value *val = arg->data.list.items[3]; - if (list->type == VAL_LIST && row < 0) row += list->data.list.count; - if (list->type == VAL_LIST && row >= 0 && row < list->data.list.count) { - Value *rowv = list->data.list.items[row]; - if (rowv->type == VAL_LIST && col < 0) col += rowv->data.list.count; - if (rowv->type == VAL_LIST && col >= 0 && col < rowv->data.list.count) { - val_incref(val); - val_decref(rowv->data.list.items[col]); - rowv->data.list.items[col] = val; - } + if (!list || list->type != VAL_LIST) { + rt_error(EK_TYPE, 0, "set_at: first argument must be a list"); + return make_null(); + } + int row; + if (!at_index(arg->data.list.items[1], list->data.list.count, "set_at row", &row)) + return make_null(); + Value *rowv = list->data.list.items[row]; + if (!rowv || rowv->type != VAL_LIST) { + rt_error(EK_TYPE, 0, "set_at: row %d is not a list", row); + return make_null(); } + int col; + if (!at_index(arg->data.list.items[2], rowv->data.list.count, "set_at col", &col)) + return make_null(); + val_incref(val); + val_decref(rowv->data.list.items[col]); + rowv->data.list.items[col] = val; return list; } + rt_error(EK_TYPE, 0, "set_at takes [list, index, value] or " + "[list, row, col, value]"); return make_null(); } /* ==== BUILTIN: get_at — read a list element ==== */ /* get_at of [list, index] or get_at of [list, row, col] */ Value* builtin_get_at(Value *arg) { - if (!arg || arg->type != VAL_LIST) return make_null(); + if (!arg || arg->type != VAL_LIST) { + rt_error(EK_TYPE, 0, "get_at requires [list, index] or [list, row, col]"); + return make_null(); + } int argc = arg->data.list.count; if (argc == 2) { Value *list = arg->data.list.items[0]; - int idx = (arg->data.list.items[1]->type == VAL_NUM) ? (int)arg->data.list.items[1]->data.num : 0; - /* #312: negative indices count from the end, matching []. */ - if (list->type == VAL_LIST && idx < 0) idx += list->data.list.count; - if (list->type == VAL_LIST && idx >= 0 && idx < list->data.list.count) { - val_incref(list->data.list.items[idx]); - return list->data.list.items[idx]; + if (!list || list->type != VAL_LIST) { + rt_error(EK_TYPE, 0, "get_at: first argument must be a list"); + return make_null(); } - return make_num(0.0); + int idx; + if (!at_index(arg->data.list.items[1], list->data.list.count, "get_at", &idx)) + return make_null(); + val_incref(list->data.list.items[idx]); + return list->data.list.items[idx]; } if (argc == 3) { Value *list = arg->data.list.items[0]; - int row = (arg->data.list.items[1]->type == VAL_NUM) ? (int)arg->data.list.items[1]->data.num : 0; - int col = (arg->data.list.items[2]->type == VAL_NUM) ? (int)arg->data.list.items[2]->data.num : 0; - if (list->type == VAL_LIST && row < 0) row += list->data.list.count; - if (list->type == VAL_LIST && row >= 0 && row < list->data.list.count) { - Value *rowv = list->data.list.items[row]; - if (rowv->type == VAL_LIST && col < 0) col += rowv->data.list.count; - if (rowv->type == VAL_LIST && col >= 0 && col < rowv->data.list.count) { - val_incref(rowv->data.list.items[col]); - return rowv->data.list.items[col]; - } + if (!list || list->type != VAL_LIST) { + rt_error(EK_TYPE, 0, "get_at: first argument must be a list"); + return make_null(); + } + int row; + if (!at_index(arg->data.list.items[1], list->data.list.count, "get_at row", &row)) + return make_null(); + Value *rowv = list->data.list.items[row]; + if (!rowv || rowv->type != VAL_LIST) { + rt_error(EK_TYPE, 0, "get_at: row %d is not a list", row); + return make_null(); } - return make_num(0.0); + int col; + if (!at_index(arg->data.list.items[2], rowv->data.list.count, "get_at col", &col)) + return make_null(); + val_incref(rowv->data.list.items[col]); + return rowv->data.list.items[col]; } + rt_error(EK_TYPE, 0, "get_at takes [list, index] or [list, row, col]"); return make_null(); } @@ -5113,22 +5180,44 @@ Value* builtin_reshape(Value *arg) { /* buf_get of [buf, index] — O(1) indexed read */ Value* builtin_buf_get(Value *arg) { - if (!arg || arg->type != VAL_LIST || arg->data.list.count < 2) return make_num(0); + /* #502: out-of-range used to fold to 0 — indistinguishable from a real + * stored 0. Raise index_range, matching the buffer `[i]` operator. */ + if (!arg || arg->type != VAL_LIST || arg->data.list.count < 2) { + rt_error(EK_TYPE, 0, "buf_get requires [buffer, index]"); + return make_num(0); + } Value *buf = arg->data.list.items[0]; + if (!buf || buf->type != VAL_BUFFER) { + rt_error(EK_TYPE, 0, "buf_get: first argument must be a buffer"); + return make_num(0); + } int idx = (int)arg->data.list.items[1]->data.num; - if (!buf || buf->type != VAL_BUFFER) return make_num(0); - if (idx < 0 || idx >= buf->data.buffer.count) return make_num(0); + if (idx < 0 || idx >= buf->data.buffer.count) { + rt_error(EK_INDEX, 0, "buffer index %d out of range (length %d)", + idx, buf->data.buffer.count); + return make_num(0); + } return make_num(buf->data.buffer.data[idx]); } /* buf_set of [buf, index, value] — O(1) indexed write */ Value* builtin_buf_set(Value *arg) { - if (!arg || arg->type != VAL_LIST || arg->data.list.count < 3) return make_null(); + if (!arg || arg->type != VAL_LIST || arg->data.list.count < 3) { /* #502 */ + rt_error(EK_TYPE, 0, "buf_set requires [buffer, index, value]"); + return make_null(); + } Value *buf = arg->data.list.items[0]; + if (!buf || buf->type != VAL_BUFFER) { + rt_error(EK_TYPE, 0, "buf_set: first argument must be a buffer"); + return make_null(); + } int idx = (int)arg->data.list.items[1]->data.num; double val = arg->data.list.items[2]->data.num; - if (!buf || buf->type != VAL_BUFFER) return make_null(); - if (idx < 0 || idx >= buf->data.buffer.count) return make_null(); + if (idx < 0 || idx >= buf->data.buffer.count) { + rt_error(EK_INDEX, 0, "buffer index %d out of range (length %d)", + idx, buf->data.buffer.count); + return make_null(); + } buf->data.buffer.data[idx] = val; return make_null(); } @@ -5374,9 +5463,20 @@ Value* builtin_sort_by(Value *arg) { if (!pairs) return make_null(); for (int i = 0; i < n; i++) { Value *kv = call_eigs_fn(key_fn, list->data.list.items[i]); - pairs[i].key = (kv && kv->type == VAL_NUM) ? kv->data.num : 0.0; + /* #501: a non-numeric key used to coerce to 0.0 — a silent wrong + * order. Raise instead (mirrors `sort` #368). */ + if (!kv || kv->type != VAL_NUM) { + const char *kt = kv ? val_type_name(kv->type) : "null"; + if (kv) val_decref(kv); + free(pairs); + rt_error(EK_TYPE, 0, + "sort_by: key function must return a number (element %d " + "gave %s)", i, kt); + return make_null(); + } + pairs[i].key = kv->data.num; pairs[i].index = i; - if (kv) val_decref(kv); + val_decref(kv); } qsort(pairs, n, sizeof(SortByPair), sort_by_pair_cmp); Value *result = make_list(n); diff --git a/src/builtins_tensor.c b/src/builtins_tensor.c index fb78d76..705fe90 100644 --- a/src/builtins_tensor.c +++ b/src/builtins_tensor.c @@ -339,17 +339,31 @@ Value* builtin_tensor_negative(Value *arg) { return tensor_unary(arg, op_neg); } /* ==== BUILTIN: matmul ==== */ Value* builtin_tensor_matmul(Value *arg) { - if (!arg || arg->type != VAL_LIST || arg->data.list.count < 2) return make_null(); + /* #512: invalid shapes/types raise instead of returning a silent null — + * a null in a numeric pipeline (training, games) is hard to spot. + * type_mismatch for non-matrix operands, value for incompatible shapes, + * limit for an oversized result. */ + if (!arg || arg->type != VAL_LIST || arg->data.list.count < 2) { + rt_error(EK_TYPE, 0, "matmul requires [A, B]"); + return make_null(); + } Value *a = arg->data.list.items[0]; Value *b = arg->data.list.items[1]; /* flat-buffer fast path: compute in place, no flatten/rebuild */ if (a->type == VAL_BUFFER && b->type == VAL_BUFFER) { int ar, ac, br, bc; buf_dims(a, &ar, &ac); buf_dims(b, &br, &bc); - if (ac != br) return make_null(); + if (ac != br) { + rt_error(EK_VALUE, 0, "matmul: incompatible shapes " + "(%dx%d · %dx%d)", ar, ac, br, bc); + return make_null(); + } /* Reject an oversized result before the kernel writes ar*bc doubles — * ar*bc overflowed int into a tiny allocation, then OOB heap writes. */ - if ((int64_t)ar * bc > 10000000) return make_null(); + if ((int64_t)ar * bc > 10000000) { + rt_error(EK_LIMIT, 0, "matmul: result too large (%dx%d)", ar, bc); + return make_null(); + } /* a 1-D left operand yields a 1-D result (mirrors flat_to_tensor_1d) */ Value *res = (a->data.buffer.rows == 0) ? make_shaped_buffer(0, bc) : make_shaped_buffer(ar, bc); @@ -359,8 +373,23 @@ Value* builtin_tensor_matmul(Value *arg) { int ar, ac, br, bc; double *af = tensor_to_flat(a, &ar, &ac); double *bf = tensor_to_flat(b, &br, &bc); - if (!af || !bf || ac != br) { free(af); free(bf); return make_null(); } - if ((int64_t)ar * bc > 10000000) { free(af); free(bf); return make_null(); } + if (!af || !bf) { + free(af); free(bf); + rt_error(EK_TYPE, 0, "matmul: expected matrices (got %s, %s)", + val_type_name(a->type), val_type_name(b->type)); + return make_null(); + } + if (ac != br) { + free(af); free(bf); + rt_error(EK_VALUE, 0, "matmul: incompatible shapes " + "(%dx%d · %dx%d)", ar, ac, br, bc); + return make_null(); + } + if ((int64_t)ar * bc > 10000000) { + free(af); free(bf); + rt_error(EK_LIMIT, 0, "matmul: result too large (%dx%d)", ar, bc); + return make_null(); + } double *out = xcalloc((size_t)ar * bc, sizeof(double)); ne_matmul_buf(af, ar, ac, bf, bc, out); Value *result; diff --git a/src/hash.c b/src/hash.c index 28cd08a..33a5b21 100644 --- a/src/hash.c +++ b/src/hash.c @@ -235,7 +235,12 @@ static void bytes_to_hex(const uint8_t *bytes, int len, char *hex, int hex_size) * ================================================================ */ Value* builtin_sha256(Value *arg) { - if (!arg || arg->type != VAL_STR) return make_str(""); + /* #511: a non-string input hashed to the empty-string sentinel — a + * silent wrong digest. Raise instead. */ + if (!arg || arg->type != VAL_STR) { + rt_error(EK_TYPE, 0, "sha256 requires a string"); + return make_str(""); + } SHA256_CTX ctx; sha256_init(&ctx); sha256_update(&ctx, (uint8_t*)arg->data.str, strlen(arg->data.str)); @@ -247,7 +252,10 @@ Value* builtin_sha256(Value *arg) { } Value* builtin_md5(Value *arg) { - if (!arg || arg->type != VAL_STR) return make_str(""); + if (!arg || arg->type != VAL_STR) { /* #511 */ + rt_error(EK_TYPE, 0, "md5 requires a string"); + return make_str(""); + } MD5_CTX ctx; md5_init(&ctx); md5_update(&ctx, (uint8_t*)arg->data.str, strlen(arg->data.str)); @@ -260,9 +268,15 @@ Value* builtin_md5(Value *arg) { #if !EIGENSCRIPT_FREESTANDING Value* builtin_sha256_file(Value *arg) { - if (!arg || arg->type != VAL_STR) return make_str(""); + if (!arg || arg->type != VAL_STR) { /* #511 */ + rt_error(EK_TYPE, 0, "sha256_file requires a string path"); + return make_str(""); + } FILE *f = fopen(arg->data.str, "rb"); - if (!f) return make_str(""); + if (!f) { + rt_error(EK_IO, 0, "sha256_file: cannot open '%s'", arg->data.str); + return make_str(""); + } SHA256_CTX ctx; sha256_init(&ctx); uint8_t buf[4096]; @@ -278,9 +292,15 @@ Value* builtin_sha256_file(Value *arg) { } Value* builtin_md5_file(Value *arg) { - if (!arg || arg->type != VAL_STR) return make_str(""); + if (!arg || arg->type != VAL_STR) { /* #511 */ + rt_error(EK_TYPE, 0, "md5_file requires a string path"); + return make_str(""); + } FILE *f = fopen(arg->data.str, "rb"); - if (!f) return make_str(""); + if (!f) { + rt_error(EK_IO, 0, "md5_file: cannot open '%s'", arg->data.str); + return make_str(""); + } MD5_CTX ctx; md5_init(&ctx); uint8_t buf[4096]; @@ -297,12 +317,16 @@ Value* builtin_md5_file(Value *arg) { #endif /* !EIGENSCRIPT_FREESTANDING */ Value* builtin_hmac_sha256(Value *arg) { - if (!arg || arg->type != VAL_LIST || arg->data.list.count < 2) + if (!arg || arg->type != VAL_LIST || arg->data.list.count < 2) { /* #511 */ + rt_error(EK_TYPE, 0, "hmac_sha256 requires [key, message]"); return make_str(""); + } Value *key_val = arg->data.list.items[0]; Value *msg_val = arg->data.list.items[1]; - if (!key_val || key_val->type != VAL_STR || !msg_val || msg_val->type != VAL_STR) + if (!key_val || key_val->type != VAL_STR || !msg_val || msg_val->type != VAL_STR) { + rt_error(EK_TYPE, 0, "hmac_sha256: key and message must be strings"); return make_str(""); + } const uint8_t *key = (const uint8_t*)key_val->data.str; size_t key_len = strlen(key_val->data.str); diff --git a/tests/run_all_tests.sh b/tests/run_all_tests.sh index 15dfd31..bdb8502 100755 --- a/tests/run_all_tests.sh +++ b/tests/run_all_tests.sh @@ -1020,6 +1020,17 @@ else fi rm -f "$CPG_FILE" +# [116] Silent-tolerance audit batch-2 (#497/#498/#499/#501/#502/#511/#512). +# Builtins that used to return a silent null/0/""/wrong-order on invalid +# input now raise a catchable error from the closed error-kind set: matmul +# (shape/type), sha256/md5/hmac (non-string), range (non-numeric + past the +# 1M cap), sort_by (non-numeric key), get_at/set_at + buf_get/buf_set (OOB +# → index_range, matching the [] operator). Each case asserts it raises (and +# the kind where it matters) plus a happy-path sanity check. +echo "[116] Silent-Tolerance Batch-2: bad input raises (7 issues)" +check_eigs_suite "invalid input raises instead of silent null/0/empty" \ + test_raise_on_bad_input.eigs "ALL_RAISE_TESTS_DONE" 21 + # [23] Named parameters echo "[23/27] Named Parameters (9 checks)" NP_OUTPUT=$(./eigenscript ../tests/test_named_params.eigs 2>&1); NP_OUTPUT_RC=$? diff --git a/tests/test_builtin_contracts.eigs b/tests/test_builtin_contracts.eigs index d29d842..3c93499 100644 --- a/tests/test_builtin_contracts.eigs +++ b/tests/test_builtin_contracts.eigs @@ -33,11 +33,26 @@ xs is [10, 20, 30] assert_eq of [get_at of [xs, -1], 30, "get_at -1 is the last element"] assert_eq of [get_at of [xs, -1], xs[-1], "get_at -1 matches the [] operator"] assert_eq of [get_at of [xs, -3], 10, "get_at -len is the first element"] -assert_eq of [get_at of [xs, -4], 0, "get_at past-the-start keeps the 0 miss"] +# #499: past-the-start now raises index_range, matching the [] operator +# (xs[-4] on a 3-list raises) — the whole point of #312 was "honor negative +# indices like []", and [] raises out of range rather than folding to 0. +ga_past is 0 +ga_past_k is "" +try: + ig is get_at of [xs, -4] +catch e: + ga_past is 1 + ga_past_k is e.kind +assert_true of [ga_past == 1 and ga_past_k == "index_range", "get_at past-the-start raises index_range"] set_at of [xs, -1, 99] assert_eq of [xs[2], 99, "set_at -1 writes the last element"] -set_at of [xs, -4, 111] -assert_eq of [xs[0], 10, "set_at past-the-start stays a no-op"] +sa_past is 0 +try: + set_at of [xs, -4, 111] +catch e: + sa_past is 1 +assert_true of [sa_past == 1, "set_at past-the-start raises (was a silent no-op)"] +assert_eq of [xs[0], 10, "set_at past-the-start left the list unchanged"] grid is [[1, 2], [3, 4]] assert_eq of [get_at of [grid, -1, -1], 4, "2D get_at negative row and col"] set_at of [grid, -1, -2, 77] diff --git a/tests/test_builtin_indirect.eigs b/tests/test_builtin_indirect.eigs index 08c12bb..134e498 100644 --- a/tests/test_builtin_indirect.eigs +++ b/tests/test_builtin_indirect.eigs @@ -75,8 +75,22 @@ b is buf_from_list of [1.5, 2.5, 3.5] check of ["buf_from_list type", (type of b) == "buffer"] check of ["buf_len direct", (buf_len of b) == 3] check of ["buf_get direct", (buf_get of [b, 1]) == 2.5] -check of ["buf_get OOR returns 0", (buf_get of [b, 99]) == 0] -check of ["buf_get neg returns 0", (buf_get of [b, 0 - 1]) == 0] +# #502: out-of-range buf_get raises index_range (was a silent fold-to-0, +# indistinguishable from a real stored 0). Mirrors the buffer [i] operator. +oor_raised is 0 +oor_kind is "" +try: + ignore is buf_get of [b, 99] +catch e: + oor_raised is 1 + oor_kind is e.kind +check of ["buf_get OOR raises index_range", oor_raised == 1 and oor_kind == "index_range"] +neg_raised is 0 +try: + ignore2 is buf_get of [b, 0 - 99] +catch e: + neg_raised is 1 +check of ["buf_get far-neg raises", neg_raised == 1] bfl is buf_from_list bget is buf_get diff --git a/tests/test_builtin_overflow.sh b/tests/test_builtin_overflow.sh index 95ccc11..a15d110 100755 --- a/tests/test_builtin_overflow.sh +++ b/tests/test_builtin_overflow.sh @@ -55,12 +55,18 @@ print of "survived"' "survived" # matmul: the result element count ar*bc overflowed int into a tiny allocation # while the kernel wrote ar*bc doubles (a 65536x1 by 1x65536 product). Now the # product is computed in 64-bit and rejected over the 10M cap — no heap overflow. -run_case "flat-buffer matmul result overflow → no OOB" \ +# #512: the over-cap case now RAISES `limit` (was a silent null); the +# crash-safety guarantee (no OOB/SIGSEGV) holds either way, so catch it. +run_case "flat-buffer matmul result overflow → catchable, no OOB" \ 'base is buffer of 65536 a is reshape of [base, 65536, 1] b is reshape of [base, 1, 65536] -c is matmul of [a, b] -print of "survived"' "survived" +caught is 0 +try: + c is matmul of [a, b] +catch e: + caught is 1 +print of caught' "1" # A normal matmul still produces the right answer (guards the cap refactor). run_case "matmul normal correctness" \ diff --git a/tests/test_coverage_v2.eigs b/tests/test_coverage_v2.eigs index 7d82d9c..7268ace 100644 --- a/tests/test_coverage_v2.eigs +++ b/tests/test_coverage_v2.eigs @@ -168,9 +168,8 @@ assert of [(len of r_single) == 5, "CV2-45 range single-elem list (paren form)"] r_zero_step is range of [0, 3, 0] assert of [(len of r_zero_step) == 3, "CV2-46 range zero step defaults to 1"] -# --- range on non-num/non-list --- -r_bad is range of "hello" -assert of [(len of r_bad) == 0, "CV2-47 range bad arg"] +# --- range on non-num/non-list (#497: raises, was a silent []) --- +expect_error of [() => range of "hello", "CV2-47 range bad arg raises"] # --- get_at / set_at 2D --- matrix is [[1, 2], [3, 4], [5, 6]] @@ -181,9 +180,8 @@ set_at of [matrix, 1, 1, 99] v2d2 is get_at of [matrix, 1, 1] assert of [v2d2 == 99, "CV2-49 set_at 2D"] -# --- get_at out of range --- -v_oor is get_at of [matrix, 10] -assert of [v_oor == 0, "CV2-50 get_at out of range"] +# --- get_at out of range (#499: raises index_range, was a silent 0) --- +expect_error of [() => get_at of [matrix, 10], "CV2-50 get_at out of range raises"] # --- coalesce --- c1 is coalesce of [null, "default"] diff --git a/tests/test_hash.eigs b/tests/test_hash.eigs index f21748c..f88538a 100644 --- a/tests/test_hash.eigs +++ b/tests/test_hash.eigs @@ -19,9 +19,31 @@ rm of "/tmp/test_hash.txt" # HMAC-SHA256 assert_eq of [hmac_sha256 of ["key", "hello"], "9307b3b915efb5171ff14d8cb55fbcc798c6c0ef1456d66ded1a6aa723a58b7b", "hmac-sha256"] -# Edge cases -assert_eq of [sha256 of null, "", "sha256 null returns empty"] -assert_eq of [md5 of null, "", "md5 null returns empty"] -assert_eq of [sha256_file of "/nonexistent", "", "sha256_file missing returns empty"] +# Edge cases — #511: non-string input and a missing file now RAISE (were the +# empty-string sentinel, i.e. a silent wrong/absent digest). +sha_null is 0 +sha_null_k is "" +try: + ig is sha256 of null +catch e: + sha_null is 1 + sha_null_k is e.kind +assert_true of [sha_null == 1 and sha_null_k == "type_mismatch", "sha256 of null raises type_mismatch"] + +md5_null is 0 +try: + ig is md5 of null +catch e: + md5_null is 1 +assert_true of [md5_null == 1, "md5 of null raises"] + +sfile_miss is 0 +sfile_miss_k is "" +try: + ig is sha256_file of "/nonexistent" +catch e: + sfile_miss is 1 + sfile_miss_k is e.kind +assert_true of [sfile_miss == 1 and sfile_miss_k == "io", "sha256_file missing raises io"] test_summary of null diff --git a/tests/test_raise_on_bad_input.eigs b/tests/test_raise_on_bad_input.eigs new file mode 100644 index 0000000..8dff37d --- /dev/null +++ b/tests/test_raise_on_bad_input.eigs @@ -0,0 +1,153 @@ +# test_raise_on_bad_input.eigs — batch-2 of the silent-tolerance audit. +# Builtins that used to return a silent null / 0 / "" / wrong-order on +# invalid input now RAISE a catchable error from the closed error-kind +# set. One block per issue: assert it raises (and the kind where it +# matters), plus a valid-path sanity check so we didn't break the happy path. +load_file of "lib/test.eigs" + +# ---- #512 matmul: shape / type errors raise (were silent null) ---- +mm_shape is 0 +mm_shape_k is "" +try: + ig is matmul of [[[1, 2]], [[1], [2], [3]]] +catch e: + mm_shape is 1 + mm_shape_k is e.kind +assert_true of [mm_shape == 1 and mm_shape_k == "value", "matmul shape mismatch raises value"] + +mm_type is 0 +mm_type_k is "" +try: + ig is matmul of [1, 2] +catch e: + mm_type is 1 + mm_type_k is e.kind +assert_true of [mm_type == 1 and mm_type_k == "type_mismatch", "matmul non-matrix raises type_mismatch"] + +mm_ok is matmul of [[[1, 2], [3, 4]], [[1, 0], [0, 1]]] +assert_true of [mm_ok[0][0] == 1 and mm_ok[1][1] == 4, "matmul identity still works"] + +# ---- #511 sha256 / md5 / hmac: non-string raises (was "") ---- +sha_bad is 0 +try: + ig is sha256 of 123 +catch e: + sha_bad is 1 +assert_true of [sha_bad == 1, "sha256 of non-string raises"] + +md5_bad is 0 +try: + ig is md5 of [1, 2] +catch e: + md5_bad is 1 +assert_true of [md5_bad == 1, "md5 of non-string raises"] + +hmac_bad is 0 +try: + ig is hmac_sha256 of [123, "msg"] +catch e: + hmac_bad is 1 +assert_true of [hmac_bad == 1, "hmac_sha256 non-string key raises"] + +assert_eq of [sha256 of "abc", "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", "sha256 of a string still works"] + +# ---- #497 range: non-numeric argument raises (was silent []) ---- +rng_str is 0 +rng_str_k is "" +try: + ig is range of "hello" +catch e: + rng_str is 1 + rng_str_k is e.kind +assert_true of [rng_str == 1 and rng_str_k == "type_mismatch", "range of a string raises type_mismatch"] + +rng_elem is 0 +try: + ig is range of [0, "x"] +catch e: + rng_elem is 1 +assert_true of [rng_elem == 1, "range with a non-numeric bound raises"] + +# ---- #498 range: past the 1M cap raises instead of OOM-building ---- +rng_big is 0 +rng_big_k is "" +try: + ig is range of [0, 5000000] +catch e: + rng_big is 1 + rng_big_k is e.kind +assert_true of [rng_big == 1 and rng_big_k == "limit", "range past 1M cap raises limit"] + +assert_eq of [len of (range of 3), 3, "range of n still works"] + +# ---- #501 sort_by: non-numeric key raises (was coerced to 0.0) ---- +define bad_key(x) as: + return "not a number" +define neg_key(x) as: + return 0 - x + +sb_bad is 0 +sb_bad_k is "" +try: + ig is sort_by of [[1, 2, 3], bad_key] +catch e: + sb_bad is 1 + sb_bad_k is e.kind +assert_true of [sb_bad == 1 and sb_bad_k == "type_mismatch", "sort_by non-numeric key raises type_mismatch"] + +sb_ok is sort_by of [[3, 1, 2], neg_key] +assert_true of [sb_ok[0] == 3 and sb_ok[2] == 1, "sort_by with a numeric key still works"] + +# ---- #499 get_at / set_at: OOB raises index_range, non-list raises ---- +xs is [10, 20, 30] +ga_oob is 0 +ga_oob_k is "" +try: + ig is get_at of [xs, 10] +catch e: + ga_oob is 1 + ga_oob_k is e.kind +assert_true of [ga_oob == 1 and ga_oob_k == "index_range", "get_at OOB raises index_range"] + +ga_type is 0 +try: + ig is get_at of [42, 0] +catch e: + ga_type is 1 +assert_true of [ga_type == 1, "get_at on a non-list raises"] + +sa_oob is 0 +sa_oob_k is "" +try: + set_at of [xs, 10, 99] +catch e: + sa_oob is 1 + sa_oob_k is e.kind +assert_true of [sa_oob == 1 and sa_oob_k == "index_range", "set_at OOB raises index_range (was a silent no-op)"] + +assert_eq of [get_at of [xs, -1], 30, "get_at negative index still counts from the end"] +set_at of [xs, 0, 99] +assert_eq of [get_at of [xs, 0], 99, "set_at still writes in range"] + +# ---- #502 buf_get / buf_set: OOB raises index_range ---- +b is buf_from_list of [1, 2, 3] +bg_oob is 0 +bg_oob_k is "" +try: + ig is buf_get of [b, 99] +catch e: + bg_oob is 1 + bg_oob_k is e.kind +assert_true of [bg_oob == 1 and bg_oob_k == "index_range", "buf_get OOB raises index_range"] + +bs_oob is 0 +try: + buf_set of [b, 99, 7] +catch e: + bs_oob is 1 +assert_true of [bs_oob == 1, "buf_set OOB raises"] + +assert_eq of [buf_get of [b, 1], 2, "buf_get in range still works"] + +print of "ALL_RAISE_TESTS_DONE" +test_summary of null