diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ff0c6778..6cb446b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ jobs: run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DMULTITHREAD_SUPPORT_ENABLED=${{ matrix.multithread }} - name: Build - run: cmake --build build -j + run: cmake --build build - name: Test run: ctest --test-dir build --output-on-failure @@ -49,7 +49,7 @@ jobs: run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DMULTITHREAD_SUPPORT_ENABLED=${{ matrix.multithread }} - name: Build - run: cmake --build build -j + run: cmake --build build - name: Test run: ctest --test-dir build --output-on-failure @@ -71,7 +71,7 @@ jobs: run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DENABLE_ADDRESS_SANITIZER=ON -DENABLE_UNDEFINED_SANITIZER=ON - name: Build - run: cmake --build build -j + run: cmake --build build - name: Test run: ctest --test-dir build --output-on-failure @@ -93,7 +93,7 @@ jobs: run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DENABLE_ADDRESS_SANITIZER=ON -DENABLE_UNDEFINED_SANITIZER=ON - name: Build - run: cmake --build build -j + run: cmake --build build - name: Test run: ctest --test-dir build --output-on-failure @@ -135,7 +135,7 @@ jobs: run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DENABLE_THREAD_SANITIZER=ON -DMULTITHREAD_SUPPORT_ENABLED=ON - name: Build - run: cmake --build build -j + run: cmake --build build - name: Test run: ctest --test-dir build --output-on-failure @@ -157,7 +157,7 @@ jobs: run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DENABLE_THREAD_SANITIZER=ON -DMULTITHREAD_SUPPORT_ENABLED=ON - name: Build - run: cmake --build build -j + run: cmake --build build - name: Test run: ctest --test-dir build --output-on-failure diff --git a/include/chaiscript/language/chaiscript_common.hpp b/include/chaiscript/language/chaiscript_common.hpp index fb75a933..9fca319c 100644 --- a/include/chaiscript/language/chaiscript_common.hpp +++ b/include/chaiscript/language/chaiscript_common.hpp @@ -106,7 +106,8 @@ namespace chaiscript { Constant, Compiled, Const_Var_Decl, - Const_Assign_Decl + Const_Assign_Decl, + Namespace_Block }; enum class Operator_Precedence { @@ -127,7 +128,7 @@ namespace chaiscript { namespace { /// Helper lookup to get the name of each node type constexpr const char *ast_node_type_to_string(AST_Node_Type ast_node_type) noexcept { - constexpr const char *const ast_node_types[] = {"Id", "Fun_Call", "Unused_Return_Fun_Call", "Arg_List", "Equation", "Var_Decl", "Assign_Decl", "Array_Call", "Dot_Access", "Lambda", "Block", "Scopeless_Block", "Def", "While", "If", "For", "Ranged_For", "Inline_Array", "Inline_Map", "Return", "File", "Prefix", "Break", "Continue", "Map_Pair", "Value_Range", "Inline_Range", "Try", "Catch", "Finally", "Method", "Attr_Decl", "Logical_And", "Logical_Or", "Reference", "Switch", "Case", "Default", "Noop", "Class", "Binary", "Arg", "Global_Decl", "Constant", "Compiled", "Const_Var_Decl", "Const_Assign_Decl"}; + constexpr const char *const ast_node_types[] = {"Id", "Fun_Call", "Unused_Return_Fun_Call", "Arg_List", "Equation", "Var_Decl", "Assign_Decl", "Array_Call", "Dot_Access", "Lambda", "Block", "Scopeless_Block", "Def", "While", "If", "For", "Ranged_For", "Inline_Array", "Inline_Map", "Return", "File", "Prefix", "Break", "Continue", "Map_Pair", "Value_Range", "Inline_Range", "Try", "Catch", "Finally", "Method", "Attr_Decl", "Logical_And", "Logical_Or", "Reference", "Switch", "Case", "Default", "Noop", "Class", "Binary", "Arg", "Global_Decl", "Constant", "Compiled", "Const_Var_Decl", "Const_Assign_Decl", "Namespace_Block"}; return ast_node_types[static_cast(ast_node_type)]; } diff --git a/include/chaiscript/language/chaiscript_engine.hpp b/include/chaiscript/language/chaiscript_engine.hpp index 4afd0449..de5e4b2f 100644 --- a/include/chaiscript/language/chaiscript_engine.hpp +++ b/include/chaiscript/language/chaiscript_engine.hpp @@ -188,10 +188,17 @@ namespace chaiscript { m_engine.add(fun([this](const Boxed_Value &t_bv, const std::string &t_name) { add_global(t_bv, t_name); }), "add_global"); m_engine.add(fun([this](const Boxed_Value &t_bv, const std::string &t_name) { set_global(t_bv, t_name); }), "set_global"); - // why this unused parameter to Namespace? m_engine.add(fun([this](const std::string &t_namespace_name) { - register_namespace([](Namespace & /*space*/) noexcept {}, t_namespace_name); - import(t_namespace_name); + if (!m_namespace_generators.count(t_namespace_name)) { + register_namespace([](Namespace & /*space*/) noexcept {}, t_namespace_name); + } + const auto sep_pos = t_namespace_name.find("::"); + const std::string root_name = (sep_pos != std::string::npos) ? t_namespace_name.substr(0, sep_pos) : t_namespace_name; + if (!m_engine.get_scripting_objects().count(root_name)) { + import(root_name); + } else if (m_namespace_generators.count(root_name)) { + nest_children(root_name, m_namespace_generators[root_name]()); + } }), "namespace"); m_engine.add(fun([this](const std::string &t_namespace_name) { import(t_namespace_name); }), "import"); @@ -730,28 +737,59 @@ namespace chaiscript { if (m_engine.get_scripting_objects().count(t_namespace_name)) { throw std::runtime_error("Namespace: " + t_namespace_name + " was already defined"); } else if (m_namespace_generators.count(t_namespace_name)) { - m_engine.add_global(var(std::ref(m_namespace_generators[t_namespace_name]())), t_namespace_name); + auto &ns = m_namespace_generators[t_namespace_name](); + nest_children(t_namespace_name, ns); + m_engine.add_global(var(std::ref(ns)), t_namespace_name); } else { throw std::runtime_error("No registered namespace: " + t_namespace_name); } } /// \brief Registers a namespace generator, which delays generation of the namespace until it is imported, saving memory if it is never - /// used. \param[in] t_namespace_generator Namespace generator function. \param[in] t_namespace_name Name of the Namespace function - /// being registered. \throw std::runtime_error In the case that the namespace name was already registered. + /// used. Supports C++-style nested names (e.g. "constants::si") for nested namespaces; parent namespaces are auto-registered if absent. + /// \param[in] t_namespace_generator Namespace generator function. + /// \param[in] t_namespace_name Name of the Namespace function being registered (may contain :: for nesting). + /// \throw std::runtime_error In the case that the namespace name was already registered. void register_namespace(const std::function &t_namespace_generator, const std::string &t_namespace_name) { chaiscript::detail::threading::unique_lock l(m_use_mutex); - if (!m_namespace_generators.count(t_namespace_name)) { - // contain the namespace object memory within the m_namespace_generators map - m_namespace_generators.emplace(std::make_pair(t_namespace_name, [=, space = Namespace()]() mutable -> Namespace & { - t_namespace_generator(space); - return space; - })); - } else { + if (m_namespace_generators.count(t_namespace_name)) { throw std::runtime_error("Namespace: " + t_namespace_name + " was already registered."); } + + m_namespace_generators.emplace(std::make_pair(t_namespace_name, [=, space = Namespace()]() mutable -> Namespace & { + t_namespace_generator(space); + return space; + })); + + auto pos = t_namespace_name.rfind("::"); + while (pos != std::string::npos) { + const std::string parent = t_namespace_name.substr(0, pos); + if (!m_namespace_generators.count(parent)) { + m_namespace_generators.emplace(std::make_pair(parent, [space = Namespace()]() mutable -> Namespace & { + return space; + })); + } + pos = parent.rfind("::"); + } } + + private: + void nest_children(const std::string &t_parent_name, Namespace &t_parent) { + const std::string prefix = t_parent_name + "::"; + for (auto &[name, generator] : m_namespace_generators) { + if (name.size() > prefix.size() && name.compare(0, prefix.size(), prefix) == 0) { + const std::string remainder = name.substr(prefix.size()); + if (remainder.find("::") == std::string::npos) { + auto &child_ns = generator(); + nest_children(name, child_ns); + t_parent[remainder] = var(std::ref(child_ns)); + } + } + } + } + + public: }; } // namespace chaiscript diff --git a/include/chaiscript/language/chaiscript_eval.hpp b/include/chaiscript/language/chaiscript_eval.hpp index 4ff0d6d5..6a9f3ca6 100644 --- a/include/chaiscript/language/chaiscript_eval.hpp +++ b/include/chaiscript/language/chaiscript_eval.hpp @@ -788,40 +788,43 @@ namespace chaiscript { return false; } - Boxed_Value eval_internal(const chaiscript::detail::Dispatch_State &t_ss) const override { + static std::shared_ptr make_proxy_function( + const Def_AST_Node &t_node, const chaiscript::detail::Dispatch_State &t_ss) { std::vector t_param_names; size_t numparams = 0; dispatch::Param_Types param_types; - if ((this->children.size() > 1) && (this->children[1]->identifier == AST_Node_Type::Arg_List)) { - numparams = this->children[1]->children.size(); - t_param_names = Arg_List_AST_Node::get_arg_names(*this->children[1]); - param_types = Arg_List_AST_Node::get_arg_types(*this->children[1], t_ss); + if ((t_node.children.size() > 1) && (t_node.children[1]->identifier == AST_Node_Type::Arg_List)) { + numparams = t_node.children[1]->children.size(); + t_param_names = Arg_List_AST_Node::get_arg_names(*t_node.children[1]); + param_types = Arg_List_AST_Node::get_arg_types(*t_node.children[1], t_ss); } std::reference_wrapper engine(*t_ss); std::shared_ptr guard; - if (m_guard_node) { + if (t_node.m_guard_node) { guard = dispatch::make_dynamic_proxy_function( - [engine, guardnode = m_guard_node, t_param_names](const Function_Params &t_params) { + [engine, guardnode = t_node.m_guard_node, t_param_names](const Function_Params &t_params) { return detail::eval_function(engine, *guardnode, t_param_names, t_params); }, static_cast(numparams), - m_guard_node); + t_node.m_guard_node); } + return dispatch::make_dynamic_proxy_function( + [engine, func_node = t_node.m_body_node, t_param_names](const Function_Params &t_params) { + return detail::eval_function(engine, *func_node, t_param_names, t_params); + }, + static_cast(numparams), + t_node.m_body_node, + param_types, + guard); + } + + Boxed_Value eval_internal(const chaiscript::detail::Dispatch_State &t_ss) const override { try { - const std::string &l_function_name = this->children[0]->text; - t_ss->add(dispatch::make_dynamic_proxy_function( - [engine, func_node = m_body_node, t_param_names](const Function_Params &t_params) { - return detail::eval_function(engine, *func_node, t_param_names, t_params); - }, - static_cast(numparams), - m_body_node, - param_types, - guard), - l_function_name); + t_ss->add(make_proxy_function(*this, t_ss), this->children[0]->text); } catch (const exception::name_conflict_error &e) { throw exception::eval_error("Function redefined '" + e.name() + "'"); } @@ -887,6 +890,87 @@ namespace chaiscript { } }; + template + struct Namespace_Block_AST_Node final : AST_Node_Impl { + Namespace_Block_AST_Node(std::string t_ast_node_text, Parse_Location t_loc, std::vector> t_children) + : AST_Node_Impl(std::move(t_ast_node_text), AST_Node_Type::Namespace_Block, std::move(t_loc), std::move(t_children)) { + } + + Boxed_Value eval_internal(const chaiscript::detail::Dispatch_State &t_ss) const override { + const auto &ns_name = this->children[0]->text; + + auto ns_name_bv = const_var(ns_name); + t_ss->call_function("namespace", m_ns_loc, Function_Params{ns_name_bv}, t_ss.conversions()); + + std::vector parts; + { + std::string::size_type start = 0; + std::string::size_type pos = 0; + while ((pos = ns_name.find("::", start)) != std::string::npos) { + parts.push_back(ns_name.substr(start, pos - start)); + start = pos + 2; + } + parts.push_back(ns_name.substr(start)); + } + + Boxed_Value ns_bv = t_ss.get_object(parts[0], m_root_loc); + + for (size_t i = 1; i < parts.size(); ++i) { + auto &parent_ns = boxed_cast(ns_bv); + ns_bv = parent_ns.get_attr(parts[i]); + } + + auto &target_ns = boxed_cast(ns_bv); + + const auto process_statement = [&](const AST_Node_Impl &stmt) { + if (stmt.identifier == AST_Node_Type::Def) { + const auto &def_node = static_cast &>(stmt); + target_ns[def_node.children[0]->text] = + Boxed_Value(Def_AST_Node::make_proxy_function(def_node, t_ss)); + } else if (stmt.identifier == AST_Node_Type::Assign_Decl + || stmt.identifier == AST_Node_Type::Const_Assign_Decl) { + const auto &var_name = stmt.children[0]->text; + auto value = detail::clone_if_necessary(stmt.children[1]->eval(t_ss), m_clone_loc, t_ss); + value.reset_return_value(); + if (stmt.identifier == AST_Node_Type::Const_Assign_Decl) { + value.make_const(); + } + target_ns[var_name] = std::move(value); + } else if (stmt.identifier == AST_Node_Type::Equation + && !stmt.children.empty() + && (stmt.children[0]->identifier == AST_Node_Type::Var_Decl + || stmt.children[0]->identifier == AST_Node_Type::Const_Var_Decl)) { + const auto &var_name = stmt.children[0]->children[0]->text; + auto value = detail::clone_if_necessary(stmt.children[1]->eval(t_ss), m_clone_loc, t_ss); + value.reset_return_value(); + target_ns[var_name] = std::move(value); + } else if (stmt.identifier == AST_Node_Type::Var_Decl) { + const auto &var_name = stmt.children[0]->text; + target_ns[var_name] = Boxed_Value(); + } else { + throw exception::eval_error("Only declarations (def, var, auto, global) are allowed inside namespace blocks"); + } + }; + + const auto &body = this->children[1]; + if (body->identifier == AST_Node_Type::Block + || body->identifier == AST_Node_Type::Scopeless_Block) { + for (const auto &child : body->children) { + process_statement(*child); + } + } else { + process_statement(*body); + } + + return void_var(); + } + + private: + mutable std::atomic_uint_fast32_t m_ns_loc = {0}; + mutable std::atomic_uint_fast32_t m_root_loc = {0}; + mutable std::atomic_uint_fast32_t m_clone_loc = {0}; + }; + template struct If_AST_Node final : AST_Node_Impl { If_AST_Node(std::string t_ast_node_text, Parse_Location t_loc, std::vector> t_children) diff --git a/include/chaiscript/language/chaiscript_parser.hpp b/include/chaiscript/language/chaiscript_parser.hpp index be2a7525..91871ca2 100644 --- a/include/chaiscript/language/chaiscript_parser.hpp +++ b/include/chaiscript/language/chaiscript_parser.hpp @@ -1990,6 +1990,44 @@ namespace chaiscript { } /// Reads a class block from input + bool Namespace_Block() { + Depth_Counter dc{this}; + const auto prev_stack_top = m_match_stack.size(); + const auto prev_pos = m_position; + + if (Keyword("namespace")) { + if (Id(true)) { + std::string ns_name = m_match_stack.back()->text; + + while (Symbol("::")) { + if (!Id(true)) { + throw exception::eval_error("Incomplete namespace name after '::'", + File_Position(m_position.line, m_position.col), + *m_filename); + } + ns_name += "::" + m_match_stack.back()->text; + m_match_stack.pop_back(); + } + + m_match_stack.back() = make_node>(ns_name, prev_pos.line, prev_pos.col); + + while (Eol()) { + } + + if (Block()) { + build_match>(prev_stack_top); + return true; + } + } + + m_position = prev_pos; + while (prev_stack_top != m_match_stack.size()) { + m_match_stack.pop_back(); + } + } + return false; + } + bool Class(const bool t_class_allowed) { Depth_Counter dc{this}; bool retval = false; @@ -2379,7 +2417,7 @@ namespace chaiscript { } build_match>(prev_stack_top); - } else if (Symbol(".")) { + } else if (Symbol(".") || Symbol("::")) { has_more = true; if (!(Id(true))) { throw exception::eval_error("Incomplete dot access fun call", File_Position(m_position.line, m_position.col), *m_filename); @@ -2776,7 +2814,7 @@ namespace chaiscript { while (has_more) { const auto start = m_position; - if (Def() || Try() || If() || While() || Class(t_class_allowed) || For() || Switch()) { + if (Def() || Try() || If() || While() || Namespace_Block() || Class(t_class_allowed) || For() || Switch()) { if (!saw_eol) { throw exception::eval_error("Two function definitions missing line separator", File_Position(start.line, start.col), diff --git a/unittests/compiled_tests.cpp b/unittests/compiled_tests.cpp index 09e14d9a..680535ce 100644 --- a/unittests/compiled_tests.cpp +++ b/unittests/compiled_tests.cpp @@ -1822,6 +1822,124 @@ TEST_CASE("eval_error with AST_Node_Trace call stack compiles in C++20") { } } +TEST_CASE("Nested namespaces via register_namespace with :: separator") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + chai.register_namespace( + [](chaiscript::Namespace &si) { + si["mu_B"] = chaiscript::const_var(9.274); + }, + "constants::si"); + + chai.register_namespace( + [](chaiscript::Namespace &mm) { + mm["mu_B"] = chaiscript::const_var(0.05788); + }, + "constants::mm"); + + chai.import("constants"); + + CHECK(chai.eval("constants.si.mu_B") == Approx(9.274)); + CHECK(chai.eval("constants.mm.mu_B") == Approx(0.05788)); + + // Scope resolution via :: works the same as . for access + CHECK(chai.eval("constants::si::mu_B") == Approx(9.274)); + CHECK(chai.eval("constants::mm::mu_B") == Approx(0.05788)); +} + +TEST_CASE("Deeply nested namespaces via register_namespace") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + chai.register_namespace( + [](chaiscript::Namespace &leaf) { + leaf["val"] = chaiscript::const_var(42); + }, + "a::b::c"); + + chai.import("a"); + + CHECK(chai.eval("a.b.c.val") == 42); + CHECK(chai.eval("a::b::c::val") == 42); +} + +TEST_CASE("Block namespace declaration with ::") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + chai.eval(R"( + namespace math { + def square(x) { x * x } + } + )"); + + CHECK(chai.eval("math::square(5)") == 25); + CHECK(chai.eval("math.square(5)") == 25); +} + +TEST_CASE("Nested block namespace declaration") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + chai.eval(R"( + namespace physics::constants { + def speed_of_light() { return 299792458 } + } + )"); + + CHECK(chai.eval("physics::constants::speed_of_light()") == 299792458); +} + +TEST_CASE("Namespace block reopening") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + chai.eval(R"( + namespace ns { + def foo() { return 1 } + } + namespace ns { + def bar() { return 2 } + } + )"); + + CHECK(chai.eval("ns::foo()") == 1); + CHECK(chai.eval("ns::bar()") == 2); +} + +TEST_CASE("Namespace block with var declarations") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + chai.eval(R"( + namespace config { + var pi = 3.14 + var name = "hello" + } + )"); + + CHECK(chai.eval("config::pi") == Approx(3.14)); + CHECK(chai.eval("config::name") == "hello"); +} + +TEST_CASE("Namespace block rejects non-declaration statements") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + CHECK_THROWS_AS(chai.eval(R"( + namespace bad { + 1 + 2 + } + )"), chaiscript::exception::eval_error); + + CHECK_THROWS_AS(chai.eval(R"( + namespace bad { + print("hello") + } + )"), chaiscript::exception::eval_error); + + CHECK_THROWS_AS(chai.eval(R"( + var x = 5 + namespace bad { + x = 10 + } + )"), chaiscript::exception::eval_error); +} + TEST_CASE("C++ runtime_error thrown from registered function is catchable in ChaiScript") { chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); diff --git a/unittests/nested_namespaces.chai b/unittests/nested_namespaces.chai new file mode 100644 index 00000000..f1ca7339 --- /dev/null +++ b/unittests/nested_namespaces.chai @@ -0,0 +1,55 @@ +// Test C++-style block namespace declarations +namespace constants::si { + def mu_B() { return 1.0 } +} + +namespace constants::mm { + def mu_B() { return 2.0 } +} + +assert_equal(1.0, constants::si::mu_B()) +assert_equal(2.0, constants::mm::mu_B()) + +// Test deeper nesting with block syntax +namespace a::b::c { + def val() { return 42 } +} + +assert_equal(42, a::b::c::val()) + +// Test reopening a namespace to add more members +namespace math { + def square(x) { x * x } +} + +namespace math::trig { + def double_angle(x) { 2.0 * x } +} + +assert_equal(16, math::square(4)) +assert_equal(6.0, math::trig::double_angle(3.0)) + +// Test reopening a namespace (C++ allows this) +namespace math { + def cube(x) { x * x * x } +} + +assert_equal(27, math::cube(3)) + +// Test that :: scope resolution works the same as . for access +assert_equal(16, math.square(4)) +assert_equal(6.0, math.trig.double_angle(3.0)) + +// Test namespace with var declarations +namespace config { + var pi = 3.14159 + var name = "test" +} + +assert_equal(3.14159, config::pi) +assert_equal("test", config::name) + +// Test function-call style still works +namespace("compat") +compat.legacy = 99 +assert_equal(99, compat::legacy)