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
1 change: 1 addition & 0 deletions include/openscad_cpp_parser/ast/ast_node.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ enum class NodeKind {
LetOp,
EchoOp,
AssertOp,
ProfileTimeOp,
FunctionLiteral,
// Operators
UnaryMinusOp,
Expand Down
14 changes: 14 additions & 0 deletions include/openscad_cpp_parser/ast/expression.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,20 @@ class EchoOp : public Expression {
void buildScope(Scope& parentScope) override;
};

// `profile_time(arguments...) body` -- same shape as EchoOp; times the body.
// Unlike echo/assert the body is required: timing nothing is meaningless.
class ProfileTimeOp : public Expression {
public:
ProfileTimeOp(Position position, std::vector<std::unique_ptr<Argument>> arguments, std::unique_ptr<Expression> body)
: Expression(NodeKind::ProfileTimeOp, std::move(position)), arguments(std::move(arguments)), body(std::move(body)) {}

std::vector<std::unique_ptr<Argument>> arguments;
std::unique_ptr<Expression> body;

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

// `assert(arguments...) body` -- same shape as EchoOp.
class AssertOp : public Expression {
public:
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 @@ -21,6 +21,7 @@ const char* nodeKindName(NodeKind kind) {
case NodeKind::LetOp: return "LetOp";
case NodeKind::EchoOp: return "EchoOp";
case NodeKind::AssertOp: return "AssertOp";
case NodeKind::ProfileTimeOp: return "ProfileTimeOp";
case NodeKind::FunctionLiteral: return "FunctionLiteral";
case NodeKind::UnaryMinusOp: return "UnaryMinusOp";
case NodeKind::AdditionOp: return "AdditionOp";
Expand Down
13 changes: 13 additions & 0 deletions src/ast/expression.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,18 @@ void EchoOp::buildScope(Scope& parentScope) {
body->buildScope(parentScope);
}

std::string ProfileTimeOp::toString() const {
return "profile_time(" + joinToString(arguments, ", ") + ") " + body->toString();
}

void ProfileTimeOp::buildScope(Scope& parentScope) {
setScope(parentScope);
for (auto& a : arguments) {
a->buildScope(parentScope);
}
body->buildScope(parentScope);
}

std::string AssertOp::toString() const {
return "assert(" + joinToString(arguments, ", ") + ") " + body->toString();
}
Expand Down Expand Up @@ -219,6 +231,7 @@ int operatorPrecedence(NodeKind kind) {
case NodeKind::LetOp:
case NodeKind::EchoOp:
case NodeKind::AssertOp:
case NodeKind::ProfileTimeOp:
case NodeKind::FunctionLiteral:
return 0;
case NodeKind::LogicalOrOp:
Expand Down
5 changes: 5 additions & 0 deletions src/grammar/driver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ NodePtr makeEchoOp(ParserDriver& driver, const OscadLocation& loc, NodeList argu
nodeCast<Expression>(std::move(body)));
}

NodePtr makeProfileTimeOp(ParserDriver& driver, const OscadLocation& loc, NodeList arguments, NodePtr body) {
return std::make_unique<ProfileTimeOp>(driver.toPosition(loc), nodeListCast<Argument>(std::move(arguments)),
nodeCast<Expression>(std::move(body)));
}

NodePtr makeAssertOp(ParserDriver& driver, const OscadLocation& loc, NodeList arguments, NodePtr body) {
return std::make_unique<AssertOp>(driver.toPosition(loc), nodeListCast<Argument>(std::move(arguments)),
nodeCast<Expression>(std::move(body)));
Expand Down
1 change: 1 addition & 0 deletions src/grammar/driver.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ NodePtr makeAssignment(ParserDriver& driver, const OscadLocation& loc, const Osc

NodePtr makeLetOp(ParserDriver& driver, const OscadLocation& loc, NodeList assignments, NodePtr body);
NodePtr makeEchoOp(ParserDriver& driver, const OscadLocation& loc, NodeList arguments, NodePtr body);
NodePtr makeProfileTimeOp(ParserDriver& driver, const OscadLocation& loc, NodeList arguments, NodePtr body);
NodePtr makeAssertOp(ParserDriver& driver, const OscadLocation& loc, NodeList arguments, NodePtr body);
NodePtr makeFunctionLiteral(ParserDriver& driver, const OscadLocation& loc, NodeList parameters, NodePtr body);

Expand Down
3 changes: 3 additions & 0 deletions src/grammar/lexer.l
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ const std::unordered_map<std::string, KeywordFactory>& keywordTable() {
// 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); }},
// Reserved for the same reason as `render`: its expression form
// (`x = profile_time("label") expr;`) starts like a function call.
{"profile_time", [](const OscadLocation& l) { return yy::parser::make_KW_PROFILE_TIME(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
13 changes: 12 additions & 1 deletion src/grammar/parser.y
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
KW_INTERSECTION_FOR "intersection_for"
KW_EACH "each"
KW_RENDER "render"
KW_PROFILE_TIME "profile_time"
KW_UNDEF "undef"
KW_TRUE "true"
KW_FALSE "false"
Expand Down Expand Up @@ -158,7 +159,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> render_stmt render_expr profile_time_stmt
%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 @@ -330,6 +331,7 @@ single_module_instantiation:
| modular_echo { $$ = std::move($1); }
| modular_call { $$ = std::move($1); }
| render_stmt { $$ = std::move($1); }
| profile_time_stmt { $$ = std::move($1); }
;

modular_for:
Expand Down Expand Up @@ -378,6 +380,14 @@ render_stmt:
}
;

// Same arrangement as render_stmt: the keyword exists only for the
// expression form, and the statement form is an ordinary builtin call.
profile_time_stmt:
"profile_time" "(" arguments ")" child_statement {
$$ = makeModularCall(driver, @$, @1, "profile_time", std::move($3), std::move($5));
}
;

// -- Expressions ----------------------------------------------------------
//
// `expr` covers let/assert/echo/funclit_def/ternary plus the operator
Expand All @@ -393,6 +403,7 @@ expr:
| "assert" "(" arguments ")" { $$ = makeAssertOp(driver, @$, std::move($3), makeUndefinedLiteral(driver, @$)); }
| "echo" "(" arguments ")" expr { $$ = makeEchoOp(driver, @$, std::move($3), std::move($5)); }
| "echo" "(" arguments ")" { $$ = makeEchoOp(driver, @$, std::move($3), makeUndefinedLiteral(driver, @$)); }
| "profile_time" "(" arguments ")" expr { $$ = makeProfileTimeOp(driver, @$, std::move($3), std::move($5)); }
| "function" "(" parameters ")" expr { $$ = makeFunctionLiteral(driver, @$, std::move($3), std::move($5)); }
| opchain "?" expr ":" expr { $$ = makeTernaryOp(driver, @$, std::move($1), std::move($3), std::move($5)); }
| opchain { $$ = std::move($1); }
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 @@ -180,6 +180,12 @@ void classifyNode(ASTNode& node, std::vector<ExprSlot>& exprFields, std::vector<
addExpr(n.body, exprFields);
break;
}
case NodeKind::ProfileTimeOp: {
auto& n = static_cast<ProfileTimeOp&>(node);
addArgumentExprList(n.arguments, exprFields);
addExpr(n.body, exprFields);
break;
}
case NodeKind::AssertOp: {
auto& n = static_cast<AssertOp&>(node);
addArgumentExprList(n.arguments, exprFields);
Expand Down
3 changes: 3 additions & 0 deletions src/pretty_print.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,9 @@ std::string fmtExpr(const ASTNode& exprNode, int indent, int w) {
}
return "echo(" + args + ")\n" + pad + fmtExpr(*e->body, indent, w);
}
if (auto* p = dynamic_cast<const ProfileTimeOp*>(&exprNode)) {
return "profile_time(" + joinToString(p->arguments, ", ") + ")\n" + pad + fmtExpr(*p->body, indent, w);
}
if (auto* l = dynamic_cast<const LetOp*>(&exprNode)) {
return fmtLetOpExpr(*l, indent, w);
}
Expand Down
11 changes: 11 additions & 0 deletions src/serialization/json_io.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,12 @@ json toJsonImpl(const ASTNode& node, bool includePos) {
j["body"] = valueToJson(n.body.get(), includePos);
break;
}
case NodeKind::ProfileTimeOp: {
auto& n = static_cast<const ProfileTimeOp&>(node);
j["arguments"] = listToJson(n.arguments, includePos);
j["body"] = valueToJson(n.body.get(), includePos);
break;
}
case NodeKind::AssertOp: {
auto& n = static_cast<const AssertOp&>(node);
j["arguments"] = listToJson(n.arguments, includePos);
Expand Down Expand Up @@ -446,6 +452,11 @@ const std::unordered_map<std::string, Builder>& registry() {
return std::make_unique<EchoOp>(std::move(pos), listFromJson<Argument>(j, "arguments"),
childFromJson<Expression>(j, "body"));
}},
{"ProfileTimeOp",
[](const json& j, Position pos) -> std::unique_ptr<ASTNode> {
return std::make_unique<ProfileTimeOp>(std::move(pos), listFromJson<Argument>(j, "arguments"),
childFromJson<Expression>(j, "body"));
}},
{"AssertOp",
[](const json& j, Position pos) -> std::unique_ptr<ASTNode> {
return std::make_unique<AssertOp>(std::move(pos), listFromJson<Argument>(j, "arguments"),
Expand Down
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ add_executable(oscad_tests
test_ast_generation.cpp
test_node_str.cpp
test_render_expression.cpp
test_profile_time.cpp
test_strict_commas.cpp
test_concurrent_parse.cpp
)
Expand Down
86 changes: 86 additions & 0 deletions tests/test_profile_time.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// `profile_time()`: the statement form times its children, the expression
// form (`x = profile_time("label") expr;`) its body. `profile_time` is a
// reserved keyword only because the expression form would otherwise be
// indistinguishable from a function call; the statement form is a plain
// ModularCall, so builtin dispatch downstream sees an ordinary module.

#include "openscad_cpp_parser/api.hpp"
#include "openscad_cpp_parser/pretty_print.hpp"
#include "openscad_cpp_parser/serialization.hpp"
#include "test_helpers.hpp"

#include <gtest/gtest.h>

using namespace oscad;

namespace {

const ProfileTimeOp* profileOf(const std::vector<std::unique_ptr<ASTNode>>& ast) {
auto* a = dynamic_cast<Assignment*>(ast[0].get());
return a ? dynamic_cast<const ProfileTimeOp*>(a->expr.get()) : nullptr;
}

} // namespace

TEST(ProfileTime, StatementFormIsAPlainModularCall) {
auto ast = parseSrc("profile_time(\"gears\") { cube(1); sphere(2); }");
ASSERT_EQ(ast.size(), 1u);
auto* call = dynamic_cast<ModularCall*>(ast[0].get());
ASSERT_NE(call, nullptr);
EXPECT_EQ(call->name->name, "profile_time");
EXPECT_EQ(call->arguments.size(), 1u);
EXPECT_EQ(call->children.size(), 2u);
}

TEST(ProfileTime, ExpressionFormWrapsItsBody) {
auto ast = parseSrc("x = profile_time(\"sum\") 1 + 2;");
const ProfileTimeOp* p = profileOf(ast);
ASSERT_NE(p, nullptr);
EXPECT_EQ(p->kind(), NodeKind::ProfileTimeOp);
EXPECT_EQ(p->arguments.size(), 1u);
EXPECT_EQ(p->body->kind(), NodeKind::AdditionOp);
}

TEST(ProfileTime, ExpressionFormAllowsNoLabel) {
const ProfileTimeOp* p = nullptr;
auto ast = parseSrc("x = profile_time() f(3);");
p = profileOf(ast);
ASSERT_NE(p, nullptr);
EXPECT_TRUE(p->arguments.empty());
}

TEST(ProfileTime, ExpressionFormNeedsABody) {
EXPECT_THROW(parseSrc("x = profile_time(\"a\");"), ParseError);
}

TEST(ProfileTime, IsReserved) {
EXPECT_THROW(parseSrc("profile_time = 1;"), ParseError);
EXPECT_THROW(parseSrc("function profile_time() = 1;"), ParseError);
EXPECT_THROW(parseSrc("module profile_time() {}"), ParseError);
// `$profile_time` is a different identifier and stays usable.
EXPECT_NO_THROW(parseSrc("$profile_time = 1;"));
}

TEST(ProfileTime, ToStringRoundTrips) {
auto ast = parseSrc("x = profile_time(\"a\") 1 + 2;");
EXPECT_EQ(ast[0]->toString(), "x = profile_time(\"a\") 1 + 2");
EXPECT_NE(profileOf(parseSrc(ast[0]->toString() + ";")), nullptr);
}

TEST(ProfileTime, JsonRoundTrips) {
auto ast = parseSrc("x = profile_time(\"a\") 1 + 2;");
auto back = astFromJsonString(astToJsonString(ast));
const ProfileTimeOp* p = profileOf(back);
ASSERT_NE(p, nullptr);
EXPECT_EQ(p->body->kind(), NodeKind::AdditionOp);
}

TEST(ProfileTime, PrettyPrintReparses) {
for (const char* src : {"x = profile_time(\"a\") 1 + 2;",
"profile_time(\"g\") { cube(1); }"}) {
auto ast = parseSrc(src);
std::string out = toOpenscad(ast);
EXPECT_NE(out.find("profile_time(\""), std::string::npos) << out;
EXPECT_NO_THROW(parseSrc(out)) << out;
}
}
Loading