Skip to content

Commit 9ff5642

Browse files
leftibotclaude
andauthored
Fix #552: Feature-request: nested namespaces (#675)
* 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> * Address review: use :: instead of . as nested namespace separator Switch from dotted names (e.g. "constants.si") to C++-style :: separator (e.g. "constants::si") for nested namespace declarations, both in the C++ API (register_namespace) and in script (namespace()). The original implementation used . because namespace members are accessed via dot notation at runtime (constants.si.mu_B), making the declaration separator match the access syntax. However, :: is more consistent with C++ namespace conventions and aligns with ChaiScript's existing use of :: for method (def Class::method) and attribute (attr Class::attr) declarations. Member access in scripts remains dot-based (constants.si.mu_B) since that is ChaiScript's member access operator. Requested by @lefticus in PR #675 review. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Address review: C++-style namespace scoping and block declarations Add :: scope resolution operator for member access (ns::func works like ns.func). Add block namespace declarations: namespace x::y { def func() { ... } } Functions and variables declared inside a namespace block are added as members of the namespace, accessible via :: or dot notation. Namespaces can be reopened to add more members, matching C++ behavior. Requested by @lefticus in PR #675 review. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Address review: extract shared make_proxy_function from Def_AST_Node Namespace_Block_AST_Node was duplicating the entire proxy function creation logic from Def_AST_Node::eval_internal. Extract a static make_proxy_function helper so both nodes share the same code path, eliminating fragile duplication that would drift if Def handling changes. Requested by @lefticus in PR #675 review. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Address review: reject non-declaration statements inside namespace blocks Only def, var, auto, and global declarations are now allowed inside namespace { } blocks. Arbitrary expressions, assignments, and function calls are rejected with an eval_error. Added compiled tests verifying that expressions, function calls, and assignments are rejected. Requested by @lefticus in PR #675 review. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Address review: remove -j parameter from unix builds Ninja handles parallelism intelligently on its own; the explicit -j flag was causing memory pressure on sanitizer builds. Windows (non-Ninja) build retains -j. Requested by @lefticus in PR #675 review. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: leftibot <leftibot@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 092ec41 commit 9ff5642

7 files changed

Lines changed: 375 additions & 41 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DMULTITHREAD_SUPPORT_ENABLED=${{ matrix.multithread }}
2727

2828
- name: Build
29-
run: cmake --build build -j
29+
run: cmake --build build
3030

3131
- name: Test
3232
run: ctest --test-dir build --output-on-failure
@@ -49,7 +49,7 @@ jobs:
4949
run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DMULTITHREAD_SUPPORT_ENABLED=${{ matrix.multithread }}
5050

5151
- name: Build
52-
run: cmake --build build -j
52+
run: cmake --build build
5353

5454
- name: Test
5555
run: ctest --test-dir build --output-on-failure
@@ -71,7 +71,7 @@ jobs:
7171
run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DENABLE_ADDRESS_SANITIZER=ON -DENABLE_UNDEFINED_SANITIZER=ON
7272

7373
- name: Build
74-
run: cmake --build build -j
74+
run: cmake --build build
7575

7676
- name: Test
7777
run: ctest --test-dir build --output-on-failure
@@ -93,7 +93,7 @@ jobs:
9393
run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DENABLE_ADDRESS_SANITIZER=ON -DENABLE_UNDEFINED_SANITIZER=ON
9494

9595
- name: Build
96-
run: cmake --build build -j
96+
run: cmake --build build
9797

9898
- name: Test
9999
run: ctest --test-dir build --output-on-failure
@@ -135,7 +135,7 @@ jobs:
135135
run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DENABLE_THREAD_SANITIZER=ON -DMULTITHREAD_SUPPORT_ENABLED=ON
136136

137137
- name: Build
138-
run: cmake --build build -j
138+
run: cmake --build build
139139

140140
- name: Test
141141
run: ctest --test-dir build --output-on-failure
@@ -157,7 +157,7 @@ jobs:
157157
run: cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DENABLE_THREAD_SANITIZER=ON -DMULTITHREAD_SUPPORT_ENABLED=ON
158158

159159
- name: Build
160-
run: cmake --build build -j
160+
run: cmake --build build
161161

162162
- name: Test
163163
run: ctest --test-dir build --output-on-failure

‎include/chaiscript/language/chaiscript_common.hpp‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,8 @@ namespace chaiscript {
106106
Constant,
107107
Compiled,
108108
Const_Var_Decl,
109-
Const_Assign_Decl
109+
Const_Assign_Decl,
110+
Namespace_Block
110111
};
111112

112113
enum class Operator_Precedence {
@@ -127,7 +128,7 @@ namespace chaiscript {
127128
namespace {
128129
/// Helper lookup to get the name of each node type
129130
constexpr const char *ast_node_type_to_string(AST_Node_Type ast_node_type) noexcept {
130-
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"};
131+
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"};
131132

132133
return ast_node_types[static_cast<int>(ast_node_type)];
133134
}

‎include/chaiscript/language/chaiscript_engine.hpp‎

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -188,10 +188,17 @@ namespace chaiscript {
188188
m_engine.add(fun([this](const Boxed_Value &t_bv, const std::string &t_name) { add_global(t_bv, t_name); }), "add_global");
189189
m_engine.add(fun([this](const Boxed_Value &t_bv, const std::string &t_name) { set_global(t_bv, t_name); }), "set_global");
190190

191-
// why this unused parameter to Namespace?
192191
m_engine.add(fun([this](const std::string &t_namespace_name) {
193-
register_namespace([](Namespace & /*space*/) noexcept {}, t_namespace_name);
194-
import(t_namespace_name);
192+
if (!m_namespace_generators.count(t_namespace_name)) {
193+
register_namespace([](Namespace & /*space*/) noexcept {}, t_namespace_name);
194+
}
195+
const auto sep_pos = t_namespace_name.find("::");
196+
const std::string root_name = (sep_pos != std::string::npos) ? t_namespace_name.substr(0, sep_pos) : t_namespace_name;
197+
if (!m_engine.get_scripting_objects().count(root_name)) {
198+
import(root_name);
199+
} else if (m_namespace_generators.count(root_name)) {
200+
nest_children(root_name, m_namespace_generators[root_name]());
201+
}
195202
}),
196203
"namespace");
197204
m_engine.add(fun([this](const std::string &t_namespace_name) { import(t_namespace_name); }), "import");
@@ -730,28 +737,59 @@ namespace chaiscript {
730737
if (m_engine.get_scripting_objects().count(t_namespace_name)) {
731738
throw std::runtime_error("Namespace: " + t_namespace_name + " was already defined");
732739
} else if (m_namespace_generators.count(t_namespace_name)) {
733-
m_engine.add_global(var(std::ref(m_namespace_generators[t_namespace_name]())), t_namespace_name);
740+
auto &ns = m_namespace_generators[t_namespace_name]();
741+
nest_children(t_namespace_name, ns);
742+
m_engine.add_global(var(std::ref(ns)), t_namespace_name);
734743
} else {
735744
throw std::runtime_error("No registered namespace: " + t_namespace_name);
736745
}
737746
}
738747

739748
/// \brief Registers a namespace generator, which delays generation of the namespace until it is imported, saving memory if it is never
740-
/// used. \param[in] t_namespace_generator Namespace generator function. \param[in] t_namespace_name Name of the Namespace function
741-
/// being registered. \throw std::runtime_error In the case that the namespace name was already registered.
749+
/// used. Supports C++-style nested names (e.g. "constants::si") for nested namespaces; parent namespaces are auto-registered if absent.
750+
/// \param[in] t_namespace_generator Namespace generator function.
751+
/// \param[in] t_namespace_name Name of the Namespace function being registered (may contain :: for nesting).
752+
/// \throw std::runtime_error In the case that the namespace name was already registered.
742753
void register_namespace(const std::function<void(Namespace &)> &t_namespace_generator, const std::string &t_namespace_name) {
743754
chaiscript::detail::threading::unique_lock<chaiscript::detail::threading::recursive_mutex> l(m_use_mutex);
744755

745-
if (!m_namespace_generators.count(t_namespace_name)) {
746-
// contain the namespace object memory within the m_namespace_generators map
747-
m_namespace_generators.emplace(std::make_pair(t_namespace_name, [=, space = Namespace()]() mutable -> Namespace & {
748-
t_namespace_generator(space);
749-
return space;
750-
}));
751-
} else {
756+
if (m_namespace_generators.count(t_namespace_name)) {
752757
throw std::runtime_error("Namespace: " + t_namespace_name + " was already registered.");
753758
}
759+
760+
m_namespace_generators.emplace(std::make_pair(t_namespace_name, [=, space = Namespace()]() mutable -> Namespace & {
761+
t_namespace_generator(space);
762+
return space;
763+
}));
764+
765+
auto pos = t_namespace_name.rfind("::");
766+
while (pos != std::string::npos) {
767+
const std::string parent = t_namespace_name.substr(0, pos);
768+
if (!m_namespace_generators.count(parent)) {
769+
m_namespace_generators.emplace(std::make_pair(parent, [space = Namespace()]() mutable -> Namespace & {
770+
return space;
771+
}));
772+
}
773+
pos = parent.rfind("::");
774+
}
754775
}
776+
777+
private:
778+
void nest_children(const std::string &t_parent_name, Namespace &t_parent) {
779+
const std::string prefix = t_parent_name + "::";
780+
for (auto &[name, generator] : m_namespace_generators) {
781+
if (name.size() > prefix.size() && name.compare(0, prefix.size(), prefix) == 0) {
782+
const std::string remainder = name.substr(prefix.size());
783+
if (remainder.find("::") == std::string::npos) {
784+
auto &child_ns = generator();
785+
nest_children(name, child_ns);
786+
t_parent[remainder] = var(std::ref(child_ns));
787+
}
788+
}
789+
}
790+
}
791+
792+
public:
755793
};
756794

757795
} // namespace chaiscript

‎include/chaiscript/language/chaiscript_eval.hpp‎

Lines changed: 102 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -788,40 +788,43 @@ namespace chaiscript {
788788
return false;
789789
}
790790

791-
Boxed_Value eval_internal(const chaiscript::detail::Dispatch_State &t_ss) const override {
791+
static std::shared_ptr<dispatch::Proxy_Function_Base> make_proxy_function(
792+
const Def_AST_Node<T> &t_node, const chaiscript::detail::Dispatch_State &t_ss) {
792793
std::vector<std::string> t_param_names;
793794
size_t numparams = 0;
794795

795796
dispatch::Param_Types param_types;
796797

797-
if ((this->children.size() > 1) && (this->children[1]->identifier == AST_Node_Type::Arg_List)) {
798-
numparams = this->children[1]->children.size();
799-
t_param_names = Arg_List_AST_Node<T>::get_arg_names(*this->children[1]);
800-
param_types = Arg_List_AST_Node<T>::get_arg_types(*this->children[1], t_ss);
798+
if ((t_node.children.size() > 1) && (t_node.children[1]->identifier == AST_Node_Type::Arg_List)) {
799+
numparams = t_node.children[1]->children.size();
800+
t_param_names = Arg_List_AST_Node<T>::get_arg_names(*t_node.children[1]);
801+
param_types = Arg_List_AST_Node<T>::get_arg_types(*t_node.children[1], t_ss);
801802
}
802803

803804
std::reference_wrapper<chaiscript::detail::Dispatch_Engine> engine(*t_ss);
804805
std::shared_ptr<dispatch::Proxy_Function_Base> guard;
805-
if (m_guard_node) {
806+
if (t_node.m_guard_node) {
806807
guard = dispatch::make_dynamic_proxy_function(
807-
[engine, guardnode = m_guard_node, t_param_names](const Function_Params &t_params) {
808+
[engine, guardnode = t_node.m_guard_node, t_param_names](const Function_Params &t_params) {
808809
return detail::eval_function(engine, *guardnode, t_param_names, t_params);
809810
},
810811
static_cast<int>(numparams),
811-
m_guard_node);
812+
t_node.m_guard_node);
812813
}
813814

815+
return dispatch::make_dynamic_proxy_function(
816+
[engine, func_node = t_node.m_body_node, t_param_names](const Function_Params &t_params) {
817+
return detail::eval_function(engine, *func_node, t_param_names, t_params);
818+
},
819+
static_cast<int>(numparams),
820+
t_node.m_body_node,
821+
param_types,
822+
guard);
823+
}
824+
825+
Boxed_Value eval_internal(const chaiscript::detail::Dispatch_State &t_ss) const override {
814826
try {
815-
const std::string &l_function_name = this->children[0]->text;
816-
t_ss->add(dispatch::make_dynamic_proxy_function(
817-
[engine, func_node = m_body_node, t_param_names](const Function_Params &t_params) {
818-
return detail::eval_function(engine, *func_node, t_param_names, t_params);
819-
},
820-
static_cast<int>(numparams),
821-
m_body_node,
822-
param_types,
823-
guard),
824-
l_function_name);
827+
t_ss->add(make_proxy_function(*this, t_ss), this->children[0]->text);
825828
} catch (const exception::name_conflict_error &e) {
826829
throw exception::eval_error("Function redefined '" + e.name() + "'");
827830
}
@@ -887,6 +890,87 @@ namespace chaiscript {
887890
}
888891
};
889892

893+
template<typename T>
894+
struct Namespace_Block_AST_Node final : AST_Node_Impl<T> {
895+
Namespace_Block_AST_Node(std::string t_ast_node_text, Parse_Location t_loc, std::vector<AST_Node_Impl_Ptr<T>> t_children)
896+
: AST_Node_Impl<T>(std::move(t_ast_node_text), AST_Node_Type::Namespace_Block, std::move(t_loc), std::move(t_children)) {
897+
}
898+
899+
Boxed_Value eval_internal(const chaiscript::detail::Dispatch_State &t_ss) const override {
900+
const auto &ns_name = this->children[0]->text;
901+
902+
auto ns_name_bv = const_var(ns_name);
903+
t_ss->call_function("namespace", m_ns_loc, Function_Params{ns_name_bv}, t_ss.conversions());
904+
905+
std::vector<std::string> parts;
906+
{
907+
std::string::size_type start = 0;
908+
std::string::size_type pos = 0;
909+
while ((pos = ns_name.find("::", start)) != std::string::npos) {
910+
parts.push_back(ns_name.substr(start, pos - start));
911+
start = pos + 2;
912+
}
913+
parts.push_back(ns_name.substr(start));
914+
}
915+
916+
Boxed_Value ns_bv = t_ss.get_object(parts[0], m_root_loc);
917+
918+
for (size_t i = 1; i < parts.size(); ++i) {
919+
auto &parent_ns = boxed_cast<dispatch::Dynamic_Object &>(ns_bv);
920+
ns_bv = parent_ns.get_attr(parts[i]);
921+
}
922+
923+
auto &target_ns = boxed_cast<dispatch::Dynamic_Object &>(ns_bv);
924+
925+
const auto process_statement = [&](const AST_Node_Impl<T> &stmt) {
926+
if (stmt.identifier == AST_Node_Type::Def) {
927+
const auto &def_node = static_cast<const Def_AST_Node<T> &>(stmt);
928+
target_ns[def_node.children[0]->text] =
929+
Boxed_Value(Def_AST_Node<T>::make_proxy_function(def_node, t_ss));
930+
} else if (stmt.identifier == AST_Node_Type::Assign_Decl
931+
|| stmt.identifier == AST_Node_Type::Const_Assign_Decl) {
932+
const auto &var_name = stmt.children[0]->text;
933+
auto value = detail::clone_if_necessary(stmt.children[1]->eval(t_ss), m_clone_loc, t_ss);
934+
value.reset_return_value();
935+
if (stmt.identifier == AST_Node_Type::Const_Assign_Decl) {
936+
value.make_const();
937+
}
938+
target_ns[var_name] = std::move(value);
939+
} else if (stmt.identifier == AST_Node_Type::Equation
940+
&& !stmt.children.empty()
941+
&& (stmt.children[0]->identifier == AST_Node_Type::Var_Decl
942+
|| stmt.children[0]->identifier == AST_Node_Type::Const_Var_Decl)) {
943+
const auto &var_name = stmt.children[0]->children[0]->text;
944+
auto value = detail::clone_if_necessary(stmt.children[1]->eval(t_ss), m_clone_loc, t_ss);
945+
value.reset_return_value();
946+
target_ns[var_name] = std::move(value);
947+
} else if (stmt.identifier == AST_Node_Type::Var_Decl) {
948+
const auto &var_name = stmt.children[0]->text;
949+
target_ns[var_name] = Boxed_Value();
950+
} else {
951+
throw exception::eval_error("Only declarations (def, var, auto, global) are allowed inside namespace blocks");
952+
}
953+
};
954+
955+
const auto &body = this->children[1];
956+
if (body->identifier == AST_Node_Type::Block
957+
|| body->identifier == AST_Node_Type::Scopeless_Block) {
958+
for (const auto &child : body->children) {
959+
process_statement(*child);
960+
}
961+
} else {
962+
process_statement(*body);
963+
}
964+
965+
return void_var();
966+
}
967+
968+
private:
969+
mutable std::atomic_uint_fast32_t m_ns_loc = {0};
970+
mutable std::atomic_uint_fast32_t m_root_loc = {0};
971+
mutable std::atomic_uint_fast32_t m_clone_loc = {0};
972+
};
973+
890974
template<typename T>
891975
struct If_AST_Node final : AST_Node_Impl<T> {
892976
If_AST_Node(std::string t_ast_node_text, Parse_Location t_loc, std::vector<AST_Node_Impl_Ptr<T>> t_children)

‎include/chaiscript/language/chaiscript_parser.hpp‎

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1990,6 +1990,44 @@ namespace chaiscript {
19901990
}
19911991

19921992
/// Reads a class block from input
1993+
bool Namespace_Block() {
1994+
Depth_Counter dc{this};
1995+
const auto prev_stack_top = m_match_stack.size();
1996+
const auto prev_pos = m_position;
1997+
1998+
if (Keyword("namespace")) {
1999+
if (Id(true)) {
2000+
std::string ns_name = m_match_stack.back()->text;
2001+
2002+
while (Symbol("::")) {
2003+
if (!Id(true)) {
2004+
throw exception::eval_error("Incomplete namespace name after '::'",
2005+
File_Position(m_position.line, m_position.col),
2006+
*m_filename);
2007+
}
2008+
ns_name += "::" + m_match_stack.back()->text;
2009+
m_match_stack.pop_back();
2010+
}
2011+
2012+
m_match_stack.back() = make_node<eval::Id_AST_Node<Tracer>>(ns_name, prev_pos.line, prev_pos.col);
2013+
2014+
while (Eol()) {
2015+
}
2016+
2017+
if (Block()) {
2018+
build_match<eval::Namespace_Block_AST_Node<Tracer>>(prev_stack_top);
2019+
return true;
2020+
}
2021+
}
2022+
2023+
m_position = prev_pos;
2024+
while (prev_stack_top != m_match_stack.size()) {
2025+
m_match_stack.pop_back();
2026+
}
2027+
}
2028+
return false;
2029+
}
2030+
19932031
bool Class(const bool t_class_allowed) {
19942032
Depth_Counter dc{this};
19952033
bool retval = false;
@@ -2379,7 +2417,7 @@ namespace chaiscript {
23792417
}
23802418

23812419
build_match<eval::Array_Call_AST_Node<Tracer>>(prev_stack_top);
2382-
} else if (Symbol(".")) {
2420+
} else if (Symbol(".") || Symbol("::")) {
23832421
has_more = true;
23842422
if (!(Id(true))) {
23852423
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 {
27762814

27772815
while (has_more) {
27782816
const auto start = m_position;
2779-
if (Def() || Try() || If() || While() || Class(t_class_allowed) || For() || Switch()) {
2817+
if (Def() || Try() || If() || While() || Namespace_Block() || Class(t_class_allowed) || For() || Switch()) {
27802818
if (!saw_eol) {
27812819
throw exception::eval_error("Two function definitions missing line separator",
27822820
File_Position(start.line, start.col),

0 commit comments

Comments
 (0)