diff --git a/CMakeLists.txt b/CMakeLists.txt index 4f23ec1..5f8ca10 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,11 +1,18 @@ cmake_minimum_required(VERSION 3.26) project(cura-formulae-engine) +include(CTest) + find_package(standardprojectsettings REQUIRED) option(ENABLE_TESTS "Build with unit test" ON) option(EXTENSIVE_WARNINGS "Build with all warnings" ON) option(ENABLE_APPS "Build apps example" ON) +if (ENABLE_TESTS AND NOT BUILD_TESTING) + message(STATUS "ENABLE_TESTS is ON, forcing BUILD_TESTING=ON so CTest can discover tests") + set(BUILD_TESTING ON CACHE BOOL "Enable CTest" FORCE) +endif () + find_package(spdlog REQUIRED) find_package(lexy REQUIRED) find_package(zeus_expected REQUIRED) @@ -59,6 +66,7 @@ set(CURA_FORMULAE_ENGINE__SRC src/ast/slice_expr.cpp src/ast/tuple_expr.cpp src/ast/variable_expr.cpp + src/ast/property_access_expr.cpp src/parser/parser.cpp src/ast/binary_expr/binary_expr.cpp src/ast/binary_expr/add_expr.cpp diff --git a/include/cura-formulae-engine/ast/fn_application_expr.h b/include/cura-formulae-engine/ast/fn_application_expr.h index bb9b0a6..6d0d51d 100644 --- a/include/cura-formulae-engine/ast/fn_application_expr.h +++ b/include/cura-formulae-engine/ast/fn_application_expr.h @@ -8,12 +8,25 @@ namespace CuraFormulaeEngine::ast struct FnApplicationExpr final : Expr { + struct KeywordArg + { + std::string name; + ExprPtr value; + + [[nodiscard]] bool deepEq(const KeywordArg& other) const noexcept + { + return name == other.name && value.deepEq(other.value); + } + }; + ExprPtr fn; std::vector args; + std::vector kwargs; - FnApplicationExpr(ExprPtr fn, std::vector args) + FnApplicationExpr(ExprPtr fn, std::vector args, std::vector kwargs = {}) : fn(std::move(fn)) , args(std::move(args)) + , kwargs(std::move(kwargs)) { } diff --git a/include/cura-formulae-engine/ast/property_access_expr.h b/include/cura-formulae-engine/ast/property_access_expr.h new file mode 100644 index 0000000..9225b00 --- /dev/null +++ b/include/cura-formulae-engine/ast/property_access_expr.h @@ -0,0 +1,33 @@ +#pragma once + +#include "cura-formulae-engine/ast/ast.h" +#include "expr_ptr.h" + +#include + +namespace CuraFormulaeEngine::ast +{ + +struct PropertyAccessExpr final : Expr +{ + ExprPtr object; + std::string property; + + PropertyAccessExpr(ExprPtr object, std::string property) + : object(std::move(object)) + , property(std::move(property)) + { + } + + [[nodiscard]] std::string toString() const noexcept final; + + [[nodiscard]] eval::Result evaluate(const env::Environment* environment) const noexcept final; + + [[nodiscard]] std::unordered_set freeVariables() const noexcept final; + + [[nodiscard]] bool deepEq(const Expr& other) const noexcept final; + + void visitAll(std::function visitor) const noexcept final; +}; + +} // namespace CuraFormulaeEngine::ast diff --git a/include/cura-formulae-engine/env/int_fn.h b/include/cura-formulae-engine/env/int_fn.h index 1997112..d9b2671 100644 --- a/include/cura-formulae-engine/env/int_fn.h +++ b/include/cura-formulae-engine/env/int_fn.h @@ -2,9 +2,19 @@ #include "cura-formulae-engine/eval.h" +#include +#include + namespace CuraFormulaeEngine::env { +struct IntFunction +{ + [[nodiscard]] eval::Result operator()(const std::vector& args) const noexcept; + [[nodiscard]] std::vector getSignature() const noexcept; +}; + +extern const IntFunction int_function; extern const eval::Value::fn_t int_fn; } // namespace CuraFormulaeEngine::env diff --git a/include/cura-formulae-engine/env/math_log.h b/include/cura-formulae-engine/env/math_log.h index 2f9356e..63ea17b 100644 --- a/include/cura-formulae-engine/env/math_log.h +++ b/include/cura-formulae-engine/env/math_log.h @@ -2,9 +2,19 @@ #include "cura-formulae-engine/eval.h" +#include +#include + namespace CuraFormulaeEngine::env { +struct MathLogFunction +{ + [[nodiscard]] eval::Result operator()(const std::vector& args) const noexcept; + [[nodiscard]] std::vector getSignature() const noexcept; +}; + +extern const MathLogFunction math_log_function; extern const eval::Value::fn_t math_log; } // namespace CuraFormulaeEngine::env diff --git a/include/cura-formulae-engine/env/min.h b/include/cura-formulae-engine/env/min.h index 5a03c6f..a0f6a94 100644 --- a/include/cura-formulae-engine/env/min.h +++ b/include/cura-formulae-engine/env/min.h @@ -2,9 +2,19 @@ #include "cura-formulae-engine/eval.h" +#include +#include + namespace CuraFormulaeEngine::env { +struct MinFunction +{ + [[nodiscard]] eval::Result operator()(const std::vector& args) const noexcept; + [[nodiscard]] std::vector getSignature() const noexcept; +}; + +extern const MinFunction min_function; extern const eval::Value::fn_t min; } // namespace CuraFormulaeEngine::env diff --git a/include/cura-formulae-engine/env/round.h b/include/cura-formulae-engine/env/round.h index ef665b3..0b52a06 100644 --- a/include/cura-formulae-engine/env/round.h +++ b/include/cura-formulae-engine/env/round.h @@ -2,9 +2,19 @@ #include "cura-formulae-engine/eval.h" +#include +#include + namespace CuraFormulaeEngine::env { +struct RoundFunction +{ + [[nodiscard]] eval::Result operator()(const std::vector& args) const noexcept; + [[nodiscard]] std::vector getSignature() const noexcept; +}; + +extern const RoundFunction round_function; extern const eval::Value::fn_t round; } // namespace CuraFormulaeEngine::env diff --git a/include/cura-formulae-engine/eval.h b/include/cura-formulae-engine/eval.h index 82626b2..ed063a3 100644 --- a/include/cura-formulae-engine/eval.h +++ b/include/cura-formulae-engine/eval.h @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -44,7 +45,31 @@ struct Value { using fn_t = std::function&)>; - std::variant, fn_t, std::nullptr_t> value = nullptr; + struct rich_fn_t + { + fn_t operation; + std::vector signature; + + [[nodiscard]] Result operator()(const std::vector& args) const noexcept + { + if (! operation) + { + return zeus::unexpected(Error::TypeMismatch); + } + return operation(args); + } + + [[nodiscard]] std::optional> getSignature() const noexcept + { + if (signature.empty()) + { + return std::nullopt; + } + return signature; + } + }; + + std::variant, fn_t, rich_fn_t, std::unordered_map, std::nullptr_t> value = nullptr; Value() noexcept = default; @@ -78,6 +103,16 @@ struct Value { } + Value(const rich_fn_t& value) noexcept + : value{ value } + { + } + + Value(const std::unordered_map& value) noexcept + : value{ value } + { + } + Value(const std::nullptr_t& value) noexcept : value{ value } { diff --git a/include/cura-formulae-engine/formula.md b/include/cura-formulae-engine/formula.md index 56d80ee..9041ecc 100644 --- a/include/cura-formulae-engine/formula.md +++ b/include/cura-formulae-engine/formula.md @@ -33,7 +33,7 @@ parsing parts. For instance, function application and array indexing. The gramma similar. ``` -FN APPLICATION : { variable } '(' { expression } ')' +FN APPLICATION : { variable } '(' { expression | identifier '=' expression } ')' ARRAY INDEXING : { variable } '[' { expression } ']' ``` diff --git a/include/cura-formulae-engine/parser/math_expr_grammar.h b/include/cura-formulae-engine/parser/math_expr_grammar.h index f05ab9c..b43d9a1 100644 --- a/include/cura-formulae-engine/parser/math_expr_grammar.h +++ b/include/cura-formulae-engine/parser/math_expr_grammar.h @@ -12,12 +12,6 @@ #include "cura-formulae-engine/ast/expr_ptr.h" #include "cura-formulae-engine/ast/unary_expr/neg_expr.h" #include "cura-formulae-engine/ast/unary_expr/not_expr.h" -#include "bool_grammar.h" -#include "list_grammar.h" -#include "none_grammar.h" -#include "number_grammar.h" -#include "parens_grammar.h" -#include "string_grammar.h" #include "variable_or_fn_application_grammar_or_array_indexing_grammar.h" #include @@ -35,13 +29,7 @@ struct MathExprGrammar : lexy::expression_production // clang-format off static constexpr auto atom - = lexy::dsl::p - | lexy::dsl::p - | lexy::dsl::p - | lexy::dsl::p - | lexy::dsl::p - | lexy::dsl::p - | lexy::dsl::p; + = lexy::dsl::p; // clang-format on static constexpr auto op_pow = lexy::dsl::op(LEXY_LIT("**")); diff --git a/include/cura-formulae-engine/parser/variable_grammar.h b/include/cura-formulae-engine/parser/variable_grammar.h index a25262a..82d1885 100644 --- a/include/cura-formulae-engine/parser/variable_grammar.h +++ b/include/cura-formulae-engine/parser/variable_grammar.h @@ -11,7 +11,7 @@ namespace CuraFormulaeEngine::parser struct VariableGrammar : lexy::token_production { - static constexpr auto rule = lexy::dsl::identifier(lexy::dsl::ascii::alpha_digit_underscore / lexy::dsl::lit_c<'.'>); + static constexpr auto rule = lexy::dsl::identifier(lexy::dsl::ascii::alpha_digit_underscore); static constexpr auto value = lexy::callback([](const auto&& variable) { return ast::ExprPtr(std::make_unique(std::string(variable.begin(), variable.end()))); diff --git a/include/cura-formulae-engine/parser/variable_or_fn_application_grammar_or_array_indexing_grammar.h b/include/cura-formulae-engine/parser/variable_or_fn_application_grammar_or_array_indexing_grammar.h index 3ddbe81..97b18aa 100644 --- a/include/cura-formulae-engine/parser/variable_or_fn_application_grammar_or_array_indexing_grammar.h +++ b/include/cura-formulae-engine/parser/variable_or_fn_application_grammar_or_array_indexing_grammar.h @@ -3,8 +3,15 @@ #include "cura-formulae-engine/ast/expr_ptr.h" #include "cura-formulae-engine/ast/fn_application_expr.h" #include "cura-formulae-engine/ast/index_expr.h" +#include "cura-formulae-engine/ast/property_access_expr.h" #include "cura-formulae-engine/ast/slice_expr.h" +#include "bool_grammar.h" +#include "list_grammar.h" #include "nested_grammar.h" +#include "none_grammar.h" +#include "number_grammar.h" +#include "parens_grammar.h" +#include "string_grammar.h" #include "variable_grammar.h" #include @@ -14,6 +21,8 @@ #include #include #include +#include +#include namespace CuraFormulaeEngine::parser { @@ -67,6 +76,8 @@ struct ApplySliceExpr final : ApplyExpr struct ApplyFnApplicationExpr final : ApplyExpr { + using FnArgElement = std::variant; + ApplyFnApplicationExpr() = default; ApplyFnApplicationExpr(std::vector&& args) @@ -74,9 +85,45 @@ struct ApplyFnApplicationExpr final : ApplyExpr { } + ApplyFnApplicationExpr(std::vector&& arg_elements) + { + for (auto& arg : arg_elements) + { + if (std::holds_alternative(arg)) + { + args.push_back(std::move(std::get(arg))); + } + else + { + kwargs.push_back(std::move(std::get(arg))); + } + } + } + std::vector args; + std::vector kwargs; - ast::ExprPtr apply(ast::ExprPtr&& variable) override { return { std::make_unique(std::move(variable), std::move(args)) }; } + ast::ExprPtr apply(ast::ExprPtr&& variable) override + { + return { std::make_unique(std::move(variable), std::move(args), std::move(kwargs)) }; + } +}; + +struct ApplyPropertyAccessExpr final : ApplyExpr +{ + ApplyPropertyAccessExpr() = default; + + explicit ApplyPropertyAccessExpr(std::string property_name) + : property_name(std::move(property_name)) + { + } + + std::string property_name; + + ast::ExprPtr apply(ast::ExprPtr&& object) override + { + return { std::make_unique(std::move(object), property_name) }; + } }; struct VariableOrFnApplicationGrammarOrArrayIndexingGrammar : lexy::token_production @@ -95,14 +142,39 @@ struct VariableOrFnApplicationGrammarOrArrayIndexingGrammar : lexy::token_produc struct FnApplicationGrammar : token_production { + struct FnKeywordArgGrammar : token_production + { + static constexpr auto rule + = lexy::dsl::peek(lexy::dsl::identifier(lexy::dsl::ascii::alpha_digit_underscore) >> lexy::dsl::lit_c<'='> >> lexy::dsl::peek_not(lexy::dsl::lit_c<'='>)) + >> lexy::dsl::identifier(lexy::dsl::ascii::alpha_digit_underscore) + lexy::dsl::lit_c<'='> + lexy::dsl::p; + + static constexpr auto value = lexy::callback( + [](const auto& identifier, ast::ExprPtr value) + { + ast::FnApplicationExpr::KeywordArg kwarg; + kwarg.name = std::string(identifier.begin(), identifier.end()); + kwarg.value = std::move(value); + return kwarg; + }); + }; + + struct FnArgElementGrammar : token_production + { + static constexpr auto rule = lexy::dsl::p | (lexy::dsl::else_ >> lexy::dsl::p); + static constexpr auto value = lexy::callback( + [](ast::FnApplicationExpr::KeywordArg kwarg) -> ApplyFnApplicationExpr::FnArgElement { return kwarg; }, + [](ast::ExprPtr arg) -> ApplyFnApplicationExpr::FnArgElement { return arg; }); + }; + struct FnApplicationGrammarInner : token_production { static constexpr auto whitespace = lexy::dsl::whitespace(lexy::dsl::ascii::space); - static constexpr auto rule = lexy::dsl::round_bracketed.opt_list(lexy::dsl::p, lexy::dsl::ignore_trailing_sep(lexy::dsl::comma)); + static constexpr auto rule = lexy::dsl::round_bracketed.opt_list(lexy::dsl::p, lexy::dsl::ignore_trailing_sep(lexy::dsl::comma)); static constexpr auto value - = lexy::as_list> >> lexy::callback>( + = lexy::as_list> >> lexy::callback>( [](lexy::nullopt = {}) -> std::unique_ptr { return std::make_unique(); }, - [](std::vector args) -> std::unique_ptr { return std::make_unique(std::move(args)); }); + [](std::vector args) -> std::unique_ptr + { return std::make_unique(std::move(args)); }); }; static constexpr auto rule = lexy::dsl::peek(lexy::dsl::lit_c<'('>) >> lexy::dsl::p; @@ -122,13 +194,36 @@ struct VariableOrFnApplicationGrammarOrArrayIndexingGrammar : lexy::token_produc { return std::make_unique(std::move(start_index), std::move(end_index), std::move(step)); }); }; + struct PropertyAccessGrammar : token_production + { + static constexpr auto rule = lexy::dsl::peek(lexy::dsl::lit_c<'.'>) + >> lexy::dsl::lit_c<'.'> + lexy::dsl::identifier(lexy::dsl::ascii::alpha_digit_underscore); + static constexpr auto value = lexy::callback>( + [](const auto& property_name) + { + return std::make_unique(std::string(property_name.begin(), property_name.end())); + }); + }; + struct List : token_production { - static constexpr auto rule = lexy::dsl::list(lexy::dsl::p | lexy::dsl::p); + static constexpr auto rule = lexy::dsl::list(lexy::dsl::p | lexy::dsl::p | lexy::dsl::p); static constexpr auto value = lexy::as_list>>; }; - static constexpr auto rule = lexy::dsl::p >> lexy::dsl::if_(lexy::dsl::peek(lexy::dsl::lit_c<'['> | lexy::dsl::lit_c<'('>) >> lexy::dsl::p); + struct PrimaryExpr : token_production + { + static constexpr auto rule = lexy::dsl::p + | lexy::dsl::p + | lexy::dsl::p + | lexy::dsl::p + | lexy::dsl::p + | lexy::dsl::p; + static constexpr auto value = lexy::forward; + }; + + static constexpr auto rule = (lexy::dsl::p >> lexy::dsl::if_(lexy::dsl::peek(lexy::dsl::lit_c<'['> | lexy::dsl::lit_c<'('> | lexy::dsl::lit_c<'.'>) >> lexy::dsl::p)) + | (lexy::dsl::p >> lexy::dsl::if_(lexy::dsl::peek(lexy::dsl::lit_c<'['> | lexy::dsl::lit_c<'('> | lexy::dsl::lit_c<'.'>) >> lexy::dsl::p)); static constexpr auto value = lexy::callback( [](auto&& expr) { return std::forward(expr); }, [](auto&& expr, lexy::nullopt) { return std::forward(expr); }, diff --git a/src/ast/fn_application_expr.cpp b/src/ast/fn_application_expr.cpp index 17d553d..c9639fc 100644 --- a/src/ast/fn_application_expr.cpp +++ b/src/ast/fn_application_expr.cpp @@ -1,4 +1,5 @@ #include "cura-formulae-engine/ast/fn_application_expr.h" +#include "cura-formulae-engine/ast/property_access_expr.h" #include "cura-formulae-engine/ast/variable_expr.h" #include @@ -8,6 +9,9 @@ #include #include +#include +#include +#include #include #include #include @@ -15,11 +19,83 @@ namespace CuraFormulaeEngine::ast { +namespace +{ +std::optional> bindKeywordArguments( + const std::vector& parameter_names, + const std::vector& positional_args, + const std::vector>& keyword_args +) noexcept +{ + if (positional_args.size() > parameter_names.size()) + { + return std::nullopt; + } + + std::vector> slots(parameter_names.size()); + for (size_t i = 0; i < positional_args.size(); ++i) + { + slots[i] = positional_args[i]; + } + + for (const auto& [name, value] : keyword_args) + { + auto it = std::find(parameter_names.begin(), parameter_names.end(), name); + if (it == parameter_names.end()) + { + return std::nullopt; + } + + const auto idx = static_cast(std::distance(parameter_names.begin(), it)); + if (slots[idx].has_value()) + { + return std::nullopt; + } + slots[idx] = value; + } + + std::vector bound; + for (const auto& slot : slots) + { + if (! slot.has_value()) + { + break; + } + bound.push_back(slot.value()); + } + + // If any slot beyond the consecutively-filled slots from the start has a + // value there is a gap (a required earlier argument is missing while a + // later one was supplied via keyword). + // Return nullopt so the caller can report InvalidNumberOfArguments. + for (size_t i = bound.size(); i < slots.size(); ++i) + { + if (slots[i].has_value()) + { + return std::nullopt; + } + } + + return bound; +} + +} // namespace [[nodiscard]] std::string FnApplicationExpr::toString() const noexcept { - auto args_str - = args | ranges::views::transform([](const auto& arg) { return arg.toString(); }) | ranges::views::join(ranges::views::c_str(", ")) | ranges::to(); + std::vector all_args; + all_args.reserve(args.size() + kwargs.size()); + + for (const auto& arg : args) + { + all_args.push_back(arg.toString()); + } + for (const auto& kwarg : kwargs) + { + all_args.push_back(fmt::format("{}={}", kwarg.name, kwarg.value.toString())); + } + + auto args_str = all_args | ranges::views::join(ranges::views::c_str(", ")) | ranges::to(); if (const auto& variable = dynamic_cast(fn.ptr.get())) { @@ -30,14 +106,32 @@ namespace CuraFormulaeEngine::ast [[nodiscard]] eval::Result FnApplicationExpr::evaluate(const env::Environment* environment) const noexcept { - const auto fn_result = try_get(fn.evaluate(environment)); + const auto fn_result = fn.evaluate(environment); if (! fn_result.has_value()) { return zeus::unexpected(fn_result.error()); } - const auto& fn_value = fn_result.value(); - std::vector arg_results; + eval::Value::fn_t fn_value; + std::optional> parameter_names; + const auto& fn_variant = fn_result.value().value; + if (std::holds_alternative(fn_variant)) + { + fn_value = std::get(fn_variant); + } + else if (std::holds_alternative(fn_variant)) + { + const auto& rich_fn = std::get(fn_variant); + fn_value = rich_fn.operation; + parameter_names = rich_fn.getSignature(); + } + else + { + return zeus::unexpected(eval::Error::TypeMismatch); + } + + std::vector positional_arg_results; + positional_arg_results.reserve(args.size()); for (const auto& arg : args) { const auto arg_result = arg.evaluate(environment); @@ -45,10 +139,38 @@ namespace CuraFormulaeEngine::ast { return zeus::unexpected(arg_result.error()); } - arg_results.push_back(arg_result.value()); + positional_arg_results.push_back(arg_result.value()); + } + + if (kwargs.empty()) + { + return fn_value(positional_arg_results); + } + + std::vector> keyword_arg_results; + keyword_arg_results.reserve(kwargs.size()); + for (const auto& kwarg : kwargs) + { + const auto value_result = kwarg.value.evaluate(environment); + if (! value_result.has_value()) + { + return zeus::unexpected(value_result.error()); + } + keyword_arg_results.emplace_back(kwarg.name, value_result.value()); + } + + if (! parameter_names.has_value()) + { + return zeus::unexpected(eval::Error::InvalidNumberOfArguments); } - return fn_value(arg_results); + const auto bound_args = bindKeywordArguments(parameter_names.value(), positional_arg_results, keyword_arg_results); + if (! bound_args.has_value()) + { + return zeus::unexpected(eval::Error::InvalidNumberOfArguments); + } + + return fn_value(bound_args.value()); } [[nodiscard]] std::unordered_set FnApplicationExpr::freeVariables() const noexcept @@ -61,6 +183,11 @@ namespace CuraFormulaeEngine::ast const auto arg_vars = arg.freeVariables(); result.insert(arg_vars.begin(), arg_vars.end()); } + for (const auto& kwarg : kwargs) + { + const auto kwarg_vars = kwarg.value.freeVariables(); + result.insert(kwarg_vars.begin(), kwarg_vars.end()); + } return result; } @@ -83,6 +210,19 @@ namespace CuraFormulaeEngine::ast return false; } } + + if (kwargs.size() != other_fn_application->kwargs.size()) + { + return false; + } + for (size_t i = 0; i < kwargs.size(); ++i) + { + if (! kwargs[i].deepEq(other_fn_application->kwargs[i])) + { + return false; + } + } + return true; } return false; @@ -97,6 +237,10 @@ void FnApplicationExpr::visitAll(std::function visitor) const { arg.visitAll(visitor); } + for (const auto& kwarg : kwargs) + { + kwarg.value.visitAll(visitor); + } } } // namespace CuraFormulaeEngine::ast diff --git a/src/ast/property_access_expr.cpp b/src/ast/property_access_expr.cpp new file mode 100644 index 0000000..d84fcac --- /dev/null +++ b/src/ast/property_access_expr.cpp @@ -0,0 +1,142 @@ +#include "cura-formulae-engine/ast/property_access_expr.h" +#include "cura-formulae-engine/ast/variable_expr.h" + +#include +#include + +#include +#include +#include +#include + +namespace CuraFormulaeEngine::ast +{ + +[[nodiscard]] std::string PropertyAccessExpr::toString() const noexcept +{ + return fmt::format("{}.{}", object.toString(), property); +} + +[[nodiscard]] eval::Result PropertyAccessExpr::evaluate(const env::Environment* environment) const noexcept +{ + const auto object_result = object.evaluate(environment); + if (! object_result.has_value()) + { + return zeus::unexpected(object_result.error()); + } + + const auto& object_value = object_result.value(); + + // Handle property maps (e.g., math.pi) + if (std::holds_alternative>(object_value.value)) + { + const auto& map = std::get>(object_value.value); + const auto it = map.find(property); + if (it == map.end()) + { + return zeus::unexpected(eval::Error::UndefinedVariable); + } + return it->second; + } + + // Handle built-in methods on strings (e.g., separator.join(iterable)) + if (std::holds_alternative(object_value.value)) + { + if (property == "join") + { + const auto separator = std::get(object_value.value); + return eval::Value::fn_t([separator](const std::vector& args) -> eval::Result + { + if (args.size() != 1) + { + return zeus::unexpected(eval::Error::InvalidNumberOfArguments); + } + + const auto* items_ptr = std::get_if>(&args[0].value); + if (! items_ptr) + { + return zeus::unexpected(eval::Error::TypeMismatch); + } + + const auto to_string = [](const eval::Value& v) -> zeus::expected + { + if (const auto* s = std::get_if(&v.value)) return *s; + if (const auto* i = std::get_if(&v.value)) return std::to_string(*i); + if (const auto* d = std::get_if(&v.value)) return std::to_string(*d); + if (const auto* b = std::get_if(&v.value)) return *b ? std::string("True") : std::string("False"); + return zeus::unexpected(eval::Error::TypeMismatch); + }; + + std::string result; + bool first = true; + for (const auto& item : *items_ptr) + { + auto s = to_string(item); + if (! s.has_value()) + { + return zeus::unexpected(s.error()); + } + if (! first) result += separator; + result += s.value(); + first = false; + } + return eval::Value(result); + }); + } + return zeus::unexpected(eval::Error::UndefinedVariable); + } + + // Handle built-in methods on lists/vectors (e.g., list.index) + if (std::holds_alternative>(object_value.value)) + { + if (property == "index") + { + const auto list = object_value; + return eval::Value::fn_t([list](const std::vector& args) -> eval::Result + { + if (args.size() != 1) + { + return zeus::unexpected(eval::Error::InvalidNumberOfArguments); + } + + const auto& search_value = args[0]; + const auto& list_items = std::get>(list.value); + + for (size_t i = 0; i < list_items.size(); ++i) + { + if (list_items[i].deepEq(search_value)) + { + return eval::Value(static_cast(i)); + } + } + + return zeus::unexpected(eval::Error::ValueError); + }); + } + return zeus::unexpected(eval::Error::UndefinedVariable); + } + + return zeus::unexpected(eval::Error::TypeMismatch); +} + +[[nodiscard]] std::unordered_set PropertyAccessExpr::freeVariables() const noexcept +{ + return object.freeVariables(); +} + +[[nodiscard]] bool PropertyAccessExpr::deepEq(const Expr& other) const noexcept +{ + if (const auto& other_property_access = dynamic_cast(&other)) + { + return object.deepEq(other_property_access->object) && property == other_property_access->property; + } + return false; +} + +void PropertyAccessExpr::visitAll(std::function visitor) const noexcept +{ + visitor(*this); + object.visitAll(visitor); +} + +} // namespace CuraFormulaeEngine::ast diff --git a/src/env/env.cpp b/src/env/env.cpp index 208267c..07e266c 100644 --- a/src/env/env.cpp +++ b/src/env/env.cpp @@ -43,30 +43,35 @@ const EnvironmentMap std_env = []() env.set("all", eval::Value(all)); env.set("any", eval::Value(any)); env.set("float", eval::Value(float_fn)); - env.set("int", eval::Value(int_fn)); + env.set("int", eval::Value(eval::Value::rich_fn_t{ int_fn, int_function.getSignature() })); env.set("len", eval::Value(len)); env.set("map", eval::Value(map)); - env.set("math.atan", eval::Value(math_atan)); - env.set("math.ceil", eval::Value(math_ceil)); - env.set("math.cos", eval::Value(math_cos)); - env.set("math.degrees", eval::Value(math_degrees)); - env.set("math.e", eval::Value(std::numbers::e)); - env.set("math.floor", eval::Value(math_floor)); - env.set("math.inf", eval::Value(std::numeric_limits::infinity())); - env.set("math.log", eval::Value(math_log)); - env.set("math.nan", eval::Value(std::nan("1"))); - env.set("math.pi", eval::Value(std::numbers::pi)); - env.set("math.sin", eval::Value(math_sin)); - env.set("math.tan", eval::Value(math_tan)); - env.set("math.tau", eval::Value(std::numbers::pi * 2.0)); - env.set("math.radians", eval::Value(math_radians)); - env.set("math.sqrt", eval::Value(math_sqrt)); env.set("max", eval::Value(max)); - env.set("min", eval::Value(min)); - env.set("round", eval::Value(round)); + env.set("min", eval::Value(eval::Value::rich_fn_t{ min, min_function.getSignature() })); + env.set("round", eval::Value(eval::Value::rich_fn_t{ round, round_function.getSignature() })); env.set("sum", eval::Value(sum)); env.set("str", eval::Value(str)); + // Create math object with properties + std::unordered_map math_props; + math_props["atan"] = eval::Value(math_atan); + math_props["ceil"] = eval::Value(math_ceil); + math_props["cos"] = eval::Value(math_cos); + math_props["degrees"] = eval::Value(math_degrees); + math_props["e"] = eval::Value(std::numbers::e); + math_props["floor"] = eval::Value(math_floor); + math_props["inf"] = eval::Value(std::numeric_limits::infinity()); + math_props["log"] = eval::Value(eval::Value::rich_fn_t{ math_log, math_log_function.getSignature() }); + math_props["nan"] = eval::Value(std::nan("1")); + math_props["pi"] = eval::Value(std::numbers::pi); + math_props["sin"] = eval::Value(math_sin); + math_props["tan"] = eval::Value(math_tan); + math_props["tau"] = eval::Value(std::numbers::pi * 2.0); + math_props["radians"] = eval::Value(math_radians); + math_props["sqrt"] = eval::Value(math_sqrt); + + env.set("math", eval::Value(math_props)); + return env; }(); diff --git a/src/env/int_fn.cpp b/src/env/int_fn.cpp index 59c0023..668c95f 100644 --- a/src/env/int_fn.cpp +++ b/src/env/int_fn.cpp @@ -10,7 +10,7 @@ namespace CuraFormulaeEngine::env { -const eval::Value::fn_t int_fn = [](const std::vector &args) -> eval::Result +[[nodiscard]] eval::Result IntFunction::operator()(const std::vector &args) const noexcept { if (args.size() == 2) { @@ -50,6 +50,17 @@ const eval::Value::fn_t int_fn = [](const std::vector &args) -> eva return static_cast(std::stod(std::get(x.value))); } return zeus::unexpected(eval::Error::TypeMismatch); +} + +[[nodiscard]] std::vector IntFunction::getSignature() const noexcept +{ + return { "x", "base" }; +} + +const IntFunction int_function{}; +const eval::Value::fn_t int_fn = [](const std::vector& args) -> eval::Result +{ + return int_function(args); }; } // namespace CuraFormulaeEngine::env diff --git a/src/env/map.cpp b/src/env/map.cpp index 2ec7403..cc73ea2 100644 --- a/src/env/map.cpp +++ b/src/env/map.cpp @@ -15,16 +15,25 @@ const eval::Value::fn_t map = [](const std::vector &args) -> eval:: return zeus::unexpected(eval::Error::InvalidNumberOfArguments); } - if (! std::holds_alternative(args[0].value)) + if (! std::holds_alternative>(args[1].value)) { return zeus::unexpected(eval::Error::TypeMismatch); } - if (! std::holds_alternative>(args[1].value)) + + eval::Value::fn_t fn; + if (std::holds_alternative(args[0].value)) + { + fn = std::get(args[0].value); + } + else if (std::holds_alternative(args[0].value)) + { + fn = std::get(args[0].value).operation; + } + else { return zeus::unexpected(eval::Error::TypeMismatch); } - const auto fn = std::get(args[0].value); const auto list = std::get>(args[1].value); std::vector result; diff --git a/src/env/math_log.cpp b/src/env/math_log.cpp index b92eda7..80da9ca 100644 --- a/src/env/math_log.cpp +++ b/src/env/math_log.cpp @@ -10,7 +10,7 @@ namespace CuraFormulaeEngine::env { -const eval::Value::fn_t math_log = [](const std::vector &args) -> eval::Result +[[nodiscard]] eval::Result MathLogFunction::operator()(const std::vector &args) const noexcept { if (args.empty() || args.size() > 2) { @@ -58,6 +58,17 @@ const eval::Value::fn_t math_log = [](const std::vector &args) -> e } return eval::Value(std::log(x) / std::log(base)); +} + +[[nodiscard]] std::vector MathLogFunction::getSignature() const noexcept +{ + return { "x", "base" }; +} + +const MathLogFunction math_log_function{}; +const eval::Value::fn_t math_log = [](const std::vector& args) -> eval::Result +{ + return math_log_function(args); }; } // namespace CuraFormulaeEngine::env diff --git a/src/env/min.cpp b/src/env/min.cpp index 085dc85..94bf9dc 100644 --- a/src/env/min.cpp +++ b/src/env/min.cpp @@ -9,7 +9,7 @@ namespace CuraFormulaeEngine::env { -const eval::Value::fn_t min = [](const std::vector &args) -> eval::Result +[[nodiscard]] eval::Result MinFunction::operator()(const std::vector &args) const noexcept { if (args.empty()) { @@ -35,11 +35,94 @@ const eval::Value::fn_t min = [](const std::vector &args) -> eval:: return min; }; + const auto find_min_with_key = [](const std::vector& vec, const eval::Value& key_fn) -> eval::Result + { + if (vec.empty()) + { + return zeus::unexpected(eval::Error::InvalidNumberOfArguments); + } + + eval::Value::fn_t fn; + if (std::holds_alternative(key_fn.value)) + { + fn = std::get(key_fn.value); + } + else if (std::holds_alternative(key_fn.value)) + { + fn = std::get(key_fn.value).operation; + } + else + { + return zeus::unexpected(eval::Error::TypeMismatch); + } + + auto min_elem = vec[0]; + auto min_key = fn(std::vector{min_elem}); + if (! min_key.has_value()) + { + return zeus::unexpected(min_key.error()); + } + + for (const auto& elem : vec | ranges::views::drop(1)) + { + auto elem_key = fn(std::vector{elem}); + if (! elem_key.has_value()) + { + return zeus::unexpected(elem_key.error()); + } + + const auto cmp = elem_key.value() < min_key.value(); + if (! cmp.has_value()) + { + return zeus::unexpected(eval::Error::TypeMismatch); + } + + if (cmp.value()) + { + min_elem = elem; + min_key = elem_key; + } + } + return min_elem; + }; + if (args.size() == 1 && std::holds_alternative>(args[0].value)) { return find_min(std::get>(args[0].value)); } + + if (args.size() == 2) + { + const bool second_arg_is_callable = std::holds_alternative(args[1].value) + || std::holds_alternative(args[1].value); + + // key parameter is only supported for min(iterable, key) + if (second_arg_is_callable && std::holds_alternative>(args[0].value)) + { + return find_min_with_key(std::get>(args[0].value), args[1]); + } + if (second_arg_is_callable) + { + return zeus::unexpected(eval::Error::TypeMismatch); + } + + // Two positional args: treat both as values. + return find_min(args); + } + + // Multiple arguments: find min among them (no key) return find_min(args); +} + +[[nodiscard]] std::vector MinFunction::getSignature() const noexcept +{ + return { "iterable", "key" }; +} + +const MinFunction min_function{}; +const eval::Value::fn_t min = [](const std::vector& args) -> eval::Result +{ + return min_function(args); }; } // namespace CuraFormulaeEngine::env diff --git a/src/env/round.cpp b/src/env/round.cpp index 5cc06de..86fe965 100644 --- a/src/env/round.cpp +++ b/src/env/round.cpp @@ -10,7 +10,7 @@ namespace CuraFormulaeEngine::env { -const eval::Value::fn_t round = [](const std::vector &args) -> eval::Result +[[nodiscard]] eval::Result RoundFunction::operator()(const std::vector &args) const noexcept { if (args.size() > 2) { @@ -54,6 +54,17 @@ const eval::Value::fn_t round = [](const std::vector &args) -> eval return static_cast(value); } return value; +} + +[[nodiscard]] std::vector RoundFunction::getSignature() const noexcept +{ + return { "x", "base" }; +} + +const RoundFunction round_function{}; +const eval::Value::fn_t round = [](const std::vector& args) -> eval::Result +{ + return round_function(args); }; } // namespace CuraFormulaeEngine::env diff --git a/src/eval.cpp b/src/eval.cpp index 11e314d..ca41c8c 100644 --- a/src/eval.cpp +++ b/src/eval.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -60,6 +61,14 @@ namespace CuraFormulaeEngine::eval { { return ""; } + if (std::holds_alternative(value)) + { + return ""; + } + if (std::holds_alternative>(value)) + { + return ""; + } return ""; } @@ -105,6 +114,28 @@ namespace CuraFormulaeEngine::eval { { return typeid(std::holds_alternative(value)) == typeid(std::holds_alternative(other.value)); } + if (std::holds_alternative(value) && std::holds_alternative(other.value)) + { + return true; + } + if (std::holds_alternative>(value) && std::holds_alternative>(other.value)) + { + const auto &lhs = std::get>(value); + const auto &rhs = std::get>(other.value); + if (lhs.size() != rhs.size()) + { + return false; + } + for (const auto& [key, lhs_val] : lhs) + { + const auto it = rhs.find(key); + if (it == rhs.end() || !lhs_val.deepEq(it->second)) + { + return false; + } + } + return true; + } return false; } @@ -130,6 +161,10 @@ namespace CuraFormulaeEngine::eval { { return ! std::get>(value).empty(); } + if (std::holds_alternative>(value)) + { + return ! std::get>(value).empty(); + } return false; }; @@ -188,6 +223,20 @@ namespace CuraFormulaeEngine::eval { { throw std::runtime_error("Cannot convert function to emscripten"); } + if (std::holds_alternative(value)) + { + throw std::runtime_error("Cannot convert function to emscripten"); + } + if (std::holds_alternative>(value)) + { + emscripten::val obj = emscripten::val::object(); + const auto& map = std::get>(value); + for (const auto& [key, val] : map) + { + obj.set(key, val.toEmscripten()); + } + return obj; + } throw std::runtime_error("Unknown type in `Value::toEmscripten`"); } @@ -276,6 +325,29 @@ bool operator==(const CuraFormulaeEngine::eval::Value& lhs, const CuraFormulaeEn }); } + if (std::holds_alternative>(lhs.value) && + std::holds_alternative>(rhs.value)) + { + const auto& map_lhs = std::get>(lhs.value); + const auto& map_rhs = std::get>(rhs.value); + + if (map_lhs.size() != map_rhs.size()) + { + return false; + } + + for (const auto& [key, val_lhs] : map_lhs) + { + const auto it = map_rhs.find(key); + if (it == map_rhs.end() || val_lhs != it->second) + { + return false; + } + } + + return true; + } + return false; } diff --git a/tests/parser.cpp b/tests/parser.cpp index e6a0483..f06fefc 100644 --- a/tests/parser.cpp +++ b/tests/parser.cpp @@ -1,4 +1,5 @@ #include "cura-formulae-engine/cura-formulae-engine.h" +#include "cura-formulae-engine/ast/property_access_expr.h" #include #include @@ -804,7 +805,7 @@ TEST_CASE("trailing comma tuple", "[parser, tuple]") TEST_CASE("variable math.pi", "[parser, variable]") { auto input = "math.pi"sv; - const auto expected_ast = make_expr_ptr("math.pi"); + const auto expected_ast = make_expr_ptr(make_expr_ptr("math"), "pi"); const auto expected_eval = CuraFormulaeEngine::eval::Value(std::numbers::pi); const auto message = CuraFormulaeEngine::parser::parse(input); @@ -819,7 +820,7 @@ TEST_CASE("variable math.pi", "[parser, variable]") TEST_CASE("variable math.e", "[parser, variable]") { auto input = "math.e"sv; - const auto expected_ast = make_expr_ptr("math.e"); + const auto expected_ast = make_expr_ptr(make_expr_ptr("math"), "e"); const auto expected_eval = CuraFormulaeEngine::eval::Value(std::numbers::e); const auto message = CuraFormulaeEngine::parser::parse(input); @@ -834,7 +835,7 @@ TEST_CASE("variable math.e", "[parser, variable]") TEST_CASE("fn cos", "[parser, fn]") { auto input = "math.cos(math.pi)"sv; - const auto expected_ast = make_expr_ptr("math.cos")(make_expr_ptr("math.pi")); + const auto expected_ast = make_expr_ptr(make_expr_ptr("math"), "cos")(make_expr_ptr(make_expr_ptr("math"), "pi")); const auto expected_eval = CuraFormulaeEngine::eval::Value(std::cos(std::numbers::pi)); const auto message = CuraFormulaeEngine::parser::parse(input); @@ -850,7 +851,7 @@ TEST_CASE("fn cos", "[parser, fn]") TEST_CASE("fn sin", "[parser, fn]") { auto input = "math.sin(math.pi)"sv; - const auto expected_ast = make_expr_ptr("math.sin")(make_expr_ptr("math.pi")); + const auto expected_ast = make_expr_ptr(make_expr_ptr("math"), "sin")(make_expr_ptr(make_expr_ptr("math"), "pi")); const auto expected_eval = CuraFormulaeEngine::eval::Value(std::sin(std::numbers::pi)); const auto message = CuraFormulaeEngine::parser::parse(input); @@ -865,7 +866,7 @@ TEST_CASE("fn sin", "[parser, fn]") TEST_CASE("fn tan", "[parser, fn]") { auto input = "math.tan(math.pi)"sv; - const auto expected_ast = make_expr_ptr("math.tan")(make_expr_ptr("math.pi")); + const auto expected_ast = make_expr_ptr(make_expr_ptr("math"), "tan")(make_expr_ptr(make_expr_ptr("math"), "pi")); const auto expected_eval = CuraFormulaeEngine::eval::Value(std::tan(std::numbers::pi)); const auto message = CuraFormulaeEngine::parser::parse(input); @@ -1371,7 +1372,7 @@ TEST_CASE("fn int str input base", "[parser, fn]") TEST_CASE("fn floor", "[parser, fn]") { auto input = "math.floor(0.1)"sv; - const auto expected_ast = make_expr_ptr("math.floor")(make_expr_ptr(0.1)); + const auto expected_ast = make_expr_ptr(make_expr_ptr("math"), "floor")(make_expr_ptr(0.1)); const auto expected_eval = CuraFormulaeEngine::eval::Value(0.0); const auto message = CuraFormulaeEngine::parser::parse(input); @@ -1386,7 +1387,7 @@ TEST_CASE("fn floor", "[parser, fn]") TEST_CASE("fn ceil", "[parser, fn]") { auto input = "math.ceil(0.1)"sv; - const auto expected_ast = make_expr_ptr("math.ceil")(make_expr_ptr(0.1)); + const auto expected_ast = make_expr_ptr(make_expr_ptr("math"), "ceil")(make_expr_ptr(0.1)); const auto expected_eval = CuraFormulaeEngine::eval::Value(1.0); const auto message = CuraFormulaeEngine::parser::parse(input); @@ -1401,7 +1402,7 @@ TEST_CASE("fn ceil", "[parser, fn]") TEST_CASE("fn math.log(1)", "[parser, fn]") { auto input = "math.log(1)"sv; - const auto expected_ast = make_expr_ptr("math.log")(make_expr_ptr(int64_t(1))); + const auto expected_ast = make_expr_ptr(make_expr_ptr("math"), "log")(make_expr_ptr(int64_t(1))); const auto expected_eval = CuraFormulaeEngine::eval::Value(0.0); const auto message = CuraFormulaeEngine::parser::parse(input); @@ -1416,7 +1417,7 @@ TEST_CASE("fn math.log(1)", "[parser, fn]") TEST_CASE("fn math.log(1.0)", "[parser, fn]") { auto input = "math.log(1.0)"sv; - const auto expected_ast = make_expr_ptr("math.log")(make_expr_ptr(1.0)); + const auto expected_ast = make_expr_ptr(make_expr_ptr("math"), "log")(make_expr_ptr(1.0)); const auto expected_eval = CuraFormulaeEngine::eval::Value(0.0); const auto message = CuraFormulaeEngine::parser::parse(input); @@ -1431,7 +1432,7 @@ TEST_CASE("fn math.log(1.0)", "[parser, fn]") TEST_CASE("fn math.log(4096, 8)", "[parser, fn]") { auto input = "math.log(4096, 8)"sv; - const auto expected_ast = make_expr_ptr("math.log")(make_expr_ptr(int64_t(4096)), + const auto expected_ast = make_expr_ptr(make_expr_ptr("math"), "log")(make_expr_ptr(int64_t(4096)), make_expr_ptr(int64_t(8))); const auto expected_eval = CuraFormulaeEngine::eval::Value(4.0); @@ -1447,7 +1448,7 @@ TEST_CASE("fn math.log(4096, 8)", "[parser, fn]") TEST_CASE("fn math.log(True)", "[parser, fn]") { auto input = "math.log(True)"sv; - const auto expected_ast = make_expr_ptr("math.log")(make_expr_ptr(true)); + const auto expected_ast = make_expr_ptr(make_expr_ptr("math"), "log")(make_expr_ptr(true)); const auto expected_eval = CuraFormulaeEngine::eval::Value(0.0); const auto message = CuraFormulaeEngine::parser::parse(input); @@ -2062,6 +2063,71 @@ TEST_CASE("fn round base=5", "[parser, fn]") REQUIRE(eval.value().deepEq(expected_eval)); } +TEST_CASE("fn round keyword args", "[parser, fn, keyword]") +{ + auto input = "round(x=1.5, base=1)"sv; + const auto expected_eval = CuraFormulaeEngine::eval::Value(1.5); + + const auto message = CuraFormulaeEngine::parser::parse(input); + REQUIRE(message.has_value()); + const auto &ast = message.value(); + const auto eval = ast.evaluate(&CuraFormulaeEngine::env::std_env); + REQUIRE(eval.has_value()); + REQUIRE(eval.value().deepEq(expected_eval)); +} + +TEST_CASE("fn int keyword args", "[parser, fn, keyword]") +{ + auto input = "int(x='10', base=2)"sv; + const auto expected_eval = CuraFormulaeEngine::eval::Value(int64_t(2)); + + const auto message = CuraFormulaeEngine::parser::parse(input); + REQUIRE(message.has_value()); + const auto &ast = message.value(); + const auto eval = ast.evaluate(&CuraFormulaeEngine::env::std_env); + REQUIRE(eval.has_value()); + REQUIRE(eval.value().deepEq(expected_eval)); +} + +TEST_CASE("fn math.log keyword args", "[parser, fn, keyword]") +{ + auto input = "math.log(x=8, base=2)"sv; + + const auto message = CuraFormulaeEngine::parser::parse(input); + REQUIRE(message.has_value()); + const auto &ast = message.value(); + const auto eval = ast.evaluate(&CuraFormulaeEngine::env::std_env); + REQUIRE(eval.has_value()); + REQUIRE(std::abs(std::get(eval.value().value) - 3.0) < 1e-12); +} + +TEST_CASE("fn unknown keyword arg", "[parser, fn, keyword]") +{ + auto input = "round(foo=1.5)"sv; + + const auto message = CuraFormulaeEngine::parser::parse(input); + REQUIRE(message.has_value()); + const auto &ast = message.value(); + const auto eval = ast.evaluate(&CuraFormulaeEngine::env::std_env); + REQUIRE_FALSE(eval.has_value()); + REQUIRE(eval.error() == CuraFormulaeEngine::eval::Error::InvalidNumberOfArguments); +} + +TEST_CASE("fn keyword arg with gap in positional args", "[parser, fn, keyword]") +{ + // round(base=1) omits the required first argument 'x'; bindKeywordArguments + // should detect the gap and return InvalidNumberOfArguments instead of + // invoking RoundFunction with 0 args (which would cause out-of-bounds UB). + auto input = "round(base=1)"sv; + + const auto message = CuraFormulaeEngine::parser::parse(input); + REQUIRE(message.has_value()); + const auto &ast = message.value(); + const auto eval = ast.evaluate(&CuraFormulaeEngine::env::std_env); + REQUIRE_FALSE(eval.has_value()); + REQUIRE(eval.error() == CuraFormulaeEngine::eval::Error::InvalidNumberOfArguments); +} + TEST_CASE("fn any trailing comma", "[parser, fn]") { auto input = "any([True], )"sv; @@ -2254,7 +2320,7 @@ TEST_CASE("s8 bottom_thickness setting parser", "[parser, fdm_printer.def.json e CuraFormulaeEngine::env::LocalEnvironment custom_env{&CuraFormulaeEngine::env::std_env}; custom_env.set("layer_height", 0.2006); - custom_env.set("top_layers", int64_t(4)); + custom_env.set("top_layers", static_cast(4)); custom_env.set("support_enable", false); custom_env.set("top_bottom_thickness", 0.8); @@ -2279,3 +2345,292 @@ TEST_CASE("Falcon infill", "[parser, fdm_printer.def.json example]") REQUIRE(eval.has_value()); REQUIRE(eval.value().deepEq(expected_eval)); } + +// Property access tests +TEST_CASE("math.pi property access", "[parser, property_access]") +{ + auto input = "math.pi"sv; + const auto expected_eval = CuraFormulaeEngine::eval::Value(std::numbers::pi); + + const auto message = CuraFormulaeEngine::parser::parse(input); + REQUIRE(message.has_value()); + const auto &ast = message.value(); + + const auto eval = ast.evaluate(&CuraFormulaeEngine::env::std_env); + REQUIRE(eval.has_value()); + REQUIRE(eval.value().deepEq(expected_eval)); +} + +TEST_CASE("math.e property access", "[parser, property_access]") +{ + auto input = "math.e"sv; + const auto expected_eval = CuraFormulaeEngine::eval::Value(std::numbers::e); + + const auto message = CuraFormulaeEngine::parser::parse(input); + REQUIRE(message.has_value()); + const auto &ast = message.value(); + + const auto eval = ast.evaluate(&CuraFormulaeEngine::env::std_env); + REQUIRE(eval.has_value()); + REQUIRE(eval.value().deepEq(expected_eval)); +} + +TEST_CASE("math.sqrt function call via property", "[parser, property_access]") +{ + auto input = "math.sqrt(4)"sv; + const auto expected_eval = CuraFormulaeEngine::eval::Value(2.0); + + const auto message = CuraFormulaeEngine::parser::parse(input); + REQUIRE(message.has_value()); + const auto &ast = message.value(); + + const auto eval = ast.evaluate(&CuraFormulaeEngine::env::std_env); + REQUIRE(eval.has_value()); + REQUIRE(eval.value().deepEq(expected_eval)); +} + +TEST_CASE("math.log function call with keyword args", "[parser, property_access]") +{ + auto input = "math.log(x=8, base=2)"sv; + const auto expected_eval = CuraFormulaeEngine::eval::Value(3.0); + + const auto message = CuraFormulaeEngine::parser::parse(input); + REQUIRE(message.has_value()); + const auto &ast = message.value(); + + const auto eval = ast.evaluate(&CuraFormulaeEngine::env::std_env); + REQUIRE(eval.has_value()); + REQUIRE(std::abs(eval.value().numeric().value() - expected_eval.numeric().value()) < 1e-10); +} + +TEST_CASE("math.sin function call", "[parser, property_access]") +{ + auto input = "math.sin(0)"sv; + const auto expected_eval = CuraFormulaeEngine::eval::Value(0.0); + + const auto message = CuraFormulaeEngine::parser::parse(input); + REQUIRE(message.has_value()); + const auto &ast = message.value(); + + const auto eval = ast.evaluate(&CuraFormulaeEngine::env::std_env); + REQUIRE(eval.has_value()); + REQUIRE(eval.value().deepEq(expected_eval)); +} + +TEST_CASE("math.pi in arithmetic", "[parser, property_access]") +{ + auto input = "2 * math.pi"sv; + const auto expected_eval = CuraFormulaeEngine::eval::Value(2.0 * std::numbers::pi); + + const auto message = CuraFormulaeEngine::parser::parse(input); + REQUIRE(message.has_value()); + const auto &ast = message.value(); + + const auto eval = ast.evaluate(&CuraFormulaeEngine::env::std_env); + REQUIRE(eval.has_value()); + REQUIRE(eval.value().deepEq(expected_eval)); +} + +TEST_CASE("property access in list", "[parser, property_access]") +{ + auto input = "[math.pi, math.e]"sv; + const auto expected_eval = CuraFormulaeEngine::eval::Value{std::vector{ + CuraFormulaeEngine::eval::Value(std::numbers::pi), + CuraFormulaeEngine::eval::Value(std::numbers::e) + }}; + + const auto message = CuraFormulaeEngine::parser::parse(input); + REQUIRE(message.has_value()); + const auto &ast = message.value(); + + const auto eval = ast.evaluate(&CuraFormulaeEngine::env::std_env); + REQUIRE(eval.has_value()); + REQUIRE(eval.value().deepEq(expected_eval)); +} + +// Method calls on tuples/lists +TEST_CASE("tuple.index method", "[parser, method_access]") +{ + auto input = "('raft', 'brim', 'skirt', 'none').index('raft')"sv; + const auto expected_eval = CuraFormulaeEngine::eval::Value(int64_t(0)); + + const auto message = CuraFormulaeEngine::parser::parse(input); + REQUIRE(message.has_value()); + const auto &ast = message.value(); + + const auto eval = ast.evaluate(&CuraFormulaeEngine::env::std_env); + REQUIRE(eval.has_value()); + REQUIRE(eval.value().deepEq(expected_eval)); +} + +TEST_CASE("tuple.index method find second element", "[parser, method_access]") +{ + auto input = "('raft', 'brim', 'skirt', 'none').index('brim')"sv; + const auto expected_eval = CuraFormulaeEngine::eval::Value(int64_t(1)); + + const auto message = CuraFormulaeEngine::parser::parse(input); + REQUIRE(message.has_value()); + const auto &ast = message.value(); + + const auto eval = ast.evaluate(&CuraFormulaeEngine::env::std_env); + REQUIRE(eval.has_value()); + REQUIRE(eval.value().deepEq(expected_eval)); +} + +TEST_CASE("list.index method", "[parser, method_access]") +{ + auto input = "[1, 2, 3, 4].index(3)"sv; + const auto expected_eval = CuraFormulaeEngine::eval::Value(int64_t(2)); + + const auto message = CuraFormulaeEngine::parser::parse(input); + REQUIRE(message.has_value()); + const auto &ast = message.value(); + + const auto eval = ast.evaluate(&CuraFormulaeEngine::env::std_env); + REQUIRE(eval.has_value()); + REQUIRE(eval.value().deepEq(expected_eval)); +} + +// extruderValues and min with key tests +namespace +{ +namespace extruder_values_test +{ +const CuraFormulaeEngine::eval::Value::rich_fn_t kExtruderValues{ + [](const std::vector& args) -> CuraFormulaeEngine::eval::Result + { + if (args.size() != 1) + { + return zeus::unexpected(CuraFormulaeEngine::eval::Error::InvalidNumberOfArguments); + } + if (! std::holds_alternative(args[0].value)) + { + return zeus::unexpected(CuraFormulaeEngine::eval::Error::TypeMismatch); + } + + const auto& option = std::get(args[0].value); + if (option == "adhesion_type") + { + return CuraFormulaeEngine::eval::Value{std::vector{ + CuraFormulaeEngine::eval::Value(std::string("raft")), + CuraFormulaeEngine::eval::Value(std::string("brim")) + }}; + } + + return CuraFormulaeEngine::eval::Value{std::vector{}}; + }, + { "option" } +}; +} // namespace extruder_values_test +} // namespace + +TEST_CASE("extruderValues function", "[env, extruder_values]") +{ + auto input = "extruderValues('adhesion_type')"sv; + const auto expected_eval = CuraFormulaeEngine::eval::Value{std::vector{ + CuraFormulaeEngine::eval::Value(std::string("raft")), + CuraFormulaeEngine::eval::Value(std::string("brim")) + }}; + + const auto message = CuraFormulaeEngine::parser::parse(input); + REQUIRE(message.has_value()); + const auto &ast = message.value(); + + CuraFormulaeEngine::env::LocalEnvironment custom_env{&CuraFormulaeEngine::env::std_env}; + custom_env.set("extruderValues", CuraFormulaeEngine::eval::Value(extruder_values_test::kExtruderValues)); + + const auto eval = ast.evaluate(&custom_env); + REQUIRE(eval.has_value()); + REQUIRE(eval.value().deepEq(expected_eval)); +} + +TEST_CASE("min with key parameter", "[env, min, key]") +{ + auto input = "min(['raft', 'brim'], key=('raft', 'brim', 'skirt', 'none').index)"sv; + const auto expected_eval = CuraFormulaeEngine::eval::Value(std::string("raft")); + + const auto message = CuraFormulaeEngine::parser::parse(input); + REQUIRE(message.has_value()); + const auto &ast = message.value(); + + const auto eval = ast.evaluate(&CuraFormulaeEngine::env::std_env); + REQUIRE(eval.has_value()); + REQUIRE(eval.value().deepEq(expected_eval)); +} + +TEST_CASE("min with key - full expression", "[env, min, key]") +{ + auto input = "min(extruderValues('adhesion_type'), key=('raft', 'brim', 'skirt', 'none').index)"sv; + const auto expected_eval = CuraFormulaeEngine::eval::Value(std::string("raft")); + + const auto message = CuraFormulaeEngine::parser::parse(input); + REQUIRE(message.has_value()); + const auto &ast = message.value(); + + CuraFormulaeEngine::env::LocalEnvironment custom_env{&CuraFormulaeEngine::env::std_env}; + custom_env.set("extruderValues", CuraFormulaeEngine::eval::Value(extruder_values_test::kExtruderValues)); + + const auto eval = ast.evaluate(&custom_env); + REQUIRE(eval.has_value()); + REQUIRE(eval.value().deepEq(expected_eval)); +} + +TEST_CASE("min with custom env variables", "[env, min]") +{ + auto input = "min(machine_max_feedrate_x, machine_max_feedrate_y)"sv; + const auto expected_eval = CuraFormulaeEngine::eval::Value(int64_t(125)); + + const auto message = CuraFormulaeEngine::parser::parse(input); + REQUIRE(message.has_value()); + const auto &ast = message.value(); + + CuraFormulaeEngine::env::LocalEnvironment custom_env{&CuraFormulaeEngine::env::std_env}; + custom_env.set("machine_max_feedrate_x", CuraFormulaeEngine::eval::Value(int64_t(125))); + custom_env.set("machine_max_feedrate_y", CuraFormulaeEngine::eval::Value(int64_t(125))); + + const auto eval = ast.evaluate(&custom_env); + REQUIRE(eval.has_value()); + REQUIRE(eval.value().deepEq(expected_eval)); +} + +TEST_CASE("string join integers", "[parser, method_access, join]") +{ + auto input = R"(",".join([1, 2, 3]))"sv; + const auto expected_eval = CuraFormulaeEngine::eval::Value(std::string("1,2,3")); + + const auto message = CuraFormulaeEngine::parser::parse(input); + REQUIRE(message.has_value()); + const auto &ast = message.value(); + + const auto eval = ast.evaluate(&CuraFormulaeEngine::env::std_env); + REQUIRE(eval.has_value()); + REQUIRE(eval.value().deepEq(expected_eval)); +} + +TEST_CASE("string join strings", "[parser, method_access, join]") +{ + auto input = R"(" ".join(["hello", "world"]))"sv; + const auto expected_eval = CuraFormulaeEngine::eval::Value(std::string("hello world")); + + const auto message = CuraFormulaeEngine::parser::parse(input); + REQUIRE(message.has_value()); + const auto &ast = message.value(); + + const auto eval = ast.evaluate(&CuraFormulaeEngine::env::std_env); + REQUIRE(eval.has_value()); + REQUIRE(eval.value().deepEq(expected_eval)); +} + +TEST_CASE("string join empty list", "[parser, method_access, join]") +{ + auto input = R"(",".join([]))"sv; + const auto expected_eval = CuraFormulaeEngine::eval::Value(std::string("")); + + const auto message = CuraFormulaeEngine::parser::parse(input); + REQUIRE(message.has_value()); + const auto &ast = message.value(); + + const auto eval = ast.evaluate(&CuraFormulaeEngine::env::std_env); + REQUIRE(eval.has_value()); + REQUIRE(eval.value().deepEq(expected_eval)); +}