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) <noreply@anthropic.com>
This commit is contained in:
leftibot 2026-04-12 17:24:49 -06:00
parent 07d62aae99
commit 42ecde5197
3 changed files with 108 additions and 12 deletions

View File

@ -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<void(Namespace &)> &t_namespace_generator, const std::string &t_namespace_name) {
chaiscript::detail::threading::unique_lock<chaiscript::detail::threading::recursive_mutex> 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

View File

@ -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<double>("constants.si.mu_B") == Approx(9.274));
CHECK(chai.eval<double>("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<int>("a.b.c.val") == 42);
}

View File

@ -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))