From d4c5bdb3e4ef46bc433b6956970a18360e914839 Mon Sep 17 00:00:00 2001 From: leftibot Date: Mon, 13 Apr 2026 19:05:55 -0600 Subject: [PATCH 1/7] Fix #61: Comprehensive exception test suite and fix for silently swallowed exceptions (#681) When all typed catch blocks failed to match a thrown exception's type, handle_exception() would silently discard the exception and return a default-constructed Boxed_Value instead of propagating it. This meant code like `try { throw(42) } catch(string e) { }` would swallow the int exception rather than letting it propagate to an outer handler. The fix adds an explicit re-throw when no catch block matches, and restructures eval_internal() with a nested try/catch to ensure the finally block still executes before the unhandled exception propagates. Co-authored-by: leftibot Co-authored-by: Claude Opus 4.6 (1M context) --- .../chaiscript/language/chaiscript_eval.hpp | 34 +- unittests/compiled_tests.cpp | 188 ++++ unittests/exception_comprehensive.chai | 862 ++++++++++++++++++ 3 files changed, 1070 insertions(+), 14 deletions(-) create mode 100644 unittests/exception_comprehensive.chai diff --git a/include/chaiscript/language/chaiscript_eval.hpp b/include/chaiscript/language/chaiscript_eval.hpp index 03189fb4..4ff0d6d5 100644 --- a/include/chaiscript/language/chaiscript_eval.hpp +++ b/include/chaiscript/language/chaiscript_eval.hpp @@ -1305,6 +1305,7 @@ namespace chaiscript { Boxed_Value handle_exception(const chaiscript::detail::Dispatch_State &t_ss, const Boxed_Value &t_except) const { Boxed_Value retval; + bool handled = false; size_t end_point = this->children.size(); if (this->children.back()->identifier == AST_Node_Type::Finally) { @@ -1318,6 +1319,7 @@ namespace chaiscript { if (catch_block.children.size() == 1) { // No variable capture retval = catch_block.children[0]->eval(t_ss); + handled = true; break; } else if (catch_block.children.size() == 2 || catch_block.children.size() == 3) { const auto name = Arg_List_AST_Node::get_arg_name(*catch_block.children[0]); @@ -1331,17 +1333,19 @@ namespace chaiscript { if (catch_block.children.size() == 2) { // Variable capture retval = catch_block.children[1]->eval(t_ss); + handled = true; break; } } } else { - if (this->children.back()->identifier == AST_Node_Type::Finally) { - this->children.back()->children[0]->eval(t_ss); - } throw exception::eval_error("Internal error: catch block size unrecognized"); } } + if (!handled) { + throw; + } + return retval; } @@ -1351,17 +1355,19 @@ namespace chaiscript { chaiscript::eval::detail::Scope_Push_Pop spp(t_ss); try { - retval = this->children[0]->eval(t_ss); - } catch (const exception::eval_error &e) { - retval = handle_exception(t_ss, Boxed_Value(std::ref(e))); - } catch (const std::runtime_error &e) { - retval = handle_exception(t_ss, Boxed_Value(std::ref(e))); - } catch (const std::out_of_range &e) { - retval = handle_exception(t_ss, Boxed_Value(std::ref(e))); - } catch (const std::exception &e) { - retval = handle_exception(t_ss, Boxed_Value(std::ref(e))); - } catch (Boxed_Value &e) { - retval = handle_exception(t_ss, e); + try { + retval = this->children[0]->eval(t_ss); + } catch (const exception::eval_error &e) { + retval = handle_exception(t_ss, Boxed_Value(std::ref(e))); + } catch (const std::runtime_error &e) { + retval = handle_exception(t_ss, Boxed_Value(std::ref(e))); + } catch (const std::out_of_range &e) { + retval = handle_exception(t_ss, Boxed_Value(std::ref(e))); + } catch (const std::exception &e) { + retval = handle_exception(t_ss, Boxed_Value(std::ref(e))); + } catch (Boxed_Value &e) { + retval = handle_exception(t_ss, e); + } } catch (...) { if (this->children.back()->identifier == AST_Node_Type::Finally) { this->children.back()->children[0]->eval(t_ss); diff --git a/unittests/compiled_tests.cpp b/unittests/compiled_tests.cpp index 7b601b15..516c8bd5 100644 --- a/unittests/compiled_tests.cpp +++ b/unittests/compiled_tests.cpp @@ -1782,3 +1782,191 @@ TEST_CASE("eval_error with AST_Node_Trace call stack compiles in C++20") { (void)stack; } } + +TEST_CASE("C++ runtime_error thrown from registered function is catchable in ChaiScript") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + chai.add(chaiscript::fun([]() -> int { throw std::runtime_error("cpp_runtime_error"); }), "cpp_throw_runtime"); + + CHECK(chai.eval(R"( + var caught = false + try { + cpp_throw_runtime() + } + catch(e) { + caught = true + } + caught + )") == true); +} + +TEST_CASE("C++ out_of_range thrown from registered function is catchable in ChaiScript") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + chai.add(chaiscript::fun([]() -> int { throw std::out_of_range("cpp_out_of_range"); }), "cpp_throw_oor"); + + CHECK(chai.eval(R"( + var caught = false + try { + cpp_throw_oor() + } + catch(e) { + caught = true + } + caught + )") == true); +} + +TEST_CASE("C++ logic_error thrown from registered function is catchable in ChaiScript") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + chai.add(chaiscript::fun([]() -> int { throw std::logic_error("cpp_logic_error"); }), "cpp_throw_logic"); + + CHECK(chai.eval(R"( + var caught = false + try { + cpp_throw_logic() + } + catch(e) { + caught = true + } + caught + )") == true); +} + +TEST_CASE("ChaiScript throw(int) propagates as Boxed_Value to C++") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + try { + chai.eval("throw(42)"); + REQUIRE(false); + } catch (chaiscript::Boxed_Value &bv) { + CHECK(chaiscript::boxed_cast(bv) == 42); + } +} + +TEST_CASE("ChaiScript throw(string) propagates as Boxed_Value to C++") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + try { + chai.eval(R"(throw("error msg"))"); + REQUIRE(false); + } catch (chaiscript::Boxed_Value &bv) { + CHECK(chaiscript::boxed_cast(bv) == "error msg"); + } +} + +TEST_CASE("Typed catch with no match propagates exception") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + CHECK_THROWS_AS(chai.eval(R"( + try { + throw(42) + } + catch(string e) { + // wrong type, should not match + } + )"), chaiscript::Boxed_Value); +} + +TEST_CASE("Typed catch with no match still runs finally block") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + CHECK_THROWS_AS(chai.eval(R"( + var finally_ran = false + try { + throw(42) + } + catch(string e) { + // wrong type + } + finally { + finally_ran = true + } + )"), chaiscript::Boxed_Value); + + CHECK(chai.eval("finally_ran") == true); +} + +TEST_CASE("Multiple C++ exception types from registered functions") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + chai.add(chaiscript::fun([](int which) -> int { + switch (which) { + case 0: throw std::runtime_error("runtime"); + case 1: throw std::out_of_range("range"); + case 2: throw std::logic_error("logic"); + default: return which; + } + }), "cpp_multi_throw"); + + CHECK(chai.eval(R"( + var catch_count = 0 + for (var i = 0; i < 3; ++i) { + try { + cpp_multi_throw(i) + } + catch(e) { + catch_count = catch_count + 1 + } + } + catch_count + )") == 3); + + CHECK(chai.eval("cpp_multi_throw(5)") == 5); +} + +TEST_CASE("Exception from C++ binary operator is catchable in ChaiScript") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + struct ThrowingType { + int value; + }; + + chai.add(chaiscript::user_type(), "ThrowingType"); + chai.add(chaiscript::constructor(), "ThrowingType"); + chai.add(chaiscript::fun([](const ThrowingType &, const ThrowingType &) -> ThrowingType { + throw std::runtime_error("cpp operator+ threw"); + }), "+"); + + CHECK(chai.eval(R"( + var caught = false + try { + var a = ThrowingType(1) + var b = ThrowingType(2) + var c = a + b + } + catch(e) { + caught = true + } + caught + )") == true); +} + +TEST_CASE("Exception from C++ [] operator is catchable in ChaiScript") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + struct IndexableType { + int value; + }; + + chai.add(chaiscript::user_type(), "IndexableType"); + chai.add(chaiscript::constructor(), "IndexableType"); + chai.add(chaiscript::fun([](const IndexableType &, int idx) -> int { + if (idx < 0) { throw std::out_of_range("negative index"); } + return idx; + }), "[]"); + + CHECK(chai.eval("var obj = IndexableType(0); obj[5]") == 5); + + CHECK(chai.eval(R"( + var caught = false + try { + var x = obj[-1] + } + catch(e) { + caught = true + } + caught + )") == true); +} diff --git a/unittests/exception_comprehensive.chai b/unittests/exception_comprehensive.chai new file mode 100644 index 00000000..6e39ceb0 --- /dev/null +++ b/unittests/exception_comprehensive.chai @@ -0,0 +1,862 @@ +// Comprehensive exception throwing and catching tests +// Tests throw/catch from various contexts: operators, functions, lambdas, +// methods, [] operator, nested scopes, and with various value types. + +// ============================================================ +// Section 1: Throwing and catching different value types +// ============================================================ + +// Throw int +try { + throw(42) +} +catch(e) { + assert_equal(42, e) +} + +// Throw string +try { + throw("error message") +} +catch(e) { + assert_equal("error message", e) +} + +// Throw double +try { + throw(3.14) +} +catch(e) { + assert_equal(3.14, e) +} + +// Throw bool +try { + throw(true) +} +catch(e) { + assert_equal(true, e) +} + +// Throw a Vector (via named variable to preserve mutability) +auto thrown_vec = [1, 2, 3] +try { + throw(thrown_vec) +} +catch(e) { + assert_equal(3, e.size()) + assert_equal(1, e[0]) + assert_equal(2, e[1]) + assert_equal(3, e[2]) +} + +// Throw a Map (via named variable to preserve mutability) +auto thrown_map = ["key": 42] +try { + throw(thrown_map) +} +catch(e) { + assert_equal(42, e["key"]) +} + +// ============================================================ +// Section 2: Typed catch blocks +// ============================================================ + +// Typed catch matching int +auto typed_result = 0 +try { + throw(10) +} +catch(int e) { + typed_result = e + 1 +} +assert_equal(11, typed_result) + +// Typed catch matching string +typed_result = 0 +try { + throw("hello") +} +catch(string e) { + typed_result = 1 +} +assert_equal(1, typed_result) + +// Typed catch mismatch falls through to untyped +typed_result = 0 +try { + throw(42) +} +catch(string e) { + typed_result = 1 +} +catch(e) { + typed_result = 2 +} +assert_equal(2, typed_result) + +// Multiple typed catches - first match wins +typed_result = 0 +try { + throw("test") +} +catch(int e) { + typed_result = 1 +} +catch(string e) { + typed_result = 2 +} +catch(e) { + typed_result = 3 +} +assert_equal(2, typed_result) + +// Typed catch with int, multiple typed blocks, none match except untyped +typed_result = 0 +try { + throw(3.14) +} +catch(int e) { + typed_result = 1 +} +catch(string e) { + typed_result = 2 +} +catch(e) { + typed_result = 3 +} +assert_equal(3, typed_result) + +// ============================================================ +// Section 3: Catch-all (no variable) catch block +// ============================================================ + +auto catch_all_reached = false +try { + throw(99) +} +catch { + catch_all_reached = true +} +assert_true(catch_all_reached) + +// ============================================================ +// Section 4: Finally block semantics +// ============================================================ + +// Finally runs after exception +auto finally_ran = false +try { + throw(1) +} +catch(e) { + // caught +} +finally { + finally_ran = true +} +assert_true(finally_ran) + +// Finally runs without exception +finally_ran = false +try { + auto x = 1 +} +catch(e) { + // not reached +} +finally { + finally_ran = true +} +assert_true(finally_ran) + +// Finally runs even with typed catch that matches +finally_ran = false +try { + throw(42) +} +catch(int e) { + assert_equal(42, e) +} +finally { + finally_ran = true +} +assert_true(finally_ran) + +// Finally runs when typed catch does NOT match (exception not caught) +finally_ran = false +auto outer_caught = false +try { + try { + throw(42) + } + catch(string e) { + // wrong type, won't match + } + finally { + finally_ran = true + } +} +catch(e) { + outer_caught = true +} +assert_true(finally_ran) +assert_true(outer_caught) + +// ============================================================ +// Section 5: Throwing from functions +// ============================================================ + +def throwing_function() { + throw("from function") +} + +try { + throwing_function() +} +catch(e) { + assert_equal("from function", e) +} + +// Throwing from nested function calls +def inner_throw() { + throw("inner") +} + +def outer_call() { + inner_throw() +} + +try { + outer_call() +} +catch(e) { + assert_equal("inner", e) +} + +// Function that throws conditionally +def conditional_throw(should_throw) { + if (should_throw) { + throw("conditional") + } + return "no throw" +} + +assert_equal("no throw", conditional_throw(false)) + +try { + conditional_throw(true) +} +catch(e) { + assert_equal("conditional", e) +} + +// ============================================================ +// Section 6: Throwing from lambdas +// ============================================================ + +auto throwing_lambda = fun() { throw("from lambda") } + +try { + throwing_lambda() +} +catch(e) { + assert_equal("from lambda", e) +} + +// Lambda that captures and throws +auto captured_val = "captured" +auto capture_lambda = fun[captured_val]() { throw(captured_val) } + +try { + capture_lambda() +} +catch(e) { + assert_equal("captured", e) +} + +// ============================================================ +// Section 7: Throwing from binary operators +// ============================================================ + +// Define a type and an operator that throws +attr ThrowOnAdd::val +def ThrowOnAdd::ThrowOnAdd(v) { this.val = v } + +def `+`(ThrowOnAdd x, ThrowOnAdd y) { + throw("add not supported") +} + +try { + auto a = ThrowOnAdd(1) + auto b = ThrowOnAdd(2) + auto c = a + b + assert_true(false) // should not reach here +} +catch(e) { + assert_equal("add not supported", e) +} + +// Operator that throws for specific values +def `-`(ThrowOnAdd x, ThrowOnAdd y) { + if (x.val == y.val) { + throw("cannot subtract equal values") + } + return ThrowOnAdd(x.val - y.val) +} + +try { + auto a = ThrowOnAdd(5) + auto b = ThrowOnAdd(5) + auto c = a - b + assert_true(false) +} +catch(e) { + assert_equal("cannot subtract equal values", e) +} + +// Multiplication operator that throws (not pre-defined for Dynamic_Object) +def `*`(ThrowOnAdd x, ThrowOnAdd y) { + throw("multiply not supported") +} + +try { + auto a = ThrowOnAdd(1) + auto b = ThrowOnAdd(2) + auto c = a * b + assert_true(false) +} +catch(e) { + assert_equal("multiply not supported", e) +} + +// Subtraction works for non-equal values +auto sub_result = ThrowOnAdd(10) - ThrowOnAdd(3) +assert_equal(7, sub_result.val) + +// ============================================================ +// Section 8: Throwing from unary/prefix operators +// ============================================================ + +def `++`(ThrowOnAdd x) { + throw("increment not supported") +} + +try { + auto a = ThrowOnAdd(1) + ++a + assert_true(false) +} +catch(e) { + assert_equal("increment not supported", e) +} + +// ============================================================ +// Section 9: Throwing from [] operator +// ============================================================ + +attr ThrowOnIndex::data +def ThrowOnIndex::ThrowOnIndex() { this.data = [1, 2, 3] } + +def `[]`(ThrowOnIndex obj, int idx) { + if (idx < 0) { + throw("negative index not allowed") + } + return obj.data[idx] +} + +auto toi = ThrowOnIndex() +assert_equal(1, toi[0]) +assert_equal(2, toi[1]) + +try { + auto val = toi[-1] + assert_true(false) +} +catch(e) { + assert_equal("negative index not allowed", e) +} + +// ============================================================ +// Section 10: Throwing from member functions +// ============================================================ + +attr Validatable::value +def Validatable::Validatable(v) { this.value = v } + +def Validatable::validate() { + if (this.value < 0) { + throw("validation failed: negative value") + } + return true +} + +auto valid_obj = Validatable(10) +assert_true(valid_obj.validate()) + +auto invalid_obj = Validatable(-1) +try { + invalid_obj.validate() + assert_true(false) +} +catch(e) { + assert_equal("validation failed: negative value", e) +} + +// ============================================================ +// Section 11: Nested try/catch +// ============================================================ + +auto inner_caught = false +auto outer_caught_val = 0 +try { + try { + throw(1) + } + catch(e) { + inner_caught = true + throw(e + 10) + } +} +catch(e) { + outer_caught_val = e +} +assert_true(inner_caught) +assert_equal(11, outer_caught_val) + +// Deeply nested try/catch +auto depth = 0 +try { + try { + try { + throw("deep") + } + catch(e) { + depth = 1 + throw(e + "er") + } + } + catch(e) { + depth = 2 + throw(e + "est") + } +} +catch(e) { + depth = 3 + assert_equal("deeperest", e) +} +assert_equal(3, depth) + +// ============================================================ +// Section 12: Rethrow from catch block +// ============================================================ + +auto rethrow_caught = false +try { + try { + throw("rethrown") + } + catch(e) { + throw(e) + } +} +catch(e) { + rethrow_caught = true + assert_equal("rethrown", e) +} +assert_true(rethrow_caught) + +// ============================================================ +// Section 13: Exception in for loop +// ============================================================ + +auto loop_exception_val = 0 +try { + for (auto i = 0; i < 10; ++i) { + if (i == 5) { + throw(i) + } + } +} +catch(e) { + loop_exception_val = e +} +assert_equal(5, loop_exception_val) + +// ============================================================ +// Section 14: Exception in while loop +// ============================================================ + +auto while_exc_val = 0 +auto counter = 0 +try { + while (true) { + ++counter + if (counter == 3) { + throw(counter) + } + } +} +catch(e) { + while_exc_val = e +} +assert_equal(3, while_exc_val) + +// ============================================================ +// Section 15: Exception preserves value through nested calls +// ============================================================ + +def deep_throw(val) { + throw(val) +} + +def middle_call(val) { + deep_throw(val) +} + +def top_call(val) { + middle_call(val) +} + +auto nested_map = ["key": "value"] +try { + top_call(nested_map) +} +catch(e) { + assert_equal("value", e["key"]) +} + +auto nested_vec = [10, 20, 30] +try { + top_call(nested_vec) +} +catch(e) { + assert_equal(3, e.size()) + assert_equal(20, e[1]) +} + +// ============================================================ +// Section 16: Code after throw is not executed +// ============================================================ + +auto after_throw = false +try { + throw(1) + after_throw = true +} +catch(e) { + // caught +} +assert_false(after_throw) + +// ============================================================ +// Section 17: Exception value is usable in catch block arithmetic +// ============================================================ + +auto catch_computed = 0 +try { + throw(1) +} +catch(e) { + catch_computed = e + 100 +} +assert_equal(101, catch_computed) + +// ============================================================ +// Section 18: No exception means catch is skipped +// ============================================================ + +auto catch_skipped = true +try { + auto x = 42 +} +catch(e) { + catch_skipped = false +} +assert_true(catch_skipped) + +// ============================================================ +// Section 19: Exception from dynamic object method chaining +// ============================================================ + +attr Chain::val +def Chain::Chain(v) { this.val = v } + +def Chain::add(n) { + if (this.val + n > 100) { + throw("overflow: " + to_string(this.val + n)) + } + this.val = this.val + n + return this +} + +auto chain = Chain(50) +try { + chain.add(30).add(30) + assert_true(false) +} +catch(e) { + assert_equal("overflow: 110", e) +} +assert_equal(80, chain.val) + +// ============================================================ +// Section 20: Exception thrown during map construction +// ============================================================ + +def exploding_value() { + throw("boom during construction") +} + +try { + auto m = ["ok": 1, "bad": exploding_value()] + assert_true(false) +} +catch(e) { + assert_equal("boom during construction", e) +} + +// ============================================================ +// Section 21: Exception thrown during vector construction +// ============================================================ + +try { + auto v = [1, 2, exploding_value(), 4] + assert_true(false) +} +catch(e) { + assert_equal("boom during construction", e) +} + +// ============================================================ +// Section 22: Exception in if-condition +// ============================================================ + +def exploding_condition() { + throw("condition exploded") +} + +try { + if (exploding_condition()) { + assert_true(false) + } +} +catch(e) { + assert_equal("condition exploded", e) +} + +// ============================================================ +// Section 23: Multiple catch blocks - only first matching runs +// ============================================================ + +auto catch_count = 0 +try { + throw(42) +} +catch(int e) { + ++catch_count +} +catch(e) { + ++catch_count +} +assert_equal(1, catch_count) + +// ============================================================ +// Section 24: Throwing from within catch, with finally +// ============================================================ + +auto s24_finally = false +auto s24_outer = false +try { + try { + throw("original") + } + catch(e) { + throw("replaced: " + e) + } + finally { + s24_finally = true + } +} +catch(e) { + s24_outer = true + assert_equal("replaced: original", e) +} +assert_true(s24_finally) +assert_true(s24_outer) + +// ============================================================ +// Section 25: Unhandled typed catch propagates exception +// ============================================================ + +auto s25_caught = false +try { + try { + throw(3.14) + } + catch(int e) { + assert_true(false) // should not match double + } + catch(string e) { + assert_true(false) // should not match double + } +} +catch(e) { + s25_caught = true + assert_equal(3.14, e) +} +assert_true(s25_caught) + +// ============================================================ +// Section 26: Throw from range-based for +// ============================================================ + +auto s26_val = 0 +try { + for (x : [10, 20, 30, 40]) { + if (x == 30) { + throw(x) + } + } +} +catch(e) { + s26_val = e +} +assert_equal(30, s26_val) + +// ============================================================ +// Section 27: Throw from eval +// ============================================================ + +try { + eval("throw(\"from eval\")") +} +catch(e) { + assert_equal("from eval", e) +} + +// ============================================================ +// Section 28: Exception from built-in operations (out of range) +// ============================================================ + +auto s28_caught = false +try { + auto v = [1, 2, 3] + auto x = v[10] +} +catch(e) { + s28_caught = true +} +assert_true(s28_caught) + +// ============================================================ +// Section 29: Throw zero and empty string (falsy values) +// ============================================================ + +try { + throw(0) +} +catch(e) { + assert_equal(0, e) +} + +try { + throw("") +} +catch(e) { + assert_equal("", e) +} + +// ============================================================ +// Section 30: Throw from ternary-style inline_if +// ============================================================ + +def maybe_throw(do_it) { + if (do_it) { throw("inline threw") } else { "ok" } +} + +try { + maybe_throw(true) +} +catch(e) { + assert_equal("inline threw", e) +} +assert_equal("ok", maybe_throw(false)) + +// ============================================================ +// Section 31: Verify catch variable scope isolation +// ============================================================ + +auto outer_e = "untouched" +try { + throw("caught_value") +} +catch(e) { + assert_equal("caught_value", e) +} +assert_equal("untouched", outer_e) + +// ============================================================ +// Section 32: Exception from recursive function +// ============================================================ + +def recursive_throw(n) { + if (n == 0) { + throw("bottom") + } + recursive_throw(n - 1) +} + +try { + recursive_throw(5) +} +catch(e) { + assert_equal("bottom", e) +} + +// ============================================================ +// Section 33: Try/catch in a function body +// ============================================================ + +def safe_divide(a, b) { + try { + if (b == 0) { + throw("division by zero") + } + return a / b + } + catch(e) { + return e + } +} + +assert_equal(5, safe_divide(10, 2)) +assert_equal("division by zero", safe_divide(10, 0)) + +// ============================================================ +// Section 34: Throw from [] on a Map with missing key +// ============================================================ + +auto s34_caught = false +try { + auto m = ["a": 1] + auto x = m["nonexistent"] +} +catch(e) { + s34_caught = true +} +assert_true(s34_caught) + +// ============================================================ +// Section 35: Arithmetic exception (divide by zero) +// ============================================================ + +auto s35_caught = false +try { + auto x = 1 / 0 +} +catch(e) { + s35_caught = true +} +assert_true(s35_caught) From e61be765315292e58c1b27ed9f603a0bc3f50e21 Mon Sep 17 00:00:00 2001 From: leftibot Date: Mon, 13 Apr 2026 20:20:11 -0600 Subject: [PATCH 2/7] Fix #601: Allow operator functions to accept any type with an add() method (#686) The operator functions in chaiscript::bootstrap::operators (equal, not_equal, assign, etc.) hardcoded Module& as their first parameter, preventing chaiscript::utility::add_class for enum types from working when passed a ChaiScript& reference directly. Added a second template parameter ModuleType to all operator functions so they accept any type that provides an add() method, matching the already-templatized add_class functions in utility.hpp. Co-authored-by: leftibot Co-authored-by: Claude Opus 4.6 (1M context) --- include/chaiscript/dispatchkit/operators.hpp | 132 +++++++++---------- unittests/compiled_tests.cpp | 39 ++++++ 2 files changed, 105 insertions(+), 66 deletions(-) diff --git a/include/chaiscript/dispatchkit/operators.hpp b/include/chaiscript/dispatchkit/operators.hpp index 2545bfce..d32ee9d6 100644 --- a/include/chaiscript/dispatchkit/operators.hpp +++ b/include/chaiscript/dispatchkit/operators.hpp @@ -15,168 +15,168 @@ #include "register_function.hpp" namespace chaiscript::bootstrap::operators { - template - void assign(Module &m) { + template + void assign(ModuleType &m) { m.add(chaiscript::fun([](T &lhs, const T &rhs) -> T & { return lhs = rhs; }), "="); } - template - void assign_bitwise_and(Module &m) { + template + void assign_bitwise_and(ModuleType &m) { m.add(chaiscript::fun([](T &lhs, const T &rhs) -> T & { return lhs &= rhs; }), "&="); } - template - void assign_xor(Module &m) { + template + void assign_xor(ModuleType &m) { m.add(chaiscript::fun([](T &lhs, const T &rhs) -> T & { return lhs ^= rhs; }), "^="); } - template - void assign_bitwise_or(Module &m) { + template + void assign_bitwise_or(ModuleType &m) { m.add(chaiscript::fun([](T &lhs, const T &rhs) -> T & { return lhs |= rhs; }), "|="); } - template - void assign_difference(Module &m) { + template + void assign_difference(ModuleType &m) { m.add(chaiscript::fun([](T &lhs, const T &rhs) -> T & { return lhs -= rhs; }), "-="); } - template - void assign_left_shift(Module &m) { + template + void assign_left_shift(ModuleType &m) { m.add(chaiscript::fun([](T &lhs, const T &rhs) -> T & { return lhs <<= rhs; }), "<<="); } - template - void assign_product(Module &m) { + template + void assign_product(ModuleType &m) { m.add(chaiscript::fun([](T &lhs, const T &rhs) -> T & { return lhs <<= rhs; }), "*="); } - template - void assign_quotient(Module &m) { + template + void assign_quotient(ModuleType &m) { m.add(chaiscript::fun([](T &lhs, const T &rhs) -> T & { return lhs /= rhs; }), "/="); } - template - void assign_remainder(Module &m) { + template + void assign_remainder(ModuleType &m) { m.add(chaiscript::fun([](T &lhs, const T &rhs) -> T & { return lhs %= rhs; }), "%="); } - template - void assign_right_shift(Module &m) { + template + void assign_right_shift(ModuleType &m) { m.add(chaiscript::fun([](T &lhs, const T &rhs) -> T & { return lhs >>= rhs; }), ">>="); } - template - void assign_sum(Module &m) { + template + void assign_sum(ModuleType &m) { m.add(chaiscript::fun([](T &lhs, const T &rhs) -> T & { return lhs += rhs; }), "+="); } - template - void prefix_decrement(Module &m) { + template + void prefix_decrement(ModuleType &m) { m.add(chaiscript::fun([](T &lhs) -> T & { return --lhs; }), "--"); } - template - void prefix_increment(Module &m) { + template + void prefix_increment(ModuleType &m) { m.add(chaiscript::fun([](T &lhs) -> T & { return ++lhs; }), "++"); } - template - void equal(Module &m) { + template + void equal(ModuleType &m) { m.add(chaiscript::fun([](const T &lhs, const T &rhs) { return lhs == rhs; }), "=="); } - template - void greater_than(Module &m) { + template + void greater_than(ModuleType &m) { m.add(chaiscript::fun([](const T &lhs, const T &rhs) { return lhs > rhs; }), ">"); } - template - void greater_than_equal(Module &m) { + template + void greater_than_equal(ModuleType &m) { m.add(chaiscript::fun([](const T &lhs, const T &rhs) { return lhs >= rhs; }), ">="); } - template - void less_than(Module &m) { + template + void less_than(ModuleType &m) { m.add(chaiscript::fun([](const T &lhs, const T &rhs) { return lhs < rhs; }), "<"); } - template - void less_than_equal(Module &m) { + template + void less_than_equal(ModuleType &m) { m.add(chaiscript::fun([](const T &lhs, const T &rhs) { return lhs <= rhs; }), "<="); } - template - void logical_compliment(Module &m) { + template + void logical_compliment(ModuleType &m) { m.add(chaiscript::fun([](const T &lhs) { return !lhs; }), "!"); } - template - void not_equal(Module &m) { + template + void not_equal(ModuleType &m) { m.add(chaiscript::fun([](const T &lhs, const T &rhs) { return lhs != rhs; }), "!="); } - template - void addition(Module &m) { + template + void addition(ModuleType &m) { m.add(chaiscript::fun([](const T &lhs, const T &rhs) { return lhs + rhs; }), "+"); } - template - void unary_plus(Module &m) { + template + void unary_plus(ModuleType &m) { m.add(chaiscript::fun([](const T &lhs) { return +lhs; }), "+"); } - template - void subtraction(Module &m) { + template + void subtraction(ModuleType &m) { m.add(chaiscript::fun([](const T &lhs, const T &rhs) { return lhs - rhs; }), "-"); } - template - void unary_minus(Module &m) { + template + void unary_minus(ModuleType &m) { m.add(chaiscript::fun([](const T &lhs) { return -lhs; }), "-"); } - template - void bitwise_and(Module &m) { + template + void bitwise_and(ModuleType &m) { m.add(chaiscript::fun([](const T &lhs, const T &rhs) { return lhs & rhs; }), "&"); } - template - void bitwise_compliment(Module &m) { + template + void bitwise_compliment(ModuleType &m) { m.add(chaiscript::fun([](const T &lhs) { return ~lhs; }), "~"); } - template - void bitwise_xor(Module &m) { + template + void bitwise_xor(ModuleType &m) { m.add(chaiscript::fun([](const T &lhs, const T &rhs) { return lhs ^ rhs; }), "^"); } - template - void bitwise_or(Module &m) { + template + void bitwise_or(ModuleType &m) { m.add(chaiscript::fun([](const T &lhs, const T &rhs) { return lhs | rhs; }), "|"); } - template - void division(Module &m) { + template + void division(ModuleType &m) { m.add(chaiscript::fun([](const T &lhs, const T &rhs) { return lhs / rhs; }), "/"); } - template - void left_shift(Module &m) { + template + void left_shift(ModuleType &m) { m.add(chaiscript::fun([](const T &lhs, const T &rhs) { return lhs << rhs; }), "<<"); } - template - void multiplication(Module &m) { + template + void multiplication(ModuleType &m) { m.add(chaiscript::fun([](const T &lhs, const T &rhs) { return lhs * rhs; }), "*"); } - template - void remainder(Module &m) { + template + void remainder(ModuleType &m) { m.add(chaiscript::fun([](const T &lhs, const T &rhs) { return lhs % rhs; }), "%"); } - template - void right_shift(Module &m) { + template + void right_shift(ModuleType &m) { m.add(chaiscript::fun([](const T &lhs, const T &rhs) { return lhs >> rhs; }), ">>"); } } // namespace chaiscript::bootstrap::operators diff --git a/unittests/compiled_tests.cpp b/unittests/compiled_tests.cpp index 516c8bd5..09e14d9a 100644 --- a/unittests/compiled_tests.cpp +++ b/unittests/compiled_tests.cpp @@ -604,6 +604,45 @@ TEST_CASE("Utility_Test utility class wrapper for enum") { CHECK_NOTHROW(chai.eval("var o = ONE; o = TWO")); } +// Issue #601: add_class for enums should work directly with ChaiScript reference +enum class Issue601_EnumClass { Apple, Banana, Pear }; + +TEST_CASE("Issue 601: add_class enum with ChaiScript reference directly") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + // This should compile and work — previously it failed because the operator + // functions in chaiscript::bootstrap::operators hardcoded Module& as their + // first parameter instead of using a template parameter. + chaiscript::utility::add_class(chai, + "Issue601_EnumClass", + {{Issue601_EnumClass::Apple, "Apple"}, + {Issue601_EnumClass::Banana, "Banana"}, + {Issue601_EnumClass::Pear, "Pear"}}); + + CHECK(chai.eval("Apple == Apple")); + CHECK(chai.eval("Apple != Banana")); + CHECK_NOTHROW(chai.eval("var e = Apple; e = Pear")); + CHECK(chai.eval("Banana") == Issue601_EnumClass::Banana); +} + +// Also test non-scoped enum directly with ChaiScript reference +enum Issue601_PlainEnum { Issue601_Red = 0, Issue601_Green = 1, Issue601_Blue = 2 }; + +TEST_CASE("Issue 601: add_class plain enum with ChaiScript reference directly") { + chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser()); + + chaiscript::utility::add_class(chai, + "Issue601_PlainEnum", + {{Issue601_Red, "Red"}, + {Issue601_Green, "Green"}, + {Issue601_Blue, "Blue"}}); + + CHECK(chai.eval("Red == Red")); + CHECK(chai.eval("Red == 0")); + CHECK(chai.eval("Red != Green")); + CHECK_NOTHROW(chai.eval("var c = Red; c = Blue")); +} + ////// Object copy count test class Object_Copy_Count_Test { From 0fd9cab65490cb98ff99835f6b731cdfbecb32d2 Mon Sep 17 00:00:00 2001 From: leftibot Date: Mon, 13 Apr 2026 22:40:26 -0600 Subject: [PATCH 3/7] 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 4/7] 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 5/7] 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 6/7] 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) From bb069190614774917762af4daf0630a4f95429a8 Mon Sep 17 00:00:00 2001 From: leftibot Date: Wed, 15 Apr 2026 14:48:49 -0600 Subject: [PATCH 7/7] Fix #677: Add strong typedefs (#680) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix #677: Add strong typedefs via 'using Type = BaseType' syntax Strong typedefs create distinct types backed by Dynamic_Object, so 'using Meters = int' makes Meters a type that is not interchangeable with int or other typedefs of int. The constructor Meters(val) wraps the base value, and function dispatch enforces the type distinction. Co-Authored-By: Claude Opus 4.6 (1M context) * Address review: add to_underlying function for strong typedefs Registers a to_underlying() function for each strong typedef that returns the wrapped base value from the Dynamic_Object's __value attr. Requested by @lefticus in PR #680 review. Co-Authored-By: Claude Opus 4.6 (1M context) * Address review: expose strongly-typed operators for strong typedefs Register forwarding binary operators at typedef creation time via a custom Proxy_Function_Base subclass (Strong_Typedef_Binary_Op). Each operator unwraps __value from both operands, dispatches on the underlying types, and re-wraps arithmetic results in the typedef. Comparison operators return the raw bool. - Arithmetic: +, -, *, /, % → Meters + Meters -> Meters - Comparison: <, >, <=, >=, ==, != → Meters < Meters -> bool - Operators that don't exist on the base type error at call time (e.g. StrongString * StrongString -> error) - Users can extend typedefs with their own operations using to_underlying() for unwrapping Tests cover int-based arithmetic, string-based concatenation, string multiplication error, comparison ops, type safety of results, and user-defined operator extensions. Requested by @lefticus in PR #680 review. Co-Authored-By: Claude Opus 4.6 (1M context) * Address review: conditionally register operators based on underlying type support Only register strong typedef operators that actually exist for the underlying type. Previously all operators were added unconditionally, causing confusing reflection entries (e.g. * for StrongString) that would fail at runtime. Now each operator is probed via call_match against default-constructed base type values before registration. Also adds bitwise/shift operators (&, |, ^, <<, >>) for types that support them, and expands test coverage for unsupported operator rejection. Requested by @lefticus in PR #680 review. Co-Authored-By: Claude Opus 4.6 (1M context) * Address review: register all operators unconditionally and add compound assignment operators Remove conditional operator registration (op_exists_for_base_type check) since users could add underlying operators later, and the runtime check was expensive. Operators that fail on the underlying type now error at call time instead of being absent. Add compound assignment operators (*=, +=, -=, /=, %=, <<=, >>=, &=, |=, ^=) via Strong_Typedef_Compound_Assign_Op which computes the base operation and stores the result back in __value. Requested by @lefticus in PR #680 review. Co-Authored-By: Claude Opus 4.6 (1M context) * Merge upstream/develop into fix/issue-677-add-strong-typedefs Resolve merge conflicts with ChaiScript:develop. Upstream added nested namespace support (#675), grammar railroad diagrams (#673), and WASM exception support (#689). Conflicts in chaiscript_common.hpp, chaiscript_eval.hpp, and chaiscript_parser.hpp resolved by keeping both Using and Namespace_Block AST node types. Co-Authored-By: Claude Opus 4.6 (1M context) * Address review: add strong typedef documentation to cheatsheet Add a new "Strong Typedefs" section to the cheatsheet covering: - Basic usage with `using Type = BaseType` syntax - Arithmetic and comparison operator forwarding - String-based strong typedefs - Accessing the underlying value via to_underlying - Extending strong typedefs with custom operations Requested by @lefticus in PR #680 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 | 88 ++++++ grammar/chaiscript.ebnf | 1 + .../chaiscript/language/chaiscript_common.hpp | 5 +- .../chaiscript/language/chaiscript_eval.hpp | 276 ++++++++++++++++++ .../chaiscript/language/chaiscript_parser.hpp | 39 ++- unittests/strong_typedef.chai | 200 +++++++++++++ 6 files changed, 606 insertions(+), 3 deletions(-) create mode 100644 unittests/strong_typedef.chai diff --git a/cheatsheet.md b/cheatsheet.md index d2fb216b..4f5f0ff5 100644 --- a/cheatsheet.md +++ b/cheatsheet.md @@ -829,6 +829,94 @@ class My_Class { }; ``` +## Strong Typedefs + +Strong typedefs create distinct types that are not interchangeable with their underlying type +or with other typedefs of the same underlying type. They use `Dynamic_Object` internally and +automatically expose operators that the underlying type supports. + +### Basic Usage + +``` +using Meters = int +using Seconds = int + +var d = Meters(100) +var t = Seconds(10) + +// d and t are distinct types — you cannot accidentally mix them +// Meters + Seconds would require an explicit conversion +``` + +### Arithmetic and Comparison + +Operators from the underlying type are forwarded and remain strongly typed: + +``` +using Meters = int + +var a = Meters(10) +var b = Meters(20) + +var c = a + b // Meters(30) — result is still Meters +var bigger = b > a // true — comparisons return bool + +// Compound assignment operators work too +a += b // a is now Meters(30) +``` + +### String-Based Strong Typedefs + +Strong typedefs work with any type, not just numeric types: + +``` +using Name = string + +var n = Name("Alice") +var greeting = Name("Hello, ") + Name("world") // Name — string concatenation is forwarded +``` + +### Accessing the Underlying Value + +Use `to_underlying` to extract the wrapped value: + +``` +using Meters = int + +var d = Meters(42) +var raw = to_underlying(d) // 42, plain int +``` + +### Extending Strong Typedefs + +You can add custom operations to strong typedefs just like any other ChaiScript type: + +``` +using Meters = int +using Seconds = int +using MetersPerSecond = int + +def speed(Meters d, Seconds t) { + MetersPerSecond(to_underlying(d) / to_underlying(t)) +} + +var s = speed(Meters(100), Seconds(10)) // MetersPerSecond(10) +``` + +You can also overload operators between different strong typedefs: + +``` +using Meters = int +using Feet = int + +def to_feet(Meters m) { + Feet((to_underlying(m) * 328) / 100) +} + +var m = Meters(10) +var f = to_feet(m) // Feet(32) +``` + ## method_missing A function of the signature `method_missing(object, name, param1, param2, param3)` will be called if an appropriate diff --git a/grammar/chaiscript.ebnf b/grammar/chaiscript.ebnf index d95fe92b..6e13d547 100644 --- a/grammar/chaiscript.ebnf +++ b/grammar/chaiscript.ebnf @@ -68,6 +68,7 @@ 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 36383731..0e770e9e 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"), utility::hash("enum")}; + 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("using"), utility::hash("enum")}; return words.count(utility::hash(s)) == 1; } @@ -107,6 +107,7 @@ namespace chaiscript { Compiled, Const_Var_Decl, Const_Assign_Decl, + Using, Enum, Namespace_Block }; @@ -129,7 +130,7 @@ namespace chaiscript { namespace { /// Helper lookup to get the name of each node type constexpr const char *ast_node_type_to_string(AST_Node_Type ast_node_type) noexcept { - constexpr const char *const ast_node_types[] = {"Id", "Fun_Call", "Unused_Return_Fun_Call", "Arg_List", "Equation", "Var_Decl", "Assign_Decl", "Array_Call", "Dot_Access", "Lambda", "Block", "Scopeless_Block", "Def", "While", "If", "For", "Ranged_For", "Inline_Array", "Inline_Map", "Return", "File", "Prefix", "Break", "Continue", "Map_Pair", "Value_Range", "Inline_Range", "Try", "Catch", "Finally", "Method", "Attr_Decl", "Logical_And", "Logical_Or", "Reference", "Switch", "Case", "Default", "Noop", "Class", "Binary", "Arg", "Global_Decl", "Constant", "Compiled", "Const_Var_Decl", "Const_Assign_Decl", "Enum", "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", "Using", "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 970cdd3a..759accd9 100644 --- a/include/chaiscript/language/chaiscript_eval.hpp +++ b/include/chaiscript/language/chaiscript_eval.hpp @@ -109,6 +109,169 @@ namespace chaiscript { return incoming; } } + class Strong_Typedef_Binary_Op final : public dispatch::Proxy_Function_Base { + public: + Strong_Typedef_Binary_Op( + std::string t_type_name, + std::string t_op_name, + Operators::Opers t_oper, + bool t_rewrap, + chaiscript::detail::Dispatch_Engine &t_engine) + : Proxy_Function_Base( + {chaiscript::detail::Get_Type_Info::get(), + user_type(), + user_type()}, + 2) + , m_type_name(std::move(t_type_name)) + , m_op_name(std::move(t_op_name)) + , m_oper(t_oper) + , m_rewrap(t_rewrap) + , m_engine(t_engine) { + } + + bool operator==(const Proxy_Function_Base &f) const noexcept override { + if (const auto *other = dynamic_cast(&f)) { + return m_type_name == other->m_type_name && m_op_name == other->m_op_name; + } + return false; + } + + bool call_match(const Function_Params &vals, const Type_Conversions_State &t_conversions) const noexcept override { + return vals.size() == 2 + && type_matches(vals[0], t_conversions) + && type_matches(vals[1], t_conversions); + } + + protected: + Boxed_Value do_call(const Function_Params ¶ms, const Type_Conversions_State &t_conversions) const override { + if (!call_match(params, t_conversions)) { + throw chaiscript::exception::guard_error(); + } + + const auto &lhs = boxed_cast(params[0], &t_conversions); + const auto &rhs = boxed_cast(params[1], &t_conversions); + const auto lhs_val = lhs.get_attr("__value"); + const auto rhs_val = rhs.get_attr("__value"); + + Boxed_Value result; + if (m_oper != Operators::Opers::invalid + && lhs_val.get_type_info().is_arithmetic() + && rhs_val.get_type_info().is_arithmetic()) { + result = Boxed_Number::do_oper(m_oper, lhs_val, rhs_val); + } else { + std::array underlying_params{lhs_val, rhs_val}; + result = m_engine.call_function(m_op_name, m_loc, Function_Params(underlying_params), t_conversions); + } + + if (m_rewrap) { + auto bv = Boxed_Value(dispatch::Dynamic_Object(m_type_name), true); + auto *obj = static_cast(bv.get_ptr()); + obj->get_attr("__value") = result; + return bv; + } + return result; + } + + private: + bool type_matches(const Boxed_Value &bv, const Type_Conversions_State &t_conversions) const noexcept { + if (!bv.get_type_info().bare_equal(user_type())) { + return false; + } + try { + const auto &d = boxed_cast(bv, &t_conversions); + return d.get_type_name() == m_type_name; + } catch (...) { + return false; + } + } + + std::string m_type_name; + std::string m_op_name; + Operators::Opers m_oper; + bool m_rewrap; + chaiscript::detail::Dispatch_Engine &m_engine; + mutable std::atomic_uint_fast32_t m_loc{0}; + }; + + class Strong_Typedef_Compound_Assign_Op final : public dispatch::Proxy_Function_Base { + public: + Strong_Typedef_Compound_Assign_Op( + std::string t_type_name, + std::string t_op_name, + Operators::Opers t_base_oper, + std::string t_base_op_name, + chaiscript::detail::Dispatch_Engine &t_engine) + : Proxy_Function_Base( + {user_type(), + user_type(), + user_type()}, + 2) + , m_type_name(std::move(t_type_name)) + , m_op_name(std::move(t_op_name)) + , m_base_oper(t_base_oper) + , m_base_op_name(std::move(t_base_op_name)) + , m_engine(t_engine) { + } + + bool operator==(const Proxy_Function_Base &f) const noexcept override { + if (const auto *other = dynamic_cast(&f)) { + return m_type_name == other->m_type_name && m_op_name == other->m_op_name; + } + return false; + } + + bool call_match(const Function_Params &vals, const Type_Conversions_State &t_conversions) const noexcept override { + return vals.size() == 2 + && type_matches(vals[0], t_conversions) + && type_matches(vals[1], t_conversions); + } + + protected: + Boxed_Value do_call(const Function_Params ¶ms, const Type_Conversions_State &t_conversions) const override { + if (!call_match(params, t_conversions)) { + throw chaiscript::exception::guard_error(); + } + + auto &lhs = boxed_cast(params[0], &t_conversions); + const auto &rhs = boxed_cast(params[1], &t_conversions); + const auto lhs_val = lhs.get_attr("__value"); + const auto rhs_val = rhs.get_attr("__value"); + + Boxed_Value result; + if (m_base_oper != Operators::Opers::invalid + && lhs_val.get_type_info().is_arithmetic() + && rhs_val.get_type_info().is_arithmetic()) { + result = Boxed_Number::do_oper(m_base_oper, lhs_val, rhs_val); + } else { + std::array underlying_params{lhs_val, rhs_val}; + result = m_engine.call_function(m_base_op_name, m_loc, Function_Params(underlying_params), t_conversions); + } + + lhs.get_attr("__value") = result; + return params[0]; + } + + private: + bool type_matches(const Boxed_Value &bv, const Type_Conversions_State &t_conversions) const noexcept { + if (!bv.get_type_info().bare_equal(user_type())) { + return false; + } + try { + const auto &d = boxed_cast(bv, &t_conversions); + return d.get_type_name() == m_type_name; + } catch (...) { + return false; + } + } + + std::string m_type_name; + std::string m_op_name; + Operators::Opers m_base_oper; + std::string m_base_op_name; + chaiscript::detail::Dispatch_Engine &m_engine; + mutable std::atomic_uint_fast32_t m_loc{0}; + }; + } // namespace detail template @@ -891,6 +1054,119 @@ namespace chaiscript { } }; + template + struct Using_AST_Node final : AST_Node_Impl { + Using_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::Using, std::move(t_loc), std::move(t_children)) { + assert(this->children.size() == 2); + } + + Boxed_Value eval_internal(const chaiscript::detail::Dispatch_State &t_ss) const override { + const auto &new_type_name = this->children[0]->text; + const auto &base_type_name = this->children[1]->text; + + const auto base_type = t_ss->get_type(base_type_name, true); + + t_ss->add(user_type(), new_type_name); + + dispatch::Param_Types param_types(std::vector>{ + {new_type_name, Type_Info()}, + {base_type_name, base_type}}); + + auto ctor_body = dispatch::make_dynamic_proxy_function( + [](const Function_Params &t_params) -> Boxed_Value { + auto *obj = static_cast(t_params[0].get_ptr()); + obj->get_attr("__value") = t_params[1]; + return void_var(); + }, + 2, + std::shared_ptr(), + param_types); + + try { + t_ss->add(std::make_shared(new_type_name, ctor_body), new_type_name); + } catch (const exception::name_conflict_error &e) { + throw exception::eval_error("Type alias redefined '" + e.name() + "'"); + } + + dispatch::Param_Types to_underlying_param_types(std::vector>{ + {new_type_name, user_type()}}); + + auto to_underlying_body = dispatch::make_dynamic_proxy_function( + [](const Function_Params &t_params) -> Boxed_Value { + const auto *obj = static_cast(t_params[0].get_const_ptr()); + return obj->get_attr("__value"); + }, + 1, + std::shared_ptr(), + to_underlying_param_types); + + t_ss->add(to_underlying_body, "to_underlying"); + + auto &engine = *t_ss; + + struct Op_Entry { + const char *name; + Operators::Opers oper; + bool rewrap; + }; + + static constexpr Op_Entry ops[] = { + {"+", Operators::Opers::sum, true}, + {"-", Operators::Opers::difference, true}, + {"*", Operators::Opers::product, true}, + {"/", Operators::Opers::quotient, true}, + {"%", Operators::Opers::remainder, true}, + {"<<", Operators::Opers::shift_left, true}, + {">>", Operators::Opers::shift_right, true}, + {"&", Operators::Opers::bitwise_and, true}, + {"|", Operators::Opers::bitwise_or, true}, + {"^", Operators::Opers::bitwise_xor, true}, + {"<", Operators::Opers::less_than, false}, + {">", Operators::Opers::greater_than, false}, + {"<=", Operators::Opers::less_than_equal, false}, + {">=", Operators::Opers::greater_than_equal, false}, + {"==", Operators::Opers::equals, false}, + {"!=", Operators::Opers::not_equal, false}, + }; + + for (const auto &op : ops) { + t_ss->add( + chaiscript::make_shared( + new_type_name, std::string(op.name), op.oper, op.rewrap, engine), + op.name); + } + + struct Compound_Op_Entry { + const char *name; + Operators::Opers base_oper; + const char *base_op_name; + }; + + static constexpr Compound_Op_Entry compound_ops[] = { + {"+=", Operators::Opers::sum, "+"}, + {"-=", Operators::Opers::difference, "-"}, + {"*=", Operators::Opers::product, "*"}, + {"/=", Operators::Opers::quotient, "/"}, + {"%=", Operators::Opers::remainder, "%"}, + {"<<=", Operators::Opers::shift_left, "<<"}, + {">>=", Operators::Opers::shift_right, ">>"}, + {"&=", Operators::Opers::bitwise_and, "&"}, + {"|=", Operators::Opers::bitwise_or, "|"}, + {"^=", Operators::Opers::bitwise_xor, "^"}, + }; + + for (const auto &op : compound_ops) { + t_ss->add( + chaiscript::make_shared( + new_type_name, std::string(op.name), op.base_oper, std::string(op.base_op_name), engine), + op.name); + } + + return void_var(); + } + }; + 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) diff --git a/include/chaiscript/language/chaiscript_parser.hpp b/include/chaiscript/language/chaiscript_parser.hpp index 0af66d4e..3ad3a7a2 100644 --- a/include/chaiscript/language/chaiscript_parser.hpp +++ b/include/chaiscript/language/chaiscript_parser.hpp @@ -2069,6 +2069,43 @@ namespace chaiscript { return retval; } + bool Using(const bool t_class_allowed) { + Depth_Counter dc{this}; + + const auto prev_stack_top = m_match_stack.size(); + + if (Keyword("using")) { + if (!t_class_allowed) { + throw exception::eval_error("Type alias definitions only allowed at top scope", + File_Position(m_position.line, m_position.col), + *m_filename); + } + + if (!Id(true)) { + throw exception::eval_error("Missing type name in 'using' declaration", + File_Position(m_position.line, m_position.col), + *m_filename); + } + + if (!Symbol("=", true)) { + throw exception::eval_error("Missing '=' in 'using' declaration", + File_Position(m_position.line, m_position.col), + *m_filename); + } + + if (!Id(true)) { + throw exception::eval_error("Missing base type name in 'using' declaration", + File_Position(m_position.line, m_position.col), + *m_filename); + } + + build_match>(prev_stack_top); + return true; + } + + return false; + } + bool Enum(const bool t_allowed) { Depth_Counter dc{this}; bool retval = false; @@ -2905,7 +2942,7 @@ namespace chaiscript { while (has_more) { const auto start = m_position; - if (Def() || Try() || If() || While() || Namespace_Block() || Class(t_class_allowed) || Enum(t_class_allowed) || For() || Switch()) { + if (Def() || Try() || If() || While() || Namespace_Block() || Class(t_class_allowed) || Using(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/strong_typedef.chai b/unittests/strong_typedef.chai new file mode 100644 index 00000000..73aaa4a9 --- /dev/null +++ b/unittests/strong_typedef.chai @@ -0,0 +1,200 @@ +// Strong typedef: using Type = int creates a distinct type +using Meters = int + +def measure(Meters m) { + return m +} + +// Constructing a strong typedef value should work +var m = Meters(42) + +// Calling with the typedef'd value should succeed +measure(m) + +// Calling with a plain int should fail (strong typedef) +try { + measure(42) + assert_equal(true, false) +} catch(e) { + // Expected: type mismatch because int is not Meters +} + +// Multiple strong typedefs from the same base type should be distinct +using Seconds = int + +def wait(Seconds s) { + return s +} + +var s = Seconds(10) +wait(s) + +// Meters and Seconds should not be interchangeable +try { + wait(m) + assert_equal(true, false) +} catch(e) { + // Expected: Meters is not Seconds +} + +try { + measure(s) + assert_equal(true, false) +} catch(e) { + // Expected: Seconds is not Meters +} + +// to_underlying should return the base value +assert_equal(to_underlying(m), 42) +assert_equal(to_underlying(s), 10) + +// to_underlying result should be a plain value, not a strong typedef +def takes_int(int i) { + return i +} +assert_equal(takes_int(to_underlying(m)), 42) + +// --- Arithmetic operators: strongly typed --- +var m2 = Meters(8) +var m_sum = m + m2 +assert_equal(to_underlying(m_sum), 50) +measure(m_sum) + +var m_diff = m - m2 +assert_equal(to_underlying(m_diff), 34) + +var m_prod = Meters(3) * Meters(4) +assert_equal(to_underlying(m_prod), 12) + +var m_quot = Meters(20) / Meters(5) +assert_equal(to_underlying(m_quot), 4) + +var m_rem = Meters(17) % Meters(5) +assert_equal(to_underlying(m_rem), 2) + +// Arithmetic result is strongly typed, not plain int +try { + takes_int(m_sum) + assert_equal(true, false) +} catch(e) { + // Expected: m_sum is Meters, not int +} + +// --- Comparison operators --- +assert_equal(Meters(5) == Meters(5), true) +assert_equal(Meters(5) != Meters(3), true) +assert_equal(Meters(3) < Meters(5), true) +assert_equal(Meters(5) > Meters(3), true) +assert_equal(Meters(5) <= Meters(5), true) +assert_equal(Meters(3) >= Meters(3), true) +assert_equal(Meters(3) >= Meters(5), false) + +// --- Bitwise and shift operators --- +assert_equal(to_underlying(Meters(6) & Meters(3)), 2) +assert_equal(to_underlying(Meters(6) | Meters(3)), 7) +assert_equal(to_underlying(Meters(6) ^ Meters(3)), 5) +assert_equal(to_underlying(Meters(5) << Meters(2)), 20) +assert_equal(to_underlying(Meters(12) >> Meters(1)), 6) + +// Bitwise results are strongly typed +try { + takes_int(Meters(6) & Meters(3)) + assert_equal(true, false) +} catch(e) { + // Expected: result is Meters, not int +} + +// --- Strong typedef over string --- +using StrongString = string + +var ss1 = StrongString("hello") +var ss2 = StrongString(" world") +var ss_cat = ss1 + ss2 +assert_equal(to_underlying(ss_cat), "hello world") + +// StrongString + StrongString -> StrongString (strongly typed) +def takes_strong_string(StrongString ss) { + return ss +} +takes_strong_string(ss_cat) + +// Operators not supported by the underlying type error at call time +try { + var bad = ss1 * ss2 + assert_equal(true, false) +} catch(e) { + // Expected: underlying string has no * operator +} +try { + var bad = ss1 - ss2 + assert_equal(true, false) +} catch(e) { + // Expected: underlying string has no - operator +} +try { + var bad = ss1 / ss2 + assert_equal(true, false) +} catch(e) { + // Expected: underlying string has no / operator +} +try { + var bad = ss1 % ss2 + assert_equal(true, false) +} catch(e) { + // Expected: underlying string has no % operator +} + +// Comparison on StrongString +assert_equal(StrongString("abc") < StrongString("def"), true) +assert_equal(StrongString("abc") == StrongString("abc"), true) +assert_equal(StrongString("abc") != StrongString("def"), true) +assert_equal(StrongString("def") > StrongString("abc"), true) +assert_equal(StrongString("abc") <= StrongString("abc"), true) +assert_equal(StrongString("def") >= StrongString("abc"), true) + +// --- User-defined extensions on strong typedefs --- +def first_char(StrongString ss) { + return to_string(to_underlying(ss)[0]) +} +assert_equal(first_char(StrongString("hello")), "h") + +def double_meters(Meters m) { + return Meters(to_underlying(m) * 2) +} +assert_equal(to_underlying(double_meters(Meters(21))), 42) + +// User-defined operator extension +def `[]`(StrongString ss, int offset) { + return to_string(to_underlying(ss)[offset]) +} +assert_equal(StrongString("hello")[1], "e") + +// --- Compound assignment operators --- +var m3 = Meters(10) +m3 += Meters(5) +assert_equal(to_underlying(m3), 15) +measure(m3) + +m3 -= Meters(3) +assert_equal(to_underlying(m3), 12) + +m3 *= Meters(2) +assert_equal(to_underlying(m3), 24) + +m3 /= Meters(4) +assert_equal(to_underlying(m3), 6) + +m3 %= Meters(4) +assert_equal(to_underlying(m3), 2) + +// Compound assignment result is still the strong typedef +var m4 = Meters(10) +m4 += Meters(5) +assert_equal(to_underlying(m4), 15) +measure(m4) + +// Compound assignment on StrongString +var ss3 = StrongString("hello") +ss3 += StrongString(" world") +assert_equal(to_underlying(ss3), "hello world") +takes_strong_string(ss3)