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
4 changes: 4 additions & 0 deletions include/openscad_cpp_parser/ast/ast_node.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ enum class NodeKind {
PrimaryCall,
PrimaryIndex,
PrimaryMember,
// render() in EXPRESSION position -- evaluates its children as geometry
// and yields an object() of measurements. The STATEMENT form of render()
// is a plain ModularCall named "render"; only this one is new.
RenderExpression,
// List comprehension
ListCompLet,
ListCompEach,
Expand Down
27 changes: 27 additions & 0 deletions include/openscad_cpp_parser/ast/expression.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,33 @@ class FunctionLiteral : public Expression {
void buildScope(Scope& parentScope) override;
};

// `render()` in EXPRESSION position: `obj = render() { cube(10); };`
//
// Evaluates its children as geometry, measures the result, and yields an
// object() -- it draws nothing. The STATEMENT form (`render() cube(1);`)
// stays a plain ModularCall named "render"; this class exists only because
// an Expression cannot be a ModuleInstantiation (they are siblings under
// ASTNode, not parent/child).
//
// `arguments` and `children` deliberately mirror ModularCall's field types
// so the evaluator can hand them straight to resolveCallArgs()/evalChildren()
// with no adaptation. `children` is ASTNode, not ModuleInstantiation, for
// the same reason ModularCall's is -- a `{ ... }` block is `statement*` and
// may hold Assignments, which is why buildScope() below hoists.
class RenderExpression : public Expression {
public:
RenderExpression(Position position, std::vector<std::unique_ptr<Argument>> arguments,
std::vector<std::unique_ptr<ASTNode>> children)
: Expression(NodeKind::RenderExpression, std::move(position)), arguments(std::move(arguments)),
children(std::move(children)) {}

std::vector<std::unique_ptr<Argument>> arguments;
std::vector<std::unique_ptr<ASTNode>> children;

std::string toString() const override;
void buildScope(Scope& parentScope) override;
};

// -- Unary operators --------------------------------------------------

#define OSCAD_UNARY_OP(ClassName) \
Expand Down
1 change: 1 addition & 0 deletions src/ast/ast_node.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const char* nodeKindName(NodeKind kind) {
case NodeKind::PrimaryCall: return "PrimaryCall";
case NodeKind::PrimaryIndex: return "PrimaryIndex";
case NodeKind::PrimaryMember: return "PrimaryMember";
case NodeKind::RenderExpression: return "RenderExpression";
case NodeKind::ListCompLet: return "ListCompLet";
case NodeKind::ListCompEach: return "ListCompEach";
case NodeKind::ListCompFor: return "ListCompFor";
Expand Down
42 changes: 42 additions & 0 deletions src/ast/expression.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "openscad_cpp_parser/ast/expression.hpp"

#include "format_utils.hpp"
#include "openscad_cpp_parser/ast/scope_builder.hpp"
#include "openscad_cpp_parser/scope.hpp"

#include <array>
Expand Down Expand Up @@ -155,6 +156,47 @@ void FunctionLiteral::buildScope(Scope& parentScope) {
body->buildScope(funcScope);
}

std::string RenderExpression::toString() const {
// Deliberately NOT ModuleInstantiation's formatChildBlock, which omits
// both braces (for a lone child) and every statement terminator, because
// for a STATEMENT the terminators come from the statement-level printer.
// A RenderExpression sits inside an expression, where nothing downstream
// adds them -- and `render() cube(1)` unbraced is precisely the form that
// does NOT parse (the child_statement swallows the `;`, leaving the
// enclosing assignment unterminated). So: always braces, always a `;`
// after every child.
//
// This must stay REPARSEABLE -- pretty_print.cpp's fmtExpr falls through
// to toString() for expression kinds it has no case for, so this string
// is what a formatter emits. NodeStr.RenderExpressionRoundTrips guards it.
//
// The unconditional `;` is safe even after a child that already ends in
// `}` (a nested block, a module declaration): a bare `;` is itself a legal
// statement (parser.y's `statement: ";"`), so a redundant one is a no-op.
std::string s = "render(" + joinToString(arguments, ", ") + ") { ";
for (const auto& c : children) {
s += c->toString() + "; ";
}
return s + "}";
}

void RenderExpression::buildScope(Scope& parentScope) {
// Same shape as ModularCall::buildScope minus the name lookup (there is
// no Identifier -- "render" is a keyword token here). The hoist is
// required: `render() { x = 1; cube(x); }` must resolve x.
setScope(parentScope);
for (auto& a : arguments) {
a->buildScope(parentScope);
}
if (!children.empty()) {
Scope& childrenScope = parentScope.childScope();
collectHoistedDeclarations(children, childrenScope);
for (auto& c : children) {
c->buildScope(childrenScope);
}
}
}

// -- Operator precedence for minimal-parenthesization toString() ----------
//
// Matches the reference's nodes.py::_PREC/_lp/_rp: toString() adds parens
Expand Down
5 changes: 5 additions & 0 deletions src/grammar/driver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,11 @@ NodePtr makeListComprehension(ParserDriver& driver, const OscadLocation& loc, No
return std::make_unique<ListComprehension>(driver.toPosition(loc), std::move(elements));
}

NodePtr makeRenderExpression(ParserDriver& driver, const OscadLocation& loc, NodeList arguments, NodeList children) {
return std::make_unique<RenderExpression>(driver.toPosition(loc), nodeListCast<Argument>(std::move(arguments)),
std::move(children));
}

NodePtr makeModularCall(ParserDriver& driver, const OscadLocation& loc, const OscadLocation& nameLoc, std::string name,
NodeList arguments, NodeList children) {
auto nameNode = makeIdentifier(driver, nameLoc, std::move(name));
Expand Down
2 changes: 2 additions & 0 deletions src/grammar/driver.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ NodePtr makeListCompIfElse(ParserDriver& driver, const OscadLocation& loc, NodeP
NodePtr falseExpr);
NodePtr makeListComprehension(ParserDriver& driver, const OscadLocation& loc, NodeList elements);

NodePtr makeRenderExpression(ParserDriver& driver, const OscadLocation& loc, NodeList arguments, NodeList children);

NodePtr makeModularCall(ParserDriver& driver, const OscadLocation& loc, const OscadLocation& nameLoc, std::string name,
NodeList arguments, NodeList children);
NodePtr makeModularFor(ParserDriver& driver, const OscadLocation& loc, NodeList assignments, NodeList body);
Expand Down
5 changes: 5 additions & 0 deletions src/grammar/lexer.l
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ const std::unordered_map<std::string, KeywordFactory>& keywordTable() {
{"for", [](const OscadLocation& l) { return yy::parser::make_KW_FOR(l); }},
{"intersection_for", [](const OscadLocation& l) { return yy::parser::make_KW_INTERSECTION_FOR(l); }},
{"each", [](const OscadLocation& l) { return yy::parser::make_KW_EACH(l); }},
// Reserved so `render` can lead an EXPRESSION-position geometry block
// (`obj = render() { cube(1); };`). LALR(1) cannot otherwise tell that
// apart from a function call. Note `$render` still lexes as NAME --
// this table is keyed on the full IDENT text, including the `$`.
{"render", [](const OscadLocation& l) { return yy::parser::make_KW_RENDER(l); }},
{"undef", [](const OscadLocation& l) { return yy::parser::make_KW_UNDEF(l); }},
{"true", [](const OscadLocation& l) { return yy::parser::make_KW_TRUE(l); }},
{"false", [](const OscadLocation& l) { return yy::parser::make_KW_FALSE(l); }},
Expand Down
30 changes: 30 additions & 0 deletions src/grammar/parser.y
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
KW_FOR "for"
KW_INTERSECTION_FOR "intersection_for"
KW_EACH "each"
KW_RENDER "render"
KW_UNDEF "undef"
KW_TRUE "true"
KW_FALSE "false"
Expand Down Expand Up @@ -157,6 +158,7 @@
%type <NodePtr> modifier_show_only modifier_highlight modifier_background modifier_disable
%type <NodePtr> if_statement ifelse_statement
%type <NodePtr> modular_for modular_intersection_for modular_let modular_assert modular_echo modular_call
%type <NodePtr> render_stmt render_expr
%type <NodePtr> expr opchain postfix primary
%type <NodePtr> range_expr vector_expr vector_element
%type <NodePtr> listcomp_elements listcomp_paren_expr listcomp_let listcomp_each
Expand Down Expand Up @@ -305,6 +307,7 @@ single_module_instantiation:
| modular_assert { $$ = std::move($1); }
| modular_echo { $$ = std::move($1); }
| modular_call { $$ = std::move($1); }
| render_stmt { $$ = std::move($1); }
;

modular_for:
Expand Down Expand Up @@ -343,6 +346,16 @@ modular_call:
}
;

// `render` is a reserved keyword (see lexer.l) purely so the EXPRESSION form
// below is unambiguous. The STATEMENT form still builds a plain ModularCall
// named "render", so everything downstream -- builtin dispatch, the argument
// allowlist, the pretty-printer, json_io -- is unchanged.
render_stmt:
"render" "(" arguments ")" child_statement {
$$ = makeModularCall(driver, @$, @1, "render", std::move($3), std::move($5));
}
;

// -- Expressions ----------------------------------------------------------
//
// `expr` covers let/assert/echo/funclit_def/ternary plus the operator
Expand Down Expand Up @@ -406,6 +419,23 @@ primary:
| STRING { $$ = makeStringLiteral(driver, @$, std::move($1)); }
| NUMBER { $$ = makeNumberLiteral(driver, @$, $1); }
| NAME { $$ = makeIdentifier(driver, @$, std::move($1)); }
| render_expr { $$ = std::move($1); }
;

// Same RHS as render_stmt, reached only from `primary`. This is NOT a
// reduce/reduce conflict: LALR merges states only on identical LR(0) cores,
// and after shifting "render" the statement and expression kernels differ --
// no single state closes over both (statement-start closes over
// module_instantiation; every expr-start position closes over no statement
// nonterminal). Bison verifies this claim at build time via %expect.
//
// It lives in `primary` rather than `expr` so `render(){...}.volume` parses
// through the existing `postfix "." NAME` rule, and so it can appear as an
// argument, a list element, or an operand.
render_expr:
"render" "(" arguments ")" child_statement {
$$ = makeRenderExpression(driver, @$, std::move($3), std::move($5));
}
;

range_expr:
Expand Down
6 changes: 6 additions & 0 deletions src/inline_comment_attach.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,12 @@ void classifyNode(ASTNode& node, std::vector<ExprSlot>& exprFields, std::vector<
addAstNodeList(static_cast<ListComprehension&>(node).elements, exprFields, nonExprChildren);
break;

case NodeKind::RenderExpression: {
auto& n = static_cast<RenderExpression&>(node);
addArgumentExprList(n.arguments, exprFields);
addAstNodeList(n.children, exprFields, nonExprChildren);
break;
}
case NodeKind::ModularCall: {
auto& n = static_cast<ModularCall&>(node);
addArgumentExprList(n.arguments, exprFields);
Expand Down
11 changes: 11 additions & 0 deletions src/pretty_print.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ std::string fmtNode(const ASTNode& node, int indent, int w);
std::string fmtExpr(const ASTNode& expr, int indent, int w);
std::string fmtInst(const ASTNode& node, int indent, int w, const std::string& prefix);
std::string fmtListElem(const ASTNode& elem, int indent, int w);
std::string fmtBlock(const std::vector<std::unique_ptr<ASTNode>>& nodes, int indent, int w);

std::string fmtAssign(const Assignment& a, int indent, int w) {
return a.name->name + " = " + fmtExpr(*a.expr, indent, w);
Expand Down Expand Up @@ -508,6 +509,16 @@ std::string fmtExpr(const ASTNode& exprNode, int indent, int w) {
if (auto* lc = dynamic_cast<const ListComprehension*>(&exprNode)) {
return fmtListComprehension(*lc, indent, w);
}
if (auto* r = dynamic_cast<const RenderExpression*>(&exprNode)) {
// fmtBlock, NOT fmtChild: fmtChild drops the braces for a lone child,
// and `x = render() cube(1);` does not parse -- the child_statement
// swallows the `;` and leaves the assignment unterminated. fmtBlock
// always braces and routes children through fmtNode, which is what
// supplies their statement terminators. This arm is why toString()'s
// own (reference-matching, terminator-free) child rendering never
// reaches emitted source.
return "render(" + joinToString(r->arguments, ", ") + ") " + fmtBlock(r->children, indent, w);
}
return exprNode.toString();
}

Expand Down
12 changes: 12 additions & 0 deletions src/serialization/json_io.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,13 @@ json toJsonImpl(const ASTNode& node, bool includePos) {
j["elements"] = listToJson(static_cast<const ListComprehension&>(node).elements, includePos);
break;

case NodeKind::RenderExpression: {
auto& n = static_cast<const RenderExpression&>(node);
j["arguments"] = listToJson(n.arguments, includePos);
j["children"] = listToJson(n.children, includePos);
break;
}

case NodeKind::ModularCall: {
auto& n = static_cast<const ModularCall&>(node);
j["name"] = valueToJson(n.name.get(), includePos);
Expand Down Expand Up @@ -543,6 +550,11 @@ const std::unordered_map<std::string, Builder>& registry() {
return std::make_unique<ListComprehension>(std::move(pos), listFromJson<ASTNode>(j, "elements"));
}},

{"RenderExpression",
[](const json& j, Position pos) -> std::unique_ptr<ASTNode> {
return std::make_unique<RenderExpression>(std::move(pos), listFromJson<Argument>(j, "arguments"),
listFromJson<ASTNode>(j, "children"));
}},
{"ModularCall",
[](const json& j, Position pos) -> std::unique_ptr<ASTNode> {
return std::make_unique<ModularCall>(std::move(pos), childFromJson<Identifier>(j, "name"),
Expand Down
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ add_executable(oscad_tests
test_scope.cpp
test_ast_generation.cpp
test_node_str.cpp
test_render_expression.cpp
)
target_link_libraries(oscad_tests PRIVATE openscad_cpp_parser GTest::gtest_main)

Expand Down
Loading
Loading