From 42ecde5197783b5e0fcdeed04b5ff13991cc2708 Mon Sep 17 00:00:00 2001 From: leftibot Date: Sun, 12 Apr 2026 17:24:49 -0600 Subject: [PATCH] Fix #552: Support nested namespaces via dotted names Namespaces can now be nested using dotted name syntax, both from C++ (register_namespace(gen, "constants.si")) and from script (namespace("constants.si")). Parent namespaces are auto-registered when absent, and child namespaces are automatically nested into their parent on import. This allows clean hierarchical organization like constants.si.mu_B instead of flat names like constants_si. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../chaiscript/language/chaiscript_engine.hpp | 60 +++++++++++++++---- unittests/compiled_tests.cpp | 35 +++++++++++ unittests/nested_namespaces.chai | 25 ++++++++ 3 files changed, 108 insertions(+), 12 deletions(-) create mode 100644 unittests/nested_namespaces.chai diff --git a/include/chaiscript/language/chaiscript_engine.hpp b/include/chaiscript/language/chaiscript_engine.hpp index 4afd0449..d13906f1 100644 --- a/include/chaiscript/language/chaiscript_engine.hpp +++ b/include/chaiscript/language/chaiscript_engine.hpp @@ -188,10 +188,15 @@ 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); + const auto dot_pos = t_namespace_name.find('.'); + const std::string root_name = (dot_pos != std::string::npos) ? t_namespace_name.substr(0, dot_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 +735,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 dotted 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 dots 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/unittests/compiled_tests.cpp b/unittests/compiled_tests.cpp index 7b601b15..2f1eda3f 100644 --- a/unittests/compiled_tests.cpp +++ b/unittests/compiled_tests.cpp @@ -1782,3 +1782,38 @@ TEST_CASE("eval_error with AST_Node_Trace call stack compiles in C++20") { (void)stack; } } + +TEST_CASE("Nested namespaces via register_namespace with dotted names") { + 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)); +} + +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); +} diff --git a/unittests/nested_namespaces.chai b/unittests/nested_namespaces.chai new file mode 100644 index 00000000..8550e689 --- /dev/null +++ b/unittests/nested_namespaces.chai @@ -0,0 +1,25 @@ +// Test nested namespaces using dotted names +namespace("constants.si") +constants.si.mu_B = 1.0 + +namespace("constants.mm") +constants.mm.mu_B = 2.0 + +assert_equal(1.0, constants.si.mu_B) +assert_equal(2.0, constants.mm.mu_B) + +// Test deeper nesting +namespace("a.b.c") +a.b.c.val = 42 + +assert_equal(42, a.b.c.val) + +// Test that existing namespace can gain nested children +namespace("math") +math.square = fun(x) { x * x } + +namespace("math.trig") +math.trig.double_angle = fun(x) { 2.0 * x } + +assert_equal(16, math.square(4)) +assert_equal(6.0, math.trig.double_angle(3.0))