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
20 changes: 18 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions changes/internal/1665-slot-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Add the container slot-access API, rehearsal build, view escape guard, and migration gate proving ground.
237 changes: 191 additions & 46 deletions src/container.h
Original file line number Diff line number Diff line change
@@ -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;
Comment on lines +194 to +198

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear the dictionary entry when taking its slot

dict_value_take_slot returns the dictionary's only owned reference without removing or replacing the entry. If a caller follows the ownership implied by take and calls slot_decref鈥攁s the new test does鈥攖he dictionary retains a dangling pointer and later access or destruction can use or free it again; under slot rehearsal, numeric entries are freed inside this function and become dangling immediately. Replace the entry with a safe null/empty value before transferring the slot.

Useful? React with 馃憤聽/ 馃憥.

}
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);
Comment on lines +203 to +204

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pass logical slots to list-sort comparators

list_sort exposes the underlying storage cells directly to its comparator, so the new rehearsal test receives Value ** cells and stays green even though the post-flip array will contain EigsSlot values. A comparator written for today's Value ** representation will dereference tagged slot bits after the flip, while a slot-aware comparator cannot run correctly in rehearsal, meaning sort call sites cannot be migrated safely before the representation change. Define the callback in terms of logical EigsSlot values and adapt the current boxed storage inside this helper.

Useful? React with 馃憤聽/ 馃憥.

}

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); }

Expand Down
16 changes: 16 additions & 0 deletions src/vm.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions src/vm.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 ---- */
Expand Down
28 changes: 28 additions & 0 deletions tests/sections/0ff-container-slots-1665.sh
Original file line number Diff line number Diff line change
@@ -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
Comment on lines +3 to +7

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Build the slot fixture with the active sanitizer variant

When the suite runs against any variant other than exactly asan or slot-rehearsal, this falls back to release; in particular, the checked ASan workflow builds asan-server and then runs run_all_tests.sh, so this section silently compiles and executes an uninstrumented release fixture. Consequently the new ownership and lifetime API receives no sanitizer coverage in the repository's actual ASan suite. Derive the hard-linked active variant (including asan-server and asan-pool-off) instead of defaulting those runs to release.

Useful? React with 馃憤聽/ 馃憥.

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 ""
Loading
Loading