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
98 changes: 71 additions & 27 deletions include/openscad_cpp_evaluator/bytecode.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,9 @@ enum class Op {
CallModule,

// -- Builtin-wrap compilation (closes the "NativeStatement gap" for ----
// -- translate/rotate/scale/mirror/multmatrix/resize/color/#/%/!) ------
// -- translate/rotate/scale/mirror/multmatrix/resize/color/#/%/!, ------
// -- hull/minkowski/render/linear_extrude/rotate_extrude/projection/ ---
// -- offset/roof) --------------------------------------------------------
// A builtin-with-children statement that isn't a "call" the way
// Op::CallModule's target is (no callStack_/profiling participation,
// no named scope with upvalue semantics -- see CompiledChunk::
Expand All @@ -284,31 +286,41 @@ enum class Op {
// (mirrors Evaluator::buildTreeNode's own ordering exactly -- doing
// this AFTER would silently drop uncacheable/ManifoldCache taint
// tracking for a rands() call embedded in the wrapper's own
// arguments), computes this site's own params (and, for Transform/
// Color kinds, a possibly-$-scoped child EvalContext, pushed onto
// f.ctxChain unconditionally -- Modifier kind needs neither params
// nor a ctx push), pushes a fresh ev.treeStack_ frame, and stashes
// {params, randsBefore, siteIdx} onto VmFrame::builtinWrapStack (a
// real per-frame LIFO stack, not a single slot: Push/PopBuiltinWrap
// pairs can nest or sequence within one frame's own instruction
// stream, e.g. `translate(a) translate(b) recur();`). The compiler
// always emits a plain Op::CheckDebugStatement immediately before
// this (see emitBuiltinWrap, bytecode_compiler.cpp), mirroring
// ModularEcho/ModularAssert's own pattern -- NOT skipped the way
// Op::CallModule's own call site skips one: this is a genuine
// statement doing real work here, unlike a module call (which
// transfers control to a declaration whose OWN body statements each
// get their own check instead).
// arguments), computes this site's own params for every kind except
// Modifier (empty, no ctx push) and Roof (deferred to Pop -- see
// BuiltinWrapSite's own doc comment) via that kind's own native
// compute function (computeTransformParams/computeColorParams/etc.,
// builtins.hpp) -- always a possibly-$-scoped child EvalContext,
// pushed onto f.ctxChain unconditionally for every non-Modifier kind
// (Roof pushes it too, params or not) -- pushes a fresh ev.treeStack_
// frame, and stashes {params, randsBefore, siteIdx, (Roof only)
// deferredArgs} onto VmFrame::builtinWrapStack (a real per-frame LIFO
// stack, not a single slot: Push/PopBuiltinWrap pairs can nest or
// sequence within one frame's own instruction stream, e.g.
// `translate(a) translate(b) recur();`). The compiler always emits a
// plain Op::CheckDebugStatement immediately before this (see
// emitBuiltinWrap, bytecode_compiler.cpp), mirroring ModularEcho/
// ModularAssert's own pattern -- NOT skipped the way Op::CallModule's
// own call site skips one: this is a genuine statement doing real
// work here, unlike a module call (which transfers control to a
// declaration whose OWN body statements each get their own check
// instead).
PushBuiltinWrap,

// Closes the matching Op::PushBuiltinWrap bracket: pops
// VmFrame::builtinWrapStack's own top entry FIRST (before anything
// that can itself throw, e.g. Evaluator::setTreeDepthOrThrow below --
// so the exception-teardown path's own pending count is already
// correct if THIS throws), pops the ctx push Push made (Transform/
// Color kinds only), pops ev.treeStack_ to retrieve the children this
// bracket's own body produced, and builds the tagged CSGNode exactly
// like Evaluator::buildTreeNode's own post-resolveBody() half does
// correct if THIS throws); for Roof only, computes `pending.params`
// HERE via computeRoofParams(ev, pending.deferredArgs, ctx) -- the one
// kind whose params computation has an observable side effect
// (ev.warn()) that must stay ordered AFTER children, unlike every
// other kind's params (computed at Push, before children, since
// nothing else in this group has an order-sensitive side effect); pops
// the ctx Push made (every kind except Modifier); pops ev.treeStack_
// to retrieve the children this bracket's own body produced, and
// builds the tagged CSGNode exactly like Evaluator::buildTreeNode's
// own post-resolveBody() half does
// (kind/node/isBuiltin=true/children/params/uncacheable/
// setTreeDepthOrThrow), pushing it onto the new top of treeStack_. a
// = index into CompiledChunk::builtinWrapSites (same site Push used).
Expand Down Expand Up @@ -465,8 +477,9 @@ enum class Op {
// A single "native passthrough" statement -- intersection_for, every
// OTHER builtin module call not covered by Op::PushBuiltinWrap/
// Op::PushCsgWrap (see those ops' own doc comments for exactly which
// builtins ARE covered -- cube/sphere/hull/linear_extrude/etc., the
// ones that never wrap a recursive call in idiomatic OpenSCAD), the `*`
// builtins ARE covered -- cube/sphere/import/text/surface/etc., the
// true LEAVES that never take a `children` block at all, so wrapping a
// recursive call inside one isn't even syntactically possible), the `*`
// modifier's own no-op case, or a user-module call that didn't
// resolve at compile time (shadowed, forward-declared, or otherwise
// not statically known) -- anything compileStatementList doesn't give
Expand All @@ -488,7 +501,18 @@ enum class Op {
// fell here too, and were the last remaining REAL native-reentry risk
// (bespoke group_sizes bookkeeping meant they couldn't just reuse
// PushBuiltinWrap's own bracket) -- they now have Op::PushCsgWrap,
// above.)
// above. hull()/minkowski()/render()/linear_extrude()/rotate_extrude()/
// projection()/offset()/roof() fell here too, closing out every
// remaining non-leaf builtin whose OWN children can wrap a recursive
// call in an idiomatic script (a rounded-hull chain, a recursive
// extrude, etc.) -- they now share Op::PushBuiltinWrap's own bracket
// via 6 new Kind values (Passthrough/LinearExtrude/RotateExtrude/
// Projection/Offset/Roof), reusing the exact same mechanism rather
// than inventing another. Only intersection_for remains -- its
// group_sizes are keyed by RUNTIME loop iteration, not a static
// source-statement list the way union/difference/intersection's are,
// so it can't reuse either existing bracket shape without new loop-
// compilation machinery; still deliberately native as of this writing.)
// a = index into CompiledChunk::nativeStatements. Runtime just does
// what Evaluator::evalChildren's own per-statement loop already does
// for one node: derive childCtx via ctx.withScope(...), checkDebug,
Expand Down Expand Up @@ -813,8 +837,8 @@ struct CompiledChunk {
// replay the native param-resolution step and rebuild the tagged
// CSGNode at Pop time.
//
// `node`'s concrete type depends on `kind`: Transform/Color sites are
// genuine `ModularCall`s (down-cast to read `.name`/`.arguments`/
// `node`'s concrete type depends on `kind`: every kind except Modifier
// is a genuine `ModularCall` (down-cast to read `.name`/`.arguments`/
// `.children`); Modifier sites are the wrapper node itself
// (`ModularModifierHighlight`/`Background`/`ShowOnly` -- `!`/`#`/`%`;
// `*`/Disable never reaches here, its child never evaluates at all,
Expand All @@ -826,10 +850,30 @@ struct CompiledChunk {
// union of concrete pointers) because that's all Op::PopBuiltinWrap
// itself ever needs `node` for: the resulting CSGNode's own
// `node`/error-position field.
//
// Passthrough/LinearExtrude/RotateExtrude/Projection/Offset/Roof
// (added for hull()/minkowski()/render()/linear_extrude()/
// rotate_extrude()/projection()/offset()/roof()) share Transform/
// Color's exact push-time-computed-params shape, just via a different
// native compute function each (Op::PushBuiltinWrap's own runtime
// handler switches on `kind` to pick it) -- see that op's own doc
// comment for the one exception (Roof, whose params computation has
// to run at POP time instead, after children, to preserve an
// observable warn() ordering).
struct BuiltinWrapSite {
enum class Kind { Transform, Color, Modifier };
enum class Kind {
Transform,
Color,
Modifier,
Passthrough, // hull()/minkowski()/render() -- empty params, no compute function needed
LinearExtrude,
RotateExtrude,
Projection,
Offset,
Roof, // computed at POP time, not PUSH -- see this struct's own doc comment
};
Kind kind;
std::string tagName; // "translate"/"rotate"/.../"color"/"highlight"/"background"/"show_only"
std::string tagName; // "translate"/"rotate"/.../"color"/"highlight"/"background"/"show_only"/"hull"/...
const oscad::ASTNode* node = nullptr;
};

Expand Down
10 changes: 10 additions & 0 deletions include/openscad_cpp_evaluator/bytecode_vm.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include "openscad_cpp_evaluator/bound_args.hpp"
#include "openscad_cpp_evaluator/bytecode.hpp"
#include "openscad_cpp_evaluator/call_args.hpp"
#include "openscad_cpp_evaluator/csg_node.hpp"
#include "openscad_cpp_evaluator/eval_context.hpp"
#include "openscad_cpp_evaluator/value.hpp"
Expand All @@ -28,6 +29,15 @@ struct PendingBuiltinWrap {
CSGParams params;
std::uint64_t randsBefore = 0;
int siteIdx = -1;
// Only populated for BuiltinWrapSite::Kind::Roof -- the already-
// resolved call arguments, retained across the whole bracket so
// Op::PopBuiltinWrap's handler can compute roof()'s own params AFTER
// children finish (computeRoofParams needs them; re-running
// resolveCallArgs at Pop time instead would re-evaluate every argument
// expression a second time -- double rands()/side effects). Every
// other kind computes its params at Push time and leaves this default-
// empty.
CallArgs deferredArgs;
};

// One still-open Op::PushCsgWrap bracket's own state -- see that op's own
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.5"
version = "0.13.6"
description = "C++ OpenSCAD evaluator with Python bindings"
readme = "README.md"
requires-python = ">=3.12"
Expand Down
17 changes: 17 additions & 0 deletions src/builtins/builtins.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once

#include "openscad_cpp_evaluator/call_args.hpp"
#include "openscad_cpp_evaluator/dispatch.hpp"

#include <optional>
Expand Down Expand Up @@ -153,21 +154,30 @@ std::vector<ColoredBody> generateMinkowski(Evaluator& ev, const CSGParams& param
const oscad::ASTNode& node);

// linear_extrude()/rotate_extrude()/projection() -- extrude.cpp.
// computeXParams: the pure "resolve args, build params" half, split out
// like computeTransformParams/computeColorParams so Op::PushBuiltinWrap's
// own runtime handler can call it directly (bytecode_vm.cpp) instead of
// through the whole resolve function, whose OTHER half (evalChildren) is
// exactly the native reentry that split exists to avoid.
BuiltinWrapParams computeLinearExtrudeParams(Evaluator& ev, const oscad::ModularCall& node, EvalContext& ctx);
CSGParams resolveLinearExtrude(Evaluator& ev, const oscad::ModularCall& node, EvalContext& ctx);
std::vector<ColoredBody> generateLinearExtrude(Evaluator& ev, const CSGParams& params,
const std::vector<std::unique_ptr<CSGNode>>& children,
const oscad::ASTNode& node);

BuiltinWrapParams computeRotateExtrudeParams(Evaluator& ev, const oscad::ModularCall& node, EvalContext& ctx);
CSGParams resolveRotateExtrude(Evaluator& ev, const oscad::ModularCall& node, EvalContext& ctx);
std::vector<ColoredBody> generateRotateExtrude(Evaluator& ev, const CSGParams& params,
const std::vector<std::unique_ptr<CSGNode>>& children,
const oscad::ASTNode& node);

BuiltinWrapParams computeProjectionParams(Evaluator& ev, const oscad::ModularCall& node, EvalContext& ctx);
CSGParams resolveProjection(Evaluator& ev, const oscad::ModularCall& node, EvalContext& ctx);
std::vector<ColoredBody> generateProjection(Evaluator& ev, const CSGParams& params,
const std::vector<std::unique_ptr<CSGNode>>& children,
const oscad::ASTNode& node);

BuiltinWrapParams computeOffsetParams(Evaluator& ev, const oscad::ModularCall& node, EvalContext& ctx);
CSGParams resolveOffset(Evaluator& ev, const oscad::ModularCall& node, EvalContext& ctx);
std::vector<ColoredBody> generateOffset(Evaluator& ev, const CSGParams& params,
const std::vector<std::unique_ptr<CSGNode>>& children, const oscad::ASTNode& node);
Expand All @@ -176,6 +186,13 @@ std::vector<ColoredBody> generateOffset(Evaluator& ev, const CSGParams& params,
// contour polygons) + Tier 3 (SDF/level-set fallback) only, per the plan --
// Tier 2 (general multi-contour/hole straight skeleton) is a named,
// documented follow-up, not silently dropped (see CLAUDE.md).
// computeRoofParams: UNLIKE computeLinearExtrudeParams and friends above,
// this is the params-computation half taken from AFTER evalChildren (it
// calls ev.warn(), an observable side effect that must stay ordered after
// any child's own echo/warn output) -- takes already-resolved CallArgs,
// not a raw node+ctx, since Op::PushBuiltinWrap's own runtime handler
// calls this at POP time. See its own doc comment (roof.cpp).
CSGParams computeRoofParams(Evaluator& ev, const CallArgs& args, EvalContext& effCtx);
CSGParams resolveRoof(Evaluator& ev, const oscad::ModularCall& node, EvalContext& ctx);
std::vector<ColoredBody> generateRoof(Evaluator& ev, const CSGParams& params,
const std::vector<std::unique_ptr<CSGNode>>& children, const oscad::ASTNode& node);
Expand Down
Loading