From 0fd9cab65490cb98ff99835f6b731cdfbecb32d2 Mon Sep 17 00:00:00 2001 From: leftibot Date: Mon, 13 Apr 2026 22:40:26 -0600 Subject: [PATCH 1/4] 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/4] 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/4] 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) From 1df1b4ad9272817f34c33fb5c41ac2468f8f355c Mon Sep 17 00:00:00 2001 From: leftibot Date: Tue, 14 Apr 2026 22:08:28 -0600 Subject: [PATCH 4/4] Fix #19: Add enum support (#679) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix #19: Add strongly-typed enum support to ChaiScript Adds the ability to define enums inside ChaiScript with syntax: enum Color { Red, Green, Blue } enum Priority { Low = 10, Medium = 20, High = 30 } Enum values are strongly typed Dynamic_Objects accessed via :: syntax (e.g. Color::Red). A validating constructor from int is registered that rejects values outside the defined range. Functions declared with an enum parameter type (e.g. def fun(Color val)) correctly reject plain integers, enforcing type safety at the dispatch level. Co-Authored-By: Claude Opus 4.6 (1M context) * Address review: simplify enum implementation Remove Enum_Access AST node type — reuse Id_AST_Node for enum value lookups by combining "EnumName::ValueName" at parse time. Replace std::set with std::vector for valid value tracking (enums are small). Net removal of ~18 lines. Requested by @lefticus in PR #679 review. Co-Authored-By: Claude Opus 4.6 (1M context) * Address review: rename to_int to to_underlying, add switch tests Requested by @lefticus in PR #679 review. Co-Authored-By: Claude Opus 4.6 (1M context) * Address review: enum class syntax, constructor, configurable underlying type - Change syntax from `enum` to `enum class` (only strongly-typed enums) - Support optional underlying type: `enum class Flags : char { ... }` (defaults to `int` when omitted) - Replace `from_int` with a constructor named after the enum type, accessed as `Color::Color(1)` — the underlying type is no longer hardcoded to int - Use Boxed_Number for type-generic value storage and comparison - to_underlying now returns the actual underlying type Note: `Color(1)` syntax is not possible because ChaiScript's global objects shadow functions with the same name; `Color::Color(1)` is the C++-consistent alternative. Requested by @lefticus in PR #679 review. Co-Authored-By: Claude Opus 4.6 (1M context) * Address review: support enum struct syntax alongside enum class Requested by @lefticus in PR #679 review. Co-Authored-By: Claude Opus 4.6 (1M context) * Address review: update EBNF grammar and cheatsheet with enum documentation Add enum production rules to the EBNF grammar. Add comprehensive enum section to the cheatsheet covering syntax, explicit values, underlying type specification, construction, to_underlying, comparison, type-safe dispatch, and switch usage. Document that the underlying type must be a numeric type (string cannot be used) and list all available types. Requested by @lefticus in PR #679 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 | 118 ++++++++++++++++++ grammar/chaiscript.ebnf | 15 ++- .../chaiscript/language/chaiscript_common.hpp | 5 +- .../chaiscript/language/chaiscript_eval.hpp | 70 +++++++++++ .../chaiscript/language/chaiscript_parser.hpp | 93 +++++++++++++- unittests/enum.chai | 110 ++++++++++++++++ 6 files changed, 406 insertions(+), 5 deletions(-) create mode 100644 unittests/enum.chai diff --git a/cheatsheet.md b/cheatsheet.md index 41b703f8..d2fb216b 100644 --- a/cheatsheet.md +++ b/cheatsheet.md @@ -665,6 +665,124 @@ copy.width = 99 print(original.width) // still 10 ``` +## Enums + +ChaiScript supports strongly-typed enums using `enum class` (or equivalently `enum struct`), +matching C++ scoped-enum semantics. Values are accessed via `::` syntax and are type-safe — +a plain integer cannot be passed where an enum type is expected. + +### Basic Definition + +``` +enum class Color { Red, Green, Blue } +``` + +Values are auto-numbered starting from 0. Access them with `Color::Red`, `Color::Green`, etc. + +### Explicit Values + +``` +enum class Priority { Low = 10, Medium = 20, High = 30 } +``` + +Auto-numbering continues from the last explicit value: + +``` +enum class Status { Pending, Active = 5, Done } +// Pending = 0, Active = 5, Done = 6 +``` + +### Specifying an Underlying Type + +By default the underlying type is `int`. Use `: type` to choose a different numeric type: + +``` +enum class Flags : char { Read = 1, Write = 2, Execute = 4 } +``` + +The underlying type must be a numeric type registered in ChaiScript. `string` and other +non-numeric types cannot be used. The available underlying types are: + +| Type | Description | +|------|-------------| +| `int` | (default) signed integer | +| `unsigned_int` | unsigned integer | +| `long` | signed long | +| `unsigned_long` | unsigned long | +| `long_long` | signed long long | +| `unsigned_long_long` | unsigned long long | +| `char` | character (8-bit) | +| `wchar_t` | wide character | +| `char16_t` | 16-bit character | +| `char32_t` | 32-bit character | +| `float` | single-precision float | +| `double` | double-precision float | +| `long_double` | extended-precision float | +| `size_t` | unsigned size type | +| `int8_t` | signed 8-bit | +| `int16_t` | signed 16-bit | +| `int32_t` | signed 32-bit | +| `int64_t` | signed 64-bit | +| `uint8_t` | unsigned 8-bit | +| `uint16_t` | unsigned 16-bit | +| `uint32_t` | unsigned 32-bit | + +### `enum struct` Syntax + +`enum struct` is accepted as a synonym for `enum class`, just like in C++: + +``` +enum struct Direction { North, East, South, West } +``` + +### Constructing from a Value + +Each enum type has a constructor that accepts the underlying type. It validates that the +value matches one of the defined enumerators: + +``` +auto c = Color::Color(1) // creates Color::Green +Color::Color(52) // throws: invalid value +``` + +### `to_underlying` + +Convert an enum value back to its underlying numeric type: + +``` +Color::Red.to_underlying() // 0 +Priority::High.to_underlying() // 30 +``` + +### Comparison + +`==` and `!=` are defined for values of the same enum type: + +``` +assert_true(Color::Red == Color::Red) +assert_true(Color::Red != Color::Green) +``` + +### Type-Safe Dispatch + +Functions declared with an enum parameter type reject plain integers: + +``` +def handle(Color c) { /* ... */ } +handle(Color::Red) // ok +handle(42) // throws: dispatch error +``` + +### Using with `switch` + +``` +switch(Color::Green) { + case (Color::Red) { print("red"); break } + case (Color::Green) { print("green"); break } + case (Color::Blue) { print("blue"); break } +} +``` + ## Dynamic Objects All ChaiScript defined types and generic Dynamic_Object support dynamic parameters diff --git a/grammar/chaiscript.ebnf b/grammar/chaiscript.ebnf index 8016f920..d95fe92b 100644 --- a/grammar/chaiscript.ebnf +++ b/grammar/chaiscript.ebnf @@ -18,8 +18,8 @@ /* ---- Top-level ---- */ -statements ::= ( def | try | if | while | class | for - | switch | return | break | continue +statements ::= ( def | try | if | while | class | enum + | for | switch | return | break | continue | equation | block | eol )+ /* ---- Functions ---- */ @@ -57,6 +57,17 @@ class ::= "class" id ( ":" id )? eol* class_block class_block ::= "{" class_statements* "}" class_statements ::= def | var_decl | eol +/* ---- Enums ---- */ + +enum ::= "enum" ( "class" | "struct" ) id ( ":" underlying_type )? + "{" enum_entries? "}" + +enum_entries ::= enum_entry ( "," enum_entry )* + +enum_entry ::= id ( "=" integer )? + +underlying_type ::= id + /* ---- Blocks & flow keywords ---- */ block ::= "{" statements* "}" diff --git a/include/chaiscript/language/chaiscript_common.hpp b/include/chaiscript/language/chaiscript_common.hpp index 9fca319c..36383731 100644 --- a/include/chaiscript/language/chaiscript_common.hpp +++ b/include/chaiscript/language/chaiscript_common.hpp @@ -33,7 +33,7 @@ namespace chaiscript { template static bool is_reserved_word(const T &s) noexcept { const static std::unordered_set - words{utility::hash("def"), utility::hash("fun"), utility::hash("while"), utility::hash("for"), utility::hash("if"), utility::hash("else"), utility::hash("&&"), utility::hash("||"), utility::hash(","), utility::hash("auto"), utility::hash("return"), utility::hash("break"), utility::hash("true"), utility::hash("false"), utility::hash("class"), utility::hash("attr"), utility::hash("var"), utility::hash("global"), utility::hash("GLOBAL"), utility::hash("_"), utility::hash("__LINE__"), utility::hash("__FILE__"), utility::hash("__FUNC__"), utility::hash("__CLASS__"), utility::hash("const")}; + words{utility::hash("def"), utility::hash("fun"), utility::hash("while"), utility::hash("for"), utility::hash("if"), utility::hash("else"), utility::hash("&&"), utility::hash("||"), utility::hash(","), utility::hash("auto"), utility::hash("return"), utility::hash("break"), utility::hash("true"), utility::hash("false"), utility::hash("class"), utility::hash("attr"), utility::hash("var"), utility::hash("global"), utility::hash("GLOBAL"), utility::hash("_"), utility::hash("__LINE__"), utility::hash("__FILE__"), utility::hash("__FUNC__"), utility::hash("__CLASS__"), utility::hash("const"), utility::hash("enum")}; return words.count(utility::hash(s)) == 1; } @@ -107,6 +107,7 @@ namespace chaiscript { Compiled, Const_Var_Decl, Const_Assign_Decl, + Enum, Namespace_Block }; @@ -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", "Namespace_Block"}; + 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(ast_node_type)]; } diff --git a/include/chaiscript/language/chaiscript_eval.hpp b/include/chaiscript/language/chaiscript_eval.hpp index 6a9f3ca6..970cdd3a 100644 --- a/include/chaiscript/language/chaiscript_eval.hpp +++ b/include/chaiscript/language/chaiscript_eval.hpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -890,6 +891,75 @@ namespace chaiscript { } }; + template + struct Enum_AST_Node final : AST_Node_Impl { + Enum_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::Enum, std::move(t_loc), std::move(t_children)) { + } + + Boxed_Value eval_internal(const chaiscript::detail::Dispatch_State &t_ss) const override { + const auto &enum_name = this->children[0]->text; + const auto &underlying_type_name = this->children[1]->text; + const auto underlying_ti = t_ss->get_type(underlying_type_name); + + dispatch::Dynamic_Object container(enum_name); + std::vector valid_values; + + for (size_t i = 2; i < this->children.size(); i += 2) { + const auto &val_name = this->children[i]->text; + const auto val_bv = Boxed_Number(this->children[i + 1]->eval(t_ss)).get_as(underlying_ti).bv; + valid_values.push_back(val_bv); + + dispatch::Dynamic_Object dobj(enum_name); + dobj.get_attr("value") = val_bv; + dobj.set_explicit(true); + container[val_name] = const_var(dobj); + } + + auto shared_valid = std::make_shared>(std::move(valid_values)); + + container[enum_name] = var( + fun([shared_valid, enum_name, underlying_ti](const Boxed_Number &t_val) -> Boxed_Value { + const auto converted = t_val.get_as(underlying_ti); + for (const auto &v : *shared_valid) { + if (Boxed_Number::equals(Boxed_Number(v), converted)) { + dispatch::Dynamic_Object dobj(enum_name); + dobj.get_attr("value") = converted.bv; + dobj.set_explicit(true); + return const_var(dobj); + } + } + throw exception::eval_error("Value is not valid for enum '" + enum_name + "'"); + })); + + t_ss->add_global_const(const_var(container), enum_name); + + t_ss->add( + std::make_shared( + enum_name, + fun([](const dispatch::Dynamic_Object &lhs, const dispatch::Dynamic_Object &rhs) { + return Boxed_Number::equals(Boxed_Number(lhs.get_attr("value")), Boxed_Number(rhs.get_attr("value"))); + })), + "=="); + + t_ss->add( + std::make_shared( + enum_name, + fun([](const dispatch::Dynamic_Object &lhs, const dispatch::Dynamic_Object &rhs) { + return !Boxed_Number::equals(Boxed_Number(lhs.get_attr("value")), Boxed_Number(rhs.get_attr("value"))); + })), + "!="); + + t_ss->add( + std::make_shared( + enum_name, + fun([](const dispatch::Dynamic_Object &obj) { return obj.get_attr("value"); })), + "to_underlying"); + + return void_var(); + } + }; + 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) diff --git a/include/chaiscript/language/chaiscript_parser.hpp b/include/chaiscript/language/chaiscript_parser.hpp index 91871ca2..0af66d4e 100644 --- a/include/chaiscript/language/chaiscript_parser.hpp +++ b/include/chaiscript/language/chaiscript_parser.hpp @@ -2069,6 +2069,97 @@ namespace chaiscript { return retval; } + bool Enum(const bool t_allowed) { + Depth_Counter dc{this}; + bool retval = false; + + const auto prev_stack_top = m_match_stack.size(); + + if (Keyword("enum")) { + if (!Keyword("class") && !Keyword("struct")) { + throw exception::eval_error("Expected 'class' or 'struct' after 'enum' (only 'enum class'/'enum struct' is supported)", + File_Position(m_position.line, m_position.col), + *m_filename); + } + + if (!t_allowed) { + throw exception::eval_error("Enum definitions only allowed at top scope", + File_Position(m_position.line, m_position.col), + *m_filename); + } + + retval = true; + + if (!Id(true)) { + throw exception::eval_error("Missing enum class name in definition", File_Position(m_position.line, m_position.col), *m_filename); + } + + std::string underlying_type = "int"; + if (Char(':')) { + if (!Id(false)) { + throw exception::eval_error("Expected underlying type after ':'", + File_Position(m_position.line, m_position.col), + *m_filename); + } + underlying_type = m_match_stack.back()->text; + m_match_stack.pop_back(); + } + + m_match_stack.push_back( + make_node>(underlying_type, m_position.line, m_position.col, const_var(underlying_type))); + + if (!Char('{')) { + throw exception::eval_error("Expected '{' after enum class declaration", File_Position(m_position.line, m_position.col), *m_filename); + } + + int next_value = 0; + + while (Eol()) { + } + + if (!Char('}')) { + do { + while (Eol()) { + } + + if (!Id(true)) { + throw exception::eval_error("Expected enum value name", File_Position(m_position.line, m_position.col), *m_filename); + } + + if (Symbol("=")) { + if (!Num()) { + throw exception::eval_error("Expected integer after '=' in enum definition", + File_Position(m_position.line, m_position.col), + *m_filename); + } + next_value = static_cast(std::stoi(m_match_stack.back()->text)); + m_match_stack.pop_back(); + } + + m_match_stack.push_back( + make_node>(std::to_string(next_value), m_position.line, m_position.col, const_var(next_value))); + ++next_value; + + while (Eol()) { + } + } while (Char(',') && !Char('}')); + + while (Eol()) { + } + + if (!Char('}')) { + throw exception::eval_error("Expected '}' to close enum class definition", + File_Position(m_position.line, m_position.col), + *m_filename); + } + } + + build_match>(prev_stack_top); + } + + return retval; + } + /// Reads a while block from input bool While() { Depth_Counter dc{this}; @@ -2814,7 +2905,7 @@ namespace chaiscript { while (has_more) { const auto start = m_position; - if (Def() || Try() || If() || While() || Namespace_Block() || Class(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), diff --git a/unittests/enum.chai b/unittests/enum.chai new file mode 100644 index 00000000..1821af7a --- /dev/null +++ b/unittests/enum.chai @@ -0,0 +1,110 @@ +// Basic enum class definition (default underlying type: int) +enum class Color { Red, Green, Blue } + +// Access via :: syntax +auto r = Color::Red +auto g = Color::Green +auto b = Color::Blue + +// Equality and inequality +assert_true(Color::Red == Color::Red) +assert_false(Color::Red == Color::Green) +assert_true(Color::Red != Color::Green) +assert_false(Color::Red != Color::Red) + +// Constructor from valid underlying value +auto c = Color::Color(1) +assert_true(c == Color::Green) + +// Constructor from invalid value throws +try { + Color::Color(52) + assert_true(false) +} catch(e) { + // expected +} + +// Strong typing: function with typed parameter +def takes_color(Color val) { val } +takes_color(Color::Red) +takes_color(Color::Green) +takes_color(Color::Color(2)) + +// Cannot pass int where Color is expected +try { + takes_color(52) + assert_true(false) +} catch(e) { + // expected: dispatch error +} + +// to_underlying accessor +assert_equal(0, Color::Red.to_underlying()) +assert_equal(1, Color::Green.to_underlying()) +assert_equal(2, Color::Blue.to_underlying()) + +// Enum class with explicit values +enum class Priority { Low = 10, Medium = 20, High = 30 } +assert_equal(10, Priority::Low.to_underlying()) +assert_equal(20, Priority::Medium.to_underlying()) +assert_equal(30, Priority::High.to_underlying()) + +auto p = Priority::Priority(20) +assert_true(p == Priority::Medium) + +// Mixed auto and explicit values +enum class Status { Pending, Active = 5, Done } +assert_equal(0, Status::Pending.to_underlying()) +assert_equal(5, Status::Active.to_underlying()) +assert_equal(6, Status::Done.to_underlying()) + +// Switch on enum values +var result = "" +switch(Color::Green) { + case (Color::Red) { + result = "red" + break + } + case (Color::Green) { + result = "green" + break + } + case (Color::Blue) { + result = "blue" + break + } +} +assert_equal("green", result) + +// Switch on enum with explicit values +var prio_result = "" +switch(Priority::High) { + case (Priority::Low) { + prio_result = "low" + break + } + case (Priority::Medium) { + prio_result = "medium" + break + } + case (Priority::High) { + prio_result = "high" + break + } +} +assert_equal("high", prio_result) + +// Enum class with explicit underlying type +enum class Flags : char { Read = 1, Write = 2, Execute = 4 } +assert_equal(1, Flags::Read.to_underlying()) +assert_equal(2, Flags::Write.to_underlying()) +assert_equal(4, Flags::Execute.to_underlying()) + +auto f = Flags::Flags(2) +assert_true(f == Flags::Write) + +// enum struct syntax (equivalent to enum class, like C++) +enum struct Direction { North, East, South, West } +assert_equal(0, Direction::North.to_underlying()) +assert_equal(3, Direction::West.to_underlying()) +assert_true(Direction::East != Direction::South)