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
46 changes: 43 additions & 3 deletions inc/cvc/state/state_exec/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,24 @@ struct bytes_value {
bool operator==(const bytes_value &o) const { return data == o.data; }
};

namespace detail {
/// Drop one owner of a nested value's storage (a list, dict, closure, generator or
/// environment) without recursing once per nesting level. Freeing a 100000-deep list through
/// plain shared_ptr destructors takes a native stack frame per level and overflows the stack.
/// Here the first release on a thread runs a loop: owners dropped while it frees something
/// (that thing's elements, bindings, enclosing scope) are queued and freed one at a time by the
/// loop, so the stack stays a few frames deep whatever the depth.
void release_nested(std::shared_ptr<void> owner) noexcept;

/// Drop `owner` through release_nested when it is the last one; dropping any other owner only
/// decrements a count. (Should another thread drop the last but one meanwhile, this drop frees
/// the storage inline, and its elements start releases of their own: a few frames, still.)
template <class T> void drop_owner(std::shared_ptr<T> &owner) noexcept {
if (owner && owner.use_count() == 1)
release_nested(std::move(owner));
}
} // namespace detail

/// The DSL value variant. All runtime values are represented as value_t.
///
/// Variants:
Expand Down Expand Up @@ -114,6 +132,23 @@ struct value_tag {
value_tag(generator_ptr g) : v(std::move(g)) {}
value_tag(bytes_value b) : v(std::move(b)) {}

value_tag(const value_tag &) = default;
value_tag(value_tag &&) = default;
value_tag &operator=(const value_tag &) = default;
value_tag &operator=(value_tag &&) = default;

/// Nested storage is freed through detail::drop_owner, never recursively.
~value_tag() {
if (auto *l = std::get_if<list_ptr>(&v))
detail::drop_owner(*l);
else if (auto *d = std::get_if<dict_ptr>(&v))
detail::drop_owner(*d);
else if (auto *c = std::get_if<closure_ptr>(&v))
detail::drop_owner(*c);
else if (auto *g = std::get_if<generator_ptr>(&v))
detail::drop_owner(*g);
}

/// Check if the value is nil (monostate).
bool is_nil() const { return std::holds_alternative<std::monostate>(v); }

Expand Down Expand Up @@ -145,6 +180,9 @@ struct environment {
std::unordered_map<std::string, value_t> bindings;
std::shared_ptr<environment> outer;

/// Drops `outer` through detail::drop_owner, so a long scope chain is not freed recursively.
~environment();

/// Look up a name, walking the scope chain.
value_t *lookup(const std::string &name);
const value_t *lookup(const std::string &name) const;
Expand Down Expand Up @@ -189,13 +227,15 @@ inline const value_t true_value{true};
inline const value_t false_value{false};

/// Convert a value_t to a human-readable string representation. Bounded and cycle-safe:
/// output is length- and depth-capped (a shared/deep/cyclic structure is truncated with an
/// ellipsis rather than expanding exponentially or overflowing the stack).
/// output is length-capped (a shared/cyclic structure is truncated with an ellipsis rather
/// than expanding exponentially), and the walk is iterative, so depth alone neither truncates
/// it nor overflows the stack.
std::string to_string(const value_t &val);

/// Check structural equality of two values. Memoized on pointer-pairs, so a shared or
/// cyclic structure is compared in time proportional to its PHYSICAL size (distinct node
/// pairs), never its exponential logical unfolding.
/// pairs), never its exponential logical unfolding. Iterative, so any depth compares; it polls
/// the evaluation deadline and throws std::runtime_error once that is spent.
bool values_equal(const value_t &a, const value_t &b);

// -- time budgets -------------------------------------------------------------
Expand Down
86 changes: 43 additions & 43 deletions src/cvc/state/state_exec/intrinsics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -92,51 +92,51 @@ std::string coerce_state_string(const value_t &v) {
// not alias node-resident storage. Bounded and cycle-safe, mirroring values_equal / to_string
// (it is the third structural walker and must not be the weak link): it MEMOIZES on the source
// node pointer, so a shared-pointer DAG is copied in PHYSICAL time (the copy preserves the same
// sharing, disjoint from the stored original) and a cycle terminates; it caps recursion depth
// (stack safety on a deeply-nested value); and it polls the evaluation deadline so a huge
// payload aborts at the time budget instead of running to completion in one native step.
constexpr int kDeepCopyMaxDepth = 1000;

value_t deep_copy_impl(const value_t &v, std::unordered_map<const void *, value_t> &memo,
int depth) {
if (eval_deadline_expired())
throw std::runtime_error("state-data-get: evaluation exceeded time budget");
if (depth > kDeepCopyMaxDepth)
throw std::runtime_error("state-data-get: value nested too deeply to copy");
if (auto *l = std::get_if<list_ptr>(&v.v)) {
if (!*l)
return v;
auto found = memo.find(static_cast<const void *>(l->get()));
if (found != memo.end())
return found->second; // shared node / cycle already copied -> reuse (bounds the walk)
auto out = std::make_shared<std::vector<value_t>>();
value_t result{out};
memo.emplace(static_cast<const void *>(l->get()), result); // register BEFORE recursing
out->reserve((*l)->size());
for (const auto &e : **l)
out->push_back(deep_copy_impl(e, memo, depth + 1));
return result;
}
if (auto *dp = std::get_if<dict_ptr>(&v.v)) {
if (!*dp)
return v;
auto found = memo.find(static_cast<const void *>(dp->get()));
if (found != memo.end())
return found->second;
auto out = std::make_shared<std::vector<std::pair<std::string, value_t>>>();
value_t result{out};
memo.emplace(static_cast<const void *>(dp->get()), result);
out->reserve((*dp)->size());
for (const auto &kv : **dp)
out->emplace_back(kv.first, deep_copy_impl(kv.second, memo, depth + 1));
return result;
}
return v; // scalar / symbol (opaque callables are refused at state-data-set)
}

// sharing, disjoint from the stored original) and a cycle terminates; it keeps the elements
// still to copy on a heap stack, so any depth copies without native recursion; and it polls
// the evaluation deadline so a huge payload aborts at the time budget instead of running to
// completion in one native step.
value_t deep_copy(const value_t &v) {
std::unordered_map<const void *, value_t> memo;
return deep_copy_impl(v, memo, 0);
value_t result;
// (source, destination) pairs: each destination is a slot of a copy already allocated at its
// final size, so it stays put while the copy fills in.
std::vector<std::pair<const value_t *, value_t *>> pending{{&v, &result}};
while (!pending.empty()) {
if (eval_deadline_expired())
throw std::runtime_error("state-data-get: evaluation exceeded time budget");
auto [src, dst] = pending.back();
pending.pop_back();
if (auto *l = std::get_if<list_ptr>(&src->v); l && *l) {
auto found = memo.find(static_cast<const void *>(l->get()));
if (found != memo.end()) {
*dst = found->second; // shared node / cycle already copied -> reuse (bounds the walk)
continue;
}
auto out = std::make_shared<std::vector<value_t>>((*l)->size());
*dst = value_t{out};
memo.emplace(static_cast<const void *>(l->get()), *dst); // register BEFORE descending
for (std::size_t i = (*l)->size(); i-- > 0;)
pending.emplace_back(&(**l)[i], &(*out)[i]);
} else if (auto *dp = std::get_if<dict_ptr>(&src->v); dp && *dp) {
auto found = memo.find(static_cast<const void *>(dp->get()));
if (found != memo.end()) {
*dst = found->second;
continue;
}
auto out = std::make_shared<std::vector<std::pair<std::string, value_t>>>();
out->reserve((*dp)->size());
for (const auto &kv : **dp)
out->emplace_back(kv.first, value_t{});
*dst = value_t{out};
memo.emplace(static_cast<const void *>(dp->get()), *dst);
for (std::size_t i = (*dp)->size(); i-- > 0;)
pending.emplace_back(&(**dp)[i].second, &(*out)[i].second);
} else {
*dst = *src; // scalar / symbol / null (opaque callables are refused at state-data-set)
}
}
return result;
}

void require_root(const intrinsics_context *ctx, const char *name) {
Expand Down
35 changes: 20 additions & 15 deletions src/cvc/state/state_exec/stackless_evaluator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1081,22 +1081,27 @@ void stackless_evaluator::handle_defclass(evaluator_state &state, eval_frame &fr

value_t stackless_evaluator::substitute(const value_t &tmpl,
const std::unordered_map<std::string, value_t> &subst) {
if (auto *sym = std::get_if<symbol>(&tmpl.v)) {
auto it = subst.find(sym->name);
if (it != subst.end())
return it->second;
return tmpl;
}
if (auto *lp = std::get_if<list_ptr>(&tmpl.v)) {
if (!*lp)
return tmpl;
auto result = std::make_shared<std::vector<value_t>>();
result->reserve((*lp)->size());
for (auto &e : **lp)
result->push_back(substitute(e, subst));
return value_t{std::move(result)};
// Like the evaluator, the copy keeps its work on the heap, not the native stack, so a
// template of any depth expands. (source, destination) pairs: each destination is a slot of
// a list copy allocated at its final size, so it stays put while the copy fills in.
value_t result;
std::vector<std::pair<const value_t *, value_t *>> pending{{&tmpl, &result}};
while (!pending.empty()) {
auto [src, dst] = pending.back();
pending.pop_back();
if (auto *sym = std::get_if<symbol>(&src->v)) {
auto it = subst.find(sym->name);
*dst = it != subst.end() ? it->second : *src;
} else if (auto *lp = std::get_if<list_ptr>(&src->v); lp && *lp) {
auto copy = std::make_shared<std::vector<value_t>>((*lp)->size());
*dst = value_t{copy};
for (std::size_t i = (*lp)->size(); i-- > 0;)
pending.emplace_back(&(**lp)[i], &(*copy)[i]);
} else {
*dst = *src;
}
}
return tmpl;
return result;
}

} // namespace cvc::state_exec
Loading
Loading