diff --git a/include/openscad_cpp_parser/ast/ast_node.hpp b/include/openscad_cpp_parser/ast/ast_node.hpp index 578a823..ac301be 100644 --- a/include/openscad_cpp_parser/ast/ast_node.hpp +++ b/include/openscad_cpp_parser/ast/ast_node.hpp @@ -34,6 +34,7 @@ enum class NodeKind { LetOp, EchoOp, AssertOp, + ProfileTimeOp, FunctionLiteral, // Operators UnaryMinusOp, diff --git a/include/openscad_cpp_parser/ast/expression.hpp b/include/openscad_cpp_parser/ast/expression.hpp index 456d499..647b78a 100644 --- a/include/openscad_cpp_parser/ast/expression.hpp +++ b/include/openscad_cpp_parser/ast/expression.hpp @@ -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> arguments, std::unique_ptr body) + : Expression(NodeKind::ProfileTimeOp, std::move(position)), arguments(std::move(arguments)), body(std::move(body)) {} + + std::vector> arguments; + std::unique_ptr body; + + std::string toString() const override; + void buildScope(Scope& parentScope) override; +}; + // `assert(arguments...) body` -- same shape as EchoOp. class AssertOp : public Expression { public: diff --git a/src/ast/ast_node.cpp b/src/ast/ast_node.cpp index 7e6edf4..8bd8661 100644 --- a/src/ast/ast_node.cpp +++ b/src/ast/ast_node.cpp @@ -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"; diff --git a/src/ast/expression.cpp b/src/ast/expression.cpp index 84875af..dcc9f72 100644 --- a/src/ast/expression.cpp +++ b/src/ast/expression.cpp @@ -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(); } @@ -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: diff --git a/src/grammar/driver.cpp b/src/grammar/driver.cpp index fb6a26d..f5003e3 100644 --- a/src/grammar/driver.cpp +++ b/src/grammar/driver.cpp @@ -88,6 +88,11 @@ NodePtr makeEchoOp(ParserDriver& driver, const OscadLocation& loc, NodeList argu nodeCast(std::move(body))); } +NodePtr makeProfileTimeOp(ParserDriver& driver, const OscadLocation& loc, NodeList arguments, NodePtr body) { + return std::make_unique(driver.toPosition(loc), nodeListCast(std::move(arguments)), + nodeCast(std::move(body))); +} + NodePtr makeAssertOp(ParserDriver& driver, const OscadLocation& loc, NodeList arguments, NodePtr body) { return std::make_unique(driver.toPosition(loc), nodeListCast(std::move(arguments)), nodeCast(std::move(body))); diff --git a/src/grammar/driver.hpp b/src/grammar/driver.hpp index 4352fc8..100153b 100644 --- a/src/grammar/driver.hpp +++ b/src/grammar/driver.hpp @@ -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); diff --git a/src/grammar/lexer.l b/src/grammar/lexer.l index 0346380..3817af5 100644 --- a/src/grammar/lexer.l +++ b/src/grammar/lexer.l @@ -52,6 +52,9 @@ const std::unordered_map& 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); }}, diff --git a/src/grammar/parser.y b/src/grammar/parser.y index 4196841..a51bcf7 100644 --- a/src/grammar/parser.y +++ b/src/grammar/parser.y @@ -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" @@ -158,7 +159,7 @@ %type modifier_show_only modifier_highlight modifier_background modifier_disable %type if_statement ifelse_statement %type modular_for modular_intersection_for modular_let modular_assert modular_echo modular_call -%type render_stmt render_expr +%type render_stmt render_expr profile_time_stmt %type expr opchain postfix primary %type range_expr vector_expr vector_element %type listcomp_elements listcomp_paren_expr listcomp_let listcomp_each @@ -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: @@ -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 @@ -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); } diff --git a/src/inline_comment_attach.cpp b/src/inline_comment_attach.cpp index 0e305e5..65bf430 100644 --- a/src/inline_comment_attach.cpp +++ b/src/inline_comment_attach.cpp @@ -180,6 +180,12 @@ void classifyNode(ASTNode& node, std::vector& exprFields, std::vector< addExpr(n.body, exprFields); break; } + case NodeKind::ProfileTimeOp: { + auto& n = static_cast(node); + addArgumentExprList(n.arguments, exprFields); + addExpr(n.body, exprFields); + break; + } case NodeKind::AssertOp: { auto& n = static_cast(node); addArgumentExprList(n.arguments, exprFields); diff --git a/src/pretty_print.cpp b/src/pretty_print.cpp index 380fdfd..9241923 100644 --- a/src/pretty_print.cpp +++ b/src/pretty_print.cpp @@ -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(&exprNode)) { + return "profile_time(" + joinToString(p->arguments, ", ") + ")\n" + pad + fmtExpr(*p->body, indent, w); + } if (auto* l = dynamic_cast(&exprNode)) { return fmtLetOpExpr(*l, indent, w); } diff --git a/src/serialization/json_io.cpp b/src/serialization/json_io.cpp index c5faf7d..9b7950f 100644 --- a/src/serialization/json_io.cpp +++ b/src/serialization/json_io.cpp @@ -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(node); + j["arguments"] = listToJson(n.arguments, includePos); + j["body"] = valueToJson(n.body.get(), includePos); + break; + } case NodeKind::AssertOp: { auto& n = static_cast(node); j["arguments"] = listToJson(n.arguments, includePos); @@ -446,6 +452,11 @@ const std::unordered_map& registry() { return std::make_unique(std::move(pos), listFromJson(j, "arguments"), childFromJson(j, "body")); }}, + {"ProfileTimeOp", + [](const json& j, Position pos) -> std::unique_ptr { + return std::make_unique(std::move(pos), listFromJson(j, "arguments"), + childFromJson(j, "body")); + }}, {"AssertOp", [](const json& j, Position pos) -> std::unique_ptr { return std::make_unique(std::move(pos), listFromJson(j, "arguments"), diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 5dc90a1..bdae19d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -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 ) diff --git a/tests/test_profile_time.cpp b/tests/test_profile_time.cpp new file mode 100644 index 0000000..69feec5 --- /dev/null +++ b/tests/test_profile_time.cpp @@ -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 + +using namespace oscad; + +namespace { + +const ProfileTimeOp* profileOf(const std::vector>& ast) { + auto* a = dynamic_cast(ast[0].get()); + return a ? dynamic_cast(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(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; + } +}