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/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/cheatsheet.md b/cheatsheet.md index cab7504f..bb704105 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 @@ -711,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 @@ -913,5 +1119,51 @@ The print handler can also be set from within ChaiScript itself via `set_print_h set_print_handler(fun(s) { my_custom_log(s) }) ``` +## Custom File Loading + +By default, ChaiScript reads files from the filesystem when `eval_file()` or `use()` is called. +You can override this behavior on a per-instance basis by setting a custom file reader callback. +This follows the same pattern as `set_print_handler` and enables use cases such as encrypted +script files, in-memory virtual filesystems, or platform-specific file access (e.g., Android assets). + +```cpp +chaiscript::ChaiScript chai; + +// Provide scripts from an in-memory map instead of the filesystem +std::map virtual_fs = { + {"init.chai", "var x = 42"}, + {"utils.chai", "def add(a, b) { a + b }"} +}; + +chai.set_file_reader([&virtual_fs](const std::string &filename) -> std::string { + const auto it = virtual_fs.find(filename); + if (it != virtual_fs.end()) { + return it->second; + } + throw chaiscript::exception::file_not_found_error(filename); +}); + +chai.eval_file("init.chai"); // evaluates "var x = 42" +chai.use("utils.chai"); // evaluates "def add(a, b) { a + b }" +``` + +The file reader can also be set from within ChaiScript itself via `set_file_reader`: + +```chaiscript +// Override file loading from within a script +set_file_reader(fun(filename) { return my_custom_read(filename) }) +``` + +When no custom file reader is set, ChaiScript uses its built-in filesystem reader. + ## 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/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/grammar/chaiscript.ebnf b/grammar/chaiscript.ebnf new file mode 100644 index 00000000..6e13d547 --- /dev/null +++ b/grammar/chaiscript.ebnf @@ -0,0 +1,189 @@ +/* + * 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 | enum + | 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 + +/* ---- 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* "}" +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/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/include/chaiscript/language/chaiscript_common.hpp b/include/chaiscript/language/chaiscript_common.hpp index fb75a933..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")}; + 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; } @@ -106,7 +106,10 @@ namespace chaiscript { Constant, Compiled, Const_Var_Decl, - Const_Assign_Decl + Const_Assign_Decl, + Using, + Enum, + Namespace_Block }; enum class Operator_Precedence { @@ -127,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"}; + 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_engine.hpp b/include/chaiscript/language/chaiscript_engine.hpp index a88b9d70..a3c25639 100644 --- a/include/chaiscript/language/chaiscript_engine.hpp +++ b/include/chaiscript/language/chaiscript_engine.hpp @@ -193,10 +193,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"); @@ -749,28 +756,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 03189fb4..759accd9 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 @@ -108,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 @@ -788,40 +952,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 +1054,269 @@ 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) + : 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) + : 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) @@ -1305,6 +1735,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 +1749,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 +1763,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 +1785,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/include/chaiscript/language/chaiscript_parser.hpp b/include/chaiscript/language/chaiscript_parser.hpp index be2a7525..3ad3a7a2 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; @@ -2031,6 +2069,134 @@ 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; + + 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}; @@ -2379,7 +2545,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 +2942,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) || 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/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/releasenotes.md b/releasenotes.md index a622566e..f1b2b53a 100644 --- a/releasenotes.md +++ b/releasenotes.md @@ -4,6 +4,7 @@ Current Version: 6.1.1 ### Changes since 6.1.0 + * Add `set_file_reader` callback for custom file loading #116 — allows overriding how `eval_file` and `use` read files * Handle the returning of `&` to `*` types. This specifically comes up with `std::vector` and similar containers * Update CMake to use `LIBDIR` instead of `lib` #502 by @guoyunhe * Add documentation for installing ChaiScript with vcpkg #500 by @grdowns diff --git a/unittests/compiled_tests.cpp b/unittests/compiled_tests.cpp index 8d342d61..4ac537ca 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 { @@ -1821,3 +1860,308 @@ TEST_CASE("Test use with set_file_reader") { chai.use("virtual_file.chai"); CHECK(chai.eval("use_reader_val") == 99); } +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()); + + 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/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; +} 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) 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) 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) 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) 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)