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
17 changes: 16 additions & 1 deletion include/openscad_cpp_evaluator/value.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,20 @@ class IterableValues {
// it, keeping expandIterable() itself free of any Evaluator/echo coupling.
using RangeTooManyFn = std::function<void(size_t count)>;

// Called when a range's step points AWAY from its end -- begin > end with a
// positive step, or begin < end with a negative step. Such a range is
// naturally empty, and the reference warns rather than iterating zero times
// in silence. `stepPositive` picks which of its two messages applies; use
// rangeDirectionWarning() to build the text.
//
// Separate from RangeTooManyFn for the same reason that one exists: it
// keeps expandIterable() free of any Evaluator/echo coupling.
using RangeDirectionFn = std::function<void(bool stepPositive)>;

// The reference's own wording, quoted verbatim, for a range whose step
// points away from its end. Shared so the five call sites cannot drift.
std::string rangeDirectionWarning(bool stepPositive);

// Converts a for()/intersection_for() loop assignment's evaluated RHS into
// the sequence of values to iterate: undef -> empty, range -> a LAZY
// sequence (matching OscRange's own start/step/end iteration, half-open
Expand All @@ -380,7 +394,8 @@ using RangeTooManyFn = std::function<void(size_t count)>;
// one with a live consumer (BelfrySCAD) that surfaced it. The count is
// computed in closed form (not IterableValues::size()'s O(n) walk) so a
// legitimate huge-but-under-the-limit range never pays an eager pass.
IterableValues expandIterable(const Value& v, const RangeTooManyFn& onTooMany = nullptr);
IterableValues expandIterable(const Value& v, const RangeTooManyFn& onTooMany = nullptr,
const RangeDirectionFn& onWrongDirection = nullptr);

// `each <body>`'s own flatten-one-level rule, shared by the AST interpreter
// (evalListLiteral/evalListElement's ListCompEach handling, expr_eval.cpp)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"

[project]
name = "openscad_cpp_evaluator"
version = "0.39.0"
version = "0.40.0"
description = "C++ OpenSCAD evaluator with Python bindings"
readme = "README.md"
requires-python = ">=3.12"
Expand Down
2 changes: 2 additions & 0 deletions src/builtins/control.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ CSGParams resolveIntersectionFor(Evaluator& ev, const oscad::ModularIntersection
const oscad::Position* pos = &assign->position();
IterableValues iter = expandIterable(values, [&](size_t count) {
ev.warn("Bad range parameter in for statement: too many elements (" + std::to_string(count) + ")", pos);
}, [&](bool stepPositive) {
ev.warn(rangeDirectionWarning(stepPositive), pos);
});
for (const Value& val : iter) {
EvalContext childCtx = parentCtx.childCtx(nullptr, std::nullopt, ctx.childrenNodes, ctx.childrenCallerCtx);
Expand Down
7 changes: 6 additions & 1 deletion src/builtins/function_builtins.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -952,7 +952,12 @@ Value evalBuiltinFunction(Evaluator& ev, const std::string& name, const CallArgs
}
if (const OscRange* r = std::get_if<OscRange>(&c)) {
std::string out;
for (const Value& item : expandIterable(Value{*r})) out += encode(item);
// The reference warns here too -- chr([70:1:65]) is
// as much a typo as for(i=[70:1:65]) would be.
const IterableValues seq = expandIterable(Value{*r}, nullptr, [&](bool stepPositive) {
ev.warn(rangeDirectionWarning(stepPositive), &node.position());
});
for (const Value& item : seq) out += encode(item);
return out;
}
return {};
Expand Down
4 changes: 4 additions & 0 deletions src/bytecode_vm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -740,6 +740,8 @@ Value driveVm(Evaluator& ev, size_t floor) {
il.values = expandIterable(v, [&](size_t count) {
ev.warn("Bad range parameter in for statement: too many elements (" + std::to_string(count) + ")",
ins.pos);
}, [&](bool stepPositive) {
ev.warn(rangeDirectionWarning(stepPositive), ins.pos);
});
il.index = 0;
il.total = il.values.size();
Expand Down Expand Up @@ -1464,6 +1466,8 @@ Value driveVm(Evaluator& ev, size_t floor) {
il.values = expandIterable(v, [&](size_t count) {
ev.warn("Bad range parameter in for statement: too many elements (" + std::to_string(count) + ")",
ins.pos);
}, [&](bool stepPositive) {
ev.warn(rangeDirectionWarning(stepPositive), ins.pos);
});
il.index = 0;
il.total = il.values.size();
Expand Down
2 changes: 2 additions & 0 deletions src/expr_eval.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ void Evaluator::evalListElement(const oscad::ASTNode& elem, EvalContext& ctx, st
const oscad::Position* pos = &assign->position();
IterableValues iter = expandIterable(values, [&](size_t count) {
warn("Bad range parameter in for statement: too many elements (" + std::to_string(count) + ")", pos);
}, [&](bool stepPositive) {
warn(rangeDirectionWarning(stepPositive), pos);
});
for (const Value& val : iter) {
EvalContext childCtx = parentCtx.letChildCtx();
Expand Down
2 changes: 2 additions & 0 deletions src/stmt_eval.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ void Evaluator::evalFor(const oscad::ModularFor& node, EvalContext& ctx) {
const oscad::Position* pos = &assign->position();
IterableValues iter = expandIterable(values, [&](size_t count) {
warn("Bad range parameter in for statement: too many elements (" + std::to_string(count) + ")", pos);
}, [&](bool stepPositive) {
warn(rangeDirectionWarning(stepPositive), pos);
});
for (const Value& val : iter) {
EvalContext childCtx = parentCtx.childCtx(nullptr, std::nullopt, ctx.childrenNodes, ctx.childrenCallerCtx);
Expand Down
56 changes: 53 additions & 3 deletions src/user_calls.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -984,16 +984,66 @@ std::optional<Evaluator::ChildrenForward> Evaluator::prepareChildrenForward(cons
// filtered statement may produce 0 bodies, which would shift every
// subsequent body-index lookup, so the Nth statement is evaluated
// directly instead.
const int idx = static_cast<int>(toDoubleLenient(idxArg));
std::vector<const oscad::ASTNode*> geoNodes;
for (const oscad::ASTNode* c : *ctx.childrenNodes) {
if (c->kind() != oscad::NodeKind::Assignment && c->kind() != oscad::NodeKind::ModuleDeclaration &&
c->kind() != oscad::NodeKind::FunctionDeclaration) {
geoNodes.push_back(c);
}
}
if (idx < 0 || static_cast<size_t>(idx) >= geoNodes.size()) return std::nullopt;
return ChildrenForward{std::move(evalCtx), {geoNodes[static_cast<size_t>(idx)]}};

// The argument is a number, a VECTOR of numbers, or a RANGE --
// children([3:1:5]) is children(3); children(4); children(5), and
// children([3:-1:1]) is 3, 2, 1 in that order. Only a plain number was
// handled before, and toDoubleLenient collapses a list or a range to 0,
// so every vector/range form silently rendered child 0 instead.
//
// Ranges go through expandIterable, the same path a for-loop uses, so
// step direction, fractional steps and naturally-empty ranges all
// behave identically in both places rather than growing a second
// interpretation here.
std::vector<Value> indexValues;
if (std::holds_alternative<double>(idxArg)) {
indexValues.push_back(idxArg);
} else if (std::holds_alternative<ListPtr>(idxArg) || std::holds_alternative<OscRange>(idxArg)) {
const IterableValues iter = expandIterable(idxArg, nullptr, [&](bool stepPositive) {
warn(rangeDirectionWarning(stepPositive), currentWarnEntry());
});
for (const Value& v : iter) indexValues.push_back(v);
} else {
warn("Bad parameter type (" + fmtValue(idxArg) +
") for children, only accept: empty, number, vector, range.",
currentWarnEntry());
return std::nullopt;
}

// Order is preserved and duplicates are kept: children([2,2,2]) really
// does evaluate child 2 three times, and the order shows through in the
// CSG tree even though a union usually hides it.
std::vector<const oscad::ASTNode*> picked;
picked.reserve(indexValues.size());
for (const Value& v : indexValues) {
if (!std::holds_alternative<double>(v)) {
warn("Bad parameter type (" + fmtValue(v) +
") for children, only accept: empty, number, vector, range.",
currentWarnEntry());
return std::nullopt;
}
// Truncates, matching the reference: children([1.7]) is child 1.
const long long idx = static_cast<long long>(std::get<double>(v));
if (idx < 0 || static_cast<size_t>(idx) >= geoNodes.size()) {
// An out-of-range index is skipped, not fatal -- children([0,99])
// still draws child 0. Warned about either way; this used to
// return silently.
warn("Children index (" + fmtValue(v) + ") out of bounds (" +
std::to_string(geoNodes.size()) + " children)",
currentWarnEntry());
continue;
}
picked.push_back(geoNodes[static_cast<size_t>(idx)]);
}
if (picked.empty()) return std::nullopt;
return ChildrenForward{std::move(evalCtx), std::move(picked)};
}

void Evaluator::builtinChildren(const CallArgs& args, EvalContext& ctx) {
Expand Down
19 changes: 18 additions & 1 deletion src/value.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -490,13 +490,30 @@ std::optional<size_t> rangeElementCount(const OscRange& r) {
return static_cast<size_t>(std::floor(n + 1e-10)) + 1;
}

IterableValues expandIterable(const Value& v, const RangeTooManyFn& onTooMany) {
std::string rangeDirectionWarning(bool stepPositive) {
return stepPositive ? "begin is greater than the end, but step is positive"
: "begin is smaller than the end, but step is negative";
}

IterableValues expandIterable(const Value& v, const RangeTooManyFn& onTooMany,
const RangeDirectionFn& onWrongDirection) {
if (std::holds_alternative<std::monostate>(v)) return IterableValues{};
if (const OscRange* r = std::get_if<OscRange>(&v)) {
if (const std::optional<size_t> count = rangeElementCount(*r); count && *count >= 1'000'000) {
if (onTooMany) onTooMany(*count);
return IterableValues{};
}
// A step pointing away from the end yields nothing. That is almost
// always a typo -- [1:0] where [1:-1:0] was meant -- so the
// reference says so rather than silently running zero times.
//
// A zero step is deliberately NOT reported here: it is a different
// failure (the reference calls it "too many elements") and shares
// rangeElementCount's nullopt with this case only by coincidence.
if (onWrongDirection && r->step != 0.0) {
const double n = (r->end - r->start) / r->step;
if (n < -1e-10) onWrongDirection(r->step > 0.0);
}
// Lazy -- IterableValues itself reproduces this exact
// termination condition (a zero step is naturally empty: neither
// `x <= end` nor `x >= end` branch ever fires for it) without
Expand Down
Loading