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
12 changes: 6 additions & 6 deletions bindings/module.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -388,8 +388,8 @@ nb::object evaluate(const std::string& path, nb::dict viewportParams,
nb::gil_scoped_release rel;
auto logFn = [&echoes](const std::string& m) { echoes.push_back(m); };
try {
std::vector<std::unique_ptr<oscad::ASTNode>> ast = oscad::getASTFromFile(path);
oscadeval::ResolvedUseScopes used = oscadeval::resolveUseScopes(ast, path, logFn);
oscad::ParsedProgram program = oscad::getProgramFromFile(path);
oscadeval::ResolvedUseScopes used = oscadeval::resolveUseScopes(program.nodes, path, logFn);
oscadeval::Evaluator ev(logFn, nullptr, manifoldCache, oscadeval::DebugHooks{}, profile);
oscadeval::EvalContext ctx = oscadeval::EvalContext::makeRoot(used.rootScope.get());
bodies = oscadeval::toRenderableBodies(ev.evaluate(used.processedNodes, ctx, vp, generate));
Expand Down Expand Up @@ -693,8 +693,8 @@ nb::object debugEvaluate(const std::string& path, nb::dict viewportParams, nb::c
};
}
try {
std::vector<std::unique_ptr<oscad::ASTNode>> ast = oscad::getASTFromFile(path);
oscadeval::ResolvedUseScopes used = oscadeval::resolveUseScopes(ast, path, echoCpp);
oscad::ParsedProgram program = oscad::getProgramFromFile(path);
oscadeval::ResolvedUseScopes used = oscadeval::resolveUseScopes(program.nodes, path, echoCpp);
oscadeval::Evaluator ev(echoCpp, nullptr, manifoldCache, hooks, false);
evPtr = &ev;
if (fastContinueSignal) ev.setFastContinueInterruptFlag(fastContinueSignal->flag());
Expand Down Expand Up @@ -729,8 +729,8 @@ nb::list parseDecls(const std::string& path) {
std::vector<Decl> decls;
{
nb::gil_scoped_release rel;
std::vector<std::unique_ptr<oscad::ASTNode>> ast = oscad::getASTFromFile(path);
oscadeval::ResolvedUseScopes used = oscadeval::resolveUseScopes(ast, path, [](const std::string&) {});
oscad::ParsedProgram program = oscad::getProgramFromFile(path);
oscadeval::ResolvedUseScopes used = oscadeval::resolveUseScopes(program.nodes, path, [](const std::string&) {});
for (const oscad::ASTNode* n : used.processedNodes) {
const oscad::Position& p = n->position();
const char* ns = nullptr;
Expand Down
11 changes: 8 additions & 3 deletions include/openscad_cpp_evaluator/bytecode_compiler.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ namespace oscadeval {
// yet -- the caller falls back to the ordinary AST interpreter for that
// whole function, unconditionally correct since nothing about this
// function's behavior changes based on whether it happened to compile.
std::optional<CompiledChunk> tryCompileFunction(const oscad::FunctionDeclaration& decl);
std::optional<CompiledChunk> tryCompileFunction(const oscad::FunctionDeclaration& decl,
const oscad::ScopeTable* scopeTable);

// Compiles a bare STATEMENT-context expression -- an assignment's RHS, an
// if/for condition, a module-call or echo()/assert() argument -- rather
Expand Down Expand Up @@ -48,7 +49,8 @@ std::optional<CompiledChunk> tryCompileFunction(const oscad::FunctionDeclaration
// always set by buildScope() regardless of node kind) -- needed to resolve
// any callee inside it statically, the same way a function body's call
// sites are.
std::optional<CompiledChunk> tryCompileStatementExpr(const oscad::Expression& expr, const oscad::Scope* scope);
std::optional<CompiledChunk> tryCompileStatementExpr(const oscad::Expression& expr, const oscad::Scope* scope,
const oscad::ScopeTable* scopeTable);

// Compiles a run of SIBLING assignment statements sharing one scope --
// exactly Evaluator::evalChildren's own `assignments` sub-list (stmt_eval.
Expand Down Expand Up @@ -82,6 +84,7 @@ std::optional<CompiledChunk> tryCompileStatementExpr(const oscad::Expression& ex
// contained by, so it would leak into the caller's ctx.dyn permanently
// instead of just for that one assignment's own RHS.
std::optional<CompiledChunk> tryCompileAssignmentBlock(const std::vector<const oscad::Assignment*>& assigns,
const oscad::ScopeTable* scopeTable,
const oscad::Scope* scope);

// Attempts to compile `decl`'s parameter defaults + STATEMENT-list body
Expand All @@ -97,7 +100,8 @@ std::optional<CompiledChunk> tryCompileAssignmentBlock(const std::vector<const o
// construct does for tryCompileFunction; only a genuinely unsupported
// EXPRESSION (inside an if-condition/for-range/argument) can still bail
// (via tryCompileStatementExpr's own NotCompilable, propagated up).
std::optional<CompiledChunk> tryCompileModuleBody(const oscad::ModuleDeclaration& decl);
std::optional<CompiledChunk> tryCompileModuleBody(const oscad::ModuleDeclaration& decl,
const oscad::ScopeTable* scopeTable);

// Same compilation (assignment/if/for/resolved-module-call get real
// bytecode, everything else a native passthrough), but for an ARBITRARY
Expand All @@ -121,6 +125,7 @@ std::optional<CompiledChunk> tryCompileModuleBody(const oscad::ModuleDeclaration
// resolving a call site's callee -- callers pass `children.front()->
// scope()`, mirroring tryRunCompiledAssignmentBlock's own convention.
std::optional<CompiledChunk> tryCompileChildrenList(const std::vector<const oscad::ASTNode*>& children,
const oscad::ScopeTable* scopeTable,
const oscad::Scope* scope);

} // namespace oscadeval
17 changes: 16 additions & 1 deletion include/openscad_cpp_evaluator/eval_context.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "openscad_cpp_parser/ast/ast_node.hpp"
#include "openscad_cpp_parser/position.hpp"
#include "openscad_cpp_parser/scope.hpp"
#include "openscad_cpp_parser/scope_table.hpp"

#include <array>
#include <memory>
Expand Down Expand Up @@ -74,11 +75,25 @@ struct EvalContext {
// one that sees it.
bool viaChildren = false;

// Where every node's lexical Scope lives, since it cannot live in the
// node itself -- one parsed tree is shared by every script that
// includes it, and `include` puts those nodes in the INCLUDER's scope.
// Owned by the Evaluator for the length of a run; aliased, never
// copied, by every derived context.
const oscad::ScopeTable* scopeTable = nullptr;

// This node's own lexical scope, or null if none was recorded. Reads
// like the old ASTNode::scope() it replaces.
const oscad::Scope* scopeOf(const oscad::ASTNode& node) const {
return scopeTable ? scopeTable->get(node) : nullptr;
}

// The one genuinely fresh construction: seeds `dyn` with OpenSCAD's
// built-in $-variable defaults ($fn=0, $fa=12, $fs=2, $t=0,
// $parent_modules=0). Every other EvalContext in a run is derived
// from this one via the methods below.
static EvalContext makeRoot(const oscad::Scope* rootScope);
static EvalContext makeRoot(const oscad::Scope* rootScope,
const oscad::ScopeTable* scopeTable = nullptr);

// Mirrors _eval_children's direct EvalContext(...) construction: swaps
// only `scope` (to a sibling statement's own lexical scope from
Expand Down
9 changes: 9 additions & 0 deletions include/openscad_cpp_evaluator/eval_use.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ struct ResolvedUseScopes {
// these (not anything currentFile itself pulled in via `use`; "nested
// use has no effect on the base file's environment").
std::vector<const oscad::ASTNode*> ownNodesFiltered;
// Owns the ScopeTable holding every node's lexical scope -- see
// oscad::ScopeTable for why that cannot live in the nodes themselves.
std::unique_ptr<oscad::Scope> rootScope;
};

Expand All @@ -48,4 +50,11 @@ struct ResolvedUseScopes {
ResolvedUseScopes resolveUseScopes(const std::vector<std::unique_ptr<oscad::ASTNode>>& ownNodes,
const std::string& currentFile, const std::function<void(const std::string&)>& logFn);

// Same, for a statement list that is BORROWED rather than owned -- what
// oscad::ParsedProgram hands back when includes come from the shared AST
// cache. The caller must keep that ParsedProgram alive alongside the
// result, exactly as it must keep an owned AST alive.
ResolvedUseScopes resolveUseScopes(const std::vector<const oscad::ASTNode*>& ownNodes, const std::string& currentFile,
const std::function<void(const std::string&)>& logFn);

} // namespace oscadeval
11 changes: 11 additions & 0 deletions include/openscad_cpp_evaluator/evaluator.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,16 @@ class Evaluator {
// fallback.
Value evalExprMaybeCompiled(const oscad::Expression& node, EvalContext& ctx);

// The run's ScopeTable, latched from the EvalContext handed to
// evaluate()/resolveTree(). A handful of chunk-compile helpers below
// need a node's scope but are reached from the VM without an
// EvalContext in hand; they read it here instead of every caller
// growing a parameter. Null outside a run, which reads the same as the
// unset ASTNode::scope() this replaced.
const oscad::Scope* scopeOfNode(const oscad::ASTNode& node) const {
return scopeTable_ ? scopeTable_->get(node) : nullptr;
}

// Evaluates a block's statements in OpenSCAD's assignment-before-
// geometry order (all Assignment nodes first, then everything else,
// each group preserving source order), each against its own lexical
Expand Down Expand Up @@ -1191,6 +1201,7 @@ class Evaluator {
// this exists. Not a reentrancy guard (resolveTreeImpl is never called
// while another one is already active), just an on/off switch for
// whether evalExprMaybeCompiled's cache is safe to touch right now.
const oscad::ScopeTable* scopeTable_ = nullptr;
bool inResolvePass_ = false;

// True only while evalRenderExpr (or the VM's Kind::Measure bracket) is
Expand Down
9 changes: 9 additions & 0 deletions include/openscad_cpp_evaluator/value.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

namespace oscad {
class FunctionLiteral;
class Scope;
} // namespace oscad

namespace oscadeval {
Expand Down Expand Up @@ -75,6 +76,14 @@ using Value = std::variant<std::monostate, // undef
struct Closure {
const oscad::FunctionLiteral* node = nullptr;
std::shared_ptr<void> capturedLet;
// The literal's own lexical scope, captured when the closure is made.
// A node no longer carries its scope (one parsed tree is shared
// between evaluations -- see oscad::ScopeTable), and a closure can be
// called from generate time, where there is no EvalContext left to
// look it up through. Not part of identity: operator== still compares
// `node`, since the same literal always resolves to the same scope
// within one run.
const oscad::Scope* scope = nullptr;

friend bool operator==(const Closure& a, const Closure& b) { return a.node == b.node; }
};
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 = "1.3.1"
version = "1.4.0"
description = "C++ OpenSCAD evaluator with Python bindings"
readme = "README.md"
requires-python = ">=3.12"
Expand Down
2 changes: 1 addition & 1 deletion src/builtins/topology.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -873,7 +873,7 @@ std::vector<ColoredBody> generateLevelSet(Evaluator& ev, const CSGParams& params
std::optional<EvalContext> fnCtx;
std::vector<std::string> fnParams2;
if (fieldFn) {
fnCtx = EvalContext::makeRoot((*fieldFn)->node->scope());
fnCtx = EvalContext::makeRoot((*fieldFn)->scope);
for (const auto& prm : (*fieldFn)->node->parameters) fnParams2.push_back(prm->name->name);
}

Expand Down
Loading