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
29 changes: 20 additions & 9 deletions include/openscad_cpp_evaluator/bytecode.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -104,18 +104,29 @@ enum class Op {
// themselves lists).
AccumMergeEach,

// ListCompFor's per-assignment iteration -- materialize once (a
// dimension's own RHS expression is evaluated exactly once, matching
// the interpreter's own upfront `pairs.push_back(...)` loop), then
// reset+iterate however many times that dimension is (re-)entered
// (once per outer-dimension iteration for a nested `for`). `a` = an
// iterList id (see CompiledChunk::numIterLists), unique per assignment
// across the whole chunk.
// ListCompFor's per-assignment iteration. `a` = an iterList id (see
// CompiledChunk::numIterLists), unique per assignment across the whole
// chunk. Each dimension's own IterMaterialize is emitted INSIDE the
// enclosing dimension's own loop body (compileListElement's own
// emitDim recursion), so it re-executes once per outer-dimension
// binding -- required for a later dimension's own RHS to see an
// earlier one's current value (e.g. `[for (p=[1:N], pt=f(p)) pt]`),
// matching evalFor's own doc comment (stmt_eval.cpp) and real
// OpenSCAD.app's verified behavior.
IterMaterialize, // pops one Value (the assignment's RHS), expandIterable()s it into iterLists[a], resets its index
IterReset, // resets iterLists[a]'s index to 0 WITHOUT re-materializing (a re-entry, not the first entry)
// Resets iterLists[a]'s index to 0 WITHOUT re-materializing. UNREACHABLE
// as of the materialize-inside-the-nesting fix above: re-materializing
// on every dimension re-entry already resets the index as a side
// effect (see IterMaterialize's own doc comment), so there's no longer
// a "reset without re-materializing" case left to reach for either
// ListCompFor (compileListElement) or ModularFor (compileForLoop).
// Left defined rather than removed in the same change -- a distinct,
// lower-risk cleanup (same convention as LoadUpvalue's own doc
// comment, above).
IterReset,
IterNext, // a = loop-variable slot, b = iterList id, c = jump target for exhaustion: if iterLists[b] has a
// next value (at its current index), write it to slots[a], advance the index, fall through;
// else jump to c (index is left as-is; the next IterReset for this id will restart it)
// else jump to c (index is left as-is; the next IterMaterialize for this id will restart it)

// ListCompCFor's runaway-loop guard, mirroring evalListElement's own
// 1,000,000-iteration safety limit exactly (see its own doc comment
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.13.7"
version = "0.13.9"
description = "C++ OpenSCAD evaluator with Python bindings"
readme = "README.md"
requires-python = ">=3.12"
Expand Down
29 changes: 16 additions & 13 deletions src/builtins/control.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -97,24 +97,21 @@ ColoredBody combineBodies(const std::vector<ColoredBody>& bodies) {
// children) -- same rationale as booleans.cpp's own group_sizes. Mirrors
// _resolve_intersection_for/_generate_intersection_for.
CSGParams resolveIntersectionFor(Evaluator& ev, const oscad::ModularIntersectionFor& node, EvalContext& ctx) {
std::vector<std::pair<std::string, IterableValues>> varSeqs;
varSeqs.reserve(node.assignments.size());
for (const auto& assign : node.assignments) {
Value values = ev.evalExpr(*assign->expr, ctx);
const oscad::Position* pos = &assign->position();
varSeqs.emplace_back(assign->name->name, expandIterable(values, [&](size_t count) {
ev.warn("Bad range parameter in for statement: too many elements (" + std::to_string(count) + ")", pos);
}));
}

std::vector<const oscad::ASTNode*> bodyNodes;
bodyNodes.reserve(node.body.size());
for (const auto& b : node.body) bodyNodes.push_back(b.get());

std::vector<Value> groupSizes;

// Each dimension's own RHS is evaluated against `parentCtx` (not
// upfront against the original `ctx`), re-evaluated on every entry
// into this recursion level -- see evalFor's own doc comment
// (stmt_eval.cpp) for the full "verified against real OpenSCAD.app"
// rationale; this is the identical bug in intersection_for's own
// cartesian loop (e.g. `intersection_for (i=[0:2], j=[0:i]) ...`
// needs `i` visible in `j`'s own range expression).
std::function<void(size_t, EvalContext&)> recurse = [&](size_t depth, EvalContext& parentCtx) {
if (depth == varSeqs.size()) {
if (depth == node.assignments.size()) {
// One body-entry marker per full cartesian-product iteration
// and nothing per individual variable binding (unlike
// evalFor) -- mirrors _resolve_intersection_for's single
Expand All @@ -126,9 +123,15 @@ CSGParams resolveIntersectionFor(Evaluator& ev, const oscad::ModularIntersection
groupSizes.push_back(Value{static_cast<double>(after - before)});
return;
}
for (const Value& val : varSeqs[depth].second) {
const auto& assign = node.assignments[depth];
Value values = ev.evalExpr(*assign->expr, parentCtx);
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);
});
for (const Value& val : iter) {
EvalContext childCtx = parentCtx.childCtx(nullptr, std::nullopt, ctx.childrenNodes, ctx.childrenCallerCtx);
childCtx.let_->set(varSeqs[depth].first, val);
childCtx.let_->set(assign->name->name, val);
recurse(depth + 1, childCtx);
}
};
Expand Down
Loading