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
7 changes: 4 additions & 3 deletions bindings/module.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,10 @@ oscadeval::Value pyToValue(nb::handle v) {
if (nb::isinstance<nb::int_>(v) || nb::isinstance<nb::float_>(v)) return oscadeval::Value{nb::cast<double>(v)};
if (nb::isinstance<nb::str>(v)) return oscadeval::Value{nb::cast<std::string>(v)};
if (nb::isinstance<nb::list>(v) || nb::isinstance<nb::tuple>(v)) {
oscadeval::ValueList vl;
for (nb::handle item : nb::borrow<nb::sequence>(v)) vl.items.push_back(pyToValue(item));
return oscadeval::Value{oscadeval::ListPtr(std::make_shared<const oscadeval::ValueList>(std::move(vl)))};
std::vector<oscadeval::Value> items;
for (nb::handle item : nb::borrow<nb::sequence>(v)) items.push_back(pyToValue(item));
return oscadeval::Value{oscadeval::ListPtr(
std::make_shared<const oscadeval::ValueList>(oscadeval::ValueList{std::move(items)}))};
}
return oscadeval::Value{}; // monostate = undef
}
Expand Down
67 changes: 63 additions & 4 deletions include/openscad_cpp_evaluator/value.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

#include "openscad_cpp_evaluator/osc_range.hpp"

#include <atomic>
#include <functional>
#include <initializer_list>
#include <memory>
#include <optional>
#include <string>
Expand Down Expand Up @@ -98,13 +100,70 @@ inline std::shared_ptr<TrailView<Value>> capturedLetTrail(const Closure& c) {
return std::static_pointer_cast<TrailView<Value>>(c.capturedLet);
}

// Defined after Value so both can hold Value by value -- the standard
// recursive-variant pattern (indirection through a forward-declared,
// heap-allocated aggregate).
// A list's elements: a read-only view of the first `size()` elements of a
// buffer that other lists may share. Lists that are prefixes of one another
// share one buffer, which is what makes repeated appending cheap: `acc`
// and `concat(acc, [x])` are the same buffer, the second one element
// longer (listAppend). Defined after Value so both can hold Value by value
// -- the standard recursive-variant pattern.
//
// The buffer only ever grows within the capacity it was given, so element
// addresses never move while any view of it is alive, and an element, once
// written, is never changed. `used` is how far the longest view reaches;
// only a list ending exactly there may extend it, claiming the new slots
// with one compare-and-swap, so two lists sharing a prefix can never write
// the same slot (the loser copies).
struct ListBuffer {
std::vector<Value> v;
std::atomic<size_t> used;
// Set when this buffer came from appending: a list that was appended
// to once is likely to be again, so ITS copies grow geometrically,
// while a one-off concat's result is allocated at exactly its size.
bool fromAppend = false;
explicit ListBuffer(std::vector<Value>&& items) : v(std::move(items)), used(v.size()) {}
};

class ListItems {
public:
ListItems() = default;
ListItems(std::vector<Value>&& items) // NOLINT: implicit, as the old vector was
: buf_(std::make_shared<ListBuffer>(std::move(items))), n_(buf_->v.size()) {}
ListItems(const std::vector<Value>& items) : ListItems(std::vector<Value>(items)) {} // NOLINT
ListItems(std::initializer_list<Value> items) : ListItems(std::vector<Value>(items)) {}

size_t size() const { return n_; }
bool empty() const { return n_ == 0; }
const Value* data() const { return buf_ ? buf_->v.data() : nullptr; }
const Value* begin() const { return data(); }
const Value* end() const { return data() + n_; }
const Value& operator[](size_t i) const { return data()[i]; }
const Value& at(size_t i) const;
const Value& front() const { return data()[0]; }
const Value& back() const { return data()[n_ - 1]; }
// A copy as a plain vector, for the few callers that build on one.
// Explicit, so a copy is never made by accident binding a reference.
explicit operator std::vector<Value>() const { return std::vector<Value>(begin(), end()); }
std::vector<Value> toVector() const { return std::vector<Value>(begin(), end()); }

private:
ListItems(std::shared_ptr<ListBuffer> buf, size_t n) : buf_(std::move(buf)), n_(n) {}
std::shared_ptr<ListBuffer> buf_;
size_t n_ = 0;
friend ListPtr listAppend(const ListPtr& base, std::vector<Value>&& extra);
};

struct ValueList {
std::vector<Value> items;
ListItems items;
};

// `base` followed by `extra`, sharing base's buffer whenever base is the
// longest list using it and the buffer has room: amortized O(|extra|)
// rather than O(|base| + |extra|), which is what turns the idiomatic
// tail-recursive accumulator, f(i, concat(acc, [x])), from quadratic into
// linear. Safe whatever else still holds `base`: base keeps seeing only
// its own elements.
ListPtr listAppend(const ListPtr& base, std::vector<Value>&& extra);

// Insertion-ordered key/value pairs, not a map: object()'s iteration order
// and `==` are order-sensitive (doc: openscad_evaluator/docs/evaluator.md,
// the object() entry).
Expand Down
2 changes: 1 addition & 1 deletion src/builtins/booleans.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ std::vector<ColoredBody> generateCsg(Evaluator& ev, const CSGParams& params, con
const std::string& op = std::get<std::string>(params.at("op"));
const auto& groupSizes = std::get<ListPtr>(params.at("group_sizes"))->items;
const auto emptyIsAGroupIt = params.find("empty_is_a_group");
const std::vector<Value>* emptyIsAGroup =
const ListItems* emptyIsAGroup =
emptyIsAGroupIt == params.end()
? nullptr
: &std::get<ListPtr>(emptyIsAGroupIt->second)->items;
Expand Down
23 changes: 15 additions & 8 deletions src/builtins/function_builtins.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -616,11 +616,11 @@ Value builtinSearch(const CallArgs& args, Evaluator& ev, const oscad::Position*
const unsigned numReturns = static_cast<unsigned>(static_cast<long long>(nrRaw));
const unsigned indexCol = static_cast<unsigned>(static_cast<long long>(icRaw));

static const std::vector<Value> kNoItems;
static const ListItems kNoItems;
const ListPtr* tablePtr = std::get_if<ListPtr>(&tableArg);
const std::vector<Value>& table = (tablePtr && *tablePtr) ? (*tablePtr)->items : kNoItems;
const ListItems& table = (tablePtr && *tablePtr) ? (*tablePtr)->items : kNoItems;

const auto itemsOf = [](const Value& v) -> const std::vector<Value>& {
const auto itemsOf = [](const Value& v) -> const ListItems& {
const ListPtr* l = std::get_if<ListPtr>(&v);
return (l && *l) ? (*l)->items : kNoItems;
};
Expand All @@ -629,7 +629,7 @@ Value builtinSearch(const CallArgs& args, Evaluator& ev, const oscad::Position*
// only when the entry really is a long enough vector.
const auto hits = [&](const Value& needle, const Value& entry) {
if (indexCol == 0 && oscEqual(needle, entry)) return true;
const std::vector<Value>& ev2 = itemsOf(entry);
const ListItems& ev2 = itemsOf(entry);
return indexCol < ev2.size() && oscEqual(needle, ev2[indexCol]);
};
const auto num = [](size_t j) { return Value{static_cast<double>(j)}; };
Expand Down Expand Up @@ -682,7 +682,7 @@ Value builtinSearch(const CallArgs& args, Evaluator& ev, const oscad::Position*
unsigned matchCount = 0;
std::vector<Value> resultvec;
for (size_t j = 0; j < table.size(); ++j) {
const std::vector<Value>& entryVec = itemsOf(table[j]);
const ListItems& entryVec = itemsOf(table[j]);
if (entryVec.size() <= indexCol) {
ev.warn("Invalid entry in search vector at index " + std::to_string(j) +
", required number of values in the entry: " + std::to_string(indexCol + 1) +
Expand Down Expand Up @@ -1162,9 +1162,9 @@ bool checkMinMax(Evaluator& ev, const std::string& name, const CallArgs& args, c
return false;
}
if (nPos == 1 && std::holds_alternative<ListPtr>(positionalAt(args, 0))) {
static const std::vector<Value> kEmptyItems;
static const ListItems kEmptyItems;
const ListPtr& l = std::get<ListPtr>(positionalAt(args, 0));
const std::vector<Value>& items = l ? l->items : kEmptyItems;
const ListItems& items = l ? l->items : kEmptyItems;
if (items.empty()) {
warnArity(ev, name, "at least 1 vector element", 0, pos);
return false;
Expand Down Expand Up @@ -1346,14 +1346,21 @@ Value evalBuiltinFunctionResolved(Evaluator& ev, BuiltinFnId id, const std::vect
toDoubleLenient(getArg(args, 2, "value_count", Value{})), getArg(args, 3, "seed", Value{}));
}
case BuiltinFnId::Concat: {
// Everything after the first argument, appended to the first
// when it is a list: listAppend extends its buffer in place when
// it can, so an accumulator loop is linear, not quadratic.
const size_t count = positionalCount(args);
const ListPtr* first = count ? std::get_if<ListPtr>(&positionalAt(args, 0)) : nullptr;
std::vector<Value> out;
for (const Value& a : allPositional(args)) {
for (size_t i = (first && *first) ? 1 : 0; i < count; ++i) {
const Value& a = positionalAt(args, i);
if (const ListPtr* l = std::get_if<ListPtr>(&a); l && *l) {
out.insert(out.end(), (*l)->items.begin(), (*l)->items.end());
} else {
out.push_back(a);
}
}
if (first && *first) return Value{listAppend(*first, std::move(out))};
return listOf(std::move(out));
}
case BuiltinFnId::Len: {
Expand Down
2 changes: 1 addition & 1 deletion src/builtins/primitives_3d.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ CSGParams resolveCube(Evaluator& ev, const oscad::ModularCall& node, EvalContext
} else if (l && *l && (*l)->items.size() == 3 &&
std::all_of((*l)->items.begin(), (*l)->items.end(),
[](const Value& v) { return std::holds_alternative<double>(v); })) {
sizeVec = (*l)->items;
sizeVec = (*l)->items.toVector();
} else if (!std::holds_alternative<std::monostate>(sizeArg)) {
ev.warn("Unable to convert cube(size=" + fmtValue(sizeArg) +
", ...) parameter to a number or a vec3 of numbers",
Expand Down
2 changes: 1 addition & 1 deletion src/expr_eval.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ std::optional<bool> valueLess(const Value& a, const Value& b) {

const ListPtr& la = std::get<ListPtr>(a);
const ListPtr& lb = std::get<ListPtr>(b);
static const std::vector<Value> kEmpty;
static const ListItems kEmpty;
const auto& ia = la ? la->items : kEmpty;
const auto& ib = lb ? lb->items : kEmpty;
const size_t n = std::min(ia.size(), ib.size());
Expand Down
54 changes: 44 additions & 10 deletions src/value.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <limits>
#include <optional>
#include <sstream>
#include <stdexcept>

namespace oscadeval {

Expand Down Expand Up @@ -205,8 +206,8 @@ namespace {
// matrix*matrix branch of multiply_visitor -- including which check fires
// first, since that decides which diagnostic a malformed operand produces.
// `fail` records the message and returns undef.
const std::vector<Value>& itemsOf(const Value& v) {
static const std::vector<Value> kEmpty;
const ListItems& itemsOf(const Value& v) {
static const ListItems kEmpty;
const ListPtr* l = std::get_if<ListPtr>(&v);
return (l && *l) ? (*l)->items : kEmpty;
}
Expand All @@ -219,7 +220,7 @@ Value fail(std::string* error, std::string message) {
}

// Vector dot product. Sizes are equal by the caller's own check.
Value multVecVec(const std::vector<Value>& v1, const std::vector<Value>& v2, std::string* error) {
Value multVecVec(const ListItems& v1, const ListItems& v2, std::string* error) {
double r = 0.0;
for (size_t i = 0; i < v1.size(); ++i) {
if (!isNum(v1[i]) || !isNum(v2[i])) {
Expand All @@ -230,11 +231,11 @@ Value multVecVec(const std::vector<Value>& v1, const std::vector<Value>& v2, std
return Value{r};
}

Value multMatVec(const std::vector<Value>& mat, const std::vector<Value>& vec, std::string* error) {
Value multMatVec(const ListItems& mat, const ListItems& vec, std::string* error) {
std::vector<Value> out;
out.reserve(mat.size());
for (size_t i = 0; i < mat.size(); ++i) {
const std::vector<Value>& row = itemsOf(mat[i]);
const ListItems& row = itemsOf(mat[i]);
if (!isVec(mat[i]) || row.size() != vec.size()) {
return fail(error, "Matrix must be rectangular. Problem at row " + std::to_string(i));
}
Expand All @@ -254,14 +255,14 @@ Value multMatVec(const std::vector<Value>& mat, const std::vector<Value>& vec, s
return makeList(std::move(out));
}

Value multVecMat(const std::vector<Value>& vec, const std::vector<Value>& mat, std::string* error) {
Value multVecMat(const ListItems& vec, const ListItems& mat, std::string* error) {
const size_t firstRowSize = itemsOf(mat[0]).size();
std::vector<Value> out;
out.reserve(firstRowSize);
for (size_t i = 0; i < firstRowSize; ++i) {
double re = 0.0;
for (size_t j = 0; j < vec.size(); ++j) {
const std::vector<Value>& row = itemsOf(mat[j]);
const ListItems& row = itemsOf(mat[j]);
if (!isVec(mat[j]) || row.size() != firstRowSize) {
return fail(error, "Matrix must be rectangular. Problem at row " + std::to_string(j));
}
Expand All @@ -282,8 +283,8 @@ Value multVecMat(const std::vector<Value>& vec, const std::vector<Value>& mat, s
} // namespace

Value matmul(const Value& a, const Value& b, std::string* error) {
const std::vector<Value>& al = itemsOf(a);
const std::vector<Value>& bl = itemsOf(b);
const ListItems& al = itemsOf(a);
const ListItems& bl = itemsOf(b);
// The reference checks emptiness before anything else, so `[] * [1,2]`
// is this message rather than a length mismatch.
if (al.empty() || bl.empty()) return fail(error, "Multiplication is undefined on empty vectors");
Expand Down Expand Up @@ -323,7 +324,7 @@ Value matmul(const Value& a, const Value& b, std::string* error) {
std::vector<Value> rows;
rows.reserve(al.size());
for (size_t i = 0; i < al.size(); ++i) {
const std::vector<Value>& srcRow = itemsOf(al[i]);
const ListItems& srcRow = itemsOf(al[i]);
if (srcRow.size() != bl.size()) {
return fail(error,
"matrix*matrix left operand row length does not match right operand row count (" +
Expand Down Expand Up @@ -618,4 +619,37 @@ void appendEachInto(std::vector<Value>& out, const Value& v) {
if (!std::holds_alternative<std::monostate>(v)) out.push_back(v);
}

const Value& ListItems::at(size_t i) const {
if (i >= n_) throw std::out_of_range("list index out of range");
return data()[i];
}

ListPtr listAppend(const ListPtr& base, std::vector<Value>&& extra) {
if (!base) return std::make_shared<const ValueList>(ValueList{std::move(extra)});
if (extra.empty()) return base;
const ListItems& items = base->items;
const size_t n = items.n_, k = extra.size();
if (const auto& buf = items.buf_; buf && buf->v.capacity() - n >= k) {
size_t expected = n;
if (buf->used.compare_exchange_strong(expected, n + k)) {
// This list ended at the buffer's frontier and now owns the
// slots after it. Within capacity, push_back never reallocates,
// so every other view's elements stay where they are.
for (Value& x : extra) buf->v.push_back(std::move(x));
return std::make_shared<const ValueList>(ValueList{ListItems(buf, n + k)});
}
}
// Copy: a fresh buffer, given room to grow if this list has been
// appended to before (the accumulator case), exact otherwise.
const bool growing = items.buf_ && items.buf_->fromAppend;
std::vector<Value> out;
out.reserve(growing ? std::max<size_t>(2 * (n + k), 16) : n + k);
out.insert(out.end(), items.begin(), items.end());
for (Value& x : extra) out.push_back(std::move(x));
auto buf = std::make_shared<ListBuffer>(std::move(out));
buf->fromAppend = true;
const size_t len = buf->v.size();
return std::make_shared<const ValueList>(ValueList{ListItems(std::move(buf), len)});
}

} // namespace oscadeval
18 changes: 9 additions & 9 deletions tests/test_function_builtins.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ double asNum(const Value& v) { return std::get<double>(v); }
bool asBool(const Value& v) { return std::get<bool>(v); }
bool isUndef(const Value& v) { return std::holds_alternative<std::monostate>(v); }
std::string asStr(const Value& v) { return std::get<std::string>(v); }
const std::vector<Value>& asList(const Value& v) { return std::get<ListPtr>(v)->items; }
const ListItems& asList(const Value& v) { return std::get<ListPtr>(v)->items; }

} // namespace

Expand Down Expand Up @@ -742,7 +742,7 @@ TEST(LinearSolve, SolvesASquareSystem) {
// asList() returns a reference INTO v, so v has to outlive it -- taking
// asList(evalSrc(...)) directly dangles the moment the temporary dies.
const Value v = evalSrc("linear_solve([[2,1],[1,3]], [5,10]).x", ev);
const std::vector<Value>& x = asList(v);
const ListItems& x = asList(v);
ASSERT_EQ(x.size(), 2u);
EXPECT_NEAR(asNum(x[0]), 1.0, 1e-12);
EXPECT_NEAR(asNum(x[1]), 3.0, 1e-12);
Expand Down Expand Up @@ -779,7 +779,7 @@ TEST(LinearSolve, SingularityIsRelativeToTheMatrixNotAbsolute) {
Evaluator ev;
EXPECT_FALSE(asBool(evalSrc("linear_solve([[2e-10,0],[0,2e-10]], [1e-10,1e-10]).singular", ev)));
const Value v = evalSrc("linear_solve([[2e-10,0],[0,2e-10]], [1e-10,1e-10]).x", ev);
const std::vector<Value>& x = asList(v);
const ListItems& x = asList(v);
ASSERT_EQ(x.size(), 2u);
EXPECT_NEAR(asNum(x[0]), 0.5, 1e-9);
}
Expand All @@ -789,9 +789,9 @@ TEST(LinearSolve, AMatrixRightHandSideSolvesEveryColumn) {
// Solving against the identity is the inverse; [[2,1],[1,3]] has
// det 5, so the inverse is [[0.6,-0.2],[-0.2,0.4]].
const Value v = evalSrc("linear_solve([[2,1],[1,3]], [[1,0],[0,1]]).x", ev);
const std::vector<Value>& rows = asList(v);
const ListItems& rows = asList(v);
ASSERT_EQ(rows.size(), 2u);
const std::vector<Value>& r0 = asList(rows[0]);
const ListItems& r0 = asList(rows[0]);
ASSERT_EQ(r0.size(), 2u);
EXPECT_NEAR(asNum(r0[0]), 0.6, 1e-12);
EXPECT_NEAR(asNum(r0[1]), -0.2, 1e-12);
Expand All @@ -814,7 +814,7 @@ TEST(LinearSolve, OverdeterminedGivesTheLeastSquaresFit) {
// y = 1 + 2x, so the residual is zero and a=1, b=2.
Evaluator ev;
const Value v = evalSrc("linear_solve([[1,0],[1,1],[1,2],[1,3]], [1,3,5,7]).x", ev);
const std::vector<Value>& x = asList(v);
const ListItems& x = asList(v);
ASSERT_EQ(x.size(), 2u);
EXPECT_NEAR(asNum(x[0]), 1.0, 1e-10);
EXPECT_NEAR(asNum(x[1]), 2.0, 1e-10);
Expand All @@ -826,7 +826,7 @@ TEST(LinearSolve, OverdeterminedWithNoExactSolutionMinimisesTheResidual) {
// read off this implementation.
Evaluator ev;
const Value v = evalSrc("linear_solve([[1,0],[1,1],[1,2],[1,3]], [0,1,2,10]).x", ev);
const std::vector<Value>& x = asList(v);
const ListItems& x = asList(v);
ASSERT_EQ(x.size(), 2u);
EXPECT_NEAR(asNum(x[0]), -1.4, 1e-9);
EXPECT_NEAR(asNum(x[1]), 3.1, 1e-9);
Expand All @@ -838,7 +838,7 @@ TEST(LinearSolve, UnderdeterminedGivesTheMinimUmNormSolution) {
// asserts the NORM -- that is the whole claim of a minimum-norm solve.
Evaluator ev;
const Value v = evalSrc("linear_solve([[1,1]], [2]).x", ev);
const std::vector<Value>& x = asList(v);
const ListItems& x = asList(v);
ASSERT_EQ(x.size(), 2u);
EXPECT_NEAR(asNum(x[0]), 1.0, 1e-12);
EXPECT_NEAR(asNum(x[1]), 1.0, 1e-12);
Expand All @@ -848,7 +848,7 @@ TEST(LinearSolve, UnderdeterminedSatisfiesEveryEquation) {
// 2 x 3: the plane_intersection shape BOSL2 relies on.
Evaluator ev;
const Value v = evalSrc("linear_solve([[1,0,1],[0,1,1]], [3,5]).x", ev);
const std::vector<Value>& x = asList(v);
const ListItems& x = asList(v);
ASSERT_EQ(x.size(), 3u);
const double a = asNum(x[0]), b = asNum(x[1]), c = asNum(x[2]);
EXPECT_NEAR(a + c, 3.0, 1e-12);
Expand Down
Loading
Loading