Address review: resolve merge conflicts with upstream namespace support

Merge upstream/develop (including namespace support from #552, WASM
support from #678, and grammar diagrams from #628) and adapt enum
implementation for compatibility with the :: dot-access semantics.

Enum values are now stored as attributes on a container Dynamic_Object,
aligning with how namespaces store members. The from_int() factory
method replaces direct constructor syntax since a name cannot be both
a global object (for :: access) and a callable constructor.

Requested by @lefticus in PR #679 review.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
leftibot 2026-04-14 12:53:12 -06:00
commit 2c807d8c4a
15 changed files with 889 additions and 70 deletions

View File

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

View File

@ -440,6 +440,10 @@ if(BUILD_TESTING)
target_link_libraries(emscripten_eval_test ${LIBS})
add_test(NAME Emscripten_Eval_Test COMMAND emscripten_eval_test)
add_executable(emscripten_exception_test unittests/emscripten_exception_test.cpp)
target_link_libraries(emscripten_exception_test ${LIBS})
add_test(NAME Emscripten_Exception_Test COMMAND emscripten_exception_test)
add_executable(threading_config_test unittests/threading_config_test.cpp)
target_link_libraries(threading_config_test ${LIBS})
add_test(NAME Threading_Config_Test COMMAND threading_config_test)

View File

@ -915,3 +915,12 @@ set_print_handler(fun(s) { my_custom_log(s) })
## Extras
ChaiScript itself does not provide a link to the math functions defined in `<cmath>`. You can either add them yourself, or use the [ChaiScript_Extras](https://github.com/ChaiScript/ChaiScript_Extras) helper library. (Which also provides some additional string functions.)
## Grammar Railroad Diagrams
A formal EBNF grammar for ChaiScript is available in [`grammar/chaiscript.ebnf`](grammar/chaiscript.ebnf). You can visualize it as navigable railroad diagrams by pasting its contents into one of these tools:
* [rr — Railroad Diagram Generator (IPv6)](https://www.bottlecaps.de/rr/ui)
* [rr — Railroad Diagram Generator (IPv4)](https://rr.red-dove.com/ui)
Open either link, switch to the **Edit Grammar** tab, paste the file contents, then click **View Diagram**.

View File

@ -17,9 +17,14 @@ add_definitions(-DCHAISCRIPT_NO_THREADS -DCHAISCRIPT_NO_DYNLOAD)
add_executable(chaiscript chaiscript_em.cpp)
target_include_directories(chaiscript PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include)
# Enable WASM exception handling ChaiScript relies on C++ exceptions for
# error propagation; without this flag exceptions cause an abort in WASM.
target_compile_options(chaiscript PRIVATE -fwasm-exceptions)
# Emscripten link flags: enable embind, allow memory growth, export as ES module-compatible
target_link_options(chaiscript PRIVATE
--bind
-fwasm-exceptions
-sALLOW_MEMORY_GROWTH=1
-sEXPORT_ES6=0
-sMODULARIZE=0

177
grammar/chaiscript.ebnf Normal file
View File

@ -0,0 +1,177 @@
/*
* ChaiScript Grammar EBNF for Railroad Diagram Generation
*
* View as navigable railroad diagrams at:
* https://www.bottlecaps.de/rr/ui (IPv6)
* https://rr.red-dove.com/ui (IPv4)
*
* Copy and paste this file into the 'Edit Grammar' tab, then
* click 'View Diagram'.
*
* This grammar uses the notation accepted by
* https://github.com/GuntherRademacher/rr :
* - "::=" as rule separator
* - no semicolon at end of rule
* - "?" "+" "*" for repetition
* - C comments
*/
/* ---- Top-level ---- */
statements ::= ( def | try | if | while | class | for
| switch | return | break | continue
| equation | block | eol )+
/* ---- Functions ---- */
def ::= "def" id ( "::" id )? "(" decl_arg_list ")" eol*
( ":" guard )? eol* block
lambda ::= "fun" ( "[" id_arg_list "]" )? "(" decl_arg_list ")" eol* block
guard ::= operator
/* ---- Exception handling ---- */
try ::= "try" eol* block catch* finally?
catch ::= "catch" ( "(" arg ")" )? eol* block
finally ::= "finally" eol* block
/* ---- Control flow ---- */
if ::= "if" "(" equation ( eol equation )? ")" eol* block
( "else" ( if | eol* block ) )*
while ::= "while" "(" operator ")" eol* block
for ::= "for" "(" ( for_guards | equation ":" equation ) ")" eol* block
for_guards ::= equation eol equation eol equation
switch ::= "switch" "(" operator ")" eol* "{" ( case | default )+ "}"
case ::= "case" "(" operator ")" eol* block
default ::= "default" eol* block
/* ---- Classes ---- */
class ::= "class" id ( ":" id )? eol* class_block
class_block ::= "{" class_statements* "}"
class_statements ::= def | var_decl | eol
/* ---- Blocks & flow keywords ---- */
block ::= "{" statements* "}"
return ::= "return" operator?
break ::= "break"
continue ::= "continue"
/* ---- Line termination ---- */
eol ::= "\n" | "\r\n" | ";"
/* ---- Equations & operators ---- */
equation ::= operator ( ( "=" | ":=" | "+=" | "-=" | "*=" | "/="
| "%=" | "<<=" | ">>=" | "&=" | "^=" | "|=" )
equation )?
operator ::= prefix
| value
| operator binary_operator operator
| operator "?" operator ":" operator
prefix ::= ( "++" | "--" | "-" | "+" | "!" | "~" ) operator
binary_operator ::= "||" | "&&"
| "|" | "^" | "&"
| "==" | "!="
| "<" | "<=" | ">" | ">="
| "<<" | ">>"
| "+" | "-"
| "*" | "/" | "%"
/* ---- Values & access ---- */
value ::= var_decl | dot_fun_array | prefix
dot_fun_array ::= ( lambda | num | quoted_string
| single_quoted_string | raw_string
| paren_expression | inline_container
| id )
( fun_call | array_call | dot_access )*
fun_call ::= "(" arg_list ")"
array_call ::= "[" operator "]"
dot_access ::= "." id
/* ---- Variable declarations ---- */
var_decl ::= ( "auto" | "var" | "const" ) ( reference | id )
| "global" ( reference | id )
| "attr" id ( "::" id )?
reference ::= "&" id
/* ---- Parenthesised & inline containers ---- */
paren_expression ::= "(" operator ")"
inline_container ::= "[" container_arg_list "]"
container_arg_list ::= value_range
| map_pair ( "," map_pair )*
| operator ( "," operator )*
value_range ::= operator ".." operator
map_pair ::= operator ":" operator
/* ---- String literals ---- */
quoted_string ::= '"' ( char | escape | interpolation )* '"'
single_quoted_string ::= "'" ( char | escape ) "'"
raw_string ::= 'R"' delimiter? "(" char* ")" delimiter? '"'
delimiter ::= [a-zA-Z0-9_]+
interpolation ::= "${" equation "}"
/* ---- Escape sequences ---- */
escape ::= "\" ( "'" | '"' | "?" | "\" | "a" | "b"
| "f" | "n" | "r" | "t" | "v" | "$"
| "0"
| "x" hex_digit+
| "u" hex_digit hex_digit hex_digit hex_digit
| "U" hex_digit hex_digit hex_digit hex_digit
hex_digit hex_digit hex_digit hex_digit
| octal_digit+ )
/* ---- Argument lists ---- */
id_arg_list ::= id ( "," id )*
decl_arg_list ::= ( arg ( "," arg )* )?
arg_list ::= ( equation ( "," equation )* )?
arg ::= id id?
/* ---- Identifiers ---- */
id ::= ( [a-zA-Z_] [a-zA-Z0-9_]* )
| ( "`" [^`]+ "`" )
| "true" | "false"
| "Infinity" | "NaN"
| "_"
| "__LINE__" | "__FILE__" | "__FUNC__" | "__CLASS__"
/* ---- Numeric literals ---- */
num ::= hex | binary | float | integer
hex ::= "0" ( "x" | "X" ) [0-9a-fA-F]+ int_suffix*
binary ::= "0" ( "b" | "B" ) [01]+ int_suffix*
float ::= [0-9]+ "." [0-9]+ ( ( "e" | "E" ) ( "+" | "-" )? [0-9]+ )? float_suffix?
integer ::= [0-9]+ int_suffix*
int_suffix ::= "l" | "L" | "ll" | "LL" | "u" | "U"
float_suffix ::= "l" | "L" | "f" | "F"
/* ---- Character classes ---- */
octal_digit ::= [0-7]
hex_digit ::= [0-9a-fA-F]
char ::= [^"\]

View File

@ -107,7 +107,8 @@ namespace chaiscript {
Compiled,
Const_Var_Decl,
Const_Assign_Decl,
Enum
Enum,
Namespace_Block
};
enum class Operator_Precedence {
@ -128,7 +129,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", "Enum"};
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", "Enum", "Namespace_Block"};
return ast_node_types[static_cast<int>(ast_node_type)];
}

View File

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

@ -789,40 +789,43 @@ namespace chaiscript {
return false;
}
Boxed_Value eval_internal(const chaiscript::detail::Dispatch_State &t_ss) const override {
static std::shared_ptr<dispatch::Proxy_Function_Base> make_proxy_function(
const Def_AST_Node<T> &t_node, const chaiscript::detail::Dispatch_State &t_ss) {
std::vector<std::string> 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<T>::get_arg_names(*this->children[1]);
param_types = Arg_List_AST_Node<T>::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<T>::get_arg_names(*t_node.children[1]);
param_types = Arg_List_AST_Node<T>::get_arg_types(*t_node.children[1], t_ss);
}
std::reference_wrapper<chaiscript::detail::Dispatch_Engine> engine(*t_ss);
std::shared_ptr<dispatch::Proxy_Function_Base> 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<int>(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<int>(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<int>(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() + "'");
}
@ -897,6 +900,7 @@ namespace chaiscript {
Boxed_Value eval_internal(const chaiscript::detail::Dispatch_State &t_ss) const override {
const auto &enum_name = this->children[0]->text;
dispatch::Dynamic_Object container(enum_name);
std::vector<int> valid_values;
for (size_t i = 1; i < this->children.size(); i += 2) {
@ -907,22 +911,23 @@ namespace chaiscript {
dispatch::Dynamic_Object dobj(enum_name);
dobj.get_attr("value") = Boxed_Value(val_int);
dobj.set_explicit(true);
t_ss->add_global_const(const_var(dobj), enum_name + "::" + val_name);
container[val_name] = const_var(dobj);
}
auto shared_valid = std::make_shared<const std::vector<int>>(std::move(valid_values));
t_ss->add(
std::make_shared<dispatch::detail::Dynamic_Object_Constructor>(
enum_name,
fun([shared_valid, enum_name](dispatch::Dynamic_Object &t_obj, int t_val) {
if (std::find(shared_valid->begin(), shared_valid->end(), t_val) == shared_valid->end()) {
throw exception::eval_error("Value " + std::to_string(t_val) + " is not valid for enum '" + enum_name + "'");
}
t_obj.get_attr("value") = Boxed_Value(t_val);
t_obj.set_explicit(true);
})),
enum_name);
container["from_int"] = var(
fun([shared_valid, enum_name](int t_val) -> Boxed_Value {
if (std::find(shared_valid->begin(), shared_valid->end(), t_val) == shared_valid->end()) {
throw exception::eval_error("Value " + std::to_string(t_val) + " is not valid for enum '" + enum_name + "'");
}
dispatch::Dynamic_Object dobj(enum_name);
dobj.get_attr("value") = Boxed_Value(t_val);
dobj.set_explicit(true);
return const_var(dobj);
}));
t_ss->add_global_const(const_var(container), enum_name);
t_ss->add(
std::make_shared<dispatch::detail::Dynamic_Object_Function>(
@ -950,6 +955,87 @@ namespace chaiscript {
}
};
template<typename T>
struct Namespace_Block_AST_Node final : AST_Node_Impl<T> {
Namespace_Block_AST_Node(std::string t_ast_node_text, Parse_Location t_loc, std::vector<AST_Node_Impl_Ptr<T>> t_children)
: AST_Node_Impl<T>(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<std::string> 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<dispatch::Dynamic_Object &>(ns_bv);
ns_bv = parent_ns.get_attr(parts[i]);
}
auto &target_ns = boxed_cast<dispatch::Dynamic_Object &>(ns_bv);
const auto process_statement = [&](const AST_Node_Impl<T> &stmt) {
if (stmt.identifier == AST_Node_Type::Def) {
const auto &def_node = static_cast<const Def_AST_Node<T> &>(stmt);
target_ns[def_node.children[0]->text] =
Boxed_Value(Def_AST_Node<T>::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<typename T>
struct If_AST_Node final : AST_Node_Impl<T> {
If_AST_Node(std::string t_ast_node_text, Parse_Location t_loc, std::vector<AST_Node_Impl_Ptr<T>> t_children)

View File

@ -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<eval::Id_AST_Node<Tracer>>(ns_name, prev_pos.line, prev_pos.col);
while (Eol()) {
}
if (Block()) {
build_match<eval::Namespace_Block_AST_Node<Tracer>>(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;
@ -2450,20 +2488,7 @@ namespace chaiscript {
}
build_match<eval::Array_Call_AST_Node<Tracer>>(prev_stack_top);
} else if (!m_match_stack.empty() && m_match_stack.back()->identifier == AST_Node_Type::Id && Symbol("::")) {
has_more = true;
const auto enum_name = m_match_stack.back()->text;
const auto start_loc = m_match_stack.back()->location;
m_match_stack.pop_back();
if (!Id(true)) {
throw exception::eval_error("Expected identifier after '::'", File_Position(m_position.line, m_position.col), *m_filename);
}
const auto val_name = m_match_stack.back()->text;
m_match_stack.pop_back();
m_match_stack.push_back(make_node<eval::Id_AST_Node<Tracer>>(enum_name + "::" + val_name, start_loc.start.line, start_loc.start.column));
} 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);
@ -2860,7 +2885,7 @@ namespace chaiscript {
while (has_more) {
const auto start = m_position;
if (Def() || Try() || If() || While() || Class(t_class_allowed) || Enum(t_class_allowed) || For() || Switch()) {
if (Def() || Try() || If() || While() || Namespace_Block() || Class(t_class_allowed) || Enum(t_class_allowed) || For() || Switch()) {
if (!saw_eol) {
throw exception::eval_error("Two function definitions missing line separator",
File_Position(start.line, start.col),

View File

@ -91,6 +91,16 @@ the doxygen documentation in the build folder or see the website
http://www.chaiscript.com.
Grammar
=======
A formal EBNF grammar for ChaiScript is available in
[grammar/chaiscript.ebnf](grammar/chaiscript.ebnf). To view it as a railroad
diagram, paste the grammar into
[mingodad's railroad diagram generator](https://mingodad.github.io/plgh/json2ebnf.html)
or [bottlecaps.de/rr](https://www.bottlecaps.de/rr/ui).
The shortest complete example possible follows:
```C++

View File

@ -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<double>("constants.si.mu_B") == Approx(9.274));
CHECK(chai.eval<double>("constants.mm.mu_B") == Approx(0.05788));
// Scope resolution via :: works the same as . for access
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);
CHECK(chai.eval<int>("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<int>("math::square(5)") == 25);
CHECK(chai.eval<int>("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<int>("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<int>("ns::foo()") == 1);
CHECK(chai.eval<int>("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<double>("config::pi") == Approx(3.14));
CHECK(chai.eval<std::string>("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());

View File

@ -0,0 +1,72 @@
// Test that validates exception propagation through the Emscripten eval wrapper.
// Without proper exception support flags (-fwasm-exceptions) in the WASM build,
// C++ exceptions would cause an abort instead of being catchable.
#ifndef CHAISCRIPT_NO_THREADS
#define CHAISCRIPT_NO_THREADS
#endif
#ifndef CHAISCRIPT_NO_DYNLOAD
#define CHAISCRIPT_NO_DYNLOAD
#endif
#include <chaiscript/chaiscript.hpp>
#include "../emscripten/chaiscript_eval.hpp"
#include <cassert>
#include <iostream>
#include <stdexcept>
#include <string>
int main() {
// Verify that ChaiScript evaluation errors propagate as exceptions
// through the eval wrapper functions. In WASM builds without exception
// support, these would abort instead of throwing.
bool caught = false;
// Test 1: eval with undefined variable should throw
caught = false;
try {
chaiscript_eval("this_variable_does_not_exist");
} catch (const chaiscript::exception::eval_error &) {
caught = true;
}
assert(caught && "eval of undefined variable must throw eval_error");
// Test 2: evalString with a type mismatch should throw
caught = false;
try {
chaiscript_eval_string("1 + 2");
} catch (const chaiscript::exception::bad_boxed_cast &) {
caught = true;
}
assert(caught && "evalString with non-string result must throw bad_boxed_cast");
// Test 3: evalInt with invalid syntax should throw
caught = false;
try {
chaiscript_eval_int("def {}");
} catch (const chaiscript::exception::eval_error &) {
caught = true;
}
assert(caught && "evalInt with syntax error must throw eval_error");
// Test 4: eval with throw statement should propagate exception
caught = false;
try {
chaiscript_eval("throw(\"user exception\")");
} catch (const chaiscript::Boxed_Value &) {
caught = true;
} catch (...) {
caught = true;
}
assert(caught && "ChaiScript throw must propagate as an exception");
// Test 5: Verify normal operation still works after caught exceptions
chaiscript_eval("var post_exception_test = 100");
const int result = chaiscript_eval_int("post_exception_test");
assert(result == 100 && "normal eval must work after caught exceptions");
std::cout << "All emscripten exception tests passed.\n";
return 0;
}

View File

@ -13,12 +13,12 @@ assert_true(Color::Red != Color::Green)
assert_false(Color::Red != Color::Red)
// Construction from valid int
auto c = Color(1)
auto c = Color::from_int(1)
assert_true(c == Color::Green)
// Construction from invalid int throws
try {
Color(52)
Color::from_int(52)
assert_true(false)
} catch(e) {
// expected
@ -28,7 +28,7 @@ try {
def takes_color(Color val) { val }
takes_color(Color::Red)
takes_color(Color::Green)
takes_color(Color(2))
takes_color(Color::from_int(2))
// Cannot pass int where Color is expected
try {
@ -49,7 +49,7 @@ assert_equal(10, Priority::Low.to_underlying())
assert_equal(20, Priority::Medium.to_underlying())
assert_equal(30, Priority::High.to_underlying())
auto p = Priority(20)
auto p = Priority::from_int(20)
assert_true(p == Priority::Medium)
// Mixed auto and explicit values

View File

@ -0,0 +1,219 @@
// Regression test: exercises grammar constructs documented in grammar/chaiscript.ebnf
// --- Variable declarations ---
var a = 1
auto b = 2
global c = 3
const d = 42
assert_equal(1, a)
assert_equal(2, b)
assert_equal(3, c)
assert_equal(42, d)
// --- Reference variables ---
var orig = 10
var &ref = orig
ref = 20
assert_equal(20, orig)
// --- Numeric literals ---
assert_equal(255, 0xFF)
assert_equal(255, 0xff)
assert_equal(5, 0b101)
assert_equal(42, 42)
assert_equal(3.14, 3.14)
// --- String interpolation ---
var name = "world"
assert_equal("hello world", "hello ${name}")
// --- Escape sequences ---
assert_equal("\n", "\n")
assert_equal("\t", "\t")
// --- Single-quoted char ---
assert_equal('A', 'A')
// --- Operators and precedence ---
assert_equal(7, 1 + 2 * 3)
assert_equal(true, 5 > 3 && 2 < 4)
assert_equal(true, false || true)
assert_equal(6, 3 << 1)
assert_equal(1, 3 >> 1)
assert_equal(5, 7 & 5)
assert_equal(7, 5 | 3)
assert_equal(6, 5 ^ 3)
assert_equal(-1, ~0)
// --- Ternary operator ---
assert_equal("yes", true ? "yes" : "no")
assert_equal("no", false ? "yes" : "no")
// --- Prefix operators ---
var x = 5
++x
assert_equal(6, x)
--x
assert_equal(5, x)
assert_equal(true, !false)
// --- Assignment operators ---
var v = 10
v += 5; assert_equal(15, v)
v -= 3; assert_equal(12, v)
v *= 2; assert_equal(24, v)
v /= 4; assert_equal(6, v)
v %= 4; assert_equal(2, v)
v <<= 2; assert_equal(8, v)
v >>= 1; assert_equal(4, v)
v |= 3; assert_equal(7, v)
v &= 5; assert_equal(5, v)
v ^= 3; assert_equal(6, v)
// --- Lambda ---
var add = fun(a, b) { a + b }
assert_equal(5, add(2, 3))
// --- Lambda with capture ---
var captured = 100
var get_captured = fun[captured]() { captured }
assert_equal(100, get_captured())
// --- Function definition ---
def multiply(a, b) { a * b }
assert_equal(12, multiply(3, 4))
// --- Guard condition on function ---
def abs_val(x) : x >= 0 { x }
def abs_val(x) : x < 0 { -x }
assert_equal(5, abs_val(5))
assert_equal(5, abs_val(-5))
// --- Class definition ---
class Animal
{
attr sound
def Animal(s) { this.sound = s }
def speak() { this.sound }
}
var dog = Animal("woof")
assert_equal("woof", dog.speak())
// --- Class with inheritance ---
class Puppy : Animal
{
attr name
def Puppy(n, s) { this.name = n; this.sound = s }
def greet() { to_string(this.name) + " says " + to_string(this.speak()) }
}
var p = Puppy("Rex", "yip")
assert_equal("Rex says yip", p.greet())
// --- Control flow: if/else ---
var result = ""
if (true) { result = "yes" } else { result = "no" }
assert_equal("yes", result)
// --- Control flow: while ---
var counter = 0
while (counter < 3) { ++counter }
assert_equal(3, counter)
// --- Control flow: for ---
var sum = 0
for (var i = 0; i < 5; ++i) { sum += i }
assert_equal(10, sum)
// --- Control flow: ranged for ---
var items = [10, 20, 30]
var total = 0
for (item : items) { total += item }
assert_equal(60, total)
// --- Switch/case ---
def classify(n) {
var label = ""
switch (n) {
case (1) { label = "one"; break }
case (2) { label = "two"; break }
default { label = "other" }
}
return label
}
assert_equal("one", classify(1))
assert_equal("two", classify(2))
assert_equal("other", classify(99))
// --- Try/catch/finally ---
var caught = false
var finalized = false
try {
throw("oops")
} catch (e) {
caught = true
} finally {
finalized = true
}
assert_true(caught)
assert_true(finalized)
// --- Inline containers ---
var vec = [1, 2, 3]
assert_equal(3, vec.size())
var m = ["a": 1, "b": 2]
assert_equal(1, m["a"])
var r = [1, 2, 3, 4, 5]
assert_equal(5, r.size())
// --- Dot access chaining ---
assert_equal(3, [1, 2, 3].size())
// --- Array access ---
var arr = [10, 20, 30]
assert_equal(20, arr[1])
// --- Backtick identifier ---
var `my var` = 42
assert_equal(42, `my var`)
// --- Special identifiers ---
assert_equal(true, true)
assert_equal(false, false)
// --- Nested block ---
var block_result = 0
{ block_result = 42 }
assert_equal(42, block_result)
// --- Break and continue ---
var break_sum = 0
for (var i = 0; i < 10; ++i) {
if (i == 5) { break }
break_sum += i
}
assert_equal(10, break_sum)
var cont_sum = 0
for (var i = 0; i < 5; ++i) {
if (i == 2) { continue }
cont_sum += i
}
assert_equal(8, cont_sum)
// --- Return from function ---
def early_return(n) {
if (n > 0) { return "positive" }
return "non-positive"
}
assert_equal("positive", early_return(1))
assert_equal("non-positive", early_return(-1))
// --- Colon assignment ---
var ca = 0
ca := 99
assert_equal(99, ca)

View File

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