diff --git a/Makefile b/Makefile index 93b1eb3a..048a6658 100644 --- a/Makefile +++ b/Makefile @@ -77,7 +77,7 @@ define AUX_REFRESH done endef -.PHONY: db-params-test all build server server-db full http net gfx zlib lib amalgamation tsan test test-changed precheck sandbox-intern-test install install-gfx clean coverage coverage-clean fuzz fuzz-run lsp lsp-asan dap jit-smoke embed-smoke embed-smoke-asan embed-smoke-asan-server embed-smoke-gfx embed-concurrent asan asan-server valgrind pgo poison freestanding-check freestanding-libc-diff asan-http asan-gfx tsan-server tsan-http nativefn-test arming-mt-test embed-roads print-% sigpipe-contract-test sigpipe-partial-test ui-sdl-input-gfx pool-off asan-pool-off pool-off-num pool-off-callenv +.PHONY: db-params-test all build slot-rehearsal server server-db full http net gfx zlib lib amalgamation tsan test test-changed precheck sandbox-intern-test install install-gfx clean coverage coverage-clean fuzz fuzz-run lsp lsp-asan dap jit-smoke embed-smoke embed-smoke-asan embed-smoke-asan-server embed-smoke-gfx embed-concurrent asan asan-server valgrind pgo poison freestanding-check freestanding-libc-diff asan-http asan-gfx tsan-server tsan-http nativefn-test arming-mt-test embed-roads print-% sigpipe-contract-test sigpipe-partial-test ui-sdl-input-gfx pool-off asan-pool-off pool-off-num pool-off-callenv # ---- Per-variant objdir engine (#740) ------------------------------------- # The engine's rules are defined before `all`, so pin the default goal. @@ -106,6 +106,10 @@ SRC_V_release := $(HOSTED_SOURCES) FLAGS_release := $(CFLAGS) $(DEFS_OFF) -DEIGENSCRIPT_EXT_GFX=1 $(VERDEF) LIBS_release := $(LDFLAGS) -ldl +SRC_V_slot-rehearsal := $(HOSTED_SOURCES) +FLAGS_slot-rehearsal := $(CFLAGS) $(DEFS_OFF) -DEIGENSCRIPT_EXT_GFX=1 -DEIGS_SLOT_REHEARSAL -DEIGS_BORROW_GUARD=1 -DEIGS_SLOT_TEST_HOOK $(VERDEF) +LIBS_slot-rehearsal := $(LDFLAGS) -ldl + SRC_V_server := $(SERVER_SOURCES) FLAGS_server := $(CFLAGS) -DEIGENSCRIPT_EXT_HTTP=1 -DEIGENSCRIPT_EXT_MODEL=1 -DEIGENSCRIPT_EXT_DB=0 -DEIGENSCRIPT_EXT_NET=1 -DEIGENSCRIPT_EXT_GFX=1 $(VERDEF) LIBS_server := $(LDFLAGS) -ldl @@ -175,7 +179,7 @@ FLAGS_pool-off-callenv := $(CFLAGS) $(DEFS_OFF) -DEIGENSCRIPT_EXT_GFX=1 -DEIGS_P LIBS_pool-off-callenv := $(LDFLAGS) -ldl -VARIANTS := release server server-db zlib asan asan-server tsan tsan-server valgrind poison pool-off asan-pool-off pool-off-num pool-off-callenv +VARIANTS := release slot-rehearsal server server-db zlib asan asan-server tsan tsan-server valgrind poison pool-off asan-pool-off pool-off-num pool-off-callenv # Objects depend on Makefile+VERSION so a flag or version-string change # rebuilds; header edits are covered by the generated .d files. @@ -215,6 +219,18 @@ build: build/release/eigenscript $(call RELINK,release) @echo "EigenScript $(VERSION) built. Binary: $$(du -sh build/release/eigenscript | cut -f1)" +slot-rehearsal: build/slot-rehearsal/eigenscript + $(call RELINK,slot-rehearsal) + @echo "EigenScript $(VERSION) slot rehearsal built. Binary: $$(du -sh build/slot-rehearsal/eigenscript | cut -f1)" + +SLOT_TEST_VARIANT ?= release +SLOT_TEST_OBJ := $(filter-out build/$(SLOT_TEST_VARIANT)/main.o,$(OBJ_$(SLOT_TEST_VARIANT))) +build/$(SLOT_TEST_VARIANT)/test_container_slots: tests/test_container_slots.c $(SLOT_TEST_OBJ) $(wildcard $(SRC_DIR)/*.h) Makefile tools/werror_flags.txt + $(CC) $(FLAGS_$(SLOT_TEST_VARIANT)) -I$(SRC_DIR) -o $@ $< $(SLOT_TEST_OBJ) $(LIBS_$(SLOT_TEST_VARIANT)) +.PHONY: container-slot-test +container-slot-test: build/$(SLOT_TEST_VARIANT)/test_container_slots + @echo "Container slot API test built: $<" + # Focused lifetime regression for sandbox descriptor interns (#964). Link the # public embedding/runtime surface without CLI-only translation units so the # C test can inspect the internal thread-local intern accounting directly. diff --git a/changes/internal/1665-slot-api.md b/changes/internal/1665-slot-api.md new file mode 100644 index 00000000..850a024a --- /dev/null +++ b/changes/internal/1665-slot-api.md @@ -0,0 +1 @@ +- Add the container slot-access API, rehearsal build, view escape guard, and migration gate proving ground. diff --git a/src/container.h b/src/container.h index f9c59d7e..beab8e34 100644 --- a/src/container.h +++ b/src/container.h @@ -1,68 +1,213 @@ #ifndef EIGENSCRIPT_CONTAINER_H #define EIGENSCRIPT_CONTAINER_H -/* Container element access boundary (#1665 arm D). - * - * Storage is still Value ** in step 1. Step 2 changes only these helpers to - * decode EigsSlot storage. A borrowed getter is valid until the next - * container mutation and must not be decref'd. For an immediate numeric slot - * step 2 will materialise into per-thread scratch Value storage; callers that - * retain a value must therefore use the owned getter. This avoids allocating - * on the overwhelmingly common inspect-and-discard path, while the owned API - * gives retaining callers a stable heap Value. - * - * *_set_owned consumes the caller's reference and writes it into a slot whose - * previous ownership has already been dealt with by the caller (or which is - * vacant). *_set_borrow adds the container's reference before writing. The - * setters intentionally do not release the displaced element: this preserves - * the existing low-level mutation contract and makes replacement ordering - * explicit at call sites. - */ -static inline Value *list_get_borrow(const Value *list, int i) { - return list->data.list.items[i]; +/* Container slot boundary (#1665 arm D). The representation is Value ** in + * B0; EIGS_SLOT_REHEARSAL deliberately presents boxed numbers as immediate + * slots so users of this API can be proved ready for the later storage flip. */ +#define EIGS_VIEW_RC (1 << 29) + +typedef struct { Value v; } EigsView; + +static inline EigsSlot eigs_container_slot(Value *v) { +#ifdef EIGS_SLOT_REHEARSAL + if (v && v->type == VAL_NUM) return slot_from_num(VAL_NUM_RAW(v)); +#endif + return slot_from_heap(v); +} + +/* A slot read is borrowed: it owns no reference and remains valid only until + * the container is mutated. In rehearsal a number is an immediate. */ +static inline EigsSlot list_slot(const Value *list, int i) { + return eigs_container_slot(list->data.list.items[i]); +} +static inline EigsSlot dict_value_slot(const Value *dict, int i) { + return eigs_container_slot(dict->data.dict.vals[i]); +} + +/* Return the logical type of a borrowed slot without allocating. */ +static inline ValType slot_type(EigsSlot s) { + if (slot_is_num(s)) return VAL_NUM; + if (slot_is_null(s)) return VAL_NULL; + if (slot_is_bool(s)) return VAL_BOOL; + return slot_as_ptr(s)->type; +} +static inline Value *eigs_slot_ref(EigsSlot s) { + if (slot_is_null(s)) return make_null(); + if (slot_is_bool(s)) return make_bool(slot_as_bool(s)); + return slot_as_ptr(s); +} +static inline ValType list_elem_type(const Value *list, int i) { + return slot_type(list_slot(list, i)); +} +static inline ValType dict_value_type(const Value *dict, int i) { + return slot_type(dict_value_slot(dict, i)); +} + +/* Return the real heap pointer (including the immortal null/bool singleton), + * or NULL when the element is numeric. */ +static inline Value *list_get_ref(const Value *list, int i) { + Value *v = list->data.list.items[i]; + return v && v->type == VAL_NUM ? NULL : v; +} +static inline Value *dict_value_get_ref(const Value *dict, int i) { + Value *v = dict->data.dict.vals[i]; + return v && v->type == VAL_NUM ? NULL : v; +} + +/* Numeric readers have the same strict/bool behavior as the Value readers: + * required raises for every non-number, element raises for bool or in strict + * mode, and optional treats null as absent. */ +static inline double eigs_slot_num(EigsSlot s, const char *who) { + if (slot_is_num(s)) return SLOT_NUM_RAW(s); + if (slot_is_heap(s) && slot_as_ptr(s)->type == VAL_NUM) + return eigs_num_arg(slot_as_ptr(s), who); + return eigs_num_arg_slow(eigs_slot_ref(s), who); +} +static inline double eigs_slot_elem_num(EigsSlot s, const char *who) { + if (slot_is_num(s)) return SLOT_NUM_RAW(s); + Value *v = eigs_slot_ref(s); + if (v && v->type == VAL_NUM) return eigs_num_arg(v, who); + if (g_strict || (v && v->type == VAL_BOOL)) return eigs_num_arg_slow(v, who); + return 0.0; +} +static inline int eigs_slot_opt_num(EigsSlot s, double *out, const char *who) { + if (slot_is_null(s) || (slot_is_heap(s) && slot_as_ptr(s)->type == VAL_NULL)) return 0; + if (slot_is_num(s)) { *out = SLOT_NUM_RAW(s); return 1; } + if (slot_is_heap(s) && slot_as_ptr(s)->type == VAL_NUM) { + *out = eigs_num_arg(slot_as_ptr(s), who); return 1; + } + (void)eigs_num_arg_slow(eigs_slot_ref(s), who); + return 0; +} + +/* A numeric view is a caller-owned, allocation-free Value facade. Reference + * slots return their real pointer. The cleanup guard catches retaining or + * decrefing a numeric facade: its sentinel refcount must be unchanged. */ +static inline Value *slot_view(EigsSlot s, EigsView *view) { + if (!slot_is_num(s) && slot_type(s) != VAL_NUM) return eigs_slot_ref(s); + view->v.type = VAL_NUM; + VAL_NUM_RAW(&view->v) = slot_is_num(s) ? SLOT_NUM_RAW(s) + : eigs_num_arg(slot_as_ptr(s), "slot_view"); + view->v.refcount = EIGS_VIEW_RC; + return &view->v; +} +static inline Value *list_get_view(const Value *list, int i, EigsView *view) { + return slot_view(list_slot(list, i), view); +} +static inline Value *dict_value_get_view(const Value *dict, int i, EigsView *view) { + return slot_view(dict_value_slot(dict, i), view); +} +static inline void eigs_view_check(EigsView *view) { + if (view->v.type == VAL_NUM && view->v.refcount != 0 && + view->v.refcount != EIGS_VIEW_RC) { + fprintf(stderr, "%s:%d: EigenScript FATAL: numeric view refcount moved (%d)\n", + __FILE__, __LINE__, view->v.refcount); + abort(); + } } +#if defined(__GNUC__) || defined(__clang__) +#define EIGS_VIEW(name) EigsView name __attribute__((cleanup(eigs_view_check))) = {{0}} +#else +#define EIGS_VIEW(name) EigsView name = {{0}} +#endif + +/* Owned/return reads make numeric storage independent. Reference values are + * increfed by the owned form; return preserves the builtin direct-child + * borrow protocol for references while numbers are always fresh. */ static inline Value *list_get_owned(const Value *list, int i) { - Value *v = list_get_borrow(list, i); + Value *v = list->data.list.items[i]; + if (v->type == VAL_NUM) return make_num(VAL_NUM_RAW(v)); val_incref(v); return v; } -static inline void list_set_owned(Value *list, int i, Value *v) { - list->data.list.items[i] = v; +static inline Value *list_get_return(const Value *list, int i) { + Value *v = list->data.list.items[i]; + return v->type == VAL_NUM ? make_num(VAL_NUM_RAW(v)) : v; } -static inline void list_set_borrow(Value *list, int i, Value *v) { +static inline Value *dict_value_get_owned(const Value *dict, int i) { + Value *v = dict->data.dict.vals[i]; + if (v->type == VAL_NUM) return make_num(VAL_NUM_RAW(v)); val_incref(v); - list_set_owned(list, i, v); + return v; } -static inline int list_get_num(const Value *list, int i, double *out) { - Value *v = list_get_borrow(list, i); - if (!v || v->type != VAL_NUM) return 0; - *out = VAL_NUM_RAW(v); - return 1; +static inline Value *dict_value_get_return(const Value *dict, int i) { + Value *v = dict->data.dict.vals[i]; + return v->type == VAL_NUM ? make_num(VAL_NUM_RAW(v)) : v; } -static inline int list_iter_count(const Value *list) { return list->data.list.count; } -static inline Value *list_iter_borrow(const Value *list, int i) { return list_get_borrow(list, i); } -static inline Value *dict_value_get_borrow(const Value *dict, int i) { - return dict->data.dict.vals[i]; +/* Low-level compatibility accessors retain their pre-flip meaning. */ +static inline Value *list_get_borrow(const Value *list, int i) { return list->data.list.items[i]; } +static inline Value *dict_value_get_borrow(const Value *dict, int i) { return dict->data.dict.vals[i]; } +static inline void list_set_owned(Value *list, int i, Value *v) { list->data.list.items[i] = v; } +static inline void dict_value_set_owned(Value *dict, int i, Value *v) { dict->data.dict.vals[i] = v; } +static inline void list_set_borrow(Value *list, int i, Value *v) { val_incref(v); list_set_owned(list, i, v); } +static inline void dict_value_set_borrow(Value *dict, int i, Value *v) { val_incref(v); dict_value_set_owned(dict, i, v); } + +/* Slot writes own exactly one container reference. Owned consumes the slot; + * append borrows it and therefore increfs. Every double path applies + * num_guard. Before the flip list_set_num retains the exclusive-box reuse. */ +static inline Value *eigs_slot_box(EigsSlot s) { + if (slot_is_num(s)) return make_num(num_guard(SLOT_NUM_RAW(s))); + Value *v = eigs_slot_ref(s); val_incref(v); return v; } -static inline Value *dict_value_get_owned(const Value *dict, int i) { - Value *v = dict_value_get_borrow(dict, i); - val_incref(v); - return v; +static inline void list_set_slot_owned(Value *list, int i, EigsSlot s) { + Value *old = list->data.list.items[i]; + Value *v = slot_is_num(s) ? make_num(num_guard(SLOT_NUM_RAW(s))) : eigs_slot_ref(s); + list->data.list.items[i] = v; + val_decref(old); } -static inline void dict_value_set_owned(Value *dict, int i, Value *v) { +static inline void dict_value_set_slot_owned(Value *dict, int i, EigsSlot s) { + Value *old = dict->data.dict.vals[i]; + Value *v = slot_is_num(s) ? make_num(num_guard(SLOT_NUM_RAW(s))) : eigs_slot_ref(s); dict->data.dict.vals[i] = v; + val_decref(old); } -static inline void dict_value_set_borrow(Value *dict, int i, Value *v) { - val_incref(v); - dict_value_set_owned(dict, i, v); +static inline void list_append_slot(Value *list, EigsSlot s) { + Value *v = eigs_slot_box(s); list_append_owned(list, v); +} +static inline void list_set_num(Value *list, int i, double d) { + d = num_guard(d); +#ifndef EIGS_SLOT_REHEARSAL + Value *old = list->data.list.items[i]; + if (old && old->type == VAL_NUM && old->refcount == 1) { VAL_NUM_RAW(old) = d; return; } +#endif + list_set_slot_owned(list, i, slot_from_num(d)); +} +static inline void list_append_num(Value *list, double d) { list_append_owned(list, make_num(num_guard(d))); } +static inline void dict_value_set_num(Value *dict, int i, double d) { + d = num_guard(d); +#ifndef EIGS_SLOT_REHEARSAL + Value *old = dict->data.dict.vals[i]; + if (old && old->type == VAL_NUM && old->refcount == 1) { VAL_NUM_RAW(old) = d; return; } +#endif + dict_value_set_slot_owned(dict, i, slot_from_num(d)); +} +static inline EigsSlot list_take_slot(Value *list, int i) { + Value *v = list->data.list.items[i]; + EigsSlot s = eigs_container_slot(v); + if (slot_is_num(s)) val_decref(v); + memmove(&list->data.list.items[i], &list->data.list.items[i + 1], + (size_t)(list->data.list.count - i - 1) * sizeof(Value *)); + list->data.list.count--; + return s; } -static inline int dict_value_get_num(const Value *dict, int i, double *out) { - Value *v = dict_value_get_borrow(dict, i); - if (!v || v->type != VAL_NUM) return 0; - *out = VAL_NUM_RAW(v); - return 1; +static inline EigsSlot dict_value_take_slot(Value *dict, int i) { + Value *v = dict->data.dict.vals[i]; + EigsSlot s = eigs_container_slot(v); + if (slot_is_num(s)) val_decref(v); + return s; } +static inline void list_move(Value *list, int dst, int src, int n) { + memmove(&list->data.list.items[dst], &list->data.list.items[src], (size_t)n * sizeof(Value *)); +} +static inline void list_sort(Value *list, int (*cmp)(const void *, const void *)) { + qsort(list->data.list.items, (size_t)list->data.list.count, sizeof(Value *), cmp); +} + +static inline int list_get_num(const Value *list, int i, double *out) { Value *v=list_get_borrow(list,i); if(!v||v->type!=VAL_NUM)return 0; *out=VAL_NUM_RAW(v); return 1; } +static inline int dict_value_get_num(const Value *dict, int i, double *out) { Value *v=dict_value_get_borrow(dict,i); if(!v||v->type!=VAL_NUM)return 0; *out=VAL_NUM_RAW(v); return 1; } +static inline int list_iter_count(const Value *list) { return list->data.list.count; } +static inline Value *list_iter_borrow(const Value *list, int i) { return list_get_borrow(list, i); } static inline int dict_value_iter_count(const Value *dict) { return dict->data.dict.count; } static inline Value *dict_value_iter_borrow(const Value *dict, int i) { return dict_value_get_borrow(dict, i); } diff --git a/src/vm.c b/src/vm.c index 7e49679f..de6dc4bd 100644 --- a/src/vm.c +++ b/src/vm.c @@ -2422,6 +2422,12 @@ static const char* vm_builtin_name_for(Env *env, BuiltinFn fn) { static inline void vm_borrow_scan(Value *arg, Value *result, Value *fn_val, Env *env) { (void)fn_val; (void)env; +#if EIGS_BORROW_GUARD || defined(EIGS_SLOT_REHEARSAL) + if (result && result->refcount >= EIGS_VIEW_RC) { + fprintf(stderr, "EigenScript FATAL: builtin returned an escaped numeric view\n"); + abort(); + } +#endif if (arg && arg->type == VAL_LIST) { int n = arg->data.list.count; if (n > VM_BORROW_SCAN_CAP) n = VM_BORROW_SCAN_CAP; @@ -2496,6 +2502,16 @@ void vm_borrow_compensate(Value *arg, Value *result, int caller_owns_arg, vm_borrow_scan(arg, result, fn_val, env); } +#ifdef EIGS_SLOT_TEST_HOOK +/* Test-only planted builtin result. It is deliberately absent from the + * registry and from production builds: the harness sends it through the real + * builtin-return compensation path to prove the escape guard aborts. */ +Value *eigs_test_builtin_number_view(void) { + static EigsView escaped; + return slot_view(slot_from_num(1665.0), &escaped); +} +#endif + /* JIT Stage 4r/5f: out-of-line helper for OP_CALL. * * Mirrors the VAL_BUILTIN branch of CASE(CALL), plus (Stage 5f) a diff --git a/src/vm.h b/src/vm.h index c87d2126..309ddf86 100644 --- a/src/vm.h +++ b/src/vm.h @@ -51,6 +51,9 @@ typedef struct ASTNode ASTNode; * definition in vm.c. */ void vm_borrow_compensate(Value *arg, Value *result, int caller_owns_arg, Value *fn_val, Env *env); +#ifdef EIGS_SLOT_TEST_HOOK +Value *eigs_test_builtin_number_view(void); +#endif /* ---- Opcodes ---- */ diff --git a/tests/sections/0ff-container-slots-1665.sh b/tests/sections/0ff-container-slots-1665.sh new file mode 100644 index 00000000..3856c998 --- /dev/null +++ b/tests/sections/0ff-container-slots-1665.sh @@ -0,0 +1,28 @@ +echo "[0ff] Container slot API (#1665 B0)" +TOTAL=$((TOTAL + 1)) +variant=release +if [ -e ../build/slot-rehearsal/eigenscript ] && [ ./eigenscript -ef ../build/slot-rehearsal/eigenscript ]; then + variant=slot-rehearsal +elif [ -e ../build/asan/eigenscript ] && [ ./eigenscript -ef ../build/asan/eigenscript ]; then + variant=asan +fi +if make -s -C .. SLOT_TEST_VARIANT="$variant" container-slot-test && \ + ../build/$variant/test_container_slots; then + if [ "$variant" != slot-rehearsal ]; then + PASS=$((PASS + 1)) + else + set +e + ../build/$variant/test_container_slots --escape-view >/tmp/eigs-slot-escape.$$ 2>&1 + rc=$? + set -e + if [ "$rc" -gt 128 ] && grep -q 'escaped numeric view' /tmp/eigs-slot-escape.$$; then + PASS=$((PASS + 1)) + else + FAIL=$((FAIL + 1)); echo " FAIL: escaped view plant rc=$rc"; cat /tmp/eigs-slot-escape.$$ + fi + rm -f /tmp/eigs-slot-escape.$$ + fi +else + FAIL=$((FAIL + 1)); echo " FAIL: container slot API" +fi +echo "" diff --git a/tests/sections/99ze-container-access.sh b/tests/sections/99ze-container-access.sh index 9e107e7e..72fe270c 100644 --- a/tests/sections/99ze-container-access.sh +++ b/tests/sections/99ze-container-access.sh @@ -3,11 +3,30 @@ echo "[99ze] Container accessor boundary (#1665)" TOTAL=$((TOTAL + 1)) CA_OUTPUT=$(bash "$TESTS_DIR/../tools/container_access_check.sh" 2>&1); CA_RC=$? if [ "$CA_RC" -eq 0 ] && \ - grep -qE '^container-access: examined=[1-9][0-9]* allowed=[0-9]+ violations=0$' <<< "$CA_OUTPUT"; then + grep -qE '^container-access: examined=[1-9][0-9]* allowed=[0-9]+ violations=0$' <<< "$CA_OUTPUT" && \ + grep -qE '^container-migrated: files=[0-9]+ examined=[0-9]+ violations=0$' <<< "$CA_OUTPUT"; then PASS=$((PASS + 1)) printf '%s\n' "$CA_OUTPUT" | grep '^container-access:' + printf '%s\n' "$CA_OUTPUT" | grep '^container-migrated:' else FAIL=$((FAIL + 1)) echo " FAIL: container accessor boundary (rc=$CA_RC); output follows" printf '%s\n' "$CA_OUTPUT" | sed 's/^/ /' fi + +# Planted fault: enrolling a forbidden legacy borrow must turn the gate red. +TOTAL=$((TOTAL + 1)) +plant_dir=$(mktemp -d "${TESTS_DIR}/../.container-plant.XXXXXX") +printf 'void plant(void) { list_get_borrow(0, 0); }\n' > "$plant_dir/plant.c" +printf '%s\n' "${plant_dir#"$TESTS_DIR/../"}/plant.c" > "$plant_dir/list.txt" +set +e +PLANT_OUTPUT=$(cd "$TESTS_DIR/.." && EIGS_CONTAINER_EXPECT_VIOLATION=1 EIGS_CONTAINER_MIGRATED_LIST="${plant_dir#"$TESTS_DIR/../"}/list.txt" bash tools/container_access_check.sh 2>&1) +PLANT_RC=$? +set -e +rm -rf "$plant_dir" +if [ "$PLANT_RC" -eq 0 ] && grep -qE '^container-migrated: files=1 examined=1 violations=1$' <<< "$PLANT_OUTPUT" && \ + grep -q '^container-migrated-plant: PASS$' <<< "$PLANT_OUTPUT"; then + PASS=$((PASS + 1)) +else + FAIL=$((FAIL + 1)); echo " FAIL: migrated-access plant rc=$PLANT_RC"; printf '%s\n' "$PLANT_OUTPUT" | tail -20 +fi diff --git a/tests/test_container_slots.c b/tests/test_container_slots.c new file mode 100644 index 00000000..6c2db91e --- /dev/null +++ b/tests/test_container_slots.c @@ -0,0 +1,100 @@ +/* #1665 B0: direct proving ground for the Value** slot boundary. */ +#include +#include +#include +#include "eigs_embed.h" +#include "eigenscript.h" +#include "vm.h" + +static int failed; +#define CHECK(x) do { if (!(x)) { fprintf(stderr, "FAIL:%d: %s\n", __LINE__, #x); failed = 1; } } while (0) + +static int num_cmp(const void *a, const void *b) { + const Value *va = *(Value *const *)a, *vb = *(Value *const *)b; + double da = eigs_num_arg(va, __func__), db = eigs_num_arg(vb, __func__); + return (da > db) - (da < db); +} + +int main(int argc, char **argv) { + EigsState *state = eigs_open(); + CHECK(state != NULL); +#ifdef EIGS_SLOT_TEST_HOOK + if (argc == 2 && strcmp(argv[1], "--escape-view") == 0) { + Value *arg = make_list(1); + list_append_num(arg, 1.0); + vm_borrow_compensate(arg, eigs_test_builtin_number_view(), 1, NULL, NULL); + return 99; + } +#else + (void)argc; (void)argv; +#endif + Value *list = make_list(8); + Value *nested = make_list(1); + Value *str = make_str("slot"); + list_append_num(list, 3.0); + list_append_slot(list, eigs_container_slot(str)); + list_append_slot(list, eigs_container_slot(nested)); + list_append_slot(list, eigs_container_slot(make_null())); + list_append_slot(list, eigs_container_slot(make_bool(1))); + val_decref(str); val_decref(nested); + + CHECK(slot_type(list_slot(list, 0)) == VAL_NUM); + CHECK(list_elem_type(list, 1) == VAL_STR); + CHECK(list_get_ref(list, 0) == NULL && list_get_ref(list, 1) != NULL); + CHECK(eigs_slot_num(list_slot(list, 0), "test") == 3.0); + CHECK(eigs_slot_elem_num(list_slot(list, 0), "test") == 3.0); + double opt = 7.0; + CHECK(!eigs_slot_opt_num(list_slot(list, 3), &opt, "test") && opt == 7.0); + CHECK(eigs_slot_opt_num(list_slot(list, 0), &opt, "test") && opt == 3.0); + { + EIGS_VIEW(a); EIGS_VIEW(b); + Value *av = list_get_view(list, 0, &a); + list_append_num(list, 9.0); + Value *bv = list_get_view(list, 5, &b); + CHECK(av != bv && eigs_num_arg(av, "test") == 3.0 && eigs_num_arg(bv, "test") == 9.0); + CHECK(list_get_view(list, 1, &a) == list_get_ref(list, 1)); + } + Value *owned = list_get_owned(list, 0); + Value *returned = list_get_return(list, 0); + CHECK(owned != list_get_borrow(list, 0) && returned != list_get_borrow(list, 0)); + val_decref(owned); val_decref(returned); + list_set_num(list, 0, 4.0); + list_set_slot_owned(list, 1, slot_from_heap(make_str("owned"))); + CHECK(eigs_slot_num(list_slot(list, 0), "test") == 4.0); + EigsSlot taken = list_take_slot(list, 0); + CHECK(eigs_slot_num(taken, "test") == 4.0); + slot_decref(taken); + list_move(list, 0, 0, 1); + + Value *sort = make_list(3); + list_append_num(sort, 2); list_append_num(sort, 1); list_append_num(sort, 3); + list_sort(sort, num_cmp); + CHECK(eigs_slot_num(list_slot(sort, 0), "test") == 1.0); + + Value *dict = make_dict(4); + dict_set_owned(dict, "n", make_num(2)); + dict_set_owned(dict, "s", make_str("dict")); + dict_set_owned(dict, "l", make_list(1)); + dict_set_owned(dict, "z", make_null()); + dict_set_owned(dict, "b", make_bool(0)); + int ni = env_hash_find_dict(dict, "n", env_hash_name("n")); + int si = env_hash_find_dict(dict, "s", env_hash_name("s")); + CHECK(dict_value_type(dict, ni) == VAL_NUM && dict_value_get_ref(dict, ni) == NULL); + CHECK(eigs_slot_num(dict_value_slot(dict, ni), "test") == 2.0); + { EIGS_VIEW(v); CHECK(eigs_num_arg(dict_value_get_view(dict, ni, &v), "test") == 2.0); } + owned = dict_value_get_owned(dict, ni); returned = dict_value_get_return(dict, ni); + CHECK(owned != dict_value_get_borrow(dict, ni) && returned != dict_value_get_borrow(dict, ni)); + val_decref(owned); val_decref(returned); + dict_value_set_num(dict, ni, 8.0); + dict_value_set_slot_owned(dict, si, slot_from_heap(make_str("changed"))); + CHECK(eigs_slot_num(dict_value_slot(dict, ni), "test") == 8.0); + taken = dict_value_take_slot(dict, ni); + CHECK(eigs_slot_num(taken, "test") == 8.0); + slot_decref(taken); + dict_value_set_owned(dict, ni, make_num(8.0)); + + val_decref(dict); val_decref(sort); val_decref(list); + eigs_close(state); + puts("container slots: PASS"); + return failed; +} diff --git a/tools/container_access_check.sh b/tools/container_access_check.sh index d029d852..74853f39 100755 --- a/tools/container_access_check.sh +++ b/tools/container_access_check.sh @@ -37,3 +37,32 @@ if (( violations )); then exit 1 fi (( examined > 0 )) || { echo 'container-access: scan found nothing (vacuous)' >&2; exit 1; } + +# Files declared migrated may use only the slot API. Count forbidden calls +# independently so an empty B0 list is an explicit, non-vacuous population. +migrated_files=0 +migrated_hits=0 +migrated_violations=0 +while IFS= read -r file; do + case "$file" in ''|'#'*) continue ;; esac + migrated_files=$((migrated_files + 1)) + if [[ ! -f $file ]]; then + echo "container-migrated: missing listed file: $file" >&2 + migrated_violations=$((migrated_violations + 1)) + continue + fi + while IFS= read -r hit; do + [[ -z $hit ]] && continue + migrated_hits=$((migrated_hits + 1)) + echo "forbidden migrated container access: $file:$hit" >&2 + migrated_violations=$((migrated_violations + 1)) + done < <(grep -n -E 'list_get_borrow[[:space:]]*[(]|dict_value_get_borrow[[:space:]]*[(]|(^|[^[:alnum:]_])dict_get(_hashed|_cached)?[[:space:]]*[(]|list_values_storage[[:space:]]*[(]' "$file" || true) +done < "${EIGS_CONTAINER_MIGRATED_LIST:-tools/container_migrated_files.txt}" +echo "container-migrated: files=$migrated_files examined=$migrated_hits violations=$migrated_violations" +if (( migrated_violations != 0 )); then + if [[ ${EIGS_CONTAINER_EXPECT_VIOLATION:-0} == 1 ]]; then + echo "container-migrated-plant: PASS" + else + exit 1 + fi +fi diff --git a/tools/container_migrated_files.txt b/tools/container_migrated_files.txt new file mode 100644 index 00000000..a6420e2a --- /dev/null +++ b/tools/container_migrated_files.txt @@ -0,0 +1 @@ +# B0 has no migrated production call sites. Add one src-relative path per line. diff --git a/tools/num_read_allowlist.txt b/tools/num_read_allowlist.txt index 3687a955..6e2a786b 100644 --- a/tools/num_read_allowlist.txt +++ b/tools/num_read_allowlist.txt @@ -8,7 +8,21 @@ builtins_tensor.c|builtin_numerical_grad_rows|3|in-place writes of a matrix cell that tensor_cells_numeric proved a number before any mutation builtins_tensor.c|builtin_tensor_matmul|1|writes the NaN-boxed null sentinel into a result buffer cell (EIGS_STRICT=0 path); a write, no Value is read container.h|dict_value_get_num|1|accessor numeric fast path: reads the number only behind its own type == VAL_NUM test (#1665 arm D) +container.h|dict_value_get_owned|1|owned numeric read copies a proven VAL_NUM box for flip-safe ownership +container.h|dict_value_get_return|1|builtin-return numeric read copies a proven VAL_NUM box +container.h|dict_value_set_num|1|exclusive VAL_NUM box reuse writes only after type and refcount checks +container.h|dict_value_set_slot_owned|1|slot write boxes an immediate only after slot_is_num +container.h|eigs_container_slot|1|rehearsal decoder reads a box only after type == VAL_NUM +container.h|eigs_slot_box|1|borrowed slot write boxes a double only after slot_is_num +container.h|eigs_slot_elem_num|1|element numeric accessor reads only after slot_is_num +container.h|eigs_slot_num|1|required numeric accessor reads only after slot_is_num +container.h|eigs_slot_opt_num|1|optional numeric accessor reads only after slot_is_num container.h|list_get_num|1|accessor numeric fast path: reads the number only behind its own type == VAL_NUM test (#1665 arm D) +container.h|list_get_owned|1|owned numeric read copies a proven VAL_NUM box for flip-safe ownership +container.h|list_get_return|1|builtin-return numeric read copies a proven VAL_NUM box +container.h|list_set_num|1|exclusive VAL_NUM box reuse writes only after type and refcount checks +container.h|list_set_slot_owned|1|slot write boxes an immediate only after slot_is_num +container.h|slot_view|2|numeric view copies a proven immediate into caller storage chunk.c|chunk_disassemble|1|debug disassembly prints a constant behind its own type == VAL_NUM test chunk.c|const_equal|2|constant-pool dedup compares two constants after testing both are VAL_NUM chunk.c|const_hash_value|1|constant-pool hash of a constant inside its type == VAL_NUM branch diff --git a/tools/strict_shape_contracts.json b/tools/strict_shape_contracts.json index 00baedbd..c144f042 100644 --- a/tools/strict_shape_contracts.json +++ b/tools/strict_shape_contracts.json @@ -1464,7 +1464,7 @@ "cab31d774010bcc6b59ae3d4de9f380592fae037878071700a560932bb4f80d3" ], "vm_borrow_scan": [ - "5a91678f79479ec4c73cd2fa648e41eb4d23cacc237689800cb04284235985b2" + "f3944b1ed1b44bb9d331580dac563f62c5964d3fba2d039b36d9c1dab22cadb2" ] } },