From 0fd9cab65490cb98ff99835f6b731cdfbecb32d2 Mon Sep 17 00:00:00 2001 From: leftibot Date: Mon, 13 Apr 2026 22:40:26 -0600 Subject: [PATCH 1/3] Fix #678: Add WASM exception support to Emscripten build (#689) ChaiScript relies heavily on C++ exceptions for error propagation, but the Emscripten build was missing the -fwasm-exceptions flag. Without it, any C++ exception in the WASM module causes an abort instead of being catchable by JavaScript. Added -fwasm-exceptions as both a compile and link option, and added a regression test verifying exception propagation through the eval wrapper functions. Co-authored-by: leftibot Co-authored-by: Claude Opus 4.6 (1M context) --- CMakeLists.txt | 4 ++ emscripten/CMakeLists.txt | 5 ++ unittests/emscripten_exception_test.cpp | 72 +++++++++++++++++++++++++ 3 files changed, 81 insertions(+) create mode 100644 unittests/emscripten_exception_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 30924841..b2e9ed1d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/emscripten/CMakeLists.txt b/emscripten/CMakeLists.txt index 7751631f..2a08d520 100644 --- a/emscripten/CMakeLists.txt +++ b/emscripten/CMakeLists.txt @@ -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 diff --git a/unittests/emscripten_exception_test.cpp b/unittests/emscripten_exception_test.cpp new file mode 100644 index 00000000..1dfe26d1 --- /dev/null +++ b/unittests/emscripten_exception_test.cpp @@ -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 +#include "../emscripten/chaiscript_eval.hpp" +#include +#include +#include +#include + +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; +} From 092ec417d2593b434e51f7cfbe0c94d1be9ed6ca Mon Sep 17 00:00:00 2001 From: leftibot Date: Tue, 14 Apr 2026 10:59:48 -0600 Subject: [PATCH 2/3] Fix #628: Grammar railroad diagram (#673) * Fix #628: Add EBNF grammar for railroad diagram generation Add a formal EBNF grammar file (grammar/chaiscript.ebnf) that can be pasted into rr (https://www.bottlecaps.de/rr/ui) to produce navigable railroad diagrams of ChaiScript's syntax. The grammar was validated against the parser implementation and covers all language constructs including class inheritance, guard conditions, raw strings, and const declarations that were missing from the original proposal. A reference section was added to the cheatsheet, and a regression test exercises every documented grammar construct. Co-Authored-By: Claude Opus 4.6 (1M context) * Address review: add grammar railroad diagram link to README Add a Grammar section to readme.md linking to the EBNF grammar file and to mingodad's railroad diagram generator for direct viewing. Requested by @lefticus in PR #673 review. Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: leftibot Co-authored-by: Claude Opus 4.6 (1M context) --- cheatsheet.md | 9 ++ grammar/chaiscript.ebnf | 177 ++++++++++++++++++++++++ readme.md | 10 ++ unittests/grammar_constructs.chai | 219 ++++++++++++++++++++++++++++++ 4 files changed, 415 insertions(+) create mode 100644 grammar/chaiscript.ebnf create mode 100644 unittests/grammar_constructs.chai diff --git a/cheatsheet.md b/cheatsheet.md index cab7504f..41b703f8 100644 --- a/cheatsheet.md +++ b/cheatsheet.md @@ -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 ``. 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**. diff --git a/grammar/chaiscript.ebnf b/grammar/chaiscript.ebnf new file mode 100644 index 00000000..8016f920 --- /dev/null +++ b/grammar/chaiscript.ebnf @@ -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 ::= [^"\] diff --git a/readme.md b/readme.md index f36788a9..6b57f329 100644 --- a/readme.md +++ b/readme.md @@ -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++ diff --git a/unittests/grammar_constructs.chai b/unittests/grammar_constructs.chai new file mode 100644 index 00000000..1a0db0c4 --- /dev/null +++ b/unittests/grammar_constructs.chai @@ -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) From 9ff56426e0b01f04bd2b9dbbdd117c6263c380f2 Mon Sep 17 00:00:00 2001 From: leftibot Date: Tue, 14 Apr 2026 11:49:00 -0600 Subject: [PATCH 3/3] 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) * 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) * 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) * 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) * 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) * 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) --------- Co-authored-by: leftibot Co-authored-by: Claude Opus 4.6 (1M context) --- .github/workflows/ci.yml | 12 +- .../chaiscript/language/chaiscript_common.hpp | 5 +- .../chaiscript/language/chaiscript_engine.hpp | 64 ++++++++-- .../chaiscript/language/chaiscript_eval.hpp | 120 +++++++++++++++--- .../chaiscript/language/chaiscript_parser.hpp | 42 +++++- unittests/compiled_tests.cpp | 118 +++++++++++++++++ unittests/nested_namespaces.chai | 55 ++++++++ 7 files changed, 375 insertions(+), 41 deletions(-) create mode 100644 unittests/nested_namespaces.chai 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)